├── tests ├── __init__.py ├── twoNodes.py └── bigGraph.py ├── pyflowgraph ├── __init__.py ├── selection_rect.py ├── graph_view_widget.py ├── connection.py ├── mouse_grabber.py ├── node.py ├── port.py └── graph_view.py ├── setup.cfg ├── README.rst ├── .gitignore ├── LICENSE.txt └── setup.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyflowgraph/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Pyflowgraph 2 | =========== 3 | 4 | An interactive data flow graph editor. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python modules. 2 | *.pyc 3 | 4 | # Setuptools distribution folder. 5 | /dist/ 6 | /build/ 7 | 8 | # Python egg metadata, regenerated from source files by setuptools. 9 | /*.egg-info 10 | 11 | # vim save files 12 | *.swp 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2017, Eric Thivierge. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 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. 8 | 9 | Neither the name of Kraken nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | 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 HOLDER 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. 12 | -------------------------------------------------------------------------------- /pyflowgraph/selection_rect.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Copyright 2015-2017 Eric Thivierge 4 | # 5 | 6 | from qtpy import QtGui, QtWidgets, QtCore 7 | 8 | 9 | class SelectionRect(QtWidgets.QGraphicsWidget): 10 | __backgroundColor = QtGui.QColor(100, 100, 100, 50) 11 | __pen = QtGui.QPen(QtGui.QColor(25, 25, 25), 1.0, QtCore.Qt.DashLine) 12 | 13 | def __init__(self, graph, mouseDownPos): 14 | super(SelectionRect, self).__init__() 15 | self.setZValue(-1) 16 | 17 | self.__graph = graph 18 | self.__graph.scene().addItem(self) 19 | self.__mouseDownPos = mouseDownPos 20 | self.setPos(self.__mouseDownPos) 21 | self.resize(0, 0) 22 | 23 | def setDragPoint(self, dragPoint): 24 | topLeft = QtCore.QPointF(self.__mouseDownPos) 25 | bottomRight = QtCore.QPointF(dragPoint) 26 | if dragPoint.x() < self.__mouseDownPos.x(): 27 | topLeft.setX(dragPoint.x()) 28 | bottomRight.setX(self.__mouseDownPos.x()) 29 | if dragPoint.y() < self.__mouseDownPos.y(): 30 | topLeft.setY(dragPoint.y()) 31 | bottomRight.setY(self.__mouseDownPos.y()) 32 | self.setPos(topLeft) 33 | self.resize(bottomRight.x() - topLeft.x(), bottomRight.y() - topLeft.y()) 34 | 35 | 36 | def paint(self, painter, option, widget): 37 | rect = self.windowFrameRect() 38 | painter.setBrush(self.__backgroundColor) 39 | painter.setPen(self.__pen) 40 | painter.drawRect(rect) 41 | 42 | def destroy(self): 43 | self.__graph.scene().removeItem(self) 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from setuptools import setup, find_packages 4 | 5 | long_description = """An interactive data flow graph editor.""" 6 | 7 | setup(name='pyflowgraph', 8 | version='0.0.3', 9 | description='An interactive data flow graph editor', 10 | long_description=long_description, 11 | url='https://github.com/EricTRocks/pyflowgraph', 12 | author='Eric Thivierge', 13 | author_email='ethivierge@gmail.com', 14 | license='BSD 3-clause "New" or "Revised" License', 15 | classifiers=[ 16 | # How mature is this project? Common values are 17 | # 3 - Alpha 18 | # 4 - Beta 19 | # 5 - Production/Stable 20 | 'Development Status :: 3 - Alpha', 21 | 22 | # Indicate who your project is intended for 23 | 'Intended Audience :: Developers', 24 | 'Topic :: Software Development :: User Interfaces', 25 | 26 | # Pick your license as you wish (should match "license" above) 27 | 'License :: OSI Approved :: BSD License', 28 | 29 | # Specify the Python versions you support here. In particular, ensure 30 | # that you indicate whether you support Python 2, Python 3 or both. 31 | 'Programming Language :: Python :: 2', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3.4', 35 | 'Programming Language :: Python :: 3.5', 36 | ], 37 | keywords='data flow graph', 38 | packages=find_packages(exclude=['tests']), 39 | install_requires=['PySide>=1.2.2,<1.2.4','qtpy','six','future'], 40 | zip_safe=False) 41 | 42 | -------------------------------------------------------------------------------- /tests/twoNodes.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Copyright 2015-2017 Eric Thivierge 4 | # 5 | import sys 6 | from qtpy import QtGui, QtWidgets, QtCore 7 | 8 | # Add the pyflowgraph module to the current environment if it does not already exist 9 | import imp 10 | try: 11 | imp.find_module('pyflowgraph') 12 | found = True 13 | except ImportError: 14 | import os, sys 15 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", ".."))) 16 | 17 | from pyflowgraph.graph_view import GraphView 18 | from pyflowgraph.graph_view_widget import GraphViewWidget 19 | from pyflowgraph.node import Node 20 | from pyflowgraph.port import InputPort, OutputPort, IOPort 21 | 22 | 23 | app = QtWidgets.QApplication(sys.argv) 24 | 25 | widget = GraphViewWidget() 26 | graph = GraphView(parent=widget) 27 | 28 | node1 = Node(graph, 'Short') 29 | node1.addPort(InputPort(node1, graph, 'InPort1', QtGui.QColor(128, 170, 170, 255), 'MyDataX')) 30 | node1.addPort(InputPort(node1, graph, 'InPort2', QtGui.QColor(128, 170, 170, 255), 'MyDataX')) 31 | node1.addPort(OutputPort(node1, graph, 'OutPort', QtGui.QColor(32, 255, 32, 255), 'MyDataY')) 32 | node1.addPort(IOPort(node1, graph, 'IOPort1', QtGui.QColor(32, 255, 32, 255), 'MyDataY')) 33 | node1.addPort(IOPort(node1, graph, 'IOPort2', QtGui.QColor(32, 255, 32, 255), 'MyDataY')) 34 | node1.setGraphPos(QtCore.QPointF( -100, 0 )) 35 | 36 | graph.addNode(node1) 37 | 38 | node2 = Node(graph, 'ReallyLongLabel') 39 | node2.addPort(InputPort(node2, graph, 'InPort1', QtGui.QColor(128, 170, 170, 255), 'MyDataY')) 40 | node2.addPort(InputPort(node2, graph, 'InPort2', QtGui.QColor(128, 170, 170, 255), 'MyDataX')) 41 | node2.addPort(OutputPort(node2, graph, 'OutPort', QtGui.QColor(32, 255, 32, 255), 'MyDataY')) 42 | node2.addPort(IOPort(node2, graph, 'IOPort1', QtGui.QColor(32, 255, 32, 255), 'MyDataY')) 43 | node2.addPort(IOPort(node2, graph, 'IOPort2', QtGui.QColor(32, 255, 32, 255), 'MyDataY')) 44 | node2.setGraphPos(QtCore.QPointF( 100, 0 )) 45 | 46 | graph.addNode(node2) 47 | graph.connectPorts(node1, 'OutPort', node2, 'InPort1') 48 | 49 | widget.setGraphView(graph) 50 | widget.show() 51 | 52 | sys.exit(app.exec_()) 53 | -------------------------------------------------------------------------------- /tests/bigGraph.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Copyright 2015-2017 Eric Thivierge 4 | # 5 | import sys 6 | from qtpy import QtGui, QtWidgets, QtCore 7 | 8 | # Add the pyflowgraph module to the current environment if it does not already exist 9 | import imp 10 | try: 11 | imp.find_module('pyflowgraph') 12 | found = True 13 | except ImportError: 14 | import os, sys 15 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", ".."))) 16 | 17 | from pyflowgraph.graph_view import GraphView 18 | from pyflowgraph.graph_view_widget import GraphViewWidget 19 | from pyflowgraph.node import Node 20 | from pyflowgraph.port import InputPort, OutputPort, IOPort 21 | 22 | print(GraphView) 23 | 24 | app = QtWidgets.QApplication(sys.argv) 25 | 26 | widget = GraphViewWidget() 27 | graph = GraphView(parent=widget) 28 | 29 | # generate a diamod shape graph. 30 | totalCount = 0 31 | def generateNodes(count, offset, depth): 32 | for i in range(count): 33 | node1 = Node(graph, 'node' + str(depth) + str(i)) 34 | node1.addPort(InputPort(node1, graph, 'InPort', QtGui.QColor(128, 170, 170, 255), 'MyDataX')) 35 | node1.addPort(OutputPort(node1, graph, 'OutPort', QtGui.QColor(32, 255, 32, 255), 'MyDataX')) 36 | node1.setGraphPos(QtCore.QPointF(offset, i * 80 )) 37 | 38 | graph.addNode(node1) 39 | 40 | global totalCount 41 | totalCount += 1 42 | 43 | if depth < 6: 44 | generateNodes( count * 2, offset+160, depth+1) 45 | 46 | for i in range(count): 47 | graph.connectPorts('node' + str(depth) + str(i), 'OutPort', 'node' + str(depth+1) + str(i*2), 'InPort') 48 | graph.connectPorts('node' + str(depth) + str(i), 'OutPort', 'node' + str(depth+1) + str(i*2+1), 'InPort') 49 | elif depth < 12: 50 | generateNodes( int(count / 2), offset+160, depth+1) 51 | 52 | for i in range(count//2): 53 | graph.connectPorts('node' + str(depth) + str(i), 'OutPort', 'node' + str(depth+1) + str(int(i)), 'InPort') 54 | 55 | 56 | generateNodes( 1, 0, 0) 57 | print("totalCount:" + str(totalCount)) 58 | 59 | widget.setGraphView(graph) 60 | widget.show() 61 | 62 | sys.exit(app.exec_()) 63 | -------------------------------------------------------------------------------- /pyflowgraph/graph_view_widget.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Copyright 2015-2017 Eric Thivierge 4 | # 5 | 6 | import sys 7 | from qtpy import QtGui, QtWidgets, QtCore 8 | 9 | from .graph_view import GraphView 10 | 11 | class GraphViewWidget(QtWidgets.QWidget): 12 | 13 | rigNameChanged = QtCore.Signal() 14 | 15 | def __init__(self, parent=None): 16 | 17 | # constructors of base classes 18 | super(GraphViewWidget, self).__init__(parent) 19 | self.openedFile = None 20 | self.setObjectName('graphViewWidget') 21 | self.setAttribute(QtCore.Qt.WA_WindowPropagation, True) 22 | 23 | 24 | def setGraphView(self, graphView): 25 | 26 | self.graphView = graphView 27 | 28 | # Setup Layout 29 | layout = QtWidgets.QVBoxLayout(self) 30 | layout.addWidget(self.graphView) 31 | self.setLayout(layout) 32 | 33 | ######################### 34 | ## Setup hotkeys for the following actions. 35 | deleteShortcut = QtWidgets.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Delete), self) 36 | deleteShortcut.activated.connect(self.graphView.deleteSelectedNodes) 37 | 38 | frameShortcut = QtWidgets.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_F), self) 39 | frameShortcut.activated.connect(self.graphView.frameSelectedNodes) 40 | 41 | frameShortcut = QtWidgets.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_A), self) 42 | frameShortcut.activated.connect(self.graphView.frameAllNodes) 43 | 44 | 45 | def getGraphView(self): 46 | return self.graphView 47 | 48 | 49 | 50 | if __name__ == "__main__": 51 | app = QtWidgets.QApplication(sys.argv) 52 | 53 | widget = GraphViewWidget() 54 | graph = GraphView(parent=widget) 55 | 56 | from .node import Node 57 | from .port import InputPort, OutputPort, IOPort 58 | 59 | node1 = Node(graph, 'Short') 60 | node1.addPort(InputPort(node1, graph, 'InPort1', QtGui.QColor(128, 170, 170, 255), 'MyDataX')) 61 | node1.addPort(InputPort(node1, graph, 'InPort2', QtGui.QColor(128, 170, 170, 255), 'MyDataX')) 62 | node1.addPort(OutputPort(node1, graph, 'OutPort', QtGui.QColor(32, 255, 32, 255), 'MyDataY')) 63 | node1.addPort(IOPort(node1, graph, 'IOPort1', QtGui.QColor(32, 255, 32, 255), 'MyDataY')) 64 | node1.addPort(IOPort(node1, graph, 'IOPort2', QtGui.QColor(32, 255, 32, 255), 'MyDataY')) 65 | node1.setGraphPos(QtCore.QPointF( -100, 0 )) 66 | 67 | graph.addNode(node1) 68 | 69 | node2 = Node(graph, 'ReallyLongLabel') 70 | node2.addPort(InputPort(node2, graph, 'InPort1', QtGui.QColor(128, 170, 170, 255), 'MyDataX')) 71 | node2.addPort(InputPort(node2, graph, 'InPort2', QtGui.QColor(128, 170, 170, 255), 'MyDataX')) 72 | node2.addPort(OutputPort(node2, graph, 'OutPort', QtGui.QColor(32, 255, 32, 255), 'MyDataY')) 73 | node2.addPort(IOPort(node2, graph, 'IOPort1', QtGui.QColor(32, 255, 32, 255), 'MyDataY')) 74 | node2.addPort(IOPort(node2, graph, 'IOPort2', QtGui.QColor(32, 255, 32, 255), 'MyDataY')) 75 | node2.setGraphPos(QtCore.QPointF( 100, 0 )) 76 | 77 | graph.addNode(node2) 78 | 79 | widget.setGraphView(graph) 80 | widget.show() 81 | 82 | sys.exit(app.exec_()) 83 | -------------------------------------------------------------------------------- /pyflowgraph/connection.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Copyright 2015-2017 Eric Thivierge 4 | # 5 | 6 | from qtpy import QtGui, QtWidgets, QtCore 7 | 8 | 9 | class Connection(QtWidgets.QGraphicsPathItem): 10 | __defaultPen = QtGui.QPen(QtGui.QColor(168, 134, 3), 1.5) 11 | 12 | def __init__(self, graph, srcPortCircle, dstPortCircle): 13 | super(Connection, self).__init__() 14 | 15 | self.__graph = graph 16 | self.__srcPortCircle = srcPortCircle 17 | self.__dstPortCircle = dstPortCircle 18 | penStyle = QtCore.Qt.DashLine 19 | 20 | self.__connectionColor = QtGui.QColor(0, 0, 0) 21 | self.__connectionColor.setRgbF(*self.__srcPortCircle.getColor().getRgbF()) 22 | self.__connectionColor.setAlpha(125) 23 | 24 | self.__defaultPen = QtGui.QPen(self.__connectionColor, 1.5, style=penStyle) 25 | self.__defaultPen.setDashPattern([1, 2, 2, 1]) 26 | 27 | self.__connectionHoverColor = QtGui.QColor(0, 0, 0) 28 | self.__connectionHoverColor.setRgbF(*self.__srcPortCircle.getColor().getRgbF()) 29 | self.__connectionHoverColor.setAlpha(255) 30 | 31 | self.__hoverPen = QtGui.QPen(self.__connectionHoverColor, 1.5, style=penStyle) 32 | self.__hoverPen.setDashPattern([1, 2, 2, 1]) 33 | 34 | self.setPen(self.__defaultPen) 35 | self.setZValue(-1) 36 | 37 | self.setAcceptHoverEvents(True) 38 | self.connect() 39 | 40 | 41 | def setPenStyle(self, penStyle): 42 | self.__defaultPen.setStyle(penStyle) 43 | self.__hoverPen.setStyle(penStyle) 44 | self.setPen(self.__defaultPen) # Force a redraw 45 | 46 | 47 | def setPenWidth(self, width): 48 | self.__defaultPen.setWidthF(width) 49 | self.__hoverPen.setWidthF(width) 50 | self.setPen(self.__defaultPen) # Force a redraw 51 | 52 | 53 | def getSrcPortCircle(self): 54 | return self.__srcPortCircle 55 | 56 | 57 | def getDstPortCircle(self): 58 | return self.__dstPortCircle 59 | 60 | 61 | def getSrcPort(self): 62 | return self.__srcPortCircle.getPort() 63 | 64 | 65 | def getDstPort(self): 66 | return self.__dstPortCircle.getPort() 67 | 68 | 69 | def boundingRect(self): 70 | srcPoint = self.mapFromScene(self.__srcPortCircle.centerInSceneCoords()) 71 | dstPoint = self.mapFromScene(self.__dstPortCircle.centerInSceneCoords()) 72 | penWidth = self.__defaultPen.width() 73 | 74 | return QtCore.QRectF( 75 | min(srcPoint.x(), dstPoint.x()), 76 | min(srcPoint.y(), dstPoint.y()), 77 | abs(dstPoint.x() - srcPoint.x()), 78 | abs(dstPoint.y() - srcPoint.y()), 79 | ).adjusted(-penWidth/2, -penWidth/2, +penWidth/2, +penWidth/2) 80 | 81 | 82 | def paint(self, painter, option, widget): 83 | srcPoint = self.mapFromScene(self.__srcPortCircle.centerInSceneCoords()) 84 | dstPoint = self.mapFromScene(self.__dstPortCircle.centerInSceneCoords()) 85 | 86 | dist_between = dstPoint - srcPoint 87 | 88 | self.__path = QtGui.QPainterPath() 89 | self.__path.moveTo(srcPoint) 90 | self.__path.cubicTo( 91 | srcPoint + QtCore.QPointF(dist_between.x() * 0.4, 0), 92 | dstPoint - QtCore.QPointF(dist_between.x() * 0.4, 0), 93 | dstPoint 94 | ) 95 | self.setPath(self.__path) 96 | super(Connection, self).paint(painter, option, widget) 97 | 98 | 99 | def hoverEnterEvent(self, event): 100 | self.setPen(self.__hoverPen) 101 | super(Connection, self).hoverEnterEvent(event) 102 | 103 | 104 | def hoverLeaveEvent(self, event): 105 | self.setPen(self.__defaultPen) 106 | super(Connection, self).hoverLeaveEvent(event) 107 | 108 | 109 | def mousePressEvent(self, event): 110 | if event.button() == QtCore.Qt.LeftButton: 111 | self.__dragging = True 112 | self._lastDragPoint = self.mapToScene(event.pos()) 113 | event.accept() 114 | else: 115 | super(Connection, self).mousePressEvent(event) 116 | 117 | 118 | def mouseMoveEvent(self, event): 119 | if self.__dragging: 120 | pos = self.mapToScene(event.pos()) 121 | delta = pos - self._lastDragPoint 122 | if delta.x() != 0: 123 | 124 | self.__graph.removeConnection(self) 125 | 126 | from . import mouse_grabber 127 | if delta.x() < 0: 128 | mouse_grabber.MouseGrabber(self.__graph, pos, self.__srcPortCircle, 'In') 129 | else: 130 | mouse_grabber.MouseGrabber(self.__graph, pos, self.__dstPortCircle, 'Out') 131 | 132 | else: 133 | super(Connection, self).mouseMoveEvent(event) 134 | 135 | 136 | def disconnect(self): 137 | self.__srcPortCircle.removeConnection(self) 138 | self.__dstPortCircle.removeConnection(self) 139 | 140 | 141 | def connect(self): 142 | self.__srcPortCircle.addConnection(self) 143 | self.__dstPortCircle.addConnection(self) 144 | -------------------------------------------------------------------------------- /pyflowgraph/mouse_grabber.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Copyright 2015-2017 Eric Thivierge 4 | # 5 | 6 | from qtpy import QtGui, QtCore 7 | from .port import PortCircle, PortLabel 8 | from .connection import Connection 9 | 10 | class MouseGrabber(PortCircle): 11 | """docstring for MouseGrabber""" 12 | 13 | def __init__(self, graph, pos, otherPortCircle, connectionPointType): 14 | super(MouseGrabber, self).__init__(None, graph, 0, otherPortCircle.getPort().getColor(), connectionPointType) 15 | 16 | self._ellipseItem.setPos(0, 0) 17 | self._ellipseItem.setStartAngle(0) 18 | self._ellipseItem.setSpanAngle(360 * 16) 19 | 20 | self.__otherPortItem = otherPortCircle 21 | 22 | self._graph.scene().addItem(self) 23 | 24 | 25 | self.setZValue(-1) 26 | self.setTransform(QtGui.QTransform.fromTranslate(pos.x(), pos.y()), False) 27 | self.grabMouse() 28 | 29 | #import .connection as connection 30 | if self.connectionPointType() == 'Out': 31 | self.__connection = Connection(self._graph, self, otherPortCircle) 32 | elif self.connectionPointType() == 'In': 33 | self.__connection = Connection(self._graph, otherPortCircle, self) 34 | # Do not emit a notification for this temporary connection. 35 | self._graph.addConnection(self.__connection, emitSignal=False) 36 | self.__mouseOverPortCircle = None 37 | self._graph.emitBeginConnectionManipulationSignal() 38 | 39 | 40 | def getColor(self): 41 | return self.__otherPortItem.getPort().getColor() 42 | 43 | 44 | def mouseMoveEvent(self, event): 45 | scenePos = self.mapToScene(event.pos()) 46 | 47 | for connection in self.getConnections(): 48 | connection.prepareGeometryChange() 49 | 50 | self.setTransform(QtGui.QTransform.fromTranslate(scenePos.x(), scenePos.y()), False) 51 | 52 | collidingItems = self.collidingItems(QtCore.Qt.IntersectsItemBoundingRect) 53 | collidingPortItems = list(filter(lambda item: isinstance(item, (PortCircle, PortLabel)), collidingItems)) 54 | 55 | def canConnect(item): 56 | if isinstance(item, PortCircle): 57 | mouseOverPortCircle = item 58 | else: 59 | if self.connectionPointType() == 'In': 60 | mouseOverPortCircle = item.getPort().inCircle() 61 | else: 62 | mouseOverPortCircle = item.getPort().outCircle() 63 | 64 | if mouseOverPortCircle == None: 65 | return False 66 | 67 | return mouseOverPortCircle.canConnectTo(self.__otherPortItem) 68 | 69 | 70 | collidingPortItems = list(filter(lambda port: canConnect(port), collidingPortItems)) 71 | if len(collidingPortItems) > 0: 72 | 73 | if isinstance(collidingPortItems[0], PortCircle): 74 | self.setMouseOverPortCircle(collidingPortItems[0]) 75 | else: 76 | if self.connectionPointType() == 'In': 77 | self.setMouseOverPortCircle(collidingPortItems[0].getPort().inCircle()) 78 | else: 79 | self.setMouseOverPortCircle(collidingPortItems[0].getPort().outCircle()) 80 | 81 | elif self.__mouseOverPortCircle != None: 82 | self.setMouseOverPortCircle(None) 83 | 84 | 85 | def mouseReleaseEvent(self, event): 86 | 87 | # Destroy the temporary connection. 88 | self._graph.removeConnection(self.__connection, emitSignal=False) 89 | self.__connection = None 90 | 91 | if self.__mouseOverPortCircle is not None: 92 | try: 93 | if self.connectionPointType() == 'In': 94 | sourcePortCircle = self.__otherPortItem 95 | targetPortCircle = self.__mouseOverPortCircle 96 | elif self.connectionPointType() == 'Out': 97 | sourcePortCircle = self.__mouseOverPortCircle 98 | targetPortCircle = self.__otherPortItem 99 | 100 | connection = Connection(self._graph, sourcePortCircle, targetPortCircle) 101 | self._graph.addConnection(connection) 102 | self._graph.emitEndConnectionManipulationSignal() 103 | 104 | except Exception as e: 105 | print("Exception in MouseGrabber.mouseReleaseEvent: " + str(e)) 106 | 107 | self.setMouseOverPortCircle(None) 108 | 109 | self.destroy() 110 | 111 | 112 | def setMouseOverPortCircle(self, portCircle): 113 | 114 | if self.__mouseOverPortCircle != portCircle: 115 | if self.__mouseOverPortCircle != None: 116 | self.__mouseOverPortCircle.unhighlight() 117 | self.__mouseOverPortCircle.getPort().labelItem().unhighlight() 118 | 119 | self.__mouseOverPortCircle = portCircle 120 | 121 | if self.__mouseOverPortCircle != None: 122 | self.__mouseOverPortCircle.highlight() 123 | self.__mouseOverPortCircle.getPort().labelItem().highlight() 124 | 125 | # def paint(self, painter, option, widget): 126 | # super(MouseGrabber, self).paint(painter, option, widget) 127 | # painter.setPen(QtGui.QPen(self.getColor())) 128 | # painter.drawRect(self.windowFrameRect()) 129 | 130 | def destroy(self): 131 | self.ungrabMouse() 132 | scene = self.scene() 133 | if self.__connection is not None: 134 | self._graph.removeConnection(self.__connection, emitSignal=False) 135 | # Destroy the grabber. 136 | scene.removeItem(self) 137 | -------------------------------------------------------------------------------- /pyflowgraph/node.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Copyright 2015-2017 Eric Thivierge 4 | # 5 | 6 | import math 7 | import json 8 | from qtpy import QtGui, QtWidgets, QtCore 9 | from .port import InputPort, OutputPort 10 | 11 | class NodeTitle(QtWidgets.QGraphicsWidget): 12 | 13 | __color = QtGui.QColor(25, 25, 25) 14 | __font = QtGui.QFont('Decorative', 14) 15 | __font.setLetterSpacing(QtGui.QFont.PercentageSpacing, 115) 16 | __labelBottomSpacing = 12 17 | 18 | def __init__(self, text, parent=None): 19 | super(NodeTitle, self).__init__(parent) 20 | 21 | self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)) 22 | 23 | self.__textItem = QtWidgets.QGraphicsTextItem(text, self) 24 | self.__textItem.setDefaultTextColor(self.__color) 25 | self.__textItem.setFont(self.__font) 26 | self.__textItem.setPos(0, -2) 27 | option = self.__textItem.document().defaultTextOption() 28 | option.setWrapMode(QtGui.QTextOption.NoWrap) 29 | self.__textItem.document().setDefaultTextOption(option) 30 | self.__textItem.adjustSize() 31 | 32 | self.setPreferredSize(self.textSize()) 33 | 34 | def setText(self, text): 35 | self.__textItem.setPlainText(text) 36 | self.__textItem.adjustSize() 37 | self.setPreferredSize(self.textSize()) 38 | 39 | def textSize(self): 40 | return QtCore.QSizeF( 41 | self.__textItem.textWidth(), 42 | self.__font.pointSizeF() + self.__labelBottomSpacing 43 | ) 44 | 45 | # def paint(self, painter, option, widget): 46 | # super(NodeTitle, self).paint(painter, option, widget) 47 | # painter.setPen(QtGui.QPen(QtGui.QColor(0, 255, 0))) 48 | # painter.drawRect(self.windowFrameRect()) 49 | 50 | 51 | class NodeHeader(QtWidgets.QGraphicsWidget): 52 | 53 | def __init__(self, text, parent=None): 54 | super(NodeHeader, self).__init__(parent) 55 | 56 | self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)) 57 | 58 | layout = QtWidgets.QGraphicsLinearLayout() 59 | layout.setContentsMargins(0, 0, 0, 0) 60 | layout.setSpacing(3) 61 | layout.setOrientation(QtCore.Qt.Horizontal) 62 | self.setLayout(layout) 63 | 64 | self._titleWidget = NodeTitle(text, self) 65 | layout.addItem(self._titleWidget) 66 | layout.setAlignment(self._titleWidget, QtCore.Qt.AlignCenter | QtCore.Qt.AlignTop) 67 | 68 | 69 | def setText(self, text): 70 | self._titleWidget.setText(text) 71 | 72 | # def paint(self, painter, option, widget): 73 | # super(NodeHeader, self).paint(painter, option, widget) 74 | # painter.setPen(QtGui.QPen(QtGui.QColor(0, 255, 100))) 75 | # painter.drawRect(self.windowFrameRect()) 76 | 77 | 78 | class PortList(QtWidgets.QGraphicsWidget): 79 | def __init__(self, parent): 80 | super(PortList, self).__init__(parent) 81 | layout = QtWidgets.QGraphicsLinearLayout() 82 | layout.setContentsMargins(0, 0, 0, 0) 83 | layout.setSpacing(7) 84 | layout.setOrientation(QtCore.Qt.Vertical) 85 | self.setLayout(layout) 86 | 87 | def addPort(self, port, alignment): 88 | layout = self.layout() 89 | layout.addItem(port) 90 | layout.setAlignment(port, alignment) 91 | self.adjustSize() 92 | return port 93 | 94 | # def paint(self, painter, option, widget): 95 | # super(PortList, self).paint(painter, option, widget) 96 | # painter.setPen(QtGui.QPen(QtGui.QColor(255, 255, 0))) 97 | # painter.drawRect(self.windowFrameRect()) 98 | 99 | class Node(QtWidgets.QGraphicsWidget): 100 | 101 | nameChanged = QtCore.Signal(str, str) 102 | 103 | __defaultColor = QtGui.QColor(154, 205, 50, 255) 104 | __unselectedColor = QtGui.QColor(25, 25, 25) 105 | __selectedColor = QtGui.QColor(255, 255, 255, 255) 106 | 107 | __unselectedPen = QtGui.QPen(__unselectedColor, 1.6) 108 | __selectedPen = QtGui.QPen(__selectedColor, 1.6) 109 | __linePen = QtGui.QPen(QtGui.QColor(25, 25, 25, 255), 1.25) 110 | 111 | def __init__(self, graph, name): 112 | super(Node, self).__init__() 113 | 114 | self.__name = name 115 | self.__graph = graph 116 | self.__color = self.__defaultColor 117 | 118 | self.setMinimumWidth(60) 119 | self.setMinimumHeight(20) 120 | self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)) 121 | 122 | layout = QtWidgets.QGraphicsLinearLayout() 123 | layout.setContentsMargins(5, 0, 5, 7) 124 | layout.setSpacing(7) 125 | layout.setOrientation(QtCore.Qt.Vertical) 126 | self.setLayout(layout) 127 | 128 | self.__headerItem = NodeHeader(self.__name, self) 129 | layout.addItem(self.__headerItem) 130 | layout.setAlignment(self.__headerItem, QtCore.Qt.AlignCenter | QtCore.Qt.AlignTop) 131 | 132 | self.__ports = [] 133 | self.__inputPortsHolder = PortList(self) 134 | self.__ioPortsHolder = PortList(self) 135 | self.__outputPortsHolder = PortList(self) 136 | 137 | layout.addItem(self.__inputPortsHolder) 138 | layout.addItem(self.__ioPortsHolder) 139 | layout.addItem(self.__outputPortsHolder) 140 | 141 | self.__selected = False 142 | self.__dragging = False 143 | 144 | # ===== 145 | # Name 146 | # ===== 147 | def getName(self): 148 | return self.__name 149 | 150 | def setName(self, name): 151 | if name != self.__name: 152 | origName = self.__name 153 | self.__name = name 154 | self.__headerItem.setText(self.__name) 155 | 156 | # Emit an event, so that the graph can update itsself. 157 | self.nameChanged.emit(origName, name) 158 | 159 | # Update the node so that the size is computed. 160 | self.adjustSize() 161 | 162 | # ======= 163 | # Colors 164 | # ======= 165 | def getColor(self): 166 | return self.__color 167 | 168 | def setColor(self, color): 169 | self.__color = color 170 | self.update() 171 | 172 | 173 | def getUnselectedColor(self): 174 | return self.__unselectedColor 175 | 176 | def setUnselectedColor(self, color): 177 | self.__unselectedColor = color 178 | self.__unselectedPen.setColor(self.__unselectedColor) 179 | self.update() 180 | 181 | 182 | def getSelectedColor(self): 183 | return self.__selectedColor 184 | 185 | def setSelectedColor(self, color): 186 | self.__selectedColor = color 187 | self.__selectedPen.setColor(self.__selectedColor) 188 | self.update() 189 | 190 | # ============= 191 | # Misc Methods 192 | # ============= 193 | def getGraph(self): 194 | return self.__graph 195 | 196 | 197 | def getHeader(self): 198 | return self.__headerItem 199 | 200 | 201 | # ========== 202 | # Selection 203 | # ========== 204 | def isSelected(self): 205 | return self.__selected 206 | 207 | def setSelected(self, selected=True): 208 | self.setZValue(10.0 if selected else 0.0) 209 | self.__selected = selected 210 | self.update() 211 | 212 | 213 | ######################### 214 | ## Graph Pos 215 | 216 | def getGraphPos(self): 217 | transform = self.transform() 218 | size = self.size() 219 | return QtCore.QPointF(transform.dx()+(size.width()*0.5), transform.dy()+(size.height()*0.5)) 220 | 221 | 222 | def setGraphPos(self, graphPos): 223 | self.prepareConnectionGeometryChange() 224 | size = self.size() 225 | self.setTransform(QtGui.QTransform.fromTranslate(graphPos.x()-(size.width()*0.5), graphPos.y()-(size.height()*0.5)), False) 226 | 227 | 228 | def translate(self, x, y): 229 | self.prepareConnectionGeometryChange() 230 | super(Node, self).moveBy(x, y) 231 | 232 | 233 | # Prior to moving the node, we need to tell the connections to prepare for a geometry change. 234 | # This method must be called preior to moving a node. 235 | def prepareConnectionGeometryChange(self): 236 | for port in self.__ports: 237 | if port.inCircle(): 238 | for connection in port.inCircle().getConnections(): 239 | connection.prepareGeometryChange() 240 | if port.outCircle(): 241 | for connection in port.outCircle().getConnections(): 242 | connection.prepareGeometryChange() 243 | 244 | ######################### 245 | ## Ports 246 | 247 | def addPort(self, port): 248 | if isinstance(port, InputPort): 249 | self.__inputPortsHolder.addPort(port, QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) 250 | elif isinstance(port, OutputPort): 251 | self.__outputPortsHolder.addPort(port, QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) 252 | else: 253 | self.__ioPortsHolder.addPort(port, QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) 254 | self.__ports.append(port) 255 | self.adjustSize() 256 | return port 257 | 258 | 259 | def getPort(self, name): 260 | for port in self.__ports: 261 | if port.getName() == name: 262 | return port 263 | return None 264 | 265 | 266 | def paint(self, painter, option, widget): 267 | rect = self.windowFrameRect() 268 | painter.setBrush(self.__color) 269 | 270 | painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, 0), 0)) 271 | 272 | roundingY = 10 273 | roundingX = 10 274 | 275 | painter.drawRoundedRect(rect, roundingX, roundingY) 276 | 277 | # Title BG 278 | titleHeight = self.__headerItem.size().height() - 3 279 | 280 | painter.setBrush(self.__color.darker(125)) 281 | roundingY = rect.width() * roundingX / titleHeight 282 | painter.drawRoundedRect(0, 0, rect.width(), titleHeight, roundingX, roundingY, QtCore.Qt.AbsoluteSize) 283 | painter.drawRect(0, titleHeight * 0.5 + 2, rect.width(), titleHeight * 0.5) 284 | 285 | painter.setBrush(QtGui.QColor(0, 0, 0, 0)) 286 | if self.__selected: 287 | painter.setPen(self.__selectedPen) 288 | else: 289 | painter.setPen(self.__unselectedPen) 290 | 291 | roundingY = 10 292 | roundingX = 10 293 | 294 | painter.drawRoundedRect(rect, roundingX, roundingY, QtCore.Qt.AbsoluteSize) 295 | 296 | 297 | ######################### 298 | ## Events 299 | 300 | def mousePressEvent(self, event): 301 | if event.button() == QtCore.Qt.LeftButton: 302 | 303 | modifiers = event.modifiers() 304 | if modifiers == QtCore.Qt.ControlModifier: 305 | if not self.isSelected(): 306 | self.__graph.selectNode(self, clearSelection=False) 307 | else: 308 | self.__graph.deselectNode(self) 309 | 310 | elif modifiers == QtCore.Qt.ShiftModifier: 311 | if not self.isSelected(): 312 | self.__graph.selectNode(self, clearSelection=False) 313 | else: 314 | if not self.isSelected(): 315 | self.__graph.selectNode(self, clearSelection=True) 316 | 317 | self.__dragging = True 318 | self._mouseDownPoint = self.mapToScene(event.pos()) 319 | self._mouseDelta = self._mouseDownPoint - self.getGraphPos() 320 | self._lastDragPoint = self._mouseDownPoint 321 | self._nodesMoved = False 322 | 323 | else: 324 | super(Node, self).mousePressEvent(event) 325 | 326 | 327 | def mouseMoveEvent(self, event): 328 | if self.__dragging: 329 | newPos = self.mapToScene(event.pos()) 330 | 331 | graph = self.getGraph() 332 | if graph.getSnapToGrid() is True: 333 | gridSize = graph.getGridSize() 334 | 335 | newNodePos = newPos - self._mouseDelta 336 | 337 | snapPosX = math.floor(newNodePos.x() / gridSize) * gridSize; 338 | snapPosY = math.floor(newNodePos.y() / gridSize) * gridSize; 339 | snapPos = QtCore.QPointF(snapPosX, snapPosY) 340 | 341 | newPosOffset = snapPos - newNodePos 342 | 343 | newPos = newPos + newPosOffset 344 | 345 | delta = newPos - self._lastDragPoint 346 | self.__graph.moveSelectedNodes(delta) 347 | self._lastDragPoint = newPos 348 | self._nodesMoved = True 349 | else: 350 | super(Node, self).mouseMoveEvent(event) 351 | 352 | 353 | def mouseReleaseEvent(self, event): 354 | if self.__dragging: 355 | if self._nodesMoved: 356 | 357 | newPos = self.mapToScene(event.pos()) 358 | 359 | delta = newPos - self._mouseDownPoint 360 | self.__graph.endMoveSelectedNodes(delta) 361 | 362 | self.setCursor(QtCore.Qt.ArrowCursor) 363 | self.__dragging = False 364 | else: 365 | super(Node, self).mouseReleaseEvent(event) 366 | 367 | 368 | ######################### 369 | ## shut down 370 | 371 | def disconnectAllPorts(self): 372 | # gather all the connections into a list, and then remove them from the graph. 373 | # This is because we can't remove connections from ports while 374 | # iterating over the set. 375 | connections = [] 376 | 377 | for port in self.__ports: 378 | if port.inCircle(): 379 | for connection in port.inCircle().getConnections(): 380 | connections.append(connection) 381 | if port.outCircle(): 382 | for connection in port.outCircle().getConnections(): 383 | connections.append(connection) 384 | 385 | for connection in connections: 386 | self.__graph.removeConnection(connection) 387 | -------------------------------------------------------------------------------- /pyflowgraph/port.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Copyright 2015-2017 Eric Thivierge 4 | # 5 | 6 | import json 7 | from qtpy import QtGui, QtWidgets, QtCore 8 | 9 | 10 | class PortLabel(QtWidgets.QGraphicsWidget): 11 | __font = QtGui.QFont('Decorative', 12) 12 | __fontMetrics = QtGui.QFontMetrics(__font) 13 | 14 | def __init__(self, port, text, hOffset, color, highlightColor): 15 | super(PortLabel, self).__init__(port) 16 | self.__port = port 17 | self.__text = text 18 | self.__textItem = QtWidgets.QGraphicsTextItem(text, self) 19 | self._labelColor = color 20 | self.__highlightColor = highlightColor 21 | self.__textItem.setDefaultTextColor(self._labelColor) 22 | self.__textItem.setFont(self.__font) 23 | self.__textItem.transform().translate(0, self.__font.pointSizeF() * -0.5) 24 | option = self.__textItem.document().defaultTextOption() 25 | option.setWrapMode(QtGui.QTextOption.NoWrap) 26 | self.__textItem.document().setDefaultTextOption(option) 27 | self.__textItem.document().setDocumentMargin(0) 28 | self.__textItem.adjustSize() 29 | 30 | self.setPreferredSize(self.textSize()) 31 | self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)) 32 | self.setWindowFrameMargins(0, 0, 0, 0) 33 | self.setHOffset(hOffset) 34 | 35 | self.setAcceptHoverEvents(True) 36 | self.__mousDownPos = None 37 | 38 | def text(self): 39 | return self.__text 40 | 41 | 42 | def setHOffset(self, hOffset): 43 | self.transform().translate(hOffset, 0) 44 | 45 | 46 | def setColor(self, color): 47 | self.__textItem.setDefaultTextColor(color) 48 | self.update() 49 | 50 | 51 | def textSize(self): 52 | return QtCore.QSizeF( 53 | self.__fontMetrics.width(self.__text), 54 | self.__fontMetrics.height() 55 | ) 56 | 57 | 58 | def getPort(self): 59 | return self.__port 60 | 61 | 62 | def highlight(self): 63 | self.setColor(self.__highlightColor) 64 | 65 | 66 | def unhighlight(self): 67 | self.setColor(self._labelColor) 68 | 69 | 70 | def hoverEnterEvent(self, event): 71 | self.highlight() 72 | super(PortLabel, self).hoverEnterEvent(event) 73 | 74 | 75 | def hoverLeaveEvent(self, event): 76 | self.unhighlight() 77 | super(PortLabel, self).hoverLeaveEvent(event) 78 | 79 | 80 | def mousePressEvent(self, event): 81 | self.__mousDownPos = self.mapToScene(event.pos()) 82 | 83 | 84 | def mouseMoveEvent(self, event): 85 | self.unhighlight() 86 | scenePos = self.mapToScene(event.pos()) 87 | 88 | # When clicking on an UI port label, it is ambigous which connection point should be activated. 89 | # We let the user drag the mouse in either direction to select the conneciton point to activate. 90 | delta = scenePos - self.__mousDownPos 91 | if delta.x() < 0: 92 | if self.__port.inCircle() is not None: 93 | self.__port.inCircle().mousePressEvent(event) 94 | else: 95 | if self.__port.outCircle() is not None: 96 | self.__port.outCircle().mousePressEvent(event) 97 | 98 | # def paint(self, painter, option, widget): 99 | # super(PortLabel, self).paint(painter, option, widget) 100 | # painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 255))) 101 | # painter.drawRect(self.windowFrameRect()) 102 | 103 | 104 | class PortCircle(QtWidgets.QGraphicsWidget): 105 | 106 | __radius = 4.5 107 | __diameter = 2 * __radius 108 | 109 | def __init__(self, port, graph, hOffset, color, connectionPointType): 110 | super(PortCircle, self).__init__(port) 111 | 112 | self.__port = port 113 | self._graph = graph 114 | self._connectionPointType = connectionPointType 115 | self.__connections = set() 116 | self._supportsOnlySingleConnections = connectionPointType == 'In' 117 | 118 | self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)) 119 | size = QtCore.QSizeF(self.__diameter, self.__diameter) 120 | self.setPreferredSize(size) 121 | self.setWindowFrameMargins(0, 0, 0, 0) 122 | 123 | self.transform().translate(self.__radius * hOffset, 0) 124 | 125 | self.__defaultPen = QtGui.QPen(QtGui.QColor(25, 25, 25), 1.0) 126 | self.__hoverPen = QtGui.QPen(QtGui.QColor(255, 255, 100), 1.5) 127 | 128 | self._ellipseItem = QtWidgets.QGraphicsEllipseItem(self) 129 | self._ellipseItem.setPen(self.__defaultPen) 130 | self._ellipseItem.setPos(size.width()/2, size.height()/2) 131 | self._ellipseItem.setRect( 132 | -self.__radius, 133 | -self.__radius, 134 | self.__diameter, 135 | self.__diameter, 136 | ) 137 | if connectionPointType == 'In': 138 | self._ellipseItem.setStartAngle(270 * 16) 139 | self._ellipseItem.setSpanAngle(180 * 16) 140 | 141 | self.setColor(color) 142 | self.setAcceptHoverEvents(True) 143 | 144 | def getPort(self): 145 | return self.__port 146 | 147 | 148 | def getColor(self): 149 | return self.getPort().getColor() 150 | 151 | 152 | def centerInSceneCoords(self): 153 | return self._ellipseItem.mapToScene(0, 0) 154 | 155 | 156 | def setColor(self, color): 157 | self._color = color 158 | self._ellipseItem.setBrush(QtGui.QBrush(self._color)) 159 | 160 | 161 | def setDefaultPen(self, pen): 162 | self.__defaultPen = pen 163 | self._ellipseItem.setPen(self.__defaultPen) 164 | 165 | 166 | def setHoverPen(self, pen): 167 | self.__hoverPen = pen 168 | 169 | 170 | def highlight(self): 171 | self._ellipseItem.setBrush(QtGui.QBrush(self._color.lighter())) 172 | # make the port bigger to highlight it can accept the connection. 173 | self._ellipseItem.setRect( 174 | -self.__radius * 1.3, 175 | -self.__radius * 1.3, 176 | self.__diameter * 1.3, 177 | self.__diameter * 1.3, 178 | ) 179 | 180 | 181 | def unhighlight(self): 182 | self._ellipseItem.setBrush(QtGui.QBrush(self._color)) 183 | self._ellipseItem.setRect( 184 | -self.__radius, 185 | -self.__radius, 186 | self.__diameter, 187 | self.__diameter, 188 | ) 189 | 190 | 191 | # =================== 192 | # Connection Methods 193 | # =================== 194 | def connectionPointType(self): 195 | return self._connectionPointType 196 | 197 | def isInConnectionPoint(self): 198 | return self._connectionPointType == 'In' 199 | 200 | def isOutConnectionPoint(self): 201 | return self._connectionPointType == 'Out' 202 | 203 | def supportsOnlySingleConnections(self): 204 | return self._supportsOnlySingleConnections 205 | 206 | def setSupportsOnlySingleConnections(self, value): 207 | self._supportsOnlySingleConnections = value 208 | 209 | def canConnectTo(self, otherPortCircle): 210 | 211 | if self.connectionPointType() == otherPortCircle.connectionPointType(): 212 | return False 213 | 214 | if self.getPort().getDataType() != otherPortCircle.getPort().getDataType(): 215 | return False 216 | 217 | # Check if you're trying to connect to a port on the same node. 218 | # TODO: Do propper cycle checking.. 219 | otherPort = otherPortCircle.getPort() 220 | port = self.getPort() 221 | if otherPort.getNode() == port.getNode(): 222 | return False 223 | 224 | return True 225 | 226 | def addConnection(self, connection): 227 | """Adds a connection to the list. 228 | Arguments: 229 | connection -- connection, new connection to add. 230 | Return: 231 | True if successful. 232 | """ 233 | 234 | if self._supportsOnlySingleConnections and len(self.__connections) != 0: 235 | # gather all the connections into a list, and then remove them from the graph. 236 | # This is because we can't remove connections from ports while 237 | # iterating over the set. 238 | connections = [] 239 | for c in self.__connections: 240 | connections.append(c) 241 | for c in connections: 242 | self._graph.removeConnection(c) 243 | 244 | self.__connections.add(connection) 245 | 246 | return True 247 | 248 | def removeConnection(self, connection): 249 | """Removes a connection to the list. 250 | Arguments: 251 | connection -- connection, connection to remove. 252 | Return: 253 | True if successful. 254 | """ 255 | 256 | self.__connections.remove(connection) 257 | 258 | return True 259 | 260 | def getConnections(self): 261 | """Gets the ports connections list. 262 | Return: 263 | List, connections to this port. 264 | """ 265 | 266 | return self.__connections 267 | 268 | # ====== 269 | # Events 270 | # ====== 271 | def hoverEnterEvent(self, event): 272 | self.highlight() 273 | super(PortCircle, self).hoverEnterEvent(event) 274 | 275 | 276 | def hoverLeaveEvent(self, event): 277 | self.unhighlight() 278 | super(PortCircle, self).hoverLeaveEvent(event) 279 | 280 | def mousePressEvent(self, event): 281 | 282 | self.unhighlight() 283 | 284 | scenePos = self.mapToScene(event.pos()) 285 | 286 | from .mouse_grabber import MouseGrabber 287 | if self.isInConnectionPoint(): 288 | MouseGrabber(self._graph, scenePos, self, 'Out') 289 | elif self.isOutConnectionPoint(): 290 | MouseGrabber(self._graph, scenePos, self, 'In') 291 | 292 | 293 | # def paint(self, painter, option, widget): 294 | # super(PortCircle, self).paint(painter, option, widget) 295 | # painter.setPen(QtGui.QPen(QtGui.QColor(255, 255, 0))) 296 | # painter.drawRect(self.windowFrameRect()) 297 | 298 | 299 | class ItemHolder(QtWidgets.QGraphicsWidget): 300 | """docstring for ItemHolder""" 301 | def __init__(self, parent): 302 | super(ItemHolder, self).__init__(parent) 303 | 304 | layout = QtWidgets.QGraphicsLinearLayout() 305 | layout.setSpacing(0) 306 | layout.setContentsMargins(0, 0, 0, 0) 307 | self.setLayout(layout) 308 | 309 | def setItem(self, item): 310 | item.setParentItem(self) 311 | self.layout().addItem(item) 312 | 313 | # def paint(self, painter, option, widget): 314 | # super(ItemHolder, self).paint(painter, option, widget) 315 | # painter.setPen(QtGui.QPen(QtGui.QColor(255, 255, 0))) 316 | # painter.drawRect(self.windowFrameRect()) 317 | 318 | 319 | 320 | class BasePort(QtWidgets.QGraphicsWidget): 321 | 322 | _labelColor = QtGui.QColor(25, 25, 25) 323 | _labelHighlightColor = QtGui.QColor(225, 225, 225, 255) 324 | 325 | def __init__(self, parent, graph, name, color, dataType, connectionPointType): 326 | super(BasePort, self).__init__(parent) 327 | 328 | self._node = parent 329 | self._graph = graph 330 | self._name = name 331 | self._dataType = dataType 332 | self._connectionPointType = connectionPointType 333 | 334 | self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)) 335 | 336 | layout = QtWidgets.QGraphicsLinearLayout() 337 | layout.setSpacing(0) 338 | layout.setContentsMargins(0, 0, 0, 0) 339 | self.setLayout(layout) 340 | 341 | self._color = color 342 | 343 | self._inCircle = None 344 | self._outCircle = None 345 | self._labelItem = None 346 | 347 | self._inCircleHolder = ItemHolder(self) 348 | self._outCircleHolder = ItemHolder(self) 349 | self._labelItemHolder = ItemHolder(self) 350 | 351 | self.layout().addItem(self._inCircleHolder) 352 | self.layout().setAlignment(self._inCircleHolder, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) 353 | 354 | self.layout().addItem(self._labelItemHolder) 355 | self.layout().setAlignment(self._labelItemHolder, QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) 356 | 357 | self.layout().addItem(self._outCircleHolder) 358 | self.layout().setAlignment(self._outCircleHolder, QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) 359 | 360 | 361 | def getName(self): 362 | return self._name 363 | 364 | 365 | def getDataType(self): 366 | return self._dataType 367 | 368 | 369 | def getNode(self): 370 | return self._node 371 | 372 | 373 | def getGraph(self): 374 | return self._graph 375 | 376 | 377 | def getColor(self): 378 | return self._color 379 | 380 | 381 | def setColor(self, color): 382 | if self._inCircle is not None: 383 | self._inCircle.setColor(color) 384 | if self._outCircle is not None: 385 | self._outCircle.setColor(color) 386 | self._color = color 387 | 388 | 389 | def inCircle(self): 390 | return self._inCircle 391 | 392 | 393 | def setInCircle(self, inCircle): 394 | self._inCircleHolder.setItem(inCircle) 395 | self._inCircle = inCircle 396 | self.layout().insertStretch(2, 2) 397 | self.updatecontentMargins() 398 | 399 | def outCircle(self): 400 | return self._outCircle 401 | 402 | 403 | def setOutCircle(self, outCircle): 404 | self._outCircleHolder.setItem(outCircle) 405 | self._outCircle = outCircle 406 | self.layout().insertStretch(1, 2) 407 | self.updatecontentMargins() 408 | 409 | def updatecontentMargins(self): 410 | left = 0 411 | right = 0 412 | if self._inCircle is None: 413 | left = 30 414 | if self._outCircle is None: 415 | right = 30 416 | self.layout().setContentsMargins(left, 0, right, 0) 417 | 418 | 419 | def labelItem(self): 420 | return self._labelItem 421 | 422 | 423 | def setLabelItem(self, labelItem): 424 | self._labelItemHolder.setItem(labelItem) 425 | self._labelItem = labelItem 426 | 427 | 428 | # =================== 429 | # Connection Methods 430 | # =================== 431 | def connectionPointType(self): 432 | return self._connectionPointType 433 | 434 | # def paint(self, painter, option, widget): 435 | # super(BasePort, self).paint(painter, option, widget) 436 | # painter.setPen(QtGui.QPen(QtGui.QColor(255, 255, 0))) 437 | # painter.drawRect(self.windowFrameRect()) 438 | 439 | 440 | class InputPort(BasePort): 441 | 442 | def __init__(self, parent, graph, name, color, dataType): 443 | super(InputPort, self).__init__(parent, graph, name, color, dataType, 'In') 444 | 445 | self.setInCircle(PortCircle(self, graph, -2, color, 'In')) 446 | self.setLabelItem(PortLabel(self, name, -10, self._labelColor, self._labelHighlightColor)) 447 | 448 | 449 | 450 | class OutputPort(BasePort): 451 | 452 | def __init__(self, parent, graph, name, color, dataType): 453 | super(OutputPort, self).__init__(parent, graph, name, color, dataType, 'Out') 454 | 455 | self.setLabelItem(PortLabel(self, self._name, 10, self._labelColor, self._labelHighlightColor)) 456 | self.setOutCircle(PortCircle(self, graph, 2, color, 'Out')) 457 | 458 | 459 | 460 | class IOPort(BasePort): 461 | 462 | def __init__(self, parent, graph, name, color, dataType): 463 | super(IOPort, self).__init__(parent, graph, name, color, dataType, 'IO') 464 | 465 | self.setInCircle(PortCircle(self, graph, -2, color, 'In')) 466 | self.setLabelItem(PortLabel(self, self._name, 0, self._labelColor, self._labelHighlightColor)) 467 | self.setOutCircle(PortCircle(self, graph, 2, color, 'Out')) 468 | 469 | 470 | -------------------------------------------------------------------------------- /pyflowgraph/graph_view.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Copyright 2015-2017 Eric Thivierge 4 | # 5 | 6 | import copy 7 | from future.utils import iteritems 8 | from past.builtins import basestring 9 | 10 | from qtpy import QtGui, QtWidgets, QtCore, PYQT5 11 | 12 | from .node import Node 13 | from .connection import Connection 14 | 15 | from .selection_rect import SelectionRect 16 | 17 | MANIP_MODE_NONE = 0 18 | MANIP_MODE_SELECT = 1 19 | MANIP_MODE_PAN = 2 20 | MANIP_MODE_MOVE = 3 21 | MANIP_MODE_ZOOM = 4 22 | 23 | 24 | class GraphView(QtWidgets.QGraphicsView): 25 | 26 | nodeAdded = QtCore.Signal(Node) 27 | nodeRemoved = QtCore.Signal(Node) 28 | nodeNameChanged = QtCore.Signal(str, str) 29 | beginDeleteSelection = QtCore.Signal() 30 | endDeleteSelection = QtCore.Signal() 31 | 32 | beginConnectionManipulation = QtCore.Signal() 33 | endConnectionManipulation = QtCore.Signal() 34 | connectionAdded = QtCore.Signal(Connection) 35 | connectionRemoved = QtCore.Signal(Connection) 36 | 37 | beginNodeSelection = QtCore.Signal() 38 | endNodeSelection = QtCore.Signal() 39 | selectionChanged = QtCore.Signal(list, list) 40 | 41 | # During the movement of the nodes, this signal is emitted with the incremental delta. 42 | selectionMoved = QtCore.Signal(set, QtCore.QPointF) 43 | 44 | # After moving the nodes interactively, this signal is emitted with the final delta. 45 | endSelectionMoved = QtCore.Signal(set, QtCore.QPointF) 46 | 47 | 48 | 49 | _clipboardData = None 50 | 51 | _backgroundColor = QtGui.QColor(50, 50, 50) 52 | _gridPenS = QtGui.QPen(QtGui.QColor(44, 44, 44, 255), 0.5) 53 | _gridPenL = QtGui.QPen(QtGui.QColor(40, 40, 40, 255), 1.0) 54 | _gridSizeFine = 30 55 | _gridSizeCourse = 300 56 | 57 | _mouseWheelZoomRate = 0.0005 58 | 59 | _snapToGrid = False 60 | 61 | def __init__(self, parent=None): 62 | super(GraphView, self).__init__(parent) 63 | self.setObjectName('graphView') 64 | 65 | self.__graphViewWidget = parent 66 | 67 | self.setRenderHint(QtGui.QPainter.Antialiasing) 68 | self.setRenderHint(QtGui.QPainter.TextAntialiasing) 69 | 70 | self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 71 | self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 72 | 73 | # Explicitly set the scene rect. This ensures all view parameters will be explicitly controlled 74 | # in the event handlers of this class. 75 | size = QtCore.QSize(600, 400); 76 | self.resize(size) 77 | self.setSceneRect(-size.width() * 0.5, -size.height() * 0.5, size.width(), size.height()) 78 | 79 | self.setAcceptDrops(True) 80 | self.reset() 81 | 82 | 83 | def getGraphViewWidget(self): 84 | return self.__graphViewWidget 85 | 86 | 87 | ################################################ 88 | ## Graph 89 | def reset(self): 90 | self.setScene(QtWidgets.QGraphicsScene()) 91 | 92 | self.__connections = set() 93 | self.__nodes = {} 94 | self.__selection = set() 95 | 96 | self._manipulationMode = MANIP_MODE_NONE 97 | self._selectionRect = None 98 | 99 | def getGridSize(self): 100 | """Gets the size of the grid of the graph. 101 | 102 | Returns: 103 | int: Size of the grid. 104 | 105 | """ 106 | 107 | return self._gridSizeFine 108 | 109 | def getSnapToGrid(self): 110 | """Gets the snap to grid value. 111 | 112 | Returns: 113 | Boolean: Whether snap to grid is active or not. 114 | 115 | """ 116 | 117 | return self._snapToGrid 118 | 119 | def setSnapToGrid(self, snap): 120 | """Sets the snap to grid value. 121 | 122 | Args: 123 | snap (Boolean): True to snap to grid, false not to. 124 | 125 | """ 126 | 127 | self._snapToGrid = snap 128 | 129 | 130 | ################################################ 131 | ## Nodes 132 | 133 | def addNode(self, node, emitSignal=True): 134 | self.scene().addItem(node) 135 | self.__nodes[node.getName()] = node 136 | node.nameChanged.connect(self._onNodeNameChanged) 137 | 138 | if emitSignal: 139 | self.nodeAdded.emit(node) 140 | 141 | return node 142 | 143 | def removeNode(self, node, emitSignal=True): 144 | 145 | del self.__nodes[node.getName()] 146 | self.scene().removeItem(node) 147 | node.nameChanged.disconnect(self._onNodeNameChanged) 148 | 149 | if emitSignal: 150 | self.nodeRemoved.emit(node) 151 | 152 | 153 | def hasNode(self, name): 154 | return name in self.__nodes 155 | 156 | def getNode(self, name): 157 | if name in self.__nodes: 158 | return self.__nodes[name] 159 | return None 160 | 161 | 162 | def _onNodeNameChanged(self, origName, newName ): 163 | if newName in self.__nodes and self.__nodes[origName] != self.__nodes[newName]: 164 | raise Exception("New name collides with existing node.") 165 | node = self.__nodes[origName] 166 | self.__nodes[newName] = node 167 | del self.__nodes[origName] 168 | self.nodeNameChanged.emit( origName, newName ) 169 | 170 | 171 | def clearSelection(self, emitSignal=True): 172 | 173 | prevSelection = [] 174 | if emitSignal: 175 | for node in self.__selection: 176 | prevSelection.append(node) 177 | 178 | for node in self.__selection: 179 | node.setSelected(False) 180 | self.__selection.clear() 181 | 182 | if emitSignal and len(prevSelection) != 0: 183 | self.selectionChanged.emit(prevSelection, []) 184 | 185 | def selectNode(self, node, clearSelection=False, emitSignal=True): 186 | prevSelection = [] 187 | if emitSignal: 188 | for n in self.__selection: 189 | prevSelection.append(n) 190 | 191 | if clearSelection is True: 192 | self.clearSelection(emitSignal=False) 193 | 194 | if node in self.__selection: 195 | raise IndexError("Node is already in selection!") 196 | 197 | node.setSelected(True) 198 | self.__selection.add(node) 199 | 200 | if emitSignal: 201 | 202 | newSelection = [] 203 | for n in self.__selection: 204 | newSelection.append(n) 205 | 206 | self.selectionChanged.emit(prevSelection, newSelection) 207 | 208 | 209 | def deselectNode(self, node, emitSignal=True): 210 | 211 | if node not in self.__selection: 212 | raise IndexError("Node is not in selection!") 213 | 214 | prevSelection = [] 215 | if emitSignal: 216 | for n in self.__selection: 217 | prevSelection.append(n) 218 | 219 | node.setSelected(False) 220 | self.__selection.remove(node) 221 | 222 | if emitSignal: 223 | newSelection = [] 224 | for n in self.__selection: 225 | newSelection.append(n) 226 | 227 | self.selectionChanged.emit(prevSelection, newSelection) 228 | 229 | def getSelectedNodes(self): 230 | return self.__selection 231 | 232 | 233 | def deleteSelectedNodes(self): 234 | self.beginDeleteSelection.emit() 235 | 236 | selectedNodes = self.getSelectedNodes() 237 | names = "" 238 | for node in selectedNodes: 239 | node.disconnectAllPorts() 240 | self.removeNode(node) 241 | 242 | self.endDeleteSelection.emit() 243 | 244 | 245 | def frameNodes(self, nodes): 246 | if len(nodes) == 0: 247 | return 248 | 249 | def computeWindowFrame(): 250 | windowRect = self.rect() 251 | windowRect.setLeft(windowRect.left() + 16) 252 | windowRect.setRight(windowRect.right() - 16) 253 | windowRect.setTop(windowRect.top() + 16) 254 | windowRect.setBottom(windowRect.bottom() - 16) 255 | return windowRect 256 | 257 | nodesRect = None 258 | for node in nodes: 259 | nodeRectF = node.transform().mapRect(node.rect()) 260 | nodeRect = QtCore.QRect(nodeRectF.x(), nodeRectF.y(), nodeRectF.width(), nodeRectF.height()) 261 | if nodesRect is None: 262 | nodesRect = nodeRect 263 | else: 264 | nodesRect = nodesRect.united(nodeRect) 265 | 266 | 267 | windowRect = computeWindowFrame() 268 | 269 | scaleX = float(windowRect.width()) / float(nodesRect.width()) 270 | scaleY = float(windowRect.height()) / float(nodesRect.height()) 271 | if scaleY > scaleX: 272 | scale = scaleX 273 | else: 274 | scale = scaleY 275 | 276 | if scale < 1.0: 277 | self.setTransform(QtGui.QTransform.fromScale(scale, scale)) 278 | else: 279 | self.setTransform(QtGui.QTransform()) 280 | 281 | sceneRect = self.sceneRect() 282 | pan = sceneRect.center() - nodesRect.center() 283 | sceneRect.translate(-pan.x(), -pan.y()) 284 | self.setSceneRect(sceneRect) 285 | 286 | # Update the main panel when reframing. 287 | self.update() 288 | 289 | 290 | def frameSelectedNodes(self): 291 | self.frameNodes(self.getSelectedNodes()) 292 | 293 | def frameAllNodes(self): 294 | allnodes = [] 295 | for name, node in iteritems(self.__nodes): 296 | allnodes.append(node) 297 | self.frameNodes(allnodes) 298 | 299 | def getSelectedNodesCentroid(self): 300 | selectedNodes = self.getSelectedNodes() 301 | 302 | leftMostNode = None 303 | topMostNode = None 304 | for node in selectedNodes: 305 | nodePos = node.getGraphPos() 306 | 307 | if leftMostNode is None: 308 | leftMostNode = node 309 | else: 310 | if nodePos.x() < leftMostNode.getGraphPos().x(): 311 | leftMostNode = node 312 | 313 | if topMostNode is None: 314 | topMostNode = node 315 | else: 316 | if nodePos.y() < topMostNode.getGraphPos().y(): 317 | topMostNode = node 318 | 319 | xPos = leftMostNode.getGraphPos().x() 320 | yPos = topMostNode.getGraphPos().y() 321 | pos = QtCore.QPoint(xPos, yPos) 322 | 323 | return pos 324 | 325 | 326 | def moveSelectedNodes(self, delta, emitSignal=True): 327 | for node in self.__selection: 328 | node.translate(delta.x(), delta.y()) 329 | 330 | if emitSignal: 331 | self.selectionMoved.emit(self.__selection, delta) 332 | 333 | # After moving the nodes interactively, this signal is emitted with the final delta. 334 | def endMoveSelectedNodes(self, delta): 335 | self.endSelectionMoved.emit(self.__selection, delta) 336 | 337 | ################################################ 338 | ## Connections 339 | 340 | def emitBeginConnectionManipulationSignal(self): 341 | self.beginConnectionManipulation.emit() 342 | 343 | 344 | def emitEndConnectionManipulationSignal(self): 345 | self.endConnectionManipulation.emit() 346 | 347 | 348 | def addConnection(self, connection, emitSignal=True): 349 | 350 | self.__connections.add(connection) 351 | self.scene().addItem(connection) 352 | if emitSignal: 353 | self.connectionAdded.emit(connection) 354 | return connection 355 | 356 | def removeConnection(self, connection, emitSignal=True): 357 | 358 | connection.disconnect() 359 | self.__connections.remove(connection) 360 | self.scene().removeItem(connection) 361 | if emitSignal: 362 | self.connectionRemoved.emit(connection) 363 | 364 | 365 | def connectPorts(self, srcNode, outputName, tgtNode, inputName): 366 | 367 | if isinstance(srcNode, Node): 368 | sourceNode = srcNode 369 | elif isinstance(srcNode, basestring): 370 | sourceNode = self.getNode(srcNode) 371 | if not sourceNode: 372 | raise Exception("Node not found:" + str(srcNode)) 373 | else: 374 | raise Exception("Invalid srcNode:" + str(srcNode)) 375 | 376 | 377 | sourcePort = sourceNode.getPort(outputName) 378 | if not sourcePort: 379 | raise Exception("Node '" + sourceNode.getName() + "' does not have output:" + outputName) 380 | 381 | 382 | if isinstance(tgtNode, Node): 383 | targetNode = tgtNode 384 | elif isinstance(tgtNode, basestring): 385 | targetNode = self.getNode(tgtNode) 386 | if not targetNode: 387 | raise Exception("Node not found:" + str(tgtNode)) 388 | else: 389 | raise Exception("Invalid tgtNode:" + str(tgtNode)) 390 | 391 | targetPort = targetNode.getPort(inputName) 392 | if not targetPort: 393 | raise Exception("Node '" + targetNode.getName() + "' does not have input:" + inputName) 394 | 395 | connection = Connection(self, sourcePort.outCircle(), targetPort.inCircle()) 396 | self.addConnection(connection, emitSignal=False) 397 | 398 | return connection 399 | 400 | ################################################ 401 | ## Events 402 | 403 | def mousePressEvent(self, event): 404 | 405 | if event.button() == QtCore.Qt.LeftButton and self.itemAt(event.pos()) is None: 406 | self.beginNodeSelection.emit() 407 | self._manipulationMode = MANIP_MODE_SELECT 408 | self._mouseDownSelection = copy.copy(self.getSelectedNodes()) 409 | self.clearSelection(emitSignal=False) 410 | self._selectionRect = SelectionRect(graph=self, mouseDownPos=self.mapToScene(event.pos())) 411 | 412 | elif event.button() == QtCore.Qt.MidButton or event.button() == QtCore.Qt.MiddleButton: 413 | self.setCursor(QtCore.Qt.OpenHandCursor) 414 | self._manipulationMode = MANIP_MODE_PAN 415 | self._lastPanPoint = self.mapToScene(event.pos()) 416 | 417 | elif event.button() == QtCore.Qt.RightButton: 418 | self.setCursor(QtCore.Qt.SizeHorCursor) 419 | self._manipulationMode = MANIP_MODE_ZOOM 420 | self._lastMousePos = event.pos() 421 | self._lastTransform = QtGui.QTransform(self.transform()) 422 | self._lastSceneRect = self.sceneRect() 423 | self._lastSceneCenter = self._lastSceneRect.center() 424 | self._lastScenePos = self.mapToScene(event.pos()) 425 | self._lastOffsetFromSceneCenter = self._lastScenePos - self._lastSceneCenter 426 | 427 | else: 428 | super(GraphView, self).mousePressEvent(event) 429 | 430 | def mouseMoveEvent(self, event): 431 | modifiers = QtWidgets.QApplication.keyboardModifiers() 432 | 433 | if self._manipulationMode == MANIP_MODE_SELECT: 434 | dragPoint = self.mapToScene(event.pos()) 435 | self._selectionRect.setDragPoint(dragPoint) 436 | 437 | # This logic allows users to use ctrl and shift with rectangle 438 | # select to add / remove nodes. 439 | if modifiers == QtCore.Qt.ControlModifier: 440 | for name, node in iteritems(self.__nodes): 441 | 442 | if node in self._mouseDownSelection: 443 | if node.isSelected() and self._selectionRect.collidesWithItem(node): 444 | self.deselectNode(node, emitSignal=False) 445 | elif not node.isSelected() and not self._selectionRect.collidesWithItem(node): 446 | self.selectNode(node, emitSignal=False) 447 | else: 448 | if not node.isSelected() and self._selectionRect.collidesWithItem(node): 449 | self.selectNode(node, emitSignal=False) 450 | elif node.isSelected() and not self._selectionRect.collidesWithItem(node): 451 | if node not in self._mouseDownSelection: 452 | self.deselectNode(node, emitSignal=False) 453 | 454 | elif modifiers == QtCore.Qt.ShiftModifier: 455 | for name, node in iteritems(self.__nodes): 456 | if not node.isSelected() and self._selectionRect.collidesWithItem(node): 457 | self.selectNode(node, emitSignal=False) 458 | elif node.isSelected() and not self._selectionRect.collidesWithItem(node): 459 | if node not in self._mouseDownSelection: 460 | self.deselectNode(node, emitSignal=False) 461 | 462 | else: 463 | self.clearSelection(emitSignal=False) 464 | 465 | for name, node in iteritems(self.__nodes): 466 | if not node.isSelected() and self._selectionRect.collidesWithItem(node): 467 | self.selectNode(node, emitSignal=False) 468 | elif node.isSelected() and not self._selectionRect.collidesWithItem(node): 469 | self.deselectNode(node, emitSignal=False) 470 | 471 | elif self._manipulationMode == MANIP_MODE_PAN: 472 | delta = self.mapToScene(event.pos()) - self._lastPanPoint 473 | 474 | rect = self.sceneRect() 475 | rect.translate(-delta.x(), -delta.y()) 476 | self.setSceneRect(rect) 477 | 478 | self._lastPanPoint = self.mapToScene(event.pos()) 479 | 480 | elif self._manipulationMode == MANIP_MODE_MOVE: 481 | 482 | newPos = self.mapToScene(event.pos()) 483 | delta = newPos - self._lastDragPoint 484 | self._lastDragPoint = newPos 485 | 486 | selectedNodes = self.getSelectedNodes() 487 | 488 | # Apply the delta to each selected node 489 | for node in selectedNodes: 490 | node.translate(delta.x(), delta.y()) 491 | 492 | elif self._manipulationMode == MANIP_MODE_ZOOM: 493 | 494 | # How much 495 | delta = event.pos() - self._lastMousePos 496 | zoomFactor = 1.0 497 | if delta.x() > 0: 498 | zoomFactor = 1.0 + delta.x() / 100.0 499 | else: 500 | zoomFactor = 1.0 / (1.0 + abs(delta.x()) / 100.0) 501 | 502 | # Limit zoom to 3x 503 | if self._lastTransform.m22() * zoomFactor >= 2.0: 504 | return 505 | 506 | # Reset to when we mouse pressed 507 | self.setSceneRect(self._lastSceneRect) 508 | self.setTransform(self._lastTransform) 509 | 510 | # Center scene around mouse down 511 | rect = self.sceneRect() 512 | rect.translate(self._lastOffsetFromSceneCenter) 513 | self.setSceneRect(rect) 514 | 515 | # Zoom in (QGraphicsView auto-centers!) 516 | self.scale(zoomFactor, zoomFactor) 517 | 518 | newSceneCenter = self.sceneRect().center() 519 | newScenePos = self.mapToScene(self._lastMousePos) 520 | newOffsetFromSceneCenter = newScenePos - newSceneCenter 521 | 522 | # Put mouse down back where is was on screen 523 | rect = self.sceneRect() 524 | rect.translate(-1 * newOffsetFromSceneCenter) 525 | self.setSceneRect(rect) 526 | 527 | # Call udpate to redraw background 528 | self.update() 529 | 530 | 531 | else: 532 | super(GraphView, self).mouseMoveEvent(event) 533 | 534 | def mouseReleaseEvent(self, event): 535 | if self._manipulationMode == MANIP_MODE_SELECT: 536 | 537 | # If users simply clicks in the empty space, clear selection. 538 | if self.mapToScene(event.pos()) == self._selectionRect.pos(): 539 | self.clearSelection(emitSignal=False) 540 | 541 | self._selectionRect.destroy() 542 | self._selectionRect = None 543 | self._manipulationMode = MANIP_MODE_NONE 544 | 545 | selection = self.getSelectedNodes() 546 | 547 | deselectedNodes = [] 548 | selectedNodes = [] 549 | 550 | for node in self._mouseDownSelection: 551 | if node not in selection: 552 | deselectedNodes.append(node) 553 | 554 | for node in selection: 555 | if node not in self._mouseDownSelection: 556 | selectedNodes.append(node) 557 | 558 | if selectedNodes != deselectedNodes: 559 | self.selectionChanged.emit(deselectedNodes, selectedNodes) 560 | 561 | self.endNodeSelection.emit() 562 | 563 | elif self._manipulationMode == MANIP_MODE_PAN: 564 | self.setCursor(QtCore.Qt.ArrowCursor) 565 | self._manipulationMode = MANIP_MODE_NONE 566 | 567 | elif self._manipulationMode == MANIP_MODE_ZOOM: 568 | self.setCursor(QtCore.Qt.ArrowCursor) 569 | self._manipulationMode = MANIP_MODE_NONE 570 | #self.setTransformationAnchor(self._lastAnchor) 571 | 572 | else: 573 | super(GraphView, self).mouseReleaseEvent(event) 574 | 575 | def wheelEvent(self, event): 576 | 577 | (xfo, invRes) = self.transform().inverted() 578 | topLeft = xfo.map(self.rect().topLeft()) 579 | bottomRight = xfo.map(self.rect().bottomRight()) 580 | center = ( topLeft + bottomRight ) * 0.5 581 | 582 | if PYQT5: 583 | zoomFactor = 1.0 + event.angleDelta().y() * self._mouseWheelZoomRate 584 | else: 585 | zoomFactor = 1.0 + event.delta() * self._mouseWheelZoomRate 586 | 587 | transform = self.transform() 588 | 589 | # Limit zoom to 3x 590 | if transform.m22() * zoomFactor >= 2.0: 591 | return 592 | 593 | self.scale(zoomFactor, zoomFactor) 594 | 595 | # Call udpate to redraw background 596 | self.update() 597 | 598 | 599 | ################################################ 600 | ## Painting 601 | 602 | def drawBackground(self, painter, rect): 603 | 604 | oldTransform = painter.transform() 605 | painter.fillRect(rect, self._backgroundColor) 606 | 607 | left = int(rect.left()) - (int(rect.left()) % self._gridSizeFine) 608 | top = int(rect.top()) - (int(rect.top()) % self._gridSizeFine) 609 | 610 | # Draw horizontal fine lines 611 | gridLines = [] 612 | painter.setPen(self._gridPenS) 613 | y = float(top) 614 | while y < float(rect.bottom()): 615 | gridLines.append(QtCore.QLineF( rect.left(), y, rect.right(), y )) 616 | y += self._gridSizeFine 617 | painter.drawLines(gridLines) 618 | 619 | # Draw vertical fine lines 620 | gridLines = [] 621 | painter.setPen(self._gridPenS) 622 | x = float(left) 623 | while x < float(rect.right()): 624 | gridLines.append(QtCore.QLineF( x, rect.top(), x, rect.bottom())) 625 | x += self._gridSizeFine 626 | painter.drawLines(gridLines) 627 | 628 | # Draw thick grid 629 | left = int(rect.left()) - (int(rect.left()) % self._gridSizeCourse) 630 | top = int(rect.top()) - (int(rect.top()) % self._gridSizeCourse) 631 | 632 | # Draw vertical thick lines 633 | gridLines = [] 634 | painter.setPen(self._gridPenL) 635 | x = left 636 | while x < rect.right(): 637 | gridLines.append(QtCore.QLineF( x, rect.top(), x, rect.bottom() )) 638 | x += self._gridSizeCourse 639 | painter.drawLines(gridLines) 640 | 641 | # Draw horizontal thick lines 642 | gridLines = [] 643 | painter.setPen(self._gridPenL) 644 | y = top 645 | while y < rect.bottom(): 646 | gridLines.append(QtCore.QLineF( rect.left(), y, rect.right(), y )) 647 | y += self._gridSizeCourse 648 | painter.drawLines(gridLines) 649 | 650 | return super(GraphView, self).drawBackground(painter, rect) 651 | --------------------------------------------------------------------------------