260 lines
12 KiB
Python
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')
|