402 lines
14 KiB
Diff
402 lines
14 KiB
Diff
--- before/wechat_gui_qt.py
|
|
+++ C:\kefu\wechat_rpa\wechat_gui_qt.py
|
|
@@ -66,6 +66,7 @@
|
|
QListWidget,
|
|
QListWidgetItem,
|
|
QMainWindow,
|
|
+ QMenu,
|
|
QMessageBox,
|
|
QPlainTextEdit,
|
|
QProgressBar,
|
|
@@ -78,6 +79,7 @@
|
|
QStyle,
|
|
QStyleOption,
|
|
QStyleOptionButton,
|
|
+ QSystemTrayIcon,
|
|
QTableWidget,
|
|
QTableWidgetItem,
|
|
QTextEdit,
|
|
@@ -9255,6 +9257,40 @@
|
|
"正在停止" if state == "stopping" else "已停止")
|
|
|
|
|
|
+class _ApplicationQuitFilter(QObject):
|
|
+ """An explicit application quit must bypass window close-to-tray."""
|
|
+ def __init__(self, window, parent=None):
|
|
+ super().__init__(parent)
|
|
+ self.window = window
|
|
+
|
|
+ def eventFilter(self, watched, event) -> bool:
|
|
+ if event.type() == QEvent.Type.Quit:
|
|
+ self.window._exit_requested = True
|
|
+ return False
|
|
+
|
|
+
|
|
+def _consume_instance_activation(app, guard, window=None) -> None:
|
|
+ # The login/update dialog runs a nested event loop before MainWindow is
|
|
+ # constructed. Do not consume a launch request until a window exists.
|
|
+ target = app.activeModalWidget()
|
|
+ if target is None:
|
|
+ target = window or app.activeWindow()
|
|
+ if target is None:
|
|
+ target = next((widget for widget in app.topLevelWidgets()
|
|
+ if isinstance(widget, QDialog) and widget.isVisible()), None)
|
|
+ if target is None or getattr(target, "_exit_requested", False):
|
|
+ return
|
|
+ if not guard.consume_activation():
|
|
+ return
|
|
+ if isinstance(target, MainWindow):
|
|
+ target.restore_from_tray()
|
|
+ else:
|
|
+ target.setWindowState(target.windowState() & ~Qt.WindowMinimized)
|
|
+ target.show()
|
|
+ target.raise_()
|
|
+ target.activateWindow()
|
|
+
|
|
+
|
|
class MainWindow(QMainWindow):
|
|
desktopSessionChecked = Signal(str, object)
|
|
wechatDatabaseLoaded = Signal(object)
|
|
@@ -9334,11 +9370,7 @@
|
|
except Exception:
|
|
pass
|
|
|
|
- self.capsule.hide()
|
|
- self.showNormal()
|
|
- self.show()
|
|
- self.raise_()
|
|
- self.activateWindow()
|
|
+ self.restore_from_tray()
|
|
QMessageBox.warning(
|
|
self,
|
|
"账号已被强制下线",
|
|
@@ -9349,7 +9381,7 @@
|
|
|
|
account = ensure_desktop_login(self)
|
|
if not account:
|
|
- QApplication.quit()
|
|
+ self.request_exit()
|
|
return
|
|
self._set_account_menu_title(account)
|
|
self._desktop_session_state = "valid"
|
|
@@ -9379,7 +9411,7 @@
|
|
|
|
backend_client.desktop_logout()
|
|
finally:
|
|
- QApplication.quit()
|
|
+ self.request_exit()
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
@@ -9493,6 +9525,7 @@
|
|
root_layout.addWidget(self.console_shell, 1)
|
|
|
|
self.capsule = CapsuleWindow()
|
|
+ self._initialize_system_tray()
|
|
self._connect_signals()
|
|
self.business_refresh_timer = QTimer(self)
|
|
self.business_refresh_timer.setSingleShot(True)
|
|
@@ -11343,12 +11376,18 @@
|
|
self._schedule_console_push(urgent=True)
|
|
|
|
def changeEvent(self, event) -> None:
|
|
+ if (event.type() == QEvent.Type.WindowStateChange and not self.isMinimized()
|
|
+ and not getattr(self, "_tray_hidden", False)):
|
|
+ self._close_minimized = False
|
|
if event.type() == QEvent.Type.WindowStateChange and hasattr(self, "_scale_timer"):
|
|
self._scale_timer.start()
|
|
if (
|
|
event.type() == QEvent.Type.WindowStateChange
|
|
and self.isMinimized()
|
|
and getattr(self, "_running", False)
|
|
+ and not getattr(self, "_tray_hidden", False)
|
|
+ and not getattr(self, "_close_minimized", False)
|
|
+ and not getattr(self, "_exit_requested", False)
|
|
and bool(self.runtime_settings.get("minimize_on_close", True))
|
|
and not self.capsule.isVisible()
|
|
and not self._capsule_minimize_pending
|
|
@@ -11382,6 +11421,9 @@
|
|
self._capsule_minimize_pending = False
|
|
if (
|
|
not self._running
|
|
+ or getattr(self, "_tray_hidden", False)
|
|
+ or getattr(self, "_close_minimized", False)
|
|
+ or getattr(self, "_exit_requested", False)
|
|
or not self.isMinimized()
|
|
or self.capsule.isVisible()
|
|
):
|
|
@@ -11399,6 +11441,9 @@
|
|
was_maximized: bool | None = None,
|
|
portal_was_visible: bool | None = None,
|
|
) -> None:
|
|
+ if (getattr(self, "_tray_hidden", False) or getattr(self, "_close_minimized", False)
|
|
+ or getattr(self, "_exit_requested", False)):
|
|
+ return
|
|
if self.capsule.isVisible():
|
|
return
|
|
self._saved_geometry = (
|
|
@@ -11432,6 +11477,8 @@
|
|
self.hide()
|
|
|
|
def expand_console(self) -> None:
|
|
+ self._tray_hidden = False
|
|
+ self._close_minimized = False
|
|
self.capsule.hide()
|
|
if self._saved_geometry is not None:
|
|
self.restoreGeometry(self._saved_geometry)
|
|
@@ -11446,11 +11493,14 @@
|
|
self._console_was_visible = False
|
|
if getattr(self, "_portal_was_visible", False) and self.stack.currentIndex() == 0:
|
|
def restore_portal() -> None:
|
|
+ if (getattr(self, "_exit_requested", False)
|
|
+ or getattr(self, "_tray_hidden", False) or not self.isVisible()):
|
|
+ return
|
|
if self.portal_page.view is not None:
|
|
self.portal_page.view.show()
|
|
self.portal_page.view.setFocus(Qt.OtherFocusReason)
|
|
|
|
- QTimer.singleShot(80, restore_portal)
|
|
+ QTimer.singleShot(80, self, restore_portal)
|
|
self._portal_was_visible = False
|
|
|
|
def _finish_thread(self, final_state: str) -> None:
|
|
@@ -11541,7 +11591,7 @@
|
|
self.queue_page.apply_visual_state(data)
|
|
except KeyboardInterrupt:
|
|
# Qt 默认会把 Ctrl+C 打断留在当前槽函数中并打印堆栈;转为正常关闭。
|
|
- QTimer.singleShot(0, self.close)
|
|
+ QTimer.singleShot(0, self.request_exit)
|
|
return
|
|
except queue.Empty:
|
|
pass
|
|
@@ -11552,34 +11602,153 @@
|
|
self.dashboard_page.timer.setText(f"运行时长 {runtime}")
|
|
self.capsule.timer.setText(runtime)
|
|
|
|
- def closeEvent(self, event) -> None:
|
|
- if (
|
|
- self._running
|
|
- and bool(self.runtime_settings.get("keep_background", True))
|
|
- and not self.capsule.isVisible()
|
|
- ):
|
|
- event.ignore()
|
|
- self.enter_capsule_mode()
|
|
- return
|
|
- self.save_runtime_settings()
|
|
+ def _initialize_system_tray(self) -> None:
|
|
+ self._tray_icon = None
|
|
+ self._tray_hidden = False
|
|
+ self._close_minimized = False
|
|
+ self._exit_requested = False
|
|
+ self._shutdown_complete = False
|
|
+ app = QApplication.instance()
|
|
+ self._quit_filter = _ApplicationQuitFilter(self, app)
|
|
+ app.installEventFilter(self._quit_filter)
|
|
+ app.aboutToQuit.connect(self._shutdown_application)
|
|
+ if "--qt-smoke-test" not in sys.argv:
|
|
+ self._ensure_system_tray()
|
|
+
|
|
+ def _ensure_system_tray(self) -> bool:
|
|
+ if not QSystemTrayIcon.isSystemTrayAvailable():
|
|
+ return False
|
|
+ if getattr(self, "_tray_icon", None) is None:
|
|
+ tray = QSystemTrayIcon(_app_icon(), self)
|
|
+ tray.setToolTip(WIN_TITLE)
|
|
+ menu = QMenu(self)
|
|
+ open_action = menu.addAction("打开主界面")
|
|
+ open_action.triggered.connect(self.restore_from_tray)
|
|
+ menu.addSeparator()
|
|
+ exit_action = menu.addAction("退出程序")
|
|
+ exit_action.triggered.connect(self.request_exit)
|
|
+ tray.setContextMenu(menu)
|
|
+ tray.activated.connect(self._tray_activated)
|
|
+ tray.messageClicked.connect(self.restore_from_tray)
|
|
+ self._tray_menu = menu
|
|
+ self._tray_icon = tray
|
|
+ self._tray_icon.show()
|
|
+ return self._tray_icon.isVisible()
|
|
+
|
|
+ def _tray_activated(self, reason) -> None:
|
|
+ if reason in (QSystemTrayIcon.ActivationReason.Trigger,
|
|
+ QSystemTrayIcon.ActivationReason.DoubleClick):
|
|
+ self.restore_from_tray()
|
|
+
|
|
+ def _hide_to_system_tray(self) -> None:
|
|
+ if not self._ensure_system_tray():
|
|
+ # Without a tray (e.g. Explorer restarting), remain reachable on
|
|
+ # the taskbar and suppress the listener's floating-capsule path.
|
|
+ if not self.isVisible() or self.capsule.isVisible():
|
|
+ self.expand_console()
|
|
+ self._close_minimized = True
|
|
+ self._tray_hidden = False
|
|
+ self.capsule.hide()
|
|
+ self.showMinimized()
|
|
+ return
|
|
+ self._tray_hidden = True
|
|
+ self._close_minimized = False
|
|
+ self._capsule_minimize_pending = False
|
|
+ if self.isVisible():
|
|
+ self._saved_geometry = self.saveGeometry()
|
|
+ self._was_maximized = bool(self.windowState() & Qt.WindowMaximized)
|
|
+ console = getattr(self, "console_shell", None)
|
|
+ if console is not None:
|
|
+ self._console_was_visible = console.isVisible()
|
|
+ console.hide()
|
|
+ portal = self.portal_page.view
|
|
+ self._portal_was_visible = bool(portal is not None and portal.isVisible())
|
|
+ if self._portal_was_visible:
|
|
+ portal.hide()
|
|
+ QApplication.processEvents()
|
|
+ self.capsule.hide()
|
|
+ self.hide()
|
|
+
|
|
+ def restore_from_tray(self) -> None:
|
|
+ if getattr(self, "_exit_requested", False):
|
|
+ return
|
|
+ if not self.isVisible() or self.isMinimized() or self.capsule.isVisible():
|
|
+ if self.isMinimized() and not getattr(self, "_tray_hidden", False):
|
|
+ self._was_maximized = bool(self.windowState() & Qt.WindowMaximized)
|
|
+ self.expand_console()
|
|
+ else:
|
|
+ self.raise_()
|
|
+ self.activateWindow()
|
|
+
|
|
+ def request_exit(self) -> None:
|
|
+ self._exit_requested = True
|
|
+ self.close()
|
|
+ QApplication.quit()
|
|
+
|
|
+ def _shutdown_application(self) -> None:
|
|
+ if getattr(self, "_shutdown_complete", False):
|
|
+ return
|
|
+ self._exit_requested = True
|
|
+ self._shutdown_complete = True
|
|
self._running = False
|
|
+ for name in ("queue_timer", "queue_page_timer", "backend_sync_timer",
|
|
+ "desktop_session_timer", "business_refresh_timer",
|
|
+ "_console_push_timer", "_scale_timer"):
|
|
+ timer = getattr(self, name, None)
|
|
+ if timer is not None:
|
|
+ timer.stop()
|
|
+ try:
|
|
+ self.save_runtime_settings()
|
|
+ except Exception:
|
|
+ pass
|
|
if bool(self.runtime_settings.get("clear_clipboard_on_exit", True)):
|
|
try:
|
|
QApplication.clipboard().clear()
|
|
except Exception:
|
|
pass
|
|
- if self._thread is not None and self._thread.is_alive():
|
|
- self._thread.stop()
|
|
- if sys.stdout is self._stdout_proxy:
|
|
+ worker = getattr(self, "_thread", None)
|
|
+ if worker is not None and worker.is_alive():
|
|
+ worker.stop()
|
|
+ proxy = getattr(self, "_stdout_proxy", None)
|
|
+ if proxy is not None and sys.stdout is proxy:
|
|
sys.stdout = self._original_stdout
|
|
+ if proxy is not None:
|
|
+ try:
|
|
+ proxy.close()
|
|
+ except Exception:
|
|
+ pass
|
|
+ self._stdout_proxy = None
|
|
+ tray = getattr(self, "_tray_icon", None)
|
|
+ if tray is not None:
|
|
+ tray.hide()
|
|
self.capsule.close()
|
|
try:
|
|
+ from archive_auto_backup import stop_auto_backup
|
|
+ stop_auto_backup()
|
|
+ except Exception:
|
|
+ pass
|
|
+ try:
|
|
from dsh_agent import close_desk
|
|
-
|
|
close_desk()
|
|
except Exception:
|
|
pass
|
|
+
|
|
+ def closeEvent(self, event) -> None:
|
|
+ if not hasattr(self, "runtime_settings"):
|
|
+ event.accept()
|
|
+ return
|
|
+ app = QApplication.instance()
|
|
+ if app is not None and app.isSavingSession():
|
|
+ self._exit_requested = True
|
|
+ if (not getattr(self, "_exit_requested", False)
|
|
+ and bool(self.runtime_settings.get("minimize_on_close", True))):
|
|
+ event.ignore()
|
|
+ self._hide_to_system_tray()
|
|
+ return
|
|
+ self._shutdown_application()
|
|
event.accept()
|
|
+ QApplication.quit()
|
|
+
|
|
|
|
|
|
def _app_icon() -> QIcon:
|
|
@@ -11610,8 +11779,7 @@
|
|
if shutdown_requested:
|
|
return
|
|
shutdown_requested = True
|
|
- QTimer.singleShot(0, window.close)
|
|
- QTimer.singleShot(0, app.quit)
|
|
+ QTimer.singleShot(0, window.request_exit)
|
|
|
|
handled = [signal.SIGINT]
|
|
if hasattr(signal, "SIGBREAK"):
|
|
@@ -12042,6 +12210,41 @@
|
|
if "--packaging-self-check" in sys.argv:
|
|
raise SystemExit(run_packaging_self_check(app))
|
|
|
|
+ if "--qt-smoke-test" in sys.argv:
|
|
+ return _run_desktop_session(app, [None])
|
|
+
|
|
+ from desktop_instance import DesktopInstanceGuard
|
|
+ guard = None
|
|
+ activation_timer = None
|
|
+ try:
|
|
+ try:
|
|
+ guard = DesktopInstanceGuard()
|
|
+ primary = guard.acquire()
|
|
+ except OSError as exc:
|
|
+ QMessageBox.critical(None, "无法启动客户端", f"无法建立单实例保护,请稍后重试。\n{exc}")
|
|
+ return
|
|
+ if not primary:
|
|
+ try:
|
|
+ guard.request_activation()
|
|
+ except OSError:
|
|
+ pass
|
|
+ return
|
|
+ window_holder = [None]
|
|
+ activation_timer = QTimer(app)
|
|
+ activation_timer.setInterval(250)
|
|
+ activation_timer.timeout.connect(
|
|
+ lambda: _consume_instance_activation(app, guard, window_holder[0]))
|
|
+ activation_timer.start()
|
|
+ return _run_desktop_session(app, window_holder)
|
|
+ finally:
|
|
+ if activation_timer is not None:
|
|
+ activation_timer.stop()
|
|
+ activation_timer.deleteLater()
|
|
+ if guard is not None:
|
|
+ guard.close()
|
|
+
|
|
+
|
|
+def _run_desktop_session(app, window_holder) -> None:
|
|
if "--qt-smoke-test" not in sys.argv:
|
|
from desktop_login import ensure_desktop_login
|
|
|
|
@@ -12049,6 +12252,9 @@
|
|
return
|
|
|
|
window = MainWindow()
|
|
+ window_holder[0] = window
|
|
+ if "--qt-smoke-test" not in sys.argv:
|
|
+ app.setQuitOnLastWindowClosed(False)
|
|
console_shutdown = install_console_shutdown_handler(app, window)
|
|
window.show()
|
|
QTimer.singleShot(160, window.portal_page.ensure_view)
|