mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 18:16:43 +00:00
Refactor SerenaDashboardViewer to improve macOS dock handling and streamline event delegation
This commit is contained in:
1 parent
4b0f408e3e
commit
7c202de493
2 files changed
+21
-131
No files matched your search
+20
-25
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
Reference in new issue
Block a user