Files

217 lines
7.1 KiB
Python
Raw Permalink Normal View History

2025-08-14 18:45:16 +08:00
# coding:utf-8
from PySide6.QtCore import Qt, QSize, QRectF, QModelIndex, QEvent
from PySide6.QtGui import QPainter, QColor, QPalette
from PySide6.QtWidgets import QTreeWidget, QStyledItemDelegate, QStyle, QTreeView, QApplication, QStyleOptionViewItem
from ...common.style_sheet import FluentStyleSheet, themeColor, isDarkTheme, setCustomStyleSheet
from ...common.font import getFont
from ...common.color import autoFallbackThemeColor
from .check_box import CheckBoxIcon
from .scroll_area import SmoothScrollDelegate
class TreeItemDelegate(QStyledItemDelegate):
""" Tree item delegate """
def __init__(self, parent: QTreeView):
super().__init__(parent)
self.lightCheckedColor = QColor()
self.darkCheckedColor = QColor()
def setCheckedColor(self, light, dark):
""" set the color of indicator in checked status
Parameters
----------
light, dark: str | QColor | Qt.GlobalColor
color in light/dark theme mode
"""
self.lightCheckedColor = QColor(light)
self.darkCheckedColor = QColor(dark)
self.parent().viewport().update()
def paint(self, painter, option, index):
painter.setRenderHints(
QPainter.Antialiasing | QPainter.TextAntialiasing)
super().paint(painter, option, index)
if index.data(Qt.CheckStateRole) is not None:
self._drawCheckBox(painter, option, index)
if not (option.state & (QStyle.State_Selected | QStyle.State_MouseOver)):
return
painter.save()
painter.setPen(Qt.NoPen)
# draw background
h = option.rect.height() - 4
c = 255 if isDarkTheme() else 0
painter.setBrush(QColor(c, c, c, 9))
painter.drawRoundedRect(
4, option.rect.y() + 2, self.parent().width() - 8, h, 4, 4)
# draw indicator
if option.state & QStyle.State_Selected and self.parent().horizontalScrollBar().value() == 0:
painter.setBrush(autoFallbackThemeColor(self.lightCheckedColor, self.darkCheckedColor))
painter.drawRoundedRect(4, 9+option.rect.y(), 3, h - 13, 1.5, 1.5)
painter.restore()
def _drawCheckBox(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex):
painter.save()
checkState = Qt.CheckState(index.data(Qt.ItemDataRole.CheckStateRole))
isDark = isDarkTheme()
r = 4.5
x = option.rect.x() + 23
y = option.rect.center().y() - 9
rect = QRectF(x, y, 19, 19)
if checkState == Qt.CheckState.Unchecked:
painter.setBrush(QColor(0, 0, 0, 26)
if isDark else QColor(0, 0, 0, 6))
painter.setPen(QColor(255, 255, 255, 142)
if isDark else QColor(0, 0, 0, 122))
painter.drawRoundedRect(rect, r, r)
else:
color = autoFallbackThemeColor(self.lightCheckedColor, self.darkCheckedColor)
painter.setPen(color)
painter.setBrush(color)
painter.drawRoundedRect(rect, r, r)
if checkState == Qt.CheckState.Checked:
CheckBoxIcon.ACCEPT.render(painter, rect)
else:
CheckBoxIcon.PARTIAL_ACCEPT.render(painter, rect)
painter.restore()
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
# font
option.font = index.data(Qt.FontRole) or getFont(13)
# text color
textColor = Qt.white if isDarkTheme() else Qt.black
textBrush = index.data(Qt.ForegroundRole)
if textBrush is not None:
textColor = textBrush.color()
option.palette.setColor(QPalette.Text, textColor)
option.palette.setColor(QPalette.HighlightedText, textColor)
class TreeViewBase:
""" Tree view base class """
def _initView(self):
self.scrollDelagate = SmoothScrollDelegate(self)
self.header().setHighlightSections(False)
self.header().setDefaultAlignment(Qt.AlignCenter)
self.setItemDelegate(TreeItemDelegate(self))
self.setIconSize(QSize(16, 16))
self.setMouseTracking(True)
FluentStyleSheet.TREE_VIEW.apply(self)
def setCheckedColor(self, light, dark):
""" set the color in checked status
Parameters
----------
light, dark: str | QColor | Qt.GlobalColor
color in light/dark theme mode
"""
self.itemDelegate().setCheckedColor(light, dark)
def drawBranches(self, painter, rect, index):
rect.moveLeft(15)
return QTreeView.drawBranches(self, painter, rect, index)
def setBorderVisible(self, isVisible: bool):
""" set the visibility of border """
self.setProperty("isBorderVisible", isVisible)
self.setStyle(QApplication.style())
def setBorderRadius(self, radius: int):
""" set the radius of border """
qss = f"QTreeView{{border-radius: {radius}px}}"
setCustomStyleSheet(self, qss, qss)
class TreeWidget(TreeViewBase, QTreeWidget):
""" Tree widget """
def __init__(self, parent=None):
super().__init__(parent=parent)
self._initView()
def viewportEvent(self, event):
"""
Catch the click event to override the item "expand/collapse" function which is
still called in the place it was before moving the branches in the drawBranches method.
"""
if event.type() != QEvent.Type.MouseButtonPress:
return super().viewportEvent(event)
index = self.indexAt(event.pos())
item = self.itemFromIndex(index)
if item is None:
return super().viewportEvent(event)
level = 0
while item.parent() is not None:
item = item.parent()
level += 1
indent = level * self.indentation() + 20
if event.pos().x() > indent and event.pos().x() < indent + 10:
if self.isExpanded(index):
self.collapse(index)
else:
self.expand(index)
return super().viewportEvent(event)
class TreeView(TreeViewBase, QTreeView):
""" Tree view """
def __init__(self, parent=None):
super().__init__(parent=parent)
self._initView()
def viewportEvent(self, event):
"""
Catch the click event to override the item "expand/collapse" function which is
still called in the place it was before moving the branches in the drawBranches method.
"""
if event.type() != QEvent.Type.MouseButtonPress:
return super().viewportEvent(event)
index = self.indexAt(event.pos())
if not index.isValid():
return super().viewportEvent(event)
level = 0
currentIndex = index
while currentIndex.parent().isValid():
currentIndex = currentIndex.parent()
level += 1
indent = level * self.indentation() + 20
if event.pos().x() > indent and event.pos().x() < indent + 10:
if self.isExpanded(index):
self.collapse(index)
else:
self.expand(index)
return super().viewportEvent(event)