import sys import os import json import threading import socket import struct import time as _time import datetime import urllib.request import webbrowser import qtawesome as qta from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QPushButton, QApplication, QMenu, QSystemTrayIcon, QLineEdit, QLabel, QMessageBox, QGridLayout, ) from PyQt6.QtCore import ( Qt, QPoint, QPropertyAnimation, QEasingCurve, QTimer, QEvent, QSize, pyqtSignal, ) from PyQt6.QtGui import QPixmap, QPainter, QColor, QBrush, QPen, QIcon, QCursor from db import database from ui.group import GroupWidget import ui.theme as theme import ui.dialog_style as dialog_style from ui.updater import Updater from app_info import app_title PANEL_W = 260 PANEL_H = 40 MIN_W, MIN_H = 400, 5 ANIM_MS = 180 RESIZE_M = 8 class WeatherBox(QWidget): """顶部天气信息模块(重写:大图标 + 左侧文案 + 右侧温度布局)""" def __init__(self, parent=None): super().__init__(parent) self._weather_dir = os.path.join( os.path.dirname(os.path.dirname(__file__)), "assets", "imgs", "weather" ) # 顶部栏高度由 PanelWindow 强制对齐(weather_bar = 80) self.setFixedHeight(80) # 控制宽度,避免把右侧“网络时间”挤出 self.setFixedWidth(170) # 左侧:图标(大)+ 天气名称(小一点) self._left_widget = QWidget() self._left_widget.setFixedWidth(68) left_lay = QVBoxLayout(self._left_widget) left_lay.setContentsMargins(0, 0, 0, 0) left_lay.setSpacing(6) self._icon_label = QLabel() self._icon_label.setFixedSize(70, 70) self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self._icon_label.setStyleSheet("background:transparent;") self._weather_name_label = QLabel("") self._weather_name_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self._weather_name_label.setStyleSheet( "font-size:12px; font-weight:700; background:transparent;" ) self._weather_name_label.setFixedWidth(self._left_widget.width()) left_lay.addWidget(self._icon_label) left_lay.addWidget(self._weather_name_label) # 右侧:温度(大)+ 地区 + 风力 self._right_widget = QWidget() self._right_widget.setFixedWidth(102) right_lay = QVBoxLayout(self._right_widget) right_lay.setContentsMargins(0, 0, 0, 0) right_lay.setSpacing(4) self._temp_label = QLabel("--°C") self._temp_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) self._temp_label.setFixedWidth(self._right_widget.width()) self._temp_label.setStyleSheet( "font-weight:900; font-size:28px; background:transparent; padding:0px; margin:0px;" ) self._location_label = QLabel("") self._location_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) self._location_label.setFixedWidth(self._right_widget.width()) self._location_label.setStyleSheet( "font-size:12px; font-weight:700; background:transparent;" ) self._wind_label = QLabel("") self._wind_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) self._wind_label.setFixedWidth(self._right_widget.width()) self._wind_label.setStyleSheet( "font-size:12px; background:transparent;" ) right_lay.addWidget(self._temp_label) right_lay.addWidget(self._location_label) right_lay.addWidget(self._wind_label) lay = QHBoxLayout(self) lay.setContentsMargins(0, 0, 0, 0) lay.setSpacing(6) lay.addWidget(self._left_widget) lay.addWidget(self._right_widget) self._last_tooltip: str | None = None def set_theme_colors(self, color_fg: str): self._temp_label.setStyleSheet( f"font-weight:900; font-size:28px; color:{color_fg}; background:transparent; padding:0px; margin:0px;" ) self._weather_name_label.setStyleSheet( f"font-size:12px; font-weight:700; color:{color_fg}; background:transparent;" ) self._location_label.setStyleSheet( f"font-size:12px; font-weight:700; color:{color_fg}; background:transparent;" ) self._wind_label.setStyleSheet( f"font-size:12px; color:{color_fg}; background:transparent;" ) def _set_elided(self, label: QLabel, text: str): text = text or "" # 统一右侧省略,避免撑爆布局 fm = label.fontMetrics() label.setText( fm.elidedText(text, Qt.TextElideMode.ElideRight, max(10, label.width())) ) def _make_weather_icon(self, weather: str) -> QPixmap: wname = (weather or "").strip() img_path = os.path.join(self._weather_dir, f"{wname}.png") if os.path.exists(img_path): pm = QPixmap(img_path) if not pm.isNull(): target = self._icon_label.size() return pm.scaled( target.width(), target.height(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation, ) # fallback:透明背景的占位圆 pm = QPixmap(self._icon_label.size()) pm.fill(Qt.GlobalColor.transparent) p = QPainter(pm) p.setRenderHint(QPainter.RenderHint.Antialiasing) p.setPen(QPen(QColor("#888888"), 2)) p.drawEllipse(6, 6, pm.width() - 12, pm.height() - 12) p.end() return pm def update_weather(self, data: dict): if not data: return temp = data.get("temperature", "--") weather = str(data.get("weather", "") or "") province = str(data.get("province", "") or "") city = str(data.get("city", "") or "") district = str(data.get("district", "") or "") wind_direction = str(data.get("wind_direction", "") or "") wind_power = str(data.get("wind_power", "") or "") # 地点:优先 city,其次 province;如有 district 追加 location = city or province or "--" if district and district not in location: location = f"{location}{district}" # 温度:尽量显示为整数(接口一般是数字) if temp in ("", None): temp_text = "--°C" else: try: temp_text = f"{int(float(temp))}°C" except Exception: temp_text = f"{temp}°C" self._temp_label.setText(temp_text) self._icon_label.setPixmap(self._make_weather_icon(weather)) self._set_elided(self._weather_name_label, weather) self._set_elided(self._location_label, location) self._set_elided(self._wind_label, f"{wind_direction}{wind_power}".replace(" ", "")) def show_loading(self): self._temp_label.setText("--°C") self._icon_label.setPixmap(QPixmap(self._icon_label.size())) self._weather_name_label.setText("获取中…") self._location_label.setText("") self._wind_label.setText("") self._last_tooltip = None def _make_tray_icon(): logo = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png") if os.path.exists(logo): return QIcon(logo) px = QPixmap(32, 32) px.fill(Qt.GlobalColor.transparent) p = QPainter(px) p.setRenderHint(QPainter.RenderHint.Antialiasing) p.setBrush(QBrush(QColor("#4a9eff"))) p.setPen(Qt.PenStyle.NoPen) p.drawRoundedRect(2, 2, 28, 28, 6, 6) p.setBrush(QBrush(QColor("white"))) for y in [9, 15, 21]: p.drawRoundedRect(7, y, 18, 3, 1, 1) p.end() return QIcon(px) class GroupsContainer(QWidget): """支持分组拖拽排序的容器""" def __init__(self, parent=None): super().__init__(parent) self.setAcceptDrops(True) self._indicator_pos = -1 # 指示线位置(像素 y) def dragEnterEvent(self, event): if event.mimeData().hasFormat("application/x-group-id"): event.acceptProposedAction() else: event.ignore() def dragMoveEvent(self, event): if event.mimeData().hasFormat("application/x-group-id"): self._indicator_pos = event.position().toPoint().y() self.update() event.acceptProposedAction() def dragLeaveEvent(self, event): self._indicator_pos = -1 self.update() def dropEvent(self, event): self._indicator_pos = -1 self.update() if not event.mimeData().hasFormat("application/x-group-id"): event.ignore() return gid = int(event.mimeData().data("application/x-group-id").data().decode()) drop_y = event.position().toPoint().y() # 找到插入位置 layout = self.layout() insert_idx = layout.count() - 1 # 默认插到最后(stretch 前) for i in range(layout.count() - 1): item = layout.itemAt(i) if item and item.widget(): geo = item.widget().geometry() if drop_y < geo.center().y(): insert_idx = i break # 收集当前顺序,把拖动的移到 insert_idx ids = [] for i in range(layout.count() - 1): item = layout.itemAt(i) if item and item.widget(): ids.append(item.widget().group_data["id"]) if gid in ids: ids.remove(gid) ids.insert(min(insert_idx, len(ids)), gid) from db import database database.reorder_groups(ids) # 通知父级刷新 p = self.parent() while p: if hasattr(p, "refresh_groups"): p.refresh_groups() break p = p.parent() event.acceptProposedAction() def paintEvent(self, event): super().paintEvent(event) if self._indicator_pos < 0: return p = QPainter(self) p.setPen(QPen(QColor("#4a9eff"), 2)) p.drawLine(0, self._indicator_pos, self.width(), self._indicator_pos) p.end() class PanelWindow(QWidget): weather_updated = pyqtSignal(dict) # base_utc_ts: 接口返回的 UTC 时间戳(秒) # tz_offset_sec: 目标时区相对 UTC 的偏移(秒) time_synced = pyqtSignal(float, int) def __init__(self): super().__init__() self.setWindowTitle(app_title()) # 默认不置顶;仅图钉开启时与悬浮球一并置顶(见 _apply_pin_window_layer) self.setWindowFlags(Qt.WindowType.FramelessWindowHint) # 任务栏/开始菜单图标:使用项目 logo.png logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png") if os.path.exists(logo_path): self.setWindowIcon(QIcon(logo_path)) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.setAcceptDrops(True) self.setMinimumSize(MIN_W, MIN_H) self.resize(PANEL_W, PANEL_H) self._persist_w = PANEL_W self._persist_h = PANEL_H self._screen_hooked = False # 让窗口本身也追踪鼠标(不按键也收到 mouseMoveEvent) self.setMouseTracking(True) self._anim = None self._win_drag_pos = None self._resizing = False self._resize_edge = None self._resize_start_global = None self._resize_start_geo = None self._pinned = False self._build_ui() self._setup_tray() self._apply_theme() self._restore_geometry() self.weather_updated.connect(self._on_weather_updated) self.time_synced.connect(self._on_time_synced) self._weather_fetching = False self._time_base_utc_ts: float = 0.0 self._time_mono_base: float = 0.0 # 默认按中国时区(大概率与你的 IP 定位一致);后续会在天气接口返回后修正 self._tz_offset_sec: int = 8 * 3600 self._time_timer = QTimer(self) self._time_timer.setInterval(1000) self._time_timer.timeout.connect(self._refresh_time_label) self._updater = Updater(self) def showEvent(self, event): super().showEvent(event) if not self._screen_hooked: wh = self.windowHandle() if wh is not None: wh.screenChanged.connect(self._on_screen_changed) self._screen_hooked = True # 首次显示时异步获取一次天气 if getattr(self, "_weather_once", False) is False: self._weather_once = True self._update_weather_async() if getattr(self, "_time_once", False) is False: self._time_once = True self._update_time_from_ip_async() def _on_screen_changed(self, screen=None): """跨不同 DPI 的显示器拖动时,Qt 可能改变窗口逻辑尺寸,按记录值拉回。""" if getattr(self, "_body", None) is not None and not self._body.isVisible(): return w = getattr(self, "_persist_w", PANEL_W) h = getattr(self, "_persist_h", PANEL_H) if w >= MIN_W and h >= MIN_H: self.resize(w, h) # ── 天气 ───────────────────────────────────────────── def _update_weather_async(self): def _worker(): data: dict | None = None try: url = "https://uapis.cn/api/v1/misc/weather" req = urllib.request.Request( url, headers={ "User-Agent": "Mozilla/5.0", "Accept": "application/json", }, ) with urllib.request.urlopen(req, timeout=5) as resp: raw = resp.read().decode("utf-8", "replace").strip() if raw: data = json.loads(raw) if not isinstance(data, dict): data = None except Exception as e: # 方便你在控制台看到原因 print("Weather fetch failed:", repr(e)) print("Weather raw (if any):", repr(locals().get("raw", ""))[:200]) data = None # 线程内不要直接操作 QWidget,改为发信号 self.weather_updated.emit(data or {}) threading.Thread(target=_worker, daemon=True).start() def _on_weather_updated(self, data: dict): if not hasattr(self, "weather_box"): return self.weather_box.update_weather(data) self._weather_fetching = False # 用 weather 接口返回的 adcode(由 IP 自动定位)推断时区偏移 # 目前你的接口数据基本为中国区(你给的示例 adcode=320706),中国全时区统一为 UTC+8 try: adcode = data.get("adcode", "") ad_str = str(adcode).strip() if ad_str.isdigit() and len(ad_str) == 6: self._tz_offset_sec = 8 * 3600 else: self._tz_offset_sec = 0 except Exception: self._tz_offset_sec = 8 * 3600 if self._time_base_utc_ts > 0: self._refresh_time_label() def _refresh_weather(self): if getattr(self, "_weather_fetching", False): return self._weather_fetching = True try: self.weather_box.show_loading() except Exception: pass self._update_weather_async() def _refresh_time_label(self): if not hasattr(self, "time_label"): return if self._time_base_utc_ts <= 0: return # 用接口返回的 UTC 基准时间 + monotonic 增量(不受本机时间影响) delta_sec = _time.monotonic() - self._time_mono_base utc_now = self._time_base_utc_ts + delta_sec local_now = utc_now + self._tz_offset_sec dt = datetime.datetime.utcfromtimestamp(local_now) self.time_label.setText(dt.strftime("%H:%M:%S")) self.time_date_label.setText(dt.strftime("%Y-%m-%d")) def _update_time_from_ip_async(self): def _ntp_query(server: str, timeout: float) -> float: # ... (保持原有的 ntp_query 逻辑不变) ... msg = b"\x1b" + 47 * b"\0" with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.settimeout(timeout) s.sendto(msg, (server, 123)) data, _ = s.recvfrom(512) t = struct.unpack("!II", data[40:48]) return t[0] + t[1] / 2**32 - 2208988800.0 def _worker(): # 这里的服务器列表是解决问题的核心:优先国内阿里、腾讯节点 servers = [ "ntp.aliyun.com", "ntp.tencent.com", "cn.pool.ntp.org", "pool.ntp.org", ] base_utc_ts = 0.0 for srv in servers: try: # 尝试连接,超时时间设为 2.5 秒 base_utc_ts = float(_ntp_query(srv, timeout=2.5)) if base_utc_ts > 0: break # 只要有一个成功,就跳出循环 except Exception: continue # 失败了换下一个服务器 # 兜底:如果所有服务器都连不上,回退到本机时间 if base_utc_ts <= 0: base_utc_ts = _time.time() tz_offset_sec = int(getattr(self, "_tz_offset_sec", 8 * 3600)) self.time_synced.emit(base_utc_ts, tz_offset_sec) threading.Thread(target=_worker, daemon=True).start() def _on_time_synced(self, base_utc_ts: float, tz_offset_sec: int): self._time_base_utc_ts = base_utc_ts self._tz_offset_sec = tz_offset_sec self._time_mono_base = _time.monotonic() self._refresh_time_label() if not self._time_timer.isActive(): self._time_timer.start() # ── UI ────────────────────────────────────────────── def _get_quick_actions(self): """返回快捷动作列表,悬浮球右键菜单和左侧快捷栏共用同一份。 每项: (tooltip, qtawesome_icon, callback) 排除重启/退出。""" return [ ("微信多开", "fa5b.weixin", self._open_wechat_multi), ("管理员运行 CMD", "fa5s.terminal", self._open_admin_cmd), ("管理员运行 PowerShell", "fa5b.windows", self._open_admin_powershell), ("打开默认浏览器", "fa5s.globe", self._open_default_browser), ] def _build_ui(self): root = QVBoxLayout(self) root.setContentsMargins(0, 0, 0, 0) self.container = QWidget() self.container.setObjectName("container") self.container.setMouseTracking(True) # 横向:左侧快捷栏 + 右侧主内容 h_layout = QHBoxLayout(self.container) h_layout.setContentsMargins(0, 0, 0, 0) h_layout.setSpacing(0) # ── 左侧快捷栏 self._quick_bar = QWidget() self._quick_bar.setObjectName("quick_bar") self._quick_bar.setFixedWidth(50) # 快捷栏上的按钮会吃掉鼠标事件;为保证边缘 resize 可用,快捷栏自身也追踪鼠标并接入事件过滤 self._quick_bar.setMouseTracking(True) self._quick_bar.installEventFilter(self) quick_layout = QVBoxLayout(self._quick_bar) quick_layout.setContentsMargins(0, 10, 0, 10) quick_layout.setSpacing(6) quick_layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter) self._quick_btns = [] for tooltip, icon_name, callback in self._get_quick_actions(): btn = QPushButton() btn.setFixedSize(34, 34) btn.setToolTip(tooltip) btn.setStyleSheet("border:none; background:transparent; border-radius:4px;") btn.setMouseTracking(True) # 鼠标落在按钮上时,也要能触发边缘检测/resize btn.installEventFilter(self) try: btn.setIcon(qta.icon(icon_name, color="#888")) btn.setIconSize(QSize(16, 16)) except Exception: btn.setText(tooltip[0]) btn.clicked.connect(callback) quick_layout.addWidget(btn, alignment=Qt.AlignmentFlag.AlignHCenter) self._quick_btns.append((btn, icon_name)) quick_layout.addStretch() h_layout.addWidget(self._quick_bar) # ── 右侧主内容 right_widget = QWidget() right_widget.setMouseTracking(True) inner = QVBoxLayout(right_widget) inner.setContentsMargins(8, 10, 8, 8) inner.setSpacing(6) h_layout.addWidget(right_widget) # ── 标题栏(仅此处可拖动窗口,避免分组内框选时带动面板) self._title_bar = QWidget() self._title_bar.setMouseTracking(True) title_bar = QHBoxLayout(self._title_bar) title_bar.setContentsMargins(0, 0, 0, 0) title_bar.setSpacing(6) self.pin_btn = QPushButton() self.pin_btn.setFixedSize(26, 26) self.pin_btn.setToolTip( "固定:面板与悬浮球置顶;开启后最小化只收缩内容,不变悬浮球" ) self.pin_btn.setStyleSheet("border:none; background:transparent;") self.pin_btn.clicked.connect(self._toggle_pin) # 程序名称 self.app_title = QLabel(app_title()) self.app_title.setStyleSheet( "font-size:13px; font-weight:bold; background:transparent;" ) self._min_btn = QPushButton() self._min_btn.setFixedSize(26, 26) self._min_btn.setToolTip("最小化") self._min_btn.setStyleSheet("border:none; background:transparent;") self._min_btn.clicked.connect(self._on_min_click) title_bar.addWidget(self.pin_btn) title_bar.addWidget(self.app_title) title_bar.addStretch() title_bar.addWidget(self._min_btn) inner.addWidget(self._title_bar) # ── 顶部信息栏:左天气 + 右网络时间 self.weather_bar = QWidget() self.weather_bar.setFixedHeight(80) # 强制整个天气栏高度对齐 WeatherBox wb_layout = QHBoxLayout(self.weather_bar) wb_layout.setContentsMargins(5, 0, 5, 0) # 上下边距设为 0,消除空隙 wb_layout.setSpacing(10) self.weather_box = WeatherBox() wb_layout.addWidget(self.weather_box) self.weather_refresh_btn = QPushButton("刷新") self.weather_refresh_btn.setFixedSize(24, 24) self.weather_refresh_btn.setText("") self.weather_refresh_btn.setStyleSheet( "border:none; background:transparent; border-radius:6px;" ) self.weather_refresh_btn.clicked.connect(self._refresh_weather) wb_layout.addWidget(self.weather_refresh_btn) wb_layout.addStretch() self.time_box = QWidget() tb_layout = QVBoxLayout(self.time_box) tb_layout.setContentsMargins(0, 0, 0, 0) tb_layout.setSpacing(0) self.time_title_label = QLabel("网络时间") self.time_title_label.setStyleSheet("font-size:11px; color:#888;") self.time_title_label.setAlignment( Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop ) self.time_label = QLabel("--:--:--") self.time_label.setStyleSheet("font-weight:bold; font-size:18px; color:#ddd;") self.time_label.setAlignment( Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter ) self.time_date_label = QLabel("--") self.time_date_label.setStyleSheet("font-size:11px; color:#888;") self.time_date_label.setAlignment( Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter ) tb_layout.addWidget(self.time_title_label) tb_layout.addWidget(self.time_label) tb_layout.addWidget(self.time_date_label) wb_layout.addWidget(self.time_box) inner.addWidget(self.weather_bar) # ── 搜索框 + 文字按钮(同一行) search_row = QHBoxLayout() search_row.setContentsMargins(0, 0, 0, 0) search_row.setSpacing(6) self.search_box = QLineEdit() self.search_box.setPlaceholderText("🔍 搜索程序...") self.search_box.textChanged.connect(self._on_search) self.add_group_btn = QPushButton("添加分组") self.add_group_btn.setFixedHeight(32) self.add_group_btn.setToolTip("新建分组") self.add_group_btn.clicked.connect(self._add_group) self.add_folder_btn = QPushButton("添加文件夹") self.add_folder_btn.setFixedHeight(32) self.add_folder_btn.setToolTip("读取文件夹,自动创建分组") self.add_folder_btn.clicked.connect(self._add_from_folder) search_row.addWidget(self.search_box) search_row.addWidget(self.add_group_btn) search_row.addWidget(self.add_folder_btn) # ── 可收缩的内容区(搜索+分组+底部栏) self._body = QWidget() body_layout = QVBoxLayout(self._body) body_layout.setContentsMargins(0, 0, 0, 0) body_layout.setSpacing(6) body_layout.addLayout(search_row) # ── 分组滚动区 self.scroll = QScrollArea() self.scroll.setWidgetResizable(True) self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.groups_container = GroupsContainer() self.groups_container.setStyleSheet("background:transparent;") self.groups_layout = QVBoxLayout(self.groups_container) self.groups_layout.setContentsMargins(0, 0, 0, 0) self.groups_layout.setSpacing(6) self.groups_layout.addStretch() self.scroll.setWidget(self.groups_container) body_layout.addWidget(self.scroll) # ── 底部工具栏 self.bottom_bar = QWidget() self.bottom_bar.setObjectName("bottom_bar") self.bottom_bar.setFixedHeight(36) bottom_layout = QHBoxLayout(self.bottom_bar) bottom_layout.setContentsMargins(6, 0, 6, 0) bottom_layout.setSpacing(4) self.menu_btn = QPushButton() self.menu_btn.setFixedSize(28, 28) self.menu_btn.setToolTip("菜单") self.menu_btn.setStyleSheet( "border:none; background:transparent; border-radius:4px;" ) self.menu_btn.clicked.connect(self._show_bottom_menu) self.theme_btn = QPushButton() self.theme_btn.setFixedSize(28, 28) self.theme_btn.setToolTip("切换亮/暗主题") self.theme_btn.setStyleSheet( "border:none; background:transparent; border-radius:4px;" ) self.theme_btn.clicked.connect(self._toggle_theme) self.settings_btn = QPushButton() self.settings_btn.setFixedSize(28, 28) self.settings_btn.setToolTip("设置") self.settings_btn.setStyleSheet( "border:none; background:transparent; border-radius:4px;" ) self.settings_btn.clicked.connect(self._show_settings) self.quit_btn = QPushButton() self.quit_btn.setFixedSize(28, 28) self.quit_btn.setToolTip("退出程序") self.quit_btn.setStyleSheet( "border:none; background:transparent; border-radius:4px;" ) self.quit_btn.clicked.connect(self._quit_application) bottom_layout.addWidget(self.menu_btn) bottom_layout.addStretch() self.update_btn = QPushButton() self.update_btn.setFixedSize(28, 28) self.update_btn.setToolTip("检查更新") self.update_btn.setStyleSheet("border:none; background:transparent; border-radius:4px;") self.update_btn.clicked.connect(lambda: self._updater.check(silent_if_latest=False)) bottom_layout.addWidget(self.update_btn) bottom_layout.addWidget(self.theme_btn) bottom_layout.addWidget(self.settings_btn) bottom_layout.addWidget(self.quit_btn) body_layout.addWidget(self.bottom_bar) inner.addWidget(self._body) root.addWidget(self.container) # eventFilter 只用于 resize 拖拽,不拦截子 widget 的 drop self.container.installEventFilter(self) self.refresh_groups() def _apply_theme(self): t = theme.current() is_dark = theme.name() == "dark" ic = "#cccccc" if is_dark else "#555555" self._apply_tooltip_theme(t, is_dark) self.container.setStyleSheet( f""" QWidget#container {{ background: {t['panel_bg']}; border-radius: 10px; border: 1px solid {t['panel_border']}; }} """ ) self.search_box.setStyleSheet( f""" QLineEdit {{ background: {t['search_bg']}; border: 1px solid {t['search_border']}; border-radius: 5px; color: {t['search_color']}; padding: 5px 10px; font-size: 12px; }} QLineEdit:focus {{ border-color: {t['search_focus']}; }} """ ) txt_btn_style = f""" QPushButton {{ border: 1px solid {t['search_border']}; border-radius: 5px; color: {t['btn_color']}; background: {t['search_bg']}; font-size: 11px; padding: 0 8px; }} QPushButton:hover {{ background: {t['header_hover']}; color: {t['btn_hover']}; }} """ self.add_group_btn.setStyleSheet(txt_btn_style) self.add_folder_btn.setStyleSheet(txt_btn_style) self.scroll.setStyleSheet( f""" QScrollArea {{ border:none; background:transparent; }} QScrollBar:vertical {{ background:transparent; width:4px; }} QScrollBar::handle:vertical {{ background:{t['scrollbar']}; border-radius:2px; }} QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height:0; }} """ ) self.pin_btn.setIcon( qta.icon("fa5s.thumbtack", color="#f90" if self._pinned else ic) ) self.pin_btn.setIconSize(QSize(13, 13)) self._min_btn.setIcon(qta.icon("fa5s.minus", color=ic)) self._min_btn.setIconSize(QSize(13, 13)) self.app_title.setStyleSheet( f"font-size:13px; font-weight:bold; background:transparent; color:{t['search_color']};" ) # 顶部天气栏 self.weather_bar.setStyleSheet("background:transparent; border-radius:6px;") self.weather_box.set_theme_colors(t["search_color"]) if hasattr(self, "weather_refresh_btn"): self.weather_refresh_btn.setStyleSheet( "border:none; background:transparent; border-radius:6px;" ) # 刷新按钮图标跟随主题颜色 try: self.weather_refresh_btn.setIcon( qta.icon("fa5s.sync-alt", color=t["search_color"]) ) self.weather_refresh_btn.setIconSize(QSize(14, 14)) except Exception: pass # 网络时间文字 if hasattr(self, "time_label"): self.time_label.setStyleSheet( f"font-weight:bold; font-size:18px; color:{t['search_color']};" ) if hasattr(self, "time_date_label"): self.time_date_label.setStyleSheet( f"font-size:11px; color:{t['search_color']};" ) if hasattr(self, "time_title_label"): self.time_title_label.setStyleSheet("font-size:11px; color: #888;") # 左侧快捷栏 bar_side_bg = "rgba(0,0,0,20)" if is_dark else "rgba(0,0,0,6)" self._quick_bar.setStyleSheet( f""" QWidget#quick_bar {{ background: {bar_side_bg}; border: 1px solid {t['panel_border']}; border-radius: 10px; }} QPushButton {{ border:none; background:transparent; border-radius:4px; }} QPushButton:hover {{ background:{t['header_hover']}; }} """ ) for btn, icon_name in self._quick_btns: try: btn.setIcon(qta.icon(icon_name, color=ic)) btn.setIconSize(QSize(14, 14)) except Exception: pass # 底部工具栏 bar_bg2 = "rgba(0,0,0,30)" if is_dark else "rgba(0,0,0,8)" self.bottom_bar.setStyleSheet( f""" QWidget#bottom_bar {{ background: {bar_bg2}; border-radius: 0 0 10px 10px; border-top: 1px solid {t['panel_border']}; }} QPushButton {{ border:none; background:transparent; border-radius:4px; }} QPushButton:hover {{ background:{t['header_hover']}; }} """ ) self.theme_btn.setIcon( qta.icon("fa5s.sun" if is_dark else "fa5s.moon", color=ic) ) self.theme_btn.setIconSize(QSize(14, 14)) self.update_btn.setIcon(qta.icon("fa5s.sync-alt", color=ic)) self.update_btn.setIconSize(QSize(14, 14)) self.menu_btn.setIcon(qta.icon("fa5s.bars", color=ic)) self.menu_btn.setIconSize(QSize(14, 14)) self.settings_btn.setIcon(qta.icon("fa5s.cog", color=ic)) self.settings_btn.setIconSize(QSize(14, 14)) self.quit_btn.setIcon(qta.icon("fa5s.sign-out-alt", color=ic)) self.quit_btn.setIconSize(QSize(14, 14)) for i in range(self.groups_layout.count() - 1): item = self.groups_layout.itemAt(i) if item and item.widget(): item.widget()._apply_theme() def _apply_tooltip_theme(self, t: dict, is_dark: bool): """让所有控件 tooltip 跟随主题(含快捷栏按钮)。""" app = QApplication.instance() if app is None: return bg = "rgba(45,45,45,245)" if is_dark else "rgba(255,255,255,250)" fg = "#eeeeee" if is_dark else "#111111" bd = t.get("menu_border") or t.get("search_border") or t.get("panel_border") or "#666" start = "/*TOOLTIP_THEME_START*/" end = "/*TOOLTIP_THEME_END*/" tooltip_css = ( f"{start}\n" "QToolTip {\n" f" background-color: {bg};\n" f" color: {fg};\n" f" border: 1px solid {bd};\n" " border-radius: 6px;\n" " padding: 6px 8px;\n" " font-size: 12px;\n" "}\n" f"{end}\n" ) ss = app.styleSheet() or "" if start in ss and end in ss: pre = ss.split(start, 1)[0] post = ss.split(end, 1)[1] ss = pre + tooltip_css + post else: if ss and not ss.endswith("\n"): ss += "\n" ss += tooltip_css app.setStyleSheet(ss) def _quit_application(self): ret = dialog_style.question( self, "退出", "确认退出吗?", default_button=QMessageBox.StandardButton.No, ) if ret == QMessageBox.StandardButton.Yes: QApplication.quit() def _restart_application(self): import subprocess subprocess.Popen([sys.executable] + sys.argv) QApplication.quit() def _open_admin_cmd(self): import ctypes ctypes.windll.shell32.ShellExecuteW(None, "runas", "cmd.exe", None, None, 1) def _open_admin_powershell(self): import ctypes ctypes.windll.shell32.ShellExecuteW(None, "runas", "powershell.exe", None, None, 1) def _open_default_browser(self): import webbrowser webbrowser.open("https://www.baidu.com") def _open_wechat_multi(self): from ui.wechat_multi import WechatMultiDialog dlg = WechatMultiDialog(self) dlg.exec() def _toggle_theme(self): theme.set_theme("light" if theme.name() == "dark" else "dark") self._apply_theme() # 若设置窗口已打开,同步更新其主题 if hasattr(self, "_settings_win") and self._settings_win.isVisible(): try: self._settings_win._apply_theme() except Exception: pass def _show_settings(self): from ui.settings_window import SettingsWindow if hasattr(self, "_settings_win") and self._settings_win.isVisible(): self._settings_win.raise_() self._settings_win.activateWindow() return self._settings_win = SettingsWindow(self) self._settings_win.show() def _show_bottom_menu(self): menu = QMenu(self) menu.setStyleSheet(self._bottom_menu_stylesheet()) menu.addAction(self._make_doubao_icon(), "豆包会话").triggered.connect( self._open_doubao_session ) menu.addAction( self._make_round_menu_icon("D", "#4d6bfe"), "DeepSeek" ).triggered.connect( lambda: self._open_url_in_browser( "https://chat.deepseek.com/", "DeepSeek" ) ) menu.addAction( self._make_round_menu_icon("G", "#4285f4"), "Gemini" ).triggered.connect( lambda: self._open_url_in_browser( "https://gemini.google.com/", "Gemini" ) ) # 菜单贴近按钮下方显示,避免漂移到不自然位置 pos = self.menu_btn.mapToGlobal(self.menu_btn.rect().bottomLeft()) menu.exec(pos) def _bottom_menu_stylesheet(self) -> str: t = theme.current() return f""" QMenu {{ background: {t['menu_bg']}; color: {t['menu_color']}; border: 1px solid {t['menu_border']}; border-radius: 8px; padding: 4px; }} QMenu::item {{ padding: 6px 14px; border-radius: 5px; background: transparent; }} QMenu::item:selected {{ background: {t['menu_selected']}; }} QMenu::separator {{ height: 1px; background: {t['menu_border']}; margin: 4px 8px; }} """ def _make_doubao_icon(self) -> QIcon: return self._make_round_menu_icon("豆", "#3b82f6", font_pt=9) def _make_round_menu_icon( self, letter: str, bg_hex: str, *, font_pt: int = 9 ) -> QIcon: pm = QPixmap(18, 18) pm.fill(Qt.GlobalColor.transparent) p = QPainter(pm) p.setRenderHint(QPainter.RenderHint.Antialiasing) p.setBrush(QBrush(QColor(bg_hex))) p.setPen(Qt.PenStyle.NoPen) p.drawEllipse(1, 1, 16, 16) p.setPen(QPen(QColor("white"))) f = p.font() f.setPointSize(font_pt) f.setBold(True) p.setFont(f) p.drawText(pm.rect(), int(Qt.AlignmentFlag.AlignCenter), letter) p.end() return QIcon(pm) def _open_url_in_browser(self, url: str, title: str) -> None: try: ok = webbrowser.open(url) except Exception as e: dialog_style.warning( self, "打开失败", f"无法在默认浏览器中打开{title}:{e}" ) return if not ok: dialog_style.warning( self, "打开失败", f"系统未注册可用的默认浏览器,请手动在浏览器中打开{title}:\n{url}", ) def _open_doubao_session(self): """用系统默认浏览器打开豆包会话页。""" self._open_url_in_browser("https://www.doubao.com/chat/", "豆包") def refresh_groups(self): # 记录当前折叠状态 collapsed_ids = set() for i in range(self.groups_layout.count() - 1): item = self.groups_layout.itemAt(i) if item and item.widget(): gw = item.widget() if gw.collapsed: collapsed_ids.add(gw.group_data["id"]) while self.groups_layout.count() > 1: item = self.groups_layout.takeAt(0) if item.widget(): item.widget().deleteLater() for gdata in database.get_groups(): gw = GroupWidget(gdata) gw.request_refresh_all.connect(self.refresh_groups) # 恢复折叠状态 if gdata["id"] in collapsed_ids: gw.collapsed = True gw.flow.setVisible(False) t = theme.current() ic = "#cccccc" if theme.name() == "dark" else "#555555" import qtawesome as _qta gw.toggle_icon.setPixmap( _qta.icon("fa5s.chevron-right", color=ic).pixmap(14, 14) ) self.groups_layout.insertWidget(self.groups_layout.count() - 1, gw) def reset_groups_and_items(self): """清空所有分组与程序,并刷新界面。""" database.reset_groups_and_items(recreate_default_group=True) # 清空搜索关键字,确保刷新后可见全部内容 try: self.search_box.setText("") except Exception: pass self.refresh_groups() def _add_group(self): name, ok = dialog_style.get_text(self, "新建分组", "分组名称:") if ok and name.strip(): database.add_group(name.strip()) self.refresh_groups() def _add_from_folder(self): folder = dialog_style.get_existing_directory(self, "选择文件夹") if not folder: return # 统一路径分隔符 folder = os.path.normpath(folder) group_name = os.path.basename(folder) gid = database.add_group(group_name, folder_path=folder) # 文件夹分组直接读目录,不写入 items 表 count = sum( 1 for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f)) ) self.refresh_groups() dialog_style.information( self, "完成", f"已创建分组「{group_name}」,共 {count} 个文件(实时读取)。", ) def _sync_pin_dependent_ui(self): """图钉图标、最小化提示、置顶标志(面板 + 悬浮球)。""" ic = "#cccccc" if theme.name() == "dark" else "#555555" self.pin_btn.setIcon( qta.icon("fa5s.thumbtack", color="#f90" if self._pinned else ic) ) self.pin_btn.setIconSize(QSize(13, 13)) if self._pinned: self._min_btn.setToolTip("收缩内容区(固定模式)") else: self._min_btn.setToolTip("最小化到悬浮球") self._apply_pin_window_layer() def _apply_pin_window_layer(self): """图钉开 → 面板与悬浮球 WindowStaysOnTop;关 → 普通叠放,不挡其他程序。""" on_top = self._pinned if getattr(self, "_pin_top_applied", None) is not on_top: self._pin_top_applied = on_top was_visible = self.isVisible() geo = self.geometry() flags = Qt.WindowType.FramelessWindowHint if on_top: flags |= Qt.WindowType.WindowStaysOnTopHint self.setWindowFlags(flags) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True) self.setAcceptDrops(True) logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png") if os.path.exists(logo_path): self.setWindowIcon(QIcon(logo_path)) if was_visible: self.setGeometry(geo) self.show() ball = getattr(self, "_ball_ref", None) if ball is not None: ball.set_stays_on_top(on_top) def _toggle_pin(self): self._pinned = not self._pinned self._sync_pin_dependent_ui() def _on_min_click(self): """图钉开启时收缩内容区,否则最小化到悬浮球""" if self._pinned: if self._body.isVisible(): self._collapse_body() else: self._expand_body() else: self.minimize_to_ball() def _collapse_body(self): """收缩:隐藏内容区,窗口缩到只剩标题栏高度""" self._body.hide() self.weather_bar.hide() self._quick_bar.hide() self._expanded_height = self.height() title_h = self._title_bar.sizeHint().height() + 20 self.setFixedHeight(title_h) ic = "#cccccc" if theme.name() == "dark" else "#555555" self._min_btn.setIcon(qta.icon("fa5s.chevron-down", color=ic)) self._min_btn.setIconSize(QSize(13, 13)) def _expand_body(self): """展开:恢复内容区和窗口高度""" self.weather_bar.show() self._body.show() self._quick_bar.show() h = getattr(self, "_expanded_height", 520) self.setMinimumHeight(MIN_H) self.setMaximumHeight(16777215) self.resize(self.width(), h) self._persist_w = self.width() self._persist_h = h ic = "#cccccc" if theme.name() == "dark" else "#555555" self._min_btn.setIcon(qta.icon("fa5s.minus", color=ic)) self._min_btn.setIconSize(QSize(13, 13)) def _save_geometry(self): """把当前位置和尺寸写入数据库""" g = self.geometry() database.set_setting("panel_x", str(g.x())) database.set_setting("panel_y", str(g.y())) # 收缩为仅标题栏时高度变小,勿把临时高度写入库/覆盖跨屏用的 persist if getattr(self, "_body", None) is not None and not self._body.isVisible(): return self._persist_w = g.width() self._persist_h = g.height() database.set_setting("panel_w", str(g.width())) database.set_setting("panel_h", str(g.height())) def _restore_geometry(self): """从数据库恢复上次的位置、尺寸和透明度""" try: x = int(database.get_setting("panel_x", "")) y = int(database.get_setting("panel_y", "")) w = int(database.get_setting("panel_w", str(PANEL_W))) h = int(database.get_setting("panel_h", str(PANEL_H))) # 用保存坐标所在屏幕来限制范围,支持多显示器 screen_obj = QApplication.screenAt(QPoint(x, y)) if screen_obj is None: screen_obj = QApplication.primaryScreen() screen = screen_obj.availableGeometry() x = max(screen.left(), min(x, screen.right() - w)) y = max(screen.top(), min(y, screen.bottom() - h)) self._persist_w = w self._persist_h = h self.setGeometry(x, y, w, h) except (ValueError, TypeError): pass # 恢复透明度 try: opacity = int(database.get_setting("panel_opacity", "100")) self.setWindowOpacity(max(30, min(100, opacity)) / 100) except (ValueError, TypeError): pass def _on_search(self, keyword): for i in range(self.groups_layout.count() - 1): item = self.groups_layout.itemAt(i) if item and item.widget(): item.widget().filter(keyword) # ── 显示/隐藏 ──────────────────────────────────────── def show_near(self, ball_pos: QPoint, ball_size: int): # 根据球所在屏幕来定位 panel,避免跑到主屏幕 screen_obj = QApplication.screenAt(ball_pos) if screen_obj is None: screen_obj = QApplication.primaryScreen() screen = screen_obj.availableGeometry() pw, ph = self.width(), self.height() bx, by = ball_pos.x(), ball_pos.y() x = bx - pw - 8 if x < screen.left(): x = bx + ball_size + 8 y = max(screen.top(), min(by + ball_size // 2 - ph // 2, screen.bottom() - ph)) self.move(x, y + 30) self.setWindowOpacity(0) self.show() self.raise_() if hasattr(self, "_ball_ref"): self._ball_ref.hide() if self._anim: self._anim.stop() self._anim = QPropertyAnimation(self, b"windowOpacity") self._anim.setDuration(ANIM_MS) self._anim.setStartValue(0.0) self._anim.setEndValue(1.0) self._anim.start() self._anim2 = QPropertyAnimation(self, b"pos") self._anim2.setDuration(ANIM_MS) self._anim2.setEasingCurve(QEasingCurve.Type.OutCubic) self._anim2.setStartValue(QPoint(x, y + 30)) self._anim2.setEndValue(QPoint(x, y)) self._anim2.finished.connect(self._save_geometry) self._anim2.start() def hide_panel(self): if self._anim: self._anim.stop() self._anim = QPropertyAnimation(self, b"windowOpacity") self._anim.setDuration(ANIM_MS) self._anim.setStartValue(1.0) self._anim.setEndValue(0.0) self._anim.finished.connect(self.hide) self._anim.start() def minimize_to_ball(self): self.hide_panel() if hasattr(self, "_ball_ref"): self._ball_ref.place_on_screen_of(self) self._ball_ref.show() self._ball_ref.setWindowOpacity(1.0) def toggle_near(self, ball_pos: QPoint, ball_size: int): if self.isVisible(): if not self._pinned: self.minimize_to_ball() else: self.show_near(ball_pos, ball_size) # ── 边缘检测 ───────────────────────────────────────── def _edge_at(self, p: QPoint): x, y, w, h = p.x(), p.y(), self.width(), self.height() m = RESIZE_M l = x <= m r = x >= w - m t = y <= m b = y >= h - m if b and r: return "br" if b and l: return "bl" if t and r: return "tr" if t and l: return "tl" if r: return "r" if l: return "l" if b: return "b" if t: return "t" return None _EDGE_CUR = { "r": Qt.CursorShape.SizeHorCursor, "l": Qt.CursorShape.SizeHorCursor, "b": Qt.CursorShape.SizeVerCursor, "t": Qt.CursorShape.SizeVerCursor, "br": Qt.CursorShape.SizeFDiagCursor, "tl": Qt.CursorShape.SizeFDiagCursor, "bl": Qt.CursorShape.SizeBDiagCursor, "tr": Qt.CursorShape.SizeBDiagCursor, } def _update_cursor(self, global_pos: QPoint): """根据全局坐标更新光标形状(不依赖事件来源 widget)""" local = self.mapFromGlobal(global_pos) edge = self._edge_at(local) self.setCursor(self._EDGE_CUR.get(edge, Qt.CursorShape.ArrowCursor)) def _do_resize(self, gp: QPoint): d = gp - self._resize_start_global g = self._resize_start_geo x, y, w, h = g.x(), g.y(), g.width(), g.height() e = self._resize_edge if "r" in e: w = max(MIN_W, w + d.x()) if "l" in e: nw = max(MIN_W, w - d.x()) x += w - nw w = nw if "b" in e: h = max(MIN_H, h + d.y()) if "t" in e: nh = max(MIN_H, h - d.y()) y += h - nh h = nh self.setGeometry(x, y, w, h) def _container_pos_is_title_bar(self, pos_in_container: QPoint) -> bool: """仅标题栏区域允许拖动整个面板。""" w = self.container.childAt(pos_in_container) p = w while p: if p is self._title_bar: return True p = p.parentWidget() return False # ── eventFilter:处理 container 上的 resize/drag ───── def eventFilter(self, obj, event): is_quick = ( obj is getattr(self, "_quick_bar", None) or (hasattr(self, "_quick_bar") and isinstance(obj, QPushButton) and obj.parentWidget() is self._quick_bar) ) if obj is self.container or is_quick: et = event.type() # drop 事件放行给子 widget if et in ( QEvent.Type.DragEnter, QEvent.Type.DragMove, QEvent.Type.Drop, QEvent.Type.DragLeave, ): return False if ( et == QEvent.Type.MouseButtonPress and event.button() == Qt.MouseButton.LeftButton ): local = self.mapFromGlobal(event.globalPosition().toPoint()) edge = self._edge_at(local) if edge: self._resizing = True self._resize_edge = edge self._resize_start_global = event.globalPosition().toPoint() self._resize_start_geo = self.geometry() return True # 快捷栏/按钮区域不允许拖动窗口(只保留边缘 resize) if obj is self.container: pos_c = self.container.mapFrom(self, local) if not self._container_pos_is_title_bar(pos_c): return False self._win_drag_pos = ( event.globalPosition().toPoint() - self.frameGeometry().topLeft() ) elif et == QEvent.Type.MouseMove: gp = event.globalPosition().toPoint() if self._resizing: self._do_resize(gp) return True if event.buttons() & Qt.MouseButton.LeftButton and self._win_drag_pos: self.move(gp - self._win_drag_pos) return True # 悬停时更新光标 self._update_cursor(gp) elif et == QEvent.Type.MouseButtonRelease: self._resizing = False self._resize_edge = None self._win_drag_pos = None self._update_cursor(event.globalPosition().toPoint()) self._save_geometry() return super().eventFilter(obj, event) # ── PanelWindow 自身的鼠标事件(鼠标在 container 外时) def mouseMoveEvent(self, event): if self._resizing: self._do_resize(event.globalPosition().toPoint()) return if event.buttons() & Qt.MouseButton.LeftButton and self._win_drag_pos: self.move(event.globalPosition().toPoint() - self._win_drag_pos) return self._update_cursor(event.globalPosition().toPoint()) def mousePressEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: edge = self._edge_at(event.pos()) if edge: self._resizing = True self._resize_edge = edge self._resize_start_global = event.globalPosition().toPoint() self._resize_start_geo = self.geometry() elif self.container.geometry().contains(event.pos()): pos_c = self.container.mapFrom(self, event.pos()) if self._container_pos_is_title_bar(pos_c): self._win_drag_pos = ( event.globalPosition().toPoint() - self.frameGeometry().topLeft() ) def mouseReleaseEvent(self, event): self._resizing = False self._resize_edge = None self._win_drag_pos = None self._update_cursor(event.globalPosition().toPoint()) self._save_geometry() # 拖动/resize 结束后保存位置和尺寸 def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.acceptProposedAction() def focusOutEvent(self, event): if not self._pinned: QTimer.singleShot(200, self._check_focus_lost) def _check_focus_lost(self): if not self.isActiveWindow() and not self._pinned: self.hide_panel() # ── 托盘 ───────────────────────────────────────────── def _setup_tray(self): self.tray = QSystemTrayIcon(self) self.tray.setIcon(_make_tray_icon()) self.tray.setToolTip("桌面文件整理") menu = QMenu() menu.setStyleSheet( """ QMenu { background:#2b2b2b; color:#eee; border:1px solid #555; padding:4px; border-radius:6px; } QMenu::item { padding:6px 20px; border-radius:4px; } QMenu::item:selected { background:#3a3a3a; } QMenu::separator { height:1px; background:#444; margin:4px 8px; } """ ) for tooltip, _icon, callback in self._get_quick_actions(): menu.addAction(tooltip).triggered.connect(callback) menu.addSeparator() menu.addAction("🔄 重启本程序").triggered.connect(self._restart_application) menu.addAction("❌ 退出本程序").triggered.connect(QApplication.quit) self.tray.setContextMenu(menu) self.tray.activated.connect(self._on_tray_activated) self.tray.show() def _on_tray_activated(self, reason): if reason in ( QSystemTrayIcon.ActivationReason.Trigger, QSystemTrayIcon.ActivationReason.DoubleClick, ): if self.isVisible(): self.minimize_to_ball() elif hasattr(self, "_ball_ref"): self.show_near(self._ball_ref.pos(), self._ball_ref.width()) def _toggle_autostart(self): self._autostart_enabled = not self._autostart_enabled _set_autostart(self._autostart_enabled) self._autostart_act.setText( f"🚀 开机自启: {'开' if self._autostart_enabled else '关'}" ) class SettingsPopup(QWidget): """设置弹出面板:透明度调节等""" def __init__(self, panel: "PanelWindow"): super().__init__(panel, Qt.WindowType.Popup) self._panel = panel self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.setWindowFlags(Qt.WindowType.Popup | Qt.WindowType.FramelessWindowHint) self._build() def _build(self): from PyQt6.QtWidgets import QSlider, QLabel, QVBoxLayout, QHBoxLayout t = theme.current() is_dark = theme.name() == "dark" self.setStyleSheet( f""" QWidget {{ background: {t['panel_bg']}; border: 1px solid {t['panel_border']}; border-radius: 8px; color: {t['search_color']}; }} 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; }} """ ) layout = QVBoxLayout(self) layout.setContentsMargins(14, 12, 14, 12) layout.setSpacing(10) # 透明度 row = QHBoxLayout() row.setSpacing(10) lbl = QLabel("透明度") lbl.setStyleSheet( f"color:{t['search_color']}; font-size:12px; background:transparent; border:none;" ) lbl.setFixedWidth(42) self._opacity_slider = QSlider(Qt.Orientation.Horizontal) self._opacity_slider.setRange(30, 100) saved_opacity = int(database.get_setting("panel_opacity", "100")) self._opacity_slider.setValue(saved_opacity) self._opacity_slider.setFixedWidth(140) self._opacity_slider.valueChanged.connect(self._on_opacity) self._opacity_val = QLabel(f"{self._opacity_slider.value()}%") self._opacity_val.setStyleSheet( f"color:{t['search_color']}; font-size:11px; background:transparent; border:none;" ) self._opacity_val.setFixedWidth(34) row.addWidget(lbl) row.addWidget(self._opacity_slider) row.addWidget(self._opacity_val) layout.addLayout(row) self.adjustSize() 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 _set_autostart(enable: bool): try: import winreg key = winreg.OpenKey( winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_SET_VALUE, ) app_name = "DesktopOrganizer" if enable: exe = ( f'"{sys.executable}" "{os.path.abspath("main.py")}"' if not getattr(sys, "frozen", False) else f'"{sys.executable}"' ) winreg.SetValueEx(key, app_name, 0, winreg.REG_SZ, exe) else: try: winreg.DeleteValue(key, app_name) except FileNotFoundError: pass winreg.CloseKey(key) except Exception as e: print(f"自启设置失败: {e}")