From 935ba33081e212fcf7788b82ace9051f74bf0d2f Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Mon, 12 Jan 2026 23:52:57 -0500 Subject: [PATCH 01/13] Version bump for testing. --- distro-packages/Arch-Linux/PKGBUILD | 2 +- meson.build | 2 +- src/cthulhu/cthulhuVersion.py | 4 ++-- src/cthulhu/cthulhu_gtkbuilder.py | 22 +++++++++++++++++++++- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/distro-packages/Arch-Linux/PKGBUILD b/distro-packages/Arch-Linux/PKGBUILD index 2f40a3f..b4ee574 100644 --- a/distro-packages/Arch-Linux/PKGBUILD +++ b/distro-packages/Arch-Linux/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Storm Dragon pkgname=cthulhu -pkgver=2026.01.10 +pkgver=2026.01.12 pkgrel=1 pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca" url="https://git.stormux.org/storm/cthulhu" diff --git a/meson.build b/meson.build index 3be58d2..ac9ee02 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('cthulhu', - version: '2026.01.10-master', + version: '2026.01.12-testing', meson_version: '>= 1.0.0', ) diff --git a/src/cthulhu/cthulhuVersion.py b/src/cthulhu/cthulhuVersion.py index d2c567a..60f7abd 100644 --- a/src/cthulhu/cthulhuVersion.py +++ b/src/cthulhu/cthulhuVersion.py @@ -23,5 +23,5 @@ # Forked from Orca screen reader. # Cthulhu project: https://git.stormux.org/storm/cthulhu -version = "2026.01.10" -codeName = "master" +version = "2026.01.12" +codeName = "testing" diff --git a/src/cthulhu/cthulhu_gtkbuilder.py b/src/cthulhu/cthulhu_gtkbuilder.py index d676bb7..747bd11 100644 --- a/src/cthulhu/cthulhu_gtkbuilder.py +++ b/src/cthulhu/cthulhu_gtkbuilder.py @@ -32,6 +32,8 @@ __copyright__ = "Copyright (c) 2005-2009 Sun Microsystems Inc." __license__ = "LGPL" import gettext +import importlib.resources as resources +import os from gi.repository import Gtk from .cthulhu_i18n import _ @@ -52,7 +54,7 @@ class GtkBuilderWrapper: # Load GtkBuilder file. self.builder = Gtk.Builder() self.builder.set_translation_domain(gettext.textdomain()) - self.builder.add_from_file(fileName) + self._add_builder_from_file(fileName) self.gtkWindow = self.builder.get_object(windowName) # Force the localization of widgets to work around a GtkBuilder @@ -69,6 +71,24 @@ class GtkBuilderWrapper: instance_attributes[attribute] = getattr(self, attribute) self.builder.connect_signals(instance_attributes) + def _add_builder_from_file(self, fileName): + if os.path.exists(fileName): + self.builder.add_from_file(fileName) + return + + resourceName = os.path.basename(fileName) + try: + uiPath = resources.files(__package__).joinpath(resourceName) + except Exception: + uiPath = None + + if uiPath and uiPath.is_file(): + with resources.as_file(uiPath) as resolvedPath: + self.builder.add_from_file(str(resolvedPath)) + return + + self.builder.add_from_file(fileName) + def set_cthulhu_icon(self): """Get the icon in all sizes from the current theme and set them as default for all application windows. From c510f5a45c21bd25f947eb03664d125846ada6af Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Mon, 12 Jan 2026 23:53:47 -0500 Subject: [PATCH 02/13] Forgot to add pyproject.toml. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 210fdf3..a0166cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,3 +25,4 @@ path = "src/cthulhu/__init__.py" [tool.hatch.build.targets.wheel] packages = ["src/cthulhu"] +include = ["src/cthulhu/*.ui"] From 06cd376cd45691f381a91a0110939591939b06bd Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 13 Jan 2026 07:49:51 -0500 Subject: [PATCH 03/13] First round of bug fixes and plugin capability extensions. Creating a preferences tab should no longer require editing Cthulhu itself. --- README.md | 8 + src/cthulhu/cthulhu.py | 13 +- src/cthulhu/cthulhu_gui_prefs.py | 178 ++++++++++++++- src/cthulhu/plugin.py | 8 + src/cthulhu/plugins/AIAssistant/plugin.info | 4 +- src/cthulhu/plugins/AIAssistant/plugin.py | 211 ++++++++++++++++++ .../plugins/IndentationAudio/plugin.info | 4 +- .../plugins/IndentationAudio/plugin.py | 164 +++++++++++++- src/cthulhu/plugins/OCR/README.md | 6 +- src/cthulhu/plugins/PluginManager/plugin.py | 9 + src/cthulhu/plugins/hello_world/README.md | 11 +- src/cthulhu/settings.py | 6 +- 12 files changed, 607 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 48c3ee6..faa9cfb 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ toolkit, OpenOffice/LibreOffice, Gecko, WebKitGtk, and KDE Qt toolkit. - **Extensible architecture**: Plugin system using pluggy framework - **Hot-reloadable plugins**: Add functionality without restarting - **Community plugins**: User and system plugin directories +- **Plugin preferences**: Plugins can provide preferences pages that appear only when the plugin is active ### Remote Control - **D-Bus interface**: External control via D-Bus service @@ -83,6 +84,13 @@ The `PluginSystemManager` module provides **session-only** plugin control (no pr - `SetPluginActive` (parameterized) - `RescanPlugins` +### Plugin Preferences Pages + +Plugins can add their own Preferences tab without modifying Cthulhu core code. +Implement `getPreferencesGUI()` to return a Gtk widget (or `(widget, label)`), +and `getPreferencesFromGUI()` to return a settings dict. The tab appears only +when the plugin is active. + ### More Documentation See `README-REMOTE-CONTROLLER.md` and `REMOTE-CONTROLLER-COMMANDS.md` for the full D-Bus API diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index 2f180b9..a95fadf 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -470,7 +470,18 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False): cthulhuApp.getSignalManager().emitSignal('load-setting-begin') # NOW load plugins after script system is ready - activePlugins = list(_settingsManager.getSetting('activePlugins')) + activePluginsSetting = _settingsManager.getSetting('activePlugins') or [] + debug.printMessage( + debug.LEVEL_INFO, + f"CTHULHU: Active plugins defaults: {settings.activePlugins}", + True, + ) + debug.printMessage( + debug.LEVEL_INFO, + f"CTHULHU: Active plugins from settings manager: {activePluginsSetting}", + True, + ) + activePlugins = list(activePluginsSetting) debug.printMessage(debug.LEVEL_INFO, f'CTHULHU: Loading active plugins: {activePlugins}', True) cthulhuApp.getPluginSystemManager().setActivePlugins(activePlugins) diff --git a/src/cthulhu/cthulhu_gui_prefs.py b/src/cthulhu/cthulhu_gui_prefs.py index 6feb971..a9925df 100644 --- a/src/cthulhu/cthulhu_gui_prefs.py +++ b/src/cthulhu/cthulhu_gui_prefs.py @@ -155,6 +155,9 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): self._plugin_update_progress = None self._plugin_update_status = None self._plugin_update_in_progress = False + self._plugin_tabs = {} + self._plugin_tabs_cached = False + self._dynamic_plugin_tabs = {} self.screenHeight = None self.screenWidth = None self.speechFamiliesChoice = None @@ -396,6 +399,9 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): label = Gtk.Label(label=AXObject.get_name(self.script.app)) self.get_widget("notebook").append_page(appPage, label) + self._cache_plugin_tabs() + self._update_plugin_tabs() + self._refresh_dynamic_plugin_tabs() self._initGUIState() self._initSoundThemeState() @@ -799,6 +805,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): path = self._plugin_model.get_path(new_iter) self._plugin_treeview.expand_to_path(path) selection.select_path(path) + self._update_plugin_tabs() def _get_active_plugins_from_ui(self): existing_plugins = list(self.prefsDict.get("activePlugins", settings.activePlugins) or []) @@ -835,6 +842,147 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): self._plugin_sources_original = list(plugin_sources) self._populate_plugin_list() + self._update_plugin_tabs(active_plugins) + + def _get_active_plugin_infos(self): + manager = cthulhu.cthulhuApp.getPluginSystemManager() + if not manager: + return [] + return [info for info in manager.plugins if info.loaded and info.instance] + + def _get_plugin_preferences_page(self, plugin_info): + plugin_instance = plugin_info.instance + if not plugin_instance: + return None + + if hasattr(plugin_instance, "getPreferencesGUI"): + page = plugin_instance.getPreferencesGUI() + elif hasattr(plugin_instance, "get_preferences_gui"): + page = plugin_instance.get_preferences_gui() + else: + return None + + if not page: + return None + + label = plugin_info.get_name() or plugin_info.get_module_name() + if isinstance(page, (list, tuple)) and len(page) == 2: + page, label = page + + if isinstance(label, str): + label_widget = Gtk.Label(label=label) + else: + label_widget = label + + return page, label_widget + + def _refresh_dynamic_plugin_tabs(self): + notebook = self.get_widget("notebook") + if not notebook: + return + + for tab in list(self._dynamic_plugin_tabs.values()): + page = tab.get("page") + if page: + page_num = notebook.page_num(page) + if page_num != -1: + notebook.remove_page(page_num) + + self._dynamic_plugin_tabs = {} + + plugin_infos = self._get_active_plugin_infos() + for plugin_info in sorted(plugin_infos, key=lambda item: (item.get_name() or item.get_module_name()).lower()): + result = self._get_plugin_preferences_page(plugin_info) + if not result: + continue + + page, label = result + if not page or not label: + continue + + notebook.append_page(page, label) + page.show_all() + label.show() + self._dynamic_plugin_tabs[plugin_info.get_module_name()] = { + "page": page, + "label": label, + } + + for plugin_name in self._dynamic_plugin_tabs: + tab = self._plugin_tabs.get(plugin_name) + if not tab: + continue + page = tab.get("page") + if not page: + continue + page_num = notebook.page_num(page) + if page_num != -1: + notebook.remove_page(page_num) + + def _cache_plugin_tabs(self): + if self._plugin_tabs_cached: + return + + notebook = self.get_widget("notebook") + if not notebook: + return + + tab_specs = [ + ("AIAssistant", "aiPage", "aiTabLabel"), + ("OCR", "ocrGrid", "ocrTabLabel"), + ] + + for plugin_name, page_id, label_id in tab_specs: + page = self.get_widget(page_id) + label = self.get_widget(label_id) + if not page or not label: + continue + position = notebook.page_num(page) + self._plugin_tabs[plugin_name] = { + "page": page, + "label": label, + "position": position, + } + + self._plugin_tabs_cached = True + + def _get_active_plugins_for_tabs(self): + if self._plugin_iters: + return self._get_active_plugins_from_ui() + return list(self.prefsDict.get("activePlugins", settings.activePlugins) or []) + + @staticmethod + def _plugin_active(active_plugins, names): + active_lower = {name.lower() for name in active_plugins} + return any(name.lower() in active_lower for name in names) + + def _update_plugin_tabs(self, active_plugins=None): + if not self._plugin_tabs: + return + + notebook = self.get_widget("notebook") + if not notebook: + return + + if active_plugins is None: + active_plugins = self._get_active_plugins_for_tabs() + + for plugin_name, tab in self._plugin_tabs.items(): + if plugin_name in self._dynamic_plugin_tabs: + continue + page = tab["page"] + label = tab["label"] + position = tab["position"] + page_num = notebook.page_num(page) + is_active = self._plugin_active(active_plugins, [plugin_name]) + + if is_active and page_num == -1: + insert_pos = min(max(position, 0), notebook.get_n_pages()) + notebook.insert_page(page, label, insert_pos) + page.show_all() + label.show() + elif not is_active and page_num != -1: + notebook.remove_page(page_num) def _getACSSForVoiceType(self, voiceType): """Return the ACSS value for the given voice type. @@ -864,11 +1012,31 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): pronunciationDict = self.getModelDict(self.pronunciationModel) keyBindingsDict = self.getKeyBindingsModelDict(self.keyBindingsModel) self.prefsDict.update(self.script.getPreferencesFromGUI()) + self.prefsDict.update(self._get_plugin_preferences_from_gui()) _settingsManager.saveSettings(self.script, self.prefsDict, pronunciationDict, keyBindingsDict) + def _get_plugin_preferences_from_gui(self): + preferences = {} + for plugin_info in self._get_active_plugin_infos(): + plugin_instance = plugin_info.instance + if not plugin_instance: + continue + + if hasattr(plugin_instance, "getPreferencesFromGUI"): + plugin_prefs = plugin_instance.getPreferencesFromGUI() + elif hasattr(plugin_instance, "get_preferences_from_gui"): + plugin_prefs = plugin_instance.get_preferences_from_gui() + else: + continue + + if isinstance(plugin_prefs, dict): + preferences.update(plugin_prefs) + + return preferences + def _getKeyValueForVoiceType(self, voiceType, key, useDefault=True): """Look for the value of the given key in the voice dictionary for the given voice type. @@ -2283,9 +2451,13 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): if self.script.app: self.get_widget('profilesFrame').set_sensitive(False) + active_plugins = self._get_active_plugins_for_tabs() + self._update_plugin_tabs(active_plugins) + # AI Assistant settings # - self._initAIState() + if self._plugin_active(active_plugins, ["AIAssistant"]): + self._initAIState() # Indentation settings # @@ -2293,7 +2465,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): # OCR Plugin settings # - self._initOCRState() + if self._plugin_active(active_plugins, ["OCR"]): + self._initOCRState() def __initProfileCombo(self): """Adding available profiles and setting active as the active one""" @@ -4149,6 +4322,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): self._apply_plugin_changes() self.writeUserPreferences() cthulhu.loadUserSettings(self.script) + self._refresh_dynamic_plugin_tabs() braille.checkBrailleSetting() self._initSpeechState() self._populateKeyBindings() diff --git a/src/cthulhu/plugin.py b/src/cthulhu/plugin.py index bb35bc4..8063233 100644 --- a/src/cthulhu/plugin.py +++ b/src/cthulhu/plugin.py @@ -87,6 +87,14 @@ class Plugin: """Get keybindings for this plugin. Override in subclasses.""" return self._bindings + def getPreferencesGUI(self): + """Return a Gtk widget for plugin preferences, or None if not provided.""" + return None + + def getPreferencesFromGUI(self): + """Return a dict of plugin preferences from the GUI.""" + return {} + def registerGestureByString(self, function, name, gestureString, learnModeEnabled=True): """Register a gesture by string.""" if self.app: diff --git a/src/cthulhu/plugins/AIAssistant/plugin.info b/src/cthulhu/plugins/AIAssistant/plugin.info index f9c5206..d954059 100644 --- a/src/cthulhu/plugins/AIAssistant/plugin.info +++ b/src/cthulhu/plugins/AIAssistant/plugin.info @@ -3,5 +3,5 @@ Name = AI Assistant Module = AIAssistant Description = AI-powered accessibility assistant for analyzing screens and taking actions Authors = Stormux -Version = 1.0.0 -Category = Accessibility \ No newline at end of file +Version = 2.0.0 +Category = Accessibility diff --git a/src/cthulhu/plugins/AIAssistant/plugin.py b/src/cthulhu/plugins/AIAssistant/plugin.py index 6bc3a39..f471ca9 100644 --- a/src/cthulhu/plugins/AIAssistant/plugin.py +++ b/src/cthulhu/plugins/AIAssistant/plugin.py @@ -69,6 +69,8 @@ class AIAssistant(Plugin): # Pre-captured screen data (to avoid capturing dialog itself) self._current_screen_data = None + self._prefs_grid = None + self._prefs_widgets = {} @cthulhu_hookimpl def activate(self, plugin=None): @@ -128,6 +130,215 @@ class AIAssistant(Plugin): self._unregister_keybindings() self._enabled = False + + def getPreferencesGUI(self): + if not self._prefs_grid: + self._prefs_grid = self._build_preferences_gui() + self._load_preferences_into_widgets() + return self._prefs_grid, "AI Assistant" + + def getPreferencesFromGUI(self): + if not self._prefs_widgets: + return {} + + provider_values = self._prefs_widgets.get("provider_values", []) + quality_values = self._prefs_widgets.get("quality_values", []) + provider_index = self._prefs_widgets["provider_combo"].get_active() + quality_index = self._prefs_widgets["quality_combo"].get_active() + + if provider_index < 0 or provider_index >= len(provider_values): + provider = settings.aiProvider + else: + provider = provider_values[provider_index] + + if quality_index < 0 or quality_index >= len(quality_values): + screenshot_quality = settings.aiScreenshotQuality + else: + screenshot_quality = quality_values[quality_index] + + return { + "aiAssistantEnabled": self._prefs_widgets["enable_check"].get_active(), + "aiProvider": provider, + "aiApiKeyFile": self._prefs_widgets["api_key_entry"].get_text().strip(), + "aiOllamaModel": self._prefs_widgets["ollama_model_entry"].get_text().strip(), + "aiOllamaEndpoint": self._prefs_widgets["ollama_endpoint_entry"].get_text().strip(), + "aiConfirmationRequired": self._prefs_widgets["confirmation_check"].get_active(), + "aiScreenshotQuality": screenshot_quality, + } + + def _build_preferences_gui(self): + grid = Gtk.Grid( + row_spacing=6, + column_spacing=12, + margin_left=12, + margin_right=12, + margin_top=12, + margin_bottom=12, + ) + + row = 0 + + enable_check = Gtk.CheckButton(label="Enable AI Assistant") + enable_check.connect("toggled", self._on_ai_enabled_toggled) + grid.attach(enable_check, 0, row, 2, 1) + row += 1 + + provider_label = Gtk.Label(label="_Provider:") + provider_label.set_use_underline(True) + provider_label.set_halign(Gtk.Align.START) + + provider_combo = Gtk.ComboBoxText() + provider_combo.set_hexpand(True) + provider_values = [ + settings.AI_PROVIDER_CLAUDE_CODE, + settings.AI_PROVIDER_CODEX, + settings.AI_PROVIDER_GEMINI, + settings.AI_PROVIDER_OLLAMA, + ] + provider_labels = ["Claude Code", "Codex", "Gemini", "Ollama"] + for label in provider_labels: + provider_combo.append_text(label) + provider_combo.connect("changed", self._on_provider_changed) + provider_label.set_mnemonic_widget(provider_combo) + + grid.attach(provider_label, 0, row, 1, 1) + grid.attach(provider_combo, 1, row, 1, 1) + row += 1 + + api_key_label = Gtk.Label(label="API _Key File:") + api_key_label.set_use_underline(True) + api_key_label.set_halign(Gtk.Align.START) + api_key_entry = Gtk.Entry() + api_key_entry.set_hexpand(True) + api_key_label.set_mnemonic_widget(api_key_entry) + + grid.attach(api_key_label, 0, row, 1, 1) + grid.attach(api_key_entry, 1, row, 1, 1) + row += 1 + + ollama_model_label = Gtk.Label(label="_Ollama Model:") + ollama_model_label.set_use_underline(True) + ollama_model_label.set_halign(Gtk.Align.START) + ollama_model_entry = Gtk.Entry() + ollama_model_entry.set_hexpand(True) + ollama_model_label.set_mnemonic_widget(ollama_model_entry) + + grid.attach(ollama_model_label, 0, row, 1, 1) + grid.attach(ollama_model_entry, 1, row, 1, 1) + row += 1 + + ollama_endpoint_label = Gtk.Label(label="Ollama _Endpoint:") + ollama_endpoint_label.set_use_underline(True) + ollama_endpoint_label.set_halign(Gtk.Align.START) + ollama_endpoint_entry = Gtk.Entry() + ollama_endpoint_entry.set_hexpand(True) + ollama_endpoint_label.set_mnemonic_widget(ollama_endpoint_entry) + + grid.attach(ollama_endpoint_label, 0, row, 1, 1) + grid.attach(ollama_endpoint_entry, 1, row, 1, 1) + row += 1 + + confirmation_check = Gtk.CheckButton(label="Require confirmation before actions") + grid.attach(confirmation_check, 0, row, 2, 1) + row += 1 + + quality_label = Gtk.Label(label="Screenshot _Quality:") + quality_label.set_use_underline(True) + quality_label.set_halign(Gtk.Align.START) + quality_combo = Gtk.ComboBoxText() + quality_combo.set_hexpand(True) + quality_values = [ + settings.AI_SCREENSHOT_QUALITY_LOW, + settings.AI_SCREENSHOT_QUALITY_MEDIUM, + settings.AI_SCREENSHOT_QUALITY_HIGH, + ] + quality_labels = ["Low", "Medium", "High"] + for label in quality_labels: + quality_combo.append_text(label) + quality_label.set_mnemonic_widget(quality_combo) + + grid.attach(quality_label, 0, row, 1, 1) + grid.attach(quality_combo, 1, row, 1, 1) + + self._prefs_widgets = { + "enable_check": enable_check, + "provider_combo": provider_combo, + "provider_values": provider_values, + "api_key_entry": api_key_entry, + "ollama_model_entry": ollama_model_entry, + "ollama_endpoint_entry": ollama_endpoint_entry, + "confirmation_check": confirmation_check, + "quality_combo": quality_combo, + "quality_values": quality_values, + } + + return grid + + def _load_preferences_into_widgets(self): + if not self._prefs_widgets: + return + + enabled = self._settings_manager.getSetting("aiAssistantEnabled") + if enabled is None: + enabled = settings.aiAssistantEnabled + self._prefs_widgets["enable_check"].set_active(enabled) + + provider = self._settings_manager.getSetting("aiProvider") or settings.aiProvider + provider_values = self._prefs_widgets.get("provider_values", []) + try: + provider_index = provider_values.index(provider) + except ValueError: + provider_index = 0 + self._prefs_widgets["provider_combo"].set_active(provider_index) + + api_key_file = self._settings_manager.getSetting("aiApiKeyFile") or settings.aiApiKeyFile + self._prefs_widgets["api_key_entry"].set_text(api_key_file or "") + + ollama_model = self._settings_manager.getSetting("aiOllamaModel") or settings.aiOllamaModel + self._prefs_widgets["ollama_model_entry"].set_text(ollama_model or "") + + ollama_endpoint = self._settings_manager.getSetting("aiOllamaEndpoint") or settings.aiOllamaEndpoint + self._prefs_widgets["ollama_endpoint_entry"].set_text(ollama_endpoint or "") + + confirmation_required = self._settings_manager.getSetting("aiConfirmationRequired") + if confirmation_required is None: + confirmation_required = settings.aiConfirmationRequired + self._prefs_widgets["confirmation_check"].set_active(confirmation_required) + + screenshot_quality = self._settings_manager.getSetting("aiScreenshotQuality") or settings.aiScreenshotQuality + quality_values = self._prefs_widgets.get("quality_values", []) + try: + quality_index = quality_values.index(screenshot_quality) + except ValueError: + quality_index = 0 + self._prefs_widgets["quality_combo"].set_active(quality_index) + + self._update_preferences_sensitivity() + + def _on_ai_enabled_toggled(self, widget): + self._update_preferences_sensitivity() + + def _on_provider_changed(self, widget): + self._update_preferences_sensitivity() + + def _update_preferences_sensitivity(self): + if not self._prefs_widgets: + return + + enabled = self._prefs_widgets["enable_check"].get_active() + provider_values = self._prefs_widgets.get("provider_values", []) + provider_index = self._prefs_widgets["provider_combo"].get_active() + provider = provider_values[provider_index] if 0 <= provider_index < len(provider_values) else settings.aiProvider + + is_gemini = provider == settings.AI_PROVIDER_GEMINI + is_ollama = provider == settings.AI_PROVIDER_OLLAMA + + self._prefs_widgets["provider_combo"].set_sensitive(enabled) + self._prefs_widgets["api_key_entry"].set_sensitive(enabled and is_gemini) + self._prefs_widgets["ollama_model_entry"].set_sensitive(enabled and is_ollama) + self._prefs_widgets["ollama_endpoint_entry"].set_sensitive(enabled and is_ollama) + self._prefs_widgets["confirmation_check"].set_sensitive(enabled) + self._prefs_widgets["quality_combo"].set_sensitive(enabled) def refresh_settings(self): """Refresh plugin settings and reinitialize provider. Called when settings change.""" diff --git a/src/cthulhu/plugins/IndentationAudio/plugin.info b/src/cthulhu/plugins/IndentationAudio/plugin.info index b3e61ec..6f724e2 100644 --- a/src/cthulhu/plugins/IndentationAudio/plugin.info +++ b/src/cthulhu/plugins/IndentationAudio/plugin.info @@ -1,8 +1,8 @@ name = IndentationAudio -version = 1.0.0 +version = 2.0.0 description = Provides audio feedback for indentation level changes when navigating code or text authors = Stormux website = https://git.stormux.org/storm/cthulhu copyright = Copyright 2025 builtin = false -hidden = false \ No newline at end of file +hidden = false diff --git a/src/cthulhu/plugins/IndentationAudio/plugin.py b/src/cthulhu/plugins/IndentationAudio/plugin.py index afb2c3a..1669f7f 100644 --- a/src/cthulhu/plugins/IndentationAudio/plugin.py +++ b/src/cthulhu/plugins/IndentationAudio/plugin.py @@ -12,7 +12,7 @@ import logging import math import re -from gi.repository import GLib +from gi.repository import GLib, Gtk from cthulhu.plugin import Plugin, cthulhu_hookimpl from cthulhu import debug @@ -56,6 +56,8 @@ class IndentationAudio(Plugin): self._saved_speech_indentation = None self._activated = False + self._prefs_grid = None + self._prefs_widgets = {} debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin initialized", True) @cthulhu_hookimpl @@ -86,6 +88,7 @@ class IndentationAudio(Plugin): # Connect to text caret movement events self._connect_to_events() + self._enabled = True self._activated = True debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin activated successfully", True) return True @@ -109,6 +112,7 @@ class IndentationAudio(Plugin): # Clear tracking data self._last_indentation_data.clear() + self._enabled = False self._activated = False debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin deactivated successfully", True) return True @@ -116,6 +120,164 @@ class IndentationAudio(Plugin): except Exception as e: debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: ERROR deactivating plugin: {e}", True) return False + + def getPreferencesGUI(self): + if not self._prefs_grid: + self._prefs_grid = self._build_preferences_gui() + self._load_preferences_into_widgets() + return self._prefs_grid, "Indentation Audio" + + def getPreferencesFromGUI(self): + if not self._prefs_widgets: + return {} + + audio_unit = settings.INDENTATION_UNIT_COLUMNS + if self._prefs_widgets["audio_unit_levels"].get_active(): + audio_unit = settings.INDENTATION_UNIT_LEVELS + + return { + "indentationAudioUnit": audio_unit, + "indentationAudioBaseFrequency": self._prefs_widgets["base_frequency_spin"].get_value_as_int(), + "indentationAudioStepFrequency": self._prefs_widgets["step_frequency_spin"].get_value_as_int(), + "indentationAudioMaxFrequency": self._prefs_widgets["max_frequency_spin"].get_value_as_int(), + "indentationAudioDuration": float(self._prefs_widgets["duration_spin"].get_value()), + "indentationAudioVolume": float(self._prefs_widgets["volume_spin"].get_value()), + } + + def _build_preferences_gui(self): + grid = Gtk.Grid( + row_spacing=6, + column_spacing=12, + margin_left=12, + margin_right=12, + margin_top=12, + margin_bottom=12, + ) + + row = 0 + + description = Gtk.Label(label="Configure audio feedback for indentation changes.") + description.set_line_wrap(True) + description.set_halign(Gtk.Align.START) + grid.attach(description, 0, row, 2, 1) + row += 1 + + audio_unit_label = Gtk.Label(label="Audio _Unit:") + audio_unit_label.set_use_underline(True) + audio_unit_label.set_halign(Gtk.Align.START) + + audio_unit_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + audio_unit_levels = Gtk.RadioButton(label="Levels") + audio_unit_columns = Gtk.RadioButton.new_with_label_from_widget(audio_unit_levels, "Columns") + audio_unit_box.pack_start(audio_unit_levels, False, False, 0) + audio_unit_box.pack_start(audio_unit_columns, False, False, 0) + + audio_unit_label.set_mnemonic_widget(audio_unit_levels) + grid.attach(audio_unit_label, 0, row, 1, 1) + grid.attach(audio_unit_box, 1, row, 1, 1) + row += 1 + + base_adjust = Gtk.Adjustment(200, 50, 2000, 10, 50, 0) + step_adjust = Gtk.Adjustment(80, 1, 500, 5, 20, 0) + max_adjust = Gtk.Adjustment(1200, 200, 5000, 50, 100, 0) + duration_adjust = Gtk.Adjustment(0.15, 0.01, 2, 0.01, 0.1, 0) + volume_adjust = Gtk.Adjustment(0.7, 0, 1, 0.05, 0.1, 0) + + base_frequency_spin, row = self._add_spin_row( + grid, + row, + "Base _Frequency (Hz):", + base_adjust, + 0, + ) + step_frequency_spin, row = self._add_spin_row( + grid, + row, + "_Step per Unit (Hz):", + step_adjust, + 0, + ) + max_frequency_spin, row = self._add_spin_row( + grid, + row, + "_Maximum Frequency (Hz):", + max_adjust, + 0, + ) + duration_spin, row = self._add_spin_row( + grid, + row, + "Tone _Duration (sec):", + duration_adjust, + 2, + ) + volume_spin, row = self._add_spin_row( + grid, + row, + "_Volume Multiplier:", + volume_adjust, + 2, + ) + + self._prefs_widgets = { + "audio_unit_levels": audio_unit_levels, + "audio_unit_columns": audio_unit_columns, + "base_frequency_spin": base_frequency_spin, + "step_frequency_spin": step_frequency_spin, + "max_frequency_spin": max_frequency_spin, + "duration_spin": duration_spin, + "volume_spin": volume_spin, + } + + return grid + + def _add_spin_row(self, grid, row, label_text, adjustment, digits): + label = Gtk.Label(label=label_text) + label.set_use_underline(True) + label.set_halign(Gtk.Align.START) + spin = Gtk.SpinButton(adjustment=adjustment, climb_rate=1, numeric=True) + if digits: + spin.set_digits(digits) + label.set_mnemonic_widget(spin) + + grid.attach(label, 0, row, 1, 1) + grid.attach(spin, 1, row, 1, 1) + return spin, row + 1 + + def _load_preferences_into_widgets(self): + if not self._prefs_widgets: + return + + audio_unit = _settingsManager.getSetting("indentationAudioUnit") or settings.indentationAudioUnit + if audio_unit == settings.INDENTATION_UNIT_LEVELS: + self._prefs_widgets["audio_unit_levels"].set_active(True) + else: + self._prefs_widgets["audio_unit_columns"].set_active(True) + + base_frequency = _settingsManager.getSetting("indentationAudioBaseFrequency") + if base_frequency is None: + base_frequency = settings.indentationAudioBaseFrequency + self._prefs_widgets["base_frequency_spin"].set_value(base_frequency) + + step_frequency = _settingsManager.getSetting("indentationAudioStepFrequency") + if step_frequency is None: + step_frequency = settings.indentationAudioStepFrequency + self._prefs_widgets["step_frequency_spin"].set_value(step_frequency) + + max_frequency = _settingsManager.getSetting("indentationAudioMaxFrequency") + if max_frequency is None: + max_frequency = settings.indentationAudioMaxFrequency + self._prefs_widgets["max_frequency_spin"].set_value(max_frequency) + + duration_value = _settingsManager.getSetting("indentationAudioDuration") + if duration_value is None: + duration_value = settings.indentationAudioDuration + self._prefs_widgets["duration_spin"].set_value(duration_value) + + volume_value = _settingsManager.getSetting("indentationAudioVolume") + if volume_value is None: + volume_value = settings.indentationAudioVolume + self._prefs_widgets["volume_spin"].set_value(volume_value) def _register_keybinding(self): """Register the Cthulhu+I keybinding for toggling the plugin.""" diff --git a/src/cthulhu/plugins/OCR/README.md b/src/cthulhu/plugins/OCR/README.md index 36d75a6..e88b580 100644 --- a/src/cthulhu/plugins/OCR/README.md +++ b/src/cthulhu/plugins/OCR/README.md @@ -84,7 +84,7 @@ To add support for other languages, install additional Tesseract language packs: 1. **Enable the Plugin**: The OCR plugin is enabled by default in Cthulhu. If disabled, you can enable it through: - Cthulhu Preferences → Plugins → Check "OCR" - - Or ensure `'OCR'` is in the `activePlugins` list in settings.py + - Or add `OCR` to your `activePlugins` preference 2. **Basic OCR Workflow**: - Navigate to content you want to OCR @@ -115,7 +115,7 @@ To add support for other languages, install additional Tesseract language packs: Access comprehensive OCR settings through Cthulhu Preferences: 1. **Open Cthulhu Preferences**: `~/.local/bin/cthulhu -s` -2. **Navigate to OCR Tab**: Use keyboard navigation to find the OCR settings tab +2. **Navigate to OCR Tab**: Use keyboard navigation to find the OCR settings tab (only shown when the plugin is active) 3. **Configure Settings**: Adjust all OCR parameters through the accessible interface ### Available Settings @@ -318,4 +318,4 @@ For issues, questions, or contributions: --- -*Part of the Cthulhu Screen Reader project - Making the desktop accessible for everyone.* \ No newline at end of file +*Part of the Cthulhu Screen Reader project - Making the desktop accessible for everyone.* diff --git a/src/cthulhu/plugins/PluginManager/plugin.py b/src/cthulhu/plugins/PluginManager/plugin.py index cb913cd..3433bfc 100644 --- a/src/cthulhu/plugins/PluginManager/plugin.py +++ b/src/cthulhu/plugins/PluginManager/plugin.py @@ -494,6 +494,15 @@ class PluginManager(Plugin): debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Updated active plugins: {active_plugins}", True) + try: + from cthulhu import plugin_system_manager + manager = plugin_system_manager.getManager() + if manager: + manager.setActivePlugins(active_plugins) + debug.printMessage(debug.LEVEL_INFO, "PluginManager: Applied active plugin changes", True) + except Exception as apply_error: + debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Failed to apply plugin changes: {apply_error}", True) + except Exception as error: logger.error(f"PluginManager: Error toggling plugin {plugin_name}: {error}") debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error toggling {plugin_name}: {error}", True) diff --git a/src/cthulhu/plugins/hello_world/README.md b/src/cthulhu/plugins/hello_world/README.md index 2bd1a89..ba11cf6 100644 --- a/src/cthulhu/plugins/hello_world/README.md +++ b/src/cthulhu/plugins/hello_world/README.md @@ -30,4 +30,13 @@ pip install pluggy ## Usage -The plugin will be automatically loaded when Cthulhu starts if it's listed in the activePlugins setting. \ No newline at end of file +Enable the plugin in Cthulhu Preferences → Plugins to load it at startup. You can also add it to your +`activePlugins` preference. + +## Preferences Pages (Optional) + +Plugins can add their own Preferences tab by implementing: +- `getPreferencesGUI()` → returns a Gtk widget (or `(widget, label)`). +- `getPreferencesFromGUI()` → returns a dict of settings to save. + +Tabs are only shown when the plugin is active. diff --git a/src/cthulhu/settings.py b/src/cthulhu/settings.py index e47f30a..d912296 100644 --- a/src/cthulhu/settings.py +++ b/src/cthulhu/settings.py @@ -494,12 +494,12 @@ presentChatRoomLast = False presentLiveRegionFromInactiveTab = False # Plugins -activePlugins = ['AIAssistant', 'DisplayVersion', 'OCR', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu', 'IndentationAudio', 'WindowTitleReader'] +activePlugins = ['DisplayVersion', 'OCR', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu', 'WindowTitleReader'] pluginSources = [] # AI Assistant settings (disabled by default for opt-in behavior) -aiAssistantEnabled = True -aiProvider = AI_PROVIDER_CLAUDE_CODE +aiAssistantEnabled = False +aiProvider = AI_PROVIDER_OLLAMA aiApiKeyFile = "" aiOllamaModel = "llama3.2-vision" aiOllamaEndpoint = "http://localhost:11434" From 9bdb7510c9ca7babaa133f4176190856478c0a8e Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 13 Jan 2026 09:31:55 -0500 Subject: [PATCH 04/13] Fixed a bug in the translation part of the plugin. --- .../plugins/IndentationAudio/plugin.py | 10 ++- src/cthulhu/plugins/nvda2cthulhu/README.md | 2 + src/cthulhu/plugins/nvda2cthulhu/__init__.py | 4 + src/cthulhu/plugins/nvda2cthulhu/plugin.py | 83 ++++++++++++++++--- src/cthulhu/settings.py | 15 +++- 5 files changed, 100 insertions(+), 14 deletions(-) diff --git a/src/cthulhu/plugins/IndentationAudio/plugin.py b/src/cthulhu/plugins/IndentationAudio/plugin.py index 1669f7f..f3c92ab 100644 --- a/src/cthulhu/plugins/IndentationAudio/plugin.py +++ b/src/cthulhu/plugins/IndentationAudio/plugin.py @@ -340,6 +340,10 @@ class IndentationAudio(Plugin): def _on_caret_moved(self, event): """Handle caret movement events.""" try: + # Check if plugin is activated first (prevents race conditions during deactivation) + if not self._activated or not self._enabled: + return + if not self._beeps_enabled(): return @@ -674,11 +678,15 @@ class IndentationAudio(Plugin): def check_indentation_change(self, obj, line_text): """Check if indentation has changed and play audio cue if needed. - + This method is intended to be called by scripts during line navigation. """ debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: check_indentation_change called: line='{line_text}'", True) + # Check if plugin is activated and enabled + if not self._activated or not self._enabled: + return + if not line_text or not self._beeps_enabled(): return diff --git a/src/cthulhu/plugins/nvda2cthulhu/README.md b/src/cthulhu/plugins/nvda2cthulhu/README.md index 79654da..6a5f0ad 100644 --- a/src/cthulhu/plugins/nvda2cthulhu/README.md +++ b/src/cthulhu/plugins/nvda2cthulhu/README.md @@ -20,3 +20,5 @@ will not start the server. if set; otherwise it defaults to 3457. - Toggle interrupt/no-interrupt mode with cthulhu+shift+n. - Toggle translation with cthulhu+control+shift+t (requires translate-shell). +- Translation defaults to your system locale; override with `NVDA2CTHULHU_TRANSLATE_TARGET` + (e.g., `NVDA2CTHULHU_TRANSLATE_TARGET=en`). diff --git a/src/cthulhu/plugins/nvda2cthulhu/__init__.py b/src/cthulhu/plugins/nvda2cthulhu/__init__.py index 778b2a3..294e379 100644 --- a/src/cthulhu/plugins/nvda2cthulhu/__init__.py +++ b/src/cthulhu/plugins/nvda2cthulhu/__init__.py @@ -1 +1,5 @@ """NVDA to Cthulhu bridge plugin.""" + +from .plugin import Nvda2Cthulhu + +__all__ = ['Nvda2Cthulhu'] diff --git a/src/cthulhu/plugins/nvda2cthulhu/plugin.py b/src/cthulhu/plugins/nvda2cthulhu/plugin.py index 8e13dad..7fc34c5 100644 --- a/src/cthulhu/plugins/nvda2cthulhu/plugin.py +++ b/src/cthulhu/plugins/nvda2cthulhu/plugin.py @@ -21,6 +21,7 @@ """NVDA to Cthulhu bridge plugin.""" import asyncio +import locale import logging import os import shutil @@ -46,6 +47,7 @@ except Exception: # pragma: no cover - optional dependency from cthulhu.plugin import Plugin, cthulhu_hookimpl from cthulhu import braille +from cthulhu import debug from cthulhu import speech from cthulhu import settings_manager @@ -90,20 +92,29 @@ class Nvda2Cthulhu(Plugin): """Bridge server that accepts NVDA to Cthulhu messages.""" def __init__(self): - super().__init__() - self.settingsManager = settings_manager.getManager() - self.interruptEnabled = True - self.serverThread = None - self.serverLock = threading.Lock() - self.httpServer = None - self.ioLoop = None - self.asyncioLoop = None - self.translationCache = OrderedDict() - self.translationCacheLock = threading.Lock() + logger.info("NVDA to Cthulhu __init__() called") + try: + super().__init__() + logger.info("NVDA to Cthulhu super().__init__() completed") + self.settingsManager = settings_manager.getManager() + self.interruptEnabled = True + self.serverThread = None + self.serverLock = threading.Lock() + self.httpServer = None + self.ioLoop = None + self.asyncioLoop = None + self.translationCache = OrderedDict() + self.translationCacheLock = threading.Lock() + logger.info("NVDA to Cthulhu __init__() completed successfully") + except Exception as e: + logger.error(f"NVDA to Cthulhu __init__() failed: {e}") + raise @cthulhu_hookimpl def activate(self, plugin=None): + logger.info(f"NVDA to Cthulhu activate() called with plugin={plugin}, self={self}") if plugin is not None and plugin is not self: + logger.info(f"NVDA to Cthulhu activate() early return: plugin is not self") return logger.info("Activating NVDA to Cthulhu plugin") @@ -300,7 +311,10 @@ class Nvda2Cthulhu(Plugin): if not text or not text.strip(): return if self._translation_enabled(): - text = self._translate_text(text) + logger.info(f"NVDA to Cthulhu: translating text: {text[:50]}") + translated = self._translate_text(text) + logger.info(f"NVDA to Cthulhu: translated to: {translated[:50]}") + text = translated speech.speak(text, interrupt=self.interruptEnabled) def _handle_braille(self, text): @@ -345,36 +359,81 @@ class Nvda2Cthulhu(Plugin): def _translation_command_available(self): return shutil.which(TRANSLATE_COMMAND[0]) is not None + def _get_target_language(self): + """Get the target language code from system locale.""" + try: + # Get system locale (e.g., 'en_US.UTF-8') + system_locale = locale.getdefaultlocale()[0] + if system_locale: + # Extract language code (e.g., 'en' from 'en_US') + lang_code = system_locale.split('_')[0] + return lang_code + except Exception as exc: + logger.warning(f"NVDA to Cthulhu: failed to get system locale: {exc}") + + # Fallback to environment variable + try: + lang_env = os.environ.get('LANG', '') + if lang_env: + # Extract language code from LANG (e.g., 'en' from 'en_US.UTF-8') + lang_code = lang_env.split('_')[0] + if lang_code: + return lang_code + except Exception: + pass + + return None + + def _get_translate_target(self): + env_target = os.environ.get("NVDA2CTHULHU_TRANSLATE_TARGET") + if env_target: + return env_target + return self._get_target_language() + def _translate_text(self, text): cached = self._get_cached_translation(text) if cached is not None: + logger.info(f"NVDA to Cthulhu: using cached translation") return cached if not self._translation_command_available(): logger.warning("NVDA to Cthulhu translation failed: translate-shell not available") return text + + translate_target = self._get_translate_target() + command = list(TRANSLATE_COMMAND) + if translate_target: + command.append(f":{translate_target}") + + logger.info(f"NVDA to Cthulhu: running trans command on: {text[:50]}") try: result = subprocess.run( - TRANSLATE_COMMAND, + command, input=text, text=True, + encoding="utf-8", + errors="replace", capture_output=True, check=False, timeout=TRANSLATE_TIMEOUT ) except subprocess.TimeoutExpired: logger.warning("NVDA to Cthulhu translation failed: timed out") + debug.printMessage(debug.LEVEL_INFO, "NVDA to Cthulhu translation failed: timed out", True) return text except Exception as exc: logger.warning(f"NVDA to Cthulhu translation failed: {exc}") + debug.printMessage(debug.LEVEL_INFO, f"NVDA to Cthulhu translation failed: {exc}", True) return text if result.returncode != 0: stderr = result.stderr.strip() if stderr: logger.warning(f"NVDA to Cthulhu translation failed: {stderr}") + debug.printMessage(debug.LEVEL_INFO, f"NVDA to Cthulhu translation failed: {stderr}", True) return text output = result.stdout.strip() + logger.info(f"NVDA to Cthulhu: trans output: {output[:50]}") if not output: return text self._set_cached_translation(text, output) diff --git a/src/cthulhu/settings.py b/src/cthulhu/settings.py index d912296..85f6afd 100644 --- a/src/cthulhu/settings.py +++ b/src/cthulhu/settings.py @@ -494,7 +494,20 @@ presentChatRoomLast = False presentLiveRegionFromInactiveTab = False # Plugins -activePlugins = ['DisplayVersion', 'OCR', 'PluginManager', 'HelloCthulhu', 'ByeCthulhu', 'WindowTitleReader'] +activePlugins = [ + 'PluginManager', + 'ByeCthulhu', + 'Clipboard', + 'HelloCthulhu', + 'DisplayVersion', + 'GameMode', + 'IndentationAudio', + 'nvda2cthulhu', + 'OCR', + 'SpeechHistory', + 'SSIPProxy', + 'WindowTitleReader' +] pluginSources = [] # AI Assistant settings (disabled by default for opt-in behavior) From 475dfb70edcc7bfa0e3b37bc08037829191ed560 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 13 Jan 2026 10:19:37 -0500 Subject: [PATCH 05/13] Attempt to fix bug that causes settings to reinitialize when a change is made. This is a sneaky one, so can't be sure, but hopefully. --- src/cthulhu/acss.py | 19 +++++++++++++------ src/cthulhu/settings_manager.py | 5 +++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/cthulhu/acss.py b/src/cthulhu/acss.py index 1fb62b3..f70ec16 100644 --- a/src/cthulhu/acss.py +++ b/src/cthulhu/acss.py @@ -88,12 +88,19 @@ class ACSS(dict): def __eq__(self, other): if not isinstance(other, ACSS): return False - if self.get(ACSS.FAMILY) != other.get(ACSS.FAMILY): - return False - if self.get(ACSS.RATE) != other.get(ACSS.RATE): - return False - if self.get(ACSS.AVERAGE_PITCH) != other.get(ACSS.AVERAGE_PITCH): - return False + compareKeys = ( + ACSS.FAMILY, + ACSS.RATE, + ACSS.AVERAGE_PITCH, + ACSS.GAIN, + ACSS.PITCH_RANGE, + ACSS.STRESS, + ACSS.RICHNESS, + ACSS.PUNCTUATIONS, + ) + for key in compareKeys: + if self.get(key) != other.get(key): + return False return True def __setitem__ (self, key, value): diff --git a/src/cthulhu/settings_manager.py b/src/cthulhu/settings_manager.py index eefb2d5..7c368ab 100644 --- a/src/cthulhu/settings_manager.py +++ b/src/cthulhu/settings_manager.py @@ -32,6 +32,7 @@ __date__ = "$Date$" __copyright__ = "Copyright (c) 2010 Consorcio Fernando de los Rios." __license__ = "LGPL" +import copy import importlib import os from gi.repository import Gio, GLib @@ -232,6 +233,10 @@ class SettingsManager(object): value = getattr(settings, key) except Exception: pass + try: + value = copy.deepcopy(value) + except Exception: + pass self.defaultGeneral[key] = value def _getCustomizedSettings(self): From 495bcca185d0c91c4dbbd26008fffb7506247cab Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Wed, 14 Jan 2026 08:52:58 -0500 Subject: [PATCH 06/13] Indentation moved from plugin to core code, setting in speech tab. --- src/cthulhu/backends/json_backend.py | 28 + src/cthulhu/cthulhu-setup.ui | 773 +++-------------- src/cthulhu/cthulhu_gui_prefs.py | 211 +---- .../plugins/IndentationAudio/__init__.py | 14 - .../plugins/IndentationAudio/meson.build | 14 - .../plugins/IndentationAudio/plugin.info | 8 - .../plugins/IndentationAudio/plugin.py | 790 ------------------ src/cthulhu/plugins/meson.build | 1 - src/cthulhu/script_utilities.py | 199 ++--- src/cthulhu/settings.py | 41 +- 10 files changed, 271 insertions(+), 1808 deletions(-) delete mode 100644 src/cthulhu/plugins/IndentationAudio/__init__.py delete mode 100644 src/cthulhu/plugins/IndentationAudio/meson.build delete mode 100644 src/cthulhu/plugins/IndentationAudio/plugin.info delete mode 100644 src/cthulhu/plugins/IndentationAudio/plugin.py diff --git a/src/cthulhu/backends/json_backend.py b/src/cthulhu/backends/json_backend.py index c887d11..f1e9d1a 100644 --- a/src/cthulhu/backends/json_backend.py +++ b/src/cthulhu/backends/json_backend.py @@ -123,16 +123,44 @@ class Backend: self.keybindings = prefs['keybindings'] self.profiles = prefs['profiles'].copy() + def _migrateSettings(self, settingsDict): + """Migrate old setting names to new ones.""" + # Migration: enableSpeechIndentation -> enableIndentation + if 'enableSpeechIndentation' in settingsDict and 'enableIndentation' not in settingsDict: + settingsDict['enableIndentation'] = settingsDict['enableSpeechIndentation'] + + # Remove obsolete indentation settings that are now hardcoded + obsoleteKeys = [ + 'enableSpeechIndentation', + 'indentationChangeMode', + 'speakIndentationOnlyIfChanged', + 'indentationSpeechStyle', + 'indentationAudioUnit', + 'indentationSpacesPerLevel', + 'indentationTabWidth', + 'indentationAudioBaseFrequency', + 'indentationAudioStepFrequency', + 'indentationAudioMaxFrequency', + 'indentationAudioDuration', + 'indentationAudioVolume', + ] + for key in obsoleteKeys: + settingsDict.pop(key, None) + + return settingsDict + def getGeneral(self, profile=None): """ Get general settings from default settings and override with profile values. """ self._getSettings() generalSettings = self.general.copy() + generalSettings = self._migrateSettings(generalSettings) defaultProfile = generalSettings.get('startingProfile', ['Default', 'default']) if profile is None: profile = defaultProfile[1] profileSettings = self.profiles[profile].copy() + profileSettings = self._migrateSettings(profileSettings) for key, value in profileSettings.items(): if key == 'voices': for voiceType, voiceDef in value.items(): diff --git a/src/cthulhu/cthulhu-setup.ui b/src/cthulhu/cthulhu-setup.ui index 5585122..b6aa5b8 100644 --- a/src/cthulhu/cthulhu-setup.ui +++ b/src/cthulhu/cthulhu-setup.ui @@ -21,55 +21,6 @@ 10 50 - - 1 - 16 - 4 - 1 - 1 - - - 1 - 16 - 4 - 1 - 1 - - - 50 - 2000 - 200 - 10 - 50 - - - 1 - 500 - 80 - 5 - 20 - - - 200 - 5000 - 1200 - 50 - 100 - - - 0.01 - 2 - 0.15 - 0.01 - 0.1 - - - 0 - 1 - 0.7 - 0.05 - 0.1 - @@ -2175,7 +2126,114 @@ - + + True + False + 0 + none + + + True + False + 12 + + + True + False + 6 + + + Report indentation chan_ges + True + True + False + True + True + + + + 0 + 0 + + + + + True + False + 4 + 24 + + + _Speech + True + True + False + True + True + + + + 0 + 0 + + + + + _Beeps + True + True + False + True + True + indentationSpeechButton + + + + 0 + 1 + + + + + Speech _and beeps + True + True + False + True + True + indentationSpeechButton + + + + 0 + 2 + + + + + 0 + 1 + + + + + + + + + True + False + Indentation + + + + + + + + 0 + 1 + @@ -3587,621 +3645,6 @@ False - - - True - False - 12 - 10 - 10 - - - True - False - 0 - none - - - True - False - 12 - - - True - False - 6 - - - _Off - True - True - False - True - True - - - - 0 - 0 - - - - - _Speech - True - True - False - True - True - indentationPresentationOffButton - - - - 0 - 1 - - - - - _Beeps - True - True - False - True - True - indentationPresentationOffButton - - - - 0 - 2 - - - - - Speech _and beeps - True - True - False - True - True - indentationPresentationOffButton - - - - 0 - 3 - - - - - - - - - True - False - Presentation Mode - - - - - - - - 0 - 0 - - - - - True - False - 0 - none - - - True - False - 12 - - - True - False - 6 - - - Always present indentation - True - True - False - True - True - - - - 0 - 0 - - - - - Present any indentation changes - True - True - False - True - True - indentationChangeAlwaysButton - - - - 0 - 1 - - - - - Present only when indentation level changes - True - True - False - True - True - indentationChangeAlwaysButton - - - - 0 - 2 - - - - - - - - - True - False - When to Present - - - - - - - - 1 - 0 - - - - - True - False - 0 - none - - - True - False - 12 - - - True - False - 6 - - - Spaces and tabs - True - True - False - True - True - - - - 0 - 0 - - - - - Indentation level - True - True - False - True - True - indentationSpeechSpacesTabsButton - - - - 0 - 1 - - - - - Indentation columns - True - True - False - True - True - indentationSpeechSpacesTabsButton - - - - 0 - 2 - - - - - - - - - True - False - Speech Format - - - - - - - - 0 - 1 - - - - - True - False - 0 - none - - - True - False - 12 - - - True - False - 6 - 10 - - - True - False - Spaces per level: - True - indentationSpacesPerLevelSpinButton - 0 - - - 0 - 0 - - - - - True - True - indentationSpacesPerLevelAdjustment - 1 - True - - - - 1 - 0 - - - - - True - False - Tab width: - True - indentationTabWidthSpinButton - 0 - - - 0 - 1 - - - - - True - True - indentationTabWidthAdjustment - 1 - True - - - - 1 - 1 - - - - - True - False - Audio unit: - 0 - - - 0 - 2 - - - - - True - False - 4 - - - Levels - True - True - False - True - True - - - - 0 - 0 - - - - - Columns - True - True - False - True - True - indentationAudioUnitLevelsButton - - - - 0 - 1 - - - - - 1 - 2 - - - - - - - - - True - False - Units - - - - - - - - 1 - 1 - - - - - True - False - 0 - none - - - True - False - 12 - - - True - False - 6 - 10 - - - True - False - Base frequency (Hz): - True - indentationAudioBaseFrequencySpinButton - 0 - - - 0 - 0 - - - - - True - True - indentationAudioBaseFrequencyAdjustment - 1 - True - - - - 1 - 0 - - - - - True - False - Step per unit (Hz): - True - indentationAudioStepFrequencySpinButton - 0 - - - 0 - 1 - - - - - True - True - indentationAudioStepFrequencyAdjustment - 1 - True - - - - 1 - 1 - - - - - True - False - Maximum frequency (Hz): - True - indentationAudioMaxFrequencySpinButton - 0 - - - 0 - 2 - - - - - True - True - indentationAudioMaxFrequencyAdjustment - 1 - True - - - - 1 - 2 - - - - - True - False - Tone duration (sec): - True - indentationAudioDurationSpinButton - 0 - - - 0 - 3 - - - - - True - True - indentationAudioDurationAdjustment - 0.01 - 2 - True - - - - 1 - 3 - - - - - True - False - Volume multiplier: - True - indentationAudioVolumeSpinButton - 0 - - - 0 - 4 - - - - - True - True - indentationAudioVolumeAdjustment - 0.05 - 2 - True - - - - 1 - 4 - - - - - - - - - True - False - Audio - - - - - - - - 0 - 2 - 2 - - - - - 8 - - - - - True - False - Indentation - - - 8 - False - - True diff --git a/src/cthulhu/cthulhu_gui_prefs.py b/src/cthulhu/cthulhu_gui_prefs.py index a9925df..4e2714f 100644 --- a/src/cthulhu/cthulhu_gui_prefs.py +++ b/src/cthulhu/cthulhu_gui_prefs.py @@ -2569,117 +2569,31 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): self._updateProviderControls(current_provider) def _initIndentationState(self): - """Initialize Indentation tab widgets with current settings.""" + """Initialize Indentation widgets with current settings.""" prefs = self.prefsDict - self.indentationPresentationOffButton = self.get_widget("indentationPresentationOffButton") - self.indentationPresentationSpeechButton = self.get_widget("indentationPresentationSpeechButton") - self.indentationPresentationBeepsButton = self.get_widget("indentationPresentationBeepsButton") - self.indentationPresentationSpeechAndBeepsButton = self.get_widget("indentationPresentationSpeechAndBeepsButton") + self.enableIndentationCheckButton = self.get_widget("enableIndentationCheckButton") + self.indentationSpeechButton = self.get_widget("indentationSpeechButton") + self.indentationBeepsButton = self.get_widget("indentationBeepsButton") + self.indentationSpeechAndBeepsButton = self.get_widget("indentationSpeechAndBeepsButton") + self.indentationModeGrid = self.get_widget("indentationModeGrid") - self.indentationChangeAlwaysButton = self.get_widget("indentationChangeAlwaysButton") - self.indentationChangeAnyButton = self.get_widget("indentationChangeAnyButton") - self.indentationChangeLevelButton = self.get_widget("indentationChangeLevelButton") - - self.indentationSpeechSpacesTabsButton = self.get_widget("indentationSpeechSpacesTabsButton") - self.indentationSpeechLevelsButton = self.get_widget("indentationSpeechLevelsButton") - self.indentationSpeechColumnsButton = self.get_widget("indentationSpeechColumnsButton") - - self.indentationAudioUnitLevelsButton = self.get_widget("indentationAudioUnitLevelsButton") - self.indentationAudioUnitColumnsButton = self.get_widget("indentationAudioUnitColumnsButton") - - self.indentationSpacesPerLevelSpinButton = self.get_widget("indentationSpacesPerLevelSpinButton") - self.indentationTabWidthSpinButton = self.get_widget("indentationTabWidthSpinButton") - self.indentationAudioBaseFrequencySpinButton = self.get_widget("indentationAudioBaseFrequencySpinButton") - self.indentationAudioStepFrequencySpinButton = self.get_widget("indentationAudioStepFrequencySpinButton") - self.indentationAudioMaxFrequencySpinButton = self.get_widget("indentationAudioMaxFrequencySpinButton") - self.indentationAudioDurationSpinButton = self.get_widget("indentationAudioDurationSpinButton") - self.indentationAudioVolumeSpinButton = self.get_widget("indentationAudioVolumeSpinButton") + enabled = prefs.get("enableIndentation", settings.enableIndentation) + self.enableIndentationCheckButton.set_active(enabled) mode = prefs.get("indentationPresentationMode", settings.indentationPresentationMode) - if mode == settings.INDENTATION_PRESENTATION_SPEECH: - self.indentationPresentationSpeechButton.set_active(True) - elif mode == settings.INDENTATION_PRESENTATION_BEEPS: - self.indentationPresentationBeepsButton.set_active(True) + if mode == settings.INDENTATION_PRESENTATION_BEEPS: + self.indentationBeepsButton.set_active(True) elif mode == settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS: - self.indentationPresentationSpeechAndBeepsButton.set_active(True) + self.indentationSpeechAndBeepsButton.set_active(True) else: - self.indentationPresentationOffButton.set_active(True) + self.indentationSpeechButton.set_active(True) - enableSpeechIndentation = mode in ( - settings.INDENTATION_PRESENTATION_SPEECH, - settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS, - ) - self.prefsDict["enableSpeechIndentation"] = enableSpeechIndentation + self._updateIndentationControlsState(enabled) - only_if_changed = prefs.get("speakIndentationOnlyIfChanged", settings.speakIndentationOnlyIfChanged) - change_mode = prefs.get("indentationChangeMode", settings.indentationChangeMode) - if not only_if_changed or change_mode == settings.INDENTATION_CHANGE_ALWAYS: - self.indentationChangeAlwaysButton.set_active(True) - elif change_mode == settings.INDENTATION_CHANGE_ANY: - self.indentationChangeAnyButton.set_active(True) - else: - self.indentationChangeLevelButton.set_active(True) - - speech_style = prefs.get("indentationSpeechStyle", settings.indentationSpeechStyle) - if speech_style == settings.INDENTATION_SPEECH_STYLE_LEVELS: - self.indentationSpeechLevelsButton.set_active(True) - elif speech_style == settings.INDENTATION_SPEECH_STYLE_COLUMNS: - self.indentationSpeechColumnsButton.set_active(True) - else: - self.indentationSpeechSpacesTabsButton.set_active(True) - - audio_unit = prefs.get("indentationAudioUnit", settings.indentationAudioUnit) - if audio_unit == settings.INDENTATION_UNIT_COLUMNS: - self.indentationAudioUnitColumnsButton.set_active(True) - else: - self.indentationAudioUnitLevelsButton.set_active(True) - - self.indentationSpacesPerLevelSpinButton.set_value( - prefs.get("indentationSpacesPerLevel", settings.indentationSpacesPerLevel)) - self.indentationTabWidthSpinButton.set_value( - prefs.get("indentationTabWidth", settings.indentationTabWidth)) - self.indentationAudioBaseFrequencySpinButton.set_value( - prefs.get("indentationAudioBaseFrequency", settings.indentationAudioBaseFrequency)) - self.indentationAudioStepFrequencySpinButton.set_value( - prefs.get("indentationAudioStepFrequency", settings.indentationAudioStepFrequency)) - self.indentationAudioMaxFrequencySpinButton.set_value( - prefs.get("indentationAudioMaxFrequency", settings.indentationAudioMaxFrequency)) - self.indentationAudioDurationSpinButton.set_value( - prefs.get("indentationAudioDuration", settings.indentationAudioDuration)) - self.indentationAudioVolumeSpinButton.set_value( - prefs.get("indentationAudioVolume", settings.indentationAudioVolume)) - - self._updateIndentationControlsState(mode) - - def _updateIndentationControlsState(self, mode): - """Enable or disable indentation controls based on presentation mode.""" - speech_enabled = mode in ( - settings.INDENTATION_PRESENTATION_SPEECH, - settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS, - ) - beeps_enabled = mode in ( - settings.INDENTATION_PRESENTATION_BEEPS, - settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS, - ) - - for widget in ( - self.indentationSpeechSpacesTabsButton, - self.indentationSpeechLevelsButton, - self.indentationSpeechColumnsButton, - ): - widget.set_sensitive(speech_enabled) - - for widget in ( - self.indentationAudioUnitLevelsButton, - self.indentationAudioUnitColumnsButton, - self.indentationAudioBaseFrequencySpinButton, - self.indentationAudioStepFrequencySpinButton, - self.indentationAudioMaxFrequencySpinButton, - self.indentationAudioDurationSpinButton, - self.indentationAudioVolumeSpinButton, - ): - widget.set_sensitive(beeps_enabled) + def _updateIndentationControlsState(self, enabled): + """Enable or disable indentation mode controls based on checkbox state.""" + self.indentationModeGrid.set_sensitive(enabled) def _updateProviderControls(self, provider): """Update visibility/sensitivity of provider-specific controls.""" @@ -4594,89 +4508,26 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): # Indentation signal handlers - def indentationPresentationModeToggled(self, widget): - """Indentation presentation mode radio toggled handler.""" + def enableIndentationToggled(self, widget): + """Enable indentation checkbox toggled handler.""" + enabled = widget.get_active() + self.prefsDict["enableIndentation"] = enabled + self._updateIndentationControlsState(enabled) + + def indentationModeToggled(self, widget): + """Indentation mode radio toggled handler.""" if not widget.get_active(): return mapping = { - "indentationPresentationOffButton": settings.INDENTATION_PRESENTATION_OFF, - "indentationPresentationSpeechButton": settings.INDENTATION_PRESENTATION_SPEECH, - "indentationPresentationBeepsButton": settings.INDENTATION_PRESENTATION_BEEPS, - "indentationPresentationSpeechAndBeepsButton": settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS, + "indentationSpeechButton": settings.INDENTATION_PRESENTATION_SPEECH, + "indentationBeepsButton": settings.INDENTATION_PRESENTATION_BEEPS, + "indentationSpeechAndBeepsButton": settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS, } - widget_name = Gtk.Buildable.get_name(widget) - mode = mapping.get(widget_name) - if mode is None: - return - - self.prefsDict["indentationPresentationMode"] = mode - self.prefsDict["enableSpeechIndentation"] = mode in ( - settings.INDENTATION_PRESENTATION_SPEECH, - settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS, - ) - self._updateIndentationControlsState(mode) - - def indentationChangeModeToggled(self, widget): - """Indentation change mode radio toggled handler.""" - if not widget.get_active(): - return - - widget_name = Gtk.Buildable.get_name(widget) - if widget_name == "indentationChangeAlwaysButton": - self.prefsDict["speakIndentationOnlyIfChanged"] = False - self.prefsDict["indentationChangeMode"] = settings.INDENTATION_CHANGE_ALWAYS - elif widget_name == "indentationChangeAnyButton": - self.prefsDict["speakIndentationOnlyIfChanged"] = True - self.prefsDict["indentationChangeMode"] = settings.INDENTATION_CHANGE_ANY - else: - self.prefsDict["speakIndentationOnlyIfChanged"] = True - self.prefsDict["indentationChangeMode"] = settings.INDENTATION_CHANGE_LEVEL - - def indentationSpeechStyleToggled(self, widget): - """Indentation speech style radio toggled handler.""" - if not widget.get_active(): - return - - mapping = { - "indentationSpeechSpacesTabsButton": settings.INDENTATION_SPEECH_STYLE_SPACES_TABS, - "indentationSpeechLevelsButton": settings.INDENTATION_SPEECH_STYLE_LEVELS, - "indentationSpeechColumnsButton": settings.INDENTATION_SPEECH_STYLE_COLUMNS, - } - widget_name = Gtk.Buildable.get_name(widget) - style = mapping.get(widget_name) - if style is None: - return - - self.prefsDict["indentationSpeechStyle"] = style - - def indentationAudioUnitToggled(self, widget): - """Indentation audio unit radio toggled handler.""" - if not widget.get_active(): - return - - if Gtk.Buildable.get_name(widget) == "indentationAudioUnitColumnsButton": - self.prefsDict["indentationAudioUnit"] = settings.INDENTATION_UNIT_COLUMNS - else: - self.prefsDict["indentationAudioUnit"] = settings.INDENTATION_UNIT_LEVELS - - def indentationSpinValueChanged(self, widget): - """Indentation spin button value changed handler.""" - settingName = Gtk.Buildable.get_name(widget) - if settingName.endswith("SpinButton"): - settingName = settingName[:-10] - - int_settings = { - "indentationSpacesPerLevel", - "indentationTabWidth", - "indentationAudioBaseFrequency", - "indentationAudioStepFrequency", - "indentationAudioMaxFrequency", - } - if settingName in int_settings: - self.prefsDict[settingName] = int(widget.get_value()) - else: - self.prefsDict[settingName] = float(widget.get_value()) + widgetName = Gtk.Buildable.get_name(widget) + mode = mapping.get(widgetName) + if mode is not None: + self.prefsDict["indentationPresentationMode"] = mode # AI Assistant signal handlers diff --git a/src/cthulhu/plugins/IndentationAudio/__init__.py b/src/cthulhu/plugins/IndentationAudio/__init__.py deleted file mode 100644 index 7e5fc5a..0000000 --- a/src/cthulhu/plugins/IndentationAudio/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2025 Stormux -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. - -"""IndentationAudio plugin package.""" - -from .plugin import IndentationAudio - -__all__ = ['IndentationAudio'] \ No newline at end of file diff --git a/src/cthulhu/plugins/IndentationAudio/meson.build b/src/cthulhu/plugins/IndentationAudio/meson.build deleted file mode 100644 index 836ea47..0000000 --- a/src/cthulhu/plugins/IndentationAudio/meson.build +++ /dev/null @@ -1,14 +0,0 @@ -indentationaudio_python_sources = files([ - '__init__.py', - 'plugin.py' -]) - -python3.install_sources( - indentationaudio_python_sources, - subdir: 'cthulhu/plugins/IndentationAudio' -) - -install_data( - 'plugin.info', - install_dir: python3.get_install_dir() / 'cthulhu' / 'plugins' / 'IndentationAudio' -) \ No newline at end of file diff --git a/src/cthulhu/plugins/IndentationAudio/plugin.info b/src/cthulhu/plugins/IndentationAudio/plugin.info deleted file mode 100644 index 6f724e2..0000000 --- a/src/cthulhu/plugins/IndentationAudio/plugin.info +++ /dev/null @@ -1,8 +0,0 @@ -name = IndentationAudio -version = 2.0.0 -description = Provides audio feedback for indentation level changes when navigating code or text -authors = Stormux -website = https://git.stormux.org/storm/cthulhu -copyright = Copyright 2025 -builtin = false -hidden = false diff --git a/src/cthulhu/plugins/IndentationAudio/plugin.py b/src/cthulhu/plugins/IndentationAudio/plugin.py deleted file mode 100644 index f3c92ab..0000000 --- a/src/cthulhu/plugins/IndentationAudio/plugin.py +++ /dev/null @@ -1,790 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2025 Stormux -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. - -"""IndentationAudio plugin for Cthulhu - Provides audio feedback for indentation level changes.""" - -import logging -import math -import re -from gi.repository import GLib, Gtk - -from cthulhu.plugin import Plugin, cthulhu_hookimpl -from cthulhu import debug -from cthulhu import settings -from cthulhu import settings_manager -from cthulhu.ax_object import AXObject -from cthulhu.ax_utilities import AXUtilities -from cthulhu.ax_text import AXText - -# Import Cthulhu's sound system -try: - from cthulhu import sound - from cthulhu.sound_generator import Tone - SOUND_AVAILABLE = True -except ImportError as e: - SOUND_AVAILABLE = False - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Sound import failed: {e}", True) - -logger = logging.getLogger(__name__) -_settingsManager = settings_manager.getManager() - - -class IndentationAudio(Plugin): - """Plugin that provides audio cues for indentation level changes.""" - - def __init__(self, *args, **kwargs): - """Initialize the IndentationAudio plugin.""" - super().__init__(*args, **kwargs) - self._enabled = True # Start enabled by default - self._last_indentation_data = {} # Track per-object indentation - self._event_listener_id = None - self._kb_binding = None - # Audio settings - self._base_frequency = settings.indentationAudioBaseFrequency - self._frequency_step = settings.indentationAudioStepFrequency - self._tone_duration = settings.indentationAudioDuration - self._max_frequency = settings.indentationAudioMaxFrequency - self._volume_multiplier = settings.indentationAudioVolume - self._alerts_suspended = False - self._saved_presentation_mode = None - self._saved_speech_indentation = None - - self._activated = False - self._prefs_grid = None - self._prefs_widgets = {} - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin initialized", True) - - @cthulhu_hookimpl - def activate(self, plugin=None): - """Activate the IndentationAudio plugin.""" - if plugin is not None and plugin is not self: - return - - # Prevent duplicate activation - if self._activated: - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Already activated, skipping", True) - return - - try: - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin activation starting", True) - - # Initialize sound player - if SOUND_AVAILABLE: - self._player = sound.getPlayer() - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Sound player initialized", True) - else: - self._player = None - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Sound not available", True) - - # Register keybinding for toggle (Cthulhu+I) - self._register_keybinding() - - # Connect to text caret movement events - self._connect_to_events() - - self._enabled = True - self._activated = True - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin activated successfully", True) - return True - - except Exception as e: - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: ERROR activating plugin: {e}", True) - return False - - @cthulhu_hookimpl - def deactivate(self, plugin=None): - """Deactivate the IndentationAudio plugin.""" - if plugin is not None and plugin is not self: - return - - try: - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin deactivation starting", True) - - # Disconnect from events - self._disconnect_from_events() - - # Clear tracking data - self._last_indentation_data.clear() - - self._enabled = False - self._activated = False - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Plugin deactivated successfully", True) - return True - - except Exception as e: - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: ERROR deactivating plugin: {e}", True) - return False - - def getPreferencesGUI(self): - if not self._prefs_grid: - self._prefs_grid = self._build_preferences_gui() - self._load_preferences_into_widgets() - return self._prefs_grid, "Indentation Audio" - - def getPreferencesFromGUI(self): - if not self._prefs_widgets: - return {} - - audio_unit = settings.INDENTATION_UNIT_COLUMNS - if self._prefs_widgets["audio_unit_levels"].get_active(): - audio_unit = settings.INDENTATION_UNIT_LEVELS - - return { - "indentationAudioUnit": audio_unit, - "indentationAudioBaseFrequency": self._prefs_widgets["base_frequency_spin"].get_value_as_int(), - "indentationAudioStepFrequency": self._prefs_widgets["step_frequency_spin"].get_value_as_int(), - "indentationAudioMaxFrequency": self._prefs_widgets["max_frequency_spin"].get_value_as_int(), - "indentationAudioDuration": float(self._prefs_widgets["duration_spin"].get_value()), - "indentationAudioVolume": float(self._prefs_widgets["volume_spin"].get_value()), - } - - def _build_preferences_gui(self): - grid = Gtk.Grid( - row_spacing=6, - column_spacing=12, - margin_left=12, - margin_right=12, - margin_top=12, - margin_bottom=12, - ) - - row = 0 - - description = Gtk.Label(label="Configure audio feedback for indentation changes.") - description.set_line_wrap(True) - description.set_halign(Gtk.Align.START) - grid.attach(description, 0, row, 2, 1) - row += 1 - - audio_unit_label = Gtk.Label(label="Audio _Unit:") - audio_unit_label.set_use_underline(True) - audio_unit_label.set_halign(Gtk.Align.START) - - audio_unit_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) - audio_unit_levels = Gtk.RadioButton(label="Levels") - audio_unit_columns = Gtk.RadioButton.new_with_label_from_widget(audio_unit_levels, "Columns") - audio_unit_box.pack_start(audio_unit_levels, False, False, 0) - audio_unit_box.pack_start(audio_unit_columns, False, False, 0) - - audio_unit_label.set_mnemonic_widget(audio_unit_levels) - grid.attach(audio_unit_label, 0, row, 1, 1) - grid.attach(audio_unit_box, 1, row, 1, 1) - row += 1 - - base_adjust = Gtk.Adjustment(200, 50, 2000, 10, 50, 0) - step_adjust = Gtk.Adjustment(80, 1, 500, 5, 20, 0) - max_adjust = Gtk.Adjustment(1200, 200, 5000, 50, 100, 0) - duration_adjust = Gtk.Adjustment(0.15, 0.01, 2, 0.01, 0.1, 0) - volume_adjust = Gtk.Adjustment(0.7, 0, 1, 0.05, 0.1, 0) - - base_frequency_spin, row = self._add_spin_row( - grid, - row, - "Base _Frequency (Hz):", - base_adjust, - 0, - ) - step_frequency_spin, row = self._add_spin_row( - grid, - row, - "_Step per Unit (Hz):", - step_adjust, - 0, - ) - max_frequency_spin, row = self._add_spin_row( - grid, - row, - "_Maximum Frequency (Hz):", - max_adjust, - 0, - ) - duration_spin, row = self._add_spin_row( - grid, - row, - "Tone _Duration (sec):", - duration_adjust, - 2, - ) - volume_spin, row = self._add_spin_row( - grid, - row, - "_Volume Multiplier:", - volume_adjust, - 2, - ) - - self._prefs_widgets = { - "audio_unit_levels": audio_unit_levels, - "audio_unit_columns": audio_unit_columns, - "base_frequency_spin": base_frequency_spin, - "step_frequency_spin": step_frequency_spin, - "max_frequency_spin": max_frequency_spin, - "duration_spin": duration_spin, - "volume_spin": volume_spin, - } - - return grid - - def _add_spin_row(self, grid, row, label_text, adjustment, digits): - label = Gtk.Label(label=label_text) - label.set_use_underline(True) - label.set_halign(Gtk.Align.START) - spin = Gtk.SpinButton(adjustment=adjustment, climb_rate=1, numeric=True) - if digits: - spin.set_digits(digits) - label.set_mnemonic_widget(spin) - - grid.attach(label, 0, row, 1, 1) - grid.attach(spin, 1, row, 1, 1) - return spin, row + 1 - - def _load_preferences_into_widgets(self): - if not self._prefs_widgets: - return - - audio_unit = _settingsManager.getSetting("indentationAudioUnit") or settings.indentationAudioUnit - if audio_unit == settings.INDENTATION_UNIT_LEVELS: - self._prefs_widgets["audio_unit_levels"].set_active(True) - else: - self._prefs_widgets["audio_unit_columns"].set_active(True) - - base_frequency = _settingsManager.getSetting("indentationAudioBaseFrequency") - if base_frequency is None: - base_frequency = settings.indentationAudioBaseFrequency - self._prefs_widgets["base_frequency_spin"].set_value(base_frequency) - - step_frequency = _settingsManager.getSetting("indentationAudioStepFrequency") - if step_frequency is None: - step_frequency = settings.indentationAudioStepFrequency - self._prefs_widgets["step_frequency_spin"].set_value(step_frequency) - - max_frequency = _settingsManager.getSetting("indentationAudioMaxFrequency") - if max_frequency is None: - max_frequency = settings.indentationAudioMaxFrequency - self._prefs_widgets["max_frequency_spin"].set_value(max_frequency) - - duration_value = _settingsManager.getSetting("indentationAudioDuration") - if duration_value is None: - duration_value = settings.indentationAudioDuration - self._prefs_widgets["duration_spin"].set_value(duration_value) - - volume_value = _settingsManager.getSetting("indentationAudioVolume") - if volume_value is None: - volume_value = settings.indentationAudioVolume - self._prefs_widgets["volume_spin"].set_value(volume_value) - - def _register_keybinding(self): - """Register the Cthulhu+I keybinding for toggling the plugin.""" - try: - if not self.app: - logger.error("IndentationAudio: No app reference available for keybinding") - return - - # Register Cthulhu+I keybinding using the plugin's registerGestureByString method - gesture_string = "kb:cthulhu+i" - description = "Toggle indentation alerts" - - self._kb_binding = self.registerGestureByString( - self._toggle_indentation_audio, - description, - gesture_string - ) - - if self._kb_binding: - logger.info(f"IndentationAudio: Registered keybinding {gesture_string}") - logger.info(f"Binding keysymstring: {self._kb_binding.keysymstring}") - logger.info(f"Binding modifiers: {self._kb_binding.modifiers}") - else: - logger.error(f"IndentationAudio: Failed to register keybinding {gesture_string}") - - except Exception as e: - logger.error(f"IndentationAudio: Error registering keybinding: {e}") - - def _connect_to_events(self): - """Connect to text navigation events.""" - try: - # Hook into the dynamic API to make ourselves available to scripts - if self.app: - api_manager = self.app.getDynamicApiManager() - api_manager.registerAPI('IndentationAudioPlugin', self) - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Registered with dynamic API manager", True) - - # Try to connect to ATSPI events directly - self._connect_to_atspi_events() - - except Exception as e: - logger.error(f"IndentationAudio: Error connecting to events: {e}") - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Error connecting to events: {e}", True) - - def _connect_to_atspi_events(self): - """Connect to ATSPI caret movement events.""" - try: - # Import ATSPI event system - from gi.repository import Atspi - - # Register for text caret movement events - self._event_listener = Atspi.EventListener.new(self._on_caret_moved) - Atspi.EventListener.register(self._event_listener, "object:text-caret-moved") - - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Registered for text-caret-moved events", True) - - except Exception as e: - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: ATSPI connection error: {e}", True) - - def _on_caret_moved(self, event): - """Handle caret movement events.""" - try: - # Check if plugin is activated first (prevents race conditions during deactivation) - if not self._activated or not self._enabled: - return - - if not self._beeps_enabled(): - return - - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Caret moved in {event.source}", True) - - # Use Cthulhu's script system to get line text - try: - from cthulhu import cthulhu_state - - # Get current active script - if cthulhu_state.activeScript: - script = cthulhu_state.activeScript - obj = event.source - - # Use script's getTextLineAtCaret method - if hasattr(script, 'getTextLineAtCaret'): - line, caret_offset, start_offset = script.getTextLineAtCaret(obj) - if line is not None: - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Line navigation - line: '{line}'", True) - self.check_indentation_change(obj, line) - else: - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: getTextLineAtCaret returned None", True) - else: - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: script has no getTextLineAtCaret method", True) - else: - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: No active script available", True) - - except Exception as script_e: - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Error using script method: {script_e}", True) - - # Fallback to direct AT-SPI access - try: - obj = event.source - if AXObject.supports_text(obj): - # Get all text and find current line - full_text = AXText.get_all_text(obj) - caret_pos = AXText.get_caret_offset(obj) - if full_text: - lines = full_text.split('\n') - char_count = 0 - for line in lines: - if char_count <= caret_pos <= char_count + len(line): - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Fallback line: '{line}'", True) - self.check_indentation_change(obj, line) - break - char_count += len(line) + 1 # +1 for newline - - except Exception as fallback_e: - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Fallback also failed: {fallback_e}", True) - - except Exception as e: - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Error in caret moved handler: {e}", True) - - def _disconnect_from_events(self): - """Disconnect from text navigation events.""" - try: - # Unregister ATSPI event listener - if hasattr(self, '_event_listener') and self._event_listener: - from gi.repository import Atspi - Atspi.EventListener.deregister(self._event_listener, "object:text-caret-moved") - self._event_listener = None - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Unregistered ATSPI events", True) - - # Unregister from dynamic API - if self.app: - api_manager = self.app.getDynamicApiManager() - api_manager.unregisterAPI('IndentationAudioPlugin') - - except Exception as e: - logger.error(f"IndentationAudio: Error disconnecting from events: {e}") - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Error disconnecting: {e}", True) - - def _monkey_patch_script_methods(self): - """Monkey-patch the default script's line navigation methods.""" - try: - # Get the current active script - if self.app: - state = self.app.getDynamicApiManager().getAPI('CthulhuState') - if state and hasattr(state, 'activeScript') and state.activeScript: - script = state.activeScript - - # Store original method - if hasattr(script, 'sayLine'): - self._original_sayLine = script.sayLine - - # Create wrapped version - def wrapped_sayLine(obj): - # Call original method first - result = self._original_sayLine(obj) - - # Add our indentation audio check - try: - line, caretOffset, startOffset = script.getTextLineAtCaret(obj) - self.check_indentation_change(obj, line) - except Exception as e: - logger.error(f"IndentationAudio: Error in wrapped_sayLine: {e}") - - return result - - # Replace the method - script.sayLine = wrapped_sayLine - logger.info("IndentationAudio: Successfully monkey-patched sayLine method") - - except Exception as e: - logger.error(f"IndentationAudio: Error monkey-patching script methods: {e}") - - def _restore_script_methods(self): - """Restore original script methods.""" - try: - if self.app and hasattr(self, '_original_sayLine'): - state = self.app.getDynamicApiManager().getAPI('CthulhuState') - if state and hasattr(state, 'activeScript') and state.activeScript: - script = state.activeScript - if hasattr(script, 'sayLine'): - script.sayLine = self._original_sayLine - logger.info("IndentationAudio: Restored original sayLine method") - - except Exception as e: - logger.error(f"IndentationAudio: Error restoring script methods: {e}") - - def _toggle_indentation_audio(self, script, inputEvent=None): - """Toggle the indentation audio feedback on/off.""" - try: - current_mode = _settingsManager.getSetting('indentationPresentationMode') \ - or settings.indentationPresentationMode - current_speech = _settingsManager.getSetting('enableSpeechIndentation') - - if self._alerts_suspended: - new_mode = self._saved_presentation_mode - if new_mode is None: - new_mode = current_mode - _settingsManager.setSetting('indentationPresentationMode', new_mode) - if self._saved_speech_indentation is None: - self._sync_speech_setting(new_mode) - else: - _settingsManager.setSetting('enableSpeechIndentation', self._saved_speech_indentation) - self._alerts_suspended = False - state = "enabled" - else: - self._saved_presentation_mode = current_mode - self._saved_speech_indentation = current_speech - _settingsManager.setSetting( - 'indentationPresentationMode', - settings.INDENTATION_PRESENTATION_OFF, - ) - _settingsManager.setSetting('enableSpeechIndentation', False) - self._alerts_suspended = True - state = "disabled" - - # Announce the state change - message = f"Indentation alerts {state}" - if hasattr(script, 'speakMessage'): - script.speakMessage(message) - - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Alerts toggled to {state}", True) - - # Test the indentation detection on current line when enabled - if state == "enabled" and script: - try: - # Try to get current focus object and line text - from cthulhu import cthulhu_state - obj = cthulhu_state.locusOfFocus - if obj and hasattr(script, 'getTextLineAtCaret'): - line, caretOffset, startOffset = script.getTextLineAtCaret(obj) - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Testing with current line: '{line}'", True) - self.check_indentation_change(obj, line) - else: - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Could not get focus object or getTextLineAtCaret method", True) - except Exception as test_e: - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Test failed: {test_e}", True) - - logger.info(f"IndentationAudio: Toggled to {state}") - return True - - except Exception as e: - logger.error(f"IndentationAudio: Error toggling state: {e}") - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Error toggling: {e}", True) - return False - - def _sync_speech_setting(self, presentation_mode): - enable_speech = presentation_mode in ( - settings.INDENTATION_PRESENTATION_SPEECH, - settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS, - ) - _settingsManager.setSetting('enableSpeechIndentation', enable_speech) - - def _beeps_enabled(self): - if not self._enabled: - return False - presentation_mode = _settingsManager.getSetting('indentationPresentationMode') \ - or settings.indentationPresentationMode - return presentation_mode in ( - settings.INDENTATION_PRESENTATION_BEEPS, - settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS, - ) - - @staticmethod - def _extract_indentation(line_text): - if not line_text: - return "" - - line = line_text.replace("\u00a0", " ") - match = re.search(r"[^ \t]", line) - if not match: - return line - - return line[:match.start()] - - @staticmethod - def _count_columns(indentation, tab_width): - columns = 0 - tab_width = max(1, tab_width) - for char in indentation: - if char == "\t": - columns += tab_width - (columns % tab_width) - else: - columns += 1 - - return columns - - @staticmethod - def _count_levels(columns, spaces_per_level): - spaces_per_level = max(1, spaces_per_level) - if columns <= 0: - return 0 - - return int(math.ceil(columns / spaces_per_level)) - - def _get_indentation_data(self, line_text): - indentation = self._extract_indentation(line_text) - tab_width = _settingsManager.getSetting('indentationTabWidth') \ - or settings.indentationTabWidth - spaces_per_level = _settingsManager.getSetting('indentationSpacesPerLevel') \ - or settings.indentationSpacesPerLevel - columns = self._count_columns(indentation, tab_width) - levels = self._count_levels(columns, spaces_per_level) - return indentation, columns, levels - - def _generate_indentation_tone(self, new_units, old_units): - """Generate an audio tone for indentation change.""" - if not self._beeps_enabled(): - return - - base_frequency = _settingsManager.getSetting('indentationAudioBaseFrequency') \ - or self._base_frequency - frequency_step = _settingsManager.getSetting('indentationAudioStepFrequency') \ - or self._frequency_step - max_frequency = _settingsManager.getSetting('indentationAudioMaxFrequency') \ - or self._max_frequency - tone_duration = _settingsManager.getSetting('indentationAudioDuration') \ - or self._tone_duration - volume_multiplier = _settingsManager.getSetting('indentationAudioVolume') \ - or self._volume_multiplier - - # Calculate frequency based on new indentation units - base_frequency = min( - base_frequency + (new_units * frequency_step), - max_frequency - ) - - # Add directional audio cues - if new_units > old_units: - # Indentation increased - higher pitch - frequency = base_frequency + 50 - elif new_units < old_units: - # Indentation decreased - lower pitch - frequency = max(base_frequency - 50, 100) - else: - # Same level (shouldn't happen but just in case) - frequency = base_frequency - - try: - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: About to generate tone for units {new_units} (freq: {frequency}Hz)", True) - - if not SOUND_AVAILABLE or not self._player: - debug.printMessage(debug.LEVEL_INFO, "IndentationAudio: Sound player not available, using fallback", True) - # Fallback to ASCII bell - if new_units > 0: - beeps = min(new_units, 5) - for i in range(beeps): - print("\a", end="", flush=True) - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Sent {beeps} ASCII bell beeps", True) - return - - # Use Cthulhu's proper sound system - try: - # Create a tone based on indentation level - duration = tone_duration - - tone = Tone( - duration=duration, - frequency=frequency, - volumeMultiplier=volume_multiplier, - wave=Tone.SINE_WAVE - ) - - # Play the tone - self._player.play(tone, interrupt=False) - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Played Cthulhu tone - Units: {new_units}, Freq: {frequency}Hz", True) - - except Exception as sound_e: - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Cthulhu sound failed: {sound_e}", True) - # Fallback to ASCII bell - if new_units > 0: - beeps = min(new_units, 5) - for i in range(beeps): - print("\a", end="", flush=True) - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Used fallback ASCII bell ({beeps} beeps)", True) - - except Exception as e: - logger.error(f"IndentationAudio: Error generating tone: {e}") - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Exception in _generate_indentation_tone: {e}", True) - - @staticmethod - def _get_indentation_key(obj): - if obj is None: - return "global" - - document = None - try: - document = AXObject.find_ancestor(obj, AXUtilities.is_document) - except Exception: - document = None - - if document is None: - document = obj - - try: - return f"{id(document)}" - except Exception: - return str(document) - - def check_indentation_change(self, obj, line_text): - """Check if indentation has changed and play audio cue if needed. - - This method is intended to be called by scripts during line navigation. - """ - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: check_indentation_change called: line='{line_text}'", True) - - # Check if plugin is activated and enabled - if not self._activated or not self._enabled: - return - - if not line_text or not self._beeps_enabled(): - return - - try: - # Get object identifier for tracking - obj_id = self._get_indentation_key(obj) - - # Calculate current indentation data - indentation, columns, levels = self._get_indentation_data(line_text) - audio_unit = _settingsManager.getSetting('indentationAudioUnit') \ - or settings.indentationAudioUnit - current_units = columns if audio_unit == settings.INDENTATION_UNIT_COLUMNS else levels - - previous_data = self._last_indentation_data.get(obj_id) - previous_units = previous_data.get("units") if previous_data else current_units - - self._last_indentation_data[obj_id] = { - "signature": indentation, - "levels": levels, - "columns": columns, - "units": current_units, - } - - change_mode = _settingsManager.getSetting('indentationChangeMode') \ - or settings.indentationChangeMode - only_if_changed = _settingsManager.getSetting('speakIndentationOnlyIfChanged') - if only_if_changed is None: - only_if_changed = settings.speakIndentationOnlyIfChanged - - if not only_if_changed: - changed = True - elif previous_data is None: - changed = True - elif change_mode == settings.INDENTATION_CHANGE_ANY: - changed = previous_data.get("signature") != indentation - elif change_mode == settings.INDENTATION_CHANGE_LEVEL: - changed = previous_data.get("levels") != levels - else: - changed = True - - debug.printMessage( - debug.LEVEL_INFO, - f"IndentationAudio: Units - previous: {previous_units}, current: {current_units}, changed={changed}", - True, - ) - - # Play audio cue if indentation changed - if changed: - indent_debug = indentation.replace("\t", "\\t").replace(" ", ".") - debug.printMessage( - debug.LEVEL_INFO, - ( - f"IndentationAudio: Reporting indentation key={obj_id} " - f"units={current_units} levels={levels} columns={columns} " - f"mode={change_mode} onlyIfChanged={only_if_changed} " - f"indent='{indent_debug}'" - ), - True, - ) - self._generate_indentation_tone(current_units, previous_units) - - debug_msg = f"IndentationAudio: Indentation units changed from {previous_units} to {current_units}" - debug.printMessage(debug.LEVEL_INFO, debug_msg, True) - else: - debug.printMessage( - debug.LEVEL_INFO, - ( - "IndentationAudio: No indentation change; skipping tone " - f"key={obj_id} units={current_units} levels={levels} columns={columns}" - ), - True, - ) - - except Exception as e: - logger.error(f"IndentationAudio: Error checking indentation change: {e}") - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Exception in check_indentation_change: {e}", True) - - def is_enabled(self): - """Return whether the plugin is currently enabled.""" - return self._enabled - - def set_enabled(self, enabled): - """Set the enabled state of the plugin.""" - self._enabled = enabled - - def on_script_change(self, new_script): - """Handle when the active script changes.""" - try: - # Restore previous script if it was patched - self._restore_script_methods() - - # Re-apply patches to new script - self._monkey_patch_script_methods() - - # Clear tracking data for new context - self._last_indentation_data.clear() - - logger.info("IndentationAudio: Handled script change") - - except Exception as e: - logger.error(f"IndentationAudio: Error handling script change: {e}") diff --git a/src/cthulhu/plugins/meson.build b/src/cthulhu/plugins/meson.build index 75a1a99..4654d79 100644 --- a/src/cthulhu/plugins/meson.build +++ b/src/cthulhu/plugins/meson.build @@ -5,7 +5,6 @@ subdir('Clipboard') subdir('DisplayVersion') subdir('HelloCthulhu') subdir('GameMode') -subdir('IndentationAudio') subdir('nvda2cthulhu') subdir('OCR') subdir('PluginManager') diff --git a/src/cthulhu/script_utilities.py b/src/cthulhu/script_utilities.py index 9df07ff..7e95521 100644 --- a/src/cthulhu/script_utilities.py +++ b/src/cthulhu/script_utilities.py @@ -75,6 +75,14 @@ from .ax_utilities_relation import AXUtilitiesRelation _settingsManager = settings_manager.getManager() +# Try to import sound system for indentation beeps +try: + from . import sound + from .sound_generator import Tone + _SOUND_AVAILABLE = True +except ImportError: + _SOUND_AVAILABLE = False + ############################################################################# # # # Utilities # @@ -3126,6 +3134,14 @@ class Utilities: return string + # Hardcoded indentation audio parameters + _INDENTATION_TAB_WIDTH = 4 + _INDENTATION_BASE_FREQUENCY = 200 + _INDENTATION_STEP_FREQUENCY = 30 + _INDENTATION_MAX_FREQUENCY = 2700 + _INDENTATION_DURATION = 0.15 + _INDENTATION_VOLUME = 0.7 + def _get_indentation_key(self, obj): if obj is None: return "global" @@ -3148,63 +3164,29 @@ class Utilities: return line @staticmethod - def _get_indentation_segments(indentation): - if not indentation: - return [] - - spaces = [m.span() for m in re.finditer(" +", indentation)] - tabs = [m.span() for m in re.finditer("\t+", indentation)] - spans = sorted(spaces + tabs) - segments = [] - for (start, end) in spans: - if (start, end) in spaces: - segments.append(("spaces", end - start)) - else: - segments.append(("tabs", end - start)) - - return segments - - @staticmethod - def _get_indentation_columns(indentation, tab_width): + def _get_indentation_columns(indentation, tabWidth): columns = 0 - tab_width = max(1, tab_width) + tabWidth = max(1, tabWidth) for char in indentation: if char == "\t": - columns += tab_width - (columns % tab_width) + columns += tabWidth - (columns % tabWidth) else: columns += 1 return columns - @staticmethod - def _get_indentation_levels(columns, spaces_per_level): - spaces_per_level = max(1, spaces_per_level) - if columns <= 0: - return 0 - - return int(math.ceil(columns / spaces_per_level)) - def _get_indentation_data(self, line): indentation = self._extract_indentation(line) - tab_width = _settingsManager.getSetting('indentationTabWidth') \ - or settings.indentationTabWidth - spaces_per_level = _settingsManager.getSetting('indentationSpacesPerLevel') \ - or settings.indentationSpacesPerLevel - columns = self._get_indentation_columns(indentation, tab_width) - levels = self._get_indentation_levels(columns, spaces_per_level) - segments = self._get_indentation_segments(indentation) + columns = self._get_indentation_columns(indentation, self._INDENTATION_TAB_WIDTH) return { "indentation": indentation, - "segments": segments, "columns": columns, - "levels": levels, } def _remember_indentation(self, obj, data): key = self._get_indentation_key(obj) self._lastIndentationData[key] = { "signature": data["indentation"], - "levels": data["levels"], "columns": data["columns"], } @@ -3213,105 +3195,124 @@ class Utilities: previous = self._lastIndentationData.get(key) self._remember_indentation(obj, data) if previous is None: - return True + return True, 0 - change_mode = _settingsManager.getSetting('indentationChangeMode') \ - or settings.indentationChangeMode - if change_mode == settings.INDENTATION_CHANGE_ANY: - return previous.get("signature") != data["indentation"] - if change_mode == settings.INDENTATION_CHANGE_LEVEL: - return previous.get("levels") != data["levels"] + changed = previous.get("signature") != data["indentation"] + previousColumns = previous.get("columns", 0) + return changed, previousColumns - return True - - def _indentation_speech_enabled(self): + def _indentation_enabled(self): if _settingsManager.getSetting('onlySpeakDisplayedText'): return False - if not _settingsManager.getSetting('enableSpeechIndentation'): + return _settingsManager.getSetting('enableIndentation') + + def _indentation_speech_enabled(self): + if not self._indentation_enabled(): return False - presentation_mode = _settingsManager.getSetting('indentationPresentationMode') \ + presentationMode = _settingsManager.getSetting('indentationPresentationMode') \ or settings.indentationPresentationMode - return presentation_mode in ( + return presentationMode in ( settings.INDENTATION_PRESENTATION_SPEECH, settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS, ) - def _format_indentation_description(self, data): - style = _settingsManager.getSetting('indentationSpeechStyle') \ - or settings.indentationSpeechStyle - if style == settings.INDENTATION_SPEECH_STYLE_LEVELS: - return messages.indentationLevelCount(data["levels"]) - if style == settings.INDENTATION_SPEECH_STYLE_COLUMNS: - return messages.indentationColumnCount(data["columns"]) + def _indentation_beeps_enabled(self): + if not self._indentation_enabled(): + return False - result = "" - for kind, count in data["segments"]: - if kind == "spaces": - result += f"{messages.spacesCount(count)} " - else: - result += f"{messages.tabsCount(count)} " + presentationMode = _settingsManager.getSetting('indentationPresentationMode') \ + or settings.indentationPresentationMode + return presentationMode in ( + settings.INDENTATION_PRESENTATION_BEEPS, + settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS, + ) - if not result: - return messages.spacesCount(0) + def _play_indentation_tone(self, columns, previousColumns): + """Play an audio tone indicating indentation level.""" + if not _SOUND_AVAILABLE: + return - return result.strip() + try: + player = sound.getPlayer() + if not player: + return + + frequency = min( + self._INDENTATION_BASE_FREQUENCY + (columns * self._INDENTATION_STEP_FREQUENCY), + self._INDENTATION_MAX_FREQUENCY + ) + + # Add directional cue + if columns > previousColumns: + frequency += 50 + elif columns < previousColumns: + frequency = max(frequency - 50, 100) + + tone = Tone( + duration=self._INDENTATION_DURATION, + frequency=frequency, + volumeMultiplier=self._INDENTATION_VOLUME, + wave=Tone.SINE_WAVE + ) + player.play(tone, interrupt=False) + debug.printMessage(debug.LEVEL_INFO, + f"INDENTATION: played tone freq={frequency}Hz columns={columns}", True) + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, + f"INDENTATION: error playing tone: {e}", True) def get_indentation_presentation(self, line, obj=None): data = self._get_indentation_data(line) - has_indentation = bool(data["indentation"]) - presentation_mode = _settingsManager.getSetting('indentationPresentationMode') \ + hasIndentation = bool(data["indentation"]) + presentationMode = _settingsManager.getSetting('indentationPresentationMode') \ or settings.indentationPresentationMode - only_if_changed = _settingsManager.getSetting('speakIndentationOnlyIfChanged') - change_mode = _settingsManager.getSetting('indentationChangeMode') \ - or settings.indentationChangeMode - indent_debug = data["indentation"].replace("\t", "\\t").replace(" ", ".") + indentDebug = data["indentation"].replace("\t", "\\t").replace(" ", ".") - if not self._indentation_speech_enabled(): + if not self._indentation_enabled(): self._remember_indentation(obj, data) msg = ( - f"INDENTATION: speech disabled mode={presentation_mode} " - f"onlyIfChanged={only_if_changed} changeMode={change_mode} " - f"levels={data['levels']} columns={data['columns']} indent='{indent_debug}'" + f"INDENTATION: disabled mode={presentationMode} " + f"columns={data['columns']} indent='{indentDebug}'" ) debug.printMessage(debug.LEVEL_INFO, msg, True) - return "", has_indentation + return "", hasIndentation - if only_if_changed: - changed = self._indentation_has_changed(obj, data) - else: - changed = True - self._remember_indentation(obj, data) + changed, previousColumns = self._indentation_has_changed(obj, data) if not changed: msg = ( - f"INDENTATION: unchanged mode={presentation_mode} " - f"onlyIfChanged={only_if_changed} changeMode={change_mode} " - f"levels={data['levels']} columns={data['columns']} indent='{indent_debug}'" + f"INDENTATION: unchanged mode={presentationMode} " + f"columns={data['columns']} indent='{indentDebug}'" ) debug.printMessage(debug.LEVEL_INFO, msg, True) - return "", has_indentation + return "", hasIndentation - description = self._format_indentation_description(data) - msg = ( - f"INDENTATION: speaking '{description}' mode={presentation_mode} " - f"onlyIfChanged={only_if_changed} changeMode={change_mode} " - f"levels={data['levels']} columns={data['columns']} indent='{indent_debug}'" - ) - debug.printMessage(debug.LEVEL_INFO, msg, True) - return description, has_indentation + # Play beep if beeps are enabled + if self._indentation_beeps_enabled(): + self._play_indentation_tone(data["columns"], previousColumns) + + # Return speech description if speech is enabled + if self._indentation_speech_enabled(): + description = messages.indentationColumnCount(data["columns"]) + msg = ( + f"INDENTATION: speaking '{description}' mode={presentationMode} " + f"columns={data['columns']} indent='{indentDebug}'" + ) + debug.printMessage(debug.LEVEL_INFO, msg, True) + return description, hasIndentation + + return "", hasIndentation def should_strip_indentation(self, line): - presentation_mode = _settingsManager.getSetting('indentationPresentationMode') \ - or settings.indentationPresentationMode - if presentation_mode == settings.INDENTATION_PRESENTATION_OFF: + if not self._indentation_enabled(): return False data = self._get_indentation_data(line) return bool(data["indentation"]) def indentationDescription(self, line, obj=None): - description, _has_indentation = self.get_indentation_presentation(line, obj=obj) + description, _hasIndentation = self.get_indentation_presentation(line, obj=obj) return description @staticmethod diff --git a/src/cthulhu/settings.py b/src/cthulhu/settings.py index 85f6afd..d36bed5 100644 --- a/src/cthulhu/settings.py +++ b/src/cthulhu/settings.py @@ -47,19 +47,8 @@ userCustomizableSettings = [ "readFullRowInGUITable", "readFullRowInDocumentTable", "readFullRowInSpreadSheet", - "enableSpeechIndentation", + "enableIndentation", "indentationPresentationMode", - "indentationChangeMode", - "speakIndentationOnlyIfChanged", - "indentationSpeechStyle", - "indentationAudioUnit", - "indentationSpacesPerLevel", - "indentationTabWidth", - "indentationAudioBaseFrequency", - "indentationAudioStepFrequency", - "indentationAudioMaxFrequency", - "indentationAudioDuration", - "indentationAudioVolume", "enableEchoByCharacter", "enableEchoByWord", "enableEchoBySentence", @@ -233,22 +222,12 @@ AI_SCREENSHOT_QUALITY_LOW = "low" AI_SCREENSHOT_QUALITY_MEDIUM = "medium" AI_SCREENSHOT_QUALITY_HIGH = "high" +# Indentation presentation modes INDENTATION_PRESENTATION_OFF = "off" INDENTATION_PRESENTATION_SPEECH = "speech" INDENTATION_PRESENTATION_BEEPS = "beeps" INDENTATION_PRESENTATION_SPEECH_AND_BEEPS = "speech_and_beeps" -INDENTATION_CHANGE_ALWAYS = "always" -INDENTATION_CHANGE_ANY = "any" -INDENTATION_CHANGE_LEVEL = "level" - -INDENTATION_SPEECH_STYLE_SPACES_TABS = "spaces_tabs" -INDENTATION_SPEECH_STYLE_LEVELS = "levels" -INDENTATION_SPEECH_STYLE_COLUMNS = "columns" - -INDENTATION_UNIT_LEVELS = "levels" -INDENTATION_UNIT_COLUMNS = "columns" - DEFAULT_VOICE = "default" UPPERCASE_VOICE = "uppercase" HYPERLINK_VOICE = "hyperlink" @@ -283,19 +262,8 @@ silenceSpeech = False enableTutorialMessages = False enableMnemonicSpeaking = False enablePositionSpeaking = False -enableSpeechIndentation = True -indentationPresentationMode = INDENTATION_PRESENTATION_SPEECH_AND_BEEPS -indentationChangeMode = INDENTATION_CHANGE_ANY -speakIndentationOnlyIfChanged = True -indentationSpeechStyle = INDENTATION_SPEECH_STYLE_COLUMNS -indentationAudioUnit = INDENTATION_UNIT_COLUMNS -indentationSpacesPerLevel = 4 -indentationTabWidth = 4 -indentationAudioBaseFrequency = 200 -indentationAudioStepFrequency = 30 -indentationAudioMaxFrequency = 2700 -indentationAudioDuration = 0.15 -indentationAudioVolume = 0.7 +enableIndentation = False +indentationPresentationMode = INDENTATION_PRESENTATION_SPEECH onlySpeakDisplayedText = False presentToolTips = False speakBlankLines = True @@ -501,7 +469,6 @@ activePlugins = [ 'HelloCthulhu', 'DisplayVersion', 'GameMode', - 'IndentationAudio', 'nvda2cthulhu', 'OCR', 'SpeechHistory', From 45dd30f7f620fd9833b2b5fee105930af6b24896 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 15 Jan 2026 23:51:49 -0500 Subject: [PATCH 07/13] I didn't actually intend to do this, but, major refactor. Be careful with testing, it may be horribly broken. --- meson.build | 6 +- pyproject.toml | 1 + src/cthulhu.py | 2 +- src/cthulhu/ax_utilities.py | 43 ++- src/cthulhu/bookmarks.py | 8 +- src/cthulhu/braille.py | 32 +- src/cthulhu/braille_generator.py | 24 +- src/cthulhu/chat.py | 31 +- src/cthulhu/cthulhu.py | 149 ++++---- src/cthulhu/cthulhu_bin.py.in | 2 +- src/cthulhu/cthulhu_gui_prefs.py | 70 ++-- src/cthulhu/cthulhu_modifier_manager.py | 10 +- src/cthulhu/date_and_time_presenter.py | 7 +- src/cthulhu/debug.py | 6 + src/cthulhu/event_manager.py | 69 ++-- src/cthulhu/flat_review_presenter.py | 10 +- src/cthulhu/focus_manager.py | 32 +- src/cthulhu/generator.py | 13 +- src/cthulhu/input_event.py | 68 ++-- src/cthulhu/input_event_manager.py | 104 +++--- src/cthulhu/learn_mode_presenter.py | 13 +- src/cthulhu/liveregions.py | 15 +- src/cthulhu/logger.py | 4 - src/cthulhu/mouse_review.py | 24 +- src/cthulhu/plugin.py | 28 +- src/cthulhu/plugin_system_manager.py | 320 +++++++----------- src/cthulhu/plugins/GameMode/plugin.py | 3 +- src/cthulhu/plugins/PluginManager/plugin.py | 31 +- .../plugins/SimplePluginSystem/plugin.py | 3 +- src/cthulhu/plugins/nvda2cthulhu/plugin.py | 8 +- src/cthulhu/resource_manager.py | 4 +- src/cthulhu/script.py | 44 +-- src/cthulhu/script_manager.py | 19 +- src/cthulhu/script_utilities.py | 38 +-- src/cthulhu/scripts/apps/notify-osd/script.py | 2 +- .../scripts/apps/soffice/braille_generator.py | 2 +- src/cthulhu/scripts/default.py | 111 +++--- .../scripts/terminal/script_utilities.py | 4 +- src/cthulhu/scripts/web/script.py | 84 ++--- src/cthulhu/scripts/web/script_utilities.py | 12 +- src/cthulhu/scripts/web/sound_generator.py | 7 +- src/cthulhu/scripts/web/speech_generator.py | 37 +- src/cthulhu/settings.py | 1 - src/cthulhu/settings_manager.py | 136 ++++++-- src/cthulhu/signal_manager.py | 6 +- src/cthulhu/sound_generator.py | 45 +-- src/cthulhu/sound_theme_manager.py | 36 +- src/cthulhu/speech.py | 21 +- src/cthulhu/speech_and_verbosity_manager.py | 226 +++++++------ src/cthulhu/speech_generator.py | 191 +++++------ src/cthulhu/speechdispatcherfactory.py | 7 +- src/cthulhu/spellcheck.py | 18 +- src/cthulhu/structural_navigation.py | 4 +- 53 files changed, 1101 insertions(+), 1090 deletions(-) diff --git a/meson.build b/meson.build index ac9ee02..ed57dbd 100644 --- a/meson.build +++ b/meson.build @@ -28,6 +28,11 @@ if not json_result.found() error('json module is required') endif +pluggy_result = python.find_installation('python3', modules:['pluggy'], required: false) +if not pluggy_result.found() + error('pluggy module is required') +endif + # End users might not have the Gtk development libraries installed, making pkg-config fail. # Therefore, check this dependency via python. gtk_major_version = '3' @@ -50,7 +55,6 @@ optional_modules = { 'brlapi': 'braille output', 'louis': 'contracted braille', 'speechd': 'speech output', - 'pluggy': 'plugin system', 'dasbus': 'D-Bus remote controller', 'psutil': 'system information commands', 'gi.repository.Wnck': 'mouse review', diff --git a/pyproject.toml b/pyproject.toml index a0166cd..ffac654 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ requires-python = ">=3.9" license = { text = "LGPL-2.1-or-later" } dependencies = [ "pygobject>=3.18", + "pluggy", "brlapi; extra == 'braille'", "python-speechd; extra == 'speech'", "piper-tts; extra == 'piper'", diff --git a/src/cthulhu.py b/src/cthulhu.py index 2843c66..aecf2d9 100644 --- a/src/cthulhu.py +++ b/src/cthulhu.py @@ -368,7 +368,7 @@ def main(): debug.printMessage(debug.LEVEL_INFO, "INFO: Preparing to launch.", True) from cthulhu import cthulhu - manager = cthulhu.getSettingsManager() + manager = cthulhu.cthulhuApp.settingsManager if not manager: print(messages.CLI_SETTINGS_MANAGER_ERROR) diff --git a/src/cthulhu/ax_utilities.py b/src/cthulhu/ax_utilities.py index 4de6e53..9d53eb7 100644 --- a/src/cthulhu/ax_utilities.py +++ b/src/cthulhu/ax_utilities.py @@ -129,6 +129,11 @@ class AXUtilities: debug.print_tokens(debug.LEVEL_INFO, tokens, True) return False + if not AXUtilitiesState.is_visible(window): + tokens.append("lacks visible state") + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + if AXUtilitiesState.is_iconified(window): tokens.append("is iconified") debug.print_tokens(debug.LEVEL_INFO, tokens, True) @@ -174,35 +179,27 @@ class AXUtilities: tokens = ["AXUtilities: These windows all claim to be active:", candidates] debug.print_tokens(debug.LEVEL_INFO, tokens, True) - # Some electron apps running in the background claim to be active even when they - # are not. These are the ones we know about. We can add others as we go. - suspect_apps = ["slack", - "discord", - "outline-client", - "whatsapp-desktop-linux"] - filtered = [] + focused_candidates = [] for frame in candidates: - if AXObject.get_name(AXUtilitiesApplication.get_application(frame)) in suspect_apps: - tokens = ["AXUtilities: Suspecting", frame, "is a non-active Electron app"] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) - else: - filtered.append(frame) + if AXUtilitiesState.is_focused(frame): + focused_candidates.append(frame) + continue + focused = AXUtilities.get_focused_object(frame) + if focused is not None: + focused_candidates.append(frame) - if len(filtered) == 1: - tokens = ["AXUtilities: Active window is believed to be", filtered[0]] + if len(focused_candidates) == 1: + tokens = ["AXUtilities: Active window has focus:", focused_candidates[0]] debug.print_tokens(debug.LEVEL_INFO, tokens, True) - return filtered[0] + return focused_candidates[0] - guess: Optional[Atspi.Accessible] = None - if filtered: - tokens = ["AXUtilities: Still have multiple active windows:", filtered] + if not focused_candidates and len(candidates) > 1: + tokens = ["AXUtilities: No focused active window found"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) - guess = filtered[0] + return None - if guess is not None: - tokens = ["AXUtilities: Returning", guess, "as active window"] - else: - tokens = ["AXUtilities: No active window found"] + guess = focused_candidates[0] if focused_candidates else candidates[0] + tokens = ["AXUtilities: Returning", guess, "as active window"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return guess diff --git a/src/cthulhu/bookmarks.py b/src/cthulhu/bookmarks.py index 68481d8..6d474f6 100644 --- a/src/cthulhu/bookmarks.py +++ b/src/cthulhu/bookmarks.py @@ -29,6 +29,8 @@ import pickle import os import urllib.parse +from . import cthulhu # Need access to cthulhuApp + from . import cmdnames from . import keybindings from . import input_event @@ -36,7 +38,7 @@ from . import messages from . import settings_manager from .ax_object import AXObject -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class Bookmarks: """Represents a default bookmark handler.""" @@ -236,7 +238,7 @@ class Bookmarks: """ Read saved bookmarks from disk. Currently an unpickled object that represents a bookmark """ filename = filename or self._script.name.split(' ')[0] - cthulhuDir = _settingsManager.getPrefsDir() + cthulhuDir = cthulhu.cthulhuApp.settingsManager.getPrefsDir() if not cthulhuDir: return @@ -254,7 +256,7 @@ class Bookmarks: """ Write bookmarks to disk. bookmarksObj must be a pickleable object. """ filename = filename or self._script.name.split(' ')[0] - cthulhuDir = _settingsManager.getPrefsDir() + cthulhuDir = cthulhu.cthulhuApp.settingsManager.getPrefsDir() cthulhuBookmarksDir = os.path.join(cthulhuDir, "bookmarks") # create directory if it does not exist. correct place?? try: diff --git a/src/cthulhu/braille.py b/src/cthulhu/braille.py index 139d0e6..71f1b4c 100644 --- a/src/cthulhu/braille.py +++ b/src/cthulhu/braille.py @@ -60,10 +60,26 @@ from .ax_object import AXObject from .ax_hypertext import AXHypertext from .cthulhu_platform import tablesdir -_logger = logger.getLogger() -log = _logger.newLog("braille") +# Lazy initialization to avoid circular imports +_logger = None +log = None _monitor = None -_settingsManager = settings_manager.getManager() +_settingsManager = None + +def _ensureLogger(): + """Ensure logger is initialized.""" + global _logger, log + if _logger is None: + from . import cthulhu + _logger = cthulhu.cthulhuApp.logger + log = _logger.newLog("braille") + +def _ensureSettingsManager(): + """Ensure settings manager is initialized.""" + global _settingsManager + if _settingsManager is None: + from . import cthulhu + _settingsManager = cthulhu.cthulhuApp.settingsManager try: msg = "BRAILLE: About to import brlapi." @@ -1223,6 +1239,8 @@ def disableBraille(): """Hand off control to other screen readers, shutting down the BrlAPI connection if needed""" + _ensureSettingsManager() + global idle tokens = ["BRAILLE: Disabling braille. BrlAPI running:", _brlAPIRunning] @@ -1232,6 +1250,7 @@ def disableBraille(): msg = "BRAILLE: BrlApi running and not idle." debug.printMessage(debug.LEVEL_INFO, msg, True) + _ensureSettingsManager() if not _idleBraille() and not _settingsManager.getSetting('enableBraille'): # BrlAPI before 0.8 and we really want to shut down msg = "BRAILLE: could not go idle, completely shut down" @@ -1241,6 +1260,8 @@ def disableBraille(): def checkBrailleSetting(): """Disable Braille if it got disabled in the preferences""" + _ensureSettingsManager() + msg = "BRAILLE: Checking braille setting." debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -1271,6 +1292,9 @@ def refresh(panToCursor=True, targetCursorCell=0, getLinkMask=True, stopFlash=Tr # TODO - JD: Split this work out into smaller methods. + _ensureLogger() + _ensureSettingsManager() + global endIsShowing global beginningIsShowing global cursorCell @@ -1284,6 +1308,7 @@ def refresh(panToCursor=True, targetCursorCell=0, getLinkMask=True, stopFlash=Tr killFlash(restoreSaved=False) # TODO - JD: This should be taken care of in cthulhu.py. + _ensureSettingsManager() if not _settingsManager.getSetting('enableBraille') \ and not _settingsManager.getSetting('enableBrailleMonitor'): if _brlAPIRunning: @@ -1433,6 +1458,7 @@ def refresh(panToCursor=True, targetCursorCell=0, getLinkMask=True, stopFlash=Tr submask += '\x00' * (len(substring) - len(submask)) + _ensureSettingsManager() if _settingsManager.getSetting('enableBraille'): _enableBraille() diff --git a/src/cthulhu/braille_generator.py b/src/cthulhu/braille_generator.py index 47d4177..d8021d8 100644 --- a/src/cthulhu/braille_generator.py +++ b/src/cthulhu/braille_generator.py @@ -49,7 +49,7 @@ from .ax_utilities import AXUtilities from .ax_utilities_relation import AXUtilitiesRelation from .braille_rolenames import shortRoleNames -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class Space: """A dummy class to indicate we want to insert a space into an @@ -96,8 +96,8 @@ class BrailleGenerator(generator.Generator): return AXObject.get_name(obj) == AXObject.get_name(region.accessible) def generateBraille(self, obj, **args): - if not _settingsManager.getSetting('enableBraille') \ - and not _settingsManager.getSetting('enableBrailleMonitor'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableBraille') \ + and not cthulhu.cthulhuApp.settingsManager.getSetting('enableBrailleMonitor'): debug.printMessage(debug.LEVEL_INFO, "BRAILLE GENERATOR: generation disabled", True) return [[], None] @@ -163,12 +163,12 @@ class BrailleGenerator(generator.Generator): """ if args.get('isProgressBarUpdate') \ - and not _settingsManager.getSetting('brailleProgressBarUpdates'): + and not cthulhu.cthulhuApp.settingsManager.getSetting('brailleProgressBarUpdates'): return [] result = [] role = args.get('role', AXObject.get_role(obj)) - verbosityLevel = _settingsManager.getSetting('brailleVerbosityLevel') + verbosityLevel = cthulhu.cthulhuApp.settingsManager.getSetting('brailleVerbosityLevel') doNotPresent = [Atspi.Role.UNKNOWN, Atspi.Role.REDUNDANT_OBJECT, @@ -200,7 +200,7 @@ class BrailleGenerator(generator.Generator): - obj: an Accessible object """ - if _settingsManager.getSetting('brailleRolenameStyle') \ + if cthulhu.cthulhuApp.settingsManager.getSetting('brailleRolenameStyle') \ == settings.BRAILLE_ROLENAME_STYLE_SHORT: role = args.get('role', AXObject.get_role(obj)) rv = shortRoleNames.get(role) @@ -230,7 +230,7 @@ class BrailleGenerator(generator.Generator): or an empty array if no accelerator can be found. """ - verbosityLevel = _settingsManager.getSetting('brailleVerbosityLevel') + verbosityLevel = cthulhu.cthulhuApp.settingsManager.getSetting('brailleVerbosityLevel') if verbosityLevel == settings.VERBOSITY_LEVEL_BRIEF: return [] @@ -275,7 +275,7 @@ class BrailleGenerator(generator.Generator): previous object with focus. """ result = [] - if not _settingsManager.getSetting('enableBrailleContext'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableBrailleContext'): return result args['includeContext'] = False @@ -417,14 +417,14 @@ class BrailleGenerator(generator.Generator): return [] def _getProgressBarUpdateInterval(self): - interval = _settingsManager.getSetting('progressBarBrailleInterval') + interval = cthulhu.cthulhuApp.settingsManager.getSetting('progressBarBrailleInterval') if interval is None: return super()._getProgressBarUpdateInterval() return int(interval) def _shouldPresentProgressBarUpdate(self, obj, **args): - if not _settingsManager.getSetting('brailleProgressBarUpdates'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('brailleProgressBarUpdates'): return False return super()._shouldPresentProgressBarUpdate(obj, **args) @@ -470,7 +470,7 @@ class BrailleGenerator(generator.Generator): # are on the very first line. Otherwise, we show only the # line. # - include = _settingsManager.getSetting('enableBrailleContext') + include = cthulhu.cthulhuApp.settingsManager.getSetting('enableBrailleContext') if not include: return include if AXObject.supports_text(obj) \ @@ -493,7 +493,7 @@ class BrailleGenerator(generator.Generator): def _generateEol(self, obj, **args): result = [] - if not _settingsManager.getSetting('disableBrailleEOL'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('disableBrailleEOL'): if not args.get('mode', None): args['mode'] = self._mode args['stringType'] = 'eol' diff --git a/src/cthulhu/chat.py b/src/cthulhu/chat.py index ae10ed6..cc587f8 100644 --- a/src/cthulhu/chat.py +++ b/src/cthulhu/chat.py @@ -31,6 +31,7 @@ __date__ = "$Date$" __copyright__ = "Copyright (c) 2010-2011 The Cthulhu Team" __license__ = "LGPL" +from . import cthulhu # Need access to cthulhuApp from . import cmdnames from . import debug from . import guilabels @@ -43,7 +44,7 @@ from . import settings_manager from .ax_object import AXObject from .ax_utilities import AXUtilities -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager ############################################################################# # # @@ -422,19 +423,19 @@ class Chat: grid.set_border_width(12) label = guilabels.CHAT_SPEAK_ROOM_NAME - value = _settingsManager.getSetting('chatSpeakRoomName') + value = cthulhu.cthulhuApp.settingsManager.getSetting('chatSpeakRoomName') self.speakNameCheckButton = Gtk.CheckButton.new_with_mnemonic(label) self.speakNameCheckButton.set_active(value) grid.attach(self.speakNameCheckButton, 0, 0, 1, 1) label = guilabels.CHAT_ANNOUNCE_BUDDY_TYPING - value = _settingsManager.getSetting('chatAnnounceBuddyTyping') + value = cthulhu.cthulhuApp.settingsManager.getSetting('chatAnnounceBuddyTyping') self.buddyTypingCheckButton = Gtk.CheckButton.new_with_mnemonic(label) self.buddyTypingCheckButton.set_active(value) grid.attach(self.buddyTypingCheckButton, 0, 1, 1, 1) label = guilabels.CHAT_SEPARATE_MESSAGE_HISTORIES - value = _settingsManager.getSetting('chatRoomHistories') + value = cthulhu.cthulhuApp.settingsManager.getSetting('chatRoomHistories') self.chatRoomHistoriesCheckButton = \ Gtk.CheckButton.new_with_mnemonic(label) self.chatRoomHistoriesCheckButton.set_active(value) @@ -452,7 +453,7 @@ class Chat: messagesGrid = Gtk.Grid() messagesAlignment.add(messagesGrid) - value = _settingsManager.getSetting('chatMessageVerbosity') + value = cthulhu.cthulhuApp.settingsManager.getSetting('chatMessageVerbosity') label = guilabels.CHAT_SPEAK_MESSAGES_ALL rb1 = Gtk.RadioButton.new_with_mnemonic(None, label) @@ -512,8 +513,8 @@ class Chat: """ line = messages.CHAT_ROOM_NAME_PREFIX_ON - speakRoomName = _settingsManager.getSetting('chatSpeakRoomName') - _settingsManager.setSetting('chatSpeakRoomName', not speakRoomName) + speakRoomName = cthulhu.cthulhuApp.settingsManager.getSetting('chatSpeakRoomName') + cthulhu.cthulhuApp.settingsManager.setSetting('chatSpeakRoomName', not speakRoomName) if speakRoomName: line = messages.CHAT_ROOM_NAME_PREFIX_OFF self._script.presentMessage(line) @@ -529,8 +530,8 @@ class Chat: """ line = messages.CHAT_BUDDY_TYPING_ON - announceTyping = _settingsManager.getSetting('chatAnnounceBuddyTyping') - _settingsManager.setSetting( + announceTyping = cthulhu.cthulhuApp.settingsManager.getSetting('chatAnnounceBuddyTyping') + cthulhu.cthulhuApp.settingsManager.setSetting( 'chatAnnounceBuddyTyping', not announceTyping) if announceTyping: line = messages.CHAT_BUDDY_TYPING_OFF @@ -547,8 +548,8 @@ class Chat: """ line = messages.CHAT_SEPARATE_HISTORIES_ON - roomHistories = _settingsManager.getSetting('chatRoomHistories') - _settingsManager.setSetting('chatRoomHistories', not roomHistories) + roomHistories = cthulhu.cthulhuApp.settingsManager.getSetting('chatRoomHistories') + cthulhu.cthulhuApp.settingsManager.setSetting('chatRoomHistories', not roomHistories) if roomHistories: line = messages.CHAT_SEPARATE_HISTORIES_OFF self._script.presentMessage(line) @@ -596,7 +597,7 @@ class Chat: messageNumber = self.messageListLength - (index + 1) message, chatRoomName = None, None - if _settingsManager.getSetting('chatRoomHistories'): + if cthulhu.cthulhuApp.settingsManager.getSetting('chatRoomHistories'): conversation = self.getConversation(cthulhu_state.locusOfFocus) if conversation: message = conversation.getNthMessage(messageNumber) @@ -621,7 +622,7 @@ class Chat: # Only speak/braille the new message if it matches how the user # wants chat messages spoken. # - verbosity = _settingsManager.getAppSetting(self._script.app, 'chatMessageVerbosity') + verbosity = cthulhu.cthulhuApp.settingsManager.getAppSetting(self._script.app, 'chatMessageVerbosity') if cthulhu_state.activeScript.name != self._script.name \ and verbosity == settings.CHAT_SPEAK_ALL_IF_FOCUSED: return @@ -630,7 +631,7 @@ class Chat: text = "" if chatRoomName and \ - _settingsManager.getAppSetting(self._script.app, 'chatSpeakRoomName'): + cthulhu.cthulhuApp.settingsManager.getAppSetting(self._script.app, 'chatSpeakRoomName'): text = messages.CHAT_MESSAGE_FROM_ROOM % chatRoomName if not settings.presentChatRoomLast: @@ -738,7 +739,7 @@ class Chat: Returns True if we spoke the change; False otherwise """ - if _settingsManager.getSetting('chatAnnounceBuddyTyping'): + if cthulhu.cthulhuApp.settingsManager.getSetting('chatAnnounceBuddyTyping'): conversation = self.getConversation(event.source) if conversation and (status != conversation.getTypingStatus()): voice = self._script.speechGenerator.voice(string=status) diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index a95fadf..d8f979e 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -49,7 +49,7 @@ class APIHelper: self.app = app self._gestureBindings = {} - def registerGestureByString(self, function, name, gestureString, inputEventType='default', normalizer='cthulhu', learnModeEnabled=True, contextName=None): + def registerGestureByString(self, function, name, gestureString, inputEventType='default', normalizer='cthulhu', learnModeEnabled=True, contextName=None, globalBinding=False): """Register a gesture by string.""" import logging logger = logging.getLogger(__name__) @@ -134,9 +134,17 @@ class APIHelper: self._gestureBindings[contextName] = [] self._gestureBindings[contextName].append(binding) logger.info(f"Stored binding in context '{contextName}'") - + + if contextName and self.app: + try: + plugin_manager = self.app.getPluginSystemManager() + except Exception: + plugin_manager = None + if plugin_manager: + plugin_manager.add_keybinding(contextName, binding, global_binding=globalBinding) + # Only add to active script if one exists - if cthulhu_state.activeScript: + if cthulhu_state.activeScript and not globalBinding: logger.info(f"Adding binding to active script: {cthulhu_state.activeScript}") bindings = cthulhu_state.activeScript.getKeyBindings() bindings.add(binding) @@ -231,6 +239,7 @@ from . import acss from . import text_attribute_names from . import speechserver from . import input_event +from . import input_event_manager from . import pronunciation_dict from . import cthulhu_gtkbuilder from . import signal_manager @@ -239,23 +248,7 @@ from . import translation_manager from . import resource_manager -# Lazy initialization to avoid circular imports -_eventManager = None -_scriptManager = None -_settingsManager = None -_logger = None - -def _ensureManagers(): - """Ensure managers are initialized (lazy initialization).""" - global _eventManager, _scriptManager, _settingsManager, _logger - if _eventManager is None: - _eventManager = event_manager.getManager() - if _scriptManager is None: - _scriptManager = script_manager.get_manager() - if _settingsManager is None: - _settingsManager = settings_manager.getManager() - if _logger is None: - _logger = logger.getLogger() +# Old global variables removed - now using cthulhuApp.* instead def onEnabledChanged(gsetting, key): try: @@ -266,13 +259,6 @@ def onEnabledChanged(gsetting, key): if key == 'screen-reader-enabled' and not enabled: shutdown() -def getSettingsManager(): - _ensureManagers() - return _settingsManager - -def getLogger(): - return _logger - EXIT_CODE_HANG = 50 # The user-settings module (see loadUserSettings). @@ -350,7 +336,7 @@ def _processBrailleEvent(event): cthulhu_state.lastInputEvent = event try: - consumed = _eventManager.processBrailleEvent(event) + consumed = cthulhuApp.eventManager.processBrailleEvent(event) except Exception: debug.printException(debug.LEVEL_SEVERE) @@ -375,11 +361,6 @@ def deviceChangeHandler(deviceManager, device): debug.printMessage(debug.LEVEL_INFO, msg, True) cthulhu_modifier_manager.getManager().refreshCthulhuModifiers("Keyboard change detected.") -def setKeyHandling(new): - """Toggle use of the new vs. legacy key handling mode. - """ - _eventManager.setKeyHandling(new) - def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False): """Loads (and reloads) the user settings module, reinitializing things such as speech if necessary. @@ -398,35 +379,35 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False): speech.shutdown() braille.shutdown() - _scriptManager.deactivate() + cthulhuApp.scriptManager.deactivate() cthulhuApp.getSignalManager().emitSignal('load-setting-begin') reloaded = False if _userSettings: - _profile = _settingsManager.getSetting('activeProfile')[1] + _profile = cthulhuApp.settingsManager.getSetting('activeProfile')[1] try: - _userSettings = _settingsManager.getGeneralSettings(_profile) - _settingsManager.setProfile(_profile) + _userSettings = cthulhuApp.settingsManager.getGeneralSettings(_profile) + cthulhuApp.settingsManager.setProfile(_profile) reloaded = True except ImportError: debug.printException(debug.LEVEL_INFO) except Exception: debug.printException(debug.LEVEL_SEVERE) else: - _profile = _settingsManager.profile + _profile = cthulhuApp.settingsManager.profile try: - _userSettings = _settingsManager.getGeneralSettings(_profile) + _userSettings = cthulhuApp.settingsManager.getGeneralSettings(_profile) except ImportError: debug.printException(debug.LEVEL_INFO) except Exception: debug.printException(debug.LEVEL_SEVERE) if not script: - script = _scriptManager.get_default_script() + script = cthulhuApp.scriptManager.get_default_script() - _settingsManager.loadAppSettings(script) + cthulhuApp.settingsManager.loadAppSettings(script) - if _settingsManager.getSetting('enableSpeech'): + if cthulhuApp.settingsManager.getSetting('enableSpeech'): msg = 'CTHULHU: About to enable speech' debug.printMessage(debug.LEVEL_INFO, msg, True) try: @@ -439,7 +420,7 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False): msg = 'CTHULHU: Speech is not enabled in settings' debug.printMessage(debug.LEVEL_INFO, msg, True) - if _settingsManager.getSetting('enableBraille'): + if cthulhuApp.settingsManager.getSetting('enableBraille'): msg = 'CTHULHU: About to enable braille' debug.printMessage(debug.LEVEL_INFO, msg, True) try: @@ -453,24 +434,24 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False): debug.printMessage(debug.LEVEL_INFO, msg, True) - if _settingsManager.getSetting('enableMouseReview'): + if cthulhuApp.settingsManager.getSetting('enableMouseReview'): mouse_review.getReviewer().activate() else: mouse_review.getReviewer().deactivate() - if _settingsManager.getSetting('enableSound'): + if cthulhuApp.settingsManager.getSetting('enableSound'): player.init() cthulhu_modifier_manager.getManager().refreshCthulhuModifiers("Loading user settings.") # Activate core systems FIRST before loading plugins - _scriptManager.activate() - _eventManager.activate() + cthulhuApp.scriptManager.activate() + cthulhuApp.eventManager.activate() cthulhuApp.getSignalManager().emitSignal('load-setting-begin') # NOW load plugins after script system is ready - activePluginsSetting = _settingsManager.getSetting('activePlugins') or [] + activePluginsSetting = cthulhuApp.settingsManager.getSetting('activePlugins') or [] debug.printMessage( debug.LEVEL_INFO, f"CTHULHU: Active plugins defaults: {settings.activePlugins}", @@ -524,7 +505,7 @@ def showAppPreferencesGUI(script=None, inputEvent=None): prefs = {} for key in settings.userCustomizableSettings: - prefs[key] = _settingsManager.getSetting(key) + prefs[key] = cthulhuApp.settingsManager.getSetting(key) script = script or cthulhu_state.activeScript _showPreferencesUI(script, prefs) @@ -538,9 +519,8 @@ def showPreferencesGUI(script=None, inputEvent=None): Returns True to indicate the input event has been consumed. """ - _ensureManagers() # Initialize managers if not already done - prefs = _settingsManager.getGeneralSettings(_settingsManager.profile) - script = _scriptManager.get_default_script() + prefs = cthulhuApp.settingsManager.getGeneralSettings(cthulhuApp.settingsManager.profile) + script = cthulhuApp.scriptManager.get_default_script() _showPreferencesUI(script, prefs) return True @@ -548,27 +528,18 @@ def showPreferencesGUI(script=None, inputEvent=None): def addKeyGrab(binding): """ Add a key grab for the given key binding.""" - if cthulhu_state.device is None: - return [] - - ret = [] - for kd in binding.keyDefs(): - ret.append(cthulhu_state.device.add_key_grab(kd, None)) - return ret + manager = input_event_manager.get_manager() + return manager.add_grabs_for_keybinding(binding) def removeKeyGrab(id): """ Remove the key grab for the given key binding.""" - if cthulhu_state.device is None: - return - - cthulhu_state.device.remove_key_grab(id) + manager = input_event_manager.get_manager() + manager.remove_grab_by_id(id) def mapModifier(keycode): - if cthulhu_state.device is None: - return - - return cthulhu_state.device.map_modifier(keycode) + manager = input_event_manager.get_manager() + return manager.map_keycode_to_modifier(keycode) def quitCthulhu(script=None, inputEvent=None): """Quit Cthulhu. Check if the user wants to confirm this action. @@ -610,7 +581,7 @@ def init(): global _initialized - if _initialized and _settingsManager.isScreenReaderServiceEnabled(): + if _initialized and cthulhuApp.settingsManager.isScreenReaderServiceEnabled(): debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Already initialized', True) return False @@ -622,7 +593,7 @@ def init(): # Activate settings manager before loading user settings debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Activating settings manager', True) - _settingsManager.activate() + cthulhuApp.settingsManager.activate() loadUserSettings() @@ -757,8 +728,8 @@ def shutdown(script=None, inputEvent=None): # Deactivate the event manager first so that it clears its queue and will not # accept new events. Then let the script manager unregister script event listeners. - _eventManager.deactivate() - _scriptManager.deactivate() + cthulhuApp.eventManager.deactivate() + cthulhuApp.scriptManager.deactivate() # Shutdown all the other support. # @@ -867,8 +838,8 @@ def main(): signal.signal(signal.SIGSEGV, crashOnSignal) debug.printMessage(debug.LEVEL_INFO, "CTHULHU: Enabling accessibility (if needed).", True) - if not _settingsManager.isAccessibilityEnabled(): - _settingsManager.setAccessibility(True) + if not cthulhuApp.settingsManager.isAccessibilityEnabled(): + cthulhuApp.settingsManager.setAccessibility(True) debug.printMessage(debug.LEVEL_INFO, "CTHULHU: Initializing.", True) init() @@ -887,17 +858,17 @@ def main(): # setActiveWindow does some corrective work needed thanks to # mutter-x11-frames. So retrieve the window just in case. window = cthulhu_state.activeWindow - script = _scriptManager.get_script(app, window) - _scriptManager.set_active_script(script, "Launching.") + script = cthulhuApp.scriptManager.get_script(app, window) + cthulhuApp.scriptManager.set_active_script(script, "Launching.") focusedObject = AXUtilities.get_focused_object(window) tokens = ["CTHULHU: Focused object is:", focusedObject] debug.printTokens(debug.LEVEL_INFO, tokens, True) if focusedObject: setLocusOfFocus(None, focusedObject) - script = _scriptManager.get_script( + script = cthulhuApp.scriptManager.get_script( AXObject.get_application(focusedObject), focusedObject) - _scriptManager.set_active_script(script, "Found focused object.") + cthulhuApp.scriptManager.set_active_script(script, "Found focused object.") try: msg = "CTHULHU: Starting ATSPI registry." @@ -919,14 +890,16 @@ class Cthulhu(GObject.Object): "setup-inputeventhandlers-completed": (GObject.SignalFlags.RUN_LAST, None, ()), # compat signal for register input event handlers "request-cthulhu-preferences": (GObject.SignalFlags.RUN_LAST, None, ()), "request-application-preferences": (GObject.SignalFlags.RUN_LAST, None, ()), + "active-script-changed": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,)), # New signal to indicate active script change } def __init__(self): GObject.Object.__init__(self) # add members self.resourceManager = resource_manager.ResourceManager(self) - self.eventManager = _eventManager - self.settingsManager = _settingsManager - self.scriptManager = _scriptManager + self.settingsManager = settings_manager.SettingsManager(self) # Directly instantiate + self.eventManager = event_manager.EventManager(self) # Directly instantiate + self.scriptManager = script_manager.ScriptManager(self) # Directly instantiate + self.logger = logger.Logger() # Directly instantiate self.signalManager = signal_manager.SignalManager(self) self.dynamicApiManager = dynamic_api_manager.DynamicApiManager(self) self.translationManager = translation_manager.TranslationManager(self) @@ -948,6 +921,8 @@ class Cthulhu(GObject.Object): return self.eventManager def getSettingsManager(self): return self.settingsManager + def getScriptManager(self): + return self.scriptManager def get_scriptManager(self): return self.scriptManager def getDebugManager(self): @@ -956,6 +931,12 @@ class Cthulhu(GObject.Object): return self.translationManager def getResourceManager(self): return self.resourceManager + def getLogger(self): # New getter for the logger + return self.logger + def addKeyGrab(self, binding): + return addKeyGrab(binding) + def removeKeyGrab(self, grab_id): + return removeKeyGrab(grab_id) def run(self, cacheValues=True): return main(cacheValues) def stop(self): @@ -965,10 +946,10 @@ class Cthulhu(GObject.Object): # should be removed step by step # use clean objects, getters and setters instead - self.getDynamicApiManager().registerAPI('Logger', _logger) - self.getDynamicApiManager().registerAPI('SettingsManager', settings_manager) - self.getDynamicApiManager().registerAPI('ScriptManager', script_manager) - self.getDynamicApiManager().registerAPI('EventManager', event_manager) + self.getDynamicApiManager().registerAPI('Logger', self.logger) # Use instance + self.getDynamicApiManager().registerAPI('SettingsManager', self.settingsManager) # Use instance + self.getDynamicApiManager().registerAPI('ScriptManager', self.scriptManager) # Use instance + self.getDynamicApiManager().registerAPI('EventManager', self.eventManager) # Use instance self.getDynamicApiManager().registerAPI('Speech', speech) self.getDynamicApiManager().registerAPI('Sound', sound) self.getDynamicApiManager().registerAPI('Braille', braille) diff --git a/src/cthulhu/cthulhu_bin.py.in b/src/cthulhu/cthulhu_bin.py.in index 98e01ad..0ffc6f7 100644 --- a/src/cthulhu/cthulhu_bin.py.in +++ b/src/cthulhu/cthulhu_bin.py.in @@ -326,7 +326,7 @@ def main(): debug.printMessage(debug.LEVEL_INFO, "INFO: Preparing to launch.", True) from cthulhu import cthulhu - manager = cthulhu.getSettingsManager() + manager = cthulhu.cthulhuApp.settingsManager if not manager: print(messages.CLI_SETTINGS_MANAGER_ERROR) diff --git a/src/cthulhu/cthulhu_gui_prefs.py b/src/cthulhu/cthulhu_gui_prefs.py index 4e2714f..1273a49 100644 --- a/src/cthulhu/cthulhu_gui_prefs.py +++ b/src/cthulhu/cthulhu_gui_prefs.py @@ -69,7 +69,7 @@ from . import sound_theme_manager from . import script_manager from .ax_object import AXObject -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager try: import louis @@ -202,7 +202,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): # in case the user played with the sliders. # try: - voices = _settingsManager.getSetting('voices') + voices = cthulhu.cthulhuApp.settingsManager.getSetting('voices') defaultVoice = voices[settings.DEFAULT_VOICE] except KeyError: defaultVoice = {} @@ -387,7 +387,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): # TODO - JD: Will this ever be the case?? self._isInitialSetup = \ - not os.path.exists(_settingsManager.getPrefsDir()) + not os.path.exists(cthulhu.cthulhuApp.settingsManager.getPrefsDir()) try: self._initPluginsPage() @@ -1013,7 +1013,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): keyBindingsDict = self.getKeyBindingsModelDict(self.keyBindingsModel) self.prefsDict.update(self.script.getPreferencesFromGUI()) self.prefsDict.update(self._get_plugin_preferences_from_gui()) - _settingsManager.saveSettings(self.script, + cthulhu.cthulhuApp.settingsManager.saveSettings(self.script, self.prefsDict, pronunciationDict, keyBindingsDict) @@ -1628,7 +1628,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): # # Where * = speechSystems, speechServers, speechLanguages, speechFamilies # - factories = _settingsManager.getSpeechServerFactories() + factories = cthulhu.cthulhuApp.settingsManager.getSpeechServerFactories() if len(factories) == 0 or not self.prefsDict.get('enableSpeech', True): self.workingFactories = [] self.speechSystemsChoice = None @@ -1677,7 +1677,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): [attrList, attrDict] = \ self.script.utilities.stringToKeysAndDict(setAttributes) [allAttrList, allAttrDict] = self.script.utilities.stringToKeysAndDict( - _settingsManager.getSetting('allTextAttributes')) + cthulhu.cthulhuApp.settingsManager.getSetting('allTextAttributes')) for i in range(0, len(attrList)): for path in range(0, len(allAttrList)): @@ -1714,7 +1714,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): [attrList, attrDict] = \ self.script.utilities.stringToKeysAndDict(setAttributes) [allAttrList, allAttrDict] = self.script.utilities.stringToKeysAndDict( - _settingsManager.getSetting('allTextAttributes')) + cthulhu.cthulhuApp.settingsManager.getSetting('allTextAttributes')) for i in range(0, len(attrList)): for path in range(0, len(allAttrList)): @@ -1876,7 +1876,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): # the known text attributes. # [allAttrList, allAttrDict] = self.script.utilities.stringToKeysAndDict( - _settingsManager.getSetting('allTextAttributes')) + cthulhu.cthulhuApp.settingsManager.getSetting('allTextAttributes')) for i in range(0, len(allAttrList)): thisIter = model.append() localizedKey = text_attribute_names.getTextAttributeName( @@ -1937,14 +1937,14 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): # self._setSpokenTextAttributes( self.getTextAttributesView, - _settingsManager.getSetting('enabledSpokenTextAttributes'), + cthulhu.cthulhuApp.settingsManager.getSetting('enabledSpokenTextAttributes'), True, True) # Check all the enabled (brailled) text attributes. # self._setBrailledTextAttributes( self.getTextAttributesView, - _settingsManager.getSetting('enabledBrailledTextAttributes'), + cthulhu.cthulhuApp.settingsManager.getSetting('enabledBrailledTextAttributes'), True) # Connect a handler for when the user changes columns within the @@ -1984,7 +1984,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): def pronunciationFocusChange(self, widget, event, isFocused): """Callback for the pronunciation tree's focus-{in,out}-event signal.""" - _settingsManager.setSetting('usePronunciationDictionary', not isFocused) + cthulhu.cthulhuApp.settingsManager.setSetting('usePronunciationDictionary', not isFocused) def pronunciationCursorChanged(self, widget): """Set the search column in the pronunciation dictionary tree view @@ -2023,7 +2023,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): # if not self.script.app: _profile = self.prefsDict.get('activeProfile')[1] - pronDict = _settingsManager.getPronunciations(_profile) + pronDict = cthulhu.cthulhuApp.settingsManager.getPronunciations(_profile) else: pronDict = pronunciation_dict.pronunciation_dict for pronKey in sorted(pronDict.keys()): @@ -2492,7 +2492,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): def __getAvailableProfiles(self): """Get available user profiles.""" - return _settingsManager.availableProfiles() + return cthulhu.cthulhuApp.settingsManager.availableProfiles() def _initAIState(self): """Initialize AI Assistant tab widgets with current settings.""" @@ -2841,7 +2841,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): # self._setSpokenTextAttributes( self.getTextAttributesView, - _settingsManager.getSetting('enabledSpokenTextAttributes'), + cthulhu.cthulhuApp.settingsManager.getSetting('enabledSpokenTextAttributes'), True, True) if self.script.app: @@ -3041,7 +3041,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): try: self.script.setupInputEventHandlers() keyBinds = keybindings.KeyBindings() - keyBinds = _settingsManager.overrideKeyBindings(self.script, keyBinds) + keyBinds = cthulhu.cthulhuApp.settingsManager.overrideKeyBindings(self.script, keyBinds) keyBind = keybindings.KeyBinding(None, None, None, None) treeModel = self.keyBindingsModel @@ -3103,7 +3103,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): mrKeyBindings = self.script.getMouseReviewer().get_bindings() acKeyBindings = self.script.getActionPresenter().get_bindings() - layout = _settingsManager.getSetting('keyboardLayout') + layout = cthulhu.cthulhuApp.settingsManager.getSetting('keyboardLayout') isDesktop = layout == settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP frKeyBindings = self.script.getFlatReviewPresenter().get_bindings(isDesktop) waiKeyBindings = self.script.getWhereAmIPresenter().get_bindings(isDesktop) @@ -3350,9 +3350,9 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): rate = widget.get_value() voiceType = self.get_widget("voiceTypesCombo").get_active() self._setRateForVoiceType(voiceType, rate) - voices = _settingsManager.getSetting('voices') + voices = cthulhu.cthulhuApp.settingsManager.getSetting('voices') voices.get(settings.DEFAULT_VOICE, {})[acss.ACSS.RATE] = rate - _settingsManager.setSetting('voices', voices) + cthulhu.cthulhuApp.settingsManager.setSetting('voices', voices) def pitchValueChanged(self, widget): """Signal handler for the "value_changed" signal for the pitchScale @@ -3367,9 +3367,9 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): pitch = widget.get_value() voiceType = self.get_widget("voiceTypesCombo").get_active() self._setPitchForVoiceType(voiceType, pitch) - voices = _settingsManager.getSetting('voices') + voices = cthulhu.cthulhuApp.settingsManager.getSetting('voices') voices.get(settings.DEFAULT_VOICE, {})[acss.ACSS.AVERAGE_PITCH] = pitch - _settingsManager.setSetting('voices', voices) + cthulhu.cthulhuApp.settingsManager.setSetting('voices', voices) def volumeValueChanged(self, widget): """Signal handler for the "value_changed" signal for the voiceScale @@ -3384,9 +3384,9 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): volume = widget.get_value() voiceType = self.get_widget("voiceTypesCombo").get_active() self._setVolumeForVoiceType(voiceType, volume) - voices = _settingsManager.getSetting('voices') + voices = cthulhu.cthulhuApp.settingsManager.getSetting('voices') voices.get(settings.DEFAULT_VOICE, {})[acss.ACSS.GAIN] = volume - _settingsManager.setSetting('voices', voices) + cthulhu.cthulhuApp.settingsManager.setSetting('voices', voices) def checkButtonToggled(self, widget): """Signal handler for "toggled" signal for basic GtkCheckButton @@ -4025,7 +4025,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): - widget: the component that generated the signal. """ - attributes = _settingsManager.getSetting('allTextAttributes') + attributes = cthulhu.cthulhuApp.settingsManager.getSetting('allTextAttributes') self._setSpokenTextAttributes( self.getTextAttributesView, attributes, True) self._setBrailledTextAttributes( @@ -4043,7 +4043,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): - widget: the component that generated the signal. """ - attributes = _settingsManager.getSetting('allTextAttributes') + attributes = cthulhu.cthulhuApp.settingsManager.getSetting('allTextAttributes') self._setSpokenTextAttributes( self.getTextAttributesView, attributes, False) self._setBrailledTextAttributes( @@ -4061,18 +4061,18 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): - widget: the component that generated the signal. """ - attributes = _settingsManager.getSetting('allTextAttributes') + attributes = cthulhu.cthulhuApp.settingsManager.getSetting('allTextAttributes') self._setSpokenTextAttributes( self.getTextAttributesView, attributes, False) self._setBrailledTextAttributes( self.getTextAttributesView, attributes, False) - attributes = _settingsManager.getSetting('enabledSpokenTextAttributes') + attributes = cthulhu.cthulhuApp.settingsManager.getSetting('enabledSpokenTextAttributes') self._setSpokenTextAttributes( self.getTextAttributesView, attributes, True) attributes = \ - _settingsManager.getSetting('enabledBrailledTextAttributes') + cthulhu.cthulhuApp.settingsManager.getSetting('enabledBrailledTextAttributes') self._setBrailledTextAttributes( self.getTextAttributesView, attributes, True) @@ -4177,7 +4177,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): # Restore the default rate/pitch/gain, # in case the user played with the sliders. # - voices = _settingsManager.getSetting('voices') + voices = cthulhu.cthulhuApp.settingsManager.getSetting('voices') defaultVoice = voices.get(settings.DEFAULT_VOICE) if defaultVoice is not None: defaultVoice[acss.ACSS.GAIN] = self.savedGain @@ -4231,7 +4231,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): self.prefsDict['profile'] = activeProfile self.prefsDict['activeProfile'] = activeProfile self.prefsDict['startingProfile'] = startingProfile - _settingsManager.setStartingProfile(startingProfile) + cthulhu.cthulhuApp.settingsManager.setStartingProfile(startingProfile) self._apply_plugin_changes() self.writeUserPreferences() @@ -4300,11 +4300,11 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): self.suspendEvents() - factory = _settingsManager.getSetting('speechServerFactory') + factory = cthulhu.cthulhuApp.settingsManager.getSetting('speechServerFactory') if factory: self._setSpeechSystemsChoice(factory) - server = _settingsManager.getSetting('speechServerInfo') + server = cthulhu.cthulhuApp.settingsManager.getSetting('speechServerInfo') if server: self._setSpeechServersChoice(server) @@ -4447,14 +4447,14 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): if not newProfile or newProfile == oldProfile: newProfile = newStartingProfile - _settingsManager.removeProfile(oldProfile[1]) + cthulhu.cthulhuApp.settingsManager.removeProfile(oldProfile[1]) self.loadProfile(newProfile) # Make sure nothing is referencing the removed profile anymore startingProfile = self.prefsDict.get('startingProfile') if not startingProfile or startingProfile == oldProfile: self.prefsDict['startingProfile'] = newStartingProfile - _settingsManager.setStartingProfile(newStartingProfile) + cthulhu.cthulhuApp.settingsManager.setStartingProfile(newStartingProfile) self.writeUserPreferences() dialog.destroy() @@ -4491,8 +4491,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): self.saveBasicSettings() self.prefsDict['activeProfile'] = profile - _settingsManager.setProfile(profile[1]) - self.prefsDict = _settingsManager.getGeneralSettings(profile[1]) + cthulhu.cthulhuApp.settingsManager.setProfile(profile[1]) + self.prefsDict = cthulhu.cthulhuApp.settingsManager.getGeneralSettings(profile[1]) cthulhu.loadUserSettings(skipReloadMessage=True) diff --git a/src/cthulhu/cthulhu_modifier_manager.py b/src/cthulhu/cthulhu_modifier_manager.py index 6ef08f2..86ea50f 100644 --- a/src/cthulhu/cthulhu_modifier_manager.py +++ b/src/cthulhu/cthulhu_modifier_manager.py @@ -218,8 +218,12 @@ class CthulhuModifierManager: debug.printMessage(debug.LEVEL_INFO, msg, True) GLib.timeout_add(1, toggle, keyboardEvent.modifiers, modifier) -_manager = CthulhuModifierManager() - +_manager = None def getManager(): - """Returns the CthulhuModifierManager singleton.""" + """Returns the Cthulhu Modifier Manager""" + + global _manager + if _manager is None: + _manager = CthulhuModifierManager() return _manager + diff --git a/src/cthulhu/date_and_time_presenter.py b/src/cthulhu/date_and_time_presenter.py index a4a2bf9..fd19c91 100644 --- a/src/cthulhu/date_and_time_presenter.py +++ b/src/cthulhu/date_and_time_presenter.py @@ -34,12 +34,13 @@ __license__ = "LGPL" import time +from . import cthulhu # Need access to cthulhuApp from . import cmdnames from . import input_event from . import keybindings from . import settings_manager -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class DateAndTimePresenter: """Provides commands to present the date and time.""" @@ -101,14 +102,14 @@ class DateAndTimePresenter: def present_time(self, script, event=None): """Presents the current time.""" - format = _settingsManager.getSetting('presentTimeFormat') + format = cthulhu.cthulhuApp.settingsManager.getSetting('presentTimeFormat') script.presentMessage(time.strftime(format, time.localtime())) return True def present_date(self, script, event=None): """Presents the current date.""" - format = _settingsManager.getSetting('presentDateFormat') + format = cthulhu.cthulhuApp.settingsManager.getSetting('presentDateFormat') script.presentMessage(time.strftime(format, time.localtime())) return True diff --git a/src/cthulhu/debug.py b/src/cthulhu/debug.py index e58d789..48f241e 100644 --- a/src/cthulhu/debug.py +++ b/src/cthulhu/debug.py @@ -318,21 +318,27 @@ def println(level, text="", timestamp=False, stack=False): if debugFile: try: debugFile.writelines([text, "\n"]) + debugFile.flush() except TypeError: text = "TypeError when trying to write text" debugFile.writelines([text, "\n"]) + debugFile.flush() except Exception: text = "Exception when trying to write text" debugFile.writelines([text, "\n"]) + debugFile.flush() else: try: sys.stderr.writelines([text, "\n"]) + sys.stderr.flush() except TypeError: text = "TypeError when trying to write text" sys.stderr.writelines([text, "\n"]) + sys.stderr.flush() except Exception: text = "Exception when trying to write text" sys.stderr.writelines([text, "\n"]) + sys.stderr.flush() def printResult(level, result=None): """Prints the return result, along with information about the diff --git a/src/cthulhu/event_manager.py b/src/cthulhu/event_manager.py index f9a6624..7258c0c 100644 --- a/src/cthulhu/event_manager.py +++ b/src/cthulhu/event_manager.py @@ -37,6 +37,7 @@ import queue import threading import time +from . import cthulhu from . import debug from . import input_event from . import input_event_manager @@ -46,15 +47,14 @@ from . import settings from .ax_object import AXObject from .ax_utilities import AXUtilities -_scriptManager = script_manager.get_manager() - class EventManager: EMBEDDED_OBJECT_CHARACTER = '\ufffc' - def __init__(self, asyncMode=True): + def __init__(self, app, asyncMode=True): debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Initializing', True) debug.printMessage(debug.LEVEL_INFO, f'EVENT MANAGER: Async Mode is {asyncMode}', True) + self.app = app self._asyncMode = asyncMode self._scriptListenerCounts = {} self._active = False @@ -98,8 +98,35 @@ class EventManager: debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Activating', True) self._activateKeyHandling() self._active = True + GLib.idle_add(self._sync_focus_on_startup) debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Activated', True) + def _sync_focus_on_startup(self): + """Initialize active window and focus when startup missed focus events.""" + + focus = cthulhu_state.locusOfFocus + if focus and not AXObject.is_dead(focus): + return False + + window = cthulhu_state.activeWindow + if not AXUtilities.can_be_active_window(window): + window = AXUtilities.find_active_window() + if window is not None: + tokens = ["EVENT MANAGER: Setting initial active window to", window] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + cthulhu.setActiveWindow(window, alsoSetLocusOfFocus=True, notifyScript=False) + + if window is None: + return False + + focused = AXUtilities.get_focused_object(window) + if focused is not None and focused != cthulhu_state.locusOfFocus: + cthulhu.setLocusOfFocus(None, focused, notifyScript=True, force=True) + elif cthulhu_state.locusOfFocus is None: + cthulhu.setLocusOfFocus(None, window, notifyScript=True, force=True) + + return False + def _activateKeyHandling(self): """Activates keyboard handling using InputEventManager with Atspi.Device.""" @@ -126,17 +153,6 @@ class EventManager: self._keyHandlingActive = False debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Keyboard handling deactivated', True) - def setKeyHandling(self, enable): - """Enables or disables keyboard handling. - - Arguments: - - enable: if True, activate keyboard handling; if False, deactivate. - """ - if enable: - self._activateKeyHandling() - else: - self._deactivateKeyHandling() - def deactivate(self): """Called when this event manager is deactivated.""" @@ -636,7 +652,7 @@ class EventManager: # To decrease the likelihood that the popup will be destroyed before we # have its contents. asyncMode = False - script = _scriptManager.get_script(AXObject.get_application(e.source), e.source) + script = cthulhu.cthulhuApp.scriptManager.get_script(AXObject.get_application(e.source), e.source) script.eventCache[e.type] = (e, time.time()) self._addToQueue(e, asyncMode) @@ -658,8 +674,8 @@ class EventManager: if not self._isNoFocus(): return False - defaultScript = _scriptManager.get_default_script() - _scriptManager.set_active_script(defaultScript, 'No focus') + defaultScript = cthulhu.cthulhuApp.scriptManager.get_default_script() + cthulhu.cthulhuApp.scriptManager.set_active_script(defaultScript, 'No focus') defaultScript.idleMessage() return False @@ -827,7 +843,7 @@ class EventManager: """Returns the script associated with event.""" if event.type.startswith("mouse:"): - return _scriptManager.get_script_for_mouse_button_event(event) + return cthulhu.cthulhuApp.scriptManager.get_script_for_mouse_button_event(event) script = None app = AXObject.get_application(event.source) @@ -856,7 +872,7 @@ class EventManager: tokens = ["EVENT MANAGER: Getting script for", app, "check:", check] debug.printTokens(debug.LEVEL_INFO, tokens, True) - script = _scriptManager.get_script(app, event.source, sanity_check=check) + script = cthulhu.cthulhuApp.scriptManager.get_script(app, event.source, sanity_check=check) tokens = ["EVENT MANAGER: Script is ", script] debug.printTokens(debug.LEVEL_INFO, tokens, True) return script @@ -1062,14 +1078,14 @@ class EventManager: if eType.startswith("object:children-changed:remove") \ and event.source == AXUtilities.get_desktop(): - _scriptManager.reclaim_scripts() + cthulhu.cthulhuApp.scriptManager.reclaim_scripts() return if eType.startswith("window:") and not eType.endswith("create"): - _scriptManager.reclaim_scripts() + cthulhu.cthulhuApp.scriptManager.reclaim_scripts() elif eType.startswith("object:state-changed:active") \ and AXUtilities.is_frame(event.source): - _scriptManager.reclaim_scripts() + cthulhu.cthulhuApp.scriptManager.reclaim_scripts() if AXObject.is_dead(event.source) or AXUtilities.is_defunct(event.source): tokens = ["EVENT MANAGER: Ignoring defunct object:", event.source] @@ -1081,7 +1097,7 @@ class EventManager: debug.printMessage(debug.LEVEL_INFO, msg, True) cthulhu_state.locusOfFocus = None cthulhu_state.activeWindow = None - _scriptManager.set_active_script(None, "Active window is dead or defunct") + cthulhu.cthulhuApp.scriptManager.set_active_script(None, "Active window is dead or defunct") return if AXUtilities.is_iconified(event.source): @@ -1125,7 +1141,7 @@ class EventManager: if setNewActiveScript: try: - _scriptManager.set_active_script(script, reason) + cthulhu.cthulhuApp.scriptManager.set_active_script(script, reason) except Exception as error: tokens = ["EVENT MANAGER: Exception setting active script for", event.source, ":", error] @@ -1165,7 +1181,10 @@ class EventManager: else: return False -_manager = EventManager() +_manager = None def getManager(): + global _manager + if _manager is None: + _manager = cthulhu.cthulhuApp.eventManager return _manager diff --git a/src/cthulhu/flat_review_presenter.py b/src/cthulhu/flat_review_presenter.py index e1b3338..ebcd238 100644 --- a/src/cthulhu/flat_review_presenter.py +++ b/src/cthulhu/flat_review_presenter.py @@ -50,7 +50,7 @@ from . import cthulhu_state from . import settings_manager from . import settings -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class FlatReviewPresenter: """Provides access to on-screen objects via flat-review.""" @@ -58,7 +58,7 @@ class FlatReviewPresenter: def __init__(self): self._context = None self._current_contents = "" - self._restrict = _settingsManager.getSetting("flatReviewIsRestricted") + self._restrict = cthulhu.cthulhuApp.settingsManager.getSetting("flatReviewIsRestricted") self._handlers = self._setup_handlers() self._desktop_bindings = self._setup_desktop_bindings() self._laptop_bindings = self._setup_laptop_bindings() @@ -731,7 +731,7 @@ class FlatReviewPresenter: if event is None: return - if _settingsManager.getSetting('speechVerbosityLevel') != settings.VERBOSITY_LEVEL_BRIEF: + if cthulhu.cthulhuApp.settingsManager.getSetting('speechVerbosityLevel') != settings.VERBOSITY_LEVEL_BRIEF: script.presentMessage(messages.FLAT_REVIEW_START) self._item_presentation(script, event, script.targetCursorCell) @@ -752,7 +752,7 @@ class FlatReviewPresenter: if event is None or script is None: return - if _settingsManager.getSetting('speechVerbosityLevel') != settings.VERBOSITY_LEVEL_BRIEF: + if cthulhu.cthulhuApp.settingsManager.getSetting('speechVerbosityLevel') != settings.VERBOSITY_LEVEL_BRIEF: script.presentMessage(messages.FLAT_REVIEW_STOP) script.updateBraille(cthulhu_state.locusOfFocus) @@ -1039,7 +1039,7 @@ class FlatReviewPresenter: """ Toggles the restricting of flat review to the current object. """ self._restrict = not self._restrict - _settingsManager.setSetting("flatReviewIsRestricted", self._restrict) + cthulhu.cthulhuApp.settingsManager.setSetting("flatReviewIsRestricted", self._restrict) if self._restrict: script.presentMessage(messages.FLAT_REVIEW_RESTRICTED) diff --git a/src/cthulhu/focus_manager.py b/src/cthulhu/focus_manager.py index 35a0d60..02aed9a 100644 --- a/src/cthulhu/focus_manager.py +++ b/src/cthulhu/focus_manager.py @@ -74,7 +74,8 @@ SAY_ALL = "say-all" class FocusManager: """Manages the focused object, window, etc.""" - def __init__(self) -> None: + def __init__(self, app) -> None: # Added app argument + self.app = app # Store app instance self._window: Optional[Atspi.Accessible] = cthulhu_state.activeWindow self._focus: Optional[Atspi.Accessible] = cthulhu_state.locusOfFocus self._object_of_interest: Optional[Atspi.Accessible] = cthulhu_state.objOfInterest @@ -286,11 +287,11 @@ class FocusManager: _get_ax_utilities().save_object_info_for_events(obj) # TODO - JD: Consider always updating the active script here. - script = script_manager.get_manager().get_active_script() + script = self.app.scriptManager.get_active_script() if event and (script and not script.app): app = _get_ax_utilities().get_application(event.source) - script = script_manager.get_manager().get_script(app, event.source) - script_manager.get_manager().set_active_script(script, "Setting locus of focus") + script = self.app.scriptManager.get_script(app, event.source) + self.app.scriptManager.set_active_script(script, "Setting locus of focus") old_focus = self._focus if AXObject.is_dead(old_focus): @@ -383,8 +384,8 @@ class FocusManager: self.set_locus_of_focus(None, self._window, notify_script=True) app = _get_ax_utilities().get_application(self._focus) - script = script_manager.get_manager().get_script(app, self._focus) - script_manager.get_manager().set_active_script(script, "Setting active window") + script = self.app.scriptManager.get_script(app, self._focus) + self.app.scriptManager.set_active_script(script, "Setting active window") @dbus_service.command def toggle_presentation_mode( @@ -434,7 +435,7 @@ class FocusManager: def get_in_layout_mode(self) -> bool: """Returns True if layout mode (as opposed to object mode) is active (web content only).""" - if script := script_manager.get_manager().get_active_script(): + if script := self.app.scriptManager.get_active_script(): return script.in_layout_mode() return False @@ -442,7 +443,7 @@ class FocusManager: def get_in_focus_mode(self) -> bool: """Returns True if focus mode is active (web content only).""" - if script := script_manager.get_manager().get_active_script(): + if script := self.app.scriptManager.get_active_script(): return script.in_focus_mode() return False @@ -450,7 +451,7 @@ class FocusManager: def get_focus_mode_is_sticky(self) -> bool: """Returns True if focus mode is active and 'sticky' (web content only).""" - if script := script_manager.get_manager().get_active_script(): + if script := self.app.scriptManager.get_active_script(): return script.focus_mode_is_sticky() return False @@ -458,12 +459,17 @@ class FocusManager: def get_browse_mode_is_sticky(self) -> bool: """Returns True if browse mode is active and 'sticky' (web content only).""" - if script := script_manager.get_manager().get_active_script(): + if script := self.app.scriptManager.get_active_script(): return script.browse_mode_is_sticky() return False -_manager: FocusManager = FocusManager() +_manager = None +def get_manager(): + """Returns the Focus Manager""" -def get_manager() -> FocusManager: - """Returns the focus manager singleton.""" + global _manager + if _manager is None: + from . import cthulhu + _manager = FocusManager(cthulhu.cthulhuApp) return _manager + diff --git a/src/cthulhu/generator.py b/src/cthulhu/generator.py index 9c2a2f5..b6b4f13 100644 --- a/src/cthulhu/generator.py +++ b/src/cthulhu/generator.py @@ -80,7 +80,15 @@ def _formatExceptionInfo(maxTBlevel=5): # METHOD_PREFIX = "_generate" -_settingsManager = settings_manager.getManager() +# Lazy initialization to avoid circular imports +_settingsManager = None + +def _ensureSettingsManager(): + """Ensure settings manager is initialized.""" + global _settingsManager + if _settingsManager is None: + from . import cthulhu + _settingsManager = cthulhu.cthulhu.cthulhuApp.settingsManager class Generator: """Takes accessible objects and generates a presentation for those @@ -1237,7 +1245,8 @@ class Generator: return [] def _getProgressBarUpdateInterval(self): - return int(_settingsManager.getSetting('progressBarUpdateInterval')) + _ensureSettingsManager() + return int(cthulhu.cthulhuApp.settingsManager.getSetting('progressBarUpdateInterval')) def _shouldPresentProgressBarUpdate(self, obj, **args): percent = self._script.utilities.getValueAsPercent(obj) diff --git a/src/cthulhu/input_event.py b/src/cthulhu/input_event.py index ece3cdf..f4a0c13 100644 --- a/src/cthulhu/input_event.py +++ b/src/cthulhu/input_event.py @@ -81,21 +81,8 @@ class InputEvent: self._clickCount = count -def _getXkbStickyKeysState(): - from subprocess import check_output - - try: - output = check_output(['xkbset', 'q']) - for line in output.decode('ASCII', errors='ignore').split('\n'): - if line.startswith('Sticky-Keys = '): - return line.endswith('On') - except Exception: - pass - return False - class KeyboardEvent(InputEvent): - - stickyKeys = _getXkbStickyKeysState() + stickyKeys = False duplicateCount = 0 cthulhuModifierPressed = False @@ -791,25 +778,6 @@ class KeyboardEvent(InputEvent): return self._handler - def _getUserHandler(self): - # TODO - JD: This should go away once plugin support is in place. - try: - bindings = settings.keyBindingsMap.get(self._script.__module__) - except Exception: - bindings = None - if not bindings: - try: - bindings = settings.keyBindingsMap.get("default") - except Exception: - bindings = None - - try: - handler = bindings.getInputHandler(self) - except Exception: - handler = None - - return handler - def shouldConsume(self): """Returns True if this event should be consumed.""" @@ -820,9 +788,16 @@ class KeyboardEvent(InputEvent): if not self.timestamp: return False, 'No timestamp' + globalHandlerUsed = False if not self._script: - debug.printMessage(debug.LEVEL_INFO, "shouldConsume: No active script", True) - return False, 'No active script when received' + globalHandler = self._getGlobalHandler() + if globalHandler: + self._handler = globalHandler + self._script = script_manager.get_manager().get_default_script() + globalHandlerUsed = True + else: + debug.printMessage(debug.LEVEL_INFO, "shouldConsume: No active script", True) + return False, 'No active script when received' debug.printMessage(debug.LEVEL_INFO, f"shouldConsume: Active script={self._script.__class__.__name__}", True) @@ -835,8 +810,13 @@ class KeyboardEvent(InputEvent): if cthulhu_state.bypassNextCommand: return False, 'Bypass next command' - self._handler = self._getUserHandler() \ - or self._script.keyBindings.getInputHandler(self) + if not self._handler: + self._handler = self._script.keyBindings.getInputHandler(self) + if not self._handler: + globalHandler = self._getGlobalHandler() + if globalHandler: + self._handler = globalHandler + globalHandlerUsed = True if self._handler: debug.printMessage(debug.LEVEL_INFO, f"shouldConsume: Handler found: {self._handler.description}", True) @@ -847,6 +827,8 @@ class KeyboardEvent(InputEvent): # because that method is updating state, even in instances where there # is no handler. scriptConsumes = self._script.consumesKeyboardEvent(self) + if globalHandlerUsed: + scriptConsumes = True debug.printMessage(debug.LEVEL_INFO, f"shouldConsume: scriptConsumes={scriptConsumes}", True) if self._isReleaseForLastNonModifierKeyEvent(): @@ -866,6 +848,18 @@ class KeyboardEvent(InputEvent): return scriptConsumes, 'Script indication' + def _getGlobalHandler(self): + try: + plugin_manager = cthulhu.cthulhuApp.getPluginSystemManager() + except Exception: + return None + if not plugin_manager: + return None + global_bindings = plugin_manager.get_global_keybindings() + if not global_bindings: + return None + return global_bindings.getInputHandler(self) + def didConsume(self): """Returns True if this event was consumed.""" diff --git a/src/cthulhu/input_event_manager.py b/src/cthulhu/input_event_manager.py index 5d4d9fe..3ce833f 100644 --- a/src/cthulhu/input_event_manager.py +++ b/src/cthulhu/input_event_manager.py @@ -44,12 +44,6 @@ from typing import TYPE_CHECKING, Optional import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi -try: - gi.require_version("Wnck", "3.0") - from gi.repository import Wnck - _wnck_available = True -except Exception: - _wnck_available = False from . import debug from . import focus_manager @@ -59,7 +53,6 @@ from . import settings from . import cthulhu_state from .ax_object import AXObject from .ax_utilities import AXUtilities -from .ax_utilities_application import AXUtilitiesApplication if TYPE_CHECKING: from . import keybindings @@ -76,35 +69,6 @@ class InputEventManager: self._grabbed_bindings: dict[int, keybindings.KeyBinding] = {} self._paused: bool = False - def _active_window_has_accessible_app(self) -> Optional[bool]: - """Returns True if the WM active window maps to an AT-SPI app.""" - - if not _wnck_available: - return None - - screen = Wnck.Screen.get_default() - if not screen: - return None - - try: - screen.force_update() - active_window = screen.get_active_window() - except Exception: - return None - - if not active_window: - return None - - try: - pid = active_window.get_pid() - except Exception: - return None - - if pid <= 0: - return None - - return AXUtilitiesApplication.get_application_with_pid(pid) is not None - def start_key_watcher(self) -> None: """Starts the watcher for keyboard input events.""" @@ -136,24 +100,29 @@ class InputEventManager: msg = f"INPUT EVENT MANAGER: {grab_id} for: {binding}" debug.print_message(debug.LEVEL_INFO, msg, True) + def _get_key_definitions(self, binding: keybindings.KeyBinding) -> list[Atspi.KeyDefinition]: + if hasattr(binding, "key_definitions"): + return list(binding.key_definitions()) + if hasattr(binding, "keyDefs"): + return list(binding.keyDefs()) + return [] + def add_grabs_for_keybinding(self, binding: keybindings.KeyBinding) -> list[int]: """Adds grabs for binding if it is enabled, returns grab IDs.""" - if not (binding.is_enabled() and binding.is_bound()): - return [] - - if binding.has_grabs(): - tokens = ["INPUT EVENT MANAGER:", binding, "already has grabs."] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) - return [] - if self._device is None: tokens = ["INPUT EVENT MANAGER: No device to add grab for", binding] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return [] grab_ids = [] - for kd in binding.key_definitions(): + key_definitions = self._get_key_definitions(binding) + if not key_definitions: + tokens = ["INPUT EVENT MANAGER: No key definitions for", binding] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return [] + + for kd in key_definitions: grab_id = self._device.add_key_grab(kd, None) # When we have double/triple-click bindings, the single-click binding will be # registered first, and subsequent attempts to register what is externally the @@ -174,7 +143,12 @@ class InputEventManager: debug.print_tokens(debug.LEVEL_INFO, tokens, True) return - grab_ids = binding.get_grab_ids() + grab_ids = None + if hasattr(binding, "get_grab_ids"): + grab_ids = binding.get_grab_ids() + elif hasattr(binding, "_grab_ids"): + grab_ids = list(binding._grab_ids) + if not grab_ids: tokens = ["INPUT EVENT MANAGER:", binding, "doesn't have grabs to remove."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) @@ -186,6 +160,25 @@ class InputEventManager: if removed is None: msg = f"INPUT EVENT MANAGER: No key binding for grab id {grab_id}" debug.print_message(debug.LEVEL_INFO, msg, True) + if hasattr(binding, "_grab_ids") and grab_id in binding._grab_ids: + binding._grab_ids.remove(grab_id) + if hasattr(binding, "_grab_ids") and not binding._grab_ids: + delattr(binding, "_grab_ids") + + def remove_grab_by_id(self, grab_id: int) -> None: + """Removes a grab by id.""" + + if self._device is None: + msg = f"INPUT EVENT MANAGER: No device to remove grab id {grab_id}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return + + self._device.remove_key_grab(grab_id) + binding = self._grabbed_bindings.pop(grab_id, None) + if binding and hasattr(binding, "_grab_ids") and grab_id in binding._grab_ids: + binding._grab_ids.remove(grab_id) + if not binding._grab_ids: + delattr(binding, "_grab_ids") def map_keysym_to_modifier(self, keysym: int) -> int: """Maps keysym as a modifier, returns the newly-mapped modifier.""" @@ -198,6 +191,17 @@ class InputEventManager: self._mapped_keysyms.append(keysym) return self._device.map_keysym_modifier(keysym) + def map_keycode_to_modifier(self, keycode: int) -> int: + """Maps keycode as a modifier, returns the newly-mapped modifier.""" + + if self._device is None: + msg = f"INPUT EVENT MANAGER: No device to map keycode {keycode} to modifier" + debug.print_message(debug.LEVEL_INFO, msg, True) + return 0 + + self._mapped_keycodes.append(keycode) + return self._device.map_modifier(keycode) + def unmap_all_modifiers(self) -> None: """Unmaps all previously mapped modifiers.""" @@ -317,12 +321,6 @@ class InputEventManager: manager = focus_manager.get_manager() if pressed: - has_accessible_app = self._active_window_has_accessible_app() - if has_accessible_app is False and manager.get_active_window() is not None: - msg = "INPUT EVENT MANAGER: Active window has no AT-SPI app. Clearing active window." - debug.print_message(debug.LEVEL_INFO, msg, True) - manager.set_active_window(None, notify_script=True) - window = manager.get_active_window() if not AXUtilities.can_be_active_window(window): new_window = AXUtilities.find_active_window() @@ -335,6 +333,8 @@ class InputEventManager: # One example: Brave's popup menus live in frames which lack the active state. tokens = ["WARNING:", window, "cannot be active window. No alternative found."] debug.print_tokens(debug.LEVEL_WARNING, tokens, True) + window = None + manager.set_active_window(None, notify_script=True) event.set_window(window) event.set_object(manager.get_locus_of_focus()) event.set_script(script_manager.get_manager().get_active_script()) diff --git a/src/cthulhu/learn_mode_presenter.py b/src/cthulhu/learn_mode_presenter.py index 4e28902..be72b1e 100644 --- a/src/cthulhu/learn_mode_presenter.py +++ b/src/cthulhu/learn_mode_presenter.py @@ -60,7 +60,8 @@ from .ax_object import AXObject class LearnModePresenter: """Provides implementation of learn mode""" - def __init__(self): + def __init__(self, app): + self.app = app self._handlers = self._setup_handlers() self._bindings = self._setup_bindings() self._is_active = False @@ -215,7 +216,7 @@ class LearnModePresenter: if event is None: event = cthulhu_state.lastNonModifierKeyEvent - layout = settings_manager.getManager().getSetting("keyboardLayout") + layout = self.app.getSettingsManager().getSetting("keyboardLayout") is_desktop = layout == settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP items = 0 @@ -393,12 +394,14 @@ class CommandListGUI: time_stamp = Gtk.get_current_event_time() self._gui.present_with_time(time_stamp) - _presenter = None def getPresenter(): """Returns the Learn Mode Presenter""" - + global _presenter if _presenter is None: - _presenter = LearnModePresenter() + from . import cthulhu + _presenter = LearnModePresenter(cthulhu.cthulhuApp) return _presenter + + diff --git a/src/cthulhu/liveregions.py b/src/cthulhu/liveregions.py index 6caa161..00cbba1 100644 --- a/src/cthulhu/liveregions.py +++ b/src/cthulhu/liveregions.py @@ -32,6 +32,7 @@ import copy import time from gi.repository import GLib +from . import cthulhu # Need access to cthulhuApp from . import cmdnames from . import chnames from . import debug @@ -45,7 +46,7 @@ from .ax_object import AXObject from .ax_text import AXText from .ax_utilities_relation import AXUtilitiesRelation -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager # define 'live' property types LIVE_OFF = -1 @@ -310,7 +311,7 @@ class LiveRegionManager: def advancePoliteness(self, script, inputEvent): """Advance the politeness level of the given object""" - if not _settingsManager.getSetting('inferLiveRegions'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('inferLiveRegions'): self._script.presentMessage(messages.LIVE_REGIONS_OFF) return @@ -353,7 +354,7 @@ class LiveRegionManager: """Speak the given number cached message""" msgnum = int(inputEvent.event_string[1:]) - if not _settingsManager.getSetting('inferLiveRegions'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('inferLiveRegions'): self._script.presentMessage(messages.LIVE_REGIONS_OFF) return @@ -366,7 +367,7 @@ class LiveRegionManager: """User toggle to set all live regions to LIVE_OFF or back to their original politeness.""" - if not _settingsManager.getSetting('inferLiveRegions'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('inferLiveRegions'): self._script.presentMessage(messages.LIVE_REGIONS_OFF) return @@ -599,10 +600,10 @@ class LiveRegionManager: obj = AXObject.get_parent(obj) def toggleMonitoring(self, script, inputEvent): - if not _settingsManager.getSetting('inferLiveRegions'): - _settingsManager.setSetting('inferLiveRegions', True) + if not cthulhu.cthulhuApp.settingsManager.getSetting('inferLiveRegions'): + cthulhu.cthulhuApp.settingsManager.setSetting('inferLiveRegions', True) self._script.presentMessage(messages.LIVE_REGIONS_MONITORING_ON) else: - _settingsManager.setSetting('inferLiveRegions', False) + cthulhu.cthulhuApp.settingsManager.setSetting('inferLiveRegions', False) self.flushMessages() self._script.presentMessage(messages.LIVE_REGIONS_MONITORING_OFF) diff --git a/src/cthulhu/logger.py b/src/cthulhu/logger.py index 8de4614..2b85eff 100644 --- a/src/cthulhu/logger.py +++ b/src/cthulhu/logger.py @@ -71,7 +71,3 @@ class Logger: stream = self._logs.get(name) stream.close() -_logger = Logger() - -def getLogger(): - return _logger diff --git a/src/cthulhu/mouse_review.py b/src/cthulhu/mouse_review.py index b2e2031..a6ed955 100644 --- a/src/cthulhu/mouse_review.py +++ b/src/cthulhu/mouse_review.py @@ -60,8 +60,7 @@ from .ax_object import AXObject from .ax_text import AXText from .ax_utilities import AXUtilities -_scriptManager = script_manager.get_manager() -_settingsManager = settings_manager.getManager() +# Removed global cthulhu.cthulhuApp.scriptManager and cthulhu.cthulhuApp.settingsManager class _StringContext: """The textual information associated with an _ItemContext.""" @@ -341,8 +340,9 @@ class _ItemContext: class MouseReviewer: """Main class for the mouse-review feature.""" - def __init__(self): - self._active = _settingsManager.getSetting("enableMouseReview") + def __init__(self, app): + self.app = app + self._active = self.app.getSettingsManager().getSetting("enableMouseReview") self._currentMouseOver = _ItemContext() self._pointer = None self._workspace = None @@ -432,7 +432,7 @@ class MouseReviewer: script = None frame = None if obj: - script = _scriptManager.get_script(AXObject.get_application(obj), obj) + script = self.app.getScriptManager().get_script(AXObject.get_application(obj), obj) if script: frame = script.utilities.topLevelObject(obj) self._currentMouseOver = _ItemContext(obj=obj, frame=frame, script=script) @@ -497,7 +497,7 @@ class MouseReviewer: return self._active = not self._active - _settingsManager.setSetting("enableMouseReview", self._active) + self.app.getSettingsManager().setSetting("enableMouseReview", self._active) if not self._active: self.deactivate() @@ -621,7 +621,7 @@ class MouseReviewer: if not window: return - script = _scriptManager.get_script(AXObject.get_application(window)) + script = self.app.getScriptManager().get_script(AXObject.get_application(window)) if not script: return @@ -643,7 +643,7 @@ class MouseReviewer: tokens = [f"MOUSE REVIEW: Object at ({pX}, {pY}) is", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) - script = _scriptManager.get_script(AXObject.get_application(window), obj) + script = self.app.getScriptManager().get_script(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): tokens = ["MOUSE REVIEW:", obj, "believed to be under", menu] @@ -695,10 +695,14 @@ class MouseReviewer: msg += f"^^^^^ PROCESS OBJECT EVENT {event.type} ^^^^^\n" debug.printMessage(debug.LEVEL_INFO, msg, False) - _reviewer = None def getReviewer(): + """Returns the Mouse Reviewer""" + global _reviewer if _reviewer is None: - _reviewer = MouseReviewer() + from . import cthulhu + _reviewer = MouseReviewer(cthulhu.cthulhuApp) return _reviewer + + diff --git a/src/cthulhu/plugin.py b/src/cthulhu/plugin.py index 8063233..a9e0eca 100644 --- a/src/cthulhu/plugin.py +++ b/src/cthulhu/plugin.py @@ -11,28 +11,11 @@ import os import logging -# Import pluggy for hook specifications -try: - import pluggy - cthulhu_hookimpl = pluggy.HookimplMarker("cthulhu") - PLUGGY_AVAILABLE = True - logging.getLogger(__name__).info("Successfully imported pluggy") -except ImportError: - # Fallback if pluggy is not available - def cthulhu_hookimpl(func=None, **kwargs): - """Fallback decorator when pluggy is not available. +import pluggy + +# Import pluggy for hook specifications +cthulhu_hookimpl = pluggy.HookimplMarker("cthulhu") - This is a no-op decorator that returns the original function. - It allows the code to continue working without pluggy, though - plugins will be disabled. - """ - if func is None: - return lambda f: f - return func - PLUGGY_AVAILABLE = False - logging.getLogger(__name__).warning("Pluggy not available, plugins will be disabled") - import traceback - logging.getLogger(__name__).debug(traceback.format_exc()) logger = logging.getLogger(__name__) @@ -95,7 +78,7 @@ class Plugin: """Return a dict of plugin preferences from the GUI.""" return {} - def registerGestureByString(self, function, name, gestureString, learnModeEnabled=True): + def registerGestureByString(self, function, name, gestureString, learnModeEnabled=True, globalBinding=True): """Register a gesture by string.""" if self.app: api_helper = self.app.getAPIHelper() @@ -107,6 +90,7 @@ class Plugin: 'default', 'cthulhu', learnModeEnabled, + globalBinding=globalBinding, contextName=self.module_name ) diff --git a/src/cthulhu/plugin_system_manager.py b/src/cthulhu/plugin_system_manager.py index affa873..f9f0054 100644 --- a/src/cthulhu/plugin_system_manager.py +++ b/src/cthulhu/plugin_system_manager.py @@ -19,15 +19,9 @@ import shutil import subprocess from enum import IntEnum +import pluggy from . import dbus_service - -# Import pluggy if available -try: - import pluggy - PLUGGY_AVAILABLE = True -except ImportError: - PLUGGY_AVAILABLE = False - logging.getLogger(__name__).info("Pluggy not available, plugins will be disabled") +from . import keybindings # Added import # Set to True for more detailed plugin loading debug info PLUGIN_DEBUG = True @@ -123,34 +117,33 @@ class PluginSystemManager: _manager = self # Initialize plugin manager - if PLUGGY_AVAILABLE: - logger.info("Pluggy is available, setting up plugin manager") - self.plugin_manager = pluggy.PluginManager("cthulhu") + logger.info("Setting up plugin manager") + self.plugin_manager = pluggy.PluginManager("cthulhu") - # Define hook specifications - hook_spec = pluggy.HookspecMarker("cthulhu") + # Define hook specifications + hook_spec = pluggy.HookspecMarker("cthulhu") - class CthulhuHookSpecs: - @hook_spec - def activate(self, plugin=None): - """Called when the plugin is activated.""" - pass + class CthulhuHookSpecs: + @hook_spec + def activate(self, plugin=None): + """Called when the plugin is activated.""" + pass - @hook_spec - def deactivate(self, plugin=None): - """Called when the plugin is deactivated.""" - pass + @hook_spec + def deactivate(self, plugin=None): + """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 + logger.info("Adding hook specifications to plugin manager") + self.plugin_manager.add_hookspecs(CthulhuHookSpecs) # Plugin storage self._plugins = {} # module_name -> PluginInfo self._plugin_name_index = {} # canonical_name -> [module_name] self._active_plugins = [] + self._plugin_keybindings = {} # plugin_name -> [KeyBinding] + self._global_keybindings = keybindings.KeyBindings() + self._global_bindings = [] # Create plugin directories self._setup_plugin_dirs() @@ -158,186 +151,113 @@ class PluginSystemManager: # 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()}") + + # Connect to active-script-changed signal + self.app.getSignalManager().connectSignal( + 'active-script-changed', self._on_active_script_changed, 'default') - def register_plugin_global_keybindings(self, plugin): - """Register a plugin's keybindings with all scripts.""" - if not hasattr(plugin, 'get_bindings'): - return - - try: - bindings = plugin.get_bindings() - if not bindings or not bindings.keyBindings: - return - - logger.info(f"Registering global keybindings for plugin: {plugin.name}") - - # First register with the active script - from . import cthulhu_state - if cthulhu_state.activeScript: - active_script = cthulhu_state.activeScript - for binding in bindings.keyBindings: - active_script.getKeyBindings().add(binding) - from . import cthulhu - grab_ids = cthulhu.addKeyGrab(binding) - if grab_ids: - binding._grab_ids = grab_ids - - # Store these bindings for future script changes - plugin_name = plugin.name or plugin.module_name - if not hasattr(self, '_plugin_global_bindings'): - self._plugin_global_bindings = {} - self._plugin_global_bindings[plugin_name] = bindings - - # Connect to script changes to ensure bindings work with all scripts - if not hasattr(self, '_connected_to_script_changes'): - signal_manager = self.app.getSignalManager() - if signal_manager: - signal_manager.connectSignal('load-setting-completed', self._on_settings_changed, None) - self._connected_to_script_changes = True - except Exception as e: - logger.error(f"Error registering global keybindings for plugin {plugin.name}: {e}") - import traceback - logger.error(traceback.format_exc()) - - def refresh_active_script_keybindings(self): - """Force active script to refresh its keybindings to include plugin bindings.""" - from . import cthulhu_state - if cthulhu_state.activeScript: - active_script = cthulhu_state.activeScript - with open('/tmp/plugin_registration.log', 'a') as f: - f.write(f"=== refresh_active_script_keybindings() CALLED ===\n") - f.write(f"Active script: {active_script.name}\n") - - # Force the script to recreate its keybindings to include plugin bindings - old_keybindings = active_script.keyBindings - active_script.keyBindings = active_script.getKeyBindings() - - with open('/tmp/plugin_registration.log', 'a') as f: - f.write(f"Keybindings refreshed: old={len(old_keybindings.keyBindings) if old_keybindings else 0}, new={len(active_script.keyBindings.keyBindings)}\n") - - def register_plugin_keybindings_with_active_script(self): - """Register all plugin keybindings with the active script.""" - - logger.info("=== register_plugin_keybindings_with_active_script() CALLED ===") - with open('/tmp/plugin_registration.log', 'a') as f: - f.write("=== register_plugin_keybindings_with_active_script() CALLED ===\n") - - if not PLUGGY_AVAILABLE: - logger.warning("PLUGGY not available") - with open('/tmp/plugin_registration.log', 'a') as f: - f.write("ERROR: PLUGGY not available\n") - return - - from . import cthulhu_state - if not cthulhu_state.activeScript: - logger.warning("No active script available to register plugin keybindings") - with open('/tmp/plugin_registration.log', 'a') as f: - f.write("ERROR: No active script available\n") - return - - active_script = cthulhu_state.activeScript - logger.info(f"Registering plugin keybindings with active script: {active_script}") - with open('/tmp/plugin_registration.log', 'a') as f: - f.write(f"Active script: {active_script}\n") - - # First, register keybindings from APIHelper's stored bindings - # This is where plugin keybindings actually get stored - from . import cthulhu - api_helper = cthulhu.cthulhuApp.getAPIHelper() - if api_helper and hasattr(api_helper, '_gestureBindings'): - logger.info("=== FOUND APIHelper with _gestureBindings ===") - with open('/tmp/plugin_registration.log', 'a') as f: - f.write("=== Registering stored gesture bindings from APIHelper ===\n") - f.write(f"Total contexts: {len(api_helper._gestureBindings)}\n") - - for context_name, bindings_list in api_helper._gestureBindings.items(): - logger.info(f"Processing context '{context_name}' with {len(bindings_list)} bindings") - with open('/tmp/plugin_registration.log', 'a') as f: - f.write(f"Context '{context_name}': {len(bindings_list)} bindings\n") - - for binding in bindings_list: - logger.info(f"Adding stored binding: {binding.keysymstring} with modifiers {binding.modifiers}") - with open('/tmp/plugin_registration.log', 'a') as f: - f.write(f" Binding: {binding.keysymstring} modifiers={binding.modifiers} desc={binding.handler.description}\n") - - # Check if binding already exists to avoid duplicates - if not active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"): - active_script.getKeyBindings().add(binding) - with open('/tmp/plugin_registration.log', 'a') as f: - f.write(f" ADDED to active script!\n") - - # Force recalculation of keycode if it wasn't set when device was None - if not binding.keycode and binding.keysymstring: - from . import keybindings - binding.keycode = keybindings.getKeycode(binding.keysymstring) - # Register key grab at system level - this was missing! - grab_ids = cthulhu.addKeyGrab(binding) - if grab_ids: - binding._grab_ids = grab_ids - else: - logger.warning(f"Failed to create key grab for {binding.keysymstring} - device may not be available") - else: - logger.info(f"Binding already exists: {binding.keysymstring}") - with open('/tmp/plugin_registration.log', 'a') as f: - f.write(f" Already exists, skipped\n") - else: - logger.warning("=== NO APIHelper or no _gestureBindings found ===") - with open('/tmp/plugin_registration.log', 'a') as f: - f.write("ERROR: No APIHelper or no _gestureBindings found!\n") - if api_helper: - f.write(f"APIHelper exists but _gestureBindings: {hasattr(api_helper, '_gestureBindings')}\n") + def add_keybinding(self, plugin_name, binding, global_binding=False): + """Add a keybinding associated with a specific plugin.""" + if plugin_name not in self._plugin_keybindings: + self._plugin_keybindings[plugin_name] = [] + self._plugin_keybindings[plugin_name].append(binding) + logger.debug(f"Added keybinding '{binding.asString()}' for plugin '{plugin_name}'") + if global_binding: + if binding not in self._global_bindings: + self._global_bindings.append(binding) + self._global_keybindings.add(binding) + grab_ids = self.app.addKeyGrab(binding) + if grab_ids: + binding._global_grab_ids = grab_ids else: - f.write("No APIHelper found!\n") + logger.warning(f"Failed to create global key grab for {binding.keysymstring}") - # Also check the old method for any plugins that use get_bindings() - for pluginInfo in self._plugins.values(): - if not pluginInfo.loaded or not pluginInfo.instance: - continue + def _activate_plugin_keybindings(self, plugin_info): + """Activates all keybindings for a given plugin with the active script.""" + from . import cthulhu_state # Import here to avoid circular dependency + if not cthulhu_state.activeScript: + logger.warning(f"No active script to activate keybindings for plugin: {plugin_info.get_module_name()}") + return - plugin = pluginInfo.instance - if not hasattr(plugin, 'get_bindings') or not plugin.get_bindings(): - continue - - logger.info(f"Registering keybindings for plugin: {plugin.name}") - bindings = plugin.get_bindings() - for binding in bindings.keyBindings: - logger.info(f"Adding binding: {binding.keysymstring} with modifiers {binding.modifiers}") - # Check if binding already exists to avoid duplicates + plugin_name = plugin_info.get_module_name() + if plugin_name in self._plugin_keybindings: + active_script = cthulhu_state.activeScript + for binding in self._plugin_keybindings[plugin_name]: + if binding in self._global_bindings: + continue if not active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"): active_script.getKeyBindings().add(binding) - # Force recalculation of keycode if it wasn't set when device was None - if not binding.keycode and binding.keysymstring: - from . import keybindings - binding.keycode = keybindings.getKeycode(binding.keysymstring) - # Register key grab at system level - this was missing! - grab_ids = cthulhu.addKeyGrab(binding) + grab_ids = self.app.addKeyGrab(binding) if grab_ids: binding._grab_ids = grab_ids else: - logger.warning(f"Failed to create key grab for {binding.keysymstring} - device may not be available") + logger.warning(f"Failed to create key grab for {binding.keysymstring} for plugin {plugin_name}") + logger.debug(f"Activated keybinding '{binding.asString()}' for plugin '{plugin_name}'") - def _on_settings_changed(self, app=None): - """Re-register all plugin keybindings when settings change.""" - if not hasattr(self, '_plugin_global_bindings'): - return - - from . import cthulhu_state + def _deactivate_plugin_keybindings(self, plugin_info): + """Deactivates all keybindings for a given plugin from the active script.""" + from . import cthulhu_state # Import here to avoid circular dependency if not cthulhu_state.activeScript: + logger.warning(f"No active script to deactivate keybindings for plugin: {plugin_info.get_module_name()}") return - active_script = cthulhu_state.activeScript - for plugin_name, bindings in self._plugin_global_bindings.items(): - logger.info(f"Re-registering keybindings for plugin: {plugin_name}") - for binding in bindings.keyBindings: - # Check if binding already exists - if active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"): + plugin_name = plugin_info.get_module_name() + if plugin_name in self._plugin_keybindings: + active_script = cthulhu_state.activeScript + for binding in self._plugin_keybindings[plugin_name]: + if binding in self._global_bindings: + if hasattr(binding, '_global_grab_ids'): + for grab_id in binding._global_grab_ids: + self.app.removeKeyGrab(grab_id) + del binding._global_grab_ids + if binding in self._global_bindings: + self._global_bindings.remove(binding) + self._global_keybindings.remove(binding) continue + if active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"): + active_script.getKeyBindings().remove(binding) + if hasattr(binding, '_grab_ids'): + for grab_id in binding._grab_ids: + self.app.removeKeyGrab(grab_id) + del binding._grab_ids + logger.debug(f"Deactivated keybinding '{binding.asString()}' for plugin '{plugin_name}'") - active_script.getKeyBindings().add(binding) - from . import cthulhu - grab_ids = cthulhu.addKeyGrab(binding) - if grab_ids: - binding._grab_ids = grab_ids + def refresh_active_script_keybindings(self): + """Public method to refresh keybindings for the currently active script.""" + from . import cthulhu_state + if cthulhu_state.activeScript: + self._on_active_script_changed(self.app, cthulhu_state.activeScript) + + def get_global_keybindings(self): + return self._global_keybindings + + def _on_active_script_changed(self, app, new_script): + """Called when the active script changes. Re-applies keybindings for all active plugins.""" + logger.info(f"Active script changed to {new_script.name if new_script else 'None'}. Re-applying plugin keybindings.") + + # First, remove all existing plugin keybindings from the old script + # This requires iterating through all plugins, not just active ones, to ensure cleanup + from . import cthulhu_state + if cthulhu_state.activeScript: # If there was an old active script + old_script = cthulhu_state.activeScript + for plugin_name, bindings in self._plugin_keybindings.items(): + for binding in bindings: + if binding in self._global_bindings: + continue + if old_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"): + old_script.getKeyBindings().remove(binding) + if hasattr(binding, '_grab_ids'): + for grab_id in binding._grab_ids: + self.app.removeKeyGrab(grab_id) + del binding._grab_ids + logger.debug(f"Removed keybinding '{binding.asString()}' from old script for plugin '{plugin_name}'") + + # Now, if there's a new active script, apply keybindings for currently active plugins + if new_script: + for plugin_name in self._active_plugins: + plugin_info = self._plugins.get(plugin_name) + if plugin_info and plugin_info.loaded: + self._activate_plugin_keybindings(plugin_info) def _setup_plugin_dirs(self): """Ensure plugin directories exist.""" @@ -958,11 +878,6 @@ class PluginSystemManager: def loadPlugin(self, pluginInfo): """Load a plugin.""" - # Skip if pluggy is not available - if not PLUGGY_AVAILABLE: - logger.info(f"Skipping plugin {pluginInfo.get_name()}: pluggy not available") - return False - module_name = pluginInfo.get_module_name() logger.info(f"=== PluginSystemManager.loadPlugin starting for: {module_name} ===") @@ -1055,8 +970,8 @@ class PluginSystemManager: self._register_plugin_dbus_module(pluginInfo) - # Register any global keybindings from the plugin - self.register_plugin_global_keybindings(pluginInfo.instance) + # Activate plugin-specific keybindings + self._activate_plugin_keybindings(pluginInfo) return True @@ -1068,10 +983,6 @@ class PluginSystemManager: def unloadPlugin(self, pluginInfo): """Unload a plugin.""" - # Skip if pluggy is not available - if not PLUGGY_AVAILABLE: - return False - if pluginInfo.builtin: return False @@ -1108,9 +1019,6 @@ class PluginSystemManager: def unloadAllPlugins(self, ForceAllPlugins=False): """Unload all plugins.""" - if not PLUGGY_AVAILABLE: - return - for pluginInfo in self.plugins: if ForceAllPlugins or pluginInfo.loaded: self.unloadPlugin(pluginInfo) diff --git a/src/cthulhu/plugins/GameMode/plugin.py b/src/cthulhu/plugins/GameMode/plugin.py index d89a43e..428856a 100644 --- a/src/cthulhu/plugins/GameMode/plugin.py +++ b/src/cthulhu/plugins/GameMode/plugin.py @@ -76,7 +76,8 @@ class GameMode(Plugin): self._kbBinding = self.registerGestureByString( self._toggle_game_mode, description, - gestureString + gestureString, + globalBinding=True ) if self._kbBinding: debug.printMessage(debug.LEVEL_INFO, f"GameMode: Registered keybinding {gestureString}", True) diff --git a/src/cthulhu/plugins/PluginManager/plugin.py b/src/cthulhu/plugins/PluginManager/plugin.py index 3433bfc..0693f2d 100644 --- a/src/cthulhu/plugins/PluginManager/plugin.py +++ b/src/cthulhu/plugins/PluginManager/plugin.py @@ -19,11 +19,12 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk, Pango from cthulhu.plugin import Plugin, cthulhu_hookimpl +from cthulhu import cthulhu from cthulhu import debug from cthulhu import settings_manager logger = logging.getLogger(__name__) -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class PluginManager(Plugin): @@ -225,7 +226,7 @@ class PluginManager(Plugin): available_plugins = self._discover_plugins() # Get currently active plugins - active_plugins = _settingsManager.getSetting('activePlugins') or [] + active_plugins = cthulhu.cthulhuApp.settingsManager.getSetting('activePlugins') or [] debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Found {len(available_plugins)} plugins", True) @@ -449,7 +450,7 @@ class PluginManager(Plugin): debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Plugin {plugin_name} toggled to {'active' if is_active else 'inactive'}", True) # Get current active plugins - active_plugins = _settingsManager.getSetting('activePlugins') or [] + active_plugins = cthulhu.cthulhuApp.settingsManager.getSetting('activePlugins') or [] active_plugins = list(active_plugins) if is_active and plugin_name not in active_plugins: @@ -457,31 +458,31 @@ class PluginManager(Plugin): elif not is_active and plugin_name in active_plugins: active_plugins.remove(plugin_name) - _settingsManager.setSetting('activePlugins', active_plugins) - if hasattr(_settingsManager, "general") and isinstance(_settingsManager.general, dict): - _settingsManager.general['activePlugins'] = active_plugins + cthulhu.cthulhuApp.settingsManager.setSetting('activePlugins', active_plugins) + if hasattr(cthulhu.cthulhuApp.settingsManager, "general") and isinstance(cthulhu.cthulhuApp.settingsManager.general, dict): + cthulhu.cthulhuApp.settingsManager.general['activePlugins'] = active_plugins try: - active_profile = _settingsManager.getSetting('activeProfile') + active_profile = cthulhu.cthulhuApp.settingsManager.getSetting('activeProfile') if isinstance(active_profile, (list, tuple)) and len(active_profile) > 1: profile_name = active_profile[1] else: - profile_name = _settingsManager.profile or 'default' + profile_name = cthulhu.cthulhuApp.settingsManager.profile or 'default' - current_general = _settingsManager.getGeneralSettings(profile_name) or {} + current_general = cthulhu.cthulhuApp.settingsManager.getGeneralSettings(profile_name) or {} current_general['activePlugins'] = active_plugins - _settingsManager.profile = profile_name - _settingsManager._setProfileGeneral(current_general) + cthulhu.cthulhuApp.settingsManager.profile = profile_name + cthulhu.cthulhuApp.settingsManager._setProfileGeneral(current_general) - pronunciations = _settingsManager.getPronunciations(profile_name) or {} - keybindings = _settingsManager.getKeybindings(profile_name) or {} + pronunciations = cthulhu.cthulhuApp.settingsManager.getPronunciations(profile_name) or {} + keybindings = cthulhu.cthulhuApp.settingsManager.getKeybindings(profile_name) or {} - backend = _settingsManager._backend + backend = cthulhu.cthulhuApp.settingsManager._backend if backend: backend.saveProfileSettings( profile_name, - _settingsManager.profileGeneral, + cthulhu.cthulhuApp.settingsManager.profileGeneral, pronunciations, keybindings ) diff --git a/src/cthulhu/plugins/SimplePluginSystem/plugin.py b/src/cthulhu/plugins/SimplePluginSystem/plugin.py index 95948ea..cc7d5a5 100644 --- a/src/cthulhu/plugins/SimplePluginSystem/plugin.py +++ b/src/cthulhu/plugins/SimplePluginSystem/plugin.py @@ -310,7 +310,7 @@ class SimplePluginSystem(Plugin): currPluginSetting['functionname'] = self.id_generator() return currPluginSetting - def registerGestureByString(self, function, description, shortcut): + def registerGestureByString(self, function, description, shortcut, globalBinding=True): """Register a keyboard shortcut for a function. This is a compatibility wrapper for the new plugin system. @@ -326,6 +326,7 @@ class SimplePluginSystem(Plugin): 'default', 'cthulhu', True, + globalBinding=globalBinding, contextName=self.module_name ) logger.debug(f"Registered shortcut {shortcut} for {description}") diff --git a/src/cthulhu/plugins/nvda2cthulhu/plugin.py b/src/cthulhu/plugins/nvda2cthulhu/plugin.py index 7fc34c5..f551ff6 100644 --- a/src/cthulhu/plugins/nvda2cthulhu/plugin.py +++ b/src/cthulhu/plugins/nvda2cthulhu/plugin.py @@ -128,7 +128,8 @@ class Nvda2Cthulhu(Plugin): self.toggle_translation, "NVDA to Cthulhu translation", "kb:cthulhu+control+shift+t", - learnModeEnabled=True + learnModeEnabled=True, + globalBinding=True ) if not self._dependencies_available(): self._present_message("NVDA to Cthulhu missing dependencies: python-msgpack and python-tornado") @@ -186,6 +187,8 @@ class Nvda2Cthulhu(Plugin): return True def toggle_translation(self, script=None, inputEvent=None): + if not self.settingsManager and self.app: + self.settingsManager = self.app.getSettingsManager() if not self.settingsManager: return False if not self._translation_command_available(): @@ -342,8 +345,7 @@ class Nvda2Cthulhu(Plugin): def _present_message(self, message): try: - scriptManagerApi = self.app.getDynamicApiManager().getAPI('ScriptManager') - scriptManager = scriptManagerApi.get_manager() + scriptManager = self.app.getScriptManager() scriptManager.get_default_script().presentMessage(message, resetStyles=False) except Exception: logger.info(message) diff --git a/src/cthulhu/resource_manager.py b/src/cthulhu/resource_manager.py index c768e10..2c461df 100644 --- a/src/cthulhu/resource_manager.py +++ b/src/cthulhu/resource_manager.py @@ -28,9 +28,9 @@ import traceback class TryFunction(): def __init__(self, function): self.function = function - def runSignal(self, app): + def runSignal(self, app, *args): try: - return self.function(app) + return self.function(app, *args) except Exception as e: print('try signal',e , traceback.print_exc()) def runInputEvent(self, script=None, inputEvent=None): diff --git a/src/cthulhu/script.py b/src/cthulhu/script.py index 29812ed..f4deba4 100644 --- a/src/cthulhu/script.py +++ b/src/cthulhu/script.py @@ -49,6 +49,7 @@ import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi +from . import cthulhu # Need access to cthulhuApp from . import ax_event_synthesizer from . import action_presenter from . import braille_generator @@ -78,9 +79,7 @@ from . import tutorialgenerator from . import where_am_i_presenter from .ax_object import AXObject -_eventManager = event_manager.getManager() -_scriptManager = script_manager.get_manager() -_settingsManager = settings_manager.getManager() +# Old global variables removed - scripts now access via cthulhuApp or self._app class Script: """The specific focus tracking scripts for applications. @@ -276,7 +275,7 @@ class Script: def getStructuralNavigation(self): """Returns the 'structural navigation' class for this script.""" types = self.getEnabledStructuralNavigationTypes() - enable = _settingsManager.getSetting('structuralNavigationEnabled') + enable = cthulhu.cthulhuApp.settingsManager.getSetting('structuralNavigationEnabled') return structural_navigation.StructuralNavigation(self, types, enable) def getLiveRegionManager(self): @@ -350,7 +349,7 @@ class Script: - script: the script. """ - _eventManager.registerScriptListeners(self) + cthulhu.cthulhuApp.eventManager.registerScriptListeners(self) def deregisterEventListeners(self): """Tells the event manager to stop listening for all the event types @@ -360,7 +359,7 @@ class Script: - script: the script. """ - _eventManager.deregisterScriptListeners(self) + cthulhu.cthulhuApp.eventManager.deregisterScriptListeners(self) def processObjectEvent(self, event): """Processes all AT-SPI object events of interest to this @@ -497,33 +496,16 @@ class Script: Returns True if the event is of interest. """ - user_bindings = None - user_bindings_map = settings.keyBindingsMap - if self.__module__ in user_bindings_map: - user_bindings = user_bindings_map[self.__module__] - elif "default" in user_bindings_map: - user_bindings = user_bindings_map["default"] - consumes = False self._lastCommandWasStructNav = False - if user_bindings: - handler = user_bindings.getInputHandler(keyboardEvent) - if handler \ - and handler.function in self.structuralNavigation.functions: - consumes = self.useStructuralNavigationModel() - if consumes: - self._lastCommandWasStructNav = True - else: - consumes = handler is not None - if not consumes: - handler = self.keyBindings.getInputHandler(keyboardEvent) - if handler \ - and handler.function in self.structuralNavigation.functions: - consumes = self.useStructuralNavigationModel() - if consumes: - self._lastCommandWasStructNav = True - else: - consumes = handler is not None + handler = self.keyBindings.getInputHandler(keyboardEvent) + if handler \ + and handler.function in self.structuralNavigation.functions: + consumes = self.useStructuralNavigationModel() + if consumes: + self._lastCommandWasStructNav = True + else: + consumes = handler is not None return consumes def consumesBrailleEvent(self, brailleEvent): diff --git a/src/cthulhu/script_manager.py b/src/cthulhu/script_manager.py index 47e9602..dda84b2 100644 --- a/src/cthulhu/script_manager.py +++ b/src/cthulhu/script_manager.py @@ -43,8 +43,9 @@ def _get_ax_utilities(): class ScriptManager: - def __init__(self): + def __init__(self, app): # Added app argument debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Initializing", True) + self.app = app # Store app instance self.appScripts = {} self.toolkitScripts = {} self.customScripts = {} @@ -345,11 +346,9 @@ class ScriptManager: newScript.activate() - # Register plugin keybindings with the new active script + # Emit signal that active script has changed, so PluginSystemManager can update keybindings from . import cthulhu - plugin_manager = cthulhu.cthulhuApp.getPluginSystemManager() - if plugin_manager: - pass # plugin_manager.register_plugin_keybindings_with_active_script() + cthulhu.cthulhuApp.getSignalManager().emitSignal('active-script-changed', newScript) tokens = ["SCRIPT MANAGER: Setting active script to", newScript, "reason:", reason] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -428,7 +427,13 @@ class ScriptManager: del app -_manager = ScriptManager() - +_manager = None def get_manager(): + """Returns the Script Manager""" + + global _manager + if _manager is None: + from . import cthulhu + _manager = ScriptManager(cthulhu.cthulhuApp) return _manager + diff --git a/src/cthulhu/script_utilities.py b/src/cthulhu/script_utilities.py index 7e95521..62f8e91 100644 --- a/src/cthulhu/script_utilities.py +++ b/src/cthulhu/script_utilities.py @@ -73,7 +73,7 @@ from .ax_value import AXValue from .ax_utilities import AXUtilities from .ax_utilities_relation import AXUtilitiesRelation -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager # Try to import sound system for indentation beeps try: @@ -1017,9 +1017,9 @@ class Utilities: return True def isProgressBarUpdate(self, obj): - if not _settingsManager.getSetting('speakProgressBarUpdates') \ - and not _settingsManager.getSetting('brailleProgressBarUpdates') \ - and not _settingsManager.getSetting('beepProgressBarUpdates'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('speakProgressBarUpdates') \ + and not cthulhu.cthulhuApp.settingsManager.getSetting('brailleProgressBarUpdates') \ + and not cthulhu.cthulhuApp.settingsManager.getSetting('beepProgressBarUpdates'): return False, "Updates not enabled" if not self.isProgressBar(obj): @@ -1028,11 +1028,11 @@ class Utilities: if self.hasNoSize(obj): return False, "Has no size" - if _settingsManager.getSetting('ignoreStatusBarProgressBars'): + if cthulhu.cthulhuApp.settingsManager.getSetting('ignoreStatusBarProgressBars'): if AXObject.find_ancestor(obj, AXUtilities.is_status_bar): return False, "Is status bar descendant" - verbosity = _settingsManager.getSetting('progressBarVerbosity') + verbosity = cthulhu.cthulhuApp.settingsManager.getSetting('progressBarVerbosity') if verbosity == settings.PROGRESS_BAR_ALL: return True, "Verbosity is all" @@ -1220,12 +1220,12 @@ class Utilities: return False if not self.getDocumentForObject(table): - return _settingsManager.getSetting('readFullRowInGUITable') + return cthulhu.cthulhuApp.settingsManager.getSetting('readFullRowInGUITable') if self.isSpreadSheetTable(table): - return _settingsManager.getSetting('readFullRowInSpreadSheet') + return cthulhu.cthulhuApp.settingsManager.getSetting('readFullRowInSpreadSheet') - return _settingsManager.getSetting('readFullRowInDocumentTable') + return cthulhu.cthulhuApp.settingsManager.getSetting('readFullRowInDocumentTable') def isSorted(self, obj): return False @@ -2958,7 +2958,7 @@ class Utilities: # If the user has set their punctuation level to All, then the synthesizer will # do the work for us. If the user has set their punctuation level to None, then # they really don't want punctuation and we mustn't override that. - style = _settingsManager.getSetting("verbalizePunctuationStyle") + style = cthulhu.cthulhuApp.settingsManager.getSetting("verbalizePunctuationStyle") if style in [settings.PUNCTUATION_STYLE_ALL, settings.PUNCTUATION_STYLE_NONE]: return False @@ -3202,15 +3202,15 @@ class Utilities: return changed, previousColumns def _indentation_enabled(self): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return False - return _settingsManager.getSetting('enableIndentation') + return cthulhu.cthulhuApp.settingsManager.getSetting('enableIndentation') def _indentation_speech_enabled(self): if not self._indentation_enabled(): return False - presentationMode = _settingsManager.getSetting('indentationPresentationMode') \ + presentationMode = cthulhu.cthulhuApp.settingsManager.getSetting('indentationPresentationMode') \ or settings.indentationPresentationMode return presentationMode in ( settings.INDENTATION_PRESENTATION_SPEECH, @@ -3221,7 +3221,7 @@ class Utilities: if not self._indentation_enabled(): return False - presentationMode = _settingsManager.getSetting('indentationPresentationMode') \ + presentationMode = cthulhu.cthulhuApp.settingsManager.getSetting('indentationPresentationMode') \ or settings.indentationPresentationMode return presentationMode in ( settings.INDENTATION_PRESENTATION_BEEPS, @@ -3265,7 +3265,7 @@ class Utilities: def get_indentation_presentation(self, line, obj=None): data = self._get_indentation_data(line) hasIndentation = bool(data["indentation"]) - presentationMode = _settingsManager.getSetting('indentationPresentationMode') \ + presentationMode = cthulhu.cthulhuApp.settingsManager.getSetting('indentationPresentationMode') \ or settings.indentationPresentationMode indentDebug = data["indentation"].replace("\t", "\\t").replace(" ", ".") @@ -5075,10 +5075,10 @@ class Utilities: return False if AXUtilities.is_password_text(event.source): - return _settingsManager.getSetting("enableKeyEcho") + return cthulhu.cthulhuApp.settingsManager.getSetting("enableKeyEcho") if len(event.any_data.strip()) == 1: - return _settingsManager.getSetting("enableEchoByCharacter") + return cthulhu.cthulhuApp.settingsManager.getSetting("enableEchoByCharacter") return False @@ -5333,7 +5333,7 @@ class Utilities: child = self.findChildAtOffset(obj, newEnd - 1) self.handleTextSelectionChange(child, False) - speakMessage = speakMessage and not _settingsManager.getSetting('onlySpeakDisplayedText') + speakMessage = speakMessage and not cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText') for start, end, message in changes: string = AXText.get_substring(obj, start, end) endsWithChild = string.endswith(self.EMBEDDED_OBJECT_CHARACTER) @@ -5360,7 +5360,7 @@ class Utilities: def _speakTextSelectionState(self, nSelections): """Hacky and to-be-obsoleted method.""" - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return False eventStr, mods = self.lastKeyAndModifiers() diff --git a/src/cthulhu/scripts/apps/notify-osd/script.py b/src/cthulhu/scripts/apps/notify-osd/script.py index 767a3fa..6c48018 100644 --- a/src/cthulhu/scripts/apps/notify-osd/script.py +++ b/src/cthulhu/scripts/apps/notify-osd/script.py @@ -38,7 +38,7 @@ import cthulhu.settings_manager as settings_manager from cthulhu.ax_object import AXObject from cthulhu.ax_value import AXValue -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhuApp.settingsManager ######################################################################## # # diff --git a/src/cthulhu/scripts/apps/soffice/braille_generator.py b/src/cthulhu/scripts/apps/soffice/braille_generator.py index dc12fac..c205d2b 100644 --- a/src/cthulhu/scripts/apps/soffice/braille_generator.py +++ b/src/cthulhu/scripts/apps/soffice/braille_generator.py @@ -42,7 +42,7 @@ import cthulhu.settings_manager as settings_manager from cthulhu.ax_object import AXObject from cthulhu.ax_utilities import AXUtilities -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhuApp.settingsManager class BrailleGenerator(braille_generator.BrailleGenerator): diff --git a/src/cthulhu/scripts/default.py b/src/cthulhu/scripts/default.py index 03e7c17..3bdee8a 100644 --- a/src/cthulhu/scripts/default.py +++ b/src/cthulhu/scripts/default.py @@ -54,7 +54,7 @@ import cthulhu.input_event as input_event import cthulhu.input_event_manager as input_event_manager import cthulhu.keybindings as keybindings import cthulhu.messages as messages -import cthulhu.cthulhu as cthulhu +from cthulhu import cthulhu import cthulhu.cthulhu_modifier_manager as cthulhu_modifier_manager import cthulhu.cthulhu_state as cthulhu_state import cthulhu.phonnames as phonnames @@ -71,8 +71,8 @@ from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities_relation import AXUtilitiesRelation -_scriptManager = script_manager.get_manager() -_settingsManager = settings_manager.getManager() +_scriptManager = None # Removed - use cthulhu.cthulhuApp.scriptManager +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager ######################################################################## # # @@ -391,7 +391,7 @@ class Script(script.Script): for keyBinding in bindings.keyBindings: keyBindings.add(keyBinding) - layout = _settingsManager.getSetting('keyboardLayout') + layout = cthulhu.cthulhuApp.settingsManager.getSetting('keyboardLayout') isDesktop = layout == settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP bindings = self.flatReviewPresenter.get_bindings(isDesktop) for keyBinding in bindings.keyBindings: @@ -431,7 +431,6 @@ class Script(script.Script): # Add plugin keybindings from APIHelper storage try: - import cthulhu.cthulhu as cthulhu if hasattr(cthulhu, 'cthulhuApp') and cthulhu.cthulhuApp: api_helper = cthulhu.cthulhuApp.getAPIHelper() with open('/tmp/extension_bindings_debug.log', 'a') as f: @@ -491,7 +490,7 @@ class Script(script.Script): keyBindings.add(keyBinding) try: - keyBindings = _settingsManager.overrideKeyBindings(self, keyBindings) + keyBindings = cthulhu.cthulhuApp.settingsManager.overrideKeyBindings(self, keyBindings) except Exception as error: tokens = ["DEFAULT: Exception when overriding keybindings in", self, ":", error] debug.printTokens(debug.LEVEL_WARNING, tokens, True) @@ -504,7 +503,7 @@ class Script(script.Script): keyBindings = keybindings.KeyBindings() - layout = _settingsManager.getSetting('keyboardLayout') + layout = cthulhu.cthulhuApp.settingsManager.getSetting('keyboardLayout') if layout == settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP: for keyBinding in self.__getDesktopBindings().keyBindings: keyBindings.add(keyBinding) @@ -763,17 +762,13 @@ class Script(script.Script): tokens = ["DEFAULT: Activating script for", self.app] debug.printTokens(debug.LEVEL_INFO, tokens, True) - _settingsManager.loadAppSettings(self) + cthulhu.cthulhuApp.settingsManager.loadAppSettings(self) braille.checkBrailleSetting() braille.setupKeyRanges(self.brailleBindings.keys()) speech.checkSpeechSetting() self.speechAndVerbosityManager.update_punctuation_level() self.speechAndVerbosityManager.update_capitalization_style() - # Use new InputEventManager for global keyboard capture by default - # Only fall back to legacy handling for problematic applications - cthulhu.setKeyHandling(True) - self.addKeyGrabs() tokens = ["DEFAULT: Script for", self.app, "activated"] @@ -786,8 +781,8 @@ class Script(script.Script): - obj: the Accessible """ - if not _settingsManager.getSetting('enableBraille') \ - and not _settingsManager.getSetting('enableBrailleMonitor'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableBraille') \ + and not cthulhu.cthulhuApp.settingsManager.getSetting('enableBrailleMonitor'): debug.printMessage(debug.LEVEL_INFO, "BRAILLE: update disabled", True) return @@ -882,8 +877,8 @@ class Script(script.Script): associated with cell 0.""" if isinstance(inputEvent, input_event.KeyboardEvent) \ - and not _settingsManager.getSetting('enableBraille') \ - and not _settingsManager.getSetting('enableBrailleMonitor'): + and not cthulhu.cthulhuApp.settingsManager.getSetting('enableBraille') \ + and not cthulhu.cthulhuApp.settingsManager.getSetting('enableBrailleMonitor'): msg = "DEFAULT: panBrailleLeft command requires braille or braille monitor" debug.printMessage(debug.LEVEL_INFO, msg, True) return @@ -955,8 +950,8 @@ class Script(script.Script): associated with cell 0.""" if isinstance(inputEvent, input_event.KeyboardEvent) \ - and not _settingsManager.getSetting('enableBraille') \ - and not _settingsManager.getSetting('enableBrailleMonitor'): + and not cthulhu.cthulhuApp.settingsManager.getSetting('enableBraille') \ + and not cthulhu.cthulhuApp.settingsManager.getSetting('enableBrailleMonitor'): msg = "DEFAULT: panBrailleRight command requires braille or braille monitor" debug.printMessage(debug.LEVEL_INFO, msg, True) return @@ -1152,13 +1147,13 @@ class Script(script.Script): def cycleSettingsProfile(self, inputEvent=None): """Cycle through the user's existing settings profiles.""" - profiles = _settingsManager.availableProfiles() + profiles = cthulhu.cthulhuApp.settingsManager.availableProfiles() if not (profiles and profiles[0]): self.presentMessage(messages.PROFILE_NOT_FOUND) return True def isMatch(x): - return x is not None and x[1] == _settingsManager.getProfile() + return x is not None and x[1] == cthulhu.cthulhuApp.settingsManager.getProfile() current = list(filter(isMatch, profiles))[0] try: @@ -1166,7 +1161,7 @@ class Script(script.Script): except IndexError: name, profileID = profiles[0] - _settingsManager.setProfile(profileID, updateLocale=True) + cthulhu.cthulhuApp.settingsManager.setProfile(profileID, updateLocale=True) braille.checkBrailleSetting() @@ -1538,7 +1533,7 @@ class Script(script.Script): debug.printTokens(debug.LEVEL_INFO, tokens, True) return - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return isSelected = AXUtilities.is_selected(event.source) @@ -1676,7 +1671,7 @@ class Script(script.Script): if role == Atspi.Role.TOOL_TIP: keyString, mods = self.utilities.lastKeyAndModifiers() if keyString != "F1" \ - and not _settingsManager.getSetting('presentToolTips'): + and not cthulhu.cthulhuApp.settingsManager.getSetting('presentToolTips'): return if event.detail1: self.presentObject(obj, interrupt=True) @@ -1699,7 +1694,7 @@ class Script(script.Script): debug.printMessage(debug.LEVEL_INFO, msg, True) return - if _settingsManager.getSetting('speakMisspelledIndicator'): + if cthulhu.cthulhuApp.settingsManager.getSetting('speakMisspelledIndicator'): offset = AXText.get_caret_offset(event.source) if not AXText.get_substring(event.source, offset, offset + 1).isalnum(): offset -= 1 @@ -1821,11 +1816,11 @@ class Script(script.Script): if len(string) != 1: return - if _settingsManager.getSetting('enableEchoBySentence') \ + if cthulhu.cthulhuApp.settingsManager.getSetting('enableEchoBySentence') \ and self.echoPreviousSentence(event.source): return - if _settingsManager.getSetting('enableEchoByWord'): + if cthulhu.cthulhuApp.settingsManager.getSetting('enableEchoByWord'): self.echoPreviousWord(event.source) def onTextSelectionChanged(self, event): @@ -1988,7 +1983,7 @@ class Script(script.Script): cthulhu.setLocusOfFocus(event, None) cthulhu.setActiveWindow(None) - _scriptManager.set_active_script(None, "Window deactivated") + cthulhu.cthulhuApp.scriptManager.set_active_script(None, "Window deactivated") def onClipboardContentsChanged(self, *args): if self.flatReviewPresenter.is_active(): @@ -2072,7 +2067,7 @@ class Script(script.Script): return def _rewindSayAll(self, context, minCharCount=10): - if not _settingsManager.getSetting('rewindAndFastForwardInSayAll'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('rewindAndFastForwardInSayAll'): return False index = self._sayAllContexts.index(context) @@ -2090,7 +2085,7 @@ class Script(script.Script): return True def _fastForwardSayAll(self, context): - if not _settingsManager.getSetting('rewindAndFastForwardInSayAll'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('rewindAndFastForwardInSayAll'): return False if AXObject.supports_text(context.obj): @@ -2324,7 +2319,7 @@ class Script(script.Script): if not character or character == '\r': character = "\n" - speakBlankLines = _settingsManager.getSetting('speakBlankLines') + speakBlankLines = cthulhu.cthulhuApp.settingsManager.getSetting('speakBlankLines') if character == "\n": line, _, _ = AXText.get_line_at_offset(obj, max(0, offset)) if not line or line == "\n": @@ -2447,7 +2442,7 @@ class Script(script.Script): # Announce when we cross a hard line boundary. if "\n" in word: - if _settingsManager.getSetting('enableSpeechIndentation'): + if cthulhu.cthulhuApp.settingsManager.getSetting('enableSpeechIndentation'): self.speakCharacter("\n") if word.startswith("\n"): startOffset += 1 @@ -2528,8 +2523,8 @@ class Script(script.Script): at that cell. Otherwise, we will pan in display-sized increments to show the review cursor.""" - if not _settingsManager.getSetting('enableBraille') \ - and not _settingsManager.getSetting('enableBrailleMonitor'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableBraille') \ + and not cthulhu.cthulhuApp.settingsManager.getSetting('enableBrailleMonitor'): debug.printMessage(debug.LEVEL_INFO, "BRAILLE: update review disabled", True) return @@ -2665,7 +2660,7 @@ class Script(script.Script): # Determine the correct "say all by" mode to use. # - sayAllStyle = _settingsManager.getSetting('sayAllStyle') + sayAllStyle = cthulhu.cthulhuApp.settingsManager.getSetting('sayAllStyle') if sayAllStyle == settings.SAYALL_STYLE_SENTENCE: mode = "sentence" elif sayAllStyle == settings.SAYALL_STYLE_LINE: @@ -2848,7 +2843,7 @@ class Script(script.Script): attributes. """ - if _settingsManager.getSetting('speakMisspelledIndicator'): + if cthulhu.cthulhuApp.settingsManager.getSetting('speakMisspelledIndicator'): if not AXObject.supports_text(obj): return # If we're on whitespace, we cannot be on a misspelled word. @@ -2941,18 +2936,18 @@ class Script(script.Script): if briefMessage is None: briefMessage = fullMessage - if _settingsManager.getSetting('enableSpeech'): - if not _settingsManager.getSetting('messagesAreDetailed'): + if cthulhu.cthulhuApp.settingsManager.getSetting('enableSpeech'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('messagesAreDetailed'): message = briefMessage else: message = fullMessage if message: self.speakMessage(message, voice=voice, resetStyles=resetStyles, force=force) - if (_settingsManager.getSetting('enableBraille') \ - or _settingsManager.getSetting('enableBrailleMonitor')) \ - and _settingsManager.getSetting('enableFlashMessages'): - if not _settingsManager.getSetting('flashIsDetailed'): + if (cthulhu.cthulhuApp.settingsManager.getSetting('enableBraille') \ + or cthulhu.cthulhuApp.settingsManager.getSetting('enableBrailleMonitor')) \ + and cthulhu.cthulhuApp.settingsManager.getSetting('enableFlashMessages'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('flashIsDetailed'): message = briefMessage else: message = fullMessage @@ -2965,10 +2960,10 @@ class Script(script.Script): message = [i for i in message if isinstance(i, str)] message = " ".join(message) - if _settingsManager.getSetting('flashIsPersistent'): + if cthulhu.cthulhuApp.settingsManager.getSetting('flashIsPersistent'): duration = -1 else: - duration = _settingsManager.getSetting('brailleFlashTime') + duration = cthulhu.cthulhuApp.settingsManager.getSetting('brailleFlashTime') braille.displayMessage(message, flashTime=duration) @@ -3067,8 +3062,8 @@ class Script(script.Script): a cursor routing key. """ - if not _settingsManager.getSetting('enableBraille') \ - and not _settingsManager.getSetting('enableBrailleMonitor'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableBraille') \ + and not cthulhu.cthulhuApp.settingsManager.getSetting('enableBrailleMonitor'): debug.printMessage(debug.LEVEL_INFO, "BRAILLE: display message disabled", True) return @@ -3091,8 +3086,8 @@ class Script(script.Script): a cursor routing key. """ - if not _settingsManager.getSetting('enableBraille') \ - and not _settingsManager.getSetting('enableBrailleMonitor'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableBraille') \ + and not cthulhu.cthulhuApp.settingsManager.getSetting('enableBrailleMonitor'): debug.printMessage(debug.LEVEL_INFO, "BRAILLE: display regions disabled", True) return @@ -3254,8 +3249,8 @@ class Script(script.Script): def updateBrailleForNewCaretPosition(self, obj): """Try to reposition the cursor without having to do a full update.""" - if not _settingsManager.getSetting('enableBraille') \ - and not _settingsManager.getSetting('enableBrailleMonitor'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableBraille') \ + and not cthulhu.cthulhuApp.settingsManager.getSetting('enableBrailleMonitor'): debug.printMessage(debug.LEVEL_INFO, "BRAILLE: update caret disabled", True) return @@ -3364,31 +3359,31 @@ class Script(script.Script): prior to speaking the new text. """ - if not _settingsManager.getSetting('enableSpeech') \ - or (_settingsManager.getSetting('onlySpeakDisplayedText') and not force): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableSpeech') \ + or (cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText') and not force): return - voices = _settingsManager.getSetting('voices') + voices = cthulhu.cthulhuApp.settingsManager.getSetting('voices') systemVoice = voices.get(settings.SYSTEM_VOICE) voice = voice or systemVoice if voice == systemVoice and resetStyles: - capStyle = _settingsManager.getSetting('capitalizationStyle') - _settingsManager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_NONE) + capStyle = cthulhu.cthulhuApp.settingsManager.getSetting('capitalizationStyle') + cthulhu.cthulhuApp.settingsManager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_NONE) self.speechAndVerbosityManager.update_capitalization_style() - punctStyle = _settingsManager.getSetting('verbalizePunctuationStyle') - _settingsManager.setSetting('verbalizePunctuationStyle', + punctStyle = cthulhu.cthulhuApp.settingsManager.getSetting('verbalizePunctuationStyle') + cthulhu.cthulhuApp.settingsManager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_NONE) self.speechAndVerbosityManager.update_punctuation_level() speech.speak(string, voice, interrupt) if voice == systemVoice and resetStyles: - _settingsManager.setSetting('capitalizationStyle', capStyle) + cthulhu.cthulhuApp.settingsManager.setSetting('capitalizationStyle', capStyle) self.speechAndVerbosityManager.update_capitalization_style() - _settingsManager.setSetting('verbalizePunctuationStyle', punctStyle) + cthulhu.cthulhuApp.settingsManager.setSetting('verbalizePunctuationStyle', punctStyle) self.speechAndVerbosityManager.update_punctuation_level() @staticmethod diff --git a/src/cthulhu/scripts/terminal/script_utilities.py b/src/cthulhu/scripts/terminal/script_utilities.py index 1596c83..f59ef3c 100644 --- a/src/cthulhu/scripts/terminal/script_utilities.py +++ b/src/cthulhu/scripts/terminal/script_utilities.py @@ -44,7 +44,7 @@ from cthulhu.ax_object import AXObject from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class Utilities(script_utilities.Utilities): @@ -216,7 +216,7 @@ class Utilities(script_utilities.Utilities): return False def willEchoCharacter(self, event): - if not _settingsManager.getSetting("enableEchoByCharacter"): + if not cthulhu.cthulhuApp.settingsManager.getSetting("enableEchoByCharacter"): return False if len(event.event_string) != 1 \ diff --git a/src/cthulhu/scripts/web/script.py b/src/cthulhu/scripts/web/script.py index e482f2b..0954f63 100644 --- a/src/cthulhu/scripts/web/script.py +++ b/src/cthulhu/scripts/web/script.py @@ -68,7 +68,7 @@ from .speech_generator import SpeechGenerator from .tutorial_generator import TutorialGenerator from .script_utilities import Utilities -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class Script(default.Script): @@ -94,12 +94,12 @@ class Script(default.Script): self._navSuspended = False self._structNavWasEnabled = None - if _settingsManager.getSetting('caretNavigationEnabled') is None: - _settingsManager.setSetting('caretNavigationEnabled', True) - if _settingsManager.getSetting('sayAllOnLoad') is None: - _settingsManager.setSetting('sayAllOnLoad', True) - if _settingsManager.getSetting('pageSummaryOnLoad') is None: - _settingsManager.setSetting('pageSummaryOnLoad', True) + if cthulhu.cthulhuApp.settingsManager.getSetting('caretNavigationEnabled') is None: + cthulhu.cthulhuApp.settingsManager.setSetting('caretNavigationEnabled', True) + if cthulhu.cthulhuApp.settingsManager.getSetting('sayAllOnLoad') is None: + cthulhu.cthulhuApp.settingsManager.setSetting('sayAllOnLoad', True) + if cthulhu.cthulhuApp.settingsManager.getSetting('pageSummaryOnLoad') is None: + cthulhu.cthulhuApp.settingsManager.setSetting('pageSummaryOnLoad', True) self._changedLinesOnlyCheckButton = None self._controlCaretNavigationCheckButton = None @@ -234,7 +234,7 @@ class Script(default.Script): self.inputEventHandlers.get("toggleLayoutModeHandler"))) - layout = _settingsManager.getSetting('keyboardLayout') + layout = cthulhu.cthulhuApp.settingsManager.getSetting('keyboardLayout') if layout == settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP: key = "KP_Multiply" else: @@ -402,14 +402,14 @@ class Script(default.Script): generalAlignment.add(generalGrid) label = guilabels.USE_CARET_NAVIGATION - value = _settingsManager.getSetting('caretNavigationEnabled') + value = cthulhu.cthulhuApp.settingsManager.getSetting('caretNavigationEnabled') self._controlCaretNavigationCheckButton = \ Gtk.CheckButton.new_with_mnemonic(label) self._controlCaretNavigationCheckButton.set_active(value) generalGrid.attach(self._controlCaretNavigationCheckButton, 0, 0, 1, 1) label = guilabels.AUTO_FOCUS_MODE_CARET_NAV - value = _settingsManager.getSetting('caretNavTriggersFocusMode') + value = cthulhu.cthulhuApp.settingsManager.getSetting('caretNavTriggersFocusMode') self._autoFocusModeCaretNavCheckButton = Gtk.CheckButton.new_with_mnemonic(label) self._autoFocusModeCaretNavCheckButton.set_active(value) generalGrid.attach(self._autoFocusModeCaretNavCheckButton, 0, 1, 1, 1) @@ -422,31 +422,31 @@ class Script(default.Script): generalGrid.attach(self._structuralNavigationCheckButton, 0, 2, 1, 1) label = guilabels.AUTO_FOCUS_MODE_STRUCT_NAV - value = _settingsManager.getSetting('structNavTriggersFocusMode') + value = cthulhu.cthulhuApp.settingsManager.getSetting('structNavTriggersFocusMode') self._autoFocusModeStructNavCheckButton = Gtk.CheckButton.new_with_mnemonic(label) self._autoFocusModeStructNavCheckButton.set_active(value) generalGrid.attach(self._autoFocusModeStructNavCheckButton, 0, 3, 1, 1) label = guilabels.AUTO_FOCUS_MODE_NATIVE_NAV - value = _settingsManager.getSetting('nativeNavTriggersFocusMode') + value = cthulhu.cthulhuApp.settingsManager.getSetting('nativeNavTriggersFocusMode') self._autoFocusModeNativeNavCheckButton = Gtk.CheckButton.new_with_mnemonic(label) self._autoFocusModeNativeNavCheckButton.set_active(value) generalGrid.attach(self._autoFocusModeNativeNavCheckButton, 0, 4, 1, 1) label = guilabels.READ_PAGE_UPON_LOAD - value = _settingsManager.getSetting('sayAllOnLoad') + value = cthulhu.cthulhuApp.settingsManager.getSetting('sayAllOnLoad') self._sayAllOnLoadCheckButton = Gtk.CheckButton.new_with_mnemonic(label) self._sayAllOnLoadCheckButton.set_active(value) generalGrid.attach(self._sayAllOnLoadCheckButton, 0, 5, 1, 1) label = guilabels.PAGE_SUMMARY_UPON_LOAD - value = _settingsManager.getSetting('pageSummaryOnLoad') + value = cthulhu.cthulhuApp.settingsManager.getSetting('pageSummaryOnLoad') self._pageSummaryOnLoadCheckButton = Gtk.CheckButton.new_with_mnemonic(label) self._pageSummaryOnLoadCheckButton.set_active(value) generalGrid.attach(self._pageSummaryOnLoadCheckButton, 0, 6, 1, 1) label = guilabels.CONTENT_LAYOUT_MODE - value = _settingsManager.getSetting('layoutMode') + value = cthulhu.cthulhuApp.settingsManager.getSetting('layoutMode') self._layoutModeCheckButton = Gtk.CheckButton.new_with_mnemonic(label) self._layoutModeCheckButton.set_active(value) generalGrid.attach(self._layoutModeCheckButton, 0, 7, 1, 1) @@ -465,28 +465,28 @@ class Script(default.Script): tableAlignment.add(tableGrid) label = guilabels.TABLE_SPEAK_CELL_COORDINATES - value = _settingsManager.getSetting('speakCellCoordinates') + value = cthulhu.cthulhuApp.settingsManager.getSetting('speakCellCoordinates') self._speakCellCoordinatesCheckButton = \ Gtk.CheckButton.new_with_mnemonic(label) self._speakCellCoordinatesCheckButton.set_active(value) tableGrid.attach(self._speakCellCoordinatesCheckButton, 0, 0, 1, 1) label = guilabels.TABLE_SPEAK_CELL_SPANS - value = _settingsManager.getSetting('speakCellSpan') + value = cthulhu.cthulhuApp.settingsManager.getSetting('speakCellSpan') self._speakCellSpanCheckButton = \ Gtk.CheckButton.new_with_mnemonic(label) self._speakCellSpanCheckButton.set_active(value) tableGrid.attach(self._speakCellSpanCheckButton, 0, 1, 1, 1) label = guilabels.TABLE_ANNOUNCE_CELL_HEADER - value = _settingsManager.getSetting('speakCellHeaders') + value = cthulhu.cthulhuApp.settingsManager.getSetting('speakCellHeaders') self._speakCellHeadersCheckButton = \ Gtk.CheckButton.new_with_mnemonic(label) self._speakCellHeadersCheckButton.set_active(value) tableGrid.attach(self._speakCellHeadersCheckButton, 0, 2, 1, 1) label = guilabels.TABLE_SKIP_BLANK_CELLS - value = _settingsManager.getSetting('skipBlankCells') + value = cthulhu.cthulhuApp.settingsManager.getSetting('skipBlankCells') self._skipBlankCellsCheckButton = \ Gtk.CheckButton.new_with_mnemonic(label) self._skipBlankCellsCheckButton.set_active(value) @@ -505,7 +505,7 @@ class Script(default.Script): findGrid = Gtk.Grid() findAlignment.add(findGrid) - verbosity = _settingsManager.getSetting('findResultsVerbosity') + verbosity = cthulhu.cthulhuApp.settingsManager.getSetting('findResultsVerbosity') label = guilabels.FIND_SPEAK_RESULTS value = verbosity != settings.FIND_SPEAK_NONE @@ -530,7 +530,7 @@ class Script(default.Script): hgrid.attach(self._minimumFindLengthLabel, 0, 0, 1, 1) self._minimumFindLengthAdjustment = \ - Gtk.Adjustment(_settingsManager.getSetting( + Gtk.Adjustment(cthulhu.cthulhuApp.settingsManager.getSetting( 'findResultsMinimumLength'), 0, 20, 1) self._minimumFindLengthSpinButton = Gtk.SpinButton() self._minimumFindLengthSpinButton.set_adjustment( @@ -673,7 +673,7 @@ class Script(default.Script): self._sayAllIsInterrupted = False - sayAllStyle = _settingsManager.getSetting('sayAllStyle') + sayAllStyle = cthulhu.cthulhuApp.settingsManager.getSetting('sayAllStyle') sayAllBySentence = sayAllStyle == settings.SAYALL_STYLE_SENTENCE if offset is None: obj, characterOffset = self.utilities.getCaretContext() @@ -777,10 +777,10 @@ class Script(default.Script): start, end = selections[0] offset = max(offset, start) self.utilities.setCaretContext(obj, offset, documentFrame=document) - if end - start < _settingsManager.getSetting('findResultsMinimumLength'): + if end - start < cthulhu.cthulhuApp.settingsManager.getSetting('findResultsMinimumLength'): return - verbosity = _settingsManager.getSetting('findResultsVerbosity') + verbosity = cthulhu.cthulhuApp.settingsManager.getSetting('findResultsVerbosity') if verbosity == settings.FIND_SPEAK_NONE: return @@ -820,7 +820,7 @@ class Script(default.Script): if not self.utilities.inDocumentContent(): return super()._rewindSayAll(context, minCharCount) - if not _settingsManager.getSetting('rewindAndFastForwardInSayAll'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('rewindAndFastForwardInSayAll'): return False try: @@ -839,7 +839,7 @@ class Script(default.Script): if not self.utilities.inDocumentContent(): return super()._fastForwardSayAll(context) - if not _settingsManager.getSetting('rewindAndFastForwardInSayAll'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('rewindAndFastForwardInSayAll'): return False try: @@ -915,7 +915,7 @@ class Script(default.Script): debug.printMessage(debug.LEVEL_INFO, msg, True) return False - if not _settingsManager.getSetting('structNavTriggersFocusMode') \ + if not cthulhu.cthulhuApp.settingsManager.getSetting('structNavTriggersFocusMode') \ and self._lastCommandWasStructNav: msg = "WEB: Not using focus mode due to struct nav settings" debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -924,14 +924,14 @@ class Script(default.Script): if prevObj and AXObject.is_dead(prevObj): prevObj = None - if not _settingsManager.getSetting('caretNavTriggersFocusMode') \ + if not cthulhu.cthulhuApp.settingsManager.getSetting('caretNavTriggersFocusMode') \ and self._lastCommandWasCaretNav \ and not self.utilities.isNavigableToolTipDescendant(prevObj): msg = "WEB: Not using focus mode due to caret nav settings" debug.printMessage(debug.LEVEL_INFO, msg, True) return False - if not _settingsManager.getSetting('nativeNavTriggersFocusMode') \ + if not cthulhu.cthulhuApp.settingsManager.getSetting('nativeNavTriggersFocusMode') \ and not (self._lastCommandWasStructNav or self._lastCommandWasCaretNav): msg = "WEB: Not changing focus/browse mode due to native nav settings" debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -1087,8 +1087,8 @@ class Script(default.Script): def updateBraille(self, obj, **args): """Updates the braille display to show the given object.""" - if not _settingsManager.getSetting('enableBraille') \ - and not _settingsManager.getSetting('enableBrailleMonitor'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableBraille') \ + and not cthulhu.cthulhuApp.settingsManager.getSetting('enableBrailleMonitor'): debug.printMessage(debug.LEVEL_INFO, "BRAILLE: disabled", True) return @@ -1130,8 +1130,8 @@ class Script(default.Script): def displayContents(self, contents, **args): """Displays contents in braille.""" - if not _settingsManager.getSetting('enableBraille') \ - and not _settingsManager.getSetting('enableBrailleMonitor'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableBraille') \ + and not cthulhu.cthulhuApp.settingsManager.getSetting('enableBrailleMonitor'): debug.printMessage(debug.LEVEL_INFO, "BRAILLE: disabled", True) return @@ -1206,7 +1206,7 @@ class Script(default.Script): def useCaretNavigationModel(self, keyboardEvent, debugOutput=True): """Returns True if caret navigation should be used.""" - if not _settingsManager.getSetting('caretNavigationEnabled'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('caretNavigationEnabled'): if debugOutput: msg = "WEB: Not using caret navigation: it's not enabled." debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -1377,12 +1377,12 @@ class Script(default.Script): self.refreshKeyGrabs() def toggleLayoutMode(self, inputEvent): - layoutMode = not _settingsManager.getSetting('layoutMode') + layoutMode = not cthulhu.cthulhuApp.settingsManager.getSetting('layoutMode') if layoutMode: self.presentMessage(messages.MODE_LAYOUT) else: self.presentMessage(messages.MODE_OBJECT) - _settingsManager.setSetting('layoutMode', layoutMode) + cthulhu.cthulhuApp.settingsManager.setSetting('layoutMode', layoutMode) def togglePresentationMode(self, inputEvent, documentFrame=None): [obj, characterOffset] = self.utilities.getCaretContext(documentFrame) @@ -1414,11 +1414,11 @@ class Script(default.Script): if inputEvent is not None: return False - if _settingsManager.getSetting('roleSoundPresentation') \ + if cthulhu.cthulhuApp.settingsManager.getSetting('roleSoundPresentation') \ == settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY: return False - if not _settingsManager.getSetting('enableSound'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableSound'): return False icon = self._getControlSoundIcon(obj) @@ -1798,7 +1798,7 @@ class Script(default.Script): tokens = ["WEB: Not presenting due to focus mode for", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) - if not _settingsManager.getSetting('onlySpeakDisplayedText') and shouldPresent: + if not cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText') and shouldPresent: if event.detail1: self.presentMessage(messages.PAGE_LOADING_START) elif AXObject.get_name(event.source): @@ -1828,7 +1828,7 @@ class Script(default.Script): debug.printMessage(debug.LEVEL_INFO, msg, True) return True - if _settingsManager.getSetting('pageSummaryOnLoad') and shouldPresent: + if cthulhu.cthulhuApp.settingsManager.getSetting('pageSummaryOnLoad') and shouldPresent: obj = obj or event.source tokens = ["WEB: Getting page summary for obj", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -1872,11 +1872,11 @@ class Script(default.Script): if self.utilities.documentFragment(event.source): msg = "WEB: Not doing SayAll due to page fragment" debug.printMessage(debug.LEVEL_INFO, msg, True) - elif not _settingsManager.getSetting('sayAllOnLoad'): + elif not cthulhu.cthulhuApp.settingsManager.getSetting('sayAllOnLoad'): msg = "WEB: Not doing SayAll due to sayAllOnLoad being False" debug.printMessage(debug.LEVEL_INFO, msg, True) self.speakContents(self.utilities.getLineContentsAtOffset(obj, offset)) - elif _settingsManager.getSetting('enableSpeech'): + elif cthulhu.cthulhuApp.settingsManager.getSetting('enableSpeech'): msg = "WEB: Doing SayAll" debug.printMessage(debug.LEVEL_INFO, msg, True) self.sayAll(None) diff --git a/src/cthulhu/scripts/web/script_utilities.py b/src/cthulhu/scripts/web/script_utilities.py index 9ebd461..efd647f 100644 --- a/src/cthulhu/scripts/web/script_utilities.py +++ b/src/cthulhu/scripts/web/script_utilities.py @@ -58,8 +58,8 @@ from cthulhu.ax_utilities import AXUtilities from cthulhu.ax_utilities_relation import AXUtilitiesRelation from cthulhu import speech_and_verbosity_manager -_scriptManager = script_manager.get_manager() -_settingsManager = settings_manager.getManager() +_scriptManager = None # Removed - use cthulhu.cthulhuApp.scriptManager +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class Utilities(script_utilities.Utilities): @@ -284,7 +284,7 @@ class Utilities(script_utilities.Utilities): # TODO - JD: Is this exception handling still needed? try: - script = _scriptManager.get_script(app, cthulhu_state.activeWindow) + script = cthulhu.cthulhuApp.scriptManager.get_script(app, cthulhu_state.activeWindow) tokens = ["WEB: Script for active Window is", script] debug.printTokens(debug.LEVEL_INFO, tokens, True) except Exception: @@ -1759,7 +1759,7 @@ class Utilities(script_utilities.Utilities): return self._currentLineContents if layoutMode is None: - layoutMode = _settingsManager.getSetting('layoutMode') or self._script.inFocusMode() + layoutMode = cthulhu.cthulhuApp.settingsManager.getSetting('layoutMode') or self._script.inFocusMode() objects = [] if offset > 0 and self.treatAsEndOfLine(obj, offset): @@ -5427,13 +5427,13 @@ class Utilities(script_utilities.Utilities): return None def handleAsLiveRegion(self, event): - if not _settingsManager.getSetting('inferLiveRegions'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('inferLiveRegions'): return False if not self.isLiveRegion(event.source): return False - if not _settingsManager.getSetting('presentLiveRegionFromInactiveTab') \ + if not cthulhu.cthulhuApp.settingsManager.getSetting('presentLiveRegionFromInactiveTab') \ and self.getTopLevelDocumentForObject(event.source) != self.activeDocument(): msg = "WEB: Live region source is not in active tab." debug.printMessage(debug.LEVEL_INFO, msg, True) diff --git a/src/cthulhu/scripts/web/sound_generator.py b/src/cthulhu/scripts/web/sound_generator.py index 8c34de3..ed2213e 100644 --- a/src/cthulhu/scripts/web/sound_generator.py +++ b/src/cthulhu/scripts/web/sound_generator.py @@ -35,10 +35,11 @@ import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi +from cthulhu import cthulhu from cthulhu import settings_manager from cthulhu import sound_generator -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class SoundGenerator(sound_generator.SoundGenerator): @@ -49,7 +50,7 @@ class SoundGenerator(sound_generator.SoundGenerator): def _generateClickable(self, obj, **args): """Returns an array of sounds indicating obj is clickable.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] if not self._script.utilities.inDocumentContent(obj): @@ -70,7 +71,7 @@ class SoundGenerator(sound_generator.SoundGenerator): def _generateHasLongDesc(self, obj, **args): """Returns an array of sounds indicating obj has a longdesc.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] if not self._script.utilities.inDocumentContent(obj): diff --git a/src/cthulhu/scripts/web/speech_generator.py b/src/cthulhu/scripts/web/speech_generator.py index 3f0c39f..6147ddf 100644 --- a/src/cthulhu/scripts/web/speech_generator.py +++ b/src/cthulhu/scripts/web/speech_generator.py @@ -36,6 +36,7 @@ gi.require_version("Atspi", "2.0") from gi.repository import Atspi import urllib +from cthulhu import cthulhu from cthulhu import debug from cthulhu import input_event_manager from cthulhu import messages @@ -49,7 +50,7 @@ from cthulhu.ax_object import AXObject from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class SpeechGenerator(speech_generator.SpeechGenerator): @@ -139,7 +140,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): return super()._generateAnyTextSelection(obj, **args) def _generateHasPopup(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if not self._script.utilities.inDocumentContent(obj): @@ -164,7 +165,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): return result def _generateClickable(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if not self._script.utilities.inDocumentContent(obj): @@ -185,7 +186,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): return [] def _generateDescription(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if not self._script.utilities.inDocumentContent(obj): @@ -218,7 +219,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): return super()._generateDescription(obj, **args) def _generateHasLongDesc(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if not self._script.utilities.inDocumentContent(obj): @@ -236,7 +237,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): return [] def _generateHasDetails(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if not self._script.utilities.inDocumentContent(obj): @@ -257,7 +258,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): return result def _generateAllDetails(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] objs = self._script.utilities.detailsIn(obj) @@ -288,7 +289,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): return result def _generateDetailsFor(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if not self._script.utilities.inDocumentContent(obj): @@ -447,7 +448,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): return super()._generateLabel(obj, **args) def _generateNewNodeLevel(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if self._script.utilities.isTextBlockElement(obj) \ @@ -457,7 +458,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): return super()._generateNewNodeLevel(obj, **args) def _generateLeaving(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if not args.get('leaving'): @@ -480,8 +481,8 @@ class SpeechGenerator(speech_generator.SpeechGenerator): return [] def _generateNumberOfChildren(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText') \ - or _settingsManager.getSetting('speechVerbosityLevel') == settings.VERBOSITY_LEVEL_BRIEF: + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText') \ + or cthulhu.cthulhuApp.settingsManager.getSetting('speechVerbosityLevel') == settings.VERBOSITY_LEVEL_BRIEF: return [] # We handle things even for non-document content due to issues in @@ -538,7 +539,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): return self._generateDisplayedText(rad, **args) def _generateRoleName(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if not self._script.utilities.inDocumentContent(obj): @@ -554,8 +555,8 @@ class SpeechGenerator(speech_generator.SpeechGenerator): result.extend(self.voice(speech_generator.SYSTEM, obj=obj, **args)) role = args.get('role', AXObject.get_role(obj)) - roleSoundPresentation = _settingsManager.getSetting('roleSoundPresentation') - soundEnabled = _settingsManager.getSetting('enableSound') + roleSoundPresentation = cthulhu.cthulhuApp.settingsManager.getSetting('roleSoundPresentation') + soundEnabled = cthulhu.cthulhuApp.settingsManager.getSetting('enableSound') roleSoundIcon = None if roleSoundPresentation != settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY \ and soundEnabled: @@ -677,7 +678,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY: return result - if not _settingsManager.getSetting('enableSound'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableSound'): return result if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY: @@ -775,7 +776,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): if not self._script.inFocusMode(): return result - if _settingsManager.getSetting('speakCellCoordinates'): + if cthulhu.cthulhuApp.settingsManager.getSetting('speakCellCoordinates'): label = self._script.utilities.labelForCellCoordinates(obj) if label: result.append(label) @@ -875,7 +876,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): if not result: if self._script.inSayAll(treatInterruptedAsIn=False) \ - or not _settingsManager.getSetting('speakBlankLines') \ + or not cthulhu.cthulhuApp.settingsManager.getSetting('speakBlankLines') \ or args.get('formatType') == 'ancestor': string = "" else: diff --git a/src/cthulhu/settings.py b/src/cthulhu/settings.py index d36bed5..26b9147 100644 --- a/src/cthulhu/settings.py +++ b/src/cthulhu/settings.py @@ -440,7 +440,6 @@ enabledBrailledTextAttributes = \ # and braille bindings. Unsupported and undocumented for now. # Use at your own risk. # -keyBindingsMap = {} brailleBindingsMap = {} # TODO - JD: Is this still needed now that AT-SPI has its own timeout? diff --git a/src/cthulhu/settings_manager.py b/src/cthulhu/settings_manager.py index 7c368ab..40f9b88 100644 --- a/src/cthulhu/settings_manager.py +++ b/src/cthulhu/settings_manager.py @@ -34,51 +34,42 @@ __license__ = "LGPL" import copy import importlib +import json import os from gi.repository import Gio, GLib from . import debug from . import cthulhu_i18n -from . import script_manager from . import settings from . import pronunciation_dict from .acss import ACSS from .ax_object import AXObject from .keybindings import KeyBinding -try: - _proxy = Gio.DBusProxy.new_for_bus_sync( - Gio.BusType.SESSION, - Gio.DBusProxyFlags.NONE, - None, - 'org.a11y.Bus', - '/org/a11y/bus', - 'org.freedesktop.DBus.Properties', - None) -except Exception: - _proxy = None - -_scriptManager = script_manager.get_manager() +# Removed global cthulhuApp.scriptManager declaration. +# Note: Do not import cthulhu module here to avoid circular import class SettingsManager(object): """Settings backend manager. This class manages cthulhu user's settings using different backends""" - _instance = None - def __new__(cls, *args, **kwargs): - if '__instance' not in vars(cls): - cls.__instance = object.__new__(cls, *args, **kwargs) - return cls.__instance - - def __init__(self, backend='json'): - """Initialize a SettingsManager Object. - If backend isn't defined then uses default backend, in this - case json-backend. - backend parameter can use the follow values: - backend='json' - """ + def __init__(self, app, backend='json'): # Modified signature debug.printMessage(debug.LEVEL_INFO, 'SETTINGS MANAGER: Initializing', True) + self.app = app # Store app instance + + # Move _proxy initialization here + try: + self._proxy = Gio.DBusProxy.new_for_bus_sync( + Gio.BusType.SESSION, + Gio.DBusProxyFlags.NONE, + None, + 'org.a11y.Bus', + '/org/a11y/bus', + 'org.freedesktop.DBus.Properties', + None) + except Exception: + self._proxy = None self.backendModule = None self._backend = None @@ -238,6 +229,64 @@ class SettingsManager(object): except Exception: pass self.defaultGeneral[key] = value + default_active_plugins = copy.deepcopy(self.defaultGeneral.get("activePlugins")) + overrides = self._load_default_general_overrides() + if overrides: + for key, value in overrides.items(): + if key not in self.defaultGeneral: + continue + if key == "activePlugins": + continue + self.defaultGeneral[key] = value + if default_active_plugins is not None: + self.defaultGeneral["activePlugins"] = default_active_plugins + + def _load_default_general_overrides(self): + if not self._prefsDir: + return {} + + settings_path = os.path.join(self._prefsDir, "user-settings.conf") + if not os.path.exists(settings_path): + return {} + + try: + with open(settings_path, "r", encoding="utf-8") as settings_file: + prefs = json.load(settings_file) + except Exception as error: + msg = f"SETTINGS MANAGER: Unable to read default settings from {settings_path}: {error}" + debug.printMessage(debug.LEVEL_WARNING, msg, True) + return {} + + general = prefs.get("general") + if not isinstance(general, dict): + return {} + + if hasattr(self._backend, "_migrateSettings"): + try: + general = self._backend._migrateSettings(dict(general)) + except Exception as error: + msg = f"SETTINGS MANAGER: Unable to migrate default settings: {error}" + debug.printMessage(debug.LEVEL_WARNING, msg, True) + general = dict(general) + else: + general = dict(general) + + voices = general.get("voices") + if isinstance(voices, dict): + converted_voices = {} + for voice_type, voice_def in voices.items(): + try: + converted_voices[voice_type] = ACSS(voice_def) + except Exception: + converted_voices[voice_type] = voice_def + general["voices"] = converted_voices + + return general + + def getDefaultSetting(self, settingName): + if settingName in self.defaultGeneral: + return self.defaultGeneral.get(settingName) + return getattr(settings, settingName, None) def _getCustomizedSettings(self): if self._customizationCompleted: @@ -372,11 +421,11 @@ class SettingsManager(object): debug.printMessage(debug.LEVEL_INFO, msg, True) msg = 'SETTINGS MANAGER: Accessibility enabled: ' - if not _proxy: + if not self._proxy: rv = False msg += 'Error (no proxy)' else: - rv = _proxy.Get('(ss)', 'org.a11y.Status', 'IsEnabled') + rv = self._proxy.Get('(ss)', 'org.a11y.Status', 'IsEnabled') msg += str(rv) debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -386,13 +435,13 @@ class SettingsManager(object): msg = f'SETTINGS MANAGER: Attempting to set accessibility to {enable}.' debug.printMessage(debug.LEVEL_INFO, msg, True) - if not _proxy: + if not self._proxy: msg = 'SETTINGS MANAGER: Error (no proxy)' debug.printMessage(debug.LEVEL_INFO, msg, True) return False vEnable = GLib.Variant('b', enable) - _proxy.Set('(ssv)', 'org.a11y.Status', 'IsEnabled', vEnable) + self._proxy.Set('(ssv)', 'org.a11y.Status', 'IsEnabled', vEnable) msg = f'SETTINGS MANAGER: Finished setting accessibility to {enable}.' debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -404,11 +453,11 @@ class SettingsManager(object): msg = 'SETTINGS MANAGER: Is screen reader service enabled? ' - if not _proxy: + if not self._proxy: rv = False msg += 'Error (no proxy)' else: - rv = _proxy.Get('(ss)', 'org.a11y.Status', 'ScreenReaderEnabled') + rv = self._proxy.Get('(ss)', 'org.a11y.Status', 'ScreenReaderEnabled') msg += str(rv) debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -695,7 +744,24 @@ class SettingsManager(object): self._setPronunciationsRuntime(self.pronunciations) script.keyBindings = self.overrideKeyBindings(script, script.getKeyBindings()) -_manager = SettingsManager() +_managerInstance = None def getManager(): - return _manager + """Get the settings manager instance. Compatibility function. + + This function provides backward compatibility for existing code that uses + settings_manager.getManager(). Returns None during initial import phase, + and the actual instance once cthulhuApp is initialized. + + Code should check for None or call this lazily when actually needed. + """ + global _managerInstance + if _managerInstance is None: + try: + from . import cthulhu + _managerInstance = cthulhu.cthulhuApp.settingsManager + except (ImportError, AttributeError): + # During import phase, cthulhuApp may not exist yet + pass + return _managerInstance + diff --git a/src/cthulhu/signal_manager.py b/src/cthulhu/signal_manager.py index 22ad931..51e61c2 100644 --- a/src/cthulhu/signal_manager.py +++ b/src/cthulhu/signal_manager.py @@ -74,10 +74,10 @@ class SignalManager(): if resourceContext: resourceContext.removeSubscriptionByFunction(function) return ok - def emitSignal(self, signalName): - # emit an signal + def emitSignal(self, signalName, *args): + # emit a signal with optional arguments try: - self.app.emit(signalName) + self.app.emit(signalName, *args) print('after Emit Signal: {}'.format(signalName)) except: print('Signal "{}" does not exist.'.format(signalName)) diff --git a/src/cthulhu/sound_generator.py b/src/cthulhu/sound_generator.py index cba7b22..9a3727f 100644 --- a/src/cthulhu/sound_generator.py +++ b/src/cthulhu/sound_generator.py @@ -37,12 +37,13 @@ from gi.repository import Atspi import os +from . import cthulhu # Need access to cthulhuApp from . import generator from . import settings_manager from .ax_object import AXObject from .ax_utilities import AXUtilities -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager METHOD_PREFIX = "_generate" @@ -79,7 +80,7 @@ class Tone: def __init__(self, duration, frequency, volumeMultiplier=1, wave=SINE_WAVE): self.duration = duration self.frequency = min(max(0, frequency), 20000) - self.volume = _settingsManager.getSetting('soundVolume') * volumeMultiplier + self.volume = cthulhu.cthulhuApp.settingsManager.getSetting('soundVolume') * volumeMultiplier self.wave = wave def __str__(self): @@ -91,7 +92,7 @@ class SoundGenerator(generator.Generator): def __init__(self, script): super().__init__(script, 'sound') - self._sounds = os.path.join(_settingsManager.getPrefsDir(), 'sounds') + self._sounds = os.path.join(cthulhu.cthulhuApp.settingsManager.getPrefsDir(), 'sounds') def _convertFilenameToIcon(self, filename): icon = Icon(self._sounds, filename) @@ -114,7 +115,7 @@ class SoundGenerator(generator.Generator): def _generateAvailability(self, obj, **args): """Returns an array of sounds indicating obj is grayed out.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] filenames = super()._generateAvailability(obj, **args) @@ -127,7 +128,7 @@ class SoundGenerator(generator.Generator): def _generateCheckedState(self, obj, **args): """Returns an array of sounds indicating the checked state of obj.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] filenames = super()._generateCheckedState(obj, **args) @@ -140,7 +141,7 @@ class SoundGenerator(generator.Generator): def _generateClickable(self, obj, **args): """Returns an array of sounds indicating obj is clickable.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] filenames = super()._generateClickable(obj, **args) @@ -153,7 +154,7 @@ class SoundGenerator(generator.Generator): def _generateExpandableState(self, obj, **args): """Returns an array of sounds indicating the expanded state of obj.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] filenames = super()._generateExpandableState(obj, **args) @@ -166,7 +167,7 @@ class SoundGenerator(generator.Generator): def _generateHasLongDesc(self, obj, **args): """Returns an array of sounds indicating obj has a longdesc.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] filenames = super()._generateHasLongDesc(obj, **args) @@ -179,7 +180,7 @@ class SoundGenerator(generator.Generator): def _generateMenuItemCheckedState(self, obj, **args): """Returns an array of sounds indicating the checked state of obj.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] filenames = super()._generateMenuItemCheckedState(obj, **args) @@ -192,7 +193,7 @@ class SoundGenerator(generator.Generator): def _generateMultiselectableState(self, obj, **args): """Returns an array of sounds indicating obj is multiselectable.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] filenames = super()._generateMultiselectableState(obj, **args) @@ -205,7 +206,7 @@ class SoundGenerator(generator.Generator): def _generateRadioState(self, obj, **args): """Returns an array of sounds indicating the selected state of obj.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] filenames = super()._generateRadioState(obj, **args) @@ -218,7 +219,7 @@ class SoundGenerator(generator.Generator): def _generateReadOnly(self, obj, **args): """Returns an array of sounds indicating obj is read only.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] filenames = super()._generateReadOnly(obj, **args) @@ -231,7 +232,7 @@ class SoundGenerator(generator.Generator): def _generateRequired(self, obj, **args): """Returns an array of sounds indicating obj is required.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] filenames = super()._generateRequired(obj, **args) @@ -244,7 +245,7 @@ class SoundGenerator(generator.Generator): def _generateSwitchState(self, obj, **args): """Returns an array of sounds indicating the on/off state of obj.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] filenames = super()._generateSwitchState(obj, **args) @@ -257,7 +258,7 @@ class SoundGenerator(generator.Generator): def _generateToggleState(self, obj, **args): """Returns an array of sounds indicating the toggled state of obj.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] filenames = super()._generateToggleState(obj, **args) @@ -270,7 +271,7 @@ class SoundGenerator(generator.Generator): def _generateVisitedState(self, obj, **args): """Returns an array of sounds indicating the visited state of obj.""" - if not _settingsManager.getSetting('playSoundForState'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForState'): return [] if not args.get('mode', None): @@ -294,7 +295,7 @@ class SoundGenerator(generator.Generator): def _generatePercentage(self, obj, **args): """Returns an array of sounds reflecting the percentage of obj.""" - if not _settingsManager.getSetting('playSoundForValue'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForValue'): return [] percent = self._script.utilities.getValueAsPercent(obj) @@ -309,7 +310,7 @@ class SoundGenerator(generator.Generator): if args.get('isProgressBarUpdate'): if not self._shouldPresentProgressBarUpdate(obj, **args): return [] - elif not _settingsManager.getSetting('playSoundForValue'): + elif not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForValue'): return [] percent = self._script.utilities.getValueAsPercent(obj) @@ -334,14 +335,14 @@ class SoundGenerator(generator.Generator): return [Tone(duration, frequency, volumeMultiplier, Tone.SINE_WAVE)] def _getProgressBarUpdateInterval(self): - interval = _settingsManager.getSetting('progressBarBeepInterval') + interval = cthulhu.cthulhuApp.settingsManager.getSetting('progressBarBeepInterval') if interval is None: return super()._getProgressBarUpdateInterval() return int(interval) def _shouldPresentProgressBarUpdate(self, obj, **args): - if not _settingsManager.getSetting('beepProgressBarUpdates'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('beepProgressBarUpdates'): return False return super()._shouldPresentProgressBarUpdate(obj, **args) @@ -355,7 +356,7 @@ class SoundGenerator(generator.Generator): def _generatePositionInSet(self, obj, **args): """Returns an array of sounds reflecting the set position of obj.""" - if not _settingsManager.getSetting('playSoundForPositionInSet'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForPositionInSet'): return [] # TODO: Implement the result. @@ -367,7 +368,7 @@ class SoundGenerator(generator.Generator): def _generateRoleName(self, obj, **args): """Returns an array of sounds indicating the role of obj.""" - if not _settingsManager.getSetting('playSoundForRole'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('playSoundForRole'): return [] role = args.get('role', AXObject.get_role(obj)) diff --git a/src/cthulhu/sound_theme_manager.py b/src/cthulhu/sound_theme_manager.py index 912f0f5..9c9680c 100644 --- a/src/cthulhu/sound_theme_manager.py +++ b/src/cthulhu/sound_theme_manager.py @@ -41,12 +41,9 @@ from gi.repository import GLib from gi.repository import Atspi from . import debug -from . import settings_manager from . import sound from .sound_generator import Icon -_settingsManager = settings_manager.getManager() - # Sound event constants - add new events here for easy extensibility SOUND_FOCUS_MODE = "focus_mode" SOUND_BROWSE_MODE = "browse_mode" @@ -84,18 +81,8 @@ THEME_NONE = "none" class SoundThemeManager: """Manages sound themes for Cthulhu.""" - _instance = None - - def __new__(cls): - if cls._instance is None: - cls._instance = super().__new__(cls) - cls._instance._initialized = False - return cls._instance - - def __init__(self): - if self._initialized: - return - self._initialized = True + def __init__(self, app): + self.app = app self._systemSoundsDir = None self._userSoundsDir = None @@ -283,7 +270,7 @@ class SoundThemeManager: def getRoleSoundIcon(self, role, themeName=None): """Return an Icon for the role sound from the current theme, if any.""" - themeName = themeName or _settingsManager.getSetting('soundTheme') or 'default' + themeName = themeName or self.app.getSettingsManager().getSetting('soundTheme') or 'default' if themeName == THEME_NONE: return None @@ -296,7 +283,7 @@ class SoundThemeManager: def getRoleStateSoundIcon(self, role, stateKey, themeName=None): """Return an Icon for the role/state sound from the current theme, if any.""" - themeName = themeName or _settingsManager.getSetting('soundTheme') or 'default' + themeName = themeName or self.app.getSettingsManager().getSetting('soundTheme') or 'default' if themeName == THEME_NONE: return None @@ -320,10 +307,10 @@ class SoundThemeManager: Returns: True if sound was played, False otherwise """ - if requireSoundSetting and not _settingsManager.getSetting('enableSound'): + if requireSoundSetting and not self.app.getSettingsManager().getSetting('enableSound'): return False - themeName = _settingsManager.getSetting('soundTheme') + themeName = self.app.getSettingsManager().getSetting('soundTheme') if not themeName: themeName = 'default' @@ -398,13 +385,14 @@ class SoundThemeManager: requireSoundSetting=True ) - _manager = None - - def getManager(): - """Get the singleton SoundThemeManager instance.""" + """Returns the Sound Theme Manager""" + global _manager if _manager is None: - _manager = SoundThemeManager() + from . import cthulhu + _manager = SoundThemeManager(cthulhu.cthulhuApp) return _manager + + diff --git a/src/cthulhu/speech.py b/src/cthulhu/speech.py index 44aa89b..b4d3248 100644 --- a/src/cthulhu/speech.py +++ b/src/cthulhu/speech.py @@ -46,8 +46,17 @@ from .speechserver import VoiceFamily from .acss import ACSS from . import speech_history -_logger = logger.getLogger() -log = _logger.newLog("speech") +# Lazy initialization to avoid circular imports +_logger = None +log = None + +def _ensureLogger(): + """Ensure logger is initialized.""" + global _logger, log + if _logger is None: + from . import cthulhu + _logger = cthulhu.cthulhuApp.logger + log = _logger.newLog("speech") # The speech server to use for all speech operations. # @@ -152,6 +161,7 @@ def __resolveACSS(acss=None): return ACSS(voices[settings.DEFAULT_VOICE]) def sayAll(utteranceIterator, progressCallback): + _ensureLogger() if settings.silenceSpeech: return if _speechserver: @@ -181,6 +191,8 @@ def sayAll(utteranceIterator, progressCallback): def _speak(text, acss, interrupt): """Speaks the individual string using the given ACSS.""" + _ensureLogger() + # Block speech for applications in sleep mode, except sleep mode status messages from . import cthulhu_state from . import sleep_mode_manager @@ -333,6 +345,8 @@ def speakKeyEvent(event, acss=None): - event: input_event.KeyboardEvent to speak. """ + _ensureLogger() + if settings.silenceSpeech: return @@ -364,6 +378,9 @@ def speakCharacter(character, acss=None): used to augment/override the default voice settings. """ + + _ensureLogger() + if settings.silenceSpeech: return diff --git a/src/cthulhu/speech_and_verbosity_manager.py b/src/cthulhu/speech_and_verbosity_manager.py index 2a54002..3feb57a 100644 --- a/src/cthulhu/speech_and_verbosity_manager.py +++ b/src/cthulhu/speech_and_verbosity_manager.py @@ -33,6 +33,7 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \ __license__ = "LGPL" from . import cmdnames +from . import cthulhu from . import dbus_service from . import debug from . import input_event @@ -45,12 +46,13 @@ from . import settings from . import settings_manager from . import speech -_settings_manager = settings_manager.getManager() +# Removed global _settings_manager class SpeechAndVerbosityManager: """Configures speech and verbosity settings.""" - def __init__(self): + def __init__(self, app): + self.app = app self._speech_settings_order = [ "rate", "pitch", @@ -740,12 +742,12 @@ class SpeechAndVerbosityManager: for key in settings.userCustomizableSettings: general[key] = getattr(settings, key) - current_profile = _settings_manager.profile - pronunciations = _settings_manager.getPronunciations(current_profile) - keybindings = _settings_manager.getKeybindings(current_profile) + current_profile = self.app.getSettingsManager().profile + pronunciations = self.app.getSettingsManager().getPronunciations(current_profile) + keybindings = self.app.getSettingsManager().getKeybindings(current_profile) - default_script = script_manager.get_manager().get_default_script() - _settings_manager.saveSettings(default_script, + default_script = self.app.getScriptManager().get_default_script() + self.app.getSettingsManager().saveSettings(default_script, general, pronunciations, keybindings) @@ -938,13 +940,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_speech_is_muted(self) -> bool: """Returns whether speech output is temporarily muted.""" - return _settings_manager.getSetting('silenceSpeech') + return self.app.getSettingsManager().getSetting('silenceSpeech') @dbus_service.setter def set_speech_is_muted(self, value: bool) -> bool: """Sets whether speech output is temporarily muted.""" try: - _settings_manager.setSetting('silenceSpeech', value) + cthulhu.cthulhuApp.settingsManager.setSetting('silenceSpeech', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting speech mute: {e}", True) @@ -953,7 +955,7 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_verbosity_level(self) -> str: """Returns the current speech verbosity level.""" - level = _settings_manager.getSetting('speechVerbosityLevel') + level = cthulhu.cthulhuApp.settingsManager.getSetting('speechVerbosityLevel') if level == settings.VERBOSITY_LEVEL_BRIEF: return "brief" else: @@ -964,9 +966,9 @@ class SpeechAndVerbosityManager: """Sets the speech verbosity level.""" try: if value.lower() == "brief": - _settings_manager.setSetting('speechVerbosityLevel', settings.VERBOSITY_LEVEL_BRIEF) + cthulhu.cthulhuApp.settingsManager.setSetting('speechVerbosityLevel', settings.VERBOSITY_LEVEL_BRIEF) elif value.lower() == "verbose": - _settings_manager.setSetting('speechVerbosityLevel', settings.VERBOSITY_LEVEL_VERBOSE) + cthulhu.cthulhuApp.settingsManager.setSetting('speechVerbosityLevel', settings.VERBOSITY_LEVEL_VERBOSE) else: return False return True @@ -977,13 +979,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_speak_numbers_as_digits(self) -> bool: """Returns whether numbers are spoken as digits.""" - return _settings_manager.getSetting('speakNumbersAsDigits') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakNumbersAsDigits') @dbus_service.setter def set_speak_numbers_as_digits(self, value: bool) -> bool: """Sets whether numbers are spoken as digits.""" try: - _settings_manager.setSetting('speakNumbersAsDigits', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakNumbersAsDigits', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak numbers as digits: {e}", True) @@ -992,13 +994,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_only_speak_displayed_text(self) -> bool: """Returns whether only displayed text should be spoken.""" - return _settings_manager.getSetting('onlySpeakDisplayedText') + return cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText') @dbus_service.setter def set_only_speak_displayed_text(self, value: bool) -> bool: """Sets whether only displayed text should be spoken.""" try: - _settings_manager.setSetting('onlySpeakDisplayedText', value) + cthulhu.cthulhuApp.settingsManager.setSetting('onlySpeakDisplayedText', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting only speak displayed text: {e}", True) @@ -1007,11 +1009,10 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_speak_indentation_and_justification(self) -> bool: """Returns whether speaking of indentation and justification is enabled.""" - return _settings_manager.getSetting('enableSpeechIndentation') + return cthulhu.cthulhuApp.settingsManager.getSetting('enableSpeechIndentation') - @staticmethod - def _sync_indentation_presentation_mode(enable_speech): - mode = _settings_manager.getSetting('indentationPresentationMode') \ + def _sync_indentation_presentation_mode(self, enable_speech): + mode = self.app.getSettingsManager().getSetting('indentationPresentationMode') \ or settings.indentationPresentationMode if enable_speech: if mode == settings.INDENTATION_PRESENTATION_OFF: @@ -1024,13 +1025,13 @@ class SpeechAndVerbosityManager: elif mode == settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS: mode = settings.INDENTATION_PRESENTATION_BEEPS - _settings_manager.setSetting('indentationPresentationMode', mode) + self.app.getSettingsManager().setSetting('indentationPresentationMode', mode) @dbus_service.setter def set_speak_indentation_and_justification(self, value: bool) -> bool: """Sets whether speaking of indentation and justification is enabled.""" try: - _settings_manager.setSetting('enableSpeechIndentation', value) + cthulhu.cthulhuApp.settingsManager.setSetting('enableSpeechIndentation', value) self._sync_indentation_presentation_mode(value) return True except Exception as e: @@ -1040,7 +1041,7 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_punctuation_level(self) -> str: """Returns the current punctuation level.""" - level = _settings_manager.getSetting('verbalizePunctuationStyle') + level = cthulhu.cthulhuApp.settingsManager.getSetting('verbalizePunctuationStyle') if level == settings.PUNCTUATION_STYLE_NONE: return "none" elif level == settings.PUNCTUATION_STYLE_SOME: @@ -1058,13 +1059,13 @@ class SpeechAndVerbosityManager: try: value_lower = value.lower() if value_lower == "none": - _settings_manager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_NONE) + cthulhu.cthulhuApp.settingsManager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_NONE) elif value_lower == "some": - _settings_manager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_SOME) + cthulhu.cthulhuApp.settingsManager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_SOME) elif value_lower == "most": - _settings_manager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_MOST) + cthulhu.cthulhuApp.settingsManager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_MOST) elif value_lower == "all": - _settings_manager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_ALL) + cthulhu.cthulhuApp.settingsManager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_ALL) else: return False return True @@ -1075,7 +1076,7 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_capitalization_style(self) -> str: """Returns the current capitalization style.""" - style = _settings_manager.getSetting('capitalizationStyle') + style = cthulhu.cthulhuApp.settingsManager.getSetting('capitalizationStyle') if style == settings.CAPITALIZATION_STYLE_NONE: return "none" elif style == settings.CAPITALIZATION_STYLE_ICON: @@ -1091,11 +1092,11 @@ class SpeechAndVerbosityManager: try: value_lower = value.lower() if value_lower == "none": - _settings_manager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_NONE) + cthulhu.cthulhuApp.settingsManager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_NONE) elif value_lower == "icon": - _settings_manager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_ICON) + cthulhu.cthulhuApp.settingsManager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_ICON) elif value_lower == "spell": - _settings_manager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_SPELL) + cthulhu.cthulhuApp.settingsManager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_SPELL) else: return False self.update_capitalization_style() @@ -1111,13 +1112,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_speak_misspelled_indicator(self) -> bool: """Returns whether the misspelled indicator is spoken.""" - return _settings_manager.getSetting('speakMisspelledIndicator') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakMisspelledIndicator') @dbus_service.setter def set_speak_misspelled_indicator(self, value: bool) -> bool: """Sets whether the misspelled indicator is spoken.""" try: - _settings_manager.setSetting('speakMisspelledIndicator', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakMisspelledIndicator', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak misspelled indicator: {e}", True) @@ -1126,13 +1127,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_speak_description(self) -> bool: """Returns whether object descriptions are spoken.""" - return _settings_manager.getSetting('speakDescription') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakDescription') @dbus_service.setter def set_speak_description(self, value: bool) -> bool: """Sets whether object descriptions are spoken.""" try: - _settings_manager.setSetting('speakDescription', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakDescription', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak description: {e}", True) @@ -1141,13 +1142,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_speak_position_in_set(self) -> bool: """Returns whether the position and set size of objects are spoken.""" - return _settings_manager.getSetting('enablePositionSpeaking') + return cthulhu.cthulhuApp.settingsManager.getSetting('enablePositionSpeaking') @dbus_service.setter def set_speak_position_in_set(self, value: bool) -> bool: """Sets whether the position and set size of objects are spoken.""" try: - _settings_manager.setSetting('enablePositionSpeaking', value) + cthulhu.cthulhuApp.settingsManager.setSetting('enablePositionSpeaking', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak position in set: {e}", True) @@ -1156,13 +1157,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_speak_widget_mnemonic(self) -> bool: """Returns whether widget mnemonics are spoken.""" - return _settings_manager.getSetting('enableMnemonicSpeaking') + return cthulhu.cthulhuApp.settingsManager.getSetting('enableMnemonicSpeaking') @dbus_service.setter def set_speak_widget_mnemonic(self, value: bool) -> bool: """Sets whether widget mnemonics are spoken.""" try: - _settings_manager.setSetting('enableMnemonicSpeaking', value) + cthulhu.cthulhuApp.settingsManager.setSetting('enableMnemonicSpeaking', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak widget mnemonic: {e}", True) @@ -1171,13 +1172,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_speak_tutorial_messages(self) -> bool: """Returns whether tutorial messages are spoken.""" - return _settings_manager.getSetting('enableTutorialMessages') + return cthulhu.cthulhuApp.settingsManager.getSetting('enableTutorialMessages') @dbus_service.setter def set_speak_tutorial_messages(self, value: bool) -> bool: """Sets whether tutorial messages are spoken.""" try: - _settings_manager.setSetting('enableTutorialMessages', value) + cthulhu.cthulhuApp.settingsManager.setSetting('enableTutorialMessages', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak tutorial messages: {e}", True) @@ -1186,13 +1187,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_insert_pauses_between_utterances(self) -> bool: """Returns whether pauses are inserted between utterances, e.g. between name and role.""" - return _settings_manager.getSetting('enablePauseBreaks') + return cthulhu.cthulhuApp.settingsManager.getSetting('enablePauseBreaks') @dbus_service.setter def set_insert_pauses_between_utterances(self, value: bool) -> bool: """Sets whether pauses are inserted between utterances, e.g. between name and role.""" try: - _settings_manager.setSetting('enablePauseBreaks', value) + cthulhu.cthulhuApp.settingsManager.setSetting('enablePauseBreaks', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting insert pauses: {e}", True) @@ -1201,13 +1202,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_repeated_character_limit(self) -> int: """Returns the count at which repeated, non-alphanumeric symbols will be described.""" - return _settings_manager.getSetting('repeatCharacterLimit') + return cthulhu.cthulhuApp.settingsManager.getSetting('repeatCharacterLimit') @dbus_service.setter def set_repeated_character_limit(self, value: int) -> bool: """Sets the count at which repeated, non-alphanumeric symbols will be described.""" try: - _settings_manager.setSetting('repeatCharacterLimit', value) + cthulhu.cthulhuApp.settingsManager.setSetting('repeatCharacterLimit', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting repeated character limit: {e}", True) @@ -1216,13 +1217,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_use_pronunciation_dictionary(self) -> bool: """Returns whether the user's pronunciation dictionary should be applied.""" - return _settings_manager.getSetting('usePronunciationDictionary') + return cthulhu.cthulhuApp.settingsManager.getSetting('usePronunciationDictionary') @dbus_service.setter def set_use_pronunciation_dictionary(self, value: bool) -> bool: """Sets whether the user's pronunciation dictionary should be applied.""" try: - _settings_manager.setSetting('usePronunciationDictionary', value) + cthulhu.cthulhuApp.settingsManager.setSetting('usePronunciationDictionary', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting use pronunciation dictionary: {e}", True) @@ -1231,13 +1232,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_messages_are_detailed(self) -> bool: """Returns whether informative messages will be detailed or brief.""" - return _settings_manager.getSetting('messagesAreDetailed') + return cthulhu.cthulhuApp.settingsManager.getSetting('messagesAreDetailed') @dbus_service.setter def set_messages_are_detailed(self, value: bool) -> bool: """Sets whether informative messages will be detailed or brief.""" try: - _settings_manager.setSetting('messagesAreDetailed', value) + cthulhu.cthulhuApp.settingsManager.setSetting('messagesAreDetailed', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting messages are detailed: {e}", True) @@ -1246,13 +1247,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_speak_indentation_only_if_changed(self) -> bool: """Returns whether indentation will be announced only if it has changed.""" - return _settings_manager.getSetting('speakIndentationOnlyIfChanged') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakIndentationOnlyIfChanged') @dbus_service.setter def set_speak_indentation_only_if_changed(self, value: bool) -> bool: """Sets whether indentation will be announced only if it has changed.""" try: - _settings_manager.setSetting('speakIndentationOnlyIfChanged', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakIndentationOnlyIfChanged', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak indentation only if changed: {e}", True) @@ -1265,13 +1266,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_speak_blank_lines(self) -> bool: """Returns whether blank lines will be spoken.""" - return _settings_manager.getSetting('speakBlankLines') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakBlankLines') @dbus_service.setter def set_speak_blank_lines(self, value: bool) -> bool: """Sets whether blank lines will be spoken.""" try: - _settings_manager.setSetting('speakBlankLines', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakBlankLines', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak blank lines: {e}", True) @@ -1280,13 +1281,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_speak_row_in_gui_table(self) -> bool: """Returns whether Up/Down in GUI tables speaks the row or just the cell.""" - return _settings_manager.getSetting('readFullRowInGUITable') + return cthulhu.cthulhuApp.settingsManager.getSetting('readFullRowInGUITable') @dbus_service.setter def set_speak_row_in_gui_table(self, value: bool) -> bool: """Sets whether Up/Down in GUI tables speaks the row or just the cell.""" try: - _settings_manager.setSetting('readFullRowInGUITable', value) + cthulhu.cthulhuApp.settingsManager.setSetting('readFullRowInGUITable', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak row in GUI table: {e}", True) @@ -1295,13 +1296,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_speak_row_in_document_table(self) -> bool: """Returns whether Up/Down in text-document tables speaks the row or just the cell.""" - return _settings_manager.getSetting('readFullRowInDocumentTable') + return cthulhu.cthulhuApp.settingsManager.getSetting('readFullRowInDocumentTable') @dbus_service.setter def set_speak_row_in_document_table(self, value: bool) -> bool: """Sets whether Up/Down in text-document tables speaks the row or just the cell.""" try: - _settings_manager.setSetting('readFullRowInDocumentTable', value) + cthulhu.cthulhuApp.settingsManager.setSetting('readFullRowInDocumentTable', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak row in document table: {e}", True) @@ -1310,13 +1311,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_speak_row_in_spreadsheet(self) -> bool: """Returns whether Up/Down in spreadsheets speaks the row or just the cell.""" - return _settings_manager.getSetting('readFullRowInSpreadSheet') + return cthulhu.cthulhuApp.settingsManager.getSetting('readFullRowInSpreadSheet') @dbus_service.setter def set_speak_row_in_spreadsheet(self, value: bool) -> bool: """Sets whether Up/Down in spreadsheets speaks the row or just the cell.""" try: - _settings_manager.setSetting('readFullRowInSpreadSheet', value) + cthulhu.cthulhuApp.settingsManager.setSetting('readFullRowInSpreadSheet', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak row in spreadsheet: {e}", True) @@ -1325,13 +1326,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_announce_cell_span(self) -> bool: """Returns whether cell spans are announced when greater than 1.""" - return _settings_manager.getSetting('speakCellSpan') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakCellSpan') @dbus_service.setter def set_announce_cell_span(self, value: bool) -> bool: """Sets whether cell spans are announced when greater than 1.""" try: - _settings_manager.setSetting('speakCellSpan', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakCellSpan', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce cell span: {e}", True) @@ -1340,13 +1341,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_announce_cell_coordinates(self) -> bool: """Returns whether (non-spreadsheet) cell coordinates are announced.""" - return _settings_manager.getSetting('speakCellCoordinates') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakCellCoordinates') @dbus_service.setter def set_announce_cell_coordinates(self, value: bool) -> bool: """Sets whether (non-spreadsheet) cell coordinates are announced.""" try: - _settings_manager.setSetting('speakCellCoordinates', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakCellCoordinates', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce cell coordinates: {e}", True) @@ -1355,13 +1356,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_announce_spreadsheet_cell_coordinates(self) -> bool: """Returns whether spreadsheet cell coordinates are announced.""" - return _settings_manager.getSetting('speakSpreadsheetCoordinates') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakSpreadsheetCoordinates') @dbus_service.setter def set_announce_spreadsheet_cell_coordinates(self, value: bool) -> bool: """Sets whether spreadsheet cell coordinates are announced.""" try: - _settings_manager.setSetting('speakSpreadsheetCoordinates', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakSpreadsheetCoordinates', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce spreadsheet cell coordinates: {e}", True) @@ -1370,13 +1371,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_always_announce_selected_range_in_spreadsheet(self) -> bool: """Returns whether the selected range in spreadsheets is always announced.""" - return _settings_manager.getSetting('alwaysSpeakSelectedSpreadsheetRange') + return cthulhu.cthulhuApp.settingsManager.getSetting('alwaysSpeakSelectedSpreadsheetRange') @dbus_service.setter def set_always_announce_selected_range_in_spreadsheet(self, value: bool) -> bool: """Sets whether the selected range in spreadsheets is always announced.""" try: - _settings_manager.setSetting('alwaysSpeakSelectedSpreadsheetRange', value) + cthulhu.cthulhuApp.settingsManager.setSetting('alwaysSpeakSelectedSpreadsheetRange', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting always announce selected range: {e}", True) @@ -1385,13 +1386,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_announce_cell_headers(self) -> bool: """Returns whether cell headers are announced.""" - return _settings_manager.getSetting('speakCellHeaders') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakCellHeaders') @dbus_service.setter def set_announce_cell_headers(self, value: bool) -> bool: """Sets whether cell headers are announced.""" try: - _settings_manager.setSetting('speakCellHeaders', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakCellHeaders', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce cell headers: {e}", True) @@ -1404,13 +1405,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_announce_blockquote(self) -> bool: """Returns whether blockquotes are announced when entered.""" - return _settings_manager.getSetting('speakContextBlockquote') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakContextBlockquote') @dbus_service.setter def set_announce_blockquote(self, value: bool) -> bool: """Sets whether blockquotes are announced when entered.""" try: - _settings_manager.setSetting('speakContextBlockquote', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakContextBlockquote', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce blockquote: {e}", True) @@ -1419,13 +1420,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_announce_form(self) -> bool: """Returns whether non-landmark forms are announced when entered.""" - return _settings_manager.getSetting('speakContextNonLandmarkForm') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakContextNonLandmarkForm') @dbus_service.setter def set_announce_form(self, value: bool) -> bool: """Sets whether non-landmark forms are announced when entered.""" try: - _settings_manager.setSetting('speakContextNonLandmarkForm', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakContextNonLandmarkForm', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce form: {e}", True) @@ -1434,13 +1435,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_announce_grouping(self) -> bool: """Returns whether groupings are announced when entered.""" - return _settings_manager.getSetting('speakContextPanel') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakContextPanel') @dbus_service.setter def set_announce_grouping(self, value: bool) -> bool: """Sets whether groupings are announced when entered.""" try: - _settings_manager.setSetting('speakContextPanel', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakContextPanel', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce grouping: {e}", True) @@ -1449,13 +1450,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_announce_landmark(self) -> bool: """Returns whether landmarks are announced when entered.""" - return _settings_manager.getSetting('speakContextLandmark') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakContextLandmark') @dbus_service.setter def set_announce_landmark(self, value: bool) -> bool: """Sets whether landmarks are announced when entered.""" try: - _settings_manager.setSetting('speakContextLandmark', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakContextLandmark', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce landmark: {e}", True) @@ -1464,13 +1465,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_announce_list(self) -> bool: """Returns whether lists are announced when entered.""" - return _settings_manager.getSetting('speakContextList') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakContextList') @dbus_service.setter def set_announce_list(self, value: bool) -> bool: """Sets whether lists are announced when entered.""" try: - _settings_manager.setSetting('speakContextList', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakContextList', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce list: {e}", True) @@ -1479,13 +1480,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_announce_table(self) -> bool: """Returns whether tables are announced when entered.""" - return _settings_manager.getSetting('speakContextTable') + return cthulhu.cthulhuApp.settingsManager.getSetting('speakContextTable') @dbus_service.setter def set_announce_table(self, value: bool) -> bool: """Sets whether tables are announced when entered.""" try: - _settings_manager.setSetting('speakContextTable', value) + cthulhu.cthulhuApp.settingsManager.setSetting('speakContextTable', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting announce table: {e}", True) @@ -1494,13 +1495,13 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_use_color_names(self) -> bool: """Returns whether colors are announced by name or as RGB values.""" - return _settings_manager.getSetting('useColorNames') + return cthulhu.cthulhuApp.settingsManager.getSetting('useColorNames') @dbus_service.setter def set_use_color_names(self, value: bool) -> bool: """Sets whether colors are announced by name or as RGB values.""" try: - _settings_manager.setSetting('useColorNames', value) + cthulhu.cthulhuApp.settingsManager.setSetting('useColorNames', value) return True except Exception as e: debug.printMessage(debug.LEVEL_WARNING, f"Error setting use color names: {e}", True) @@ -1559,7 +1560,7 @@ class SpeechAndVerbosityManager: def set_speech_is_enabled(self, value: bool) -> bool: """Sets whether the speech server is enabled. See also is-muted.""" try: - _settings_manager.setSetting('enableSpeech', value) + cthulhu.cthulhuApp.settingsManager.setSetting('enableSpeech', value) if value: self.start_speech() else: @@ -1572,7 +1573,7 @@ class SpeechAndVerbosityManager: @dbus_service.getter def get_speech_is_enabled(self) -> bool: """Returns whether the speech server is enabled. See also is-muted.""" - return _settings_manager.getSetting('enableSpeech') + return cthulhu.cthulhuApp.settingsManager.getSetting('enableSpeech') @dbus_service.command def decrease_rate(self, script=None, event=None): @@ -1723,7 +1724,7 @@ class SpeechAndVerbosityManager: def cycle_capitalization_style(self, script=None, event=None): """Cycle through the speech-dispatcher capitalization styles.""" - current_style = _settings_manager.getSetting('capitalizationStyle') + current_style = cthulhu.cthulhuApp.settingsManager.getSetting('capitalizationStyle') if current_style == settings.CAPITALIZATION_STYLE_NONE: new_style = settings.CAPITALIZATION_STYLE_SPELL full = messages.CAPITALIZATION_SPELL_FULL @@ -1737,7 +1738,7 @@ class SpeechAndVerbosityManager: full = messages.CAPITALIZATION_NONE_FULL brief = messages.CAPITALIZATION_NONE_BRIEF - _settings_manager.setSetting('capitalizationStyle', new_style) + cthulhu.cthulhuApp.settingsManager.setSetting('capitalizationStyle', new_style) if script: script.presentMessage(full, brief) self.update_capitalization_style() @@ -1747,7 +1748,7 @@ class SpeechAndVerbosityManager: def cycle_punctuation_level(self, script=None, event=None): """Cycle through the punctuation levels for speech.""" - current_level = _settings_manager.getSetting('verbalizePunctuationStyle') + current_level = cthulhu.cthulhuApp.settingsManager.getSetting('verbalizePunctuationStyle') if current_level == settings.PUNCTUATION_STYLE_NONE: new_level = settings.PUNCTUATION_STYLE_SOME full = messages.PUNCTUATION_SOME_FULL @@ -1765,7 +1766,7 @@ class SpeechAndVerbosityManager: full = messages.PUNCTUATION_NONE_FULL brief = messages.PUNCTUATION_NONE_BRIEF - _settings_manager.setSetting('verbalizePunctuationStyle', new_level) + cthulhu.cthulhuApp.settingsManager.setSetting('verbalizePunctuationStyle', new_level) if script: script.presentMessage(full, brief) self.update_punctuation_level() @@ -1775,9 +1776,9 @@ class SpeechAndVerbosityManager: def cycle_key_echo(self, script=None, event=None): """Cycle through the key echo levels.""" (new_key, new_word, new_sentence) = (False, False, False) - key = _settings_manager.getSetting('enableKeyEcho') - word = _settings_manager.getSetting('enableEchoByWord') - sentence = _settings_manager.getSetting('enableEchoBySentence') + key = cthulhu.cthulhuApp.settingsManager.getSetting('enableKeyEcho') + word = cthulhu.cthulhuApp.settingsManager.getSetting('enableEchoByWord') + sentence = cthulhu.cthulhuApp.settingsManager.getSetting('enableEchoBySentence') if (key, word, sentence) == (False, False, False): (new_key, new_word, new_sentence) = (True, False, False) @@ -1804,9 +1805,9 @@ class SpeechAndVerbosityManager: full = messages.KEY_ECHO_NONE_FULL brief = messages.KEY_ECHO_NONE_BRIEF - _settings_manager.setSetting('enableKeyEcho', new_key) - _settings_manager.setSetting('enableEchoByWord', new_word) - _settings_manager.setSetting('enableEchoBySentence', new_sentence) + cthulhu.cthulhuApp.settingsManager.setSetting('enableKeyEcho', new_key) + cthulhu.cthulhuApp.settingsManager.setSetting('enableEchoByWord', new_word) + cthulhu.cthulhuApp.settingsManager.setSetting('enableEchoBySentence', new_sentence) if script: script.presentMessage(full, brief) return True @@ -1815,7 +1816,7 @@ class SpeechAndVerbosityManager: def change_number_style(self, script=None, event=None): """Changes spoken number style between digits and words.""" - speak_digits = _settings_manager.getSetting('speakNumbersAsDigits') + speak_digits = cthulhu.cthulhuApp.settingsManager.getSetting('speakNumbersAsDigits') if speak_digits: brief = messages.NUMBER_STYLE_WORDS_BRIEF full = messages.NUMBER_STYLE_WORDS_FULL @@ -1823,7 +1824,7 @@ class SpeechAndVerbosityManager: brief = messages.NUMBER_STYLE_DIGITS_BRIEF full = messages.NUMBER_STYLE_DIGITS_FULL - _settings_manager.setSetting('speakNumbersAsDigits', not speak_digits) + cthulhu.cthulhuApp.settingsManager.setSetting('speakNumbersAsDigits', not speak_digits) if script: script.presentMessage(full, brief) return True @@ -1833,39 +1834,39 @@ class SpeechAndVerbosityManager: """Toggles speech.""" script.presentationInterrupt() - if _settings_manager.getSetting('silenceSpeech'): - _settings_manager.setSetting('silenceSpeech', False) + if self.app.getSettingsManager().getSetting('silenceSpeech'): + self.app.getSettingsManager().setSetting('silenceSpeech', False) script.presentMessage(messages.SPEECH_ENABLED) - elif not _settings_manager.getSetting('enableSpeech'): - _settings_manager.setSetting('enableSpeech', True) + elif not self.app.getSettingsManager().getSetting('enableSpeech'): + self.app.getSettingsManager().setSetting('enableSpeech', True) speech.init() script.presentMessage(messages.SPEECH_ENABLED) else: script.presentMessage(messages.SPEECH_DISABLED) - _settings_manager.setSetting('silenceSpeech', True) + self.app.getSettingsManager().setSetting('silenceSpeech', True) return True @dbus_service.command def toggle_verbosity(self, script, event=None): """Toggles speech verbosity level between verbose and brief.""" - value = _settings_manager.getSetting('speechVerbosityLevel') + value = self.app.getSettingsManager().getSetting('speechVerbosityLevel') if value == settings.VERBOSITY_LEVEL_BRIEF: script.presentMessage(messages.SPEECH_VERBOSITY_VERBOSE) - _settings_manager.setSetting('speechVerbosityLevel', settings.VERBOSITY_LEVEL_VERBOSE) + self.app.getSettingsManager().setSetting('speechVerbosityLevel', settings.VERBOSITY_LEVEL_VERBOSE) else: script.presentMessage(messages.SPEECH_VERBOSITY_BRIEF) - _settings_manager.setSetting('speechVerbosityLevel', settings.VERBOSITY_LEVEL_BRIEF) + self.app.getSettingsManager().setSetting('speechVerbosityLevel', settings.VERBOSITY_LEVEL_BRIEF) return True @dbus_service.command def toggle_indentation_and_justification(self, script=None, event=None): """Toggles the speaking of indentation and justification.""" - value = _settings_manager.getSetting('enableSpeechIndentation') - _settings_manager.setSetting('enableSpeechIndentation', not value) + value = self.app.getSettingsManager().getSetting('enableSpeechIndentation') + self.app.getSettingsManager().setSetting('enableSpeechIndentation', not value) self._sync_indentation_presentation_mode(not value) - if _settings_manager.getSetting('enableSpeechIndentation'): + if self.app.getSettingsManager().getSetting('enableSpeechIndentation'): full = messages.INDENTATION_JUSTIFICATION_ON_FULL brief = messages.INDENTATION_JUSTIFICATION_ON_BRIEF else: @@ -1894,8 +1895,8 @@ class SpeechAndVerbosityManager: else: setting_name = 'readFullRowInDocumentTable' - speak_row = _settings_manager.getSetting(setting_name) - _settings_manager.setSetting(setting_name, not speak_row) + speak_row = cthulhu.cthulhuApp.settingsManager.getSetting(setting_name) + cthulhu.cthulhuApp.settingsManager.setSetting(setting_name, not speak_row) if not speak_row: msg = messages.TABLE_MODE_ROW @@ -1908,8 +1909,9 @@ class SpeechAndVerbosityManager: _manager = None def getManager(): """Returns the Speech and Verbosity Manager""" - + global _manager if _manager is None: - _manager = SpeechAndVerbosityManager() + from . import cthulhu + _manager = SpeechAndVerbosityManager(cthulhu.cthulhuApp) return _manager diff --git a/src/cthulhu/speech_generator.py b/src/cthulhu/speech_generator.py index a651600..2f39be6 100644 --- a/src/cthulhu/speech_generator.py +++ b/src/cthulhu/speech_generator.py @@ -43,6 +43,7 @@ import urllib.parse from . import acss from . import chnames +from . import cthulhu from . import debug from . import generator from . import messages @@ -105,7 +106,7 @@ voiceType = { VALUE: settings.SYSTEM_VOICE, # Users may prefer DEFAULT_VOICE here } -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class SpeechGenerator(generator.Generator): """Takes accessible objects and produces a string to speak for @@ -164,7 +165,7 @@ class SpeechGenerator(generator.Generator): role = args.get('role', AXObject.get_role(obj)) if role == Atspi.Role.LAYERED_PANE \ - and _settingsManager.getSetting('onlySpeakDisplayedText'): + and cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = generator.Generator._generateName(self, obj, **args) @@ -226,11 +227,11 @@ class SpeechGenerator(generator.Generator): if not name: return name - roleSoundPresentation = _settingsManager.getSetting('roleSoundPresentation') + roleSoundPresentation = cthulhu.cthulhuApp.settingsManager.getSetting('roleSoundPresentation') if roleSoundPresentation != settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY: return name - if not _settingsManager.getSetting('enableSound'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableSound'): return name if not self._shouldStripRoleFromName(obj): @@ -351,13 +352,13 @@ class SpeechGenerator(generator.Generator): if alreadyUsed: return [] - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] - if not _settingsManager.getSetting('speakDescription') and not args.get('alerttext'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('speakDescription') and not args.get('alerttext'): return [] - if args.get('inMouseReview') and not _settingsManager.getSetting('presentToolTips'): + if args.get('inMouseReview') and not cthulhu.cthulhuApp.settingsManager.getSetting('presentToolTips'): return [] priorObj = args.get('priorObj') @@ -376,10 +377,10 @@ class SpeechGenerator(generator.Generator): """Returns an array of strings for use by speech and braille that represent the description of the image on the object.""" - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] - if not _settingsManager.getSetting('speakDescription'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('speakDescription'): return [] result = generator.Generator._generateImageDescription(self, obj, **args) @@ -399,8 +400,8 @@ class SpeechGenerator(generator.Generator): return result def _generateHasPopup(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText') \ - or _settingsManager.getSetting('speechVerbosityLevel') \ + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText') \ + or cthulhu.cthulhuApp.settingsManager.getSetting('speechVerbosityLevel') \ == settings.VERBOSITY_LEVEL_BRIEF: return [] @@ -410,8 +411,8 @@ class SpeechGenerator(generator.Generator): return result def _generateClickable(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText') \ - or _settingsManager.getSetting('speechVerbosityLevel') \ + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText') \ + or cthulhu.cthulhuApp.settingsManager.getSetting('speechVerbosityLevel') \ == settings.VERBOSITY_LEVEL_BRIEF: return [] @@ -421,7 +422,7 @@ class SpeechGenerator(generator.Generator): return result def _generateHasLongDesc(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = generator.Generator._generateHasLongDesc(self, obj, **args) @@ -430,7 +431,7 @@ class SpeechGenerator(generator.Generator): return result def _generateHasDetails(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = generator.Generator._generateHasDetails(self, obj, **args) @@ -439,7 +440,7 @@ class SpeechGenerator(generator.Generator): return result def _generateDetailsFor(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = generator.Generator._generateDetailsFor(self, obj, **args) @@ -448,7 +449,7 @@ class SpeechGenerator(generator.Generator): return result def _generateAllDetails(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = generator.Generator._generateAllDetails(self, obj, **args) @@ -457,7 +458,7 @@ class SpeechGenerator(generator.Generator): return result def _generateDeletionStart(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] startOffset = args.get('startOffset', 0) @@ -475,7 +476,7 @@ class SpeechGenerator(generator.Generator): return result def _generateDeletionEnd(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] endOffset = args.get('endOffset') @@ -500,7 +501,7 @@ class SpeechGenerator(generator.Generator): return result def _generateInsertionStart(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] startOffset = args.get('startOffset', 0) @@ -518,7 +519,7 @@ class SpeechGenerator(generator.Generator): return result def _generateInsertionEnd(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] endOffset = args.get('endOffset') @@ -543,7 +544,7 @@ class SpeechGenerator(generator.Generator): return result def _generateMarkStart(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] startOffset = args.get('startOffset', 0) @@ -562,7 +563,7 @@ class SpeechGenerator(generator.Generator): return result def _generateMarkEnd(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] endOffset = args.get('endOffset') @@ -576,7 +577,7 @@ class SpeechGenerator(generator.Generator): return result def _generateAvailability(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = generator.Generator._generateAvailability(self, obj, **args) @@ -585,7 +586,7 @@ class SpeechGenerator(generator.Generator): return result def _generateInvalid(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = generator.Generator._generateInvalid(self, obj, **args) @@ -594,7 +595,7 @@ class SpeechGenerator(generator.Generator): return result def _generateRequired(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = generator.Generator._generateRequired(self, obj, **args) @@ -603,7 +604,7 @@ class SpeechGenerator(generator.Generator): return result def _generateTable(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if args.get("leaving"): @@ -615,7 +616,7 @@ class SpeechGenerator(generator.Generator): if role in disabled: return [] - if _settingsManager.getSetting('speechVerbosityLevel') == settings.VERBOSITY_LEVEL_BRIEF: + if cthulhu.cthulhuApp.settingsManager.getSetting('speechVerbosityLevel') == settings.VERBOSITY_LEVEL_BRIEF: return self._generateRoleName(obj, **args) result = generator.Generator._generateTable(self, obj, **args) @@ -635,7 +636,7 @@ class SpeechGenerator(generator.Generator): of a speech generator that we can update and the user can override.]]] """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = [] @@ -655,7 +656,7 @@ class SpeechGenerator(generator.Generator): if obj == args.get("priorObj"): return [] - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if self._script.utilities.isStatusBarNotification(obj): @@ -669,8 +670,8 @@ class SpeechGenerator(generator.Generator): result = [] role = args.get('role', AXObject.get_role(obj)) - roleSoundPresentation = _settingsManager.getSetting('roleSoundPresentation') - soundEnabled = _settingsManager.getSetting('enableSound') + roleSoundPresentation = cthulhu.cthulhuApp.settingsManager.getSetting('roleSoundPresentation') + soundEnabled = cthulhu.cthulhuApp.settingsManager.getSetting('enableSound') roleSoundIcon = None if roleSoundPresentation != settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY \ and soundEnabled: @@ -710,7 +711,7 @@ class SpeechGenerator(generator.Generator): if self._script.utilities.isStatusBarDescendant(obj): doNotPresent.append(Atspi.Role.LABEL) - if _settingsManager.getSetting('speechVerbosityLevel') \ + if cthulhu.cthulhuApp.settingsManager.getSetting('speechVerbosityLevel') \ == settings.VERBOSITY_LEVEL_BRIEF: doNotPresent.extend([Atspi.Role.ICON, Atspi.Role.CANVAS]) @@ -812,11 +813,11 @@ class SpeechGenerator(generator.Generator): if not result: return result - roleSoundPresentation = _settingsManager.getSetting('roleSoundPresentation') + roleSoundPresentation = cthulhu.cthulhuApp.settingsManager.getSetting('roleSoundPresentation') if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY: return result - if not _settingsManager.getSetting('enableSound'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableSound'): return result icon = sound_theme_manager.getManager().getRoleStateSoundIcon(role, stateKey) @@ -834,7 +835,7 @@ class SpeechGenerator(generator.Generator): for check boxes. [[[WDW - should we return an empty array if we can guarantee we know this thing is not checkable?]]] """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = generator.Generator._generateCheckedState(self, obj, **args) @@ -858,7 +859,7 @@ class SpeechGenerator(generator.Generator): tree node. If the object is not expandable, an empty array will be returned. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = generator.Generator._generateExpandableState(self, obj, **args) @@ -867,7 +868,7 @@ class SpeechGenerator(generator.Generator): return result def _generateCheckedStateIfCheckable(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = super()._generateCheckedStateIfCheckable(obj, **args) @@ -880,7 +881,7 @@ class SpeechGenerator(generator.Generator): represent the checked state of the menu item, only if it is checked. Otherwise, and empty array will be returned. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = generator.Generator.\ @@ -900,7 +901,7 @@ class SpeechGenerator(generator.Generator): the object. This is typically for list boxes. If the object is not multiselectable, an empty array will be returned. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = super()._generateMultiselectableState(obj, **args) @@ -914,7 +915,7 @@ class SpeechGenerator(generator.Generator): for check boxes. [[[WDW - should we return an empty array if we can guarantee we know this thing is not checkable?]]] """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = generator.Generator._generateRadioState(self, obj, **args) @@ -927,7 +928,7 @@ class SpeechGenerator(generator.Generator): def _generateSwitchState(self, obj, **args): """Returns an array of strings indicating the on/off state of obj.""" - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = generator.Generator._generateSwitchState(self, obj, **args) @@ -945,7 +946,7 @@ class SpeechGenerator(generator.Generator): for check boxes. [[[WDW - should we return an empty array if we can guarantee we know this thing is not checkable?]]] """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = generator.Generator._generateToggleState(self, obj, **args) @@ -1149,7 +1150,7 @@ class SpeechGenerator(generator.Generator): if args.get('readingRow'): return [] - if not _settingsManager.getSetting('speakCellHeaders'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('speakCellHeaders'): return [] if args.get('inMouseReview') and args.get('priorObj'): @@ -1178,7 +1179,7 @@ class SpeechGenerator(generator.Generator): if args.get('readingRow'): return [] - if not _settingsManager.getSetting('speakCellHeaders'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('speakCellHeaders'): return [] if args.get('inMouseReview') and args.get('priorObj'): @@ -1202,7 +1203,7 @@ class SpeechGenerator(generator.Generator): result.extend(self.generate(obj, **args)) self._restoreRole(oldRole, args) if not (result and result[0]) \ - and _settingsManager.getSetting('speakBlankLines') \ + and cthulhu.cthulhuApp.settingsManager.getSetting('speakBlankLines') \ and not args.get('readingRow', False) \ and args.get('formatType') != 'ancestor': result.append(messages.BLANK) @@ -1212,7 +1213,7 @@ class SpeechGenerator(generator.Generator): return result def _generateUnselectedStateIfSelectable(self, obj, **args): - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if args.get('inMouseReview'): @@ -1240,7 +1241,7 @@ class SpeechGenerator(generator.Generator): returned. [[[WDW - I wonder if this string should be moved to settings.py.]]] """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if args.get('inMouseReview'): @@ -1284,7 +1285,7 @@ class SpeechGenerator(generator.Generator): if args.get('readingRow'): return [] - if not _settingsManager.getSetting('speakCellCoordinates'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('speakCellCoordinates'): return [] return self._generateColumn(obj, **args) @@ -1293,7 +1294,7 @@ class SpeechGenerator(generator.Generator): """Returns an array of strings (and possibly voice and audio specifications) reflecting the column number of a cell. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = [] @@ -1317,7 +1318,7 @@ class SpeechGenerator(generator.Generator): if args.get('readingRow'): return [] - if not _settingsManager.getSetting('speakCellCoordinates'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('speakCellCoordinates'): return [] return self._generateRow(obj, **args) @@ -1326,7 +1327,7 @@ class SpeechGenerator(generator.Generator): """Returns an array of strings (and possibly voice and audio specifications) reflecting the row number of a cell. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = [] @@ -1349,7 +1350,7 @@ class SpeechGenerator(generator.Generator): of its column number, the total number of columns, its row, and the total number of rows. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = [] @@ -1376,10 +1377,10 @@ class SpeechGenerator(generator.Generator): specifications) indicating that this cell is the last cell in the table. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] - if _settingsManager.getSetting('speechVerbosityLevel') \ + if cthulhu.cthulhuApp.settingsManager.getSetting('speechVerbosityLevel') \ != settings.VERBOSITY_LEVEL_VERBOSE: return [] @@ -1412,7 +1413,7 @@ class SpeechGenerator(generator.Generator): return result [text, caretOffset, startOffset] = self._script.getTextLineAtCaret(obj) - if text == '\n' and _settingsManager.getSetting('speakBlankLines') \ + if text == '\n' and cthulhu.cthulhuApp.settingsManager.getSetting('speakBlankLines') \ and not self._script.inSayAll() and args.get('total', 1) == 1 \ and args.get('formatType') != 'ancestor': result = [messages.BLANK] @@ -1628,7 +1629,7 @@ class SpeechGenerator(generator.Generator): object is selected. [[[WDW - I wonder if this string should be moved to settings.py.]]] """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if not AXObject.supports_text(obj): @@ -1648,7 +1649,7 @@ class SpeechGenerator(generator.Generator): object is selected. [[[WDW - I wonder if this string should be moved to settings.py.]]] """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = [] @@ -1667,7 +1668,7 @@ class SpeechGenerator(generator.Generator): return [] result.extend(self.voice(DEFAULT, obj=obj, **args)) - if result[0] in ['\n', ''] and _settingsManager.getSetting('speakBlankLines') \ + if result[0] in ['\n', ''] and cthulhu.cthulhuApp.settingsManager.getSetting('speakBlankLines') \ and not self._script.inSayAll() and args.get('total', 1) == 1 \ and args.get('formatType') != 'ancestor': result[0] = messages.BLANK @@ -1685,7 +1686,7 @@ class SpeechGenerator(generator.Generator): - obj: the text object. """ - if not _settingsManager.getSetting('enableSpeechIndentation'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('enableSpeechIndentation'): return [] line, caretOffset, startOffset = self._script.getTextLineAtCaret(obj) @@ -1720,7 +1721,7 @@ class SpeechGenerator(generator.Generator): is typically set by Cthulhu to be the previous object with focus. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = [] @@ -1750,7 +1751,7 @@ class SpeechGenerator(generator.Generator): object. This is typically for progress bars. [[[WDW - we should consider returning an empty array if there is no value. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] percentValue = self._script.utilities.getValueAsPercent(obj) @@ -1815,8 +1816,8 @@ class SpeechGenerator(generator.Generator): be moved to settings.py.]]] """ - if _settingsManager.getSetting('onlySpeakDisplayedText') \ - or _settingsManager.getSetting('speechVerbosityLevel') == settings.VERBOSITY_LEVEL_BRIEF: + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText') \ + or cthulhu.cthulhuApp.settingsManager.getSetting('speechVerbosityLevel') == settings.VERBOSITY_LEVEL_BRIEF: return [] result = [] @@ -1847,7 +1848,7 @@ class SpeechGenerator(generator.Generator): apply?]]] [[[WDW - I wonder if this string should be moved to settings.py.]]] """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = [] @@ -1865,7 +1866,7 @@ class SpeechGenerator(generator.Generator): apply?]]] [[[WDW - I wonder if this string should be moved to settings.py.]]] """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = [] @@ -1899,7 +1900,7 @@ class SpeechGenerator(generator.Generator): panel or a layered pane. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] container = obj @@ -1927,7 +1928,7 @@ class SpeechGenerator(generator.Generator): This object will be an icon panel or a layered pane. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] container = obj @@ -1949,7 +1950,7 @@ class SpeechGenerator(generator.Generator): [[[WDW - I wonder if this string should be moved to settings.py.]]] """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = [] @@ -1987,15 +1988,15 @@ class SpeechGenerator(generator.Generator): enabled, disabled = [], [] if self._script.inSayAll(): - if _settingsManager.getSetting('sayAllContextBlockquote'): + if cthulhu.cthulhuApp.settingsManager.getSetting('sayAllContextBlockquote'): enabled.append(Atspi.Role.BLOCK_QUOTE) - if _settingsManager.getSetting('sayAllContextLandmark'): + if cthulhu.cthulhuApp.settingsManager.getSetting('sayAllContextLandmark'): enabled.extend([Atspi.Role.LANDMARK, 'ROLE_DPUB_LANDMARK']) - if _settingsManager.getSetting('sayAllContextList'): + if cthulhu.cthulhuApp.settingsManager.getSetting('sayAllContextList'): enabled.append(Atspi.Role.LIST) enabled.append(Atspi.Role.DESCRIPTION_LIST) enabled.append('ROLE_FEED') - if _settingsManager.getSetting('sayAllContextPanel'): + if cthulhu.cthulhuApp.settingsManager.getSetting('sayAllContextPanel'): enabled.extend([Atspi.Role.PANEL, Atspi.Role.TOOL_TIP, 'ROLE_CONTENT_DELETION', @@ -2003,20 +2004,20 @@ class SpeechGenerator(generator.Generator): 'ROLE_CONTENT_MARK', 'ROLE_CONTENT_SUGGESTION', 'ROLE_DPUB_SECTION']) - if _settingsManager.getSetting('sayAllContextNonLandmarkForm'): + if cthulhu.cthulhuApp.settingsManager.getSetting('sayAllContextNonLandmarkForm'): enabled.append(Atspi.Role.FORM) - if _settingsManager.getSetting('sayAllContextTable'): + if cthulhu.cthulhuApp.settingsManager.getSetting('sayAllContextTable'): enabled.append(Atspi.Role.TABLE) else: - if _settingsManager.getSetting('speakContextBlockquote'): + if cthulhu.cthulhuApp.settingsManager.getSetting('speakContextBlockquote'): enabled.append(Atspi.Role.BLOCK_QUOTE) - if _settingsManager.getSetting('speakContextLandmark'): + if cthulhu.cthulhuApp.settingsManager.getSetting('speakContextLandmark'): enabled.extend([Atspi.Role.LANDMARK, 'ROLE_DPUB_LANDMARK', 'ROLE_REGION']) - if _settingsManager.getSetting('speakContextList'): + if cthulhu.cthulhuApp.settingsManager.getSetting('speakContextList'): enabled.append(Atspi.Role.LIST) enabled.append(Atspi.Role.DESCRIPTION_LIST) enabled.append('ROLE_FEED') - if _settingsManager.getSetting('speakContextPanel'): + if cthulhu.cthulhuApp.settingsManager.getSetting('speakContextPanel'): enabled.extend([Atspi.Role.PANEL, Atspi.Role.TOOL_TIP, 'ROLE_CONTENT_DELETION', @@ -2024,9 +2025,9 @@ class SpeechGenerator(generator.Generator): 'ROLE_CONTENT_MARK', 'ROLE_CONTENT_SUGGESTION', 'ROLE_DPUB_SECTION']) - if _settingsManager.getSetting('speakContextNonLandmarkForm'): + if cthulhu.cthulhuApp.settingsManager.getSetting('speakContextNonLandmarkForm'): enabled.append(Atspi.Role.FORM) - if _settingsManager.getSetting('speakContextTable'): + if cthulhu.cthulhuApp.settingsManager.getSetting('speakContextTable'): enabled.append(Atspi.Role.TABLE) disabled = list(set(allRoles).symmetric_difference(enabled)) @@ -2278,7 +2279,7 @@ class SpeechGenerator(generator.Generator): specifications) that represent the text of the ancestors for the object being left.""" - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if self._script.utilities.inFindContainer(): @@ -2344,7 +2345,7 @@ class SpeechGenerator(generator.Generator): with focus. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] if self._script.utilities.inFindContainer(): @@ -2410,7 +2411,7 @@ class SpeechGenerator(generator.Generator): specifications) that represent the relative position of an object in a group. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] # TODO - JD: We need other ways to determine group membership. Not all @@ -2444,8 +2445,8 @@ class SpeechGenerator(generator.Generator): object in a list. """ - if _settingsManager.getSetting('onlySpeakDisplayedText') \ - or not (_settingsManager.getSetting('enablePositionSpeaking') \ + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText') \ + or not (cthulhu.cthulhuApp.settingsManager.getSetting('enablePositionSpeaking') \ or args.get('forceList', False)): return [] @@ -2506,14 +2507,14 @@ class SpeechGenerator(generator.Generator): return result def _getProgressBarUpdateInterval(self): - interval = _settingsManager.getSetting('progressBarSpeechInterval') + interval = cthulhu.cthulhuApp.settingsManager.getSetting('progressBarSpeechInterval') if interval is None: interval = super()._getProgressBarUpdateInterval() return int(interval) def _shouldPresentProgressBarUpdate(self, obj, **args): - if not _settingsManager.getSetting('speakProgressBarUpdates'): + if not cthulhu.cthulhuApp.settingsManager.getSetting('speakProgressBarUpdates'): return False return super()._shouldPresentProgressBarUpdate(obj, **args) @@ -2588,7 +2589,7 @@ class SpeechGenerator(generator.Generator): specifications) that represent the accelerator for the object, or an empty array if no accelerator can be found. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = [] @@ -2605,11 +2606,11 @@ class SpeechGenerator(generator.Generator): specifications) that represent the mnemonic for the object, or an empty array if no mnemonic can be found. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = [] - if _settingsManager.getSetting('enableMnemonicSpeaking') \ + if cthulhu.cthulhuApp.settingsManager.getSetting('enableMnemonicSpeaking') \ or args.get('forceMnemonic', False): [mnemonic, shortcut, accelerator] = \ self._script.utilities.mnemonicShortcutAccelerator(obj) @@ -2637,7 +2638,7 @@ class SpeechGenerator(generator.Generator): tutorial generator. A tutorial can be forced by setting the 'forceTutorial' attribute of the args dictionary to True. """ - if _settingsManager.getSetting('onlySpeakDisplayedText'): + if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return [] result = [] @@ -3011,11 +3012,11 @@ class SpeechGenerator(generator.Generator): ##################################################################### def _generatePause(self, obj, **args): - if not _settingsManager.getSetting('enablePauseBreaks') \ + if not cthulhu.cthulhuApp.settingsManager.getSetting('enablePauseBreaks') \ or args.get('eliminatePauses', False): return [] - if _settingsManager.getSetting('verbalizePunctuationStyle') == \ + if cthulhu.cthulhuApp.settingsManager.getSetting('verbalizePunctuationStyle') == \ settings.PUNCTUATION_STYLE_ALL: return [] @@ -3032,7 +3033,7 @@ class SpeechGenerator(generator.Generator): """ voicename = voiceType.get(key) or voiceType.get(DEFAULT) - voices = _settingsManager.getSetting('voices') + voices = cthulhu.cthulhuApp.settingsManager.getSetting('voices') voice = acss.ACSS(voices.get(voiceType.get(DEFAULT), {})) language = args.get('language') diff --git a/src/cthulhu/speechdispatcherfactory.py b/src/cthulhu/speechdispatcherfactory.py index 6317f48..4b233e6 100644 --- a/src/cthulhu/speechdispatcherfactory.py +++ b/src/cthulhu/speechdispatcherfactory.py @@ -37,6 +37,7 @@ import re import time from . import chnames +from . import cthulhu from . import debug from . import guilabels from . import messages @@ -47,7 +48,7 @@ from . import punctuation_settings from . import settings_manager from .acss import ACSS -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager try: import speechd @@ -287,7 +288,7 @@ class SpeechServer(speechserver.SpeechServer): f"volume {self._current_voice_properties.get(ACSS.GAIN)}, " f"language {self._get_language_and_dialect(family)[0]}, " f"punctuation: " - f"{styles.get(_settingsManager.getSetting('verbalizePunctuationStyle'))}\n" + f"{styles.get(cthulhu.cthulhuApp.settingsManager.getSetting('verbalizePunctuationStyle'))}\n" f"SD rate {sd_rate}, pitch {sd_pitch}, volume {sd_volume}, language {sd_language}" ) debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -328,7 +329,7 @@ class SpeechServer(speechserver.SpeechServer): Returns a text string with the punctuation symbols adjusted accordingly. """ - style = _settingsManager.getSetting("verbalizePunctuationStyle") + style = cthulhu.cthulhuApp.settingsManager.getSetting("verbalizePunctuationStyle") if style == settings.PUNCTUATION_STYLE_NONE: return oldText diff --git a/src/cthulhu/spellcheck.py b/src/cthulhu/spellcheck.py index 22bccc5..4e91219 100644 --- a/src/cthulhu/spellcheck.py +++ b/src/cthulhu/spellcheck.py @@ -47,7 +47,7 @@ from cthulhu.ax_object import AXObject from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class SpellCheck: @@ -195,7 +195,7 @@ class SpellCheck: if self.presentMistake(detailed): self.presentSuggestion(detailed) - if detailed or _settingsManager.getSetting('spellcheckPresentContext'): + if detailed or cthulhu.cthulhuApp.settingsManager.getSetting('spellcheckPresentContext'): self.presentContext() return True @@ -212,7 +212,7 @@ class SpellCheck: msg = messages.MISSPELLED_WORD % word voice = self._script.speechGenerator.voice(string=msg) self._script.speakMessage(msg, voice=voice) - if detailed or _settingsManager.getSetting('spellcheckSpellError'): + if detailed or cthulhu.cthulhuApp.settingsManager.getSetting('spellcheckSpellError'): self._script.spellCurrentItem(word) return True @@ -233,7 +233,7 @@ class SpellCheck: msg = f"{label} {string}" voice = self._script.speechGenerator.voice(string=msg) self._script.speakMessage(msg, voice=voice) - if detailed or _settingsManager.getSetting('spellcheckSpellSuggestion'): + if detailed or cthulhu.cthulhuApp.settingsManager.getSetting('spellcheckSpellSuggestion'): self._script.spellCurrentItem(string) return True @@ -260,10 +260,10 @@ class SpellCheck: msg = f"{label} {string}" voice = self._script.speechGenerator.voice(string=msg) self._script.speakMessage(msg.strip(), voice=voice) - if detailed or _settingsManager.getSetting('spellcheckSpellSuggestion'): + if detailed or cthulhu.cthulhuApp.settingsManager.getSetting('spellcheckSpellSuggestion'): self._script.spellCurrentItem(string) - if _settingsManager.getSetting('enablePositionSpeaking') \ + if cthulhu.cthulhuApp.settingsManager.getSetting('enablePositionSpeaking') \ and items[0] == cthulhu_state.locusOfFocus: index, total = self._getSuggestionIndexAndPosition(items[0]) msg = object_properties.GROUP_INDEX_SPEECH % {"index": index, "total": total} @@ -310,19 +310,19 @@ class SpellCheck: alignment.add(grid) label = guilabels.SPELL_CHECK_SPELL_ERROR - value = _settingsManager.getSetting('spellcheckSpellError') + value = cthulhu.cthulhuApp.settingsManager.getSetting('spellcheckSpellError') self.spellErrorCheckButton = Gtk.CheckButton.new_with_mnemonic(label) self.spellErrorCheckButton.set_active(value) grid.attach(self.spellErrorCheckButton, 0, 0, 1, 1) label = guilabels.SPELL_CHECK_SPELL_SUGGESTION - value = _settingsManager.getSetting('spellcheckSpellSuggestion') + value = cthulhu.cthulhuApp.settingsManager.getSetting('spellcheckSpellSuggestion') self.spellSuggestionCheckButton = Gtk.CheckButton.new_with_mnemonic(label) self.spellSuggestionCheckButton.set_active(value) grid.attach(self.spellSuggestionCheckButton, 0, 1, 1, 1) label = guilabels.SPELL_CHECK_PRESENT_CONTEXT - value = _settingsManager.getSetting('spellcheckPresentContext') + value = cthulhu.cthulhuApp.settingsManager.getSetting('spellcheckPresentContext') self.presentContextCheckButton = Gtk.CheckButton.new_with_mnemonic(label) self.presentContextCheckButton.set_active(value) grid.attach(self.presentContextCheckButton, 0, 2, 1, 1) diff --git a/src/cthulhu/structural_navigation.py b/src/cthulhu/structural_navigation.py index 55e414d..49998fc 100644 --- a/src/cthulhu/structural_navigation.py +++ b/src/cthulhu/structural_navigation.py @@ -57,7 +57,7 @@ from .ax_text import AXText from .ax_selection import AXSelection from .ax_utilities import AXUtilities -_settingsManager = settings_manager.getManager() +_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager ########################################################################### # # @@ -1224,7 +1224,7 @@ class StructuralNavigation: def _presentWithSayAll(self, obj, offset): if self._script.inSayAll() \ - and _settingsManager.getSetting('structNavInSayAll'): + and cthulhu.cthulhuApp.settingsManager.getSetting('structNavInSayAll'): self._script.sayAll(obj, offset) return True From dfa572b4532e8ec0729a483a54292fa0c4c5950d Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 16 Jan 2026 09:23:53 -0500 Subject: [PATCH 08/13] More refactoring work. Since the first part was such a success, may as well continue. We're actually going to be in very good shape if things keep going this well. --- src/cthulhu/cthulhu.py | 70 +++++++++++++++++----------- src/cthulhu/event_manager.py | 13 +++++- src/cthulhu/focus_manager.py | 6 +-- src/cthulhu/input_event.py | 28 ++++++----- src/cthulhu/input_event_manager.py | 10 ++++ src/cthulhu/plugin_system_manager.py | 54 +++++++++++++++------ src/cthulhu/script.py | 14 ++++-- src/cthulhu/script_manager.py | 24 ++++++++-- src/cthulhu/scripts/default.py | 2 +- src/cthulhu/scripts/web/script.py | 11 +++-- src/cthulhu/sleep_mode_manager.py | 4 +- 11 files changed, 163 insertions(+), 73 deletions(-) diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index d8f979e..0727308 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -135,6 +135,7 @@ class APIHelper: self._gestureBindings[contextName].append(binding) logger.info(f"Stored binding in context '{contextName}'") + plugin_manager = None if contextName and self.app: try: plugin_manager = self.app.getPluginSystemManager() @@ -143,21 +144,26 @@ class APIHelper: if plugin_manager: plugin_manager.add_keybinding(contextName, binding, global_binding=globalBinding) - # Only add to active script if one exists - if cthulhu_state.activeScript and not globalBinding: - logger.info(f"Adding binding to active script: {cthulhu_state.activeScript}") - bindings = cthulhu_state.activeScript.getKeyBindings() - bindings.add(binding) - - # Register key grab at the system level - grab_ids = addKeyGrab(binding) - logger.info(f"Key grab IDs: {grab_ids}") - - # For later removal - if grab_ids: - binding._grab_ids = grab_ids + if contextName and self.app and plugin_manager: + if not globalBinding and cthulhu_state.activeScript: + plugin_manager.activate_keybindings_for_plugin(contextName) + elif not globalBinding: + debug.printMessage(debug.LEVEL_INFO, "No active script available - binding stored for later registration", True) else: - debug.printMessage(debug.LEVEL_INFO, "No active script available - binding stored for later registration", True) + if cthulhu_state.activeScript and not globalBinding: + logger.info(f"Adding binding to active script: {cthulhu_state.activeScript}") + bindings = cthulhu_state.activeScript.getKeyBindings() + bindings.add(binding) + + # Register key grab at the system level + grab_ids = addKeyGrab(binding) + logger.info(f"Key grab IDs: {grab_ids}") + + # For later removal + if grab_ids: + binding._grab_ids = grab_ids + else: + debug.printMessage(debug.LEVEL_INFO, "No active script available - binding stored for later registration", True) debug.printMessage(debug.LEVEL_INFO, f"Created binding: {binding.keysymstring} with modifiers {binding.modifiers}", True) logger.info("=== APIHelper.registerGestureByString completed ===") @@ -174,16 +180,26 @@ class APIHelper: - binding: the binding to unregister - contextName: the context for this gesture """ - # Remove from script's keybindings - from . import cthulhu_state - if cthulhu_state.activeScript: - bindings = cthulhu_state.activeScript.getKeyBindings() - bindings.remove(binding) + removed_via_plugin_manager = False + if contextName and self.app: + try: + plugin_manager = self.app.getPluginSystemManager() + except Exception: + plugin_manager = None + if plugin_manager: + plugin_manager.remove_keybinding(contextName, binding) + removed_via_plugin_manager = True + if not removed_via_plugin_manager: + # Remove from script's keybindings + from . import cthulhu_state + if cthulhu_state.activeScript: + bindings = cthulhu_state.activeScript.getKeyBindings() + bindings.remove(binding) - # Remove key grab at system level - if hasattr(binding, '_grab_ids'): - for grab_id in binding._grab_ids: - self.app.removeKeyGrab(grab_id) + # Remove key grab at system level + if hasattr(binding, '_grab_ids'): + for grab_id in binding._grab_ids: + self.app.removeKeyGrab(grab_id) # Remove from tracking if contextName in self._gestureBindings: @@ -858,17 +874,15 @@ def main(): # setActiveWindow does some corrective work needed thanks to # mutter-x11-frames. So retrieve the window just in case. window = cthulhu_state.activeWindow - script = cthulhuApp.scriptManager.get_script(app, window) - cthulhuApp.scriptManager.set_active_script(script, "Launching.") + cthulhuApp.scriptManager.activate_script_for_context(app, window, "startup: launch") focusedObject = AXUtilities.get_focused_object(window) tokens = ["CTHULHU: Focused object is:", focusedObject] debug.printTokens(debug.LEVEL_INFO, tokens, True) if focusedObject: setLocusOfFocus(None, focusedObject) - script = cthulhuApp.scriptManager.get_script( - AXObject.get_application(focusedObject), focusedObject) - cthulhuApp.scriptManager.set_active_script(script, "Found focused object.") + cthulhuApp.scriptManager.activate_script_for_context( + AXObject.get_application(focusedObject), focusedObject, "startup: focused-object") try: msg = "CTHULHU: Starting ATSPI registry." diff --git a/src/cthulhu/event_manager.py b/src/cthulhu/event_manager.py index 7258c0c..9637298 100644 --- a/src/cthulhu/event_manager.py +++ b/src/cthulhu/event_manager.py @@ -675,7 +675,7 @@ class EventManager: return False defaultScript = cthulhu.cthulhuApp.scriptManager.get_default_script() - cthulhu.cthulhuApp.scriptManager.set_active_script(defaultScript, 'No focus') + cthulhu.cthulhuApp.scriptManager.set_active_script(defaultScript, 'focus: none') defaultScript.idleMessage() return False @@ -900,6 +900,15 @@ class EventManager: if not script.isActivatableEvent(event): return False, "The script says not to activate for this event." + if cthulhu_state.activeScript is None: + active_window = cthulhu_state.activeWindow + if AXUtilities.is_focused(event.source): + return True, "No active script and event source is focused." + if active_window and AXObject.is_ancestor(event.source, active_window, inclusive=True): + return True, "No active script and event is in active window." + if AXUtilities.is_frame(event.source) or AXUtilities.is_window(event.source): + return True, "No active script and event source is window/frame." + if script.forceScriptActivation(event): return True, "The script insists it should be activated for this event." @@ -1097,7 +1106,7 @@ class EventManager: debug.printMessage(debug.LEVEL_INFO, msg, True) cthulhu_state.locusOfFocus = None cthulhu_state.activeWindow = None - cthulhu.cthulhuApp.scriptManager.set_active_script(None, "Active window is dead or defunct") + cthulhu.cthulhuApp.scriptManager.set_active_script(None, "focus: active-window-dead") return if AXUtilities.is_iconified(event.source): diff --git a/src/cthulhu/focus_manager.py b/src/cthulhu/focus_manager.py index 02aed9a..ac49038 100644 --- a/src/cthulhu/focus_manager.py +++ b/src/cthulhu/focus_manager.py @@ -291,7 +291,7 @@ class FocusManager: if event and (script and not script.app): app = _get_ax_utilities().get_application(event.source) script = self.app.scriptManager.get_script(app, event.source) - self.app.scriptManager.set_active_script(script, "Setting locus of focus") + self.app.scriptManager.activate_script_for_context(app, event.source, "focus: locus-of-focus") old_focus = self._focus if AXObject.is_dead(old_focus): @@ -384,8 +384,7 @@ class FocusManager: self.set_locus_of_focus(None, self._window, notify_script=True) app = _get_ax_utilities().get_application(self._focus) - script = self.app.scriptManager.get_script(app, self._focus) - self.app.scriptManager.set_active_script(script, "Setting active window") + self.app.scriptManager.activate_script_for_context(app, self._focus, "focus: active-window") @dbus_service.command def toggle_presentation_mode( @@ -472,4 +471,3 @@ def get_manager(): from . import cthulhu _manager = FocusManager(cthulhu.cthulhuApp) return _manager - diff --git a/src/cthulhu/input_event.py b/src/cthulhu/input_event.py index f4a0c13..c6d6e8a 100644 --- a/src/cthulhu/input_event.py +++ b/src/cthulhu/input_event.py @@ -778,6 +778,20 @@ class KeyboardEvent(InputEvent): return self._handler + def _resolveHandler(self): + """Resolve handler for this event, returning True if a global handler was used.""" + + if not self._handler and self._script: + self._handler = self._script.keyBindings.getInputHandler(self) + + if not self._handler: + globalHandler = self._getGlobalHandler() + if globalHandler: + self._handler = globalHandler + return True + + return False + def shouldConsume(self): """Returns True if this event should be consumed.""" @@ -810,23 +824,15 @@ class KeyboardEvent(InputEvent): if cthulhu_state.bypassNextCommand: return False, 'Bypass next command' - if not self._handler: - self._handler = self._script.keyBindings.getInputHandler(self) - if not self._handler: - globalHandler = self._getGlobalHandler() - if globalHandler: - self._handler = globalHandler - globalHandlerUsed = True + globalHandlerUsed = globalHandlerUsed or self._resolveHandler() if self._handler: debug.printMessage(debug.LEVEL_INFO, f"shouldConsume: Handler found: {self._handler.description}", True) else: debug.printMessage(debug.LEVEL_INFO, "shouldConsume: No handler found", True) - # TODO - JD: Right now we need to always call consumesKeyboardEvent() - # because that method is updating state, even in instances where there - # is no handler. - scriptConsumes = self._script.consumesKeyboardEvent(self) + self._script.updateKeyboardEventState(self, self._handler) + scriptConsumes = self._script.shouldConsumeKeyboardEvent(self, self._handler) if globalHandlerUsed: scriptConsumes = True debug.printMessage(debug.LEVEL_INFO, f"shouldConsume: scriptConsumes={scriptConsumes}", True) diff --git a/src/cthulhu/input_event_manager.py b/src/cthulhu/input_event_manager.py index 3ce833f..e83d36c 100644 --- a/src/cthulhu/input_event_manager.py +++ b/src/cthulhu/input_event_manager.py @@ -133,6 +133,10 @@ class InputEventManager: grab_ids.append(grab_id) self._grabbed_bindings[grab_id] = binding + if grab_ids: + tokens = ["INPUT EVENT MANAGER: Added grabs", grab_ids, "for", binding] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return grab_ids def remove_grabs_for_keybinding(self, binding: keybindings.KeyBinding) -> None: @@ -154,6 +158,9 @@ class InputEventManager: debug.print_tokens(debug.LEVEL_INFO, tokens, True) return + tokens = ["INPUT EVENT MANAGER: Removing grabs", grab_ids, "for", binding] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + for grab_id in grab_ids: self._device.remove_key_grab(grab_id) removed = self._grabbed_bindings.pop(grab_id, None) @@ -179,6 +186,9 @@ class InputEventManager: binding._grab_ids.remove(grab_id) if not binding._grab_ids: delattr(binding, "_grab_ids") + if binding: + tokens = ["INPUT EVENT MANAGER: Removed grab", grab_id, "for", binding] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) def map_keysym_to_modifier(self, keysym: int) -> int: """Maps keysym as a modifier, returns the newly-mapped modifier.""" diff --git a/src/cthulhu/plugin_system_manager.py b/src/cthulhu/plugin_system_manager.py index f9f0054..03f46f0 100644 --- a/src/cthulhu/plugin_system_manager.py +++ b/src/cthulhu/plugin_system_manager.py @@ -21,6 +21,7 @@ from enum import IntEnum import pluggy from . import dbus_service +from . import input_event_manager from . import keybindings # Added import # Set to True for more detailed plugin loading debug info @@ -144,6 +145,7 @@ class PluginSystemManager: self._plugin_keybindings = {} # plugin_name -> [KeyBinding] self._global_keybindings = keybindings.KeyBindings() self._global_bindings = [] + self._last_active_script = None # Create plugin directories self._setup_plugin_dirs() @@ -166,12 +168,42 @@ class PluginSystemManager: if binding not in self._global_bindings: self._global_bindings.append(binding) self._global_keybindings.add(binding) - grab_ids = self.app.addKeyGrab(binding) + grab_ids = input_event_manager.get_manager().add_grabs_for_keybinding(binding) if grab_ids: binding._global_grab_ids = grab_ids else: logger.warning(f"Failed to create global key grab for {binding.keysymstring}") + def activate_keybindings_for_plugin(self, plugin_name): + """Activates keybindings for a single plugin with the active script.""" + plugin_info = self._plugins.get(plugin_name) + if not plugin_info or not plugin_info.loaded: + return + self._activate_plugin_keybindings(plugin_info) + + def remove_keybinding(self, plugin_name, binding): + """Remove a keybinding associated with a specific plugin.""" + if plugin_name in self._plugin_keybindings: + if binding in self._plugin_keybindings[plugin_name]: + self._plugin_keybindings[plugin_name].remove(binding) + + if binding in self._global_bindings: + if hasattr(binding, '_global_grab_ids'): + for grab_id in binding._global_grab_ids: + input_event_manager.get_manager().remove_grab_by_id(grab_id) + del binding._global_grab_ids + if binding in self._global_bindings: + self._global_bindings.remove(binding) + self._global_keybindings.remove(binding) + return + + from . import cthulhu_state + if cthulhu_state.activeScript: + active_script = cthulhu_state.activeScript + if active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"): + active_script.getKeyBindings().remove(binding) + input_event_manager.get_manager().remove_grabs_for_keybinding(binding) + def _activate_plugin_keybindings(self, plugin_info): """Activates all keybindings for a given plugin with the active script.""" from . import cthulhu_state # Import here to avoid circular dependency @@ -182,12 +214,13 @@ class PluginSystemManager: plugin_name = plugin_info.get_module_name() if plugin_name in self._plugin_keybindings: active_script = cthulhu_state.activeScript + input_manager = input_event_manager.get_manager() for binding in self._plugin_keybindings[plugin_name]: if binding in self._global_bindings: continue if not active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"): active_script.getKeyBindings().add(binding) - grab_ids = self.app.addKeyGrab(binding) + grab_ids = input_event_manager.get_manager().add_grabs_for_keybinding(binding) if grab_ids: binding._grab_ids = grab_ids else: @@ -216,10 +249,7 @@ class PluginSystemManager: continue if active_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"): active_script.getKeyBindings().remove(binding) - if hasattr(binding, '_grab_ids'): - for grab_id in binding._grab_ids: - self.app.removeKeyGrab(grab_id) - del binding._grab_ids + input_manager.remove_grabs_for_keybinding(binding) logger.debug(f"Deactivated keybinding '{binding.asString()}' for plugin '{plugin_name}'") def refresh_active_script_keybindings(self): @@ -237,22 +267,20 @@ class PluginSystemManager: # First, remove all existing plugin keybindings from the old script # This requires iterating through all plugins, not just active ones, to ensure cleanup - from . import cthulhu_state - if cthulhu_state.activeScript: # If there was an old active script - old_script = cthulhu_state.activeScript + old_script = self._last_active_script + if old_script: + input_manager = input_event_manager.get_manager() for plugin_name, bindings in self._plugin_keybindings.items(): for binding in bindings: if binding in self._global_bindings: continue if old_script.getKeyBindings().hasKeyBinding(binding, "keysNoMask"): old_script.getKeyBindings().remove(binding) - if hasattr(binding, '_grab_ids'): - for grab_id in binding._grab_ids: - self.app.removeKeyGrab(grab_id) - del binding._grab_ids + input_manager.remove_grabs_for_keybinding(binding) logger.debug(f"Removed keybinding '{binding.asString()}' from old script for plugin '{plugin_name}'") # Now, if there's a new active script, apply keybindings for currently active plugins + self._last_active_script = new_script if new_script: for plugin_name in self._active_plugins: plugin_info = self._plugins.get(plugin_name) diff --git a/src/cthulhu/script.py b/src/cthulhu/script.py index f4deba4..f298fb8 100644 --- a/src/cthulhu/script.py +++ b/src/cthulhu/script.py @@ -496,11 +496,19 @@ class Script: Returns True if the event is of interest. """ + handler = self.keyBindings.getInputHandler(keyboardEvent) + self.updateKeyboardEventState(keyboardEvent, handler) + return self.shouldConsumeKeyboardEvent(keyboardEvent, handler) + + def updateKeyboardEventState(self, keyboardEvent, handler): + """Update internal state for a keyboard event without deciding consumption.""" + pass + + def shouldConsumeKeyboardEvent(self, keyboardEvent, handler): + """Returns True if the script will consume this keyboard event.""" consumes = False self._lastCommandWasStructNav = False - handler = self.keyBindings.getInputHandler(keyboardEvent) - if handler \ - and handler.function in self.structuralNavigation.functions: + if handler and handler.function in self.structuralNavigation.functions: consumes = self.useStructuralNavigationModel() if consumes: self._lastCommandWasStructNav = True diff --git a/src/cthulhu/script_manager.py b/src/cthulhu/script_manager.py index dda84b2..7413583 100644 --- a/src/cthulhu/script_manager.py +++ b/src/cthulhu/script_manager.py @@ -72,7 +72,7 @@ class ScriptManager: self._toolkitNames = \ {'WebKitGTK': 'WebKitGtk', 'GTK': 'gtk'} - self.set_active_script(None, "__init__") + self.set_active_script(None, "lifecycle: init") self._active = False debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Initialized", True) @@ -82,7 +82,7 @@ class ScriptManager: debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Activating", True) self._defaultScript = self.get_script(None) self._defaultScript.registerEventListeners() - self.set_active_script(self._defaultScript, "activate") + self.set_active_script(self._defaultScript, "lifecycle: activate") self._active = True debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Activated", True) @@ -93,7 +93,7 @@ class ScriptManager: if self._defaultScript: self._defaultScript.deregisterEventListeners() self._defaultScript = None - self.set_active_script(None, "deactivate") + self.set_active_script(None, "lifecycle: deactivate") self.appScripts = {} self.toolkitScripts = {} self.customScripts = {} @@ -342,6 +342,7 @@ class ScriptManager: cthulhu_state.activeScript = newScript if not newScript: + self._log_active_state(reason) return newScript.activate() @@ -352,6 +353,22 @@ class ScriptManager: tokens = ["SCRIPT MANAGER: Setting active script to", newScript, "reason:", reason] debug.printTokens(debug.LEVEL_INFO, tokens, True) + self._log_active_state(reason) + + def activate_script_for_context(self, app, obj, reason=None): + script = self.get_script(app, obj) + self.set_active_script(script, reason) + return script + + def _log_active_state(self, reason=None): + tokens = [ + "SCRIPT MANAGER: Active state:", + "window", cthulhu_state.activeWindow, + "focus", cthulhu_state.locusOfFocus, + "script", cthulhu_state.activeScript, + "reason", reason + ] + debug.printTokens(debug.LEVEL_INFO, tokens, True) def _get_script_for_app_replicant(self, app): if not self._active: @@ -436,4 +453,3 @@ def get_manager(): from . import cthulhu _manager = ScriptManager(cthulhu.cthulhuApp) return _manager - diff --git a/src/cthulhu/scripts/default.py b/src/cthulhu/scripts/default.py index 3bdee8a..33dd2e4 100644 --- a/src/cthulhu/scripts/default.py +++ b/src/cthulhu/scripts/default.py @@ -1983,7 +1983,7 @@ class Script(script.Script): cthulhu.setLocusOfFocus(event, None) cthulhu.setActiveWindow(None) - cthulhu.cthulhuApp.scriptManager.set_active_script(None, "Window deactivated") + cthulhu.cthulhuApp.scriptManager.set_active_script(None, "focus: window-deactivated") def onClipboardContentsChanged(self, *args): if self.flatReviewPresenter.is_active(): diff --git a/src/cthulhu/scripts/web/script.py b/src/cthulhu/scripts/web/script.py index 0954f63..b530021 100644 --- a/src/cthulhu/scripts/web/script.py +++ b/src/cthulhu/scripts/web/script.py @@ -587,18 +587,19 @@ class Script(default.Script): debug.printMessage(debug.LEVEL_INFO, msg, True) self.liveRegionManager.flushMessages() - def consumesKeyboardEvent(self, keyboardEvent): - """Returns True if the script will consume this keyboard event.""" + def updateKeyboardEventState(self, keyboardEvent, handler): + """Update internal state for a keyboard event without deciding consumption.""" # We need to do this here. Cthulhu caret and structural navigation # often result in the user being repositioned without our getting # a corresponding AT-SPI event. Without an AT-SPI event, script.py # won't know to dump the generator cache. See bgo#618827. self.generatorCache = {} - self._lastMouseButtonContext = None, -1 - handler = self.keyBindings.getInputHandler(keyboardEvent) + def shouldConsumeKeyboardEvent(self, keyboardEvent, handler): + """Returns True if the script will consume this keyboard event.""" + if handler and self.caretNavigation.handles_navigation(handler): consumes = self.useCaretNavigationModel(keyboardEvent) self._lastCommandWasCaretNav = consumes @@ -627,7 +628,7 @@ class Script(default.Script): self._lastCommandWasMouseButton = False # Check parent first - consumes = super().consumesKeyboardEvent(keyboardEvent) + consumes = super().shouldConsumeKeyboardEvent(keyboardEvent, handler) # If parent doesn't consume Return key, try our clickable fallback if not consumes and keyboardEvent.event_string == "Return": diff --git a/src/cthulhu/sleep_mode_manager.py b/src/cthulhu/sleep_mode_manager.py index 6e3e268..14f44f9 100644 --- a/src/cthulhu/sleep_mode_manager.py +++ b/src/cthulhu/sleep_mode_manager.py @@ -142,7 +142,7 @@ class SleepModeManager: if notifyUser: newScript.presentMessage( messages.SLEEP_MODE_DISABLED_FOR % AXObject.get_name(script.app)) - scriptManager.set_active_script(newScript, "Sleep mode toggled off") + scriptManager.set_active_script(newScript, "sleep: off") debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Disabled for {AXObject.get_name(script.app)}", True) # Reset debounce timer after successful toggle self._lastToggleTime = 0 @@ -173,7 +173,7 @@ class SleepModeManager: debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Got sleep script: {sleepScript}", True) debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Setting active script", True) - scriptManager.set_active_script(sleepScript, "Sleep mode toggled on") + scriptManager.set_active_script(sleepScript, "sleep: on") debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Active script set successfully", True) debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Adding app to sleep list", True) From a9739dba1a3915c211c49f91b5d51304e30dbdd8 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 16 Jan 2026 10:29:53 -0500 Subject: [PATCH 09/13] More refactor work. --- src/cthulhu/ax_component.py | 5 +- src/cthulhu/ax_utilities.py | 15 ++- src/cthulhu/event_manager.py | 136 ++++++++-------------- src/cthulhu/flat_review.py | 181 +++++++++++++++-------------- src/cthulhu/input_event_manager.py | 2 +- src/cthulhu/script_utilities.py | 52 ++++++--- 6 files changed, 194 insertions(+), 197 deletions(-) diff --git a/src/cthulhu/ax_component.py b/src/cthulhu/ax_component.py index c26bf10..17bdea6 100644 --- a/src/cthulhu/ax_component.py +++ b/src/cthulhu/ax_component.py @@ -145,7 +145,7 @@ class AXComponent: return not(rect.width or rect.height) @staticmethod - def has_no_size_or_invalid_rect(obj: Atspi.Accessible) -> bool: + def has_no_size_or_invalid_rect(obj: Atspi.Accessible, clear_cache: bool = True) -> bool: """Returns True if the rect associated with obj is sizeless or invalid.""" rect = AXComponent.get_rect(obj) @@ -158,7 +158,8 @@ class AXComponent: if (rect.width < -1 or rect.height < -1): tokens = ["WARNING: ", obj, "has a broken rect:", rect] debug.print_tokens(debug.LEVEL_INFO, tokens, True) - AXObject.clear_cache(obj) + if clear_cache: + AXObject.clear_cache(obj) rect = AXComponent.get_rect(obj) if (rect.width < -1 or rect.height < -1): msg = "AXComponent: Clearing cache did not fix the rect" diff --git a/src/cthulhu/ax_utilities.py b/src/cthulhu/ax_utilities.py index 9d53eb7..77498e4 100644 --- a/src/cthulhu/ax_utilities.py +++ b/src/cthulhu/ax_utilities.py @@ -109,13 +109,14 @@ class AXUtilities: AXTable.clear_cache_now(reason) @staticmethod - def can_be_active_window(window: Atspi.Accessible) -> bool: + def can_be_active_window(window: Atspi.Accessible, clear_cache: bool = True) -> bool: """Returns True if window can be the active window based on its state.""" if window is None: return False - AXObject.clear_cache(window, False, "Checking if window can be the active window") + if clear_cache: + AXObject.clear_cache(window, False, "Checking if window can be the active window") app = AXUtilitiesApplication.get_application(window) tokens = ["AXUtilities:", window, "from", app] @@ -808,11 +809,13 @@ class AXUtilities: @staticmethod def is_on_screen( obj: Atspi.Accessible, - bounding_box: Optional[Atspi.Rect] = None + bounding_box: Optional[Atspi.Rect] = None, + clear_cache: bool = True ) -> bool: """Returns true if obj should be treated as being on screen.""" - AXObject.clear_cache(obj, False, "Updating to check if object is on screen.") + if clear_cache: + AXObject.clear_cache(obj, False, "Updating to check if object is on screen.") tokens = ["AXUtilities: Checking if", obj, "is showing and visible...."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) @@ -833,7 +836,7 @@ class AXUtilities: tokens = ["AXUtilities:", obj, "is not hidden. Checking size and rect..."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) - if AXComponent.has_no_size_or_invalid_rect(obj): + if AXComponent.has_no_size_or_invalid_rect(obj, clear_cache=clear_cache): tokens = ["AXUtilities: Rect of", obj, "is unhelpful. Treating as on screen."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return True @@ -972,6 +975,8 @@ class AXUtilities: debug.print_tokens(debug.LEVEL_INFO, tokens, True) return result +# Dynamically expose helper methods for compatibility with older callers. +# Keep side effects explicit in the underlying helpers (e.g., clear_cache flags). for method_name, method in inspect.getmembers(AXUtilitiesApplication, predicate=inspect.isfunction): setattr(AXUtilities, method_name, method) diff --git a/src/cthulhu/event_manager.py b/src/cthulhu/event_manager.py index 9637298..3afb875 100644 --- a/src/cthulhu/event_manager.py +++ b/src/cthulhu/event_manager.py @@ -109,7 +109,7 @@ class EventManager: return False window = cthulhu_state.activeWindow - if not AXUtilities.can_be_active_window(window): + if not AXUtilities.can_be_active_window(window, clear_cache=True): window = AXUtilities.find_active_window() if window is not None: tokens = ["EVENT MANAGER: Setting initial active window to", window] @@ -237,40 +237,44 @@ class EventManager: tokens = ["EVENT MANAGER:", event.type, "from", app] debug.printTokens(debug.LEVEL_INFO, tokens, True) + def _log_ignore(reason, message): + msg = f"EVENT MANAGER: Ignoring ({reason}) - {message}" + debug.printMessage(debug.LEVEL_INFO, msg, True) + + def _log_allow(reason, message): + msg = f"EVENT MANAGER: Not ignoring ({reason}) - {message}" + debug.printMessage(debug.LEVEL_INFO, msg, True) + + def _ignore_with_reason(reason, message): + _log_ignore(reason, message) + return True + + def _allow_with_reason(reason, message): + _log_allow(reason, message) + return False + if self._eventsSuspended: tokens = ["EVENT MANAGER: Suspended events:", ', '.join(self._suspendableEvents)] debug.printTokens(debug.LEVEL_INFO, tokens, True) if not self._active: - msg = 'EVENT MANAGER: Ignoring because event manager is not active' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("inactive", "event manager is not active") if list(filter(event.type.startswith, self._ignoredEvents)): - msg = 'EVENT MANAGER: Ignoring because event type is ignored' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("type-ignored", "event type is ignored") if AXObject.get_name(app) == 'gnome-shell': if event.type.startswith('object:children-changed:remove'): - msg = 'EVENT MANAGER: Ignoring event based on type and app' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("gnome-shell", "children-changed:remove") if event.type.startswith('window'): - msg = 'EVENT MANAGER: Not ignoring because event type is never ignored' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return False + return _allow_with_reason("window-event", "event type is never ignored") if event.type.startswith('mouse:button'): - msg = 'EVENT MANAGER: Not ignoring because event type is never ignored' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return False + return _allow_with_reason("mouse-event", "event type is never ignored") if self._isDuplicateEvent(event): - msg = 'EVENT MANAGER: Ignoring duplicate event' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("duplicate", "duplicate event") # Thunderbird spams us with these when a message list thread is expanded or collapsed. if event.type.endswith('system') \ @@ -278,40 +282,29 @@ class EventManager: if AXUtilities.is_table_related(event.source) \ or AXUtilities.is_tree_related(event.source) \ or AXUtilities.is_section(event.source): - msg = 'EVENT MANAGER: Ignoring system event based on role' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("thunderbird-system", "system event based on role") if self._inDeluge() and self._ignoreDuringDeluge(event): - msg = 'EVENT MANAGER: Ignoring event type due to deluge' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("deluge", "event type during deluge") script = cthulhu_state.activeScript if event.type.startswith('object:children-changed') \ or event.type.startswith('object:state-changed:sensitive'): if not script: - msg = 'EVENT MANAGER: Ignoring because there is no active script' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("no-active-script", "no active script") if script.app != app: # Allow Steam notifications from inactive apps. if self._isSteamApp(app) and self._isSteamNotificationEvent(event): - msg = 'EVENT MANAGER: Allowing Steam notification from inactive app' - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log_allow("steam-notification", "inactive app notification") else: - msg = 'EVENT MANAGER: Ignoring because event is not from active app' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("inactive-app", "event not from active app") if event.type.startswith('object:text-changed') \ and self.EMBEDDED_OBJECT_CHARACTER in event.any_data \ and not event.any_data.replace(self.EMBEDDED_OBJECT_CHARACTER, ""): # We should also get children-changed events telling us the same thing. # Getting a bunch of both can result in a flood that grinds us to a halt. - msg = 'EVENT MANAGER: Ignoring because changed text is only embedded objects' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("embedded-only", "changed text only embedded objects") # TODO - JD: For now we won't ask for the name. Simply asking for the name should # not break anything, and should be a reliable way to quickly identify defunct @@ -321,14 +314,10 @@ class EventManager: #name = Atspi.Accessible.get_name(event.source) if AXUtilities.has_no_state(event.source): - msg = 'EVENT MANAGER: Ignoring event due to empty state set' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("empty-state", "empty state set") if AXUtilities.is_defunct(event.source): - msg = 'EVENT MANAGER: Ignoring event from defunct source' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("defunct-source", "defunct source") role = AXObject.get_role(event.source) if event.type.startswith('object:property-change:accessible-name'): @@ -344,28 +333,20 @@ class EventManager: Atspi.Role.IMAGE, # Thunderbird spam Atspi.Role.MENU, Atspi.Role.MENU_ITEM]: - msg = 'EVENT MANAGER: Ignoring event type due to role' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("name-change-role", "role filtered") # TeamTalk5 is notoriously spammy here, and name change events on widgets are # typically only presented if they are focused. if not AXUtilities.is_focused(event.source) \ and role in [Atspi.Role.PUSH_BUTTON, Atspi.Role.CHECK_BOX, Atspi.Role.RADIO_BUTTON]: - msg = 'EVENT MANAGER: Ignoring event type due to role and state' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("name-change-unfocused", "role and state") elif event.type.startswith('object:property-change:accessible-value'): if role == Atspi.Role.SPLIT_PANE and not AXUtilities.is_focused(event.source): - msg = 'EVENT MANAGER: Ignoring event type due to role and state' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("value-change-unfocused", "role and state") elif event.type.startswith('object:text-changed:insert') and event.detail2 > 1000 \ and role in [Atspi.Role.TEXT, Atspi.Role.STATIC]: - msg = 'EVENT MANAGER: Ignoring because inserted text has more than 1000 chars' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("text-insert-large", "inserted text > 1000 chars") elif event.type.startswith('object:state-changed:sensitive'): if role in [Atspi.Role.MENU_ITEM, Atspi.Role.MENU, @@ -373,14 +354,10 @@ class EventManager: Atspi.Role.PANEL, Atspi.Role.CHECK_MENU_ITEM, Atspi.Role.RADIO_MENU_ITEM]: - msg = 'EVENT MANAGER: Ignoring event type due to role' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("sensitive-role", "role filtered") elif event.type.startswith('object:state-changed:selected'): if not event.detail1 and role in [Atspi.Role.PUSH_BUTTON]: - msg = 'EVENT MANAGER: Ignoring event type due to role and detail1' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("selected-button-false", "role and detail1") elif event.type.startswith('object:state-changed:showing'): if role not in [Atspi.Role.ALERT, Atspi.Role.ANIMATION, @@ -390,39 +367,27 @@ class EventManager: Atspi.Role.DIALOG, Atspi.Role.STATUS_BAR, Atspi.Role.TOOL_TIP]: - msg = 'EVENT MANAGER: Ignoring event type due to role' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("showing-role", "role filtered") elif event.type.startswith('object:text-caret-moved'): if role in [Atspi.Role.LABEL] and not AXUtilities.is_focused(event.source): - msg = 'EVENT MANAGER: Ignoring event type due to role and state' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("caret-unfocused-label", "role and state") elif event.type.startswith('object:selection-changed'): if event.source in self._parentsOfDefunctDescendants: - msg = 'EVENT MANAGER: Ignoring event from parent of defunct descendants' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("defunct-descendant-parent", "parent of defunct descendants") if AXObject.is_dead(event.source): - msg = 'EVENT MANAGER: Ignoring event from dead source' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("dead-source", "dead source") if event.type.startswith('object:children-changed') \ or event.type.startswith('object:active-descendant-changed'): if role in [Atspi.Role.MENU, Atspi.Role.LAYERED_PANE, Atspi.Role.MENU_ITEM]: - msg = 'EVENT MANAGER: Ignoring event type due to role' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("children-role", "role filtered") if event.any_data is None: - msg = 'EVENT_MANAGER: Ignoring due to lack of event.any_data' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("missing-any-data", "no event.any_data") if event.type.endswith('remove'): if event.any_data == cthulhu_state.locusOfFocus: msg = 'EVENT MANAGER: Locus of focus is being destroyed' @@ -439,8 +404,7 @@ class EventManager: defunct = AXObject.is_dead(event.any_data) or AXUtilities.is_defunct(event.any_data) if defunct: - msg = 'EVENT MANAGER: Ignoring event for potentially-defunct child/descendant' - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log_ignore("defunct-child", "potentially defunct child/descendant") if AXUtilities.manages_descendants(event.source) \ and event.source not in self._parentsOfDefunctDescendants: self._parentsOfDefunctDescendants.append(event.source) @@ -455,21 +419,15 @@ class EventManager: # reason for ignoring it here rather than quickly processing it is the # potential for event floods like we're seeing from matrix.org. if AXUtilities.is_image(event.any_data): - msg = 'EVENT MANAGER: Ignoring event type due to role' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("child-image", "role filtered") # In normal apps we would have caught this from the parent role. # But gnome-shell has panel parents adding/removing menu items. if event.type.startswith('object:children-changed'): if AXUtilities.is_menu_item(event.any_data): - msg = 'EVENT MANAGER: Ignoring event type due to child role' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return True + return _ignore_with_reason("child-menu-item", "child role filtered") - msg = 'EVENT MANAGER: Not ignoring due to lack of cause' - debug.printMessage(debug.LEVEL_INFO, msg, True) - return False + return _allow_with_reason("no-cause", "no ignore condition met") def _addToQueue(self, event, asyncMode): debugging = debug.debugEventQueue diff --git a/src/cthulhu/flat_review.py b/src/cthulhu/flat_review.py index effc082..8cefcc3 100644 --- a/src/cthulhu/flat_review.py +++ b/src/cthulhu/flat_review.py @@ -111,24 +111,29 @@ class Word: if attr != "chars": return super().__getattribute__(attr) + chars = [] + for i, char in enumerate(self.string): + start = i + self.startOffset + extents = self._getCharExtents(start) + chars.append(Char(self, i, start, char, *extents)) + + return chars + + def _getCharExtents(self, start): # TODO - JD: For now, don't fake character and word extents. # The main goal is to improve reviewability. extents = self.x, self.y, self.width, self.height - chars = [] - for i, char in enumerate(self.string): - start = i + self.startOffset - if AXObject.supports_text(self.zone.accessible): - try: - rect = Atspi.Text.get_range_extents( - self.zone.accessible, start, start + 1, Atspi.CoordType.SCREEN) - extents = rect.x, rect.y, rect.width, rect.height - except Exception as error: - tokens = ["FLAT REVIEW: Exception in getRangeExtents:", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - chars.append(Char(self, i, start, char, *extents)) + if AXObject.supports_text(self.zone.accessible): + try: + rect = Atspi.Text.get_range_extents( + self.zone.accessible, start, start + 1, Atspi.CoordType.SCREEN) + extents = rect.x, rect.y, rect.width, rect.height + except Exception as error: + tokens = ["FLAT REVIEW: Exception in getRangeExtents:", error] + debug.printTokens(debug.LEVEL_INFO, tokens, True) - return chars + return extents def getRelativeOffset(self, offset): """Returns the char offset with respect to this word or -1.""" @@ -143,6 +148,16 @@ class Zone: """Represents text that is a portion of a single horizontal line.""" WORDS_RE = re.compile(r"(\S+\s*)", re.UNICODE) + TEXT_ROLES = ( + Atspi.Role.LABEL, + Atspi.Role.MENU, + Atspi.Role.MENU_ITEM, + Atspi.Role.CHECK_MENU_ITEM, + Atspi.Role.RADIO_MENU_ITEM, + Atspi.Role.PAGE_TAB, + Atspi.Role.PUSH_BUTTON, + Atspi.Role.TABLE_CELL, + ) def __init__(self, accessible, string, x, y, width, height, role=None): """Creates a new Zone. @@ -180,10 +195,7 @@ class Zone: if not self._shouldFakeText(): return self._words - # TODO - JD: For now, don't fake character and word extents. - # The main goal is to improve reviewability. - extents = self.x, self.y, self.width, self.height - + extents = self._getFakeTextExtents() words = [] for i, word in enumerate(re.finditer(self.WORDS_RE, self._string)): words.append(Word(self, i, word.start(), word.group(), *extents)) @@ -194,20 +206,16 @@ class Zone: def _shouldFakeText(self): """Returns True if we should try to fake the text interface""" - textRoles = [Atspi.Role.LABEL, - Atspi.Role.MENU, - Atspi.Role.MENU_ITEM, - Atspi.Role.CHECK_MENU_ITEM, - Atspi.Role.RADIO_MENU_ITEM, - Atspi.Role.PAGE_TAB, - Atspi.Role.PUSH_BUTTON, - Atspi.Role.TABLE_CELL] - - if self.role in textRoles: + if self.role in self.TEXT_ROLES: return True return False + def _getFakeTextExtents(self): + # TODO - JD: For now, don't fake character and word extents. + # The main goal is to improve reviewability. + return self.x, self.y, self.width, self.height + def _extentsAreOnSameLine(self, zone, pixelDelta=5): """Returns True if this Zone is physically on the same line as zone.""" @@ -291,7 +299,7 @@ class TextZone(Zone): string = AXText.get_substring(self.accessible, self.startOffset, self.endOffset) words = [] for i, word in enumerate(re.finditer(self.WORDS_RE, string)): - start, end = map(lambda x: x + self.startOffset, word.span()) + start, end = (pos + self.startOffset for pos in word.span()) try: rect = Atspi.Text.get_range_extents(self.accessible, start, end, Atspi.CoordType.SCREEN) extents = rect.x, rect.y, rect.width, rect.height @@ -338,6 +346,9 @@ class StateZone(Zone): else: generator = cthulhu_state.activeScript.brailleGenerator + return self._getStateString(generator) + + def _getStateString(self, generator): result = generator.getStateIndicator(self.accessible, role=self.role) if result: return result[0] @@ -362,21 +373,33 @@ class ValueZone(Zone): else: generator = cthulhu_state.activeScript.brailleGenerator - result = "" + return self._getValueString(generator) + def _getValueString(self, generator): # TODO - JD: This cobbling together beats what we had, but the # generators should also be doing the assembly. rolename = generator.getLocalizedRoleName(self.accessible) value = generator.getValue(self.accessible) if rolename and value: - result = f"{rolename} {value[0]}" + return f"{rolename} {value[0]}" - return result + return "" class Line: """A Line is a single line across a window and is composed of Zones.""" + TEXT_BRAILLE_ROLES = ( + Atspi.Role.TEXT, + Atspi.Role.PASSWORD_TEXT, + Atspi.Role.TERMINAL, + ) + TEXT_BRAILLE_FALLBACK_ROLES = ( + Atspi.Role.PARAGRAPH, + Atspi.Role.HEADING, + Atspi.Role.LINK, + ) + def __init__(self, index, zones): @@ -395,16 +418,16 @@ class Line: return " ".join([zone.string for zone in self.zones]) if attr == "x": - return min([zone.x for zone in self.zones]) + return min(zone.x for zone in self.zones) if attr == "y": - return min([zone.y for zone in self.zones]) + return min(zone.y for zone in self.zones) if attr == "width": - return sum([zone.width for zone in self.zones]) + return sum(zone.width for zone in self.zones) if attr == "height": - return max([zone.height for zone in self.zones]) + return max(zone.height for zone in self.zones) return super().__getattribute__(attr) @@ -420,19 +443,13 @@ class Line: # The 'isinstance(zone, TextZone)' test is a sanity check # to handle problems with Java text. See Bug 435553. if isinstance(zone, TextZone) and \ - ((AXObject.get_role(zone.accessible) in \ - (Atspi.Role.TEXT, - Atspi.Role.PASSWORD_TEXT, - Atspi.Role.TERMINAL)) or \ + ((AXObject.get_role(zone.accessible) in self.TEXT_BRAILLE_ROLES) or \ # [[[TODO: Eitan - HACK: # This is just to get FF3 cursor key routing support. # We really should not be determining all this stuff here, # it should be in the scripts. # Same applies to roles above.]]] - (AXObject.get_role(zone.accessible) in \ - (Atspi.Role.PARAGRAPH, - Atspi.Role.HEADING, - Atspi.Role.LINK))): + (AXObject.get_role(zone.accessible) in self.TEXT_BRAILLE_FALLBACK_ROLES)): region = braille.ReviewText(zone.accessible, zone.string, zone.startOffset, @@ -485,6 +502,17 @@ class Context: WRAP_TOP_BOTTOM = 1 << 1 WRAP_ALL = (WRAP_LINE | WRAP_TOP_BOTTOM) + CONTAINER_ROLES = (Atspi.Role.MENU,) + VALUE_ZONE_ROLES = (Atspi.Role.SCROLL_BAR, Atspi.Role.SLIDER, Atspi.Role.PROGRESS_BAR) + REDUNDANT_NAME_ROLES = (Atspi.Role.TABLE_ROW,) + USELESS_NAME_ROLES = (Atspi.Role.TABLE_CELL, Atspi.Role.LABEL) + STATE_ZONE_ROLES = ( + Atspi.Role.CHECK_BOX, + Atspi.Role.CHECK_MENU_ITEM, + Atspi.Role.RADIO_BUTTON, + Atspi.Role.RADIO_MENU_ITEM, + ) + def __init__(self, script, root=None): """Create a new Context for script.""" @@ -518,10 +546,8 @@ class Context: tokens = ["ERROR: Exception getting extents of", self.topLevel] debug.printTokens(debug.LEVEL_INFO, tokens, True) - containerRoles = [Atspi.Role.MENU] - def isContainer(x): - return AXObject.get_role(x) in containerRoles + return AXObject.get_role(x) in self.CONTAINER_ROLES container = AXObject.find_ancestor(self.focusObj, isContainer) if not container and isContainer(self.focusObj): @@ -567,8 +593,9 @@ class Context: cliprect = self._ensureRect(cliprect) zones = [] - substrings = [(*m.span(), m.group(0)) for m in re.finditer(r"[^\ufffc]+", string)] - substrings = list(map(lambda x: (x[0] + startOffset, x[1] + startOffset, x[2]), substrings)) + substrings = [(*m.span(), m.group(0)) for m in EMBEDDED_OBJECT_RE.finditer(string)] + substrings = [(start + startOffset, end + startOffset, text) + for (start, end, text) in substrings] for (start, end, substring) in substrings: try: rect = Atspi.Text.get_range_extents(accessible, start, end, Atspi.CoordType.SCREEN) @@ -581,23 +608,6 @@ class Context: return zones - def _getLines(self, accessible, startOffset, endOffset): - # TODO - JD: Move this into the script utilities so we can better handle - # app and toolkit quirks and also reuse this (e.g. for SayAll). - if not AXObject.supports_text(accessible): - return [] - - lines = [] - offset = startOffset - maxOffset = min(endOffset, AXText.get_character_count(accessible)) - while offset < maxOffset: - line, start, end = AXText.get_line_at_offset(accessible, offset) - if line and (line, start, end) not in lines: - lines.append((line, start, end)) - offset = max(end, offset + 1) - - return lines - def getZonesFromText(self, accessible, cliprect): """Gets a list of Zones from an object that implements the AccessibleText specialization. @@ -613,15 +623,9 @@ class Context: if not self.script.utilities.hasPresentableText(accessible): return [] - zones = [] - - # TODO - JD: This is here temporarily whilst I sort out the rest - # of the text-related mess. - if AXObject.supports_editable_text(accessible) \ - and AXUtilities.is_single_line(accessible): - rect = AXComponent.get_rect(accessible) - return [TextZone(accessible, 0, AXText.get_substring(accessible, 0, -1), - rect.x, rect.y, rect.width, rect.height)] + zones = self._getSingleLineEditableZones(accessible) + if zones: + return zones upperMax = lowerMax = AXText.get_character_count(accessible) upperMid = lowerMid = int(upperMax / 2) @@ -657,7 +661,7 @@ class Context: msg = "FLAT REVIEW: Getting lines for %s offsets %i-%i" % (accessible, upperMin, lowerMax) debug.printMessage(debug.LEVEL_INFO, msg, True) - lines = self._getLines(accessible, upperMin, lowerMax) + lines = self.script.utilities.getLinesForRange(accessible, upperMin, lowerMax) tokens = ["FLAT REVIEW:", len(lines), "lines found for", accessible] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -666,6 +670,17 @@ class Context: return zones + def _getSingleLineEditableZones(self, accessible): + # TODO - JD: This is here temporarily whilst I sort out the rest + # of the text-related mess. + if not (AXObject.supports_editable_text(accessible) + and AXUtilities.is_single_line(accessible)): + return [] + + rect = AXComponent.get_rect(accessible) + return [TextZone(accessible, 0, AXText.get_substring(accessible, 0, -1), + rect.x, rect.y, rect.width, rect.height)] + def _insertStateZone(self, zones, accessible, extents): """If the accessible presents non-textual state, such as a checkbox or radio button, insert a StateZone representing @@ -687,10 +702,7 @@ class Context: and self.script.utilities.hasMeaningfulToggleAction(accessible): role = Atspi.Role.CHECK_BOX - if role not in [Atspi.Role.CHECK_BOX, - Atspi.Role.CHECK_MENU_ITEM, - Atspi.Role.RADIO_BUTTON, - Atspi.Role.RADIO_MENU_ITEM]: + if role not in self.STATE_ZONE_ROLES: return zone = None @@ -733,18 +745,14 @@ class Context: return [] zones = self.getZonesFromText(accessible, cliprect) - if not zones and role in [Atspi.Role.SCROLL_BAR, - Atspi.Role.SLIDER, - Atspi.Role.PROGRESS_BAR]: + if not zones and role in self.VALUE_ZONE_ROLES: zones.append(ValueZone(accessible, *extents)) elif not zones: string = "" - redundant = [Atspi.Role.TABLE_ROW] - if role not in redundant: + if role not in self.REDUNDANT_NAME_ROLES: string = self.script.speechGenerator.getName(accessible, inFlatReview=True) - useless = [Atspi.Role.TABLE_CELL, Atspi.Role.LABEL] - if not string and role not in useless: + if not string and role not in self.USELESS_NAME_ROLES: string = self.script.speechGenerator.getRoleName(accessible) if string: zones.append(Zone(accessible, string, *extents)) @@ -1467,3 +1475,4 @@ class Context: raise Exception("Invalid type: %d" % flatReviewType) return moved +EMBEDDED_OBJECT_RE = re.compile(r"[^\ufffc]+") diff --git a/src/cthulhu/input_event_manager.py b/src/cthulhu/input_event_manager.py index e83d36c..d680677 100644 --- a/src/cthulhu/input_event_manager.py +++ b/src/cthulhu/input_event_manager.py @@ -332,7 +332,7 @@ class InputEventManager: manager = focus_manager.get_manager() if pressed: window = manager.get_active_window() - if not AXUtilities.can_be_active_window(window): + if not AXUtilities.can_be_active_window(window, clear_cache=True): new_window = AXUtilities.find_active_window() if new_window is not None: window = new_window diff --git a/src/cthulhu/script_utilities.py b/src/cthulhu/script_utilities.py index 62f8e91..931342b 100644 --- a/src/cthulhu/script_utilities.py +++ b/src/cthulhu/script_utilities.py @@ -101,6 +101,22 @@ class Utilities: SUBSCRIPT_DIGITS = \ ['\u2080', '\u2081', '\u2082', '\u2083', '\u2084', '\u2085', '\u2086', '\u2087', '\u2088', '\u2089'] + MENU_ROLES_IN_OPEN_MENU = { + Atspi.Role.MENU, + Atspi.Role.MENU_ITEM, + Atspi.Role.CHECK_MENU_ITEM, + Atspi.Role.RADIO_MENU_ITEM, + Atspi.Role.SEPARATOR, + } + ZOMBIE_TOP_LEVEL_ROLES = { + Atspi.Role.APPLICATION, + Atspi.Role.ALERT, + Atspi.Role.DIALOG, + Atspi.Role.LABEL, # For Unity Panel Service bug + Atspi.Role.PAGE, # For Evince bug + Atspi.Role.WINDOW, + Atspi.Role.FRAME, + } flags = re.UNICODE WORDS_RE = re.compile(r"(\W+)", flags) @@ -3687,6 +3703,21 @@ class Utilities: debug.printException(debug.LEVEL_WARNING) return "" + def getLinesForRange(self, obj, startOffset, endOffset): + if not AXObject.supports_text(obj): + return [] + + lines = [] + offset = startOffset + maxOffset = min(endOffset, AXText.get_character_count(obj)) + while offset < maxOffset: + line, start, end = AXText.get_line_at_offset(obj, offset) + if line and (line, start, end) not in lines: + lines.append((line, start, end)) + offset = max(end, offset + 1) + + return lines + def getLineContentsAtOffset(self, obj, offset, layoutMode=True, useCache=True): return [] @@ -4573,30 +4604,23 @@ class Utilities: # seems to be present in multiple toolkits, so it's either being # inherited (e.g. from Gtk in Firefox Chrome, LO, Eclipse) or it # may be an AT-SPI2 bug. For now, handling it here. - menuRoles = [Atspi.Role.MENU, - Atspi.Role.MENU_ITEM, - Atspi.Role.CHECK_MENU_ITEM, - Atspi.Role.RADIO_MENU_ITEM, - Atspi.Role.SEPARATOR] - if AXObject.get_role(obj) in menuRoles and self.isInOpenMenuBarMenu(obj): + if self._is_open_menu_bar_menu_role(obj): tokens = ["HACK: Treating", obj, "as showing and visible"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return True return False + def _is_open_menu_bar_menu_role(self, obj): + if AXObject.get_role(obj) not in self.MENU_ROLES_IN_OPEN_MENU: + return False + return self.isInOpenMenuBarMenu(obj) + def isZombie(self, obj): index = AXObject.get_index_in_parent(obj) - topLevelRoles = [Atspi.Role.APPLICATION, - Atspi.Role.ALERT, - Atspi.Role.DIALOG, - Atspi.Role.LABEL, # For Unity Panel Service bug - Atspi.Role.PAGE, # For Evince bug - Atspi.Role.WINDOW, - Atspi.Role.FRAME] role = AXObject.get_role(obj) tokens = ["SCRIPT UTILITIES: ", obj, "is zombie:"] - if index == -1 and role not in topLevelRoles: + if index == -1 and role not in self.ZOMBIE_TOP_LEVEL_ROLES: tokens.append("index is -1") debug.printTokens(debug.LEVEL_INFO, tokens, True) return True From fde43df2d072f5cfca1b84f35c5c5a7c461b5076 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 16 Jan 2026 10:45:33 -0500 Subject: [PATCH 10/13] Finished with this section of the refactor. Only a couple more to go. --- src/cthulhu/flat_review.py | 75 +++++++++--------- src/cthulhu/script_utilities.py | 133 ++++++++++++++++++++++---------- 2 files changed, 133 insertions(+), 75 deletions(-) diff --git a/src/cthulhu/flat_review.py b/src/cthulhu/flat_review.py index 8cefcc3..5a5735d 100644 --- a/src/cthulhu/flat_review.py +++ b/src/cthulhu/flat_review.py @@ -120,8 +120,7 @@ class Word: return chars def _getCharExtents(self, start): - # TODO - JD: For now, don't fake character and word extents. - # The main goal is to improve reviewability. + # NOTE: We intentionally avoid faking character extents to improve reviewability. extents = self.x, self.y, self.width, self.height if AXObject.supports_text(self.zone.accessible): @@ -212,8 +211,7 @@ class Zone: return False def _getFakeTextExtents(self): - # TODO - JD: For now, don't fake character and word extents. - # The main goal is to improve reviewability. + # NOTE: We intentionally avoid faking word extents to improve reviewability. return self.x, self.y, self.width, self.height def _extentsAreOnSameLine(self, zone, pixelDelta=5): @@ -376,8 +374,7 @@ class ValueZone(Zone): return self._getValueString(generator) def _getValueString(self, generator): - # TODO - JD: This cobbling together beats what we had, but the - # generators should also be doing the assembly. + # NOTE: This assembly is a stopgap until generators handle it directly. rolename = generator.getLocalizedRoleName(self.accessible) value = generator.getValue(self.accessible) if rolename and value: @@ -440,29 +437,7 @@ class Line: self.brailleRegions = [] brailleOffset = 0 for zone in self.zones: - # The 'isinstance(zone, TextZone)' test is a sanity check - # to handle problems with Java text. See Bug 435553. - if isinstance(zone, TextZone) and \ - ((AXObject.get_role(zone.accessible) in self.TEXT_BRAILLE_ROLES) or \ - # [[[TODO: Eitan - HACK: - # This is just to get FF3 cursor key routing support. - # We really should not be determining all this stuff here, - # it should be in the scripts. - # Same applies to roles above.]]] - (AXObject.get_role(zone.accessible) in self.TEXT_BRAILLE_FALLBACK_ROLES)): - region = braille.ReviewText(zone.accessible, - zone.string, - zone.startOffset, - zone) - else: - try: - brailleString = zone.brailleString - except Exception: - brailleString = zone.string - region = braille.ReviewComponent(zone.accessible, - brailleString, - 0, # cursor offset - zone) + region = self._createBrailleRegion(zone) if len(self.brailleRegions): pad = braille.Region(" ") pad.brailleOffset = brailleOffset @@ -488,6 +463,36 @@ class Line: return self.brailleRegions + def _createBrailleRegion(self, zone): + if self._shouldUseTextBrailleRegion(zone): + return braille.ReviewText(zone.accessible, + zone.string, + zone.startOffset, + zone) + + try: + brailleString = zone.brailleString + except Exception: + brailleString = zone.string + + return braille.ReviewComponent(zone.accessible, + brailleString, + 0, # cursor offset + zone) + + def _shouldUseTextBrailleRegion(self, zone): + # The 'isinstance(zone, TextZone)' test is a sanity check + # to handle problems with Java text. See Bug 435553. + if not isinstance(zone, TextZone): + return False + + role = AXObject.get_role(zone.accessible) + if role in self.TEXT_BRAILLE_ROLES: + return True + + # NOTE: Fallback roles kept for legacy cursor key routing support. + return role in self.TEXT_BRAILLE_FALLBACK_ROLES + class Context: """Contains the flat review regions for the current top-level object.""" @@ -671,8 +676,7 @@ class Context: return zones def _getSingleLineEditableZones(self, accessible): - # TODO - JD: This is here temporarily whilst I sort out the rest - # of the text-related mess. + # NOTE: Temporary workaround while text handling is refined. if not (AXObject.supports_editable_text(accessible) and AXUtilities.is_single_line(accessible)): return [] @@ -686,8 +690,7 @@ class Context: checkbox or radio button, insert a StateZone representing that state.""" - # TODO - JD: This whole thing is pretty hacky. Either do it - # right or nuke it. + # NOTE: Heuristic state-zone placement until handled by scripts. extents = self._ensureRect(extents) indicatorExtents = [extents.x, extents.y, 1, extents.height] @@ -910,8 +913,7 @@ class Context: def getCurrent(self, flatReviewType=ZONE): """Returns the current string, offset, and extent information.""" - # TODO - JD: This method has not (yet) been renamed. But we have a - # getter and setter which do totally different things.... + # NOTE: Legacy name; prefer getCurrentItem() for clarity. zone = self._getCurrentZone() if not zone: @@ -930,6 +932,9 @@ class Context: return current.string, current.x, current.y, current.width, current.height + def getCurrentItem(self, flatReviewType=ZONE): + return self.getCurrent(flatReviewType) + def setCurrent(self, lineIndex, zoneIndex, wordIndex, charIndex): """Sets the current character of interest. diff --git a/src/cthulhu/script_utilities.py b/src/cthulhu/script_utilities.py index 931342b..2a02e00 100644 --- a/src/cthulhu/script_utilities.py +++ b/src/cthulhu/script_utilities.py @@ -117,6 +117,18 @@ class Utilities: Atspi.Role.WINDOW, Atspi.Role.FRAME, } + DISPLAYED_TEXT_DIRECT_NAME_ROLES = { + Atspi.Role.PUSH_BUTTON, + Atspi.Role.LABEL, + } + DISPLAYED_TEXT_SKIP_NAME_ROLES = { + Atspi.Role.COMBO_BOX, + Atspi.Role.SPIN_BUTTON, + } + DISPLAYED_TEXT_LABEL_FALLBACK_ROLES = { + Atspi.Role.PUSH_BUTTON, + Atspi.Role.LIST_ITEM, + } flags = re.UNICODE WORDS_RE = re.compile(r"(\W+)", flags) @@ -485,7 +497,7 @@ class Utilities: any text being shown. """ - # TODO - JD: It's finally time to consider killing this for real. + # NOTE: Legacy behavior; removal requires thorough testing. try: return self._script.generatorCache[self.DISPLAYED_TEXT][obj] @@ -494,25 +506,16 @@ class Utilities: name = AXObject.get_name(obj) role = AXObject.get_role(obj) - if role in [Atspi.Role.PUSH_BUTTON, Atspi.Role.LABEL] and name: - return name - if AXObject.supports_text(obj): - displayedText = AXText.get_all_text(obj) - if self.EMBEDDED_OBJECT_CHARACTER in displayedText: - displayedText = None + directText = self._getDisplayedTextDirectName(role, name) + if directText is not None: + return directText - if not displayedText and role not in [Atspi.Role.COMBO_BOX, Atspi.Role.SPIN_BUTTON]: - # TODO - JD: This should probably get nuked. But all sorts of - # existing code might be relying upon this bogus hack. So it - # will need thorough testing when removed. - displayedText = name - - if not displayedText and role in [Atspi.Role.PUSH_BUTTON, Atspi.Role.LIST_ITEM]: - labels = self.unrelatedLabels(obj, minimumWords=1) - if not labels: - labels = self.unrelatedLabels(obj, onlyShowing=False, minimumWords=1) - displayedText = " ".join(map(self.displayedText, labels)) + displayedText = self._getDisplayedTextFromText(obj) + if not displayedText: + displayedText = self._getDisplayedTextFallbackName(role, name) + if not displayedText: + displayedText = self._getDisplayedTextFromLabels(obj, role) if self.DISPLAYED_TEXT not in self._script.generatorCache: self._script.generatorCache[self.DISPLAYED_TEXT] = {} @@ -520,6 +523,37 @@ class Utilities: self._script.generatorCache[self.DISPLAYED_TEXT][obj] = displayedText return self._script.generatorCache[self.DISPLAYED_TEXT][obj] + def _getDisplayedTextDirectName(self, role, name): + if role in self.DISPLAYED_TEXT_DIRECT_NAME_ROLES and name: + return name + return None + + def _getDisplayedTextFromText(self, obj): + if not AXObject.supports_text(obj): + return None + + displayedText = AXText.get_all_text(obj) + if self.EMBEDDED_OBJECT_CHARACTER in displayedText: + return None + + return displayedText + + def _getDisplayedTextFallbackName(self, role, name): + # NOTE: Legacy fallback; removal requires thorough testing. + if name and role not in self.DISPLAYED_TEXT_SKIP_NAME_ROLES: + return name + + return None + + def _getDisplayedTextFromLabels(self, obj, role): + if role not in self.DISPLAYED_TEXT_LABEL_FALLBACK_ROLES: + return None + + labels = self.unrelatedLabels(obj, minimumWords=1) + if not labels: + labels = self.unrelatedLabels(obj, onlyShowing=False, minimumWords=1) + return " ".join(map(self.displayedText, labels)) + def documentFrame(self, obj=None): """Returns the document frame which is displaying the content. Note that this is intended primarily for web content.""" @@ -1548,8 +1582,10 @@ class Utilities: if self.isLink(obj): return False - # TODO - JD: This might have been enough way back when, but additional - # checks are needed now. + return self._isTextAreaByType(obj) + + def _isTextAreaByType(self, obj): + # NOTE: This is legacy and may need more checks now. return AXUtilities.is_text_input(obj) \ or AXUtilities.is_text(obj) \ or AXUtilities.is_paragraph(obj) @@ -2669,19 +2705,30 @@ class Utilities: msg = "SCRIPT UTILITIES: Broken text insertion event" debug.printMessage(debug.LEVEL_INFO, msg, True) - if AXUtilities.is_password_text(event.source): - text = self.queryNonEmptyText(event.source) - if text: - string = AXText.get_all_text(event.source) - if string: - tokens = ["HACK: Returning last char in '", string, "'"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return string[-1] + fallbackText = self._getFallbackInsertedText(event) + if fallbackText: + return fallbackText msg = "FAIL: Unable to correct broken text insertion event" debug.printMessage(debug.LEVEL_INFO, msg, True) return "" + def _getFallbackInsertedText(self, event): + if not AXUtilities.is_password_text(event.source): + return "" + + text = self.queryNonEmptyText(event.source) + if not text: + return "" + + string = AXText.get_all_text(event.source) + if not string: + return "" + + tokens = ["HACK: Returning last char in '", string, "'"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return string[-1] + def selectedText(self, obj): """Get the text selection for the given object. @@ -4600,10 +4647,9 @@ class Utilities: if AXUtilities.is_showing(obj) and AXUtilities.is_visible(obj): return True - # TODO - JD: This really should be in the toolkit scripts. But it - # seems to be present in multiple toolkits, so it's either being - # inherited (e.g. from Gtk in Firefox Chrome, LO, Eclipse) or it - # may be an AT-SPI2 bug. For now, handling it here. + # NOTE: This likely belongs in toolkit scripts, but it's shared across + # toolkits (Gtk, Firefox, Chrome, LO, Eclipse) and might be an AT-SPI2 + # issue, so we keep it here for now. if self._is_open_menu_bar_menu_role(obj): tokens = ["HACK: Treating", obj, "as showing and visible"] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -4788,19 +4834,22 @@ class Utilities: for x in [k for k in textSelections.keys() if textSelections.get(k) == value]: textSelections.pop(x) - # TODO: JD - this doesn't yet handle the case of multiple non-contiguous - # selections in a single accessible object. start, end, string = 0, 0, '' if text: - string, start, end = AXText.get_selected_text(obj) - if string: - string = self.expandEOCs(obj, start, end) + string, start, end = self._getSingleSelectionText(obj) tokens = ["SCRIPT UTILITIES: New selection for", obj, f"is '{string}' ({start}, {end})"] debug.printTokens(debug.LEVEL_INFO, tokens, True) textSelections[hash(obj)] = start, end, string self._script.pointOfReference['textSelections'] = textSelections + def _getSingleSelectionText(self, obj): + # NOTE: Does not handle multiple non-contiguous selections. + string, start, end = AXText.get_selected_text(obj) + if string: + string = self.expandEOCs(obj, start, end) + return string, start, end + @staticmethod def onClipboardContentsChanged(*args): script = cthulhu_state.activeScript @@ -5135,9 +5184,13 @@ class Utilities: if bool(re.search(r"\w", event.any_data)) != bool(re.search(r"\w", contents)): return False - # HACK: If the application treats each paragraph as a separate object, - # we'll get individual events for each paragraph rather than a single - # event whose any_data matches the clipboard contents. + if self._isParagraphClipboardEvent(event, contents): + return True + + return False + + def _isParagraphClipboardEvent(self, event, contents): + # NOTE: Paragraph-per-object toolkits can emit per-paragraph events. if "\n" in contents and event.any_data.rstrip() in contents: return True From 1862de64eef84075e2107ea694d643f5c7fce096 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 16 Jan 2026 11:14:32 -0500 Subject: [PATCH 11/13] Getting very close to final refactor push. I think one more after this one will do it. --- src/cthulhu/ax_utilities_role.py | 3 +- src/cthulhu/braille_generator.py | 3 +- src/cthulhu/formatting.py | 47 +++---- src/cthulhu/generator.py | 130 ++++++++++++------ src/cthulhu/meson.build | 1 + src/cthulhu/role_keys.py | 57 ++++++++ .../scripts/apps/soffice/formatting.py | 5 +- .../scripts/apps/soffice/speech_generator.py | 5 +- src/cthulhu/scripts/web/speech_generator.py | 7 +- src/cthulhu/sound_generator.py | 3 +- src/cthulhu/sound_theme_manager.py | 4 +- src/cthulhu/speech_generator.py | 80 +++++------ 12 files changed, 232 insertions(+), 113 deletions(-) create mode 100644 src/cthulhu/role_keys.py diff --git a/src/cthulhu/ax_utilities_role.py b/src/cthulhu/ax_utilities_role.py index 269d39d..7428867 100644 --- a/src/cthulhu/ax_utilities_role.py +++ b/src/cthulhu/ax_utilities_role.py @@ -40,6 +40,7 @@ from gi.repository import Atspi from . import debug from . import object_properties +from . import role_keys from .ax_object import AXObject from .ax_utilities_state import AXUtilitiesState @@ -386,7 +387,7 @@ class AXUtilitiesRole: return object_properties.ROLE_PROLOGUE if AXUtilitiesRole.is_dpub_toc(obj, role): return object_properties.ROLE_TOC - elif role == "ROLE_DPUB_SECTION": + elif role == role_keys.ROLE_DPUB_SECTION: if AXUtilitiesRole.is_dpub_abstract(obj, role): return object_properties.ROLE_ABSTRACT if AXUtilitiesRole.is_dpub_colophon(obj, role): diff --git a/src/cthulhu/braille_generator.py b/src/cthulhu/braille_generator.py index d8021d8..33f3001 100644 --- a/src/cthulhu/braille_generator.py +++ b/src/cthulhu/braille_generator.py @@ -40,6 +40,7 @@ from . import debug from . import generator from . import messages from . import object_properties +from . import role_keys from . import cthulhu_state from . import settings from . import settings_manager @@ -453,7 +454,7 @@ class BrailleGenerator(generator.Generator): # We will provide the support for someone to override this, # however, so we use REAL_ROLE_SCROLL_PANE here. # - oldRole = self._overrideRole('REAL_ROLE_SCROLL_PANE', args) + oldRole = self._overrideRole(role_keys.REAL_ROLE_SCROLL_PANE, args) result.extend(self.generate(obj, **args)) self._restoreRole(oldRole, args) return result diff --git a/src/cthulhu/formatting.py b/src/cthulhu/formatting.py index 929c4a1..6c253a6 100644 --- a/src/cthulhu/formatting.py +++ b/src/cthulhu/formatting.py @@ -44,6 +44,7 @@ gi.require_version("Atspi", "2.0") from gi.repository import Atspi from . import object_properties +from . import role_keys TUTORIAL = '(tutorial and (pause + tutorial) or [])' MNEMONIC = '(mnemonic and (pause + mnemonic + lineBreak) or [])' @@ -153,7 +154,7 @@ formatting = { 'focused': 'labelOrName + roleName', 'unfocused': 'labelOrName + roleName + pause + currentLineText + allTextSelection', }, - 'ROLE_ARTICLE_IN_FEED' : { + role_keys.ROLE_ARTICLE_IN_FEED: { 'unfocused': '(labelOrName or currentLineText or roleName) + pause + positionInList', }, Atspi.Role.BLOCK_QUOTE: { @@ -197,25 +198,25 @@ formatting = { 'unfocused': 'labelOrName + roleName + pause + currentLineText + allTextSelection', }, # TODO - JD: When we bump dependencies to 2.34, remove this fake role and use the real one. - 'ROLE_CONTENT_DELETION': { + role_keys.ROLE_CONTENT_DELETION: { 'focused': 'leaving or deletionStart', 'unfocused': 'deletionStart + pause + displayedText + pause + deletionEnd', }, - 'ROLE_CONTENT_ERROR': { + role_keys.ROLE_CONTENT_ERROR: { 'unfocused': 'displayedText + pause + invalid', }, # TODO - JD: When we bump dependencies to 2.34, remove this fake role and use the real one. - 'ROLE_CONTENT_INSERTION': { + role_keys.ROLE_CONTENT_INSERTION: { 'focused': 'leaving or insertionStart', 'unfocused': 'insertionStart + pause + displayedText + pause + insertionEnd', }, # TODO - JD: When we bump dependencies to 2.36, remove this fake role and use the real one. - 'ROLE_CONTENT_MARK': { + role_keys.ROLE_CONTENT_MARK: { 'focused': 'leaving or markStart', 'unfocused': 'markStart + pause + displayedText + pause + markEnd', }, # TODO - JD: When we bump dependencies to 2.36, remove this fake role and use the real one. - 'ROLE_CONTENT_SUGGESTION': { + role_keys.ROLE_CONTENT_SUGGESTION: { 'focused': 'leaving or roleName', }, Atspi.Role.DESCRIPTION_LIST: { @@ -247,11 +248,11 @@ formatting = { 'basicWhereAmI': 'labelOrName + readOnly + textRole + textContent + anyTextSelection + ' + MNEMONIC, 'detailedWhereAmI': 'labelorName + readOnly + textRole + textContentWithAttributes + anyTextSelection + ' + MNEMONIC }, - 'ROLE_DPUB_LANDMARK': { + role_keys.ROLE_DPUB_LANDMARK: { 'focused': 'leaving or labelOrName', 'unfocused': 'labelOrName + currentLineText + allTextSelection' }, - 'ROLE_DPUB_SECTION': { + role_keys.ROLE_DPUB_SECTION: { 'focused': 'leaving or (labelOrName + roleName)', 'unfocused': 'labelOrName + currentLineText + allTextSelection' }, @@ -265,7 +266,7 @@ formatting = { 'basicWhereAmI': 'labelOrName + readOnly + textRole + (textContent or placeholderText) + anyTextSelection + required + pause + invalid + ' + MNEMONIC, 'detailedWhereAmI': 'labelOrName + readOnly + textRole + (textContentWithAttributes or placeholderText) + anyTextSelection + required + pause + invalid + ' + MNEMONIC, }, - 'ROLE_FEED': { + role_keys.ROLE_FEED: { 'focused': 'leaving or (labelOrName + pause + (numberOfChildren or roleName))', 'unfocused': 'labelOrName + pause + (numberOfChildren or roleName)', }, @@ -341,11 +342,11 @@ formatting = { 'unfocused': 'math', }, # TODO - JD: When we bump dependencies to TBD, remove this fake role and use the real one. - 'ROLE_MATH_ENCLOSED': { + role_keys.ROLE_MATH_ENCLOSED: { 'unfocused': 'enclosedBase + enclosedEnclosures', }, # TODO - JD: When we bump dependencies to TBD, remove this fake role and use the real one. - 'ROLE_MATH_FENCED': { + role_keys.ROLE_MATH_FENCED: { 'unfocused': 'fencedStart + pause + fencedContents + pause + fencedEnd', }, Atspi.Role.MATH_FRACTION: { @@ -355,23 +356,23 @@ formatting = { 'unfocused': 'rootStart + rootBase + pause + rootEnd + pause', }, # TODO - JD: When we bump dependencies to TBD, remove this fake role and use the real one. - 'ROLE_MATH_MULTISCRIPT': { + role_keys.ROLE_MATH_MULTISCRIPT: { 'unfocused': 'scriptBase + pause + scriptPrescripts + pause + scriptPostscripts + pause', }, # TODO - JD: When we bump dependencies to TBD, remove this fake role and use the real one. - 'ROLE_MATH_SCRIPT_SUBSUPER': { + role_keys.ROLE_MATH_SCRIPT_SUBSUPER: { 'unfocused': 'scriptBase + pause + scriptSubscript + pause + scriptSuperscript + pause', }, # TODO - JD: When we bump dependencies to TBD, remove this fake role and use the real one. - 'ROLE_MATH_SCRIPT_UNDEROVER': { + role_keys.ROLE_MATH_SCRIPT_UNDEROVER: { 'unfocused': 'scriptBase + pause + scriptUnderscript + pause + scriptOverscript + pause', }, # TODO - JD: When we bump dependencies to TBD, remove this fake role and use the real one. - 'ROLE_MATH_TABLE': { + role_keys.ROLE_MATH_TABLE: { 'unfocused': 'mathTableStart + pause + mathTableRows + pause + mathTableEnd + pause', }, # TODO - JD: When we bump dependencies to TBD, remove this fake role and use the real one. - 'ROLE_MATH_TABLE_ROW': { + role_keys.ROLE_MATH_TABLE_ROW: { 'unfocused': 'mathRow', }, Atspi.Role.MENU: { @@ -440,7 +441,7 @@ formatting = { 'unfocused': 'labelOrName + radioState + roleName + availability + ' + MNEMONIC + ' + accelerator + positionInList', 'basicWhereAmI': 'ancestors + labelOrName + roleName + radioState + accelerator + positionInList + ' + MNEMONIC }, - 'ROLE_REGION': { + role_keys.ROLE_REGION: { 'focused': 'leaving or (roleName + labelOrName)', 'unfocused': 'labelOrName + roleName + currentLineText + allTextSelection' }, @@ -492,7 +493,7 @@ formatting = { Atspi.Role.SUPERSCRIPT: { 'unfocused': 'roleName + currentLineText + allTextSelection', }, - 'ROLE_SWITCH': { + role_keys.ROLE_SWITCH: { 'focused': 'switchState', 'unfocused': 'labelOrName + roleName + switchState + availability + ' + MNEMONIC + ' + accelerator', 'basicWhereAmI': 'labelOrName + roleName + switchState' @@ -509,7 +510,7 @@ formatting = { 'basicWhereAmI': 'parentRoleName + pause + columnHeader + pause + rowHeader + pause + roleName + pause + cellCheckedState + pause + (realActiveDescendantDisplayedText or imageDescription + image) + pause + columnAndRow + pause + expandableState + pause + nodeLevel + pause', 'detailedWhereAmI': 'parentRoleName + pause + columnHeader + pause + rowHeader + pause + roleName + pause + cellCheckedState + pause + (realActiveDescendantDisplayedText or imageDescription + image) + pause + columnAndRow + pause + tableCellRow + pause + expandableState + pause + nodeLevel + pause', }, - 'REAL_ROLE_TABLE_CELL': { + role_keys.REAL_ROLE_TABLE_CELL: { # the real cell information # note that Atspi.Role.TABLE_CELL is used to work out if we need to # read a whole row. It calls REAL_ROLE_TABLE_CELL internally. @@ -630,7 +631,7 @@ formatting = { or ([Component(obj, asString(labelAndName + roleName))]\ + (childWidget and ([Region(" ")] + childWidget))))' }, - 'ROLE_ARTICLE_IN_FEED': { + role_keys.ROLE_ARTICLE_IN_FEED: { 'unfocused': '((substring and ' + BRAILLE_TEXT + ')\ or ([Component(obj, asString(labelOrName + roleName))]))' }, @@ -830,7 +831,7 @@ formatting = { 'unfocused': '[Component(obj, asString(labelOrName + roleName))]\ + [Region(" ")] + statusBar', }, - 'ROLE_SWITCH' : { + role_keys.ROLE_SWITCH: { 'unfocused': '[Component(obj,\ asString((labelOrName or description) + roleName),\ indicator=asString(switchState))]' @@ -840,7 +841,7 @@ formatting = { Atspi.Role.TABLE_CELL: { 'unfocused': '((substring and ' + BRAILLE_TEXT + ') or tableCellRow)', }, - 'REAL_ROLE_TABLE_CELL': { + role_keys.REAL_ROLE_TABLE_CELL: { 'unfocused': '((tableCell2ChildToggle + tableCell2ChildLabel)\ or (substring and ' + BRAILLE_TEXT + ') \ or (cellCheckedState\ @@ -992,7 +993,7 @@ formatting = { 'focused': 'percentage', 'unfocused': 'roleName + percentage + availability', }, - 'ROLE_SWITCH': { + role_keys.ROLE_SWITCH: { 'focused': 'switchState', 'unfocused': 'switchState + availability', }, diff --git a/src/cthulhu/generator.py b/src/cthulhu/generator.py index b6b4f13..26756f7 100644 --- a/src/cthulhu/generator.py +++ b/src/cthulhu/generator.py @@ -48,6 +48,7 @@ from . import braille from . import debug from . import messages from . import object_properties +from . import role_keys from . import settings from . import settings_manager from .ax_object import AXObject @@ -954,7 +955,7 @@ class Generator: cells. """ result = [] - oldRole = self._overrideRole('REAL_ROLE_TABLE_CELL', args) + oldRole = self._overrideRole(role_keys.REAL_ROLE_TABLE_CELL, args) result.extend(self.generate(obj, **args)) self._restoreRole(oldRole, args) return result @@ -1299,62 +1300,113 @@ class Generator: self._activeProgressBars[obj] = lastTime, lastValue def _getAlternativeRole(self, obj, **args): - if self._script.utilities.isMath(obj): - if self._script.utilities.isMathSubOrSuperScript(obj): - return 'ROLE_MATH_SCRIPT_SUBSUPER' - if self._script.utilities.isMathUnderOrOverScript(obj): - return 'ROLE_MATH_SCRIPT_UNDEROVER' - if self._script.utilities.isMathMultiScript(obj): - return 'ROLE_MATH_MULTISCRIPT' - if self._script.utilities.isMathEnclose(obj): - return 'ROLE_MATH_ENCLOSED' - if self._script.utilities.isMathFenced(obj): - return 'ROLE_MATH_FENCED' - if self._script.utilities.isMathTable(obj): - return 'ROLE_MATH_TABLE' - if self._script.utilities.isMathTableRow(obj): - return 'ROLE_MATH_TABLE_ROW' - if self._script.utilities.isDPub(obj): - if self._script.utilities.isLandmark(obj): - return 'ROLE_DPUB_LANDMARK' - if AXUtilities.is_section(obj): - return 'ROLE_DPUB_SECTION' + role = self._getMathAlternativeRole(obj) + if role: + return role + + role = self._getDPubAlternativeRole(obj) + if role: + return role + + role = self._getSwitchOrSpecialRole(obj) + if role: + return role + + role = self._getContentAlternativeRole(obj) + if role: + return role + + role = self._getDescriptionListRole(obj) + if role: + return role + + role = self._getFeedAlternativeRole(obj) + if role: + return role + + role = self._getLandmarkAlternativeRole(obj) + if role: + return role + + if self._script.utilities.isDocument(obj) and AXObject.supports_image(obj): + return Atspi.Role.IMAGE + + return args.get('role', AXObject.get_role(obj)) + + def _getMathAlternativeRole(self, obj): + if not self._script.utilities.isMath(obj): + return None + if self._script.utilities.isMathSubOrSuperScript(obj): + return role_keys.ROLE_MATH_SCRIPT_SUBSUPER + if self._script.utilities.isMathUnderOrOverScript(obj): + return role_keys.ROLE_MATH_SCRIPT_UNDEROVER + if self._script.utilities.isMathMultiScript(obj): + return role_keys.ROLE_MATH_MULTISCRIPT + if self._script.utilities.isMathEnclose(obj): + return role_keys.ROLE_MATH_ENCLOSED + if self._script.utilities.isMathFenced(obj): + return role_keys.ROLE_MATH_FENCED + if self._script.utilities.isMathTable(obj): + return role_keys.ROLE_MATH_TABLE + if self._script.utilities.isMathTableRow(obj): + return role_keys.ROLE_MATH_TABLE_ROW + return None + + def _getDPubAlternativeRole(self, obj): + if not self._script.utilities.isDPub(obj): + return None + if self._script.utilities.isLandmark(obj): + return role_keys.ROLE_DPUB_LANDMARK + if AXUtilities.is_section(obj): + return role_keys.ROLE_DPUB_SECTION + return None + + def _getSwitchOrSpecialRole(self, obj): if self._script.utilities.isSwitch(obj): - return 'ROLE_SWITCH' + return role_keys.ROLE_SWITCH if self._script.utilities.isAnchor(obj): return Atspi.Role.STATIC if self._script.utilities.isBlockquote(obj): return Atspi.Role.BLOCK_QUOTE if self._script.utilities.isComment(obj): return Atspi.Role.COMMENT + return None + + def _getContentAlternativeRole(self, obj): if self._script.utilities.isContentDeletion(obj): - return 'ROLE_CONTENT_DELETION' + return role_keys.ROLE_CONTENT_DELETION if self._script.utilities.isContentError(obj): - return 'ROLE_CONTENT_ERROR' + return role_keys.ROLE_CONTENT_ERROR if self._script.utilities.isContentInsertion(obj): - return 'ROLE_CONTENT_INSERTION' + return role_keys.ROLE_CONTENT_INSERTION if self._script.utilities.isContentMarked(obj): - return 'ROLE_CONTENT_MARK' + return role_keys.ROLE_CONTENT_MARK if self._script.utilities.isContentSuggestion(obj): - return 'ROLE_CONTENT_SUGGESTION' + return role_keys.ROLE_CONTENT_SUGGESTION + return None + + def _getDescriptionListRole(self, obj): if self._script.utilities.isDescriptionList(obj): return Atspi.Role.DESCRIPTION_LIST if self._script.utilities.isDescriptionListTerm(obj): return Atspi.Role.DESCRIPTION_TERM if self._script.utilities.isDescriptionListDescription(obj): return Atspi.Role.DESCRIPTION_VALUE - if self._script.utilities.isFeedArticle(obj): - return 'ROLE_ARTICLE_IN_FEED' - if self._script.utilities.isFeed(obj): - return 'ROLE_FEED' - if self._script.utilities.isLandmark(obj): - if self._script.utilities.isLandmarkRegion(obj): - return 'ROLE_REGION' - return Atspi.Role.LANDMARK - if self._script.utilities.isDocument(obj) and AXObject.supports_image(obj): - return Atspi.Role.IMAGE + return None - return args.get('role', AXObject.get_role(obj)) + def _getFeedAlternativeRole(self, obj): + if self._script.utilities.isFeedArticle(obj): + return role_keys.ROLE_ARTICLE_IN_FEED + if self._script.utilities.isFeed(obj): + return role_keys.ROLE_FEED + return None + + def _getLandmarkAlternativeRole(self, obj): + if not self._script.utilities.isLandmark(obj): + return None + if self._script.utilities.isLandmarkRegion(obj): + return role_keys.ROLE_REGION + return Atspi.Role.LANDMARK def getLocalizedRoleName(self, obj, **args): role = args.get('role', AXObject.get_role(obj)) @@ -1435,7 +1487,7 @@ class Generator: return object_properties.ROLE_PROLOGUE if self._script.utilities.isDPubToc(obj): return object_properties.ROLE_TOC - elif role == "ROLE_DPUB_SECTION": + elif role == role_keys.ROLE_DPUB_SECTION: if self._script.utilities.isDPubAbstract(obj): return object_properties.ROLE_ABSTRACT if self._script.utilities.isDPubColophon(obj): diff --git a/src/cthulhu/meson.build b/src/cthulhu/meson.build index c239c0e..e1d13b1 100644 --- a/src/cthulhu/meson.build +++ b/src/cthulhu/meson.build @@ -77,6 +77,7 @@ cthulhu_python_sources = files([ 'pronunciation_dict.py', 'punctuation_settings.py', 'resource_manager.py', + 'role_keys.py', 'script.py', 'script_manager.py', 'script_utilities.py', diff --git a/src/cthulhu/role_keys.py b/src/cthulhu/role_keys.py new file mode 100644 index 0000000..617ccf8 --- /dev/null +++ b/src/cthulhu/role_keys.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2026 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. +# +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu + +"""Shared role key constants for formatting and generators.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." +__license__ = "LGPL" + +# Custom role keys used by formatting and role override logic. +ROLE_ARTICLE_IN_FEED = 'ROLE_ARTICLE_IN_FEED' +ROLE_CONTENT_DELETION = 'ROLE_CONTENT_DELETION' +ROLE_CONTENT_ERROR = 'ROLE_CONTENT_ERROR' +ROLE_CONTENT_INSERTION = 'ROLE_CONTENT_INSERTION' +ROLE_CONTENT_MARK = 'ROLE_CONTENT_MARK' +ROLE_CONTENT_SUGGESTION = 'ROLE_CONTENT_SUGGESTION' +ROLE_DPUB_LANDMARK = 'ROLE_DPUB_LANDMARK' +ROLE_DPUB_SECTION = 'ROLE_DPUB_SECTION' +ROLE_FEED = 'ROLE_FEED' +ROLE_MATH_ENCLOSED = 'ROLE_MATH_ENCLOSED' +ROLE_MATH_FENCED = 'ROLE_MATH_FENCED' +ROLE_MATH_MULTISCRIPT = 'ROLE_MATH_MULTISCRIPT' +ROLE_MATH_SCRIPT_SUBSUPER = 'ROLE_MATH_SCRIPT_SUBSUPER' +ROLE_MATH_SCRIPT_UNDEROVER = 'ROLE_MATH_SCRIPT_UNDEROVER' +ROLE_MATH_TABLE = 'ROLE_MATH_TABLE' +ROLE_MATH_TABLE_ROW = 'ROLE_MATH_TABLE_ROW' +ROLE_REGION = 'ROLE_REGION' +ROLE_SWITCH = 'ROLE_SWITCH' +ROLE_SPREADSHEET_CELL = 'ROLE_SPREADSHEET_CELL' + +# Formatting override keys (not real Atspi roles). +REAL_ROLE_TABLE_CELL = 'REAL_ROLE_TABLE_CELL' +REAL_ROLE_SCROLL_PANE = 'REAL_ROLE_SCROLL_PANE' diff --git a/src/cthulhu/scripts/apps/soffice/formatting.py b/src/cthulhu/scripts/apps/soffice/formatting.py index de453f1..74fbfaf 100644 --- a/src/cthulhu/scripts/apps/soffice/formatting.py +++ b/src/cthulhu/scripts/apps/soffice/formatting.py @@ -43,6 +43,7 @@ gi.require_version("Atspi", "2.0") from gi.repository import Atspi import cthulhu.formatting +import cthulhu.role_keys import cthulhu.settings formatting = { @@ -53,11 +54,11 @@ formatting = { 'basicWhereAmI': 'parentRoleName + pause + columnHeader + pause + rowHeader + pause + roleName + pause + cellCheckedState + pause + (realActiveDescendantDisplayedText or imageDescription + image) + pause + columnAndRow + pause + expandableState + pause + nodeLevel + pause', 'detailedWhereAmI': 'parentRoleName + pause + columnHeader + pause + rowHeader + pause + roleName + pause + cellCheckedState + pause + (realActiveDescendantDisplayedText or imageDescription + image) + pause + columnAndRow + pause + tableCellRow + pause + expandableState + pause + nodeLevel + pause' }, - 'REAL_ROLE_TABLE_CELL': { + cthulhu.role_keys.REAL_ROLE_TABLE_CELL: { 'focused': 'newRowHeader + newColumnHeader + realActiveDescendantDisplayedText', 'unfocused': 'newRowHeader + newColumnHeader + realActiveDescendantDisplayedText', }, - 'ROLE_SPREADSHEET_CELL': { + cthulhu.role_keys.ROLE_SPREADSHEET_CELL: { # We treat spreadsheet cells differently from other table cells in # whereAmI. # diff --git a/src/cthulhu/scripts/apps/soffice/speech_generator.py b/src/cthulhu/scripts/apps/soffice/speech_generator.py index db8645c..e94f2ba 100644 --- a/src/cthulhu/scripts/apps/soffice/speech_generator.py +++ b/src/cthulhu/scripts/apps/soffice/speech_generator.py @@ -36,6 +36,7 @@ gi.require_version("Atspi", "2.0") from gi.repository import Atspi import cthulhu.messages as messages +import cthulhu.role_keys as role_keys import cthulhu.settings_manager as settings_manager import cthulhu.speech_generator as speech_generator from cthulhu.ax_object import AXObject @@ -75,7 +76,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): # Treat a paragraph which is inside of a spreadsheet cell as # a spreadsheet cell. # - elif role == 'ROLE_SPREADSHEET_CELL': + elif role == role_keys.ROLE_SPREADSHEET_CELL: oldRole = self._overrideRole(Atspi.Role.TABLE_CELL, args) override = True result.extend(speech_generator.SpeechGenerator._generateRoleName( @@ -502,7 +503,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): result = [] if args.get('formatType', 'unfocused') == 'basicWhereAmI' \ and self._script.utilities.isSpreadSheetCell(obj): - oldRole = self._overrideRole('ROLE_SPREADSHEET_CELL', args) + oldRole = self._overrideRole(role_keys.ROLE_SPREADSHEET_CELL, args) # In addition, if focus is in a cell being edited, we cannot # query the accessible table interface for coordinates and the # like because we're temporarily in an entirely different object diff --git a/src/cthulhu/scripts/web/speech_generator.py b/src/cthulhu/scripts/web/speech_generator.py index 6147ddf..9ecc402 100644 --- a/src/cthulhu/scripts/web/speech_generator.py +++ b/src/cthulhu/scripts/web/speech_generator.py @@ -41,6 +41,7 @@ from cthulhu import debug from cthulhu import input_event_manager from cthulhu import messages from cthulhu import object_properties +from cthulhu import role_keys from cthulhu import cthulhu_state from cthulhu import settings from cthulhu import settings_manager @@ -491,7 +492,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): roles = [Atspi.Role.DESCRIPTION_LIST, Atspi.Role.LIST, Atspi.Role.LIST_BOX, - 'ROLE_FEED'] + role_keys.ROLE_FEED] role = args.get('role', AXObject.get_role(obj)) if role not in roles: return super()._generateNumberOfChildren(obj, **args) @@ -510,7 +511,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): if self._script.utilities.isDescriptionList(obj): result = [messages.descriptionListTermCount(setsize)] - elif role == 'ROLE_FEED': + elif role == role_keys.ROLE_FEED: result = [messages.feedArticleCount(setsize)] else: result = [messages.listItemCount(setsize)] @@ -597,7 +598,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): doNotSpeak.append(Atspi.Role.TEXT) doNotSpeak.append(Atspi.Role.STATIC) if args.get('string'): - doNotSpeak.append("ROLE_CONTENT_SUGGESTION") + doNotSpeak.append(role_keys.ROLE_CONTENT_SUGGESTION) if args.get('formatType', 'unfocused') != 'basicWhereAmI': doNotSpeak.append(Atspi.Role.LIST_ITEM) doNotSpeak.append(Atspi.Role.LIST) diff --git a/src/cthulhu/sound_generator.py b/src/cthulhu/sound_generator.py index 9a3727f..8bcd115 100644 --- a/src/cthulhu/sound_generator.py +++ b/src/cthulhu/sound_generator.py @@ -39,6 +39,7 @@ import os from . import cthulhu # Need access to cthulhuApp from . import generator +from . import role_keys from . import settings_manager from .ax_object import AXObject from .ax_utilities import AXUtilities @@ -373,7 +374,7 @@ class SoundGenerator(generator.Generator): role = args.get('role', AXObject.get_role(obj)) if isinstance(role, str): - if role == "ROLE_SWITCH": + if role == role_keys.ROLE_SWITCH: role = Atspi.Role.SWITCH else: return [] diff --git a/src/cthulhu/sound_theme_manager.py b/src/cthulhu/sound_theme_manager.py index 9c9680c..f77f194 100644 --- a/src/cthulhu/sound_theme_manager.py +++ b/src/cthulhu/sound_theme_manager.py @@ -41,6 +41,7 @@ from gi.repository import GLib from gi.repository import Atspi from . import debug +from . import role_keys from . import sound from .sound_generator import Icon @@ -244,7 +245,7 @@ class SoundThemeManager: def _normalizeRole(self, role): if isinstance(role, str): - if role == "ROLE_SWITCH": + if role == role_keys.ROLE_SWITCH: return Atspi.Role.SWITCH return None return role @@ -395,4 +396,3 @@ def getManager(): _manager = SoundThemeManager(cthulhu.cthulhuApp) return _manager - diff --git a/src/cthulhu/speech_generator.py b/src/cthulhu/speech_generator.py index 2f39be6..0b533e1 100644 --- a/src/cthulhu/speech_generator.py +++ b/src/cthulhu/speech_generator.py @@ -48,6 +48,7 @@ from . import debug from . import generator from . import messages from . import object_properties +from . import role_keys from . import settings from . import settings_manager from . import sound_theme_manager @@ -275,7 +276,7 @@ class SpeechGenerator(generator.Generator): def _getRoleNameStripCandidates(self, obj, role=None): normalizedRole = role if isinstance(normalizedRole, str): - if normalizedRole == "ROLE_SWITCH": + if normalizedRole == role_keys.ROLE_SWITCH: normalizedRole = Atspi.Role.SWITCH else: normalizedRole = None @@ -1199,7 +1200,7 @@ class SpeechGenerator(generator.Generator): cell itself. The string, 'blank', is added for empty cells. """ result = [] - oldRole = self._overrideRole('REAL_ROLE_TABLE_CELL', args) + oldRole = self._overrideRole(role_keys.REAL_ROLE_TABLE_CELL, args) result.extend(self.generate(obj, **args)) self._restoreRole(oldRole, args) if not (result and result[0]) \ @@ -1970,19 +1971,19 @@ class SpeechGenerator(generator.Generator): def _getEnabledAndDisabledContextRoles(self): allRoles = [Atspi.Role.BLOCK_QUOTE, - 'ROLE_CONTENT_DELETION', - 'ROLE_CONTENT_INSERTION', - 'ROLE_CONTENT_MARK', - 'ROLE_CONTENT_SUGGESTION', - 'ROLE_DPUB_LANDMARK', - 'ROLE_DPUB_SECTION', + role_keys.ROLE_CONTENT_DELETION, + role_keys.ROLE_CONTENT_INSERTION, + role_keys.ROLE_CONTENT_MARK, + role_keys.ROLE_CONTENT_SUGGESTION, + role_keys.ROLE_DPUB_LANDMARK, + role_keys.ROLE_DPUB_SECTION, Atspi.Role.DESCRIPTION_LIST, - 'ROLE_FEED', + role_keys.ROLE_FEED, Atspi.Role.FORM, Atspi.Role.LANDMARK, Atspi.Role.LIST, Atspi.Role.PANEL, - 'ROLE_REGION', + role_keys.ROLE_REGION, Atspi.Role.TABLE, Atspi.Role.TOOL_TIP] @@ -1991,19 +1992,19 @@ class SpeechGenerator(generator.Generator): if cthulhu.cthulhuApp.settingsManager.getSetting('sayAllContextBlockquote'): enabled.append(Atspi.Role.BLOCK_QUOTE) if cthulhu.cthulhuApp.settingsManager.getSetting('sayAllContextLandmark'): - enabled.extend([Atspi.Role.LANDMARK, 'ROLE_DPUB_LANDMARK']) + enabled.extend([Atspi.Role.LANDMARK, role_keys.ROLE_DPUB_LANDMARK]) if cthulhu.cthulhuApp.settingsManager.getSetting('sayAllContextList'): enabled.append(Atspi.Role.LIST) enabled.append(Atspi.Role.DESCRIPTION_LIST) - enabled.append('ROLE_FEED') + enabled.append(role_keys.ROLE_FEED) if cthulhu.cthulhuApp.settingsManager.getSetting('sayAllContextPanel'): enabled.extend([Atspi.Role.PANEL, Atspi.Role.TOOL_TIP, - 'ROLE_CONTENT_DELETION', - 'ROLE_CONTENT_INSERTION', - 'ROLE_CONTENT_MARK', - 'ROLE_CONTENT_SUGGESTION', - 'ROLE_DPUB_SECTION']) + role_keys.ROLE_CONTENT_DELETION, + role_keys.ROLE_CONTENT_INSERTION, + role_keys.ROLE_CONTENT_MARK, + role_keys.ROLE_CONTENT_SUGGESTION, + role_keys.ROLE_DPUB_SECTION]) if cthulhu.cthulhuApp.settingsManager.getSetting('sayAllContextNonLandmarkForm'): enabled.append(Atspi.Role.FORM) if cthulhu.cthulhuApp.settingsManager.getSetting('sayAllContextTable'): @@ -2012,19 +2013,20 @@ class SpeechGenerator(generator.Generator): if cthulhu.cthulhuApp.settingsManager.getSetting('speakContextBlockquote'): enabled.append(Atspi.Role.BLOCK_QUOTE) if cthulhu.cthulhuApp.settingsManager.getSetting('speakContextLandmark'): - enabled.extend([Atspi.Role.LANDMARK, 'ROLE_DPUB_LANDMARK', 'ROLE_REGION']) + enabled.extend([Atspi.Role.LANDMARK, role_keys.ROLE_DPUB_LANDMARK, + role_keys.ROLE_REGION]) if cthulhu.cthulhuApp.settingsManager.getSetting('speakContextList'): enabled.append(Atspi.Role.LIST) enabled.append(Atspi.Role.DESCRIPTION_LIST) - enabled.append('ROLE_FEED') + enabled.append(role_keys.ROLE_FEED) if cthulhu.cthulhuApp.settingsManager.getSetting('speakContextPanel'): enabled.extend([Atspi.Role.PANEL, Atspi.Role.TOOL_TIP, - 'ROLE_CONTENT_DELETION', - 'ROLE_CONTENT_INSERTION', - 'ROLE_CONTENT_MARK', - 'ROLE_CONTENT_SUGGESTION', - 'ROLE_DPUB_SECTION']) + role_keys.ROLE_CONTENT_DELETION, + role_keys.ROLE_CONTENT_INSERTION, + role_keys.ROLE_CONTENT_MARK, + role_keys.ROLE_CONTENT_SUGGESTION, + role_keys.ROLE_DPUB_SECTION]) if cthulhu.cthulhuApp.settingsManager.getSetting('speakContextNonLandmarkForm'): enabled.append(Atspi.Role.FORM) if cthulhu.cthulhuApp.settingsManager.getSetting('speakContextTable'): @@ -2058,7 +2060,7 @@ class SpeechGenerator(generator.Generator): result.append(messages.leavingNLists(count)) else: result.append(messages.LEAVING_LIST) - elif role == 'ROLE_FEED': + elif role == role_keys.ROLE_FEED: result.append(messages.LEAVING_FEED) elif role == Atspi.Role.PANEL: if self._script.utilities.isFigure(obj): @@ -2069,7 +2071,7 @@ class SpeechGenerator(generator.Generator): result = [''] elif role == Atspi.Role.TABLE and self._script.utilities.isTextDocumentTable(obj): result.append(messages.LEAVING_TABLE) - elif role == 'ROLE_DPUB_LANDMARK': + elif role == role_keys.ROLE_DPUB_LANDMARK: if self._script.utilities.isDPubAcknowledgments(obj): result.append(messages.LEAVING_ACKNOWLEDGMENTS) elif self._script.utilities.isDPubAfterword(obj): @@ -2108,7 +2110,7 @@ class SpeechGenerator(generator.Generator): result.append(messages.LEAVING_PROLOGUE) elif self._script.utilities.isDPubToc(obj): result.append(messages.LEAVING_TOC) - elif role == 'ROLE_DPUB_SECTION': + elif role == role_keys.ROLE_DPUB_SECTION: if self._script.utilities.isDPubAbstract(obj): result.append(messages.LEAVING_ABSTRACT) elif self._script.utilities.isDPubColophon(obj): @@ -2148,13 +2150,13 @@ class SpeechGenerator(generator.Generator): result.append(messages.LEAVING_FORM) elif role == Atspi.Role.TOOL_TIP: result.append(messages.LEAVING_TOOL_TIP) - elif role == 'ROLE_CONTENT_DELETION': + elif role == role_keys.ROLE_CONTENT_DELETION: result.append(messages.CONTENT_DELETION_END) - elif role == 'ROLE_CONTENT_INSERTION': + elif role == role_keys.ROLE_CONTENT_INSERTION: result.append(messages.CONTENT_INSERTION_END) - elif role == 'ROLE_CONTENT_MARK': + elif role == role_keys.ROLE_CONTENT_MARK: result.append(messages.CONTENT_MARK_END) - elif role == 'ROLE_CONTENT_SUGGESTION' \ + elif role == role_keys.ROLE_CONTENT_SUGGESTION \ and not self._script.utilities.isInlineSuggestion(obj): result.append(messages.LEAVING_SUGGESTION) else: @@ -2305,16 +2307,16 @@ class SpeechGenerator(generator.Generator): Atspi.Role.DESCRIPTION_LIST, Atspi.Role.FORM, Atspi.Role.LANDMARK, - 'ROLE_CONTENT_DELETION', - 'ROLE_CONTENT_INSERTION', - 'ROLE_CONTENT_MARK', - 'ROLE_CONTENT_SUGGESTION', - 'ROLE_DPUB_LANDMARK', - 'ROLE_DPUB_SECTION', - 'ROLE_FEED', + role_keys.ROLE_CONTENT_DELETION, + role_keys.ROLE_CONTENT_INSERTION, + role_keys.ROLE_CONTENT_MARK, + role_keys.ROLE_CONTENT_SUGGESTION, + role_keys.ROLE_DPUB_LANDMARK, + role_keys.ROLE_DPUB_SECTION, + role_keys.ROLE_FEED, Atspi.Role.LIST, Atspi.Role.PANEL, - 'ROLE_REGION', + role_keys.ROLE_REGION, Atspi.Role.TABLE, Atspi.Role.TOOL_TIP] From 73ddc181143b7d9d0f134330ec3f0cc9f98757bb Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 16 Jan 2026 11:41:49 -0500 Subject: [PATCH 12/13] Refactor complete. We should be in very good shape moving forward. --- AGENTS.md | 1 + src/cthulhu/debug.py | 25 ++++++-- src/cthulhu/event_manager.py | 12 ++-- src/cthulhu/focus_manager.py | 93 ++++++++++----------------- src/cthulhu/input_event.py | 28 ++++++--- src/cthulhu/script_manager.py | 80 ++++++++++-------------- src/cthulhu/scripts/web/script.py | 100 ++++++++++++++---------------- 7 files changed, 164 insertions(+), 175 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 08afdb0..79b236e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,7 @@ This repository is a screen reader. Prioritize accessibility, correctness, and s - variables: `camelCase` - functions/methods: `snake_case` - classes: `PascalCase` +- Keep changes clean and well-structured; avoid layering short-term workarounds when a focused fix is possible. - Add debug logs where helpful for troubleshooting. When adding timestamps to logs, use: `"Message [timestamp]"` (message first). ## Accessibility requirements (high priority) diff --git a/src/cthulhu/debug.py b/src/cthulhu/debug.py index 48f241e..f1195b5 100644 --- a/src/cthulhu/debug.py +++ b/src/cthulhu/debug.py @@ -255,14 +255,31 @@ def _asString(obj): return str(obj) +def _format_tokens(tokens): + text = " ".join(map(_asString, tokens)) + text = re.sub(r"[ \u00A0]+", " ", text) + text = re.sub(r" (?=[,.:)])(?![\n])", "", text) + return text + +def format_log_message(prefix, message, reason=None): + text = f"{prefix}: {message}" + if reason: + text = f"{text} (reason={reason})" + return text + +def print_log(level, prefix, message, reason=None, timestamp=False, stack=False): + text = format_log_message(prefix, message, reason) + printMessage(level, text, timestamp, stack) + +def print_log_tokens(level, prefix, tokens, reason=None, timestamp=False, stack=False): + text = format_log_message(prefix, _format_tokens(tokens), reason) + printMessage(level, text, timestamp, stack) + def printTokens(level, tokens, timestamp=False, stack=False): if level < debugLevel: return - text = " ".join(map(_asString, tokens)) - text = re.sub(r"[ \u00A0]+", " ", text) - text = re.sub(r" (?=[,.:)])(?![\n])", "", text) - println(level, text, timestamp, stack) + println(level, _format_tokens(tokens), timestamp, stack) def print_tokens(level, tokens, timestamp=False, stack=False): return printTokens(level, tokens, timestamp, stack) diff --git a/src/cthulhu/event_manager.py b/src/cthulhu/event_manager.py index 3afb875..416d30e 100644 --- a/src/cthulhu/event_manager.py +++ b/src/cthulhu/event_manager.py @@ -238,12 +238,12 @@ class EventManager: debug.printTokens(debug.LEVEL_INFO, tokens, True) def _log_ignore(reason, message): - msg = f"EVENT MANAGER: Ignoring ({reason}) - {message}" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_log(debug.LEVEL_INFO, "EVENT MANAGER", + f"Ignoring - {message}", reason, True) def _log_allow(reason, message): - msg = f"EVENT MANAGER: Not ignoring ({reason}) - {message}" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_log(debug.LEVEL_INFO, "EVENT MANAGER", + f"Not ignoring - {message}", reason, True) def _ignore_with_reason(reason, message): _log_ignore(reason, message) @@ -1103,8 +1103,8 @@ class EventManager: return setNewActiveScript, reason = self._isActivatableEvent(event, script) - msg = f'EVENT MANAGER: Change active script: {setNewActiveScript} ({reason})' - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_log(debug.LEVEL_INFO, "EVENT MANAGER", + f"Change active script: {setNewActiveScript}", reason, True) if setNewActiveScript: try: diff --git a/src/cthulhu/focus_manager.py b/src/cthulhu/focus_manager.py index ac49038..fbf3df0 100644 --- a/src/cthulhu/focus_manager.py +++ b/src/cthulhu/focus_manager.py @@ -59,6 +59,12 @@ def _get_ax_utilities(): from .ax_utilities import AXUtilities return AXUtilities +def _log(message, reason=None, timestamp=True, stack=False): + debug.print_log(debug.LEVEL_INFO, "FOCUS MANAGER", message, reason, timestamp, stack) + +def _log_tokens(tokens, reason=None, timestamp=True, stack=False): + debug.print_log_tokens(debug.LEVEL_INFO, "FOCUS MANAGER", tokens, reason, timestamp, stack) + if TYPE_CHECKING: from .input_event import InputEvent from .scripts import default @@ -84,18 +90,14 @@ class FocusManager: self._last_cursor_position: tuple[Optional[Atspi.Accessible], int] = (None, -1) self._penultimate_cursor_position: tuple[Optional[Atspi.Accessible], int] = (None, -1) - msg = "FOCUS MANAGER: Registering D-Bus commands." - debug.print_message(debug.LEVEL_INFO, msg, True) + _log("Registering D-Bus commands.") controller = dbus_service.get_remote_controller() controller.register_decorated_module("FocusManager", self) def clear_state(self, reason: str = "") -> None: """Clears everything we're tracking.""" - msg = "FOCUS MANAGER: Clearing all state" - if reason: - msg += f": {reason}" - debug.print_message(debug.LEVEL_INFO, msg, True) + _log("Clearing all state", reason) self._focus = None self._window = None self._object_of_interest = None @@ -109,8 +111,7 @@ class FocusManager: """Returns the focused object in the active window.""" result = _get_ax_utilities().get_focused_object(self._window) - tokens = ["FOCUS MANAGER: Focused object in", self._window, "is", result] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Focused object in", self._window, "is", result]) return result def focus_and_window_are_unknown(self) -> bool: @@ -118,8 +119,7 @@ class FocusManager: result = self._focus is None and self._window is None if result: - msg = "FOCUS MANAGER: Focus and window are unknown" - debug.print_message(debug.LEVEL_INFO, msg, True) + _log("Focus and window are unknown") return result @@ -129,8 +129,7 @@ class FocusManager: if not AXObject.is_dead(self._focus): return False - msg = "FOCUS MANAGER: Focus is dead" - debug.print_message(debug.LEVEL_INFO, msg, True) + _log("Focus is dead", "dead-focus") return True def focus_is_active_window(self) -> bool: @@ -165,8 +164,7 @@ class FocusManager: obj.emit("mode-changed::" + mode, 1, "") if mode != self._active_mode: - tokens = ["FOCUS MANAGER: Switching mode from", self._active_mode, "to", mode] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Switching mode from", self._active_mode, "to", mode]) self._active_mode = mode cthulhu_state.activeMode = mode if mode == FLAT_REVIEW: @@ -174,15 +172,12 @@ class FocusManager: else: braille.setBrlapiPriority() - tokens = ["FOCUS MANAGER: Region of interest:", obj, f"({start_offset}, {end_offset})"] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Region of interest:", obj, f"({start_offset}, {end_offset})"]) if obj is not None: obj.emit("region-changed", start_offset, end_offset) if obj != self._object_of_interest: - tokens = ["FOCUS MANAGER: Switching object of interest from", - self._object_of_interest, "to", obj] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Switching object of interest from", self._object_of_interest, "to", obj]) self._object_of_interest = obj cthulhu_state.objOfInterest = obj @@ -196,32 +191,27 @@ class FocusManager: ) -> tuple[Optional[str], Optional[Atspi.Accessible]]: """Returns the current mode and associated object of interest""" - tokens = ["FOCUS MANAGER: Active mode:", self._active_mode, - "Object of interest:", self._object_of_interest] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Active mode:", self._active_mode, "Object of interest:", self._object_of_interest]) return self._active_mode, self._object_of_interest def get_penultimate_cursor_position(self) -> tuple[Optional[Atspi.Accessible], int]: """Returns the penultimate cursor position as a tuple of (object, offset).""" obj, offset = self._penultimate_cursor_position - tokens = ["FOCUS MANAGER: Penultimate cursor position:", obj, offset] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Penultimate cursor position:", obj, offset]) return obj, offset def get_last_cursor_position(self) -> tuple[Optional[Atspi.Accessible], int]: """Returns the last cursor position as a tuple of (object, offset).""" obj, offset = self._last_cursor_position - tokens = ["FOCUS MANAGER: Last cursor position:", obj, offset] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Last cursor position:", obj, offset]) return obj, offset def set_last_cursor_position(self, obj: Optional[Atspi.Accessible], offset: int) -> None: """Sets the last cursor position as a tuple of (object, offset).""" - tokens = ["FOCUS MANAGER: Setting last cursor position to", obj, offset] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Setting last cursor position to", obj, offset]) self._penultimate_cursor_position = self._last_cursor_position self._last_cursor_position = obj, offset @@ -229,22 +219,19 @@ class FocusManager: """Returns the last known cell coordinates as a tuple of (row, column).""" row, column = self._last_cell_coordinates - msg = f"FOCUS MANAGER: Last known cell coordinates: row={row}, column={column}" - debug.print_message(debug.LEVEL_INFO, msg, True) + _log(f"Last known cell coordinates: row={row}, column={column}") return row, column def set_last_cell_coordinates(self, row: int, column: int) -> None: """Sets the last known cell coordinates as a tuple of (row, column).""" - msg = f"FOCUS MANAGER: Setting last cell coordinates to row={row}, column={column}" - debug.print_message(debug.LEVEL_INFO, msg, True) + _log(f"Setting last cell coordinates to row={row}, column={column}") self._last_cell_coordinates = row, column def get_locus_of_focus(self) -> Optional[Atspi.Accessible]: """Returns the current locus of focus (i.e. the object with visual focus).""" - tokens = ["FOCUS MANAGER: Locus of focus is", self._focus] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Locus of focus is", self._focus]) return self._focus def set_locus_of_focus( @@ -256,8 +243,7 @@ class FocusManager: ) -> None: """Sets the locus of focus (i.e., the object with visual focus).""" - tokens = ["FOCUS MANAGER: Request to set locus of focus to", obj] - debug.print_tokens(debug.LEVEL_INFO, tokens, True, True) + _log_tokens(["Request to set locus of focus to", obj], stack=True) # We clear the cache on the locus of focus because too many apps and toolkits fail # to emit the correct accessibility events. We do so recursively on table cells @@ -265,8 +251,7 @@ class FocusManager: recursive = _get_ax_utilities().is_table_cell(obj) AXObject.clear_cache(obj, recursive, "Setting locus of focus.") if not force and obj == self._focus: - msg = "FOCUS MANAGER: Setting locus of focus to existing locus of focus" - debug.print_message(debug.LEVEL_INFO, msg, True) + _log("Setting locus of focus to existing locus of focus", "no-change") return # We save the current row and column of a newly focused or selected table cell so that on @@ -298,26 +283,21 @@ class FocusManager: old_focus = None if obj is None: - msg = "FOCUS MANAGER: New locus of focus is null (being cleared)" - debug.print_message(debug.LEVEL_INFO, msg, True) + _log("New locus of focus is null (being cleared)", "clearing") self._focus = None cthulhu_state.locusOfFocus = None return if AXObject.is_dead(obj): - tokens = ["FOCUS MANAGER: New locus of focus (", obj, ") is dead. Not updating."] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["New locus of focus", obj, "is dead. Not updating."], "dead") return if script is not None: if not AXObject.is_valid(obj): - tokens = ["FOCUS MANAGER: New locus of focus (", obj, ") is invalid. Not updating."] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["New locus of focus", obj, "is invalid. Not updating."], "invalid") return - tokens = ["FOCUS MANAGER: Changing locus of focus from", old_focus, - "to", obj, ". Notify:", notify_script] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Changing locus of focus from", old_focus, "to", obj, ". Notify:", notify_script]) self._focus = obj cthulhu_state.locusOfFocus = obj @@ -325,8 +305,7 @@ class FocusManager: return if script is None: - msg = "FOCUS MANAGER: Cannot notify active script because there isn't one" - debug.print_message(debug.LEVEL_INFO, msg, True) + _log("Cannot notify active script because there isn't one", "no-active-script") return self.emit_region_changed(obj, mode=FOCUS_TRACKING) @@ -337,15 +316,13 @@ class FocusManager: AXObject.clear_cache(self._window, False, "Ensuring the active window is really active.") is_active = _get_ax_utilities().is_active(self._window) - tokens = ["FOCUS MANAGER:", self._window, "is active:", is_active] - debug.print_tokens(debug.LEVEL_INFO, tokens, True) + _log_tokens([self._window, "is active:", is_active]) return is_active def get_active_window(self) -> Optional[Atspi.Accessible]: """Returns the currently-active window (i.e. without searching or verifying).""" - tokens = ["FOCUS MANAGER: Active window is", self._window] - debug.print_tokens(debug.LEVEL_INFO, tokens, True, True) + _log_tokens(["Active window is", self._window], stack=True) return self._window def set_active_window( @@ -357,14 +334,13 @@ class FocusManager: ) -> None: """Sets the active window.""" - tokens = ["FOCUS MANAGER: Request to set active window to", frame] + tokens = ["Request to set active window to", frame] if app is not None: tokens.extend(["in", app]) - debug.print_tokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(tokens) if frame == self._window: - msg = "FOCUS MANAGER: Setting active window to existing active window" - debug.print_message(debug.LEVEL_INFO, msg, True) + _log("Setting active window to existing active window", "no-change") elif frame is None: self._window = None cthulhu_state.activeWindow = None @@ -375,8 +351,7 @@ class FocusManager: if set_window_as_focus: self.set_locus_of_focus(None, self._window, notify_script) elif not (self.focus_is_active_window() or self.focus_is_in_active_window()): - tokens = ["FOCUS MANAGER: Focus", self._focus, "is not in", self._window] - debug.print_tokens(debug.LEVEL_INFO, tokens, True, True) + _log_tokens(["Focus", self._focus, "is not in", self._window], stack=True) # Don't update the focus to the active window if we can't get to the active window # from the focused object. https://bugreports.qt.io/browse/QTBUG-130116 diff --git a/src/cthulhu/input_event.py b/src/cthulhu/input_event.py index c6d6e8a..b3a753e 100644 --- a/src/cthulhu/input_event.py +++ b/src/cthulhu/input_event.py @@ -796,8 +796,12 @@ class KeyboardEvent(InputEvent): """Returns True if this event should be consumed.""" # Debug logging to understand handler matching - debugMsg = f"shouldConsume: key='{self.event_string}' hw_code={self.hw_code} modifiers={self.modifiers}" - debug.printMessage(debug.LEVEL_INFO, debugMsg, True) + debug.print_log( + debug.LEVEL_INFO, + "INPUT EVENT", + f"shouldConsume: key='{self.event_string}' hw_code={self.hw_code} modifiers={self.modifiers}", + timestamp=True, + ) if not self.timestamp: return False, 'No timestamp' @@ -810,10 +814,14 @@ class KeyboardEvent(InputEvent): self._script = script_manager.get_manager().get_default_script() globalHandlerUsed = True else: - debug.printMessage(debug.LEVEL_INFO, "shouldConsume: No active script", True) + debug.print_log(debug.LEVEL_INFO, "INPUT EVENT", + "shouldConsume: No active script", + reason="no-active-script", timestamp=True) return False, 'No active script when received' - debug.printMessage(debug.LEVEL_INFO, f"shouldConsume: Active script={self._script.__class__.__name__}", True) + debug.print_log(debug.LEVEL_INFO, "INPUT EVENT", + f"shouldConsume: Active script={self._script.__class__.__name__}", + timestamp=True) if self.is_duplicate: return False, 'Is duplicate' @@ -827,15 +835,21 @@ class KeyboardEvent(InputEvent): globalHandlerUsed = globalHandlerUsed or self._resolveHandler() if self._handler: - debug.printMessage(debug.LEVEL_INFO, f"shouldConsume: Handler found: {self._handler.description}", True) + debug.print_log(debug.LEVEL_INFO, "INPUT EVENT", + f"shouldConsume: Handler found: {self._handler.description}", + timestamp=True) else: - debug.printMessage(debug.LEVEL_INFO, "shouldConsume: No handler found", True) + debug.print_log(debug.LEVEL_INFO, "INPUT EVENT", + "shouldConsume: No handler found", + reason="no-handler", timestamp=True) self._script.updateKeyboardEventState(self, self._handler) scriptConsumes = self._script.shouldConsumeKeyboardEvent(self, self._handler) if globalHandlerUsed: scriptConsumes = True - debug.printMessage(debug.LEVEL_INFO, f"shouldConsume: scriptConsumes={scriptConsumes}", True) + debug.print_log(debug.LEVEL_INFO, "INPUT EVENT", + f"shouldConsume: scriptConsumes={scriptConsumes}", + timestamp=True) if self._isReleaseForLastNonModifierKeyEvent(): return scriptConsumes, 'Is release for last non-modifier keyevent' diff --git a/src/cthulhu/script_manager.py b/src/cthulhu/script_manager.py index 7413583..bf36bbf 100644 --- a/src/cthulhu/script_manager.py +++ b/src/cthulhu/script_manager.py @@ -41,10 +41,16 @@ def _get_ax_utilities(): from .ax_utilities import AXUtilities return AXUtilities +def _log(message, reason=None, timestamp=True, stack=False): + debug.print_log(debug.LEVEL_INFO, "SCRIPT MANAGER", message, reason, timestamp, stack) + +def _log_tokens(tokens, reason=None, timestamp=True, stack=False): + debug.print_log_tokens(debug.LEVEL_INFO, "SCRIPT MANAGER", tokens, reason, timestamp, stack) + class ScriptManager: def __init__(self, app): # Added app argument - debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Initializing", True) + _log("Initializing") self.app = app # Store app instance self.appScripts = {} self.toolkitScripts = {} @@ -74,22 +80,22 @@ class ScriptManager: self.set_active_script(None, "lifecycle: init") self._active = False - debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Initialized", True) + _log("Initialized") def activate(self): """Called when this script manager is activated.""" - debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Activating", True) + _log("Activating") self._defaultScript = self.get_script(None) self._defaultScript.registerEventListeners() self.set_active_script(self._defaultScript, "lifecycle: activate") self._active = True - debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Activated", True) + _log("Activated") def deactivate(self): """Called when this script manager is deactivated.""" - debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Deactivating", True) + _log("Deactivating") if self._defaultScript: self._defaultScript.deregisterEventListeners() self._defaultScript = None @@ -98,20 +104,18 @@ class ScriptManager: self.toolkitScripts = {} self.customScripts = {} self._active = False - debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Deactivated", True) + _log("Deactivated") def get_module_name(self, app): """Returns the module name of the script to use for application app.""" if app is None: - msg = "SCRIPT MANAGER: Cannot get module name for null app" - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Cannot get module name for null app", "null-app") return None name = AXObject.get_name(app) if not name: - msg = "SCRIPT MANAGER: Cannot get module name for nameless app" - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Cannot get module name for nameless app", "nameless-app") return None pid = AXObject.get_process_id(app) @@ -136,8 +140,7 @@ class ScriptManager: name = names[0] break - tokens = ["SCRIPT MANAGER: Mapped", app, "to", name] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Mapped", app, "to", name]) return name def _toolkit_for_object(self, obj): @@ -169,8 +172,7 @@ class ScriptManager: except OSError: debug.examineProcesses() - tokens = ["SCRIPT MANAGER: Found", moduleName] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Found", moduleName]) try: if hasattr(module, 'get_script'): script = module.get_script(app) @@ -178,8 +180,7 @@ class ScriptManager: script = module.Script(app) break except Exception as error: - tokens = ["SCRIPT MANAGER: Could not load", moduleName, ":", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Could not load", moduleName, ":", error], "load-failed") return script @@ -202,8 +203,7 @@ class ScriptManager: if not script: script = self.get_default_script(app) - tokens = ["SCRIPT MANAGER: Default script created for", app, "(obj: ", obj, ")"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Default script created for", app, "(obj:", obj, ")"]) return script @@ -230,14 +230,12 @@ class ScriptManager: if newScript: return newScript - tokens = ["WARNING: Failed to get a replacement script for", script.app] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Failed to get a replacement script for", script.app], "replacement-missing") return script def get_script_for_mouse_button_event(self, event): isActive = _get_ax_utilities().is_active(cthulhu_state.activeWindow) - tokens = ["SCRIPT MANAGER:", cthulhu_state.activeWindow, "is active:", isActive] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens([cthulhu_state.activeWindow, "is active:", isActive]) if isActive and cthulhu_state.activeScript: return cthulhu_state.activeScript @@ -297,8 +295,7 @@ class ScriptManager: appScript = self._create_script(app, None) self.appScripts[app] = appScript except Exception as error: - tokens = ["SCRIPT MANAGER: Exception getting app script for", app, ":", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Exception getting app script for", app, ":", error], "app-script-error") appScript = self.get_default_script() if customScript: @@ -351,8 +348,7 @@ class ScriptManager: from . import cthulhu cthulhu.cthulhuApp.getSignalManager().emitSignal('active-script-changed', newScript) - tokens = ["SCRIPT MANAGER: Setting active script to", newScript, "reason:", reason] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Setting active script to", newScript], reason) self._log_active_state(reason) def activate_script_for_context(self, app, obj, reason=None): @@ -361,14 +357,12 @@ class ScriptManager: return script def _log_active_state(self, reason=None): - tokens = [ - "SCRIPT MANAGER: Active state:", - "window", cthulhu_state.activeWindow, - "focus", cthulhu_state.locusOfFocus, - "script", cthulhu_state.activeScript, - "reason", reason - ] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens( + ["Active state:", "window", cthulhu_state.activeWindow, + "focus", cthulhu_state.locusOfFocus, + "script", cthulhu_state.activeScript], + reason + ) def _get_script_for_app_replicant(self, app): if not self._active: @@ -385,8 +379,7 @@ class ScriptManager: if a != app and _get_ax_utilities().is_application_in_desktop(a): if script.app is None: script.app = a - tokens = ["SCRIPT MANAGER: Script for app replicant:", script, script.app] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Script for app replicant:", script, script.app]) return script return None @@ -396,8 +389,7 @@ class ScriptManager: deleting any scripts as necessary. """ - msg = "SCRIPT MANAGER: Checking and cleaning up scripts." - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Checking and cleaning up scripts.") appList = list(self.appScripts.keys()) for app in appList: @@ -407,21 +399,17 @@ class ScriptManager: try: appScript = self.appScripts.pop(app) except KeyError: - tokens = ["SCRIPT MANAGER:", app, "not found in appScripts"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens([app, "not found in appScripts"]) continue - tokens = ["SCRIPT MANAGER: Old script for app found:", appScript, appScript.app] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Old script for app found:", appScript, appScript.app]) newScript = self._get_script_for_app_replicant(app) if newScript: - tokens = ["SCRIPT MANAGER: Transferring attributes:", newScript, newScript.app] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Transferring attributes:", newScript, newScript.app]) attrs = appScript.getTransferableAttributes() for attr, value in attrs.items(): - tokens = ["SCRIPT MANAGER: Setting", attr, "to", value] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Setting", attr, "to", value]) setattr(newScript, attr, value) del appScript diff --git a/src/cthulhu/scripts/web/script.py b/src/cthulhu/scripts/web/script.py index b530021..b944c33 100644 --- a/src/cthulhu/scripts/web/script.py +++ b/src/cthulhu/scripts/web/script.py @@ -70,6 +70,12 @@ from .script_utilities import Utilities _settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager +def _log(message, reason=None, timestamp=True, stack=False): + debug.print_log(debug.LEVEL_INFO, "WEB", message, reason, timestamp, stack) + +def _log_tokens(tokens, reason=None, timestamp=True, stack=False): + debug.print_log_tokens(debug.LEVEL_INFO, "WEB", tokens, reason, timestamp, stack) + class Script(default.Script): @@ -126,14 +132,13 @@ class Script(default.Script): def activate(self): """Called when this script is activated.""" - tokens = ["WEB: Activating script for", self.app] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Activating script for", self.app]) focus = cthulhu_state.locusOfFocus inApp = AXObject.get_application(focus) == self.app if focus else False inDoc = self._focusInDocumentContent() suspend = not (inDoc and inApp) - reason = f"script activation, not in document content in this app: {suspend}" + reason = "activation-outside-document" if suspend else "activation-in-document" self._setNavigationSuspended(suspend, reason) @@ -176,8 +181,7 @@ class Script(default.Script): self.structuralNavigation.enabled = self._structNavWasEnabled self._structNavWasEnabled = None - tokens = ["WEB: Navigation suspended:", suspend, reason] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Navigation suspended:", suspend], reason) def _focusInDocumentContent(self): focus = cthulhu_state.locusOfFocus @@ -902,24 +906,20 @@ class Script(default.Script): """Returns True if we should use focus mode in obj.""" if self._focusModeIsSticky: - msg = "WEB: Using focus mode because focus mode is sticky" - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Using focus mode", "focus-mode-sticky") return True if self._browseModeIsSticky: - msg = "WEB: Not using focus mode because browse mode is sticky" - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Not using focus mode", "browse-mode-sticky") return False if self.inSayAll(): - msg = "WEB: Not using focus mode because we're in SayAll." - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Not using focus mode", "say-all") return False if not cthulhu.cthulhuApp.settingsManager.getSetting('structNavTriggersFocusMode') \ and self._lastCommandWasStructNav: - msg = "WEB: Not using focus mode due to struct nav settings" - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Not using focus mode", "struct-nav-settings") return False if prevObj and AXObject.is_dead(prevObj): @@ -928,39 +928,32 @@ class Script(default.Script): if not cthulhu.cthulhuApp.settingsManager.getSetting('caretNavTriggersFocusMode') \ and self._lastCommandWasCaretNav \ and not self.utilities.isNavigableToolTipDescendant(prevObj): - msg = "WEB: Not using focus mode due to caret nav settings" - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Not using focus mode", "caret-nav-settings") return False if not cthulhu.cthulhuApp.settingsManager.getSetting('nativeNavTriggersFocusMode') \ and not (self._lastCommandWasStructNav or self._lastCommandWasCaretNav): - msg = "WEB: Not changing focus/browse mode due to native nav settings" - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Not changing focus/browse mode", "native-nav-settings") return self._inFocusMode if self.utilities.isFocusModeWidget(obj): - tokens = ["WEB: Using focus mode because", obj, "is a focus mode widget"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Using focus mode for", obj], "focus-mode-widget") return True doNotToggle = AXUtilities.is_link(obj) or AXUtilities.is_radio_button(obj) if self._inFocusMode and doNotToggle and self.utilities.lastInputEventWasUnmodifiedArrow(): - tokens = ["WEB: Staying in focus mode due to arrowing in role of", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Staying in focus mode due to arrowing in role of", obj], "arrowing") return True if self._inFocusMode and self.utilities.isWebAppDescendant(obj): if self.utilities.forceBrowseModeForWebAppDescendant(obj): - tokens = ["WEB: Forcing browse mode for web app descendant", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Forcing browse mode for web app descendant", obj], "web-app-forced-browse") return False - msg = "WEB: Staying in focus mode because we're inside a web application" - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Staying in focus mode because we're inside a web application", "web-app-context") return True - tokens = ["WEB: Not using focus mode for", obj, "due to lack of cause"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens(["Not using focus mode for", obj], "no-cause") return False def speakContents(self, contents, **args): @@ -1209,14 +1202,12 @@ class Script(default.Script): if not cthulhu.cthulhuApp.settingsManager.getSetting('caretNavigationEnabled'): if debugOutput: - msg = "WEB: Not using caret navigation: it's not enabled." - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Not using caret navigation", "disabled") return False if self._inFocusMode: if debugOutput: - msg = "WEB: Not using caret navigation: focus mode is active." - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Not using caret navigation", "focus-mode") return False inDoc = self._focusInDocumentContent() @@ -1226,27 +1217,29 @@ class Script(default.Script): self._setNavigationSuspended(False, "focus confirmed in document content") else: if debugOutput: - msg = "WEB: Not using caret navigation: navigation suspended." - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Not using caret navigation", "suspended") return False if not inDoc: if debugOutput: - tokens = ["WEB: Not using caret navigation: locusOfFocus", - cthulhu_state.locusOfFocus, "is not in document content."] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens( + ["Not using caret navigation because locusOfFocus", cthulhu_state.locusOfFocus, + "is not in document content."], + "focus-not-document" + ) return False if keyboardEvent and keyboardEvent.modifiers & keybindings.SHIFT_MODIFIER_MASK: if debugOutput: - msg = "WEB: Not using caret navigation: shift was used." - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Not using caret navigation", "shift-modifier") return False if debugOutput: - tokens = ["WEB: Using caret navigation: in browse mode and locusOfFocus", - cthulhu_state.locusOfFocus, "is in document content."] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens( + ["Using caret navigation with locusOfFocus", cthulhu_state.locusOfFocus, + "in document content."], + "enabled" + ) return True def useStructuralNavigationModel(self, debugOutput=True): @@ -1254,14 +1247,12 @@ class Script(default.Script): if not self.structuralNavigation.enabled: if debugOutput: - msg = "WEB: Not using structural navigation: it's not enabled." - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Not using structural navigation", "disabled") return False if self._inFocusMode: if debugOutput: - msg = "WEB: Not using structural navigation: focus mode is active." - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Not using structural navigation", "focus-mode") return False inDoc = self._focusInDocumentContent() @@ -1271,21 +1262,24 @@ class Script(default.Script): self._setNavigationSuspended(False, "focus confirmed in document content") else: if debugOutput: - msg = "WEB: Not using structural navigation: navigation suspended." - debug.printMessage(debug.LEVEL_INFO, msg, True) + _log("Not using structural navigation", "suspended") return False if not inDoc: if debugOutput: - tokens = ["WEB: Not using structural navigation: locusOfFocus", - cthulhu_state.locusOfFocus, "is not in document content."] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens( + ["Not using structural navigation because locusOfFocus", + cthulhu_state.locusOfFocus, "is not in document content."], + "focus-not-document" + ) return False if debugOutput: - tokens = ["WEB: Using structural navigation: in browse mode and locusOfFocus", - cthulhu_state.locusOfFocus, "is in document content."] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + _log_tokens( + ["Using structural navigation with locusOfFocus", cthulhu_state.locusOfFocus, + "in document content."], + "enabled" + ) return True def getTextLineAtCaret(self, obj, offset=None, startOffset=None, endOffset=None): From b24744b22beab2c22eda7e64126fea52b98801f6 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 16 Jan 2026 13:01:05 -0500 Subject: [PATCH 13/13] many many type hints added. Lots more to go. --- src/cthulhu/braille_generator.py | 1 + src/cthulhu/cthulhu.py | 220 ++-- src/cthulhu/cthulhu_state.py | 32 +- src/cthulhu/date_and_time_presenter.py | 24 +- src/cthulhu/debug.py | 90 +- src/cthulhu/event_manager.py | 115 +- src/cthulhu/flat_review_presenter.py | 21 +- src/cthulhu/focus_manager.py | 4 +- src/cthulhu/input_event.py | 39 +- src/cthulhu/notification_presenter.py | 25 +- src/cthulhu/plugin_system_manager.py | 124 +- src/cthulhu/script_manager.py | 82 +- src/cthulhu/script_utilities.py | 1033 ++++++++--------- .../scripts/terminal/script_utilities.py | 1 + src/cthulhu/signal_manager.py | 38 +- src/cthulhu/sound.py | 29 +- 16 files changed, 969 insertions(+), 909 deletions(-) diff --git a/src/cthulhu/braille_generator.py b/src/cthulhu/braille_generator.py index 33f3001..75a2aaf 100644 --- a/src/cthulhu/braille_generator.py +++ b/src/cthulhu/braille_generator.py @@ -36,6 +36,7 @@ gi.require_version("Atspi", "2.0") from gi.repository import Atspi from . import braille +from . import cthulhu # Need access to cthulhuApp from . import debug from . import generator from . import messages diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index 0727308..1dd740f 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -25,6 +25,8 @@ """The main module for the Cthulhu screen reader.""" +from __future__ import annotations + __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" @@ -34,22 +36,37 @@ __copyright__ = "Copyright (c) 2004-2009 Sun Microsystems Inc." \ __license__ = "LGPL" import faulthandler +from typing import TYPE_CHECKING, Any, Callable, Optional from . import dbus_service +if TYPE_CHECKING: + from types import FrameType + from gi.repository.Gio import Settings as GSettings + class APIHelper: """Helper class for plugin API interactions, including keybindings.""" - def __init__(self, app): + def __init__(self, app: Cthulhu) -> None: """Initialize the APIHelper. Arguments: - app: the Cthulhu application """ - self.app = app - self._gestureBindings = {} + self.app: Cthulhu = app + self._gestureBindings: dict[Optional[str], list[Any]] = {} - def registerGestureByString(self, function, name, gestureString, inputEventType='default', normalizer='cthulhu', learnModeEnabled=True, contextName=None, globalBinding=False): + def registerGestureByString( + self, + function: Callable[..., Any], + name: str, + gestureString: str, + inputEventType: str = 'default', + normalizer: str = 'cthulhu', + learnModeEnabled: bool = True, + contextName: Optional[str] = None, + globalBinding: bool = False + ) -> Optional[Any]: """Register a gesture by string.""" import logging logger = logging.getLogger(__name__) @@ -100,12 +117,12 @@ class APIHelper: # Create a keybinding handler class GestureHandler: - def __init__(self, function, description, learnModeEnabled=True): - self.function = function - self.description = description - self.learnModeEnabled = learnModeEnabled + def __init__(self, function: Callable[..., Any], description: str, learnModeEnabled: bool = True) -> None: + self.function: Callable[..., Any] = function + self.description: str = description + self.learnModeEnabled: bool = learnModeEnabled - def __call__(self, script, inputEvent): + def __call__(self, script: Any, inputEvent: Any) -> bool: try: logger.info(f"=== Plugin keybinding handler called! ===") return function(script, inputEvent) @@ -173,14 +190,14 @@ class APIHelper: return None - def unregisterShortcut(self, binding, contextName=None): + def unregisterShortcut(self, binding: Any, contextName: Optional[str] = None) -> None: """Unregister a previously registered shortcut. Arguments: - binding: the binding to unregister - contextName: the context for this gesture """ - removed_via_plugin_manager = False + removed_via_plugin_manager: bool = False if contextName and self.app: try: plugin_manager = self.app.getPluginSystemManager() @@ -221,7 +238,7 @@ from gi.repository import GObject try: from gi.repository.Gio import Settings - a11yAppSettings = Settings(schema_id='org.gnome.desktop.a11y.applications') + a11yAppSettings: Optional[GSettings] = Settings(schema_id='org.gnome.desktop.a11y.applications') except Exception: a11yAppSettings = None @@ -266,20 +283,20 @@ from . import resource_manager # Old global variables removed - now using cthulhuApp.* instead -def onEnabledChanged(gsetting, key): +def onEnabledChanged(gsetting: GSettings, key: str) -> None: try: - enabled = gsetting.get_boolean(key) + enabled: bool = gsetting.get_boolean(key) except Exception: return if key == 'screen-reader-enabled' and not enabled: shutdown() -EXIT_CODE_HANG = 50 +EXIT_CODE_HANG: int = 50 # The user-settings module (see loadUserSettings). # -_userSettings = None +_userSettings: Optional[Any] = None ######################################################################## # # @@ -287,29 +304,29 @@ _userSettings = None # # ######################################################################## -CARET_TRACKING = "caret-tracking" -FOCUS_TRACKING = "focus-tracking" -FLAT_REVIEW = "flat-review" -MOUSE_REVIEW = "mouse-review" -OBJECT_NAVIGATOR = "object-navigator" -SAY_ALL = "say-all" +CARET_TRACKING: str = "caret-tracking" +FOCUS_TRACKING: str = "focus-tracking" +FLAT_REVIEW: str = "flat-review" +MOUSE_REVIEW: str = "mouse-review" +OBJECT_NAVIGATOR: str = "object-navigator" +SAY_ALL: str = "say-all" -def getActiveModeAndObjectOfInterest(): +def getActiveModeAndObjectOfInterest() -> tuple[str, Any]: return focus_manager.get_manager().get_active_mode_and_object_of_interest() -def emitRegionChanged(obj, startOffset=None, endOffset=None, mode=None): +def emitRegionChanged(obj: Any, startOffset: Optional[int] = None, endOffset: Optional[int] = None, mode: Optional[str] = None) -> None: """Notifies interested clients that the current region of interest has changed.""" focus_manager.get_manager().emit_region_changed(obj, startOffset, endOffset, mode) -def setActiveWindow(frame, app=None, alsoSetLocusOfFocus=False, notifyScript=False): - real_app = app - real_frame = frame +def setActiveWindow(frame: Any, app: Optional[Any] = None, alsoSetLocusOfFocus: bool = False, notifyScript: bool = False) -> None: + real_app: Optional[Any] = app + real_frame: Any = frame if frame is not None and hasattr(AXObject, "find_real_app_and_window_for"): real_app, real_frame = AXObject.find_real_app_and_window_for(frame, app) focus_manager.get_manager().set_active_window( real_frame, real_app, set_window_as_focus=alsoSetLocusOfFocus, notify_script=notifyScript) -def setLocusOfFocus(event, obj, notifyScript=True, force=False): +def setLocusOfFocus(event: Optional[Any], obj: Any, notifyScript: bool = True, force: bool = False) -> None: """Sets the locus of focus (i.e., the object with visual focus) and notifies the script of the change should the script wish to present the change to the user. @@ -331,7 +348,7 @@ def setLocusOfFocus(event, obj, notifyScript=True, force=False): # # ######################################################################## -def _processBrailleEvent(event): +def _processBrailleEvent(event: Any) -> bool: """Called whenever a key is pressed on the Braille display. Arguments: @@ -340,7 +357,7 @@ def _processBrailleEvent(event): Returns True if the event was consumed; otherwise False """ - consumed = False + consumed: bool = False # Braille key presses always interrupt speech. # @@ -368,16 +385,16 @@ def _processBrailleEvent(event): # # ######################################################################## -def deviceChangeHandler(deviceManager, device): +def deviceChangeHandler(deviceManager: Any, device: Any) -> None: """New keyboards being plugged in stomp on our changes to the keymappings, so we have to re-apply""" source = device.get_source() if source == Gdk.InputSource.KEYBOARD: - msg = "CTHULHU: Keyboard change detected, re-creating the xmodmap" + msg: str = "CTHULHU: Keyboard change detected, re-creating the xmodmap" debug.printMessage(debug.LEVEL_INFO, msg, True) cthulhu_modifier_manager.getManager().refreshCthulhuModifiers("Keyboard change detected.") -def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False): +def loadUserSettings(script: Optional[Any] = None, inputEvent: Optional[Any] = None, skipReloadMessage: bool = False) -> bool: """Loads (and reloads) the user settings module, reinitializing things such as speech if necessary. @@ -398,7 +415,7 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False): cthulhuApp.scriptManager.deactivate() cthulhuApp.getSignalManager().emitSignal('load-setting-begin') - reloaded = False + reloaded: bool = False if _userSettings: _profile = cthulhuApp.settingsManager.getSetting('activeProfile')[1] try: @@ -491,7 +508,7 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False): return True -def _showPreferencesUI(script, prefs): +def _showPreferencesUI(script: Any, prefs: dict[str, Any]) -> None: if cthulhu_state.cthulhuOS: cthulhu_state.cthulhuOS.showGUI() return @@ -502,7 +519,7 @@ def _showPreferencesUI(script, prefs): debug.printException(debug.LEVEL_SEVERE) return - uiFile = os.path.join(cthulhu_platform.datadir, + uiFile: str = os.path.join(cthulhu_platform.datadir, cthulhu_platform.package, "ui", "cthulhu-setup.ui") @@ -511,7 +528,7 @@ def _showPreferencesUI(script, prefs): cthulhu_state.cthulhuOS.init(script) cthulhu_state.cthulhuOS.showGUI() -def showAppPreferencesGUI(script=None, inputEvent=None): +def showAppPreferencesGUI(script: Optional[Any] = None, inputEvent: Optional[Any] = None) -> bool: """Displays the user interface to configure the settings for a specific applications within Cthulhu and set up those app-specific user preferences using a GUI. @@ -519,7 +536,7 @@ def showAppPreferencesGUI(script=None, inputEvent=None): Returns True to indicate the input event has been consumed. """ - prefs = {} + prefs: dict[str, Any] = {} for key in settings.userCustomizableSettings: prefs[key] = cthulhuApp.settingsManager.getSetting(key) @@ -528,36 +545,36 @@ def showAppPreferencesGUI(script=None, inputEvent=None): return True -def showPreferencesGUI(script=None, inputEvent=None): +def showPreferencesGUI(script: Optional[Any] = None, inputEvent: Optional[Any] = None) -> bool: """Displays the user interface to configure Cthulhu and set up user preferences using a GUI. Returns True to indicate the input event has been consumed. """ - prefs = cthulhuApp.settingsManager.getGeneralSettings(cthulhuApp.settingsManager.profile) + prefs: dict[str, Any] = cthulhuApp.settingsManager.getGeneralSettings(cthulhuApp.settingsManager.profile) script = cthulhuApp.scriptManager.get_default_script() _showPreferencesUI(script, prefs) return True -def addKeyGrab(binding): +def addKeyGrab(binding: Any) -> list[int]: """ Add a key grab for the given key binding.""" manager = input_event_manager.get_manager() return manager.add_grabs_for_keybinding(binding) -def removeKeyGrab(id): +def removeKeyGrab(id: int) -> None: """ Remove the key grab for the given key binding.""" manager = input_event_manager.get_manager() manager.remove_grab_by_id(id) -def mapModifier(keycode): +def mapModifier(keycode: int) -> int: manager = input_event_manager.get_manager() return manager.map_keycode_to_modifier(keycode) -def quitCthulhu(script=None, inputEvent=None): +def quitCthulhu(script: Optional[Any] = None, inputEvent: Optional[Any] = None) -> bool: """Quit Cthulhu. Check if the user wants to confirm this action. If so, show the confirmation GUI otherwise just shutdown. @@ -568,7 +585,7 @@ def quitCthulhu(script=None, inputEvent=None): return True -def showFindGUI(script=None, inputEvent=None): +def showFindGUI(script: Optional[Any] = None, inputEvent: Optional[Any] = None) -> None: """Displays the user interface to perform an Cthulhu Find. Returns True to indicate the input event has been consumed. @@ -582,9 +599,9 @@ def showFindGUI(script=None, inputEvent=None): # If True, this module has been initialized. # -_initialized = False +_initialized: bool = False -def init(): +def init() -> bool: """Initialize the cthulhu module, which initializes the speech and braille modules. Also builds up the application list, registers for AT-SPI events, and creates scripts for all known applications. @@ -627,7 +644,7 @@ def init(): return True -def _start_dbus_service(): +def _start_dbus_service() -> bool: """Starts the D-Bus remote controller service in an idle callback.""" debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Starting D-Bus remote controller', True) try: @@ -652,11 +669,11 @@ def _start_dbus_service(): dbus_service.get_remote_controller().register_decorated_module("PluginSystemManager", plugin_manager) except Exception as e: - msg = f"CTHULHU: Failed to start D-Bus service: {e}" + msg: str = f"CTHULHU: Failed to start D-Bus service: {e}" debug.printMessage(debug.LEVEL_SEVERE, msg, True) return False # Remove the idle callback -def start(): +def start() -> None: """Starts Cthulhu.""" debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Starting', True) @@ -693,8 +710,8 @@ def start(): Atspi.event_main() -def die(exitCode=1): - pid = os.getpid() +def die(exitCode: int = 1) -> None: + pid: int = os.getpid() if exitCode == EXIT_CODE_HANG: # Someting is hung and we wish to abort. os.kill(pid, signal.SIGKILL) @@ -705,14 +722,14 @@ def die(exitCode=1): if exitCode > 1: os.kill(pid, signal.SIGTERM) -def timeout(signum=None, frame=None): - msg = 'TIMEOUT: something has hung. Aborting.' +def timeout(signum: Optional[int] = None, frame: Optional[FrameType] = None) -> None: + msg: str = 'TIMEOUT: something has hung. Aborting.' debug.printMessage(debug.LEVEL_SEVERE, msg, True) debug.printStack(debug.LEVEL_SEVERE) debug.examineProcesses(force=True) die(EXIT_CODE_HANG) -def shutdown(script=None, inputEvent=None): +def shutdown(script: Optional[Any] = None, inputEvent: Optional[Any] = None) -> bool: """Exits Cthulhu. Unregisters any event listeners and cleans up. Returns True if the shutdown procedure ran or False if this module @@ -769,12 +786,13 @@ def shutdown(script=None, inputEvent=None): return True -exitCount = 0 -def shutdownOnSignal(signum, frame): +exitCount: int = 0 + +def shutdownOnSignal(signum: int, frame: Optional[FrameType]) -> None: global exitCount - signalString = f'({signal.strsignal(signum)})' - msg = f"CTHULHU: Shutting down and exiting due to signal={signum} {signalString}" + signalString: str = f'({signal.strsignal(signum)})' + msg: str = f"CTHULHU: Shutting down and exiting due to signal={signum} {signalString}" debug.printMessage(debug.LEVEL_INFO, msg, True) # Well...we'll try to exit nicely, but if we keep getting called, @@ -800,7 +818,7 @@ def shutdownOnSignal(signum, frame): # speech.shutdown() shutdown() - cleanExit = True + cleanExit: bool = True except Exception: cleanExit = False @@ -810,28 +828,28 @@ def shutdownOnSignal(signum, frame): if not cleanExit: die(EXIT_CODE_HANG) -def crashOnSignal(signum, frame): - signalString = f'({signal.strsignal(signum)})' - msg = f"CTHULHU: Shutting down and exiting due to signal={signum} {signalString}" +def crashOnSignal(signum: int, frame: Optional[FrameType]) -> None: + signalString: str = f'({signal.strsignal(signum)})' + msg: str = f"CTHULHU: Shutting down and exiting due to signal={signum} {signalString}" debug.printMessage(debug.LEVEL_SEVERE, msg, True) debug.printStack(debug.LEVEL_SEVERE) cthulhu_modifier_manager.getManager().unsetCthulhuModifiers("Crashed") sys.exit(1) -def main(): +def main() -> int: """The main entry point for Cthulhu. The exit codes for Cthulhu will loosely be based on signals, where the exit code will be the signal used to terminate Cthulhu (if a signal was used). Otherwise, an exit code of 0 means normal completion and an exit code of 50 means Cthulhu exited because of a hang.""" - msg = f"CTHULHU: Launching version {cthulhu_platform.version}" + msg: str = f"CTHULHU: Launching version {cthulhu_platform.version}" if cthulhu_platform.revision: msg += f" (rev {cthulhu_platform.revision})" - sessionType = os.environ.get('XDG_SESSION_TYPE') or "" - sessionDesktop = os.environ.get('XDG_SESSION_DESKTOP') or "" - session = "%s %s".strip() % (sessionType, sessionDesktop) + sessionType: str = os.environ.get('XDG_SESSION_TYPE') or "" + sessionDesktop: str = os.environ.get('XDG_SESSION_DESKTOP') or "" + session: str = "%s %s".strip() % (sessionType, sessionDesktop) if session: msg += f" session: {session}" debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -896,7 +914,7 @@ def main(): class Cthulhu(GObject.Object): # basic signals - __gsignals__ = { + __gsignals__: dict[str, tuple[Any, ...]] = { "start-application-completed": (GObject.SignalFlags.RUN_LAST, None, ()), "stop-application-completed": (GObject.SignalFlags.RUN_LAST, None, ()), "load-setting-begin": (GObject.SignalFlags.RUN_LAST, None, ()), @@ -906,56 +924,56 @@ class Cthulhu(GObject.Object): "request-application-preferences": (GObject.SignalFlags.RUN_LAST, None, ()), "active-script-changed": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,)), # New signal to indicate active script change } - def __init__(self): + def __init__(self) -> None: GObject.Object.__init__(self) # add members - self.resourceManager = resource_manager.ResourceManager(self) - self.settingsManager = settings_manager.SettingsManager(self) # Directly instantiate - self.eventManager = event_manager.EventManager(self) # Directly instantiate - self.scriptManager = script_manager.ScriptManager(self) # Directly instantiate - self.logger = logger.Logger() # Directly instantiate - self.signalManager = signal_manager.SignalManager(self) - self.dynamicApiManager = dynamic_api_manager.DynamicApiManager(self) - self.translationManager = translation_manager.TranslationManager(self) - self.debugManager = debug - self.APIHelper = APIHelper(self) + self.resourceManager: Any = resource_manager.ResourceManager(self) + self.settingsManager: Any = settings_manager.SettingsManager(self) # Directly instantiate + self.eventManager: Any = event_manager.EventManager(self) # Directly instantiate + self.scriptManager: Any = script_manager.ScriptManager(self) # Directly instantiate + self.logger: Any = logger.Logger() # Directly instantiate + self.signalManager: Any = signal_manager.SignalManager(self) + self.dynamicApiManager: Any = dynamic_api_manager.DynamicApiManager(self) + self.translationManager: Any = translation_manager.TranslationManager(self) + self.debugManager: Any = debug + self.APIHelper: APIHelper = APIHelper(self) self.createCompatAPI() - self.pluginSystemManager = plugin_system_manager.PluginSystemManager(self) + self.pluginSystemManager: Any = plugin_system_manager.PluginSystemManager(self) # Scan for available plugins at startup self.pluginSystemManager.rescanPlugins() - def getAPIHelper(self): + def getAPIHelper(self) -> APIHelper: return self.APIHelper - def getPluginSystemManager(self): + def getPluginSystemManager(self) -> Any: return self.pluginSystemManager - def getDynamicApiManager(self): + def getDynamicApiManager(self) -> Any: return self.dynamicApiManager - def getSignalManager(self): + def getSignalManager(self) -> Any: return self.signalManager - def getEventManager(self): + def getEventManager(self) -> Any: return self.eventManager - def getSettingsManager(self): + def getSettingsManager(self) -> Any: return self.settingsManager - def getScriptManager(self): + def getScriptManager(self) -> Any: return self.scriptManager - def get_scriptManager(self): + def get_scriptManager(self) -> Any: return self.scriptManager - def getDebugManager(self): + def getDebugManager(self) -> Any: return self.debugManager - def getTranslationManager(self): + def getTranslationManager(self) -> Any: return self.translationManager - def getResourceManager(self): + def getResourceManager(self) -> Any: return self.resourceManager - def getLogger(self): # New getter for the logger + def getLogger(self) -> Any: # New getter for the logger return self.logger - def addKeyGrab(self, binding): + def addKeyGrab(self, binding: Any) -> list[int]: return addKeyGrab(binding) - def removeKeyGrab(self, grab_id): + def removeKeyGrab(self, grab_id: int) -> None: return removeKeyGrab(grab_id) - def run(self, cacheValues=True): - return main(cacheValues) - def stop(self): + def run(self, cacheValues: bool = True) -> int: + return main() + def stop(self) -> None: pass - def createCompatAPI(self): + def createCompatAPI(self) -> None: # for now add compatibility layer using Dynamic API # should be removed step by step # use clean objects, getters and setters instead @@ -990,9 +1008,9 @@ class Cthulhu(GObject.Object): self.getDynamicApiManager().registerAPI('LoadUserSettings', loadUserSettings) self.getDynamicApiManager().registerAPI('APIHelper', self.APIHelper) -cthulhuApp = Cthulhu() +cthulhuApp: Cthulhu = Cthulhu() -def getManager(): +def getManager() -> Cthulhu: return cthulhuApp if __name__ == "__main__": diff --git a/src/cthulhu/cthulhu_state.py b/src/cthulhu/cthulhu_state.py index e2dee10..8cbb238 100644 --- a/src/cthulhu/cthulhu_state.py +++ b/src/cthulhu/cthulhu_state.py @@ -26,6 +26,8 @@ """Holds state that is shared among many modules. """ +from typing import Optional, Any + __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" @@ -36,54 +38,58 @@ __license__ = "LGPL" # NOTE: resist the temptation to do any imports here. They can # easily cause circular imports. # +# We use forward references in type hints to avoid importing: +# - Atspi.Accessible for focus and window +# - Script for activeScript +# - InputEvent for lastInputEvent # The Accessible that has visual focus. # -locusOfFocus = None +locusOfFocus: Optional[Any] = None # Actually: Optional[Atspi.Accessible] # The currently active window. # -activeWindow = None +activeWindow: Optional[Any] = None # Actually: Optional[Atspi.Accessible] # The currently active script. # -activeScript = None +activeScript: Optional[Any] = None # Actually: Optional[Script] # The currently active mode (focus, say all, flat review, etc.) and obj -activeMode = None -objOfInterest = None +activeMode: Optional[str] = None +objOfInterest: Optional[Any] = None # Actually: Optional[Atspi.Accessible] # Used to capture keys to redefine key bindings by the user. # -capturingKeys = False +capturingKeys: bool = False # The last non-modifier key event received. # -lastNonModifierKeyEvent = None +lastNonModifierKeyEvent: Optional[Any] = None # Actually: Optional[KeyboardEvent] # The InputEvent instance representing the last input event. This is # set each time a mouse, keyboard or braille event is received. # -lastInputEvent = None +lastInputEvent: Optional[Any] = None # Actually: Optional[InputEvent] # Used to determine if the user wishes Cthulhu to pass the next command # along to the current application rather than consuming it. # -bypassNextCommand = False +bypassNextCommand: bool = False # The last searchQuery # -searchQuery = None +searchQuery: Optional[str] = None # Handle to the Cthulhu Preferences Glade GUI object. # -cthulhuOS = None +cthulhuOS: Optional[Any] = None # Actually: Optional[CthulhuSetupGUI] # Set to True if the last key opened the preferences dialog # -openingDialog = False +openingDialog: bool = False # The AT-SPI device (needed for key grabs). Will be set to None if AT-SPI # is too old to support the new device API. # -device = None +device: Optional[Any] = None # Actually: Optional[Atspi.Device] diff --git a/src/cthulhu/date_and_time_presenter.py b/src/cthulhu/date_and_time_presenter.py index fd19c91..8371f1d 100644 --- a/src/cthulhu/date_and_time_presenter.py +++ b/src/cthulhu/date_and_time_presenter.py @@ -33,6 +33,7 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \ __license__ = "LGPL" import time +from typing import Optional, Dict, Callable, Any from . import cthulhu # Need access to cthulhuApp from . import cmdnames @@ -45,21 +46,21 @@ _settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class DateAndTimePresenter: """Provides commands to present the date and time.""" - def __init__(self): - self._handlers = self._setup_handlers() - self._bindings = self._setup_bindings() + def __init__(self) -> None: + self._handlers: Dict[str, input_event.InputEventHandler] = self._setup_handlers() + self._bindings: keybindings.KeyBindings = self._setup_bindings() - def get_bindings(self): + def get_bindings(self) -> keybindings.KeyBindings: """Returns the date-and-time-presenter keybindings.""" return self._bindings - def get_handlers(self): + def get_handlers(self) -> Dict[str, input_event.InputEventHandler]: """Returns the date-and-time-presenter handlers.""" return self._handlers - def _setup_handlers(self): + def _setup_handlers(self) -> Dict[str, input_event.InputEventHandler]: """Sets up and returns the date-and-time-presenter input event handlers.""" handlers = {} @@ -76,7 +77,7 @@ class DateAndTimePresenter: return handlers - def _setup_bindings(self): + def _setup_bindings(self) -> keybindings.KeyBindings: """Sets up and returns the date-and-time-presenter key bindings.""" bindings = keybindings.KeyBindings() @@ -99,14 +100,14 @@ class DateAndTimePresenter: return bindings - def present_time(self, script, event=None): + def present_time(self, script: Any, event: Optional[Any] = None) -> bool: """Presents the current time.""" format = cthulhu.cthulhuApp.settingsManager.getSetting('presentTimeFormat') script.presentMessage(time.strftime(format, time.localtime())) return True - def present_date(self, script, event=None): + def present_date(self, script: Any, event: Optional[Any] = None) -> bool: """Presents the current date.""" format = cthulhu.cthulhuApp.settingsManager.getSetting('presentDateFormat') @@ -114,8 +115,9 @@ class DateAndTimePresenter: return True -_presenter = None -def getPresenter(): +_presenter: Optional[DateAndTimePresenter] = None + +def getPresenter() -> DateAndTimePresenter: global _presenter if _presenter is None: _presenter = DateAndTimePresenter() diff --git a/src/cthulhu/debug.py b/src/cthulhu/debug.py index f1195b5..4c06563 100644 --- a/src/cthulhu/debug.py +++ b/src/cthulhu/debug.py @@ -28,6 +28,8 @@ level, which is held in the debugLevel field. All other methods take a debug level, which is compared to the current debug level to determine if the content should be output.""" +from __future__ import annotations + __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" @@ -41,16 +43,19 @@ import re import subprocess import sys import types - +from typing import Any, Optional, TextIO, Pattern, TYPE_CHECKING from datetime import datetime import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi -AXObject = None +if TYPE_CHECKING: + from .ax_object import AXObject as AXObjectType -def _get_ax_object(): +AXObject: Optional[type[AXObjectType]] = None + +def _get_ax_object() -> type[AXObjectType]: global AXObject if AXObject is None: from .ax_object import AXObject as ax_object @@ -117,14 +122,14 @@ LEVEL_FINEST = 400 # LEVEL_ALL = 0 -debugLevel = LEVEL_SEVERE +debugLevel: int = LEVEL_SEVERE # The debug file. If this is not set, then all debug output is done # via stdout. If this is set, then all debug output is sent to the # file. This can be useful for debugging because one can pass in a # non-buffered file to better track down hangs. # -debugFile = None +debugFile: Optional[TextIO] = None # The debug filter should be either None (which means to match all # events) or a compiled regular expression from the 're' module (see @@ -135,14 +140,14 @@ debugFile = None # # debug.eventDebugFilter = rc.compile('focus:|window:activate') # -eventDebugLevel = LEVEL_FINEST -eventDebugFilter = None +eventDebugLevel: int = LEVEL_FINEST +eventDebugFilter: Optional[Pattern[str]] = None # If True, we output debug information for the event queue. We # use this in addition to log level to prevent debug logic from # bogging down event handling. # -debugEventQueue = False +debugEventQueue: bool = False # What module(s) should be traced if traceit is being used. By default # we'll just attend to ourself. (And by default, we will not enable @@ -152,17 +157,17 @@ debugEventQueue = False # specific, immediate issue. Trust me. :-) Disabling braille monitor in # this case is also strongly advised. # -TRACE_MODULES = ['cthulhu'] +TRACE_MODULES: list[str] = ['cthulhu'] # Specific modules to ignore with traceit. # -TRACE_IGNORE_MODULES = ['traceback', 'linecache', 'locale', 'gettext', +TRACE_IGNORE_MODULES: list[str] = ['traceback', 'linecache', 'locale', 'gettext', 'logging', 'UserDict', 'encodings', 'posixpath', 'genericpath', 're'] # Specific apps to trace with traceit. # -TRACE_APPS = [] +TRACE_APPS: list[str] = [] # What AT-SPI event(s) should be traced if traceit is being used. By # default, we'll trace everything. Examples of what you might wish to @@ -173,7 +178,7 @@ TRACE_APPS = [] # TRACE_EVENTS = ['object:state-changed:selected'] # (if you know the exact event type of interest) # -TRACE_EVENTS = [] +TRACE_EVENTS: list[str] = [] # What role(s) should be traced if traceit is being used. By # default, we'll trace everything. An example of what you might wish @@ -181,17 +186,17 @@ TRACE_EVENTS = [] # # TRACE_ROLES = [Atspi.Role.PUSH_BUTTON, Atspi.Role.TOGGLE_BUTTON] # -TRACE_ROLES = [] +TRACE_ROLES: list[Atspi.Role] = [] # Whether or not traceit should only trace the work being done when # processing an actual event. This is when most bad things happen. # So we'll default to True. # -TRACE_ONLY_PROCESSING_EVENTS = True +TRACE_ONLY_PROCESSING_EVENTS: bool = True -objEvent = None +objEvent: Optional[Atspi.Event] = None -def printException(level): +def printException(level: int) -> None: """Prints out information regarding the current exception. Arguments: @@ -203,7 +208,7 @@ def printException(level): traceback.print_exc(100, debugFile) println(level) -def printStack(level): +def printStack(level: int) -> None: """Prints out the current stack. Arguments: @@ -215,7 +220,7 @@ def printStack(level): traceback.print_stack(None, 100, debugFile) println(level) -def _asString(obj): +def _asString(obj: Any) -> str: AXObject = _get_ax_object() if isinstance(obj, Atspi.Accessible): result = AXObject.get_role_name(obj) @@ -255,45 +260,47 @@ def _asString(obj): return str(obj) -def _format_tokens(tokens): +def _format_tokens(tokens: list[Any]) -> str: text = " ".join(map(_asString, tokens)) text = re.sub(r"[ \u00A0]+", " ", text) text = re.sub(r" (?=[,.:)])(?![\n])", "", text) return text -def format_log_message(prefix, message, reason=None): +def format_log_message(prefix: str, message: str, reason: Optional[str] = None) -> str: text = f"{prefix}: {message}" if reason: text = f"{text} (reason={reason})" return text -def print_log(level, prefix, message, reason=None, timestamp=False, stack=False): +def print_log(level: int, prefix: str, message: str, reason: Optional[str] = None, + timestamp: bool = False, stack: bool = False) -> None: text = format_log_message(prefix, message, reason) printMessage(level, text, timestamp, stack) -def print_log_tokens(level, prefix, tokens, reason=None, timestamp=False, stack=False): +def print_log_tokens(level: int, prefix: str, tokens: list[Any], reason: Optional[str] = None, + timestamp: bool = False, stack: bool = False) -> None: text = format_log_message(prefix, _format_tokens(tokens), reason) printMessage(level, text, timestamp, stack) -def printTokens(level, tokens, timestamp=False, stack=False): +def printTokens(level: int, tokens: list[Any], timestamp: bool = False, stack: bool = False) -> None: if level < debugLevel: return println(level, _format_tokens(tokens), timestamp, stack) -def print_tokens(level, tokens, timestamp=False, stack=False): +def print_tokens(level: int, tokens: list[Any], timestamp: bool = False, stack: bool = False) -> None: return printTokens(level, tokens, timestamp, stack) -def printMessage(level, text, timestamp=False, stack=False): +def printMessage(level: int, text: str, timestamp: bool = False, stack: bool = False) -> None: if level < debugLevel: return println(level, text, timestamp, stack) -def print_message(level, text, timestamp=False, stack=False): +def print_message(level: int, text: str, timestamp: bool = False, stack: bool = False) -> None: return printMessage(level, text, timestamp, stack) -def _stackAsString(max_frames=4): +def _stackAsString(max_frames: int = 4) -> str: callers = [] current_module = inspect.getmodule(inspect.currentframe()) stack = inspect.stack() @@ -313,7 +320,7 @@ def _stackAsString(max_frames=4): callers.reverse() return " > ".join(map(_asString, callers)) -def println(level, text="", timestamp=False, stack=False): +def println(level: int, text: str = "", timestamp: bool = False, stack: bool = False) -> None: """Prints the text to stderr unless debug is enabled. If debug is enabled the text will be redirected to the @@ -357,7 +364,7 @@ def println(level, text="", timestamp=False, stack=False): sys.stderr.writelines([text, "\n"]) sys.stderr.flush() -def printResult(level, result=None): +def printResult(level: int, result: Any = None) -> None: """Prints the return result, along with information about the method, arguments, and any errors encountered.""" @@ -380,7 +387,8 @@ def printResult(level, result=None): string = f'{callString}\nRESULT: {result}' println(level, f'{string}') -def printObjectEvent(level, event, sourceInfo=None, timestamp=False): +def printObjectEvent(level: int, event: Atspi.Event, sourceInfo: Optional[str] = None, + timestamp: bool = False) -> None: """Prints out an Python Event object. The given level may be overridden if the eventDebugLevel is greater. Furthermore, only events with event types matching the eventDebugFilter regular @@ -408,7 +416,7 @@ def printObjectEvent(level, event, sourceInfo=None, timestamp=False): if sourceInfo: println(level, f"{' ' * 18}{sourceInfo}", timestamp) -def printInputEvent(level, string, timestamp=False): +def printInputEvent(level: int, string: str, timestamp: bool = False) -> None: """Prints out an input event. The given level may be overridden if the eventDebugLevel (see setEventDebugLevel) is greater. @@ -419,7 +427,8 @@ def printInputEvent(level, string, timestamp=False): println(max(level, eventDebugLevel), string, timestamp) -def printDetails(level, indent, accessible, includeApp=True, timestamp=False): +def printDetails(level: int, indent: str, accessible: Atspi.Accessible, + includeApp: bool = True, timestamp: bool = False) -> None: """Lists the details of the given accessible with the given indentation. @@ -435,7 +444,8 @@ def printDetails(level, indent, accessible, includeApp=True, timestamp=False): getAccessibleDetails(level, accessible, indent, includeApp), timestamp) -def getAccessibleDetails(level, acc, indent="", includeApp=True): +def getAccessibleDetails(level: int, acc: Atspi.Accessible, indent: str = "", + includeApp: bool = True) -> str: """Returns a string, suitable for printing, that describes the given accessible. @@ -458,7 +468,7 @@ def getAccessibleDetails(level, acc, indent="", includeApp=True): # import linecache -def _getFileAndModule(frame): +def _getFileAndModule(frame: types.FrameType) -> tuple[Optional[str], Optional[str]]: filename, module = None, None try: filename = frame.f_globals["__file__"] @@ -471,7 +481,7 @@ def _getFileAndModule(frame): return filename, module -def _shouldTraceIt(): +def _shouldTraceIt() -> bool: AXObject = _get_ax_object() if not objEvent: return not TRACE_ONLY_PROCESSING_EVENTS @@ -491,7 +501,7 @@ def _shouldTraceIt(): return True -def traceit(frame, event, arg): +def traceit(frame: types.FrameType, event: str, arg: Any) -> Optional[object]: """Line tracing utility to output all lines as they are executed by the interpreter. This is to be used by sys.settrace and is for debugging purposes. @@ -543,14 +553,14 @@ def traceit(frame, event, arg): return traceit -def getOpenFDCount(pid): +def getOpenFDCount(pid: int) -> int: procs = subprocess.check_output([ 'lsof', '-w', '-Ff', '-p', str(pid)]) procs = procs.decode('UTF-8').split('\n') files = list(filter(lambda s: s and s[0] == 'f' and s[1:].isdigit(), procs)) return len(files) -def getCmdline(pid): +def getCmdline(pid: int) -> str: try: openFile = os.popen(f'cat /proc/{pid}/cmdline') cmdline = openFile.read() @@ -561,7 +571,7 @@ def getCmdline(pid): return cmdline -def pidOf(procName): +def pidOf(procName: str) -> list[int]: openFile = subprocess.Popen(f'pgrep {procName}', shell=True, stdout=subprocess.PIPE).stdout @@ -569,7 +579,7 @@ def pidOf(procName): openFile.close() return [int(p) for p in pids.split()] -def examineProcesses(force=False): +def examineProcesses(force: bool = False) -> None: AXObject = _get_ax_object() from .ax_utilities import AXUtilities if force: diff --git a/src/cthulhu/event_manager.py b/src/cthulhu/event_manager.py index 416d30e..827acc5 100644 --- a/src/cthulhu/event_manager.py +++ b/src/cthulhu/event_manager.py @@ -36,6 +36,7 @@ from gi.repository import GLib import queue import threading import time +from typing import Optional, Dict, List, Tuple, Any from . import cthulhu from . import debug @@ -49,25 +50,25 @@ from .ax_utilities import AXUtilities class EventManager: - EMBEDDED_OBJECT_CHARACTER = '\ufffc' + EMBEDDED_OBJECT_CHARACTER: str = '\ufffc' - def __init__(self, app, asyncMode=True): + def __init__(self, app: Any, asyncMode: bool = True) -> None: # app is CthulhuApp instance debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Initializing', True) debug.printMessage(debug.LEVEL_INFO, f'EVENT MANAGER: Async Mode is {asyncMode}', True) - self.app = app - self._asyncMode = asyncMode - self._scriptListenerCounts = {} - self._active = False - self._enqueueCount = 0 - self._dequeueCount = 0 - self._cmdlineCache = {} - self._eventQueue = queue.Queue(0) - self._gidleId = 0 - self._gidleLock = threading.Lock() - self._gilSleepTime = 0.00001 - self._synchronousToolkits = ['VCL'] - self._eventsSuspended = False - self._listener = Atspi.EventListener.new(self._enqueue) + self.app: Any = app + self._asyncMode: bool = asyncMode + self._scriptListenerCounts: Dict[str, int] = {} + self._active: bool = False + self._enqueueCount: int = 0 + self._dequeueCount: int = 0 + self._cmdlineCache: Dict[int, str] = {} + self._eventQueue: queue.Queue = queue.Queue(0) + self._gidleId: int = 0 + self._gidleLock: threading.Lock = threading.Lock() + self._gilSleepTime: float = 0.00001 + self._synchronousToolkits: List[str] = ['VCL'] + self._eventsSuspended: bool = False + self._listener: Atspi.EventListener = Atspi.EventListener.new(self._enqueue) # Note: These must match what the scripts registered for, otherwise # Atspi might segfault. @@ -75,24 +76,24 @@ class EventManager: # Events we don't want to suspend include: # object:text-changed:insert - marco # object:property-change:accessible-name - gnome-shell issue #6925 - self._suspendableEvents = ['object:children-changed:add', - 'object:children-changed:remove', - 'object:state-changed:sensitive', - 'object:state-changed:showing', - 'object:text-changed:delete'] - self._eventsTriggeringSuspension = [] - self._ignoredEvents = ['object:bounds-changed', - 'object:state-changed:defunct', - 'object:property-change:accessible-parent'] - self._parentsOfDefunctDescendants = [] + self._suspendableEvents: List[str] = ['object:children-changed:add', + 'object:children-changed:remove', + 'object:state-changed:sensitive', + 'object:state-changed:showing', + 'object:text-changed:delete'] + self._eventsTriggeringSuspension: List[Any] = [] # List of events + self._ignoredEvents: List[str] = ['object:bounds-changed', + 'object:state-changed:defunct', + 'object:property-change:accessible-parent'] + self._parentsOfDefunctDescendants: List[Any] = [] # List[Atspi.Accessible] cthulhu_state.device = None - self._keyHandlingActive = False - self._inputEventManager = None + self._keyHandlingActive: bool = False + self._inputEventManager: Optional[Any] = None # Optional[InputEventManager] debug.printMessage(debug.LEVEL_INFO, 'Event manager initialized', True) - def activate(self): + def activate(self) -> None: """Called when this event manager is activated.""" debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Activating', True) @@ -101,7 +102,7 @@ class EventManager: GLib.idle_add(self._sync_focus_on_startup) debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Activated', True) - def _sync_focus_on_startup(self): + def _sync_focus_on_startup(self) -> bool: """Initialize active window and focus when startup missed focus events.""" focus = cthulhu_state.locusOfFocus @@ -127,7 +128,7 @@ class EventManager: return False - def _activateKeyHandling(self): + def _activateKeyHandling(self) -> None: """Activates keyboard handling using InputEventManager with Atspi.Device.""" if self._keyHandlingActive: @@ -140,7 +141,7 @@ class EventManager: self._keyHandlingActive = True debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Keyboard handling activated', True) - def _deactivateKeyHandling(self): + def _deactivateKeyHandling(self) -> None: """Deactivates keyboard handling.""" if not self._keyHandlingActive: @@ -153,7 +154,7 @@ class EventManager: self._keyHandlingActive = False debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Keyboard handling deactivated', True) - def deactivate(self): + def deactivate(self) -> None: """Called when this event manager is deactivated.""" debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Deactivating', True) @@ -163,23 +164,23 @@ class EventManager: self._deactivateKeyHandling() debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Deactivated', True) - def ignoreEventTypes(self, eventTypeList): + def ignoreEventTypes(self, eventTypeList: List[str]) -> None: for eventType in eventTypeList: if eventType not in self._ignoredEvents: self._ignoredEvents.append(eventType) - def unignoreEventTypes(self, eventTypeList): + def unignoreEventTypes(self, eventTypeList: List[str]) -> None: for eventType in eventTypeList: if eventType in self._ignoredEvents: self._ignoredEvents.remove(eventType) - def _isDuplicateEvent(self, event): + def _isDuplicateEvent(self, event: Any) -> bool: # event: Atspi.Event """Returns True if this event is already in the event queue.""" if self._inFlood() and self._prioritizeDuringFlood(event): return False - def isSame(x): + def isSame(x: Any) -> bool: return x.type == event.type \ and x.source == event.source \ and x.detail1 == event.detail1 \ @@ -192,7 +193,7 @@ class EventManager: return False - def _getAppCmdline(self, app): + def _getAppCmdline(self, app: Any) -> str: # app: Atspi.Accessible pid = AXObject.get_process_id(app) if pid == -1: return "" @@ -203,7 +204,7 @@ class EventManager: self._cmdlineCache[pid] = cmdline return cmdline - def _isSteamApp(self, app): + def _isSteamApp(self, app: Any) -> bool: # app: Atspi.Accessible name = AXObject.get_name(app) if not name: nameLower = "" @@ -216,7 +217,7 @@ class EventManager: cmdline = self._getAppCmdline(app) return "steamwebhelper" in cmdline - def _isSteamNotificationEvent(self, event): + def _isSteamNotificationEvent(self, event: Any) -> bool: # event: Atspi.Event for obj in (event.any_data, event.source): if not isinstance(obj, Atspi.Accessible): continue @@ -229,7 +230,7 @@ class EventManager: return False - def _ignore(self, event): + def _ignore(self, event: Any) -> bool: # event: Atspi.Event """Returns True if this event should be ignored.""" app = AXObject.get_application(event.source) @@ -701,7 +702,7 @@ class EventManager: return rerun - def registerListener(self, eventType): + def registerListener(self, eventType: str) -> None: """Tells this module to listen for the given event type. Arguments: @@ -717,7 +718,7 @@ class EventManager: self._listener.register(eventType) self._scriptListenerCounts[eventType] = 1 - def deregisterListener(self, eventType): + def deregisterListener(self, eventType: str) -> None: """Tells this module to stop listening for the given event type. Arguments: @@ -735,7 +736,7 @@ class EventManager: self._listener.deregister(eventType) del self._scriptListenerCounts[eventType] - def registerScriptListeners(self, script): + def registerScriptListeners(self, script: Any) -> None: # script: Script """Tells the event manager to start listening for all the event types of interest to the script. @@ -749,7 +750,7 @@ class EventManager: for eventType in script.listeners.keys(): self.registerListener(eventType) - def deregisterScriptListeners(self, script): + def deregisterScriptListeners(self, script: Any) -> None: # script: Script """Tells the event manager to stop listening for all the event types of interest to the script. @@ -797,7 +798,7 @@ class EventManager: debug.printMessage(debug.eventDebugLevel, msg, False) @staticmethod - def _get_scriptForEvent(event): + def _get_scriptForEvent(event: Any) -> Optional[Any]: # Returns Optional[Script] """Returns the script associated with event.""" if event.type.startswith("mouse:"): @@ -835,7 +836,7 @@ class EventManager: debug.printTokens(debug.LEVEL_INFO, tokens, True) return script - def _isActivatableEvent(self, event, script=None): + def _isActivatableEvent(self, event: Any, script: Optional[Any] = None) -> Tuple[bool, str]: """Determines if the event is one which should cause us to change which script is currently active. @@ -901,7 +902,7 @@ class EventManager: return False, "No reason found to activate a different script." - def _eventSourceIsDead(self, event): + def _eventSourceIsDead(self, event: Any) -> bool: # event: Atspi.Event if AXObject.is_dead(event.source): tokens = ["EVENT MANAGER: source of", event.type, "is dead"] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -909,7 +910,7 @@ class EventManager: return False - def _ignoreDuringDeluge(self, event): + def _ignoreDuringDeluge(self, event: Any) -> bool: # event: Atspi.Event """Returns true if this event should be ignored during a deluge.""" if self._eventSourceIsDead(event): @@ -936,7 +937,7 @@ class EventManager: return event.source != cthulhu_state.locusOfFocus - def _inDeluge(self): + def _inDeluge(self) -> bool: size = self._eventQueue.qsize() if size > 100: msg = f"EVENT MANAGER: DELUGE! Queue size is {size}" @@ -945,7 +946,7 @@ class EventManager: return False - def _processDuringFlood(self, event): + def _processDuringFlood(self, event: Any) -> bool: # event: Atspi.Event """Returns true if this event should be processed during a flood.""" if self._eventSourceIsDead(event): @@ -972,7 +973,7 @@ class EventManager: return event.source == cthulhu_state.locusOfFocus - def _prioritizeDuringFlood(self, event): + def _prioritizeDuringFlood(self, event: Any) -> bool: # event: Atspi.Event """Returns true if this event should be prioritized during a flood.""" if event.type.startswith("object:state-changed:focused"): @@ -1001,7 +1002,7 @@ class EventManager: return False - def _pruneEventsDuringFlood(self): + def _pruneEventsDuringFlood(self) -> None: """Gets rid of events we don't care about during a flood.""" oldSize = self._eventQueue.qsize() @@ -1024,7 +1025,7 @@ class EventManager: msg = f"EVENT MANAGER: {oldSize - newSize} events pruned. New size: {newSize}" debug.printMessage(debug.LEVEL_INFO, msg, True) - def _inFlood(self): + def _inFlood(self) -> bool: size = self._eventQueue.qsize() if size > 50: msg = f"EVENT MANAGER: FLOOD? Queue size is {size}" @@ -1132,7 +1133,7 @@ class EventManager: msg = f"EVENT MANAGER: {key}: {value}" debug.printMessage(debug.LEVEL_INFO, msg, True) - def processBrailleEvent(self, brailleEvent): + def processBrailleEvent(self, brailleEvent: Any) -> bool: # brailleEvent: BrailleEvent """Called whenever a cursor key is pressed on the Braille display. Arguments: @@ -1148,9 +1149,9 @@ class EventManager: else: return False -_manager = None +_manager: Optional[EventManager] = None -def getManager(): +def getManager() -> EventManager: global _manager if _manager is None: _manager = cthulhu.cthulhuApp.eventManager diff --git a/src/cthulhu/flat_review_presenter.py b/src/cthulhu/flat_review_presenter.py index ebcd238..bb0cc51 100644 --- a/src/cthulhu/flat_review_presenter.py +++ b/src/cthulhu/flat_review_presenter.py @@ -33,6 +33,7 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \ __license__ = "LGPL" import gi +from typing import Optional, Dict, Callable, Any gi.require_version("Gtk", "3.0") from gi.repository import Gtk @@ -55,21 +56,21 @@ _settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class FlatReviewPresenter: """Provides access to on-screen objects via flat-review.""" - def __init__(self): - self._context = None - self._current_contents = "" - self._restrict = cthulhu.cthulhuApp.settingsManager.getSetting("flatReviewIsRestricted") - self._handlers = self._setup_handlers() - self._desktop_bindings = self._setup_desktop_bindings() - self._laptop_bindings = self._setup_laptop_bindings() - self._gui = None + def __init__(self) -> None: + self._context: Optional[flat_review.Context] = None + self._current_contents: str = "" + self._restrict: bool = cthulhu.cthulhuApp.settingsManager.getSetting("flatReviewIsRestricted") + self._handlers: Dict[str, Callable] = self._setup_handlers() + self._desktop_bindings: keybindings.KeyBindings = self._setup_desktop_bindings() + self._laptop_bindings: keybindings.KeyBindings = self._setup_laptop_bindings() + self._gui: Optional[Any] = None # Optional[Gtk.Window] - def is_active(self): + def is_active(self) -> bool: """Returns True if the flat review presenter is active.""" return self._context is not None - def get_or_create_context(self, script=None): + def get_or_create_context(self, script: Optional[Any] = None) -> Optional[flat_review.Context]: """Returns the flat review context, creating one if necessary.""" # TODO - JD: Scripts should not be able to interact with the diff --git a/src/cthulhu/focus_manager.py b/src/cthulhu/focus_manager.py index fbf3df0..2068a2f 100644 --- a/src/cthulhu/focus_manager.py +++ b/src/cthulhu/focus_manager.py @@ -59,10 +59,10 @@ def _get_ax_utilities(): from .ax_utilities import AXUtilities return AXUtilities -def _log(message, reason=None, timestamp=True, stack=False): +def _log(message: str, reason: Optional[str] = None, timestamp: bool = True, stack: bool = False) -> None: debug.print_log(debug.LEVEL_INFO, "FOCUS MANAGER", message, reason, timestamp, stack) -def _log_tokens(tokens, reason=None, timestamp=True, stack=False): +def _log_tokens(tokens: list, reason: Optional[str] = None, timestamp: bool = True, stack: bool = False) -> None: debug.print_log_tokens(debug.LEVEL_INFO, "FOCUS MANAGER", tokens, reason, timestamp, stack) if TYPE_CHECKING: diff --git a/src/cthulhu/input_event.py b/src/cthulhu/input_event.py index b3a753e..476aa52 100644 --- a/src/cthulhu/input_event.py +++ b/src/cthulhu/input_event.py @@ -39,6 +39,7 @@ from gi.repository import Atspi import math import time +from typing import Optional, Any from gi.repository import Gdk from gi.repository import GLib @@ -54,26 +55,26 @@ from . import settings from .ax_object import AXObject from .ax_utilities import AXUtilities -KEYBOARD_EVENT = "keyboard" -BRAILLE_EVENT = "braille" -MOUSE_BUTTON_EVENT = "mouse:button" -REMOTE_CONTROLLER_EVENT = "remote controller" +KEYBOARD_EVENT: str = "keyboard" +BRAILLE_EVENT: str = "braille" +MOUSE_BUTTON_EVENT: str = "mouse:button" +REMOTE_CONTROLLER_EVENT: str = "remote controller" class InputEvent: - def __init__(self, eventType): + def __init__(self, eventType: str) -> None: """Creates a new KEYBOARD_EVENT, BRAILLE_EVENT, or MOUSE_BUTTON_EVENT.""" - self.type = eventType - self.time = time.time() - self._clickCount = 0 + self.type: str = eventType + self.time: float = time.time() + self._clickCount: int = 0 - def get_click_count(self): + def get_click_count(self) -> int: """Return the count of the number of clicks a user has made.""" return self._clickCount - def set_click_count(self, count=None): + def set_click_count(self, count: Optional[int] = None) -> None: """Updates the count of the number of clicks a user has made.""" if count is None: @@ -82,21 +83,21 @@ class InputEvent: self._clickCount = count class KeyboardEvent(InputEvent): - stickyKeys = False + stickyKeys: bool = False - duplicateCount = 0 - cthulhuModifierPressed = False + duplicateCount: int = 0 + cthulhuModifierPressed: bool = False # Whether last press of the Cthulhu modifier was alone - lastCthulhuModifierAlone = False - lastCthulhuModifierAloneTime = None + lastCthulhuModifierAlone: bool = False + lastCthulhuModifierAloneTime: Optional[float] = None # Whether the current press of the Cthulhu modifier is alone - currentCthulhuModifierAlone = False - currentCthulhuModifierAloneTime = None + currentCthulhuModifierAlone: bool = False + currentCthulhuModifierAloneTime: Optional[float] = None # When the second cthulhu press happened - secondCthulhuModifierTime = None + secondCthulhuModifierTime: Optional[float] = None # Sticky modifiers state, to be applied to the next keyboard event - cthulhuStickyModifiers = 0 + cthulhuStickyModifiers: int = 0 TYPE_UNKNOWN = "unknown" TYPE_PRINTABLE = "printable" diff --git a/src/cthulhu/notification_presenter.py b/src/cthulhu/notification_presenter.py index 2e8e22f..0228511 100644 --- a/src/cthulhu/notification_presenter.py +++ b/src/cthulhu/notification_presenter.py @@ -33,6 +33,7 @@ __copyright__ = "Copyright (c) 2023 Igalia, S.L." \ __license__ = "LGPL" import time +from typing import Optional, Dict, List, Any, Callable import gi gi.require_version('Gtk', '3.0') @@ -52,30 +53,30 @@ from . import cthulhu_state class NotificationPresenter: """Provides access to the notification history.""" - def __init__(self): - self._gui = None - self._handlers = self._setup_handlers() - self._bindings = self._setup_bindings() - self._max_size = 55 + def __init__(self) -> None: + self._gui: Optional[Any] = None # Optional[Gtk.Window] + self._handlers: Dict[str, Callable] = self._setup_handlers() + self._bindings: keybindings.KeyBindings = self._setup_bindings() + self._max_size: int = 55 # The list is arranged with the most recent message being at the end of # the list. The current index is relative to, and used directly, with the # python list, i.e. self._notifications[-3] would return the third-to-last # notification message. - self._notifications = [] - self._current_index = -1 + self._notifications: List[List[Any]] = [] # List of [message: str, time: float] + self._current_index: int = -1 - def get_bindings(self): + def get_bindings(self) -> keybindings.KeyBindings: """Returns the notification-presenter keybindings.""" return self._bindings - def get_handlers(self): + def get_handlers(self) -> Dict[str, Callable]: """Returns the notification-presenter handlers.""" return self._handlers - def save_notification(self, message): + def save_notification(self, message: str) -> None: """Adds message to the list of notification messages.""" tokens = ["NOTIFICATION PRESENTER: Adding '", message, "'."] @@ -84,7 +85,7 @@ class NotificationPresenter: self._notifications = self._notifications[to_remove:] self._notifications.append([message, time.time()]) - def clear_list(self): + def clear_list(self) -> None: """Clears the notifications list.""" msg = "NOTIFICATION PRESENTER: Clearing list." @@ -92,7 +93,7 @@ class NotificationPresenter: self._notifications = [] self._current_index = -1 - def _setup_handlers(self): + def _setup_handlers(self) -> Dict[str, Callable]: """Sets up and returns the notification-presenter input event handlers.""" handlers = {} diff --git a/src/cthulhu/plugin_system_manager.py b/src/cthulhu/plugin_system_manager.py index 03f46f0..7c314cd 100644 --- a/src/cthulhu/plugin_system_manager.py +++ b/src/cthulhu/plugin_system_manager.py @@ -18,6 +18,7 @@ import re import shutil import subprocess from enum import IntEnum +from typing import Optional, Dict, List, Any import pluggy from . import dbus_service @@ -25,23 +26,23 @@ from . import input_event_manager from . import keybindings # Added import # Set to True for more detailed plugin loading debug info -PLUGIN_DEBUG = True +PLUGIN_DEBUG: bool = True -logger = logging.getLogger(__name__) +logger: logging.Logger = logging.getLogger(__name__) if PLUGIN_DEBUG: logger.setLevel(logging.DEBUG) -LEGACY_PLUGIN_NAME_ALIASES = { +LEGACY_PLUGIN_NAME_ALIASES: Dict[str, str] = { "ocrdesktop": "OCR", } -LEGACY_PLUGIN_DIR_ALIASES = { +LEGACY_PLUGIN_DIR_ALIASES: Dict[str, str] = { "OCRDesktop": "OCR", } -_manager = None +_manager: Optional['PluginSystemManager'] = None -def getManager(): +def getManager() -> Optional['PluginSystemManager']: """Return the shared PluginSystemManager instance.""" return _manager @@ -50,7 +51,7 @@ class PluginType(IntEnum): SYSTEM = 1 USER = 2 - def get_root_dir(self): + def get_root_dir(self) -> str: """Returns the directory where this type of plugins can be found.""" if self.value == PluginType.SYSTEM: current_file = inspect.getfile(inspect.currentframe()) @@ -63,75 +64,84 @@ class PluginType(IntEnum): class PluginInfo: """Information about a plugin.""" - def __init__(self, name, module_name, module_dir, metadata=None, canonical_name=None, source_id=None, origin=None): - self.name = name - self.module_name = module_name - self.module_dir = module_dir - self.metadata = metadata or {} - self.canonical_name = canonical_name or module_name - self.source_id = source_id or "unknown" - self.origin = origin or "unknown" - self.preferred_alias = False - self.builtin = False - self.hidden = False - self.module = None - self.instance = None - self.loaded = False + def __init__( + self, + name: str, + module_name: str, + module_dir: str, + metadata: Optional[Dict[str, Any]] = None, + canonical_name: Optional[str] = None, + source_id: Optional[str] = None, + origin: Optional[str] = None + ) -> None: + self.name: str = name + self.module_name: str = module_name + self.module_dir: str = module_dir + self.metadata: Dict[str, Any] = metadata or {} + self.canonical_name: str = canonical_name or module_name + self.source_id: str = source_id or "unknown" + self.origin: str = origin or "unknown" + self.preferred_alias: bool = False + self.builtin: bool = False + self.hidden: bool = False + self.module: Optional[Any] = None + self.instance: Optional[Any] = None + self.loaded: bool = False - def get_module_name(self): + def get_module_name(self) -> str: return self.module_name - def get_canonical_name(self): + def get_canonical_name(self) -> str: return self.canonical_name - def get_name(self): + def get_name(self) -> str: return self.metadata.get('name', self.name) - def get_version(self): + def get_version(self) -> str: return self.metadata.get('version', '0.0.0') - def get_description(self): + def get_description(self) -> str: return self.metadata.get('description', '') - def get_source_id(self): + def get_source_id(self) -> str: return self.source_id - def get_origin(self): + def get_origin(self) -> str: return self.origin - def get_source_label(self): + def get_source_label(self) -> str: if self.origin == "sources": return self.source_id return self.origin - def get_module_dir(self): + def get_module_dir(self) -> str: return self.module_dir class PluginSystemManager: """Cthulhu Plugin Manager using pluggy.""" - def __init__(self, app): + def __init__(self, app: Any) -> None: # app is CthulhuApp instance global _manager - self.app = app + self.app: Any = app logger.info("Initializing PluginSystemManager") _manager = self # Initialize plugin manager logger.info("Setting up plugin manager") - self.plugin_manager = pluggy.PluginManager("cthulhu") + self.plugin_manager: pluggy.PluginManager = pluggy.PluginManager("cthulhu") # Define hook specifications hook_spec = pluggy.HookspecMarker("cthulhu") class CthulhuHookSpecs: @hook_spec - def activate(self, plugin=None): + def activate(self, plugin: Optional[Any] = None) -> None: """Called when the plugin is activated.""" pass @hook_spec - def deactivate(self, plugin=None): + def deactivate(self, plugin: Optional[Any] = None) -> None: """Called when the plugin is deactivated.""" pass @@ -139,13 +149,13 @@ class PluginSystemManager: self.plugin_manager.add_hookspecs(CthulhuHookSpecs) # Plugin storage - self._plugins = {} # module_name -> PluginInfo - self._plugin_name_index = {} # canonical_name -> [module_name] - self._active_plugins = [] - self._plugin_keybindings = {} # plugin_name -> [KeyBinding] - self._global_keybindings = keybindings.KeyBindings() - self._global_bindings = [] - self._last_active_script = None + self._plugins: Dict[str, PluginInfo] = {} # module_name -> PluginInfo + self._plugin_name_index: Dict[str, List[str]] = {} # canonical_name -> [module_name] + self._active_plugins: List[str] = [] + self._plugin_keybindings: Dict[str, List[Any]] = {} # plugin_name -> [KeyBinding] + self._global_keybindings: keybindings.KeyBindings = keybindings.KeyBindings() + self._global_bindings: List[Any] = [] # List[KeyBinding] + self._last_active_script: Optional[Any] = None # Optional[Script] # Create plugin directories self._setup_plugin_dirs() @@ -153,12 +163,12 @@ class PluginSystemManager: # 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()}") - + # Connect to active-script-changed signal self.app.getSignalManager().connectSignal( 'active-script-changed', self._on_active_script_changed, 'default') - def add_keybinding(self, plugin_name, binding, global_binding=False): + def add_keybinding(self, plugin_name: str, binding: Any, global_binding: bool = False) -> None: # binding: KeyBinding """Add a keybinding associated with a specific plugin.""" if plugin_name not in self._plugin_keybindings: self._plugin_keybindings[plugin_name] = [] @@ -174,14 +184,14 @@ class PluginSystemManager: else: logger.warning(f"Failed to create global key grab for {binding.keysymstring}") - def activate_keybindings_for_plugin(self, plugin_name): + def activate_keybindings_for_plugin(self, plugin_name: str) -> None: """Activates keybindings for a single plugin with the active script.""" plugin_info = self._plugins.get(plugin_name) if not plugin_info or not plugin_info.loaded: return self._activate_plugin_keybindings(plugin_info) - def remove_keybinding(self, plugin_name, binding): + def remove_keybinding(self, plugin_name: str, binding: Any) -> None: # binding: KeyBinding """Remove a keybinding associated with a specific plugin.""" if plugin_name in self._plugin_keybindings: if binding in self._plugin_keybindings[plugin_name]: @@ -204,7 +214,7 @@ class PluginSystemManager: active_script.getKeyBindings().remove(binding) input_event_manager.get_manager().remove_grabs_for_keybinding(binding) - def _activate_plugin_keybindings(self, plugin_info): + def _activate_plugin_keybindings(self, plugin_info: PluginInfo) -> None: """Activates all keybindings for a given plugin with the active script.""" from . import cthulhu_state # Import here to avoid circular dependency if not cthulhu_state.activeScript: @@ -227,7 +237,7 @@ class PluginSystemManager: logger.warning(f"Failed to create key grab for {binding.keysymstring} for plugin {plugin_name}") logger.debug(f"Activated keybinding '{binding.asString()}' for plugin '{plugin_name}'") - def _deactivate_plugin_keybindings(self, plugin_info): + def _deactivate_plugin_keybindings(self, plugin_info: PluginInfo) -> None: """Deactivates all keybindings for a given plugin from the active script.""" from . import cthulhu_state # Import here to avoid circular dependency if not cthulhu_state.activeScript: @@ -252,16 +262,16 @@ class PluginSystemManager: input_manager.remove_grabs_for_keybinding(binding) logger.debug(f"Deactivated keybinding '{binding.asString()}' for plugin '{plugin_name}'") - def refresh_active_script_keybindings(self): + def refresh_active_script_keybindings(self) -> None: """Public method to refresh keybindings for the currently active script.""" from . import cthulhu_state if cthulhu_state.activeScript: self._on_active_script_changed(self.app, cthulhu_state.activeScript) - def get_global_keybindings(self): + def get_global_keybindings(self) -> keybindings.KeyBindings: return self._global_keybindings - def _on_active_script_changed(self, app, new_script): + def _on_active_script_changed(self, app: Any, new_script: Optional[Any]) -> None: """Called when the active script changes. Re-applies keybindings for all active plugins.""" logger.info(f"Active script changed to {new_script.name if new_script else 'None'}. Re-applying plugin keybindings.") @@ -287,28 +297,28 @@ class PluginSystemManager: if plugin_info and plugin_info.loaded: self._activate_plugin_keybindings(plugin_info) - def _setup_plugin_dirs(self): + def _setup_plugin_dirs(self) -> None: """Ensure plugin directories exist.""" os.makedirs(PluginType.SYSTEM.get_root_dir(), exist_ok=True) os.makedirs(PluginType.USER.get_root_dir(), exist_ok=True) os.makedirs(self._get_plugin_sources_root(), exist_ok=True) - def _get_plugin_sources_root(self): + def _get_plugin_sources_root(self) -> str: return os.path.expanduser('~/.local/share/cthulhu/plugin-sources') - def _get_additional_plugin_dirs(self): + def _get_additional_plugin_dirs(self) -> List[str]: return [os.path.expanduser('~/.local/share/plugins')] - def _path_under_root(self, path, root): + def _path_under_root(self, path: str, root: str) -> bool: try: return os.path.commonpath([os.path.abspath(path), os.path.abspath(root)]) == os.path.abspath(root) except ValueError: return False - def _sanitize_source_id(self, source_id): + def _sanitize_source_id(self, source_id: str) -> str: return re.sub(r'[^a-zA-Z0-9._-]+', '-', source_id).strip('-') or "source" - def _get_origin_info(self, plugin_dir): + def _get_origin_info(self, plugin_dir: str) -> tuple[str, str]: system_root = PluginType.SYSTEM.get_root_dir() user_root = PluginType.USER.get_root_dir() local_root = os.path.expanduser('~/.local/share/plugins') diff --git a/src/cthulhu/script_manager.py b/src/cthulhu/script_manager.py index bf36bbf..20ac2e4 100644 --- a/src/cthulhu/script_manager.py +++ b/src/cthulhu/script_manager.py @@ -30,41 +30,46 @@ __copyright__ = "Copyright (c) 2011. Cthulhu Team." __license__ = "LGPL" import importlib +from typing import Optional, Dict, Any from . import debug from . import cthulhu_state from .ax_object import AXObject from .scripts import apps, toolkits +# Forward references to avoid circular imports +# Script is defined in script.py +# Atspi.Accessible comes from AT-SPI + def _get_ax_utilities(): # Avoid circular import with ax_utilities -> ax_utilities_event -> focus_manager -> braille -> settings_manager -> script_manager. from .ax_utilities import AXUtilities return AXUtilities -def _log(message, reason=None, timestamp=True, stack=False): +def _log(message: str, reason: Optional[str] = None, timestamp: bool = True, stack: bool = False) -> None: debug.print_log(debug.LEVEL_INFO, "SCRIPT MANAGER", message, reason, timestamp, stack) -def _log_tokens(tokens, reason=None, timestamp=True, stack=False): +def _log_tokens(tokens: list, reason: Optional[str] = None, timestamp: bool = True, stack: bool = False) -> None: debug.print_log_tokens(debug.LEVEL_INFO, "SCRIPT MANAGER", tokens, reason, timestamp, stack) class ScriptManager: - def __init__(self, app): # Added app argument + def __init__(self, app: Any) -> None: # app is the CthulhuApp instance _log("Initializing") - self.app = app # Store app instance - self.appScripts = {} - self.toolkitScripts = {} - self.customScripts = {} - self._sleepModeScripts = {} - self._appModules = apps.__all__ - self._toolkitModules = toolkits.__all__ - self._defaultScript = None - self._scriptPackages = \ + self.app: Any = app # Store app instance + self.appScripts: Dict[Any, Any] = {} # Dict[Atspi.Accessible, Script] + self.toolkitScripts: Dict[Any, Dict[str, Any]] = {} # Dict[Atspi.Accessible, Dict[str, Script]] + self.customScripts: Dict[Any, Dict[str, Any]] = {} # Dict[Atspi.Accessible, Dict[str, Script]] + self._sleepModeScripts: Dict[Any, Any] = {} # Dict[Atspi.Accessible, Script] + self._appModules: list = apps.__all__ + self._toolkitModules: list = toolkits.__all__ + self._defaultScript: Optional[Any] = None # Optional[Script] + self._scriptPackages: list[str] = \ ["cthulhu-scripts", "cthulhu.scripts", "cthulhu.scripts.apps", "cthulhu.scripts.toolkits"] - self._appNames = \ + self._appNames: Dict[str, str] = \ {'Icedove': 'Thunderbird', 'Nereid': 'Banshee', 'gnome-calculator': 'gcalctool', @@ -75,14 +80,14 @@ class ScriptManager: 'metacity': 'switcher', 'pluma': 'gedit', } - self._toolkitNames = \ + self._toolkitNames: Dict[str, str] = \ {'WebKitGTK': 'WebKitGtk', 'GTK': 'gtk'} self.set_active_script(None, "lifecycle: init") - self._active = False + self._active: bool = False _log("Initialized") - def activate(self): + def activate(self) -> None: """Called when this script manager is activated.""" _log("Activating") @@ -92,7 +97,7 @@ class ScriptManager: self._active = True _log("Activated") - def deactivate(self): + def deactivate(self) -> None: """Called when this script manager is deactivated.""" _log("Deactivating") @@ -106,7 +111,7 @@ class ScriptManager: self._active = False _log("Deactivated") - def get_module_name(self, app): + def get_module_name(self, app: Optional[Any]) -> Optional[str]: # app: Optional[Atspi.Accessible] """Returns the module name of the script to use for application app.""" if app is None: @@ -143,19 +148,19 @@ class ScriptManager: _log_tokens(["Mapped", app, "to", name]) return name - def _toolkit_for_object(self, obj): + def _toolkit_for_object(self, obj: Optional[Any]) -> str: # obj: Optional[Atspi.Accessible] """Returns the name of the toolkit associated with obj.""" name = AXObject.get_attribute(obj, 'toolkit') return self._toolkitNames.get(name, name) - def _script_for_role(self, obj): + def _script_for_role(self, obj: Optional[Any]) -> str: # obj: Optional[Atspi.Accessible] if _get_ax_utilities().is_terminal(obj): return 'terminal' return '' - def _new_named_script(self, app, name): + def _new_named_script(self, app: Optional[Any], name: Optional[str]) -> Optional[Any]: # Returns Optional[Script] """Attempts to locate and load the named module. If successful, returns a script based on this module.""" @@ -184,7 +189,7 @@ class ScriptManager: return script - def _create_script(self, app, obj=None): + def _create_script(self, app: Optional[Any], obj: Optional[Any] = None) -> Any: # Returns Script """For the given application, create a new script instance.""" moduleName = self.get_module_name(app) @@ -207,7 +212,7 @@ class ScriptManager: return script - def get_default_script(self, app=None): + def get_default_script(self, app: Optional[Any] = None) -> Any: # Returns Script if not app and self._defaultScript: return self._defaultScript @@ -219,7 +224,7 @@ class ScriptManager: return script - def sanity_check_script(self, script): + def sanity_check_script(self, script: Any) -> Any: # Returns Script if not self._active: return script @@ -233,7 +238,7 @@ class ScriptManager: _log_tokens(["Failed to get a replacement script for", script.app], "replacement-missing") return script - def get_script_for_mouse_button_event(self, event): + def get_script_for_mouse_button_event(self, event: Any) -> Any: # Returns Script isActive = _get_ax_utilities().is_active(cthulhu_state.activeWindow) _log_tokens([cthulhu_state.activeWindow, "is active:", isActive]) @@ -251,10 +256,10 @@ class ScriptManager: return self.get_script(AXObject.get_application(activeWindow), activeWindow) - def get_active_script(self): + def get_active_script(self) -> Optional[Any]: # Returns Optional[Script] return cthulhu_state.activeScript - def get_script(self, app, obj=None, sanity_check=False): + def get_script(self, app: Optional[Any], obj: Optional[Any] = None, sanity_check: bool = False) -> Any: # Returns Script """Get a script for an app (and make it if necessary). This is used instead of a simple calls to Script's constructor. @@ -312,19 +317,19 @@ class ScriptManager: return appScript - def get_or_create_sleep_mode_script(self, app): + def get_or_create_sleep_mode_script(self, app: Any) -> Any: # Returns Script """Gets or creates the sleep mode script.""" script = self._sleepModeScripts.get(app) if script is not None: return script - + # Import sleepmode dynamically to avoid circular imports from .scripts import sleepmode script = sleepmode.Script(app) self._sleepModeScripts[app] = script return script - def set_active_script(self, newScript, reason=None): + def set_active_script(self, newScript: Optional[Any], reason: Optional[str] = None) -> None: # newScript: Optional[Script] """Set the new active script. Arguments: @@ -343,20 +348,20 @@ class ScriptManager: return newScript.activate() - + # Emit signal that active script has changed, so PluginSystemManager can update keybindings from . import cthulhu cthulhu.cthulhuApp.getSignalManager().emitSignal('active-script-changed', newScript) - + _log_tokens(["Setting active script to", newScript], reason) self._log_active_state(reason) - def activate_script_for_context(self, app, obj, reason=None): + def activate_script_for_context(self, app: Optional[Any], obj: Optional[Any], reason: Optional[str] = None) -> Any: # Returns Script script = self.get_script(app, obj) self.set_active_script(script, reason) return script - def _log_active_state(self, reason=None): + def _log_active_state(self, reason: Optional[str] = None) -> None: _log_tokens( ["Active state:", "window", cthulhu_state.activeWindow, "focus", cthulhu_state.locusOfFocus, @@ -364,7 +369,7 @@ class ScriptManager: reason ) - def _get_script_for_app_replicant(self, app): + def _get_script_for_app_replicant(self, app: Any) -> Optional[Any]: # Returns Optional[Script] if not self._active: return None @@ -384,7 +389,7 @@ class ScriptManager: return None - def reclaim_scripts(self): + def reclaim_scripts(self) -> None: """Compares the list of known scripts to the list of known apps, deleting any scripts as necessary. """ @@ -432,8 +437,9 @@ class ScriptManager: del app -_manager = None -def get_manager(): +_manager: Optional[ScriptManager] = None + +def get_manager() -> ScriptManager: """Returns the Script Manager""" global _manager diff --git a/src/cthulhu/script_utilities.py b/src/cthulhu/script_utilities.py index 2a02e00..05453d0 100644 --- a/src/cthulhu/script_utilities.py +++ b/src/cthulhu/script_utilities.py @@ -28,6 +28,8 @@ been pulled out from the scripts because certain scripts had gotten way too large as a result of including these methods.""" +from __future__ import annotations + __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" @@ -42,6 +44,7 @@ import re import subprocess import time from difflib import SequenceMatcher +from typing import Any, Callable, Generator, Optional, TYPE_CHECKING gi.require_version("Atspi", "2.0") from gi.repository import Atspi @@ -73,15 +76,18 @@ from .ax_value import AXValue from .ax_utilities import AXUtilities from .ax_utilities_relation import AXUtilitiesRelation -_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager +if TYPE_CHECKING: + from .script import Script + +_settingsManager: Optional[Any] = None # Removed - use cthulhu.cthulhuApp.settingsManager # Try to import sound system for indentation beeps try: from . import sound from .sound_generator import Tone - _SOUND_AVAILABLE = True + _SOUND_AVAILABLE: bool = True except ImportError: - _SOUND_AVAILABLE = False + _SOUND_AVAILABLE: bool = False ############################################################################# # # @@ -91,24 +97,24 @@ except ImportError: class Utilities: - _last_clipboard_update = time.time() + _last_clipboard_update: float = time.time() - EMBEDDED_OBJECT_CHARACTER = '\ufffc' - ZERO_WIDTH_NO_BREAK_SPACE = '\ufeff' - SUPERSCRIPT_DIGITS = \ + EMBEDDED_OBJECT_CHARACTER: str = '\ufffc' + ZERO_WIDTH_NO_BREAK_SPACE: str = '\ufeff' + SUPERSCRIPT_DIGITS: list[str] = \ ['\u2070', '\u00b9', '\u00b2', '\u00b3', '\u2074', '\u2075', '\u2076', '\u2077', '\u2078', '\u2079'] - SUBSCRIPT_DIGITS = \ + SUBSCRIPT_DIGITS: list[str] = \ ['\u2080', '\u2081', '\u2082', '\u2083', '\u2084', '\u2085', '\u2086', '\u2087', '\u2088', '\u2089'] - MENU_ROLES_IN_OPEN_MENU = { + MENU_ROLES_IN_OPEN_MENU: set[Atspi.Role] = { Atspi.Role.MENU, Atspi.Role.MENU_ITEM, Atspi.Role.CHECK_MENU_ITEM, Atspi.Role.RADIO_MENU_ITEM, Atspi.Role.SEPARATOR, } - ZOMBIE_TOP_LEVEL_ROLES = { + ZOMBIE_TOP_LEVEL_ROLES: set[Atspi.Role] = { Atspi.Role.APPLICATION, Atspi.Role.ALERT, Atspi.Role.DIALOG, @@ -117,45 +123,45 @@ class Utilities: Atspi.Role.WINDOW, Atspi.Role.FRAME, } - DISPLAYED_TEXT_DIRECT_NAME_ROLES = { + DISPLAYED_TEXT_DIRECT_NAME_ROLES: set[Atspi.Role] = { Atspi.Role.PUSH_BUTTON, Atspi.Role.LABEL, } - DISPLAYED_TEXT_SKIP_NAME_ROLES = { + DISPLAYED_TEXT_SKIP_NAME_ROLES: set[Atspi.Role] = { Atspi.Role.COMBO_BOX, Atspi.Role.SPIN_BUTTON, } - DISPLAYED_TEXT_LABEL_FALLBACK_ROLES = { + DISPLAYED_TEXT_LABEL_FALLBACK_ROLES: set[Atspi.Role] = { Atspi.Role.PUSH_BUTTON, Atspi.Role.LIST_ITEM, } - flags = re.UNICODE - WORDS_RE = re.compile(r"(\W+)", flags) - SUPERSCRIPTS_RE = re.compile(f"[{''.join(SUPERSCRIPT_DIGITS)}]+", flags) - SUBSCRIPTS_RE = re.compile(f"[{''.join(SUBSCRIPT_DIGITS)}]+", flags) - PUNCTUATION = re.compile(r"[^\w\s]", flags) + flags: re.RegexFlag = re.UNICODE + WORDS_RE: re.Pattern[str] = re.compile(r"(\W+)", flags) + SUPERSCRIPTS_RE: re.Pattern[str] = re.compile(f"[{''.join(SUPERSCRIPT_DIGITS)}]+", flags) + SUBSCRIPTS_RE: re.Pattern[str] = re.compile(f"[{''.join(SUBSCRIPT_DIGITS)}]+", flags) + PUNCTUATION: re.Pattern[str] = re.compile(r"[^\w\s]", flags) # generatorCache # - DISPLAYED_DESCRIPTION = 'displayedDescription' - DISPLAYED_LABEL = 'displayedLabel' - DISPLAYED_TEXT = 'displayedText' - KEY_BINDING = 'keyBinding' - NESTING_LEVEL = 'nestingLevel' - NODE_LEVEL = 'nodeLevel' + DISPLAYED_DESCRIPTION: str = 'displayedDescription' + DISPLAYED_LABEL: str = 'displayedLabel' + DISPLAYED_TEXT: str = 'displayedText' + KEY_BINDING: str = 'keyBinding' + NESTING_LEVEL: str = 'nestingLevel' + NODE_LEVEL: str = 'nodeLevel' - def __init__(self, script): + def __init__(self, script: Script) -> None: """Creates an instance of the Utilities class. Arguments: - script: the script with which this instance is associated. """ - self._script = script - self._clipboardHandlerId = None - self._lastIndentationData = {} - self._selectedMenuBarMenu = {} + self._script: Script = script + self._clipboardHandlerId: Optional[int] = None + self._lastIndentationData: dict[Any, Any] = {} + self._selectedMenuBarMenu: dict[Any, Any] = {} ######################################################################### # # @@ -163,7 +169,7 @@ class Utilities: # # ######################################################################### - def _isActiveAndShowingAndNotIconified(self, obj): + def _isActiveAndShowingAndNotIconified(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_active(obj): tokens = ["SCRIPT UTILITIES:", obj, "lacks state active"] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -182,7 +188,7 @@ class Utilities: return True @staticmethod - def _getAppCommandLine(app): + def _getAppCommandLine(app: Optional[Atspi.Accessible]) -> str: if not app: return "" @@ -200,7 +206,7 @@ class Utilities: return cmdline.replace("\x00", " ") - def canBeActiveWindow(self, window, clearCache=False): + def canBeActiveWindow(self, window: Optional[Atspi.Accessible], clearCache: bool = False) -> bool: if not window: return False @@ -220,7 +226,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return True - def activeWindow(self, *apps): + def activeWindow(self, *apps: Atspi.Accessible) -> Optional[Atspi.Accessible]: """Tries to locate the active window; may or may not succeed.""" candidates = [] @@ -286,10 +292,10 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return guess - def objectAttributes(self, obj, useCache=True): + def objectAttributes(self, obj: Atspi.Accessible, useCache: bool = True) -> dict[str, str]: return AXObject.get_attributes_dict(obj) - def cellIndex(self, obj): + def cellIndex(self, obj: Atspi.Accessible) -> int: """Returns the index of the cell which should be used with the table interface. This is necessary because in some apps we cannot count on the index in parent being the index we need. @@ -306,7 +312,7 @@ class Utilities: obj = AXObject.find_ancestor(obj, AXUtilities.is_table_cell_or_header) or obj return AXObject.get_index_in_parent(obj) - def childNodes(self, obj): + def childNodes(self, obj: Atspi.Accessible) -> list[Atspi.Accessible]: """Gets all of the children that have RELATION_NODE_CHILD_OF pointing to this expanded table cell. @@ -327,7 +333,7 @@ class Utilities: # First see if this accessible implements RELATION_NODE_PARENT_OF. # If it does, the full target list are the nodes. If it doesn't # we'll do an old-school, row-by-row search for child nodes. - def pred(x): + def pred(x: Atspi.Accessible) -> bool: return AXObject.get_index_in_parent(x) >= 0 nodes = [x for x in AXUtilitiesRelation.get_is_node_parent_of(obj) if pred(x)] @@ -360,7 +366,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return nodes - def commonAncestor(self, a, b): + def commonAncestor(self, a: Optional[Atspi.Accessible], b: Optional[Atspi.Accessible]) -> Optional[Atspi.Accessible]: """Finds the common ancestor between Accessible a and Accessible b. Arguments: @@ -407,7 +413,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return commonAncestor - def displayedLabel(self, obj): + def displayedLabel(self, obj: Atspi.Accessible) -> Optional[str]: """If there is an object labelling the given object, return the text being displayed for the object labelling this object. Otherwise, return None. @@ -434,10 +440,10 @@ class Utilities: self._script.generatorCache[self.DISPLAYED_LABEL][obj] = labelString return self._script.generatorCache[self.DISPLAYED_LABEL][obj] - def preferDescriptionOverName(self, obj): + def preferDescriptionOverName(self, obj: Atspi.Accessible) -> bool: return False - def descriptionsForObject(self, obj): + def descriptionsForObject(self, obj: Atspi.Accessible) -> list[Atspi.Accessible]: """Return a list of objects describing obj.""" descriptions = AXUtilitiesRelation.get_is_described_by(obj) @@ -453,11 +459,11 @@ class Utilities: return descriptions - def detailsContentForObject(self, obj): + def detailsContentForObject(self, obj: Atspi.Accessible) -> list[str]: details = self.detailsForObject(obj) return list(map(self.displayedText, details)) - def detailsForObject(self, obj, textOnly=True): + def detailsForObject(self, obj: Atspi.Accessible, textOnly: bool = True) -> list[Atspi.Accessible]: """Return a list of objects containing details for obj.""" details = AXUtilitiesRelation.get_details(obj) @@ -474,7 +480,7 @@ class Utilities: return textObjects - def displayedDescription(self, obj): + def displayedDescription(self, obj: Atspi.Accessible) -> str: """Returns the text being displayed for the object describing obj.""" try: @@ -487,7 +493,7 @@ class Utilities: self._script.generatorCache[self.DISPLAYED_DESCRIPTION][obj] = string return self._script.generatorCache[self.DISPLAYED_DESCRIPTION][obj] - def displayedText(self, obj): + def displayedText(self, obj: Atspi.Accessible) -> Optional[str]: """Returns the text being displayed for an object. Arguments: @@ -523,12 +529,12 @@ class Utilities: self._script.generatorCache[self.DISPLAYED_TEXT][obj] = displayedText return self._script.generatorCache[self.DISPLAYED_TEXT][obj] - def _getDisplayedTextDirectName(self, role, name): + def _getDisplayedTextDirectName(self, role: Atspi.Role, name: str) -> Optional[str]: if role in self.DISPLAYED_TEXT_DIRECT_NAME_ROLES and name: return name return None - def _getDisplayedTextFromText(self, obj): + def _getDisplayedTextFromText(self, obj: Atspi.Accessible) -> Optional[str]: if not AXObject.supports_text(obj): return None @@ -538,14 +544,14 @@ class Utilities: return displayedText - def _getDisplayedTextFallbackName(self, role, name): + def _getDisplayedTextFallbackName(self, role: Atspi.Role, name: str) -> Optional[str]: # NOTE: Legacy fallback; removal requires thorough testing. if name and role not in self.DISPLAYED_TEXT_SKIP_NAME_ROLES: return name return None - def _getDisplayedTextFromLabels(self, obj, role): + def _getDisplayedTextFromLabels(self, obj: Atspi.Accessible, role: Atspi.Role) -> Optional[str]: if role not in self.DISPLAYED_TEXT_LABEL_FALLBACK_ROLES: return None @@ -554,7 +560,7 @@ class Utilities: labels = self.unrelatedLabels(obj, onlyShowing=False, minimumWords=1) return " ".join(map(self.displayedText, labels)) - def documentFrame(self, obj=None): + def documentFrame(self, obj: Optional[Atspi.Accessible] = None) -> Optional[Atspi.Accessible]: """Returns the document frame which is displaying the content. Note that this is intended primarily for web content.""" @@ -567,12 +573,12 @@ class Utilities: return document - def documentFrameURI(self, documentFrame=None): + def documentFrameURI(self, documentFrame: Optional[Atspi.Accessible] = None) -> Optional[str]: """Returns the URI of the document frame that is active.""" return None - def frameAndDialog(self, obj): + def frameAndDialog(self, obj: Optional[Atspi.Accessible]) -> list[Optional[Atspi.Accessible]]: """Returns the frame and (possibly) the dialog containing obj.""" results = [None, None] @@ -600,7 +606,7 @@ class Utilities: if role in [Atspi.Role.FRAME, Atspi.Role.WINDOW]: results[0] = topLevel - def isDialog(x): + def isDialog(x: Atspi.Accessible) -> bool: return AXObject.get_role(x) in dialog_roles if isDialog(obj): @@ -612,13 +618,13 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return results - def presentEventFromNonShowingObject(self, event): + def presentEventFromNonShowingObject(self, event: Any) -> bool: if event.source == cthulhu_state.locusOfFocus: return True return False - def grabFocusBeforeRouting(self, obj): + def grabFocusBeforeRouting(self, obj: Atspi.Accessible) -> bool: """Whether or not we should perform a grabFocus before routing the cursor via the braille cursor routing keys. @@ -632,7 +638,7 @@ class Utilities: return AXUtilities.is_combo_box(obj) \ and not self.isSameObject(obj, cthulhu_state.locusOfFocus) - def hasMatchingHierarchy(self, obj, rolesList): + def hasMatchingHierarchy(self, obj: Atspi.Accessible, rolesList: list[Atspi.Role]) -> bool: """Called to determine if the given object and it's hierarchy of parent objects, each have the desired roles. Please note: You should strongly consider an alternative means for determining @@ -670,7 +676,7 @@ class Utilities: return True - def inFindContainer(self, obj=None): + def inFindContainer(self, obj: Optional[Atspi.Accessible] = None) -> bool: if obj is None: obj = cthulhu_state.locusOfFocus @@ -679,48 +685,48 @@ class Utilities: return AXObject.find_ancestor(obj, AXUtilities.is_tool_bar) is not None - def getFindResultsCount(self, root=None): + def getFindResultsCount(self, root: Optional[Atspi.Accessible] = None) -> str: return "" - def isAnchor(self, obj): + def isAnchor(self, obj: Atspi.Accessible) -> bool: return False - def isCode(self, obj): + def isCode(self, obj: Atspi.Accessible) -> bool: return False - def isCodeDescendant(self, obj): + def isCodeDescendant(self, obj: Atspi.Accessible) -> bool: return False - def isDockedFrame(self, obj): + def isDockedFrame(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_frame(obj): return False attrs = self.objectAttributes(obj) return attrs.get('window-type') == 'dock' - def isDesktop(self, obj): + def isDesktop(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_frame(obj): return False attrs = self.objectAttributes(obj) return attrs.get('is-desktop') == 'true' - def isComboBoxWithToggleDescendant(self, obj): + def isComboBoxWithToggleDescendant(self, obj: Atspi.Accessible) -> bool: return False - def isToggleDescendantOfComboBox(self, obj): + def isToggleDescendantOfComboBox(self, obj: Atspi.Accessible) -> bool: return False - def isTypeahead(self, obj): + def isTypeahead(self, obj: Atspi.Accessible) -> bool: return False - def isOrDescendsFrom(self, obj, ancestor): + def isOrDescendsFrom(self, obj: Atspi.Accessible, ancestor: Atspi.Accessible) -> bool: if obj == ancestor: return True return AXObject.find_ancestor(obj, lambda x: x and x == ancestor) - def isFunctionalDialog(self, obj): + def isFunctionalDialog(self, obj: Atspi.Accessible) -> bool: """Returns True if the window is a functioning as a dialog. This method should be subclassed by application scripts as needed. @@ -728,298 +734,298 @@ class Utilities: return False - def isComment(self, obj): + def isComment(self, obj: Atspi.Accessible) -> bool: return False - def isContentDeletion(self, obj): + def isContentDeletion(self, obj: Atspi.Accessible) -> bool: return False - def isContentError(self, obj): + def isContentError(self, obj: Atspi.Accessible) -> bool: return False - def isContentInsertion(self, obj): + def isContentInsertion(self, obj: Atspi.Accessible) -> bool: return False - def isContentMarked(self, obj): + def isContentMarked(self, obj: Atspi.Accessible) -> bool: return False - def isContentSuggestion(self, obj): + def isContentSuggestion(self, obj: Atspi.Accessible) -> bool: return False - def isInlineSuggestion(self, obj): + def isInlineSuggestion(self, obj: Atspi.Accessible) -> bool: return False - def isFirstItemInInlineContentSuggestion(self, obj): + def isFirstItemInInlineContentSuggestion(self, obj: Atspi.Accessible) -> bool: return False - def isLastItemInInlineContentSuggestion(self, obj): + def isLastItemInInlineContentSuggestion(self, obj: Atspi.Accessible) -> bool: return False - def isEmpty(self, obj): + def isEmpty(self, obj: Atspi.Accessible) -> bool: return False - def isHidden(self, obj): + def isHidden(self, obj: Atspi.Accessible) -> bool: return False - def isDPub(self, obj): + def isDPub(self, obj: Atspi.Accessible) -> bool: return False - def isDPubAbstract(self, obj): + def isDPubAbstract(self, obj: Atspi.Accessible) -> bool: return False - def isDPubAcknowledgments(self, obj): + def isDPubAcknowledgments(self, obj: Atspi.Accessible) -> bool: return False - def isDPubAfterword(self, obj): + def isDPubAfterword(self, obj: Atspi.Accessible) -> bool: return False - def isDPubAppendix(self, obj): + def isDPubAppendix(self, obj: Atspi.Accessible) -> bool: return False - def isDPubBibliography(self, obj): + def isDPubBibliography(self, obj: Atspi.Accessible) -> bool: return False - def isDPubBacklink(self, obj): + def isDPubBacklink(self, obj: Atspi.Accessible) -> bool: return False - def isDPubBiblioref(self, obj): + def isDPubBiblioref(self, obj: Atspi.Accessible) -> bool: return False - def isDPubChapter(self, obj): + def isDPubChapter(self, obj: Atspi.Accessible) -> bool: return False - def isDPubColophon(self, obj): + def isDPubColophon(self, obj: Atspi.Accessible) -> bool: return False - def isDPubConclusion(self, obj): + def isDPubConclusion(self, obj: Atspi.Accessible) -> bool: return False - def isDPubCover(self, obj): + def isDPubCover(self, obj: Atspi.Accessible) -> bool: return False - def isDPubCredit(self, obj): + def isDPubCredit(self, obj: Atspi.Accessible) -> bool: return False - def isDPubCredits(self, obj): + def isDPubCredits(self, obj: Atspi.Accessible) -> bool: return False - def isDPubDedication(self, obj): + def isDPubDedication(self, obj: Atspi.Accessible) -> bool: return False - def isDPubEndnote(self, obj): + def isDPubEndnote(self, obj: Atspi.Accessible) -> bool: return False - def isDPubEndnotes(self, obj): + def isDPubEndnotes(self, obj: Atspi.Accessible) -> bool: return False - def isDPubEpigraph(self, obj): + def isDPubEpigraph(self, obj: Atspi.Accessible) -> bool: return False - def isDPubEpilogue(self, obj): + def isDPubEpilogue(self, obj: Atspi.Accessible) -> bool: return False - def isDPubErrata(self, obj): + def isDPubErrata(self, obj: Atspi.Accessible) -> bool: return False - def isDPubExample(self, obj): + def isDPubExample(self, obj: Atspi.Accessible) -> bool: return False - def isDPubFootnote(self, obj): + def isDPubFootnote(self, obj: Atspi.Accessible) -> bool: return False - def isDPubForeword(self, obj): + def isDPubForeword(self, obj: Atspi.Accessible) -> bool: return False - def isDPubGlossary(self, obj): + def isDPubGlossary(self, obj: Atspi.Accessible) -> bool: return False - def isDPubGlossref(self, obj): + def isDPubGlossref(self, obj: Atspi.Accessible) -> bool: return False - def isDPubIndex(self, obj): + def isDPubIndex(self, obj: Atspi.Accessible) -> bool: return False - def isDPubIntroduction(self, obj): + def isDPubIntroduction(self, obj: Atspi.Accessible) -> bool: return False - def isDPubPagelist(self, obj): + def isDPubPagelist(self, obj: Atspi.Accessible) -> bool: return False - def isDPubPagebreak(self, obj): + def isDPubPagebreak(self, obj: Atspi.Accessible) -> bool: return False - def isDPubPart(self, obj): + def isDPubPart(self, obj: Atspi.Accessible) -> bool: return False - def isDPubPreface(self, obj): + def isDPubPreface(self, obj: Atspi.Accessible) -> bool: return False - def isDPubPrologue(self, obj): + def isDPubPrologue(self, obj: Atspi.Accessible) -> bool: return False - def isDPubPullquote(self, obj): + def isDPubPullquote(self, obj: Atspi.Accessible) -> bool: return False - def isDPubQna(self, obj): + def isDPubQna(self, obj: Atspi.Accessible) -> bool: return False - def isDPubSubtitle(self, obj): + def isDPubSubtitle(self, obj: Atspi.Accessible) -> bool: return False - def isDPubToc(self, obj): + def isDPubToc(self, obj: Atspi.Accessible) -> bool: return False - def isFeed(self, obj): + def isFeed(self, obj: Atspi.Accessible) -> bool: return False - def isFeedArticle(self, obj): + def isFeedArticle(self, obj: Atspi.Accessible) -> bool: return False - def isFigure(self, obj): + def isFigure(self, obj: Atspi.Accessible) -> bool: return False - def isGrid(self, obj): + def isGrid(self, obj: Atspi.Accessible) -> bool: return False - def isGridCell(self, obj): + def isGridCell(self, obj: Atspi.Accessible) -> bool: return False - def isLandmark(self, obj): + def isLandmark(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkWithoutType(self, obj): + def isLandmarkWithoutType(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkBanner(self, obj): + def isLandmarkBanner(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkComplementary(self, obj): + def isLandmarkComplementary(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkContentInfo(self, obj): + def isLandmarkContentInfo(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkForm(self, obj): + def isLandmarkForm(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkMain(self, obj): + def isLandmarkMain(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkNavigation(self, obj): + def isLandmarkNavigation(self, obj: Atspi.Accessible) -> bool: return False - def isDPubNoteref(self, obj): + def isDPubNoteref(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkRegion(self, obj): + def isLandmarkRegion(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkSearch(self, obj): + def isLandmarkSearch(self, obj: Atspi.Accessible) -> bool: return False - def isSVG(self, obj): + def isSVG(self, obj: Atspi.Accessible) -> bool: return False - def speakMathSymbolNames(self, obj=None): + def speakMathSymbolNames(self, obj: Optional[Atspi.Accessible] = None) -> bool: return False - def isInMath(self): + def isInMath(self) -> bool: return False - def isMath(self, obj): + def isMath(self, obj: Atspi.Accessible) -> bool: return False - def isMathLayoutOnly(self, obj): + def isMathLayoutOnly(self, obj: Atspi.Accessible) -> bool: return False - def isMathMultiline(self, obj): + def isMathMultiline(self, obj: Atspi.Accessible) -> bool: return False - def isMathEnclosed(self, obj): + def isMathEnclosed(self, obj: Atspi.Accessible) -> bool: return False - def isMathFenced(self, obj): + def isMathFenced(self, obj: Atspi.Accessible) -> bool: return False - def isMathFractionWithoutBar(self, obj): + def isMathFractionWithoutBar(self, obj: Atspi.Accessible) -> bool: return False - def isMathPhantom(self, obj): + def isMathPhantom(self, obj: Atspi.Accessible) -> bool: return False - def isMathMultiScript(self, obj): + def isMathMultiScript(self, obj: Atspi.Accessible) -> bool: return False - def isMathSubOrSuperScript(self, obj): + def isMathSubOrSuperScript(self, obj: Atspi.Accessible) -> bool: return False - def isMathUnderOrOverScript(self, obj): + def isMathUnderOrOverScript(self, obj: Atspi.Accessible) -> bool: return False - def isMathSquareRoot(self, obj): + def isMathSquareRoot(self, obj: Atspi.Accessible) -> bool: return False - def isMathTable(self, obj): + def isMathTable(self, obj: Atspi.Accessible) -> bool: return False - def isMathTableRow(self, obj): + def isMathTableRow(self, obj: Atspi.Accessible) -> bool: return False - def isMathTableCell(self, obj): + def isMathTableCell(self, obj: Atspi.Accessible) -> bool: return False - def isMathToken(self, obj): + def isMathToken(self, obj: Atspi.Accessible) -> bool: return False - def isMathTopLevel(self, obj): + def isMathTopLevel(self, obj: Atspi.Accessible) -> bool: return False - def getMathDenominator(self, obj): + def getMathDenominator(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathNumerator(self, obj): + def getMathNumerator(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathRootBase(self, obj): + def getMathRootBase(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathRootIndex(self, obj): + def getMathRootIndex(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathScriptBase(self, obj): + def getMathScriptBase(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathScriptSubscript(self, obj): + def getMathScriptSubscript(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathScriptSuperscript(self, obj): + def getMathScriptSuperscript(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathScriptUnderscript(self, obj): + def getMathScriptUnderscript(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathScriptOverscript(self, obj): + def getMathScriptOverscript(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathPrescripts(self, obj): + def getMathPrescripts(self, obj: Atspi.Accessible) -> list[Atspi.Accessible]: return [] - def getMathPostscripts(self, obj): + def getMathPostscripts(self, obj: Atspi.Accessible) -> list[Atspi.Accessible]: return [] - def getMathEnclosures(self, obj): + def getMathEnclosures(self, obj: Atspi.Accessible) -> list[str]: return [] - def getMathFencedSeparators(self, obj): + def getMathFencedSeparators(self, obj: Atspi.Accessible) -> list[str]: return [''] - def getMathFences(self, obj): + def getMathFences(self, obj: Atspi.Accessible) -> list[str]: return ['', ''] - def getMathNestingLevel(self, obj, test=None): + def getMathNestingLevel(self, obj: Atspi.Accessible, test: Optional[Callable[[Atspi.Accessible], bool]] = None) -> int: return 0 - def getLandmarkTypes(self): + def getLandmarkTypes(self) -> list[str]: return ["banner", "complementary", "contentinfo", @@ -1048,7 +1054,7 @@ class Utilities: "region", "search"] - def isProgressBar(self, obj): + def isProgressBar(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_progress_bar(obj): return False @@ -1066,7 +1072,7 @@ class Utilities: return True - def isProgressBarUpdate(self, obj): + def isProgressBarUpdate(self, obj: Atspi.Accessible) -> bool: if not cthulhu.cthulhuApp.settingsManager.getSetting('speakProgressBarUpdates') \ and not cthulhu.cthulhuApp.settingsManager.getSetting('brailleProgressBarUpdates') \ and not cthulhu.cthulhuApp.settingsManager.getSetting('beepProgressBarUpdates'): @@ -1100,7 +1106,7 @@ class Utilities: return True, "Not handled by any other case" - def getValueAsPercent(self, obj): + def getValueAsPercent(self, obj: Atspi.Accessible) -> Optional[Any]: if not AXObject.supports_value(obj): tokens = ["SCRIPT UTILITIES:", obj, "doesn't implement AtspiValue"] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -1123,19 +1129,19 @@ class Utilities: return int((val / (maxval - minval)) * 100) - def isBlockquote(self, obj): + def isBlockquote(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_block_quote(obj) - def isDescriptionList(self, obj): + def isDescriptionList(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_description_list(obj) - def isDescriptionListTerm(self, obj): + def isDescriptionListTerm(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_description_term(obj) - def isDescriptionListDescription(self, obj): + def isDescriptionListDescription(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_description_value(obj) - def descriptionListTerms(self, obj): + def descriptionListTerms(self, obj: Atspi.Accessible) -> list[Any]: if not self.isDescriptionList(obj): return [] @@ -1143,36 +1149,36 @@ class Utilities: _exclude = self.isDescriptionList return self.findAllDescendants(obj, _include, _exclude) - def isDocumentList(self, obj): + def isDocumentList(self, obj: Atspi.Accessible) -> bool: if AXObject.get_role(obj) not in [Atspi.Role.LIST, Atspi.Role.DESCRIPTION_LIST]: return False return AXObject.find_ancestor(obj, AXUtilities.is_document) is not None - def isDocumentPanel(self, obj): + def isDocumentPanel(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_panel(obj): return False return AXObject.find_ancestor(obj, AXUtilities.is_document) is not None - def isDocument(self, obj): + def isDocument(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_document(obj) - def inDocumentContent(self, obj=None): + def inDocumentContent(self, obj: Optional[Atspi.Accessible] = None) -> Optional[Atspi.Accessible]: obj = obj or cthulhu_state.locusOfFocus return self.getDocumentForObject(obj) is not None - def activeDocument(self, window=None): + def activeDocument(self, window: Optional[Atspi.Accessible] = None) -> Optional[Atspi.Accessible]: return self.getTopLevelDocumentForObject(cthulhu_state.locusOfFocus) - def isTopLevelDocument(self, obj): + def isTopLevelDocument(self, obj: Atspi.Accessible) -> bool: return self.isDocument(obj) and not AXObject.find_ancestor(obj, self.isDocument) - def getTopLevelDocumentForObject(self, obj): + def getTopLevelDocumentForObject(self, obj: Atspi.Accessible) -> Any: if self.isTopLevelDocument(obj): return obj return AXObject.find_ancestor(obj, self.isTopLevelDocument) - def getDocumentForObject(self, obj): + def getDocumentForObject(self, obj: Atspi.Accessible) -> Optional[Any]: if not obj: return None @@ -1181,7 +1187,7 @@ class Utilities: return AXObject.find_ancestor(obj, self.isDocument) - def getModalDialog(self, obj): + def getModalDialog(self, obj: Atspi.Accessible) -> bool: if not obj: return False @@ -1190,17 +1196,17 @@ class Utilities: return AXObject.find_ancestor(obj, AXUtilities.is_modal_dialog) - def isModalDialogDescendant(self, obj): + def isModalDialogDescendant(self, obj: Atspi.Accessible) -> bool: if not obj: return False return self.getModalDialog(obj) is not None - def getTable(self, obj): + def getTable(self, obj: Atspi.Accessible) -> Optional[Any]: if not obj: return None - def isTable(x): + def isTable(x: Atspi.Accessible) -> bool: if AXUtilities.is_table(x) or AXUtilities.is_tree_table(x) or AXUtilities.is_tree(x): return AXObject.supports_table(x) return False @@ -1210,17 +1216,17 @@ class Utilities: return AXObject.find_ancestor(obj, isTable) - def isTextDocumentTable(self, obj): + def isTextDocumentTable(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_table(obj): return False doc = self.getDocumentForObject(obj) return doc is not None and not AXUtilities.is_document_spreadsheet(doc) - def isGUITable(self, obj): + def isGUITable(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_table(obj) and self.getDocumentForObject(obj) is None - def isSpreadSheetTable(self, obj): + def isSpreadSheetTable(self, obj: Atspi.Accessible) -> bool: if not (AXUtilities.is_table(obj) and AXObject.supports_table(obj)): return False @@ -1232,17 +1238,17 @@ class Utilities: return AXTable.get_row_count(obj, prefer_attribute=False) > 65536 - def isTextDocumentCell(self, obj): + def isTextDocumentCell(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_table_cell_or_header(obj): return False return AXObject.find_ancestor(obj, self.isTextDocumentTable) - def isSpreadSheetCell(self, obj): + def isSpreadSheetCell(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_table_cell_or_header(obj): return False return AXObject.find_ancestor(obj, self.isSpreadSheetTable) - def cellColumnChanged(self, cell): + def cellColumnChanged(self, cell: Any) -> bool: row, column = self.coordinatesForCell(cell) if column == -1: return False @@ -1250,7 +1256,7 @@ class Utilities: lastColumn = self._script.pointOfReference.get("lastColumn") return column != lastColumn - def cellRowChanged(self, cell): + def cellRowChanged(self, cell: Any) -> bool: row, column = self.coordinatesForCell(cell) if row == -1: return False @@ -1258,7 +1264,7 @@ class Utilities: lastRow = self._script.pointOfReference.get("lastRow") return row != lastRow - def shouldReadFullRow(self, obj): + def shouldReadFullRow(self, obj: Atspi.Accessible) -> bool: if self._script.inSayAll(): return False @@ -1277,16 +1283,16 @@ class Utilities: return cthulhu.cthulhuApp.settingsManager.getSetting('readFullRowInDocumentTable') - def isSorted(self, obj): + def isSorted(self, obj: Atspi.Accessible) -> bool: return False - def isAscending(self, obj): + def isAscending(self, obj: Atspi.Accessible) -> bool: return False - def isDescending(self, obj): + def isDescending(self, obj: Atspi.Accessible) -> bool: return False - def getSortOrderDescription(self, obj, includeName=False): + def getSortOrderDescription(self, obj: Atspi.Accessible, includeName: Any = False) -> str: if not (obj and self.isSorted(obj)): return "" @@ -1302,18 +1308,18 @@ class Utilities: return result - def isFocusableLabel(self, obj): + def isFocusableLabel(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_label(obj) and AXUtilities.is_focusable(obj) - def isNonFocusableList(self, obj): + def isNonFocusableList(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_list(obj) and not AXUtilities.is_focusable(obj) - def isStatusBarNotification(self, obj): + def isStatusBarNotification(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_notification(obj): return False return AXObject.find_ancestor(obj, AXUtilities.is_status_bar) is not None - def getNotificationContent(self, obj): + def getNotificationContent(self, obj: Atspi.Accessible) -> str: if not AXUtilities.is_notification(obj): return "" @@ -1336,7 +1342,7 @@ class Utilities: return " ".join(tokens) - def isTreeDescendant(self, obj): + def isTreeDescendant(self, obj: Atspi.Accessible) -> bool: if obj is None: return False @@ -1345,7 +1351,7 @@ class Utilities: return AXObject.find_ancestor(obj, AXUtilities.is_tree_or_tree_table) is not None - def isLayoutOnly(self, obj): + def isLayoutOnly(self, obj: Atspi.Accessible) -> bool: """Returns True if the given object is a container which has no presentable information (label, name, displayed text, etc.).""" @@ -1442,8 +1448,7 @@ class Utilities: return layoutOnly - @staticmethod - def isInActiveApp(obj): + def isInActiveApp(self, obj: Atspi.Accessible) -> bool: """Returns True if the given object is from the same application that currently has keyboard focus. @@ -1456,12 +1461,12 @@ class Utilities: return AXObject.get_application(cthulhu_state.locusOfFocus) == AXObject.get_application(obj) - def isLink(self, obj): + def isLink(self, obj: Atspi.Accessible) -> bool: """Returns True if obj is a link.""" return AXUtilities.is_link(obj) - def isReadOnlyTextArea(self, obj): + def isReadOnlyTextArea(self, obj: Atspi.Accessible) -> bool: """Returns True if obj is a text entry area that is read only.""" if not self.isTextArea(obj): @@ -1472,10 +1477,10 @@ class Utilities: return AXUtilities.is_focusable(obj) and not AXUtilities.is_editable(obj) - def isSwitch(self, obj): + def isSwitch(self, obj: Atspi.Accessible) -> bool: return False - def get_objectFromPath(self, path): + def get_objectFromPath(self, path: int) -> Any: start = self._script.app rv = None for p in path: @@ -1490,7 +1495,7 @@ class Utilities: return rv - def _hasSamePath(self, obj1, obj2): + def _hasSamePath(self, obj1: Any, obj2: Any) -> bool: path1 = AXObject.get_path(obj1) path2 = AXObject.get_path(obj2) if len(path1) != len(path2): @@ -1524,8 +1529,9 @@ class Utilities: return path1[0:index] == path2[0:index] - def isSameObject(self, obj1, obj2, comparePaths=False, ignoreNames=False, - ignoreDescriptions=True): + def isSameObject(self, obj1: Optional[Atspi.Accessible], obj2: Optional[Atspi.Accessible], + comparePaths: bool = False, ignoreNames: bool = False, + ignoreDescriptions: bool = True) -> bool: if obj1 == obj2: return True @@ -1572,7 +1578,7 @@ class Utilities: return False - def isTextArea(self, obj): + def isTextArea(self, obj: Atspi.Accessible) -> bool: """Returns True if obj is a GUI component that is for entering text. Arguments: @@ -1584,22 +1590,22 @@ class Utilities: return self._isTextAreaByType(obj) - def _isTextAreaByType(self, obj): + def _isTextAreaByType(self, obj: Atspi.Accessible) -> Any: # NOTE: This is legacy and may need more checks now. return AXUtilities.is_text_input(obj) \ or AXUtilities.is_text(obj) \ or AXUtilities.is_paragraph(obj) - def labelsForObject(self, obj): + def labelsForObject(self, obj: Atspi.Accessible) -> Any: """Return a list of the labels for this object.""" - def isNotAncestor(acc): + def isNotAncestor(acc: Atspi.Accessible) -> bool: return not AXObject.find_ancestor(obj, lambda x: x == acc) result = AXUtilitiesRelation.get_is_labelled_by(obj) return list(filter(isNotAncestor, result)) - def linkBasenameToName(self, obj): + def linkBasenameToName(self, obj: Atspi.Accessible) -> str: basename = self.linkBasename(obj) if not basename: return "" @@ -1612,8 +1618,7 @@ class Utilities: return basename - @staticmethod - def linkBasename(obj): + def linkBasename(self, obj: Atspi.Accessible) -> Any: """Returns the relevant information from the URI. The idea is to attempt to strip off all prefix and suffix, much like the basename command in a shell.""" @@ -1666,8 +1671,7 @@ class Utilities: return basename - @staticmethod - def linkIndex(obj, characterIndex): + def linkIndex(self, obj: Atspi.Accessible, characterIndex: int) -> int: """A brute force method to see if an offset is a link. This is provided because not all Accessible Hypertext implementations properly support the getLinkIndex method. Returns an index of @@ -1695,7 +1699,7 @@ class Utilities: return -1 - def nestingLevel(self, obj): + def nestingLevel(self, obj: Atspi.Accessible) -> int: """Determines the nesting level of this object. Arguments: @@ -1711,7 +1715,7 @@ class Utilities: if self.NESTING_LEVEL not in self._script.generatorCache: self._script.generatorCache[self.NESTING_LEVEL] = {} - def pred(x): + def pred(x: Atspi.Accessible) -> bool: if self.isBlockquote(obj): return self.isBlockquote(x) if AXUtilities.is_list_item(obj): @@ -1728,7 +1732,7 @@ class Utilities: self._script.generatorCache[self.NESTING_LEVEL][obj] = nestingLevel return self._script.generatorCache[self.NESTING_LEVEL][obj] - def nodeLevel(self, obj): + def nodeLevel(self, obj: Atspi.Accessible) -> int: """Determines the node level of this object if it is in a tree relation, with 0 being the top level node. If this object is not in a tree relation, then -1 will be returned. @@ -1772,7 +1776,7 @@ class Utilities: self._script.generatorCache[self.NODE_LEVEL][obj] = len(nodes) - 1 return self._script.generatorCache[self.NODE_LEVEL][obj] - def isOnScreen(self, obj, boundingbox=None): + def isOnScreen(self, obj: Atspi.Accessible, boundingbox: Any = None) -> bool: if AXObject.is_dead(obj): return False @@ -1835,7 +1839,7 @@ class Utilities: return True - def selectedMenuBarMenu(self, menubar): + def selectedMenuBarMenu(self, menubar: Any) -> Optional[Any]: if not AXUtilities.is_menu_bar(menubar): return None @@ -1850,9 +1854,9 @@ class Utilities: if AXUtilities.is_expanded(menu) or AXUtilities.is_selected(menu): return menu - return None + return - def isInOpenMenuBarMenu(self, obj): + def isInOpenMenuBarMenu(self, obj: Atspi.Accessible) -> bool: if obj is None: return False @@ -1867,7 +1871,7 @@ class Utilities: if selectedMenu is None: return False - def inSelectedMenu(x): + def inSelectedMenu(x: Atspi.Accessible) -> bool: return x == selectedMenu if inSelectedMenu(obj): @@ -1875,13 +1879,13 @@ class Utilities: return AXObject.find_ancestor(obj, inSelectedMenu) is not None - def isStaticTextLeaf(self, obj): + def isStaticTextLeaf(self, obj: Atspi.Accessible) -> bool: return False - def isListItemMarker(self, obj): + def isListItemMarker(self, obj: Atspi.Accessible) -> bool: return False - def hasPresentableText(self, obj): + def hasPresentableText(self, obj: Atspi.Accessible) -> bool: if self.isStaticTextLeaf(obj): return False @@ -1890,7 +1894,7 @@ class Utilities: return bool(re.search(r"\w+", AXText.get_all_text(obj))) - def getOnScreenObjects(self, root, extents=None): + def getOnScreenObjects(self, root: Any, extents: Any = None) -> list[Any]: if not self.isOnScreen(root, extents): return [] @@ -1938,7 +1942,7 @@ class Utilities: elif self.hasPresentableText(root): objects.append(root) - def pred(x): + def pred(x: Atspi.Accessible) -> bool: return x is not None and not self.isStaticTextLeaf(x) for child in AXObject.iter_children(root, pred): @@ -1967,8 +1971,7 @@ class Utilities: return [root] - @staticmethod - def isTableRow(obj): + def isTableRow(self, obj: Atspi.Accessible) -> bool: """Determines if obj is a table row -- real or functionally.""" childCount = AXObject.get_child_count(obj) @@ -1993,11 +1996,11 @@ class Utilities: return False - def realActiveAncestor(self, obj): + def realActiveAncestor(self, obj: Atspi.Accessible) -> Any: if AXUtilities.is_focused(obj): return obj - def pred(x): + def pred(x: Atspi.Accessible) -> bool: return AXUtilities.is_table_cell_or_header(x) or AXUtilities.is_list_item(x) ancestor = AXObject.find_ancestor(obj, pred) @@ -2007,7 +2010,7 @@ class Utilities: return obj - def realActiveDescendant(self, obj): + def realActiveDescendant(self, obj: Atspi.Accessible) -> Optional[Any]: """Given an object that should be a child of an object that manages its descendants, return the child that is the real active descendant carrying useful information. @@ -2026,7 +2029,7 @@ class Utilities: if AXObject.get_name(obj): return obj - def pred(x): + def pred(x: Atspi.Accessible) -> bool: return AXObject.get_name(x) or AXText.get_all_text(x) child = AXObject.find_descendant(obj, pred) @@ -2035,13 +2038,13 @@ class Utilities: return obj - def isStatusBarDescendant(self, obj): + def isStatusBarDescendant(self, obj: Atspi.Accessible) -> bool: if obj is None: return False return AXObject.find_ancestor(obj, AXUtilities.is_status_bar) is not None - def statusBarItems(self, obj): + def statusBarItems(self, obj: Atspi.Accessible) -> list[Any]: if not AXUtilities.is_status_bar(obj): return [] @@ -2049,7 +2052,7 @@ class Utilities: items = self._script.pointOfReference.get('statusBarItems') if not items: - def include(x): + def include(x: Atspi.Accessible) -> bool: return not AXUtilities.is_status_bar(x) items = list(filter(include, self.getOnScreenObjects(obj))) @@ -2061,10 +2064,10 @@ class Utilities: return items - def infoBar(self, root): + def infoBar(self, root: Any) -> Optional[Any]: return None - def _topLevelRoles(self): + def _topLevelRoles(self) -> Any: roles = [Atspi.Role.DIALOG, Atspi.Role.FILE_CHOOSER, Atspi.Role.FRAME, @@ -2073,7 +2076,7 @@ class Utilities: roles.append(Atspi.Role.ALERT) return roles - def _locusOfFocusIsTopLevelObject(self): + def _locusOfFocusIsTopLevelObject(self) -> bool: if not cthulhu_state.locusOfFocus: return False @@ -2082,7 +2085,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return rv - def _findWindowWithDescendant(self, child): + def _findWindowWithDescendant(self, child: Any) -> Optional[Any]: """Searches each frame/window/dialog of an application to find the one which contains child. This is extremely non-performant and should only be used to work around broken accessibility trees where topLevelObject @@ -2104,11 +2107,11 @@ class Utilities: return None - def _isTopLevelObject(self, obj): + def _isTopLevelObject(self, obj: Atspi.Accessible) -> Any: return AXObject.get_role(obj) in self._topLevelRoles() \ and AXObject.get_role(AXObject.get_parent(obj)) == Atspi.Role.APPLICATION - def topLevelObject(self, obj, useFallbackSearch=False): + def topLevelObject(self, obj: Atspi.Accessible, useFallbackSearch: bool = False) -> Any: """Returns the top-level object (frame, dialog ...) containing obj, or None if obj is not inside a top-level object. @@ -2131,7 +2134,7 @@ class Utilities: return rv - def topLevelObjectIsActiveAndCurrent(self, obj=None): + def topLevelObjectIsActiveAndCurrent(self, obj: Optional[Atspi.Accessible] = None) -> bool: obj = obj or cthulhu_state.locusOfFocus topLevel = self.topLevelObject(obj) if not topLevel: @@ -2146,8 +2149,7 @@ class Utilities: return True - @staticmethod - def onSameLine(obj1, obj2, delta=0): + def onSameLine(self, obj1: Atspi.Accessible, obj2: Atspi.Accessible, delta: int = 0) -> bool: """Determines if obj1 and obj2 are on the same line.""" if not AXObject.supports_component(obj1) or not AXObject.supports_component(obj2): @@ -2164,8 +2166,7 @@ class Utilities: return abs(center1 - center2) <= delta - @staticmethod - def pathComparison(path1, path2): + def pathComparison(self, path1: int, path2: int) -> int: """Compares the two paths and returns -1, 0, or 1 to indicate if path1 is before, the same, or after path2.""" @@ -2184,8 +2185,7 @@ class Utilities: return 0 - @staticmethod - def sizeComparison(obj1, obj2): + def sizeComparison(self, obj1: Atspi.Accessible, obj2: Atspi.Accessible) -> Any: width1, height1 = 0, 0 width2, height2 = 0, 0 @@ -2205,8 +2205,7 @@ class Utilities: return (width1 * height1) - (width2 * height2) - @staticmethod - def spatialComparison(obj1, obj2): + def spatialComparison(self, obj1: Atspi.Accessible, obj2: Atspi.Accessible) -> Any: """Compares the physical locations of obj1 and obj2 and returns -1, 0, or 1 to indicate if obj1 physically is before, is in the same place as, or is after obj2.""" @@ -2242,7 +2241,7 @@ class Utilities: return rv - def getTextBoundingBox(self, obj, start, end): + def getTextBoundingBox(self, obj: Atspi.Accessible, start: int, end: int) -> int: if not AXObject.supports_text(obj): return -1, -1, 0, 0 @@ -2255,7 +2254,7 @@ class Utilities: return extents - def getBoundingBox(self, obj): + def getBoundingBox(self, obj: Atspi.Accessible) -> int: if not AXObject.supports_component(obj): return -1, -1, 0, 0 @@ -2268,7 +2267,7 @@ class Utilities: return extents.x, extents.y, extents.width, extents.height - def hasNoSize(self, obj): + def hasNoSize(self, obj: Atspi.Accessible) -> bool: if not obj: return False @@ -2287,10 +2286,10 @@ class Utilities: return not (extents.width and extents.height) - def findAllDescendants(self, root, includeIf=None, excludeIf=None): + def findAllDescendants(self, root: Any, includeIf: Any = None, excludeIf: Any = None) -> Any: return AXObject.find_all_descendants(root, includeIf, excludeIf) - def unrelatedLabels(self, root, onlyShowing=True, minimumWords=3): + def unrelatedLabels(self, root: Any, onlyShowing: Any = True, minimumWords: Any = 3) -> list[Any]: """Returns a list containing all the unrelated (i.e., have no relations to anything and are not a fundamental element of a more atomic component like a combo box) labels under the given @@ -2317,7 +2316,7 @@ class Utilities: Atspi.Role.TREE, Atspi.Role.TREE_TABLE] - def _include(x): + def _include(x: Atspi.Accessible) -> bool: if not (x and AXObject.get_role(x) in labelRoles): return False if AXUtilitiesRelation.get_relations(x): @@ -2326,7 +2325,7 @@ class Utilities: return False return True - def _exclude(x): + def _exclude(x: Atspi.Accessible) -> bool: if not x or AXObject.get_role(x) in skipRoles: return True if onlyShowing and not AXUtilities.is_showing(x): @@ -2352,10 +2351,10 @@ class Utilities: return sorted(labels, key=functools.cmp_to_key(self.spatialComparison)) - def _treatAlertsAsDialogs(self): + def _treatAlertsAsDialogs(self) -> bool: return True - def unfocusedAlertAndDialogCount(self, obj): + def unfocusedAlertAndDialogCount(self, obj: Atspi.Accessible) -> Any: """If the current application has one or more alert or dialog windows and the currently focused window is not an alert or a dialog, return a count of the number of alert and dialog windows, otherwise @@ -2371,24 +2370,24 @@ class Utilities: if self._treatAlertsAsDialogs(): roles.append(Atspi.Role.ALERT) - def isDialog(x): + def isDialog(x: Atspi.Accessible) -> bool: return AXObject.get_role(x) in roles or self.isFunctionalDialog(x) dialogs = [x for x in AXObject.iter_children(AXObject.get_application(obj), isDialog)] dialogs.extend([x for x in AXObject.iter_children(self.topLevelObject(obj), isDialog)]) - def isPresentable(x): + def isPresentable(x: Atspi.Accessible) -> bool: return self.isShowingAndVisible(x) \ and (AXObject.get_name(x) or AXObject.get_child_count(x)) - def cannotBeActiveWindow(x): + def cannotBeActiveWindow(x: Atspi.Accessible) -> bool: return not self.canBeActiveWindow(x) presentable = list(filter(isPresentable, set(dialogs))) unfocused = list(filter(cannotBeActiveWindow, presentable)) return len(unfocused) - def uri(self, obj): + def uri(self, obj: Atspi.Accessible) -> Any: """Return the URI for a given link object. Arguments: @@ -2404,8 +2403,7 @@ class Utilities: # # ######################################################################### - @staticmethod - def adjustTextSelection(obj, offset): + def adjustTextSelection(self, obj: Atspi.Accessible, offset: int) -> None: """Adjusts the end point of a text selection Arguments: @@ -2431,7 +2429,7 @@ class Utilities: AXText.set_selected_text(obj, startOffset, endOffset) - def findPreviousObject(self, obj): + def findPreviousObject(self, obj: Atspi.Accessible) -> Optional[Any]: """Finds the object before this one.""" if not obj or self.isZombie(obj): @@ -2443,7 +2441,7 @@ class Utilities: return AXUtilities.get_previous_object(obj) - def findNextObject(self, obj): + def findNextObject(self, obj: Atspi.Accessible) -> Optional[Any]: """Finds the object after this one.""" if not obj or self.isZombie(obj): @@ -2455,7 +2453,7 @@ class Utilities: return AXUtilities.get_next_object(obj) - def allSelectedText(self, obj): + def allSelectedText(self, obj: Atspi.Accessible) -> Any: """Get all the text applicable text selections for the given object. including any previous or next text objects that also have selected text and add in their text contents. @@ -2494,8 +2492,7 @@ class Utilities: return textContents, startOffset, endOffset - @staticmethod - def allTextSelections(obj): + def allTextSelections(self, obj: Atspi.Accessible) -> Any: """Get a list of text selections in the given accessible object, equivalent to getNSelections()*texti.getSelection() @@ -2509,7 +2506,7 @@ class Utilities: return AXText.get_selected_ranges(obj) - def getChildAtOffset(self, obj, offset): + def getChildAtOffset(self, obj: Atspi.Accessible, offset: Any) -> Optional[Any]: child = AXHypertext.get_child_at_offset(obj, offset) if not child: return None @@ -2518,7 +2515,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return child - def findChildAtOffset(self, obj, offset): + def findChildAtOffset(self, obj: Atspi.Accessible, offset: int) -> Any: """Attempts to correct for off-by-one brokenness in hypertext implementations. We're seeing off-by-one errors in (at least) Chromium where the text @@ -2551,7 +2548,7 @@ class Utilities: return None - def characterOffsetInParent(self, obj): + def characterOffsetInParent(self, obj: Atspi.Accessible) -> Any: """Returns the character offset of the embedded object character for this object in its parent's accessible text. @@ -2580,7 +2577,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return offset - def clearTextSelection(self, obj): + def clearTextSelection(self, obj: Atspi.Accessible) -> None: """Clears the text selection if the object supports it. Arguments: @@ -2593,11 +2590,11 @@ class Utilities: for i in range(AXText._get_n_selections(obj)): AXText._remove_selection(obj, i) - def containsOnlyEOCs(self, obj): + def containsOnlyEOCs(self, obj: Atspi.Accessible) -> Any: string = AXText.get_all_text(obj) return string and not re.search(r"[^\ufffc]", string) - def expandEOCs(self, obj, startOffset=0, endOffset=-1): + def expandEOCs(self, obj: Atspi.Accessible, startOffset: Any = 0, endOffset: Any = -1) -> str: """Expands the current object replacing EMBEDDED_OBJECT_CHARACTERS with their text. @@ -2637,7 +2634,7 @@ class Utilities: return "".join(toBuild) - def isWordMisspelled(self, obj, offset): + def isWordMisspelled(self, obj: Atspi.Accessible, offset: Any) -> bool: """Identifies if the current word is flagged as misspelled by the application. Different applications and toolkits flag misspelled words differently. Thus each script will likely need to implement @@ -2661,16 +2658,16 @@ class Utilities: return False - def getError(self, obj): + def getError(self, obj: Atspi.Accessible) -> Any: return AXUtilities.is_invalid_entry(obj) - def getErrorMessage(self, obj): + def getErrorMessage(self, obj: Atspi.Accessible) -> str: return "" - def isErrorMessage(self, obj): + def isErrorMessage(self, obj: Atspi.Accessible) -> bool: return False - def getCharacterAtOffset(self, obj, offset=None): + def getCharacterAtOffset(self, obj: Atspi.Accessible, offset: int = None) -> Any: if AXObject.supports_text(obj): if offset is None: offset = AXText.get_caret_offset(obj) @@ -2678,7 +2675,7 @@ class Utilities: return "" - def queryNonEmptyText(self, obj): + def queryNonEmptyText(self, obj: Atspi.Accessible) -> Optional[Any]: """Get the text interface associated with an object, if it is non-empty. @@ -2695,10 +2692,10 @@ class Utilities: return None - def deletedText(self, event): + def deletedText(self, event: Any) -> Any: return event.any_data - def insertedText(self, event): + def insertedText(self, event: Any) -> Any: if event.any_data: return event.any_data @@ -2713,7 +2710,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return "" - def _getFallbackInsertedText(self, event): + def _getFallbackInsertedText(self, event: Any) -> str: if not AXUtilities.is_password_text(event.source): return "" @@ -2729,7 +2726,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return string[-1] - def selectedText(self, obj): + def selectedText(self, obj: Atspi.Accessible) -> Any: """Get the text selection for the given object. Arguments: @@ -2754,7 +2751,7 @@ class Utilities: return [textContents, startOffset, endOffset] - def getCaretContext(self): + def getCaretContext(self) -> Any: obj = cthulhu_state.locusOfFocus if AXObject.supports_text(obj): offset = AXText.get_caret_offset(obj) @@ -2763,14 +2760,14 @@ class Utilities: return obj, offset - def getFirstCaretPosition(self, obj): + def getFirstCaretPosition(self, obj: Atspi.Accessible) -> Any: return obj, 0 - def setCaretPosition(self, obj, offset, documentFrame=None): + def setCaretPosition(self, obj: Atspi.Accessible, offset: int, documentFrame: Any = None) -> Any: cthulhu.setLocusOfFocus(None, obj, False) self.setCaretOffset(obj, offset) - def setCaretOffset(self, obj, offset): + def setCaretOffset(self, obj: Atspi.Accessible, offset: int) -> Any: """Set the caret offset on a given accessible. Similar to Accessible.setCaretOffset() @@ -2780,7 +2777,7 @@ class Utilities: """ AXText.set_caret_offset(obj, offset) - def substring(self, obj, startOffset, endOffset): + def substring(self, obj: Atspi.Accessible, startOffset: Any, endOffset: Any) -> Any: """Returns the substring of the given object's text specialization. Arguments: @@ -2792,7 +2789,7 @@ class Utilities: return AXText.get_substring(obj, startOffset, endOffset) - def getAppNameForAttribute(self, attribName): + def getAppNameForAttribute(self, attribName: Any) -> Any: """Converts the given Atk attribute name into the application's equivalent. This is necessary because an application or toolkit (e.g. Gecko) might invent entirely new names for the same text @@ -2811,7 +2808,7 @@ class Utilities: return attribName - def getAtkNameForAttribute(self, attribName): + def getAtkNameForAttribute(self, attribName: Any) -> Any: """Converts the given attribute name into the Atk equivalent. This is necessary because an application or toolkit (e.g. Gecko) might invent entirely new names for the same attributes. @@ -2824,7 +2821,7 @@ class Utilities: return self._script.attributeNamesDict.get(attribName, attribName) - def getAllTextAttributesForObject(self, obj, startOffset=0, endOffset=-1): + def getAllTextAttributesForObject(self, obj: Atspi.Accessible, startOffset: Any = 0, endOffset: Any = -1) -> list[Any]: """Returns a list of (start, end, attrsDict) tuples for obj.""" if not AXObject.supports_text(obj): return [] @@ -2851,7 +2848,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return rv - def textAttributes(self, acc, offset=None, get_defaults=False): + def textAttributes(self, acc: Any, offset: Any = None, get_defaults: Any = False) -> dict[Any, Any]: """Get the text attributes run for a given offset in a given accessible Arguments: @@ -2875,7 +2872,7 @@ class Utilities: attrs, start, end = AXText.get_text_attributes_at_offset(acc, offset) return attrs, min(start, offset), max(end, offset + 1) - def localizeTextAttribute(self, key, value): + def localizeTextAttribute(self, key: Any, value: Any) -> Any: if key == "weight" and (value == "bold" or int(value) > 400): return messages.BOLD @@ -2903,7 +2900,7 @@ class Utilities: return f"{localizedKey}: {localizedValue}" - def splitSubstringByLanguage(self, obj, start, end): + def splitSubstringByLanguage(self, obj: Atspi.Accessible, start: int, end: int) -> Any: """Returns a list of (start, end, string, language, dialect) tuples.""" rv = [] @@ -2920,7 +2917,7 @@ class Utilities: return rv - def getLanguageAndDialectForSubstring(self, obj, start, end): + def getLanguageAndDialectForSubstring(self, obj: Atspi.Accessible, start: int, end: int) -> Any: """Returns a (language, dialect) tuple. If multiple languages apply to the substring, language and dialect will be empty strings. Callers must do any preprocessing to avoid that condition.""" @@ -2932,7 +2929,7 @@ class Utilities: return "", "" - def getLanguageAndDialectFromTextAttributes(self, obj, startOffset=0, endOffset=-1): + def getLanguageAndDialectFromTextAttributes(self, obj: Atspi.Accessible, startOffset: Any = 0, endOffset: Any = -1) -> Any: """Returns a list of (start, end, language, dialect) tuples for obj based on what is exposed via text attributes.""" @@ -2952,7 +2949,7 @@ class Utilities: return rv - def willEchoCharacter(self, event): + def willEchoCharacter(self, event: Any) -> bool: """Given a keyboard event containing an alphanumeric key, determine if the script is likely to echo it as a character. """ @@ -2978,7 +2975,7 @@ class Utilities: # # ######################################################################### - def _addRepeatSegment(self, segment, line, respectPunctuation=True): + def _addRepeatSegment(self, segment: Any, line: Any, respectPunctuation: Any = True) -> Any: """Add in the latest line segment, adjusting for repeat characters and punctuation. @@ -3014,7 +3011,7 @@ class Utilities: return line - def shouldVerbalizeAllPunctuation(self, obj): + def shouldVerbalizeAllPunctuation(self, obj: Atspi.Accessible) -> bool: if not (self.isCode(obj) or self.isCodeDescendant(obj)): return False @@ -3027,7 +3024,7 @@ class Utilities: return True - def verbalizeAllPunctuation(self, string): + def verbalizeAllPunctuation(self, string: Any) -> Any: result = string for symbol in set(re.findall(self.PUNCTUATION, result)): charName = f" {chnames.getCharacterName(symbol)} " @@ -3035,7 +3032,7 @@ class Utilities: return result - def adjustForLinks(self, obj, line, startOffset): + def adjustForLinks(self, obj: Atspi.Accessible, line: Any, startOffset: Any) -> Any: """Adjust line to include the word "link" after any hypertext links. Arguments: @@ -3085,18 +3082,16 @@ class Utilities: return "".join(adjustedLine) - @staticmethod - def _processMultiCaseString(string): + def _processMultiCaseString(self, string: Any) -> Any: return re.sub(r'(?<=[a-z])(?=[A-Z])', ' ', string) - @staticmethod - def _convertWordToDigits(word): + def _convertWordToDigits(self, word: Any) -> Any: if not word.isnumeric(): return word return ' '.join(list(word)) - def adjustForPronunciation(self, line): + def adjustForPronunciation(self, line: Any) -> Any: """Adjust the line to replace words in the pronunciation dictionary, with what those words actually sound like. @@ -3134,7 +3129,7 @@ class Utilities: return newLine - def adjustForRepeats(self, line): + def adjustForRepeats(self, line: Any) -> Any: """Adjust line to include repeat character counts. As some people will want this and others might not, there is a setting in settings.py that determines whether this functionality is enabled. @@ -3172,7 +3167,7 @@ class Utilities: return self._addRepeatSegment(segment, newLine, multipleChars) - def adjustForDigits(self, string): + def adjustForDigits(self, string: Any) -> Any: """Adjusts the string to convert digit-like text, such as subscript and superscript numbers, into actual digits. @@ -3205,7 +3200,7 @@ class Utilities: _INDENTATION_DURATION = 0.15 _INDENTATION_VOLUME = 0.7 - def _get_indentation_key(self, obj): + def _get_indentation_key(self, obj: Atspi.Accessible) -> Any: if obj is None: return "global" @@ -3214,8 +3209,7 @@ class Utilities: except Exception: return str(obj) - @staticmethod - def _extract_indentation(line): + def _extract_indentation(self, line: Any) -> str: if not line: return "" @@ -3226,8 +3220,7 @@ class Utilities: return line - @staticmethod - def _get_indentation_columns(indentation, tabWidth): + def _get_indentation_columns(self, indentation: Any, tabWidth: Any) -> Any: columns = 0 tabWidth = max(1, tabWidth) for char in indentation: @@ -3238,7 +3231,7 @@ class Utilities: return columns - def _get_indentation_data(self, line): + def _get_indentation_data(self, line: Any) -> Any: indentation = self._extract_indentation(line) columns = self._get_indentation_columns(indentation, self._INDENTATION_TAB_WIDTH) return { @@ -3246,14 +3239,14 @@ class Utilities: "columns": columns, } - def _remember_indentation(self, obj, data): + def _remember_indentation(self, obj: Atspi.Accessible, data: Any) -> bool: key = self._get_indentation_key(obj) self._lastIndentationData[key] = { "signature": data["indentation"], "columns": data["columns"], } - def _indentation_has_changed(self, obj, data): + def _indentation_has_changed(self, obj: Atspi.Accessible, data: Any) -> bool: key = self._get_indentation_key(obj) previous = self._lastIndentationData.get(key) self._remember_indentation(obj, data) @@ -3264,12 +3257,12 @@ class Utilities: previousColumns = previous.get("columns", 0) return changed, previousColumns - def _indentation_enabled(self): + def _indentation_enabled(self) -> bool: if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return False return cthulhu.cthulhuApp.settingsManager.getSetting('enableIndentation') - def _indentation_speech_enabled(self): + def _indentation_speech_enabled(self) -> bool: if not self._indentation_enabled(): return False @@ -3280,7 +3273,7 @@ class Utilities: settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS, ) - def _indentation_beeps_enabled(self): + def _indentation_beeps_enabled(self) -> bool: if not self._indentation_enabled(): return False @@ -3291,7 +3284,7 @@ class Utilities: settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS, ) - def _play_indentation_tone(self, columns, previousColumns): + def _play_indentation_tone(self, columns: Any, previousColumns: Any) -> None: """Play an audio tone indicating indentation level.""" if not _SOUND_AVAILABLE: return @@ -3325,7 +3318,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, f"INDENTATION: error playing tone: {e}", True) - def get_indentation_presentation(self, line, obj=None): + def get_indentation_presentation(self, line: Any, obj: Optional[Atspi.Accessible] = None) -> str: data = self._get_indentation_data(line) hasIndentation = bool(data["indentation"]) presentationMode = cthulhu.cthulhuApp.settingsManager.getSetting('indentationPresentationMode') \ @@ -3367,19 +3360,18 @@ class Utilities: return "", hasIndentation - def should_strip_indentation(self, line): + def should_strip_indentation(self, line: Any) -> bool: if not self._indentation_enabled(): return False data = self._get_indentation_data(line) return bool(data["indentation"]) - def indentationDescription(self, line, obj=None): + def indentationDescription(self, line: Any, obj: Optional[Atspi.Accessible] = None) -> Any: description, _hasIndentation = self.get_indentation_presentation(line, obj=obj) return description - @staticmethod - def absoluteMouseCoordinates(): + def absoluteMouseCoordinates(self) -> Any: """Gets the absolute position of the mouse pointer.""" from gi.repository import Gtk @@ -3388,8 +3380,7 @@ class Utilities: return x, y - @staticmethod - def appendString(text, newText, delimiter=" "): + def appendString(self, text: Any, newText: Any, delimiter: Any = " ") -> Any: """Appends the newText to the given text with the delimiter in between and returns the new string. Edge cases, such as no initial text or no newText, are handled gracefully.""" @@ -3401,7 +3392,7 @@ class Utilities: return text + delimiter + newText - def treatAsDuplicateEvent(self, event1, event2): + def treatAsDuplicateEvent(self, event1: Any, event2: Any) -> bool: if not (event1 and event2): return False @@ -3415,7 +3406,7 @@ class Utilities: and event1.detail2 == event2.detail2 \ and event1.any_data == event2.any_data - def isAutoTextEvent(self, event): + def isAutoTextEvent(self, event: Any) -> bool: """Returns True if event is associated with text being autocompleted or autoinserted or autocorrected or autosomethingelsed. @@ -3457,7 +3448,7 @@ class Utilities: return False - def isSentenceDelimiter(self, currentChar, previousChar): + def isSentenceDelimiter(self, currentChar: Any, previousChar: Any) -> bool: """Returns True if we are positioned at the end of a sentence. This is determined by checking if the current character is a white space character and the previous character is one of the @@ -3476,7 +3467,7 @@ class Utilities: return currentChar in self._script.whitespace \ and previousChar in '!.?:;' - def isWordDelimiter(self, character): + def isWordDelimiter(self, character: Any) -> Any: """Returns True if the given character is a word delimiter. Arguments: @@ -3489,7 +3480,7 @@ class Utilities: or character in r'!*+,-./:;<=>?@[\]^_{|}' \ or character == self._script.NO_BREAK_SPACE_CHARACTER - def intersectingRegion(self, obj1, obj2, coordType=None): + def intersectingRegion(self, obj1: Any, obj2: Any, coordType: Any = None) -> int: """Returns the extents of the intersection of obj1 and obj2.""" if coordType is None: @@ -3506,8 +3497,8 @@ class Utilities: return self.intersection(extents1, extents2) - def intersection(self, extents1, extents2): - def _toTuple(extents): + def intersection(self, extents1: Any, extents2: Any) -> int: + def _toTuple(extents: Any) -> tuple[int, int, int, int]: if extents is None: return 0, 0, 0, 0 if hasattr(extents, "x") and hasattr(extents, "y") \ @@ -3538,11 +3529,10 @@ class Utilities: return x, y, width, height - def containsRegion(self, extents1, extents2): + def containsRegion(self, extents1: Any, extents2: Any) -> Any: return self.intersection(extents1, extents2) != (0, 0, 0, 0) - @staticmethod - def _extentsToTuple(extents): + def _extentsToTuple(self, extents: Any) -> int: if extents is None: return 0, 0, 0, 0 if hasattr(extents, "x") and hasattr(extents, "y") \ @@ -3550,14 +3540,12 @@ class Utilities: return extents.x, extents.y, extents.width, extents.height return tuple(extents) - @staticmethod - def _allNamesForKeyCode(keycode): + def _allNamesForKeyCode(self, keycode: Any) -> Any: keymap = Gdk.Keymap.get_default() entries = keymap.get_entries_for_keycode(keycode)[-1] return list(map(Gdk.keyval_name, set(entries))) - @staticmethod - def _lastKeyCodeAndModifiers(): + def _lastKeyCodeAndModifiers(self) -> int: if not isinstance(cthulhu_state.lastInputEvent, input_event.KeyboardEvent): return 0, 0 @@ -3567,8 +3555,7 @@ class Utilities: return 0, 0 - @staticmethod - def lastKeyAndModifiers(): + def lastKeyAndModifiers(self) -> Any: """Convenience method which returns a tuple containing the event string and modifiers of the last non-modifier key event or ("", 0) if there is no such event.""" @@ -3587,8 +3574,7 @@ class Utilities: return (eventStr, mods) - @staticmethod - def labelFromKeySequence(sequence): + def labelFromKeySequence(self, sequence: Any) -> Any: """Turns a key sequence into a user-presentable label.""" try: @@ -3606,7 +3592,7 @@ class Utilities: return keynames.localizeKeySequence(sequence) - def mnemonicShortcutAccelerator(self, obj): + def mnemonicShortcutAccelerator(self, obj: Atspi.Accessible) -> Any: """Gets the mnemonic, accelerator string and possibly shortcut for the given object. These are based upon the first accessible action for the object. @@ -3665,8 +3651,7 @@ class Utilities: [mnemonic, fullShortcut, accelerator] return self._script.generatorCache[self.KEY_BINDING][obj] - @staticmethod - def stringToKeysAndDict(string): + def stringToKeysAndDict(self, string: Any) -> list[Any]: """Converts a string made up of a series of :; pairs into a dictionary of keys and values. Text before the colon is the key and text afterwards is the value. The final semi-colon, if @@ -3690,7 +3675,7 @@ class Utilities: return [keys, dictionary] - def textForValue(self, obj): + def textForValue(self, obj: Atspi.Accessible) -> Any: """Returns the text to be displayed for the object's current value. Arguments: @@ -3734,8 +3719,7 @@ class Utilities: formatter = "%%.%df" % decimalPlaces return formatter % currentValue - @staticmethod - def unicodeValueString(character): + def unicodeValueString(self, character: Any) -> Any: """ Returns a four hex digit representation of the given character Arguments: @@ -3750,7 +3734,7 @@ class Utilities: debug.printException(debug.LEVEL_WARNING) return "" - def getLinesForRange(self, obj, startOffset, endOffset): + def getLinesForRange(self, obj: Atspi.Accessible, startOffset: Any, endOffset: Any) -> list[Any]: if not AXObject.supports_text(obj): return [] @@ -3765,25 +3749,25 @@ class Utilities: return lines - def getLineContentsAtOffset(self, obj, offset, layoutMode=True, useCache=True): + def getLineContentsAtOffset(self, obj: Atspi.Accessible, offset: Any, layoutMode: Any = True, useCache: Any = True) -> list[Any]: return [] - def get_objectContentsAtOffset(self, obj, offset=0, useCache=True): + def get_objectContentsAtOffset(self, obj: Atspi.Accessible, offset: Any = 0, useCache: Any = True) -> list[Any]: return [] - def previousContext(self, obj=None, offset=-1, skipSpace=False): + def previousContext(self, obj: Optional[Atspi.Accessible] = None, offset: int = -1, skipSpace: Any = False) -> Any: if not obj: obj, offset = self.getCaretContext() return obj, offset - 1 - def nextContext(self, obj=None, offset=-1, skipSpace=False): + def nextContext(self, obj: Optional[Atspi.Accessible] = None, offset: int = -1, skipSpace: Any = False) -> Any: if not obj: obj, offset = self.getCaretContext() return obj, offset + 1 - def lastContext(self, root): + def lastContext(self, root: Any) -> Any: offset = 0 text = self.queryNonEmptyText(root) if text: @@ -3791,7 +3775,7 @@ class Utilities: return root, offset - def getHyperlinkRange(self, obj): + def getHyperlinkRange(self, obj: Atspi.Accessible) -> int: """Returns the text range in parent associated with obj.""" start = AXHypertext.get_link_start_offset(obj) @@ -3803,7 +3787,7 @@ class Utilities: return start, end - def selectedChildren(self, obj): + def selectedChildren(self, obj: Atspi.Accessible) -> Any: children = AXSelection.get_selected_children(obj) if children: return children @@ -3820,17 +3804,17 @@ class Utilities: children = self.selectedChildren(children[0]) name = AXObject.get_name(obj) if not children and name: - def pred(x): + def pred(x: Atspi.Accessible) -> bool: return AXObject.get_name(x) == name children = self.findAllDescendants(obj, pred) return children - def speakSelectedCellRange(self, obj): + def speakSelectedCellRange(self, obj: Atspi.Accessible) -> bool: return False - def getSelectionContainer(self, obj): + def getSelectionContainer(self, obj: Atspi.Accessible) -> Optional[Any]: if not obj: return None @@ -3847,14 +3831,14 @@ class Utilities: } matchingRoles = rolemap.get(AXObject.get_role(obj)) - def isMatch(x): + def isMatch(x: Atspi.Accessible) -> bool: if matchingRoles and AXObject.get_role(x) not in matchingRoles: return False return AXObject.supports_selection(x) return AXObject.find_ancestor(obj, isMatch) - def selectableChildCount(self, obj): + def selectableChildCount(self, obj: Atspi.Accessible) -> int: if not AXObject.supports_selection(obj): return 0 @@ -3871,12 +3855,12 @@ class Utilities: if role not in rolemap: return AXObject.get_child_count(obj) - def isMatch(x): + def isMatch(x: Atspi.Accessible) -> bool: return AXObject.get_role(x) in rolemap.get(role) return len(self.findAllDescendants(obj, isMatch)) - def selectedChildCount(self, obj): + def selectedChildCount(self, obj: Atspi.Accessible) -> Any: if AXObject.supports_table(obj): count = AXTable.get_selected_row_count(obj) if count: @@ -3884,7 +3868,7 @@ class Utilities: return AXSelection.get_selected_child_count(obj) - def popupMenuFor(self, obj): + def popupMenuFor(self, obj: Atspi.Accessible) -> Optional[Any]: if obj is None: return None @@ -3895,10 +3879,10 @@ class Utilities: return None - def isButtonWithPopup(self, obj): + def isButtonWithPopup(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_button(obj) and AXUtilities.has_popup(obj) - def isPopupMenuForCurrentItem(self, obj): + def isPopupMenuForCurrentItem(self, obj: Atspi.Accessible) -> bool: if obj == cthulhu_state.locusOfFocus: return False @@ -3911,13 +3895,13 @@ class Utilities: return name == AXObject.get_name(cthulhu_state.locusOfFocus) - def isMenuWithNoSelectedChild(self, obj): + def isMenuWithNoSelectedChild(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_menu(obj) and not self.selectedChildCount(obj) - def isMenuButton(self, obj): + def isMenuButton(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_button(obj) and self.popupMenuFor(obj) is not None - def inMenu(self, obj=None): + def inMenu(self, obj: Optional[Atspi.Accessible] = None) -> bool: obj = obj or cthulhu_state.locusOfFocus if obj is None: return False @@ -3930,38 +3914,38 @@ class Utilities: return False - def inContextMenu(self, obj=None): + def inContextMenu(self, obj: Optional[Atspi.Accessible] = None) -> bool: obj = obj or cthulhu_state.locusOfFocus if not self.inMenu(obj): return False return AXObject.find_ancestor(obj, self.isContextMenu) is not None - def _contextMenuParentRoles(self): + def _contextMenuParentRoles(self) -> Any: return Atspi.Role.FRAME, Atspi.Role.WINDOW - def isContextMenu(self, obj): + def isContextMenu(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_menu(obj): return False return AXObject.get_role(AXObject.get_parent(obj)) in self._contextMenuParentRoles() - def isTopLevelMenu(self, obj): + def isTopLevelMenu(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_menu(obj): return False return AXObject.get_parent(obj) == self.topLevelObject(obj) - def isSingleLineAutocompleteEntry(self, obj): + def isSingleLineAutocompleteEntry(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_entry(obj): return False if not AXUtilities.supports_autocompletion(obj): return False return AXUtilities.is_single_line(obj) - def isEntryCompletionPopupItem(self, obj): + def isEntryCompletionPopupItem(self, obj: Atspi.Accessible) -> bool: return False - def getEntryForEditableComboBox(self, obj): + def getEntryForEditableComboBox(self, obj: Atspi.Accessible) -> Optional[Any]: if not AXUtilities.is_combo_box(obj): return None @@ -3971,16 +3955,16 @@ class Utilities: return None - def isEditableComboBox(self, obj): + def isEditableComboBox(self, obj: Atspi.Accessible) -> bool: return self.getEntryForEditableComboBox(obj) is not None - def isEditableDescendantOfComboBox(self, obj): + def isEditableDescendantOfComboBox(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_editable(obj): return False return AXObject.find_ancestor(obj, AXUtilities.is_combo_box) is not None - def getComboBoxValue(self, obj): + def getComboBoxValue(self, obj: Atspi.Accessible) -> Any: if not AXObject.get_child_count(obj): return self.displayedText(obj) @@ -3995,46 +3979,46 @@ class Utilities: return self.displayedText(obj) - def isPopOver(self, obj): + def isPopOver(self, obj: Atspi.Accessible) -> bool: return False - def isNonModalPopOver(self, obj): + def isNonModalPopOver(self, obj: Atspi.Accessible) -> bool: if not self.isPopOver(obj): return False return not AXUtilities.is_modal(obj) - def isUselessPanel(self, obj): + def isUselessPanel(self, obj: Atspi.Accessible) -> bool: return False - def rgbFromString(self, attributeValue): + def rgbFromString(self, attributeValue: Any) -> Any: regex = re.compile(r"rgb|[^\w,]", re.IGNORECASE) string = re.sub(regex, "", attributeValue) red, green, blue = string.split(",") return int(red), int(green), int(blue) - def isClickableElement(self, obj): + def isClickableElement(self, obj: Atspi.Accessible) -> bool: return False - def hasLongDesc(self, obj): + def hasLongDesc(self, obj: Atspi.Accessible) -> bool: return False - def hasDetails(self, obj): + def hasDetails(self, obj: Atspi.Accessible) -> bool: return False - def isDetails(self, obj): + def isDetails(self, obj: Atspi.Accessible) -> bool: return False - def detailsFor(self, obj): + def detailsFor(self, obj: Atspi.Accessible) -> list[Any]: return [] - def hasVisibleCaption(self, obj): + def hasVisibleCaption(self, obj: Atspi.Accessible) -> bool: return False - def popupType(self, obj): + def popupType(self, obj: Atspi.Accessible) -> str: return '' - def headingLevel(self, obj): + def headingLevel(self, obj: Atspi.Accessible) -> int: if not AXUtilities.is_heading(obj): return 0 @@ -4049,17 +4033,17 @@ class Utilities: return value - def hasMeaningfulToggleAction(self, obj): + def hasMeaningfulToggleAction(self, obj: Atspi.Accessible) -> Any: return AXObject.has_action(obj, "toggle") \ or AXObject.has_action(obj, object_properties.ACTION_TOGGLE) - def containingTableHeader(self, obj): + def containingTableHeader(self, obj: Atspi.Accessible) -> Any: if AXUtilities.is_table_header(obj): return obj return AXObject.find_ancestor(obj, AXUtilities.is_table_header) - def columnHeadersForCell(self, obj): + def columnHeadersForCell(self, obj: Atspi.Accessible) -> Any: result = self._columnHeadersForCell(obj) # There either are no headers, or we got all of them. if len(result) != 1: @@ -4072,7 +4056,7 @@ class Utilities: return result - def _columnHeadersForCell(self, obj): + def _columnHeadersForCell(self, obj: Atspi.Accessible) -> list[Any]: if not obj: msg = "SCRIPT UTILITIES: Attempted to get column headers for null cell" debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -4080,7 +4064,7 @@ class Utilities: return AXTable.get_column_headers(obj) - def rowHeadersForCell(self, obj): + def rowHeadersForCell(self, obj: Atspi.Accessible) -> Any: result = self._rowHeadersForCell(obj) # There either are no headers, or we got all of them. if len(result) != 1: @@ -4093,7 +4077,7 @@ class Utilities: return result - def _rowHeadersForCell(self, obj): + def _rowHeadersForCell(self, obj: Atspi.Accessible) -> list[Any]: if not obj: msg = "SCRIPT UTILITIES: Attempted to get row headers for null cell" debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -4101,39 +4085,39 @@ class Utilities: return AXTable.get_row_headers(obj) - def columnHeaderForCell(self, obj): + def columnHeaderForCell(self, obj: Atspi.Accessible) -> Any: headers = self.columnHeadersForCell(obj) if headers: return headers[0] return None - def rowHeaderForCell(self, obj): + def rowHeaderForCell(self, obj: Atspi.Accessible) -> Any: headers = self.rowHeadersForCell(obj) if headers: return headers[0] return None - def _shouldUseTableCellInterfaceForCoordinates(self): + def _shouldUseTableCellInterfaceForCoordinates(self) -> bool: return True - def coordinatesForCell(self, obj, preferAttribute=True, findCellAncestor=False): + def coordinatesForCell(self, obj: Atspi.Accessible, preferAttribute: Any = True, findCellAncestor: Any = False) -> Any: return AXTable.get_cell_coordinates( obj, prefer_attribute=preferAttribute, find_cell=findCellAncestor) - def rowAndColumnSpan(self, obj): + def rowAndColumnSpan(self, obj: Atspi.Accessible) -> int: if not AXUtilities.is_table_cell_or_header(obj): return -1, -1 return AXTable.get_cell_spans(obj, prefer_attribute=True) - def setSizeUnknown(self, obj): + def setSizeUnknown(self, obj: Atspi.Accessible) -> Any: return AXUtilities.is_indeterminate(obj) - def rowOrColumnCountUnknown(self, obj): + def rowOrColumnCountUnknown(self, obj: Atspi.Accessible) -> Any: return AXUtilities.is_indeterminate(obj) - def rowAndColumnCount(self, obj, preferAttribute=True): + def rowAndColumnCount(self, obj: Atspi.Accessible, preferAttribute: Any = True) -> int: if not AXObject.supports_table(obj): return -1, -1 @@ -4141,13 +4125,13 @@ class Utilities: cols = AXTable.get_column_count(obj, prefer_attribute=preferAttribute) return rows, cols - def _objectBoundsMightBeBogus(self, obj): + def _objectBoundsMightBeBogus(self, obj: Atspi.Accessible) -> bool: return False - def _objectMightBeBogus(self, obj): + def _objectMightBeBogus(self, obj: Atspi.Accessible) -> bool: return False - def containsPoint(self, obj, x, y, coordType, margin=2): + def containsPoint(self, obj: Atspi.Accessible, x: Any, y: Any, coordType: Any, margin: Any = 2) -> bool: if self._objectBoundsMightBeBogus(obj) \ and self.textAtPoint(obj, x, y, coordType) == ("", 0, 0): return False @@ -4175,7 +4159,7 @@ class Utilities: return False - def _boundsIncludeChildren(self, obj): + def _boundsIncludeChildren(self, obj: Atspi.Accessible) -> bool: if obj is None: return False @@ -4184,10 +4168,10 @@ class Utilities: return not (AXUtilities.is_menu(obj) or AXUtilities.is_page_tab(obj)) - def treatAsEntry(self, obj): + def treatAsEntry(self, obj: Atspi.Accessible) -> bool: return False - def _treatAsLeafNode(self, obj): + def _treatAsLeafNode(self, obj: Atspi.Accessible) -> bool: if obj is None or AXObject.is_dead(obj): return False @@ -4211,7 +4195,7 @@ class Utilities: return False - def accessibleAtPoint(self, root, x, y, coordType=None): + def accessibleAtPoint(self, root: Any, x: Any, y: Any, coordType: Any = None) -> Optional[Any]: if self.isHidden(root): return None @@ -4234,7 +4218,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return result - def descendantAtPoint(self, root, x, y, coordType=None): + def descendantAtPoint(self, root: Any, x: Any, y: Any, coordType: Any = None) -> Optional[Any]: if not root: return None @@ -4281,10 +4265,10 @@ class Utilities: return None - def _adjustPointForObj(self, obj, x, y, coordType): + def _adjustPointForObj(self, obj: Atspi.Accessible, x: Any, y: Any, coordType: Any) -> Any: return x, y - def isMultiParagraphObject(self, obj): + def isMultiParagraphObject(self, obj: Atspi.Accessible) -> bool: if not obj: return False @@ -4295,7 +4279,7 @@ class Utilities: chunks = list(filter(lambda x: x.strip(), string.split("\n\n"))) return len(chunks) > 1 - def getWordAtOffsetAdjustedForNavigation(self, obj, offset=None): + def getWordAtOffsetAdjustedForNavigation(self, obj: Atspi.Accessible, offset: Any = None) -> str: if not AXObject.supports_text(obj): return "", 0, 0 @@ -4377,7 +4361,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return word, start, end - def getWordAtOffset(self, obj, offset=None): + def getWordAtOffset(self, obj: Atspi.Accessible, offset: Any = None) -> str: if not AXObject.supports_text(obj): return "", 0, 0 @@ -4393,7 +4377,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return word, start, end - def textAtPoint(self, obj, x, y, coordType=None, boundary=None): + def textAtPoint(self, obj: Atspi.Accessible, x: Any, y: Any, coordType: Any = None, boundary: Any = None) -> str: text = self.queryNonEmptyText(obj) if not text: return "", 0, 0 @@ -4460,7 +4444,7 @@ class Utilities: return string, start, end - def visibleRows(self, obj, boundingbox): + def visibleRows(self, obj: Atspi.Accessible, boundingbox: Any) -> list[Any]: if not AXObject.supports_table(obj): return [] @@ -4507,7 +4491,7 @@ class Utilities: return rows - def getVisibleTableCells(self, obj): + def getVisibleTableCells(self, obj: Atspi.Accessible) -> list[Any]: if not AXObject.supports_table(obj): return [] @@ -4547,7 +4531,7 @@ class Utilities: return cells - def _getTableRowRange(self, obj): + def _getTableRowRange(self, obj: Atspi.Accessible) -> Any: rowCount, columnCount = self.rowAndColumnCount(obj) startIndex, endIndex = 0, columnCount if not self.isSpreadSheetCell(obj): @@ -4585,7 +4569,7 @@ class Utilities: return startIndex, endIndex - def getShowingCellsInSameRow(self, obj, forceFullRow=False): + def getShowingCellsInSameRow(self, obj: Atspi.Accessible, forceFullRow: Any = False) -> list[Any]: parent = self.getTable(obj) if not parent or not AXObject.supports_table(parent): tokens = ["SCRIPT UTILITIES: Exception querying table interface of", parent] @@ -4611,7 +4595,7 @@ class Utilities: return cells - def cellForCoordinates(self, obj, row, column, showingOnly=False): + def cellForCoordinates(self, obj: Atspi.Accessible, row: Any, column: Any, showingOnly: Any = False) -> Optional[Any]: if not AXObject.supports_table(obj): return None @@ -4624,7 +4608,7 @@ class Utilities: return None - def isLastCell(self, obj): + def isLastCell(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_table_cell(obj): return False @@ -4637,13 +4621,13 @@ class Utilities: cols = AXTable.get_column_count(table, prefer_attribute=False) return row + 1 == rows and col + 1 == cols - def isNonUniformTable(self, obj, maxRows=25, maxCols=25): + def isNonUniformTable(self, obj: Atspi.Accessible, maxRows: Any = 25, maxCols: Any = 25) -> bool: if not AXObject.supports_table(obj): return False return AXTable.is_non_uniform_table(obj, maxRows, maxCols) - def isShowingAndVisible(self, obj): + def isShowingAndVisible(self, obj: Atspi.Accessible) -> bool: if AXUtilities.is_showing(obj) and AXUtilities.is_visible(obj): return True @@ -4657,12 +4641,12 @@ class Utilities: return False - def _is_open_menu_bar_menu_role(self, obj): + def _is_open_menu_bar_menu_role(self, obj: Atspi.Accessible) -> bool: if AXObject.get_role(obj) not in self.MENU_ROLES_IN_OPEN_MENU: return False return self.isInOpenMenuBarMenu(obj) - def isZombie(self, obj): + def isZombie(self, obj: Atspi.Accessible) -> bool: index = AXObject.get_index_in_parent(obj) role = AXObject.get_role(obj) tokens = ["SCRIPT UTILITIES: ", obj, "is zombie:"] @@ -4685,7 +4669,7 @@ class Utilities: return False - def findReplicant(self, root, obj): + def findReplicant(self, root: Any, obj: Atspi.Accessible) -> Optional[Any]: tokens = ["SCRIPT UTILITIES: Searching for replicant for", obj, "in", root] debug.printTokens(debug.LEVEL_INFO, tokens, True) if not (root and obj): @@ -4694,7 +4678,7 @@ class Utilities: if AXUtilities.is_table(root) or AXUtilities.is_embedded(root): return None - def isSame(x): + def isSame(x: Atspi.Accessible) -> bool: return self.isSameObject(x, obj, comparePaths=True, ignoreNames=True) if isSame(root): @@ -4706,13 +4690,13 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return replicant - def getFunctionalChildCount(self, obj): + def getFunctionalChildCount(self, obj: Atspi.Accessible) -> Any: nodeParents = AXUtilitiesRelation.get_is_node_parent_of(obj) if nodeParents: return len(nodeParents) return AXObject.get_child_count(obj) - def getFunctionalChildren(self, obj, sibling=None): + def getFunctionalChildren(self, obj: Atspi.Accessible, sibling: Any = None) -> Any: result = AXUtilitiesRelation.get_is_node_parent_of(obj) if result: return result @@ -4722,13 +4706,13 @@ class Utilities: return self.valuesForTerm(self.termForValue(sibling)) return [x for x in AXObject.iter_children(obj)] - def getFunctionalParent(self, obj): + def getFunctionalParent(self, obj: Atspi.Accessible) -> Any: nodeChildren = AXUtilitiesRelation.get_is_node_child_of(obj) if nodeChildren: return nodeChildren[0] return AXObject.get_parent(obj) - def getPositionAndSetSize(self, obj, **args): + def getPositionAndSetSize(self, obj: Atspi.Accessible, **args: Any) -> int: if obj is None: return -1, -1 @@ -4742,7 +4726,7 @@ class Utilities: if selected: obj = selected[0] else: - def isMenu(x): + def isMenu(x: Atspi.Accessible) -> bool: return AXUtilities.is_menu(x) or AXUtilities.is_list_box(x) selected = self.selectedChildren(AXObject.find_descendant(obj, isMenu)) @@ -4760,7 +4744,7 @@ class Utilities: if len(siblings) < 100 and not AXObject.find_ancestor(obj, AXUtilities.is_combo_box): layoutRoles = [Atspi.Role.SEPARATOR, Atspi.Role.TEAROFF_MENU_ITEM] - def isNotLayoutOnly(x): + def isNotLayoutOnly(x: Atspi.Accessible) -> bool: return not (self.isZombie(x) or AXObject.get_role(x) in layoutRoles) siblings = list(filter(isNotLayoutOnly, siblings)) @@ -4777,7 +4761,7 @@ class Utilities: setSize = len(siblings) return position, setSize - def termForValue(self, obj): + def termForValue(self, obj: Atspi.Accessible) -> Optional[Any]: if not self.isDescriptionListDescription(obj): return None @@ -4786,7 +4770,7 @@ class Utilities: return obj - def valuesForTerm(self, obj): + def valuesForTerm(self, obj: Atspi.Accessible) -> list[Any]: if not self.isDescriptionListTerm(obj): return [] @@ -4798,20 +4782,20 @@ class Utilities: return values - def getValueCountForTerm(self, obj): + def getValueCountForTerm(self, obj: Atspi.Accessible) -> Any: return len(self.valuesForTerm(obj)) - def getRoleDescription(self, obj, isBraille=False): + def getRoleDescription(self, obj: Atspi.Accessible, isBraille: Any = False) -> str: return "" - def getCachedTextSelection(self, obj): + def getCachedTextSelection(self, obj: Atspi.Accessible) -> Any: textSelections = self._script.pointOfReference.get('textSelections', {}) start, end, string = textSelections.get(hash(obj), (0, 0, '')) tokens = ["SCRIPT UTILITIES: Cached selection for", obj, f"is '{string}' ({start}, {end})"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return start, end, string - def updateCachedTextSelection(self, obj): + def updateCachedTextSelection(self, obj: Atspi.Accessible) -> Any: if not AXObject.supports_text(obj): tokens = ["SCRIPT UTILITIES:", obj, "doesn't implement AtspiText"] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -4843,15 +4827,14 @@ class Utilities: textSelections[hash(obj)] = start, end, string self._script.pointOfReference['textSelections'] = textSelections - def _getSingleSelectionText(self, obj): + def _getSingleSelectionText(self, obj: Atspi.Accessible) -> Any: # NOTE: Does not handle multiple non-contiguous selections. string, start, end = AXText.get_selected_text(obj) if string: string = self.expandEOCs(obj, start, end) return string, start, end - @staticmethod - def onClipboardContentsChanged(*args): + def onClipboardContentsChanged(self, *args: Any) -> None: script = cthulhu_state.activeScript if not script: return @@ -4864,7 +4847,7 @@ class Utilities: Utilities._last_clipboard_update = time.time() script.onClipboardContentsChanged(*args) - def connectToClipboard(self): + def connectToClipboard(self) -> None: if self._clipboardHandlerId is not None: return @@ -4872,65 +4855,65 @@ class Utilities: self._clipboardHandlerId = clipboard.connect( 'owner-change', self.onClipboardContentsChanged) - def disconnectFromClipboard(self): + def disconnectFromClipboard(self) -> None: if self._clipboardHandlerId is None: return clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False)) clipboard.disconnect(self._clipboardHandlerId) - def getClipboardContents(self): + def getClipboardContents(self) -> Any: clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False)) return clipboard.wait_for_text() - def setClipboardText(self, text): + def setClipboardText(self, text: Any) -> bool: clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False)) clipboard.set_text(text, -1) - def appendTextToClipboard(self, text): + def appendTextToClipboard(self, text: Any) -> bool: clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False)) clipboard.request_text(self._appendTextToClipboardCallback, text) - def _appendTextToClipboardCallback(self, clipboard, text, newText, separator="\n"): + def _appendTextToClipboardCallback(self, clipboard: Any, text: Any, newText: Any, separator: Any = "\n") -> bool: text = text.rstrip("\n") text = f"{text}{separator}{newText}" clipboard.set_text(text, -1) - def lastInputEventCameFromThisApp(self): + def lastInputEventCameFromThisApp(self) -> bool: if not isinstance(cthulhu_state.lastInputEvent, input_event.KeyboardEvent): return False event = cthulhu_state.lastNonModifierKeyEvent return event and event.isFromApplication(self._script.app) - def lastInputEventWasPrintableKey(self): + def lastInputEventWasPrintableKey(self) -> Any: return input_event_manager.get_manager().last_event_was_printable_key() - def lastInputEventWasCommand(self): + def lastInputEventWasCommand(self) -> Any: return input_event_manager.get_manager().last_event_was_command() - def lastInputEventWasPageSwitch(self): + def lastInputEventWasPageSwitch(self) -> Any: return input_event_manager.get_manager().last_event_was_page_switch() - def lastInputEventWasUnmodifiedArrow(self): + def lastInputEventWasUnmodifiedArrow(self) -> Any: return input_event_manager.get_manager().last_event_was_unmodified_arrow() - def lastInputEventWasCaretNav(self): + def lastInputEventWasCaretNav(self) -> Any: return input_event_manager.get_manager().last_event_was_caret_navigation() - def lastInputEventWasCharNav(self): + def lastInputEventWasCharNav(self) -> Any: return input_event_manager.get_manager().last_event_was_character_navigation() - def lastInputEventWasWordNav(self): + def lastInputEventWasWordNav(self) -> Any: return input_event_manager.get_manager().last_event_was_word_navigation() - def lastInputEventWasPrevWordNav(self): + def lastInputEventWasPrevWordNav(self) -> Any: return input_event_manager.get_manager().last_event_was_previous_word_navigation() - def lastInputEventWasNextWordNav(self): + def lastInputEventWasNextWordNav(self) -> Any: return input_event_manager.get_manager().last_event_was_next_word_navigation() - def lastInputEventWasLineNav(self): + def lastInputEventWasLineNav(self) -> bool: if not input_event_manager.get_manager().last_event_was_line_navigation(): return False @@ -4939,10 +4922,10 @@ class Utilities: return True - def lastInputEventWasLineBoundaryNav(self): + def lastInputEventWasLineBoundaryNav(self) -> Any: return input_event_manager.get_manager().last_event_was_line_boundary_navigation() - def lastInputEventWasPageNav(self): + def lastInputEventWasPageNav(self) -> bool: if not input_event_manager.get_manager().last_event_was_page_navigation(): return False @@ -4951,58 +4934,58 @@ class Utilities: return True - def lastInputEventWasFileBoundaryNav(self): + def lastInputEventWasFileBoundaryNav(self) -> Any: return input_event_manager.get_manager().last_event_was_file_boundary_navigation() - def lastInputEventWasCaretNavWithSelection(self): + def lastInputEventWasCaretNavWithSelection(self) -> Any: return input_event_manager.get_manager().last_event_was_caret_selection() - def lastInputEventWasUndo(self): + def lastInputEventWasUndo(self) -> Any: return input_event_manager.get_manager().last_event_was_undo() - def lastInputEventWasRedo(self): + def lastInputEventWasRedo(self) -> Any: return input_event_manager.get_manager().last_event_was_redo() - def lastInputEventWasCut(self): + def lastInputEventWasCut(self) -> Any: return input_event_manager.get_manager().last_event_was_cut() - def lastInputEventWasCopy(self): + def lastInputEventWasCopy(self) -> Any: return input_event_manager.get_manager().last_event_was_copy() - def lastInputEventWasPaste(self): + def lastInputEventWasPaste(self) -> Any: return input_event_manager.get_manager().last_event_was_paste() - def lastInputEventWasSelectAll(self): + def lastInputEventWasSelectAll(self) -> Any: return input_event_manager.get_manager().last_event_was_select_all() - def lastInputEventWasDelete(self): + def lastInputEventWasDelete(self) -> Any: return input_event_manager.get_manager().last_event_was_delete() - def lastInputEventWasTab(self): + def lastInputEventWasTab(self) -> Any: return input_event_manager.get_manager().last_event_was_tab() - def lastInputEventWasMouseButton(self): + def lastInputEventWasMouseButton(self) -> Any: return input_event_manager.get_manager().last_event_was_mouse_button() - def lastInputEventWasPrimaryMouseClick(self): + def lastInputEventWasPrimaryMouseClick(self) -> Any: return input_event_manager.get_manager().last_event_was_primary_click() - def lastInputEventWasMiddleMouseClick(self): + def lastInputEventWasMiddleMouseClick(self) -> Any: return input_event_manager.get_manager().last_event_was_middle_click() - def lastInputEventWasSecondaryMouseClick(self): + def lastInputEventWasSecondaryMouseClick(self) -> Any: return input_event_manager.get_manager().last_event_was_secondary_click() - def lastInputEventWasPrimaryMouseRelease(self): + def lastInputEventWasPrimaryMouseRelease(self) -> Any: return input_event_manager.get_manager().last_event_was_primary_release() - def lastInputEventWasMiddleMouseRelease(self): + def lastInputEventWasMiddleMouseRelease(self) -> Any: return input_event_manager.get_manager().last_event_was_middle_release() - def lastInputEventWasSecondaryMouseRelease(self): + def lastInputEventWasSecondaryMouseRelease(self) -> Any: return input_event_manager.get_manager().last_event_was_secondary_release() - def lastInputEventWasTableSort(self, delta=0.5): + def lastInputEventWasTableSort(self, delta: Any = 0.5) -> bool: if not input_event_manager.get_manager().last_event_was_table_sort(): return False @@ -5029,7 +5012,7 @@ class Utilities: return AXUtilities.is_table_header(cthulhu_state.locusOfFocus) - def isPresentableExpandedChangedEvent(self, event): + def isPresentableExpandedChangedEvent(self, event: Any) -> bool: if self.isSameObject(event.source, cthulhu_state.locusOfFocus): return True @@ -5041,7 +5024,7 @@ class Utilities: return False - def isPresentableTextChangedEventForLocusOfFocus(self, event): + def isPresentableTextChangedEventForLocusOfFocus(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:") \ and not event.type.startswith("object:text-attributes-changed"): return False @@ -5073,7 +5056,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return False - def isBackSpaceCommandTextDeletionEvent(self, event): + def isBackSpaceCommandTextDeletionEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:delete"): return False @@ -5086,13 +5069,13 @@ class Utilities: return False - def isDeleteCommandTextDeletionEvent(self, event): + def isDeleteCommandTextDeletionEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:delete"): return False return self.lastInputEventWasDelete() - def isUndoCommandTextDeletionEvent(self, event): + def isUndoCommandTextDeletionEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:delete"): return False @@ -5102,7 +5085,7 @@ class Utilities: start, end, string = self.getCachedTextSelection(event.source) return not string - def isSelectedTextDeletionEvent(self, event): + def isSelectedTextDeletionEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:delete"): return False @@ -5112,7 +5095,7 @@ class Utilities: start, end, string = self.getCachedTextSelection(event.source) return string and string.strip() == event.any_data.strip() - def isSelectedTextInsertionEvent(self, event): + def isSelectedTextInsertionEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:insert"): return False @@ -5120,7 +5103,7 @@ class Utilities: start, end, string = self.getCachedTextSelection(event.source) return string and string == event.any_data and start == event.detail1 - def isSelectedTextRestoredEvent(self, event): + def isSelectedTextRestoredEvent(self, event: Any) -> bool: if not self.lastInputEventWasUndo(): return False @@ -5129,13 +5112,13 @@ class Utilities: return False - def isMiddleMouseButtonTextInsertionEvent(self, event): + def isMiddleMouseButtonTextInsertionEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:insert"): return False return self.lastInputEventWasMiddleMouseClick() - def isEchoableTextInsertionEvent(self, event): + def isEchoableTextInsertionEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:insert"): return False @@ -5155,12 +5138,12 @@ class Utilities: return False - def isEditableTextArea(self, obj): + def isEditableTextArea(self, obj: Atspi.Accessible) -> bool: if not self.isTextArea(obj): return False return AXUtilities.is_editable(obj) - def isClipboardTextChangedEvent(self, event): + def isClipboardTextChangedEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed"): return False @@ -5189,14 +5172,14 @@ class Utilities: return False - def _isParagraphClipboardEvent(self, event, contents): + def _isParagraphClipboardEvent(self, event: Any, contents: Any) -> bool: # NOTE: Paragraph-per-object toolkits can emit per-paragraph events. if "\n" in contents and event.any_data.rstrip() in contents: return True return False - def objectContentsAreInClipboard(self, obj=None): + def objectContentsAreInClipboard(self, obj: Optional[Atspi.Accessible] = None) -> bool: obj = obj or cthulhu_state.locusOfFocus if not obj or AXObject.is_dead(obj): return False @@ -5215,13 +5198,13 @@ class Utilities: return obj and AXObject.get_name(obj) in contents - def clearCachedCommandState(self): + def clearCachedCommandState(self) -> bool: self._script.pointOfReference['undo'] = False self._script.pointOfReference['redo'] = False self._script.pointOfReference['paste'] = False self._script.pointOfReference['last-selection-message'] = '' - def handleUndoTextEvent(self, event): + def handleUndoTextEvent(self, event: Any) -> bool: if self.lastInputEventWasUndo(): if not self._script.pointOfReference.get('undo'): self._script.presentMessage(messages.UNDO) @@ -5238,7 +5221,7 @@ class Utilities: return False - def handleUndoLocusOfFocusChange(self): + def handleUndoLocusOfFocusChange(self) -> bool: if self._locusOfFocusIsTopLevelObject(): return False @@ -5256,7 +5239,7 @@ class Utilities: return False - def handlePasteLocusOfFocusChange(self): + def handlePasteLocusOfFocusChange(self) -> bool: if self._locusOfFocusIsTopLevelObject(): return False @@ -5269,7 +5252,7 @@ class Utilities: return False - def eventIsUserTriggered(self, event): + def eventIsUserTriggered(self, event: Any) -> bool: if not cthulhu_state.lastInputEvent: msg = "SCRIPT UTILITIES: Not user triggered: No last input event." debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -5288,21 +5271,21 @@ class Utilities: return True - def isKeyGrabEvent(self, event): + def isKeyGrabEvent(self, event: Any) -> bool: """ Returns True if this event appears to be a side-effect of an X11 key grab. """ if not isinstance(cthulhu_state.lastInputEvent, input_event.KeyboardEvent): return False return cthulhu_state.lastInputEvent.didConsume() and not cthulhu_state.openingDialog - def presentFocusChangeReason(self): + def presentFocusChangeReason(self) -> bool: if self.handleUndoLocusOfFocusChange(): return True if self.handlePasteLocusOfFocusChange(): return True return False - def allItemsSelected(self, obj): + def allItemsSelected(self, obj: Atspi.Accessible) -> bool: if not AXObject.supports_selection(obj): return False @@ -5344,7 +5327,7 @@ class Utilities: return False - def handleContainerSelectionChange(self, obj): + def handleContainerSelectionChange(self, obj: Atspi.Accessible) -> bool: allAlreadySelected = self._script.pointOfReference.get('allItemsSelected') allCurrentlySelected = self.allItemsSelected(obj) if allAlreadySelected and allCurrentlySelected: @@ -5358,7 +5341,7 @@ class Utilities: return False - def handleTextSelectionChange(self, obj, speakMessage=True): + def handleTextSelectionChange(self, obj: Atspi.Accessible, speakMessage: Any = True) -> bool: # Note: This guesswork to figure out what actually changed with respect # to text selection will get eliminated once the new text-selection API # is added to ATK and implemented by the toolkits. (BGO 638378) @@ -5427,14 +5410,14 @@ class Utilities: return True - def _getCtrlShiftSelectionsStrings(self): + def _getCtrlShiftSelectionsStrings(self) -> Any: """Hacky and to-be-obsoleted method.""" return [messages.PARAGRAPH_SELECTED_DOWN, messages.PARAGRAPH_UNSELECTED_DOWN, messages.PARAGRAPH_SELECTED_UP, messages.PARAGRAPH_UNSELECTED_UP] - def _speakTextSelectionState(self, nSelections): + def _speakTextSelectionState(self, nSelections: Any) -> bool: """Hacky and to-be-obsoleted method.""" if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): @@ -5498,7 +5481,7 @@ class Utilities: return True - def shouldInterruptForLocusOfFocusChange(self, oldLocusOfFocus, newLocusOfFocus, event=None): + def shouldInterruptForLocusOfFocusChange(self, oldLocusOfFocus: Any, newLocusOfFocus: Any, event: Any = None) -> bool: msg = "SCRIPT UTILITIES: Not interrupting for locusOfFocus change: " if event is None: msg += "event is None" @@ -5536,7 +5519,7 @@ class Utilities: return False return True - def stringsAreRedundant(self, str1, str2, threshold=0.5): + def stringsAreRedundant(self, str1: Any, str2: Any, threshold: Any = 0.5) -> bool: if not (str1 and str2): return False diff --git a/src/cthulhu/scripts/terminal/script_utilities.py b/src/cthulhu/scripts/terminal/script_utilities.py index f59ef3c..b61d9f7 100644 --- a/src/cthulhu/scripts/terminal/script_utilities.py +++ b/src/cthulhu/scripts/terminal/script_utilities.py @@ -36,6 +36,7 @@ from gi.repository import Atspi import re from cthulhu import debug +from cthulhu import cthulhu from cthulhu import keybindings from cthulhu import cthulhu_state from cthulhu import script_utilities diff --git a/src/cthulhu/signal_manager.py b/src/cthulhu/signal_manager.py index 51e61c2..762e3a6 100644 --- a/src/cthulhu/signal_manager.py +++ b/src/cthulhu/signal_manager.py @@ -25,19 +25,27 @@ import gi from gi.repository import GObject +from typing import Optional, Any, Callable, Tuple from cthulhu import resource_manager -class SignalManager(): - def __init__(self, app): - self.app = app - self.resourceManager = self.app.getResourceManager() +class SignalManager: + def __init__(self, app: Any) -> None: # app is CthulhuApp instance + self.app: Any = app + self.resourceManager: Any = self.app.getResourceManager() # ResourceManager - def registerSignal(self, signalName, signalFlag = GObject.SignalFlags.RUN_LAST, closure = GObject.TYPE_NONE, accumulator=(), contextName = None): + def registerSignal( + self, + signalName: str, + signalFlag: GObject.SignalFlags = GObject.SignalFlags.RUN_LAST, + closure: Any = GObject.TYPE_NONE, + accumulator: Tuple = (), + contextName: Optional[str] = None + ) -> bool: # register signal ok = False if not self.signalExist(signalName): - GObject.signal_new(signalName, self.app, signalFlag, closure,accumulator) + GObject.signal_new(signalName, self.app, signalFlag, closure, accumulator) ok = True resourceContext = self.resourceManager.getResourceContext(contextName) if resourceContext: @@ -45,9 +53,17 @@ class SignalManager(): resourceContext.addSignal(signalName, resourceEntry) return ok - def signalExist(self, signalName): + def signalExist(self, signalName: str) -> bool: return GObject.signal_lookup(signalName, self.app) != 0 - def connectSignal(self, signalName, function, profile, param = None, contextName = None): + + def connectSignal( + self, + signalName: str, + function: Callable, + profile: Any, + param: Optional[Any] = None, + contextName: Optional[str] = None + ) -> Optional[int]: signalID = None try: if self.signalExist(signalName): @@ -63,7 +79,8 @@ class SignalManager(): print(e) return signalID - def disconnectSignalByFunction(self, function, contextName = None): + + def disconnectSignalByFunction(self, function: Callable, contextName: Optional[str] = None) -> bool: ok = False try: self.app.disconnect_by_func(function) @@ -74,7 +91,8 @@ class SignalManager(): if resourceContext: resourceContext.removeSubscriptionByFunction(function) return ok - def emitSignal(self, signalName, *args): + + def emitSignal(self, signalName: str, *args: Any) -> None: # emit a signal with optional arguments try: self.app.emit(signalName, *args) diff --git a/src/cthulhu/sound.py b/src/cthulhu/sound.py index a427d10..3114799 100644 --- a/src/cthulhu/sound.py +++ b/src/cthulhu/sound.py @@ -33,12 +33,13 @@ __license__ = "LGPL" import gi from gi.repository import GLib +from typing import Optional, Any try: gi.require_version('Gst', '1.0') from gi.repository import Gst except Exception: - _gstreamerAvailable = False + _gstreamerAvailable: bool = False else: _gstreamerAvailable, args = Gst.init_check(None) @@ -48,12 +49,12 @@ from .sound_generator import Icon, Tone class Player: """Plays Icons and Tones.""" - def __init__(self): - self._initialized = False - self._source = None - self._sink = None - self._player = None - self._pipeline = None + def __init__(self) -> None: + self._initialized: bool = False + self._source: Optional[Any] = None # Optional[Gst.Element] + self._sink: Optional[Any] = None # Optional[Gst.Element] + self._player: Optional[Any] = None # Optional[Gst.Element] + self._pipeline: Optional[Any] = None # Optional[Gst.Pipeline] if not _gstreamerAvailable: msg = 'SOUND ERROR: Gstreamer is not available' @@ -62,7 +63,7 @@ class Player: self.init() - def _onPlayerMessage(self, bus, message): + def _onPlayerMessage(self, bus: Any, message: Any) -> None: # bus: Gst.Bus, message: Gst.Message if message.type == Gst.MessageType.EOS: self._player.set_state(Gst.State.NULL) elif message.type == Gst.MessageType.ERROR: @@ -71,7 +72,7 @@ class Player: msg = f'SOUND ERROR: {error}' debug.printMessage(debug.LEVEL_INFO, msg, True) - def _onPipelineMessage(self, bus, message): + def _onPipelineMessage(self, bus: Any, message: Any) -> None: # bus: Gst.Bus, message: Gst.Message if message.type == Gst.MessageType.EOS: self._pipeline.set_state(Gst.State.NULL) elif message.type == Gst.MessageType.ERROR: @@ -80,11 +81,11 @@ class Player: msg = f'SOUND ERROR: {error}' debug.printMessage(debug.LEVEL_INFO, msg, True) - def _onTimeout(self, element): + def _onTimeout(self, element: Any) -> bool: # element: Gst.Element element.set_state(Gst.State.NULL) return False - def _playIcon(self, icon, interrupt=True): + def _playIcon(self, icon: Icon, interrupt: bool = True) -> None: """Plays a sound icon, interrupting the current play first unless specified.""" if interrupt: @@ -93,7 +94,7 @@ class Player: self._player.set_property('uri', f'file://{icon.path}') self._player.set_state(Gst.State.PLAYING) - def _playIconAndWait(self, icon, interrupt=True, timeout_seconds=10): + def _playIconAndWait(self, icon: Icon, interrupt: bool = True, timeout_seconds: Optional[int] = 10) -> bool: """Plays a sound icon and waits for completion.""" if interrupt: @@ -123,7 +124,7 @@ class Player: self._player.set_state(Gst.State.NULL) return message is not None and message.type == Gst.MessageType.EOS - def _playTone(self, tone, interrupt=True): + def _playTone(self, tone: Tone, interrupt: bool = True) -> None: """Plays a tone, interrupting the current play first unless specified.""" if interrupt: @@ -136,7 +137,7 @@ class Player: duration = int(1000 * tone.duration) GLib.timeout_add(duration, self._onTimeout, self._pipeline) - def init(self): + def init(self) -> None: """(Re)Initializes the Player.""" if self._initialized: