diff --git a/menubar.py b/menubar.py index 3690ce8..4bef8d9 100644 --- a/menubar.py +++ b/menubar.py @@ -333,6 +333,7 @@ class AppDelegate(NSObject): _history_entries_cache = objc.ivar() _history_entries_cache_fingerprint = objc.ivar() _quota_notifier = objc.ivar() + _switch_menu_action_taken = objc.ivar() language = objc.ivar() def initWithMock_interval_(self, mock: bool, interval: int) -> AppDelegate: @@ -359,6 +360,7 @@ def initWithMock_interval_(self, mock: bool, interval: int) -> AppDelegate: self._fs_stream = None self._history_entries_cache = None self._history_entries_cache_fingerprint = None + self._switch_menu_action_taken = False return self def applicationDidFinishLaunching_(self, notification: Any) -> None: @@ -518,13 +520,20 @@ def switchPanel_(self, sender: Any) -> None: butler_item.setState_(1 if _session_resume_enabled() else 0) butler_item.setToolTip_(_t(self.language, "project_butler_tooltip")) menu.addItem_(butler_item) + self._switch_menu_action_taken = False menu.popUpMenuPositioningItem_atLocation_inView_(None, NSMakePoint(0, 0), sender) + if self._switch_menu_action_taken: + self._resync_popover_after_menu() + else: + self._close_popover_after_menu() def selectPanel_(self, sender: Any) -> None: + self._mark_switch_menu_action() panel_id = str(sender.representedObject()) self._set_active_panel_id(panel_id) def toggleLaunchAtLogin_(self, sender: Any) -> None: + self._mark_switch_menu_action() try: if login_item.is_enabled(): login_item.disable() @@ -535,6 +544,7 @@ def toggleLaunchAtLogin_(self, sender: Any) -> None: logger.warning("toggle launch at login failed", exc_info=True) def toggleAutoUpdateCheck_(self, sender: Any) -> None: + self._mark_switch_menu_action() prefs = _load_preferences() enabled = not _auto_update_check_enabled(prefs) prefs["auto_update_check"] = enabled @@ -550,6 +560,7 @@ def toggleAutoUpdateCheck_(self, sender: Any) -> None: thread.start() def toggleHideCodex_(self, sender: Any) -> None: + self._mark_switch_menu_action() prefs = _load_preferences() enabled = not _hide_codex_enabled(prefs) prefs["hide_codex_section"] = enabled @@ -560,6 +571,7 @@ def toggleHideCodex_(self, sender: Any) -> None: self.popover_controller.setState_(self.latest_state) def toggleQuotaNotifications_(self, sender: Any) -> None: + self._mark_switch_menu_action() prefs = _load_preferences() enabled = not _quota_notifications_enabled(prefs) prefs["quota_notifications"] = enabled @@ -570,6 +582,7 @@ def toggleQuotaNotifications_(self, sender: Any) -> None: self._request_notification_authorization() def toggleSessionResume_(self, sender: Any) -> None: + self._mark_switch_menu_action() thread = threading.Thread(target=self._toggle_session_resume_in_background, daemon=True) thread.start() @@ -749,6 +762,28 @@ def _set_active_panel_id(self, panel_id: str) -> None: NSMinYEdge, ) + def _mark_switch_menu_action(self) -> None: + self._switch_menu_action_taken = True + + def _close_popover_after_menu(self) -> None: + if not hasattr(self, "popover") or self.popover is None: + return + if not self.popover.isShown(): + return + self.popover.performClose_(None) + + def _resync_popover_after_menu(self) -> None: + if not hasattr(self, "popover") or not hasattr(self, "popover_controller"): + return + if not hasattr(self, "status_item"): + return + if self.popover is None or self.popover_controller is None or self.status_item is None: + return + if not self.popover.isShown(): + return + self.popover_controller.setState_(self.latest_state) + self.popover.setContentSize_(_popover_size(self.latest_state, self.active_panel)) + def togglePopover_(self, sender: Any) -> None: if self.popover.isShown(): self.popover.performClose_(sender) diff --git a/tests/test_menubar.py b/tests/test_menubar.py index 7f41028..729ebaf 100644 --- a/tests/test_menubar.py +++ b/tests/test_menubar.py @@ -416,6 +416,81 @@ def test_switch_panel_menu_contains_update_items(monkeypatch: pytest.MonkeyPatch assert "Show in report" not in main_titles +def test_switch_panel_cancel_closes_visible_popover( + monkeypatch: pytest.MonkeyPatch, +) -> None: + class FakeController: + def __init__(self) -> None: + self.states: list[object] = [] + + def setState_(self, state: object) -> None: + self.states.append(state) + + class FakeButton: + def bounds(self) -> str: + return "button-bounds" + + class FakeStatusItem: + def __init__(self) -> None: + self._button = FakeButton() + + def button(self) -> FakeButton: + return self._button + + class FakePopover: + def __init__(self) -> None: + self.closed = 0 + self.sizes: list[object] = [] + self.shown: list[tuple[object, object, object]] = [] + + def isShown(self) -> bool: + return True + + def performClose_(self, sender: object) -> None: + self.closed += 1 + + def setContentSize_(self, size: object) -> None: + self.sizes.append(size) + + def showRelativeToRect_ofView_preferredEdge_( + self, + rect: object, + view: object, + edge: object, + ) -> None: + self.shown.append((rect, view, edge)) + + class FakePanel: + id = "classic" + codex_card_height = 0.0 + + def preferred_size(self) -> tuple[float, float]: + return (300.0, 400.0) + + delegate = menubar.AppDelegate.alloc().initWithMock_interval_(True, 60) + delegate.language = "en" + delegate.latest_state = menubar._empty_state(language="en") + delegate.active_panel = FakePanel() + delegate.popover_controller = FakeController() + delegate.popover = FakePopover() + delegate.status_item = FakeStatusItem() + + monkeypatch.setattr(menubar, "NSMenu", _FakeMenu) + monkeypatch.setattr(menubar, "NSMenuItem", _FakeMenuItem) + monkeypatch.setattr( + "menubar.panels.all_panels", + lambda: [SimpleNamespace(id="classic", i18n_key="panel_default_name")], + ) + monkeypatch.setattr("menubar.login_item.is_enabled", lambda: False) + + menubar.AppDelegate.switchPanel_(delegate, object()) + + assert delegate.popover.closed == 1 + assert delegate.popover_controller.states == [] + assert delegate.popover.sizes == [] + assert delegate.popover.shown == [] + + def test_auto_update_disabled_skips_background_check(monkeypatch: pytest.MonkeyPatch) -> None: called = False