diff --git a/distro-packages/Slint/README b/distro-packages/Slint/README index 319b02f..09beca3 100644 --- a/distro-packages/Slint/README +++ b/distro-packages/Slint/README @@ -17,7 +17,6 @@ This package requires the following packages, all available from SlackBuilds.org - at-spi2-core - brltty - gobject-introspection -- gsettings-desktop-schemas - gstreamer - gst-plugins-base - gst-plugins-good diff --git a/distro-packages/Slint/cthulhu-info b/distro-packages/Slint/cthulhu-info index 7f30753..bdafddb 100644 --- a/distro-packages/Slint/cthulhu-info +++ b/distro-packages/Slint/cthulhu-info @@ -5,6 +5,6 @@ DOWNLOAD="https://git.stormux.org/storm/cthulhu.git" MD5SUM="SKIP" DOWNLOAD_x86_64="" MD5SUM_x86_64="" -REQUIRES="at-spi2-core brltty gobject-introspection gsettings-desktop-schemas gstreamer gst-plugins-base gst-plugins-good gtk3 liblouis libwnck3 python3-atspi python3-cairo python3-gobject python3-setproctitle speech-dispatcher" +REQUIRES="at-spi2-core brltty gobject-introspection gstreamer gst-plugins-base gst-plugins-good gtk3 liblouis libwnck3 python3-atspi python3-cairo python3-gobject python3-setproctitle speech-dispatcher" MAINTAINER="Storm Dragon" EMAIL="storm_dragon@stormux.org" diff --git a/docs/man/cthulhu.1 b/docs/man/cthulhu.1 index d080880..1d9bfa5 100644 --- a/docs/man/cthulhu.1 +++ b/docs/man/cthulhu.1 @@ -322,17 +322,15 @@ originated as a community effort led by the Sun Microsystems Inc. Accessibility Program Office and with contributions from many community members. .SH SEE ALSO For more information please visit -.B cthulhu -wiki at -.UR http://live.gnome.org/Cthulhu - +.B Cthulhu +at +.UR https://git.stormux.org/storm/cthulhu + .UE .P The -.B cthulhu -mailing list -.UR http://mail.gnome.org/mailman/listinfo/cthulhu-list - -To post a message to all -.B cthulhu -list, send a email to https://groups.io/g/stormux +.B Stormux +community list is available at +.UR https://groups.io/g/stormux + +.UE diff --git a/src/cthulhu/backends/toml_backend.py b/src/cthulhu/backends/toml_backend.py index d8ca97e..98248ed 100644 --- a/src/cthulhu/backends/toml_backend.py +++ b/src/cthulhu/backends/toml_backend.py @@ -109,6 +109,111 @@ class Backend: if key not in targetTable: targetTable[key] = newValue + def _legacyProfileValue(self, profileTable, sectionName, legacyKey): + section = profileTable.get(sectionName) + if not isinstance(section, dict): + return None + return section.get(legacyKey) + + def _normalizeLegacyValue(self, currentKey, legacyValue): + if currentKey == 'keyboardLayout' and isinstance(legacyValue, str): + layout = legacyValue.strip().lower() + if layout == 'desktop': + return settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP + if layout == 'laptop': + return settings.GENERAL_KEYBOARD_LAYOUT_LAPTOP + return legacyValue + + def _normalizeLegacyProfile(self, profileName, profileTable): + if not isinstance(profileTable, dict): + return profileTable + + profileSettings = {} + legacySections = { + 'metadata', + 'plugins', + 'ai-assistant', + 'ocr', + } + + for key, value in profileTable.items(): + if key in legacySections: + continue + if key == 'keybindings': + if isinstance(value, dict): + keybindings = dict(value) + keybindings.pop('keyboard-layout', None) + keybindings.pop('desktop-modifier-keys', None) + keybindings.pop('laptop-modifier-keys', None) + if keybindings: + profileSettings[key] = keybindings + continue + profileSettings[key] = value + + displayName = self._legacyProfileValue(profileTable, 'metadata', 'display-name') + internalName = self._legacyProfileValue(profileTable, 'metadata', 'internal-name') + if 'profile' not in profileSettings and (displayName or internalName): + profileSettings['profile'] = [ + displayName or str(profileName).title(), + internalName or profileName, + ] + + legacyKeyMap = { + ('keybindings', 'keyboard-layout'): 'keyboardLayout', + ('keybindings', 'desktop-modifier-keys'): 'cthulhuModifierKeys', + ('keybindings', 'laptop-modifier-keys'): 'cthulhuModifierKeys', + ('plugins', 'active-plugins'): 'activePlugins', + ('plugins', 'plugin-sources'): 'pluginSources', + ('ai-assistant', 'enabled'): 'aiAssistantEnabled', + ('ai-assistant', 'provider'): 'aiProvider', + ('ai-assistant', 'api-key-file'): 'aiApiKeyFile', + ('ai-assistant', 'ollama-model'): 'aiOllamaModel', + ('ai-assistant', 'ollama-endpoint'): 'aiOllamaEndpoint', + ('ai-assistant', 'confirmation-required'): 'aiConfirmationRequired', + ('ai-assistant', 'action-timeout'): 'aiActionTimeout', + ('ai-assistant', 'screenshot-quality'): 'aiScreenshotQuality', + ('ai-assistant', 'max-context-length'): 'aiMaxContextLength', + ('ocr', 'language-code'): 'ocrLanguageCode', + ('ocr', 'scale-factor'): 'ocrScaleFactor', + ('ocr', 'grayscale-image'): 'ocrGrayscaleImg', + ('ocr', 'invert-image'): 'ocrInvertImg', + ('ocr', 'black-white-image'): 'ocrBlackWhiteImg', + ('ocr', 'black-white-threshold'): 'ocrBlackWhiteImgValue', + ('ocr', 'color-calculation'): 'ocrColorCalculation', + ('ocr', 'color-calculation-max'): 'ocrColorCalculationMax', + ('ocr', 'copy-to-clipboard'): 'ocrCopyToClipboard', + } + + for (sectionName, legacyKey), currentKey in legacyKeyMap.items(): + if currentKey in profileSettings: + continue + legacyValue = self._legacyProfileValue(profileTable, sectionName, legacyKey) + if legacyValue is not None: + profileSettings[currentKey] = self._normalizeLegacyValue(currentKey, legacyValue) + + return profileSettings + + def _normalizeProfiles(self, profiles): + if not isinstance(profiles, dict): + return {} + return { + profileName: self._normalizeLegacyProfile(profileName, profileTable) + for profileName, profileTable in profiles.items() + } + + def _normalizeProfilesDocument(self, prefsDoc): + profiles = prefsDoc.get('profiles') + if not isinstance(profiles, dict): + return + + for profileName in list(profiles.keys()): + profileTable = profiles[profileName] + normalizedProfile = self._normalizeLegacyProfile(profileName, profileTable) + profiles[profileName] = self._stripNone(normalizedProfile) + + if 'format-version' in prefsDoc: + del prefsDoc['format-version'] + def saveDefaultSettings(self, general, pronunciations, keybindings): """ Save default settings for all the properties from cthulhu.settings. """ @@ -167,6 +272,7 @@ class Backend: general = self._stripNone(general) prefsDoc = self._readDocument(self.settingsFile) + self._normalizeProfilesDocument(prefsDoc) profiles = prefsDoc.get('profiles') if profiles is None or not isinstance(profiles, dict): prefsDoc['profiles'] = {} @@ -192,7 +298,7 @@ class Backend: self.general = dict(prefsDoc.get('general', {})) self.pronunciations = dict(prefsDoc.get('pronunciations', {})) self.keybindings = dict(prefsDoc.get('keybindings', {})) - self.profiles = dict(prefsDoc.get('profiles', {})) + self.profiles = self._normalizeProfiles(dict(prefsDoc.get('profiles', {}))) except Exception: return diff --git a/src/cthulhu/braille_generator.py b/src/cthulhu/braille_generator.py index 75a2aaf..43d3151 100644 --- a/src/cthulhu/braille_generator.py +++ b/src/cthulhu/braille_generator.py @@ -178,7 +178,7 @@ class BrailleGenerator(generator.Generator): Atspi.Role.EXTENDED, Atspi.Role.LINK] - # egg-list-box, e.g. privacy panel in gnome-control-center + # egg-list-box-style containers can expose selected panels as list items. if AXUtilities.is_list_box(AXObject.get_parent(obj)): doNotPresent.append(AXObject.get_role(obj)) diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index baf180f..006ece5 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -42,7 +42,6 @@ from . import dbus_service if TYPE_CHECKING: from types import FrameType - from gi.repository.Gio import Settings as GSettings from gi.repository import Gtk from .settings_manager import SettingsManager @@ -250,12 +249,6 @@ from gi.repository import Atspi from gi.repository import Gdk from gi.repository import GObject -try: - from gi.repository.Gio import Settings - a11yAppSettings: Optional[GSettings] = Settings(schema_id='org.gnome.desktop.a11y.applications') -except Exception: - a11yAppSettings = None - from . import braille from . import debug from . import event_manager @@ -299,15 +292,6 @@ from . import resource_manager # Old global variables removed - now using cthulhuApp.* instead -def onEnabledChanged(gsetting: GSettings, key: str) -> None: - try: - enabled: bool = gsetting.get_boolean(key) - except Exception: - return - - if key == 'screen-reader-enabled' and not enabled: - shutdown() - EXIT_CODE_HANG: int = 50 # The user-settings module (see loadUserSettings). @@ -651,12 +635,6 @@ def init() -> bool: signal.alarm(0) _initialized = True - # In theory, we can do this through dbus. In practice, it fails to - # work sometimes. Until we know why, we need to leave this as-is - # so that we respond when gnome-control-center is used to stop Cthulhu. - if a11yAppSettings: - a11yAppSettings.connect('changed', onEnabledChanged) - debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Initialized', True) return True diff --git a/src/cthulhu/cthulhu_gui_prefs.py b/src/cthulhu/cthulhu_gui_prefs.py index 6ecfa54..8d86621 100644 --- a/src/cthulhu/cthulhu_gui_prefs.py +++ b/src/cthulhu/cthulhu_gui_prefs.py @@ -4984,12 +4984,10 @@ print(json.dumps(result)) def applyButtonClicked(self, widget): """Signal handler for the "clicked" signal for the applyButton GtkButton widget. The user has clicked the Apply button. - Write out the users preferences. If GNOME accessibility hadn't - previously been enabled, warn the user that they will need to - log out. Shut down any active speech servers that were started. - Reload the users preferences to get the new speech, braille and - key echo value to take effect. Do not dismiss the configuration - window. + Write out the users preferences. Shut down any active speech servers + that were started. Reload the users preferences to get the new + speech, braille and key echo value to take effect. Do not dismiss + the configuration window. Arguments: - widget: the component that generated the signal. @@ -5042,11 +5040,10 @@ print(json.dumps(result)) def okButtonClicked(self, widget=None): """Signal handler for the "clicked" signal for the okButton GtkButton widget. The user has clicked the OK button. - Write out the users preferences. If GNOME accessibility hadn't - previously been enabled, warn the user that they will need to - log out. Shut down any active speech servers that were started. - Reload the users preferences to get the new speech, braille and - key echo value to take effect. Hide the configuration window. + Write out the users preferences. Shut down any active speech servers + that were started. Reload the users preferences to get the new + speech, braille and key echo value to take effect. Hide the + configuration window. Arguments: - widget: the component that generated the signal. diff --git a/src/cthulhu/scripts/default.py b/src/cthulhu/scripts/default.py index 45dffa8..2c6b037 100644 --- a/src/cthulhu/scripts/default.py +++ b/src/cthulhu/scripts/default.py @@ -1525,6 +1525,106 @@ class Script(script.Script): for character in itemString: self.speakCharacter(character) + def _diagnostic_object_summary(self, obj): + if obj is None: + return "None" + + try: + if AXObject.is_dead(obj): + return "dead" + except Exception: + pass + + try: + name = AXObject.get_name(obj) or "" + except Exception: + name = "" + + try: + role = AXObject.get_role_name(obj) or "" + except Exception: + role = "" + + try: + pid = AXObject.get_process_id(obj) + except Exception: + pid = "" + + return f"name={name!r}, role={role!r}, pid={pid}" + + def _diagnostic_callable_value(self, obj, methodName): + method = getattr(obj, methodName, None) + if not callable(method): + return "" + try: + return method() + except Exception as error: + return f"" + + @dbus_service.command + def getDiagnosticState(self, script=None, event=None, notify_user=True) -> str: + """Dumps runtime state useful for diagnosing sluggish web-app behavior.""" + + app = cthulhu.cthulhuApp + activeScript = cthulhu_state.activeScript + eventManager = getattr(app, "eventManager", None) + compositor = getattr(app, "compositorStateAdapter", None) + inputManager = getattr(eventManager, "_inputEventManager", None) + + lines = [ + f"timestamp={time.strftime('%Y-%m-%d %H:%M:%S')}", + f"default-script={self.__class__.__module__}.{self.__class__.__name__}", + f"active-script={activeScript.__class__.__module__}.{activeScript.__class__.__name__}" if activeScript else "active-script=None", + f"active-window={self._diagnostic_object_summary(cthulhu_state.activeWindow)}", + f"locus-of-focus={self._diagnostic_object_summary(cthulhu_state.locusOfFocus)}", + f"pending-self-hosted-focus={self._diagnostic_object_summary(getattr(cthulhu_state, 'pendingSelfHostedFocus', None))}", + ] + + if eventManager is not None: + eventQueue = getattr(eventManager, "_eventQueue", None) + try: + queueSize = eventQueue.qsize() if eventQueue is not None else "" + except Exception as error: + queueSize = f"" + prioritizedEvent = getattr(eventManager, "_prioritizedEvent", None) + lines.extend([ + f"event-manager-active={getattr(eventManager, '_active', '')}", + f"event-queue-size={queueSize}", + f"events-suspended={getattr(eventManager, '_eventsSuspended', '')}", + f"churn-suppressed={getattr(eventManager, '_churnSuppressed', '')}", + f"state-pause-atspi-churn={cthulhu_state.pauseAtspiChurn}", + f"prioritized-context-token={getattr(eventManager, '_prioritizedContextToken', None)}", + f"state-prioritized-context-token={cthulhu_state.prioritizedDesktopContextToken}", + f"prioritized-event-type={getattr(prioritizedEvent, 'type', None)}", + f"gidle-id={getattr(eventManager, '_gidleId', '')}", + f"prioritized-idle-id={getattr(eventManager, '_prioritizedIdleId', '')}", + ]) + + if inputManager is not None: + lines.extend([ + f"key-handling-active={getattr(eventManager, '_keyHandlingActive', '')}", + f"input-manager-device={getattr(inputManager, '_device', None)}", + f"input-manager-watcher={getattr(inputManager, '_keyWatcher', None)}", + ]) + + if compositor is not None: + snapshot = self._diagnostic_callable_value(compositor, "get_snapshot") + lines.append(f"compositor-snapshot={snapshot}") + + if activeScript is not None: + lines.extend([ + f"active-script-focus-mode={self._diagnostic_callable_value(activeScript, 'inFocusMode')}", + f"active-script-focus-sticky={self._diagnostic_callable_value(activeScript, 'focusModeIsSticky')}", + f"active-script-browse-sticky={self._diagnostic_callable_value(activeScript, 'browseModeIsSticky')}", + f"active-script-structural-navigation={getattr(getattr(activeScript, 'structuralNavigation', None), 'enabled', '')}", + ]) + + report = "\n".join(lines) + debug.printMessage(debug.LEVEL_INFO, "CTHULHU DIAGNOSTIC STATE:\n" + report, True) + if notify_user and script is not None: + script.presentMessage("Cthulhu diagnostic state written to debug log.") + return report + @dbus_service.command def sayAll(self, inputEvent, obj=None, offset=None, notify_user=True): """Speaks the entire document or text, starting from the current position.""" diff --git a/src/cthulhu/settings_manager.py b/src/cthulhu/settings_manager.py index 61e9bda..fbccfc4 100644 --- a/src/cthulhu/settings_manager.py +++ b/src/cthulhu/settings_manager.py @@ -462,8 +462,7 @@ class SettingsManager(object): debug.printMessage(debug.LEVEL_INFO, msg, True) def _enableAccessibility(self) -> bool: - """Enables the GNOME accessibility flag. Users need to log out and - then back in for this to take effect. + """Enables the desktop accessibility bus flag when available. Returns True if an action was taken (i.e., accessibility was not set prior to this call). diff --git a/src/cthulhu/speech_dbus_manager.py b/src/cthulhu/speech_dbus_manager.py deleted file mode 100644 index 974b043..0000000 --- a/src/cthulhu/speech_dbus_manager.py +++ /dev/null @@ -1,532 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2025 Stormux -# Copyright (c) 2025 Igalia, S.L. -# -# 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. - -"""Enhanced speech settings management for D-Bus remote controller.""" - -__id__ = "$Id$" -__version__ = "$Revision$" -__date__ = "$Date$" -__copyright__ = "Copyright (c) 2025 Stormux" -__license__ = "LGPL" - -from . import cthulhu_state -from . import debug -from . import dbus_service -from . import messages -from . import settings -from . import settings_manager - -class SpeechDBusManager: - """Enhanced speech settings for D-Bus remote control.""" - - def __init__(self): - """Initialize the speech D-Bus manager.""" - self._settings_manager = settings_manager.getManager() - - @dbus_service.getter - def get_verbosity_level(self) -> str: - """Returns the current speech verbosity level.""" - - level = self._settings_manager.getSetting("speechVerbosityLevel") - if level == settings.VERBOSITY_LEVEL_BRIEF: - return "brief" - else: - return "verbose" - - @dbus_service.setter - def set_verbosity_level(self, value: str) -> bool: - """Sets the speech verbosity level.""" - - if value.lower() == "brief": - setting_value = settings.VERBOSITY_LEVEL_BRIEF - elif value.lower() == "verbose": - setting_value = settings.VERBOSITY_LEVEL_VERBOSE - else: - msg = f"SPEECH DBUS MANAGER: Invalid verbosity level: {value}" - debug.printMessage(debug.LEVEL_WARNING, msg, True) - return False - - msg = f"SPEECH DBUS MANAGER: Setting verbosity level to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("speechVerbosityLevel", setting_value) - return True - - @dbus_service.getter - def get_capitalization_style(self) -> str: - """Returns the current capitalization style.""" - - style = self._settings_manager.getSetting("capitalizationStyle") - if style == settings.CAPITALIZATION_STYLE_NONE: - return "none" - elif style == settings.CAPITALIZATION_STYLE_SPELL: - return "spell" - elif style == settings.CAPITALIZATION_STYLE_ICON: - return "icon" - else: - return "none" - - @dbus_service.setter - def set_capitalization_style(self, value: str) -> bool: - """Sets the capitalization style.""" - - value_lower = value.lower() - if value_lower == "none": - setting_value = settings.CAPITALIZATION_STYLE_NONE - elif value_lower == "spell": - setting_value = settings.CAPITALIZATION_STYLE_SPELL - elif value_lower == "icon": - setting_value = settings.CAPITALIZATION_STYLE_ICON - else: - msg = f"SPEECH DBUS MANAGER: Invalid capitalization style: {value}" - debug.printMessage(debug.LEVEL_WARNING, msg, True) - return False - - msg = f"SPEECH DBUS MANAGER: Setting capitalization style to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("capitalizationStyle", setting_value) - return True - - @dbus_service.getter - def get_punctuation_level(self) -> str: - """Returns the current punctuation level.""" - - level = self._settings_manager.getSetting("verbalizePunctuationStyle") - if level == settings.PUNCTUATION_STYLE_NONE: - return "none" - elif level == settings.PUNCTUATION_STYLE_SOME: - return "some" - elif level == settings.PUNCTUATION_STYLE_MOST: - return "most" - elif level == settings.PUNCTUATION_STYLE_ALL: - return "all" - else: - return "some" - - @dbus_service.setter - def set_punctuation_level(self, value: str) -> bool: - """Sets the punctuation level.""" - - value_lower = value.lower() - if value_lower == "none": - setting_value = settings.PUNCTUATION_STYLE_NONE - elif value_lower == "some": - setting_value = settings.PUNCTUATION_STYLE_SOME - elif value_lower == "most": - setting_value = settings.PUNCTUATION_STYLE_MOST - elif value_lower == "all": - setting_value = settings.PUNCTUATION_STYLE_ALL - else: - msg = f"SPEECH DBUS MANAGER: Invalid punctuation level: {value}" - debug.printMessage(debug.LEVEL_WARNING, msg, True) - return False - - msg = f"SPEECH DBUS MANAGER: Setting punctuation level to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("verbalizePunctuationStyle", setting_value) - return True - - @dbus_service.getter - def get_speak_numbers_as_digits(self) -> bool: - """Returns whether numbers are spoken as digits.""" - - return self._settings_manager.getSetting("speakNumbersAsDigits") - - @dbus_service.setter - def set_speak_numbers_as_digits(self, value: bool) -> bool: - """Sets whether numbers are spoken as digits.""" - - msg = f"SPEECH DBUS MANAGER: Setting speak numbers as digits to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("speakNumbersAsDigits", value) - return True - - @dbus_service.getter - def get_speech_is_muted(self) -> bool: - """Returns whether speech output is temporarily muted.""" - - return self._settings_manager.getSetting("silenceSpeech") - - @dbus_service.setter - def set_speech_is_muted(self, value: bool) -> bool: - """Sets whether speech output is temporarily muted.""" - - msg = f"SPEECH DBUS MANAGER: Setting speech muted to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("silenceSpeech", value) - return True - - @dbus_service.getter - def get_only_speak_displayed_text(self) -> bool: - """Returns whether only displayed text should be spoken.""" - - return self._settings_manager.getSetting("onlySpeakDisplayedText") - - @dbus_service.setter - def set_only_speak_displayed_text(self, value: bool) -> bool: - """Sets whether only displayed text should be spoken.""" - - msg = f"SPEECH DBUS MANAGER: Setting only speak displayed text to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("onlySpeakDisplayedText", value) - return True - - @dbus_service.getter - def get_speak_indentation_and_justification(self) -> bool: - """Returns whether speaking of indentation and justification is enabled.""" - - return self._settings_manager.getSetting("enableSpeechIndentation") - - def _sync_indentation_presentation_mode(self, enable_speech): - mode = self._settings_manager.getSetting("indentationPresentationMode") \ - or settings.indentationPresentationMode - if enable_speech: - if mode == settings.INDENTATION_PRESENTATION_OFF: - mode = settings.INDENTATION_PRESENTATION_SPEECH - elif mode == settings.INDENTATION_PRESENTATION_BEEPS: - mode = settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS - else: - if mode == settings.INDENTATION_PRESENTATION_SPEECH: - mode = settings.INDENTATION_PRESENTATION_OFF - elif mode == settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS: - mode = settings.INDENTATION_PRESENTATION_BEEPS - - self._settings_manager.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.""" - - msg = f"SPEECH DBUS MANAGER: Setting speak indentation and justification to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("enableSpeechIndentation", value) - self._sync_indentation_presentation_mode(value) - return True - - @dbus_service.command - def toggle_speech(self, script=None, event=None): - """Toggles speech on and off.""" - - tokens = ["SPEECH DBUS MANAGER: toggle_speech. Script:", script, "Event:", event] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - - if script is not None: - script.presentationInterrupt() - - if self.get_speech_is_muted(): - self.set_speech_is_muted(False) - if script is not None: - script.presentMessage(messages.SPEECH_ENABLED) - elif not self._settings_manager.getSetting("enableSpeech"): - self._settings_manager.setSetting("enableSpeech", True) - if script is not None: - script.presentMessage(messages.SPEECH_ENABLED) - else: - if script is not None: - script.presentMessage(messages.SPEECH_DISABLED) - self.set_speech_is_muted(True) - - @dbus_service.command - def toggle_verbosity(self, script=None, event=None): - """Toggles speech verbosity level between verbose and brief.""" - - tokens = ["SPEECH DBUS MANAGER: toggle_verbosity. Script:", script, "Event:", event] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - - current_level = self._settings_manager.getSetting("speechVerbosityLevel") - if current_level == settings.VERBOSITY_LEVEL_BRIEF: - if script is not None: - script.presentMessage(messages.SPEECH_VERBOSITY_VERBOSE) - self._settings_manager.setSetting("speechVerbosityLevel", settings.VERBOSITY_LEVEL_VERBOSE) - else: - if script is not None: - script.presentMessage(messages.SPEECH_VERBOSITY_BRIEF) - self._settings_manager.setSetting("speechVerbosityLevel", settings.VERBOSITY_LEVEL_BRIEF) - - @dbus_service.command - def change_number_style(self, script=None, event=None): - """Changes spoken number style between digits and words.""" - - tokens = ["SPEECH DBUS MANAGER: change_number_style. Script:", script, "Event:", event] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - - speak_digits = self.get_speak_numbers_as_digits() - if speak_digits: - brief = messages.NUMBER_STYLE_WORDS_BRIEF - full = messages.NUMBER_STYLE_WORDS_FULL - else: - brief = messages.NUMBER_STYLE_DIGITS_BRIEF - full = messages.NUMBER_STYLE_DIGITS_FULL - - self.set_speak_numbers_as_digits(not speak_digits) - if script is not None: - script.presentMessage(full, brief) - - @dbus_service.command - def say_all(self, script=None, event=None): - """Speaks the entire document or text, starting from the current position.""" - - tokens = ["SPEECH DBUS MANAGER: say_all. Script:", script, "Event:", event] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - - # Use the current active script if not provided - if script is None: - script = cthulhu_state.activeScript - - if script is None: - msg = "SPEECH DBUS MANAGER: No active script available for Say All" - debug.printMessage(debug.LEVEL_WARNING, msg, True) - return False - - # Call the script's Say All method - try: - script.sayAll(event, notify_user=False) - return True - except Exception as e: - msg = f"SPEECH DBUS MANAGER: Error during Say All: {e}" - debug.printMessage(debug.LEVEL_SEVERE, msg, True) - return False - - # Key Echo Controls - @dbus_service.getter - def get_key_echo_enabled(self) -> bool: - """Returns whether echo of key presses is enabled.""" - - return self._settings_manager.getSetting("enableKeyEcho") - - @dbus_service.setter - def set_key_echo_enabled(self, value: bool) -> bool: - """Sets whether echo of key presses is enabled.""" - - msg = f"SPEECH DBUS MANAGER: Setting enable key echo to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("enableKeyEcho", value) - return True - - @dbus_service.getter - def get_character_echo_enabled(self) -> bool: - """Returns whether echo of inserted characters is enabled.""" - - return self._settings_manager.getSetting("enableEchoByCharacter") - - @dbus_service.setter - def set_character_echo_enabled(self, value: bool) -> bool: - """Sets whether echo of inserted characters is enabled.""" - - msg = f"SPEECH DBUS MANAGER: Setting enable character echo to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("enableEchoByCharacter", value) - return True - - @dbus_service.getter - def get_word_echo_enabled(self) -> bool: - """Returns whether word echo is enabled.""" - - return self._settings_manager.getSetting("enableEchoByWord") - - @dbus_service.setter - def set_word_echo_enabled(self, value: bool) -> bool: - """Sets whether word echo is enabled.""" - - msg = f"SPEECH DBUS MANAGER: Setting enable word echo to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("enableEchoByWord", value) - return True - - @dbus_service.getter - def get_sentence_echo_enabled(self) -> bool: - """Returns whether sentence echo is enabled.""" - - return self._settings_manager.getSetting("enableEchoBySentence") - - @dbus_service.setter - def set_sentence_echo_enabled(self, value: bool) -> bool: - """Sets whether sentence echo is enabled.""" - - msg = f"SPEECH DBUS MANAGER: Setting enable sentence echo to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("enableEchoBySentence", value) - return True - - @dbus_service.getter - def get_alphabetic_keys_enabled(self) -> bool: - """Returns whether alphabetic keys will be echoed when key echo is enabled.""" - - return self._settings_manager.getSetting("enableAlphabeticKeys") - - @dbus_service.setter - def set_alphabetic_keys_enabled(self, value: bool) -> bool: - """Sets whether alphabetic keys will be echoed when key echo is enabled.""" - - msg = f"SPEECH DBUS MANAGER: Setting enable alphabetic keys to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("enableAlphabeticKeys", value) - return True - - @dbus_service.getter - def get_numeric_keys_enabled(self) -> bool: - """Returns whether numeric keys will be echoed when key echo is enabled.""" - - return self._settings_manager.getSetting("enableNumericKeys") - - @dbus_service.setter - def set_numeric_keys_enabled(self, value: bool) -> bool: - """Sets whether numeric keys will be echoed when key echo is enabled.""" - - msg = f"SPEECH DBUS MANAGER: Setting enable numeric keys to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("enableNumericKeys", value) - return True - - @dbus_service.getter - def get_punctuation_keys_enabled(self) -> bool: - """Returns whether punctuation keys will be echoed when key echo is enabled.""" - - return self._settings_manager.getSetting("enablePunctuationKeys") - - @dbus_service.setter - def set_punctuation_keys_enabled(self, value: bool) -> bool: - """Sets whether punctuation keys will be echoed when key echo is enabled.""" - - msg = f"SPEECH DBUS MANAGER: Setting enable punctuation keys to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("enablePunctuationKeys", value) - return True - - @dbus_service.getter - def get_space_enabled(self) -> bool: - """Returns whether space key will be echoed when key echo is enabled.""" - - return self._settings_manager.getSetting("enableSpace") - - @dbus_service.setter - def set_space_enabled(self, value: bool) -> bool: - """Sets whether space key will be echoed when key echo is enabled.""" - - msg = f"SPEECH DBUS MANAGER: Setting enable space to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("enableSpace", value) - return True - - @dbus_service.getter - def get_modifier_keys_enabled(self) -> bool: - """Returns whether modifier keys will be echoed when key echo is enabled.""" - - return self._settings_manager.getSetting("enableModifierKeys") - - @dbus_service.setter - def set_modifier_keys_enabled(self, value: bool) -> bool: - """Sets whether modifier keys will be echoed when key echo is enabled.""" - - msg = f"SPEECH DBUS MANAGER: Setting enable modifier keys to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("enableModifierKeys", value) - return True - - @dbus_service.getter - def get_function_keys_enabled(self) -> bool: - """Returns whether function keys will be echoed when key echo is enabled.""" - - return self._settings_manager.getSetting("enableFunctionKeys") - - @dbus_service.setter - def set_function_keys_enabled(self, value: bool) -> bool: - """Sets whether function keys will be echoed when key echo is enabled.""" - - msg = f"SPEECH DBUS MANAGER: Setting enable function keys to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("enableFunctionKeys", value) - return True - - @dbus_service.getter - def get_action_keys_enabled(self) -> bool: - """Returns whether action keys will be echoed when key echo is enabled.""" - - return self._settings_manager.getSetting("enableActionKeys") - - @dbus_service.setter - def set_action_keys_enabled(self, value: bool) -> bool: - """Sets whether action keys will be echoed when key echo is enabled.""" - - msg = f"SPEECH DBUS MANAGER: Setting enable action keys to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("enableActionKeys", value) - return True - - @dbus_service.getter - def get_navigation_keys_enabled(self) -> bool: - """Returns whether navigation keys will be echoed when key echo is enabled.""" - - return self._settings_manager.getSetting("enableNavigationKeys") - - @dbus_service.setter - def set_navigation_keys_enabled(self, value: bool) -> bool: - """Sets whether navigation keys will be echoed when key echo is enabled.""" - - msg = f"SPEECH DBUS MANAGER: Setting enable navigation keys to {value}." - debug.printMessage(debug.LEVEL_INFO, msg, True) - self._settings_manager.setSetting("enableNavigationKeys", value) - return True - - @dbus_service.command - def cycle_key_echo(self, script=None, event=None): - """Cycle through the key echo levels.""" - - tokens = ["SPEECH DBUS MANAGER: cycle_key_echo. Script:", script, "Event:", event] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - - # Get current settings - key = self._settings_manager.getSetting("enableKeyEcho") - word = self._settings_manager.getSetting("enableEchoByWord") - sentence = self._settings_manager.getSetting("enableEchoBySentence") - - # Cycle through the combinations: none -> key -> word -> sentence -> all -> none - if not key and not word and not sentence: - # None -> Key only - new_key, new_word, new_sentence = True, False, False - brief = messages.KEY_ECHO_KEY_BRIEF - full = messages.KEY_ECHO_KEY_FULL - elif key and not word and not sentence: - # Key -> Word - new_key, new_word, new_sentence = False, True, False - brief = messages.KEY_ECHO_WORD_BRIEF - full = messages.KEY_ECHO_WORD_FULL - elif not key and word and not sentence: - # Word -> Sentence - new_key, new_word, new_sentence = False, False, True - brief = messages.KEY_ECHO_SENTENCE_BRIEF - full = messages.KEY_ECHO_SENTENCE_FULL - elif not key and not word and sentence: - # Sentence -> All - new_key, new_word, new_sentence = True, True, True - brief = messages.KEY_ECHO_KEY_AND_WORD_BRIEF - full = messages.KEY_ECHO_KEY_AND_WORD_FULL - else: - # All -> None - new_key, new_word, new_sentence = False, False, False - brief = messages.KEY_ECHO_NONE_BRIEF - full = messages.KEY_ECHO_NONE_FULL - - # Apply new settings - self._settings_manager.setSetting("enableKeyEcho", new_key) - self._settings_manager.setSetting("enableEchoByWord", new_word) - self._settings_manager.setSetting("enableEchoBySentence", new_sentence) - - if script is not None: - script.presentMessage(full, brief) diff --git a/src/cthulhu/speech_generator.py b/src/cthulhu/speech_generator.py index 3c048e7..1426353 100644 --- a/src/cthulhu/speech_generator.py +++ b/src/cthulhu/speech_generator.py @@ -717,7 +717,7 @@ class SpeechGenerator(generator.Generator): and AXUtilities.is_selected(obj): return [] - # egg-list-box, e.g. privacy panel in gnome-control-center + # egg-list-box-style containers can expose selected panels as list items. if AXUtilities.is_list_box(parent): doNotPresent.append(AXObject.get_role(obj)) diff --git a/tests/test_toml_backend_legacy_schema_regressions.py b/tests/test_toml_backend_legacy_schema_regressions.py index 0dbb870..0fc97ba 100644 --- a/tests/test_toml_backend_legacy_schema_regressions.py +++ b/tests/test_toml_backend_legacy_schema_regressions.py @@ -86,6 +86,25 @@ class LegacyTomlSchemaMigrationTests(unittest.TestCase): self.assertNotIn("format-version = 2", savedSettings) self.assertNotIn("[profiles.default.metadata]", savedSettings) + def test_legacy_profile_keybindings_are_preserved(self): + legacySettings = LEGACY_SETTINGS.replace( + 'desktop-modifier-keys = ["Insert", "KP_Insert"]', + 'desktop-modifier-keys = ["Insert", "KP_Insert"]\ncustom-binding = "kb:cthulhu+x"', + ) + + with tempfile.TemporaryDirectory() as tempDir: + Path(tempDir, "user-settings.toml").write_text( + legacySettings, + encoding="utf-8", + ) + + backend = Backend(tempDir) + + self.assertEqual( + backend.getKeybindings("default"), + {"custom-binding": "kb:cthulhu+x"}, + ) + if __name__ == "__main__": unittest.main()