From e0590631157cb7c40fed8f022899f2dbbd3d9a7b Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 30 Dec 2025 07:04:16 -0500 Subject: [PATCH] Better handling of sound only setting. --- src/cthulhu/scripts/web/speech_generator.py | 4 + src/cthulhu/speech_generator.py | 85 +++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/src/cthulhu/scripts/web/speech_generator.py b/src/cthulhu/scripts/web/speech_generator.py index deb6eb9..53b6867 100644 --- a/src/cthulhu/scripts/web/speech_generator.py +++ b/src/cthulhu/scripts/web/speech_generator.py @@ -375,6 +375,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): if self._script.utilities.shouldVerbalizeAllPunctuation(obj): name = self._script.utilities.verbalizeAllPunctuation(name) + name = self._stripRoleFromNameIfNeeded(obj, name, role) result = [name] result.extend(self.voice(speech_generator.DEFAULT, obj=obj, **args)) return result @@ -423,6 +424,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): name = AXObject.get_name(obj) if not self._script.utilities.hasExplicitName(obj): name = name.strip() + name = self._stripRoleFromNameIfNeeded(obj, name, role) result = [name] result.extend(self.voice(speech_generator.DEFAULT, obj=obj, **args)) @@ -439,6 +441,8 @@ class SpeechGenerator(speech_generator.SpeechGenerator): label, objects = self._script.utilities.inferLabelFor(obj) if label: + role = args.get('role', AXObject.get_role(obj)) + label = self._stripRoleFromNameIfNeeded(obj, label, role) result = [label] result.extend(self.voice(speech_generator.DEFAULT, obj=obj, **args)) return result diff --git a/src/cthulhu/speech_generator.py b/src/cthulhu/speech_generator.py index 8bc1473..618ec8d 100644 --- a/src/cthulhu/speech_generator.py +++ b/src/cthulhu/speech_generator.py @@ -169,6 +169,8 @@ class SpeechGenerator(generator.Generator): result = generator.Generator._generateName(self, obj, **args) if result: + if isinstance(result[0], str): + result[0] = self._stripRoleFromNameIfNeeded(obj, result[0], role) if role == Atspi.Role.LAYERED_PANE: result.extend(self.voice(SYSTEM, obj=obj, **args)) else: @@ -185,6 +187,9 @@ class SpeechGenerator(generator.Generator): result = generator.Generator._generateLabel(self, obj, **args) if result: + role = args.get('role', AXObject.get_role(obj)) + if isinstance(result[0], str): + result[0] = self._stripRoleFromNameIfNeeded(obj, result[0], role) result.extend(self.voice(DEFAULT, obj=obj, **args)) return result @@ -205,6 +210,7 @@ class SpeechGenerator(generator.Generator): if not result: name = AXObject.get_name(obj) if name: + name = self._stripRoleFromNameIfNeeded(obj, name, role) result.append(name) result.extend(self.voice(DEFAULT, obj=obj, **args)) if result: @@ -216,6 +222,85 @@ class SpeechGenerator(generator.Generator): return result + def _stripRoleFromNameIfNeeded(self, obj, name, role=None): + if not name: + return name + + roleSoundPresentation = _settingsManager.getSetting('roleSoundPresentation') + if roleSoundPresentation != settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY: + return name + + if not _settingsManager.getSetting('enableSound'): + return name + + if not self._shouldStripRoleFromName(obj): + return name + + candidates = self._getRoleNameStripCandidates(obj, role) + if not candidates: + return name + + strippedName = name.strip() + strippedNameLower = strippedName.casefold() + for candidate in candidates: + candidate = candidate.strip() + if not candidate: + continue + + candidateLower = candidate.casefold() + if strippedNameLower == candidateLower: + continue + + suffix = f" {candidateLower}" + if strippedNameLower.endswith(suffix): + return strippedName[:-(len(candidateLower) + 1)].rstrip() + + suffix = f" ({candidateLower})" + if strippedNameLower.endswith(suffix): + return strippedName[:-(len(candidateLower) + 3)].rstrip() + + return name + + def _shouldStripRoleFromName(self, obj): + return AXUtilities.is_check_box(obj) \ + or AXUtilities.is_check_menu_item(obj) \ + or AXUtilities.is_combo_box(obj) \ + or AXUtilities.is_push_button(obj) \ + or AXUtilities.is_radio_button(obj) \ + or AXUtilities.is_radio_menu_item(obj) \ + or AXUtilities.is_toggle_button(obj) \ + or AXUtilities.is_switch(obj) + + def _getRoleNameStripCandidates(self, obj, role=None): + normalizedRole = role + if isinstance(normalizedRole, str): + if normalizedRole == "ROLE_SWITCH": + normalizedRole = Atspi.Role.SWITCH + else: + normalizedRole = None + + candidates = [] + if normalizedRole is None and isinstance(role, str): + localizedRoleName = self.getLocalizedRoleName(obj) + else: + localizedRoleName = self.getLocalizedRoleName(obj, role=normalizedRole or role) + if localizedRoleName: + candidates.append(localizedRoleName) + if " " in localizedRoleName: + candidates.append(localizedRoleName.replace(" ", "")) + + if normalizedRole is not None and not isinstance(normalizedRole, str): + roleName = Atspi.role_get_name(normalizedRole) + if roleName: + candidates.append(roleName) + if " " in roleName: + candidates.append(roleName.replace(" ", "")) + + if AXUtilities.is_push_button(obj): + candidates.append("button") + + return list(dict.fromkeys(candidates)) + def _generatePlaceholderText(self, obj, **args): """Returns an array of strings for use by speech and braille that represent the 'placeholder' text. This is typically text that