ui: request_words, keyboard ui

This commit is contained in:
Peter Jensen 2017-11-03 17:14:01 +01:00 committed by Jan Pochyla
parent 95db112d10
commit ed9e63142d
23 changed files with 242 additions and 120 deletions

BIN
assets/5390-200.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/left.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
assets/recovery-old.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
assets/send-old.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,30 @@
from trezor import wire, ui, loop
from trezor.utils import unimport
# used to confirm/cancel the dialogs from outside of this module (i.e.
# through debug link)
if __debug__:
signal = loop.signal()
@ui.layout
@unimport
async def request_words(ctx, content, code=None, *args, **kwargs):
from trezor.ui.word_select import WordSelector
from trezor.messages.ButtonRequest import ButtonRequest
from trezor.messages.ButtonRequestType import Other
from trezor.messages.wire_types import ButtonAck
ui.display.clear()
dialog = WordSelector(content, *args, **kwargs)
dialog.render()
if code is None:
code = Other
await ctx.call(ButtonRequest(code=code), ButtonAck)
if __debug__:
waiter = loop.wait(signal, dialog)
else:
waiter = dialog
return await waiter

View File

@ -11,15 +11,20 @@ async def layout_recovery_device(ctx, msg):
from trezor.ui.text import Text from trezor.ui.text import Text
from apps.common import storage from apps.common import storage
from apps.common.confirm import require_confirm from apps.common.confirm import require_confirm
from apps.common.request_words import request_words
if storage.is_initialized(): if storage.is_initialized():
raise wire.FailureError(UnexpectedMessage, 'Already initialized') raise wire.FailureError(UnexpectedMessage, 'Already initialized')
words = [] words = []
wc = await request_words(ctx, Text(
'Device recovery', ui.ICON_RECOVERY, 'Number of words?'))
msg.word_count = int(wc)
ui.display.clear()
kbd = KeyboardMultiTap() kbd = KeyboardMultiTap()
for i in range(0, msg.word_count): for i in range(0, msg.word_count):
kbd.prompt = '%s. ' % (i + 1) kbd.prompt = 'Type %s. word' % (i + 1)
word = await kbd word = await kbd
words.append(word) words.append(word)

BIN
src/trezor/res/click.toig Normal file

Binary file not shown.

Binary file not shown.

BIN
src/trezor/res/cross2.toig Normal file

Binary file not shown.

BIN
src/trezor/res/left.toig Normal file

Binary file not shown.

Binary file not shown.

BIN
src/trezor/res/send2.toig Normal file

Binary file not shown.

View File

@ -106,11 +106,10 @@ def layout(f):
return inner return inner
def header(title: str, icon: bytes=ICON_RESET, fg: int=BG, bg: int=BG): def header(title: str, icon: bytes=ICON_RESET, fg: int=BG, bg: int=BG, ifg: int=BG):
display.bar(0, 0, 240, 32, bg)
if icon is not None: if icon is not None:
display.icon(8, 4, res.load(icon), fg, bg) display.icon(14, 14, res.load(icon), ifg, bg)
display.text(8 + 24 + 2, 24, title, BOLD, fg, bg) display.text(44, 35, title, BOLD, fg, bg)
class Widget: class Widget:

View File

@ -32,11 +32,13 @@ class Button(Widget):
self.state = BTN_DIRTY self.state = BTN_DIRTY
def enable(self): def enable(self):
self.state &= ~BTN_DISABLED if self.state & BTN_DISABLED:
self.state |= BTN_DIRTY self.state &= ~BTN_DISABLED
self.state |= BTN_DIRTY
def disable(self): def disable(self):
self.state |= BTN_DISABLED | BTN_DIRTY if not self.state & BTN_DISABLED:
self.state |= BTN_DISABLED | BTN_DIRTY
def taint(self): def taint(self):
self.state |= BTN_DIRTY self.state |= BTN_DIRTY
@ -54,6 +56,7 @@ class Button(Widget):
ax, ay, aw, ah = self.area ax, ay, aw, ah = self.area
tx = ax + aw // 2 tx = ax + aw // 2
ty = ay + ah // 2 + 8 ty = ay + ah // 2 + 8
display.bar_radius(ax, ay, aw, ah, display.bar_radius(ax, ay, aw, ah,
s['border-color'], s['border-color'],
ui.BG, ui.BG,
@ -70,7 +73,7 @@ class Button(Widget):
s['bg-color']) s['bg-color'])
else: else:
display.icon(tx - 15, ty - 20, self.content, display.icon(tx - 8, ty - 16, self.content,
s['fg-color'], s['fg-color'],
s['bg-color']) s['bg-color'])

View File

@ -4,7 +4,7 @@ from trezor.ui import display
from trezor.ui.button import Button, BTN_CLICKED from trezor.ui.button import Button, BTN_CLICKED
def cell_area(i, n_x=3, n_y=3, start_x=0, start_y=40, end_x=240, end_y=240, spacing=0): 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 w = (end_x - start_x) // n_x
h = (end_y - start_y) // n_y h = (end_y - start_y) // n_y
x = (i % n_x) * w x = (i % n_x) * w
@ -17,7 +17,8 @@ def key_buttons():
# keys = [' ', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz'] # keys = [' ', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz']
return [Button(cell_area(i), k, return [Button(cell_area(i), k,
normal_style=ui.BTN_KEY, normal_style=ui.BTN_KEY,
active_style=ui.BTN_KEY_ACTIVE) for i, k in enumerate(keys)] active_style=ui.BTN_KEY_ACTIVE,
disabled_style=ui.BTN_KEY_DISABLED) for i, k in enumerate(keys)]
def compute_mask(text): def compute_mask(text):
@ -41,38 +42,52 @@ class KeyboardMultiTap(ui.Widget):
self.pending_index = 0 self.pending_index = 0
self.key_buttons = key_buttons() self.key_buttons = key_buttons()
self.sugg_button = Button((5, 5, 240 - 35, 30), '') self.sugg_button = Button((63, 0, 240 - 65, 57), '')
self.bs_button = Button((240 - 35, 5, 30, 30), self.bs_button = Button((6, 5, 57, 60),
res.load('trezor/res/pin_close.toig'), res.load('trezor/res/left.toig'),
normal_style=ui.BTN_CLEAR, normal_style=ui.BTN_CLEAR,
active_style=ui.BTN_CLEAR_ACTIVE) active_style=ui.BTN_CLEAR_ACTIVE)
def render(self): def render(self):
# clear canvas under input line
display.bar(0, 0, 205, 40, ui.BG)
# input line
content_width = display.text_width(self.prompt + self.content, ui.BOLD)
display.text(20, 30, self.prompt + self.content, ui.BOLD, ui.FG, ui.BG)
# pending marker
if self.pending_button is not None:
pending_width = display.text_width(self.content[-1:], ui.BOLD)
pending_x = 20 + content_width - pending_width
display.bar(pending_x, 33, pending_width + 2, 3, ui.FG)
# auto-suggest
if self.sugg_word is not None:
sugg_rest = self.sugg_word[len(self.content):]
sugg_x = 20 + content_width
display.text(sugg_x, 30, sugg_rest, ui.BOLD, ui.GREY, ui.BG)
# render backspace button
if self.content: if self.content:
display.bar(62, 8, 168, 54, ui.BG)
content_width = display.text_width(self.content, ui.BOLD)
offset_x = 78
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() 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:
display.bar(240 - 48, 0, 48, 42, ui.BG) # prompt
display.bar(0, 8, 240, 60, 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.key_buttons:
@ -87,10 +102,12 @@ class KeyboardMultiTap(ui.Widget):
self._update_buttons() self._update_buttons()
return return
if self.sugg_button.touch(event, pos) == BTN_CLICKED: if self.sugg_button.touch(event, pos) == BTN_CLICKED:
if self.content == bip39.find_word(self.content): if not self.content or self.sugg_word is None:
return None
if self.content == self.sugg_word:
result = self.content result = self.content
self.content = '' self.content = ''
elif self.sugg_word is not None: else:
result = None result = None
self.content = self.sugg_word self.content = self.sugg_word
self.pending_button = None self.pending_button = None
@ -128,19 +145,22 @@ class KeyboardMultiTap(ui.Widget):
else: else:
btn.disable() btn.disable()
def __iter__(self): async def __iter__(self):
timeout = loop.sleep(1000 * 1000 * 1) timeout = loop.sleep(1000 * 1000 * 1)
touch = loop.select(io.TOUCH) touch = loop.select(io.TOUCH)
wait_timeout = loop.wait(touch, timeout) wait_timeout = loop.wait(touch, timeout)
wait_touch = loop.wait(touch) wait_touch = loop.wait(touch)
content = None content = None
self.bs_button.taint()
while content is None: while content is None:
self.render() self.render()
if self.pending_button is not None: if self.pending_button is not None:
wait = wait_timeout wait = wait_timeout
else: else:
wait = wait_touch wait = wait_touch
result = yield wait result = await wait
if touch in wait.finished: if touch in wait.finished:
event, *pos = result event, *pos = result
content = self.touch(event, pos) content = self.touch(event, pos)
@ -154,90 +174,90 @@ class KeyboardMultiTap(ui.Widget):
return content return content
def zoom_buttons(keys, upper=False): # def zoom_buttons(keys, upper=False):
n_x = len(keys) # n_x = len(keys)
if upper: # if upper:
keys = keys + keys.upper() # keys = keys + keys.upper()
n_y = 2 # n_y = 2
else: # else:
n_y = 1 # n_y = 1
return [Button(cell_area(i, n_x, n_y), key) for i, key in enumerate(keys)] # return [Button(cell_area(i, n_x, n_y), key) for i, key in enumerate(keys)]
class KeyboardZooming(ui.Widget): # class KeyboardZooming(ui.Widget):
def __init__(self, content='', uppercase=True): # def __init__(self, content='', uppercase=True):
self.content = content # self.content = content
self.uppercase = uppercase # self.uppercase = uppercase
self.zoom_buttons = None # self.zoom_buttons = None
self.key_buttons = key_buttons() # self.key_buttons = key_buttons()
self.bs_button = Button((240 - 35, 5, 30, 30), # self.bs_button = Button((240 - 35, 5, 30, 30),
res.load('trezor/res/pin_close.toig'), # res.load('trezor/res/pin_close.toig'),
normal_style=ui.BTN_CLEAR, # normal_style=ui.BTN_CLEAR,
active_style=ui.BTN_CLEAR_ACTIVE) # active_style=ui.BTN_CLEAR_ACTIVE)
def render(self): # def render(self):
self.render_input() # self.render_input()
if self.zoom_buttons: # if self.zoom_buttons:
for btn in self.zoom_buttons: # for btn in self.zoom_buttons:
btn.render() # btn.render()
else: # else:
for btn in self.key_buttons: # for btn in self.key_buttons:
btn.render() # btn.render()
def render_input(self): # def render_input(self):
if self.content: # if self.content:
display.bar(0, 0, 200, 40, ui.BG) # display.bar(0, 0, 200, 40, ui.BG)
else: # else:
display.bar(0, 0, 240, 40, ui.BG) # display.bar(0, 0, 240, 40, ui.BG)
display.text(20, 30, self.content, ui.BOLD, ui.GREY, ui.BG) # display.text(20, 30, self.content, ui.BOLD, ui.GREY, ui.BG)
if self.content: # if self.content:
self.bs_button.render() # self.bs_button.render()
def touch(self, event, pos): # def touch(self, event, pos):
if self.bs_button.touch(event, pos) == BTN_CLICKED: # if self.bs_button.touch(event, pos) == BTN_CLICKED:
self.content = self.content[:-1] # self.content = self.content[:-1]
self.bs_button.taint() # self.bs_button.taint()
return # return
if self.zoom_buttons: # if self.zoom_buttons:
return self.touch_zoom(event, pos) # return self.touch_zoom(event, pos)
else: # else:
return self.touch_keyboard(event, pos) # return self.touch_keyboard(event, pos)
def touch_zoom(self, event, pos): # def touch_zoom(self, event, pos):
for btn in self.zoom_buttons: # for btn in self.zoom_buttons:
if btn.touch(event, pos) == BTN_CLICKED: # if btn.touch(event, pos) == BTN_CLICKED:
self.content += btn.content # self.content += btn.content
self.zoom_buttons = None # self.zoom_buttons = None
for b in self.key_buttons: # for b in self.key_buttons:
b.taint() # b.taint()
self.bs_button.taint() # self.bs_button.taint()
break # break
def touch_keyboard(self, event, pos): # def touch_keyboard(self, event, pos):
for btn in self.key_buttons: # for btn in self.key_buttons:
if btn.touch(event, pos) == BTN_CLICKED: # if btn.touch(event, pos) == BTN_CLICKED:
self.zoom_buttons = zoom_buttons(btn.content, self.uppercase) # self.zoom_buttons = zoom_buttons(btn.content, self.uppercase)
for b in self.zoom_buttons: # for b in self.zoom_buttons:
b.taint() # b.taint()
self.bs_button.taint() # self.bs_button.taint()
break # break
def __iter__(self): # def __iter__(self):
timeout = loop.sleep(1000 * 1000 * 1) # timeout = loop.sleep(1000 * 1000 * 1)
touch = loop.select(io.TOUCH) # touch = loop.select(io.TOUCH)
wait = loop.wait(touch, timeout) # wait = loop.wait(touch, timeout)
while True: # while True:
self.render() # self.render()
result = yield wait # result = yield wait
if touch in wait.finished: # if touch in wait.finished:
event, *pos = result # event, *pos = result
self.touch(event, pos) # self.touch(event, pos)
elif self.zoom_buttons: # elif self.zoom_buttons:
self.zoom_buttons = None # self.zoom_buttons = None
for btn in self.key_buttons: # for btn in self.key_buttons:
btn.taint() # btn.taint()
Keyboard = KeyboardMultiTap Keyboard = KeyboardMultiTap

View File

@ -38,7 +38,10 @@ DARK_GREY = rgb(0x3E, 0x3E, 0x3E)
BLUE_GRAY = rgb(0x60, 0x7D, 0x8B) BLUE_GRAY = rgb(0x60, 0x7D, 0x8B)
BLACK = rgb(0x00, 0x00, 0x00) BLACK = rgb(0x00, 0x00, 0x00)
WHITE = rgb(0xFA, 0xFA, 0xFA) WHITE = rgb(0xFA, 0xFA, 0xFA)
BLACKISH = rgb(0x20, 0x20, 0x20) BLACKISH = rgb(0x30, 0x30, 0x30)
TITLE_GREY = rgb(0x9B, 0x9B, 0x9B)
ORANGE_ICON = rgb(0xF5, 0xA6, 0x23)
# common color styles # common color styles
BG = BLACK BG = BLACK
@ -47,11 +50,13 @@ FG = WHITE
# icons # icons
ICON_RESET = 'trezor/res/header_icons/reset.toig' ICON_RESET = 'trezor/res/header_icons/reset.toig'
ICON_WIPE = 'trezor/res/header_icons/wipe.toig' ICON_WIPE = 'trezor/res/header_icons/wipe.toig'
ICON_RECOVERY = 'trezor/res/header_icons/recovery.toig' ICON_RECOVERY = 'trezor/res/header_icons/reset.toig'
ICON_CLEAR = 'trezor/res/clear.toig' ICON_CLEAR = 'trezor/res/clear.toig'
ICON_CONFIRM = 'trezor/res/confirm.toig' ICON_CONFIRM = 'trezor/res/confirm.toig'
ICON_CONFIRM2 = 'trezor/res/confirm2.toig'
ICON_LOCK = 'trezor/res/lock.toig' ICON_LOCK = 'trezor/res/lock.toig'
ICON_SEND = 'trezor/res/send.toig' ICON_SEND = 'trezor/res/send.toig'
ICON_CLICK = 'trezor/res/click.toig'
# buttons # buttons
BTN_DEFAULT = { BTN_DEFAULT = {
@ -104,7 +109,7 @@ BTN_CONFIRM_ACTIVE = {
'radius': RADIUS, 'radius': RADIUS,
} }
BTN_CLEAR = { BTN_CLEAR = {
'bg-color': BG, 'bg-color': ORANGE,
'fg-color': FG, 'fg-color': FG,
'text-style': NORMAL, 'text-style': NORMAL,
'border-color': BG, 'border-color': BG,
@ -131,6 +136,13 @@ BTN_KEY_ACTIVE = {
'border-color': FG, 'border-color': FG,
'radius': RADIUS, 'radius': RADIUS,
} }
BTN_KEY_DISABLED = {
'bg-color': BG,
'fg-color': GREY,
'text-style': MONO,
'border-color': BG,
'radius': RADIUS,
}
# loader # loader
LDR_DEFAULT = { LDR_DEFAULT = {

View File

@ -1,9 +1,9 @@
from micropython import const from micropython import const
from trezor import ui from trezor import ui
TEXT_HEADER_HEIGHT = const(32) TEXT_HEADER_HEIGHT = const(51)
TEXT_LINE_HEIGHT = const(23) TEXT_LINE_HEIGHT = const(23)
TEXT_MARGIN_LEFT = const(10) TEXT_MARGIN_LEFT = const(14)
class Text(ui.Widget): class Text(ui.Widget):
@ -19,7 +19,7 @@ class Text(ui.Widget):
style = ui.NORMAL style = ui.NORMAL
fg = ui.FG fg = ui.FG
bg = ui.BG bg = ui.BG
ui.header(self.header_text, self.header_icon, ui.GREEN, ui.BG) ui.header(self.header_text, self.header_icon, ui.TITLE_GREY, ui.BG, ui.ORANGE_ICON)
for item in self.content: for item in self.content:
if isinstance(item, str): if isinstance(item, str):

View File

@ -0,0 +1,53 @@
from micropython import const
from trezor import loop
from trezor import ui, res
from trezor.ui import Widget
from trezor.ui.button import Button, BTN_CLICKED, BTN_STARTED, BTN_ACTIVE
W12 = '12'
W15 = '15'
W18 = '18'
W24 = '24'
class WordSelector(Widget):
def __init__(self, content):
self.content = content
self.w12 = Button((6, 135, 114, 51), W12,
normal_style=ui.BTN_KEY,
active_style=ui.BTN_KEY_ACTIVE)
self.w15 = Button((120, 135, 114, 51), W15,
normal_style=ui.BTN_KEY,
active_style=ui.BTN_KEY_ACTIVE)
self.w18 = Button((6, 186, 114, 51), W18,
normal_style=ui.BTN_KEY,
active_style=ui.BTN_KEY_ACTIVE)
self.w24 = Button((120, 186, 114, 51), W24,
normal_style=ui.BTN_KEY,
active_style=ui.BTN_KEY_ACTIVE)
def render(self):
self.w12.render()
self.w15.render()
self.w18.render()
self.w24.render()
def touch(self, event, pos):
if self.w12.touch(event, pos) == BTN_CLICKED:
return W12
if self.w15.touch(event, pos) == BTN_CLICKED:
return W15
if self.w18.touch(event, pos) == BTN_CLICKED:
return W18
if self.w24.touch(event, pos) == BTN_CLICKED:
return W24
async def __iter__(self):
return await loop.wait(super().__iter__(), self.content)
_STARTED = const(-1)
_STOPPED = const(-2)