From 44dbde3355f3844a44e191ff64a2b53d22435ea9 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sun, 31 May 2026 00:46:05 +0800 Subject: [PATCH] =?UTF-8?q?fix(theme):=20=E4=B8=BB=E9=A2=98=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E4=BA=A4=E5=8F=89=E5=AE=A1=E6=9F=A5=E7=BC=BA=E9=99=B7?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 启动恢复: - _initializeAppearance 自定义主题加载失败时调用 clearTheme 回退配色方案 列表校验: - listThemes 同时校验 info.json 和 theme.qss 完整性 - 损坏的主题文件记录 LogManager 警告并跳过 - 按 (名称, 作者) 去重,同一作者同名主题仅保留一个 导入保护: - importTheme 新增 (名称, 作者) 冲突检查 - applyTheme 缺少 theme.qss 时抛出明确 ValueError 状态一致性: - saveAndApply 在 syncRadioFromNeedTheme 后重新采集 THEME 再保存 - __original_theme / __original_custom_theme 随每次 Apply 同步更新 - Reset 按钮恢复组合框到原始位置并刷新状态标签 代码质量: - 提取 _colorSchemeFor 静态方法消除 applyTheme/clearTheme 中的重复映射 - 移除未使用的 _applyTheme 死代码 - _active_style_name 默认值从 '' 改为 'Fusion' - 日志调用统一使用 LogManager - _applyCustomTheme 异常时通过 LogManager 记录详细错误 Co-Authored-By: Claude Opus 4.8 --- src/boot/AppInitializer.py | 17 +-- src/gui/ALSettingsWidget.py | 82 ++++++++------ src/managers/theme/ThemeManager.py | 176 +++++++++++++---------------- 3 files changed, 134 insertions(+), 141 deletions(-) diff --git a/src/boot/AppInitializer.py b/src/boot/AppInitializer.py index c90cbbf..039a163 100644 --- a/src/boot/AppInitializer.py +++ b/src/boot/AppInitializer.py @@ -12,15 +12,14 @@ import os from PySide6.QtCore import QStandardPaths, QDir from PySide6.QtWidgets import QApplication -from gui.ALSettingsWidget import ( - _setActiveStyleName, - _applyTheme, -) from interfaces.ConfigProvider import CfgKey from managers.config.ConfigManager import instance as configInstance from managers.driver.WebDriverManager import instance as webdriverInstance from managers.log.LogManager import instance as logInstance -from managers.theme.ThemeManager import instance as themeInstance +from managers.theme.ThemeManager import( + setActiveStyle, + instance as themeInstance +) def _initializeLogManager( @@ -82,14 +81,16 @@ def _initializeAppearance( saved_theme = cfg.get(CfgKey.GLOBAL.APPEARANCE.THEME, "system") saved_custom_theme = cfg.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "") app.setStyle(saved_style) - _setActiveStyleName(saved_style) + setActiveStyle(saved_style) logger = logInstance().getLogger("AppInitializer") if saved_custom_theme: try: themeInstance().applyTheme(saved_custom_theme) except Exception: - logger.warning("无法应用自定义主题 '%s',回退到默认外观", saved_custom_theme) - _applyTheme(saved_theme) + logger.warning("无法应用自定义主题 '%s',回退到默认外观", saved_custom_theme) + themeInstance().clearTheme(saved_theme) + return + themeInstance().clearTheme(saved_theme) def initializeApp( ) -> bool: diff --git a/src/gui/ALSettingsWidget.py b/src/gui/ALSettingsWidget.py index 705e4fd..ab11c84 100644 --- a/src/gui/ALSettingsWidget.py +++ b/src/gui/ALSettingsWidget.py @@ -31,8 +31,9 @@ from PySide6.QtWidgets import ( ) import managers.config.ConfigManager as ConfigManager -from managers.theme.ThemeManager import ( - ThemeManager, +from managers.log.LogManager import instance as logInstance +from managers.theme.ThemeManager import( + getActiveStyle, instance as themeInstance ) @@ -43,31 +44,34 @@ from interfaces.ConfigProvider import ( ) -_active_style_name = "" - - -def _setActiveStyleName( - name: str +def _applyCustomTheme( + name: str, + fallback_theme: str = "system" ): - global _active_style_name - _active_style_name = name - -def _applyTheme( - theme: str -): - - global _active_style_name - app : QApplication | None = QApplication.instance() - if not app: + if not name: + themeInstance().clearTheme(fallback_theme) return - if theme == "dark": - app.styleHints().setColorScheme(Qt.ColorScheme.Dark) - elif theme == "light": - app.styleHints().setColorScheme(Qt.ColorScheme.Light) + try: + themeInstance().applyTheme(name) + except Exception as e: + logInstance().getLogger("ALSettingsWidget").warning( + f"无法应用自定义主题 '{name}',回退到 {fallback_theme} 外观: {e}" + ) + themeInstance().clearTheme(fallback_theme) + +def _themeToReadable( + need_theme: str +) -> str: + + if need_theme == "dark": + return "深色" + elif need_theme == "light": + return "浅色" + elif need_theme == "both": + return "所有" else: - app.styleHints().setColorScheme(Qt.ColorScheme.Unknown) - app.setStyle(QStyleFactory.create(_active_style_name)) + return "未知" def _restartApp( ): @@ -75,6 +79,7 @@ def _restartApp( QApplication.instance().quit() QProcess.startDetached(sys.executable, sys.argv) + class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): settingsWidgetIsClosed = Signal() @@ -126,12 +131,6 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): self.StyleComboBox.clear() self.StyleComboBox.addItems(QStyleFactory.keys()) - def currentStyleKey( - self - ) -> str: - - return _active_style_name - def connectSignals( self ): @@ -181,7 +180,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): custom_theme = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "") self.__original_theme = theme self.__original_custom_theme = custom_theme - self.__original_style = self.currentStyleKey() + self.__original_style = getActiveStyle() if theme == "light": self.LightThemeRadio.setChecked(True) elif theme == "dark": @@ -224,7 +223,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): need_theme = t.get("need_theme", "both") brief = t.get("brief", "没有相关简介") self.ThemeInfoLabel.setText( - f"{name} - 适用于 {ThemeManager.themeToReadable(need_theme)} 主题
" + f"{name} - 适用于 {_themeToReadable(need_theme)} 主题
" f"作者:{author}

" f"{brief}" ) @@ -267,15 +266,18 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): theme, style, custom_theme = self.collectSettings() self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.STYLE, style) self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, custom_theme) - themeInstance().applyThemeOrClear(custom_theme, theme) + _applyCustomTheme(custom_theme, theme) self.syncRadioFromNeedTheme(custom_theme) + # Re-read theme after syncRadioFromNeedTheme — the radio may have + # changed to match the custom theme's need_theme theme, _, _ = self.collectSettings() self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme) - _applyTheme(theme) self.setNavigationIcons() self.updateThemeStatus() self.updateThemeInfo() - self.__original_style = self.currentStyleKey() + self.__original_theme = theme + self.__original_custom_theme = custom_theme if custom_theme else "" + self.__original_style = getActiveStyle() def maybeRestart( self @@ -351,7 +353,14 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): ): self.ThemeComboBox.blockSignals(True) - self.ThemeComboBox.setCurrentIndex(0) + if self.__original_custom_theme: + idx = self.ThemeComboBox.findText(self.__original_custom_theme) + if idx >= 0: + self.ThemeComboBox.setCurrentIndex(idx) + else: + self.ThemeComboBox.setCurrentIndex(0) + else: + self.ThemeComboBox.setCurrentIndex(0) self.ThemeComboBox.blockSignals(False) if self.__original_theme == "light": self.LightThemeRadio.setChecked(True) @@ -359,7 +368,8 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): self.DarkThemeRadio.setChecked(True) else: self.SystemThemeRadio.setChecked(True) - themeInstance().clearTheme(self.__original_theme) + _applyCustomTheme(self.__original_custom_theme, self.__original_theme) + self.updateThemeStatus() self.updateThemeInfo() @Slot() diff --git a/src/managers/theme/ThemeManager.py b/src/managers/theme/ThemeManager.py index 8984e4a..412e402 100644 --- a/src/managers/theme/ThemeManager.py +++ b/src/managers/theme/ThemeManager.py @@ -14,9 +14,13 @@ import threading import zipfile from PySide6.QtCore import Qt -from PySide6.QtWidgets import QApplication +from PySide6.QtWidgets import ( + QApplication, + QStyleFactory +) from managers.config.ConfigManager import instance as configInstance +from managers.log.LogManager import instance as logInstance from utils.ThemeUtils import ( packTheme, readThemeInfo, @@ -25,6 +29,22 @@ from utils.ThemeUtils import ( ) +_active_style_name = "Fusion" + + +def setActiveStyle( + style_name: str +): + + global _active_style_name + _active_style_name = style_name + +def getActiveStyle( +) -> str: + + return _active_style_name + + class ThemeManager: """ Theme manager class. @@ -46,6 +66,21 @@ class ThemeManager: self.__current_theme_name = "" os.makedirs(self.__themes_dir, exist_ok=True) + @staticmethod + def _colorSchemeFor( + theme: str + ) -> Qt.ColorScheme: + """ + Map a theme identifier to the corresponding Qt color scheme. + """ + + if theme == "dark": + return Qt.ColorScheme.Dark + elif theme == "light": + return Qt.ColorScheme.Light + else: + return Qt.ColorScheme.Unknown + def themesDir( self ) -> str: @@ -101,6 +136,14 @@ class ThemeManager: dest_path = os.path.join(self.__themes_dir, safe_name + ".altheme") if os.path.exists(dest_path): raise ValueError(f"主题 '{safe_name}' 已存在") + # Check for name collision with existing themes by the same author + new_author = info.get("author", "") + for existing in self.listThemes(): + if (existing.get("name", "") == safe_name + and existing.get("author", "") == new_author): + raise ValueError( + f"主题名称 '{safe_name}' (作者 '{new_author}') 已存在" + ) shutil.copy2(source_path, dest_path) return safe_name else: @@ -120,6 +163,7 @@ class ThemeManager: """ themes = [] + seen_keys = set() if not os.path.isdir(self.__themes_dir): return themes for filename in sorted(os.listdir(self.__themes_dir)): @@ -127,9 +171,27 @@ class ThemeManager: filepath = os.path.join(self.__themes_dir, filename) try: info = readThemeInfo(filepath) + with zipfile.ZipFile(filepath, "r") as zf: + if "theme.qss" not in zf.namelist(): + raise ValueError("缺少 theme.qss") + name = info.get("name", "") + author = info.get("author", "") + key = (name, author) + if key in seen_keys: + logInstance().getLogger("ThemeManager").warning( + f"主题名称 '{name}' (作者 '{author}') 重复 (文件 '{filename}') 已跳过" + ) + continue + seen_keys.add(key) themes.append(info) - except Exception: - pass + except Exception as e: + logInstance().getLogger("ThemeManager").warning( + f"无法读取主题文件 '{filename}',已跳过: {e}" + ) + else: + logInstance().getLogger("ThemeManager").warning( + f"未知文件类型 '{filename}',已跳过" + ) return themes def removeTheme( @@ -152,7 +214,7 @@ class ThemeManager: os.remove(filepath) if self.__current_theme_name == name: self.__current_theme_name = "" - self._clearQss() + self.clearTheme("system") def applyTheme( self, @@ -186,13 +248,17 @@ class ThemeManager: app = QApplication.instance() if app: app.setStyleSheet(qss) + else: + raise ValueError( + f"主题 '{name}' 的 .altheme 文件中缺少 theme.qss" + ) app = QApplication.instance() if app: need_theme = info.get("need_theme", "both") - if need_theme == "dark": - app.styleHints().setColorScheme(Qt.ColorScheme.Dark) - elif need_theme == "light": - app.styleHints().setColorScheme(Qt.ColorScheme.Light) + app.styleHints().setColorScheme( + ThemeManager._colorSchemeFor(need_theme) + ) + app.setStyle(QStyleFactory.create(_active_style_name)) self.__current_theme_name = name def clearTheme( @@ -207,99 +273,15 @@ class ThemeManager: ("light", "dark", or "system"). """ - app = QApplication.instance() - if app: - app.setStyleSheet("") - self._applyColorScheme(theme) - - def applyThemeOrClear( - self, - name: str, - fallback_theme: str = "system" - ): - """ - Apply a custom theme by name, or clear to fallback if empty. - - Args: - name (str): The theme name to apply, or empty to clear. - fallback_theme (str): Color scheme to use if name is empty - or if the theme fails to apply. - """ - - if not name: - self.clearTheme(fallback_theme) - return - try: - self.applyTheme(name) - except Exception: - self.clearTheme(fallback_theme) - - def _applyColorScheme( - self, - theme: str - ): - """ - Set the Qt application color scheme. - - Args: - theme (str): "dark", "light", or any other value for system default. - """ - app = QApplication.instance() if not app: return - if theme == "dark": - app.styleHints().setColorScheme(Qt.ColorScheme.Dark) - elif theme == "light": - app.styleHints().setColorScheme(Qt.ColorScheme.Light) - else: - app.styleHints().setColorScheme(Qt.ColorScheme.Unknown) + app.setStyleSheet("") + app.styleHints().setColorScheme( + ThemeManager._colorSchemeFor(theme) + ) + app.setStyle(QStyleFactory.create(_active_style_name)) - @staticmethod - def themeToReadable( - need_theme: str - ) -> str: - """ - Convert a need_theme code to human-readable Chinese text. - - Args: - need_theme (str): "dark", "light", "both", or other. - - Returns: - str: Readable Chinese label. - """ - - if need_theme == "dark": - return "深色" - elif need_theme == "light": - return "浅色" - elif need_theme == "both": - return "所有" - else: - return "未知" - - def currentThemeName( - self - ) -> str: - """ - Get the name of the currently active theme. - - Returns: - str: Current theme name, or empty string if none is active. - """ - - return self.__current_theme_name - - def _clearQss( - self - ): - """ - Clear the current QSS stylesheet from the application. - """ - - app = QApplication.instance() - if app: - app.setStyleSheet("") # ThemeManager singleton instance. _theme_manager_instance = None