def get_icon_html(icon: QIcon, size: QSize) -> str: """ Transform an icon to html <img> tag. """ if not size.isValid(): return "" if size.width() < 0 or size.height() < 0: size = QSize(16, 16) # just in case byte_array = QByteArray() buffer = QBuffer(byte_array) buffer.open(QIODevice.WriteOnly) pixmap = icon.pixmap(size) if pixmap.isNull(): return "" pixmap.save(buffer, "PNG") buffer.close() dpr = pixmap.devicePixelRatioF() if dpr != 1.0: size_ = pixmap.size() / dpr size_part = ' width="{}" height="{}"'.format( int(math.floor(size_.width())), int(math.floor(size_.height())) ) else: size_part = '' img_encoded = byte_array.toBase64().data().decode("utf-8") return '<img src="data:image/png;base64,{}"{}/>'.format(img_encoded, size_part)
class IconWidget(QWidget): """ A widget displaying an `QIcon` """ def __init__(self, parent=None, icon=QIcon(), iconSize=QSize(), **kwargs): sizePolicy = kwargs.pop( "sizePolicy", QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) super().__init__(parent, **kwargs) self.__icon = QIcon(icon) self.__iconSize = QSize(iconSize) self.setSizePolicy(sizePolicy) def setIcon(self, icon): # type: (QIcon) -> None if self.__icon != icon: self.__icon = QIcon(icon) self.updateGeometry() self.update() def icon(self): # type: () -> QIcon return QIcon(self.__icon) def iconSize(self): # type: () -> QSize if not self.__iconSize.isValid(): size = self.style().pixelMetric(QStyle.PM_ButtonIconSize) return QSize(size, size) else: return QSize(self.__iconSize) def setIconSize(self, iconSize): # type: (QSize) -> None if self.__iconSize != iconSize: self.__iconSize = QSize(iconSize) self.updateGeometry() self.update() def sizeHint(self): sh = self.iconSize() m = self.contentsMargins() return QSize(sh.width() + m.left() + m.right(), sh.height() + m.top() + m.bottom()) def paintEvent(self, event): painter = QStylePainter(self) opt = QStyleOption() opt.initFrom(self) painter.drawPrimitive(QStyle.PE_Widget, opt) if not self.__icon.isNull(): rect = self.contentsRect() if opt.state & QStyle.State_Active: mode = QIcon.Active else: mode = QIcon.Disabled self.__icon.paint(painter, rect, Qt.AlignCenter, mode, QIcon.Off) painter.end()
class IconWidget(QWidget): """ A widget displaying an `QIcon` """ def __init__(self, parent=None, icon=QIcon(), iconSize=QSize(), **kwargs): sizePolicy = kwargs.pop("sizePolicy", QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) super().__init__(parent, **kwargs) self.__icon = QIcon(icon) self.__iconSize = QSize(iconSize) self.setSizePolicy(sizePolicy) def setIcon(self, icon): # type: (QIcon) -> None if self.__icon != icon: self.__icon = QIcon(icon) self.updateGeometry() self.update() def icon(self): # type: () -> QIcon return QIcon(self.__icon) def iconSize(self): # type: () -> QSize if not self.__iconSize.isValid(): size = self.style().pixelMetric(QStyle.PM_ButtonIconSize) return QSize(size, size) else: return QSize(self.__iconSize) def setIconSize(self, iconSize): # type: (QSize) -> None if self.__iconSize != iconSize: self.__iconSize = QSize(iconSize) self.updateGeometry() self.update() def sizeHint(self): sh = self.iconSize() m = self.contentsMargins() return QSize(sh.width() + m.left() + m.right(), sh.height() + m.top() + m.bottom()) def paintEvent(self, event): painter = QStylePainter(self) opt = QStyleOption() opt.initFrom(self) painter.drawPrimitive(QStyle.PE_Widget, opt) if not self.__icon.isNull(): rect = self.contentsRect() if opt.state & QStyle.State_Active: mode = QIcon.Active else: mode = QIcon.Disabled self.__icon.paint(painter, rect, Qt.AlignCenter, mode, QIcon.Off) painter.end()
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()
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
class StartItem(QWidget): """ An active item in the bottom row of the welcome screen. """ def __init__(self, *args, text="", icon=QIcon(), iconSize=QSize(), iconActive=QIcon(), **kwargs): self.__iconSize = QSize() self.__icon = QIcon() self.__icon_active = QIcon() self.__text = "" self.__active = False super().__init__(*args, **kwargs) self.setAutoFillBackground(True) font = self.font() font.setPointSize(18) self.setFont(font) self.setAttribute(Qt.WA_SetFont, False) self.setText(text) self.setIcon(icon) self.setIconSize(iconSize) self.setIconActive(iconActive) self.installEventFilter(self) def iconSize(self): if not self.__iconSize.isValid(): size = self.style().pixelMetric(QStyle.PM_LargeIconSize, None, self) * 2 return QSize(size, size) else: return QSize(self.__iconSize) def setIconSize(self, size): if size != self.__iconSize: self.__iconSize = QSize(size) self.updateGeometry() iconSize_ = Property(QSize, iconSize, setIconSize, designable=True) def icon(self): if self.__active: return QIcon(self.__icon_active) else: return QIcon(self.__icon) def setIcon(self, icon): self.__icon = QIcon(icon) self.update() icon_ = Property(QIcon, icon, setIcon, designable=True) def iconActive(self): return QIcon(self.__icon_active) def setIconActive(self, icon): self.__icon_active = QIcon(icon) self.update() icon_active_ = Property(QIcon, iconActive, setIconActive, designable=True) def sizeHint(self): return QSize(200, 150) def setText(self, text): if self.__text != text: self.__text = text self.updateGeometry() self.update() def text(self): return self.__text text_ = Property(str, text, setText, designable=True) def initStyleOption(self, option): # type: (QStyleOptionViewItem) -> None option.initFrom(self) option.backgroundBrush = option.palette.brush( self.backgroundRole()) option.font = self.font() option.text = self.text() option.icon = self.icon() option.decorationPosition = QStyleOptionViewItem.Top option.decorationAlignment = Qt.AlignCenter option.decorationSize = self.iconSize() option.displayAlignment = Qt.AlignCenter option.features = (QStyleOptionViewItem.WrapText | QStyleOptionViewItem.HasDecoration | QStyleOptionViewItem.HasDisplay) option.showDecorationSelected = True option.widget = self def paintEvent(self, event): style = self.style() # type: QStyle painter = QPainter(self) option = QStyleOption() option.initFrom(self) style.drawPrimitive(QStyle.PE_Widget, option, painter, self) option = QStyleOptionViewItem() self.initStyleOption(option) style.drawControl(QStyle.CE_ItemViewItem, option, painter, self) def eventFilter(self, obj, event): try: if event.type() == QEvent.Enter: self.__active = True self.setCursor(Qt.PointingHandCursor) self.update() return True elif event.type() == QEvent.Leave: self.__active = False self.unsetCursor() self.update() return True except Exception as ex: pass return False
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()