优化更新
This commit is contained in:
+335
-110
@@ -21,7 +21,6 @@ from PyQt6.QtWidgets import (
|
||||
QLineEdit,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QGridLayout,
|
||||
QFrame,
|
||||
)
|
||||
from PyQt6.QtCore import (
|
||||
@@ -178,8 +177,6 @@ class WeatherBox(QWidget):
|
||||
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;"
|
||||
@@ -264,7 +261,6 @@ class WeatherBox(QWidget):
|
||||
self._weather_name_label.setText("获取中…")
|
||||
self._location_label.setText("")
|
||||
self._wind_label.setText("")
|
||||
self._last_tooltip = None
|
||||
|
||||
|
||||
def _make_tray_icon():
|
||||
@@ -410,16 +406,32 @@ class PanelWindow(QWidget):
|
||||
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
|
||||
# 默认取本机系统时区;天气接口仅用于定位参考,不再反向改写时区
|
||||
try:
|
||||
self._tz_offset_sec: int = int(
|
||||
datetime.datetime.now().astimezone().utcoffset().total_seconds()
|
||||
)
|
||||
except Exception:
|
||||
self._tz_offset_sec = 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)
|
||||
|
||||
self._dock_state = "none"
|
||||
self._is_collapsed = False
|
||||
self._is_hiding = False
|
||||
self._temp_disable_collapse = False
|
||||
self._dock_timer = QTimer(self)
|
||||
self._dock_timer.setInterval(200)
|
||||
self._dock_timer.timeout.connect(self._on_dock_timer_timeout)
|
||||
self._dock_timer.start()
|
||||
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
self._apply_win32_blur(theme.name() == "dark")
|
||||
if not self._screen_hooked:
|
||||
wh = self.windowHandle()
|
||||
if wh is not None:
|
||||
@@ -478,17 +490,8 @@ class PanelWindow(QWidget):
|
||||
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
|
||||
# 时区不再依赖天气接口(接口失败时旧逻辑会把时区清零,导致时间差 8 小时),
|
||||
# 统一使用初始化时的系统时区。
|
||||
if self._time_base_utc_ts > 0:
|
||||
self._refresh_time_label()
|
||||
|
||||
@@ -511,7 +514,7 @@ class PanelWindow(QWidget):
|
||||
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)
|
||||
dt = datetime.datetime.fromtimestamp(local_now, tz=datetime.timezone.utc)
|
||||
self.time_label.setText(dt.strftime("%H:%M:%S"))
|
||||
self.time_date_label.setText(dt.strftime("%Y-%m-%d"))
|
||||
|
||||
@@ -573,6 +576,7 @@ class PanelWindow(QWidget):
|
||||
("管理员运行 PowerShell", "fa5b.windows", self._open_admin_powershell),
|
||||
("打开默认浏览器", "fa5s.globe", self._open_default_browser),
|
||||
("解除占用", "fa5s.unlock", self._open_unlocker),
|
||||
("硬件温度", "fa5s.thermometer-half", self._open_temperature),
|
||||
]
|
||||
|
||||
def _build_ui(self):
|
||||
@@ -674,6 +678,7 @@ class PanelWindow(QWidget):
|
||||
self.weather_refresh_btn = QPushButton("刷新")
|
||||
self.weather_refresh_btn.setFixedSize(24, 24)
|
||||
self.weather_refresh_btn.setText("")
|
||||
_install_themed_tooltip(self.weather_refresh_btn, "刷新天气")
|
||||
self.weather_refresh_btn.setStyleSheet(
|
||||
"border:none; background:transparent; border-radius:6px;"
|
||||
)
|
||||
@@ -718,8 +723,14 @@ class PanelWindow(QWidget):
|
||||
search_row.setSpacing(6)
|
||||
|
||||
self.search_box = QLineEdit()
|
||||
self.search_box.setPlaceholderText("🔍 搜索程序...")
|
||||
self.search_box.setPlaceholderText("搜索程序...")
|
||||
self.search_box.setClearButtonEnabled(True)
|
||||
self.search_box.textChanged.connect(self._on_search)
|
||||
# 前置搜索图标(随主题在 _apply_theme 中重新着色)
|
||||
self._search_action = self.search_box.addAction(
|
||||
qta.icon("fa5s.search", color="#888"),
|
||||
QLineEdit.ActionPosition.LeadingPosition,
|
||||
)
|
||||
|
||||
self.add_group_btn = QPushButton("添加分组")
|
||||
self.add_group_btn.setFixedHeight(32)
|
||||
@@ -823,12 +834,13 @@ class PanelWindow(QWidget):
|
||||
ic = "#cccccc" if is_dark else "#555555"
|
||||
|
||||
self._apply_tooltip_theme(t, is_dark)
|
||||
self._apply_win32_blur(is_dark)
|
||||
|
||||
self.container.setStyleSheet(
|
||||
f"""
|
||||
QWidget#container {{
|
||||
background: {t['panel_bg']};
|
||||
border-radius: 10px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid {t['panel_border']};
|
||||
}}
|
||||
"""
|
||||
@@ -859,6 +871,19 @@ class PanelWindow(QWidget):
|
||||
"""
|
||||
self.add_group_btn.setStyleSheet(txt_btn_style)
|
||||
self.add_folder_btn.setStyleSheet(txt_btn_style)
|
||||
# 文字按钮前置图标(随主题着色)
|
||||
try:
|
||||
self.add_group_btn.setIcon(qta.icon("fa5s.plus", color=ic))
|
||||
self.add_group_btn.setIconSize(QSize(11, 11))
|
||||
self.add_folder_btn.setIcon(qta.icon("fa5s.folder-plus", color=ic))
|
||||
self.add_folder_btn.setIconSize(QSize(11, 11))
|
||||
except Exception:
|
||||
pass
|
||||
# 搜索图标随主题着色
|
||||
try:
|
||||
self._search_action.setIcon(qta.icon("fa5s.search", color=ic))
|
||||
except Exception:
|
||||
pass
|
||||
self.scroll.setStyleSheet(
|
||||
f"""
|
||||
QScrollArea {{ border:none; background:transparent; }}
|
||||
@@ -874,6 +899,13 @@ class PanelWindow(QWidget):
|
||||
self.pin_btn.setIconSize(QSize(13, 13))
|
||||
self._min_btn.setIcon(qta.icon("fa5s.minus", color=ic))
|
||||
self._min_btn.setIconSize(QSize(13, 13))
|
||||
# 标题栏按钮悬停反馈
|
||||
title_btn_style = (
|
||||
f"QPushButton {{ border:none; background:transparent; border-radius:5px; }}"
|
||||
f"QPushButton:hover {{ background:{t['header_hover']}; }}"
|
||||
)
|
||||
self.pin_btn.setStyleSheet(title_btn_style)
|
||||
self._min_btn.setStyleSheet(title_btn_style)
|
||||
self.app_title.setStyleSheet(
|
||||
f"font-size:13px; font-weight:bold; background:transparent; color:{t['search_color']};"
|
||||
)
|
||||
@@ -883,7 +915,8 @@ class PanelWindow(QWidget):
|
||||
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;"
|
||||
f"QPushButton {{ border:none; background:transparent; border-radius:6px; }}"
|
||||
f"QPushButton:hover {{ background:{t['header_hover']}; }}"
|
||||
)
|
||||
# 刷新按钮图标跟随主题颜色
|
||||
try:
|
||||
@@ -912,7 +945,7 @@ class PanelWindow(QWidget):
|
||||
QWidget#quick_bar {{
|
||||
background: {bar_side_bg};
|
||||
border: 1px solid {t['panel_border']};
|
||||
border-radius: 10px;
|
||||
border-radius: 12px;
|
||||
}}
|
||||
QPushButton {{ border:none; background:transparent; border-radius:4px; }}
|
||||
QPushButton:hover {{ background:{t['header_hover']}; }}
|
||||
@@ -931,7 +964,7 @@ class PanelWindow(QWidget):
|
||||
f"""
|
||||
QWidget#bottom_bar {{
|
||||
background: {bar_bg2};
|
||||
border-radius: 0 0 10px 10px;
|
||||
border-radius: 0 0 12px 12px;
|
||||
border-top: 1px solid {t['panel_border']};
|
||||
}}
|
||||
QPushButton {{ border:none; background:transparent; border-radius:4px; }}
|
||||
@@ -951,6 +984,10 @@ class PanelWindow(QWidget):
|
||||
self.quit_btn.setIcon(qta.icon("fa5s.sign-out-alt", color=ic))
|
||||
self.quit_btn.setIconSize(QSize(14, 14))
|
||||
|
||||
# 托盘菜单跟随主题
|
||||
if getattr(self, "_tray_menu", None) is not None:
|
||||
self._tray_menu.setStyleSheet(self._bottom_menu_stylesheet())
|
||||
|
||||
for i in range(self.groups_layout.count() - 1):
|
||||
item = self.groups_layout.itemAt(i)
|
||||
if item and item.widget():
|
||||
@@ -1036,6 +1073,21 @@ class PanelWindow(QWidget):
|
||||
dlg = UnlockDialog(self)
|
||||
dlg.exec()
|
||||
|
||||
def _open_temperature(self):
|
||||
try:
|
||||
from ui.temperature import TemperatureWindow
|
||||
except Exception as e:
|
||||
dialog_style.warning(self, "无法打开硬件温度", f"加载模块失败:{e}")
|
||||
return
|
||||
# 非模态独立窗口:可与其它功能窗口同时打开、自由切换
|
||||
win = getattr(self, "_temperature_win", None)
|
||||
if win is None:
|
||||
win = TemperatureWindow(self)
|
||||
self._temperature_win = win
|
||||
win.show()
|
||||
win.raise_()
|
||||
win.activateWindow()
|
||||
|
||||
def _open_wechat_multi(self):
|
||||
from ui.wechat_multi import WechatMultiDialog
|
||||
dlg = WechatMultiDialog(self)
|
||||
@@ -1050,6 +1102,12 @@ class PanelWindow(QWidget):
|
||||
self._settings_win._apply_theme()
|
||||
except Exception:
|
||||
pass
|
||||
# 温度窗口同样跟随主题
|
||||
if hasattr(self, "_temperature_win") and self._temperature_win.isVisible():
|
||||
try:
|
||||
self._temperature_win._apply_theme()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _show_settings(self):
|
||||
from ui.settings_window import SettingsWindow
|
||||
@@ -1249,6 +1307,7 @@ class PanelWindow(QWidget):
|
||||
if was_visible:
|
||||
self.setGeometry(geo)
|
||||
self.show()
|
||||
self._apply_win32_blur(theme.name() == "dark")
|
||||
ball = getattr(self, "_ball_ref", None)
|
||||
if ball is not None:
|
||||
ball.set_stays_on_top(on_top)
|
||||
@@ -1256,6 +1315,10 @@ class PanelWindow(QWidget):
|
||||
def _toggle_pin(self):
|
||||
self._pinned = not self._pinned
|
||||
self._sync_pin_dependent_ui()
|
||||
if self._pinned:
|
||||
if getattr(self, "_is_collapsed", False):
|
||||
self._expand_panel()
|
||||
self._dock_state = "none"
|
||||
|
||||
def _on_min_click(self):
|
||||
"""图钉开启时收缩内容区,否则最小化到悬浮球"""
|
||||
@@ -1296,6 +1359,8 @@ class PanelWindow(QWidget):
|
||||
|
||||
def _save_geometry(self):
|
||||
"""把当前位置和尺寸写入数据库"""
|
||||
if getattr(self, "_is_collapsed", False):
|
||||
return
|
||||
g = self.geometry()
|
||||
database.set_setting("panel_x", str(g.x()))
|
||||
database.set_setting("panel_y", str(g.y()))
|
||||
@@ -1306,6 +1371,7 @@ class PanelWindow(QWidget):
|
||||
self._persist_h = g.height()
|
||||
database.set_setting("panel_w", str(g.width()))
|
||||
database.set_setting("panel_h", str(g.height()))
|
||||
self._check_dock_state()
|
||||
|
||||
def _restore_geometry(self):
|
||||
"""从数据库恢复上次的位置、尺寸和透明度"""
|
||||
@@ -1332,6 +1398,7 @@ class PanelWindow(QWidget):
|
||||
self.setWindowOpacity(max(30, min(100, opacity)) / 100)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
self._check_dock_state()
|
||||
|
||||
def _on_search(self, keyword):
|
||||
for i in range(self.groups_layout.count() - 1):
|
||||
@@ -1341,6 +1408,61 @@ class PanelWindow(QWidget):
|
||||
|
||||
# ── 显示/隐藏 ────────────────────────────────────────
|
||||
def show_near(self, ball_pos: QPoint, ball_size: int):
|
||||
self._is_hiding = False
|
||||
|
||||
if getattr(self, "_dock_state", "none") != "none":
|
||||
self._is_collapsed = False
|
||||
self._temp_disable_collapse = True
|
||||
|
||||
# 从窗口当前位置的屏幕中获取大小,恢复到边缘贴靠展开位置
|
||||
screen_obj = QApplication.screenAt(self.pos())
|
||||
if screen_obj is None:
|
||||
screen_obj = QApplication.primaryScreen()
|
||||
screen = screen_obj.availableGeometry()
|
||||
x, y, w, h = self.x(), self.y(), self.width(), self.height()
|
||||
|
||||
# 计算展开时的最终坐标
|
||||
if self._dock_state == "top":
|
||||
y = screen.top()
|
||||
elif self._dock_state == "left":
|
||||
x = screen.left()
|
||||
elif self._dock_state == "right":
|
||||
x = screen.right() - w
|
||||
|
||||
# 进场滑出偏移方向
|
||||
offset_x, offset_y = x, y
|
||||
if self._dock_state == "top":
|
||||
offset_y = y - 30
|
||||
elif self._dock_state == "left":
|
||||
offset_x = x - 30
|
||||
elif self._dock_state == "right":
|
||||
offset_x = x + 30
|
||||
|
||||
self.move(offset_x, offset_y)
|
||||
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(offset_x, offset_y))
|
||||
self._anim2.setEndValue(QPoint(x, y))
|
||||
self._anim2.finished.connect(self._save_geometry)
|
||||
self._anim2.start()
|
||||
return
|
||||
|
||||
# 根据球所在屏幕来定位 panel,避免跑到主屏幕
|
||||
screen_obj = QApplication.screenAt(ball_pos)
|
||||
if screen_obj is None:
|
||||
@@ -1354,6 +1476,8 @@ class PanelWindow(QWidget):
|
||||
x = bx + ball_size + 8
|
||||
y = max(screen.top(), min(by + ball_size // 2 - ph // 2, screen.bottom() - ph))
|
||||
|
||||
self._is_collapsed = False
|
||||
self._temp_disable_collapse = True
|
||||
self.move(x, y + 30)
|
||||
self.setWindowOpacity(0)
|
||||
self.show()
|
||||
@@ -1379,15 +1503,24 @@ class PanelWindow(QWidget):
|
||||
self._anim2.start()
|
||||
|
||||
def hide_panel(self):
|
||||
self._is_hiding = True
|
||||
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)
|
||||
|
||||
def _on_hide_finished():
|
||||
self.hide()
|
||||
self._is_hiding = False
|
||||
|
||||
self._anim.finished.connect(_on_hide_finished)
|
||||
self._anim.start()
|
||||
|
||||
def is_really_visible(self) -> bool:
|
||||
return self.isVisible() and not getattr(self, "_is_hiding", False)
|
||||
|
||||
def minimize_to_ball(self):
|
||||
self.hide_panel()
|
||||
if hasattr(self, "_ball_ref"):
|
||||
@@ -1518,6 +1651,8 @@ class PanelWindow(QWidget):
|
||||
self._do_resize(gp)
|
||||
return True
|
||||
if event.buttons() & Qt.MouseButton.LeftButton and self._win_drag_pos:
|
||||
self._dock_state = "none"
|
||||
self._is_collapsed = False
|
||||
self.move(gp - self._win_drag_pos)
|
||||
return True
|
||||
# 悬停时更新光标
|
||||
@@ -1528,6 +1663,7 @@ class PanelWindow(QWidget):
|
||||
self._resize_edge = None
|
||||
self._win_drag_pos = None
|
||||
self._update_cursor(event.globalPosition().toPoint())
|
||||
self._check_dock_state()
|
||||
self._save_geometry()
|
||||
|
||||
return super().eventFilter(obj, event)
|
||||
@@ -1538,6 +1674,8 @@ class PanelWindow(QWidget):
|
||||
self._do_resize(event.globalPosition().toPoint())
|
||||
return
|
||||
if event.buttons() & Qt.MouseButton.LeftButton and self._win_drag_pos:
|
||||
self._dock_state = "none"
|
||||
self._is_collapsed = False
|
||||
self.move(event.globalPosition().toPoint() - self._win_drag_pos)
|
||||
return
|
||||
self._update_cursor(event.globalPosition().toPoint())
|
||||
@@ -1563,6 +1701,7 @@ class PanelWindow(QWidget):
|
||||
self._resize_edge = None
|
||||
self._win_drag_pos = None
|
||||
self._update_cursor(event.globalPosition().toPoint())
|
||||
self._check_dock_state()
|
||||
self._save_geometry() # 拖动/resize 结束后保存位置和尺寸
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
@@ -1574,8 +1713,26 @@ class PanelWindow(QWidget):
|
||||
QTimer.singleShot(200, self._check_focus_lost)
|
||||
|
||||
def _check_focus_lost(self):
|
||||
if not self.isActiveWindow() and not self._pinned:
|
||||
self.hide_panel()
|
||||
if self._pinned or not self.isVisible():
|
||||
return
|
||||
if self.isActiveWindow():
|
||||
return
|
||||
# 焦点转移到了本面板弹出的对话框/子窗口(添加分组、设置、右键菜单等)时不隐藏
|
||||
active = QApplication.activeWindow()
|
||||
if active is not None and active is not self:
|
||||
p = active.parentWidget()
|
||||
while p is not None:
|
||||
if p is self:
|
||||
return
|
||||
p = p.parentWidget()
|
||||
if QApplication.activePopupWidget() is not None:
|
||||
return
|
||||
if getattr(self, "_dock_state", "none") != "none":
|
||||
self._temp_disable_collapse = False
|
||||
if not self._is_collapsed:
|
||||
self._collapse_panel()
|
||||
return
|
||||
self.hide_panel()
|
||||
|
||||
# ── 托盘 ─────────────────────────────────────────────
|
||||
def _setup_tray(self):
|
||||
@@ -1584,15 +1741,7 @@ class PanelWindow(QWidget):
|
||||
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; }
|
||||
"""
|
||||
)
|
||||
menu.setStyleSheet(self._bottom_menu_stylesheet())
|
||||
for tooltip, _icon, callback in self._get_quick_actions():
|
||||
menu.addAction(tooltip).triggered.connect(callback)
|
||||
menu.addSeparator()
|
||||
@@ -1600,6 +1749,7 @@ class PanelWindow(QWidget):
|
||||
menu.addAction("❌ 退出本程序").triggered.connect(QApplication.quit)
|
||||
|
||||
self.tray.setContextMenu(menu)
|
||||
self._tray_menu = menu # 主题切换时同步样式
|
||||
self.tray.activated.connect(self._on_tray_activated)
|
||||
self.tray.show()
|
||||
|
||||
@@ -1613,93 +1763,167 @@ class PanelWindow(QWidget):
|
||||
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 '关'}"
|
||||
)
|
||||
# ── 边缘吸附与收起 ─────────────────────────────────────
|
||||
def _check_dock_state(self):
|
||||
if self._pinned:
|
||||
self._dock_state = "none"
|
||||
return
|
||||
|
||||
screen = QApplication.screenAt(self.geometry().center())
|
||||
if not screen:
|
||||
screen = QApplication.primaryScreen()
|
||||
screen_geom = screen.geometry()
|
||||
|
||||
class SettingsPopup(QWidget):
|
||||
"""设置弹出面板:透明度调节等"""
|
||||
x, y, w, h = self.x(), self.y(), self.width(), self.height()
|
||||
threshold = 20
|
||||
|
||||
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()
|
||||
# 检测顶部吸附
|
||||
if abs(y - screen_geom.top()) <= threshold:
|
||||
self._dock_state = "top"
|
||||
self.move(x, screen_geom.top())
|
||||
# 检测左侧吸附
|
||||
elif abs(x - screen_geom.left()) <= threshold:
|
||||
self._dock_state = "left"
|
||||
self.move(screen_geom.left(), y)
|
||||
# 检测右侧吸附
|
||||
elif abs((x + w) - screen_geom.right()) <= threshold:
|
||||
self._dock_state = "right"
|
||||
self.move(screen_geom.right() - w, y)
|
||||
else:
|
||||
self._dock_state = "none"
|
||||
|
||||
def _build(self):
|
||||
from PyQt6.QtWidgets import QSlider, QLabel, QVBoxLayout, QHBoxLayout
|
||||
def _on_dock_timer_timeout(self):
|
||||
if not self.isVisible() or self._dock_state == "none" or self._pinned or self._win_drag_pos or self._resizing:
|
||||
return
|
||||
|
||||
t = theme.current()
|
||||
is_dark = theme.name() == "dark"
|
||||
cursor_pos = QCursor.pos()
|
||||
geom = self.geometry()
|
||||
is_hovered = geom.contains(cursor_pos)
|
||||
|
||||
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;
|
||||
}}
|
||||
"""
|
||||
)
|
||||
if not is_hovered:
|
||||
screen = QApplication.screenAt(cursor_pos)
|
||||
if screen:
|
||||
screen_geom = screen.geometry()
|
||||
tolerance = 3
|
||||
if self._dock_state == "top" and cursor_pos.y() <= screen_geom.top() + tolerance:
|
||||
if geom.left() <= cursor_pos.x() <= geom.right():
|
||||
is_hovered = True
|
||||
elif self._dock_state == "left" and cursor_pos.x() <= screen_geom.left() + tolerance:
|
||||
if geom.top() <= cursor_pos.y() <= geom.bottom():
|
||||
is_hovered = True
|
||||
elif self._dock_state == "right" and cursor_pos.x() >= screen_geom.right() - tolerance:
|
||||
if geom.top() <= cursor_pos.y() <= geom.bottom():
|
||||
is_hovered = True
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(14, 12, 14, 12)
|
||||
layout.setSpacing(10)
|
||||
if is_hovered:
|
||||
self._temp_disable_collapse = False
|
||||
if self._is_collapsed:
|
||||
self._expand_panel()
|
||||
else:
|
||||
if not self._is_collapsed and not getattr(self, "_temp_disable_collapse", False):
|
||||
self._collapse_panel()
|
||||
|
||||
# 透明度
|
||||
row = QHBoxLayout()
|
||||
row.setSpacing(10)
|
||||
lbl = QLabel("透明度")
|
||||
lbl.setStyleSheet(
|
||||
f"color:{t['search_color']}; font-size:12px; background:transparent; border:none;"
|
||||
)
|
||||
lbl.setFixedWidth(42)
|
||||
def _collapse_panel(self):
|
||||
if self._is_collapsed:
|
||||
return
|
||||
self._is_collapsed = True
|
||||
|
||||
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)
|
||||
screen = QApplication.screenAt(self.geometry().center())
|
||||
if not screen:
|
||||
screen = QApplication.primaryScreen()
|
||||
screen_geom = screen.geometry()
|
||||
|
||||
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)
|
||||
x, y, w, h = self.x(), self.y(), self.width(), self.height()
|
||||
target_x, target_y = x, y
|
||||
sliver = 3 # 边缘缩进后露出的像素宽度
|
||||
|
||||
row.addWidget(lbl)
|
||||
row.addWidget(self._opacity_slider)
|
||||
row.addWidget(self._opacity_val)
|
||||
layout.addLayout(row)
|
||||
if self._dock_state == "top":
|
||||
target_y = screen_geom.top() - h + sliver
|
||||
elif self._dock_state == "left":
|
||||
target_x = screen_geom.left() - w + sliver
|
||||
elif self._dock_state == "right":
|
||||
target_x = screen_geom.right() - sliver
|
||||
|
||||
self.adjustSize()
|
||||
self._animate_to(target_x, target_y)
|
||||
|
||||
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 _expand_panel(self):
|
||||
if not self._is_collapsed:
|
||||
return
|
||||
self._is_collapsed = False
|
||||
|
||||
screen = QApplication.screenAt(self.geometry().center())
|
||||
if not screen:
|
||||
screen = QApplication.primaryScreen()
|
||||
screen_geom = screen.geometry()
|
||||
|
||||
x, y, w, h = self.x(), self.y(), self.width(), self.height()
|
||||
target_x, target_y = x, y
|
||||
|
||||
if self._dock_state == "top":
|
||||
target_y = screen_geom.top()
|
||||
elif self._dock_state == "left":
|
||||
target_x = screen_geom.left()
|
||||
elif self._dock_state == "right":
|
||||
target_x = screen_geom.right() - w
|
||||
|
||||
self._animate_to(target_x, target_y)
|
||||
|
||||
def _animate_to(self, x, y):
|
||||
if hasattr(self, "_dock_anim") and self._dock_anim:
|
||||
self._dock_anim.stop()
|
||||
|
||||
self._dock_anim = QPropertyAnimation(self, b"pos")
|
||||
self._dock_anim.setDuration(200)
|
||||
self._dock_anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
self._dock_anim.setStartValue(self.pos())
|
||||
self._dock_anim.setEndValue(QPoint(x, y))
|
||||
if not self._is_collapsed:
|
||||
self._dock_anim.finished.connect(self.raise_)
|
||||
self._dock_anim.start()
|
||||
|
||||
def _apply_win32_blur(self, is_dark: bool):
|
||||
if sys.platform != "win32":
|
||||
return
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import windll, sizeof, byref, c_int, Structure
|
||||
|
||||
class ACCENT_POLICY(Structure):
|
||||
_fields_ = [
|
||||
("AccentState", c_int),
|
||||
("AccentFlags", c_int),
|
||||
("GradientColor", c_int),
|
||||
("AnimationId", c_int)
|
||||
]
|
||||
|
||||
class WINDOWCOMPOSITIONATTRIBDATA(Structure):
|
||||
_fields_ = [
|
||||
("Attribute", c_int),
|
||||
("Data", ctypes.c_void_p),
|
||||
("SizeOfData", ctypes.c_int)
|
||||
]
|
||||
|
||||
hwnd = int(self.winId())
|
||||
accent = ACCENT_POLICY()
|
||||
accent.AccentState = 4 # ACCENT_ENABLE_ACRYLICBLURBEHIND
|
||||
|
||||
if is_dark:
|
||||
# 磨砂黑色:深灰色半透明
|
||||
accent.GradientColor = 0x90202020
|
||||
else:
|
||||
# 磨砂白色:白色半透明
|
||||
accent.GradientColor = 0x90F5F5F5
|
||||
|
||||
accent.AccentFlags = 2
|
||||
|
||||
data = WINDOWCOMPOSITIONATTRIBDATA()
|
||||
data.Attribute = 19 # WCA_ACCENT_POLICY
|
||||
data.Data = ctypes.cast(byref(accent), ctypes.c_void_p)
|
||||
data.SizeOfData = sizeof(accent)
|
||||
|
||||
windll.user32.SetWindowCompositionAttribute(hwnd, byref(data))
|
||||
except Exception as e:
|
||||
print("Failed to apply win32 composition blur:", e)
|
||||
|
||||
|
||||
def _set_autostart(enable: bool):
|
||||
@@ -1715,7 +1939,8 @@ def _set_autostart(enable: bool):
|
||||
app_name = "DesktopOrganizer"
|
||||
if enable:
|
||||
exe = (
|
||||
f'"{sys.executable}" "{os.path.abspath("main.py")}"'
|
||||
# 用 __file__ 定位入口,避免从其它工作目录启动时写入错误路径
|
||||
f'"{sys.executable}" "{os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), "main.py"))}"'
|
||||
if not getattr(sys, "frozen", False)
|
||||
else f'"{sys.executable}"'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user