mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-16 17:42:02 +00:00
core: revamp the UI subsystem
This commit is contained in:
parent
2f4c123466
commit
36534325f0
@ -1,13 +1,12 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import ui
|
||||
from trezor.messages import ButtonRequestType, MessageType
|
||||
from trezor.messages.ButtonRequest import ButtonRequest
|
||||
from trezor.ui.confirm import CONFIRMED, ConfirmDialog, HoldToConfirmDialog
|
||||
from trezor.ui.scroll import Scrollpage, animate_swipe, paginate
|
||||
from trezor.ui.scroll import Paginated
|
||||
from trezor.ui.text import Text
|
||||
from trezor.utils import chunks, format_amount
|
||||
|
||||
from apps.common.confirm import confirm, hold_to_confirm
|
||||
|
||||
|
||||
def format_coin_amount(amount):
|
||||
return "%s %s" % (format_amount(amount, 6), "ADA")
|
||||
@ -16,107 +15,34 @@ def format_coin_amount(amount):
|
||||
async def confirm_sending(ctx, amount, to):
|
||||
to_lines = list(chunks(to, 17))
|
||||
|
||||
t1 = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t1.normal("Confirm sending:")
|
||||
t1.bold(format_coin_amount(amount))
|
||||
t1.normal("to:")
|
||||
t1.bold(to_lines[0])
|
||||
pages = [t1]
|
||||
|
||||
LINES_PER_PAGE = 4
|
||||
PER_PAGE = const(4)
|
||||
pages = [t1]
|
||||
if len(to_lines) > 1:
|
||||
to_pages = list(chunks(to_lines[1:], LINES_PER_PAGE))
|
||||
to_pages = list(chunks(to_lines[1:], PER_PAGE))
|
||||
for page in to_pages:
|
||||
t = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
t = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
for line in page:
|
||||
t.bold(line)
|
||||
pages.append(t)
|
||||
|
||||
await ctx.call(ButtonRequest(code=ButtonRequestType.Other), MessageType.ButtonAck)
|
||||
|
||||
paginator = paginate(create_renderer(ConfirmDialog), len(pages), const(0), pages)
|
||||
return await ctx.wait(paginator) == CONFIRMED
|
||||
return await confirm(ctx, Paginated(pages))
|
||||
|
||||
|
||||
async def confirm_transaction(ctx, amount, fee, network_name):
|
||||
t1 = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t1.normal("Total amount:")
|
||||
t1.bold(format_coin_amount(amount))
|
||||
t1.normal("including fee:")
|
||||
t1.bold(format_coin_amount(fee))
|
||||
|
||||
t2 = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
t2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t2.normal("Network:")
|
||||
t2.bold(network_name)
|
||||
|
||||
pages = [t1, t2]
|
||||
|
||||
await ctx.call(ButtonRequest(code=ButtonRequestType.Other), MessageType.ButtonAck)
|
||||
|
||||
paginator = paginate(
|
||||
create_renderer(HoldToConfirmDialog), len(pages), const(0), pages
|
||||
)
|
||||
return await ctx.wait(paginator) == CONFIRMED
|
||||
|
||||
|
||||
def create_renderer(confirmation_wrapper):
|
||||
@ui.layout
|
||||
async def page_renderer(page: int, page_count: int, pages: list):
|
||||
# for some reason page index can be equal to page count
|
||||
if page >= page_count:
|
||||
page = page_count - 1
|
||||
|
||||
pages[page].taint()
|
||||
content = Scrollpage(pages[page], page, page_count)
|
||||
if page + 1 >= page_count:
|
||||
return await confirmation_wrapper(content)
|
||||
else:
|
||||
content.render()
|
||||
await animate_swipe()
|
||||
|
||||
return page_renderer
|
||||
|
||||
|
||||
async def confirm_with_pagination(
|
||||
ctx, content, title: str, icon=ui.ICON_RESET, icon_color=ui.ORANGE
|
||||
):
|
||||
first_page = const(0)
|
||||
lines_per_page = const(4)
|
||||
|
||||
if isinstance(content, (list, tuple)):
|
||||
lines = content
|
||||
else:
|
||||
lines = list(chunks(content, 17))
|
||||
pages = list(chunks(lines, lines_per_page))
|
||||
|
||||
await ctx.call(ButtonRequest(code=ButtonRequestType.Address), MessageType.ButtonAck)
|
||||
|
||||
paginator = paginate(
|
||||
show_text_page, len(pages), first_page, pages, title, icon, icon_color
|
||||
)
|
||||
return await ctx.wait(paginator) == CONFIRMED
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def show_text_page(
|
||||
page: int,
|
||||
page_count: int,
|
||||
pages: list,
|
||||
title: str,
|
||||
icon=ui.ICON_RESET,
|
||||
icon_color=ui.ORANGE,
|
||||
):
|
||||
if page_count == 1:
|
||||
page = 0
|
||||
|
||||
lines = pages[page]
|
||||
content = Text(title, icon, icon_color=icon_color)
|
||||
content.mono(*lines)
|
||||
|
||||
content = Scrollpage(content, page, page_count)
|
||||
|
||||
if page + 1 >= page_count:
|
||||
return await ConfirmDialog(content)
|
||||
else:
|
||||
content.render()
|
||||
await animate_swipe()
|
||||
return await hold_to_confirm(ctx, Paginated([t1, t2]))
|
||||
|
@ -1,29 +1,59 @@
|
||||
from trezor import ui, wire
|
||||
from trezor import wire
|
||||
from trezor.messages import ButtonRequestType, MessageType
|
||||
from trezor.messages.ButtonRequest import ButtonRequest
|
||||
from trezor.ui.confirm import CONFIRMED, ConfirmDialog, HoldToConfirmDialog
|
||||
from trezor.ui.confirm import CONFIRMED, Confirm, HoldToConfirm
|
||||
|
||||
if __debug__:
|
||||
from apps.debug import confirm_signal
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def confirm(ctx, content, code=None, *args, **kwargs):
|
||||
if code is None:
|
||||
code = ButtonRequestType.Other
|
||||
async def confirm(
|
||||
ctx,
|
||||
content,
|
||||
code=ButtonRequestType.Other,
|
||||
confirm=Confirm.DEFAULT_CONFIRM,
|
||||
confirm_style=Confirm.DEFAULT_CONFIRM_STYLE,
|
||||
cancel=Confirm.DEFAULT_CANCEL,
|
||||
cancel_style=Confirm.DEFAULT_CANCEL_STYLE,
|
||||
):
|
||||
await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck)
|
||||
|
||||
dialog = ConfirmDialog(content, *args, **kwargs)
|
||||
if content.__class__.__name__ == "Paginated":
|
||||
content.pages[-1] = Confirm(
|
||||
content.pages[-1], confirm, confirm_style, cancel, cancel_style
|
||||
)
|
||||
dialog = content
|
||||
else:
|
||||
dialog = Confirm(content, confirm, confirm_style, cancel, cancel_style)
|
||||
|
||||
return await ctx.wait(dialog) == CONFIRMED
|
||||
if __debug__:
|
||||
return await ctx.wait(dialog, confirm_signal) is CONFIRMED
|
||||
else:
|
||||
return await ctx.wait(dialog) is CONFIRMED
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def hold_to_confirm(ctx, content, code=None, *args, **kwargs):
|
||||
if code is None:
|
||||
code = ButtonRequestType.Other
|
||||
async def hold_to_confirm(
|
||||
ctx,
|
||||
content,
|
||||
code=ButtonRequestType.Other,
|
||||
confirm=HoldToConfirm.DEFAULT_CONFIRM,
|
||||
confirm_style=HoldToConfirm.DEFAULT_CONFIRM_STYLE,
|
||||
loader_style=HoldToConfirm.DEFAULT_LOADER_STYLE,
|
||||
):
|
||||
await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck)
|
||||
|
||||
dialog = HoldToConfirmDialog(content, "Hold to confirm", *args, **kwargs)
|
||||
if content.__class__.__name__ == "Paginated":
|
||||
content.pages[-1] = HoldToConfirm(
|
||||
content.pages[-1], confirm, confirm_style, loader_style
|
||||
)
|
||||
dialog = content
|
||||
else:
|
||||
dialog = HoldToConfirm(content, confirm, confirm_style, loader_style)
|
||||
|
||||
return await ctx.wait(dialog) == CONFIRMED
|
||||
if __debug__:
|
||||
return await ctx.wait(dialog, confirm_signal) is CONFIRMED
|
||||
else:
|
||||
return await ctx.wait(dialog) is CONFIRMED
|
||||
|
||||
|
||||
async def require_confirm(*args, **kwargs):
|
||||
|
@ -3,6 +3,7 @@ from ubinascii import hexlify
|
||||
|
||||
from trezor import ui
|
||||
from trezor.messages import ButtonRequestType
|
||||
from trezor.ui.button import ButtonDefault
|
||||
from trezor.ui.container import Container
|
||||
from trezor.ui.qr import Qr
|
||||
from trezor.ui.text import Text
|
||||
@ -12,42 +13,45 @@ from apps.common import HARDENED
|
||||
from apps.common.confirm import confirm, require_confirm
|
||||
|
||||
|
||||
async def show_address(ctx, address: str, desc: str = None, network: str = None):
|
||||
text = Text(
|
||||
desc if desc else "Confirm address", ui.ICON_RECEIVE, icon_color=ui.GREEN
|
||||
)
|
||||
if network:
|
||||
async def show_address(
|
||||
ctx, address: str, desc: str = "Confirm address", network: str = None
|
||||
):
|
||||
text = Text(desc, ui.ICON_RECEIVE, ui.GREEN)
|
||||
if network is not None:
|
||||
text.normal("%s network" % network)
|
||||
text.mono(*split_address(address))
|
||||
|
||||
return await confirm(
|
||||
ctx, text, code=ButtonRequestType.Address, cancel="QR", cancel_style=ui.BTN_KEY
|
||||
ctx,
|
||||
text,
|
||||
code=ButtonRequestType.Address,
|
||||
cancel="QR",
|
||||
cancel_style=ButtonDefault,
|
||||
)
|
||||
|
||||
|
||||
async def show_qr(ctx, address: str, desc: str = None):
|
||||
qr_x = const(120)
|
||||
qr_y = const(115)
|
||||
qr_coef = const(4)
|
||||
|
||||
qr = Qr(address, (qr_x, qr_y), qr_coef)
|
||||
text = Text(
|
||||
desc if desc else "Confirm address", ui.ICON_RECEIVE, icon_color=ui.GREEN
|
||||
)
|
||||
async def show_qr(ctx, address: str, desc: str = "Confirm address"):
|
||||
QR_X = const(120)
|
||||
QR_Y = const(115)
|
||||
QR_COEF = const(4)
|
||||
qr = Qr(address, QR_X, QR_Y, QR_COEF)
|
||||
text = Text(desc, ui.ICON_RECEIVE, ui.GREEN)
|
||||
content = Container(qr, text)
|
||||
|
||||
return await confirm(
|
||||
ctx,
|
||||
content,
|
||||
code=ButtonRequestType.Address,
|
||||
cancel="Address",
|
||||
cancel_style=ui.BTN_KEY,
|
||||
cancel_style=ButtonDefault,
|
||||
)
|
||||
|
||||
|
||||
async def show_pubkey(ctx, pubkey: bytes):
|
||||
lines = chunks(hexlify(pubkey).decode(), 18)
|
||||
text = Text("Confirm public key", ui.ICON_RECEIVE, icon_color=ui.GREEN)
|
||||
text = Text("Confirm public key", ui.ICON_RECEIVE, ui.GREEN)
|
||||
text.mono(*lines)
|
||||
return await require_confirm(ctx, text, code=ButtonRequestType.PublicKey)
|
||||
return await require_confirm(ctx, text, ButtonRequestType.PublicKey)
|
||||
|
||||
|
||||
def split_address(address: str):
|
||||
|
@ -1,4 +1,4 @@
|
||||
from trezor import ui
|
||||
from trezor import ui, workflow
|
||||
from trezor.crypto import bip39
|
||||
|
||||
from apps.common import storage
|
||||
@ -18,8 +18,11 @@ def get_seed(passphrase: str = "", progress_bar=True):
|
||||
module = bip39
|
||||
if progress_bar:
|
||||
_start_progress()
|
||||
return module.seed(secret.decode(), passphrase, _render_progress)
|
||||
return module.seed(secret.decode(), passphrase)
|
||||
result = module.seed(secret.decode(), passphrase, _render_progress)
|
||||
_stop_progress()
|
||||
else:
|
||||
result = module.seed(secret.decode(), passphrase)
|
||||
return result
|
||||
|
||||
|
||||
def process(mnemonics: list, mnemonic_type: int):
|
||||
@ -36,14 +39,18 @@ def restore() -> str:
|
||||
|
||||
|
||||
def _start_progress():
|
||||
ui.backlight_slide_sync(ui.BACKLIGHT_DIM)
|
||||
ui.backlight_fade(ui.BACKLIGHT_DIM)
|
||||
ui.display.clear()
|
||||
ui.header("Please wait")
|
||||
ui.display.refresh()
|
||||
ui.backlight_slide_sync(ui.BACKLIGHT_NORMAL)
|
||||
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
|
||||
|
||||
|
||||
def _render_progress(progress: int, total: int):
|
||||
p = 1000 * progress // total
|
||||
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
|
||||
ui.display.refresh()
|
||||
|
||||
|
||||
def _stop_progress():
|
||||
workflow.restartdefault()
|
||||
|
@ -15,14 +15,12 @@ async def validate_path(ctx, validate_func, keychain, path, curve, **kwargs):
|
||||
|
||||
|
||||
async def show_path_warning(ctx, path: list):
|
||||
text = Text("Confirm path", ui.ICON_WRONG, icon_color=ui.RED)
|
||||
text = Text("Confirm path", ui.ICON_WRONG, ui.RED)
|
||||
text.normal("Path")
|
||||
text.mono(*break_address_n_to_lines(path))
|
||||
text.normal("is unknown.")
|
||||
text.normal("Are you sure?")
|
||||
return await require_confirm(
|
||||
ctx, text, code=ButtonRequestType.UnknownDerivationPath
|
||||
)
|
||||
return await require_confirm(ctx, text, ButtonRequestType.UnknownDerivationPath)
|
||||
|
||||
|
||||
def validate_path_for_get_public_key(path: list, slip44_id: int) -> bool:
|
||||
@ -50,7 +48,8 @@ def validate_path_for_get_public_key(path: list, slip44_id: int) -> bool:
|
||||
def is_hardened(i: int) -> bool:
|
||||
if i & HARDENED:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def break_address_n_to_lines(address_n: list) -> list:
|
||||
|
@ -5,79 +5,74 @@ from trezor.messages import ButtonRequestType, MessageType, PassphraseSourceType
|
||||
from trezor.messages.ButtonRequest import ButtonRequest
|
||||
from trezor.messages.PassphraseRequest import PassphraseRequest
|
||||
from trezor.messages.PassphraseStateRequest import PassphraseStateRequest
|
||||
from trezor.ui.entry_select import DEVICE, EntrySelector
|
||||
from trezor.ui.passphrase import CANCELLED, PassphraseKeyboard
|
||||
from trezor.ui.passphrase import CANCELLED, PassphraseKeyboard, PassphraseSource
|
||||
from trezor.ui.text import Text
|
||||
|
||||
from apps.common import storage
|
||||
from apps.common.cache import get_state
|
||||
from apps.common import cache, storage
|
||||
|
||||
if __debug__:
|
||||
from apps.debug import input_signal
|
||||
|
||||
_MAX_PASSPHRASE_LEN = const(50)
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def request_passphrase_entry(ctx):
|
||||
async def protect_by_passphrase(ctx) -> str:
|
||||
if storage.has_passphrase():
|
||||
return await request_passphrase(ctx)
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
async def request_passphrase(ctx) -> str:
|
||||
source = storage.get_passphrase_source()
|
||||
if source == PassphraseSourceType.ASK:
|
||||
source = await request_passphrase_source(ctx)
|
||||
passphrase = await request_passphrase_ack(
|
||||
ctx, source == PassphraseSourceType.DEVICE
|
||||
)
|
||||
if len(passphrase) > _MAX_PASSPHRASE_LEN:
|
||||
raise wire.DataError("Maximum passphrase length is %d" % _MAX_PASSPHRASE_LEN)
|
||||
return passphrase
|
||||
|
||||
|
||||
async def request_passphrase_source(ctx) -> int:
|
||||
req = ButtonRequest(code=ButtonRequestType.PassphraseType)
|
||||
await ctx.call(req, MessageType.ButtonAck)
|
||||
|
||||
text = Text("Enter passphrase", ui.ICON_CONFIG)
|
||||
text.normal("Where to enter your", "passphrase?")
|
||||
text.render()
|
||||
source = PassphraseSource(text)
|
||||
|
||||
ack = await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.PassphraseType),
|
||||
MessageType.ButtonAck,
|
||||
MessageType.Cancel,
|
||||
)
|
||||
if ack.MESSAGE_WIRE_TYPE == MessageType.Cancel:
|
||||
raise wire.ActionCancelled("Passphrase cancelled")
|
||||
|
||||
selector = EntrySelector(text)
|
||||
return await ctx.wait(selector)
|
||||
return await ctx.wait(source)
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def request_passphrase_ack(ctx, on_device):
|
||||
async def request_passphrase_ack(ctx, on_device: bool) -> str:
|
||||
if not on_device:
|
||||
text = Text("Passphrase entry", ui.ICON_CONFIG)
|
||||
text.normal("Please, type passphrase", "on connected host.")
|
||||
text.render()
|
||||
|
||||
req = PassphraseRequest(on_device=on_device)
|
||||
ack = await ctx.call(req, MessageType.PassphraseAck, MessageType.Cancel)
|
||||
if ack.MESSAGE_WIRE_TYPE == MessageType.Cancel:
|
||||
raise wire.ActionCancelled("Passphrase cancelled")
|
||||
ack = await ctx.call(req, MessageType.PassphraseAck)
|
||||
|
||||
if on_device:
|
||||
if ack.passphrase is not None:
|
||||
raise wire.ProcessError("Passphrase provided when it should not be")
|
||||
keyboard = PassphraseKeyboard("Enter passphrase")
|
||||
passphrase = await ctx.wait(keyboard)
|
||||
if passphrase == CANCELLED:
|
||||
|
||||
keyboard = PassphraseKeyboard("Enter passphrase", _MAX_PASSPHRASE_LEN)
|
||||
if __debug__:
|
||||
passphrase = await ctx.wait(keyboard, input_signal)
|
||||
else:
|
||||
passphrase = await ctx.wait(keyboard)
|
||||
if passphrase is CANCELLED:
|
||||
raise wire.ActionCancelled("Passphrase cancelled")
|
||||
else:
|
||||
if ack.passphrase is None:
|
||||
raise wire.ProcessError("Passphrase not provided")
|
||||
passphrase = ack.passphrase
|
||||
|
||||
req = PassphraseStateRequest(
|
||||
state=get_state(prev_state=ack.state, passphrase=passphrase)
|
||||
)
|
||||
state = cache.get_state(prev_state=ack.state, passphrase=passphrase)
|
||||
req = PassphraseStateRequest(state=state)
|
||||
ack = await ctx.call(req, MessageType.PassphraseStateAck, MessageType.Cancel)
|
||||
|
||||
return passphrase
|
||||
|
||||
|
||||
async def request_passphrase(ctx):
|
||||
if storage.get_passphrase_source() == PassphraseSourceType.ASK:
|
||||
on_device = await request_passphrase_entry(ctx) == DEVICE
|
||||
else:
|
||||
on_device = storage.get_passphrase_source() == PassphraseSourceType.DEVICE
|
||||
passphrase = await request_passphrase_ack(ctx, on_device)
|
||||
if len(passphrase) > _MAX_PASSPHRASE_LEN:
|
||||
raise wire.DataError("Maximum passphrase length is %d" % _MAX_PASSPHRASE_LEN)
|
||||
return passphrase
|
||||
|
||||
|
||||
async def protect_by_passphrase(ctx):
|
||||
if storage.has_passphrase():
|
||||
return await request_passphrase(ctx)
|
||||
else:
|
||||
return ""
|
||||
|
@ -1,6 +1,5 @@
|
||||
from trezor import loop, res, ui
|
||||
from trezor.ui.confirm import CONFIRMED, ConfirmDialog
|
||||
from trezor.ui.pin import PinMatrix
|
||||
from trezor import loop
|
||||
from trezor.ui.pin import CANCELLED, PinDialog
|
||||
|
||||
if __debug__:
|
||||
from apps.debug import input_signal
|
||||
@ -10,71 +9,25 @@ class PinCancelled(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def request_pin(
|
||||
label=None, attempts_remaining=None, cancellable: bool = True
|
||||
prompt: str = "Enter your PIN",
|
||||
attempts_remaining: int = None,
|
||||
allow_cancel: bool = True,
|
||||
) -> str:
|
||||
def onchange():
|
||||
c = dialog.cancel
|
||||
if matrix.pin:
|
||||
back = res.load(ui.ICON_BACK)
|
||||
if c.content is not back:
|
||||
c.normal_style = ui.BTN_CLEAR["normal"]
|
||||
c.content = back
|
||||
c.enable()
|
||||
c.taint()
|
||||
else:
|
||||
lock = res.load(ui.ICON_LOCK)
|
||||
if not cancellable and c.content:
|
||||
c.content = ""
|
||||
c.disable()
|
||||
c.taint()
|
||||
elif c.content is not lock:
|
||||
c.normal_style = ui.BTN_CANCEL["normal"]
|
||||
c.content = lock
|
||||
c.enable()
|
||||
c.taint()
|
||||
c.render()
|
||||
if attempts_remaining is None:
|
||||
subprompt = None
|
||||
elif attempts_remaining == 1:
|
||||
subprompt = "This is your last attempt"
|
||||
else:
|
||||
subprompt = "%s attempts remaining" % attempts_remaining
|
||||
|
||||
c = dialog.confirm
|
||||
if matrix.pin:
|
||||
if not c.is_enabled():
|
||||
c.enable()
|
||||
c.taint()
|
||||
else:
|
||||
if c.is_enabled():
|
||||
c.disable()
|
||||
c.taint()
|
||||
c.render()
|
||||
|
||||
if label is None:
|
||||
label = "Enter your PIN"
|
||||
sublabel = None
|
||||
if attempts_remaining:
|
||||
if attempts_remaining == 1:
|
||||
sublabel = "This is your last attempt"
|
||||
else:
|
||||
sublabel = "{} attempts remaining".format(attempts_remaining)
|
||||
matrix = PinMatrix(label, sublabel)
|
||||
matrix.onchange = onchange
|
||||
dialog = ConfirmDialog(matrix)
|
||||
dialog.cancel.area = ui.grid(12)
|
||||
dialog.confirm.area = ui.grid(14)
|
||||
matrix.onchange()
|
||||
dialog = PinDialog(prompt, subprompt, allow_cancel)
|
||||
|
||||
while True:
|
||||
if __debug__:
|
||||
result = await loop.spawn(dialog, input_signal)
|
||||
if isinstance(result, str):
|
||||
return result
|
||||
else:
|
||||
result = await dialog
|
||||
if result == CONFIRMED:
|
||||
if not matrix.pin:
|
||||
continue
|
||||
return matrix.pin
|
||||
elif matrix.pin: # reset
|
||||
matrix.change("")
|
||||
continue
|
||||
else: # cancel
|
||||
if result is CANCELLED:
|
||||
raise PinCancelled()
|
||||
return result
|
||||
|
@ -1,4 +1,4 @@
|
||||
from trezor import ui, wire
|
||||
from trezor import wire
|
||||
from trezor.crypto import bip32
|
||||
|
||||
from apps.common import HARDENED, cache, mnemonic, storage
|
||||
@ -64,30 +64,16 @@ async def get_keychain(ctx: wire.Context, namespaces: list) -> Keychain:
|
||||
raise wire.ProcessError("Device is not initialized")
|
||||
seed = cache.get_seed()
|
||||
if seed is None:
|
||||
seed = await _compute_seed(ctx)
|
||||
passphrase = cache.get_passphrase()
|
||||
if passphrase is None:
|
||||
passphrase = await protect_by_passphrase(ctx)
|
||||
cache.set_passphrase(passphrase)
|
||||
seed = mnemonic.get_seed(passphrase)
|
||||
cache.set_seed(seed)
|
||||
keychain = Keychain(seed, namespaces)
|
||||
return keychain
|
||||
|
||||
|
||||
def _path_hardened(path: list) -> bool:
|
||||
# TODO: move to paths.py after #538 is fixed
|
||||
for i in path:
|
||||
if not (i & HARDENED):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@ui.layout_no_slide
|
||||
async def _compute_seed(ctx: wire.Context) -> bytes:
|
||||
passphrase = cache.get_passphrase()
|
||||
if passphrase is None:
|
||||
passphrase = await protect_by_passphrase(ctx)
|
||||
cache.set_passphrase(passphrase)
|
||||
seed = mnemonic.get_seed(passphrase)
|
||||
cache.set_seed(seed)
|
||||
return seed
|
||||
|
||||
|
||||
def derive_node_without_passphrase(
|
||||
path: list, curve_name: str = "secp256k1"
|
||||
) -> bip32.HDNode:
|
||||
@ -102,3 +88,7 @@ def derive_node_without_passphrase(
|
||||
def remove_ed25519_prefix(pubkey: bytes) -> bytes:
|
||||
# 0x01 prefix is not part of the actual public key, hence removed
|
||||
return pubkey[1:]
|
||||
|
||||
|
||||
def _path_hardened(path: list) -> bool:
|
||||
return all(i & HARDENED for i in path)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from micropython import const
|
||||
from ubinascii import hexlify
|
||||
|
||||
from trezor import ui, wire
|
||||
from trezor import ui
|
||||
from trezor.messages import (
|
||||
ButtonRequestType,
|
||||
EosActionBuyRam,
|
||||
@ -17,11 +17,8 @@ from trezor.messages import (
|
||||
EosActionUnlinkAuth,
|
||||
EosActionUpdateAuth,
|
||||
EosActionVoteProducer,
|
||||
MessageType,
|
||||
)
|
||||
from trezor.messages.ButtonRequest import ButtonRequest
|
||||
from trezor.ui.confirm import CONFIRMED, ConfirmDialog
|
||||
from trezor.ui.scroll import Scrollpage, animate_swipe, paginate
|
||||
from trezor.ui.scroll import Paginated
|
||||
from trezor.ui.text import Text
|
||||
from trezor.utils import chunks
|
||||
|
||||
@ -38,11 +35,19 @@ _FOUR_FIELDS_PER_PAGE = const(4)
|
||||
_FIVE_FIELDS_PER_PAGE = const(5)
|
||||
|
||||
|
||||
async def confirm_action_buyram(ctx, msg: EosActionBuyRam):
|
||||
await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
|
||||
)
|
||||
async def _require_confirm_paginated(ctx, header, fields, per_page):
|
||||
pages = []
|
||||
for page in chunks(fields, per_page):
|
||||
if header == "Arbitrary data":
|
||||
text = Text(header, ui.ICON_WIPE, ui.RED)
|
||||
else:
|
||||
text = Text(header, ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.mono(*page)
|
||||
pages.append(text)
|
||||
await require_confirm(ctx, Paginated(pages), ButtonRequestType.ConfirmOutput)
|
||||
|
||||
|
||||
async def confirm_action_buyram(ctx, msg: EosActionBuyRam):
|
||||
text = "Buy RAM"
|
||||
fields = []
|
||||
fields.append("Payer:")
|
||||
@ -51,18 +56,10 @@ async def confirm_action_buyram(ctx, msg: EosActionBuyRam):
|
||||
fields.append(helpers.eos_name_to_string(msg.receiver))
|
||||
fields.append("Amount:")
|
||||
fields.append(helpers.eos_asset_to_string(msg.quantity))
|
||||
|
||||
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
|
||||
|
||||
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
|
||||
await ctx.wait(paginator)
|
||||
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
|
||||
|
||||
|
||||
async def confirm_action_buyrambytes(ctx, msg: EosActionBuyRamBytes):
|
||||
await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
|
||||
)
|
||||
|
||||
text = "Buy RAM"
|
||||
fields = []
|
||||
fields.append("Payer:")
|
||||
@ -71,18 +68,10 @@ async def confirm_action_buyrambytes(ctx, msg: EosActionBuyRamBytes):
|
||||
fields.append(helpers.eos_name_to_string(msg.receiver))
|
||||
fields.append("Bytes:")
|
||||
fields.append(str(msg.bytes))
|
||||
|
||||
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
|
||||
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
|
||||
|
||||
await ctx.wait(paginator)
|
||||
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
|
||||
|
||||
|
||||
async def confirm_action_delegate(ctx, msg: EosActionDelegate):
|
||||
await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
|
||||
)
|
||||
|
||||
text = "Delegate"
|
||||
fields = []
|
||||
fields.append("Sender:")
|
||||
@ -101,35 +90,20 @@ async def confirm_action_delegate(ctx, msg: EosActionDelegate):
|
||||
else:
|
||||
fields.append("Transfer: No")
|
||||
|
||||
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
|
||||
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
|
||||
|
||||
await ctx.wait(paginator)
|
||||
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
|
||||
|
||||
|
||||
async def confirm_action_sellram(ctx, msg: EosActionSellRam):
|
||||
await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
|
||||
)
|
||||
|
||||
text = "Sell RAM"
|
||||
fields = []
|
||||
fields.append("Receiver:")
|
||||
fields.append(helpers.eos_name_to_string(msg.account))
|
||||
fields.append("Bytes:")
|
||||
fields.append(str(msg.bytes))
|
||||
|
||||
pages = list(chunks(fields, _TWO_FIELDS_PER_PAGE))
|
||||
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
|
||||
|
||||
await ctx.wait(paginator)
|
||||
await _require_confirm_paginated(ctx, text, fields, _TWO_FIELDS_PER_PAGE)
|
||||
|
||||
|
||||
async def confirm_action_undelegate(ctx, msg: EosActionUndelegate):
|
||||
await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
|
||||
)
|
||||
|
||||
text = "Undelegate"
|
||||
fields = []
|
||||
fields.append("Sender:")
|
||||
@ -140,11 +114,7 @@ async def confirm_action_undelegate(ctx, msg: EosActionUndelegate):
|
||||
fields.append(helpers.eos_asset_to_string(msg.cpu_quantity))
|
||||
fields.append("NET:")
|
||||
fields.append(helpers.eos_asset_to_string(msg.net_quantity))
|
||||
|
||||
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
|
||||
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
|
||||
|
||||
await ctx.wait(paginator)
|
||||
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
|
||||
|
||||
|
||||
async def confirm_action_refund(ctx, msg: EosActionRefund):
|
||||
@ -166,13 +136,12 @@ async def confirm_action_voteproducer(ctx, msg: EosActionVoteProducer):
|
||||
|
||||
elif msg.producers:
|
||||
# PRODUCERS
|
||||
await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
|
||||
)
|
||||
producers = list(enumerate(msg.producers))
|
||||
pages = list(chunks(producers, _FIVE_FIELDS_PER_PAGE))
|
||||
paginator = paginate(show_voter_page, len(pages), _FIRST_PAGE, pages)
|
||||
await ctx.wait(paginator)
|
||||
text = "Vote for producers"
|
||||
fields = [
|
||||
"{:2d}. {}".format(wi + 1, helpers.eos_name_to_string(producer))
|
||||
for wi, producer in enumerate(msg.producers)
|
||||
]
|
||||
await _require_confirm_paginated(ctx, text, fields, _FIVE_FIELDS_PER_PAGE)
|
||||
|
||||
else:
|
||||
# Cancel vote
|
||||
@ -183,10 +152,6 @@ async def confirm_action_voteproducer(ctx, msg: EosActionVoteProducer):
|
||||
|
||||
|
||||
async def confirm_action_transfer(ctx, msg: EosActionTransfer, account: str):
|
||||
await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
|
||||
)
|
||||
|
||||
text = "Transfer"
|
||||
fields = []
|
||||
fields.append("From:")
|
||||
@ -200,19 +165,12 @@ async def confirm_action_transfer(ctx, msg: EosActionTransfer, account: str):
|
||||
|
||||
if msg.memo is not None:
|
||||
fields.append("Memo:")
|
||||
fields += split_data(msg.memo[:512])
|
||||
fields.extend(split_data(msg.memo[:512]))
|
||||
|
||||
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
|
||||
|
||||
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
|
||||
await ctx.wait(paginator)
|
||||
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
|
||||
|
||||
|
||||
async def confirm_action_updateauth(ctx, msg: EosActionUpdateAuth):
|
||||
await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
|
||||
)
|
||||
|
||||
text = "Update Auth"
|
||||
fields = []
|
||||
fields.append("Account:")
|
||||
@ -221,12 +179,8 @@ async def confirm_action_updateauth(ctx, msg: EosActionUpdateAuth):
|
||||
fields.append(helpers.eos_name_to_string(msg.permission))
|
||||
fields.append("Parent:")
|
||||
fields.append(helpers.eos_name_to_string(msg.parent))
|
||||
fields += authorization_fields(msg.auth)
|
||||
|
||||
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
|
||||
|
||||
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
|
||||
await ctx.wait(paginator)
|
||||
fields.extend(authorization_fields(msg.auth))
|
||||
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
|
||||
|
||||
|
||||
async def confirm_action_deleteauth(ctx, msg: EosActionDeleteAuth):
|
||||
@ -239,10 +193,6 @@ async def confirm_action_deleteauth(ctx, msg: EosActionDeleteAuth):
|
||||
|
||||
|
||||
async def confirm_action_linkauth(ctx, msg: EosActionLinkAuth):
|
||||
await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
|
||||
)
|
||||
|
||||
text = "Link Auth"
|
||||
fields = []
|
||||
fields.append("Account:")
|
||||
@ -253,18 +203,10 @@ async def confirm_action_linkauth(ctx, msg: EosActionLinkAuth):
|
||||
fields.append(helpers.eos_name_to_string(msg.type))
|
||||
fields.append("Requirement:")
|
||||
fields.append(helpers.eos_name_to_string(msg.requirement))
|
||||
|
||||
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
|
||||
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
|
||||
|
||||
await ctx.wait(paginator)
|
||||
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
|
||||
|
||||
|
||||
async def confirm_action_unlinkauth(ctx, msg: EosActionUnlinkAuth):
|
||||
await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
|
||||
)
|
||||
|
||||
text = "Unlink Auth"
|
||||
fields = []
|
||||
fields.append("Account:")
|
||||
@ -273,86 +215,31 @@ async def confirm_action_unlinkauth(ctx, msg: EosActionUnlinkAuth):
|
||||
fields.append(helpers.eos_name_to_string(msg.code))
|
||||
fields.append("Type:")
|
||||
fields.append(helpers.eos_name_to_string(msg.type))
|
||||
|
||||
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
|
||||
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
|
||||
|
||||
await ctx.wait(paginator)
|
||||
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
|
||||
|
||||
|
||||
async def confirm_action_newaccount(ctx, msg: EosActionNewAccount):
|
||||
await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
|
||||
)
|
||||
|
||||
text = "New Account"
|
||||
fields = []
|
||||
fields.append("Creator:")
|
||||
fields.append(helpers.eos_name_to_string(msg.creator))
|
||||
fields.append("Name:")
|
||||
fields.append(helpers.eos_name_to_string(msg.name))
|
||||
fields += authorization_fields(msg.owner)
|
||||
fields += authorization_fields(msg.active)
|
||||
|
||||
pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE))
|
||||
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
|
||||
|
||||
await ctx.wait(paginator)
|
||||
fields.extend(authorization_fields(msg.owner))
|
||||
fields.extend(authorization_fields(msg.active))
|
||||
await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE)
|
||||
|
||||
|
||||
async def confirm_action_unknown(ctx, action, checksum):
|
||||
await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck
|
||||
)
|
||||
text = "Arbitrary data"
|
||||
fields = []
|
||||
fields.append("Contract:")
|
||||
fields.append(helpers.eos_name_to_string(action.account))
|
||||
fields.append("Action Name:")
|
||||
fields.append(helpers.eos_name_to_string(action.name))
|
||||
|
||||
fields.append("Checksum: ")
|
||||
fields += split_data(hexlify(checksum).decode("ascii"))
|
||||
|
||||
pages = list(chunks(fields, _FIVE_FIELDS_PER_PAGE))
|
||||
paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text)
|
||||
|
||||
await ctx.wait(paginator)
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def show_lines_page(page: int, page_count: int, pages: list, header: str):
|
||||
if header == "Arbitrary data":
|
||||
text = Text(header, ui.ICON_WIPE, icon_color=ui.RED)
|
||||
else:
|
||||
text = Text(header, ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text.mono(*pages[page])
|
||||
|
||||
content = Scrollpage(text, page, page_count)
|
||||
if page + 1 == page_count:
|
||||
if await ConfirmDialog(content) != CONFIRMED:
|
||||
raise wire.ActionCancelled("Action cancelled")
|
||||
else:
|
||||
content.render()
|
||||
await animate_swipe()
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def show_voter_page(page: int, page_count: int, pages: list):
|
||||
lines = [
|
||||
"{:2d}. {}".format(wi + 1, helpers.eos_name_to_string(producer))
|
||||
for wi, producer in pages[page]
|
||||
]
|
||||
text = Text("Vote for producers", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text.mono(*lines)
|
||||
content = Scrollpage(text, page, page_count)
|
||||
|
||||
if page + 1 == page_count:
|
||||
if await ConfirmDialog(content) != CONFIRMED:
|
||||
raise wire.ActionCancelled("Action cancelled")
|
||||
else:
|
||||
content.render()
|
||||
await animate_swipe()
|
||||
fields.extend(split_data(hexlify(checksum).decode("ascii")))
|
||||
await _require_confirm_paginated(ctx, text, fields, _FIVE_FIELDS_PER_PAGE)
|
||||
|
||||
|
||||
def authorization_fields(auth):
|
||||
|
@ -6,14 +6,14 @@ from apps.common.confirm import require_confirm
|
||||
|
||||
|
||||
async def require_get_public_key(ctx, public_key):
|
||||
text = Text("Confirm public key", ui.ICON_RECEIVE, icon_color=ui.GREEN)
|
||||
text = Text("Confirm public key", ui.ICON_RECEIVE, ui.GREEN)
|
||||
text.normal(public_key)
|
||||
return await require_confirm(ctx, text, code=ButtonRequestType.PublicKey)
|
||||
return await require_confirm(ctx, text, ButtonRequestType.PublicKey)
|
||||
|
||||
|
||||
async def require_sign_tx(ctx, num_actions):
|
||||
text = Text("Sign transaction", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Sign transaction", ui.ICON_SEND, ui.GREEN)
|
||||
text.normal("You are about")
|
||||
text.normal("to sign {}".format(num_actions))
|
||||
text.normal("action(s).")
|
||||
return await require_confirm(ctx, text, code=ButtonRequestType.SignTx)
|
||||
return await require_confirm(ctx, text, ButtonRequestType.SignTx)
|
||||
|
@ -16,7 +16,7 @@ async def require_confirm_tx(ctx, to_bytes, value, chain_id, token=None, tx_type
|
||||
to_str = address_from_bytes(to_bytes, networks.by_chain_id(chain_id))
|
||||
else:
|
||||
to_str = "new contract?"
|
||||
text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN, new_lines=False)
|
||||
text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN, new_lines=False)
|
||||
text.bold(format_ethereum_amount(value, token, chain_id, tx_type))
|
||||
text.normal(ui.GREY, "to", ui.FG)
|
||||
for to_line in split_address(to_str):
|
||||
@ -29,9 +29,7 @@ async def require_confirm_tx(ctx, to_bytes, value, chain_id, token=None, tx_type
|
||||
async def require_confirm_fee(
|
||||
ctx, spending, gas_price, gas_limit, chain_id, token=None, tx_type=None
|
||||
):
|
||||
text = Text(
|
||||
"Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN, new_lines=False
|
||||
)
|
||||
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN, new_lines=False)
|
||||
text.bold(format_ethereum_amount(spending, token, chain_id, tx_type))
|
||||
text.normal(ui.GREY, "Gas price:", ui.FG)
|
||||
text.bold(format_ethereum_amount(gas_price, None, chain_id, tx_type))
|
||||
@ -48,7 +46,7 @@ async def require_confirm_data(ctx, data, data_total):
|
||||
data_str = hexlify(data[:36]).decode()
|
||||
if data_total > 36:
|
||||
data_str = data_str[:-2] + ".."
|
||||
text = Text("Confirm data", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm data", ui.ICON_SEND, ui.GREEN)
|
||||
text.bold("Size: %d bytes" % data_total)
|
||||
text.mono(*split_data(data_str))
|
||||
# we use SignTx, not ConfirmOutput, for compatibility with T1
|
||||
|
@ -5,9 +5,9 @@ from apps.common import storage
|
||||
|
||||
async def homescreen():
|
||||
# render homescreen in dimmed mode and fade back in
|
||||
await ui.backlight_slide(ui.BACKLIGHT_DIM)
|
||||
ui.backlight_fade(ui.BACKLIGHT_DIM)
|
||||
display_homescreen()
|
||||
await ui.backlight_slide(ui.BACKLIGHT_NORMAL)
|
||||
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
|
||||
|
||||
# loop forever, never return
|
||||
touch = loop.wait(io.TOUCH)
|
||||
|
@ -10,7 +10,7 @@ from apps.common.layout import show_pubkey, split_address
|
||||
|
||||
|
||||
async def require_confirm_tx(ctx, to, value):
|
||||
text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN)
|
||||
text.bold(format_coin_amount(value))
|
||||
text.normal("to")
|
||||
text.mono(*split_address(to))
|
||||
@ -18,7 +18,7 @@ async def require_confirm_tx(ctx, to, value):
|
||||
|
||||
|
||||
async def require_confirm_delegate_registration(ctx, delegate_name):
|
||||
text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
text.normal("Do you really want to")
|
||||
text.normal("register a delegate?")
|
||||
text.bold(*chunks(delegate_name, 20))
|
||||
@ -26,7 +26,7 @@ async def require_confirm_delegate_registration(ctx, delegate_name):
|
||||
|
||||
|
||||
async def require_confirm_vote_tx(ctx, votes):
|
||||
text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
text.normal(*get_vote_tx_text(votes))
|
||||
return await require_confirm(ctx, text, ButtonRequestType.SignTx)
|
||||
|
||||
@ -36,7 +36,7 @@ async def require_confirm_public_key(ctx, public_key):
|
||||
|
||||
|
||||
async def require_confirm_multisig(ctx, multisignature):
|
||||
text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
text.normal("Keys group length: %s" % len(multisignature.keys_group))
|
||||
text.normal("Life time: %s" % multisignature.life_time)
|
||||
text.normal("Min: %s" % multisignature.min)
|
||||
@ -44,7 +44,7 @@ async def require_confirm_multisig(ctx, multisignature):
|
||||
|
||||
|
||||
async def require_confirm_fee(ctx, value, fee):
|
||||
text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
text.bold(format_coin_amount(value))
|
||||
text.normal("fee:")
|
||||
text.bold(format_coin_amount(fee))
|
||||
|
@ -51,14 +51,14 @@ async def apply_settings(ctx, msg):
|
||||
async def require_confirm_change_homescreen(ctx):
|
||||
text = Text("Change homescreen", ui.ICON_CONFIG)
|
||||
text.normal("Do you really want to", "change homescreen?")
|
||||
await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall)
|
||||
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)
|
||||
|
||||
|
||||
async def require_confirm_change_label(ctx, label):
|
||||
text = Text("Change label", ui.ICON_CONFIG)
|
||||
text.normal("Do you really want to", "change label to")
|
||||
text.bold("%s?" % label)
|
||||
await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall)
|
||||
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)
|
||||
|
||||
|
||||
async def require_confirm_change_passphrase(ctx, use):
|
||||
@ -66,7 +66,7 @@ async def require_confirm_change_passphrase(ctx, use):
|
||||
text.normal("Do you really want to")
|
||||
text.normal("enable passphrase" if use else "disable passphrase")
|
||||
text.normal("encryption?")
|
||||
await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall)
|
||||
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)
|
||||
|
||||
|
||||
async def require_confirm_change_passphrase_source(ctx, source):
|
||||
@ -79,7 +79,7 @@ async def require_confirm_change_passphrase_source(ctx, source):
|
||||
text = Text("Passphrase source", ui.ICON_CONFIG)
|
||||
text.normal("Do you really want to", "change the passphrase", "source to")
|
||||
text.bold("ALWAYS %s?" % desc)
|
||||
await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall)
|
||||
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)
|
||||
|
||||
|
||||
async def require_confirm_change_display_rotation(ctx, rotation):
|
||||
@ -95,4 +95,4 @@ async def require_confirm_change_display_rotation(ctx, rotation):
|
||||
text.normal("Do you really want to", "change display rotation")
|
||||
text.normal("to")
|
||||
text.bold("%s?" % label)
|
||||
await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall)
|
||||
await require_confirm(ctx, text, ButtonRequestType.ProtectCall)
|
||||
|
@ -4,8 +4,8 @@ from trezor.messages.Success import Success
|
||||
from apps.common import mnemonic, storage
|
||||
from apps.management.reset_device import (
|
||||
check_mnemonic,
|
||||
show_backup_warning,
|
||||
show_mnemonic,
|
||||
show_warning,
|
||||
show_wrong_entry,
|
||||
)
|
||||
|
||||
@ -19,7 +19,7 @@ async def backup_device(ctx, msg):
|
||||
words = mnemonic.restore()
|
||||
|
||||
# warn user about mnemonic safety
|
||||
await show_warning(ctx)
|
||||
await show_backup_warning(ctx)
|
||||
|
||||
storage.set_unfinished_backup(True)
|
||||
storage.set_backed_up()
|
||||
|
@ -1,8 +1,9 @@
|
||||
from trezor import config, loop, ui, wire
|
||||
from trezor import config, ui, wire
|
||||
from trezor.messages import ButtonRequestType, MessageType
|
||||
from trezor.messages.ButtonRequest import ButtonRequest
|
||||
from trezor.messages.Success import Success
|
||||
from trezor.pin import pin_to_int
|
||||
from trezor.ui.popup import Popup
|
||||
from trezor.ui.text import Text
|
||||
|
||||
from apps.common.confirm import require_confirm
|
||||
@ -81,11 +82,10 @@ async def request_pin_ack(ctx, *args, **kwargs):
|
||||
raise wire.ActionCancelled("Cancelled")
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def pin_mismatch():
|
||||
text = Text("PIN mismatch", ui.ICON_WRONG, icon_color=ui.RED)
|
||||
text = Text("PIN mismatch", ui.ICON_WRONG, ui.RED)
|
||||
text.normal("Entered PINs do not", "match each other.")
|
||||
text.normal("")
|
||||
text.normal("Please, try again...")
|
||||
text.render()
|
||||
await loop.sleep(3 * 1000 * 1000)
|
||||
popup = Popup(text, 3000) # show for 3 seconds
|
||||
await popup
|
||||
|
@ -19,6 +19,9 @@ from apps.common import mnemonic, storage
|
||||
from apps.common.confirm import require_confirm
|
||||
from apps.management.change_pin import request_pin_ack, request_pin_confirm
|
||||
|
||||
if __debug__:
|
||||
from apps.debug import input_signal
|
||||
|
||||
|
||||
async def recovery_device(ctx, msg):
|
||||
"""
|
||||
@ -65,7 +68,7 @@ async def recovery_device(ctx, msg):
|
||||
|
||||
# ask for pin repeatedly
|
||||
if msg.pin_protection:
|
||||
newpin = await request_pin_confirm(ctx, cancellable=False)
|
||||
newpin = await request_pin_confirm(ctx, allow_cancel=False)
|
||||
else:
|
||||
newpin = ""
|
||||
|
||||
@ -100,26 +103,31 @@ async def recovery_device(ctx, msg):
|
||||
return Success(message="Device recovered")
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def request_wordcount(ctx, title: str) -> int:
|
||||
await ctx.call(ButtonRequest(code=MnemonicWordCount), ButtonAck)
|
||||
|
||||
text = Text(title, ui.ICON_RECOVERY)
|
||||
text.normal("Number of words?")
|
||||
count = await ctx.wait(WordSelector(text))
|
||||
|
||||
if __debug__:
|
||||
count = await ctx.wait(WordSelector(text), input_signal)
|
||||
count = int(count) # if input_signal was triggered, count is a string
|
||||
else:
|
||||
count = await ctx.wait(WordSelector(text))
|
||||
|
||||
return count
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def request_mnemonic(ctx, count: int) -> str:
|
||||
await ctx.call(ButtonRequest(code=MnemonicInput), ButtonAck)
|
||||
|
||||
words = []
|
||||
board = MnemonicKeyboard()
|
||||
for i in range(count):
|
||||
board.prompt = "Type the %s word:" % format_ordinal(i + 1)
|
||||
word = await ctx.wait(board)
|
||||
keyboard = MnemonicKeyboard("Type the %s word:" % format_ordinal(i + 1))
|
||||
if __debug__:
|
||||
word = await ctx.wait(keyboard, input_signal)
|
||||
else:
|
||||
word = await ctx.wait(keyboard)
|
||||
words.append(word)
|
||||
|
||||
return " ".join(words)
|
||||
|
@ -1,21 +1,19 @@
|
||||
from micropython import const
|
||||
from ubinascii import hexlify
|
||||
|
||||
from trezor import config, ui, wire, workflow
|
||||
from trezor import config, ui, wire
|
||||
from trezor.crypto import bip39, hashlib, random
|
||||
from trezor.messages import ButtonRequestType, MessageType
|
||||
from trezor.messages.ButtonRequest import ButtonRequest
|
||||
from trezor.messages.EntropyRequest import EntropyRequest
|
||||
from trezor.messages.Success import Success
|
||||
from trezor.pin import pin_to_int
|
||||
from trezor.ui.confirm import HoldToConfirmDialog
|
||||
from trezor.ui.mnemonic import MnemonicKeyboard
|
||||
from trezor.ui.scroll import Scrollpage, animate_swipe, paginate
|
||||
from trezor.ui.scroll import Paginated
|
||||
from trezor.ui.text import Text
|
||||
from trezor.utils import chunks, format_ordinal
|
||||
|
||||
from apps.common import mnemonic, storage
|
||||
from apps.common.confirm import require_confirm
|
||||
from apps.common.confirm import hold_to_confirm, require_confirm
|
||||
from apps.management.change_pin import request_pin_confirm
|
||||
|
||||
if __debug__:
|
||||
@ -31,18 +29,8 @@ async def reset_device(ctx, msg):
|
||||
if storage.is_initialized():
|
||||
raise wire.UnexpectedMessage("Already initialized")
|
||||
|
||||
text = Text("Create a new wallet", ui.ICON_RESET, new_lines=False)
|
||||
text.normal("Do you really want to")
|
||||
text.br()
|
||||
text.normal("create a new wallet?")
|
||||
text.br()
|
||||
text.br_half()
|
||||
text.normal("By continuing you agree")
|
||||
text.br()
|
||||
text.normal("to")
|
||||
text.bold("https://trezor.io/tos")
|
||||
|
||||
await require_confirm(ctx, text, code=ButtonRequestType.ResetDevice)
|
||||
# make sure use knows he's setting up a new wallet
|
||||
await show_reset_warning(ctx)
|
||||
|
||||
# request new PIN
|
||||
if msg.pin_protection:
|
||||
@ -63,7 +51,7 @@ async def reset_device(ctx, msg):
|
||||
|
||||
if not msg.skip_backup and not msg.no_backup:
|
||||
# require confirmation of the mnemonic safety
|
||||
await show_warning(ctx)
|
||||
await show_backup_warning(ctx)
|
||||
|
||||
# show mnemonic and require confirmation of a random word
|
||||
while True:
|
||||
@ -87,12 +75,9 @@ async def reset_device(ctx, msg):
|
||||
no_backup=msg.no_backup,
|
||||
)
|
||||
|
||||
# show success message. if we skipped backup, it's possible that homescreen
|
||||
# is still running, uninterrupted. restart it to pick up new label.
|
||||
# show success message
|
||||
if not msg.skip_backup and not msg.no_backup:
|
||||
await show_success(ctx)
|
||||
else:
|
||||
workflow.restartdefault()
|
||||
|
||||
return Success(message="Initialized")
|
||||
|
||||
@ -105,7 +90,21 @@ def generate_mnemonic(strength: int, int_entropy: bytes, ext_entropy: bytes) ->
|
||||
return bip39.from_data(entropy[: strength // 8])
|
||||
|
||||
|
||||
async def show_warning(ctx):
|
||||
async def show_reset_warning(ctx):
|
||||
text = Text("Create a new wallet", ui.ICON_RESET, new_lines=False)
|
||||
text.normal("Do you really want to")
|
||||
text.br()
|
||||
text.normal("create a new wallet?")
|
||||
text.br()
|
||||
text.br_half()
|
||||
text.normal("By continuing you agree")
|
||||
text.br()
|
||||
text.normal("to")
|
||||
text.bold("https://trezor.io/tos")
|
||||
await require_confirm(ctx, text, code=ButtonRequestType.ResetDevice)
|
||||
|
||||
|
||||
async def show_backup_warning(ctx):
|
||||
text = Text("Backup your seed", ui.ICON_NOCOPY)
|
||||
text.normal(
|
||||
"Never make a digital",
|
||||
@ -119,7 +118,7 @@ async def show_warning(ctx):
|
||||
|
||||
|
||||
async def show_wrong_entry(ctx):
|
||||
text = Text("Wrong entry!", ui.ICON_WRONG, icon_color=ui.RED)
|
||||
text = Text("Wrong entry!", ui.ICON_WRONG, ui.RED)
|
||||
text.normal("You have entered", "wrong seed word.", "Please check again.")
|
||||
await require_confirm(
|
||||
ctx, text, ButtonRequestType.ResetDevice, confirm="Check again", cancel=None
|
||||
@ -127,7 +126,7 @@ async def show_wrong_entry(ctx):
|
||||
|
||||
|
||||
async def show_success(ctx):
|
||||
text = Text("Backup is done!", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Backup is done!", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.normal(
|
||||
"Never make a digital",
|
||||
"copy of your recovery",
|
||||
@ -148,32 +147,33 @@ async def show_entropy(ctx, entropy: bytes):
|
||||
|
||||
|
||||
async def show_mnemonic(ctx, mnemonic: str):
|
||||
await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.ResetDevice), MessageType.ButtonAck
|
||||
)
|
||||
first_page = const(0)
|
||||
words_per_page = const(4)
|
||||
words = list(enumerate(mnemonic.split()))
|
||||
pages = list(chunks(words, words_per_page))
|
||||
paginator = paginate(show_mnemonic_page, len(pages), first_page, pages)
|
||||
await ctx.wait(paginator)
|
||||
# split mnemonic words into pages
|
||||
PER_PAGE = const(4)
|
||||
words = mnemonic.split()
|
||||
words = list(enumerate(words))
|
||||
words = list(chunks(words, PER_PAGE))
|
||||
|
||||
# display the pages, with a confirmation dialog on the last one
|
||||
pages = [get_mnemonic_page(page) for page in words]
|
||||
paginated = Paginated(pages)
|
||||
|
||||
@ui.layout
|
||||
async def show_mnemonic_page(page: int, page_count: int, pages: list):
|
||||
if __debug__:
|
||||
debug.reset_current_words = [word for _, word in pages[page]]
|
||||
|
||||
lines = ["%2d. %s" % (wi + 1, word) for wi, word in pages[page]]
|
||||
def export_displayed_words():
|
||||
# export currently displayed mnemonic words into debuglink
|
||||
debug.reset_current_words = [w for _, w in words[paginated.page]]
|
||||
|
||||
paginated.on_change = export_displayed_words
|
||||
export_displayed_words()
|
||||
|
||||
await hold_to_confirm(ctx, paginated, ButtonRequestType.ResetDevice)
|
||||
|
||||
|
||||
def get_mnemonic_page(words: list):
|
||||
text = Text("Recovery seed", ui.ICON_RESET)
|
||||
text.mono(*lines)
|
||||
content = Scrollpage(text, page, page_count)
|
||||
|
||||
if page + 1 == page_count:
|
||||
await HoldToConfirmDialog(content)
|
||||
else:
|
||||
content.render()
|
||||
await animate_swipe()
|
||||
for index, word in words:
|
||||
text.mono("%2d. %s" % (index + 1, word))
|
||||
return text
|
||||
|
||||
|
||||
async def check_mnemonic(ctx, mnemonic: str) -> bool:
|
||||
@ -192,11 +192,12 @@ async def check_mnemonic(ctx, mnemonic: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def check_word(ctx, words: list, index: int):
|
||||
if __debug__:
|
||||
debug.reset_word_index = index
|
||||
|
||||
keyboard = MnemonicKeyboard("Type the %s word:" % format_ordinal(index + 1))
|
||||
result = await ctx.wait(keyboard)
|
||||
if __debug__:
|
||||
result = await ctx.wait(keyboard, debug.input_signal)
|
||||
else:
|
||||
result = await ctx.wait(keyboard)
|
||||
return result == words[index]
|
||||
|
@ -1,6 +1,8 @@
|
||||
from trezor import ui
|
||||
from trezor.messages import ButtonRequestType
|
||||
from trezor.messages.Success import Success
|
||||
from trezor.ui.button import ButtonCancel
|
||||
from trezor.ui.loader import LoaderDanger
|
||||
from trezor.ui.text import Text
|
||||
|
||||
from apps.common import storage
|
||||
@ -8,17 +10,15 @@ from apps.common.confirm import require_hold_to_confirm
|
||||
|
||||
|
||||
async def wipe_device(ctx, msg):
|
||||
|
||||
text = Text("Wipe device", ui.ICON_WIPE, icon_color=ui.RED)
|
||||
text = Text("Wipe device", ui.ICON_WIPE, ui.RED)
|
||||
text.normal("Do you really want to", "wipe the device?", "")
|
||||
text.bold("All data will be lost.")
|
||||
|
||||
await require_hold_to_confirm(
|
||||
ctx,
|
||||
text,
|
||||
code=ButtonRequestType.WipeDevice,
|
||||
button_style=ui.BTN_CANCEL,
|
||||
loader_style=ui.LDR_DANGER,
|
||||
ButtonRequestType.WipeDevice,
|
||||
confirm_style=ButtonCancel,
|
||||
loader_style=LoaderDanger,
|
||||
)
|
||||
|
||||
storage.wipe()
|
||||
|
@ -1,7 +1,43 @@
|
||||
from trezor import res, ui, utils
|
||||
from trezor.messages import ButtonRequestType
|
||||
from trezor import loop, ui, utils
|
||||
from trezor.messages import ButtonRequestType, MessageType
|
||||
from trezor.messages.ButtonRequest import ButtonRequest
|
||||
from trezor.ui.text import Text
|
||||
from trezor.utils import chunks
|
||||
|
||||
if __debug__:
|
||||
from apps.debug import confirm_signal
|
||||
|
||||
|
||||
async def naive_pagination(
|
||||
ctx, lines, title, icon=ui.ICON_RESET, icon_color=ui.ORANGE, per_page=5
|
||||
):
|
||||
from trezor.ui.scroll import CANCELLED, CONFIRMED, PaginatedWithButtons
|
||||
|
||||
pages = []
|
||||
page_lines = paginate_lines(lines, per_page)
|
||||
|
||||
for i, lines in enumerate(page_lines):
|
||||
if len(page_lines) > 1:
|
||||
paging = "%s/%s" % (i + 1, len(page_lines))
|
||||
else:
|
||||
paging = ""
|
||||
text = Text("%s %s" % (title, paging), icon, icon_color)
|
||||
text.normal(*lines)
|
||||
pages.append(text)
|
||||
|
||||
paginated = PaginatedWithButtons(pages, one_by_one=True)
|
||||
|
||||
while True:
|
||||
await ctx.call(
|
||||
ButtonRequest(code=ButtonRequestType.SignTx), MessageType.ButtonAck
|
||||
)
|
||||
if __debug__:
|
||||
result = await loop.spawn(paginated, confirm_signal)
|
||||
else:
|
||||
result = await paginated
|
||||
if result is CONFIRMED:
|
||||
return True
|
||||
if result is CANCELLED:
|
||||
return False
|
||||
|
||||
|
||||
def paginate_lines(lines, lines_per_page=5):
|
||||
@ -28,94 +64,9 @@ def paginate_lines(lines, lines_per_page=5):
|
||||
return pages
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def tx_dialog(
|
||||
ctx,
|
||||
code,
|
||||
content,
|
||||
cancel_btn,
|
||||
confirm_btn,
|
||||
cancel_style,
|
||||
confirm_style,
|
||||
scroll_tuple=None,
|
||||
):
|
||||
from trezor.messages import MessageType
|
||||
from trezor.messages.ButtonRequest import ButtonRequest
|
||||
from trezor.ui.confirm import ConfirmDialog
|
||||
from trezor.ui.scroll import Scrollpage
|
||||
|
||||
await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck)
|
||||
|
||||
if scroll_tuple and scroll_tuple[1] > 1:
|
||||
content = Scrollpage(content, scroll_tuple[0], scroll_tuple[1])
|
||||
|
||||
dialog = ConfirmDialog(
|
||||
content,
|
||||
cancel=cancel_btn,
|
||||
confirm=confirm_btn,
|
||||
cancel_style=cancel_style,
|
||||
confirm_style=confirm_style,
|
||||
)
|
||||
return await ctx.wait(dialog)
|
||||
|
||||
|
||||
async def naive_pagination(
|
||||
ctx, lines, title, icon=ui.ICON_RESET, icon_color=ui.ORANGE, per_page=5
|
||||
):
|
||||
from trezor.ui.confirm import CANCELLED, CONFIRMED, DEFAULT_CANCEL, DEFAULT_CONFIRM
|
||||
|
||||
if isinstance(lines, (list, tuple)):
|
||||
lines = lines
|
||||
else:
|
||||
lines = list(chunks(lines, 16))
|
||||
|
||||
pages = paginate_lines(lines, per_page)
|
||||
npages = len(pages)
|
||||
cur_step = 0
|
||||
code = ButtonRequestType.SignTx
|
||||
iback = res.load(ui.ICON_BACK)
|
||||
inext = res.load(ui.ICON_CLICK)
|
||||
|
||||
while cur_step <= npages:
|
||||
text = pages[cur_step]
|
||||
fst_page = cur_step == 0
|
||||
lst_page = cur_step + 1 >= npages
|
||||
|
||||
cancel_btn = DEFAULT_CANCEL if fst_page else iback
|
||||
cancel_style = ui.BTN_CANCEL if fst_page else ui.BTN_DEFAULT
|
||||
confirm_btn = DEFAULT_CONFIRM if lst_page else inext
|
||||
confirm_style = ui.BTN_CONFIRM if lst_page else ui.BTN_DEFAULT
|
||||
|
||||
paging = ("%d/%d" % (cur_step + 1, npages)) if npages > 1 else ""
|
||||
content = Text("%s %s" % (title, paging), icon, icon_color=icon_color)
|
||||
content.normal(*text)
|
||||
|
||||
reaction = await tx_dialog(
|
||||
ctx,
|
||||
code,
|
||||
content,
|
||||
cancel_btn,
|
||||
confirm_btn,
|
||||
cancel_style,
|
||||
confirm_style,
|
||||
(cur_step, npages),
|
||||
)
|
||||
|
||||
if fst_page and reaction == CANCELLED:
|
||||
return False
|
||||
elif not lst_page and reaction == CONFIRMED:
|
||||
cur_step += 1
|
||||
elif lst_page and reaction == CONFIRMED:
|
||||
return True
|
||||
elif reaction == CANCELLED:
|
||||
cur_step -= 1
|
||||
elif reaction == CONFIRMED:
|
||||
cur_step += 1
|
||||
|
||||
|
||||
def format_amount(value):
|
||||
return "%s XMR" % utils.format_amount(value, 12)
|
||||
|
||||
|
||||
def split_address(address):
|
||||
return chunks(address, 16)
|
||||
return utils.chunks(address, 16)
|
||||
|
@ -2,6 +2,7 @@ from ubinascii import hexlify
|
||||
|
||||
from trezor import ui, wire
|
||||
from trezor.messages import ButtonRequestType
|
||||
from trezor.ui.popup import Popup
|
||||
from trezor.ui.text import Text
|
||||
from trezor.utils import chunks
|
||||
|
||||
@ -12,25 +13,25 @@ DUMMY_PAYMENT_ID = b"\x00" * 8
|
||||
|
||||
|
||||
async def require_confirm_watchkey(ctx):
|
||||
content = Text("Confirm export", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
content = Text("Confirm export", ui.ICON_SEND, ui.GREEN)
|
||||
content.normal("Do you really want to", "export watch-only", "credentials?")
|
||||
return await require_confirm(ctx, content, ButtonRequestType.SignTx)
|
||||
|
||||
|
||||
async def require_confirm_keyimage_sync(ctx):
|
||||
content = Text("Confirm ki sync", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
content = Text("Confirm ki sync", ui.ICON_SEND, ui.GREEN)
|
||||
content.normal("Do you really want to", "sync key images?")
|
||||
return await require_confirm(ctx, content, ButtonRequestType.SignTx)
|
||||
|
||||
|
||||
async def require_confirm_live_refresh(ctx):
|
||||
content = Text("Confirm refresh", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
content = Text("Confirm refresh", ui.ICON_SEND, ui.GREEN)
|
||||
content.normal("Do you really want to", "start refresh?")
|
||||
return await require_confirm(ctx, content, ButtonRequestType.SignTx)
|
||||
|
||||
|
||||
async def require_confirm_tx_key(ctx, export_key=False):
|
||||
content = Text("Confirm export", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
content = Text("Confirm export", ui.ICON_SEND, ui.GREEN)
|
||||
txt = ["Do you really want to"]
|
||||
if export_key:
|
||||
txt.append("export tx_key?")
|
||||
@ -111,14 +112,55 @@ async def _require_confirm_payment_id(ctx, payment_id):
|
||||
|
||||
|
||||
async def _require_confirm_fee(ctx, fee):
|
||||
content = Text("Confirm fee", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
content = Text("Confirm fee", ui.ICON_SEND, ui.GREEN)
|
||||
content.bold(common.format_amount(fee))
|
||||
await require_hold_to_confirm(ctx, content, ButtonRequestType.ConfirmOutput)
|
||||
|
||||
|
||||
@ui.layout_no_slide
|
||||
class TransactionStep(ui.Control):
|
||||
def __init__(self, state, info):
|
||||
self.state = state
|
||||
self.info = info
|
||||
|
||||
def on_render(self):
|
||||
state = self.state
|
||||
info = self.info
|
||||
ui.header("Signing transaction", ui.ICON_SEND, ui.TITLE_GREY, ui.BG, ui.BLUE)
|
||||
p = 1000 * state.progress_cur // state.progress_total
|
||||
ui.display.loader(p, False, -4, ui.WHITE, ui.BG)
|
||||
ui.display.text_center(ui.WIDTH // 2, 210, info[0], ui.NORMAL, ui.FG, ui.BG)
|
||||
if len(info) > 1:
|
||||
ui.display.text_center(ui.WIDTH // 2, 235, info[1], ui.NORMAL, ui.FG, ui.BG)
|
||||
|
||||
|
||||
class KeyImageSyncStep(ui.Control):
|
||||
def __init__(self, current, total_num):
|
||||
self.current = current
|
||||
self.total_num = total_num
|
||||
|
||||
def on_render(self):
|
||||
current = self.current
|
||||
total_num = self.total_num
|
||||
ui.header("Syncing", ui.ICON_SEND, ui.TITLE_GREY, ui.BG, ui.BLUE)
|
||||
p = (1000 * (current + 1) // total_num) if total_num > 0 else 0
|
||||
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
|
||||
|
||||
|
||||
class LiveRefreshStep(ui.Control):
|
||||
def __init__(self, current):
|
||||
self.current = current
|
||||
|
||||
def on_render(self):
|
||||
current = self.current
|
||||
ui.header("Refreshing", ui.ICON_SEND, ui.TITLE_GREY, ui.BG, ui.BLUE)
|
||||
p = (1000 * current // 8) % 1000
|
||||
ui.display.loader(p, True, 18, ui.WHITE, ui.BG)
|
||||
ui.display.text_center(
|
||||
ui.WIDTH // 2, 145, "%d" % current, ui.NORMAL, ui.FG, ui.BG
|
||||
)
|
||||
|
||||
|
||||
async def transaction_step(state, step, sub_step=None):
|
||||
info = []
|
||||
if step == 0:
|
||||
info = ["Signing..."]
|
||||
elif step == 100:
|
||||
@ -139,43 +181,16 @@ async def transaction_step(state, step, sub_step=None):
|
||||
info = ["Processing..."]
|
||||
|
||||
state.progress_cur += 1
|
||||
|
||||
ui.display.clear()
|
||||
text = Text("Signing transaction", ui.ICON_SEND, icon_color=ui.BLUE)
|
||||
text.render()
|
||||
|
||||
p = 1000 * state.progress_cur // state.progress_total
|
||||
ui.display.loader(p, False, -4, ui.WHITE, ui.BG)
|
||||
ui.display.text_center(ui.WIDTH // 2, 210, info[0], ui.NORMAL, ui.FG, ui.BG)
|
||||
if len(info) > 1:
|
||||
ui.display.text_center(ui.WIDTH // 2, 235, info[1], ui.NORMAL, ui.FG, ui.BG)
|
||||
ui.display.refresh()
|
||||
await Popup(TransactionStep(state, info))
|
||||
|
||||
|
||||
@ui.layout_no_slide
|
||||
async def keyimage_sync_step(ctx, current, total_num):
|
||||
if current is None:
|
||||
return
|
||||
ui.display.clear()
|
||||
text = Text("Syncing", ui.ICON_SEND, icon_color=ui.BLUE)
|
||||
text.render()
|
||||
|
||||
p = (1000 * (current + 1) // total_num) if total_num > 0 else 0
|
||||
ui.display.loader(p, False, 18, ui.WHITE, ui.BG)
|
||||
ui.display.refresh()
|
||||
await Popup(KeyImageSyncStep(current, total_num))
|
||||
|
||||
|
||||
@ui.layout_no_slide
|
||||
async def live_refresh_step(ctx, current):
|
||||
if current is None:
|
||||
return
|
||||
ui.display.clear()
|
||||
text = Text("Refreshing", ui.ICON_SEND, icon_color=ui.BLUE)
|
||||
text.render()
|
||||
|
||||
step = 8
|
||||
p = (1000 * current // step) % 1000
|
||||
|
||||
ui.display.loader(p, True, 18, ui.WHITE, ui.BG)
|
||||
ui.display.text_center(ui.WIDTH // 2, 145, "%d" % current, ui.NORMAL, ui.FG, ui.BG)
|
||||
ui.display.refresh()
|
||||
await Popup(LiveRefreshStep(current))
|
||||
|
@ -10,7 +10,7 @@ from apps.common.confirm import require_confirm, require_hold_to_confirm
|
||||
|
||||
async def require_confirm_text(ctx, action: str):
|
||||
content = action.split(" ")
|
||||
text = Text("Confirm action", ui.ICON_SEND, icon_color=ui.GREEN, new_lines=False)
|
||||
text = Text("Confirm action", ui.ICON_SEND, ui.GREEN, new_lines=False)
|
||||
text.normal(*content)
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
|
||||
@ -26,13 +26,13 @@ async def require_confirm_fee(ctx, action: str, fee: int):
|
||||
|
||||
|
||||
async def require_confirm_content(ctx, headline: str, content: list):
|
||||
text = Text(headline, ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text(headline, ui.ICON_SEND, ui.GREEN)
|
||||
text.normal(*content)
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
|
||||
|
||||
async def require_confirm_final(ctx, fee: int):
|
||||
text = Text("Final confirm", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Final confirm", ui.ICON_SEND, ui.GREEN)
|
||||
text.normal("Sign this transaction")
|
||||
text.bold("and pay %s XEM" % format_amount(fee, NEM_MAX_DIVISIBILITY))
|
||||
text.normal("for network fee?")
|
||||
|
@ -1,5 +1,3 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import ui, wire
|
||||
from trezor.messages import (
|
||||
NEMMosaicCreation,
|
||||
@ -9,8 +7,8 @@ from trezor.messages import (
|
||||
NEMSupplyChangeType,
|
||||
NEMTransactionCommon,
|
||||
)
|
||||
from trezor.ui.confirm import CONFIRMED, ConfirmDialog
|
||||
from trezor.ui.scroll import Scrollpage, animate_swipe, paginate
|
||||
from trezor.ui.confirm import CONFIRMED, Confirm
|
||||
from trezor.ui.scroll import Paginated
|
||||
from trezor.ui.text import Text
|
||||
|
||||
from ..layout import (
|
||||
@ -22,6 +20,9 @@ from ..layout import (
|
||||
|
||||
from apps.common.layout import split_address
|
||||
|
||||
if __debug__:
|
||||
from apps.debug import confirm_signal
|
||||
|
||||
|
||||
async def ask_mosaic_creation(
|
||||
ctx, common: NEMTransactionCommon, creation: NEMMosaicCreation
|
||||
@ -76,21 +77,16 @@ def _supply_message(supply_change):
|
||||
|
||||
async def _require_confirm_properties(ctx, definition: NEMMosaicDefinition):
|
||||
# TODO: we should send a button request here
|
||||
properties = _get_mosaic_properties(definition)
|
||||
first_page = const(0)
|
||||
paginator = paginate(_show_page, len(properties), first_page, properties)
|
||||
await ctx.wait(paginator)
|
||||
pages = _get_mosaic_properties(definition)
|
||||
pages[-1] = Confirm(pages[-1])
|
||||
paginated = Paginated(pages)
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def _show_page(page: int, page_count: int, content):
|
||||
content = Scrollpage(content[page], page, page_count)
|
||||
if page + 1 == page_count:
|
||||
if await ConfirmDialog(content) != CONFIRMED:
|
||||
raise wire.ActionCancelled("Action cancelled")
|
||||
if __debug__:
|
||||
result = await ctx.wait(paginated, confirm_signal)
|
||||
else:
|
||||
content.render()
|
||||
await animate_swipe()
|
||||
result = await ctx.wait(paginated)
|
||||
if result is not CONFIRMED:
|
||||
raise wire.ActionCancelled("Action cancelled")
|
||||
|
||||
|
||||
def _get_mosaic_properties(definition: NEMMosaicDefinition):
|
||||
|
@ -53,7 +53,7 @@ async def ask_aggregate_modification(
|
||||
|
||||
|
||||
async def _require_confirm_address(ctx, action: str, address: str):
|
||||
text = Text("Confirm address", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm address", ui.ICON_SEND, ui.GREEN)
|
||||
text.normal(action)
|
||||
text.mono(*split_address(address))
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
|
@ -48,7 +48,7 @@ async def ask_transfer_mosaic(
|
||||
mosaic_quantity = mosaic.quantity * transfer.amount / NEM_MOSAIC_AMOUNT_DIVISOR
|
||||
|
||||
if definition:
|
||||
msg = Text("Confirm mosaic", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
msg = Text("Confirm mosaic", ui.ICON_SEND, ui.GREEN)
|
||||
msg.normal("Confirm transfer of")
|
||||
msg.bold(
|
||||
format_amount(mosaic_quantity, definition["divisibility"])
|
||||
@ -60,20 +60,20 @@ async def ask_transfer_mosaic(
|
||||
|
||||
if "levy" in definition and "fee" in definition:
|
||||
levy_msg = _get_levy_msg(definition, mosaic_quantity, common.network)
|
||||
msg = Text("Confirm mosaic", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
msg = Text("Confirm mosaic", ui.ICON_SEND, ui.GREEN)
|
||||
msg.normal("Confirm mosaic", "levy fee of")
|
||||
msg.bold(levy_msg)
|
||||
await require_confirm(ctx, msg, ButtonRequestType.ConfirmOutput)
|
||||
|
||||
else:
|
||||
msg = Text("Confirm mosaic", ui.ICON_SEND, icon_color=ui.RED)
|
||||
msg = Text("Confirm mosaic", ui.ICON_SEND, ui.RED)
|
||||
msg.bold("Unknown mosaic!")
|
||||
msg.normal("Divisibility and levy")
|
||||
msg.normal("cannot be shown for")
|
||||
msg.normal("unknown mosaics")
|
||||
await require_confirm(ctx, msg, ButtonRequestType.ConfirmOutput)
|
||||
|
||||
msg = Text("Confirm mosaic", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
msg = Text("Confirm mosaic", ui.ICON_SEND, ui.GREEN)
|
||||
msg.normal("Confirm transfer of")
|
||||
msg.bold("%s raw units" % mosaic_quantity)
|
||||
msg.normal("of")
|
||||
@ -121,7 +121,7 @@ async def ask_importance_transfer(
|
||||
|
||||
|
||||
async def _require_confirm_transfer(ctx, recipient, value):
|
||||
text = Text("Confirm transfer", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm transfer", ui.ICON_SEND, ui.GREEN)
|
||||
text.bold("Send %s XEM" % format_amount(value, NEM_MAX_DIVISIBILITY))
|
||||
text.normal("to")
|
||||
text.mono(*split_address(recipient))
|
||||
@ -132,11 +132,11 @@ async def _require_confirm_payload(ctx, payload: bytearray, encrypt=False):
|
||||
payload = bytes(payload).decode()
|
||||
|
||||
if encrypt:
|
||||
text = Text("Confirm payload", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm payload", ui.ICON_SEND, ui.GREEN)
|
||||
text.bold("Encrypted:")
|
||||
text.normal(*payload.split(" "))
|
||||
else:
|
||||
text = Text("Confirm payload", ui.ICON_SEND, icon_color=ui.RED)
|
||||
text = Text("Confirm payload", ui.ICON_SEND, ui.RED)
|
||||
text.bold("Unencrypted:")
|
||||
text.normal(*payload.split(" "))
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
|
@ -10,14 +10,14 @@ from apps.common.layout import split_address
|
||||
|
||||
|
||||
async def require_confirm_fee(ctx, fee):
|
||||
text = Text("Confirm fee", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm fee", ui.ICON_SEND, ui.GREEN)
|
||||
text.normal("Transaction fee:")
|
||||
text.bold(format_amount(fee, helpers.DIVISIBILITY) + " XRP")
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
|
||||
|
||||
async def require_confirm_destination_tag(ctx, tag):
|
||||
text = Text("Confirm tag", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm tag", ui.ICON_SEND, ui.GREEN)
|
||||
text.normal("Destination tag:")
|
||||
text.bold(str(tag))
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
@ -25,7 +25,7 @@ async def require_confirm_destination_tag(ctx, tag):
|
||||
|
||||
async def require_confirm_tx(ctx, to, value):
|
||||
|
||||
text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN)
|
||||
text.bold(format_amount(value, helpers.DIVISIBILITY) + " XRP")
|
||||
text.normal("to")
|
||||
text.mono(*split_address(to))
|
||||
|
@ -9,7 +9,7 @@ from apps.stellar import consts
|
||||
async def require_confirm_init(
|
||||
ctx, address: str, network_passphrase: str, accounts_match: bool
|
||||
):
|
||||
text = Text("Confirm Stellar", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm Stellar", ui.ICON_SEND, ui.GREEN)
|
||||
text.normal("Initialize signing with")
|
||||
if accounts_match:
|
||||
text.normal("your account")
|
||||
@ -19,14 +19,14 @@ async def require_confirm_init(
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
network = get_network_warning(network_passphrase)
|
||||
if network:
|
||||
text = Text("Confirm network", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm network", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.normal("Transaction is on")
|
||||
text.bold(network)
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
|
||||
|
||||
async def require_confirm_memo(ctx, memo_type: int, memo_text: str):
|
||||
text = Text("Confirm memo", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm memo", ui.ICON_CONFIRM, ui.GREEN)
|
||||
if memo_type == consts.MEMO_TYPE_TEXT:
|
||||
text.bold("Memo (TEXT)")
|
||||
elif memo_type == consts.MEMO_TYPE_ID:
|
||||
@ -47,7 +47,7 @@ async def require_confirm_final(ctx, fee: int, num_operations: int):
|
||||
op_str = str(num_operations) + " operation"
|
||||
if num_operations > 1:
|
||||
op_str += "s"
|
||||
text = Text("Final confirm", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Final confirm", ui.ICON_SEND, ui.GREEN)
|
||||
text.normal("Sign this transaction")
|
||||
text.normal("made up of " + op_str)
|
||||
text.bold("and pay " + format_amount(fee))
|
||||
|
@ -21,7 +21,7 @@ from apps.stellar.layout import format_amount, require_confirm, split, trim_to_r
|
||||
|
||||
|
||||
async def confirm_source_account(ctx, source_account: bytes):
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold("Source account:")
|
||||
text.mono(*split(source_account))
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
@ -32,7 +32,7 @@ async def confirm_allow_trust_op(ctx, op: StellarAllowTrustOp):
|
||||
t = "Allow Trust"
|
||||
else:
|
||||
t = "Revoke Trust"
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold(t)
|
||||
text.normal("of %s by:" % op.asset_code)
|
||||
text.mono(*split(trim_to_rows(op.trusted_account, 3)))
|
||||
@ -41,7 +41,7 @@ async def confirm_allow_trust_op(ctx, op: StellarAllowTrustOp):
|
||||
|
||||
|
||||
async def confirm_account_merge_op(ctx, op: StellarAccountMergeOp):
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold("Account Merge")
|
||||
text.normal("All XLM will be sent to:")
|
||||
text.mono(*split(trim_to_rows(op.destination_account, 3)))
|
||||
@ -49,7 +49,7 @@ async def confirm_account_merge_op(ctx, op: StellarAccountMergeOp):
|
||||
|
||||
|
||||
async def confirm_bump_sequence_op(ctx, op: StellarBumpSequenceOp):
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold("Bump Sequence")
|
||||
text.normal("Set sequence to")
|
||||
text.mono(str(op.bump_to))
|
||||
@ -61,7 +61,7 @@ async def confirm_change_trust_op(ctx, op: StellarChangeTrustOp):
|
||||
t = "Delete Trust"
|
||||
else:
|
||||
t = "Add Trust"
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold(t)
|
||||
text.normal("Asset: %s" % op.asset.code)
|
||||
text.normal("Amount: %s" % format_amount(op.limit, ticker=False))
|
||||
@ -70,7 +70,7 @@ async def confirm_change_trust_op(ctx, op: StellarChangeTrustOp):
|
||||
|
||||
|
||||
async def confirm_create_account_op(ctx, op: StellarCreateAccountOp):
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold("Create Account")
|
||||
text.normal("with %s" % format_amount(op.starting_balance))
|
||||
text.mono(*split(trim_to_rows(op.new_account, 3)))
|
||||
@ -98,7 +98,7 @@ async def confirm_manage_offer_op(ctx, op: StellarManageOfferOp):
|
||||
|
||||
|
||||
async def _confirm_offer(ctx, title, op):
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold(title)
|
||||
text.normal(
|
||||
"Sell %s %s" % (format_amount(op.amount, ticker=False), op.selling_asset.code)
|
||||
@ -117,28 +117,28 @@ async def confirm_manage_data_op(ctx, op: StellarManageDataOp):
|
||||
title = "Set"
|
||||
else:
|
||||
title = "Clear"
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold("%s data value key" % title)
|
||||
text.mono(*split(op.key))
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
if op.value:
|
||||
digest = sha256(op.value).digest()
|
||||
digest_str = hexlify(digest).decode()
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold("Value (SHA-256):")
|
||||
text.mono(*split(digest_str))
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
|
||||
|
||||
async def confirm_path_payment_op(ctx, op: StellarPathPaymentOp):
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold("Path Pay %s" % format_amount(op.destination_amount, ticker=False))
|
||||
text.bold("%s to:" % format_asset_code(op.destination_asset))
|
||||
text.mono(*split(trim_to_rows(op.destination_account, 3)))
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
await confirm_asset_issuer(ctx, op.destination_asset)
|
||||
# confirm what the sender is using to pay
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.normal("Pay using")
|
||||
text.bold(format_amount(op.send_max, ticker=False))
|
||||
text.bold(format_asset_code(op.send_asset))
|
||||
@ -149,7 +149,7 @@ async def confirm_path_payment_op(ctx, op: StellarPathPaymentOp):
|
||||
|
||||
|
||||
async def confirm_payment_op(ctx, op: StellarPaymentOp):
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold("Pay %s" % format_amount(op.amount, ticker=False))
|
||||
text.bold("%s to:" % format_asset_code(op.asset))
|
||||
text.mono(*split(trim_to_rows(op.destination_account, 3)))
|
||||
@ -159,30 +159,30 @@ async def confirm_payment_op(ctx, op: StellarPaymentOp):
|
||||
|
||||
async def confirm_set_options_op(ctx, op: StellarSetOptionsOp):
|
||||
if op.inflation_destination_account:
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold("Set Inflation Destination")
|
||||
text.mono(*split(op.inflation_destination_account))
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
if op.clear_flags:
|
||||
t = _format_flags(op.clear_flags)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold("Clear Flags")
|
||||
text.mono(*t)
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
if op.set_flags:
|
||||
t = _format_flags(op.set_flags)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold("Set Flags")
|
||||
text.mono(*t)
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
thresholds = _format_thresholds(op)
|
||||
if thresholds:
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold("Account Thresholds")
|
||||
text.mono(*thresholds)
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
if op.home_domain:
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold("Home Domain")
|
||||
text.mono(*split(op.home_domain))
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
@ -192,7 +192,7 @@ async def confirm_set_options_op(ctx, op: StellarSetOptionsOp):
|
||||
else:
|
||||
t = "Remove Signer (%s)"
|
||||
if op.signer_type == consts.SIGN_TYPE_ACCOUNT:
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold(t % "acc")
|
||||
text.mono(*split(helpers.address_from_public_key(op.signer_key)))
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
@ -201,7 +201,7 @@ async def confirm_set_options_op(ctx, op: StellarSetOptionsOp):
|
||||
signer_type = "auth"
|
||||
else:
|
||||
signer_type = "hash"
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold(t % signer_type)
|
||||
text.mono(*split(hexlify(op.signer_key).decode()))
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
@ -244,7 +244,7 @@ def format_asset_code(asset: StellarAssetType) -> str:
|
||||
async def confirm_asset_issuer(ctx, asset: StellarAssetType):
|
||||
if asset is None or asset.type == consts.ASSET_TYPE_NATIVE:
|
||||
return
|
||||
text = Text("Confirm issuer", ui.ICON_CONFIRM, icon_color=ui.GREEN)
|
||||
text = Text("Confirm issuer", ui.ICON_CONFIRM, ui.GREEN)
|
||||
text.bold("%s issuer:" % asset.code)
|
||||
text.mono(*split(asset.issuer))
|
||||
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
|
@ -26,6 +26,6 @@ async def get_public_key(ctx, msg, keychain):
|
||||
|
||||
async def _show_tezos_pubkey(ctx, pubkey):
|
||||
lines = chunks(pubkey, 18)
|
||||
text = Text("Confirm public key", ui.ICON_RECEIVE, icon_color=ui.GREEN)
|
||||
text = Text("Confirm public key", ui.ICON_RECEIVE, ui.GREEN)
|
||||
text.mono(*lines)
|
||||
return await require_confirm(ctx, text, code=ButtonRequestType.PublicKey)
|
||||
|
@ -1,10 +1,6 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import ui, wire
|
||||
from trezor.messages import ButtonRequestType, MessageType
|
||||
from trezor.messages.ButtonRequest import ButtonRequest
|
||||
from trezor.ui.confirm import CANCELLED, ConfirmDialog
|
||||
from trezor.ui.scroll import Scrollpage, animate_swipe, paginate
|
||||
from trezor import ui
|
||||
from trezor.messages import ButtonRequestType
|
||||
from trezor.ui.scroll import Paginated
|
||||
from trezor.ui.text import Text
|
||||
from trezor.utils import chunks, format_amount
|
||||
|
||||
@ -13,7 +9,7 @@ from apps.tezos.helpers import TEZOS_AMOUNT_DIVISIBILITY
|
||||
|
||||
|
||||
async def require_confirm_tx(ctx, to, value):
|
||||
text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN)
|
||||
text.bold(format_tezos_amount(value))
|
||||
text.normal("to")
|
||||
text.mono(*split_address(to))
|
||||
@ -21,7 +17,7 @@ async def require_confirm_tx(ctx, to, value):
|
||||
|
||||
|
||||
async def require_confirm_fee(ctx, value, fee):
|
||||
text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
text.normal("Amount:")
|
||||
text.bold(format_tezos_amount(value))
|
||||
text.normal("Fee:")
|
||||
@ -30,14 +26,14 @@ async def require_confirm_fee(ctx, value, fee):
|
||||
|
||||
|
||||
async def require_confirm_origination(ctx, address):
|
||||
text = Text("Confirm origination", ui.ICON_SEND, icon_color=ui.ORANGE)
|
||||
text = Text("Confirm origination", ui.ICON_SEND, ui.ORANGE)
|
||||
text.normal("Address:")
|
||||
text.mono(*split_address(address))
|
||||
return await require_confirm(ctx, text, ButtonRequestType.SignTx)
|
||||
|
||||
|
||||
async def require_confirm_origination_fee(ctx, balance, fee):
|
||||
text = Text("Confirm origination", ui.ICON_SEND, icon_color=ui.ORANGE)
|
||||
text = Text("Confirm origination", ui.ICON_SEND, ui.ORANGE)
|
||||
text.normal("Balance:")
|
||||
text.bold(format_tezos_amount(balance))
|
||||
text.normal("Fee:")
|
||||
@ -46,21 +42,21 @@ async def require_confirm_origination_fee(ctx, balance, fee):
|
||||
|
||||
|
||||
async def require_confirm_delegation_baker(ctx, baker):
|
||||
text = Text("Confirm delegation", ui.ICON_SEND, icon_color=ui.BLUE)
|
||||
text = Text("Confirm delegation", ui.ICON_SEND, ui.BLUE)
|
||||
text.normal("Baker address:")
|
||||
text.mono(*split_address(baker))
|
||||
return await require_confirm(ctx, text, ButtonRequestType.SignTx)
|
||||
|
||||
|
||||
async def require_confirm_set_delegate(ctx, fee):
|
||||
text = Text("Confirm delegation", ui.ICON_SEND, icon_color=ui.BLUE)
|
||||
text = Text("Confirm delegation", ui.ICON_SEND, ui.BLUE)
|
||||
text.normal("Fee:")
|
||||
text.bold(format_tezos_amount(fee))
|
||||
await require_hold_to_confirm(ctx, text, ButtonRequestType.SignTx)
|
||||
|
||||
|
||||
async def require_confirm_register_delegate(ctx, address, fee):
|
||||
text = Text("Register delegate", ui.ICON_SEND, icon_color=ui.BLUE)
|
||||
text = Text("Register delegate", ui.ICON_SEND, ui.BLUE)
|
||||
text.bold("Fee: " + format_tezos_amount(fee))
|
||||
text.normal("Address:")
|
||||
text.mono(*split_address(address))
|
||||
@ -88,28 +84,18 @@ async def require_confirm_ballot(ctx, proposal, ballot):
|
||||
await require_confirm(ctx, text, ButtonRequestType.SignTx)
|
||||
|
||||
|
||||
# use, when there are more then one proposals in one operation
|
||||
async def require_confirm_proposals(ctx, proposals):
|
||||
await ctx.call(ButtonRequest(code=ButtonRequestType.SignTx), MessageType.ButtonAck)
|
||||
first_page = const(0)
|
||||
pages = proposals
|
||||
title = "Submit proposals" if len(proposals) > 1 else "Submit proposal"
|
||||
|
||||
paginator = paginate(show_proposal_page, len(pages), first_page, pages, title)
|
||||
return await ctx.wait(paginator)
|
||||
|
||||
|
||||
@ui.layout
|
||||
async def show_proposal_page(page: int, page_count: int, pages: list, title: str):
|
||||
text = Text(title, ui.ICON_SEND, icon_color=ui.PURPLE)
|
||||
text.bold("Proposal {}: ".format(page + 1))
|
||||
text.mono(*split_proposal(pages[page]))
|
||||
content = Scrollpage(text, page, page_count)
|
||||
|
||||
if page + 1 >= page_count:
|
||||
confirm = await ConfirmDialog(content)
|
||||
if confirm == CANCELLED:
|
||||
raise wire.ActionCancelled("Cancelled")
|
||||
if len(proposals) > 1:
|
||||
title = "Submit proposals"
|
||||
else:
|
||||
content.render()
|
||||
await animate_swipe()
|
||||
title = "Submit proposal"
|
||||
|
||||
pages = []
|
||||
for page, proposal in enumerate(proposals):
|
||||
text = Text(title, ui.ICON_SEND, icon_color=ui.PURPLE)
|
||||
text.bold("Proposal {}: ".format(page + 1))
|
||||
text.mono(*split_proposal(proposal))
|
||||
pages.append(text)
|
||||
paginated = Paginated(pages)
|
||||
|
||||
await require_confirm(ctx, paginated, ButtonRequestType.SignTx)
|
||||
|
@ -29,19 +29,19 @@ async def confirm_output(ctx, output, coin):
|
||||
data = output.op_return_data
|
||||
if omni.is_valid(data):
|
||||
# OMNI transaction
|
||||
text = Text("OMNI transaction", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("OMNI transaction", ui.ICON_SEND, ui.GREEN)
|
||||
text.normal(omni.parse(data))
|
||||
else:
|
||||
# generic OP_RETURN
|
||||
data = hexlify(data).decode()
|
||||
if len(data) >= 18 * 5:
|
||||
data = data[: (18 * 5 - 3)] + "..."
|
||||
text = Text("OP_RETURN", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("OP_RETURN", ui.ICON_SEND, ui.GREEN)
|
||||
text.mono(*split_op_return(data))
|
||||
else:
|
||||
address = output.address
|
||||
address_short = addresses.address_short(coin, address)
|
||||
text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN)
|
||||
text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN)
|
||||
text.normal(format_coin_amount(output.amount, coin) + " to")
|
||||
text.mono(*split_address(address_short))
|
||||
return await confirm(ctx, text, ButtonRequestType.ConfirmOutput)
|
||||
|
@ -383,37 +383,32 @@ class ConfirmState:
|
||||
workflow.onclose(self.workflow)
|
||||
self.workflow = None
|
||||
|
||||
@ui.layout
|
||||
async def confirm_layout(self) -> None:
|
||||
workflow.webauthn_stop_signal.reset()
|
||||
await loop.spawn(self.confirm_layout_inner(), workflow.webauthn_stop_signal)
|
||||
|
||||
async def confirm_layout_inner(self) -> None:
|
||||
from trezor.ui.confirm import ConfirmDialog, CONFIRMED
|
||||
from trezor.ui.confirm import Confirm, CONFIRMED
|
||||
from trezor.ui.text import Text
|
||||
|
||||
app_id = bytes(self.app_id) # could be bytearray, which doesn't have __hash__
|
||||
|
||||
if app_id == _BOGUS_APPID and self.action == _CONFIRM_REGISTER:
|
||||
text = Text("U2F", ui.ICON_WRONG, icon_color=ui.RED)
|
||||
text = Text("U2F", ui.ICON_WRONG, ui.RED)
|
||||
text.normal(
|
||||
"Another U2F device", "was used to register", "in this application."
|
||||
)
|
||||
text.render()
|
||||
dialog = ConfirmDialog(text)
|
||||
dialog = Confirm(text)
|
||||
else:
|
||||
content = ConfirmContent(self.action, app_id)
|
||||
dialog = ConfirmDialog(content)
|
||||
dialog = Confirm(content)
|
||||
|
||||
self.confirmed = await dialog == CONFIRMED
|
||||
self.confirmed = await dialog is CONFIRMED
|
||||
|
||||
|
||||
class ConfirmContent(ui.Widget):
|
||||
class ConfirmContent(ui.Control):
|
||||
def __init__(self, action: int, app_id: bytes) -> None:
|
||||
self.action = action
|
||||
self.app_id = app_id
|
||||
self.app_name = None
|
||||
self.app_icon = None
|
||||
self.repaint = True
|
||||
self.boot()
|
||||
|
||||
def boot(self) -> None:
|
||||
@ -439,14 +434,18 @@ class ConfirmContent(ui.Widget):
|
||||
self.app_name = name
|
||||
self.app_icon = icon
|
||||
|
||||
def render(self) -> None:
|
||||
if self.action == _CONFIRM_REGISTER:
|
||||
header = "U2F Register"
|
||||
else:
|
||||
header = "U2F Authenticate"
|
||||
ui.header(header, ui.ICON_DEFAULT, ui.GREEN, ui.BG, ui.GREEN)
|
||||
ui.display.image((ui.WIDTH - 64) // 2, 64, self.app_icon)
|
||||
ui.display.text_center(ui.WIDTH // 2, 168, self.app_name, ui.MONO, ui.FG, ui.BG)
|
||||
def on_render(self) -> None:
|
||||
if self.repaint:
|
||||
if self.action == _CONFIRM_REGISTER:
|
||||
header = "U2F Register"
|
||||
else:
|
||||
header = "U2F Authenticate"
|
||||
ui.header(header, ui.ICON_DEFAULT, ui.GREEN, ui.BG, ui.GREEN)
|
||||
ui.display.image((ui.WIDTH - 64) // 2, 64, self.app_icon)
|
||||
ui.display.text_center(
|
||||
ui.WIDTH // 2, 168, self.app_name, ui.MONO, ui.FG, ui.BG
|
||||
)
|
||||
self.repaint = False
|
||||
|
||||
|
||||
def dispatch_cmd(req: Cmd, state: ConfirmState) -> Cmd:
|
||||
|
@ -14,7 +14,7 @@ async def bootscreen():
|
||||
storage.init_unlocked()
|
||||
return
|
||||
await lockscreen()
|
||||
label = None
|
||||
label = "Enter your PIN"
|
||||
while True:
|
||||
pin = await request_pin(label, config.get_pin_rem())
|
||||
if config.unlock(pin_to_int(pin)):
|
||||
@ -35,7 +35,7 @@ async def lockscreen():
|
||||
if not image:
|
||||
image = res.load("apps/homescreen/res/bg.toif")
|
||||
|
||||
await ui.backlight_slide(ui.BACKLIGHT_DIM)
|
||||
ui.backlight_fade(ui.BACKLIGHT_DIM)
|
||||
|
||||
ui.display.clear()
|
||||
ui.display.avatar(48, 48, image, ui.TITLE_GREY, ui.BG)
|
||||
@ -50,12 +50,13 @@ async def lockscreen():
|
||||
)
|
||||
ui.display.icon(45, 202, res.load(ui.ICON_CLICK), ui.TITLE_GREY, ui.BG)
|
||||
|
||||
await ui.backlight_slide(ui.BACKLIGHT_NORMAL)
|
||||
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
|
||||
|
||||
await ui.click()
|
||||
|
||||
|
||||
ui.display.backlight(ui.BACKLIGHT_NONE)
|
||||
ui.backlight_slide_sync(ui.BACKLIGHT_NORMAL)
|
||||
ui.backlight_fade(ui.BACKLIGHT_NORMAL)
|
||||
config.init(show_pin_timeout)
|
||||
loop.schedule(bootscreen())
|
||||
loop.run()
|
||||
|
@ -18,6 +18,7 @@ after_step_hook = None # function, called after each task step
|
||||
_QUEUE_SIZE = const(64) # maximum number of scheduled tasks
|
||||
_queue = utimeq.utimeq(_QUEUE_SIZE)
|
||||
_paused = {}
|
||||
_finalizers = {}
|
||||
|
||||
if __debug__:
|
||||
# for performance stats
|
||||
@ -28,13 +29,15 @@ if __debug__:
|
||||
log_delay_rb = array.array("i", [0] * log_delay_rb_len)
|
||||
|
||||
|
||||
def schedule(task, value=None, deadline=None):
|
||||
def schedule(task, value=None, deadline=None, finalizer=None):
|
||||
"""
|
||||
Schedule task to be executed with `value` on given `deadline` (in
|
||||
microseconds). Does not start the event loop itself, see `run`.
|
||||
"""
|
||||
if deadline is None:
|
||||
deadline = utime.ticks_us()
|
||||
if finalizer is not None:
|
||||
_finalizers[id(task)] = finalizer
|
||||
_queue.push(deadline, task, value)
|
||||
|
||||
|
||||
@ -45,11 +48,18 @@ def pause(task, iface):
|
||||
tasks.add(task)
|
||||
|
||||
|
||||
def finalize(task, value):
|
||||
fn = _finalizers.pop(id(task), None)
|
||||
if fn is not None:
|
||||
fn(task, value)
|
||||
|
||||
|
||||
def close(task):
|
||||
for iface in _paused:
|
||||
_paused[iface].discard(task)
|
||||
_queue.discard(task)
|
||||
task.close()
|
||||
finalize(task, GeneratorExit())
|
||||
|
||||
|
||||
def run():
|
||||
@ -93,16 +103,18 @@ def run():
|
||||
|
||||
def _step(task, value):
|
||||
try:
|
||||
if isinstance(value, Exception):
|
||||
if isinstance(value, BaseException):
|
||||
result = task.throw(value)
|
||||
else:
|
||||
result = task.send(value)
|
||||
except StopIteration: # as e:
|
||||
except StopIteration as e: # as e:
|
||||
if __debug__:
|
||||
log.debug(__name__, "finish: %s", task)
|
||||
finalize(task, e.value)
|
||||
except Exception as e:
|
||||
if __debug__:
|
||||
log.exception(__name__, e)
|
||||
finalize(task, e)
|
||||
else:
|
||||
if isinstance(result, Syscall):
|
||||
result.handle(task)
|
||||
@ -213,6 +225,9 @@ class signal(Syscall):
|
||||
raise
|
||||
|
||||
|
||||
_type_gen = type((lambda: (yield))())
|
||||
|
||||
|
||||
class spawn(Syscall):
|
||||
"""
|
||||
Execute one or more children tasks and wait until one of them exits.
|
||||
@ -241,39 +256,41 @@ class spawn(Syscall):
|
||||
def __init__(self, *children, exit_others=True):
|
||||
self.children = children
|
||||
self.exit_others = exit_others
|
||||
self.scheduled = None # list of scheduled wrapper tasks
|
||||
self.finished = None # list of children that finished
|
||||
self.scheduled = [] # list of scheduled tasks
|
||||
self.finished = [] # list of children that finished
|
||||
self.callback = None
|
||||
|
||||
def handle(self, task):
|
||||
finalizer = self._finish
|
||||
scheduled = self.scheduled
|
||||
finished = self.finished
|
||||
|
||||
self.callback = task
|
||||
self.finished = []
|
||||
self.scheduled = []
|
||||
for index, child in enumerate(self.children):
|
||||
parent = self._wait(child, index)
|
||||
schedule(parent)
|
||||
self.scheduled.append(parent)
|
||||
scheduled.clear()
|
||||
finished.clear()
|
||||
|
||||
def exit(self, skip_index=-1):
|
||||
for index, parent in enumerate(self.scheduled):
|
||||
if index != skip_index:
|
||||
close(parent)
|
||||
for child in self.children:
|
||||
if isinstance(child, _type_gen):
|
||||
child_task = child
|
||||
else:
|
||||
child_task = iter(child)
|
||||
schedule(child_task, None, None, finalizer)
|
||||
scheduled.append(child_task)
|
||||
|
||||
async def _wait(self, child, index):
|
||||
try:
|
||||
result = await child
|
||||
except Exception as e:
|
||||
self._finish(child, index, e)
|
||||
if __debug__:
|
||||
log.exception(__name__, e)
|
||||
else:
|
||||
self._finish(child, index, result)
|
||||
def exit(self, except_for=None):
|
||||
for task in self.scheduled:
|
||||
if task != except_for:
|
||||
close(task)
|
||||
|
||||
def _finish(self, child, index, result):
|
||||
def _finish(self, task, result):
|
||||
if not self.finished:
|
||||
for index, child_task in enumerate(self.scheduled):
|
||||
if child_task is task:
|
||||
child = self.children[index]
|
||||
break
|
||||
self.finished.append(child)
|
||||
if self.exit_others:
|
||||
self.exit(index)
|
||||
self.exit(task)
|
||||
schedule(self.callback, result)
|
||||
|
||||
def __iter__(self):
|
||||
@ -284,66 +301,3 @@ class spawn(Syscall):
|
||||
# close() or throw(), kill the children tasks and re-raise
|
||||
self.exit()
|
||||
raise
|
||||
|
||||
|
||||
class put(Syscall):
|
||||
def __init__(self, ch, value=None):
|
||||
self.ch = ch
|
||||
self.value = value
|
||||
|
||||
def __call__(self, value):
|
||||
self.value = value
|
||||
return self
|
||||
|
||||
def handle(self, task):
|
||||
self.ch.schedule_put(schedule, task, self.value)
|
||||
|
||||
|
||||
class take(Syscall):
|
||||
def __init__(self, ch):
|
||||
self.ch = ch
|
||||
|
||||
def __call__(self):
|
||||
return self
|
||||
|
||||
def handle(self, task):
|
||||
if self.ch.schedule_take(schedule, task) and self.ch.id is not None:
|
||||
pause(self.ch, self.ch.id)
|
||||
|
||||
|
||||
class chan:
|
||||
def __init__(self, id=None):
|
||||
self.id = id
|
||||
self.putters = []
|
||||
self.takers = []
|
||||
self.put = put(self)
|
||||
self.take = take(self)
|
||||
|
||||
def schedule_publish(self, schedule, value):
|
||||
if self.takers:
|
||||
for taker in self.takers:
|
||||
schedule(taker, value)
|
||||
self.takers.clear()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def schedule_put(self, schedule, putter, value):
|
||||
if self.takers:
|
||||
taker = self.takers.pop(0)
|
||||
schedule(taker, value)
|
||||
schedule(putter, value)
|
||||
return True
|
||||
else:
|
||||
self.putters.append((putter, value))
|
||||
return False
|
||||
|
||||
def schedule_take(self, schedule, taker):
|
||||
if self.putters:
|
||||
putter, value = self.putters.pop(0)
|
||||
schedule(taker, value)
|
||||
schedule(putter, value)
|
||||
return True
|
||||
else:
|
||||
self.takers.append(taker)
|
||||
return False
|
||||
|
@ -31,6 +31,10 @@ SIZE = Display.FONT_SIZE
|
||||
WIDTH = Display.WIDTH
|
||||
HEIGHT = Display.HEIGHT
|
||||
|
||||
# viewport margins
|
||||
VIEWX = const(6)
|
||||
VIEWY = const(9)
|
||||
|
||||
|
||||
def lerpi(a: int, b: int, t: float) -> int:
|
||||
return int(a + t * (b - a))
|
||||
@ -55,43 +59,9 @@ from trezor.ui import style # isort:skip
|
||||
from trezor.ui.style import * # isort:skip # noqa: F401,F403
|
||||
|
||||
|
||||
def contains(area: tuple, pos: tuple) -> bool:
|
||||
x, y = pos
|
||||
ax, ay, aw, ah = area
|
||||
return ax <= x <= ax + aw and ay <= y <= ay + ah
|
||||
|
||||
|
||||
def rotate(pos: tuple) -> tuple:
|
||||
r = display.orientation()
|
||||
if r == 0:
|
||||
return pos
|
||||
x, y = pos
|
||||
if r == 90:
|
||||
return (y, WIDTH - x)
|
||||
if r == 180:
|
||||
return (WIDTH - x, HEIGHT - y)
|
||||
if r == 270:
|
||||
return (HEIGHT - y, x)
|
||||
|
||||
|
||||
def pulse(delay: int):
|
||||
while True:
|
||||
# normalize sin from interval -1:1 to 0:1
|
||||
yield 0.5 + 0.5 * math.sin(utime.ticks_us() / delay)
|
||||
|
||||
|
||||
async def alert(count: int = 3):
|
||||
short_sleep = loop.sleep(20000)
|
||||
long_sleep = loop.sleep(80000)
|
||||
current = display.backlight()
|
||||
for i in range(count * 2):
|
||||
if i % 2 == 0:
|
||||
display.backlight(style.BACKLIGHT_MAX)
|
||||
yield short_sleep
|
||||
else:
|
||||
display.backlight(style.BACKLIGHT_NORMAL)
|
||||
yield long_sleep
|
||||
display.backlight(current)
|
||||
# normalize sin from interval -1:1 to 0:1
|
||||
return 0.5 + 0.5 * math.sin(utime.ticks_us() / delay)
|
||||
|
||||
|
||||
async def click() -> tuple:
|
||||
@ -107,53 +77,22 @@ async def click() -> tuple:
|
||||
return pos
|
||||
|
||||
|
||||
async def backlight_slide(val: int, delay: int = 35000, step: int = 20):
|
||||
sleep = loop.sleep(delay)
|
||||
def backlight_fade(val: int, delay: int = 14000, step: int = 15):
|
||||
if __debug__:
|
||||
if utils.DISABLE_FADE:
|
||||
display.backlight(val)
|
||||
return
|
||||
current = display.backlight()
|
||||
for i in range(current, val, -step if current > val else step):
|
||||
display.backlight(i)
|
||||
yield sleep
|
||||
|
||||
|
||||
def backlight_slide_sync(val: int, delay: int = 35000, step: int = 20):
|
||||
current = display.backlight()
|
||||
for i in range(current, val, -step if current > val else step):
|
||||
if current > val:
|
||||
step = -step
|
||||
for i in range(current, val, step):
|
||||
display.backlight(i)
|
||||
utime.sleep_us(delay)
|
||||
|
||||
|
||||
def layout(f):
|
||||
async def inner(*args, **kwargs):
|
||||
await backlight_slide(style.BACKLIGHT_DIM)
|
||||
slide = backlight_slide(style.BACKLIGHT_NORMAL)
|
||||
try:
|
||||
layout = f(*args, **kwargs)
|
||||
workflow.onlayoutstart(layout)
|
||||
loop.schedule(slide)
|
||||
display.clear()
|
||||
return await layout
|
||||
finally:
|
||||
loop.close(slide)
|
||||
workflow.onlayoutclose(layout)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def layout_no_slide(f):
|
||||
async def inner(*args, **kwargs):
|
||||
try:
|
||||
layout = f(*args, **kwargs)
|
||||
workflow.onlayoutstart(layout)
|
||||
return await layout
|
||||
finally:
|
||||
workflow.onlayoutclose(layout)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def header(
|
||||
title: str,
|
||||
icon: bytes = style.ICON_DEFAULT,
|
||||
icon: str = style.ICON_DEFAULT,
|
||||
fg: int = style.FG,
|
||||
bg: int = style.BG,
|
||||
ifg: int = style.GREEN,
|
||||
@ -163,10 +102,6 @@ def header(
|
||||
display.text(44, 35, title, BOLD, fg, bg)
|
||||
|
||||
|
||||
VIEWX = const(6)
|
||||
VIEWY = const(9)
|
||||
|
||||
|
||||
def grid(
|
||||
i: int,
|
||||
n_x: int = 3,
|
||||
@ -186,23 +121,90 @@ def grid(
|
||||
return (x + start_x, y + start_y, (w - spacing) * cells_x, (h - spacing) * cells_y)
|
||||
|
||||
|
||||
class Widget:
|
||||
tainted = True
|
||||
def in_area(area: tuple, x: int, y: int) -> bool:
|
||||
ax, ay, aw, ah = area
|
||||
return ax <= x <= ax + aw and ay <= y <= ay + ah
|
||||
|
||||
def taint(self):
|
||||
self.tainted = True
|
||||
|
||||
def render(self):
|
||||
# render events
|
||||
RENDER = const(-255)
|
||||
REPAINT = const(-256)
|
||||
|
||||
|
||||
class Control:
|
||||
def dispatch(self, event, x, y):
|
||||
if event is RENDER:
|
||||
self.on_render()
|
||||
elif event is io.TOUCH_START:
|
||||
self.on_touch_start(x, y)
|
||||
elif event is io.TOUCH_MOVE:
|
||||
self.on_touch_move(x, y)
|
||||
elif event is io.TOUCH_END:
|
||||
self.on_touch_end(x, y)
|
||||
elif event is REPAINT:
|
||||
self.repaint = True
|
||||
|
||||
def on_render(self):
|
||||
pass
|
||||
|
||||
def touch(self, event, pos):
|
||||
def on_touch_start(self, x, y):
|
||||
pass
|
||||
|
||||
def __iter__(self):
|
||||
def on_touch_move(self, x, y):
|
||||
pass
|
||||
|
||||
def on_touch_end(self, x, y):
|
||||
pass
|
||||
|
||||
|
||||
_RENDER_DELAY_US = const(10000) # 10 msec
|
||||
|
||||
|
||||
class LayoutCancelled(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Result(Exception):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
|
||||
class Layout(Control):
|
||||
"""
|
||||
"""
|
||||
|
||||
async def __iter__(self):
|
||||
value = None
|
||||
try:
|
||||
if workflow.layout_signal.task is not None:
|
||||
workflow.layout_signal.send(LayoutCancelled())
|
||||
workflow.onlayoutstart(self)
|
||||
while True:
|
||||
layout_tasks = self.create_tasks()
|
||||
await loop.spawn(workflow.layout_signal, *layout_tasks)
|
||||
except Result as result:
|
||||
value = result.value
|
||||
finally:
|
||||
workflow.onlayoutclose(self)
|
||||
return value
|
||||
|
||||
def create_tasks(self):
|
||||
return self.handle_input(), self.handle_rendering()
|
||||
|
||||
def handle_input(self):
|
||||
touch = loop.wait(io.TOUCH)
|
||||
result = None
|
||||
while result is None:
|
||||
self.render()
|
||||
event, *pos = yield touch
|
||||
result = self.touch(event, pos)
|
||||
return result
|
||||
while True:
|
||||
event, x, y = yield touch
|
||||
self.dispatch(event, x, y)
|
||||
self.dispatch(RENDER, 0, 0)
|
||||
|
||||
def handle_rendering(self):
|
||||
backlight_fade(style.BACKLIGHT_DIM)
|
||||
display.clear()
|
||||
self.dispatch(RENDER, 0, 0)
|
||||
display.refresh()
|
||||
backlight_fade(style.BACKLIGHT_NORMAL)
|
||||
sleep = loop.sleep(_RENDER_DELAY_US)
|
||||
while True:
|
||||
self.dispatch(RENDER, 0, 0)
|
||||
yield sleep
|
||||
|
@ -1,115 +1,213 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import io, ui
|
||||
from trezor.ui import Widget, contains, display, rotate
|
||||
from trezor import ui
|
||||
from trezor.ui import display, in_area
|
||||
|
||||
|
||||
class ButtonDefault:
|
||||
class normal:
|
||||
bg_color = ui.BLACKISH
|
||||
fg_color = ui.FG
|
||||
text_style = ui.BOLD
|
||||
border_color = ui.BG
|
||||
radius = ui.RADIUS
|
||||
|
||||
class active:
|
||||
bg_color = ui.FG
|
||||
fg_color = ui.BLACKISH
|
||||
text_style = ui.BOLD
|
||||
border_color = ui.FG
|
||||
radius = ui.RADIUS
|
||||
|
||||
class disabled:
|
||||
bg_color = ui.BG
|
||||
fg_color = ui.GREY
|
||||
text_style = ui.NORMAL
|
||||
border_color = ui.BG
|
||||
radius = ui.RADIUS
|
||||
|
||||
|
||||
class ButtonMono(ButtonDefault):
|
||||
class normal(ButtonDefault.normal):
|
||||
text_style = ui.MONO
|
||||
|
||||
class active(ButtonDefault.active):
|
||||
text_style = ui.MONO
|
||||
|
||||
class disabled(ButtonDefault.disabled):
|
||||
text_style = ui.MONO
|
||||
|
||||
|
||||
class ButtonMonoDark:
|
||||
class normal:
|
||||
bg_color = ui.DARK_BLACK
|
||||
fg_color = ui.DARK_WHITE
|
||||
text_style = ui.MONO
|
||||
border_color = ui.BG
|
||||
radius = ui.RADIUS
|
||||
|
||||
class active:
|
||||
bg_color = ui.FG
|
||||
fg_color = ui.DARK_BLACK
|
||||
text_style = ui.MONO
|
||||
border_color = ui.FG
|
||||
radius = ui.RADIUS
|
||||
|
||||
class disabled:
|
||||
bg_color = ui.DARK_BLACK
|
||||
fg_color = ui.GREY
|
||||
text_style = ui.MONO
|
||||
border_color = ui.BG
|
||||
radius = ui.RADIUS
|
||||
|
||||
|
||||
class ButtonConfirm(ButtonDefault):
|
||||
class normal(ButtonDefault.normal):
|
||||
bg_color = ui.GREEN
|
||||
|
||||
class active(ButtonDefault.active):
|
||||
fg_color = ui.GREEN
|
||||
|
||||
|
||||
class ButtonCancel(ButtonDefault):
|
||||
class normal(ButtonDefault.normal):
|
||||
bg_color = ui.RED
|
||||
|
||||
class active(ButtonDefault.active):
|
||||
fg_color = ui.RED
|
||||
|
||||
|
||||
class ButtonClear(ButtonDefault):
|
||||
class normal(ButtonDefault.normal):
|
||||
bg_color = ui.ORANGE
|
||||
|
||||
class active(ButtonDefault.active):
|
||||
fg_color = ui.ORANGE
|
||||
|
||||
|
||||
class ButtonMonoConfirm(ButtonDefault):
|
||||
class normal(ButtonDefault.normal):
|
||||
text_style = ui.MONO
|
||||
bg_color = ui.GREEN
|
||||
|
||||
class active(ButtonDefault.active):
|
||||
text_style = ui.MONO
|
||||
fg_color = ui.GREEN
|
||||
|
||||
class disabled(ButtonDefault.disabled):
|
||||
text_style = ui.MONO
|
||||
|
||||
# button events
|
||||
BTN_CLICKED = const(1)
|
||||
|
||||
# button states
|
||||
BTN_INITIAL = const(0)
|
||||
BTN_DISABLED = const(1)
|
||||
BTN_FOCUSED = const(2)
|
||||
BTN_ACTIVE = const(3)
|
||||
_INITIAL = const(0)
|
||||
_PRESSED = const(1)
|
||||
_RELEASED = const(2)
|
||||
_DISABLED = const(3)
|
||||
|
||||
# constants
|
||||
ICON = const(16) # icon size in pixels
|
||||
BORDER = const(4) # border size in pixels
|
||||
# button constants
|
||||
_ICON = const(16) # icon size in pixels
|
||||
_BORDER = const(4) # border size in pixels
|
||||
|
||||
|
||||
class Button(Widget):
|
||||
def __init__(self, area: tuple, content: str, style: dict = ui.BTN_KEY):
|
||||
class Button(ui.Control):
|
||||
def __init__(self, area, content, style=ButtonDefault):
|
||||
self.area = area
|
||||
self.content = content
|
||||
self.normal_style = style["normal"] or ui.BTN_KEY["normal"]
|
||||
self.active_style = style["active"] or ui.BTN_KEY["active"]
|
||||
self.disabled_style = style["disabled"] or ui.BTN_KEY["disabled"]
|
||||
self.state = BTN_INITIAL
|
||||
self.normal_style = style.normal
|
||||
self.active_style = style.active
|
||||
self.disabled_style = style.disabled
|
||||
self.state = _INITIAL
|
||||
self.repaint = True
|
||||
|
||||
def enable(self):
|
||||
if self.state == BTN_DISABLED:
|
||||
self.state = BTN_INITIAL
|
||||
self.tainted = True
|
||||
if self.state is not _INITIAL:
|
||||
self.state = _INITIAL
|
||||
self.repaint = True
|
||||
|
||||
def disable(self):
|
||||
if self.state != BTN_DISABLED:
|
||||
self.state = BTN_DISABLED
|
||||
self.tainted = True
|
||||
if self.state is not _DISABLED:
|
||||
self.state = _DISABLED
|
||||
self.repaint = True
|
||||
|
||||
def is_enabled(self):
|
||||
return self.state != BTN_DISABLED
|
||||
|
||||
def render(self):
|
||||
if not self.tainted:
|
||||
return
|
||||
state = self.state
|
||||
if state == BTN_DISABLED:
|
||||
s = self.disabled_style
|
||||
elif state == BTN_ACTIVE:
|
||||
s = self.active_style
|
||||
else:
|
||||
s = self.normal_style
|
||||
ax, ay, aw, ah = self.area
|
||||
self.render_background(s, ax, ay, aw, ah)
|
||||
self.render_content(s, ax, ay, aw, ah)
|
||||
self.tainted = False
|
||||
def on_render(self):
|
||||
if self.repaint:
|
||||
if self.state is _DISABLED:
|
||||
s = self.disabled_style
|
||||
elif self.state is _PRESSED:
|
||||
s = self.active_style
|
||||
else:
|
||||
s = self.normal_style
|
||||
ax, ay, aw, ah = self.area
|
||||
self.render_background(s, ax, ay, aw, ah)
|
||||
self.render_content(s, ax, ay, aw, ah)
|
||||
self.repaint = False
|
||||
|
||||
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:
|
||||
radius = s.radius
|
||||
bg_color = s.bg_color
|
||||
border_color = s.border_color
|
||||
if border_color == bg_color:
|
||||
# we don't need to render the border
|
||||
display.bar_radius(ax, ay, aw, ah, bg_color, ui.BG, radius)
|
||||
else:
|
||||
# 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,
|
||||
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"])
|
||||
t = self.content
|
||||
if isinstance(t, str):
|
||||
display.text_center(tx, ty, t, s.text_style, s.fg_color, s.bg_color)
|
||||
elif isinstance(t, bytes):
|
||||
display.icon(tx - _ICON // 2, ty - _ICON, t, s.fg_color, s.bg_color)
|
||||
|
||||
def touch(self, event, pos):
|
||||
pos = rotate(pos)
|
||||
|
||||
state = self.state
|
||||
if state == BTN_DISABLED:
|
||||
def on_touch_start(self, x, y):
|
||||
if self.state is _DISABLED:
|
||||
return
|
||||
if in_area(self.area, x, y):
|
||||
self.state = _PRESSED
|
||||
self.repaint = True
|
||||
self.on_press_start()
|
||||
|
||||
if event == io.TOUCH_START:
|
||||
if contains(self.area, pos):
|
||||
self.state = BTN_ACTIVE
|
||||
self.tainted = True
|
||||
def on_touch_move(self, x, y):
|
||||
if self.state is _DISABLED:
|
||||
return
|
||||
if in_area(self.area, x, y):
|
||||
if self.state is _RELEASED:
|
||||
self.state = _PRESSED
|
||||
self.repaint = True
|
||||
self.on_press_start()
|
||||
else:
|
||||
if self.state is _PRESSED:
|
||||
self.state = _RELEASED
|
||||
self.repaint = True
|
||||
self.on_press_end()
|
||||
|
||||
elif event == io.TOUCH_MOVE:
|
||||
if contains(self.area, pos):
|
||||
if state == BTN_FOCUSED:
|
||||
self.state = BTN_ACTIVE
|
||||
self.tainted = True
|
||||
else:
|
||||
if state == BTN_ACTIVE:
|
||||
self.state = BTN_FOCUSED
|
||||
self.tainted = True
|
||||
def on_touch_end(self, x, y):
|
||||
state = self.state
|
||||
if state is not _INITIAL and state is not _DISABLED:
|
||||
self.state = _INITIAL
|
||||
self.repaint = True
|
||||
if in_area(self.area, x, y):
|
||||
if state is _PRESSED:
|
||||
self.on_press_end()
|
||||
self.on_click()
|
||||
|
||||
elif event == io.TOUCH_END:
|
||||
if state != BTN_INITIAL:
|
||||
self.state = BTN_INITIAL
|
||||
self.tainted = True
|
||||
if state == BTN_ACTIVE and contains(self.area, pos):
|
||||
return BTN_CLICKED
|
||||
def on_press_start(self):
|
||||
pass
|
||||
|
||||
def on_press_end(self):
|
||||
pass
|
||||
|
||||
def on_click(self):
|
||||
pass
|
||||
|
@ -1,115 +1,106 @@
|
||||
from micropython import const
|
||||
from trezor import res, ui
|
||||
from trezor.ui.button import Button, ButtonCancel, ButtonConfirm
|
||||
from trezor.ui.loader import Loader, LoaderDefault
|
||||
|
||||
from trezor import loop, res, ui
|
||||
from trezor.ui import Widget
|
||||
from trezor.ui.button import BTN_ACTIVE, BTN_CLICKED, Button
|
||||
from trezor.ui.loader import Loader
|
||||
|
||||
if __debug__:
|
||||
from apps.debug import confirm_signal
|
||||
|
||||
CONFIRMED = const(1)
|
||||
CANCELLED = const(2)
|
||||
DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM)
|
||||
DEFAULT_CANCEL = res.load(ui.ICON_CANCEL)
|
||||
CONFIRMED = object()
|
||||
CANCELLED = object()
|
||||
|
||||
|
||||
class ConfirmDialog(Widget):
|
||||
class Confirm(ui.Layout):
|
||||
DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM)
|
||||
DEFAULT_CONFIRM_STYLE = ButtonConfirm
|
||||
DEFAULT_CANCEL = res.load(ui.ICON_CANCEL)
|
||||
DEFAULT_CANCEL_STYLE = ButtonCancel
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content,
|
||||
confirm=DEFAULT_CONFIRM,
|
||||
confirm_style=DEFAULT_CONFIRM_STYLE,
|
||||
cancel=DEFAULT_CANCEL,
|
||||
confirm_style=ui.BTN_CONFIRM,
|
||||
cancel_style=ui.BTN_CANCEL,
|
||||
cancel_style=DEFAULT_CANCEL_STYLE,
|
||||
):
|
||||
self.content = content
|
||||
if cancel is not None:
|
||||
self.confirm = Button(ui.grid(9, n_x=2), confirm, style=confirm_style)
|
||||
self.cancel = Button(ui.grid(8, n_x=2), cancel, style=cancel_style)
|
||||
|
||||
if confirm is not None:
|
||||
if cancel is None:
|
||||
area = ui.grid(4, n_x=1)
|
||||
else:
|
||||
area = ui.grid(9, n_x=2)
|
||||
self.confirm = Button(area, confirm, confirm_style)
|
||||
self.confirm.on_click = self.on_confirm
|
||||
else:
|
||||
self.confirm = None
|
||||
|
||||
if cancel is not None:
|
||||
if confirm is None:
|
||||
area = ui.grid(4, n_x=1)
|
||||
else:
|
||||
area = ui.grid(8, n_x=2)
|
||||
self.cancel = Button(area, cancel, cancel_style)
|
||||
self.cancel.on_click = self.on_cancel
|
||||
else:
|
||||
self.confirm = Button(ui.grid(4, n_x=1), confirm, style=confirm_style)
|
||||
self.cancel = None
|
||||
|
||||
def render(self):
|
||||
self.confirm.render()
|
||||
def dispatch(self, event, x, y):
|
||||
self.content.dispatch(event, x, y)
|
||||
if self.confirm is not None:
|
||||
self.confirm.dispatch(event, x, y)
|
||||
if self.cancel is not None:
|
||||
self.cancel.render()
|
||||
self.cancel.dispatch(event, x, y)
|
||||
|
||||
def touch(self, event, pos):
|
||||
if self.confirm.touch(event, pos) == BTN_CLICKED:
|
||||
return CONFIRMED
|
||||
if self.cancel is not None:
|
||||
if self.cancel.touch(event, pos) == BTN_CLICKED:
|
||||
return CANCELLED
|
||||
def on_confirm(self):
|
||||
raise ui.Result(CONFIRMED)
|
||||
|
||||
async def __iter__(self):
|
||||
if __debug__:
|
||||
return await loop.spawn(super().__iter__(), self.content, confirm_signal)
|
||||
else:
|
||||
return await loop.spawn(super().__iter__(), self.content)
|
||||
def on_cancel(self):
|
||||
raise ui.Result(CANCELLED)
|
||||
|
||||
|
||||
_STARTED = const(-1)
|
||||
_STOPPED = const(-2)
|
||||
class HoldToConfirm(ui.Layout):
|
||||
DEFAULT_CONFIRM = "Hold To Confirm"
|
||||
DEFAULT_CONFIRM_STYLE = ButtonConfirm
|
||||
DEFAULT_LOADER_STYLE = LoaderDefault
|
||||
|
||||
|
||||
class HoldToConfirmDialog(Widget):
|
||||
def __init__(
|
||||
self,
|
||||
content,
|
||||
hold="Hold to confirm",
|
||||
button_style=ui.BTN_CONFIRM,
|
||||
loader_style=ui.LDR_DEFAULT,
|
||||
confirm=DEFAULT_CONFIRM,
|
||||
confirm_style=DEFAULT_CONFIRM_STYLE,
|
||||
loader_style=DEFAULT_LOADER_STYLE,
|
||||
):
|
||||
self.content = content
|
||||
self.button = Button(ui.grid(4, n_x=1), hold, style=button_style)
|
||||
self.loader = Loader(style=loader_style)
|
||||
|
||||
if content.__class__.__iter__ is not Widget.__iter__:
|
||||
raise TypeError(
|
||||
"HoldToConfirmDialog does not support widgets with custom event loop"
|
||||
)
|
||||
self.loader = Loader(loader_style)
|
||||
self.loader.on_start = self._on_loader_start
|
||||
|
||||
def taint(self):
|
||||
super().taint()
|
||||
self.button.taint()
|
||||
self.content.taint()
|
||||
self.button = Button(ui.grid(4, n_x=1), confirm, confirm_style)
|
||||
self.button.on_press_start = self._on_press_start
|
||||
self.button.on_press_end = self._on_press_end
|
||||
self.button.on_click = self._on_click
|
||||
|
||||
def render(self):
|
||||
self.button.render()
|
||||
if not self.loader.is_active():
|
||||
self.content.render()
|
||||
def _on_press_start(self):
|
||||
self.loader.start()
|
||||
|
||||
def touch(self, event, pos):
|
||||
button = self.button
|
||||
was_active = button.state == BTN_ACTIVE
|
||||
button.touch(event, pos)
|
||||
is_active = button.state == BTN_ACTIVE
|
||||
if is_active and not was_active:
|
||||
ui.display.clear()
|
||||
self.loader.start()
|
||||
return _STARTED
|
||||
if was_active and not is_active:
|
||||
if self.loader.stop():
|
||||
return CONFIRMED
|
||||
else:
|
||||
return _STOPPED
|
||||
def _on_press_end(self):
|
||||
self.loader.stop()
|
||||
|
||||
async def __iter__(self):
|
||||
result = None
|
||||
while result is None or result < 0: # _STARTED or _STOPPED
|
||||
if self.loader.is_active():
|
||||
if __debug__:
|
||||
result = await loop.spawn(
|
||||
self.loader, super().__iter__(), confirm_signal
|
||||
)
|
||||
else:
|
||||
result = await loop.spawn(self.loader, super().__iter__())
|
||||
else:
|
||||
self.content.taint()
|
||||
if __debug__:
|
||||
result = await loop.spawn(super().__iter__(), confirm_signal)
|
||||
else:
|
||||
result = await super().__iter__()
|
||||
return result
|
||||
def _on_loader_start(self):
|
||||
# Loader has either started growing, or returned to the 0-position.
|
||||
# In the first case we need to clear the content leftovers, in the latter
|
||||
# we need to render the content again.
|
||||
ui.display.bar(0, 0, ui.WIDTH, ui.HEIGHT - 60, ui.BG)
|
||||
self.content.dispatch(ui.REPAINT, 0, 0)
|
||||
|
||||
def _on_click(self):
|
||||
if self.loader.elapsed_ms() >= self.loader.target_ms:
|
||||
self.on_confirm()
|
||||
|
||||
def dispatch(self, event, x, y):
|
||||
if self.loader.start_ms is not None:
|
||||
self.loader.dispatch(event, x, y)
|
||||
else:
|
||||
self.content.dispatch(event, x, y)
|
||||
self.button.dispatch(event, x, y)
|
||||
|
||||
def on_confirm(self):
|
||||
raise ui.Result(CONFIRMED)
|
||||
|
@ -1,21 +1,10 @@
|
||||
from trezor.ui import Widget
|
||||
from trezor import ui
|
||||
|
||||
|
||||
class Container(Widget):
|
||||
class Container(ui.Control):
|
||||
def __init__(self, *children):
|
||||
self.children = children
|
||||
|
||||
def taint(self):
|
||||
super().taint()
|
||||
def dispatch(self, event, x, y):
|
||||
for child in self.children:
|
||||
child.taint()
|
||||
|
||||
def render(self):
|
||||
for child in self.children:
|
||||
child.render()
|
||||
|
||||
def touch(self, event, pos):
|
||||
for child in self.children:
|
||||
result = child.touch(event, pos)
|
||||
if result is not None:
|
||||
return result
|
||||
child.dispatch(event, x, y)
|
||||
|
@ -1,33 +0,0 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import loop, ui
|
||||
from trezor.ui import Widget
|
||||
from trezor.ui.button import BTN_CLICKED, Button
|
||||
|
||||
DEVICE = const(0)
|
||||
HOST = const(1)
|
||||
|
||||
|
||||
class EntrySelector(Widget):
|
||||
def __init__(self, content):
|
||||
self.content = content
|
||||
self.device = Button(ui.grid(8, n_y=4, n_x=4, cells_x=4), "Device")
|
||||
self.host = Button(ui.grid(12, n_y=4, n_x=4, cells_x=4), "Host")
|
||||
|
||||
def taint(self):
|
||||
super().taint()
|
||||
self.device.taint()
|
||||
self.host.taint()
|
||||
|
||||
def render(self):
|
||||
self.device.render()
|
||||
self.host.render()
|
||||
|
||||
def touch(self, event, pos):
|
||||
if self.device.touch(event, pos) == BTN_CLICKED:
|
||||
return DEVICE
|
||||
if self.host.touch(event, pos) == BTN_CLICKED:
|
||||
return HOST
|
||||
|
||||
async def __iter__(self):
|
||||
return await loop.spawn(super().__iter__(), self.content)
|
@ -1,36 +1,57 @@
|
||||
import utime
|
||||
from micropython import const
|
||||
|
||||
from trezor import loop, res, ui
|
||||
from trezor import res, ui
|
||||
from trezor.ui import display
|
||||
|
||||
|
||||
class LoaderDefault:
|
||||
class normal:
|
||||
bg_color = ui.BG
|
||||
fg_color = ui.GREEN
|
||||
icon = None
|
||||
icon_fg_color = None
|
||||
|
||||
class active:
|
||||
bg_color = ui.BG
|
||||
fg_color = ui.GREEN
|
||||
icon = ui.ICON_CHECK
|
||||
icon_fg_color = ui.WHITE
|
||||
|
||||
|
||||
class LoaderDanger:
|
||||
class normal(LoaderDefault.normal):
|
||||
fg_color = ui.RED
|
||||
|
||||
class active(LoaderDefault.active):
|
||||
fg_color = ui.RED
|
||||
|
||||
|
||||
_TARGET_MS = const(1000)
|
||||
_SHRINK_BY = const(2)
|
||||
|
||||
|
||||
class Loader(ui.Widget):
|
||||
def __init__(self, style=ui.LDR_DEFAULT):
|
||||
class Loader(ui.Control):
|
||||
def __init__(self, style=LoaderDefault):
|
||||
self.normal_style = style.normal
|
||||
self.active_style = style.active
|
||||
self.target_ms = _TARGET_MS
|
||||
self.normal_style = style["normal"] or ui.LDR_DEFAULT["normal"]
|
||||
self.active_style = style["active"] or ui.LDR_DEFAULT["active"]
|
||||
self.start_ms = None
|
||||
self.stop_ms = None
|
||||
|
||||
def start(self):
|
||||
self.start_ms = utime.ticks_ms()
|
||||
self.stop_ms = None
|
||||
self.on_start()
|
||||
|
||||
def stop(self):
|
||||
if self.start_ms is not None and self.stop_ms is None:
|
||||
diff_ms = utime.ticks_ms() - self.start_ms
|
||||
else:
|
||||
diff_ms = 0
|
||||
self.stop_ms = utime.ticks_ms()
|
||||
return diff_ms >= self.target_ms
|
||||
|
||||
def is_active(self):
|
||||
return self.start_ms is not None
|
||||
def elapsed_ms(self):
|
||||
if self.start_ms is None:
|
||||
return 0
|
||||
return utime.ticks_ms() - self.start_ms
|
||||
|
||||
def render(self):
|
||||
def on_render(self):
|
||||
target = self.target_ms
|
||||
start = self.start_ms
|
||||
stop = self.stop_ms
|
||||
@ -38,35 +59,24 @@ class Loader(ui.Widget):
|
||||
if stop is None:
|
||||
r = min(now - start, target)
|
||||
else:
|
||||
r = max(stop - start + (stop - now) * _SHRINK_BY, 0)
|
||||
if r == 0:
|
||||
self.start_ms = None
|
||||
self.stop_ms = None
|
||||
r = max(stop - start + (stop - now) * 2, 0)
|
||||
if r == target:
|
||||
s = self.active_style
|
||||
else:
|
||||
s = self.normal_style
|
||||
if s["icon"] is None:
|
||||
ui.display.loader(r, False, -24, s["fg-color"], s["bg-color"])
|
||||
elif s["icon-fg-color"] is None:
|
||||
ui.display.loader(
|
||||
r, False, -24, s["fg-color"], s["bg-color"], res.load(s["icon"])
|
||||
)
|
||||
else:
|
||||
ui.display.loader(
|
||||
r,
|
||||
False,
|
||||
-24,
|
||||
s["fg-color"],
|
||||
s["bg-color"],
|
||||
res.load(s["icon"]),
|
||||
s["icon-fg-color"],
|
||||
)
|
||||
|
||||
def __iter__(self):
|
||||
sleep = loop.sleep(1000000 // 30) # 30 fps
|
||||
ui.display.bar(0, 32, ui.WIDTH, ui.HEIGHT - 83, ui.BG) # clear
|
||||
while self.is_active():
|
||||
self.render()
|
||||
yield sleep
|
||||
ui.display.bar(0, 32, ui.WIDTH, ui.HEIGHT - 83, ui.BG) # clear
|
||||
Y = const(-24)
|
||||
|
||||
if s.icon is None:
|
||||
display.loader(r, False, Y, s.fg_color, s.bg_color)
|
||||
else:
|
||||
display.loader(
|
||||
r, False, Y, s.fg_color, s.bg_color, res.load(s.icon), s.icon_fg_color
|
||||
)
|
||||
if r == 0:
|
||||
self.start_ms = None
|
||||
self.stop_ms = None
|
||||
self.on_start()
|
||||
|
||||
def on_start(self):
|
||||
pass
|
||||
|
@ -1,16 +1,7 @@
|
||||
from trezor import io, loop, res, ui
|
||||
from trezor.crypto import bip39
|
||||
from trezor.ui import display
|
||||
from trezor.ui.button import BTN_CLICKED, ICON, Button
|
||||
|
||||
if __debug__:
|
||||
from apps.debug import input_signal
|
||||
|
||||
MNEMONIC_KEYS = ("abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz")
|
||||
|
||||
|
||||
def key_buttons(keys):
|
||||
return [Button(ui.grid(i + 3, n_y=4), k) for i, k in enumerate(keys)]
|
||||
from trezor.ui.button import Button, ButtonClear, ButtonMono, ButtonMonoConfirm
|
||||
|
||||
|
||||
def compute_mask(text: str) -> int:
|
||||
@ -23,42 +14,59 @@ def compute_mask(text: str) -> int:
|
||||
return mask
|
||||
|
||||
|
||||
class Input(Button):
|
||||
def __init__(self, area: tuple, content: str = "", word: str = ""):
|
||||
class KeyButton(Button):
|
||||
def __init__(self, area, content, keyboard):
|
||||
self.keyboard = keyboard
|
||||
super().__init__(area, content)
|
||||
|
||||
def on_click(self):
|
||||
self.keyboard.on_key_click(self)
|
||||
|
||||
|
||||
class InputButton(Button):
|
||||
def __init__(self, area, content, word):
|
||||
super().__init__(area, content)
|
||||
self.word = word
|
||||
self.icon = None
|
||||
self.pending = False
|
||||
self.icon = None
|
||||
self.disable()
|
||||
|
||||
def edit(self, content: str, word: str, pending: bool):
|
||||
def edit(self, content, word, pending):
|
||||
self.word = word
|
||||
self.content = content
|
||||
self.pending = pending
|
||||
self.taint()
|
||||
if content == word: # confirm button
|
||||
self.enable()
|
||||
self.normal_style = ui.BTN_KEY_CONFIRM["normal"]
|
||||
self.active_style = ui.BTN_KEY_CONFIRM["active"]
|
||||
self.icon = ui.ICON_CONFIRM
|
||||
elif word: # auto-complete button
|
||||
self.enable()
|
||||
self.normal_style = ui.BTN_KEY["normal"]
|
||||
self.active_style = ui.BTN_KEY["active"]
|
||||
self.icon = ui.ICON_CLICK
|
||||
self.repaint = True
|
||||
if word:
|
||||
if content == word: # confirm button
|
||||
self.enable()
|
||||
self.normal_style = ButtonMonoConfirm.normal
|
||||
self.active_style = ButtonMonoConfirm.active
|
||||
self.icon = ui.ICON_CONFIRM
|
||||
else: # auto-complete button
|
||||
self.enable()
|
||||
self.normal_style = ButtonMono.normal
|
||||
self.active_style = ButtonMono.active
|
||||
self.icon = ui.ICON_CLICK
|
||||
else: # disabled button
|
||||
self.disabled_style = ButtonMono.disabled
|
||||
self.disable()
|
||||
self.icon = None
|
||||
|
||||
def render_content(self, s, ax, ay, aw, ah):
|
||||
text_style = s["text-style"]
|
||||
fg_color = s["fg-color"]
|
||||
bg_color = s["bg-color"]
|
||||
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
|
||||
w = self.word[len(t) :] # suggested word
|
||||
i = self.icon # rendered icon
|
||||
|
||||
if not t:
|
||||
# render prompt
|
||||
display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG)
|
||||
return
|
||||
|
||||
tx = ax + 24 # x-offset of the content
|
||||
ty = ay + ah // 2 + 8 # y-offset of the content
|
||||
|
||||
@ -73,79 +81,103 @@ class Input(Button):
|
||||
display.bar(px, ty + 2, pw + 1, 3, fg_color)
|
||||
|
||||
if i: # icon
|
||||
ix = ax + aw - ICON * 2
|
||||
iy = ty - ICON
|
||||
ix = ax + aw - 16 * 2
|
||||
iy = ty - 16
|
||||
display.icon(ix, iy, res.load(i), fg_color, bg_color)
|
||||
|
||||
|
||||
class MnemonicKeyboard(ui.Widget):
|
||||
def __init__(self, prompt: str = ""):
|
||||
class Prompt(ui.Control):
|
||||
def __init__(self, prompt):
|
||||
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), style=ui.BTN_CLEAR
|
||||
)
|
||||
self.keys = key_buttons(MNEMONIC_KEYS)
|
||||
self.pbutton = None # pending key button
|
||||
self.pindex = 0 # index of current pending char in pbutton
|
||||
self.repaint = True
|
||||
|
||||
def taint(self):
|
||||
super().taint()
|
||||
self.input.taint()
|
||||
self.back.taint()
|
||||
for btn in self.keys:
|
||||
btn.taint()
|
||||
|
||||
def render(self):
|
||||
if self.input.content:
|
||||
# content button and backspace
|
||||
self.input.render()
|
||||
self.back.render()
|
||||
else:
|
||||
# prompt
|
||||
def on_render(self):
|
||||
if self.repaint:
|
||||
display.bar(0, 8, ui.WIDTH, 60, ui.BG)
|
||||
display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG)
|
||||
# key buttons
|
||||
for btn in self.keys:
|
||||
btn.render()
|
||||
self.repaint = False
|
||||
|
||||
def touch(self, event, pos):
|
||||
|
||||
class MnemonicKeyboard(ui.Layout):
|
||||
def __init__(self, prompt):
|
||||
self.prompt = Prompt(prompt)
|
||||
|
||||
icon_back = res.load(ui.ICON_BACK)
|
||||
self.back = Button(ui.grid(0, n_x=4, n_y=4), icon_back, ButtonClear)
|
||||
self.back.on_click = self.on_back_click
|
||||
|
||||
self.input = InputButton(ui.grid(1, n_x=4, n_y=4, cells_x=3), "", "")
|
||||
self.input.on_click = self.on_input_click
|
||||
|
||||
self.keys = [
|
||||
KeyButton(ui.grid(i + 3, n_y=4), k, self)
|
||||
for i, k in enumerate(
|
||||
("abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz")
|
||||
)
|
||||
]
|
||||
self.pending_button = None
|
||||
self.pending_index = 0
|
||||
|
||||
def dispatch(self, event: int, x: int, y: int):
|
||||
for btn in self.keys:
|
||||
btn.dispatch(event, x, y)
|
||||
if self.input.content:
|
||||
self.input.dispatch(event, x, y)
|
||||
self.back.dispatch(event, x, y)
|
||||
else:
|
||||
self.prompt.dispatch(event, x, y)
|
||||
|
||||
def on_back_click(self):
|
||||
# Backspace was clicked, let's delete the last character of input.
|
||||
self.edit(self.input.content[:-1])
|
||||
|
||||
def on_input_click(self):
|
||||
# Input button was clicked. If the content matches the suggested word,
|
||||
# let's confirm it, otherwise just auto-complete.
|
||||
content = self.input.content
|
||||
word = self.input.word
|
||||
if word and word == content:
|
||||
self.edit("")
|
||||
self.on_confirm(word)
|
||||
else:
|
||||
self.edit(word)
|
||||
|
||||
if self.back.touch(event, pos) == BTN_CLICKED:
|
||||
# backspace, delete the last character of input
|
||||
self.edit(content[:-1])
|
||||
return
|
||||
def on_key_click(self, btn: Button):
|
||||
# Key button was clicked. If this button is pending, let's cycle the
|
||||
# pending character in input. If not, let's just append the first
|
||||
# character.
|
||||
if self.pending_button is btn:
|
||||
index = (self.pending_index + 1) % len(btn.content)
|
||||
content = self.input.content[:-1] + btn.content[index]
|
||||
else:
|
||||
index = 0
|
||||
content = self.input.content + btn.content[0]
|
||||
self.edit(content, btn, index)
|
||||
|
||||
if self.input.touch(event, pos) == BTN_CLICKED:
|
||||
# input press, either auto-complete or confirm
|
||||
if word and content == word:
|
||||
self.edit("")
|
||||
return content
|
||||
else:
|
||||
self.edit(word)
|
||||
return
|
||||
def on_timeout(self):
|
||||
# Timeout occurred. If we can auto-complete current input, let's just
|
||||
# reset the pending marker. If not, input is invalid, let's backspace
|
||||
# the last character.
|
||||
if self.input.word:
|
||||
self.edit(self.input.content)
|
||||
else:
|
||||
self.edit(self.input.content[:-1])
|
||||
|
||||
for btn in self.keys:
|
||||
if btn.touch(event, pos) == BTN_CLICKED:
|
||||
# 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]
|
||||
self.edit(content, btn, index)
|
||||
return
|
||||
def on_confirm(self, word):
|
||||
# Word was confirmed by the user.
|
||||
raise ui.Result(word)
|
||||
|
||||
def edit(self, content, button=None, index=0):
|
||||
def edit(self, content: str, button: KeyButton = None, index: int = 0):
|
||||
self.pending_button = button
|
||||
self.pending_index = index
|
||||
|
||||
# find the completions
|
||||
pending = button is not None
|
||||
word = bip39.find_word(content) or ""
|
||||
mask = bip39.complete_word(content)
|
||||
|
||||
self.pbutton = button
|
||||
self.pindex = index
|
||||
self.input.edit(content, word, button is not None)
|
||||
# modify the input state
|
||||
self.input.edit(content, word, pending)
|
||||
|
||||
# enable or disable key buttons
|
||||
for btn in self.keys:
|
||||
@ -154,37 +186,25 @@ class MnemonicKeyboard(ui.Widget):
|
||||
else:
|
||||
btn.disable()
|
||||
|
||||
async def __iter__(self):
|
||||
if __debug__:
|
||||
return await loop.spawn(self.edit_loop(), input_signal)
|
||||
else:
|
||||
return await self.edit_loop()
|
||||
# invalidate the prompt if we display it next frame
|
||||
if not self.input.content:
|
||||
self.prompt.repaint = True
|
||||
|
||||
async def edit_loop(self):
|
||||
timeout = loop.sleep(1000 * 1000 * 1)
|
||||
async def handle_input(self):
|
||||
touch = loop.wait(io.TOUCH)
|
||||
wait_timeout = loop.spawn(touch, timeout)
|
||||
wait_touch = loop.spawn(touch)
|
||||
content = None
|
||||
timeout = loop.sleep(1000 * 1000 * 1)
|
||||
spawn_touch = loop.spawn(touch)
|
||||
spawn_timeout = loop.spawn(touch, timeout)
|
||||
|
||||
self.back.taint()
|
||||
self.input.taint()
|
||||
while True:
|
||||
if self.pending_button is not None:
|
||||
spawn = spawn_timeout
|
||||
else:
|
||||
spawn = spawn_touch
|
||||
result = await spawn
|
||||
|
||||
while content is None:
|
||||
self.render()
|
||||
if self.pbutton is not None:
|
||||
wait = wait_timeout
|
||||
if touch in spawn.finished:
|
||||
event, x, y = result
|
||||
self.dispatch(event, x, y)
|
||||
else:
|
||||
wait = wait_touch
|
||||
result = await wait
|
||||
if touch in wait.finished:
|
||||
event, *pos = result
|
||||
content = self.touch(event, pos)
|
||||
else:
|
||||
if self.input.word:
|
||||
# just reset the pending state
|
||||
self.edit(self.input.content)
|
||||
else:
|
||||
# invalid character, backspace it
|
||||
self.edit(self.input.content[:-1])
|
||||
return content
|
||||
self.on_timeout()
|
||||
|
@ -1,17 +1,14 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import res, ui
|
||||
from trezor.ui import display
|
||||
from trezor.ui.button import BTN_CLICKED, Button
|
||||
from trezor.ui.button import Button, ButtonMono, ButtonMonoDark
|
||||
|
||||
ITEMS_PER_PAGE = 10
|
||||
PLUS_BUTTON_POSITION = 11
|
||||
BACK_BUTTON_POSITION = 9
|
||||
_ITEMS_PER_PAGE = const(10)
|
||||
_BACK_BUTTON_POSITION = const(9)
|
||||
_PLUS_BUTTON_POSITION = const(11)
|
||||
|
||||
|
||||
def digit_area(i):
|
||||
return ui.grid(i + 3) # skip the first line
|
||||
|
||||
|
||||
class NumPad(ui.Widget):
|
||||
class NumPad(ui.Layout):
|
||||
def __init__(self, label: str, start: int, end: int):
|
||||
"""
|
||||
Generates a numpad with numbers from `start` to `end` excluding.
|
||||
@ -20,50 +17,63 @@ class NumPad(ui.Widget):
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.page = 0
|
||||
self.buttons = generate_buttons(self.start, self.end, self.page, self)
|
||||
|
||||
self._generate_buttons()
|
||||
|
||||
def render(self):
|
||||
for btn in self.buttons:
|
||||
btn.render()
|
||||
|
||||
# header label
|
||||
display.text_center(ui.WIDTH // 2, 36, self.label, ui.BOLD, ui.GREY, ui.BG)
|
||||
|
||||
def touch(self, event, pos):
|
||||
for btn in self.buttons:
|
||||
if btn.touch(event, pos) == BTN_CLICKED:
|
||||
if "+" in btn.content:
|
||||
self.page += 1
|
||||
self._generate_buttons()
|
||||
elif isinstance(btn.content, bytes):
|
||||
self.page -= 1
|
||||
self._generate_buttons()
|
||||
else:
|
||||
return btn.content
|
||||
|
||||
def _generate_buttons(self):
|
||||
display.clear() # we need to clear old buttons
|
||||
start = self.start + (ITEMS_PER_PAGE + 1) * self.page - self.page
|
||||
end = min(self.end, (ITEMS_PER_PAGE + 1) * (self.page + 1) - self.page)
|
||||
|
||||
digits = list(range(start, end))
|
||||
self.buttons = [Button(digit_area(i), str(d)) for i, d in enumerate(digits)]
|
||||
if len(digits) == ITEMS_PER_PAGE:
|
||||
more = Button(
|
||||
digit_area(PLUS_BUTTON_POSITION), str(end) + "+", style=ui.BTN_KEY_DARK
|
||||
)
|
||||
self.buttons.append(more)
|
||||
# move the tenth button to its proper place and make place for the back button
|
||||
self.buttons[BACK_BUTTON_POSITION].area = digit_area(
|
||||
BACK_BUTTON_POSITION + 1
|
||||
def dispatch(self, event, x, y):
|
||||
for button in self.buttons:
|
||||
button.dispatch(event, x, y)
|
||||
if event is ui.RENDER:
|
||||
# render header label
|
||||
ui.display.text_center(
|
||||
ui.WIDTH // 2, 36, self.label, ui.BOLD, ui.GREY, ui.BG
|
||||
)
|
||||
|
||||
back = Button(
|
||||
digit_area(BACK_BUTTON_POSITION),
|
||||
res.load(ui.ICON_BACK),
|
||||
style=ui.BTN_KEY_DARK,
|
||||
)
|
||||
if self.page == 0:
|
||||
back.disable()
|
||||
self.buttons.append(back)
|
||||
def on_back(self):
|
||||
self.page -= 1
|
||||
self.buttons = generate_buttons(self.start, self.end, self.page, self)
|
||||
ui.display.clear() # we need to clear old buttons
|
||||
|
||||
def on_plus(self):
|
||||
self.page += 1
|
||||
self.buttons = generate_buttons(self.start, self.end, self.page, self)
|
||||
ui.display.clear() # we need to clear old buttons
|
||||
|
||||
def on_select(self, number):
|
||||
raise ui.Result(number)
|
||||
|
||||
|
||||
class NumButton(Button):
|
||||
def __init__(self, index, digit, pad):
|
||||
self.pad = pad
|
||||
area = ui.grid(index + 3) # skip the first line
|
||||
super().__init__(area, str(digit), ButtonMono)
|
||||
|
||||
def on_click(self):
|
||||
self.pad.on_select(int(self.content))
|
||||
|
||||
|
||||
def generate_buttons(start, end, page, pad):
|
||||
start = start + (_ITEMS_PER_PAGE + 1) * page - page
|
||||
end = min(end, (_ITEMS_PER_PAGE + 1) * (page + 1) - page)
|
||||
digits = list(range(start, end))
|
||||
|
||||
buttons = [NumButton(i, d, pad) for i, d in enumerate(digits)]
|
||||
|
||||
area = ui.grid(_PLUS_BUTTON_POSITION + 3)
|
||||
plus = Button(area, str(end) + "+", ButtonMonoDark)
|
||||
plus.on_click = pad.on_plus
|
||||
|
||||
area = ui.grid(_BACK_BUTTON_POSITION + 3)
|
||||
back = Button(area, res.load(ui.ICON_BACK), ButtonMonoDark)
|
||||
back.on_click = pad.on_back
|
||||
|
||||
if len(digits) == _ITEMS_PER_PAGE:
|
||||
# move the tenth button to its proper place and make place for the back button
|
||||
buttons[-1].area = ui.grid(_PLUS_BUTTON_POSITION - 1 + 3)
|
||||
buttons.append(plus)
|
||||
|
||||
if page == 0:
|
||||
back.disable()
|
||||
buttons.append(back)
|
||||
|
||||
return buttons
|
||||
|
@ -1,8 +1,9 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import io, loop, res, ui
|
||||
from trezor.messages import PassphraseSourceType
|
||||
from trezor.ui import display
|
||||
from trezor.ui.button import BTN_CLICKED, Button
|
||||
from trezor.ui.button import Button, ButtonClear, ButtonConfirm
|
||||
from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe
|
||||
|
||||
SPACE = res.load(ui.ICON_SPACE)
|
||||
@ -21,45 +22,60 @@ def digit_area(i):
|
||||
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)
|
||||
BBOX = const(240)
|
||||
SIZE = const(8)
|
||||
pages = len(KEYBOARD_KEYS)
|
||||
|
||||
padding = 12
|
||||
page_count = len(KEYBOARD_KEYS)
|
||||
if pages * padding > BBOX:
|
||||
padding = BBOX // pages
|
||||
|
||||
if page_count * padding > bbox:
|
||||
padding = bbox // page_count
|
||||
x = (BBOX // 2) - (pages // 2) * padding
|
||||
Y = const(44)
|
||||
|
||||
x = (bbox // 2) - (page_count // 2) * padding
|
||||
y = 44
|
||||
for i in range(0, pages):
|
||||
if i == page:
|
||||
fg = ui.FG
|
||||
else:
|
||||
fg = ui.DARK_GREY
|
||||
ui.display.bar_radius(x + i * padding, Y, SIZE, SIZE, fg, ui.BG, SIZE // 2)
|
||||
|
||||
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 KeyButton(Button):
|
||||
def __init__(self, area, content, keyboard):
|
||||
self.keyboard = keyboard
|
||||
super().__init__(area, content)
|
||||
|
||||
def on_click(self):
|
||||
self.keyboard.on_key_click(self)
|
||||
|
||||
def get_text_content(self):
|
||||
if self.content is SPACE:
|
||||
return " "
|
||||
else:
|
||||
return self.content
|
||||
|
||||
|
||||
def key_buttons(keys, keyboard):
|
||||
return [KeyButton(digit_area(i), k, keyboard) for i, k in enumerate(keys)]
|
||||
|
||||
|
||||
class Input(Button):
|
||||
def __init__(self, area: tuple, content: str = ""):
|
||||
def __init__(self, area, content):
|
||||
super().__init__(area, content)
|
||||
self.pending = False
|
||||
self.disable()
|
||||
|
||||
def edit(self, content: str, pending: bool):
|
||||
def edit(self, content, pending):
|
||||
self.content = content
|
||||
self.pending = pending
|
||||
self.taint()
|
||||
self.repaint = True
|
||||
|
||||
def render_content(self, s, ax, ay, aw, ah):
|
||||
text_style = s["text-style"]
|
||||
fg_color = s["fg-color"]
|
||||
bg_color = s["bg-color"]
|
||||
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
|
||||
@ -76,142 +92,167 @@ class Input(Button):
|
||||
|
||||
if p: # pending marker
|
||||
pw = display.text_width(t[-1:], text_style)
|
||||
display.bar(tx + width - pw, ty + 2, pw + 1, 3, fg_color)
|
||||
px = tx + width - pw
|
||||
display.bar(px, ty + 2, pw + 1, 3, fg_color)
|
||||
else: # cursor
|
||||
display.bar(tx + width + 1, ty - 18, 2, 22, fg_color)
|
||||
cx = tx + width + 1
|
||||
display.bar(cx, ty - 18, 2, 22, fg_color)
|
||||
|
||||
def on_click(self):
|
||||
pass
|
||||
|
||||
|
||||
class Prompt(ui.Widget):
|
||||
class Prompt(ui.Control):
|
||||
def __init__(self, text):
|
||||
self.text = text
|
||||
self.repaint = True
|
||||
|
||||
def render(self):
|
||||
if self.tainted:
|
||||
def on_render(self):
|
||||
if self.repaint:
|
||||
display.bar(0, 0, ui.WIDTH, 48, ui.BG)
|
||||
display.text_center(ui.WIDTH // 2, 32, self.text, ui.BOLD, ui.GREY, ui.BG)
|
||||
self.tainted = False
|
||||
self.repaint = False
|
||||
|
||||
|
||||
CANCELLED = const(0)
|
||||
CANCELLED = object()
|
||||
|
||||
|
||||
class PassphraseKeyboard(ui.Widget):
|
||||
def __init__(self, prompt, page=1):
|
||||
class PassphraseKeyboard(ui.Layout):
|
||||
def __init__(self, prompt, max_length, page=1):
|
||||
self.prompt = Prompt(prompt)
|
||||
self.max_length = max_length
|
||||
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 taint(self):
|
||||
super().taint()
|
||||
self.prompt.taint()
|
||||
self.input.taint()
|
||||
self.back.taint()
|
||||
self.done.taint()
|
||||
for btn in self.keys:
|
||||
btn.taint()
|
||||
self.back = Button(ui.grid(12), res.load(ui.ICON_BACK), ButtonClear)
|
||||
self.back.on_click = self.on_back_click
|
||||
self.back.disable()
|
||||
|
||||
def render(self):
|
||||
# passphrase or prompt
|
||||
self.done = Button(ui.grid(14), res.load(ui.ICON_CONFIRM), ButtonConfirm)
|
||||
self.done.on_click = self.on_confirm
|
||||
|
||||
self.keys = key_buttons(KEYBOARD_KEYS[self.page], self)
|
||||
self.pending_button = None
|
||||
self.pending_index = 0
|
||||
|
||||
def dispatch(self, event, x, y):
|
||||
if self.input.content:
|
||||
self.input.render()
|
||||
self.input.dispatch(event, x, y)
|
||||
else:
|
||||
self.prompt.render()
|
||||
render_scrollbar(self.page)
|
||||
# buttons
|
||||
self.back.render()
|
||||
self.done.render()
|
||||
self.prompt.dispatch(event, x, y)
|
||||
self.back.dispatch(event, x, y)
|
||||
self.done.dispatch(event, x, y)
|
||||
for btn in self.keys:
|
||||
btn.render()
|
||||
btn.dispatch(event, x, y)
|
||||
|
||||
def touch(self, event, pos):
|
||||
if event == ui.RENDER:
|
||||
render_scrollbar(self.page)
|
||||
|
||||
def on_back_click(self):
|
||||
# Backspace was clicked. If we have any content in the input, let's delete
|
||||
# the last character. Otherwise cancel.
|
||||
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 += " "
|
||||
if content:
|
||||
self.edit(content[:-1])
|
||||
else:
|
||||
self.on_cancel()
|
||||
|
||||
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
|
||||
def on_key_click(self, button: KeyButton):
|
||||
# Key button was clicked. If this button is pending, let's cycle the
|
||||
# pending character in input. If not, let's just append the first
|
||||
# character.
|
||||
button_text = button.get_text_content()
|
||||
if self.pending_button is button:
|
||||
index = (self.pending_index + 1) % len(button_text)
|
||||
prefix = self.input.content[:-1]
|
||||
else:
|
||||
index = 0
|
||||
self.pbutton = button
|
||||
self.pindex = index
|
||||
self.input.edit(content, button is not None)
|
||||
prefix = self.input.content
|
||||
if len(button_text) > 1:
|
||||
self.edit(prefix + button_text[index], button, index)
|
||||
else:
|
||||
self.edit(prefix + button_text[index])
|
||||
|
||||
def on_timeout(self):
|
||||
# Timeout occurred, let's just reset the pending marker.
|
||||
self.edit(self.input.content)
|
||||
|
||||
def edit(self, content: str, button: Button = None, index: int = 0):
|
||||
if len(content) > self.max_length:
|
||||
return
|
||||
|
||||
self.pending_button = button
|
||||
self.pending_index = index
|
||||
|
||||
# modify the input state
|
||||
pending = button is not None
|
||||
self.input.edit(content, pending)
|
||||
|
||||
if content:
|
||||
self.back.enable()
|
||||
else:
|
||||
self.back.disable()
|
||||
self.prompt.taint()
|
||||
self.prompt.repaint = True
|
||||
|
||||
async def __iter__(self):
|
||||
self.edit(self.input.content) # init button state
|
||||
while True:
|
||||
change = self.change_page()
|
||||
enter = self.enter_text()
|
||||
wait = loop.spawn(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)
|
||||
async def handle_input(self):
|
||||
touch = loop.wait(io.TOUCH)
|
||||
wait_timeout = loop.spawn(touch, timeout)
|
||||
wait_touch = loop.spawn(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
|
||||
timeout = loop.sleep(1000 * 1000 * 1)
|
||||
spawn_touch = loop.spawn(touch)
|
||||
spawn_timeout = loop.spawn(touch, timeout)
|
||||
|
||||
async def change_page(self):
|
||||
swipe = await Swipe(directions=SWIPE_HORIZONTAL)
|
||||
while True:
|
||||
if self.pending_button is not None:
|
||||
spawn = spawn_timeout
|
||||
else:
|
||||
spawn = spawn_touch
|
||||
result = await spawn
|
||||
|
||||
if touch in spawn.finished:
|
||||
event, x, y = result
|
||||
self.dispatch(event, x, y)
|
||||
else:
|
||||
self.on_timeout()
|
||||
|
||||
async def handle_paging(self):
|
||||
swipe = await Swipe(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])
|
||||
self.back.taint()
|
||||
self.done.taint()
|
||||
self.input.taint()
|
||||
self.prompt.taint()
|
||||
self.keys = key_buttons(KEYBOARD_KEYS[self.page], self)
|
||||
self.back.repaint = True
|
||||
self.done.repaint = True
|
||||
self.input.repaint = True
|
||||
self.prompt.repaint = True
|
||||
|
||||
def on_cancel(self):
|
||||
raise ui.Result(CANCELLED)
|
||||
|
||||
def on_confirm(self):
|
||||
raise ui.Result(self.input.content)
|
||||
|
||||
def create_tasks(self):
|
||||
return self.handle_input(), self.handle_rendering(), self.handle_paging()
|
||||
|
||||
|
||||
class PassphraseSource(ui.Layout):
|
||||
def __init__(self, content):
|
||||
self.content = content
|
||||
|
||||
self.device = Button(ui.grid(8, n_y=4, n_x=4, cells_x=4), "Device")
|
||||
self.device.on_click = self.on_device
|
||||
|
||||
self.host = Button(ui.grid(12, n_y=4, n_x=4, cells_x=4), "Host")
|
||||
self.host.on_click = self.on_host
|
||||
|
||||
def dispatch(self, event, x, y):
|
||||
self.content.dispatch(event, x, y)
|
||||
self.device.dispatch(event, x, y)
|
||||
self.host.dispatch(event, x, y)
|
||||
|
||||
def on_device(self):
|
||||
raise ui.Result(PassphraseSourceType.DEVICE)
|
||||
|
||||
def on_host(self):
|
||||
raise ui.Result(PassphraseSourceType.HOST)
|
||||
|
@ -1,9 +1,15 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import ui
|
||||
from trezor import res, ui
|
||||
from trezor.crypto import random
|
||||
from trezor.ui import display
|
||||
from trezor.ui.button import BTN_CLICKED, Button
|
||||
from trezor.ui.button import (
|
||||
Button,
|
||||
ButtonCancel,
|
||||
ButtonClear,
|
||||
ButtonConfirm,
|
||||
ButtonMono,
|
||||
)
|
||||
|
||||
|
||||
def digit_area(i):
|
||||
@ -15,78 +21,119 @@ def digit_area(i):
|
||||
def generate_digits():
|
||||
digits = list(range(0, 10)) # 0-9
|
||||
random.shuffle(digits)
|
||||
return digits
|
||||
# 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).
|
||||
return digits[6:] + digits[3:6] + digits[:3]
|
||||
|
||||
|
||||
class PinMatrix(ui.Widget):
|
||||
def __init__(self, label, sublabel, pin="", maxlength=9):
|
||||
self.label = label
|
||||
self.sublabel = sublabel
|
||||
class PinInput(ui.Control):
|
||||
def __init__(self, prompt, subprompt, pin):
|
||||
self.prompt = prompt
|
||||
self.subprompt = subprompt
|
||||
self.pin = pin
|
||||
self.maxlength = maxlength
|
||||
self.digits = generate_digits()
|
||||
self.repaint = True
|
||||
|
||||
# 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)
|
||||
reordered_digits = self.digits[6:] + self.digits[3:6] + self.digits[:3]
|
||||
def on_render(self):
|
||||
if self.repaint:
|
||||
if self.pin:
|
||||
self.render_pin()
|
||||
else:
|
||||
self.render_prompt()
|
||||
self.repaint = False
|
||||
|
||||
self.pin_buttons = [
|
||||
Button(digit_area(i), str(d)) for i, d in enumerate(reordered_digits)
|
||||
]
|
||||
self.onchange = None
|
||||
def render_pin(self):
|
||||
display.bar(0, 0, ui.WIDTH, 50, ui.BG)
|
||||
count = len(self.pin)
|
||||
BOX_WIDTH = const(240)
|
||||
DOT_SIZE = const(10)
|
||||
PADDING = const(14)
|
||||
RENDER_Y = const(20)
|
||||
render_x = (BOX_WIDTH - count * PADDING) // 2
|
||||
for i in range(0, count):
|
||||
display.bar_radius(
|
||||
render_x + i * PADDING, RENDER_Y, DOT_SIZE, DOT_SIZE, ui.GREY, ui.BG, 4
|
||||
)
|
||||
|
||||
def taint(self):
|
||||
super().taint()
|
||||
for btn in self.pin_buttons:
|
||||
btn.taint()
|
||||
|
||||
def render(self):
|
||||
# pin matrix buttons
|
||||
for btn in self.pin_buttons:
|
||||
btn.render()
|
||||
|
||||
if not self.tainted:
|
||||
return
|
||||
|
||||
# clear canvas under input line
|
||||
display.bar(0, 0, ui.WIDTH, 52, ui.BG)
|
||||
|
||||
if self.pin:
|
||||
# input line with pin
|
||||
l = len(self.pin)
|
||||
y = const(20)
|
||||
size = const(10)
|
||||
padding = const(14)
|
||||
box_w = const(240)
|
||||
x = (box_w - l * padding) // 2
|
||||
for i in range(0, l):
|
||||
ui.display.bar_radius(x + i * padding, y, size, size, ui.GREY, ui.BG, 4)
|
||||
elif self.sublabel:
|
||||
# input line with header label and sublabel
|
||||
display.text_center(ui.WIDTH // 2, 20, self.label, ui.BOLD, ui.GREY, ui.BG)
|
||||
def render_prompt(self):
|
||||
display.bar(0, 0, ui.WIDTH, 50, ui.BG)
|
||||
if self.subprompt:
|
||||
display.text_center(ui.WIDTH // 2, 20, self.prompt, ui.BOLD, ui.GREY, ui.BG)
|
||||
display.text_center(
|
||||
ui.WIDTH // 2, 46, self.sublabel, ui.NORMAL, ui.GREY, ui.BG
|
||||
ui.WIDTH // 2, 46, self.subprompt, ui.NORMAL, ui.GREY, ui.BG
|
||||
)
|
||||
else:
|
||||
# input line with header label
|
||||
display.text_center(ui.WIDTH // 2, 36, self.label, ui.BOLD, ui.GREY, ui.BG)
|
||||
display.text_center(ui.WIDTH // 2, 36, self.prompt, ui.BOLD, ui.GREY, ui.BG)
|
||||
|
||||
self.tainted = False
|
||||
|
||||
def touch(self, event, pos):
|
||||
class PinButton(Button):
|
||||
def __init__(self, index, digit, matrix):
|
||||
self.matrix = matrix
|
||||
super().__init__(digit_area(index), str(digit), ButtonMono)
|
||||
|
||||
def on_click(self):
|
||||
self.matrix.assign(self.matrix.input.pin + self.content)
|
||||
|
||||
|
||||
CANCELLED = object()
|
||||
|
||||
|
||||
class PinDialog(ui.Layout):
|
||||
def __init__(self, prompt, subprompt, allow_cancel=True, maxlength=9):
|
||||
self.maxlength = maxlength
|
||||
self.input = PinInput(prompt, subprompt, "")
|
||||
|
||||
icon_confirm = res.load(ui.ICON_CONFIRM)
|
||||
self.confirm_button = Button(ui.grid(14), icon_confirm, ButtonConfirm)
|
||||
self.confirm_button.on_click = self.on_confirm
|
||||
|
||||
icon_back = res.load(ui.ICON_BACK)
|
||||
self.reset_button = Button(ui.grid(12), icon_back, ButtonClear)
|
||||
self.reset_button.on_click = self.on_reset
|
||||
|
||||
if allow_cancel:
|
||||
icon_lock = res.load(ui.ICON_LOCK)
|
||||
self.cancel_button = Button(ui.grid(12), icon_lock, ButtonCancel)
|
||||
self.cancel_button.on_click = self.on_cancel
|
||||
else:
|
||||
self.cancel_button = Button(ui.grid(12), "")
|
||||
self.cancel_button.disable()
|
||||
|
||||
self.pin_buttons = [
|
||||
PinButton(i, d, self) for i, d in enumerate(generate_digits())
|
||||
]
|
||||
|
||||
def dispatch(self, event, x, y):
|
||||
for btn in self.pin_buttons:
|
||||
if btn.touch(event, pos) == BTN_CLICKED:
|
||||
if len(self.pin) < self.maxlength:
|
||||
self.change(self.pin + btn.content)
|
||||
break
|
||||
btn.dispatch(event, x, y)
|
||||
self.input.dispatch(event, x, y)
|
||||
self.confirm_button.dispatch(event, x, y)
|
||||
if self.input.pin:
|
||||
self.reset_button.dispatch(event, x, y)
|
||||
else:
|
||||
self.cancel_button.dispatch(event, x, y)
|
||||
|
||||
def change(self, pin):
|
||||
self.tainted = True
|
||||
self.pin = pin
|
||||
def assign(self, pin):
|
||||
if len(pin) > self.maxlength:
|
||||
return
|
||||
for btn in self.pin_buttons:
|
||||
if len(self.pin) == self.maxlength:
|
||||
btn.disable()
|
||||
else:
|
||||
if len(pin) < self.maxlength:
|
||||
btn.enable()
|
||||
if self.onchange:
|
||||
self.onchange()
|
||||
else:
|
||||
btn.disable()
|
||||
if pin:
|
||||
self.reset_button.enable()
|
||||
self.cancel_button.disable()
|
||||
else:
|
||||
self.reset_button.disable()
|
||||
self.cancel_button.enable()
|
||||
self.input.pin = pin
|
||||
self.input.repaint = True
|
||||
|
||||
def on_reset(self):
|
||||
self.assign("")
|
||||
|
||||
def on_cancel(self):
|
||||
raise ui.Result(CANCELLED)
|
||||
|
||||
def on_confirm(self):
|
||||
raise ui.Result(self.input.pin)
|
||||
|
17
core/src/trezor/ui/popup.py
Normal file
17
core/src/trezor/ui/popup.py
Normal file
@ -0,0 +1,17 @@
|
||||
from trezor import loop, ui
|
||||
|
||||
|
||||
class Popup(ui.Layout):
|
||||
def __init__(self, content, time_ms=0):
|
||||
self.content = content
|
||||
self.time_ms = time_ms
|
||||
|
||||
def dispatch(self, event, x, y):
|
||||
self.content.dispatch(event, x, y)
|
||||
|
||||
def create_tasks(self):
|
||||
return self.handle_input(), self.handle_rendering(), self.handle_timeout()
|
||||
|
||||
def handle_timeout(self):
|
||||
yield loop.sleep(self.time_ms * 1000)
|
||||
raise ui.Result(None)
|
@ -1,11 +1,12 @@
|
||||
from trezor import ui
|
||||
|
||||
|
||||
class Qr(ui.Widget):
|
||||
def __init__(self, data, pos, scale):
|
||||
class Qr(ui.Control):
|
||||
def __init__(self, data, x, y, scale):
|
||||
self.data = data
|
||||
self.pos = pos
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.scale = scale
|
||||
|
||||
def render(self):
|
||||
ui.display.qrcode(self.pos[0], self.pos[1], self.data, self.scale)
|
||||
def on_render(self):
|
||||
ui.display.qrcode(self.x, self.y, self.data, self.scale)
|
||||
|
@ -1,92 +1,180 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import loop, res, ui
|
||||
from trezor.ui.button import Button, ButtonCancel, ButtonConfirm, ButtonDefault
|
||||
from trezor.ui.confirm import CANCELLED, CONFIRMED
|
||||
from trezor.ui.swipe import SWIPE_DOWN, SWIPE_UP, SWIPE_VERTICAL, Swipe
|
||||
|
||||
if __debug__:
|
||||
from apps.debug import swipe_signal
|
||||
|
||||
|
||||
async def change_page(page, page_count):
|
||||
while True:
|
||||
if page == 0:
|
||||
d = SWIPE_UP
|
||||
elif page == page_count - 1:
|
||||
d = SWIPE_DOWN
|
||||
else:
|
||||
d = SWIPE_VERTICAL
|
||||
swipe = Swipe(directions=d)
|
||||
if __debug__:
|
||||
s = await loop.spawn(swipe, swipe_signal)
|
||||
else:
|
||||
s = await swipe
|
||||
if s == SWIPE_UP:
|
||||
return min(page + 1, page_count - 1) # scroll down
|
||||
elif s == SWIPE_DOWN:
|
||||
return max(page - 1, 0) # scroll up
|
||||
|
||||
|
||||
async def paginate(render_page, page_count, page=0, *args):
|
||||
while True:
|
||||
changer = change_page(page, page_count)
|
||||
renderer = render_page(page, page_count, *args)
|
||||
waiter = loop.spawn(changer, renderer)
|
||||
result = await waiter
|
||||
if changer in waiter.finished:
|
||||
page = result
|
||||
else:
|
||||
return result
|
||||
|
||||
|
||||
async def animate_swipe():
|
||||
time_delay = const(40000)
|
||||
draw_delay = const(200000)
|
||||
|
||||
ui.display.text_center(130, 220, "Swipe", ui.BOLD, ui.GREY, ui.BG)
|
||||
|
||||
sleep = loop.sleep(time_delay)
|
||||
icon = res.load(ui.ICON_SWIPE)
|
||||
for t in ui.pulse(draw_delay):
|
||||
fg = ui.blend(ui.GREY, ui.DARK_GREY, t)
|
||||
ui.display.icon(70, 205, icon, fg, ui.BG)
|
||||
yield sleep
|
||||
|
||||
|
||||
def render_scrollbar(page, page_count):
|
||||
bbox = const(220)
|
||||
size = const(8)
|
||||
def render_scrollbar(pages: int, page: int):
|
||||
BBOX = const(220)
|
||||
SIZE = const(8)
|
||||
|
||||
padding = 14
|
||||
if page_count * padding > bbox:
|
||||
padding = bbox // page_count
|
||||
if pages * padding > BBOX:
|
||||
padding = BBOX // pages
|
||||
|
||||
x = const(220)
|
||||
y = (bbox // 2) - (page_count // 2) * padding
|
||||
X = const(220)
|
||||
Y = (BBOX // 2) - (pages // 2) * padding
|
||||
|
||||
for i in range(0, page_count):
|
||||
if i != page:
|
||||
ui.display.bar_radius(x, y + i * padding, size, size, ui.GREY, ui.BG, 4)
|
||||
ui.display.bar_radius(x, y + page * padding, size, size, ui.FG, ui.BG, 4)
|
||||
for i in range(0, pages):
|
||||
if i == page:
|
||||
fg = ui.FG
|
||||
else:
|
||||
fg = ui.GREY
|
||||
ui.display.bar_radius(X, Y + i * padding, SIZE, SIZE, fg, ui.BG, 4)
|
||||
|
||||
|
||||
class Scrollpage(ui.Widget):
|
||||
def __init__(self, content, page, page_count):
|
||||
self.content = content
|
||||
def render_swipe_icon():
|
||||
DRAW_DELAY = const(200000)
|
||||
|
||||
icon = res.load(ui.ICON_SWIPE)
|
||||
t = ui.pulse(DRAW_DELAY)
|
||||
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
|
||||
ui.display.icon(70, 205, icon, c, ui.BG)
|
||||
|
||||
|
||||
def render_swipe_text():
|
||||
ui.display.text_center(130, 220, "Swipe", ui.BOLD, ui.GREY, ui.BG)
|
||||
|
||||
|
||||
class Paginated(ui.Layout):
|
||||
def __init__(self, pages, page=0, one_by_one=False):
|
||||
self.pages = pages
|
||||
self.page = page
|
||||
self.page_count = page_count
|
||||
self.one_by_one = one_by_one
|
||||
self.repaint = True
|
||||
|
||||
if content.__class__.__iter__ is not ui.Widget.__iter__:
|
||||
raise TypeError(
|
||||
"Scrollpage does not support widgets with custom event loop"
|
||||
)
|
||||
def dispatch(self, event, x, y):
|
||||
pages = self.pages
|
||||
page = self.page
|
||||
pages[page].dispatch(event, x, y)
|
||||
|
||||
def taint(self):
|
||||
super().taint()
|
||||
self.content.taint()
|
||||
if event is ui.RENDER:
|
||||
length = len(pages)
|
||||
if page < length - 1:
|
||||
render_swipe_icon()
|
||||
if self.repaint:
|
||||
render_swipe_text()
|
||||
if self.repaint:
|
||||
render_scrollbar(length, page)
|
||||
self.repaint = False
|
||||
|
||||
def render(self):
|
||||
self.content.render()
|
||||
render_scrollbar(self.page, self.page_count)
|
||||
async def handle_paging(self):
|
||||
if self.page == 0:
|
||||
directions = SWIPE_UP
|
||||
elif self.page == len(self.pages) - 1:
|
||||
directions = SWIPE_DOWN
|
||||
else:
|
||||
directions = SWIPE_VERTICAL
|
||||
|
||||
def touch(self, event, pos):
|
||||
return self.content.touch(event, pos)
|
||||
if __debug__:
|
||||
swipe = await loop.spawn(Swipe(directions), swipe_signal)
|
||||
else:
|
||||
swipe = await Swipe(directions)
|
||||
|
||||
if swipe is SWIPE_UP:
|
||||
self.page = min(self.page + 1, len(self.pages) - 1)
|
||||
elif swipe is SWIPE_DOWN:
|
||||
self.page = max(self.page - 1, 0)
|
||||
|
||||
self.pages[self.page].dispatch(ui.REPAINT, 0, 0)
|
||||
self.repaint = True
|
||||
|
||||
self.on_change()
|
||||
|
||||
def create_tasks(self):
|
||||
return self.handle_input(), self.handle_rendering(), self.handle_paging()
|
||||
|
||||
def on_change(self):
|
||||
if self.one_by_one:
|
||||
raise ui.Result(self.page)
|
||||
|
||||
|
||||
class PageWithButtons(ui.Control):
|
||||
def __init__(self, content, paginated, index, count):
|
||||
self.content = content
|
||||
self.paginated = paginated
|
||||
self.index = index
|
||||
self.count = count
|
||||
|
||||
if self.index == 0:
|
||||
# first page, we can cancel or go down
|
||||
left = res.load(ui.ICON_CANCEL)
|
||||
left_style = ButtonCancel
|
||||
right = res.load(ui.ICON_CLICK)
|
||||
right_style = ButtonDefault
|
||||
elif self.index == count - 1:
|
||||
# last page, we can go up or confirm
|
||||
left = res.load(ui.ICON_BACK)
|
||||
left_style = ButtonDefault
|
||||
right = res.load(ui.ICON_CONFIRM)
|
||||
right_style = ButtonConfirm
|
||||
else:
|
||||
# somewhere in the middle, we can go up or down
|
||||
left = res.load(ui.ICON_BACK)
|
||||
left_style = ButtonDefault
|
||||
right = res.load(ui.ICON_CLICK)
|
||||
right_style = ButtonDefault
|
||||
|
||||
self.left = Button(ui.grid(8, n_x=2), left, left_style)
|
||||
self.left.on_click = self.on_left
|
||||
|
||||
self.right = Button(ui.grid(9, n_x=2), right, right_style)
|
||||
self.right.on_click = self.on_right
|
||||
|
||||
def dispatch(self, event, x, y):
|
||||
self.content.dispatch(event, x, y)
|
||||
self.left.dispatch(event, x, y)
|
||||
self.right.dispatch(event, x, y)
|
||||
|
||||
def on_left(self):
|
||||
if self.index == 0:
|
||||
self.paginated.on_cancel()
|
||||
else:
|
||||
self.paginated.on_down()
|
||||
|
||||
def on_right(self):
|
||||
if self.index == self.count - 1:
|
||||
self.paginated.on_confirm()
|
||||
else:
|
||||
self.paginated.on_up()
|
||||
|
||||
|
||||
class PaginatedWithButtons(ui.Layout):
|
||||
def __init__(self, pages, page=0, one_by_one=False):
|
||||
self.pages = [
|
||||
PageWithButtons(p, self, i, len(pages)) for i, p in enumerate(pages)
|
||||
]
|
||||
self.page = page
|
||||
self.one_by_one = one_by_one
|
||||
|
||||
def dispatch(self, event, x, y):
|
||||
pages = self.pages
|
||||
page = self.page
|
||||
pages[page].dispatch(event, x, y)
|
||||
if event is ui.RENDER:
|
||||
render_scrollbar(len(pages), page)
|
||||
|
||||
def on_up(self):
|
||||
self.page = max(self.page - 1, 0)
|
||||
self.pages[self.page].dispatch(ui.REPAINT, 0, 0)
|
||||
self.on_change()
|
||||
|
||||
def on_down(self):
|
||||
self.page = min(self.page + 1, len(self.pages) - 1)
|
||||
self.pages[self.page].dispatch(ui.REPAINT, 0, 0)
|
||||
self.on_change()
|
||||
|
||||
def on_confirm(self):
|
||||
raise ui.Result(CONFIRMED)
|
||||
|
||||
def on_cancel(self):
|
||||
raise ui.Result(CANCELLED)
|
||||
|
||||
def on_change(self):
|
||||
if self.one_by_one:
|
||||
raise ui.Result(self.page)
|
||||
|
@ -1,6 +1,6 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor.ui import BOLD, MONO, NORMAL, rgb
|
||||
from trezor.ui import rgb
|
||||
|
||||
# radius for buttons and other elements
|
||||
RADIUS = const(2)
|
||||
@ -66,193 +66,3 @@ ICON_BACK = "trezor/res/left.toig"
|
||||
ICON_SWIPE = "trezor/res/swipe.toig"
|
||||
ICON_CHECK = "trezor/res/check.toig"
|
||||
ICON_SPACE = "trezor/res/space.toig"
|
||||
|
||||
# buttons
|
||||
BTN_DEFAULT = {
|
||||
"normal": {
|
||||
"bg-color": BG,
|
||||
"fg-color": FG,
|
||||
"text-style": NORMAL,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
"active": {
|
||||
"bg-color": FG,
|
||||
"fg-color": BG,
|
||||
"text-style": BOLD,
|
||||
"border-color": FG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
"disabled": {
|
||||
"bg-color": BG,
|
||||
"fg-color": GREY,
|
||||
"text-style": NORMAL,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
}
|
||||
|
||||
BTN_CANCEL = {
|
||||
"normal": {
|
||||
"bg-color": RED,
|
||||
"fg-color": FG,
|
||||
"text-style": BOLD,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
"active": {
|
||||
"bg-color": FG,
|
||||
"fg-color": RED,
|
||||
"text-style": BOLD,
|
||||
"border-color": FG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
"disabled": {
|
||||
"bg-color": BG,
|
||||
"fg-color": GREY,
|
||||
"text-style": NORMAL,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
}
|
||||
|
||||
BTN_CONFIRM = {
|
||||
"normal": {
|
||||
"bg-color": GREEN,
|
||||
"fg-color": FG,
|
||||
"text-style": BOLD,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
"active": {
|
||||
"bg-color": FG,
|
||||
"fg-color": GREEN,
|
||||
"text-style": BOLD,
|
||||
"border-color": FG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
"disabled": {
|
||||
"bg-color": BG,
|
||||
"fg-color": GREY,
|
||||
"text-style": NORMAL,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
}
|
||||
|
||||
BTN_CLEAR = {
|
||||
"normal": {
|
||||
"bg-color": ORANGE,
|
||||
"fg-color": FG,
|
||||
"text-style": NORMAL,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
"active": {
|
||||
"bg-color": BG,
|
||||
"fg-color": GREY,
|
||||
"text-style": NORMAL,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
"disabled": {
|
||||
"bg-color": BG,
|
||||
"fg-color": GREY,
|
||||
"text-style": MONO,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
}
|
||||
|
||||
BTN_KEY = {
|
||||
"normal": {
|
||||
"bg-color": BLACKISH,
|
||||
"fg-color": FG,
|
||||
"text-style": MONO,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
"active": {
|
||||
"bg-color": FG,
|
||||
"fg-color": BLACKISH,
|
||||
"text-style": MONO,
|
||||
"border-color": FG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
"disabled": {
|
||||
"bg-color": BG,
|
||||
"fg-color": GREY,
|
||||
"text-style": MONO,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
}
|
||||
|
||||
BTN_KEY_DARK = {
|
||||
"normal": {
|
||||
"bg-color": DARK_BLACK,
|
||||
"fg-color": DARK_WHITE,
|
||||
"text-style": MONO,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
"active": {
|
||||
"bg-color": FG,
|
||||
"fg-color": DARK_BLACK,
|
||||
"text-style": MONO,
|
||||
"border-color": FG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
"disabled": {
|
||||
"bg-color": DARK_BLACK,
|
||||
"fg-color": GREY,
|
||||
"text-style": MONO,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
}
|
||||
|
||||
BTN_KEY_CONFIRM = {
|
||||
"normal": {
|
||||
"bg-color": GREEN,
|
||||
"fg-color": FG,
|
||||
"text-style": MONO,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
"active": {
|
||||
"bg-color": FG,
|
||||
"fg-color": GREEN,
|
||||
"text-style": MONO,
|
||||
"border-color": FG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
"disabled": {
|
||||
"bg-color": BG,
|
||||
"fg-color": GREY,
|
||||
"text-style": MONO,
|
||||
"border-color": BG,
|
||||
"radius": RADIUS,
|
||||
},
|
||||
}
|
||||
|
||||
# loader
|
||||
LDR_DEFAULT = {
|
||||
"normal": {"bg-color": BG, "fg-color": GREEN, "icon": None, "icon-fg-color": None},
|
||||
"active": {
|
||||
"bg-color": BG,
|
||||
"fg-color": GREEN,
|
||||
"icon": ICON_CHECK,
|
||||
"icon-fg-color": WHITE,
|
||||
},
|
||||
}
|
||||
|
||||
LDR_DANGER = {
|
||||
"normal": {"bg-color": BG, "fg-color": RED, "icon": None, "icon-fg-color": None},
|
||||
"active": {
|
||||
"bg-color": BG,
|
||||
"fg-color": RED,
|
||||
"icon": ICON_CHECK,
|
||||
"icon-fg-color": WHITE,
|
||||
},
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import io, ui
|
||||
from trezor.ui import contains, rotate
|
||||
from trezor import io, loop, ui
|
||||
|
||||
SWIPE_UP = const(0x01)
|
||||
SWIPE_DOWN = const(0x02)
|
||||
@ -12,79 +11,99 @@ SWIPE_HORIZONTAL = const(SWIPE_LEFT | SWIPE_RIGHT)
|
||||
SWIPE_ALL = const(SWIPE_VERTICAL | SWIPE_HORIZONTAL)
|
||||
|
||||
_SWIPE_DISTANCE = const(120)
|
||||
_SWIPE_TRESHOLD = const(30)
|
||||
|
||||
|
||||
class Swipe(ui.Widget):
|
||||
def __init__(self, area=None, absolute=False, directions=SWIPE_ALL, treshold=30):
|
||||
self.area = area or (0, 0, ui.WIDTH, ui.HEIGHT)
|
||||
self.absolute = absolute
|
||||
class Swipe(ui.Control):
|
||||
def __init__(self, directions=SWIPE_ALL, area=None):
|
||||
if area is None:
|
||||
area = (0, 0, ui.WIDTH, ui.HEIGHT)
|
||||
self.area = area
|
||||
self.directions = directions
|
||||
self.treshold = treshold
|
||||
self.start_pos = None
|
||||
self.start_x = None
|
||||
self.start_y = None
|
||||
self.light_origin = None
|
||||
self.light_target = ui.BACKLIGHT_NONE
|
||||
|
||||
def touch(self, event, pos):
|
||||
|
||||
if not self.absolute:
|
||||
pos = rotate(pos)
|
||||
|
||||
if event == io.TOUCH_MOVE and self.start_pos is not None:
|
||||
pdx = pos[0] - self.start_pos[0]
|
||||
pdy = pos[1] - self.start_pos[1]
|
||||
|
||||
pdxa = abs(pdx)
|
||||
pdya = abs(pdy)
|
||||
if pdxa > pdya and self.directions & SWIPE_HORIZONTAL:
|
||||
# Horizontal direction
|
||||
if (pdx > 0 and self.directions & SWIPE_RIGHT) or (
|
||||
pdx < 0 and self.directions & SWIPE_LEFT
|
||||
):
|
||||
ui.display.backlight(
|
||||
ui.lerpi(
|
||||
self.light_origin,
|
||||
self.light_target,
|
||||
pdxa / _SWIPE_DISTANCE if pdxa < _SWIPE_DISTANCE else 1,
|
||||
)
|
||||
)
|
||||
elif pdxa < pdya and self.directions & SWIPE_VERTICAL:
|
||||
# Vertical direction
|
||||
if (pdy > 0 and self.directions & SWIPE_DOWN) or (
|
||||
pdy < 0 and self.directions & SWIPE_UP
|
||||
):
|
||||
ui.display.backlight(
|
||||
ui.lerpi(
|
||||
self.light_origin,
|
||||
self.light_target,
|
||||
pdya / _SWIPE_DISTANCE if pdya < _SWIPE_DISTANCE else 1,
|
||||
)
|
||||
)
|
||||
|
||||
elif event == io.TOUCH_START and contains(self.area, pos):
|
||||
self.start_pos = pos
|
||||
def on_touch_start(self, x, y):
|
||||
if ui.in_area(self.area, x, y):
|
||||
self.start_x = x
|
||||
self.start_y = y
|
||||
self.light_origin = ui.BACKLIGHT_NORMAL
|
||||
|
||||
elif event == io.TOUCH_END and self.start_pos is not None:
|
||||
pdx = pos[0] - self.start_pos[0]
|
||||
pdy = pos[1] - self.start_pos[1]
|
||||
pdxa = abs(pdx)
|
||||
pdya = abs(pdy)
|
||||
if pdxa > pdya and self.directions & SWIPE_HORIZONTAL:
|
||||
# Horizontal direction
|
||||
ratio = pdxa / _SWIPE_DISTANCE if pdxa < _SWIPE_DISTANCE else 1
|
||||
if ratio * 100 >= self.treshold:
|
||||
if pdx > 0 and self.directions & SWIPE_RIGHT:
|
||||
return SWIPE_RIGHT
|
||||
elif pdx < 0 and self.directions & SWIPE_LEFT:
|
||||
return SWIPE_LEFT
|
||||
elif pdxa < pdya and self.directions & SWIPE_VERTICAL:
|
||||
# Vertical direction
|
||||
ratio = pdya / _SWIPE_DISTANCE if pdya < _SWIPE_DISTANCE else 1
|
||||
if ratio * 100 >= self.treshold:
|
||||
if pdy > 0 and self.directions & SWIPE_DOWN:
|
||||
return SWIPE_DOWN
|
||||
elif pdy < 0 and self.directions & SWIPE_UP:
|
||||
return SWIPE_UP
|
||||
# No swipe, reset the state
|
||||
self.start_pos = None
|
||||
ui.display.backlight(self.light_origin)
|
||||
def on_touch_move(self, x, y):
|
||||
if self.start_x is None:
|
||||
return # not started in our area
|
||||
|
||||
dirs = self.directions
|
||||
pdx = x - self.start_x
|
||||
pdy = y - self.start_y
|
||||
pdxa = abs(pdx)
|
||||
pdya = abs(pdy)
|
||||
if pdxa > pdya and dirs & SWIPE_HORIZONTAL:
|
||||
# horizontal direction
|
||||
if (pdx > 0 and dirs & SWIPE_RIGHT) or (pdx < 0 and dirs & SWIPE_LEFT):
|
||||
ui.display.backlight(
|
||||
ui.lerpi(
|
||||
self.light_origin,
|
||||
self.light_target,
|
||||
min(pdxa / _SWIPE_DISTANCE, 1),
|
||||
)
|
||||
)
|
||||
elif pdxa < pdya and dirs & SWIPE_VERTICAL:
|
||||
# vertical direction
|
||||
if (pdy > 0 and dirs & SWIPE_DOWN) or (pdy < 0 and dirs & SWIPE_UP):
|
||||
ui.display.backlight(
|
||||
ui.lerpi(
|
||||
self.light_origin,
|
||||
self.light_target,
|
||||
min(pdya / _SWIPE_DISTANCE, 1),
|
||||
)
|
||||
)
|
||||
|
||||
def on_touch_end(self, x, y):
|
||||
if self.start_x is None:
|
||||
return # not started in our area
|
||||
|
||||
dirs = self.directions
|
||||
pdx = x - self.start_x
|
||||
pdy = y - self.start_y
|
||||
pdxa = abs(pdx)
|
||||
pdya = abs(pdy)
|
||||
if pdxa > pdya and dirs & SWIPE_HORIZONTAL:
|
||||
# horizontal direction
|
||||
ratio = min(pdxa / _SWIPE_DISTANCE, 1)
|
||||
if ratio * 100 >= _SWIPE_TRESHOLD:
|
||||
if pdx > 0 and dirs & SWIPE_RIGHT:
|
||||
self.on_swipe(SWIPE_RIGHT)
|
||||
return
|
||||
elif pdx < 0 and dirs & SWIPE_LEFT:
|
||||
self.on_swipe(SWIPE_LEFT)
|
||||
return
|
||||
elif pdxa < pdya and dirs & SWIPE_VERTICAL:
|
||||
# vertical direction
|
||||
ratio = min(pdya / _SWIPE_DISTANCE, 1)
|
||||
if ratio * 100 >= _SWIPE_TRESHOLD:
|
||||
if pdy > 0 and dirs & SWIPE_DOWN:
|
||||
self.on_swipe(SWIPE_DOWN)
|
||||
return
|
||||
elif pdy < 0 and dirs & SWIPE_UP:
|
||||
self.on_swipe(SWIPE_UP)
|
||||
return
|
||||
|
||||
# no swipe detected, reset the state
|
||||
ui.display.backlight(self.light_origin)
|
||||
self.start_x = None
|
||||
self.start_y = None
|
||||
|
||||
def on_swipe(self, swipe):
|
||||
raise ui.Result(swipe)
|
||||
|
||||
def __iter__(self):
|
||||
try:
|
||||
touch = loop.wait(io.TOUCH)
|
||||
while True:
|
||||
event, x, y = yield touch
|
||||
self.dispatch(event, x, y)
|
||||
except ui.Result as result:
|
||||
return result.value
|
||||
|
@ -33,13 +33,13 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None:
|
||||
has_next_word = word_index < len(words) - 1
|
||||
|
||||
if isinstance(word, int):
|
||||
if word in [BR, BR_HALF]:
|
||||
if word is BR or word is BR_HALF:
|
||||
# line break or half-line break
|
||||
if offset_y >= OFFSET_Y_MAX:
|
||||
ui.display.text(offset_x, offset_y, "...", ui.BOLD, ui.GREY, bg)
|
||||
return
|
||||
offset_x = TEXT_MARGIN_LEFT
|
||||
offset_y += TEXT_LINE_HEIGHT if word == BR else TEXT_LINE_HEIGHT_HALF
|
||||
offset_y += TEXT_LINE_HEIGHT if word is BR else TEXT_LINE_HEIGHT_HALF
|
||||
elif word in FONTS:
|
||||
# change of font style
|
||||
font = word
|
||||
@ -108,7 +108,7 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None:
|
||||
offset_x += SPACE
|
||||
|
||||
|
||||
class Text(ui.Widget):
|
||||
class Text(ui.Control):
|
||||
def __init__(
|
||||
self,
|
||||
header_text: str,
|
||||
@ -123,6 +123,7 @@ class Text(ui.Widget):
|
||||
self.max_lines = max_lines
|
||||
self.new_lines = new_lines
|
||||
self.content = []
|
||||
self.repaint = True
|
||||
|
||||
def normal(self, *content):
|
||||
self.content.append(ui.NORMAL)
|
||||
@ -146,8 +147,8 @@ class Text(ui.Widget):
|
||||
def br_half(self):
|
||||
self.content.append(BR_HALF)
|
||||
|
||||
def render(self):
|
||||
if self.tainted:
|
||||
def on_render(self):
|
||||
if self.repaint:
|
||||
ui.header(
|
||||
self.header_text,
|
||||
self.header_icon,
|
||||
@ -156,4 +157,4 @@ class Text(ui.Widget):
|
||||
self.icon_color,
|
||||
)
|
||||
render_text(self.content, self.new_lines, self.max_lines)
|
||||
self.tainted = False
|
||||
self.repaint = False
|
||||
|
@ -1,56 +1,28 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import loop, ui
|
||||
from trezor.ui import Widget
|
||||
from trezor.ui.button import BTN_CLICKED, Button
|
||||
|
||||
if __debug__:
|
||||
from apps.debug import input_signal
|
||||
from trezor import ui
|
||||
from trezor.ui.button import Button
|
||||
|
||||
|
||||
_W12 = const(12)
|
||||
_W18 = const(18)
|
||||
_W24 = const(24)
|
||||
|
||||
|
||||
class WordSelector(Widget):
|
||||
class WordSelector(ui.Layout):
|
||||
def __init__(self, content):
|
||||
self.content = content
|
||||
self.w12 = Button(
|
||||
ui.grid(6, n_y=4, n_x=3, cells_y=2), str(_W12), style=ui.BTN_KEY
|
||||
)
|
||||
self.w18 = Button(
|
||||
ui.grid(7, n_y=4, n_x=3, cells_y=2), str(_W18), style=ui.BTN_KEY
|
||||
)
|
||||
self.w24 = Button(
|
||||
ui.grid(8, n_y=4, n_x=3, cells_y=2), str(_W24), style=ui.BTN_KEY
|
||||
)
|
||||
self.w12 = Button(ui.grid(6, n_y=4, n_x=3, cells_y=2), "12")
|
||||
self.w12.on_click = self.on_w12
|
||||
self.w18 = Button(ui.grid(7, n_y=4, n_x=3, cells_y=2), "18")
|
||||
self.w18.on_click = self.on_w18
|
||||
self.w24 = Button(ui.grid(8, n_y=4, n_x=3, cells_y=2), "24")
|
||||
self.w24.on_click = self.on_w24
|
||||
|
||||
def taint(self):
|
||||
super().taint()
|
||||
self.w12.taint()
|
||||
self.w18.taint()
|
||||
self.w24.taint()
|
||||
def dispatch(self, event, x, y):
|
||||
self.content.dispatch(event, x, y)
|
||||
self.w12.dispatch(event, x, y)
|
||||
self.w18.dispatch(event, x, y)
|
||||
self.w24.dispatch(event, x, y)
|
||||
|
||||
def render(self):
|
||||
self.w12.render()
|
||||
self.w18.render()
|
||||
self.w24.render()
|
||||
def on_w12(self):
|
||||
raise ui.Result(12)
|
||||
|
||||
def touch(self, event, pos):
|
||||
if self.w12.touch(event, pos) == BTN_CLICKED:
|
||||
return _W12
|
||||
if self.w18.touch(event, pos) == BTN_CLICKED:
|
||||
return _W18
|
||||
if self.w24.touch(event, pos) == BTN_CLICKED:
|
||||
return _W24
|
||||
def on_w18(self):
|
||||
raise ui.Result(18)
|
||||
|
||||
async def __iter__(self):
|
||||
if __debug__:
|
||||
result = await loop.spawn(super().__iter__(), self.content, input_signal)
|
||||
if isinstance(result, str):
|
||||
return int(result)
|
||||
else:
|
||||
return result
|
||||
else:
|
||||
return await loop.spawn(super().__iter__(), self.content)
|
||||
def on_w24(self):
|
||||
raise ui.Result(24)
|
||||
|
@ -18,10 +18,12 @@ if __debug__:
|
||||
import uos
|
||||
|
||||
TEST = int(uos.getenv("TREZOR_TEST") or "0")
|
||||
DISABLE_FADE = int(uos.getenv("TREZOR_DISABLE_FADE") or "0")
|
||||
SAVE_SCREEN = int(uos.getenv("TREZOR_SAVE_SCREEN") or "0")
|
||||
LOG_MEMORY = int(uos.getenv("TREZOR_LOG_MEMORY") or "0")
|
||||
else:
|
||||
TEST = 0
|
||||
DISABLE_FADE = 0
|
||||
SAVE_SCREEN = 0
|
||||
LOG_MEMORY = 0
|
||||
|
||||
|
@ -2,12 +2,10 @@ from trezor import loop
|
||||
|
||||
workflows = []
|
||||
layouts = []
|
||||
layout_signal = loop.signal()
|
||||
default = None
|
||||
default_layout = None
|
||||
|
||||
# HACK: workaround way to stop the WebAuthn layout from the outside
|
||||
webauthn_stop_signal = loop.signal()
|
||||
|
||||
|
||||
def onstart(w):
|
||||
workflows.append(w)
|
||||
@ -54,7 +52,6 @@ def restartdefault():
|
||||
def onlayoutstart(l):
|
||||
closedefault()
|
||||
layouts.append(l)
|
||||
webauthn_stop_signal.send(None)
|
||||
|
||||
|
||||
def onlayoutclose(l):
|
||||
|
Loading…
Reference in New Issue
Block a user