ui/keyboard: refactor

This commit is contained in:
Jan Pochyla 2018-01-11 20:06:01 +01:00
parent 4a83864593
commit cd0fa4df4a
1 changed files with 115 additions and 201 deletions

View File

@ -1,27 +1,18 @@
from trezor import ui, res, loop, io from trezor import ui, res, loop, io
from trezor.crypto import bip39 from trezor.crypto import bip39
from trezor.ui import display from trezor.ui import display
from trezor.ui.button import Button, BTN_CLICKED from trezor.ui.button import Button, BTN_CLICKED, ICON
def cell_area(i, n_x=3, n_y=3, start_x=6, start_y=66, end_x=234, end_y=237, spacing=0):
w = (end_x - start_x) // n_x
h = (end_y - start_y) // n_y
x = (i % n_x) * w
y = (i // n_x) * h
return (x + start_x, y + start_y, w - spacing, h - spacing)
def key_buttons(): def key_buttons():
keys = ['abc', 'def', 'ghi', 'jkl', 'mno', 'pqr', 'stu', 'vwx', 'yz'] keys = ['abc', 'def', 'ghi', 'jkl', 'mno', 'pqr', 'stu', 'vwx', 'yz']
# keys = [' ', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz'] return [Button(ui.grid(i + 3, n_y=4), k,
return [Button(cell_area(i), k,
normal_style=ui.BTN_KEY, normal_style=ui.BTN_KEY,
active_style=ui.BTN_KEY_ACTIVE, active_style=ui.BTN_KEY_ACTIVE,
disabled_style=ui.BTN_KEY_DISABLED) for i, k in enumerate(keys)] disabled_style=ui.BTN_KEY_DISABLED) for i, k in enumerate(keys)]
def compute_mask(text): def compute_mask(text: str) -> int:
mask = 0 mask = 0
for c in text: for c in text:
shift = ord(c) - 97 # ord('a') == 97 shift = ord(c) - 97 # ord('a') == 97
@ -31,116 +22,127 @@ def compute_mask(text):
return mask return mask
class KeyboardMultiTap(ui.Widget): class Input(Button):
def __init__(self, area: tuple, content: str='', word: str=''):
super().__init__(area, content)
self.word = word
self.icon = None
self.pending = False
def __init__(self, content='', prompt=''): def edit(self, content: str, word: str, pending: bool):
self.content = content self.content = content
self.prompt = prompt self.word = word
self.sugg_mask = 0xffffffff self.pending = pending
self.sugg_word = None self.taint()
self.pending_button = None if content == word: # confirm button
self.pending_index = 0 self.enable()
self.normal_style = ui.BTN_CONFIRM
self.active_style = ui.BTN_CONFIRM_ACTIVE
self.icon = ui.ICON_CONFIRM
elif word: # auto-complete button
self.enable()
self.normal_style = ui.BTN_KEY
self.active_style = ui.BTN_KEY_ACTIVE
self.icon = ui.ICON_CLICK
else: # disabled button
self.disable()
self.icon = None
self.key_buttons = key_buttons() def render_content(self, s, ax, ay, aw, ah):
self.sugg_button = Button((63, 0, 240 - 65, 57), '') text_style = s['text-style']
self.bs_button = Button((6, 5, 57, 60), fg_color = s['fg-color']
res.load('trezor/res/left.toig'), bg_color = s['bg-color']
normal_style=ui.BTN_CLEAR,
active_style=ui.BTN_CLEAR_ACTIVE) p = self.pending # should we draw the pending marker?
t = self.content # input content
w = self.word[len(t):] # suggested word
i = self.icon # rendered icon
tx = ax + 24 # x-offset of the content
ty = ay + ah//2 + 8 # y-offset of the content
# input content and the suggested word
display.text(tx, ty, t, text_style, fg_color, bg_color)
width = display.text_width(t, text_style)
display.text(tx + width, ty, w, text_style, ui.GREY, bg_color)
if p: # pending marker
pw = display.text_width(t[-1:], text_style)
px = tx + width - pw
display.bar(px, ty, pw + 2, 3, fg_color)
if i: # icon
ix = ax + aw - ICON*2
iy = ty - ICON
display.icon(ix, iy, res.load(i), fg_color, bg_color)
class MnemonicKeyboard(ui.Widget):
def __init__(self, prompt: str=''):
self.prompt = prompt
self.input = Input(ui.grid(1, n_x=4, n_y=4, cells_x=3), '', '')
self.back = Button(ui.grid(0, n_x=4, n_y=4),
res.load(ui.ICON_BACK),
normal_style=ui.BTN_CLEAR,
active_style=ui.BTN_CLEAR_ACTIVE)
self.keys = key_buttons()
self.pbutton = None # pending key button
self.pindex = 0 # index of current pending char in pbutton
def render(self): def render(self):
if self.content: if self.input.content:
display.bar(62, 8, 168, 54, ui.BG) # content button and backspace
content_width = display.text_width(self.content, ui.BOLD) self.input.render()
offset_x = 78 self.back.render()
if self.content == self.sugg_word:
# confirm button + content
display.bar_radius(67, 8, 164, 54, ui.GREEN, ui.BG, ui.RADIUS)
type_icon = res.load(ui.ICON_CONFIRM2)
display.icon(228 - 30, 28, type_icon, ui.WHITE, ui.GREEN)
display.text(offset_x, 40, self.content, ui.BOLD, ui.WHITE, ui.GREEN)
elif self.sugg_word is not None:
# auto-suggest button + content + suggestion
display.bar_radius(67, 8, 164, 54, ui.BLACKISH, ui.BG, ui.RADIUS)
display.text(offset_x, 40, self.content, ui.BOLD, ui.FG, ui.BLACKISH)
sugg_text = self.sugg_word[len(self.content):]
sugg_x = offset_x + content_width
type_icon = res.load(ui.ICON_CLICK)
display.icon(228 - 30, 24, type_icon, ui.GREY, ui.BLACKISH)
display.text(sugg_x, 40, sugg_text, ui.BOLD, ui.GREY, ui.BLACKISH)
else:
# content
display.bar(63, 8, 168, 54, ui.BG)
display.text(offset_x, 40, self.content, ui.BOLD, ui.FG, ui.BG)
# backspace button
self.bs_button.render()
# pending marker
if self.pending_button is not None:
pending_width = display.text_width(self.content[-1:], ui.BOLD)
pending_x = offset_x + content_width - pending_width
display.bar(pending_x, 42, pending_width + 2, 3, ui.FG)
else: else:
# prompt # prompt
display.bar(0, 8, 240, 60, ui.BG) display.bar(0, 8, 240, 60, ui.BG)
display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG) display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG)
# key buttons # key buttons
for btn in self.key_buttons: for btn in self.keys:
btn.render() btn.render()
def touch(self, event, pos): def touch(self, event, pos):
if self.bs_button.touch(event, pos) == BTN_CLICKED: content = self.input.content
self.content = self.content[:-1] word = self.input.word
self.pending_button = None
self.pending_index = 0 if self.back.touch(event, pos) == BTN_CLICKED:
self._update_suggestion() # backspace, delete the last character of input
self._update_buttons() self.edit(content[:-1])
return return
if self.sugg_button.touch(event, pos) == BTN_CLICKED:
if not self.content or self.sugg_word is None: if self.input.touch(event, pos) == BTN_CLICKED:
return None # input press, either auto-complete or confirm
if self.content == self.sugg_word: if content == word:
result = self.content self.edit('')
self.content = '' return content
else: else:
result = None self.edit(word)
self.content = self.sugg_word
self.pending_button = None
self.pending_index = 0
self._update_suggestion()
self._update_buttons()
return result
for btn in self.key_buttons:
if btn.touch(event, pos) == BTN_CLICKED:
if self.pending_button is btn:
self.pending_index = (
self.pending_index + 1) % len(btn.content)
self.content = self.content[:-1]
self.content += btn.content[self.pending_index]
self._update_suggestion()
else:
self.content += btn.content[0]
self._update_suggestion()
self.pending_button = btn
self.pending_index = 0
return return
def _update_suggestion(self): for btn in self.keys:
if self.content: if btn.touch(event, pos) == BTN_CLICKED:
self.sugg_word = bip39.find_word(self.content) # key press, add new char to input or cycle the pending button
self.sugg_mask = bip39.complete_word(self.content) if self.pbutton is btn:
else: index = (self.pindex + 1) % len(btn.content)
self.sugg_word = None content = content[:-1] + btn.content[index]
self.sugg_mask = 0xffffffff else:
index = 0
content += btn.content[0]
self.edit(content, btn, index)
return
def _update_buttons(self): def edit(self, content, button=None, index=0):
for btn in self.key_buttons: word = bip39.find_word(content) or ''
if btn is self.pending_button or compute_mask(btn.content) & self.sugg_mask: mask = bip39.complete_word(content)
self.pbutton = button
self.pindex = index
self.input.edit(content, word, button is not None)
# enable or disable key buttons
for btn in self.keys:
if btn is button or compute_mask(btn.content) & mask:
btn.enable() btn.enable()
else: else:
btn.disable() btn.disable()
@ -152,11 +154,12 @@ class KeyboardMultiTap(ui.Widget):
wait_touch = loop.wait(touch) wait_touch = loop.wait(touch)
content = None content = None
self.bs_button.taint() self.back.taint()
self.input.taint()
while content is None: while content is None:
self.render() self.render()
if self.pending_button is not None: if self.pbutton is not None:
wait = wait_timeout wait = wait_timeout
else: else:
wait = wait_touch wait = wait_touch
@ -165,99 +168,10 @@ class KeyboardMultiTap(ui.Widget):
event, *pos = result event, *pos = result
content = self.touch(event, pos) content = self.touch(event, pos)
else: else:
self.pending_button = None if self.input.word:
self.pending_index = 0 # just reset the pending state
if self.sugg_word is None: self.edit(self.input.content)
self.content = self.content[:-1] else:
self._update_suggestion() # invalid character, backspace it
self._update_buttons() self.edit(self.input.content[:-1])
return content return content
# def zoom_buttons(keys, upper=False):
# n_x = len(keys)
# if upper:
# keys = keys + keys.upper()
# n_y = 2
# else:
# n_y = 1
# return [Button(cell_area(i, n_x, n_y), key) for i, key in enumerate(keys)]
# class KeyboardZooming(ui.Widget):
# def __init__(self, content='', uppercase=True):
# self.content = content
# self.uppercase = uppercase
# self.zoom_buttons = None
# self.key_buttons = key_buttons()
# self.bs_button = Button((240 - 35, 5, 30, 30),
# res.load('trezor/res/pin_close.toig'),
# normal_style=ui.BTN_CLEAR,
# active_style=ui.BTN_CLEAR_ACTIVE)
# def render(self):
# self.render_input()
# if self.zoom_buttons:
# for btn in self.zoom_buttons:
# btn.render()
# else:
# for btn in self.key_buttons:
# btn.render()
# def render_input(self):
# if self.content:
# display.bar(0, 0, 200, 40, ui.BG)
# else:
# display.bar(0, 0, 240, 40, ui.BG)
# display.text(20, 30, self.content, ui.BOLD, ui.GREY, ui.BG)
# if self.content:
# self.bs_button.render()
# def touch(self, event, pos):
# if self.bs_button.touch(event, pos) == BTN_CLICKED:
# self.content = self.content[:-1]
# self.bs_button.taint()
# return
# if self.zoom_buttons:
# return self.touch_zoom(event, pos)
# else:
# return self.touch_keyboard(event, pos)
# def touch_zoom(self, event, pos):
# for btn in self.zoom_buttons:
# if btn.touch(event, pos) == BTN_CLICKED:
# self.content += btn.content
# self.zoom_buttons = None
# for b in self.key_buttons:
# b.taint()
# self.bs_button.taint()
# break
# def touch_keyboard(self, event, pos):
# for btn in self.key_buttons:
# if btn.touch(event, pos) == BTN_CLICKED:
# self.zoom_buttons = zoom_buttons(btn.content, self.uppercase)
# for b in self.zoom_buttons:
# b.taint()
# self.bs_button.taint()
# break
# def __iter__(self):
# timeout = loop.sleep(1000 * 1000 * 1)
# touch = loop.select(io.TOUCH)
# wait = loop.wait(touch, timeout)
# while True:
# self.render()
# result = yield wait
# if touch in wait.finished:
# event, *pos = result
# self.touch(event, pos)
# elif self.zoom_buttons:
# self.zoom_buttons = None
# for btn in self.key_buttons:
# btn.taint()
Keyboard = KeyboardMultiTap