fix(settings): gray out Dictation Tone while sound effects are off (#877)

* fix(settings): gray out Dictation Tone while sound effects are off

The Enable Sound Effects switch mutes every dictation cue, so the tone
picker changed nothing while it was off. The row now goes insensitive
with the switch and follows the state loaded from config, matching the
keep-alive timeout and auto-pause sensitivity pattern.

Also bump the pre-commit isort pin 5.13.2 -> 8.0.1 to match uv.lock: the
two versions disagree on single-name import wrapping, so the old hook
collapsed imports the locked isort (and CI) then expanded back.

Fixes #849

* test(settings): cover tone-row sensitivity on settings load

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
devin-ai-integration[bot]
2026-09-29 22:43:56 -07:00
committed by GitHub
co-authored by Jatin K Malik
parent 77def71354
commit d29c884782
3 changed files with 107 additions and 3 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ repos:
# Import sorting with isort (black-compatible)
- repo: https://github.com/pycqa/isort
rev: 5.13.2
rev: 8.0.1
hooks:
- id: isort
args: ["--profile", "black"]
+9 -2
View File
@@ -3524,11 +3524,16 @@ class SettingsDialog(Gtk.Dialog):
logger.info(f"Paste shortcut set to {self.config_manager.get_paste_shortcut()}")
return False
def _update_sound_effects_sensitivity(self, enabled: bool) -> None:
"""Gray out the tone picker while sound effects are disabled."""
self.tone_row.set_sensitive(enabled)
def _on_sound_effects_toggled(self, widget, state):
enabled = bool(state)
self._update_sound_effects_sensitivity(enabled)
if self._initializing or self._applying_settings:
return False
enabled = bool(state)
logger.info(f"Sound effects toggled: {enabled}")
self.config_manager.set_sound_effects_enabled(enabled)
self.config_manager.save_settings()
@@ -5994,7 +5999,9 @@ class SettingsDialog(Gtk.Dialog):
self.append_trailing_space_switch.set_active(append_trailing_space)
if not self.paste_shortcut_combo.set_active_id(paste_shortcut):
self.paste_shortcut_combo.set_active_id(DEFAULT_PASTE_SHORTCUT)
self.sound_effects_switch.set_active(self.config_manager.is_sound_effects_enabled())
sound_effects_enabled = self.config_manager.is_sound_effects_enabled()
self.sound_effects_switch.set_active(sound_effects_enabled)
self._update_sound_effects_sensitivity(sound_effects_enabled)
duck_enabled = self.config_manager.is_playback_duck_enabled()
self.duck_level_scale.set_value(self.config_manager.get_playback_duck_percent())
self.duck_playback_switch.set_active(duck_enabled)
+97
View File
@@ -0,0 +1,97 @@
"""The Dictation Tone row is grayed out while sound effects are off (#849)."""
from __future__ import annotations
import importlib
import sys
from collections.abc import Iterator
from typing import Any
from unittest.mock import MagicMock, Mock, patch
import pytest
import vocalinux.ui
@pytest.fixture(scope="module")
def settings_dialog() -> Iterator[Any]:
"""Import settings_dialog with real base classes for its GTK subclasses.
Same approach as tests/test_follow_keyboard_layout.py; both sys.modules and
the package attribute are restored so later tests patching the module by
name do not reach a second module object (#686).
"""
repository = sys.modules["gi.repository"]
bases = {name: type(name, (), {}) for name in ("Box", "ListBoxRow", "Dialog")}
saved_module = sys.modules.pop("vocalinux.ui.settings_dialog", None)
saved_attribute = getattr(vocalinux.ui, "settings_dialog", None)
try:
with patch.object(repository, "Gtk", MagicMock(**bases)):
module = importlib.import_module("vocalinux.ui.settings_dialog")
yield module
finally:
sys.modules.pop("vocalinux.ui.settings_dialog", None)
if saved_module is not None:
sys.modules["vocalinux.ui.settings_dialog"] = saved_module
if saved_attribute is not None:
vocalinux.ui.settings_dialog = saved_attribute
elif hasattr(vocalinux.ui, "settings_dialog"):
del vocalinux.ui.settings_dialog
@pytest.fixture
def dialog_class(settings_dialog: Any) -> type[Any]:
return settings_dialog.SettingsDialog
def _dialog_stub(dialog_class: type[Any], initializing: bool = False) -> Mock:
"""A Mock dialog whose self-calls reach the real sensitivity helper."""
dialog = Mock()
dialog._initializing = initializing
dialog._applying_settings = False
dialog._update_sound_effects_sensitivity = (
lambda enabled: dialog_class._update_sound_effects_sensitivity(dialog, enabled)
)
return dialog
@pytest.mark.parametrize("enabled", [True, False])
def test_toggling_sound_effects_sets_tone_row_sensitivity(
dialog_class: type[Any], enabled: bool
) -> None:
dialog = _dialog_stub(dialog_class)
dialog_class._on_sound_effects_toggled(dialog, Mock(), enabled)
dialog.tone_row.set_sensitive.assert_called_once_with(enabled)
dialog.config_manager.set_sound_effects_enabled.assert_called_once_with(enabled)
def test_sensitivity_still_updates_while_settings_load(dialog_class: type[Any]) -> None:
"""set_active() during load must sync the row even though saving is skipped."""
dialog = _dialog_stub(dialog_class, initializing=True)
dialog_class._on_sound_effects_toggled(dialog, Mock(), False)
dialog.tone_row.set_sensitive.assert_called_once_with(False)
dialog.config_manager.set_sound_effects_enabled.assert_not_called()
def test_loading_settings_syncs_tone_row_sensitivity(
settings_dialog: Any, dialog_class: type[Any]
) -> None:
"""A saved sound-effects=off state must gray out the tone row on load."""
dialog = _dialog_stub(dialog_class)
dialog._get_current_settings.return_value = {
"engine": "whisper_cpp",
"language": "en",
"model_size": "base",
}
dialog.config_manager.get_settings.return_value = {}
dialog.config_manager.is_sound_effects_enabled.return_value = False
dialog.engine_combo.get_model.return_value = [["whisper.cpp"]]
with patch.object(settings_dialog, "get_available_engines", return_value={"whisper_cpp": True}):
dialog_class._load_and_apply_settings(dialog)
dialog.tone_row.set_sensitive.assert_called_once_with(False)