Merge pull request #93 from trezor/recovery
Seed recovery with on-screen keyboard
BIN
assets/5390-200.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
assets/left.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/recovery-old.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
assets/send-old.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/send.png
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
@ -18,7 +18,7 @@ STATIC mp_obj_t mod_trezorcrypto_bip39_find_word(mp_obj_t prefix)
|
|||||||
mp_buffer_info_t pfx;
|
mp_buffer_info_t pfx;
|
||||||
mp_get_buffer_raise(prefix, &pfx, MP_BUFFER_READ);
|
mp_get_buffer_raise(prefix, &pfx, MP_BUFFER_READ);
|
||||||
if (pfx.len == 0) {
|
if (pfx.len == 0) {
|
||||||
mp_raise_ValueError("Invalid word prefix");
|
return mp_const_none;
|
||||||
}
|
}
|
||||||
for (const char * const *w = mnemonic_wordlist(); *w != 0; w++) {
|
for (const char * const *w = mnemonic_wordlist(); *w != 0; w++) {
|
||||||
if (strncmp(*w, pfx.buf, pfx.len) == 0) {
|
if (strncmp(*w, pfx.buf, pfx.len) == 0) {
|
||||||
@ -39,7 +39,7 @@ STATIC mp_obj_t mod_trezorcrypto_bip39_complete_word(mp_obj_t prefix)
|
|||||||
mp_buffer_info_t pfx;
|
mp_buffer_info_t pfx;
|
||||||
mp_get_buffer_raise(prefix, &pfx, MP_BUFFER_READ);
|
mp_get_buffer_raise(prefix, &pfx, MP_BUFFER_READ);
|
||||||
if (pfx.len == 0) {
|
if (pfx.len == 0) {
|
||||||
mp_raise_ValueError("Invalid word prefix");
|
return mp_obj_new_int(0xFFFFFFFF); // all letters
|
||||||
}
|
}
|
||||||
uint32_t res = 0;
|
uint32_t res = 0;
|
||||||
uint8_t bit;
|
uint8_t bit;
|
||||||
|
@ -106,7 +106,7 @@ int usb_hid_read(uint8_t iface_num, uint8_t *buf, uint32_t len) {
|
|||||||
static const char *ping_req = "PINGPING";
|
static const char *ping_req = "PINGPING";
|
||||||
static const char *ping_resp = "PONGPONG";
|
static const char *ping_resp = "PONGPONG";
|
||||||
if (r == strlen(ping_req) && memcmp(ping_req, buf, strlen(ping_req)) == 0) {
|
if (r == strlen(ping_req) && memcmp(ping_req, buf, strlen(ping_req)) == 0) {
|
||||||
usb_hid_write(0, (const uint8_t *)ping_resp, strlen(ping_resp));
|
ensure(usb_hid_write(0, (const uint8_t *)ping_resp, strlen(ping_resp)), "usb_hid_write");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
|
@ -26,13 +26,12 @@ async def request_pin(code: int = None) -> str:
|
|||||||
matrix = PinMatrix(label, with_zero=True)
|
matrix = PinMatrix(label, with_zero=True)
|
||||||
matrix.onchange = onchange
|
matrix.onchange = onchange
|
||||||
dialog = ConfirmDialog(matrix)
|
dialog = ConfirmDialog(matrix)
|
||||||
dialog.cancel.area = (0, 240 - 48, 80, 48)
|
dialog.cancel.area = ui.grid(12)
|
||||||
dialog.confirm.area = (240 - 80, 240 - 48, 80, 48)
|
dialog.confirm.area = ui.grid(14)
|
||||||
matrix.onchange()
|
matrix.onchange()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
result = await dialog
|
result = await dialog
|
||||||
|
|
||||||
if result == CONFIRMED:
|
if result == CONFIRMED:
|
||||||
return matrix.pin
|
return matrix.pin
|
||||||
elif result != CONFIRMED and matrix.pin:
|
elif result != CONFIRMED and matrix.pin:
|
||||||
|
30
src/apps/common/request_words.py
Normal 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
|
@ -24,8 +24,8 @@ def dispatch_WipeDevice(*args, **kwargs):
|
|||||||
|
|
||||||
@unimport
|
@unimport
|
||||||
def dispatch_RecoveryDevice(*args, **kwargs):
|
def dispatch_RecoveryDevice(*args, **kwargs):
|
||||||
from .recovery_device import layout_recovery_device
|
from .recovery_device import recovery_device
|
||||||
return layout_recovery_device(*args, **kwargs)
|
return recovery_device(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@unimport
|
@unimport
|
||||||
|
@ -1,23 +1,55 @@
|
|||||||
from trezor import ui, wire
|
from trezor import ui, wire
|
||||||
from trezor.utils import unimport
|
|
||||||
|
|
||||||
|
|
||||||
def nth(n):
|
async def recovery_device(ctx, msg):
|
||||||
if 4 <= n % 100 <= 20:
|
'''
|
||||||
sfx = 'th'
|
Recover BIP39 seed into empty device.
|
||||||
else:
|
|
||||||
sfx = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th')
|
1. Ask for the number of words in recovered seed.
|
||||||
return str(n) + sfx
|
2. Let user type in the mnemonic words one by one.
|
||||||
|
3. Optionally check the seed validity.
|
||||||
|
4. Optionally ask for the PIN, with confirmation.
|
||||||
|
5. Save into storage.
|
||||||
|
'''
|
||||||
|
from trezor import config
|
||||||
|
from trezor.crypto import bip39
|
||||||
|
from trezor.messages.FailureType import UnexpectedMessage, ProcessError
|
||||||
|
from trezor.messages.Success import Success
|
||||||
|
from trezor.ui.text import Text
|
||||||
|
from apps.common import storage
|
||||||
|
from apps.common.request_pin import request_pin
|
||||||
|
from apps.common.request_words import request_words
|
||||||
|
|
||||||
|
if storage.is_initialized():
|
||||||
|
raise wire.FailureError(UnexpectedMessage, 'Already initialized')
|
||||||
|
|
||||||
|
wordcount = await request_words(ctx,
|
||||||
|
Text('Device recovery', ui.ICON_RECOVERY,
|
||||||
|
'Number of words?'))
|
||||||
|
mnemonic = await request_mnemonic(wordcount, 'Type %s. word')
|
||||||
|
|
||||||
|
if msg.enforce_wordlist and not bip39.check(mnemonic):
|
||||||
|
raise wire.FailureError(ProcessError, 'Mnemonic is not valid')
|
||||||
|
|
||||||
|
if msg.pin_protection:
|
||||||
|
curpin = ''
|
||||||
|
newpin = await request_pin(ctx)
|
||||||
|
config.change_pin(curpin, newpin)
|
||||||
|
|
||||||
|
storage.load_settings(label=msg.label,
|
||||||
|
use_passphrase=msg.passphrase_protection)
|
||||||
|
storage.load_mnemonic(mnemonic)
|
||||||
|
return Success()
|
||||||
|
|
||||||
|
|
||||||
@unimport
|
async def request_mnemonic(count: int, prompt: str) -> str:
|
||||||
async def layout_recovery_device(ctx, msg):
|
from trezor.ui.keyboard import MnemonicKeyboard
|
||||||
|
|
||||||
msg = 'Please enter ' + nth(msg.word_count) + ' word'
|
words = []
|
||||||
|
board = MnemonicKeyboard()
|
||||||
|
for i in range(0, count):
|
||||||
|
board.prompt = prompt % (i + 1)
|
||||||
|
word = await board
|
||||||
|
words.append(word)
|
||||||
|
|
||||||
ui.display.clear()
|
return ' '.join(words)
|
||||||
ui.header('Recovery device', ui.ICON_RECOVERY, ui.BG, ui.LIGHT_GREEN)
|
|
||||||
ui.display.text(10, 74, msg, ui.BOLD, ui.FG, ui.BG)
|
|
||||||
ui.display.text(10, 104, 'of your mnemonic.', ui.BOLD, ui.FG, ui.BG)
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
|
@ -11,6 +11,7 @@ if __debug__:
|
|||||||
|
|
||||||
@unimport
|
@unimport
|
||||||
async def layout_reset_device(ctx, msg):
|
async def layout_reset_device(ctx, msg):
|
||||||
|
from trezor import config
|
||||||
from trezor.ui.text import Text
|
from trezor.ui.text import Text
|
||||||
from trezor.crypto import hashlib, random, bip39
|
from trezor.crypto import hashlib, random, bip39
|
||||||
from trezor.messages.EntropyRequest import EntropyRequest
|
from trezor.messages.EntropyRequest import EntropyRequest
|
||||||
@ -19,7 +20,7 @@ async def layout_reset_device(ctx, msg):
|
|||||||
from trezor.messages import ButtonRequestType
|
from trezor.messages import ButtonRequestType
|
||||||
from trezor.messages.wire_types import EntropyAck
|
from trezor.messages.wire_types import EntropyAck
|
||||||
|
|
||||||
from apps.common.request_pin import request_pin_twice
|
from apps.management.change_pin import request_pin_confirm
|
||||||
from apps.common.confirm import require_confirm
|
from apps.common.confirm import require_confirm
|
||||||
from apps.common import storage
|
from apps.common import storage
|
||||||
|
|
||||||
@ -42,9 +43,11 @@ async def layout_reset_device(ctx, msg):
|
|||||||
await require_confirm(ctx, entropy_content, ButtonRequestType.ResetDevice)
|
await require_confirm(ctx, entropy_content, ButtonRequestType.ResetDevice)
|
||||||
|
|
||||||
if msg.pin_protection:
|
if msg.pin_protection:
|
||||||
pin = await request_pin_twice(ctx)
|
curpin = ''
|
||||||
|
newpin = await request_pin_confirm(ctx)
|
||||||
else:
|
else:
|
||||||
pin = None
|
curpin = ''
|
||||||
|
newpin = ''
|
||||||
|
|
||||||
external_entropy_ack = await ctx.call(EntropyRequest(), EntropyAck)
|
external_entropy_ack = await ctx.call(EntropyRequest(), EntropyAck)
|
||||||
ehash = hashlib.sha256()
|
ehash = hashlib.sha256()
|
||||||
@ -55,11 +58,11 @@ async def layout_reset_device(ctx, msg):
|
|||||||
|
|
||||||
await show_mnemonic_by_word(ctx, mnemonic)
|
await show_mnemonic_by_word(ctx, mnemonic)
|
||||||
|
|
||||||
|
if curpin != newpin:
|
||||||
|
config.change_pin(curpin, newpin)
|
||||||
|
storage.load_settings(label=msg.label,
|
||||||
|
use_passphrase=msg.passphrase_protection)
|
||||||
storage.load_mnemonic(mnemonic)
|
storage.load_mnemonic(mnemonic)
|
||||||
storage.load_settings(pin=pin,
|
|
||||||
passphrase_protection=msg.passphrase_protection,
|
|
||||||
language=msg.language,
|
|
||||||
label=msg.label)
|
|
||||||
|
|
||||||
return Success(message='Initialized')
|
return Success(message='Initialized')
|
||||||
|
|
||||||
|
BIN
src/trezor/res/click.toig
Normal file
BIN
src/trezor/res/confirm2.toig
Normal file
BIN
src/trezor/res/cross2.toig
Normal file
BIN
src/trezor/res/left.toig
Normal file
BIN
src/trezor/res/recovery.toig
Normal file
BIN
src/trezor/res/send2.toig
Normal file
@ -106,11 +106,36 @@ 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,
|
||||||
display.bar(0, 0, 240, 32, bg)
|
icon: bytes=ICON_RESET,
|
||||||
|
fg: int=BG,
|
||||||
|
bg: int=BG,
|
||||||
|
ifg: int=BG):
|
||||||
if icon is not None:
|
if icon is not None:
|
||||||
display.icon(8, 4, res.load(icon), fg, bg)
|
display.icon(14, 18, res.load(icon), ifg, bg)
|
||||||
display.text(8 + 24 + 2, 24, title, BOLD, fg, bg)
|
display.text(44, 35, title, BOLD, fg, bg)
|
||||||
|
|
||||||
|
|
||||||
|
VIEWX = const(6)
|
||||||
|
VIEWY = const(9)
|
||||||
|
VIEW = const(228) # SCREEN - 2 * VIEWX
|
||||||
|
|
||||||
|
|
||||||
|
def grid(i: int,
|
||||||
|
n_x: int=3,
|
||||||
|
n_y: int=5,
|
||||||
|
start_x: int=VIEWX,
|
||||||
|
start_y: int=VIEWY,
|
||||||
|
end_x: int=(VIEWX + VIEW),
|
||||||
|
end_y: int=(VIEWY + VIEW),
|
||||||
|
cells_x: int=1,
|
||||||
|
cells_y: int=1,
|
||||||
|
spacing: int=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) * cells_x, (h - spacing) * cells_y)
|
||||||
|
|
||||||
|
|
||||||
class Widget:
|
class Widget:
|
||||||
|
@ -15,6 +15,9 @@ BTN_ACTIVE = const(2)
|
|||||||
BTN_DIRTY = const(4)
|
BTN_DIRTY = const(4)
|
||||||
BTN_DISABLED = const(8)
|
BTN_DISABLED = const(8)
|
||||||
|
|
||||||
|
ICON = const(16) # icon size in pixels
|
||||||
|
BORDER = const(4) # border size in pixels
|
||||||
|
|
||||||
|
|
||||||
class Button(Widget):
|
class Button(Widget):
|
||||||
|
|
||||||
@ -25,18 +28,20 @@ class Button(Widget):
|
|||||||
absolute=False):
|
absolute=False):
|
||||||
self.area = area
|
self.area = area
|
||||||
self.content = content
|
self.content = content
|
||||||
self.normal_style = normal_style or ui.BTN_DEFAULT
|
self.normal_style = normal_style or ui.BTN_KEY
|
||||||
self.active_style = active_style or ui.BTN_DEFAULT_ACTIVE
|
self.active_style = active_style or ui.BTN_KEY_ACTIVE
|
||||||
self.disabled_style = disabled_style or ui.BTN_DEFAULT_DISABLED
|
self.disabled_style = disabled_style or ui.BTN_KEY_DISABLED
|
||||||
self.absolute = absolute
|
self.absolute = absolute
|
||||||
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
|
||||||
@ -52,46 +57,66 @@ class Button(Widget):
|
|||||||
else:
|
else:
|
||||||
s = self.normal_style
|
s = self.normal_style
|
||||||
ax, ay, aw, ah = self.area
|
ax, ay, aw, ah = self.area
|
||||||
tx = ax + aw // 2
|
|
||||||
ty = ay + ah // 2 + 8
|
|
||||||
display.bar_radius(ax, ay, aw, ah,
|
|
||||||
s['border-color'],
|
|
||||||
ui.BG,
|
|
||||||
s['radius'])
|
|
||||||
display.bar_radius(ax + 4, ay + 4, aw - 8, ah - 8,
|
|
||||||
s['bg-color'],
|
|
||||||
s['border-color'],
|
|
||||||
s['radius'])
|
|
||||||
|
|
||||||
if isinstance(self.content, str):
|
|
||||||
display.text_center(tx, ty, self.content,
|
|
||||||
s['text-style'],
|
|
||||||
s['fg-color'],
|
|
||||||
s['bg-color'])
|
|
||||||
|
|
||||||
else:
|
|
||||||
display.icon(tx - 15, ty - 20, self.content,
|
|
||||||
s['fg-color'],
|
|
||||||
s['bg-color'])
|
|
||||||
|
|
||||||
|
self.render_background(s, ax, ay, aw, ah)
|
||||||
|
self.render_content(s, ax, ay, aw, ah)
|
||||||
self.state = state
|
self.state = state
|
||||||
|
|
||||||
|
def render_background(self, s, ax, ay, aw, ah):
|
||||||
|
radius = s['radius']
|
||||||
|
bg_color = s['bg-color']
|
||||||
|
border_color = s['border-color']
|
||||||
|
if border_color != bg_color:
|
||||||
|
# render border and background on top of it
|
||||||
|
display.bar_radius(ax, ay,
|
||||||
|
aw, ah,
|
||||||
|
border_color,
|
||||||
|
ui.BG,
|
||||||
|
radius)
|
||||||
|
display.bar_radius(ax + BORDER, ay + BORDER,
|
||||||
|
aw - BORDER*2, ah - BORDER*2,
|
||||||
|
bg_color,
|
||||||
|
border_color,
|
||||||
|
radius)
|
||||||
|
else:
|
||||||
|
# render only the background
|
||||||
|
display.bar_radius(ax, ay,
|
||||||
|
aw, ah,
|
||||||
|
bg_color,
|
||||||
|
ui.BG,
|
||||||
|
radius)
|
||||||
|
|
||||||
|
def render_content(self, s, ax, ay, aw, ah):
|
||||||
|
c = self.content
|
||||||
|
tx = ax + aw//2
|
||||||
|
ty = ay + ah//2 + 8
|
||||||
|
if isinstance(c, str):
|
||||||
|
display.text_center(
|
||||||
|
tx, ty, c, s['text-style'], s['fg-color'], s['bg-color'])
|
||||||
|
else:
|
||||||
|
display.icon(
|
||||||
|
tx - ICON//2, ty - ICON, c, s['fg-color'], s['bg-color'])
|
||||||
|
|
||||||
def touch(self, event, pos):
|
def touch(self, event, pos):
|
||||||
if self.state & BTN_DISABLED:
|
state = self.state
|
||||||
|
if state & BTN_DISABLED:
|
||||||
return
|
return
|
||||||
if not self.absolute:
|
if not self.absolute:
|
||||||
pos = rotate(pos)
|
pos = rotate(pos)
|
||||||
|
|
||||||
if event == io.TOUCH_START:
|
if event == io.TOUCH_START:
|
||||||
if contains(self.area, pos):
|
if contains(self.area, pos):
|
||||||
self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE
|
self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE
|
||||||
elif event == io.TOUCH_MOVE and self.state & BTN_STARTED:
|
|
||||||
|
elif event == io.TOUCH_MOVE and state & BTN_STARTED:
|
||||||
if contains(self.area, pos):
|
if contains(self.area, pos):
|
||||||
if not self.state & BTN_ACTIVE:
|
if not state & BTN_ACTIVE:
|
||||||
self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE
|
self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE
|
||||||
else:
|
else:
|
||||||
if self.state & BTN_ACTIVE:
|
if state & BTN_ACTIVE:
|
||||||
self.state = BTN_STARTED | BTN_DIRTY
|
self.state = BTN_STARTED | BTN_DIRTY
|
||||||
elif event == io.TOUCH_END and self.state & BTN_STARTED:
|
|
||||||
|
elif event == io.TOUCH_END and state & BTN_STARTED:
|
||||||
self.state = BTN_DIRTY
|
self.state = BTN_DIRTY
|
||||||
if contains(self.area, pos):
|
if contains(self.area, pos):
|
||||||
return BTN_CLICKED
|
return BTN_CLICKED
|
||||||
|
@ -16,17 +16,17 @@ class ConfirmDialog(Widget):
|
|||||||
def __init__(self, content, confirm=DEFAULT_CONFIRM, cancel=DEFAULT_CANCEL):
|
def __init__(self, content, confirm=DEFAULT_CONFIRM, cancel=DEFAULT_CANCEL):
|
||||||
self.content = content
|
self.content = content
|
||||||
if cancel is not None:
|
if cancel is not None:
|
||||||
self.confirm = Button((121, 240 - 48, 119, 48), confirm,
|
self.confirm = Button(ui.grid(8, n_x=2), confirm,
|
||||||
normal_style=ui.BTN_CONFIRM,
|
normal_style=ui.BTN_CONFIRM,
|
||||||
active_style=ui.BTN_CONFIRM_ACTIVE)
|
active_style=ui.BTN_CONFIRM_ACTIVE)
|
||||||
self.cancel = Button((0, 240 - 48, 119, 48), cancel,
|
self.cancel = Button(ui.grid(9, n_x=2), cancel,
|
||||||
normal_style=ui.BTN_CANCEL,
|
normal_style=ui.BTN_CANCEL,
|
||||||
active_style=ui.BTN_CANCEL_ACTIVE)
|
active_style=ui.BTN_CANCEL_ACTIVE)
|
||||||
else:
|
else:
|
||||||
self.cancel = None
|
self.confirm = Button(ui.grid(4, n_x=1), confirm,
|
||||||
self.confirm = Button((0, 240 - 48, 240, 48), confirm,
|
|
||||||
normal_style=ui.BTN_CONFIRM,
|
normal_style=ui.BTN_CONFIRM,
|
||||||
active_style=ui.BTN_CONFIRM_ACTIVE)
|
active_style=ui.BTN_CONFIRM_ACTIVE)
|
||||||
|
self.cancel = None
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
self.confirm.render()
|
self.confirm.render()
|
||||||
@ -52,7 +52,7 @@ class HoldToConfirmDialog(Widget):
|
|||||||
|
|
||||||
def __init__(self, content, hold='Hold to confirm', *args, **kwargs):
|
def __init__(self, content, hold='Hold to confirm', *args, **kwargs):
|
||||||
self.content = content
|
self.content = content
|
||||||
self.button = Button((0, 240 - 48, 240, 48), hold,
|
self.button = Button(ui.grid(4, n_x=1), hold,
|
||||||
normal_style=ui.BTN_CONFIRM,
|
normal_style=ui.BTN_CONFIRM,
|
||||||
active_style=ui.BTN_CONFIRM_ACTIVE)
|
active_style=ui.BTN_CONFIRM_ACTIVE)
|
||||||
self.loader = Loader(*args, **kwargs)
|
self.loader = Loader(*args, **kwargs)
|
||||||
@ -66,6 +66,7 @@ class HoldToConfirmDialog(Widget):
|
|||||||
button.touch(event, pos)
|
button.touch(event, pos)
|
||||||
is_started = button.state & BTN_STARTED and button.state & BTN_ACTIVE
|
is_started = button.state & BTN_STARTED and button.state & BTN_ACTIVE
|
||||||
if is_started and not was_started:
|
if is_started and not was_started:
|
||||||
|
ui.display.clear()
|
||||||
self.loader.start()
|
self.loader.start()
|
||||||
return _STARTED
|
return _STARTED
|
||||||
if was_started and not is_started:
|
if was_started and not is_started:
|
||||||
|
@ -1,26 +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=0, start_y=40, end_x=240, end_y=240 - 48, 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) 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: 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
|
||||||
@ -30,199 +22,156 @@ 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=''):
|
def edit(self, content: str, word: str, pending: bool):
|
||||||
self.content = content
|
self.content = content
|
||||||
self.sugg_mask = 0xffffffff
|
self.word = word
|
||||||
self.sugg_word = None
|
self.pending = pending
|
||||||
self.pending_button = None
|
self.taint()
|
||||||
self.pending_index = 0
|
if content == word: # confirm button
|
||||||
|
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((5, 5, 240 - 35, 30), '')
|
text_style = s['text-style']
|
||||||
self.bs_button = Button((240 - 35, 5, 30, 30),
|
fg_color = s['fg-color']
|
||||||
res.load('trezor/res/pin_close.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 + 2, pw + 1, 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.input.content:
|
||||||
# clear canvas under input line
|
# content button and backspace
|
||||||
display.bar(0, 0, 205, 40, ui.BG)
|
self.input.render()
|
||||||
|
self.back.render()
|
||||||
# input line
|
|
||||||
content_width = display.text_width(self.content, ui.BOLD)
|
|
||||||
display.text(20, 30, 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:
|
|
||||||
self.bs_button.render()
|
|
||||||
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.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 and self.sugg_word is not None:
|
|
||||||
self.content = self.sugg_word
|
if self.input.touch(event, pos) == BTN_CLICKED:
|
||||||
self.pending_button = None
|
# input press, either auto-complete or confirm
|
||||||
self.pending_index = 0
|
if content == word:
|
||||||
self._update_suggestion()
|
self.edit('')
|
||||||
self._update_buttons()
|
return content
|
||||||
return
|
else:
|
||||||
for btn in self.key_buttons:
|
self.edit(word)
|
||||||
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 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()
|
||||||
|
|
||||||
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 = loop.wait(touch, timeout)
|
wait_timeout = loop.wait(touch, timeout)
|
||||||
while True:
|
wait_touch = loop.wait(touch)
|
||||||
|
content = None
|
||||||
|
|
||||||
|
self.back.taint()
|
||||||
|
self.input.taint()
|
||||||
|
|
||||||
|
while content is None:
|
||||||
self.render()
|
self.render()
|
||||||
result = yield wait
|
if self.pbutton is not None:
|
||||||
if touch in wait.finished:
|
wait = wait_timeout
|
||||||
event, *pos = result
|
|
||||||
self.touch(event, pos)
|
|
||||||
else:
|
else:
|
||||||
self.pending_button = None
|
wait = wait_touch
|
||||||
self.pending_index = 0
|
result = await wait
|
||||||
self._update_suggestion()
|
|
||||||
self._update_buttons()
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
if touch in wait.finished:
|
||||||
event, *pos = result
|
event, *pos = result
|
||||||
self.touch(event, pos)
|
content = self.touch(event, pos)
|
||||||
elif self.zoom_buttons:
|
else:
|
||||||
self.zoom_buttons = None
|
if self.input.word:
|
||||||
for btn in self.key_buttons:
|
# just reset the pending state
|
||||||
btn.taint()
|
self.edit(self.input.content)
|
||||||
|
else:
|
||||||
|
# invalid character, backspace it
|
||||||
Keyboard = KeyboardMultiTap
|
self.edit(self.input.content[:-1])
|
||||||
|
return content
|
||||||
|
@ -37,13 +37,13 @@ class Loader(ui.Widget):
|
|||||||
s = self.normal_style
|
s = self.normal_style
|
||||||
if s['icon'] is None:
|
if s['icon'] is None:
|
||||||
ui.display.loader(
|
ui.display.loader(
|
||||||
progress, -8, s['fg-color'], s['bg-color'])
|
progress, -24, s['fg-color'], s['bg-color'])
|
||||||
elif s['icon-fg-color'] is None:
|
elif s['icon-fg-color'] is None:
|
||||||
ui.display.loader(
|
ui.display.loader(
|
||||||
progress, -8, s['fg-color'], s['bg-color'], res.load(s['icon']))
|
progress, -24, s['fg-color'], s['bg-color'], res.load(s['icon']))
|
||||||
else:
|
else:
|
||||||
ui.display.loader(
|
ui.display.loader(
|
||||||
progress, -8, s['fg-color'], s['bg-color'], res.load(s['icon']), s['icon-fg-color'])
|
progress, -24, s['fg-color'], s['bg-color'], res.load(s['icon']), s['icon-fg-color'])
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
sleep = loop.sleep(1000000 // 60) # 60 fps
|
sleep = loop.sleep(1000000 // 60) # 60 fps
|
||||||
|
@ -6,14 +6,9 @@ from trezor.ui.button import Button, BTN_CLICKED
|
|||||||
|
|
||||||
|
|
||||||
def digit_area(i):
|
def digit_area(i):
|
||||||
width = const(80)
|
|
||||||
height = const(48)
|
|
||||||
if i == 9: # 0-position
|
if i == 9: # 0-position
|
||||||
i = 10 # display it in the middle
|
i = 10 # display it in the middle
|
||||||
x = (i % 3) * width
|
return ui.grid(i + 3) # skip the first line
|
||||||
y = (i // 3) * height
|
|
||||||
# 48px is offset of input line, -1px is the border size
|
|
||||||
return (x, y + 48, width - 1, height - 1)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_digits(with_zero):
|
def generate_digits(with_zero):
|
||||||
@ -36,19 +31,18 @@ class PinMatrix(ui.Widget):
|
|||||||
# we lay out the buttons top-left to bottom-right, but the order of the
|
# we lay out the buttons top-left to bottom-right, but the order of the
|
||||||
# digits is defined as bottom-left to top-right (on numpad)
|
# digits is defined as bottom-left to top-right (on numpad)
|
||||||
reordered_digits = self.digits[6:] + self.digits[3:6] + self.digits[:3]
|
reordered_digits = self.digits[6:] + self.digits[3:6] + self.digits[:3]
|
||||||
|
|
||||||
self.pin_buttons = [Button(digit_area(i), str(d))
|
self.pin_buttons = [Button(digit_area(i), str(d))
|
||||||
for i, d in enumerate(reordered_digits)]
|
for i, d in enumerate(reordered_digits)]
|
||||||
self.onchange = None
|
self.onchange = None
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
|
|
||||||
header = '*' * len(self.pin) if self.pin else self.label
|
|
||||||
|
|
||||||
# clear canvas under input line
|
# clear canvas under input line
|
||||||
display.bar(0, 0, 205, 48, ui.BG)
|
display.bar(0, 0, 205, 48, ui.BG)
|
||||||
|
|
||||||
# input line with a header
|
# input line with a header
|
||||||
display.text_center(120, 30, header, ui.NORMAL, ui.blend(ui.BG, ui.FG, 0.5), ui.BG)
|
header = '*' * len(self.pin) if self.pin else self.label
|
||||||
|
display.text_center(120, 36, header, ui.BOLD, ui.GREY, ui.BG)
|
||||||
|
|
||||||
# pin matrix buttons
|
# pin matrix buttons
|
||||||
for btn in self.pin_buttons:
|
for btn in self.pin_buttons:
|
||||||
|
@ -12,9 +12,7 @@ BACKLIGHT_NONE = const(2)
|
|||||||
BACKLIGHT_MAX = const(255)
|
BACKLIGHT_MAX = const(255)
|
||||||
|
|
||||||
# color palette
|
# color palette
|
||||||
LIGHT_RED = rgb(0xFF, 0x00, 0x00)
|
RED = rgb(0xFF, 0x00, 0x00)
|
||||||
RED = rgb(0xE4, 0x57, 0x2E) # RED E4572E
|
|
||||||
ACTIVE_RED = rgb(0xA6, 0x40, 0x22) # ACTIVE DARK RED A64022
|
|
||||||
PINK = rgb(0xE9, 0x1E, 0x63)
|
PINK = rgb(0xE9, 0x1E, 0x63)
|
||||||
PURPLE = rgb(0x9C, 0x27, 0xB0)
|
PURPLE = rgb(0x9C, 0x27, 0xB0)
|
||||||
DEEP_PURPLE = rgb(0x67, 0x3A, 0xB7)
|
DEEP_PURPLE = rgb(0x67, 0x3A, 0xB7)
|
||||||
@ -23,8 +21,7 @@ BLUE = rgb(0x21, 0x96, 0xF3)
|
|||||||
LIGHT_BLUE = rgb(0x03, 0xA9, 0xF4)
|
LIGHT_BLUE = rgb(0x03, 0xA9, 0xF4)
|
||||||
CYAN = rgb(0x00, 0xBC, 0xD4)
|
CYAN = rgb(0x00, 0xBC, 0xD4)
|
||||||
TEAL = rgb(0x00, 0x96, 0x88)
|
TEAL = rgb(0x00, 0x96, 0x88)
|
||||||
GREEN = rgb(0x4C, 0xC1, 0x48) # GREEN 4CC148
|
GREEN = rgb(0x00, 0xAE, 0x0B)
|
||||||
ACTIVE_GREEN = rgb(0x1A, 0x8C, 0x14) # ACTIVE DARK GREEN 1A8C14
|
|
||||||
LIGHT_GREEN = rgb(0x87, 0xCE, 0x26)
|
LIGHT_GREEN = rgb(0x87, 0xCE, 0x26)
|
||||||
LIME = rgb(0xCD, 0xDC, 0x39)
|
LIME = rgb(0xCD, 0xDC, 0x39)
|
||||||
YELLOW = rgb(0xFF, 0xEB, 0x3B)
|
YELLOW = rgb(0xFF, 0xEB, 0x3B)
|
||||||
@ -38,7 +35,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 +47,14 @@ 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/recovery.toig'
|
||||||
ICON_CLEAR = 'trezor/res/clear.toig'
|
ICON_CLEAR = 'trezor/res/cross2.toig'
|
||||||
ICON_CONFIRM = 'trezor/res/confirm.toig'
|
ICON_CONFIRM = 'trezor/res/confirm2.toig'
|
||||||
|
ICON_CONFIRM2 = 'trezor/res/confirm.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'
|
||||||
|
ICON_BACK = 'trezor/res/left.toig'
|
||||||
|
|
||||||
# buttons
|
# buttons
|
||||||
BTN_DEFAULT = {
|
BTN_DEFAULT = {
|
||||||
@ -76,24 +79,24 @@ BTN_DEFAULT_DISABLED = {
|
|||||||
'radius': RADIUS,
|
'radius': RADIUS,
|
||||||
}
|
}
|
||||||
BTN_CANCEL = {
|
BTN_CANCEL = {
|
||||||
'bg-color': LIGHT_RED,
|
'bg-color': RED,
|
||||||
'fg-color': FG,
|
'fg-color': FG,
|
||||||
'text-style': BOLD,
|
'text-style': BOLD,
|
||||||
'border-color': LIGHT_RED,
|
'border-color': BG,
|
||||||
'radius': RADIUS,
|
'radius': RADIUS,
|
||||||
}
|
}
|
||||||
BTN_CANCEL_ACTIVE = {
|
BTN_CANCEL_ACTIVE = {
|
||||||
'bg-color': FG,
|
'bg-color': FG,
|
||||||
'fg-color': LIGHT_RED,
|
'fg-color': RED,
|
||||||
'text-style': BOLD,
|
'text-style': BOLD,
|
||||||
'border-color': LIGHT_RED,
|
'border-color': FG,
|
||||||
'radius': RADIUS,
|
'radius': RADIUS,
|
||||||
}
|
}
|
||||||
BTN_CONFIRM = {
|
BTN_CONFIRM = {
|
||||||
'bg-color': GREEN,
|
'bg-color': GREEN,
|
||||||
'fg-color': FG,
|
'fg-color': FG,
|
||||||
'text-style': BOLD,
|
'text-style': BOLD,
|
||||||
'border-color': GREEN,
|
'border-color': BG,
|
||||||
'radius': RADIUS,
|
'radius': RADIUS,
|
||||||
}
|
}
|
||||||
BTN_CONFIRM_ACTIVE = {
|
BTN_CONFIRM_ACTIVE = {
|
||||||
@ -104,7 +107,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,
|
||||||
@ -122,18 +125,27 @@ BTN_KEY = {
|
|||||||
'fg-color': FG,
|
'fg-color': FG,
|
||||||
'text-style': MONO,
|
'text-style': MONO,
|
||||||
'border-color': BG,
|
'border-color': BG,
|
||||||
|
'radius': RADIUS,
|
||||||
}
|
}
|
||||||
BTN_KEY_ACTIVE = {
|
BTN_KEY_ACTIVE = {
|
||||||
'bg-color': GREY,
|
'bg-color': FG,
|
||||||
'fg-color': BG,
|
'fg-color': BLACKISH,
|
||||||
'text-style': MONO,
|
'text-style': MONO,
|
||||||
'border-color': GREY,
|
'border-color': FG,
|
||||||
|
'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 = {
|
||||||
'bg-color': BG,
|
'bg-color': BG,
|
||||||
'fg-color': FG,
|
'fg-color': GREEN,
|
||||||
'icon': None,
|
'icon': None,
|
||||||
'icon-fg-color': None,
|
'icon-fg-color': None,
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
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(48)
|
||||||
TEXT_LINE_HEIGHT = const(23)
|
TEXT_LINE_HEIGHT = const(26)
|
||||||
TEXT_MARGIN_LEFT = const(10)
|
TEXT_MARGIN_LEFT = const(14)
|
||||||
|
|
||||||
|
|
||||||
class Text(ui.Widget):
|
class Text(ui.Widget):
|
||||||
|
|
||||||
def __init__(self, header_text, header_icon, *content):
|
def __init__(self, header_text, header_icon, *content, icon_color=ui.ORANGE_ICON):
|
||||||
self.header_text = header_text
|
self.header_text = header_text
|
||||||
self.header_icon = header_icon
|
self.header_icon = header_icon
|
||||||
|
self.icon_color = icon_color
|
||||||
self.content = content
|
self.content = content
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
@ -19,7 +20,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, self.icon_color)
|
||||||
|
|
||||||
for item in self.content:
|
for item in self.content:
|
||||||
if isinstance(item, str):
|
if isinstance(item, str):
|
||||||
|
47
src/trezor/ui/word_select.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
from micropython import const
|
||||||
|
from trezor import loop
|
||||||
|
from trezor import ui
|
||||||
|
from trezor.ui import Widget
|
||||||
|
from trezor.ui.button import Button, BTN_CLICKED
|
||||||
|
|
||||||
|
_W12 = const(12)
|
||||||
|
_W15 = const(15)
|
||||||
|
_W18 = const(18)
|
||||||
|
_W24 = const(24)
|
||||||
|
|
||||||
|
|
||||||
|
class WordSelector(Widget):
|
||||||
|
|
||||||
|
def __init__(self, content):
|
||||||
|
self.content = content
|
||||||
|
self.w12 = Button(ui.grid(8, n_y=4, n_x=4, cells_x=2), str(_W12),
|
||||||
|
normal_style=ui.BTN_KEY,
|
||||||
|
active_style=ui.BTN_KEY_ACTIVE)
|
||||||
|
self.w15 = Button(ui.grid(10, n_y=4, n_x=4, cells_x=2), str(_W15),
|
||||||
|
normal_style=ui.BTN_KEY,
|
||||||
|
active_style=ui.BTN_KEY_ACTIVE)
|
||||||
|
self.w18 = Button(ui.grid(12, n_y=4, n_x=4, cells_x=2), str(_W18),
|
||||||
|
normal_style=ui.BTN_KEY,
|
||||||
|
active_style=ui.BTN_KEY_ACTIVE)
|
||||||
|
self.w24 = Button(ui.grid(14, n_y=4, n_x=4, cells_x=2), str(_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)
|