├── MANIFEST.in ├── example.png ├── qt-dataflow-logo.png ├── qtdataflow ├── examples │ ├── icons │ │ ├── onebit_11.png │ │ ├── onebit_16.png │ │ └── onebit_31.png │ ├── __init__.py │ ├── example_all_together.py │ ├── example_pyqtgraph.py │ ├── example_widget.py │ ├── example_matplotlib_on_canvas.py │ └── example.py ├── tests │ ├── __init__.py │ └── test.py ├── Qt.py ├── __init__.py ├── gui.py ├── model.py └── view.py ├── .gitattributes ├── MANIFEST ├── setup.py ├── LICENSE.txt ├── .gitignore └── README.rst /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tillsten/qt-dataflow/HEAD/example.png -------------------------------------------------------------------------------- /qt-dataflow-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tillsten/qt-dataflow/HEAD/qt-dataflow-logo.png -------------------------------------------------------------------------------- /qtdataflow/examples/icons/onebit_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tillsten/qt-dataflow/HEAD/qtdataflow/examples/icons/onebit_11.png -------------------------------------------------------------------------------- /qtdataflow/examples/icons/onebit_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tillsten/qt-dataflow/HEAD/qtdataflow/examples/icons/onebit_16.png -------------------------------------------------------------------------------- /qtdataflow/examples/icons/onebit_31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tillsten/qt-dataflow/HEAD/qtdataflow/examples/icons/onebit_31.png -------------------------------------------------------------------------------- /qtdataflow/examples/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Tue Feb 19 16:08:03 2013 4 | 5 | @author: tillsten 6 | """ 7 | 8 | -------------------------------------------------------------------------------- /qtdataflow/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Tue Feb 19 16:15:15 2013 4 | 5 | @author: tillsten 6 | """ 7 | 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | README.rst 3 | setup.py 4 | qtdataflow\Qt.py 5 | qtdataflow\__init__.py 6 | qtdataflow\gui.py 7 | qtdataflow\model.py 8 | qtdataflow\view.py 9 | qtdataflow\examples\__init__.py 10 | qtdataflow\examples\example.py 11 | qtdataflow\examples\example_all_together.py 12 | qtdataflow\examples\example_matplotlib_on_canvas.py 13 | qtdataflow\examples\example_pyqtgraph.py 14 | qtdataflow\examples\example_widget.py 15 | qtdataflow\examples\icons\onebit_11.png 16 | qtdataflow\examples\icons\onebit_16.png 17 | qtdataflow\examples\icons\onebit_31.png 18 | qtdataflow\tests\__init__.py 19 | qtdataflow\tests\test.py 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Tue Feb 19 15:46:55 2013 4 | 5 | @author: tillsten 6 | """ 7 | 8 | from distutils.core import setup 9 | 10 | setup(name='qt-dataflow', 11 | version='0.2.3', 12 | description='A base for custom visual programming enviroments', 13 | long_description = open('README.rst', 'rt').read(), 14 | author='Till Stensitzki', 15 | author_email='tillsten@zedat.fu-berlin.de', 16 | url='https://github.com/Tillsten/qt-dataflow', 17 | packages=['qtdataflow', 'qtdataflow.examples', 'qtdataflow.tests'], 18 | package_data={'qtdataflow.examples': ['icons/*.png']}, 19 | ) -------------------------------------------------------------------------------- /qtdataflow/examples/example_all_together.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Tillsten' 2 | import numpy as np 3 | from example import DataGenNode 4 | 5 | 6 | class VarDataGenNode(DataGenNode): 7 | 8 | def __init__(self): 9 | super(VarDataGenNode, self).__init__() 10 | self.accepts_input = True 11 | 12 | def get(self): 13 | if len(self.in_conn) == 1: 14 | m = self.in_conn[0].get() 15 | else: 16 | m = 1 17 | return np.random.randn(self.num_points) * m 18 | 19 | 20 | if __name__ == '__main__': 21 | from qtdataflow.gui import ChartWindow 22 | from qtdataflow.Qt import QtGui 23 | 24 | from example_matplotlib_on_canvas import MatplotlibNode 25 | from example_widget import SpinBoxNode 26 | from example_pyqtgraph import PlotOnCanvasNode 27 | 28 | app = QtGui.QApplication([]) 29 | cw = ChartWindow() 30 | cw.tb.add_node(PlotOnCanvasNode) 31 | cw.tb.add_node(VarDataGenNode) 32 | cw.tb.add_node(MatplotlibNode) 33 | cw.tb.add_node(SpinBoxNode) 34 | cw.show() 35 | app.exec_() 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Till Stensitzki 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the names of its contributors may be used to endorse 12 | or promote products derived from this software without specific prior 13 | written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /qtdataflow/examples/example_pyqtgraph.py: -------------------------------------------------------------------------------- 1 | import pyqtgraph 2 | 3 | __author__ = 'Tillsten' 4 | 5 | 6 | from pyqtgraph import PlotItem 7 | from qtdataflow.model import Node, Schema 8 | from qtdataflow.view import PixmapNodeView, NodeView 9 | from qtdataflow.Qt import QtCore, QtGui 10 | 11 | class PlotOnCanvasItem(NodeView, PlotItem): 12 | def __init__(self, Node): 13 | PlotItem.__init__(self) 14 | self.setMinimumSize(300, 300) 15 | NodeView.__init__(self, Node) 16 | 17 | 18 | class PlotOnCanvasNode(Node): 19 | def __init__(self): 20 | super(PlotOnCanvasNode, self).__init__() 21 | self.node_type = 'pyqtgraph-Plotter' 22 | self.accepts_input = True 23 | 24 | def get_view(self): 25 | p = PlotOnCanvasItem(self) 26 | self.plot = p.plot() 27 | self.plot.setPen(pyqtgraph.mkPen('r', lw=3)) 28 | return p 29 | 30 | def get_toolbar_view(self): 31 | self.icon_path = 'icons/onebit_16.png' 32 | p = PixmapNodeView(self) 33 | return p 34 | 35 | def show_widget(self): 36 | pass 37 | 38 | def update(self): 39 | self.plot.setData(self.in_conn[0].get()) 40 | 41 | def new_connection_out(self, node): 42 | self.timer = QtCore.QTimer() 43 | self.timer.timeout.connect(self.update) 44 | self.timer.start(50.) 45 | 46 | 47 | if __name__ == '__main__': 48 | from example import DataGenNode 49 | from qtdataflow.gui import ChartWindow 50 | from PySide.QtGui import QApplication 51 | app = QApplication([]) 52 | cw = ChartWindow() 53 | cw.tb.add_node(PlotOnCanvasNode) 54 | cw.tb.add_node(DataGenNode) 55 | cw.show() 56 | app.exec_() 57 | -------------------------------------------------------------------------------- /qtdataflow/examples/example_widget.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Tillsten' 2 | 3 | from qtdataflow.view import WidgetNodeView, SchemaView 4 | from qtdataflow.model import Node, Schema 5 | from qtdataflow.Qt import QtGui 6 | QSpinBox = QtGui.QSpinBox 7 | QApplication = QtGui.QApplication 8 | QGraphicsView = QtGui.QGraphicsView 9 | 10 | class SpinBoxNode(Node): 11 | def __init__(self): 12 | super(SpinBoxNode, self).__init__() 13 | self.node_type = 'SpinBoxNode' 14 | self.generates_output = True 15 | self.sb = QSpinBox() 16 | self.sb.setGeometry(0,0,50,25) 17 | self.sb.valueChanged.connect(self.signal_change) 18 | 19 | def get_view(self): 20 | return WidgetNodeView(self) 21 | 22 | def get_widget(self): 23 | return self.sb 24 | 25 | def get(self): 26 | return self.sb.value() 27 | 28 | def signal_change(self): 29 | for n in self.out_conn: 30 | n.update() 31 | 32 | 33 | class SumLabelNode(Node): 34 | def __init__(self): 35 | super(SumLabelNode, self).__init__() 36 | self.node_type = 'Sum Label' 37 | self.accepts_input = True 38 | self.lbl = QLabel() 39 | 40 | def get_view(self): 41 | return WidgetNodeView(self) 42 | 43 | def get_widget(self): 44 | return self.lbl 45 | 46 | def update(self): 47 | s = sum((n.get() for n in self.in_conn)) 48 | self.lbl.setText(str(s)) 49 | 50 | if __name__ == '__main__': 51 | app = QApplication([]) 52 | sch = Schema() 53 | sv = SchemaView(sch) 54 | n = SpinBoxNode() 55 | n2 = SpinBoxNode() 56 | n3 = SumLabelNode() 57 | sch.add_node(n) 58 | sch.add_node(n2) 59 | sch.add_node(n3) 60 | gv = QGraphicsView(sv) 61 | gv.show() 62 | 63 | app.exec_() -------------------------------------------------------------------------------- /qtdataflow/Qt.py: -------------------------------------------------------------------------------- 1 | ## Do all Qt imports from here to allow easier PyQt / PySide compatibility 2 | import sys, re 3 | 4 | ## Automatically determine whether to use PyQt or PySide. 5 | ## This is done by first checking to see whether one of the libraries 6 | ## is already imported. If not, then attempt to import PyQt4, then PySide. 7 | if 'PyQt4' in sys.modules: 8 | USE_PYSIDE = False 9 | elif 'PySide' in sys.modules: 10 | USE_PYSIDE = True 11 | else: 12 | try: 13 | import PyQt4 14 | 15 | USE_PYSIDE = False 16 | except ImportError: 17 | try: 18 | import PySide 19 | 20 | USE_PYSIDE = True 21 | except ImportError: 22 | raise Exception( 23 | "PyQtGraph requires either PyQt4 or PySide; neither package could be imported.") 24 | 25 | if USE_PYSIDE: 26 | from PySide import QtGui, QtCore, QtOpenGL, QtSvg 27 | import PySide 28 | 29 | VERSION_INFO = 'PySide ' + PySide.__version__ 30 | else: 31 | from PyQt4 import QtGui, QtCore 32 | 33 | try: 34 | from PyQt4 import QtSvg 35 | except ImportError: 36 | pass 37 | try: 38 | from PyQt4 import QtOpenGL 39 | except ImportError: 40 | pass 41 | 42 | QtCore.Signal = QtCore.pyqtSignal 43 | VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR 44 | 45 | 46 | ## Make sure we have Qt >= 4.7 47 | versionReq = [4, 7] 48 | QtVersion = PySide.QtCore.__version__ if USE_PYSIDE else QtCore.QT_VERSION_STR 49 | m = re.match(r'(\d+)\.(\d+).*', QtVersion) 50 | if m is not None and list(map(int, m.groups())) < versionReq: 51 | print(map(int, m.groups())) 52 | raise Exception( 53 | 'pyqtgraph requires Qt version >= %d.%d (your version is %s)' % ( 54 | versionReq[0], versionReq[1], QtVersion)) 55 | -------------------------------------------------------------------------------- /qtdataflow/tests/test.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Tillsten' 2 | from nose.tools import raises, assert_raises 3 | from qtdataflow.model import Schema, Node 4 | 5 | 6 | def test_schema(): 7 | schema = Schema() 8 | n1 = Node() 9 | n2 = Node() 10 | n3 = Node() 11 | schema.add_node(n1) 12 | schema.add_node(n2) 13 | schema.add_node(n3) 14 | assert(n1 in schema.nodes) 15 | assert(n2 in schema.nodes) 16 | schema.delete_node(n1) 17 | assert(n1 not in schema.nodes) 18 | 19 | 20 | @raises(ValueError) 21 | def test_schema_exception(): 22 | schema = Schema() 23 | n1 = Node() 24 | schema.add_node(n1) 25 | schema.add_node(n1) 26 | 27 | 28 | def test_schema_connections(): 29 | schema = Schema() 30 | n1 = Node() 31 | n2 = Node() 32 | n3 = Node() 33 | schema.add_node(n1) 34 | schema.add_node(n2) 35 | assert_raises(ValueError, schema.connect_nodes, n1, n1) 36 | schema.connect_nodes(n1, n2) 37 | assert((n1, n2) in schema.connections) 38 | assert(n1.out_conn[0] is n2) 39 | assert(n2.in_conn[0] is n1) 40 | assert_raises(ValueError, schema.disconnect_nodes, n2, n1) 41 | schema.connect_nodes(n3, n2) 42 | schema.disconnect_nodes(n1, n2) 43 | assert(schema.connections == [(n3, n2)]) 44 | assert(n1.out_conn == []) 45 | assert(n2.in_conn == [n3]) 46 | 47 | 48 | def test_schema_tofile(): 49 | from StringIO import StringIO 50 | s = Schema() 51 | n1 = Node() 52 | n2 = Node() 53 | s.add_node(n1) 54 | s.add_node(n2) 55 | s.connect_nodes(n1, n2) 56 | f = StringIO() 57 | s.to_disk(f) 58 | f.seek(0) 59 | s2 = Schema() 60 | s2.from_disk(f) 61 | assert(len(s.connections) == len(s2.connections)) 62 | assert(len(s.nodes) == len(s2.nodes)) 63 | 64 | if __name__ == '__main__': 65 | import nose 66 | nose.run() 67 | 68 | -------------------------------------------------------------------------------- /qtdataflow/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Tue Feb 19 15:46:44 2013 4 | 5 | @author: tillsten 6 | """ 7 | 8 | ## Do all Qt imports from here to allow easier PyQt / PySide compatibility 9 | import sys, re 10 | 11 | ## Automatically determine whether to use PyQt or PySide. 12 | ## This is done by first checking to see whether one of the libraries 13 | ## is already imported. If not, then attempt to import PyQt4, then PySide. 14 | if 'PyQt4' in sys.modules: 15 | USE_PYSIDE = False 16 | elif 'PySide' in sys.modules: 17 | USE_PYSIDE = True 18 | else: 19 | try: 20 | import PyQt4 21 | 22 | USE_PYSIDE = False 23 | except ImportError: 24 | try: 25 | import PySide 26 | 27 | USE_PYSIDE = True 28 | except ImportError: 29 | raise Exception( 30 | "PyQtGraph requires either PyQt4 or PySide; neither package could be imported.") 31 | 32 | if USE_PYSIDE: 33 | from PySide import QtGui, QtCore, QtOpenGL, QtSvg 34 | import PySide 35 | 36 | VERSION_INFO = 'PySide ' + PySide.__version__ 37 | else: 38 | from PyQt4 import QtGui, QtCore 39 | 40 | try: 41 | from PyQt4 import QtSvg 42 | except ImportError: 43 | pass 44 | try: 45 | from PyQt4 import QtOpenGL 46 | except ImportError: 47 | pass 48 | 49 | QtCore.Signal = QtCore.pyqtSignal 50 | VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR 51 | 52 | 53 | ## Make sure we have Qt >= 4.7 54 | versionReq = [4, 7] 55 | QtVersion = PySide.QtCore.__version__ if USE_PYSIDE else QtCore.QT_VERSION_STR 56 | m = re.match(r'(\d+)\.(\d+).*', QtVersion) 57 | if m is not None and list(map(int, m.groups())) < versionReq: 58 | print(map(int, m.groups())) 59 | raise Exception( 60 | 'pyqtgraph requires Qt version >= %d.%d (your version is %s)' % ( 61 | versionReq[0], versionReq[1], QtVersion)) 62 | -------------------------------------------------------------------------------- /qtdataflow/examples/example_matplotlib_on_canvas.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from qtdataflow.model import Node, Schema 3 | from qtdataflow.view import PixmapNodeView 4 | import numpy as np 5 | import matplotlib 6 | 7 | matplotlib.use('Agg') 8 | import matplotlib.pylab as plt 9 | 10 | 11 | class DataGenNode(Node): 12 | """ 13 | A test node which outputs a random number. Widget allow to set the number. 14 | """ 15 | def __init__(self): 16 | super(DataGenNode, self).__init__() 17 | self.node_type = 'Random Array' 18 | self.icon_path = 'icons/onebit_11.png' 19 | self.min = 0 20 | self.max = 1 21 | self.num_points = 100 22 | self.generates_output = True 23 | 24 | def get_view(self): 25 | return PixmapNodeView(self) 26 | 27 | def get(self): 28 | num = np.random.random(self.num_points) * (self.max - self.min) + self.min 29 | return num 30 | 31 | def show_widget(self): 32 | int, ok = Qt.QtGui.QInputDialog.getInteger(None, 'Input Dialog', 33 | 'Number of Points', self.num_points) 34 | if ok: 35 | self.num_points = int 36 | 37 | 38 | def make_plot(y): 39 | fig = plt.figure() 40 | ax = fig.add_subplot(111) 41 | fig.set_size_inches(4, 4) 42 | ax.plot(y) 43 | fig.savefig('tmp.png', dpi=75) 44 | 45 | class MatplotlibNodeView(PixmapNodeView): 46 | def __init__(self, node): 47 | super(MatplotlibNodeView, self).__init__(node) 48 | make_plot([0]) 49 | self.update_view() 50 | 51 | def update_view(self): 52 | self.setPixmap('tmp.png') 53 | self.layout_nodes() 54 | import os 55 | os.remove('tmp.png') 56 | self.update() 57 | 58 | class MatplotlibNode(Node): 59 | def __init__(self): 60 | super(MatplotlibNode, self).__init__() 61 | self.node_type = 'Matplotlib Image' 62 | self.accepts_input = True 63 | self.generates_output = False 64 | self.icon_path = 'icons/onebit_16.png' 65 | 66 | def get_view(self): 67 | self.view = MatplotlibNodeView(self) 68 | return self.view 69 | 70 | def get_toolbar_view(self): 71 | return PixmapNodeView(self) 72 | 73 | def show_widget(self): 74 | a = self.in_conn[0].get() 75 | make_plot(a) 76 | self.view.update_view() 77 | 78 | 79 | if __name__ == '__main__': 80 | from qtdataflow.gui import ChartWindow 81 | from qtdataflow import Qt 82 | app = Qt.QtGui.QApplication([]) 83 | cw = ChartWindow() 84 | cw.tb.add_node(DataGenNode) 85 | cw.tb.add_node(MatplotlibNode) 86 | cw.show() 87 | app.exec_() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .spyderproject 8 | .metadata 9 | bin/ 10 | tmp/ 11 | *.tmp 12 | *.bak 13 | *.swp 14 | *.pyc 15 | *~.nib 16 | local.properties 17 | .classpath 18 | .settings/ 19 | .loadpath 20 | .idea/* 21 | # External tool builders 22 | .externalToolBuilders/ 23 | 24 | # Locally stored "Eclipse launch configurations" 25 | *.launch 26 | 27 | # CDT-specific 28 | .cproject 29 | 30 | # PDT-specific 31 | .buildpath 32 | 33 | 34 | ################# 35 | ## Visual Studio 36 | ################# 37 | 38 | ## Ignore Visual Studio temporary files, build results, and 39 | ## files generated by popular Visual Studio add-ons. 40 | 41 | # User-specific files 42 | *.suo 43 | *.user 44 | *.sln.docstates 45 | 46 | # Build results 47 | [Dd]ebug/ 48 | [Rr]elease/ 49 | *_i.c 50 | *_p.c 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.vspscc 65 | .builds 66 | *.dotCover 67 | 68 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 69 | #packages/ 70 | 71 | # Visual C++ cache files 72 | ipch/ 73 | *.aps 74 | *.ncb 75 | *.opensdf 76 | *.sdf 77 | 78 | # Visual Studio profiler 79 | *.psess 80 | *.vsp 81 | 82 | # ReSharper is a .NET coding add-in 83 | _ReSharper* 84 | 85 | # Installshield output folder 86 | [Ee]xpress 87 | 88 | # DocProject is a documentation generator add-in 89 | DocProject/buildhelp/ 90 | DocProject/Help/*.HxT 91 | DocProject/Help/*.HxC 92 | DocProject/Help/*.hhc 93 | DocProject/Help/*.hhk 94 | DocProject/Help/*.hhp 95 | DocProject/Help/Html2 96 | DocProject/Help/html 97 | 98 | # Click-Once directory 99 | publish 100 | 101 | # Others 102 | [Bb]in 103 | [Oo]bj 104 | sql 105 | TestResults 106 | *.Cache 107 | ClientBin 108 | stylecop.* 109 | ~$* 110 | *.dbmdl 111 | Generated_Code #added for RIA/Silverlight projects 112 | 113 | # Backup & report files from converting an old project file to a newer 114 | # Visual Studio version. Backup files are not needed, because we have git ;-) 115 | _UpgradeReport_Files/ 116 | Backup*/ 117 | UpgradeLog*.XML 118 | 119 | 120 | 121 | ############ 122 | ## Windows 123 | ############ 124 | 125 | # Windows image file caches 126 | Thumbs.db 127 | 128 | # Folder config file 129 | Desktop.ini 130 | 131 | 132 | ############# 133 | ## Python 134 | ############# 135 | 136 | *.py[co] 137 | 138 | # Packages 139 | *.egg 140 | *.egg-info 141 | dist 142 | build 143 | eggs 144 | parts 145 | bin 146 | var 147 | sdist 148 | develop-eggs 149 | .installed.cfg 150 | 151 | # Installer logs 152 | pip-log.txt 153 | 154 | # Unit test / coverage reports 155 | .coverage 156 | .tox 157 | 158 | #Translations 159 | *.mo 160 | 161 | #Mr Developer 162 | .mr.developer.cfg 163 | 164 | # Mac crap 165 | .DS_Store 166 | -------------------------------------------------------------------------------- /qtdataflow/gui.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Tillsten' 2 | 3 | from qtdataflow.Qt import QtGui 4 | from qtdataflow.Qt import QtCore 5 | 6 | 7 | from view import SchemaView, NodeView, PixmapNodeView 8 | from model import Schema 9 | 10 | 11 | class ToolBar(QtGui.QGraphicsView): 12 | """ 13 | Toolbar which show the availeble nodes. 14 | """ 15 | node_clicked = QtCore.Signal(object) 16 | 17 | def __init__(self, parent): 18 | super(ToolBar, self).__init__(parent) 19 | self.nodes = [] 20 | self.scene = QtGui.QGraphicsScene() 21 | self.setRenderHint(QtGui.QPainter.Antialiasing) 22 | self.setScene(self.scene) 23 | self.setStyleSheet("background: transparent") 24 | self._bottom = 0 25 | self.setLayout(QtGui.QHBoxLayout()) 26 | size_pol = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, 27 | QtGui.QSizePolicy.Minimum) 28 | self.setSizePolicy(size_pol) 29 | 30 | def add_node(self, node): 31 | rep = node().get_toolbar_view() 32 | self.scene.addItem(rep) 33 | rep.setPos(0., self._bottom) 34 | rep.setFlag(rep.ItemIsMovable, False) 35 | rep.setFlag(rep.ItemIsSelectable, False) 36 | rep.setHandlesChildEvents(True) 37 | rect = (rep.childrenBoundingRect() | rep.boundingRect()) 38 | self._bottom += rect.height() + 5 39 | self.setSceneRect(self.scene.itemsBoundingRect().adjusted(-3, 0, 0, 0)) 40 | 41 | def mousePressEvent(self, ev): 42 | super(ToolBar, self).mouseReleaseEvent(ev) 43 | item = self.itemAt(ev.pos()) 44 | if item is not None: 45 | if not hasattr(item, 'node'): 46 | item = item.parentItem() 47 | t = type(item.node) 48 | self.node_clicked.emit(t()) 49 | 50 | 51 | class ChartWindow(QtGui.QWidget): 52 | def __init__(self, schema=None, parent=None): 53 | super(ChartWindow, self).__init__(parent) 54 | lay = QtGui.QHBoxLayout() 55 | self.setWindowTitle("qt-flowgraph") 56 | self.setLayout(lay) 57 | self.setMinimumSize(500, 300) 58 | self.tb = ToolBar(self) 59 | self.view = QtGui.QGraphicsView(self) 60 | self.view.setRenderHint(QtGui.QPainter.Antialiasing) 61 | self.view.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) 62 | self.schema = schema or Schema() 63 | self.sv = SchemaView(self.schema) 64 | self.view.setScene(self.sv) 65 | 66 | lay.addWidget(self.tb) 67 | lay.addWidget(self.view) 68 | 69 | self.tb.node_clicked.connect(self.schema.add_node) 70 | 71 | 72 | 73 | class SchemaApp(QtGui.QMainWindow): 74 | def __init__(self): 75 | super(SchemaApp, self).__init__() 76 | cw = ChartWindow(self) 77 | self.setCentralWidget(cw) 78 | tb = self.addToolBar() 79 | save_action = QtGui.QAction('Save') 80 | 81 | 82 | 83 | 84 | 85 | 86 | class SaveAction(QtGui.QAction): 87 | def __init__(self): 88 | 89 | super(SaveAction, self).__init__() 90 | self.setIconText(QtGui.QStyle.SP_DialogSaveButton) 91 | self.iconText('Save Schema') 92 | -------------------------------------------------------------------------------- /qtdataflow/examples/example.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Simple example. Has random generating Node, a filter node and a plotting node. 4 | """ 5 | from __future__ import print_function 6 | from qtdataflow.model import Node, Schema 7 | from qtdataflow.view import PixmapNodeView 8 | import numpy as np 9 | import matplotlib 10 | matplotlib.use('Qt4Agg') 11 | import matplotlib.pylab as plt 12 | 13 | 14 | class DataGenNode(Node): 15 | """ 16 | A test node which outputs a random number. Widget allow to set the number. 17 | """ 18 | def __init__(self): 19 | super(DataGenNode, self).__init__() 20 | self.node_type = 'Random Array' 21 | self.icon_path = 'icons/onebit_11.png' 22 | self.min = 0 23 | self.max = 1 24 | self.num_points = 100 25 | self.generates_output = True 26 | 27 | def get_view(self): 28 | return PixmapNodeView(self) 29 | 30 | def get(self): 31 | num = np.random.random(self.num_points) * (self.max - self.min) + self.min 32 | return num 33 | 34 | def show_widget(self): 35 | int, ok = Qt.QtGui.QInputDialog.getInteger(None, 'Input Dialog', 36 | 'Number of Points', self.num_points) 37 | if ok: 38 | self.num_points = int 39 | 40 | 41 | class FilterNode(Node): 42 | """ 43 | Applies a simple on the data filter. 44 | """ 45 | def __init__(self): 46 | super(FilterNode, self).__init__() 47 | self.accepts_input = True 48 | self.generates_output = True 49 | self.node_type = 'Filter' 50 | self.icon_path = 'icons/onebit_31.png' 51 | self.rel = 1. 52 | 53 | def get_view(self): 54 | return PixmapNodeView(self) 55 | 56 | def get(self): 57 | data = self.in_conn[0].get() 58 | m = data.mean() 59 | s = data.std() 60 | return np.where(np.abs(data - m) > self.rel * s, m, data) 61 | 62 | def show_widget(self): 63 | int, ok = QInputDialog.getDouble(None, 'Input Dialog', 64 | 'Number of Points', self.rel) 65 | if ok: 66 | self.sel = int 67 | 68 | 69 | 70 | class PlotNode(Node): 71 | """ 72 | A test node to plot output from Datagen Node. 73 | """ 74 | def __init__(self): 75 | super(PlotNode, self).__init__() 76 | self.accepts_input = True 77 | self.node_type = 'Plotter' 78 | self.icon_path = 'icons/onebit_16.png' 79 | 80 | def get_view(self): 81 | return PixmapNodeView(self) 82 | 83 | def show_widget(self): 84 | data = [i.get() for i in self.in_conn] 85 | fig = plt.figure() 86 | ax = fig.add_subplot(111) 87 | for d in data: 88 | ax.plot(d) 89 | fig.show() 90 | 91 | if __name__ == '__main__': 92 | 93 | from qtdataflow.gui import ChartWindow 94 | app = Qt.QtGui.QApplication([]) 95 | cw = ChartWindow() 96 | cw.tb.add_node(FilterNode) 97 | cw.tb.add_node(DataGenNode) 98 | cw.tb.add_node(PlotNode) 99 | f = FilterNode() 100 | d = DataGenNode() 101 | cw.show() 102 | app.exec_() 103 | 104 | -------------------------------------------------------------------------------- /qtdataflow/model.py: -------------------------------------------------------------------------------- 1 | from qtdataflow.Qt import QtCore 2 | QObject = QtCore.QObject 3 | Signal = QtCore.Signal 4 | 5 | 6 | class Node(object): 7 | """ 8 | Logical Representation of a node. 9 | """ 10 | 11 | def __init__(self): 12 | #self.schema = schema 13 | self.node_type = 'BaseNode' 14 | self.accepts_input = False 15 | self.generates_output = False 16 | self.out_conn = [] 17 | self.in_conn = [] 18 | 19 | def accept_type(self, node): 20 | return True 21 | 22 | def get_view(self): 23 | """ 24 | Which view-class to use, has to return a QGraphicsItem 25 | """ 26 | raise NotImplemented 27 | 28 | def get_toolbar_view(self): 29 | """ 30 | Which view-class is used in the Toolbar, defaults to 31 | the standard view. 32 | """ 33 | return self.get_view() 34 | 35 | def new_connection_out(self, node): 36 | """ 37 | Called if a new connection (out) was made. 38 | """ 39 | pass 40 | 41 | def new_connection_in(self, node): 42 | pass 43 | 44 | 45 | class MultiTerminalNode(Node): 46 | """ 47 | Node which can have more than one input/output Terminal. 48 | """ 49 | 50 | def __init__(self): 51 | Node.__init__(self) 52 | self.input_terminals = {} 53 | self.output_terminals = {} 54 | 55 | @property 56 | def accepts_input(self): 57 | return len(self.input_terminals) > 0 58 | 59 | @property 60 | def generates_output(self): 61 | return len(self.output_terminals) > 0 62 | 63 | 64 | 65 | 66 | 67 | class Schema(QObject): 68 | """ 69 | Model a Schema, which includes all Nodes and connections. 70 | """ 71 | node_created = Signal(Node) 72 | node_deleted = Signal(Node) 73 | nodes_connected = Signal(list) 74 | nodes_disconnected = Signal(list) 75 | 76 | def __init__(self): 77 | super(Schema, self).__init__() 78 | self.nodes = [] 79 | self.connections = [] 80 | 81 | def add_node(self, node): 82 | """ 83 | Add given Node to the Schema. 84 | """ 85 | if node not in self.nodes: 86 | self.nodes.append(node) 87 | self.node_created.emit(node) 88 | else: 89 | raise ValueError('Node already in Schema.') 90 | 91 | def delete_node(self, node): 92 | """ 93 | Deletes given Node from the Schema, calls node_deleted event. 94 | """ 95 | to_delete = [(o, i) for (o, i) in self.connections 96 | if o == node or i == node] 97 | 98 | for o, i in to_delete: 99 | self.disconnect_nodes(o, i) 100 | 101 | self.nodes.remove(node) 102 | self.node_deleted.emit(node) 103 | 104 | def connect_nodes(self, out_node, in_node): 105 | if out_node is in_node: 106 | raise ValueError("Node can't connect to itself") 107 | out_node.out_conn.append(in_node) 108 | in_node.in_conn.append(out_node) 109 | 110 | self.connections.append((out_node, in_node)) 111 | out_node.new_connection_in(in_node) 112 | in_node.new_connection_out(out_node) 113 | self.nodes_connected.emit([out_node, in_node]) 114 | 115 | def disconnect_nodes(self, out_node, in_node): 116 | if (out_node, in_node) not in self.connections: 117 | raise ValueError("Nodes are not connected") 118 | self.nodes_disconnected.emit([out_node, in_node]) 119 | out_node.out_conn.remove(in_node) 120 | in_node.in_conn.remove(out_node) 121 | self.connections.remove((out_node, in_node)) 122 | 123 | def to_disk(self, file): 124 | import pickle 125 | to_pickle = (self.nodes, self.connections) 126 | return pickle.dump(to_pickle, file) 127 | 128 | def from_disk(self, file): 129 | import pickle 130 | nodes, connections = pickle.load(file) 131 | for n in nodes: 132 | self.add_node(n) 133 | for c in connections: 134 | self.connect_nodes(*c) 135 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | qt-dataflow 3 | =========== 4 | .. image:: https://github.com/Tillsten/qt-dataflow/raw/master/qt-dataflow-logo.png 5 | 6 | This package tries to provide components for building your own 7 | visual programming environment. The authors aim is to make his 8 | data analysis tool available to his colleagues who don't 9 | know programming or Python. 10 | 11 | Because a standard gui is not very flexible, this projects tries 12 | to make visual canvas on which the dataflow can be defined and modified. 13 | Extensibility is given through simply adding or modifying Nodes. 14 | 15 | 16 | This project is inspired by Orange - where i did not see an easy way to just 17 | use the canvas part (also: license differences). Also the design tries 18 | to be more flexible. 19 | 20 | 21 | Requirements 22 | ------------ 23 | It is made with Python 2.7. Not tested for lower versions or 24 | Python 3 (patches welcome). It should work with PySide and PyQt, 25 | but at the moment, the imports need to be manually changed. 26 | 27 | The examples may have additional requirements: 28 | * numpy 29 | * matplotlib 30 | * pyqtgraph 31 | 32 | Examples 33 | -------- 34 | See example.py for an simple example using icons which react to double click. 35 | To make a connection draw from one nodes termial to another 36 | (only out-> in is allowed). 37 | 38 | .. image:: https://github.com/Tillsten/qt-dataflow/raw/master/example.png 39 | 40 | * example_widget uses widgets on the canvas directly, it also implements 41 | a simple callback. Note how the label updates after changing the 42 | SpinBox-value. 43 | 44 | * example_pyqtgraph need also the pyqtgraph package. It plot directly on the 45 | canvas. 46 | 47 | * example_matplotlib_on_canvas does the same, but uses matplotlib via 48 | a temporary file. 49 | 50 | Code Example 51 | ------------ 52 | To make custom nodes you need to subclass Node. It must return 53 | a NodeView via its 'get_view' method. The following example 54 | implements a Node which make a random number. 55 | 56 | .. code-block:: python 57 | 58 | class RandomNumber(Node): 59 | """ 60 | A test node which outputs a random number. Widget allow to set the number. 61 | """ 62 | def __init__(self): 63 | super(DataGenNode, self).__init__() 64 | #Node type/name 65 | self.node_type = 'Random Array' 66 | #Icon_path is needed for the PixmapNodeView 67 | self.icon_path = 'icons/onebit_11.png' 68 | #The makes the node have an output terminal. 69 | self.generates_output = True 70 | 71 | def get_view(self): 72 | return PixmapNodeView(self) 73 | 74 | def get(self): 75 | #Method which can be called by other nodes. The name is just 76 | #a convention. 77 | num = [random.random() for i in range(self.num_points)] 78 | return num 79 | 80 | def show_widget(self): 81 | #Method called by double clicking on the icon. 82 | int, ok = Qt.QtGui.QInputDialog.getInteger(None, 'Input Dialog', 83 | 'Number of Points', self.num_points) 84 | if ok: 85 | self.num_points = int 86 | 87 | 88 | A node saves its connections in node.in_conn and node.out_conn. Also 89 | note, that each node view must be a child of a QGraphicsItem and NodeView. 90 | 91 | 92 | Structure 93 | --------- 94 | 95 | In model the base Node- and Schema-classes are found. In view are some 96 | view available. gui contains some additional ready to use elements. 97 | 98 | Todo 99 | ---- 100 | * add different icons (simple) 101 | * nicer toolbar (drag-n-drop would be nice) 102 | * test persistence, define a stable protocol if pickling does not work 103 | * make an example with less requirements. 104 | * checking and introducing a connection type 105 | * move some logic for allowing or denying connections 106 | from SchemaView to the NodeView. 107 | * checking and improving compatibility with different Python versions. 108 | * better documentation 109 | * make number of terminals variable. 110 | * ... 111 | 112 | Coding Style 113 | ------------ 114 | This projects tries to follow PEP8. 115 | 116 | License 117 | ------- 118 | Example icons are from http://www.icojam.com/blog/?p=177 (Public Domain). 119 | Qt.py from pyqtgraph 120 | BSD - 3 clauses, see license.txt. 121 | -------------------------------------------------------------------------------- /qtdataflow/view.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from qtdataflow.Qt import QtCore, QtGui 3 | QRectF = QtCore.QRectF 4 | QPointF = QtCore.QPointF 5 | 6 | class TerminalItem(QtGui.QGraphicsEllipseItem): 7 | 8 | def __init__(self): 9 | super(TerminalItem).__init__(self) 10 | 11 | def add_label(self, text): 12 | self.label = QtGui.QGraphicsSimpleTextItem(text, self) 13 | #self.label.align 14 | #TODO make text left align for in, etc for out. 15 | self.label.setPos(self.boundingRect().top()) 16 | 17 | 18 | 19 | #TODO if multiterm node is finnished, it should be the baseclass 20 | class NodeView(object): 21 | """ 22 | Class responsible for drawing and interaction of a Node. Note that 23 | your subclass has to be a subtype of QGraphicsItem. (only one qt 24 | parent is allowed) 25 | """ 26 | def __init__(self, node, *args): 27 | #super(NodeView, self).__init__(*args) 28 | self.node = node 29 | self.setAcceptHoverEvents(True) 30 | flags = [QtGui.QGraphicsItem.ItemIsMovable, 31 | QtGui.QGraphicsItem.ItemIsSelectable] 32 | for f in flags: 33 | self.setFlag(f) 34 | 35 | self.setGraphicsEffect(None) 36 | self.add_label(node.node_type) 37 | self.add_terminals() 38 | 39 | def add_terminals(self): 40 | """ 41 | Adds terminals to the item. 42 | """ 43 | if self.node.accepts_input: 44 | term = TerminalItem(self) 45 | term.setRect(0, 0, 10, 10) 46 | term._con = 'in' 47 | self.term_in = term 48 | 49 | if self.node.generates_output: 50 | term = TerminalItem(self) 51 | term.setRect(0, 0, 10, 10) 52 | term._con = 'out' 53 | self.term_out = term 54 | 55 | self.layout_nodes() 56 | 57 | def layout_nodes(self): 58 | if hasattr(self, 'term_in'): 59 | pos = _get_left(self.boundingRect()) 60 | pos += QtCore.QPointF(-15., 0.) 61 | self.term_in.setPos(pos) 62 | 63 | if hasattr(self, 'term_out'): 64 | pos = _get_right(self.boundingRect()) 65 | pos += QtCore.QPointF(5., 0.) 66 | self.term_out.setPos(pos) 67 | 68 | if hasattr(self, 'label'): 69 | self.label.setPos(self.boundingRect().bottomLeft()) 70 | 71 | def add_label(self, text): 72 | self.label = QtGui.QGraphicsSimpleTextItem(text, self) 73 | self.label.setPos(self.boundingRect().bottomLeft()) 74 | 75 | def hoverEnterEvent(self, ev): 76 | self.setGraphicsEffect(QtGui.QGraphicsColorizeEffect()) 77 | 78 | def hoverLeaveEvent(self, ev): 79 | self.setGraphicsEffect(None) 80 | 81 | def mouseDoubleClickEvent(self, *args, **kwargs): 82 | self.node.show_widget() 83 | 84 | 85 | class MultiTermNodeView(NodeView): 86 | def add_terminals(self): 87 | for t in self.terms_in: 88 | self.add_terminal(t, 'in') 89 | 90 | for t in self.terms_out: 91 | self.add_terminal(t, 'out') 92 | 93 | def add_terminal(self, name, io_type): 94 | """ 95 | Adds a single terminal to the view. 96 | """ 97 | assert(io_type in 'in', 'out') 98 | term = TerminalItem(self) 99 | term.setRect(0, 0, 10, 10) 100 | term._con = io_type 101 | term.add_label(name) 102 | self.term_dict[name] = term 103 | 104 | def layout_nodes(self): 105 | n_out = len(self.terms_out) 106 | coords = _get_n_side(self.rect(), n_out, 'right') 107 | for i, name in enumerate(self.terms_out): 108 | self.term_dict[name].setPos(coords[i]) 109 | 110 | n_out = len(self.terms_in) 111 | coords = _get_n_side(self.rect(), n_out, 'left') 112 | for i, name in enumerate(self.terms_out): 113 | self.term_dict[name].setPos(coords[i]) 114 | 115 | 116 | 117 | 118 | 119 | class PixmapNodeView(NodeView, QtGui.QGraphicsPixmapItem): 120 | """ 121 | Node using a pixmap (icon). 122 | """ 123 | 124 | def __init__(self, node, *args): 125 | QtGui.QGraphicsPixmapItem.__init__(self) 126 | pixmap = QtGui.QPixmap(node.icon_path) 127 | self.setPixmap(pixmap) 128 | self.setScale(1.) 129 | NodeView.__init__(self, node, *args) 130 | 131 | 132 | class WidgetNodeView(NodeView, QtGui.QGraphicsRectItem): 133 | """ 134 | Node using a full fledged widget. 135 | """ 136 | def __init__(self, node): 137 | QtGui.QGraphicsRectItem.__init__(self, 0., 0., 70., 50.) 138 | proxy = QtGui.QGraphicsProxyWidget(self) 139 | proxy.setWidget(node.get_widget()) 140 | proxy.setPos(15., 15.) 141 | NodeView.__init__(self, node) 142 | 143 | 144 | class LinkLine(QtGui.QGraphicsPathItem): 145 | """ 146 | Like Link line, but only one pos is a node. 147 | """ 148 | def __init__(self): 149 | super(LinkLine, self).__init__() 150 | self.pen = QtGui.QPen() 151 | self.pen.setWidth(3) 152 | self.setPen(self.pen) 153 | 154 | def paint(self, *args): 155 | start_pos = self.end_pos 156 | end_pos = self.start_pos 157 | path_rect = QRectF(start_pos, end_pos) 158 | path = QtGui.QPainterPath(path_rect.topLeft()) 159 | path.cubicTo(path_rect.topRight(), 160 | path_rect.bottomLeft(), 161 | path_rect.bottomRight()) 162 | self.setPath(path) 163 | super(LinkLine, self).paint(*args) 164 | 165 | 166 | class LinkNodesLine(LinkLine): 167 | """ 168 | Visual representation for a connection between nodes. 169 | """ 170 | def __init__(self, from_node, to_node): 171 | super(LinkNodesLine, self).__init__() 172 | self.from_node = from_node 173 | self.to_node = to_node 174 | self.setFlag(self.ItemIsSelectable) 175 | 176 | def paint(self, *args): 177 | self.start_pos = self.from_node.sceneBoundingRect().center() 178 | self.end_pos = self.to_node.sceneBoundingRect().center() 179 | super(LinkNodesLine, self).paint(*args) 180 | 181 | 182 | class TempLinkLine(LinkLine): 183 | """ 184 | Line from node to end_pos 185 | """ 186 | def __init__(self, from_node, pos): 187 | super(TempLinkLine, self).__init__() 188 | self.from_node = from_node 189 | self.end_pos = pos 190 | 191 | def paint(self, *args): 192 | self.start_pos = self.from_node.sceneBoundingRect().center() 193 | super(TempLinkLine, self).paint(*args) 194 | 195 | 196 | # Simple helper funcs to get points of a QRectF 197 | def _get_right(rect): 198 | return QPointF(rect.right(), rect.bottom() - rect.height() / 2.) 199 | 200 | 201 | def _get_left(rect): 202 | return QPointF(rect.left(), rect.bottom() - rect.height() / 2.) 203 | 204 | 205 | def _get_n_side(rect, n, side): 206 | if side == 'left': 207 | x = rect.left() 208 | elif side == 'right': 209 | x = rect.right() 210 | part_h = rect.height() / (n + 1.) 211 | points = [] 212 | for i in range(1, n + 1.): 213 | p = QPointF(x, rect.bottom() - part_h * i) 214 | points.append(p) 215 | return points 216 | 217 | 218 | def _get_bot(rect): 219 | return QPointF(rect.left() + rect.width() / 2., rect.bottom()) 220 | 221 | 222 | class SchemaView(QtGui.QGraphicsScene): 223 | """ 224 | The view of a Schema, manges GUI interaction. 225 | """ 226 | def __init__(self, schema, *args): 227 | super(SchemaView, self).__init__(*args) 228 | self.schema = schema 229 | self.drawn_icons = [] 230 | self._pressed = None 231 | self.nodes_drawn = {} 232 | self.connections_drawn = {} 233 | self.connect_to_schema_sig() 234 | 235 | 236 | def connect_to_schema_sig(self): 237 | self.schema.node_created.connect(self.draw_schema) 238 | self.schema.nodes_connected.connect(self.add_link) 239 | self.schema.node_deleted.connect(self.remove_node) 240 | self.schema.nodes_disconnected.connect(self.remove_link) 241 | 242 | def draw_schema(self): 243 | """ 244 | Draw Nodes 245 | """ 246 | i = 0 247 | for n in self.schema.nodes: 248 | if n not in self.nodes_drawn: 249 | it = n.get_view() 250 | self.addItem(it) 251 | self.nodes_drawn[n] = it 252 | offset = QPointF(100., 0.) 253 | offset.setX(i * 100) 254 | it.setPos(it.pos() + offset) 255 | i += 1 256 | 257 | def add_link(self, nodes): 258 | """ 259 | Adds connection between nodes. 260 | """ 261 | out_node, in_node = nodes 262 | in_it = self.nodes_drawn[in_node] 263 | out_it = self.nodes_drawn[out_node] 264 | 265 | ll = LinkNodesLine(out_it.term_out, in_it.term_in) 266 | self.addItem(ll) 267 | self.connections_drawn[(out_it.node, in_it.node)] = ll 268 | 269 | def remove_link(self, nodes): 270 | """Remove connection from view""" 271 | node_out, node_in = nodes 272 | ll = self.connections_drawn.pop((node_out, node_in)) 273 | self.removeItem(ll) 274 | 275 | def remove_node(self, node): 276 | """Remove node from view""" 277 | self.removeItem(self.nodes_drawn[node]) 278 | self.nodes_drawn[node] = None 279 | 280 | #--------------------- Eventhandling after here ----------------- 281 | 282 | def mousePressEvent(self, ev): 283 | super(SchemaView, self).mousePressEvent(ev) 284 | it = self.itemAt(ev.scenePos()) 285 | #Check if connection is started 286 | if hasattr(it, '_con'): 287 | self.temp_ll = TempLinkLine(it, ev.scenePos()) 288 | self.addItem(self.temp_ll) 289 | self._pressed = True 290 | self._start_con = it._con 291 | self._start_node = it.parentItem().node 292 | 293 | def mouseMoveEvent(self, ev): 294 | #While connectiong, draw temp line 295 | super(SchemaView, self).mouseMoveEvent(ev) 296 | if self._pressed: 297 | self.temp_ll.end_pos = ev.scenePos() 298 | self.temp_ll.update() 299 | 300 | def mouseReleaseEvent(self, ev): 301 | super(SchemaView, self).mouseReleaseEvent(ev) 302 | #If connecting, check if endpoint is ok and add connection. 303 | if self._pressed: 304 | it = self.items(ev.scenePos()) 305 | it = [i for i in it if hasattr(i, '_con')] 306 | it = it[0] if len(it) > 0 else None 307 | if hasattr(it, '_con'): 308 | if it._con != self._start_con: 309 | if it._con == 'in': 310 | in_node = it.parentItem().node 311 | self.schema.connect_nodes(self._start_node, in_node) 312 | else: 313 | out_node = it.parentItem().node 314 | self.schema.connect_nodes(out_node, self._start_node) 315 | self.removeItem(self.temp_ll) 316 | self._pressed = False 317 | self._start_node = None 318 | 319 | def keyPressEvent(self, ev): 320 | super(SchemaView, self).keyPressEvent(ev) 321 | # Delte canvas item 322 | if ev.key() == QtCore.Qt.Key_Delete and self.selectedItems() != []: 323 | for it in self.selectedItems(): 324 | #Delete connections 325 | if it in self.connections_drawn.values(): 326 | self.schema.disconnect_nodes(it.from_node.parentItem().node, 327 | it.to_node.parentItem().node) 328 | #Delete Nodes 329 | if it in self.nodes_drawn.values(): 330 | self.schema.delete_node(it.node) 331 | 332 | 333 | 334 | --------------------------------------------------------------------------------