1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-18 23:43:02 +08:00

feat(theme): 引入 .altheme 主题文件格式与主题管理系统

- 新增 .altheme 文件格式(zip 压缩包包含 info.json 与 theme.qss)
- 新增 utils/ThemeUtils.py:主题文件打包/解包/读取工具函数
- 新增 managers/theme/ThemeManager:主题目录管理器,支持导入/列举/删除/应用
- 新增 LightLake 浅色主题 QSS 文件
- 新增 CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME 配置键
- 配置模板新增 custom_theme 字段
- ALSettingsWidget 接入 ThemeManager,替换裸 QSS 路径模式
- AppInitializer 启动时恢复自定义主题状态
- Zip Slip 防护与线程安全保护
This commit is contained in:
2026-05-30 21:01:18 +08:00
parent c0b6e0899c
commit 35253dadbb
8 changed files with 913 additions and 46 deletions
+96 -44
View File
@@ -24,6 +24,7 @@ from PySide6.QtGui import (
)
from PySide6.QtWidgets import (
QApplication,
QComboBox,
QFileDialog,
QMessageBox,
QStyleFactory,
@@ -31,6 +32,7 @@ from PySide6.QtWidgets import (
)
import managers.config.ConfigManager as ConfigManager
from managers.theme.ThemeManager import instance as themeInstance
from gui.resources.ui.Ui_ALSettingsWidget import Ui_ALSettingsWidget
from interfaces.ConfigProvider import (
@@ -56,6 +58,18 @@ def _clearQss(
if app:
app.setStyleSheet("")
def _applyThemeByName(
name: str
):
if not name:
_clearQss()
return
try:
themeInstance().applyTheme(name)
except Exception:
_clearQss()
def _loadQss(
file_path: str
) -> str:
@@ -129,6 +143,15 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self.NavigationList.setCurrentRow(0)
self.populateStyles()
self.setNavigationIcons()
self.QssPathEdit.hide()
self.ApplyQssButton.hide()
self.ResetQssButton.setText("重置主题")
self.CustomQssHintLabel.setText("选择一个主题,或导入新的主题文件:")
self.ThemeComboBox = QComboBox(self.CustomQssGroupBox)
self.ThemeComboBox.setObjectName("ThemeComboBox")
self.ThemeComboBox.setMinimumSize(160, 25)
self.QssPathLayout.insertWidget(0, self.ThemeComboBox)
self.ThemeStatusLabel = self.QssStatusLabel
def setNavigationIcons(
self
@@ -157,8 +180,8 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self
):
self.BrowseQssButton.clicked.connect(self.onBrowseQssButtonClicked)
self.ApplyQssButton.clicked.connect(self.onApplyQssButtonClicked)
self.BrowseQssButton.clicked.connect(self.onImportThemeButtonClicked)
self.ThemeComboBox.currentTextChanged.connect(self.onThemeComboBoxChanged)
self.ResetQssButton.clicked.connect(self.onResetQssButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
self.ApplyButton.clicked.connect(self.onApplyButtonClicked)
@@ -199,7 +222,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
theme = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.THEME, "system")
style = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.STYLE, "Fusion")
custom_qss = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_QSS, "")
custom_theme = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
self.__original_style = self.currentStyleKey()
if theme == "light":
self.LightThemeRadio.setChecked(True)
@@ -211,19 +234,22 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
if index < 0:
index = 0
self.StyleComboBox.setCurrentIndex(index)
self.QssPathEdit.setText(custom_qss)
self.updateQssStatus(custom_qss)
self.populateThemeList()
if custom_theme:
idx = self.ThemeComboBox.findText(custom_theme)
if idx >= 0:
self.ThemeComboBox.setCurrentIndex(idx)
self.updateThemeStatus()
def updateQssStatus(
self,
qss_path: str
def updateThemeStatus(
self
):
if qss_path and os.path.isfile(qss_path):
filename = os.path.basename(qss_path)
self.QssStatusLabel.setText(f"已加载自定义样式文件{filename}")
name = self.ThemeComboBox.currentText()
if name:
self.ThemeStatusLabel.setText(f"已加载主题{name}")
else:
self.QssStatusLabel.setText("当前使用程序默认外观。")
self.ThemeStatusLabel.setText("当前使用程序默认外观。")
def collectSettings(
self
@@ -236,21 +262,21 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
else:
theme = "system"
style = self.StyleComboBox.currentText()
custom_qss = self.QssPathEdit.text().strip()
return theme, style, custom_qss
custom_theme = self.ThemeComboBox.currentText()
return theme, style, custom_theme
def saveAndApply(
self
):
theme, style, custom_qss = self.collectSettings()
theme, style, custom_theme = self.collectSettings()
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme)
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.STYLE, style)
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_QSS, custom_qss)
_applyQss(custom_qss)
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, custom_theme)
_applyThemeByName(custom_theme)
_applyTheme(theme)
self.setNavigationIcons()
self.updateQssStatus(custom_qss)
self.updateThemeStatus()
self.__original_style = self.currentStyleKey()
def maybeRestart(
@@ -269,49 +295,75 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
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", f"未知主题 {len(self.__theme_cache)+1}")
if name:
self.__theme_cache[name] = t
self.ThemeComboBox.addItem(name)
self.ThemeComboBox.blockSignals(False)
@Slot()
def onBrowseQssButtonClicked(
def onImportThemeButtonClicked(
self
):
file_path, _ = QFileDialog.getOpenFileName(
self,
"选择 QSS 样式文件 - AutoLibrary",
self.QssPathEdit.text(),
"QSS 样式表文件 (*.qss);;所有文件 (*)"
"导入主题 - AutoLibrary",
"",
"主题文件 (*.altheme *.qss);;所有文件 (*)"
)
if file_path:
self.QssPathEdit.setText(file_path)
if not file_path:
return
try:
name = themeInstance().importTheme(file_path)
self.populateThemeList()
idx = self.ThemeComboBox.findText(name)
if idx >= 0:
self.ThemeComboBox.setCurrentIndex(idx)
_applyThemeByName(name)
self.updateThemeStatus()
except Exception as e:
QMessageBox.warning(
self,
"导入失败 - AutoLibrary",
f"无法导入主题文件:{e}"
)
@Slot()
def onApplyQssButtonClicked(
def onThemeComboBoxChanged(
self
):
qss_path = self.QssPathEdit.text().strip()
if not qss_path:
QMessageBox.warning(
self,
"提示 - AutoLibrary",
"请先选择或输入 QSS 样式表文件路径。"
)
return
if not os.path.isfile(qss_path):
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"未找到指定的样式文件:\n{qss_path}"
)
return
_applyQss(qss_path)
self.updateQssStatus(qss_path)
name = self.ThemeComboBox.currentText()
if name:
_applyThemeByName(name)
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)
else:
_clearQss()
self.updateThemeStatus()
@Slot()
def onResetQssButtonClicked(
self
):
self.QssPathEdit.clear()
self.ThemeComboBox.setCurrentIndex(0)
_clearQss()
if self.LightThemeRadio.isChecked():
_applyTheme("light")
@@ -320,7 +372,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
else:
_applyTheme("system")
self.setNavigationIcons()
self.updateQssStatus("")
self.updateThemeStatus()
@Slot()
def onCancelButtonClicked(
+421
View File
@@ -0,0 +1,421 @@
/*
* 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.
*
*
* AutoLibrary Official Style Theme : LightLake
*/
/* ---- Global ---- */
QMainWindow::separator {
background-color: #c0cdda;
width: 1px;
height: 1px;
}
/* ---- Menu Bar ---- */
QMenuBar {
background-color: #dce4ee;
border-bottom: 1px solid #c0cdda;
padding: 2px 6px;
color: #1a2740;
}
QMenuBar::item {
padding: 4px 10px;
border-radius: 4px;
}
QMenuBar::item:selected {
background-color: #d5dde8;
}
QMenu {
background-color: #ffffff;
border-style: solid;
border-color: #d0d8e4;
border-width: 1px;
padding: 4px;
border-radius: 6px;
}
QMenu::item {
padding: 5px 15px 5px 10px;
border-radius: 4px;
}
QMenu::item:selected {
background-color: #0ea58a;
color: #ffffff;
}
QMenu::separator {
height: 1px;
background-color: #d0d8e4;
margin: 4px 8px;
}
/* ---- Button ---- */
QPushButton {
border-style: solid;
border-color: #c0cdda;
border-width: 1px;
border-radius: 5px;
color: #1a2740;
padding: 4px 12px;
background-color: #d5dde8;
}
QPushButton:hover {
background-color: #c8d4e2;
border-color: #90a4c4;
}
QPushButton:pressed {
background-color: #e2e8f0;
border-color: #0ea58a;
}
QPushButton:disabled {
background-color: #e8ecf2;
color: #98a8c0;
border-color: #d5dde8;
}
QPushButton[default="true"] {
background-color: #0ea58a;
color: #ffffff;
border-color: #0ea58a;
}
QPushButton[default="true"]:hover {
background-color: #14c7a4;
}
/* ---- Input ---- */
QLineEdit,
QPlainTextEdit,
QTextEdit,
QSpinBox,
QDoubleSpinBox,
QDateEdit,
QTimeEdit {
background-color: #ffffff;
border-style: solid;
border-color: #c0cdda;
border-width: 1px;
border-radius: 5px;
padding: 4px 8px;
color: #1a2740;
selection-background-color: #0ea58a;
selection-color: #ffffff;
}
QLineEdit:focus,
QPlainTextEdit:focus,
QTextEdit:focus,
QSpinBox:focus,
QDoubleSpinBox:focus,
QDateEdit:focus,
QTimeEdit:focus {
border-color: #0ea58a;
}
QPlainTextEdit,
QTextEdit {
background-color: #ffffff;
}
/* ---- Combo Box ---- */
QComboBox {
background-color: #d5dde8;
border-style: solid;
border-color: #c0cdda;
border-width: 1px;
border-radius: 5px;
padding: 4px 10px;
color: #1a2740;
}
QComboBox:hover {
border-color: #90a4c4;
}
QComboBox:focus {
border-color: #0ea58a;
}
QComboBox::drop-down {
subcontrol-origin: padding;
subcontrol-position: top right;
width: 24px;
border-left: 1px solid #c0cdda;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
QComboBox::down-arrow {
image: none;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 6px solid #6a7898;
margin-right: 6px;
}
QComboBox QAbstractItemView {
background-color: #ffffff;
border-style: solid;
border-color: #d0d8e4;
border-width: 1px;
border-radius: 4px;
selection-background-color: #0ea58a;
selection-color: #ffffff;
outline: none;
}
/* ---- Check Box / Radio Button ---- */
QCheckBox,
QRadioButton {
spacing: 8px;
color: #1a2740;
}
QCheckBox::indicator,
QRadioButton::indicator {
border-style: solid;
border-color: #90a4c4;
border-width: 2px;
border-radius: 3px;
background-color: #ffffff;
}
QCheckBox::indicator:hover,
QRadioButton::indicator:hover {
border-color: #0ea58a;
}
QCheckBox::indicator:checked {
background-color: #0ea58a;
border-color: #0ea58a;
}
QRadioButton::indicator {
border-radius: 10px;
}
QRadioButton::indicator:checked {
background-color: #0ea58a;
border-color: #0ea58a;
}
QCheckBox::indicator:disabled,
QRadioButton::indicator:disabled {
border-color: #c0cdda;
background-color: #e8ecf2;
}
/* ---- Group Box ---- */
QGroupBox {
border-style: solid;
border-color: #c0cdda;
border-width: 1px;
border-radius: 6px;
margin-top: 12px;
padding-top: 14px;
color: #1a2740;
font-weight: bold;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 12px;
padding: 0 6px;
color: #4a6080;
}
/* ---- Tab ---- */
QTabWidget::pane {
border-style: solid;
border-color: #c0cdda;
border-width: 1px;
border-radius: 5px;
background-color: #f0f4f8;
top: -1px;
}
QTabBar::tab {
background-color: #e0e6ee;
border-style: solid;
border-color: #c0cdda;
border-width: 1px;
border-bottom: none;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
padding: 6px 16px;
margin-right: 2px;
color: #6a7898;
}
QTabBar::tab:selected {
background-color: #f0f4f8;
color: #0ea58a;
border-bottom: 2px solid #0ea58a;
}
QTabBar::tab:hover:!selected {
background-color: #d5dde8;
color: #1a2740;
}
/* ---- List / Tree ---- */
QListWidget,
QTreeWidget,
QTableWidget {
background-color: #ffffff;
border-style: solid;
border-color: #c0cdda;
border-width: 1px;
border-radius: 5px;
outline: none;
color: #1a2740;
alternate-background-color: #f4f7fa;
}
QListWidget::item,
QTreeWidget::item,
QTableWidget::item {
padding: 5px 10px;
border: none;
}
QListWidget::item:selected,
QTreeWidget::item:selected,
QTableWidget::item:selected {
background-color: #0ea58a;
color: #ffffff;
}
QListWidget::item:hover:!selected,
QTreeWidget::item:hover:!selected {
background-color: #e2e8f0;
}
QHeaderView::section {
background-color: #dce4ee;
border: none;
border-right: 1px solid #c0cdda;
border-bottom: 1px solid #c0cdda;
padding: 6px 10px;
color: #4a6080;
font-weight: bold;
}
/* ---- Scroll Bar ---- */
QScrollBar:vertical {
background-color: #eef2f6;
width: 10px;
border-radius: 5px;
}
QScrollBar::handle:vertical {
background-color: #a0b4cc;
min-height: 30px;
border-radius: 5px;
}
QScrollBar::handle:vertical:hover {
background-color: #8098b8;
}
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
height: 0;
}
QScrollBar:horizontal {
background-color: #eef2f6;
height: 10px;
border-radius: 5px;
}
QScrollBar::handle:horizontal {
background-color: #a0b4cc;
min-width: 30px;
border-radius: 5px;
}
QScrollBar::handle:horizontal:hover {
background-color: #8098b8;
}
QScrollBar::add-line:horizontal,
QScrollBar::sub-line:horizontal {
width: 0;
}
/* ---- Progress Bar ---- */
QProgressBar {
background-color: #ffffff;
border-style: solid;
border-color: #c0cdda;
border-width: 1px;
border-radius: 5px;
height: 10px;
text-align: center;
color: #1a2740;
}
QProgressBar::chunk {
background-color: #0ea58a;
border-radius: 4px;
}
/* ---- Slider ---- */
QSlider::groove:horizontal {
background-color: #d5dde8;
height: 6px;
border-radius: 3px;
}
QSlider::handle:horizontal {
background-color: #0ea58a;
width: 16px;
height: 16px;
margin: -5px 0;
border-radius: 8px;
}
QSlider::sub-page:horizontal {
background-color: #0ea58a;
border-radius: 3px;
}
/* ---- Tool Tip ---- */
QToolTip {
background-color: #d5dde8;
border-style: solid;
border-color: #0ea58a;
border-width: 1px;
border-radius: 4px;
padding: 4px 8px;
color: #1a2740;
}
/* ---- Status Bar ---- */
QStatusBar {
background-color: #e8ecf2;
border-top: 1px solid #c0cdda;
color: #6a7898;
}
/* ---- Splitter ---- */
QSplitter::handle {
background-color: #c0cdda;
margin: 1px;
}
QSplitter::handle:horizontal {
width: 2px;
}
QSplitter::handle:vertical {
height: 2px;
}
/* ---- Dialog ---- */
QDialog {
background-color: #f0f4f8;
}
/* ---- Date / Time Editor Drop-down ---- */
QDateEdit::drop-down,
QTimeEdit::drop-down {
subcontrol-origin: padding;
subcontrol-position: top right;
width: 24px;
border-left: 1px solid #c0cdda;
}
QCalendarWidget {
background-color: #ffffff;
border-style: solid;
border-color: #c0cdda;
border-width: 1px;
border-radius: 6px;
}
QCalendarWidget QToolButton {
color: #1a2740;
border-radius: 4px;
padding: 4px 8px;
}
QCalendarWidget QToolButton:hover {
background-color: #d5dde8;
}
QCalendarWidget QMenu {
background-color: #ffffff;
}
/* ---- Frame ---- */
QFrame[frameShape="4"], /* HLine */
QFrame[frameShape="5"] /* VLine */ {
background-color: #c0cdda;
}