Hopefully fix plugins not actually binding keyboard shortcuts.

This commit is contained in:
Storm Dragon 2025-04-04 19:37:21 -04:00
parent 02be96aa69
commit 1969c8498f
3 changed files with 356 additions and 109 deletions

View File

@ -34,108 +34,6 @@ __copyright__ = "Copyright (c) 2004-2009 Sun Microsystems Inc." \
__license__ = "LGPL" __license__ = "LGPL"
import faulthandler 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
"""
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
elif "ctrl" in key_parts or "control" in key_parts:
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):
return self.function(script, inputEvent)
handler = GestureHandler(function, name)
# Register the binding with the active script
from . import cthulhu_state
if cthulhu_state.activeScript:
bindings = cthulhu_state.activeScript.getKeyBindings()
binding = keybindings.KeyBinding(
final_key,
keybindings.defaultModifierMask,
modifiers,
handler)
bindings.add(binding)
# Store binding for later reference
if contextName not in self._gestureBindings:
self._gestureBindings[contextName] = []
self._gestureBindings[contextName].append(binding)
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
"""
# Remove from script's keybindings
from . import cthulhu_state
if cthulhu_state.activeScript:
bindings = cthulhu_state.activeScript.getKeyBindings()
bindings.remove(binding)
# Remove from our tracking
if contextName in self._gestureBindings:
if binding in self._gestureBindings[contextName]:
self._gestureBindings[contextName].remove(binding)
import gi import gi
import importlib import importlib
import os import os
@ -190,6 +88,250 @@ from . import translation_manager
from . import resource_manager from . import resource_manager
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 = {} # contextName -> [bindings]
# Register for script change notifications
self.app.getSignalManager().connectSignal(
'active-script-changed',
self._handleScriptChanged
)
def _handleScriptChanged(self, script):
"""Handle when the active script changes by updating bindings.
Arguments:
- script: the new active script
"""
if not script:
return
# Sync all known bindings with the new script
self._syncBindingsWithScript(script)
def _syncBindingsWithScript(self, script):
"""Sync all registered bindings with the given script.
Arguments:
- script: the script to sync bindings with
"""
from . import debug
if not script:
return
debug.printMessage(debug.LEVEL_INFO, f"Syncing {sum(len(v) for v in self._gestureBindings.values())} bindings to script {script}", True)
# Get the script's key bindings
try:
bindings = script.getKeyBindings()
if not bindings:
debug.printMessage(debug.LEVEL_WARNING, "Script has no key bindings manager", True)
return
# First, remove any plugin bindings that might already exist
# (in case we're re-registering with the same script)
for contextName, contextBindings in self._gestureBindings.items():
for binding in contextBindings:
try:
bindings.remove(binding)
except:
# Binding might not be in this script, that's OK
pass
# Then add all our stored bindings to the script
for contextName, contextBindings in self._gestureBindings.items():
for binding in contextBindings:
try:
bindings.add(binding)
debug.printMessage(debug.LEVEL_INFO, f"Added binding for {binding.handler.description} to script", True)
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error adding binding to script: {e}", True)
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Failed to sync bindings with script: {e}", True)
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
"""
from . import debug
debug.printMessage(debug.LEVEL_INFO, f"Registering gesture: {gestureString} for context: {contextName}", True)
if not gestureString.startswith("kb:"):
debug.printMessage(debug.LEVEL_WARNING, f"Invalid gesture string format: {gestureString}", True)
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
elif "ctrl" in key_parts or "control" in key_parts:
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):
return self.function(script, inputEvent)
handler = GestureHandler(function, name)
# Create the binding
binding = keybindings.KeyBinding(
final_key,
keybindings.defaultModifierMask,
modifiers,
handler)
# Store binding for later reference (do this BEFORE registering with script)
if not contextName:
debug.printMessage(debug.LEVEL_WARNING, "No context name provided, using 'default'", True)
contextName = "default"
if contextName not in self._gestureBindings:
self._gestureBindings[contextName] = []
self._gestureBindings[contextName].append(binding)
# Register with active script if available
from . import cthulhu_state
if cthulhu_state.activeScript:
try:
bindings = cthulhu_state.activeScript.getKeyBindings()
if bindings:
bindings.add(binding)
debug.printMessage(debug.LEVEL_INFO,
f"Added binding {binding} to active script {cthulhu_state.activeScript}", True)
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING,
f"Failed to add binding to active script: {e}", True)
return binding
debug.printMessage(debug.LEVEL_WARNING, f"Gesture doesn't use Cthulhu modifier: {gestureString}", True)
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
"""
from . import debug
# For compatibility with old code that passes a string instead of a binding
if isinstance(binding, str):
debug.printMessage(debug.LEVEL_WARNING,
f"Deprecated: unregisterShortcut called with string '{binding}' instead of binding object", True)
# Try to find the binding by its description in all context bindings
found = False
for ctx, bindings in self._gestureBindings.items():
for b in bindings:
if binding in b.handler.description:
debug.printMessage(debug.LEVEL_INFO,
f"Found binding {b} matching '{binding}', removing", True)
self.unregisterShortcut(b, ctx)
found = True
if not found and contextName:
# If we have a context, try to clean up all bindings for that context
debug.printMessage(debug.LEVEL_INFO,
f"Could not find binding for '{binding}', unregistering all for context {contextName}", True)
self.unregisterAllForContext(contextName)
return
debug.printMessage(debug.LEVEL_INFO, f"Unregistering shortcut for context: {contextName}", True)
# Remove from all scripts
from . import script_manager
for script in script_manager.getManager().getAllScripts():
try:
bindings = script.getKeyBindings()
if bindings and binding in bindings:
bindings.remove(binding)
debug.printMessage(debug.LEVEL_INFO, f"Removed binding from script: {script}", True)
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING, f"Error removing binding from script {script}: {e}", True)
# Remove from active script explicitly (in case it wasn't in getAllScripts)
from . import cthulhu_state
if cthulhu_state.activeScript:
try:
bindings = cthulhu_state.activeScript.getKeyBindings()
if bindings:
bindings.remove(binding)
except Exception as e:
debug.printMessage(debug.LEVEL_WARNING,
f"Error removing binding from active script: {e}", True)
# Remove from our tracking
if contextName in self._gestureBindings:
if binding in self._gestureBindings[contextName]:
self._gestureBindings[contextName].remove(binding)
debug.printMessage(debug.LEVEL_INFO,
f"Removed binding from _gestureBindings for context: {contextName}", True)
def unregisterAllForContext(self, contextName):
"""Unregister all shortcuts for a specific context.
Arguments:
- contextName: the context to unregister all shortcuts for
"""
from . import debug
debug.printMessage(debug.LEVEL_INFO, f"Unregistering all shortcuts for context: {contextName}", True)
if contextName not in self._gestureBindings:
debug.printMessage(debug.LEVEL_INFO, f"No bindings found for context: {contextName}", True)
return
# Get a copy of the bindings
bindings = self._gestureBindings[contextName].copy()
# Unregister each binding
for binding in bindings:
self.unregisterShortcut(binding, contextName)
# Ensure the context is empty
self._gestureBindings[contextName] = []
_eventManager = event_manager.getManager() _eventManager = event_manager.getManager()
_scriptManager = script_manager.getManager() _scriptManager = script_manager.getManager()
_settingsManager = settings_manager.getManager() _settingsManager = settings_manager.getManager()
@ -1017,7 +1159,9 @@ class Cthulhu(GObject.Object):
"setup-inputeventhandlers-completed": (GObject.SignalFlags.RUN_LAST, None, ()), # compat signal for register input event handlers "setup-inputeventhandlers-completed": (GObject.SignalFlags.RUN_LAST, None, ()), # compat signal for register input event handlers
"request-cthulhu-preferences": (GObject.SignalFlags.RUN_LAST, None, ()), "request-cthulhu-preferences": (GObject.SignalFlags.RUN_LAST, None, ()),
"request-application-preferences": (GObject.SignalFlags.RUN_LAST, None, ()), "request-application-preferences": (GObject.SignalFlags.RUN_LAST, None, ()),
"active-script-changed": (GObject.SignalFlags.RUN_LAST, None, (object,))
} }
def __init__(self): def __init__(self):
GObject.Object.__init__(self) GObject.Object.__init__(self)
# add members # add members
@ -1029,11 +1173,38 @@ class Cthulhu(GObject.Object):
self.dynamicApiManager = dynamic_api_manager.DynamicApiManager(self) self.dynamicApiManager = dynamic_api_manager.DynamicApiManager(self)
self.translationManager = translation_manager.TranslationManager(self) self.translationManager = translation_manager.TranslationManager(self)
self.debugManager = debug self.debugManager = debug
self._setupScriptChangeMonitoring()
self.APIHelper = APIHelper(self) self.APIHelper = APIHelper(self)
self.createCompatAPI() self.createCompatAPI()
self.pluginSystemManager = plugin_system_manager.PluginSystemManager(self) self.pluginSystemManager = plugin_system_manager.PluginSystemManager(self)
# Scan for available plugins at startup # Scan for available plugins at startup
self.pluginSystemManager.rescanPlugins() self.pluginSystemManager.rescanPlugins()
def _setupScriptChangeMonitoring(self):
"""Set up monitoring for script changes to emit signals."""
from . import script_manager
mgr = script_manager.getManager()
# Save the original setActiveScript method
original_setActiveScript = mgr.setActiveScript
# Create a replacement that emits our signal
def setActiveScript_with_signal(script, reason=None):
result = original_setActiveScript(script, reason)
# Emit our signal with the new script
try:
self.emit("active-script-changed", script)
from . import debug
debug.printMessage(debug.LEVEL_INFO, f"Emitted active-script-changed signal with {script}", True)
except Exception as e:
from . import debug
debug.printMessage(debug.LEVEL_WARNING, f"Error emitting script change signal: {e}", True)
return result
# Replace the method
mgr.setActiveScript = setActiveScript_with_signal
def getAPIHelper(self): def getAPIHelper(self):
return self.APIHelper return self.APIHelper
def getPluginSystemManager(self): def getPluginSystemManager(self):

View File

@ -47,6 +47,7 @@ class Plugin:
self.name = '' self.name = ''
self.version = '' self.version = ''
self.description = '' self.description = ''
self._registered_bindings = [] # Track registered bindings for cleanup
def set_app(self, app): def set_app(self, app):
"""Set the application reference.""" """Set the application reference."""
@ -73,14 +74,31 @@ class Plugin:
"""Deactivate the plugin. Override this in subclasses.""" """Deactivate the plugin. Override this in subclasses."""
if plugin is not None and plugin is not self: if plugin is not None and plugin is not self:
return return
logger.info(f"Deactivating plugin: {self.name}") logger.info(f"Deactivating plugin: {self.name}")
def registerGestureByString(self, function, name, gestureString, learnModeEnabled=True): # Clean up all registered shortcuts
"""Register a gesture by string."""
if self.app: if self.app:
api_helper = self.app.getAPIHelper() api_helper = self.app.getAPIHelper()
if api_helper: if api_helper:
return api_helper.registerGestureByString( api_helper.unregisterAllForContext(self.module_name)
self._registered_bindings = [] # Clear our binding list
def registerGestureByString(self, function, name, gestureString, learnModeEnabled=True):
"""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')
- learnModeEnabled: whether this should be available in learn mode
Returns the binding object or None if registration failed
"""
if self.app:
api_helper = self.app.getAPIHelper()
if api_helper:
binding = api_helper.registerGestureByString(
function, function,
name, name,
gestureString, gestureString,
@ -89,4 +107,40 @@ class Plugin:
learnModeEnabled, learnModeEnabled,
contextName=self.module_name contextName=self.module_name
) )
if binding and binding not in self._registered_bindings:
self._registered_bindings.append(binding)
return binding
logger.warning(f"Could not register gesture: {gestureString}")
return None return None
def unregisterGestureByString(self, gestureString):
"""Backwards compatibility method to unregister a gesture by string.
This method is provided for compatibility with existing plugins,
but new plugins should store and use the binding objects.
Arguments:
- gestureString: the gesture string to unregister
"""
if self.app:
api_helper = self.app.getAPIHelper()
if api_helper:
# We'll let the APIHelper try to find the binding
api_helper.unregisterShortcut(gestureString, self.module_name)
def unregisterBinding(self, binding):
"""Unregister a specific binding.
Arguments:
- binding: the binding object to unregister
"""
if binding in self._registered_bindings:
self._registered_bindings.remove(binding)
if self.app:
api_helper = self.app.getAPIHelper()
if api_helper:
api_helper.unregisterShortcut(binding, self.module_name)

View File

@ -431,6 +431,7 @@ class PluginSystemManager:
return False return False
module_name = pluginInfo.get_module_name() module_name = pluginInfo.get_module_name()
logger.info(f"Unloading plugin: {module_name}")
try: try:
# Not loaded? # Not loaded?
@ -441,22 +442,43 @@ class PluginSystemManager:
plugin_instance = pluginInfo.instance plugin_instance = pluginInfo.instance
if plugin_instance: if plugin_instance:
try: try:
logger.info(f"Calling deactivate hook for plugin: {module_name}")
self.plugin_manager.hook.deactivate(plugin=plugin_instance) self.plugin_manager.hook.deactivate(plugin=plugin_instance)
except Exception as e: except Exception as e:
logger.error(f"Error deactivating plugin {module_name}: {e}") logger.error(f"Error deactivating plugin {module_name}: {e}")
import traceback
logger.error(traceback.format_exc())
# Make sure any registered bindings are cleaned up
try:
if self.getApp():
api_helper = self.getApp().getAPIHelper()
if api_helper:
logger.info(f"Cleaning up any remaining bindings for {module_name}")
api_helper.unregisterAllForContext(module_name)
except Exception as e:
logger.error(f"Failed to clean up bindings for {module_name}: {e}")
import traceback
logger.error(traceback.format_exc())
# Unregister from pluggy # Unregister from pluggy
try:
self.plugin_manager.unregister(plugin_instance) self.plugin_manager.unregister(plugin_instance)
logger.info(f"Unregistered plugin {module_name} from pluggy")
except Exception as e:
logger.error(f"Error unregistering plugin from pluggy: {e}")
# Clean up # Clean up
pluginInfo.instance = None pluginInfo.instance = None
pluginInfo.loaded = False pluginInfo.loaded = False
logger.info(f"Unloaded plugin: {module_name}") logger.info(f"Successfully unloaded plugin: {module_name}")
return True return True
except Exception as e: except Exception as e:
logger.error(f"Failed to unload plugin {module_name}: {e}") logger.error(f"Failed to unload plugin {module_name}: {e}")
import traceback
logger.error(traceback.format_exc())
return False return False
def unloadAllPlugins(self, ForceAllPlugins=False): def unloadAllPlugins(self, ForceAllPlugins=False):