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 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
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())
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())
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 create_settings_menu(self): """ Creates and returns the settings menu for this widget :return: QMenu """ menu = QMenu('Item View', self) menu.setIcon(resources.icon('eye')) menu.addSeparator() # copy_text_menu = self.tree_widget().create_copy_text_menu() # menu.addMenu(copy_text_menu) # menu.addSeparator() size_action = action.SliderAction('Size', menu) size_action.slider().setMinimum(10) size_action.slider().setMaximum(200) size_action.slider().setValue(self.zoom_amount()) size_action.slider().valueChanged.connect(self.set_zoom_amount) menu.addAction(size_action) border_action = action.SliderAction('Border', menu) border_action.slider().setMinimum(0) border_action.slider().setMaximum(20) border_action.slider().setValue(self.padding()) border_action.slider().valueChanged.connect(self.set_padding) menu.addAction(border_action) spacing_action = action.SliderAction('Spacing', menu) spacing_action.slider().setMinimum(self.DEFAULT_MIN_SPACING) spacing_action.slider().setMaximum(self.DEFAULT_MAX_SPACING) spacing_action.slider().setValue(self.spacing()) spacing_action.slider().valueChanged.connect(self.set_spacing) menu.addAction(spacing_action) menu.addAction(action.SeparatorAction('Label Options', menu)) for option in item.LabelDisplayOption.values(): option_action = QAction(option.title(), menu) option_action.setCheckable(True) option_action.setChecked(option == self.label_display_option()) option_callback = partial(self.set_label_display_option, option) option_action.triggered.connect(option_callback) menu.addAction(option_action) return menu
def _create_header_menu(self, column): """ Internal function that creates a new header menu :param column, iht :return: QMenu """ menu = QMenu(self) label = self.label_from_column(column) hide_action = menu.addAction('Hide "{}"'.format(label)) hide_callback = partial(self.setColumnHidden, column, True) hide_action.triggered.connect(hide_callback) menu.addSeparator() resize_action = menu.addAction('Resize to Contents') resize_callback = partial(self.resizeColumnToContents, column) resize_action.triggered.connect(resize_callback) return menu
def create_copy_text_menu(self): """ Creates a menu to cpoy the selected item data to the clipboard :return: QMenu """ menu = QMenu('Copy Text', self) if self.selectedItems(): for column in range(self.columnCount()): label = self.label_from_column(column) action = menu.addAction(label) action_callback = partial(self.copy_text, column) action.triggered.connect(action_callback) else: action = menu.addAction('No items selected') action.setEnabled(False) return menu
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 create_items_menu(self, items=None): """ Creates the item menu for the given items :param items: list(LibraryItem) :return: QMenu """ item = items or self.selected_item() menu = QMenu(self) if item: try: item.context_menu(menu) except Exception as e: LOGGER.exception(e) else: item_action = QAction(menu) item_action.setText('No Item selected') item_action.setDisabled(True) menu.addAction(item_action) return menu
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 _create_hide_column_menu(self): """ Internal function that creates the hide column menu :return: QMenu """ menu = QMenu('Show/Hide Column', self) show_all_action = menu.addAction('Show All') show_all_action.triggered.connect(self.show_all_columns) hide_all_action = menu.addAction('Hide All') hide_all_action.triggered.connect(self.hide_all_columns) menu.addSeparator() for column in range(self.columnCount()): label = self.label_from_column(column) is_hidden = self.isColumnHidden(column) action = menu.addAction(label) action.setCheckable(True) action.setChecked(not is_hidden) action_callback = partial(self.setColumnHidden, column, not is_hidden) action.triggered.connect(action_callback) return menu
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()]))
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()
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())
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())
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 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()]))
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 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 MultiSelectComboBox(QFrame): ''' This class offers comboBox to select multiple objects at once ''' selectionDone = Signal(list) def __init__(self, parent=None, msg='--Select--', triState=False): super(MultiSelectComboBox, self).__init__(parent) loadUi(osp.join(uiPath, 'multiSelectComboBox.ui'), self) self.triState = triState self.msg = msg self.menu = QMenu(self) self.menu.setStyleSheet("QMenu { menu-scrollable: 1; }") self.button.setMenu(self.menu) self.menu.hideEvent = self.menuHideEvent self.setHintText(self.msg) def addDefaultWidgets(self): button = QPushButton('Invert Selection', self) button.clicked.connect(self.invertSelection) checkableAction = QWidgetAction(self.menu) checkableAction.setDefaultWidget(button) self.menu.addAction(checkableAction) btn = self.addItem('Select All') btn.clicked.connect(lambda: self.toggleAll(btn)) self.menu.addSeparator() def invertSelection(self): for cBox in self.getWidgetItems(): cBox.setChecked(not cBox.isChecked()) self.toggleSelectAllButton() def toggleAll(self, btn): for cBox in self.getWidgetItems(): cBox.setCheckState(btn.checkState()) def toggleSelectAllButton(self): flag = True for cBox in self.getWidgetItems(): if not cBox.isChecked(): flag = False break for action in self.menu.actions(): if type(action) == QAction: continue widget = action.defaultWidget() if widget.text() == 'Select All': widget.setChecked(flag) def setHintText(self, text): self.button.setText(text) def menuHideEvent(self, event): items = self.getSelectedItems() if items: s = '' if len(items) > 2: s = ',...' self.button.setText(','.join(items[:2]) + s) else: self.setHintText(self.msg) if event: QMenu.hideEvent(self.menu, event) self.selectionDone.emit(items) def getSelectedItems(self): return [ cBox.text().strip() for cBox in self.getWidgetItems() if cBox.isChecked() ] def getState(self): return { cBox.text().strip(): cBox.checkState() for cBox in self.getWidgetItems() } def getWidgetItems(self): return [ cBox for cBox in [ action.defaultWidget() for action in self.menu.actions() if not type(action) is QAction ] if cBox.text().strip() != 'Select All' and cBox.text().strip() != 'Invert Selection' ] def getItems(self): return [cBox.text().strip() for cBox in self.getWidgetItems()] def addItem(self, item, selected=False): checkBox = QCheckBox(item, self.menu) checkBox.setTristate(self.triState) if selected: checkBox.setChecked(True) checkableAction = QWidgetAction(self.menu) checkableAction.setDefaultWidget(checkBox) self.menu.addAction(checkableAction) return checkBox def appendItems(self, items, selected=None): self.addItems(items, selected=selected, clear=False) def addItems(self, items, selected=None, clear=True): items = sorted(items) if clear: self.clearItems() self.addDefaultWidgets() for item in items: sel = False if selected and item in selected: sel = True btn = self.addItem(item, sel) btn.clicked.connect(self.toggleSelectAllButton) if selected: self.menuHideEvent(None) def clearItems(self): self.menu.clear() self.setHintText(self.msg)
class MayaShelf(abstract_shelf.AbstractShelf, object): def __init__(self, name='MayaShelf', label_background=(0, 0, 0, 0), label_color=(0.9, 0.9, 0.9), category_icon=None, enable_labels=True): super(MayaShelf, self).__init__(name=name, label_background=label_background, label_color=label_color, category_icon=category_icon, enable_labels=enable_labels) @staticmethod def add_menu_item(parent, label, command='', icon=''): """ Adds a menu item with the given attributes :param parent: :param label: :param command: :param icon: :return: """ return maya.cmds.menuItem(parent=parent, label=label, command=command, image=icon or '') @staticmethod def add_sub_menu(parent, label, icon=None): """ Adds a sub menu item with the given label and icon to the given parent popup menu :param parent: :param label: :param icon: :return: """ return maya.cmds.menuItem(parent=parent, label=label, icon=icon or '', subMenu=True) def create(self, delete_if_exists=True): """ Creates a new shelf """ if delete_if_exists: if gui.shelf_exists(shelf_name=self._name): gui.delete_shelf(shelf_name=self._name) else: assert not gui.shelf_exists(self._name), 'Shelf with name {} already exists!'.format(self._name) self._name = gui.create_shelf(name=self._name) # ======================================================================================================== self._category_btn = QPushButton('') if self._category_icon: self._category_btn.setIcon(self._category_icon) self._category_btn.setIconSize(QSize(18, 18)) self._category_menu = QMenu(self._category_btn) self._category_btn.setStyleSheet( 'QPushButton::menu-indicator {image: url(myindicator.png);' 'subcontrol-position: right center;subcontrol-origin: padding;left: -2px;}') self._category_btn.setMenu(self._category_menu) self._category_lbl = QLabel('MAIN') self._category_lbl.setAlignment(Qt.AlignCenter) font = self._category_lbl.font() font.setPointSize(6) self._category_lbl.setFont(font) menu_ptr = maya.OpenMayaUI.MQtUtil.findControl(self._name) menu_widget = qtutils.wrapinstance(menu_ptr, QWidget) menu_widget.layout().addWidget(self._category_btn) menu_widget.layout().addWidget(self._category_lbl) self.add_separator() def set_as_active(self): """ Sets this shelf as active shelf in current DCC session """ main_shelf = maya.mel.eval("$_tempVar = $gShelfTopLevel") maya.cmds.tabLayout(main_shelf, edit=True, selectTab=self._name) def add_button(self, label, tooltip=None, icon='customIcon.png', command=None, double_command=None, command_type='python'): """ Adds a shelf button width the given parameters :param label: :param tooltip: :param icon: :param command: :param double_command: :param command_type: :return: """ maya.cmds.setParent(self._name) command = command or '' double_command = double_command or '' if not self._enable_labels: label = '' return maya.cmds.shelfButton(width=37, height=37, image=icon or '', label=label, command=command, doubleClickCommand=double_command, annotation=tooltip or '', imageOverlayLabel=label, overlayLabelBackColor=self._label_background, overlayLabelColor=self._label_color, sourceType=command_type) def add_separator(self): """ Adds a separator to shelf :param parent: :return: """ maya.cmds.separator( parent=self._name, manage=True, visible=True, horizontal=False, style='shelf', enableBackground=False, preventOverride=False) def build_category(self, shelf_file, category_name): self._category_lbl.setText(category_name.upper()) self.load_category(shelf_file, 'general', clear=True) if category_name != 'general': self.add_separator() self.load_category(shelf_file, category_name, clear=False) def build_categories(self, shelf_file, categories): """ Builds all categories given :param categories: list<str>, list of categories to build """ self._category_lbl.setText('ALL') self.load_category(shelf_file, 'general', clear=True) for cat in categories: if cat == 'general': continue self.add_separator() self.load_category(shelf_file, cat, clear=False) def load_category(self, shelf_file, category_name, clear=True): """ Loads into a shelf all the items of given category name, if exists :param category_name: str, name of the category """ if clear: self.clear_list() # self.add_separator() with open(shelf_file) as f: shelf_data = json.load(f, object_pairs_hook=OrderedDict) for item, item_data in shelf_data.items(): if item != category_name: continue for i in item_data: icon = i.get('icon') command = i.get('command') annotation = i.get('annotation') label = i.get('label') if annotation == 'separator': self.add_separator() else: self.add_button(label=label, command=command, icon=icon, tooltip=annotation) return def build(self, shelf_file): """ Builds shelf from JSON file :param shelf_file: str """ first_item = None all_categories = list() with open(shelf_file) as f: shelf_data = json.load(f, object_pairs_hook=OrderedDict) for i, item in enumerate(shelf_data.keys()): if i == 0: first_item = item category_action = self._category_menu.addAction(item.title()) category_action.triggered.connect(partial(self.build_category, shelf_file, item)) all_categories.append(item) category_action = self._category_menu.addAction('All') category_action.triggered.connect(partial(self.build_categories, shelf_file, all_categories)) if first_item: self.load_category(shelf_file, first_item, clear=False) def clear_list(self): """ Clears all the elements of the shelf """ if gui.shelf_exists(shelf_name=self._name): menu_items = maya.cmds.shelfLayout(self._name, query=True, childArray=True) for item in menu_items: try: maya.cmds.deleteUI(item) except Exception: pass
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 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)
def _create_button(self, name=None, icon=None, fn=None, status=None, tooltip=None, description=None, instructions=None, whats_this='', settings=None, settings_fn=None, info_message='', error_message='', widgets=None): """ Internal function to easily create buttons for the box widget :param name: :param icon: :param fn: :param status: :param tooltip: :return: """ name = name or '' new_btn = CommandButton(parent=self).small() if icon: if python.is_string(icon): icon = resources.icon(icon) new_btn.setIcon(icon) else: new_btn.setText(name) new_btn.setObjectName(name.replace(' ', '').lower() or description) new_btn.setStatusTip(status or name) new_btn.setToolTip(tooltip or name) new_btn.setProperty('name', name) new_btn.setProperty('description', description or tooltip or status or '') new_btn.setProperty('instructions', instructions or '') new_btn.setProperty('tooltip_help', { 'title': name, 'description': description }) new_btn.setStyleSheet('qproperty-iconSize: 20px 20px;') new_btn.setIconSize(QSize(20, 20)) new_btn.setWhatsThis(whats_this or tooltip or name) new_btn.setContextMenuPolicy(Qt.CustomContextMenu) new_btn.installEventFilter(self) if widgets: valid_widgets = list() for widget in widgets: if hasattr(self, widget): valid_widgets.append(getattr(self, widget)) if valid_widgets: self._widgets[new_btn.objectName()] = valid_widgets new_btn.has_menu = True if fn: info_message = info_message or 'Command "{}" executed successfully!'.format( name) error_message = error_message or 'Error while executing command "{}"! Check log for more info.'.format( name) new_btn.clicked.connect( partial(self._on_execute_command, name, fn, info_message, error_message)) new_btn.customContextMenuRequested.connect(self._on_show_context_menu) if settings: settings_icon = 'menu_dots' if settings and len( settings) > 1 else 'settings' buttons_widget = QWidget() buttons_widget.setStyleSheet( 'QPushButton::menu-indicator{ image: none; };') buttons_layout = layouts.HorizontalLayout(spacing=0, margins=(0, 0, 0, 0)) buttons_widget.setLayout(buttons_layout) if len(settings) > 1: options_name = '{} Options'.format(name) options_description = 'Show {} Menu'.format(options_name) options_btn = self._create_button( icon=settings_icon, fn=settings_fn, name=options_name, description=options_description) options_menu = QMenu(self) options_menu.installEventFilter(self) options_btn.setMenu(options_menu) for setting in settings: option_name = setting.get('name', None) if not option_name: continue option_fn = setting.get('fn', None) if not option_fn: continue option_description = setting.get('description', '') option_instructions = setting.get('instructions', '') option_icon = resources.icon( setting.get('icon', 'tpRigToolkit')) option_action = QAction(option_icon, option_name, self) option_action.setProperty('description', option_description or '') option_action.setProperty('instructions', option_instructions or '') option_info_message = info_message or 'Command "{}" executed successfully!'.format( name) option_error_message = error_message or 'Error while executing command "{}"! ' \ 'Check log for more info.'.format(name) option_action.triggered.connect( partial(self._on_execute_command, option_name, option_fn, option_info_message, option_error_message)) options_menu.addAction(option_action) else: setting = settings[0] option_description = setting.get('description', '') option_name = setting.get('name', None) option_fn = setting.get('fn', None) options_btn = self._create_button( icon=settings_icon, fn=option_fn, name=option_name, description=option_description) new_btn.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) new_btn.setStyleSheet( new_btn.styleSheet() + ';border-top-right-radius: 0px; border-bottom-right-radius: 0px; ' 'border-right: 0px;') new_btn.setMaximumWidth(28) options_btn.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) options_btn.setMaximumWidth(18) options_btn.setStyleSheet( options_btn.styleSheet() + ';border-top-left-radius: 0px; border-bottom-left-radius: 0px; ' 'qproperty-iconSize: 12px 12px;') buttons_layout.addWidget(new_btn) buttons_layout.addWidget(options_btn) self._buttons.append(new_btn) return buttons_widget self._buttons.append(new_btn) return new_btn
def _setup_menubar(self): """ Internal function that setups menu bar for the widget :return: """ menubar = self.menuBar() save_icon = resources.icon('save') load_icon = resources.icon('open_folder') play_icon = resources.icon('play') clear_icon = resources.icon('delete') resume_icon = resources.icon('resume') undo_icon = resources.icon('undo') redo_icon = resources.icon('redo') copy_icon = resources.icon('copy') cut_icon = resources.icon('cut') paste_icon = resources.icon('paste') tab_icon = resources.icon('tab') quote_icon = resources.icon('quote') rename_icon = resources.icon('rename') keyboard_icon = resources.icon('keyboard') help_icon = resources.icon('help') file_menu = QMenu('File', menubar) menubar.addMenu(file_menu) save_session_action = QAction(save_icon, 'Save Session', file_menu) load_script_action = QAction(load_icon, 'Load Script', file_menu) save_script_action = QAction(save_icon, 'Save Script', file_menu) file_menu.addAction(save_session_action) file_menu.addAction(load_script_action) file_menu.addAction(save_script_action) load_script_action.setShortcut('Ctrl+O') # load_script_action.setShortcutContext(Qt.WidgetShortcut) save_script_action.setShortcut('Ctrl+S') # save_script_action.setShortcutContext(Qt.WidgetShortcut) edit_menu = QMenu('Edit', self) menubar.addMenu(edit_menu) undo_action = QAction(undo_icon, 'Undo', edit_menu) redo_action = QAction(redo_icon, 'Redo', edit_menu) copy_action = QAction(copy_icon, 'Copy', edit_menu) cut_action = QAction(cut_icon, 'Cut', edit_menu) paste_action = QAction(paste_icon, 'Paste', edit_menu) tab_to_spaces_action = QAction(tab_icon, 'Tab to Spaces', edit_menu) comment_action = QAction(quote_icon, 'Comment', edit_menu) find_and_replace = QAction(rename_icon, 'Find and Replace', edit_menu) edit_menu.addAction(undo_action) edit_menu.addAction(redo_action) edit_menu.addSeparator() edit_menu.addAction(copy_action) edit_menu.addAction(cut_action) edit_menu.addAction(paste_action) edit_menu.addSeparator() edit_menu.addAction(tab_to_spaces_action) edit_menu.addAction(comment_action) edit_menu.addAction(find_and_replace) run_menu = QMenu('Run', self) menubar.addMenu(run_menu) self._execute_all_action = QAction(play_icon, 'Execute All', run_menu) self._execute_all_action.setShortcut('Ctrl+Shift+Return') self._execute_all_action.setShortcutContext(Qt.ApplicationShortcut) self._execute_selected_action = QAction(resume_icon, 'Execute Selected', run_menu) self._execute_selected_action.setShortcut('Ctrl+Return') self._execute_selected_action.setShortcutContext( Qt.WidgetWithChildrenShortcut) self._clear_output_action = QAction(clear_icon, 'Clear Output', run_menu) run_menu.addAction(self._execute_all_action) run_menu.addAction(self._execute_selected_action) run_menu.addAction(self._clear_output_action) help_menu = QMenu('Help', self) menubar.addMenu(help_menu) show_shortcuts_action = QAction(keyboard_icon, 'Show Shortcuts', help_menu) print_help_action = QAction(help_icon, 'Print Help', help_menu) help_menu.addAction(show_shortcuts_action) help_menu.addSeparator() help_menu.addAction(print_help_action) undo_action.setShortcut('Ctrl+Z') undo_action.setShortcutContext(Qt.WidgetShortcut) redo_action.setShortcut('Ctrl+Y') redo_action.setShortcutContext(Qt.WidgetShortcut) copy_action.setShortcut('Ctrl+C') copy_action.setShortcutContext(Qt.WidgetShortcut) cut_action.setShortcut('Ctrl+X') cut_action.setShortcutContext(Qt.WidgetShortcut) paste_action.setShortcut('Ctrl+V') paste_action.setShortcutContext(Qt.WidgetShortcut) comment_action.setShortcut(QKeySequence(Qt.ALT + Qt.Key_Q)) comment_action.setShortcutContext(Qt.WidgetShortcut) self._execute_all_action.triggered.connect( self._controller.execute_script) self._execute_selected_action.triggered.connect( partial(self._controller.execute_script, True)) self._clear_output_action.triggered.connect( partial(self._controller.set_output_text, '')) # open_settings_folder_action.triggered.connect(self._open_settings) # show_shortcuts_action.triggered.connect(self._open_shortcuts) # print_help_action.triggered.connect(self.editor_help) save_session_action.triggered.connect( self._controller.save_current_session) save_script_action.triggered.connect(self._controller.save_script) load_script_action.triggered.connect(self._controller.load_script) undo_action.triggered.connect(self._scripts_tab.undo) redo_action.triggered.connect(self._scripts_tab.redo) copy_action.triggered.connect(self._scripts_tab.copy) cut_action.triggered.connect(self._scripts_tab.cut) paste_action.triggered.connect(self._scripts_tab.paste) tab_to_spaces_action.triggered.connect( self._controller.convert_tab_to_spaces) # find_and_replace.triggered.connect(self._open_find_replace) comment_action.triggered.connect(self._scripts_tab.comment) return menubar
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 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 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 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)