From b8d33dd4ecd6e1f4f79a669e3460f2e78f89785a Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 27 Sep 2021 22:43:43 -0500 Subject: [PATCH] Interim commit Initial implementation of new `view_function` Controller loop and `back_stack`. --- src/seedsigner/__init__.py | 1 + src/seedsigner/controller.py | 190 ++++++++++++------ src/seedsigner/gui/renderer.py | 15 +- src/seedsigner/gui/templates.py | 174 ++++++++++------ src/seedsigner/helpers/buttons.py | 51 +++-- src/seedsigner/views/menu_view.py | 212 +++++--------------- src/seedsigner/views/seed_tools_view.py | 28 +-- src/seedsigner/views/settings_tools_view.py | 35 ++-- src/seedsigner/views/view.py | 5 - 9 files changed, 364 insertions(+), 347 deletions(-) create mode 100644 src/seedsigner/__init__.py diff --git a/src/seedsigner/__init__.py b/src/seedsigner/__init__.py new file mode 100644 index 00000000..d1ee4f42 --- /dev/null +++ b/src/seedsigner/__init__.py @@ -0,0 +1 @@ +from .controller import Controller \ No newline at end of file diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 81fe3988..8c2dc76a 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -46,7 +46,7 @@ class Controller(ConfigurableSingleton): cls._instance = controller # Input Buttons - controller.buttons = Buttons() + controller.buttons = Buttons.get_instance() # models controller.storage = SeedStorage() @@ -60,6 +60,9 @@ class Controller(ConfigurableSingleton): # Configure the Renderer Renderer.configure_instance({"text_color": controller.color}) + # TODO: Refactor so that we don't need the Renderer here + controller.renderer = Renderer.get_instance() + # Views controller.menu_view = MenuView() controller.seed_tools_view = SeedToolsView() @@ -68,6 +71,8 @@ class Controller(ConfigurableSingleton): controller.settings_tools_view = SettingsToolsView() controller.screensaver = ScreensaverView(controller.buttons) + controller.back_stack = [] + # Other behavior constants controller.screensaver_activation_ms = 120 * 1000 @@ -78,11 +83,74 @@ class Controller(ConfigurableSingleton): return Camera.get_instance() + def pop_back_stack(self): + if len(self.back_stack) > 0: + return self.back_stack.pop() + return (None, {}) + + def start(self) -> None: + opening_splash = OpeningSplashView() + # opening_splash.start() + + """ Functions can be stored as variables in python! + + This loop receives a view function to execute and stores it in the + `view_function` var along with any input arguments in the `function_args` + dict. + + The `view_function` is executed w/its `function_args`. Each view returns either + a new view function to execute next or None. + + Example: + def my_view(some_arg, other_arg): + print(other_arg) + + def other_view(): + return (my_view, {"some_arg": 1, "other_arg": "hello"}) + + When `other_view` is run, we capture its return values: + + (view_function, function_args) = other_view() + + And then we can execute that view function: + + view_function(**function_args) + """ + try: + view_function = None + function_args = {} + while True: + if not view_function: + # None is a special case; render the Home screen + view_function = self.menu_view.display_main_menu + function_args = {} + + # Home always wipes the back_stack + self.back_stack = [] + + print(f"Executing {view_function}") + + # Each view returns the next view function to display plus any + # function_args in an argument dict. + (view_function, function_args) = view_function(**function_args) + + print(f"view_function: {view_function}") + + if view_function == self.pop_back_stack: + # "Back" arrow was clicked; load the previous view + (view_function, function_args) = self.pop_back_stack() + else: + self.back_stack.append((view_function, function_args)) + + finally: + # Clear the screen when exiting + self.renderer.display_blank_screen() + + + from seedsigner.gui.templates import BaseScreen, ButtonListScreen, FontTesterScreen, BottomButtonScreen from seedsigner.gui.components import Fonts - opening_splash = OpeningSplashView() - opening_splash.start() # tests = [ # ("OpenSans-SemiBold", 16), @@ -238,7 +306,7 @@ class Controller(ConfigurableSingleton): self.show_main_menu() finally: # Clear the screen when exiting - self.menu_view.display_blank_screen() + self.renderer.display_blank_screen() else: # Handle Unexpected crashes by restarting up to 3 times @@ -251,12 +319,12 @@ class Controller(ConfigurableSingleton): break else: print('Caught this error: ' + repr(error)) # debug - self.menu_view.draw_modal(["Crashed ..."], "", "restarting") + self.renderer.draw_modal(["Crashed ..."], "", "restarting") time.sleep(5) crash_cnt += 1 - self.menu_view.draw_modal(["Crashed ..."], "", "requires hard restart") + self.renderer.draw_modal(["Crashed ..."], "", "requires hard restart") def start_screensaver(self): @@ -315,7 +383,7 @@ class Controller(ConfigurableSingleton): def show_power_off(self): - r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Power Off?") + r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Power Off?") if r == 1: #Yes self.menu_view.display_power_off_screen() call("sudo shutdown --poweroff now", shell=True) @@ -358,12 +426,12 @@ class Controller(ConfigurableSingleton): # Ask to save seed if self.storage.slot_avaliable(): - r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Save Seed?") + r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Save Seed?") if r == 1: #Yes slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None) if slot_num in (1,2,3): self.storage.add_seed(seed, slot_num) - self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu") + self.renderer.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu") input = self.buttons.wait_for([B.KEY_RIGHT]) return Path.MAIN_MENU @@ -392,12 +460,12 @@ class Controller(ConfigurableSingleton): # Ask to save seed if self.storage.slot_avaliable(): - r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Save Seed?") + r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Save Seed?") if r == 1: #Yes slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None) if slot_num in (1,2,3): self.storage.add_seed(seed, slot_num) - self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu") + self.renderer.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu") input = self.buttons.wait_for([B.KEY_RIGHT]) return Path.MAIN_MENU @@ -430,12 +498,12 @@ class Controller(ConfigurableSingleton): # Ask to save seed if self.storage.slot_avaliable(): - r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Save Seed?") + r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Save Seed?") if r == 1: #Yes slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None) if slot_num in (1,2,3): self.storage.add_seed(seed, slot_num) - self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu") + self.renderer.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu") input = self.buttons.wait_for([B.KEY_RIGHT]) return Path.MAIN_MENU @@ -487,11 +555,11 @@ class Controller(ConfigurableSingleton): if not seed: # Seed is not valid, Exit if not valid with message - self.menu_view.draw_modal(["Seed Invalid", "check seed phrase", "and try again", ""], "", "Right to Continue") + self.renderer.draw_modal(["Seed Invalid", "check seed phrase", "and try again", ""], "", "Right to Continue") input = self.buttons.wait_for([B.KEY_RIGHT]) return Path.SEED_TOOLS_SUB_MENU else: - self.menu_view.draw_modal(["Valid Seed!"], "", "Right to Continue") + self.renderer.draw_modal(["Valid Seed!"], "", "Right to Continue") input = self.buttons.wait_for([B.KEY_RIGHT]) while display_saved_seed == False: @@ -504,7 +572,7 @@ class Controller(ConfigurableSingleton): if seed: self.storage.add_seed(seed, slot_num) - self.menu_view.draw_modal(["", "Saved to Slot #" + str(slot_num)], "", "Right to Exit") + self.renderer.draw_modal(["", "Saved to Slot #" + str(slot_num)], "", "Right to Exit") input = self.buttons.wait_for([B.KEY_RIGHT]) return Path.SEED_TOOLS_SUB_MENU @@ -516,7 +584,7 @@ class Controller(ConfigurableSingleton): r = 0 if self.storage.num_of_saved_seeds() == 0: - self.menu_view.draw_modal(["Store a seed phrase", "prior to adding", "a passphrase"], "Error", "Right to Continue") + self.renderer.draw_modal(["Store a seed phrase", "prior to adding", "a passphrase"], "Error", "Right to Continue") self.buttons.wait_for([B.KEY_RIGHT]) return Path.SEED_TOOLS_SUB_MENU @@ -533,11 +601,11 @@ class Controller(ConfigurableSingleton): if self.storage.check_slot_passphrase(slot_num) == True: # only display menu to remove/update if there is a passphrase to remove - r = self.menu_view.display_generic_selection_menu(["... [ Return to Seed Tools ]", "Change Passphrase", "Remove Passphrase"], "Passphrase Action") + r = self.renderer.display_generic_selection_menu(["... [ Return to Seed Tools ]", "Change Passphrase", "Remove Passphrase"], "Passphrase Action") if r == 3: # Remove Passphrase Workflow self.storage.delete_passphrase(slot_num) - self.menu_view.draw_modal(["Passphrase Deleted", "from Slot #" + str(slot_num)], "", "Right to Continue") + self.renderer.draw_modal(["Passphrase Deleted", "from Slot #" + str(slot_num)], "", "Right to Continue") self.buttons.wait_for([B.KEY_RIGHT]) return Path.SEED_TOOLS_SUB_MENU @@ -552,9 +620,9 @@ class Controller(ConfigurableSingleton): self.storage.save_passphrase(passphrase, slot_num) if r in (0,1): - self.menu_view.draw_modal(["Passphrase Added", passphrase, "Added to Slot #" + str(slot_num)], "", "Right to Continue") + self.renderer.draw_modal(["Passphrase Added", passphrase, "Added to Slot #" + str(slot_num)], "", "Right to Continue") elif r == 2: - self.menu_view.draw_modal(["Passphrase Updated", passphrase, "Added to Slot #" + str(slot_num)], "", "Right to Continue") + self.renderer.draw_modal(["Passphrase Updated", passphrase, "Added to Slot #" + str(slot_num)], "", "Right to Continue") self.buttons.wait_for([B.KEY_RIGHT]) return Path.SEED_TOOLS_SUB_MENU @@ -570,7 +638,7 @@ class Controller(ConfigurableSingleton): # If there is a saved seed, ask to use saved seed if self.storage.num_of_saved_seeds() > 0: - r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Use Saved Seed?") + r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Use Saved Seed?") if r == 1: #Yes slot_num = self.menu_view.display_saved_seed_menu(self.storage,3,None) if slot_num not in (1,2,3): @@ -595,21 +663,21 @@ class Controller(ConfigurableSingleton): # check if seed phrase is valid if not seed: - self.menu_view.draw_modal(["Seed Invalid", "check seed phrase", "and try again"], "", "Right to Continue") + self.renderer.draw_modal(["Seed Invalid", "check seed phrase", "and try again"], "", "Right to Continue") input = self.buttons.wait_for([B.KEY_RIGHT]) return Path.MAIN_MENU - r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?") + r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?") if r == 1: # display a tool to pick letters/numbers to make a passphrase seed.passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry() if len(seed.passphrase) == 0: - self.menu_view.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue") + self.renderer.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue") input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT]) if input == B.KEY_LEFT: return Path.MAIN_MENU else: - self.menu_view.draw_modal(["Optional passphrase", "added to seed words", seed.passphrase], "", "Right to Continue") + self.renderer.draw_modal(["Optional passphrase", "added to seed words", seed.passphrase], "", "Right to Continue") self.buttons.wait_for([B.KEY_RIGHT]) # display seed phrase @@ -625,14 +693,14 @@ class Controller(ConfigurableSingleton): wallet_type = "multisig" script_type = "native segwit" derivation = self.settings.custom_derivation - r = self.menu_view.display_generic_selection_menu(["Single Sig", "Multisig"], "Wallet Type?") + r = self.renderer.display_generic_selection_menu(["Single Sig", "Multisig"], "Wallet Type?") if r == 1: wallet_type = "single sig" elif r == 2: wallet_type = "multisig" # choose derivation standard - r = self.menu_view.display_generic_selection_menu(["Native Segwit", "Nested Segwit", "Custom"], "Derivation Path?") + r = self.renderer.display_generic_selection_menu(["Native Segwit", "Nested Segwit", "Custom"], "Derivation Path?") if r == 1: script_type = "native segwit" elif r == 2: @@ -648,12 +716,12 @@ class Controller(ConfigurableSingleton): derivation = Settings.calc_derivation(self.settings.network, wallet_type, script_type) if derivation == "" or derivation == None: - self.menu_view.draw_modal(["Invalid Derivation", "try again"], "", "Right to Continue") + self.renderer.draw_modal(["Invalid Derivation", "try again"], "", "Right to Continue") return Path.SEED_TOOLS_SUB_MENU if self.settings.software == "Prompt": lines = ["Specter Desktop", "Blue Wallet", "Sparrow"] - r = self.menu_view.display_generic_selection_menu(lines, "Which Wallet?") + r = self.renderer.display_generic_selection_menu(lines, "Which Wallet?") qr_xpub_type = Settings.getXPubType(lines[r-1]) else: qr_xpub_type = self.settings.qr_xpub_type @@ -697,7 +765,7 @@ class Controller(ConfigurableSingleton): # reusable qr scan function def scan_qr(scan_text="Scan QR"): # Scan QR using Camera - self.menu_view.draw_modal(["Initializing Camera"]) + self.renderer.draw_modal(["Initializing Camera"]) self.camera.start_video_stream_mode(resolution=(480, 480), framerate=12, format="rgb") decoder = DecodeQR(wordlist=self.settings.wordlist) @@ -738,46 +806,46 @@ class Controller(ConfigurableSingleton): if decoder.isComplete() and decoder.isPSBT(): # first QR is PSBT - self.menu_view.draw_modal(["Validating PSBT"]) + self.renderer.draw_modal(["Validating PSBT"]) psbt = decoder.getPSBT() - self.menu_view.draw_modal(["PSBT Valid!", "Enter", "seed phrase", "to sign this tx"], "", "Right to Continue") + self.renderer.draw_modal(["PSBT Valid!", "Enter", "seed phrase", "to sign this tx"], "", "Right to Continue") input = self.buttons.wait_for([B.KEY_RIGHT]) elif decoder.isComplete() and decoder.isSeed(): # first QR is Seed - self.menu_view.draw_modal(["Validating Seed"]) + self.renderer.draw_modal(["Validating Seed"]) seed.mnemonic = decoder.getSeedPhrase() if not seed: # seed is not valid, Exit if not valid with message - self.menu_view.draw_modal(["Seed Invalid", "check seed phrase", "and try again", ""], "", "Right to Continue") + self.renderer.draw_modal(["Seed Invalid", "check seed phrase", "and try again", ""], "", "Right to Continue") input = self.buttons.wait_for([B.KEY_RIGHT]) return Path.MAIN_MENU else: - self.menu_view.draw_modal(["Valid Seed!"], "", "Right to Continue") + self.renderer.draw_modal(["Valid Seed!"], "", "Right to Continue") input = self.buttons.wait_for([B.KEY_RIGHT]) - r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?") + r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?") if r == 1: # display a tool to pick letters/numbers to make a passphrase seed.passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry() if len(seed.passphrase) == 0: - self.menu_view.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue") + self.renderer.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue") input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT]) if input == B.KEY_LEFT: return Path.MAIN_MENU else: - self.menu_view.draw_modal(["Optional passphrase", "added to seed words"], "", "Right to Continue") + self.renderer.draw_modal(["Optional passphrase", "added to seed words"], "", "Right to Continue") self.buttons.wait_for([B.KEY_RIGHT]) # Ask to save seed if self.storage.slot_avaliable(): - r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Save Seed?") + r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Save Seed?") if r == 1: #Yes slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None) if slot_num in (1,2,3): self.storage.add_seed(seed, slot_num) - self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue") + self.renderer.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue") input = self.buttons.wait_for([B.KEY_RIGHT]) # display seed phrase @@ -794,17 +862,17 @@ class Controller(ConfigurableSingleton): if decoder.isComplete() and decoder.isPSBT(): # second QR must be a PSBT - self.menu_view.draw_modal(["Validating PSBT"]) + self.renderer.draw_modal(["Validating PSBT"]) psbt = decoder.getPSBT() elif ( decoder.isComplete() and not decoder.isPSBT() ) or decoder.isInvalid(): - self.menu_view.draw_modal(["Not a valid PSBT QR"], "", "Right to Exit") + self.renderer.draw_modal(["Not a valid PSBT QR"], "", "Right to Exit") input = self.buttons.wait_for([B.KEY_RIGHT]) return Path.MAIN_MENU else: return Path.MAIN_MENU elif ( decoder.isComplete() and not decoder.isPSBT() ) or decoder.isInvalid(): - self.menu_view.draw_modal(["Not a valid PSBT QR"], "", "Right to Exit") + self.renderer.draw_modal(["Not a valid PSBT QR"], "", "Right to Exit") input = self.buttons.wait_for([B.KEY_RIGHT]) return Path.MAIN_MENU else: @@ -813,7 +881,7 @@ class Controller(ConfigurableSingleton): if not seed: # No valid seed yet, If there is a saved seed, ask to use saved seed if self.storage.num_of_saved_seeds() > 0: - r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Use Save Seed?") + r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Use Save Seed?") if r == 1: #Yes slot_num = self.menu_view.display_saved_seed_menu(self.storage,3,None) if slot_num == 0: @@ -838,24 +906,24 @@ class Controller(ConfigurableSingleton): return Path.MAIN_MENU # check if seed phrase is valid - self.menu_view.draw_modal(["Validating Seed ..."]) + self.renderer.draw_modal(["Validating Seed ..."]) if not seed: - self.menu_view.draw_modal(["Seed Invalid", "check seed phrase", "and try again"], "", "Right to Continue") + self.renderer.draw_modal(["Seed Invalid", "check seed phrase", "and try again"], "", "Right to Continue") input = self.buttons.wait_for([B.KEY_RIGHT]) return Path.MAIN_MENU if len(seed.passphrase) == 0: - r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?") + r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?") if r == 1: # display a tool to pick letters/numbers to make a passphrase seed.passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry() if len(seed.passphrase) == 0: - self.menu_view.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue") + self.renderer.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue") input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT]) if input == B.KEY_LEFT: return Path.MAIN_MENU else: - self.menu_view.draw_modal(["Optional passphrase", "added to seed words", seed.passphrase], "", "Right to Continue") + self.renderer.draw_modal(["Optional passphrase", "added to seed words", seed.passphrase], "", "Right to Continue") self.buttons.wait_for([B.KEY_RIGHT]) # display seed phrase @@ -869,16 +937,16 @@ class Controller(ConfigurableSingleton): # Ask to save seed if self.storage.slot_avaliable() and used_saved_seed == False: - r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Save Seed?") + r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Save Seed?") if r == 1: #Yes slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None) if slot_num in (1,2,3): self.storage.add_seed(seed, slot_num) - self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue") + self.renderer.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue") input = self.buttons.wait_for([B.KEY_RIGHT]) # show transaction information before sign - self.menu_view.draw_modal(["Parsing PSBT"]) + self.renderer.draw_modal(["Parsing PSBT"]) p = PSBTParser(psbt,seed,self.settings.network) self.signing_tools_view.display_transaction_information(p) input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT], False) @@ -886,19 +954,19 @@ class Controller(ConfigurableSingleton): return Path.MAIN_MENU # Sign PSBT - self.menu_view.draw_modal(["PSBT Signing ..."]) + self.renderer.draw_modal(["PSBT Signing ..."]) sig_cnt = PSBTParser.sigCount(psbt) psbt.sign_with(p.root) trimmed_psbt = PSBTParser.trim(psbt) if sig_cnt == PSBTParser.sigCount(trimmed_psbt): - self.menu_view.draw_modal(["Signing failed", "left to exit", "or right to continue", "to display PSBT QR"], "", "") + self.renderer.draw_modal(["Signing failed", "left to exit", "or right to continue", "to display PSBT QR"], "", "") input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT], False) if input == B.KEY_LEFT: return Path.MAIN_MENU # Display Animated QR Code - self.menu_view.draw_modal(["Generating PSBT QR ..."]) + self.renderer.draw_modal(["Generating PSBT QR ..."]) e = EncodeQR(psbt=trimmed_psbt, qr_type=self.settings.qr_psbt_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist) while True: image = e.nextPartImage(240,240,1) @@ -967,12 +1035,12 @@ class Controller(ConfigurableSingleton): r = self.settings_tools_view.display_persistent_settings() if r is not None: if r == True: - self.menu_view.draw_modal(["Persistent settings", "keeps settings saved", "across reboot.", "Seeds are never saved"], "Warning", "Right to Continue") + self.renderer.draw_modal(["Persistent settings", "keeps settings saved", "across reboot.", "Seeds are never saved"], "Warning", "Right to Continue") input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT]) if input == B.KEY_RIGHT: self.settings.persistent = r else: - self.menu_view.draw_modal(["This will restore", "the default", "settings.", ""], "Warning", "Right to Continue") + self.renderer.draw_modal(["This will restore", "the default", "settings.", ""], "Warning", "Right to Continue") input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT]) if input == B.KEY_RIGHT: self.settings.persistent = r @@ -1000,12 +1068,12 @@ class Controller(ConfigurableSingleton): return Path.MAIN_MENU def show_reset_tool(self): - self.menu_view.draw_modal(["This will restore", "default settings and", "restart the app", ""], "Warning", "Right to Continue") + self.renderer.draw_modal(["This will restore", "default settings and", "restart the app", ""], "Warning", "Right to Continue") input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT]) if input == B.KEY_RIGHT: - r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Reset SeedSigner?") + r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Reset SeedSigner?") if r == 1: #Yes - self.menu_view.display_blank_screen() + self.renderer.display_blank_screen() self.settings.restoreDefault() time.sleep(0.1) # give time to write to disk diff --git a/src/seedsigner/gui/renderer.py b/src/seedsigner/gui/renderer.py index 2bb40c8f..301003f5 100644 --- a/src/seedsigner/gui/renderer.py +++ b/src/seedsigner/gui/renderer.py @@ -84,6 +84,7 @@ class Renderer(ConfigurableSingleton): self.disp.ShowImage(crop, 0, 0) + # TODO: Should probably move this to templates.py def show_image_with_text(self, image, text, font=None, text_color="GREY", text_background=None): image_copy = image.copy().convert("RGBA") draw = ImageDraw.Draw(image_copy) @@ -100,6 +101,7 @@ class Renderer(ConfigurableSingleton): self.show_image(image_copy, alpha_overlay=text_overlay) + # TODO: Should probably move this to templates.py def draw_modal(self, lines = [], title = "", bottom = "") -> None: self.draw.rectangle((0, 0, self.canvas_width, self.canvas_height), outline=0, fill=0) @@ -141,14 +143,14 @@ class Renderer(ConfigurableSingleton): return + # TODO: Should probably move this to templates.py def draw_prompt_yes_no(self, lines = [], title = "", bottom = "") -> None: - self.draw_prompt_custom("", "Yes ", "No ", lines, title, bottom) return + # TODO: Should probably move this to templates.py def draw_prompt_custom(self, a_txt, b_txt, c_txt, lines = [], title = "", bottom = "") -> None: - self.draw.rectangle((0, 0, self.canvas_width, self.canvas_height), outline=0, fill=0) if len(title) > 0: @@ -196,3 +198,12 @@ class Renderer(ConfigurableSingleton): self.show_image() return + + + def display_blank_screen(self): + self.draw.rectangle((0, 0, self.canvas_width, self.canvas_height), outline=0, fill=0) + self.show_image() + + + + diff --git a/src/seedsigner/gui/templates.py b/src/seedsigner/gui/templates.py index a6d771da..e27d9318 100644 --- a/src/seedsigner/gui/templates.py +++ b/src/seedsigner/gui/templates.py @@ -2,70 +2,118 @@ from .components import (EDGE_PADDING, COMPONENT_PADDING, Button, TopNav, TextArea) +from dataclasses import dataclass from PIL import ImageFont -from seedsigner.helpers import B +from seedsigner.helpers import B, Buttons from seedsigner.views import View +@dataclass class BaseScreen: - def __init__(self, - title:str, - font: ImageFont = None): + # Avoid setting defaults on parent dataclasses, otherwise you must have defaults on + # all child attrs. see: https://stackoverflow.com/a/53085935 + # No base attrs specified yet + + + def __post_init__(self): from seedsigner.gui import Renderer self.renderer = Renderer.get_instance() - self.title = title + self.hw_inputs = Buttons.get_instance() + + + def render(self, show_image: bool = True): + # Clear the whole canvas + self.renderer.draw.rectangle((0, 0, self.renderer.canvas_width, self.renderer.canvas_height), fill=0) + + if show_image: + # Will be false when called by child classes; they'll want to keep writing + # their own UI content before calling show_image() themselves. + self.renderer.show_image() + + + def run(self): + """ + Screen can run on its own until it returns a final exit input from the user. + + For example: A basic menu screen where the user can key up and down. The + Screen can handle the UI updates to light up the currently selected menu item + on its own. Only when the user clicks to make a selection would run() exit + and returns the selected option. + + But an alternate use case returns immediately after each user input so the + View can update its controlling logic accordingly (e.g. as the user joysticks + over different letters in the keyboard UI, we need to make matching changes + to the list of mnemonic seed words that match the new letter). + + In this case, it would be called repeatedly in a loop: + * run() and wait for it to handle user input + * run() exits and returns the user input (e.g. KEY_UP) + * View updates its state of the world accordingly + * loop and call run() again + """ + raise Exception("Must implement in a child class") + + + +@dataclass +class BaseTopNavScreen(BaseScreen): + title:str + + def __post_init__(self): + super().__post_init__() self.top_nav = TopNav( text=self.title, width=self.renderer.canvas_width, height=(2 * EDGE_PADDING) + int(self.renderer.canvas_width * 2.0 / 15.0), # 32px on a 240x240 screen - font=font, ) - def render(self): - self.renderer.draw.rectangle((0, 0, self.renderer.canvas_width, self.renderer.canvas_height), fill=0) + def render(self, show_image: bool = True): + super().render(show_image=False) # Child always tells parent to wait + self.top_nav.render() - self.renderer.show_image() + + if show_image: + self.renderer.show_image() -class ButtonListScreen(BaseScreen): - def __init__(self, - title: str, - button_data: list, # w/Python 3.9 we can be more specific: list[str] - is_button_text_centered: bool, - is_bottom_list: bool, - title_font: ImageFont = None, - button_font: ImageFont = None, - button_selected_color: str = "orange"): - super().__init__(title, title_font) +@dataclass +class ButtonListScreen(BaseTopNavScreen): + title: str # Not necessary to include this since it's in parent; just here for clarity + button_labels: list # w/Python 3.9 we can be more specific: list[str] + selected_button: int = 0 + is_button_text_centered: bool = True + is_bottom_list: bool = False + button_font: ImageFont = None + button_selected_color: str = "orange" - self.button_data = button_data - self.is_bottom_list = is_bottom_list + def __post_init__(self): + super().__post_init__() button_height = int(self.renderer.canvas_height * 3.0 / 20.0) # 36px on a 240x240 screen - if len(self.button_data) == 1: + if len(self.button_labels) == 1: button_list_height = button_height else: - button_list_height = (len(self.button_data) * button_height) + (COMPONENT_PADDING * (len(self.button_data) - 1)) + button_list_height = (len(self.button_labels) * button_height) + (COMPONENT_PADDING * (len(self.button_labels) - 1)) - if is_bottom_list: + if self.is_bottom_list: button_list_y = self.renderer.canvas_height - (button_list_height + EDGE_PADDING) else: button_list_y = self.top_nav.height + int((self.renderer.canvas_height - self.top_nav.height - button_list_height) / 2) self.buttons = [] - for i, entry in enumerate(button_data): + for i, button_label in enumerate(self.button_labels): button = Button( - text=entry["text"], + text=button_label, screen_x=EDGE_PADDING, screen_y=button_list_y + i * (button_height + COMPONENT_PADDING), width=self.renderer.canvas_width - (2 * EDGE_PADDING), height=button_height, - is_text_centered=is_button_text_centered, - font=button_font, - selected_color=button_selected_color + is_text_centered=self.is_button_text_centered, + font=self.button_font, + selected_color=self.button_selected_color ) self.buttons.append(button) @@ -73,43 +121,47 @@ class ButtonListScreen(BaseScreen): self.selected_button = 0 - def render(self): - self.renderer.draw.rectangle((0, 0, self.renderer.canvas_width, self.renderer.canvas_height), fill=0) - self.top_nav.render() + def render(self, show_image: bool = True): + super().render(show_image=False) # Child always tells parent to wait + for button in self.buttons: button.render() - self.renderer.show_image() - - - def update_from_input(self, input): - if input == B.KEY_UP: - if self.selected_button == 0: - # Move selection up to top_nav - pass - else: - self.buttons[self.selected_button].is_selected = False - self.buttons[self.selected_button].render() - self.selected_button -= 1 - self.buttons[self.selected_button].is_selected = True - self.buttons[self.selected_button].render() - - elif input == B.KEY_DOWN: - if self.selected_button == len(self.buttons) - 1: - # Trap selection at bottom? - pass - else: - self.buttons[self.selected_button].is_selected = False - self.buttons[self.selected_button].render() - self.selected_button += 1 - self.buttons[self.selected_button].is_selected = True - self.buttons[self.selected_button].render() - - else: - return self.renderer.show_image() + def run(self): + while True: + user_input = self.hw_inputs.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS], check_release=True, release_keys=[B.KEY_PRESS]) + if user_input == B.KEY_UP: + if self.selected_button == 0: + # TODO: Move selection up to top_nav + pass + else: + self.buttons[self.selected_button].is_selected = False + self.buttons[self.selected_button].render() + self.selected_button -= 1 + self.buttons[self.selected_button].is_selected = True + self.buttons[self.selected_button].render() + + elif user_input == B.KEY_DOWN: + if self.selected_button == len(self.buttons) - 1: + # TODO: Trap selection at bottom? + pass + else: + self.buttons[self.selected_button].is_selected = False + self.buttons[self.selected_button].render() + self.selected_button += 1 + self.buttons[self.selected_button].is_selected = True + self.buttons[self.selected_button].render() + + elif user_input == B.KEY_PRESS: + return self.selected_button + + # Write the screen updates + self.renderer.show_image() + + class BottomButtonScreen(ButtonListScreen): def __init__(self, @@ -173,5 +225,3 @@ class FontTesterScreen(ButtonListScreen): font=font, button_font=button_font ) - - diff --git a/src/seedsigner/helpers/buttons.py b/src/seedsigner/helpers/buttons.py index d7f6fea3..cba15f96 100644 --- a/src/seedsigner/helpers/buttons.py +++ b/src/seedsigner/helpers/buttons.py @@ -1,9 +1,10 @@ import RPi.GPIO as GPIO import time +from seedsigner.models import Singleton -class Buttons: +class Buttons(Singleton): KEY_UP_PIN = 6 KEY_DOWN_PIN = 19 KEY_LEFT_PIN = 5 @@ -14,29 +15,37 @@ class Buttons: KEY2_PIN = 20 KEY3_PIN = 16 - def __init__(self) -> None: - #init GPIO - GPIO.setmode(GPIO.BCM) - GPIO.setup(Buttons.KEY_UP_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up - GPIO.setup(Buttons.KEY_DOWN_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up - GPIO.setup(Buttons.KEY_LEFT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up - GPIO.setup(Buttons.KEY_RIGHT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up - GPIO.setup(Buttons.KEY_PRESS_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up - GPIO.setup(Buttons.KEY1_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up - GPIO.setup(Buttons.KEY2_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up - GPIO.setup(Buttons.KEY3_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up - self.GPIO = GPIO - self.override_ind = False + @classmethod + def get_instance(cls): + # This is the only way to access the one and only instance + if cls._instance is None: + cls._instance = cls.__new__(cls) - self.add_events([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS, B.KEY_LEFT, B.KEY_RIGHT, B.KEY1, B.KEY2, B.KEY3]) + #init GPIO + GPIO.setmode(GPIO.BCM) + GPIO.setup(Buttons.KEY_UP_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up + GPIO.setup(Buttons.KEY_DOWN_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up + GPIO.setup(Buttons.KEY_LEFT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up + GPIO.setup(Buttons.KEY_RIGHT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up + GPIO.setup(Buttons.KEY_PRESS_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up + GPIO.setup(Buttons.KEY1_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up + GPIO.setup(Buttons.KEY2_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up + GPIO.setup(Buttons.KEY3_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up - # Track state over time so we can apply input delays/ignores as needed - self.cur_input = None # Track which direction or button was last pressed - self.cur_input_started = None # Track when that input began - self.last_input_time = int(time.time() * 1000) # How long has it been since the last input? - self.first_repeat_threshold = 175 # Long-press time required before returning continuous input - self.next_repeat_threshold = 250 # Amount of time where we no longer consider input a continuous hold + cls._instance.GPIO = GPIO + cls._instance.override_ind = False + + cls._instance.add_events([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS, B.KEY_LEFT, B.KEY_RIGHT, B.KEY1, B.KEY2, B.KEY3]) + + # Track state over time so we can apply input delays/ignores as needed + 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.next_repeat_threshold = 250 # Amount of time where we no longer consider input a continuous hold + + return cls._instance def wait_for(self, keys=[], check_release=True, release_keys=[]) -> int: diff --git a/src/seedsigner/views/menu_view.py b/src/seedsigner/views/menu_view.py index f9b0793b..9a53c198 100644 --- a/src/seedsigner/views/menu_view.py +++ b/src/seedsigner/views/menu_view.py @@ -2,7 +2,8 @@ from . import View from seedsigner.gui.components import Fonts -from seedsigner.helpers import B, Path +from seedsigner.gui.templates import ButtonListScreen +from seedsigner.helpers import B, Path, Buttons from seedsigner.models import SeedStorage, Settings, Seed # External Dependencies @@ -18,53 +19,48 @@ class MenuView(View): self.menu_lines = [] self.selected_menu_num = 1 + ### ### Main Navigation ### ### Main Menu - def display_main_menu(self, sub_menu = None) -> int: - ret_val = 0 - input = 0 - lines = ["Seed Tools", "Scan QR", "Settings", "Power Off"] + button_labels = [ + "Seed Tools", + "Scan QR", + "Settings", + "Power Off" + ] - if sub_menu == Path.SEED_TOOLS_SUB_MENU: - return self.display_seed_tools_menu() - elif sub_menu == Path.SIGNING_TOOLS_SUB_MENU: - return Path.SIGN_TRANSACTION - elif sub_menu == Path.SETTINGS_SUB_MENU: - return self.display_settings_menu() - else: - self.draw_menu(lines, 1) + # if sub_menu == Path.SEED_TOOLS_SUB_MENU: + # return self.display_seed_tools_menu() + # elif sub_menu == Path.SIGNING_TOOLS_SUB_MENU: + # return Path.SIGN_TRANSACTION + # elif sub_menu == Path.SETTINGS_SUB_MENU: + # return self.display_settings_menu() + + menu_screen = ButtonListScreen(title="SeedSigner", button_labels=button_labels, selected_button=0) + menu_screen.render() + selected_menu_num = menu_screen.run() - # Wait for Button Input (specifically menu selection/press) - while True: - if ret_val == 0: - input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS], check_release=True, release_keys=[B.KEY_PRESS]) - else: - return ret_val - if input == B.KEY_UP: - self.menu_up() - elif input == B.KEY_DOWN: - self.menu_down() - elif input == B.KEY_PRESS: - if self.selected_menu_num == 1: - ret_val = self.display_seed_tools_menu() - elif self.selected_menu_num == 2: - ret_val = Path.SIGN_TRANSACTION - elif self.selected_menu_num == 3: - ret_val = self.display_settings_menu() - elif self.selected_menu_num == 4: - ret_val = Path.POWER_OFF + print(f"selected_menu_num: {selected_menu_num}") + + if selected_menu_num == 0: + print("returning selected_menu_num") + return (self.display_seed_tools_menu, {}) + elif selected_menu_num == 1: + ret_val = Path.SIGN_TRANSACTION + elif selected_menu_num == 2: + return (self.display_settings_menu, {}) + elif selected_menu_num == 3: + ret_val = Path.POWER_OFF + + # if ret_val != Path.MAIN_MENU: # When no main menu, return to controller + # return ret_val - if ret_val != Path.MAIN_MENU: # When no main menu, return to controller - return ret_val - else: - self.draw_menu(lines) ### Seed Tools Menu - def display_seed_tools_menu(self) -> int: seed_storage_line = "Store a Seed (temp)" if self.controller.storage.num_of_saved_seeds() > 0: @@ -100,8 +96,8 @@ class MenuView(View): elif self.selected_menu_num == 7: return Path.IMAGE_GEN_SEED - ### Signing Tools Menu + ### Signing Tools Menu def display_signing_tools_menu(self) -> None: lines = ["... [ Return to Main ]", "Generate xPub", "Sign a Transaction"] self.draw_menu(lines, 1) @@ -123,16 +119,22 @@ class MenuView(View): return Path.SIGN_TRANSACTION raise Exception("Unhandled case") - ### Settings Menu + ### Settings Menu def display_settings_menu(self) -> int: - lines = ["... [ Return to Main ]", "Wallet: ", "Network: ", "QR Density: ", "Input / Output Tests", "Persistent Settings: ", "Camera Rotation", "Version Info", "Donate to SeedSigner", "Reset SeedSigner"] + lines = [ + "... [ Return to Main ]", + f"Wallet: {Settings.get_instance().software}", + f"Network: {Settings.get_instance().network}", + f"QR Density: {Settings.get_instance().qr_density_name}", + "Input / Output Tests", + f"Persistent Settings: {Settings.get_instance().persistent_display}", + "Camera Rotation", + "Version Info", + "Donate to SeedSigner", + "Reset SeedSigner" + ] input = 0 - - lines[1] = lines[1].replace("", Settings.get_instance().software) - lines[2] = lines[2].replace("", Settings.get_instance().network) - lines[3] = lines[3].replace("", Settings.get_instance().qr_density_name) - lines[5] = lines[5].replace("", Settings.get_instance().persistent_display) # Draw Menu self.selected_menu_num = 1 @@ -168,21 +170,6 @@ class MenuView(View): return Path.RESET raise Exception("Unhandled case") - ### Generic Single Menu Selection (returns 1,2,3,4,5,6 ...) - - def display_generic_selection_menu(self, lines = [], title = None, bottom = None) -> int: - self.selected_menu_num = 1 - self.draw_menu(lines, 1, title, bottom, True) - - while True: - input = self.buttons.wait_for([B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS], check_release=True, release_keys=[B.KEY_PRESS]) - if input == B.KEY_UP: - self.menu_up(title, bottom) - elif input == B.KEY_DOWN: - self.menu_down(title, bottom) - elif input == B.KEY_PRESS: - return self.selected_menu_num - raise Exception("Unhandled case") ### Generic Word 12 or 24 seed phrase menu # internal method @@ -276,111 +263,6 @@ class MenuView(View): return int(re.search("#(\d+)", lines[self.selected_menu_num-1], re.IGNORECASE).group(1)) raise Exception("Unhandled case") - ### - ### Generic Reusable Menu Methods/Functions - ### - - ### Generic Draw Menu Method - # TODO: Optimize updates by just redrawing the no-longer highlighted line and the newly highlighted line - def draw_menu(self, lines, selected_menu_num = 1, title = None, bottom = None, force_redraw = False) -> None: - if title == None: - t = "SeedSigner v" + self.controller.VERSION - else: - t = title - - if bottom == None and len(lines) <= 5: - b = "Press Control Stick to Select" - elif bottom == None: - if len(lines) >= 6 and len(lines) <= 10: - if selected_menu_num <= 5: - b = "Page 1 of 2" - elif selected_menu_num >= 6 and selected_menu_num <= 10: - b = "Page 2 of 2" - elif len(lines) >= 11 and len(lines) <= 15: - if selected_menu_num <= 5: - b = "Page 1 of 3" - elif selected_menu_num >= 6 and selected_menu_num <= 10: - b = "Page 2 of 3" - elif selected_menu_num >= 11 and selected_menu_num <= 15: - b = "Page 3 of 3" - else: - b = "Press Control Stick to Select" - else: - b = bottom - - if lines != self.menu_lines or selected_menu_num != self.selected_menu_num or force_redraw == True: - #Menu has changed, redraw - - self.renderer.draw.rectangle((0, 0, self.canvas_width, self.canvas_height), outline=0, fill=0) - tw, th = self.renderer.draw.textsize(t, font=Fonts.get_font("Assistant-Medium", 22)) - self.renderer.draw.text(((240 - tw) / 2, 2), t, fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - - num_of_lines = len(lines) - - if selected_menu_num <= 5: - if num_of_lines >= 1: - self.draw_menu_text(15, 43 , lines[0], (True if selected_menu_num == 1 else False)) - if num_of_lines >= 2: - self.draw_menu_text(15, 76 , lines[1], (True if selected_menu_num == 2 else False)) - if num_of_lines >= 3: - self.draw_menu_text(15, 109, lines[2], (True if selected_menu_num == 3 else False)) - if num_of_lines >= 4: - self.draw_menu_text(15, 142, lines[3], (True if selected_menu_num == 4 else False)) - if num_of_lines >= 5: - self.draw_menu_text(15, 175, lines[4], (True if selected_menu_num == 5 else False)) - elif selected_menu_num >= 6 and selected_menu_num <= 10: - if num_of_lines >= 6: - self.draw_menu_text(15, 43 , lines[5], (True if selected_menu_num == 6 else False)) - if num_of_lines >= 7: - self.draw_menu_text(15, 76 , lines[6], (True if selected_menu_num == 7 else False)) - if num_of_lines >= 8: - self.draw_menu_text(15, 109, lines[7], (True if selected_menu_num == 8 else False)) - if num_of_lines >= 9: - self.draw_menu_text(15, 142, lines[8], (True if selected_menu_num == 9 else False)) - if num_of_lines >= 10: - self.draw_menu_text(15, 175, lines[9], (True if selected_menu_num == 10 else False)) - elif selected_menu_num >= 11 and selected_menu_num <= 15: - if num_of_lines >= 11: - self.draw_menu_text(15, 43 , lines[10], (True if selected_menu_num == 11 else False)) - if num_of_lines >= 12: - self.draw_menu_text(15, 76 , lines[11], (True if selected_menu_num == 12 else False)) - if num_of_lines >= 13: - self.draw_menu_text(15, 109, lines[12], (True if selected_menu_num == 13 else False)) - if num_of_lines >= 14: - self.draw_menu_text(15, 142, lines[13], (True if selected_menu_num == 14 else False)) - if num_of_lines >= 15: - self.draw_menu_text(15, 175, lines[14], (True if selected_menu_num == 15 else False)) - - tw, th = self.renderer.draw.textsize(b, font=Fonts.get_font("Assistant-Medium", 18)) - self.renderer.draw.text(((240 - tw) / 2, 210), b, fill=self.color, font=Fonts.get_font("Assistant-Medium", 18)) - self.renderer.show_image() - - # saved update menu lines and selection - self.menu_lines = lines - self.selected_menu_num = selected_menu_num - ### Generic Menu Navigation - def menu_up(self, title = None, bottom = None): - if self.selected_menu_num <= 1: - self.draw_menu(self.menu_lines, len(self.menu_lines), title, bottom) - else: - self.draw_menu(self.menu_lines, self.selected_menu_num - 1, title, bottom) - - def menu_down(self, title = None, bottom = None): - if self.selected_menu_num >= len(self.menu_lines): - self.draw_menu(self.menu_lines, 1, title, bottom) - else: - self.draw_menu(self.menu_lines, self.selected_menu_num + 1, title, bottom) - - ### Internal View Method to Display a Line in a Menu Screen - - def draw_menu_text(self, x, y, line, selected) -> None: - if selected == True: - self.renderer.draw.rectangle((5, y-3, 235, y+28), outline=0, fill=self.color) - self.renderer.draw.text((x, y) , line, fill="BLACK", font=Fonts.get_font("Assistant-Bold", 20)) - else: - self.renderer.draw.text((x, y) , line, fill=self.color, font=Fonts.get_font("Assistant-Medium", 20)) - - return diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 5a1d2025..bc309107 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -59,7 +59,7 @@ class SeedToolsView(View): if len(self.words) >= cur_word: initial_letters = list(self.words[cur_word - 1]) # zero-indexed - ret_val = self.renderer.draw_seed_word_keyboard_entry(num_word=cur_word, initial_letters=initial_letters) + ret_val = self.draw_seed_word_keyboard_entry(num_word=cur_word, initial_letters=initial_letters) if ret_val == Keyboard.KEY_PREVIOUS_PAGE: # Reload previous word @@ -684,7 +684,7 @@ class SeedToolsView(View): self.dice_selected = 5 self.roll_data = "" - self.renderer.draw_dice(self.dice_selected) + self.draw_dice(self.dice_selected) time.sleep(1) # pause for 1 second before accepting input # Wait for Button Input (specifically menu selection/press) @@ -719,7 +719,7 @@ class SeedToolsView(View): new_selection = 3 if self.dice_selected != new_selection and new_selection != 0: - self.renderer.draw_dice(new_selection) + self.draw_dice(new_selection) return True @@ -734,7 +734,7 @@ class SeedToolsView(View): new_selection = 6 if self.dice_selected != new_selection and new_selection != 0: - self.renderer.draw_dice(new_selection) + self.draw_dice(new_selection) return True @@ -751,25 +751,25 @@ class SeedToolsView(View): new_selection = 6 if self.dice_selected != new_selection and new_selection != 0: - self.renderer.draw_dice(new_selection) + self.draw_dice(new_selection) return True def dice_arrow_left(self): if self.dice_selected == 1: - self.renderer.draw_prompt_custom("Undo ", "Cancel ", "Exit ", ["Action: ", "", ""]) + self.draw_prompt_custom("Undo ", "Cancel ", "Exit ", ["Action: ", "", ""]) input = self.buttons.wait_for([B.KEY1, B.KEY2, B.KEY3]) if input == B.KEY1: #Undo self.roll_number = self.roll_number - 1 self.roll_data = self.roll_data[:-1] # remove last character from string if self.roll_number >= 1: - self.renderer.draw_dice(self.dice_selected) + self.draw_dice(self.dice_selected) return True else: return False elif input == B.KEY2: # Cancel - self.renderer.draw_dice(self.dice_selected) + self.draw_dice(self.dice_selected) return True elif input == B.KEY3: # Exit return False @@ -785,7 +785,7 @@ class SeedToolsView(View): new_selection = 4 if self.dice_selected != new_selection and new_selection != 0: - self.renderer.draw_dice(new_selection) + self.draw_dice(new_selection) return True @@ -800,7 +800,7 @@ class SeedToolsView(View): # Reset for the next UI render self.dice_selected = 5 if self.roll_number < 100: - self.renderer.draw_dice(self.dice_selected) + self.draw_dice(self.dice_selected) return True @@ -1090,7 +1090,7 @@ class SeedToolsView(View): ) while True: - # self.renderer.draw_text_over_image("click to exit", font=Fonts.get_font("Assistant-Medium", 18), text_color="BLACK", text_background="ORANGE") + # self.draw_text_over_image("click to exit", font=Fonts.get_font("Assistant-Medium", 18), text_color="BLACK", text_background="ORANGE") input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS]) if input == B.KEY_RIGHT: @@ -1134,7 +1134,7 @@ class SeedToolsView(View): cur_y = next_y def read_seed_phrase_qr(self): - self.renderer.draw_modal(["Scanning..."], "Seed QR" ,"Right to Exit") + self.draw_modal(["Scanning..."], "Seed QR" ,"Right to Exit") try: self.controller.camera.start_video_stream_mode(resolution=(480, 480), framerate=12, format="rgb") decoder = DecodeQR(wordlist=self.controller.settings.wordlist) @@ -1155,10 +1155,10 @@ class SeedToolsView(View): if decoder.isComplete() and decoder.isSeed(): self.words = decoder.getSeedPhrase() elif not decoder.isPSBT(): - self.renderer.draw_modal(["Not a valid Seed QR"], "", "Right to Exit") + self.draw_modal(["Not a valid Seed QR"], "", "Right to Exit") input = self.buttons.wait_for([B.KEY_RIGHT]) else: - self.renderer.draw_modal(["QR Parsing Failed"], "", "Right to Exit") + self.draw_modal(["QR Parsing Failed"], "", "Right to Exit") input = self.buttons.wait_for([B.KEY_RIGHT]) self.words = [] diff --git a/src/seedsigner/views/settings_tools_view.py b/src/seedsigner/views/settings_tools_view.py index 920cc9d4..0953c965 100644 --- a/src/seedsigner/views/settings_tools_view.py +++ b/src/seedsigner/views/settings_tools_view.py @@ -3,8 +3,9 @@ from . import View from seedsigner.helpers import B, QR, Keyboard, TextEntryDisplay from seedsigner.models import EncodeQRDensity -class SettingsToolsView(View): + +class SettingsToolsView(View): def __init__(self) -> None: View.__init__(self) @@ -12,22 +13,23 @@ class SettingsToolsView(View): self.donate_image = None self.derivation = None - ### Donate Menu Item + ### Donate Menu Item def display_donate_info_screen(self): - self.draw_modal(["You can support", "SeedSigner by donating", "any amount of BTC", "Thank You!!!"], "", "(Press right for a QR code)") + self.renderer.draw_modal(["You can support", "SeedSigner by donating", "any amount of BTC", "Thank You!!!"], "", "(Press right for a QR code)") return True + def display_donate_qr(self): - self.draw_modal(["Loading..."]) + self.renderer.draw_modal(["Loading..."]) self.donate_image = self.qr.qrimage("bc1qphlyv2dde290tqdlnk8uswztnshw3x9rjurexqqhksvu7vdevhtsuw4efe") self.renderer.show_image(self.donate_image) return True - ### Display Network Selection + ### Display Network Selection def display_current_network(self) -> str: - r = self.controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Mainnet", "Testnet"], "Which Network?") + r = self.renderer.display_generic_selection_menu(["... [ Return to Settings ]", "Mainnet", "Testnet"], "Which Network?") if r == 2: return "main" elif r == 3: @@ -35,10 +37,10 @@ class SettingsToolsView(View): else: return None - ### Display Wallet Selection + ### Display Wallet Selection def display_wallet_selection(self) -> str: - r = self.controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Prompt", "Specter Desktop", "Blue Wallet", "Sparrow"], "Which Wallet?") + r = self.renderer.display_generic_selection_menu(["... [ Return to Settings ]", "Prompt", "Specter Desktop", "Blue Wallet", "Sparrow"], "Which Wallet?") if r == 2: return "Prompt" elif r == 3: @@ -50,10 +52,10 @@ class SettingsToolsView(View): else: return None - ### Display QR Density Selection + ### Display QR Density Selection def display_qr_density_selection(self) -> str: - r = self.controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Low", "Medium", "High"], "Which QR Density?") + r = self.renderer.display_generic_selection_menu(["... [ Return to Settings ]", "Low", "Medium", "High"], "Which QR Density?") if r == 2: return EncodeQRDensity.LOW elif r == 3: @@ -63,13 +65,13 @@ class SettingsToolsView(View): else: return None - def display_persistent_settings(self) -> bool: + def display_persistent_settings(self) -> bool: lines = ["... [ Return to Settings ]"] lines.append("Yes") lines.append("No") - r = self.controller.menu_view.display_generic_selection_menu(lines, "Use Persistent Settings?") + r = self.renderer.display_generic_selection_menu(lines, "Use Persistent Settings?") if r == 1: return None elif r == 2: @@ -87,7 +89,7 @@ class SettingsToolsView(View): lines.append("180") lines.append("270") - r = self.controller.menu_view.display_generic_selection_menu(lines, "Camera Rotation") + r = self.renderer.display_generic_selection_menu(lines, "Camera Rotation") if r == 1: return None elif r == 2: @@ -101,12 +103,11 @@ class SettingsToolsView(View): else: return None + ### ### Version Info ### - - def display_version_info(self): - + def display_version_info(self): line1 = "SeedSigner" line2 = "Version v" + self.controller.VERSION line3 = "(Joystick RIGHT to EXIT)" @@ -120,10 +121,10 @@ class SettingsToolsView(View): self.renderer.draw.text(((240 - tw) / 2, 210), line3, fill=View.color, font=Fonts.get_font("Assistant-Medium", 18)) self.renderer.show_image() + ### ### Custom Derivation Path ### - def draw_derivation_keyboard_entry(self, existing_derivation = "m/"): def render_right_panel(): row_height = 28 diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 158532fc..02c5763c 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -57,11 +57,6 @@ class View: self.renderer.show_image() - def display_blank_screen(self): - self.renderer.draw.rectangle((0, 0, self.canvas_width, self.canvas_height), outline=0, fill=0) - self.renderer.show_image() - - ### ### Reusable components ###