From 697130789ca493dcc632b438fbc3b10e97e4cf8b Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Mon, 23 Apr 2012 10:13:21 -0400 Subject: [PATCH] flowchart updates. Added nodes, expanded context menus. --- flowchart/Flowchart.py | 33 ++++++++++++--- flowchart/Node.py | 17 ++++++-- flowchart/Terminal.py | 81 ++++++++++++++++++++++++------------ flowchart/library/Data.py | 20 ++++----- flowchart/library/Display.py | 25 +++++++++++ 5 files changed, 131 insertions(+), 45 deletions(-) diff --git a/flowchart/Flowchart.py b/flowchart/Flowchart.py index e253741f..71551ba4 100644 --- a/flowchart/Flowchart.py +++ b/flowchart/Flowchart.py @@ -84,14 +84,18 @@ class Flowchart(Node): self.widget() - self.inputNode = Node('Input', allowRemove=False) - self.outputNode = Node('Output', allowRemove=False) + 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) @@ -121,11 +125,20 @@ class Flowchart(Node): if opts['io'] == 'in': ## inputs to the flowchart become outputs on the input node opts['io'] = 'out' opts['multi'] = False - term2 = self.inputNode.addTerminal(name, **opts) + 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 - term2 = self.outputNode.addTerminal(name, **opts) + 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): @@ -138,6 +151,16 @@ class Flowchart(Node): def internalTerminalRenamed(self, term, oldName): self[oldName].rename(term.name()) + def internalTerminalAdded(self, node, term): + if term._io == 'in': + io = 'out' + else: + io = 'in' + Node.addTerminal(self, term.name(), io=io, renamable=term.isRenamable(), removable=term.isRemovable(), multiable=term.isMultiable()) + + def internalTerminalRemoved(self, node, term): + Node.removeTerminal(self, term.name()) + def terminalRenamed(self, term, oldName): newName = term.name() #print "flowchart rename", newName, oldName @@ -475,7 +498,7 @@ class Flowchart(Node): self.inputNode.restoreState(state.get('inputNode', {})) self.outputNode.restoreState(state.get('outputNode', {})) - #self.restoreTerminals(state['terminals']) + self.restoreTerminals(state['terminals']) for n1, t1, n2, t2 in state['connects']: try: self.connectTerminals(self._nodes[n1][t1], self._nodes[n2][t2]) diff --git a/flowchart/Node.py b/flowchart/Node.py index 5604fc15..43f617d4 100644 --- a/flowchart/Node.py +++ b/flowchart/Node.py @@ -20,7 +20,9 @@ class Node(QtCore.QObject): sigOutputChanged = QtCore.Signal(object) # self sigClosed = QtCore.Signal(object) sigRenamed = QtCore.Signal(object, object) - sigTerminalRenamed = QtCore.Signal(object, object) + sigTerminalRenamed = QtCore.Signal(object, object) # term, oldName + sigTerminalAdded = QtCore.Signal(object, object) # self, term + sigTerminalRemoved = QtCore.Signal(object, object) # self, term def __init__(self, name, terminals=None, allowAddInput=False, allowAddOutput=False, allowRemove=True): @@ -77,6 +79,8 @@ class Node(QtCore.QObject): if name in self._outputs: del self._outputs[name] self.graphicsItem().updateTerminals() + self.sigTerminalRemoved.emit(self, term) + def terminalRenamed(self, term, oldName): """Called after a terminal has been renamed""" @@ -107,6 +111,7 @@ class Node(QtCore.QObject): elif term.isOutput(): self._outputs[name] = term self.graphicsItem().updateTerminals() + self.sigTerminalAdded.emit(self, term) return term @@ -527,16 +532,22 @@ class NodeGraphicsItem(GraphicsObject): def buildMenu(self): self.menu = QtGui.QMenu() self.menu.setTitle("Node") - a = self.menu.addAction("Add input", self.node.addInput) + a = self.menu.addAction("Add input", self.addInputFromMenu) if not self.node._allowAddInput: a.setEnabled(False) - a = self.menu.addAction("Add output", self.node.addOutput) + a = self.menu.addAction("Add output", self.addOutputFromMenu) if not self.node._allowAddOutput: a.setEnabled(False) a = self.menu.addAction("Remove node", self.node.close) if not self.node._allowRemove: a.setEnabled(False) + def addInputFromMenu(self): ## called when add input is clicked in context menu + self.node.addInput(renamable=True, removable=True, multiable=True) + + def addOutputFromMenu(self): ## called when add output is clicked in context menu + self.node.addOutput(renamable=True, removable=True, multiable=False) + #def menuTriggered(self, action): ##print "node.menuTriggered called. action:", action #act = str(action.text()) diff --git a/flowchart/Terminal.py b/flowchart/Terminal.py index d41f702e..a9addfaf 100644 --- a/flowchart/Terminal.py +++ b/flowchart/Terminal.py @@ -8,15 +8,23 @@ from pyqtgraph.Point import Point from eq import * class Terminal: - def __init__(self, node, name, io, optional=False, multi=False, pos=None, renamable=False, bypass=None): - """Construct a new terminal. Optiona are: - node - the node to which this terminal belongs - name - string, the name of the terminal - io - 'in' or 'out' - optional - bool, whether the node may process without connection to this terminal - multi - bool, for inputs: whether this terminal may make multiple connections - for outputs: whether this terminal creates a different value for each connection - pos - [x, y], the position of the terminal within its node's boundaries + def __init__(self, node, name, io, optional=False, multi=False, pos=None, renamable=False, removable=False, multiable=False, bypass=None): + """ + Construct a new terminal. + + ============== ================================================================================= + **Arguments:** + node the node to which this terminal belongs + name string, the name of the terminal + io 'in' or 'out' + optional bool, whether the node may process without connection to this terminal + multi bool, for inputs: whether this terminal may make multiple connections + for outputs: whether this terminal creates a different value for each connection + pos [x, y], the position of the terminal within its node's boundaries + renamable (bool) Whether the terminal can be renamed by the user + removable (bool) Whether the terminal can be removed by the user + multiable (bool) Whether the user may toggle the *multi* option for this terminal + ============== ================================================================================= """ self._io = io #self._isOutput = opts[0] in ['out', 'io'] @@ -27,6 +35,8 @@ class Terminal: self._node = weakref.ref(node) self._name = name self._renamable = renamable + self._removable = removable + self._multiable = multiable self._connections = {} self._graphicsItem = TerminalGraphicsItem(self, parent=self._node().graphicsItem()) self._bypass = bypass @@ -121,6 +131,10 @@ class Terminal: def isMultiValue(self): return self._multi + + def setMultiValue(self, b): + """Set whether this is a multi-value terminal.""" + self._multi = b def isOutput(self): return self._io == 'out' @@ -128,6 +142,12 @@ class Terminal: def isRenamable(self): return self._renamable + def isRemovable(self): + return self._removable + + def isMultiable(self): + return self._multiable + def name(self): return self._name @@ -278,7 +298,7 @@ class Terminal: item.scene().removeItem(item) def saveState(self): - return {'io': self._io, 'multi': self._multi, 'optional': self._optional} + return {'io': self._io, 'multi': self._multi, 'optional': self._optional, 'renamable': self._renamable, 'removable': self._removable, 'multiable': self._multiable} #class TerminalGraphicsItem(QtGui.QGraphicsItem): @@ -357,40 +377,46 @@ class TerminalGraphicsItem(GraphicsObject): def mousePressEvent(self, ev): #ev.accept() - ev.ignore() + ev.ignore() ## necessary to allow click/drag events to process correctly def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: ev.accept() self.label.setFocus(QtCore.Qt.MouseFocusReason) - if ev.button() == QtCore.Qt.RightButton: - if self.raiseContextMenu(ev): - ev.accept() + elif ev.button() == QtCore.Qt.RightButton: + ev.accept() + self.raiseContextMenu(ev) def raiseContextMenu(self, ev): ## only raise menu if this terminal is removable menu = self.getMenu() - if menu is None: - return False menu = self.scene().addParentContextMenus(self, menu, ev) pos = ev.screenPos() menu.popup(QtCore.QPoint(pos.x(), pos.y())) - return True def getMenu(self): if self.menu is None: - if self.removable(): - self.menu = QtGui.QMenu() - self.menu.setTitle("Terminal") - self.menu.addAction("Remove terminal", self.removeSelf) - else: - return None + self.menu = QtGui.QMenu() + self.menu.setTitle("Terminal") + remAct = QtGui.QAction("Remove terminal", self.menu) + remAct.triggered.connect(self.removeSelf) + self.menu.addAction(remAct) + self.menu.remAct = remAct + if not self.term.isRemovable(): + remAct.setEnabled(False) + multiAct = QtGui.QAction("Multi-value", self.menu) + multiAct.setCheckable(True) + multiAct.setChecked(self.term.isMultiValue()) + multiAct.triggered.connect(self.toggleMulti) + self.menu.addAction(multiAct) + self.menu.multiAct = multiAct + if self.term.isMultiable(): + multiAct.setEnabled = False return self.menu - def removable(self): - return ( - (self.term.isOutput() and self.term.node()._allowAddOutput) or - (self.term.isInput() and self.term.node()._allowAddInput)) + def toggleMulti(self): + multi = self.menu.multiAct.isChecked() + self.term.setMultiValue(multi) ## probably never need this #def getContextMenus(self, ev): @@ -441,6 +467,7 @@ class TerminalGraphicsItem(GraphicsObject): def hoverEvent(self, ev): if not ev.isExit() and ev.acceptDrags(QtCore.Qt.LeftButton): ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it. + ev.acceptClicks(QtCore.Qt.RightButton) self.box.setBrush(fn.mkBrush('w')) else: self.box.setBrush(self.brush) diff --git a/flowchart/library/Data.py b/flowchart/library/Data.py index ee303487..a129fc95 100644 --- a/flowchart/library/Data.py +++ b/flowchart/library/Data.py @@ -195,31 +195,31 @@ class EvalNode(Node): self.ui = QtGui.QWidget() self.layout = QtGui.QGridLayout() - self.addInBtn = QtGui.QPushButton('+Input') - self.addOutBtn = QtGui.QPushButton('+Output') + #self.addInBtn = QtGui.QPushButton('+Input') + #self.addOutBtn = QtGui.QPushButton('+Output') self.text = QtGui.QTextEdit() self.text.setTabStopWidth(30) self.text.setPlainText("# Access inputs as args['input_name']\nreturn {'output': None} ## one key per output terminal") - self.layout.addWidget(self.addInBtn, 0, 0) - self.layout.addWidget(self.addOutBtn, 0, 1) + #self.layout.addWidget(self.addInBtn, 0, 0) + #self.layout.addWidget(self.addOutBtn, 0, 1) self.layout.addWidget(self.text, 1, 0, 1, 2) self.ui.setLayout(self.layout) #QtCore.QObject.connect(self.addInBtn, QtCore.SIGNAL('clicked()'), self.addInput) - self.addInBtn.clicked.connect(self.addInput) + #self.addInBtn.clicked.connect(self.addInput) #QtCore.QObject.connect(self.addOutBtn, QtCore.SIGNAL('clicked()'), self.addOutput) - self.addOutBtn.clicked.connect(self.addOutput) + #self.addOutBtn.clicked.connect(self.addOutput) self.text.focusOutEvent = self.focusOutEvent self.lastText = None def ctrlWidget(self): return self.ui - def addInput(self): - Node.addInput(self, 'input', renamable=True) + #def addInput(self): + #Node.addInput(self, 'input', renamable=True) - def addOutput(self): - Node.addOutput(self, 'output', renamable=True) + #def addOutput(self): + #Node.addOutput(self, 'output', renamable=True) def focusOutEvent(self, ev): text = str(self.text.toPlainText()) diff --git a/flowchart/library/Display.py b/flowchart/library/Display.py index 4379858f..af8eb1ba 100644 --- a/flowchart/library/Display.py +++ b/flowchart/library/Display.py @@ -5,6 +5,7 @@ import weakref from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.graphicsItems.ScatterPlotItem import ScatterPlotItem from pyqtgraph.graphicsItems.PlotCurveItem import PlotCurveItem +from pyqtgraph import PlotDataItem from common import * import numpy as np @@ -117,6 +118,30 @@ class CanvasNode(Node): del self.items[vid] +class PlotCurve(CtrlNode): + """Generates a plot curve from x/y data""" + nodeName = 'PlotCurve' + uiTemplate = [ + ('color', 'color'), + ] + + def __init__(self, name): + CtrlNode.__init__(self, name, terminals={ + 'x': {'io': 'in'}, + 'y': {'io': 'in'}, + 'plot': {'io': 'out'} + }) + self.item = PlotDataItem() + + def process(self, x, y, display=True): + #print "scatterplot process" + if not display: + return {'plot': None} + + self.item.setData(x, y, pen=self.ctrls['color'].color()) + return {'plot': self.item} + + class ScatterPlot(CtrlNode):