mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-20 00:13:02 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c250fa4a6e | |||
| 8f8e3e4ba7 | |||
| 88a74a7a47 | |||
| 5552af1345 | |||
| f9175371dc | |||
| 8e1b28f3fe | |||
| 57f1cfb3f2 | |||
| 007b4dc2ef | |||
| 67f297b434 | |||
| 86f0761eed |
Binary file not shown.
@@ -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("复制脚本")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -252,7 +252,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="CustomQssGroupBox">
|
<widget class="QGroupBox" name="CustomThemeGroupBox">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>自定义外观</string>
|
<string>自定义外观</string>
|
||||||
</property>
|
</property>
|
||||||
@@ -273,7 +273,7 @@
|
|||||||
<number>3</number>
|
<number>3</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="CustomQssHintLabel">
|
<widget class="QLabel" name="CustomThemeHintLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>选择一个主题,或导入新的主题文件:</string>
|
<string>选择一个主题,或导入新的主题文件:</string>
|
||||||
</property>
|
</property>
|
||||||
@@ -283,12 +283,12 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="QssPathLayout">
|
<layout class="QHBoxLayout" name="CustomThemePathLayout">
|
||||||
<property name="spacing">
|
<property name="spacing">
|
||||||
<number>5</number>
|
<number>5</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QComboBox" name="ThemeComboBox">
|
<widget class="QComboBox" name="CustomThemeComboBox">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>160</width>
|
<width>160</width>
|
||||||
@@ -298,7 +298,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLineEdit" name="QssPathEdit">
|
<widget class="QLineEdit" name="CustomThemePathEdit">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>0</width>
|
<width>0</width>
|
||||||
@@ -314,7 +314,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="BrowseQssButton">
|
<widget class="QPushButton" name="ImportCustomThemeButton">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>25</width>
|
<width>25</width>
|
||||||
@@ -328,14 +328,33 @@
|
|||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>...</string>
|
<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>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="ThemeInfoLabel">
|
<widget class="QLabel" name="CustomThemeInfoLabel">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>0</width>
|
<width>0</width>
|
||||||
@@ -349,7 +368,7 @@
|
|||||||
<enum>Qt::TextFormat::RichText</enum>
|
<enum>Qt::TextFormat::RichText</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="alignment">
|
||||||
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignTop</set>
|
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
|
||||||
</property>
|
</property>
|
||||||
<property name="wordWrap">
|
<property name="wordWrap">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
@@ -357,28 +376,12 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="QssActionLayout">
|
<layout class="QHBoxLayout" name="CustomThemeActionLayout">
|
||||||
<property name="spacing">
|
<property name="spacing">
|
||||||
<number>5</number>
|
<number>5</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="ApplyQssButton">
|
<widget class="QPushButton" name="ResetCustomThemeButton">
|
||||||
<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">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>80</width>
|
<width>80</width>
|
||||||
@@ -391,7 +394,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="QssActionSpacer">
|
<spacer name="CustomThemeActionSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
@@ -406,7 +409,7 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="QssStatusLabel">
|
<widget class="QLabel" name="CustomThemeStatusLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>当前使用程序 默认 外观。</string>
|
<string>当前使用程序 默认 外观。</string>
|
||||||
</property>
|
</property>
|
||||||
|
|||||||
@@ -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,16 +250,15 @@ 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(
|
||||||
@@ -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()
|
app = QApplication.instance()
|
||||||
if app:
|
if app:
|
||||||
app.setStyleSheet(qss)
|
app.setStyleSheet(qss)
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"主题 '{name}' 的 .altheme 文件中缺少 theme.qss"
|
|
||||||
)
|
|
||||||
app = QApplication.instance()
|
|
||||||
if app:
|
|
||||||
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)
|
||||||
|
|||||||
+76
-8
@@ -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:
|
||||||
|
try:
|
||||||
info = json.loads(fh.read().decode("utf-8"))
|
info = json.loads(fh.read().decode("utf-8"))
|
||||||
if "name" not in info:
|
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||||
raise ValueError("无效的 .altheme: info.json 缺少 'name' 字段")
|
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
|
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": "没有相关简介"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user