class EdgeItem(GraphItem): _qt_pen_styles = { 'dashed': Qt.DashLine, 'dotted': Qt.DotLine, 'solid': Qt.SolidLine, } def __init__(self, highlight_level, spline, label_center, label, from_node, to_node, parent=None, penwidth=1, edge_color=None, style='solid'): super(EdgeItem, self).__init__(highlight_level, parent) self.from_node = from_node self.from_node.add_outgoing_edge(self) self.to_node = to_node self.to_node.add_incoming_edge(self) self._default_edge_color = self._COLOR_BLACK if edge_color is not None: self._default_edge_color = edge_color self._default_text_color = self._COLOR_BLACK self._default_color = self._COLOR_BLACK self._text_brush = QBrush(self._default_color) self._shape_brush = QBrush(self._default_color) if style in ['dashed', 'dotted']: self._shape_brush = QBrush(Qt.transparent) self._label_pen = QPen() self._label_pen.setColor(self._default_text_color) self._label_pen.setJoinStyle(Qt.RoundJoin) self._edge_pen = QPen(self._label_pen) self._edge_pen.setWidth(penwidth) self._edge_pen.setColor(self._default_edge_color) self._edge_pen.setStyle(self._qt_pen_styles.get(style, Qt.SolidLine)) self._sibling_edges = set() self._label = None if label is not None: self._label = QGraphicsSimpleTextItem(label) self._label.setFont(GraphItem._LABEL_FONT) label_rect = self._label.boundingRect() label_rect.moveCenter(label_center) self._label.setPos(label_rect.x(), label_rect.y()) self._label.hoverEnterEvent = self._handle_hoverEnterEvent self._label.hoverLeaveEvent = self._handle_hoverLeaveEvent self._label.setAcceptHoverEvents(True) # spline specification according to # http://www.graphviz.org/doc/info/attrs.html#k:splineType coordinates = spline.split(' ') # extract optional end_point end_point = None if (coordinates[0].startswith('e,')): parts = coordinates.pop(0)[2:].split(',') end_point = QPointF(float(parts[0]), -float(parts[1])) # extract optional start_point if (coordinates[0].startswith('s,')): parts = coordinates.pop(0).split(',') # first point parts = coordinates.pop(0).split(',') point = QPointF(float(parts[0]), -float(parts[1])) path = QPainterPath(point) while len(coordinates) > 2: # extract triple of points for a cubic spline parts = coordinates.pop(0).split(',') point1 = QPointF(float(parts[0]), -float(parts[1])) parts = coordinates.pop(0).split(',') point2 = QPointF(float(parts[0]), -float(parts[1])) parts = coordinates.pop(0).split(',') point3 = QPointF(float(parts[0]), -float(parts[1])) path.cubicTo(point1, point2, point3) self._arrow = None if end_point is not None: # draw arrow self._arrow = QGraphicsPolygonItem() polygon = QPolygonF() polygon.append(point3) offset = QPointF(end_point - point3) corner1 = QPointF(-offset.y(), offset.x()) * 0.35 corner2 = QPointF(offset.y(), -offset.x()) * 0.35 polygon.append(point3 + corner1) polygon.append(end_point) polygon.append(point3 + corner2) self._arrow.setPolygon(polygon) self._arrow.hoverEnterEvent = self._handle_hoverEnterEvent self._arrow.hoverLeaveEvent = self._handle_hoverLeaveEvent self._arrow.setAcceptHoverEvents(True) self._path = QGraphicsPathItem(parent) self._path.setPath(path) self.addToGroup(self._path) self.set_node_color() self.set_label_color() def add_to_scene(self, scene): scene.addItem(self) if self._label is not None: scene.addItem(self._label) if self._arrow is not None: scene.addItem(self._arrow) def setToolTip(self, tool_tip): super(EdgeItem, self).setToolTip(tool_tip) if self._label is not None: self._label.setToolTip(tool_tip) if self._arrow is not None: self._arrow.setToolTip(tool_tip) def add_sibling_edge(self, edge): self._sibling_edges.add(edge) def set_node_color(self, color=None): if color is None: self._label_pen.setColor(self._default_text_color) self._text_brush.setColor(self._default_color) if self._shape_brush.isOpaque(): self._shape_brush.setColor(self._default_edge_color) self._edge_pen.setColor(self._default_edge_color) else: self._label_pen.setColor(color) self._text_brush.setColor(color) if self._shape_brush.isOpaque(): self._shape_brush.setColor(color) self._edge_pen.setColor(color) self._path.setPen(self._edge_pen) if self._arrow is not None: self._arrow.setBrush(self._shape_brush) self._arrow.setPen(self._edge_pen) def set_label_color(self, color=None): if color is None: self._label_pen.setColor(self._default_text_color) else: self._label_pen.setColor(color) if self._label is not None: self._label.setBrush(self._text_brush) self._label.setPen(self._label_pen) def _handle_hoverEnterEvent(self, event): # hovered edge item in red self.set_node_color(self._COLOR_RED) self.set_label_color(self._COLOR_RED) if self._highlight_level > 1: if self.from_node != self.to_node: # from-node in blue self.from_node.set_node_color(self._COLOR_BLUE) # to-node in green self.to_node.set_node_color(self._COLOR_GREEN) else: # from-node/in-node in teal self.from_node.set_node_color(self._COLOR_TEAL) self.to_node.set_node_color(self._COLOR_TEAL) if self._highlight_level > 2: # sibling edges in orange for sibling_edge in self._sibling_edges: sibling_edge.set_node_color(self._COLOR_ORANGE) def _handle_hoverLeaveEvent(self, event): self.set_node_color() self.set_label_color() if self._highlight_level > 1: self.from_node.set_node_color() self.to_node.set_node_color() if self._highlight_level > 2: for sibling_edge in self._sibling_edges: sibling_edge.set_node_color()
class Node(QGraphicsItem): logger = logging.getLogger('ViewNode') i = 0 NODE_MIN_WIDTH = 100 NODE_MAX_WIDTH = 150 NODE_HEIGHT = 50 NODE_COLOR = (152, 193, 217) # LIGHT BLUE TACTIC_COLOR = (255, 51, 51) # RED STRATEGY_COLOR = (77, 255, 77) # GREEN ROLE_COLOR = (166, 77, 255) # PURPLE KEEPER_COLOR = (255, 255, 26) # YELLOW OTHER_SUBTREE_COLOR = (147, 147, 147) # GREY DECORATOR_COLOR = (51, 51, 255) # DARK BLUE COMPOSITE_COLOR = (255, 153, 0) # ORANGE OTHER_NODE_TYPES_COLOR = (255, 102, 153) # PINK DEFAULT_SIMULATOR_COLOR = Qt.white def __init__(self, x: float, y: float, scene: QGraphicsScene, model_node: ModelNode, title: str = None, parent: QGraphicsItem = None, node_types: NodeTypes = None): """ The constructor for a UI node :param x: x position for the center of the node :param y: y position for the center of the node :param title: title of the node displayed in the ui :param parent: parent of this graphics item """ if title: self.title = title else: # give node a unique title self.title = "node {}".format(Node.i) self.id = model_node.id self.x = x self.y = y Node.i += 1 self.scene = scene self.model_node = model_node self.children = [] self.edges = [] # store node positional data when detaching from parent self.expand_data = None # add node name label centered in the eclipse, elide if title is too long self.node_text = QGraphicsSimpleTextItem() metrics = QFontMetrics(self.node_text.font()) elided_title = metrics.elidedText(self.title, Qt.ElideRight, self.NODE_MAX_WIDTH) self.node_text.setText(elided_title) self.node_text.setAcceptedMouseButtons(Qt.NoButton) self.node_text.setAcceptHoverEvents(False) self.text_width = self.node_text.boundingRect().width() self.text_height = self.node_text.boundingRect().height() self.node_text.setX(x - (self.text_width / 2)) # call super function now we know the node size super(Node, self).__init__(parent) self.node_text.setParentItem(self) # indicates if node is being dragged self.dragging = False self.setCursor(Qt.PointingHandCursor) self.setAcceptHoverEvents(True) # give the node a default color self.brush = QBrush(QColor(*self.NODE_COLOR)) self.simulator_brush = QBrush(self.DEFAULT_SIMULATOR_COLOR) # give node another color if node_types: # check for node types and color them types = node_types.get_node_type_by_name(model_node.title) if len(types) > 0: category, node_type = types[0] if category == 'decorators': self.brush.setColor(QColor(*self.DECORATOR_COLOR)) elif category == 'composites': self.brush.setColor(QColor(*self.COMPOSITE_COLOR)) else: self.brush.setColor(QColor(*self.OTHER_NODE_TYPES_COLOR)) # check for a strategy, role, tactic or keeper if 'name' in model_node.attributes.keys() or 'role' in model_node.attributes.keys(): if model_node.title == 'Tactic': self.brush.setColor(QColor(*self.TACTIC_COLOR)) elif model_node.title == 'Strategy': self.brush.setColor(QColor(*self.STRATEGY_COLOR)) elif model_node.title == 'Keeper': self.brush.setColor(QColor(*self.KEEPER_COLOR)) elif model_node.title == 'Role': self.brush.setColor(QColor(*self.ROLE_COLOR)) else: self.brush.setColor(QColor(*self.OTHER_SUBTREE_COLOR)) self.info_display = [] self.max_width = 0 self.total_height = 0 self.bottom_collapse_expand_button = None self.top_collapse_expand_button = None self._rect = None self.initiate_view() def initiate_view(self, propagate=False): """ Initiates all the children for the current view :param propagate: Propagate initiate view signal to children """ for rect in self.info_display: rect.setParentItem(None) if self.top_collapse_expand_button and self.bottom_collapse_expand_button: self.top_collapse_expand_button.setParentItem(None) self.bottom_collapse_expand_button.setParentItem(None) self.info_display = [] self.max_width = self.text_width + 10 self.total_height = self.NODE_HEIGHT if self.scene.info_mode: model_node = self.scene.gui.tree.nodes[self.id] self.create_info_display(self.x, self.y, model_node.attributes) if self.max_width > self.NODE_MIN_WIDTH - 10: self._rect = QRect(self.x - self.max_width / 2, self.y - self.total_height / 2, self.max_width, self.total_height) else: self._rect = QRect(self.x - self.NODE_MIN_WIDTH / 2, self.y - self.total_height / 2, self.NODE_MIN_WIDTH, self.total_height) # set node size based on children self.node_text.setY(self.y - self.total_height / 2 + self.NODE_HEIGHT / 2 - self.text_height / 2) self.create_expand_collapse_buttons() self.scene.update() if propagate: for c in self.children: c.initiate_view(True) for e in self.edges: e.change_position() def create_expand_collapse_buttons(self): """ Creates the expand/collapse buttons of the node """ # create the bottom collapse/expand button for this node if self.bottom_collapse_expand_button: bottom_collapsed = self.bottom_collapse_expand_button.isCollapsed else: bottom_collapsed = False self.bottom_collapse_expand_button = CollapseExpandButton(self) self.bottom_collapse_expand_button.setParentItem(self) self.bottom_collapse_expand_button.collapse.connect(self.collapse_children) self.bottom_collapse_expand_button.expand.connect(self.expand_children) self.bottom_collapse_expand_button.isCollapsed = bottom_collapsed # position the bottom button at the bottom-center of the node button_x = self.x - (self.bottom_collapse_expand_button.boundingRect().width() / 2) button_y = self.y + self.total_height / 2 - (self.bottom_collapse_expand_button.boundingRect().height() / 2) self.bottom_collapse_expand_button.setPos(button_x, button_y) # hidden by default, the button is only needed if the node has children if not self.children: self.bottom_collapse_expand_button.hide() # create the top collapse/expand button for this node if self.top_collapse_expand_button: top_collapsed = self.top_collapse_expand_button.isCollapsed else: top_collapsed = False self.top_collapse_expand_button = CollapseExpandButton(self) self.top_collapse_expand_button.setParentItem(self) self.top_collapse_expand_button.collapse.connect(self.collapse_upwards) self.top_collapse_expand_button.expand.connect(self.expand_upwards) self.top_collapse_expand_button.isCollapsed = top_collapsed if self.scene.root_ui_node == self or self in self.scene.disconnected_nodes \ or self.scene.reconnecting_node == self: self.top_collapse_expand_button.hide() # position the top button at the top-center of the node button_x = self.x - (self.top_collapse_expand_button.boundingRect().width() / 2) button_y = self.y - self.total_height / 2 - (self.top_collapse_expand_button.boundingRect().height() / 2) self.top_collapse_expand_button.setPos(button_x, button_y) def create_info_display(self, x, y, attributes): """ Creates view elements for the info display :param x: x position of the node :param y: y position of the node :param attributes: attributes that will be displayed in the view :return: """ start_height = y + (self.NODE_HEIGHT / 2) # unfold dictionary values at the bottom of the list sorted_attributes = [] for k, v in sorted(attributes.items(), key=lambda tup: isinstance(tup[1], dict)): if isinstance(v, dict): sorted_attributes.append((k, v)) sorted_attributes.extend(v.items()) else: sorted_attributes.append((k, v)) # create property rows for i, (k, v) in enumerate(sorted_attributes): value_text = None value_height = 0 if isinstance(v, dict): # display dictionary key as title text = "{}".format(k) if len(text) > 20: text = text[:20] + "..." key_text = QGraphicsSimpleTextItem(text) f = key_text.font() f.setBold(True) key_text.setFont(f) text_width = key_text.boundingRect().width() else: key_text = QGraphicsSimpleTextItem("{}:".format(k) if k else " ") text = str(v) if len(text) > 20: text = text[:20] + "..." value_text = QGraphicsSimpleTextItem(text) value_height = value_text.boundingRect().height() text_width = key_text.boundingRect().width() + value_text.boundingRect().width() # create box around property attribute_container = QGraphicsRectItem(x, start_height, text_width + 10, max(key_text.boundingRect().height(), value_height) + 10) attribute_container.setBrush(QBrush(Qt.white)) self.total_height += attribute_container.rect().height() key_text.setParentItem(attribute_container) if value_text: value_text.setParentItem(attribute_container) self.max_width = max(self.max_width, attribute_container.rect().width()) attribute_container.setParentItem(self) self.info_display.append(attribute_container) start_height += max(key_text.boundingRect().height(), value_height) + 10 # calculate correct coordinates for positioning of the attribute boxes if self.max_width > self.NODE_MIN_WIDTH - 10: x -= (self.max_width + 10) / 2 y -= self.total_height / 2 self.max_width += 10 else: x -= self.NODE_MIN_WIDTH / 2 y -= self.total_height / 2 self.max_width = self.NODE_MIN_WIDTH h = 0 # position all the elements previously created for attribute_container in self.info_display: rect: QRectF = attribute_container.rect() rect.setX(x) rect_height = rect.height() rect.setY(y + self.NODE_HEIGHT + h) rect.setHeight(rect_height) key_child = attribute_container.childItems()[0] if len(attribute_container.childItems()) == 2: key_child.setX(x + 5) value_child = attribute_container.childItems()[1] value_child.setX(x + self.max_width - value_child.boundingRect().width() - 5) value_child.setY(y + self.NODE_HEIGHT + h + 5) else: key_child.setX(x - key_child.boundingRect().width() / 2 + self.max_width / 2) key_child.setY(y + self.NODE_HEIGHT + h + 5) h += rect.height() rect.setWidth(self.max_width) attribute_container.setRect(rect) def paint(self, painter: QPainter, style_options: QStyleOptionGraphicsItem, widget=None): """ Paint the basic shape of the node (ellipse or rectangle) :param painter: painter used to paint objects :param style_options: Styling options for the graphics item :param widget: The widget being painted """ painter.setPen(Qt.SolidLine) if self == self.scene.root_ui_node: pen = QPen(Qt.black, 2.0) pen.setStyle(Qt.DotLine) painter.setPen(pen) if self.scene.simulator_mode: brush = self.simulator_brush else: brush = self.brush painter.setBrush(brush) if self.scene.info_mode: painter.drawRect(self.rect().x(), self.rect().y(), self.rect().width(), self.NODE_HEIGHT) else: painter.drawEllipse(self.rect()) def add_child(self, child): """ Add a child node Inheritance looks like: parent > edge > child :param child: Another ui node """ edge = Edge(self, child) edge.setParentItem(self) # edge should stay behind the expand/collapse button edge.stackBefore(self.bottom_collapse_expand_button) self.children.append(child) self.edges.append(edge) # show the expand/collapse button when the first child is added if not self.bottom_collapse_expand_button.isVisible(): self.bottom_collapse_expand_button.show() if not child.top_collapse_expand_button.isVisible(): child.top_collapse_expand_button.show() def remove_child(self, child): """ Removes child from this node (no data changes) :param child: Child of this node """ if child not in self.children: Node.logger.error("Incorrect child can not be removed from wrong parent.") edge = child.parentItem() child.setParentItem(None) self.children.remove(child) self.edges.remove(edge) edge.setParentItem(None) self.scene.removeItem(edge) if not self.children: self.bottom_collapse_expand_button.hide() def nodes_below(self): nodes = [] for c in self.children: nodes.append(c) nodes.extend(c.nodes_below()) return nodes def moveBy(self, x, y): super(Node, self).moveBy(x, y) # move edge correctly with node if self.parentItem() and isinstance(self.parentItem(), Edge): self.parentItem().change_position() def setPos(self, *args): super(Node, self).setPos(*args) # move edge correctly with node if self.parentItem() and isinstance(self.parentItem(), Edge): self.parentItem().change_position() def xoffset(self): """ recursively adds the relative x distances from this node up until the root node. :return: the sum of the relative x distances """ if self.parentItem(): return self.pos().x() + self.parentItem().xoffset() else: return self.pos().x() + self.rect().x() + self.rect().width() / 2 def yoffset(self): """ recursively adds the relative y distances from this node up until the root node. :return: the sum of the relative y distances """ if self.parentItem(): return self.pos().y() + self.parentItem().yoffset() else: return self.pos().y() + self.rect().y() + self.rect().height() / 2 def xpos(self): """ Calculates the x position of this node using the x offset :return: the x position of the node """ return self.xoffset() def ypos(self): """ Calculates the y position of this node using the y offset :return: the y position of the node """ return self.yoffset() def boundingRect(self): return QRectF(self._rect) def rect(self): return self._rect def detach_from_parent(self): """ Detaches node from parent (no data changes) :return: Positional data that can be used to reattach node """ if not self.parentItem() or not self.parentItem().parentItem(): Node.logger.error("The node can't detach from parent, no parent") return # store attach data used to restore the state when attaching xpos, ypos = self.xpos(), self.ypos() root_item = self.scene.root_ui_node parent_node = self.parentItem().parentItem() attach_data = { "abs_pos": QPointF(xpos, ypos), "old_parent": parent_node, "top_level_item": self.topLevelItem(), } parent_node.remove_child(self) # move node to retain correct position self.setPos(0, 0) root_x = root_item.xpos() if root_item else self.scene.node_init_pos[0] root_y = root_item.ypos() if root_item else self.scene.node_init_pos[1] move_x = xpos - root_x - (self.scene.node_init_pos[0] - root_x) move_y = ypos - root_y - (self.scene.node_init_pos[1] - root_y) self.moveBy(move_x, move_y) return attach_data def attach_to_parent(self, data, parent=None): """ Attaches node to parent (no data changes) :param: data: Positional data from detachment used for attaching """ if not parent: parent = data['old_parent'] new_abs_pos = QPointF(self.xpos(), self.ypos()) # reset parent item e = Edge(parent, self) e.setParentItem(parent) parent.children.append(self) parent.edges.append(e) parent.sort_children() parent_abs_pos = QPointF(parent.xpos(), parent.ypos()) # reset relative position to parent self.setPos(new_abs_pos - parent_abs_pos) def collapse_upwards(self): """ Collapses the tree upwards only displaying this node and its children :return: """ self.expand_data = self.detach_from_parent() # hide parent nodes self.expand_data['top_level_item'].hide() def expand_upwards(self): """ Expands the tree upwards displaying all expanded parent nodes :return: """ self.attach_to_parent(self.expand_data) # show expanded parent nodes self.topLevelItem().show() def collapse_children(self): """ Collapses this node's children by hiding all child edges (and therefore the whole subtree) """ for c in self.childItems(): if isinstance(c, Edge): c.hide() def expand_children(self): """ Expands this node's children by showing all child edges previously hidden by the collapse function """ for c in self.childItems(): if isinstance(c, Edge): c.show() def sort_children(self): """ Sort child edges/nodes based on x position :return: The model nodes in order """ # gather all the edges child_edges = [edge for edge in self.childItems() if isinstance(edge, Edge)] # sort edges by x position of the child nodes child_edges.sort(key=lambda c: c.end_node.xpos()) # reset internal structure self.edges.clear() self.children.clear() # add children back in correct order for e in child_edges: e.setParentItem(None) self.edges.append(e) self.children.append(e.end_node) # set the parent of the children in the correct order for e in child_edges: e.setParentItem(self) # return the model nodes in the correct order model_nodes_order = [e.end_node.model_node for e in child_edges] return model_nodes_order def detect_order_change(self): """ Detects if node order has changed and updates model accordingly """ if not self.parentItem(): # sort top level nodes, this prevents alignment issues self.scene.disconnected_nodes = sorted(self.scene.disconnected_nodes, key=lambda n: n.xpos()) else: # parent node of self parent_node = self.parentItem().parentItem() parent_model_node = self.scene.gui.tree.nodes.get(parent_node.id) # own child index node_index = parent_node.children.index(self) # check if node is swapped with left neighbour try: if node_index - 1 >= 0: # can throw IndexError if there is no left neighbour left_node = parent_node.children[node_index - 1] # check if node is swapped if left_node.xpos() > self.xpos(): # sort children of parent sorted_nodes = parent_node.sort_children() # change model tree structure accordingly parent_model_node.children = [n.id for n in sorted_nodes] self.scene.gui.update_tree(parent_model_node) except IndexError: pass # check if node is swapped with right neighbour try: # can throw IndexError if there is no right neighbour right_node = parent_node.children[node_index + 1] # check if node is swapped if right_node.xpos() < self.xpos(): # sort children of parent sorted_nodes = parent_node.sort_children() # change model tree structure accordingly parent_model_node.children = [n.id for n in sorted_nodes] self.scene.gui.update_tree(parent_model_node) except IndexError: pass def delete_self(self): """ Deletes this node and makes children disconnected subtrees/nodes """ for c in self.children[:]: c.detach_from_parent() # add child to disconnected nodes if self in self.scene.disconnected_nodes: index = self.scene.disconnected_nodes.index(self) self.scene.disconnected_nodes.insert(index, c) else: self.scene.disconnected_nodes.insert(0, c) c.top_collapse_expand_button.hide() parent_model_node = None if self.parentItem(): parent_node: Node = self.parentItem().parentItem() parent_node.remove_child(self) parent_model_node = self.scene.gui.tree.nodes.get(parent_node.id) parent_model_node.children.remove(self.id) if self in self.scene.disconnected_nodes: self.scene.disconnected_nodes.remove(self) self.scene.removeItem(self) self.scene.close_property_display() del self.scene.nodes[self.id] # reset root if this is the root if self.scene.gui.tree.root == self.id: self.scene.gui.tree.root = '' # remove node from internal tree structure del self.scene.gui.tree.nodes[self.id] if parent_model_node: self.scene.gui.update_tree(parent_model_node) def delete_subtree(self, delete_parent_relation=True, update_tree=True): """ Deletes node and its children :param delete_parent_relation: Boolean indicating if parent relation should be modified :param update_tree: Boolean indicating if the tree needs an update """ # remove children for c in self.children: c.delete_subtree(delete_parent_relation=False) # remove child reference from parent parent_node = None if delete_parent_relation and self.parentItem(): parent_node: Node = self.parentItem().parentItem() parent_node.remove_child(self) try: self.scene.gui.tree.nodes[parent_node.id].children.remove(self.id) except ValueError: pass self.scene.removeItem(self) self.scene.close_property_display() if self in self.scene.disconnected_nodes: self.scene.disconnected_nodes.remove(self) self.scene.nodes.pop(self.id, None) if self.scene.gui.tree.root == self.id: self.scene.gui.tree.root = '' # remove node from internal tree structure self.scene.gui.tree.nodes.pop(self.id, None) if delete_parent_relation and parent_node and update_tree: node = self.scene.gui.tree.nodes.get(parent_node.id) self.scene.gui.update_tree(node) def reconnect_edge(self): """ Starts edge reconnection process """ if not self.parentItem() and self not in self.scene.disconnected_nodes: Node.logger.error("The edge trying to reconnect does not exist.") else: self.scene.start_reconnect_edge(self) def mousePressEvent(self, m_event): """ Handles a mouse press on a node :param m_event: The mouse press event and its details """ super(Node, self).mousePressEvent(m_event) tree = self.scene.gui.tree.nodes[self.id] if self.scene.view.parent().property_display: self.scene.view.parent().property_display.setParent(None) self.scene.view.parent().property_display.deleteLater() self.scene.view.parent().property_display = view.widgets.TreeViewPropertyDisplay( self.scene.view.parent().graphics_scene, tree.attributes, parent=self.scene.view.parent(), node_id=tree.id, node_title=tree.title) def mouseMoveEvent(self, m_event): """ Handles a mouse move over a node :param m_event: The mouse move event and its details """ super(Node, self).mouseMoveEvent(m_event) if self.dragging: # move the node with the mouse and adjust the edges to the new position dx = m_event.scenePos().x() - m_event.lastScenePos().x() dy = m_event.scenePos().y() - m_event.lastScenePos().y() self.setPos(self.pos().x() + dx, self.pos().y() + dy) # Set correct order for children if node has a parent and the order of disconnected nodes self.detect_order_change() # reposition incoming edge if isinstance(self.parentItem(), Edge): self.parentItem().change_position() def contextMenuEvent(self, menu_event): """ Creates context menu for right clicks on this node :param menu_event: Context about the right click event """ menu = QMenu() reconnect_edge_action = QAction("Reconnect Edge" if self.parentItem() else "Connect Edge") reconnect_edge_action.triggered.connect(self.reconnect_edge) menu.addAction(reconnect_edge_action) delete_action = QAction("Delete Node") delete_action.setToolTip('Delete only this node.') delete_action.triggered.connect(self.delete_self) menu.addAction(delete_action) delete_subtree_action = QAction("Delete Subtree") delete_subtree_action.setToolTip('Delete node and all its children.') delete_subtree_action.triggered.connect(lambda: self.delete_subtree()) menu.addAction(delete_subtree_action) menu.exec(menu_event.screenPos()) menu_event.setAccepted(True)