Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c26025a81 | |||
| 0405200980 | |||
| 408fb85730 | |||
| 0f25245d3d | |||
| 13f110ab34 | |||
| 62f46c0eb7 | |||
| e2364a154a | |||
| 2090767794 | |||
| ea50d8b024 | |||
| a1d90a7245 | |||
| 2eb6d3c7dd | |||
| 0edbefac47 | |||
| 90aecf8055 | |||
| 48d99e8813 | |||
| 314aa18a1b | |||
| a21f1aa13b | |||
| 5181944de0 | |||
| 0a8bb684ec | |||
| d94ba1accb | |||
| 399f449484 | |||
| ecd122786f | |||
| 01273618a7 | |||
| d8df2ed757 | |||
| c376b2489a | |||
| 39dca0574a | |||
| 8b1f501fe7 | |||
| 96335baf5d | |||
| 51984a6540 | |||
| 3296e5d571 | |||
| 1e6f4b8913 | |||
| 331b1c3ad5 | |||
| 04b8592ed3 | |||
| c64591a162 | |||
| 80212d616f | |||
| 9790a8d494 | |||
| ec90906052 | |||
| f01374d15e | |||
| 0347b7feea | |||
| 0580dda131 |
@@ -1,6 +1,6 @@
|
|||||||
Welcome to Cthulhu
|
Welcome to Cthulhu
|
||||||
|
|
||||||
We are excited to have you here and welcome your contributions to the Cthulhu screen reader project! This project is a fork of Cthulhu, with a focus on creating an open and collaborative community where contributions are encouraged.
|
We are excited to have you here and welcome your contributions to the Cthulhu screen reader project! This project is a fork of Orca, with a focus on creating an open and collaborative community where contributions are encouraged.
|
||||||
|
|
||||||
|
|
||||||
How to Contribute
|
How to Contribute
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
ACLOCAL_AMFLAGS = -I m4 ${ACLOCAL_FLAGS}
|
ACLOCAL_AMFLAGS = -I m4 ${ACLOCAL_FLAGS}
|
||||||
|
|
||||||
|
if BUILD_HELP
|
||||||
SUBDIRS = docs icons po src help
|
SUBDIRS = docs icons po src help
|
||||||
|
else
|
||||||
|
SUBDIRS = docs icons po src
|
||||||
|
endif
|
||||||
|
|
||||||
DISTCHECK_CONFIGURE_FLAGS = \
|
DISTCHECK_CONFIGURE_FLAGS = \
|
||||||
--disable-scrollkeeper
|
--disable-scrollkeeper
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
+8
-4
@@ -32,10 +32,14 @@ autoreconf --verbose --force --install -Wno-portability || {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
which yelp-build > /dev/null || {
|
# Only check for yelp-build if help documentation will be built
|
||||||
echo "Try installing the 'yelp-tools' package."
|
# Skip check if SKIP_YELP environment variable is set
|
||||||
exit 1
|
if [ "${SKIP_YELP}" != "1" ]; then
|
||||||
}
|
which yelp-build > /dev/null || {
|
||||||
|
echo "Try installing the 'yelp-tools' package, or set SKIP_YELP=1 to skip documentation."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
cd "$olddir"
|
cd "$olddir"
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -24,8 +24,16 @@ GETTEXT_PACKAGE=AC_PACKAGE_TARNAME
|
|||||||
AC_SUBST(GETTEXT_PACKAGE)
|
AC_SUBST(GETTEXT_PACKAGE)
|
||||||
AC_DEFINE_UNQUOTED(GETTEXT_PACKAGE,"$GETTEXT_PACKAGE", [gettext package])
|
AC_DEFINE_UNQUOTED(GETTEXT_PACKAGE,"$GETTEXT_PACKAGE", [gettext package])
|
||||||
|
|
||||||
# User Documentation
|
# User Documentation (optional)
|
||||||
YELP_HELP_INIT
|
AC_ARG_ENABLE([help],
|
||||||
|
AS_HELP_STRING([--disable-help], [Disable building help documentation]))
|
||||||
|
AS_IF([test "x$enable_help" != "xno"], [
|
||||||
|
YELP_HELP_INIT
|
||||||
|
BUILD_HELP=yes
|
||||||
|
], [
|
||||||
|
BUILD_HELP=no
|
||||||
|
])
|
||||||
|
AM_CONDITIONAL(BUILD_HELP, test "x$BUILD_HELP" = "xyes")
|
||||||
|
|
||||||
PKG_CHECK_MODULES([PYGOBJECT], [pygobject-3.0 >= pygobject_required_version])
|
PKG_CHECK_MODULES([PYGOBJECT], [pygobject-3.0 >= pygobject_required_version])
|
||||||
PKG_CHECK_MODULES([ATSPI2], [atspi-2 >= atspi_required_version])
|
PKG_CHECK_MODULES([ATSPI2], [atspi-2 >= atspi_required_version])
|
||||||
@@ -129,6 +137,7 @@ src/cthulhu/plugins/ByeCthulhu/Makefile
|
|||||||
src/cthulhu/plugins/HelloCthulhu/Makefile
|
src/cthulhu/plugins/HelloCthulhu/Makefile
|
||||||
src/cthulhu/plugins/Clipboard/Makefile
|
src/cthulhu/plugins/Clipboard/Makefile
|
||||||
src/cthulhu/plugins/DisplayVersion/Makefile
|
src/cthulhu/plugins/DisplayVersion/Makefile
|
||||||
|
src/cthulhu/plugins/IndentationAudio/Makefile
|
||||||
src/cthulhu/plugins/hello_world/Makefile
|
src/cthulhu/plugins/hello_world/Makefile
|
||||||
src/cthulhu/plugins/self_voice/Makefile
|
src/cthulhu/plugins/self_voice/Makefile
|
||||||
src/cthulhu/plugins/SimplePluginSystem/Makefile
|
src/cthulhu/plugins/SimplePluginSystem/Makefile
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||||
|
|
||||||
pkgname=cthulhu
|
pkgname=cthulhu
|
||||||
pkgver=0.4
|
pkgver=2025.07.01
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Screen reader for individuals who are blind or visually impaired forked from Orca"
|
pkgdesc="Screen reader for individuals who are blind or visually impaired forked from Orca"
|
||||||
url="https://git.stormux.org/storm/cthulhu"
|
url="https://git.stormux.org/storm/cthulhu"
|
||||||
@@ -25,6 +25,7 @@ depends=(
|
|||||||
python-atspi
|
python-atspi
|
||||||
python-cairo
|
python-cairo
|
||||||
python-gobject
|
python-gobject
|
||||||
|
python-pluggy
|
||||||
python-setproctitle
|
python-setproctitle
|
||||||
socat
|
socat
|
||||||
speech-dispatcher
|
speech-dispatcher
|
||||||
@@ -33,15 +34,14 @@ depends=(
|
|||||||
)
|
)
|
||||||
makedepends=(
|
makedepends=(
|
||||||
git
|
git
|
||||||
itstool
|
|
||||||
yelp-tools
|
|
||||||
)
|
)
|
||||||
source=("git+https://git.stormux.org/storm/cthulhu.git")
|
source=("git+https://git.stormux.org/storm/cthulhu.git")
|
||||||
b2sums=('SKIP')
|
b2sums=('SKIP')
|
||||||
|
|
||||||
prepare() {
|
prepare() {
|
||||||
cd cthulhu
|
cd cthulhu
|
||||||
NOCONFIGURE=1 ./autogen.sh
|
git checkout testing
|
||||||
|
NOCONFIGURE=1 SKIP_YELP=1 ./autogen.sh
|
||||||
}
|
}
|
||||||
|
|
||||||
pkgver() {
|
pkgver() {
|
||||||
@@ -51,7 +51,7 @@ pkgver() {
|
|||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd cthulhu
|
cd cthulhu
|
||||||
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var
|
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --disable-help
|
||||||
make
|
make
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+89
-49
@@ -37,91 +37,118 @@ 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, inputEventType='default', normalizer='cthulhu', learnModeEnabled=True, contextName=None):
|
||||||
|
"""Register a gesture by string."""
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def registerGestureByString(self, function, name, gestureString,
|
logger.info(f"=== APIHelper.registerGestureByString called ===")
|
||||||
inputEventType='default', normalizer='cthulhu',
|
logger.info(f"gestureString: {gestureString}")
|
||||||
learnModeEnabled=True, contextName=None):
|
logger.info(f"name: {name}")
|
||||||
"""Register a gesture by string.
|
logger.info(f"contextName: {contextName}")
|
||||||
|
|
||||||
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:"):
|
||||||
|
logger.warning(f"Gesture string doesn't start with 'kb:': {gestureString}")
|
||||||
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]
|
||||||
|
logger.info(f"Extracted key: {key}")
|
||||||
|
|
||||||
# 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("+")
|
||||||
|
logger.info(f"Key parts: {key_parts}")
|
||||||
# Determine appropriate modifier mask
|
|
||||||
|
# Start with the base Cthulhu modifier
|
||||||
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]
|
||||||
|
logger.info(f"Final key: {final_key}")
|
||||||
# Check for additional modifiers
|
|
||||||
|
# Check for additional modifiers and combine them properly
|
||||||
if "shift" in key_parts:
|
if "shift" in key_parts:
|
||||||
|
# Use the pre-defined combined mask rather than trying to OR them
|
||||||
modifiers = keybindings.CTHULHU_SHIFT_MODIFIER_MASK
|
modifiers = keybindings.CTHULHU_SHIFT_MODIFIER_MASK
|
||||||
elif "ctrl" in key_parts or "control" in key_parts:
|
logger.info(f"Using CTHULHU_SHIFT_MODIFIER_MASK: {modifiers}")
|
||||||
modifiers = keybindings.CTHULHU_CTRL_MODIFIER_MASK
|
else:
|
||||||
elif "alt" in key_parts:
|
logger.info(f"Using CTHULHU_MODIFIER_MASK: {modifiers}")
|
||||||
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:
|
||||||
|
logger.info(f"=== DisplayVersion keybinding handler called! ===")
|
||||||
|
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)
|
||||||
|
logger.info(f"Created handler: {handler}")
|
||||||
# Register the binding with the active script
|
|
||||||
|
# Create the binding object regardless of whether there's an active script
|
||||||
|
# This allows plugins to define bindings that will work when a script becomes active
|
||||||
from . import cthulhu_state
|
from . import cthulhu_state
|
||||||
|
from . import keybindings
|
||||||
|
binding = keybindings.KeyBinding(
|
||||||
|
final_key,
|
||||||
|
keybindings.defaultModifierMask,
|
||||||
|
modifiers,
|
||||||
|
handler)
|
||||||
|
|
||||||
|
logger.info(f"Created binding: keysym={binding.keysymstring}, modifiers={binding.modifiers}, mask={binding.modifier_mask}")
|
||||||
|
|
||||||
|
# Store binding for later reference
|
||||||
|
if contextName not in self._gestureBindings:
|
||||||
|
self._gestureBindings[contextName] = []
|
||||||
|
self._gestureBindings[contextName].append(binding)
|
||||||
|
logger.info(f"Stored binding in context '{contextName}'")
|
||||||
|
|
||||||
|
# Only add to active script if one exists
|
||||||
if cthulhu_state.activeScript:
|
if cthulhu_state.activeScript:
|
||||||
|
logger.info(f"Adding binding to active script: {cthulhu_state.activeScript}")
|
||||||
bindings = cthulhu_state.activeScript.getKeyBindings()
|
bindings = cthulhu_state.activeScript.getKeyBindings()
|
||||||
binding = keybindings.KeyBinding(
|
|
||||||
final_key,
|
|
||||||
keybindings.defaultModifierMask,
|
|
||||||
modifiers,
|
|
||||||
handler)
|
|
||||||
bindings.add(binding)
|
bindings.add(binding)
|
||||||
|
|
||||||
# Store binding for later reference
|
# Register key grab at the system level
|
||||||
if contextName not in self._gestureBindings:
|
grab_ids = self.app.addKeyGrab(binding)
|
||||||
self._gestureBindings[contextName] = []
|
logger.info(f"Key grab IDs: {grab_ids}")
|
||||||
self._gestureBindings[contextName].append(binding)
|
|
||||||
|
|
||||||
return binding
|
# For later removal
|
||||||
|
if grab_ids:
|
||||||
|
binding._grab_ids = grab_ids
|
||||||
|
else:
|
||||||
|
logger.warning("No active script available - binding stored for later registration")
|
||||||
|
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, f"Created binding: {binding.keysymstring} with modifiers {binding.modifiers}", True)
|
||||||
|
logger.info("=== APIHelper.registerGestureByString completed ===")
|
||||||
|
return binding
|
||||||
|
else:
|
||||||
|
logger.warning(f"Key doesn't contain 'cthulhu+': {key}")
|
||||||
|
|
||||||
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 +158,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
|
||||||
@@ -629,6 +663,12 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False):
|
|||||||
|
|
||||||
_scriptManager.activate()
|
_scriptManager.activate()
|
||||||
_eventManager.activate()
|
_eventManager.activate()
|
||||||
|
|
||||||
|
# Refresh keybindings to include plugin bindings (after script manager is active)
|
||||||
|
cthulhuApp.getPluginSystemManager().refresh_active_script_keybindings()
|
||||||
|
|
||||||
|
cthulhuApp.getSignalManager().emitSignal('load-setting-begin')
|
||||||
|
# cthulhuApp.getPluginSystemManager().register_plugin_keybindings_with_active_script()
|
||||||
|
|
||||||
cthulhuApp.getSignalManager().emitSignal('load-setting-completed')
|
cthulhuApp.getSignalManager().emitSignal('load-setting-completed')
|
||||||
|
|
||||||
|
|||||||
@@ -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.07.01"
|
||||||
codeName = "testing"
|
codeName = "testing"
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ def println(level, text="", timestamp=False, stack=False):
|
|||||||
text = text.replace("\ufffc", "[OBJ]")
|
text = text.replace("\ufffc", "[OBJ]")
|
||||||
if timestamp:
|
if timestamp:
|
||||||
text = text.replace("\n", f"\n{' ' * 18}")
|
text = text.replace("\n", f"\n{' ' * 18}")
|
||||||
text = f"{datetime.now().strftime('%H:%M:%S.%f')} - {text}"
|
text = f"{text} - {datetime.now().strftime('%H:%M:%S.%f')}"
|
||||||
if stack:
|
if stack:
|
||||||
text += f" {_stackAsString()}"
|
text += f" {_stackAsString()}"
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,13 @@ class EventManager:
|
|||||||
cthulhu_state.device.key_watcher = cthulhu_state.device.add_key_watcher(
|
cthulhu_state.device.key_watcher = cthulhu_state.device.add_key_watcher(
|
||||||
self._processNewKeyboardEvent)
|
self._processNewKeyboardEvent)
|
||||||
self.newKeyHandlingActive = True
|
self.newKeyHandlingActive = True
|
||||||
|
|
||||||
|
# Notify plugin system that device is now available for keybinding registration
|
||||||
|
from . import cthulhu
|
||||||
|
if hasattr(cthulhu, 'cthulhuApp') and cthulhu.cthulhuApp:
|
||||||
|
plugin_manager = cthulhu.cthulhuApp.getPluginSystemManager()
|
||||||
|
if plugin_manager:
|
||||||
|
pass # plugin_manager.register_plugin_keybindings_with_active_script()
|
||||||
|
|
||||||
def activateLegacyKeyHandling(self):
|
def activateLegacyKeyHandling(self):
|
||||||
if not self.legacyKeyHandlingActive:
|
if not self.legacyKeyHandlingActive:
|
||||||
|
|||||||
@@ -251,6 +251,23 @@ class KeyBinding:
|
|||||||
if not self.keycode:
|
if not self.keycode:
|
||||||
self.keycode = getKeycode(self.keysymstring)
|
self.keycode = getKeycode(self.keysymstring)
|
||||||
|
|
||||||
|
# Debug logging for DisplayVersion plugin specifically
|
||||||
|
if self.keysymstring == 'v' and self.modifiers == 257:
|
||||||
|
with open('/tmp/displayversion_matches.log', 'a') as f:
|
||||||
|
f.write(f"=== DisplayVersion matches() debug ===\n")
|
||||||
|
f.write(f"Self keycode: {self.keycode}\n")
|
||||||
|
f.write(f"Self keysymstring: {self.keysymstring}\n")
|
||||||
|
f.write(f"Self modifiers: {self.modifiers}\n")
|
||||||
|
f.write(f"Self modifier_mask: {self.modifier_mask}\n")
|
||||||
|
f.write(f"Input keycode: {keycode}\n")
|
||||||
|
f.write(f"Input modifiers: {modifiers}\n")
|
||||||
|
f.write(f"Keycode match: {self.keycode == keycode}\n")
|
||||||
|
if self.keycode == keycode:
|
||||||
|
result = modifiers & self.modifier_mask
|
||||||
|
f.write(f"Modifier calculation: {modifiers} & {self.modifier_mask} = {result}\n")
|
||||||
|
f.write(f"Modifier match: {result == self.modifiers}\n")
|
||||||
|
f.write(f"Overall match: {self.keycode == keycode and (modifiers & self.modifier_mask) == self.modifiers}\n")
|
||||||
|
|
||||||
if self.keycode == keycode:
|
if self.keycode == keycode:
|
||||||
result = modifiers & self.modifier_mask
|
result = modifiers & self.modifier_mask
|
||||||
return result == self.modifiers
|
return result == self.modifiers
|
||||||
@@ -470,13 +487,41 @@ class KeyBindings:
|
|||||||
given keycode and modifiers, or None if no match exists.
|
given keycode and modifiers, or None if no match exists.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Check if this might be the DisplayVersion key combination
|
||||||
|
event_str = keyboardEvent.event_string if hasattr(keyboardEvent, 'event_string') else 'unknown'
|
||||||
|
if event_str.lower() == 'v':
|
||||||
|
logger.info(f"=== KeyBindings.getInputHandler: Looking for handler ===")
|
||||||
|
logger.info(f"Event string: {event_str}")
|
||||||
|
logger.info(f"Hardware code: {keyboardEvent.hw_code}")
|
||||||
|
logger.info(f"Modifiers: {keyboardEvent.modifiers}")
|
||||||
|
logger.info(f"Total keybindings to check: {len(self.keyBindings)}")
|
||||||
|
|
||||||
|
with open('/tmp/keybinding_lookup.log', 'a') as f:
|
||||||
|
f.write(f"=== Looking for 'v' key handler ===\n")
|
||||||
|
f.write(f"Event string: {event_str}\n")
|
||||||
|
f.write(f"Hardware code: {keyboardEvent.hw_code}\n")
|
||||||
|
f.write(f"Modifiers: {keyboardEvent.modifiers}\n")
|
||||||
|
f.write(f"Total keybindings: {len(self.keyBindings)}\n")
|
||||||
|
|
||||||
|
# Log all keybindings for comparison
|
||||||
|
for i, kb in enumerate(self.keyBindings):
|
||||||
|
if 'v' in kb.keysymstring.lower() or 'version' in kb.handler.description.lower():
|
||||||
|
logger.info(f"Binding {i}: keysym={kb.keysymstring}, modifiers={kb.modifiers}, mask={kb.modifier_mask}, desc={kb.handler.description}")
|
||||||
|
with open('/tmp/keybinding_lookup.log', 'a') as f:
|
||||||
|
f.write(f"Found V-related binding {i}: keysym={kb.keysymstring}, modifiers={kb.modifiers}, mask={kb.modifier_mask}, desc={kb.handler.description}\n")
|
||||||
|
|
||||||
matches = []
|
matches = []
|
||||||
candidates = []
|
candidates = []
|
||||||
clickCount = keyboardEvent.getClickCount()
|
clickCount = keyboardEvent.getClickCount()
|
||||||
for keyBinding in self.keyBindings:
|
for keyBinding in self.keyBindings:
|
||||||
if keyBinding.matches(keyboardEvent.hw_code, keyboardEvent.modifiers):
|
if keyBinding.matches(keyboardEvent.hw_code, keyboardEvent.modifiers):
|
||||||
if keyBinding.modifier_mask == keyboardEvent.modifiers and \
|
if event_str.lower() == 'v':
|
||||||
keyBinding.click_count == clickCount:
|
logger.info(f"MATCH found! keysym={keyBinding.keysymstring}, desc={keyBinding.handler.description}")
|
||||||
|
if (keyboardEvent.modifiers & keyBinding.modifier_mask) == keyBinding.modifiers and \
|
||||||
|
keyBinding.click_count == clickCount:
|
||||||
matches.append(keyBinding)
|
matches.append(keyBinding)
|
||||||
# If there's no keysymstring, it's unbound and cannot be
|
# If there's no keysymstring, it's unbound and cannot be
|
||||||
# a match.
|
# a match.
|
||||||
@@ -484,8 +529,17 @@ class KeyBindings:
|
|||||||
if keyBinding.keysymstring:
|
if keyBinding.keysymstring:
|
||||||
candidates.append(keyBinding)
|
candidates.append(keyBinding)
|
||||||
|
|
||||||
|
if event_str.lower() == 'v':
|
||||||
|
logger.info(f"Exact matches: {len(matches)}")
|
||||||
|
logger.info(f"Candidates: {len(candidates)}")
|
||||||
|
with open('/tmp/keybinding_lookup.log', 'a') as f:
|
||||||
|
f.write(f"Exact matches: {len(matches)}\n")
|
||||||
|
f.write(f"Candidates: {len(candidates)}\n")
|
||||||
|
|
||||||
self._checkMatchingBindings(keyboardEvent, matches)
|
self._checkMatchingBindings(keyboardEvent, matches)
|
||||||
if matches:
|
if matches:
|
||||||
|
if event_str.lower() == 'v':
|
||||||
|
logger.info(f"Returning exact match handler: {matches[0].handler.description}")
|
||||||
return matches[0].handler
|
return matches[0].handler
|
||||||
|
|
||||||
if keyboardEvent.isKeyPadKeyWithNumlockOn():
|
if keyboardEvent.isKeyPadKeyWithNumlockOn():
|
||||||
@@ -499,8 +553,12 @@ class KeyBindings:
|
|||||||
self._checkMatchingBindings(keyboardEvent, candidates)
|
self._checkMatchingBindings(keyboardEvent, candidates)
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
if candidate.click_count <= clickCount:
|
if candidate.click_count <= clickCount:
|
||||||
|
if event_str.lower() == 'v':
|
||||||
|
logger.info(f"Returning candidate handler: {candidate.handler.description}")
|
||||||
return candidate.handler
|
return candidate.handler
|
||||||
|
|
||||||
|
if event_str.lower() == 'v':
|
||||||
|
logger.info("No handler found!")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def load(self, keymap, handlers):
|
def load(self, keymap, handlers):
|
||||||
|
|||||||
+17
-2
@@ -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,190 @@ 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.connectSignal('load-setting-completed', self._on_settings_changed, None)
|
||||||
|
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 refresh_active_script_keybindings(self):
|
||||||
|
"""Force active script to refresh its keybindings to include plugin bindings."""
|
||||||
|
from . import cthulhu_state
|
||||||
|
if cthulhu_state.activeScript:
|
||||||
|
active_script = cthulhu_state.activeScript
|
||||||
|
with open('/tmp/plugin_registration.log', 'a') as f:
|
||||||
|
f.write(f"=== refresh_active_script_keybindings() CALLED ===\n")
|
||||||
|
f.write(f"Active script: {active_script.name}\n")
|
||||||
|
|
||||||
|
# Force the script to recreate its keybindings to include plugin bindings
|
||||||
|
old_keybindings = active_script.keyBindings
|
||||||
|
active_script.keyBindings = active_script.getKeyBindings()
|
||||||
|
|
||||||
|
with open('/tmp/plugin_registration.log', 'a') as f:
|
||||||
|
f.write(f"Keybindings refreshed: old={len(old_keybindings.keyBindings) if old_keybindings else 0}, new={len(active_script.keyBindings.keyBindings)}\n")
|
||||||
|
|
||||||
|
def register_plugin_keybindings_with_active_script(self):
|
||||||
|
"""Register all plugin keybindings with the active script."""
|
||||||
|
|
||||||
|
logger.info("=== register_plugin_keybindings_with_active_script() CALLED ===")
|
||||||
|
with open('/tmp/plugin_registration.log', 'a') as f:
|
||||||
|
f.write("=== register_plugin_keybindings_with_active_script() CALLED ===\n")
|
||||||
|
|
||||||
|
if not PLUGGY_AVAILABLE:
|
||||||
|
logger.warning("PLUGGY not available")
|
||||||
|
with open('/tmp/plugin_registration.log', 'a') as f:
|
||||||
|
f.write("ERROR: PLUGGY not available\n")
|
||||||
|
return
|
||||||
|
|
||||||
|
from . import cthulhu_state
|
||||||
|
if not cthulhu_state.activeScript:
|
||||||
|
logger.warning("No active script available to register plugin keybindings")
|
||||||
|
with open('/tmp/plugin_registration.log', 'a') as f:
|
||||||
|
f.write("ERROR: No active script available\n")
|
||||||
|
return
|
||||||
|
|
||||||
|
active_script = cthulhu_state.activeScript
|
||||||
|
logger.info(f"Registering plugin keybindings with active script: {active_script}")
|
||||||
|
with open('/tmp/plugin_registration.log', 'a') as f:
|
||||||
|
f.write(f"Active script: {active_script}\n")
|
||||||
|
|
||||||
|
# First, register keybindings from APIHelper's stored bindings
|
||||||
|
# This is where plugin keybindings actually get stored
|
||||||
|
from . import cthulhu
|
||||||
|
api_helper = cthulhu.cthulhuApp.getAPIHelper()
|
||||||
|
if api_helper and hasattr(api_helper, '_gestureBindings'):
|
||||||
|
logger.info("=== FOUND APIHelper with _gestureBindings ===")
|
||||||
|
with open('/tmp/plugin_registration.log', 'a') as f:
|
||||||
|
f.write("=== Registering stored gesture bindings from APIHelper ===\n")
|
||||||
|
f.write(f"Total contexts: {len(api_helper._gestureBindings)}\n")
|
||||||
|
|
||||||
|
for context_name, bindings_list in api_helper._gestureBindings.items():
|
||||||
|
logger.info(f"Processing context '{context_name}' with {len(bindings_list)} bindings")
|
||||||
|
with open('/tmp/plugin_registration.log', 'a') as f:
|
||||||
|
f.write(f"Context '{context_name}': {len(bindings_list)} bindings\n")
|
||||||
|
|
||||||
|
for binding in bindings_list:
|
||||||
|
logger.info(f"Adding stored binding: {binding.keysymstring} with modifiers {binding.modifiers}")
|
||||||
|
with open('/tmp/plugin_registration.log', 'a') as f:
|
||||||
|
f.write(f" Binding: {binding.keysymstring} modifiers={binding.modifiers} desc={binding.handler.description}\n")
|
||||||
|
|
||||||
|
# Check if binding already exists to avoid duplicates
|
||||||
|
if not active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"):
|
||||||
|
active_script.getKeyBindings().add(binding)
|
||||||
|
with open('/tmp/plugin_registration.log', 'a') as f:
|
||||||
|
f.write(f" ADDED to active script!\n")
|
||||||
|
|
||||||
|
# Force recalculation of keycode if it wasn't set when device was None
|
||||||
|
if not binding.keycode and binding.keysymstring:
|
||||||
|
from . import keybindings
|
||||||
|
binding.keycode = keybindings.getKeycode(binding.keysymstring)
|
||||||
|
# Register key grab at system level - this was missing!
|
||||||
|
grab_ids = cthulhu.addKeyGrab(binding)
|
||||||
|
if grab_ids:
|
||||||
|
binding._grab_ids = grab_ids
|
||||||
|
else:
|
||||||
|
logger.warning(f"Failed to create key grab for {binding.keysymstring} - device may not be available")
|
||||||
|
else:
|
||||||
|
logger.info(f"Binding already exists: {binding.keysymstring}")
|
||||||
|
with open('/tmp/plugin_registration.log', 'a') as f:
|
||||||
|
f.write(f" Already exists, skipped\n")
|
||||||
|
else:
|
||||||
|
logger.warning("=== NO APIHelper or no _gestureBindings found ===")
|
||||||
|
with open('/tmp/plugin_registration.log', 'a') as f:
|
||||||
|
f.write("ERROR: No APIHelper or no _gestureBindings found!\n")
|
||||||
|
if api_helper:
|
||||||
|
f.write(f"APIHelper exists but _gestureBindings: {hasattr(api_helper, '_gestureBindings')}\n")
|
||||||
|
else:
|
||||||
|
f.write("No APIHelper found!\n")
|
||||||
|
|
||||||
|
# Also check the old method for any plugins that use get_bindings()
|
||||||
|
for pluginInfo in self._plugins.values():
|
||||||
|
if not pluginInfo.loaded or not pluginInfo.instance:
|
||||||
|
continue
|
||||||
|
|
||||||
|
plugin = pluginInfo.instance
|
||||||
|
if not hasattr(plugin, 'get_bindings') or not plugin.get_bindings():
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f"Registering keybindings for plugin: {plugin.name}")
|
||||||
|
bindings = plugin.get_bindings()
|
||||||
|
for binding in bindings.keyBindings:
|
||||||
|
logger.info(f"Adding binding: {binding.keysymstring} with modifiers {binding.modifiers}")
|
||||||
|
# Check if binding already exists to avoid duplicates
|
||||||
|
if not active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"):
|
||||||
|
active_script.getKeyBindings().add(binding)
|
||||||
|
# Force recalculation of keycode if it wasn't set when device was None
|
||||||
|
if not binding.keycode and binding.keysymstring:
|
||||||
|
from . import keybindings
|
||||||
|
binding.keycode = keybindings.getKeycode(binding.keysymstring)
|
||||||
|
# Register key grab at system level - this was missing!
|
||||||
|
grab_ids = cthulhu.addKeyGrab(binding)
|
||||||
|
if grab_ids:
|
||||||
|
binding._grab_ids = grab_ids
|
||||||
|
else:
|
||||||
|
logger.warning(f"Failed to create key grab for {binding.keysymstring} - device may not be available")
|
||||||
|
|
||||||
|
def _on_settings_changed(self, app=None):
|
||||||
|
"""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)
|
||||||
|
from . import cthulhu
|
||||||
|
grab_ids = cthulhu.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)
|
||||||
@@ -218,26 +397,34 @@ 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"=== PluginSystemManager.setActivePlugins called ===")
|
||||||
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}")
|
||||||
|
|
||||||
|
# Check specifically for DisplayVersion
|
||||||
|
if 'DisplayVersion' in self._active_plugins:
|
||||||
|
logger.info("DisplayVersion is in active plugins list!")
|
||||||
|
else:
|
||||||
|
logger.warning("DisplayVersion is NOT in active plugins list!")
|
||||||
|
|
||||||
# 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()
|
||||||
|
logger.info("=== PluginSystemManager.setActivePlugins completed ===")
|
||||||
|
|
||||||
def setPluginActive(self, pluginInfo, active):
|
def setPluginActive(self, pluginInfo, active):
|
||||||
"""Set the active state of a plugin."""
|
"""Set the active state of a plugin."""
|
||||||
@@ -258,7 +445,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 +458,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 +498,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}")
|
||||||
@@ -326,7 +513,7 @@ class PluginSystemManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
module_name = pluginInfo.get_module_name()
|
module_name = pluginInfo.get_module_name()
|
||||||
logger.info(f"Attempting to load plugin: {module_name}")
|
logger.info(f"=== PluginSystemManager.loadPlugin starting for: {module_name} ===")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Already loaded?
|
# Already loaded?
|
||||||
@@ -337,27 +524,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 +572,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 +585,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 +600,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:
|
||||||
|
|||||||
@@ -23,3 +23,8 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
"""Clipboard plugin package."""
|
||||||
|
|
||||||
|
from .plugin import Clipboard
|
||||||
|
|
||||||
|
__all__ = ['Clipboard']
|
||||||
|
|||||||
@@ -72,42 +72,7 @@ class Clipboard(Plugin):
|
|||||||
return
|
return
|
||||||
|
|
||||||
logger.info("Deactivating Clipboard plugin")
|
logger.info("Deactivating Clipboard plugin")
|
||||||
try:
|
# Note: Currently no unregister method needed as keybindings are managed by APIHelper
|
||||||
# Unregister keyboard shortcut
|
|
||||||
if self.app:
|
|
||||||
api_helper = self.app.getAPIHelper()
|
|
||||||
if api_helper and hasattr(api_helper, 'unregisterShortcut'):
|
|
||||||
api_helper.unregisterShortcut('kb:cthulhu+shift+c')
|
|
||||||
logger.debug("Unregistered clipboard shortcut")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error deactivating Clipboard plugin: {e}")
|
|
||||||
"""Activate the plugin."""
|
|
||||||
# Skip if this activation call isn't for us
|
|
||||||
if plugin is not None and plugin is not self:
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info("Activating Clipboard plugin")
|
|
||||||
try:
|
|
||||||
# Register keyboard shortcut
|
|
||||||
self.registerGestureByString(self.speakClipboard, _('clipboard'), 'kb:cthulhu+shift+c')
|
|
||||||
logger.debug("Registered shortcut for clipboard")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error activating Clipboard plugin: {e}")
|
|
||||||
|
|
||||||
@cthulhu_hookimpl
|
|
||||||
def deactivate(self, plugin=None):
|
|
||||||
"""Deactivate the plugin."""
|
|
||||||
# Skip if this deactivation call isn't for us
|
|
||||||
if plugin is not None and plugin is not self:
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info("Deactivating Clipboard plugin")
|
|
||||||
try:
|
|
||||||
# Unregister keyboard shortcut
|
|
||||||
self.unregisterGestureByString('kb:cthulhu+shift+c')
|
|
||||||
logger.debug("Unregistered clipboard shortcut")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error deactivating Clipboard plugin: {e}")
|
|
||||||
|
|
||||||
def speakClipboard(self, script=None, inputEvent=None):
|
def speakClipboard(self, script=None, inputEvent=None):
|
||||||
"""Present the contents of the clipboard."""
|
"""Present the contents of the clipboard."""
|
||||||
|
|||||||
@@ -32,16 +32,62 @@ class DisplayVersion(Plugin):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info("Activating DisplayVersion plugin")
|
logger.info("=== DisplayVersion plugin activation starting ===")
|
||||||
|
logger.info(f"Plugin name: {self.name}")
|
||||||
|
logger.info(f"App reference: {self.app}")
|
||||||
|
|
||||||
|
# Also write to a debug file
|
||||||
|
with open('/tmp/displayversion_debug.log', 'a') as f:
|
||||||
|
f.write("=== DisplayVersion plugin activation starting ===\n")
|
||||||
|
f.write(f"Plugin name: {self.name}\n")
|
||||||
|
f.write(f"App reference: {self.app}\n")
|
||||||
|
|
||||||
|
# Check if we have access to the API helper
|
||||||
|
if not self.app:
|
||||||
|
logger.error("DisplayVersion: No app reference available!")
|
||||||
|
return
|
||||||
|
|
||||||
|
api_helper = self.app.getAPIHelper()
|
||||||
|
if not api_helper:
|
||||||
|
logger.error("DisplayVersion: No API helper available!")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"API helper: {api_helper}")
|
||||||
|
|
||||||
# Register keyboard shortcut
|
# Register keyboard shortcut
|
||||||
|
gesture_string = 'kb:cthulhu+shift+v'
|
||||||
|
logger.info(f"DisplayVersion: Attempting to register gesture: {gesture_string}")
|
||||||
|
|
||||||
self._kb_binding = self.registerGestureByString(
|
self._kb_binding = self.registerGestureByString(
|
||||||
self.speakText,
|
self.speakText,
|
||||||
f'Cthulhu screen reader version {cthulhuVersion.version}-{cthulhuVersion.codeName}',
|
f'Cthulhu screen reader version {cthulhuVersion.version}-{cthulhuVersion.codeName}',
|
||||||
'kb:cthulhu+shift+v'
|
gesture_string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(f"DisplayVersion: Registered keybinding result: {self._kb_binding}")
|
||||||
|
if self._kb_binding:
|
||||||
|
logger.info(f"Binding keysymstring: {self._kb_binding.keysymstring}")
|
||||||
|
logger.info(f"Binding modifiers: {self._kb_binding.modifiers}")
|
||||||
|
logger.info(f"Binding modifier_mask: {self._kb_binding.modifier_mask}")
|
||||||
|
logger.info(f"Binding handler: {self._kb_binding.handler}")
|
||||||
|
|
||||||
|
with open('/tmp/displayversion_debug.log', 'a') as f:
|
||||||
|
f.write(f"SUCCESS: Keybinding created!\n")
|
||||||
|
f.write(f" keysymstring: {self._kb_binding.keysymstring}\n")
|
||||||
|
f.write(f" modifiers: {self._kb_binding.modifiers}\n")
|
||||||
|
f.write(f" modifier_mask: {self._kb_binding.modifier_mask}\n")
|
||||||
|
else:
|
||||||
|
logger.error("DisplayVersion: Failed to create keybinding!")
|
||||||
|
with open('/tmp/displayversion_debug.log', 'a') as f:
|
||||||
|
f.write("ERROR: Failed to create keybinding!\n")
|
||||||
|
|
||||||
|
logger.info("=== DisplayVersion plugin activation completed ===")
|
||||||
|
with open('/tmp/displayversion_debug.log', 'a') as f:
|
||||||
|
f.write("=== DisplayVersion plugin activation completed ===\n\n")
|
||||||
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 +101,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 +112,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
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
indentationaudio_PYTHON = \
|
||||||
|
__init__.py \
|
||||||
|
plugin.py
|
||||||
|
|
||||||
|
indentationaudiodir = $(pkgdatadir)/cthulhu/plugins/IndentationAudio
|
||||||
|
|
||||||
|
indentationaudio_DATA = \
|
||||||
|
plugin.info
|
||||||
|
|
||||||
|
EXTRA_DIST = $(indentationaudio_DATA)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright (c) 2025 Stormux
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
"""IndentationAudio plugin package."""
|
||||||
|
|
||||||
|
from .plugin import IndentationAudio
|
||||||
|
|
||||||
|
__all__ = ['IndentationAudio']
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Core]
|
||||||
|
Name = IndentationAudio
|
||||||
|
Module = IndentationAudio
|
||||||
|
|
||||||
|
[Documentation]
|
||||||
|
Description = Provides audio feedback for indentation level changes when navigating code or text
|
||||||
|
Author = Stormux
|
||||||
|
Version = 1.0.0
|
||||||
|
Website = https://git.stormux.org/storm/cthulhu
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright (c) 2025 Stormux
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
"""IndentationAudio plugin for Cthulhu - Provides audio feedback for indentation level changes."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
from cthulhu.plugin import Plugin, cthulhu_hookimpl
|
||||||
|
from cthulhu import cmdnames
|
||||||
|
from cthulhu import debug
|
||||||
|
from cthulhu import input_event
|
||||||
|
from cthulhu import keybindings
|
||||||
|
from cthulhu import messages
|
||||||
|
from cthulhu import script_utilities
|
||||||
|
from cthulhu import settings_manager
|
||||||
|
from cthulhu import sound
|
||||||
|
from cthulhu.sound_generator import Tone
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
_settingsManager = settings_manager.getManager()
|
||||||
|
|
||||||
|
|
||||||
|
class IndentationAudio(Plugin):
|
||||||
|
"""Plugin that provides audio cues for indentation level changes."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initialize the IndentationAudio plugin."""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._enabled = True # Start enabled by default
|
||||||
|
self._last_indentation_level = {} # Track per-object indentation
|
||||||
|
self._event_listener_id = None
|
||||||
|
self._kb_binding = None
|
||||||
|
self._player = None
|
||||||
|
|
||||||
|
# Audio settings
|
||||||
|
self._base_frequency = 200 # Base frequency in Hz
|
||||||
|
self._frequency_step = 80 # Hz per indentation level
|
||||||
|
self._tone_duration = 0.15 # Seconds
|
||||||
|
self._max_frequency = 1200 # Cap frequency to avoid harsh sounds
|
||||||
|
|
||||||
|
logger.info("IndentationAudio plugin initialized")
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def activate(self, plugin=None):
|
||||||
|
"""Activate the IndentationAudio plugin."""
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info("=== IndentationAudio plugin activation starting ===")
|
||||||
|
|
||||||
|
# Initialize sound player
|
||||||
|
self._player = sound.getPlayer()
|
||||||
|
|
||||||
|
# Register keybinding for toggle (Cthulhu+I)
|
||||||
|
self._register_keybinding()
|
||||||
|
|
||||||
|
# Connect to text caret movement events
|
||||||
|
self._connect_to_events()
|
||||||
|
|
||||||
|
logger.info("IndentationAudio plugin activated successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to activate IndentationAudio plugin: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def deactivate(self, plugin=None):
|
||||||
|
"""Deactivate the IndentationAudio plugin."""
|
||||||
|
if plugin is not None and plugin is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info("=== IndentationAudio plugin deactivation starting ===")
|
||||||
|
|
||||||
|
# Disconnect from events
|
||||||
|
self._disconnect_from_events()
|
||||||
|
|
||||||
|
# Clear tracking data
|
||||||
|
self._last_indentation_level.clear()
|
||||||
|
|
||||||
|
logger.info("IndentationAudio plugin deactivated successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to deactivate IndentationAudio plugin: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _register_keybinding(self):
|
||||||
|
"""Register the Cthulhu+I keybinding for toggling the plugin."""
|
||||||
|
try:
|
||||||
|
if not self.app:
|
||||||
|
logger.error("IndentationAudio: No app reference available for keybinding")
|
||||||
|
return
|
||||||
|
|
||||||
|
api_helper = self.app.getAPIHelper()
|
||||||
|
if not api_helper:
|
||||||
|
logger.error("IndentationAudio: No API helper available")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Register Cthulhu+I keybinding
|
||||||
|
gesture_string = "Cthulhu+i"
|
||||||
|
description = "Toggle indentation audio feedback"
|
||||||
|
handler = self._toggle_indentation_audio
|
||||||
|
|
||||||
|
self._kb_binding = api_helper.registerGestureByString(
|
||||||
|
gesture_string, handler, description
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._kb_binding:
|
||||||
|
logger.info(f"IndentationAudio: Registered keybinding {gesture_string}")
|
||||||
|
else:
|
||||||
|
logger.error(f"IndentationAudio: Failed to register keybinding {gesture_string}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"IndentationAudio: Error registering keybinding: {e}")
|
||||||
|
|
||||||
|
def _connect_to_events(self):
|
||||||
|
"""Connect to text navigation events."""
|
||||||
|
try:
|
||||||
|
# Hook into the dynamic API to make ourselves available to scripts
|
||||||
|
if self.app:
|
||||||
|
api_manager = self.app.getDynamicApiManager()
|
||||||
|
api_manager.registerAPI('IndentationAudioPlugin', self)
|
||||||
|
logger.info("IndentationAudio: Registered with dynamic API manager")
|
||||||
|
|
||||||
|
# We'll also monkey-patch the default script's sayLine method
|
||||||
|
self._monkey_patch_script_methods()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"IndentationAudio: Error connecting to events: {e}")
|
||||||
|
|
||||||
|
def _disconnect_from_events(self):
|
||||||
|
"""Disconnect from text navigation events."""
|
||||||
|
try:
|
||||||
|
# Unregister from dynamic API
|
||||||
|
if self.app:
|
||||||
|
api_manager = self.app.getDynamicApiManager()
|
||||||
|
api_manager.unregisterAPI('IndentationAudioPlugin')
|
||||||
|
|
||||||
|
# Restore original script methods
|
||||||
|
self._restore_script_methods()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"IndentationAudio: Error disconnecting from events: {e}")
|
||||||
|
|
||||||
|
def _monkey_patch_script_methods(self):
|
||||||
|
"""Monkey-patch the default script's line navigation methods."""
|
||||||
|
try:
|
||||||
|
# Get the current active script
|
||||||
|
if self.app:
|
||||||
|
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
|
||||||
|
if state and hasattr(state, 'activeScript') and state.activeScript:
|
||||||
|
script = state.activeScript
|
||||||
|
|
||||||
|
# Store original method
|
||||||
|
if hasattr(script, 'sayLine'):
|
||||||
|
self._original_sayLine = script.sayLine
|
||||||
|
|
||||||
|
# Create wrapped version
|
||||||
|
def wrapped_sayLine(obj):
|
||||||
|
# Call original method first
|
||||||
|
result = self._original_sayLine(obj)
|
||||||
|
|
||||||
|
# Add our indentation audio check
|
||||||
|
try:
|
||||||
|
line, caretOffset, startOffset = script.getTextLineAtCaret(obj)
|
||||||
|
self.check_indentation_change(obj, line)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"IndentationAudio: Error in wrapped_sayLine: {e}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Replace the method
|
||||||
|
script.sayLine = wrapped_sayLine
|
||||||
|
logger.info("IndentationAudio: Successfully monkey-patched sayLine method")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"IndentationAudio: Error monkey-patching script methods: {e}")
|
||||||
|
|
||||||
|
def _restore_script_methods(self):
|
||||||
|
"""Restore original script methods."""
|
||||||
|
try:
|
||||||
|
if self.app and hasattr(self, '_original_sayLine'):
|
||||||
|
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
|
||||||
|
if state and hasattr(state, 'activeScript') and state.activeScript:
|
||||||
|
script = state.activeScript
|
||||||
|
if hasattr(script, 'sayLine'):
|
||||||
|
script.sayLine = self._original_sayLine
|
||||||
|
logger.info("IndentationAudio: Restored original sayLine method")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"IndentationAudio: Error restoring script methods: {e}")
|
||||||
|
|
||||||
|
def _toggle_indentation_audio(self, script, inputEvent=None):
|
||||||
|
"""Toggle the indentation audio feedback on/off."""
|
||||||
|
try:
|
||||||
|
self._enabled = not self._enabled
|
||||||
|
state = "enabled" if self._enabled else "disabled"
|
||||||
|
|
||||||
|
# Announce the state change
|
||||||
|
message = f"Indentation audio {state}"
|
||||||
|
if hasattr(script, 'speakMessage'):
|
||||||
|
script.speakMessage(message)
|
||||||
|
|
||||||
|
logger.info(f"IndentationAudio: Toggled to {state}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"IndentationAudio: Error toggling state: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _calculate_indentation_level(self, line_text):
|
||||||
|
"""Calculate the indentation level of a line."""
|
||||||
|
if not line_text:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Remove non-breaking spaces and convert to regular spaces
|
||||||
|
line = line_text.replace("\u00a0", " ")
|
||||||
|
|
||||||
|
# Find the first non-whitespace character
|
||||||
|
match = re.search(r"[^ \t]", line)
|
||||||
|
if not match:
|
||||||
|
return 0 # Empty or whitespace-only line
|
||||||
|
|
||||||
|
indent_text = line[:match.start()]
|
||||||
|
|
||||||
|
# Calculate indentation level (4 spaces = 1 level, 1 tab = 1 level)
|
||||||
|
level = 0
|
||||||
|
for char in indent_text:
|
||||||
|
if char == '\t':
|
||||||
|
level += 1
|
||||||
|
elif char == ' ':
|
||||||
|
level += 0.25 # 4 spaces = 1 level
|
||||||
|
|
||||||
|
return int(level)
|
||||||
|
|
||||||
|
def _generate_indentation_tone(self, new_level, old_level):
|
||||||
|
"""Generate an audio tone for indentation level change."""
|
||||||
|
if not self._enabled or not self._player:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calculate frequency based on new indentation level
|
||||||
|
frequency = min(
|
||||||
|
self._base_frequency + (new_level * self._frequency_step),
|
||||||
|
self._max_frequency
|
||||||
|
)
|
||||||
|
|
||||||
|
# Determine stereo panning based on change direction
|
||||||
|
# Left channel for indent increase, right for decrease
|
||||||
|
volume_multiplier = 0.7
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create tone
|
||||||
|
tone = Tone(
|
||||||
|
duration=self._tone_duration,
|
||||||
|
frequency=frequency,
|
||||||
|
volumeMultiplier=volume_multiplier,
|
||||||
|
wave=Tone.SINE_WAVE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Play the tone
|
||||||
|
self._player.play(tone, interrupt=False)
|
||||||
|
|
||||||
|
debug_msg = f"IndentationAudio: Played tone - Level: {new_level}, Freq: {frequency}Hz"
|
||||||
|
logger.debug(debug_msg)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"IndentationAudio: Error generating tone: {e}")
|
||||||
|
|
||||||
|
def check_indentation_change(self, obj, line_text):
|
||||||
|
"""Check if indentation has changed and play audio cue if needed.
|
||||||
|
|
||||||
|
This method is intended to be called by scripts during line navigation.
|
||||||
|
"""
|
||||||
|
if not self._enabled or not line_text:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get object identifier for tracking
|
||||||
|
obj_id = str(obj) if obj else "unknown"
|
||||||
|
|
||||||
|
# Calculate current indentation level
|
||||||
|
current_level = self._calculate_indentation_level(line_text)
|
||||||
|
|
||||||
|
# Get previous level for this object
|
||||||
|
previous_level = self._last_indentation_level.get(obj_id, current_level)
|
||||||
|
|
||||||
|
# Update tracking
|
||||||
|
self._last_indentation_level[obj_id] = current_level
|
||||||
|
|
||||||
|
# Play audio cue if indentation changed
|
||||||
|
if current_level != previous_level:
|
||||||
|
self._generate_indentation_tone(current_level, previous_level)
|
||||||
|
|
||||||
|
debug_msg = f"IndentationAudio: Indentation changed from {previous_level} to {current_level}"
|
||||||
|
logger.debug(debug_msg)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"IndentationAudio: Error checking indentation change: {e}")
|
||||||
|
|
||||||
|
def is_enabled(self):
|
||||||
|
"""Return whether the plugin is currently enabled."""
|
||||||
|
return self._enabled
|
||||||
|
|
||||||
|
def set_enabled(self, enabled):
|
||||||
|
"""Set the enabled state of the plugin."""
|
||||||
|
self._enabled = enabled
|
||||||
|
|
||||||
|
def on_script_change(self, new_script):
|
||||||
|
"""Handle when the active script changes."""
|
||||||
|
try:
|
||||||
|
# Restore previous script if it was patched
|
||||||
|
self._restore_script_methods()
|
||||||
|
|
||||||
|
# Re-apply patches to new script
|
||||||
|
self._monkey_patch_script_methods()
|
||||||
|
|
||||||
|
# Clear tracking data for new context
|
||||||
|
self._last_indentation_level.clear()
|
||||||
|
|
||||||
|
logger.info("IndentationAudio: Handled script change")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"IndentationAudio: Error handling script change: {e}")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
SUBDIRS = Clipboard DisplayVersion hello_world self_voice ByeCthulhu HelloCthulhu SimplePluginSystem
|
SUBDIRS = Clipboard DisplayVersion IndentationAudio hello_world self_voice ByeCthulhu HelloCthulhu SimplePluginSystem
|
||||||
|
|
||||||
cthulhu_pythondir=$(pkgpythondir)/plugins
|
cthulhu_pythondir=$(pkgpythondir)/plugins
|
||||||
|
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|
||||||
|
|||||||
@@ -318,6 +318,13 @@ class ScriptManager:
|
|||||||
return
|
return
|
||||||
|
|
||||||
newScript.activate()
|
newScript.activate()
|
||||||
|
|
||||||
|
# Register plugin keybindings with the new active script
|
||||||
|
from . import cthulhu
|
||||||
|
plugin_manager = cthulhu.cthulhuApp.getPluginSystemManager()
|
||||||
|
if plugin_manager:
|
||||||
|
pass # plugin_manager.register_plugin_keybindings_with_active_script()
|
||||||
|
|
||||||
tokens = ["SCRIPT MANAGER: Setting active script to", newScript, "reason:", reason]
|
tokens = ["SCRIPT MANAGER: Setting active script to", newScript, "reason:", reason]
|
||||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
|
||||||
|
|||||||
@@ -363,6 +363,8 @@ class Script(script.Script):
|
|||||||
return keyBindings
|
return keyBindings
|
||||||
|
|
||||||
def getExtensionBindings(self):
|
def getExtensionBindings(self):
|
||||||
|
with open('/tmp/extension_bindings_debug.log', 'a') as f:
|
||||||
|
f.write(f"=== getExtensionBindings() called ===\n")
|
||||||
keyBindings = keybindings.KeyBindings()
|
keyBindings = keybindings.KeyBindings()
|
||||||
|
|
||||||
bindings = self.notificationPresenter.get_bindings()
|
bindings = self.notificationPresenter.get_bindings()
|
||||||
@@ -407,6 +409,41 @@ class Script(script.Script):
|
|||||||
for keyBinding in bindings.keyBindings:
|
for keyBinding in bindings.keyBindings:
|
||||||
keyBindings.add(keyBinding)
|
keyBindings.add(keyBinding)
|
||||||
|
|
||||||
|
# Add plugin keybindings from APIHelper storage
|
||||||
|
try:
|
||||||
|
import cthulhu.cthulhu as cthulhu
|
||||||
|
if hasattr(cthulhu, 'cthulhuApp') and cthulhu.cthulhuApp:
|
||||||
|
api_helper = cthulhu.cthulhuApp.getAPIHelper()
|
||||||
|
with open('/tmp/extension_bindings_debug.log', 'a') as f:
|
||||||
|
f.write(f"=== Checking for plugin bindings ===\n")
|
||||||
|
f.write(f"api_helper exists: {api_helper is not None}\n")
|
||||||
|
if api_helper:
|
||||||
|
f.write(f"api_helper has _gestureBindings: {hasattr(api_helper, '_gestureBindings')}\n")
|
||||||
|
if hasattr(api_helper, '_gestureBindings'):
|
||||||
|
f.write(f"_gestureBindings content: {api_helper._gestureBindings}\n")
|
||||||
|
f.write(f"Available contexts: {list(api_helper._gestureBindings.keys())}\n")
|
||||||
|
|
||||||
|
if api_helper and hasattr(api_helper, '_gestureBindings'):
|
||||||
|
with open('/tmp/extension_bindings_debug.log', 'a') as f:
|
||||||
|
f.write(f"=== Adding plugin bindings in getExtensionBindings() ===\n")
|
||||||
|
|
||||||
|
for context_name, context_bindings in api_helper._gestureBindings.items():
|
||||||
|
for binding in context_bindings:
|
||||||
|
keyBindings.add(binding)
|
||||||
|
with open('/tmp/extension_bindings_debug.log', 'a') as f:
|
||||||
|
f.write(f"Added plugin binding: {binding.keysymstring} modifiers={binding.modifiers} desc={binding.handler.description}\n")
|
||||||
|
else:
|
||||||
|
with open('/tmp/extension_bindings_debug.log', 'a') as f:
|
||||||
|
f.write(f"=== No plugin bindings available ===\n")
|
||||||
|
else:
|
||||||
|
with open('/tmp/extension_bindings_debug.log', 'a') as f:
|
||||||
|
f.write(f"=== cthulhuApp not available ===\n")
|
||||||
|
except Exception as e:
|
||||||
|
import cthulhu.debug as debug
|
||||||
|
debug.printMessage(debug.LEVEL_WARNING, f"Failed to add plugin bindings: {e}", True)
|
||||||
|
with open('/tmp/extension_bindings_debug.log', 'a') as f:
|
||||||
|
f.write(f"Exception in plugin binding addition: {e}\n")
|
||||||
|
|
||||||
return keyBindings
|
return keyBindings
|
||||||
|
|
||||||
def getKeyBindings(self):
|
def getKeyBindings(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user