From 63e4de866271ba8f703678e1d67949dba09dc22f Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 10 Mar 2022 14:41:12 -0600 Subject: [PATCH] Tweaks to passphrase flow, settings; SeedWordsView now highlighting four words at a time --- docs/feature_roadmap.md | 1 + src/seedsigner/gui/screens/screen.py | 4 +- src/seedsigner/gui/screens/seed_screens.py | 75 ++++--- src/seedsigner/hardware/buttons.py | 2 +- src/seedsigner/models/settings.py | 2 + src/seedsigner/models/settings_definition.py | 22 ++- src/seedsigner/views/scan_views.py | 6 +- src/seedsigner/views/seed_views.py | 195 ++++++++----------- src/seedsigner/views/tools_views.py | 15 +- src/seedsigner/views/view.py | 6 + 10 files changed, 168 insertions(+), 160 deletions(-) diff --git a/docs/feature_roadmap.md b/docs/feature_roadmap.md index ce4af493..2406eb18 100644 --- a/docs/feature_roadmap.md +++ b/docs/feature_roadmap.md @@ -53,6 +53,7 @@ All of the above! These features will not be included in the initial v0.5.0 release and will have varying degrees of priority for subsequent releases (or possibly not at all). * Multisig wallet descriptor QR scan(?) and addr verification(?) +* Sign taproot txs * Multi-language support (Transifex free for open source projects) * Multisig: sign PSBT with multiple keys at once. * Custom OS, possibly with swappable SD card PSBT and multisig wallet descriptor storage diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index ea776e51..fa906e63 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -517,6 +517,7 @@ class LargeButtonScreen(BaseTopNavScreen): button_font_name: str = GUIConstants.BUTTON_FONT_NAME button_font_size: int = 20 button_selected_color: str = GUIConstants.ACCENT_COLOR + selected_button: int = 0 def __post_init__(self): super().__post_init__() @@ -570,8 +571,7 @@ class LargeButtonScreen(BaseTopNavScreen): if i == 1: button_start_y += button_height + GUIConstants.COMPONENT_PADDING - self.buttons[0].is_selected = True - self.selected_button = 0 + self.buttons[self.selected_button].is_selected = True def _run(self): diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index 21089dd7..b2c1877c 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -443,43 +443,49 @@ class SeedOptionsScreen(ButtonListScreen): @dataclass class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen): - title: str = "Seed Words" seed: Seed = None - is_first_page: bool = True + page_index: int = 0 + num_pages: int = 3 is_bottom_list: bool = True warning_color: str = GUIConstants.DIRE_WARNING_COLOR def __post_init__(self): + self.title = f"Seed Words: {self.page_index+1}/{self.num_pages}" super().__post_init__() - # Can only render 12 words per screen + # Can only render 4 words per screen + words_per_page = 4 mnemonic = self.seed.mnemonic_display_list - if len(mnemonic) == 12 or self.is_first_page: - self.mnemonic = mnemonic[:12] - else: - self.mnemonic = mnemonic[12:] + + # Slice the mnemonic to our current 4-word section + self.mnemonic = mnemonic[self.page_index*words_per_page:(self.page_index + 1)*words_per_page] self.body_x = 0 self.body_y = self.top_nav.height - int(GUIConstants.COMPONENT_PADDING / 2) self.body_height = self.buttons[0].screen_y - self.body_y # Have to supersample the whole body since it's all at the small font size - supersampling_factor = 2 - font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, 16 * supersampling_factor) + supersampling_factor = 1 + font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, (GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2) * supersampling_factor) - # Calc vertical placement for the numbers - (number_x, number_y) = calc_text_centering( - font=font, - text="1234567890", - is_text_centered=True, - total_width=20 * supersampling_factor, - total_height=20 * supersampling_factor - ) - number_box_x = GUIConstants.EDGE_PADDING * supersampling_factor - number_box_y = GUIConstants.COMPONENT_PADDING * supersampling_factor - number_box_width = 20 * supersampling_factor - number_box_height = 20 * supersampling_factor + # Calc horizontal center based on longest word + max_word_width = 0 + for word in self.mnemonic: + (left, top, right, bottom) = font.getbbox(word, anchor="ls") + if right > max_word_width: + max_word_width = right + + # Measure the max digit height for the numbering boxes, from baseline + number_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE * supersampling_factor) + (left, top, right, bottom) = number_font.getbbox("24", anchor="ls") + number_height = -1 * top + number_width = right + number_box_width = number_width + int(GUIConstants.COMPONENT_PADDING/2 * supersampling_factor) + number_box_height = number_box_width + + number_box_x = int((self.canvas_width * supersampling_factor - number_box_width - GUIConstants.COMPONENT_PADDING*supersampling_factor - max_word_width))/2 + number_box_y = GUIConstants.COMPONENT_PADDING * supersampling_factor # Set up our temp supersampled rendering surface self.body_img = Image.new( @@ -490,37 +496,30 @@ class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen): draw = ImageDraw.Draw(self.body_img) for index, word in enumerate(self.mnemonic): - if index == 6: - # Start of the second column of words - number_box_x = (int(self.canvas_width / 2) + 4) * supersampling_factor - number_box_y = GUIConstants.COMPONENT_PADDING * supersampling_factor - draw.rounded_rectangle( (number_box_x, number_box_y, number_box_x + number_box_width, number_box_y + number_box_height), fill="#202020", radius=5 * supersampling_factor ) - if self.is_first_page: - number_str = str(index + 1) - else: - number_str = str(index + 1 + 12) - tw, th = font.getsize(number_str) + baseline_y = number_box_y + number_box_height - int((number_box_height - number_height)/2) draw.text( - (number_box_x + int((number_box_width - tw) / 2), number_box_y + number_y), - font=font, - text=number_str, - fill="#0084ff" + (number_box_x + int(number_box_width/2), baseline_y), + font=number_font, + text=str(self.page_index * words_per_page + index + 1), + fill="#0084ff", + anchor="ms" # Middle (centered), baSeline ) # Now draw the word draw.text( - (number_box_x + number_box_width + (4 * supersampling_factor), number_box_y + number_y), + (number_box_x + number_box_width + (GUIConstants.COMPONENT_PADDING * supersampling_factor), baseline_y), font=font, text=word, - fill=GUIConstants.BODY_FONT_COLOR + fill=GUIConstants.BODY_FONT_COLOR, + anchor="ls", # Left, baSeline ) - number_box_y += number_box_height + (4 * supersampling_factor) + number_box_y += number_box_height + (int(1.5*GUIConstants.COMPONENT_PADDING) * supersampling_factor) # Resize to target and sharpen final image self.body_img = self.body_img.resize((self.canvas_width, self.body_height), Image.LANCZOS) diff --git a/src/seedsigner/hardware/buttons.py b/src/seedsigner/hardware/buttons.py index 75b1f44c..eacc8117 100644 --- a/src/seedsigner/hardware/buttons.py +++ b/src/seedsigner/hardware/buttons.py @@ -42,7 +42,7 @@ class HardwareButtons(Singleton): cls._instance.cur_input = None # Track which direction or button was last pressed cls._instance.cur_input_started = None # Track when that input began cls._instance.last_input_time = int(time.time() * 1000) # How long has it been since the last input? - cls._instance.first_repeat_threshold = 175 # Long-press time required before returning continuous input + cls._instance.first_repeat_threshold = 225 # Long-press time required before returning continuous input cls._instance.next_repeat_threshold = 250 # Amount of time where we no longer consider input a continuous hold return cls._instance diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index fdf62d44..2b57e45e 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -74,6 +74,8 @@ class Settings(Singleton): if type(new_settings[entry.attr_name]) == str: # Break comma-separated SettingsQR input into List new_settings[entry.attr_name] = new_settings[entry.attr_name].split(",") + + # TODO: If value is not in entry.selection_options... # Can't just merge the _data dict; have to replace keys they have in common diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index c711129d..2c1c5f15 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -13,6 +13,14 @@ class SettingsConstants: (OPTION__ENABLED, "Enabled"), (OPTION__DISABLED, "Disabled"), ] + OPTIONS__PROMPT_REQUIRED_DISABLED = [ + (OPTION__PROMPT, "Prompt"), + (OPTION__REQUIRED, "Required"), + (OPTION__DISABLED, "Disabled"), + ] + OPTIONS__ENABLED_DISABLED_REQUIRED = OPTIONS__ENABLED_DISABLED +[ + (OPTION__REQUIRED, "Required"), + ] OPTIONS__ENABLED_DISABLED_PROMPT = OPTIONS__ENABLED_DISABLED + [ (OPTION__PROMPT, "Prompt"), ] @@ -244,6 +252,18 @@ class SettingsEntry: return display_name + def get_selection_option_value_by_display_name(self, display_name: str): + for option in self.selection_options: + if type(option) == tuple: + option_value = option[0] + option_display_name = option[1] + else: + option_value = option + option_display_name = option + if option_display_name == display_name: + return option_value + + def to_dict(self) -> dict: if self.selection_options: selection_options = [] @@ -377,7 +397,7 @@ class SettingsDefinition: display_name="BIP-39 passphrase", type=SettingsConstants.TYPE__SELECT_1, visibility=SettingsConstants.VISIBILITY__ADVANCED, - selection_options=SettingsConstants.ALL_OPTIONS, + selection_options=SettingsConstants.OPTIONS__ENABLED_DISABLED_REQUIRED, default_value=SettingsConstants.OPTION__ENABLED), SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, diff --git a/src/seedsigner/views/scan_views.py b/src/seedsigner/views/scan_views.py index 5b7141a9..2e31bc54 100644 --- a/src/seedsigner/views/scan_views.py +++ b/src/seedsigner/views/scan_views.py @@ -3,7 +3,7 @@ from seedsigner.models.psbt_parser import PSBTParser from seedsigner.models.settings import SettingsConstants from seedsigner.views.psbt_views import PSBTSelectSeedView -from seedsigner.views.seed_views import SeedAddPassphrasePromptView, SeedAddPassphraseView +from seedsigner.views.seed_views import SeedAddPassphraseView from .view import BackStackView, MainMenuView, NotYetImplementedView, View, Destination @@ -37,9 +37,7 @@ class ScanView(View): self.controller.storage.set_pending_seed( Seed(mnemonic=seed_mnemonic, wordlist_language_code=wordlist_language_code) ) - if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__PROMPT: - return Destination(SeedAddPassphrasePromptView) - elif self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__REQUIRED: + if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__REQUIRED: return Destination(SeedAddPassphraseView) else: return Destination(SeedFinalizeView) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 2e70c974..356fe979 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -4,6 +4,8 @@ import time from binascii import hexlify from embit.networks import NETWORKS +from seedsigner.models.settings_definition import SettingsDefinition + from .view import NotYetImplementedView, View, Destination, BackStackView, MainMenuView from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants @@ -57,6 +59,9 @@ class SeedsMenuView(View): +"""**************************************************************************** + Loading seeds, passphrases, etc +****************************************************************************""" class LoadSeedView(View): def run(self): SEED_QR = (" Scan a SeedQR", FontAwesomeIconConstants.QRCODE) @@ -189,9 +194,6 @@ class SeedMnemonicInvalidView(View): -"""**************************************************************************** - Loading seeds, passphrases, etc -****************************************************************************""" class SeedFinalizeView(View): def __init__(self): super().__init__() @@ -200,101 +202,27 @@ class SeedFinalizeView(View): def run(self): - PASSPHRASE = ("Add Passphrase", FontAwesomeIconConstants.UNLOCK) FINALIZE = "Done" - CANCEL = ("Cancel", None, None, "red") + PASSPHRASE = ("Add Passphrase", FontAwesomeIconConstants.LOCK) button_data = [] - if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) in [ - SettingsConstants.OPTION__ENABLED, - SettingsConstants.OPTION__PROMPT, - SettingsConstants.OPTION__REQUIRED]: - if self.seed.passphrase: - PASSPHRASE = "Edit Passphrase" - button_data.append(PASSPHRASE) - button_data.append(FINALIZE) - button_data.append(CANCEL) + + if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) != SettingsConstants.OPTION__DISABLED: + button_data.append(PASSPHRASE) selected_menu_num = seed_screens.SeedFinalizeScreen( fingerprint=self.fingerprint, button_data=button_data, ).display() - if button_data[selected_menu_num] == PASSPHRASE: - return Destination(SeedAddPassphraseView) - - elif button_data[selected_menu_num] == FINALIZE: + if button_data[selected_menu_num] == FINALIZE: seed_num = self.controller.storage.finalize_pending_seed() return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True) - elif button_data[selected_menu_num] == CANCEL: - return Destination(SeedDiscardView) - - - -class SeedDiscardView(View): - def __init__(self, seed_num: int = None): - super().__init__() - self.seed_num = seed_num - print(f"self.seed_num: {self.seed_num}") - if self.seed_num is not None: - self.seed = self.controller.get_seed(self.seed_num) - else: - self.seed = self.controller.storage.pending_seed - - - def run(self): - KEEP = "Keep Seed" - DISCARD = ("Discard", None, None, "red") - button_data = [KEEP, DISCARD] - - fingerprint = self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) - selected_menu_num = WarningScreen( - title="Discard Seed?", - warning_headline=None, - warning_text=f"Remove seed {fingerprint} from memory?", - show_back_button=False, - button_data=button_data, - ).display() - - print(f"selected_menu_num: {selected_menu_num}") - - if button_data[selected_menu_num] == KEEP: - if self.seed_num is not None: - return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}) - else: - return Destination(SeedFinalizeView) - - elif button_data[selected_menu_num] == DISCARD: - if self.seed_num is not None: - self.controller.discard_seed(self.seed_num) - else: - self.controller.storage.clear_pending_seed() - return Destination(MainMenuView) - - - -class SeedAddPassphrasePromptView(View): - def run(self): - YES = "Yes" - NO = "No" - button_data = [YES, NO] - - selected_menu_num = LargeButtonScreen( - title="Add Passphrase?", - button_data=button_data - ).display() - - if selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - - if button_data[selected_menu_num] == YES: + elif button_data[selected_menu_num] == PASSPHRASE: return Destination(SeedAddPassphraseView) - elif button_data[selected_menu_num] == NO: - return Destination(SeedFinalizeView) - class SeedAddPassphraseView(View): @@ -327,8 +255,8 @@ class SeedReviewPassphraseView(View): def run(self): EDIT = "Edit passphrase" - CONTINUE = "Continue" - button_data = [EDIT, CONTINUE] + DONE = "Done" + button_data = [EDIT, DONE] # Get the before/after fingerprints network = self.settings.get_value(SettingsConstants.SETTING__NETWORK) @@ -348,21 +276,60 @@ class SeedReviewPassphraseView(View): show_back_button=False, ).display() - if selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - - elif button_data[selected_menu_num] == EDIT: + if button_data[selected_menu_num] == EDIT: return Destination(SeedAddPassphraseView) - elif button_data[selected_menu_num] == CONTINUE: - return Destination(SeedFinalizeView) + elif button_data[selected_menu_num] == DONE: + seed_num = self.controller.storage.finalize_pending_seed() + return Destination(SeedOptionsView, view_args={"seed_num": seed_num}) + + + +class SeedDiscardView(View): + def __init__(self, seed_num: int = None): + super().__init__() + self.seed_num = seed_num + print(f"self.seed_num: {self.seed_num}") + if self.seed_num is not None: + self.seed = self.controller.get_seed(self.seed_num) + else: + self.seed = self.controller.storage.pending_seed + + + def run(self): + KEEP = "Keep Seed" + DISCARD = ("Discard", None, None, "red") + button_data = [KEEP, DISCARD] + + fingerprint = self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) + selected_menu_num = WarningScreen( + title="Discard Seed?", + warning_headline=None, + warning_text=f"Wipe seed {fingerprint} from the device?", + show_back_button=False, + button_data=button_data, + ).display() + + print(f"selected_menu_num: {selected_menu_num}") + + if button_data[selected_menu_num] == KEEP: + if self.seed_num is not None: + return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}) + else: + return Destination(SeedFinalizeView) + + elif button_data[selected_menu_num] == DISCARD: + if self.seed_num is not None: + self.controller.discard_seed(self.seed_num) + else: + self.controller.storage.clear_pending_seed() + return Destination(MainMenuView) """**************************************************************************** Views for actions on individual seeds: ****************************************************************************""" - class SeedOptionsView(View): def __init__(self, seed_num: int): super().__init__() @@ -439,7 +406,7 @@ class SeedWordsWarningView(View): def run(self): - destination = Destination(SeedWordsView, view_args={"seed_num": self.seed_num}) + destination = Destination(SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": 0}) if self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__DISABLED: # Forward straight to showing the words destination.skip_current_view = True @@ -459,38 +426,39 @@ class SeedWordsWarningView(View): class SeedWordsView(View): - def __init__(self, seed_num: int, is_first_page: bool = True): + def __init__(self, seed_num: int, page_index: int = 0): super().__init__() self.seed_num = seed_num self.seed = self.controller.get_seed(self.seed_num) - self.is_first_page = is_first_page + self.page_index = page_index + self.num_pages=int(len(self.seed.mnemonic_list)/4) def run(self): - NEXT_12 = "Next" - SEED_OPTIONS = "Seed Options" + NEXT = "Next" + DONE = "Done" button_data = [] - if self.is_first_page and len(self.seed.mnemonic_list) == 24: - button_data.append(NEXT_12) + if self.page_index < self.num_pages - 1: + button_data.append(NEXT) else: - button_data.append(SEED_OPTIONS) + button_data.append(DONE) selected_menu_num = seed_screens.SeedWordsScreen( seed=self.seed, - is_first_page=self.is_first_page, + page_index=self.page_index, + num_pages=self.num_pages, button_data=button_data, ).display() if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - if button_data[selected_menu_num] == NEXT_12: - # Go on to page 2 - return Destination(SeedWordsView, view_args={"seed_num": self.seed_num, "is_first_page": False}) + if button_data[selected_menu_num] == NEXT: + return Destination(SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": self.page_index + 1}) - elif button_data[selected_menu_num] == SEED_OPTIONS: - # Back to SeedOptions for this seed; cannot back ("<") to this View. + elif button_data[selected_menu_num] == DONE: + # Must clear history to avoid BACK button returning to private info return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, clear_history=True) @@ -513,7 +481,7 @@ class SeedExportXpubSigTypeView(View): MULTISIG = "Multisig" button_data=[SINGLE_SIG, MULTISIG] - selected_menu_num = LargeButtonScreen( + selected_menu_num = ButtonListScreen( title="Export Xpub", button_data=button_data ).display() @@ -543,17 +511,21 @@ class SeedExportXpubScriptTypeView(View): args["script_type"] = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)[0] return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True) + button_data = [] + for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES): + button_data.append(script_type) selected_menu_num = ButtonListScreen( title="Export Xpub", is_button_text_centered=False, - is_bottom_list=True, - button_data=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES), + button_data=button_data, ).display() if selected_menu_num < len(SettingsConstants.ALL_SCRIPT_TYPES): - args["script_type"] = SettingsConstants.ALL_SCRIPT_TYPES[selected_menu_num][0] + script_types_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES) + selected_display_name = button_data[selected_menu_num] + args["script_type"] = script_types_settings_entry.get_selection_option_value_by_display_name(selected_display_name) - if SettingsConstants.ALL_SCRIPT_TYPES[selected_menu_num][0] == SettingsConstants.CUSTOM_DERIVATION: + if button_data[selected_menu_num] == SettingsConstants.CUSTOM_DERIVATION: return Destination(SeedExportXpubCustomDerivationView, view_args=args) return Destination(SeedExportXpubCoordinatorView, view_args=args) @@ -615,7 +587,6 @@ class SeedExportXpubCoordinatorView(View): selected_menu_num = ButtonListScreen( title="Export Xpub", is_button_text_centered=False, - is_bottom_list=True, button_data=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS), ).display() diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 161c6313..17d08d74 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -4,6 +4,7 @@ import time from PIL import Image from PIL.ImageOps import autocontrast +from seedsigner.gui.screens.screen import LargeButtonScreen from seedsigner.hardware.camera import Camera from seedsigner.gui.components import FontAwesomeIconConstants @@ -45,6 +46,9 @@ class ToolsMenuView(View): +"""**************************************************************************** + Image entropy Views +****************************************************************************""" class ToolsImageEntropyLivePreviewView(View): def run(self): self.controller.image_entropy_preview_frames = None @@ -99,10 +103,14 @@ class ToolsImageEntropyMnemonicLengthView(View): TWENTYFOUR_WORDS = "24 words" button_data = [TWELVE_WORDS, TWENTYFOUR_WORDS] - selected_menu_num = ButtonListScreen( + # selected_menu_num = ButtonListScreen( + # title="Mnemonic Length?", + # button_data=button_data, + # ).display() + + selected_menu_num = LargeButtonScreen( title="Mnemonic Length?", button_data=button_data, - is_bottom_list=True, ).display() if selected_menu_num == RET_CODE__BACK_BUTTON: @@ -164,6 +172,9 @@ class ToolsImageEntropyMnemonicLengthView(View): +"""**************************************************************************** + Calc final word Views +****************************************************************************""" class ToolsCalcFinalWordNumWordsView(View): def run(self): TWELVE = "12 words" diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index c5fbf06c..9677185e 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -33,6 +33,12 @@ class BackStackView: As with http requests, Views can receive input vars to inform their behavior. Views can also prepare the next set of vars to set up the next View that should be displayed (akin to Flask's `return redirect(url, param1=x, param2=y))`). + + Navigation guidance: + "Next" - Continue to next step + "Done" - End of flow, return to entry point (non-destructive) + "OK/Close" - Exit current screen (non-destructive) + "Cancel" - End task and return to entry point (destructive) """ class View: def __init__(self) -> None: