192 lines
6.3 KiB
Python
192 lines
6.3 KiB
Python
from micropython import const
|
|
from trezor import io, loop, ui, res
|
|
from trezor.ui import display
|
|
from trezor.ui.button import BTN_CLICKED, Button
|
|
from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe
|
|
|
|
SPACE = res.load(ui.ICON_SPACE)
|
|
|
|
KEYBOARD_KEYS = (
|
|
('1', '2', '3', '4', '5', '6', '7', '8', '9', '0'),
|
|
(SPACE, 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz', '*#'),
|
|
(SPACE, 'ABC', 'DEF', 'GHI', 'JKL', 'MNO', 'PQRS', 'TUV', 'WXYZ', '*#'),
|
|
('_', '.', '/', '!', '+', '-', '?', ',', ';', '$'))
|
|
|
|
|
|
def digit_area(i):
|
|
if i == 9: # 0-position
|
|
i = 10 # display it in the middle
|
|
return ui.grid(i + 3) # skip the first line
|
|
|
|
|
|
def key_buttons(keys):
|
|
return [Button(digit_area(i), k) for i, k in enumerate(keys)]
|
|
|
|
|
|
def render_scrollbar(page):
|
|
bbox = const(240)
|
|
size = const(8)
|
|
padding = 12
|
|
page_count = len(KEYBOARD_KEYS)
|
|
|
|
if page_count * padding > bbox:
|
|
padding = bbox // page_count
|
|
|
|
x = (bbox // 2) - (page_count // 2) * padding
|
|
y = 44
|
|
|
|
for i in range(0, page_count):
|
|
if i != page:
|
|
ui.display.bar_radius(
|
|
x + i * padding, y, size, size, ui.DARK_GREY, ui.BG, size // 2)
|
|
ui.display.bar_radius(
|
|
x + page * padding, y, size, size, ui.FG, ui.BG, size // 2)
|
|
|
|
|
|
class Input(Button):
|
|
def __init__(self, area: tuple, content: str=''):
|
|
super().__init__(area, content)
|
|
self.pending = False
|
|
self.disable()
|
|
|
|
def edit(self, content: str, pending: bool):
|
|
self.content = content
|
|
self.pending = pending
|
|
self.taint()
|
|
|
|
def render_content(self, s, ax, ay, aw, ah):
|
|
text_style = s['text-style']
|
|
fg_color = s['fg-color']
|
|
bg_color = s['bg-color']
|
|
|
|
p = self.pending # should we draw the pending marker?
|
|
t = self.content # input content
|
|
|
|
tx = ax + 24 # x-offset of the content
|
|
ty = ay + ah // 2 + 8 # y-offset of the content
|
|
maxlen = const(14) # maximum text length
|
|
|
|
# input content
|
|
if len(t) > maxlen:
|
|
t = '<' + t[-maxlen:] # too long, align to the right
|
|
width = display.text_width(t, text_style)
|
|
display.text(tx, ty, t, text_style, fg_color, bg_color)
|
|
|
|
if p: # pending marker
|
|
pw = display.text_width(t[-1:], text_style)
|
|
display.bar(tx + width - pw, ty + 2, pw + 1, 3, fg_color)
|
|
else: # cursor
|
|
display.bar(tx + width + 1, ty - 18, 2, 22, fg_color)
|
|
|
|
|
|
CANCELLED = const(0)
|
|
|
|
|
|
class PassphraseKeyboard(ui.Widget):
|
|
def __init__(self, prompt, page=1):
|
|
self.prompt = prompt
|
|
self.page = page
|
|
self.input = Input(ui.grid(0, n_x=1, n_y=6), '')
|
|
self.back = Button(ui.grid(12), res.load(ui.ICON_BACK), style=ui.BTN_CLEAR)
|
|
self.done = Button(ui.grid(14), res.load(ui.ICON_CONFIRM), style=ui.BTN_CONFIRM)
|
|
self.keys = key_buttons(KEYBOARD_KEYS[self.page])
|
|
self.pbutton = None # pending key button
|
|
self.pindex = 0 # index of current pending char in pbutton
|
|
|
|
def render(self):
|
|
# passphrase or prompt
|
|
if self.input.content:
|
|
self.input.render()
|
|
else:
|
|
display.bar(0, 0, 240, 48, ui.BG)
|
|
display.text_center(ui.WIDTH // 2, 32, self.prompt, ui.BOLD, ui.GREY, ui.BG)
|
|
render_scrollbar(self.page)
|
|
# buttons
|
|
self.back.render()
|
|
self.done.render()
|
|
for btn in self.keys:
|
|
btn.render()
|
|
|
|
def touch(self, event, pos):
|
|
content = self.input.content
|
|
if self.back.touch(event, pos) == BTN_CLICKED:
|
|
if content:
|
|
# backspace, delete the last character of input
|
|
self.edit(content[:-1])
|
|
return
|
|
else:
|
|
# cancel
|
|
return CANCELLED
|
|
if self.done.touch(event, pos) == BTN_CLICKED:
|
|
# confirm button, return the content
|
|
return content
|
|
for btn in self.keys:
|
|
if btn.touch(event, pos) == BTN_CLICKED:
|
|
if isinstance(btn.content[0], str):
|
|
# key press, add new char to input or cycle the pending button
|
|
if self.pbutton is btn:
|
|
index = (self.pindex + 1) % len(btn.content)
|
|
content = content[:-1] + btn.content[index]
|
|
else:
|
|
index = 0
|
|
content += btn.content[0]
|
|
else:
|
|
index = 0
|
|
content += ' '
|
|
|
|
self.edit(content, btn, index)
|
|
return
|
|
|
|
def edit(self, content, button=None, index=0):
|
|
if button and len(button.content) == 1:
|
|
# one-letter buttons are never pending
|
|
button = None
|
|
index = 0
|
|
self.pbutton = button
|
|
self.pindex = index
|
|
self.input.edit(content, button is not None)
|
|
if content:
|
|
self.back.enable()
|
|
else:
|
|
self.back.disable()
|
|
|
|
async def __iter__(self):
|
|
self.edit(self.input.content) # init button state
|
|
while True:
|
|
change = self.change_page()
|
|
enter = self.enter_text()
|
|
wait = loop.wait(change, enter)
|
|
result = await wait
|
|
if enter in wait.finished:
|
|
return result
|
|
|
|
@ui.layout
|
|
async def enter_text(self):
|
|
timeout = loop.sleep(1000 * 1000 * 1)
|
|
touch = loop.select(io.TOUCH)
|
|
wait_timeout = loop.wait(touch, timeout)
|
|
wait_touch = loop.wait(touch)
|
|
content = None
|
|
while content is None:
|
|
self.render()
|
|
if self.pbutton is not None:
|
|
wait = wait_timeout
|
|
else:
|
|
wait = wait_touch
|
|
result = await wait
|
|
if touch in wait.finished:
|
|
event, *pos = result
|
|
content = self.touch(event, pos)
|
|
else:
|
|
# disable the pending buttons
|
|
self.edit(self.input.content)
|
|
return content
|
|
|
|
async def change_page(self):
|
|
swipe = await Swipe(directions=SWIPE_HORIZONTAL)
|
|
if swipe == SWIPE_LEFT:
|
|
self.page = (self.page + 1) % len(KEYBOARD_KEYS)
|
|
else:
|
|
self.page = (self.page - 1) % len(KEYBOARD_KEYS)
|
|
self.keys = key_buttons(KEYBOARD_KEYS[self.page])
|