diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index f0cb7c82..0e2f7129 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -695,10 +695,8 @@ class SerenaDashboardViewer: height: int = 900, ): self.url = url + # Use system tray to allow hiding to tray and intercepting close to hide instead of quit self.tray = True - """ - whether we use the system tray to allow minimizing to tray and intercepting the close event to hide to tray instead of quitting. - """ self.title = "Serena Dashboard" self.width = width self.height = height @@ -733,22 +731,18 @@ class SerenaDashboardViewer: icon_path = str(dashboard_path / icon_filename) # Create hidden to avoid flash; show/restore/minimize in start callback. - # When tray is enabled, confirm_close allows us to intercept the X button. window = webview.create_window( self.title, self.url, width=self.width, height=self.height, hidden=self.start_minimized, - confirm_close=False, ) assert window is not None self.window = window if self.tray: self.window.events.closing += self._on_closing - - if self.tray: self._start_tray() def _start_callback() -> None: @@ -783,26 +777,32 @@ class SerenaDashboardViewer: from AppKit import NSApplication, NSObject from objc import python_method # type: ignore[import-untyped] - viewer = self # capture reference for the delegate + viewer = self - # Use a unique class name to avoid conflict with pywebview's AppDelegate class SerenaDockDelegate(NSObject): + """Wraps pywebview's delegate to handle dock icon clicks.""" + @python_method def initWithOriginalDelegate_(self, original: Any) -> Any: obj = self.init() if obj is not None: - obj._original_delegate = original + obj._original = original return obj + @python_method + def _forward(self, method: str, *args: Any) -> Any: + """Forward method call to original delegate if it has the method.""" + orig = getattr(self, "_original", None) + if orig and hasattr(orig, method): + return getattr(orig, method)(*args) + return None + def applicationShouldHandleReopen_hasVisibleWindows_(self, sender: Any, has_visible_windows: bool) -> bool: """Called when dock icon is clicked.""" - if not has_visible_windows and viewer.window: + if not has_visible_windows: viewer._show_window() - # Forward to original delegate if it exists and has this method - if hasattr(self, "_original_delegate") and self._original_delegate is not None: - if hasattr(self._original_delegate, "applicationShouldHandleReopen_hasVisibleWindows_"): - return self._original_delegate.applicationShouldHandleReopen_hasVisibleWindows_(sender, has_visible_windows) - return True + result = self._forward("applicationShouldHandleReopen_hasVisibleWindows_", sender, has_visible_windows) + return result if result is not None else True def applicationDidBecomeActive_(self, notification: Any) -> None: """Fallback for dock activation paths that do not invoke reopen.""" @@ -810,21 +810,16 @@ class SerenaDashboardViewer: viewer._suppress_next_activate_reopen = False elif viewer._window_hidden_to_tray: viewer._show_window() - # Forward to original delegate if it exists and has this method - if hasattr(self, "_original_delegate") and self._original_delegate is not None: - if hasattr(self._original_delegate, "applicationDidBecomeActive_"): - self._original_delegate.applicationDidBecomeActive_(notification) + self._forward("applicationDidBecomeActive_", notification) def forwardingTargetForSelector_(self, selector: Any) -> Any: """Forward unknown selectors to original delegate.""" - if hasattr(self, "_original_delegate") and self._original_delegate is not None: - if self._original_delegate.respondsToSelector_(selector): - return self._original_delegate - return None + orig = getattr(self, "_original", None) + return orig if orig and orig.respondsToSelector_(selector) else None ns_app = NSApplication.sharedApplication() original_delegate = ns_app.delegate() - if original_delegate is getattr(self, "_app_delegate", None): + if original_delegate is self._app_delegate: return self._app_delegate = SerenaDockDelegate.alloc().initWithOriginalDelegate_(original_delegate) ns_app.setDelegate_(self._app_delegate) diff --git a/test/serena/test_dashboard.py b/test/serena/test_dashboard.py index 578c06cd..8de6e8eb 100644 --- a/test/serena/test_dashboard.py +++ b/test/serena/test_dashboard.py @@ -1,8 +1,6 @@ -import sys from types import SimpleNamespace -import serena.dashboard as dashboard_module -from serena.dashboard import SerenaDashboardAPI, SerenaDashboardViewer +from serena.dashboard import SerenaDashboardAPI from solidlsp.ls_config import Language @@ -55,106 +53,3 @@ def test_available_languages_exclude_project_languages(): assert Language.MARKDOWN.value not in available # ensure experimental languages remain available for selection assert Language.ANSIBLE.value in available - - -def test_dashboard_viewer_initializes_macos_delegate_slot(): - viewer = SerenaDashboardViewer("http://localhost:24282/dashboard/index.html") - assert viewer._app_delegate is None - assert viewer._window_hidden_to_tray is False - assert viewer._suppress_next_activate_reopen is False - - -def test_dashboard_viewer_initializes_hidden_state_for_minimized_start(): - viewer = SerenaDashboardViewer("http://localhost:24282/dashboard/index.html", start_minimized=True) - assert viewer._window_hidden_to_tray is True - assert viewer._suppress_next_activate_reopen is True - - -def test_dashboard_viewer_show_and_hide_helpers_update_state(): - viewer = SerenaDashboardViewer("http://localhost:24282/dashboard/index.html", start_minimized=True) - - class _FakeWindow: - def __init__(self) -> None: - self.calls: list[str] = [] - - def show(self) -> None: - self.calls.append("show") - - def restore(self) -> None: - self.calls.append("restore") - - def hide(self) -> None: - self.calls.append("hide") - - viewer.window = _FakeWindow() - - viewer._show_window() - assert viewer.window.calls == ["show", "restore"] - assert viewer._window_hidden_to_tray is False - assert viewer._suppress_next_activate_reopen is False - - viewer._hide_window() - assert viewer.window.calls == ["show", "restore", "hide"] - assert viewer._window_hidden_to_tray is True - - -def test_dashboard_viewer_installs_macos_dock_handler_after_pywebview_start(monkeypatch): - order: list[str] = [] - - class _ClosingEvent: - def __iadd__(self, _handler): - order.append("bind-closing") - return self - - class _FakeWindow: - def __init__(self) -> None: - self.events = SimpleNamespace(closing=_ClosingEvent()) - - def minimize(self) -> None: - order.append("minimize") - - def show(self) -> None: - order.append("show") - - def restore(self) -> None: - order.append("restore") - - fake_window = _FakeWindow() - - def fake_create_window(*args, **kwargs): - order.append("create-window") - return fake_window - - def fake_start(callback, **kwargs): - del kwargs - order.append("start") - callback() - - class _FakeAppHelperModule: - @staticmethod - def callAfter(func, *args, **kwargs): - del args, kwargs - order.append("schedule-dock-handler") - func() - - monkeypatch.setattr(dashboard_module.sys, "platform", "darwin") - monkeypatch.setattr(dashboard_module.webview, "create_window", fake_create_window) - monkeypatch.setattr(dashboard_module.webview, "start", fake_start) - monkeypatch.setitem(sys.modules, "PyObjCTools.AppHelper", _FakeAppHelperModule) - - viewer = SerenaDashboardViewer("http://localhost:24282/dashboard/index.html") - monkeypatch.setattr(viewer, "_start_tray", lambda: order.append("start-tray")) - monkeypatch.setattr(viewer, "_setup_macos_dock_handler", lambda: order.append("setup-dock-handler")) - - viewer.run() - - assert order == [ - "create-window", - "bind-closing", - "start-tray", - "start", - "schedule-dock-handler", - "setup-dock-handler", - "show", - "restore", - ]