From 296c47be361d534d06accc484d91bcb08eb02900 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 5 Apr 2026 21:32:06 -0400 Subject: [PATCH] Sounds split off into own tab in settings. --- src/cthulhu/cthulhu-setup.ui | 368 +++++++++++++++++--- src/cthulhu/cthulhu_gui_prefs.py | 38 ++ src/cthulhu/scripts/web/script.py | 4 - tests/test_sound_preferences_regressions.py | 148 ++++++++ tests/test_web_router_regressions.py | 1 + 5 files changed, 503 insertions(+), 56 deletions(-) diff --git a/src/cthulhu/cthulhu-setup.ui b/src/cthulhu/cthulhu-setup.ui index 295958b..1fa5b4a 100644 --- a/src/cthulhu/cthulhu-setup.ui +++ b/src/cthulhu/cthulhu-setup.ui @@ -124,6 +124,12 @@ 1 10 + + 9999 + 0 + 1 + 10 + 100 50 @@ -136,6 +142,12 @@ 0.10000000149 1 + + 1 + 0.5 + 0.05000000075 + 0.10000000149 + 10 5 @@ -777,25 +789,6 @@ 3 - - - Bee_p updates - True - True - False - start - True - True - 0 - True - True - - - - 0 - 2 - - @@ -1036,23 +1029,96 @@ 2 + + + 0 + + + + + True + False + General + + + 0 + False + + + + + True + False + 12 + 10 + 10 - + True False 0 none - + True False 12 - + True False - vertical - 6 + 10 + 20 + + + Enable _sounds + True + True + False + True + 0 + True + True + + + + 0 + 0 + 2 + + + + + True + False + 0 + Sound _volume: + True + soundVolumeScale + + + + + + 0 + 1 + + + + + True + True + soundVolumeAdjustment + 2 + 2 + right + + + + 1 + 1 + + True @@ -1093,11 +1159,48 @@ - False - True - 0 + 0 + 2 + 2 + + + + + + + True + False + Output + + + + + + + + 0 + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 10 + 20 True @@ -1138,9 +1241,9 @@ - False - True - 1 + 0 + 0 + 2 @@ -1183,9 +1286,77 @@ - False - True - 2 + 0 + 1 + 2 + + + + + Play sound for _role + True + True + False + True + 0 + True + + + + 0 + 2 + 2 + + + + + Play sound for _state + True + True + False + True + 0 + True + + + + 0 + 3 + 2 + + + + + Play sound for p_osition in set + True + True + False + True + 0 + True + + + + 0 + 4 + 2 + + + + + Play sound for _value + True + True + False + True + 0 + True + + + + 0 + 5 + 2 @@ -1193,10 +1364,10 @@ - + True False - Sound + Presentation @@ -1205,18 +1376,111 @@ 1 - 4 + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 10 + 20 + + + Bee_p progress bar updates + True + True + False + True + 0 + True + True + + + + 0 + 0 + 2 + + + + + True + False + 0 + Beep _frequency (secs): + True + progressBarBeepIntervalSpinButton + + + + + + 0 + 1 + + + + + True + True + 0 + progressBarBeepIntervalAdjustment + 1 + True + 0 + + + + 1 + 1 + + + + + + + + + True + False + Alerts + + + + + + + + 0 + 1 + 2 + + 1 + - + True False - General + Sounds + 1 False @@ -1614,7 +1878,7 @@ - 1 + 2 @@ -1624,7 +1888,7 @@ Voice - 1 + 2 False @@ -2312,7 +2576,7 @@ - 2 + 3 @@ -2322,7 +2586,7 @@ Speech - 2 + 3 False @@ -2911,7 +3175,7 @@ - 3 + 4 @@ -2921,7 +3185,7 @@ Braille - 3 + 4 False @@ -3436,7 +3700,7 @@ - 4 + 5 @@ -3446,7 +3710,7 @@ Echo - 4 + 5 False @@ -3528,7 +3792,7 @@ - 5 + 6 @@ -3538,7 +3802,7 @@ Key Bindings - 5 + 6 False @@ -3638,7 +3902,7 @@ - 6 + 7 @@ -3648,7 +3912,7 @@ Pronunciation - 6 + 7 False @@ -3983,7 +4247,7 @@ - 7 + 8 @@ -3993,7 +4257,7 @@ Text Attributes - 7 + 8 False diff --git a/src/cthulhu/cthulhu_gui_prefs.py b/src/cthulhu/cthulhu_gui_prefs.py index 412a5d1..6ecfa54 100644 --- a/src/cthulhu/cthulhu_gui_prefs.py +++ b/src/cthulhu/cthulhu_gui_prefs.py @@ -198,6 +198,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): self.soundThemeCombo = None self.soundSinkCombo = None self.roleSoundPresentationCombo = None + self.soundVolumeScale = None + self.progressBarBeepIntervalSpinButton = None self._isInitialSetup = False self._updatingSpeechFamilies = False self.selectedFamilyChoices = {} @@ -3111,9 +3113,35 @@ print(json.dumps(result)) self.soundSinkCombo = self.get_widget("soundSinkCombo") self.soundThemeCombo = self.get_widget("soundThemeCombo") self.roleSoundPresentationCombo = self.get_widget("roleSoundPresentationCombo") + self.soundVolumeScale = self.get_widget("soundVolumeScale") + self.progressBarBeepIntervalSpinButton = self.get_widget("progressBarBeepIntervalSpinButton") self.soundSinkCombo.set_can_focus(False) self.soundThemeCombo.set_can_focus(False) self.roleSoundPresentationCombo.set_can_focus(False) + self.get_widget("enableSoundCheckButton").set_active( + prefs.get("enableSound", settings.enableSound) + ) + self.soundVolumeScale.set_value( + float(prefs.get("soundVolume", settings.soundVolume)) + ) + self.get_widget("playSoundForRoleCheckButton").set_active( + prefs.get("playSoundForRole", settings.playSoundForRole) + ) + self.get_widget("playSoundForStateCheckButton").set_active( + prefs.get("playSoundForState", settings.playSoundForState) + ) + self.get_widget("playSoundForPositionInSetCheckButton").set_active( + prefs.get("playSoundForPositionInSet", settings.playSoundForPositionInSet) + ) + self.get_widget("playSoundForValueCheckButton").set_active( + prefs.get("playSoundForValue", settings.playSoundForValue) + ) + self.get_widget("beepProgressBarUpdatesCheckButton").set_active( + prefs.get("beepProgressBarUpdates", settings.beepProgressBarUpdates) + ) + self.progressBarBeepIntervalSpinButton.set_value( + int(prefs.get("progressBarBeepInterval", settings.progressBarBeepInterval)) + ) self._soundSinkChoices = [ (settings.SOUND_SINK_AUTO, guilabels.SOUND_BACKEND_AUTO), @@ -3199,6 +3227,14 @@ print(json.dumps(result)) value = self._roleSoundPresentationChoices[activeIndex][0] self.prefsDict["roleSoundPresentation"] = value + def soundVolumeValueChanged(self, widget): + """Signal handler for the sound volume scale.""" + self.prefsDict["soundVolume"] = widget.get_value() + + def progressBarBeepIntervalValueChanged(self, widget): + """Signal handler for the progress bar beep interval spin button.""" + self.prefsDict["progressBarBeepInterval"] = widget.get_value_as_int() + def _updateCthulhuModifier(self): combobox = self.get_widget("cthulhuModifierComboBox") @@ -4974,6 +5010,7 @@ print(json.dumps(result)) self._apply_plugin_changes() self.writeUserPreferences() cthulhu.loadUserSettings(self.script) + self._initSoundThemeState() self._refresh_dynamic_plugin_tabs() braille.checkBrailleSetting() self._initSpeechState() @@ -5236,6 +5273,7 @@ print(json.dumps(result)) cthulhu.loadUserSettings(skipReloadMessage=True) self._initGUIState() + self._initSoundThemeState() braille.checkBrailleSetting() diff --git a/src/cthulhu/scripts/web/script.py b/src/cthulhu/scripts/web/script.py index c4bbdac..9916b99 100644 --- a/src/cthulhu/scripts/web/script.py +++ b/src/cthulhu/scripts/web/script.py @@ -1503,13 +1503,11 @@ class Script(default.Script): self.presentMessage("Activating...") result = self._performClickableAction(obj) if result: - self._presentDelayedMessage("Element activated", 50) return True from cthulhu import ax_event_synthesizer result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj) if result: - self._presentDelayedMessage("Element activated", 50) self._restoreFocusAfterClick(original_focus) return True @@ -1520,13 +1518,11 @@ class Script(default.Script): self.presentMessage("Activating...") result = self._performClickableAction(obj) if result: - self._presentDelayedMessage("Element activated", 50) return True from cthulhu import ax_event_synthesizer result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj) if result: - self._presentDelayedMessage("Element activated", 50) self._restoreFocusAfterClick(original_focus) return True diff --git a/tests/test_sound_preferences_regressions.py b/tests/test_sound_preferences_regressions.py index 4e31837..531a1c3 100644 --- a/tests/test_sound_preferences_regressions.py +++ b/tests/test_sound_preferences_regressions.py @@ -1,12 +1,15 @@ import sys import tempfile import unittest +import xml.etree.ElementTree as ET from pathlib import Path from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) +from cthulhu import cthulhu from cthulhu import settings +from cthulhu import cthulhu_gui_prefs from cthulhu import settings_manager @@ -23,5 +26,150 @@ class SoundSettingsPersistenceTests(unittest.TestCase): self.assertIn("progressBarBeepInterval = 0", settingsText) +class SoundPreferencesBuilderTests(unittest.TestCase): + def test_ui_definition_exposes_sounds_tab_controls(self): + uiPath = Path(__file__).resolve().parents[1] / "src" / "cthulhu" / "cthulhu-setup.ui" + root = ET.fromstring(uiPath.read_text(encoding="utf-8")) + + objectIds = { + element.attrib["id"] + for element in root.iter() + if element.tag == "object" and "id" in element.attrib + } + + self.assertIn("soundsGrid", objectIds) + self.assertIn("soundsTabLabel", objectIds) + self.assertIn("enableSoundCheckButton", objectIds) + self.assertIn("soundVolumeScale", objectIds) + self.assertIn("progressBarBeepIntervalSpinButton", objectIds) + + def test_notebook_tab_positions_keep_sounds_page_and_label_aligned(self): + uiPath = Path(__file__).resolve().parents[1] / "src" / "cthulhu" / "cthulhu-setup.ui" + root = ET.fromstring(uiPath.read_text(encoding="utf-8")) + notebook = next( + element for element in root.iter() + if element.tag == "object" and element.attrib.get("id") == "notebook" + ) + + positions = {} + for child in notebook.findall("child"): + obj = child.find("object") + if obj is None or "id" not in obj.attrib: + continue + position = None + packing = child.find("packing") + if packing is not None: + for prop in packing.findall("property"): + if prop.attrib.get("name") == "position": + position = int(prop.text) + break + positions[obj.attrib["id"]] = position + + self.assertEqual(positions["generalGrid"], 0) + self.assertEqual(positions["generalTabLabel"], 0) + self.assertEqual(positions["soundsGrid"], 1) + self.assertEqual(positions["soundsTabLabel"], 1) + self.assertEqual(positions["voiceGrid"], 2) + self.assertEqual(positions["voiceLabel"], 2) + self.assertEqual(positions["textAttributesGrid"], 8) + self.assertEqual(positions["textAttributesTabLabel"], 8) + + +class SoundPreferencesControllerTests(unittest.TestCase): + def test_init_sound_state_loads_sound_volume_and_beep_interval(self): + gui = cthulhu_gui_prefs.CthulhuSetupGUI.__new__(cthulhu_gui_prefs.CthulhuSetupGUI) + gui.prefsDict = { + "enableSound": False, + "soundSink": settings.SOUND_SINK_PULSE, + "soundTheme": "default", + "roleSoundPresentation": settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY, + "soundVolume": 0.65, + "progressBarBeepInterval": 0, + "beepProgressBarUpdates": True, + "playSoundForRole": True, + "playSoundForState": True, + "playSoundForPositionInSet": False, + "playSoundForValue": False, + } + widgets = { + "soundSinkCombo": mock.Mock(), + "soundThemeCombo": mock.Mock(), + "roleSoundPresentationCombo": mock.Mock(), + "soundVolumeScale": mock.Mock(), + "progressBarBeepIntervalSpinButton": mock.Mock(), + "enableSoundCheckButton": mock.Mock(), + "playSoundForRoleCheckButton": mock.Mock(), + "playSoundForStateCheckButton": mock.Mock(), + "playSoundForPositionInSetCheckButton": mock.Mock(), + "playSoundForValueCheckButton": mock.Mock(), + "beepProgressBarUpdatesCheckButton": mock.Mock(), + } + gui.get_widget = widgets.__getitem__ + + fakeThemeManager = mock.Mock() + fakeThemeManager.getAvailableThemes.return_value = ["default"] + fakeSettingsManager = mock.Mock() + fakeSettingsManager.getSetting.return_value = settings.SOUND_SINK_AUTO + fakeApp = mock.Mock(settingsManager=fakeSettingsManager) + + with ( + mock.patch.object(cthulhu_gui_prefs.cthulhu, "cthulhuApp", fakeApp), + mock.patch.object( + cthulhu_gui_prefs.sound_theme_manager, + "getManager", + return_value=fakeThemeManager, + ), + ): + gui._initSoundThemeState() + + widgets["enableSoundCheckButton"].set_active.assert_called_once_with(False) + widgets["soundVolumeScale"].set_value.assert_called_once_with(0.65) + widgets["playSoundForRoleCheckButton"].set_active.assert_called_once_with(True) + widgets["playSoundForStateCheckButton"].set_active.assert_called_once_with(True) + widgets["playSoundForPositionInSetCheckButton"].set_active.assert_called_once_with(False) + widgets["playSoundForValueCheckButton"].set_active.assert_called_once_with(False) + widgets["beepProgressBarUpdatesCheckButton"].set_active.assert_called_once_with(True) + widgets["progressBarBeepIntervalSpinButton"].set_value.assert_called_once_with(0) + + def test_sound_value_handlers_store_current_values(self): + gui = cthulhu_gui_prefs.CthulhuSetupGUI.__new__(cthulhu_gui_prefs.CthulhuSetupGUI) + gui.prefsDict = {} + + volumeWidget = mock.Mock() + volumeWidget.get_value.return_value = 0.7 + beepWidget = mock.Mock() + beepWidget.get_value_as_int.return_value = 0 + + gui.soundVolumeValueChanged(volumeWidget) + gui.progressBarBeepIntervalValueChanged(beepWidget) + + self.assertEqual(gui.prefsDict["soundVolume"], 0.7) + self.assertEqual(gui.prefsDict["progressBarBeepInterval"], 0) + + def test_load_profile_reinitializes_sound_state(self): + gui = cthulhu_gui_prefs.CthulhuSetupGUI.__new__(cthulhu_gui_prefs.CthulhuSetupGUI) + gui.saveBasicSettings = mock.Mock() + gui._initGUIState = mock.Mock() + gui._initSoundThemeState = mock.Mock() + gui._initSpeechState = mock.Mock() + gui._initEchoSpeechState = mock.Mock() + gui._populateKeyBindings = mock.Mock() + gui._CthulhuSetupGUI__initProfileCombo = mock.Mock() + gui.prefsDict = {} + + fakeSettingsManager = mock.Mock() + fakeSettingsManager.getGeneralSettings.return_value = {"soundVolume": 0.5} + fakeApp = mock.Mock(settingsManager=fakeSettingsManager) + + with ( + mock.patch.object(cthulhu_gui_prefs.cthulhu, "cthulhuApp", fakeApp), + mock.patch.object(cthulhu_gui_prefs.cthulhu, "loadUserSettings"), + mock.patch.object(cthulhu_gui_prefs.braille, "checkBrailleSetting"), + ): + gui.loadProfile(["Default", "default"]) + + gui._initSoundThemeState.assert_called_once_with() + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_web_router_regressions.py b/tests/test_web_router_regressions.py index b06c549..9683f1b 100644 --- a/tests/test_web_router_regressions.py +++ b/tests/test_web_router_regressions.py @@ -58,6 +58,7 @@ class WebClickableActivationTests(unittest.TestCase): doAction.assert_called_once_with(caretObject, "click-ancestor") clickObject.assert_not_called() + testScript._presentDelayedMessage.assert_not_called() class WebHiddenPopupTests(unittest.TestCase):