mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 23:18:25 +00:00
Interim commit
Initial implementation of new `view_function` Controller loop and `back_stack`.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
from .controller import Controller
|
||||
+129
-61
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+112
-62
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: <wallet>", "Network: <network>", "QR Density: <density>", "Input / Output Tests", "Persistent Settings: <persistent>", "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("<wallet>", Settings.get_instance().software)
|
||||
lines[2] = lines[2].replace("<network>", Settings.get_instance().network)
|
||||
lines[3] = lines[3].replace("<density>", Settings.get_instance().qr_density_name)
|
||||
lines[5] = lines[5].replace("<persistent>", 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
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
###
|
||||
|
||||
Reference in New Issue
Block a user