from logging import getLogger from typing import Union, Protocol from PyQt5.QtCore import ( Qt, QRect, QSize, QPoint, pyqtSignal, ) from PyQt5.QtGui import QFontMetrics from PyQt5.QtWidgets import ( QStyle, QLabel, QWidget, QStyleOptionTab, QStylePainter, QTabBar, QTabWidget, QVBoxLayout, QScrollArea, QLayout, QSizePolicy, ) from rare.utils.misc import qta_icon logger = getLogger("SideTab") class SideTabBar(QTabBar): def __init__(self, padding: int = -1, parent=None): super(SideTabBar, self).__init__(parent=parent) self.setObjectName(type(self).__name__) self.padding = padding self.fm = QFontMetrics(self.font()) def tabSizeHint(self, index): width = QTabBar.tabSizeHint(self, index).height() if self.padding < 0: width += QTabBar.tabSizeHint(self, index).width() else: width += self.padding return QSize(width, self.fm.height() + 18) def paintEvent(self, event): painter = QStylePainter(self) opt = QStyleOptionTab() for i in range(self.count()): self.initStyleOption(opt, i) painter.drawControl(QStyle.CE_TabBarTabShape, opt) painter.save() s = opt.rect.size() s.transpose() r = QRect(QPoint(), s) r.moveCenter(opt.rect.center()) opt.rect = r c = self.tabRect(i).center() painter.translate(c) painter.rotate(90) painter.translate(-c) painter.drawControl(QStyle.CE_TabBarTabLabel, opt) painter.restore() class SideTabContents(object): # str: title set_title = pyqtSignal(str) implements_scrollarea: bool = False class SideTabContentsProtocol(Protocol): implements_scrollarea: bool def layout(self) -> QLayout: pass def set_title(self) -> pyqtSignal: pass def sizeHint(self) -> QSize: pass class SideTabContainer(QWidget): def __init__(self, widget: Union[QWidget, SideTabContentsProtocol], title: str = "", parent: QWidget = None): super(SideTabContainer, self).__init__(parent=parent) self.title = QLabel(self) self.setTitle(title) if widget.layout(): widget.layout().setAlignment(Qt.AlignTop) widget.layout().setContentsMargins(0, 0, 3, 0) if hasattr(widget, "set_title"): widget.set_title.connect(self.setTitle) layout = QVBoxLayout(self) layout.addWidget(self.title) if not hasattr(widget, "implements_scrollarea") or not widget.implements_scrollarea: self.scrollarea = QScrollArea(self) self.scrollarea.setSizeAdjustPolicy(QScrollArea.AdjustToContents) self.scrollarea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.scrollarea.setFrameStyle(QScrollArea.NoFrame) self.scrollarea.setMinimumWidth( widget.sizeHint().width() + self.scrollarea.verticalScrollBar().sizeHint().width() ) self.scrollarea.setWidgetResizable(True) widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.scrollarea.setWidget(widget) layout.addWidget(self.scrollarea) else: layout.addWidget(widget) layout.setAlignment(Qt.AlignTop) def setTitle(self, text: str) -> None: self.title.setText(f"

{text}

") self.title.setVisible(bool(text)) class SideTabWidget(QTabWidget): back_clicked = pyqtSignal() def __init__(self, show_back: bool = False, padding: int = -1, parent=None): super(SideTabWidget, self).__init__(parent=parent) self.setTabBar(SideTabBar(padding=padding, parent=self)) self.setDocumentMode(True) self.setTabPosition(QTabWidget.West) if show_back: super(SideTabWidget, self).addTab( QWidget(self), qta_icon("mdi.keyboard-backspace", "ei.backward"), self.tr("Back") ) self.tabBarClicked.connect(self.back_func) def back_func(self, tab): # shortcut for tab == 0 if not tab: self.back_clicked.emit() def addTab(self, widget: Union[QWidget, SideTabContentsProtocol], a1: str, title: str = "") -> int: container = SideTabContainer(widget, title, parent=self) return super(SideTabWidget, self).addTab(container, a1)