Compare commits
6 Commits
2766f70c5d
...
testing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2092a3e257 | ||
|
|
d46d8de3ee | ||
|
|
75a8447759 | ||
|
|
1650eec768 | ||
|
|
5bb786ef4c | ||
|
|
7f7faa17d3 |
@@ -3,9 +3,9 @@
|
||||
enabled=True
|
||||
|
||||
# Select the driver used to play sounds, choices are genericDriver and gstreamerDriver.
|
||||
# Sox is the default.
|
||||
#driver=gstreamerDriver
|
||||
driver=genericDriver
|
||||
# Gstreamer is the default.
|
||||
driver=gstreamerDriver
|
||||
#driver=genericDriver
|
||||
|
||||
# Sound themes. These are the pack of sounds used for sound alerts.
|
||||
# Sound packs may be located at /usr/share/sounds
|
||||
@@ -217,6 +217,13 @@ list=
|
||||
|
||||
[menu]
|
||||
vmenuPath=
|
||||
# quickMenu: Semicolon-separated list of settings for quick adjustment
|
||||
# Format: section#setting;section#setting;...
|
||||
# Supported settings:
|
||||
# - speech#rate, speech#pitch, speech#volume (0.0-1.0)
|
||||
# - speech#module, speech#voice (speechdDriver only, auto-added)
|
||||
# Note: speech#module and speech#voice are automatically added when
|
||||
# speechdDriver is active. Do not add them manually.
|
||||
quickMenu=speech#rate;speech#pitch;speech#volume
|
||||
|
||||
[prompt]
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
from fenrirscreenreader.core import debug
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _(
|
||||
"TUI focus mode handler - suppresses screen update spam "
|
||||
"for interactive TUI applications"
|
||||
)
|
||||
|
||||
def run(self):
|
||||
# Check if TUI mode is enabled
|
||||
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||
"focus", "tui"
|
||||
):
|
||||
return
|
||||
|
||||
# TUI mode is active - set suppression flag for incoming handler
|
||||
# This prevents the 70000-incoming.py command from announcing
|
||||
# screen updates
|
||||
self.env["commandBuffer"]["tuiSuppressIncoming"] = True
|
||||
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"tui_focus_handler: TUI mode active, suppressing incoming text",
|
||||
debug.DebugLevel.INFO
|
||||
)
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
# Load base configuration class
|
||||
_base_path = os.path.join(os.path.dirname(__file__), "..", "config_base.py")
|
||||
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||
_module = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_module)
|
||||
config_command = _module.config_command
|
||||
|
||||
|
||||
class command(config_command):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_description(self):
|
||||
return "Set keyboard layout to Desktop"
|
||||
|
||||
def run(self):
|
||||
current_layout = self.get_setting("keyboard", "keyboardLayout", "desktop")
|
||||
|
||||
if current_layout.lower() == "desktop":
|
||||
self.present_text("Keyboard layout already set to Desktop")
|
||||
return
|
||||
|
||||
success = self.set_setting("keyboard", "keyboardLayout", "desktop")
|
||||
|
||||
if success:
|
||||
self.present_text("Keyboard layout set to Desktop")
|
||||
self.present_text("Please restart Fenrir for this change to take effect")
|
||||
self.play_sound("Accept")
|
||||
else:
|
||||
self.present_text("Failed to change keyboard layout")
|
||||
self.play_sound("Error")
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
# Load base configuration class
|
||||
_base_path = os.path.join(os.path.dirname(__file__), "..", "config_base.py")
|
||||
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||
_module = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_module)
|
||||
config_command = _module.config_command
|
||||
|
||||
|
||||
class command(config_command):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_description(self):
|
||||
return "Set keyboard layout to Laptop"
|
||||
|
||||
def run(self):
|
||||
current_layout = self.get_setting("keyboard", "keyboardLayout", "desktop")
|
||||
|
||||
if current_layout.lower() == "laptop":
|
||||
self.present_text("Keyboard layout already set to Laptop")
|
||||
return
|
||||
|
||||
success = self.set_setting("keyboard", "keyboardLayout", "laptop")
|
||||
|
||||
if success:
|
||||
self.present_text("Keyboard layout set to Laptop")
|
||||
self.present_text("Please restart Fenrir for this change to take effect")
|
||||
self.play_sound("Accept")
|
||||
else:
|
||||
self.present_text("Failed to change keyboard layout")
|
||||
self.play_sound("Error")
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
# Load base configuration class
|
||||
_base_path = os.path.join(os.path.dirname(__file__), "..", "config_base.py")
|
||||
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||
_module = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_module)
|
||||
config_command = _module.config_command
|
||||
|
||||
|
||||
class command(config_command):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_description(self):
|
||||
return "Set keyboard layout to PTY (terminal emulation)"
|
||||
|
||||
def run(self):
|
||||
current_layout = self.get_setting("keyboard", "keyboardLayout", "desktop")
|
||||
|
||||
if current_layout.lower() == "pty":
|
||||
self.present_text("Keyboard layout already set to PTY")
|
||||
return
|
||||
|
||||
success = self.set_setting("keyboard", "keyboardLayout", "pty")
|
||||
|
||||
if success:
|
||||
self.present_text("Keyboard layout set to PTY for terminal emulation")
|
||||
self.present_text("Please restart Fenrir for this change to take effect")
|
||||
self.play_sound("Accept")
|
||||
else:
|
||||
self.present_text("Failed to change keyboard layout")
|
||||
self.play_sound("Error")
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
# Load base configuration class
|
||||
_base_path = os.path.join(os.path.dirname(__file__), "..", "config_base.py")
|
||||
_spec = importlib.util.spec_from_file_location("config_base", _base_path)
|
||||
_module = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_module)
|
||||
config_command = _module.config_command
|
||||
|
||||
|
||||
class command(config_command):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_description(self):
|
||||
return "Set keyboard layout to PTY2 (alternative terminal layout)"
|
||||
|
||||
def run(self):
|
||||
current_layout = self.get_setting("keyboard", "keyboardLayout", "desktop")
|
||||
|
||||
if current_layout.lower() == "pty2":
|
||||
self.present_text("Keyboard layout already set to PTY2")
|
||||
return
|
||||
|
||||
success = self.set_setting("keyboard", "keyboardLayout", "pty2")
|
||||
|
||||
if success:
|
||||
self.present_text("Keyboard layout set to PTY2 alternative terminal layout")
|
||||
self.present_text("Please restart Fenrir for this change to take effect")
|
||||
self.play_sound("Accept")
|
||||
else:
|
||||
self.present_text("Failed to change keyboard layout")
|
||||
self.play_sound("Error")
|
||||
203
src/fenrirscreenreader/core/dynamicKeyboardLayoutMenu.py
Normal file
203
src/fenrirscreenreader/core/dynamicKeyboardLayoutMenu.py
Normal file
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
from fenrirscreenreader.core import debug
|
||||
|
||||
|
||||
class DynamicKeyboardLayoutCommand:
|
||||
"""Dynamic command class for keyboard layout selection"""
|
||||
|
||||
def __init__(self, layoutName, layoutPath, env):
|
||||
self.layoutName = layoutName
|
||||
self.layoutPath = layoutPath
|
||||
# Extract just the base name without extension for comparison
|
||||
self.layoutBaseName = layoutName
|
||||
self.env = env
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return f"Set keyboard layout to {self.layoutName}"
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
settingsManager = self.env["runtime"]["SettingsManager"]
|
||||
currentLayout = settingsManager.get_setting(
|
||||
"keyboard", "keyboardLayout"
|
||||
)
|
||||
|
||||
# Check if already set (compare both full path and base name)
|
||||
currentBaseName = os.path.splitext(os.path.basename(currentLayout))[0] if currentLayout else ""
|
||||
if currentBaseName.lower() == self.layoutBaseName.lower() or currentLayout.lower() == self.layoutPath.lower():
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
f"Keyboard layout already set to {self.layoutName}"
|
||||
)
|
||||
return
|
||||
|
||||
# Set the new layout in the config file using full path
|
||||
try:
|
||||
# Update the setting in memory
|
||||
settingsManager.set_setting(
|
||||
"keyboard", "keyboardLayout", self.layoutPath
|
||||
)
|
||||
|
||||
# Save to the actual config file
|
||||
configFilePath = settingsManager.get_settings_file()
|
||||
settingsManager.save_settings(configFilePath)
|
||||
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
f"Keyboard layout set to {self.layoutName}. Please restart Fenrir for this change to take effect."
|
||||
)
|
||||
# Play accept sound
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
"", sound_icon="Accept", interrupt=False
|
||||
)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
f"Failed to change keyboard layout to {self.layoutName}"
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
f"DynamicKeyboardLayout: Error setting layout {self.layoutName}: {e}",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
# Play error sound
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
"", sound_icon="ErrorSound", interrupt=False
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
f"Keyboard layout change error: {str(e)}", interrupt=True
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
f"DynamicKeyboardLayout: Unexpected error for {self.layoutName}: {e}",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
|
||||
|
||||
def add_dynamic_keyboard_layout_menus(VmenuManager):
|
||||
"""Add dynamic keyboard layout menus to vmenu system"""
|
||||
try:
|
||||
env = VmenuManager.env
|
||||
|
||||
# Get keyboard layout files
|
||||
layouts = get_keyboard_layouts(env)
|
||||
if not layouts:
|
||||
return
|
||||
|
||||
# Create keyboard layouts submenu
|
||||
layoutMenu = {}
|
||||
|
||||
# Add layout commands
|
||||
for layoutName, layoutPath in layouts:
|
||||
layoutCommand = DynamicKeyboardLayoutCommand(
|
||||
layoutName, layoutPath, env
|
||||
)
|
||||
layoutMenu[f"{layoutName} Action"] = layoutCommand
|
||||
|
||||
# Find keyboard menu in existing vmenu structure
|
||||
# If fenrir menu exists, add layouts under it
|
||||
if "fenrir Menu" in VmenuManager.menuDict:
|
||||
fenrirMenu = VmenuManager.menuDict["fenrir Menu"]
|
||||
if "keyboard Menu" in fenrirMenu:
|
||||
# Add dynamic layouts to existing keyboard menu
|
||||
keyboardMenu = fenrirMenu["keyboard Menu"]
|
||||
keyboardMenu["Keyboard Layouts Menu"] = layoutMenu
|
||||
else:
|
||||
# Create keyboard menu with layouts
|
||||
fenrirMenu["keyboard Menu"] = {
|
||||
"Keyboard Layouts Menu": layoutMenu
|
||||
}
|
||||
else:
|
||||
# Create standalone keyboard layouts menu
|
||||
VmenuManager.menuDict["Keyboard Layouts Menu"] = layoutMenu
|
||||
|
||||
except Exception as e:
|
||||
# Use debug manager for error logging
|
||||
if "DebugManager" in env.get("runtime", {}):
|
||||
env["runtime"]["DebugManager"].write_debug_out(
|
||||
f"Error creating dynamic keyboard layout menus: {e}",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
else:
|
||||
print(f"Error creating dynamic keyboard layout menus: {e}")
|
||||
|
||||
|
||||
def get_keyboard_layouts(env):
|
||||
"""Get available keyboard layouts from keyboard directory"""
|
||||
layouts = []
|
||||
|
||||
try:
|
||||
# Get keyboard directory paths
|
||||
keyboardDirs = []
|
||||
|
||||
# Check system installation path
|
||||
systemKeyboardPath = "/etc/fenrirscreenreader/keyboard/"
|
||||
if os.path.exists(systemKeyboardPath):
|
||||
keyboardDirs.append(systemKeyboardPath)
|
||||
|
||||
# Check source/development path
|
||||
try:
|
||||
import fenrirscreenreader
|
||||
|
||||
fenrirPath = os.path.dirname(fenrirscreenreader.__file__)
|
||||
devKeyboardPath = os.path.join(
|
||||
fenrirPath, "..", "..", "config", "keyboard"
|
||||
)
|
||||
devKeyboardPath = os.path.abspath(devKeyboardPath)
|
||||
if os.path.exists(devKeyboardPath):
|
||||
keyboardDirs.append(devKeyboardPath)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Get current layout setting path
|
||||
try:
|
||||
currentLayoutSetting = env["runtime"]["SettingsManager"].get_setting(
|
||||
"keyboard", "keyboardLayout"
|
||||
)
|
||||
if currentLayoutSetting and os.path.exists(currentLayoutSetting):
|
||||
currentLayoutDir = os.path.dirname(currentLayoutSetting)
|
||||
if currentLayoutDir not in keyboardDirs:
|
||||
keyboardDirs.append(currentLayoutDir)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Scan for .conf files
|
||||
seenLayouts = set()
|
||||
for keyboardDir in keyboardDirs:
|
||||
try:
|
||||
confFiles = glob.glob(os.path.join(keyboardDir, "*.conf"))
|
||||
for confFile in confFiles:
|
||||
layoutName = os.path.splitext(os.path.basename(confFile))[
|
||||
0
|
||||
]
|
||||
if layoutName not in seenLayouts:
|
||||
seenLayouts.add(layoutName)
|
||||
layouts.append((layoutName, confFile))
|
||||
except Exception as e:
|
||||
if "DebugManager" in env.get("runtime", {}):
|
||||
env["runtime"]["DebugManager"].write_debug_out(
|
||||
f"Error scanning keyboard directory {keyboardDir}: {e}",
|
||||
debug.DebugLevel.WARNING,
|
||||
)
|
||||
|
||||
# Sort layouts alphabetically
|
||||
layouts.sort(key=lambda x: x[0].lower())
|
||||
|
||||
except Exception as e:
|
||||
if "DebugManager" in env.get("runtime", {}):
|
||||
env["runtime"]["DebugManager"].write_debug_out(
|
||||
f"Error getting keyboard layouts: {e}",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
|
||||
return layouts
|
||||
@@ -7,25 +7,285 @@
|
||||
from fenrirscreenreader.core import debug
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
from fenrirscreenreader.core.settingsData import settings_data
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
class QuickMenuManager:
|
||||
class SpeechHelperMixin:
|
||||
"""Helper methods for querying speech-dispatcher modules and voices.
|
||||
|
||||
Provides caching and query functionality for speech-dispatcher module
|
||||
and voice enumeration, reusing proven logic from voice_browser.py.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._modules_cache = None
|
||||
self._voices_cache = {} # {module_name: [voice_list]}
|
||||
self._cache_timestamp = 0
|
||||
self._cache_timeout = 300 # 5 minutes
|
||||
|
||||
def get_speechd_modules(self):
|
||||
"""Get available speech-dispatcher modules (cached).
|
||||
|
||||
Returns:
|
||||
list: Available module names (e.g., ['espeak-ng', 'festival'])
|
||||
"""
|
||||
now = time.time()
|
||||
|
||||
# Return cached if valid
|
||||
if (self._modules_cache and
|
||||
(now - self._cache_timestamp) < self._cache_timeout):
|
||||
return self._modules_cache
|
||||
|
||||
# Query spd-say
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["spd-say", "-O"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=8
|
||||
)
|
||||
if result.returncode == 0:
|
||||
lines = result.stdout.strip().split("\n")
|
||||
self._modules_cache = [
|
||||
line.strip() for line in lines[1:]
|
||||
if line.strip() and line.strip().lower() != "dummy"
|
||||
]
|
||||
self._cache_timestamp = now
|
||||
return self._modules_cache
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
(f"QuickMenuManager get_speechd_modules: "
|
||||
f"Error querying modules: {e}"),
|
||||
debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
return []
|
||||
|
||||
def get_module_voices(self, module):
|
||||
"""Get voices for a specific module (cached per-module).
|
||||
|
||||
Args:
|
||||
module (str): Module name (e.g., 'espeak-ng')
|
||||
|
||||
Returns:
|
||||
list: Available voice names for this module
|
||||
"""
|
||||
# Return cached if available
|
||||
if module in self._voices_cache:
|
||||
return self._voices_cache[module]
|
||||
|
||||
# Query spd-say
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["spd-say", "-o", module, "-L"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=8
|
||||
)
|
||||
if result.returncode == 0:
|
||||
lines = result.stdout.strip().split("\n")
|
||||
voices = []
|
||||
for line in lines[1:]:
|
||||
if not line.strip():
|
||||
continue
|
||||
if module.lower() == "espeak-ng":
|
||||
voice = self._process_espeak_voice(line)
|
||||
if voice:
|
||||
voices.append(voice)
|
||||
elif module.lower() == "voxin":
|
||||
# For Voxin, store voice name with language
|
||||
voice_data = self._process_voxin_voice(line)
|
||||
if voice_data:
|
||||
voices.append(voice_data)
|
||||
else:
|
||||
# For other modules, extract first field (voice name)
|
||||
parts = line.strip().split()
|
||||
if parts:
|
||||
voices.append(parts[0])
|
||||
|
||||
self._voices_cache[module] = voices
|
||||
return voices
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
(f"QuickMenuManager get_module_voices: "
|
||||
f"Error querying voices for {module}: {e}"),
|
||||
debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
return []
|
||||
|
||||
def _process_espeak_voice(self, voice_line):
|
||||
"""Process espeak-ng voice format into usable voice name.
|
||||
|
||||
Args:
|
||||
voice_line (str): Raw line from spd-say -L output
|
||||
|
||||
Returns:
|
||||
str: Processed voice name (e.g., 'en-us' or 'en-us+f3')
|
||||
"""
|
||||
parts = [p for p in voice_line.split() if p]
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
lang_code = parts[-2].lower()
|
||||
variant = parts[-1].lower()
|
||||
return (f"{lang_code}+{variant}"
|
||||
if variant and variant != "none" else lang_code)
|
||||
|
||||
def _process_voxin_voice(self, voice_line):
|
||||
"""Process Voxin voice format with language information.
|
||||
|
||||
Args:
|
||||
voice_line (str): Raw line from spd-say -o voxin -L output
|
||||
Format: NAME LANGUAGE VARIANT
|
||||
|
||||
Returns:
|
||||
str: Voice name with language encoded (e.g., 'daniel-embedded-high|en-GB')
|
||||
"""
|
||||
parts = [p for p in voice_line.split() if p]
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
voice_name = parts[0]
|
||||
language = parts[1]
|
||||
# Encode language with voice for later extraction
|
||||
return f"{voice_name}|{language}"
|
||||
|
||||
def _select_default_voice(self, voices):
|
||||
"""Select a sensible default voice from list, preferring user's
|
||||
language.
|
||||
|
||||
Args:
|
||||
voices (list): List of available voice names
|
||||
|
||||
Returns:
|
||||
str: Selected default voice (matches user language if possible)
|
||||
"""
|
||||
if not voices:
|
||||
return ""
|
||||
|
||||
# Get current voice to preserve language preference
|
||||
current_voice = self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"speech", "voice"
|
||||
)
|
||||
|
||||
# Get configured language from settings
|
||||
configured_lang = self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"speech", "language"
|
||||
)
|
||||
|
||||
# Extract language code from current voice if available
|
||||
current_lang = None
|
||||
if current_voice:
|
||||
# Extract language code (e.g., 'en-gb' from 'en-gb+male')
|
||||
current_lang = current_voice.split('+')[0].lower()
|
||||
|
||||
# Build preference list: current language, configured language, English
|
||||
preferences = []
|
||||
if current_lang:
|
||||
preferences.append(current_lang)
|
||||
if configured_lang:
|
||||
preferences.append(configured_lang.lower())
|
||||
preferences.extend(['en-gb', 'en-us', 'en'])
|
||||
|
||||
# Remove duplicates while preserving order
|
||||
seen = set()
|
||||
preferences = [x for x in preferences
|
||||
if not (x in seen or seen.add(x))]
|
||||
|
||||
# Try exact matches for preferred languages
|
||||
for pref in preferences:
|
||||
for voice in voices:
|
||||
# Extract language if voice is in "name|lang" format
|
||||
voice_to_check = voice
|
||||
if "|" in voice:
|
||||
_, voice_lang = voice.split("|", 1)
|
||||
voice_to_check = voice_lang
|
||||
if voice_to_check.lower() == pref:
|
||||
return voice
|
||||
|
||||
# Try voices starting with preferred language codes
|
||||
for pref in preferences:
|
||||
for voice in voices:
|
||||
# Extract language if voice is in "name|lang" format
|
||||
voice_to_check = voice
|
||||
if "|" in voice:
|
||||
_, voice_lang = voice.split("|", 1)
|
||||
voice_to_check = voice_lang
|
||||
if voice_to_check.lower().startswith(pref):
|
||||
return voice
|
||||
|
||||
# Fall back to first available voice
|
||||
return voices[0]
|
||||
|
||||
def invalidate_speech_cache(self):
|
||||
"""Clear cached module and voice data."""
|
||||
self._modules_cache = None
|
||||
self._voices_cache = {}
|
||||
self._cache_timestamp = 0
|
||||
|
||||
|
||||
class QuickMenuManager(SpeechHelperMixin):
|
||||
def __init__(self):
|
||||
SpeechHelperMixin.__init__(self)
|
||||
self.position = 0
|
||||
self.quickMenu = []
|
||||
self.settings = settings_data
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
self.load_menu(
|
||||
self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"menu", "quickMenu"
|
||||
)
|
||||
|
||||
# Load base menu from config
|
||||
menu_string = self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"menu", "quickMenu"
|
||||
)
|
||||
|
||||
# Dynamically add speech-dispatcher specific items
|
||||
if self._is_speechd_driver_active():
|
||||
menu_string = self._add_speechd_menu_items(menu_string)
|
||||
|
||||
self.load_menu(menu_string)
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def _is_speechd_driver_active(self):
|
||||
"""Check if speechdDriver is currently active.
|
||||
|
||||
Returns:
|
||||
bool: True if speech driver is speechdDriver
|
||||
"""
|
||||
try:
|
||||
driver = self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"speech", "driver"
|
||||
)
|
||||
return driver == "speechdDriver"
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
(f"QuickMenuManager _is_speechd_driver_active: "
|
||||
f"Error checking driver: {e}"),
|
||||
debug.DebugLevel.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
def _add_speechd_menu_items(self, menu_string):
|
||||
"""Add speech-dispatcher module and voice to quick menu.
|
||||
|
||||
Args:
|
||||
menu_string (str): Existing menu string from config
|
||||
|
||||
Returns:
|
||||
str: Updated menu string with module and voice added
|
||||
"""
|
||||
if not menu_string:
|
||||
return "speech#module;speech#voice"
|
||||
|
||||
# Check if already present (user manually added)
|
||||
if "speech#module" in menu_string or "speech#voice" in menu_string:
|
||||
return menu_string
|
||||
|
||||
# Add module and voice after existing items
|
||||
return f"{menu_string};speech#module;speech#voice"
|
||||
|
||||
def load_menu(self, menuString):
|
||||
self.position = 0
|
||||
self.quickMenu = []
|
||||
@@ -86,8 +346,15 @@ class QuickMenuManager:
|
||||
|
||||
try:
|
||||
if isinstance(self.settings[section][setting], str):
|
||||
value = str(value_string)
|
||||
return False
|
||||
# Check for special string cycling cases
|
||||
if section == "speech" and setting == "module":
|
||||
return self.cycle_speech_module("next")
|
||||
elif section == "speech" and setting == "voice":
|
||||
return self.cycle_speech_voice("next")
|
||||
else:
|
||||
# Generic strings not supported for cycling
|
||||
value = str(value_string)
|
||||
return False
|
||||
elif isinstance(self.settings[section][setting], bool):
|
||||
if value_string not in ["True", "False"]:
|
||||
return False
|
||||
@@ -132,8 +399,15 @@ class QuickMenuManager:
|
||||
return False
|
||||
try:
|
||||
if isinstance(self.settings[section][setting], str):
|
||||
value = str(value_string)
|
||||
return False
|
||||
# Check for special string cycling cases
|
||||
if section == "speech" and setting == "module":
|
||||
return self.cycle_speech_module("prev")
|
||||
elif section == "speech" and setting == "voice":
|
||||
return self.cycle_speech_voice("prev")
|
||||
else:
|
||||
# Generic strings not supported for cycling
|
||||
value = str(value_string)
|
||||
return False
|
||||
elif isinstance(self.settings[section][setting], bool):
|
||||
if value_string not in ["True", "False"]:
|
||||
return False
|
||||
@@ -161,6 +435,199 @@ class QuickMenuManager:
|
||||
return False
|
||||
return True
|
||||
|
||||
def cycle_speech_module(self, direction):
|
||||
"""Cycle to next/previous speech-dispatcher module.
|
||||
|
||||
Args:
|
||||
direction (str): 'next' or 'prev'
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Get available modules
|
||||
modules = self.get_speechd_modules()
|
||||
if not modules:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
"No modules available", interrupt=True
|
||||
)
|
||||
return False
|
||||
|
||||
# Get current module
|
||||
current_module = self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"speech", "module"
|
||||
)
|
||||
|
||||
# Find current index
|
||||
try:
|
||||
current_index = (modules.index(current_module)
|
||||
if current_module else 0)
|
||||
except ValueError:
|
||||
current_index = 0
|
||||
|
||||
# Cycle to next/previous
|
||||
if direction == "next":
|
||||
new_index = (current_index + 1) % len(modules)
|
||||
else: # prev
|
||||
new_index = (current_index - 1) % len(modules)
|
||||
|
||||
new_module = modules[new_index]
|
||||
|
||||
# Update setting (runtime only)
|
||||
self.env["runtime"]["SettingsManager"].set_setting(
|
||||
"speech", "module", new_module
|
||||
)
|
||||
|
||||
# Select sensible default voice for new module
|
||||
voices = self.get_module_voices(new_module)
|
||||
if voices:
|
||||
default_voice = self._select_default_voice(voices)
|
||||
|
||||
# Parse voice name and language for modules like Voxin
|
||||
voice_name = default_voice
|
||||
voice_lang = None
|
||||
if "|" in default_voice:
|
||||
voice_name, voice_lang = default_voice.split("|", 1)
|
||||
|
||||
self.env["runtime"]["SettingsManager"].set_setting(
|
||||
"speech", "voice", voice_name
|
||||
)
|
||||
|
||||
# Apply voice to speech driver immediately
|
||||
if "SpeechDriver" in self.env["runtime"]:
|
||||
try:
|
||||
self.env["runtime"]["SpeechDriver"].set_module(
|
||||
new_module
|
||||
)
|
||||
# Set language first if available
|
||||
if voice_lang:
|
||||
self.env["runtime"]["SpeechDriver"].set_language(
|
||||
voice_lang
|
||||
)
|
||||
# Then set voice
|
||||
self.env["runtime"]["SpeechDriver"].set_voice(
|
||||
voice_name
|
||||
)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
(f"QuickMenuManager cycle_speech_module: "
|
||||
f"Error applying voice: {e}"),
|
||||
debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
# Announce new module
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
new_module, interrupt=True
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
f"QuickMenuManager cycle_speech_module: Error: {e}",
|
||||
debug.DebugLevel.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
def cycle_speech_voice(self, direction):
|
||||
"""Cycle to next/previous voice for current module.
|
||||
|
||||
Args:
|
||||
direction (str): 'next' or 'prev'
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Get current module
|
||||
current_module = self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"speech", "module"
|
||||
)
|
||||
|
||||
if not current_module:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
"No module selected", interrupt=True
|
||||
)
|
||||
return False
|
||||
|
||||
# Get available voices for this module
|
||||
voices = self.get_module_voices(current_module)
|
||||
if not voices:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
f"No voices for module {current_module}", interrupt=True
|
||||
)
|
||||
return False
|
||||
|
||||
# Get current voice
|
||||
current_voice = self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"speech", "voice"
|
||||
)
|
||||
|
||||
# Find current index (handle Voxin voice|language format)
|
||||
current_index = 0
|
||||
if current_voice:
|
||||
try:
|
||||
# Try exact match first
|
||||
current_index = voices.index(current_voice)
|
||||
except ValueError:
|
||||
# For Voxin, compare just the voice name part
|
||||
for i, voice in enumerate(voices):
|
||||
voice_name = voice.split("|")[0] if "|" in voice else voice
|
||||
if voice_name == current_voice:
|
||||
current_index = i
|
||||
break
|
||||
|
||||
# Cycle to next/previous
|
||||
if direction == "next":
|
||||
new_index = (current_index + 1) % len(voices)
|
||||
else: # prev
|
||||
new_index = (current_index - 1) % len(voices)
|
||||
|
||||
new_voice = voices[new_index]
|
||||
|
||||
# Parse voice name and language for modules like Voxin
|
||||
voice_name = new_voice
|
||||
voice_lang = None
|
||||
if "|" in new_voice:
|
||||
# Format: "voicename|language" (e.g., "daniel-embedded-high|en-GB")
|
||||
voice_name, voice_lang = new_voice.split("|", 1)
|
||||
|
||||
# Update setting (runtime only) - store the voice name only
|
||||
self.env["runtime"]["SettingsManager"].set_setting(
|
||||
"speech", "voice", voice_name
|
||||
)
|
||||
|
||||
# Apply voice to speech driver immediately
|
||||
if "SpeechDriver" in self.env["runtime"]:
|
||||
try:
|
||||
# Set language first if available
|
||||
if voice_lang:
|
||||
self.env["runtime"]["SpeechDriver"].set_language(
|
||||
voice_lang
|
||||
)
|
||||
# Then set voice
|
||||
self.env["runtime"]["SpeechDriver"].set_voice(voice_name)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
(f"QuickMenuManager cycle_speech_voice: "
|
||||
f"Error applying voice: {e}"),
|
||||
debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
# Announce new voice (voice name only, not language)
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
voice_name, interrupt=True
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
f"QuickMenuManager cycle_speech_voice: Error: {e}",
|
||||
debug.DebugLevel.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
def get_current_entry(self):
|
||||
if len(self.quickMenu) == 0:
|
||||
return ""
|
||||
|
||||
@@ -217,6 +217,16 @@ class VmenuManager:
|
||||
except Exception as e:
|
||||
print(f"Error adding dynamic voice menus: {e}")
|
||||
|
||||
# Add dynamic keyboard layout menus
|
||||
try:
|
||||
from fenrirscreenreader.core.dynamicKeyboardLayoutMenu import (
|
||||
add_dynamic_keyboard_layout_menus,
|
||||
)
|
||||
|
||||
add_dynamic_keyboard_layout_menus(self)
|
||||
except Exception as e:
|
||||
print(f"Error adding dynamic keyboard layout menus: {e}")
|
||||
|
||||
# index still valid?
|
||||
if self.curr_index is not None:
|
||||
try:
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
version = "2025.11.24"
|
||||
code_name = "master"
|
||||
version = "2025.12.02"
|
||||
code_name = "testing"
|
||||
|
||||
Reference in New Issue
Block a user