# -*- coding: utf-8 -*- """ Copyright (c) 2026 KenanZhu. All rights reserved. This software is provided "as is", without any warranty of any kind. You may use, modify, and distribute this file under the terms of the MIT License. See the LICENSE file for details. """ import os import sys import qtawesome as qta from PySide6.QtCore import ( QProcess, Qt, Signal, Slot ) from PySide6.QtGui import ( QCloseEvent ) from PySide6.QtWidgets import ( QApplication, QFileDialog, QMessageBox, QStyleFactory, QWidget ) import managers.config.ConfigManager as ConfigManager from managers.log.LogManager import instance as logInstance from managers.theme.ThemeManager import( getActiveStyle, setActiveStyle, instance as themeInstance ) from gui.ALWidgetMixin import CenterOnParentMixin from gui.resources.ui.Ui_ALSettingsWidget import Ui_ALSettingsWidget from interfaces.ConfigProvider import ( CfgKey, ConfigProvider ) def _applyCustomTheme( name: str, fallback_theme: str = "system" ) -> bool: if not name: themeInstance().clearTheme(fallback_theme) return True try: themeInstance().applyTheme(name) return True except Exception as e: logInstance().getLogger("ALSettingsWidget").warning( f"无法应用自定义主题 '{name}',回退到 {fallback_theme} 外观: {e}" ) themeInstance().clearTheme(fallback_theme) return False def _themeToReadable( need_theme: str ) -> str: if need_theme == "dark": return "深色" elif need_theme == "light": return "浅色" elif need_theme == "both": return "所有" else: return "未知" def _restartApp( ): QApplication.instance().quit() QProcess.startDetached(sys.executable, sys.argv) class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget): settingsWidgetIsClosed = Signal() def __init__( self, parent=None ): super().__init__(parent) self.__cfg_mgr: ConfigProvider = ConfigManager.instance() self.__original_theme: str = "" self.__original_custom_theme: str = "" self.__original_style: str = "" self.setupUi(self) self.modifyUi() self.connectSignals() self.loadSettings() def closeEvent( self, event: QCloseEvent ): self.settingsWidgetIsClosed.emit() super().closeEvent(event) def modifyUi( self ): self.setWindowFlags(Qt.WindowType.Window) self.NavigationList.setCurrentRow(0) self.populateStyles() self.setNavigationIcons() color = QApplication.instance().palette().color( QApplication.instance().palette().ColorRole.WindowText ).name() self.BrowseQssButton.setIcon(qta.icon("fa6s.plus", color=color)) self.BrowseQssButton.setText("") self.RemoveThemeButton.setIcon(qta.icon("fa6s.minus", color=color)) self.RemoveThemeButton.setText("") self.ThemeInfoLabel.setTextFormat(Qt.TextFormat.RichText) self.ThemeInfoLabel.setStyleSheet( "border: 1px solid palette(mid);"\ "border-radius: 2px;"\ "padding: 5px;" ) def setNavigationIcons( self ): app : QApplication | None = QApplication.instance() color = app.palette().color(app.palette().ColorRole.WindowText).name() item = self.NavigationList.item(0) if item: item.setIcon(qta.icon("fa6s.palette", color=color)) def populateStyles( self ): self.StyleComboBox.clear() self.StyleComboBox.addItems(QStyleFactory.keys()) def connectSignals( self ): self.BrowseQssButton.clicked.connect(self.onImportThemeButtonClicked) self.RemoveThemeButton.clicked.connect(self.onRemoveThemeButtonClicked) self.ThemeComboBox.currentIndexChanged.connect(self.onThemeComboBoxChanged) self.ResetThemeButton.clicked.connect(self.onResetThemeButtonClicked) self.CancelButton.clicked.connect(self.onCancelButtonClicked) self.ApplyButton.clicked.connect(self.onApplyButtonClicked) self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) def loadSettings( self ): theme = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.THEME, "system") style = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.STYLE, "Fusion") custom_theme = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "") self.__original_theme = theme self.__original_custom_theme = custom_theme self.__original_style = getActiveStyle() if theme == "light": self.LightThemeRadio.setChecked(True) elif theme == "dark": self.DarkThemeRadio.setChecked(True) else: self.SystemThemeRadio.setChecked(True) index = self.StyleComboBox.findText(style) if index < 0: index = 0 self.StyleComboBox.setCurrentIndex(index) self.populateThemeList() if custom_theme: idx = self.ThemeComboBox.findData(custom_theme) if idx >= 0: self.ThemeComboBox.setCurrentIndex(idx) self.updateThemeStatus() self.updateThemeInfo() def updateThemeStatus( self ): file = self.ThemeComboBox.currentData() t = self.__theme_cache.get(file) if file else None name = t.get("name", "") if t else "" if name: self.QssStatusLabel.setText(f"当前使用 {name} 主题。") else: self.QssStatusLabel.setText("当前使用 默认 主题。") def updateThemeInfo( self ): file = self.ThemeComboBox.currentData() if not file: self.ThemeInfoLabel.setText("") return t = self.__theme_cache.get(file) if t: name = t.get("name", "未知") author = t.get("author", "未知作者") need_theme = t.get("need_theme", "both") brief = t.get("brief", "没有相关简介") self.ThemeInfoLabel.setText( f"{name} - 适用于 {_themeToReadable(need_theme)} 主题
" f"作者:{author}

" f"{brief}" ) else: self.ThemeInfoLabel.setText("") def syncRadioFromNeedTheme( self, name: str ): t = self.__theme_cache.get(name) if t: need_theme = t.get("need_theme", "both") if need_theme == "light": self.LightThemeRadio.setChecked(True) elif need_theme == "dark": self.DarkThemeRadio.setChecked(True) def collectSettings( self ): if self.LightThemeRadio.isChecked(): theme = "light" elif self.DarkThemeRadio.isChecked(): theme = "dark" else: theme = "system" style = self.StyleComboBox.currentText() custom_theme = self.ThemeComboBox.currentData() or "" if not custom_theme: custom_theme = "" return theme, style, custom_theme def saveAndApply( self ): 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) setActiveStyle(style) if not _applyCustomTheme(custom_theme, theme): self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_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) self.setNavigationIcons() self.updateThemeStatus() self.updateThemeInfo() self.__original_theme = theme self.__original_custom_theme = custom_theme if custom_theme else "" self.__original_style = getActiveStyle() def maybeRestart( self ) -> bool: reply = QMessageBox.question( self, "提示 - AutoLibrary", "界面风格已修改,需要重启程序才能生效。是否立即重启?", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes ) if reply == QMessageBox.Yes: _restartApp() return True return False def populateThemeList( self ): self.ThemeComboBox.blockSignals(True) self.ThemeComboBox.clear() self.ThemeComboBox.addItem("默认", "") self.__theme_cache = {} themes = themeInstance().listThemes() for t in themes: name = t.get("name", "") file = t.get("file", name) author = t.get("author", "") if name: self.__theme_cache[file] = t self.ThemeComboBox.addItem(name, file) self.ThemeComboBox.blockSignals(False) @Slot() def onRemoveThemeButtonClicked( self ): file = self.ThemeComboBox.currentData() if not file: QMessageBox.information( self, "提示 - AutoLibrary", "请先选择一个主题。" ) return t = self.__theme_cache.get(file) name = t.get("name", file) if t else file reply = QMessageBox.question( self, "删除主题 - AutoLibrary", f"确定要删除主题 \"{name}\" 吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply != QMessageBox.Yes: return try: themeInstance().removeTheme(file) self.populateThemeList() self.ThemeComboBox.setCurrentIndex(0) self.updateThemeStatus() self.updateThemeInfo() except Exception as e: QMessageBox.warning( self, "删除失败 - AutoLibrary", f"无法删除主题:{e}" ) @Slot() def onImportThemeButtonClicked( self ): file_path, _ = QFileDialog.getOpenFileName( self, "导入主题 - AutoLibrary", "", "主题文件 (*.altheme *.qss);;所有文件 (*)" ) if not file_path: return try: file_id = themeInstance().importTheme(file_path) self.populateThemeList() idx = self.ThemeComboBox.findData(file_id) if idx >= 0: self.ThemeComboBox.setCurrentIndex(idx) self.updateThemeStatus() self.updateThemeInfo() except Exception as e: QMessageBox.warning( self, "导入失败 - AutoLibrary", f"无法导入主题文件:{e}" ) @Slot() def onThemeComboBoxChanged( self, index: int ): self.updateThemeInfo() @Slot() def onResetThemeButtonClicked( self ): self.ThemeComboBox.blockSignals(True) if self.__original_custom_theme: idx = self.ThemeComboBox.findData(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) elif self.__original_theme == "dark": self.DarkThemeRadio.setChecked(True) else: self.SystemThemeRadio.setChecked(True) self.updateThemeStatus() self.updateThemeInfo() @Slot() def onCancelButtonClicked( self ): self.close() @Slot() def onApplyButtonClicked( self ): _, style, _ = self.collectSettings() style_changed = self.__original_style != style self.saveAndApply() if style_changed: self.maybeRestart() @Slot() def onConfirmButtonClicked( self ): self.onApplyButtonClicked() # virtually call apply button clicked self.close()