niumasoftware/ui/settings_window.py
2026-04-05 22:43:43 +08:00

542 lines
20 KiB
Python

import sys
import os
from PyQt6.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QListWidget, QListWidgetItem,
QStackedWidget, QWidget, QLabel, QSlider, QPushButton,
QCheckBox, QGroupBox, QFormLayout, QSpinBox, QFrame
)
from PyQt6.QtCore import Qt, QSize
from PyQt6.QtWidgets import QMessageBox
from PyQt6.QtGui import QIcon, QPixmap
import qtawesome as qta
from db import database
import ui.theme as theme
import ui.dialog_style as dialog_style
class SettingsWindow(QDialog):
def __init__(self, panel, parent=None):
super().__init__(parent)
self._panel = panel
self.setWindowTitle("设置")
# 适当放大:让页面内容更舒展,并限制最小拖拽尺寸
self.setMinimumSize(500, 500)
self.resize(760, 520)
self.setWindowFlags(
Qt.WindowType.Dialog |
Qt.WindowType.WindowCloseButtonHint
)
self._build_ui()
self._apply_theme()
def _build_ui(self):
root = QHBoxLayout(self)
root.setContentsMargins(0, 0, 0, 0)
root.setSpacing(0)
# ── 左侧导航 ─────────────────────────────────────
self.nav = QListWidget()
self.nav.setFixedWidth(130)
self.nav.setSpacing(2)
self.nav.setIconSize(QSize(18, 18))
self.nav.setStyleSheet("""
QListWidget { border:none; outline:none; padding:8px 4px; }
QListWidget::item { height:36px; border-radius:6px;
padding-left:10px; font-size:13px; }
QListWidget::item:selected { background:#4a9eff; color:white; }
QListWidget::item:hover:!selected { background:rgba(128,128,128,40); }
""")
pages = [
("fa5s.paint-brush", "外观"),
("fa5s.window-maximize", "窗口"),
("fa5s.rocket", "启动"),
("fa5s.heart", "捐赠"),
("fa5s.eraser", "初始化"),
]
self._nav_ready = False
self._block_nav = False
for icon_name, label in pages:
try:
icon = qta.icon(icon_name, color="#888")
item = QListWidgetItem(icon, label)
except Exception:
# 防止图标名不存在导致设置页直接报错
icon = qta.icon("fa5s.rocket", color="#888")
item = QListWidgetItem(icon, label)
if label == "初始化":
item.setData(Qt.ItemDataRole.UserRole, "init")
self.nav.addItem(item)
self.nav.setCurrentRow(0)
self.nav.currentRowChanged.connect(self._on_nav)
# ── 右侧内容区 ───────────────────────────────────
self.stack = QStackedWidget()
self.stack.addWidget(self._page_appearance())
self.stack.addWidget(self._page_window())
self.stack.addWidget(self._page_startup())
self.stack.addWidget(self._page_donate())
self.stack.addWidget(self._page_initialization())
# 此时 stack 已就绪,允许导航回调正常工作
self._nav_ready = True
self._last_nav_index = self.nav.currentRow()
self.stack.setCurrentIndex(self._last_nav_index)
# 分割线
line = QFrame()
line.setFrameShape(QFrame.Shape.VLine)
line.setStyleSheet("color: rgba(128,128,128,40);")
root.addWidget(self.nav)
root.addWidget(line)
root.addWidget(self.stack)
# ── 外观页 ───────────────────────────────────────────
def _page_appearance(self) -> QWidget:
page = QWidget()
layout = QVBoxLayout(page)
layout.setContentsMargins(24, 20, 24, 20)
layout.setSpacing(20)
layout.addWidget(self._section_title("主题"))
theme_row = QHBoxLayout()
theme_row.setSpacing(12)
self._dark_btn = self._theme_card("🌙 暗色", "dark")
self._light_btn = self._theme_card("☀️ 亮色", "light")
self._dark_btn.clicked.connect(lambda: self._set_theme("dark"))
self._light_btn.clicked.connect(lambda: self._set_theme("light"))
theme_row.addWidget(self._dark_btn)
theme_row.addWidget(self._light_btn)
theme_row.addStretch()
layout.addLayout(theme_row)
self._update_theme_cards()
layout.addWidget(self._divider())
layout.addWidget(self._section_title("透明度"))
opacity_row = QHBoxLayout()
opacity_row.setSpacing(12)
self._opacity_slider = QSlider(Qt.Orientation.Horizontal)
self._opacity_slider.setRange(30, 100)
saved = int(database.get_setting("panel_opacity", "100"))
self._opacity_slider.setValue(saved)
self._opacity_slider.setFixedWidth(220)
self._opacity_val = QLabel(f"{saved}%")
self._opacity_val.setFixedWidth(36)
self._opacity_slider.valueChanged.connect(self._on_opacity)
opacity_row.addWidget(self._opacity_slider)
opacity_row.addWidget(self._opacity_val)
opacity_row.addStretch()
layout.addLayout(opacity_row)
layout.addStretch()
return page
def _theme_card(self, text: str, theme_name: str) -> QPushButton:
btn = QPushButton(text)
btn.setFixedSize(110, 44)
btn.setCheckable(True)
btn.setProperty("theme_name", theme_name)
return btn
def _update_theme_cards(self):
cur = theme.name()
for btn in (self._dark_btn, self._light_btn):
active = btn.property("theme_name") == cur
btn.setChecked(active)
if active:
btn.setStyleSheet("""
QPushButton { background:#4a9eff; color:white;
border-radius:8px; font-size:13px;
border:2px solid #4a9eff; }
""")
else:
t = theme.current()
btn.setStyleSheet(f"""
QPushButton {{ background:{t['search_bg']}; color:{t['search_color']};
border-radius:8px; font-size:13px;
border:1px solid {t['search_border']}; }}
QPushButton:hover {{ border-color:#4a9eff; }}
""")
def _set_theme(self, name: str):
theme.set_theme(name)
self._panel._apply_theme()
self._update_theme_cards()
self._apply_theme()
def _on_opacity(self, val: int):
self._panel.setWindowOpacity(val / 100)
self._opacity_val.setText(f"{val}%")
database.set_setting("panel_opacity", str(val))
# ── 窗口页 ───────────────────────────────────────────
def _page_window(self) -> QWidget:
page = QWidget()
layout = QVBoxLayout(page)
layout.setContentsMargins(24, 20, 24, 20)
layout.setSpacing(16)
layout.addWidget(self._section_title("窗口行为"))
self._pin_check = QCheckBox("固定面板(置顶、不自动隐藏)")
self._pin_check.setChecked(self._panel._pinned)
self._pin_check.toggled.connect(self._on_pin)
layout.addWidget(self._pin_check)
layout.addWidget(self._divider())
layout.addWidget(self._section_title("图标大小"))
size_row = QHBoxLayout()
size_row.setSpacing(12)
size_lbl = QLabel("图标宽度:")
self._icon_size_spin = QSpinBox()
self._icon_size_spin.setRange(48, 120)
self._icon_size_spin.setSingleStep(4)
saved_size = int(database.get_setting("icon_width", "72"))
self._icon_size_spin.setValue(saved_size)
self._icon_size_spin.setSuffix(" px")
self._icon_size_spin.setFixedWidth(90)
apply_btn = QPushButton("应用")
apply_btn.setFixedWidth(60)
apply_btn.clicked.connect(self._on_icon_size)
size_row.addWidget(size_lbl)
size_row.addWidget(self._icon_size_spin)
size_row.addWidget(apply_btn)
size_row.addStretch()
layout.addLayout(size_row)
layout.addStretch()
return page
def _on_pin(self, checked: bool):
self._panel._pinned = checked
self._panel._sync_pin_dependent_ui()
def _on_icon_size(self):
val = self._icon_size_spin.value()
database.set_setting("icon_width", str(val))
# 通知所有 ItemWidget 更新尺寸
from ui.flow_layout import ITEM_W
import ui.flow_layout as fl
fl.ITEM_W = val
self._panel.refresh_groups()
# ── 启动页 ───────────────────────────────────────────
def _page_startup(self) -> QWidget:
page = QWidget()
layout = QVBoxLayout(page)
layout.setContentsMargins(24, 20, 24, 20)
layout.setSpacing(16)
layout.addWidget(self._section_title("开机启动"))
self._autostart_check = QCheckBox("开机自动启动")
self._autostart_check.setChecked(self._is_autostart_enabled())
self._autostart_check.toggled.connect(self._on_autostart)
layout.addWidget(self._autostart_check)
layout.addWidget(self._divider())
layout.addWidget(self._section_title("关于"))
about = QLabel("桌面文件整理 v1.0\n整理你的桌面快捷方式,保持桌面干净。")
about.setStyleSheet("color:#888; font-size:12px; line-height:1.6;")
layout.addWidget(about)
layout.addStretch()
return page
# ── 捐赠页 ───────────────────────────────────────────
def _page_donate(self) -> QWidget:
page = QWidget()
layout = QVBoxLayout(page)
layout.setContentsMargins(24, 20, 24, 20)
layout.setSpacing(16)
layout.addWidget(self._section_title("捐赠支持"))
info = QLabel("如果你喜欢这个软件,欢迎通过二维码支持开发者持续优化与维护。你的鼓励会成为重要的动力。<br>感谢你的支持!")
info.setWordWrap(True)
info.setStyleSheet("font-size:14px; line-height:1.1; background:transparent;")
layout.addWidget(info)
layout.addWidget(self._divider())
card = QFrame()
card.setObjectName("donate_card")
grid = QHBoxLayout(card)
grid.setContentsMargins(14, 14, 14, 14)
grid.setSpacing(18)
donate_dir = os.path.join(
os.path.dirname(os.path.dirname(__file__)), "assets", "imgs", "donate"
)
wx_path = os.path.join(donate_dir, "wx.jpg")
zfb_path = os.path.join(donate_dir, "zfb.jpg")
def _qr_column(img_path: str, caption: str) -> QVBoxLayout:
col = QVBoxLayout()
col.setContentsMargins(0, 0, 0, 0)
col.setSpacing(8)
img = QLabel()
img.setFixedSize(170, 170)
img.setAlignment(Qt.AlignmentFlag.AlignCenter)
img.setStyleSheet("background:transparent; border-radius:12px;")
pm = QPixmap(img_path)
if not pm.isNull():
img.setPixmap(
pm.scaled(
img.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
)
img.setCursor(Qt.CursorShape.PointingHandCursor)
img.setToolTip("点击放大")
# 直接覆盖点击事件:点击二维码弹出大图
img.mousePressEvent = ( # type: ignore[assignment]
lambda e, p=img_path: self._open_donate_image(p)
)
cap = QLabel(caption)
cap.setAlignment(Qt.AlignmentFlag.AlignCenter)
cap.setStyleSheet("font-size:12px; font-weight:700; background:transparent;")
col.addWidget(img)
col.addWidget(cap)
return col
grid.addLayout(_qr_column(wx_path, "微信支持"))
grid.addLayout(_qr_column(zfb_path, "支付宝支持"))
layout.addWidget(card)
# 供 _apply_theme 动态设置样式
self._donate_card = card
return page
def _page_initialization(self) -> QWidget:
page = QWidget()
layout = QVBoxLayout(page)
layout.setContentsMargins(24, 20, 24, 20)
layout.setSpacing(16)
layout.addWidget(self._section_title("初始化"))
desc = QLabel(
"点击左侧「初始化」页签后,将弹出确认提醒。\n"
"确认后会清空所有分组和程序(不可撤销)。"
)
desc.setWordWrap(True)
desc.setStyleSheet("color:#888; font-size:13px; line-height:1.6;")
layout.addWidget(desc)
self._init_status_lbl = QLabel("")
self._init_status_lbl.setStyleSheet("font-size:12px; color:#f90;")
layout.addWidget(self._init_status_lbl)
layout.addStretch()
return page
def _open_donate_image(self, img_path: str):
"""点击二维码后的弹窗大图查看。"""
t = theme.current()
d = QDialog(self)
d.setWindowTitle("二维码放大查看")
d.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowCloseButtonHint)
d.setFixedSize(800, 600)
d.setStyleSheet(f"""
QDialog {{
background: {t['panel_bg']};
border: 1px solid {t['panel_border']};
border-radius: 10px;
color: {t['search_color']};
}}
""")
layout = QVBoxLayout(d)
layout.setContentsMargins(14, 14, 14, 14)
layout.setSpacing(0)
lbl = QLabel()
lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
lbl.setStyleSheet("background:transparent;")
lbl.setScaledContents(False) # 不拉伸,保持等比
pm = QPixmap(img_path)
if not pm.isNull():
# 适配框体:保持等比缩放(不会被拉伸)
target_w = d.width() - 28
target_h = d.height() - 28
lbl.setPixmap(
pm.scaled(
target_w,
target_h,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
)
layout.addWidget(lbl)
d.exec()
def _is_autostart_enabled(self) -> bool:
try:
import winreg
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Run",
0, winreg.KEY_READ
)
winreg.QueryValueEx(key, "DesktopOrganizer")
winreg.CloseKey(key)
return True
except Exception:
return False
def _on_autostart(self, checked: bool):
from ui.dock import _set_autostart
_set_autostart(checked)
# ── 工具方法 ─────────────────────────────────────────
def _section_title(self, text: str) -> QLabel:
lbl = QLabel(text)
lbl.setStyleSheet("font-size:14px; font-weight:bold;")
return lbl
def _divider(self) -> QFrame:
line = QFrame()
line.setFrameShape(QFrame.Shape.HLine)
line.setStyleSheet("color: rgba(128,128,128,40);")
return line
def _on_nav(self, index: int):
if self._block_nav:
return
if not getattr(self, "_nav_ready", False) or not hasattr(self, "stack"):
return
item = self.nav.item(index)
is_init_tab = bool(item and item.data(Qt.ItemDataRole.UserRole) == "init")
if not is_init_tab:
self.stack.setCurrentIndex(index)
self._last_nav_index = index
return
ret = dialog_style.question(
self,
"初始化",
"提醒请谨慎操作!!!!!\n\n"
"此操作会清空所有分组和程序,且无法撤销。是否继续?",
icon=QMessageBox.Icon.Warning,
)
if ret == QMessageBox.StandardButton.Yes:
try:
self._panel.reset_groups_and_items()
except Exception as e:
# 初始化失败也要尽量让用户知道原因
dialog_style.warning(self, "初始化失败", f"清空数据失败:{e}")
# 初始化失败则回退到上一个页签
self._block_nav = True
self.nav.setCurrentRow(self._last_nav_index)
self._block_nav = False
self.stack.setCurrentIndex(self._last_nav_index)
return
if hasattr(self, "_init_status_lbl"):
self._init_status_lbl.setText("已完成初始化(分组和程序已清空)。")
self.stack.setCurrentIndex(index)
self._last_nav_index = index
else:
# 用户取消:回退到上一个页签
self._block_nav = True
self.nav.setCurrentRow(self._last_nav_index)
self._block_nav = False
self.stack.setCurrentIndex(self._last_nav_index)
def _apply_theme(self):
t = theme.current()
is_dark = theme.name() == "dark"
# 把 rgba(r,g,b,a) 转成 rgb(r,g,b)
panel_bg = t["panel_bg"]
if panel_bg.startswith("rgba"):
tmp = panel_bg.replace("rgba", "rgb")
# "rgb(r,g,b,a)" -> 取前三项 "rgb(r,g,b"
head = tmp.rsplit(",", 1)[0]
panel_rgb = head + ")"
else:
panel_rgb = panel_bg
self.setStyleSheet(f"""
QDialog, QWidget {{
background: {panel_rgb};
color: {t['search_color']};
}}
QListWidget {{
background: {'rgba(0,0,0,30)' if is_dark else 'rgba(0,0,0,6)'};
}}
QSlider::groove:horizontal {{
height:4px; background:{t['scrollbar']}; border-radius:2px;
}}
QSlider::handle:horizontal {{
width:14px; height:14px; margin:-5px 0;
background:#4a9eff; border-radius:7px;
}}
QSlider::sub-page:horizontal {{
background:#4a9eff; border-radius:2px;
}}
QCheckBox {{ font-size:13px; spacing:8px; }}
QCheckBox::indicator {{
width:18px; height:18px; border-radius:4px;
border:1px solid {t['search_border']};
background:{t['search_bg']};
}}
QCheckBox::indicator:checked {{
background:#4a9eff; border-color:#4a9eff;
image: url(none);
}}
QSpinBox {{
background:{t['search_bg']}; color:{t['search_color']};
border:1px solid {t['search_border']}; border-radius:5px;
padding:4px 8px; font-size:12px;
}}
QPushButton {{
background:{t['search_bg']}; color:{t['search_color']};
border:1px solid {t['search_border']}; border-radius:6px;
padding:6px 14px; font-size:12px;
}}
QPushButton:hover {{ border-color:#4a9eff; color:#4a9eff; }}
""")
# 导航背景
nav_bg = "rgba(0,0,0,30)" if is_dark else "rgba(0,0,0,6)"
self.nav.setStyleSheet(f"""
QListWidget {{ border:none; outline:none; padding:8px 4px;
background:{nav_bg}; }}
QListWidget::item {{ height:36px; border-radius:6px;
padding-left:10px; font-size:13px;
color:{t['search_color']}; }}
QListWidget::item:selected {{ background:#4a9eff; color:white; }}
QListWidget::item:hover:!selected {{
background:{'rgba(255,255,255,10)' if is_dark else 'rgba(0,0,0,6)'}; }}
""")
self._update_theme_cards()
# 捐赠卡片样式
if hasattr(self, "_donate_card"):
card_bg = "rgba(0,0,0,25)" if is_dark else "rgba(255,255,255,10)"
self._donate_card.setStyleSheet(f"""
QFrame#donate_card {{
background:{card_bg};
border: 1px solid {t['panel_border']};
border-radius:14px;
}}
""")