Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
04b8592ed3 | ||
|
c64591a162 | ||
|
80212d616f | ||
|
9790a8d494 | ||
|
ec90906052 | ||
|
f01374d15e | ||
|
0347b7feea | ||
|
0580dda131 |
@ -1 +0,0 @@
|
|||||||
See http://wiki.gnome.org/Projects/Cthulhu
|
|
@ -1,5 +1,10 @@
|
|||||||
# Cthulhu
|
# Cthulhu
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
If you somehow stumbled across this while looking for a desktop screen reader for Linux, you most likely want [Orca](https://orca.gnome.org/) instead. Cthulhu is currently a supplemental screen reader that fills a nitch for some advanced users. E.g. some older QT based programs may work with Cthulhu, and if you use certain window managers like i3, Mozilla applications like Firefox and Thunderbird may work better.
|
||||||
|
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Cthulhu is a free, open source, flexible, and extensible screen reader
|
Cthulhu is a free, open source, flexible, and extensible screen reader
|
||||||
@ -20,7 +25,7 @@ Cthulhu has the following dependencies:
|
|||||||
|
|
||||||
* Python 3 - Python platform
|
* Python 3 - Python platform
|
||||||
* pygobject-3.0 - Python bindings for the GObject library
|
* pygobject-3.0 - Python bindings for the GObject library
|
||||||
* libpeas - GObject based Plugin engine
|
* pluggy - Plugin and hook calling mechanisms for python
|
||||||
* gtk+-3.0 - GTK+ toolkit
|
* gtk+-3.0 - GTK+ toolkit
|
||||||
* json-py - a JSON (<https://json.org/>) reader and writer in Python
|
* json-py - a JSON (<https://json.org/>) reader and writer in Python
|
||||||
* python-speechd - Python bindings for Speech Dispatcher (optional)
|
* python-speechd - Python bindings for Speech Dispatcher (optional)
|
||||||
@ -30,7 +35,6 @@ Cthulhu has the following dependencies:
|
|||||||
* py-setproctitle - Python library to set the process title (optional)
|
* py-setproctitle - Python library to set the process title (optional)
|
||||||
* gstreamer-1.0 - GStreamer - Streaming media framework (optional)
|
* gstreamer-1.0 - GStreamer - Streaming media framework (optional)
|
||||||
* socat - Used for self-voicing functionality.
|
* socat - Used for self-voicing functionality.
|
||||||
* libpeas - For the plugin system.
|
|
||||||
|
|
||||||
You are strongly encouraged to also have the latest stable versions
|
You are strongly encouraged to also have the latest stable versions
|
||||||
of AT-SPI2 and ATK.
|
of AT-SPI2 and ATK.
|
||||||
|
@ -37,49 +37,35 @@ import faulthandler
|
|||||||
|
|
||||||
class APIHelper:
|
class APIHelper:
|
||||||
"""Helper class for plugin API interactions, including keybindings."""
|
"""Helper class for plugin API interactions, including keybindings."""
|
||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
"""Initialize the APIHelper.
|
"""Initialize the APIHelper.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
- app: the Cthulhu application
|
- app: the Cthulhu application
|
||||||
"""
|
"""
|
||||||
self.app = app
|
self.app = app
|
||||||
self._gestureBindings = {}
|
self._gestureBindings = {}
|
||||||
|
|
||||||
def registerGestureByString(self, function, name, gestureString,
|
def registerGestureByString(self, function, name, gestureString, inputEventType='default', normalizer='cthulhu', learnModeEnabled=True, contextName=None):
|
||||||
inputEventType='default', normalizer='cthulhu',
|
"""Register a gesture by string."""
|
||||||
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:"):
|
if not gestureString.startswith("kb:"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Extract the key portion from the gesture string
|
# Extract the key portion from the gesture string
|
||||||
key = gestureString.split(":", 1)[1]
|
key = gestureString.split(":", 1)[1]
|
||||||
|
|
||||||
# Handle Cthulhu modifier specially
|
# Handle Cthulhu modifier specially
|
||||||
if "cthulhu+" in key.lower():
|
if "cthulhu+" in key.lower():
|
||||||
from . import keybindings
|
from . import keybindings
|
||||||
key_parts = key.lower().split("+")
|
key_parts = key.lower().split("+")
|
||||||
|
|
||||||
# Determine appropriate modifier mask
|
# Determine appropriate modifier mask
|
||||||
modifiers = keybindings.CTHULHU_MODIFIER_MASK
|
modifiers = keybindings.CTHULHU_MODIFIER_MASK
|
||||||
|
|
||||||
# Extract the final key (without modifiers)
|
# Extract the final key (without modifiers)
|
||||||
final_key = key_parts[-1]
|
final_key = key_parts[-1]
|
||||||
|
|
||||||
# Check for additional modifiers
|
# Check for additional modifiers
|
||||||
if "shift" in key_parts:
|
if "shift" in key_parts:
|
||||||
modifiers = keybindings.CTHULHU_SHIFT_MODIFIER_MASK
|
modifiers = keybindings.CTHULHU_SHIFT_MODIFIER_MASK
|
||||||
@ -87,18 +73,23 @@ class APIHelper:
|
|||||||
modifiers = keybindings.CTHULHU_CTRL_MODIFIER_MASK
|
modifiers = keybindings.CTHULHU_CTRL_MODIFIER_MASK
|
||||||
elif "alt" in key_parts:
|
elif "alt" in key_parts:
|
||||||
modifiers = keybindings.CTHULHU_ALT_MODIFIER_MASK
|
modifiers = keybindings.CTHULHU_ALT_MODIFIER_MASK
|
||||||
|
|
||||||
# Create a keybinding handler
|
# Create a keybinding handler
|
||||||
class GestureHandler:
|
class GestureHandler:
|
||||||
def __init__(self, function, description):
|
def __init__(self, function, description):
|
||||||
self.function = function
|
self.function = function
|
||||||
self.description = description
|
self.description = description
|
||||||
|
|
||||||
def __call__(self, script, inputEvent):
|
def __call__(self, script, inputEvent):
|
||||||
return self.function(script, inputEvent)
|
try:
|
||||||
|
return function(script, inputEvent)
|
||||||
|
except Exception as e:
|
||||||
|
import logging
|
||||||
|
logging.getLogger(__name__).error(f"Error in keybinding handler: {e}")
|
||||||
|
return True
|
||||||
|
|
||||||
handler = GestureHandler(function, name)
|
handler = GestureHandler(function, name)
|
||||||
|
|
||||||
# Register the binding with the active script
|
# Register the binding with the active script
|
||||||
from . import cthulhu_state
|
from . import cthulhu_state
|
||||||
if cthulhu_state.activeScript:
|
if cthulhu_state.activeScript:
|
||||||
@ -108,20 +99,30 @@ class APIHelper:
|
|||||||
keybindings.defaultModifierMask,
|
keybindings.defaultModifierMask,
|
||||||
modifiers,
|
modifiers,
|
||||||
handler)
|
handler)
|
||||||
|
|
||||||
|
# Add the binding to the active script
|
||||||
bindings.add(binding)
|
bindings.add(binding)
|
||||||
|
|
||||||
# Store binding for later reference
|
# Store binding for later reference
|
||||||
if contextName not in self._gestureBindings:
|
if contextName not in self._gestureBindings:
|
||||||
self._gestureBindings[contextName] = []
|
self._gestureBindings[contextName] = []
|
||||||
self._gestureBindings[contextName].append(binding)
|
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
|
||||||
|
|
||||||
|
logger.info(f"Created binding: {binding.keysymstring} with modifiers {binding.modifiers}")
|
||||||
return binding
|
return binding
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def unregisterShortcut(self, binding, contextName=None):
|
def unregisterShortcut(self, binding, contextName=None):
|
||||||
"""Unregister a previously registered shortcut.
|
"""Unregister a previously registered shortcut.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
- binding: the binding to unregister
|
- binding: the binding to unregister
|
||||||
- contextName: the context for this gesture
|
- contextName: the context for this gesture
|
||||||
@ -131,11 +132,18 @@ class APIHelper:
|
|||||||
if cthulhu_state.activeScript:
|
if cthulhu_state.activeScript:
|
||||||
bindings = cthulhu_state.activeScript.getKeyBindings()
|
bindings = cthulhu_state.activeScript.getKeyBindings()
|
||||||
bindings.remove(binding)
|
bindings.remove(binding)
|
||||||
|
|
||||||
# Remove from our tracking
|
# 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 contextName in self._gestureBindings:
|
||||||
if binding in self._gestureBindings[contextName]:
|
if binding in self._gestureBindings[contextName]:
|
||||||
self._gestureBindings[contextName].remove(binding)
|
self._gestureBindings[contextName].remove(binding)
|
||||||
|
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
|
@ -23,5 +23,5 @@
|
|||||||
# Fork of Orca Screen Reader (GNOME)
|
# Fork of Orca Screen Reader (GNOME)
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
# Original source: https://gitlab.gnome.org/GNOME/orca
|
||||||
|
|
||||||
version = "2025.04.04"
|
version = "2025.04.18"
|
||||||
codeName = "testing"
|
codeName = "plugins"
|
||||||
|
@ -470,6 +470,8 @@ class KeyBindings:
|
|||||||
given keycode and modifiers, or None if no match exists.
|
given keycode and modifiers, or None if no match exists.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
logger.info(f"Looking for handler for key: {keyboardEvent.hw_code} with modifiers {keyboardEvent.modifiers}")
|
||||||
|
return binding
|
||||||
matches = []
|
matches = []
|
||||||
candidates = []
|
candidates = []
|
||||||
clickCount = keyboardEvent.getClickCount()
|
clickCount = keyboardEvent.getClickCount()
|
||||||
|
@ -21,7 +21,7 @@ except ImportError:
|
|||||||
# Fallback if pluggy is not available
|
# Fallback if pluggy is not available
|
||||||
def cthulhu_hookimpl(func=None, **kwargs):
|
def cthulhu_hookimpl(func=None, **kwargs):
|
||||||
"""Fallback decorator when pluggy is not available.
|
"""Fallback decorator when pluggy is not available.
|
||||||
|
|
||||||
This is a no-op decorator that returns the original function.
|
This is a no-op decorator that returns the original function.
|
||||||
It allows the code to continue working without pluggy, though
|
It allows the code to continue working without pluggy, though
|
||||||
plugins will be disabled.
|
plugins will be disabled.
|
||||||
@ -47,6 +47,8 @@ class Plugin:
|
|||||||
self.name = ''
|
self.name = ''
|
||||||
self.version = ''
|
self.version = ''
|
||||||
self.description = ''
|
self.description = ''
|
||||||
|
self._bindings = None
|
||||||
|
self._gestureBindings = {}
|
||||||
|
|
||||||
def set_app(self, app):
|
def set_app(self, app):
|
||||||
"""Set the application reference."""
|
"""Set the application reference."""
|
||||||
@ -75,12 +77,16 @@ class Plugin:
|
|||||||
return
|
return
|
||||||
logger.info(f"Deactivating plugin: {self.name}")
|
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):
|
def registerGestureByString(self, function, name, gestureString, learnModeEnabled=True):
|
||||||
"""Register a gesture by string."""
|
"""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(
|
binding = api_helper.registerGestureByString(
|
||||||
function,
|
function,
|
||||||
name,
|
name,
|
||||||
gestureString,
|
gestureString,
|
||||||
@ -89,4 +95,13 @@ class Plugin:
|
|||||||
learnModeEnabled,
|
learnModeEnabled,
|
||||||
contextName=self.module_name
|
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
|
return None
|
||||||
|
@ -112,11 +112,72 @@ class PluginSystemManager:
|
|||||||
|
|
||||||
# Create plugin directories
|
# Create plugin directories
|
||||||
self._setup_plugin_dirs()
|
self._setup_plugin_dirs()
|
||||||
|
|
||||||
# Log available plugins directory paths
|
# Log available plugins directory paths
|
||||||
logger.info(f"System plugins directory: {PluginType.SYSTEM.get_root_dir()}")
|
logger.info(f"System plugins directory: {PluginType.SYSTEM.get_root_dir()}")
|
||||||
logger.info(f"User plugins directory: {PluginType.USER.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):
|
def _setup_plugin_dirs(self):
|
||||||
"""Ensure plugin directories exist."""
|
"""Ensure plugin directories exist."""
|
||||||
os.makedirs(PluginType.SYSTEM.get_root_dir(), exist_ok=True)
|
os.makedirs(PluginType.SYSTEM.get_root_dir(), exist_ok=True)
|
||||||
@ -219,24 +280,24 @@ class PluginSystemManager:
|
|||||||
def setActivePlugins(self, activePlugins):
|
def setActivePlugins(self, activePlugins):
|
||||||
"""Set active plugins and sync their state."""
|
"""Set active plugins and sync their state."""
|
||||||
logger.info(f"Setting active plugins: {activePlugins}")
|
logger.info(f"Setting active plugins: {activePlugins}")
|
||||||
|
|
||||||
# Make sure we have scanned for plugins first
|
# Make sure we have scanned for plugins first
|
||||||
if not self._plugins:
|
if not self._plugins:
|
||||||
logger.info("No plugins found, rescanning...")
|
logger.info("No plugins found, rescanning...")
|
||||||
self.rescanPlugins()
|
self.rescanPlugins()
|
||||||
|
|
||||||
self._active_plugins = activePlugins
|
self._active_plugins = activePlugins
|
||||||
|
|
||||||
# Log active vs available plugins
|
# Log active vs available plugins
|
||||||
available_plugins = [p.get_module_name() for p in self.plugins]
|
available_plugins = [p.get_module_name() for p in self.plugins]
|
||||||
logger.info(f"Available plugins: {available_plugins}")
|
logger.info(f"Available plugins: {available_plugins}")
|
||||||
logger.info(f"Active plugins: {self._active_plugins}")
|
logger.info(f"Active plugins: {self._active_plugins}")
|
||||||
|
|
||||||
# Find missing plugins
|
# Find missing plugins
|
||||||
missing_plugins = [p for p in self._active_plugins if p not in available_plugins]
|
missing_plugins = [p for p in self._active_plugins if p not in available_plugins]
|
||||||
if missing_plugins:
|
if missing_plugins:
|
||||||
logger.warning(f"Active plugins not found: {missing_plugins}")
|
logger.warning(f"Active plugins not found: {missing_plugins}")
|
||||||
|
|
||||||
self.syncAllPluginsActive()
|
self.syncAllPluginsActive()
|
||||||
|
|
||||||
def setPluginActive(self, pluginInfo, active):
|
def setPluginActive(self, pluginInfo, active):
|
||||||
@ -258,7 +319,7 @@ class PluginSystemManager:
|
|||||||
def isPluginActive(self, pluginInfo):
|
def isPluginActive(self, pluginInfo):
|
||||||
"""Check if a plugin is active."""
|
"""Check if a plugin is active."""
|
||||||
module_name = pluginInfo.get_module_name()
|
module_name = pluginInfo.get_module_name()
|
||||||
|
|
||||||
# Builtin plugins are always active
|
# Builtin plugins are always active
|
||||||
if pluginInfo.builtin:
|
if pluginInfo.builtin:
|
||||||
logger.debug(f"Plugin {module_name} is builtin, active by default")
|
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
|
# Check case-insensitive match in active plugins list
|
||||||
active_plugins = self.getActivePlugins()
|
active_plugins = self.getActivePlugins()
|
||||||
|
|
||||||
# Try exact match first
|
# Try exact match first
|
||||||
if module_name in active_plugins:
|
if module_name in active_plugins:
|
||||||
logger.debug(f"Plugin {module_name} found in active plugins list")
|
logger.debug(f"Plugin {module_name} found in active plugins list")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Try case-insensitive match
|
# Try case-insensitive match
|
||||||
module_name_lower = module_name.lower()
|
module_name_lower = module_name.lower()
|
||||||
is_active = any(plugin.lower() == module_name_lower for plugin in active_plugins)
|
is_active = any(plugin.lower() == module_name_lower for plugin in active_plugins)
|
||||||
|
|
||||||
if is_active:
|
if is_active:
|
||||||
logger.debug(f"Plugin {module_name} found in active plugins list (case-insensitive match)")
|
logger.debug(f"Plugin {module_name} found in active plugins list (case-insensitive match)")
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Plugin {module_name} not found in active plugins list")
|
logger.debug(f"Plugin {module_name} not found in active plugins list")
|
||||||
|
|
||||||
return is_active
|
return is_active
|
||||||
|
|
||||||
def syncAllPluginsActive(self):
|
def syncAllPluginsActive(self):
|
||||||
"""Sync the active state of all plugins."""
|
"""Sync the active state of all plugins."""
|
||||||
logger.info("Syncing active state of all plugins")
|
logger.info("Syncing active state of all plugins")
|
||||||
|
|
||||||
# Log plugin status before syncing
|
# Log plugin status before syncing
|
||||||
if PLUGIN_DEBUG:
|
if PLUGIN_DEBUG:
|
||||||
for pluginInfo in self.plugins:
|
for pluginInfo in self.plugins:
|
||||||
is_active = self.isPluginActive(pluginInfo)
|
is_active = self.isPluginActive(pluginInfo)
|
||||||
is_loaded = pluginInfo.loaded
|
is_loaded = pluginInfo.loaded
|
||||||
logger.debug(f"Plugin {pluginInfo.get_module_name()}: active={is_active}, loaded={is_loaded}")
|
logger.debug(f"Plugin {pluginInfo.get_module_name()}: active={is_active}, loaded={is_loaded}")
|
||||||
|
|
||||||
# First unload inactive plugins
|
# First unload inactive plugins
|
||||||
for pluginInfo in self.plugins:
|
for pluginInfo in self.plugins:
|
||||||
if not self.isPluginActive(pluginInfo) and pluginInfo.loaded:
|
if not self.isPluginActive(pluginInfo) and pluginInfo.loaded:
|
||||||
@ -311,7 +372,7 @@ class PluginSystemManager:
|
|||||||
logger.info(f"Loading active plugin: {pluginInfo.get_module_name()}")
|
logger.info(f"Loading active plugin: {pluginInfo.get_module_name()}")
|
||||||
result = self.loadPlugin(pluginInfo)
|
result = self.loadPlugin(pluginInfo)
|
||||||
logger.info(f"Plugin {pluginInfo.get_module_name()} load result: {result}")
|
logger.info(f"Plugin {pluginInfo.get_module_name()} load result: {result}")
|
||||||
|
|
||||||
# Log final plugin status
|
# Log final plugin status
|
||||||
active_plugins = [p.get_module_name() for p in self.plugins if p.loaded]
|
active_plugins = [p.get_module_name() for p in self.plugins if p.loaded]
|
||||||
logger.info(f"Active plugins after sync: {active_plugins}")
|
logger.info(f"Active plugins after sync: {active_plugins}")
|
||||||
@ -337,27 +398,27 @@ class PluginSystemManager:
|
|||||||
# Try to find the plugin file
|
# Try to find the plugin file
|
||||||
module_name = pluginInfo.get_module_name()
|
module_name = pluginInfo.get_module_name()
|
||||||
plugin_dir = pluginInfo.get_module_dir()
|
plugin_dir = pluginInfo.get_module_dir()
|
||||||
|
|
||||||
# Check for plugin.py first (standard format)
|
# Check for plugin.py first (standard format)
|
||||||
plugin_file = os.path.join(plugin_dir, "plugin.py")
|
plugin_file = os.path.join(plugin_dir, "plugin.py")
|
||||||
|
|
||||||
# Fall back to [PluginName].py if plugin.py doesn't exist
|
# Fall back to [PluginName].py if plugin.py doesn't exist
|
||||||
if not os.path.exists(plugin_file):
|
if not os.path.exists(plugin_file):
|
||||||
alternative_plugin_file = os.path.join(plugin_dir, f"{module_name}.py")
|
alternative_plugin_file = os.path.join(plugin_dir, f"{module_name}.py")
|
||||||
if os.path.exists(alternative_plugin_file):
|
if os.path.exists(alternative_plugin_file):
|
||||||
plugin_file = alternative_plugin_file
|
plugin_file = alternative_plugin_file
|
||||||
logger.info(f"Using alternative plugin file: {alternative_plugin_file}")
|
logger.info(f"Using alternative plugin file: {alternative_plugin_file}")
|
||||||
|
|
||||||
if not os.path.exists(plugin_file):
|
if not os.path.exists(plugin_file):
|
||||||
logger.error(f"Plugin file not found: {plugin_file}")
|
logger.error(f"Plugin file not found: {plugin_file}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.info(f"Loading plugin from: {plugin_file}")
|
logger.info(f"Loading plugin from: {plugin_file}")
|
||||||
spec = importlib.util.spec_from_file_location(module_name, plugin_file)
|
spec = importlib.util.spec_from_file_location(module_name, plugin_file)
|
||||||
if spec is None:
|
if spec is None:
|
||||||
logger.error(f"Failed to create spec for plugin: {module_name}")
|
logger.error(f"Failed to create spec for plugin: {module_name}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module)
|
||||||
pluginInfo.module = module
|
pluginInfo.module = module
|
||||||
@ -385,7 +446,7 @@ class PluginSystemManager:
|
|||||||
# Ensure plugins have a reference to the app
|
# Ensure plugins have a reference to the app
|
||||||
plugin_instance.app = self.getApp()
|
plugin_instance.app = self.getApp()
|
||||||
logger.info(f"Set app reference for plugin: {module_name}")
|
logger.info(f"Set app reference for plugin: {module_name}")
|
||||||
|
|
||||||
if hasattr(plugin_instance, 'set_app'):
|
if hasattr(plugin_instance, 'set_app'):
|
||||||
plugin_instance.set_app(self.getApp())
|
plugin_instance.set_app(self.getApp())
|
||||||
logger.info(f"Called set_app() for plugin: {module_name}")
|
logger.info(f"Called set_app() for plugin: {module_name}")
|
||||||
@ -398,10 +459,10 @@ class PluginSystemManager:
|
|||||||
if self.plugin_manager is None:
|
if self.plugin_manager is None:
|
||||||
logger.error(f"Plugin manager is None when loading {module_name}")
|
logger.error(f"Plugin manager is None when loading {module_name}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.info(f"Registering plugin with pluggy: {module_name}")
|
logger.info(f"Registering plugin with pluggy: {module_name}")
|
||||||
self.plugin_manager.register(plugin_instance)
|
self.plugin_manager.register(plugin_instance)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"Activating plugin: {module_name}")
|
logger.info(f"Activating plugin: {module_name}")
|
||||||
self.plugin_manager.hook.activate(plugin=plugin_instance)
|
self.plugin_manager.hook.activate(plugin=plugin_instance)
|
||||||
@ -413,6 +474,10 @@ class PluginSystemManager:
|
|||||||
|
|
||||||
pluginInfo.loaded = True
|
pluginInfo.loaded = True
|
||||||
logger.info(f"Successfully loaded plugin: {module_name}")
|
logger.info(f"Successfully loaded plugin: {module_name}")
|
||||||
|
|
||||||
|
# Register any global keybindings from the plugin
|
||||||
|
self.register_plugin_global_keybindings(pluginInfo.instance)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -40,8 +40,11 @@ class DisplayVersion(Plugin):
|
|||||||
f'Cthulhu screen reader version {cthulhuVersion.version}-{cthulhuVersion.codeName}',
|
f'Cthulhu screen reader version {cthulhuVersion.version}-{cthulhuVersion.codeName}',
|
||||||
'kb:cthulhu+shift+v'
|
'kb:cthulhu+shift+v'
|
||||||
)
|
)
|
||||||
|
logger.info(f"Registered keybinding: {self._kb_binding}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error activating DisplayVersion plugin: {e}")
|
logger.error(f"Error activating DisplayVersion plugin: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
@cthulhu_hookimpl
|
@cthulhu_hookimpl
|
||||||
def deactivate(self, plugin=None):
|
def deactivate(self, plugin=None):
|
||||||
@ -55,6 +58,7 @@ class DisplayVersion(Plugin):
|
|||||||
def speakText(self, script=None, inputEvent=None):
|
def speakText(self, script=None, inputEvent=None):
|
||||||
"""Speak the Cthulhu version when shortcut is pressed."""
|
"""Speak the Cthulhu version when shortcut is pressed."""
|
||||||
try:
|
try:
|
||||||
|
logger.info("DisplayVersion plugin: speakText called")
|
||||||
if self.app:
|
if self.app:
|
||||||
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
|
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
|
||||||
if state.activeScript:
|
if state.activeScript:
|
||||||
@ -65,4 +69,4 @@ class DisplayVersion(Plugin):
|
|||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in DisplayVersion speakText: {e}")
|
logger.error(f"Error in DisplayVersion speakText: {e}")
|
||||||
return False
|
return False
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
#
|
#
|
||||||
# Copyright (c) 2024 Stormux
|
# Copyright (c) 2024 Stormux
|
||||||
# Copyright (c) 2010-2012 The Orca Team
|
|
||||||
# Copyright (c) 2012 Igalia, S.L.
|
|
||||||
# Copyright (c) 2005-2010 Sun Microsystems Inc.
|
|
||||||
#
|
#
|
||||||
# This library is free software; you can redistribute it and/or
|
# This library is free software; you can redistribute it and/or
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
@ -20,8 +17,6 @@
|
|||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||||
# Boston MA 02110-1301 USA.
|
# Boston MA 02110-1301 USA.
|
||||||
#
|
#
|
||||||
# Fork of Orca Screen Reader (GNOME)
|
|
||||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
|
||||||
|
|
||||||
"""Self Voice plugin for Cthulhu screen reader."""
|
"""Self Voice plugin for Cthulhu screen reader."""
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user