From 97c9253372909fc151ad67e35db8729ceb918226 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Mon, 29 Dec 2025 19:23:20 -0500 Subject: [PATCH] More work on sound support. --- distro-packages/Arch-Linux/PKGBUILD | 2 +- meson.build | 2 +- sounds/default/button.wav | Bin 0 -> 38444 bytes src/cthulhu/cthulhu-setup.ui | 45 ++++++++++++ src/cthulhu/cthulhuVersion.py | 2 +- src/cthulhu/cthulhu_gui_prefs.py | 28 +++++++ src/cthulhu/scripts/web/speech_generator.py | 20 ++++- src/cthulhu/settings.py | 6 ++ src/cthulhu/sound_theme_manager.py | 77 ++++++++++++++++++++ src/cthulhu/speech.py | 12 ++- src/cthulhu/speech_generator.py | 56 ++++++++++++++ 11 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 sounds/default/button.wav diff --git a/distro-packages/Arch-Linux/PKGBUILD b/distro-packages/Arch-Linux/PKGBUILD index 435cdd0..be65c82 100644 --- a/distro-packages/Arch-Linux/PKGBUILD +++ b/distro-packages/Arch-Linux/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Storm Dragon pkgname=cthulhu -pkgver=2025.12.27 +pkgver=2025.12.28 pkgrel=1 pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca" url="https://git.stormux.org/storm/cthulhu" diff --git a/meson.build b/meson.build index 53233f1..e105bc3 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('cthulhu', - version: '2025.12.28-testing', + version: '2025.12.29-testing', meson_version: '>= 1.0.0', ) diff --git a/sounds/default/button.wav b/sounds/default/button.wav new file mode 100644 index 0000000000000000000000000000000000000000..0bf85234ed5797985a4ec949559fc02df7dbf435 GIT binary patch literal 38444 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 literal 0 HcmV?d00001 diff --git a/src/cthulhu/cthulhu-setup.ui b/src/cthulhu/cthulhu-setup.ui index 04dc7fa..f12d146 100644 --- a/src/cthulhu/cthulhu-setup.ui +++ b/src/cthulhu/cthulhu-setup.ui @@ -1097,6 +1097,51 @@ 1 + + + True + False + 12 + + + True + False + 0 + Element _presentation: + True + roleSoundPresentationCombo + + + + + + False + True + 0 + + + + + True + False + + + + + + + False + True + 1 + + + + + False + True + 2 + + diff --git a/src/cthulhu/cthulhuVersion.py b/src/cthulhu/cthulhuVersion.py index 1ff8320..51fa8f9 100644 --- a/src/cthulhu/cthulhuVersion.py +++ b/src/cthulhu/cthulhuVersion.py @@ -23,5 +23,5 @@ # Forked from Orca screen reader. # Cthulhu project: https://git.stormux.org/storm/cthulhu -version = "2025.12.28" +version = "2025.12.29" codeName = "testing" diff --git a/src/cthulhu/cthulhu_gui_prefs.py b/src/cthulhu/cthulhu_gui_prefs.py index 9d86efb..ff9e7e5 100644 --- a/src/cthulhu/cthulhu_gui_prefs.py +++ b/src/cthulhu/cthulhu_gui_prefs.py @@ -2007,6 +2007,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): self.enableModeChangeSoundCheckButton = self.get_widget( "enableModeChangeSoundCheckButton") self.soundThemeCombo = self.get_widget("soundThemeCombo") + self.roleSoundPresentationCombo = self.get_widget("roleSoundPresentationCombo") # Set enable mode change sound checkbox enabled = prefs.get("enableModeChangeSound", settings.enableModeChangeSound) @@ -2035,6 +2036,25 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): else: self.soundThemeCombo.set_active(0) + self._roleSoundPresentationChoices = [ + (settings.ROLE_SOUND_PRESENTATION_SOUND_AND_SPEECH, "Sound and speech"), + (settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY, "Speech only"), + (settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY, "Sound only"), + ] + self.roleSoundPresentationCombo.remove_all() + for _, label in self._roleSoundPresentationChoices: + self.roleSoundPresentationCombo.append_text(label) + currentPresentation = prefs.get( + "roleSoundPresentation", + settings.roleSoundPresentation + ) + presentationIndex = 0 + for index, (value, _) in enumerate(self._roleSoundPresentationChoices): + if value == currentPresentation: + presentationIndex = index + break + self.roleSoundPresentationCombo.set_active(presentationIndex) + # Update sensitivity based on checkbox self._updateSoundThemeWidgetSensitivity() @@ -2054,6 +2074,14 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): if activeText: self.prefsDict["soundTheme"] = activeText + def roleSoundPresentationComboChanged(self, widget): + """Signal handler for the role sound presentation combo box.""" + activeIndex = widget.get_active() + if activeIndex < 0: + return + value = self._roleSoundPresentationChoices[activeIndex][0] + self.prefsDict["roleSoundPresentation"] = value + def _updateCthulhuModifier(self): combobox = self.get_widget("cthulhuModifierComboBox") keystring = ", ".join(self.prefsDict["cthulhuModifierKeys"]) diff --git a/src/cthulhu/scripts/web/speech_generator.py b/src/cthulhu/scripts/web/speech_generator.py index d21c176..fcf95c7 100644 --- a/src/cthulhu/scripts/web/speech_generator.py +++ b/src/cthulhu/scripts/web/speech_generator.py @@ -43,6 +43,7 @@ from cthulhu import object_properties from cthulhu import cthulhu_state from cthulhu import settings from cthulhu import settings_manager +from cthulhu import sound_theme_manager from cthulhu import speech_generator from cthulhu.ax_object import AXObject from cthulhu.ax_text import AXText @@ -550,7 +551,6 @@ class SpeechGenerator(speech_generator.SpeechGenerator): if roledescription: result = [roledescription] result.extend(self.voice(speech_generator.SYSTEM, obj=obj, **args)) - return result role = args.get('role', AXObject.get_role(obj)) enabled, disabled = self._getEnabledAndDisabledContextRoles() @@ -656,7 +656,23 @@ class SpeechGenerator(speech_generator.SpeechGenerator): and (index == total - 1 or AXObject.get_name(obj) == AXObject.get_name(ancestor)): result.extend(self._generateRoleName(ancestor)) - return result + if not result: + return result + + roleSoundPresentation = _settingsManager.getSetting('roleSoundPresentation') + if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY: + return result + if not _settingsManager.getSetting('enableSound'): + return result + + roleSoundIcon = sound_theme_manager.getManager().getRoleSoundIcon(role) + if not roleSoundIcon: + return result + + if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY: + return [roleSoundIcon] + + return [roleSoundIcon] + result def _generatePageSummary(self, obj, **args): if not self._script.utilities.inDocumentContent(obj): diff --git a/src/cthulhu/settings.py b/src/cthulhu/settings.py index ab18cb4..5a2cad0 100644 --- a/src/cthulhu/settings.py +++ b/src/cthulhu/settings.py @@ -78,6 +78,7 @@ userCustomizableSettings = [ "playSoundForState", "playSoundForPositionInSet", "playSoundForValue", + "roleSoundPresentation", "soundTheme", "enableModeChangeSound", "verbalizePunctuationStyle", @@ -309,12 +310,17 @@ textAttributesBrailleIndicator = BRAILLE_UNDERLINE_NONE brailleVerbosityLevel = VERBOSITY_LEVEL_VERBOSE # Sound +ROLE_SOUND_PRESENTATION_SOUND_AND_SPEECH = "sound_and_speech" +ROLE_SOUND_PRESENTATION_SPEECH_ONLY = "speech_only" +ROLE_SOUND_PRESENTATION_SOUND_ONLY = "sound_only" + enableSound = True soundVolume = 0.5 playSoundForRole = False playSoundForState = False playSoundForPositionInSet = False playSoundForValue = False +roleSoundPresentation = ROLE_SOUND_PRESENTATION_SOUND_AND_SPEECH soundTheme = "default" enableModeChangeSound = True diff --git a/src/cthulhu/sound_theme_manager.py b/src/cthulhu/sound_theme_manager.py index 58c2dde..0625227 100644 --- a/src/cthulhu/sound_theme_manager.py +++ b/src/cthulhu/sound_theme_manager.py @@ -38,6 +38,7 @@ __license__ = "LGPL" import os from gi.repository import GLib +from gi.repository import Atspi from . import debug from . import settings_manager @@ -53,6 +54,28 @@ SOUND_BUTTON = "button" SOUND_START = "start" SOUND_STOP = "stop" +ROLE_SOUND_ALIASES = { + Atspi.Role.PUSH_BUTTON: ["button", "push_button"], + Atspi.Role.TOGGLE_BUTTON: ["toggle_button", "button"], + Atspi.Role.RADIO_BUTTON: ["radio_button", "radio"], + Atspi.Role.CHECK_BOX: ["checkbox", "check_box"], +} + +ROLE_STATE_SOUND_BASES = { + Atspi.Role.CHECK_BOX: ["checkbox", "check_box"], + Atspi.Role.CHECK_MENU_ITEM: ["checkbox", "check_box"], + Atspi.Role.RADIO_BUTTON: ["radiobutton", "radio_button"], + Atspi.Role.RADIO_MENU_ITEM: ["radiobutton", "radio_button"], + Atspi.Role.TOGGLE_BUTTON: ["togglebutton", "toggle_button"], + Atspi.Role.SWITCH: ["switch"], +} + +STATE_SOUND_SUFFIXES = { + "checked": ["checked", "on", "pressed", "selected"], + "unchecked": ["unchecked", "off", "not_pressed", "unselected"], + "mixed": ["mixed", "partially_checked", "indeterminate"], +} + # Special theme name for no sounds THEME_NONE = "none" @@ -146,6 +169,60 @@ class SoundThemeManager: return None + def _getRoleSoundCandidates(self, role): + """Return candidate sound names for a given role.""" + candidates = ROLE_SOUND_ALIASES.get(role, []) + if candidates: + return candidates + + roleName = Atspi.role_get_name(role) + if roleName: + return [roleName.replace(' ', '_')] + + return [] + + def _getRoleStateSoundCandidates(self, role, stateKey): + """Return candidate sound names for a given role/state pair.""" + bases = ROLE_STATE_SOUND_BASES.get(role, []) + if not bases: + return [] + + suffixes = STATE_SOUND_SUFFIXES.get(stateKey, []) + if not suffixes: + return [] + + candidates = [] + for base in bases: + for suffix in suffixes: + candidates.append(f"{base}_{suffix}") + return candidates + + def getRoleSoundIcon(self, role, themeName=None): + """Return an Icon for the role sound from the current theme, if any.""" + themeName = themeName or _settingsManager.getSetting('soundTheme') or 'default' + if themeName == THEME_NONE: + return None + + for candidate in self._getRoleSoundCandidates(role): + soundPath = self.getSoundPath(themeName, candidate) + if soundPath: + return Icon(os.path.dirname(soundPath), os.path.basename(soundPath)) + + return None + + def getRoleStateSoundIcon(self, role, stateKey, themeName=None): + """Return an Icon for the role/state sound from the current theme, if any.""" + themeName = themeName or _settingsManager.getSetting('soundTheme') or 'default' + if themeName == THEME_NONE: + return None + + for candidate in self._getRoleStateSoundCandidates(role, stateKey): + soundPath = self.getSoundPath(themeName, candidate) + if soundPath: + return Icon(os.path.dirname(soundPath), os.path.basename(soundPath)) + + return None + def _playThemeSound(self, soundName, interrupt=True, wait=False, requireModeChangeSetting=False, requireSoundSetting=False): """Play a themed sound with optional gating and blocking. diff --git a/src/cthulhu/speech.py b/src/cthulhu/speech.py index 3b394a0..eabffb9 100644 --- a/src/cthulhu/speech.py +++ b/src/cthulhu/speech.py @@ -39,6 +39,8 @@ from . import debug from . import logger from . import settings from . import speech_generator +from . import sound +from .sound_generator import Icon from .speechserver import VoiceFamily from .acss import ACSS @@ -240,7 +242,7 @@ def speak(content, acss=None, interrupt=True): return validTypes = (str, list, speech_generator.Pause, - speech_generator.LineBreak, ACSS) + speech_generator.LineBreak, ACSS, Icon) error = "SPEECH: bad content sent to speak(): '%s'" if not isinstance(content, validTypes): debug.printMessage(debug.LEVEL_INFO, error % content, True) @@ -277,6 +279,14 @@ def speak(content, acss=None, interrupt=True): elif isinstance(element, str): if len(element): toSpeak.append(element) + elif isinstance(element, Icon): + if toSpeak: + string = " ".join(toSpeak) + _speak(string, activeVoice, interrupt) + toSpeak = [] + if element.isValid(): + player = sound.getPlayer() + player.play(element, interrupt=False) elif toSpeak: newVoice = ACSS(acss) newItemsToSpeak = [] diff --git a/src/cthulhu/speech_generator.py b/src/cthulhu/speech_generator.py index 50495a1..03d9988 100644 --- a/src/cthulhu/speech_generator.py +++ b/src/cthulhu/speech_generator.py @@ -49,6 +49,7 @@ from . import messages from . import object_properties from . import settings from . import settings_manager +from . import sound_theme_manager from . import speech from . import text_attribute_names from .ax_object import AXObject @@ -579,6 +580,11 @@ class SpeechGenerator(generator.Generator): result = [] role = args.get('role', AXObject.get_role(obj)) + roleSoundPresentation = _settingsManager.getSetting('roleSoundPresentation') + roleSoundIcon = None + if roleSoundPresentation != settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY \ + and _settingsManager.getSetting('enableSound'): + roleSoundIcon = sound_theme_manager.getManager().getRoleSoundIcon(role) doNotPresent = [Atspi.Role.UNKNOWN, Atspi.Role.REDUNDANT_OBJECT, @@ -620,6 +626,13 @@ class SpeechGenerator(generator.Generator): if role not in doNotPresent and not result: result.append(self.getLocalizedRoleName(obj, **args)) result.extend(self.voice(SYSTEM, obj=obj, **args)) + + if result and roleSoundIcon: + if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY: + return [roleSoundIcon] + if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_AND_SPEECH: + return [roleSoundIcon] + result + return result def getRoleName(self, obj, **args): @@ -693,6 +706,23 @@ class SpeechGenerator(generator.Generator): # # ##################################################################### + def _applyStateSound(self, result, role, stateKey): + if not result: + return result + + if _settingsManager.getSetting('roleSoundPresentation') \ + == settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY: + return result + + if not _settingsManager.getSetting('enableSound'): + return result + + icon = sound_theme_manager.getManager().getRoleStateSoundIcon(role, stateKey) + if not icon: + return result + + return [icon] + result + def _generateCheckedState(self, obj, **args): """Returns an array of strings for use by speech and braille that represent the checked state of the object. This is typically @@ -705,6 +735,16 @@ class SpeechGenerator(generator.Generator): result = generator.Generator._generateCheckedState(self, obj, **args) if result: result.extend(self.voice(STATE, obj=obj, **args)) + role = args.get('role', AXObject.get_role(obj)) + if AXUtilities.is_checkable(obj) or AXUtilities.is_check_menu_item(obj): + role = Atspi.Role.CHECK_BOX + if AXUtilities.is_indeterminate(obj): + stateKey = "mixed" + elif AXUtilities.is_checked(obj): + stateKey = "checked" + else: + stateKey = "unchecked" + result = self._applyStateSound(result, role, stateKey) return result def _generateExpandableState(self, obj, **args): @@ -742,6 +782,11 @@ class SpeechGenerator(generator.Generator): _generateMenuItemCheckedState(self, obj, **args) if result: result.extend(self.voice(STATE, obj=obj, **args)) + result = self._applyStateSound( + result, + Atspi.Role.CHECK_MENU_ITEM, + "checked" + ) return result def _generateMultiselectableState(self, obj, **args): @@ -770,6 +815,9 @@ class SpeechGenerator(generator.Generator): result = generator.Generator._generateRadioState(self, obj, **args) if result: result.extend(self.voice(STATE, obj=obj, **args)) + stateKey = "checked" if AXUtilities.is_checked(obj) else "unchecked" + role = args.get('role', AXObject.get_role(obj)) + result = self._applyStateSound(result, role, stateKey) return result def _generateSwitchState(self, obj, **args): @@ -780,6 +828,10 @@ class SpeechGenerator(generator.Generator): result = generator.Generator._generateSwitchState(self, obj, **args) if result: result.extend(self.voice(STATE, obj=obj, **args)) + stateKey = "checked" if (AXUtilities.is_checked(obj) or AXUtilities.is_pressed(obj)) \ + else "unchecked" + role = args.get('role', AXObject.get_role(obj)) + result = self._applyStateSound(result, role, stateKey) return result def _generateToggleState(self, obj, **args): @@ -794,6 +846,10 @@ class SpeechGenerator(generator.Generator): result = generator.Generator._generateToggleState(self, obj, **args) if result: result.extend(self.voice(STATE, obj=obj, **args)) + stateKey = "checked" if (AXUtilities.is_checked(obj) or AXUtilities.is_pressed(obj)) \ + else "unchecked" + role = args.get('role', AXObject.get_role(obj)) + result = self._applyStateSound(result, role, stateKey) return result #####################################################################