mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-17 23:13:03 +08:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9175371dc | |||
| 8e1b28f3fe | |||
| 57f1cfb3f2 | |||
| 007b4dc2ef | |||
| 67f297b434 | |||
| 86f0761eed | |||
| 79e5b43498 | |||
| b56d2c203e | |||
| 44dbde3355 | |||
| 62f8ec3d91 | |||
| 2d77cbec79 | |||
| 10d731518a | |||
| 9fdb6f7652 | |||
| ef903ee817 | |||
| d6e8eef8c8 | |||
| e893752c25 | |||
| 1cfd7382be | |||
| 1d9e41ab86 | |||
| 645f07b4d2 | |||
| 732f104c5c | |||
| a2bc1881bc | |||
| c1004ed2bc | |||
| 38489191f5 | |||
| 35253dadbb | |||
| c0b6e0899c | |||
| 9c1772b186 | |||
| 05b93799d4 |
Binary file not shown.
@@ -24,7 +24,6 @@ def main():
|
||||
translator = QTranslator()
|
||||
if translator.load(":/res/translators/qtbase_zh_CN.ts"):
|
||||
app.installTranslator(translator)
|
||||
app.setStyle("Fusion")
|
||||
app.setApplicationName("AutoLibrary")
|
||||
if not initializeApp():
|
||||
sys.exit(-1)
|
||||
|
||||
@@ -10,10 +10,16 @@ See the LICENSE file for details.
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import QStandardPaths, QDir
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from managers.log.LogManager import instance as logInstance
|
||||
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(
|
||||
setActiveStyle,
|
||||
instance as themeInstance
|
||||
)
|
||||
|
||||
|
||||
def _initializeLogManager(
|
||||
@@ -64,13 +70,35 @@ def _initializeWebDriverManager(
|
||||
webdriverInstance(driver_dir)
|
||||
return True
|
||||
|
||||
def _initializeAppearance(
|
||||
):
|
||||
|
||||
app = QApplication.instance()
|
||||
if not app:
|
||||
return
|
||||
cfg = configInstance()
|
||||
saved_style = cfg.get(CfgKey.GLOBAL.APPEARANCE.STYLE, "Fusion")
|
||||
saved_theme = cfg.get(CfgKey.GLOBAL.APPEARANCE.THEME, "system")
|
||||
saved_custom_theme = cfg.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
|
||||
app.setStyle(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)
|
||||
themeInstance().clearTheme(saved_theme)
|
||||
return
|
||||
themeInstance().clearTheme(saved_theme)
|
||||
|
||||
def initializeApp(
|
||||
) -> bool:
|
||||
"""
|
||||
Initialize the application components
|
||||
|
||||
Order:
|
||||
LogManager -> ConfigManager -> WebDriverManager
|
||||
LogManager -> ConfigManager -> WebDriverManager -> Appearance
|
||||
"""
|
||||
|
||||
if not _initializeLogManager():
|
||||
@@ -79,4 +107,5 @@ def initializeApp(
|
||||
return False
|
||||
if not _initializeWebDriverManager():
|
||||
return False
|
||||
_initializeAppearance()
|
||||
return True
|
||||
|
||||
@@ -9,6 +9,8 @@ See the LICENSE file for details.
|
||||
"""
|
||||
from copy import deepcopy
|
||||
|
||||
import qtawesome as qta
|
||||
|
||||
from PySide6.QtCore import (
|
||||
QDate,
|
||||
QSize,
|
||||
@@ -20,7 +22,6 @@ from PySide6.QtCore import (
|
||||
from PySide6.QtGui import (
|
||||
QColor,
|
||||
QFont,
|
||||
QIcon,
|
||||
QSyntaxHighlighter,
|
||||
QTextCharFormat,
|
||||
)
|
||||
@@ -210,23 +211,27 @@ class ALAutoScriptEditDialog(QDialog):
|
||||
Layout.setSpacing(3)
|
||||
Layout.setContentsMargins(3, 3, 3, 3)
|
||||
ToolbarLayout = QHBoxLayout()
|
||||
self.ZoomInBtn = QPushButton("+")
|
||||
self.ZoomInBtn = QPushButton("")
|
||||
self.ZoomInBtn.setIcon(qta.icon("fa6s.plus", color=self._iconColor()))
|
||||
self.ZoomInBtn.setIconSize(QSize(14, 14))
|
||||
self.ZoomInBtn.setFixedSize(25, 25)
|
||||
self.ZoomOutBtn = QPushButton("-")
|
||||
self.ZoomOutBtn = QPushButton("")
|
||||
self.ZoomOutBtn.setIcon(qta.icon("fa6s.minus", color=self._iconColor()))
|
||||
self.ZoomOutBtn.setIconSize(QSize(14, 14))
|
||||
self.ZoomOutBtn.setFixedSize(25, 25)
|
||||
self.ZoomResetBtn = QPushButton("")
|
||||
self.ZoomResetBtn.setIcon(QIcon(":/res/icons/Reset.svg"))
|
||||
self.ZoomResetBtn.setIconSize(QSize(20, 20))
|
||||
self.ZoomResetBtn.setIcon(qta.icon("fa6s.rotate-left", color=self._iconColor()))
|
||||
self.ZoomResetBtn.setIconSize(QSize(14, 14))
|
||||
self.ZoomResetBtn.setFixedSize(25, 25)
|
||||
self.ZoomResetBtn.setToolTip("重置缩放")
|
||||
self.ZoomLabel = QLabel(f"{self._fontSize}px")
|
||||
self.ZoomLabel.setFixedHeight(25)
|
||||
self.OrchBtn = QPushButton("编排")
|
||||
self.OrchBtn.setFixedHeight(25)
|
||||
self.OrchBtn.setFixedSize(80, 25)
|
||||
self.OrchBtn.setToolTip("可视化生成 AutoScript 代码并插入到光标位置")
|
||||
ToolbarLayout.addWidget(self.OrchBtn)
|
||||
self.DebugBtn = QPushButton("▶ 调试运行")
|
||||
self.DebugBtn.setFixedHeight(25)
|
||||
self.DebugBtn.setFixedSize(80, 25)
|
||||
self.DebugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化")
|
||||
ToolbarLayout.addWidget(self.DebugBtn)
|
||||
Sep = QFrame()
|
||||
@@ -240,8 +245,8 @@ class ALAutoScriptEditDialog(QDialog):
|
||||
ToolbarLayout.addWidget(self.ZoomLabel)
|
||||
ToolbarLayout.addStretch()
|
||||
self.CopyBtn = QPushButton("")
|
||||
self.CopyBtn.setIcon(QIcon(":/res/icons/Copy.svg"))
|
||||
self.CopyBtn.setIconSize(QSize(20, 20))
|
||||
self.CopyBtn.setIcon(qta.icon("fa6s.copy", color=self._iconColor()))
|
||||
self.CopyBtn.setIconSize(QSize(14, 14))
|
||||
self.CopyBtn.setFixedSize(25, 25)
|
||||
self.CopyBtn.setToolTip("复制脚本")
|
||||
ToolbarLayout.addWidget(self.CopyBtn)
|
||||
@@ -264,7 +269,9 @@ class ALAutoScriptEditDialog(QDialog):
|
||||
QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
self.BtnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
|
||||
self.BtnBox.button(QDialogButtonBox.StandardButton.Ok).setFixedSize(80, 25)
|
||||
self.BtnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
|
||||
self.BtnBox.button(QDialogButtonBox.StandardButton.Cancel).setFixedSize(80, 25)
|
||||
Layout.addWidget(self.BtnBox)
|
||||
|
||||
def createButtonPanel(
|
||||
@@ -537,6 +544,14 @@ class ALAutoScriptEditDialog(QDialog):
|
||||
else:
|
||||
widget.setText(str(value))
|
||||
|
||||
def _iconColor(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
return QApplication.instance().palette().color(
|
||||
QApplication.instance().palette().ColorRole.WindowText
|
||||
).name()
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
+31
-1
@@ -33,6 +33,7 @@ from PySide6.QtWidgets import (
|
||||
from base.MsgBase import MsgBase
|
||||
from gui.ALAboutDialog import ALAboutDialog
|
||||
from gui.ALConfigWidget import ALConfigWidget
|
||||
from gui.ALSettingsWidget import ALSettingsWidget
|
||||
from gui.ALMainWorkers import (
|
||||
AutoLibWorker,
|
||||
TimerTaskWorker
|
||||
@@ -60,6 +61,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
self.__config_paths = ConfigUtils.getAutomationConfigPaths()
|
||||
self.__alTimerTaskManageWidget = None
|
||||
self.__alConfigWidget = None
|
||||
self.__alSettingsWidget = None
|
||||
self.__auto_lib_thread = None
|
||||
self.__current_timer_task_thread = None
|
||||
self.__is_running_timer_task = False
|
||||
@@ -81,6 +83,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
|
||||
self.ManualAction.triggered.connect(self.onManualActionTriggered)
|
||||
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
|
||||
self.SettingsAction.triggered.connect(self.onSettingsActionTriggered)
|
||||
|
||||
# initialize timer task widget, but not show it
|
||||
try:
|
||||
@@ -125,7 +128,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
return
|
||||
self.TrayIcon = QSystemTrayIcon(self.Icon, self)
|
||||
self.TrayIcon.setToolTip("AutoLibrary")
|
||||
|
||||
self.TrayMenu = QMenu()
|
||||
self.TrayMenu.addAction("显示主窗口", self.showNormal)
|
||||
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskManageWidgetButtonClicked)
|
||||
@@ -190,6 +192,9 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
if self.__alConfigWidget:
|
||||
self.__alConfigWidget.close()
|
||||
# the config widget is already deleted in the 'self.onConfigWidgetClosed'
|
||||
if self.__alSettingsWidget:
|
||||
self.__alSettingsWidget.close()
|
||||
# the settings widget is already deleted in the 'self.onSettingsWidgetClosed'
|
||||
self._showLog("主窗口关闭")
|
||||
QMainWindow.closeEvent(self, event)
|
||||
|
||||
@@ -302,6 +307,31 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
self.setControlButtons(True, None, None)
|
||||
self._showLog("配置窗口已关闭,配置文件路径已更新")
|
||||
|
||||
@Slot()
|
||||
def onSettingsWidgetClosed(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__alSettingsWidget:
|
||||
self.__alSettingsWidget.settingsWidgetIsClosed.disconnect(self.onSettingsWidgetClosed)
|
||||
self.__alSettingsWidget.deleteLater()
|
||||
self.__alSettingsWidget = None
|
||||
self.SettingsAction.setEnabled(True)
|
||||
|
||||
@Slot()
|
||||
def onSettingsActionTriggered(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__alSettingsWidget is None:
|
||||
self.__alSettingsWidget = ALSettingsWidget(self)
|
||||
self.__alSettingsWidget.settingsWidgetIsClosed.connect(self.onSettingsWidgetClosed)
|
||||
self.__alSettingsWidget.show()
|
||||
self.__alSettingsWidget.raise_()
|
||||
self.__alSettingsWidget.activateWindow()
|
||||
self.SettingsAction.setEnabled(False)
|
||||
self._showLog("打开全局设置窗口")
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsReady(
|
||||
self,
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
# -*- 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,
|
||||
QShowEvent
|
||||
)
|
||||
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.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(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 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 showEvent(
|
||||
self,
|
||||
event: QShowEvent
|
||||
):
|
||||
|
||||
result = super().showEvent(event)
|
||||
screen_rect = self.screen().geometry()
|
||||
target_pos = self.parent().geometry().center()
|
||||
target_pos.setX(target_pos.x() - self.width()//2)
|
||||
target_pos.setY(target_pos.y() - self.height()//2)
|
||||
if target_pos.x() < 0:
|
||||
target_pos.setX(0)
|
||||
if target_pos.x() + self.width() > screen_rect.width():
|
||||
target_pos.setX(screen_rect.width() - self.width())
|
||||
if target_pos.y() < 0:
|
||||
target_pos.setY(0)
|
||||
if target_pos.y() + self.height() > screen_rect.height():
|
||||
target_pos.setY(screen_rect.height() - self.height())
|
||||
self.move(target_pos)
|
||||
return result
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
self.settingsWidgetIsClosed.emit()
|
||||
super().closeEvent(event)
|
||||
|
||||
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"<b>{name}</b> - 适用于 <i>{_themeToReadable(need_theme)}</i> 主题<br>"
|
||||
f"作者:{author}<br><br>"
|
||||
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)
|
||||
_applyCustomTheme(self.__original_custom_theme, self.__original_theme)
|
||||
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
|
||||
):
|
||||
|
||||
_, style, _ = self.collectSettings()
|
||||
style_changed = self.__original_style != style
|
||||
self.saveAndApply()
|
||||
if style_changed:
|
||||
self.maybeRestart()
|
||||
self.close()
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* 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 Theme : BlueForest
|
||||
*/
|
||||
|
||||
/* ---- Global ---- */
|
||||
QMainWindow::separator {
|
||||
background-color: #1c2840;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
/* ---- Menu Bar ---- */
|
||||
QMenuBar {
|
||||
background-color: #0f1628;
|
||||
border-bottom: 1px solid #1c2840;
|
||||
padding: 2px 5px;
|
||||
color: #d0daf0;
|
||||
}
|
||||
QMenuBar::item {
|
||||
padding: 2px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QMenuBar::item:selected {
|
||||
background-color: #1c2840;
|
||||
}
|
||||
QMenu {
|
||||
background-color: #162038;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QMenu::item {
|
||||
padding: 5px 15px 5px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QMenu::item:selected {
|
||||
background-color: #2dd4bf;
|
||||
color: #0f1119;
|
||||
}
|
||||
QMenu::separator {
|
||||
height: 1px;
|
||||
background-color: #253250;
|
||||
margin: 4px 8px;
|
||||
}
|
||||
|
||||
/* ---- Button ---- */
|
||||
QPushButton {
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
color: #d0daf0;
|
||||
padding: 4px 12px;
|
||||
background-color: #1c2840;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #243458;
|
||||
border-color: #334478;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #162038;
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #162038;
|
||||
color: #5568a0;
|
||||
border-color: #1c2840;
|
||||
}
|
||||
QPushButton[default="true"] {
|
||||
background-color: #2dd4bf;
|
||||
color: #0f1119;
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QPushButton[default="true"]:hover {
|
||||
background-color: #3de0cc;
|
||||
}
|
||||
|
||||
/* ---- Input ---- */
|
||||
QLineEdit,
|
||||
QPlainTextEdit,
|
||||
QTextEdit,
|
||||
QSpinBox,
|
||||
QDoubleSpinBox,
|
||||
QDateEdit,
|
||||
QTimeEdit {
|
||||
background-color: #0a1020;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
padding: 4px 8px;
|
||||
color: #d0daf0;
|
||||
selection-background-color: #2dd4bf;
|
||||
selection-color: #0f1119;
|
||||
}
|
||||
QLineEdit:focus,
|
||||
QPlainTextEdit:focus,
|
||||
QTextEdit:focus,
|
||||
QSpinBox:focus,
|
||||
QDoubleSpinBox:focus,
|
||||
QDateEdit:focus,
|
||||
QTimeEdit:focus {
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QPlainTextEdit,
|
||||
QTextEdit {
|
||||
background-color: #0a1020;
|
||||
}
|
||||
QLineEdit:disabled,
|
||||
QPlainTextEdit:disabled,
|
||||
QTextEdit:disabled,
|
||||
QSpinBox:disabled,
|
||||
QDoubleSpinBox:disabled,
|
||||
QDateEdit:disabled,
|
||||
QTimeEdit:disabled {
|
||||
background-color: #162038;
|
||||
color: #5568a0;
|
||||
border-color: #1c2840;
|
||||
}
|
||||
|
||||
/* ---- Spin Button Arrows ---- */
|
||||
QSpinBox::up-button,
|
||||
QDoubleSpinBox::up-button,
|
||||
QDateEdit::up-button,
|
||||
QTimeEdit::up-button {
|
||||
subcontrol-origin: border;
|
||||
subcontrol-position: top right;
|
||||
width: 10px;
|
||||
border-left: 1px solid #253250;
|
||||
border-bottom: 1px solid #253250;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
QSpinBox::up-button:hover,
|
||||
QDoubleSpinBox::up-button:hover,
|
||||
QDateEdit::up-button:hover,
|
||||
QTimeEdit::up-button:hover {
|
||||
background-color: #1c2840;
|
||||
}
|
||||
QSpinBox::up-arrow,
|
||||
QDoubleSpinBox::up-arrow,
|
||||
QDateEdit::up-arrow,
|
||||
QTimeEdit::up-arrow {
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 5px solid #7888b8;
|
||||
margin-top: 2px;
|
||||
}
|
||||
QSpinBox::down-button,
|
||||
QDoubleSpinBox::down-button,
|
||||
QDateEdit::down-button,
|
||||
QTimeEdit::down-button {
|
||||
width: 10px;
|
||||
subcontrol-origin: border;
|
||||
subcontrol-position: bottom right;
|
||||
border-left: 1px solid #253250;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
QSpinBox::down-button:hover,
|
||||
QDoubleSpinBox::down-button:hover,
|
||||
QDateEdit::down-button:hover,
|
||||
QTimeEdit::down-button:hover {
|
||||
background-color: #1c2840;
|
||||
}
|
||||
QSpinBox::down-arrow,
|
||||
QDoubleSpinBox::down-arrow,
|
||||
QDateEdit::down-arrow,
|
||||
QTimeEdit::down-arrow {
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #7888b8;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
QSpinBox::up-button:disabled,
|
||||
QDoubleSpinBox::up-button:disabled,
|
||||
QDateEdit::up-button:disabled,
|
||||
QTimeEdit::up-button:disabled,
|
||||
QSpinBox::down-button:disabled,
|
||||
QDoubleSpinBox::down-button:disabled,
|
||||
QDateEdit::down-button:disabled,
|
||||
QTimeEdit::down-button:disabled {
|
||||
background-color: #162038;
|
||||
}
|
||||
QSpinBox::up-arrow:disabled,
|
||||
QDoubleSpinBox::up-arrow:disabled,
|
||||
QDateEdit::up-arrow:disabled,
|
||||
QTimeEdit::up-arrow:disabled {
|
||||
border-bottom-color: #5568a0;
|
||||
}
|
||||
QSpinBox::down-arrow:disabled,
|
||||
QDoubleSpinBox::down-arrow:disabled,
|
||||
QDateEdit::down-arrow:disabled,
|
||||
QTimeEdit::down-arrow:disabled {
|
||||
border-top-color: #5568a0;
|
||||
}
|
||||
|
||||
/* ---- Combo Box ---- */
|
||||
QComboBox {
|
||||
background-color: #1c2840;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
padding: 4px 10px;
|
||||
color: #d0daf0;
|
||||
}
|
||||
QComboBox:hover {
|
||||
border-color: #334478;
|
||||
}
|
||||
QComboBox:focus {
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QComboBox::drop-down {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: top right;
|
||||
width: 24px;
|
||||
border-left: 1px solid #253250;
|
||||
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 #7888b8;
|
||||
margin-right: 6px;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #162038;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
selection-background-color: #2dd4bf;
|
||||
selection-color: #0f1119;
|
||||
outline: none;
|
||||
}
|
||||
QComboBox:disabled {
|
||||
background-color: #162038;
|
||||
color: #5568a0;
|
||||
border-color: #1c2840;
|
||||
}
|
||||
|
||||
/* ---- Check Box / Radio Button ---- */
|
||||
QCheckBox,
|
||||
QRadioButton {
|
||||
spacing: 5px;
|
||||
color: #d0daf0;
|
||||
}
|
||||
QCheckBox::indicator,
|
||||
QRadioButton::indicator {
|
||||
border-style: solid;
|
||||
border-color: #334478;
|
||||
border-width: 2px;
|
||||
background-color: #0a1020;
|
||||
}
|
||||
QCheckBox::indicator {
|
||||
border-radius: 3px;
|
||||
}
|
||||
QRadioButton::indicator {
|
||||
border-radius: 7px;
|
||||
}
|
||||
QCheckBox::indicator:hover,
|
||||
QRadioButton::indicator:hover {
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
background-color: #2dd4bf;
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QRadioButton::indicator:checked {
|
||||
background-color: #2dd4bf;
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QCheckBox::indicator:disabled,
|
||||
QRadioButton::indicator:disabled {
|
||||
border-color: #253250;
|
||||
background-color: #162038;
|
||||
}
|
||||
QCheckBox::indicator:checked:hover,
|
||||
QRadioButton::indicator:checked:hover {
|
||||
border-color: #a0f0e8;
|
||||
}
|
||||
|
||||
/* Tree / List / Table Widget CheckBox Indicator */
|
||||
QTreeWidget::indicator,
|
||||
QListWidget::indicator,
|
||||
QTableWidget::indicator {
|
||||
border: 2px solid #5568a0;
|
||||
border-radius: 3px;
|
||||
background-color: #162038;
|
||||
}
|
||||
QTreeWidget::indicator:hover,
|
||||
QListWidget::indicator:hover,
|
||||
QTableWidget::indicator:hover {
|
||||
border-color: #a0f0e8;
|
||||
}
|
||||
QTreeWidget::indicator:checked,
|
||||
QListWidget::indicator:checked,
|
||||
QTableWidget::indicator:checked {
|
||||
background-color: #2dd4bf;
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
QTreeWidget::indicator:checked:hover,
|
||||
QListWidget::indicator:checked:hover,
|
||||
QTableWidget::indicator:checked:hover {
|
||||
border-color: #a0f0e8;
|
||||
}
|
||||
QTreeWidget::indicator:disabled,
|
||||
QListWidget::indicator:disabled,
|
||||
QTableWidget::indicator:disabled {
|
||||
background-color: #1c2840;
|
||||
border-color: #334478;
|
||||
}
|
||||
QTreeWidget::indicator:indeterminate,
|
||||
QListWidget::indicator:indeterminate,
|
||||
QTableWidget::indicator:indeterminate {
|
||||
background-color: #2dd4bf;
|
||||
border-color: #a0f0e8;
|
||||
}
|
||||
|
||||
/* ---- Group Box ---- */
|
||||
QGroupBox {
|
||||
margin-top: 5px;
|
||||
padding-top: 15px;
|
||||
color: #b4c2f5;
|
||||
font-weight: bold;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* ---- Tab ---- */
|
||||
QTabWidget::pane {
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
background-color: #0f1a2e;
|
||||
top: -1px;
|
||||
}
|
||||
QTabBar::tab {
|
||||
background-color: #162038;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-bottom: none;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
padding: 6px 16px;
|
||||
margin-right: 2px;
|
||||
color: #7888b8;
|
||||
}
|
||||
QTabBar::tab:selected {
|
||||
background-color: #0f1a2e;
|
||||
color: #2dd4bf;
|
||||
border-bottom: 2px solid #2dd4bf;
|
||||
}
|
||||
|
||||
/* ---- List / Tree ---- */
|
||||
QListWidget,
|
||||
QTreeWidget,
|
||||
QTableWidget {
|
||||
background-color: #0a1020;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
color: #d0daf0;
|
||||
alternate-background-color: #101c30;
|
||||
}
|
||||
QListWidget::item,
|
||||
QTreeWidget::item,
|
||||
QTableWidget::item {
|
||||
padding: 5px 5px;
|
||||
}
|
||||
QHeaderView::section {
|
||||
background-color: #0f1628;
|
||||
border-right: 1px solid #253250;
|
||||
border-bottom: 1px solid #253250;
|
||||
padding: 5px 10px;
|
||||
color: #8b9ad0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* ---- Scroll Bar ---- */
|
||||
QScrollBar:vertical {
|
||||
background-color: #0f1a2e;
|
||||
width: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
background-color: #334478;
|
||||
min-height: 30px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background-color: #5568a0;
|
||||
}
|
||||
QScrollBar::add-line:vertical,
|
||||
QScrollBar::sub-line:vertical {
|
||||
height: 0;
|
||||
}
|
||||
QScrollBar:horizontal {
|
||||
background-color: #0f1a2e;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QScrollBar::handle:horizontal {
|
||||
background-color: #334478;
|
||||
min-width: 30px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background-color: #5568a0;
|
||||
}
|
||||
QScrollBar::add-line:horizontal,
|
||||
QScrollBar::sub-line:horizontal {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
/* ---- Progress Bar ---- */
|
||||
QProgressBar {
|
||||
background-color: #0a1020;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
height: 10px;
|
||||
text-align: center;
|
||||
color: #d0daf0;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #2dd4bf;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ---- Slider ---- */
|
||||
QSlider::groove:horizontal {
|
||||
background-color: #1c2840;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QSlider::handle:horizontal {
|
||||
background-color: #2dd4bf;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: -5px 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
QSlider::sub-page:horizontal {
|
||||
background-color: #2dd4bf;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QSlider::handle:horizontal:disabled {
|
||||
background-color: #5568a0;
|
||||
}
|
||||
QSlider::sub-page:horizontal:disabled {
|
||||
background-color: #5568a0;
|
||||
}
|
||||
|
||||
/* ---- Tool Tip ---- */
|
||||
QToolTip {
|
||||
background-color: #1c2840;
|
||||
border-style: solid;
|
||||
border-color: #2dd4bf;
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
color: #d0daf0;
|
||||
}
|
||||
|
||||
/* ---- Status Bar ---- */
|
||||
QStatusBar {
|
||||
background-color: #0f1628;
|
||||
border-top: 1px solid #1c2840;
|
||||
color: #7888b8;
|
||||
}
|
||||
|
||||
/* ---- Splitter ---- */
|
||||
QSplitter::handle {
|
||||
background-color: #253250;
|
||||
margin: 1px;
|
||||
}
|
||||
QSplitter::handle:horizontal {
|
||||
width: 2px;
|
||||
}
|
||||
QSplitter::handle:vertical {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
/* ---- Dialog ---- */
|
||||
QDialog {
|
||||
background-color: #0f1a2e;
|
||||
}
|
||||
|
||||
/* ---- Date / Time Editor Drop-down ---- */
|
||||
QDateEdit::drop-down,
|
||||
QTimeEdit::drop-down {
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: top right;
|
||||
width: 24px;
|
||||
border-left: 1px solid #253250;
|
||||
}
|
||||
QCalendarWidget {
|
||||
background-color: #162038;
|
||||
border-style: solid;
|
||||
border-color: #253250;
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
QCalendarWidget QToolButton {
|
||||
color: #d0daf0;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
QCalendarWidget QToolButton:hover {
|
||||
background-color: #1c2840;
|
||||
}
|
||||
QCalendarWidget QMenu {
|
||||
background-color: #162038;
|
||||
}
|
||||
|
||||
/* ---- Frame ---- */
|
||||
QFrame[frameShape="4"], /* HLine */
|
||||
QFrame[frameShape="5"] /* VLine */ {
|
||||
background-color: #253250;
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* 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 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 5px;
|
||||
color: #1a2740;
|
||||
}
|
||||
QMenuBar::item {
|
||||
padding: 2px 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;
|
||||
}
|
||||
QLineEdit:disabled,
|
||||
QPlainTextEdit:disabled,
|
||||
QTextEdit:disabled,
|
||||
QSpinBox:disabled,
|
||||
QDoubleSpinBox:disabled,
|
||||
QDateEdit:disabled,
|
||||
QTimeEdit:disabled {
|
||||
background-color: #e8ecf2;
|
||||
color: #98a8c0;
|
||||
border-color: #d5dde8;
|
||||
}
|
||||
|
||||
/* ---- Spin Button Arrows ---- */
|
||||
QSpinBox::up-button,
|
||||
QDoubleSpinBox::up-button,
|
||||
QDateEdit::up-button,
|
||||
QTimeEdit::up-button {
|
||||
subcontrol-origin: border;
|
||||
subcontrol-position: top right;
|
||||
width: 10px;
|
||||
border-left: 1px solid #c0cdda;
|
||||
border-bottom: 1px solid #c0cdda;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
QSpinBox::up-button:hover,
|
||||
QDoubleSpinBox::up-button:hover,
|
||||
QDateEdit::up-button:hover,
|
||||
QTimeEdit::up-button:hover {
|
||||
background-color: #d5dde8;
|
||||
}
|
||||
QSpinBox::up-arrow,
|
||||
QDoubleSpinBox::up-arrow,
|
||||
QDateEdit::up-arrow,
|
||||
QTimeEdit::up-arrow {
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 5px solid #6a7898;
|
||||
margin-top: 2px;
|
||||
}
|
||||
QSpinBox::down-button,
|
||||
QDoubleSpinBox::down-button,
|
||||
QDateEdit::down-button,
|
||||
QTimeEdit::down-button {
|
||||
width: 10px;
|
||||
subcontrol-origin: border;
|
||||
subcontrol-position: bottom right;
|
||||
border-left: 1px solid #c0cdda;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
QSpinBox::down-button:hover,
|
||||
QDoubleSpinBox::down-button:hover,
|
||||
QDateEdit::down-button:hover,
|
||||
QTimeEdit::down-button:hover {
|
||||
background-color: #d5dde8;
|
||||
}
|
||||
QSpinBox::down-arrow,
|
||||
QDoubleSpinBox::down-arrow,
|
||||
QDateEdit::down-arrow,
|
||||
QTimeEdit::down-arrow {
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #6a7898;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
QSpinBox::up-button:disabled,
|
||||
QDoubleSpinBox::up-button:disabled,
|
||||
QDateEdit::up-button:disabled,
|
||||
QTimeEdit::up-button:disabled,
|
||||
QSpinBox::down-button:disabled,
|
||||
QDoubleSpinBox::down-button:disabled,
|
||||
QDateEdit::down-button:disabled,
|
||||
QTimeEdit::down-button:disabled {
|
||||
background-color: #e8ecf2;
|
||||
}
|
||||
QSpinBox::up-arrow:disabled,
|
||||
QDoubleSpinBox::up-arrow:disabled,
|
||||
QDateEdit::up-arrow:disabled,
|
||||
QTimeEdit::up-arrow:disabled {
|
||||
border-bottom-color: #98a8c0;
|
||||
}
|
||||
QSpinBox::down-arrow:disabled,
|
||||
QDoubleSpinBox::down-arrow:disabled,
|
||||
QDateEdit::down-arrow:disabled,
|
||||
QTimeEdit::down-arrow:disabled {
|
||||
border-top-color: #98a8c0;
|
||||
}
|
||||
|
||||
/* ---- 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;
|
||||
}
|
||||
QComboBox:disabled {
|
||||
background-color: #e8ecf2;
|
||||
color: #98a8c0;
|
||||
border-color: #d5dde8;
|
||||
}
|
||||
|
||||
/* ---- Check Box / Radio Button ---- */
|
||||
QCheckBox,
|
||||
QRadioButton {
|
||||
spacing: 5px;
|
||||
color: #1a2740;
|
||||
}
|
||||
QCheckBox::indicator,
|
||||
QRadioButton::indicator {
|
||||
border-style: solid;
|
||||
border-color: #90a4c4;
|
||||
border-width: 2px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
QCheckBox::indicator {
|
||||
border-radius: 3px;
|
||||
}
|
||||
QRadioButton::indicator {
|
||||
border-radius: 7px;
|
||||
}
|
||||
QCheckBox::indicator:hover,
|
||||
QRadioButton::indicator:hover {
|
||||
border-color: #0ea58a;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
background-color: #0ea58a;
|
||||
border-color: #0ea58a;
|
||||
}
|
||||
QRadioButton::indicator:checked {
|
||||
background-color: #0ea58a;
|
||||
border-color: #0ea58a;
|
||||
}
|
||||
QCheckBox::indicator:disabled,
|
||||
QRadioButton::indicator:disabled {
|
||||
border-color: #c0cdda;
|
||||
background-color: #e8ecf2;
|
||||
}
|
||||
QCheckBox::indicator:checked:hover,
|
||||
QRadioButton::indicator:checked:hover {
|
||||
border-color: #14c7a4;
|
||||
}
|
||||
|
||||
/* Tree / List / Table Widget CheckBox Indicator */
|
||||
QTreeWidget::indicator,
|
||||
QListWidget::indicator,
|
||||
QTableWidget::indicator {
|
||||
border: 2px solid #a0b4cc;
|
||||
border-radius: 3px;
|
||||
background-color: #e8ecf2;
|
||||
}
|
||||
QTreeWidget::indicator:hover,
|
||||
QListWidget::indicator:hover,
|
||||
QTableWidget::indicator:hover {
|
||||
border-color: #14c7a4;
|
||||
}
|
||||
QTreeWidget::indicator:checked,
|
||||
QListWidget::indicator:checked,
|
||||
QTableWidget::indicator:checked {
|
||||
background-color: #0ea58a;
|
||||
border-color: #0ea58a;
|
||||
}
|
||||
QTreeWidget::indicator:checked:hover,
|
||||
QListWidget::indicator:checked:hover,
|
||||
QTableWidget::indicator:checked:hover {
|
||||
border-color: #14c7a4;
|
||||
}
|
||||
QTreeWidget::indicator:disabled,
|
||||
QListWidget::indicator:disabled,
|
||||
QTableWidget::indicator:disabled {
|
||||
background-color: #d5dde8;
|
||||
border-color: #c0cdda;
|
||||
}
|
||||
QTreeWidget::indicator:indeterminate,
|
||||
QListWidget::indicator:indeterminate,
|
||||
QTableWidget::indicator:indeterminate {
|
||||
background-color: #0ea58a;
|
||||
border-color: #14c7a4;
|
||||
}
|
||||
|
||||
/* ---- Group Box ---- */
|
||||
QGroupBox {
|
||||
margin-top: 5px;
|
||||
padding-top: 15px;
|
||||
color: #4a6080;
|
||||
font-weight: bold;
|
||||
border-style: solid;
|
||||
border-color: #c0cdda;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* ---- 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;
|
||||
}
|
||||
|
||||
/* ---- 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 5px;
|
||||
}
|
||||
QHeaderView::section {
|
||||
background-color: #dce4ee;
|
||||
border-right: 1px solid #c0cdda;
|
||||
border-bottom: 1px solid #c0cdda;
|
||||
padding: 5px 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;
|
||||
}
|
||||
QSlider::handle:horizontal:disabled {
|
||||
background-color: #98a8c0;
|
||||
}
|
||||
QSlider::sub-page:horizontal:disabled {
|
||||
background-color: #98a8c0;
|
||||
}
|
||||
|
||||
/* ---- 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;
|
||||
}
|
||||
@@ -1956,13 +1956,13 @@
|
||||
<widget class="QPushButton" name="ExportConfigButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<width>120</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<width>120</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
|
||||
@@ -281,6 +281,12 @@ font: 700 9pt;</string>
|
||||
<property name="nativeMenuBar">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QMenu" name="ToolsMenu">
|
||||
<property name="title">
|
||||
<string>工具</string>
|
||||
</property>
|
||||
<addaction name="SettingsAction"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="HelpMenu">
|
||||
<property name="mouseTracking">
|
||||
<bool>true</bool>
|
||||
@@ -291,6 +297,7 @@ font: 700 9pt;</string>
|
||||
<addaction name="ManualAction"/>
|
||||
<addaction name="AboutAction"/>
|
||||
</widget>
|
||||
<addaction name="ToolsMenu"/>
|
||||
<addaction name="HelpMenu"/>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="StatusBar">
|
||||
@@ -308,6 +315,11 @@ font: 700 9pt;</string>
|
||||
<string>关于</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="SettingsAction">
|
||||
<property name="text">
|
||||
<string>全局设置</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
|
||||
@@ -0,0 +1,555 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ALSettingsWidget</class>
|
||||
<widget class="QWidget" name="ALSettingsWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>520</width>
|
||||
<height>420</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>480</width>
|
||||
<height>420</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>580</width>
|
||||
<height>420</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>全局设置 - AutoLibrary</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="ALSettingsWidgetLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="ContentLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QListWidget" name="NavigationList">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::StrongFocus</enum>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="currentRow">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>外观</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset theme="preferences-desktop-color"/>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QScrollArea" name="AppearanceScrollArea">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="AppearancePageContent">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>450</width>
|
||||
<height>380</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="AppearancePageLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="AppearanceGroupBox">
|
||||
<property name="title">
|
||||
<string>主题模式</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="AppearanceGroupBoxLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="LightThemeRadio">
|
||||
<property name="text">
|
||||
<string>浅色</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="DarkThemeRadio">
|
||||
<property name="text">
|
||||
<string>深色</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="SystemThemeRadio">
|
||||
<property name="text">
|
||||
<string>跟随系统</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="InterfaceGroupBox">
|
||||
<property name="title">
|
||||
<string>界面风格</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="InterfaceGroupBoxLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="StyleSelectLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="StyleSelectLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>应用程序样式:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="StyleComboBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>160</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="StyleHintLabel">
|
||||
<property name="text">
|
||||
<string>更改样式将在下次启动应用程序时生效。</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="CustomQssGroupBox">
|
||||
<property name="title">
|
||||
<string>自定义外观</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="CustomQssGroupBoxLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="CustomQssHintLabel">
|
||||
<property name="text">
|
||||
<string>选择一个主题,或导入新的主题文件:</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="QssPathLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QComboBox" name="ThemeComboBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>160</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="QssPathEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>选择或输入 QSS 样式表文件路径...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="BrowseQssButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>+</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="RemoveThemeButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>-</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="ThemeInfoLabel">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>60</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::TextFormat::RichText</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignTop</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="QssActionLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ApplyQssButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>应用样式</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ResetThemeButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>重置主题</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="QssActionSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="QssStatusLabel">
|
||||
<property name="text">
|
||||
<string>当前使用程序 默认 外观。</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="AppearancePageSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="ButtonLayout">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="ButtonLeftSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="CancelButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>取消</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ApplyButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>应用</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ConfirmButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>确认</string>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -7,13 +7,13 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>350</width>
|
||||
<height>400</height>
|
||||
<height>500</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>350</width>
|
||||
<height>460</height>
|
||||
<height>500</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
|
||||
@@ -66,6 +66,12 @@ class CfgKey:
|
||||
CURRENT = ConfigPath(ConfigType.GLOBAL, "automation.user_path.current")
|
||||
PATHS = ConfigPath(ConfigType.GLOBAL, "automation.user_path.paths")
|
||||
|
||||
class APPEARANCE:
|
||||
ROOT = ConfigPath(ConfigType.GLOBAL, "appearance")
|
||||
THEME = ConfigPath(ConfigType.GLOBAL, "appearance.theme")
|
||||
STYLE = ConfigPath(ConfigType.GLOBAL, "appearance.style")
|
||||
CUSTOM_THEME = ConfigPath(ConfigType.GLOBAL, "appearance.custom_theme")
|
||||
|
||||
class TIMERTASK:
|
||||
ROOT = ConfigPath(ConfigType.TIMERTASK, "")
|
||||
TIMER_TASKS = ConfigPath(ConfigType.TIMERTASK, "timer_tasks")
|
||||
|
||||
@@ -54,6 +54,11 @@ class ConfigTemplate:
|
||||
"current": 0,
|
||||
"paths": []
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
"theme": "system",
|
||||
"style": "Fusion",
|
||||
"custom_theme": ""
|
||||
}
|
||||
}
|
||||
case ConfigType.BULLETIN:
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
# -*- 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 shutil
|
||||
import threading
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QStyleFactory
|
||||
)
|
||||
|
||||
from interfaces.ConfigProvider import CfgKey
|
||||
from managers.config.ConfigManager import instance as configInstance
|
||||
from managers.log.LogManager import instance as logInstance
|
||||
from utils.ThemeUtils import (
|
||||
readThemeQss,
|
||||
validateTheme,
|
||||
wrapQssToAtheme
|
||||
)
|
||||
|
||||
|
||||
_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.
|
||||
|
||||
Manages the themes storage directory, providing import,
|
||||
list, remove, and apply operations for .altheme theme files.
|
||||
|
||||
Args:
|
||||
themes_dir (str): Path to the themes storage directory.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
themes_dir: str
|
||||
):
|
||||
|
||||
self.__themes_dir = os.path.abspath(themes_dir)
|
||||
self.__lock = threading.Lock()
|
||||
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:
|
||||
"""
|
||||
Get the themes directory path.
|
||||
|
||||
Returns:
|
||||
str: The absolute path to the themes storage directory.
|
||||
"""
|
||||
|
||||
return self.__themes_dir
|
||||
|
||||
def _resolveDestPath(
|
||||
self,
|
||||
theme_name: str,
|
||||
author: str
|
||||
) -> str:
|
||||
"""
|
||||
Resolve the destination path for an imported theme.
|
||||
|
||||
If the default {name}.altheme path does not exist, use it directly.
|
||||
If it exists and has a different author, use {name}_{author}.altheme.
|
||||
If it exists and has the same author, raise ValueError.
|
||||
|
||||
Args:
|
||||
theme_name (str): Sanitised theme name.
|
||||
author (str): Theme author string.
|
||||
|
||||
Returns:
|
||||
str: The resolved destination file path.
|
||||
|
||||
Raises:
|
||||
ValueError: If a theme with the same name and author already exists.
|
||||
"""
|
||||
|
||||
default_path = os.path.join(self.__themes_dir, theme_name + ".altheme")
|
||||
if not os.path.exists(default_path):
|
||||
return default_path
|
||||
try:
|
||||
existing_info = validateTheme(default_path)
|
||||
existing_author = existing_info.get("author", "")
|
||||
except Exception:
|
||||
self._removeThemeFile(theme_name) # caller holds the lock
|
||||
raise ValueError(
|
||||
f"主题 '{theme_name}' 已存在但无法通过验证, 已清理该主题文件"
|
||||
)
|
||||
if existing_author == author:
|
||||
raise ValueError(
|
||||
f"主题名称 '{theme_name}' (作者 '{author}') 已存在"
|
||||
)
|
||||
safe_author = os.path.basename(author) if author else "未知作者"
|
||||
alt_path = os.path.join(
|
||||
self.__themes_dir, f"{theme_name}_{safe_author}.altheme"
|
||||
)
|
||||
if os.path.exists(alt_path):
|
||||
raise ValueError(
|
||||
f"主题名称 '{theme_name}' (作者 '{author}') 已存在"
|
||||
)
|
||||
return alt_path
|
||||
|
||||
def importTheme(
|
||||
self,
|
||||
source_path: str
|
||||
) -> str:
|
||||
"""
|
||||
Import a theme file into the themes directory.
|
||||
|
||||
Supports .altheme (zip archive) and bare .qss files.
|
||||
Bare .qss files are automatically wrapped into .altheme format.
|
||||
For .altheme files, validates that theme.qss exists in the archive
|
||||
and sanitises the theme name to prevent path traversal.
|
||||
|
||||
Args:
|
||||
source_path (str): Path to the .altheme or .qss file.
|
||||
|
||||
Returns:
|
||||
str: The imported theme name.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If source_path does not exist.
|
||||
ValueError: If the file type is unsupported or the .altheme is invalid.
|
||||
"""
|
||||
|
||||
if not os.path.isfile(source_path):
|
||||
raise FileNotFoundError(source_path)
|
||||
ext = os.path.splitext(source_path)[1].lower()
|
||||
with self.__lock:
|
||||
if ext == ".qss":
|
||||
name = os.path.splitext(os.path.basename(source_path))[0]
|
||||
dest_path = self._resolveDestPath(name, "未知作者")
|
||||
wrapQssToAtheme(source_path, dest_path, "both")
|
||||
return os.path.splitext(os.path.basename(dest_path))[0]
|
||||
elif ext == ".altheme":
|
||||
info = validateTheme(source_path)
|
||||
name = info.get("name", os.path.splitext(os.path.basename(source_path))[0])
|
||||
safe_name = os.path.basename(name)
|
||||
new_author = info.get("author", "")
|
||||
dest_path = self._resolveDestPath(safe_name, new_author)
|
||||
shutil.copy2(source_path, dest_path)
|
||||
return os.path.splitext(os.path.basename(dest_path))[0]
|
||||
else:
|
||||
raise ValueError(f"不支持的文件类型: {ext}")
|
||||
|
||||
def listThemes(
|
||||
self
|
||||
) -> list:
|
||||
"""
|
||||
List all available themes in the themes directory.
|
||||
|
||||
Scans the themes directory for .altheme files and reads
|
||||
their info.json metadata.
|
||||
|
||||
Returns:
|
||||
list[dict]: A list of theme info dictionaries.
|
||||
"""
|
||||
|
||||
themes = []
|
||||
seen_keys = set()
|
||||
if not os.path.isdir(self.__themes_dir):
|
||||
return themes
|
||||
for filename in sorted(os.listdir(self.__themes_dir)):
|
||||
if filename.endswith(".altheme"):
|
||||
filepath = os.path.join(self.__themes_dir, filename)
|
||||
try:
|
||||
info = validateTheme(filepath, check_qss=False) # skip QSS read for list scan
|
||||
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)
|
||||
info["file"] = os.path.splitext(filename)[0]
|
||||
themes.append(info)
|
||||
except Exception as e:
|
||||
logInstance().getLogger("ThemeManager").warning(
|
||||
f"无法读取主题文件 '{filename}',已跳过: {e}"
|
||||
)
|
||||
else:
|
||||
logInstance().getLogger("ThemeManager").warning(
|
||||
f"未知文件类型 '{filename}',已跳过"
|
||||
)
|
||||
return themes
|
||||
|
||||
def _removeThemeFile(
|
||||
self,
|
||||
name: str
|
||||
):
|
||||
"""
|
||||
Remove a theme file without locking.
|
||||
|
||||
The caller must hold self.__lock before invoking this method.
|
||||
"""
|
||||
|
||||
filepath = os.path.join(self.__themes_dir, name + ".altheme")
|
||||
if os.path.isfile(filepath):
|
||||
os.remove(filepath)
|
||||
if self.__current_theme_name == name:
|
||||
self.__current_theme_name = ""
|
||||
saved_theme = configInstance().get(
|
||||
CfgKey.GLOBAL.APPEARANCE.THEME, "system"
|
||||
)
|
||||
self.clearTheme(saved_theme)
|
||||
|
||||
def removeTheme(
|
||||
self,
|
||||
name: str
|
||||
):
|
||||
"""
|
||||
Remove a theme by name.
|
||||
|
||||
If the removed theme is currently active, clears the QSS
|
||||
stylesheet from the application.
|
||||
|
||||
Args:
|
||||
name (str): The theme name to remove.
|
||||
"""
|
||||
|
||||
with self.__lock:
|
||||
self._removeThemeFile(name)
|
||||
|
||||
def applyTheme(
|
||||
self,
|
||||
name: str
|
||||
):
|
||||
"""
|
||||
Apply a theme by name.
|
||||
|
||||
Extracts the QSS from the .altheme file, applies it to
|
||||
QApplication, and sets the Qt color scheme based on
|
||||
the theme's need_theme metadata.
|
||||
|
||||
Args:
|
||||
name (str): The theme name to apply.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the theme .altheme file does not exist.
|
||||
"""
|
||||
|
||||
filepath = os.path.join(self.__themes_dir, name + ".altheme")
|
||||
if not os.path.isfile(filepath):
|
||||
raise FileNotFoundError(filepath)
|
||||
with self.__lock:
|
||||
info = validateTheme(filepath)
|
||||
qss = readThemeQss(filepath)
|
||||
app = QApplication.instance()
|
||||
if app:
|
||||
app.setStyleSheet(qss)
|
||||
need_theme = info.get("need_theme", "both")
|
||||
app.styleHints().setColorScheme(
|
||||
ThemeManager._colorSchemeFor(need_theme)
|
||||
)
|
||||
app.setStyle(QStyleFactory.create(_active_style_name))
|
||||
self.__current_theme_name = name
|
||||
|
||||
def clearTheme(
|
||||
self,
|
||||
theme: str
|
||||
):
|
||||
"""
|
||||
Clear the current QSS stylesheet and apply the given color scheme.
|
||||
|
||||
Args:
|
||||
theme (str): The color scheme to apply after clearing
|
||||
("light", "dark", or "system").
|
||||
"""
|
||||
|
||||
app = QApplication.instance()
|
||||
if not app:
|
||||
return
|
||||
app.setStyleSheet("")
|
||||
app.styleHints().setColorScheme(
|
||||
ThemeManager._colorSchemeFor(theme)
|
||||
)
|
||||
app.setStyle(QStyleFactory.create(_active_style_name))
|
||||
|
||||
|
||||
# ThemeManager singleton instance.
|
||||
_theme_manager_instance = None
|
||||
|
||||
# Singleton instance lock.
|
||||
_instance_lock = threading.Lock()
|
||||
|
||||
|
||||
def instance(
|
||||
themes_dir: str = ""
|
||||
) -> ThemeManager:
|
||||
"""
|
||||
Get the ThemeManager singleton instance.
|
||||
|
||||
On first call, initialises the ThemeManager with the themes
|
||||
directory derived from ConfigManager's config directory.
|
||||
|
||||
Args:
|
||||
themes_dir (str): Optional themes directory path.
|
||||
|
||||
Returns:
|
||||
ThemeManager: The singleton ThemeManager instance.
|
||||
"""
|
||||
|
||||
global _theme_manager_instance
|
||||
with _instance_lock:
|
||||
if _theme_manager_instance is None:
|
||||
if not themes_dir:
|
||||
cfg = configInstance()
|
||||
themes_dir = os.path.join(cfg.configDir(), "themes")
|
||||
_theme_manager_instance = ThemeManager(themes_dir)
|
||||
return _theme_manager_instance
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
@@ -0,0 +1,203 @@
|
||||
# -*- 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 json
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
|
||||
def packTheme(
|
||||
qss_path: str,
|
||||
info: dict,
|
||||
output_path: str
|
||||
):
|
||||
"""
|
||||
Pack a .qss file and info dict into a .altheme file.
|
||||
|
||||
The .altheme file is a zip archive containing info.json and theme.qss.
|
||||
|
||||
Args:
|
||||
qss_path (str): Path to the .qss stylesheet file.
|
||||
info (dict): Theme metadata dict with keys name, author, need_theme, brief.
|
||||
output_path (str): Destination path for the .altheme file.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If qss_path does not exist.
|
||||
"""
|
||||
|
||||
if not os.path.isfile(qss_path):
|
||||
raise FileNotFoundError(qss_path)
|
||||
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr("info.json", json.dumps(info, ensure_ascii=False, indent=4))
|
||||
zf.write(qss_path, "theme.qss")
|
||||
|
||||
def unpackTheme(
|
||||
altheme_path: str,
|
||||
output_dir: str
|
||||
):
|
||||
"""
|
||||
Extract a .altheme file to a directory.
|
||||
|
||||
Performs Zip Slip validation before extraction.
|
||||
|
||||
Args:
|
||||
altheme_path (str): Path to the .altheme file.
|
||||
output_dir (str): Directory to extract contents into.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If altheme_path does not exist.
|
||||
ValueError: If a zip entry contains an unsafe path.
|
||||
"""
|
||||
|
||||
if not os.path.isfile(altheme_path):
|
||||
raise FileNotFoundError(altheme_path)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
with zipfile.ZipFile(altheme_path, "r") as zf:
|
||||
for name in zf.namelist():
|
||||
if name.startswith("/") or ".." in name:
|
||||
raise ValueError(f"不安全的 .altheme 入口: {name}")
|
||||
zf.extractall(output_dir)
|
||||
|
||||
def readThemeInfo(
|
||||
altheme_path: str
|
||||
) -> dict:
|
||||
"""
|
||||
Read only the info.json metadata from a .altheme file.
|
||||
|
||||
Args:
|
||||
altheme_path (str): Path to the .altheme file.
|
||||
|
||||
Returns:
|
||||
dict: The theme metadata dictionary.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If altheme_path does not exist.
|
||||
ValueError: If the .altheme does not contain info.json.
|
||||
"""
|
||||
|
||||
if not os.path.isfile(altheme_path):
|
||||
raise FileNotFoundError(altheme_path)
|
||||
with zipfile.ZipFile(altheme_path, "r") as zf:
|
||||
if "info.json" not in zf.namelist():
|
||||
raise ValueError("无效的 .altheme: 缺少 info.json")
|
||||
with zf.open("info.json") as fh:
|
||||
info = json.loads(fh.read().decode("utf-8"))
|
||||
if "name" not in info:
|
||||
raise ValueError("无效的 .altheme: info.json 缺少 'name' 字段")
|
||||
return info
|
||||
|
||||
def readThemeQss(
|
||||
altheme_path: str
|
||||
) -> str:
|
||||
"""
|
||||
Read the theme.qss content directly from a .altheme archive.
|
||||
|
||||
Args:
|
||||
altheme_path (str): Path to the .altheme file.
|
||||
|
||||
Returns:
|
||||
str: The QSS stylesheet content.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If altheme_path does not exist.
|
||||
ValueError: If theme.qss is missing from the archive.
|
||||
"""
|
||||
|
||||
if not os.path.isfile(altheme_path):
|
||||
raise FileNotFoundError(altheme_path)
|
||||
with zipfile.ZipFile(altheme_path, "r") as zf:
|
||||
if "theme.qss" not in zf.namelist():
|
||||
raise ValueError("无效的 .altheme: 缺少 theme.qss")
|
||||
return zf.read("theme.qss").decode("utf-8")
|
||||
|
||||
def validateTheme(
|
||||
altheme_path: str,
|
||||
check_qss: bool = True
|
||||
) -> dict:
|
||||
"""
|
||||
Validate a .altheme file and return its metadata.
|
||||
|
||||
Checks that info.json and theme.qss both exist, info.json
|
||||
contains all required fields with valid values, and theme.qss
|
||||
is a non-empty entry in the archive.
|
||||
|
||||
Args:
|
||||
altheme_path (str): Path to the .altheme file.
|
||||
check_qss (bool): If False, skip theme.qss existence and
|
||||
content checks (for list-only operations).
|
||||
|
||||
Returns:
|
||||
dict: The validated theme metadata dictionary.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If altheme_path does not exist.
|
||||
ValueError: If validation fails for any reason.
|
||||
"""
|
||||
|
||||
if not os.path.isfile(altheme_path):
|
||||
raise FileNotFoundError(altheme_path)
|
||||
with zipfile.ZipFile(altheme_path, "r") as zf:
|
||||
names = zf.namelist()
|
||||
if "info.json" not in names:
|
||||
raise ValueError("无效的 .altheme: 缺少 info.json")
|
||||
if "theme.qss" not in names:
|
||||
raise ValueError("无效的 .altheme: 缺少 theme.qss")
|
||||
info_bytes = zf.read("info.json")
|
||||
qss_bytes = zf.read("theme.qss") if check_qss else None # skip QSS read when only listing
|
||||
try:
|
||||
info = json.loads(info_bytes.decode("utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||
raise ValueError(f"无效的 .altheme: info.json 解析失败 — {e}")
|
||||
if "name" not in info or not isinstance(info.get("name"), str) or not info["name"].strip():
|
||||
raise ValueError("无效的 .altheme: info.json 缺少有效的 'name' 字段")
|
||||
# reject blank author so info.json does not drift from the "未知作者" filename fallback
|
||||
if ("author" not in info or not isinstance(info.get("author"), str)
|
||||
or not info["author"].strip()):
|
||||
raise ValueError("无效的 .altheme: info.json 缺少有效的 'author' 字段")
|
||||
need_theme = info.get("need_theme", "both")
|
||||
if need_theme not in ("light", "dark", "both"):
|
||||
raise ValueError(
|
||||
f"无效的 .altheme: need_theme 值 '{need_theme}' 无效, "
|
||||
f"应为 'light'、'dark' 或 'both'"
|
||||
)
|
||||
if "brief" not in info or not isinstance(info.get("brief"), str):
|
||||
raise ValueError("无效的 .altheme: info.json 缺少有效的 'brief' 字段")
|
||||
if check_qss and not qss_bytes.strip():
|
||||
raise ValueError("无效的 .altheme: theme.qss 为空")
|
||||
return info
|
||||
|
||||
def wrapQssToAtheme(
|
||||
qss_path: str,
|
||||
output_path: str,
|
||||
current_theme: str
|
||||
):
|
||||
"""
|
||||
Wrap a bare .qss file into a .altheme file with auto-generated metadata.
|
||||
|
||||
The generated info.json uses the filename as the theme name
|
||||
and sets default values for author and brief.
|
||||
|
||||
Args:
|
||||
qss_path (str): Path to the bare .qss stylesheet file.
|
||||
output_path (str): Destination path for the .altheme file.
|
||||
current_theme (str): The need_theme value to embed in metadata
|
||||
("light", "dark", or "both").
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If qss_path does not exist.
|
||||
"""
|
||||
|
||||
filename = os.path.splitext(os.path.basename(qss_path))[0]
|
||||
info = {
|
||||
"name": filename,
|
||||
"author": "未知作者",
|
||||
"need_theme": current_theme,
|
||||
"brief": "没有相关简介"
|
||||
}
|
||||
packTheme(qss_path, info, output_path)
|
||||
Reference in New Issue
Block a user