Files
kefu/deploy/single-instance-tray-20260917/update_qt_lifecycle.py
T
2026-09-21 10:34:06 +08:00

260 lines
12 KiB
Python

from pathlib import Path
import os
import difflib
ARTIFACT = Path(__file__).parent
TRAY_METHODS = ''' 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.
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
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 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()
'''
QUIT_FILTER = '''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()
'''
MAIN_GUARD = ''' if "--qt-smoke-test" in sys.argv:
return _run_desktop_session(app, [None])
from desktop_instance import DesktopInstanceGuard
guard = DesktopInstanceGuard()
activation_timer = None
try:
try:
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()
guard.close()
def _run_desktop_session(app, window_holder) -> None:
'''
for root in (Path('C:/wechat_rpa'), Path('C:/kefu/wechat_rpa')):
path = root / 'wechat_gui_qt.py'
old = path.read_text(encoding='utf-8')
backup = root / 'backups/single-instance-tray-20260917/wechat_gui_qt.py'
backup.parent.mkdir(parents=True, exist_ok=True)
if not backup.exists():
backup.write_text(old, encoding='utf-8')
s = old
assert 'def _initialize_system_tray' not in s
s = s.replace(' QMainWindow,\n', ' QMainWindow,\n QMenu,\n')
s = s.replace(' QStyleOptionButton,\n', ' QStyleOptionButton,\n QSystemTrayIcon,\n')
s = s.replace(' self.capsule = CapsuleWindow()\n', ' self.capsule = CapsuleWindow()\n self._initialize_system_tray()\n', 1)
start = s.index(' def closeEvent(self, event) -> None:', s.index('class MainWindow('))
end = s.index('\ndef _app_icon()', start)
s = s[:start] + TRAY_METHODS + '\n' + s[end:]
s = s.replace(' and getattr(self, "_running", False)\n', ' and getattr(self, "_running", False)\n and not getattr(self, "_tray_hidden", False)\n and not getattr(self, "_close_minimized", False)\n and not getattr(self, "_exit_requested", False)\n', 1)
s = s.replace(' not self._running\n or not self.isMinimized()', ' not self._running\n or getattr(self, "_tray_hidden", False)\n or getattr(self, "_close_minimized", False)\n or getattr(self, "_exit_requested", False)\n or not self.isMinimized()', 1)
marker = ' def enter_capsule_mode('
begin = s.index(marker)
pos = s.index(' if self.capsule.isVisible():', begin)
s = s[:pos] + ' if (getattr(self, "_tray_hidden", False) or getattr(self, "_close_minimized", False)\n or getattr(self, "_exit_requested", False)):\n return\n' + s[pos:]
s = s.replace(' def expand_console(self) -> None:\n self.capsule.hide()', ' def expand_console(self) -> None:\n self._tray_hidden = False\n self._close_minimized = False\n self.capsule.hide()', 1)
s = s.replace(' QTimer.singleShot(0, self.close)', ' QTimer.singleShot(0, self.request_exit)')
s = s.replace(' QTimer.singleShot(0, window.close)\n QTimer.singleShot(0, app.quit)', ' QTimer.singleShot(0, window.request_exit)')
s = s.replace(' if not account:\n QApplication.quit()', ' if not account:\n self.request_exit()', 1)
s = s.replace(' finally:\n QApplication.quit()\n\n def __init__', ' finally:\n self.request_exit()\n\n def __init__', 1)
# Keep diagnostic branches completely outside the interactive guard.
main = s.index('def main() -> None:')
body = s.index(' if "--qt-smoke-test" not in sys.argv:', main)
s = s[:body] + MAIN_GUARD + s[body:]
session = s.index('def _run_desktop_session(')
pos = s.index(' window = MainWindow()\n', session)
s = s[:pos] + s[pos:].replace(' window = MainWindow()\n', ' window = MainWindow()\n window_holder[0] = window\n if "--qt-smoke-test" not in sys.argv:\n app.setQuitOnLastWindowClosed(False)\n', 1)
s = s.replace('class MainWindow(QMainWindow):', QUIT_FILTER + 'class MainWindow(QMainWindow):', 1)
compile(s, str(path), 'exec')
tmp = path.with_suffix('.tray.tmp')
tmp.write_text(s, encoding='utf-8')
os.replace(tmp, path)
diffname = 'production-qt.diff' if root == Path('C:/wechat_rpa') else 'source-qt.diff'
(ARTIFACT / diffname).write_text(''.join(difflib.unified_diff(old.splitlines(True), s.splitlines(True), fromfile=str(backup), tofile=str(path))), encoding='utf-8')
print(root, 'updated')