示例#1
0
    def __init__(self,
                 parent=None,
                 columns=4,
                 buttonSize=None,
                 iconSize=None,
                 toolButtonStyle=Qt.ToolButtonTextUnderIcon):
        QFrame.__init__(self, parent)

        if buttonSize is not None:
            buttonSize = QSize(buttonSize)

        if iconSize is not None:
            iconSize = QSize(iconSize)

        self.__columns = columns
        self.__buttonSize = buttonSize or QSize(50, 50)
        self.__iconSize = iconSize or QSize(26, 26)
        self.__toolButtonStyle = toolButtonStyle

        self.__gridSlots = []

        self.__buttonListener = ToolButtonEventListener(self)
        self.__buttonListener.buttonRightClicked.connect(
            self.__onButtonRightClick)

        self.__buttonListener.buttonEnter.connect(self.__onButtonEnter)

        self.__mapper = QSignalMapper()
        self.__mapper.mapped[QObject].connect(self.__onClicked)

        self.__setupUi()
示例#2
0
    def __init__(self,
                 parent=None, columns=4, buttonSize=QSize(),
                 iconSize=QSize(), toolButtonStyle=Qt.ToolButtonTextUnderIcon,
                 **kwargs):
        # type: (Optional[QWidget], int, QSize, QSize, Qt.ToolButtonStyle, Any) -> None
        sizePolicy = kwargs.pop("sizePolicy", None)  # type: Optional[QSizePolicy]
        super().__init__(parent, **kwargs)

        if buttonSize is None:
            buttonSize = QSize()
        if iconSize is None:
            iconSize = QSize()

        self.__columns = columns
        self.__buttonSize = QSize(buttonSize)
        self.__iconSize = QSize(iconSize)
        self.__toolButtonStyle = toolButtonStyle

        self.__gridSlots = []  # type: List[_ToolGridSlot]
        self.__mapper = QSignalMapper()
        self.__mapper.mapped[QObject].connect(self.__onClicked)

        layout = QGridLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        self.setLayout(layout)
        if sizePolicy is None:
            self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
            self.setAttribute(Qt.WA_WState_OwnSizePolicy, True)
        else:
            self.setSizePolicy(sizePolicy)
示例#3
0
    def __init__(self, parent=None, columns=4, buttonSize=None,
                 iconSize=None, toolButtonStyle=Qt.ToolButtonTextUnderIcon):
        QFrame.__init__(self, parent)

        if buttonSize is not None:
            buttonSize = QSize(buttonSize)

        if iconSize is not None:
            iconSize = QSize(iconSize)

        self.__columns = columns
        self.__buttonSize = buttonSize or QSize(50, 50)
        self.__iconSize = iconSize or QSize(26, 26)
        self.__toolButtonStyle = toolButtonStyle

        self.__gridSlots = []

        self.__buttonListener = ToolButtonEventListener(self)
        self.__buttonListener.buttonRightClicked.connect(
                self.__onButtonRightClick)

        self.__buttonListener.buttonEnter.connect(
                self.__onButtonEnter)

        self.__mapper = QSignalMapper()
        self.__mapper.mapped[QObject].connect(self.__onClicked)

        self.__setupUi()
示例#4
0
    def __init__(self, parent=None, **kwargs):
        # type: (Optional[QWidget], Any)-> None
        super().__init__(parent, **kwargs)
        self.__pages = []  # type: List[_ToolBoxPage]
        self.__tabButtonHeight = -1
        self.__tabIconSize = QSize()
        self.__exclusive = False
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)

        # Scroll area for the contents.
        self.__scrollArea = QScrollArea(
            self,
            objectName="toolbox-scroll-area",
            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
                                   QSizePolicy.MinimumExpanding),
            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,
            widgetResizable=True,
        )
        sb = ScrollBar()
        sb.styleChange.connect(self.updateGeometry)
        self.__scrollArea.setVerticalScrollBar(sb)
        self.__scrollArea.setFrameStyle(QScrollArea.NoFrame)

        # 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.__onTabActionToggled)
    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")

        # The layout where all the tab/pages are placed
        self.__contentsLayout = QVBoxLayout()
        self.__contentsLayout.setContentsMargins(0, 0, 0, 0)
        self.__contentsLayout.setSizeConstraint(QVBoxLayout.SetMinAndMaxSize)
        self.__contentsLayout.setSpacing(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)
示例#6
0
    def __init__(self, *args, **kwargs):
        # type: (Any, Any) -> None
        super().__init__(*args, **kwargs)

        self.scheme = None    # type: Optional[Scheme]
        self.registry = None  # type: Optional[WidgetRegistry]

        # All node items
        self.__node_items = []  # type: List[NodeItem]
        # Mapping from SchemeNodes to canvas items
        self.__item_for_node = {}  # type: Dict[SchemeNode, NodeItem]
        # All link items
        self.__link_items = []  # type: List[LinkItem]
        # Mapping from SchemeLinks to canvas items.
        self.__item_for_link = {}  # type: Dict[SchemeLink, LinkItem]

        # All annotation items
        self.__annotation_items = []  # type: List[Annotation]
        # Mapping from SchemeAnnotations to canvas items.
        self.__item_for_annotation = {}  # type: Dict[BaseSchemeAnnotation, Annotation]

        # Is the scene editable
        self.editable = True

        # Anchor Layout
        self.__anchor_layout = AnchorLayout()
        self.addItem(self.__anchor_layout)

        self.__channel_names_visible = True
        self.__node_animation_enabled = True
        self.__animations_temporarily_disabled = False

        self.user_interaction_handler = None  # type: Optional[UserInteraction]

        self.activated_mapper = QSignalMapper(self)
        self.activated_mapper.mapped[QObject].connect(
            lambda node: self.node_item_activated.emit(node)
        )
        self.hovered_mapper = QSignalMapper(self)
        self.hovered_mapper.mapped[QObject].connect(
            lambda node: self.node_item_hovered.emit(node)
        )
        self.position_change_mapper = QSignalMapper(self)
        self.position_change_mapper.mapped[QObject].connect(
            self._on_position_change
        )
        self.link_activated_mapper = QSignalMapper(self)
        self.link_activated_mapper.mapped[QObject].connect(
            lambda node: self.link_item_activated.emit(node)
        )
示例#7
0
    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")

        # The layout where all the tab/pages are placed
        self.__contentsLayout = QVBoxLayout()
        self.__contentsLayout.setContentsMargins(0, 0, 0, 0)
        self.__contentsLayout.setSizeConstraint(QVBoxLayout.SetMinAndMaxSize)
        self.__contentsLayout.setSpacing(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)
示例#8
0
    def __init__(self, *args, **kwargs):
        QGraphicsScene.__init__(self, *args, **kwargs)

        self.scheme = None
        self.registry = None

        # All node items
        self.__node_items = []
        # Mapping from SchemeNodes to canvas items
        self.__item_for_node = {}
        # All link items
        self.__link_items = []
        # Mapping from SchemeLinks to canvas items.
        self.__item_for_link = {}

        # All annotation items
        self.__annotation_items = []
        # Mapping from SchemeAnnotations to canvas items.
        self.__item_for_annotation = {}

        # Is the scene editable
        self.editable = True

        # Anchor Layout
        self.__anchor_layout = AnchorLayout()
        self.addItem(self.__anchor_layout)

        self.__channel_names_visible = True
        self.__node_animation_enabled = True

        self.user_interaction_handler = None

        self.activated_mapper = QSignalMapper(self)
        self.activated_mapper.mapped[QObject].connect(
            lambda node: self.node_item_activated.emit(node)
        )
        self.hovered_mapper = QSignalMapper(self)
        self.hovered_mapper.mapped[QObject].connect(
            lambda node: self.node_item_hovered.emit(node)
        )
        self.position_change_mapper = QSignalMapper(self)
        self.position_change_mapper.mapped[QObject].connect(
            self._on_position_change
        )
        log.info("'%s' intitialized." % self)
示例#9
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()
示例#10
0
class CanvasScene(QGraphicsScene):
    """
    A Graphics Scene for displaying an :class:`~.scheme.Scheme` instance.
    """

    #: Signal emitted when a :class:`NodeItem` has been added to the scene.
    node_item_added = Signal(object)

    #: Signal emitted when a :class:`NodeItem` has been removed from the
    #: scene.
    node_item_removed = Signal(object)

    #: Signal emitted when a new :class:`LinkItem` has been added to the
    #: scene.
    link_item_added = Signal(object)

    #: Signal emitted when a :class:`LinkItem` has been removed.
    link_item_removed = Signal(object)

    #: Signal emitted when a :class:`Annotation` item has been added.
    annotation_added = Signal(object)

    #: Signal emitted when a :class:`Annotation` item has been removed.
    annotation_removed = Signal(object)

    #: Signal emitted when the position of a :class:`NodeItem` has changed.
    node_item_position_changed = Signal(object, QPointF)

    #: Signal emitted when an :class:`NodeItem` has been double clicked.
    node_item_double_clicked = Signal(object)

    #: An node item has been activated (clicked)
    node_item_activated = Signal(object)

    #: An node item has been hovered
    node_item_hovered = Signal(object)

    #: Link item has been hovered
    link_item_hovered = Signal(object)

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

        self.scheme = None
        self.registry = None

        # All node items
        self.__node_items = []
        # Mapping from SchemeNodes to canvas items
        self.__item_for_node = {}
        # All link items
        self.__link_items = []
        # Mapping from SchemeLinks to canvas items.
        self.__item_for_link = {}

        # All annotation items
        self.__annotation_items = []
        # Mapping from SchemeAnnotations to canvas items.
        self.__item_for_annotation = {}

        # Is the scene editable
        self.editable = True

        # Anchor Layout
        self.__anchor_layout = AnchorLayout()
        self.addItem(self.__anchor_layout)

        self.__channel_names_visible = True
        self.__node_animation_enabled = True

        self.user_interaction_handler = None

        self.activated_mapper = QSignalMapper(self)
        self.activated_mapper.mapped[QObject].connect(
            lambda node: self.node_item_activated.emit(node))
        self.hovered_mapper = QSignalMapper(self)
        self.hovered_mapper.mapped[QObject].connect(
            lambda node: self.node_item_hovered.emit(node))
        self.position_change_mapper = QSignalMapper(self)
        self.position_change_mapper.mapped[QObject].connect(
            self._on_position_change)
        log.info("'%s' intitialized." % self)

    def clear_scene(self):
        """
        Clear (reset) the scene.
        """
        if self.scheme is not None:
            self.scheme.node_added.disconnect(self.add_node)
            self.scheme.node_removed.disconnect(self.remove_node)

            self.scheme.link_added.disconnect(self.add_link)
            self.scheme.link_removed.disconnect(self.remove_link)

            self.scheme.annotation_added.disconnect(self.add_annotation)
            self.scheme.annotation_removed.disconnect(self.remove_annotation)

            self.scheme.node_state_changed.disconnect(
                self.on_widget_state_change)
            self.scheme.channel_state_changed.disconnect(
                self.on_link_state_change)

            # Remove all items to make sure all signals from scheme items
            # to canvas items are disconnected.

            for annot in self.scheme.annotations:
                if annot in self.__item_for_annotation:
                    self.remove_annotation(annot)

            for link in self.scheme.links:
                if link in self.__item_for_link:
                    self.remove_link(link)

            for node in self.scheme.nodes:
                if node in self.__item_for_node:
                    self.remove_node(node)

        self.scheme = None
        self.__node_items = []
        self.__item_for_node = {}
        self.__link_items = []
        self.__item_for_link = {}
        self.__annotation_items = []
        self.__item_for_annotation = {}

        self.__anchor_layout.deleteLater()

        self.user_interaction_handler = None

        self.clear()
        log.info("'%s' cleared." % self)

    def set_scheme(self, scheme):
        """
        Set the scheme to display. Populates the scene with nodes and links
        already in the scheme. Any further change to the scheme will be
        reflected in the scene.

        Parameters
        ----------
        scheme : :class:`~.scheme.Scheme`

        """
        if self.scheme is not None:
            # Clear the old scheme
            self.clear_scene()

        log.info("Setting scheme '%s' on '%s'" % (scheme, self))

        self.scheme = scheme
        if self.scheme is not None:
            self.scheme.node_added.connect(self.add_node)
            self.scheme.node_removed.connect(self.remove_node)

            self.scheme.link_added.connect(self.add_link)
            self.scheme.link_removed.connect(self.remove_link)

            self.scheme.annotation_added.connect(self.add_annotation)
            self.scheme.annotation_removed.connect(self.remove_annotation)

            self.scheme.node_state_changed.connect(self.on_widget_state_change)
            self.scheme.channel_state_changed.connect(
                self.on_link_state_change)

            self.scheme.topology_changed.connect(self.on_scheme_change)

        for node in scheme.nodes:
            self.add_node(node)

        for link in scheme.links:
            self.add_link(link)

        for annot in scheme.annotations:
            self.add_annotation(annot)

    def set_registry(self, registry):
        """
        Set the widget registry.
        """
        # TODO: Remove/Deprecate. Is used only to get the category/background
        # color. That should be part of the SchemeNode/WidgetDescription.
        log.info("Setting registry '%s on '%s'." % (registry, self))
        self.registry = registry

    def set_anchor_layout(self, layout):
        """
        Set an :class:`~.layout.AnchorLayout`
        """
        if self.__anchor_layout != layout:
            if self.__anchor_layout:
                self.__anchor_layout.deleteLater()
                self.__anchor_layout = None

            self.__anchor_layout = layout

    def anchor_layout(self):
        """
        Return the anchor layout instance.
        """
        return self.__anchor_layout

    def set_channel_names_visible(self, visible):
        """
        Set the channel names visibility.
        """
        self.__channel_names_visible = visible
        for link in self.__link_items:
            link.setChannelNamesVisible(visible)

    def channel_names_visible(self):
        """
        Return the channel names visibility state.
        """
        return self.__channel_names_visible

    def set_node_animation_enabled(self, enabled):
        """
        Set node animation enabled state.
        """
        if self.__node_animation_enabled != enabled:
            self.__node_animation_enabled = enabled

            for node in self.__node_items:
                node.setAnimationEnabled(enabled)

    def add_node_item(self, item):
        """
        Add a :class:`.NodeItem` instance to the scene.
        """
        if item in self.__node_items:
            raise ValueError("%r is already in the scene." % item)

        if item.pos().isNull():
            if self.__node_items:
                pos = self.__node_items[-1].pos() + QPointF(150, 0)
            else:
                pos = QPointF(150, 150)

            item.setPos(pos)

        item.setFont(self.font())

        # Set signal mappings
        self.activated_mapper.setMapping(item, item)
        item.activated.connect(self.activated_mapper.map)

        self.hovered_mapper.setMapping(item, item)
        item.hovered.connect(self.hovered_mapper.map)

        self.position_change_mapper.setMapping(item, item)
        item.positionChanged.connect(self.position_change_mapper.map)

        self.addItem(item)

        self.__node_items.append(item)

        self.node_item_added.emit(item)

        log.info("Added item '%s' to '%s'" % (item, self))
        return item

    def add_node(self, node):
        """
        Add and return a default constructed :class:`.NodeItem` for a
        :class:`SchemeNode` instance `node`. If the `node` is already in
        the scene do nothing and just return its item.

        """
        if node in self.__item_for_node:
            # Already added
            return self.__item_for_node[node]

        item = self.new_node_item(node.description)

        if node.position:
            pos = QPointF(*node.position)
            item.setPos(pos)

        item.setTitle(node.title)
        item.setProcessingState(node.processing_state)
        item.setProgress(node.progress)

        for message in node.state_messages():
            item.setStateMessage(message)

        item.setStatusMessage(node.status_message())

        self.__item_for_node[node] = item

        node.position_changed.connect(self.__on_node_pos_changed)
        node.title_changed.connect(item.setTitle)
        node.progress_changed.connect(item.setProgress)
        node.processing_state_changed.connect(item.setProcessingState)
        node.state_message_changed.connect(item.setStateMessage)
        node.status_message_changed.connect(item.setStatusMessage)

        return self.add_node_item(item)

    def new_node_item(self, widget_desc, category_desc=None):
        """
        Construct an new :class:`.NodeItem` from a `WidgetDescription`.
        Optionally also set `CategoryDescription`.

        """
        item = items.NodeItem()
        item.setWidgetDescription(widget_desc)

        if category_desc is None and self.registry and widget_desc.category:
            category_desc = self.registry.category(widget_desc.category)

        if category_desc is None and self.registry is not None:
            try:
                category_desc = self.registry.category(widget_desc.category)
            except KeyError:
                pass

        if category_desc is not None:
            item.setWidgetCategory(category_desc)

        item.setAnimationEnabled(self.__node_animation_enabled)
        return item

    def remove_node_item(self, item):
        """
        Remove `item` (:class:`.NodeItem`) from the scene.
        """
        self.activated_mapper.removeMappings(item)
        self.hovered_mapper.removeMappings(item)
        self.position_change_mapper.removeMappings(item)

        item.hide()
        self.removeItem(item)
        self.__node_items.remove(item)

        self.node_item_removed.emit(item)

        log.info("Removed item '%s' from '%s'" % (item, self))

    def remove_node(self, node):
        """
        Remove the :class:`.NodeItem` instance that was previously
        constructed for a :class:`SchemeNode` `node` using the `add_node`
        method.

        """
        item = self.__item_for_node.pop(node)

        node.position_changed.disconnect(self.__on_node_pos_changed)
        node.title_changed.disconnect(item.setTitle)
        node.progress_changed.disconnect(item.setProgress)
        node.processing_state_changed.disconnect(item.setProcessingState)
        node.state_message_changed.disconnect(item.setStateMessage)

        self.remove_node_item(item)

    def node_items(self):
        """
        Return all :class:`.NodeItem` instances in the scene.
        """
        return list(self.__node_items)

    def add_link_item(self, item):
        """
        Add a link (:class:`.LinkItem`) to the scene.
        """
        if item.scene() is not self:
            self.addItem(item)

        item.setFont(self.font())
        self.__link_items.append(item)

        self.link_item_added.emit(item)

        log.info("Added link %r -> %r to '%s'" % \
                 (item.sourceItem.title(), item.sinkItem.title(), self))

        self.__anchor_layout.invalidateLink(item)

        return item

    def add_link(self, scheme_link):
        """
        Create and add a :class:`.LinkItem` instance for a
        :class:`SchemeLink` instance. If the link is already in the scene
        do nothing and just return its :class:`.LinkItem`.

        """
        if scheme_link in self.__item_for_link:
            return self.__item_for_link[scheme_link]

        source = self.__item_for_node[scheme_link.source_node]
        sink = self.__item_for_node[scheme_link.sink_node]

        item = self.new_link_item(source, scheme_link.source_channel, sink,
                                  scheme_link.sink_channel)

        item.setEnabled(scheme_link.enabled)
        scheme_link.enabled_changed.connect(item.setEnabled)

        if scheme_link.is_dynamic():
            item.setDynamic(True)
            item.setDynamicEnabled(scheme_link.dynamic_enabled)
            scheme_link.dynamic_enabled_changed.connect(item.setDynamicEnabled)

        item.setRuntimeState(scheme_link.runtime_state())
        scheme_link.state_changed.connect(item.setRuntimeState)

        self.add_link_item(item)
        self.__item_for_link[scheme_link] = item
        return item

    def new_link_item(self, source_item, source_channel, sink_item,
                      sink_channel):
        """
        Construct and return a new :class:`.LinkItem`
        """
        item = items.LinkItem()
        item.setSourceItem(source_item)
        item.setSinkItem(sink_item)

        def channel_name(channel):
            if isinstance(channel, str):
                return channel
            else:
                return channel.name

        source_name = channel_name(source_channel)
        sink_name = channel_name(sink_channel)

        fmt = "<b>{0}</b>&nbsp; \u2192 &nbsp;<b>{1}</b>"
        item.setToolTip(fmt.format(escape(source_name), escape(sink_name)))

        item.setSourceName(source_name)
        item.setSinkName(sink_name)
        item.setChannelNamesVisible(self.__channel_names_visible)

        return item

    def remove_link_item(self, item):
        """
        Remove a link (:class:`.LinkItem`) from the scene.
        """
        # Invalidate the anchor layout.
        self.__anchor_layout.invalidateAnchorItem(
            item.sourceItem.outputAnchorItem)
        self.__anchor_layout.invalidateAnchorItem(
            item.sinkItem.inputAnchorItem)

        self.__link_items.remove(item)

        # Remove the anchor points.
        item.removeLink()
        self.removeItem(item)

        self.link_item_removed.emit(item)

        log.info("Removed link '%s' from '%s'" % (item, self))

        return item

    def remove_link(self, scheme_link):
        """
        Remove a :class:`.LinkItem` instance that was previously constructed
        for a :class:`SchemeLink` instance `link` using the `add_link` method.

        """
        item = self.__item_for_link.pop(scheme_link)
        scheme_link.enabled_changed.disconnect(item.setEnabled)

        if scheme_link.is_dynamic():
            scheme_link.dynamic_enabled_changed.disconnect(
                item.setDynamicEnabled)
        scheme_link.state_changed.disconnect(item.setRuntimeState)
        self.remove_link_item(item)

    def link_items(self):
        """
        Return all :class:`.LinkItem`\s in the scene.
        """
        return list(self.__link_items)

    def add_annotation_item(self, annotation):
        """
        Add an :class:`.Annotation` item to the scene.
        """
        self.__annotation_items.append(annotation)
        self.addItem(annotation)
        self.annotation_added.emit(annotation)
        return annotation

    def add_annotation(self, scheme_annot):
        """
        Create a new item for :class:`SchemeAnnotation` and add it
        to the scene. If the `scheme_annot` is already in the scene do
        nothing and just return its item.

        """
        if scheme_annot in self.__item_for_annotation:
            # Already added
            return self.__item_for_annotation[scheme_annot]

        if isinstance(scheme_annot, scheme.SchemeTextAnnotation):
            item = items.TextAnnotation()
            x, y, w, h = scheme_annot.rect
            item.setPos(x, y)
            item.resize(w, h)
            item.setTextInteractionFlags(Qt.TextEditorInteraction)

            font = font_from_dict(scheme_annot.font, item.font())
            item.setFont(font)
            item.setContent(scheme_annot.content, scheme_annot.content_type)
            scheme_annot.content_changed.connect(item.setContent)
        elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation):
            item = items.ArrowAnnotation()
            start, end = scheme_annot.start_pos, scheme_annot.end_pos
            item.setLine(QLineF(QPointF(*start), QPointF(*end)))
            item.setColor(QColor(scheme_annot.color))

        scheme_annot.geometry_changed.connect(
            self.__on_scheme_annot_geometry_change)

        self.add_annotation_item(item)
        self.__item_for_annotation[scheme_annot] = item

        return item

    def remove_annotation_item(self, annotation):
        """
        Remove an :class:`.Annotation` instance from the scene.

        """
        self.__annotation_items.remove(annotation)
        self.removeItem(annotation)
        self.annotation_removed.emit(annotation)

    def remove_annotation(self, scheme_annotation):
        """
        Remove an :class:`.Annotation` instance that was previously added
        using :func:`add_anotation`.

        """
        item = self.__item_for_annotation.pop(scheme_annotation)

        scheme_annotation.geometry_changed.disconnect(
            self.__on_scheme_annot_geometry_change)

        if isinstance(scheme_annotation, scheme.SchemeTextAnnotation):
            scheme_annotation.content_changed.disconnect(item.setContent)
        self.remove_annotation_item(item)

    def annotation_items(self):
        """
        Return all :class:`.Annotation` items in the scene.
        """
        return self.__annotation_items

    def item_for_annotation(self, scheme_annotation):
        return self.__item_for_annotation[scheme_annotation]

    def annotation_for_item(self, item):
        rev = dict(reversed(item) \
                   for item in self.__item_for_annotation.items())
        return rev[item]

    def commit_scheme_node(self, node):
        """
        Commit the `node` into the scheme.
        """
        if not self.editable:
            raise Exception("Scheme not editable.")

        if node not in self.__item_for_node:
            raise ValueError("No 'NodeItem' for node.")

        item = self.__item_for_node[node]

        try:
            self.scheme.add_node(node)
        except Exception:
            log.error("An error occurred while committing node '%s'",
                      node,
                      exc_info=True)
            # Cleanup (remove the node item)
            self.remove_node_item(item)
            raise

        log.info("Commited node '%s' from '%s' to '%s'" % \
                 (node, self, self.scheme))

    def commit_scheme_link(self, link):
        """
        Commit a scheme link.
        """
        if not self.editable:
            raise Exception("Scheme not editable")

        if link not in self.__item_for_link:
            raise ValueError("No 'LinkItem' for link.")

        self.scheme.add_link(link)
        log.info("Commited link '%s' from '%s' to '%s'" % \
                 (link, self, self.scheme))

    def node_for_item(self, item):
        """
        Return the `SchemeNode` for the `item`.
        """
        rev = dict([(v, k) for k, v in self.__item_for_node.items()])
        return rev[item]

    def item_for_node(self, node):
        """
        Return the :class:`NodeItem` instance for a :class:`SchemeNode`.
        """
        return self.__item_for_node[node]

    def link_for_item(self, item):
        """
        Return the `SchemeLink for `item` (:class:`LinkItem`).
        """
        rev = dict([(v, k) for k, v in self.__item_for_link.items()])
        return rev[item]

    def item_for_link(self, link):
        """
        Return the :class:`LinkItem` for a :class:`SchemeLink`
        """
        return self.__item_for_link[link]

    def selected_node_items(self):
        """
        Return the selected :class:`NodeItem`'s.
        """
        return [item for item in self.__node_items if item.isSelected()]

    def selected_annotation_items(self):
        """
        Return the selected :class:`Annotation`'s
        """
        return [item for item in self.__annotation_items if item.isSelected()]

    def node_links(self, node_item):
        """
        Return all links from the `node_item` (:class:`NodeItem`).
        """
        return self.node_output_links(node_item) + \
               self.node_input_links(node_item)

    def node_output_links(self, node_item):
        """
        Return a list of all output links from `node_item`.
        """
        return [
            link for link in self.__link_items if link.sourceItem == node_item
        ]

    def node_input_links(self, node_item):
        """
        Return a list of all input links for `node_item`.
        """
        return [
            link for link in self.__link_items if link.sinkItem == node_item
        ]

    def neighbor_nodes(self, node_item):
        """
        Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes.
        """
        neighbors = list(
            map(attrgetter("sourceItem"), self.node_input_links(node_item)))

        neighbors.extend(
            map(attrgetter("sinkItem"), self.node_output_links(node_item)))
        return neighbors

    def on_widget_state_change(self, widget, state):
        pass

    def on_link_state_change(self, link, state):
        pass

    def on_scheme_change(self, ):
        pass

    def _on_position_change(self, item):
        # Invalidate the anchor point layout and schedule a layout.
        self.__anchor_layout.invalidateNode(item)

        self.node_item_position_changed.emit(item, item.pos())

    def __on_node_pos_changed(self, pos):
        node = self.sender()
        item = self.__item_for_node[node]
        item.setPos(*pos)

    def __on_scheme_annot_geometry_change(self):
        annot = self.sender()
        item = self.__item_for_annotation[annot]
        if isinstance(annot, scheme.SchemeTextAnnotation):
            item.setGeometry(QRectF(*annot.rect))
        elif isinstance(annot, scheme.SchemeArrowAnnotation):
            p1 = item.mapFromScene(QPointF(*annot.start_pos))
            p2 = item.mapFromScene(QPointF(*annot.end_pos))
            item.setLine(QLineF(p1, p2))
        else:
            pass

    def item_at(self, pos, type_or_tuple=None, buttons=0):
        """Return the item at `pos` that is an instance of the specified
        type (`type_or_tuple`). If `buttons` (`Qt.MouseButtons`) is given
        only return the item if it is the top level item that would
        accept any of the buttons (`QGraphicsItem.acceptedMouseButtons`).

        """
        rect = QRectF(pos, QSizeF(1, 1))
        items = self.items(rect)

        if buttons:
            items = itertools.dropwhile(
                lambda item: not item.acceptedMouseButtons() & buttons, items)
            items = list(items)[:1]

        if type_or_tuple:
            items = [i for i in items if isinstance(i, type_or_tuple)]

        return items[0] if items else None

    if USE_PYQT and PYQT_VERSION < 0x40900:
        # For QGraphicsObject subclasses items, itemAt ... return a
        # QGraphicsItem wrapper instance and not the actual class instance.
        def itemAt(self, *args, **kwargs):
            item = QGraphicsScene.itemAt(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

        def items(self, *args, **kwargs):
            items = QGraphicsScene.items(self, *args, **kwargs)
            return list(map(toGraphicsObjectIfPossible, items))

        def selectedItems(self, *args, **kwargs):
            return list(
                map(toGraphicsObjectIfPossible,
                    QGraphicsScene.selectedItems(self, *args, **kwargs)))

        def collidingItems(self, *args, **kwargs):
            return list(
                map(toGraphicsObjectIfPossible,
                    QGraphicsScene.collidingItems(self, *args, **kwargs)))

        def focusItem(self, *args, **kwargs):
            item = QGraphicsScene.focusItem(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

        def mouseGrabberItem(self, *args, **kwargs):
            item = QGraphicsScene.mouseGrabberItem(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

    def mousePressEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mousePressEvent(event):
            return

        # Right (context) click on the node item. If the widget is not
        # in the current selection then select the widget (only the widget).
        # Else simply return and let customContextMenuRequested signal
        # handle it
        shape_item = self.item_at(event.scenePos(), items.NodeItem)
        if shape_item and event.button() == Qt.RightButton and \
                shape_item.flags() & QGraphicsItem.ItemIsSelectable:
            if not shape_item.isSelected():
                self.clearSelection()
                shape_item.setSelected(True)

        return QGraphicsScene.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mouseMoveEvent(event):
            return

        return QGraphicsScene.mouseMoveEvent(self, event)

    def mouseReleaseEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mouseReleaseEvent(event):
            return
        return QGraphicsScene.mouseReleaseEvent(self, event)

    def mouseDoubleClickEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mouseDoubleClickEvent(event):
            return

        return QGraphicsScene.mouseDoubleClickEvent(self, event)

    def keyPressEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.keyPressEvent(event):
            return
        return QGraphicsScene.keyPressEvent(self, event)

    def keyReleaseEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.keyReleaseEvent(event):
            return
        return QGraphicsScene.keyReleaseEvent(self, event)

    def contextMenuEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.contextMenuEvent(event):
            return
        super().contextMenuEvent(event)

    def set_user_interaction_handler(self, handler):
        if self.user_interaction_handler and \
                not self.user_interaction_handler.isFinished():
            self.user_interaction_handler.cancel()

        log.info("Setting interaction '%s' to '%s'" % (handler, self))

        self.user_interaction_handler = handler
        if handler:
            handler.start()

    def event(self, event):
        # TODO: change the base class of Node/LinkItem to QGraphicsWidget.
        # It already handles font changes.
        if event.type() == QEvent.FontChange:
            self.__update_font()

        return QGraphicsScene.event(self, event)

    def __update_font(self):
        font = self.font()
        for item in self.__node_items + self.__link_items:
            item.setFont(font)

    def __str__(self):
        return "%s(objectName=%r, ...)" % \
                (type(self).__name__, str(self.objectName()))
示例#11
0
class ToolGrid(QFrame):
    """
    A widget containing a grid of actions/buttons.

    Actions can be added using standard :func:`QWidget.addAction(QAction)`
    and :func:`QWidget.insertAction(int, QAction)` methods.

    Parameters
    ----------
    parent : :class:`QWidget`
        Parent widget.
    columns : int
        Number of columns in the grid layout.
    buttonSize : QSize
        Size of tool buttons in the grid.
    iconSize : QSize
        Size of icons in the buttons.
    toolButtonStyle : :class:`Qt.ToolButtonStyle`
        Tool button style.
    """
    #: Signal emitted when an action is triggered
    actionTriggered = Signal(QAction)
    #: Signal emitted when an action is hovered
    actionHovered = Signal(QAction)

    def __init__(self,
                 parent=None, columns=4, buttonSize=QSize(),
                 iconSize=QSize(), toolButtonStyle=Qt.ToolButtonTextUnderIcon,
                 **kwargs):
        # type: (Optional[QWidget], int, QSize, QSize, Qt.ToolButtonStyle, Any) -> None
        sizePolicy = kwargs.pop("sizePolicy", None)  # type: Optional[QSizePolicy]
        super().__init__(parent, **kwargs)

        if buttonSize is None:
            buttonSize = QSize()
        if iconSize is None:
            iconSize = QSize()

        self.__columns = columns
        self.__buttonSize = QSize(buttonSize)
        self.__iconSize = QSize(iconSize)
        self.__toolButtonStyle = toolButtonStyle

        self.__gridSlots = []  # type: List[_ToolGridSlot]
        self.__mapper = QSignalMapper()
        self.__mapper.mapped[QObject].connect(self.__onClicked)

        layout = QGridLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        self.setLayout(layout)
        if sizePolicy is None:
            self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
            self.setAttribute(Qt.WA_WState_OwnSizePolicy, True)
        else:
            self.setSizePolicy(sizePolicy)

    def setButtonSize(self, size):
        # type: (QSize) -> None
        """
        Set the button size.
        """
        if self.__buttonSize != size:
            self.__buttonSize = QSize(size)
            for slot in self.__gridSlots:
                slot.button.setFixedSize(size)

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

    def setIconSize(self, size):
        # type: (QSize) -> None
        """
        Set the button icon size.

        The default icon size is style defined.
        """
        if self.__iconSize != size:
            self.__iconSize = QSize(size)
            size = self.__effectiveIconSize()
            for slot in self.__gridSlots:
                slot.button.setIconSize(size)

    def iconSize(self):
        # type: () -> QSize
        """
        Return the icon size. If no size is set a default style defined size
        is returned.
        """
        return self.__effectiveIconSize()

    def __effectiveIconSize(self):
        # type: () -> QSize
        if not self.__iconSize.isValid():
            opt = QStyleOptionToolButton()
            opt.initFrom(self)
            s = self.style().pixelMetric(QStyle.PM_LargeIconSize, opt, None)
            return QSize(s, s)
        else:
            return QSize(self.__iconSize)

    def changeEvent(self, event):
        # type: (QEvent) -> None
        if event.type() == QEvent.StyleChange:
            size = self.__effectiveIconSize()
            for item in self.__gridSlots:
                item.button.setIconSize(size)
        super().changeEvent(event)

    def setToolButtonStyle(self, style):
        # type: (Qt.ToolButtonStyle) -> None
        """
        Set the tool button style.
        """
        if self.__toolButtonStyle != style:
            self.__toolButtonStyle = style
            for slot in self.__gridSlots:
                slot.button.setToolButtonStyle(style)

    def toolButtonStyle(self):
        # type: () -> Qt.ToolButtonStyle
        """
        Return the tool button style.
        """
        return self.__toolButtonStyle

    def setColumnCount(self, columns):
        # type: (int) -> None
        """
        Set the number of button/action columns.
        """
        if self.__columns != columns:
            self.__columns = columns
            self.__relayout()

    def columns(self):
        # type: () -> int
        """
        Return the number of columns in the grid.
        """
        return self.__columns

    def clear(self):
        # type: () -> None
        """
        Clear all actions/buttons.
        """
        for slot in reversed(list(self.__gridSlots)):
            self.removeAction(slot.action)
        self.__gridSlots = []

    def insertAction(self, before, action):
        # type: (Union[QAction, int], QAction) -> None
        """
        Insert a new action at the position currently occupied
        by `before` (can also be an index).

        Parameters
        ----------
        before : :class:`QAction` or int
            Position where the `action` should be inserted.
        action : :class:`QAction`
            Action to insert
        """
        if isinstance(before, int):
            actions = list(self.actions())
            if len(actions) == 0 or before >= len(actions):
                # Insert as the first action or the last action.
                return self.addAction(action)

            before = actions[before]

        return super().insertAction(before, action)

    def setActions(self, actions):
        # type: (Iterable[QAction]) -> None
        """
        Clear the grid and add `actions`.
        """
        self.clear()

        for action in actions:
            self.addAction(action)

    def buttonForAction(self, action):
        # type: (QAction) -> QToolButton
        """
        Return the :class:`QToolButton` instance button for `action`.
        """
        actions = [slot.action for slot in self.__gridSlots]
        index = actions.index(action)
        return self.__gridSlots[index].button

    def createButtonForAction(self, action):
        # type: (QAction) -> QToolButton
        """
        Create and return a :class:`QToolButton` for action.
        """
        button = ToolGridButton(self)
        button.setDefaultAction(action)

        if self.__buttonSize.isValid():
            button.setFixedSize(self.__buttonSize)
        button.setIconSize(self.__effectiveIconSize())
        button.setToolButtonStyle(self.__toolButtonStyle)
        button.setProperty("tool-grid-button", True)
        return button

    def count(self):
        # type: () -> int
        """
        Return the number of buttons/actions in the grid.
        """
        return len(self.__gridSlots)

    def actionEvent(self, event):
        # type: (QActionEvent) -> None
        super().actionEvent(event)

        if event.type() == QEvent.ActionAdded:
            # Note: the action is already in the self.actions() list.
            actions = list(self.actions())
            index = actions.index(event.action())
            self.__insertActionButton(index, event.action())

        elif event.type() == QEvent.ActionRemoved:
            self.__removeActionButton(event.action())

    def __insertActionButton(self, index, action):
        # type: (int, QAction) -> None
        """Create a button for the action and add it to the layout at index.
        """
        self.__shiftGrid(index, 1)
        button = self.createButtonForAction(action)

        row = index // self.__columns
        column = index % self.__columns

        layout = self.layout()
        assert isinstance(layout, QGridLayout)
        layout.addWidget(button, row, column)

        self.__gridSlots.insert(
            index, _ToolGridSlot(button, action, row, column)
        )

        self.__mapper.setMapping(button, action)
        button.clicked.connect(self.__mapper.map)
        button.installEventFilter(self)

    def __removeActionButton(self, action):
        # type: (QAction) -> None
        """Remove the button for the action from the layout and delete it.
        """
        actions = [slot.action for slot in self.__gridSlots]
        index = actions.index(action)
        slot = self.__gridSlots.pop(index)

        slot.button.removeEventFilter(self)
        self.__mapper.removeMappings(slot.button)

        self.layout().removeWidget(slot.button)
        self.__shiftGrid(index + 1, -1)

        slot.button.deleteLater()

    def __shiftGrid(self, start, count=1):
        # type: (int, int) -> None
        """Shift all buttons starting at index `start` by `count` cells.
        """
        layout = self.layout()
        assert isinstance(layout, QGridLayout)
        button_count = layout.count()
        columns = self.__columns

        direction = 1 if count >= 0 else -1
        if direction == 1:
            start, end = button_count - 1, start - 1
        else:
            start, end = start, button_count

        for index in range(start, end, -direction):
            item = layout.itemAtPosition(
                index // columns, index % columns
            )
            if item:
                button = item.widget()
                new_index = index + count
                layout.addWidget(
                    button, new_index // columns, new_index % columns,
                )

    def __relayout(self):
        # type: () -> None
        """Relayout the buttons.
        """
        layout = self.layout()
        assert isinstance(layout, QGridLayout)

        for i in reversed(range(layout.count())):
            layout.takeAt(i)

        self.__gridSlots = [
            _ToolGridSlot(slot.button, slot.action,
                          i // self.__columns, i % self.__columns)
            for i, slot in enumerate(self.__gridSlots)
        ]
        for slot in self.__gridSlots:
            layout.addWidget(slot.button, slot.row, slot.column)

    def __indexOf(self, button):
        # type: (QWidget) -> int
        """Return the index of button widget.
        """
        buttons = [slot.button for slot in self.__gridSlots]
        return buttons.index(button)

    def __onButtonEnter(self, button):
        # type: (QToolButton) -> None
        action = button.defaultAction()
        self.actionHovered.emit(action)

    @Slot(QObject)
    def __onClicked(self, action):
        # type: (QAction) -> None
        assert isinstance(action, QAction)
        self.actionTriggered.emit(action)

    def eventFilter(self, obj, event):
        # type: (QObject, QEvent) -> bool
        etype = event.type()
        if etype == QEvent.KeyPress and obj.hasFocus():
            key = event.key()
            if key in [Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]:
                if self.__focusMove(obj, key):
                    event.accept()
                    return True
        elif etype == QEvent.HoverEnter and obj.parent() is self:
            self.__onButtonEnter(obj)
        return super().eventFilter(obj, event)

    def __focusMove(self, focus, key):
        # type: (QWidget, Qt.Key) -> bool
        assert focus is self.focusWidget()
        try:
            index = self.__indexOf(focus)
        except IndexError:
            return False

        if key == Qt.Key_Down:
            index += self.__columns
        elif key == Qt.Key_Up:
            index -= self.__columns
        elif key == Qt.Key_Left:
            index -= 1
        elif key == Qt.Key_Right:
            index += 1

        if 0 <= index < self.count():
            button = self.__gridSlots[index].button
            button.setFocus(Qt.TabFocusReason)
            return True
        else:
            return False
示例#12
0
class CanvasScene(QGraphicsScene):
    """
    A Graphics Scene for displaying an :class:`~.scheme.Scheme` instance.
    """

    #: Signal emitted when a :class:`NodeItem` has been added to the scene.
    node_item_added = Signal(object)

    #: Signal emitted when a :class:`NodeItem` has been removed from the
    #: scene.
    node_item_removed = Signal(object)

    #: Signal emitted when a new :class:`LinkItem` has been added to the
    #: scene.
    link_item_added = Signal(object)

    #: Signal emitted when a :class:`LinkItem` has been removed.
    link_item_removed = Signal(object)

    #: Signal emitted when a :class:`Annotation` item has been added.
    annotation_added = Signal(object)

    #: Signal emitted when a :class:`Annotation` item has been removed.
    annotation_removed = Signal(object)

    #: Signal emitted when the position of a :class:`NodeItem` has changed.
    node_item_position_changed = Signal(object, QPointF)

    #: Signal emitted when an :class:`NodeItem` has been double clicked.
    node_item_double_clicked = Signal(object)

    #: An node item has been activated (clicked)
    node_item_activated = Signal(object)

    #: An node item has been hovered
    node_item_hovered = Signal(object)

    #: Link item has been hovered
    link_item_hovered = Signal(object)

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

        self.scheme = None
        self.registry = None

        # All node items
        self.__node_items = []
        # Mapping from SchemeNodes to canvas items
        self.__item_for_node = {}
        # All link items
        self.__link_items = []
        # Mapping from SchemeLinks to canvas items.
        self.__item_for_link = {}

        # All annotation items
        self.__annotation_items = []
        # Mapping from SchemeAnnotations to canvas items.
        self.__item_for_annotation = {}

        # Is the scene editable
        self.editable = True

        # Anchor Layout
        self.__anchor_layout = AnchorLayout()
        self.addItem(self.__anchor_layout)

        self.__channel_names_visible = True
        self.__node_animation_enabled = True

        self.user_interaction_handler = None

        self.activated_mapper = QSignalMapper(self)
        self.activated_mapper.mapped[QObject].connect(
            lambda node: self.node_item_activated.emit(node)
        )
        self.hovered_mapper = QSignalMapper(self)
        self.hovered_mapper.mapped[QObject].connect(
            lambda node: self.node_item_hovered.emit(node)
        )
        self.position_change_mapper = QSignalMapper(self)
        self.position_change_mapper.mapped[QObject].connect(
            self._on_position_change
        )
        log.info("'%s' intitialized." % self)

    def clear_scene(self):
        """
        Clear (reset) the scene.
        """
        if self.scheme is not None:
            self.scheme.node_added.disconnect(self.add_node)
            self.scheme.node_removed.disconnect(self.remove_node)

            self.scheme.link_added.disconnect(self.add_link)
            self.scheme.link_removed.disconnect(self.remove_link)

            self.scheme.annotation_added.disconnect(self.add_annotation)
            self.scheme.annotation_removed.disconnect(self.remove_annotation)

            self.scheme.node_state_changed.disconnect(
                self.on_widget_state_change
            )
            self.scheme.channel_state_changed.disconnect(
                self.on_link_state_change
            )

            # Remove all items to make sure all signals from scheme items
            # to canvas items are disconnected.

            for annot in self.scheme.annotations:
                if annot in self.__item_for_annotation:
                    self.remove_annotation(annot)

            for link in self.scheme.links:
                if link in self.__item_for_link:
                    self.remove_link(link)

            for node in self.scheme.nodes:
                if node in self.__item_for_node:
                    self.remove_node(node)

        self.scheme = None
        self.__node_items = []
        self.__item_for_node = {}
        self.__link_items = []
        self.__item_for_link = {}
        self.__annotation_items = []
        self.__item_for_annotation = {}

        self.__anchor_layout.deleteLater()

        self.user_interaction_handler = None

        self.clear()
        log.info("'%s' cleared." % self)

    def set_scheme(self, scheme):
        """
        Set the scheme to display. Populates the scene with nodes and links
        already in the scheme. Any further change to the scheme will be
        reflected in the scene.

        Parameters
        ----------
        scheme : :class:`~.scheme.Scheme`

        """
        if self.scheme is not None:
            # Clear the old scheme
            self.clear_scene()

        log.info("Setting scheme '%s' on '%s'" % (scheme, self))

        self.scheme = scheme
        if self.scheme is not None:
            self.scheme.node_added.connect(self.add_node)
            self.scheme.node_removed.connect(self.remove_node)

            self.scheme.link_added.connect(self.add_link)
            self.scheme.link_removed.connect(self.remove_link)

            self.scheme.annotation_added.connect(self.add_annotation)
            self.scheme.annotation_removed.connect(self.remove_annotation)

            self.scheme.node_state_changed.connect(
                self.on_widget_state_change
            )
            self.scheme.channel_state_changed.connect(
                self.on_link_state_change
            )

            self.scheme.topology_changed.connect(self.on_scheme_change)

        for node in scheme.nodes:
            self.add_node(node)

        for link in scheme.links:
            self.add_link(link)

        for annot in scheme.annotations:
            self.add_annotation(annot)

    def set_registry(self, registry):
        """
        Set the widget registry.
        """
        # TODO: Remove/Deprecate. Is used only to get the category/background
        # color. That should be part of the SchemeNode/WidgetDescription.
        log.info("Setting registry '%s on '%s'." % (registry, self))
        self.registry = registry

    def set_anchor_layout(self, layout):
        """
        Set an :class:`~.layout.AnchorLayout`
        """
        if self.__anchor_layout != layout:
            if self.__anchor_layout:
                self.__anchor_layout.deleteLater()
                self.__anchor_layout = None

            self.__anchor_layout = layout

    def anchor_layout(self):
        """
        Return the anchor layout instance.
        """
        return self.__anchor_layout

    def set_channel_names_visible(self, visible):
        """
        Set the channel names visibility.
        """
        self.__channel_names_visible = visible
        for link in self.__link_items:
            link.setChannelNamesVisible(visible)

    def channel_names_visible(self):
        """
        Return the channel names visibility state.
        """
        return self.__channel_names_visible

    def set_node_animation_enabled(self, enabled):
        """
        Set node animation enabled state.
        """
        if self.__node_animation_enabled != enabled:
            self.__node_animation_enabled = enabled

            for node in self.__node_items:
                node.setAnimationEnabled(enabled)

    def add_node_item(self, item):
        """
        Add a :class:`.NodeItem` instance to the scene.
        """
        if item in self.__node_items:
            raise ValueError("%r is already in the scene." % item)

        if item.pos().isNull():
            if self.__node_items:
                pos = self.__node_items[-1].pos() + QPointF(150, 0)
            else:
                pos = QPointF(150, 150)

            item.setPos(pos)

        item.setFont(self.font())

        # Set signal mappings
        self.activated_mapper.setMapping(item, item)
        item.activated.connect(self.activated_mapper.map)

        self.hovered_mapper.setMapping(item, item)
        item.hovered.connect(self.hovered_mapper.map)

        self.position_change_mapper.setMapping(item, item)
        item.positionChanged.connect(self.position_change_mapper.map)

        self.addItem(item)

        self.__node_items.append(item)

        self.node_item_added.emit(item)

        log.info("Added item '%s' to '%s'" % (item, self))
        return item

    def add_node(self, node):
        """
        Add and return a default constructed :class:`.NodeItem` for a
        :class:`SchemeNode` instance `node`. If the `node` is already in
        the scene do nothing and just return its item.

        """
        if node in self.__item_for_node:
            # Already added
            return self.__item_for_node[node]

        item = self.new_node_item(node.description)

        if node.position:
            pos = QPointF(*node.position)
            item.setPos(pos)

        item.setTitle(node.title)
        item.setProcessingState(node.processing_state)
        item.setProgress(node.progress)

        for message in node.state_messages():
            item.setStateMessage(message)

        item.setStatusMessage(node.status_message())

        self.__item_for_node[node] = item

        node.position_changed.connect(self.__on_node_pos_changed)
        node.title_changed.connect(item.setTitle)
        node.progress_changed.connect(item.setProgress)
        node.processing_state_changed.connect(item.setProcessingState)
        node.state_message_changed.connect(item.setStateMessage)
        node.status_message_changed.connect(item.setStatusMessage)

        return self.add_node_item(item)

    def new_node_item(self, widget_desc, category_desc=None):
        """
        Construct an new :class:`.NodeItem` from a `WidgetDescription`.
        Optionally also set `CategoryDescription`.

        """
        item = items.NodeItem()
        item.setWidgetDescription(widget_desc)

        if category_desc is None and self.registry and widget_desc.category:
            category_desc = self.registry.category(widget_desc.category)

        if category_desc is None and self.registry is not None:
            try:
                category_desc = self.registry.category(widget_desc.category)
            except KeyError:
                pass

        if category_desc is not None:
            item.setWidgetCategory(category_desc)

        item.setAnimationEnabled(self.__node_animation_enabled)
        return item

    def remove_node_item(self, item):
        """
        Remove `item` (:class:`.NodeItem`) from the scene.
        """
        self.activated_mapper.removeMappings(item)
        self.hovered_mapper.removeMappings(item)
        self.position_change_mapper.removeMappings(item)

        item.hide()
        self.removeItem(item)
        self.__node_items.remove(item)

        self.node_item_removed.emit(item)

        log.info("Removed item '%s' from '%s'" % (item, self))

    def remove_node(self, node):
        """
        Remove the :class:`.NodeItem` instance that was previously
        constructed for a :class:`SchemeNode` `node` using the `add_node`
        method.

        """
        item = self.__item_for_node.pop(node)

        node.position_changed.disconnect(self.__on_node_pos_changed)
        node.title_changed.disconnect(item.setTitle)
        node.progress_changed.disconnect(item.setProgress)
        node.processing_state_changed.disconnect(item.setProcessingState)
        node.state_message_changed.disconnect(item.setStateMessage)

        self.remove_node_item(item)

    def node_items(self):
        """
        Return all :class:`.NodeItem` instances in the scene.
        """
        return list(self.__node_items)

    def add_link_item(self, item):
        """
        Add a link (:class:`.LinkItem`) to the scene.
        """
        if item.scene() is not self:
            self.addItem(item)

        item.setFont(self.font())
        self.__link_items.append(item)

        self.link_item_added.emit(item)

        log.info("Added link %r -> %r to '%s'" % \
                 (item.sourceItem.title(), item.sinkItem.title(), self))

        self.__anchor_layout.invalidateLink(item)

        return item

    def add_link(self, scheme_link):
        """
        Create and add a :class:`.LinkItem` instance for a
        :class:`SchemeLink` instance. If the link is already in the scene
        do nothing and just return its :class:`.LinkItem`.

        """
        if scheme_link in self.__item_for_link:
            return self.__item_for_link[scheme_link]

        source = self.__item_for_node[scheme_link.source_node]
        sink = self.__item_for_node[scheme_link.sink_node]

        item = self.new_link_item(source, scheme_link.source_channel,
                                  sink, scheme_link.sink_channel)

        item.setEnabled(scheme_link.enabled)
        scheme_link.enabled_changed.connect(item.setEnabled)

        if scheme_link.is_dynamic():
            item.setDynamic(True)
            item.setDynamicEnabled(scheme_link.dynamic_enabled)
            scheme_link.dynamic_enabled_changed.connect(item.setDynamicEnabled)

        item.setRuntimeState(scheme_link.runtime_state())
        scheme_link.state_changed.connect(item.setRuntimeState)

        self.add_link_item(item)
        self.__item_for_link[scheme_link] = item
        return item

    def new_link_item(self, source_item, source_channel,
                      sink_item, sink_channel):
        """
        Construct and return a new :class:`.LinkItem`
        """
        item = items.LinkItem()
        item.setSourceItem(source_item)
        item.setSinkItem(sink_item)

        def channel_name(channel):
            if isinstance(channel, str):
                return channel
            else:
                return channel.name

        source_name = channel_name(source_channel)
        sink_name = channel_name(sink_channel)

        fmt = "<b>{0}</b>&nbsp; \u2192 &nbsp;<b>{1}</b>"
        item.setToolTip(
            fmt.format(escape(source_name),
                       escape(sink_name))
        )

        item.setSourceName(source_name)
        item.setSinkName(sink_name)
        item.setChannelNamesVisible(self.__channel_names_visible)

        return item

    def remove_link_item(self, item):
        """
        Remove a link (:class:`.LinkItem`) from the scene.
        """
        # Invalidate the anchor layout.
        self.__anchor_layout.invalidateAnchorItem(
            item.sourceItem.outputAnchorItem
        )
        self.__anchor_layout.invalidateAnchorItem(
            item.sinkItem.inputAnchorItem
        )

        self.__link_items.remove(item)

        # Remove the anchor points.
        item.removeLink()
        self.removeItem(item)

        self.link_item_removed.emit(item)

        log.info("Removed link '%s' from '%s'" % (item, self))

        return item

    def remove_link(self, scheme_link):
        """
        Remove a :class:`.LinkItem` instance that was previously constructed
        for a :class:`SchemeLink` instance `link` using the `add_link` method.

        """
        item = self.__item_for_link.pop(scheme_link)
        scheme_link.enabled_changed.disconnect(item.setEnabled)

        if scheme_link.is_dynamic():
            scheme_link.dynamic_enabled_changed.disconnect(
                item.setDynamicEnabled
            )
        scheme_link.state_changed.disconnect(item.setRuntimeState)
        self.remove_link_item(item)

    def link_items(self):
        """
        Return all :class:`.LinkItem`\s in the scene.
        """
        return list(self.__link_items)

    def add_annotation_item(self, annotation):
        """
        Add an :class:`.Annotation` item to the scene.
        """
        self.__annotation_items.append(annotation)
        self.addItem(annotation)
        self.annotation_added.emit(annotation)
        return annotation

    def add_annotation(self, scheme_annot):
        """
        Create a new item for :class:`SchemeAnnotation` and add it
        to the scene. If the `scheme_annot` is already in the scene do
        nothing and just return its item.

        """
        if scheme_annot in self.__item_for_annotation:
            # Already added
            return self.__item_for_annotation[scheme_annot]

        if isinstance(scheme_annot, scheme.SchemeTextAnnotation):
            item = items.TextAnnotation()
            x, y, w, h = scheme_annot.rect
            item.setPos(x, y)
            item.resize(w, h)
            item.setTextInteractionFlags(Qt.TextEditorInteraction)

            font = font_from_dict(scheme_annot.font, item.font())
            item.setFont(font)
            item.setContent(scheme_annot.content, scheme_annot.content_type)
            scheme_annot.content_changed.connect(item.setContent)
        elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation):
            item = items.ArrowAnnotation()
            start, end = scheme_annot.start_pos, scheme_annot.end_pos
            item.setLine(QLineF(QPointF(*start), QPointF(*end)))
            item.setColor(QColor(scheme_annot.color))

        scheme_annot.geometry_changed.connect(
            self.__on_scheme_annot_geometry_change
        )

        self.add_annotation_item(item)
        self.__item_for_annotation[scheme_annot] = item

        return item

    def remove_annotation_item(self, annotation):
        """
        Remove an :class:`.Annotation` instance from the scene.

        """
        self.__annotation_items.remove(annotation)
        self.removeItem(annotation)
        self.annotation_removed.emit(annotation)

    def remove_annotation(self, scheme_annotation):
        """
        Remove an :class:`.Annotation` instance that was previously added
        using :func:`add_anotation`.

        """
        item = self.__item_for_annotation.pop(scheme_annotation)

        scheme_annotation.geometry_changed.disconnect(
            self.__on_scheme_annot_geometry_change
        )

        if isinstance(scheme_annotation, scheme.SchemeTextAnnotation):
            scheme_annotation.content_changed.disconnect(item.setContent)
        self.remove_annotation_item(item)

    def annotation_items(self):
        """
        Return all :class:`.Annotation` items in the scene.
        """
        return self.__annotation_items

    def item_for_annotation(self, scheme_annotation):
        return self.__item_for_annotation[scheme_annotation]

    def annotation_for_item(self, item):
        rev = dict(reversed(item) \
                   for item in self.__item_for_annotation.items())
        return rev[item]

    def commit_scheme_node(self, node):
        """
        Commit the `node` into the scheme.
        """
        if not self.editable:
            raise Exception("Scheme not editable.")

        if node not in self.__item_for_node:
            raise ValueError("No 'NodeItem' for node.")

        item = self.__item_for_node[node]

        try:
            self.scheme.add_node(node)
        except Exception:
            log.error("An error occurred while committing node '%s'",
                      node, exc_info=True)
            # Cleanup (remove the node item)
            self.remove_node_item(item)
            raise

        log.info("Commited node '%s' from '%s' to '%s'" % \
                 (node, self, self.scheme))

    def commit_scheme_link(self, link):
        """
        Commit a scheme link.
        """
        if not self.editable:
            raise Exception("Scheme not editable")

        if link not in self.__item_for_link:
            raise ValueError("No 'LinkItem' for link.")

        self.scheme.add_link(link)
        log.info("Commited link '%s' from '%s' to '%s'" % \
                 (link, self, self.scheme))

    def node_for_item(self, item):
        """
        Return the `SchemeNode` for the `item`.
        """
        rev = dict([(v, k) for k, v in self.__item_for_node.items()])
        return rev[item]

    def item_for_node(self, node):
        """
        Return the :class:`NodeItem` instance for a :class:`SchemeNode`.
        """
        return self.__item_for_node[node]

    def link_for_item(self, item):
        """
        Return the `SchemeLink for `item` (:class:`LinkItem`).
        """
        rev = dict([(v, k) for k, v in self.__item_for_link.items()])
        return rev[item]

    def item_for_link(self, link):
        """
        Return the :class:`LinkItem` for a :class:`SchemeLink`
        """
        return self.__item_for_link[link]

    def selected_node_items(self):
        """
        Return the selected :class:`NodeItem`'s.
        """
        return [item for item in self.__node_items if item.isSelected()]

    def selected_annotation_items(self):
        """
        Return the selected :class:`Annotation`'s
        """
        return [item for item in self.__annotation_items if item.isSelected()]

    def node_links(self, node_item):
        """
        Return all links from the `node_item` (:class:`NodeItem`).
        """
        return self.node_output_links(node_item) + \
               self.node_input_links(node_item)

    def node_output_links(self, node_item):
        """
        Return a list of all output links from `node_item`.
        """
        return [link for link in self.__link_items
                if link.sourceItem == node_item]

    def node_input_links(self, node_item):
        """
        Return a list of all input links for `node_item`.
        """
        return [link for link in self.__link_items
                if link.sinkItem == node_item]

    def neighbor_nodes(self, node_item):
        """
        Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes.
        """
        neighbors = list(map(attrgetter("sourceItem"),
                             self.node_input_links(node_item)))

        neighbors.extend(map(attrgetter("sinkItem"),
                             self.node_output_links(node_item)))
        return neighbors

    def on_widget_state_change(self, widget, state):
        pass

    def on_link_state_change(self, link, state):
        pass

    def on_scheme_change(self, ):
        pass

    def _on_position_change(self, item):
        # Invalidate the anchor point layout and schedule a layout.
        self.__anchor_layout.invalidateNode(item)

        self.node_item_position_changed.emit(item, item.pos())

    def __on_node_pos_changed(self, pos):
        node = self.sender()
        item = self.__item_for_node[node]
        item.setPos(*pos)

    def __on_scheme_annot_geometry_change(self):
        annot = self.sender()
        item = self.__item_for_annotation[annot]
        if isinstance(annot, scheme.SchemeTextAnnotation):
            item.setGeometry(QRectF(*annot.rect))
        elif isinstance(annot, scheme.SchemeArrowAnnotation):
            p1 = item.mapFromScene(QPointF(*annot.start_pos))
            p2 = item.mapFromScene(QPointF(*annot.end_pos))
            item.setLine(QLineF(p1, p2))
        else:
            pass

    def item_at(self, pos, type_or_tuple=None, buttons=0):
        """Return the item at `pos` that is an instance of the specified
        type (`type_or_tuple`). If `buttons` (`Qt.MouseButtons`) is given
        only return the item if it is the top level item that would
        accept any of the buttons (`QGraphicsItem.acceptedMouseButtons`).

        """
        rect = QRectF(pos, QSizeF(1, 1))
        items = self.items(rect)

        if buttons:
            items = itertools.dropwhile(
                lambda item: not item.acceptedMouseButtons() & buttons,
                items
            )
            items = list(items)[:1]

        if type_or_tuple:
            items = [i for i in items if isinstance(i, type_or_tuple)]

        return items[0] if items else None

    if USE_PYQT and PYQT_VERSION < 0x40900:
        # For QGraphicsObject subclasses items, itemAt ... return a
        # QGraphicsItem wrapper instance and not the actual class instance.
        def itemAt(self, *args, **kwargs):
            item = QGraphicsScene.itemAt(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

        def items(self, *args, **kwargs):
            items = QGraphicsScene.items(self, *args, **kwargs)
            return list(map(toGraphicsObjectIfPossible, items))

        def selectedItems(self, *args, **kwargs):
            return [toGraphicsObjectIfPossible(obj)
                    for obj in
                    QGraphicsScene.selectedItems(self, *args, **kwargs)]

        def collidingItems(self, *args, **kwargs):
            return [toGraphicsObjectIfPossible(obj)
                    for obj in
                    QGraphicsScene.collidingItems(self, *args, **kwargs)]

        def focusItem(self, *args, **kwargs):
            item = QGraphicsScene.focusItem(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

        def mouseGrabberItem(self, *args, **kwargs):
            item = QGraphicsScene.mouseGrabberItem(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

    def mousePressEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mousePressEvent(event):
            return

        # Right (context) click on the node item. If the widget is not
        # in the current selection then select the widget (only the widget).
        # Else simply return and let customContextMenuRequested signal
        # handle it
        shape_item = self.item_at(event.scenePos(), items.NodeItem)
        if shape_item and event.button() == Qt.RightButton and \
                shape_item.flags() & QGraphicsItem.ItemIsSelectable:
            if not shape_item.isSelected():
                self.clearSelection()
                shape_item.setSelected(True)

        return QGraphicsScene.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mouseMoveEvent(event):
            return

        return QGraphicsScene.mouseMoveEvent(self, event)

    def mouseReleaseEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mouseReleaseEvent(event):
            return
        return QGraphicsScene.mouseReleaseEvent(self, event)

    def mouseDoubleClickEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mouseDoubleClickEvent(event):
            return

        return QGraphicsScene.mouseDoubleClickEvent(self, event)

    def keyPressEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.keyPressEvent(event):
            return
        return QGraphicsScene.keyPressEvent(self, event)

    def keyReleaseEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.keyReleaseEvent(event):
            return
        return QGraphicsScene.keyReleaseEvent(self, event)

    def contextMenuEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.contextMenuEvent(event):
            return
        super().contextMenuEvent(event)

    def set_user_interaction_handler(self, handler):
        if self.user_interaction_handler and \
                not self.user_interaction_handler.isFinished():
            self.user_interaction_handler.cancel()

        log.info("Setting interaction '%s' to '%s'" % (handler, self))

        self.user_interaction_handler = handler
        if handler:
            handler.start()

    def __str__(self):
        return "%s(objectName=%r, ...)" % \
                (type(self).__name__, str(self.objectName()))
示例#13
0
class ToolGrid(QFrame):
    """
    A widget containing a grid of actions/buttons.

    Actions can be added using standard :func:`QWidget.addAction(QAction)`
    and :func:`QWidget.insertAction(int, QAction)` methods.

    Parameters
    ----------
    parent : :class:`QWidget`
        Parent widget.
    columns : int
        Number of columns in the grid layout.
    buttonSize : :class:`QSize`, optional
        Size of tool buttons in the grid.
    iconSize : :class:`QSize`, optional
        Size of icons in the buttons.
    toolButtonStyle : :class:`Qt.ToolButtonStyle`
        Tool button style.

    """

    actionTriggered = Signal(QAction)
    actionHovered = Signal(QAction)

    def __init__(self, parent=None, columns=4, buttonSize=None,
                 iconSize=None, toolButtonStyle=Qt.ToolButtonTextUnderIcon):
        QFrame.__init__(self, parent)

        if buttonSize is not None:
            buttonSize = QSize(buttonSize)

        if iconSize is not None:
            iconSize = QSize(iconSize)

        self.__columns = columns
        self.__buttonSize = buttonSize or QSize(50, 50)
        self.__iconSize = iconSize or QSize(26, 26)
        self.__toolButtonStyle = toolButtonStyle

        self.__gridSlots = []
        self.__mapper = QSignalMapper()
        self.__mapper.mapped[QObject].connect(self.__onClicked)

        self.__setupUi()

    def __setupUi(self):
        layout = QGridLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.setSizeConstraint(QGridLayout.SetFixedSize)
        self.setLayout(layout)
        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)

    def setButtonSize(self, size):
        """
        Set the button size.
        """
        if self.__buttonSize != size:
            self.__buttonSize = size
            for slot in self.__gridSlots:
                slot.button.setFixedSize(size)

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

    def setIconSize(self, size):
        """
        Set the button icon size.
        """
        if self.__iconSize != size:
            self.__iconSize = size
            for slot in self.__gridSlots:
                slot.button.setIconSize(size)

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

    def setToolButtonStyle(self, style):
        """
        Set the tool button style.
        """
        if self.__toolButtonStyle != style:
            self.__toolButtonStyle = style
            for slot in self.__gridSlots:
                slot.button.setToolButtonStyle(style)

    def toolButtonStyle(self):
        """
        Return the tool button style.
        """
        return self.__toolButtonStyle

    def setColumnCount(self, columns):
        """
        Set the number of button/action columns.
        """
        if self.__columns != columns:
            self.__columns = columns
            self.__relayout()

    def columns(self):
        """
        Return the number of columns in the grid.
        """
        return self.__columns

    def clear(self):
        """
        Clear all actions/buttons.
        """
        for slot in reversed(list(self.__gridSlots)):
            self.removeAction(slot.action)
        self.__gridSlots = []

    def insertAction(self, before, action):
        """
        Insert a new action at the position currently occupied
        by `before` (can also be an index).

        Parameters
        ----------
        before : :class:`QAction` or int
            Position where the `action` should be inserted.
        action : :class:`QAction`
            Action to insert

        """
        if isinstance(before, int):
            actions = list(self.actions())
            if len(actions) == 0 or before >= len(actions):
                # Insert as the first action or the last action.
                return self.addAction(action)

            before = actions[before]

        return QFrame.insertAction(self, before, action)

    def setActions(self, actions):
        """
        Clear the grid and add `actions`.
        """
        self.clear()

        for action in actions:
            self.addAction(action)

    def buttonForAction(self, action):
        """
        Return the :class:`QToolButton` instance button for `action`.
        """
        actions = [slot.action for slot in self.__gridSlots]
        index = actions.index(action)
        return self.__gridSlots[index].button

    def createButtonForAction(self, action):
        """
        Create and return a :class:`QToolButton` for action.
        """
        button = _ToolGridButton(self)
        button.setDefaultAction(action)

        if self.__buttonSize.isValid():
            button.setFixedSize(self.__buttonSize)
        if self.__iconSize.isValid():
            button.setIconSize(self.__iconSize)

        button.setToolButtonStyle(self.__toolButtonStyle)
        button.setProperty("tool-grid-button", qtcompat.qwrap(True))
        return button

    def count(self):
        """
        Return the number of buttons/actions in the grid.
        """
        return len(self.__gridSlots)

    def actionEvent(self, event):
        QFrame.actionEvent(self, event)

        if event.type() == QEvent.ActionAdded:
            # Note: the action is already in the self.actions() list.
            actions = list(self.actions())
            index = actions.index(event.action())
            self.__insertActionButton(index, event.action())

        elif event.type() == QEvent.ActionRemoved:
            self.__removeActionButton(event.action())

    def __insertActionButton(self, index, action):
        """Create a button for the action and add it to the layout
        at index.

        """
        self.__shiftGrid(index, 1)
        button = self.createButtonForAction(action)

        row = index // self.__columns
        column = index % self.__columns

        self.layout().addWidget(
            button, row, column,
            Qt.AlignLeft | Qt.AlignTop
        )

        self.__gridSlots.insert(
            index, _ToolGridSlot(button, action, row, column)
        )

        self.__mapper.setMapping(button, action)
        button.clicked.connect(self.__mapper.map)
        button.installEventFilter(self)

    def __removeActionButton(self, action):
        """Remove the button for the action from the layout and delete it.
        """
        actions = [slot.action for slot in self.__gridSlots]
        index = actions.index(action)
        slot = self.__gridSlots.pop(index)

        slot.button.removeEventFilter(self)
        self.__mapper.removeMappings(slot.button)

        self.layout().removeWidget(slot.button)
        self.__shiftGrid(index + 1, -1)

        slot.button.deleteLater()

    def __shiftGrid(self, start, count=1):
        """Shift all buttons starting at index `start` by `count` cells.
        """
        button_count = self.layout().count()
        direction = 1 if count >= 0 else -1
        if direction == 1:
            start, end = button_count - 1, start - 1
        else:
            start, end = start, button_count

        for index in range(start, end, -direction):
            item = self.layout().itemAtPosition(index / self.__columns,
                                                index % self.__columns)
            if item:
                button = item.widget()
                new_index = index + count
                self.layout().addWidget(button, new_index / self.__columns,
                                        new_index % self.__columns,
                                        Qt.AlignLeft | Qt.AlignTop)

    def __relayout(self):
        """Relayout the buttons.
        """
        for i in reversed(range(self.layout().count())):
            self.layout().takeAt(i)

        self.__gridSlots = [_ToolGridSlot(slot.button, slot.action,
                                          i / self.__columns,
                                          i % self.__columns)
                            for i, slot in enumerate(self.__gridSlots)]

        for slot in self.__gridSlots:
            self.layout().addWidget(slot.button, slot.row, slot.column,
                                    Qt.AlignLeft | Qt.AlignTop)

    def __indexOf(self, button):
        """Return the index of button widget.
        """
        buttons = [slot.button for slot in self.__gridSlots]
        return buttons.index(button)

    def __onButtonEnter(self, button):
        action = button.defaultAction()
        self.actionHovered.emit(action)

    def __onClicked(self, action):
        self.actionTriggered.emit(action)

    def paintEvent(self, event):
        return utils.StyledWidget_paintEvent(self, event)

    def eventFilter(self, obj, event):
        etype = event.type()
        if etype == QEvent.KeyPress and obj.hasFocus():
            key = event.key()
            if key in [Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]:
                if self.__focusMove(obj, key):
                    event.accept()
                    return True
        elif etype == QEvent.HoverEnter and obj.parent() is self:
            self.__onButtonEnter(obj)
        return QFrame.eventFilter(self, obj, event)

    def __focusMove(self, focus, key):
        assert(focus is self.focusWidget())
        try:
            index = self.__indexOf(focus)
        except IndexError:
            return False

        if key == Qt.Key_Down:
            index += self.__columns
        elif key == Qt.Key_Up:
            index -= self.__columns
        elif key == Qt.Key_Left:
            index -= 1
        elif key == Qt.Key_Right:
            index += 1

        if index >= 0 and index < self.count():
            button = self.__gridSlots[index].button
            button.setFocus(Qt.TabFocusReason)
            return True
        else:
            return False
示例#14
0
class ToolGrid(QFrame):
    """
    A widget containing a grid of actions/buttons.

    Actions can be added using standard :func:`QWidget.addAction(QAction)`
    and :func:`QWidget.insertAction(int, QAction)` methods.

    Parameters
    ----------
    parent : :class:`QWidget`
        Parent widget.
    columns : int
        Number of columns in the grid layout.
    buttonSize : :class:`QSize`, optional
        Size of tool buttons in the grid.
    iconSize : :class:`QSize`, optional
        Size of icons in the buttons.
    toolButtonStyle : :class:`Qt.ToolButtonStyle`
        Tool button style.

    """

    actionTriggered = Signal(QAction)
    actionHovered = Signal(QAction)

    def __init__(self, parent=None, columns=4, buttonSize=None,
                 iconSize=None, toolButtonStyle=Qt.ToolButtonTextUnderIcon):
        super().__init__(parent)

        if buttonSize is not None:
            buttonSize = QSize(buttonSize)

        if iconSize is not None:
            iconSize = QSize(iconSize)

        self.__columns = columns
        self.__buttonSize = buttonSize or QSize(50, 50)
        self.__iconSize = iconSize or QSize(26, 26)
        self.__toolButtonStyle = toolButtonStyle

        self.__gridSlots = []
        self.__mapper = QSignalMapper()
        self.__mapper.mapped[QObject].connect(self.__onClicked)

        self.__setupUi()

    def __setupUi(self):
        layout = QGridLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.setSizeConstraint(QGridLayout.SetFixedSize)
        self.setLayout(layout)
        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)

    def setButtonSize(self, size):
        """
        Set the button size.
        """
        if self.__buttonSize != size:
            self.__buttonSize = size
            for slot in self.__gridSlots:
                slot.button.setFixedSize(size)

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

    def setIconSize(self, size):
        """
        Set the button icon size.
        """
        if self.__iconSize != size:
            self.__iconSize = size
            for slot in self.__gridSlots:
                slot.button.setIconSize(size)

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

    def setToolButtonStyle(self, style):
        """
        Set the tool button style.
        """
        if self.__toolButtonStyle != style:
            self.__toolButtonStyle = style
            for slot in self.__gridSlots:
                slot.button.setToolButtonStyle(style)

    def toolButtonStyle(self):
        """
        Return the tool button style.
        """
        return self.__toolButtonStyle

    def setColumnCount(self, columns):
        """
        Set the number of button/action columns.
        """
        if self.__columns != columns:
            self.__columns = columns
            self.__relayout()

    def columns(self):
        """
        Return the number of columns in the grid.
        """
        return self.__columns

    def clear(self):
        """
        Clear all actions/buttons.
        """
        for slot in reversed(list(self.__gridSlots)):
            self.removeAction(slot.action)
        self.__gridSlots = []

    def insertAction(self, before, action):
        """
        Insert a new action at the position currently occupied
        by `before` (can also be an index).

        Parameters
        ----------
        before : :class:`QAction` or int
            Position where the `action` should be inserted.
        action : :class:`QAction`
            Action to insert

        """
        if isinstance(before, int):
            actions = list(self.actions())
            if len(actions) == 0 or before >= len(actions):
                # Insert as the first action or the last action.
                return self.addAction(action)

            before = actions[before]

        return super().insertAction(before, action)

    def setActions(self, actions):
        """
        Clear the grid and add `actions`.
        """
        self.clear()

        for action in actions:
            self.addAction(action)

    def buttonForAction(self, action):
        """
        Return the :class:`QToolButton` instance button for `action`.
        """
        actions = [slot.action for slot in self.__gridSlots]
        index = actions.index(action)
        return self.__gridSlots[index].button

    def createButtonForAction(self, action):
        """
        Create and return a :class:`QToolButton` for action.
        """
        button = _ToolGridButton(self)
        button.setDefaultAction(action)

        if self.__buttonSize.isValid():
            button.setFixedSize(self.__buttonSize)
        if self.__iconSize.isValid():
            button.setIconSize(self.__iconSize)

        button.setToolButtonStyle(self.__toolButtonStyle)
        button.setProperty("tool-grid-button", True)
        return button

    def count(self):
        """
        Return the number of buttons/actions in the grid.
        """
        return len(self.__gridSlots)

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

        if event.type() == QEvent.ActionAdded:
            # Note: the action is already in the self.actions() list.
            actions = list(self.actions())
            index = actions.index(event.action())
            self.__insertActionButton(index, event.action())

        elif event.type() == QEvent.ActionRemoved:
            self.__removeActionButton(event.action())

    def __insertActionButton(self, index, action):
        """Create a button for the action and add it to the layout
        at index.

        """
        self.__shiftGrid(index, 1)
        button = self.createButtonForAction(action)

        row = index // self.__columns
        column = index % self.__columns

        self.layout().addWidget(
            button, row, column,
            Qt.AlignLeft | Qt.AlignTop
        )

        self.__gridSlots.insert(
            index, _ToolGridSlot(button, action, row, column)
        )

        self.__mapper.setMapping(button, action)
        button.clicked.connect(self.__mapper.map)
        button.installEventFilter(self)

    def __removeActionButton(self, action):
        """Remove the button for the action from the layout and delete it.
        """
        actions = [slot.action for slot in self.__gridSlots]
        index = actions.index(action)
        slot = self.__gridSlots.pop(index)

        slot.button.removeEventFilter(self)
        self.__mapper.removeMappings(slot.button)

        self.layout().removeWidget(slot.button)
        self.__shiftGrid(index + 1, -1)

        slot.button.deleteLater()

    def __shiftGrid(self, start, count=1):
        """Shift all buttons starting at index `start` by `count` cells.
        """
        button_count = self.layout().count()
        direction = 1 if count >= 0 else -1
        if direction == 1:
            start, end = button_count - 1, start - 1
        else:
            start, end = start, button_count

        for index in range(start, end, -direction):
            item = self.layout().itemAtPosition(index / self.__columns,
                                                index % self.__columns)
            if item:
                button = item.widget()
                new_index = index + count
                self.layout().addWidget(button, new_index / self.__columns,
                                        new_index % self.__columns,
                                        Qt.AlignLeft | Qt.AlignTop)

    def __relayout(self):
        """Relayout the buttons.
        """
        for i in reversed(range(self.layout().count())):
            self.layout().takeAt(i)

        self.__gridSlots = [_ToolGridSlot(slot.button, slot.action,
                                          i / self.__columns,
                                          i % self.__columns)
                            for i, slot in enumerate(self.__gridSlots)]

        for slot in self.__gridSlots:
            self.layout().addWidget(slot.button, slot.row, slot.column,
                                    Qt.AlignLeft | Qt.AlignTop)

    def __indexOf(self, button):
        """Return the index of button widget.
        """
        buttons = [slot.button for slot in self.__gridSlots]
        return buttons.index(button)

    def __onButtonEnter(self, button):
        action = button.defaultAction()
        self.actionHovered.emit(action)

    def __onClicked(self, action):
        self.actionTriggered.emit(action)

    def paintEvent(self, event):
        return utils.StyledWidget_paintEvent(self, event)

    def eventFilter(self, obj, event):
        etype = event.type()
        if etype == QEvent.KeyPress and obj.hasFocus():
            key = event.key()
            if key in [Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]:
                if self.__focusMove(obj, key):
                    event.accept()
                    return True
        elif etype == QEvent.HoverEnter and obj.parent() is self:
            self.__onButtonEnter(obj)
        return super().eventFilter(obj, event)

    def __focusMove(self, focus, key):
        assert(focus is self.focusWidget())
        try:
            index = self.__indexOf(focus)
        except IndexError:
            return False

        if key == Qt.Key_Down:
            index += self.__columns
        elif key == Qt.Key_Up:
            index -= self.__columns
        elif key == Qt.Key_Left:
            index -= 1
        elif key == Qt.Key_Right:
            index += 1

        if index >= 0 and index < self.count():
            button = self.__gridSlots[index].button
            button.setFocus(Qt.TabFocusReason)
            return True
        else:
            return False
示例#15
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):
        super().__init__(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()