From 9790a8d49463705631a482243ead751d92161cc9 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 18 Apr 2025 14:28:16 -0400 Subject: [PATCH] More attempts to fix keyboard. --- src/cthulhu/cthulhu.py | 58 ++++------ src/cthulhu/cthulhuVersion.py | 2 +- src/cthulhu/plugin.py | 19 +++- src/cthulhu/plugin_system_manager.py | 109 +++++++++++++++---- src/cthulhu/plugins/DisplayVersion/plugin.py | 6 +- 5 files changed, 133 insertions(+), 61 deletions(-) diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index ff3844e..d88e58c 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -37,47 +37,35 @@ import faulthandler class APIHelper: """Helper class for plugin API interactions, including keybindings.""" - + def __init__(self, app): """Initialize the APIHelper. - + Arguments: - app: the Cthulhu application """ self.app = app self._gestureBindings = {} - + def registerGestureByString(self, function, name, gestureString, inputEventType='default', normalizer='cthulhu', learnModeEnabled=True, contextName=None): - """Register a gesture by string. - - Arguments: - - function: the function to call when the gesture is performed - - name: a human-readable name for this gesture - - gestureString: string representation of the gesture (e.g., 'kb:cthulhu+z') - - inputEventType: the type of input event - - normalizer: the normalizer to use - - learnModeEnabled: whether this should be available in learn mode - - contextName: the context for this gesture (e.g., plugin name) - - Returns the binding ID or None if registration failed - """ + """Register a gesture by string.""" if not gestureString.startswith("kb:"): return None - + # Extract the key portion from the gesture string key = gestureString.split(":", 1)[1] - + # Handle Cthulhu modifier specially if "cthulhu+" in key.lower(): from . import keybindings key_parts = key.lower().split("+") - + # Determine appropriate modifier mask modifiers = keybindings.CTHULHU_MODIFIER_MASK - + # Extract the final key (without modifiers) final_key = key_parts[-1] - + # Check for additional modifiers if "shift" in key_parts: modifiers = keybindings.CTHULHU_SHIFT_MODIFIER_MASK @@ -85,13 +73,13 @@ class APIHelper: modifiers = keybindings.CTHULHU_CTRL_MODIFIER_MASK elif "alt" in key_parts: modifiers = keybindings.CTHULHU_ALT_MODIFIER_MASK - + # Create a keybinding handler class GestureHandler: def __init__(self, function, description): self.function = function self.description = description - + def __call__(self, script, inputEvent): try: return function(script, inputEvent) @@ -99,9 +87,9 @@ class APIHelper: import logging logging.getLogger(__name__).error(f"Error in keybinding handler: {e}") return True - + handler = GestureHandler(function, name) - + # Register the binding with the active script from . import cthulhu_state if cthulhu_state.activeScript: @@ -111,29 +99,29 @@ class APIHelper: keybindings.defaultModifierMask, modifiers, handler) - + # Add the binding to the active script bindings.add(binding) - + # Store binding for later reference if contextName not in self._gestureBindings: self._gestureBindings[contextName] = [] self._gestureBindings[contextName].append(binding) - + # Register key grab at the system level grab_ids = self.app.addKeyGrab(binding) - + # For later removal if grab_ids: binding._grab_ids = grab_ids - + return binding - + return None - + def unregisterShortcut(self, binding, contextName=None): """Unregister a previously registered shortcut. - + Arguments: - binding: the binding to unregister - contextName: the context for this gesture @@ -143,12 +131,12 @@ class APIHelper: 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 from tracking if contextName in self._gestureBindings: if binding in self._gestureBindings[contextName]: diff --git a/src/cthulhu/cthulhuVersion.py b/src/cthulhu/cthulhuVersion.py index 794f76f..2f306d4 100644 --- a/src/cthulhu/cthulhuVersion.py +++ b/src/cthulhu/cthulhuVersion.py @@ -24,4 +24,4 @@ # Original source: https://gitlab.gnome.org/GNOME/orca version = "2025.04.18" -codeName = "testing" +codeName = "plugins" diff --git a/src/cthulhu/plugin.py b/src/cthulhu/plugin.py index 88a3080..6f7f7fb 100644 --- a/src/cthulhu/plugin.py +++ b/src/cthulhu/plugin.py @@ -21,7 +21,7 @@ except ImportError: # Fallback if pluggy is not available def cthulhu_hookimpl(func=None, **kwargs): """Fallback decorator when pluggy is not available. - + This is a no-op decorator that returns the original function. It allows the code to continue working without pluggy, though plugins will be disabled. @@ -47,6 +47,8 @@ class Plugin: self.name = '' self.version = '' self.description = '' + self._bindings = None + self._gestureBindings = {} def set_app(self, app): """Set the application reference.""" @@ -75,12 +77,16 @@ class Plugin: return logger.info(f"Deactivating plugin: {self.name}") + def get_bindings(self): + """Get keybindings for this plugin. Override in subclasses.""" + return self._bindings + def registerGestureByString(self, function, name, gestureString, learnModeEnabled=True): """Register a gesture by string.""" if self.app: api_helper = self.app.getAPIHelper() if api_helper: - return api_helper.registerGestureByString( + binding = api_helper.registerGestureByString( function, name, gestureString, @@ -89,4 +95,13 @@ class Plugin: learnModeEnabled, contextName=self.module_name ) + + # Also store the binding locally so get_bindings() can use it + if binding: + if not self._bindings: + from . import keybindings + self._bindings = keybindings.KeyBindings() + self._bindings.add(binding) + + return binding return None diff --git a/src/cthulhu/plugin_system_manager.py b/src/cthulhu/plugin_system_manager.py index 2d6d5cd..80fe9c5 100644 --- a/src/cthulhu/plugin_system_manager.py +++ b/src/cthulhu/plugin_system_manager.py @@ -112,11 +112,72 @@ class PluginSystemManager: # Create plugin directories self._setup_plugin_dirs() - + # Log available plugins directory paths logger.info(f"System plugins directory: {PluginType.SYSTEM.get_root_dir()}") logger.info(f"User plugins directory: {PluginType.USER.get_root_dir()}") + def register_plugin_global_keybindings(self, plugin): + """Register a plugin's keybindings with all scripts.""" + if not hasattr(plugin, 'get_bindings'): + return + + try: + bindings = plugin.get_bindings() + if not bindings or not bindings.keyBindings: + return + + logger.info(f"Registering global keybindings for plugin: {plugin.name}") + + # First register with the active script + from . import cthulhu_state + if cthulhu_state.activeScript: + active_script = cthulhu_state.activeScript + for binding in bindings.keyBindings: + active_script.getKeyBindings().add(binding) + grab_ids = self.app.addKeyGrab(binding) + if grab_ids: + binding._grab_ids = grab_ids + + # Store these bindings for future script changes + plugin_name = plugin.name or plugin.module_name + if not hasattr(self, '_plugin_global_bindings'): + self._plugin_global_bindings = {} + self._plugin_global_bindings[plugin_name] = bindings + + # Connect to script changes to ensure bindings work with all scripts + if not hasattr(self, '_connected_to_script_changes'): + signal_manager = self.app.getSignalManager() + if signal_manager: + signal_manager.connect('load-setting-completed', self._on_settings_changed) + self._connected_to_script_changes = True + except Exception as e: + logger.error(f"Error registering global keybindings for plugin {plugin.name}: {e}") + import traceback + logger.error(traceback.format_exc()) + + def _on_settings_changed(self): + """Re-register all plugin keybindings when settings change.""" + if not hasattr(self, '_plugin_global_bindings'): + return + + from . import cthulhu_state + if not cthulhu_state.activeScript: + return + + active_script = cthulhu_state.activeScript + for plugin_name, bindings in self._plugin_global_bindings.items(): + logger.info(f"Re-registering keybindings for plugin: {plugin_name}") + for binding in bindings.keyBindings: + # Check if binding already exists + if active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"): + continue + + active_script.getKeyBindings().add(binding) + grab_ids = self.app.addKeyGrab(binding) + if grab_ids: + binding._grab_ids = grab_ids + def _setup_plugin_dirs(self): """Ensure plugin directories exist.""" os.makedirs(PluginType.SYSTEM.get_root_dir(), exist_ok=True) @@ -219,24 +280,24 @@ class PluginSystemManager: def setActivePlugins(self, activePlugins): """Set active plugins and sync their state.""" logger.info(f"Setting active plugins: {activePlugins}") - + # Make sure we have scanned for plugins first if not self._plugins: logger.info("No plugins found, rescanning...") self.rescanPlugins() - + self._active_plugins = activePlugins - + # Log active vs available plugins available_plugins = [p.get_module_name() for p in self.plugins] logger.info(f"Available plugins: {available_plugins}") logger.info(f"Active plugins: {self._active_plugins}") - + # Find missing plugins missing_plugins = [p for p in self._active_plugins if p not in available_plugins] if missing_plugins: logger.warning(f"Active plugins not found: {missing_plugins}") - + self.syncAllPluginsActive() def setPluginActive(self, pluginInfo, active): @@ -258,7 +319,7 @@ class PluginSystemManager: def isPluginActive(self, pluginInfo): """Check if a plugin is active.""" module_name = pluginInfo.get_module_name() - + # Builtin plugins are always active if pluginInfo.builtin: logger.debug(f"Plugin {module_name} is builtin, active by default") @@ -271,34 +332,34 @@ class PluginSystemManager: # Check case-insensitive match in active plugins list active_plugins = self.getActivePlugins() - + # Try exact match first if module_name in active_plugins: logger.debug(f"Plugin {module_name} found in active plugins list") return True - + # Try case-insensitive match module_name_lower = module_name.lower() is_active = any(plugin.lower() == module_name_lower for plugin in active_plugins) - + if is_active: logger.debug(f"Plugin {module_name} found in active plugins list (case-insensitive match)") else: logger.debug(f"Plugin {module_name} not found in active plugins list") - + return is_active def syncAllPluginsActive(self): """Sync the active state of all plugins.""" logger.info("Syncing active state of all plugins") - + # Log plugin status before syncing if PLUGIN_DEBUG: for pluginInfo in self.plugins: is_active = self.isPluginActive(pluginInfo) is_loaded = pluginInfo.loaded logger.debug(f"Plugin {pluginInfo.get_module_name()}: active={is_active}, loaded={is_loaded}") - + # First unload inactive plugins for pluginInfo in self.plugins: if not self.isPluginActive(pluginInfo) and pluginInfo.loaded: @@ -311,7 +372,7 @@ class PluginSystemManager: logger.info(f"Loading active plugin: {pluginInfo.get_module_name()}") result = self.loadPlugin(pluginInfo) logger.info(f"Plugin {pluginInfo.get_module_name()} load result: {result}") - + # Log final plugin status active_plugins = [p.get_module_name() for p in self.plugins if p.loaded] logger.info(f"Active plugins after sync: {active_plugins}") @@ -337,27 +398,27 @@ class PluginSystemManager: # Try to find the plugin file module_name = pluginInfo.get_module_name() plugin_dir = pluginInfo.get_module_dir() - + # Check for plugin.py first (standard format) plugin_file = os.path.join(plugin_dir, "plugin.py") - + # Fall back to [PluginName].py if plugin.py doesn't exist if not os.path.exists(plugin_file): alternative_plugin_file = os.path.join(plugin_dir, f"{module_name}.py") if os.path.exists(alternative_plugin_file): plugin_file = alternative_plugin_file logger.info(f"Using alternative plugin file: {alternative_plugin_file}") - + if not os.path.exists(plugin_file): logger.error(f"Plugin file not found: {plugin_file}") return False - + logger.info(f"Loading plugin from: {plugin_file}") spec = importlib.util.spec_from_file_location(module_name, plugin_file) if spec is None: logger.error(f"Failed to create spec for plugin: {module_name}") return False - + module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) pluginInfo.module = module @@ -385,7 +446,7 @@ class PluginSystemManager: # Ensure plugins have a reference to the app plugin_instance.app = self.getApp() logger.info(f"Set app reference for plugin: {module_name}") - + if hasattr(plugin_instance, 'set_app'): plugin_instance.set_app(self.getApp()) logger.info(f"Called set_app() for plugin: {module_name}") @@ -398,10 +459,10 @@ class PluginSystemManager: if self.plugin_manager is None: logger.error(f"Plugin manager is None when loading {module_name}") return False - + logger.info(f"Registering plugin with pluggy: {module_name}") self.plugin_manager.register(plugin_instance) - + try: logger.info(f"Activating plugin: {module_name}") self.plugin_manager.hook.activate(plugin=plugin_instance) @@ -413,6 +474,10 @@ class PluginSystemManager: pluginInfo.loaded = True logger.info(f"Successfully loaded plugin: {module_name}") + + # Register any global keybindings from the plugin + self.register_plugin_global_keybindings(pluginInfo.instance) + return True except Exception as e: diff --git a/src/cthulhu/plugins/DisplayVersion/plugin.py b/src/cthulhu/plugins/DisplayVersion/plugin.py index 9f7017b..75ab90a 100644 --- a/src/cthulhu/plugins/DisplayVersion/plugin.py +++ b/src/cthulhu/plugins/DisplayVersion/plugin.py @@ -40,8 +40,11 @@ class DisplayVersion(Plugin): f'Cthulhu screen reader version {cthulhuVersion.version}-{cthulhuVersion.codeName}', 'kb:cthulhu+shift+v' ) + logger.info(f"Registered keybinding: {self._kb_binding}") except Exception as e: logger.error(f"Error activating DisplayVersion plugin: {e}") + import traceback + logger.error(traceback.format_exc()) @cthulhu_hookimpl def deactivate(self, plugin=None): @@ -55,6 +58,7 @@ class DisplayVersion(Plugin): def speakText(self, script=None, inputEvent=None): """Speak the Cthulhu version when shortcut is pressed.""" try: + logger.info("DisplayVersion plugin: speakText called") if self.app: state = self.app.getDynamicApiManager().getAPI('CthulhuState') if state.activeScript: @@ -65,4 +69,4 @@ class DisplayVersion(Plugin): return True except Exception as e: logger.error(f"Error in DisplayVersion speakText: {e}") - return False \ No newline at end of file + return False