Interim commit

* Scrolling enabled for TopNav titles, Button, LargeIconButton.
* Button label text can be different between Active and Inactive states.
* Syncing to latest updates in screenshots submodule.
* Update to how child Component threads get managed by parent Screen.
This commit is contained in:
kdmukai
2024-11-12 13:50:36 -06:00
parent 80ea2c8b5e
commit 917a5783b0
6 changed files with 433 additions and 240 deletions
+252 -67
View File
@@ -379,6 +379,7 @@ class TextArea(BaseComponent):
height: int = None # None = special case: autosize to min height
screen_x: int = 0
screen_y: int = 0
scroll_y: int = 0
min_text_x: int = 0 # Text can not start at x any less than this
background_color: str = GUIConstants.BACKGROUND_COLOR
font_name: str = None
@@ -386,10 +387,13 @@ class TextArea(BaseComponent):
font_color: str = GUIConstants.BODY_FONT_COLOR
edge_padding: int = GUIConstants.EDGE_PADDING
is_text_centered: bool = True
supersampling_factor: int = 1
supersampling_factor: int = 2 # 1 = disabled; 2 = default, double sample (4px square rendered for 1px)
auto_line_break: bool = True
allow_text_overflow: bool = False
is_horizontal_scrolling_enabled: bool = False
horizontal_scroll_speed: int = 40 # px per sec
horizontal_scroll_begin_hold_secs: float = 2.0
horizontal_scroll_end_hold_secs: float = 1.0
height_ignores_below_baseline: bool = False # If True, characters that render below the baseline (e.g. "pqgy") will not affect the final height calculation
@@ -499,8 +503,15 @@ class TextArea(BaseComponent):
# Add a `resample_padding` above and below when supersampling to avoid edge
# effects (resized text that's right up against the top/bottom gets slightly
# dimmer at the edge otherwise).
if self.font_size < 20 and (not self.supersampling_factor or self.supersampling_factor == 1):
self.supersampling_factor = 2
# if self.background_color == GUIConstants.ACCENT_COLOR and self.supersampling_factor == 1:
# # Don't boost supersampling factor. Text against the accent color does not
# # render well when supersampled.
# pass
# elif self.font_size < 20 and (not self.supersampling_factor or self.supersampling_factor == 1):
# self.supersampling_factor = 2
if self.font_size >= 20 and self.supersampling_factor != 1:
self.supersampling_factor = 1
logger.warning(f"Supersampling disabled for large font size: {self.font_size}")
if self.height_ignores_below_baseline:
# Even though we're ignoring the pixels below the baseline for spacing
@@ -508,14 +519,21 @@ class TextArea(BaseComponent):
# supersampling operations here.
total_text_height += self.text_height_below_baseline
resample_padding = 10 if self.supersampling_factor > 1.0 else 0
resample_padding = 10 if self.supersampling_factor > 1 else 0
if self.is_horizontal_scrolling_enabled:
# Temp img will be the full width of the text
image_width = self.text_width
else:
# Temp img will be the component's width
image_width = self.width
# Temp img will be the component's width, but must respect right edge padding
image_width = self.width - self.edge_padding
if self.supersampling_factor > 1:
start = time.time()
supersampled_font = Fonts.get_font(self.font_name, int(self.supersampling_factor * self.font_size))
print(f"Supersampled font load time: {time.time() - start:.04}")
else:
supersampled_font = font
img = Image.new(
"RGBA",
@@ -524,11 +542,11 @@ class TextArea(BaseComponent):
(total_text_height + 2*resample_padding) * self.supersampling_factor
),
self.background_color
# "red"
)
draw = ImageDraw.Draw(img)
cur_y = (resample_padding + self.text_height_above_baseline) * self.supersampling_factor
supersampled_font = Fonts.get_font(self.font_name, int(self.supersampling_factor * self.font_size))
if self.is_text_centered:
# middle baseline
@@ -561,7 +579,7 @@ class TextArea(BaseComponent):
cur_y += (self.text_height_above_baseline + self.line_spacing) * self.supersampling_factor
# Crop off the top_padding and resize the result down to onscreen size
if self.supersampling_factor > 1.0:
if self.supersampling_factor > 1:
resized = img.resize((image_width, total_text_height + 2*resample_padding), Image.LANCZOS)
sharpened = resized.filter(ImageFilter.SHARPEN)
@@ -569,28 +587,48 @@ class TextArea(BaseComponent):
self.rendered_text_img = img
self.horizontal_text_scroll_thread: TextArea.HorizontalTextScrollThread = None
if self.is_horizontal_scrolling_enabled:
# Start the horizontal scrolling renderer thread
self.threads.append(
TextArea.HorizontalTextScrollThread(
rendered_text_img=self.rendered_text_img,
screen_x=self.min_text_x,
screen_y=self.screen_y + self.text_offset_y,
visible_width=self.visible_width
)
self.horizontal_text_scroll_thread = TextArea.HorizontalTextScrollThread(
rendered_text_img=self.rendered_text_img,
screen_x=self.screen_x + self.min_text_x,
screen_y=self.screen_y + self.text_y - self.text_height_above_baseline,
visible_width=self.visible_width,
horizontal_scroll_speed=self.horizontal_scroll_speed,
begin_hold_secs=self.horizontal_scroll_begin_hold_secs,
end_hold_secs=self.horizontal_scroll_end_hold_secs
)
class HorizontalTextScrollThread(BaseThread):
def __init__(self, rendered_text_img: Image, screen_x: int, screen_y: int, visible_width: int):
def __init__(self, rendered_text_img: Image, screen_x: int, screen_y: int, visible_width: int, horizontal_scroll_speed:int, begin_hold_secs: float, end_hold_secs: float):
super().__init__()
self.rendered_text_img = rendered_text_img
self.screen_x = screen_x
self.screen_y = screen_y
self.visible_width = visible_width
self.horizontal_scroll_speed = horizontal_scroll_speed
self.begin_hold_secs = begin_hold_secs
self.end_hold_secs = end_hold_secs
self.scroll_y = 0
self.scrolling_active = True
self.horizontal_scroll_position = 0
self.scroll_increment_sign = 1 # flip to negative to scroll text to the right
self.renderer = Renderer.get_instance()
def stop_scrolling(self):
self.scrolling_active = False
def start_scrolling(self):
# Reset scroll position to left edge
self.horizontal_scroll_position = 0
self.scroll_increment_sign = 1
self.scrolling_active = True
self.renderer = Renderer.get_instance()
def run(self):
"""
@@ -598,39 +636,37 @@ class TextArea(BaseComponent):
50px/sec creates a slight ghosting / doubling effect that impedes
readability. 45px/sec is better but still perceptually a bit stuttery.
"""
horizontal_scroll_position = 0
scroll_pace = 45 # px per sec
scroll_increment_sign = 1 # flip to negative to scroll text to the right
begin_hold_secs = 2.0
end_hold_secs = 1.0
max_scroll = self.rendered_text_img.width - self.visible_width
while self.keep_running:
if not self.scrolling_active:
time.sleep(0.1)
continue
with self.renderer.lock:
img = self.rendered_text_img.crop((horizontal_scroll_position, 0, horizontal_scroll_position + self.visible_width, self.rendered_text_img.height))
self.renderer.canvas.paste(img, (self.screen_x, self.screen_y))
img = self.rendered_text_img.crop((self.horizontal_scroll_position, 0, self.horizontal_scroll_position + self.visible_width, self.rendered_text_img.height))
self.renderer.canvas.paste(img, (self.screen_x, self.screen_y - self.scroll_y))
self.renderer.show_image()
if horizontal_scroll_position == 0:
if self.horizontal_scroll_position == 0:
# Pause on initial (left-justified) position...
time.sleep(begin_hold_secs)
time.sleep(self.begin_hold_secs)
# Don't count those pause seconds
last_render_time = None
# Scroll the text left
scroll_increment_sign = 1
self.scroll_increment_sign = 1
elif horizontal_scroll_position == max_scroll:
elif self.horizontal_scroll_position == max_scroll:
# ...and slight pause at end of scroll
time.sleep(end_hold_secs)
time.sleep(self.end_hold_secs)
# Don't count those pause seconds
last_render_time = None
# Scroll the text right
scroll_increment_sign = -1
self.scroll_increment_sign = -1
else:
# No need to CPU limit when running in its own thread?
@@ -641,13 +677,13 @@ class TextArea(BaseComponent):
if not last_render_time:
# First frame when pulling off either end will move 1 pixel; have to
# "get off zero" for the real increment calc logic to kick in.
scroll_position_increment = 1 * scroll_increment_sign
scroll_position_increment = 1 * self.scroll_increment_sign
else:
scroll_position_increment = int(scroll_pace * (next_render_time - last_render_time) * scroll_increment_sign)
scroll_position_increment = int(self.horizontal_scroll_speed * (next_render_time - last_render_time) * self.scroll_increment_sign)
if abs(scroll_position_increment) > 0:
horizontal_scroll_position += scroll_position_increment
horizontal_scroll_position = max(0, min(horizontal_scroll_position, max_scroll))
self.horizontal_scroll_position += scroll_position_increment
self.horizontal_scroll_position = max(0, min(self.horizontal_scroll_position, max_scroll))
last_render_time = next_render_time
else:
@@ -667,12 +703,19 @@ class TextArea(BaseComponent):
text_img = self.rendered_text_img
if self.is_horizontal_scrolling_enabled:
# Scrolling text has no edge considerations so must be placed exactly
text_x = self.min_text_x
text_x += self.min_text_x
# Must also account for the right edge running off our visible width
text_img = text_img.crop((0, 0, self.visible_width, text_img.height))
self.canvas.paste(text_img, (text_x, self.screen_y + self.text_offset_y))
self.canvas.paste(text_img, (text_x, self.screen_y + self.text_y - self.text_height_above_baseline - self.scroll_y))
def set_scroll_y(self, scroll_y: int):
""" Used by ButtonListScreen """
self.scroll_y = scroll_y
if self.horizontal_text_scroll_thread:
self.horizontal_text_scroll_thread.scroll_y = scroll_y
@@ -691,6 +734,16 @@ class ScrollableTextLine(TextArea):
super().__post_init__()
@property
def needs_scroll(self) -> bool:
return self.horizontal_text_scroll_thread is not None
@property
def scroll_thread(self) -> TextArea.HorizontalTextScrollThread:
return self.horizontal_text_scroll_thread
@dataclass
class Icon(BaseComponent):
@@ -1251,12 +1304,29 @@ class BtcAmount(BaseComponent):
@dataclass
class Button(BaseComponent):
# TODO: Rename the seedsigner.helpers.Buttons class (to Inputs?) to reduce confusion
# with this GUI component.
"""
Attrs with defaults must be listed last.
Buttons offer two rendering methods:
* Reusable in-memory image (is_scrollable_text = True; default): For both active and
inactive states, the text is rendered once (on a just-in-time basis) into an
in-memory image that is then reused as needed during the life of the Component.
Specifically built with l10n in mind. Will automatically add scrolling via
ScrollableTextLine for the Button's active state when necessary; a static
TextArea is used otherwise.
This means that this setting is not suitable for Buttons whose
text label needs to interactively change (e.g. the "ABC" vs "abc" soft keys in
the passphrase entry Keyboard).
* Real-time text (is_scrollable_text = False): The label text's active/inactive state
is just rendered as basic text on-the-fly, so it can support uses where the button
label can change. Text scrolling is not supported in this mode so in general it
should not to used with l10n content whose length might vary by language.
"""
text: str = "Button Label"
active_text: str = None # Optional alt text to replace the button label when the button is selected
screen_x: int = 0
screen_y: int = 0
scroll_y: int = 0
@@ -1282,6 +1352,7 @@ class Button(BaseComponent):
selected_outline_color: str = None
is_text_centered: bool = True
is_selected: bool = False
is_scrollable_text: bool = True # True: active state will automatically scroll if necessary, text is rendered once (not dynamic)
def __post_init__(self):
@@ -1302,16 +1373,11 @@ class Button(BaseComponent):
if not self.icon_color:
self.icon_color = GUIConstants.BUTTON_FONT_COLOR
self.font = Fonts.get_font(self.font_name, self.font_size)
self.active_button_label = None
if self.text is not None:
if self.is_text_centered:
self.text_x = int(self.width/2)
self.text_anchor = "ms" # centered horizontally, baseline
else:
self.text_x = GUIConstants.COMPONENT_PADDING
self.text_anchor = "ls" # left, baseline
self.font = Fonts.get_font(self.font_name, self.font_size)
# Calc true pixel height (any anchor from "baseline" will work)
(left, top, self.text_width, bottom) = self.font.getbbox(self.text, anchor="ls")
# print(f"left: {left} | top: {top} | right: {self.text_width} | bottom: {bottom}")
@@ -1321,25 +1387,65 @@ class Button(BaseComponent):
# regardless of the Button text.
self.text_height = -1 * top
# TODO: Only apply screen_y at render
# Total space available just for the text (will contract later if there are icons)
self.visible_text_width = self.width - 2*GUIConstants.COMPONENT_PADDING
if self.text_width > self.visible_text_width and not self.is_scrollable_text:
logger.warning("Button label \"{self.text}\" will not fit but is_scrollable_text is False")
if self.is_text_centered and self.text_width < self.visible_text_width:
# self.text_x = int(self.width/2)
# self.text_anchor = "ms" # centered horizontally, baseline
# Calculate the centered text's starting point, but relative to the "ls"
# anchor point.
self.text_x = int((self.width - self.text_width)/2)
self.text_anchor = "ls" # left, baseline
else:
# Text is left-justified or has to be because it will be scrolled
self.is_text_centered = False
self.text_x = GUIConstants.COMPONENT_PADDING
self.text_anchor = "ls" # left, baseline
if self.text_y_offset:
self.text_y = self.text_y_offset + self.text_height
else:
self.text_y = self.height - int((self.height - self.text_height)/2)
print(f"Button: {self.text_y_offset=}, {self.text_height=}, {self.height=}, {self.text_y=} | {self.text=}")
# Preload the icon and its "_selected" variant
icon_padding = GUIConstants.COMPONENT_PADDING
if self.icon_name:
icon_padding = GUIConstants.COMPONENT_PADDING
self.icon = Icon(icon_name=self.icon_name, icon_size=self.icon_size, icon_color=self.icon_color)
self.icon_selected = Icon(icon_name=self.icon_name, icon_size=self.icon_size, icon_color=self.selected_icon_color)
if self.icon_y_offset:
self.icon_y = self.icon_y_offset
else:
self.icon_y = math.ceil((self.height - self.icon.height)/2)
if self.is_icon_inline:
self.visible_text_width -= self.icon.width + icon_padding
if self.text_width > self.visible_text_width:
self.is_text_centered = False
self.text_x = GUIConstants.COMPONENT_PADDING
if not self.is_scrollable_text:
logger.warning("Button label \"{self.text}\" with icon inline will not fit but is_scrollable_text is False")
if self.is_text_centered:
# Shift the text's centering
if self.text:
# Shift the text's center-based anchor to the right to make room
# self.text_x += int((self.icon.width + icon_padding) / 2)
# Shift the text's "ls"-based anchor to the right to make room
self.text_x += int((self.icon.width + icon_padding) / 2)
self.icon_x = self.text_x - int(self.text_width/2) - (self.icon.width + icon_padding)
# Position the icon's left-based anchor on the left
self.icon_x = self.text_x - (self.icon.width + icon_padding)
else:
# TODO: Is an inline icon but w/no text even a sensible input configuration?
self.icon_x = math.ceil((self.width - self.icon.width)/2)
else:
@@ -1349,20 +1455,62 @@ class Button(BaseComponent):
else:
self.icon_x = int((self.width - self.icon.width) / 2)
if self.icon_y_offset:
self.icon_y = self.icon_y_offset
else:
self.icon_y = math.ceil((self.height - self.icon.height)/2)
if self.text:
self.text_y = self.icon_y + self.icon.height + GUIConstants.COMPONENT_PADDING
if self.right_icon_name:
self.right_icon = Icon(icon_name=self.right_icon_name, icon_size=self.right_icon_size, icon_color=self.right_icon_color)
self.right_icon_selected = Icon(icon_name=self.right_icon_name, icon_size=self.right_icon_size, icon_color=self.selected_icon_color)
self.visible_text_width -= self.right_icon.width + icon_padding
if self.text_width > self.visible_text_width:
self.is_text_centered = False
if not self.is_scrollable_text:
logger.warning("Button label \"{self.text}\" with icon inline will not fit but is_scrollable_text is False")
self.right_icon_x = self.width - self.right_icon.width - GUIConstants.COMPONENT_PADDING
self.right_icon_y = math.ceil((self.height - self.right_icon.height)/2)
if self.text and self.is_scrollable_text:
button_kwargs = dict(
text=self.active_text if self.active_text else self.text,
font_name=self.font_name,
font_size=self.font_size,
supersampling_factor=1, # disable; not necessary at button font size. Also black text on orange supersamples poorly
font_color=self.selected_font_color,
background_color=self.selected_color,
screen_x=self.screen_x,
screen_y=self.screen_y + self.text_y_offset,
width=self.width,
height=self.text_height if self.icon_name and not self.is_icon_inline else self.height,
min_text_x=self.text_x if self.icon_name and self.is_icon_inline else GUIConstants.COMPONENT_PADDING,
is_text_centered=self.is_text_centered,
height_ignores_below_baseline=True, # Consistently vertically center text, ignoring chars that render below baseline (e.g. "pqyj")
horizontal_scroll_speed=30, #px per sec
horizontal_scroll_begin_hold_secs=0.5,
horizontal_scroll_end_hold_secs=0.5,
)
# ButtonListScreens with lots of buttons will take too long to pre-render all
# the Buttons, so we use a just-in-time approach to create BOTH the active and
# inactive Buttons. For simple "Done" screens, the inactive state will never be
# rendered.
self.active_button_label = None
self.active_button_label_kwargs = button_kwargs.copy()
button_kwargs["text"] = self.text
button_kwargs["font_color"] = self.font_color
button_kwargs["background_color"] = self.background_color
button_kwargs["allow_text_overflow"] = True
button_kwargs["auto_line_break"] = False
del button_kwargs["horizontal_scroll_begin_hold_secs"]
del button_kwargs["horizontal_scroll_end_hold_secs"]
self.inactive_button_label = None
self.inactive_button_label_kwargs = button_kwargs.copy()
def render(self):
if self.is_selected:
@@ -1388,13 +1536,44 @@ class Button(BaseComponent):
)
if self.text is not None:
self.image_draw.text(
(self.screen_x + self.text_x, self.screen_y + self.text_y - self.scroll_y),
self.text,
fill=font_color,
font=self.font,
anchor=self.text_anchor
)
if not self.is_scrollable_text:
# Just directly render the text for the current active/inactive state
self.image_draw.text(
(self.screen_x + self.text_x, self.screen_y + self.text_y - self.scroll_y),
self.text,
fill=font_color,
font=self.font,
anchor=self.text_anchor
)
else:
# Use just-in-time instatiation of pre-rendered ScrollableTextLine and TextArea
if self.is_selected:
if not self.active_button_label:
# Just-in-time create the active button label
self.active_button_label = ScrollableTextLine(**self.active_button_label_kwargs)
if self.active_button_label.needs_scroll:
self.threads.append(self.active_button_label.scroll_thread)
self.active_button_label.scroll_thread.start()
self.active_button_label.set_scroll_y(self.scroll_y)
self.active_button_label.render()
if self.active_button_label.needs_scroll:
# Activate the scrollable text line
self.active_button_label.scroll_thread.start_scrolling()
else:
if self.active_button_label and self.active_button_label.needs_scroll:
self.active_button_label.scroll_thread.stop_scrolling()
if not self.inactive_button_label:
# Just-in-time create the inactive button label
self.inactive_button_label = TextArea(**self.inactive_button_label_kwargs)
self.inactive_button_label.set_scroll_y(self.scroll_y)
self.inactive_button_label.render()
if self.icon_name:
icon = self.icon
@@ -1457,6 +1636,7 @@ class IconButton(Button):
text: str = None
is_icon_inline: bool = False
is_text_centered: bool = True
is_scrollable_text: bool = False
@@ -1468,6 +1648,7 @@ class LargeIconButton(IconButton):
"""
icon_size: int = GUIConstants.ICON_LARGE_BUTTON_SIZE
icon_y_offset: int = GUIConstants.COMPONENT_PADDING
is_scrollable_text: bool = True
@@ -1525,6 +1706,7 @@ class TopNav(BaseComponent):
min_text_x = self.left_button.screen_x + self.left_button.width + GUIConstants.COMPONENT_PADDING
if self.icon_name:
# TODO: Refactor IconTextLine to use ScrollableTextLine
self.title = IconTextLine(
screen_x=0,
screen_y=0,
@@ -1550,7 +1732,10 @@ class TopNav(BaseComponent):
font_size=self.font_size,
height_ignores_below_baseline=True, # Consistently vertically center text, ignoring chars that render below baseline (e.g. "pqyj")
)
self.threads += self.title.threads
if self.title.needs_scroll:
# Add the scroll thread to TopNav's self.threads so it automatically runs
# for the life of the Component.
self.threads.append(self.title.scroll_thread)
@property
+26 -27
View File
@@ -44,27 +44,31 @@ class BaseScreen(BaseComponent):
# Tracks position on scrollable pages, determines which elements are visible.
self.scroll_y = 0
def get_threads(self) -> List[BaseThread]:
threads = self.threads.copy()
for component in self.components:
threads += component.threads
return threads
def display(self) -> Any:
# each Component could have its own child threads; collect them first
for component in self.components:
self.threads += component.threads
try:
with self.renderer.lock:
self._render()
self.renderer.show_image()
for t in self.threads:
t.start()
for t in self.get_threads():
if not t.is_alive():
t.start()
return self._run()
except Exception as e:
repr(e)
raise e
finally:
for t in self.threads:
for t in self.get_threads():
t.stop()
@@ -268,6 +272,7 @@ class ButtonOption:
right_icon_name: str = None
button_label_color: str = None
return_data: Any = None
active_button_label: str = None # Changes displayed button label when button is active
@@ -326,29 +331,14 @@ class ButtonListScreen(BaseTopNavScreen):
icon_color = button_option.icon_color
right_icon_name = button_option.right_icon_name
button_label_color = button_option.button_label_color
active_button_label = button_option.active_button_label
else:
raise Exception("Refactor needed!")
# # TODO: Complete refactor away from str|tuple to ButtonOption
# elif type(button_option) == str:
# button_label = button_option
# elif type(button_option) == tuple:
# if len(button_option) == 2:
# (button_label, icon_name) = button_option
# icon_color = GUIConstants.BUTTON_FONT_COLOR
# elif len(button_option) == 3:
# (button_label, icon_name, icon_color) = button_option
# elif len(button_option) == 4:
# (button_label, icon_name, icon_color, button_label_color) = button_option
# elif len(button_option) == 5:
# (button_label, icon_name, icon_color, button_label_color, right_icon_name) = button_option
button_kwargs = dict(
text=_(button_label), # Wrap here for just-in-time translations
active_text=_(active_button_label), # Wrap here for just-in-time translations
icon_name=icon_name,
icon_color=icon_color if icon_color else GUIConstants.BUTTON_FONT_COLOR,
is_icon_inline=True,
@@ -362,13 +352,14 @@ class ButtonListScreen(BaseTopNavScreen):
font_name=self.button_font_name,
font_size=self.button_font_size,
font_color=button_label_color if button_label_color else GUIConstants.BUTTON_FONT_COLOR,
selected_color=self.button_selected_color
selected_color=self.button_selected_color,
is_scrollable_text=True, # We need to use the ScrollableText class for long button labels
)
if self.checked_buttons and i in self.checked_buttons:
button_kwargs["is_checked"] = True
button = self.Button_cls(**button_kwargs)
self.buttons.append(button)
if self.has_scroll_arrows:
self.arrow_half_width = 10
self.cur_scroll_y = self.scroll_y_initial_offset if self.scroll_y_initial_offset is not None else 0
@@ -389,6 +380,14 @@ class ButtonListScreen(BaseTopNavScreen):
cur_selected_button.is_selected = True
def get_threads(self) -> List[BaseThread]:
threads = super().get_threads()
for button in self.buttons:
if button.is_scrollable_text:
threads += button.threads
return threads
def _render(self):
super()._render()
self._render_visible_buttons()
@@ -550,7 +549,7 @@ class ButtonListScreen(BaseTopNavScreen):
@dataclass
class LargeButtonScreen(BaseTopNavScreen):
button_data: list = None # list can be a mix of str or tuple(label: str, icon_name: str)
button_data: list = None
button_font_name: str = None
button_font_size: int = None
button_selected_color: str = GUIConstants.ACCENT_COLOR
+147 -141
View File
@@ -91,6 +91,7 @@ class SeedMnemonicEntryScreen(BaseTopNavScreen):
screen_y=self.highlighted_row_y,
width=self.canvas_width - self.matches_list_x + GUIConstants.COMPONENT_PADDING,
height=int(0.75*GUIConstants.BUTTON_HEIGHT),
is_scrollable_text=False,
)
arrow_button_width = GUIConstants.BUTTON_HEIGHT + GUIConstants.EDGE_PADDING
@@ -815,6 +816,7 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
width=self.right_panel_buttons_width,
screen_x=hw_button_x,
screen_y=hw_button_y - 3*GUIConstants.COMPONENT_PADDING - GUIConstants.BUTTON_HEIGHT,
is_scrollable_text=False,
)
self.hw_button2 = Button(
@@ -825,6 +827,7 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
width=self.right_panel_buttons_width,
screen_x=hw_button_x,
screen_y=hw_button_y,
is_scrollable_text=False,
)
self.hw_button3 = IconButton(
@@ -833,6 +836,7 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
width=self.right_panel_buttons_width,
screen_x=hw_button_x,
screen_y=hw_button_y + 3*GUIConstants.COMPONENT_PADDING + GUIConstants.BUTTON_HEIGHT,
is_scrollable_text=False,
)
@@ -865,169 +869,171 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
keyboard_swap = False
# Check our two possible exit conditions
# TODO: note the unusual return value, consider refactoring to a Response object in the future
if input == HardwareButtonsConstants.KEY3:
# Save!
# First light up key3
self.hw_button3.is_selected = True
self.hw_button3.render()
self.renderer.show_image()
return dict(passphrase=self.passphrase)
with self.renderer.lock:
# Check our two possible exit conditions
# TODO: note the unusual return value, consider refactoring to a Response object in the future
if input == HardwareButtonsConstants.KEY3:
# Save!
# First light up key3
self.hw_button3.is_selected = True
self.hw_button3.render()
self.renderer.show_image()
return dict(passphrase=self.passphrase)
elif input == HardwareButtonsConstants.KEY_PRESS and self.top_nav.is_selected:
# Back button clicked
return dict(passphrase=self.passphrase, is_back_button=True)
elif input == HardwareButtonsConstants.KEY_PRESS and self.top_nav.is_selected:
# Back button clicked
return dict(passphrase=self.passphrase, is_back_button=True)
# Check for keyboard swaps
if input == HardwareButtonsConstants.KEY1:
# First light up key1
self.hw_button1.is_selected = True
self.hw_button1.render()
# Check for keyboard swaps
if input == HardwareButtonsConstants.KEY1:
# First light up key1
self.hw_button1.is_selected = True
self.hw_button1.render()
# Return to the same button2 keyboard, if applicable
if cur_keyboard == self.keyboard_digits:
cur_button2_text = self.KEYBOARD__DIGITS_BUTTON_TEXT
elif cur_keyboard == self.keyboard_symbols_1:
cur_button2_text = self.KEYBOARD__SYMBOLS_1_BUTTON_TEXT
elif cur_keyboard == self.keyboard_symbols_2:
cur_button2_text = self.KEYBOARD__SYMBOLS_2_BUTTON_TEXT
# Return to the same button2 keyboard, if applicable
if cur_keyboard == self.keyboard_digits:
cur_button2_text = self.KEYBOARD__DIGITS_BUTTON_TEXT
elif cur_keyboard == self.keyboard_symbols_1:
cur_button2_text = self.KEYBOARD__SYMBOLS_1_BUTTON_TEXT
elif cur_keyboard == self.keyboard_symbols_2:
cur_button2_text = self.KEYBOARD__SYMBOLS_2_BUTTON_TEXT
if cur_button1_text == self.KEYBOARD__LOWERCASE_BUTTON_TEXT:
self.keyboard_abc.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"])
cur_keyboard = self.keyboard_abc
cur_button1_text = self.KEYBOARD__UPPERCASE_BUTTON_TEXT
else:
self.keyboard_ABC.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"])
cur_keyboard = self.keyboard_ABC
cur_button1_text = self.KEYBOARD__LOWERCASE_BUTTON_TEXT
cur_keyboard.render_keys()
# Show the changes; this loop will have two renders
self.renderer.show_image()
keyboard_swap = True
ret_val = None
elif input == HardwareButtonsConstants.KEY2:
# First light up key2
self.hw_button2.is_selected = True
self.hw_button2.render()
self.renderer.show_image()
# And reset for next redraw
self.hw_button2.is_selected = False
# Return to the same button1 keyboard, if applicable
if cur_keyboard == self.keyboard_abc:
cur_button1_text = self.KEYBOARD__LOWERCASE_BUTTON_TEXT
elif cur_keyboard == self.keyboard_ABC:
cur_button1_text = self.KEYBOARD__UPPERCASE_BUTTON_TEXT
if cur_button2_text == self.KEYBOARD__DIGITS_BUTTON_TEXT:
self.keyboard_digits.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"])
cur_keyboard = self.keyboard_digits
cur_keyboard.render_keys()
cur_button2_text = self.KEYBOARD__SYMBOLS_1_BUTTON_TEXT
elif cur_button2_text == self.KEYBOARD__SYMBOLS_1_BUTTON_TEXT:
self.keyboard_symbols_1.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"])
cur_keyboard = self.keyboard_symbols_1
cur_keyboard.render_keys()
cur_button2_text = self.KEYBOARD__SYMBOLS_2_BUTTON_TEXT
elif cur_button2_text == self.KEYBOARD__SYMBOLS_2_BUTTON_TEXT:
self.keyboard_symbols_2.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"])
cur_keyboard = self.keyboard_symbols_2
cur_keyboard.render_keys()
cur_button2_text = self.KEYBOARD__DIGITS_BUTTON_TEXT
cur_keyboard.render_keys()
# Show the changes; this loop will have two renders
self.renderer.show_image()
keyboard_swap = True
ret_val = None
if cur_button1_text == self.KEYBOARD__LOWERCASE_BUTTON_TEXT:
self.keyboard_abc.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"])
cur_keyboard = self.keyboard_abc
cur_button1_text = self.KEYBOARD__UPPERCASE_BUTTON_TEXT
else:
self.keyboard_ABC.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"])
cur_keyboard = self.keyboard_ABC
cur_button1_text = self.KEYBOARD__LOWERCASE_BUTTON_TEXT
cur_keyboard.render_keys()
# 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()
# Show the changes; this loop will have two renders
self.renderer.show_image()
# 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
keyboard_swap = True
ret_val = None
ret_val = cur_keyboard.update_from_input(input)
elif input == HardwareButtonsConstants.KEY2:
# First light up key2
self.hw_button2.is_selected = True
self.hw_button2.render()
self.renderer.show_image()
# And reset for next redraw
self.hw_button2.is_selected = False
# Return to the same button1 keyboard, if applicable
if cur_keyboard == self.keyboard_abc:
cur_button1_text = self.KEYBOARD__LOWERCASE_BUTTON_TEXT
elif cur_keyboard == self.keyboard_ABC:
cur_button1_text = self.KEYBOARD__UPPERCASE_BUTTON_TEXT
if cur_button2_text == self.KEYBOARD__DIGITS_BUTTON_TEXT:
self.keyboard_digits.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"])
cur_keyboard = self.keyboard_digits
cur_keyboard.render_keys()
cur_button2_text = self.KEYBOARD__SYMBOLS_1_BUTTON_TEXT
elif cur_button2_text == self.KEYBOARD__SYMBOLS_1_BUTTON_TEXT:
self.keyboard_symbols_1.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"])
cur_keyboard = self.keyboard_symbols_1
cur_keyboard.render_keys()
cur_button2_text = self.KEYBOARD__SYMBOLS_2_BUTTON_TEXT
elif cur_button2_text == self.KEYBOARD__SYMBOLS_2_BUTTON_TEXT:
self.keyboard_symbols_2.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"])
cur_keyboard = self.keyboard_symbols_2
cur_keyboard.render_keys()
cur_button2_text = self.KEYBOARD__DIGITS_BUTTON_TEXT
cur_keyboard.render_keys()
# Show the changes; this loop will have two renders
self.renderer.show_image()
keyboard_swap = True
ret_val = None
else:
# 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
# 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()
# 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
elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS:
if ret_val == Keyboard.KEY_BACKSPACE["code"]:
if cursor_position == 0:
pass
elif cursor_position == len(self.passphrase):
self.passphrase = self.passphrase[:-1]
else:
self.passphrase = self.passphrase[:cursor_position - 1] + self.passphrase[cursor_position:]
ret_val = cur_keyboard.update_from_input(input)
cursor_position -= 1
# 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 == Keyboard.KEY_CURSOR_LEFT["code"]:
cursor_position -= 1
if cursor_position < 0:
cursor_position = 0
elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS:
if ret_val == Keyboard.KEY_BACKSPACE["code"]:
if cursor_position == 0:
pass
elif cursor_position == len(self.passphrase):
self.passphrase = self.passphrase[:-1]
else:
self.passphrase = self.passphrase[:cursor_position - 1] + self.passphrase[cursor_position:]
elif ret_val == Keyboard.KEY_CURSOR_RIGHT["code"]:
cursor_position += 1
if cursor_position > len(self.passphrase):
cursor_position = len(self.passphrase)
cursor_position -= 1
elif ret_val == Keyboard.KEY_SPACE["code"]:
if cursor_position == len(self.passphrase):
self.passphrase += " "
else:
self.passphrase = self.passphrase[:cursor_position] + " " + self.passphrase[cursor_position:]
cursor_position += 1
elif ret_val == Keyboard.KEY_CURSOR_LEFT["code"]:
cursor_position -= 1
if cursor_position < 0:
cursor_position = 0
# Update the text entry display and cursor
self.text_entry_display.render(self.passphrase, cursor_position)
elif ret_val == Keyboard.KEY_CURSOR_RIGHT["code"]:
cursor_position += 1
if cursor_position > len(self.passphrase):
cursor_position = len(self.passphrase)
elif ret_val == Keyboard.KEY_SPACE["code"]:
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.passphrase):
self.passphrase += " "
self.passphrase += ret_val
else:
self.passphrase = self.passphrase[:cursor_position] + " " + self.passphrase[cursor_position:]
self.passphrase = self.passphrase[:cursor_position] + ret_val + self.passphrase[cursor_position:]
cursor_position += 1
# Update the text entry display and cursor
self.text_entry_display.render(self.passphrase, cursor_position)
# Update the text entry display and cursor
self.text_entry_display.render(self.passphrase, cursor_position)
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.passphrase):
self.passphrase += ret_val
else:
self.passphrase = self.passphrase[:cursor_position] + ret_val + self.passphrase[cursor_position:]
cursor_position += 1
elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN or keyboard_swap:
# 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
if keyboard_swap:
# Show the hw buttons' updated text and not active state
self.hw_button1.text = cur_button1_text
self.hw_button2.text = cur_button2_text
self.hw_button1.is_selected = False
self.hw_button2.is_selected = False
self.hw_button1.render()
self.hw_button2.render()
# Update the text entry display and cursor
self.text_entry_display.render(self.passphrase, cursor_position)
elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN or keyboard_swap:
# 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
if keyboard_swap:
# Show the hw buttons' updated text and not active state
self.hw_button1.text = cur_button1_text
self.hw_button2.text = cur_button2_text
self.hw_button1.is_selected = False
self.hw_button2.is_selected = False
self.hw_button1.render()
self.hw_button2.render()
self.renderer.show_image()
self.renderer.show_image()
@@ -145,6 +145,7 @@ class IOTestScreen(BaseTopNavScreen):
screen_x=self.canvas_width - key_button_width + GUIConstants.EDGE_PADDING,
screen_y=key2_y,
outline_color=GUIConstants.ACCENT_COLOR,
is_scrollable_text=False, # Text has to dynamically update, can't use scrollable Button
)
self.key2_button.text = " " # but default state is empty
self.components.append(self.key2_button)
@@ -166,6 +167,7 @@ class IOTestScreen(BaseTopNavScreen):
screen_x=self.canvas_width - key_button_width + GUIConstants.EDGE_PADDING,
screen_y=key2_y + 3*GUIConstants.COMPONENT_PADDING + key_button_height,
outline_color=GUIConstants.ACCENT_COLOR,
is_scrollable_text=False, # No help for l10n, but currently ScrollableTextLine interferes with the small button's left edge. (TODO:)
)
self.components.append(self.key3_button)
@@ -187,18 +189,19 @@ class IOTestScreen(BaseTopNavScreen):
cur_selected_button = self.key1_button
with self.renderer.lock:
cur_selected_button.is_selected = True
cur_selected_button.render()
camera_message.render()
# Render edges around message box
self.image_draw.rectangle(
(
-1, int((self.canvas_height - msg_height)/ 2) - 1,
self.canvas_width + 1, int((self.canvas_height + msg_height)/ 2) + 1
),
fill="black",
outline=GUIConstants.ACCENT_COLOR,
width=1,
)
cur_selected_button.is_selected = True
cur_selected_button.render()
camera_message.render()
self.renderer.show_image()
# Snap a pic, render it as the background, re-render all onscreen elements
+1 -1
View File
@@ -683,7 +683,7 @@ class ToolsAddressExplorerAddressListView(View):
end_digits = -5
else:
end_digits = -4
button_data.append(ButtonOption(f"{cur_index}:{address[:8]}...{address[end_digits:]}"))
button_data.append(ButtonOption(f"{cur_index}:{address[:8]}...{address[end_digits:]}", active_button_label=f"{cur_index}:{address}"))
# TRANSLATOR_NOTE: Insert the number of addrs displayed per screen (e.g. "Next 10")
button_label = _("Next {}").format(addrs_per_screen)