feat(tray): allow disabling missing tray warning dialog (#628)
* feat(tray): allow disabling missing tray warning dialog Fixes #620 * test(tray): stabilize missing-watcher opt-out coverage Default the D-Bus ListNames mock to include StatusNotifierWatcher so TrayIndicator construction no longer opens the warning dialog during setUp, and bind the don't-show-again checkbox test to the module Gtk mock. * test(tray): make missing-watcher opt-out tests deterministic
This commit is contained in:
+10
-5
@@ -2806,7 +2806,8 @@ install_whispercpp_with_gpu_support() {
|
||||
},
|
||||
"ui": {
|
||||
"start_minimized": false,
|
||||
"show_notifications": true
|
||||
"show_notifications": true,
|
||||
"show_missing_tray_warning": true
|
||||
},
|
||||
"advanced": {
|
||||
"debug_logging": false,
|
||||
@@ -2981,7 +2982,8 @@ PY
|
||||
},
|
||||
"ui": {
|
||||
"start_minimized": false,
|
||||
"show_notifications": true
|
||||
"show_notifications": true,
|
||||
"show_missing_tray_warning": true
|
||||
},
|
||||
"advanced": {
|
||||
"debug_logging": false,
|
||||
@@ -3033,7 +3035,8 @@ WHISPER_CONFIG
|
||||
},
|
||||
"ui": {
|
||||
"start_minimized": false,
|
||||
"show_notifications": true
|
||||
"show_notifications": true,
|
||||
"show_missing_tray_warning": true
|
||||
},
|
||||
"advanced": {
|
||||
"debug_logging": false,
|
||||
@@ -3073,7 +3076,8 @@ FALLBACK_CONFIG
|
||||
},
|
||||
"ui": {
|
||||
"start_minimized": false,
|
||||
"show_notifications": true
|
||||
"show_notifications": true,
|
||||
"show_missing_tray_warning": true
|
||||
},
|
||||
"advanced": {
|
||||
"debug_logging": false,
|
||||
@@ -3135,7 +3139,8 @@ VOSK_CONFIG
|
||||
},
|
||||
"ui": {
|
||||
"start_minimized": false,
|
||||
"show_notifications": true
|
||||
"show_notifications": true,
|
||||
"show_missing_tray_warning": true
|
||||
},
|
||||
"advanced": {
|
||||
"debug_logging": false,
|
||||
|
||||
@@ -54,6 +54,7 @@ DEFAULT_CONFIG = {
|
||||
"ui": {
|
||||
"start_minimized": False,
|
||||
"show_notifications": True,
|
||||
"show_missing_tray_warning": True,
|
||||
},
|
||||
"general": {
|
||||
"autostart": False,
|
||||
|
||||
@@ -1498,8 +1498,8 @@ class SettingsDialog(Gtk.Dialog):
|
||||
self.audio_device_combo.connect("changed", self._on_audio_device_changed)
|
||||
|
||||
def _build_general_section(self):
|
||||
"""Build the Application page: startup behavior."""
|
||||
group = PreferencesGroup(title="Startup")
|
||||
"""Build the Application page: general behavior."""
|
||||
group = PreferencesGroup(title="General")
|
||||
|
||||
self.autostart_switch = Gtk.Switch()
|
||||
autostart_row = PreferenceRow(
|
||||
@@ -1519,10 +1519,20 @@ class SettingsDialog(Gtk.Dialog):
|
||||
)
|
||||
group.add_row(start_minimized_row)
|
||||
|
||||
self.missing_tray_warning_switch = Gtk.Switch()
|
||||
missing_tray_warning_row = PreferenceRow(
|
||||
title="Warn if tray support is not detected",
|
||||
subtitle="Show a warning when Vocalinux cannot detect AppIndicator support",
|
||||
widget=self.missing_tray_warning_switch,
|
||||
keywords=("tray", "appindicator", "warning"),
|
||||
)
|
||||
group.add_row(missing_tray_warning_row)
|
||||
|
||||
self.general_tab.pack_start(group, False, False, 0)
|
||||
|
||||
self.autostart_switch.connect("state-set", self._on_autostart_toggled)
|
||||
self.start_minimized_switch.connect("state-set", self._on_start_minimized_toggled)
|
||||
self.missing_tray_warning_switch.connect("state-set", self._on_missing_tray_warning_toggled)
|
||||
|
||||
def _build_auto_pause_section(self):
|
||||
"""Build Auto-Pause settings: enable toggle + process name list."""
|
||||
@@ -1898,6 +1908,17 @@ class SettingsDialog(Gtk.Dialog):
|
||||
logger.info(f"Start minimized {'enabled' if enabled else 'disabled'}")
|
||||
return False
|
||||
|
||||
def _on_missing_tray_warning_toggled(self, widget, state):
|
||||
"""Handle toggle of the missing tray support warning switch."""
|
||||
if self._initializing or self._applying_settings:
|
||||
return False
|
||||
|
||||
enabled = bool(state)
|
||||
logger.info(f"Missing tray warning toggled: {enabled}")
|
||||
self.config_manager.set("ui", "show_missing_tray_warning", enabled)
|
||||
self.config_manager.save_settings()
|
||||
return False
|
||||
|
||||
def _on_copy_to_clipboard_toggled(self, widget, state):
|
||||
"""Handle toggle of the copy to clipboard switch."""
|
||||
if self._initializing or self._applying_settings:
|
||||
@@ -3195,12 +3216,14 @@ class SettingsDialog(Gtk.Dialog):
|
||||
|
||||
autostart_enabled = general_settings.get("autostart", False)
|
||||
start_minimized = ui_settings.get("start_minimized", False)
|
||||
show_missing_tray_warning = ui_settings.get("show_missing_tray_warning", True)
|
||||
copy_to_clipboard = text_injection_settings.get("copy_to_clipboard", False)
|
||||
auto_capitalize = text_injection_settings.get("auto_capitalize", True)
|
||||
append_trailing_space = text_injection_settings.get("append_trailing_space", True)
|
||||
|
||||
self.autostart_switch.set_active(autostart_enabled)
|
||||
self.start_minimized_switch.set_active(start_minimized)
|
||||
self.missing_tray_warning_switch.set_active(show_missing_tray_warning)
|
||||
self.copy_to_clipboard_switch.set_active(copy_to_clipboard)
|
||||
self.auto_capitalize_switch.set_active(auto_capitalize)
|
||||
self.append_trailing_space_switch.set_active(append_trailing_space)
|
||||
|
||||
@@ -262,7 +262,8 @@ class TrayIndicator:
|
||||
"No StatusNotifierWatcher on D-Bus session bus; tray icon may not appear. "
|
||||
"On GNOME, install gnome-shell-extension-appindicator."
|
||||
)
|
||||
GLib.idle_add(self._show_missing_watcher_dialog)
|
||||
if self.config_manager.get_bool("ui", "show_missing_tray_warning", True):
|
||||
GLib.idle_add(self._show_missing_watcher_dialog)
|
||||
|
||||
# Create the menu
|
||||
self.menu = Gtk.Menu()
|
||||
@@ -339,8 +340,18 @@ class TrayIndicator:
|
||||
"\n"
|
||||
"Keyboard shortcuts will still work even without the tray icon."
|
||||
)
|
||||
dialog.connect("response", lambda d, _: d.destroy())
|
||||
dialog.show()
|
||||
dont_show_again = Gtk.CheckButton(label="Don't show again")
|
||||
dont_show_again.set_margin_top(8)
|
||||
dialog.get_message_area().pack_start(dont_show_again, False, False, 0)
|
||||
|
||||
def on_response(d, _response):
|
||||
if dont_show_again.get_active():
|
||||
self.config_manager.set("ui", "show_missing_tray_warning", False)
|
||||
self.config_manager.save_settings()
|
||||
d.destroy()
|
||||
|
||||
dialog.connect("response", on_response)
|
||||
dialog.show_all()
|
||||
return False
|
||||
|
||||
def _show_appindicator_error_dialog(self, error_detail: str):
|
||||
|
||||
@@ -110,6 +110,9 @@ class TestConfigManager(unittest.TestCase):
|
||||
) # From defaults
|
||||
self.assertEqual(config_manager.config["ui"]["start_minimized"], True)
|
||||
self.assertEqual(config_manager.config["ui"]["show_notifications"], True) # From defaults
|
||||
self.assertEqual(
|
||||
config_manager.config["ui"]["show_missing_tray_warning"], True
|
||||
) # From defaults
|
||||
|
||||
def test_load_config_file_error(self):
|
||||
"""Test handling of errors when loading config file."""
|
||||
@@ -436,6 +439,11 @@ class TestConfigManager(unittest.TestCase):
|
||||
config_manager = ConfigManager()
|
||||
self.assertTrue(config_manager.is_sound_effects_enabled())
|
||||
|
||||
def test_missing_tray_warning_enabled_by_default(self):
|
||||
"""Test that the missing tray support warning is enabled by default."""
|
||||
config_manager = ConfigManager()
|
||||
self.assertTrue(config_manager.get_bool("ui", "show_missing_tray_warning", False))
|
||||
|
||||
def test_set_sound_effects_enabled(self):
|
||||
"""Test setting sound effects enabled state."""
|
||||
config_manager = ConfigManager()
|
||||
|
||||
@@ -823,6 +823,16 @@ class TestSettingsNavigation(unittest.TestCase):
|
||||
]:
|
||||
self.assertIn(f'SettingsPage("{name}", "{title}"', self.source_code)
|
||||
|
||||
def test_application_page_has_tray_warning_toggle(self):
|
||||
self.assertIn('PreferencesGroup(title="General")', self.source_code)
|
||||
self.assertIn("self.missing_tray_warning_switch = Gtk.Switch()", self.source_code)
|
||||
self.assertIn('title="Warn if tray support is not detected"', self.source_code)
|
||||
self.assertIn('"show_missing_tray_warning", enabled', self.source_code)
|
||||
self.assertIn(
|
||||
'ui_settings.get("show_missing_tray_warning", True)',
|
||||
self.source_code,
|
||||
)
|
||||
|
||||
def test_status_controls_live_in_sidebar_footer(self):
|
||||
"""Status, mic level, test, and Close live in the sidebar footer so
|
||||
they stay visible from every page without a bottom strip."""
|
||||
|
||||
@@ -107,6 +107,8 @@ class TestTrayIndicator(unittest.TestCase):
|
||||
self.mock_speech_engine.state = RecognitionState.IDLE
|
||||
self.mock_text_injector = MagicMock()
|
||||
self.mock_config_manager = MagicMock()
|
||||
self.mock_config_manager.get_bool.side_effect = lambda section, key, default=False: default
|
||||
self.mock_config_manager.get_str.side_effect = lambda section, key, default="": default
|
||||
|
||||
# Patch os path functions
|
||||
self.patcher_path_exists = patch("os.path.exists", return_value=True)
|
||||
@@ -131,6 +133,17 @@ class TestTrayIndicator(unittest.TestCase):
|
||||
self.mock_config_manager_class = self.patcher_config_manager.start()
|
||||
self.mock_config_manager_class.return_value = self.mock_config_manager
|
||||
|
||||
# Default D-Bus ListNames to include StatusNotifierWatcher so constructing
|
||||
# TrayIndicator (which idle_add's _init_indicator) does not open the
|
||||
# missing-watcher dialog during setUp. Individual tests can override.
|
||||
self.patcher_dbus_proxy = patch(
|
||||
"vocalinux.ui.tray_indicator.Gio.DBusProxy.new_for_bus_sync"
|
||||
)
|
||||
self.mock_dbus_proxy_factory = self.patcher_dbus_proxy.start()
|
||||
mock_proxy = MagicMock()
|
||||
mock_proxy.call_sync.return_value.unpack.return_value = (["org.kde.StatusNotifierWatcher"],)
|
||||
self.mock_dbus_proxy_factory.return_value = mock_proxy
|
||||
|
||||
# Import and create TrayIndicator
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
@@ -146,6 +159,7 @@ class TestTrayIndicator(unittest.TestCase):
|
||||
self.patcher_listdir.stop()
|
||||
self.patcher_makedirs.stop()
|
||||
self.patcher_config_manager.stop()
|
||||
self.patcher_dbus_proxy.stop()
|
||||
self.patcher_settings_dialog.stop()
|
||||
self.thread_patcher.stop()
|
||||
self.ksm_patcher.stop()
|
||||
@@ -498,6 +512,55 @@ class TestTrayIndicator(unittest.TestCase):
|
||||
self.assertEqual(result, False)
|
||||
mock_dialog.assert_called_once()
|
||||
|
||||
def test_init_indicator_missing_watcher_respects_opt_out(self):
|
||||
get_bool = MagicMock(
|
||||
side_effect=lambda section, key, default=False: (
|
||||
False if (section, key) == ("ui", "show_missing_tray_warning") else default
|
||||
)
|
||||
)
|
||||
self.tray_indicator.config_manager.get_bool = get_bool
|
||||
scheduled = []
|
||||
|
||||
def record_idle(func, *args):
|
||||
scheduled.append(func)
|
||||
return False # do not execute callbacks
|
||||
|
||||
with patch.object(
|
||||
self.tray_indicator,
|
||||
"_check_status_notifier_watcher",
|
||||
return_value=False,
|
||||
):
|
||||
with patch("vocalinux.ui.tray_indicator.GLib.idle_add", side_effect=record_idle):
|
||||
result = self.tray_indicator._init_indicator()
|
||||
|
||||
self.assertEqual(result, False)
|
||||
get_bool.assert_any_call("ui", "show_missing_tray_warning", True)
|
||||
self.assertNotIn(self.tray_indicator._show_missing_watcher_dialog, scheduled)
|
||||
|
||||
def test_missing_watcher_dialog_dont_show_again_saves_opt_out(self):
|
||||
mock_dialog = MagicMock()
|
||||
mock_checkbox = MagicMock()
|
||||
mock_checkbox.get_active.return_value = True
|
||||
config = MagicMock()
|
||||
self.tray_indicator.config_manager = config
|
||||
|
||||
with patch("vocalinux.ui.tray_indicator.Gtk.MessageDialog", return_value=mock_dialog):
|
||||
with patch("vocalinux.ui.tray_indicator.Gtk.CheckButton", return_value=mock_checkbox):
|
||||
result = self.tray_indicator._show_missing_watcher_dialog()
|
||||
|
||||
self.assertEqual(result, False)
|
||||
mock_dialog.get_message_area.return_value.pack_start.assert_called_once_with(
|
||||
mock_checkbox, False, False, 0
|
||||
)
|
||||
mock_dialog.show_all.assert_called_once()
|
||||
|
||||
response_callback = mock_dialog.connect.call_args[0][1]
|
||||
response_callback(mock_dialog, None)
|
||||
|
||||
config.set.assert_called_once_with("ui", "show_missing_tray_warning", False)
|
||||
config.save_settings.assert_called_once()
|
||||
mock_dialog.destroy.assert_called_once()
|
||||
|
||||
def test_init_indicator_creation_failure_shows_error_dialog(self):
|
||||
with patch(
|
||||
"vocalinux.ui.tray_indicator.AppIndicator3.Indicator.new_with_path",
|
||||
|
||||
Reference in New Issue
Block a user