Beispiel #1
0
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)
Beispiel #2
0
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()
Beispiel #3
0
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)
Beispiel #4
0
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)