From 6e25e3f94bd6f46aaf3c3eda9974e60661f94db4 Mon Sep 17 00:00:00 2001 From: kdmukai <934746+kdmukai@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:08:22 -0600 Subject: [PATCH] UX changes to require a selection for multiselect settings --- src/seedsigner/views/settings_views.py | 38 +++++++++++++++++++++++++ tests/screenshot_generator/generator.py | 1 + tests/test_flows_settings.py | 28 ++++++++++++++---- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index cbf66e08..56141c20 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -204,6 +204,13 @@ class SettingsEntryUpdateSelectionView(View): ) if ret_value == RET_CODE__BACK_BUTTON: + if self.settings_entry.type == SettingsConstants.TYPE__MULTISELECT: + # After the user finishes toggling multiselect options, initial_value will + # have their final selections when they hit BACK to exit. All current + # multiselect settings require at least one option to be selected. + if not initial_value: + return Destination(SettingsSelectionRequiredWarningView, view_args={"attr_name": self.settings_entry.attr_name}) + if self.blocking_view: return Destination(self.blocking_view, clear_history=True) return settings_menu_view_destination @@ -261,6 +268,37 @@ class SettingsEntryUpdateSelectionView(View): +class SettingsSelectionRequiredWarningView(View): + def __init__(self, attr_name: str): + super().__init__() + self.settings_entry = SettingsDefinition.get_settings_entry(attr_name) + + + def run(self): + from seedsigner.gui.screens.screen import WarningScreen + + # TRANSLATOR_NOTE: Title of a warning dialog when configuring a setting that requires at least one option to be selected. + title = _("Selection Required") + + # TRANSLATOR_NOTE: The name of the setting being configured (e.g. "Script types") will be inserted. + text = _("At least one option must be selected for \"{}\".").format(self.settings_entry.display_name) + + # TRANSLATOR_NOTE: Text for the button that returns the user to the setting configuration screen. + button_text = _("Return to setting") + + self.run_screen( + WarningScreen, + title=title, + status_headline=None, + text=text, + button_data=[ButtonOption(button_text)], + show_back_button=False, + ) + + return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entry.attr_name)) + + + class SettingsIngestSettingsQRView(View): def __init__(self, data: str): from seedsigner.hardware.microsd import MicroSD diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index 673086ff..69654196 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -427,6 +427,7 @@ def generate_screenshots(locale): ScreenshotConfig(settings_views.DonateView), ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), screenshot_name="SettingsIngestSettingsQRView_persistent"), ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), screenshot_name="SettingsIngestSettingsQRView_not_persistent"), + ScreenshotConfig(settings_views.SettingsSelectionRequiredWarningView, dict(attr_name=SettingsConstants.SETTING__SCRIPT_TYPES)), ], "Misc Error Views": [ ScreenshotConfig(NotYetImplementedView), diff --git a/tests/test_flows_settings.py b/tests/test_flows_settings.py index 75fc530b..ff73a3d7 100644 --- a/tests/test_flows_settings.py +++ b/tests/test_flows_settings.py @@ -37,17 +37,35 @@ class TestSettingsFlows(FlowTest): def test_multiselect(self): - """ Multiselect Settings options should stay in-place; requires BACK to exit. """ + """ + Multiselect Settings options should stay in-place; requires BACK to exit. If no + selections are made, route to the warning screen and return the user to the + settings entry until at least one option is selected. + """ # Which option are we testing? - settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__XPUB_QR_FORMAT) + settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SIG_TYPES) + + # Enable all options to start + self.settings.set_value(settings_entry.attr_name, [option[0] for option in settings_entry.selection_options]) + + # Sanity check, we only expect two options for this setting + assert len(settings_entry.selection_options) == 2 self.run_sequence([ FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS), FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_views.SettingsMenuView.ADVANCED), FlowStep(settings_views.SettingsMenuView, button_data_selection=ButtonOption(settings_entry.display_name)), - FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # select/deselect first option - FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option - FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option + FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # deselect first option + FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # deselect second option + FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select second option + FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # deselect second option + FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON), # BACK to exit + + # Both options were deselected, should route to the warning screen + FlowStep(settings_views.SettingsSelectionRequiredWarningView), + FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # select first option + + # Now we can exit FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON), # BACK to exit FlowStep(settings_views.SettingsMenuView), ])