# coding:utf-8 from typing import Union, List from PySide6.QtCore import (Qt, Signal, QRect, QRectF, QPropertyAnimation, Property, QMargins, QEasingCurve, QPoint, QEvent) from PySide6.QtGui import QColor, QPainter, QPen, QIcon, QCursor from PySide6.QtWidgets import QWidget, QVBoxLayout from ...common.config import isDarkTheme from ...common.style_sheet import themeColor from ...common.icon import drawIcon, toQIcon from ...common.icon import FluentIcon as FIF class NavigationWidget(QWidget): """ Navigation widget """ clicked = Signal(bool) # whether triggered by the user EXPAND_WIDTH = 312 def __init__(self, isSelectable: bool, parent=None): super().__init__(parent) self.isCompacted = True self.isSelected = False self.isPressed = False self.isEnter = False self.isSelectable = isSelectable self.treeParent = None self.nodeDepth = 0 self.setFixedSize(40, 36) def enterEvent(self, e): self.isEnter = True self.update() def leaveEvent(self, e): self.isEnter = False self.isPressed = False self.update() def mousePressEvent(self, e): self.isPressed = True self.update() def mouseReleaseEvent(self, e): self.isPressed = False self.update() self.clicked.emit(True) def setCompacted(self, isCompacted: bool): """ set whether the widget is compacted """ if isCompacted == self.isCompacted: return self.isCompacted = isCompacted if isCompacted: self.setFixedSize(40, 36) else: self.setFixedSize(self.EXPAND_WIDTH, 36) self.update() def setSelected(self, isSelected: bool): """ set whether the button is selected Parameters ---------- isSelected: bool whether the button is selected """ if not self.isSelectable: return self.isSelected = isSelected self.update() class NavigationPushButton(NavigationWidget): """ Navigation push button """ def __init__(self, icon: Union[str, QIcon, FIF], text: str, isSelectable: bool, parent=None): """ Parameters ---------- icon: str | QIcon | FluentIconBase the icon to be drawn text: str the text of button """ super().__init__(isSelectable=isSelectable, parent=parent) self._icon = icon self._text = text self.setStyleSheet( "NavigationPushButton{font: 14px 'Segoe UI', 'Microsoft YaHei'}") def text(self): return self._text def setText(self, text: str): self._text = text self.update() def icon(self): return toQIcon(self._icon) def setIcon(self, icon: Union[str, QIcon, FIF]): self._icon = icon self.update() def _margins(self): return QMargins(0, 0, 0, 0) def _canDrawIndicator(self): return self.isSelected def paintEvent(self, e): painter = QPainter(self) painter.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) painter.setPen(Qt.NoPen) if self.isPressed: painter.setOpacity(0.7) if not self.isEnabled(): painter.setOpacity(0.4) # draw background c = 255 if isDarkTheme() else 0 m = self._margins() pl, pr = m.left(), m.right() globalRect = QRect(self.mapToGlobal(QPoint()), self.size()) if self._canDrawIndicator(): painter.setBrush(QColor(c, c, c, 6 if self.isEnter else 10)) painter.drawRoundedRect(self.rect(), 5, 5) # draw indicator painter.setBrush(themeColor()) painter.drawRoundedRect(pl, 10, 3, 16, 1.5, 1.5) elif self.isEnter and self.isEnabled() and globalRect.contains(QCursor.pos()): painter.setBrush(QColor(c, c, c, 10)) painter.drawRoundedRect(self.rect(), 5, 5) drawIcon(self._icon, painter, QRectF(11.5+pl, 10, 16, 16)) # draw text if self.isCompacted: return painter.setFont(self.font()) painter.setPen(QColor(c, c, c)) painter.drawText(QRect(44+pl, 0, self.width()-57-pl-pr, self.height()), Qt.AlignVCenter, self.text()) class NavigationToolButton(NavigationPushButton): """ Navigation tool button """ def __init__(self, icon: Union[str, QIcon, FIF], parent=None): super().__init__(icon, '', False, parent) def setCompacted(self, isCompacted: bool): self.setFixedSize(40, 36) class NavigationSeparator(NavigationWidget): """ Navigation Separator """ def __init__(self, parent=None): super().__init__(False, parent=parent) self.setCompacted(True) def setCompacted(self, isCompacted: bool): if isCompacted: self.setFixedSize(48, 3) else: self.setFixedSize(self.EXPAND_WIDTH + 10, 3) self.update() def paintEvent(self, e): painter = QPainter(self) c = 255 if isDarkTheme() else 0 pen = QPen(QColor(c, c, c, 15)) pen.setCosmetic(True) painter.setPen(pen) painter.drawLine(0, 1, self.width(), 1) class NavigationTreeItem(NavigationPushButton): """ Navigation tree item widget """ itemClicked = Signal(bool, bool) # triggerByUser, clickArrow def __init__(self, icon: Union[str, QIcon, FIF], text: str, isSelectable: bool, parent=None): super().__init__(icon, text, isSelectable, parent) self._arrowAngle = 0 self.rotateAni = QPropertyAnimation(self, b'arrowAngle', self) def setExpanded(self, isExpanded: bool): self.rotateAni.stop() self.rotateAni.setEndValue(180 if isExpanded else 0) self.rotateAni.setDuration(150) self.rotateAni.start() def mouseReleaseEvent(self, e): super().mouseReleaseEvent(e) clickArrow = QRectF(self.width()-30, 8, 20, 20).contains(e.pos()) self.itemClicked.emit(True, clickArrow) self.update() def _canDrawIndicator(self): p = self.parent() # type: NavigationTreeWidget if p.isLeaf() or p.isSelected: return p.isSelected for child in p.treeChildren: if child.itemWidget._canDrawIndicator() and not child.isVisible(): return True return False def _margins(self): p = self.parent() # type: NavigationTreeWidget return QMargins(p.nodeDepth*28, 0, 20*bool(p.treeChildren), 0) def paintEvent(self, e): super().paintEvent(e) if self.isCompacted or not self.parent().treeChildren: return painter = QPainter(self) painter.setRenderHints(QPainter.Antialiasing) painter.setPen(Qt.NoPen) if self.isPressed: painter.setOpacity(0.7) if not self.isEnabled(): painter.setOpacity(0.4) painter.translate(self.width() - 20, 18) painter.rotate(self.arrowAngle) FIF.ARROW_DOWN.render(painter, QRectF(-5, -5, 9.6, 9.6)) def getArrowAngle(self): return self._arrowAngle def setArrowAngle(self, angle): self._arrowAngle = angle self.update() arrowAngle = Property(float, getArrowAngle, setArrowAngle) class NavigationTreeWidgetBase(NavigationWidget): """ Navigation tree widget base class """ def addChild(self, child): """ add child Parameters ---------- child: NavigationTreeWidgetBase child item """ raise NotImplementedError def insertChild(self, index: int, child: NavigationWidget): """ insert child Parameters ---------- child: NavigationTreeWidgetBase child item """ raise NotImplementedError def removeChild(self, child: NavigationWidget): """ remove child Parameters ---------- child: NavigationTreeWidgetBase child item """ raise NotImplementedError def isRoot(self): """ is root node """ return True def isLeaf(self): """ is leaf node """ return True def setExpanded(self, isExpanded: bool): """ set the expanded status Parameters ---------- isExpanded: bool whether to expand node """ raise NotImplementedError def childItems(self) -> list: """ return child items """ raise NotImplementedError class NavigationTreeWidget(NavigationTreeWidgetBase): """ Navigation tree widget """ def __init__(self, icon: Union[str, QIcon, FIF], text: str, isSelectable: bool, parent=None): super().__init__(isSelectable, parent) self.treeChildren = [] # type: List[NavigationTreeWidget] self.isExpanded = False self.itemWidget = NavigationTreeItem(icon, text, isSelectable, self) self.vBoxLayout = QVBoxLayout(self) self.expandAni = QPropertyAnimation(self, b'geometry', self) self.__initWidget() def __initWidget(self): self.vBoxLayout.setSpacing(4) self.vBoxLayout.setContentsMargins(0, 0, 0, 0) self.vBoxLayout.addWidget(self.itemWidget, 0, Qt.AlignTop) self.itemWidget.itemClicked.connect(self._onClicked) self.setAttribute(Qt.WA_TranslucentBackground) self.expandAni.valueChanged.connect(lambda g: self.setFixedSize(g.size())) def addChild(self, child): self.insertChild(-1, child) def text(self): return self.itemWidget.text() def icon(self): return self.itemWidget.icon() def setText(self, text): self.itemWidget.setText(text) def setIcon(self, icon: Union[str, QIcon, FIF]): self.itemWidget.setIcon(icon) def insertChild(self, index, child): if child in self.treeChildren: return child.treeParent = self child.nodeDepth = self.nodeDepth + 1 child.setVisible(self.isExpanded) child.expandAni.valueChanged.connect(lambda: self.setFixedSize(self.sizeHint())) if index < 0: index = len(self.treeChildren) index += 1 # item widget should always be the first self.treeChildren.insert(index, child) self.vBoxLayout.insertWidget(index, child, 0, Qt.AlignTop) def removeChild(self, child): self.treeChildren.remove(child) self.vBoxLayout.removeWidget(child) def childItems(self) -> list: return self.treeChildren def setExpanded(self, isExpanded: bool, ani=False): """ set the expanded status """ if isExpanded == self.isExpanded: return self.isExpanded = isExpanded self.itemWidget.setExpanded(isExpanded) for child in self.treeChildren: child.setVisible(isExpanded) child.setFixedSize(child.sizeHint()) if ani: self.expandAni.stop() self.expandAni.setStartValue(self.geometry()) self.expandAni.setEndValue(QRect(self.pos(), self.sizeHint())) self.expandAni.setDuration(120) self.expandAni.setEasingCurve(QEasingCurve.OutQuad) self.expandAni.start() else: self.setFixedSize(self.sizeHint()) def isRoot(self): return self.treeParent is None def isLeaf(self): return len(self.treeChildren) == 0 def setSelected(self, isSelected: bool): super().setSelected(isSelected) self.itemWidget.setSelected(isSelected) def mouseReleaseEvent(self, e): pass def setCompacted(self, isCompacted: bool): super().setCompacted(isCompacted) self.itemWidget.setCompacted(isCompacted) def _onClicked(self, triggerByUser, clickArrow): if not self.isCompacted: if self.isSelectable and not self.isSelected and not clickArrow: self.setExpanded(True, ani=True) else: self.setExpanded(not self.isExpanded, ani=True) if not clickArrow or self.isCompacted: self.clicked.emit(triggerByUser)