From dfa572b4532e8ec0729a483a54292fa0c4c5950d Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 16 Jan 2026 09:23:53 -0500 Subject: [PATCH] More refactoring work. Since the first part was such a success, may as well continue. We're actually going to be in very good shape if things keep going this well. --- src/cthulhu/cthulhu.py | 70 +++++++++++++++++----------- src/cthulhu/event_manager.py | 13 +++++- src/cthulhu/focus_manager.py | 6 +-- src/cthulhu/input_event.py | 28 ++++++----- src/cthulhu/input_event_manager.py | 10 ++++ src/cthulhu/plugin_system_manager.py | 54 +++++++++++++++------ src/cthulhu/script.py | 14 ++++-- src/cthulhu/script_manager.py | 24 ++++++++-- src/cthulhu/scripts/default.py | 2 +- src/cthulhu/scripts/web/script.py | 11 +++-- src/cthulhu/sleep_mode_manager.py | 4 +- 11 files changed, 163 insertions(+), 73 deletions(-) diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index d8f979e..0727308 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -135,6 +135,7 @@ class APIHelper: self._gestureBindings[contextName].append(binding) logger.info(f"Stored binding in context '{contextName}'") + plugin_manager = None if contextName and self.app: try: plugin_manager = self.app.getPluginSystemManager() @@ -143,21 +144,26 @@ class APIHelper: if plugin_manager: plugin_manager.add_keybinding(contextName, binding, global_binding=globalBinding) - # Only add to active script if one exists - if cthulhu_state.activeScript and not globalBinding: - logger.info(f"Adding binding to active script: {cthulhu_state.activeScript}") - bindings = cthulhu_state.activeScript.getKeyBindings() - bindings.add(binding) - - # Register key grab at the system level - grab_ids = addKeyGrab(binding) - logger.info(f"Key grab IDs: {grab_ids}") - - # For later removal - if grab_ids: - binding._grab_ids = grab_ids + if contextName and self.app and plugin_manager: + if not globalBinding and cthulhu_state.activeScript: + plugin_manager.activate_keybindings_for_plugin(contextName) + elif not globalBinding: + debug.printMessage(debug.LEVEL_INFO, "No active script available - binding stored for later registration", True) else: - debug.printMessage(debug.LEVEL_INFO, "No active script available - binding stored for later registration", True) + if cthulhu_state.activeScript and not globalBinding: + logger.info(f"Adding binding to active script: {cthulhu_state.activeScript}") + bindings = cthulhu_state.activeScript.getKeyBindings() + bindings.add(binding) + + # Register key grab at the system level + grab_ids = addKeyGrab(binding) + logger.info(f"Key grab IDs: {grab_ids}") + + # For later removal + if grab_ids: + binding._grab_ids = grab_ids + else: + debug.printMessage(debug.LEVEL_INFO, "No active script available - binding stored for later registration", True) debug.printMessage(debug.LEVEL_INFO, f"Created binding: {binding.keysymstring} with modifiers {binding.modifiers}", True) logger.info("=== APIHelper.registerGestureByString completed ===") @@ -174,16 +180,26 @@ class APIHelper: - binding: the binding to unregister - contextName: the context for this gesture """ - # Remove from script's keybindings - from . import cthulhu_state - if cthulhu_state.activeScript: - bindings = cthulhu_state.activeScript.getKeyBindings() - bindings.remove(binding) + removed_via_plugin_manager = False + if contextName and self.app: + try: + plugin_manager = self.app.getPluginSystemManager() + except Exception: + plugin_manager = None + if plugin_manager: + plugin_manager.remove_keybinding(contextName, binding) + removed_via_plugin_manager = True + if not removed_via_plugin_manager: + # Remove from script's keybindings + from . import cthulhu_state + if cthulhu_state.activeScript: + bindings = cthulhu_state.activeScript.getKeyBindings() + bindings.remove(binding) - # Remove key grab at system level - if hasattr(binding, '_grab_ids'): - for grab_id in binding._grab_ids: - self.app.removeKeyGrab(grab_id) + # Remove key grab at system level + if hasattr(binding, '_grab_ids'): + for grab_id in binding._grab_ids: + self.app.removeKeyGrab(grab_id) # Remove from tracking if contextName in self._gestureBindings: @@ -858,17 +874,15 @@ def main(): # setActiveWindow does some corrective work needed thanks to # mutter-x11-frames. So retrieve the window just in case. window = cthulhu_state.activeWindow - script = cthulhuApp.scriptManager.get_script(app, window) - cthulhuApp.scriptManager.set_active_script(script, "Launching.") + cthulhuApp.scriptManager.activate_script_for_context(app, window, "startup: launch") focusedObject = AXUtilities.get_focused_object(window) tokens = ["CTHULHU: Focused object is:", focusedObject] debug.printTokens(debug.LEVEL_INFO, tokens, True) if focusedObject: setLocusOfFocus(None, focusedObject) - script = cthulhuApp.scriptManager.get_script( - AXObject.get_application(focusedObject), focusedObject) - cthulhuApp.scriptManager.set_active_script(script, "Found focused object.") + cthulhuApp.scriptManager.activate_script_for_context( + AXObject.get_application(focusedObject), focusedObject, "startup: focused-object") try: msg = "CTHULHU: Starting ATSPI registry." diff --git a/src/cthulhu/event_manager.py b/src/cthulhu/event_manager.py index 7258c0c..9637298 100644 --- a/src/cthulhu/event_manager.py +++ b/src/cthulhu/event_manager.py @@ -675,7 +675,7 @@ class EventManager: return False defaultScript = cthulhu.cthulhuApp.scriptManager.get_default_script() - cthulhu.cthulhuApp.scriptManager.set_active_script(defaultScript, 'No focus') + cthulhu.cthulhuApp.scriptManager.set_active_script(defaultScript, 'focus: none') defaultScript.idleMessage() return False @@ -900,6 +900,15 @@ class EventManager: if not script.isActivatableEvent(event): return False, "The script says not to activate for this event." + if cthulhu_state.activeScript is None: + active_window = cthulhu_state.activeWindow + if AXUtilities.is_focused(event.source): + return True, "No active script and event source is focused." + if active_window and AXObject.is_ancestor(event.source, active_window, inclusive=True): + return True, "No active script and event is in active window." + if AXUtilities.is_frame(event.source) or AXUtilities.is_window(event.source): + return True, "No active script and event source is window/frame." + if script.forceScriptActivation(event): return True, "The script insists it should be activated for this event." @@ -1097,7 +1106,7 @@ class EventManager: debug.printMessage(debug.LEVEL_INFO, msg, True) cthulhu_state.locusOfFocus = None cthulhu_state.activeWindow = None - cthulhu.cthulhuApp.scriptManager.set_active_script(None, "Active window is dead or defunct") + cthulhu.cthulhuApp.scriptManager.set_active_script(None, "focus: active-window-dead") return if AXUtilities.is_iconified(event.source): diff --git a/src/cthulhu/focus_manager.py b/src/cthulhu/focus_manager.py index 02aed9a..ac49038 100644 --- a/src/cthulhu/focus_manager.py +++ b/src/cthulhu/focus_manager.py @@ -291,7 +291,7 @@ class FocusManager: if event and (script and not script.app): app = _get_ax_utilities().get_application(event.source) script = self.app.scriptManager.get_script(app, event.source) - self.app.scriptManager.set_active_script(script, "Setting locus of focus") + self.app.scriptManager.activate_script_for_context(app, event.source, "focus: locus-of-focus") old_focus = self._focus if AXObject.is_dead(old_focus): @@ -384,8 +384,7 @@ class FocusManager: self.set_locus_of_focus(None, self._window, notify_script=True) app = _get_ax_utilities().get_application(self._focus) - script = self.app.scriptManager.get_script(app, self._focus) - self.app.scriptManager.set_active_script(script, "Setting active window") + self.app.scriptManager.activate_script_for_context(app, self._focus, "focus: active-window") @dbus_service.command def toggle_presentation_mode( @@ -472,4 +471,3 @@ def get_manager(): from . import cthulhu _manager = FocusManager(cthulhu.cthulhuApp) return _manager - diff --git a/src/cthulhu/input_event.py b/src/cthulhu/input_event.py index f4a0c13..c6d6e8a 100644 --- a/src/cthulhu/input_event.py +++ b/src/cthulhu/input_event.py @@ -778,6 +778,20 @@ class KeyboardEvent(InputEvent): return self._handler + def _resolveHandler(self): + """Resolve handler for this event, returning True if a global handler was used.""" + + if not self._handler and self._script: + self._handler = self._script.keyBindings.getInputHandler(self) + + if not self._handler: + globalHandler = self._getGlobalHandler() + if globalHandler: + self._handler = globalHandler + return True + + return False + def shouldConsume(self): """Returns True if this event should be consumed.""" @@ -810,23 +824,15 @@ class KeyboardEvent(InputEvent): if cthulhu_state.bypassNextCommand: return False, 'Bypass next command' - if not self._handler: - self._handler = self._script.keyBindings.getInputHandler(self) - if not self._handler: - globalHandler = self._getGlobalHandler() - if globalHandler: - self._handler = globalHandler - globalHandlerUsed = True + globalHandlerUsed = globalHandlerUsed or self._resolveHandler() if self._handler: debug.printMessage(debug.LEVEL_INFO, f"shouldConsume: Handler found: {self._handler.description}", True) else: debug.printMessage(debug.LEVEL_INFO, "shouldConsume: No handler found", True) - # TODO - JD: Right now we need to always call consumesKeyboardEvent() - # because that method is updating state, even in instances where there - # is no handler. - scriptConsumes = self._script.consumesKeyboardEvent(self) + self._script.updateKeyboardEventState(self, self._handler) + scriptConsumes = self._script.shouldConsumeKeyboardEvent(self, self._handler) if globalHandlerUsed: scriptConsumes = True debug.printMessage(debug.LEVEL_INFO, f"shouldConsume: scriptConsumes={scriptConsumes}", True) diff --git a/src/cthulhu/input_event_manager.py b/src/cthulhu/input_event_manager.py index 3ce833f..e83d36c 100644 --- a/src/cthulhu/input_event_manager.py +++ b/src/cthulhu/input_event_manager.py @@ -133,6 +133,10 @@ class InputEventManager: grab_ids.append(grab_id) self._grabbed_bindings[grab_id] = binding + if grab_ids: + tokens = ["INPUT EVENT MANAGER: Added grabs", grab_ids, "for", binding] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return grab_ids def remove_grabs_for_keybinding(self, binding: keybindings.KeyBinding) -> None: @@ -154,6 +158,9 @@ class InputEventManager: debug.print_tokens(debug.LEVEL_INFO, tokens, True) return + tokens = ["INPUT EVENT MANAGER: Removing grabs", grab_ids, "for", binding] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + for grab_id in grab_ids: self._device.remove_key_grab(grab_id) removed = self._grabbed_bindings.pop(grab_id, None) @@ -179,6 +186,9 @@ class InputEventManager: binding._grab_ids.remove(grab_id) if not binding._grab_ids: delattr(binding, "_grab_ids") + if binding: + tokens = ["INPUT EVENT MANAGER: Removed grab", grab_id, "for", binding] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) def map_keysym_to_modifier(self, keysym: int) -> int: """Maps keysym as a modifier, returns the newly-mapped modifier.""" diff --git a/src/cthulhu/plugin_system_manager.py b/src/cthulhu/plugin_system_manager.py index f9f0054..03f46f0 100644 --- a/src/cthulhu/plugin_system_manager.py +++ b/src/cthulhu/plugin_system_manager.py @@ -21,6 +21,7 @@ from enum import IntEnum import pluggy from . import dbus_service +from . import input_event_manager from . import keybindings # Added import # Set to True for more detailed plugin loading debug info @@ -144,6 +145,7 @@ class PluginSystemManager: self._plugin_keybindings = {} # plugin_name -> [KeyBinding] self._global_keybindings = keybindings.KeyBindings() self._global_bindings = [] + self._last_active_script = None # Create plugin directories self._setup_plugin_dirs() @@ -166,12 +168,42 @@ class PluginSystemManager: if binding not in self._global_bindings: self._global_bindings.append(binding) self._global_keybindings.add(binding) - grab_ids = self.app.addKeyGrab(binding) + grab_ids = input_event_manager.get_manager().add_grabs_for_keybinding(binding) if grab_ids: binding._global_grab_ids = grab_ids else: logger.warning(f"Failed to create global key grab for {binding.keysymstring}") + def activate_keybindings_for_plugin(self, plugin_name): + """Activates keybindings for a single plugin with the active script.""" + plugin_info = self._plugins.get(plugin_name) + if not plugin_info or not plugin_info.loaded: + return + self._activate_plugin_keybindings(plugin_info) + + def remove_keybinding(self, plugin_name, binding): + """Remove a keybinding associated with a specific plugin.""" + if plugin_name in self._plugin_keybindings: + if binding in self._plugin_keybindings[plugin_name]: + self._plugin_keybindings[plugin_name].remove(binding) + + if binding in self._global_bindings: + if hasattr(binding, '_global_grab_ids'): + for grab_id in binding._global_grab_ids: + input_event_manager.get_manager().remove_grab_by_id(grab_id) + del binding._global_grab_ids + if binding in self._global_bindings: + self._global_bindings.remove(binding) + self._global_keybindings.remove(binding) + return + + from . import cthulhu_state + if cthulhu_state.activeScript: + active_script = cthulhu_state.activeScript + if active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"): + active_script.getKeyBindings().remove(binding) + input_event_manager.get_manager().remove_grabs_for_keybinding(binding) + def _activate_plugin_keybindings(self, plugin_info): """Activates all keybindings for a given plugin with the active script.""" from . import cthulhu_state # Import here to avoid circular dependency @@ -182,12 +214,13 @@ class PluginSystemManager: plugin_name = plugin_info.get_module_name() if plugin_name in self._plugin_keybindings: active_script = cthulhu_state.activeScript + input_manager = input_event_manager.get_manager() for binding in self._plugin_keybindings[plugin_name]: if binding in self._global_bindings: continue if not active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"): active_script.getKeyBindings().add(binding) - grab_ids = self.app.addKeyGrab(binding) + grab_ids = input_event_manager.get_manager().add_grabs_for_keybinding(binding) if grab_ids: binding._grab_ids = grab_ids else: @@ -216,10 +249,7 @@ class PluginSystemManager: continue if active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"): active_script.getKeyBindings().remove(binding) - if hasattr(binding, '_grab_ids'): - for grab_id in binding._grab_ids: - self.app.removeKeyGrab(grab_id) - del binding._grab_ids + input_manager.remove_grabs_for_keybinding(binding) logger.debug(f"Deactivated keybinding '{binding.asString()}' for plugin '{plugin_name}'") def refresh_active_script_keybindings(self): @@ -237,22 +267,20 @@ class PluginSystemManager: # First, remove all existing plugin keybindings from the old script # This requires iterating through all plugins, not just active ones, to ensure cleanup - from . import cthulhu_state - if cthulhu_state.activeScript: # If there was an old active script - old_script = cthulhu_state.activeScript + old_script = self._last_active_script + if old_script: + input_manager = input_event_manager.get_manager() for plugin_name, bindings in self._plugin_keybindings.items(): for binding in bindings: if binding in self._global_bindings: continue if old_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"): old_script.getKeyBindings().remove(binding) - if hasattr(binding, '_grab_ids'): - for grab_id in binding._grab_ids: - self.app.removeKeyGrab(grab_id) - del binding._grab_ids + input_manager.remove_grabs_for_keybinding(binding) logger.debug(f"Removed keybinding '{binding.asString()}' from old script for plugin '{plugin_name}'") # Now, if there's a new active script, apply keybindings for currently active plugins + self._last_active_script = new_script if new_script: for plugin_name in self._active_plugins: plugin_info = self._plugins.get(plugin_name) diff --git a/src/cthulhu/script.py b/src/cthulhu/script.py index f4deba4..f298fb8 100644 --- a/src/cthulhu/script.py +++ b/src/cthulhu/script.py @@ -496,11 +496,19 @@ class Script: Returns True if the event is of interest. """ + handler = self.keyBindings.getInputHandler(keyboardEvent) + self.updateKeyboardEventState(keyboardEvent, handler) + return self.shouldConsumeKeyboardEvent(keyboardEvent, handler) + + def updateKeyboardEventState(self, keyboardEvent, handler): + """Update internal state for a keyboard event without deciding consumption.""" + pass + + def shouldConsumeKeyboardEvent(self, keyboardEvent, handler): + """Returns True if the script will consume this keyboard event.""" consumes = False self._lastCommandWasStructNav = False - handler = self.keyBindings.getInputHandler(keyboardEvent) - if handler \ - and handler.function in self.structuralNavigation.functions: + if handler and handler.function in self.structuralNavigation.functions: consumes = self.useStructuralNavigationModel() if consumes: self._lastCommandWasStructNav = True diff --git a/src/cthulhu/script_manager.py b/src/cthulhu/script_manager.py index dda84b2..7413583 100644 --- a/src/cthulhu/script_manager.py +++ b/src/cthulhu/script_manager.py @@ -72,7 +72,7 @@ class ScriptManager: self._toolkitNames = \ {'WebKitGTK': 'WebKitGtk', 'GTK': 'gtk'} - self.set_active_script(None, "__init__") + self.set_active_script(None, "lifecycle: init") self._active = False debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Initialized", True) @@ -82,7 +82,7 @@ class ScriptManager: debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Activating", True) self._defaultScript = self.get_script(None) self._defaultScript.registerEventListeners() - self.set_active_script(self._defaultScript, "activate") + self.set_active_script(self._defaultScript, "lifecycle: activate") self._active = True debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Activated", True) @@ -93,7 +93,7 @@ class ScriptManager: if self._defaultScript: self._defaultScript.deregisterEventListeners() self._defaultScript = None - self.set_active_script(None, "deactivate") + self.set_active_script(None, "lifecycle: deactivate") self.appScripts = {} self.toolkitScripts = {} self.customScripts = {} @@ -342,6 +342,7 @@ class ScriptManager: cthulhu_state.activeScript = newScript if not newScript: + self._log_active_state(reason) return newScript.activate() @@ -352,6 +353,22 @@ class ScriptManager: tokens = ["SCRIPT MANAGER: Setting active script to", newScript, "reason:", reason] debug.printTokens(debug.LEVEL_INFO, tokens, True) + self._log_active_state(reason) + + def activate_script_for_context(self, app, obj, reason=None): + script = self.get_script(app, obj) + self.set_active_script(script, reason) + return script + + def _log_active_state(self, reason=None): + tokens = [ + "SCRIPT MANAGER: Active state:", + "window", cthulhu_state.activeWindow, + "focus", cthulhu_state.locusOfFocus, + "script", cthulhu_state.activeScript, + "reason", reason + ] + debug.printTokens(debug.LEVEL_INFO, tokens, True) def _get_script_for_app_replicant(self, app): if not self._active: @@ -436,4 +453,3 @@ def get_manager(): from . import cthulhu _manager = ScriptManager(cthulhu.cthulhuApp) return _manager - diff --git a/src/cthulhu/scripts/default.py b/src/cthulhu/scripts/default.py index 3bdee8a..33dd2e4 100644 --- a/src/cthulhu/scripts/default.py +++ b/src/cthulhu/scripts/default.py @@ -1983,7 +1983,7 @@ class Script(script.Script): cthulhu.setLocusOfFocus(event, None) cthulhu.setActiveWindow(None) - cthulhu.cthulhuApp.scriptManager.set_active_script(None, "Window deactivated") + cthulhu.cthulhuApp.scriptManager.set_active_script(None, "focus: window-deactivated") def onClipboardContentsChanged(self, *args): if self.flatReviewPresenter.is_active(): diff --git a/src/cthulhu/scripts/web/script.py b/src/cthulhu/scripts/web/script.py index 0954f63..b530021 100644 --- a/src/cthulhu/scripts/web/script.py +++ b/src/cthulhu/scripts/web/script.py @@ -587,18 +587,19 @@ class Script(default.Script): debug.printMessage(debug.LEVEL_INFO, msg, True) self.liveRegionManager.flushMessages() - def consumesKeyboardEvent(self, keyboardEvent): - """Returns True if the script will consume this keyboard event.""" + def updateKeyboardEventState(self, keyboardEvent, handler): + """Update internal state for a keyboard event without deciding consumption.""" # We need to do this here. Cthulhu caret and structural navigation # often result in the user being repositioned without our getting # a corresponding AT-SPI event. Without an AT-SPI event, script.py # won't know to dump the generator cache. See bgo#618827. self.generatorCache = {} - self._lastMouseButtonContext = None, -1 - handler = self.keyBindings.getInputHandler(keyboardEvent) + def shouldConsumeKeyboardEvent(self, keyboardEvent, handler): + """Returns True if the script will consume this keyboard event.""" + if handler and self.caretNavigation.handles_navigation(handler): consumes = self.useCaretNavigationModel(keyboardEvent) self._lastCommandWasCaretNav = consumes @@ -627,7 +628,7 @@ class Script(default.Script): self._lastCommandWasMouseButton = False # Check parent first - consumes = super().consumesKeyboardEvent(keyboardEvent) + consumes = super().shouldConsumeKeyboardEvent(keyboardEvent, handler) # If parent doesn't consume Return key, try our clickable fallback if not consumes and keyboardEvent.event_string == "Return": diff --git a/src/cthulhu/sleep_mode_manager.py b/src/cthulhu/sleep_mode_manager.py index 6e3e268..14f44f9 100644 --- a/src/cthulhu/sleep_mode_manager.py +++ b/src/cthulhu/sleep_mode_manager.py @@ -142,7 +142,7 @@ class SleepModeManager: if notifyUser: newScript.presentMessage( messages.SLEEP_MODE_DISABLED_FOR % AXObject.get_name(script.app)) - scriptManager.set_active_script(newScript, "Sleep mode toggled off") + scriptManager.set_active_script(newScript, "sleep: off") debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Disabled for {AXObject.get_name(script.app)}", True) # Reset debounce timer after successful toggle self._lastToggleTime = 0 @@ -173,7 +173,7 @@ class SleepModeManager: debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Got sleep script: {sleepScript}", True) debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Setting active script", True) - scriptManager.set_active_script(sleepScript, "Sleep mode toggled on") + scriptManager.set_active_script(sleepScript, "sleep: on") debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Active script set successfully", True) debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Adding app to sleep list", True)