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

Compare commits

..

10 Commits

Author SHA1 Message Date
KenanZhu c250fa4a6e refactor(theme): 将重复的主题逻辑下沉至 ThemeUtils,消除 validateTheme 职责过重 2026-06-19 11:21:50 +08:00
KenanZhu 8f8e3e4ba7 refactor(gui): 自定义主题控件及函数重命名,统一 CustomTheme 前缀 2026-06-19 10:22:36 +08:00
KenanZhu 88a74a7a47 refactor(gui): 提取窗口居中逻辑至 CenterOnParentMixin,消除5处重复 showEvent 2026-06-19 10:20:35 +08:00
KenanZhu 5552af1345 refactor(gui): 消除确认按钮重复逻辑,重置按钮不再提前应用主题 2026-06-19 09:36:18 +08:00
KenanZhu f9175371dc feat(gui): +/- 按钮文本替换为 QtAwesome 图标,fa5s 统一升级为 fa6s
- ALSettingsWidget: BrowseQssButton/RemoveThemeButton 的 + / - 文本改为 fa6s.plus/fa6s.minus 图标
- ALAutoScriptEditDialog: ZoomInBtn/ZoomOutBtn 的全角 +/- 改为 fa6s.plus/fa6s.minus 图标
- 其余图标同步从 fa5s 升级至 fa6s (Font Awesome 6)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 08:15:03 +08:00
KenanZhu 8e1b28f3fe fix: requirements.txt 编码从 UTF-16 LE 转为 UTF-8,移除 8 个多余依赖包
移除的包: altgraph, mpmath, pefile, pyinstaller-hooks-contrib, pywin32-ctypes, setuptools, sympy, websocket-client
(这些均为传递依赖,pip 会根据直接依赖自动解析安装)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 22:19:05 +08:00
KenanZhu 57f1cfb3f2 fix(theme): 修复死锁、冗余读取、空作者字符串等交叉审查问题
- ThemeManager 拆分 _removeThemeFile 无锁版本, 消除 importTheme 持锁
  时调用 removeTheme 导致的死锁
- validateTheme 增加 check_qss 参数, listThemes 跳过 QSS 读取
- validateTheme 拒绝空/空白作者字符串, 避免 info.json 与文件名不一致
- 统一默认作者为 "未知作者"
- ALSettingsWidget.ui 增加删除按钮 [-], 浏览按钮改为 [+]
- ALSettingsWidget 实现 onRemoveThemeButtonClicked 删除逻辑
2026-06-16 19:37:09 +08:00
KenanZhu 007b4dc2ef fix(theme): 修复同名主题无法区分作者及导入链路边界问题
- 新增 ThemeUtils.validateTheme 和 readThemeQss 集中校验与读取逻辑
- ThemeManager.importTheme 通过 _resolveDestPath 处理同名主题:
  不同作者自动命名为 {主题名}_{作者名}.altheme, 首次导入保持原名
- ThemeManager.listThemes 返回 file 字段以便 UI 层定位文件
- ALSettingsWidget 全线改用 file 标识符, 组合框按作者消歧义显示
- 移除 applyTheme 中的临时目录解压, 改用 readThemeQss 直接读取
2026-06-16 18:37:47 +08:00
KenanZhu 67f297b434 revert(ALConfigWidget.ui): 撤回上次提交中的 ui 文件的启动默认页面 2026-06-07 12:53:02 +08:00
KenanZhu 86f0761eed refactor(theme): 优化 LightLake 与 BlueForest 主题显示样式 2026-06-07 12:50:32 +08:00
14 changed files with 657 additions and 594 deletions
BIN
View File
Binary file not shown.
+8 -4
View File
@@ -211,12 +211,16 @@ class ALAutoScriptEditDialog(QDialog):
Layout.setSpacing(3) Layout.setSpacing(3)
Layout.setContentsMargins(3, 3, 3, 3) Layout.setContentsMargins(3, 3, 3, 3)
ToolbarLayout = QHBoxLayout() 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.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.ZoomOutBtn.setFixedSize(25, 25)
self.ZoomResetBtn = QPushButton("") self.ZoomResetBtn = QPushButton("")
self.ZoomResetBtn.setIcon(qta.icon("fa5s.undo", color=self._iconColor())) self.ZoomResetBtn.setIcon(qta.icon("fa6s.rotate-left", color=self._iconColor()))
self.ZoomResetBtn.setIconSize(QSize(14, 14)) self.ZoomResetBtn.setIconSize(QSize(14, 14))
self.ZoomResetBtn.setFixedSize(25, 25) self.ZoomResetBtn.setFixedSize(25, 25)
self.ZoomResetBtn.setToolTip("重置缩放") self.ZoomResetBtn.setToolTip("重置缩放")
@@ -241,7 +245,7 @@ class ALAutoScriptEditDialog(QDialog):
ToolbarLayout.addWidget(self.ZoomLabel) ToolbarLayout.addWidget(self.ZoomLabel)
ToolbarLayout.addStretch() ToolbarLayout.addStretch()
self.CopyBtn = QPushButton("") self.CopyBtn = QPushButton("")
self.CopyBtn.setIcon(qta.icon("fa5s.copy", color=self._iconColor())) self.CopyBtn.setIcon(qta.icon("fa6s.copy", color=self._iconColor()))
self.CopyBtn.setIconSize(QSize(14, 14)) self.CopyBtn.setIconSize(QSize(14, 14))
self.CopyBtn.setFixedSize(25, 25) self.CopyBtn.setFixedSize(25, 25)
self.CopyBtn.setToolTip("复制脚本") self.CopyBtn.setToolTip("复制脚本")
+2 -24
View File
@@ -42,6 +42,7 @@ from gui.ALUserTreeWidget import (
ALUserTreeWidget ALUserTreeWidget
) )
from gui.ALWebDriverDownloadDialog import ALWebDriverDownloadDialog from gui.ALWebDriverDownloadDialog import ALWebDriverDownloadDialog
from gui.ALWidgetMixin import CenterOnParentMixin
from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget
from interfaces.ConfigProvider import ( from interfaces.ConfigProvider import (
CfgKey, CfgKey,
@@ -52,7 +53,7 @@ from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter from utils.JSONWriter import JSONWriter
class ALConfigWidget(QWidget, Ui_ALConfigWidget): class ALConfigWidget(CenterOnParentMixin, QWidget, Ui_ALConfigWidget):
configWidgetIsClosed = Signal() configWidgetIsClosed = Signal()
@@ -110,29 +111,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked) self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def showEvent(
self,
event
):
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( def closeEvent(
self, self,
event: QCloseEvent event: QCloseEvent
+2 -25
View File
@@ -24,9 +24,9 @@ from PySide6.QtWidgets import (
) )
from gui.ALSeatMapView import ALSeatMapView from gui.ALSeatMapView import ALSeatMapView
from gui.ALWidgetMixin import CenterOnParentMixin
class ALSeatMapSelectDialog(CenterOnParentMixin, QDialog):
class ALSeatMapSelectDialog(QDialog):
seatMapSelectDialogIsClosed = Signal(list) seatMapSelectDialogIsClosed = Signal(list)
@@ -96,29 +96,6 @@ class ALSeatMapSelectDialog(QDialog):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked) self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def showEvent(
self,
event
):
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( def closeEvent(
self, self,
event: QCloseEvent event: QCloseEvent
+128 -103
View File
@@ -19,8 +19,7 @@ from PySide6.QtCore import (
Slot Slot
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QCloseEvent, QCloseEvent
QShowEvent
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication, QApplication,
@@ -38,6 +37,7 @@ from managers.theme.ThemeManager import(
instance as themeInstance instance as themeInstance
) )
from gui.ALWidgetMixin import CenterOnParentMixin
from gui.resources.ui.Ui_ALSettingsWidget import Ui_ALSettingsWidget from gui.resources.ui.Ui_ALSettingsWidget import Ui_ALSettingsWidget
from interfaces.ConfigProvider import ( from interfaces.ConfigProvider import (
CfgKey, CfgKey,
@@ -83,7 +83,7 @@ def _restartApp(
QProcess.startDetached(sys.executable, sys.argv) QProcess.startDetached(sys.executable, sys.argv)
class ALSettingsWidget(QWidget, Ui_ALSettingsWidget): class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
settingsWidgetIsClosed = Signal() settingsWidgetIsClosed = Signal()
@@ -102,20 +102,36 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self.connectSignals() self.connectSignals()
self.loadSettings() self.loadSettings()
def closeEvent(
self,
event: QCloseEvent
):
self.settingsWidgetIsClosed.emit()
super().closeEvent(event)
def modifyUi( def modifyUi(
self self
): ):
self.setWindowFlags(Qt.WindowType.Window) self.setWindowFlags(Qt.WindowType.Window)
self.NavigationList.setCurrentRow(0)
self.populateStyles()
self.setNavigationIcons() self.setNavigationIcons()
self.ThemeInfoLabel.setTextFormat(Qt.TextFormat.RichText) color = QApplication.instance().palette().color(
self.ThemeInfoLabel.setStyleSheet( QApplication.instance().palette().ColorRole.WindowText
).name()
self.ImportCustomThemeButton.setIcon(qta.icon("fa6s.plus", color=color))
self.ImportCustomThemeButton.setText("")
self.RemoveCustomThemeButton.setIcon(qta.icon("fa6s.minus", color=color))
self.RemoveCustomThemeButton.setText("")
self.CustomThemeInfoLabel.setTextFormat(Qt.TextFormat.RichText)
self.CustomThemeInfoLabel.setStyleSheet(
"border: 1px solid palette(mid);"\ "border: 1px solid palette(mid);"\
"border-radius: 2px;"\ "border-radius: 2px;"\
"padding: 5px;" "padding: 5px;"
) )
self.NavigationList.setCurrentRow(0)
self.populateStyles()
self.populateCustomThemes()
def setNavigationIcons( def setNavigationIcons(
self self
@@ -125,7 +141,7 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
color = app.palette().color(app.palette().ColorRole.WindowText).name() color = app.palette().color(app.palette().ColorRole.WindowText).name()
item = self.NavigationList.item(0) item = self.NavigationList.item(0)
if item: if item:
item.setIcon(qta.icon("fa5s.palette", color=color)) item.setIcon(qta.icon("fa6s.palette", color=color))
def populateStyles( def populateStyles(
self self
@@ -134,46 +150,36 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self.StyleComboBox.clear() self.StyleComboBox.clear()
self.StyleComboBox.addItems(QStyleFactory.keys()) self.StyleComboBox.addItems(QStyleFactory.keys())
def populateCustomThemes(
self
):
self.CustomThemeComboBox.blockSignals(True)
self.CustomThemeComboBox.clear()
self.CustomThemeComboBox.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.CustomThemeComboBox.addItem(name, file)
self.CustomThemeComboBox.blockSignals(False)
def connectSignals( def connectSignals(
self self
): ):
self.BrowseQssButton.clicked.connect(self.onImportThemeButtonClicked) self.ImportCustomThemeButton.clicked.connect(self.onImportCustomThemeButtonClicked)
self.ThemeComboBox.currentIndexChanged.connect(self.onThemeComboBoxChanged) self.RemoveCustomThemeButton.clicked.connect(self.onRemoveCustomThemeButtonClicked)
self.ResetThemeButton.clicked.connect(self.onResetThemeButtonClicked) self.CustomThemeComboBox.currentIndexChanged.connect(self.onCustomThemeComboBoxChanged)
self.ResetCustomThemeButton.clicked.connect(self.onResetCustomThemeButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked) self.CancelButton.clicked.connect(self.onCancelButtonClicked)
self.ApplyButton.clicked.connect(self.onApplyButtonClicked) self.ApplyButton.clicked.connect(self.onApplyButtonClicked)
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) 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( def loadSettings(
self self
): ):
@@ -194,44 +200,46 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
if index < 0: if index < 0:
index = 0 index = 0
self.StyleComboBox.setCurrentIndex(index) self.StyleComboBox.setCurrentIndex(index)
self.populateThemeList()
if custom_theme: if custom_theme:
idx = self.ThemeComboBox.findText(custom_theme) idx = self.CustomThemeComboBox.findData(custom_theme)
if idx >= 0: if idx >= 0:
self.ThemeComboBox.setCurrentIndex(idx) self.CustomThemeComboBox.setCurrentIndex(idx)
self.updateThemeStatus() self.updateCustomThemeInfo()
self.updateThemeInfo() self.updateCustomThemeStatus()
def updateThemeStatus( def updateCustomThemeInfo(
self self
): ):
name = self.ThemeComboBox.currentText() file = self.CustomThemeComboBox.currentData()
if name and name != "默认": if not file:
self.QssStatusLabel.setText(f"当前使用 {name} 主题。") self.CustomThemeInfoLabel.setText("")
else:
self.QssStatusLabel.setText("当前使用 默认 主题。")
def updateThemeInfo(
self
):
name = self.ThemeComboBox.currentText()
if not name or name == "默认":
self.ThemeInfoLabel.setText("")
return return
t = self.__theme_cache.get(name) t = self.__theme_cache.get(file)
if t: if t:
author = t.get("author", "未知") name = t.get("name", "未知")
author = t.get("author", "未知作者")
need_theme = t.get("need_theme", "both") need_theme = t.get("need_theme", "both")
brief = t.get("brief", "没有相关简介") brief = t.get("brief", "没有相关简介")
self.ThemeInfoLabel.setText( self.CustomThemeInfoLabel.setText(
f"<b>{name}</b> - 适用于 <i>{_themeToReadable(need_theme)}</i> 主题<br>" f"<b>{name}</b> - 适用于 <i>{_themeToReadable(need_theme)}</i> 主题<br>"
f"作者:{author}<br><br>" f"作者:{author}<br><br>"
f"{brief}" f"{brief}"
) )
else: else:
self.ThemeInfoLabel.setText("") self.CustomThemeInfoLabel.setText("")
def updateCustomThemeStatus(
self
):
file = self.CustomThemeComboBox.currentData()
t = self.__theme_cache.get(file) if file else None
name = t.get("name", "") if t else ""
if name:
self.CustomThemeStatusLabel.setText(f"当前使用 {name} 主题。")
else:
self.CustomThemeStatusLabel.setText("当前使用 默认 主题。")
def syncRadioFromNeedTheme( def syncRadioFromNeedTheme(
self, self,
@@ -257,8 +265,8 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
else: else:
theme = "system" theme = "system"
style = self.StyleComboBox.currentText() style = self.StyleComboBox.currentText()
custom_theme = self.ThemeComboBox.currentText() custom_theme = self.CustomThemeComboBox.currentData() or ""
if custom_theme == "默认": if not custom_theme:
custom_theme = "" custom_theme = ""
return theme, style, custom_theme return theme, style, custom_theme
@@ -278,8 +286,8 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
theme, _, _ = self.collectSettings() theme, _, _ = self.collectSettings()
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme) self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme)
self.setNavigationIcons() self.setNavigationIcons()
self.updateThemeStatus() self.updateCustomThemeStatus()
self.updateThemeInfo() self.updateCustomThemeInfo()
self.__original_theme = theme self.__original_theme = theme
self.__original_custom_theme = custom_theme if custom_theme else "" self.__original_custom_theme = custom_theme if custom_theme else ""
self.__original_style = getActiveStyle() self.__original_style = getActiveStyle()
@@ -300,24 +308,45 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
return True return True
return False return False
def populateThemeList( @Slot()
def onRemoveCustomThemeButtonClicked(
self self
): ):
self.ThemeComboBox.blockSignals(True) file = self.CustomThemeComboBox.currentData()
self.ThemeComboBox.clear() if not file:
self.ThemeComboBox.addItem("默认") QMessageBox.information(
self.__theme_cache = {} self,
themes = themeInstance().listThemes() "提示 - AutoLibrary",
for t in themes: "请先选择一个主题。"
name = t.get("name", "") )
if name: return
self.__theme_cache[name] = t t = self.__theme_cache.get(file)
self.ThemeComboBox.addItem(name) name = t.get("name", file) if t else file
self.ThemeComboBox.blockSignals(False) reply = QMessageBox.question(
self,
"删除主题 - AutoLibrary",
f"确定要删除主题 \"{name}\" 吗?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply != QMessageBox.Yes:
return
try:
themeInstance().removeTheme(file)
self.populateCustomThemes()
self.CustomThemeComboBox.setCurrentIndex(0)
self.updateCustomThemeStatus()
self.updateCustomThemeInfo()
except Exception as e:
QMessageBox.warning(
self,
"删除失败 - AutoLibrary",
f"无法删除主题:{e}"
)
@Slot() @Slot()
def onImportThemeButtonClicked( def onImportCustomThemeButtonClicked(
self self
): ):
@@ -330,13 +359,13 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
if not file_path: if not file_path:
return return
try: try:
name = themeInstance().importTheme(file_path) file_id = themeInstance().importTheme(file_path)
self.populateThemeList() self.populateCustomThemes()
idx = self.ThemeComboBox.findText(name) idx = self.CustomThemeComboBox.findData(file_id)
if idx >= 0: if idx >= 0:
self.ThemeComboBox.setCurrentIndex(idx) self.CustomThemeComboBox.setCurrentIndex(idx)
self.updateThemeStatus() self.updateCustomThemeStatus()
self.updateThemeInfo() self.updateCustomThemeInfo()
except Exception as e: except Exception as e:
QMessageBox.warning( QMessageBox.warning(
self, self,
@@ -345,37 +374,37 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
) )
@Slot() @Slot()
def onThemeComboBoxChanged( def onCustomThemeComboBoxChanged(
self, self,
index: int index: int
): ):
self.updateThemeInfo() self.updateCustomThemeInfo()
# no status update, because custom theme is not applied yet.
@Slot() @Slot()
def onResetThemeButtonClicked( def onResetCustomThemeButtonClicked(
self self
): ):
self.ThemeComboBox.blockSignals(True) self.CustomThemeComboBox.blockSignals(True)
if self.__original_custom_theme: if self.__original_custom_theme:
idx = self.ThemeComboBox.findText(self.__original_custom_theme) idx = self.CustomThemeComboBox.findData(self.__original_custom_theme)
if idx >= 0: if idx >= 0:
self.ThemeComboBox.setCurrentIndex(idx) self.CustomThemeComboBox.setCurrentIndex(idx)
else: else:
self.ThemeComboBox.setCurrentIndex(0) self.CustomThemeComboBox.setCurrentIndex(0)
else: else:
self.ThemeComboBox.setCurrentIndex(0) self.CustomThemeComboBox.setCurrentIndex(0)
self.ThemeComboBox.blockSignals(False) self.CustomThemeComboBox.blockSignals(False)
if self.__original_theme == "light": if self.__original_theme == "light":
self.LightThemeRadio.setChecked(True) self.LightThemeRadio.setChecked(True)
elif self.__original_theme == "dark": elif self.__original_theme == "dark":
self.DarkThemeRadio.setChecked(True) self.DarkThemeRadio.setChecked(True)
else: else:
self.SystemThemeRadio.setChecked(True) self.SystemThemeRadio.setChecked(True)
_applyCustomTheme(self.__original_custom_theme, self.__original_theme) self.updateCustomThemeInfo()
self.updateThemeStatus() self.updateCustomThemeStatus()
self.updateThemeInfo()
@Slot() @Slot()
def onCancelButtonClicked( def onCancelButtonClicked(
@@ -400,9 +429,5 @@ class ALSettingsWidget(QWidget, Ui_ALSettingsWidget):
self self
): ):
_, style, _ = self.collectSettings() self.onApplyButtonClicked() # virtually call apply button clicked
style_changed = self.__original_style != style
self.saveAndApply()
if style_changed:
self.maybeRestart()
self.close() self.close()
+2 -24
View File
@@ -43,6 +43,7 @@ from gui.ALTimerTaskAddDialog import (
ALTimerTaskStatus ALTimerTaskStatus
) )
from gui.ALTimerTaskHistoryDialog import ALTimerTaskHistoryDialog from gui.ALTimerTaskHistoryDialog import ALTimerTaskHistoryDialog
from gui.ALWidgetMixin import CenterOnParentMixin
from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget
from interfaces.ConfigProvider import ( from interfaces.ConfigProvider import (
CfgKey, CfgKey,
@@ -189,7 +190,7 @@ class ALTimerTaskItemWidget(QWidget):
Menu.exec(self.mapToGlobal(pos)) Menu.exec(self.mapToGlobal(pos))
class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): class ALTimerTaskManageWidget(CenterOnParentMixin, QWidget, Ui_ALTimerTaskManageWidget):
class SortPolicy(Enum): class SortPolicy(Enum):
@@ -299,29 +300,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
) )
return False return False
def showEvent(
self,
event
):
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( def closeEvent(
self, self,
event: QCloseEvent event: QCloseEvent
+2 -23
View File
@@ -38,6 +38,7 @@ from managers.driver.WebDriverManager import (
WebDriverStatus WebDriverStatus
) )
from gui.ALStatusLabel import ALStatusLabel from gui.ALStatusLabel import ALStatusLabel
from gui.ALWidgetMixin import CenterOnParentMixin
class DownloadWorker(QThread): class DownloadWorker(QThread):
@@ -123,7 +124,7 @@ class DownloadWorker(QThread):
self.wait() self.wait()
class ALWebDriverDownloadDialog(QDialog): class ALWebDriverDownloadDialog(CenterOnParentMixin, QDialog):
def __init__( def __init__(
self, self,
@@ -152,28 +153,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.initializeDriverManager() self.initializeDriverManager()
self.refreshDriverList() self.refreshDriverList()
def showEvent(
self,
event
):
result = super().showEvent(event)
if self.parent():
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 setupUi( def setupUi(
self self
): ):
+49
View File
@@ -0,0 +1,49 @@
# -*- 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.
"""
from PySide6.QtGui import QShowEvent
class CenterOnParentMixin:
"""
Mixin that centres the widget relative to its parent on first show,
clamping the position to the screen bounds.
Usage::
class MyWidget(CenterOnParentMixin, QWidget, Ui_MyWidget):
pass
class MyDialog(CenterOnParentMixin, QDialog):
pass
The mixin must appear **before** QWidget / QDialog in the base list
so that ``super().showEvent(event)`` resolves up the MRO correctly.
"""
def showEvent(
self,
event: QShowEvent
):
super().showEvent(event)
if self.parent():
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)
+15 -29
View File
@@ -21,11 +21,11 @@ QMainWindow::separator {
QMenuBar { QMenuBar {
background-color: #0f1628; background-color: #0f1628;
border-bottom: 1px solid #1c2840; border-bottom: 1px solid #1c2840;
padding: 2px 6px; padding: 2px 5px;
color: #d0daf0; color: #d0daf0;
} }
QMenuBar::item { QMenuBar::item {
padding: 4px 10px; padding: 2px 10px;
border-radius: 4px; border-radius: 4px;
} }
QMenuBar::item:selected { QMenuBar::item:selected {
@@ -150,7 +150,6 @@ QSpinBox::up-arrow,
QDoubleSpinBox::up-arrow, QDoubleSpinBox::up-arrow,
QDateEdit::up-arrow, QDateEdit::up-arrow,
QTimeEdit::up-arrow { QTimeEdit::up-arrow {
/* image: none; */
border-left: 4px solid transparent; border-left: 4px solid transparent;
border-right: 4px solid transparent; border-right: 4px solid transparent;
border-bottom: 5px solid #7888b8; border-bottom: 5px solid #7888b8;
@@ -176,7 +175,6 @@ QSpinBox::down-arrow,
QDoubleSpinBox::down-arrow, QDoubleSpinBox::down-arrow,
QDateEdit::down-arrow, QDateEdit::down-arrow,
QTimeEdit::down-arrow { QTimeEdit::down-arrow {
image: none;
border-left: 4px solid transparent; border-left: 4px solid transparent;
border-right: 4px solid transparent; border-right: 4px solid transparent;
border-top: 5px solid #7888b8; border-top: 5px solid #7888b8;
@@ -255,7 +253,7 @@ QComboBox:disabled {
/* ---- Check Box / Radio Button ---- */ /* ---- Check Box / Radio Button ---- */
QCheckBox, QCheckBox,
QRadioButton { QRadioButton {
spacing: 8px; spacing: 5px;
color: #d0daf0; color: #d0daf0;
} }
QCheckBox::indicator, QCheckBox::indicator,
@@ -263,9 +261,14 @@ QRadioButton::indicator {
border-style: solid; border-style: solid;
border-color: #334478; border-color: #334478;
border-width: 2px; border-width: 2px;
border-radius: 3px;
background-color: #0a1020; background-color: #0a1020;
} }
QCheckBox::indicator {
border-radius: 3px;
}
QRadioButton::indicator {
border-radius: 7px;
}
QCheckBox::indicator:hover, QCheckBox::indicator:hover,
QRadioButton::indicator:hover { QRadioButton::indicator:hover {
border-color: #2dd4bf; border-color: #2dd4bf;
@@ -274,9 +277,6 @@ QCheckBox::indicator:checked {
background-color: #2dd4bf; background-color: #2dd4bf;
border-color: #2dd4bf; border-color: #2dd4bf;
} }
QRadioButton::indicator {
border-radius: 10px;
}
QRadioButton::indicator:checked { QRadioButton::indicator:checked {
background-color: #2dd4bf; background-color: #2dd4bf;
border-color: #2dd4bf; border-color: #2dd4bf;
@@ -330,20 +330,14 @@ QTableWidget::indicator:indeterminate {
/* ---- Group Box ---- */ /* ---- Group Box ---- */
QGroupBox { QGroupBox {
margin-top: 5px;
padding-top: 15px;
color: #b4c2f5;
font-weight: bold;
border-style: solid; border-style: solid;
border-color: #253250; border-color: #253250;
border-width: 1px; border-width: 1px;
border-radius: 6px; border-radius: 5px;
margin-top: 12px;
padding-top: 14px;
color: #d0daf0;
font-weight: bold;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 12px;
padding: 0 6px;
color: #8b9ad0;
} }
/* ---- Tab ---- */ /* ---- Tab ---- */
@@ -389,18 +383,10 @@ QTableWidget {
QListWidget::item, QListWidget::item,
QTreeWidget::item, QTreeWidget::item,
QTableWidget::item { QTableWidget::item {
padding: 5px 10px; padding: 5px 5px;
border: none;
}
QListWidget::item:selected,
QTreeWidget::item:selected,
QTableWidget::item:selected {
background-color: #2dd4bf;
color: #0f1119;
} }
QHeaderView::section { QHeaderView::section {
background-color: #0f1628; background-color: #0f1628;
border: none;
border-right: 1px solid #253250; border-right: 1px solid #253250;
border-bottom: 1px solid #253250; border-bottom: 1px solid #253250;
padding: 5px 10px; padding: 5px 10px;
+15 -33
View File
@@ -21,11 +21,11 @@ QMainWindow::separator {
QMenuBar { QMenuBar {
background-color: #dce4ee; background-color: #dce4ee;
border-bottom: 1px solid #c0cdda; border-bottom: 1px solid #c0cdda;
padding: 2px 6px; padding: 2px 5px;
color: #1a2740; color: #1a2740;
} }
QMenuBar::item { QMenuBar::item {
padding: 4px 10px; padding: 2px 10px;
border-radius: 4px; border-radius: 4px;
} }
QMenuBar::item:selected { QMenuBar::item:selected {
@@ -150,7 +150,6 @@ QSpinBox::up-arrow,
QDoubleSpinBox::up-arrow, QDoubleSpinBox::up-arrow,
QDateEdit::up-arrow, QDateEdit::up-arrow,
QTimeEdit::up-arrow { QTimeEdit::up-arrow {
/* image: none; */
border-left: 4px solid transparent; border-left: 4px solid transparent;
border-right: 4px solid transparent; border-right: 4px solid transparent;
border-bottom: 5px solid #6a7898; border-bottom: 5px solid #6a7898;
@@ -176,7 +175,6 @@ QSpinBox::down-arrow,
QDoubleSpinBox::down-arrow, QDoubleSpinBox::down-arrow,
QDateEdit::down-arrow, QDateEdit::down-arrow,
QTimeEdit::down-arrow { QTimeEdit::down-arrow {
image: none;
border-left: 4px solid transparent; border-left: 4px solid transparent;
border-right: 4px solid transparent; border-right: 4px solid transparent;
border-top: 5px solid #6a7898; border-top: 5px solid #6a7898;
@@ -255,7 +253,7 @@ QComboBox:disabled {
/* ---- Check Box / Radio Button ---- */ /* ---- Check Box / Radio Button ---- */
QCheckBox, QCheckBox,
QRadioButton { QRadioButton {
spacing: 8px; spacing: 5px;
color: #1a2740; color: #1a2740;
} }
QCheckBox::indicator, QCheckBox::indicator,
@@ -263,9 +261,14 @@ QRadioButton::indicator {
border-style: solid; border-style: solid;
border-color: #90a4c4; border-color: #90a4c4;
border-width: 2px; border-width: 2px;
border-radius: 3px;
background-color: #ffffff; background-color: #ffffff;
} }
QCheckBox::indicator {
border-radius: 3px;
}
QRadioButton::indicator {
border-radius: 7px;
}
QCheckBox::indicator:hover, QCheckBox::indicator:hover,
QRadioButton::indicator:hover { QRadioButton::indicator:hover {
border-color: #0ea58a; border-color: #0ea58a;
@@ -274,9 +277,6 @@ QCheckBox::indicator:checked {
background-color: #0ea58a; background-color: #0ea58a;
border-color: #0ea58a; border-color: #0ea58a;
} }
QRadioButton::indicator {
border-radius: 10px;
}
QRadioButton::indicator:checked { QRadioButton::indicator:checked {
background-color: #0ea58a; background-color: #0ea58a;
border-color: #0ea58a; border-color: #0ea58a;
@@ -330,20 +330,14 @@ QTableWidget::indicator:indeterminate {
/* ---- Group Box ---- */ /* ---- Group Box ---- */
QGroupBox { QGroupBox {
margin-top: 5px;
padding-top: 15px;
color: #4a6080;
font-weight: bold;
border-style: solid; border-style: solid;
border-color: #c0cdda; border-color: #c0cdda;
border-width: 1px; border-width: 1px;
border-radius: 6px; border-radius: 5px;
margin-top: 12px;
padding-top: 14px;
color: #1a2740;
font-weight: bold;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 12px;
padding: 0 6px;
color: #4a6080;
} }
/* ---- Tab ---- */ /* ---- Tab ---- */
@@ -372,10 +366,6 @@ QTabBar::tab:selected {
color: #0ea58a; color: #0ea58a;
border-bottom: 2px solid #0ea58a; border-bottom: 2px solid #0ea58a;
} }
QTabBar::tab:hover:!selected {
background-color: #d5dde8;
color: #1a2740;
}
/* ---- List / Tree ---- */ /* ---- List / Tree ---- */
QListWidget, QListWidget,
@@ -393,18 +383,10 @@ QTableWidget {
QListWidget::item, QListWidget::item,
QTreeWidget::item, QTreeWidget::item,
QTableWidget::item { QTableWidget::item {
padding: 5px 10px; padding: 5px 5px;
border: none;
}
QListWidget::item:selected,
QTreeWidget::item:selected,
QTableWidget::item:selected {
background-color: #0ea58a;
color: #ffffff;
} }
QHeaderView::section { QHeaderView::section {
background-color: #dce4ee; background-color: #dce4ee;
border: none;
border-right: 1px solid #c0cdda; border-right: 1px solid #c0cdda;
border-bottom: 1px solid #c0cdda; border-bottom: 1px solid #c0cdda;
padding: 5px 10px; padding: 5px 10px;
+2 -2
View File
@@ -1956,13 +1956,13 @@
<widget class="QPushButton" name="ExportConfigButton"> <widget class="QPushButton" name="ExportConfigButton">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>120</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>100</width> <width>120</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
+267 -264
View File
@@ -115,9 +115,9 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>-51</y>
<width>450</width> <width>397</width>
<height>380</height> <height>434</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="AppearancePageLayout"> <layout class="QVBoxLayout" name="AppearancePageLayout">
@@ -138,305 +138,308 @@
</property> </property>
<item> <item>
<widget class="QGroupBox" name="AppearanceGroupBox"> <widget class="QGroupBox" name="AppearanceGroupBox">
<property name="title"> <property name="title">
<string>主题模式</string> <string>主题模式</string>
</property> </property>
<layout class="QVBoxLayout" name="AppearanceGroupBoxLayout"> <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"> <property name="spacing">
<number>5</number> <number>5</number>
</property> </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> <item>
<widget class="QLabel" name="StyleSelectLabel"> <widget class="QRadioButton" name="LightThemeRadio">
<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"> <property name="text">
<string>应用程序样式:</string> <string>浅色</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QComboBox" name="StyleComboBox"> <widget class="QRadioButton" name="DarkThemeRadio">
<property name="minimumSize"> <property name="text">
<size> <string>深色</string>
<width>160</width> </property>
<height>25</height> </widget>
</size> </item>
<item>
<widget class="QRadioButton" name="SystemThemeRadio">
<property name="text">
<string>跟随系统</string>
</property>
<property name="checked">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</item> </widget>
<item> </item>
<widget class="QLabel" name="StyleHintLabel"> <item>
<property name="text"> <widget class="QGroupBox" name="InterfaceGroupBox">
<string>更改样式将在下次启动应用程序时生效。</string> <property name="title">
</property> <string>界面风格</string>
</widget> </property>
</item> <layout class="QVBoxLayout" name="InterfaceGroupBoxLayout">
</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"> <property name="spacing">
<number>5</number> <number>5</number>
</property> </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> <item>
<widget class="QComboBox" name="ThemeComboBox"> <layout class="QHBoxLayout" name="StyleSelectLayout">
<property name="minimumSize"> <property name="spacing">
<size> <number>5</number>
<width>160</width> </property>
<height>25</height> <item>
</size> <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="CustomThemeGroupBox">
<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="CustomThemeHintLabel">
<property name="text">
<string>选择一个主题,或导入新的主题文件:</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLineEdit" name="QssPathEdit"> <layout class="QHBoxLayout" name="CustomThemePathLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QComboBox" name="CustomThemeComboBox">
<property name="minimumSize">
<size>
<width>160</width>
<height>25</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="CustomThemePathEdit">
<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="ImportCustomThemeButton">
<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="RemoveCustomThemeButton">
<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="CustomThemeInfoLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
<height>25</height> <height>60</height>
</size> </size>
</property> </property>
<property name="visible"> <property name="text">
<bool>false</bool> <string/>
</property> </property>
<property name="placeholderText"> <property name="textFormat">
<string>选择或输入 QSS 样式表文件路径...</string> <enum>Qt::TextFormat::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QPushButton" name="BrowseQssButton"> <layout class="QHBoxLayout" name="CustomThemeActionLayout">
<property name="minimumSize"> <property name="spacing">
<size> <number>5</number>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property> </property>
<item>
<widget class="QPushButton" name="ResetCustomThemeButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>重置主题</string>
</property>
</widget>
</item>
<item>
<spacer name="CustomThemeActionSpacer">
<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="CustomThemeStatusLabel">
<property name="text"> <property name="text">
<string>...</string> <string>当前使用程序 默认 外观。</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</item> </widget>
<item> </item>
<widget class="QLabel" name="ThemeInfoLabel"> <item>
<property name="minimumSize"> <spacer name="AppearancePageSpacer">
<size> <property name="orientation">
<width>0</width> <enum>Qt::Orientation::Vertical</enum>
<height>60</height> </property>
</size> <property name="sizeHint" stdset="0">
</property> <size>
<property name="text"> <width>20</width>
<string/> <height>40</height>
</property> </size>
<property name="textFormat"> </property>
<enum>Qt::TextFormat::RichText</enum> </spacer>
</property> </item>
<property name="alignment"> </layout>
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignTop</set> </widget>
</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>
</widget> </item>
</item> </layout>
</layout> </item>
</item>
<item> <item>
<layout class="QHBoxLayout" name="ButtonLayout"> <layout class="QHBoxLayout" name="ButtonLayout">
<property name="spacing"> <property name="spacing">
+87 -53
View File
@@ -9,9 +9,7 @@ See the LICENSE file for details.
""" """
import os import os
import shutil import shutil
import tempfile
import threading import threading
import zipfile
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
@@ -23,9 +21,9 @@ from interfaces.ConfigProvider import CfgKey
from managers.config.ConfigManager import instance as configInstance from managers.config.ConfigManager import instance as configInstance
from managers.log.LogManager import instance as logInstance from managers.log.LogManager import instance as logInstance
from utils.ThemeUtils import ( from utils.ThemeUtils import (
packTheme,
readThemeInfo, readThemeInfo,
unpackTheme, readThemeQss,
validateTheme,
wrapQssToAtheme wrapQssToAtheme
) )
@@ -82,6 +80,70 @@ class ThemeManager:
else: else:
return Qt.ColorScheme.Unknown return Qt.ColorScheme.Unknown
def _deleteThemeFile(
self,
name: str
):
"""
Delete a theme file in the themes storage directory.
The caller must hold self.__lock before invoking this method.
**This method ONLY deletes the file**.
"""
filepath = os.path.join(self.__themes_dir, name + ".altheme")
if os.path.isfile(filepath):
os.remove(filepath)
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._deleteThemeFile(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 themesDir( def themesDir(
self self
) -> str: ) -> str:
@@ -119,35 +181,21 @@ class ThemeManager:
if not os.path.isfile(source_path): if not os.path.isfile(source_path):
raise FileNotFoundError(source_path) raise FileNotFoundError(source_path)
ext = os.path.splitext(source_path)[1].lower() base_name, ext = os.path.splitext(os.path.basename(source_path))
ext = ext.lower()
with self.__lock: with self.__lock:
if ext == ".qss": if ext == ".qss":
name = os.path.splitext(os.path.basename(source_path))[0] dest_path = self._resolveDestPath(base_name, "未知作者")
dest_path = os.path.join(self.__themes_dir, name + ".altheme")
if os.path.exists(dest_path):
raise ValueError(f"主题 '{name}' 已存在")
wrapQssToAtheme(source_path, dest_path, "both") wrapQssToAtheme(source_path, dest_path, "both")
return name return os.path.splitext(os.path.basename(dest_path))[0]
elif ext == ".altheme": elif ext == ".altheme":
with zipfile.ZipFile(source_path, "r") as zf: info = validateTheme(source_path)
if "theme.qss" not in zf.namelist(): name = info.get("name", base_name)
raise ValueError("无效的 .altheme: 缺少 theme.qss")
info = readThemeInfo(source_path)
name = info.get("name", os.path.splitext(os.path.basename(source_path))[0])
safe_name = os.path.basename(name) safe_name = os.path.basename(name)
dest_path = os.path.join(self.__themes_dir, safe_name + ".altheme")
if os.path.exists(dest_path):
raise ValueError(f"主题 '{safe_name}' 已存在")
# Check for name collision with existing themes by the same author
new_author = info.get("author", "") new_author = info.get("author", "")
for existing in self.listThemes(): dest_path = self._resolveDestPath(safe_name, new_author)
if (existing.get("name", "") == safe_name
and existing.get("author", "") == new_author):
raise ValueError(
f"主题名称 '{safe_name}' (作者 '{new_author}') 已存在"
)
shutil.copy2(source_path, dest_path) shutil.copy2(source_path, dest_path)
return safe_name return os.path.splitext(os.path.basename(dest_path))[0]
else: else:
raise ValueError(f"不支持的文件类型: {ext}") raise ValueError(f"不支持的文件类型: {ext}")
@@ -172,10 +220,7 @@ class ThemeManager:
if filename.endswith(".altheme"): if filename.endswith(".altheme"):
filepath = os.path.join(self.__themes_dir, filename) filepath = os.path.join(self.__themes_dir, filename)
try: try:
info = readThemeInfo(filepath) info = validateTheme(filepath)
with zipfile.ZipFile(filepath, "r") as zf:
if "theme.qss" not in zf.namelist():
raise ValueError("缺少 theme.qss")
name = info.get("name", "") name = info.get("name", "")
author = info.get("author", "") author = info.get("author", "")
key = (name, author) key = (name, author)
@@ -185,6 +230,7 @@ class ThemeManager:
) )
continue continue
seen_keys.add(key) seen_keys.add(key)
info["file"] = os.path.splitext(filename)[0]
themes.append(info) themes.append(info)
except Exception as e: except Exception as e:
logInstance().getLogger("ThemeManager").warning( logInstance().getLogger("ThemeManager").warning(
@@ -204,22 +250,21 @@ class ThemeManager:
Remove a theme by name. Remove a theme by name.
If the removed theme is currently active, clears the QSS If the removed theme is currently active, clears the QSS
stylesheet from the application. stylesheet from the application and reverts to the saved
colour scheme.
Args: Args:
name (str): The theme name to remove. name (str): The theme name to remove.
""" """
filepath = os.path.join(self.__themes_dir, name + ".altheme")
with self.__lock: with self.__lock:
if os.path.isfile(filepath): self._deleteThemeFile(name)
os.remove(filepath) if self.__current_theme_name == name:
if self.__current_theme_name == name: self.__current_theme_name = ""
self.__current_theme_name = "" saved_theme = configInstance().get(
saved_theme = configInstance().get( CfgKey.GLOBAL.APPEARANCE.THEME, "system"
CfgKey.GLOBAL.APPEARANCE.THEME, "system" )
) self.clearTheme(saved_theme)
self.clearTheme(saved_theme)
def applyTheme( def applyTheme(
self, self,
@@ -244,21 +289,10 @@ class ThemeManager:
raise FileNotFoundError(filepath) raise FileNotFoundError(filepath)
with self.__lock: with self.__lock:
info = readThemeInfo(filepath) info = readThemeInfo(filepath)
with tempfile.TemporaryDirectory() as tmpdir: qss = readThemeQss(filepath)
unpackTheme(filepath, tmpdir)
qss_path = os.path.join(tmpdir, "theme.qss")
if os.path.isfile(qss_path):
with open(qss_path, "r", encoding="utf-8") as fh:
qss = fh.read()
app = QApplication.instance()
if app:
app.setStyleSheet(qss)
else:
raise ValueError(
f"主题 '{name}' 的 .altheme 文件中缺少 theme.qss"
)
app = QApplication.instance() app = QApplication.instance()
if app: if app:
app.setStyleSheet(qss)
need_theme = info.get("need_theme", "both") need_theme = info.get("need_theme", "both")
app.styleHints().setColorScheme( app.styleHints().setColorScheme(
ThemeManager._colorSchemeFor(need_theme) ThemeManager._colorSchemeFor(need_theme)
+78 -10
View File
@@ -37,7 +37,6 @@ def packTheme(
zf.writestr("info.json", json.dumps(info, ensure_ascii=False, indent=4)) zf.writestr("info.json", json.dumps(info, ensure_ascii=False, indent=4))
zf.write(qss_path, "theme.qss") zf.write(qss_path, "theme.qss")
def unpackTheme( def unpackTheme(
altheme_path: str, altheme_path: str,
output_dir: str output_dir: str
@@ -65,22 +64,24 @@ def unpackTheme(
raise ValueError(f"不安全的 .altheme 入口: {name}") raise ValueError(f"不安全的 .altheme 入口: {name}")
zf.extractall(output_dir) zf.extractall(output_dir)
def readThemeInfo( def readThemeInfo(
altheme_path: str altheme_path: str
) -> dict: ) -> dict:
""" """
Read only the info.json metadata from a .altheme file. Read and validate the info.json metadata from a .altheme file.
Verifies that all required fields (name, author, need_theme, brief)
are present with valid values.
Args: Args:
altheme_path (str): Path to the .altheme file. altheme_path (str): Path to the .altheme file.
Returns: Returns:
dict: The theme metadata dictionary. dict: The validated theme metadata dictionary.
Raises: Raises:
FileNotFoundError: If altheme_path does not exist. FileNotFoundError: If altheme_path does not exist.
ValueError: If the .altheme does not contain info.json. ValueError: If info.json is missing or any field is invalid.
""" """
if not os.path.isfile(altheme_path): if not os.path.isfile(altheme_path):
@@ -89,11 +90,78 @@ def readThemeInfo(
if "info.json" not in zf.namelist(): if "info.json" not in zf.namelist():
raise ValueError("无效的 .altheme: 缺少 info.json") raise ValueError("无效的 .altheme: 缺少 info.json")
with zf.open("info.json") as fh: with zf.open("info.json") as fh:
info = json.loads(fh.read().decode("utf-8")) try:
if "name" not in info: info = json.loads(fh.read().decode("utf-8"))
raise ValueError("无效的 .altheme: info.json 缺少 'name' 字段") except (json.JSONDecodeError, UnicodeDecodeError) as e:
return info 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 that info.json does not drift from the
# "未知作者" filename fallback used by wrapQssToAtheme
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' 字段")
return info
def readThemeQss(
altheme_path: str
) -> str:
"""
Read the theme.qss content from a .altheme archive.
Args:
altheme_path (str): Path to the .altheme file.
Returns:
str: The non-empty QSS stylesheet content.
Raises:
FileNotFoundError: If altheme_path does not exist.
ValueError: If theme.qss is missing or empty.
"""
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")
qss = zf.read("theme.qss").decode("utf-8")
if not qss.strip():
raise ValueError("无效的 .altheme: theme.qss 为空")
return qss
def validateTheme(
altheme_path: str
) -> dict:
"""
Fully validate a .altheme file and return its metadata.
Delegates info validation to readThemeInfo and QSS validation
to readThemeQss, then additionally checks that theme.qss is
non-empty.
Args:
altheme_path (str): Path to the .altheme file.
Returns:
dict: The validated theme metadata dictionary.
Raises:
FileNotFoundError: If altheme_path does not exist.
ValueError: If validation fails for any reason.
"""
info = readThemeInfo(altheme_path)
readThemeQss(altheme_path) # validates existence and non-empty
return info
def wrapQssToAtheme( def wrapQssToAtheme(
qss_path: str, qss_path: str,
@@ -119,7 +187,7 @@ def wrapQssToAtheme(
filename = os.path.splitext(os.path.basename(qss_path))[0] filename = os.path.splitext(os.path.basename(qss_path))[0]
info = { info = {
"name": filename, "name": filename,
"author": "未知", "author": "未知作者",
"need_theme": current_theme, "need_theme": current_theme,
"brief": "没有相关简介" "brief": "没有相关简介"
} }