391 lines
12 KiB
Python
391 lines
12 KiB
Python
|
|
# coding:utf-8
|
||
|
|
from typing import List, Union
|
||
|
|
from PySide6.QtCore import QEvent, Qt, QPropertyAnimation, Property, QEasingCurve, QRectF
|
||
|
|
from PySide6.QtGui import QColor, QPainter, QIcon, QPainterPath
|
||
|
|
from PySide6.QtWidgets import QFrame, QWidget, QAbstractButton, QApplication, QScrollArea, QVBoxLayout
|
||
|
|
|
||
|
|
from ...common.config import isDarkTheme
|
||
|
|
from ...common.icon import FluentIcon as FIF
|
||
|
|
from ...common.style_sheet import FluentStyleSheet
|
||
|
|
from .setting_card import SettingCard
|
||
|
|
from ..layout.v_box_layout import VBoxLayout
|
||
|
|
|
||
|
|
|
||
|
|
class ExpandButton(QAbstractButton):
|
||
|
|
""" Expand button """
|
||
|
|
|
||
|
|
def __init__(self, parent=None):
|
||
|
|
super().__init__(parent)
|
||
|
|
self.setFixedSize(30, 30)
|
||
|
|
self.__angle = 0
|
||
|
|
self.isHover = False
|
||
|
|
self.isPressed = False
|
||
|
|
self.rotateAni = QPropertyAnimation(self, b'angle', self)
|
||
|
|
self.clicked.connect(self.__onClicked)
|
||
|
|
|
||
|
|
def paintEvent(self, e):
|
||
|
|
painter = QPainter(self)
|
||
|
|
painter.setRenderHints(QPainter.Antialiasing |
|
||
|
|
QPainter.SmoothPixmapTransform)
|
||
|
|
painter.setPen(Qt.NoPen)
|
||
|
|
|
||
|
|
# draw background
|
||
|
|
r = 255 if isDarkTheme() else 0
|
||
|
|
color = Qt.transparent
|
||
|
|
|
||
|
|
if self.isEnabled():
|
||
|
|
if self.isPressed:
|
||
|
|
color = QColor(r, r, r, 10)
|
||
|
|
elif self.isHover:
|
||
|
|
color = QColor(r, r, r, 14)
|
||
|
|
else:
|
||
|
|
painter.setOpacity(0.36)
|
||
|
|
|
||
|
|
painter.setBrush(color)
|
||
|
|
painter.drawRoundedRect(self.rect(), 4, 4)
|
||
|
|
|
||
|
|
# draw icon
|
||
|
|
painter.translate(self.width()//2, self.height()//2)
|
||
|
|
painter.rotate(self.__angle)
|
||
|
|
FIF.ARROW_DOWN.render(painter, QRectF(-5, -5, 9.6, 9.6))
|
||
|
|
|
||
|
|
def enterEvent(self, e):
|
||
|
|
self.setHover(True)
|
||
|
|
|
||
|
|
def leaveEvent(self, e):
|
||
|
|
self.setHover(False)
|
||
|
|
|
||
|
|
def mousePressEvent(self, e):
|
||
|
|
super().mousePressEvent(e)
|
||
|
|
self.setPressed(True)
|
||
|
|
|
||
|
|
def mouseReleaseEvent(self, e):
|
||
|
|
super().mouseReleaseEvent(e)
|
||
|
|
self.setPressed(False)
|
||
|
|
|
||
|
|
def setHover(self, isHover: bool):
|
||
|
|
self.isHover = isHover
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
def setPressed(self, isPressed: bool):
|
||
|
|
self.isPressed = isPressed
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
def __onClicked(self):
|
||
|
|
self.setExpand(self.angle < 180)
|
||
|
|
|
||
|
|
def setExpand(self, isExpand: bool):
|
||
|
|
self.rotateAni.stop()
|
||
|
|
self.rotateAni.setEndValue(180 if isExpand else 0)
|
||
|
|
self.rotateAni.setDuration(200)
|
||
|
|
self.rotateAni.start()
|
||
|
|
|
||
|
|
def getAngle(self):
|
||
|
|
return self.__angle
|
||
|
|
|
||
|
|
def setAngle(self, angle):
|
||
|
|
self.__angle = angle
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
angle = Property(float, getAngle, setAngle)
|
||
|
|
|
||
|
|
|
||
|
|
class SpaceWidget(QWidget):
|
||
|
|
""" Spacing widget """
|
||
|
|
|
||
|
|
def __init__(self, parent=None):
|
||
|
|
super().__init__(parent=parent)
|
||
|
|
self.setAttribute(Qt.WA_TranslucentBackground)
|
||
|
|
self.setFixedHeight(1)
|
||
|
|
|
||
|
|
|
||
|
|
class HeaderSettingCard(SettingCard):
|
||
|
|
""" Header setting card """
|
||
|
|
|
||
|
|
def __init__(self, icon, title, content=None, parent=None):
|
||
|
|
super().__init__(icon, title, content, parent)
|
||
|
|
self.expandButton = ExpandButton(self)
|
||
|
|
|
||
|
|
self.hBoxLayout.addWidget(self.expandButton, 0, Qt.AlignRight)
|
||
|
|
self.hBoxLayout.addSpacing(8)
|
||
|
|
|
||
|
|
self.titleLabel.setObjectName("titleLabel")
|
||
|
|
self.installEventFilter(self)
|
||
|
|
|
||
|
|
def eventFilter(self, obj, e):
|
||
|
|
if obj is self:
|
||
|
|
if e.type() == QEvent.Enter:
|
||
|
|
self.expandButton.setHover(True)
|
||
|
|
elif e.type() == QEvent.Leave:
|
||
|
|
self.expandButton.setHover(False)
|
||
|
|
elif e.type() == QEvent.MouseButtonPress and e.button() == Qt.LeftButton:
|
||
|
|
self.expandButton.setPressed(True)
|
||
|
|
elif e.type() == QEvent.MouseButtonRelease and e.button() == Qt.LeftButton:
|
||
|
|
self.expandButton.setPressed(False)
|
||
|
|
self.expandButton.click()
|
||
|
|
|
||
|
|
return super().eventFilter(obj, e)
|
||
|
|
|
||
|
|
def addWidget(self, widget: QWidget):
|
||
|
|
""" add widget to tail """
|
||
|
|
N = self.hBoxLayout.count()
|
||
|
|
self.hBoxLayout.removeItem(self.hBoxLayout.itemAt(N - 1))
|
||
|
|
self.hBoxLayout.addWidget(widget, 0, Qt.AlignRight)
|
||
|
|
self.hBoxLayout.addSpacing(19)
|
||
|
|
self.hBoxLayout.addWidget(self.expandButton, 0, Qt.AlignRight)
|
||
|
|
self.hBoxLayout.addSpacing(8)
|
||
|
|
|
||
|
|
def paintEvent(self, e):
|
||
|
|
painter = QPainter(self)
|
||
|
|
painter.setRenderHints(QPainter.Antialiasing)
|
||
|
|
painter.setPen(Qt.NoPen)
|
||
|
|
|
||
|
|
if isDarkTheme():
|
||
|
|
painter.setBrush(QColor(255, 255, 255, 13))
|
||
|
|
else:
|
||
|
|
painter.setBrush(QColor(255, 255, 255, 170))
|
||
|
|
|
||
|
|
p = self.parent() # type: ExpandSettingCard
|
||
|
|
path = QPainterPath()
|
||
|
|
path.setFillRule(Qt.WindingFill)
|
||
|
|
path.addRoundedRect(QRectF(self.rect().adjusted(1, 1, -1, -1)), 6, 6)
|
||
|
|
|
||
|
|
# set the bottom border radius to 0 if parent is expanded
|
||
|
|
if p.isExpand:
|
||
|
|
path.addRect(1, self.height() - 8, self.width() - 2, 8)
|
||
|
|
|
||
|
|
painter.drawPath(path.simplified())
|
||
|
|
|
||
|
|
|
||
|
|
class ExpandBorderWidget(QWidget):
|
||
|
|
""" Expand setting card border widget """
|
||
|
|
|
||
|
|
def __init__(self, parent=None):
|
||
|
|
super().__init__(parent=parent)
|
||
|
|
self.setAttribute(Qt.WA_TransparentForMouseEvents)
|
||
|
|
parent.installEventFilter(self)
|
||
|
|
|
||
|
|
def eventFilter(self, obj, e):
|
||
|
|
if obj is self.parent() and e.type() == QEvent.Resize:
|
||
|
|
self.resize(e.size())
|
||
|
|
|
||
|
|
return super().eventFilter(obj, e)
|
||
|
|
|
||
|
|
def paintEvent(self, e):
|
||
|
|
painter = QPainter(self)
|
||
|
|
painter.setRenderHints(QPainter.Antialiasing)
|
||
|
|
painter.setBrush(Qt.NoBrush)
|
||
|
|
|
||
|
|
if isDarkTheme():
|
||
|
|
painter.setPen(QColor(0, 0, 0, 50))
|
||
|
|
else:
|
||
|
|
painter.setPen(QColor(0, 0, 0, 19))
|
||
|
|
|
||
|
|
p = self.parent() # type: ExpandSettingCard
|
||
|
|
r, d = 6, 12
|
||
|
|
ch, h, w = p.card.height(), self.height(), self.width()
|
||
|
|
|
||
|
|
# only draw rounded rect if parent is not expanded
|
||
|
|
painter.drawRoundedRect(self.rect().adjusted(1, 1, -1, -1), r, r)
|
||
|
|
|
||
|
|
# draw the seperator line under card widget
|
||
|
|
if ch < h:
|
||
|
|
painter.drawLine(1, ch, w - 1, ch)
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
class ExpandSettingCard(QScrollArea):
|
||
|
|
""" Expandable setting card """
|
||
|
|
|
||
|
|
def __init__(self, icon: Union[str, QIcon, FIF], title: str, content: str = None, parent=None):
|
||
|
|
super().__init__(parent=parent)
|
||
|
|
self.isExpand = False
|
||
|
|
|
||
|
|
self.scrollWidget = QFrame(self)
|
||
|
|
self.view = QFrame(self.scrollWidget)
|
||
|
|
self.card = HeaderSettingCard(icon, title, content, self)
|
||
|
|
|
||
|
|
self.scrollLayout = QVBoxLayout(self.scrollWidget)
|
||
|
|
self.viewLayout = QVBoxLayout(self.view)
|
||
|
|
self.spaceWidget = SpaceWidget(self.scrollWidget)
|
||
|
|
self.borderWidget = ExpandBorderWidget(self)
|
||
|
|
|
||
|
|
# expand animation
|
||
|
|
self.expandAni = QPropertyAnimation(self.verticalScrollBar(), b'value', self)
|
||
|
|
|
||
|
|
self.__initWidget()
|
||
|
|
|
||
|
|
def __initWidget(self):
|
||
|
|
""" initialize widgets """
|
||
|
|
self.setWidget(self.scrollWidget)
|
||
|
|
self.setWidgetResizable(True)
|
||
|
|
self.setFixedHeight(self.card.height())
|
||
|
|
self.setViewportMargins(0, self.card.height(), 0, 0)
|
||
|
|
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||
|
|
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||
|
|
|
||
|
|
# initialize layout
|
||
|
|
self.scrollLayout.setContentsMargins(0, 0, 0, 0)
|
||
|
|
self.scrollLayout.setSpacing(0)
|
||
|
|
self.scrollLayout.addWidget(self.view)
|
||
|
|
self.scrollLayout.addWidget(self.spaceWidget)
|
||
|
|
|
||
|
|
# initialize expand animation
|
||
|
|
self.expandAni.setEasingCurve(QEasingCurve.OutQuad)
|
||
|
|
self.expandAni.setDuration(200)
|
||
|
|
|
||
|
|
# initialize style sheet
|
||
|
|
self.view.setObjectName('view')
|
||
|
|
self.scrollWidget.setObjectName('scrollWidget')
|
||
|
|
self.setProperty('isExpand', False)
|
||
|
|
FluentStyleSheet.EXPAND_SETTING_CARD.apply(self.card)
|
||
|
|
FluentStyleSheet.EXPAND_SETTING_CARD.apply(self)
|
||
|
|
|
||
|
|
self.card.installEventFilter(self)
|
||
|
|
self.expandAni.valueChanged.connect(self._onExpandValueChanged)
|
||
|
|
self.card.expandButton.clicked.connect(self.toggleExpand)
|
||
|
|
|
||
|
|
def addWidget(self, widget: QWidget):
|
||
|
|
""" add widget to tail """
|
||
|
|
self.card.addWidget(widget)
|
||
|
|
|
||
|
|
def wheelEvent(self, e):
|
||
|
|
e.ignore()
|
||
|
|
|
||
|
|
def setExpand(self, isExpand: bool):
|
||
|
|
""" set the expand status of card """
|
||
|
|
if self.isExpand == isExpand:
|
||
|
|
return
|
||
|
|
|
||
|
|
# update style sheet
|
||
|
|
self.isExpand = isExpand
|
||
|
|
self.setProperty('isExpand', isExpand)
|
||
|
|
self.setStyle(QApplication.style())
|
||
|
|
|
||
|
|
# start expand animation
|
||
|
|
if isExpand:
|
||
|
|
h = self.viewLayout.sizeHint().height()
|
||
|
|
self.verticalScrollBar().setValue(h)
|
||
|
|
self.expandAni.setStartValue(h)
|
||
|
|
self.expandAni.setEndValue(0)
|
||
|
|
else:
|
||
|
|
self.expandAni.setStartValue(0)
|
||
|
|
self.expandAni.setEndValue(self.verticalScrollBar().maximum())
|
||
|
|
|
||
|
|
self.expandAni.start()
|
||
|
|
self.card.expandButton.setExpand(isExpand)
|
||
|
|
|
||
|
|
def toggleExpand(self):
|
||
|
|
""" toggle expand status """
|
||
|
|
self.setExpand(not self.isExpand)
|
||
|
|
|
||
|
|
def resizeEvent(self, e):
|
||
|
|
self.card.resize(self.width(), self.card.height())
|
||
|
|
self.scrollWidget.resize(self.width(), self.scrollWidget.height())
|
||
|
|
|
||
|
|
def _onExpandValueChanged(self):
|
||
|
|
vh = self.viewLayout.sizeHint().height()
|
||
|
|
h = self.viewportMargins().top()
|
||
|
|
self.setFixedHeight(max(h + vh - self.verticalScrollBar().value(), h))
|
||
|
|
|
||
|
|
def _adjustViewSize(self):
|
||
|
|
""" adjust view size """
|
||
|
|
h = self.viewLayout.sizeHint().height()
|
||
|
|
self.spaceWidget.setFixedHeight(h)
|
||
|
|
|
||
|
|
if self.isExpand:
|
||
|
|
self.setFixedHeight(self.card.height()+h)
|
||
|
|
|
||
|
|
def setValue(self, value):
|
||
|
|
""" set the value of config item """
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
class GroupSeparator(QWidget):
|
||
|
|
""" group separator """
|
||
|
|
|
||
|
|
def __init__(self, parent=None):
|
||
|
|
super().__init__(parent=parent)
|
||
|
|
self.setFixedHeight(3)
|
||
|
|
|
||
|
|
def paintEvent(self, e):
|
||
|
|
painter = QPainter(self)
|
||
|
|
painter.setRenderHints(QPainter.Antialiasing)
|
||
|
|
|
||
|
|
if isDarkTheme():
|
||
|
|
painter.setPen(QColor(0, 0, 0, 50))
|
||
|
|
else:
|
||
|
|
painter.setPen(QColor(0, 0, 0, 19))
|
||
|
|
|
||
|
|
painter.drawLine(0, 1, self.width(), 1)
|
||
|
|
|
||
|
|
|
||
|
|
class ExpandGroupSettingCard(ExpandSettingCard):
|
||
|
|
""" Expand group setting card """
|
||
|
|
|
||
|
|
def __init__(self, icon: Union[str, QIcon, FIF], title: str, content: str = None, parent=None):
|
||
|
|
super().__init__(icon, title, content, parent)
|
||
|
|
self.widgets = [] # type: List[QWidget]
|
||
|
|
|
||
|
|
self.viewLayout.setContentsMargins(0, 0, 0, 0)
|
||
|
|
self.viewLayout.setSpacing(0)
|
||
|
|
|
||
|
|
def addGroupWidget(self, widget: QWidget):
|
||
|
|
""" add widget to group """
|
||
|
|
# add separator
|
||
|
|
if self.viewLayout.count() >= 1:
|
||
|
|
self.viewLayout.addWidget(GroupSeparator(self.view))
|
||
|
|
|
||
|
|
widget.setParent(self.view)
|
||
|
|
self.widgets.append(widget)
|
||
|
|
self.viewLayout.addWidget(widget)
|
||
|
|
self._adjustViewSize()
|
||
|
|
|
||
|
|
def removeGroupWidget(self, widget: QWidget):
|
||
|
|
""" remove a group from card """
|
||
|
|
if widget not in self.widgets:
|
||
|
|
return
|
||
|
|
|
||
|
|
layoutIndex = self.viewLayout.indexOf(widget)
|
||
|
|
index = self.widgets.index(widget)
|
||
|
|
|
||
|
|
self.viewLayout.removeWidget(widget)
|
||
|
|
self.widgets.remove(widget)
|
||
|
|
|
||
|
|
if not self.widgets:
|
||
|
|
return self._adjustViewSize()
|
||
|
|
|
||
|
|
# remove separator
|
||
|
|
if layoutIndex >= 1:
|
||
|
|
separator = self.viewLayout.itemAt(layoutIndex - 1).widget()
|
||
|
|
separator.deleteLater()
|
||
|
|
self.viewLayout.removeWidget(separator)
|
||
|
|
elif index == 0:
|
||
|
|
separator = self.viewLayout.itemAt(0).widget()
|
||
|
|
separator.deleteLater()
|
||
|
|
self.viewLayout.removeWidget(separator)
|
||
|
|
|
||
|
|
self._adjustViewSize()
|
||
|
|
|
||
|
|
def _adjustViewSize(self):
|
||
|
|
""" adjust view size """
|
||
|
|
h = sum(w.sizeHint().height() + 3 for w in self.widgets)
|
||
|
|
self.spaceWidget.setFixedHeight(h)
|
||
|
|
|
||
|
|
if self.isExpand:
|
||
|
|
self.setFixedHeight(self.card.height()+h)
|
||
|
|
|
||
|
|
|
||
|
|
class SimpleExpandGroupSettingCard(ExpandGroupSettingCard):
|
||
|
|
""" Simple expand group setting card """
|
||
|
|
|
||
|
|
def _adjustViewSize(self):
|
||
|
|
""" adjust view size """
|
||
|
|
h = self.viewLayout.sizeHint().height()
|
||
|
|
self.spaceWidget.setFixedHeight(h)
|
||
|
|
|
||
|
|
if self.isExpand:
|
||
|
|
self.setFixedHeight(self.card.height()+h)
|