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
|
||||
|
||||
## 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
|
||||
|
||||
Cthulhu is a free, open source, flexible, and extensible screen reader
|
||||
@ -20,7 +25,7 @@ Cthulhu has the following dependencies:
|
||||
|
||||
* Python 3 - Python platform
|
||||
* 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
|
||||
* json-py - a JSON (<https://json.org/>) reader and writer in Python
|
||||
* 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)
|
||||
* gstreamer-1.0 - GStreamer - Streaming media framework (optional)
|
||||
* socat - Used for self-voicing functionality.
|
||||
* libpeas - For the plugin system.
|
||||
|
||||
You are strongly encouraged to also have the latest stable versions
|
||||
of AT-SPI2 and ATK.
|
||||
|
@ -47,22 +47,8 @@ class APIHelper:
|
||||
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
|
||||
"""
|
||||
def registerGestureByString(self, function, name, gestureString, inputEventType='default', normalizer='cthulhu', learnModeEnabled=True, contextName=None):
|
||||
"""Register a gesture by string."""
|
||||
if not gestureString.startswith("kb:"):
|
||||
return None
|
||||
|
||||
@ -95,7 +81,12 @@ class APIHelper:
|
||||
self.description = description
|
||||
|
||||
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)
|
||||
|
||||
@ -108,6 +99,8 @@ class APIHelper:
|
||||
keybindings.defaultModifierMask,
|
||||
modifiers,
|
||||
handler)
|
||||
|
||||
# Add the binding to the active script
|
||||
bindings.add(binding)
|
||||
|
||||
# Store binding for later reference
|
||||
@ -115,6 +108,14 @@ class APIHelper:
|
||||
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
|
||||
|
||||
logger.info(f"Created binding: {binding.keysymstring} with modifiers {binding.modifiers}")
|
||||
return binding
|
||||
|
||||
return None
|
||||
@ -132,10 +133,17 @@ class APIHelper:
|
||||
bindings = cthulhu_state.activeScript.getKeyBindings()
|
||||
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 binding in self._gestureBindings[contextName]:
|
||||
self._gestureBindings[contextName].remove(binding)
|
||||
|
||||
|
||||
import gi
|
||||
import importlib
|
||||
import os
|
||||
|
@ -23,5 +23,5 @@
|
||||
# Fork of Orca Screen Reader (GNOME)
|
||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
||||
|
||||
version = "2025.04.04"
|
||||
codeName = "testing"
|
||||
version = "2025.04.18"
|
||||
codeName = "plugins"
|
||||
|
@ -470,6 +470,8 @@ class KeyBindings:
|
||||
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 = []
|
||||
candidates = []
|
||||
clickCount = keyboardEvent.getClickCount()
|
||||
|
@ -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
|
||||
|
@ -117,6 +117,67 @@ class PluginSystemManager:
|
||||
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)
|
||||
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -1,9 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# 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
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
@ -20,8 +17,6 @@
|
||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||
# 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."""
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user