diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 8c2dc76a..4e812161 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -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): diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 9bda3e33..b8d4abc6 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -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" diff --git a/src/seedsigner/gui/templates.py b/src/seedsigner/gui/screens.py similarity index 72% rename from src/seedsigner/gui/templates.py rename to src/seedsigner/gui/screens.py index e27d9318..2d59ea03 100644 --- a/src/seedsigner/gui/templates.py +++ b/src/seedsigner/gui/screens.py @@ -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: diff --git a/src/seedsigner/models/__init__.py b/src/seedsigner/models/__init__.py index 03520dea..c4843c31 100644 --- a/src/seedsigner/models/__init__.py +++ b/src/seedsigner/models/__init__.py @@ -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 * \ No newline at end of file diff --git a/src/seedsigner/views/__init__.py b/src/seedsigner/views/__init__.py index bcf195a7..f664238f 100644 --- a/src/seedsigner/views/__init__.py +++ b/src/seedsigner/views/__init__.py @@ -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 * diff --git a/src/seedsigner/views/menu_view.py b/src/seedsigner/views/menu_view.py index 9a53c198..e9f91fc2 100644 --- a/src/seedsigner/views/menu_view.py +++ b/src/seedsigner/views/menu_view.py @@ -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"] diff --git a/src/seedsigner/views/screensaver.py b/src/seedsigner/views/screensaver.py index d3ae3efc..7260fd2e 100644 --- a/src/seedsigner/views/screensaver.py +++ b/src/seedsigner/views/screensaver.py @@ -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 diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 02c5763c..0ff263b2 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -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) +