mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
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:
+1
-1
Submodule seedsigner-screenshots updated: facec05bca...e17733ba08
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user