Compare commits
54 Commits
2025.04.03
...
5d48f4770c
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d48f4770c | |||
| 81cc4627f7 | |||
| 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 | |||
| d36b664319 | |||
| 02be96aa69 | |||
| 48575ab6cd | |||
| 2c28021ed4 | |||
| 8a79725df8 | |||
| 1b4c4916e3 | |||
| 35a83327ac | |||
| c712bea421 | |||
| 815d39fc3f | |||
| 231d74efa0 | |||
| 7876a18c12 | |||
| 0b7cf681c3 | |||
| 4b8ebcb599 | |||
| d6a373c726 | |||
| dfe20fca30 |
@@ -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.
|
||||
|
||||
@@ -127,15 +127,10 @@ src/cthulhu/scripts/toolkits/gtk/Makefile
|
||||
src/cthulhu/plugins/Makefile
|
||||
src/cthulhu/plugins/ByeCthulhu/Makefile
|
||||
src/cthulhu/plugins/HelloCthulhu/Makefile
|
||||
src/cthulhu/plugins/PluginManager/Makefile
|
||||
src/cthulhu/plugins/Clipboard/Makefile
|
||||
src/cthulhu/plugins/DisplayVersion/Makefile
|
||||
src/cthulhu/plugins/hello_world/Makefile
|
||||
src/cthulhu/plugins/CapsLockHack/Makefile
|
||||
src/cthulhu/plugins/self_voice/Makefile
|
||||
src/cthulhu/plugins/Date/Makefile
|
||||
src/cthulhu/plugins/Time/Makefile
|
||||
src/cthulhu/plugins/MouseReview/Makefile
|
||||
src/cthulhu/plugins/SimplePluginSystem/Makefile
|
||||
src/cthulhu/backends/Makefile
|
||||
src/cthulhu/cthulhu_bin.py
|
||||
|
||||
+97
-41
@@ -37,77 +37,118 @@ import faulthandler
|
||||
|
||||
class APIHelper:
|
||||
"""Helper class for plugin API interactions, including keybindings."""
|
||||
|
||||
|
||||
def __init__(self, app):
|
||||
"""Initialize the APIHelper.
|
||||
|
||||
|
||||
Arguments:
|
||||
- app: the Cthulhu application
|
||||
"""
|
||||
self.app = app
|
||||
self._gestureBindings = {}
|
||||
|
||||
def registerGestureByString(self, function, name, gestureString, inputEventType='default', normalizer='cthulhu', learnModeEnabled=True, contextName=None):
|
||||
"""Register a gesture by string."""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def registerGestureByString(self, function, name, gestureString,
|
||||
inputEventType='default', normalizer='cthulhu',
|
||||
learnModeEnabled=True, contextName=None):
|
||||
"""Register a gesture by string.
|
||||
logger.info(f"=== APIHelper.registerGestureByString called ===")
|
||||
logger.info(f"gestureString: {gestureString}")
|
||||
logger.info(f"name: {name}")
|
||||
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:"):
|
||||
logger.warning(f"Gesture string doesn't start with 'kb:': {gestureString}")
|
||||
return None
|
||||
|
||||
|
||||
# Extract the key portion from the gesture string
|
||||
key = gestureString.split(":", 1)[1]
|
||||
|
||||
logger.info(f"Extracted key: {key}")
|
||||
|
||||
# Handle Cthulhu modifier specially
|
||||
if "cthulhu+" in key.lower():
|
||||
from . import keybindings
|
||||
key = key.lower().replace("cthulhu+", "")
|
||||
|
||||
key_parts = key.lower().split("+")
|
||||
logger.info(f"Key parts: {key_parts}")
|
||||
|
||||
# Start with the base Cthulhu modifier
|
||||
modifiers = keybindings.CTHULHU_MODIFIER_MASK
|
||||
|
||||
# Extract the final key (without modifiers)
|
||||
final_key = key_parts[-1]
|
||||
logger.info(f"Final key: {final_key}")
|
||||
|
||||
# Check for additional modifiers and combine them properly
|
||||
if "shift" in key_parts:
|
||||
# Use the pre-defined combined mask rather than trying to OR them
|
||||
modifiers = keybindings.CTHULHU_SHIFT_MODIFIER_MASK
|
||||
logger.info(f"Using CTHULHU_SHIFT_MODIFIER_MASK: {modifiers}")
|
||||
else:
|
||||
logger.info(f"Using CTHULHU_MODIFIER_MASK: {modifiers}")
|
||||
|
||||
# Create a keybinding handler
|
||||
class GestureHandler:
|
||||
def __init__(self, function, description):
|
||||
self.function = function
|
||||
self.description = description
|
||||
|
||||
|
||||
def __call__(self, script, inputEvent):
|
||||
return self.function(script, inputEvent)
|
||||
|
||||
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)
|
||||
|
||||
# Register the binding with the active script
|
||||
logger.info(f"Created handler: {handler}")
|
||||
|
||||
# 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 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:
|
||||
logger.info(f"Adding binding to active script: {cthulhu_state.activeScript}")
|
||||
bindings = cthulhu_state.activeScript.getKeyBindings()
|
||||
binding = keybindings.KeyBinding(
|
||||
key,
|
||||
keybindings.defaultModifierMask,
|
||||
keybindings.CTHULHU_MODIFIER_MASK,
|
||||
handler)
|
||||
bindings.add(binding)
|
||||
|
||||
# Store binding for later reference
|
||||
if contextName not in self._gestureBindings:
|
||||
self._gestureBindings[contextName] = []
|
||||
self._gestureBindings[contextName].append(binding)
|
||||
# Register key grab at the system level
|
||||
grab_ids = self.app.addKeyGrab(binding)
|
||||
logger.info(f"Key grab IDs: {grab_ids}")
|
||||
|
||||
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
|
||||
|
||||
|
||||
def unregisterShortcut(self, binding, contextName=None):
|
||||
"""Unregister a previously registered shortcut.
|
||||
|
||||
|
||||
Arguments:
|
||||
- binding: the binding to unregister
|
||||
- contextName: the context for this gesture
|
||||
@@ -117,11 +158,18 @@ class APIHelper:
|
||||
if cthulhu_state.activeScript:
|
||||
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
|
||||
@@ -615,6 +663,12 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False):
|
||||
|
||||
_scriptManager.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')
|
||||
|
||||
@@ -1018,6 +1072,8 @@ class Cthulhu(GObject.Object):
|
||||
self.APIHelper = APIHelper(self)
|
||||
self.createCompatAPI()
|
||||
self.pluginSystemManager = plugin_system_manager.PluginSystemManager(self)
|
||||
# Scan for available plugins at startup
|
||||
self.pluginSystemManager.rescanPlugins()
|
||||
def getAPIHelper(self):
|
||||
return self.APIHelper
|
||||
def getPluginSystemManager(self):
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
# Fork of Orca Screen Reader (GNOME)
|
||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
||||
|
||||
version = "2025.04.03"
|
||||
codeName = "testing"
|
||||
version = "2025.06.06"
|
||||
codeName = "master"
|
||||
|
||||
@@ -298,7 +298,7 @@ def println(level, text="", timestamp=False, stack=False):
|
||||
text = text.replace("\ufffc", "[OBJ]")
|
||||
if timestamp:
|
||||
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:
|
||||
text += f" {_stackAsString()}"
|
||||
|
||||
|
||||
@@ -112,6 +112,13 @@ class EventManager:
|
||||
cthulhu_state.device.key_watcher = cthulhu_state.device.add_key_watcher(
|
||||
self._processNewKeyboardEvent)
|
||||
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):
|
||||
if not self.legacyKeyHandlingActive:
|
||||
|
||||
@@ -251,6 +251,23 @@ class KeyBinding:
|
||||
if not self.keycode:
|
||||
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:
|
||||
result = modifiers & self.modifier_mask
|
||||
return result == self.modifiers
|
||||
@@ -470,13 +487,41 @@ class KeyBindings:
|
||||
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 = []
|
||||
candidates = []
|
||||
clickCount = keyboardEvent.getClickCount()
|
||||
for keyBinding in self.keyBindings:
|
||||
if keyBinding.matches(keyboardEvent.hw_code, keyboardEvent.modifiers):
|
||||
if keyBinding.modifier_mask == keyboardEvent.modifiers and \
|
||||
keyBinding.click_count == clickCount:
|
||||
if event_str.lower() == 'v':
|
||||
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)
|
||||
# If there's no keysymstring, it's unbound and cannot be
|
||||
# a match.
|
||||
@@ -484,8 +529,17 @@ class KeyBindings:
|
||||
if keyBinding.keysymstring:
|
||||
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)
|
||||
if matches:
|
||||
if event_str.lower() == 'v':
|
||||
logger.info(f"Returning exact match handler: {matches[0].handler.description}")
|
||||
return matches[0].handler
|
||||
|
||||
if keyboardEvent.isKeyPadKeyWithNumlockOn():
|
||||
@@ -499,8 +553,12 @@ class KeyBindings:
|
||||
self._checkMatchingBindings(keyboardEvent, candidates)
|
||||
for candidate in candidates:
|
||||
if candidate.click_count <= clickCount:
|
||||
if event_str.lower() == 'v':
|
||||
logger.info(f"Returning candidate handler: {candidate.handler.description}")
|
||||
return candidate.handler
|
||||
|
||||
if event_str.lower() == 'v':
|
||||
logger.info("No handler found!")
|
||||
return None
|
||||
|
||||
def load(self, keymap, handlers):
|
||||
|
||||
+21
-3
@@ -16,11 +16,12 @@ try:
|
||||
import pluggy
|
||||
cthulhu_hookimpl = pluggy.HookimplMarker("cthulhu")
|
||||
PLUGGY_AVAILABLE = True
|
||||
logging.getLogger(__name__).info("Successfully imported pluggy")
|
||||
except ImportError:
|
||||
# Fallback if pluggy is not available
|
||||
def cthulhu_hookimpl(func=None, **kwargs):
|
||||
"""Fallback decorator when pluggy is not available.
|
||||
|
||||
|
||||
This is a no-op decorator that returns the original function.
|
||||
It allows the code to continue working without pluggy, though
|
||||
plugins will be disabled.
|
||||
@@ -29,7 +30,9 @@ except ImportError:
|
||||
return lambda f: f
|
||||
return func
|
||||
PLUGGY_AVAILABLE = False
|
||||
logging.getLogger(__name__).info("Pluggy not available, plugins will be disabled")
|
||||
logging.getLogger(__name__).warning("Pluggy not available, plugins will be disabled")
|
||||
import traceback
|
||||
logging.getLogger(__name__).debug(traceback.format_exc())
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,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."""
|
||||
@@ -72,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,
|
||||
@@ -86,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
|
||||
|
||||
@@ -22,7 +22,12 @@ except ImportError:
|
||||
PLUGGY_AVAILABLE = False
|
||||
logging.getLogger(__name__).info("Pluggy not available, plugins will be disabled")
|
||||
|
||||
# Set to True for more detailed plugin loading debug info
|
||||
PLUGIN_DEBUG = True
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
if PLUGIN_DEBUG:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
class PluginType(IntEnum):
|
||||
"""Types of plugins we support."""
|
||||
@@ -74,9 +79,11 @@ class PluginSystemManager:
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
logger.info("Initializing PluginSystemManager")
|
||||
|
||||
# Initialize plugin manager
|
||||
if PLUGGY_AVAILABLE:
|
||||
logger.info("Pluggy is available, setting up plugin manager")
|
||||
self.plugin_manager = pluggy.PluginManager("cthulhu")
|
||||
|
||||
# Define hook specifications
|
||||
@@ -93,8 +100,10 @@ class PluginSystemManager:
|
||||
"""Called when the plugin is deactivated."""
|
||||
pass
|
||||
|
||||
logger.info("Adding hook specifications to plugin manager")
|
||||
self.plugin_manager.add_hookspecs(CthulhuHookSpecs)
|
||||
else:
|
||||
logger.warning("Pluggy is not available, plugins will be disabled")
|
||||
self.plugin_manager = None
|
||||
|
||||
# Plugin storage
|
||||
@@ -104,6 +113,189 @@ class PluginSystemManager:
|
||||
# Create plugin directories
|
||||
self._setup_plugin_dirs()
|
||||
|
||||
# Log available plugins directory paths
|
||||
logger.info(f"System plugins directory: {PluginType.SYSTEM.get_root_dir()}")
|
||||
logger.info(f"User plugins directory: {PluginType.USER.get_root_dir()}")
|
||||
|
||||
def register_plugin_global_keybindings(self, plugin):
|
||||
"""Register a plugin's keybindings with all scripts."""
|
||||
if not hasattr(plugin, 'get_bindings'):
|
||||
return
|
||||
|
||||
try:
|
||||
bindings = plugin.get_bindings()
|
||||
if not bindings or not bindings.keyBindings:
|
||||
return
|
||||
|
||||
logger.info(f"Registering global keybindings for plugin: {plugin.name}")
|
||||
|
||||
# First register with the active script
|
||||
from . import cthulhu_state
|
||||
if cthulhu_state.activeScript:
|
||||
active_script = cthulhu_state.activeScript
|
||||
for binding in bindings.keyBindings:
|
||||
active_script.getKeyBindings().add(binding)
|
||||
grab_ids = self.app.addKeyGrab(binding)
|
||||
if grab_ids:
|
||||
binding._grab_ids = grab_ids
|
||||
|
||||
# Store these bindings for future script changes
|
||||
plugin_name = plugin.name or plugin.module_name
|
||||
if not hasattr(self, '_plugin_global_bindings'):
|
||||
self._plugin_global_bindings = {}
|
||||
self._plugin_global_bindings[plugin_name] = bindings
|
||||
|
||||
# Connect to script changes to ensure bindings work with all scripts
|
||||
if not hasattr(self, '_connected_to_script_changes'):
|
||||
signal_manager = self.app.getSignalManager()
|
||||
if signal_manager:
|
||||
signal_manager.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):
|
||||
"""Ensure plugin directories exist."""
|
||||
os.makedirs(PluginType.SYSTEM.get_root_dir(), exist_ok=True)
|
||||
@@ -136,19 +328,31 @@ class PluginSystemManager:
|
||||
def _scan_plugins_in_directory(self, directory):
|
||||
"""Scan for plugins in a directory."""
|
||||
if not os.path.exists(directory) or not os.path.isdir(directory):
|
||||
logger.warning(f"Plugin directory not found or not a directory: {directory}")
|
||||
return
|
||||
|
||||
logger.info(f"Scanning for plugins in directory: {directory}")
|
||||
for item in os.listdir(directory):
|
||||
plugin_dir = os.path.join(directory, item)
|
||||
if not os.path.isdir(plugin_dir):
|
||||
continue
|
||||
|
||||
# Check for the traditional structure first (plugin.py & plugin.info)
|
||||
plugin_file = os.path.join(plugin_dir, "plugin.py")
|
||||
metadata_file = os.path.join(plugin_dir, "plugin.info")
|
||||
|
||||
# Fall back to [PluginName].py if plugin.py doesn't exist
|
||||
if not os.path.isfile(plugin_file):
|
||||
alternative_plugin_file = os.path.join(plugin_dir, f"{item}.py")
|
||||
if os.path.isfile(alternative_plugin_file):
|
||||
plugin_file = alternative_plugin_file
|
||||
logger.info(f"Using alternative plugin file: {alternative_plugin_file}")
|
||||
|
||||
# Check if we have any valid plugin file
|
||||
if os.path.isfile(plugin_file):
|
||||
# Extract plugin info
|
||||
module_name = os.path.basename(plugin_dir)
|
||||
logger.info(f"Found plugin: {module_name} in {plugin_dir}")
|
||||
metadata = self._load_plugin_metadata(metadata_file)
|
||||
|
||||
plugin_info = PluginInfo(
|
||||
@@ -162,7 +366,10 @@ class PluginSystemManager:
|
||||
plugin_info.builtin = metadata.get('builtin', 'false').lower() == 'true'
|
||||
plugin_info.hidden = metadata.get('hidden', 'false').lower() == 'true'
|
||||
|
||||
logger.info(f"Adding plugin to registry: {module_name}")
|
||||
self._plugins[module_name] = plugin_info
|
||||
else:
|
||||
logger.warning(f"No plugin file found in directory: {plugin_dir}")
|
||||
|
||||
def _load_plugin_metadata(self, metadata_file):
|
||||
"""Load plugin metadata from a file."""
|
||||
@@ -190,8 +397,34 @@ class PluginSystemManager:
|
||||
|
||||
def setActivePlugins(self, activePlugins):
|
||||
"""Set active plugins and sync their state."""
|
||||
logger.info(f"=== PluginSystemManager.setActivePlugins called ===")
|
||||
logger.info(f"Setting active plugins: {activePlugins}")
|
||||
|
||||
# Make sure we have scanned for plugins first
|
||||
if not self._plugins:
|
||||
logger.info("No plugins found, rescanning...")
|
||||
self.rescanPlugins()
|
||||
|
||||
self._active_plugins = activePlugins
|
||||
|
||||
# Log active vs available plugins
|
||||
available_plugins = [p.get_module_name() for p in self.plugins]
|
||||
logger.info(f"Available plugins: {available_plugins}")
|
||||
logger.info(f"Active plugins: {self._active_plugins}")
|
||||
|
||||
# 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
|
||||
missing_plugins = [p for p in self._active_plugins if p not in available_plugins]
|
||||
if missing_plugins:
|
||||
logger.warning(f"Active plugins not found: {missing_plugins}")
|
||||
|
||||
self.syncAllPluginsActive()
|
||||
logger.info("=== PluginSystemManager.setActivePlugins completed ===")
|
||||
|
||||
def setPluginActive(self, pluginInfo, active):
|
||||
"""Set the active state of a plugin."""
|
||||
@@ -211,25 +444,66 @@ class PluginSystemManager:
|
||||
|
||||
def isPluginActive(self, pluginInfo):
|
||||
"""Check if a plugin is active."""
|
||||
module_name = pluginInfo.get_module_name()
|
||||
|
||||
# Builtin plugins are always active
|
||||
if pluginInfo.builtin:
|
||||
logger.debug(f"Plugin {module_name} is builtin, active by default")
|
||||
return True
|
||||
|
||||
# If a plugin is already loaded, it's active
|
||||
if pluginInfo.loaded:
|
||||
logger.debug(f"Plugin {module_name} is already loaded, considered active")
|
||||
return True
|
||||
|
||||
return pluginInfo.get_module_name() in self.getActivePlugins()
|
||||
# Check case-insensitive match in active plugins list
|
||||
active_plugins = self.getActivePlugins()
|
||||
|
||||
# Try exact match first
|
||||
if module_name in active_plugins:
|
||||
logger.debug(f"Plugin {module_name} found in active plugins list")
|
||||
return True
|
||||
|
||||
# Try case-insensitive match
|
||||
module_name_lower = module_name.lower()
|
||||
is_active = any(plugin.lower() == module_name_lower for plugin in active_plugins)
|
||||
|
||||
if is_active:
|
||||
logger.debug(f"Plugin {module_name} found in active plugins list (case-insensitive match)")
|
||||
else:
|
||||
logger.debug(f"Plugin {module_name} not found in active plugins list")
|
||||
|
||||
return is_active
|
||||
|
||||
def syncAllPluginsActive(self):
|
||||
"""Sync the active state of all plugins."""
|
||||
logger.info("Syncing active state of all plugins")
|
||||
|
||||
# Log plugin status before syncing
|
||||
if PLUGIN_DEBUG:
|
||||
for pluginInfo in self.plugins:
|
||||
is_active = self.isPluginActive(pluginInfo)
|
||||
is_loaded = pluginInfo.loaded
|
||||
logger.debug(f"Plugin {pluginInfo.get_module_name()}: active={is_active}, loaded={is_loaded}")
|
||||
|
||||
# First unload inactive plugins
|
||||
for pluginInfo in self.plugins:
|
||||
if not self.isPluginActive(pluginInfo) and pluginInfo.loaded:
|
||||
logger.info(f"Unloading inactive plugin: {pluginInfo.get_module_name()}")
|
||||
self.unloadPlugin(pluginInfo)
|
||||
|
||||
# Then load active plugins
|
||||
for pluginInfo in self.plugins:
|
||||
if self.isPluginActive(pluginInfo) and not pluginInfo.loaded:
|
||||
self.loadPlugin(pluginInfo)
|
||||
logger.info(f"Loading active plugin: {pluginInfo.get_module_name()}")
|
||||
result = self.loadPlugin(pluginInfo)
|
||||
logger.info(f"Plugin {pluginInfo.get_module_name()} load result: {result}")
|
||||
|
||||
# Log final plugin status
|
||||
active_plugins = [p.get_module_name() for p in self.plugins if p.loaded]
|
||||
logger.info(f"Active plugins after sync: {active_plugins}")
|
||||
inactive_plugins = [p.get_module_name() for p in self.plugins if not p.loaded]
|
||||
logger.info(f"Inactive plugins after sync: {inactive_plugins}")
|
||||
|
||||
def loadPlugin(self, pluginInfo):
|
||||
"""Load a plugin."""
|
||||
@@ -239,15 +513,38 @@ class PluginSystemManager:
|
||||
return False
|
||||
|
||||
module_name = pluginInfo.get_module_name()
|
||||
logger.info(f"=== PluginSystemManager.loadPlugin starting for: {module_name} ===")
|
||||
|
||||
try:
|
||||
# Already loaded?
|
||||
if pluginInfo.loaded:
|
||||
logger.info(f"Plugin {module_name} already loaded, skipping")
|
||||
return True
|
||||
|
||||
# Import plugin module
|
||||
plugin_file = os.path.join(pluginInfo.get_module_dir(), "plugin.py")
|
||||
# Try to find the plugin file
|
||||
module_name = pluginInfo.get_module_name()
|
||||
plugin_dir = pluginInfo.get_module_dir()
|
||||
|
||||
# Check for plugin.py first (standard format)
|
||||
plugin_file = os.path.join(plugin_dir, "plugin.py")
|
||||
|
||||
# Fall back to [PluginName].py if plugin.py doesn't exist
|
||||
if not os.path.exists(plugin_file):
|
||||
alternative_plugin_file = os.path.join(plugin_dir, f"{module_name}.py")
|
||||
if os.path.exists(alternative_plugin_file):
|
||||
plugin_file = alternative_plugin_file
|
||||
logger.info(f"Using alternative plugin file: {alternative_plugin_file}")
|
||||
|
||||
if not os.path.exists(plugin_file):
|
||||
logger.error(f"Plugin file not found: {plugin_file}")
|
||||
return False
|
||||
|
||||
logger.info(f"Loading plugin from: {plugin_file}")
|
||||
spec = importlib.util.spec_from_file_location(module_name, plugin_file)
|
||||
if spec is None:
|
||||
logger.error(f"Failed to create spec for plugin: {module_name}")
|
||||
return False
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
pluginInfo.module = module
|
||||
@@ -260,6 +557,7 @@ class PluginSystemManager:
|
||||
attr.__module__ == module.__name__ and
|
||||
hasattr(attr, 'activate')):
|
||||
plugin_class = attr
|
||||
logger.info(f"Found plugin class: {attr.__name__} in {module_name}")
|
||||
break
|
||||
|
||||
if not plugin_class:
|
||||
@@ -267,32 +565,51 @@ class PluginSystemManager:
|
||||
return False
|
||||
|
||||
# Create and initialize plugin instance
|
||||
logger.info(f"Creating instance of plugin class: {plugin_class.__name__}")
|
||||
plugin_instance = plugin_class()
|
||||
pluginInfo.instance = plugin_instance
|
||||
|
||||
# Ensure plugins have a reference to the app
|
||||
plugin_instance.app = self.getApp()
|
||||
|
||||
logger.info(f"Set app reference for plugin: {module_name}")
|
||||
|
||||
if hasattr(plugin_instance, 'set_app'):
|
||||
plugin_instance.set_app(self.getApp())
|
||||
logger.info(f"Called set_app() for plugin: {module_name}")
|
||||
|
||||
if hasattr(plugin_instance, 'set_plugin_info'):
|
||||
plugin_instance.set_plugin_info(pluginInfo)
|
||||
logger.info(f"Called set_plugin_info() for plugin: {module_name}")
|
||||
|
||||
# Register with pluggy and activate
|
||||
if self.plugin_manager is None:
|
||||
logger.error(f"Plugin manager is None when loading {module_name}")
|
||||
return False
|
||||
|
||||
logger.info(f"Registering plugin with pluggy: {module_name}")
|
||||
self.plugin_manager.register(plugin_instance)
|
||||
|
||||
try:
|
||||
logger.info(f"Activating plugin: {module_name}")
|
||||
self.plugin_manager.hook.activate(plugin=plugin_instance)
|
||||
except Exception as e:
|
||||
logger.error(f"Error activating plugin {module_name}: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
pluginInfo.loaded = True
|
||||
logger.info(f"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
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load plugin {module_name}: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
def unloadPlugin(self, pluginInfo):
|
||||
|
||||
@@ -37,7 +37,8 @@ class ByeCthulhu(Plugin):
|
||||
signal_manager = self.app.getSignalManager()
|
||||
self._signal_handler_id = signal_manager.connectSignal(
|
||||
"stop-application-completed",
|
||||
self.process
|
||||
self.process,
|
||||
"default" # Add profile parameter
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error activating ByeCthulhu plugin: {e}")
|
||||
@@ -54,9 +55,9 @@ class ByeCthulhu(Plugin):
|
||||
# Disconnect signal if we have an ID
|
||||
if self._signal_handler_id is not None:
|
||||
signal_manager = self.app.getSignalManager()
|
||||
signal_manager.disconnectSignal(
|
||||
"stop-application-completed",
|
||||
self._signal_handler_id
|
||||
# Use disconnectSignalByFunction instead since disconnectSignal doesn't exist
|
||||
signal_manager.disconnectSignalByFunction(
|
||||
self.process
|
||||
)
|
||||
self._signal_handler_id = None
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[Plugin]
|
||||
Module=CapsLockHack
|
||||
Loader=python3
|
||||
Name=Caps Lock Hack
|
||||
Description=Fix Capslock sometimes switch on / off when its used as modifier
|
||||
Authors=Chrys chrys@linux-a11y.org
|
||||
@@ -1,144 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
from cthulhu import plugin
|
||||
|
||||
import gi
|
||||
gi.require_version('Peas', '1.0')
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Peas
|
||||
from threading import Thread, Lock
|
||||
import subprocess, time, re, os
|
||||
|
||||
class CapsLockHack(GObject.Object, Peas.Activatable, plugin.Plugin):
|
||||
__gtype_name__ = 'CapsLockHack'
|
||||
|
||||
object = GObject.Property(type=GObject.Object)
|
||||
def __init__(self):
|
||||
plugin.Plugin.__init__(self)
|
||||
self.lock = Lock()
|
||||
self.active = False
|
||||
self.workerThread = Thread(target=self.worker)
|
||||
def do_activate(self):
|
||||
API = self.object
|
||||
"""Enable or disable use of the caps lock key as an Cthulhu modifier key."""
|
||||
self.interpretCapsLineProg = re.compile(
|
||||
r'^\s*interpret\s+Caps[_+]Lock[_+]AnyOfOrNone\s*\(all\)\s*{\s*$', re.I)
|
||||
self.normalCapsLineProg = re.compile(
|
||||
r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Lock\s*\)\s*;\s*$', re.I)
|
||||
self.interpretShiftLineProg = re.compile(
|
||||
r'^\s*interpret\s+Shift[_+]Lock[_+]AnyOf\s*\(\s*Shift\s*\+\s*Lock\s*\)\s*{\s*$', re.I)
|
||||
self.normalShiftLineProg = re.compile(
|
||||
r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Shift\s*\)\s*;\s*$', re.I)
|
||||
self.disabledModLineProg = re.compile(
|
||||
r'^\s*action\s*=\s*NoAction\s*\(\s*\)\s*;\s*$', re.I)
|
||||
self.normalCapsLine = ' action= LockMods(modifiers=Lock);'
|
||||
self.normalShiftLine = ' action= LockMods(modifiers=Shift);'
|
||||
self.disabledModLine = ' action= NoAction();'
|
||||
self.activateWorker()
|
||||
def do_deactivate(self):
|
||||
API = self.object
|
||||
self.deactivateWorker()
|
||||
def do_update_state(self):
|
||||
API = self.object
|
||||
def deactivateWorker(self):
|
||||
with self.lock:
|
||||
self.active = False
|
||||
self.workerThread.join()
|
||||
def activateWorker(self):
|
||||
with self.lock:
|
||||
self.active = True
|
||||
self.workerThread.start()
|
||||
def isActive(self):
|
||||
with self.lock:
|
||||
return self.active
|
||||
def worker(self):
|
||||
"""Makes an Cthulhu-specific Xmodmap so that the keys behave as we
|
||||
need them to do. This is especially the case for the Cthulhu modifier.
|
||||
"""
|
||||
API = self.object
|
||||
capsLockCleared = False
|
||||
settings = API.app.getDynamicApiManager().getAPI('Settings')
|
||||
time.sleep(3)
|
||||
while self.isActive():
|
||||
if "Caps_Lock" in settings.cthulhuModifierKeys \
|
||||
or "Shift_Lock" in settings.cthulhuModifierKeys:
|
||||
self.setCapsLockAsCthulhuModifier(True)
|
||||
capsLockCleared = True
|
||||
elif capsLockCleared:
|
||||
self.setCapsLockAsCthulhuModifier(False)
|
||||
capsLockCleared = False
|
||||
time.sleep(1)
|
||||
|
||||
def setCapsLockAsCthulhuModifier(self, enable):
|
||||
originalXmodmap = None
|
||||
lines = None
|
||||
try:
|
||||
originalXmodmap = subprocess.check_output(['xkbcomp', os.environ['DISPLAY'], '-'])
|
||||
lines = originalXmodmap.decode('UTF-8').split('\n')
|
||||
except:
|
||||
return
|
||||
foundCapsInterpretSection = False
|
||||
foundShiftInterpretSection = False
|
||||
modified = False
|
||||
for i, line in enumerate(lines):
|
||||
if not foundCapsInterpretSection and not foundShiftInterpretSection:
|
||||
if self.interpretCapsLineProg.match(line):
|
||||
foundCapsInterpretSection = True
|
||||
elif self.interpretShiftLineProg.match(line):
|
||||
foundShiftInterpretSection = True
|
||||
elif foundCapsInterpretSection:
|
||||
if enable:
|
||||
if self.normalCapsLineProg.match(line):
|
||||
lines[i] = self.disabledModLine
|
||||
modified = True
|
||||
else:
|
||||
if self.disabledModLineProg.match(line):
|
||||
lines[i] = self.normalCapsLine
|
||||
modified = True
|
||||
if line.find('}'):
|
||||
foundCapsInterpretSection = False
|
||||
else: # foundShiftInterpretSection
|
||||
if enable:
|
||||
if self.normalShiftLineProg.match(line):
|
||||
lines[i] = self.disabledModLine
|
||||
modified = True
|
||||
else:
|
||||
if self.disabledModLineProg.match(line):
|
||||
lines[i] = self.normalShiftLine
|
||||
modified = True
|
||||
if line.find('}'):
|
||||
foundShiftInterpretSection = False
|
||||
if modified:
|
||||
newXmodMap = bytes('\n'.join(lines), 'UTF-8')
|
||||
self.setXmodmap(newXmodMap)
|
||||
def setXmodmap(self, xkbmap):
|
||||
"""Set the keyboard map using xkbcomp."""
|
||||
try:
|
||||
p = subprocess.Popen(['xkbcomp', '-w0', '-', os.environ['DISPLAY']],
|
||||
stdin=subprocess.PIPE, stdout=None, stderr=None)
|
||||
p.communicate(xkbmap)
|
||||
except:
|
||||
pass
|
||||
@@ -1,7 +0,0 @@
|
||||
cthulhu_python_PYTHON = \
|
||||
__init__.py \
|
||||
CapsLockHack.plugin \
|
||||
CapsLockHack.py
|
||||
|
||||
cthulhu_pythondir=$(pkgpythondir)/plugins/CapsLockHack
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[Plugin]
|
||||
Module=Clipboard
|
||||
Loader=python3
|
||||
Name=Clipboard
|
||||
Description=Present the content of the current clipboard
|
||||
Authors=Chrys chrys@linux-a11y.org
|
||||
@@ -1,101 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
from cthulhu import plugin
|
||||
|
||||
import gi, os
|
||||
gi.require_version('Peas', '1.0')
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Peas
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
class Clipboard(GObject.Object, Peas.Activatable, plugin.Plugin):
|
||||
#__gtype_name__ = 'Clipboard'
|
||||
|
||||
object = GObject.Property(type=GObject.Object)
|
||||
def __init__(self):
|
||||
plugin.Plugin.__init__(self)
|
||||
def do_activate(self):
|
||||
API = self.object
|
||||
self.registerGestureByString(self.speakClipboard, _('clipboard'), 'kb:cthulhu+c')
|
||||
def do_deactivate(self):
|
||||
API = self.object
|
||||
def do_update_state(self):
|
||||
API = self.object
|
||||
def speakClipboard(self, script=None, inputEvent=None):
|
||||
API = self.object
|
||||
Message = self.getClipboard()
|
||||
API.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(Message, resetStyles=False)
|
||||
return True
|
||||
def getClipboard(self):
|
||||
Message = ""
|
||||
FoundClipboardContent = False
|
||||
# Get Clipboard
|
||||
ClipboardObj = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
|
||||
ClipboardText = ClipboardObj.wait_for_text()
|
||||
ClipboardImage = ClipboardObj.wait_for_image()
|
||||
ClipboardURI = ClipboardObj.wait_for_uris()
|
||||
if (ClipboardText != None):
|
||||
FoundClipboardContent = True
|
||||
if (ClipboardObj.wait_is_uris_available()):
|
||||
noOfObjects = 0
|
||||
noOfFolder = 0
|
||||
noOfFiles = 0
|
||||
noOfDisks = 0
|
||||
noOfLinks = 0
|
||||
for Uri in ClipboardURI:
|
||||
if Uri == '':
|
||||
continue
|
||||
noOfObjects += 1
|
||||
uriWithoutProtocoll = Uri[Uri.find('://') + 3:]
|
||||
Message += " " + Uri[Uri.rfind('/') + 1:] + " "
|
||||
if (os.path.isdir(uriWithoutProtocoll)):
|
||||
noOfFolder += 1
|
||||
Message = Message + _("Folder") #Folder
|
||||
if (os.path.isfile(uriWithoutProtocoll)):
|
||||
noOfFiles += 1
|
||||
Message = Message + _("File") #File
|
||||
if (os.path.ismount(uriWithoutProtocoll)):
|
||||
noOfDisks += 1
|
||||
Message = Message + _("Disk") #Mountpoint
|
||||
if (os.path.islink(uriWithoutProtocoll)):
|
||||
noOfLinks += 1
|
||||
Message = Message + _("Link") #Link
|
||||
if (noOfObjects > 1):
|
||||
Message = str(noOfObjects) + _(" Objects in clipboard ") + Message # X Objects in Clipboard Object Object
|
||||
else:
|
||||
Message = str(noOfObjects) + _(" Object in clipboard ") + Message # 1 Object in Clipboard Object
|
||||
else:
|
||||
Message = _("Text in clipboard ") + ClipboardText # Text in Clipboard
|
||||
|
||||
if (ClipboardImage != None):
|
||||
FoundClipboardContent = True
|
||||
Message = _("The clipboard contains a image") # Image is in Clipboard
|
||||
|
||||
if (not FoundClipboardContent):
|
||||
Message = _("The clipboard is empty")
|
||||
return Message
|
||||
@@ -1,7 +1,7 @@
|
||||
cthulhu_python_PYTHON = \
|
||||
__init__.py \
|
||||
Clipboard.plugin \
|
||||
Clipboard.py
|
||||
plugin.info \
|
||||
plugin.py
|
||||
|
||||
cthulhu_pythondir=$(pkgpythondir)/plugins/Clipboard
|
||||
|
||||
|
||||
@@ -23,3 +23,8 @@
|
||||
# Fork of Orca Screen Reader (GNOME)
|
||||
# Original source: https://gitlab.gnome.org/GNOME/orca
|
||||
|
||||
"""Clipboard plugin package."""
|
||||
|
||||
from .plugin import Clipboard
|
||||
|
||||
__all__ = ['Clipboard']
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[Plugin]
|
||||
Name = Clipboard
|
||||
Module = Clipboard
|
||||
Description = Present the content of the current clipboard
|
||||
Authors = Storm Dragon <storm_dragon@stormux.org>
|
||||
Version = 1.0
|
||||
Category = Utilities
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
"""Clipboard plugin for Cthulhu."""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import gettext
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
from cthulhu.plugin import Plugin, cthulhu_hookimpl
|
||||
|
||||
# Set up translation function
|
||||
_ = gettext.gettext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Clipboard(Plugin):
|
||||
"""Plugin to read the clipboard contents."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the plugin."""
|
||||
super().__init__(*args, **kwargs)
|
||||
logger.info("Clipboard plugin initialized")
|
||||
self._signal_handler_id = None
|
||||
|
||||
@cthulhu_hookimpl
|
||||
def activate(self, plugin=None):
|
||||
"""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")
|
||||
# Note: Currently no unregister method needed as keybindings are managed by APIHelper
|
||||
|
||||
def speakClipboard(self, script=None, inputEvent=None):
|
||||
"""Present the contents of the clipboard."""
|
||||
try:
|
||||
message = self.getClipboard()
|
||||
|
||||
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
|
||||
if state and state.activeScript:
|
||||
state.activeScript.presentMessage(message, resetStyles=False)
|
||||
logger.debug("Presented clipboard contents")
|
||||
else:
|
||||
logger.warning("Could not present clipboard: no active script")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error in speakClipboard: {e}")
|
||||
return False
|
||||
|
||||
def getClipboard(self):
|
||||
"""Get the contents of the clipboard."""
|
||||
try:
|
||||
message = ""
|
||||
found_clipboard_content = False
|
||||
|
||||
# Get Clipboard
|
||||
clipboard_obj = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
|
||||
clipboard_text = clipboard_obj.wait_for_text()
|
||||
clipboard_image = clipboard_obj.wait_for_image()
|
||||
clipboard_uri = clipboard_obj.wait_for_uris()
|
||||
|
||||
if clipboard_text is not None:
|
||||
found_clipboard_content = True
|
||||
|
||||
if clipboard_obj.wait_is_uris_available():
|
||||
no_of_objects = 0
|
||||
no_of_folder = 0
|
||||
no_of_files = 0
|
||||
no_of_disks = 0
|
||||
no_of_links = 0
|
||||
|
||||
for uri in clipboard_uri:
|
||||
if uri == '':
|
||||
continue
|
||||
|
||||
no_of_objects += 1
|
||||
uri_without_protocol = uri[uri.find('://') + 3:]
|
||||
message += " " + uri[uri.rfind('/') + 1:] + " "
|
||||
|
||||
if os.path.isdir(uri_without_protocol):
|
||||
no_of_folder += 1
|
||||
message = message + _("Folder")
|
||||
|
||||
if os.path.isfile(uri_without_protocol):
|
||||
no_of_files += 1
|
||||
message = message + _("File")
|
||||
|
||||
if os.path.ismount(uri_without_protocol):
|
||||
no_of_disks += 1
|
||||
message = message + _("Disk")
|
||||
|
||||
if os.path.islink(uri_without_protocol):
|
||||
no_of_links += 1
|
||||
message = message + _("Link")
|
||||
|
||||
if no_of_objects > 1:
|
||||
message = str(no_of_objects) + _(" Objects in clipboard ") + message
|
||||
else:
|
||||
message = str(no_of_objects) + _(" Object in clipboard ") + message
|
||||
else:
|
||||
message = _("Text in clipboard ") + clipboard_text
|
||||
|
||||
if clipboard_image is not None:
|
||||
found_clipboard_content = True
|
||||
message = _("The clipboard contains a image")
|
||||
|
||||
if not found_clipboard_content:
|
||||
message = _("The clipboard is empty")
|
||||
|
||||
return message
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting clipboard content: {e}")
|
||||
return _("Error accessing clipboard")
|
||||
@@ -1,6 +0,0 @@
|
||||
[Plugin]
|
||||
Module=Date
|
||||
Loader=python3
|
||||
Name=Date
|
||||
Description=Present the current date
|
||||
Authors=Chrys chrys@linux-a11y.org
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
from cthulhu import plugin
|
||||
|
||||
import gi, time
|
||||
gi.require_version('Peas', '1.0')
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Peas
|
||||
|
||||
class Date(GObject.Object, Peas.Activatable, plugin.Plugin):
|
||||
#__gtype_name__ = 'Date'
|
||||
|
||||
object = GObject.Property(type=GObject.Object)
|
||||
def __init__(self):
|
||||
plugin.Plugin.__init__(self)
|
||||
def do_activate(self):
|
||||
API = self.object
|
||||
self.connectSignal("setup-inputeventhandlers-completed", self.setupCompatBinding)
|
||||
def setupCompatBinding(self, app):
|
||||
cmdnames = app.getDynamicApiManager().getAPI('Cmdnames')
|
||||
inputEventHandlers = app.getDynamicApiManager().getAPI('inputEventHandlers')
|
||||
inputEventHandlers['presentDateHandler'] = app.getAPIHelper().createInputEventHandler(self.presentDate, cmdnames.PRESENT_CURRENT_DATE)
|
||||
def do_deactivate(self):
|
||||
API = self.object
|
||||
inputEventHandlers = API.app.getDynamicApiManager().getAPI('inputEventHandlers')
|
||||
del inputEventHandlers['presentDateHandler']
|
||||
def presentDate(self, script=None, inputEvent=None):
|
||||
""" Presents the current time. """
|
||||
API = self.object
|
||||
settings_manager = API.app.getDynamicApiManager().getAPI('SettingsManager')
|
||||
_settingsManager = settings_manager.getManager()
|
||||
dateFormat = _settingsManager.getSetting('presentDateFormat')
|
||||
message = time.strftime(dateFormat, time.localtime())
|
||||
API.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(message, resetStyles=False)
|
||||
return True
|
||||
@@ -1,7 +0,0 @@
|
||||
cthulhu_python_PYTHON = \
|
||||
__init__.py \
|
||||
Date.plugin \
|
||||
Date.py
|
||||
|
||||
cthulhu_pythondir=$(pkgpythondir)/plugins/Date
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
@@ -32,16 +32,62 @@ class DisplayVersion(Plugin):
|
||||
return
|
||||
|
||||
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
|
||||
gesture_string = 'kb:cthulhu+shift+v'
|
||||
logger.info(f"DisplayVersion: Attempting to register gesture: {gesture_string}")
|
||||
|
||||
self._kb_binding = self.registerGestureByString(
|
||||
self.speakText,
|
||||
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:
|
||||
logger.error(f"Error activating DisplayVersion plugin: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
@cthulhu_hookimpl
|
||||
def deactivate(self, plugin=None):
|
||||
@@ -55,6 +101,7 @@ class DisplayVersion(Plugin):
|
||||
def speakText(self, script=None, inputEvent=None):
|
||||
"""Speak the Cthulhu version when shortcut is pressed."""
|
||||
try:
|
||||
logger.info("DisplayVersion plugin: speakText called")
|
||||
if self.app:
|
||||
state = self.app.getDynamicApiManager().getAPI('CthulhuState')
|
||||
if state.activeScript:
|
||||
@@ -65,4 +112,4 @@ class DisplayVersion(Plugin):
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error in DisplayVersion speakText: {e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[Plugin]
|
||||
Module=HelloCthulhu
|
||||
Loader=python3
|
||||
Name=Cthulhu say hello
|
||||
Description=startup announcement for Cthulhu
|
||||
Authors=Chrys chrys@linux-a11y.org
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
from cthulhu import plugin
|
||||
|
||||
import gi
|
||||
gi.require_version('Peas', '1.0')
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Peas
|
||||
|
||||
class HelloCthulhu(GObject.Object, Peas.Activatable, plugin.Plugin):
|
||||
#__gtype_name__ = 'HelloCthulhu'
|
||||
|
||||
object = GObject.Property(type=GObject.Object)
|
||||
def __init__(self):
|
||||
plugin.Plugin.__init__(self)
|
||||
def do_activate(self):
|
||||
API = self.object
|
||||
self.connectSignal("start-application-completed", self.process)
|
||||
def do_deactivate(self):
|
||||
API = self.object
|
||||
def do_update_state(self):
|
||||
API = self.object
|
||||
def process(self, app):
|
||||
messages = app.getDynamicApiManager().getAPI('Messages')
|
||||
app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(messages.START_CTHULHU, resetStyles=False)
|
||||
@@ -1,7 +1,7 @@
|
||||
cthulhu_python_PYTHON = \
|
||||
__init__.py \
|
||||
HelloCthulhu.plugin \
|
||||
HelloCthulhu.py
|
||||
plugin.info \
|
||||
plugin.py
|
||||
|
||||
cthulhu_pythondir=$(pkgpythondir)/plugins/HelloCthulhu
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[Plugin]
|
||||
Name = Cthulhu say hello
|
||||
Module = HelloCthulhu
|
||||
Description = Startup announcement for Cthulhu
|
||||
Authors = Storm Dragon <storm_dragon@stormux.org>
|
||||
Version = 1.0
|
||||
Category = Interaction
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2024 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.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||
# Boston MA 02110-1301 USA.
|
||||
#
|
||||
|
||||
"""Hello Cthulhu plugin for Cthulhu."""
|
||||
|
||||
import logging
|
||||
import weakref
|
||||
from cthulhu.plugin import Plugin, cthulhu_hookimpl
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Class-level variable to track if the greeting has been presented
|
||||
# This ensures the greeting is only shown once even if multiple instances exist
|
||||
_greeting_shown = False
|
||||
|
||||
class HelloCthulhu(Plugin):
|
||||
"""Plugin that speaks a welcome message when Cthulhu starts up."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the plugin."""
|
||||
super().__init__(*args, **kwargs)
|
||||
logger.info("HelloCthulhu plugin initialized")
|
||||
self._signal_handler_id = None
|
||||
self._is_connected = False
|
||||
|
||||
@cthulhu_hookimpl
|
||||
def activate(self, plugin=None):
|
||||
"""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 HelloCthulhu plugin")
|
||||
try:
|
||||
# Only connect the signal if we haven't already
|
||||
if not self._is_connected:
|
||||
signal_manager = self.app.getSignalManager()
|
||||
self._signal_handler_id = signal_manager.connectSignal(
|
||||
"start-application-completed",
|
||||
self.process,
|
||||
"default" # Add profile parameter
|
||||
)
|
||||
self._is_connected = True
|
||||
logger.debug("Connected to start-application-completed signal")
|
||||
except Exception as e:
|
||||
logger.error(f"Error activating HelloCthulhu 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 HelloCthulhu plugin")
|
||||
try:
|
||||
# Only disconnect if we're connected
|
||||
if self._is_connected and self._signal_handler_id is not None:
|
||||
signal_manager = self.app.getSignalManager()
|
||||
signal_manager.disconnectSignalByFunction(
|
||||
self.process
|
||||
)
|
||||
self._signal_handler_id = None
|
||||
self._is_connected = False
|
||||
logger.debug("Disconnected from start-application-completed signal")
|
||||
except Exception as e:
|
||||
logger.error(f"Error deactivating HelloCthulhu plugin: {e}")
|
||||
|
||||
def process(self, app):
|
||||
"""Process the start-application-completed signal."""
|
||||
global _greeting_shown
|
||||
|
||||
# Only present the message if it hasn't been shown yet
|
||||
if _greeting_shown:
|
||||
logger.debug("Greeting already shown, skipping")
|
||||
return
|
||||
|
||||
try:
|
||||
messages = app.getDynamicApiManager().getAPI('Messages')
|
||||
state = app.getDynamicApiManager().getAPI('CthulhuState')
|
||||
if state.activeScript:
|
||||
state.activeScript.presentMessage(messages.START_CTHULHU, resetStyles=False)
|
||||
_greeting_shown = True
|
||||
logger.info("Greeting message presented")
|
||||
|
||||
# Disconnect the signal after presenting the message
|
||||
if self._is_connected and self._signal_handler_id is not None:
|
||||
signal_manager = app.getSignalManager()
|
||||
signal_manager.disconnectSignalByFunction(
|
||||
self.process
|
||||
)
|
||||
self._signal_handler_id = None
|
||||
self._is_connected = False
|
||||
logger.debug("Disconnected signal after presenting greeting")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in HelloCthulhu process: {e}")
|
||||
@@ -1,4 +1,4 @@
|
||||
SUBDIRS = Clipboard DisplayVersion hello_world self_voice Time MouseReview Date ByeCthulhu HelloCthulhu PluginManager CapsLockHack SimplePluginSystem
|
||||
SUBDIRS = Clipboard DisplayVersion hello_world self_voice ByeCthulhu HelloCthulhu SimplePluginSystem
|
||||
|
||||
cthulhu_pythondir=$(pkgpythondir)/plugins
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
cthulhu_python_PYTHON = \
|
||||
__init__.py \
|
||||
MouseReview.plugin \
|
||||
MouseReview.py
|
||||
|
||||
cthulhu_pythondir=$(pkgpythondir)/plugins/MouseReview
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[Plugin]
|
||||
Module=MouseReview
|
||||
Loader=python3
|
||||
Name=Mouse Review
|
||||
Description=Review whats below the mouse coursor
|
||||
Authors=Chrys chrys@linux-a11y.org
|
||||
@@ -1,759 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
"""Mouse review mode."""
|
||||
|
||||
__id__ = "$Id$"
|
||||
__version__ = "$Revision$"
|
||||
__date__ = "$Date$"
|
||||
__copyright__ = "Copyright (c) 2008 Eitan Isaacson" \
|
||||
"Copyright (c) 2016 Igalia, S.L."
|
||||
__license__ = "LGPL"
|
||||
|
||||
from cthulhu import plugin
|
||||
|
||||
import gi, math, time
|
||||
gi.require_version('Peas', '1.0')
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Peas
|
||||
|
||||
gi.require_version("Atspi", "2.0")
|
||||
from gi.repository import Atspi
|
||||
|
||||
from gi.repository import Gdk
|
||||
try:
|
||||
gi.require_version("Wnck", "3.0")
|
||||
from gi.repository import Wnck
|
||||
_mouseReviewCapable = True
|
||||
except Exception:
|
||||
_mouseReviewCapable = False
|
||||
|
||||
# compatibility layer, see MouseReview.do_activate
|
||||
debug = None
|
||||
event_manager = None
|
||||
cthulhu = None
|
||||
cthulhu_state = None
|
||||
script_manager = None
|
||||
settings_manager = None
|
||||
speech = None
|
||||
messages = None
|
||||
cmdnames = None
|
||||
emitRegionChanged = None
|
||||
_scriptManager = None
|
||||
_settingsManager = None
|
||||
AXObject = None
|
||||
AXUtilities = None
|
||||
keybindings = None
|
||||
input_event = None
|
||||
|
||||
class MouseReview(GObject.Object, Peas.Activatable, plugin.Plugin):
|
||||
#__gtype_name__ = 'MouseReview'
|
||||
|
||||
object = GObject.Property(type=GObject.Object)
|
||||
def __init__(self):
|
||||
plugin.Plugin.__init__(self)
|
||||
def do_activate(self):
|
||||
API = self.object
|
||||
global _mouseReviewCapable
|
||||
if not _mouseReviewCapable:
|
||||
return
|
||||
global debug
|
||||
global event_manager
|
||||
global cthulhu_state
|
||||
global script_manager
|
||||
global settings_manager
|
||||
global speech
|
||||
global _scriptManager
|
||||
global _settingsManager
|
||||
global emitRegionChanged
|
||||
global messages
|
||||
global cmdnames
|
||||
global AXObject
|
||||
global AXUtilities
|
||||
global keybindings
|
||||
global input_event
|
||||
debug= API.app.getDynamicApiManager().getAPI('Debug')
|
||||
event_manager = API.app.getDynamicApiManager().getAPI('EventManager')
|
||||
messages = API.app.getDynamicApiManager().getAPI('Messages')
|
||||
cmdnames = API.app.getDynamicApiManager().getAPI('Cmdnames')
|
||||
cthulhu_state = API.app.getDynamicApiManager().getAPI('CthulhuState')
|
||||
script_manager = API.app.getDynamicApiManager().getAPI('ScriptManager')
|
||||
settings_manager = API.app.getDynamicApiManager().getAPI('SettingsManager')
|
||||
speech = API.app.getDynamicApiManager().getAPI('Speech')
|
||||
emitRegionChanged = API.app.getDynamicApiManager().getAPI('EmitRegionChanged')
|
||||
_scriptManager = script_manager.getManager()
|
||||
_settingsManager = settings_manager.getManager()
|
||||
AXObject = API.app.getDynamicApiManager().getAPI('AXObject')
|
||||
AXUtilities = API.app.getDynamicApiManager().getAPI('AXUtilities')
|
||||
keybindings = API.app.getDynamicApiManager().getAPI('Keybindings')
|
||||
input_event = API.app.getDynamicApiManager().getAPI('InputEvent')
|
||||
mouse_review = MouseReviewer()
|
||||
self.registerAPI('MouseReview', mouse_review)
|
||||
self.Initialize(API.app)
|
||||
self.connectSignal("setup-inputeventhandlers-completed", self.setupCompatBinding)
|
||||
self.connectSignal("load-setting-completed", self.Initialize)
|
||||
|
||||
def do_deactivate(self):
|
||||
API = self.object
|
||||
global _mouseReviewCapable
|
||||
if not _mouseReviewCapable:
|
||||
return
|
||||
mouse_review = API.app.getDynamicApiManager().getAPI('MouseReview')
|
||||
|
||||
mouse_review.deactivate()
|
||||
def do_update_state(self):
|
||||
API = self.object
|
||||
def setupCompatBinding(self, app):
|
||||
API = self.object
|
||||
mouse_review = API.app.getDynamicApiManager().getAPI('MouseReview')
|
||||
cmdnames = API.app.getDynamicApiManager().getAPI('Cmdnames')
|
||||
inputEventHandlers = API.app.getDynamicApiManager().getAPI('inputEventHandlers')
|
||||
inputEventHandlers['toggleMouseReviewHandler'] = API.app.getAPIHelper().createInputEventHandler(mouse_review.toggle, cmdnames.MOUSE_REVIEW_TOGGLE)
|
||||
def Initialize(self, app):
|
||||
mouse_review = app.getDynamicApiManager().getAPI('MouseReview')
|
||||
settings_manager = app.getDynamicApiManager().getAPI('SettingsManager')
|
||||
_settingsManager = settings_manager.getManager()
|
||||
if _settingsManager.getSetting('enableMouseReview'):
|
||||
mouse_review.activate()
|
||||
else:
|
||||
mouse_review.deactivate()
|
||||
|
||||
class _StringContext:
|
||||
"""The textual information associated with an _ItemContext."""
|
||||
|
||||
def __init__(self, obj, script=None, string="", start=0, end=0):
|
||||
"""Initialize the _StringContext.
|
||||
|
||||
Arguments:
|
||||
- string: The human-consumable string
|
||||
- obj: The accessible object associated with this string
|
||||
- start: The start offset with respect to entire text, if one exists
|
||||
- end: The end offset with respect to the entire text, if one exists
|
||||
- script: The script associated with the accessible object
|
||||
"""
|
||||
|
||||
self._obj = obj
|
||||
self._script = script
|
||||
self._string = string
|
||||
self._start = start
|
||||
self._end = end
|
||||
self._boundingBox = 0, 0, 0, 0
|
||||
if script:
|
||||
self._boundingBox = script.utilities.getTextBoundingBox(obj, start, end)
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is not None \
|
||||
and self._obj == other._obj \
|
||||
and self._string == other._string \
|
||||
and self._start == other._start \
|
||||
and self._end == other._end
|
||||
|
||||
def isSubstringOf(self, other):
|
||||
"""Returns True if this is a substring of other."""
|
||||
|
||||
if other is None:
|
||||
return False
|
||||
|
||||
if not (self._obj and other._obj):
|
||||
return False
|
||||
|
||||
thisBox = self.getBoundingBox()
|
||||
if thisBox == (0, 0, 0, 0):
|
||||
return False
|
||||
|
||||
otherBox = other.getBoundingBox()
|
||||
if otherBox == (0, 0, 0, 0):
|
||||
return False
|
||||
|
||||
# We get various and sundry results for the bounding box if the implementor
|
||||
# included newline characters as part of the word or line at offset. Try to
|
||||
# detect this and adjust the bounding boxes before getting the intersection.
|
||||
if thisBox[3] != otherBox[3] and self._obj == other._obj:
|
||||
thisNewLineCount = self._string.count("\n")
|
||||
if thisNewLineCount and thisBox[3] / thisNewLineCount == otherBox[3]:
|
||||
thisBox = *thisBox[0:3], otherBox[3]
|
||||
|
||||
if self._script.utilities.intersection(thisBox, otherBox) != thisBox:
|
||||
return False
|
||||
|
||||
if not (self._string and self._string.strip() in other._string):
|
||||
return False
|
||||
|
||||
msg = f"MOUSE REVIEW: '{self._string}' is substring of '{other._string}'"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
|
||||
def getBoundingBox(self):
|
||||
"""Returns the bounding box associated with this context's range."""
|
||||
|
||||
return self._boundingBox
|
||||
|
||||
def getString(self):
|
||||
"""Returns the string associated with this context."""
|
||||
|
||||
return self._string
|
||||
|
||||
def present(self):
|
||||
"""Presents this context to the user."""
|
||||
|
||||
if not self._script:
|
||||
msg = "MOUSE REVIEW: Not presenting due to lack of script"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return False
|
||||
|
||||
if not self._string:
|
||||
msg = "MOUSE REVIEW: Not presenting due to lack of string"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return False
|
||||
|
||||
voice = self._script.speechGenerator.voice(obj=self._obj, string=self._string)
|
||||
string = self._script.utilities.adjustForRepeats(self._string)
|
||||
# TODO
|
||||
#cthulhu.emitRegionChanged(self._obj, self._start, self._end, cthulhu.MOUSE_REVIEW)
|
||||
emitRegionChanged(self._obj, self._start, self._end, "mouse-review")
|
||||
|
||||
|
||||
self._script.speakMessage(string, voice=voice, interrupt=False)
|
||||
self._script.displayBrailleMessage(self._string, -1)
|
||||
return True
|
||||
|
||||
|
||||
class _ItemContext:
|
||||
"""Holds all the information of the item at a specified point."""
|
||||
|
||||
def __init__(self, x=0, y=0, obj=None, boundary=None, frame=None, script=None):
|
||||
"""Initialize the _ItemContext.
|
||||
|
||||
Arguments:
|
||||
- x: The X coordinate
|
||||
- y: The Y coordinate
|
||||
- obj: The accessible object of interest at that coordinate
|
||||
- boundary: The accessible-text boundary type
|
||||
- frame: The containing accessible object (often a top-level window)
|
||||
- script: The script associated with the accessible object
|
||||
"""
|
||||
|
||||
self._x = x
|
||||
self._y = y
|
||||
self._obj = obj
|
||||
self._boundary = boundary
|
||||
self._frame = frame
|
||||
self._script = script
|
||||
self._string = self._getStringContext()
|
||||
self._time = time.time()
|
||||
self._boundingBox = 0, 0, 0, 0
|
||||
if script:
|
||||
self._boundingBox = script.utilities.getBoundingBox(obj)
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is not None \
|
||||
and self._frame == other._frame \
|
||||
and self._obj == other._obj \
|
||||
and self._string == other._string
|
||||
|
||||
def _treatAsDuplicate(self, prior):
|
||||
if self._obj != prior._obj or self._frame != prior._frame:
|
||||
msg = "MOUSE REVIEW: Not a duplicate: different objects"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return False
|
||||
|
||||
if self.getString() and prior.getString() and not self._isSubstringOf(prior):
|
||||
msg = "MOUSE REVIEW: Not a duplicate: not a substring of"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return False
|
||||
|
||||
if self._x == prior._x and self._y == prior._y:
|
||||
msg = "MOUSE REVIEW: Treating as duplicate: mouse didn't move"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
|
||||
interval = self._time - prior._time
|
||||
if interval > 0.5:
|
||||
msg = f"MOUSE REVIEW: Not a duplicate: was {interval:.2f}s ago"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return False
|
||||
|
||||
msg = "MOUSE REVIEW: Treating as duplicate"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
|
||||
def _treatAsSingleObject(self):
|
||||
if not AXObject.supports_text(self._obj):
|
||||
return True
|
||||
|
||||
if not self._obj.queryText().characterCount:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _getStringContext(self):
|
||||
"""Returns the _StringContext associated with the specified point."""
|
||||
|
||||
if not (self._script and self._obj):
|
||||
return _StringContext(self._obj)
|
||||
|
||||
if self._treatAsSingleObject():
|
||||
return _StringContext(self._obj, self._script)
|
||||
|
||||
string, start, end = self._script.utilities.textAtPoint(
|
||||
self._obj, self._x, self._y, boundary=self._boundary)
|
||||
if string:
|
||||
string = self._script.utilities.expandEOCs(self._obj, start, end)
|
||||
|
||||
return _StringContext(self._obj, self._script, string, start, end)
|
||||
|
||||
def _getContainer(self):
|
||||
roles = [Atspi.Role.DIALOG,
|
||||
Atspi.Role.FRAME,
|
||||
Atspi.Role.LAYERED_PANE,
|
||||
Atspi.Role.MENU,
|
||||
Atspi.Role.PAGE_TAB,
|
||||
Atspi.Role.TOOL_BAR,
|
||||
Atspi.Role.WINDOW]
|
||||
return AXObject.find_ancestor(self._obj, lambda x: AXObject.get_role(x) in roles)
|
||||
|
||||
def _isSubstringOf(self, other):
|
||||
"""Returns True if this is a substring of other."""
|
||||
|
||||
return self._string.isSubstringOf(other._string)
|
||||
|
||||
def getObject(self):
|
||||
"""Returns the accessible object associated with this context."""
|
||||
|
||||
return self._obj
|
||||
|
||||
def getBoundingBox(self):
|
||||
"""Returns the bounding box associated with this context."""
|
||||
|
||||
x, y, width, height = self._string.getBoundingBox()
|
||||
if not (width or height):
|
||||
return self._boundingBox
|
||||
|
||||
return x, y, width, height
|
||||
|
||||
def getString(self):
|
||||
"""Returns the string associated with this context."""
|
||||
|
||||
return self._string.getString()
|
||||
|
||||
def getTime(self):
|
||||
"""Returns the time associated with this context."""
|
||||
|
||||
return self._time
|
||||
|
||||
def _isInlineChild(self, prior):
|
||||
if not self._obj or not prior._obj:
|
||||
return False
|
||||
|
||||
if AXObject.get_parent(prior._obj) != self._obj:
|
||||
return False
|
||||
|
||||
if self._treatAsSingleObject():
|
||||
return False
|
||||
|
||||
return AXUtilities.is_link(prior._obj)
|
||||
|
||||
def present(self, prior):
|
||||
"""Presents this context to the user."""
|
||||
|
||||
if self == prior or self._treatAsDuplicate(prior):
|
||||
msg = "MOUSE REVIEW: Not presenting due to no change"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return False
|
||||
|
||||
interrupt = self._obj and self._obj != prior._obj \
|
||||
or math.sqrt((self._x - prior._x)**2 + (self._y - prior._y)**2) > 25
|
||||
|
||||
if interrupt:
|
||||
self._script.presentationInterrupt()
|
||||
|
||||
if self._frame and self._frame != prior._frame:
|
||||
self._script.presentObject(self._frame,
|
||||
alreadyFocused=True,
|
||||
inMouseReview=True,
|
||||
interrupt=True)
|
||||
|
||||
if self._script.utilities.containsOnlyEOCs(self._obj):
|
||||
msg = "MOUSE REVIEW: Not presenting object which contains only EOCs"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return False
|
||||
|
||||
if self._obj and self._obj != prior._obj and not self._isInlineChild(prior):
|
||||
priorObj = prior._obj or self._getContainer()
|
||||
# TODO
|
||||
#cthulhu.emitRegionChanged(self._obj, mode=cthulhu.MOUSE_REVIEW)
|
||||
emitRegionChanged(self._obj, mode="mouse-review")
|
||||
|
||||
self._script.presentObject(self._obj, priorObj=priorObj, inMouseReview=True)
|
||||
if self._string.getString() == AXObject.get_name(self._obj):
|
||||
return True
|
||||
if not self._script.utilities.isEditableTextArea(self._obj):
|
||||
return True
|
||||
if AXUtilities.is_table_cell(self._obj) \
|
||||
and self._string.getString() == self._script.utilities.displayedText(self._obj):
|
||||
return True
|
||||
|
||||
if self._string != prior._string and self._string.present():
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class MouseReviewer:
|
||||
"""Main class for the mouse-review feature."""
|
||||
|
||||
def __init__(self):
|
||||
self._active = _settingsManager.getSetting("enableMouseReview")
|
||||
self._currentMouseOver = _ItemContext()
|
||||
self._pointer = None
|
||||
self._workspace = None
|
||||
self._windows = []
|
||||
self._all_windows = []
|
||||
self._handlerIds = {}
|
||||
self._eventListener = Atspi.EventListener.new(self._listener)
|
||||
self.inMouseEvent = False
|
||||
self._handlers = self._setup_handlers()
|
||||
self._bindings = self._setup_bindings()
|
||||
|
||||
if not _mouseReviewCapable:
|
||||
msg = "MOUSE REVIEW ERROR: Wnck is not available"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return
|
||||
|
||||
display = Gdk.Display.get_default()
|
||||
try:
|
||||
seat = Gdk.Display.get_default_seat(display)
|
||||
self._pointer = seat.get_pointer()
|
||||
except AttributeError:
|
||||
msg = "MOUSE REVIEW ERROR: Gtk+ 3.20 is not available"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return
|
||||
except Exception:
|
||||
msg = "MOUSE REVIEW ERROR: Exception getting pointer for default seat."
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return
|
||||
|
||||
if not self._pointer:
|
||||
msg = "MOUSE REVIEW ERROR: No pointer for default seat."
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return
|
||||
|
||||
if not self._active:
|
||||
return
|
||||
|
||||
self.activate()
|
||||
|
||||
def get_bindings(self):
|
||||
"""Returns the mouse-review keybindings."""
|
||||
|
||||
return self._bindings
|
||||
|
||||
def get_handlers(self):
|
||||
"""Returns the mouse-review handlers."""
|
||||
|
||||
return self._handlers
|
||||
|
||||
def _setup_handlers(self):
|
||||
"""Sets up and returns the mouse-review input event handlers."""
|
||||
|
||||
handlers = {}
|
||||
|
||||
handlers["toggleMouseReviewHandler"] = \
|
||||
input_event.InputEventHandler(
|
||||
self.toggle,
|
||||
cmdnames.MOUSE_REVIEW_TOGGLE)
|
||||
|
||||
return handlers
|
||||
|
||||
def _setup_bindings(self):
|
||||
"""Sets up and returns the mouse-review key bindings."""
|
||||
|
||||
bindings = keybindings.KeyBindings()
|
||||
|
||||
bindings.add(
|
||||
keybindings.KeyBinding(
|
||||
"",
|
||||
keybindings.defaultModifierMask,
|
||||
keybindings.NO_MODIFIER_MASK,
|
||||
self._handlers.get("toggleMouseReviewHandler")))
|
||||
|
||||
return bindings
|
||||
|
||||
def activate(self):
|
||||
"""Activates mouse review."""
|
||||
|
||||
if not _mouseReviewCapable:
|
||||
msg = "MOUSE REVIEW ERROR: Wnck is not available"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return
|
||||
|
||||
# Set up the initial object as the one with the focus to avoid
|
||||
# presenting irrelevant info the first time.
|
||||
obj = cthulhu_state.locusOfFocus
|
||||
script = None
|
||||
frame = None
|
||||
if obj:
|
||||
script = _scriptManager.getScript(AXObject.get_application(obj), obj)
|
||||
if script:
|
||||
frame = script.utilities.topLevelObject(obj)
|
||||
self._currentMouseOver = _ItemContext(obj=obj, frame=frame, script=script)
|
||||
|
||||
self._eventListener.register("mouse:abs")
|
||||
screen = Wnck.Screen.get_default()
|
||||
if screen:
|
||||
# On first startup windows and workspace are likely to be None,
|
||||
# but the signals we connect to will get emitted when proper values
|
||||
# become available; but in case we got disabled and re-enabled we
|
||||
# have to get the initial values manually.
|
||||
stacked = screen.get_windows_stacked()
|
||||
if stacked:
|
||||
stacked.reverse()
|
||||
self._all_windows = stacked
|
||||
self._workspace = screen.get_active_workspace()
|
||||
if self._workspace:
|
||||
self._update_workspace_windows()
|
||||
|
||||
i = screen.connect("window-stacking-changed", self._on_stacking_changed)
|
||||
self._handlerIds[i] = screen
|
||||
i = screen.connect("active-workspace-changed", self._on_workspace_changed)
|
||||
self._handlerIds[i] = screen
|
||||
|
||||
self._active = True
|
||||
|
||||
def deactivate(self):
|
||||
"""Deactivates mouse review."""
|
||||
|
||||
self._eventListener.deregister("mouse:abs")
|
||||
for key, value in self._handlerIds.items():
|
||||
value.disconnect(key)
|
||||
self._handlerIds = {}
|
||||
self._workspace = None
|
||||
self._windows = []
|
||||
self._all_windows = []
|
||||
|
||||
self._active = False
|
||||
|
||||
def getCurrentItem(self):
|
||||
"""Returns the accessible object being reviewed."""
|
||||
|
||||
if not _mouseReviewCapable:
|
||||
return None
|
||||
|
||||
if not self._active:
|
||||
return None
|
||||
|
||||
obj = self._currentMouseOver.getObject()
|
||||
|
||||
if time.time() - self._currentMouseOver.getTime() > 0.1:
|
||||
msg = f"MOUSE REVIEW: Treating {obj} as stale"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return None
|
||||
|
||||
return obj
|
||||
|
||||
def toggle(self, script=None, event=None):
|
||||
"""Toggle mouse reviewing on or off."""
|
||||
|
||||
if not _mouseReviewCapable:
|
||||
return
|
||||
|
||||
self._active = not self._active
|
||||
_settingsManager.setSetting("enableMouseReview", self._active)
|
||||
|
||||
if not self._active:
|
||||
self.deactivate()
|
||||
msg = messages.MOUSE_REVIEW_DISABLED
|
||||
else:
|
||||
self.activate()
|
||||
msg = messages.MOUSE_REVIEW_ENABLED
|
||||
|
||||
if cthulhu_state.activeScript:
|
||||
cthulhu_state.activeScript.presentMessage(msg)
|
||||
|
||||
def _update_workspace_windows(self):
|
||||
self._windows = [w for w in self._all_windows
|
||||
if w.is_on_workspace(self._workspace)]
|
||||
|
||||
def _on_stacking_changed(self, screen):
|
||||
"""Callback for Wnck's window-stacking-changed signal."""
|
||||
|
||||
stacked = screen.get_windows_stacked()
|
||||
stacked.reverse()
|
||||
self._all_windows = stacked
|
||||
if self._workspace:
|
||||
self._update_workspace_windows()
|
||||
|
||||
def _on_workspace_changed(self, screen, prev_ws=None):
|
||||
"""Callback for Wnck's active-workspace-changed signal."""
|
||||
|
||||
self._workspace = screen.get_active_workspace()
|
||||
self._update_workspace_windows()
|
||||
|
||||
def _contains_point(self, obj, x, y, coordType=None):
|
||||
if coordType is None:
|
||||
coordType = Atspi.CoordType.SCREEN
|
||||
|
||||
try:
|
||||
return obj.queryComponent().contains(x, y, coordType)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _has_bounds(self, obj, bounds, coordType=None):
|
||||
"""Returns True if the bounding box of obj is bounds."""
|
||||
|
||||
if coordType is None:
|
||||
coordType = Atspi.CoordType.SCREEN
|
||||
|
||||
try:
|
||||
extents = obj.queryComponent().getExtents(coordType)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return list(extents) == list(bounds)
|
||||
|
||||
def _accessible_window_at_point(self, pX, pY):
|
||||
"""Returns the accessible window at the specified coordinates."""
|
||||
|
||||
window = None
|
||||
for w in self._windows:
|
||||
if w.is_minimized():
|
||||
continue
|
||||
|
||||
x, y, width, height = w.get_geometry()
|
||||
if x <= pX <= x + width and y <= pY <= y + height:
|
||||
window = w
|
||||
break
|
||||
|
||||
if not window:
|
||||
return None
|
||||
|
||||
windowApp = window.get_application()
|
||||
if not windowApp:
|
||||
return None
|
||||
|
||||
app = AXUtilities.get_application_with_pid(windowApp.get_pid())
|
||||
if not app:
|
||||
return None
|
||||
|
||||
candidates = [o for o in AXObject.iter_children(
|
||||
app, lambda x: self._contains_point(x, pX, pY))]
|
||||
if len(candidates) == 1:
|
||||
return candidates[0]
|
||||
|
||||
name = window.get_name()
|
||||
matches = [o for o in candidates if AXObject.get_name(o) == name]
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
|
||||
bbox = window.get_client_window_geometry()
|
||||
matches = [o for o in candidates if self._has_bounds(o, bbox)]
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
|
||||
return None
|
||||
|
||||
def _on_mouse_moved(self, event):
|
||||
"""Callback for mouse:abs events."""
|
||||
|
||||
screen, pX, pY = self._pointer.get_position()
|
||||
window = self._accessible_window_at_point(pX, pY)
|
||||
msg = "MOUSE REVIEW: Window at (%i, %i) is %s" % (pX, pY, window)
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
if not window:
|
||||
return
|
||||
|
||||
script = _scriptManager.getScript(AXObject.get_application(window))
|
||||
if not script:
|
||||
return
|
||||
|
||||
if script.utilities.isDead(cthulhu_state.locusOfFocus):
|
||||
menu = None
|
||||
elif AXUtilities.is_menu(cthulhu_state.locusOfFocus):
|
||||
menu = cthulhu_state.locusOfFocus
|
||||
else:
|
||||
menu = AXObject.find_ancestor(cthulhu_state.locusOfFocus, AXUtilities.is_menu)
|
||||
|
||||
screen, nowX, nowY = self._pointer.get_position()
|
||||
if (pX, pY) != (nowX, nowY):
|
||||
msg = "MOUSE REVIEW: Pointer moved again: (%i, %i)" % (nowX, nowY)
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return
|
||||
|
||||
obj = script.utilities.descendantAtPoint(menu, pX, pY) \
|
||||
or script.utilities.descendantAtPoint(window, pX, pY)
|
||||
msg = "MOUSE REVIEW: Object at (%i, %i) is %s" % (pX, pY, obj)
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
script = _scriptManager.getScript(AXObject.get_application(window), obj)
|
||||
if menu and obj and not AXObject.find_ancestor(obj, AXUtilities.is_menu):
|
||||
if script.utilities.intersectingRegion(obj, menu) != (0, 0, 0, 0):
|
||||
msg = f"MOUSE REVIEW: {obj} believed to be under {menu}"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return
|
||||
|
||||
objDocument = script.utilities.getTopLevelDocumentForObject(obj)
|
||||
if objDocument and script.utilities.inDocumentContent():
|
||||
document = script.utilities.activeDocument()
|
||||
if document != objDocument:
|
||||
msg = f"MOUSE REVIEW: {obj} is not in active document {document}"
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return
|
||||
|
||||
screen, nowX, nowY = self._pointer.get_position()
|
||||
if (pX, pY) != (nowX, nowY):
|
||||
msg = "MOUSE REVIEW: Pointer moved again: (%i, %i)" % (nowX, nowY)
|
||||
debug.println(debug.LEVEL_INFO, msg, True)
|
||||
return
|
||||
|
||||
boundary = None
|
||||
x, y, width, height = self._currentMouseOver.getBoundingBox()
|
||||
if y <= pY <= y + height and self._currentMouseOver.getString():
|
||||
boundary = Atspi.TextBoundaryType.WORD_START
|
||||
elif obj == self._currentMouseOver.getObject():
|
||||
boundary = Atspi.TextBoundaryType.LINE_START
|
||||
elif AXUtilities.is_selectable(obj):
|
||||
boundary = Atspi.TextBoundaryType.LINE_START
|
||||
elif script.utilities.isMultiParagraphObject(obj):
|
||||
boundary = Atspi.TextBoundaryType.LINE_START
|
||||
|
||||
new = _ItemContext(pX, pY, obj, boundary, window, script)
|
||||
if new.present(self._currentMouseOver):
|
||||
self._currentMouseOver = new
|
||||
|
||||
def _listener(self, event):
|
||||
"""Generic listener, mainly to output debugging info."""
|
||||
|
||||
startTime = time.time()
|
||||
msg = f"\nvvvvv PROCESS OBJECT EVENT {event.type} vvvvv"
|
||||
debug.println(debug.LEVEL_INFO, msg, False)
|
||||
|
||||
if event.type.startswith("mouse:abs"):
|
||||
self.inMouseEvent = True
|
||||
self._on_mouse_moved(event)
|
||||
self.inMouseEvent = False
|
||||
|
||||
msg = f"TOTAL PROCESSING TIME: {time.time() - startTime:.4f}\n"
|
||||
msg += f"^^^^^ PROCESS OBJECT EVENT {event.type} ^^^^^\n"
|
||||
debug.println(debug.LEVEL_INFO, msg, False)
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
cthulhu_python_PYTHON = \
|
||||
__init__.py \
|
||||
PluginManager.plugin \
|
||||
PluginManager.py \
|
||||
PluginManagerUi.py
|
||||
|
||||
cthulhu_pythondir=$(pkgpythondir)/plugins/PluginManager
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[Plugin]
|
||||
Module=PluginManager
|
||||
Loader=python3
|
||||
Name=Plugin Manager
|
||||
Description=Activate and Deactivate plugins
|
||||
Authors=Chrys chrys@linux-a11y.org
|
||||
Website=
|
||||
Version=1.0
|
||||
Copyright=
|
||||
Builtin=true
|
||||
Hidden=true
|
||||
Depends=
|
||||
Icon=
|
||||
Help=
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
from cthulhu import plugin
|
||||
|
||||
import gi
|
||||
gi.require_version('Peas', '1.0')
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Peas
|
||||
|
||||
import PluginManagerUi
|
||||
|
||||
class PluginManager(GObject.Object, Peas.Activatable, plugin.Plugin):
|
||||
#__gtype_name__ = 'PluginManager'
|
||||
|
||||
object = GObject.Property(type=GObject.Object)
|
||||
def __init__(self):
|
||||
plugin.Plugin.__init__(self)
|
||||
self.pluginManagerUi = None
|
||||
def do_activate(self):
|
||||
API = self.object
|
||||
self.registerGestureByString(self.startPluginManagerUi, _('plugin manager'), 'kb:cthulhu+e')
|
||||
|
||||
def do_deactivate(self):
|
||||
API = self.object
|
||||
|
||||
def startPluginManagerUi(self, script=None, inputEvent=None):
|
||||
self.showUI()
|
||||
return True
|
||||
def showUI(self):
|
||||
API = self.object
|
||||
if self.pluginManagerUi == None:
|
||||
self.pluginManagerUi = PluginManagerUi.PluginManagerUi(API.app)
|
||||
self.pluginManagerUi.setTranslationContext(self.getTranslationContext())
|
||||
self.pluginManagerUi.createUI()
|
||||
self.pluginManagerUi.run()
|
||||
self.pluginManagerUi = None
|
||||
else:
|
||||
self.pluginManagerUi.present()
|
||||
@@ -1,307 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
class PluginManagerUi(Gtk.ApplicationWindow):
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs, title=_("Cthulhu Plugin Manager"))
|
||||
self.app = app
|
||||
self.translationContext = None
|
||||
self.connect("destroy", self._onCancelButtonClicked)
|
||||
self.connect('key-press-event', self._onKeyPressWindow)
|
||||
def createUI(self):
|
||||
self.set_default_size(650, 650)
|
||||
self.set_position(Gtk.WindowPosition.CENTER_ALWAYS)
|
||||
|
||||
# pluginInfo (object) = 0
|
||||
# name (str) = 1
|
||||
# active (bool) = 2
|
||||
# buildIn (bool) = 3
|
||||
# dataDir (str) = 4
|
||||
# moduleDir (str) = 5
|
||||
# dependencies (object) = 6
|
||||
# moduleName (str) = 7
|
||||
# description (str) = 8
|
||||
# authors (object) = 9
|
||||
# website (str) = 10
|
||||
# copyright (str) = 11
|
||||
# version (str) = 12
|
||||
# helpUri (str) = 13
|
||||
# iconName (str) = 14
|
||||
self.listStore = Gtk.ListStore(object,str, bool, bool, str, str,object,str,str,object,str,str,str,str,str)
|
||||
|
||||
self.treeView = Gtk.TreeView(model=self.listStore)
|
||||
self.treeView.connect("row-activated", self._rowActivated)
|
||||
self.treeView.connect('key-press-event', self._onKeyPressTreeView)
|
||||
|
||||
self.rendererText = Gtk.CellRendererText()
|
||||
self.columnText = Gtk.TreeViewColumn(_("Name"), self.rendererText, text=1)
|
||||
self.treeView.append_column(self.columnText)
|
||||
|
||||
self.rendererToggle = Gtk.CellRendererToggle()
|
||||
self.rendererToggle.connect("toggled", self._onCellToggled)
|
||||
|
||||
self.columnToggle = Gtk.TreeViewColumn(_("Active"), self.rendererToggle, active=2)
|
||||
self.treeView.append_column(self.columnToggle)
|
||||
|
||||
|
||||
self.buttomBox = Gtk.Box(spacing=6)
|
||||
self.mainVBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
self.mainVBox.pack_start(self.treeView, True, True, 0)
|
||||
self.mainVBox.pack_start(self.buttomBox, False, True, 0)
|
||||
|
||||
self.add(self.mainVBox)
|
||||
self.oKButton = Gtk.Button.new_with_mnemonic(_("_Details"))
|
||||
self.oKButton.connect("clicked", self._onDetailsButtonClicked)
|
||||
self.buttomBox.pack_start(self.oKButton, True, True, 0)
|
||||
|
||||
self.oKButton = Gtk.Button.new_with_mnemonic(_("_OK"))
|
||||
self.oKButton.connect("clicked", self._onOkButtonClicked)
|
||||
self.buttomBox.pack_start(self.oKButton, True, True, 0)
|
||||
|
||||
self.applyButton = Gtk.Button.new_with_mnemonic(_("_Apply"))
|
||||
self.applyButton.connect("clicked", self._onApplyButtonClicked)
|
||||
self.buttomBox.pack_start(self.applyButton, True, True, 0)
|
||||
|
||||
self.applyButton = Gtk.Button.new_with_mnemonic(_("_Install"))
|
||||
self.applyButton.connect("clicked", self._onInstallButtonClicked)
|
||||
self.buttomBox.pack_start(self.applyButton, True, True, 0)
|
||||
|
||||
self.applyButton = Gtk.Button.new_with_mnemonic(_("_Uninstall"))
|
||||
self.applyButton.connect("clicked", self._onUninstallButtonClicked)
|
||||
self.buttomBox.pack_start(self.applyButton, True, True, 0)
|
||||
|
||||
self.cancelButton = Gtk.Button.new_with_mnemonic(_("_Cancel"))
|
||||
self.cancelButton.connect("clicked", self._onCancelButtonClicked)
|
||||
self.buttomBox.pack_start(self.cancelButton, True, True, 0)
|
||||
def setTranslationContext(self, translationContext):
|
||||
self.translationContext = translationContext
|
||||
global _
|
||||
_ = translationContext.gettext
|
||||
def closeWindow(self):
|
||||
Gtk.main_quit()
|
||||
def uninstallPlugin(self):
|
||||
selection = self.treeView.get_selection()
|
||||
model, list_iter = selection.get_selected()
|
||||
try:
|
||||
if model.get_value(list_iter,0):
|
||||
pluginInfo = model.get_value(list_iter,0)
|
||||
pluginName = self.app.getPluginSystemManager().getPluginName(pluginInfo)
|
||||
dialog = Gtk.MessageDialog(None,
|
||||
Gtk.DialogFlags.MODAL,
|
||||
type=Gtk.MessageType.INFO,
|
||||
buttons=Gtk.ButtonsType.YES_NO)
|
||||
|
||||
dialog.set_markup("<b>%s</b>" % _('Remove Plugin {}?').format(pluginName))
|
||||
dialog.format_secondary_markup(_('Do you really want to remove Plugin {}?').format(pluginName))
|
||||
response = dialog.run()
|
||||
dialog.destroy()
|
||||
if response != Gtk.ResponseType.YES:
|
||||
return
|
||||
self.app.getPluginSystemManager().uninstallPlugin(model.get_value(list_iter,0))
|
||||
self.refreshPluginList()
|
||||
except:
|
||||
pass
|
||||
|
||||
def installPlugin(self):
|
||||
ok, filePath = self.chooseFile()
|
||||
if not ok:
|
||||
return
|
||||
self.app.getPluginSystemManager().installPlugin(filePath)
|
||||
self.refreshPluginList()
|
||||
|
||||
def _onKeyPressWindow(self, _, event):
|
||||
_, key_val = event.get_keyval()
|
||||
if key_val == Gdk.KEY_Escape:
|
||||
self.closeWindow()
|
||||
def _onKeyPressTreeView(self, _, event):
|
||||
_, key_val = event.get_keyval()
|
||||
if key_val == Gdk.KEY_Return:
|
||||
self.applySettings()
|
||||
self.closeWindow()
|
||||
if key_val == Gdk.KEY_Escape:
|
||||
self.closeWindow()
|
||||
# CTRL + Q
|
||||
#modifiers = event.get_state()
|
||||
#if modifiers == Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD2_MASK:
|
||||
# if key_val == Gdk.KEY_q:
|
||||
# self._on_scan()
|
||||
def applySettings(self):
|
||||
for row in self.listStore:
|
||||
pluginInfo = row[0]
|
||||
isActive = row[2]
|
||||
self.app.getPluginSystemManager().setPluginActive(pluginInfo, isActive)
|
||||
gsettingsManager = self.app.getGsettingsManager()
|
||||
gsettingsManager.set_settings_value_list('active-plugins', self.app.getPluginSystemManager().getActivePlugins())
|
||||
|
||||
self.app.getPluginSystemManager().syncAllPluginsActive()
|
||||
self.refreshPluginList()
|
||||
|
||||
|
||||
def _rowActivated(self, tree_view, path, column):
|
||||
print('rowActivated')
|
||||
def showDetails(self):
|
||||
selection = self.treeView.get_selection()
|
||||
model, list_iter = selection.get_selected()
|
||||
try:
|
||||
if model.get_value(list_iter,0):
|
||||
pluginInfo = model.get_value(list_iter,0)
|
||||
name = self.app.getPluginSystemManager().getPluginName(pluginInfo)
|
||||
description = self.app.getPluginSystemManager().getPluginDescription(pluginInfo)
|
||||
authors = self.app.getPluginSystemManager().getPluginAuthors(pluginInfo)
|
||||
website =self.app.getPluginSystemManager().getPluginWebsite(pluginInfo)
|
||||
copyright = self.app.getPluginSystemManager().getPluginCopyright(pluginInfo)
|
||||
license = '' #self.app.getPluginSystemManager().getPluginName(pluginInfo)
|
||||
version = self.app.getPluginSystemManager().getPluginVersion(pluginInfo)
|
||||
dialog = Gtk.AboutDialog(self)
|
||||
dialog.set_authors(authors)
|
||||
dialog.set_website(website)
|
||||
dialog.set_copyright(copyright)
|
||||
dialog.set_license(license)
|
||||
dialog.set_version(version)
|
||||
dialog.set_program_name(name)
|
||||
dialog.set_comments(description)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
except:
|
||||
pass
|
||||
|
||||
def _onDetailsButtonClicked(self, widget):
|
||||
self.showDetails()
|
||||
|
||||
def _onOkButtonClicked(self, widget):
|
||||
self.applySettings()
|
||||
self.closeWindow()
|
||||
def _onApplyButtonClicked(self, widget):
|
||||
self.applySettings()
|
||||
def _onInstallButtonClicked(self, widget):
|
||||
self.installPlugin()
|
||||
def _onUninstallButtonClicked(self, widget):
|
||||
self.uninstallPlugin()
|
||||
def _onCancelButtonClicked(self, widget):
|
||||
self.closeWindow()
|
||||
def refreshPluginList(self):
|
||||
self.clearPluginList()
|
||||
pluginList = self.app.getPluginSystemManager().plugins
|
||||
for pluginInfo in pluginList:
|
||||
self.addPlugin(pluginInfo)
|
||||
def clearPluginList(self):
|
||||
self.listStore.clear()
|
||||
|
||||
def addPlugin(self, pluginInfo):
|
||||
ignoredPlugins = self.app.getPluginSystemManager().getIgnoredPlugins()
|
||||
moduleDir = self.app.getPluginSystemManager().getPluginModuleDir(pluginInfo)
|
||||
if moduleDir in ignoredPlugins:
|
||||
return
|
||||
|
||||
hidden = self.app.getPluginSystemManager().isPluginHidden(pluginInfo)
|
||||
if hidden:
|
||||
return
|
||||
|
||||
moduleName = self.app.getPluginSystemManager().getPluginModuleName(pluginInfo)
|
||||
name = self.app.getPluginSystemManager().getPluginName(pluginInfo)
|
||||
version = self.app.getPluginSystemManager().getPluginVersion(pluginInfo)
|
||||
website = self.app.getPluginSystemManager().getPluginWebsite(pluginInfo)
|
||||
authors = self.app.getPluginSystemManager().getPluginAuthors(pluginInfo)
|
||||
buildIn = self.app.getPluginSystemManager().isPluginBuildIn(pluginInfo)
|
||||
description = self.app.getPluginSystemManager().getPluginDescription(pluginInfo)
|
||||
iconName = self.app.getPluginSystemManager().getPluginIconName(pluginInfo)
|
||||
copyright = self.app.getPluginSystemManager().getPluginCopyright(pluginInfo)
|
||||
dependencies = self.app.getPluginSystemManager().getPluginDependencies(pluginInfo)
|
||||
|
||||
#settings = self.app.getPluginSystemManager().getPluginSettings(pluginInfo)
|
||||
#hasDependencies = self.app.getPluginSystemManager().hasPluginDependency(pluginInfo)
|
||||
loaded = self.app.getPluginSystemManager().isPluginLoaded(pluginInfo)
|
||||
available = self.app.getPluginSystemManager().isPluginAvailable(pluginInfo)
|
||||
active = self.app.getPluginSystemManager().isPluginActive(pluginInfo)
|
||||
|
||||
#externalData = self.app.getPluginSystemManager().getPluginExternalData(pluginInfo)
|
||||
helpUri = self.app.getPluginSystemManager().getPlugingetHelpUri(pluginInfo)
|
||||
dataDir = self.app.getPluginSystemManager().getPluginDataDir(pluginInfo)
|
||||
|
||||
# pluginInfo (object) = 0
|
||||
# name (str) = 1
|
||||
# active (bool) = 2
|
||||
# buildIn (bool) = 3
|
||||
# dataDir (str) = 4
|
||||
# moduleDir (str) = 5
|
||||
# dependencies (object) = 6
|
||||
# moduleName (str) = 7
|
||||
# description (str) = 8
|
||||
# authors (object) = 9
|
||||
# website (str) = 10
|
||||
# copyright (str) = 11
|
||||
# version (str) = 12
|
||||
# helpUri (str) = 13
|
||||
# iconName (str) = 14
|
||||
self.listStore.append([pluginInfo, name, active, buildIn, dataDir, moduleDir, dependencies, moduleName, description, authors, website, copyright, version, helpUri, iconName])
|
||||
def chooseFile(self):
|
||||
dialog = Gtk.FileChooserDialog(
|
||||
title=_("Please choose a file"), parent=self, action=Gtk.FileChooserAction.OPEN
|
||||
)
|
||||
dialog.add_buttons(
|
||||
Gtk.STOCK_CANCEL,
|
||||
Gtk.ResponseType.CANCEL,
|
||||
Gtk.STOCK_OPEN,
|
||||
Gtk.ResponseType.OK,
|
||||
)
|
||||
|
||||
filter_plugin = Gtk.FileFilter()
|
||||
filter_plugin.set_name(_("Plugin Archive"))
|
||||
filter_plugin.add_mime_type("application/gzip")
|
||||
dialog.add_filter(filter_plugin)
|
||||
|
||||
response = dialog.run()
|
||||
filePath = ''
|
||||
ok = False
|
||||
|
||||
if response == Gtk.ResponseType.OK:
|
||||
ok = True
|
||||
filePath = dialog.get_filename()
|
||||
|
||||
dialog.destroy()
|
||||
return ok, filePath
|
||||
def _onCellToggled(self, widget, path):
|
||||
|
||||
self.listStore[path][2] = not self.listStore[path][2]
|
||||
def present(self):
|
||||
cthulhu_state = self.app.getDynamicApiManager().getAPI('CthulhuState')
|
||||
ts = 0
|
||||
try:
|
||||
ts = cthulhu_state.lastInputEvent.timestamp
|
||||
except:
|
||||
pass
|
||||
if ts == 0:
|
||||
ts = Gtk.get_current_event_time()
|
||||
self.present_with_time(ts)
|
||||
def run(self):
|
||||
self.refreshPluginList()
|
||||
self.present()
|
||||
self.show_all()
|
||||
Gtk.main()
|
||||
self.destroy()
|
||||
@@ -1,107 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
class ListBoxRowWithData(Gtk.ListBoxRow):
|
||||
def __init__(self, data):
|
||||
super(Gtk.ListBoxRow, self).__init__()
|
||||
self.data = data
|
||||
self.add(Gtk.Label(label=data))
|
||||
|
||||
class PluginManagerUi(Gtk.Window):
|
||||
def __init__(self):
|
||||
Gtk.Window.__init__(self)
|
||||
self.pluginList = []
|
||||
self.set_default_size(200, -1)
|
||||
self.connect("destroy", Gtk.main_quit)
|
||||
self.listBox = Gtk.ListBox()
|
||||
|
||||
self.buttomBox = Gtk.Box(spacing=6)
|
||||
self.mainVBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
|
||||
self.mainVBox.pack_start(self.listBox, True, True, 0)
|
||||
self.mainVBox.pack_start(self.buttomBox, True, True, 0)
|
||||
|
||||
self.add(self.mainVBox)
|
||||
|
||||
self.oKButton = Gtk.Button(label="OK")
|
||||
self.oKButton.connect("clicked", self.on_oKButton_clicked)
|
||||
self.buttomBox.pack_start(self.oKButton, True, True, 0)
|
||||
|
||||
self.applyButton = Gtk.Button(label="Apply")
|
||||
self.applyButton.connect("clicked", self.on_applyButton_clicked)
|
||||
self.buttomBox.pack_start(self.applyButton, True, True, 0)
|
||||
|
||||
self.cancelButton = Gtk.Button(label="Cancel")
|
||||
self.cancelButton.connect("clicked", self.on_cancelButton_clicked)
|
||||
self.buttomBox.pack_start(self.cancelButton, True, True, 0)
|
||||
|
||||
self.listBox.connect("row-activated", self.on_row_activated)
|
||||
|
||||
def on_row_activated(self, listBox, listboxrow):
|
||||
print("Row %i activated" % (listboxrow.get_index()))
|
||||
|
||||
def on_oKButton_clicked(self, widget):
|
||||
print("OK")
|
||||
|
||||
def on_applyButton_clicked(self, widget):
|
||||
print("Apply")
|
||||
|
||||
def on_cancelButton_clicked(self, widget):
|
||||
print("Cancel")
|
||||
|
||||
|
||||
def addPlugin(self, Name, Active, Description = ''):
|
||||
self.pluginList.append([Name, Active, Description])
|
||||
|
||||
def run(self):
|
||||
for plugin in self.pluginList:
|
||||
print(plugin)
|
||||
box = Gtk.Box(spacing=10)
|
||||
pluginNameLabel = Gtk.Label(plugin[0])
|
||||
#pluginActiveCheckButton = Gtk.CheckButton(label="_Active", use_underline=True)
|
||||
#pluginActiveCheckButton.set_active(plugin[1])
|
||||
pluginActiveSwitch = Gtk.Switch()
|
||||
pluginActiveSwitch.set_active(plugin[1])
|
||||
|
||||
|
||||
pluginDescriptionLabel = Gtk.Label(plugin[2])
|
||||
|
||||
box.pack_start(pluginNameLabel, True, True, 0)
|
||||
box.pack_start(pluginActiveSwitch, True, True, 0)
|
||||
box.pack_start(pluginDescriptionLabel, True, True, 0)
|
||||
|
||||
self.listBox.add(box)
|
||||
self.show_all()
|
||||
Gtk.main()
|
||||
|
||||
if __name__ == "__main__":
|
||||
ui = PluginManagerUi()
|
||||
ui.addPlugin('plugin1', True, 'bla')
|
||||
ui.addPlugin('plugin2', True, 'bla')
|
||||
ui.addPlugin('plugin3', True, 'bla')
|
||||
ui.run()
|
||||
@@ -1,117 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class ListBoxRowWithData(Gtk.ListBoxRow):
|
||||
def __init__(self, data):
|
||||
super(Gtk.ListBoxRow, self).__init__()
|
||||
self.data = data
|
||||
self.add(Gtk.Label(label=data))
|
||||
|
||||
|
||||
class ListBoxWindow(Gtk.Window):
|
||||
def __init__(self):
|
||||
Gtk.Window.__init__(self, title="ListBox Demo")
|
||||
self.set_border_width(10)
|
||||
|
||||
box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
self.add(box_outer)
|
||||
|
||||
listbox = Gtk.ListBox()
|
||||
listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
box_outer.pack_start(listbox, True, True, 0)
|
||||
|
||||
row = Gtk.ListBoxRow()
|
||||
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
||||
row.add(hbox)
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
hbox.pack_start(vbox, True, True, 0)
|
||||
|
||||
label1 = Gtk.Label(label="Automatic Date & Time", xalign=0)
|
||||
label2 = Gtk.Label(label="Requires internet access", xalign=0)
|
||||
vbox.pack_start(label1, True, True, 0)
|
||||
vbox.pack_start(label2, True, True, 0)
|
||||
|
||||
switch = Gtk.Switch()
|
||||
switch.props.valign = Gtk.Align.CENTER
|
||||
hbox.pack_start(switch, False, True, 0)
|
||||
|
||||
listbox.add(row)
|
||||
|
||||
row = Gtk.ListBoxRow()
|
||||
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
||||
row.add(hbox)
|
||||
label = Gtk.Label(label="Enable Automatic Update", xalign=0)
|
||||
check = Gtk.CheckButton()
|
||||
hbox.pack_start(label, True, True, 0)
|
||||
hbox.pack_start(check, False, True, 0)
|
||||
|
||||
listbox.add(row)
|
||||
|
||||
row = Gtk.ListBoxRow()
|
||||
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
|
||||
row.add(hbox)
|
||||
label = Gtk.Label(label="Date Format", xalign=0)
|
||||
combo = Gtk.ComboBoxText()
|
||||
combo.insert(0, "0", "24-hour")
|
||||
combo.insert(1, "1", "AM/PM")
|
||||
hbox.pack_start(label, True, True, 0)
|
||||
hbox.pack_start(combo, False, True, 0)
|
||||
|
||||
listbox.add(row)
|
||||
|
||||
listbox_2 = Gtk.ListBox()
|
||||
items = "This is a sorted ListBox Fail".split()
|
||||
|
||||
for item in items:
|
||||
listbox_2.add(ListBoxRowWithData(item))
|
||||
|
||||
def sort_func(row_1, row_2, data, notify_destroy):
|
||||
return row_1.data.lower() > row_2.data.lower()
|
||||
|
||||
def filter_func(row, data, notify_destroy):
|
||||
return False if row.data == "Fail" else True
|
||||
|
||||
listbox_2.set_sort_func(sort_func, None, False)
|
||||
listbox_2.set_filter_func(filter_func, None, False)
|
||||
|
||||
def on_row_activated(listbox_widget, row):
|
||||
print(row.data)
|
||||
|
||||
listbox_2.connect("row-activated", on_row_activated)
|
||||
|
||||
box_outer.pack_start(listbox_2, True, True, 0)
|
||||
listbox_2.show_all()
|
||||
|
||||
|
||||
win = ListBoxWindow()
|
||||
win.connect("destroy", Gtk.main_quit)
|
||||
win.show_all()
|
||||
Gtk.main()
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
cthulhu_python_PYTHON = \
|
||||
__init__.py \
|
||||
SimplePluginSystem.plugin \
|
||||
SimplePluginSystem.py
|
||||
plugin.info \
|
||||
plugin.py
|
||||
|
||||
cthulhu_pythondir=$(pkgpythondir)/plugins/SimplePluginSystem
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
[Plugin]
|
||||
Module=SimplePluginSystem
|
||||
Loader=python3
|
||||
Name=Simple Plugin System
|
||||
Description=Simple plugin system implementation for Cthulhu
|
||||
Authors=Chrys <chrys@linux-a11y.org>;Storm Dragon <storm_dragon@stormux.org>
|
||||
Copyright=Copyright Â2024 Chrys, Storm Dragon
|
||||
Website=https://git.stormux.org/storm/cthulhu
|
||||
Version=1.0
|
||||
Builtin=true
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
from cthulhu import plugin
|
||||
|
||||
from gi.repository import GObject, Peas
|
||||
import glob
|
||||
import os
|
||||
import importlib.util
|
||||
import random
|
||||
import string
|
||||
import _thread
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
settings = None
|
||||
speech = None
|
||||
braille = None
|
||||
input_event = None
|
||||
|
||||
def outputMessage( Message):
|
||||
if (settings.enableSpeech):
|
||||
speech.speak(Message)
|
||||
if (settings.enableBraille):
|
||||
braille.displayMessage(Message)
|
||||
|
||||
class SimplePluginSystem(GObject.Object, Peas.Activatable, plugin.Plugin):
|
||||
__gtype_name__ = 'SimplePluginSystem'
|
||||
object = GObject.Property(type=GObject.Object)
|
||||
|
||||
def __init__(self):
|
||||
plugin.Plugin.__init__(self)
|
||||
self.plugin_list = []
|
||||
self.loaded = False
|
||||
self.plugin_repo = os.path.expanduser('~') + "/.local/share/cthulhu/simple-plugins-enabled/"
|
||||
|
||||
def do_activate(self):
|
||||
API = self.object
|
||||
global settings
|
||||
global speech
|
||||
global braille
|
||||
global input_event
|
||||
settings = API.app.getDynamicApiManager().getAPI('Settings')
|
||||
speech = API.app.getDynamicApiManager().getAPI('Speech')
|
||||
braille = API.app.getDynamicApiManager().getAPI('Braille')
|
||||
input_event = API.app.getDynamicApiManager().getAPI('InputEvent')
|
||||
"""Required method for plugins"""
|
||||
if not self.loaded:
|
||||
self.load_plugins()
|
||||
|
||||
def do_deactivate(self):
|
||||
"""Required method for plugins"""
|
||||
# Remove all registered keybindings
|
||||
for plugin in self.plugin_list:
|
||||
self.unregisterShortcut(plugin['function'], plugin['shortcut'])
|
||||
self.loaded = False
|
||||
self.plugin_list = []
|
||||
|
||||
def SetupShortcutAndHandle(self, currPluginSetting):
|
||||
shortcut = ''
|
||||
# just the modifier
|
||||
if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
|
||||
shortcut = 'kb:cthulhu+' + currPluginSetting['key']
|
||||
# cthulhu + alt
|
||||
if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
|
||||
shortcut = 'kb:cthulhu+alt+' + currPluginSetting['key']
|
||||
# cthulhu + CTRL
|
||||
if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
|
||||
shortcut = 'kb:cthulhu+control+' + currPluginSetting['key']
|
||||
# cthulhu + alt + CTRL
|
||||
if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
|
||||
shortcut = 'kb:cthulhu+alt+control+ ' + currPluginSetting['key']
|
||||
# cthulhu + shift
|
||||
if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
|
||||
shortcut = 'kb:cthulhu+shift+' + currPluginSetting['key']
|
||||
# alt + shift
|
||||
if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
|
||||
shortcut = 'kb:alt+shift+' + currPluginSetting['key']
|
||||
if shortcut != '':
|
||||
print(shortcut)
|
||||
currPluginSetting['shortcut'] = shortcut
|
||||
self.registerGestureByString(currPluginSetting['function'], _(currPluginSetting['pluginname']), shortcut)
|
||||
return currPluginSetting
|
||||
|
||||
def id_generator(self, size=7, chars=string.ascii_letters):
|
||||
return ''.join(random.choice(chars) for _ in range(size))
|
||||
|
||||
def initSettings(self):
|
||||
currPluginSetting={
|
||||
'pluginname':'',
|
||||
'functionname':'',
|
||||
'key':'',
|
||||
'shiftkey':False,
|
||||
'ctrlkey':False,
|
||||
'altkey':False,
|
||||
'startnotify':False,
|
||||
'stopnotify':False,
|
||||
'blockcall':False,
|
||||
'error':False,
|
||||
'exec': False,
|
||||
'parameters':'',
|
||||
'function':None,
|
||||
'inputeventhandler':None,
|
||||
'valid':False,
|
||||
'supressoutput':False,
|
||||
'shortcut': ''
|
||||
}
|
||||
return currPluginSetting
|
||||
|
||||
def getPluginSettings(self, filepath, currPluginSetting):
|
||||
try:
|
||||
currPluginSetting['file'] = filepath
|
||||
fileName, fileExtension = os.path.splitext(filepath)
|
||||
if (fileExtension and (fileExtension != '')): #if there is an extension
|
||||
currPluginSetting['loadable'] = (fileExtension.lower() == '.py') # only python is loadable
|
||||
filename = os.path.basename(filepath) #filename
|
||||
filename = os.path.splitext(filename)[0] #remove extension if we have one
|
||||
#remove pluginname seperated by __-__
|
||||
filenamehelper = filename.split('__-__')
|
||||
filename = filenamehelper[len(filenamehelper) - 1 ]
|
||||
currPluginSetting['permission'] = os.access(filepath, os.X_OK )
|
||||
currPluginSetting['pluginname'] = 'NoNameAvailable'
|
||||
if len(filenamehelper) == 2:
|
||||
currPluginSetting['pluginname'] = filenamehelper[0]
|
||||
#now get shortcuts seperated by __+__
|
||||
filenamehelper = filename.split('__+__')
|
||||
if len([y for y in filenamehelper if 'parameters_' in y.lower()]) == 1 and\
|
||||
len([y for y in filenamehelper if 'parameters_' in y.lower()][0]) > 11:
|
||||
currPluginSetting['parameters'] = [y for y in filenamehelper if 'parameters_' in y.lower()][0][11:]
|
||||
if len([y for y in filenamehelper if 'key_' in y.lower()]) == 1 and\
|
||||
len([y for y in filenamehelper if 'key_' in y.lower()][0]) > 4 :
|
||||
currPluginSetting['key'] = [y for y in filenamehelper if 'key_' in y.lower()][0][4]
|
||||
if currPluginSetting['key'] == '':
|
||||
settcurrPluginSetting = 'shift' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['ctrlkey'] = 'control' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['altkey'] = 'alt' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['startnotify'] = 'startnotify' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['stopnotify'] = 'stopnotify' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['blockcall'] = 'blockcall' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['error'] = 'error' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['supressoutput'] = 'supressoutput' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['exec'] = 'exec' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['loadmodule'] = 'loadmodule' in map(str.lower, filenamehelper)
|
||||
currPluginSetting = self.readSettingsFromPlugin(currPluginSetting)
|
||||
if not currPluginSetting['loadmodule']:
|
||||
if not currPluginSetting['permission']: #subprocessing only works with exec permission
|
||||
return self.initSettings()
|
||||
if currPluginSetting['loadmodule'] and not currPluginSetting['loadable']: #sorry.. its not loadable only .py is loadable
|
||||
return self.initSettings()
|
||||
if (len(currPluginSetting['key']) > 1): #no shortcut
|
||||
if not currPluginSetting['exec']: # and no exec -> the plugin make no sense because it isnt hooked anywhere
|
||||
return self.initSettings() #so not load it (sets valid = False)
|
||||
else:
|
||||
currPluginSetting['key'] = '' #there is a strange key, but exec? ignore the key..
|
||||
currPluginSetting['valid'] = True # we could load everything
|
||||
return currPluginSetting
|
||||
except:
|
||||
return self.initSettings()
|
||||
|
||||
def readSettingsFromPlugin(self, currPluginSetting):
|
||||
if not os.access(currPluginSetting['file'], os.R_OK ):
|
||||
return currPluginSetting
|
||||
fileName, fileExtension = os.path.splitext(currPluginSetting['file'])
|
||||
if (fileExtension and (fileExtension != '')): #if there is an extension
|
||||
if (fileExtension.lower() != '.py') and \
|
||||
(fileExtension.lower() != '.sh'):
|
||||
return currPluginSetting
|
||||
else:
|
||||
return currPluginSetting
|
||||
|
||||
with open(currPluginSetting['file'], "r") as pluginFile:
|
||||
for line in pluginFile:
|
||||
currPluginSetting['shiftkey'] = ('sopsproperty:shift' in line.lower().replace(" ", "")) or currPluginSetting['shiftkey']
|
||||
currPluginSetting['ctrlkey'] = ('sopsproperty:control' in line.lower().replace(" ", "")) or currPluginSetting['ctrlkey']
|
||||
currPluginSetting['altkey'] = ('sopsproperty:alt' in line.lower().replace(" ", "")) or currPluginSetting['altkey']
|
||||
currPluginSetting['startnotify'] = ('sopsproperty:startnotify' in line.lower().replace(" ", "")) or currPluginSetting['startnotify']
|
||||
currPluginSetting['stopnotify'] = ('sopsproperty:stopnotify' in line.lower().replace(" ", "")) or currPluginSetting['stopnotify']
|
||||
currPluginSetting['blockcall'] = ('sopsproperty:blockcall' in line.lower().replace(" ", "")) or currPluginSetting['blockcall']
|
||||
currPluginSetting['error'] = ('sopsproperty:error' in line.lower().replace(" ", "")) or currPluginSetting['error']
|
||||
currPluginSetting['supressoutput'] = ('sopsproperty:supressoutput' in line.lower().replace(" ", "")) or currPluginSetting['supressoutput']
|
||||
currPluginSetting['exec'] = ('sopsproperty:exec' in line.lower().replace(" ", "")) or currPluginSetting['exec']
|
||||
currPluginSetting['loadmodule'] = ('sopsproperty:loadmodule' in line.lower().replace(" ", "")) or currPluginSetting['loadmodule']
|
||||
return currPluginSetting
|
||||
|
||||
def buildPluginSubprocess(self, currPluginSetting):
|
||||
currplugin = "\'\"" + currPluginSetting['file'] + "\" " + currPluginSetting['parameters'] + "\'"
|
||||
pluginname = currPluginSetting['pluginname']
|
||||
if currPluginSetting['blockcall']:
|
||||
pluginname = "blocking " + pluginname
|
||||
fun_body = "global " + currPluginSetting['functionname']+"\n"
|
||||
fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
|
||||
if currPluginSetting['startnotify']:
|
||||
fun_body +=" outputMessage('start " + pluginname + "')\n"
|
||||
fun_body +=" p = Popen(" + currplugin + ", stdout=PIPE, stderr=PIPE, shell=True)\n"
|
||||
fun_body +=" stdout, stderr = p.communicate()\n"
|
||||
fun_body +=" message = ''\n"
|
||||
fun_body +=" if not " + str(currPluginSetting['supressoutput']) + " and stdout:\n"
|
||||
fun_body +=" message += str(stdout, \"utf-8\")\n"
|
||||
fun_body +=" if " + str(currPluginSetting['error']) + " and stderr:\n"
|
||||
fun_body +=" message += ' error: ' + str(stderr, \"utf-8\")\n"
|
||||
fun_body +=" outputMessage( message)\n"
|
||||
if currPluginSetting['stopnotify']:
|
||||
fun_body +=" outputMessage('finish " + pluginname + "')\n"
|
||||
fun_body +=" return True\n\n"
|
||||
fun_body += "global " + currPluginSetting['functionname']+"T\n"
|
||||
fun_body +="def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
|
||||
fun_body +=" _thread.start_new_thread("+ currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
|
||||
return fun_body
|
||||
|
||||
def buildPluginExec(self, currPluginSetting):
|
||||
pluginname = currPluginSetting['pluginname']
|
||||
if currPluginSetting['blockcall']:
|
||||
pluginname = "blocking " + pluginname
|
||||
fun_body = "global " + currPluginSetting['functionname']+"\n"
|
||||
fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
|
||||
if currPluginSetting['startnotify']:
|
||||
fun_body +=" outputMessage('start " + pluginname + "')\n"
|
||||
fun_body += " try:\n"
|
||||
fun_body += " spec = importlib.util.spec_from_file_location(\"" + currPluginSetting['functionname'] + "\",\""+ currPluginSetting['file']+"\")\n"
|
||||
fun_body += " "+currPluginSetting['functionname'] + "Module = importlib.util.module_from_spec(spec)\n"
|
||||
fun_body += " spec.loader.exec_module(" + currPluginSetting['functionname'] + "Module)\n"
|
||||
fun_body += " except:\n"
|
||||
fun_body += " pass\n"
|
||||
if currPluginSetting['error']:
|
||||
fun_body += " outputMessage(\"Error while executing " + pluginname + "\")\n"
|
||||
if currPluginSetting['stopnotify']:
|
||||
fun_body +=" outputMessage('finish " + pluginname + "')\n"
|
||||
fun_body += " return True\n\n"
|
||||
fun_body += "global " + currPluginSetting['functionname']+"T\n"
|
||||
fun_body +="def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
|
||||
fun_body +=" _thread.start_new_thread("+ currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
|
||||
return fun_body
|
||||
|
||||
def getFunctionName(self, currPluginSetting):
|
||||
currPluginSetting['functionname'] = ''
|
||||
while currPluginSetting['functionname'] == '' or currPluginSetting['functionname'] + 'T' in globals() or currPluginSetting['functionname'] in globals():
|
||||
currPluginSetting['functionname'] = self.id_generator()
|
||||
return currPluginSetting
|
||||
|
||||
def load_plugins(self):
|
||||
if not self.loaded:
|
||||
self.plugin_list = glob.glob(self.plugin_repo+'*')
|
||||
for currplugin in self.plugin_list:
|
||||
currPluginSetting = self.initSettings()
|
||||
currPluginSetting = self.getPluginSettings(currplugin, currPluginSetting)
|
||||
|
||||
if not currPluginSetting['valid']:
|
||||
continue
|
||||
|
||||
currPluginSetting = self.getFunctionName(currPluginSetting)
|
||||
|
||||
if currPluginSetting['loadmodule']:
|
||||
exec(self.buildPluginExec(currPluginSetting)) # load as python module
|
||||
else:
|
||||
exec(self.buildPluginSubprocess(currPluginSetting)) # run as subprocess
|
||||
|
||||
if currPluginSetting['blockcall']:
|
||||
currPluginSetting['function'] = globals()[currPluginSetting['functionname']] # non threaded
|
||||
else:
|
||||
currPluginSetting['function'] = globals()[currPluginSetting['functionname']+"T"] # T = Threaded
|
||||
|
||||
|
||||
if currPluginSetting['exec']: # exec on load if we want
|
||||
currPluginSetting['function']()
|
||||
|
||||
if not currPluginSetting['key'] == '':
|
||||
currPluginSetting = self.SetupShortcutAndHandle(currPluginSetting)
|
||||
print(currPluginSetting)
|
||||
self.plugin_list.append(currPluginSetting) # store in a list
|
||||
self.loaded = True
|
||||
@@ -0,0 +1,9 @@
|
||||
[Plugin]
|
||||
Name = Simple Plugin System
|
||||
Module = SimplePluginSystem
|
||||
Description = Simple plugin system implementation for Cthulhu
|
||||
Authors = Storm Dragon <storm_dragon@stormux.org>
|
||||
Copyright = Copyright (c) 2025 Stormux
|
||||
Website = https://git.stormux.org/storm/cthulhu
|
||||
Version = 1.0
|
||||
Category = System
|
||||
@@ -0,0 +1,407 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2024 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.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||
# Boston MA 02110-1301 USA.
|
||||
#
|
||||
|
||||
"""Simple Plugin System for Cthulhu."""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import importlib.util
|
||||
import random
|
||||
import string
|
||||
import _thread
|
||||
import logging
|
||||
from subprocess import Popen, PIPE
|
||||
import gettext
|
||||
|
||||
from cthulhu.plugin import Plugin, cthulhu_hookimpl
|
||||
|
||||
# Set up translation function
|
||||
_ = gettext.gettext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Global variables for API access
|
||||
settings = None
|
||||
speech = None
|
||||
braille = None
|
||||
input_event = None
|
||||
|
||||
def outputMessage(Message):
|
||||
"""Output a message via speech and/or braille depending on settings."""
|
||||
if (settings.enableSpeech):
|
||||
speech.speak(Message)
|
||||
if (settings.enableBraille):
|
||||
braille.displayMessage(Message)
|
||||
|
||||
class SimplePluginSystem(Plugin):
|
||||
"""Simple plugin system implementation for Cthulhu.
|
||||
|
||||
This plugin allows loading and managing simple script-based plugins
|
||||
from a designated directory.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the plugin system."""
|
||||
super().__init__(*args, **kwargs)
|
||||
logger.info("SimplePluginSystem plugin initialized")
|
||||
self.plugin_list = []
|
||||
self.loaded = False
|
||||
self.plugin_repo = os.path.expanduser('~') + "/.local/share/cthulhu/simple-plugins-enabled/"
|
||||
self._signal_handler_id = None
|
||||
|
||||
@cthulhu_hookimpl
|
||||
def activate(self, plugin=None):
|
||||
"""Activate the plugin system."""
|
||||
# Skip if this activation call isn't for us
|
||||
if plugin is not None and plugin is not self:
|
||||
return
|
||||
|
||||
logger.info("Activating SimplePluginSystem plugin")
|
||||
try:
|
||||
global settings
|
||||
global speech
|
||||
global braille
|
||||
global input_event
|
||||
|
||||
settings = self.app.getDynamicApiManager().getAPI('Settings')
|
||||
speech = self.app.getDynamicApiManager().getAPI('Speech')
|
||||
braille = self.app.getDynamicApiManager().getAPI('Braille')
|
||||
input_event = self.app.getDynamicApiManager().getAPI('InputEvent')
|
||||
|
||||
if not self.loaded:
|
||||
self.load_plugins()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error activating SimplePluginSystem plugin: {e}")
|
||||
|
||||
@cthulhu_hookimpl
|
||||
def deactivate(self, plugin=None):
|
||||
"""Deactivate the plugin system."""
|
||||
# Skip if this deactivation call isn't for us
|
||||
if plugin is not None and plugin is not self:
|
||||
return
|
||||
|
||||
logger.info("Deactivating SimplePluginSystem plugin")
|
||||
try:
|
||||
# Remove all registered keybindings
|
||||
for plugin in self.plugin_list:
|
||||
self.unregisterShortcut(plugin['function'], plugin['shortcut'])
|
||||
self.loaded = False
|
||||
self.plugin_list = []
|
||||
except Exception as e:
|
||||
logger.error(f"Error deactivating SimplePluginSystem plugin: {e}")
|
||||
|
||||
def SetupShortcutAndHandle(self, currPluginSetting):
|
||||
"""Set up keyboard shortcuts for a plugin."""
|
||||
shortcut = ''
|
||||
# just the modifier
|
||||
if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
|
||||
shortcut = 'kb:cthulhu+' + currPluginSetting['key']
|
||||
# cthulhu + alt
|
||||
if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
|
||||
shortcut = 'kb:cthulhu+alt+' + currPluginSetting['key']
|
||||
# cthulhu + CTRL
|
||||
if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
|
||||
shortcut = 'kb:cthulhu+control+' + currPluginSetting['key']
|
||||
# cthulhu + alt + CTRL
|
||||
if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
|
||||
shortcut = 'kb:cthulhu+alt+control+ ' + currPluginSetting['key']
|
||||
# cthulhu + shift
|
||||
if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
|
||||
shortcut = 'kb:cthulhu+shift+' + currPluginSetting['key']
|
||||
# alt + shift
|
||||
if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
|
||||
shortcut = 'kb:alt+shift+' + currPluginSetting['key']
|
||||
if shortcut != '':
|
||||
logger.debug(f"Registering shortcut: {shortcut}")
|
||||
currPluginSetting['shortcut'] = shortcut
|
||||
try:
|
||||
# Try to use the translation function, fall back to plain text if it fails
|
||||
plugin_name = _(currPluginSetting['pluginname'])
|
||||
except Exception:
|
||||
# If translation fails, use the original name
|
||||
plugin_name = currPluginSetting['pluginname']
|
||||
logger.warning(f"Translation failed for plugin: {currPluginSetting['pluginname']}")
|
||||
|
||||
self.registerGestureByString(currPluginSetting['function'], plugin_name, shortcut)
|
||||
return currPluginSetting
|
||||
|
||||
def id_generator(self, size=7, chars=string.ascii_letters):
|
||||
"""Generate a random ID string."""
|
||||
return ''.join(random.choice(chars) for _ in range(size))
|
||||
|
||||
def initSettings(self):
|
||||
"""Initialize default settings for a plugin."""
|
||||
currPluginSetting = {
|
||||
'pluginname': '',
|
||||
'functionname': '',
|
||||
'key': '',
|
||||
'shiftkey': False,
|
||||
'ctrlkey': False,
|
||||
'altkey': False,
|
||||
'startnotify': False,
|
||||
'stopnotify': False,
|
||||
'blockcall': False,
|
||||
'error': False,
|
||||
'exec': False,
|
||||
'parameters': '',
|
||||
'function': None,
|
||||
'inputeventhandler': None,
|
||||
'valid': False,
|
||||
'supressoutput': False,
|
||||
'shortcut': ''
|
||||
}
|
||||
return currPluginSetting
|
||||
|
||||
def getPluginSettings(self, filepath, currPluginSetting):
|
||||
"""Parse plugin settings from filename and content."""
|
||||
try:
|
||||
currPluginSetting['file'] = filepath
|
||||
fileName, fileExtension = os.path.splitext(filepath)
|
||||
if (fileExtension and (fileExtension != '')): # if there is an extension
|
||||
currPluginSetting['loadable'] = (fileExtension.lower() == '.py') # only python is loadable
|
||||
filename = os.path.basename(filepath) # filename
|
||||
filename = os.path.splitext(filename)[0] # remove extension if we have one
|
||||
# remove pluginname seperated by __-__
|
||||
filenamehelper = filename.split('__-__')
|
||||
filename = filenamehelper[len(filenamehelper) - 1]
|
||||
currPluginSetting['permission'] = os.access(filepath, os.X_OK)
|
||||
currPluginSetting['pluginname'] = 'NoNameAvailable'
|
||||
if len(filenamehelper) == 2:
|
||||
currPluginSetting['pluginname'] = filenamehelper[0]
|
||||
# now get shortcuts seperated by __+__
|
||||
filenamehelper = filename.split('__+__')
|
||||
if len([y for y in filenamehelper if 'parameters_' in y.lower()]) == 1 and\
|
||||
len([y for y in filenamehelper if 'parameters_' in y.lower()][0]) > 11:
|
||||
currPluginSetting['parameters'] = [y for y in filenamehelper if 'parameters_' in y.lower()][0][11:]
|
||||
if len([y for y in filenamehelper if 'key_' in y.lower()]) == 1 and\
|
||||
len([y for y in filenamehelper if 'key_' in y.lower()][0]) > 4:
|
||||
currPluginSetting['key'] = [y for y in filenamehelper if 'key_' in y.lower()][0][4]
|
||||
if currPluginSetting['key'] == '':
|
||||
settcurrPluginSetting = 'shift' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['ctrlkey'] = 'control' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['altkey'] = 'alt' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['startnotify'] = 'startnotify' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['stopnotify'] = 'stopnotify' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['blockcall'] = 'blockcall' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['error'] = 'error' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['supressoutput'] = 'supressoutput' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['exec'] = 'exec' in map(str.lower, filenamehelper)
|
||||
currPluginSetting['loadmodule'] = 'loadmodule' in map(str.lower, filenamehelper)
|
||||
currPluginSetting = self.readSettingsFromPlugin(currPluginSetting)
|
||||
if not currPluginSetting['loadmodule']:
|
||||
if not currPluginSetting['permission']: # subprocessing only works with exec permission
|
||||
return self.initSettings()
|
||||
if currPluginSetting['loadmodule'] and not currPluginSetting['loadable']: # sorry.. its not loadable only .py is loadable
|
||||
return self.initSettings()
|
||||
if (len(currPluginSetting['key']) > 1): # no shortcut
|
||||
if not currPluginSetting['exec']: # and no exec -> the plugin make no sense because it isnt hooked anywhere
|
||||
return self.initSettings() # so not load it (sets valid = False)
|
||||
else:
|
||||
currPluginSetting['key'] = '' # there is a strange key, but exec? ignore the key..
|
||||
currPluginSetting['valid'] = True # we could load everything
|
||||
return currPluginSetting
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting plugin settings: {e}")
|
||||
return self.initSettings()
|
||||
|
||||
def readSettingsFromPlugin(self, currPluginSetting):
|
||||
"""Read settings from plugin file content."""
|
||||
if not os.access(currPluginSetting['file'], os.R_OK):
|
||||
return currPluginSetting
|
||||
fileName, fileExtension = os.path.splitext(currPluginSetting['file'])
|
||||
if (fileExtension and (fileExtension != '')): # if there is an extension
|
||||
if (fileExtension.lower() != '.py') and \
|
||||
(fileExtension.lower() != '.sh'):
|
||||
return currPluginSetting
|
||||
else:
|
||||
return currPluginSetting
|
||||
|
||||
try:
|
||||
with open(currPluginSetting['file'], "r") as pluginFile:
|
||||
for line in pluginFile:
|
||||
currPluginSetting['shiftkey'] = ('sopsproperty:shift' in line.lower().replace(" ", "")) or currPluginSetting['shiftkey']
|
||||
currPluginSetting['ctrlkey'] = ('sopsproperty:control' in line.lower().replace(" ", "")) or currPluginSetting['ctrlkey']
|
||||
currPluginSetting['altkey'] = ('sopsproperty:alt' in line.lower().replace(" ", "")) or currPluginSetting['altkey']
|
||||
currPluginSetting['startnotify'] = ('sopsproperty:startnotify' in line.lower().replace(" ", "")) or currPluginSetting['startnotify']
|
||||
currPluginSetting['stopnotify'] = ('sopsproperty:stopnotify' in line.lower().replace(" ", "")) or currPluginSetting['stopnotify']
|
||||
currPluginSetting['blockcall'] = ('sopsproperty:blockcall' in line.lower().replace(" ", "")) or currPluginSetting['blockcall']
|
||||
currPluginSetting['error'] = ('sopsproperty:error' in line.lower().replace(" ", "")) or currPluginSetting['error']
|
||||
currPluginSetting['supressoutput'] = ('sopsproperty:supressoutput' in line.lower().replace(" ", "")) or currPluginSetting['supressoutput']
|
||||
currPluginSetting['exec'] = ('sopsproperty:exec' in line.lower().replace(" ", "")) or currPluginSetting['exec']
|
||||
currPluginSetting['loadmodule'] = ('sopsproperty:loadmodule' in line.lower().replace(" ", "")) or currPluginSetting['loadmodule']
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading plugin file: {e}")
|
||||
|
||||
return currPluginSetting
|
||||
|
||||
def buildPluginSubprocess(self, currPluginSetting):
|
||||
"""Build a function to execute a plugin as a subprocess."""
|
||||
currplugin = "\'\"" + currPluginSetting['file'] + "\" " + currPluginSetting['parameters'] + "\'"
|
||||
pluginname = currPluginSetting['pluginname']
|
||||
if currPluginSetting['blockcall']:
|
||||
pluginname = "blocking " + pluginname
|
||||
fun_body = "global " + currPluginSetting['functionname'] + "\n"
|
||||
fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
|
||||
if currPluginSetting['startnotify']:
|
||||
fun_body += " outputMessage('start " + pluginname + "')\n"
|
||||
fun_body += " p = Popen(" + currplugin + ", stdout=PIPE, stderr=PIPE, shell=True)\n"
|
||||
fun_body += " stdout, stderr = p.communicate()\n"
|
||||
fun_body += " message = ''\n"
|
||||
fun_body += " if not " + str(currPluginSetting['supressoutput']) + " and stdout:\n"
|
||||
fun_body += " message += str(stdout, \"utf-8\")\n"
|
||||
fun_body += " if " + str(currPluginSetting['error']) + " and stderr:\n"
|
||||
fun_body += " message += ' error: ' + str(stderr, \"utf-8\")\n"
|
||||
fun_body += " outputMessage(message)\n"
|
||||
if currPluginSetting['stopnotify']:
|
||||
fun_body += " outputMessage('finish " + pluginname + "')\n"
|
||||
fun_body += " return True\n\n"
|
||||
fun_body += "global " + currPluginSetting['functionname'] + "T\n"
|
||||
fun_body += "def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
|
||||
fun_body += " _thread.start_new_thread(" + currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
|
||||
return fun_body
|
||||
|
||||
def buildPluginExec(self, currPluginSetting):
|
||||
"""Build a function to execute a plugin as a Python module."""
|
||||
pluginname = currPluginSetting['pluginname']
|
||||
if currPluginSetting['blockcall']:
|
||||
pluginname = "blocking " + pluginname
|
||||
fun_body = "global " + currPluginSetting['functionname'] + "\n"
|
||||
fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
|
||||
if currPluginSetting['startnotify']:
|
||||
fun_body += " outputMessage('start " + pluginname + "')\n"
|
||||
fun_body += " try:\n"
|
||||
fun_body += " spec = importlib.util.spec_from_file_location(\"" + currPluginSetting['functionname'] + "\",\"" + currPluginSetting['file'] + "\")\n"
|
||||
fun_body += " " + currPluginSetting['functionname'] + "Module = importlib.util.module_from_spec(spec)\n"
|
||||
fun_body += " spec.loader.exec_module(" + currPluginSetting['functionname'] + "Module)\n"
|
||||
fun_body += " except Exception as e:\n"
|
||||
fun_body += " logger.error(f\"Error executing plugin {pluginname}: {e}\")\n"
|
||||
if currPluginSetting['error']:
|
||||
fun_body += " outputMessage(\"Error while executing " + pluginname + "\")\n"
|
||||
if currPluginSetting['stopnotify']:
|
||||
fun_body += " outputMessage('finish " + pluginname + "')\n"
|
||||
fun_body += " return True\n\n"
|
||||
fun_body += "global " + currPluginSetting['functionname'] + "T\n"
|
||||
fun_body += "def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
|
||||
fun_body += " _thread.start_new_thread(" + currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
|
||||
return fun_body
|
||||
|
||||
def getFunctionName(self, currPluginSetting):
|
||||
"""Generate a unique function name for a plugin."""
|
||||
currPluginSetting['functionname'] = ''
|
||||
while currPluginSetting['functionname'] == '' or currPluginSetting['functionname'] + 'T' in globals() or currPluginSetting['functionname'] in globals():
|
||||
currPluginSetting['functionname'] = self.id_generator()
|
||||
return currPluginSetting
|
||||
|
||||
def registerGestureByString(self, function, description, shortcut):
|
||||
"""Register a keyboard shortcut for a function.
|
||||
|
||||
This is a compatibility wrapper for the new plugin system.
|
||||
"""
|
||||
try:
|
||||
if self.app:
|
||||
api_helper = self.app.getAPIHelper()
|
||||
if api_helper:
|
||||
api_helper.registerGestureByString(
|
||||
function,
|
||||
description,
|
||||
shortcut,
|
||||
'default',
|
||||
'cthulhu',
|
||||
True,
|
||||
contextName=self.module_name
|
||||
)
|
||||
logger.debug(f"Registered shortcut {shortcut} for {description}")
|
||||
return True
|
||||
else:
|
||||
logger.error("Could not get APIHelper")
|
||||
else:
|
||||
logger.error("No app reference available")
|
||||
except Exception as e:
|
||||
logger.error(f"Error registering shortcut {shortcut}: {e}")
|
||||
|
||||
def unregisterShortcut(self, function, shortcut):
|
||||
"""Unregister a keyboard shortcut for a function.
|
||||
|
||||
This is a compatibility wrapper for the new plugin system.
|
||||
"""
|
||||
try:
|
||||
if self.app:
|
||||
api_helper = self.app.getAPIHelper()
|
||||
if api_helper and hasattr(api_helper, 'unregisterShortcut'):
|
||||
api_helper.unregisterShortcut(shortcut)
|
||||
logger.debug(f"Unregistered shortcut {shortcut}")
|
||||
return True
|
||||
else:
|
||||
logger.error("Could not get APIHelper or unregisterShortcut method")
|
||||
else:
|
||||
logger.error("No app reference available")
|
||||
except Exception as e:
|
||||
logger.error(f"Error unregistering shortcut {shortcut}: {e}")
|
||||
|
||||
def load_plugins(self):
|
||||
"""Load and setup all plugins in the plugin repository."""
|
||||
if not self.loaded:
|
||||
try:
|
||||
logger.info(f"Loading plugins from {self.plugin_repo}")
|
||||
plugin_files = glob.glob(self.plugin_repo + '*')
|
||||
self.plugin_list = [] # Reset the plugin list to avoid confusion
|
||||
|
||||
for currplugin in plugin_files:
|
||||
try:
|
||||
# Ensure currplugin is a valid path string
|
||||
if not isinstance(currplugin, (str, bytes, os.PathLike)):
|
||||
logger.error(f"Invalid plugin path: {type(currplugin)}")
|
||||
continue
|
||||
|
||||
currPluginSetting = self.initSettings()
|
||||
currPluginSetting = self.getPluginSettings(currplugin, currPluginSetting)
|
||||
|
||||
if not currPluginSetting['valid']:
|
||||
logger.debug(f"Skipping invalid plugin: {currplugin}")
|
||||
continue
|
||||
|
||||
currPluginSetting = self.getFunctionName(currPluginSetting)
|
||||
|
||||
if currPluginSetting['loadmodule']:
|
||||
exec(self.buildPluginExec(currPluginSetting)) # load as python module
|
||||
else:
|
||||
exec(self.buildPluginSubprocess(currPluginSetting)) # run as subprocess
|
||||
|
||||
if currPluginSetting['blockcall']:
|
||||
currPluginSetting['function'] = globals()[currPluginSetting['functionname']] # non threaded
|
||||
else:
|
||||
currPluginSetting['function'] = globals()[currPluginSetting['functionname'] + "T"] # T = Threaded
|
||||
|
||||
if currPluginSetting['exec']: # exec on load if we want
|
||||
currPluginSetting['function']()
|
||||
|
||||
if not currPluginSetting['key'] == '':
|
||||
currPluginSetting = self.SetupShortcutAndHandle(currPluginSetting)
|
||||
|
||||
logger.debug(f"Loaded plugin: {currPluginSetting['pluginname']}")
|
||||
self.plugin_list.append(currPluginSetting) # store in a list
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading plugin {currplugin}: {e}")
|
||||
|
||||
self.loaded = True
|
||||
logger.info(f"Loaded {len(self.plugin_list)} plugins")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in load_plugins: {e}")
|
||||
@@ -1,7 +0,0 @@
|
||||
cthulhu_python_PYTHON = \
|
||||
__init__.py \
|
||||
Time.plugin \
|
||||
Time.py
|
||||
|
||||
cthulhu_pythondir=$(pkgpythondir)/plugins/Time
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[Plugin]
|
||||
Module=Time
|
||||
Loader=python3
|
||||
Name=Time
|
||||
Description=Present current time
|
||||
Authors=Chrys chrys@linux-a11y.org
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
import gi, time
|
||||
gi.require_version('Peas', '1.0')
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Peas
|
||||
|
||||
from cthulhu import plugin
|
||||
|
||||
class Time(GObject.Object, Peas.Activatable, plugin.Plugin):
|
||||
#__gtype_name__ = 'Time'
|
||||
|
||||
object = GObject.Property(type=GObject.Object)
|
||||
def __init__(self):
|
||||
plugin.Plugin.__init__(self)
|
||||
def do_activate(self):
|
||||
API = self.object
|
||||
self.connectSignal("setup-inputeventhandlers-completed", self.setupCompatBinding)
|
||||
def setupCompatBinding(self, app):
|
||||
cmdnames = app.getDynamicApiManager().getAPI('Cmdnames')
|
||||
inputEventHandlers = app.getDynamicApiManager().getAPI('inputEventHandlers')
|
||||
inputEventHandlers['presentTimeHandler'] = app.getAPIHelper().createInputEventHandler(self.presentTime, cmdnames.PRESENT_CURRENT_TIME)
|
||||
def do_deactivate(self):
|
||||
API = self.object
|
||||
inputEventHandlers = API.app.getDynamicApiManager().getAPI('inputEventHandlers')
|
||||
del inputEventHandlers['presentTimeHandler']
|
||||
def do_update_state(self):
|
||||
API = self.object
|
||||
def presentTime(self, script=None, inputEvent=None):
|
||||
""" Presents the current time. """
|
||||
API = self.object
|
||||
settings_manager = API.app.getDynamicApiManager().getAPI('SettingsManager')
|
||||
_settingsManager = settings_manager.getManager()
|
||||
timeFormat = _settingsManager.getSetting('presentTimeFormat')
|
||||
message = time.strftime(timeFormat, time.localtime())
|
||||
API.app.getDynamicApiManager().getAPI('CthulhuState').activeScript.presentMessage(message, resetStyles=False)
|
||||
return True
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/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
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# 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
|
||||
|
||||
@@ -20,6 +20,7 @@ class HelloWorld(Plugin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the plugin."""
|
||||
super().__init__(*args, **kwargs)
|
||||
print("Plugin hello world initialized.")
|
||||
logger.info("HelloWorld plugin initialized")
|
||||
|
||||
@cthulhu_hookimpl
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -318,6 +318,13 @@ class ScriptManager:
|
||||
return
|
||||
|
||||
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]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
|
||||
@@ -363,6 +363,8 @@ class Script(script.Script):
|
||||
return keyBindings
|
||||
|
||||
def getExtensionBindings(self):
|
||||
with open('/tmp/extension_bindings_debug.log', 'a') as f:
|
||||
f.write(f"=== getExtensionBindings() called ===\n")
|
||||
keyBindings = keybindings.KeyBindings()
|
||||
|
||||
bindings = self.notificationPresenter.get_bindings()
|
||||
@@ -407,6 +409,41 @@ class Script(script.Script):
|
||||
for keyBinding in bindings.keyBindings:
|
||||
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
|
||||
|
||||
def getKeyBindings(self):
|
||||
|
||||
@@ -413,4 +413,4 @@ presentChatRoomLast = False
|
||||
presentLiveRegionFromInactiveTab = False
|
||||
|
||||
# Plugins
|
||||
activePlugins = ['Clipboard', 'DisplayVersion', 'MouseReview', 'Date', 'ByeCthulhu', 'Time', 'HelloCthulhu', 'hello_world', 'self_voice', 'PluginManager', 'SimplePluginSystem']
|
||||
activePlugins = ['Clipboard', 'DisplayVersion', 'ByeCthulhu', 'HelloCthulhu', 'hello_world', 'self_voice', 'SimplePluginSystem']
|
||||
|
||||
Reference in New Issue
Block a user