Skip to content

Commit

Permalink
Merge pull request SeedSigner#196 from kdmukai/generic_keyboardscreen
Browse files Browse the repository at this point in the history
Reusable `KeyboardScreen`
  • Loading branch information
newtonick authored May 3, 2022
2 parents 7af24c8 + 8cbe644 commit c809fa4
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 287 deletions.
212 changes: 211 additions & 1 deletion src/seedsigner/gui/screens/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
from dataclasses import dataclass
from PIL import Image, ImageDraw, ImageColor
from typing import Any, List, Tuple
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
from seedsigner.gui.renderer import Renderer

from seedsigner.models.threads import BaseThread
from seedsigner.models.encode_qr import EncodeQR
from seedsigner.models.settings import Settings, SettingsConstants

from ..components import (GUIConstants, BaseComponent, Button, Icon, LargeIconButton, SeedSignerCustomIconConstants, TopNav,
from ..components import (FontAwesomeIconConstants, GUIConstants, BaseComponent, Button, Icon, IconButton, LargeIconButton, SeedSignerCustomIconConstants, TopNav,
TextArea, load_image)

from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons
Expand Down Expand Up @@ -871,3 +872,212 @@ def __post_init__(self):
screen_y=self.top_nav.height,
height=self.canvas_height - self.top_nav.height,
))



@dataclass
class KeyboardScreen(BaseTopNavScreen):
"""
Generalized Screen for a single Keyboard layout writing user input to a
TextEntryDisplay.
Args:
* rows
* cols
* keyboard_font_name
* keyboard_font_size: Specify `None` to auto-size to Key height.
* key_height: Specify `None` to maximize key height to available space.
* keys_charset: Specify the chars displayed on the keys of the keyboard.
* keys_to_values: Optional mapping from key_charset to input value (e.g. dice icon to digit).
* return_after_n_chars: exits and returns the user's input after n characters.
* show_save_button: Render a KEY3 soft button for save & exit
* initial_value: initialize the TextEntryDisplay with an existing string
"""
rows: int = None
cols: int = None
keyboard_font_name: str = GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME
keyboard_font_size: int = GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2
key_height: int = None
keys_charset: str = None
keys_to_values: dict = None
return_after_n_chars: int = None
show_save_button: bool = False
initial_value: str = ""

def __post_init__(self):
super().__post_init__()

if self.initial_value:
self.user_input = self.initial_value
else:
self.user_input = ""

# Set up the keyboard params
if self.show_save_button:
right_panel_buttons_width = 60
hw_button_x = self.canvas_width - right_panel_buttons_width + GUIConstants.COMPONENT_PADDING
hw_button_y = int(self.canvas_height - GUIConstants.BUTTON_HEIGHT) / 2 + 60

self.keyboard_width = self.canvas_width - (GUIConstants.EDGE_PADDING + GUIConstants.COMPONENT_PADDING + right_panel_buttons_width - GUIConstants.COMPONENT_PADDING)

# Render the right button panel (only has a Key3 "Save" button)
self.save_button = IconButton(
icon_name=FontAwesomeIconConstants.SOLID_CIRCLE_CHECK,
icon_color=GUIConstants.SUCCESS_COLOR,
width=right_panel_buttons_width,
screen_x=hw_button_x,
screen_y=hw_button_y,
)
self.components.append(self.save_button)
else:
self.keyboard_width = self.canvas_width - 2*GUIConstants.EDGE_PADDING

text_entry_display_y = self.top_nav.height
text_entry_display_height = 30

keyboard_start_y = text_entry_display_y + text_entry_display_height + GUIConstants.COMPONENT_PADDING
if self.key_height is None:
self.key_height = int((self.canvas_height - GUIConstants.EDGE_PADDING - text_entry_display_y - text_entry_display_height - GUIConstants.COMPONENT_PADDING - (self.rows - 1) * 2) / self.rows)

if self.keyboard_font_size:
font_size = self.keyboard_font_size
else:
# Scale with button height
font_size = self.key_height - GUIConstants.COMPONENT_PADDING

self.keyboard = Keyboard(
draw=self.renderer.draw,
charset=self.keys_charset,
font_name=self.keyboard_font_name,
font_size=font_size,
rows=self.rows,
cols=self.cols,
rect=(
GUIConstants.EDGE_PADDING,
keyboard_start_y,
GUIConstants.EDGE_PADDING + self.keyboard_width,
keyboard_start_y + self.rows * self.key_height + (self.rows - 1) * 2
),
auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT],
render_now=False
)
self.keyboard.set_selected_key(selected_letter=self.keys_charset[0])

self.text_entry_display = TextEntryDisplay(
canvas=self.renderer.canvas,
rect=(
GUIConstants.EDGE_PADDING,
text_entry_display_y,
self.canvas_width - GUIConstants.EDGE_PADDING,
text_entry_display_y + text_entry_display_height
),
cursor_mode=TextEntryDisplay.CURSOR_MODE__BAR,
is_centered=False,
cur_text=self.initial_value,
)


def _render(self):
super()._render()

self.keyboard.render_keys()
self.text_entry_display.render()

self.renderer.show_image()


def _run(self):
self.cursor_position = len(self.user_input)

# Start the interactive update loop
while True:
input = self.hw_inputs.wait_for(
HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3],
check_release=True,
release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3]
)

# Check possible exit conditions
if self.top_nav.is_selected and input == HardwareButtonsConstants.KEY_PRESS:
return RET_CODE__BACK_BUTTON

elif self.show_save_button and input == HardwareButtonsConstants.KEY3:
# Save!
# First show the save button reacting to the click
self.save_button.is_selected = True
self.save_button.render()
self.renderer.show_image()

# Then return the input to the View
if len(self.user_input) > 0:
return self.user_input.strip()

# Process normal input
if input in [HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected:
# We're navigating off the previous button
self.top_nav.is_selected = False
self.top_nav.render_buttons()

# Override the actual input w/an ENTER signal for the Keyboard
if input == HardwareButtonsConstants.KEY_DOWN:
input = Keyboard.ENTER_TOP
else:
input = Keyboard.ENTER_BOTTOM
elif input in [HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT] and self.top_nav.is_selected:
# ignore
continue

ret_val = self.keyboard.update_from_input(input)

# Now process the result from the keyboard
if ret_val in Keyboard.EXIT_DIRECTIONS:
self.top_nav.is_selected = True
self.top_nav.render_buttons()

elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS:
if ret_val == Keyboard.KEY_BACKSPACE["code"]:
if len(self.user_input) > 0:
self.user_input = self.user_input[:-1]
self.cursor_position -= 1

elif input == HardwareButtonsConstants.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS:
# User has locked in the current letter
if self.keys_to_values:
# Map the Key display char to its output value (e.g. dice icon to digit)
ret_val = self.keys_to_values[ret_val]
self.user_input += ret_val
self.cursor_position += 1

if self.cursor_position == self.return_after_n_chars:
return self.user_input

# Render a new TextArea over the TopNav title bar
if self.update_title():
TextArea(
text=self.title,
font_name=GUIConstants.TOP_NAV_TITLE_FONT_NAME,
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE,
height=self.top_nav.height,
).render()
self.top_nav.render_buttons()

elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN:
# Live joystick movement; haven't locked this new letter in yet.
# Leave current spot blank for now. Only update the active keyboard keys
# when a selection has been locked in (KEY_PRESS) or removed ("del").
pass

# Render the text entry display and cursor block
self.text_entry_display.render(self.user_input)

self.renderer.show_image()


def update_title(self) -> bool:
"""
Optionally update the self.title after each completed key input.
e.g. to increment the dice roll count:
self.title = f"Roll {self.cursor_position + 1}"
"""
return False
150 changes: 10 additions & 140 deletions src/seedsigner/gui/screens/seed_screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from seedsigner.models.seed import Seed
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition

from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, WarningEdgesMixin
from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen, WarningEdgesMixin
from ..components import (Button, FontAwesomeIconConstants, Fonts, FormattedAddress, IconButton,
IconTextLine, SeedSignerCustomIconConstants, TextArea, GUIConstants,
calc_text_centering)
Expand Down Expand Up @@ -547,148 +547,18 @@ def __post_init__(self):


@dataclass
class SeedExportXpubCustomDerivationScreen(BaseTopNavScreen):
title: str = "Derivation Path"
derivation_path: str = "m/"

class SeedExportXpubCustomDerivationScreen(KeyboardScreen):
def __post_init__(self):
super().__post_init__()

# Set up the keyboard params
right_panel_buttons_width = 60
hw_button_x = self.canvas_width - right_panel_buttons_width + GUIConstants.COMPONENT_PADDING
hw_button_y = int(self.canvas_height - GUIConstants.BUTTON_HEIGHT) / 2 + 60

keyboard_width = self.canvas_width - (GUIConstants.EDGE_PADDING + GUIConstants.COMPONENT_PADDING + right_panel_buttons_width - GUIConstants.COMPONENT_PADDING)
text_entry_display_y = self.top_nav.height
text_entry_display_height = 30
self.title = "Derivation Path"
self.user_input = "m/"

keyboard_start_y = text_entry_display_y + text_entry_display_height + GUIConstants.COMPONENT_PADDING
rows = 3
self.keyboard_digits = Keyboard(
draw=self.renderer.draw,
charset="/'0123456789",
rows=rows,
cols=6,
rect=(
GUIConstants.EDGE_PADDING,
keyboard_start_y,
GUIConstants.EDGE_PADDING + keyboard_width,
keyboard_start_y + rows * GUIConstants.BUTTON_HEIGHT + (rows - 1) * 2
),
auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT],
render_now=False
)
self.keyboard_digits.set_selected_key(selected_letter="0")

self.text_entry_display = TextEntryDisplay(
canvas=self.renderer.canvas,
rect=(
GUIConstants.EDGE_PADDING,
text_entry_display_y,
self.canvas_width - GUIConstants.EDGE_PADDING,
text_entry_display_y + text_entry_display_height
),
cursor_mode=TextEntryDisplay.CURSOR_MODE__BAR,
is_centered=False,
cur_text=''.join(self.derivation_path)
)

# Render the right button panel (only has a Key3 "Save" button)
self.exit_button = IconButton(
icon_name=FontAwesomeIconConstants.SOLID_CIRCLE_CHECK,
icon_color=GUIConstants.SUCCESS_COLOR,
width=right_panel_buttons_width,
screen_x=hw_button_x,
screen_y=hw_button_y,
)


def _render(self):
super()._render()

self.keyboard_digits.render_keys()

self.exit_button.render()
self.text_entry_display.render(self.derivation_path)
self.renderer.show_image()


def _run(self):
cursor_position = len(self.derivation_path)
# Specify the keys in the keyboard
self.rows = 3
self.cols = 6
self.keys_charset = "/'0123456789"
self.show_save_button = True

# Start the interactive update loop
while True:
input = self.hw_inputs.wait_for(
HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3],
check_release=True,
release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3]
)

# Check our two possible exit conditions
if input == HardwareButtonsConstants.KEY3:
# Save!
self.exit_button.is_selected = True
self.exit_button.render()
self.renderer.show_image()
if len(self.derivation_path) > 0:
return self.derivation_path.strip()

elif self.top_nav.is_selected and input == HardwareButtonsConstants.KEY_PRESS:
# Prev button clicked; return empty string to signal cancel.
return self.top_nav.selected_button

# Process normal input
if input in [HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected:
# We're navigating off the previous button
self.top_nav.is_selected = False
self.top_nav.render_buttons()

# Override the actual input w/an ENTER signal for the Keyboard
if input == HardwareButtonsConstants.KEY_DOWN:
input = Keyboard.ENTER_TOP
else:
input = Keyboard.ENTER_BOTTOM
elif input in [HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT] and self.top_nav.is_selected:
# ignore
continue

ret_val = self.keyboard_digits.update_from_input(input)

# Now process the result from the keyboard
if ret_val in Keyboard.EXIT_DIRECTIONS:
self.top_nav.is_selected = True
self.top_nav.render_buttons()

elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS:
if ret_val == Keyboard.KEY_BACKSPACE["code"]:
if len(self.derivation_path) <= 2:
pass
elif cursor_position == len(self.derivation_path):
self.derivation_path = self.derivation_path[:-1]
cursor_position -= 1
else:
self.derivation_path = self.derivation_path[:cursor_position - 1] + self.derivation_path[cursor_position:]
cursor_position -= 1

elif input == HardwareButtonsConstants.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS:
# User has locked in the current letter
if cursor_position == len(self.derivation_path):
self.derivation_path += ret_val
else:
self.derivation_path = self.derivation_path[:cursor_position] + ret_val + self.derivation_path[cursor_position:]
cursor_position += 1

elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN:
# Live joystick movement; haven't locked this new letter in yet.
# Leave current spot blank for now. Only update the active keyboard keys
# when a selection has been locked in (KEY_PRESS) or removed ("del").
pass

# Render the text entry display and cursor block
self.text_entry_display.render(self.derivation_path)

self.renderer.show_image()
super().__post_init__()



Expand Down
Loading

0 comments on commit c809fa4

Please sign in to comment.