def __init__(self, mainwindow, parent=None): """ Initialize the diagram scene. :type mainwindow: MainWindow :type parent: QWidget """ super().__init__(parent) self.document = File(parent=self) self.guid = GUID(self) self.factory = ItemFactory(self) self.index = ItemIndex(self) self.meta = PredicateMetaIndex(self) self.undostack = QUndoStack(self) self.undostack.setUndoLimit(50) self.validator = OWL2Validator(self) self.mainwindow = mainwindow self.pasteOffsetX = Clipboard.PasteOffsetX self.pasteOffsetY = Clipboard.PasteOffsetY self.mode = DiagramMode.Idle self.modeParam = Item.Undefined self.mouseOverNode = None self.mousePressEdge = None self.mousePressPos = None self.mousePressNode = None self.mousePressNodePos = None self.mousePressData = {} connect(self.sgnItemAdded, self.index.add) connect(self.sgnItemRemoved, self.index.remove)
def __init__(self, name, parent): """ Initialize the diagram. :type name: str :type parent: Project """ super().__init__(parent) self.factory = ItemFactory(self) self.guid = GUID(self) self.mode = DiagramMode.Idle self.modeParam = Item.Undefined self.name = name self.pasteX = Clipboard.PasteOffsetX self.pasteY = Clipboard.PasteOffsetY self.mo_Node = None self.mp_Data = None self.mp_Edge = None self.mp_Label = None self.mp_LabelPos = None self.mp_Node = None self.mp_NodePos = None self.mp_Pos = None settings = QtCore.QSettings() self.setFont( Font(font=self.font(), pixelSize=settings.value('diagram/fontsize', self.font().pixelSize(), int))) connect(self.sgnItemAdded, self.onItemAdded) connect(self.sgnItemRemoved, self.onItemRemoved) connect(self.sgnNodeIdentification, self.doNodeIdentification)
def __init__(self, name, parent): """ Initialize the diagram. :type name: str :type parent: Project """ super().__init__(parent) self.factory = ItemFactory(self) self.guid = GUID(self) self.mode = DiagramMode.Idle self.modeParam = Item.Undefined self.name = name self.pasteX = Clipboard.PasteOffsetX self.pasteY = Clipboard.PasteOffsetY self.mo_Node = None self.mp_Data = None self.mp_Edge = None self.mp_Label = None self.mp_LabelPos = None self.mp_Node = None self.mp_NodePos = None self.mp_Pos = None connect(self.sgnItemAdded, self.onItemAdded) connect(self.sgnItemRemoved, self.onItemRemoved) connect(self.sgnNodeIdentification, self.doNodeIdentification)
def __init__(self, mainwindow, filepath, parent=None): """ Initialize the loader. :type mainwindow: MainWindow :type filepath: str :type parent: QObject """ super().__init__(parent) self.mainwindow = mainwindow self.filepath = filepath self.factory = ItemFactory(self) self.scene = None self.errors = []
class Diagram(QtWidgets.QGraphicsScene): """ Extension of QtWidgets.QGraphicsScene which implements a single Graphol diagram. Additionally to built-in signals, this class emits: * sgnItemAdded: whenever an element is added to the Diagram. * sgnItemInsertionCompleted: whenever an item 'MANUAL' insertion process is completed. * sgnItemRemoved: whenever an element is removed from the Diagram. * sgnModeChanged: whenever the Diagram operational mode (or its parameter) changes. * sgnUpdated: whenever the Diagram has been updated in any of its parts. """ GridSize = 10 MinSize = 2000 MaxSize = 1000000 SelectionRadius = 4 sgnItemAdded = QtCore.pyqtSignal('QGraphicsScene', 'QGraphicsItem') sgnItemInsertionCompleted = QtCore.pyqtSignal('QGraphicsItem', int) sgnItemRemoved = QtCore.pyqtSignal('QGraphicsScene', 'QGraphicsItem') sgnModeChanged = QtCore.pyqtSignal(DiagramMode) sgnNodeIdentification = QtCore.pyqtSignal('QGraphicsItem') sgnUpdated = QtCore.pyqtSignal() def __init__(self, name, parent): """ Initialize the diagram. :type name: str :type parent: Project """ super().__init__(parent) self.factory = ItemFactory(self) self.guid = GUID(self) self.mode = DiagramMode.Idle self.modeParam = Item.Undefined self.name = name self.pasteX = Clipboard.PasteOffsetX self.pasteY = Clipboard.PasteOffsetY self.mo_Node = None self.mp_Data = None self.mp_Edge = None self.mp_Label = None self.mp_LabelPos = None self.mp_Node = None self.mp_NodePos = None self.mp_Pos = None connect(self.sgnItemAdded, self.onItemAdded) connect(self.sgnItemRemoved, self.onItemRemoved) connect(self.sgnNodeIdentification, self.doNodeIdentification) ############################################# # FACTORY ################################# @classmethod def create(cls, name, size, project): """ Build and returns a new Diagram instance, using the given parameters. :type name: str :type size: int :type project: Project :rtype: Diagram """ diagram = Diagram(name, project) diagram.setSceneRect(QtCore.QRectF(-size / 2, -size / 2, size, size)) diagram.setItemIndexMethod(Diagram.BspTreeIndex) return diagram ############################################# # PROPERTIES ################################# @property def project(self): """ Returns the project this diagram belongs to (alias for Diagram.parent()). :rtype: Project """ return self.parent() @property def session(self): """ Returns the session this diagram belongs to (alias for Diagram.project.parent()). :rtype: Session """ return self.project.parent() ############################################# # EVENTS ################################# def dragEnterEvent(self, dragEvent): """ Executed when a dragged element enters the scene area. :type dragEvent: QGraphicsSceneDragDropEvent """ super().dragEnterEvent(dragEvent) if dragEvent.mimeData().hasFormat('text/plain'): dragEvent.setDropAction(QtCore.Qt.CopyAction) dragEvent.accept() else: dragEvent.ignore() def dragMoveEvent(self, dragEvent): """ Executed when an element is dragged over the scene. :type dragEvent: QGraphicsSceneDragDropEvent """ super().dragMoveEvent(dragEvent) if dragEvent.mimeData().hasFormat('text/plain'): dragEvent.setDropAction(QtCore.Qt.CopyAction) dragEvent.accept() else: dragEvent.ignore() def dropEvent(self, dropEvent): """ Executed when a dragged element is dropped on the diagram. :type dropEvent: QGraphicsSceneDragDropEvent """ super().dropEvent(dropEvent) if dropEvent.mimeData().hasFormat('text/plain'): snapToGrid = self.session.action('toggle_grid').isChecked() node = self.factory.create(Item.forValue(dropEvent.mimeData().text())) node.setPos(snap(dropEvent.scenePos(), Diagram.GridSize, snapToGrid)) self.session.undostack.push(CommandNodeAdd(self, node)) self.sgnItemInsertionCompleted.emit(node, dropEvent.modifiers()) dropEvent.setDropAction(QtCore.Qt.CopyAction) dropEvent.accept() else: dropEvent.ignore() def mousePressEvent(self, mouseEvent): """ Executed when a mouse button is clicked on the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ mouseModifiers = mouseEvent.modifiers() mouseButtons = mouseEvent.buttons() mousePos = mouseEvent.scenePos() if mouseButtons & QtCore.Qt.LeftButton: if self.mode is DiagramMode.NodeAdd: ############################################# # NODE INSERTION ################################# snapToGrid = self.session.action('toggle_grid').isChecked() node = self.factory.create(Item.forValue(self.modeParam)) node.setPos(snap(mousePos, Diagram.GridSize, snapToGrid)) self.session.undostack.push(CommandNodeAdd(self, node)) self.sgnItemInsertionCompleted.emit(node, mouseEvent.modifiers()) elif self.mode is DiagramMode.EdgeAdd: ############################################# # EDGE INSERTION ################################# node = first(self.items(mousePos, edges=False)) if node: edge = self.factory.create(Item.forValue(self.modeParam), source=node) edge.updateEdge(target=mousePos) self.mp_Edge = edge self.addItem(edge) else: # Execute super at first since this may change the diagram # mode: some actions are directly handle by graphics items # (i.e: edge breakpoint move, edge anchor move, node shape # resize) and we need to check whether any of them is being # performed before handling the even locally. super().mousePressEvent(mouseEvent) if self.mode is DiagramMode.Idle: if mouseModifiers & QtCore.Qt.ShiftModifier: ############################################# # LABEL MOVE ################################# item = first(self.items(mousePos, nodes=False, edges=False, labels=True)) if item and item.isMovable(): self.clearSelection() self.mp_Label = item self.mp_LabelPos = item.pos() self.mp_Pos = mousePos self.setMode(DiagramMode.LabelMove) else: ############################################# # ITEM SELECTION ################################# item = first(self.items(mousePos, labels=True)) if item: if item.isLabel(): # If we are hitting a label, check whether the label # is overlapping it's parent item and such item is # also intersecting the current mouse position: if so, # use the parent item as placeholder for the selection. parent = item.parentItem() items = self.items(mousePos) item = parent if parent in items else None if item: if mouseModifiers & QtCore.Qt.ControlModifier: # CTRL => support item multi selection. item.setSelected(not item.isSelected()) else: if self.selectedItems(): # Some elements have been already selected in the # diagram, during a previous mouse press event. if not item.isSelected(): # There are some items selected but we clicked # on a node which is not currently selected, so # make this node the only selected one. self.clearSelection() item.setSelected(True) else: # No item (nodes or edges) is selected and we just # clicked on one so make sure to select this item and # because selectedItems() filters out item Label's, # clear out the selection on the diagram. self.clearSelection() item.setSelected(True) # If we have some nodes selected we need to prepare data for a # possible item move operation: we need to make sure to retrieve # the node below the mouse cursor that will act as as mouse grabber # to compute delta movements for each component in the selection. selected = self.selectedNodes() if selected: self.mp_Node = first(self.items(mousePos, edges=False)) if self.mp_Node: self.mp_NodePos = self.mp_Node.pos() self.mp_Pos = mousePos self.mp_Data = { 'nodes': { node: { 'anchors': {k: v for k, v in node.anchors.items()}, 'pos': node.pos(), } for node in selected}, 'edges': {} } # Figure out if the nodes we are moving are sharing edges: # if that's the case, move the edge together with the nodes # (which actually means moving the edge breakpoints). for node in self.mp_Data['nodes']: for edge in node.edges: if edge not in self.mp_Data['edges']: if edge.other(node).isSelected(): self.mp_Data['edges'][edge] = edge.breakpoints[:] def mouseMoveEvent(self, mouseEvent): """ Executed when then mouse is moved on the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ mouseButtons = mouseEvent.buttons() mousePos = mouseEvent.scenePos() if mouseButtons & QtCore.Qt.LeftButton: if self.mode is DiagramMode.EdgeAdd: ############################################# # EDGE INSERTION ################################# if self.isEdgeAdd(): statusBar = self.session.statusBar() edge = self.mp_Edge edge.updateEdge(target=mousePos) previousNode = self.mo_Node if previousNode: previousNode.updateNode(selected=False) currentNode = first(self.items(mousePos, edges=False, skip={edge.source})) if currentNode: self.mo_Node = currentNode pvr = self.project.profile.checkEdge(edge.source, edge, currentNode) currentNode.updateNode(selected=False, valid=pvr.isValid()) if not pvr.isValid(): statusBar.showMessage(pvr.message()) else: statusBar.clearMessage() else: statusBar.clearMessage() self.mo_Node = None self.project.profile.reset() elif self.mode is DiagramMode.LabelMove: ############################################# # LABEL MOVE ################################# if self.isLabelMove(): snapToGrid = self.session.action('toggle_grid').isChecked() point = self.mp_LabelPos + mousePos - self.mp_Pos point = snap(point, Diagram.GridSize / 2, snapToGrid) delta = point - self.mp_LabelPos self.mp_Label.setPos(self.mp_LabelPos + delta) else: if self.mode is DiagramMode.Idle: if self.mp_Node: self.setMode(DiagramMode.NodeMove) if self.mode is DiagramMode.NodeMove: ############################################# # ITEM MOVEMENT ################################# if self.isNodeMove(): snapToGrid = self.session.action('toggle_grid').isChecked() point = self.mp_NodePos + mousePos - self.mp_Pos point = snap(point, Diagram.GridSize, snapToGrid) delta = point - self.mp_NodePos edges = set() for edge, breakpoints in self.mp_Data['edges'].items(): for i in range(len(breakpoints)): edge.breakpoints[i] = breakpoints[i] + delta for node, data in self.mp_Data['nodes'].items(): edges |= set(node.edges) node.setPos(data['pos'] + delta) for edge, pos in data['anchors'].items(): node.setAnchor(edge, pos + delta) for edge in edges: edge.updateEdge() super().mouseMoveEvent(mouseEvent) def mouseReleaseEvent(self, mouseEvent): """ Executed when the mouse is released from the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ mouseModifiers = mouseEvent.modifiers() mouseButton = mouseEvent.button() mousePos = mouseEvent.scenePos() if mouseButton == QtCore.Qt.LeftButton: if self.mode is DiagramMode.EdgeAdd: ############################################# # EDGE INSERTION ################################# if self.isEdgeAdd(): edge = self.mp_Edge edge.source.updateNode(selected=False) currentNode = first(self.items(mousePos, edges=False, skip={edge.source})) insertEdge = False if currentNode: currentNode.updateNode(selected=False) pvr = self.project.profile.checkEdge(edge.source, edge, currentNode) if pvr.isValid(): edge.target = currentNode insertEdge = True # We temporarily remove the item from the diagram and we perform the # insertion using the undo command that will also emit the sgnItemAdded # signal hence all the widgets will be notified of the edge insertion. # We do this because while creating the edge we need to display it so the # user knows what he is connecting, but we don't want to truly insert # it till it's necessary (when the mouse is released and the validation # confirms that the generated expression is a valid graphol expression). self.removeItem(edge) if insertEdge: self.session.undostack.push(CommandEdgeAdd(self, edge)) edge.updateEdge() self.clearSelection() self.project.profile.reset() statusBar = self.session.statusBar() statusBar.clearMessage() self.sgnItemInsertionCompleted.emit(edge, mouseModifiers) elif self.mode is DiagramMode.LabelMove: ############################################# # LABEL MOVE ################################# if self.isLabelMove(): pos = self.mp_Label.pos() if self.mp_LabelPos != pos: item = self.mp_Label.parentItem() command = CommandLabelMove(self, item, self.mp_LabelPos, pos) self.session.undostack.push(command) self.setMode(DiagramMode.Idle) elif self.mode is DiagramMode.NodeMove: ############################################# # ITEM MOVEMENT ################################# if self.isNodeMove(): pos = self.mp_Node.pos() if self.mp_NodePos != pos: data = { 'undo': self.mp_Data, 'redo': { 'nodes': { node: { 'anchors': {k: v for k, v in node.anchors.items()}, 'pos': node.pos(), } for node in self.mp_Data['nodes']}, 'edges': {x: x.breakpoints[:] for x in self.mp_Data['edges']} } } self.session.undostack.push(CommandNodeMove(self, data)) self.setMode(DiagramMode.Idle) elif mouseButton == QtCore.Qt.RightButton: if self.mode is not DiagramMode.SceneDrag: ############################################# # CONTEXTUAL MENU ################################# item = first(self.items(mousePos)) if not item: self.clearSelection() items = [] else: items = self.selectedItems() if item not in items: self.clearSelection() item.setSelected(True) items = [item] self.mp_Pos = mousePos menu = self.session.mf.create(self, items, mousePos) menu.exec_(mouseEvent.screenPos()) super().mouseReleaseEvent(mouseEvent) self.mo_Node = None self.mp_Data = None self.mp_Edge = None self.mp_Label = None self.mp_LabelPos = None self.mp_Node = None self.mp_NodePos = None self.mp_Pos = None ############################################# # SLOTS ################################# @QtCore.pyqtSlot('QGraphicsItem') def doNodeIdentification(self, node): """ Perform node identification. :type node: AbstractNode """ if Identity.Neutral in node.identities(): func = lambda x: Identity.Neutral in x.identities() collection = bfs(source=node, filter_on_visit=func) generators = partition(func, collection) excluded = set() strong = set(generators[1]) weak = set(generators[0]) for node in weak: identification = node.identify() if identification: strong = set.union(strong, identification[0]) strong = set.difference(strong, identification[1]) excluded = set.union(excluded, identification[2]) computed = Identity.Neutral identities = set(x.identity() for x in strong) if identities: computed = first(identities) if len(identities) > 1: computed = Identity.Unknown for node in weak - strong - excluded: node.setIdentity(computed) @QtCore.pyqtSlot('QGraphicsScene', 'QGraphicsItem') def onItemAdded(self, _, item): """ Executed whenever a connection is created/removed. :type _: Diagram :type item: AbstractItem """ if item.isEdge(): # Execute the node identification procedure only if one of the # endpoints we are connecting is currently identified as NEUTRAL. if (item.source.identity() is Identity.Neutral) ^ (item.target.identity() is Identity.Neutral): for node in (item.source, item.target): self.sgnNodeIdentification.emit(node) @QtCore.pyqtSlot('QGraphicsScene', 'QGraphicsItem') def onItemRemoved(self, _, item): """ Executed whenever a connection is created/removed. :type _: Diagram :type item: AbstractItem """ if item.isEdge(): # When an edge is removed we may be in the case where # the ontology is split into 2 subgraphs, hence we need # to run the identification procedure on the 2 subgraphs. for node in (item.source, item.target): self.sgnNodeIdentification.emit(node) ############################################# # INTERFACE ################################# def addItem(self, item): """ Add an item to the Diagram (will redraw the item to reflect its status). :type item: AbstractItem """ super().addItem(item) if item.isNode(): item.updateNode() def edge(self, eid): """ Returns the edge matching the given id or None if no edge is found. :type eid: str :rtype: AbstractEdge """ return self.project.edge(self, eid) def edges(self): """ Returns a collection with all the edges in the diagram. :rtype: set """ return self.project.edges(self) def isEdgeAdd(self): """ Returns True if an edge insertion is currently in progress, False otherwise. :rtype: bool """ return self.mode is DiagramMode.EdgeAdd and self.mp_Edge is not None def isLabelMove(self): """ Returns True if a label is currently being moved, False otherwise. :rtype: bool """ return self.mode is DiagramMode.LabelMove and \ self.mp_Label is not None and \ self.mp_LabelPos is not None and \ self.mp_Pos is not None def isNodeMove(self): """ Returns True if a node(s) is currently being moved, False otherwise. :rtype: bool """ return self.mode is DiagramMode.NodeMove and \ self.mp_Data is not None and \ self.mp_Node is not None and \ self.mp_NodePos is not None and \ self.mp_Pos is not None def isEmpty(self): """ Returns True if this diagram containts no element, False otherwise. :rtype: bool """ return len(self.project.items(self)) == 0 def items(self, mixed=None, mode=QtCore.Qt.IntersectsItemShape, **kwargs): """ Returns a collection of items ordered from TOP to BOTTOM. If no argument is supplied, an unordered list containing all the elements in the diagram is returned. :type mixed: T <= QPointF | QRectF | QPolygonF | QPainterPath :type mode: ItemSelectionMode :rtype: list """ if mixed is None: items = super().items() elif isinstance(mixed, QtCore.QPointF): x = mixed.x() - (Diagram.SelectionRadius / 2) y = mixed.y() - (Diagram.SelectionRadius / 2) w = Diagram.SelectionRadius h = Diagram.SelectionRadius items = super().items(QtCore.QRectF(x, y, w, h), mode) else: items = super().items(mixed, mode) return sorted([ x for x in items if (kwargs.get('nodes', True) and x.isNode() or kwargs.get('edges', True) and x.isEdge() or kwargs.get('labels', False) and x.isLabel()) and x not in kwargs.get('skip', set()) ], key=lambda i: i.zValue(), reverse=True) def nodes(self): """ Returns a collection with all the nodes in the diagram. :rtype: set """ return self.project.nodes(self) def node(self, nid): """ Returns the node matching the given id or None if no node is found. :type nid: str :rtype: AbstractNode """ return self.project.node(self, nid) def selectedEdges(self, filter_on_edges=lambda x: True): """ Returns the edges selected in the diagram. :type filter_on_edges: callable :rtype: list """ return [x for x in super().selectedItems() if x.isEdge() and filter_on_edges(x)] def selectedItems(self, filter_on_items=lambda x: True): """ Returns the items selected in the diagram. :type filter_on_items: callable :rtype: list """ return [x for x in super().selectedItems() if (x.isNode() or x.isEdge()) and filter_on_items(x)] def selectedNodes(self, filter_on_nodes=lambda x: True): """ Returns the nodes selected in the diagram. :type filter_on_nodes: callable :rtype: list """ return [x for x in super().selectedItems() if x.isNode() and filter_on_nodes(x)] def setMode(self, mode, param=None): """ Set the operational mode. :type mode: DiagramMode :type param: Item """ if self.mode != mode or self.modeParam != param: #LOGGER.debug('Diagram mode changed: mode=%s, param=%s', mode, param) self.mode = mode self.modeParam = param self.sgnModeChanged.emit(mode) def visibleRect(self, margin=0): """ Returns a rectangle matching the area of visible items. :type margin: float :rtype: QtCore.QRectF """ items = self.items() if items: x = set() y = set() for item in items: b = item.mapRectToScene(item.boundingRect()) x.update({b.left(), b.right()}) y.update({b.top(), b.bottom()}) return QtCore.QRectF(QtCore.QPointF(min(x) - margin, min(y) - margin), QtCore.QPointF(max(x) + margin, max(y) + margin)) return QtCore.QRectF()
class DiagramScene(QGraphicsScene): """ This class implements the main Diagram Scene. """ GridPen = QPen(QColor(80, 80, 80), 0, Qt.SolidLine) GridSize = 20 MinSize = 2000 MaxSize = 1000000 RecentNum = 5 sgnInsertionEnded = pyqtSignal('QGraphicsItem', int) sgnItemAdded = pyqtSignal('QGraphicsItem') sgnModeChanged = pyqtSignal(DiagramMode) sgnItemRemoved = pyqtSignal('QGraphicsItem') sgnUpdated = pyqtSignal() #################################################################################################################### # # # DIAGRAM SCENE IMPLEMENTATION # # # #################################################################################################################### def __init__(self, mainwindow, parent=None): """ Initialize the diagram scene. :type mainwindow: MainWindow :type parent: QWidget """ super().__init__(parent) self.document = File(parent=self) self.guid = GUID(self) self.factory = ItemFactory(self) self.index = ItemIndex(self) self.meta = PredicateMetaIndex(self) self.undostack = QUndoStack(self) self.undostack.setUndoLimit(50) self.validator = OWL2Validator(self) self.mainwindow = mainwindow self.pasteOffsetX = Clipboard.PasteOffsetX self.pasteOffsetY = Clipboard.PasteOffsetY self.mode = DiagramMode.Idle self.modeParam = Item.Undefined self.mouseOverNode = None self.mousePressEdge = None self.mousePressPos = None self.mousePressNode = None self.mousePressNodePos = None self.mousePressData = {} connect(self.sgnItemAdded, self.index.add) connect(self.sgnItemRemoved, self.index.remove) #################################################################################################################### # # # EVENTS # # # #################################################################################################################### def dragEnterEvent(self, dragEvent): """ Executed when a dragged element enters the scene area. :type dragEvent: QGraphicsSceneDragDropEvent """ super().dragEnterEvent(dragEvent) if dragEvent.mimeData().hasFormat('text/plain'): dragEvent.setDropAction(Qt.CopyAction) dragEvent.accept() else: dragEvent.ignore() def dragMoveEvent(self, dragEvent): """ Executed when an element is dragged over the scene. :type dragEvent: QGraphicsSceneDragDropEvent """ super().dragMoveEvent(dragEvent) if dragEvent.mimeData().hasFormat('text/plain'): dragEvent.setDropAction(Qt.CopyAction) dragEvent.accept() else: dragEvent.ignore() def dropEvent(self, dropEvent): """ Executed when a dragged element is dropped on the scene. :type dropEvent: QGraphicsSceneDragDropEvent """ super().dropEvent(dropEvent) if dropEvent.mimeData().hasFormat('text/plain'): item = Item.forValue(dropEvent.mimeData().text()) node = self.factory.create(item=item, scene=self) node.setPos(snap(dropEvent.scenePos(), DiagramScene.GridSize, self.mainwindow.snapToGrid)) self.undostack.push(CommandNodeAdd(scene=self, node=node)) self.sgnInsertionEnded.emit(node, dropEvent.modifiers()) dropEvent.setDropAction(Qt.CopyAction) dropEvent.accept() else: dropEvent.ignore() def mousePressEvent(self, mouseEvent): """ Executed when a mouse button is clicked on the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ mouseButtons = mouseEvent.buttons() mousePos = mouseEvent.scenePos() if mouseButtons & Qt.LeftButton: if self.mode is DiagramMode.InsertNode: ######################################################################################################## # # # NODE INSERTION # # # ######################################################################################################## item = Item.forValue(self.modeParam) node = self.factory.create(item, self) node.setPos(snap(mousePos, DiagramScene.GridSize, self.mainwindow.snapToGrid)) self.undostack.push(CommandNodeAdd(self, node)) self.sgnInsertionEnded.emit(node, mouseEvent.modifiers()) super().mousePressEvent(mouseEvent) elif self.mode is DiagramMode.InsertEdge: ######################################################################################################## # # # EDGE INSERTION # # # ######################################################################################################## node = self.itemOnTopOf(mousePos, edges=False) if node: item = Item.forValue(self.modeParam) edge = self.factory.create(item, self, source=node) edge.updateEdge(mousePos) self.mousePressEdge = edge self.addItem(edge) super().mousePressEvent(mouseEvent) else: super().mousePressEvent(mouseEvent) if self.mode is DiagramMode.Idle: #################################################################################################### # # # ITEM SELECTION # # # #################################################################################################### # See if we have some nodes selected in the scene: this is needed because itemOnTopOf # will discard labels, so if we have a node whose label is overlapping the node shape, # clicking on the label will make itemOnTopOf return the node item instead of the label. selected = self.selectedNodes() if selected: # We have some nodes selected in the scene so we probably are going to do a # move operation, prepare data for mouse move event => select a node that will act # as mouse grabber to compute delta movements for each componenet in the selection. self.mousePressNode = self.itemOnTopOf(mousePos, edges=False) if self.mousePressNode: self.mousePressNodePos = self.mousePressNode.pos() self.mousePressPos = mousePos self.mousePressData = { 'nodes': { node: { 'anchors': {k: v for k, v in node.anchors.items()}, 'pos': node.pos(), } for node in selected}, 'edges': {} } # Figure out if the nodes we are moving are sharing edges: if so, move the edge # together with the nodes (which actually means moving the edge breakpoints). for node in self.mousePressData['nodes']: for edge in node.edges: if edge not in self.mousePressData['edges']: if edge.other(node).isSelected(): self.mousePressData['edges'][edge] = edge.breakpoints[:] def mouseMoveEvent(self, mouseEvent): """ Executed when then mouse is moved on the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ mouseButtons = mouseEvent.buttons() mousePos = mouseEvent.scenePos() if mouseButtons & Qt.LeftButton: if self.mode is DiagramMode.InsertEdge: ######################################################################################################## # # # EDGE INSERTION # # # ######################################################################################################## if self.mousePressEdge: edge = self.mousePressEdge edge.updateEdge(mousePos) currentNode = self.itemOnTopOf(mousePos, edges=False, skip={edge.source}) previousNode = self.mouseOverNode statusBar = self.mainwindow.statusBar() if previousNode: previousNode.updateBrush(selected=False) if currentNode: self.mouseOverNode = currentNode res = self.validator.result(edge.source, edge, currentNode) currentNode.updateBrush(selected=False, valid=res.valid) if not res.valid: statusBar.showMessage(res.message) else: statusBar.clearMessage() else: statusBar.clearMessage() self.mouseOverNode = None self.validator.clear() else: if self.mode is DiagramMode.Idle: if self.mousePressNode: self.setMode(DiagramMode.MoveNode) if self.mode is DiagramMode.MoveNode: #################################################################################################### # # # ITEM MOVEMENT # # # #################################################################################################### point = self.mousePressNodePos + mousePos - self.mousePressPos point = snap(point, DiagramScene.GridSize, self.mainwindow.snapToGrid) delta = point - self.mousePressNodePos edges = set() # Update all the breakpoints positions. for edge, breakpoints in self.mousePressData['edges'].items(): for i in range(len(breakpoints)): edge.breakpoints[i] = breakpoints[i] + delta # Move all the selected nodes. for node, data in self.mousePressData['nodes'].items(): edges |= set(node.edges) node.setPos(data['pos'] + delta) for edge, pos in data['anchors'].items(): node.setAnchor(edge, pos + delta) # Update edges. for edge in edges: edge.updateEdge() super().mouseMoveEvent(mouseEvent) def mouseReleaseEvent(self, mouseEvent): """ Executed when the mouse is released from the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ mouseButton = mouseEvent.button() mousePos = mouseEvent.scenePos() if mouseButton == Qt.LeftButton: if self.mode is DiagramMode.InsertEdge: ######################################################################################################## # # # EDGE INSERTION # # # ######################################################################################################## if self.mousePressEdge: edge = self.mousePressEdge edge.source.updateBrush(selected=False) currentNode = self.itemOnTopOf(mousePos, edges=False, skip={edge.source}) insertEdge = False if currentNode: currentNode.updateBrush(selected=False) if self.validator.valid(edge.source, edge, currentNode): edge.target = currentNode insertEdge = True # We remove the item temporarily from the graphics scene and we perform the add using # the undo command that will also emit the sgnItemAdded signal hence all the widgets will # be notified of the edge insertion. We do this because while creating the edge we need # to display it so the users knows what is he connecting, but we don't want to truly insert # it till it's necessary (when the mouse is released and the validator allows the insertion) self.removeItem(edge) if insertEdge: self.undostack.push(CommandEdgeAdd(self, edge)) edge.updateEdge() self.mouseOverNode = None self.mousePressEdge = None self.clearSelection() self.validator.clear() statusBar = self.mainwindow.statusBar() statusBar.clearMessage() self.sgnInsertionEnded.emit(edge, mouseEvent.modifiers()) elif self.mode is DiagramMode.MoveNode: ######################################################################################################## # # # ITEM MOVEMENT # # # ######################################################################################################## data = { 'undo': self.mousePressData, 'redo': { 'nodes': { node: { 'anchors': {k: v for k, v in node.anchors.items()}, 'pos': node.pos(), } for node in self.mousePressData['nodes']}, 'edges': {x: x.breakpoints[:] for x in self.mousePressData['edges']} } } self.undostack.push(CommandNodeMove(self, data)) self.setMode(DiagramMode.Idle) elif mouseButton == Qt.RightButton: if self.mode is not DiagramMode.SceneDrag: ######################################################################################################## # # # CONTEXT MENU # # # ######################################################################################################## item = self.itemOnTopOf(mousePos) if item: self.clearSelection() item.setSelected(True) self.mousePressPos = mousePos menu = self.mainwindow.menuFactory.create(self.mainwindow, self, item, mousePos) menu.exec_(mouseEvent.screenPos()) super().mouseReleaseEvent(mouseEvent) self.mousePressPos = None self.mousePressNode = None self.mousePressNodePos = None self.mousePressData = None #################################################################################################################### # # # AXIOMS COMPOSITION # # # #################################################################################################################### def propertyAxiomComposition(self, source, restriction): """ Returns a collection of items to be added to the given source node to compose a property axiom. :type source: AbstractNode :type restriction: class :rtype: set """ node = restriction(scene=self) edge = InputEdge(scene=self, source=source, target=node) size = DiagramScene.GridSize offsets = ( QPointF(snapF(+source.width() / 2 + 70, size), 0), QPointF(snapF(-source.width() / 2 - 70, size), 0), QPointF(0, snapF(-source.height() / 2 - 70, size)), QPointF(0, snapF(+source.height() / 2 + 70, size)), QPointF(snapF(+source.width() / 2 + 70, size), snapF(-source.height() / 2 - 70, size)), QPointF(snapF(-source.width() / 2 - 70, size), snapF(-source.height() / 2 - 70, size)), QPointF(snapF(+source.width() / 2 + 70, size), snapF(+source.height() / 2 + 70, size)), QPointF(snapF(-source.width() / 2 - 70, size), snapF(+source.height() / 2 + 70, size)), ) pos = None num = sys.maxsize rad = QPointF(node.width() / 2, node.height() / 2) for o in offsets: count = len(self.items(QRectF(source.pos() + o - rad, source.pos() + o + rad))) if count < num: num = count pos = source.pos() + o node.setPos(pos) return {node, edge} def propertyDomainAxiomComposition(self, source): """ Returns a collection of items to be added to the given source node to compose a property domain. :type source: AbstractNode :rtype: set """ return self.propertyAxiomComposition(source, DomainRestrictionNode) def propertyRangeAxiomComposition(self, source): """ Returns a collection of items to be added to the given source node to compose a property range. :type source: AbstractNode :rtype: set """ return self.propertyAxiomComposition(source, RangeRestrictionNode) #################################################################################################################### # # # SLOTS # # # #################################################################################################################### @pyqtSlot() def clear(self): """ Clear the diagram by removing all the elements. """ self.index.clear() self.undostack.clear() super().clear() #################################################################################################################### # # # INTERFACE # # # #################################################################################################################### def edge(self, eid): """ Returns the edge matching the given edge id. :type eid: str """ return self.index.edgeForId(eid) def edges(self): """ Returns a view on all the edges of the diagram. :rtype: view """ return self.index.edges() def itemOnTopOf(self, point, nodes=True, edges=True, skip=None): """ Returns the shape which is on top of the given point. :type point: QPointF :type nodes: bool :type edges: bool :type skip: iterable :rtype: Item """ skip = skip or {} data = [x for x in self.items(point) if (nodes and x.node or edges and x.edge) and x not in skip] if data: return max(data, key=lambda x: x.zValue()) return None def node(self, nid): """ Returns the node matching the given node id. :type nid: str """ return self.index.nodeForId(nid) def nodes(self): """ Returns a view on all the nodes in the diagram. :rtype: view """ return self.index.nodes() def selectedEdges(self): """ Returns the edges selected in the scene. :rtype: list """ return [x for x in super(DiagramScene, self).selectedItems() if x.edge] def selectedItems(self): """ Returns the items selected in the scene (will filter out labels since we don't need them). :rtype: list """ return [x for x in super(DiagramScene, self).selectedItems() if x.node or x.edge] def selectedNodes(self): """ Returns the nodes selected in the scene. :rtype: list """ return [x for x in super(DiagramScene, self).selectedItems() if x.node] def setMode(self, mode, param=None): """ Set the operation mode. :type mode: DiagramMode :type param: int """ if self.mode != mode or self.modeParam != param: self.mode = mode self.modeParam = param self.sgnModeChanged.emit(mode) def visibleRect(self, margin=0): """ Returns a rectangle matching the area of visible items. :type margin: float :rtype: QRectF """ bound = self.itemsBoundingRect() topLeft = QPointF(bound.left() - margin, bound.top() - margin) bottomRight = QPointF(bound.right() + margin, bound.bottom() + margin) return QRectF(topLeft, bottomRight)
class Diagram(QtWidgets.QGraphicsScene): """ Extension of QtWidgets.QGraphicsScene which implements a single Graphol diagram. Additionally to built-in signals, this class emits: * sgnItemAdded: whenever an element is added to the Diagram. * sgnItemInsertionCompleted: whenever an item 'MANUAL' insertion process is completed. * sgnItemRemoved: whenever an element is removed from the Diagram. * sgnModeChanged: whenever the Diagram operational mode (or its parameter) changes. * sgnUpdated: whenever the Diagram has been updated in any of its parts. """ GridSize = 10 KeyMoveFactor = 10 MinSize = 2000 MaxSize = 1000000 MinFontSize = 8 MaxFontSize = 40 SelectionRadius = 4 sgnItemAdded = QtCore.pyqtSignal('QGraphicsScene', 'QGraphicsItem') sgnItemInsertionCompleted = QtCore.pyqtSignal('QGraphicsItem', int) sgnItemRemoved = QtCore.pyqtSignal('QGraphicsScene', 'QGraphicsItem') sgnModeChanged = QtCore.pyqtSignal(DiagramMode) sgnNodeIdentification = QtCore.pyqtSignal('QGraphicsItem') sgnUpdated = QtCore.pyqtSignal() def __init__(self, name, parent): """ Initialize the diagram. :type name: str :type parent: Project """ super().__init__(parent) self.factory = ItemFactory(self) self.guid = GUID(self) self.mode = DiagramMode.Idle self.modeParam = Item.Undefined self.name = name self.pasteX = Clipboard.PasteOffsetX self.pasteY = Clipboard.PasteOffsetY self.mo_Node = None self.mp_Data = None self.mp_Edge = None self.mp_Label = None self.mp_LabelPos = None self.mp_Node = None self.mp_NodePos = None self.mp_Pos = None settings = QtCore.QSettings() self.setFont( Font(font=self.font(), pixelSize=settings.value('diagram/fontsize', self.font().pixelSize(), int))) connect(self.sgnItemAdded, self.onItemAdded) connect(self.sgnItemRemoved, self.onItemRemoved) connect(self.sgnNodeIdentification, self.doNodeIdentification) ############################################# # FACTORY ################################# @classmethod def create(cls, name, size, project): """ Build and returns a new Diagram instance, using the given parameters. :type name: str :type size: int :type project: Project :rtype: Diagram """ diagram = Diagram(name, project) diagram.setBackgroundBrush(QtCore.Qt.white) diagram.setSceneRect(QtCore.QRectF(-size / 2, -size / 2, size, size)) diagram.setItemIndexMethod(Diagram.BspTreeIndex) return diagram ############################################# # PROPERTIES ################################# @property def project(self): """ Returns the project this diagram belongs to (alias for Diagram.parent()). :rtype: Project """ return self.parent() @property def session(self): """ Returns the session this diagram belongs to (alias for Diagram.project.parent()). :rtype: Session """ return self.project.parent() ############################################# # EVENTS ################################# def event(self, event: QtCore.QEvent) -> bool: """ Executed when an event happens in the scene, before any specialized handler executes. :type event: QtCore.QEvent :rtype: bool """ # This event is sent to itself by the scene every time the scene font property changes, # either directly (via setFont()) or indirectly (via QApplication::setFont()). # Here we cascade the event to all top-level widget items in the scene to have # them notified about the font change. if event.type() == QtCore.QEvent.FontChange: # CASCADE THE EVENT TO ALL TOP LEVEL ITEMS IN THE SCENE for item in self.items(): self.sendEvent(item, event) return super().event(event) def dragEnterEvent(self, dragEvent): """ Executed when a dragged element enters the scene area. :type dragEvent: QGraphicsSceneDragDropEvent """ super().dragEnterEvent(dragEvent) if dragEvent.mimeData().hasFormat('text/plain'): dragEvent.setDropAction(QtCore.Qt.CopyAction) dragEvent.accept() else: dragEvent.ignore() def dragMoveEvent(self, dragEvent): """ Executed when an element is dragged over the scene. :type dragEvent: QGraphicsSceneDragDropEvent """ super().dragMoveEvent(dragEvent) if dragEvent.mimeData().hasFormat('text/plain'): dragEvent.setDropAction(QtCore.Qt.CopyAction) dragEvent.accept() else: dragEvent.ignore() # noinspection PyTypeChecker def dropEvent(self, dropEvent): """ Executed when a dragged element is dropped on the diagram. :type dropEvent: QGraphicsSceneDragDropEvent """ super().dropEvent(dropEvent) if dropEvent.mimeData().hasFormat('text/plain') and Item.valueOf( dropEvent.mimeData().text()): snapToGrid = self.session.action('toggle_grid').isChecked() #TODO nodeType = dropEvent.mimeData().text() if Item.ConceptIRINode <= int( dropEvent.mimeData().text()) <= Item.IndividualIRINode: #New node associated with IRI object #node = ConceptNode(diagram=self) node = self.factory.create( Item.valueOf(dropEvent.mimeData().text())) node.setPos( snap(dropEvent.scenePos(), Diagram.GridSize, snapToGrid)) data = dropEvent.mimeData().data(dropEvent.mimeData().text()) if not data: #new element if isinstance(node, FacetNode): self.session.doOpenConstrainingFacetBuilder(node) elif isinstance(node, OntologyEntityNode) or isinstance( node, OntologyEntityResizableNode): self.session.doOpenIRIBuilder(node) elif isinstance(node, LiteralNode): self.session.doOpenLiteralBuilder(node) else: #copy of existing element (e.g. drag and drop from ontology explorer) data_str = str(data, encoding='utf-8') iri = self.project.getIRI(data_str) node.iri = iri self.doAddOntologyEntityNode(node) node.doUpdateNodeLabel() else: #Old node type node = self.factory.create( Item.valueOf(dropEvent.mimeData().text())) data = dropEvent.mimeData().data(dropEvent.mimeData().text()) iri = None if data is not None: data_str = str(data, encoding='utf-8') if data_str is not '': data_comma_seperated = data_str.split(',') iri = data_comma_seperated[0] rc = data_comma_seperated[1] txt = data_comma_seperated[2] node.setText(txt) node.remaining_characters = rc node.setPos( snap(dropEvent.scenePos(), Diagram.GridSize, snapToGrid)) commands = [] #node.emptyMethod() if iri is not None: Duplicate_dict_1 = self.project.copy_IRI_prefixes_nodes_dictionaries( self.project.IRI_prefixes_nodes_dict, dict()) Duplicate_dict_2 = self.project.copy_IRI_prefixes_nodes_dictionaries( self.project.IRI_prefixes_nodes_dict, dict()) Duplicate_dict_1 = self.project.addIRINodeEntry( Duplicate_dict_1, iri, node) if Duplicate_dict_1 is not None: pass #commands.append(CommandProjetSetIRIPrefixesNodesDict(self.project, Duplicate_dict_2, Duplicate_dict_1, [iri], None)) commands.append(CommandNodeAdd(self, node)) if any(commands): self.session.undostack.beginMacro('node Add - {0}'.format( node.name)) for command in commands: if command: self.session.undostack.push(command) self.session.undostack.endMacro() self.sgnItemInsertionCompleted.emit(node, dropEvent.modifiers()) dropEvent.setDropAction(QtCore.Qt.CopyAction) dropEvent.accept() else: dropEvent.ignore() # noinspection PyTypeChecker def mousePressEvent(self, mouseEvent): """ Executed when a mouse button is clicked on the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ self.project.colour_items_in_case_of_unsatisfiability_or_inconsistent_ontology( ) mouseModifiers = mouseEvent.modifiers() mouseButtons = mouseEvent.buttons() mousePos = mouseEvent.scenePos() if mouseButtons & QtCore.Qt.LeftButton: if self.mode is DiagramMode.NodeAdd: ############################################# # NODE INSERTION ################################# snapToGrid = self.session.action('toggle_grid').isChecked() node = self.factory.create(Item.valueOf(self.modeParam)) node.setPos(snap(mousePos, Diagram.GridSize, snapToGrid)) if isinstance(node, OntologyEntityNode) or isinstance( node, OntologyEntityResizableNode): self.session.doOpenIRIBuilder(node) elif isinstance(node, FacetNode): self.session.doOpenConstrainingFacetBuilder(node) elif isinstance(node, LiteralNode): self.session.doOpenLiteralBuilder(node) else: self.session.undostack.push(CommandNodeAdd(self, node)) self.sgnItemInsertionCompleted.emit(node, mouseEvent.modifiers()) elif self.mode is DiagramMode.EdgeAdd: ############################################# # EDGE INSERTION ################################# node = first(self.items(mousePos, edges=False)) if node: edge = self.factory.create(Item.valueOf(self.modeParam), source=node) edge.updateEdge(target=mousePos) self.mp_Edge = edge self.addItem(edge) else: # Execute super at first since this may change the diagram # mode: some actions are directly handle by graphics items # (i.e: edge breakpoint move, edge anchor move, node shape # resize) and we need to check whether any of them is being # performed before handling the even locally. super().mousePressEvent(mouseEvent) if self.mode is DiagramMode.Idle: if mouseModifiers & QtCore.Qt.ShiftModifier: ############################################# # LABEL MOVE ################################# item = first( self.items(mousePos, nodes=False, edges=False, labels=True)) if item and item.isMovable(): self.clearSelection() self.mp_Label = item self.mp_LabelPos = item.pos() self.mp_Pos = mousePos self.setMode(DiagramMode.LabelMove) else: ############################################# # ITEM SELECTION ################################# item = first(self.items(mousePos, labels=True)) if item: if item.isLabel(): # If we are hitting a label, check whether the label # is overlapping it's parent item and such item is # also intersecting the current mouse position: if so, # use the parent item as placeholder for the selection. parent = item.parentItem() items = self.items(mousePos) item = parent if parent in items else None if item: if mouseModifiers & QtCore.Qt.ControlModifier: # CTRL => support item multi selection. item.setSelected(not item.isSelected()) else: if self.selectedItems(): # Some elements have been already selected in the # diagram, during a previous mouse press event. if not item.isSelected(): # There are some items selected but we clicked # on a node which is not currently selected, so # make this node the only selected one. self.clearSelection() item.setSelected(True) else: # No item (nodes or edges) is selected and we just # clicked on one so make sure to select this item and # because selectedItems() filters out item Label's, # clear out the selection on the diagram. self.clearSelection() item.setSelected(True) # If we have some nodes selected we need to prepare data for a # possible item move operation: we need to make sure to retrieve # the node below the mouse cursor that will act as as mouse grabber # to compute delta movements for each component in the selection. selected = self.selectedNodes() if selected: self.mp_Node = first( self.items(mousePos, edges=False)) if self.mp_Node: self.mp_NodePos = self.mp_Node.pos() self.mp_Pos = mousePos self.mp_Data = self.setupMove(selected) def mouseMoveEvent(self, mouseEvent): """ Executed when then mouse is moved on the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ mouseButtons = mouseEvent.buttons() mousePos = mouseEvent.scenePos() if mouseButtons & QtCore.Qt.LeftButton: if self.mode is DiagramMode.EdgeAdd: ############################################# # EDGE INSERTION ################################# if self.isEdgeAdd(): statusBar = self.session.statusBar() edge = self.mp_Edge edge.updateEdge(target=mousePos) previousNode = self.mo_Node if previousNode: previousNode.updateNode(selected=False) currentNode = first( self.items(mousePos, edges=False, skip={edge.source})) if currentNode: self.mo_Node = currentNode pvr = self.project.profile.checkEdge( edge.source, edge, currentNode) currentNode.updateNode(selected=False, valid=pvr.isValid()) if not pvr.isValid(): statusBar.showMessage(pvr.message()) else: statusBar.clearMessage() else: statusBar.clearMessage() self.mo_Node = None self.project.profile.reset() elif self.mode is DiagramMode.LabelMove: ############################################# # LABEL MOVE ################################# if self.isLabelMove(): snapToGrid = self.session.action('toggle_grid').isChecked() point = self.mp_LabelPos + mousePos - self.mp_Pos point = snap(point, Diagram.GridSize / 2, snapToGrid) delta = point - self.mp_LabelPos self.mp_Label.setPos(self.mp_LabelPos + delta) else: if self.mode is DiagramMode.Idle: if self.mp_Node: self.setMode(DiagramMode.NodeMove) if self.mode is DiagramMode.NodeMove: ############################################# # ITEM MOVEMENT ################################# if self.isNodeMove(): snapToGrid = self.session.action( 'toggle_grid').isChecked() point = self.mp_NodePos + mousePos - self.mp_Pos point = snap(point, Diagram.GridSize, snapToGrid) delta = point - self.mp_NodePos edges = set() for edge, breakpoints in self.mp_Data['edges'].items(): for i in range(len(breakpoints)): edge.breakpoints[i] = breakpoints[i] + delta for node, data in self.mp_Data['nodes'].items(): edges |= set(node.edges) node.setPos(data['pos'] + delta) for edge, pos in data['anchors'].items(): node.setAnchor(edge, pos + delta) for edge in edges: edge.updateEdge() super().mouseMoveEvent(mouseEvent) def mouseReleaseEvent(self, mouseEvent): """ Executed when the mouse is released from the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ self.project.colour_items_in_case_of_unsatisfiability_or_inconsistent_ontology( ) mouseModifiers = mouseEvent.modifiers() mouseButton = mouseEvent.button() mousePos = mouseEvent.scenePos() if mouseButton == QtCore.Qt.LeftButton: if self.mode is DiagramMode.EdgeAdd: ############################################# # EDGE INSERTION ################################# if self.isEdgeAdd(): edge = self.mp_Edge edge.source.updateNode(selected=False) currentNode = first( self.items(mousePos, edges=False, skip={edge.source})) insertEdge = False if currentNode: currentNode.updateNode(selected=False) pvr = self.project.profile.checkEdge( edge.source, edge, currentNode) if pvr.isValid(): edge.target = currentNode insertEdge = True # We temporarily remove the item from the diagram and we perform the # insertion using the undo command that will also emit the sgnItemAdded # signal hence all the widgets will be notified of the edge insertion. # We do this because while creating the edge we need to display it so the # user knows what he is connecting, but we don't want to truly insert # it till it's necessary (when the mouse is released and the validation # confirms that the generated expression is a valid graphol expression). self.removeItem(edge) if insertEdge: self.session.undostack.push(CommandEdgeAdd(self, edge)) edge.updateEdge() self.clearSelection() self.project.profile.reset() statusBar = self.session.statusBar() statusBar.clearMessage() self.sgnItemInsertionCompleted.emit(edge, mouseModifiers) elif self.mode is DiagramMode.LabelMove: ############################################# # LABEL MOVE ################################# if self.isLabelMove(): pos = self.mp_Label.pos() if self.mp_LabelPos != pos: item = self.mp_Label.parentItem() command = CommandLabelMove(self, item, self.mp_LabelPos, pos) self.session.undostack.push(command) self.setMode(DiagramMode.Idle) elif self.mode is DiagramMode.NodeMove: ############################################# # ITEM MOVEMENT ################################# if self.isNodeMove(): pos = self.mp_Node.pos() if self.mp_NodePos != pos: moveData = self.completeMove(self.mp_Data) self.session.undostack.push( CommandNodeMove(self, self.mp_Data, moveData)) self.setMode(DiagramMode.Idle) elif mouseButton == QtCore.Qt.RightButton: if self.mode is not DiagramMode.SceneDrag: ############################################# # CONTEXTUAL MENU ################################# item = first(self.items(mousePos)) if not item: self.clearSelection() items = [] else: items = self.selectedItems() if item not in items: self.clearSelection() item.setSelected(True) items = [item] self.mp_Pos = mousePos menu = self.session.mf.create(self, items, mousePos) menu.exec_(mouseEvent.screenPos()) super().mouseReleaseEvent(mouseEvent) self.mo_Node = None self.mp_Data = None self.mp_Edge = None self.mp_Label = None self.mp_LabelPos = None self.mp_Node = None self.mp_NodePos = None self.mp_Pos = None ############################################# # SLOTS ################################# @QtCore.pyqtSlot(AbstractNode) def doAddOntologyEntityNode(self, node): """ Add to this diagram a node identified by an IRI :type node: AbstractNode """ if node: command = CommandNodeAdd(self, node) self.session.undostack.beginMacro('node Add - {0}'.format( node.iri)) if command: self.session.undostack.push(command) self.session.undostack.endMacro() #self.addItem(node) @QtCore.pyqtSlot(FacetNode) def doAddOntologyFacetNode(self, node): """ Add to this diagram a node representing a Facet :type node: FacetNode """ if node: command = CommandNodeAdd(self, node) self.session.undostack.beginMacro('node Add - {0}'.format( node.facet)) if command: self.session.undostack.push(command) self.session.undostack.endMacro() @QtCore.pyqtSlot(LiteralNode) def doAddOntologyLiteralNode(self, node): """ Add to this diagram a node representing a Literal :type node: FacetNode """ if node: command = CommandNodeAdd(self, node) self.session.undostack.beginMacro('node Add - {0}'.format( node.literal)) if command: self.session.undostack.push(command) self.session.undostack.endMacro() @QtCore.pyqtSlot('QGraphicsItem') def doNodeIdentification(self, node): """ Perform node identification. :type node: AbstractNode """ if Identity.Neutral in node.identities(): func = lambda x: Identity.Neutral in x.identities() collection = bfs(source=node, filter_on_visit=func) generators = partition(func, collection) excluded = set() strong = set(generators[1]) weak = set(generators[0]) for node in weak: identification = node.identify() if identification: strong = set.union(strong, identification[0]) strong = set.difference(strong, identification[1]) excluded = set.union(excluded, identification[2]) computed = Identity.Neutral identities = set(x.identity() for x in strong) if identities: computed = first(identities) if len(identities) > 1: computed = Identity.Unknown for node in weak - strong - excluded: node.setIdentity(computed) @QtCore.pyqtSlot('QGraphicsScene', 'QGraphicsItem') def onItemAdded(self, _, item): """ Executed whenever a connection is created/removed. :type _: Diagram :type item: AbstractItem """ # Send a font change event to the item to update its font self.sendEvent(item, QtCore.QEvent(QtCore.QEvent.FontChange)) if item.isEdge(): # Execute the node identification procedure only if one of the # endpoints we are connecting is currently identified as NEUTRAL. if (item.source.identity() is Identity.Neutral) ^ ( item.target.identity() is Identity.Neutral): for node in (item.source, item.target): self.sgnNodeIdentification.emit(node) @QtCore.pyqtSlot('QGraphicsScene', 'QGraphicsItem') def onItemRemoved(self, _, item): """ Executed whenever a connection is created/removed. :type _: Diagram :type item: AbstractItem """ if item.isEdge(): # When an edge is removed we may be in the case where # the ontology is split into 2 subgraphs, hence we need # to run the identification procedure on the 2 subgraphs. for node in (item.source, item.target): self.sgnNodeIdentification.emit(node) ############################################# # INTERFACE ################################# def addItem(self, item): """ Add an item to the Diagram (will redraw the item to reflect its status). :type item: AbstractItem """ super().addItem( item ) #TODO a partire da questo momento item.diagram restituisce risultato diverso da None if item.isIRINode(): item.connectSignals() if item.isNode(): item.updateNode() @staticmethod def completeMove(moveData, offset=QtCore.QPointF(0, 0)): """ Complete item movement, given initializated data for a collection of selected nodes. :type moveData: dict :type offset: QPointF :rtype: dict """ return { 'nodes': { node: { 'anchors': {k: v + offset for k, v in node.anchors.items()}, 'pos': node.pos() + offset, } for node in moveData['nodes'] }, 'edges': { x: [p + offset for p in x.breakpoints[:]] for x in moveData['edges'] } } def edge(self, eid): """ Returns the edge matching the given id or None if no edge is found. :type eid: str :rtype: AbstractEdge """ return self.project.edge(self, eid) def edges(self): """ Returns a collection with all the edges in the diagram. :rtype: set """ return self.project.edges(self) def isEdgeAdd(self): """ Returns True if an edge insertion is currently in progress, False otherwise. :rtype: bool """ return self.mode is DiagramMode.EdgeAdd and self.mp_Edge is not None def isLabelMove(self): """ Returns True if a label is currently being moved, False otherwise. :rtype: bool """ return self.mode is DiagramMode.LabelMove and \ self.mp_Label is not None and \ self.mp_LabelPos is not None and \ self.mp_Pos is not None def isNodeMove(self): """ Returns True if a node(s) is currently being moved, False otherwise. :rtype: bool """ return self.mode is DiagramMode.NodeMove and \ self.mp_Data is not None and \ self.mp_Node is not None and \ self.mp_NodePos is not None and \ self.mp_Pos is not None def isEmpty(self): """ Returns True if this diagram containts no element, False otherwise. :rtype: bool """ return len(self.project.items(self)) == 0 def items(self, mixed=None, mode=QtCore.Qt.IntersectsItemShape, **kwargs): """ Returns a collection of items ordered from TOP to BOTTOM. If no argument is supplied, an unordered list containing all the elements in the diagram is returned. :type mixed: T <= QPointF | QRectF | QPolygonF | QPainterPath :type mode: ItemSelectionMode :rtype: list """ if mixed is None: items = super().items() elif isinstance(mixed, QtCore.QPointF): x = mixed.x() - (Diagram.SelectionRadius / 2) y = mixed.y() - (Diagram.SelectionRadius / 2) w = Diagram.SelectionRadius h = Diagram.SelectionRadius items = super().items(QtCore.QRectF(x, y, w, h), mode) else: items = super().items(mixed, mode) return sorted([ x for x in items if (kwargs.get('nodes', True) and x.isNode() or kwargs.get( 'edges', True) and x.isEdge() or kwargs.get('labels', False) and x.isLabel()) and x not in kwargs.get('skip', set()) ], key=lambda i: i.zValue(), reverse=True) def nodes(self): """ Returns a collection with all the nodes in the diagram. :rtype: set """ return self.project.nodes(self) def node(self, nid): """ Returns the node matching the given id or None if no node is found. :type nid: str :rtype: AbstractNode """ return self.project.node(self, nid) def selectedEdges(self, filter_on_edges=lambda x: True): """ Returns the edges selected in the diagram. :type filter_on_edges: callable :rtype: list """ return [ x for x in super().selectedItems() if x.isEdge() and filter_on_edges(x) ] def selectedItems(self, filter_on_items=lambda x: True): """ Returns the items selected in the diagram. :type filter_on_items: callable :rtype: list """ return [ x for x in super().selectedItems() if (x.isNode() or x.isEdge()) and filter_on_items(x) ] def selectedNodes(self, filter_on_nodes=lambda x: True): """ Returns the nodes selected in the diagram. :type filter_on_nodes: callable :rtype: list """ return [ x for x in super().selectedItems() if x.isNode() and filter_on_nodes(x) ] def setMode(self, mode, param=None): """ Set the operational mode. :type mode: DiagramMode :type param: Item """ if self.mode != mode or self.modeParam != param: #LOGGER.debug('Diagram mode changed: mode=%s, param=%s', mode, param) self.mode = mode self.modeParam = param self.sgnModeChanged.emit(mode) @staticmethod def setupMove(selected): """ Compute necessary data to initialize item movement, given a collection of selected nodes. :type selected: T <= list | tuple :rtype: dict """ # Initialize movement data considering only # nodes which are involved in the selection. moveData = { 'nodes': { node: { 'anchors': {k: v for k, v in node.anchors.items()}, 'pos': node.pos(), } for node in selected }, 'edges': {} } # Figure out if the nodes we are moving are sharing edges: # if that's the case, move the edge together with the nodes # (which actually means moving the edge breakpoints). for node in moveData['nodes']: for edge in node.edges: if edge not in moveData['edges']: if edge.other(node).isSelected(): moveData['edges'][edge] = edge.breakpoints[:] return moveData # noinspection PyTypeChecker def visibleRect(self, margin=0): """ Returns a rectangle matching the area of visible items. :type margin: float :rtype: QtCore.QRectF """ items = self.items() if items: x = set() y = set() for item in items: b = item.mapRectToScene(item.boundingRect()) x.update({b.left(), b.right()}) y.update({b.top(), b.bottom()}) return QtCore.QRectF( QtCore.QPointF(min(x) - margin, min(y) - margin), QtCore.QPointF(max(x) + margin, max(y) + margin)) return QtCore.QRectF() def __str__(self): return 'Diagram {}'.format(self.name)