You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

424 lines
12 KiB

# 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)