Ported over Orca d-bus remote improvements.

This commit is contained in:
Storm Dragon
2025-09-16 22:44:29 -04:00
parent 928bae6d86
commit 10d94792ed
11 changed files with 1505 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
from .plugin import SpeechHistory

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,332 @@
#!/usr/bin/env python3
# Cthulhu
#
# Copyright 2005-2008 Sun Microsystems Inc.
# Copyright 2011-2025 Igalia, S.L.
# Copyright 2025 Stormux <storm_dragon@stormux.org>
#
# 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