Merge branch 'testing'

Plugins are in a much better state now, mostly working. The exception is, plugins that create a keyboard shortcut don't actually bind the shortcut. That one is turning out to be a lot harder to fix than I originally thought.
This commit is contained in:
Storm Dragon 2025-04-05 16:32:17 -04:00
commit d36b664319
47 changed files with 905 additions and 2302 deletions

View File

@ -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

View File

@ -72,8 +72,22 @@ class APIHelper:
# Handle Cthulhu modifier specially
if "cthulhu+" in key.lower():
from . import keybindings
key = key.lower().replace("cthulhu+", "")
key_parts = key.lower().split("+")
# Determine appropriate modifier mask
modifiers = keybindings.CTHULHU_MODIFIER_MASK
# Extract the final key (without modifiers)
final_key = key_parts[-1]
# Check for additional modifiers
if "shift" in key_parts:
modifiers = keybindings.CTHULHU_SHIFT_MODIFIER_MASK
elif "ctrl" in key_parts or "control" in key_parts:
modifiers = keybindings.CTHULHU_CTRL_MODIFIER_MASK
elif "alt" in key_parts:
modifiers = keybindings.CTHULHU_ALT_MODIFIER_MASK
# Create a keybinding handler
class GestureHandler:
def __init__(self, function, description):
@ -90,9 +104,9 @@ class APIHelper:
if cthulhu_state.activeScript:
bindings = cthulhu_state.activeScript.getKeyBindings()
binding = keybindings.KeyBinding(
key,
final_key,
keybindings.defaultModifierMask,
keybindings.CTHULHU_MODIFIER_MASK,
modifiers,
handler)
bindings.add(binding)
@ -1018,6 +1032,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):

View File

@ -23,5 +23,5 @@
# Fork of Orca Screen Reader (GNOME)
# Original source: https://gitlab.gnome.org/GNOME/orca
version = "2025.04.03"
version = "2025.04.04"
codeName = "testing"

View File

@ -16,6 +16,7 @@ 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):
@ -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__)

View File

@ -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
@ -103,6 +112,10 @@ 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 _setup_plugin_dirs(self):
"""Ensure plugin directories exist."""
@ -136,19 +149,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 +187,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,7 +218,25 @@ class PluginSystemManager:
def setActivePlugins(self, activePlugins):
"""Set active plugins and sync their state."""
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}")
# 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()
def setPluginActive(self, pluginInfo, active):
@ -211,25 +257,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 +326,38 @@ class PluginSystemManager:
return False
module_name = pluginInfo.get_module_name()
logger.info(f"Attempting to load plugin: {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 +370,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 +378,47 @@ 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}")
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):

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -1,7 +0,0 @@
cthulhu_python_PYTHON = \
__init__.py \
CapsLockHack.plugin \
CapsLockHack.py
cthulhu_pythondir=$(pkgpythondir)/plugins/CapsLockHack

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,7 +1,7 @@
cthulhu_python_PYTHON = \
__init__.py \
Clipboard.plugin \
Clipboard.py
plugin.info \
plugin.py
cthulhu_pythondir=$(pkgpythondir)/plugins/Clipboard

View File

@ -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

View File

@ -0,0 +1,193 @@
#!/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")
try:
# Unregister keyboard shortcut
if self.app:
api_helper = self.app.getAPIHelper()
if api_helper and hasattr(api_helper, 'unregisterShortcut'):
api_helper.unregisterShortcut('kb:cthulhu+shift+c')
logger.debug("Unregistered clipboard shortcut")
except Exception as e:
logger.error(f"Error deactivating Clipboard plugin: {e}")
"""Activate the plugin."""
# Skip if this activation call isn't for us
if plugin is not None and plugin is not self:
return
logger.info("Activating Clipboard plugin")
try:
# Register keyboard shortcut
self.registerGestureByString(self.speakClipboard, _('clipboard'), 'kb:cthulhu+shift+c')
logger.debug("Registered shortcut for clipboard")
except Exception as e:
logger.error(f"Error activating Clipboard plugin: {e}")
@cthulhu_hookimpl
def deactivate(self, plugin=None):
"""Deactivate the plugin."""
# Skip if this deactivation call isn't for us
if plugin is not None and plugin is not self:
return
logger.info("Deactivating Clipboard plugin")
try:
# Unregister keyboard shortcut
self.unregisterGestureByString('kb:cthulhu+shift+c')
logger.debug("Unregistered clipboard shortcut")
except Exception as e:
logger.error(f"Error deactivating Clipboard plugin: {e}")
def speakClipboard(self, script=None, inputEvent=None):
"""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")

View File

@ -1,6 +0,0 @@
[Plugin]
Module=Date
Loader=python3
Name=Date
Description=Present the current date
Authors=Chrys chrys@linux-a11y.org

View File

@ -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

View File

@ -1,7 +0,0 @@
cthulhu_python_PYTHON = \
__init__.py \
Date.plugin \
Date.py
cthulhu_pythondir=$(pkgpythondir)/plugins/Date

View File

@ -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

View File

@ -1,6 +0,0 @@
[Plugin]
Module=HelloCthulhu
Loader=python3
Name=Cthulhu say hello
Description=startup announcement for Cthulhu
Authors=Chrys chrys@linux-a11y.org

View File

@ -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)

View File

@ -1,7 +1,7 @@
cthulhu_python_PYTHON = \
__init__.py \
HelloCthulhu.plugin \
HelloCthulhu.py
plugin.info \
plugin.py
cthulhu_pythondir=$(pkgpythondir)/plugins/HelloCthulhu

View File

@ -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

View File

@ -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}")

View File

@ -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

View File

@ -1,7 +0,0 @@
cthulhu_python_PYTHON = \
__init__.py \
MouseReview.plugin \
MouseReview.py
cthulhu_pythondir=$(pkgpythondir)/plugins/MouseReview

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -1,8 +0,0 @@
cthulhu_python_PYTHON = \
__init__.py \
PluginManager.plugin \
PluginManager.py \
PluginManagerUi.py
cthulhu_pythondir=$(pkgpythondir)/plugins/PluginManager

View File

@ -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=

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -1,7 +1,7 @@
cthulhu_python_PYTHON = \
__init__.py \
SimplePluginSystem.plugin \
SimplePluginSystem.py
plugin.info \
plugin.py
cthulhu_pythondir=$(pkgpythondir)/plugins/SimplePluginSystem

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}")

View File

@ -1,7 +0,0 @@
cthulhu_python_PYTHON = \
__init__.py \
Time.plugin \
Time.py
cthulhu_pythondir=$(pkgpythondir)/plugins/Time

View File

@ -1,6 +0,0 @@
[Plugin]
Module=Time
Loader=python3
Name=Time
Description=Present current time
Authors=Chrys chrys@linux-a11y.org

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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']