|
| 1 | +from collections.abc import Callable |
| 2 | + |
| 3 | +from PySide6.QtCore import QSettings, Qt |
| 4 | +from PySide6.QtGui import QColor, QPalette |
| 5 | +from PySide6.QtWidgets import QApplication |
| 6 | + |
| 7 | +from .qt_logger import logger |
| 8 | + |
| 9 | +theme_update_hooks: list[Callable[[], None]] = [] |
| 10 | +"List of callables that will be called when any theme is changed." |
| 11 | + |
| 12 | + |
| 13 | +def _update_theme_hooks() -> None: |
| 14 | + """Update all theme hooks by calling each hook in the list.""" |
| 15 | + for hook in theme_update_hooks: |
| 16 | + try: |
| 17 | + hook() |
| 18 | + except Exception as e: |
| 19 | + logger.error(e) |
| 20 | + |
| 21 | + |
| 22 | +def _load_palette_from_file(file_path: str, default_palette: QPalette) -> QPalette: |
| 23 | + """Load a palette from a file and update the default palette with the loaded colors. |
| 24 | +
|
| 25 | + The file should be in the INI format and should have the following format: |
| 26 | +
|
| 27 | + [ColorRoleName] |
| 28 | + ColorGroupName = Color |
| 29 | +
|
| 30 | + ColorRoleName is the name of the color role (e.g. Window, Button, etc.) |
| 31 | + ColorGroupName is the name of the color group (e.g. Active, Inactive, Disabled, etc.) |
| 32 | + Color is the color value in the QColor supported format (e.g. #RRGGBB, blue, etc.) |
| 33 | +
|
| 34 | + Args: |
| 35 | + file_path (str): The path to the file containing color information. |
| 36 | + default_palette (QPalette): The default palette to be updated with the colors. |
| 37 | +
|
| 38 | + Returns: |
| 39 | + QPalette: The updated palette based on the colors specified in the file. |
| 40 | + """ |
| 41 | + theme = QSettings(file_path, QSettings.Format.IniFormat, QApplication.instance()) |
| 42 | + |
| 43 | + color_groups = ( |
| 44 | + QPalette.ColorGroup.Active, |
| 45 | + QPalette.ColorGroup.Inactive, |
| 46 | + QPalette.ColorGroup.Disabled, |
| 47 | + ) |
| 48 | + |
| 49 | + pal = default_palette |
| 50 | + |
| 51 | + for role in list(QPalette.ColorRole)[:-1]: # remove last color role (NColorRoles) |
| 52 | + for group in color_groups: |
| 53 | + value: str | None = theme.value(f"{role.name}/{group.name}", None, str) # type: ignore |
| 54 | + if value is not None and QColor.isValidColor(value): |
| 55 | + pal.setColor(group, role, QColor(value)) |
| 56 | + |
| 57 | + return pal |
| 58 | + |
| 59 | + |
| 60 | +def _save_palette_to_file(file_path: str, palette: QPalette) -> None: |
| 61 | + """Save the given palette colors to a file in INI format, if the color is not default. |
| 62 | +
|
| 63 | + If no color is changed, the file won't be created or changed. |
| 64 | +
|
| 65 | + The file will be in the INI format and will have the following format: |
| 66 | +
|
| 67 | + [ColorRoleName] |
| 68 | + ColorGroupName = Color |
| 69 | +
|
| 70 | + ColorRoleName is the name of the color role (e.g. Window, Button, etc.) |
| 71 | + ColorGroupName is the name of the color group (e.g. Active, Inactive, Disabled, etc.) |
| 72 | + Color is the color value in the RgbHex (#RRGGBB) or ArgbHex (#AARRGGBB) format. |
| 73 | +
|
| 74 | + Args: |
| 75 | + file_path (str): The path to the file where the palette will be saved. |
| 76 | + palette (QPalette): The palette to be saved. |
| 77 | +
|
| 78 | + Returns: |
| 79 | + None |
| 80 | + """ |
| 81 | + theme = QSettings(file_path, QSettings.Format.IniFormat, QApplication.instance()) |
| 82 | + |
| 83 | + color_groups = ( |
| 84 | + QPalette.ColorGroup.Active, |
| 85 | + QPalette.ColorGroup.Inactive, |
| 86 | + QPalette.ColorGroup.Disabled, |
| 87 | + ) |
| 88 | + default_pal = QPalette() |
| 89 | + |
| 90 | + for role in list(QPalette.ColorRole)[:-1]: # remove last color role (NColorRoles) |
| 91 | + theme.beginGroup(role.name) |
| 92 | + for group in color_groups: |
| 93 | + if default_pal.color(group, role) != palette.color(group, role): |
| 94 | + theme.setValue(group.name, palette.color(group, role).name()) |
| 95 | + theme.endGroup() |
| 96 | + |
| 97 | + |
| 98 | +def update_palette() -> None: |
| 99 | + """Update the application palette based on the settings. |
| 100 | +
|
| 101 | + This function retrieves the dark mode value and theme file paths from the settings. |
| 102 | + It then determines the dark mode status and loads the appropriate palette from the theme files. |
| 103 | + Finally, it sets the application palette and updates the theme hooks. |
| 104 | +
|
| 105 | + Returns: |
| 106 | + None |
| 107 | + """ |
| 108 | + # region XXX: temporarily getting settings data from QApplication.property("driver") |
| 109 | + instance = QApplication.instance() |
| 110 | + if instance is None: |
| 111 | + return |
| 112 | + driver = instance.property("driver") |
| 113 | + if driver is None: |
| 114 | + return |
| 115 | + settings: QSettings = driver.settings |
| 116 | + |
| 117 | + settings.beginGroup("Appearance") |
| 118 | + dark_mode_value: str = settings.value("DarkMode", -1) # type: ignore |
| 119 | + dark_theme_file: str | None = settings.value("DarkThemeFile", None) # type: ignore |
| 120 | + light_theme_file: str | None = settings.value("LightThemeFile", None) # type: ignore |
| 121 | + settings.endGroup() |
| 122 | + # endregion |
| 123 | + |
| 124 | + # TODO: get values of following from settings. |
| 125 | + # dark_mode: bool | Literal[-1] |
| 126 | + # "True: Dark mode. False: Light mode. -1: System mode." |
| 127 | + # dark_theme_file: str | None |
| 128 | + # "Path to the dark theme file." |
| 129 | + # light_theme_file: str | None |
| 130 | + # "Path to the light theme file." |
| 131 | + |
| 132 | + true_values = ("1", "yes", "true", "on") |
| 133 | + false_values = ("0", "no", "false", "off") |
| 134 | + |
| 135 | + if dark_mode_value.lower() in ("1", "yes", "true", "on"): |
| 136 | + dark_mode = True |
| 137 | + elif dark_mode_value.lower() in ("0", "no", "false", "off"): |
| 138 | + dark_mode = False |
| 139 | + elif dark_mode_value == "-1": |
| 140 | + dark_mode = -1 |
| 141 | + else: |
| 142 | + logger.error(f"""Invalid value for DarkMode: {dark_mode_value}. Defaulting to -1. |
| 143 | + possible values: {true_values=}, {false_values=}, system=-1""") |
| 144 | + dark_mode = -1 |
| 145 | + |
| 146 | + if dark_mode == -1: |
| 147 | + dark_mode = QApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark |
| 148 | + |
| 149 | + if dark_mode: |
| 150 | + if dark_theme_file is None: |
| 151 | + palette = QPalette() # default palette |
| 152 | + else: |
| 153 | + palette = _load_palette_from_file(dark_theme_file, QPalette()) |
| 154 | + else: |
| 155 | + if light_theme_file is None: |
| 156 | + palette = QPalette() # default palette |
| 157 | + else: |
| 158 | + palette = _load_palette_from_file(light_theme_file, QPalette()) |
| 159 | + |
| 160 | + QApplication.setPalette(palette) |
| 161 | + |
| 162 | + _update_theme_hooks() |
| 163 | + |
| 164 | + |
| 165 | +def save_current_palette(theme_file: str) -> None: |
| 166 | + _save_palette_to_file(theme_file, QApplication.palette()) |
| 167 | + |
| 168 | + |
| 169 | +# the following signal emits when system theme (Dark, Light) changes (Not accent color). |
| 170 | +QApplication.styleHints().colorSchemeChanged.connect(update_palette) |
0 commit comments