Source code for pyqtgraph.flowchart.Flowchart

# -*- coding: utf-8 -*-
from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYQT5
from .Node import *
from ..pgcollections import OrderedDict
from ..widgets.TreeWidget import *
from .. import FileDialog, DataTreeWidget

## pyside and pyqt use incompatible ui files.
    from . import FlowchartTemplate_pyside as FlowchartTemplate
    from . import FlowchartCtrlTemplate_pyside as FlowchartCtrlTemplate
elif USE_PYQT5:
    from . import FlowchartTemplate_pyqt5 as FlowchartTemplate
    from . import FlowchartCtrlTemplate_pyqt5 as FlowchartCtrlTemplate
    from . import FlowchartTemplate_pyqt as FlowchartTemplate
    from . import FlowchartCtrlTemplate_pyqt as FlowchartCtrlTemplate
from .Terminal import Terminal
from numpy import ndarray
from .library import LIBRARY
from ..debug import printExc
from .. import configfile as configfile
from .. import dockarea as dockarea
from . import FlowchartGraphicsView
from .. import functions as fn

def strDict(d):
    return dict([(str(k), v) for k, v in d.items()])


[docs]class Flowchart(Node): sigFileLoaded = QtCore.Signal(object) sigFileSaved = QtCore.Signal(object) #sigOutputChanged = QtCore.Signal() ## inherited from Node sigChartLoaded = QtCore.Signal() sigStateChanged = QtCore.Signal() # called when output is expected to have changed sigChartChanged = QtCore.Signal(object, object, object) # called when nodes are added, removed, or renamed. # (self, action, node)
[docs] def __init__(self, terminals=None, name=None, filePath=None, library=None): self.library = library or LIBRARY if name is None: name = "Flowchart" if terminals is None: terminals = {} self.filePath = filePath Node.__init__(self, name, allowAddInput=True, allowAddOutput=True) ## create node without terminals; we'll add these later self.inputWasSet = False ## flag allows detection of changes in the absence of input change. self._nodes = {} self.nextZVal = 10 #self.connects = [] #self._chartGraphicsItem = FlowchartGraphicsItem(self) self._widget = None self._scene = None self.processing = False ## flag that prevents recursive node updates self.widget() self.inputNode = Node('Input', allowRemove=False, allowAddOutput=True) self.outputNode = Node('Output', allowRemove=False, allowAddInput=True) self.addNode(self.inputNode, 'Input', [-150, 0]) self.addNode(self.outputNode, 'Output', [300, 0]) self.outputNode.sigOutputChanged.connect(self.outputChanged) self.outputNode.sigTerminalRenamed.connect(self.internalTerminalRenamed) self.inputNode.sigTerminalRenamed.connect(self.internalTerminalRenamed) self.outputNode.sigTerminalRemoved.connect(self.internalTerminalRemoved) self.inputNode.sigTerminalRemoved.connect(self.internalTerminalRemoved) self.outputNode.sigTerminalAdded.connect(self.internalTerminalAdded) self.inputNode.sigTerminalAdded.connect(self.internalTerminalAdded) self.viewBox.autoRange(padding = 0.04) for name, opts in terminals.items(): self.addTerminal(name, **opts)
def setLibrary(self, lib): self.library = lib self.widget().chartWidget.buildMenu()
[docs] def setInput(self, **args): """Set the input values of the flowchart. This will automatically propagate the new values throughout the flowchart, (possibly) causing the output to change. """ #print "setInput", args #Node.setInput(self, **args) #print " ....." self.inputWasSet = True self.inputNode.setOutput(**args)
def outputChanged(self): ## called when output of internal node has changed vals = self.outputNode.inputValues() self.widget().outputChanged(vals) self.setOutput(**vals) #self.sigOutputChanged.emit(self)
[docs] def output(self): """Return a dict of the values on the Flowchart's output terminals. """ return self.outputNode.inputValues()
def nodes(self): return self._nodes def addTerminal(self, name, **opts): term = Node.addTerminal(self, name, **opts) name = if opts['io'] == 'in': ## inputs to the flowchart become outputs on the input node opts['io'] = 'out' opts['multi'] = False self.inputNode.sigTerminalAdded.disconnect(self.internalTerminalAdded) try: term2 = self.inputNode.addTerminal(name, **opts) finally: self.inputNode.sigTerminalAdded.connect(self.internalTerminalAdded) else: opts['io'] = 'in' #opts['multi'] = False self.outputNode.sigTerminalAdded.disconnect(self.internalTerminalAdded) try: term2 = self.outputNode.addTerminal(name, **opts) finally: self.outputNode.sigTerminalAdded.connect(self.internalTerminalAdded) return term def removeTerminal(self, name): #print "remove:", name term = self[name] inTerm = self.internalTerminal(term) Node.removeTerminal(self, name) inTerm.node().removeTerminal( def internalTerminalRenamed(self, term, oldName): self[oldName].rename( def internalTerminalAdded(self, node, term): if term._io == 'in': io = 'out' else: io = 'in' Node.addTerminal(self,, io=io, renamable=term.isRenamable(), removable=term.isRemovable(), multiable=term.isMultiable()) def internalTerminalRemoved(self, node, term): try: Node.removeTerminal(self, except KeyError: pass def terminalRenamed(self, term, oldName): newName = #print "flowchart rename", newName, oldName #print self.terminals Node.terminalRenamed(self, self[oldName], oldName) #print self.terminals for n in [self.inputNode, self.outputNode]: if oldName in n.terminals: n[oldName].rename(newName) def createNode(self, nodeType, name=None, pos=None): if name is None: n = 0 while True: name = "%s.%d" % (nodeType, n) if name not in self._nodes: break n += 1 node = self.library.getNodeType(nodeType)(name) self.addNode(node, name, pos) return node def addNode(self, node, name, pos=None): if pos is None: pos = [0, 0] if type(pos) in [QtCore.QPoint, QtCore.QPointF]: pos = [pos.x(), pos.y()] item = node.graphicsItem() item.setZValue(self.nextZVal*2) self.nextZVal += 1 self.viewBox.addItem(item) item.moveBy(*pos) self._nodes[name] = node self.widget().addNode(node) node.sigClosed.connect(self.nodeClosed) node.sigRenamed.connect(self.nodeRenamed) node.sigOutputChanged.connect(self.nodeOutputChanged) self.sigChartChanged.emit(self, 'add', node) def removeNode(self, node): node.close() def nodeClosed(self, node): del self._nodes[] self.widget().removeNode(node) for signal in ['sigClosed', 'sigRenamed', 'sigOutputChanged']: try: getattr(node, signal).disconnect(self.nodeClosed) except (TypeError, RuntimeError): pass self.sigChartChanged.emit(self, 'remove', node) def nodeRenamed(self, node, oldName): del self._nodes[oldName] self._nodes[] = node self.widget().nodeRenamed(node, oldName) self.sigChartChanged.emit(self, 'rename', node) def arrangeNodes(self): pass
[docs] def internalTerminal(self, term): """If the terminal belongs to the external Node, return the corresponding internal terminal""" if term.node() is self: if term.isInput(): return self.inputNode[] else: return self.outputNode[] else: return term
[docs] def connectTerminals(self, term1, term2): """Connect two terminals together within this flowchart.""" term1 = self.internalTerminal(term1) term2 = self.internalTerminal(term2) term1.connectTo(term2)
[docs] def process(self, **args): """ Process data through the flowchart, returning the output. Keyword arguments must be the names of input terminals. The return value is a dict with one key per output terminal. """ data = {} ## Stores terminal:value pairs ## determine order of operations ## order should look like [('p', node1), ('p', node2), ('d', terminal1), ...] ## Each tuple specifies either (p)rocess this node or (d)elete the result from this terminal order = self.processOrder() #print "ORDER:", order ## Record inputs given to process() for n, t in self.inputNode.outputs().items(): # if n not in args: # raise Exception("Parameter %s required to process this chart." % n) if n in args: data[t] = args[n] ret = {} ## process all in order for c, arg in order: if c == 'p': ## Process a single node #print "===> process:", arg node = arg if node is self.inputNode: continue ## input node has already been processed. ## get input and output terminals for this node outs = list(node.outputs().values()) ins = list(node.inputs().values()) ## construct input value dictionary args = {} for inp in ins: inputs = inp.inputTerminals() if len(inputs) == 0: continue if inp.isMultiValue(): ## multi-input terminals require a dict of all inputs args[] = dict([(i, data[i]) for i in inputs if i in data]) else: ## single-inputs terminals only need the single input value available args[] = data[inputs[0]] if node is self.outputNode: ret = args ## we now have the return value, but must keep processing in case there are other endpoint nodes in the chart else: try: if node.isBypassed(): result = node.processBypassed(args) else: result = node.process(display=False, **args) except: print("Error processing node %s. Args are: %s" % (str(node), str(args))) raise for out in outs: #print " Output:", out, #print try: data[out] = result[] except KeyError: pass elif c == 'd': ## delete a terminal result (no longer needed; may be holding a lot of memory) #print "===> delete", arg if arg in data: del data[arg] return ret
[docs] def processOrder(self): """Return the order of operations required to process this chart. The order returned should look like [('p', node1), ('p', node2), ('d', terminal1), ...] where each tuple specifies either (p)rocess this node or (d)elete the result from this terminal """ ## first collect list of nodes/terminals and their dependencies deps = {} tdeps = {} ## {terminal: [nodes that depend on terminal]} for name, node in self._nodes.items(): deps[node] = node.dependentNodes() for t in node.outputs().values(): tdeps[t] = t.dependentNodes() #print "DEPS:", deps ## determine correct node-processing order #deps[self] = [] order = fn.toposort(deps) #print "ORDER1:", order ## construct list of operations ops = [('p', n) for n in order] ## determine when it is safe to delete terminal values dels = [] for t, nodes in tdeps.items(): lastInd = 0 lastNode = None for n in nodes: ## determine which node is the last to be processed according to order if n is self: lastInd = None break else: try: ind = order.index(n) except ValueError: continue if lastNode is None or ind > lastInd: lastNode = n lastInd = ind #tdeps[t] = lastNode if lastInd is not None: dels.append((lastInd+1, t)) dels.sort(key=lambda a: a[0], reverse=True) for i, t in dels: ops.insert(i, ('d', t)) return ops
[docs] def nodeOutputChanged(self, startNode): """Triggered when a node's output values have changed. (NOT called during process()) Propagates new data forward through network.""" ## first collect list of nodes/terminals and their dependencies if self.processing: return self.processing = True try: deps = {} for name, node in self._nodes.items(): deps[node] = [] for t in node.outputs().values(): deps[node].extend(t.dependentNodes()) ## determine order of updates order = fn.toposort(deps, nodes=[startNode]) order.reverse() ## keep track of terminals that have been updated terms = set(startNode.outputs().values()) #print "======= Updating", startNode # print("Order:", order) for node in order[1:]: # print("Processing node", node) update = False for term in list(node.inputs().values()): # print(" checking terminal", term) deps = list(term.connections().keys()) for d in deps: if d in terms: # print(" ..input", d, "changed") update |= True term.inputChanged(d, process=False) if update: # print(" processing..") node.update() terms |= set(node.outputs().values()) finally: self.processing = False if self.inputWasSet: self.inputWasSet = False else: self.sigStateChanged.emit()
[docs] def chartGraphicsItem(self): """Return the graphicsItem which displays the internals of this flowchart. (graphicsItem() still returns the external-view item)""" #return self._chartGraphicsItem return self.viewBox
def widget(self): if self._widget is None: self._widget = FlowchartCtrlWidget(self) self.scene = self._widget.scene() self.viewBox = self._widget.viewBox() #self._scene = QtGui.QGraphicsScene() #self._widget.setScene(self._scene) #self.scene.addItem(self.chartGraphicsItem()) #ci = self.chartGraphicsItem() #self.viewBox.addItem(ci) #self.viewBox.autoRange() return self._widget def listConnections(self): conn = set() for n in self._nodes.values(): terms = n.outputs() for n, t in terms.items(): for c in t.connections(): conn.add((t, c)) return conn def saveState(self): state = Node.saveState(self) state['nodes'] = [] state['connects'] = [] #state['terminals'] = self.saveTerminals() for name, node in self._nodes.items(): cls = type(node) if hasattr(cls, 'nodeName'): clsName = cls.nodeName pos = node.graphicsItem().pos() ns = {'class': clsName, 'name': name, 'pos': (pos.x(), pos.y()), 'state': node.saveState()} state['nodes'].append(ns) conn = self.listConnections() for a, b in conn: state['connects'].append((a.node().name(),, b.node().name(), state['inputNode'] = self.inputNode.saveState() state['outputNode'] = self.outputNode.saveState() return state def restoreState(self, state, clear=False): self.blockSignals(True) try: if clear: self.clear() Node.restoreState(self, state) nodes = state['nodes'] nodes.sort(key=lambda a: a['pos'][0]) for n in nodes: if n['name'] in self._nodes: #self._nodes[n['name']].graphicsItem().moveBy(*n['pos']) self._nodes[n['name']].restoreState(n['state']) continue try: node = self.createNode(n['class'], name=n['name']) node.restoreState(n['state']) except: printExc("Error creating node %s: (continuing anyway)" % n['name']) #node.graphicsItem().moveBy(*n['pos']) self.inputNode.restoreState(state.get('inputNode', {})) self.outputNode.restoreState(state.get('outputNode', {})) #self.restoreTerminals(state['terminals']) for n1, t1, n2, t2 in state['connects']: try: self.connectTerminals(self._nodes[n1][t1], self._nodes[n2][t2]) except: print(self._nodes[n1].terminals) print(self._nodes[n2].terminals) printExc("Error connecting terminals %s.%s - %s.%s:" % (n1, t1, n2, t2)) finally: self.blockSignals(False) self.sigChartLoaded.emit() self.outputChanged() self.sigStateChanged.emit() #self.sigOutputChanged.emit() def loadFile(self, fileName=None, startDir=None): if fileName is None: if startDir is None: startDir = self.filePath if startDir is None: startDir = '.' self.fileDialog = FileDialog(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) #self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) self.fileDialog.fileSelected.connect(self.loadFile) return ## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs.. #fileName = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") fileName = unicode(fileName) state = configfile.readConfigFile(fileName) self.restoreState(state, clear=True) self.viewBox.autoRange() #self.emit(QtCore.SIGNAL('fileLoaded'), fileName) self.sigFileLoaded.emit(fileName) def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'): if fileName is None: if startDir is None: startDir = self.filePath if startDir is None: startDir = '.' self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) #self.fileDialog.setDirectory(startDir) self.fileDialog.fileSelected.connect(self.saveFile) return #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") fileName = unicode(fileName) configfile.writeConfigFile(self.saveState(), fileName) self.sigFileSaved.emit(fileName) def clear(self): for n in list(self._nodes.values()): if n is self.inputNode or n is self.outputNode: continue n.close() ## calls self.nodeClosed(n) by signal #self.clearTerminals() self.widget().clear() def clearTerminals(self): Node.clearTerminals(self) self.inputNode.clearTerminals() self.outputNode.clearTerminals()
#class FlowchartGraphicsItem(QtGui.QGraphicsItem): class FlowchartGraphicsItem(GraphicsObject): def __init__(self, chart): #print "FlowchartGraphicsItem.__init__" #QtGui.QGraphicsItem.__init__(self) GraphicsObject.__init__(self) self.chart = chart ## chart is an instance of Flowchart() self.updateTerminals() def updateTerminals(self): #print "FlowchartGraphicsItem.updateTerminals" self.terminals = {} bounds = self.boundingRect() inp = self.chart.inputs() dy = bounds.height() / (len(inp)+1) y = dy for n, t in inp.items(): item = t.graphicsItem() self.terminals[n] = item item.setParentItem(self) item.setAnchor(bounds.width(), y) y += dy out = self.chart.outputs() dy = bounds.height() / (len(out)+1) y = dy for n, t in out.items(): item = t.graphicsItem() self.terminals[n] = item item.setParentItem(self) item.setAnchor(0, y) y += dy def boundingRect(self): #print "FlowchartGraphicsItem.boundingRect" return QtCore.QRectF() def paint(self, p, *args): #print "FlowchartGraphicsItem.paint" pass #p.drawRect(self.boundingRect()) class FlowchartCtrlWidget(QtGui.QWidget): """The widget that contains the list of all the nodes in a flowchart and their controls, as well as buttons for loading/saving flowcharts.""" def __init__(self, chart): self.items = {} #self.loadDir = loadDir ## where to look initially for chart files self.currentFileName = None QtGui.QWidget.__init__(self) self.chart = chart self.ui = FlowchartCtrlTemplate.Ui_Form() self.ui.setupUi(self) self.ui.ctrlList.setColumnCount(2) #self.ui.ctrlList.setColumnWidth(0, 200) self.ui.ctrlList.setColumnWidth(1, 20) self.ui.ctrlList.setVerticalScrollMode(self.ui.ctrlList.ScrollPerPixel) self.ui.ctrlList.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.chartWidget = FlowchartWidget(chart, self) #self.chartWidget.viewBox().autoRange() self.cwWin = QtGui.QMainWindow() self.cwWin.setWindowTitle('Flowchart') self.cwWin.setCentralWidget(self.chartWidget) self.cwWin.resize(1000,800) h = self.ui.ctrlList.header() if not USE_PYQT5: h.setResizeMode(0, h.Stretch) else: h.setSectionResizeMode(0, h.Stretch) self.ui.ctrlList.itemChanged.connect(self.itemChanged) self.ui.loadBtn.clicked.connect(self.loadClicked) self.ui.saveBtn.clicked.connect(self.saveClicked) self.ui.saveAsBtn.clicked.connect(self.saveAsClicked) self.ui.showChartBtn.toggled.connect(self.chartToggled) self.chart.sigFileLoaded.connect(self.setCurrentFile) self.ui.reloadBtn.clicked.connect(self.reloadClicked) self.chart.sigFileSaved.connect(self.fileSaved) #def resizeEvent(self, ev): #QtGui.QWidget.resizeEvent(self, ev) #self.ui.ctrlList.setColumnWidth(0, self.ui.ctrlList.viewport().width()-20) def chartToggled(self, b): if b: else: self.cwWin.hide() def reloadClicked(self): try: self.chartWidget.reloadLibrary() self.ui.reloadBtn.success("Reloaded.") except: self.ui.reloadBtn.success("Error.") raise def loadClicked(self): newFile = self.chart.loadFile() #self.setCurrentFile(newFile) def fileSaved(self, fileName): self.setCurrentFile(unicode(fileName)) self.ui.saveBtn.success("Saved.") def saveClicked(self): if self.currentFileName is None: self.saveAsClicked() else: try: self.chart.saveFile(self.currentFileName) #self.ui.saveBtn.success("Saved.") except: self.ui.saveBtn.failure("Error") raise def saveAsClicked(self): try: if self.currentFileName is None: newFile = self.chart.saveFile() else: newFile = self.chart.saveFile(suggestedFileName=self.currentFileName) #self.ui.saveAsBtn.success("Saved.") #print "Back to saveAsClicked." except: self.ui.saveBtn.failure("Error") raise #self.setCurrentFile(newFile) def setCurrentFile(self, fileName): self.currentFileName = unicode(fileName) if fileName is None: self.ui.fileNameLabel.setText("<b>[ new ]</b>") else: self.ui.fileNameLabel.setText("<b>%s</b>" % os.path.split(self.currentFileName)[1]) self.resizeEvent(None) def itemChanged(self, *args): pass def scene(self): return self.chartWidget.scene() ## returns the GraphicsScene object def viewBox(self): return self.chartWidget.viewBox() def nodeRenamed(self, node, oldName): self.items[node].setText(0, def addNode(self, node): ctrl = node.ctrlWidget() #if ctrl is None: #return item = QtGui.QTreeWidgetItem([, '', '']) self.ui.ctrlList.addTopLevelItem(item) byp = QtGui.QPushButton('X') byp.setCheckable(True) byp.setFixedWidth(20) item.bypassBtn = byp self.ui.ctrlList.setItemWidget(item, 1, byp) byp.node = node node.bypassButton = byp byp.setChecked(node.isBypassed()) byp.clicked.connect(self.bypassClicked) if ctrl is not None: item2 = QtGui.QTreeWidgetItem() item.addChild(item2) self.ui.ctrlList.setItemWidget(item2, 0, ctrl) self.items[node] = item def removeNode(self, node): if node in self.items: item = self.items[node] #self.disconnect(item.bypassBtn, QtCore.SIGNAL('clicked()'), self.bypassClicked) try: item.bypassBtn.clicked.disconnect(self.bypassClicked) except (TypeError, RuntimeError): pass self.ui.ctrlList.removeTopLevelItem(item) def bypassClicked(self): btn = QtCore.QObject.sender(self) btn.node.bypass(btn.isChecked()) def chartWidget(self): return self.chartWidget def outputChanged(self, data): pass #self.ui.outputTree.setData(data, hideRoot=True) def clear(self): self.chartWidget.clear() def select(self, node): item = self.items[node] self.ui.ctrlList.setCurrentItem(item) class FlowchartWidget(dockarea.DockArea): """Includes the actual graphical flowchart and debugging interface""" def __init__(self, chart, ctrl): #QtGui.QWidget.__init__(self) dockarea.DockArea.__init__(self) self.chart = chart self.ctrl = ctrl self.hoverItem = None #self.setMinimumWidth(250) #self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)) #self.ui = FlowchartTemplate.Ui_Form() #self.ui.setupUi(self) ## build user interface (it was easier to do it here than via developer) self.view = FlowchartGraphicsView.FlowchartGraphicsView(self) self.viewDock = dockarea.Dock('view', size=(1000,600)) self.viewDock.addWidget(self.view) self.viewDock.hideTitleBar() self.addDock(self.viewDock) self.hoverText = QtGui.QTextEdit() self.hoverText.setReadOnly(True) self.hoverDock = dockarea.Dock('Hover Info', size=(1000,20)) self.hoverDock.addWidget(self.hoverText) self.addDock(self.hoverDock, 'bottom') self.selInfo = QtGui.QWidget() self.selInfoLayout = QtGui.QGridLayout() self.selInfo.setLayout(self.selInfoLayout) self.selDescLabel = QtGui.QLabel() self.selNameLabel = QtGui.QLabel() self.selDescLabel.setWordWrap(True) self.selectedTree = DataTreeWidget() #self.selectedTree.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) #self.selInfoLayout.addWidget(self.selNameLabel) self.selInfoLayout.addWidget(self.selDescLabel) self.selInfoLayout.addWidget(self.selectedTree) self.selDock = dockarea.Dock('Selected Node', size=(1000,200)) self.selDock.addWidget(self.selInfo) self.addDock(self.selDock, 'bottom') self._scene = self.view.scene() self._viewBox = self.view.viewBox() #self._scene = QtGui.QGraphicsScene() #self._scene = FlowchartGraphicsView.FlowchartGraphicsScene() #self.view.setScene(self._scene) self.buildMenu() #self.ui.addNodeBtn.mouseReleaseEvent = self.addNodeBtnReleased self._scene.selectionChanged.connect(self.selectionChanged) self._scene.sigMouseHover.connect(self.hoverOver) #self.view.sigClicked.connect(self.showViewMenu) #self._scene.sigSceneContextMenu.connect(self.showViewMenu) #self._viewBox.sigActionPositionChanged.connect(self.menuPosChanged) def reloadLibrary(self): #QtCore.QObject.disconnect(self.nodeMenu, QtCore.SIGNAL('triggered(QAction*)'), self.nodeMenuTriggered) self.nodeMenu.triggered.disconnect(self.nodeMenuTriggered) self.nodeMenu = None self.subMenus = [] self.chart.library.reload() self.buildMenu() def buildMenu(self, pos=None): def buildSubMenu(node, rootMenu, subMenus, pos=None): for section, node in node.items(): menu = QtGui.QMenu(section) rootMenu.addMenu(menu) if isinstance(node, OrderedDict): buildSubMenu(node, menu, subMenus, pos=pos) subMenus.append(menu) else: act = rootMenu.addAction(section) act.nodeType = section act.pos = pos self.nodeMenu = QtGui.QMenu() self.subMenus = [] buildSubMenu(self.chart.library.getNodeTree(), self.nodeMenu, self.subMenus, pos=pos) self.nodeMenu.triggered.connect(self.nodeMenuTriggered) return self.nodeMenu def menuPosChanged(self, pos): self.menuPos = pos def showViewMenu(self, ev): #QtGui.QPushButton.mouseReleaseEvent(self.ui.addNodeBtn, ev) #if ev.button() == QtCore.Qt.RightButton: #self.menuPos = self.view.mapToScene(ev.pos()) #self.nodeMenu.popup(ev.globalPos()) #print "Flowchart.showViewMenu called" #self.menuPos = ev.scenePos() self.buildMenu(ev.scenePos()) self.nodeMenu.popup(ev.screenPos()) def scene(self): return self._scene ## the GraphicsScene item def viewBox(self): return self._viewBox ## the viewBox that items should be added to def nodeMenuTriggered(self, action): nodeType = action.nodeType if action.pos is not None: pos = action.pos else: pos = self.menuPos pos = self.viewBox().mapSceneToView(pos) self.chart.createNode(nodeType, pos=pos) def selectionChanged(self): #print "FlowchartWidget.selectionChanged called." items = self._scene.selectedItems() #print " scene.selectedItems: ", items if len(items) == 0: data = None else: item = items[0] if hasattr(item, 'node') and isinstance(item.node, Node): n = item.node data = {'outputs': n.outputValues(), 'inputs': n.inputValues()} self.selNameLabel.setText( if hasattr(n, 'nodeName'): self.selDescLabel.setText("<b>%s</b>: %s" % (n.nodeName, n.__class__.__doc__)) else: self.selDescLabel.setText("") if n.exception is not None: data['exception'] = n.exception else: data = None self.selectedTree.setData(data, hideRoot=True) def hoverOver(self, items): #print "FlowchartWidget.hoverOver called." term = None for item in items: if item is self.hoverItem: return self.hoverItem = item if hasattr(item, 'term') and isinstance(item.term, Terminal): term = item.term break if term is None: self.hoverText.setPlainText("") else: val = term.value() if isinstance(val, ndarray): val = "%s %s %s" % (type(val).__name__, str(val.shape), str(val.dtype)) else: val = str(val) if len(val) > 400: val = val[:400] + "..." self.hoverText.setPlainText("%s.%s = %s" % (term.node().name(),, val)) #self.hoverLabel.setCursorPosition(0) def clear(self): #self.outputTree.setData(None) self.selectedTree.setData(None) self.hoverText.setPlainText('') self.selNameLabel.setText('') self.selDescLabel.setText('') class FlowchartNode(Node): pass