diff --git a/requirements.txt b/requirements.txt
index b85c98d..95204cc 100644
Binary files a/requirements.txt and b/requirements.txt differ
diff --git a/src/Main.py b/src/Main.py
index c9e50ca..cf35f22 100644
--- a/src/Main.py
+++ b/src/Main.py
@@ -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)
diff --git a/src/boot/AppInitializer.py b/src/boot/AppInitializer.py
index 4512368..4c0440e 100644
--- a/src/boot/AppInitializer.py
+++ b/src/boot/AppInitializer.py
@@ -10,10 +10,13 @@ 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 gui.ALSettingsWidget import _applyTheme
+from interfaces.ConfigProvider import CfgKey
from managers.config.ConfigManager import instance as configInstance
from managers.driver.WebDriverManager import instance as webdriverInstance
+from managers.log.LogManager import instance as logInstance
def _initializeLogManager(
@@ -64,13 +67,25 @@ 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")
+ app.setStyle(saved_style)
+ _applyTheme(saved_theme)
+
def initializeApp(
) -> bool:
"""
Initialize the application components
Order:
- LogManager -> ConfigManager -> WebDriverManager
+ LogManager -> ConfigManager -> WebDriverManager -> Appearance
"""
if not _initializeLogManager():
@@ -79,4 +94,5 @@ def initializeApp(
return False
if not _initializeWebDriverManager():
return False
+ _initializeAppearance()
return True
diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py
index 750686b..a501de0 100644
--- a/src/gui/ALAutoScriptEditDialog.py
+++ b/src/gui/ALAutoScriptEditDialog.py
@@ -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,
)
@@ -215,8 +216,8 @@ class ALAutoScriptEditDialog(QDialog):
self.ZoomOutBtn = QPushButton("-")
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("fa5s.undo", color=self._iconColor()))
+ self.ZoomResetBtn.setIconSize(QSize(14, 14))
self.ZoomResetBtn.setFixedSize(25, 25)
self.ZoomResetBtn.setToolTip("重置缩放")
self.ZoomLabel = QLabel(f"{self._fontSize}px")
@@ -240,8 +241,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("fa5s.copy", color=self._iconColor()))
+ self.CopyBtn.setIconSize(QSize(14, 14))
self.CopyBtn.setFixedSize(25, 25)
self.CopyBtn.setToolTip("复制脚本")
ToolbarLayout.addWidget(self.CopyBtn)
@@ -537,6 +538,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
):
diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py
index 82a944f..5d805c6 100644
--- a/src/gui/ALMainWindow.py
+++ b/src/gui/ALMainWindow.py
@@ -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,
diff --git a/src/gui/ALSettingsWidget.py b/src/gui/ALSettingsWidget.py
new file mode 100644
index 0000000..1fe44fb
--- /dev/null
+++ b/src/gui/ALSettingsWidget.py
@@ -0,0 +1,340 @@
+# -*- 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,
+ QStyle,
+ QStyleFactory,
+ QWidget
+)
+
+import managers.config.ConfigManager as ConfigManager
+
+from gui.resources.ui.Ui_ALSettingsWidget import Ui_ALSettingsWidget
+from interfaces.ConfigProvider import (
+ CfgKey,
+ ConfigProvider
+)
+
+
+def _clearQss(
+):
+
+ app : QApplication | None = QApplication.instance()
+ if app:
+ app.setStyleSheet("")
+
+def _loadQss(
+ file_path: str
+) -> str:
+
+ if not file_path or not os.path.isfile(file_path):
+ return ""
+ try:
+ with open(file_path, "r", encoding="utf-8") as fh:
+ return fh.read()
+ except Exception:
+ return ""
+
+def _applyQss(
+ file_path: str
+):
+
+ app : QApplication | None = QApplication.instance()
+ if not app:
+ return
+ qss = _loadQss(file_path)
+ if qss:
+ app.setStyleSheet(qss)
+ else:
+ _clearQss()
+
+def _applyTheme(
+ theme: str
+):
+
+ app : QApplication | None = QApplication.instance()
+ if not app:
+ return
+ if theme == "dark":
+ app.styleHints().setColorScheme(Qt.ColorScheme.Dark)
+ elif theme == "light":
+ app.styleHints().setColorScheme(Qt.ColorScheme.Light)
+ else:
+ app.styleHints().setColorScheme(Qt.ColorScheme.Unknown)
+ app.setStyle(QStyleFactory.create(app.style().objectName()))
+
+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_style: QStyle | None = None
+
+ 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()
+
+ 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("fa5s.palette", color=color))
+
+ def populateStyles(
+ self
+ ):
+
+ self.StyleComboBox.clear()
+ self.StyleComboBox.addItems(QStyleFactory.keys())
+
+ def connectSignals(
+ self
+ ):
+
+ self.BrowseQssButton.clicked.connect(self.onBrowseQssButtonClicked)
+ self.ApplyQssButton.clicked.connect(self.onApplyQssButtonClicked)
+ self.ResetQssButton.clicked.connect(self.onResetQssButtonClicked)
+ 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_qss = self.__cfg_mgr.get(CfgKey.GLOBAL.APPEARANCE.CUSTOM_QSS, "")
+ self.__original_style = QApplication.instance().style()
+ 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:
+ self.StyleComboBox.setCurrentIndex(index)
+ else:
+ self.StyleComboBox.setCurrentIndex(0)
+ self.QssPathEdit.setText(custom_qss)
+ self.updateQssStatus(custom_qss)
+
+ def updateQssStatus(
+ self,
+ qss_path: str
+ ):
+
+ if qss_path and os.path.isfile(qss_path):
+ self.QssStatusLabel.setText(f"已加载自定义样式文件:{qss_path}")
+ else:
+ self.QssStatusLabel.setText("当前使用程序默认外观。")
+
+ def collectSettings(
+ self
+ ):
+
+ if self.LightThemeRadio.isChecked():
+ theme = "light"
+ elif self.DarkThemeRadio.isChecked():
+ theme = "dark"
+ else:
+ theme = "system"
+ style = QStyleFactory.create(self.StyleComboBox.currentText())
+ custom_qss = self.QssPathEdit.text().strip()
+ return theme, style, custom_qss
+
+ def saveAndApply(
+ self
+ ):
+
+ theme, style, custom_qss = self.collectSettings()
+ self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme)
+ self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.STYLE, style.name())
+ self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_QSS, custom_qss)
+ if custom_qss and os.path.isfile(custom_qss):
+ _applyQss(custom_qss)
+ else:
+ _clearQss()
+ _applyTheme(theme)
+ self.setNavigationIcons()
+ self.updateQssStatus(custom_qss)
+ self.__original_style = QApplication.instance().style()
+
+ def maybeRestart(
+ self
+ ) -> bool:
+
+ reply = QMessageBox.question(
+ self,
+ "提示 - AutoLibrary",
+ "界面风格已修改,需要重启程序才能生效。是否立即重启?",
+ QMessageBox.Yes | QMessageBox.No,
+ QMessageBox.Yes
+ )
+ if reply == QMessageBox.Yes:
+ _restartApp()
+ return True
+ return False
+
+ @Slot()
+ def onBrowseQssButtonClicked(
+ self
+ ):
+
+ file_path, _ = QFileDialog.getOpenFileName(
+ self,
+ "选择 QSS 样式文件 - AutoLibrary",
+ self.QssPathEdit.text(),
+ "QSS 样式表文件 (*.qss);;所有文件 (*)"
+ )
+ if file_path:
+ self.QssPathEdit.setText(file_path)
+
+ @Slot()
+ def onApplyQssButtonClicked(
+ 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)
+
+ @Slot()
+ def onResetQssButtonClicked(
+ self
+ ):
+
+ self.QssPathEdit.clear()
+ _clearQss()
+ if self.LightThemeRadio.isChecked():
+ _applyTheme("light")
+ elif self.DarkThemeRadio.isChecked():
+ _applyTheme("dark")
+ else:
+ _applyTheme("system")
+ self.setNavigationIcons()
+ self.updateQssStatus("")
+
+ @Slot()
+ def onCancelButtonClicked(
+ self
+ ):
+
+ self.close()
+
+ @Slot()
+ def onApplyButtonClicked(
+ self
+ ):
+
+ _, style, _ = self.collectSettings()
+ style_changed = self.__original_style.name() != style.name()
+ self.saveAndApply()
+ if style_changed:
+ self.maybeRestart()
+
+ @Slot()
+ def onConfirmButtonClicked(
+ self
+ ):
+
+ _, style, _ = self.collectSettings()
+ style_changed = self.__original_style.name() != style.name()
+ self.saveAndApply()
+ if style_changed:
+ self.maybeRestart()
+ self.close()
diff --git a/src/gui/resources/ui/ALMainWindow.ui b/src/gui/resources/ui/ALMainWindow.ui
index c210a8b..84aa38c 100644
--- a/src/gui/resources/ui/ALMainWindow.ui
+++ b/src/gui/resources/ui/ALMainWindow.ui
@@ -281,6 +281,12 @@ font: 700 9pt;
true
+
+
@@ -308,6 +315,11 @@ font: 700 9pt;
关于
+
+
+ 全局设置
+
+
diff --git a/src/gui/resources/ui/ALSettingsWidget.ui b/src/gui/resources/ui/ALSettingsWidget.ui
new file mode 100644
index 0000000..7db0631
--- /dev/null
+++ b/src/gui/resources/ui/ALSettingsWidget.ui
@@ -0,0 +1,480 @@
+
+
+ ALSettingsWidget
+
+
+
+ 0
+ 0
+ 400
+ 420
+
+
+
+
+ 400
+ 420
+
+
+
+
+ 500
+ 420
+
+
+
+ 全局设置 - AutoLibrary
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+
+ 100
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Qt::FocusPolicy::StrongFocus
+
+
+ QFrame::Shape::NoFrame
+
+
+ QAbstractItemView::EditTrigger::NoEditTriggers
+
+
+ QAbstractItemView::SelectionMode::SingleSelection
+
+
+
+ 20
+ 20
+
+
+
+ 0
+
+
-
+
+ 外观
+
+
+
+
+
+
+
+ -
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ 主题模式
+
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ 浅色
+
+
+
+ -
+
+
+ 深色
+
+
+
+ -
+
+
+ 跟随系统
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ 界面风格
+
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ 5
+
+
-
+
+
+
+ 100
+ 25
+
+
+
+
+ 100
+ 25
+
+
+
+ 应用程序样式:
+
+
+
+ -
+
+
+
+ 160
+ 25
+
+
+
+
+
+
+ -
+
+
+ 更改样式将在下次启动应用程序时生效。
+
+
+
+
+
+
+ -
+
+
+ 自定义外观
+
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ 自定义程序外观,文件加载后将立即生效。
+
+
+ true
+
+
+
+ -
+
+
+ 5
+
+
-
+
+
+
+ 0
+ 25
+
+
+
+ 选择或输入 QSS 样式表文件路径...
+
+
+
+ -
+
+
+
+ 25
+ 25
+
+
+
+
+ 25
+ 25
+
+
+
+ ...
+
+
+
+
+
+ -
+
+
+ 5
+
+
-
+
+
+
+ 80
+ 25
+
+
+
+ 应用样式
+
+
+
+ -
+
+
+
+ 80
+ 25
+
+
+
+ 重置外观
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ 当前使用程序默认外观。
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ -
+
+
+ 5
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 80
+ 25
+
+
+
+
+ 80
+ 25
+
+
+
+ 取消
+
+
+
+ -
+
+
+
+ 80
+ 25
+
+
+
+
+ 80
+ 25
+
+
+
+ 应用
+
+
+
+ -
+
+
+
+ 80
+ 25
+
+
+
+
+ 80
+ 25
+
+
+
+ 确认
+
+
+ true
+
+
+
+
+
+
+
+
+
+
diff --git a/src/interfaces/ConfigProvider.py b/src/interfaces/ConfigProvider.py
index 9ee5bb4..51e0d2a 100644
--- a/src/interfaces/ConfigProvider.py
+++ b/src/interfaces/ConfigProvider.py
@@ -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_QSS = ConfigPath(ConfigType.GLOBAL, "appearance.custom_qss")
+
class TIMERTASK:
ROOT = ConfigPath(ConfigType.TIMERTASK, "")
TIMER_TASKS = ConfigPath(ConfigType.TIMERTASK, "timer_tasks")
diff --git a/src/managers/config/ConfigManager.py b/src/managers/config/ConfigManager.py
index 4f89eb4..58f0787 100644
--- a/src/managers/config/ConfigManager.py
+++ b/src/managers/config/ConfigManager.py
@@ -54,6 +54,11 @@ class ConfigTemplate:
"current": 0,
"paths": []
}
+ },
+ "appearance": {
+ "theme": "system",
+ "style": "Fusion",
+ "custom_qss": ""
}
}
case ConfigType.BULLETIN: