From 8fc77c5a2fd80e071e1c83343a097529ed7566c5 Mon Sep 17 00:00:00 2001 From: Hunter Jozwiak Date: Thu, 9 Apr 2026 09:31:24 -0400 Subject: [PATCH] feat: prioritize script activation using compositor context --- src/cthulhu/event_manager.py | 12 ++++++ ..._manager_compositor_context_regressions.py | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/cthulhu/event_manager.py b/src/cthulhu/event_manager.py index 2560fa4..4c1b636 100644 --- a/src/cthulhu/event_manager.py +++ b/src/cthulhu/event_manager.py @@ -1236,6 +1236,12 @@ class EventManager: if not script: return False, "There is no script for this event." + prioritizedContextToken = self._prioritizedContextToken or cthulhu_state.prioritizedDesktopContextToken + if self._churnSuppressed and prioritizedContextToken: + eventToken = self._context_token_for_event(event) + if eventToken not in (None, prioritizedContextToken): + return False, "Event context does not match compositor-prioritized context." + if script == cthulhu_state.activeScript: return False, "The script for this event is already active." @@ -1588,6 +1594,12 @@ class EventManager: msg = f"EVENT MANAGER: Exception processing {event.type}: {error}" debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printException(debug.LEVEL_INFO) + else: + if self._compositorStateAdapter is not None: + if eType.startswith("window:activate") or ( + eType.startswith("object:state-changed:focused") and event.detail1 + ): + self._compositorStateAdapter.sync_accessible_context(event.type) tokens = ["EVENT MANAGER: locusOfFocus:", cthulhu_state.locusOfFocus, "activeScript:", cthulhu_state.activeScript] diff --git a/tests/test_event_manager_compositor_context_regressions.py b/tests/test_event_manager_compositor_context_regressions.py index d5efd4b..057a4f9 100644 --- a/tests/test_event_manager_compositor_context_regressions.py +++ b/tests/test_event_manager_compositor_context_regressions.py @@ -103,6 +103,45 @@ class EventManagerCompositorContextRegressionTests(unittest.TestCase): self.assertEqual(list(self.manager._eventQueue.queue), [currentEvent]) + def test_stale_background_event_does_not_activate_script_during_suppression(self) -> None: + script = mock.Mock() + script.isActivatableEvent.return_value = True + script.forceScriptActivation.return_value = False + self.manager._churnSuppressed = True + self.manager._prioritizedContextToken = "current" + + event = FakeEvent("object:state-changed:showing", source="old") + + result, reason = self.manager._isActivatableEvent(event, script) + + self.assertFalse(result) + self.assertIn("compositor-prioritized context", reason) + + def test_focus_event_syncs_accessible_context_back_into_adapter(self) -> None: + adapter = mock.Mock() + script = mock.Mock() + source = object() + event = FakeEvent("object:state-changed:focused", source=source, detail1=1) + self.manager._compositorStateAdapter = adapter + self.manager._get_scriptForEvent = mock.Mock(return_value=script) + self.manager._isActivatableEvent = mock.Mock(return_value=(False, "already active")) + self.manager._inFlood = mock.Mock(return_value=False) + + with ( + mock.patch.object(event_manager.debug, "printObjectEvent"), + mock.patch.object(event_manager.debug, "printDetails"), + mock.patch.object(event_manager.debug, "printMessage"), + mock.patch.object(event_manager.debug, "printTokens"), + mock.patch.object(event_manager.AXUtilities, "get_desktop", return_value=object()), + mock.patch.object(event_manager.AXUtilities, "is_defunct", return_value=False), + mock.patch.object(event_manager.AXUtilities, "is_iconified", return_value=False), + mock.patch.object(event_manager.AXUtilities, "is_frame", return_value=False), + mock.patch.object(event_manager.AXObject, "is_dead", return_value=False), + ): + self.manager._processObjectEvent(event) + + adapter.sync_accessible_context.assert_called_once_with("object:state-changed:focused") + if __name__ == "__main__": unittest.main()