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