Source code for gpi.edge

#    Copyright (C) 2014  Dignity Health
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#    NO CLINICAL USE.  THE SOFTWARE IS NOT INTENDED FOR COMMERCIAL PURPOSES
#    AND SHOULD BE USED ONLY FOR NON-COMMERCIAL RESEARCH PURPOSES.  THE
#    SOFTWARE MAY NOT IN ANY EVENT BE USED FOR ANY CLINICAL OR DIAGNOSTIC
#    PURPOSES.  YOU ACKNOWLEDGE AND AGREE THAT THE SOFTWARE IS NOT INTENDED FOR
#    USE IN ANY HIGH RISK OR STRICT LIABILITY ACTIVITY, INCLUDING BUT NOT
#    LIMITED TO LIFE SUPPORT OR EMERGENCY MEDICAL OPERATIONS OR USES.  LICENSOR
#    MAKES NO WARRANTY AND HAS NO LIABILITY ARISING FROM ANY USE OF THE
#    SOFTWARE IN ANY HIGH RISK OR STRICT LIABILITY ACTIVITIES.
#
#    The code in this file was modifed/derived from the elasticnodes.py
#    example with the license:
#############################################################################
##
## Copyright (C) 2010 Riverbank Computing Limited.
## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
## All rights reserved.
##
## This file is part of the examples of PyQt.
##
## $QT_BEGIN_LICENSE:BSD$
## You may use this file under the terms of the BSD license as follows:
##
## "Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions are
## met:
##   * Redistributions of source code must retain the above copyright
##     notice, this list of conditions and the following disclaimer.
##   * Redistributions in binary form must reproduce the above copyright
##     notice, this list of conditions and the following disclaimer in
##     the documentation and/or other materials provided with the
##     distribution.
##   * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
##     the names of its contributors may be used to endorse or promote
##     products derived from this software without specific prior written
##     permission.
##
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
## $QT_END_LICENSE$
##
#############################################################################

import math
from gpi import QtCore, QtGui

# gpi
from .defines import EdgeTYPE, GPI_PORT_EVENT
from .logger import manager
from .port import InPort, OutPort

# start logger for this module
log = manager.getLogger(__name__)


[docs]class EdgeTracer(QtGui.QGraphicsLineItem): '''When an edge is deleted it will be replaced with this static object for a few seconds and then remove itself. ''' def __init__(self, graph, destPort, sourcePort): super(EdgeTracer, self).__init__() # show a faux copy of the delete menu menu = QtGui.QMenu() menu.addAction("Delete") # position of pipe end based on port type bindout_y = 5 bindin_y = -1 p1 = self.mapFromItem(sourcePort, 3.5, bindin_y) p2 = self.mapFromItem(destPort, 3.5, bindout_y) pos = graph.mapToGlobal(graph.mapFromScene((p1-p2)/2+p2)) # render the menu without executing it menupixmap = QtGui.QPixmap().grabWidget(menu) # round edges #mask = menupixmap.createMaskFromColor(QtGui.QColor(255, 255, 255), QtCore.Qt.MaskOutColor) #p = QtGui.QPainter(menupixmap) #p.setRenderHint(QtGui.QPainter.Antialiasing) #p.drawRoundedRect(0,0,menupixmap.width(),menupixmap.height(), 5,5) #p.drawPixmap(menupixmap.rect(), mask, mask.rect()) #p.end() # display the menu image (as a dummy menu as its being built) # TODO: this could probably be moved to the FauxMenu self._tracer = QtGui.QLabel() self._tracer.setWindowFlags(QtCore.Qt.Tool | QtCore.Qt.FramelessWindowHint) self._tracer.move(pos) self._tracer.setPixmap(menupixmap) self._tracer.show() self._tracer.raise_() # draw a faux selected line line = QtCore.QLineF(p1,p2) self.setPen(QtGui.QPen(QtGui.QColor(QtCore.Qt.red), 2, QtCore.Qt.DashLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)) self.setLine(line) self.setZValue(0) # cleanup both menu item and line by removing from scene (parent). self._timer = QtCore.QTimer() self._timer.singleShot(300, lambda: graph.scene().removeItem(self))
[docs]class Edge(QtGui.QGraphicsLineItem): """Provides the connection graphic and logic for nodes. -No enforcement, just methods to retrieve connected nodes. """ Type = EdgeTYPE def __init__(self, sourcePort, destPort): super(Edge, self).__init__() self.sourcePoint = QtCore.QPointF() self.destPoint = QtCore.QPointF() self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable) self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) self.setAcceptedMouseButtons(QtCore.Qt.NoButton) self.source = sourcePort self.dest = destPort self.source.addEdge(self) self.dest.addEdge(self) self.adjust() self.setZValue(1) self.setAcceptHoverEvents(True) self._beingHovered = False def connectedPortIsHovered(self): if self.source.BeingHovered() or self.dest.BeingHovered(): self.setZValue(100) return True else: self.setZValue(1) return False def hoverEnterEvent(self, event): self._beingHovered = True self.setZValue(100) self.update() def hoverLeaveEvent(self, event): self._beingHovered = False self.setZValue(1) self.update() def type(self): return Edge.Type def sourcePort(self): if isinstance(self.source, InPort): log.critical("Edge: WARNING: sourcePort() is somehow an InPort instance! (Src: \'"+str(self.source.getName())+"\', Dst: \'"+str(self.dest.getName())+"\')") return self.source def isCyclicConnection(self): return self.sourcePort().allowsCyclicConn() or self.destPort().allowsCyclicConn() def getSourceCoords(self): c = {} c['nodeID'] = self.sourcePort().getNodeID() c['portID'] = self.sourcePort().getID() c['portName'] = self.sourcePort().portTitle c['portNum'] = self.sourcePort().portNum return c def getDestCoords(self): c = {} c['nodeID'] = self.destPort().getNodeID() c['portID'] = self.destPort().getID() c['portName'] = self.destPort().portTitle c['portNum'] = self.destPort().portNum return c def getCoords(self): # EDGE SETTINGS c = {} c['src'] = self.getSourceCoords() c['dest'] = self.getDestCoords() return c def sourceNode(self): return self.source.getNode() def setSourcePort(self, port): self.source = port self.adjust() def destPort(self): if isinstance(self.dest, OutPort): log.critical("Edge: WARNING: destPort() is somehow an InPort instance!") return self.dest def destNode(self): return self.dest.getNode() def setDestPort(self, port): self.dest = port self.adjust() def detachSelf(self, update=True, tracer=False): # EDGE '''update: triggers a processing event for OPTIONAL node obligation''' self.source.detachEdge(self) self.dest.detachEdge(self) # add tracer if tracer: self.dest.getNode().graph.scene().addItem(EdgeTracer(self.dest.getNode().graph, self.source, self.dest)) if update: self.dest.getNode().graph.calcNodeHierarchy() self.dest.getNode().setEventStatus({GPI_PORT_EVENT: self.dest.portTitle}) if self.dest.getNode().graph.inIdleState(): self.dest.getNode().graph._switchSig.emit('check') self.dest.getNode().graph.viewAndSceneForcedUpdate() def adjust(self): if not self.source or not self.dest: return # position of pipe end based on port type bindout_y = 5 bindin_y = 0 if isinstance(self.source, InPort): line = QtCore.QLineF(self.mapFromItem(self.source, 3.5, bindin_y), self.mapFromItem(self.dest, 3.5, bindout_y)) else: line = QtCore.QLineF(self.mapFromItem(self.source, 3.5, bindout_y), self.mapFromItem(self.dest, 3.5, bindin_y)) self.prepareGeometryChange() self.sourcePoint = line.p1() self.destPoint = line.p2() def boundingRect(self): if not self.source or not self.dest: return QtCore.QRectF() penWidth = 2.0 # extra padding for edge text #if self._beingHovered: # extra = (penWidth + 10.0) / 2.0 #else: extra = (penWidth + 30.0) / 2.0 # http://lists.trolltech.com/qt-interest/2000-08/thread00439-0.html # bound = QRect(QPoint(min(p0.x(),p1.x(),p2.x(),p3.x()), # min(p0.y(),p1.y(),p2.y(),p3.y())), # QPoint(max(p0.x(),p1.x(),p2.x(),p3.x()), # mxa(p0.y(),p1.y(),p2.y(),p3.y()))); return QtCore.QRectF(self.sourcePoint, QtCore.QSizeF( self.destPoint.x() - self.sourcePoint.x(), self.destPoint.y( ) - self.sourcePoint.y( ))).normalized().adjusted(-extra, -extra, extra, extra) def shape(self): # hitbox for selecting path = super(Edge, self).shape() delta = QtCore.QPointF(3,3) # padding to make it thicker line = QtGui.QPolygonF([self.sourcePoint+delta, self.destPoint+delta, self.destPoint-delta, self.sourcePoint-delta]) path.addPolygon(line) return path def paint(self, painter, option, widget): # EDGE if not self.source or not self.dest: return # Draw the line itself. line = QtCore.QLineF(self.sourcePoint, self.destPoint) if line.length() == 0.0: return if self.isSelected() or self._beingHovered or self.connectedPortIsHovered(): fade = QtGui.QColor(QtCore.Qt.red) fade.setAlpha(200) #painter.setPen(QtGui.QPen(QtCore.Qt.red, 1, QtCore.Qt.DashLine, painter.setPen(QtGui.QPen(fade, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)) elif self.isCyclicConnection(): painter.setPen(QtGui.QPen(QtCore.Qt.red, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)) else: fade = QtGui.QColor(QtCore.Qt.black) fade.setAlpha(150) #painter.setPen(QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, painter.setPen(QtGui.QPen(fade, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)) painter.drawLine(line) x = (line.x1()+line.x2())/2.0 y = (line.y1()+line.y2())/2.0 xa = (line.x1()-line.x2()) ya = (line.y1()-line.y2()) m = math.sqrt(xa*xa + ya*ya) a = math.atan2(ya, xa)*180.0/math.pi buf = self.source.getDataString() if self._beingHovered: f = QtGui.QFont("times", 8) else: f = QtGui.QFont("times", 6) fm = QtGui.QFontMetricsF(f) bw = fm.width(buf) bw2 = -bw*0.5 #bh = fm.height() # bezier curves if False: sa = (a+90.)*0.5 path = QtGui.QPainterPath(line.p1()) path.cubicTo(x-sa, y-sa, x+sa, y+sa, line.x2(), line.y2()) painter.drawPath(path) # bezier curves, change direction on the angle if False: sa = (a+90.)*0.5 if a > 90 or a < -90: path = QtGui.QPainterPath(line.p1()) path.cubicTo(x-sa, y-sa, x+sa, y+sa, line.x2(), line.y2()) painter.drawPath(path) else: path = QtGui.QPainterPath(line.p1()) path.cubicTo(x+sa, y+sa, x-sa, y-sa, line.x2(), line.y2()) painter.drawPath(path) painter.setFont(f) if self._beingHovered: painter.setPen(QtGui.QPen(QtCore.Qt.red, 1)) else: painter.setPen(QtGui.QPen(QtCore.Qt.darkGray, 1)) painter.save() painter.translate(QtCore.QPointF(x, y)) if m > bw*1.1 or self._beingHovered: if a > 90 or a < -90: painter.rotate(a+180.0) painter.drawText(QtCore.QPointF(bw2, -2.0), buf) else: painter.rotate(a) painter.drawText(QtCore.QPointF(bw2, -2.0), buf) else: painter.drawText(QtCore.QPointF(bw2, -2.0), '') painter.restore()