From ab3cb06f4e2a8f72b2deeda369a56672e34c8de9 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Thu, 8 Jul 2021 22:13:23 -0500 Subject: [PATCH] Generalizing reusable components --- src/seedsigner/helpers/__init__.py | 2 +- src/seedsigner/helpers/keyboard.py | 33 ++++++++ src/seedsigner/views/seed_tools_view.py | 107 +++++++++++++----------- src/seedsigner/views/view.py | 22 +++++ tools/seed_phrase_to_qr.py | 19 +++++ 5 files changed, 131 insertions(+), 52 deletions(-) create mode 100644 tools/seed_phrase_to_qr.py diff --git a/src/seedsigner/helpers/__init__.py b/src/seedsigner/helpers/__init__.py index f5ebe908..4120967a 100644 --- a/src/seedsigner/helpers/__init__.py +++ b/src/seedsigner/helpers/__init__.py @@ -1,6 +1,6 @@ from .buttons import * from .camera_process import * -from .keyboard import Keyboard +from .keyboard import Keyboard, TextEntryDisplay from .path import * from .qr import * from .ST7789 import * \ No newline at end of file diff --git a/src/seedsigner/helpers/keyboard.py b/src/seedsigner/helpers/keyboard.py index 680f2fbc..3d62c32e 100644 --- a/src/seedsigner/helpers/keyboard.py +++ b/src/seedsigner/helpers/keyboard.py @@ -341,3 +341,36 @@ class Keyboard: raise Exception(f"""`selected_letter` "{selected_letter}" not found in keyboard""") + +@dataclass +class TextEntryDisplay: + draw: any + rect: (int,int,int,int) + font: any + font_color: any + is_centered: bool = True + cur_text: str = " " + + + def render(self, cur_text=None): + """ Render the live text entry display """ + if cur_text: + self.cur_text = cur_text + self.draw.rectangle(self.rect, fill="black") + + cursor_block_width = 18 + cursor_block_height = 33 + + # Draw n-1 of the selected letters + tw, th = self.font.getsize(self.cur_text[:-1]) + if self.is_centered: + word_offset = int(self.rect[2] - tw - cursor_block_width)/2 + else: + word_offset = 0 + self.draw.text((word_offset, 2), self.cur_text[:-1], fill=self.font_color, font=self.font) + + # Draw the highlighted cursor block + cursor_block_offset = word_offset + tw - 1 + self.draw.rectangle((cursor_block_offset,2, cursor_block_offset + cursor_block_width, cursor_block_height), fill="#111") + self.draw.text((cursor_block_offset + 1, 2), self.cur_text[-1], fill=self.font_color, font=self.font) + diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index a2ef4363..bef96e26 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -7,7 +7,7 @@ import time # Internal file class dependencies from . import View -from seedsigner.helpers import B, QR, CameraPoll, Keyboard +from seedsigner.helpers import B, QR, CameraPoll, Keyboard, TextEntryDisplay class SeedToolsView(View): @@ -198,49 +198,10 @@ class SeedToolsView(View): ) - def render_previous_button(highlight=False): - # Set up the "back" arrow in the upper left - arrow = "<" - word_font = View.ROBOTOCONDENSED_BOLD_26 - tw, th = word_font.getsize(arrow) - if highlight: - View.draw.rectangle((0,0, tw + 6, th + 6), fill=View.color) - View.draw.text((3, 3), arrow, fill="black", font=word_font) - else: - View.draw.rectangle((0,0, tw + 6, th + 6), fill="black") - View.draw.text((3, 3), arrow, fill=View.color, font=word_font) - - return tw + 6 - - - def render_text_entry_display(draw=View.draw): - """ Internal helper method to render the live text entry display at the top. - (has access to all vars in the parent's context) - """ - draw.rectangle((previous_button_width,0, View.canvas_width,text_entry_display_height), fill="black") - - # Render the live text entry display - title = f"#{num_word}: {''.join(self.letters)}" - - cursor_block_width = 18 - cursor_block_height = 33 - - # Draw n-1 of the selected letters - word_font = View.ROBOTOCONDENSED_BOLD_26 - tw, th = word_font.getsize(title[:-1]) - word_offset = int(View.canvas_width - tw - cursor_block_width)/2 - draw.text((word_offset, 2), title[:-1], fill=View.color, font=word_font) - - # Draw the highlighted cursor block - cursor_block_offset = word_offset + tw - 1 - draw.rectangle((cursor_block_offset,2, cursor_block_offset + cursor_block_width, cursor_block_height), fill="#111") - draw.text((cursor_block_offset + 1, 2), ''.join(self.letters[-1:]), fill=View.color, font=word_font) - - # Clear the screen View.draw.rectangle((0,0, View.canvas_width,View.canvas_height), fill="black") - previous_button_width = render_previous_button() + self.render_previous_button() previous_button_is_active = False arrow_up_is_active = False arrow_down_is_active = False @@ -261,8 +222,18 @@ class SeedToolsView(View): rect=(0,text_entry_display_height + 1, keyboard_width,240), auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT]) - # Initialize the current letters/current matches + # Render the top text entry display self.letters = initial_letters + text_entry_display = TextEntryDisplay( + View.draw, + rect=(self.previous_button_width,0, View.canvas_width,text_entry_display_height), + font=View.ROBOTOCONDENSED_BOLD_26, + font_color=View.color, + cur_text="".join(self.letters) + ) + text_entry_display.render() + + # Initialize the current matches self.possible_words = [] if len(self.letters) > 1: self.letters.append(" ") # "Lock in" the last letter as if KEY_PRESS @@ -274,9 +245,6 @@ class SeedToolsView(View): keyboard.render_keys() render_possible_matches() - # Render the top text entry display - render_text_entry_display() - View.DispShowImage() # Start the interactive update loop @@ -295,13 +263,13 @@ class SeedToolsView(View): input = Keyboard.ENTER_BOTTOM # Re-render it without the highlight previous_button_is_active = False - render_previous_button() + self.render_previous_button() elif input == B.KEY_DOWN: input = Keyboard.ENTER_TOP # Re-render it without the highlight previous_button_is_active = False - render_previous_button() + self.render_previous_button() elif input in [B.KEY_RIGHT, B.KEY_LEFT]: # no action in this context @@ -310,7 +278,7 @@ class SeedToolsView(View): ret_val = keyboard.update_from_input(input) if ret_val in Keyboard.EXIT_DIRECTIONS: - render_previous_button(highlight=True) + self.render_previous_button(highlight=True) previous_button_is_active = True elif ret_val in Keyboard.ADDITIONAL_KEYS: @@ -378,7 +346,7 @@ class SeedToolsView(View): # Animate the selection storage, then return the word to the caller self.letters = list(final_selection + " ") render_possible_matches(highlight_word=final_selection) - render_text_entry_display() + text_entry_display.render("".join(self.letters)) View.DispShowImage() return final_selection @@ -433,11 +401,48 @@ class SeedToolsView(View): render_possible_matches() # Render the text entry display and cursor block - render_text_entry_display() + text_entry_display.render("".join(self.letters)) View.DispShowImage() + def draw_passphrase_keyboard_entry(self): + + # Clear the screen + View.draw.rectangle((0,0, View.canvas_width,View.canvas_height), fill="black") + + self.render_previous_button() + previous_button_is_active = False + + # Have to ensure that we don't carry any effects from a previous run + # TODO: This shouldn't be a member var + self.passphrase = " " + + # Set up the keyboard params + right_key_nav_width = 50 + keyboard_width = View.canvas_width - right_key_nav_width + text_entry_display_height = 39 + + keyboard = Keyboard(View.draw, + charset=list("1234567890").append(SeedToolsView.ALPHABET), + rows=4, + cols=10, + rect=(0,text_entry_display_height + 1, keyboard_width,240), + auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT, Keyboard.WRAP_TOP, Keyboard.WRAP_BOTTOM]) + + # Render the top text entry display + text_entry_display = TextEntryDisplay( + View.draw, + rect=(self.previous_button_width,0, View.canvas_width,text_entry_display_height), + font=View.ROBOTOCONDENSED_BOLD_26, + font_color=View.color, + is_centered=False, + cur_text=''.join(self.passphrase) + ) + text_entry_display.render() + + View.DispShowImage() + def gather_words_up(self): View.draw.polygon([(8 + ((len(self.letters)-1)*30), 85) , (14 + ((len(self.letters)-1)*30), 69) , (20 + ((len(self.letters)-1)*30), 85 )], outline=View.color, fill=View.color) View.DispShowImage() @@ -551,10 +556,10 @@ class SeedToolsView(View): return + ### ### Display Gather PassPhrase Screen ### - def display_gather_passphrase_screen(self, slot_num = 0) -> str: self.reset() self.pass_letter = "a" diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index aa864e0b..c5065055 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -64,6 +64,7 @@ class View: bus = 0 device = 0 disp = None + previous_button_width = None def __init__(self, controller) -> None: @@ -259,3 +260,24 @@ class View: def display_blank_screen(self): View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) View.DispShowImage() + + + ### + ### Reusable components + ### + def render_previous_button(self, highlight=False): + # Set up the "back" arrow in the upper left + arrow = "<" + word_font = View.ROBOTOCONDENSED_BOLD_26 + padding = 3 + tw, th = word_font.getsize(arrow) + self.previous_button_width = tw + 2 * padding + if highlight: + View.draw.rectangle((0,0, tw + 2 * padding, th + 2 * padding), fill=View.color) + View.draw.text((padding, padding), arrow, fill="black", font=word_font) + else: + View.draw.rectangle((0,0, tw + 2 * padding, th + 2 * padding), fill="black") + View.draw.text((padding, padding), arrow, fill=View.color, font=word_font) + + + diff --git a/tools/seed_phrase_to_qr.py b/tools/seed_phrase_to_qr.py new file mode 100644 index 00000000..0c818c75 --- /dev/null +++ b/tools/seed_phrase_to_qr.py @@ -0,0 +1,19 @@ + +if __name__ == "__main__": + import qrcode + import sys + from embit import bip39 + + print(sys.argv) + seed_phrase = sys.argv[1:] + print(seed_phrase) + + data = "" + for word in seed_phrase: + index = bip39.WORDLIST.index(word) + data += str("%04d" % index) + + qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=3) + qr.add_data(data) + qr.make(fit=True) + qr.make_image(fill_color="black", back_color="white").resize((240,240)).convert('RGB').show()