312 lines
9.6 KiB
Python
312 lines
9.6 KiB
Python
|
|
# coding: utf-8
|
||
|
|
from enum import Enum
|
||
|
|
|
||
|
|
from PySide6.QtCore import Qt, QTimer, Property, Signal, QEvent, QPoint, QPropertyAnimation, QEasingCurve
|
||
|
|
from PySide6.QtGui import QColor, QPainter, QHoverEvent
|
||
|
|
from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QToolButton, QWidget
|
||
|
|
|
||
|
|
from ...common.style_sheet import FluentStyleSheet, themeColor, ThemeColor, isDarkTheme, setCustomStyleSheet
|
||
|
|
from ...common.overload import singledispatchmethod
|
||
|
|
from ...common.color import fallbackThemeColor, validColor
|
||
|
|
from .button import ToolButton
|
||
|
|
|
||
|
|
|
||
|
|
class Indicator(ToolButton):
|
||
|
|
""" Indicator of switch button """
|
||
|
|
|
||
|
|
checkedChanged = Signal(bool)
|
||
|
|
|
||
|
|
def __init__(self, parent):
|
||
|
|
super().__init__(parent=parent)
|
||
|
|
self.setCheckable(True)
|
||
|
|
self.setFixedSize(42, 22)
|
||
|
|
self.lightCheckedColor = QColor()
|
||
|
|
self.darkCheckedColor = QColor()
|
||
|
|
|
||
|
|
self._sliderX = 5
|
||
|
|
self.slideAni = QPropertyAnimation(self, b'sliderX', self)
|
||
|
|
self.slideAni.setDuration(120)
|
||
|
|
|
||
|
|
self.toggled.connect(self._toggleSlider)
|
||
|
|
|
||
|
|
def mouseReleaseEvent(self, e):
|
||
|
|
""" toggle checked state when mouse release"""
|
||
|
|
super().mouseReleaseEvent(e)
|
||
|
|
self.checkedChanged.emit(self.isChecked())
|
||
|
|
|
||
|
|
def _toggleSlider(self):
|
||
|
|
self.slideAni.setEndValue(25 if self.isChecked() else 5)
|
||
|
|
self.slideAni.start()
|
||
|
|
|
||
|
|
def toggle(self):
|
||
|
|
self.setChecked(not self.isChecked())
|
||
|
|
|
||
|
|
def setDown(self, isDown: bool):
|
||
|
|
self.isPressed = isDown
|
||
|
|
super().setDown(isDown)
|
||
|
|
|
||
|
|
def setHover(self, isHover: bool):
|
||
|
|
self.isHover = isHover
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
def setCheckedColor(self, light, dark):
|
||
|
|
self.lightCheckedColor = QColor(light)
|
||
|
|
self.darkCheckedColor = QColor(dark)
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
def paintEvent(self, e):
|
||
|
|
""" paint indicator """
|
||
|
|
painter = QPainter(self)
|
||
|
|
painter.setRenderHints(QPainter.Antialiasing)
|
||
|
|
self._drawBackground(painter)
|
||
|
|
self._drawCircle(painter)
|
||
|
|
|
||
|
|
def _drawBackground(self, painter: QPainter):
|
||
|
|
r = self.height() / 2
|
||
|
|
painter.setPen(self._borderColor())
|
||
|
|
painter.setBrush(self._backgroundColor())
|
||
|
|
painter.drawRoundedRect(self.rect().adjusted(1, 1, -1, -1), r, r)
|
||
|
|
|
||
|
|
def _drawCircle(self, painter: QPainter):
|
||
|
|
painter.setPen(Qt.NoPen)
|
||
|
|
painter.setBrush(self._sliderColor())
|
||
|
|
painter.drawEllipse(int(self.sliderX), 5, 12, 12)
|
||
|
|
|
||
|
|
def _backgroundColor(self):
|
||
|
|
isDark = isDarkTheme()
|
||
|
|
|
||
|
|
if self.isChecked():
|
||
|
|
color = self.darkCheckedColor if isDark else self.lightCheckedColor
|
||
|
|
if not self.isEnabled():
|
||
|
|
return QColor(255, 255, 255, 41) if isDark else QColor(0, 0, 0, 56)
|
||
|
|
if self.isPressed:
|
||
|
|
return validColor(color, ThemeColor.LIGHT_2.color())
|
||
|
|
elif self.isHover:
|
||
|
|
return validColor(color, ThemeColor.LIGHT_1.color())
|
||
|
|
|
||
|
|
return fallbackThemeColor(color)
|
||
|
|
else:
|
||
|
|
if not self.isEnabled():
|
||
|
|
return QColor(0, 0, 0, 0)
|
||
|
|
if self.isPressed:
|
||
|
|
return QColor(255, 255, 255, 18) if isDark else QColor(0, 0, 0, 23)
|
||
|
|
elif self.isHover:
|
||
|
|
return QColor(255, 255, 255, 10) if isDark else QColor(0, 0, 0, 15)
|
||
|
|
|
||
|
|
return QColor(0, 0, 0, 0)
|
||
|
|
|
||
|
|
def _borderColor(self):
|
||
|
|
isDark = isDarkTheme()
|
||
|
|
|
||
|
|
if self.isChecked():
|
||
|
|
return self._backgroundColor() if self.isEnabled() else QColor(0, 0, 0, 0)
|
||
|
|
else:
|
||
|
|
if self.isEnabled():
|
||
|
|
return QColor(255, 255, 255, 153) if isDark else QColor(0, 0, 0, 133)
|
||
|
|
|
||
|
|
return QColor(255, 255, 255, 41) if isDark else QColor(0, 0, 0, 56)
|
||
|
|
|
||
|
|
def _sliderColor(self):
|
||
|
|
isDark = isDarkTheme()
|
||
|
|
|
||
|
|
if self.isChecked():
|
||
|
|
if self.isEnabled():
|
||
|
|
return QColor(Qt.black if isDark else Qt.white)
|
||
|
|
|
||
|
|
return QColor(255, 255, 255, 77) if isDark else QColor(255, 255, 255)
|
||
|
|
else:
|
||
|
|
if self.isEnabled():
|
||
|
|
return QColor(255, 255, 255, 201) if isDark else QColor(0, 0, 0, 156)
|
||
|
|
|
||
|
|
return QColor(255, 255, 255, 96) if isDark else QColor(0, 0, 0, 91)
|
||
|
|
|
||
|
|
def getSliderX(self):
|
||
|
|
return self._sliderX
|
||
|
|
|
||
|
|
def setSliderX(self, x):
|
||
|
|
self._sliderX = max(x, 5)
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
sliderX = Property(float, getSliderX, setSliderX)
|
||
|
|
|
||
|
|
|
||
|
|
class IndicatorPosition(Enum):
|
||
|
|
""" Indicator position """
|
||
|
|
LEFT = 0
|
||
|
|
RIGHT = 1
|
||
|
|
|
||
|
|
|
||
|
|
class SwitchButton(QWidget):
|
||
|
|
""" Switch button class
|
||
|
|
|
||
|
|
Constructors
|
||
|
|
------------
|
||
|
|
* SwitchButton(`parent`: QWidget = None)
|
||
|
|
* SwitchButton(`text`: str = "Off", `parent`: QWidget = None, `indicatorPos`=IndicatorPosition.LEFT)
|
||
|
|
"""
|
||
|
|
|
||
|
|
checkedChanged = Signal(bool)
|
||
|
|
|
||
|
|
@singledispatchmethod
|
||
|
|
def __init__(self, parent: QWidget = None, indicatorPos=IndicatorPosition.LEFT):
|
||
|
|
"""
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
parent: QWidget
|
||
|
|
parent widget
|
||
|
|
|
||
|
|
indicatorPosition: IndicatorPosition
|
||
|
|
the position of indicator
|
||
|
|
"""
|
||
|
|
super().__init__(parent=parent)
|
||
|
|
self._text = self.tr('Off')
|
||
|
|
self._offText = self.tr('Off')
|
||
|
|
self._onText = self.tr('On')
|
||
|
|
self.__spacing = 12
|
||
|
|
self.lightTextColor = QColor(0, 0, 0)
|
||
|
|
self.darkTextColor = QColor(255, 255, 255)
|
||
|
|
|
||
|
|
self.indicatorPos = indicatorPos
|
||
|
|
self.hBox = QHBoxLayout(self)
|
||
|
|
self.indicator = Indicator(self)
|
||
|
|
self.label = QLabel(self._text, self)
|
||
|
|
|
||
|
|
self.__initWidget()
|
||
|
|
|
||
|
|
@__init__.register
|
||
|
|
def _(self, text: str = 'Off', parent: QWidget = None, indicatorPos=IndicatorPosition.LEFT):
|
||
|
|
"""
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
text: str
|
||
|
|
the text of switch button
|
||
|
|
|
||
|
|
parent: QWidget
|
||
|
|
parent widget
|
||
|
|
|
||
|
|
indicatorPosition: IndicatorPosition
|
||
|
|
the position of indicator
|
||
|
|
"""
|
||
|
|
self.__init__(parent, indicatorPos)
|
||
|
|
self._offText = text
|
||
|
|
self.setText(text)
|
||
|
|
|
||
|
|
def __initWidget(self):
|
||
|
|
""" initialize widgets """
|
||
|
|
self.setAttribute(Qt.WA_StyledBackground)
|
||
|
|
self.installEventFilter(self)
|
||
|
|
self.setFixedHeight(22)
|
||
|
|
|
||
|
|
# set layout
|
||
|
|
self.hBox.setSpacing(self.__spacing)
|
||
|
|
self.hBox.setContentsMargins(2, 0, 0, 0)
|
||
|
|
|
||
|
|
if self.indicatorPos == IndicatorPosition.LEFT:
|
||
|
|
self.hBox.addWidget(self.indicator)
|
||
|
|
self.hBox.addWidget(self.label)
|
||
|
|
self.hBox.setAlignment(Qt.AlignLeft)
|
||
|
|
else:
|
||
|
|
self.hBox.addWidget(self.label, 0, Qt.AlignRight)
|
||
|
|
self.hBox.addWidget(self.indicator, 0, Qt.AlignRight)
|
||
|
|
self.hBox.setAlignment(Qt.AlignRight)
|
||
|
|
|
||
|
|
# set default style sheet
|
||
|
|
FluentStyleSheet.SWITCH_BUTTON.apply(self)
|
||
|
|
FluentStyleSheet.SWITCH_BUTTON.apply(self.label)
|
||
|
|
|
||
|
|
# connect signal to slot
|
||
|
|
self.indicator.toggled.connect(self._updateText)
|
||
|
|
self.indicator.toggled.connect(self.checkedChanged)
|
||
|
|
|
||
|
|
def eventFilter(self, obj, e: QEvent):
|
||
|
|
if obj is self and self.isEnabled():
|
||
|
|
if e.type() == QEvent.MouseButtonPress:
|
||
|
|
self.indicator.setDown(True)
|
||
|
|
elif e.type() == QEvent.MouseButtonRelease:
|
||
|
|
self.indicator.setDown(False)
|
||
|
|
self.indicator.toggle()
|
||
|
|
elif e.type() == QEvent.Enter:
|
||
|
|
self.indicator.setHover(True)
|
||
|
|
elif e.type() == QEvent.Leave:
|
||
|
|
self.indicator.setHover(False)
|
||
|
|
|
||
|
|
return super().eventFilter(obj, e)
|
||
|
|
|
||
|
|
def isChecked(self):
|
||
|
|
return self.indicator.isChecked()
|
||
|
|
|
||
|
|
def setChecked(self, isChecked):
|
||
|
|
""" set checked state """
|
||
|
|
self._updateText()
|
||
|
|
self.indicator.setChecked(isChecked)
|
||
|
|
|
||
|
|
def setTextColor(self, light, dark):
|
||
|
|
""" set the color of text
|
||
|
|
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
light, dark: str | QColor | Qt.GlobalColor
|
||
|
|
text color in light/dark theme mode
|
||
|
|
"""
|
||
|
|
self.lightTextColor = QColor(light)
|
||
|
|
self.darkTextColor = QColor(dark)
|
||
|
|
|
||
|
|
setCustomStyleSheet(
|
||
|
|
self.label,
|
||
|
|
f"SwitchButton>QLabel{{color:{self.lightTextColor.name(QColor.NameFormat.HexArgb)}}}",
|
||
|
|
f"SwitchButton>QLabel{{color:{self.darkTextColor.name(QColor.NameFormat.HexArgb)}}}"
|
||
|
|
)
|
||
|
|
|
||
|
|
def setCheckedIndicatorColor(self, light, dark):
|
||
|
|
""" set the color of indicator in checked status
|
||
|
|
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
light, dark: str | QColor | Qt.GlobalColor
|
||
|
|
border color in light/dark theme mode
|
||
|
|
"""
|
||
|
|
self.indicator.setCheckedColor(light, dark)
|
||
|
|
|
||
|
|
def toggleChecked(self):
|
||
|
|
""" toggle checked state """
|
||
|
|
self.indicator.setChecked(not self.indicator.isChecked())
|
||
|
|
|
||
|
|
def _updateText(self):
|
||
|
|
self.setText(self.onText if self.isChecked() else self.offText)
|
||
|
|
self.adjustSize()
|
||
|
|
|
||
|
|
def getText(self):
|
||
|
|
return self._text
|
||
|
|
|
||
|
|
def setText(self, text):
|
||
|
|
self._text = text
|
||
|
|
self.label.setText(text)
|
||
|
|
self.adjustSize()
|
||
|
|
|
||
|
|
def getSpacing(self):
|
||
|
|
return self.__spacing
|
||
|
|
|
||
|
|
def setSpacing(self, spacing):
|
||
|
|
self.__spacing = spacing
|
||
|
|
self.hBox.setSpacing(spacing)
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
def getOnText(self):
|
||
|
|
return self._onText
|
||
|
|
|
||
|
|
def setOnText(self, text):
|
||
|
|
self._onText = text
|
||
|
|
self._updateText()
|
||
|
|
|
||
|
|
def getOffText(self):
|
||
|
|
return self._offText
|
||
|
|
|
||
|
|
def setOffText(self, text):
|
||
|
|
self._offText = text
|
||
|
|
self._updateText()
|
||
|
|
|
||
|
|
spacing = Property(int, getSpacing, setSpacing)
|
||
|
|
checked = Property(bool, isChecked, setChecked)
|
||
|
|
text = Property(str, getText, setText)
|
||
|
|
onText = Property(str, getOnText, setOnText)
|
||
|
|
offText = Property(str, getOffText, setOffText)
|