diff --git a/src/cthulhu/compositor_state_adapter.py b/src/cthulhu/compositor_state_adapter.py index 6e28f7b..fbc4def 100644 --- a/src/cthulhu/compositor_state_adapter.py +++ b/src/cthulhu/compositor_state_adapter.py @@ -33,12 +33,8 @@ class CompositorStateAdapter: def __init__( self, - app: Optional[Any] = None, - event_manager: Optional[Any] = None, workspace_backends: Optional[Iterable[Any]] = None, ) -> None: - self.app = app - self.event_manager = event_manager self._workspaceBackends = list(workspace_backends or []) self._workspaceBackend: Optional[Any] = None self._listeners: list[Callable[[CompositorStateEvent], None]] = [] @@ -59,7 +55,7 @@ class CompositorStateAdapter: return get_session_type() def activate(self) -> None: - self._workspaceBackend = None + self._deactivate_current_backend() for backend in self._workspaceBackends: if not self._backend_is_available(backend): continue @@ -70,9 +66,7 @@ class CompositorStateAdapter: self.sync_accessible_context("activate") def deactivate(self) -> None: - if self._workspaceBackend is not None: - self._backend_deactivate(self._workspaceBackend) - self._workspaceBackend = None + self._deactivate_current_backend() cthulhu_state.pauseAtspiChurn = False def sync_accessible_context(self, reason: str) -> DesktopContextSnapshot: @@ -83,6 +77,7 @@ class CompositorStateAdapter: locus_of_focus_snapshot = self._build_object_snapshot(locus_of_focus) snapshot = DesktopContextSnapshot( session_type=self.get_session_type(), + backend_name=self._backend_name(), active_window_token=active_window_snapshot["token"], locus_of_focus_token=locus_of_focus_snapshot["token"], active_window_pid=active_window_snapshot["pid"], @@ -175,3 +170,20 @@ class CompositorStateAdapter: backend.deactivate(self) except TypeError: backend.deactivate() + + def _deactivate_current_backend(self) -> None: + if self._workspaceBackend is None: + return + + self._backend_deactivate(self._workspaceBackend) + self._workspaceBackend = None + + def _backend_name(self) -> str: + if self._workspaceBackend is None: + return "" + + name = getattr(self._workspaceBackend, "name", "") + if name: + return str(name) + + return self._workspaceBackend.__class__.__name__ diff --git a/src/cthulhu/compositor_state_types.py b/src/cthulhu/compositor_state_types.py index 9be6d5a..1ee16e1 100644 --- a/src/cthulhu/compositor_state_types.py +++ b/src/cthulhu/compositor_state_types.py @@ -26,6 +26,7 @@ class DesktopContextSnapshot: """Immutable snapshot of the desktop context tracked by the adapter.""" session_type: str + backend_name: str = "" active_window_token: str = "" locus_of_focus_token: str = "" active_window_pid: int = -1 diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index 8f0e71b..baf180f 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -957,10 +957,7 @@ class Cthulhu(GObject.Object): self.resourceManager: resource_manager.ResourceManager = resource_manager.ResourceManager(self) self.settingsManager: SettingsManager = settings_manager.SettingsManager(self) # Directly instantiate self.eventManager: EventManager = event_manager.EventManager(self) # Directly instantiate - self.compositorStateAdapter: CompositorStateAdapter = compositor_state_adapter.CompositorStateAdapter( - self, - self.eventManager, - ) + self.compositorStateAdapter: CompositorStateAdapter = compositor_state_adapter.CompositorStateAdapter() self.eventManager.set_compositor_state_adapter(self.compositorStateAdapter) self.scriptManager: ScriptManager = script_manager.ScriptManager(self) # Directly instantiate script_manager._manager = self.scriptManager diff --git a/src/cthulhu/event_manager.py b/src/cthulhu/event_manager.py index 5a29b02..5a6bc0d 100644 --- a/src/cthulhu/event_manager.py +++ b/src/cthulhu/event_manager.py @@ -143,6 +143,9 @@ class EventManager: elif cthulhu_state.locusOfFocus is None: cthulhu.setLocusOfFocus(None, window, notifyScript=True, force=True) + if getattr(self, "_compositorStateAdapter", None) is not None: + self._compositorStateAdapter.sync_accessible_context("event-manager-startup") + return False def _activateKeyHandling(self) -> None: diff --git a/tests/test_compositor_state_adapter_regressions.py b/tests/test_compositor_state_adapter_regressions.py index dea50bf..7b560f0 100644 --- a/tests/test_compositor_state_adapter_regressions.py +++ b/tests/test_compositor_state_adapter_regressions.py @@ -6,6 +6,7 @@ from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from cthulhu import cthulhu_state +from cthulhu import cthulhu from cthulhu import compositor_state_adapter from cthulhu import compositor_state_types @@ -44,7 +45,18 @@ class CompositorStateAdapterRegressionTests(unittest.TestCase): self.assertEqual(unavailableBackend.activate_calls, []) self.assertEqual(selectedBackend.activate_calls, [adapter]) - self.assertIs(adapter._workspaceBackend, selectedBackend) + self.assertEqual(adapter.get_snapshot().backend_name, "selected") + + def test_activate_is_idempotent_and_deactivates_previous_backend(self) -> None: + backend = FakeWorkspaceBackend(True, "selected") + adapter = compositor_state_adapter.CompositorStateAdapter(workspace_backends=[backend]) + + adapter.activate() + adapter.activate() + + self.assertEqual(backend.activate_calls, [adapter, adapter]) + self.assertEqual(backend.deactivate_calls, [adapter]) + self.assertEqual(adapter.get_snapshot().backend_name, "selected") def test_sync_accessible_context_emits_focus_context_changed_when_active_window_token_changes(self) -> None: adapter = compositor_state_adapter.CompositorStateAdapter() @@ -96,6 +108,29 @@ class CompositorStateAdapterRegressionTests(unittest.TestCase): self.assertEqual(snapshot.active_window_token, "4242:Terminal") self.assertEqual(cthulhu_state.compositorSnapshot.active_window_token, "4242:Terminal") + def test_event_manager_startup_resyncs_adapter_after_focus_recovery(self) -> None: + adapter = mock.Mock() + adapter.sync_accessible_context = mock.Mock(return_value=None) + listener = mock.Mock() + window = object() + focusedObject = object() + + with ( + mock.patch.object(cthulhu.event_manager.Atspi.EventListener, "new", return_value=listener), + mock.patch.object(cthulhu.event_manager.AXUtilities, "can_be_active_window", return_value=False), + mock.patch.object(cthulhu.event_manager.AXUtilities, "find_active_window", return_value=window), + mock.patch.object(cthulhu.event_manager.AXUtilities, "get_focused_object", return_value=focusedObject), + mock.patch.object(cthulhu.event_manager.cthulhu, "setActiveWindow") as setActiveWindow, + mock.patch.object(cthulhu.event_manager.cthulhu, "setLocusOfFocus") as setLocusOfFocus, + ): + manager = cthulhu.event_manager.EventManager(mock.Mock()) + manager.set_compositor_state_adapter(adapter) + manager._sync_focus_on_startup() + + setActiveWindow.assert_called_once_with(window, alsoSetLocusOfFocus=True, notifyScript=False) + setLocusOfFocus.assert_called_once_with(None, focusedObject, notifyScript=True, force=True) + adapter.sync_accessible_context.assert_called_once_with("event-manager-startup") + if __name__ == "__main__": unittest.main()