Merge branch 'testing'

# Conflicts:
#	distro-packages/Arch-Linux/PKGBUILD
#	meson.build
#	src/cthulhu/cthulhuVersion.py
This commit is contained in:
Storm Dragon
2026-03-02 15:37:50 -05:00
18 changed files with 1127 additions and 92 deletions
-8
View File
@@ -8,13 +8,11 @@ set -e # Exit on error
# Colors for output (only if stdout is a terminal) # Colors for output (only if stdout is a terminal)
if [[ -t 1 ]]; then if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
BLUE='\033[0;34m' BLUE='\033[0;34m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
else else
RED=''
GREEN='' GREEN=''
YELLOW='' YELLOW=''
BLUE='' BLUE=''
@@ -56,12 +54,6 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then
# Remove data files # Remove data files
# Remove desktop files
if [[ -f "$HOME/.local/share/applications/cthulhu-autostart.desktop" ]]; then
rm -f "$HOME/.local/share/applications/cthulhu-autostart.desktop"
echo " Removed: ~/.local/share/applications/cthulhu-autostart.desktop"
fi
# Remove icons # Remove icons
for size in 16x16 22x22 24x24 32x32 48x48 256x256 scalable symbolic; do for size in 16x16 22x22 24x24 32x32 48x48 256x256 scalable symbolic; do
icon_path="$HOME/.local/share/icons/hicolor/$size/apps" icon_path="$HOME/.local/share/icons/hicolor/$size/apps"
+1 -1
View File
@@ -1,7 +1,7 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org> # Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=cthulhu pkgname=cthulhu
pkgver=2026.02.18 pkgver=2026.02.22
pkgrel=1 pkgrel=1
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca" pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
url="https://git.stormux.org/storm/cthulhu" url="https://git.stormux.org/storm/cthulhu"
+1 -11
View File
@@ -1,5 +1,5 @@
project('cthulhu', project('cthulhu',
version: '2026.02.18-master', version: '2026.02.22-master',
meson_version: '>= 1.0.0', meson_version: '>= 1.0.0',
) )
@@ -93,16 +93,6 @@ else
summary += {'sound support': 'no (missing gstreamer)'} summary += {'sound support': 'no (missing gstreamer)'}
endif endif
# Integration with session startup
i18n.merge_file(
input: 'cthulhu-autostart.desktop.in',
output: '@BASENAME@',
type: 'desktop',
po_dir: meson.project_source_root() / 'po',
install: true,
install_dir: get_option('sysconfdir') / 'xdg' / 'autostart',
)
# Update icon cache manually (desktop-neutral) - optional, ignore failures # Update icon cache manually (desktop-neutral) - optional, ignore failures
gtk_update_icon_cache = find_program('gtk4-update-icon-cache', required: false) gtk_update_icon_cache = find_program('gtk4-update-icon-cache', required: false)
if gtk_update_icon_cache.found() if gtk_update_icon_cache.found()
-1
View File
@@ -1,6 +1,5 @@
# List of source files containing translatable strings. # List of source files containing translatable strings.
# Please keep this file sorted alphabetically. # Please keep this file sorted alphabetically.
cthulhu-autostart.desktop.in
src/cthulhu/braille_rolenames.py src/cthulhu/braille_rolenames.py
src/cthulhu/brltablenames.py src/cthulhu/brltablenames.py
src/cthulhu/chnames.py src/cthulhu/chnames.py
+48 -2
View File
@@ -85,12 +85,16 @@ class Backend:
with open(fileName, 'w', encoding='utf-8') as settingsFile: with open(fileName, 'w', encoding='utf-8') as settingsFile:
settingsFile.write(dumps(prefsDoc)) settingsFile.write(dumps(prefsDoc))
def _updateTable(self, targetTable, newValues): def _updateTable(self, targetTable, newValues, preserveMissingKeys=None):
if not isinstance(newValues, dict): if not isinstance(newValues, dict):
return return
preserveMissingKeys = set(preserveMissingKeys or [])
for key in list(targetTable.keys()): for key in list(targetTable.keys()):
if key not in newValues: if key not in newValues:
if key in preserveMissingKeys:
continue
del targetTable[key] del targetTable[key]
continue continue
newValue = newValues[key] newValue = newValues[key]
@@ -173,7 +177,12 @@ class Backend:
profiles[profile] = {} profiles[profile] = {}
profileTable = profiles[profile] profileTable = profiles[profile]
self._updateTable(profileTable, general) # Keep plugin persistence keys when callers provide partial updates.
self._updateTable(
profileTable,
general,
preserveMissingKeys={"activePlugins", "pluginSources"},
)
self._writeDocument(self.settingsFile, prefsDoc) self._writeDocument(self.settingsFile, prefsDoc)
def _getSettings(self): def _getSettings(self):
@@ -213,12 +222,33 @@ class Backend:
return settingsDict return settingsDict
def _hasLikelyCustomEchoVoice(self, echoVoice, defaultVoice):
"""Return True when echoVoice appears intentionally customized."""
if not echoVoice:
return False
echoEstablished = bool(echoVoice.get('established', False))
if not defaultVoice:
return echoEstablished or bool(echoVoice.get(acss.ACSS.FAMILY))
for key in [acss.ACSS.RATE, acss.ACSS.AVERAGE_PITCH, acss.ACSS.GAIN, acss.ACSS.FAMILY]:
echoValue = echoVoice.get(key)
defaultValue = defaultVoice.get(key)
if echoValue is not None and echoValue != defaultValue:
return True
return echoEstablished and bool(echoVoice.get(acss.ACSS.FAMILY))
def getGeneral(self, profile=None): def getGeneral(self, profile=None):
""" Get general settings from default settings and """ Get general settings from default settings and
override with profile values. """ override with profile values. """
self._getSettings() self._getSettings()
generalSettings = self.general.copy() generalSettings = self.general.copy()
generalSettings = self._migrateSettings(generalSettings) generalSettings = self._migrateSettings(generalSettings)
# Plugin state is profile-scoped; ignore legacy/global values.
generalSettings.pop('activePlugins', None)
generalSettings.pop('pluginSources', None)
defaultProfile = generalSettings.get('startingProfile', defaultProfile = generalSettings.get('startingProfile',
['Default', 'default']) ['Default', 'default'])
if profile is None: if profile is None:
@@ -229,8 +259,24 @@ class Backend:
if key == 'voices': if key == 'voices':
for voiceType, voiceDef in value.items(): for voiceType, voiceDef in value.items():
value[voiceType] = acss.ACSS(voiceDef) value[voiceType] = acss.ACSS(voiceDef)
if key == 'echoVoice' and isinstance(value, dict):
value = acss.ACSS(value)
if key not in ['startingProfile', 'activeProfile']: if key not in ['startingProfile', 'activeProfile']:
generalSettings[key] = value generalSettings[key] = value
# Backward compatibility: recover custom echo behavior when legacy
# profiles have echoVoice but are missing custom-echo toggle keys.
if 'useCustomEchoVoice' not in profileSettings:
voices = generalSettings.get('voices') or {}
defaultVoice = voices.get(settings.DEFAULT_VOICE)
echoVoice = generalSettings.get('echoVoice')
if self._hasLikelyCustomEchoVoice(echoVoice, defaultVoice):
generalSettings['useCustomEchoVoice'] = True
if 'useCustomEchoForKey' not in profileSettings:
generalSettings['useCustomEchoForKey'] = True
if 'useCustomEchoForCharacter' not in profileSettings:
generalSettings['useCustomEchoForCharacter'] = True
try: try:
generalSettings['activeProfile'] = profileSettings['profile'] generalSettings['activeProfile'] = profileSettings['profile']
except KeyError: except KeyError:
+310 -3
View File
@@ -136,6 +136,24 @@
<property name="step_increment">0.10000000149</property> <property name="step_increment">0.10000000149</property>
<property name="page_increment">1</property> <property name="page_increment">1</property>
</object> </object>
<object class="GtkAdjustment" id="echoPitchAdjustment">
<property name="upper">10</property>
<property name="value">5</property>
<property name="step_increment">0.10000000149</property>
<property name="page_increment">1</property>
</object>
<object class="GtkAdjustment" id="echoRateAdjustment">
<property name="upper">100</property>
<property name="value">50</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkAdjustment" id="echoVolumeAdjustment">
<property name="upper">10</property>
<property name="value">10</property>
<property name="step_increment">0.10000000149</property>
<property name="page_increment">1</property>
</object>
<object class="GtkDialog" id="cthulhuSetupWindow"> <object class="GtkDialog" id="cthulhuSetupWindow">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="title" translatable="yes">Cthulhu Preferences</property> <property name="title" translatable="yes">Cthulhu Preferences</property>
@@ -3037,6 +3055,295 @@
<property name="top_attach">1</property> <property name="top_attach">1</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkFrame" id="echoVoiceFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="echoVoiceAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkGrid" id="echoVoiceGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">10</property>
<property name="column_spacing">8</property>
<child>
<object class="GtkCheckButton" id="useCustomEchoVoiceCheckButton">
<property name="label" translatable="yes">Use c_ustom echo voice settings</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="useCustomEchoVoiceToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoSpeechServerCheckButton">
<property name="label" translatable="yes">Use custom _speech-dispatcher module for echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="useCustomEchoSpeechServerToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoSpeechServersLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo _module:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoSpeechServers</property>
<accessibility>
<relation type="label-for" target="echoSpeechServers"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="echoSpeechServers">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="changed" handler="echoSpeechServersChanged" swapped="no"/>
<accessibility>
<relation type="labelled-by" target="echoSpeechServersLabel"/>
</accessibility>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoSpeechFamiliesLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo _voice:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoSpeechFamilies</property>
<accessibility>
<relation type="label-for" target="echoSpeechFamilies"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="echoSpeechFamilies">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="changed" handler="echoSpeechFamiliesChanged" swapped="no"/>
<accessibility>
<relation type="labelled-by" target="echoSpeechFamiliesLabel"/>
</accessibility>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoRateLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo ra_te:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoRateScale</property>
<accessibility>
<relation type="label-for" target="echoRateScale"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkScale" id="echoRateScale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">echoRateAdjustment</property>
<property name="round_digits">0</property>
<property name="digits">0</property>
<property name="value_pos">right</property>
<signal name="value-changed" handler="echoRateValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoPitchLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo pi_tch:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoPitchScale</property>
<accessibility>
<relation type="label-for" target="echoPitchScale"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkScale" id="echoPitchScale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">echoPitchAdjustment</property>
<property name="round_digits">1</property>
<property name="value_pos">right</property>
<signal name="value-changed" handler="echoPitchValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="echoVolumeLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Echo vo_lume:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">echoVolumeScale</property>
<accessibility>
<relation type="label-for" target="echoVolumeScale"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">6</property>
</packing>
</child>
<child>
<object class="GtkScale" id="echoVolumeScale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">echoVolumeAdjustment</property>
<property name="round_digits">1</property>
<property name="value_pos">right</property>
<signal name="value-changed" handler="echoVolumeValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">6</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoForKeyCheckButton">
<property name="label" translatable="yes">Use custom voice for _key echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="checkButtonToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">7</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoForCharacterCheckButton">
<property name="label" translatable="yes">Use custom voice for _character echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="checkButtonToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">8</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoForWordCheckButton">
<property name="label" translatable="yes">Use custom voice for _word echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="checkButtonToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">9</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="useCustomEchoForSentenceCheckButton">
<property name="label" translatable="yes">Use custom voice for _sentence echo</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="checkButtonToggled" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">10</property>
<property name="width">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="echoVoiceLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Echo Voice</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child> <child>
<object class="GtkCheckButton" id="enableEchoByCharacterCheckButton"> <object class="GtkCheckButton" id="enableEchoByCharacterCheckButton">
<property name="label" translatable="yes" comments="Translators: When this option is enabled, inserted text of length 1 is spoken.">Enable echo by cha_racter</property> <property name="label" translatable="yes" comments="Translators: When this option is enabled, inserted text of length 1 is spoken.">Enable echo by cha_racter</property>
@@ -3049,7 +3356,7 @@
</object> </object>
<packing> <packing>
<property name="left_attach">0</property> <property name="left_attach">0</property>
<property name="top_attach">2</property> <property name="top_attach">3</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -3064,7 +3371,7 @@
</object> </object>
<packing> <packing>
<property name="left_attach">0</property> <property name="left_attach">0</property>
<property name="top_attach">3</property> <property name="top_attach">4</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -3079,7 +3386,7 @@
</object> </object>
<packing> <packing>
<property name="left_attach">0</property> <property name="left_attach">0</property>
<property name="top_attach">4</property> <property name="top_attach">5</property>
</packing> </packing>
</child> </child>
</object> </object>
+1 -1
View File
@@ -23,5 +23,5 @@
# Forked from Orca screen reader. # Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu # Cthulhu project: https://git.stormux.org/storm/cthulhu
version = "2026.02.18" version = "2026.02.22"
codeName = "master" codeName = "master"
+450 -13
View File
@@ -38,6 +38,9 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Atspi from gi.repository import Atspi
import os import os
import json
import subprocess
import sys
import threading import threading
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import GLib from gi.repository import GLib
@@ -173,6 +176,16 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.speechSystemsChoice = None self.speechSystemsChoice = None
self.speechSystemsChoices = None self.speechSystemsChoices = None
self.speechSystemsModel = None self.speechSystemsModel = None
self.echoVoice = None
self.echoSpeechFamiliesChoice = None
self.echoSpeechFamiliesChoices = None
self.echoSpeechFamiliesModel = None
self.echoSpeechServersChoice = None
self.echoSpeechServersChoices = None
self.echoSpeechServersModel = None
self.initializingEchoSpeech = False
self._echoVoiceFetchToken = 0
self._updatingEchoSpeechFamilies = False
self.systemVoice = None self.systemVoice = None
self.uppercaseVoice = None self.uppercaseVoice = None
self.window = None self.window = None
@@ -386,7 +399,12 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._initComboBox(self.get_widget("speechLanguages")) self._initComboBox(self.get_widget("speechLanguages"))
self.speechFamiliesModel = \ self.speechFamiliesModel = \
self._initComboBox(self.get_widget("speechFamilies")) self._initComboBox(self.get_widget("speechFamilies"))
self.echoSpeechServersModel = \
self._initComboBox(self.get_widget("echoSpeechServers"))
self.echoSpeechFamiliesModel = \
self._initComboBox(self.get_widget("echoSpeechFamilies"))
self._initSpeechState() self._initSpeechState()
self._initEchoSpeechState()
# TODO - JD: Will this ever be the case?? # TODO - JD: Will this ever be the case??
self._isInitialSetup = \ self._isInitialSetup = \
@@ -1671,6 +1689,261 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._setupSpeechSystems(factories) self._setupSpeechSystems(factories)
self.initializingSpeech = False self.initializingSpeech = False
def _getSpeechDispatcherFactory(self):
"""Returns the Speech Dispatcher factory if available."""
factories = cthulhu.cthulhuApp.settingsManager.getSpeechServerFactories()
for factory in factories:
try:
if factory.SpeechServer.getFactoryName() == guilabels.SPEECH_DISPATCHER:
return factory
except Exception:
debug.printException(debug.LEVEL_FINEST)
return None
def _setEchoVoiceFamily(self, family):
"""Sets echo voice family from the selected server family object."""
if not family:
return
name = family.get(speechserver.VoiceFamily.NAME)
language = family.get(speechserver.VoiceFamily.LANG)
dialect = family.get(speechserver.VoiceFamily.DIALECT)
variant = family.get(speechserver.VoiceFamily.VARIANT)
self.echoVoice[acss.ACSS.FAMILY] = {
speechserver.VoiceFamily.NAME: name,
speechserver.VoiceFamily.LANG: language,
speechserver.VoiceFamily.DIALECT: dialect,
speechserver.VoiceFamily.VARIANT: variant,
}
self.echoVoice['established'] = True
def _populateEchoSpeechFamilies(self, families):
"""Populate the echo family combobox from the provided families list."""
combobox = self.get_widget("echoSpeechFamilies")
combobox.set_model(None)
self.echoSpeechFamiliesModel.clear()
self.echoSpeechFamiliesChoices = list(families or [])
self.echoSpeechFamiliesChoice = None
selectedIndex = 0
selectedMatchFound = False
selectedFamily = self.echoVoice.get(acss.ACSS.FAMILY)
selectedName = selectedLanguage = selectedDialect = selectedVariant = None
if selectedFamily:
selectedName = selectedFamily.get(speechserver.VoiceFamily.NAME)
selectedLanguage = selectedFamily.get(speechserver.VoiceFamily.LANG)
selectedDialect = selectedFamily.get(speechserver.VoiceFamily.DIALECT)
selectedVariant = selectedFamily.get(speechserver.VoiceFamily.VARIANT)
for i, family in enumerate(self.echoSpeechFamiliesChoices):
name = family.get(speechserver.VoiceFamily.NAME) or ""
language = family.get(speechserver.VoiceFamily.LANG) or ""
dialect = family.get(speechserver.VoiceFamily.DIALECT) or ""
variant = family.get(speechserver.VoiceFamily.VARIANT)
display = name
locale = language
if dialect:
locale = f"{language}-{dialect}" if language else dialect
if locale:
display = f"{name} ({locale})"
self.echoSpeechFamiliesModel.append((i, display))
if selectedName == name and selectedLanguage == language \
and selectedDialect == dialect and selectedVariant == variant:
selectedIndex = i
selectedMatchFound = True
combobox.set_model(self.echoSpeechFamiliesModel)
if self.echoSpeechFamiliesChoices:
self._updatingEchoSpeechFamilies = True
try:
combobox.set_active(selectedIndex)
finally:
self._updatingEchoSpeechFamilies = False
self.echoSpeechFamiliesChoice = self.echoSpeechFamiliesChoices[selectedIndex]
def _fetchEchoSpeechFamiliesWorker(self, serverInfo, requestToken):
"""Fetch module-specific voices in a subprocess and apply asynchronously."""
queryScript = """
import json
import sys
from cthulhu import speechdispatcherfactory
from cthulhu import speechserver
serverInfo = json.loads(sys.argv[1])
if isinstance(serverInfo, list):
serverInfo = tuple(serverInfo)
server = speechdispatcherfactory.SpeechServer.getSpeechServer(serverInfo)
families = server.getVoiceFamilies() if server else []
result = []
for family in families:
result.append({
"name": family.get(speechserver.VoiceFamily.NAME),
"lang": family.get(speechserver.VoiceFamily.LANG),
"dialect": family.get(speechserver.VoiceFamily.DIALECT),
"variant": family.get(speechserver.VoiceFamily.VARIANT),
})
print(json.dumps(result))
"""
familyDefs = None
try:
completed = subprocess.run(
[sys.executable, "-c", queryScript, json.dumps(serverInfo)],
capture_output=True,
text=True,
timeout=4.0,
check=False,
)
if completed.returncode == 0 and completed.stdout.strip():
familyDefs = json.loads(completed.stdout.strip())
else:
tokens = [
"PREFERENCES DIALOG: Echo voice fetch failed for",
serverInfo,
"rc=",
completed.returncode,
]
debug.printTokens(debug.LEVEL_WARNING, tokens, True)
except subprocess.TimeoutExpired:
tokens = ["PREFERENCES DIALOG: Echo voice fetch timed out for", serverInfo]
debug.printTokens(debug.LEVEL_WARNING, tokens, True)
except Exception:
debug.printException(debug.LEVEL_WARNING)
GLib.idle_add(self._applyFetchedEchoSpeechFamilies, requestToken, familyDefs)
def _applyFetchedEchoSpeechFamilies(self, requestToken, familyDefs):
"""Apply fetched echo families if this is still the active request."""
if requestToken != self._echoVoiceFetchToken:
return False
if not isinstance(familyDefs, list):
return False
families = []
for familyDef in familyDefs:
if not isinstance(familyDef, dict):
continue
families.append(
speechserver.VoiceFamily({
speechserver.VoiceFamily.NAME: familyDef.get("name"),
speechserver.VoiceFamily.LANG: familyDef.get("lang"),
speechserver.VoiceFamily.DIALECT: familyDef.get("dialect"),
speechserver.VoiceFamily.VARIANT: familyDef.get("variant"),
})
)
if not families:
return False
self._populateEchoSpeechFamilies(families)
return False
def _setupEchoSpeechFamilies(self):
"""Populate echo voice family list for the current echo server."""
self._echoVoiceFetchToken += 1
requestToken = self._echoVoiceFetchToken
useCustomModule = self.get_widget("useCustomEchoSpeechServerCheckButton").get_active()
fallbackFamilies = list(self.speechFamiliesChoices or [])
self._populateEchoSpeechFamilies(fallbackFamilies)
if useCustomModule and self.echoSpeechServersChoice:
serverInfo = self.echoSpeechServersChoice.getInfo()
thread = threading.Thread(
target=self._fetchEchoSpeechFamiliesWorker,
args=(serverInfo, requestToken),
daemon=True,
)
thread.start()
def _setEchoSpeechServersChoice(self, serverInfo):
"""Set the active echo module based on stored server info."""
if not self.echoSpeechServersChoices:
self.echoSpeechServersChoice = None
self._setupEchoSpeechFamilies()
return
valueSet = False
for i, server in enumerate(self.echoSpeechServersChoices):
info = server.getInfo()
if serverInfo and info == serverInfo:
self.get_widget("echoSpeechServers").set_active(i)
self.echoSpeechServersChoice = server
valueSet = True
break
if not valueSet:
self.get_widget("echoSpeechServers").set_active(0)
self.echoSpeechServersChoice = self.echoSpeechServersChoices[0]
self._setupEchoSpeechFamilies()
def _setupEchoSpeechServers(self):
"""Populate available speech-dispatcher modules for echo."""
combobox = self.get_widget("echoSpeechServers")
combobox.set_model(None)
self.echoSpeechServersModel.clear()
self.echoSpeechServersChoices = []
self.echoSpeechServersChoice = None
if not self.prefsDict.get('enableSpeech', True):
combobox.set_model(self.echoSpeechServersModel)
self._setupEchoSpeechFamilies()
return
factory = self._getSpeechDispatcherFactory()
if factory is None:
combobox.set_model(self.echoSpeechServersModel)
self._setupEchoSpeechFamilies()
return
try:
self.echoSpeechServersChoices = factory.SpeechServer.getSpeechServers()
except Exception:
debug.printException(debug.LEVEL_WARNING)
self.echoSpeechServersChoices = []
for i, server in enumerate(self.echoSpeechServersChoices):
name = server.getInfo()[0]
self.echoSpeechServersModel.append((i, name))
combobox.set_model(self.echoSpeechServersModel)
self._setEchoSpeechServersChoice(self.prefsDict.get("echoSpeechServerInfo"))
def _initEchoSpeechState(self):
"""Initialize echo voice controls and choices."""
self.echoVoice = acss.ACSS(
self.prefsDict.get("echoVoice", settings.echoVoice) or {})
baseVoice = self.defaultVoice or acss.ACSS(
self.prefsDict.get("voices", {}).get(settings.DEFAULT_VOICE, {}))
rate = self.echoVoice.get(acss.ACSS.RATE, baseVoice.get(acss.ACSS.RATE, 50.0))
pitch = self.echoVoice.get(acss.ACSS.AVERAGE_PITCH, baseVoice.get(acss.ACSS.AVERAGE_PITCH, 5.0))
volume = self.echoVoice.get(acss.ACSS.GAIN, baseVoice.get(acss.ACSS.GAIN, 10.0))
self.get_widget("echoRateScale").set_value(rate)
self.get_widget("echoPitchScale").set_value(pitch)
self.get_widget("echoVolumeScale").set_value(volume)
self.initializingEchoSpeech = True
self._setupEchoSpeechServers()
self.initializingEchoSpeech = False
self._setEchoVoiceItems()
def _setSpokenTextAttributes(self, view, setAttributes, def _setSpokenTextAttributes(self, view, setAttributes,
state, moveToTop=False): state, moveToTop=False):
"""Given a set of spoken text attributes, update the model used by the """Given a set of spoken text attributes, update the model used by the
@@ -2404,6 +2677,19 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
prefs["enableEchoByWord"]) prefs["enableEchoByWord"])
self.get_widget("enableEchoBySentenceCheckButton").set_active( \ self.get_widget("enableEchoBySentenceCheckButton").set_active( \
prefs["enableEchoBySentence"]) prefs["enableEchoBySentence"])
self.get_widget("useCustomEchoVoiceCheckButton").set_active(
prefs.get("useCustomEchoVoice", settings.useCustomEchoVoice))
self.get_widget("useCustomEchoSpeechServerCheckButton").set_active(
prefs.get("useCustomEchoSpeechServer", settings.useCustomEchoSpeechServer))
self.get_widget("useCustomEchoForKeyCheckButton").set_active(
prefs.get("useCustomEchoForKey", settings.useCustomEchoForKey))
self.get_widget("useCustomEchoForCharacterCheckButton").set_active(
prefs.get("useCustomEchoForCharacter", settings.useCustomEchoForCharacter))
self.get_widget("useCustomEchoForWordCheckButton").set_active(
prefs.get("useCustomEchoForWord", settings.useCustomEchoForWord))
self.get_widget("useCustomEchoForSentenceCheckButton").set_active(
prefs.get("useCustomEchoForSentence", settings.useCustomEchoForSentence))
self._setEchoVoiceItems()
# Text attributes pane. # Text attributes pane.
# #
@@ -2563,22 +2849,20 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._updateAIControlsState(enabled) self._updateAIControlsState(enabled)
def _updateAIControlsState(self, enabled): def _updateAIControlsState(self, enabled):
"""Enable or disable AI controls based on AI enabled state.""" """Refresh AI controls while keeping configuration fields editable."""
self.aiProviderCombo.set_sensitive(enabled) _ = enabled # kept for signal/call compatibility
self.aiApiKeyEntry.set_sensitive(enabled) # Keep settings editable even when AI assistant is disabled so users
self.aiOllamaModelEntry.set_sensitive(enabled) # can configure providers/keys before enabling it.
self.aiOllamaEndpointEntry.set_sensitive(enabled) self.aiProviderCombo.set_sensitive(True)
self.aiConfirmationCheckButton.set_sensitive(enabled) self.aiConfirmationCheckButton.set_sensitive(True)
self.aiScreenshotQualityCombo.set_sensitive(enabled) self.aiScreenshotQualityCombo.set_sensitive(True)
try: try:
self.get_widget("aiGetClaudeKeyButton").set_sensitive(enabled) self.get_widget("aiGetClaudeKeyButton").set_sensitive(True)
except: except:
pass # Button might not exist in older UI files pass # Button might not exist in older UI files
# Update provider-specific controls if AI is enabled current_provider = self.prefsDict.get("aiProvider", settings.aiProvider)
if enabled: self._updateProviderControls(current_provider)
current_provider = self.prefsDict.get("aiProvider", settings.aiProvider)
self._updateProviderControls(current_provider)
def _initIndentationState(self): def _initIndentationState(self):
"""Initialize Indentation widgets with current settings.""" """Initialize Indentation widgets with current settings."""
@@ -2908,6 +3192,45 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.get_widget("enableNavigationKeysCheckButton").set_sensitive(enable) self.get_widget("enableNavigationKeysCheckButton").set_sensitive(enable)
self.get_widget("enableDiacriticalKeysCheckButton").set_sensitive( \ self.get_widget("enableDiacriticalKeysCheckButton").set_sensitive( \
enable) enable)
self._setEchoVoiceItems()
def _setEchoVoiceItems(self):
"""[In]sensitize echo voice controls based on current state."""
useCustomVoice = self.get_widget("useCustomEchoVoiceCheckButton").get_active()
useCustomModule = self.get_widget("useCustomEchoSpeechServerCheckButton").get_active()
keyEchoEnabled = self.get_widget("keyEchoCheckButton").get_active()
charEchoEnabled = self.get_widget("enableEchoByCharacterCheckButton").get_active()
wordEchoEnabled = self.get_widget("enableEchoByWordCheckButton").get_active()
sentenceEchoEnabled = self.get_widget("enableEchoBySentenceCheckButton").get_active()
speechEnabled = self.get_widget("speechSupportCheckButton").get_active()
speechSystemIsDispatcher = False
if self.speechSystemsChoice:
try:
speechSystemIsDispatcher = \
self.speechSystemsChoice.SpeechServer.getFactoryName() == guilabels.SPEECH_DISPATCHER
except Exception:
speechSystemIsDispatcher = False
voiceControlsEnabled = useCustomVoice and speechEnabled
moduleOverrideAvailable = voiceControlsEnabled and speechSystemIsDispatcher
moduleControlsEnabled = moduleOverrideAvailable and useCustomModule
self.get_widget("useCustomEchoSpeechServerCheckButton").set_sensitive(moduleOverrideAvailable)
self.get_widget("echoSpeechServers").set_sensitive(moduleControlsEnabled)
self.get_widget("echoSpeechFamilies").set_sensitive(voiceControlsEnabled)
self.get_widget("echoRateScale").set_sensitive(useCustomVoice)
self.get_widget("echoPitchScale").set_sensitive(useCustomVoice)
self.get_widget("echoVolumeScale").set_sensitive(useCustomVoice)
self.get_widget("useCustomEchoForKeyCheckButton").set_sensitive(useCustomVoice and keyEchoEnabled)
self.get_widget("useCustomEchoForCharacterCheckButton").set_sensitive(
useCustomVoice and charEchoEnabled)
self.get_widget("useCustomEchoForWordCheckButton").set_sensitive(useCustomVoice and wordEchoEnabled)
self.get_widget("useCustomEchoForSentenceCheckButton").set_sensitive(
useCustomVoice and sentenceEchoEnabled)
def _presentMessage(self, text, interrupt=False, voice=None): def _presentMessage(self, text, interrupt=False, voice=None):
"""If the text field is not None, presents the given text, optionally """If the text field is not None, presents the given text, optionally
@@ -3234,6 +3557,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
enable = widget.get_active() enable = widget.get_active()
self.prefsDict["enableSpeech"] = enable self.prefsDict["enableSpeech"] = enable
self.get_widget("speechOptionsGrid").set_sensitive(enable) self.get_widget("speechOptionsGrid").set_sensitive(enable)
self._setupEchoSpeechServers()
self._setEchoVoiceItems()
def onlySpeakDisplayedTextToggled(self, widget): def onlySpeakDisplayedTextToggled(self, widget):
"""Signal handler for the "toggled" signal for the GtkCheckButton """Signal handler for the "toggled" signal for the GtkCheckButton
@@ -3265,6 +3590,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
selectedIndex = widget.get_active() selectedIndex = widget.get_active()
self.speechSystemsChoice = self.speechSystemsChoices[selectedIndex] self.speechSystemsChoice = self.speechSystemsChoices[selectedIndex]
self._setupSpeechServers() self._setupSpeechServers()
self._setupEchoSpeechServers()
self._setEchoVoiceItems()
def speechServersChanged(self, widget): def speechServersChanged(self, widget):
"""Signal handler for the "changed" signal for the speechServers """Signal handler for the "changed" signal for the speechServers
@@ -3297,6 +3624,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
pass pass
self._setupVoices() self._setupVoices()
self._setEchoVoiceItems()
def speechLanguagesChanged(self, widget): def speechLanguagesChanged(self, widget):
"""Signal handler for the "value_changed" signal for the languages """Signal handler for the "value_changed" signal for the languages
@@ -3435,6 +3763,75 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
voices.get(settings.DEFAULT_VOICE, {})[acss.ACSS.GAIN] = volume voices.get(settings.DEFAULT_VOICE, {})[acss.ACSS.GAIN] = volume
cthulhu.cthulhuApp.settingsManager.setSetting('voices', voices) cthulhu.cthulhuApp.settingsManager.setSetting('voices', voices)
def useCustomEchoVoiceToggled(self, widget):
"""Signal handler for enabling/disabling custom echo voice settings."""
self.prefsDict["useCustomEchoVoice"] = widget.get_active()
self._setEchoVoiceItems()
self._setupEchoSpeechFamilies()
def useCustomEchoSpeechServerToggled(self, widget):
"""Signal handler for enabling/disabling custom echo module override."""
self.prefsDict["useCustomEchoSpeechServer"] = widget.get_active()
self._setEchoVoiceItems()
self._setupEchoSpeechFamilies()
def echoSpeechServersChanged(self, widget):
"""Signal handler for selecting the echo speech-dispatcher module."""
if self.initializingEchoSpeech:
return
selectedIndex = widget.get_active()
if selectedIndex < 0:
self.echoSpeechServersChoice = None
self.prefsDict["echoSpeechServerInfo"] = None
self._setupEchoSpeechFamilies()
return
self.echoSpeechServersChoice = self.echoSpeechServersChoices[selectedIndex]
self.prefsDict["echoSpeechServerInfo"] = self.echoSpeechServersChoice.getInfo()
self._setupEchoSpeechFamilies()
def echoSpeechFamiliesChanged(self, widget):
"""Signal handler for selecting the echo voice family."""
if self.initializingEchoSpeech or self._updatingEchoSpeechFamilies:
return
selectedIndex = widget.get_active()
if selectedIndex < 0:
self.echoSpeechFamiliesChoice = None
return
self.echoSpeechFamiliesChoice = self.echoSpeechFamiliesChoices[selectedIndex]
self._setEchoVoiceFamily(self.echoSpeechFamiliesChoice)
def echoRateValueChanged(self, widget):
"""Signal handler for changing custom echo rate."""
if self.echoVoice is None:
self.echoVoice = acss.ACSS({})
self.echoVoice[acss.ACSS.RATE] = widget.get_value()
self.echoVoice['established'] = True
def echoPitchValueChanged(self, widget):
"""Signal handler for changing custom echo pitch."""
if self.echoVoice is None:
self.echoVoice = acss.ACSS({})
self.echoVoice[acss.ACSS.AVERAGE_PITCH] = widget.get_value()
self.echoVoice['established'] = True
def echoVolumeValueChanged(self, widget):
"""Signal handler for changing custom echo volume."""
if self.echoVoice is None:
self.echoVoice = acss.ACSS({})
self.echoVoice[acss.ACSS.GAIN] = widget.get_value()
self.echoVoice['established'] = True
def checkButtonToggled(self, widget): def checkButtonToggled(self, widget):
"""Signal handler for "toggled" signal for basic GtkCheckButton """Signal handler for "toggled" signal for basic GtkCheckButton
widgets. The user has altered the state of the checkbox. widgets. The user has altered the state of the checkbox.
@@ -3452,6 +3849,15 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
# strip "CheckButton" from the end. # strip "CheckButton" from the end.
settingName = settingName[:-11] settingName = settingName[:-11]
self.prefsDict[settingName] = widget.get_active() self.prefsDict[settingName] = widget.get_active()
if settingName in [
"enableEchoByCharacter",
"enableEchoByWord",
"enableEchoBySentence",
"useCustomEchoForKey",
"useCustomEchoForCharacter",
"useCustomEchoForWord",
"useCustomEchoForSentence"]:
self._setEchoVoiceItems()
def keyEchoChecked(self, widget): def keyEchoChecked(self, widget):
"""Signal handler for the "toggled" signal for the """Signal handler for the "toggled" signal for the
@@ -4254,6 +4660,35 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
settings.SYSTEM_VOICE: acss.ACSS(self.systemVoice), settings.SYSTEM_VOICE: acss.ACSS(self.systemVoice),
} }
self.prefsDict["useCustomEchoVoice"] = \
self.get_widget("useCustomEchoVoiceCheckButton").get_active()
self.prefsDict["useCustomEchoSpeechServer"] = \
self.get_widget("useCustomEchoSpeechServerCheckButton").get_active()
self.prefsDict["useCustomEchoForKey"] = \
self.get_widget("useCustomEchoForKeyCheckButton").get_active()
self.prefsDict["useCustomEchoForCharacter"] = \
self.get_widget("useCustomEchoForCharacterCheckButton").get_active()
self.prefsDict["useCustomEchoForWord"] = \
self.get_widget("useCustomEchoForWordCheckButton").get_active()
self.prefsDict["useCustomEchoForSentence"] = \
self.get_widget("useCustomEchoForSentenceCheckButton").get_active()
if self.echoVoice is None:
self.echoVoice = acss.ACSS({})
# Persist slider values directly so saving does not depend on
# value-changed signal timing.
self.echoVoice[acss.ACSS.RATE] = self.get_widget("echoRateScale").get_value()
self.echoVoice[acss.ACSS.AVERAGE_PITCH] = self.get_widget("echoPitchScale").get_value()
self.echoVoice[acss.ACSS.GAIN] = self.get_widget("echoVolumeScale").get_value()
self.echoVoice['established'] = True
self.prefsDict["echoVoice"] = acss.ACSS(self.echoVoice)
if self.echoSpeechServersChoice:
self.prefsDict["echoSpeechServerInfo"] = self.echoSpeechServersChoice.getInfo()
else:
self.prefsDict["echoSpeechServerInfo"] = None
def applyButtonClicked(self, widget): def applyButtonClicked(self, widget):
"""Signal handler for the "clicked" signal for the applyButton """Signal handler for the "clicked" signal for the applyButton
GtkButton widget. The user has clicked the Apply button. GtkButton widget. The user has clicked the Apply button.
@@ -4286,6 +4721,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self._refresh_dynamic_plugin_tabs() self._refresh_dynamic_plugin_tabs()
braille.checkBrailleSetting() braille.checkBrailleSetting()
self._initSpeechState() self._initSpeechState()
self._initEchoSpeechState()
self._populateKeyBindings() self._populateKeyBindings()
self.__initProfileCombo() self.__initProfileCombo()
@@ -4548,6 +4984,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
braille.checkBrailleSetting() braille.checkBrailleSetting()
self._initSpeechState() self._initSpeechState()
self._initEchoSpeechState()
self._populateKeyBindings() self._populateKeyBindings()
+8 -7
View File
@@ -325,7 +325,6 @@ class AIAssistant(Plugin):
if not self._prefs_widgets: if not self._prefs_widgets:
return return
enabled = self._prefs_widgets["enable_check"].get_active()
provider_values = self._prefs_widgets.get("provider_values", []) provider_values = self._prefs_widgets.get("provider_values", [])
provider_index = self._prefs_widgets["provider_combo"].get_active() provider_index = self._prefs_widgets["provider_combo"].get_active()
provider = provider_values[provider_index] if 0 <= provider_index < len(provider_values) else settings.aiProvider provider = provider_values[provider_index] if 0 <= provider_index < len(provider_values) else settings.aiProvider
@@ -333,12 +332,14 @@ class AIAssistant(Plugin):
is_gemini = provider == settings.AI_PROVIDER_GEMINI is_gemini = provider == settings.AI_PROVIDER_GEMINI
is_ollama = provider == settings.AI_PROVIDER_OLLAMA is_ollama = provider == settings.AI_PROVIDER_OLLAMA
self._prefs_widgets["provider_combo"].set_sensitive(enabled) # Keep preferences editable even when the feature is disabled so users
self._prefs_widgets["api_key_entry"].set_sensitive(enabled and is_gemini) # can prepare configuration before turning AI assistant on.
self._prefs_widgets["ollama_model_entry"].set_sensitive(enabled and is_ollama) self._prefs_widgets["provider_combo"].set_sensitive(True)
self._prefs_widgets["ollama_endpoint_entry"].set_sensitive(enabled and is_ollama) self._prefs_widgets["api_key_entry"].set_sensitive(is_gemini)
self._prefs_widgets["confirmation_check"].set_sensitive(enabled) self._prefs_widgets["ollama_model_entry"].set_sensitive(is_ollama)
self._prefs_widgets["quality_combo"].set_sensitive(enabled) self._prefs_widgets["ollama_endpoint_entry"].set_sensitive(is_ollama)
self._prefs_widgets["confirmation_check"].set_sensitive(True)
self._prefs_widgets["quality_combo"].set_sensitive(True)
def refresh_settings(self): def refresh_settings(self):
"""Refresh plugin settings and reinitialize provider. Called when settings change.""" """Refresh plugin settings and reinitialize provider. Called when settings change."""
+3 -19
View File
@@ -21,10 +21,8 @@ from gi.repository import Gtk, Gdk, Pango
from cthulhu.plugin import Plugin, cthulhu_hookimpl from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import cthulhu from cthulhu import cthulhu
from cthulhu import debug from cthulhu import debug
from cthulhu import settings_manager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager
class PluginManager(Plugin): class PluginManager(Plugin):
@@ -473,25 +471,11 @@ class PluginManager(Plugin):
current_general['activePlugins'] = active_plugins current_general['activePlugins'] = active_plugins
cthulhu.cthulhuApp.settingsManager.profile = profile_name cthulhu.cthulhuApp.settingsManager.profile = profile_name
cthulhu.cthulhuApp.settingsManager._setProfileGeneral(current_general) cthulhu.cthulhuApp.settingsManager.saveProfileSettings(current_general)
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Settings saved via settings manager (profile {profile_name})", True)
pronunciations = cthulhu.cthulhuApp.settingsManager.getPronunciations(profile_name) or {}
keybindings = cthulhu.cthulhuApp.settingsManager.getKeybindings(profile_name) or {}
backend = cthulhu.cthulhuApp.settingsManager._backend
if backend:
backend.saveProfileSettings(
profile_name,
cthulhu.cthulhuApp.settingsManager.profileGeneral,
pronunciations,
keybindings
)
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Settings saved to backend (profile {profile_name})", True)
else:
debug.printMessage(debug.LEVEL_INFO, "PluginManager: No backend available for saving", True)
except Exception as save_error: except Exception as save_error:
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error saving via backend: {save_error}", True) debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Error saving plugin state: {save_error}", True)
debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Updated active plugins: {active_plugins}", True) debug.printMessage(debug.LEVEL_INFO, f"PluginManager: Updated active plugins: {active_plugins}", True)
+3
View File
@@ -5122,6 +5122,9 @@ class Utilities:
if not event.type.startswith("object:text-changed:insert"): if not event.type.startswith("object:text-changed:insert"):
return False return False
if not cthulhu.cthulhuApp.settingsManager.getSetting("enableKeyEcho"):
return False
if AXUtilities.is_focusable(event.source) \ if AXUtilities.is_focusable(event.source) \
and not AXUtilities.is_focused(event.source) \ and not AXUtilities.is_focused(event.source) \
and event.source != cthulhu_state.locusOfFocus: and event.source != cthulhu_state.locusOfFocus:
+75 -4
View File
@@ -44,6 +44,7 @@ from gi.repository import Gdk
import re import re
import time import time
import cthulhu.acss as acss
import cthulhu.braille as braille import cthulhu.braille as braille
import cthulhu.cmdnames as cmdnames import cthulhu.cmdnames as cmdnames
import cthulhu.dbus_service as dbus_service import cthulhu.dbus_service as dbus_service
@@ -1818,6 +1819,9 @@ class Script(script.Script):
if len(string) != 1: if len(string) != 1:
return return
if not cthulhu.cthulhuApp.settingsManager.getSetting('enableKeyEcho'):
return
if cthulhu.cthulhuApp.settingsManager.getSetting('enableEchoBySentence') \ if cthulhu.cthulhuApp.settingsManager.getSetting('enableEchoBySentence') \
and self.echoPreviousSentence(event.source): and self.echoPreviousSentence(event.source):
return return
@@ -1967,6 +1971,15 @@ class Script(script.Script):
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, msg, True)
return return
# Some toolkits emit transient window:deactivate/window:activate pairs
# while the same window remains active. Treat those as noise so we do
# not clear state and force a full script/settings reactivation.
AXObject.clear_cache(event.source)
if AXUtilities.is_active(event.source):
msg = "DEFAULT: Ignoring event. Source window still active."
debug.printMessage(debug.LEVEL_INFO, msg, True)
return
if self.flatReviewPresenter.is_active(): if self.flatReviewPresenter.is_active():
self.flatReviewPresenter.quit() self.flatReviewPresenter.quit()
@@ -2219,9 +2232,12 @@ class Script(script.Script):
sentence = self.utilities.substring(obj, sentenceStartOffset + 1, sentence = self.utilities.substring(obj, sentenceStartOffset + 1,
sentenceEndOffset + 1) sentenceEndOffset + 1)
voice = self.speechGenerator.voice(obj=obj, string=sentence)
sentence = self.utilities.adjustForRepeats(sentence) sentence = self.utilities.adjustForRepeats(sentence)
self.speakMessage(sentence, voice) if self._shouldUseCustomEchoVoice("sentence"):
speech.speakEchoText(sentence, self._getCustomEchoVoice())
else:
voice = self.speechGenerator.voice(obj=obj, string=sentence)
self.speakMessage(sentence, voice)
return True return True
def echoPreviousWord(self, obj, offset=None): def echoPreviousWord(self, obj, offset=None):
@@ -2288,9 +2304,12 @@ class Script(script.Script):
word = self.utilities.\ word = self.utilities.\
substring(obj, wordStartOffset + 1, wordEndOffset + 1) substring(obj, wordStartOffset + 1, wordEndOffset + 1)
voice = self.speechGenerator.voice(obj=obj, string=word)
word = self.utilities.adjustForRepeats(word) word = self.utilities.adjustForRepeats(word)
self.speakMessage(word, voice) if self._shouldUseCustomEchoVoice("word"):
speech.speakEchoText(word, self._getCustomEchoVoice())
else:
voice = self.speechGenerator.voice(obj=obj, string=word)
self.speakMessage(word, voice)
return True return True
def sayCharacter(self, obj): def sayCharacter(self, obj):
@@ -2892,6 +2911,9 @@ class Script(script.Script):
if not event.shouldEcho or event.isCthulhuModified(): if not event.shouldEcho or event.isCthulhuModified():
return False return False
if not cthulhu.cthulhuApp.settingsManager.getSetting('enableKeyEcho'):
return False
role = AXObject.get_role(cthulhu_state.locusOfFocus) role = AXObject.get_role(cthulhu_state.locusOfFocus)
if role in [Atspi.Role.DIALOG, Atspi.Role.FRAME, Atspi.Role.WINDOW]: if role in [Atspi.Role.DIALOG, Atspi.Role.FRAME, Atspi.Role.WINDOW]:
focusedObject = AXUtilities.get_focused_object(cthulhu_state.activeWindow) focusedObject = AXUtilities.get_focused_object(cthulhu_state.activeWindow)
@@ -3332,6 +3354,47 @@ class Script(script.Script):
# # # #
######################################################################## ########################################################################
def _shouldUseCustomEchoVoice(self, echoType):
"""Returns True if custom echo settings should be used for the given type."""
settingForType = {
"key": "useCustomEchoForKey",
"character": "useCustomEchoForCharacter",
"word": "useCustomEchoForWord",
"sentence": "useCustomEchoForSentence",
}
settingName = settingForType.get(echoType)
if not settingName:
return False
settingsManager = cthulhu.cthulhuApp.settingsManager
if not settingsManager.getSetting("enableKeyEcho"):
return False
if not settingsManager.getSetting("useCustomEchoVoice"):
return False
return settingsManager.getSetting(settingName)
def _getCustomEchoVoice(self):
"""Returns the effective ACSS voice for custom echo output."""
settingsManager = cthulhu.cthulhuApp.settingsManager
voices = settingsManager.getSetting("voices") or {}
defaultVoice = acss.ACSS(voices.get(settings.DEFAULT_VOICE, {}))
echoVoice = settingsManager.getSetting("echoVoice")
if not echoVoice:
return defaultVoice
try:
defaultVoice.update(acss.ACSS(echoVoice))
except Exception:
debug.printException(debug.LEVEL_INFO)
return defaultVoice
def speakKeyEvent(self, event): def speakKeyEvent(self, event):
"""Method to speak a keyboard event. Scripts should use this method """Method to speak a keyboard event. Scripts should use this method
rather than calling speech.speakKeyEvent directly.""" rather than calling speech.speakKeyEvent directly."""
@@ -3340,6 +3403,10 @@ class Script(script.Script):
if event.is_printable_key(): if event.is_printable_key():
string = event.event_string string = event.event_string
if self._shouldUseCustomEchoVoice("key"):
speech.speakEchoKeyEvent(event, self._getCustomEchoVoice())
return
voice = self.speechGenerator.voice(string=string) voice = self.speechGenerator.voice(string=string)
speech.speakKeyEvent(event, voice) speech.speakKeyEvent(event, voice)
@@ -3347,6 +3414,10 @@ class Script(script.Script):
"""Method to speak a single character. Scripts should use this """Method to speak a single character. Scripts should use this
method rather than calling speech.speakCharacter directly.""" method rather than calling speech.speakCharacter directly."""
if self._shouldUseCustomEchoVoice("character"):
speech.speakEchoCharacter(character, self._getCustomEchoVoice())
return
voice = self.speechGenerator.voice(string=character) voice = self.speechGenerator.voice(string=character)
speech.speakCharacter(character, voice) speech.speakCharacter(character, voice)
+3 -1
View File
@@ -572,7 +572,9 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
and soundEnabled: and soundEnabled:
roleSoundIcon = sound_theme_manager.getManager().getRoleSoundIcon(role) roleSoundIcon = sound_theme_manager.getManager().getRoleSoundIcon(role)
if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY \ if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY \
and soundEnabled and roleSoundIcon: and soundEnabled:
# Stateful controls present their role via state sounds; suppress
# spoken role names here even if no dedicated role icon exists.
if AXUtilities.is_check_box(obj) \ if AXUtilities.is_check_box(obj) \
or AXUtilities.is_check_menu_item(obj) \ or AXUtilities.is_check_menu_item(obj) \
or AXUtilities.is_radio_button(obj) \ or AXUtilities.is_radio_button(obj) \
+16
View File
@@ -53,6 +53,14 @@ userCustomizableSettings = [
"enableEchoByWord", "enableEchoByWord",
"enableEchoBySentence", "enableEchoBySentence",
"enableKeyEcho", "enableKeyEcho",
"useCustomEchoVoice",
"useCustomEchoSpeechServer",
"echoSpeechServerInfo",
"echoVoice",
"useCustomEchoForKey",
"useCustomEchoForCharacter",
"useCustomEchoForWord",
"useCustomEchoForSentence",
"gameMode", "gameMode",
"nvda2cthulhuTranslateEnabled", "nvda2cthulhuTranslateEnabled",
"enableAlphabeticKeys", "enableAlphabeticKeys",
@@ -352,6 +360,14 @@ enableDiacriticalKeys = False
enableEchoByCharacter = False enableEchoByCharacter = False
enableEchoByWord = False enableEchoByWord = False
enableEchoBySentence = False enableEchoBySentence = False
useCustomEchoVoice = False
useCustomEchoSpeechServer = False
echoSpeechServerInfo = None
echoVoice = ACSS({})
useCustomEchoForKey = True
useCustomEchoForCharacter = True
useCustomEchoForWord = False
useCustomEchoForSentence = False
presentLockingKeys = None presentLockingKeys = None
# Mouse review # Mouse review
+76 -9
View File
@@ -314,6 +314,13 @@ class SettingsManager(object):
converted_voices[voice_type] = voice_def converted_voices[voice_type] = voice_def
general["voices"] = converted_voices general["voices"] = converted_voices
echo_voice = general.get("echoVoice")
if isinstance(echo_voice, dict):
try:
general["echoVoice"] = ACSS(echo_voice)
except Exception:
general["echoVoice"] = echo_voice
return general return general
def getDefaultSetting(self, settingName: str) -> Any: def getDefaultSetting(self, settingName: str) -> Any:
@@ -379,6 +386,18 @@ class SettingsManager(object):
def getSetting(self, settingName: str) -> Any: def getSetting(self, settingName: str) -> Any:
return getattr(settings, settingName, None) return getattr(settings, settingName, None)
def _getListSetting(self, settingName: str) -> List[str]:
value = self.getSetting(settingName)
if isinstance(value, (list, tuple)):
return [item for item in value if isinstance(item, str)]
return []
def _ensurePluginPersistenceSettings(self, general: Dict[str, Any]) -> None:
if 'activePlugins' not in general:
general['activePlugins'] = self._getListSetting('activePlugins')
if 'pluginSources' not in general:
general['pluginSources'] = self._getListSetting('pluginSources')
def getVoiceLocale(self, voice: str = 'default') -> str: def getVoiceLocale(self, voice: str = 'default') -> str:
voices = self.getSetting('voices') voices = self.getSetting('voices')
v = ACSS(voices.get(voice, {})) v = ACSS(voices.get(voice, {}))
@@ -620,12 +639,24 @@ class SettingsManager(object):
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, msg, True)
self.profileGeneral = {} self.profileGeneral = {}
alwaysPersistKeys = {
'useCustomEchoVoice',
'useCustomEchoSpeechServer',
'useCustomEchoForKey',
'useCustomEchoForCharacter',
'useCustomEchoForWord',
'useCustomEchoForSentence',
}
for key, value in general.items(): for key, value in general.items():
if key in ['startingProfile', 'activeProfile']: if key in ['startingProfile', 'activeProfile']:
continue continue
elif key == 'profile': elif key == 'profile':
self.profileGeneral[key] = value self.profileGeneral[key] = value
elif key in ['activePlugins', 'pluginSources']:
self.profileGeneral[key] = copy.deepcopy(value)
elif key in alwaysPersistKeys:
self.profileGeneral[key] = copy.deepcopy(value)
elif value != self.defaultGeneral.get(key): elif value != self.defaultGeneral.get(key):
self.profileGeneral[key] = value self.profileGeneral[key] = value
elif self.general.get(key) != value: elif self.general.get(key) != value:
@@ -634,6 +665,46 @@ class SettingsManager(object):
msg = 'SETTINGS MANAGER: General settings for profile set' msg = 'SETTINGS MANAGER: General settings for profile set'
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, msg, True)
def saveProfileSettings(
self,
general: Dict[str, Any],
pronunciations: Optional[Dict[str, Any]] = None,
keybindings: Optional[Dict[str, Any]] = None,
) -> None:
profileName: Optional[str] = self.profile
profileSetting = general.get('profile')
if isinstance(profileSetting, (list, tuple)) and len(profileSetting) > 1:
profileName = profileSetting[1]
elif not profileName:
defaultProfile = settings.profile
if isinstance(defaultProfile, (list, tuple)) and len(defaultProfile) > 1:
profileName = defaultProfile[1]
else:
profileName = 'default'
self.profile = profileName
generalCopy = dict(general)
self._ensurePluginPersistenceSettings(generalCopy)
self._setProfileGeneral(generalCopy)
if self.profile is None:
return
profileName = self.profile
if pronunciations is None:
pronunciations = self.getPronunciations(profileName) or {}
if keybindings is None:
keybindings = self.getKeybindings(profileName) or {}
self._setProfilePronunciations(pronunciations)
self._setProfileKeybindings(keybindings)
if self._backend:
self._backend.saveProfileSettings(self.profile,
self.profileGeneral,
self.profilePronunciations,
self.profileKeybindings)
def _setProfilePronunciations(self, pronunciations: Dict[str, Any]) -> None: def _setProfilePronunciations(self, pronunciations: Dict[str, Any]) -> None:
"""Set the changed pronunciations settings from the defaults' ones """Set the changed pronunciations settings from the defaults' ones
as the profile's.""" as the profile's."""
@@ -664,9 +735,12 @@ class SettingsManager(object):
if not self._backend: if not self._backend:
return return
profileScopedKeys = {'activePlugins', 'pluginSources'}
appGeneral = {} appGeneral = {}
profileGeneral = self.getGeneralSettings(self.profile) if self.profile else {} profileGeneral = self.getGeneralSettings(self.profile) if self.profile else {}
for key, value in general.items(): for key, value in general.items():
if key in profileScopedKeys:
continue
if value != profileGeneral.get(key): if value != profileGeneral.get(key):
appGeneral[key] = value appGeneral[key] = value
@@ -705,24 +779,17 @@ class SettingsManager(object):
currentProfile = _profile[1] currentProfile = _profile[1]
self.profile = currentProfile self.profile = currentProfile
self._ensurePluginPersistenceSettings(general)
# Elements that need to stay updated in main configuration. # Elements that need to stay updated in main configuration.
self.defaultGeneral['startingProfile'] = general.get('startingProfile', self.defaultGeneral['startingProfile'] = general.get('startingProfile',
_profile) _profile)
self._setProfileGeneral(general) self.saveProfileSettings(general, pronunciations, keybindings)
self._setProfilePronunciations(pronunciations)
self._setProfileKeybindings(keybindings)
tokens = ["SETTINGS MANAGER: Saving for backend", self._backend] tokens = ["SETTINGS MANAGER: Saving for backend", self._backend]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
if self._backend and self.profile:
self._backend.saveProfileSettings(self.profile,
self.profileGeneral,
self.profilePronunciations,
self.profileKeybindings)
tokens = ["SETTINGS MANAGER: Settings for", script, "(app:", script.app, ") saved"] tokens = ["SETTINGS MANAGER: Settings for", script, "(app:", script.app, ") saved"]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
return self._enableAccessibility() return self._enableAccessibility()
+129 -11
View File
@@ -69,6 +69,7 @@ def _ensureLogger() -> None:
# The speech server to use for all speech operations. # The speech server to use for all speech operations.
# #
_speechserver: Optional[SpeechServer] = None _speechserver: Optional[SpeechServer] = None
_echoSpeechserver: Optional[SpeechServer] = None
# The last time something was spoken. # The last time something was spoken.
_timestamp: float = 0.0 _timestamp: float = 0.0
@@ -76,12 +77,15 @@ _timestamp: float = 0.0
# Optional callback for live monitoring of spoken text. # Optional callback for live monitoring of spoken text.
_monitorWriteTextCallback: Optional[Callable[[str], None]] = None _monitorWriteTextCallback: Optional[Callable[[str], None]] = None
def _initSpeechServer(moduleName: Optional[str], speechServerInfo: Optional[Any]) -> None: def _isSpeechDispatcherFactory(moduleName: Optional[str]) -> bool:
if not moduleName:
return False
return moduleName.split(".")[-1] == "speechdispatcherfactory"
global _speechserver def _initSpeechServer(moduleName: Optional[str], speechServerInfo: Optional[Any]) -> SpeechServer:
if not moduleName: if not moduleName:
return raise Exception("ERROR: No speech server module name provided")
factory = None factory = None
try: try:
@@ -94,35 +98,58 @@ def _initSpeechServer(moduleName: Optional[str], speechServerInfo: Optional[Any]
# Now, get the speech server we care about. # Now, get the speech server we care about.
# #
speechServerInfo = settings.speechServerInfo speech_server = None
if factory: if factory:
if speechServerInfo: if speechServerInfo:
_speechserver = factory.SpeechServer.getSpeechServer(speechServerInfo) # type: ignore speech_server = factory.SpeechServer.getSpeechServer(speechServerInfo) # type: ignore
if not _speechserver: if not speech_server:
_speechserver = factory.SpeechServer.getSpeechServer() # type: ignore speech_server = factory.SpeechServer.getSpeechServer() # type: ignore
if speechServerInfo: if speechServerInfo:
tokens = ["SPEECH: Invalid speechServerInfo:", speechServerInfo] tokens = ["SPEECH: Invalid speechServerInfo:", speechServerInfo]
debug.printTokens(debug.LEVEL_INFO, tokens, True) debug.printTokens(debug.LEVEL_INFO, tokens, True)
if not _speechserver: if not speech_server:
raise Exception(f"ERROR: No speech server for factory: {moduleName}") raise Exception(f"ERROR: No speech server for factory: {moduleName}")
return speech_server
def _refreshEchoSpeechServer() -> None:
global _echoSpeechserver
_echoSpeechserver = None
if not settings.enableSpeech or not settings.useCustomEchoSpeechServer:
return
if not _isSpeechDispatcherFactory(settings.speechServerFactory):
return
try:
_echoSpeechserver = _initSpeechServer(
settings.speechServerFactory,
settings.echoSpeechServerInfo
)
except Exception:
debug.printException(debug.LEVEL_INFO)
_echoSpeechserver = None
def init() -> None: def init() -> None:
global _speechserver
debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Initializing', True) debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Initializing', True)
if _speechserver: if _speechserver:
debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Already initialized', True) debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Already initialized', True)
_refreshEchoSpeechServer()
return return
chosenModuleName = settings.speechServerFactory chosenModuleName = settings.speechServerFactory
try: try:
_initSpeechServer(chosenModuleName, settings.speechServerInfo) _speechserver = _initSpeechServer(chosenModuleName, settings.speechServerInfo)
except Exception: except Exception:
moduleNames = settings.speechFactoryModules moduleNames = settings.speechFactoryModules
for moduleName in moduleNames: for moduleName in moduleNames:
if moduleName != settings.speechServerFactory: if moduleName != settings.speechServerFactory:
try: try:
_initSpeechServer(moduleName, None) _speechserver = _initSpeechServer(moduleName, None)
if _speechserver: if _speechserver:
chosenModuleName = moduleName chosenModuleName = moduleName
break break
@@ -144,6 +171,7 @@ def init() -> None:
msg = 'SPEECH: Not available' msg = 'SPEECH: Not available'
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, msg, True)
_refreshEchoSpeechServer()
debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Initialized', True) debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Initialized', True)
def checkSpeechSetting() -> None: def checkSpeechSetting() -> None:
@@ -157,6 +185,9 @@ def checkSpeechSetting() -> None:
if not _speechserver: if not _speechserver:
init() init()
return
_refreshEchoSpeechServer()
def getSpeechServer() -> Optional[SpeechServer]: def getSpeechServer() -> Optional[SpeechServer]:
"""Returns the speech server instance.""" """Returns the speech server instance."""
@@ -170,6 +201,7 @@ def setSpeechServer(speechServer: SpeechServer) -> None:
""" """
global _speechserver global _speechserver
_speechserver = speechServer _speechserver = speechServer
_refreshEchoSpeechServer()
def set_monitor_callbacks(writeText: Optional[Callable[[str], None]] = None) -> None: def set_monitor_callbacks(writeText: Optional[Callable[[str], None]] = None) -> None:
"""Sets runtime callbacks for live speech monitoring.""" """Sets runtime callbacks for live speech monitoring."""
@@ -418,6 +450,35 @@ def speakKeyEvent(event: Any, acss: Optional[Any] = None) -> None:
if _speechserver: if _speechserver:
_speechserver.speakKeyEvent(event, acss) # type: ignore _speechserver.speakKeyEvent(event, acss) # type: ignore
def speakEchoKeyEvent(event: Any, acss: Optional[Any] = None) -> None:
"""Speaks a key event for key/typing echo using echo speech settings."""
_ensureLogger()
if settings.silenceSpeech:
return
global _timestamp
if _timestamp:
msg = f"SPEECH: Last spoke {time.time() - _timestamp:.4f} seconds ago"
debug.printMessage(debug.LEVEL_INFO, msg, True)
_timestamp = time.time()
keyname = event.getKeyName()
lockingStateString = event.getLockingStateString()
acss = __resolveACSS(acss)
msg = f"{keyname} {lockingStateString}"
logLine = f"SPEECH ECHO OUTPUT: '{msg.strip()}' {acss}"
debug.printMessage(debug.LEVEL_INFO, logLine, True)
if log:
log.info(logLine)
_write_to_monitor(msg.strip())
server = _echoSpeechserver or _speechserver
if server:
server.speakKeyEvent(event, acss) # type: ignore
def speakCharacter(character: str, acss: Optional[Any] = None) -> None: def speakCharacter(character: str, acss: Optional[Any] = None) -> None:
"""Speaks a single character immediately. """Speaks a single character immediately.
@@ -452,6 +513,58 @@ def speakCharacter(character: str, acss: Optional[Any] = None) -> None:
if _speechserver: if _speechserver:
_speechserver.speakCharacter(character, acss=acss) # type: ignore _speechserver.speakCharacter(character, acss=acss) # type: ignore
def speakEchoCharacter(character: str, acss: Optional[Any] = None) -> None:
"""Speaks a single character for key/typing echo using echo speech settings."""
_ensureLogger()
if settings.silenceSpeech:
return
global _timestamp
if _timestamp:
msg = f"SPEECH: Last spoke {time.time() - _timestamp:.4f} seconds ago"
debug.printMessage(debug.LEVEL_INFO, msg, True)
_timestamp = time.time()
acss = __resolveACSS(acss)
tokens = [f"SPEECH ECHO OUTPUT: '{character}'", acss]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if log:
log.info(f"SPEECH ECHO OUTPUT: '{character}'")
_write_to_monitor(character)
server = _echoSpeechserver or _speechserver
if server:
server.speakCharacter(character, acss=acss) # type: ignore
def speakEchoText(text: str, acss: Optional[Any] = None, interrupt: bool = True) -> None:
"""Speaks text for typing echo using echo speech settings."""
_ensureLogger()
if settings.silenceSpeech:
return
global _timestamp
if _timestamp:
msg = f"SPEECH: Last spoke {time.time() - _timestamp:.4f} seconds ago"
debug.printMessage(debug.LEVEL_INFO, msg, True)
_timestamp = time.time()
acss = __resolveACSS(acss)
logLine = f"SPEECH ECHO OUTPUT: '{text}' {acss}"
debug.printMessage(debug.LEVEL_INFO, logLine, True)
if log:
log.info(logLine)
_write_to_monitor(text)
server = _echoSpeechserver or _speechserver
if server:
server.speak(text, acss, interrupt) # type: ignore
def getInfo() -> Optional[Any]: def getInfo() -> Optional[Any]:
info = None info = None
if _speechserver: if _speechserver:
@@ -462,13 +575,18 @@ def getInfo() -> Optional[Any]:
def stop() -> None: def stop() -> None:
if _speechserver: if _speechserver:
_speechserver.stop() # type: ignore _speechserver.stop() # type: ignore
if _echoSpeechserver and _echoSpeechserver != _speechserver:
_echoSpeechserver.stop() # type: ignore
def shutdown() -> None: def shutdown() -> None:
debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Shutting down', True) debug.printMessage(debug.LEVEL_INFO, 'SPEECH: Shutting down', True)
global _speechserver global _speechserver, _echoSpeechserver
if _speechserver: if _speechserver:
_speechserver.shutdownActiveServers() # type: ignore _speechserver.shutdownActiveServers() # type: ignore
_speechserver = None _speechserver = None
elif _echoSpeechserver:
_echoSpeechserver.shutdownActiveServers() # type: ignore
_echoSpeechserver = None
def reset(text: Optional[str] = None, acss: Optional[Any] = None) -> None: def reset(text: Optional[str] = None, acss: Optional[Any] = None) -> None:
if _speechserver: if _speechserver:
+3 -1
View File
@@ -678,7 +678,9 @@ class SpeechGenerator(generator.Generator):
and soundEnabled: and soundEnabled:
roleSoundIcon = sound_theme_manager.getManager().getRoleSoundIcon(role) roleSoundIcon = sound_theme_manager.getManager().getRoleSoundIcon(role)
if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY \ if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY \
and soundEnabled and roleSoundIcon: and soundEnabled:
# Stateful controls present their role via state sounds; suppress
# spoken role names here even if no dedicated role icon exists.
if AXUtilities.is_check_box(obj) \ if AXUtilities.is_check_box(obj) \
or AXUtilities.is_check_menu_item(obj) \ or AXUtilities.is_check_menu_item(obj) \
or AXUtilities.is_radio_button(obj) \ or AXUtilities.is_radio_button(obj) \