class TabButton(QToolButton):
    def __init__(self, parent=None, **kwargs):
        QToolButton.__init__(self, parent, **kwargs)
        self.setToolButtonStyle(Qt.ToolButtonIconOnly)
        self.setCheckable(True)

        self.__flat = True
        self.__showMenuIndicator = False

    def setFlat(self, flat):
        if self.__flat != flat:
            self.__flat = flat
            self.update()

    def flat(self):
        return self.__flat

    flat_ = Property(bool, fget=flat, fset=setFlat, designable=True)

    def setShownMenuIndicator(self, show):
        if self.__showMenuIndicator != show:
            self.__showMenuIndicator = show
            self.update()

    def showMenuIndicator(self):
        return self.__showMenuIndicator

    showMenuIndicator_ = Property(bool,
                                  fget=showMenuIndicator,
                                  fset=setShownMenuIndicator,
                                  designable=True)

    def paintEvent(self, event):
        opt = QStyleOptionToolButton()
        self.initStyleOption(opt)
        if self.__showMenuIndicator and self.isChecked():
            opt.features |= QStyleOptionToolButton.HasMenu
        if self.__flat:
            # Use default widget background/border styling.
            StyledWidget_paintEvent(self, event)

            p = QStylePainter(self)
            p.drawControl(QStyle.CE_ToolButtonLabel, opt)
        else:
            p = QStylePainter(self)
            p.drawComplexControl(QStyle.CC_ToolButton, opt)

    def sizeHint(self):
        opt = QStyleOptionToolButton()
        self.initStyleOption(opt)
        if self.__showMenuIndicator and self.isChecked():
            opt.features |= QStyleOptionToolButton.HasMenu
        style = self.style()

        hint = style.sizeFromContents(QStyle.CT_ToolButton, opt, opt.iconSize,
                                      self)
        return hint
Example #2
0
class LineEditButton(QToolButton):
    """
    A button in the :class:`LineEdit`.
    """
    def __init__(self, parent=None, flat=True, **kwargs):
        # type: (Optional[QWidget], bool, Any) -> None
        super().__init__(parent, **kwargs)

        self.__flat = flat

    def setFlat(self, flat):
        # type: (bool) -> None
        if self.__flat != flat:
            self.__flat = flat
            self.update()

    def flat(self):
        # type: () -> bool
        return self.__flat

    flat_ = Property(bool, fget=flat, fset=setFlat, designable=True)

    def paintEvent(self, event):
        # type: (QPaintEvent) -> None
        if self.__flat:
            opt = QStyleOptionToolButton()
            self.initStyleOption(opt)
            p = QStylePainter(self)
            p.drawControl(QStyle.CE_ToolButtonLabel, opt)
        else:
            super().paintEvent(event)
Example #3
0
    class Obj(QObject):
        _f = None
        def _set(self, val):
            self._f = val
        def _get(self):
            return self._f

        prop = Property(type_, _get, _set)
Example #4
0
class GraphicsTextEdit(GraphicsTextEdit):
    """
    QGraphicsTextItem subclass defining an additional placeholderText
    property (text displayed when no text is set).
    """
    def __init__(self, *args, placeholderText="", **kwargs):
        # type: (Any, str, Any) -> None
        kwargs.setdefault(
            "editTriggers",
            GraphicsTextEdit.DoubleClicked | GraphicsTextEdit.EditKeyPressed)
        super().__init__(*args, **kwargs)
        self.setAcceptHoverEvents(True)
        self.__placeholderText = placeholderText

    def setPlaceholderText(self, text):
        # type: (str) -> None
        """
        Set the placeholder text. This is shown when the item has no text,
        i.e when `toPlainText()` returns an empty string.

        """
        if self.__placeholderText != text:
            self.__placeholderText = text
            if not self.toPlainText():
                self.update()

    def placeholderText(self):
        # type: () -> str
        """
        Return the placeholder text.
        """
        return self.__placeholderText

    placeholderText_ = Property(str,
                                placeholderText,
                                setPlaceholderText,
                                doc="Placeholder text")

    def paint(self, painter, option, widget=None):
        # type: (QPainter, QStyleOptionGraphicsItem, Optional[QWidget]) -> None
        super().paint(painter, option, widget)

        # Draw placeholder text if necessary
        if not (self.toPlainText() and self.toHtml()) and \
                self.__placeholderText and \
                not (self.hasFocus() and \
                     self.textInteractionFlags() & Qt.TextEditable):
            brect = self.boundingRect()
            painter.setFont(self.font())
            metrics = painter.fontMetrics()
            text = metrics.elidedText(self.__placeholderText, Qt.ElideRight,
                                      brect.width())
            color = self.defaultTextColor()
            color.setAlpha(min(color.alpha(), 150))
            painter.setPen(QPen(color))
            painter.drawText(brect, Qt.AlignTop | Qt.AlignLeft, text)
Example #5
0
class GraphicsTextEdit(QGraphicsTextItem):
    """
    QGraphicsTextItem subclass defining an additional placeholderText
    property (text displayed when no text is set).

    """
    def __init__(self, *args, **kwargs):
        QGraphicsTextItem.__init__(self, *args, **kwargs)

        self.__placeholderText = ""

    def setPlaceholderText(self, text):
        """
        Set the placeholder text. This is shown when the item has no text,
        i.e when `toPlainText()` returns an empty string.

        """
        if self.__placeholderText != text:
            self.__placeholderText = text
            if not self.toPlainText():
                self.update()

    def placeholderText(self):
        """
        Return the placeholder text.
        """
        return str(self.__placeholderText)

    placeholderText_ = Property(str,
                                placeholderText,
                                setPlaceholderText,
                                doc="Placeholder text")

    def paint(self, painter, option, widget=None):
        QGraphicsTextItem.paint(self, painter, option, widget)

        # Draw placeholder text if necessary
        if not (self.toPlainText() and self.toHtml()) and \
                self.__placeholderText and \
                not (self.hasFocus() and \
                     self.textInteractionFlags() & Qt.TextEditable):
            brect = self.boundingRect()
            painter.setFont(self.font())
            metrics = painter.fontMetrics()
            text = metrics.elidedText(self.__placeholderText, Qt.ElideRight,
                                      brect.width())
            color = self.defaultTextColor()
            color.setAlpha(min(color.alpha(), 150))
            painter.setPen(QPen(color))
            painter.drawText(brect, Qt.AlignTop | Qt.AlignLeft, text)
class StyleConfigWidget(QWidget):
    DisplayNames = {
        "windowsvista": "Windows (default)",
        "macintosh": "macOS (default)",
        "windows": "MS Windows 9x",
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._current_palette = ""
        form = FormLayout()
        styles = QStyleFactory.keys()
        styles = sorted(
            styles,
            key=cmp_to_key(lambda a, b: 1 if a.lower() == "windows" and b.
                           lower() == "fusion" else (-1 if a.lower(
                           ) == "fusion" and b.lower() == "windows" else 0)))
        styles = [(self.DisplayNames.get(st.lower(), st.capitalize()), st)
                  for st in styles]
        # Default style with empty userData key so it cleared in
        # persistent settings, allowing for default style resolution
        # on application star.
        styles = [("Default", "")] + styles
        self.style_cb = style_cb = QComboBox(objectName="style-cb")
        for name, key in styles:
            self.style_cb.addItem(name, userData=key)

        style_cb.currentIndexChanged.connect(self._style_changed)

        self.colors_cb = colors_cb = QComboBox(objectName="palette-cb")
        colors_cb.addItem("Default", userData="")
        colors_cb.addItem("Breeze Light", userData="breeze-light")
        colors_cb.addItem("Breeze Dark", userData="breeze-dark")
        colors_cb.addItem("Zion Reversed", userData="zion-reversed")
        colors_cb.addItem("Dark", userData="dark")

        form.addRow("Style", style_cb)
        form.addRow("Color theme", colors_cb)
        label = QLabel(
            "<small>Changes will be applied on next application startup.</small>",
            enabled=False,
        )
        label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
        form.addRow(label)
        self.setLayout(form)
        self._update_colors_enabled_state()

        style_cb.currentIndexChanged.connect(self.selectedStyleChanged)
        colors_cb.currentIndexChanged.connect(self.selectedPaletteChanged)

    def _style_changed(self):
        self._update_colors_enabled_state()

    def _update_colors_enabled_state(self):
        current = self.style_cb.currentData(Qt.UserRole)
        enable = current is not None and current.lower() in ("fusion",
                                                             "windows")
        self._set_palette_enabled(enable)

    def _set_palette_enabled(self, state: bool):
        cb = self.colors_cb
        if cb.isEnabled() != state:
            cb.setEnabled(state)
            if not state:
                current = cb.currentData(Qt.UserRole)
                self._current_palette = current
                cb.setCurrentIndex(-1)
            else:
                index = cb.findData(self._current_palette, Qt.UserRole)
                if index == -1:
                    index = 0
                cb.setCurrentIndex(index)

    def selectedStyle(self) -> str:
        """Return the current selected style key."""
        key = self.style_cb.currentData()
        return key if key is not None else ""

    def setSelectedStyle(self, style: str) -> None:
        """Set the current selected style key."""
        idx = self.style_cb.findData(style, Qt.DisplayRole,
                                     Qt.MatchFixedString)
        if idx == -1:
            idx = 0  # select the default style
        self.style_cb.setCurrentIndex(idx)

    selectedStyleChanged = Signal()
    selectedStyle_ = Property(str,
                              selectedStyle,
                              setSelectedStyle,
                              notify=selectedStyleChanged)

    def selectedPalette(self) -> str:
        """The current selected palette key."""
        key = self.colors_cb.currentData(Qt.UserRole)
        return key if key is not None else ""

    def setSelectedPalette(self, key: str) -> None:
        """Set the current selected palette key."""
        if not self.colors_cb.isEnabled():
            self._current_palette = key
            return
        idx = self.colors_cb.findData(key, Qt.UserRole, Qt.MatchFixedString)
        if idx == -1:
            idx = 0  # select the default color theme
        self.colors_cb.setCurrentIndex(idx)

    selectedPaletteChanged = Signal()
    selectedPalette_ = Property(str,
                                selectedPalette,
                                setSelectedPalette,
                                notify=selectedPaletteChanged)
Example #7
0
class NodeItem(QGraphicsWidget):
    """
    An widget node item in the canvas.
    """

    #: Signal emitted when the scene position of the node has changed.
    positionChanged = Signal()

    #: Signal emitted when the geometry of the channel anchors changes.
    anchorGeometryChanged = Signal()

    #: Signal emitted when the item has been activated (by a mouse double
    #: click or a keyboard)
    activated = Signal()

    #: The item is under the mouse.
    hovered = Signal()

    #: Span of the anchor in degrees
    ANCHOR_SPAN_ANGLE = 90

    #: Z value of the item
    Z_VALUE = 100

    def __init__(self, widget_description=None, parent=None, **kwargs):
        self.__boundingRect = None
        super().__init__(parent, **kwargs)
        self.setFocusPolicy(Qt.ClickFocus)
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
        self.setFlag(QGraphicsItem.ItemHasNoContents, True)
        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.setFlag(QGraphicsItem.ItemIsMovable, True)
        self.setFlag(QGraphicsItem.ItemIsFocusable, True)

        # central body shape item
        self.shapeItem = None

        # in/output anchor items
        self.inputAnchorItem = None
        self.outputAnchorItem = None

        # title text item
        self.captionTextItem = None

        # error, warning, info items
        self.errorItem = None
        self.warningItem = None
        self.infoItem = None

        self.__title = ""
        self.__processingState = 0
        self.__progress = -1
        self.__statusMessage = ""

        self.__error = None
        self.__warning = None
        self.__info = None

        self.__anchorLayout = None
        self.__animationEnabled = False

        self.setZValue(self.Z_VALUE)
        self.setupGraphics()

        self.setWidgetDescription(widget_description)

    @classmethod
    def from_node(cls, node):
        """
        Create an :class:`NodeItem` instance and initialize it from a
        :class:`SchemeNode` instance.

        """
        self = cls()
        self.setWidgetDescription(node.description)
        #        self.setCategoryDescription(node.category)
        return self

    @classmethod
    def from_node_meta(cls, meta_description):
        """
        Create an `NodeItem` instance from a node meta description.
        """
        self = cls()
        self.setWidgetDescription(meta_description)
        return self

    def setupGraphics(self):
        """
        Set up the graphics.
        """
        shape_rect = QRectF(-24, -24, 48, 48)

        self.shapeItem = NodeBodyItem(self)
        self.shapeItem.setShapeRect(shape_rect)
        self.shapeItem.setAnimationEnabled(self.__animationEnabled)

        # Rect for widget's 'ears'.
        anchor_rect = QRectF(-31, -31, 62, 62)
        self.inputAnchorItem = SinkAnchorItem(self)
        input_path = QPainterPath()
        start_angle = 180 - self.ANCHOR_SPAN_ANGLE / 2
        input_path.arcMoveTo(anchor_rect, start_angle)
        input_path.arcTo(anchor_rect, start_angle, self.ANCHOR_SPAN_ANGLE)
        self.inputAnchorItem.setAnchorPath(input_path)

        self.outputAnchorItem = SourceAnchorItem(self)
        output_path = QPainterPath()
        start_angle = self.ANCHOR_SPAN_ANGLE / 2
        output_path.arcMoveTo(anchor_rect, start_angle)
        output_path.arcTo(anchor_rect, start_angle, -self.ANCHOR_SPAN_ANGLE)
        self.outputAnchorItem.setAnchorPath(output_path)

        self.inputAnchorItem.hide()
        self.outputAnchorItem.hide()

        # Title caption item
        self.captionTextItem = NameTextItem(self)

        self.captionTextItem.setPlainText("")
        self.captionTextItem.setPos(0, 33)

        def iconItem(standard_pixmap):
            item = GraphicsIconItem(self,
                                    icon=standard_icon(standard_pixmap),
                                    iconSize=QSize(16, 16))
            item.hide()
            return item

        self.errorItem = iconItem(QStyle.SP_MessageBoxCritical)
        self.warningItem = iconItem(QStyle.SP_MessageBoxWarning)
        self.infoItem = iconItem(QStyle.SP_MessageBoxInformation)

        self.prepareGeometryChange()
        self.__boundingRect = None

    # TODO: Remove the set[Widget|Category]Description. The user should
    # handle setting of icons, title, ...
    def setWidgetDescription(self, desc):
        """
        Set widget description.
        """
        self.widget_description = desc
        if desc is None:
            return

        icon = icon_loader.from_description(desc).get(desc.icon)
        if icon:
            self.setIcon(icon)

        if not self.title():
            self.setTitle(desc.name)

        if desc.inputs:
            self.inputAnchorItem.show()
        if desc.outputs:
            self.outputAnchorItem.show()

        tooltip = NodeItem_toolTipHelper(self)
        self.setToolTip(tooltip)

    def setWidgetCategory(self, desc):
        """
        Set the widget category.
        """
        self.category_description = desc
        if desc and desc.background:
            background = NAMED_COLORS.get(desc.background, desc.background)
            color = QColor(background)
            if color.isValid():
                self.setColor(color)

    def setIcon(self, icon):
        """
        Set the node item's icon (:class:`QIcon`).
        """
        if isinstance(icon, QIcon):
            self.icon_item = GraphicsIconItem(self.shapeItem,
                                              icon=icon,
                                              iconSize=QSize(36, 36))
            self.icon_item.setPos(-18, -18)
        else:
            raise TypeError

    def setColor(self, color, selectedColor=None):
        """
        Set the widget color.
        """
        if selectedColor is None:
            selectedColor = saturated(color, 150)
        palette = create_palette(color, selectedColor)
        self.shapeItem.setPalette(palette)

    def setTitle(self, title):
        """
        Set the node title. The title text is displayed at the bottom of the
        node.

        """
        self.__title = title
        self.__updateTitleText()

    def title(self):
        """
        Return the node title.
        """
        return self.__title

    title_ = Property(str, fget=title, fset=setTitle, doc="Node title text.")

    def setFont(self, font):
        """
        Set the title text font (:class:`QFont`).
        """
        if font != self.font():
            self.prepareGeometryChange()
            self.captionTextItem.setFont(font)
            self.__updateTitleText()

    def font(self):
        """
        Return the title text font.
        """
        return self.captionTextItem.font()

    def setAnimationEnabled(self, enabled):
        """
        Set the node animation enabled state.
        """
        if self.__animationEnabled != enabled:
            self.__animationEnabled = enabled
            self.shapeItem.setAnimationEnabled(enabled)

    def animationEnabled(self):
        """
        Are node animations enabled.
        """
        return self.__animationEnabled

    def setProcessingState(self, state):
        """
        Set the node processing state i.e. the node is processing
        (is busy) or is idle.

        """
        if self.__processingState != state:
            self.__processingState = state
            self.shapeItem.setProcessingState(state)
            if not state:
                # Clear the progress meter.
                self.setProgress(-1)
                if self.__animationEnabled:
                    self.shapeItem.ping()

    def processingState(self):
        """
        The node processing state.
        """
        return self.__processingState

    processingState_ = Property(int,
                                fget=processingState,
                                fset=setProcessingState)

    def setProgress(self, progress):
        """
        Set the node work progress state (number between 0 and 100).
        """
        if progress is None or progress < 0 or not self.__processingState:
            progress = -1

        progress = max(min(progress, 100), -1)
        if self.__progress != progress:
            self.__progress = progress
            self.shapeItem.setProgress(progress)
            self.__updateTitleText()

    def progress(self):
        """
        Return the node work progress state.
        """
        return self.__progress

    progress_ = Property(float,
                         fget=progress,
                         fset=setProgress,
                         doc="Node progress state.")

    def setStatusMessage(self, message):
        """
        Set the node status message text.

        This text is displayed below the node's title.

        """
        if self.__statusMessage != message:
            self.__statusMessage = message
            self.__updateTitleText()

    def statusMessage(self):
        return self.__statusMessage

    def setStateMessage(self, message):
        """
        Set a state message to display over the item.

        Parameters
        ----------
        message : UserMessage
            Message to display. `message.severity` is used to determine
            the icon and `message.contents` is used as a tool tip.

        """
        # TODO: Group messages by message_id not by severity
        # and deprecate set[Error|Warning|Error]Message
        if message.severity == UserMessage.Info:
            self.setInfoMessage(message.contents)
        elif message.severity == UserMessage.Warning:
            self.setWarningMessage(message.contents)
        elif message.severity == UserMessage.Error:
            self.setErrorMessage(message.contents)

    def setErrorMessage(self, message):
        if self.__error != message:
            self.__error = message
            self.__updateMessages()

    def setWarningMessage(self, message):
        if self.__warning != message:
            self.__warning = message
            self.__updateMessages()

    def setInfoMessage(self, message):
        if self.__info != message:
            self.__info = message
            self.__updateMessages()

    def newInputAnchor(self):
        """
        Create and return a new input :class:`AnchorPoint`.
        """
        if not (self.widget_description and self.widget_description.inputs):
            raise ValueError("Widget has no inputs.")

        anchor = AnchorPoint()
        self.inputAnchorItem.addAnchor(anchor, position=1.0)

        positions = self.inputAnchorItem.anchorPositions()
        positions = uniform_linear_layout(positions)
        self.inputAnchorItem.setAnchorPositions(positions)

        return anchor

    def removeInputAnchor(self, anchor):
        """
        Remove input anchor.
        """
        self.inputAnchorItem.removeAnchor(anchor)

        positions = self.inputAnchorItem.anchorPositions()
        positions = uniform_linear_layout(positions)
        self.inputAnchorItem.setAnchorPositions(positions)

    def newOutputAnchor(self):
        """
        Create and return a new output :class:`AnchorPoint`.
        """
        if not (self.widget_description and self.widget_description.outputs):
            raise ValueError("Widget has no outputs.")

        anchor = AnchorPoint(self)
        self.outputAnchorItem.addAnchor(anchor, position=1.0)

        positions = self.outputAnchorItem.anchorPositions()
        positions = uniform_linear_layout(positions)
        self.outputAnchorItem.setAnchorPositions(positions)

        return anchor

    def removeOutputAnchor(self, anchor):
        """
        Remove output anchor.
        """
        self.outputAnchorItem.removeAnchor(anchor)

        positions = self.outputAnchorItem.anchorPositions()
        positions = uniform_linear_layout(positions)
        self.outputAnchorItem.setAnchorPositions(positions)

    def inputAnchors(self):
        """
        Return a list of all input anchor points.
        """
        return self.inputAnchorItem.anchorPoints()

    def outputAnchors(self):
        """
        Return a list of all output anchor points.
        """
        return self.outputAnchorItem.anchorPoints()

    def setAnchorRotation(self, angle):
        """
        Set the anchor rotation.
        """
        self.inputAnchorItem.setRotation(angle)
        self.outputAnchorItem.setRotation(angle)
        self.anchorGeometryChanged.emit()

    def anchorRotation(self):
        """
        Return the anchor rotation.
        """
        return self.inputAnchorItem.rotation()

    def boundingRect(self):
        # TODO: Important because of this any time the child
        # items change geometry the self.prepareGeometryChange()
        # needs to be called.
        if self.__boundingRect is None:
            self.__boundingRect = self.childrenBoundingRect()
        return self.__boundingRect

    def shape(self):
        # Shape for mouse hit detection.
        # TODO: Should this return the union of all child items?
        return self.shapeItem.shape()

    def __updateTitleText(self):
        """
        Update the title text item.
        """
        text = ['<div align="center">%s' % escape(self.title())]

        status_text = []

        progress_included = False
        if self.__statusMessage:
            msg = escape(self.__statusMessage)
            format_fields = dict(parse_format_fields(msg))
            if "progress" in format_fields and len(format_fields) == 1:
                # Insert progress into the status text format string.
                spec, _ = format_fields["progress"]
                if spec != None:
                    progress_included = True
                    progress_str = "{0:.0f}%".format(self.progress())
                    status_text.append(msg.format(progress=progress_str))
            else:
                status_text.append(msg)

        if self.progress() >= 0 and not progress_included:
            status_text.append("%i%%" % int(self.progress()))

        if status_text:
            text += [
                "<br/>", '<span style="font-style: italic">',
                "<br/>".join(status_text), "</span>"
            ]
        text += ["</div>"]
        text = "".join(text)

        # The NodeItems boundingRect could change.
        self.prepareGeometryChange()
        self.__boundingRect = None
        self.captionTextItem.setHtml(text)
        self.captionTextItem.document().adjustSize()
        width = self.captionTextItem.textWidth()
        self.captionTextItem.setPos(-width / 2.0, 33)

    def __updateMessages(self):
        """
        Update message items (position, visibility and tool tips).
        """
        items = [self.errorItem, self.warningItem, self.infoItem]

        messages = [self.__error, self.__warning, self.__info]
        for message, item in zip(messages, items):
            item.setVisible(bool(message))
            item.setToolTip(message or "")

        shown = [item for item in items if item.isVisible()]
        count = len(shown)
        if count:
            spacing = 3
            rects = [item.boundingRect() for item in shown]
            width = sum(rect.width() for rect in rects)
            width += spacing * max(0, count - 1)
            height = max(rect.height() for rect in rects)
            origin = self.shapeItem.boundingRect().top() - spacing - height
            origin = QPointF(-width / 2, origin)
            for item, rect in zip(shown, rects):
                item.setPos(origin)
                origin = origin + QPointF(rect.width() + spacing, 0)

    def mousePressEvent(self, event):
        if self.shapeItem.path().contains(event.pos()):
            return super().mousePressEvent(event)
        else:
            event.ignore()

    def mouseDoubleClickEvent(self, event):
        if self.shapeItem.path().contains(event.pos()):
            super().mouseDoubleClickEvent(event)
            QTimer.singleShot(0, self.activated.emit)
        else:
            event.ignore()

    def contextMenuEvent(self, event):
        if self.shapeItem.path().contains(event.pos()):
            return super().contextMenuEvent(event)
        else:
            event.ignore()

    def focusInEvent(self, event):
        self.shapeItem.setHasFocus(True)
        return super().focusInEvent(event)

    def focusOutEvent(self, event):
        self.shapeItem.setHasFocus(False)
        return super().focusOutEvent(event)

    def changeEvent(self, event):
        if event.type() == QEvent.PaletteChange:
            self.__updatePalette()
        elif event.type() == QEvent.FontChange:
            self.__updateFont()
        super().changeEvent(event)

    def itemChange(self, change, value):
        if change == QGraphicsItem.ItemSelectedChange:
            self.shapeItem.setSelected(value)
            self.captionTextItem.setSelectionState(value)
        elif change == QGraphicsItem.ItemPositionHasChanged:
            self.positionChanged.emit()

        return super().itemChange(change, value)

    def __updatePalette(self):
        self.captionTextItem.setPalette(self.palette())

    def __updateFont(self):
        self.prepareGeometryChange()
        self.captionTextItem.setFont(self.font())
        self.__updateTitleText()
Example #8
0
class CanvasView(QGraphicsView):
    """Canvas View handles the zooming.
    """
    def __init__(self, *args):
        super().__init__(*args)
        self.setAlignment(Qt.AlignTop | Qt.AlignLeft)

        self.__backgroundIcon = QIcon()

        self.__autoScroll = False
        self.__autoScrollMargin = 16
        self.__autoScrollTimer = QTimer(self)
        self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance)

        # scale factor accumulating partial increments from wheel events
        self.__zoomLevel = 100
        # effective scale level(rounded to whole integers)
        self.__effectiveZoomLevel = 100

        self.__zoomInAction = QAction(
            self.tr("Zoom in"),
            self,
            objectName="action-zoom-in",
            shortcut=QKeySequence.ZoomIn,
            triggered=self.zoomIn,
        )

        self.__zoomOutAction = QAction(self.tr("Zoom out"),
                                       self,
                                       objectName="action-zoom-out",
                                       shortcut=QKeySequence.ZoomOut,
                                       triggered=self.zoomOut)
        self.__zoomResetAction = QAction(
            self.tr("Reset Zoom"),
            self,
            objectName="action-zoom-reset",
            triggered=self.zoomReset,
            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0))

    def setScene(self, scene):
        super().setScene(scene)
        self._ensureSceneRect(scene)

    def _ensureSceneRect(self, scene):
        r = scene.addRect(QRectF(0, 0, 400, 400))
        scene.sceneRect()
        scene.removeItem(r)

    def setAutoScrollMargin(self, margin):
        self.__autoScrollMargin = margin

    def autoScrollMargin(self):
        return self.__autoScrollMargin

    def setAutoScroll(self, enable):
        self.__autoScroll = enable

    def autoScroll(self):
        return self.__autoScroll

    def mousePressEvent(self, event):
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            if not self.__autoScrollTimer.isActive() and \
                    self.__shouldAutoScroll(event.pos()):
                self.__startAutoScroll()
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() & Qt.LeftButton:
            self.__stopAutoScroll()
        return super().mouseReleaseEvent(event)

    def wheelEvent(self, event: QWheelEvent):
        if event.modifiers() & Qt.ControlModifier \
                and event.buttons() == Qt.NoButton:
            delta = event.angleDelta().y()
            # use mouse position as anchor while zooming
            anchor = self.transformationAnchor()
            self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
            self.__setZoomLevel(self.__zoomLevel + 10 * delta / 120)
            self.setTransformationAnchor(anchor)
            event.accept()
        else:
            super().wheelEvent(event)

    def zoomIn(self):
        self.__setZoomLevel(self.__zoomLevel + 10)

    def zoomOut(self):
        self.__setZoomLevel(self.__zoomLevel - 10)

    def zoomReset(self):
        """
        Reset the zoom level.
        """
        self.__setZoomLevel(100)

    def zoomLevel(self):
        # type: () -> float
        """
        Return the current zoom level.

        Level is expressed in percentages; 100 is unscaled, 50 is half size, ...
        """
        return self.__effectiveZoomLevel

    def setZoomLevel(self, level):
        self.__setZoomLevel(level)

    def __setZoomLevel(self, scale):
        # type: (float) -> None
        self.__zoomLevel = max(30, min(scale, 300))
        scale = round(self.__zoomLevel)
        self.__zoomOutAction.setEnabled(scale != 30)
        self.__zoomInAction.setEnabled(scale != 300)
        if self.__effectiveZoomLevel != scale:
            self.__effectiveZoomLevel = scale
            transform = QTransform()
            transform.scale(scale / 100, scale / 100)
            self.setTransform(transform)
            self.zoomLevelChanged.emit(scale)

    zoomLevelChanged = Signal(float)
    zoomLevel_ = Property(float,
                          zoomLevel,
                          setZoomLevel,
                          notify=zoomLevelChanged)

    def __shouldAutoScroll(self, pos):
        if self.__autoScroll:
            margin = self.__autoScrollMargin
            viewrect = self.contentsRect()
            rect = viewrect.adjusted(margin, margin, -margin, -margin)
            # only do auto scroll when on the viewport's margins
            return not rect.contains(pos) and viewrect.contains(pos)
        else:
            return False

    def __startAutoScroll(self):
        self.__autoScrollTimer.start(10)
        log.debug("Auto scroll timer started")

    def __stopAutoScroll(self):
        if self.__autoScrollTimer.isActive():
            self.__autoScrollTimer.stop()
            log.debug("Auto scroll timer stopped")

    def __autoScrollAdvance(self):
        """Advance the auto scroll
        """
        pos = QCursor.pos()
        pos = self.mapFromGlobal(pos)
        margin = self.__autoScrollMargin

        vvalue = self.verticalScrollBar().value()
        hvalue = self.horizontalScrollBar().value()

        vrect = QRect(0, 0, self.width(), self.height())

        # What should be the speed
        advance = 10

        # We only do auto scroll if the mouse is inside the view.
        if vrect.contains(pos):
            if pos.x() < vrect.left() + margin:
                self.horizontalScrollBar().setValue(hvalue - advance)
            if pos.y() < vrect.top() + margin:
                self.verticalScrollBar().setValue(vvalue - advance)
            if pos.x() > vrect.right() - margin:
                self.horizontalScrollBar().setValue(hvalue + advance)
            if pos.y() > vrect.bottom() - margin:
                self.verticalScrollBar().setValue(vvalue + advance)

            if self.verticalScrollBar().value() == vvalue and \
                    self.horizontalScrollBar().value() == hvalue:
                self.__stopAutoScroll()
        else:
            self.__stopAutoScroll()

        log.debug("Auto scroll advance")

    def setBackgroundIcon(self, icon):
        if not isinstance(icon, QIcon):
            raise TypeError("A QIcon expected.")

        if self.__backgroundIcon != icon:
            self.__backgroundIcon = icon
            self.viewport().update()

    def backgroundIcon(self):
        return QIcon(self.__backgroundIcon)

    def drawBackground(self, painter, rect):
        super().drawBackground(painter, rect)

        if not self.__backgroundIcon.isNull():
            painter.setClipRect(rect)
            vrect = QRect(QPoint(0, 0), self.viewport().size())
            vrect = self.mapToScene(vrect).boundingRect()

            pm = self.__backgroundIcon.pixmap(vrect.size().toSize().boundedTo(
                QSize(200, 200)))
            pmrect = QRect(QPoint(0, 0), pm.size())
            pmrect.moveCenter(vrect.center().toPoint())
            if rect.toRect().intersects(pmrect):
                painter.drawPixmap(pmrect, pm)
Example #9
0
class CanvasView(QGraphicsView):
    """Canvas View handles the zooming.
    """
    def __init__(self, *args):
        super().__init__(*args)
        self.setAlignment(Qt.AlignTop | Qt.AlignLeft)
        self.grabGesture(Qt.PinchGesture)
        self.__backgroundIcon = QIcon()

        self.__autoScroll = False
        self.__autoScrollMargin = 16
        self.__autoScrollTimer = QTimer(self)
        self.__autoScrollTimer.timeout.connect(self.__autoScrollAdvance)

        # scale factor accumulating partial increments from wheel events
        self.__zoomLevel = 100
        # effective scale level(rounded to whole integers)
        self.__effectiveZoomLevel = 100

        self.__zoomInAction = QAction(
            self.tr("Zoom in"),
            self,
            objectName="action-zoom-in",
            shortcut=QKeySequence.ZoomIn,
            triggered=self.zoomIn,
        )

        self.__zoomOutAction = QAction(self.tr("Zoom out"),
                                       self,
                                       objectName="action-zoom-out",
                                       shortcut=QKeySequence.ZoomOut,
                                       triggered=self.zoomOut)
        self.__zoomResetAction = QAction(
            self.tr("Reset Zoom"),
            self,
            objectName="action-zoom-reset",
            triggered=self.zoomReset,
            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0))

    def setScene(self, scene):
        super().setScene(scene)
        self._ensureSceneRect(scene)

    def _ensureSceneRect(self, scene):
        r = scene.addRect(QRectF(0, 0, 400, 400))
        scene.sceneRect()
        scene.removeItem(r)

    def setAutoScrollMargin(self, margin):
        self.__autoScrollMargin = margin

    def autoScrollMargin(self):
        return self.__autoScrollMargin

    def setAutoScroll(self, enable):
        self.__autoScroll = enable

    def autoScroll(self):
        return self.__autoScroll

    def mousePressEvent(self, event):
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            if not self.__autoScrollTimer.isActive() and \
                    self.__shouldAutoScroll(event.pos()):
                self.__startAutoScroll()
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() & Qt.LeftButton:
            self.__stopAutoScroll()
        return super().mouseReleaseEvent(event)

    def __should_scroll_horizontally(self, event: QWheelEvent):
        if event.source() != Qt.MouseEventNotSynthesized:
            return False
        if (event.modifiers() & Qt.ShiftModifier and sys.platform == 'darwin'
                or event.modifiers() & Qt.AltModifier
                and sys.platform != 'darwin'):
            return True
        if event.angleDelta().x() == 0:
            vBar = self.verticalScrollBar()
            yDelta = event.angleDelta().y()
            direction = yDelta >= 0
            edgeVBarValue = vBar.minimum() if direction else vBar.maximum()
            return vBar.value() == edgeVBarValue
        return False

    def wheelEvent(self, event: QWheelEvent):
        # Zoom
        if event.modifiers() & Qt.ControlModifier \
                and event.buttons() == Qt.NoButton:
            delta = event.angleDelta().y()
            # use mouse position as anchor while zooming
            anchor = self.transformationAnchor()
            self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
            self.__setZoomLevel(self.__zoomLevel + 10 * delta / 120)
            self.setTransformationAnchor(anchor)
            event.accept()
        # Scroll horizontally
        elif self.__should_scroll_horizontally(event):
            x, y = event.angleDelta().x(), event.angleDelta().y()
            sign_value = x if x != 0 else y
            sign = 1 if sign_value >= 0 else -1
            new_angle_delta = QPoint(sign * max(abs(x), abs(y), sign_value), 0)
            new_pixel_delta = QPoint(0, 0)
            new_modifiers = event.modifiers() & ~(Qt.ShiftModifier
                                                  | Qt.AltModifier)
            new_event = QWheelEvent(event.pos(), event.globalPos(),
                                    new_pixel_delta, new_angle_delta,
                                    event.buttons(), new_modifiers,
                                    event.phase(), event.inverted(),
                                    event.source())
            event.accept()
            super().wheelEvent(new_event)
        else:
            super().wheelEvent(event)

    def gestureEvent(self, event: QGestureEvent):
        gesture = event.gesture(Qt.PinchGesture)
        if gesture is None:
            return
        if gesture.state() == Qt.GestureStarted:
            event.accept(gesture)
        elif gesture.changeFlags() & QPinchGesture.ScaleFactorChanged:
            anchor = gesture.centerPoint().toPoint()
            anchor = self.mapToScene(anchor)
            self.__setZoomLevel(self.__zoomLevel * gesture.scaleFactor(),
                                anchor=anchor)
            event.accept()
        elif gesture.state() == Qt.GestureFinished:
            event.accept()

    def event(self, event: QEvent) -> bool:
        if event.type() == QEvent.Gesture:
            self.gestureEvent(cast(QGestureEvent, event))
        return super().event(event)

    def zoomIn(self):
        self.__setZoomLevel(self.__zoomLevel + 10)

    def zoomOut(self):
        self.__setZoomLevel(self.__zoomLevel - 10)

    def zoomReset(self):
        """
        Reset the zoom level.
        """
        self.__setZoomLevel(100)

    def zoomLevel(self):
        # type: () -> float
        """
        Return the current zoom level.

        Level is expressed in percentages; 100 is unscaled, 50 is half size, ...
        """
        return self.__effectiveZoomLevel

    def setZoomLevel(self, level):
        self.__setZoomLevel(level)

    def __setZoomLevel(self, scale, anchor=None):
        # type: (float, Optional[QPointF]) -> None
        self.__zoomLevel = max(30, min(scale, 300))
        scale = round(self.__zoomLevel)
        self.__zoomOutAction.setEnabled(scale != 30)
        self.__zoomInAction.setEnabled(scale != 300)
        if self.__effectiveZoomLevel != scale:
            self.__effectiveZoomLevel = scale
            transform = QTransform()
            transform.scale(scale / 100, scale / 100)
            if anchor is not None:
                anchor = self.mapFromScene(anchor)
            self.setTransform(transform)
            if anchor is not None:
                center = self.viewport().rect().center()
                diff = self.mapToScene(center) - self.mapToScene(anchor)
                self.centerOn(anchor + diff)
            self.zoomLevelChanged.emit(scale)

    zoomLevelChanged = Signal(float)
    zoomLevel_ = Property(float,
                          zoomLevel,
                          setZoomLevel,
                          notify=zoomLevelChanged)

    def __shouldAutoScroll(self, pos):
        if self.__autoScroll:
            margin = self.__autoScrollMargin
            viewrect = self.contentsRect()
            rect = viewrect.adjusted(margin, margin, -margin, -margin)
            # only do auto scroll when on the viewport's margins
            return not rect.contains(pos) and viewrect.contains(pos)
        else:
            return False

    def __startAutoScroll(self):
        self.__autoScrollTimer.start(10)
        log.debug("Auto scroll timer started")

    def __stopAutoScroll(self):
        if self.__autoScrollTimer.isActive():
            self.__autoScrollTimer.stop()
            log.debug("Auto scroll timer stopped")

    def __autoScrollAdvance(self):
        """Advance the auto scroll
        """
        pos = QCursor.pos()
        pos = self.mapFromGlobal(pos)
        margin = self.__autoScrollMargin

        vvalue = self.verticalScrollBar().value()
        hvalue = self.horizontalScrollBar().value()

        vrect = QRect(0, 0, self.width(), self.height())

        # What should be the speed
        advance = 10

        # We only do auto scroll if the mouse is inside the view.
        if vrect.contains(pos):
            if pos.x() < vrect.left() + margin:
                self.horizontalScrollBar().setValue(hvalue - advance)
            if pos.y() < vrect.top() + margin:
                self.verticalScrollBar().setValue(vvalue - advance)
            if pos.x() > vrect.right() - margin:
                self.horizontalScrollBar().setValue(hvalue + advance)
            if pos.y() > vrect.bottom() - margin:
                self.verticalScrollBar().setValue(vvalue + advance)

            if self.verticalScrollBar().value() == vvalue and \
                    self.horizontalScrollBar().value() == hvalue:
                self.__stopAutoScroll()
        else:
            self.__stopAutoScroll()

        log.debug("Auto scroll advance")

    def setBackgroundIcon(self, icon):
        if not isinstance(icon, QIcon):
            raise TypeError("A QIcon expected.")

        if self.__backgroundIcon != icon:
            self.__backgroundIcon = icon
            self.viewport().update()

    def backgroundIcon(self):
        return QIcon(self.__backgroundIcon)

    def drawBackground(self, painter, rect):
        super().drawBackground(painter, rect)

        if not self.__backgroundIcon.isNull():
            painter.setClipRect(rect)
            vrect = QRect(QPoint(0, 0), self.viewport().size())
            vrect = self.mapToScene(vrect).boundingRect()

            pm = self.__backgroundIcon.pixmap(vrect.size().toSize().boundedTo(
                QSize(200, 200)))
            pmrect = QRect(QPoint(0, 0), pm.size())
            pmrect.moveCenter(vrect.center().toPoint())
            if rect.toRect().intersects(pmrect):
                painter.drawPixmap(pmrect, pm)
Example #10
0
class ToolBox(QFrame):
    """
    A tool box widget.
    """
    # Emitted when a tab is toggled.
    tabToogled = Signal(int, bool)

    def setExclusive(self, exclusive):
        """
        Set exclusive tabs (only one tab can be open at a time).
        """
        if self.__exclusive != exclusive:
            self.__exclusive = exclusive
            self.__tabActionGroup.setExclusive(exclusive)
            checked = self.__tabActionGroup.checkedAction()
            if checked is None:
                # The action group can be out of sync with the actions state
                # when switching between exclusive states.
                actions_checked = [page.action for page in self.__pages
                                   if page.action.isChecked()]
                if actions_checked:
                    checked = actions_checked[0]

            # Trigger/toggle remaining open pages
            if exclusive and checked is not None:
                for page in self.__pages:
                    if checked != page.action and page.action.isChecked():
                        page.action.trigger()

    def exclusive(self):
        """
        Are the tabs in the toolbox exclusive.
        """
        return self.__exclusive

    exclusive_ = Property(bool,
                          fget=exclusive,
                          fset=setExclusive,
                          designable=True,
                          doc="Exclusive tabs")

    def __init__(self, parent=None, **kwargs):
        QFrame.__init__(self, parent, **kwargs)

        self.__pages = []
        self.__tabButtonHeight = -1
        self.__tabIconSize = QSize()
        self.__exclusive = False
        self.__setupUi()

    def __setupUi(self):
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)

        # Scroll area for the contents.
        self.__scrollArea = \
                _ToolBoxScrollArea(self, objectName="toolbox-scroll-area")

        self.__scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.__scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.__scrollArea.setSizePolicy(QSizePolicy.MinimumExpanding,
                                        QSizePolicy.MinimumExpanding)
        self.__scrollArea.setFrameStyle(QScrollArea.NoFrame)
        self.__scrollArea.setWidgetResizable(True)

        # A widget with all of the contents.
        # The tabs/contents are placed in the layout inside this widget
        self.__contents = QWidget(self.__scrollArea,
                                  objectName="toolbox-contents")
        self.__contentsLayout = _ToolBoxLayout(
            sizeConstraint=_ToolBoxLayout.SetMinAndMaxSize,
            spacing=0
        )
        self.__contentsLayout.setContentsMargins(0, 0, 0, 0)
        self.__contents.setLayout(self.__contentsLayout)

        self.__scrollArea.setWidget(self.__contents)

        layout.addWidget(self.__scrollArea)

        self.setLayout(layout)
        self.setSizePolicy(QSizePolicy.Fixed,
                           QSizePolicy.MinimumExpanding)

        self.__tabActionGroup = \
                QActionGroup(self, objectName="toolbox-tab-action-group")

        self.__tabActionGroup.setExclusive(self.__exclusive)

        self.__actionMapper = QSignalMapper(self)
        self.__actionMapper.mapped[QObject].connect(self.__onTabActionToogled)

    def setTabButtonHeight(self, height):
        """
        Set the tab button height.
        """
        if self.__tabButtonHeight != height:
            self.__tabButtonHeight = height
            for page in self.__pages:
                page.button.setFixedHeight(height)

    def tabButtonHeight(self):
        """
        Return the tab button height.
        """
        return self.__tabButtonHeight

    def setTabIconSize(self, size):
        """
        Set the tab button icon size.
        """
        if self.__tabIconSize != size:
            self.__tabIconSize = size
            for page in self.__pages:
                page.button.setIconSize(size)

    def tabIconSize(self):
        """
        Return the tab icon size.
        """
        return self.__tabIconSize

    def tabButton(self, index):
        """
        Return the tab button at `index`
        """
        return self.__pages[index].button

    def tabAction(self, index):
        """
        Return open/close action for the tab at `index`.
        """
        return self.__pages[index].action

    def addItem(self, widget, text, icon=None, toolTip=None):
        """
        Append the `widget` in a new tab and return its index.

        Parameters
        ----------
        widget : :class:`QWidget`
            A widget to be inserted. The toolbox takes ownership
            of the widget.

        text : str
            Name/title of the new tab.

        icon : :class:`QIcon`, optional
            An icon for the tab button.

        toolTip : str, optional
            Tool tip for the tab button.

        """
        return self.insertItem(self.count(), widget, text, icon, toolTip)

    def insertItem(self, index, widget, text, icon=None, toolTip=None):
        """
        Insert the `widget` in a new tab at position `index`.

        See also
        --------
        ToolBox.addItem

        """
        button = self.createTabButton(widget, text, icon, toolTip)

        self.__contentsLayout.insertWidget(index * 2, button)
        self.__contentsLayout.insertWidget(index * 2 + 1, widget)

        widget.hide()

        page = _ToolBoxPage(index, widget, button.defaultAction(), button)
        self.__pages.insert(index, page)

        for i in range(index + 1, self.count()):
            self.__pages[i] = self.__pages[i]._replace(index=i)

        self.__updatePositions()

        # Show (open) the first tab.
        if self.count() == 1 and index == 0:
            page.action.trigger()

        self.__updateSelected()

        self.updateGeometry()
        return index

    def removeItem(self, index):
        """
        Remove the widget at `index`.

        .. note:: The widget hidden but is is not deleted.

        """
        self.__contentsLayout.takeAt(2 * index + 1)
        self.__contentsLayout.takeAt(2 * index)
        page = self.__pages.pop(index)

        # Update the page indexes
        for i in range(index, self.count()):
            self.__pages[i] = self.__pages[i]._replace(index=i)

        page.button.deleteLater()

        # Hide the widget and reparent to self
        # This follows QToolBox.removeItem
        page.widget.hide()
        page.widget.setParent(self)

        self.__updatePositions()
        self.__updateSelected()

        self.updateGeometry()

    def count(self):
        """
        Return the number of widgets inserted in the toolbox.
        """
        return len(self.__pages)

    def widget(self, index):
        """
        Return the widget at `index`.
        """
        return self.__pages[index].widget

    def createTabButton(self, widget, text, icon=None, toolTip=None):
        """
        Create the tab button for `widget`.
        """
        action = QAction(text, self)
        action.setCheckable(True)

        if icon:
            action.setIcon(icon)

        if toolTip:
            action.setToolTip(toolTip)
        self.__tabActionGroup.addAction(action)
        self.__actionMapper.setMapping(action, action)
        action.toggled.connect(self.__actionMapper.map)

        button = ToolBoxTabButton(self, objectName="toolbox-tab-button")
        button.setDefaultAction(action)
        button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        button.setSizePolicy(QSizePolicy.Ignored,
                             QSizePolicy.Fixed)

        if self.__tabIconSize.isValid():
            button.setIconSize(self.__tabIconSize)

        if self.__tabButtonHeight > 0:
            button.setFixedHeight(self.__tabButtonHeight)

        return button

    def ensureWidgetVisible(self, child, xmargin=50, ymargin=50):
        """
        Scroll the contents so child widget instance is visible inside
        the viewport.

        """
        self.__scrollArea.ensureWidgetVisible(child, xmargin, ymargin)

    def sizeHint(self):
        hint = self.__contentsLayout.sizeHint()

        if self.count():
            # Compute max width of hidden widgets also.
            scroll = self.__scrollArea
            scroll_w = scroll.verticalScrollBar().sizeHint().width()
            frame_w = self.frameWidth() * 2 + scroll.frameWidth() * 2
            max_w = max([p.widget.sizeHint().width() for p in self.__pages])
            hint = QSize(max(max_w, hint.width()) + scroll_w + frame_w,
                         hint.height())

        return QSize(200, 200).expandedTo(hint)

    def __onTabActionToogled(self, action):
        page = find(self.__pages, action, key=attrgetter("action"))
        on = action.isChecked()
        page.widget.setVisible(on)
        index = page.index

        if index > 0:
            # Update the `previous` tab buttons style hints
            previous = self.__pages[index - 1].button
            flag = QStyleOptionToolBox.NextIsSelected
            if on:
                previous.selected |= flag
            else:
                previous.selected &= ~flag

            previous.update()

        if index < self.count() - 1:
            next = self.__pages[index + 1].button
            flag = QStyleOptionToolBox.PreviousIsSelected
            if on:
                next.selected |= flag
            else:
                next.selected &= ~flag

            next.update()

        self.tabToogled.emit(index, on)

        self.__contentsLayout.invalidate()

    def __updateSelected(self):
        """Update the tab buttons selected style flags.
        """
        if self.count() == 0:
            return

        opt = QStyleOptionToolBox

        def update(button, next_sel, prev_sel):
            if next_sel:
                button.selected |= opt.NextIsSelected
            else:
                button.selected &= ~opt.NextIsSelected

            if prev_sel:
                button.selected |= opt.PreviousIsSelected
            else:
                button.selected &= ~ opt.PreviousIsSelected

            button.update()

        if self.count() == 1:
            update(self.__pages[0].button, False, False)
        elif self.count() >= 2:
            pages = self.__pages
            for i in range(1, self.count() - 1):
                update(pages[i].button,
                       pages[i + 1].action.isChecked(),
                       pages[i - 1].action.isChecked())

    def __updatePositions(self):
        """Update the tab buttons position style flags.
        """
        if self.count() == 0:
            return
        elif self.count() == 1:
            self.__pages[0].button.position = QStyleOptionToolBox.OnlyOneTab
        else:
            self.__pages[0].button.position = QStyleOptionToolBox.Beginning
            self.__pages[-1].button.position = QStyleOptionToolBox.End
            for p in self.__pages[1:-1]:
                p.button.position = QStyleOptionToolBox.Middle

        for p in self.__pages:
            p.button.update()
Example #11
0
class FeatureEditor(QFrame):
    FUNCTIONS = dict(chain([(key, val) for key, val in math.__dict__.items()
                            if not key.startswith("_")],
                           [("str", str)]))
    featureChanged = Signal()
    featureEdited = Signal()

    modifiedChanged = Signal(bool)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        layout = QFormLayout(
            fieldGrowthPolicy=QFormLayout.ExpandingFieldsGrow
        )
        layout.setContentsMargins(0, 0, 0, 0)
        self.nameedit = QLineEdit(
            placeholderText="Name...",
            sizePolicy=QSizePolicy(QSizePolicy.Minimum,
                                   QSizePolicy.Fixed)
        )
        self.expressionedit = QLineEdit(
            placeholderText="Expression..."
        )

        self.attrs_model = itemmodels.VariableListModel(
            ["Select Feature"], parent=self)
        self.attributescb = QComboBox(
            minimumContentsLength=16,
            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon,
            sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
        )
        self.attributescb.setModel(self.attrs_model)

        sorted_funcs = sorted(self.FUNCTIONS)
        self.funcs_model = itemmodels.PyListModelTooltip()
        self.funcs_model.setParent(self)

        self.funcs_model[:] = chain(["Select Function"], sorted_funcs)
        self.funcs_model.tooltips[:] = chain(
            [''],
            [self.FUNCTIONS[func].__doc__ for func in sorted_funcs])

        self.functionscb = QComboBox(
            minimumContentsLength=16,
            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon,
            sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum))
        self.functionscb.setModel(self.funcs_model)

        hbox = QHBoxLayout()
        hbox.addWidget(self.attributescb)
        hbox.addWidget(self.functionscb)

        layout.addRow(self.nameedit, self.expressionedit)
        layout.addRow(self.tr(""), hbox)
        self.setLayout(layout)

        self.nameedit.editingFinished.connect(self._invalidate)
        self.expressionedit.textChanged.connect(self._invalidate)
        self.attributescb.currentIndexChanged.connect(self.on_attrs_changed)
        self.functionscb.currentIndexChanged.connect(self.on_funcs_changed)

        self._modified = False

    def setModified(self, modified):
        if not type(modified) is bool:
            raise TypeError

        if self._modified != modified:
            self._modified = modified
            self.modifiedChanged.emit(modified)

    def modified(self):
        return self._modified

    modified = Property(bool, modified, setModified,
                        notify=modifiedChanged)

    def setEditorData(self, data, domain):
        self.nameedit.setText(data.name)
        self.expressionedit.setText(data.expression)
        self.setModified(False)
        self.featureChanged.emit()
        self.attrs_model[:] = ["Select Feature"]
        if domain is not None and (domain or domain.metas):
            self.attrs_model[:] += chain(domain.attributes,
                                         domain.class_vars,
                                         domain.metas)

    def editorData(self):
        return FeatureDescriptor(name=self.nameedit.text(),
                                 expression=self.nameedit.text())

    def _invalidate(self):
        self.setModified(True)
        self.featureEdited.emit()
        self.featureChanged.emit()

    def on_attrs_changed(self):
        index = self.attributescb.currentIndex()
        if index > 0:
            attr = sanitized_name(self.attrs_model[index].name)
            self.insert_into_expression(attr)
            self.attributescb.setCurrentIndex(0)

    def on_funcs_changed(self):
        index = self.functionscb.currentIndex()
        if index > 0:
            func = self.funcs_model[index]
            if func in ["atan2", "fmod", "ldexp", "log",
                        "pow", "copysign", "hypot"]:
                self.insert_into_expression(func + "(,)")
                self.expressionedit.cursorBackward(False, 2)
            elif func in ["e", "pi"]:
                self.insert_into_expression(func)
            else:
                self.insert_into_expression(func + "()")
                self.expressionedit.cursorBackward(False)
            self.functionscb.setCurrentIndex(0)

    def insert_into_expression(self, what):
        cp = self.expressionedit.cursorPosition()
        ct = self.expressionedit.text()
        text = ct[:cp] + what + ct[cp:]
        self.expressionedit.setText(text)
        self.expressionedit.setFocus()
class Scheme(QObject):
    """
    An :class:`QObject` subclass representing the scheme widget workflow
    with annotations.

    Parameters
    ----------
    parent : :class:`QObject`
        A parent QObject item (default `None`).
    title : str
        The scheme title.
    description : str
        A longer description of the scheme.


    Attributes
    ----------
    nodes : list of :class:`.SchemeNode`
        A list of all the nodes in the scheme.

    links : list of :class:`.SchemeLink`
        A list of all links in the scheme.

    annotations : list of :class:`BaseSchemeAnnotation`
        A list of all the annotations in the scheme.

    """

    # Signal emitted when a `node` is added to the scheme.
    node_added = Signal(SchemeNode)

    # Signal emitted when a `node` is removed from the scheme.
    node_removed = Signal(SchemeNode)

    # Signal emitted when a `link` is added to the scheme.
    link_added = Signal(SchemeLink)

    # Signal emitted when a `link` is removed from the scheme.
    link_removed = Signal(SchemeLink)

    # Signal emitted when a `annotation` is added to the scheme.
    annotation_added = Signal(BaseSchemeAnnotation)

    # Signal emitted when a `annotation` is removed from the scheme.
    annotation_removed = Signal(BaseSchemeAnnotation)

    # Signal emitted when the title of scheme changes.
    title_changed = Signal(str)

    # Signal emitted when the description of scheme changes.
    description_changed = Signal(str)

    node_state_changed = Signal()
    channel_state_changed = Signal()
    topology_changed = Signal()

    #: Emitted when the associated runtime environment changes
    #: runtime_env_changed(key: str, newvalue: Option[str],
    #:                     oldvalue: Option[str])
    runtime_env_changed = Signal(str, object, object)

    def __init__(self, parent=None, title=None, description=None, env={}):
        QObject.__init__(self, parent)

        self.__title = title or ""
        "Workflow title (empty string by default)."

        self.__description = description or ""
        "Workflow description (empty string by default)."

        self.__annotations = []
        self.__nodes = []
        self.__links = []
        self.__env = dict(env)

    @property
    def nodes(self):
        """
        A list of all nodes (:class:`.SchemeNode`) currently in the scheme.
        """
        return list(self.__nodes)

    @property
    def links(self):
        """
        A list of all links (:class:`.SchemeLink`) currently in the scheme.
        """
        return list(self.__links)

    @property
    def annotations(self):
        """
        A list of all annotations (:class:`.BaseSchemeAnnotation`) in the
        scheme.

        """
        return list(self.__annotations)

    def set_title(self, title):
        """
        Set the scheme title text.
        """
        if self.__title != title:
            self.__title = title
            self.title_changed.emit(title)

    def title(self):
        """
        The title (human readable string) of the scheme.
        """
        return self.__title

    title = Property(str, fget=title, fset=set_title)

    def set_description(self, description):
        """
        Set the scheme description text.
        """
        if self.__description != description:
            self.__description = description
            self.description_changed.emit(description)

    def description(self):
        """
        Scheme description text.
        """
        return self.__description

    description = Property(str, fget=description, fset=set_description)

    def add_node(self, node):
        """
        Add a node to the scheme. An error is raised if the node is
        already in the scheme.

        Parameters
        ----------
        node : :class:`.SchemeNode`
            Node instance to add to the scheme.

        """
        check_arg(node not in self.__nodes,
                  "Node already in scheme.")
        check_type(node, SchemeNode)

        self.__nodes.append(node)
        log.info("Added node %r to scheme %r." % (node.title, self.title))
        self.node_added.emit(node)

    def new_node(self, description, title=None, position=None,
                 properties=None):
        """
        Create a new :class:`.SchemeNode` and add it to the scheme.

        Same as::

            scheme.add_node(SchemeNode(description, title, position,
                                       properties))

        Parameters
        ----------
        description : :class:`WidgetDescription`
            The new node's description.
        title : str, optional
            Optional new nodes title. By default `description.name` is used.
        position : `(x, y)` tuple of floats, optional
            Optional position in a 2D space.
        properties : dict, optional
            A dictionary of optional extra properties.

        See also
        --------
        .SchemeNode, Scheme.add_node

        """
        if isinstance(description, WidgetDescription):
            node = SchemeNode(description, title=title, position=position,
                              properties=properties)
        else:
            raise TypeError("Expected %r, got %r." % \
                            (WidgetDescription, type(description)))

        self.add_node(node)
        return node

    def remove_node(self, node):
        """
        Remove a `node` from the scheme. All links into and out of the
        `node` are also removed. If the node in not in the scheme an error
        is raised.

        Parameters
        ----------
        node : :class:`.SchemeNode`
            Node instance to remove.

        """
        check_arg(node in self.__nodes,
                  "Node is not in the scheme.")

        self.__remove_node_links(node)
        self.__nodes.remove(node)
        log.info("Removed node %r from scheme %r." % (node.title, self.title))
        self.node_removed.emit(node)
        return node

    def __remove_node_links(self, node):
        """
        Remove all links for node.
        """
        links_in, links_out = [], []
        for link in self.__links:
            if link.source_node is node:
                links_out.append(link)
            elif link.sink_node is node:
                links_in.append(link)

        for link in links_out + links_in:
            self.remove_link(link)

    def add_link(self, link):
        """
        Add a `link` to the scheme.

        Parameters
        ----------
        link : :class:`.SchemeLink`
            An initialized link instance to add to the scheme.

        """
        check_type(link, SchemeLink)

        self.check_connect(link)
        self.__links.append(link)

        log.info("Added link %r (%r) -> %r (%r) to scheme %r." % \
                 (link.source_node.title, link.source_channel.name,
                  link.sink_node.title, link.sink_channel.name,
                  self.title)
                 )

        self.link_added.emit(link)

    def new_link(self, source_node, source_channel,
                 sink_node, sink_channel):
        """
        Create a new :class:`.SchemeLink` from arguments and add it to
        the scheme. The new link is returned.

        Parameters
        ----------
        source_node : :class:`.SchemeNode`
            Source node of the new link.
        source_channel : :class:`.OutputSignal`
            Source channel of the new node. The instance must be from
            ``source_node.output_channels()``
        sink_node : :class:`.SchemeNode`
            Sink node of the new link.
        sink_channel : :class:`.InputSignal`
            Sink channel of the new node. The instance must be from
            ``sink_node.input_channels()``

        See also
        --------
        .SchemeLink, Scheme.add_link

        """
        link = SchemeLink(source_node, source_channel,
                          sink_node, sink_channel)
        self.add_link(link)
        return link

    def remove_link(self, link):
        """
        Remove a link from the scheme.

        Parameters
        ----------
        link : :class:`.SchemeLink`
            Link instance to remove.

        """
        check_arg(link in self.__links,
                  "Link is not in the scheme.")

        self.__links.remove(link)
        log.info("Removed link %r (%r) -> %r (%r) from scheme %r." % \
                 (link.source_node.title, link.source_channel.name,
                  link.sink_node.title, link.sink_channel.name,
                  self.title)
                 )
        self.link_removed.emit(link)

    def check_connect(self, link):
        """
        Check if the `link` can be added to the scheme and raise an
        appropriate exception.

        Can raise:
            - :class:`TypeError` if `link` is not an instance of
              :class:`.SchemeLink`
            - :class:`.SchemeCycleError` if the `link` would introduce a cycle
            - :class:`.IncompatibleChannelTypeError` if the channel types are
              not compatible
            - :class:`.SinkChannelError` if a sink channel has a `Single` flag
              specification and the channel is already connected.
            - :class:`.DuplicatedLinkError` if a `link` duplicates an already
              present link.

        """
        check_type(link, SchemeLink)

        if self.creates_cycle(link):
            raise SchemeCycleError("Cannot create cycles in the scheme")

        if not self.compatible_channels(link):
            raise IncompatibleChannelTypeError(
                    "Cannot connect %r to %r." \
                    % (link.source_channel.type, link.sink_channel.type)
                )

        links = self.find_links(source_node=link.source_node,
                                source_channel=link.source_channel,
                                sink_node=link.sink_node,
                                sink_channel=link.sink_channel)

        if links:
            raise DuplicatedLinkError(
                    "A link from %r (%r) -> %r (%r) already exists" \
                    % (link.source_node.title, link.source_channel.name,
                       link.sink_node.title, link.sink_channel.name)
                )

        if link.sink_channel.single:
            links = self.find_links(sink_node=link.sink_node,
                                    sink_channel=link.sink_channel)
            if links:
                raise SinkChannelError(
                        "%r is already connected." % link.sink_channel.name
                    )

    def creates_cycle(self, link):
        """
        Return `True` if `link` would introduce a cycle in the scheme.

        Parameters
        ----------
        link : :class:`.SchemeLink`

        """
        check_type(link, SchemeLink)
        source_node, sink_node = link.source_node, link.sink_node
        upstream = self.upstream_nodes(source_node)
        upstream.add(source_node)
        return sink_node in upstream

    def compatible_channels(self, link):
        """
        Return `True` if the channels in `link` have compatible types.

        Parameters
        ----------
        link : :class:`.SchemeLink`

        """
        check_type(link, SchemeLink)
        return compatible_channels(link.source_channel, link.sink_channel)

    def can_connect(self, link):
        """
        Return `True` if `link` can be added to the scheme.

        See also
        --------
        Scheme.check_connect

        """
        check_type(link, SchemeLink)
        try:
            self.check_connect(link)
            return True
        except (SchemeCycleError, IncompatibleChannelTypeError,
                SinkChannelError, DuplicatedLinkError):
            return False

    def upstream_nodes(self, start_node):
        """
        Return a set of all nodes upstream from `start_node` (i.e.
        all ancestor nodes).

        Parameters
        ----------
        start_node : :class:`.SchemeNode`

        """
        visited = set()
        queue = deque([start_node])
        while queue:
            node = queue.popleft()
            snodes = [link.source_node for link in self.input_links(node)]
            for source_node in snodes:
                if source_node not in visited:
                    queue.append(source_node)

            visited.add(node)
        visited.remove(start_node)
        return visited

    def downstream_nodes(self, start_node):
        """
        Return a set of all nodes downstream from `start_node`.

        Parameters
        ----------
        start_node : :class:`.SchemeNode`

        """
        visited = set()
        queue = deque([start_node])
        while queue:
            node = queue.popleft()
            snodes = [link.sink_node for link in self.output_links(node)]
            for source_node in snodes:
                if source_node not in visited:
                    queue.append(source_node)

            visited.add(node)
        visited.remove(start_node)
        return visited

    def is_ancestor(self, node, child):
        """
        Return True if `node` is an ancestor node of `child` (is upstream
        of the child in the workflow). Both nodes must be in the scheme.

        Parameters
        ----------
        node : :class:`.SchemeNode`
        child : :class:`.SchemeNode`

        """
        return child in self.downstream_nodes(node)

    def children(self, node):
        """
        Return a set of all children of `node`.
        """
        return set(link.sink_node for link in self.output_links(node))

    def parents(self, node):
        """
        Return a set of all parents of `node`.
        """
        return set(link.source_node for link in self.input_links(node))

    def input_links(self, node):
        """
        Return a list of all input links (:class:`.SchemeLink`) connected
        to the `node` instance.

        """
        return self.find_links(sink_node=node)

    def output_links(self, node):
        """
        Return a list of all output links (:class:`.SchemeLink`) connected
        to the `node` instance.

        """
        return self.find_links(source_node=node)

    def find_links(self, source_node=None, source_channel=None,
                   sink_node=None, sink_channel=None):
        # TODO: Speedup - keep index of links by nodes and channels
        result = []
        match = lambda query, value: (query is None or value == query)
        for link in self.__links:
            if match(source_node, link.source_node) and \
                    match(sink_node, link.sink_node) and \
                    match(source_channel, link.source_channel) and \
                    match(sink_channel, link.sink_channel):
                result.append(link)

        return result

    def propose_links(self, source_node, sink_node):
        """
        Return a list of ordered (:class:`OutputSignal`,
        :class:`InputSignal`, weight) tuples that could be added to
        the scheme between `source_node` and `sink_node`.

        .. note:: This can depend on the links already in the scheme.

        """
        if source_node is sink_node or \
                self.is_ancestor(sink_node, source_node):
            # Cyclic connections are not possible.
            return []

        outputs = source_node.output_channels()
        inputs = sink_node.input_channels()

        # Get existing links to sink channels that are Single.
        links = self.find_links(None, None, sink_node)
        already_connected_sinks = [link.sink_channel for link in links \
                                   if link.sink_channel.single]

        def weight(out_c, in_c):
            # type: (OutputSignal, InputSignal) -> int
            if out_c.explicit or in_c.explicit:
                # Zero weight for explicit links
                weight = 0
            else:
                # Does the connection type check (can only ever be False for
                # dynamic signals)
                type_checks = issubclass(name_lookup(out_c.type),
                                         name_lookup(in_c.type))
                assert type_checks or out_c.dynamic
                # Dynamic signals that require runtime instance type check
                # are considered last.
                check = [type_checks,
                         in_c not in already_connected_sinks,
                         bool(in_c.default),
                         bool(out_c.default)
                         ]
                weights = [2 ** i for i in range(len(check), 0, -1)]
                weight = sum([w for w, c in zip(weights, check) if c])
            return weight

        proposed_links = []
        for out_c in outputs:
            for in_c in inputs:
                if compatible_channels(out_c, in_c):
                    proposed_links.append((out_c, in_c, weight(out_c, in_c)))

        return sorted(proposed_links, key=itemgetter(-1), reverse=True)

    def add_annotation(self, annotation):
        """
        Add an annotation (:class:`BaseSchemeAnnotation` subclass) instance
        to the scheme.

        """
        check_arg(annotation not in self.__annotations,
                  "Cannot add the same annotation multiple times.")
        check_type(annotation, BaseSchemeAnnotation)

        self.__annotations.append(annotation)
        self.annotation_added.emit(annotation)

    def remove_annotation(self, annotation):
        """
        Remove the `annotation` instance from the scheme.
        """
        check_arg(annotation in self.__annotations,
                  "Annotation is not in the scheme.")
        self.__annotations.remove(annotation)
        self.annotation_removed.emit(annotation)

    def clear(self):
        """
        Remove all nodes, links, and annotation items from the scheme.
        """
        def is_terminal(node):
            return not bool(self.find_links(source_node=node))

        while self.nodes:
            terminal_nodes = list(filter(is_terminal, self.nodes))
            for node in terminal_nodes:
                self.remove_node(node)

        for annotation in self.annotations:
            self.remove_annotation(annotation)

        assert(not (self.nodes or self.links or self.annotations))

    def sync_node_properties(self):
        """
        Called before saving, allowing a subclass to update/sync.

        The default implementation does nothing.

        """
        pass

    def save_to(self, stream, pretty=True, pickle_fallback=False):
        """
        Save the scheme as an xml formated file to `stream`

        See also
        --------
        .scheme_to_ows_stream

        """
        if isinstance(stream, str):
            stream = open(stream, "wb")

        self.sync_node_properties()

        readwrite.scheme_to_ows_stream(self, stream, pretty,
                                       pickle_fallback=pickle_fallback)

    def load_from(self, stream):
        """
        Load the scheme from xml formated stream.
        """
        if self.__nodes or self.__links or self.__annotations:
            # TODO: should we clear the scheme and load it.
            raise ValueError("Scheme is not empty.")

        if isinstance(stream, str):
            stream = open(stream, "rb")
        readwrite.scheme_load(self, stream)
#         parse_scheme(self, stream)

    def set_runtime_env(self, key, value):
        """
        Set a runtime environment variable `key` to `value`
        """
        oldvalue = self.__env.get(key, None)
        if value != oldvalue:
            self.__env[key] = value
            self.runtime_env_changed.emit(key, value, oldvalue)

    def get_runtime_env(self, key, default=None):
        """
        Return a runtime environment variable for `key`.
        """
        return self.__env.get(key, default)

    def runtime_env(self):
        """
        Return (a view to) the full runtime environment.

        The return value is a types.MappingProxyType of the
        underlying environment dictionary. Changes to the env.
        will be reflected in it.
        """
        return types.MappingProxyType(self.__env)

    def dump_settings(self, node: SchemeNode):
        """Dump current settings of the `node` to the standard output"""
        print(node.properties)
class SchemeArrowAnnotation(BaseSchemeAnnotation):
    """
    An arrow annotation in the scheme.
    """

    color_changed = Signal(str)

    def __init__(self,
                 start_pos,
                 end_pos,
                 color="red",
                 anchor=None,
                 parent=None):
        # type: (Pos, Pos, str, Any, Optional[QObject]) -> None
        super().__init__(parent)
        self.__start_pos = start_pos
        self.__end_pos = end_pos
        self.__color = color
        self.__anchor = anchor

    def set_line(self, start_pos, end_pos):
        # type: (Pos, Pos) -> None
        """
        Set arrow lines start and end position (``(x, y)`` tuples).
        """
        if self.__start_pos != start_pos or self.__end_pos != end_pos:
            self.__start_pos = start_pos
            self.__end_pos = end_pos
            self.geometry_changed.emit()

    def start_pos(self):
        # type: () -> Pos
        """
        Start position of the arrow (base point).
        """
        return self.__start_pos

    start_pos = Property(tuple, fget=start_pos)  # type: ignore

    def end_pos(self):
        """
        End position of the arrow (arrow head points toward the end).
        """
        return self.__end_pos

    end_pos = Property(tuple, fget=end_pos)  # type: ignore

    def set_geometry(self, geometry):
        # type: (Tuple[Pos, Pos]) -> None
        """
        Set the geometry of the arrow as a start and end position tuples
        (e.g. ``set_geometry(((0, 0), (100, 0))``).

        """
        (start_pos, end_pos) = geometry
        self.set_line(start_pos, end_pos)

    def geometry(self):
        # type: () -> Tuple[Pos, Pos]
        """
        Return the start and end positions of the arrow.
        """
        return (self.start_pos, self.end_pos)

    geometry = Property(tuple, fget=geometry,
                        fset=set_geometry)  # type: ignore

    def set_color(self, color):
        # type: (str) -> None
        """
        Set the fill color for the arrow as a string (`#RGB`, `#RRGGBB`,
        `#RRRGGGBBB`, `#RRRRGGGGBBBB` format or one of SVG color keyword
        names).

        """
        if self.__color != color:
            self.__color = color
            self.color_changed.emit(color)

    def color(self):
        # type: () -> str
        """
        The arrow's fill color.
        """
        return self.__color

    color = Property(str, fget=color, fset=set_color)  # type: ignore
Example #14
0
class CollapsibleDockWidget(QDockWidget):
    """
    This :class:`QDockWidget` subclass overrides the `close` header
    button to instead collapse to a smaller size. The contents contents
    to show when in each state can be set using the ``setExpandedWidget``
    and ``setCollapsedWidget``.

    .. note:: Do  not use the base class ``QDockWidget.setWidget`` method
              to set the docks contents. Use set[Expanded|Collapsed]Widget
              instead.

    """

    #: Emitted when the dock widget's expanded state changes.
    expandedChanged = Signal(bool)

    def __init__(self, *args, **kwargs):
        QDockWidget.__init__(self, *args, **kwargs)

        self.__expandedWidget = None
        self.__collapsedWidget = None
        self.__expanded = True

        self.__trueMinimumWidth = -1

        self.setFeatures(QDockWidget.DockWidgetClosable | \
                         QDockWidget.DockWidgetMovable)
        self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)

        self.featuresChanged.connect(self.__onFeaturesChanged)
        self.dockLocationChanged.connect(self.__onDockLocationChanged)

        # Use the toolbar horizontal extension button icon as the default
        # for the expand/collapse button
        icon = self.style().standardIcon(
            QStyle.SP_ToolBarHorizontalExtensionButton)

        # Mirror the icon
        transform = QTransform()
        transform = transform.scale(-1.0, 1.0)
        icon_rev = QIcon()
        for s in (8, 12, 14, 16, 18, 24, 32, 48, 64):
            pm = icon.pixmap(s, s)
            icon_rev.addPixmap(pm.transformed(transform))

        self.__iconRight = QIcon(icon)
        self.__iconLeft = QIcon(icon_rev)

        close = self.findChild(QAbstractButton,
                               name="qt_dockwidget_closebutton")

        close.installEventFilter(self)
        self.__closeButton = close

        self.__stack = AnimatedStackedWidget()

        self.__stack.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)

        self.__stack.transitionStarted.connect(self.__onTransitionStarted)
        self.__stack.transitionFinished.connect(self.__onTransitionFinished)

        QDockWidget.setWidget(self, self.__stack)

        self.__closeButton.setIcon(self.__iconLeft)

    def setExpanded(self, state):
        """
        Set the widgets `expanded` state.
        """
        if self.__expanded != state:
            self.__expanded = state
            if state and self.__expandedWidget is not None:
                log.debug("Dock expanding.")
                self.__stack.setCurrentWidget(self.__expandedWidget)
            elif not state and self.__collapsedWidget is not None:
                log.debug("Dock collapsing.")
                self.__stack.setCurrentWidget(self.__collapsedWidget)
            self.__fixIcon()

            self.expandedChanged.emit(state)

    def expanded(self):
        """
        Is the dock widget in expanded state. If `True` the
        ``expandedWidget`` will be shown, and ``collapsedWidget`` otherwise.

        """
        return self.__expanded

    expanded_ = Property(bool, fset=setExpanded, fget=expanded)

    def setWidget(self, w):
        raise NotImplementedError(
            "Use the 'setExpandedWidget'/'setCollapsedWidget' "
            "methods to set the contents of the dock widget.")

    def setExpandedWidget(self, widget):
        """
        Set the widget with contents to show while expanded.
        """
        if widget is self.__expandedWidget:
            return

        if self.__expandedWidget is not None:
            self.__stack.removeWidget(self.__expandedWidget)

        self.__stack.insertWidget(0, widget)
        self.__expandedWidget = widget

        if self.__expanded:
            self.__stack.setCurrentWidget(widget)
            self.updateGeometry()

    def expandedWidget(self):
        """
        Return the widget previously set with ``setExpandedWidget``,
        or ``None`` if no widget has been set.

        """
        return self.__expandedWidget

    def setCollapsedWidget(self, widget):
        """
        Set the widget with contents to show while collapsed.
        """
        if widget is self.__collapsedWidget:
            return

        if self.__collapsedWidget is not None:
            self.__stack.removeWidget(self.__collapsedWidget)

        self.__stack.insertWidget(1, widget)
        self.__collapsedWidget = widget

        if not self.__expanded:
            self.__stack.setCurrentWidget(widget)
            self.updateGeometry()

    def collapsedWidget(self):
        """
        Return the widget previously set with ``setCollapsedWidget``,
        or ``None`` if no widget has been set.

        """
        return self.__collapsedWidget

    def setAnimationEnabled(self, animationEnabled):
        """
        Enable/disable the transition animation.
        """
        self.__stack.setAnimationEnabled(animationEnabled)

    def animationEnabled(self):
        """
        Is transition animation enabled.
        """
        return self.__stack.animationEnabled()

    def currentWidget(self):
        """
        Return the current shown widget depending on the `expanded` state.
        """
        if self.__expanded:
            return self.__expandedWidget
        else:
            return self.__collapsedWidget

    def expand(self):
        """
        Expand the dock (same as ``setExpanded(True)``)
        """
        self.setExpanded(True)

    def collapse(self):
        """
        Collapse the dock (same as ``setExpanded(False)``)
        """
        self.setExpanded(False)

    def eventFilter(self, obj, event):
        if obj is self.__closeButton:
            etype = event.type()
            if etype == QEvent.MouseButtonPress:
                self.setExpanded(not self.__expanded)
                return True
            elif etype == QEvent.MouseButtonDblClick or \
                    etype == QEvent.MouseButtonRelease:
                return True
            # TODO: which other events can trigger the button (is the button
            # focusable).

        return QDockWidget.eventFilter(self, obj, event)

    def event(self, event):
        if event.type() == QEvent.LayoutRequest:
            self.__fixMinimumWidth()

        return QDockWidget.event(self, event)

    def __onFeaturesChanged(self, features):
        pass

    def __onDockLocationChanged(self, area):
        if area == Qt.LeftDockWidgetArea:
            self.setLayoutDirection(Qt.LeftToRight)
        else:
            self.setLayoutDirection(Qt.RightToLeft)

        self.__stack.setLayoutDirection(self.parentWidget().layoutDirection())
        self.__fixIcon()

    def __onTransitionStarted(self):
        log.debug("Dock transition started.")

    def __onTransitionFinished(self):
        log.debug("Dock transition finished (new width %i)",
                  self.size().width())

    def __fixMinimumWidth(self):
        # A workaround for forcing the QDockWidget layout to disregard the
        # default minimumSize which can be to wide for us (overriding the
        # minimumSizeHint or setting the minimum size directly does not
        # seem to have an effect (Qt 4.8.3).
        size = self.__stack.sizeHint()
        if size.isValid() and not size.isEmpty():
            left, _, right, _ = self.getContentsMargins()
            width = size.width() + left + right

            if width < self.minimumSizeHint().width():
                if not self.__hasFixedWidth():
                    log.debug(
                        "Overriding default minimum size "
                        "(setFixedWidth(%i))", width)
                    self.__trueMinimumWidth = self.minimumSizeHint().width()
                self.setFixedWidth(width)
            else:
                if self.__hasFixedWidth():
                    if width >= self.__trueMinimumWidth:
                        # Unset the fixed size.
                        log.debug(
                            "Restoring default minimum size "
                            "(setFixedWidth(%i))", QWIDGETSIZE_MAX)
                        self.__trueMinimumWidth = -1
                        self.setFixedWidth(QWIDGETSIZE_MAX)
                        self.updateGeometry()
                    else:
                        self.setFixedWidth(width)

    def __hasFixedWidth(self):
        return self.__trueMinimumWidth >= 0

    def __fixIcon(self):
        """Fix the dock close icon.
        """
        direction = self.layoutDirection()
        if direction == Qt.LeftToRight:
            if self.__expanded:
                icon = self.__iconLeft
            else:
                icon = self.__iconRight
        else:
            if self.__expanded:
                icon = self.__iconRight
            else:
                icon = self.__iconLeft

        self.__closeButton.setIcon(icon)
Example #15
0
class SchemeNode(QObject):
    """
    A node in a :class:`.Scheme`.

    Parameters
    ----------
    description : :class:`WidgetDescription`
        Node description instance.
    title : str, optional
        Node title string (if None `description.name` is used).
    position : tuple
        (x, y) tuple of floats for node position in a visual display.
    properties : dict
        Additional extra instance properties (settings, widget geometry, ...)
    parent : :class:`QObject`
        Parent object.

    """
    def __init__(self,
                 description,
                 title=None,
                 position=None,
                 properties=None,
                 parent=None):
        QObject.__init__(self, parent)
        self.description = description

        if title is None:
            title = description.name

        self.__title = title
        self.__position = position or (0, 0)
        self.__progress = -1
        self.__processing_state = 0
        self.__status_message = ""
        self.__state_messages = {}
        self.properties = properties or {}

    def input_channels(self):
        """
        Return a list of input channels (:class:`InputSignal`) for the node.
        """
        return list(self.description.inputs)

    def output_channels(self):
        """
        Return a list of output channels (:class:`OutputSignal`) for the node.
        """
        return list(self.description.outputs)

    def input_channel(self, name):
        """
        Return the input channel matching `name`. Raise a `ValueError`
        if not found.

        """
        for channel in self.input_channels():
            if channel.name == name:
                return channel
        raise ValueError("%r is not a valid input channel name for %r." %
                         (name, self.description.name))

    def output_channel(self, name):
        """
        Return the output channel matching `name`. Raise an `ValueError`
        if not found.

        """
        for channel in self.output_channels():
            if channel.name == name:
                return channel
        raise ValueError("%r is not a valid output channel name for %r." %
                         (name, self.description.name))

    #: The title of the node has changed
    title_changed = Signal(str)

    def set_title(self, title):
        """
        Set the node title.
        """
        if self.__title != title:
            self.__title = str(title)
            self.title_changed.emit(self.__title)

    def title(self):
        """
        The node title.
        """
        return self.__title

    title = Property(str, fset=set_title, fget=title)

    #: Position of the node in the scheme has changed
    position_changed = Signal(tuple)

    def set_position(self, pos):
        """
        Set the position (``(x, y)`` tuple) of the node.
        """
        if self.__position != pos:
            self.__position = pos
            self.position_changed.emit(pos)

    def position(self):
        """
        ``(x, y)`` tuple containing the position of the node in the scheme.
        """
        return self.__position

    position = Property(tuple, fset=set_position, fget=position)

    #: Node's progress value has changed.
    progress_changed = Signal(float)

    def set_progress(self, value):
        """
        Set the progress value.
        """
        if self.__progress != value:
            self.__progress = value
            self.progress_changed.emit(value)

    def progress(self):
        """
        The current progress value. -1 if progress is not set.
        """
        return self.__progress

    progress = Property(float, fset=set_progress, fget=progress)

    #: Node's processing state has changed.
    processing_state_changed = Signal(int)

    def set_processing_state(self, state):
        """
        Set the node processing state.
        """
        if self.__processing_state != state:
            self.__processing_state = state
            self.processing_state_changed.emit(state)

    def processing_state(self):
        """
        The node processing state, 0 for not processing, 1 the node is busy.
        """
        return self.__processing_state

    processing_state = Property(int,
                                fset=set_processing_state,
                                fget=processing_state)

    def set_tool_tip(self, tool_tip):
        if self.__tool_tip != tool_tip:
            self.__tool_tip = tool_tip

    def tool_tip(self):
        return self.__tool_tip

    tool_tip = Property(str, fset=set_tool_tip, fget=tool_tip)

    #: The node's status tip has changes
    status_message_changed = Signal(str)

    def set_status_message(self, text):
        if self.__status_message != text:
            self.__status_message = text
            self.status_message_changed.emit(text)

    def status_message(self):
        return self.__status_message

    #: The node's state message has changed
    state_message_changed = Signal(UserMessage)

    def set_state_message(self, message):
        """
        Set a message to be displayed by a scheme view for this node.
        """
        if message.message_id in self.__state_messages and not message.contents:
            del self.__state_messages[message.message_id]

        self.__state_messages[message.message_id] = message
        self.state_message_changed.emit(message)

    def state_messages(self):
        """
        Return a list of all state messages.
        """
        return self.__state_messages.values()

    def __str__(self):
        return "SchemeNode(description_id=%s, title=%r, ...)" % (
            str(self.description.id),
            self.title,
        )

    def __repr__(self):
        return str(self)
Example #16
0
class FramelessWindow(QWidget):
    """
    A basic frameless window widget with rounded corners (if supported by
    the windowing system).
    """
    def __init__(self, parent=None, radius=6, **kwargs):
        # type: (Optional[QWidget], int, Any) -> None
        super().__init__(parent, **kwargs)
        self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)

        self.__radius = radius
        self.__isTransparencySupported = is_transparency_supported()
        self.setAttribute(Qt.WA_TranslucentBackground,
                          self.__isTransparencySupported)

    def setRadius(self, radius):
        # type: (int) -> None
        """
        Set the window rounded border radius.
        """
        if self.__radius != radius:
            self.__radius = radius
            if not self.__isTransparencySupported:
                self.__updateMask()
            self.update()

    def radius(self):
        # type: () -> int
        """
        Return the border radius.
        """
        return self.__radius

    radius_ = Property(int,
                       fget=radius,
                       fset=setRadius,
                       designable=True,
                       doc="Window border radius")

    def resizeEvent(self, event):
        super().resizeEvent(event)
        if not self.__isTransparencySupported:
            self.__updateMask()

    def __updateMask(self):
        # type: () -> None
        opt = QStyleOption()
        opt.initFrom(self)
        rect = opt.rect

        size = rect.size()
        mask = QBitmap(size)

        p = QPainter(mask)
        p.setRenderHint(QPainter.Antialiasing)
        p.setBrush(Qt.black)
        p.setPen(Qt.NoPen)
        p.drawRoundedRect(rect, self.__radius, self.__radius)
        p.end()

        self.setMask(mask)

    def paintEvent(self, event):
        # type: (QPaintEvent) -> None
        if self.__isTransparencySupported:
            opt = QStyleOption()
            opt.initFrom(self)
            rect = opt.rect

            p = QPainter(self)
            p.setRenderHint(QPainter.Antialiasing, True)
            p.setBrush(opt.palette.brush(QPalette.Window))
            p.setPen(Qt.NoPen)
            p.drawRoundedRect(rect, self.__radius, self.__radius)
            p.end()
        else:
            StyledWidget_paintEvent(self, event)
Example #17
0
    class StartItem(QWidget):
        """
        An active item in the bottom row of the welcome screen.
        """
        def __init__(self,
                     *args,
                     text="",
                     icon=QIcon(),
                     iconSize=QSize(),
                     iconActive=QIcon(),
                     **kwargs):
            self.__iconSize = QSize()
            self.__icon = QIcon()
            self.__icon_active = QIcon()
            self.__text = ""
            self.__active = False
            super().__init__(*args, **kwargs)
            self.setAutoFillBackground(True)
            font = self.font()
            font.setPointSize(18)
            self.setFont(font)
            self.setAttribute(Qt.WA_SetFont, False)
            self.setText(text)
            self.setIcon(icon)
            self.setIconSize(iconSize)
            self.setIconActive(iconActive)
            self.installEventFilter(self)

        def iconSize(self):
            if not self.__iconSize.isValid():
                size = self.style().pixelMetric(QStyle.PM_LargeIconSize, None,
                                                self) * 2
                return QSize(size, size)
            else:
                return QSize(self.__iconSize)

        def setIconSize(self, size):
            if size != self.__iconSize:
                self.__iconSize = QSize(size)
                self.updateGeometry()

        iconSize_ = Property(QSize, iconSize, setIconSize, designable=True)

        def icon(self):
            if self.__active:
                return QIcon(self.__icon_active)
            else:
                return QIcon(self.__icon)

        def setIcon(self, icon):
            self.__icon = QIcon(icon)
            self.update()

        icon_ = Property(QIcon, icon, setIcon, designable=True)

        def iconActive(self):
            return QIcon(self.__icon_active)

        def setIconActive(self, icon):
            self.__icon_active = QIcon(icon)
            self.update()

        icon_active_ = Property(QIcon,
                                iconActive,
                                setIconActive,
                                designable=True)

        def sizeHint(self):
            return QSize(200, 150)

        def setText(self, text):
            if self.__text != text:
                self.__text = text
                self.updateGeometry()
                self.update()

        def text(self):
            return self.__text

        text_ = Property(str, text, setText, designable=True)

        def initStyleOption(self, option):
            # type: (QStyleOptionViewItem) -> None
            option.initFrom(self)
            option.backgroundBrush = option.palette.brush(
                self.backgroundRole())
            option.font = self.font()
            option.text = self.text()
            option.icon = self.icon()

            option.decorationPosition = QStyleOptionViewItem.Top
            option.decorationAlignment = Qt.AlignCenter
            option.decorationSize = self.iconSize()
            option.displayAlignment = Qt.AlignCenter
            option.features = (QStyleOptionViewItem.WrapText
                               | QStyleOptionViewItem.HasDecoration
                               | QStyleOptionViewItem.HasDisplay)
            option.showDecorationSelected = True
            option.widget = self

        def paintEvent(self, event):
            style = self.style()  # type: QStyle
            painter = QPainter(self)
            option = QStyleOption()
            option.initFrom(self)
            style.drawPrimitive(QStyle.PE_Widget, option, painter, self)

            option = QStyleOptionViewItem()
            self.initStyleOption(option)
            style.drawControl(QStyle.CE_ItemViewItem, option, painter, self)

        def eventFilter(self, obj, event):
            try:
                if event.type() == QEvent.Enter:
                    self.__active = True
                    self.setCursor(Qt.PointingHandCursor)
                    self.update()
                    return True
                elif event.type() == QEvent.Leave:
                    self.__active = False
                    self.unsetCursor()
                    self.update()
                    return True
            except Exception as ex:
                pass
            return False
Example #18
0
class IconWidget(QWidget):
    """
    A widget displaying an `QIcon`
    """
    def __init__(self, parent=None, icon=QIcon(), iconSize=QSize(), **kwargs):
        sizePolicy = kwargs.pop(
            "sizePolicy", QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
        super().__init__(parent, **kwargs)
        self._opacity = 1
        self.__icon = QIcon(icon)
        self.__iconSize = QSize(iconSize)
        self.setSizePolicy(sizePolicy)

    def setIcon(self, icon):
        # type: (QIcon) -> None
        if self.__icon != icon:
            self.__icon = QIcon(icon)
            self.updateGeometry()
            self.update()

    def getOpacity(self):
        return self._opacity

    def setOpacity(self, o):
        self._opacity = o
        self.update()

    opacity = Property(float, fget=getOpacity, fset=setOpacity)

    def icon(self):
        # type: () -> QIcon
        return QIcon(self.__icon)

    def iconSize(self):
        # type: () -> QSize
        if not self.__iconSize.isValid():
            size = self.style().pixelMetric(QStyle.PM_ButtonIconSize)
            return QSize(size, size)
        else:
            return QSize(self.__iconSize)

    def setIconSize(self, iconSize):
        # type: (QSize) -> None
        if self.__iconSize != iconSize:
            self.__iconSize = QSize(iconSize)
            self.updateGeometry()
            self.update()

    def sizeHint(self):
        sh = self.iconSize()
        m = self.contentsMargins()
        return QSize(sh.width() + m.left() + m.right(),
                     sh.height() + m.top() + m.bottom())

    def paintEvent(self, event):
        painter = QStylePainter(self)
        painter.setOpacity(self._opacity)
        opt = QStyleOption()
        opt.initFrom(self)
        painter.drawPrimitive(QStyle.PE_Widget, opt)
        if not self.__icon.isNull():
            rect = self.contentsRect()
            if opt.state & QStyle.State_Active:
                mode = QIcon.Active
            else:
                mode = QIcon.Disabled
            self.__icon.paint(painter, rect, Qt.AlignCenter, mode, QIcon.Off)
        painter.end()
Example #19
0
class GraphicsTextEdit(QGraphicsTextItem):
    """
    QGraphicsTextItem subclass defining an additional placeholderText
    property (text displayed when no text is set).

    """
    #: Signal emitted when editing operation starts (the item receives edit
    #: focus)
    editingStarted = Signal()
    #: Signal emitted when editing operation ends (the item loses edit focus)
    editingFinished = Signal()

    def __init__(self, *args, placeholderText="", **kwargs):
        # type: (Any, str, Any) -> None
        super().__init__(*args, **kwargs)
        self.setAcceptHoverEvents(True)
        self.__placeholderText = placeholderText
        self.__editing = False  # text editing in progress

    def setPlaceholderText(self, text):
        # type: (str) -> None
        """
        Set the placeholder text. This is shown when the item has no text,
        i.e when `toPlainText()` returns an empty string.

        """
        if self.__placeholderText != text:
            self.__placeholderText = text
            if not self.toPlainText():
                self.update()

    def placeholderText(self):
        # type: () -> str
        """
        Return the placeholder text.
        """
        return self.__placeholderText

    placeholderText_ = Property(str,
                                placeholderText,
                                setPlaceholderText,
                                doc="Placeholder text")

    def paint(self, painter, option, widget=None):
        # type: (QPainter, QStyleOptionGraphicsItem, Optional[QWidget]) -> None
        super().paint(painter, option, widget)

        # Draw placeholder text if necessary
        if not (self.toPlainText() and self.toHtml()) and \
                self.__placeholderText and \
                not (self.hasFocus() and \
                     self.textInteractionFlags() & Qt.TextEditable):
            brect = self.boundingRect()
            painter.setFont(self.font())
            metrics = painter.fontMetrics()
            text = metrics.elidedText(self.__placeholderText, Qt.ElideRight,
                                      brect.width())
            color = self.defaultTextColor()
            color.setAlpha(min(color.alpha(), 150))
            painter.setPen(QPen(color))
            painter.drawText(brect, Qt.AlignTop | Qt.AlignLeft, text)

    def hoverMoveEvent(self, event):
        # type: (QGraphicsSceneHoverEvent) -> None
        layout = self.document().documentLayout()
        if layout.anchorAt(event.pos()):
            self.setCursor(Qt.PointingHandCursor)
        else:
            self.unsetCursor()
        super().hoverMoveEvent(event)

    def mousePressEvent(self, event):
        # type: (QGraphicsSceneMouseEvent) -> None
        flags = self.textInteractionFlags()
        if flags & Qt.LinksAccessibleByMouse \
                and not flags & Qt.TextSelectableByMouse \
                and self.document().documentLayout().anchorAt(event.pos()):
            # QGraphicsTextItem ignores the press event without
            # Qt.TextSelectableByMouse flag set. This causes the
            # corresponding mouse release to never get to this item
            # and therefore no linkActivated/openUrl ...
            super().mousePressEvent(event)
            if not event.isAccepted():
                event.accept()
        else:
            super().mousePressEvent(event)

    def setTextInteractionFlags(self, flags):
        # type: (Union[Qt.TextInteractionFlag, Qt.TextInteractionFlags]) -> None
        super().setTextInteractionFlags(flags)
        if self.hasFocus() and flags & Qt.TextEditable and not self.__editing:
            self.__editing = True
            self.editingStarted.emit()

    def focusInEvent(self, event):
        # type: (QFocusEvent) -> None
        super().focusInEvent(event)
        if self.textInteractionFlags() & Qt.TextEditable and \
                not self.__editing:
            self.__editing = True
            self.editingStarted.emit()

    def focusOutEvent(self, event):
        # type: (QFocusEvent) -> None
        super().focusOutEvent(event)
        if self.__editing and \
                event.reason() not in {Qt.ActiveWindowFocusReason,
                                       Qt.PopupFocusReason}:
            self.__editing = False
            self.editingFinished.emit()
Example #20
0
class DropShadowFrame(QWidget):
    """
    A widget drawing a drop shadow effect around the geometry of
    another widget (works similar to :class:`QFocusFrame`).

    Parameters
    ----------
    parent : :class:`QObject`
        Parent object.
    color : :class:`QColor`
        The color of the drop shadow.
    radius : float
        Shadow radius.

    """
    def __init__(self, parent=None, color=QColor(), radius=5, **kwargs):
        QWidget.__init__(self, parent, **kwargs)
        self.setAttribute(Qt.WA_TransparentForMouseEvents, True)
        self.setAttribute(Qt.WA_NoChildEventsForParent, True)
        self.setFocusPolicy(Qt.NoFocus)

        self.__color = QColor(color)
        self.__radius = radius

        self.__widget = None
        self.__widgetParent = None
        self.__updatePixmap()

    def setColor(self, color):
        """
        Set the color of the shadow.
        """
        if not isinstance(color, QColor):
            color = QColor(color)

        if self.__color != color:
            self.__color = QColor(color)
            self.__updatePixmap()

    def color(self):
        """
        Return the color of the drop shadow.

        By default this is a color from the `palette` (for
        `self.foregroundRole()`)
        """
        if self.__color.isValid():
            return QColor(self.__color)
        else:
            return self.palette().color(self.foregroundRole())

    color_ = Property(QColor,
                      fget=color,
                      fset=setColor,
                      designable=True,
                      doc="Drop shadow color")

    def setRadius(self, radius):
        """
        Set the drop shadow's blur radius.
        """
        if self.__radius != radius:
            self.__radius = radius
            self.__updateGeometry()
            self.__updatePixmap()

    def radius(self):
        """
        Return the shadow blur radius.
        """
        return self.__radius

    radius_ = Property(
        int,
        fget=radius,
        fset=setRadius,
        designable=True,
        doc="Drop shadow blur radius.",
    )

    def setWidget(self, widget):
        """
        Set the widget around which to show the shadow.
        """
        if self.__widget:
            self.__widget.removeEventFilter(self)

        self.__widget = widget

        if self.__widget:
            self.__widget.installEventFilter(self)
            # Find the parent for the frame
            # This is the top level window a toolbar or a viewport
            # of a scroll area
            parent = widget.parentWidget()
            while not (isinstance(parent, (QAbstractScrollArea, QToolBar))
                       or parent.isWindow()):
                parent = parent.parentWidget()

            if isinstance(parent, QAbstractScrollArea):
                parent = parent.viewport()

            self.__widgetParent = parent
            self.setParent(parent)
            self.stackUnder(widget)
            self.__updateGeometry()
            self.setVisible(widget.isVisible())

    def widget(self):
        """
        Return the widget that was set by `setWidget`.
        """
        return self.__widget

    def paintEvent(self, event):
        # TODO: Use QPainter.drawPixmapFragments on Qt 4.7
        opt = QStyleOption()
        opt.initFrom(self)

        pixmap = self.__shadowPixmap

        shadow_rect = QRectF(opt.rect)
        widget_rect = QRectF(self.widget().geometry())
        widget_rect.moveTo(self.radius_, self.radius_)

        left = top = right = bottom = self.radius_
        pixmap_rect = QRectF(QPointF(0, 0), QSizeF(pixmap.size()))

        # Shadow casting rectangle in the source pixmap.
        pixmap_shadow_rect = pixmap_rect.adjusted(left, top, -right, -bottom)
        source_rects = self.__shadowPixmapFragments(pixmap_rect,
                                                    pixmap_shadow_rect)
        target_rects = self.__shadowPixmapFragments(shadow_rect, widget_rect)

        painter = QPainter(self)
        for source, target in zip(source_rects, target_rects):
            painter.drawPixmap(target, pixmap, source)
        painter.end()

    def eventFilter(self, obj, event):
        etype = event.type()
        if etype == QEvent.Move or etype == QEvent.Resize:
            self.__updateGeometry()
        elif etype == QEvent.Show:
            self.__updateGeometry()
            self.show()
        elif etype == QEvent.Hide:
            self.hide()
        return QWidget.eventFilter(self, obj, event)

    def __updateGeometry(self):
        """
        Update the shadow geometry to fit the widget's changed
        geometry.

        """
        widget = self.__widget
        parent = self.__widgetParent
        radius = self.radius_
        pos = widget.pos()
        if parent != widget.parentWidget():
            pos = widget.parentWidget().mapTo(parent, pos)

        geom = QRect(pos, widget.size())
        geom.adjust(-radius, -radius, radius, radius)
        if geom != self.geometry():
            self.setGeometry(geom)

        # Set the widget mask (punch a hole through to the `widget` instance.
        rect = self.rect()

        mask = QRegion(rect)
        transparent = QRegion(rect.adjusted(radius, radius, -radius, -radius))

        mask = mask.subtracted(transparent)
        self.setMask(mask)

    def __updatePixmap(self):
        """
        Update the cached shadow pixmap.
        """
        rect_size = QSize(50, 50)
        left = top = right = bottom = self.radius_

        # Size of the pixmap.
        pixmap_size = QSize(rect_size.width() + left + right,
                            rect_size.height() + top + bottom)
        shadow_rect = QRect(QPoint(left, top), rect_size)
        pixmap = QPixmap(pixmap_size)
        pixmap.fill(QColor(0, 0, 0, 0))
        rect_fill_color = self.palette().color(QPalette.Window)

        pixmap = render_drop_shadow_frame(
            pixmap,
            QRectF(shadow_rect),
            shadow_color=self.color_,
            offset=QPointF(0, 0),
            radius=self.radius_,
            rect_fill_color=rect_fill_color,
        )

        self.__shadowPixmap = pixmap
        self.update()

    def __shadowPixmapFragments(self, pixmap_rect, shadow_rect):
        """
        Return a list of 8 QRectF fragments for drawing a shadow.
        """
        s_left, s_top, s_right, s_bottom = (
            shadow_rect.left(),
            shadow_rect.top(),
            shadow_rect.right(),
            shadow_rect.bottom(),
        )
        s_width, s_height = shadow_rect.width(), shadow_rect.height()
        p_width, p_height = pixmap_rect.width(), pixmap_rect.height()

        top_left = QRectF(0.0, 0.0, s_left, s_top)
        top = QRectF(s_left, 0.0, s_width, s_top)
        top_right = QRectF(s_right, 0.0, p_width - s_width, s_top)
        right = QRectF(s_right, s_top, p_width - s_right, s_height)
        right_bottom = QRectF(shadow_rect.bottomRight(),
                              pixmap_rect.bottomRight())
        bottom = QRectF(
            shadow_rect.bottomLeft(),
            pixmap_rect.bottomRight() - QPointF(p_width - s_right, 0.0),
        )
        bottom_left = QRectF(
            shadow_rect.bottomLeft() - QPointF(s_left, 0.0),
            pixmap_rect.bottomLeft() + QPointF(s_left, 0.0),
        )
        left = QRectF(pixmap_rect.topLeft() + QPointF(0.0, s_top),
                      shadow_rect.bottomLeft())
        return [
            top_left,
            top,
            top_right,
            right,
            right_bottom,
            bottom,
            bottom_left,
            left,
        ]
class SchemeTextAnnotation(BaseSchemeAnnotation):
    """
    Text annotation in the scheme.
    """

    # Signal emitted when the annotation content change.
    content_changed = Signal(str, str)

    # Signal emitted when the annotation text changes.
    text_changed = Signal(str)

    # Signal emitted when the annotation text font changes.
    font_changed = Signal(dict)

    def __init__(self,
                 rect,
                 text="",
                 content_type="text/plain",
                 font=None,
                 anchor=None,
                 parent=None):
        # type: (Rect, str, str, Optional[dict], Any, Optional[QObject]) -> None
        super().__init__(parent)
        self.__rect = rect  # type: Rect
        self.__content = text
        self.__content_type = content_type
        self.__font = {} if font is None else font
        self.__anchor = anchor

    def set_rect(self, rect):
        # type: (Rect) -> None
        """
        Set the text geometry bounding rectangle (``(x, y, width, height)``
        tuple).
        """
        if self.__rect != rect:
            self.__rect = rect
            self.geometry_changed.emit()

    def rect(self):
        # type: () -> Rect
        """
        Text bounding rectangle
        """
        return self.__rect

    rect = Property(tuple, fget=rect, fset=set_rect)  # type: ignore

    def set_geometry(self, rect):
        # type: (Rect) -> None
        """
        Set the text geometry (same as ``set_rect``)
        """
        self.set_rect(rect)

    def geometry(self):
        # type: () -> Rect
        """
        Text annotation geometry (same as ``rect``)
        """
        return self.__rect

    geometry = Property(tuple, fget=geometry,
                        fset=set_geometry)  # type: ignore

    def set_text(self, text):
        # type: (str) -> None
        """
        Set the annotation text.

        Same as `set_content(text, "text/plain")`
        """
        self.set_content(text, "text/plain")

    def text(self):
        # type: () -> str
        """
        Annotation text.

        .. deprecated::
            Use `content` instead.
        """
        return self.__content

    text = Property(str, fget=text, fset=set_text)  # type: ignore

    @property
    def content_type(self):
        # type: () -> str
        """
        Return the annotations' content type.

        Currently this will be 'text/plain', 'text/html' or 'text/rst'.
        """
        return self.__content_type

    @property
    def content(self):
        # type: () -> str
        """
        The annotation content.

        How the content is interpreted/displayed depends on `content_type`.
        """
        return self.__content

    def set_content(self, content, content_type="text/plain"):
        # type: (str, str) -> None
        """
        Set the annotation content.

        Parameters
        ----------
        content : str
            The content.
        content_type : str
            Content type. Currently supported are 'text/plain' 'text/html'
            (subset supported by `QTextDocument`) and `text/rst`.
        """
        if self.__content != content or self.__content_type != content_type:
            text_changed = self.__content != content
            self.__content = content
            self.__content_type = content_type
            self.content_changed.emit(content, content_type)
            if text_changed:
                self.text_changed.emit(content)

    def set_font(self, font):
        # type: (dict) -> None
        """
        Set the annotation's default font as a dictionary of font properties
        (at the moment only family and size are used).

            >>> annotation.set_font({"family": "Helvetica", "size": 16})

        """
        check_type(font, dict)
        font = dict(font)
        if self.__font != font:
            self.__font = font
            self.font_changed.emit(font)

    def font(self):
        # type: () -> dict
        """
        Annotation's font property dictionary.
        """
        return dict(self.__font)

    font = Property(str, fget=font, fset=set_font)  # type: ignore
Example #22
0
class WidgetToolBox(ToolBox):
    """
    `WidgetToolBox` widget shows a tool box containing button grids of
    actions for a :class:`QtWidgetRegistry` item model.

    """

    triggered = Signal(QAction)
    hovered = Signal(QAction)

    def __init__(self, parent=None):
        ToolBox.__init__(self, parent)
        self.__model = None
        self.__iconSize = QSize(25, 25)
        self.__buttonSize = QSize(50, 50)
        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)

    def setIconSize(self, size):
        """
        Set the widget icon size (icons in the button grid).
        """
        self.__iconSize = size
        for widget in map(self.widget, range(self.count())):
            widget.setIconSize(size)

    def iconSize(self):
        """
        Return the widget buttons icon size.
        """
        return self.__iconSize

    iconSize_ = Property(QSize,
                         fget=iconSize,
                         fset=setIconSize,
                         designable=True)

    def setButtonSize(self, size):
        """
        Set fixed widget button size.
        """
        self.__buttonSize = size
        for widget in map(self.widget, range(self.count())):
            widget.setButtonSize(size)

    def buttonSize(self):
        """Return the widget button size
        """
        return self.__buttonSize

    buttonSize_ = Property(QSize,
                           fget=buttonSize,
                           fset=setButtonSize,
                           designable=True)

    def saveState(self):
        """
        Return the toolbox state (as a `QByteArray`).

        .. note:: Individual tabs are stored by their action's text.

        """
        version = 2

        actions = map(self.tabAction, range(self.count()))
        expanded = [action for action in actions if action.isChecked()]
        expanded = [action.text() for action in expanded]

        byte_array = QByteArray()
        stream = QDataStream(byte_array, QIODevice.WriteOnly)
        stream.writeInt(version)
        stream.writeQStringList(expanded)

        return byte_array

    def restoreState(self, state):
        """
        Restore the toolbox from a :class:`QByteArray` `state`.

        .. note:: The toolbox should already be populated for the state
                  changes to take effect.

        """
        # In version 1 of saved state the state was saved in
        # a simple dict repr string.
        if isinstance(state, QByteArray):
            stream = QDataStream(state, QIODevice.ReadOnly)
            version = stream.readInt()
            if version == 2:
                expanded = stream.readQStringList()
                for action in map(self.tabAction, range(self.count())):
                    if (action.text() in expanded) != action.isChecked():
                        action.trigger()

                return True
        return False

    def setModel(self, model):
        """
        Set the widget registry model (:class:`QStandardItemModel`) for
        this toolbox.

        """
        if self.__model is not None:
            self.__model.itemChanged.disconnect(self.__on_itemChanged)
            self.__model.rowsInserted.disconnect(self.__on_rowsInserted)
            self.__model.rowsRemoved.disconnect(self.__on_rowsRemoved)

        self.__model = model
        if self.__model is not None:
            self.__model.itemChanged.connect(self.__on_itemChanged)
            self.__model.rowsInserted.connect(self.__on_rowsInserted)
            self.__model.rowsRemoved.connect(self.__on_rowsRemoved)

        self.__initFromModel(self.__model)

    def __initFromModel(self, model):
        for cat_item in iter_item(model.invisibleRootItem()):
            self.__insertItem(cat_item, self.count())

    def __insertItem(self, item, index):
        """
        Insert category item at index.
        """
        grid = WidgetToolGrid()
        grid.setModel(item.model(), item.index())

        grid.actionTriggered.connect(self.triggered)
        grid.actionHovered.connect(self.hovered)

        grid.setIconSize(self.__iconSize)
        grid.setButtonSize(self.__buttonSize)

        text = item.text()
        icon = item.icon()
        tooltip = item.toolTip()

        # Set the 'tab-title' property to text.
        grid.setProperty("tab-title", text)
        grid.setObjectName("widgets-toolbox-grid")

        self.insertItem(index, grid, text, icon, tooltip)
        button = self.tabButton(index)

        # Set the 'highlight' color
        if item.data(Qt.BackgroundRole) is not None:
            brush = item.background()
        elif item.data(QtWidgetRegistry.BACKGROUND_ROLE) is not None:
            brush = item.data(QtWidgetRegistry.BACKGROUND_ROLE)
        else:
            brush = self.palette().brush(QPalette.Button)

        if not brush.gradient():
            gradient = create_gradient(brush.color())
            brush = QBrush(gradient)

        palette = button.palette()
        palette.setBrush(QPalette.Highlight, brush)
        button.setPalette(palette)

    def __on_itemChanged(self, item):
        """
        Item contents have changed.
        """
        parent = item.parent()
        if parent is self.__model.invisibleRootItem():
            button = self.tabButton(item.row())
            button.setIcon(item.icon())
            button.setText(item.text())
            button.setToolTip(item.toolTip())

    def __on_rowsInserted(self, parent, start, end):
        """
        Items have been inserted in the model.
        """
        # Only the top level items (categories) are handled here.
        if not parent is not None:
            root = self.__model.invisibleRootItem()
            for i in range(start, end + 1):
                item = root.child(i)
                self.__insertItem(item, i)

    def __on_rowsRemoved(self, parent, start, end):
        """
        Rows have been removed from the model.
        """
        # Only the top level items (categories) are handled here.
        if not parent is not None:
            for i in range(end, start - 1, -1):
                self.removeItem(i)
Example #23
0
class WidgetToolBox(ToolBox):
    """
    `WidgetToolBox` widget shows a tool box containing button grids of
    actions for a :class:`QtWidgetRegistry` item model.
    """

    triggered = Signal(QAction)
    hovered = Signal(QAction)

    def __init__(self, parent=None):
        # type: (Optional[QWidget]) -> None
        super().__init__(parent)
        self.__model = None  # type: Optional[QAbstractItemModel]
        self.__iconSize = QSize(25, 25)
        self.__buttonSize = QSize(50, 50)
        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)

    def setIconSize(self, size):  # type: (QSize) -> None
        """
        Set the widget icon size (icons in the button grid).
        """
        if self.__iconSize != size:
            self.__iconSize = QSize(size)
            for widget in map(self.widget, range(self.count())):
                widget.setIconSize(size)

    def iconSize(self):  # type: () -> QSize
        """
        Return the widget buttons icon size.
        """
        return QSize(self.__iconSize)

    iconSize_ = Property(QSize,
                         fget=iconSize,
                         fset=setIconSize,
                         designable=True)

    def setButtonSize(self, size):  # type: (QSize) -> None
        """
        Set fixed widget button size.
        """
        if self.__buttonSize != size:
            self.__buttonSize = QSize(size)
            for widget in map(self.widget, range(self.count())):
                widget.setButtonSize(size)

    def buttonSize(self):  # type: () -> QSize
        """Return the widget button size
        """
        return QSize(self.__buttonSize)

    buttonSize_ = Property(QSize,
                           fget=buttonSize,
                           fset=setButtonSize,
                           designable=True)

    def saveState(self):  # type: () -> QByteArray
        """
        Return the toolbox state (as a `QByteArray`).

        .. note:: Individual tabs are stored by their action's text.

        """
        version = 2

        actions = map(self.tabAction, range(self.count()))
        expanded = [action for action in actions if action.isChecked()]
        expanded = [action.text() for action in expanded]

        byte_array = QByteArray()
        stream = QDataStream(byte_array, QIODevice.WriteOnly)
        stream.writeInt(version)
        stream.writeQStringList(expanded)

        return byte_array

    def restoreState(self, state):  # type: (QByteArray) -> bool
        """
        Restore the toolbox from a :class:`QByteArray` `state`.

        .. note:: The toolbox should already be populated for the state
                  changes to take effect.

        """
        stream = QDataStream(state, QIODevice.ReadOnly)
        version = stream.readInt()
        if version == 2:
            expanded = stream.readQStringList()
            for action in map(self.tabAction, range(self.count())):
                if (action.text() in expanded) != action.isChecked():
                    action.trigger()
            return True
        return False

    def setModel(self, model):
        # type: (QAbstractItemModel) -> None
        """
        Set the widget registry model (:class:`QAbstractItemModel`) for
        this toolbox.
        """
        if self.__model is not None:
            self.__model.dataChanged.disconnect(self.__on_dataChanged)
            self.__model.rowsInserted.disconnect(self.__on_rowsInserted)
            self.__model.rowsRemoved.disconnect(self.__on_rowsRemoved)

        self.__model = model
        if self.__model is not None:
            self.__model.dataChanged.connect(self.__on_dataChanged)
            self.__model.rowsInserted.connect(self.__on_rowsInserted)
            self.__model.rowsRemoved.connect(self.__on_rowsRemoved)

        self.__initFromModel(self.__model)

    def __initFromModel(self, model):
        # type: (QAbstractItemModel) -> None
        for row in range(model.rowCount()):
            self.__insertItem(model.index(row, 0), self.count())

    def __insertItem(self, item, index):
        # type: (QModelIndex, int) -> None
        """
        Insert category item  (`QModelIndex`) at index.
        """
        grid = WidgetToolGrid()
        grid.setModel(item.model(), item)
        grid.actionTriggered.connect(self.triggered)
        grid.actionHovered.connect(self.hovered)

        grid.setIconSize(self.__iconSize)
        grid.setButtonSize(self.__buttonSize)
        grid.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)

        text = item_text(item)
        icon = item_icon(item)
        tooltip = item_tooltip(item)

        # Set the 'tab-title' property to text.
        grid.setProperty("tab-title", text)
        grid.setObjectName("widgets-toolbox-grid")

        self.insertItem(index, grid, text, icon, tooltip)
        button = self.tabButton(index)

        # Set the 'highlight' color if applicable
        highlight_foreground = None
        highlight = item_background(item)
        if highlight is None \
                and item.data(QtWidgetRegistry.BACKGROUND_ROLE) is not None:
            highlight = item.data(QtWidgetRegistry.BACKGROUND_ROLE)

        if isinstance(highlight, QBrush) and highlight.style() != Qt.NoBrush:
            if not highlight.gradient():
                value = highlight.color().value()
                gradient = create_gradient(highlight.color())
                highlight = QBrush(gradient)
                highlight_foreground = Qt.black if value > 128 else Qt.white

        palette = button.palette()

        if highlight is not None:
            palette.setBrush(QPalette.Highlight, highlight)
        if highlight_foreground is not None:
            palette.setBrush(QPalette.HighlightedText, highlight_foreground)
        button.setPalette(palette)

    def __on_dataChanged(self, topLeft, bottomRight):
        # type: (QModelIndex, QModelIndex) -> None
        parent = topLeft.parent()
        if not parent.isValid():
            for row in range(topLeft.row(), bottomRight.row() + 1):
                item = topLeft.sibling(row, topLeft.column())
                button = self.tabButton(row)
                button.setIcon(item_icon(item))
                button.setText(item_text(item))
                button.setToolTip(item_tooltip(item))

    def __on_rowsInserted(self, parent, start, end):
        # type: (QModelIndex, int, int) -> None
        """
        Items have been inserted in the model.
        """
        # Only the top level items (categories) are handled here.
        assert self.__model is not None
        if not parent.isValid():
            for i in range(start, end + 1):
                item = self.__model.index(i, 0)
                self.__insertItem(item, i)

    def __on_rowsRemoved(self, parent, start, end):
        # type: (QModelIndex, int, int) -> None
        """
        Rows have been removed from the model.
        """
        # Only the top level items (categories) are handled here.
        if not parent.isValid():
            for i in range(end, start - 1, -1):
                self.removeItem(i)
Example #24
0
class SchemeNode(QObject):
    """
    A node in a :class:`.Scheme`.

    Parameters
    ----------
    description : :class:`WidgetDescription`
        Node description instance.
    title : str, optional
        Node title string (if None `description.name` is used).
    position : tuple
        (x, y) tuple of floats for node position in a visual display.
    properties : dict
        Additional extra instance properties (settings, widget geometry, ...)
    parent : :class:`QObject`
        Parent object.

    """
    class State(enum.IntEnum):
        """
        A workflow node's runtime state flags
        """
        #: The node has no state.
        NoState = 0

        #: The node is running (i.e. executing a task).
        Running = 1

        #: The node has invalidated inputs. This flag is set when:
        #:
        #: * An input link is added or removed
        #: * An input link is marked as pending
        #:
        #: It is set/cleared by the execution manager when the inputs are
        #: propagated to the node.
        Pending = 2

        #: The node has invalidated outputs. Execution manager should not
        #: propagate this node's existing outputs to dependent nodes until
        #: this flag is cleared.
        Invalidated = 4

        #: The node is in a state where it does not accept new signals.
        #: The execution manager should not propagate inputs to this node
        #: until this flag is cleared.
        NotReady = 8

    NoState = State.NoState
    Running = State.Running
    Pending = State.Pending
    Invalidated = State.Invalidated
    NotReady = State.NotReady

    def __init__(self,
                 description,
                 title=None,
                 position=None,
                 properties=None,
                 parent=None):
        # type: (WidgetDescription, str, Tuple[float, float], dict, QObject) -> None
        super().__init__(parent)
        self.description = description

        if title is None:
            title = description.name

        self.__title = title
        self.__position = position or (0, 0)
        self.__progress = -1
        self.__processing_state = 0
        self.__status_message = ""
        self.__state_messages = {}  # type: Dict[str, UserMessage]
        self.__state = SchemeNode.NoState  # type: Union[SchemeNode.State, int]
        self.properties = properties or {}

    def input_channels(self):
        # type: () -> List[InputSignal]
        """
        Return a list of input channels (:class:`InputSignal`) for the node.
        """
        return list(self.description.inputs)

    def output_channels(self):
        # type: () -> List[OutputSignal]
        """
        Return a list of output channels (:class:`OutputSignal`) for the node.
        """
        return list(self.description.outputs)

    def input_channel(self, name):
        # type: (str) -> InputSignal
        """
        Return the input channel matching `name`. Raise a `ValueError`
        if not found.
        """
        for channel in self.input_channels():
            if channel.name == name:
                return channel
        raise ValueError("%r is not a valid input channel name for %r." % \
                         (name, self.description.name))

    def output_channel(self, name):
        # type: (str) -> OutputSignal
        """
        Return the output channel matching `name`. Raise an `ValueError`
        if not found.
        """
        for channel in self.output_channels():
            if channel.name == name:
                return channel
        raise ValueError("%r is not a valid output channel name for %r." % \
                         (name, self.description.name))

    #: The title of the node has changed
    title_changed = Signal(str)

    def set_title(self, title):
        """
        Set the node title.
        """
        if self.__title != title:
            self.__title = title
            self.title_changed.emit(self.__title)

    def title(self):
        """
        The node title.
        """
        return self.__title

    title = Property(str, fset=set_title, fget=title)  # type: ignore

    #: Position of the node in the scheme has changed
    position_changed = Signal(tuple)

    def set_position(self, pos):
        """
        Set the position (``(x, y)`` tuple) of the node.
        """
        if self.__position != pos:
            self.__position = pos
            self.position_changed.emit(pos)

    def position(self):
        """
        ``(x, y)`` tuple containing the position of the node in the scheme.
        """
        return self.__position

    position = Property(tuple, fset=set_position,
                        fget=position)  # type: ignore

    #: Node's progress value has changed.
    progress_changed = Signal(float)

    def set_progress(self, value):
        """
        Set the progress value.
        """
        if self.__progress != value:
            self.__progress = value
            self.progress_changed.emit(value)

    def progress(self):
        """
        The current progress value. -1 if progress is not set.
        """
        return self.__progress

    progress = Property(float, fset=set_progress,
                        fget=progress)  # type: ignore

    #: Node's processing state has changed.
    processing_state_changed = Signal(int)

    def set_processing_state(self, state):
        """
        Set the node processing state.
        """
        self.set_state_flags(SchemeNode.Running, bool(state))

    def processing_state(self):
        """
        The node processing state, 0 for not processing, 1 the node is busy.
        """
        return int(bool(self.state() & SchemeNode.Running))

    processing_state = Property(
        int,
        fset=set_processing_state,  # type: ignore
        fget=processing_state)

    def set_tool_tip(self, tool_tip):
        if self.__tool_tip != tool_tip:
            self.__tool_tip = tool_tip

    def tool_tip(self):
        return self.__tool_tip

    tool_tip = Property(
        str,
        fset=set_tool_tip,  # type: ignore
        fget=tool_tip)

    #: The node's status tip has changes
    status_message_changed = Signal(str)

    def set_status_message(self, text):
        # type: (str) -> None
        """Set a short status message."""
        if self.__status_message != text:
            self.__status_message = text
            self.status_message_changed.emit(text)

    def status_message(self):
        # type: () -> str
        """A short status message summarizing the current node state."""
        return self.__status_message

    #: The node's state message has changed
    state_message_changed = Signal(UserMessage)

    def set_state_message(self, message):
        # type: (UserMessage) -> None
        """
        Set a message to be displayed by a scheme view for this node.
        """
        if message.message_id is not None:
            self.__state_messages[message.message_id] = message
            self.state_message_changed.emit(message)
        else:
            warnings.warn(
                "'message' with no id was ignored. "
                "This will raise an error in the future.",
                FutureWarning,
                stacklevel=2)

    def clear_state_message(self, message_id):
        # type: (str) -> None
        """
        Clear (remove) a message with `message_id`.

        :attr:`state_message_changed` signal will be emitted with a empty
        message for the `message_id`.
        """
        if message_id in self.__state_messages:
            # emit an empty message
            m = self.__state_messages[message_id]
            m = UserMessage("", m.severity, m.message_id)
            self.__state_messages[message_id] = m
            self.state_message_changed.emit(m)
            del self.__state_messages[message_id]

    def state_message(self, message_id):
        # type: (str) -> Optional[UserMessage]
        """
        Return a message with `message_id` or None if a message with that
        id does not exist.
        """
        return self.__state_messages.get(message_id, None)

    def state_messages(self):
        # type: () -> Iterable[UserMessage]
        """
        Return a list of all state messages.
        """
        return self.__state_messages.values()

    state_changed = Signal(int)

    def set_state(self, state):
        # type: (Union[State, int]) -> None
        """
        Set the node runtime state flags

        Parameters
        ----------
        state: SchemeNode.State
        """
        if self.__state != state:
            curr = self.__state
            self.__state = state
            QCoreApplication.sendEvent(
                self, NodeEvent(NodeEvent.NodeStateChange, self))
            self.state_changed.emit(state)
            if curr & SchemeNode.Running != state & SchemeNode.Running:
                self.processing_state_changed.emit(
                    int(bool(state & SchemeNode.Running)))

    def state(self):
        # type: () -> Union[State, int]
        """
        Return the node runtime state flags.
        """
        return self.__state

    def set_state_flags(self, flags, on):
        # type: (Union[State, int], bool) -> None
        """
        Set the specified state flags on/off.

        Parameters
        ----------
        flags: SchemeNode.State
            Flag to modify
        on: bool
            Turn the flag on or off
        """
        if on:
            state = self.__state | flags
        else:
            state = self.__state & ~flags
        self.set_state(state)

    def test_state_flags(self, flag):
        # type: (State) -> bool
        """
        Return True/False if the runtime state flag is set.

        Parameters
        ----------
        flag: SchemeNode.State

        Returns
        -------
        val: bool
        """
        return bool(self.__state & flag)

    def __str__(self):
        return "SchemeNode(description_id=%r, title=%r, ...)" % \
                (str(self.description.id), self.title)

    def __repr__(self):
        return str(self)
Example #25
0
class ToolBoxTabButton(QToolButton):
    """
    A tab button for an item in a :class:`ToolBox`.
    """

    def setNativeStyling(self, state):
        """
        Render tab buttons as native (or css styled) :class:`QToolButtons`.
        If set to `False` (default) the button is pained using a custom
        paint routine.

        """
        self.__nativeStyling = state
        self.update()

    def nativeStyling(self):
        """
        Use :class:`QStyle`'s to paint the class:`QToolButton` look.
        """
        return self.__nativeStyling

    nativeStyling_ = Property(bool,
                              fget=nativeStyling,
                              fset=setNativeStyling,
                              designable=True)

    def __init__(self, *args, **kwargs):
        self.__nativeStyling = False
        self.position = QStyleOptionToolBox.OnlyOneTab
        self.selected = QStyleOptionToolBox.NotAdjacent

        QToolButton.__init__(self, *args, **kwargs)

    def paintEvent(self, event):
        if self.__nativeStyling:
            QToolButton.paintEvent(self, event)
        else:
            self.__paintEventNoStyle()

    def __paintEventNoStyle(self):
        p = QPainter(self)
        opt = QStyleOptionToolButton()
        self.initStyleOption(opt)

        fm = QFontMetrics(opt.font)
        palette = opt.palette

        # highlight brush is used as the background for the icon and background
        # when the tab is expanded and as mouse hover color (lighter).
        brush_highlight = palette.highlight()
        if opt.state & QStyle.State_Sunken:
            # State 'down' pressed during a mouse press (slightly darker).
            background_brush = brush_darker(brush_highlight, 110)
        elif opt.state & QStyle.State_MouseOver:
            background_brush = brush_darker(brush_highlight, 95)
        elif opt.state & QStyle.State_On:
            background_brush = brush_highlight
        else:
            # The default button brush.
            background_brush = palette.button()

        rect = opt.rect

        icon_area_rect = QRect(rect)
        icon_area_rect.setRight(int(icon_area_rect.height() * 1.26))

        text_rect = QRect(rect)
        text_rect.setLeft(icon_area_rect.right() + 10)

        # Background  (TODO: Should the tab button have native
        # toolbutton shape, drawn using PE_PanelButtonTool or even
        # QToolBox tab shape)

        # Default outline pen
        pen = QPen(palette.color(QPalette.Mid))

        p.save()
        p.setPen(Qt.NoPen)
        p.setBrush(QBrush(background_brush))
        p.drawRect(rect)

        # Draw the background behind the icon if the background_brush
        # is different.
        if not opt.state & QStyle.State_On:
            p.setBrush(brush_highlight)
            p.drawRect(icon_area_rect)
            # Line between the icon and text
            p.setPen(pen)
            p.drawLine(icon_area_rect.topRight(),
                       icon_area_rect.bottomRight())

        if opt.state & QStyle.State_HasFocus:
            # Set the focus frame pen and draw the border
            pen = QPen(QColor(FOCUS_OUTLINE_COLOR))
            p.setPen(pen)
            p.setBrush(Qt.NoBrush)
            # Adjust for pen
            rect = rect.adjusted(0, 0, -1, -1)
            p.drawRect(rect)

        else:
            p.setPen(pen)
            # Draw the top/bottom border
            if self.position == QStyleOptionToolBox.OnlyOneTab or \
                    self.position == QStyleOptionToolBox.Beginning or \
                    self.selected & \
                        QStyleOptionToolBox.PreviousIsSelected:

                p.drawLine(rect.topLeft(), rect.topRight())

            p.drawLine(rect.bottomLeft(), rect.bottomRight())

        p.restore()

        p.save()
        text = fm.elidedText(opt.text, Qt.ElideRight, text_rect.width())
        p.setPen(QPen(palette.color(QPalette.ButtonText)))
        p.setFont(opt.font)

        p.drawText(text_rect,
                   int(Qt.AlignVCenter | Qt.AlignLeft) | \
                   int(Qt.TextSingleLine),
                   text)

        if not opt.icon.isNull():
            if opt.state & QStyle.State_Enabled:
                mode = QIcon.Normal
            else:
                mode = QIcon.Disabled
            if opt.state & QStyle.State_On:
                state = QIcon.On
            else:
                state = QIcon.Off
            icon_area_rect = icon_area_rect
            icon_rect = QRect(QPoint(0, 0), opt.iconSize)
            icon_rect.moveCenter(icon_area_rect.center())
            opt.icon.paint(p, icon_rect, Qt.AlignCenter, mode, state)
        p.restore()
Example #26
0
class SchemeLink(QObject):
    """
    A instantiation of a link between two :class:`.SchemeNode` instances
    in a :class:`.Scheme`.

    Parameters
    ----------
    source_node : :class:`.SchemeNode`
        Source node.
    source_channel : :class:`OutputSignal`
        The source widget's signal.
    sink_node : :class:`.SchemeNode`
        The sink node.
    sink_channel : :class:`InputSignal`
        The sink widget's input signal.
    properties : `dict`
        Additional link properties.

    """

    #: The link enabled state has changed
    enabled_changed = Signal(bool)

    #: The link dynamic enabled state has changed.
    dynamic_enabled_changed = Signal(bool)

    #: Runtime link state has changed
    state_changed = Signal(int)

    class State(enum.IntEnum):
        """
        Flags indicating the runtime state of a link
        """
        #: The link has no associated state (e.g. is not associated with any
        #: execution contex)
        NoState = 0
        #: A link is empty when it has no value on it.
        Empty = 1
        #: A link is active when the source node provides a value on output.
        Active = 2
        #: A link is pending when it's sink node has not yet been notified
        #: of a change (note that Empty|Pending is a valid state)
        Pending = 4
        #: The link's source node has invalidated the source channel.
        #: The execution manager should not propagate this links source value
        #: until this flag is cleared.
        #:
        #: .. versionadded:: 0.1.8
        Invalidated = 8

    NoState = State.NoState
    Empty = State.Empty
    Active = State.Active
    Pending = State.Pending
    Invalidated = State.Invalidated

    def __init__(self,
                 source_node,
                 source_channel,
                 sink_node,
                 sink_channel,
                 enabled=True,
                 properties=None,
                 parent=None):
        # type: (Node, Output, Node, Input, bool, dict, QObject) -> None
        super().__init__(parent)
        self.source_node = source_node

        if isinstance(source_channel, str):
            source_channel = source_node.output_channel(source_channel)
        elif source_channel not in source_node.output_channels():
            raise ValueError("%r not in in nodes output channels." \
                             % source_channel)

        self.source_channel = source_channel

        self.sink_node = sink_node

        if isinstance(sink_channel, str):
            sink_channel = sink_node.input_channel(sink_channel)
        elif sink_channel not in sink_node.input_channels():
            raise ValueError("%r not in in nodes input channels." \
                             % source_channel)

        self.sink_channel = sink_channel

        if not compatible_channels(source_channel, sink_channel):
            raise IncompatibleChannelTypeError(
                "Cannot connect %r to %r" %
                (source_channel.type, sink_channel.type))

        self.__enabled = enabled
        self.__dynamic_enabled = False
        self.__state = SchemeLink.NoState  # type: Union[SchemeLink.State, int]
        self.__tool_tip = ""
        self.properties = properties or {}

    def source_type(self):
        # type: () -> type
        """
        Return the type of the source channel.

        .. deprecated:: 0.1.5
            Use :func:`source_types` instead.
        """
        warnings.warn("`source_type()` is deprecated. Use `source_types()`.",
                      DeprecationWarning,
                      stacklevel=2)
        return _get_first_type(self.source_channel.type, "source_types")

    def source_types(self):
        # type: () -> Tuple[type, ...]
        """
        Return the type(s) of the source channel.
        """
        return resolved_valid_types(self.source_channel.types)

    def sink_type(self):
        # type: () -> type
        """
        Return the type of the sink channel.

        .. deprecated:: 0.1.5
            Use :func:`sink_types` instead.
        """
        warnings.warn("`sink_type()` is deprecated. Use `sink_types()`.",
                      DeprecationWarning,
                      stacklevel=2)
        return _get_first_type(self.sink_channel.types, "sink_types")

    def sink_types(self):
        # type: () -> Tuple[type, ...]
        """
        Return the type(s) of the sink channel.
        """
        return resolved_valid_types(self.sink_channel.types)

    def is_dynamic(self):
        # type: () -> bool
        """
        Is this link dynamic.
        """
        sink_types = self.sink_types()
        source_types = self.source_types()
        if self.source_channel.dynamic:
            strict, dynamic = _classify_connection(self.source_channel,
                                                   self.sink_channel)
            # If the connection type checks (strict) then supress the dynamic
            # state.
            return not strict and dynamic
        else:
            return False

    def set_enabled(self, enabled):
        # type: (bool) -> None
        """
        Enable/disable the link.
        """
        if self.__enabled != enabled:
            self.__enabled = enabled
            self.enabled_changed.emit(enabled)

    def is_enabled(self):
        # type: () -> bool
        """
        Is this link enabled.
        """
        return self.__enabled

    enabled = Property(bool, fget=is_enabled, fset=set_enabled)

    def set_dynamic_enabled(self, enabled):
        # type: (bool) -> None
        """
        Enable/disable the dynamic link. Has no effect if the link
        is not dynamic.
        """
        if self.is_dynamic() and self.__dynamic_enabled != enabled:
            self.__dynamic_enabled = enabled
            self.dynamic_enabled_changed.emit(enabled)

    def is_dynamic_enabled(self):
        # type: () -> bool
        """
        Is this a dynamic link and is `dynamic_enabled` set to `True`
        """
        return self.is_dynamic() and self.__dynamic_enabled

    dynamic_enabled = Property(bool,
                               fget=is_dynamic_enabled,
                               fset=set_dynamic_enabled)

    def set_runtime_state(self, state):
        # type: (Union[State, int]) -> None
        """
        Set the link's runtime state.

        Parameters
        ----------
        state : SchemeLink.State
        """
        if self.__state != state:
            self.__state = state
            ev = LinkEvent(LinkEvent.InputLinkStateChange, self)
            QCoreApplication.sendEvent(self.sink_node, ev)
            ev = LinkEvent(LinkEvent.OutputLinkStateChange, self)
            QCoreApplication.sendEvent(self.source_node, ev)
            self.state_changed.emit(state)

    def runtime_state(self):
        # type: () -> Union[State, int]
        """
        Returns
        -------
        state : SchemeLink.State
        """
        return self.__state

    def set_runtime_state_flag(self, flag, on):
        # type: (State, bool) -> None
        """
        Set/unset runtime state flag.

        Parameters
        ----------
        flag: SchemeLink.State
        on: bool
        """
        if on:
            state = self.__state | flag
        else:
            state = self.__state & ~flag
        self.set_runtime_state(state)

    def test_runtime_state(self, flag):
        # type: (State) -> bool
        """
        Test if runtime state flag is on/off

        Parameters
        ----------
        flag: SchemeLink.State
            State flag to test

        Returns
        -------
        on: bool
            True if `flag` is set; False otherwise.

        """
        return bool(self.__state & flag)

    def set_tool_tip(self, tool_tip):
        # type: (str) -> None
        """
        Set the link tool tip.
        """
        if self.__tool_tip != tool_tip:
            self.__tool_tip = tool_tip

    def tool_tip(self):
        # type: () -> str
        """
        Link tool tip.
        """
        return self.__tool_tip

    tool_tip = Property(
        str,
        fget=tool_tip,  # type: ignore
        fset=set_tool_tip)

    def __str__(self):
        return "{0}(({1}, {2}) -> ({3}, {4}))".format(
            type(self).__name__, self.source_node.title,
            self.source_channel.name, self.sink_node.title,
            self.sink_channel.name)
Example #27
0
class SplitterResizer(QObject):
    """
    An object able to control the size of a widget in a QSplitter instance.
    """
    def __init__(self, parent=None, **kwargs):
        # type: (Optional[QObject], Any) -> None
        super().__init__(parent, **kwargs)
        self.__splitter = None  # type: Optional[QSplitter]
        self.__widget = None    # type: Optional[QWidget]
        self.__updateOnShow = True  # Need __update on next show event
        self.__animationEnabled = True
        self.__size = -1
        self.__expanded = False
        self.__animation = QPropertyAnimation(
            self, b"size_", self, duration=200
        )
        self.__action = QAction("toggle-expanded", self, checkable=True)
        self.__action.triggered[bool].connect(self.setExpanded)

    def setSize(self, size):
        # type: (int) -> None
        """
        Set the size of the controlled widget (either width or height
        depending on the orientation).

        .. note::
            The controlled widget's size is only updated when it it is shown.
        """
        if self.__size != size:
            self.__size = size
            self.__update()

    def size(self):
        # type: () -> int
        """
        Return the size of the widget in the splitter (either height of
        width) depending on the splitter orientation.
        """
        if self.__splitter and self.__widget:
            index = self.__splitter.indexOf(self.__widget)
            sizes = self.__splitter.sizes()
            return sizes[index]
        else:
            return -1

    size_ = Property(int, fget=size, fset=setSize)

    def setAnimationEnabled(self, enable):
        # type: (bool) -> None
        """Enable/disable animation."""
        self.__animation.setDuration(0 if enable else 200)

    def animationEnabled(self):
        # type: () -> bool
        return self.__animation.duration() == 0

    def setSplitterAndWidget(self, splitter, widget):
        # type: (QSplitter, QWidget) -> None
        """Set the QSplitter and QWidget instance the resizer should control.

        .. note:: the widget must be in the splitter.
        """
        if splitter and widget and not splitter.indexOf(widget) > 0:
            raise ValueError("Widget must be in a splitter.")

        if self.__widget is not None:
            self.__widget.removeEventFilter(self)
        if self.__splitter is not None:
            self.__splitter.removeEventFilter(self)

        self.__splitter = splitter
        self.__widget = widget

        if widget is not None:
            widget.installEventFilter(self)
        if splitter is not None:
            splitter.installEventFilter(self)

        self.__update()

        size = self.size()
        if self.__expanded and size == 0:
            self.open()
        elif not self.__expanded and size > 0:
            self.close()

    def toggleExpandedAction(self):
        # type: () -> QAction
        """Return a QAction that can be used to toggle expanded state.
        """
        return self.__action

    def toogleExpandedAction(self):
        warnings.warn(
            "'toogleExpandedAction is deprecated, use 'toggleExpandedAction' "
            "instead.", DeprecationWarning, stacklevel=2
        )
        return self.toggleExpandedAction()

    def open(self):
        # type: () -> None
        """Open the controlled widget (expand it to sizeHint).
        """
        self.__expanded = True
        self.__action.setChecked(True)

        if self.__splitter is None or self.__widget is None:
            return

        hint = self.__widget.sizeHint()

        if self.__splitter.orientation() == Qt.Vertical:
            end = hint.height()
        else:
            end = hint.width()

        self.__animation.setStartValue(0)
        self.__animation.setEndValue(end)
        self.__animation.start()

    def close(self):
        # type: () -> None
        """Close the controlled widget (shrink to size 0).
        """
        self.__expanded = False
        self.__action.setChecked(False)

        if self.__splitter is None or self.__widget is None:
            return

        self.__animation.setStartValue(self.size())
        self.__animation.setEndValue(0)
        self.__animation.start()

    def setExpanded(self, expanded):
        # type: (bool) -> None
        """Set the expanded state."""
        if self.__expanded != expanded:
            if expanded:
                self.open()
            else:
                self.close()

    def expanded(self):
        # type: () -> bool
        """Return the expanded state."""
        return self.__expanded

    def __update(self):
        # type: () -> None
        """Update the splitter sizes."""
        if self.__splitter and self.__widget:
            if sum(self.__splitter.sizes()) == 0:
                # schedule update on next show event
                self.__updateOnShow = True
                return

            splitter = self.__splitter
            index = splitter.indexOf(self.__widget)
            sizes = splitter.sizes()
            current = sizes[index]
            diff = current - self.__size
            sizes[index] = self.__size
            sizes[index - 1] = sizes[index - 1] + diff
            self.__splitter.setSizes(sizes)

    def eventFilter(self, obj, event):
        # type: (QObject, QEvent) -> bool
        if event.type() == QEvent.Resize and obj is self.__widget and \
                self.__animation.state() == QPropertyAnimation.Stopped:
            # Update the expanded state when the user opens/closes the widget
            # by dragging the splitter handle.
            assert self.__splitter is not None
            assert isinstance(event, QResizeEvent)
            if self.__splitter.orientation() == Qt.Vertical:
                size = event.size().height()
            else:
                size = event.size().width()

            if self.__expanded and size == 0:
                self.__action.setChecked(False)
                self.__expanded = False
            elif not self.__expanded and size > 0:
                self.__action.setChecked(True)
                self.__expanded = True

        if event.type() == QEvent.Show and obj is self.__splitter and \
                self.__updateOnShow:
            # Update the splitter state after receiving valid geometry
            self.__updateOnShow = False
            self.__update()
        return super().eventFilter(obj, event)
Example #28
0
class MenuPage(ToolTree):
    """
    A menu page in a :class:`QuickMenu` widget, showing a list of actions.
    Shown actions can be disabled by setting a filtering function using the
    :func:`setFilterFunc`.

    """
    def __init__(self, parent=None, title="", icon=QIcon(), **kwargs):
        # type: (Optional[QWidget], str, QIcon, Any) -> None
        super().__init__(parent, **kwargs)

        self.__title = title
        self.__icon = QIcon(icon)
        self.__sizeHint = None  # type: Optional[QSize]

        self.view().setItemDelegate(_MenuItemDelegate(self.view()))
        self.view().entered.connect(self.__onEntered)
        self.view().viewport().setMouseTracking(True)

        # Make sure the initial model is wrapped in a ItemDisableFilter.
        self.setModel(self.model())

    def setTitle(self, title):
        # type: (str) -> None
        """
        Set the title of the page.
        """
        if self.__title != title:
            self.__title = title
            self.update()

    def title(self):
        # type: () -> str
        """
        Return the title of this page.
        """
        return self.__title

    title_ = Property(str, fget=title, fset=setTitle, doc="Title of the page.")

    def setIcon(self, icon):  # type: (QIcon) -> None
        """
        Set icon for this menu page.
        """
        if self.__icon != icon:
            self.__icon = icon
            self.update()

    def icon(self):  # type: () -> QIcon
        """
        Return the icon of this menu page.
        """
        return QIcon(self.__icon)

    icon_ = Property(QIcon, fget=icon, fset=setIcon, doc="Page icon")

    def setFilterFunc(self, func):
        # type: (Optional[Callable[[QModelIndex], bool]]) -> None
        """
        Set the filtering function. `func` should a function taking a single
        :class:`QModelIndex` argument and returning True if the item at index
        should be disabled and False otherwise. To disable filtering `func` can
        be set to ``None``.

        """
        proxyModel = self.view().model()
        proxyModel.setFilterFunc(func)

    def setModel(self, model):
        # type: (QAbstractItemModel) -> None
        """
        Reimplemented from :func:`ToolTree.setModel`.
        """
        proxyModel = ItemDisableFilter(self)
        proxyModel.setSourceModel(model)
        super().setModel(proxyModel)

        self.__invalidateSizeHint()

    def setRootIndex(self, index):
        # type: (QModelIndex) -> None
        """
        Reimplemented from :func:`ToolTree.setRootIndex`
        """
        proxyModel = self.view().model()
        mappedIndex = proxyModel.mapFromSource(index)
        super().setRootIndex(mappedIndex)

        self.__invalidateSizeHint()

    def rootIndex(self):
        # type: () -> QModelIndex
        """
        Reimplemented from :func:`ToolTree.rootIndex`
        """
        proxyModel = self.view().model()
        return proxyModel.mapToSource(super().rootIndex())

    def sizeHint(self):
        # type: () -> QSize
        """
        Reimplemented from :func:`QWidget.sizeHint`.
        """
        if self.__sizeHint is None:
            view = self.view()
            model = view.model()

            # This will not work for nested items (tree).
            count = model.rowCount(view.rootIndex())

            # 'sizeHintForColumn' is the reason for size hint caching
            # since it must traverse all items in the column.
            width = view.sizeHintForColumn(0)

            if count:
                height = view.sizeHintForRow(0)
                height = height * count
            else:
                height = 0
            self.__sizeHint = QSize(width, height)

        return self.__sizeHint

    def __invalidateSizeHint(self):  # type: () -> None
        self.__sizeHint = None
        self.updateGeometry()

    def __onEntered(self, index):  # type: (QModelIndex) -> None
        if not index.isValid():
            return

        if self.view().state() != QTreeView.NoState:
            # The item view can emit an 'entered' signal while the model/view
            # is being changed (rows removed). When this happens, setting the
            # current item can segfault (in QTreeView::scrollTo).
            return

        if index.flags() & Qt.ItemIsEnabled:
            self.view().selectionModel().setCurrentIndex(
                index, QItemSelectionModel.ClearAndSelect)
Example #29
0
class CrossFadePixmapWidget(QWidget):
    """
    A widget for cross fading between two pixmaps.
    """
    def __init__(self, parent=None, pixmap1=None, pixmap2=None):
        QWidget.__init__(self, parent)
        self.setPixmap(pixmap1)
        self.setPixmap2(pixmap2)
        self.blendingFactor_ = 0.0
        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)

    def setPixmap(self, pixmap):
        """
        Set pixmap 1
        """
        self.pixmap1 = pixmap
        self.updateGeometry()

    def setPixmap2(self, pixmap):
        """
        Set pixmap 2
        """
        self.pixmap2 = pixmap
        self.updateGeometry()

    def setBlendingFactor(self, factor):
        """
        Set the blending factor between the two pixmaps.
        """
        self.__blendingFactor = factor
        self.updateGeometry()

    def blendingFactor(self):
        """
        Pixmap blending factor between 0.0 and 1.0
        """
        return self.__blendingFactor

    blendingFactor_ = Property(float,
                               fget=blendingFactor,
                               fset=setBlendingFactor)

    def sizeHint(self):
        """
        Return an interpolated size between pixmap1.size()
        and pixmap2.size()

        """
        if self.pixmap1 and self.pixmap2:
            size1 = self.pixmap1.size()
            size2 = self.pixmap2.size()
            return size1 + self.blendingFactor_ * (size2 - size1)
        else:
            return QWidget.sizeHint(self)

    def paintEvent(self, event):
        """
        Paint the interpolated pixmap image.
        """
        p = QPainter(self)
        p.setClipRect(event.rect())
        factor = self.blendingFactor_**2
        if self.pixmap1 and 1. - factor:
            p.setOpacity(1. - factor)
            p.drawPixmap(QPoint(0, 0), self.pixmap1)
        if self.pixmap2 and factor:
            p.setOpacity(factor)
            p.drawPixmap(QPoint(0, 0), self.pixmap2)
Example #30
0
class SchemeNode(QObject):
    """
    A node in a :class:`.Scheme`.

    Parameters
    ----------
    description : :class:`WidgetDescription`
        Node description instance.
    title : str, optional
        Node title string (if None `description.name` is used).
    position : tuple
        (x, y) tuple of floats for node position in a visual display.
    properties : dict
        Additional extra instance properties (settings, widget geometry, ...)
    parent : :class:`QObject`
        Parent object.

    """
    def __init__(self,
                 description,
                 title=None,
                 position=None,
                 properties=None,
                 parent=None):
        # type: (WidgetDescription, str, Tuple[float, float], dict, QObject) -> None
        super().__init__(parent)
        self.description = description

        if title is None:
            title = description.name

        self.__title = title
        self.__position = position or (0, 0)
        self.__progress = -1
        self.__processing_state = 0
        self.__status_message = ""
        self.__state_messages = {}  # type: Dict[str, UserMessage]
        self.properties = properties or {}

    def input_channels(self):
        # type: () -> List[InputSignal]
        """
        Return a list of input channels (:class:`InputSignal`) for the node.
        """
        return list(self.description.inputs)

    def output_channels(self):
        # type: () -> List[OutputSignal]
        """
        Return a list of output channels (:class:`OutputSignal`) for the node.
        """
        return list(self.description.outputs)

    def input_channel(self, name):
        # type: (str) -> InputSignal
        """
        Return the input channel matching `name`. Raise a `ValueError`
        if not found.

        """
        for channel in self.input_channels():
            if channel.name == name:
                return channel
        raise ValueError("%r is not a valid input channel name for %r." % \
                         (name, self.description.name))

    def output_channel(self, name):
        # type: (str) -> OutputSignal
        """
        Return the output channel matching `name`. Raise an `ValueError`
        if not found.

        """
        for channel in self.output_channels():
            if channel.name == name:
                return channel
        raise ValueError("%r is not a valid output channel name for %r." % \
                         (name, self.description.name))

    #: The title of the node has changed
    title_changed = Signal(str)

    def set_title(self, title):
        """
        Set the node title.
        """
        if self.__title != title:
            self.__title = title
            self.title_changed.emit(self.__title)

    def title(self):
        """
        The node title.
        """
        return self.__title

    title = Property(str, fset=set_title, fget=title)  # type: ignore

    #: Position of the node in the scheme has changed
    position_changed = Signal(tuple)

    def set_position(self, pos):
        """
        Set the position (``(x, y)`` tuple) of the node.
        """
        if self.__position != pos:
            self.__position = pos
            self.position_changed.emit(pos)

    def position(self):
        """
        ``(x, y)`` tuple containing the position of the node in the scheme.
        """
        return self.__position

    position = Property(tuple, fset=set_position,
                        fget=position)  # type: ignore

    #: Node's progress value has changed.
    progress_changed = Signal(float)

    def set_progress(self, value):
        """
        Set the progress value.
        """
        if self.__progress != value:
            self.__progress = value
            self.progress_changed.emit(value)

    def progress(self):
        """
        The current progress value. -1 if progress is not set.
        """
        return self.__progress

    progress = Property(float, fset=set_progress,
                        fget=progress)  # type: ignore

    #: Node's processing state has changed.
    processing_state_changed = Signal(int)

    def set_processing_state(self, state):
        """
        Set the node processing state.
        """
        if self.__processing_state != state:
            self.__processing_state = state
            self.processing_state_changed.emit(state)

    def processing_state(self):
        """
        The node processing state, 0 for not processing, 1 the node is busy.
        """
        return self.__processing_state

    processing_state = Property(
        int,
        fset=set_processing_state,  # type: ignore
        fget=processing_state)

    def set_tool_tip(self, tool_tip):
        if self.__tool_tip != tool_tip:
            self.__tool_tip = tool_tip

    def tool_tip(self):
        return self.__tool_tip

    tool_tip = Property(
        str,
        fset=set_tool_tip,  # type: ignore
        fget=tool_tip)

    #: The node's status tip has changes
    status_message_changed = Signal(str)

    def set_status_message(self, text):
        # type: (str) -> None
        """Set a short status message."""
        if self.__status_message != text:
            self.__status_message = text
            self.status_message_changed.emit(text)

    def status_message(self):
        # type: () -> str
        """A short status message summarizing the current node state."""
        return self.__status_message

    #: The node's state message has changed
    state_message_changed = Signal(UserMessage)

    def set_state_message(self, message):
        # type: (UserMessage) -> None
        """
        Set a message to be displayed by a scheme view for this node.
        """
        if message.message_id is not None:
            self.__state_messages[message.message_id] = message
            self.state_message_changed.emit(message)
        else:
            warnings.warn(
                "'message' with no id was ignored. "
                "This will raise an error in the future.",
                FutureWarning,
                stacklevel=2)

    def clear_state_message(self, message_id):
        # type: (str) -> None
        """
        Clear (remove) a message with `message_id`.

        :attr:`state_message_changed` signal will be emitted with a empty
        message for the `message_id`.
        """
        if message_id in self.__state_messages:
            # emit an empty message
            m = self.__state_messages[message_id]
            m = UserMessage("", m.severity, m.message_id)
            self.__state_messages[message_id] = m
            self.state_message_changed.emit(m)
            del self.__state_messages[message_id]

    def state_message(self, message_id):
        # type: (str) -> Optional[UserMessage]
        """
        Return a message with `message_id` or None if a message with that
        id does not exist.
        """
        return self.__state_messages.get(message_id, None)

    def state_messages(self):
        # type: () -> Iterable[UserMessage]
        """
        Return a list of all state messages.
        """
        return self.__state_messages.values()

    def __str__(self):
        return "SchemeNode(description_id=%r, title=%r, ...)" % \
                (str(self.description.id), self.title)

    def __repr__(self):
        return str(self)