def _on_show_menu(self): """ Internal callback that shows field menu using the actions from the data """ menu = QMenu(self) actions = self.data().get('actions', list()) for action in actions: name = action.get('name', 'No name found') enabled = action.get('enabled', True) callback = action.get('callback') fn = partial(self._action_callback, callback) action = menu.addAction(name) action.setEnabled(enabled) action.triggered.connect(fn) point = QCursor.pos() point.setX(point.x() + 3) point.setY(point.y() + 3) self._action_result = None menu.exec_(point) if self._action_result is not None: self.set_value(self._action_result)
def contextMenuEvent(self, event): menu = QMenu(self) remove_icon = resources.icon(name='delete') remove_action = QAction(remove_icon, 'Remove', menu) remove_action.setStatusTip(consts.DELETE_PROJECT_TOOLTIP) remove_action.setToolTip(consts.DELETE_PROJECT_TOOLTIP) remove_action.triggered.connect(self._on_remove_project) folder_icon = resources.icon(name='open_folder', extension='png') folder_action = QAction(folder_icon, 'Open in Browser', menu) folder_action.setStatusTip(consts.OPEN_PROJECT_IN_EXPLORER_TOOLTIP) folder_action.setToolTip(consts.OPEN_PROJECT_IN_EXPLORER_TOOLTIP) folder_action.triggered.connect(self._on_open_in_browser) image_icon = resources.icon(name='picture', extension='png') set_image_action = QAction(image_icon, 'Set Project Image', menu) set_image_action.setToolTip(consts.SET_PROJECT_IMAGE_TOOLTIP) set_image_action.setStatusTip(consts.SET_PROJECT_IMAGE_TOOLTIP) set_image_action.triggered.connect(self._on_set_project_image) for action in [ remove_action, None, folder_action, None, set_image_action ]: if action is None: menu.addSeparator() else: menu.addAction(action) menu.exec_(self.mapToGlobal(event.pos()))
def contextMenuEvent(self, event): menu = QMenu(self) remove_icon = resources.icon(name='delete') remove_action = QAction(remove_icon, 'Remove', menu) remove_tooltip = 'Delete selected project' remove_action.setStatusTip(remove_tooltip) remove_action.setToolTip(remove_tooltip) remove_action.triggered.connect(self._on_remove_project) folder_icon = resources.icon(name='open_folder', extension='png') folder_action = QAction(folder_icon, 'Open in Browser', menu) open_project_in_explorer_tooltip = 'Open project folder in explorer' folder_action.setStatusTip(open_project_in_explorer_tooltip) folder_action.setToolTip(open_project_in_explorer_tooltip) folder_action.triggered.connect(self._on_open_in_browser) image_icon = resources.icon(name='picture', extension='png') set_image_action = QAction(image_icon, 'Set Project Image', menu) set_project_image_tooltip = 'Set the image used by the project' set_image_action.setToolTip(set_project_image_tooltip) set_image_action.setStatusTip(set_project_image_tooltip) set_image_action.triggered.connect(self._on_set_project_image) for action in [remove_action, None, folder_action, None, set_image_action]: if action is None: menu.addSeparator() else: menu.addAction(action) menu.exec_(self.mapToGlobal(event.pos()))
def _on_open_menu(self): """ Internal function that is called when the user opens the context menu of the tab menu bar :param pos: QPos """ menu = QMenu(self) menu.addAction( QAction('Rename Current Tab', self, triggered=self._on_rename_tab)) menu.exec_(QCursor.pos())
class ExternalCodeList(QListWidget): directoriesChanged = Signal(object) def __init__(self, parent=None): super(ExternalCodeList, self).__init__(parent) self.setAlternatingRowColors(True) self.setSelectionMode(self.NoSelection) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self._on_item_menu) self._create_context_menu() def get_directories(self): count = self.count() found = list() if not count: return found for i in range(count): item = self.item(i) if not item: continue found.append(str(item.text())) return found def refresh(self, directories): directories = python.force_list(directories) self.clear() for diretory_found in directories: name = diretory_found if not path_utils.is_dir(diretory_found): name = 'Directory Not Valid! {}'.format(diretory_found) item = QListWidgetItem() item.setText(name) item.setSizeHint(QSize(20, 25)) self.addItem(item) def _create_context_menu(self): self._context_menu = QMenu() remove_action = self._context_menu.addAction('Remove') remove_action.setIcon(resources.icon('trash')) remove_action.triggered.connect(self._on_remove_action) def _on_item_menu(self, pos): item = self.itemAt(pos) if not item: return self._context_menu.exec_(self.viewport().mapToGlobal(pos)) def _on_remove_action(self): index = self.currentIndex() self.takeItem(index.row()) self.directoriesChanged.emit(self.get_directories())
def handlePopupMenu(self): """ Called when user right-clicks on a trace """ menu = QMenu() selected = self.selectedDataNames() if len(selected) != 0: menu.addAction(self.actionDeleteTrace) menu.exec_(QCursor.pos())
class implicitPinCall(Node): def __init__(self, name, graph): super(implicitPinCall, self).__init__(name, graph) self.inExec = self.addInputPin('inp', DataTypes.Exec, self.compute, hideLabel=True) self.uidInp = self.addInputPin('UUID', DataTypes.String) self.outExec = self.addOutputPin('out', DataTypes.Exec, hideLabel=True) self.menu = QMenu() self.actionFindPin = self.menu.addAction('Find pin') self.actionFindPin.triggered.connect(self.OnFindPin) def contextMenuEvent(self, event): self.menu.exec_(event.screenPos()) @staticmethod def pinTypeHints(): return {'inputs': [DataTypes.String, DataTypes.Exec], 'outputs': [DataTypes.Exec]} @staticmethod def category(): return 'FlowControl' @staticmethod def keywords(): return [] @staticmethod def description(): return 'Implicit execution pin call by provided <a href="https://ru.wikipedia.org/wiki/UUID"> uuid</a>.\nUse this when pins are far from each other.' def OnFindPin(self): uidStr = self.uidInp.getData() if len(uidStr) == 0: return try: uid = uuid.UUID(uidStr) pin = self.graph().pins[uid] self.graph().centerOn(pin) pin.highlight() except Exception as e: print(e) pass def compute(self): uidStr = self.uidInp.getData() if len(uidStr) == 0: return uid = uuid.UUID(uidStr) if uid in self.graph().pins: pin = self.graph().pins[uid] if not pin.hasConnections(): pin.call()
def handlePopupMenu(self): """ Called when user right-clicks on the sources tree """ menu = QMenu() menu.addAction(self.actionAdd) selected = self.main.treeView.selectedItems() if len(selected) != 0: if 'config' in selected[0].source.__dict__: menu.addAction(self.actionConfig) menu.addAction(self.actionDelete) menu.exec_(QCursor.pos())
class makeArray(Node): def __init__(self, name, graph): super(makeArray, self).__init__(name, graph) self.out0 = self.addOutputPin('Array', DataTypes.Array) self.menu = QMenu() self.action = self.menu.addAction('add input') self.action.triggered.connect(self.addInPin) def addInPin(self): if len(self.inputs) == 0: p = self.addInputPin(str(len(self.inputs)), DataTypes.Any, constraint="1") else: p = self.addInputPin(str(len(self.inputs)), DataTypes.Any, constraint="1") p.setType(self.inputs.items()[0][1]) p.setDeletable() pinAffects(p, self.out0) def contextMenuEvent(self, event): self.menu.exec_(event.screenPos()) @staticmethod def pinTypeHints(): return {'inputs': supportedDataTypesList, 'outputs': [DataTypes.Array]} @staticmethod def category(): return 'GenericTypes' @staticmethod def keywords(): return [] @staticmethod def description(): return 'Genertic array' def postCreate(self, jsonTemplate): Node.postCreate(self, jsonTemplate) # restore dynamically created inputs for inp in jsonTemplate['inputs']: p = PinWidgetBase.deserialize(self, inp) pinAffects(p, self.out0) def compute(self): self.out0.setData(list([i.getData() for i in self.inputs.values()]))
def handlePopupMenu(self): """ Called when user right-clicks on the sources tree """ menu = QMenu() menu.addAction(self.actionAdd) selected = self.main.treeView.selectedItems() if len(selected) != 0: if "config" in selected[0].source.__dict__: menu.addAction(self.actionConfig) menu.addAction(self.actionDelete) menu.exec_(QCursor.pos())
class sequence(Node): def __init__(self, name, graph): super(sequence, self).__init__(name, graph) self.inExecPin = self.addInputPin('inExec', DataTypes.Exec, self.compute, hideLabel=True) self.menu = QMenu() self.action = self.menu.addAction('add pin') self.action.triggered.connect(self.addOutPin) def addOutPin(self): p = self.addOutputPin(str(len(self.outputs)), DataTypes.Exec) pinAffects(self.inExecPin, p) def contextMenuEvent(self, event): self.menu.exec_(event.screenPos()) @staticmethod def pinTypeHints(): return {'inputs': [DataTypes.Exec], 'outputs': [DataTypes.Exec]} @staticmethod def category(): return 'FlowControl' @staticmethod def keywords(): return [] @staticmethod def description(): return 'The Sequence node allows for a single execution pulse to trigger a series of events in order. The node may have any number of outputs, all of which get called as soon as the Sequence node receives an input. They will always get called in order, but without any delay. To a typical user, the outputs will likely appear to have been triggered simultaneously.' def postCreate(self, jsonTemplate): Node.postCreate(self, jsonTemplate) # restore dynamically created outputs if len(jsonTemplate['outputs']) == 0: self.addOutPin() self.addOutPin() else: for out in jsonTemplate['outputs']: PinWidgetBase.deserialize(self, out) def compute(self): for out in self.outputs.values(): out.call()
def show_context_menu(self): """ Creates and shows the context menu for the search widget :return: QAction """ menu = QMenu(self) standard_menu = self.createStandardContextMenu() standard_menu.setTitle('Edit') menu.addMenu(standard_menu) sub_menu = QMenu(menu) sub_menu.setTitle('Space Operator') menu.addMenu(sub_menu) or_action = QAction('OR', menu) or_action.setCheckable(True) or_callback = partial(self.set_space_operator, 'or') or_action.triggered.connect(or_callback) if self.space_operator() == 'or': or_action.setChecked(True) sub_menu.addAction(or_action) and_action = QAction('AND', menu) and_action.setCheckable(True) and_callback = partial(self.set_space_operator, 'and') and_action.triggered.connect(and_callback) if self.space_operator() == 'and': and_action.setChecked(True) sub_menu.addAction(and_action) action = menu.exec_(QCursor.pos()) return action
class InputWidgetRaw(QWidget, IInputWidget): """ This type of widget can be used as a base class for complex ui generated by designer """ def __init__(self, parent=None, dataSetCallback=None, defaultValue=None, **kwds): super(InputWidgetRaw, self).__init__(parent=parent, **kwds) self._defaultValue = defaultValue # fuction with signature void(object) # this will set data to pin self.dataSetCallback = dataSetCallback self._widget = None self._menu = QMenu() self.actionReset = self._menu.addAction("ResetValue") self.actionReset.triggered.connect(self.onResetValue) def setWidgetValueNoSignals(self, value): self.blockWidgetSignals(True) self.setWidgetValue(value) self.blockWidgetSignals(False) def setWidget(self, widget): self._widget = widget def getWidget(self): assert (self._widget is not None) return self._widget def onResetValue(self): self.setWidgetValue(self._defaultValue) def setWidgetValue(self, value): '''to widget''' pass def widgetValueUpdated(self, value): '''from widget''' pass def contextMenuEvent(self, event): self._menu.exec_(event.globalPos())
class makeM44Array(Node): def __init__(self, name, graph): super(makeM44Array, self).__init__(name, graph) self.out0 = self.addOutputPin('matrices', DataTypes.Array) self.menu = QMenu() self.action = self.menu.addAction('add input') self.action.triggered.connect(self.addInPin) def addInPin(self): p = self.addInputPin(str(len(self.inputs)), DataTypes.Matrix44) pinAffects(p, self.out0) def contextMenuEvent(self, event): self.menu.exec_(event.screenPos()) @staticmethod def pinTypeHints(): return {'inputs': [DataTypes.Matrix44], 'outputs': [DataTypes.Array]} @staticmethod def category(): return 'GenericTypes' @staticmethod def keywords(): return [] @staticmethod def description(): return 'Array of matrix44.' def postCreate(self, jsonTemplate): Node.postCreate(self, jsonTemplate) # restore dynamically created inputs for inp in jsonTemplate['inputs']: p = PinWidgetBase.deserialize(self, inp) pinAffects(p, self.out0) def compute(self): self.out0.setData(list([i.getData() for i in self.inputs.values()]))
def _on_show_menu(self): """ Internal callback function that is called when menu button is clicked byu the user :return: QAction """ menu = QMenu(self) self._item_view.context_edit_menu(menu) point = QCursor.pos() point.setX(point.x() + 3) point.setY(point.y() + 3) return menu.exec_(point)
def _on_show_menu(self): """ Internal callback function triggered when item menu should be opened :return: QAction """ menu = QMenu(self) self.item().context_edit_menu(menu) point = QCursor.pos() point.setX(point.x() + 3) point.setY(point.y() + 3) return menu.exec_(point)
class UIPinBase(QGraphicsWidget): ''' Pin ui wrapper ''' # Event called when pin is connected OnPinConnected = QtCore.Signal(object) # Event called when pin is disconnected OnPinDisconnected = QtCore.Signal(object) # Event called when data been set dataBeenSet = QtCore.Signal(object) # Event called when pin name changes displayNameChanged = QtCore.Signal(str) OnPinChanged = QtCore.Signal(object) OnPinDeleted = QtCore.Signal(object) def __init__(self, owningNode, raw_pin): super(UIPinBase, self).__init__() self.setGraphicsItem(self) self.setFlag(QGraphicsWidget.ItemSendsGeometryChanges) self.setCacheMode(self.DeviceCoordinateCache) self.setAcceptHoverEvents(True) self.setZValue(1) self.setParentItem(owningNode) self.UiNode = weakref.ref(owningNode) self._rawPin = raw_pin self._rawPin.serializationHook.connect(self.serializationHook) self._rawPin.containerTypeChanged.connect(self.onContainerTypeChanged) self._displayName = self._rawPin.name self._rawPin.setWrapper(self) self._rawPin.killed.connect(self.kill) self._rawPin.nameChanged.connect(self.setDisplayName) # Context menu for pin self.menu = QMenu() self.menu.addAction("Rename").triggered.connect(self.onRename) self.menu.addAction("Remove").triggered.connect(self._rawPin.kill) self.actionDisconnect = self.menu.addAction('Disconnect all') self.actionDisconnect.triggered.connect(self._rawPin.disconnectAll) self.actionResetValue = self.menu.addAction("Reset value") self.actionResetValue.triggered.connect(self.resetToDefault) if self._rawPin._structure == PinStructure.Multi: self.menu.addAction("changeStructure").triggered.connect( self.selectStructure) # GUI self._font = QtGui.QFont("Consolas") self._font.setPointSize(6) self.pinSize = 6 self.hovered = False self.bLabelHidden = False self._pinColor = QtGui.QColor(*self._rawPin.color()) self._labelColor = QtCore.Qt.white self._execPen = QtGui.QPen(Colors.White, 0.5, QtCore.Qt.SolidLine) self._dirty_pen = QtGui.QPen(Colors.DirtyPen, 0.5, QtCore.Qt.DashLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin) self.uiConnectionList = [] self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) self.pinCircleDrawOffset = QtCore.QPointF() @property def labelColor(self): return self._labelColor @labelColor.setter def labelColor(self, value): self._labelColor = value def pinCenter(self): """Point relative to pin widget, where circle is drawn.""" frame = QtCore.QRectF(QtCore.QPointF(0, 0), self.geometry().size()) halfPinSize = self.pinSize / 2 pinX = 0 + halfPinSize + self.pinSize - halfPinSize pinY = (frame.height() / 2) if self.direction == PinDirection.Output: pinX = frame.width() - self.pinSize + halfPinSize result = QtCore.QPointF(pinX, pinY) if self.owningNode().collapsed: labelHeight = self.owningNode().labelHeight labelHeight += self.owningNode().nodeLayout.spacing() if self.direction == PinDirection.Input: result = self.mapFromParent(QtCore.QPointF(0, labelHeight)) if self.direction == PinDirection.Output: result = self.mapFromParent( QtCore.QPointF( self.owningNode().sizeHint(None, None).width(), labelHeight)) return result def onContainerTypeChanged(self, *args, **kwargs): # underlined pin is changed to list or dict # update to redraw shape self.update() def setLabel(self, labelItem): if self._label is None: self._label = weakref.ref(labelItem) def displayName(self): return self._displayName def setDisplayName(self, displayName): if displayName != self._displayName: self._displayName = displayName self.displayNameChanged.emit(self._displayName) self.prepareGeometryChange() self.updateGeometry() self.update() def jsonEncoderClass(self): return self._rawPin.jsonEncoderClass() def jsonDecoderClass(self): return self._rawPin.jsonDecoderClass() @property def owningNode(self): return self.UiNode @property def constraint(self): return self._rawPin.constraint @property def isAny(self): return self._rawPin.isAny() def setMenuItemEnabled(self, actionName, bEnabled): for action in self.menu.actions(): if action.text() == actionName: if bEnabled != action.isEnabled() and action.isVisible(): action.setEnabled(bEnabled) action.setVisible(bEnabled) def syncRenamable(self): renamingEnabled = self._rawPin.optionEnabled( PinOptions.RenamingEnabled) # self._label()._isEditable = renamingEnabled self.setMenuItemEnabled("Rename", renamingEnabled) def onRename(self): name, confirmed = QInputDialog.getText(None, "Rename", "Enter new pin name") if confirmed and name != self.name and name != "": uniqueName = self._rawPin.owningNode().graph( ).graphManager.getUniqName(name) self.setName(uniqueName) self.setDisplayName(uniqueName) self.owningNode().invalidateNodeLayouts() self.owningNode().updateNodeShape() def syncDynamic(self): self.setMenuItemEnabled("Remove", self._rawPin.optionEnabled(PinOptions.Dynamic)) @property def dirty(self): return self._rawPin.dirty @dirty.setter def dirty(self, value): self._rawPin.dirty = value def resetToDefault(self): self.setData(self.defaultValue()) def defaultValue(self): return self._rawPin.defaultValue() def currentData(self): return self._rawPin.currentData() @property def name(self): return self._rawPin.name def getName(self): return self._rawPin.getName() def hasConnections(self): return self._rawPin.hasConnections() def setClean(self): self._rawPin.setClean() def setDirty(self): self._rawPin.setDirty() @property def _data(self): return self._rawPin._data @_data.setter def _data(self, value): self._rawPin._data = value @property def affects(self): return self._rawPin.affects @property def direction(self): return self._rawPin.direction @property def affected_by(self): return self._rawPin.affected_by def supportedDataTypes(self): return self._rawPin.supportedDataTypes() @property def connections(self): return self.uiConnectionList @property def uid(self): return self._rawPin._uid @uid.setter def uid(self, value): self._rawPin._uid = value def color(self): return self._pinColor def setName(self, newName, force=False): return self._rawPin.setName(newName, force=force) def setData(self, value): self._rawPin.setData(value) self.dataBeenSet.emit(value) def getData(self): return self._rawPin.getData() def highlight(self): # TODO: draw svg arrow instead self.bAnimate = True t = QtCore.QTimeLine(900, self) t.setFrameRange(0, 100) t.frameChanged[int].connect(self.animFrameChanged) t.finished.connect(self.animationFinished) t.start() def call(self): self._rawPin.call() for e in self.connections: e.highlight() def kill(self, *args, **kwargs): """this will be called after raw pin is deleted """ scene = self.scene() if scene is None: # already deleted del self return if self._rawPin.direction == PinDirection.Input: self.owningNode().inputsLayout.removeItem(self) else: self.owningNode().outputsLayout.removeItem(self) self.OnPinDeleted.emit(self) scene.removeItem(self) self.owningNode().updateNodeShape() def assignRawPin(self, rawPin): if rawPin is not self._rawPin: self._rawPin = rawPin self.call = rawPin.call self._rawPin.setWrapper(self) self._pinColor = QtGui.QColor(*self._rawPin.color()) def serializationHook(self, *args, **kwargs): data = {} data['bLabelHidden'] = self.bLabelHidden data['displayName'] = self.displayName() return data def serialize(self): return self._rawPin.serialize() def getContainer(self): return self._container def isExec(self): return self._rawPin.isExec() @property def dataType(self): return self._rawPin.dataType def sizeHint(self, which, constraint): height = QtGui.QFontMetrics(self._font).height() width = self.pinSize * 2 if not self.bLabelHidden: width += QtGui.QFontMetrics(self._font).width(self.displayName()) if not self.isVisible(): width = 0 height = 0 return QtCore.QSizeF(width, height) def shape(self): path = QtGui.QPainterPath() path.addEllipse(self.boundingRect()) return path def isList(self): return self._rawPin.isList() def isArray(self): return self._rawPin.isArray() def paint(self, painter, option, widget): if self.isArray(): PinPainter.asArrayPin(self, painter, option, widget) else: PinPainter.asValuePin(self, painter, option, widget) def contextMenuEvent(self, event): self.menu.exec_(event.screenPos()) def getLayout(self): if self.direction == PinDirection.Input: return self.owningNode().inputsLayout else: return self.owningNode().outputsLayout def hoverEnterEvent(self, event): super(UIPinBase, self).hoverEnterEvent(event) self.update() self.hovered = True hoverMessage = "Data: {0}\r\nDirty: {1}".format( str(self._rawPin.currentData()), self._rawPin.dirty) self.setToolTip(hoverMessage) event.accept() def hoverLeaveEvent(self, event): super(UIPinBase, self).hoverLeaveEvent(event) self.update() self.hovered = False def pinConnected(self, other): self.OnPinConnected.emit(other) self.update() def pinDisconnected(self, other): self.OnPinDisconnected.emit(other) self.update() def selectStructure(self): item, ok = QInputDialog.getItem(None, "", "", ([i.name for i in list(PinStructure)]), 0, False) if ok and item: self._rawPin.changeStructure(PinStructure[item], True)
class scene_inputs(Node): def __init__(self, name, graph): super(scene_inputs, self).__init__(name, graph) self.menu = QMenu() self.action = self.menu.addAction('add port') self.action.triggered.connect(self.addInPin) self.setPos(self.graph().mapToScene(self.graph().viewport().rect().x(),self.graph().viewport().rect().y()+50) ) self.asGraphSides = True self.sizes[4] = 0 self.sizes[5] = 0 self.setFlag(QGraphicsItem.ItemIsMovable,False) self.setFlag(QGraphicsItem.ItemIsFocusable,False) self.setFlag(QGraphicsItem.ItemIsSelectable,False) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges,False) #self.setFlag(QGraphicsItem.ItemIgnoresTransformations) self.label().hide() self.sender = sender() def addInPin(self): const = len(self.outputs)+1 p = self.addOutputPin("input_%i"%const, DataTypes.Any,editable=True) #p.dynamic = True p.setDeletable() self.sender.pinCreated.emit(p) def contextMenuEvent(self, event): self.menu.exec_(event.screenPos()) @staticmethod def pinTypeHints(): return {'inputs': [], 'outputs': [supportedDataTypesList]} @staticmethod def category(): return '__hiden__' @staticmethod def keywords(): return [] @staticmethod def description(): return 'Genertic array' def postCreate(self, jsonTemplate): Node.postCreate(self, jsonTemplate) # restore dynamically created inputs for inp in jsonTemplate['outputs']: PinWidgetBase.deserialize(self, inp) #pinAffects(p, self.out0) def boundingRect(self): rect = self.childrenBoundingRect() rect.setHeight(self.graph().boundingRect.height()) self.setPos(self.graph().boundingRect.topLeft().x(),self.graph().boundingRect.topLeft().y()+50) #rect.setWidth(self.graph().boundingRect.width()/30) return rect def paint(self, painter, option, widget): NodePainter.asGraphSides(self, painter, option, widget)
class UIConnection(QGraphicsPathItem): """UIConnection is a cubic spline curve. It represents connection between two pins. """ def __init__(self, source, destination, canvas): QGraphicsPathItem.__init__(self) self.setAcceptedMouseButtons(QtCore.Qt.LeftButton) self.setAcceptHoverEvents(True) self.setFlag(QGraphicsPathItem.ItemIsSelectable) self._menu = QMenu() self.actionDisconnect = self._menu.addAction("Disconnect") self.actionDisconnect.triggered.connect(self.kill) self._uid = uuid4() self.canvasRef = weakref.ref(canvas) self.source = weakref.ref(source) self.destination = weakref.ref(destination) self.drawSource = self.source() self.drawDestination = self.destination() # Overrides for getting endpoints positions # if None - pin centers will be used self.sourcePositionOverride = None self.destinationPositionOverride = None self.mPath = QtGui.QPainterPath() self.cp1 = QtCore.QPointF(0.0, 0.0) self.cp2 = QtCore.QPointF(0.0, 0.0) self.setZValue(NodeDefaults().Z_LAYER - 1) self.color = self.source().color() self.selectedColor = self.color.lighter(150) self.thickness = 1 self.thicknessMultiplier = 1 if source.isExec(): self.thickness = 2 self.pen = QtGui.QPen(self.color, self.thickness, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin) points = self.getEndPoints() self.updateCurve(points[0], points[1]) self.setPen(self.pen) self.source().update() self.destination().update() self.fade = 0.0 self.source().uiConnectionList.append(self) self.destination().uiConnectionList.append(self) self.source().pinConnected(self.destination()) self.destination().pinConnected(self.source()) if self.source().isExec(): self.bubble = QGraphicsEllipseItem(-2.5, -2.5, 5, 5, self) self.bubble.setBrush(self.color) self.bubble.setPen(self.pen) point = self.mPath.pointAtPercent(0.0) self.bubble.setPos(point) self.bubble.hide() self.source()._rawPin.onExecute.connect(self.performEvaluationFeedback) self.shouldAnimate = False self.timeline = QtCore.QTimeLine(2000) self.timeline.setFrameRange(0, 100) self.timeline.frameChanged.connect(self.timelineFrameChanged) self.timeline.setLoopCount(0) def performEvaluationFeedback(self, *args, **kwargs): if self.timeline.state() == QtCore.QTimeLine.State.NotRunning: self.shouldAnimate = True # spawn bubble self.bubble.show() self.timeline.start() def timelineFrameChanged(self, frameNum): percentage = currentProcessorTime() - self.source()._rawPin.getLastExecutionTime() self.shouldAnimate = percentage < 0.5 point = self.mPath.pointAtPercent(float(frameNum) / float(self.timeline.endFrame())) self.bubble.setPos(point) if not self.shouldAnimate: self.timeline.stop() self.bubble.hide() def setSelected(self, value): super(UIConnection, self).setSelected(value) def isUnderCollapsedComment(self): srcNode = self.source().owningNode() dstNode = self.destination().owningNode() srcComment = srcNode.owningCommentNode dstComment = dstNode.owningCommentNode if srcComment is not None and dstComment is not None and srcComment == dstComment and srcComment.collapsed: return True return False def isUnderActiveGraph(self): return self.canvasRef().graphManager.activeGraph() == self.source()._rawPin.owningNode().graph() def __repr__(self): return "{0} -> {1}".format(self.source().getFullName(), self.destination().getFullName()) def setColor(self, color): self.pen.setColor(color) self.color = color def updateEndpointsPositions(self): srcNode = self.source().owningNode() dstNode = self.destination().owningNode() srcComment = srcNode.owningCommentNode if srcComment is not None: # if comment is collapsed or under another comment, move point to top most collapsed comment's right side srcNodeUnderCollapsedComment = srcComment.isUnderCollapsedComment() topMostCollapsedComment = srcNode.getTopMostOwningCollapsedComment() if srcComment.collapsed: rightSideEndpointGetter = srcComment.getRightSideEdgesPoint if srcNodeUnderCollapsedComment: rightSideEndpointGetter = topMostCollapsedComment.getRightSideEdgesPoint self.sourcePositionOverride = rightSideEndpointGetter else: if srcNodeUnderCollapsedComment: self.sourcePositionOverride = topMostCollapsedComment.getRightSideEdgesPoint else: self.sourcePositionOverride = None else: # if no comment return source point back to pin self.sourcePositionOverride = None # Same for right hand side dstComment = dstNode.owningCommentNode if dstComment is not None: dstNodeUnderCollapsedComment = dstComment.isUnderCollapsedComment() topMostCollapsedComment = dstNode.getTopMostOwningCollapsedComment() if dstComment.collapsed: rightSideEndpointGetter = dstComment.getLeftSideEdgesPoint if dstNodeUnderCollapsedComment: rightSideEndpointGetter = topMostCollapsedComment.getLeftSideEdgesPoint self.destinationPositionOverride = rightSideEndpointGetter else: if dstNodeUnderCollapsedComment: self.destinationPositionOverride = topMostCollapsedComment.getLeftSideEdgesPoint else: self.destinationPositionOverride = None else: self.destinationPositionOverride = None def Tick(self): # check if this instance represents existing connection # if not - destroy if not arePinsConnected(self.source()._rawPin, self.destination()._rawPin): self.canvasRef().removeConnection(self) if self.drawSource.isExec() or self.drawDestination.isExec(): if self.thickness != 2: self.thickness = 2 self.pen.setWidthF(self.thickness) if self.isSelected(): self.pen.setColor(self.selectedColor) else: self.pen.setColor(self.color) self.update() def contextMenuEvent(self, event): self._menu.exec_(event.screenPos()) @property def uid(self): return self._uid @uid.setter def uid(self, value): if self._uid in self.canvasRef().connections: self.canvasRef().connections[value] = self.canvasRef().connections.pop(self._uid) self._uid = value @staticmethod def deserialize(data, graph): srcUUID = UUID(data['sourceUUID']) dstUUID = UUID(data['destinationUUID']) # if srcUUID in graph.pins and dstUUID in graph.pins: srcPin = graph.findPinByUid(srcUUID) assert(srcPin is not None) dstPin = graph.findPinByUid(dstUUID) assert(dstPin is not None) connection = graph.connectPinsInternal(srcPin, dstPin) assert(connection is not None) connection.uid = UUID(data['uuid']) def serialize(self): script = {'sourceUUID': str(self.source().uid), 'destinationUUID': str(self.destination().uid), 'sourceName': self.source()._rawPin.getFullName(), 'destinationName': self.destination()._rawPin.getFullName(), 'uuid': str(self.uid) } return script def __str__(self): return '{0} >>> {1}'.format(self.source()._rawPin.getFullName(), self.destination()._rawPin.getFullName()) def drawThick(self): self.pen.setWidthF(self.thickness + (self.thickness / 1.5)) f = 0.5 r = abs(lerp(self.color.red(), Colors.Yellow.red(), clamp(f, 0, 1))) g = abs(lerp(self.color.green(), Colors.Yellow.green(), clamp(f, 0, 1))) b = abs(lerp(self.color.blue(), Colors.Yellow.blue(), clamp(f, 0, 1))) self.pen.setColor(QtGui.QColor.fromRgb(r, g, b)) def restoreThick(self): self.pen.setWidthF(self.thickness) self.pen.setColor(self.color) def hoverEnterEvent(self, event): super(UIConnection, self).hoverEnterEvent(event) self.drawThick() self.update() def getEndPoints(self): p1 = self.drawSource.scenePos() + self.drawSource.pinCenter() if self.sourcePositionOverride is not None: p1 = self.sourcePositionOverride() p2 = self.drawDestination.scenePos() + self.drawDestination.pinCenter() if self.destinationPositionOverride is not None: p2 = self.destinationPositionOverride() return p1, p2 def mousePressEvent(self, event): super(UIConnection, self).mousePressEvent(event) event.accept() def mouseReleaseEvent(self, event): super(UIConnection, self).mouseReleaseEvent(event) event.accept() def mouseMoveEvent(self, event): super(UIConnection, self).mouseMoveEvent(event) event.accept() def hoverLeaveEvent(self, event): super(UIConnection, self).hoverLeaveEvent(event) self.restoreThick() self.update() def source_port_name(self): return self.source().getFullName() def shape(self): qp = QtGui.QPainterPathStroker() qp.setWidth(10.0) qp.setCapStyle(QtCore.Qt.SquareCap) return qp.createStroke(self.path()) def updateCurve(self, p1, p2): xDistance = p2.x() - p1.x() multiply = 3 self.mPath = QtGui.QPainterPath() self.mPath.moveTo(p1) if xDistance < 0: self.mPath.cubicTo(QtCore.QPoint(p1.x() + xDistance / -multiply, p1.y()), QtCore.QPoint(p2.x() - xDistance / -multiply, p2.y()), p2) else: self.mPath.cubicTo(QtCore.QPoint(p1.x() + xDistance / multiply, p1.y()), QtCore.QPoint(p2.x() - xDistance / 2, p2.y()), p2) self.setPath(self.mPath) def kill(self): self.canvasRef().removeConnection(self) def paint(self, painter, option, widget): option.state &= ~QStyle.State_Selected lod = self.canvasRef().getCanvasLodValueFromCurrentScale() self.setPen(self.pen) p1, p2 = self.getEndPoints() if editableStyleSheet().ConnectionMode[0] in [ConnectionTypes.Circuit,ConnectionTypes.ComplexCircuit]: sameSide = 0 offset = 15 roundnes = editableStyleSheet().ConnectionRoundness[0] if self.destination().owningNode()._rawNode.__class__.__name__ in ["reroute", "rerouteExecs"]: xDistance = p2.x() - p1.x() if xDistance < 0: p2, p1 = self.getEndPoints() sameSide = 1 if self.source().owningNode()._rawNode.__class__.__name__ in ["reroute", "rerouteExecs"]: p11, p22 = self.getEndPoints() xDistance = p22.x() - p11.x() if xDistance < 0: sameSide = -1 p1, p2 = self.getEndPoints() self.mPath = ConnectionPainter.BasicCircuit(p1, p2, offset, roundnes, sameSide,lod,editableStyleSheet().ConnectionMode[0]==ConnectionTypes.ComplexCircuit) elif editableStyleSheet().ConnectionMode[0] == ConnectionTypes.Cubic: self.mPath = ConnectionPainter.Cubic(p1, p2, 150,lod) self.setPath(self.mPath) super(UIConnection, self).paint(painter, option, widget)
class PinWidgetBase(QGraphicsWidget, PinBase): ''' This is base class for all ui pins ''' ## Event called when pin is connected OnPinConnected = QtCore.Signal(object) ## Event called when pin is disconnected OnPinDisconnected = QtCore.Signal(object) ## Event called when data been set dataBeenSet = QtCore.Signal(object) ## Event called when pin name changes nameChanged = QtCore.Signal(str) ## Event called when setUserStruct called # used by enums userStructChanged = QtCore.Signal(object) ## Event called when pin is deleted OnPinDeleted = QtCore.Signal(object) ## Event called when pin is deleted OnPinChanged = QtCore.Signal(object) def __init__(self, name, parent, dataType, direction, **kwargs): QGraphicsWidget.__init__(self) PinBase.__init__(self, name, parent, dataType, direction, **kwargs) self.setParentItem(parent) self.setCursor(QtCore.Qt.CrossCursor) ## context menu for pin self.menu = QMenu() ## Disconnect all connections self.actionDisconnect = self.menu.addAction('disconnect all') self.actionDisconnect.triggered.connect(self.disconnectAll) ## Copy UUID to buffer self.actionCopyUid = self.menu.addAction('copy uid') self.actionCopyUid.triggered.connect(self.saveUidToClipboard) ## Call exec pin self.actionCall = self.menu.addAction('execute') self.actionCall.triggered.connect(self.call) self.newPos = QtCore.QPointF() self.setFlag(QGraphicsWidget.ItemSendsGeometryChanges) self.setCacheMode(self.DeviceCoordinateCache) self.setAcceptHoverEvents(True) self.setZValue(2) self.width = 8 + 1 self.height = 8 + 1 self.hovered = False self.startPos = None self.endPos = None self._container = None self._execPen = QtGui.QPen(Colors.Exec, 0.5, QtCore.Qt.SolidLine) self.setGeometry(0, 0, self.width, self.height) self._dirty_pen = QtGui.QPen(Colors.DirtyPen, 0.5, QtCore.Qt.DashLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin) self.pinImage = QtGui.QImage(':/icons/resources/array.png') def updateConstraint(self, constraint): self.constraint = constraint if self.parent()._Constraints.has_key(constraint): self.parent()._Constraints[constraint].append(self) else: self.parent()._Constraints[constraint] = [self] def setUserStruct(self, inStruct): PinBase.setUserStruct(self, inStruct) self.userStructChanged.emit(inStruct) def setDeletable(self): self.deletable = True self.actionRemove = self.menu.addAction('remove') self.actionRemove.triggered.connect(self.kill) def setName(self, newName): super(PinWidgetBase, self).setName(newName) self.nameChanged.emit(newName) def setData(self, value): PinBase.setData(self, value) self.dataBeenSet.emit(value) @property def dataType(self): return self._dataType @dataType.setter def dataType(self, value): self._dataType = value def setDataType(self, value): self.dataType = value def highlight(self): self.bAnimate = True t = QtCore.QTimeLine(900, self) t.setFrameRange(0, 100) t.frameChanged[int].connect(self.animFrameChanged) t.finished.connect(self.animationFinished) t.start() def animFrameChanged(self, value): self.width = clamp(math.sin(self._val) * 9, 4.5, 25) self.update() self._val += 0.1 def animationFinished(self): self.width = 9 self.update() self._val = 0 @staticmethod def color(): return QtGui.QColor() def call(self): PinBase.call(self) def kill(self): self.OnPinDeleted.emit(self) PinBase.kill(self) self.disconnectAll() if hasattr(self.parent(), self.name): delattr(self.parent(), self.name) if self._container is not None: self.parent().graph().scene().removeItem(self._container) if self.direction == PinDirection.Input: self.parent().inputsLayout.removeItem(self._container) else: self.parent().outputsLayout.removeItem(self._container) #print self.parent().outputs @staticmethod def deserialize(owningNode, jsonData): name = jsonData['name'] dataType = jsonData['dataType'] direction = jsonData['direction'] value = jsonData['value'] uid = uuid.UUID(jsonData['uuid']) bLabelHidden = jsonData['bLabelHidden'] bDirty = jsonData['bDirty'] deletable = jsonData['deletable'] if 'editable' in jsonData: editable = jsonData['editable'] else: editable = False p = None if direction == PinDirection.Input: p = owningNode.addInputPin(name, dataType, hideLabel=bLabelHidden, editable=editable) p.uid = uid else: p = owningNode.addOutputPin(name, dataType, hideLabel=bLabelHidden, editable=editable) p.uid = uid if deletable: p.setDeletable() if "curr_dataType" in jsonData and jsonData[ "curr_dataType"] != dataType: from ..Pins import CreatePin a = CreatePin("", None, jsonData["curr_dataType"], 0) p.setType(a) del a p.setData(value) return p def serialize(self): data = PinBase.serialize(self) return data def ungrabMouseEvent(self, event): super(PinWidgetBase, self).ungrabMouseEvent(event) def get_container(self): return self._container #def translate(self, x, y): # super(PinWidgetBase, self).moveBy(x, y) def boundingRect(self): if not self.dataType == DataTypes.Exec: return QtCore.QRectF(0, -0.5, 8 * 1.5, 8 + 1.0) else: return QtCore.QRectF(0, -0.5, 10 * 1.5, 10 + 1.0) def sizeHint(self, which, constraint): return QtCore.QSizeF(self.width, self.height) def saveUidToClipboard(self): clipboard = QApplication.clipboard() clipboard.clear() clipboard.setText(str(self.uid)) def disconnectAll(self): trash = [] for e in self.edge_list: if self.uid == e.destination().uid: trash.append(e) if self.uid == e.source().uid: trash.append(e) for e in trash: self.parent().graph().removeEdge(e) def shape(self): path = QtGui.QPainterPath() path.addEllipse(self.boundingRect()) return path def paint(self, painter, option, widget): background_rect = QtCore.QRectF(0, 0, self.width, self.width) self.cPos = background_rect w = background_rect.width() / 2 h = background_rect.height() / 2 linearGrad = QtGui.QRadialGradient(QtCore.QPointF(w, h), self.width / 2.5) if not self._connected: linearGrad.setColorAt(0, self.color().darker(280)) linearGrad.setColorAt(0.5, self.color().darker(280)) linearGrad.setColorAt(0.65, self.color().lighter(130)) linearGrad.setColorAt(1, self.color().lighter(70)) else: linearGrad.setColorAt(0, self.color()) linearGrad.setColorAt(1, self.color()) if self.hovered: linearGrad.setColorAt(1, self.color().lighter(200)) if self.dataType == DataTypes.Array: if self.pinImage: painter.drawImage(background_rect, self.pinImage) else: painter.setBrush(Colors.Array) rect = background_rect painter.drawRect(rect) elif self.dataType == DataTypes.Exec: painter.setPen(self._execPen) if self._connected: painter.setBrush(QtGui.QBrush(self.color())) else: painter.setBrush(QtCore.Qt.NoBrush) arrow = QtGui.QPolygonF([ QtCore.QPointF(0.0, 0.0), QtCore.QPointF(self.width / 2.0, 0.0), QtCore.QPointF(self.width, self.height / 2.0), QtCore.QPointF(self.width / 2.0, self.height), QtCore.QPointF(0, self.height) ]) painter.drawPolygon(arrow) else: painter.setBrush(QtGui.QBrush(linearGrad)) rect = background_rect.setX(background_rect.x()) painter.drawEllipse(background_rect) def contextMenuEvent(self, event): self.menu.exec_(event.screenPos()) def getLayout(self): if self.direction == PinDirection.Input: return self.parent().inputsLayout else: return self.parent().outputsLayout def hoverEnterEvent(self, event): super(PinWidgetBase, self).hoverEnterEvent(event) self.update() self.hovered = True self.setToolTip(str(self.currentData())) event.accept() def hoverLeaveEvent(self, event): super(PinWidgetBase, self).hoverLeaveEvent(event) self.update() self.hovered = False def pinConnected(self, other): PinBase.pinConnected(self, other) #if self.dynamic: # data = self.serialize() # pin = self.deserialize(self.parent(),data) # pin.dynamic=True self.OnPinConnected.emit(other) def pinDisconnected(self, other): PinBase.pinDisconnected(self, other) self.OnPinDisconnected.emit(other)
class OptionList(QGroupBox, object): editModeChanged = Signal(bool) valueChanged = Signal() FACTORY_CLASS = factory def __init__(self, parent=None, option_object=None): super(OptionList, self).__init__(parent) self._option_object = option_object self._parent = parent self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self._on_item_menu) self._context_menu = QMenu() self._context_menu.setTearOffEnabled(True) self._create_context_menu(self._context_menu, parent=self) self._has_first_group = False self._disable_auto_expand = False self._supress_update = False self._central_list = self self._option_group_class = OptionListGroup self._auto_rename = False self.setup_ui() def mousePressEvent(self, event): """ Overrides base QGroupBox mousePressEvent function :param event: QMouseEvent """ widget_at_mouse = qtutils.get_widget_at_mouse() if widget_at_mouse == self: self.clear_selection() super(OptionList, self).mousePressEvent(event) def setup_ui(self): self.main_layout = layouts.VerticalLayout(spacing=5, margins=(5, 5, 5, 5)) self.setLayout(self.main_layout) self.child_layout = layouts.VerticalLayout(spacing=5, margins=(5, 5, 5, 5)) self.child_layout.setAlignment(Qt.AlignTop) self.main_layout.addLayout(self.child_layout) self.main_layout.addSpacing(30) def get_option_object(self): """ Returns the option object linked to this widget :return: object """ return self._option_object def set_option_object(self, option_object): """ Sets option object linked to this widget :param option_object: object """ self._option_object = option_object def update_options(self): """ Updates current widget options """ if not self._option_object: LOGGER.warning( 'Impossible to update options because option object is not defined!' ) return options = self._option_object.get_options() self._load_widgets(options) def get_parent(self): """ Returns parent Option """ parent = self.parent() grand_parent = parent.parent() if hasattr(grand_parent, 'group'): parent = grand_parent if not hasattr(parent, 'child_layout'): return if parent.__class__.__name__.endswith('OptionList'): return parent return parent def add_group(self, name='group', value=True, parent=None): """ Adds new group property to the group box :param name: str :param value: bool, default value :param parent: Option """ if type(name) == bool: name = 'group' name = self._get_unique_name(name, parent) option_object = self.get_option_object() self._option_group_class.FACTORY_CLASS = self.FACTORY_CLASS group = self._option_group_class(name=name, option_object=option_object, parent=self._parent) self._create_group_context_menu(group, group._context_menu) group.set_expanded(value) if self.__class__.__name__.endswith( 'OptionListGroup') or parent.__class__.__name__.endswith( 'OptionListGroup'): if dcc.is_maya(): group.group.set_inset_dark() self._handle_parenting(group, parent) self._write_options(clear=False) self._has_first_group = True return group def update_current_widget(self, widget=None): """ Function that updates given widget status :param widget: QWidget """ if self._parent.is_edit_mode() is False: return if widget: if self.is_selected(widget): self.deselect_widget(widget) return else: self.select_widget(widget) return def is_selected(self, widget): """ Returns whether property widget is selected or not :param widget: QWidget :return: bool """ if widget in self._parent._current_widgets: return True return False def select_widget(self, widget): """ Selects given Option widget :param widget: Option """ if hasattr(widget, 'child_layout'): self._deselect_children(widget) parent = widget.get_parent() if not parent: parent = widget.parent() out_of_scope = None if parent: out_of_scope = self.sort_widgets(self._parent._current_widgets, parent, return_out_of_scope=True) if out_of_scope: for sub_widget in out_of_scope: self.deselect_widget(sub_widget) self._parent._current_widgets.append(widget) self._fill_background(widget) def deselect_widget(self, widget): """ Deselects given Option widget :param widget: Option """ if not self.is_selected(widget): return widget_index = self._parent._current_widgets.index(widget) self._parent._current_widgets.pop(widget_index) self._unfill_background(widget) def clear_selection(self): """ Clear current selected Option widgets """ for widget in self._parent._current_widgets: self._unfill_background(widget) self._parent._current_widgets = list() def sort_widgets(self, widgets, parent, return_out_of_scope=False): """ Sort current Option widgets :param widgets: list(Option) :param parent: Options :param return_out_of_scope: bool :return: list(Option) """ if not hasattr(parent, 'child_layout'): return item_count = parent.child_layout.count() found = list() for i in range(item_count): item = parent.child_layout.itemAt(i) if item: widget = item.widget() for sub_widget in widgets: if sub_widget == widget: found.append(widget) if return_out_of_scope: other_found = list() for sub_widget in widgets: if sub_widget not in found: other_found.append(sub_widget) found = other_found return found def clear_widgets(self): """ Removes all widgets from current group """ self._has_first_group = False item_count = self.child_layout.count() for i in range(item_count, -1, -1): item = self.child_layout.itemAt(i) if item: widget = item.widget() self.child_layout.removeWidget(widget) widget.deleteLater() self._parent._current_widgets = list() def set_edit(self, flag): """ Set the edit mode of the group :param flag: bool """ self.editModeChanged.emit(flag) def _create_context_menu(self, menu, parent): plus_icon = resources.icon('plus') string_icon = resources.icon('rename') directory_icon = resources.icon('folder') file_icon = resources.icon('file') integer_icon = resources.icon('number_1') float_icon = resources.icon('float_1') bool_icon = resources.icon('true_false') dict_icon = resources.icon('dictionary') list_icon = resources.icon('list') group_icon = resources.icon('group_objects') script_icon = resources.icon('source_code') title_icon = resources.icon('label') color_icon = resources.icon('palette') clear_icon = resources.icon('clean') copy_icon = resources.icon('copy') paste_icon = resources.icon('paste') create_menu = menu.findChild(QMenu, 'createMenu') if not create_menu: create_menu = menu.addMenu(plus_icon, 'Add Options') create_menu.setObjectName('createMenu') add_string_action = QAction(string_icon, 'Add String', create_menu) create_menu.addAction(add_string_action) add_directory_action = QAction(directory_icon, 'Add Directory', create_menu) create_menu.addAction(add_directory_action) add_file_action = QAction(file_icon, 'Add File', create_menu) create_menu.addAction(add_file_action) add_integer_action = QAction(integer_icon, 'Add Integer', create_menu) create_menu.addAction(add_integer_action) add_float_action = QAction(float_icon, 'Add Float', create_menu) create_menu.addAction(add_float_action) add_bool_action = QAction(bool_icon, 'Add Bool', create_menu) create_menu.addAction(add_bool_action) add_list_action = QAction(list_icon, 'Add List', create_menu) create_menu.addAction(add_list_action) add_dict_action = QAction(dict_icon, 'Add Dictionary', create_menu) create_menu.addAction(add_dict_action) add_group_action = QAction(group_icon, 'Add Group', create_menu) create_menu.addAction(add_group_action) add_script_action = QAction(script_icon, 'Add Script', create_menu) create_menu.addAction(add_script_action) add_title_action = QAction(title_icon, 'Add Title', create_menu) create_menu.addAction(add_title_action) add_color_action = QAction(color_icon, 'Add Color', create_menu) create_menu.addAction(add_color_action) add_vector3f_action = QAction(color_icon, 'Add Vector 3 float', create_menu) create_menu.addAction(add_vector3f_action) menu.addSeparator() parent.copy_action = QAction(copy_icon, 'Copy', menu) menu.addAction(parent.copy_action) parent.copy_action.setVisible(False) parent.paste_action = QAction(paste_icon, 'Paste', menu) menu.addAction(parent.paste_action) parent.paste_action.setVisible(False) menu.addSeparator() clear_action = QAction(clear_icon, 'Clear', menu) menu.addAction(clear_action) add_string_action.triggered.connect( partial(parent._add_option, 'string')) add_directory_action.triggered.connect( partial(parent._add_option, 'directory')) add_file_action.triggered.connect( partial(parent._add_option, 'file')) add_integer_action.triggered.connect( partial(parent._add_option, 'integer')) add_float_action.triggered.connect( partial(parent._add_option, 'float')) add_bool_action.triggered.connect( partial(parent._add_option, 'boolean')) add_list_action.triggered.connect( partial(parent._add_option, 'list')) add_dict_action.triggered.connect( partial(parent._add_option, 'dictionary')) add_group_action.triggered.connect(parent.add_group) add_title_action.triggered.connect( partial(parent._add_option, 'title')) add_color_action.triggered.connect( partial(parent._add_option, 'color')) add_vector3f_action.triggered.connect( partial(parent._add_option, 'vector3f')) add_script_action.triggered.connect( partial(parent._add_option, 'script')) clear_action.triggered.connect(parent._clear_action) return create_menu def _create_group_context_menu(self, group, menu): self._create_context_menu(menu=menu, parent=group) group.copy_action.setVisible(False) string_icon = resources.icon('rename') remove_icon = resources.icon('trash') rename_action = QAction(string_icon, 'Rename', menu) menu.addAction(rename_action) remove_action = QAction(remove_icon, 'Remove', menu) menu.addAction(remove_action) rename_action.triggered.connect(group.rename) remove_action.triggered.connect(group.remove) return menu def _add_option(self, option_type, name=None, value=None, parent=None): if option_type is None: return if option_type == 'group': new_option = self.add_group('group') else: option_object = self.get_option_object() name = self._get_unique_name(name or option_type, parent=parent) new_option = self.FACTORY_CLASS.add_option( option_type, name=name, value=value, parent=parent, main_widget=self._parent, option_object=option_object) if new_option: self._handle_parenting(new_option, parent=parent) self._write_options(clear=False) else: LOGGER.warning('Option of type "{}" is not supported!'.format( option_type)) return new_option def _add_custom_option(self, option_type, name=None, value=None, parent=None): pass def _get_unique_name(self, name, parent=None): """ Internal function that returns unique name for the new group :param name: str :param parent: QWidget :return: str """ found = self._get_widget_names(parent) while name in found: name = name_utils.increment_last_number(name) return name def _get_widget_names(self, parent=None): """ Internal function that returns current stored widget names :param parent: Option :return: list(str) """ if parent: scope = parent else: scope = self item_count = scope.child_layout.count() found = list() for i in range(item_count): item = scope.child_layout.itemAt(i) widget = item.widget() label = widget.get_name() found.append(label) return found def _get_unique_name(self, name, parent): """ Internal function that returns unique name for the new group :param name: str :param parent: QWidget :return: str """ found = self._get_widget_names(parent) while name in found: name = name_utils.increment_last_number(name) return name def _handle_parenting(self, widget, parent): """ Internal function that handles parenting of given widget and its parent :param widget: Options :param parent: Options """ widget.widgetClicked.connect(self.update_current_widget) # widget.editModeChanged.connect(self._on_activate_edit_mode) if parent: parent.child_layout.addWidget(widget) if hasattr(widget, 'updateValues'): widget.updateValues.connect(parent._write_options) else: self.child_layout.addWidget(widget) if hasattr(widget, 'updateValues'): widget.updateValues.connect(self._write_options) if self._auto_rename: widget.rename() def _get_path(self, widget): """ Internal function that return option path of given option :param widget: Options :return: str """ parent = widget.get_parent() path = '' parents = list() if parent: sub_parent = parent while sub_parent: if issubclass(sub_parent.__class__, OptionList) and not \ sub_parent.__class__.__name__.endswith('OptionListGroup'): break name = sub_parent.get_name() parents.append(name) sub_parent = sub_parent.get_parent() parents.reverse() for sub_parent in parents: path += '{}.'.format(sub_parent) if hasattr(widget, 'child_layout'): path = path + widget.get_name() + '.' else: path = path + widget.get_name() return path def _load_widgets(self, options): """ Internal function that loads widget with given options :param options: dict """ self.clear_widgets() if not options: return self._supress_update = True self._disable_auto_expand = True self._auto_rename = False try: for option in options: option_type = None if type(option[1]) == list: if option[0] == 'list': value = option[1] option_type = 'list' else: value = option[1][0] option_type = option[1][1] # value = option[1][0] # option_type = option[1][1] else: value = option[1] split_name = option[0].split('.') if split_name[-1] == '': search_group = string.join(split_name[:-2], '.') name = split_name[-2] else: search_group = string.join(split_name[:-1], '.') name = split_name[-1] widget = self._find_group_widget(search_group) if not widget: widget = self is_group = False if split_name[-1] == '': is_group = True parent_name = string.join(split_name[:-1], '.') group = self._find_group_widget(parent_name) if not group: self.add_group(name, value, widget) if len(split_name) > 1 and split_name[-1] != '': search_group = string.join(split_name[:-2], '.') after_search_group = string.join(split_name[:-1], '.') group_name = split_name[-2] group_widget = self._find_group_widget(search_group) widget = self._find_group_widget(after_search_group) if not widget: self.add_group(group_name, value, group_widget) widget = self._find_group_widget(after_search_group) if not is_group: if not option_type: if type(value) == unicode or type(value) == str: option_type = 'string' elif type(value) == float: option_type = 'float' elif type(option[1]) == int: option_type = 'integer' elif type(option[1]) == bool: option_type = 'boolean' elif type(option[1]) == dict: option_type = 'dictionary' elif type(option[1]) == list: option_type = 'list' elif option[1] is None: option_type = 'title' new_option = self._add_custom_option( option_type, name, value, widget) if not new_option: self._add_option(option_type, name, value, widget) except Exception: LOGGER.error(traceback.format_exc()) finally: self._disable_auto_expand = False # self.setVisible(True) # self.setUpdatesEnabled(True) self._supress_update = False self._auto_rename = True def _find_list(self, widget): if widget.__class__.__name__.endswith('OptionList'): return widget parent = widget.get_parent() if not parent: return while not parent.__class__.__name__.endswith('OptionList'): parent = parent.get_parent() return parent def _find_group_widget(self, name): """ Internal function that returns OptionList with given name (if exists) :param name: str, name of the group to find :return: variant, OptionList or None """ split_name = name.split('.') sub_widget = None for name in split_name: if not sub_widget: sub_widget = self found = False item_count = sub_widget.child_layout.count() for i in range(item_count): item = sub_widget.child_layout.itemAt(i) if item: widget = item.widget() label = widget.get_name() if label == name: sub_widget = widget found = True break else: break if not found: return return sub_widget def _deselect_children(self, widget): """ Internal function that deselects all the children widgets of the given Option :param widget: Option """ children = widget.get_children() for child in children: self.deselect_widget(child) def _clear_action(self): """ Internal function that clears all widgets """ if self.__class__ == OptionList: name = 'the list?' else: name = 'group?' item_count = self.child_layout.count() if item_count <= 0: LOGGER.debug('No widgets to clear ...') return permission = qtutils.get_permission('Clear all the widgets?', parent=self) if permission: self.clear_widgets() self._write_options(clear=True) def _write_options(self, clear=True): """ Internal function that writes current options into disk :param clear: bool """ if not self._option_object: LOGGER.warning( 'Impossible to write options because option object is not defined!' ) return if self._supress_update: return if clear: self._write_all() else: item_count = self.child_layout.count() for i in range(0, item_count): item = self.child_layout.itemAt(i) widget = item.widget() widget_type = widget.get_option_type() name = self._get_path(widget) value = widget.get_value() self._option_object.add_option(name, value, None, widget_type) self.valueChanged.emit() def _write_widget_options(self, widget): if not widget: return if not self._option_object: LOGGER.warning( 'Impossible to write options because option object is not defined!' ) return item_count = widget.child_layout.count() for i in range(item_count): item = widget.child_layout.itemAt(i) if item: sub_widget = item.widget() sub_widget_type = sub_widget.get_option_type() name = self._get_path(sub_widget) value = sub_widget.get_value() self._option_object.add_option(name, value, None, sub_widget_type) if hasattr(sub_widget, 'child_layout'): self._write_widget_options(sub_widget) def _write_all(self): if not self._option_object: LOGGER.warning( 'Impossible to write options because option object is not defined!' ) return self._option_object.clear_options() options_list = self._find_list(self) self._write_widget_options(options_list) def _fill_background(self, widget): """ Internal function used to paint the background color of the group :param widget: Option """ palette = widget.palette() if not dcc.is_maya(): palette.setColor(widget.backgroundRole(), Qt.gray) else: palette.setColor(widget.backgroundRole(), QColor(35, 150, 245, 255)) widget.setAutoFillBackground(True) widget.setPalette(palette) def _unfill_background(self, widget): """ Internal function that clears the background color of the group :param widget: Option """ palette = widget.palette() palette.setColor(widget.backgroundRole(), widget._original_background_color) widget.setAutoFillBackground(False) widget.setPalette(palette) def _on_item_menu(self, pos): """ Internal callback function that is is called when the user right click on an Option Pop ups item menu on given position :param pos: QPoint """ if not self._parent.is_edit_mode(): return if self._parent.is_widget_to_copy(): self.paste_action.setVisible(True) self._context_menu.exec_(self.mapToGlobal(pos)) def _on_activate_edit_mode(self): """ Internal callback function that is called when the user presses edit mode button """ self.editModeChanged.emit(True) def _on_copy_widget(self): """ Internal callback function that is called when the user copy a Option """ self._parent.set_widget_to_copy(self) def _on_paste_widget(self): """ Internal callback function that is called when the user paste a Option """ self.paste_action.setVisible(False) widget_to_copy = self._parent.is_widget_to_copy() if widget_to_copy.task_option_type == 'group': widget_to_copy.copy_to(self)
class Option(base.BaseWidget, object): updateValues = Signal(object) widgetClicked = Signal(object) def __init__(self, name, parent=None, main_widget=None, *args, **kwargs): self._name = name self._option_object = None self._parent = main_widget super(Option, self).__init__(parent=parent) self._original_background_color = self.palette().color( self.backgroundRole()) self._option_type = self.get_option_type() self._option_widget = self.get_option_widget() if self._option_widget: self.main_layout.addWidget(self._option_widget) self._setup_option_widget_value_change() self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self._on_item_menu) self._context_menu = None def get_main_layout(self): main_layout = layouts.HorizontalLayout() main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) return main_layout def ui(self): super(Option, self).ui() def mousePressEvent(self, event): super(Option, self).mousePressEvent(event) if not event.button() == Qt.LeftButton: return parent = self.get_parent() if parent: parent.supress_select = True self.widgetClicked.emit(self) def get_option_type(self): return None def get_option_widget(self): return None def get_name(self): name = self._option_widget.get_label_text() return name def set_name(self, name): self._option_widget.set_label_text(name) def set_value(self, value): pass def get_value(self): pass def get_parent(self): parent = self.parent() grand_parent = parent.parent() if hasattr(grand_parent, 'group'): parent = grand_parent if not hasattr(parent, 'child_layout'): return # We cannot use this because of import problems # if parent.__class__ == optionlist.OptionList: if parent.__class__.__name__ == 'OptionList': return parent return parent def rename(self): title = self.get_name() new_name = qtutils.get_string_input('Rename Option', old_name=title) found = self._get_widget_names() if new_name == title or new_name is None or new_name == '': return while new_name in found: new_name = name_utils.increment_last_number(new_name) self.set_name(new_name) self.updateValues.emit(True) def remove(self): parent = self.get_parent() if self in self._parent._current_widgets: remove_index = self._parent._current_widgets.index(self) self._parent._current_widgets.pop(remove_index) parent.child_layout.removeWidget(self) self.deleteLater() self.updateValues.emit(True) def move_up(self): parent = self.get_parent() if not parent: parent = self.parent() layout = parent.child_layout index = layout.indexOf(self) if index == 0: return index -= 1 parent.child_layout.removeWidget(self) layout.insertWidget(index, self) self.updateValues.emit(True) def move_down(self): parent = self.get_parent() if not parent: parent = self.parent() layout = parent.child_layout index = layout.indexOf(self) if index == 0: return index += 1 parent.child_layout.removeWidget(self) layout.insertWidget(index, self) self.updateValues.emit(True) def copy_to(self, parent): name = self.get_name() value = self.get_value() new_inst = self.__class__(name) new_inst.set_value(value) parent.child_layout.addWidget(new_inst) def set_option_object(self, option_object): self._option_object = option_object def _setup_option_widget_value_change(self): pass def _copy(self): self._parent.set_widget_to_copy(self) def _get_widget_names(self): item_count = self.parent().child_layout.count() found = list() for i in range(item_count): item = self.parent().child_layout.itemAt(i) widget = item.widget() widget_label = widget.get_name() found.append(widget_label) return found def _create_context_menu(self): self._context_menu = QMenu() move_up_icon = resources.icon('sort_up') move_down_icon = resources.icon('sort_down') rename_icon = resources.icon('rename') remove_icon = resources.icon('delete') copy_icon = resources.icon('copy') move_up_action = QAction(move_up_icon, 'Move Up', self._context_menu) self._context_menu.addAction(move_up_action) move_down_action = QAction(move_down_icon, 'Move Down', self._context_menu) self._context_menu.addAction(move_down_action) self._context_menu.addSeparator() copy_action = QAction(copy_icon, 'Copy', self._context_menu) self._context_menu.addAction(copy_action) rename_action = QAction(rename_icon, 'Rename', self._context_menu) self._context_menu.addAction(rename_action) remove_action = QAction(remove_icon, 'Remove', self._context_menu) self._context_menu.addAction(remove_action) move_up_action.triggered.connect(self.move_up) move_down_action.triggered.connect(self.move_down) rename_action.triggered.connect(self.rename) remove_action.triggered.connect(self.remove) def _on_item_menu(self, pos): if not self._parent or not self._parent.is_edit_mode(): return if not self._context_menu: self._create_context_menu() self._context_menu.exec_(self.mapToGlobal(pos)) def _on_value_changed(self): self.updateValues.emit(False)
class switchOnString(Node): def __init__(self, name, graph): super(switchOnString, self).__init__(name, graph) self.inExecPin = self.addInputPin('inExec', DataTypes.Exec, self.compute, hideLabel=True) self.defaultPin = None self.outString = None self.inString = self.addInputPin('String', DataTypes.String) self.menu = QMenu() self.action = self.menu.addAction('add pin') self.action.triggered.connect(self.addOutPin) self.actionDebug = self.menu.addAction('debug') self.actionDebug.triggered.connect(self.OnDebug) self._map = {} def renameOutPin(self, oldName, newName): if oldName in self._map: self._map[oldName].setName(newName) def OnDebug(self): print(self._map.keys()) def addOutPin(self): name = self.getUniqPinName("option") p = self.addOutputPin(name, DataTypes.Exec) renameAction = p.menu.addAction("rename") killAction = p.menu.addAction("kill") def OnKill(): self._map.pop(p.name) p.kill() killAction.triggered.connect(OnKill) def OnRename(): res = QInputDialog.getText(None, 'Rename pin', 'label') if res[1]: newName = self.getUniqPinName(res[0]) self._map[newName] = self._map.pop(p.name) p.setName(newName) renameAction.triggered.connect(OnRename) pinAffects(self.inExecPin, p) self._map[name] = p def contextMenuEvent(self, event): self.menu.exec_(event.screenPos()) @staticmethod def pinTypeHints(): return { 'inputs': [DataTypes.Exec, DataTypes.String], 'outputs': [DataTypes.Exec] } @staticmethod def category(): return 'FlowControl' @staticmethod def keywords(): return [] @staticmethod def description(): return 'Execute output depending on input string' def postCreate(self, jsonTemplate): Node.postCreate(self, jsonTemplate) # restore dynamically created outputs if len(jsonTemplate['outputs']) == 0: self.defaultPin = self.addOutputPin('Default', DataTypes.Exec) self.outString = self.addOutputPin('stringOut', DataTypes.String, hideLabel=True) self.addOutPin() self.addOutPin() else: for out in jsonTemplate['outputs']: PinWidgetBase.deserialize(self, out) def compute(self): string = self.inString.getData() self.outString.setData(string) if string in self._map: self._map[string].call() else: self.defaultPin.call()
class subgraphNode(Node): def __init__(self, name, graph): from ..Core import Widget as Widget super(subgraphNode, self).__init__(name, graph) self.menu = QMenu() self.actionExport = self.menu.addAction('export') self.actionExport.triggered.connect(self.export) self._graph = Widget.GraphWidget("graph", self.graph().parent) self._graph.outPinCreated.connect(self.createOutput) self._graph.inPinCreated.connect(self.createInput) self.dlg = MyDialog() self.styleSheetEditor = self.graph().styleSheetEditor self.dlg.setStyleSheet(self.styleSheetEditor.getStyleSheet()) self.dlg.setLayout(QtWidgets.QHBoxLayout()) self.dlg.layout().addWidget(self._graph) self._category = "CustomGraphs" self._keywords = "CustomGraphs" self._description = "Custom SubGraph" #self.bCallable = True self.dinOutputs = {} self.dinInputs = {} def createInput(self, pin): p = self.addInputPin(pin.name, DataTypes.Any, constraint="in%s" % pin.name) p.setType(pin) pin.nameChanged.connect(p.setName) pin.constraint = "in%s" % pin.name self._Constraints["in%s" % pin.name].append(pin) self._graph.inputsItem._Constraints["in%s" % pin.name] = [pin, p] pin.OnPinDeleted.connect(self.deletePort) pin.dataBeenSet.connect(p.setData) pinAffects(pin, p) #pinAffects(p,pin) #p.dataBeenSet.connect(pin.setData) self.dinInputs[pin] = p for i in self.inputs.values(): for o in self.outputs.values(): pinAffects(i, o) def createOutput(self, pin): p = self.addOutputPin(pin.name, DataTypes.Any, constraint="out%s" % pin.name) p.setType(pin) pin.nameChanged.connect(p.setName) pin.constraint = "out%s" % pin.name self._Constraints["out%s" % pin.name].append(pin) self._graph.outputsItem._Constraints["out%s" % pin.name] = [pin, p] pin.OnPinDeleted.connect(self.deletePort) pin.dataBeenSet.connect(p.setData) #p.dataBeenSet.connect(pin.setData) pinAffects(pin, p) #pinAffects(p,pin) self.dinOutputs[pin] = p for i in self.inputs.values(): for o in self.outputs.values(): pinAffects(i, o) def serialize(self): template = super(subgraphNode, self).serialize() graphData = self._graph.getGraphSaveData() template["graphData"] = graphData return template def export(self): from . import _nodeClasses from ..FunctionLibraries import _foos from ..SubGraphs import _subgraphClasses from .. import SubGraphs existing_nodes = [n for n in _nodeClasses] existing_nodes += [n for n in _foos] existing_nodes += [n for n in _subgraphClasses] graphData = self._graph.getGraphSaveData() graphData["Type"] = "subgraph" graphData["category"] = self._category graphData["keywords"] = self._keywords graphData["description"] = self._description name_filter = "Graph files (*.pySubgraph)" pth = QFileDialog.getSaveFileName(filter=name_filter) if not pth == '': file_path = pth path, name = os.path.split(file_path) name, ext = os.path.splitext(name) if name in existing_nodes: print("[ERROR] Node {0} already exists! Chose another name". format(name)) return # write to file. delete older if needed with open(file_path, "wb") as f: def to_serializable(val): return {"name": None} return str(val) json.dump(graphData, f, skipkeys=True, default=to_serializable, indent=2) reload(SubGraphs) SubGraphs._getClasses() def contextMenuEvent(self, event): self.menu.exec_(event.screenPos()) def postCreate(self, jsonTemplate): if "graphData" in jsonTemplate: self._graph.loadFromData(jsonTemplate["graphData"]) # restore pins for inp in self._graph.inputsItem.outputs.values(): self.createInput(inp) for out in self._graph.outputsItem.inputs.values(): self.createOutput(out) super(subgraphNode, self).postCreate(jsonTemplate) def deletePort(self, pin): if pin in self.dinInputs: self.dinInputs[pin].kill() del self.dinInputs[pin] elif pin in self.dinOutputs: self.dinOutputs[pin].kill() del self.dinOutputs[pin] @staticmethod def pinTypeHints(): ''' used by nodebox to suggest supported pins when drop wire from pin into empty space ''' return {'inputs': [], 'outputs': []} @staticmethod def category(): return 'Common' @staticmethod def keywords(): return [] @staticmethod def description(): ''' used by property view and node box widgets ''' return 'Encapsulate a graph inside a node' def mouseDoubleClickEvent(self, event): #Node.mouseDoubleClickEvent( event) self.OnDoubleClick(self.mapToScene(event.pos())) event.accept() def OnDoubleClick(self, pos): self.dlg.show() def compute(self): for key, value in self.dinInputs.iteritems(): key.setData(value.getData()) for key, value in self.dinOutputs.iteritems(): value.setData(key.getData())
class UINodeBase(QGraphicsWidget, IPropertiesViewSupport): """ Default node description """ # Event called when node name changes displayNameChanged = QtCore.Signal(str) def __init__(self, raw_node, w=80, color=Colors.NodeBackgrounds, headColorOverride=None): super(UINodeBase, self).__init__() self.setFlag(QGraphicsWidget.ItemIsMovable) self.setFlag(QGraphicsWidget.ItemIsFocusable) self.setFlag(QGraphicsWidget.ItemIsSelectable) self.setFlag(QGraphicsWidget.ItemSendsGeometryChanges) self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setAcceptHoverEvents(True) self.setZValue(NodeDefaults().Z_LAYER) self._rawNode = raw_node self._rawNode.setWrapper(self) self._rawNode.killed.connect(self.kill) self._rawNode.tick.connect(self.Tick) self.custom_widget_data = {} # node name self._displayName = self.name # GUI Layout self.opt_node_base_color = Colors.NodeBackgrounds self.opt_selected_pen_color = Colors.NodeSelectedPenColor self.opt_pen_selected_type = QtCore.Qt.SolidLine self._collapsed = False self._left_stretch = 0 self.color = color self.drawlabel = True self.headColorOverride = headColorOverride self.headColor = headColorOverride self._w = 0 self.h = 30 self.minWidth = 25 self.minHeight = self.h self._labelTextColor = QtCore.Qt.white self.drawLayoutsDebug = False self.nodeLayout = QGraphicsLinearLayout(QtCore.Qt.Vertical) self.nodeLayout.setContentsMargins(NodeDefaults().CONTENT_MARGINS, NodeDefaults().CONTENT_MARGINS, NodeDefaults().CONTENT_MARGINS, NodeDefaults().CONTENT_MARGINS) self.nodeLayout.setSpacing(NodeDefaults().LAYOUTS_SPACING) self.nodeNameFont = QtGui.QFont("Consolas") self.nodeNameFont.setPointSize(6) self.nodeTypeFont = QtGui.QFont("Consolas") self.nodeTypeFont.setPointSize(4) self.nodeTypeFont.setItalic(True) self.headerLayout = QGraphicsLinearLayout(QtCore.Qt.Horizontal) self.nodeLayout.addItem(self.headerLayout) self.nodeNameWidget = NodeName(self) self.headerLayout.addItem(self.nodeNameWidget) self.headerLayout.setContentsMargins(0, 0, 0, 0) self.headerLayout.setSpacing(3) self.nameActionsSpacer = QGraphicsWidget() self.nameActionsSpacer.setObjectName("nameActionsSpacer") self.nameActionsSpacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) self.headerLayout.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) self.headerLayout.addItem(self.nameActionsSpacer) self.headerLayout.setMaximumHeight(self.labelHeight) self.pinsLayout = QGraphicsLinearLayout(QtCore.Qt.Horizontal) self.pinsLayout.setContentsMargins(0, 0, 0, 0) self.pinsLayout.setSpacing(NodeDefaults().LAYOUTS_SPACING) self.nodeLayout.addItem(self.pinsLayout) self.nodeLayout.setStretchFactor(self.pinsLayout, 2) self.inputsLayout = QGraphicsLinearLayout(QtCore.Qt.Vertical) self.inputsLayout.setContentsMargins(0, 0, 0, 0) self.inputsLayout.setSpacing(NodeDefaults().LAYOUTS_SPACING) self.outputsLayout = QGraphicsLinearLayout(QtCore.Qt.Vertical) self.outputsLayout.setContentsMargins(0, 0, 0, 0) self.outputsLayout.setSpacing(NodeDefaults().LAYOUTS_SPACING) self.pinsLayout.addItem(self.inputsLayout) self.pinLayoutSpacer = QGraphicsWidget() self.pinLayoutSpacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.pinLayoutSpacer.setObjectName("pinLayoutSpacer") self.pinsLayout.addItem(self.pinLayoutSpacer) self.pinsLayout.addItem(self.outputsLayout) self.setLayout(self.nodeLayout) self.svgIcon = QtSvg.QGraphicsSvgItem(self) self.svgIcon.setPos(-6, -6) self._image = None self.canvasRef = None self._menu = QMenu() # Resizing Options self.initialRectWidth = self.minWidth self.initialRectHeight = self.minHeight self.expanded = True self.resizable = False self.bResize = False self.resizeDirection = (0, 0) self.resizeStripsSize = 2 self.resizeStrips = [0, 0, 0, 0, 0] # Left, Top, Right, Bottom, BottomRight self.lastMousePos = QtCore.QPointF() self.mousePressPos = QtCore.QPointF() # Hiding/Moving By Group/collapse/By Pin self.pressedCommentNode = None self.owningCommentNode = None self.edgesToHide = [] self.nodesNamesToMove = [] self.pinsToMove = {} self._rect = QtCore.QRectF(0, 0, self.minWidth, self.minHeight) # Group pins self.inputGroupPins = {} self.outputGroupPins = {} # Action buttons self._actionButtons = set() # Core nodes support self.isTemp = False self.isCommentNode = False self.propertyEditor = None # collapse action self.actionToggleCollapse = self._menu.addAction("ToggleCollapse") self.actionToggleCollapse.setToolTip( "Toggles node's body collapsed or not") self.actionToggleCollapse.triggered.connect(self.toggleCollapsed) self.actionToggleCollapse.setData( NodeActionButtonInfo(RESOURCES_DIR + "/nodeCollapse.svg", CollapseNodeActionButton)) def toggleCollapsed(self): self.collapsed = not self.collapsed def aboutToCollapse(self, futureCollapseState): """Called before collapsing or expanding.""" pass @property def collapsed(self): return self._collapsed @collapsed.setter def collapsed(self, bCollapsed): if bCollapsed != self._collapsed: self._collapsed = bCollapsed self.aboutToCollapse(self._collapsed) for i in range(0, self.inputsLayout.count()): inp = self.inputsLayout.itemAt(i) inp.setVisible(not bCollapsed) for o in range(0, self.outputsLayout.count()): out = self.outputsLayout.itemAt(o) out.setVisible(not bCollapsed) self.pinLayoutSpacer.setVisible(not bCollapsed) self.updateNodeShape() @property def image(self): return self._image @image.setter def image(self, value): self._image = value self.svgIcon.renderer().load(value) elementName = QtCore.QFileInfo(value).baseName() self.svgIcon.setElementId(elementName) # self.svgIcon.setPos(self.geometry().topRight()) def getImageDrawRect(self): topRight = self.boundingRect().topRight() topRight.setY(-12) topRight.setX(self.boundingRect().width() - 12) r = self.boundingRect() r.setWidth(24) r.setHeight(24) r.translate(topRight) return r @property def labelTextColor(self): return self._labelTextColor @labelTextColor.setter def labelTextColor(self, value): self._labelTextColor = value self.nodeNameWidget.setTextColor(self._labelTextColor) def __repr__(self): graphName = self._rawNode.graph( ).name if self._rawNode.graph is not None else str(None) return "<class[{0}]; name[{1}]; graph[{2}]>".format( self.__class__.__name__, self.getName(), graphName) def sizeHint(self, which, constraint): return QtCore.QSizeF(self.getNodeWidth(), self.getNodeHeight()) def setGeometry(self, rect): self.prepareGeometryChange() super(QGraphicsWidget, self).setGeometry(rect) self.setPos(rect.topLeft()) @property def uid(self): return self._rawNode._uid @uid.setter def uid(self, value): self._rawNode._uid = value @property def name(self): return self._rawNode.name @name.setter def name(self, value): self._rawNode.setName(value) @property def displayName(self): return self._displayName @displayName.setter def displayName(self, value): self._displayName = value self.displayNameChanged.emit(self._displayName) self.updateNodeShape() @property def pins(self): return self._rawNode.pins @property def UIPins(self): result = OrderedDict() for rawPin in self._rawNode.pins: uiPinRef = rawPin.getWrapper() if uiPinRef is not None: result[rawPin.uid] = uiPinRef() return result @property def UIinputs(self): result = OrderedDict() for rawPin in self._rawNode.pins: if rawPin.direction == PinDirection.Input: result[rawPin.uid] = rawPin.getWrapper()() return result @property def UIoutputs(self): result = OrderedDict() for rawPin in self._rawNode.pins: if rawPin.direction == PinDirection.Output: result[rawPin.uid] = rawPin.getWrapper()() return result @property def namePinOutputsMap(self): result = OrderedDict() for rawPin in self._rawNode.pins: if rawPin.direction == PinDirection.Output: result[rawPin.name] = rawPin.getWrapper()() return result @property def namePinInputsMap(self): result = OrderedDict() for rawPin in self._rawNode.pins: if rawPin.direction == PinDirection.Input: result[rawPin.name] = rawPin.getWrapper()() return result @property def w(self): return self._w @w.setter def w(self, value): self._w = value def getName(self): return self._rawNode.getName() def isRenamable(self): return False def setName(self, name): self._rawNode.setName(name) def getPin(self, name, pinsGroup=PinSelectionGroup.BothSides): pin = self._rawNode.getPin(str(name), pinsGroup) if pin is not None: if pin.getWrapper() is not None: return pin.getWrapper()() return None @staticmethod def removePinByName(node, name): pin = node.getPin(name) if pin: pin.kill() @staticmethod def recreate(node): templ = node.serialize() uid = node.uid node.kill() newNode = node.canvas.createNode(templ) newNode.uid = uid return newNode @property def isCompoundNode(self): return self._rawNode.isCompoundNode # TODO: add this to ui node interface def serializationHook(self): # this will be called by raw node # to gather ui specific info template = {} if self.resizable: template['resize'] = { 'w': self._rect.right(), 'h': self._rect.bottom() } template['displayName'] = self.displayName template['collapsed'] = self.collapsed template['headerHtml'] = self.nodeNameWidget.getHtml() return template def setHeaderHtml(self, html): self.nodeNameWidget.setHtml(html) def serialize(self): return self._rawNode.serialize() def onVisibilityChanged(self, bVisible): pass def itemChange(self, change, value): if change == QGraphicsItem.ItemPositionChange: self._rawNode.setPosition(value.x(), value.y()) if change == QGraphicsItem.ItemVisibleChange: if self.owningCommentNode is not None: if self.owningCommentNode.collapsed: self.onVisibilityChanged(False) else: self.onVisibilityChanged(bool(value)) return super(UINodeBase, self).itemChange(change, value) def isUnderActiveGraph(self): return self._rawNode.isUnderActiveGraph() def autoAffectPins(self): self._rawNode.autoAffectPins() def postCreate(self, jsonTemplate=None): # create ui pin wrappers for i in self._rawNode.getOrderedPins(): self._createUIPinWrapper(i) self.updateNodeShape() self.setPos(self._rawNode.x, self._rawNode.y) if self.canvasRef().graphManager.activeGraph() != self._rawNode.graph( ): self.hide() if not self.drawlabel: self.nodeNameWidget.hide() if self.headColorOverride is None: if self.isCallable(): self.headColor = NodeDefaults().CALLABLE_NODE_HEAD_COLOR else: self.headColor = NodeDefaults().PURE_NODE_HEAD_COLOR else: self.headColor = self.headColorOverride self.createActionButtons() headerHtml = self.name if jsonTemplate is not None: if "collapsed" in jsonTemplate["wrapper"]: self.collapsed = jsonTemplate["wrapper"]["collapsed"] if "headerHtml" in jsonTemplate["wrapper"]: headerHtml = jsonTemplate["wrapper"]["headerHtml"] self.setToolTip(self.description()) if self.resizable: w = self.getNodeWidth() h = self.getNodeHeight() if jsonTemplate is not None: if "resize" in jsonTemplate["wrapper"]: w = jsonTemplate["wrapper"]["resize"]["w"] h = jsonTemplate["wrapper"]["resize"]["h"] self._rect.setWidth(w) self._rect.setHeight(h) self.updateNodeShape() self.setHeaderHtml(headerHtml) def createActionButtons(self): # NOTE: actions with action button class specified will be added next to node name for action in self._menu.actions(): actionData = action.data() if isinstance(actionData, NodeActionButtonInfo): actionButtonClass = actionData.actionButtonClass() svgFilePath = actionData.filePath() if actionButtonClass is None: actionButtonClass = NodeActionButtonBase self.headerLayout.addItem( actionButtonClass(svgFilePath, action, self)) action.setVisible(False) def isCallable(self): return self._rawNode.isCallable() def category(self): return self._rawNode.category() def description(self): return self._rawNode.description() @property def packageName(self): return self._rawNode.packageName def getData(self, pinName): if pinName in [p.name for p in self.inputs.values()]: p = self.getPin(pinName, PinSelectionGroup.Inputs) return p.getData() def setData(self, pinName, data): if pinName in [p.name for p in self.outputs.values()]: p = self.getPin(pinName, PinSelectionGroup.Outputs) p.setData(data) @property def labelHeight(self): return self.nodeNameWidget.sizeHint(None, None).height() @property def labelWidth(self): headerWidth = self.nodeNameWidget.sizeHint(None, None).width() # actions width. 10 is svg icon size, probably need to move this value to some preferences numActions = len(self._actionButtons) headerWidth += numActions * 10 headerWidth += numActions * self.headerLayout.spacing() if self.collapsed and not self.resizable: headerWidth += self.nameActionsSpacer.boundingRect().width() headerWidth += self.headerLayout.spacing( ) + NodeDefaults().CONTENT_MARGINS * 2 return headerWidth def getNodeWidth(self): width = self.getPinsWidth() + self.pinsLayout.spacing() * 2 if self.resizable: width = max(self._rect.width(), width) width = max(width, self.labelWidth) return width def getNodeHeight(self): h = self.nodeNameWidget.sizeHint(None, None).height() h += self.nodeLayout.spacing() try: numInputs = len(self.UIinputs) numOutputs = len(self.UIoutputs) pins = self.UIinputs.values( ) if numInputs > numOutputs else self.UIoutputs.values() h += NodeDefaults().CONTENT_MARGINS * 2 for pin in pins: if pin.isVisible(): h += pin.sizeHint( None, None).height() + NodeDefaults().LAYOUTS_SPACING except: pass if h < self.minHeight: h = self.minHeight if self.resizable: h = max(self._rect.height(), h) if self.collapsed: h = max(self.minHeight, self.labelHeight) return h def getPinsWidth(self): iwidth = 0 owidth = 0 pinwidth = 0 pinwidth2 = 0 for i in self.UIPins.values(): if i.direction == PinDirection.Input: iwidth = max(iwidth, i.sizeHint(None, None).width()) # pinwidth = max(pinwidth, i.width) else: owidth = max(owidth, i.sizeHint(None, None).width()) # pinwidth2 = max(pinwidth2, i.width) return iwidth + owidth + pinwidth + pinwidth2 + Spacings.kPinOffset def invalidateNodeLayouts(self): self.inputsLayout.invalidate() self.outputsLayout.invalidate() self.pinsLayout.invalidate() self.headerLayout.invalidate() self.nodeLayout.invalidate() def updateNodeShape(self): self.prepareGeometryChange() self.invalidateNodeLayouts() self.updateGeometry() self.update() self.canvasRef().update() def onChangeColor(self, label=False): res = QColorDialog.getColor(self.color, None, 'Node color setup') if res.isValid(): res.setAlpha(80) self.color = res if label: self.update() def isUnderCollapsedComment(self): if self.owningCommentNode is None: return False else: if self.owningCommentNode.collapsed: return True parent = self.owningCommentNode.owningCommentNode while parent is not None: upperComment = parent if upperComment.collapsed: return True parent = upperComment.owningCommentNode return False def getTopMostOwningCollapsedComment(self): """Returns top most owning comment. If bCollapsed=True, it will stop when first collapsed comment is found. """ if self.owningCommentNode is None: return None # build chain of comments collapse states topMostComment = self.owningCommentNode parent = topMostComment.owningCommentNode chain = OrderedDict() chain[topMostComment] = topMostComment.collapsed while parent is not None: topMostComment = parent chain[topMostComment] = topMostComment.collapsed parent = topMostComment.owningCommentNode last = None for comment, collapsed in chain.items(): if not comment.isVisible(): continue if last is not None: if collapsed + last.collapsed == 1: topMostComment = last break last = comment else: last = comment return topMostComment def updateOwningCommentNode(self): if self.owningCommentNode is not None and self.owningCommentNode.collapsed: return collidingItems = self.collidingItems(QtCore.Qt.ContainsItemShape) collidingNodes = set() for item in collidingItems: if item.sceneBoundingRect().contains( self.sceneBoundingRect()) and isinstance(item, UINodeBase): if item.isCommentNode: collidingNodes.add(item) owningCommentNode = None if len(collidingNodes) == 1: owningCommentNode = list(collidingNodes)[0] elif len(collidingNodes) > 1: # find smallest rect smallest = list(collidingNodes)[0] for commentNode in collidingNodes: s1 = smallest.boundingRect().size() s2 = commentNode.boundingRect().size() if s1.width() > s2.width() and s1.height() > s2.height(): smallest = commentNode if self in commentNode.owningNodes: commentNode.owningNodes.remove(self) owningCommentNode = smallest self.owningCommentNode = owningCommentNode if self.owningCommentNode is not None: if owningCommentNode._rawNode.graph() == self.canvasRef( ).graphManager.activeGraph(): self.owningCommentNode.owningNodes.add(self) def getCollidedNodes(self, bFullyCollided=True, classNameFilters=set()): collidingItems = self.collidingItems() collidingNodes = set() for item in collidingItems: node = item.topLevelItem() if bFullyCollided: if self.sceneBoundingRect().contains(node.sceneBoundingRect()): if node is not self and isinstance(node, UINodeBase): if classNameFilters: if node.__class__.__name__ not in classNameFilters: continue if node._rawNode.graph() != self.canvasRef( ).graphManager.activeGraph(): continue collidingNodes.add(node) else: if node is not self and isinstance(node, UINodeBase): if classNameFilters: if node.__class__.__name__ not in classNameFilters: continue if node._rawNode.graph() != self.canvasRef( ).graphManager.activeGraph(): continue collidingNodes.add(node) return collidingNodes def translate(self, x, y): super(UINodeBase, self).moveBy(x, y) def paint(self, painter, option, widget): NodePainter.default(self, painter, option, widget) if self.drawLayoutsDebug: painter.drawRect(self.headerLayout.geometry()) painter.drawRect(self.nodeLayout.geometry()) painter.drawRect(self.inputsLayout.geometry()) painter.drawRect(self.outputsLayout.geometry()) def shouldResize(self, cursorPos): result = {"resize": False, "direction": self.resizeDirection} if self.resizeStrips[0] == 1: result["resize"] = True result["direction"] = (-1, 0) if self.resizeStrips[1] == 1: result["resize"] = True result["direction"] = (0, -1) if self.resizeStrips[2] == 1: result["resize"] = True result["direction"] = (1, 0) elif self.resizeStrips[3] == 1: result["resize"] = True result["direction"] = (0, 1) elif self.resizeStrips[4] == 1: result["resize"] = True result["direction"] = (1, 1) return result def contextMenuEvent(self, event): self._menu.exec_(event.screenPos()) def mousePressEvent(self, event): self.update() self.mousePressPos = event.pos() self.pressedCommentNode = self.owningCommentNode super(UINodeBase, self).mousePressEvent(event) self.mousePressPos = event.scenePos() self.origPos = self.pos() self.initPos = self.pos() self.initialRect = self.boundingRect() if self.expanded and self.resizable: resizeOpts = self.shouldResize(self.mapToScene(event.pos())) if resizeOpts["resize"]: self.resizeDirection = resizeOpts["direction"] self.initialRectWidth = self.initialRect.width() self.initialRectHeight = self.initialRect.height() self.setFlag(QGraphicsItem.ItemIsMovable, False) self.bResize = True def mouseMoveEvent(self, event): super(UINodeBase, self).mouseMoveEvent(event) # resize if self.bResize: delta = event.scenePos() - self.mousePressPos if self.resizeDirection == (1, 0): # right connection resize newWidth = delta.x() + self.initialRectWidth if newWidth > self.minWidth: self._rect.setWidth(newWidth) self.w = newWidth self.updateNodeShape() elif self.resizeDirection == (0, 1): newHeight = delta.y() + self.initialRectHeight if newHeight > self.minHeight: self._rect.setHeight(newHeight) self.updateNodeShape() elif self.resizeDirection == (-1, 0): # left connection resize posdelta = self.mapToScene(event.pos()) - self.origPos posdelta2 = self.mapToScene(event.pos()) - self.initPos newWidth = -posdelta2.x() + self.initialRectWidth if newWidth > self.minWidth: self.translate(posdelta.x(), 0) self.origPos = self.pos() self._rect.setWidth(newWidth) self.updateNodeShape() elif self.resizeDirection == (1, 1): newWidth = delta.x() + self.initialRectWidth newHeight = delta.y() + self.initialRectHeight if newWidth > self.minWidth: self._rect.setWidth(newWidth) self.w = newWidth self.updateNodeShape() if newHeight > self.minHeight: self._rect.setHeight(newHeight) self.updateNodeShape() self.update() self.lastMousePos = event.pos() def mouseReleaseEvent(self, event): self.bResize = False self.update() self.updateOwningCommentNode() if self.owningCommentNode != self.pressedCommentNode: if self.pressedCommentNode is not None: if self in self.pressedCommentNode.owningNodes: self.pressedCommentNode.owningNodes.remove(self) super(UINodeBase, self).mouseReleaseEvent(event) def clone(self): templ = self.serialize() templ['name'] = self.name templ['uuid'] = str(uuid.uuid4()) for inp in templ['inputs']: inp['uuid'] = str(uuid.uuid4()) for out in templ['outputs']: out['uuid'] = str(uuid.uuid4()) new_node = self.canvasRef().createNode(templ) return new_node def call(self, name): self._rawNode.call(name) def createPropertiesWidget(self, propertiesWidget): self.propertyEditor = weakref.ref(propertiesWidget) baseCategory = CollapsibleFormWidget(headName="Base") le_name = QLineEdit(self.getName()) le_name.setReadOnly(True) baseCategory.addWidget("Name", le_name) leUid = QLineEdit(str(self._rawNode.graph().name)) leUid.setReadOnly(True) baseCategory.addWidget("Owning graph", leUid) text = "{0}".format(self.packageName) if self._rawNode.lib: text += " | {0}".format(self._rawNode.lib) text += " | {0}".format(self._rawNode.__class__.__name__) leType = QLineEdit(text) leType.setReadOnly(True) baseCategory.addWidget("Type", leType) self.propertyEditor().addWidget(baseCategory) self.createInputWidgets(self.propertyEditor()) Info = CollapsibleFormWidget(headName="Info", collapsed=True) doc = QTextBrowser() doc.setOpenExternalLinks(True) doc.setHtml(self.description()) Info.addWidget(widget=doc) self.propertyEditor().addWidget(Info) def createInputWidgets(self, propertiesWidget): # inputs if len([i for i in self.UIinputs.values()]) != 0: inputsCategory = CollapsibleFormWidget(headName="Inputs") sortedInputs = sorted(self.UIinputs.values(), key=lambda x: x.name) for inp in sortedInputs: if inp.isArray(): # TODO: create list input widget continue dataSetter = inp.call if inp.isExec() else inp.setData w = createInputWidget(inp.dataType, dataSetter, inp.defaultValue()) if w: inp.dataBeenSet.connect(w.setWidgetValueNoSignals) w.blockWidgetSignals(True) w.setWidgetValue(inp.currentData()) w.blockWidgetSignals(False) w.setObjectName(inp.getName()) inputsCategory.addWidget(inp.name, w) if inp.hasConnections(): w.setEnabled(False) propertiesWidget.addWidget(inputsCategory) return inputsCategory def getChainedNodes(self): nodes = [] for pin in self.UIinputs.values(): for connection in pin.connections: node = connection.source().topLevelItem() # topLevelItem nodes.append(node) nodes += node.getChainedNodes() return nodes def kill(self, *args, **kwargs): scene = self.scene() if scene is not None: self.scene().removeItem(self) del (self) def collidesWithCommentNode(self): nodes = self.getCollidedNodes() result = None for n in nodes: if n.isCommentNode: result = n break return result def handleVisibility(self): if self._rawNode.graph() != self.canvasRef().graphManager.activeGraph( ): # if current graph != node's graph - hide node and connections self.hide() for uiPin in self.UIPins.values(): for connection in uiPin.uiConnectionList: connection.hide() else: # if current graph == node's graph - show it only if its not under collapsed comment node collidedCommentNode = self.collidesWithCommentNode() if collidedCommentNode is None: self.show() else: if collidedCommentNode.collapsed: self.hide() else: self.show() def hoverLeaveEvent(self, event): self.resizeStrips = [0, 0, 0, 0, 0] self.update() def hoverMoveEvent(self, event): if self.resizable and not self.collapsed: height = self.geometry().height() width = self.geometry().width() rf = NodeDefaults().CORNERS_ROUND_FACTOR leftStrip = QtCore.QRectF(0, rf, self.resizeStripsSize, height - rf * 2) topStrip = QtCore.QRectF(rf, 0, width - rf * 2, self.resizeStripsSize) rightStrip = QtCore.QRectF(width - self.resizeStripsSize, rf, self.resizeStripsSize, height - rf * 2) bottomStrip = QtCore.QRectF(rf, height - self.resizeStripsSize, width - rf * 2, self.resizeStripsSize) bottomRightStrip = QtCore.QRectF(width - rf, height - rf, rf, rf) # detect where on the node self.resizeStrips[0] = 1 if leftStrip.contains(event.pos()) else 0 self.resizeStrips[1] = 1 if topStrip.contains(event.pos()) else 0 self.resizeStrips[2] = 1 if rightStrip.contains(event.pos()) else 0 self.resizeStrips[3] = 1 if bottomStrip.contains( event.pos()) else 0 self.resizeStrips[4] = 1 if bottomRightStrip.contains( event.pos()) else 0 self.update() def Tick(self, delta, *args, **kwargs): # NOTE: Do not call wrapped raw node Tick method here! # this ui node tick called from underlined raw node's emitted signal # do here only UI stuff # self.handleVisibility() pass def addGroupContainer(self, portType, groupName="group"): container = QGraphicsWidget() container.setObjectName('{0}PinGroupContainerWidget'.format(self.name)) lyt = QGraphicsLinearLayout() lyt.setOrientation(QtCore.Qt.Vertical) lyt.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) lyt.setContentsMargins(1, 1, 1, 1) container.group_name = EditableLabel(name=groupName, node=self, canvas=self.canvasRef()) font = QtGui.QFont('Consolas') font.setBold(True) font.setPointSize(500) container.group_name._font = font container.group_name.nameLabel.setFont(font) container.group_name.nameLabel.update() container.group_name.setObjectName('{0}_GroupConnector'.format( container.group_name)) container.group_name.setContentsMargins(0, 0, 0, 0) container.group_name.setColor(Colors.AbsoluteBlack) grpCon = self.addContainer() container.groupIcon = UIGroupPinBase(container) lyt.addItem(grpCon) container.setLayout(lyt) if portType == PinDirection.Input: container.group_name.nameLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) grpCon.layout().addItem(container.groupIcon) grpCon.layout().addItem(container.group_name) else: container.group_name.nameLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTop) grpCon.layout().addItem(container.group_name) grpCon.layout().addItem(container.groupIcon) return container def addContainer(self): container = QGraphicsWidget() container.setObjectName('{0}PinContainerWidget'.format(self.name)) container.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum) container.sizeHint(QtCore.Qt.MinimumSize, QtCore.QSizeF(50.0, 10.0)) lyt = QGraphicsLinearLayout() lyt.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) lyt.setContentsMargins(1, 1, 1, 1) container.setLayout(lyt) return container def _createUIPinWrapper(self, rawPin, index=-1, group=None, linkedPin=None): wrapper = rawPin.getWrapper() if wrapper is not None: return wrapper() p = getUIPinInstance(self, rawPin) p.call = rawPin.call name = rawPin.name lblName = name if rawPin.direction == PinDirection.Input: self.inputsLayout.addItem(p) self.inputsLayout.setAlignment(p, QtCore.Qt.AlignLeft) elif rawPin.direction == PinDirection.Output: self.outputsLayout.addItem(p) self.outputsLayout.setAlignment(p, QtCore.Qt.AlignRight) p.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.update() # self.nodeMainGWidget.update() self.updateNodeShape() p.syncDynamic() p.syncRenamable() if self.collapsed: p.hide() return p def collapsePinGroup(self, container): for i in range(1, container.layout().count()): item = container.layout().itemAt(i) pin = item.layout().itemAt(0) if isinstance( item.layout().itemAt(0), UIPinBase) else item.layout().itemAt(1) if pin.hasConnections: if pin.direction == PinDirection.Input: for ege in pin.connections: ege.drawDestination = container.layout().itemAt( 0).layout().itemAt(0) if pin.direction == PinDirection.Output: for ege in pin.connections: ege.drawSource = container.layout().itemAt( 0).layout().itemAt(1) item.hide() def expandPinGroup(self, container): for i in range(1, container.layout().count()): item = container.layout().itemAt(i) pin = item.layout().itemAt(0) if isinstance( item.layout().itemAt(0), UIPinBase) else item.layout().itemAt(1) if pin.hasConnections: if pin.direction == PinDirection.Input: for ege in pin.connections: ege.drawDestination = pin if pin.direction == PinDirection.Output: for ege in pin.connections: ege.drawSource = pin item.show()
class UIPinBase(QGraphicsWidget): """UI pin wrapper. """ # Event called when pin is connected OnPinConnected = QtCore.Signal(object) # Event called when pin is disconnected OnPinDisconnected = QtCore.Signal(object) # Event called when data been set dataBeenSet = QtCore.Signal(object) # Event called when pin name changes displayNameChanged = QtCore.Signal(str) OnPinChanged = QtCore.Signal(object) OnPinDeleted = QtCore.Signal(object) def __init__(self, owningNode, raw_pin): """UI wrapper for :class:`PyFlow.Core.PinBase` :param owningNode: Owning node :type owningNode: :class:`PyFlow.UI.Canvas.NodeBase` :param raw_pin: PinBase reference :type raw_pin: :class:`PyFlow.Core.PinBase` """ super(UIPinBase, self).__init__() self.setGraphicsItem(self) self.setFlag(QGraphicsWidget.ItemSendsGeometryChanges) self.setCacheMode(self.DeviceCoordinateCache) self.setAcceptHoverEvents(True) self.setZValue(1) self.setParentItem(owningNode) self.UiNode = weakref.ref(owningNode) self._rawPin = raw_pin self.watchWidget = None if self._rawPin is not None: self._rawPin.serializationHook.connect(self.serializationHook) self._rawPin.containerTypeChanged.connect( self.onContainerTypeChanged) self._displayName = self._rawPin.name self._rawPin.setWrapper(self) self._rawPin.killed.connect(self.kill) self._rawPin.nameChanged.connect(self.setDisplayName) # Context menu for pin self.menu = QMenu() self.menu.addAction("Rename").triggered.connect(self.onRename) self.menu.addAction("Remove").triggered.connect(self._rawPin.kill) self.actionDisconnect = self.menu.addAction('Disconnect all') self.actionDisconnect.triggered.connect(self._rawPin.disconnectAll) self.actionResetValue = self.menu.addAction("Reset value") self.actionResetValue.triggered.connect(self.resetToDefault) if self._rawPin._structure == PinStructure.Multi: self.menu.addAction("changeStructure").triggered.connect( self.selectStructure) self.actionWatchValue = self.menu.addAction("Watch") self.actionWatchValue.triggered.connect(self.toggleWatchValue) self._rawPin.dataBeenSet.connect(self.updateWatchWidgetValue) self.actionCopyPath = self.menu.addAction("Copy path") self.actionCopyPath.triggered.connect(self.onCopyPathToClipboard) # GUI self._font = QtGui.QFont("Consolas") self._font.setPointSize(6) self.pinSize = 6 self.hovered = False self.bLabelHidden = False if self._rawPin is not None: self._pinColor = QtGui.QColor(*self._rawPin.color()) self._labelColor = QtCore.Qt.white self._execPen = QtGui.QPen(Colors.White, 0.5, QtCore.Qt.SolidLine) self._dirty_pen = QtGui.QPen(Colors.DirtyPen, 0.5, QtCore.Qt.DashLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin) self.uiConnectionList = [] self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) self.pinCircleDrawOffset = QtCore.QPointF() # TODO: This is check is for PinGroup. Improve it if self._rawPin is not None: self.setToolTip(self._rawPin.description) def onCopyPathToClipboard(self): QApplication.clipboard().clear() QApplication.clipboard().setText(self.path()) def toggleWatchValue(self): if self.watchWidget is not None: self.scene().removeItem(self.watchWidget) self.watchWidget = None else: scene = self.owningNode().canvasRef().scene() self.watchWidget = WatchItem() scene.addItem(self.watchWidget) self.watchWidget.setZValue(NodeDefaults().Z_LAYER + 1) self.updateWatchWidgetPosition() self.updateWatchWidgetValue(self.currentData()) def path(self): return self._rawPin.path() def updateWatchWidgetPosition(self): if self.watchWidget is not None: scenePos = self.sceneBoundingRect().bottomLeft( ) if self.direction == PinDirection.Input else self.sceneBoundingRect( ).bottomRight() self.watchWidget.setPos(scenePos) def updateWatchWidgetValue(self, *args, **kwargs): if self.watchWidget is not None: content = "Value: {0}".format(str(self.currentData())) if self.isAny: content += "\nActive data type: {0}".format( self._rawPin.activeDataType) content += "\nSuper: {0}".format(self._rawPin.super) self.watchWidget.setPlainText(content) self.updateWatchWidgetPosition() def heartBeat(self): self.updateWatchWidgetPosition() def getInputWidgetVariant(self): return self._rawPin.getInputWidgetVariant() @property def labelColor(self): return self._labelColor @labelColor.setter def labelColor(self, value): self._labelColor = value def pinCenter(self): """Point relative to pin widget, where circle is drawn.""" frame = QtCore.QRectF(QtCore.QPointF(0, 0), self.geometry().size()) halfPinSize = self.pinSize / 2 pinX = self.pinSize pinY = (frame.height() / 2) if not self.bLabelHidden: if self.direction == PinDirection.Output: pinX = frame.width() - self.pinSize + halfPinSize result = QtCore.QPointF(pinX, pinY) if self.owningNode().collapsed: labelHeight = self.owningNode().labelHeight #labelHeight += self.owningNode().nodeLayout.spacing() if self.direction == PinDirection.Input: result = self.mapFromItem(self.owningNode(), QtCore.QPointF(0, labelHeight)) if self.direction == PinDirection.Output: result = self.mapFromItem( self.owningNode(), QtCore.QPointF( self.owningNode().sizeHint(None, None).width(), labelHeight)) return result def onContainerTypeChanged(self, *args, **kwargs): # underlined pin is changed to list or dict # update to redraw shape self.update() def setLabel(self, labelItem): if self._label is None: self._label = weakref.ref(labelItem) def displayName(self): return self._displayName def setDisplayName(self, displayName): if displayName != self._displayName: self._displayName = displayName self.displayNameChanged.emit(self._displayName) self.prepareGeometryChange() self.updateGeometry() self.update() def jsonEncoderClass(self): return self._rawPin.jsonEncoderClass() def jsonDecoderClass(self): return self._rawPin.jsonDecoderClass() @property def owningNode(self): return self.UiNode @property def constraint(self): return self._rawPin.constraint @property def isAny(self): return self._rawPin.isAny() def setMenuItemEnabled(self, actionName, bEnabled): for action in self.menu.actions(): if action.text() == actionName: if bEnabled != action.isEnabled() and action.isVisible(): action.setEnabled(bEnabled) action.setVisible(bEnabled) def syncRenamable(self): renamingEnabled = self._rawPin.optionEnabled( PinOptions.RenamingEnabled) # self._label()._isEditable = renamingEnabled self.setMenuItemEnabled("Rename", renamingEnabled) def onRename(self): name, confirmed = QInputDialog.getText(None, "Rename", "Enter new pin name") if confirmed and name != self.name and name != "": uniqueName = self._rawPin.owningNode().getUniqPinName(name) self.setName(uniqueName) self.setDisplayName(uniqueName) self.owningNode().invalidateNodeLayouts() self.owningNode().updateNodeShape() def syncDynamic(self): self.setMenuItemEnabled("Remove", self._rawPin.optionEnabled(PinOptions.Dynamic)) @property def structureType(self): return self._rawPin.structureType @property def dirty(self): return self._rawPin.dirty @dirty.setter def dirty(self, value): self._rawPin.dirty = value def resetToDefault(self): self.setData(self.defaultValue()) def defaultValue(self): return self._rawPin.defaultValue() def currentData(self): return self._rawPin.currentData() @property def name(self): return self._rawPin.name def getFullName(self): return self._rawPin.getFullName() def hasConnections(self): return self._rawPin.hasConnections() def setClean(self): self._rawPin.setClean() def setDirty(self): self._rawPin.setDirty() @property def _data(self): return self._rawPin._data @_data.setter def _data(self, value): self._rawPin._data = value @property def affects(self): return self._rawPin.affects @property def direction(self): return self._rawPin.direction @property def affected_by(self): return self._rawPin.affected_by def supportedDataTypes(self): return self._rawPin.supportedDataTypes() @property def connections(self): return self.uiConnectionList @property def uid(self): return self._rawPin._uid @uid.setter def uid(self, value): self._rawPin._uid = value def color(self): return self._pinColor def setName(self, newName, force=False): return self._rawPin.setName(newName, force=force) def setData(self, value): self._rawPin.setData(value) self.dataBeenSet.emit(value) def getData(self): return self._rawPin.getData() def call(self): self._rawPin.call() def kill(self, *args, **kwargs): """this will be called after raw pin is deleted """ scene = self.scene() if scene is None: del self return if self._rawPin.direction == PinDirection.Input: self.owningNode().inputsLayout.removeItem(self) else: self.owningNode().outputsLayout.removeItem(self) self.OnPinDeleted.emit(self) try: scene = self.scene() if scene is None: del self return scene.removeItem(self) self.owningNode().updateNodeShape() except: pass def assignRawPin(self, rawPin): if rawPin is not self._rawPin: self._rawPin = rawPin self.call = rawPin.call self._rawPin.setWrapper(self) self._pinColor = QtGui.QColor(*self._rawPin.color()) def serializationHook(self, *args, **kwargs): data = {} data['bLabelHidden'] = self.bLabelHidden data['displayName'] = self.displayName() return data def serialize(self): return self._rawPin.serialize() def getContainer(self): return self._container def isExec(self): return self._rawPin.isExec() @property def dataType(self): return self._rawPin.dataType def sizeHint(self, which, constraint): height = QtGui.QFontMetrics(self._font).height() width = self.pinSize * 2 if not self.bLabelHidden: width += QtGui.QFontMetrics(self._font).width(self.displayName()) return QtCore.QSizeF(width, height) def shape(self): path = QtGui.QPainterPath() path.addEllipse(self.boundingRect()) return path def isArray(self): return self._rawPin.isArray() def isDict(self): return self._rawPin.isDict() def paint(self, painter, option, widget): if self.isArray(): PinPainter.asArrayPin(self, painter, option, widget) elif self.isDict(): PinPainter.asDictPin(self, painter, option, widget) else: PinPainter.asValuePin(self, painter, option, widget) def contextMenuEvent(self, event): self.menu.exec_(event.screenPos()) def getLayout(self): if self.direction == PinDirection.Input: return self.owningNode().inputsLayout else: return self.owningNode().outputsLayout @property def description(self): return self._rawPin.description @description.setter def description(self, value): self._rawPin.description = value self.setToolTip(self._rawPin.description) def hoverEnterEvent(self, event): super(UIPinBase, self).hoverEnterEvent(event) self.update() self.hovered = True event.accept() def hoverLeaveEvent(self, event): super(UIPinBase, self).hoverLeaveEvent(event) self.update() self.hovered = False def pinConnected(self, other): self.OnPinConnected.emit(other) self.update() def pinDisconnected(self, other): self.OnPinDisconnected.emit(other) self.update() def selectStructure(self): item, ok = QInputDialog.getItem(None, "", "", ([i.name for i in list(PinStructure)]), 0, False) if ok and item: self._rawPin.changeStructure(PinStructure[item], True)
class pythonNode(Node, NodeBase): def __init__(self, name, graph): super(pythonNode, self).__init__(name, graph) self.menu = QMenu() self.actionEdit = self.menu.addAction('edit') self.actionEdit.triggered.connect(self.openEditor) self.actionEdit.setIcon(QtGui.QIcon(':/icons/resources/py.png')) self.actionExport = self.menu.addAction('export') self.actionExport.triggered.connect(self.export) self.editorUUID = None self.bKillEditor = True self.label().icon = QtGui.QImage(':/icons/resources/py.png') self.currentComputeCode = Node.jsonTemplate()['computeCode'] self.color = Colors.NodeNameRect @staticmethod def pinTypeHints(): return {'inputs': [], 'outputs': []} def computeCode(self): return self.currentComputeCode def openEditor(self): self.editorUUID = uuid.uuid4() self.graph().codeEditors[self.editorUUID] = WCodeEditor( self.graph(), self, self.editorUUID) self.graph().codeEditors[self.editorUUID].show() def kill(self): if self.editorUUID in self.graph().codeEditors: ed = self.graph().codeEditors.pop(self.editorUUID) ed.deleteLater() Node.kill(self) @staticmethod def category(): return 'Utils' def postCreate(self, jsonTemplate): # restore compute self.currentComputeCode = jsonTemplate['computeCode'] foo = WCodeEditor.wrapCodeToFunction('compute', jsonTemplate['computeCode']) exec(foo) self.compute = MethodType(compute, self, Node) # restore pins for inpJson in jsonTemplate['inputs']: pin = None if inpJson['dataType'] == DataTypes.Exec: pin = self.addInputPin(inpJson['name'], inpJson['dataType'], self.compute, inpJson['bLabelHidden']) pin.uid = uuid.UUID(inpJson['uuid']) else: pin = self.addInputPin(inpJson['name'], inpJson['dataType'], None, inpJson['bLabelHidden']) pin.uid = uuid.UUID(inpJson['uuid']) pin.setData(inpJson['value']) for outJson in jsonTemplate['outputs']: pin = self.addOutputPin(outJson['name'], outJson['dataType'], None, outJson['bLabelHidden']) pin.uid = uuid.UUID(outJson['uuid']) pin.setData(outJson['value']) self.bCallable = self.isCallable() Node.postCreate(self, jsonTemplate) # restore node label self.label().setPlainText(jsonTemplate['meta']['label']) def wrapCodeToFunction(self, fooName, code): foo = "def {}(self):".format(fooName) lines = [i for i in code.split('\n') if len(i) > 0] for line in lines: foo += '\n\t\t{}'.format(line) return foo def export(self): # restore compute #print self.currentComputeCode foo = self.wrapCodeToFunction('compute', self.currentComputeCode) inputs = [] portString = "" for obj in self.inputs.values(): if obj.dataType == DataTypes.Exec: comp = "self.compute" else: comp = "None" portString += """\n self.{0} = self.addInputPin("{0}", {1},{2},hideLabel={3})""".format( obj.name, str(obj.dataType), comp, obj.bLabelHidden) for obj in self.outputs.values(): comp = "None" portString += """\n self.{0} = self.addOutputPin("{0}", {1},hideLabel={2})""".format( obj.name, str(obj.dataType), obj.bLabelHidden) self._implementPlugin(self.name, portString, foo) def contextMenuEvent(self, event): self.menu.exec_(event.screenPos()) def mouseDoubleClickEvent(self, event): #Node.mouseDoubleClickEvent( event) self.OnDoubleClick(self.mapToScene(event.pos())) event.accept() def OnDoubleClick(self, pos): self.openEditor() @staticmethod def keywords(): return ['Code', 'Expression'] @staticmethod def description(): return 'default description' def _implementPlugin(self, name, ports, computeCode): from . import _nodeClasses from ..FunctionLibraries import _foos from ..SubGraphs import _subgraphClasses from .. import SubGraphs existing_nodes = [n for n in _nodeClasses] existing_nodes += [n for n in _foos] existing_nodes += [n for n in _subgraphClasses] from .. import Nodes #file_path = "{0}/{1}.py".format(os.path.dirname(__file__), name) #existing_nodes = [n.split(".")[0] for n in os.listdir(os.path.dirname(__file__)) if n.endswith(".py") and "__init__" not in n] name_filter = "Node Files (*.py)" pth = QFileDialog.getSaveFileName(filter=name_filter) if not pth == '': if type(pth) in [tuple, list]: file_path = pth[0] else: file_path = pth path, name = os.path.split(file_path) name, ext = os.path.splitext(name) if name in existing_nodes: print("[ERROR] Node {0} already exists! Chose another name". format(name)) return NodeTemplate = """from ..Core.AbstractGraph import * from ..Core.Settings import * from ..Core import Node class {0}(Node): def __init__(self, name, graph): super({0}, self).__init__(name, graph){1} for i in self.inputs.values(): for o in self.outputs.values(): pinAffects(i, o) @staticmethod def pinTypeHints(): ''' used by nodebox to suggest supported pins when drop wire from pin into empty space ''' return {{'inputs': [DataTypes.Bool], 'outputs': [DataTypes.Bool]}} @staticmethod def category(): ''' used by nodebox to place in tree to make nested one - use '|' like this ( 'CatName|SubCatName' ) ''' return 'Common' @staticmethod def keywords(): ''' used by nodebox filter while typing ''' return [] @staticmethod def description(): ''' used by property view and node box widgets ''' return 'default description' {2} """.format(name, ports, computeCode) # write to file. delete older if needed with open(file_path, "wb") as f: f.write(NodeTemplate) print( "[INFO] Node {0} been created.\nIn order to appear in node box, restart application." .format(name)) if OS_PLATFORM == 'Windows': os.system(file_path) else: os.system(file_path) reload(Nodes) Nodes._getClasses()
class UIConnection(QGraphicsPathItem): """UIConnection is a cubic spline curve. It represents connection between two pins. """ def __init__(self, source, destination, canvas): QGraphicsPathItem.__init__(self) self.setAcceptedMouseButtons(QtCore.Qt.LeftButton) self.setAcceptHoverEvents(True) self.setFlag(QGraphicsPathItem.ItemIsSelectable) self._menu = QMenu() self.actionDisconnect = self._menu.addAction("Disconnect") self.actionDisconnect.triggered.connect(self.kill) self._uid = uuid4() self.canvasRef = weakref.ref(canvas) self.source = weakref.ref(source) self.destination = weakref.ref(destination) self.drawSource = self.source() self.drawDestination = self.destination() # Overrides for getting endpoints positions # if None - pin centers will be used self.sourcePositionOverride = None self.destinationPositionOverride = None self.mPath = QtGui.QPainterPath() self.cp1 = QtCore.QPointF(0.0, 0.0) self.cp2 = QtCore.QPointF(0.0, 0.0) self.setZValue(NodeDefaults().Z_LAYER - 1) self.color = self.source().color() self.selectedColor = self.color.lighter(150) self.thickness = 1 self.thicknessMultiplier = 1 if source.isExec(): self.thickness = 2 self.pen = QtGui.QPen(self.color, self.thickness, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin) points = self.getEndPoints() self.updateCurve(points[0], points[1]) self.setPen(self.pen) self.source().update() self.destination().update() self.fade = 0.0 self.source().uiConnectionList.append(self) self.destination().uiConnectionList.append(self) self.source().pinConnected(self.destination()) self.destination().pinConnected(self.source()) self.prevPos = None self.linPath = None self.hOffsetL = 0.0 self.hOffsetR = 0.0 self.hOffsetLSShape = 0.0 self.hOffsetRSShape = 0.0 self.vOffset = 0.0 self.vOffsetSShape = 0.0 self.offsetting = 0 self.snapVToFirst = True self.snapVToSecond = False self.sShape = False self.sameSide = 0 self.hoverSegment = -1 self.pressedSegment = -1 if self.source().isExec(): self.bubble = QGraphicsEllipseItem(-2.5, -2.5, 5, 5, self) self.bubble.setBrush(self.color) self.bubble.setPen(self.pen) point = self.mPath.pointAtPercent(0.0) self.bubble.setPos(point) self.bubble.hide() self.source()._rawPin.onExecute.connect( self.performEvaluationFeedback) self.shouldAnimate = False self.timeline = QtCore.QTimeLine(2000) self.timeline.setFrameRange(0, 100) self.timeline.frameChanged.connect(self.timelineFrameChanged) self.timeline.setLoopCount(0) def performEvaluationFeedback(self, *args, **kwargs): if self.timeline.state() == QtCore.QTimeLine.State.NotRunning: self.shouldAnimate = True # spawn bubble self.bubble.show() self.timeline.start() def timelineFrameChanged(self, frameNum): percentage = currentProcessorTime() - self.source( )._rawPin.getLastExecutionTime() self.shouldAnimate = percentage < 0.5 point = self.mPath.pointAtPercent( float(frameNum) / float(self.timeline.endFrame())) self.bubble.setPos(point) if not self.shouldAnimate: self.timeline.stop() self.bubble.hide() def setSelected(self, value): super(UIConnection, self).setSelected(value) def isUnderCollapsedComment(self): srcNode = self.source().owningNode() dstNode = self.destination().owningNode() srcComment = srcNode.owningCommentNode dstComment = dstNode.owningCommentNode if srcComment is not None and dstComment is not None and srcComment == dstComment and srcComment.collapsed: return True return False def isUnderActiveGraph(self): return self.canvasRef().graphManager.activeGraph() == self.source( )._rawPin.owningNode().graph() def __repr__(self): return "{0} -> {1}".format(self.source().getFullName(), self.destination().getFullName()) def setColor(self, color): self.pen.setColor(color) self.color = color def updateEndpointsPositions(self): srcNode = self.source().owningNode() dstNode = self.destination().owningNode() srcComment = srcNode.owningCommentNode if srcComment is not None: # if comment is collapsed or under another comment, move point to top most collapsed comment's right side srcNodeUnderCollapsedComment = srcComment.isUnderCollapsedComment() topMostCollapsedComment = srcNode.getTopMostOwningCollapsedComment( ) if srcComment.collapsed: rightSideEndpointGetter = srcComment.getRightSideEdgesPoint if srcNodeUnderCollapsedComment: rightSideEndpointGetter = topMostCollapsedComment.getRightSideEdgesPoint self.sourcePositionOverride = rightSideEndpointGetter else: if srcNodeUnderCollapsedComment: self.sourcePositionOverride = topMostCollapsedComment.getRightSideEdgesPoint else: self.sourcePositionOverride = None else: # if no comment return source point back to pin self.sourcePositionOverride = None # Same for right hand side dstComment = dstNode.owningCommentNode if dstComment is not None: dstNodeUnderCollapsedComment = dstComment.isUnderCollapsedComment() topMostCollapsedComment = dstNode.getTopMostOwningCollapsedComment( ) if dstComment.collapsed: rightSideEndpointGetter = dstComment.getLeftSideEdgesPoint if dstNodeUnderCollapsedComment: rightSideEndpointGetter = topMostCollapsedComment.getLeftSideEdgesPoint self.destinationPositionOverride = rightSideEndpointGetter else: if dstNodeUnderCollapsedComment: self.destinationPositionOverride = topMostCollapsedComment.getLeftSideEdgesPoint else: self.destinationPositionOverride = None else: self.destinationPositionOverride = None def Tick(self): # check if this instance represents existing connection # if not - destroy if not arePinsConnected(self.source()._rawPin, self.destination()._rawPin): self.canvasRef().removeConnection(self) if self.drawSource.isExec() or self.drawDestination.isExec(): if self.thickness != 2: self.thickness = 2 self.pen.setWidthF(self.thickness) if self.isSelected(): self.pen.setColor(self.selectedColor) else: self.pen.setColor(self.color) self.update() def contextMenuEvent(self, event): self._menu.exec_(event.screenPos()) @property def uid(self): return self._uid @uid.setter def uid(self, value): if self._uid in self.canvasRef().connections: self.canvasRef().connections[value] = self.canvasRef( ).connections.pop(self._uid) self._uid = value def applyJsonData(self, data): hOffsetL = data['hOffsetL'] if hOffsetL is not None: self.hOffsetL = float(hOffsetL) hOffsetR = data['hOffsetR'] if hOffsetR is not None: self.hOffsetR = float(hOffsetR) hOffsetLSShape = data['hOffsetLSShape'] if hOffsetLSShape is not None: self.hOffsetLSShape = float(hOffsetLSShape) hOffsetRSShape = data['hOffsetRSShape'] if hOffsetRSShape is not None: self.hOffsetRSShape = float(hOffsetRSShape) vOffset = data['vOffset'] if vOffset is not None: self.vOffset = float(vOffset) vOffsetSShape = data['vOffsetSShape'] if vOffsetSShape is not None: self.vOffsetSShape = float(vOffsetSShape) snapVToFirst = data['snapVToFirst'] if snapVToFirst is not None: self.snapVToFirst = bool(snapVToFirst) snapVToSecond = data['snapVToSecond'] if snapVToSecond is not None: self.snapVToSecond = bool(snapVToSecond) self.getEndPoints() def serialize(self): script = { 'sourceUUID': str(self.source().uid), 'destinationUUID': str(self.destination().uid), 'sourceName': self.source()._rawPin.getFullName(), 'destinationName': self.destination()._rawPin.getFullName(), 'uuid': str(self.uid), 'hOffsetL': str(self.hOffsetL), 'hOffsetR': str(self.hOffsetR), 'hOffsetLSShape': str(self.hOffsetLSShape), 'hOffsetRSShape': str(self.hOffsetRSShape), 'vOffset': str(self.vOffset), 'vOffsetSShape': str(self.vOffsetSShape), 'snapVToFirst': int(self.snapVToFirst), 'snapVToSecond': int(self.snapVToSecond), } return script def __str__(self): return '{0} >>> {1}'.format(self.source()._rawPin.getFullName(), self.destination()._rawPin.getFullName()) def drawThick(self): self.pen.setWidthF(self.thickness + (self.thickness / 1.5)) f = 0.5 r = abs(lerp(self.color.red(), Colors.Yellow.red(), clamp(f, 0, 1))) g = abs(lerp(self.color.green(), Colors.Yellow.green(), clamp(f, 0, 1))) b = abs(lerp(self.color.blue(), Colors.Yellow.blue(), clamp(f, 0, 1))) self.pen.setColor(QtGui.QColor.fromRgb(r, g, b)) def restoreThick(self): self.pen.setWidthF(self.thickness) self.pen.setColor(self.color) def hoverEnterEvent(self, event): super(UIConnection, self).hoverEnterEvent(event) self.drawThick() self.update() def hoverLeaveEvent(self, event): super(UIConnection, self).hoverLeaveEvent(event) self.hoverSegment = -1 self.restoreThick() self.update() def hoverMoveEvent(self, event): if self.offsetting == 0: self.hoverSegment = -1 if self.linPath is not None: tempPath = ConnectionPainter.linearPath(self.linPath) t = self.percentageByPoint(event.scenePos(), tempPath) segments = [] for i, pos in enumerate(self.linPath[:-1]): t1 = self.percentageByPoint(pos, tempPath) t2 = self.percentageByPoint(self.linPath[i + 1], tempPath) segments.append([t1, t2]) for i, seg in enumerate(segments): if t > seg[0] and t < seg[1]: valid = [] if not self.sShape: if self.snapVToFirst: valid = [0, 1] elif self.snapVToSecond: valid = [1, 2] else: valid = [1, 2, 3] else: valid = [1, 2, 3] if i in valid: self.hoverSegment = i else: self.hoverSegment = -1 def getEndPoints(self): p1 = self.drawSource.scenePos() + self.drawSource.pinCenter() if self.sourcePositionOverride is not None: p1 = self.sourcePositionOverride() p2 = self.drawDestination.scenePos() + self.drawDestination.pinCenter() if self.destinationPositionOverride is not None: p2 = self.destinationPositionOverride() if editableStyleSheet().ConnectionMode[0] in [ ConnectionTypes.Circuit, ConnectionTypes.ComplexCircuit ]: self.sameSide = 0 p1n, p2n = p1, p2 xDistance = p2.x() - p1.x() if self.destination().owningNode()._rawNode.__class__.__name__ in [ "reroute", "rerouteExecs" ]: if xDistance < 0: p2n, p1n = p1, p2 self.sameSide = 1 if self.source().owningNode()._rawNode.__class__.__name__ in [ "reroute", "rerouteExecs" ]: if xDistance < 0: p1n, p2n = p1, p2 self.sameSide = -1 p1, p2 = p1n, p2n return p1, p2 def percentageByPoint(self, point, path, precision=0.5, width=20.0): percentage = -1.0 stroker = QtGui.QPainterPathStroker() stroker.setWidth(width) strokepath = stroker.createStroke(path) t = 0.0 d = [] while t <= 100.0: d.append( QtGui.QVector2D(point - path.pointAtPercent(t / 100)).length()) t += precision percentage = d.index(min(d)) * precision return percentage def mousePressEvent(self, event): super(UIConnection, self).mousePressEvent(event) t = self.percentageByPoint(event.scenePos(), self.mPath) self.prevPos = event.pos() if abs(self.mPath.slopeAtPercent(t * 0.01)) < 1: self.offsetting = 1 else: self.offsetting = 2 if self.linPath is not None: tempPath = ConnectionPainter.linearPath(self.linPath) t = self.percentageByPoint(event.scenePos(), tempPath) segments = [] for i, pos in enumerate(self.linPath[:-1]): t1 = self.percentageByPoint(pos, tempPath) t2 = self.percentageByPoint(self.linPath[i + 1], tempPath) segments.append([t1, t2]) for i, seg in enumerate(segments): if t > seg[0] and t < seg[1]: valid = [] if not self.sShape: if self.snapVToFirst: valid = [0, 1] elif self.snapVToSecond: valid = [1, 2] else: valid = [1, 2, 3] else: valid = [1, 2, 3] if i in valid: self.pressedSegment = i else: self.pressedSegment = -1 p1, p2 = self.getEndPoints() offset1 = editableStyleSheet().ConnectionOffset[0] offset2 = -offset1 if self.sameSide == 1: offset2 = offset1 elif self.sameSide == -1: offset1 = offset2 xDistance = (p2.x() + offset2) - (p1.x() + offset1) self.sShape = xDistance < 0 event.accept() def mouseReleaseEvent(self, event): super(UIConnection, self).mouseReleaseEvent(event) self.offsetting = 0 self.pressedSegment = -1 event.accept() def mouseMoveEvent(self, event): super(UIConnection, self).mouseMoveEvent(event) if self.prevPos is not None: delta = self.prevPos - event.pos() p1, p2 = self.getEndPoints() if not self.sShape: if self.offsetting == 1: doIt = True if self.snapVToFirst and self.pressedSegment != 0: doIt = False self.pressedSegment = -1 elif self.snapVToSecond and self.pressedSegment != 2: doIt = False self.pressedSegment = -1 elif not self.snapVToFirst and not self.snapVToSecond: if self.pressedSegment != 2: doIt = False self.pressedSegment = -1 if doIt: self.vOffset -= float(delta.y()) if abs(self.vOffset) <= 3: self.snapVToFirst = True self.pressedSegment = 0 else: self.snapVToFirst = False if p1.y() + self.vOffset > p2.y() - 3 and p1.y( ) + self.vOffset < p2.y() + 3: self.snapVToSecond = True self.pressedSegment = 2 else: self.snapVToSecond = False if not self.snapVToFirst and self.pressedSegment == 0: self.pressedSegment = 2 if self.offsetting == 2: if self.snapVToFirst: self.hOffsetR -= float(delta.x()) elif self.snapVToSecond: self.hOffsetL -= float(delta.x()) else: if self.pressedSegment == 1: self.hOffsetL -= float(delta.x()) elif self.pressedSegment == 3: self.hOffsetR -= float(delta.x()) else: if self.offsetting == 1 and self.pressedSegment == 2: self.vOffsetSShape -= float(delta.y()) elif self.offsetting == 2: if self.pressedSegment == 1: self.hOffsetRSShape -= float(delta.x()) elif self.pressedSegment == 3: self.hOffsetLSShape -= float(delta.x()) self.prevPos = event.pos() event.accept() def source_port_name(self): return self.source().getFullName() def shape(self): qp = QtGui.QPainterPathStroker() qp.setWidth(10.0) qp.setCapStyle(QtCore.Qt.SquareCap) return qp.createStroke(self.path()) def updateCurve(self, p1, p2): xDistance = p2.x() - p1.x() multiply = 3 self.mPath = QtGui.QPainterPath() self.mPath.moveTo(p1) if xDistance < 0: self.mPath.cubicTo( QtCore.QPoint(p1.x() + xDistance / -multiply, p1.y()), QtCore.QPoint(p2.x() - xDistance / -multiply, p2.y()), p2) else: self.mPath.cubicTo( QtCore.QPoint(p1.x() + xDistance / multiply, p1.y()), QtCore.QPoint(p2.x() - xDistance / 2, p2.y()), p2) self.setPath(self.mPath) def kill(self): self.canvasRef().removeConnection(self) def paint(self, painter, option, widget): option.state &= ~QStyle.State_Selected lod = self.canvasRef().getCanvasLodValueFromCurrentScale() self.setPen(self.pen) p1, p2 = self.getEndPoints() roundness = editableStyleSheet().ConnectionRoundness[0] offset = editableStyleSheet().ConnectionOffset[0] offset1 = offset offset2 = -offset1 if self.sameSide == 1: offset2 = offset1 elif self.sameSide == -1: offset1 = offset2 xDistance = (p2.x() + offset2) - (p1.x() + offset1) self.sShape = xDistance < 0 sectionPath = None if editableStyleSheet().ConnectionMode[0] == ConnectionTypes.Circuit: seg = self.hoverSegment if self.hoverSegment != -1 and self.linPath and self.pressedSegment == -1 else self.pressedSegment self.mPath, self.linPath, sectionPath = ConnectionPainter.BasicCircuit( p1, p2, offset, roundness, self.sameSide, lod, False, self.vOffset, self.hOffsetL, self.vOffsetSShape, self.hOffsetR, self.hOffsetRSShape, self.hOffsetLSShape, self.snapVToFirst, self.snapVToSecond, seg) elif editableStyleSheet( ).ConnectionMode[0] == ConnectionTypes.ComplexCircuit: self.mPath, self.linPath, sectionPath = ConnectionPainter.BasicCircuit( p1, p2, offset, roundness, self.sameSide, lod, True) elif editableStyleSheet().ConnectionMode[0] == ConnectionTypes.Cubic: self.mPath = ConnectionPainter.Cubic(p1, p2, 150, lod) self.linPath = None elif editableStyleSheet().ConnectionMode[0] == ConnectionTypes.Linear: self.mPath = ConnectionPainter.Linear(p1, p2, offset, roundness, lod) self.linPath = None if self.snapVToSecond and self.offsetting == 0: self.vOffset = p2.y() - p1.y() self.setPath(self.mPath) super(UIConnection, self).paint(painter, option, widget) pen = QtGui.QPen() pen.setColor(editableStyleSheet().MainColor) pen.setWidthF(self.thickness + (self.thickness / 1.5)) painter.setPen(pen) if sectionPath: painter.drawPath(sectionPath)
class UIConnection(QGraphicsPathItem): """UIConnection is a cubic spline curve. It represents connecton between two pins. """ def __init__(self, source, destination, canvas): QGraphicsPathItem.__init__(self) self.setAcceptedMouseButtons(QtCore.Qt.LeftButton) self.setAcceptHoverEvents(True) self.setFlag(QGraphicsPathItem.ItemIsSelectable) self._menu = QMenu() self.actionDisconnect = self._menu.addAction("Disconnect") self.actionDisconnect.triggered.connect(self.kill) self._uid = uuid4() self.canvasRef = weakref.ref(canvas) self.source = weakref.ref(source) self.destination = weakref.ref(destination) self.drawSource = self.source() self.drawDestination = self.destination() # Overrides for getting endpoints positions # if None - pin centers will be used self.sourcePositionOverride = None self.destinationPositionOverride = None self.mPath = QtGui.QPainterPath() self.cp1 = QtCore.QPointF(0.0, 0.0) self.cp2 = QtCore.QPointF(0.0, 0.0) self.setZValue(NodeDefaults().Z_LAYER - 1) self.color = self.source().color() self.selectedColor = self.color.lighter(150) self.thickness = 1 if source.isExec(): self.thickness = 2 self.pen = QtGui.QPen(self.color, self.thickness, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin) points = self.getEndPoints() self.updateCurve(points[0], points[1]) self.setPen(self.pen) self.source().update() self.destination().update() self.fade = 0.0 self.source().uiConnectionList.append(self) self.destination().uiConnectionList.append(self) self.source().pinConnected(self.destination()) self.destination().pinConnected(self.source()) def setSelected(self, value): super(UIConnection, self).setSelected(value) def isUnderCollapsedComment(self): srcNode = self.source().owningNode() dstNode = self.destination().owningNode() srcComment = srcNode.owningCommentNode dstComment = dstNode.owningCommentNode if srcComment is not None and dstComment is not None and srcComment == dstComment and srcComment.collapsed: return True return False def isUnderActiveGraph(self): return self.canvasRef().graphManager.activeGraph() == self.source( )._rawPin.owningNode().graph() def __repr__(self): return "{0} -> {1}".format(self.source().getFullName(), self.destination().getFullName()) def setColor(self, color): self.pen.setColor(color) self.color = color def updateEndpointsPositions(self): srcNode = self.source().owningNode() dstNode = self.destination().owningNode() srcComment = srcNode.owningCommentNode if srcComment is not None: # if comment is collapsed or under another comment, move point to top most collapsed comment's right side srcNodeUnderCollapsedComment = srcComment.isUnderCollapsedComment() topMostCollapsedComment = srcNode.getTopMostOwningCollapsedComment( ) if srcComment.collapsed: rightSideEndpointGetter = srcComment.getRightSideEdgesPoint if srcNodeUnderCollapsedComment: rightSideEndpointGetter = topMostCollapsedComment.getRightSideEdgesPoint self.sourcePositionOverride = rightSideEndpointGetter else: if srcNodeUnderCollapsedComment: self.sourcePositionOverride = topMostCollapsedComment.getRightSideEdgesPoint else: self.sourcePositionOverride = None else: # if no comment return source point back to pin self.sourcePositionOverride = None # Same for right hand side dstComment = dstNode.owningCommentNode if dstComment is not None: dstNodeUnderCollapsedComment = dstComment.isUnderCollapsedComment() topMostCollapsedComment = dstNode.getTopMostOwningCollapsedComment( ) if dstComment.collapsed: rightSideEndpointGetter = dstComment.getLeftSideEdgesPoint if dstNodeUnderCollapsedComment: rightSideEndpointGetter = topMostCollapsedComment.getLeftSideEdgesPoint self.destinationPositionOverride = rightSideEndpointGetter else: if dstNodeUnderCollapsedComment: self.destinationPositionOverride = topMostCollapsedComment.getLeftSideEdgesPoint else: self.destinationPositionOverride = None else: self.destinationPositionOverride = None def Tick(self): # check if this instance represents existing connection # if not - destroy if not arePinsConnected(self.source()._rawPin, self.destination()._rawPin): self.canvasRef().removeConnection(self) if self.drawSource._rawPin.isExec( ) or self.drawDestination._rawPin.isExec(): if self.thickness != 2: self.thickness = 2 self.pen.setWidthF(self.thickness) self.update() if self.isSelected(): self.pen.setColor(self.selectedColor) else: self.pen.setColor(self.color) self.update() def contextMenuEvent(self, event): self._menu.exec_(event.screenPos()) @property def uid(self): return self._uid @uid.setter def uid(self, value): if self._uid in self.canvasRef().connections: self.canvasRef().connections[value] = self.canvasRef( ).connections.pop(self._uid) self._uid = value @staticmethod def deserialize(data, graph): srcUUID = UUID(data['sourceUUID']) dstUUID = UUID(data['destinationUUID']) # if srcUUID in graph.pins and dstUUID in graph.pins: srcPin = graph.findPinByUid(srcUUID) assert (srcPin is not None) dstPin = graph.findPinByUid(dstUUID) assert (dstPin is not None) connection = graph.connectPinsInternal(srcPin, dstPin) assert (connection is not None) connection.uid = UUID(data['uuid']) def serialize(self): script = { 'sourceUUID': str(self.source().uid), 'destinationUUID': str(self.destination().uid), 'sourceName': self.source()._rawPin.getFullName(), 'destinationName': self.destination()._rawPin.getFullName(), 'uuid': str(self.uid) } return script def __str__(self): return '{0} >>> {1}'.format(self.source()._rawPin.getFullName(), self.destination()._rawPin.getFullName()) def drawThick(self): self.pen.setWidthF(self.thickness + (self.thickness / 1.5)) f = 0.5 r = abs(lerp(self.color.red(), Colors.Yellow.red(), clamp(f, 0, 1))) g = abs(lerp(self.color.green(), Colors.Yellow.green(), clamp(f, 0, 1))) b = abs(lerp(self.color.blue(), Colors.Yellow.blue(), clamp(f, 0, 1))) self.pen.setColor(QtGui.QColor.fromRgb(r, g, b)) def restoreThick(self): self.pen.setWidthF(self.thickness) self.pen.setColor(self.color) def hoverEnterEvent(self, event): super(UIConnection, self).hoverEnterEvent(event) self.drawThick() self.update() def getEndPoints(self): p1 = self.drawSource.scenePos() + self.drawSource.pinCenter() if self.sourcePositionOverride is not None: p1 = self.sourcePositionOverride() p2 = self.drawDestination.scenePos() + self.drawDestination.pinCenter() if self.destinationPositionOverride is not None: p2 = self.destinationPositionOverride() return p1, p2 def mousePressEvent(self, event): super(UIConnection, self).mousePressEvent(event) event.accept() def mouseReleaseEvent(self, event): super(UIConnection, self).mouseReleaseEvent(event) event.accept() def mouseMoveEvent(self, event): super(UIConnection, self).mouseMoveEvent(event) event.accept() def hoverLeaveEvent(self, event): super(UIConnection, self).hoverLeaveEvent(event) self.restoreThick() self.update() def source_port_name(self): return self.source().getFullName() def shape(self): qp = QtGui.QPainterPathStroker() qp.setWidth(10.0) qp.setCapStyle(QtCore.Qt.SquareCap) return qp.createStroke(self.path()) def updateCurve(self, p1, p2): xDistance = p2.x() - p1.x() multiply = 3 self.mPath = QtGui.QPainterPath() self.mPath.moveTo(p1) if xDistance < 0: self.mPath.cubicTo( QtCore.QPoint(p1.x() + xDistance / -multiply, p1.y()), QtCore.QPoint(p2.x() - xDistance / -multiply, p2.y()), p2) else: self.mPath.cubicTo( QtCore.QPoint(p1.x() + xDistance / multiply, p1.y()), QtCore.QPoint(p2.x() - xDistance / 2, p2.y()), p2) self.setPath(self.mPath) def kill(self): self.canvasRef().removeConnection(self) def paint(self, painter, option, widget): option.state &= ~QStyle.State_Selected lod = self.canvasRef().getLodValueFromCurrentScale(5) self.setPen(self.pen) p1, p2 = self.getEndPoints() if lod >= 5: self.mPath = QtGui.QPainterPath() self.mPath.moveTo(p1) self.mPath.lineTo(p2) else: xDistance = p2.x() - p1.x() vDistance = p2.y() - p1.y() offset = abs(xDistance) * 0.5 defOffset = 150 if abs(xDistance) < defOffset: offset = defOffset / 2 if abs(vDistance) < 20: offset = abs(xDistance) * 0.3 multiply = 2 self.mPath = QtGui.QPainterPath() self.mPath.moveTo(p1) if xDistance < 0: self.cp1 = QtCore.QPoint(p1.x() + offset, p1.y()) self.cp2 = QtCore.QPoint(p2.x() - offset, p2.y()) else: self.cp2 = QtCore.QPoint(p2.x() - offset, p2.y()) self.cp1 = QtCore.QPoint(p1.x() + offset, p1.y()) self.mPath.cubicTo(self.cp1, self.cp2, p2) self.setPath(self.mPath) super(UIConnection, self).paint(painter, option, widget)
class pythonNode(Node, NodeBase): def __init__(self, name, graph): super(pythonNode, self).__init__(name, graph) self.menu = QMenu() self.actionEdit = self.menu.addAction('edit') self.actionEdit.triggered.connect(self.openEditor) self.actionEdit.setIcon(QtGui.QIcon(':/icons/resources/py.png')) self.editorUUID = None self.bKillEditor = True self.label().icon = QtGui.QImage(':/icons/resources/py.png') self.currentComputeCode = Node.jsonTemplate()['computeCode'] @staticmethod def pinTypeHints(): return {'inputs': [], 'outputs': []} def computeCode(self): return self.currentComputeCode def openEditor(self): self.editorUUID = uuid.uuid4() self.graph().codeEditors[self.editorUUID] = WCodeEditor( self.graph(), self, self.editorUUID) self.graph().codeEditors[self.editorUUID].show() def kill(self): if self.editorUUID in self.graph().codeEditors: ed = self.graph().codeEditors.pop(self.editorUUID) ed.deleteLater() Node.kill(self) @staticmethod def category(): return 'Utils' def postCreate(self, jsonTemplate): # restore compute self.currentComputeCode = jsonTemplate['computeCode'] foo = WCodeEditor.wrapCodeToFunction('compute', jsonTemplate['computeCode']) exec(foo) self.compute = MethodType(compute, self, Node) # restore pins for inpJson in jsonTemplate['inputs']: pin = None if inpJson['dataType'] == DataTypes.Exec: pin = self.addInputPin(inpJson['name'], inpJson['dataType'], self.compute, inpJson['bLabelHidden']) pin.uid = uuid.UUID(inpJson['uuid']) else: pin = self.addInputPin(inpJson['name'], inpJson['dataType'], None, inpJson['bLabelHidden']) pin.uid = uuid.UUID(inpJson['uuid']) pin.setData(inpJson['value']) for outJson in jsonTemplate['outputs']: pin = self.addOutputPin(outJson['name'], outJson['dataType'], None, outJson['bLabelHidden']) pin.uid = uuid.UUID(outJson['uuid']) pin.setData(outJson['value']) self.bCallable = self.isCallable() Node.postCreate(self, jsonTemplate) # restore node label self.label().setPlainText(jsonTemplate['meta']['label']) def contextMenuEvent(self, event): self.menu.exec_(event.screenPos()) @staticmethod def keywords(): return ['Code', 'Expression'] @staticmethod def description(): return 'default description'