mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 23:18:25 +00:00
view_function is now View_cls; first CBVs
Button text v-centering
This commit is contained in:
+68
-212
@@ -9,10 +9,6 @@ from binascii import hexlify
|
||||
from threading import Thread
|
||||
|
||||
# Internal file class dependencies
|
||||
from .views import (View, MenuView, SeedToolsView, SigningToolsView,
|
||||
SettingsToolsView, IOTestView, OpeningSplashView, ScreensaverView)
|
||||
from .gui import Renderer
|
||||
from .helpers import Buttons, B, Path
|
||||
from .models import (EncodeQRDensity, QRType, Seed, SeedStorage, Settings, ConfigurableSingleton, DecodeQR, DecodeQRStatus, EncodeQR, PSBTParser)
|
||||
|
||||
|
||||
@@ -39,6 +35,10 @@ class Controller(ConfigurableSingleton):
|
||||
|
||||
@classmethod
|
||||
def configure_instance(cls, config=None):
|
||||
from .gui import Renderer
|
||||
from .helpers import Buttons
|
||||
from .views import ScreensaverView
|
||||
|
||||
super().configure_instance(config)
|
||||
|
||||
# Instantiate the one and only Controller instance
|
||||
@@ -64,11 +64,11 @@ class Controller(ConfigurableSingleton):
|
||||
controller.renderer = Renderer.get_instance()
|
||||
|
||||
# Views
|
||||
controller.menu_view = MenuView()
|
||||
controller.seed_tools_view = SeedToolsView()
|
||||
controller.io_test_view = IOTestView()
|
||||
controller.signing_tools_view = SigningToolsView(controller.storage)
|
||||
controller.settings_tools_view = SettingsToolsView()
|
||||
# controller.menu_view = MenuView()
|
||||
# controller.seed_tools_view = SeedToolsView()
|
||||
# controller.io_test_view = IOTestView()
|
||||
# controller.signing_tools_view = SigningToolsView(controller.storage)
|
||||
# controller.settings_tools_view = SettingsToolsView()
|
||||
controller.screensaver = ScreensaverView(controller.buttons)
|
||||
|
||||
controller.back_stack = []
|
||||
@@ -90,241 +90,97 @@ class Controller(ConfigurableSingleton):
|
||||
|
||||
|
||||
def start(self) -> None:
|
||||
from .views import OpeningSplashView, MainMenuView
|
||||
|
||||
opening_splash = OpeningSplashView()
|
||||
# opening_splash.start()
|
||||
|
||||
""" Functions can be stored as variables in python!
|
||||
""" Class references 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.
|
||||
This loop receives a View class to execute and stores it in the `View_cls`
|
||||
var along with any input arguments in the `run_args` dict.
|
||||
|
||||
The `view_function` is executed w/its `function_args`. Each view returns either
|
||||
a new view function to execute next or None.
|
||||
The `View_cls` is instantiated and run with `run_args` passed in. It returns
|
||||
either a new View class to execute next or None.
|
||||
|
||||
Example:
|
||||
def my_view(some_arg, other_arg):
|
||||
print(other_arg)
|
||||
class MyView(View)
|
||||
def run(self, some_arg, other_arg):
|
||||
print(other_arg)
|
||||
|
||||
def other_view():
|
||||
return (my_view, {"some_arg": 1, "other_arg": "hello"})
|
||||
class OtherView():
|
||||
def run(self):
|
||||
return (MyView, {"some_arg": 1, "other_arg": "hello"})
|
||||
|
||||
When `other_view` is run, we capture its return values:
|
||||
When `OtherView` is instantiated and run, we capture its return values:
|
||||
|
||||
(view_function, function_args) = other_view()
|
||||
(View_cls, run_args) = OtherView().run()
|
||||
|
||||
And then we can execute that view function:
|
||||
And then we can instantiate and run that View class:
|
||||
|
||||
view_function(**function_args)
|
||||
View_cls().run(**run_args)
|
||||
"""
|
||||
try:
|
||||
view_function = None
|
||||
function_args = {}
|
||||
View_cls = None
|
||||
run_args = {}
|
||||
while True:
|
||||
if not view_function:
|
||||
if not View_cls or View_cls == MainMenuView:
|
||||
# None is a special case; render the Home screen
|
||||
view_function = self.menu_view.display_main_menu
|
||||
function_args = {}
|
||||
View_cls = MainMenuView
|
||||
run_args = {}
|
||||
|
||||
# Home always wipes the back_stack
|
||||
self.back_stack = []
|
||||
|
||||
print(f"Executing {view_function}")
|
||||
print(f"Executing {View_cls.__name__}")
|
||||
|
||||
# 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)
|
||||
# Instantiate the `View_cls` and run() it with the `run_args` dict
|
||||
result = View_cls().run(**run_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()
|
||||
if type(result) is tuple:
|
||||
View_cls = result[0]
|
||||
run_args = result[1]
|
||||
else:
|
||||
self.back_stack.append((view_function, function_args))
|
||||
View_cls = result
|
||||
run_args = {}
|
||||
|
||||
print(f"View_cls: {View_cls.__name__ if View_cls else 'None'}")
|
||||
|
||||
if View_cls == self.pop_back_stack:
|
||||
# "Back" arrow was clicked; load the previous view
|
||||
(View_cls, run_args) = self.pop_back_stack()
|
||||
else:
|
||||
self.back_stack.append((View_cls, run_args))
|
||||
|
||||
finally:
|
||||
# Clear the screen when exiting
|
||||
self.renderer.display_blank_screen()
|
||||
|
||||
# if self.DEBUG:
|
||||
# # Let Exceptions halt execution
|
||||
# try:
|
||||
# self.show_main_menu()
|
||||
# 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
|
||||
|
||||
# tests = [
|
||||
# ("OpenSans-SemiBold", 16),
|
||||
# ("OpenSans-SemiBold", 18),
|
||||
# ("OpenSans-SemiBold", 20),
|
||||
# ]
|
||||
|
||||
# screens = []
|
||||
|
||||
# screens.append(
|
||||
# BottomButtonScreen(
|
||||
# title="Supersampling: off",
|
||||
# button_data=[{"text":"Proceed"}],
|
||||
# is_button_text_centered=False,
|
||||
# title_font=Fonts.get_font("OpenSans-SemiBold", 18),
|
||||
# body_text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
# is_body_text_centered=True,
|
||||
# body_font_name="OpenSans-Regular",
|
||||
# body_font_size=17,
|
||||
# body_font_color="white",
|
||||
# button_font=Fonts.get_font("OpenSans-SemiBold", 18),
|
||||
# supersampling_factor=1
|
||||
# )
|
||||
# )
|
||||
# screens.append(
|
||||
# BottomButtonScreen(
|
||||
# title="Supersampling: 2",
|
||||
# button_data=[{"text":"Proceed"}],
|
||||
# is_button_text_centered=False,
|
||||
# title_font=Fonts.get_font("OpenSans-SemiBold", 18),
|
||||
# body_text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
# is_body_text_centered=True,
|
||||
# body_font_name="OpenSans-Regular",
|
||||
# body_font_size=17,
|
||||
# body_font_color="white",
|
||||
# button_font=Fonts.get_font("OpenSans-SemiBold", 18),
|
||||
# supersampling_factor=2
|
||||
# )
|
||||
# )
|
||||
# screens.append(
|
||||
# BottomButtonScreen(
|
||||
# title="Regular 16px w/SS",
|
||||
# button_data=[{"text":"Proceed"}],
|
||||
# is_button_text_centered=False,
|
||||
# title_font=Fonts.get_font("OpenSans-SemiBold", 18),
|
||||
# body_text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
# is_body_text_centered=True,
|
||||
# body_font_name="OpenSans-Regular",
|
||||
# body_font_size=16,
|
||||
# body_font_color="white",
|
||||
# button_font=Fonts.get_font("OpenSans-SemiBold", 18),
|
||||
# supersampling_factor=2
|
||||
# )
|
||||
# )
|
||||
# screens.append(
|
||||
# BottomButtonScreen(
|
||||
# title="SemiBold 16px, no SS",
|
||||
# button_data=[{"text":"Proceed"}],
|
||||
# is_button_text_centered=False,
|
||||
# title_font=Fonts.get_font("OpenSans-SemiBold", 18),
|
||||
# body_text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
# is_body_text_centered=True,
|
||||
# body_font_name="OpenSans-SemiBold",
|
||||
# body_font_size=16,
|
||||
# body_font_color="white",
|
||||
# button_font=Fonts.get_font("OpenSans-SemiBold", 18),
|
||||
# supersampling_factor=1
|
||||
# )
|
||||
# )
|
||||
# screens.append(
|
||||
# BottomButtonScreen(
|
||||
# title="SemiBold 17px, no SS",
|
||||
# button_data=[{"text":"Proceed"}],
|
||||
# is_button_text_centered=False,
|
||||
# title_font=Fonts.get_font("OpenSans-SemiBold", 18),
|
||||
# body_text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
# is_body_text_centered=True,
|
||||
# body_font_name="OpenSans-SemiBold",
|
||||
# body_font_size=17,
|
||||
# body_font_color="white",
|
||||
# button_font=Fonts.get_font("OpenSans-SemiBold", 18),
|
||||
# supersampling_factor=1
|
||||
# )
|
||||
# )
|
||||
# screens.append(
|
||||
# ButtonListScreen(
|
||||
# title="Color: Orange",
|
||||
# button_data=[
|
||||
# {"text": "cfd0883d"},
|
||||
# {"text": "72f9a6bf"},
|
||||
# ],
|
||||
# is_button_text_centered=False,
|
||||
# is_bottom_list=False,
|
||||
# title_font=Fonts.get_font("OpenSans-SemiBold", 18),
|
||||
# button_font=Fonts.get_font("OpenSans-SemiBold", 18),
|
||||
# button_selected_color="orange"
|
||||
# )
|
||||
# )
|
||||
# screens.append(
|
||||
# ButtonListScreen(
|
||||
# title="Color: #f7941d",
|
||||
# button_data=[
|
||||
# {"text": "cfd0883d"},
|
||||
# {"text": "72f9a6bf"},
|
||||
# ],
|
||||
# is_button_text_centered=False,
|
||||
# is_bottom_list=False,
|
||||
# title_font=Fonts.get_font("OpenSans-SemiBold", 18),
|
||||
# button_font=Fonts.get_font("OpenSans-SemiBold", 18),
|
||||
# button_selected_color="#f7941d"
|
||||
# )
|
||||
# )
|
||||
|
||||
|
||||
# index = 0
|
||||
# while True:
|
||||
# # test = tests[index]
|
||||
# # screen = FontTesterScreen(
|
||||
# # title="In-Memory Seeds",
|
||||
# # button_data=[
|
||||
# # {"text": "cfd0883d"},
|
||||
# # {"text": "72f9a6bf"},
|
||||
# # {"text": f"{test[0].split('.')[0]} {test[1]}"},
|
||||
# # ],
|
||||
# # is_button_text_centered=False,
|
||||
# # is_bottom_list=False,
|
||||
# # )
|
||||
|
||||
# screens[index].render()
|
||||
|
||||
# else:
|
||||
# # Handle Unexpected crashes by restarting up to 3 times
|
||||
# crash_cnt = 0
|
||||
# while True:
|
||||
# if index == 0:
|
||||
# input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_UP, B.KEY_DOWN])
|
||||
# elif index < len(screens) - 1:
|
||||
# input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN])
|
||||
# else:
|
||||
# input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN])
|
||||
# try:
|
||||
# self.show_main_menu()
|
||||
# except Exception as error:
|
||||
# if crash_cnt >= 3:
|
||||
# break
|
||||
# else:
|
||||
# print('Caught this error: ' + repr(error)) # debug
|
||||
# self.renderer.draw_modal(["Crashed ..."], "", "restarting")
|
||||
# time.sleep(5)
|
||||
|
||||
# if input == B.KEY_RIGHT:
|
||||
# index += 1
|
||||
# break
|
||||
# elif input == B.KEY_LEFT:
|
||||
# index -= 1
|
||||
# break
|
||||
# else:
|
||||
# screens[index].update_from_input(input)
|
||||
# crash_cnt += 1
|
||||
|
||||
# time.sleep(180)
|
||||
|
||||
if self.DEBUG:
|
||||
# Let Exceptions halt execution
|
||||
try:
|
||||
self.show_main_menu()
|
||||
finally:
|
||||
# Clear the screen when exiting
|
||||
self.renderer.display_blank_screen()
|
||||
|
||||
else:
|
||||
# Handle Unexpected crashes by restarting up to 3 times
|
||||
crash_cnt = 0
|
||||
while True:
|
||||
try:
|
||||
self.show_main_menu()
|
||||
except Exception as error:
|
||||
if crash_cnt >= 3:
|
||||
break
|
||||
else:
|
||||
print('Caught this error: ' + repr(error)) # debug
|
||||
self.renderer.draw_modal(["Crashed ..."], "", "restarting")
|
||||
time.sleep(5)
|
||||
|
||||
crash_cnt += 1
|
||||
|
||||
self.renderer.draw_modal(["Crashed ..."], "", "requires hard restart")
|
||||
# self.renderer.draw_modal(["Crashed ..."], "", "requires hard restart")
|
||||
|
||||
|
||||
def start_screensaver(self):
|
||||
|
||||
@@ -63,8 +63,8 @@ class TextArea(BaseComponent):
|
||||
height: int
|
||||
background_color: str = "black"
|
||||
font_name: str = "OpenSans-Regular"
|
||||
font_size: int = 16
|
||||
font_color: str = "orange"
|
||||
font_size: int = 17
|
||||
font_color: str = "white"
|
||||
is_text_centered: bool = True
|
||||
supersampling_factor: int = None
|
||||
|
||||
@@ -182,14 +182,23 @@ class Button(BaseComponent):
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if not self.font:
|
||||
self.font = Fonts.get_font("OpenSans-SemiBold", 24)
|
||||
self.font = Fonts.get_font("OpenSans-SemiBold", 18)
|
||||
|
||||
# see: https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html#text-anchors
|
||||
offset_x, offset_y = self.font.getoffset(self.text)
|
||||
(box_left, box_top, box_right, box_bottom) = self.font.getbbox(self.text, anchor='lt')
|
||||
ascent, descent = self.font.getmetrics()
|
||||
|
||||
# print(f"----- {self.text} -----")
|
||||
# print(f"offset_x, offset_y: ({offset_x}, {offset_y})")
|
||||
# print(f"(box_left, box_top, box_right, box_bottom): ({box_left}, {box_top}, {box_right}, {box_bottom})")
|
||||
# print(f"ascent, descent: ({ascent}, {descent})")
|
||||
|
||||
self.text_width, self.text_height = self.font.getsize(self.text)
|
||||
if self.is_text_centered:
|
||||
self.text_x = self.screen_x + int((self.width - self.text_width) / 2)
|
||||
self.text_x = self.screen_x + int((self.width - (box_right - offset_x)) / 2) - offset_x
|
||||
else:
|
||||
self.text_x = self.screen_x + COMPONENT_PADDING
|
||||
self.text_y = self.screen_y + int((self.height - self.text_height) / 2)
|
||||
self.text_y = self.screen_y + int((self.height - (ascent - offset_y)) / 2) - offset_y
|
||||
|
||||
|
||||
def render(self):
|
||||
@@ -211,7 +220,7 @@ class TopNav(BaseComponent):
|
||||
width: int
|
||||
height: int
|
||||
background_color: str = "black"
|
||||
font: ImageFont = Fonts.get_font("OpenSans-SemiBold", 18)
|
||||
font: ImageFont = Fonts.get_font("OpenSans-SemiBold", 19)
|
||||
font_color: str = "white"
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from .components import (EDGE_PADDING, COMPONENT_PADDING, Button, TopNav,
|
||||
from dataclasses import dataclass
|
||||
from PIL import ImageFont
|
||||
from seedsigner.helpers import B, Buttons
|
||||
from seedsigner.views import View
|
||||
|
||||
|
||||
|
||||
@@ -21,18 +20,23 @@ class BaseScreen:
|
||||
self.renderer = Renderer.get_instance()
|
||||
self.hw_inputs = Buttons.get_instance()
|
||||
|
||||
self.canvas_width = self.renderer.canvas_width
|
||||
self.canvas_height = self.renderer.canvas_height
|
||||
|
||||
def render(self, show_image: bool = True):
|
||||
|
||||
def display(self):
|
||||
self._render()
|
||||
self.renderer.show_image()
|
||||
|
||||
return self._run()
|
||||
|
||||
|
||||
def _render(self):
|
||||
# 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()
|
||||
self.renderer.draw.rectangle((0, 0, self.canvas_width, self.canvas_height), fill=0)
|
||||
|
||||
|
||||
def run(self):
|
||||
def _run(self):
|
||||
"""
|
||||
Screen can run on its own until it returns a final exit input from the user.
|
||||
|
||||
@@ -64,18 +68,56 @@ class BaseTopNavScreen(BaseScreen):
|
||||
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
|
||||
width=self.canvas_width,
|
||||
height=(2 * EDGE_PADDING) + int(self.canvas_width * 2.0 / 15.0), # 32px on a 240x240 screen
|
||||
)
|
||||
|
||||
|
||||
def render(self, show_image: bool = True):
|
||||
super().render(show_image=False) # Child always tells parent to wait
|
||||
|
||||
def _render(self):
|
||||
super()._render()
|
||||
self.top_nav.render()
|
||||
|
||||
if show_image:
|
||||
self.renderer.show_image()
|
||||
|
||||
def _run(self):
|
||||
raise Exception("Must implement in a child class")
|
||||
|
||||
|
||||
@dataclass
|
||||
class TextTopNavScreen(BaseTopNavScreen):
|
||||
title: str # Not necessary to include this since it's in parent; just here for clarity
|
||||
text: str
|
||||
is_text_centered: bool = True
|
||||
text_font_name: str = "OpenSans-Regular"
|
||||
text_font_size: int = 17
|
||||
supersampling_factor: int = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.text_font_size < 18 and not self.supersampling_factor or self.supersampling_factor == 1:
|
||||
self.supersampling_factor = 2
|
||||
|
||||
self.text_area = TextArea(
|
||||
text=self.text,
|
||||
screen_x=0,
|
||||
screen_y=self.top_nav.height,
|
||||
width=self.canvas_width,
|
||||
height=self.canvas_height - self.top_nav.height,
|
||||
font_name=self.text_font_name,
|
||||
font_size=self.text_font_size,
|
||||
is_text_centered=self.is_text_centered,
|
||||
supersampling_factor=self.supersampling_factor
|
||||
)
|
||||
|
||||
|
||||
def _render(self):
|
||||
super()._render()
|
||||
self.top_nav.render()
|
||||
self.text_area.render()
|
||||
|
||||
|
||||
def _run(self):
|
||||
# No interactive UI to handle
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -92,16 +134,20 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
button_height = int(self.renderer.canvas_height * 3.0 / 20.0) # 36px on a 240x240 screen
|
||||
button_height = int(self.canvas_height * 3.0 / 20.0) # 36px on a 240x240 screen
|
||||
if len(self.button_labels) == 1:
|
||||
button_list_height = button_height
|
||||
else:
|
||||
button_list_height = (len(self.button_labels) * button_height) + (COMPONENT_PADDING * (len(self.button_labels) - 1))
|
||||
|
||||
if self.is_bottom_list:
|
||||
button_list_y = self.renderer.canvas_height - (button_list_height + EDGE_PADDING)
|
||||
button_list_y = self.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)
|
||||
button_list_y = self.top_nav.height + int((self.canvas_height - self.top_nav.height - button_list_height) / 2)
|
||||
|
||||
if button_list_y < self.top_nav.height:
|
||||
# The button list is too long; force it to run off the bottom of the screen.
|
||||
button_list_y = self.top_nav.height + COMPONENT_PADDING
|
||||
|
||||
self.buttons = []
|
||||
for i, button_label in enumerate(self.button_labels):
|
||||
@@ -109,7 +155,7 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
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),
|
||||
width=self.canvas_width - (2 * EDGE_PADDING),
|
||||
height=button_height,
|
||||
is_text_centered=self.is_button_text_centered,
|
||||
font=self.button_font,
|
||||
@@ -121,16 +167,13 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
self.selected_button = 0
|
||||
|
||||
|
||||
def render(self, show_image: bool = True):
|
||||
super().render(show_image=False) # Child always tells parent to wait
|
||||
|
||||
def _render(self):
|
||||
super()._render()
|
||||
for button in self.buttons:
|
||||
button.render()
|
||||
|
||||
self.renderer.show_image()
|
||||
|
||||
|
||||
def run(self):
|
||||
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:
|
||||
@@ -189,7 +232,7 @@ class BottomButtonScreen(ButtonListScreen):
|
||||
text=body_text,
|
||||
screen_x=0,
|
||||
screen_y=self.top_nav.height,
|
||||
width=self.renderer.canvas_width,
|
||||
width=self.canvas_width,
|
||||
height=self.buttons[0].screen_y - self.top_nav.height,
|
||||
font_name=body_font_name,
|
||||
font_size=body_font_size,
|
||||
@@ -199,8 +242,8 @@ class BottomButtonScreen(ButtonListScreen):
|
||||
)
|
||||
|
||||
|
||||
def render(self):
|
||||
self.renderer.draw.rectangle((0, 0, self.renderer.canvas_width, self.renderer.canvas_height), fill=0)
|
||||
def _render(self):
|
||||
self.renderer.draw.rectangle((0, 0, self.canvas_width, self.canvas_height), fill=0)
|
||||
self.top_nav.render()
|
||||
self.body_textscreen.render()
|
||||
for button in self.buttons:
|
||||
@@ -1,3 +1,6 @@
|
||||
# Must import crucial base class first!
|
||||
from .singleton import Singleton, ConfigurableSingleton
|
||||
|
||||
from .seed import *
|
||||
from .qr_type import *
|
||||
from .encode_qr_density import *
|
||||
@@ -6,4 +9,3 @@ from .encode_qr import *
|
||||
from .psbt_parser import *
|
||||
from .seed_storage import *
|
||||
from .settings import *
|
||||
from .singleton import *
|
||||
@@ -1,4 +1,5 @@
|
||||
from .view import * # base class has to be first
|
||||
|
||||
from .io_test_view import *
|
||||
from .menu_view import *
|
||||
from .seed_tools_view import *
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from . import View
|
||||
|
||||
from seedsigner.gui.components import Fonts
|
||||
from seedsigner.gui.templates import ButtonListScreen
|
||||
from seedsigner.gui.screens import ButtonListScreen
|
||||
from seedsigner.helpers import B, Path, Buttons
|
||||
from seedsigner.models import SeedStorage, Settings, Seed
|
||||
|
||||
@@ -11,6 +11,46 @@ import time
|
||||
import re
|
||||
|
||||
|
||||
|
||||
class SeedToolsMenuView(View):
|
||||
def run(self, **kwargs):
|
||||
from . import MainMenuView
|
||||
|
||||
title = "Store a Seed"
|
||||
if self.controller.storage.num_of_saved_seeds() > 0:
|
||||
if self.controller.storage.num_of_saved_seeds() < 3:
|
||||
title = "View/Store Seeds"
|
||||
else:
|
||||
title = "View Seeds"
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
title=title,
|
||||
button_labels=["... [ Return to Main ]",
|
||||
"Temp Seed Storage",
|
||||
"Seed Passphrase",
|
||||
"Export xPub",
|
||||
"Calc Last Word",
|
||||
"New Seed w/Dice",
|
||||
"New Seed w/Image"]
|
||||
).display()
|
||||
|
||||
if selected_menu_num == 0:
|
||||
print("returning MainMenuView")
|
||||
return (MainMenuView, {})
|
||||
elif selected_menu_num == 1:
|
||||
return Path.SAVE_SEED
|
||||
elif selected_menu_num == 2:
|
||||
return Path.PASSPHRASE_SEED
|
||||
elif selected_menu_num == 3:
|
||||
return Path.GEN_XPUB
|
||||
elif selected_menu_num == 4:
|
||||
return Path.GEN_LAST_WORD
|
||||
elif selected_menu_num == 5:
|
||||
return Path.DICE_GEN_SEED
|
||||
elif selected_menu_num == 6:
|
||||
return Path.IMAGE_GEN_SEED
|
||||
|
||||
|
||||
class MenuView(View):
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -20,83 +60,6 @@ class MenuView(View):
|
||||
self.selected_menu_num = 1
|
||||
|
||||
|
||||
###
|
||||
### Main Navigation
|
||||
###
|
||||
|
||||
### Main Menu
|
||||
def display_main_menu(self, sub_menu = None) -> int:
|
||||
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()
|
||||
|
||||
menu_screen = ButtonListScreen(title="SeedSigner", button_labels=button_labels, selected_button=0)
|
||||
menu_screen.render()
|
||||
selected_menu_num = menu_screen.run()
|
||||
|
||||
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
|
||||
|
||||
|
||||
### 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:
|
||||
if self.controller.storage.num_of_saved_seeds() < 3:
|
||||
seed_storage_line = "View/Store Seeds (temp)"
|
||||
else:
|
||||
seed_storage_line = "View Seeds (temp)"
|
||||
|
||||
lines = ["... [ Return to Main ]", "Temp Seed Storage", "Seed Passphrase", "xPub from Seed", "Calculate Last Word", "Generate Seed with Dice", "Generate Seed with Image"]
|
||||
self.draw_menu(lines, 1)
|
||||
input = 0
|
||||
|
||||
# Wait for Button Input (specifically menu selection/press)
|
||||
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()
|
||||
elif input == B.KEY_DOWN:
|
||||
self.menu_down()
|
||||
elif input == B.KEY_PRESS:
|
||||
if self.selected_menu_num == 1:
|
||||
return Path.MAIN_MENU
|
||||
elif self.selected_menu_num == 2:
|
||||
return Path.SAVE_SEED
|
||||
elif self.selected_menu_num == 3:
|
||||
return Path.PASSPHRASE_SEED
|
||||
elif self.selected_menu_num == 4:
|
||||
return Path.GEN_XPUB
|
||||
elif self.selected_menu_num == 5:
|
||||
return Path.GEN_LAST_WORD
|
||||
elif self.selected_menu_num == 6:
|
||||
return Path.DICE_GEN_SEED
|
||||
elif self.selected_menu_num == 7:
|
||||
return Path.IMAGE_GEN_SEED
|
||||
|
||||
|
||||
### Signing Tools Menu
|
||||
def display_signing_tools_menu(self) -> None:
|
||||
lines = ["... [ Return to Main ]", "Generate xPub", "Sign a Transaction"]
|
||||
|
||||
@@ -4,7 +4,7 @@ import time
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from . import View
|
||||
from .view import View
|
||||
|
||||
from seedsigner.helpers import B
|
||||
from seedsigner.gui.components import Fonts
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
# External Dependencies
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import os
|
||||
import pathlib
|
||||
import time
|
||||
|
||||
from multiprocessing import Queue
|
||||
from seedsigner.helpers import B
|
||||
from seedsigner.gui.components import Fonts
|
||||
from seedsigner.gui.screens import ButtonListScreen, TextTopNavScreen
|
||||
|
||||
|
||||
"""
|
||||
Views contain the biz logic to handle discrete tasks, exactly analogous to a Flask
|
||||
request/response function or a Django View. Each page/screen displayed to the user
|
||||
should be implemented in its own View.
|
||||
|
||||
In a web context, the View would prepare data for the html/css/js presentation
|
||||
templates. We have to implement our own presentation layer (implemented as `Screen`
|
||||
objects). For the sake of code cleanliness and separation of concerns, the View code
|
||||
should not know anything about pixel-level rendering.
|
||||
|
||||
Sequences that require multiple pages/screens can be should be implemented as a
|
||||
series of separate Views. Exceptions can be made for complex interactive sequences,
|
||||
but in general, if your View is instantiating multiple Screens, you're probably
|
||||
putting too much functionality in that View.
|
||||
|
||||
As with http requests, Views can receive input vars to inform their behavior. Views
|
||||
can also prepare the next set of vars to set up the next View that should be
|
||||
displayed (akin to `return redirect(url, param1=x, param2=y))`).
|
||||
|
||||
"""
|
||||
class View:
|
||||
# TODO: Obviated by seedsigner.gui.components.TopNav
|
||||
previous_button_width: int = None
|
||||
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Import here to avoid circular imports
|
||||
from seedsigner.controller import Controller
|
||||
@@ -29,37 +43,17 @@ class View:
|
||||
self.buttons = self.controller.buttons
|
||||
self.color = self.controller.color
|
||||
|
||||
self.queue = Queue()
|
||||
|
||||
def run(self, **kwargs):
|
||||
raise Exception("Must implement in the child class")
|
||||
|
||||
|
||||
|
||||
###
|
||||
### Power Off Screen
|
||||
###
|
||||
|
||||
def display_power_off_screen(self):
|
||||
|
||||
self.renderer.draw.rectangle((0, 0, self.canvas_width, self.canvas_height), outline=0, fill=0)
|
||||
|
||||
line1 = "Powering Down..."
|
||||
line2 = "Please wait about"
|
||||
line3 = "30 seconds before"
|
||||
line4 = "disconnecting power."
|
||||
|
||||
tw, th = self.renderer.draw.textsize(line1, font=Fonts.get_font("Assistant-Medium", 22))
|
||||
self.renderer.draw.text(((240-tw)/2, 45), line1, fill=self.color, font=Fonts.get_font("Assistant-Medium", 22))
|
||||
tw, th = self.renderer.draw.textsize(line2, font=Fonts.get_font("Assistant-Medium", 20))
|
||||
self.renderer.draw.text(((240-tw)/2, 100), line2, fill=self.color, font=Fonts.get_font("Assistant-Medium", 20))
|
||||
tw, th = self.renderer.draw.textsize(line3, font=Fonts.get_font("Assistant-Medium", 20))
|
||||
self.renderer.draw.text(((240-tw)/2, 130), line3, fill=self.color, font=Fonts.get_font("Assistant-Medium", 20))
|
||||
tw, th = self.renderer.draw.textsize(line4, font=Fonts.get_font("Assistant-Medium", 20))
|
||||
self.renderer.draw.text(((240-tw)/2, 160), line4, fill=self.color, font=Fonts.get_font("Assistant-Medium", 20))
|
||||
self.renderer.show_image()
|
||||
|
||||
|
||||
###
|
||||
### Reusable components
|
||||
###
|
||||
# TODO: Obviated by seedsigner.gui.components.TopNav
|
||||
def render_previous_button(self, highlight=False):
|
||||
# Set up the "back" arrow in the upper left
|
||||
arrow = "<"
|
||||
@@ -80,3 +74,42 @@ class View:
|
||||
|
||||
|
||||
|
||||
#########################################################################################
|
||||
#
|
||||
# Root level Views don't have a sub-module home so they live at the top level here.
|
||||
#
|
||||
#########################################################################################
|
||||
class MainMenuView(View):
|
||||
def run(self):
|
||||
from .menu_view import SeedToolsMenuView
|
||||
selected_menu_num = ButtonListScreen(
|
||||
title="SeedSigner",
|
||||
button_labels=["Seed Tools",
|
||||
"Scan QR",
|
||||
"Settings",
|
||||
"Power Off"]
|
||||
).display()
|
||||
|
||||
if selected_menu_num == 0:
|
||||
return SeedToolsMenuView
|
||||
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:
|
||||
return PowerOffView
|
||||
|
||||
|
||||
|
||||
class PowerOffView(View):
|
||||
def run(self):
|
||||
from subprocess import call
|
||||
TextTopNavScreen(
|
||||
title="Powering Down",
|
||||
text="Please wait about 30 seconds before disconnecting power.",
|
||||
text_font_size=22
|
||||
).display()
|
||||
|
||||
# call("sudo shutdown --poweroff now", shell=True)
|
||||
time.sleep(10)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user