From dfe20fca30b37d598933a76b0a4eeed68bebcffb Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 3 Apr 2025 20:38:27 -0400 Subject: [PATCH 01/14] More work on pluggy. --- src/cthulhu/cthulhu.py | 2 + src/cthulhu/plugin.py | 5 +- src/cthulhu/plugin_system_manager.py | 91 +++++++++++++++++++++++++++- 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index 6e85089..fcf628b 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -1018,6 +1018,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): diff --git a/src/cthulhu/plugin.py b/src/cthulhu/plugin.py index 100e8f0..88a3080 100644 --- a/src/cthulhu/plugin.py +++ b/src/cthulhu/plugin.py @@ -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__) diff --git a/src/cthulhu/plugin_system_manager.py b/src/cthulhu/plugin_system_manager.py index 8a7f13b..edb35ac 100644 --- a/src/cthulhu/plugin_system_manager.py +++ b/src/cthulhu/plugin_system_manager.py @@ -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.""" @@ -190,7 +203,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 +242,52 @@ 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 if plugin is in the active plugins list + is_active = module_name in self.getActivePlugins() + logger.debug(f"Plugin {module_name} active status: {is_active}") + 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 +297,26 @@ 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") + 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 +329,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 +337,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): From d6a373c726a4f6594e9b4c75a6ea3284f35f22fd Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 3 Apr 2025 20:46:11 -0400 Subject: [PATCH 02/14] Fixed some errors with plugins. --- src/cthulhu/plugin_system_manager.py | 51 +++++++++++++++++++++--- src/cthulhu/plugins/ByeCthulhu/plugin.py | 9 +++-- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/cthulhu/plugin_system_manager.py b/src/cthulhu/plugin_system_manager.py index edb35ac..2d6d5cd 100644 --- a/src/cthulhu/plugin_system_manager.py +++ b/src/cthulhu/plugin_system_manager.py @@ -149,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( @@ -175,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.""" @@ -254,9 +269,23 @@ class PluginSystemManager: logger.debug(f"Plugin {module_name} is already loaded, considered active") return True - # Check if plugin is in the active plugins list - is_active = module_name in self.getActivePlugins() - logger.debug(f"Plugin {module_name} active status: {is_active}") + # 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): @@ -305,8 +334,20 @@ class PluginSystemManager: 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 diff --git a/src/cthulhu/plugins/ByeCthulhu/plugin.py b/src/cthulhu/plugins/ByeCthulhu/plugin.py index 4720e87..d3174ea 100644 --- a/src/cthulhu/plugins/ByeCthulhu/plugin.py +++ b/src/cthulhu/plugins/ByeCthulhu/plugin.py @@ -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: From 4b8ebcb59916932e1ee6578841670ea332732fc7 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 4 Apr 2025 02:46:01 -0400 Subject: [PATCH 03/14] Removed the Date plugin it was causing traceback. --- src/cthulhu/cthulhuVersion.py | 2 +- src/cthulhu/plugins/Date/Date.plugin | 6 --- src/cthulhu/plugins/Date/Date.py | 58 ---------------------------- src/cthulhu/plugins/Date/Makefile.am | 7 ---- src/cthulhu/plugins/Date/__init__.py | 25 ------------ src/cthulhu/plugins/Makefile.am | 2 +- src/cthulhu/settings.py | 2 +- 7 files changed, 3 insertions(+), 99 deletions(-) delete mode 100644 src/cthulhu/plugins/Date/Date.plugin delete mode 100644 src/cthulhu/plugins/Date/Date.py delete mode 100644 src/cthulhu/plugins/Date/Makefile.am delete mode 100644 src/cthulhu/plugins/Date/__init__.py diff --git a/src/cthulhu/cthulhuVersion.py b/src/cthulhu/cthulhuVersion.py index ff8827e..030b4bd 100644 --- a/src/cthulhu/cthulhuVersion.py +++ b/src/cthulhu/cthulhuVersion.py @@ -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" diff --git a/src/cthulhu/plugins/Date/Date.plugin b/src/cthulhu/plugins/Date/Date.plugin deleted file mode 100644 index 12f6a7e..0000000 --- a/src/cthulhu/plugins/Date/Date.plugin +++ /dev/null @@ -1,6 +0,0 @@ -[Plugin] -Module=Date -Loader=python3 -Name=Date -Description=Present the current date -Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/Date/Date.py b/src/cthulhu/plugins/Date/Date.py deleted file mode 100644 index 3d7887f..0000000 --- a/src/cthulhu/plugins/Date/Date.py +++ /dev/null @@ -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 diff --git a/src/cthulhu/plugins/Date/Makefile.am b/src/cthulhu/plugins/Date/Makefile.am deleted file mode 100644 index 64881f8..0000000 --- a/src/cthulhu/plugins/Date/Makefile.am +++ /dev/null @@ -1,7 +0,0 @@ -cthulhu_python_PYTHON = \ - __init__.py \ - Date.plugin \ - Date.py - -cthulhu_pythondir=$(pkgpythondir)/plugins/Date - diff --git a/src/cthulhu/plugins/Date/__init__.py b/src/cthulhu/plugins/Date/__init__.py deleted file mode 100644 index 782103c..0000000 --- a/src/cthulhu/plugins/Date/__init__.py +++ /dev/null @@ -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 - diff --git a/src/cthulhu/plugins/Makefile.am b/src/cthulhu/plugins/Makefile.am index 89e438a..042fc2b 100644 --- a/src/cthulhu/plugins/Makefile.am +++ b/src/cthulhu/plugins/Makefile.am @@ -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 Time MouseReview ByeCthulhu HelloCthulhu PluginManager CapsLockHack SimplePluginSystem cthulhu_pythondir=$(pkgpythondir)/plugins diff --git a/src/cthulhu/settings.py b/src/cthulhu/settings.py index 1c05ff9..787c5cd 100644 --- a/src/cthulhu/settings.py +++ b/src/cthulhu/settings.py @@ -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', 'MouseReview', 'ByeCthulhu', 'Time', 'HelloCthulhu', 'hello_world', 'self_voice', 'PluginManager', 'SimplePluginSystem'] From 0b7cf681c3b62dd674ebded977abe7f18bb0b716 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 4 Apr 2025 02:48:43 -0400 Subject: [PATCH 04/14] Forgot to update the configure.ac file. --- configure.ac | 1 - 1 file changed, 1 deletion(-) diff --git a/configure.ac b/configure.ac index 740db46..c26fc97 100644 --- a/configure.ac +++ b/configure.ac @@ -133,7 +133,6 @@ 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 From 7876a18c12a9c87f78ce8a7dce9f360b29399d39 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 4 Apr 2025 14:19:09 -0400 Subject: [PATCH 05/14] Working on plugin conversion. --- .../plugins/HelloCthulhu/HelloCthulhu.plugin | 6 -- .../plugins/HelloCthulhu/HelloCthulhu.py | 48 ----------- src/cthulhu/plugins/HelloCthulhu/Makefile.am | 4 +- src/cthulhu/plugins/HelloCthulhu/plugin.info | 7 ++ src/cthulhu/plugins/HelloCthulhu/plugin.py | 84 +++++++++++++++++++ src/cthulhu/plugins/hello_world/plugin.py | 1 + 6 files changed, 94 insertions(+), 56 deletions(-) delete mode 100644 src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.plugin delete mode 100644 src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.py create mode 100644 src/cthulhu/plugins/HelloCthulhu/plugin.info create mode 100644 src/cthulhu/plugins/HelloCthulhu/plugin.py diff --git a/src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.plugin b/src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.plugin deleted file mode 100644 index f45ef69..0000000 --- a/src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.plugin +++ /dev/null @@ -1,6 +0,0 @@ -[Plugin] -Module=HelloCthulhu -Loader=python3 -Name=Cthulhu say hello -Description=startup announcement for Cthulhu -Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.py b/src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.py deleted file mode 100644 index a9235b2..0000000 --- a/src/cthulhu/plugins/HelloCthulhu/HelloCthulhu.py +++ /dev/null @@ -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) diff --git a/src/cthulhu/plugins/HelloCthulhu/Makefile.am b/src/cthulhu/plugins/HelloCthulhu/Makefile.am index aa48815..e17c9d1 100644 --- a/src/cthulhu/plugins/HelloCthulhu/Makefile.am +++ b/src/cthulhu/plugins/HelloCthulhu/Makefile.am @@ -1,7 +1,7 @@ cthulhu_python_PYTHON = \ __init__.py \ - HelloCthulhu.plugin \ - HelloCthulhu.py + plugin.info \ + plugin.py cthulhu_pythondir=$(pkgpythondir)/plugins/HelloCthulhu diff --git a/src/cthulhu/plugins/HelloCthulhu/plugin.info b/src/cthulhu/plugins/HelloCthulhu/plugin.info new file mode 100644 index 0000000..f1faf33 --- /dev/null +++ b/src/cthulhu/plugins/HelloCthulhu/plugin.info @@ -0,0 +1,7 @@ +[Plugin] +Name = Cthulhu say hello +Module = HelloCthulhu +Description = Startup announcement for Cthulhu +Authors = Storm Dragon +Version = 1.0 +Category = Interaction diff --git a/src/cthulhu/plugins/HelloCthulhu/plugin.py b/src/cthulhu/plugins/HelloCthulhu/plugin.py new file mode 100644 index 0000000..de54907 --- /dev/null +++ b/src/cthulhu/plugins/HelloCthulhu/plugin.py @@ -0,0 +1,84 @@ +#!/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 +from cthulhu.plugin import Plugin, cthulhu_hookimpl + +logger = logging.getLogger(__name__) + +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 + + @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: + # Connect to the start-application-completed signal + signal_manager = self.app.getSignalManager() + self._signal_handler_id = signal_manager.connectSignal( + "start-application-completed", + self.process, + "default" # Add profile parameter + ) + 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: + # Disconnect signal if we have an ID + if self._signal_handler_id is not None: + signal_manager = self.app.getSignalManager() + # Use disconnectSignalByFunction instead since disconnectSignal doesn't exist + signal_manager.disconnectSignalByFunction( + self.process + ) + self._signal_handler_id = None + except Exception as e: + logger.error(f"Error deactivating HelloCthulhu plugin: {e}") + + def process(self, app): + """Process the start-application-completed signal.""" + try: + messages = app.getDynamicApiManager().getAPI('Messages') + state = app.getDynamicApiManager().getAPI('CthulhuState') + if state.activeScript: + state.activeScript.presentMessage(messages.START_CTHULHU, resetStyles=False) + except Exception as e: + logger.error(f"Error in HelloCthulhu process: {e}") diff --git a/src/cthulhu/plugins/hello_world/plugin.py b/src/cthulhu/plugins/hello_world/plugin.py index 874861c..0876922 100644 --- a/src/cthulhu/plugins/hello_world/plugin.py +++ b/src/cthulhu/plugins/hello_world/plugin.py @@ -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 From 231d74efa0402380a4c19ed40ddf0358b6ea521a Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 4 Apr 2025 14:32:03 -0400 Subject: [PATCH 06/14] Try to fix repeating welcome message. --- src/cthulhu/plugins/HelloCthulhu/plugin.py | 49 +++++++++++++++++----- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/src/cthulhu/plugins/HelloCthulhu/plugin.py b/src/cthulhu/plugins/HelloCthulhu/plugin.py index de54907..a790757 100644 --- a/src/cthulhu/plugins/HelloCthulhu/plugin.py +++ b/src/cthulhu/plugins/HelloCthulhu/plugin.py @@ -21,10 +21,15 @@ """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.""" @@ -33,6 +38,7 @@ class HelloCthulhu(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): @@ -43,13 +49,16 @@ class HelloCthulhu(Plugin): logger.info("Activating HelloCthulhu plugin") try: - # Connect to the start-application-completed signal - signal_manager = self.app.getSignalManager() - self._signal_handler_id = signal_manager.connectSignal( - "start-application-completed", - self.process, - "default" # Add profile parameter - ) + # 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}") @@ -62,23 +71,43 @@ class HelloCthulhu(Plugin): logger.info("Deactivating HelloCthulhu plugin") try: - # Disconnect signal if we have an ID - if self._signal_handler_id is not None: + # Only disconnect if we're connected + if self._is_connected and self._signal_handler_id is not None: signal_manager = self.app.getSignalManager() - # Use disconnectSignalByFunction instead since disconnectSignal doesn't exist 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}") From 815d39fc3f16698e388b3a72e241e306b6befb5c Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 4 Apr 2025 14:54:18 -0400 Subject: [PATCH 07/14] Remove a couple plugins that were not being used and won't be ported over. If needed, they can be rewritten later. --- configure.ac | 2 - .../plugins/CapsLockHack/CapsLockHack.plugin | 6 - .../plugins/CapsLockHack/CapsLockHack.py | 144 ---- src/cthulhu/plugins/CapsLockHack/Makefile.am | 7 - src/cthulhu/plugins/CapsLockHack/__init__.py | 25 - src/cthulhu/plugins/Makefile.am | 2 +- src/cthulhu/plugins/MouseReview/Makefile.am | 7 - .../plugins/MouseReview/MouseReview.plugin | 6 - .../plugins/MouseReview/MouseReview.py | 759 ------------------ src/cthulhu/plugins/MouseReview/__init__.py | 25 - src/cthulhu/settings.py | 2 +- 11 files changed, 2 insertions(+), 983 deletions(-) delete mode 100644 src/cthulhu/plugins/CapsLockHack/CapsLockHack.plugin delete mode 100644 src/cthulhu/plugins/CapsLockHack/CapsLockHack.py delete mode 100644 src/cthulhu/plugins/CapsLockHack/Makefile.am delete mode 100644 src/cthulhu/plugins/CapsLockHack/__init__.py delete mode 100644 src/cthulhu/plugins/MouseReview/Makefile.am delete mode 100644 src/cthulhu/plugins/MouseReview/MouseReview.plugin delete mode 100644 src/cthulhu/plugins/MouseReview/MouseReview.py delete mode 100644 src/cthulhu/plugins/MouseReview/__init__.py diff --git a/configure.ac b/configure.ac index c26fc97..b3356e3 100644 --- a/configure.ac +++ b/configure.ac @@ -131,10 +131,8 @@ 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/Time/Makefile -src/cthulhu/plugins/MouseReview/Makefile src/cthulhu/plugins/SimplePluginSystem/Makefile src/cthulhu/backends/Makefile src/cthulhu/cthulhu_bin.py diff --git a/src/cthulhu/plugins/CapsLockHack/CapsLockHack.plugin b/src/cthulhu/plugins/CapsLockHack/CapsLockHack.plugin deleted file mode 100644 index 6279400..0000000 --- a/src/cthulhu/plugins/CapsLockHack/CapsLockHack.plugin +++ /dev/null @@ -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 diff --git a/src/cthulhu/plugins/CapsLockHack/CapsLockHack.py b/src/cthulhu/plugins/CapsLockHack/CapsLockHack.py deleted file mode 100644 index a616ac2..0000000 --- a/src/cthulhu/plugins/CapsLockHack/CapsLockHack.py +++ /dev/null @@ -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 diff --git a/src/cthulhu/plugins/CapsLockHack/Makefile.am b/src/cthulhu/plugins/CapsLockHack/Makefile.am deleted file mode 100644 index c687cc7..0000000 --- a/src/cthulhu/plugins/CapsLockHack/Makefile.am +++ /dev/null @@ -1,7 +0,0 @@ -cthulhu_python_PYTHON = \ - __init__.py \ - CapsLockHack.plugin \ - CapsLockHack.py - -cthulhu_pythondir=$(pkgpythondir)/plugins/CapsLockHack - diff --git a/src/cthulhu/plugins/CapsLockHack/__init__.py b/src/cthulhu/plugins/CapsLockHack/__init__.py deleted file mode 100644 index 782103c..0000000 --- a/src/cthulhu/plugins/CapsLockHack/__init__.py +++ /dev/null @@ -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 - diff --git a/src/cthulhu/plugins/Makefile.am b/src/cthulhu/plugins/Makefile.am index 042fc2b..b90e9d5 100644 --- a/src/cthulhu/plugins/Makefile.am +++ b/src/cthulhu/plugins/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = Clipboard DisplayVersion hello_world self_voice Time MouseReview ByeCthulhu HelloCthulhu PluginManager CapsLockHack SimplePluginSystem +SUBDIRS = Clipboard DisplayVersion hello_world self_voice Time ByeCthulhu HelloCthulhu PluginManager SimplePluginSystem cthulhu_pythondir=$(pkgpythondir)/plugins diff --git a/src/cthulhu/plugins/MouseReview/Makefile.am b/src/cthulhu/plugins/MouseReview/Makefile.am deleted file mode 100644 index e7d9089..0000000 --- a/src/cthulhu/plugins/MouseReview/Makefile.am +++ /dev/null @@ -1,7 +0,0 @@ -cthulhu_python_PYTHON = \ - __init__.py \ - MouseReview.plugin \ - MouseReview.py - -cthulhu_pythondir=$(pkgpythondir)/plugins/MouseReview - diff --git a/src/cthulhu/plugins/MouseReview/MouseReview.plugin b/src/cthulhu/plugins/MouseReview/MouseReview.plugin deleted file mode 100644 index 986dfad..0000000 --- a/src/cthulhu/plugins/MouseReview/MouseReview.plugin +++ /dev/null @@ -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 diff --git a/src/cthulhu/plugins/MouseReview/MouseReview.py b/src/cthulhu/plugins/MouseReview/MouseReview.py deleted file mode 100644 index ad409c2..0000000 --- a/src/cthulhu/plugins/MouseReview/MouseReview.py +++ /dev/null @@ -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) diff --git a/src/cthulhu/plugins/MouseReview/__init__.py b/src/cthulhu/plugins/MouseReview/__init__.py deleted file mode 100644 index 782103c..0000000 --- a/src/cthulhu/plugins/MouseReview/__init__.py +++ /dev/null @@ -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 - diff --git a/src/cthulhu/settings.py b/src/cthulhu/settings.py index 787c5cd..694123d 100644 --- a/src/cthulhu/settings.py +++ b/src/cthulhu/settings.py @@ -413,4 +413,4 @@ presentChatRoomLast = False presentLiveRegionFromInactiveTab = False # Plugins -activePlugins = ['Clipboard', 'DisplayVersion', 'MouseReview', 'ByeCthulhu', 'Time', 'HelloCthulhu', 'hello_world', 'self_voice', 'PluginManager', 'SimplePluginSystem'] +activePlugins = ['Clipboard', 'DisplayVersion', 'ByeCthulhu', 'Time', 'HelloCthulhu', 'hello_world', 'self_voice', 'PluginManager', 'SimplePluginSystem'] From c712bea421206f77e227901a6b05e65bae216eb6 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 4 Apr 2025 16:03:35 -0400 Subject: [PATCH 08/14] Attempt to fix keybindings not working from plugins. --- src/cthulhu/cthulhu.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index fcf628b..ed26293 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -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) From 35a83327ac83bb6a32c8f409bcbcffeef2d826ae Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 4 Apr 2025 16:25:28 -0400 Subject: [PATCH 09/14] Convert simple plugin plugin to new plugin format. Hmm, gotta get in a couple more... plugin plugin plugin! lol --- configure.ac | 1 - src/cthulhu/plugins/Makefile.am | 2 +- .../plugins/SimplePluginSystem/Makefile.am | 4 +- .../SimplePluginSystem.plugin | 11 - .../SimplePluginSystem/SimplePluginSystem.py | 291 --------------- .../plugins/SimplePluginSystem/plugin.info | 9 + .../plugins/SimplePluginSystem/plugin.py | 343 ++++++++++++++++++ src/cthulhu/plugins/Time/Makefile.am | 7 - src/cthulhu/plugins/Time/Time.plugin | 6 - src/cthulhu/plugins/Time/Time.py | 60 --- src/cthulhu/plugins/Time/__init__.py | 25 -- 11 files changed, 355 insertions(+), 404 deletions(-) delete mode 100644 src/cthulhu/plugins/SimplePluginSystem/SimplePluginSystem.plugin delete mode 100644 src/cthulhu/plugins/SimplePluginSystem/SimplePluginSystem.py create mode 100644 src/cthulhu/plugins/SimplePluginSystem/plugin.info create mode 100644 src/cthulhu/plugins/SimplePluginSystem/plugin.py delete mode 100644 src/cthulhu/plugins/Time/Makefile.am delete mode 100644 src/cthulhu/plugins/Time/Time.plugin delete mode 100644 src/cthulhu/plugins/Time/Time.py delete mode 100644 src/cthulhu/plugins/Time/__init__.py diff --git a/configure.ac b/configure.ac index b3356e3..6f76e83 100644 --- a/configure.ac +++ b/configure.ac @@ -132,7 +132,6 @@ src/cthulhu/plugins/Clipboard/Makefile src/cthulhu/plugins/DisplayVersion/Makefile src/cthulhu/plugins/hello_world/Makefile src/cthulhu/plugins/self_voice/Makefile -src/cthulhu/plugins/Time/Makefile src/cthulhu/plugins/SimplePluginSystem/Makefile src/cthulhu/backends/Makefile src/cthulhu/cthulhu_bin.py diff --git a/src/cthulhu/plugins/Makefile.am b/src/cthulhu/plugins/Makefile.am index b90e9d5..9066cb3 100644 --- a/src/cthulhu/plugins/Makefile.am +++ b/src/cthulhu/plugins/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = Clipboard DisplayVersion hello_world self_voice Time ByeCthulhu HelloCthulhu PluginManager SimplePluginSystem +SUBDIRS = Clipboard DisplayVersion hello_world self_voice ByeCthulhu HelloCthulhu PluginManager SimplePluginSystem cthulhu_pythondir=$(pkgpythondir)/plugins diff --git a/src/cthulhu/plugins/SimplePluginSystem/Makefile.am b/src/cthulhu/plugins/SimplePluginSystem/Makefile.am index 6bd1d0a..5f64b8d 100644 --- a/src/cthulhu/plugins/SimplePluginSystem/Makefile.am +++ b/src/cthulhu/plugins/SimplePluginSystem/Makefile.am @@ -1,7 +1,7 @@ cthulhu_python_PYTHON = \ __init__.py \ - SimplePluginSystem.plugin \ - SimplePluginSystem.py + plugin.info \ + plugin.py cthulhu_pythondir=$(pkgpythondir)/plugins/SimplePluginSystem diff --git a/src/cthulhu/plugins/SimplePluginSystem/SimplePluginSystem.plugin b/src/cthulhu/plugins/SimplePluginSystem/SimplePluginSystem.plugin deleted file mode 100644 index 60dde6d..0000000 --- a/src/cthulhu/plugins/SimplePluginSystem/SimplePluginSystem.plugin +++ /dev/null @@ -1,11 +0,0 @@ -[Plugin] -Module=SimplePluginSystem -Loader=python3 -Name=Simple Plugin System -Description=Simple plugin system implementation for Cthulhu -Authors=Chrys ;Storm Dragon -Copyright=Copyright Â2024 Chrys, Storm Dragon -Website=https://git.stormux.org/storm/cthulhu -Version=1.0 -Builtin=true - diff --git a/src/cthulhu/plugins/SimplePluginSystem/SimplePluginSystem.py b/src/cthulhu/plugins/SimplePluginSystem/SimplePluginSystem.py deleted file mode 100644 index 5fa6b64..0000000 --- a/src/cthulhu/plugins/SimplePluginSystem/SimplePluginSystem.py +++ /dev/null @@ -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 diff --git a/src/cthulhu/plugins/SimplePluginSystem/plugin.info b/src/cthulhu/plugins/SimplePluginSystem/plugin.info new file mode 100644 index 0000000..cd4aad3 --- /dev/null +++ b/src/cthulhu/plugins/SimplePluginSystem/plugin.info @@ -0,0 +1,9 @@ +[Plugin] +Name = Simple Plugin System +Module = SimplePluginSystem +Description = Simple plugin system implementation for Cthulhu +Authors = Storm Dragon +Copyright = Copyright (c) 2025 Stormux +Website = https://git.stormux.org/storm/cthulhu +Version = 1.0 +Category = System diff --git a/src/cthulhu/plugins/SimplePluginSystem/plugin.py b/src/cthulhu/plugins/SimplePluginSystem/plugin.py new file mode 100644 index 0000000..f53c663 --- /dev/null +++ b/src/cthulhu/plugins/SimplePluginSystem/plugin.py @@ -0,0 +1,343 @@ +#!/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 + +from cthulhu.plugin import Plugin, cthulhu_hookimpl + +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 + self.registerGestureByString(currPluginSetting['function'], _(currPluginSetting['pluginname']), 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 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}") + self.plugin_list = glob.glob(self.plugin_repo + '*') + + for currplugin in self.plugin_list: + try: + 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}") diff --git a/src/cthulhu/plugins/Time/Makefile.am b/src/cthulhu/plugins/Time/Makefile.am deleted file mode 100644 index e729ddc..0000000 --- a/src/cthulhu/plugins/Time/Makefile.am +++ /dev/null @@ -1,7 +0,0 @@ -cthulhu_python_PYTHON = \ - __init__.py \ - Time.plugin \ - Time.py - -cthulhu_pythondir=$(pkgpythondir)/plugins/Time - diff --git a/src/cthulhu/plugins/Time/Time.plugin b/src/cthulhu/plugins/Time/Time.plugin deleted file mode 100644 index 2f290b5..0000000 --- a/src/cthulhu/plugins/Time/Time.plugin +++ /dev/null @@ -1,6 +0,0 @@ -[Plugin] -Module=Time -Loader=python3 -Name=Time -Description=Present current time -Authors=Chrys chrys@linux-a11y.org diff --git a/src/cthulhu/plugins/Time/Time.py b/src/cthulhu/plugins/Time/Time.py deleted file mode 100644 index 02114c2..0000000 --- a/src/cthulhu/plugins/Time/Time.py +++ /dev/null @@ -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 diff --git a/src/cthulhu/plugins/Time/__init__.py b/src/cthulhu/plugins/Time/__init__.py deleted file mode 100644 index 782103c..0000000 --- a/src/cthulhu/plugins/Time/__init__.py +++ /dev/null @@ -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 - From 1b4c4916e3cd31f8fbbce6cb1a03af039392acbe Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 4 Apr 2025 16:33:53 -0400 Subject: [PATCH 10/14] Hopefully fixed an error in simple plugin system. --- .../plugins/SimplePluginSystem/plugin.py | 81 +++++++++++++++---- 1 file changed, 65 insertions(+), 16 deletions(-) diff --git a/src/cthulhu/plugins/SimplePluginSystem/plugin.py b/src/cthulhu/plugins/SimplePluginSystem/plugin.py index f53c663..3ad1de9 100644 --- a/src/cthulhu/plugins/SimplePluginSystem/plugin.py +++ b/src/cthulhu/plugins/SimplePluginSystem/plugin.py @@ -1,6 +1,9 @@ #!/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 @@ -17,6 +20,8 @@ # 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 """Simple Plugin System for Cthulhu.""" @@ -28,9 +33,13 @@ 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 @@ -48,11 +57,11 @@ def outputMessage(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) @@ -68,22 +77,22 @@ class SimplePluginSystem(Plugin): # 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}") @@ -93,7 +102,7 @@ class SimplePluginSystem(Plugin): # 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 @@ -128,7 +137,15 @@ class SimplePluginSystem(Plugin): if shortcut != '': logger.debug(f"Registering shortcut: {shortcut}") currPluginSetting['shortcut'] = shortcut - self.registerGestureByString(currPluginSetting['function'], _(currPluginSetting['pluginname']), 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): @@ -237,7 +254,7 @@ class SimplePluginSystem(Plugin): 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): @@ -298,13 +315,45 @@ class SimplePluginSystem(Plugin): 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: + # Try to get the InputEventManager and register the shortcut + input_manager = self.app.getDynamicApiManager().getAPI('InputEventManager') + if input_manager: + input_manager.registerGestureByString(function, description, shortcut) + logger.debug(f"Registered shortcut {shortcut} for {description}") + else: + logger.error("Could not get InputEventManager API") + 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: + # Try to get the InputEventManager and unregister the shortcut + input_manager = self.app.getDynamicApiManager().getAPI('InputEventManager') + if input_manager: + input_manager.unregisterGestureByString(shortcut) + logger.debug(f"Unregistered shortcut {shortcut}") + else: + logger.error("Could not get InputEventManager API") + 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}") self.plugin_list = glob.glob(self.plugin_repo + '*') - + for currplugin in self.plugin_list: try: currPluginSetting = self.initSettings() @@ -313,30 +362,30 @@ class SimplePluginSystem(Plugin): 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: From 8a79725df865e330e7271e6c08383d20e80ac8b8 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 4 Apr 2025 17:08:18 -0400 Subject: [PATCH 11/14] Update clipboard plugin to new pluggy format. --- .../plugins/Clipboard/Clipboard.plugin | 6 - src/cthulhu/plugins/Clipboard/Clipboard.py | 101 ----------- src/cthulhu/plugins/Clipboard/Makefile.am | 4 +- src/cthulhu/plugins/Clipboard/plugin.info | 7 + src/cthulhu/plugins/Clipboard/plugin.py | 161 ++++++++++++++++++ 5 files changed, 170 insertions(+), 109 deletions(-) delete mode 100644 src/cthulhu/plugins/Clipboard/Clipboard.plugin delete mode 100644 src/cthulhu/plugins/Clipboard/Clipboard.py create mode 100644 src/cthulhu/plugins/Clipboard/plugin.info create mode 100644 src/cthulhu/plugins/Clipboard/plugin.py diff --git a/src/cthulhu/plugins/Clipboard/Clipboard.plugin b/src/cthulhu/plugins/Clipboard/Clipboard.plugin deleted file mode 100644 index 72fb0a3..0000000 --- a/src/cthulhu/plugins/Clipboard/Clipboard.plugin +++ /dev/null @@ -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 diff --git a/src/cthulhu/plugins/Clipboard/Clipboard.py b/src/cthulhu/plugins/Clipboard/Clipboard.py deleted file mode 100644 index 08b7572..0000000 --- a/src/cthulhu/plugins/Clipboard/Clipboard.py +++ /dev/null @@ -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 diff --git a/src/cthulhu/plugins/Clipboard/Makefile.am b/src/cthulhu/plugins/Clipboard/Makefile.am index 55115d9..3d3f13c 100644 --- a/src/cthulhu/plugins/Clipboard/Makefile.am +++ b/src/cthulhu/plugins/Clipboard/Makefile.am @@ -1,7 +1,7 @@ cthulhu_python_PYTHON = \ __init__.py \ - Clipboard.plugin \ - Clipboard.py + plugin.info \ + plugin.py cthulhu_pythondir=$(pkgpythondir)/plugins/Clipboard diff --git a/src/cthulhu/plugins/Clipboard/plugin.info b/src/cthulhu/plugins/Clipboard/plugin.info new file mode 100644 index 0000000..0e15232 --- /dev/null +++ b/src/cthulhu/plugins/Clipboard/plugin.info @@ -0,0 +1,7 @@ +[Plugin] +Name = Clipboard +Module = Clipboard +Description = Present the content of the current clipboard +Authors = Storm Dragon +Version = 1.0 +Category = Utilities diff --git a/src/cthulhu/plugins/Clipboard/plugin.py b/src/cthulhu/plugins/Clipboard/plugin.py new file mode 100644 index 0000000..e21e6ae --- /dev/null +++ b/src/cthulhu/plugins/Clipboard/plugin.py @@ -0,0 +1,161 @@ +#!/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. +# + +"""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 + 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") From 2c28021ed4d0b459fcbfbd79fa1f807051e0376a Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 4 Apr 2025 17:27:30 -0400 Subject: [PATCH 12/14] Removed the old plugin manager. It didn't work anyway and needs to be rewritten. --- src/cthulhu/plugins/Makefile.am | 2 +- src/cthulhu/plugins/PluginManager/Makefile.am | 8 - .../PluginManager/PluginManager.plugin | 14 - .../plugins/PluginManager/PluginManager.py | 61 ---- .../plugins/PluginManager/PluginManagerUi.py | 307 ------------------ .../PluginManager/PluginManagerUiListBox.py | 107 ------ .../PluginManagerUiListBox_tut.py | 117 ------- src/cthulhu/plugins/PluginManager/__init__.py | 25 -- src/cthulhu/settings.py | 2 +- 9 files changed, 2 insertions(+), 641 deletions(-) delete mode 100644 src/cthulhu/plugins/PluginManager/Makefile.am delete mode 100644 src/cthulhu/plugins/PluginManager/PluginManager.plugin delete mode 100644 src/cthulhu/plugins/PluginManager/PluginManager.py delete mode 100644 src/cthulhu/plugins/PluginManager/PluginManagerUi.py delete mode 100644 src/cthulhu/plugins/PluginManager/PluginManagerUiListBox.py delete mode 100644 src/cthulhu/plugins/PluginManager/PluginManagerUiListBox_tut.py delete mode 100644 src/cthulhu/plugins/PluginManager/__init__.py diff --git a/src/cthulhu/plugins/Makefile.am b/src/cthulhu/plugins/Makefile.am index 9066cb3..a79ed41 100644 --- a/src/cthulhu/plugins/Makefile.am +++ b/src/cthulhu/plugins/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = Clipboard DisplayVersion hello_world self_voice ByeCthulhu HelloCthulhu PluginManager SimplePluginSystem +SUBDIRS = Clipboard DisplayVersion hello_world self_voice ByeCthulhu HelloCthulhu SimplePluginSystem cthulhu_pythondir=$(pkgpythondir)/plugins diff --git a/src/cthulhu/plugins/PluginManager/Makefile.am b/src/cthulhu/plugins/PluginManager/Makefile.am deleted file mode 100644 index 6bcb860..0000000 --- a/src/cthulhu/plugins/PluginManager/Makefile.am +++ /dev/null @@ -1,8 +0,0 @@ -cthulhu_python_PYTHON = \ - __init__.py \ - PluginManager.plugin \ - PluginManager.py \ - PluginManagerUi.py - -cthulhu_pythondir=$(pkgpythondir)/plugins/PluginManager - diff --git a/src/cthulhu/plugins/PluginManager/PluginManager.plugin b/src/cthulhu/plugins/PluginManager/PluginManager.plugin deleted file mode 100644 index f992ba2..0000000 --- a/src/cthulhu/plugins/PluginManager/PluginManager.plugin +++ /dev/null @@ -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= diff --git a/src/cthulhu/plugins/PluginManager/PluginManager.py b/src/cthulhu/plugins/PluginManager/PluginManager.py deleted file mode 100644 index 6aadf1e..0000000 --- a/src/cthulhu/plugins/PluginManager/PluginManager.py +++ /dev/null @@ -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() diff --git a/src/cthulhu/plugins/PluginManager/PluginManagerUi.py b/src/cthulhu/plugins/PluginManager/PluginManagerUi.py deleted file mode 100644 index f0fd353..0000000 --- a/src/cthulhu/plugins/PluginManager/PluginManagerUi.py +++ /dev/null @@ -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("%s" % _('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() diff --git a/src/cthulhu/plugins/PluginManager/PluginManagerUiListBox.py b/src/cthulhu/plugins/PluginManager/PluginManagerUiListBox.py deleted file mode 100644 index c25689a..0000000 --- a/src/cthulhu/plugins/PluginManager/PluginManagerUiListBox.py +++ /dev/null @@ -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() diff --git a/src/cthulhu/plugins/PluginManager/PluginManagerUiListBox_tut.py b/src/cthulhu/plugins/PluginManager/PluginManagerUiListBox_tut.py deleted file mode 100644 index 6b92e6c..0000000 --- a/src/cthulhu/plugins/PluginManager/PluginManagerUiListBox_tut.py +++ /dev/null @@ -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() diff --git a/src/cthulhu/plugins/PluginManager/__init__.py b/src/cthulhu/plugins/PluginManager/__init__.py deleted file mode 100644 index 782103c..0000000 --- a/src/cthulhu/plugins/PluginManager/__init__.py +++ /dev/null @@ -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 - diff --git a/src/cthulhu/settings.py b/src/cthulhu/settings.py index 694123d..8d1a1f4 100644 --- a/src/cthulhu/settings.py +++ b/src/cthulhu/settings.py @@ -413,4 +413,4 @@ presentChatRoomLast = False presentLiveRegionFromInactiveTab = False # Plugins -activePlugins = ['Clipboard', 'DisplayVersion', 'ByeCthulhu', 'Time', 'HelloCthulhu', 'hello_world', 'self_voice', 'PluginManager', 'SimplePluginSystem'] +activePlugins = ['Clipboard', 'DisplayVersion', 'ByeCthulhu', 'HelloCthulhu', 'hello_world', 'self_voice', 'SimplePluginSystem'] From 48575ab6cd31c3b2bb7568e5e334210e4f5c3784 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 4 Apr 2025 17:28:31 -0400 Subject: [PATCH 13/14] Removed the old plugin manager. It didn't work anyway and needs to be rewritten. --- configure.ac | 1 - 1 file changed, 1 deletion(-) diff --git a/configure.ac b/configure.ac index 6f76e83..39a5497 100644 --- a/configure.ac +++ b/configure.ac @@ -127,7 +127,6 @@ 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 From 02be96aa694a7b1d504045bdc8aa2f4bde15cbde Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 4 Apr 2025 18:04:58 -0400 Subject: [PATCH 14/14] Try to fix clipboard and simple plugins. --- src/cthulhu/plugins/Clipboard/plugin.py | 32 +++++++ .../plugins/SimplePluginSystem/plugin.py | 93 +++++++++++-------- 2 files changed, 86 insertions(+), 39 deletions(-) diff --git a/src/cthulhu/plugins/Clipboard/plugin.py b/src/cthulhu/plugins/Clipboard/plugin.py index e21e6ae..5388d87 100644 --- a/src/cthulhu/plugins/Clipboard/plugin.py +++ b/src/cthulhu/plugins/Clipboard/plugin.py @@ -20,6 +20,8 @@ # 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.""" @@ -62,6 +64,36 @@ class Clipboard(Plugin): 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.""" diff --git a/src/cthulhu/plugins/SimplePluginSystem/plugin.py b/src/cthulhu/plugins/SimplePluginSystem/plugin.py index 3ad1de9..95948ea 100644 --- a/src/cthulhu/plugins/SimplePluginSystem/plugin.py +++ b/src/cthulhu/plugins/SimplePluginSystem/plugin.py @@ -1,9 +1,6 @@ #!/usr/bin/env python3 # # Copyright (c) 2024 Stormux -# Copyright (c) 2010-2012 The Orca Team -# Copyright (c) 2012 Igalia, S.L. -# Copyright (c) 2005-2010 Sun Microsystems Inc. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -20,8 +17,6 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca """Simple Plugin System for Cthulhu.""" @@ -57,11 +52,11 @@ def outputMessage(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) @@ -77,22 +72,22 @@ class SimplePluginSystem(Plugin): # 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}") @@ -102,7 +97,7 @@ class SimplePluginSystem(Plugin): # 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 @@ -144,7 +139,7 @@ class SimplePluginSystem(Plugin): # 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 @@ -254,7 +249,7 @@ class SimplePluginSystem(Plugin): 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): @@ -317,75 +312,95 @@ class SimplePluginSystem(Plugin): def registerGestureByString(self, function, description, shortcut): """Register a keyboard shortcut for a function. - + This is a compatibility wrapper for the new plugin system. """ try: - # Try to get the InputEventManager and register the shortcut - input_manager = self.app.getDynamicApiManager().getAPI('InputEventManager') - if input_manager: - input_manager.registerGestureByString(function, description, shortcut) - logger.debug(f"Registered shortcut {shortcut} for {description}") + 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("Could not get InputEventManager API") + 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: - # Try to get the InputEventManager and unregister the shortcut - input_manager = self.app.getDynamicApiManager().getAPI('InputEventManager') - if input_manager: - input_manager.unregisterGestureByString(shortcut) - logger.debug(f"Unregistered shortcut {shortcut}") + 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("Could not get InputEventManager API") + 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}") - self.plugin_list = glob.glob(self.plugin_repo + '*') - - for currplugin in self.plugin_list: + 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: