fix: tighten compositor adapter lifecycle

This commit is contained in:
2026-04-09 07:42:27 -04:00
parent 0f54fad9ba
commit f2f1de737d
5 changed files with 61 additions and 13 deletions
+20 -8
View File
@@ -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__
+1
View File
@@ -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
+1 -4
View File
@@ -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
+3
View File
@@ -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:
@@ -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()