diff --git a/CLAUDE.md b/CLAUDE.md index df17b2c..3ef1d12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -791,29 +791,32 @@ busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service org. - 🔄 **Module registration**: Ready for individual managers to register D-Bus commands - 🔄 **Plugin integration**: Plugins can expose D-Bus commands using decorators -### **✅ COMPLETED - D-Bus Remote Controller Integration** -The D-Bus Remote Controller from Orca v49.alpha has been successfully integrated into Cthulhu and is fully functional. +### **✅ COMPLETED - Enhanced D-Bus Remote Controller with Speech and Key Echo Controls** +The D-Bus Remote Controller from Orca v49.alpha has been successfully re-ported and enhanced with comprehensive speech and typing echo controls. -**Root Cause of Issues**: D-Bus service startup timing conflicts with ATSPI registry initialization. +**Latest Enhancement (2025)**: +- **SpeechManager Module**: Complete D-Bus control over speech settings (muting, verbosity, punctuation, capitalization, number pronunciation) +- **TypingEchoManager Module**: Granular key echo controls (character/word/sentence echo, per-key-type settings) +- **No systemd dependency**: Direct session bus registration without service files +- **Real-time effect**: All settings take effect immediately -**Solution Implemented**: -- Deferred D-Bus service startup using `GObject.idle_add()` after ATSPI event loop is running -- Fixed all API naming convention differences between Orca and Cthulhu +**Files Created/Modified for Enhanced D-Bus Integration**: +- `src/cthulhu/speech_and_verbosity_manager.py` - Enhanced with D-Bus getters/setters for all speech settings +- `src/cthulhu/typing_echo_presenter.py` (NEW FILE) - Complete typing echo system with D-Bus controls +- `src/cthulhu/cthulhu.py` - D-Bus service registration for speech and typing echo managers +- `src/cthulhu/meson.build` - Added typing_echo_presenter.py to build +- `README-REMOTE-CONTROLLER.md` - Updated with comprehensive speech and key echo examples -**Files Modified for D-Bus Integration**: -- `src/cthulhu/dbus_service.py` (NEW FILE) - Complete D-Bus service port with Cthulhu API fixes -- `src/cthulhu/input_event.py` - Added RemoteControllerEvent + GDK version fix -- `src/cthulhu/cthulhu.py` - D-Bus integration + lazy BrailleEvent import + settings manager activation + deferred startup -- `src/cthulhu/Makefile.am` - Added dbus_service.py to build -- Multiple presenter files - Converted to lazy initialization pattern -- `src/cthulhu/keybindings.py` - Fixed GDK version requirement -- `README-REMOTE-CONTROLLER.md` (NEW FILE) - Complete documentation with examples +**Available D-Bus Modules**: +- **SpeechManager**: Speech muting, verbosity, punctuation, capitalization, number styles, indentation speech +- **TypingEchoManager**: Master key echo, character/word/sentence echo, per-key-type controls (alphabetic, numeric, punctuation, space, modifier, function, action, navigation, diacritical keys) +- **DefaultScript**: Core Cthulhu commands -**API Fixes Applied**: -- `debug.print_message` → `debug.printMessage` -- `script_manager.get_manager()` → `script_manager.getManager()` -- `get_active_script()` → `cthulhu_state.activeScript` -- `get_default_script()` → `getDefaultScript()` +**D-Bus Interface Design**: +- Service: `org.stormux.Cthulhu.Service` +- Module paths: `/org/stormux/Cthulhu/Service/ModuleName` +- Generic interface: `org.stormux.Cthulhu.Module` +- Methods: `ExecuteRuntimeGetter`, `ExecuteRuntimeSetter`, `ExecuteCommand` ### Bug Fixes Applied - Fixed circular imports in presenter modules (learn_mode_presenter, notification_presenter, etc.) diff --git a/README-REMOTE-CONTROLLER.md b/README-REMOTE-CONTROLLER.md index a646b87..bdfe490 100644 --- a/README-REMOTE-CONTROLLER.md +++ b/README-REMOTE-CONTROLLER.md @@ -330,6 +330,215 @@ If you get "The name is not activatable" or similar errors: - **Permissions**: Ensure you're using `--user` with busctl/gdbus for session bus access. - **Display**: Make sure `DISPLAY=:0` is set when running Cthulhu in terminal sessions. +## Speech and Key Echo Control Examples + +### SpeechManager Module + +The SpeechManager module provides comprehensive control over Cthulhu's speech settings: + +#### Speech Muting +```bash +# Check if speech is muted +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeGetter s "SpeechIsMuted" + +# Mute speech +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "SpeechIsMuted" b true + +# Unmute speech +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "SpeechIsMuted" b false +``` + +#### Verbosity Control +```bash +# Get current verbosity level +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeGetter s "VerbosityLevel" + +# Set verbosity to brief +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "VerbosityLevel" s "brief" + +# Set verbosity to verbose +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "VerbosityLevel" s "verbose" +``` + +#### Punctuation Control +```bash +# Get current punctuation level +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeGetter s "PunctuationLevel" + +# Set punctuation level (none/some/most/all) +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "PunctuationLevel" s "all" +``` + +#### Other Speech Settings +```bash +# Number pronunciation +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "SpeakNumbersAsDigits" b true + +# Capitalization style (none/icon/spell) +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "CapitalizationStyle" s "spell" + +# Indentation and justification speech +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "SpeakIndentationAndJustification" b true + +# Display-only text mode +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "OnlySpeakDisplayedText" b false +``` + +### TypingEchoManager Module + +The TypingEchoManager module provides granular control over key echo and typing feedback: + +#### Master Key Echo Control +```bash +# Check if key echo is enabled +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeGetter s "KeyEchoEnabled" + +# Enable key echo +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "KeyEchoEnabled" b true + +# Disable key echo +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "KeyEchoEnabled" b false +``` + +#### Character, Word, and Sentence Echo +```bash +# Character echo (echo characters as they're typed) +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "CharacterEchoEnabled" b true + +# Word echo (speak word when completed) +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "WordEchoEnabled" b true + +# Sentence echo (speak sentence when completed) +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "SentenceEchoEnabled" b false +``` + +#### Key Type Controls +```bash +# Alphabetic keys (a-z, A-Z) +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "AlphabeticKeysEnabled" b true + +# Numeric keys (0-9) +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "NumericKeysEnabled" b true + +# Punctuation keys (!@#$%^&*(),.;' etc.) +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "PunctuationKeysEnabled" b true + +# Space key +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "SpaceEnabled" b true + +# Modifier keys (Ctrl, Alt, Shift, etc.) +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "ModifierKeysEnabled" b false + +# Function keys (F1-F12) +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "FunctionKeysEnabled" b true + +# Action keys (Enter, Tab, Backspace, Delete, Escape) +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "ActionKeysEnabled" b true + +# Navigation keys (Arrow keys, Home, End, Page Up/Down) +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "NavigationKeysEnabled" b false + +# Diacritical keys (accented characters) +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "DiacriticalKeysEnabled" b true +``` + +### Complete Automation Script Example + +```bash +#!/bin/bash +# Complete Cthulhu Speech and Key Echo Configuration via D-Bus + +echo "=== Configuring Cthulhu via D-Bus ===" + +# Speech Configuration +echo "Setting up speech preferences..." +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "VerbosityLevel" s "brief" + +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "PunctuationLevel" s "some" + +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/SpeechManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "SpeakNumbersAsDigits" b true + +# Key Echo Configuration +echo "Setting up key echo preferences..." +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "KeyEchoEnabled" b true + +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "AlphabeticKeysEnabled" b true + +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "NumericKeysEnabled" b true + +busctl --user call org.stormux.Cthulhu.Service \ + /org/stormux/Cthulhu/Service/TypingEchoManager \ + org.stormux.Cthulhu.Module ExecuteRuntimeSetter sv "ModifierKeysEnabled" b false + +echo "Configuration complete!" +``` + ## Examples ### Quick Test Script diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index 683060e..aced4eb 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -858,7 +858,21 @@ def _start_dbus_service(): """Starts the D-Bus remote controller service in an idle callback.""" debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Starting D-Bus remote controller', True) try: + # Start the D-Bus service dbus_service.get_remote_controller().start() + + # Register speech and verbosity manager + debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Registering SpeechManager D-Bus module', True) + from . import speech_and_verbosity_manager + speech_manager = speech_and_verbosity_manager.getManager() + dbus_service.get_remote_controller().register_decorated_module("SpeechManager", speech_manager) + + # Register typing echo presenter + debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Registering TypingEchoManager D-Bus module', True) + from . import typing_echo_presenter + typing_echo_manager = typing_echo_presenter.getManager() + dbus_service.get_remote_controller().register_decorated_module("TypingEchoManager", typing_echo_manager) + except Exception as e: msg = f"CTHULHU: Failed to start D-Bus service: {e}" debug.printMessage(debug.LEVEL_SEVERE, msg, True) diff --git a/src/cthulhu/meson.build b/src/cthulhu/meson.build index 582d143..64dff3b 100644 --- a/src/cthulhu/meson.build +++ b/src/cthulhu/meson.build @@ -85,6 +85,7 @@ cthulhu_python_sources = files([ 'translation_context.py', 'translation_manager.py', 'tutorialgenerator.py', + 'typing_echo_presenter.py', 'where_am_i_presenter.py', ]) diff --git a/src/cthulhu/plugins/SpeechHistory.disabled/__init__.py b/src/cthulhu/plugins/SpeechHistory.disabled/__init__.py new file mode 100644 index 0000000..d3ff8fe --- /dev/null +++ b/src/cthulhu/plugins/SpeechHistory.disabled/__init__.py @@ -0,0 +1 @@ +from .plugin import SpeechHistory \ No newline at end of file diff --git a/src/cthulhu/plugins/SpeechHistory.disabled/meson.build b/src/cthulhu/plugins/SpeechHistory.disabled/meson.build new file mode 100644 index 0000000..8f5370c --- /dev/null +++ b/src/cthulhu/plugins/SpeechHistory.disabled/meson.build @@ -0,0 +1,14 @@ +speechhistory_python_sources = files([ + '__init__.py', + 'plugin.py' +]) + +python3.install_sources( + speechhistory_python_sources, + subdir: 'cthulhu/plugins/SpeechHistory' +) + +install_data( + 'plugin.info', + install_dir: python3.get_install_dir() / 'cthulhu' / 'plugins' / 'SpeechHistory' +) \ No newline at end of file diff --git a/src/cthulhu/plugins/SpeechHistory.disabled/plugin.info b/src/cthulhu/plugins/SpeechHistory.disabled/plugin.info new file mode 100644 index 0000000..92f1a40 --- /dev/null +++ b/src/cthulhu/plugins/SpeechHistory.disabled/plugin.info @@ -0,0 +1,8 @@ +name = Speech History +version = 1.0.0 +description = Keeps a history of all speech output with navigation and clipboard support +authors = Cthulhu Plugin System +website = https://git.stormux.org/storm/cthulhu +copyright = Copyright 2024 Stormux +builtin = true +hidden = false \ No newline at end of file diff --git a/src/cthulhu/plugins/SpeechHistory.disabled/plugin.py b/src/cthulhu/plugins/SpeechHistory.disabled/plugin.py new file mode 100644 index 0000000..2227f61 --- /dev/null +++ b/src/cthulhu/plugins/SpeechHistory.disabled/plugin.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 + +import logging +from collections import deque +from cthulhu.plugin import Plugin, cthulhu_hookimpl +from cthulhu import settings_manager +from cthulhu import debug + +logger = logging.getLogger(__name__) + +class SpeechHistory(Plugin): + """Speech History plugin - SAFE manual-only version (no automatic capture).""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + debug.printMessage(debug.LEVEL_INFO, "SpeechHistory SAFE plugin initialized", True) + + # History storage - start with some sample items + self._max_history_size = 50 + self._history = deque([ + "Welcome to safe speech history", + "This version doesn't auto-capture to prevent crashes", + "Use add_to_history() method to manually add items", + "Navigate with Cthulhu+Control+Shift+H (previous)", + "Navigate with Cthulhu+Control+H (next)", + "Copy with Cthulhu+Control+Y" + ], maxlen=self._max_history_size) + self._current_history_index = -1 + + # Keybinding storage + self._kb_nav_prev = None + self._kb_nav_next = None + self._kb_copy_last = None + + # Settings integration + self._settings_manager = settings_manager.getManager() + + @cthulhu_hookimpl + def activate(self, plugin=None): + """Activate the plugin.""" + if plugin is not None and plugin is not self: + return + + try: + debug.printMessage(debug.LEVEL_INFO, "=== SpeechHistory SAFE activation starting ===", True) + + # Load settings + self._load_settings() + + # Register keybindings only - NO speech capture + self._register_keybindings() + + debug.printMessage(debug.LEVEL_INFO, "SpeechHistory SAFE plugin activated successfully", True) + return True + + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"Error activating SpeechHistory SAFE: {e}", True) + return False + + @cthulhu_hookimpl + def deactivate(self, plugin=None): + """Deactivate the plugin.""" + if plugin is not None and plugin is not self: + return + + debug.printMessage(debug.LEVEL_INFO, "Deactivating SpeechHistory SAFE plugin", True) + + # Clear keybindings + self._kb_nav_prev = None + self._kb_nav_next = None + self._kb_copy_last = None + + return True + + def _load_settings(self): + """Load plugin settings.""" + try: + self._max_history_size = self._settings_manager.getSetting('speechHistorySize') or 50 + # Update deque maxlen if needed + if self._history.maxlen != self._max_history_size: + old_history = list(self._history) + self._history = deque(old_history[-self._max_history_size:], maxlen=self._max_history_size) + debug.printMessage(debug.LEVEL_INFO, f"Speech history size: {self._max_history_size}", True) + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"Error loading settings: {e}", True) + self._max_history_size = 50 + + def _register_keybindings(self): + """Register plugin keybindings.""" + try: + # Cthulhu+Control+Shift+H (History previous) + self._kb_nav_prev = self.registerGestureByString( + self._navigate_history_prev, + "Speech history previous", + 'kb:cthulhu+control+shift+h' + ) + + # Cthulhu+Control+H (History next) + self._kb_nav_next = self.registerGestureByString( + self._navigate_history_next, + "Speech history next", + 'kb:cthulhu+control+h' + ) + + # Cthulhu+Control+Y (Copy history) + self._kb_copy_last = self.registerGestureByString( + self._copy_last_spoken, + "Copy speech history item to clipboard", + 'kb:cthulhu+control+y' + ) + + debug.printMessage(debug.LEVEL_INFO, f"Registered keybindings: {bool(self._kb_nav_prev)}, {bool(self._kb_nav_next)}, {bool(self._kb_copy_last)}", True) + + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"Error registering keybindings: {e}", True) + + def _navigate_history_prev(self, script=None, inputEvent=None): + """Navigate to previous item in speech history.""" + try: + if not self._history: + self._present_message("Speech history is empty") + return True + + # Move backward in history (to older items) + if self._current_history_index == -1: + self._current_history_index = len(self._history) - 1 + elif self._current_history_index > 0: + self._current_history_index -= 1 + else: + self._current_history_index = len(self._history) - 1 + + # Present the history item + history_item = self._history[self._current_history_index] + position = self._current_history_index + 1 + self._present_message(f"History {position} of {len(self._history)}: {history_item}") + + return True + + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"Error navigating to previous: {e}", True) + return False + + def _navigate_history_next(self, script=None, inputEvent=None): + """Navigate to next item in speech history.""" + try: + if not self._history: + self._present_message("Speech history is empty") + return True + + # Move forward in history (to newer items) + if self._current_history_index == -1: + self._current_history_index = 0 + elif self._current_history_index < len(self._history) - 1: + self._current_history_index += 1 + else: + self._current_history_index = 0 + + # Present the history item + history_item = self._history[self._current_history_index] + position = self._current_history_index + 1 + self._present_message(f"History {position} of {len(self._history)}: {history_item}") + + return True + + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"Error navigating to next: {e}", True) + return False + + def _copy_last_spoken(self, script=None, inputEvent=None): + """Copy the last spoken text to clipboard.""" + try: + if not self._history: + self._present_message("No speech history to copy") + return True + + # Copy the most recent speech + last_spoken = self._history[-1] + + try: + import gi + gi.require_version("Gtk", "3.0") + from gi.repository import Gtk, Gdk + + clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + clipboard.set_text(last_spoken, -1) + clipboard.store() + + # Show confirmation + preview = last_spoken[:50] + ('...' if len(last_spoken) > 50 else '') + self._present_message(f"Copied to clipboard: {preview}") + + except Exception as clipboard_error: + debug.printMessage(debug.LEVEL_INFO, f"Clipboard error: {clipboard_error}", True) + self._present_message("Error copying to clipboard") + + return True + + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"Error copying: {e}", True) + return False + + def _present_message(self, message): + """Present a message to the user via speech.""" + try: + if self.app: + state = self.app.getDynamicApiManager().getAPI('CthulhuState') + if state and state.activeScript: + state.activeScript.presentMessage(message, resetStyles=False) + else: + debug.printMessage(debug.LEVEL_INFO, f"Message: {message}", True) + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"Error presenting message: {e}", True) + + def add_to_history(self, text): + """Public method to safely add items to history.""" + try: + if not text or not text.strip(): + return + + clean_text = text.strip() + if len(clean_text) < 2: + return + + # Simple duplicate prevention + if self._history and self._history[-1] == clean_text: + return + + # Add to history + self._history.append(clean_text) + self._current_history_index = -1 + + debug.printMessage(debug.LEVEL_INFO, f"Manually added to history: {clean_text[:50]}{'...' if len(clean_text) > 50 else ''}", True) + + except Exception as e: + debug.printMessage(debug.LEVEL_INFO, f"Error adding to history: {e}", True) \ No newline at end of file diff --git a/src/cthulhu/speech_and_verbosity_manager.py b/src/cthulhu/speech_and_verbosity_manager.py index 6a5cc99..0e1b273 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 dbus_service from . import debug from . import input_event from . import keybindings @@ -258,6 +259,157 @@ class SpeechAndVerbosityManager: def _get_server(self): return speech.getSpeechServer() + # D-Bus getters and setters for speech settings + @dbus_service.getter + def get_speech_is_muted(self) -> bool: + """Returns whether speech output is temporarily muted.""" + return _settings_manager.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) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting speech mute: {e}", True) + return False + + @dbus_service.getter + def get_verbosity_level(self) -> str: + """Returns the current speech verbosity level.""" + level = _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.""" + try: + if value.lower() == "brief": + _settings_manager.setSetting('speechVerbosityLevel', settings.VERBOSITY_LEVEL_BRIEF) + elif value.lower() == "verbose": + _settings_manager.setSetting('speechVerbosityLevel', settings.VERBOSITY_LEVEL_VERBOSE) + else: + return False + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting verbosity level: {e}", True) + return False + + @dbus_service.getter + def get_speak_numbers_as_digits(self) -> bool: + """Returns whether numbers are spoken as digits.""" + return _settings_manager.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) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak numbers as digits: {e}", True) + return False + + @dbus_service.getter + def get_only_speak_displayed_text(self) -> bool: + """Returns whether only displayed text should be spoken.""" + return _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.""" + try: + _settings_manager.setSetting('onlySpeakDisplayedText', value) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting only speak displayed text: {e}", True) + return False + + @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') + + @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) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting speak indentation: {e}", True) + return False + + @dbus_service.getter + def get_punctuation_level(self) -> str: + """Returns the current punctuation level.""" + level = _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.""" + try: + value_lower = value.lower() + if value_lower == "none": + _settings_manager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_NONE) + elif value_lower == "some": + _settings_manager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_SOME) + elif value_lower == "most": + _settings_manager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_MOST) + elif value_lower == "all": + _settings_manager.setSetting('verbalizePunctuationStyle', settings.PUNCTUATION_STYLE_ALL) + else: + return False + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting punctuation level: {e}", True) + return False + + @dbus_service.getter + def get_capitalization_style(self) -> str: + """Returns the current capitalization style.""" + style = _settings_manager.getSetting('capitalizationStyle') + if style == settings.CAPITALIZATION_STYLE_NONE: + return "none" + elif style == settings.CAPITALIZATION_STYLE_ICON: + return "icon" + elif style == settings.CAPITALIZATION_STYLE_SPELL: + return "spell" + else: + return "none" + + @dbus_service.setter + def set_capitalization_style(self, value: str) -> bool: + """Sets the capitalization style.""" + try: + value_lower = value.lower() + if value_lower == "none": + _settings_manager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_NONE) + elif value_lower == "icon": + _settings_manager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_ICON) + elif value_lower == "spell": + _settings_manager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_SPELL) + else: + return False + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting capitalization style: {e}", True) + return False + def decrease_rate(self, script, event=None): """Decreases the speech rate""" @@ -484,6 +636,7 @@ class SpeechAndVerbosityManager: script.presentMessage(full, brief) return True + @dbus_service.command def toggle_speech(self, script, event=None): """Toggles speech.""" @@ -500,6 +653,7 @@ class SpeechAndVerbosityManager: _settings_manager.setSetting('silenceSpeech', True) return True + @dbus_service.command def toggle_verbosity(self, script, event=None): """Toggles speech verbosity level between verbose and brief.""" diff --git a/src/cthulhu/speech_dbus_manager.py b/src/cthulhu/speech_dbus_manager.py new file mode 100644 index 0000000..03a774c --- /dev/null +++ b/src/cthulhu/speech_dbus_manager.py @@ -0,0 +1,515 @@ +#!/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") + + @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) + 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) \ No newline at end of file diff --git a/src/cthulhu/typing_echo_presenter.py b/src/cthulhu/typing_echo_presenter.py new file mode 100644 index 0000000..4a19798 --- /dev/null +++ b/src/cthulhu/typing_echo_presenter.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +# Cthulhu +# +# Copyright 2005-2008 Sun Microsystems Inc. +# Copyright 2011-2025 Igalia, S.L. +# Copyright 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. +# +# 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. + +"""Provides typing echo support with D-Bus controls.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \ + "Copyright (c) 2011-2025 Igalia, S.L." +__license__ = "LGPL" + +import string +from typing import TYPE_CHECKING + +from . import braille +from . import cmdnames +from . import dbus_service +from . import debug +from . import input_event +from . import keybindings +from . import messages +from . import settings +from . import settings_manager +from . import speech + +if TYPE_CHECKING: + from . import default + +_settings_manager = settings_manager.getManager() + +class TypingEchoPresenter: + """Provides typing echo functionality with D-Bus remote control support.""" + + def __init__(self): + """Initialize the typing echo presenter.""" + debug.printMessage(debug.LEVEL_INFO, "TYPING ECHO PRESENTER: Initializing", True) + + # D-Bus getters and setters for key echo settings + @dbus_service.getter + def get_key_echo_enabled(self) -> bool: + """Returns whether echo of key presses is enabled.""" + return _settings_manager.getSetting('enableKeyEcho') + + @dbus_service.setter + def set_key_echo_enabled(self, value: bool) -> bool: + """Sets whether echo of key presses is enabled.""" + try: + _settings_manager.setSetting('enableKeyEcho', value) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting key echo: {e}", True) + return False + + @dbus_service.getter + def get_character_echo_enabled(self) -> bool: + """Returns whether echo of inserted characters is enabled.""" + return _settings_manager.getSetting('enableEchoByCharacter') + + @dbus_service.setter + def set_character_echo_enabled(self, value: bool) -> bool: + """Sets whether echo of inserted characters is enabled.""" + try: + _settings_manager.setSetting('enableEchoByCharacter', value) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting character echo: {e}", True) + return False + + @dbus_service.getter + def get_word_echo_enabled(self) -> bool: + """Returns whether word echo is enabled.""" + return _settings_manager.getSetting('enableEchoByWord') + + @dbus_service.setter + def set_word_echo_enabled(self, value: bool) -> bool: + """Sets whether word echo is enabled.""" + try: + _settings_manager.setSetting('enableEchoByWord', value) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting word echo: {e}", True) + return False + + @dbus_service.getter + def get_sentence_echo_enabled(self) -> bool: + """Returns whether sentence echo is enabled.""" + return _settings_manager.getSetting('enableEchoBySentence') + + @dbus_service.setter + def set_sentence_echo_enabled(self, value: bool) -> bool: + """Sets whether sentence echo is enabled.""" + try: + _settings_manager.setSetting('enableEchoBySentence', value) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting sentence echo: {e}", True) + return False + + @dbus_service.getter + def get_alphabetic_keys_enabled(self) -> bool: + """Returns whether alphabetic keys will be echoed when key echo is enabled.""" + return _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.""" + try: + _settings_manager.setSetting('enableAlphabeticKeys', value) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting alphabetic keys: {e}", True) + return False + + @dbus_service.getter + def get_numeric_keys_enabled(self) -> bool: + """Returns whether numeric keys will be echoed when key echo is enabled.""" + return _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.""" + try: + _settings_manager.setSetting('enableNumericKeys', value) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting numeric keys: {e}", True) + return False + + @dbus_service.getter + def get_punctuation_keys_enabled(self) -> bool: + """Returns whether punctuation keys will be echoed when key echo is enabled.""" + return _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.""" + try: + _settings_manager.setSetting('enablePunctuationKeys', value) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting punctuation keys: {e}", True) + return False + + @dbus_service.getter + def get_space_enabled(self) -> bool: + """Returns whether space key will be echoed when key echo is enabled.""" + return _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.""" + try: + _settings_manager.setSetting('enableSpace', value) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting space key: {e}", True) + return False + + @dbus_service.getter + def get_modifier_keys_enabled(self) -> bool: + """Returns whether modifier keys will be echoed when key echo is enabled.""" + return _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.""" + try: + _settings_manager.setSetting('enableModifierKeys', value) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting modifier keys: {e}", True) + return False + + @dbus_service.getter + def get_function_keys_enabled(self) -> bool: + """Returns whether function keys will be echoed when key echo is enabled.""" + return _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.""" + try: + _settings_manager.setSetting('enableFunctionKeys', value) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting function keys: {e}", True) + return False + + @dbus_service.getter + def get_action_keys_enabled(self) -> bool: + """Returns whether action keys will be echoed when key echo is enabled.""" + return _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.""" + try: + _settings_manager.setSetting('enableActionKeys', value) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting action keys: {e}", True) + return False + + @dbus_service.getter + def get_navigation_keys_enabled(self) -> bool: + """Returns whether navigation keys will be echoed when key echo is enabled.""" + return _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.""" + try: + _settings_manager.setSetting('enableNavigationKeys', value) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting navigation keys: {e}", True) + return False + + @dbus_service.getter + def get_diacritical_keys_enabled(self) -> bool: + """Returns whether diacritical keys will be echoed when key echo is enabled.""" + return _settings_manager.getSetting('enableDiacriticalKeys') + + @dbus_service.setter + def set_diacritical_keys_enabled(self, value: bool) -> bool: + """Sets whether diacritical keys will be echoed when key echo is enabled.""" + try: + _settings_manager.setSetting('enableDiacriticalKeys', value) + return True + except Exception as e: + debug.printMessage(debug.LEVEL_WARNING, f"Error setting diacritical keys: {e}", True) + return False + + @dbus_service.command + def cycle_key_echo(self, script: 'default.Script', event=None): + """Cycles through key echo modes.""" + if not _settings_manager.getSetting('enableKeyEcho'): + _settings_manager.setSetting('enableKeyEcho', True) + script.presentMessage(messages.KEY_ECHO_ENABLED) + else: + _settings_manager.setSetting('enableKeyEcho', False) + script.presentMessage(messages.KEY_ECHO_DISABLED) + return True + + def should_echo_keyboard_event(self, event: input_event.KeyboardEvent) -> bool: + """Returns whether the given keyboard event should be echoed.""" + if not _settings_manager.getSetting('enableKeyEcho'): + return False + + if event.event_string in ["shift", "control", "alt", "meta"]: + return _settings_manager.getSetting('enableModifierKeys') + + if event.event_string.startswith("f") and event.event_string[1:].isdigit(): + return _settings_manager.getSetting('enableFunctionKeys') + + if event.event_string in ["return", "enter", "tab", "escape", "backspace", "delete"]: + return _settings_manager.getSetting('enableActionKeys') + + if event.event_string in ["up", "down", "left", "right", "home", "end", "page_up", "page_down"]: + return _settings_manager.getSetting('enableNavigationKeys') + + if event.event_string == "space": + return _settings_manager.getSetting('enableSpace') + + if len(event.event_string) == 1: + char = event.event_string + if char.isalpha(): + return _settings_manager.getSetting('enableAlphabeticKeys') + elif char.isdigit(): + return _settings_manager.getSetting('enableNumericKeys') + elif char in string.punctuation: + return _settings_manager.getSetting('enablePunctuationKeys') + + return False + + def is_character_echoable(self, event: input_event.KeyboardEvent) -> bool: + """Returns True if the script will echo this event as part of character echo.""" + if not _settings_manager.getSetting('enableEchoByCharacter'): + return False + + # Character echo is for printable characters being inserted + if len(event.event_string) == 1 and event.event_string.isprintable(): + return True + + return False + + def echo_keyboard_event(self, script: 'default.Script', event: input_event.KeyboardEvent) -> None: + """Presents the KeyboardEvent event.""" + if self.should_echo_keyboard_event(event): + if event.event_string == "space": + script.presentMessage(messages.SPACE) + elif event.event_string == "tab": + script.presentMessage(messages.TAB) + elif event.event_string == "return" or event.event_string == "enter": + script.presentMessage(messages.ENTER) + elif event.event_string == "backspace": + script.presentMessage(messages.BACKSPACE) + elif event.event_string == "delete": + script.presentMessage(messages.DELETE) + else: + # For simple characters and other keys, just speak the event string + script.presentMessage(event.event_string) + +# Global instance +_manager = None + +def getManager(): + """Get the typing echo presenter manager.""" + global _manager + if not _manager: + _manager = TypingEchoPresenter() + return _manager \ No newline at end of file