# -*- coding: utf-8 -*- ########################################################################## # # # Eddy: a graphical editor for the specification of Graphol ontologies # # Copyright (C) 2015 Daniele Pantaleone <danielepantaleone@me.com> # # # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation, either version 3 of the License, or # # (at your option) any later version. # # # # This program is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program. If not, see <http://www.gnu.org/licenses/>. # # # # ##################### ##################### # # # # Graphol is developed by members of the DASI-lab group of the # # Dipartimento di Ingegneria Informatica, Automatica e Gestionale # # A.Ruberti at Sapienza University of Rome: http://www.dis.uniroma1.it # # # # - Domenico Lembo <lembo@dis.uniroma1.it> # # - Valerio Santarelli <santarelli@dis.uniroma1.it> # # - Domenico Fabio Savo <savo@dis.uniroma1.it> # # - Daniele Pantaleone <pantaleone@dis.uniroma1.it> # # - Marco Console <console@dis.uniroma1.it> # # # ########################################################################## from abc import ABCMeta, abstractmethod from itertools import permutations from PyQt5 import QtCore from PyQt5 import QtGui from eddy.core.commands.edges import CommandEdgeAnchorMove from eddy.core.commands.edges import CommandEdgeBreakpointAdd from eddy.core.commands.edges import CommandEdgeBreakpointMove from eddy.core.datatypes.misc import DiagramMode from eddy.core.functions.geometry import distance, projection from eddy.core.functions.misc import snap from eddy.core.items.common import AbstractItem from eddy.core.items.common import Polygon class AbstractEdge(AbstractItem): """ Base class for all the diagram edges. """ __metaclass__ = ABCMeta Prefix = 'e' def __init__(self, source, target=None, breakpoints=None, **kwargs): """ Initialize the edge. :type source: AbstractNode :type target: AbstractNode :type breakpoints: list """ super().__init__(**kwargs) self.source = source self.target = target self.anchors = {} # {AbstractNode: Polygon} self.breakpoints = breakpoints or [] # [QtCore.QPointF] self.handles = [] # [Polygon] self.head = Polygon(QtGui.QPolygonF()) self.path = Polygon(QtGui.QPainterPath()) self.selection = Polygon(QtGui.QPainterPath()) self.mp_AnchorNode = None self.mp_AnchorNodePos = None self.mp_BreakPoint = None self.mp_BreakPointPos = None self.mp_Pos = None self.setAcceptHoverEvents(True) self.setCacheMode(AbstractItem.DeviceCoordinateCache) self.setFlag(AbstractItem.ItemIsSelectable, True) ############################################# # INTERFACE ################################# def anchorAt(self, point): """ Returns the key of the anchor whose handle is being pressed. :type point: AbstractNode """ size = QtCore.QPointF(3, 3) area = QtCore.QRectF(point - size, point + size) for k, v, in self.anchors.items(): if v.geometry().intersects(area): return k return None def anchorMove(self, node, mousePos): """ Move the selected anchor point. :type node: AbstractNode :type mousePos: QtCore.QPointF """ nodePos = node.pos() snapToGrid = self.session.action('toggle_grid').isChecked() mousePos = snap(mousePos, self.diagram.GridSize, snapToGrid) path = self.mapFromItem(node, node.painterPath()) if path.contains(mousePos): # Mouse is inside the shape => use this position as anchor point. pos = nodePos if distance(mousePos, nodePos) < 10.0 else mousePos else: # Mouse is outside the shape => use the intersection point as anchor point. pos = node.intersection(QtCore.QLineF(mousePos, nodePos)) for pair in set(permutations([-1, -1, 0, 0, 1, 1], 2)): p = pos + QtCore.QPointF(*pair) if path.contains(p): pos = p break node.setAnchor(self, pos) def breakPointAdd(self, mousePos): """ Create a new breakpoint from the given mouse position returning its index. :type mousePos: QtCore.QPointF :rtype: int """ index = 0 point = None between = None shortest = 999 source = self.source.anchor(self) target = self.target.anchor(self) points = [source] + self.breakpoints + [target] # Estimate between which breakpoints the new one is being added. for subpath in (QtCore.QLineF(points[i], points[i + 1]) for i in range(len(points) - 1)): dis, pos = projection(subpath, mousePos) if dis < shortest: point = pos shortest = dis between = subpath.p1(), subpath.p2() # If there is no breakpoint the new one will be appended. for i, breakpoint in enumerate(self.breakpoints): if breakpoint == between[1]: # In case the new breakpoint is being added between # the source node of this edge and the last breakpoint. index = i break if breakpoint == between[0]: # In case the new breakpoint is being added between # the last breakpoint and the target node of this edge. index = i + 1 break self.session.undostack.push(CommandEdgeBreakpointAdd(self.diagram, self, index, point)) return index def breakPointAt(self, point): """ Returns the index of the breakpoint whose handle is being pressed. :type point: QtCore.QPointF :rtype: int """ size = QtCore.QPointF(3, 3) area = QtCore.QRectF(point - size, point + size) for polygon in self.handles: if polygon.geometry().intersects(area): return self.handles.index(polygon) return None def breakPointMove(self, breakpoint, mousePos): """ Move the selected breakpoint. :type breakpoint: int :type mousePos: QtCore.QPointF """ snapToGrid = self.session.action('toggle_grid').isChecked() self.breakpoints[breakpoint] = snap(mousePos, self.diagram.GridSize, snapToGrid) def canDraw(self): """ Check whether we have to draw the edge or not. :rtype: bool """ if not self.diagram: return False if self.target: source = self.source target = self.target sp = self.mapFromItem(source, source.painterPath()) tp = self.mapFromItem(target, target.painterPath()) if sp.intersects(tp): for point in self.breakpoints: if not source.contains(self.mapToItem(source, point)): if not target.contains(self.mapToItem(target, point)): return True return False return True @abstractmethod def copy(self, diagram): """ Create a copy of the current item. :type diagram: Diagram """ pass def createPath(self, source, target, points): """ Returns a list of QtCore.QLineF instance representing all the visible edge pieces. Subpaths which are obscured by the source or target shape are excluded by this method. :type source: AbstractNode :type target: AbstractNode :type points: list :rtype: list """ # Get the source node painter path (the source node is always available). A = self.mapFromItem(source, source.painterPath()) B = self.mapFromItem(target, target.painterPath()) if target else None # Exclude all the "subpaths" which are not visible (obscured by the shapes). return [x for x in (QtCore.QLineF(points[i], points[i + 1]) for i in range(len(points) - 1)) \ if (not A.contains(x.p1()) or not A.contains(x.p2())) and \ (not B or (not B.contains(x.p1()) or not B.contains(x.p2())))] def isSwapAllowed(self): """ Returns True if this edge can be swapped, False otherwise. :rtype: bool """ return self.project.profile.checkEdge(self.target, self, self.source).isValid() def moveBy(self, x, y): """ Move the edge by the given deltas. :type x: float :type y: float """ self.breakpoints = [p + QtCore.QPointF(x, y) for p in self.breakpoints] def other(self, node): """ Returns the opposite endpoint of the given node. :raise AttributeError: if the given node is not an endpoint of this edge. :type node: AttributeNode :rtype: Node """ if node is self.source: return self.target elif node is self.target: return self.source raise AttributeError('node {0} is not attached to edge {1}'.format(node, self)) def updateEdge(self, selected=None, visible=None, breakpoint=None, anchor=None, **kwargs): """ Update the current edge. :type selected: bool :type visible: bool :type breakpoint: int :type anchor: AbstractNode """ if selected is None: selected = self.isSelected() if visible is None: visible = self.canDraw() source = self.source target = self.target ## ANCHORS (GEOMETRY) --> NB: THE POINTS ARE IN THE ENDPOINTS if source and target: p = source.anchor(self) self.anchors[source] = Polygon(QtCore.QRectF(p.x() - 4, p.y() - 4, 8, 8)) p = target.anchor(self) self.anchors[target] = Polygon(QtCore.QRectF(p.x() - 4, p.y() - 4, 8, 8)) ## BREAKPOINTS (GEOMETRY) self.handles = [Polygon(QtCore.QRectF(p.x() - 4, p.y() - 4, 8, 8)) for p in self.breakpoints] ## ANCHORS + BREAKPOINTS + SELECTION (BRUSH + PEN) if visible and selected: apBrush = QtGui.QBrush(QtGui.QColor(66, 165, 245, 255)) apPen = QtGui.QPen(QtGui.QBrush(QtGui.QColor(0, 0, 0, 255)), 1.1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin) bpBrush = QtGui.QBrush(QtGui.QColor(66, 165, 245, 255)) bpPen = QtGui.QPen(QtGui.QBrush(QtGui.QColor(0, 0, 0, 255)), 1.1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin) selectionBrush = QtGui.QBrush(QtGui.QColor(248, 255, 72, 255)) else: apBrush = QtGui.QBrush(QtCore.Qt.NoBrush) apPen = QtGui.QPen(QtCore.Qt.NoPen) bpBrush = QtGui.QBrush(QtCore.Qt.NoBrush) bpPen = QtGui.QPen(QtCore.Qt.NoPen) selectionBrush = QtGui.QBrush(QtCore.Qt.NoBrush) for polygon in self.anchors.values(): polygon.setBrush(apBrush) polygon.setPen(apPen) for polygon in self.handles: polygon.setBrush(bpBrush) polygon.setPen(bpPen) self.selection.setBrush(selectionBrush) ## Z-VALUE (DEPTH) try: zValue = max(*(x.zValue() for x in self.collidingItems())) + 0.1 except TypeError: zValue = source.zValue() + 0.1 if source.label: zValue = max(zValue, source.label.zValue()) if target: zValue = max(zValue, target.zValue()) if target.label: zValue = max(zValue, target.label.zValue()) self.setZValue(zValue) ## FORCE CACHE REGENERATION self.setCacheMode(AbstractItem.NoCache) self.setCacheMode(AbstractItem.DeviceCoordinateCache) ############################################# # EVENTS ################################# def hoverEnterEvent(self, hoverEvent): """ Executed when the mouse enters the shape (NOT PRESSED). :type hoverEvent: QGraphicsSceneHoverEvent """ self.setCursor(QtCore.Qt.PointingHandCursor) super().hoverEnterEvent(hoverEvent) def hoverMoveEvent(self, hoverEvent): """ Executed when the mouse moves over the shape (NOT PRESSED). :type hoverEvent: QGraphicsSceneHoverEvent """ self.setCursor(QtCore.Qt.PointingHandCursor) super().hoverMoveEvent(hoverEvent) def hoverLeaveEvent(self, hoverEvent): """ Executed when the mouse leaves the shape (NOT PRESSED). :type hoverEvent: QGraphicsSceneHoverEvent """ self.setCursor(QtCore.Qt.ArrowCursor) super().hoverLeaveEvent(hoverEvent) def itemChange(self, change, value): """ Executed whenever the item change state. :type change: GraphicsItemChange :type value: QVariant :rtype: QVariant """ if change == AbstractEdge.ItemSelectedHasChanged: self.updateEdge(selected=value) return super().itemChange(change, value) def mousePressEvent(self, mouseEvent): """ Executed when the mouse is pressed on the selection box. :type mouseEvent: QGraphicsSceneMouseEvent """ self.mp_Pos = mouseEvent.pos() if self.diagram.mode is DiagramMode.Idle: # Check first if we need to start an anchor point movement: we need # to evaluate anchor points first because we may be in the situation # where we are trying to select the anchor point, but if the code for # breakpoint retrieval is executed first, no breakpoint is found # and hence a new one will be added upon mouseMoveEvent. anchorNode = self.anchorAt(self.mp_Pos) if anchorNode is not None: self.diagram.clearSelection() self.diagram.setMode(DiagramMode.EdgeAnchorMove) self.setSelected(True) self.mp_AnchorNode = anchorNode self.mp_AnchorNodePos = QtCore.QPointF(anchorNode.anchor(self)) self.updateEdge(selected=True, anchor=anchorNode) else: breakPoint = self.breakPointAt(self.mp_Pos) if breakPoint is not None: self.diagram.clearSelection() self.diagram.setMode(DiagramMode.EdgeBreakPointMove) self.setSelected(True) self.mp_BreakPoint = breakPoint self.mp_BreakPointPos = QtCore.QPointF(self.breakpoints[breakPoint]) self.updateEdge(selected=True, breakpoint=breakPoint) super().mousePressEvent(mouseEvent) # noinspection PyTypeChecker def mouseMoveEvent(self, mouseEvent): """ Executed when the mouse is being moved over the item while being pressed. :type mouseEvent: QGraphicsSceneMouseEvent """ mousePos = mouseEvent.pos() if self.diagram.mode is DiagramMode.EdgeAnchorMove: self.anchorMove(self.mp_AnchorNode, mousePos) self.updateEdge() else: if self.diagram.mode is DiagramMode.Idle: try: # If we are still idle we didn't succeeded in # selecting a breakpoint so we need to create # a new one and switch the operational mode. breakPoint = self.breakPointAdd(self.mp_Pos) except: pass else: self.diagram.clearSelection() self.diagram.setMode(DiagramMode.EdgeBreakPointMove) self.setSelected(True) self.mp_BreakPoint = breakPoint self.mp_BreakPointPos = QtCore.QPointF(self.breakpoints[breakPoint]) if self.diagram.mode is DiagramMode.EdgeBreakPointMove: self.breakPointMove(self.mp_BreakPoint, mousePos) self.updateEdge() def mouseReleaseEvent(self, mouseEvent): """ Executed when the mouse is released from the selection box. :type mouseEvent: QGraphicsSceneMouseEvent """ if self.diagram.mode is DiagramMode.EdgeAnchorMove: anchorNode = self.mp_AnchorNode anchorNodePos = QtCore.QPointF(anchorNode.anchor(self)) if anchorNodePos != self.mp_AnchorNodePos: data = {'undo': self.mp_AnchorNodePos, 'redo': anchorNodePos} self.session.undostack.push(CommandEdgeAnchorMove(self.diagram, self, anchorNode, data)) elif self.diagram.mode is DiagramMode.EdgeBreakPointMove: breakPoint = self.mp_BreakPoint breakPointPos = self.breakpoints[breakPoint] if breakPointPos != self.mp_BreakPointPos: data = {'undo': self.mp_BreakPointPos, 'redo': breakPointPos} self.session.undostack.push(CommandEdgeBreakpointMove(self.diagram, self, breakPoint, data)) self.mp_AnchorNode = None self.mp_AnchorNodePos = None self.mp_BreakPoint = None self.mp_BreakPointPos = None self.mp_Pos = None self.diagram.setMode(DiagramMode.Idle) self.updateEdge()