513 lines
14 KiB
Python
513 lines
14 KiB
Python
|
|
# coding:utf-8
|
||
|
|
from enum import Enum
|
||
|
|
from string import Template
|
||
|
|
from typing import List, Union
|
||
|
|
import weakref
|
||
|
|
|
||
|
|
from PySide6.QtCore import QFile, QObject, QEvent, QDynamicPropertyChangeEvent
|
||
|
|
from PySide6.QtGui import QColor
|
||
|
|
from PySide6.QtWidgets import QWidget
|
||
|
|
|
||
|
|
from .config import qconfig, Theme, isDarkTheme
|
||
|
|
|
||
|
|
|
||
|
|
class StyleSheetManager(QObject):
|
||
|
|
""" Style sheet manager """
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.widgets = weakref.WeakKeyDictionary()
|
||
|
|
|
||
|
|
def register(self, source, widget: QWidget, reset=True):
|
||
|
|
""" register widget to manager
|
||
|
|
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
source: str | StyleSheetBase
|
||
|
|
qss source, it could be:
|
||
|
|
* `str`: qss file path
|
||
|
|
* `StyleSheetBase`: style sheet instance
|
||
|
|
|
||
|
|
widget: QWidget
|
||
|
|
the widget to set style sheet
|
||
|
|
|
||
|
|
reset: bool
|
||
|
|
whether to reset the qss source
|
||
|
|
"""
|
||
|
|
if isinstance(source, str):
|
||
|
|
source = StyleSheetFile(source)
|
||
|
|
|
||
|
|
if widget not in self.widgets:
|
||
|
|
widget.destroyed.connect(lambda: self.deregister(widget))
|
||
|
|
widget.installEventFilter(CustomStyleSheetWatcher(widget))
|
||
|
|
widget.installEventFilter(DirtyStyleSheetWatcher(widget))
|
||
|
|
self.widgets[widget] = StyleSheetCompose([source, CustomStyleSheet(widget)])
|
||
|
|
|
||
|
|
if not reset:
|
||
|
|
self.source(widget).add(source)
|
||
|
|
else:
|
||
|
|
self.widgets[widget] = StyleSheetCompose([source, CustomStyleSheet(widget)])
|
||
|
|
|
||
|
|
def deregister(self, widget: QWidget):
|
||
|
|
""" deregister widget from manager """
|
||
|
|
if widget not in self.widgets:
|
||
|
|
return
|
||
|
|
|
||
|
|
self.widgets.pop(widget)
|
||
|
|
|
||
|
|
def items(self):
|
||
|
|
return self.widgets.items()
|
||
|
|
|
||
|
|
def source(self, widget: QWidget):
|
||
|
|
""" get the qss source of widget """
|
||
|
|
return self.widgets.get(widget, StyleSheetCompose([]))
|
||
|
|
|
||
|
|
|
||
|
|
styleSheetManager = StyleSheetManager()
|
||
|
|
|
||
|
|
|
||
|
|
class QssTemplate(Template):
|
||
|
|
""" style sheet template """
|
||
|
|
|
||
|
|
delimiter = '--'
|
||
|
|
|
||
|
|
|
||
|
|
def applyThemeColor(qss: str):
|
||
|
|
""" apply theme color to style sheet
|
||
|
|
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
qss: str
|
||
|
|
the style sheet string to apply theme color, the substituted variable
|
||
|
|
should be equal to the value of `ThemeColor` and starts width `--`, i.e `--ThemeColorPrimary`
|
||
|
|
"""
|
||
|
|
template = QssTemplate(qss)
|
||
|
|
mappings = {c.value: c.name() for c in ThemeColor._member_map_.values()}
|
||
|
|
return template.safe_substitute(mappings)
|
||
|
|
|
||
|
|
|
||
|
|
class StyleSheetBase:
|
||
|
|
""" Style sheet base class """
|
||
|
|
|
||
|
|
def path(self, theme=Theme.AUTO):
|
||
|
|
""" get the path of style sheet """
|
||
|
|
raise NotImplementedError
|
||
|
|
|
||
|
|
def content(self, theme=Theme.AUTO):
|
||
|
|
""" get the content of style sheet """
|
||
|
|
return getStyleSheetFromFile(self.path(theme))
|
||
|
|
|
||
|
|
def apply(self, widget: QWidget, theme=Theme.AUTO):
|
||
|
|
""" apply style sheet to widget """
|
||
|
|
setStyleSheet(widget, self, theme)
|
||
|
|
|
||
|
|
|
||
|
|
class FluentStyleSheet(StyleSheetBase, Enum):
|
||
|
|
""" Fluent style sheet """
|
||
|
|
|
||
|
|
MENU = "menu"
|
||
|
|
LABEL = "label"
|
||
|
|
PIVOT = "pivot"
|
||
|
|
BUTTON = "button"
|
||
|
|
DIALOG = "dialog"
|
||
|
|
SLIDER = "slider"
|
||
|
|
INFO_BAR = "info_bar"
|
||
|
|
SPIN_BOX = "spin_box"
|
||
|
|
TAB_VIEW = "tab_view"
|
||
|
|
TOOL_TIP = "tool_tip"
|
||
|
|
CHECK_BOX = "check_box"
|
||
|
|
COMBO_BOX = "combo_box"
|
||
|
|
FLIP_VIEW = "flip_view"
|
||
|
|
LINE_EDIT = "line_edit"
|
||
|
|
LIST_VIEW = "list_view"
|
||
|
|
TREE_VIEW = "tree_view"
|
||
|
|
INFO_BADGE = "info_badge"
|
||
|
|
PIPS_PAGER = "pips_pager"
|
||
|
|
TABLE_VIEW = "table_view"
|
||
|
|
CARD_WIDGET = "card_widget"
|
||
|
|
TIME_PICKER = "time_picker"
|
||
|
|
COLOR_DIALOG = "color_dialog"
|
||
|
|
MEDIA_PLAYER = "media_player"
|
||
|
|
SETTING_CARD = "setting_card"
|
||
|
|
TEACHING_TIP = "teaching_tip"
|
||
|
|
FLUENT_WINDOW = "fluent_window"
|
||
|
|
SWITCH_BUTTON = "switch_button"
|
||
|
|
MESSAGE_DIALOG = "message_dialog"
|
||
|
|
STATE_TOOL_TIP = "state_tool_tip"
|
||
|
|
CALENDAR_PICKER = "calendar_picker"
|
||
|
|
FOLDER_LIST_DIALOG = "folder_list_dialog"
|
||
|
|
SETTING_CARD_GROUP = "setting_card_group"
|
||
|
|
EXPAND_SETTING_CARD = "expand_setting_card"
|
||
|
|
NAVIGATION_INTERFACE = "navigation_interface"
|
||
|
|
|
||
|
|
def path(self, theme=Theme.AUTO):
|
||
|
|
theme = qconfig.theme if theme == Theme.AUTO else theme
|
||
|
|
return f":/qfluentwidgets/qss/{theme.value.lower()}/{self.value}.qss"
|
||
|
|
|
||
|
|
|
||
|
|
class StyleSheetFile(StyleSheetBase):
|
||
|
|
""" Style sheet file """
|
||
|
|
|
||
|
|
def __init__(self, path: str):
|
||
|
|
super().__init__()
|
||
|
|
self.filePath = path
|
||
|
|
|
||
|
|
def path(self, theme=Theme.AUTO):
|
||
|
|
return self.filePath
|
||
|
|
|
||
|
|
|
||
|
|
class CustomStyleSheet(StyleSheetBase):
|
||
|
|
""" Custom style sheet """
|
||
|
|
|
||
|
|
DARK_QSS_KEY = 'darkCustomQss'
|
||
|
|
LIGHT_QSS_KEY = 'lightCustomQss'
|
||
|
|
|
||
|
|
def __init__(self, widget: QWidget) -> None:
|
||
|
|
super().__init__()
|
||
|
|
self._widget = weakref.ref(widget)
|
||
|
|
|
||
|
|
def path(self, theme=Theme.AUTO):
|
||
|
|
return ''
|
||
|
|
|
||
|
|
@property
|
||
|
|
def widget(self):
|
||
|
|
return self._widget()
|
||
|
|
|
||
|
|
def __eq__(self, other: object) -> bool:
|
||
|
|
if not isinstance(other, CustomStyleSheet):
|
||
|
|
return False
|
||
|
|
|
||
|
|
return other.widget is self.widget
|
||
|
|
|
||
|
|
def setCustomStyleSheet(self, lightQss: str, darkQss: str):
|
||
|
|
""" set custom style sheet in light and dark theme mode """
|
||
|
|
self.setLightStyleSheet(lightQss)
|
||
|
|
self.setDarkStyleSheet(darkQss)
|
||
|
|
return self
|
||
|
|
|
||
|
|
def setLightStyleSheet(self, qss: str):
|
||
|
|
""" set the style sheet in light mode """
|
||
|
|
if self.widget:
|
||
|
|
self.widget.setProperty(self.LIGHT_QSS_KEY, qss)
|
||
|
|
|
||
|
|
return self
|
||
|
|
|
||
|
|
def setDarkStyleSheet(self, qss: str):
|
||
|
|
""" set the style sheet in dark mode """
|
||
|
|
if self.widget:
|
||
|
|
self.widget.setProperty(self.DARK_QSS_KEY, qss)
|
||
|
|
|
||
|
|
return self
|
||
|
|
|
||
|
|
def lightStyleSheet(self) -> str:
|
||
|
|
if not self.widget:
|
||
|
|
return ''
|
||
|
|
|
||
|
|
return self.widget.property(self.LIGHT_QSS_KEY) or ''
|
||
|
|
|
||
|
|
def darkStyleSheet(self) -> str:
|
||
|
|
if not self.widget:
|
||
|
|
return ''
|
||
|
|
|
||
|
|
return self.widget.property(self.DARK_QSS_KEY) or ''
|
||
|
|
|
||
|
|
def content(self, theme=Theme.AUTO) -> str:
|
||
|
|
theme = qconfig.theme if theme == Theme.AUTO else theme
|
||
|
|
|
||
|
|
if theme == Theme.LIGHT:
|
||
|
|
return self.lightStyleSheet()
|
||
|
|
|
||
|
|
return self.darkStyleSheet()
|
||
|
|
|
||
|
|
|
||
|
|
class CustomStyleSheetWatcher(QObject):
|
||
|
|
""" Custom style sheet watcher """
|
||
|
|
|
||
|
|
def eventFilter(self, obj: QWidget, e: QEvent):
|
||
|
|
if e.type() != QEvent.DynamicPropertyChange:
|
||
|
|
return super().eventFilter(obj, e)
|
||
|
|
|
||
|
|
name = QDynamicPropertyChangeEvent(e).propertyName().data().decode()
|
||
|
|
if name in [CustomStyleSheet.LIGHT_QSS_KEY, CustomStyleSheet.DARK_QSS_KEY]:
|
||
|
|
addStyleSheet(obj, CustomStyleSheet(obj))
|
||
|
|
|
||
|
|
return super().eventFilter(obj, e)
|
||
|
|
|
||
|
|
|
||
|
|
class DirtyStyleSheetWatcher(QObject):
|
||
|
|
""" Dirty style sheet watcher """
|
||
|
|
|
||
|
|
def eventFilter(self, obj: QWidget, e: QEvent):
|
||
|
|
if e.type() != QEvent.Type.Paint or not obj.property('dirty-qss'):
|
||
|
|
return super().eventFilter(obj, e)
|
||
|
|
|
||
|
|
obj.setProperty('dirty-qss', False)
|
||
|
|
if obj in styleSheetManager.widgets:
|
||
|
|
obj.setStyleSheet(getStyleSheet(styleSheetManager.source(obj)))
|
||
|
|
|
||
|
|
return super().eventFilter(obj, e)
|
||
|
|
|
||
|
|
|
||
|
|
class StyleSheetCompose(StyleSheetBase):
|
||
|
|
""" Style sheet compose """
|
||
|
|
|
||
|
|
def __init__(self, sources: List[StyleSheetBase]):
|
||
|
|
super().__init__()
|
||
|
|
self.sources = sources
|
||
|
|
|
||
|
|
def content(self, theme=Theme.AUTO):
|
||
|
|
return '\n'.join([i.content(theme) for i in self.sources])
|
||
|
|
|
||
|
|
def add(self, source: StyleSheetBase):
|
||
|
|
""" add style sheet source """
|
||
|
|
if source is self or source in self.sources:
|
||
|
|
return
|
||
|
|
|
||
|
|
self.sources.append(source)
|
||
|
|
|
||
|
|
def remove(self, source: StyleSheetBase):
|
||
|
|
""" remove style sheet source """
|
||
|
|
if source not in self.sources:
|
||
|
|
return
|
||
|
|
|
||
|
|
self.sources.remove(source)
|
||
|
|
|
||
|
|
|
||
|
|
def getStyleSheetFromFile(file: Union[str, QFile]):
|
||
|
|
""" get style sheet from qss file """
|
||
|
|
f = QFile(file)
|
||
|
|
f.open(QFile.ReadOnly)
|
||
|
|
qss = str(f.readAll(), encoding='utf-8')
|
||
|
|
f.close()
|
||
|
|
return qss
|
||
|
|
|
||
|
|
|
||
|
|
def getStyleSheet(source: Union[str, StyleSheetBase], theme=Theme.AUTO):
|
||
|
|
""" get style sheet
|
||
|
|
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
source: str | StyleSheetBase
|
||
|
|
qss source, it could be:
|
||
|
|
* `str`: qss file path
|
||
|
|
* `StyleSheetBase`: style sheet instance
|
||
|
|
|
||
|
|
theme: Theme
|
||
|
|
the theme of style sheet
|
||
|
|
"""
|
||
|
|
if isinstance(source, str):
|
||
|
|
source = StyleSheetFile(source)
|
||
|
|
|
||
|
|
return applyThemeColor(source.content(theme))
|
||
|
|
|
||
|
|
|
||
|
|
def setStyleSheet(widget: QWidget, source: Union[str, StyleSheetBase], theme=Theme.AUTO, register=True):
|
||
|
|
""" set the style sheet of widget
|
||
|
|
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
widget: QWidget
|
||
|
|
the widget to set style sheet
|
||
|
|
|
||
|
|
source: str | StyleSheetBase
|
||
|
|
qss source, it could be:
|
||
|
|
* `str`: qss file path
|
||
|
|
* `StyleSheetBase`: style sheet instance
|
||
|
|
|
||
|
|
theme: Theme
|
||
|
|
the theme of style sheet
|
||
|
|
|
||
|
|
register: bool
|
||
|
|
whether to register the widget to the style manager. If `register=True`, the style of
|
||
|
|
the widget will be updated automatically when the theme changes
|
||
|
|
"""
|
||
|
|
if register:
|
||
|
|
styleSheetManager.register(source, widget)
|
||
|
|
|
||
|
|
widget.setStyleSheet(getStyleSheet(source, theme))
|
||
|
|
|
||
|
|
|
||
|
|
def setCustomStyleSheet(widget: QWidget, lightQss: str, darkQss: str):
|
||
|
|
""" set custom style sheet
|
||
|
|
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
widget: QWidget
|
||
|
|
the widget to add style sheet
|
||
|
|
|
||
|
|
lightQss: str
|
||
|
|
style sheet used in light theme mode
|
||
|
|
|
||
|
|
darkQss: str
|
||
|
|
style sheet used in light theme mode
|
||
|
|
"""
|
||
|
|
CustomStyleSheet(widget).setCustomStyleSheet(lightQss, darkQss)
|
||
|
|
|
||
|
|
|
||
|
|
def addStyleSheet(widget: QWidget, source: Union[str, StyleSheetBase], theme=Theme.AUTO, register=True):
|
||
|
|
""" add style sheet to widget
|
||
|
|
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
widget: QWidget
|
||
|
|
the widget to set style sheet
|
||
|
|
|
||
|
|
source: str | StyleSheetBase
|
||
|
|
qss source, it could be:
|
||
|
|
* `str`: qss file path
|
||
|
|
* `StyleSheetBase`: style sheet instance
|
||
|
|
|
||
|
|
theme: Theme
|
||
|
|
the theme of style sheet
|
||
|
|
|
||
|
|
register: bool
|
||
|
|
whether to register the widget to the style manager. If `register=True`, the style of
|
||
|
|
the widget will be updated automatically when the theme changes
|
||
|
|
"""
|
||
|
|
if register:
|
||
|
|
styleSheetManager.register(source, widget, reset=False)
|
||
|
|
qss = getStyleSheet(styleSheetManager.source(widget), theme)
|
||
|
|
else:
|
||
|
|
qss = widget.styleSheet() + '\n' + getStyleSheet(source, theme)
|
||
|
|
|
||
|
|
if qss.rstrip() != widget.styleSheet().rstrip():
|
||
|
|
widget.setStyleSheet(qss)
|
||
|
|
|
||
|
|
|
||
|
|
def updateStyleSheet(lazy=False):
|
||
|
|
""" update the style sheet of all fluent widgets
|
||
|
|
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
lazy: bool
|
||
|
|
whether to update the style sheet lazily, set to `True` will accelerate theme switching
|
||
|
|
"""
|
||
|
|
removes = []
|
||
|
|
for widget, file in styleSheetManager.items():
|
||
|
|
try:
|
||
|
|
if not (lazy and widget.visibleRegion().isNull()):
|
||
|
|
setStyleSheet(widget, file, qconfig.theme)
|
||
|
|
else:
|
||
|
|
styleSheetManager.register(file, widget)
|
||
|
|
widget.setProperty('dirty-qss', True)
|
||
|
|
except RuntimeError:
|
||
|
|
removes.append(widget)
|
||
|
|
|
||
|
|
for widget in removes:
|
||
|
|
styleSheetManager.deregister(widget)
|
||
|
|
|
||
|
|
|
||
|
|
def setTheme(theme: Theme, save=False, lazy=False):
|
||
|
|
""" set the theme of application
|
||
|
|
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
theme: Theme
|
||
|
|
theme mode
|
||
|
|
|
||
|
|
save: bool
|
||
|
|
whether to save the change to config file
|
||
|
|
|
||
|
|
lazy: bool
|
||
|
|
whether to update the style sheet lazily, set to `True` will accelerate theme switching
|
||
|
|
"""
|
||
|
|
qconfig.set(qconfig.themeMode, theme, save)
|
||
|
|
updateStyleSheet(lazy)
|
||
|
|
qconfig.themeChangedFinished.emit()
|
||
|
|
|
||
|
|
|
||
|
|
def toggleTheme(save=False, lazy=False):
|
||
|
|
""" toggle the theme of application
|
||
|
|
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
save: bool
|
||
|
|
whether to save the change to config file
|
||
|
|
|
||
|
|
lazy: bool
|
||
|
|
whether to update the style sheet lazily, set to `True` will accelerate theme switching
|
||
|
|
"""
|
||
|
|
theme = Theme.LIGHT if isDarkTheme() else Theme.DARK
|
||
|
|
setTheme(theme, save, lazy)
|
||
|
|
|
||
|
|
|
||
|
|
class ThemeColor(Enum):
|
||
|
|
""" Theme color type """
|
||
|
|
|
||
|
|
PRIMARY = "ThemeColorPrimary"
|
||
|
|
DARK_1 = "ThemeColorDark1"
|
||
|
|
DARK_2 = "ThemeColorDark2"
|
||
|
|
DARK_3 = "ThemeColorDark3"
|
||
|
|
LIGHT_1 = "ThemeColorLight1"
|
||
|
|
LIGHT_2 = "ThemeColorLight2"
|
||
|
|
LIGHT_3 = "ThemeColorLight3"
|
||
|
|
|
||
|
|
def name(self):
|
||
|
|
return self.color().name()
|
||
|
|
|
||
|
|
def color(self):
|
||
|
|
color = qconfig.get(qconfig._cfg.themeColor) # type:QColor
|
||
|
|
|
||
|
|
# transform color into hsv space
|
||
|
|
h, s, v, _ = color.getHsvF()
|
||
|
|
|
||
|
|
if isDarkTheme():
|
||
|
|
s *= 0.84
|
||
|
|
v = 1
|
||
|
|
if self == self.DARK_1:
|
||
|
|
v *= 0.9
|
||
|
|
elif self == self.DARK_2:
|
||
|
|
s *= 0.977
|
||
|
|
v *= 0.82
|
||
|
|
elif self == self.DARK_3:
|
||
|
|
s *= 0.95
|
||
|
|
v *= 0.7
|
||
|
|
elif self == self.LIGHT_1:
|
||
|
|
s *= 0.92
|
||
|
|
elif self == self.LIGHT_2:
|
||
|
|
s *= 0.78
|
||
|
|
elif self == self.LIGHT_3:
|
||
|
|
s *= 0.65
|
||
|
|
else:
|
||
|
|
if self == self.DARK_1:
|
||
|
|
v *= 0.75
|
||
|
|
elif self == self.DARK_2:
|
||
|
|
s *= 1.05
|
||
|
|
v *= 0.5
|
||
|
|
elif self == self.DARK_3:
|
||
|
|
s *= 1.1
|
||
|
|
v *= 0.4
|
||
|
|
elif self == self.LIGHT_1:
|
||
|
|
v *= 1.05
|
||
|
|
elif self == self.LIGHT_2:
|
||
|
|
s *= 0.75
|
||
|
|
v *= 1.05
|
||
|
|
elif self == self.LIGHT_3:
|
||
|
|
s *= 0.65
|
||
|
|
v *= 1.05
|
||
|
|
|
||
|
|
return QColor.fromHsvF(h, min(s, 1), min(v, 1))
|
||
|
|
|
||
|
|
|
||
|
|
def themeColor():
|
||
|
|
""" get theme color """
|
||
|
|
return ThemeColor.PRIMARY.color()
|
||
|
|
|
||
|
|
|
||
|
|
def setThemeColor(color, save=False, lazy=False):
|
||
|
|
""" set theme color
|
||
|
|
|
||
|
|
Parameters
|
||
|
|
----------
|
||
|
|
color: QColor | Qt.GlobalColor | str
|
||
|
|
theme color
|
||
|
|
|
||
|
|
save: bool
|
||
|
|
whether to save to change to config file
|
||
|
|
|
||
|
|
lazy: bool
|
||
|
|
whether to update the style sheet lazily
|
||
|
|
"""
|
||
|
|
color = QColor(color)
|
||
|
|
qconfig.set(qconfig.themeColor, color, save=save)
|
||
|
|
updateStyleSheet(lazy)
|