Merge pull request #600 from alvroble/microsd_toast_timer

[Enhancement] MicroSD toast timer settings
This commit is contained in:
Nick Klockenga
2025-12-10 15:26:05 -05:00
committed by GitHub
8 changed files with 133 additions and 10 deletions
+6 -2
View File
@@ -11,6 +11,7 @@ from seedsigner.models.psbt_parser import PSBTParser
from seedsigner.models.seed import Seed
from seedsigner.models.seed_storage import SeedStorage
from seedsigner.models.settings import Settings
from seedsigner.models.settings import SettingsConstants
from seedsigner.models.singleton import Singleton
from seedsigner.models.threads import BaseThread
from seedsigner.views.screensaver import ScreensaverScreen
@@ -252,7 +253,7 @@ class Controller(Singleton):
* initial_destination: The first View to run. If None, the MainMenuView is
used. Only used by the test suite.
"""
from seedsigner.views import MainMenuView, BackStackView
from seedsigner.views import MainMenuView, BackStackView, RemoveMicroSDWarningView
from seedsigner.views.screensaver import OpeningSplashView
from seedsigner.gui.toast import RemoveSDCardToastManagerThread
@@ -290,7 +291,10 @@ class Controller(Singleton):
next_destination = Destination(MainMenuView)
# Set up our one-time toast notification tip to remove the SD card
self.activate_toast(RemoveSDCardToastManagerThread())
if self.settings.get_value(SettingsConstants.SETTING__MICROSD_TOAST_TIMER) == SettingsConstants.MICROSD_TOAST_TIMER_FIVE_SECONDS:
self.activate_toast(RemoveSDCardToastManagerThread())
elif self.settings.get_value(SettingsConstants.SETTING__MICROSD_TOAST_TIMER) == SettingsConstants.MICROSD_TOAST_TIMER_FOREVER:
next_destination = Destination(RemoveMicroSDWarningView)
while True:
# Destination(None) is a special case; render the Home screen
+2 -2
View File
@@ -201,11 +201,11 @@ class BaseToastOverlayManagerThread(BaseThread):
class RemoveSDCardToastManagerThread(BaseToastOverlayManagerThread):
def __init__(self, activation_delay: int = 3, duration: int = 1e6):
def __init__(self, activation_delay: int = 3, duration: int = 5):
"""
* activation_delay: configurable so the screenshot generator can get the
toast to immediately render.
* duration: default value is essentially forever. Overrideable for the
* duration: default value is 5 seconds. Overrideable for the
screenshot generator.
"""
super().__init__(
@@ -293,6 +293,15 @@ class SettingsConstants:
(CUSTOM_DERIVATION, _mft("Custom Derivation")),
]
MICROSD_TOAST_TIMER_DISABLED = "D"
MICROSD_TOAST_TIMER_FIVE_SECONDS = "E"
MICROSD_TOAST_TIMER_FOREVER = "inf"
ALL_MICROSD_TOAST_TIMERS = [
(MICROSD_TOAST_TIMER_DISABLED, "Disabled"),
(MICROSD_TOAST_TIMER_FIVE_SECONDS, "5 seconds"),
(MICROSD_TOAST_TIMER_FOREVER, "Until SD removed")
]
WORDLIST_LANGUAGE__ENGLISH = "en"
WORDLIST_LANGUAGE__CHINESE_SIMPLIFIED = "zh_Hans_CN"
WORDLIST_LANGUAGE__CHINESE_TRADITIONAL = "zh_Hant_TW"
@@ -339,6 +348,7 @@ class SettingsConstants:
SETTING__DIRE_WARNINGS = "dire_warnings"
SETTING__QR_BRIGHTNESS_TIPS = "qr_brightness_tips"
SETTING__PARTNER_LOGOS = "partner_logos"
SETTING__MICROSD_TOAST_TIMER = "microsd_toast_timer"
SETTING__DEBUG = "debug"
@@ -665,6 +675,14 @@ class SettingsDefinition:
help_text=_mft("Native Segwit only"),
visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__DISABLED),
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__MICROSD_TOAST_TIMER,
display_name="MicroSD toast timer",
type=SettingsConstants.TYPE__SELECT_1,
visibility=SettingsConstants.VISIBILITY__ADVANCED,
selection_options=SettingsConstants.ALL_MICROSD_TOAST_TIMERS,
default_value=SettingsConstants.MICROSD_TOAST_TIMER_FIVE_SECONDS),
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__MESSAGE_SIGNING,
+19 -4
View File
@@ -150,11 +150,15 @@ class SettingsEntryUpdateSelectionView(View):
Handles changes to all selection-type settings (Multiselect, SELECT_1,
Enabled/Disabled, etc).
"""
def __init__(self, attr_name: str, parent_initial_scroll: int = 0, selected_button: int = None):
def __init__(self, attr_name: str, parent_initial_scroll: int = 0, selected_button: int = None, blocking_view: View = None, unblocking_view: View = None):
super().__init__()
self.settings_entry = SettingsDefinition.get_settings_entry(attr_name)
self.selected_button = selected_button
self.parent_initial_scroll = parent_initial_scroll
# If the setting remains unchanged, navigation should return to blocking_view (if set)
self.blocking_view = blocking_view
# unblocking_view is an optional target to navigate to once the setting actually changes.
self.unblocking_view = unblocking_view
def run(self):
@@ -200,6 +204,8 @@ class SettingsEntryUpdateSelectionView(View):
)
if ret_value == RET_CODE__BACK_BUTTON:
if self.blocking_view:
return Destination(self.blocking_view, clear_history=True)
return settings_menu_view_destination
value = self.settings_entry.get_selection_option_value(ret_value)
@@ -219,8 +225,7 @@ class SettingsEntryUpdateSelectionView(View):
else:
# All other types are single selects (e.g. Enabled/Disabled, SELECT_1)
if value == initial_value:
# No change, return to menu
if value == initial_value and not self.blocking_view:
return settings_menu_view_destination
else:
updated_value = value
@@ -238,11 +243,21 @@ class SettingsEntryUpdateSelectionView(View):
if destination:
return destination
# If this selection view was opened from a blocking flow (e.g. RemoveMicroSDWarningView),
# prevent navigation away until the setting actually changes. If it hasn't changed,
# return to the blocking view so it can re-evaluate the state.
if self.blocking_view:
current_value = self.settings.get_value(self.settings_entry.attr_name)
if current_value == initial_value:
return Destination(self.blocking_view, clear_history=True)
elif self.unblocking_view:
return Destination(self.unblocking_view, clear_history=True)
# All selects stay in place; re-initialize where in the list we left off
self.selected_button = ret_value
return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entry.attr_name, parent_initial_scroll=self.parent_initial_scroll, selected_button=self.selected_button), skip_current_view=True)
return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entry.attr_name, parent_initial_scroll=self.parent_initial_scroll, selected_button=self.selected_button, blocking_view=self.blocking_view, unblocking_view=self.unblocking_view), skip_current_view=True)
+36
View File
@@ -407,3 +407,39 @@ class OptionDisabledView(View):
return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_attr), clear_history=True)
else:
return Destination(MainMenuView, clear_history=True)
class RemoveMicroSDWarningView(View):
CONTINUE = ButtonOption("Continue")
SETTINGS = ButtonOption("Settings")
def run(self):
button_data = [self.CONTINUE, self.SETTINGS]
selected_menu_num = self.run_screen(
WarningScreen,
title=_("Action Required"),
status_icon_name=SeedSignerIconConstants.MICROSD,
status_headline=None,
text=_("You must remove the\nMicroSD card to continue."),
show_back_button=False,
button_data=button_data,
)
if button_data[selected_menu_num] == self.CONTINUE:
from seedsigner.hardware.microsd import MicroSD
if not MicroSD.get_instance().is_inserted:
return Destination(MainMenuView, clear_history=True)
else:
return Destination(RemoveMicroSDWarningView, clear_history=True)
elif button_data[selected_menu_num] == self.SETTINGS:
from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView
return Destination(
SettingsEntryUpdateSelectionView,
view_args=dict(
attr_name=SettingsConstants.SETTING__MICROSD_TOAST_TIMER,
blocking_view=RemoveMicroSDWarningView,
unblocking_view=MainMenuView
)
)
+2 -1
View File
@@ -39,7 +39,7 @@ from seedsigner.models.qr_type import QRType
from seedsigner.models.seed import Seed
from seedsigner.models.settings import Settings
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
from seedsigner.views import (MainMenuView, PowerOptionsView, RestartView, NotYetImplementedView, UnhandledExceptionView,
from seedsigner.views import (MainMenuView, PowerOptionsView, RestartView, RemoveMicroSDWarningView, NotYetImplementedView, UnhandledExceptionView,
psbt_views, seed_views, settings_views, tools_views, scan_views)
from seedsigner.views.screensaver import OpeningSplashView
from seedsigner.views.view import NetworkMismatchErrorView, OptionDisabledView, PowerOffView
@@ -291,6 +291,7 @@ def generate_screenshots(locale):
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_removed', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED, activation_delay=0, duration=0)),
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_inserted', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED, activation_delay=0, duration=0)),
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_RemoveSDCardToast', toast_thread=RemoveSDCardToastManagerThread(activation_delay=0, duration=0)),
ScreenshotConfig(RemoveMicroSDWarningView),
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_DefaultToast', toast_thread=DefaultToast("This is a default text toast!", activation_delay=0, duration=0)),
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_InfoToast', toast_thread=InfoToast("This is an info toast!", activation_delay=0, duration=0)),
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SuccessToast', toast_thread=SuccessToast("This is a success toast!", activation_delay=0, duration=0)),
+1
View File
@@ -110,6 +110,7 @@ class TestController(BaseTest):
assert controller.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS_TIPS) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__PARTNER_LOGOS) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__MICROSD_TOAST_TIMER) == SettingsConstants.MICROSD_TOAST_TIMER_FIVE_SECONDS
# Hidden Settings defaults
assert controller.settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS) == 62
+49 -1
View File
@@ -9,8 +9,12 @@ from seedsigner.models.seed import Seed
from seedsigner.views import scan_views
from seedsigner.views.psbt_views import PSBTSelectSeedView
from seedsigner.views.seed_views import SeedBackupView, SeedMnemonicEntryView, SeedOptionsView, SeedsMenuView
from seedsigner.views.view import Destination, MainMenuView, PowerOptionsView, UnhandledExceptionView, View
from seedsigner.views.view import Destination, MainMenuView, PowerOptionsView, UnhandledExceptionView, RemoveMicroSDWarningView, MainMenuView, View
from seedsigner.views.tools_views import ToolsMenuView, ToolsCalcFinalWordNumWordsView
from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView
from seedsigner.models.settings_definition import SettingsDefinition
from seedsigner.models.settings import SettingsConstants
from seedsigner.hardware.microsd import MicroSD
@@ -192,3 +196,47 @@ class TestFlowTest(FlowTest):
FlowStep(MainMenuView), # Need a next Destination to force the first step to run
])
def test_remove_microsd_blocking(self):
"""
Verifies three related behaviors:
1) If the RemoveMicroSDWarningView launches the SettingsEntryUpdateSelectionView
and the user presses Back without changing the tracked setting, the flow
returns to RemoveMicroSDWarningView (the blocking condition remains).
2) If the user changes the tracked setting while in the settings entry, the
flow unblocks and navigates to MainMenuView.
3) If the MicroSD is physically removed and the user presses Continue on the
warning, the flow proceeds to MainMenuView.
"""
controller = Controller.get_instance()
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__MICROSD_TOAST_TIMER)
controller.settings.set_value(settings_entry.attr_name, SettingsConstants.MICROSD_TOAST_TIMER_FOREVER)
# There are only two ways of exiting RemoveMicroSDWarningView when SETTING__MICROSD_TOAST_TIMER -> MICROSD_TOAST_TIMER_FOREVER
self.run_sequence([
FlowStep(RemoveMicroSDWarningView, button_data_selection=RemoveMicroSDWarningView.SETTINGS),
FlowStep(SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON),
FlowStep(RemoveMicroSDWarningView, button_data_selection=RemoveMicroSDWarningView.SETTINGS),
# 1) Modifying the setting
FlowStep(SettingsEntryUpdateSelectionView, screen_return_value=0),
FlowStep(MainMenuView)
])
self.reset_controller()
controller = Controller.get_instance()
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__MICROSD_TOAST_TIMER)
controller.settings.set_value(settings_entry.attr_name, SettingsConstants.MICROSD_TOAST_TIMER_FOREVER)
# 2) Removing the MicroSD card and pressing CONTINUE
self.mock_microsd.is_inserted = False
assert MicroSD.get_instance().is_inserted is False
self.run_sequence([
FlowStep(RemoveMicroSDWarningView, button_data_selection=RemoveMicroSDWarningView.CONTINUE),
FlowStep(MainMenuView)
])
# Restore the setting for the controller
controller.settings.set_value(settings_entry.attr_name, SettingsConstants.MICROSD_TOAST_TIMER_FIVE_SECONDS)