3 Commits
7 changed files with 702 additions and 56 deletions
@@ -0,0 +1,171 @@
# Sounds Tab Design
Date: 2026-04-05
## Context
Cthulhu currently stores several non-speech sound settings in the general settings file, but the preferences dialog does not present them coherently:
- The General tab contains an inline Sound section for backend, theme, and role-sound presentation.
- The progress-bar beep checkbox lives in the General tab's progress-bar section.
- `soundVolume` exists in settings and the user's active profile, but there is no UI control for it.
- `enableSound`, `playSoundForRole`, `playSoundForState`, `playSoundForPositionInSet`, and `playSoundForValue` exist as persisted settings but are not currently exposed in the dialog.
- `progressBarBeepInterval` exists in runtime defaults but is not part of the normal persisted general-settings key list.
The result is that sound-related behavior is split across the dialog and some active settings cannot be inspected or changed from the UI.
## Goals
- Add a dedicated `Sounds` preferences tab.
- Move currently exposed sound-related controls out of `General` and into `Sounds`.
- Move the progress-bar beep option into `Sounds` while leaving the rest of the progress-bar section in `General`.
- Expose currently hidden sound-related settings in the UI.
- Add a UI control for `soundVolume`.
- Expose and persist `progressBarBeepInterval`.
- Preserve the existing settings model and save format.
- Avoid clobbering unrelated settings when saving preferences.
## Non-Goals
- No refactor of the broader preferences architecture.
- No changes to sound playback implementation outside what is needed to load and save settings correctly.
- No new settings schema beyond persisting the existing hidden `progressBarBeepInterval`.
- No dynamic enable/disable logic for sound controls in this pass.
## Design Summary
Add a new notebook page labeled `Sounds` and make it the single home for non-speech sound configuration. Remove the inline Sound frame from `General`. Keep the existing progress-bar speech/braille controls and shared progress-bar behavior controls in `General`, but move beep-specific controls into `Sounds`.
The new tab will use existing settings keys wherever possible and will save through the existing `prefsDict` and settings-manager flow.
## Sounds Tab Layout
The `Sounds` tab will contain three sections.
### Output
- `enableSound` checkbox
- `soundVolume` slider
- `soundSink` combo box
`soundVolume` should be represented by a native `GtkScale` and persisted back to the existing floating-point `soundVolume` setting. The control should initialize from `prefsDict.get("soundVolume", settings.soundVolume)`.
For this implementation, the slider should match the stored setting directly rather than introducing a new percentage representation. That keeps the implementation small and avoids conversion-only complexity in a settings path that already exists.
### Presentation
- `soundTheme` combo box
- `roleSoundPresentation` combo box
- `playSoundForRole` checkbox
- `playSoundForState` checkbox
- `playSoundForPositionInSet` checkbox
- `playSoundForValue` checkbox
These controls will expose already-existing settings and use the same `prefsDict` update pattern already used by the current dialog.
### Alerts
- `beepProgressBarUpdates` checkbox
- `progressBarBeepInterval` spin button
This section controls only beep-based progress-bar alerts. The label should make it clear that the interval is beep-specific, not the general progress-bar announcement interval.
## General Tab Changes
Keep these controls in `General`:
- `speakProgressBarUpdates`
- `brailleProgressBarUpdates`
- `progressBarUpdateInterval`
- `progressBarVerbosity`
- `ignoreStatusBarProgressBars`
Remove these from `General`:
- the inline Sound frame
- `beepProgressBarUpdates`
This preserves the current organization for general progress-bar announcement behavior while moving sound-only concerns into the new tab.
## Settings and Persistence
### Existing Keys Reused
The new UI will continue to use the existing settings keys:
- `enableSound`
- `soundVolume`
- `soundSink`
- `soundTheme`
- `roleSoundPresentation`
- `playSoundForRole`
- `playSoundForState`
- `playSoundForPositionInSet`
- `playSoundForValue`
- `beepProgressBarUpdates`
### Persisted Hidden Key
Add `progressBarBeepInterval` to the persisted general-settings key list so it round-trips through:
- defaults in `settings.py`
- settings-manager general settings
- TOML backend save/load
- `prefsDict` in the preferences dialog
This avoids the current mismatch where beep interval exists as a runtime default but is not part of the normal persisted general-settings path.
### Save Behavior
The implementation should continue to update individual keys in `prefsDict` and then rely on the existing general-settings save flow. No bulk replacement of the whole settings structure should be introduced.
This is specifically intended to avoid accidental overwrites of unrelated settings already present in `user-settings.toml`.
## Default Values
- Keep `progressBarUpdateInterval = 10` as the default for speech and braille progress-bar updates.
- Keep `progressBarBeepInterval = 0` as the default for beep-based progress-bar updates.
- Keep `soundVolume = 0.5` as the existing default unless the user explicitly changes it.
The new UI should reflect these values accurately on load.
## Accessibility
Use native GTK controls and follow the existing preferences-dialog patterns:
- every label must be associated with its control via `mnemonic_widget`
- checkboxes should use the existing `checkButtonToggled` convention when possible
- combo boxes should keep proper `label-for` / `labelled-by` relationships
- no custom-drawn controls
- no keyboard traps
The new tab should be fully reachable with standard tab navigation.
## Implementation Notes
Expected files to change:
- `src/cthulhu/cthulhu-setup.ui`
- `src/cthulhu/cthulhu_gui_prefs.py`
- `src/cthulhu/settings.py`
Implementation should:
- add the new notebook page and move or recreate the relevant widgets there
- initialize and save the new sound controls using existing dialog patterns
- add explicit handling for `soundVolume`
- add explicit handling for `progressBarBeepInterval`
- keep all unrelated preference behavior unchanged
## Verification
The implementation should be considered complete only after all of the following are checked:
1. The preferences dialog opens and the new `Sounds` tab is present.
2. Existing sound settings load into the correct controls.
3. Changing sound settings updates `prefsDict` without breaking unrelated settings.
4. Saving preferences preserves unrelated keys in `user-settings.toml`.
5. `progressBarBeepInterval` is written and reloaded correctly.
6. The General tab still handles speech/braille progress-bar settings correctly.
7. Touched Python files pass compile checks.
8. Manual verification confirms keyboard navigation and label association remain intact.
+316 -52
View File
@@ -124,6 +124,12 @@
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkAdjustment" id="progressBarBeepIntervalAdjustment">
<property name="upper">9999</property>
<property name="value">0</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkAdjustment" id="rateAdjustment">
<property name="upper">100</property>
<property name="value">50</property>
@@ -136,6 +142,12 @@
<property name="step_increment">0.10000000149</property>
<property name="page_increment">1</property>
</object>
<object class="GtkAdjustment" id="soundVolumeAdjustment">
<property name="upper">1</property>
<property name="value">0.5</property>
<property name="step_increment">0.05000000075</property>
<property name="page_increment">0.10000000149</property>
</object>
<object class="GtkAdjustment" id="echoPitchAdjustment">
<property name="upper">10</property>
<property name="value">5</property>
@@ -777,25 +789,6 @@
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="beepProgressBarUpdatesCheckButton">
<property name="label" translatable="yes" comments="Translators: This is an option in the Preferences dialog box related to the presentation of progress bar updates. If this checkbox is checked, Cthulhu will periodically emit beeps which increase in pitch as the value of the progress bar increases.">Bee_p updates</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="valign">start</property>
<property name="hexpand">True</property>
<property name="use_underline">True</property>
<property name="xalign">0</property>
<property name="active">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">2</property>
</packing>
</child>
</object>
</child>
</object>
@@ -1036,23 +1029,96 @@
<property name="height">2</property>
</packing>
</child>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="generalTabLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">General</property>
</object>
<packing>
<property name="position">0</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="soundsGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">12</property>
<property name="row_spacing">10</property>
<property name="column_spacing">10</property>
<child>
<object class="GtkFrame" id="soundThemeFrame">
<object class="GtkFrame" id="soundOutputFrame">
<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="soundThemeAlignment">
<object class="GtkAlignment" id="soundOutputAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkBox" id="soundThemeVBox">
<object class="GtkGrid" id="soundOutputGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<property name="row_spacing">10</property>
<property name="column_spacing">20</property>
<child>
<object class="GtkCheckButton" id="enableSoundCheckButton">
<property name="label" translatable="yes">Enable _sounds</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="xalign">0</property>
<property name="active">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">0</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="soundVolumeLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Sound _volume:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">soundVolumeScale</property>
<accessibility>
<relation type="label-for" target="soundVolumeScale"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkScale" id="soundVolumeScale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">soundVolumeAdjustment</property>
<property name="digits">2</property>
<property name="round_digits">2</property>
<property name="value_pos">right</property>
<signal name="value-changed" handler="soundVolumeValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="soundSinkHBox">
<property name="visible">True</property>
@@ -1093,11 +1159,48 @@
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
<property name="width">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="soundOutputTitleLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Output</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="soundPresentationFrame">
<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="soundPresentationAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkGrid" id="soundPresentationGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">10</property>
<property name="column_spacing">20</property>
<child>
<object class="GtkBox" id="soundThemeHBox">
<property name="visible">True</property>
@@ -1138,9 +1241,9 @@
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="width">2</property>
</packing>
</child>
<child>
@@ -1183,9 +1286,77 @@
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="playSoundForRoleCheckButton">
<property name="label" translatable="yes">Play sound for _role</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="xalign">0</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">2</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="playSoundForStateCheckButton">
<property name="label" translatable="yes">Play sound for _state</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="xalign">0</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">3</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="playSoundForPositionInSetCheckButton">
<property name="label" translatable="yes">Play sound for p_osition in set</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="xalign">0</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">4</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="playSoundForValueCheckButton">
<property name="label" translatable="yes">Play sound for _value</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="xalign">0</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">5</property>
<property name="width">2</property>
</packing>
</child>
</object>
@@ -1193,10 +1364,10 @@
</object>
</child>
<child type="label">
<object class="GtkLabel" id="soundThemeTitleLabel">
<object class="GtkLabel" id="soundPresentationTitleLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes" comments="Translators: This is the title of a section in the preferences dialog containing sound options.">Sound</property>
<property name="label" translatable="yes">Presentation</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
@@ -1205,18 +1376,111 @@
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">4</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="soundAlertsFrame">
<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="soundAlertsAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkGrid" id="soundAlertsGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">10</property>
<property name="column_spacing">20</property>
<child>
<object class="GtkCheckButton" id="beepProgressBarUpdatesCheckButton">
<property name="label" translatable="yes" comments="Translators: This is an option in the Preferences dialog box related to the presentation of progress bar updates. If this checkbox is checked, Cthulhu will periodically emit beeps which increase in pitch as the value of the progress bar increases.">Bee_p progress bar updates</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="xalign">0</property>
<property name="active">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">0</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="progressBarBeepIntervalLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Beep _frequency (secs):</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">progressBarBeepIntervalSpinButton</property>
<accessibility>
<relation type="label-for" target="progressBarBeepIntervalSpinButton"/>
</accessibility>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="progressBarBeepIntervalSpinButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text" translatable="yes">0</property>
<property name="adjustment">progressBarBeepIntervalAdjustment</property>
<property name="climb_rate">1</property>
<property name="numeric">True</property>
<property name="value">0</property>
<signal name="value-changed" handler="progressBarBeepIntervalValueChanged" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="soundAlertsTitleLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Alerts</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">2</property>
</packing>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="generalTabLabel">
<object class="GtkLabel" id="soundsTabLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">General</property>
<property name="label" translatable="yes">Sounds</property>
</object>
<packing>
<property name="position">1</property>
<property name="tab_fill">False</property>
</packing>
</child>
@@ -1614,7 +1878,7 @@
</child>
</object>
<packing>
<property name="position">1</property>
<property name="position">2</property>
</packing>
</child>
<child type="tab">
@@ -1624,7 +1888,7 @@
<property name="label" translatable="yes">Voice</property>
</object>
<packing>
<property name="position">1</property>
<property name="position">2</property>
<property name="tab_fill">False</property>
</packing>
</child>
@@ -2312,7 +2576,7 @@
</child>
</object>
<packing>
<property name="position">2</property>
<property name="position">3</property>
</packing>
</child>
<child type="tab">
@@ -2322,7 +2586,7 @@
<property name="label" translatable="yes">Speech</property>
</object>
<packing>
<property name="position">2</property>
<property name="position">3</property>
<property name="tab_fill">False</property>
</packing>
</child>
@@ -2911,7 +3175,7 @@
</child>
</object>
<packing>
<property name="position">3</property>
<property name="position">4</property>
</packing>
</child>
<child type="tab">
@@ -2921,7 +3185,7 @@
<property name="label" translatable="yes">Braille</property>
</object>
<packing>
<property name="position">3</property>
<property name="position">4</property>
<property name="tab_fill">False</property>
</packing>
</child>
@@ -3436,7 +3700,7 @@
</child>
</object>
<packing>
<property name="position">4</property>
<property name="position">5</property>
</packing>
</child>
<child type="tab">
@@ -3446,7 +3710,7 @@
<property name="label" translatable="yes">Echo</property>
</object>
<packing>
<property name="position">4</property>
<property name="position">5</property>
<property name="tab_fill">False</property>
</packing>
</child>
@@ -3528,7 +3792,7 @@
</child>
</object>
<packing>
<property name="position">5</property>
<property name="position">6</property>
</packing>
</child>
<child type="tab">
@@ -3538,7 +3802,7 @@
<property name="label" translatable="yes">Key Bindings</property>
</object>
<packing>
<property name="position">5</property>
<property name="position">6</property>
<property name="tab_fill">False</property>
</packing>
</child>
@@ -3638,7 +3902,7 @@
</child>
</object>
<packing>
<property name="position">6</property>
<property name="position">7</property>
</packing>
</child>
<child type="tab">
@@ -3648,7 +3912,7 @@
<property name="label" translatable="yes">Pronunciation</property>
</object>
<packing>
<property name="position">6</property>
<property name="position">7</property>
<property name="tab_fill">False</property>
</packing>
</child>
@@ -3983,7 +4247,7 @@
</child>
</object>
<packing>
<property name="position">7</property>
<property name="position">8</property>
</packing>
</child>
<child type="tab">
@@ -3993,7 +4257,7 @@
<property name="label" translatable="yes">Text Attributes</property>
</object>
<packing>
<property name="position">7</property>
<property name="position">8</property>
<property name="tab_fill">False</property>
</packing>
</child>
+38
View File
@@ -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()
-4
View File
@@ -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
+1
View File
@@ -108,6 +108,7 @@ userCustomizableSettings = [
"brailleProgressBarUpdates",
"beepProgressBarUpdates",
"progressBarUpdateInterval",
"progressBarBeepInterval",
"progressBarVerbosity",
"ignoreStatusBarProgressBars",
"enableBrailleWordWrap",
+175
View File
@@ -0,0 +1,175 @@
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
class SoundSettingsPersistenceTests(unittest.TestCase):
def test_progress_bar_beep_interval_is_user_customizable(self):
self.assertIn("progressBarBeepInterval", settings.userCustomizableSettings)
def test_activate_writes_progress_bar_beep_interval_default(self):
with tempfile.TemporaryDirectory() as tempDir:
manager = settings_manager.SettingsManager(mock.Mock())
manager.activate(prefsDir=tempDir)
settingsText = Path(tempDir, "user-settings.toml").read_text(encoding="utf-8")
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()
+1
View File
@@ -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):