Example #1
0
    def initialize(self):
        pvhistory = []
        lvhistory = []
        n = 0
        fc = Flowchart(terminals={'in': {'io': 'out'}, 'op': {'io': 'in'}})

        nodeList = fc.nodes()
        fc.removeNode(nodeList['Input'])
        fc.removeNode(nodeList['Output'])

        for i in self.mappings["vgs"]:

            vgNode = Node(i, allowRemove=False, allowAddOutput=False)
            fc.addNode(vgNode, i, [0, n])
            Node.addTerminal(vgNode, 'O', io='out', multi=True)
            Node.addTerminal(vgNode, 'I', io='in', multi=True)

            for j in self.mappings["mappings"]:
                if i == j[1]:
                    if j[2] not in lvhistory:
                        lvNode = Node(j[2],
                                      allowRemove=False,
                                      allowAddOutput=False)
                        fc.addNode(lvNode, j[2], [200, n])
                        Node.addTerminal(lvNode, 'I', io='in')
                        try:
                            fc.connectTerminals(vgNode['O'], lvNode['I'])
                        except:
                            pass
                            #cvar2 = Node.addOutput(vgNode)
                            #fc.connectTerminals(vgNode['Out'],cvar2)

                        lvhistory.append(j[2])
                    else:
                        pass

                    if j[0] not in pvhistory:
                        pvNode = Node(j[0],
                                      allowRemove=False,
                                      allowAddOutput=False)
                        fc.addNode(pvNode, j[0], [-400, n])
                        Node.addTerminal(pvNode, 'O', io='out')
                        try:
                            fc.connectTerminals(pvNode['O'], vgNode['I'])
                        except:
                            pass
                            #cvar1 = Node.addInput(vgNode)
                            #fc.connectTerminals(pvNode['Out'], cvar1)

                        pvhistory.append(j[0])
                    else:
                        pass
                    n = n + 200
        #vgNode.ctrls['doubleSpin'].setValue(5)
        #lvNode = fc.createNode('PlotWidget', 'lv')

        self.layout = QGridLayout()
        self.setLayout(self.layout)

        self.layout.addWidget(fc.widget())
Example #2
0
class Flowchart(Node):
    
    sigFileLoaded = QtCore.Signal(object)
    sigFileSaved = QtCore.Signal(object)
    
    
    #sigOutputChanged = QtCore.Signal() ## inherited from Node
    sigChartLoaded = QtCore.Signal()
    sigStateChanged = QtCore.Signal()
    
    def __init__(self, terminals=None, name=None, filePath=None):
        pg.setConfigOption('background', 'w')
        pg.setConfigOption('foreground', 'k')
        
        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 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)
        
    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 = term.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(inTerm.name())
        
    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):
        try:
            Node.removeTerminal(self, term.name())
        except KeyError:
            pass
        
    def terminalRenamed(self, term, oldName):
        newName = term.name()
        #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 = 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)
        
    def removeNode(self, node):
        node.close()
        
    def nodeClosed(self, node):
        del self._nodes[node.name()]
        self.widget().removeNode(node)
        try:
            node.sigClosed.disconnect(self.nodeClosed)
        except TypeError:
            pass
        try:
            node.sigRenamed.disconnect(self.nodeRenamed)
        except TypeError:
            pass
        try:
            node.sigOutputChanged.disconnect(self.nodeOutputChanged)
        except TypeError:
            pass
        
    def nodeRenamed(self, node, oldName):
        del self._nodes[oldName]
        self._nodes[node.name()] = node
        self.widget().nodeRenamed(node, oldName)
        
    def arrangeNodes(self):
        pass
        
    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[term.name()]
            else:
                return self.outputNode[term.name()]
        else:
            return term
        
    def connectTerminals(self, term1, term2):
        """Connect two terminals together within this flowchart."""
        term1 = self.internalTerminal(term1)
        term2 = self.internalTerminal(term2)
        term1.connectTo(term2)
        
        
    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)
            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[inp.name()] = dict([(i, data[i]) for i in inputs])
                    else:                   ## single-inputs terminals only need the single input value available
                        args[inp.name()] = 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, out.name()
                        #print out.name()
                        try:
                            data[out] = result[out.name()]
                        except:
                            print(out, out.name())
                            raise
            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
        
    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 = 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(lambda a,b: cmp(b[0], a[0]))
        for i, t in dels:
            ops.insert(i, ('d', t))
            
        return ops
        
        
    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 = 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
                for term in list(node.inputs().values()):
                    #print "  checking terminal", term
                    deps = list(term.connections().keys())
                    update = False
                    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()
        
        

    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(), a.name(), b.node().name(), b.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(lambda a, b: cmp(a['pos'][0], b['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 = pg.FileDialog(None, "Load Flowchart..", startDir, "Flowchart (*.fc)")
            #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
            #self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) 
            self.fileDialog.show()
            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 = str(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 = pg.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.show()
            self.fileDialog.fileSelected.connect(self.saveFile)
            return
            #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
        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()
Example #3
0
class Flowchart(Node):

    sigFileLoaded = QtCore.Signal(object)
    sigFileSaved = QtCore.Signal(object)

    #sigOutputChanged = QtCore.Signal() ## inherited from Node
    sigChartLoaded = QtCore.Signal()
    sigStateChanged = QtCore.Signal()

    def __init__(self, terminals=None, name=None, filePath=None):
        pg.setConfigOption('background', 'w')
        pg.setConfigOption('foreground', 'k')

        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 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)

    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 = term.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(inTerm.name())

    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):
        try:
            Node.removeTerminal(self, term.name())
        except KeyError:
            pass

    def terminalRenamed(self, term, oldName):
        newName = term.name()
        #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 = 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)

    def removeNode(self, node):
        node.close()

    def nodeClosed(self, node):
        del self._nodes[node.name()]
        self.widget().removeNode(node)
        try:
            node.sigClosed.disconnect(self.nodeClosed)
        except TypeError:
            pass
        try:
            node.sigRenamed.disconnect(self.nodeRenamed)
        except TypeError:
            pass
        try:
            node.sigOutputChanged.disconnect(self.nodeOutputChanged)
        except TypeError:
            pass

    def nodeRenamed(self, node, oldName):
        del self._nodes[oldName]
        self._nodes[node.name()] = node
        self.widget().nodeRenamed(node, oldName)

    def arrangeNodes(self):
        pass

    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[term.name()]
            else:
                return self.outputNode[term.name()]
        else:
            return term

    def connectTerminals(self, term1, term2):
        """Connect two terminals together within this flowchart."""
        term1 = self.internalTerminal(term1)
        term2 = self.internalTerminal(term2)
        term1.connectTo(term2)

    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)
            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[inp.name()] = dict([(i, data[i]) for i in inputs])
                    else:  ## single-inputs terminals only need the single input value available
                        args[inp.name()] = 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, out.name()
                        #print out.name()
                        try:
                            data[out] = result[out.name()]
                        except:
                            print(out, out.name())
                            raise
            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

    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 = 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(lambda a, b: cmp(b[0], a[0]))
        for i, t in dels:
            ops.insert(i, ('d', t))

        return ops

    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 = 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
                for term in list(node.inputs().values()):
                    #print "  checking terminal", term
                    deps = list(term.connections().keys())
                    update = False
                    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()

    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(), a.name(), b.node().name(), b.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(lambda a, b: cmp(a['pos'][0], b['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 = pg.FileDialog(None, "Load Flowchart..", startDir,
                                            "Flowchart (*.fc)")
            #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
            #self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
            self.fileDialog.show()
            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 = str(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 = pg.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.show()
            self.fileDialog.fileSelected.connect(self.saveFile)
            return
            #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
        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()