# -*- coding: utf-8 -*- from pyqtgraph.Qt import QtCore, QtGui import weakref from pyqtgraph.graphicsItems.GraphicsObject import GraphicsObject import pyqtgraph.functions as fn from pyqtgraph.Point import Point #from PySide import QtCore, QtGui 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 """ self._io = io #self._isOutput = opts[0] in ['out', 'io'] #self._isInput = opts[0]] in ['in', 'io'] #self._isIO = opts[0]=='io' self._optional = optional self._multi = multi self._node = weakref.ref(node) self._name = name self._renamable = renamable self._connections = {} self._graphicsItem = TerminalGraphicsItem(self, parent=self._node().graphicsItem()) self._bypass = bypass if multi: self._value = {} ## dictionary of terminal:value pairs. else: self._value = None self.valueOk = None self.recolor() def value(self, term=None): """Return the value this terminal provides for the connected terminal""" if term is None: return self._value if self.isMultiValue(): return self._value.get(term, None) else: return self._value def bypassValue(self): return self._bypass def setValue(self, val, process=True): """If this is a single-value terminal, val should be a single value. If this is a multi-value terminal, val should be a dict of terminal:value pairs""" if not self.isMultiValue(): if eq(val, self._value): return self._value = val else: if val is not None: self._value.update(val) self.setValueAcceptable(None) ## by default, input values are 'unchecked' until Node.update(). if self.isInput() and process: self.node().update() ## Let the flowchart handle this. #if self.isOutput(): #for c in self.connections(): #if c.isInput(): #c.inputChanged(self) self.recolor() def connected(self, term): """Called whenever this terminal has been connected to another. (note--this function is called on both terminals)""" if self.isInput() and term.isOutput(): self.inputChanged(term) if self.isOutput() and self.isMultiValue(): self.node().update() self.node().connected(self, term) def disconnected(self, term): """Called whenever this terminal has been disconnected from another. (note--this function is called on both terminals)""" if self.isMultiValue() and term in self._value: del self._value[term] self.node().update() #self.recolor() else: if self.isInput(): self.setValue(None) self.node().disconnected(self, term) #self.node().update() def inputChanged(self, term, process=True): """Called whenever there is a change to the input value to this terminal. It may often be useful to override this function.""" if self.isMultiValue(): self.setValue({term: term.value(self)}, process=process) else: self.setValue(term.value(self), process=process) def valueIsAcceptable(self): """Returns True->acceptable None->unknown False->Unacceptable""" return self.valueOk def setValueAcceptable(self, v=True): self.valueOk = v self.recolor() def connections(self): return self._connections def node(self): return self._node() def isInput(self): return self._io == 'in' def isMultiValue(self): return self._multi def isOutput(self): return self._io == 'out' def isRenamable(self): return self._renamable def name(self): return self._name def graphicsItem(self): return self._graphicsItem def isConnected(self): return len(self.connections()) > 0 def connectedTo(self, term): return term in self.connections() def hasInput(self): #conn = self.extendedConnections() for t in self.connections(): if t.isOutput(): return True return False def inputTerminals(self): """Return the terminal(s) that give input to this one.""" #terms = self.extendedConnections() #for t in terms: #if t.isOutput(): #return t return [t for t in self.connections() if t.isOutput()] def dependentNodes(self): """Return the list of nodes which receive input from this terminal.""" #conn = self.extendedConnections() #del conn[self] return set([t.node() for t in self.connections() if t.isInput()]) def connectTo(self, term, connectionItem=None): try: if self.connectedTo(term): raise Exception('Already connected') if term is self: raise Exception('Not connecting terminal to self') if term.node() is self.node(): raise Exception("Can't connect to terminal on same node.") for t in [self, term]: if t.isInput() and not t._multi and len(t.connections()) > 0: raise Exception("Cannot connect %s <-> %s: Terminal %s is already connected to %s (and does not allow multiple connections)" % (self, term, t, t.connections().keys())) #if self.hasInput() and term.hasInput(): #raise Exception('Target terminal already has input') #if term in self.node().terminals.values(): #if self.isOutput() or term.isOutput(): #raise Exception('Can not connect an output back to the same node.') except: if connectionItem is not None: connectionItem.close() raise if connectionItem is None: connectionItem = ConnectionItem(self.graphicsItem(), term.graphicsItem()) #self.graphicsItem().scene().addItem(connectionItem) self.graphicsItem().getViewBox().addItem(connectionItem) #connectionItem.setParentItem(self.graphicsItem().parent().parent()) self._connections[term] = connectionItem term._connections[self] = connectionItem self.recolor() #if self.isOutput() and term.isInput(): #term.inputChanged(self) #if term.isInput() and term.isOutput(): #self.inputChanged(term) self.connected(term) term.connected(self) return connectionItem def disconnectFrom(self, term): if not self.connectedTo(term): return item = self._connections[term] #print "removing connection", item #item.scene().removeItem(item) item.close() del self._connections[term] del term._connections[self] self.recolor() term.recolor() self.disconnected(term) term.disconnected(self) #if self.isOutput() and term.isInput(): #term.inputChanged(self) #if term.isInput() and term.isOutput(): #self.inputChanged(term) def disconnectAll(self): for t in self._connections.keys(): self.disconnectFrom(t) def recolor(self, color=None, recurse=True): if color is None: if not self.isConnected(): ## disconnected terminals are black color = QtGui.QColor(0,0,0) elif self.isInput() and not self.hasInput(): ## input terminal with no connected output terminals color = QtGui.QColor(200,200,0) elif self._value is None or eq(self._value, {}): ## terminal is connected but has no data (possibly due to processing error) color = QtGui.QColor(255,255,255) elif self.valueIsAcceptable() is None: ## terminal has data, but it is unknown if the data is ok color = QtGui.QColor(200, 200, 0) elif self.valueIsAcceptable() is True: ## terminal has good input, all ok color = QtGui.QColor(0, 200, 0) else: ## terminal has bad input color = QtGui.QColor(200, 0, 0) self.graphicsItem().setBrush(QtGui.QBrush(color)) if recurse: for t in self.connections(): t.recolor(color, recurse=False) def rename(self, name): oldName = self._name self._name = name self.node().terminalRenamed(self, oldName) self.graphicsItem().termRenamed(name) def __repr__(self): return "" % (str(self.node().name()), str(self.name())) #def extendedConnections(self, terms=None): #"""Return list of terminals (including this one) that are directly or indirectly wired to this.""" #if terms is None: #terms = {} #terms[self] = None #for t in self._connections: #if t in terms: #continue #terms.update(t.extendedConnections(terms)) #return terms def __hash__(self): return id(self) def close(self): self.disconnectAll() item = self.graphicsItem() if item.scene() is not None: item.scene().removeItem(item) def saveState(self): return {'io': self._io, 'multi': self._multi, 'optional': self._optional} #class TerminalGraphicsItem(QtGui.QGraphicsItem): class TerminalGraphicsItem(GraphicsObject): def __init__(self, term, parent=None): self.term = term #QtGui.QGraphicsItem.__init__(self, parent) GraphicsObject.__init__(self, parent) self.brush = fn.mkBrush(0,0,0) self.box = QtGui.QGraphicsRectItem(0, 0, 10, 10, self) self.label = QtGui.QGraphicsTextItem(self.term.name(), self) self.label.scale(0.7, 0.7) #self.setAcceptHoverEvents(True) self.newConnection = None self.setFiltersChildEvents(True) ## to pick up mouse events on the rectitem if self.term.isRenamable(): self.label.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction) self.label.focusOutEvent = self.labelFocusOut self.label.keyPressEvent = self.labelKeyPress self.setZValue(1) self.menu = None def labelFocusOut(self, ev): QtGui.QGraphicsTextItem.focusOutEvent(self.label, ev) self.labelChanged() def labelKeyPress(self, ev): if ev.key() == QtCore.Qt.Key_Enter or ev.key() == QtCore.Qt.Key_Return: self.labelChanged() else: QtGui.QGraphicsTextItem.keyPressEvent(self.label, ev) def labelChanged(self): newName = str(self.label.toPlainText()) if newName != self.term.name(): self.term.rename(newName) def termRenamed(self, name): self.label.setPlainText(name) def setBrush(self, brush): self.brush = brush self.box.setBrush(brush) def disconnect(self, target): self.term.disconnectFrom(target.term) def boundingRect(self): br = self.box.mapRectToParent(self.box.boundingRect()) lr = self.label.mapRectToParent(self.label.boundingRect()) return br | lr def paint(self, p, *args): pass def setAnchor(self, x, y): pos = QtCore.QPointF(x, y) self.anchorPos = pos br = self.box.mapRectToParent(self.box.boundingRect()) lr = self.label.mapRectToParent(self.label.boundingRect()) if self.term.isInput(): self.box.setPos(pos.x(), pos.y()-br.height()/2.) self.label.setPos(pos.x() + br.width(), pos.y() - lr.height()/2.) else: self.box.setPos(pos.x()-br.width(), pos.y()-br.height()/2.) self.label.setPos(pos.x()-br.width()-lr.width(), pos.y()-lr.height()/2.) self.updateConnections() def updateConnections(self): for t, c in self.term.connections().iteritems(): c.updateLine() def mousePressEvent(self, ev): #ev.accept() ev.ignore() 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() 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 return self.menu def removable(self): return ( (self.term.isOutput() and self.term.node()._allowAddOutput) or (self.term.isInput() and self.term.node()._allowAddInput)) ## probably never need this #def getContextMenus(self, ev): #return [self.getMenu()] def removeSelf(self): self.term.node().removeTerminal(self.term) def mouseDragEvent(self, ev): if ev.button() != QtCore.Qt.LeftButton: ev.ignore() return ev.accept() if ev.isStart(): if self.newConnection is None: self.newConnection = ConnectionItem(self) #self.scene().addItem(self.newConnection) self.getViewBox().addItem(self.newConnection) #self.newConnection.setParentItem(self.parent().parent()) self.newConnection.setTarget(self.mapToView(ev.pos())) elif ev.isFinish(): if self.newConnection is not None: items = self.scene().items(ev.scenePos()) gotTarget = False for i in items: if isinstance(i, TerminalGraphicsItem): self.newConnection.setTarget(i) try: self.term.connectTo(i.term, self.newConnection) gotTarget = True except: self.scene().removeItem(self.newConnection) self.newConnection = None raise break if not gotTarget: #print "remove unused connection" #self.scene().removeItem(self.newConnection) self.newConnection.close() self.newConnection = None else: if self.newConnection is not None: self.newConnection.setTarget(self.mapToView(ev.pos())) 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. self.box.setBrush(fn.mkBrush('w')) else: self.box.setBrush(self.brush) self.update() #def hoverEnterEvent(self, ev): #self.hover = True #def hoverLeaveEvent(self, ev): #self.hover = False def connectPoint(self): ## return the connect position of this terminal in view coords return self.mapToView(self.mapFromItem(self.box, self.box.boundingRect().center())) def nodeMoved(self): for t, item in self.term.connections().iteritems(): item.updateLine() #class ConnectionItem(QtGui.QGraphicsItem): class ConnectionItem(GraphicsObject): def __init__(self, source, target=None): #QtGui.QGraphicsItem.__init__(self) GraphicsObject.__init__(self) self.setFlags( self.ItemIsSelectable | self.ItemIsFocusable ) self.source = source self.target = target self.length = 0 self.hovered = False #self.line = QtGui.QGraphicsLineItem(self) self.source.getViewBox().addItem(self) self.updateLine() self.setZValue(0) def close(self): if self.scene() is not None: #self.scene().removeItem(self.line) self.scene().removeItem(self) def setTarget(self, target): self.target = target self.updateLine() def updateLine(self): start = Point(self.source.connectPoint()) if isinstance(self.target, TerminalGraphicsItem): stop = Point(self.target.connectPoint()) elif isinstance(self.target, QtCore.QPointF): stop = Point(self.target) else: return self.prepareGeometryChange() self.resetTransform() ang = (stop-start).angle(Point(0, 1)) if ang is None: ang = 0 self.rotate(ang) self.setPos(start) self.length = (start-stop).length() self.update() #self.line.setLine(start.x(), start.y(), stop.x(), stop.y()) def keyPressEvent(self, ev): if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace: #if isinstance(self.target, TerminalGraphicsItem): self.source.disconnect(self.target) ev.accept() else: ev.ignore() def mousePressEvent(self, ev): ev.ignore() def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: ev.accept() sel = self.isSelected() self.setSelected(True) if not sel and self.isSelected(): self.update() def hoverEvent(self, ev): if (not ev.isExit()) and ev.acceptClicks(QtCore.Qt.LeftButton): self.hovered = True else: self.hovered = False self.update() def boundingRect(self): #return self.line.boundingRect() px = self.pixelWidth() return QtCore.QRectF(-5*px, 0, 10*px, self.length) #def shape(self): #return self.line.shape() def paint(self, p, *args): if self.isSelected(): p.setPen(fn.mkPen(200, 200, 0, width=3)) else: if self.hovered: p.setPen(fn.mkPen(150, 150, 250, width=1)) else: p.setPen(fn.mkPen(100, 100, 250, width=1)) p.drawLine(0, 0, 0, self.length)