You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/core/src/trezor/ui/components/tt/confirm.py

258 lines
8.4 KiB

from micropython import const
from typing import TYPE_CHECKING
from trezor import loop, res, ui, utils
from trezor.ui.loader import Loader, LoaderDefault
from ..common.confirm import CANCELLED, CONFIRMED, INFO, ConfirmBase, Pageable
from .button import Button, ButtonAbort, ButtonCancel, ButtonConfirm, ButtonDefault
if TYPE_CHECKING:
from typing import Any
from .button import ButtonContent, ButtonStyleType
from trezor.ui.loader import LoaderStyleType
class Confirm(ConfirmBase):
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: ui.Component,
confirm: ButtonContent | None = DEFAULT_CONFIRM,
confirm_style: ButtonStyleType = DEFAULT_CONFIRM_STYLE,
cancel: ButtonContent | None = DEFAULT_CANCEL,
cancel_style: ButtonStyleType = DEFAULT_CANCEL_STYLE,
major_confirm: bool = False,
) -> None:
self.content = content
button_confirm: Button | None = None
button_cancel: Button | None = None
if confirm is not None:
if cancel is None:
area = ui.grid(4, n_x=1)
elif major_confirm:
area = ui.grid(13, cells_x=2)
else:
area = ui.grid(9, n_x=2)
button_confirm = Button(area, confirm, confirm_style)
button_confirm.on_click = self.on_confirm
if cancel is not None:
if confirm is None:
area = ui.grid(4, n_x=1)
elif major_confirm:
area = ui.grid(12, cells_x=1)
else:
area = ui.grid(8, n_x=2)
button_cancel = Button(area, cancel, cancel_style)
button_cancel.on_click = self.on_cancel
super().__init__(content, button_confirm, button_cancel)
class ConfirmPageable(Confirm):
def __init__(self, pageable: Pageable, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.pageable = pageable
async def handle_paging(self) -> None:
from .swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, SWIPE_RIGHT, Swipe
if self.pageable.is_first():
directions = SWIPE_LEFT
elif self.pageable.is_last():
directions = SWIPE_RIGHT
else:
directions = SWIPE_HORIZONTAL
if __debug__:
from apps.debug import swipe_signal
swipe = await loop.race(Swipe(directions), swipe_signal())
else:
swipe = await Swipe(directions)
if swipe == SWIPE_LEFT:
self.pageable.next()
else:
self.pageable.prev()
self.content.repaint = True
if self.confirm is not None:
self.confirm.repaint = True
if self.cancel is not None:
self.cancel.repaint = True
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
tasks = super().create_tasks()
if self.pageable.page_count() > 1:
return tasks + (self.handle_paging(),)
else:
return tasks
def on_render(self) -> None:
PULSE_PERIOD = const(1_200_000)
super().on_render()
if not self.pageable.is_first():
t = ui.pulse(PULSE_PERIOD)
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
icon = res.load(ui.ICON_SWIPE_RIGHT)
if utils.DISABLE_ANIMATION:
ui.display.icon(18, 68, icon, ui.GREY, ui.BG)
else:
ui.display.icon(18, 68, icon, c, ui.BG)
if not self.pageable.is_last():
t = ui.pulse(PULSE_PERIOD, PULSE_PERIOD // 2)
c = ui.blend(ui.GREY, ui.DARK_GREY, t)
icon = res.load(ui.ICON_SWIPE_LEFT)
if utils.DISABLE_ANIMATION:
ui.display.icon(205, 68, icon, ui.GREY, ui.BG)
else:
ui.display.icon(205, 68, icon, c, ui.BG)
class InfoConfirm(ui.Layout):
DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM)
DEFAULT_CONFIRM_STYLE = ButtonConfirm
DEFAULT_CANCEL = res.load(ui.ICON_CANCEL)
DEFAULT_CANCEL_STYLE = ButtonCancel
DEFAULT_INFO = res.load(ui.ICON_CLICK) # TODO: this should be (i) icon, not click
DEFAULT_INFO_STYLE = ButtonDefault
def __init__(
self,
content: ui.Component,
confirm: ButtonContent = DEFAULT_CONFIRM,
confirm_style: ButtonStyleType = DEFAULT_CONFIRM_STYLE,
cancel: ButtonContent = DEFAULT_CANCEL,
cancel_style: ButtonStyleType = DEFAULT_CANCEL_STYLE,
info: ButtonContent = DEFAULT_INFO,
info_style: ButtonStyleType = DEFAULT_INFO_STYLE,
) -> None:
super().__init__()
self.content = content
self.confirm = Button(ui.grid(14), confirm, confirm_style)
self.confirm.on_click = self.on_confirm
self.info = Button(ui.grid(13), info, info_style)
self.info.on_click = self.on_info
self.cancel = Button(ui.grid(12), cancel, cancel_style)
self.cancel.on_click = self.on_cancel
def dispatch(self, event: int, x: int, y: int) -> None:
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.dispatch(event, x, y)
if self.info is not None:
self.info.dispatch(event, x, y)
def on_confirm(self) -> None:
raise ui.Result(CONFIRMED)
def on_cancel(self) -> None:
raise ui.Result(CANCELLED)
def on_info(self) -> None:
raise ui.Result(INFO)
if __debug__:
def read_content(self) -> list[str]:
return self.content.read_content()
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
from apps.debug import confirm_signal
return super().create_tasks() + (confirm_signal(),)
class HoldToConfirm(ui.Layout):
DEFAULT_CONFIRM = "Hold to confirm"
DEFAULT_CONFIRM_STYLE = ButtonConfirm
DEFAULT_LOADER_STYLE = LoaderDefault
def __init__(
self,
content: ui.Component,
confirm: str = DEFAULT_CONFIRM,
confirm_style: ButtonStyleType = DEFAULT_CONFIRM_STYLE,
loader_style: LoaderStyleType = DEFAULT_LOADER_STYLE,
cancel: bool = True,
):
super().__init__()
self.content = content
self.loader = Loader(loader_style)
self.loader.on_start = self._on_loader_start
if cancel:
self.confirm = Button(ui.grid(17, n_x=4, cells_x=3), confirm, confirm_style)
else:
self.confirm = Button(ui.grid(4, n_x=1), confirm, confirm_style)
self.confirm.on_press_start = self._on_press_start
self.confirm.on_press_end = self._on_press_end
self.confirm.on_click = self._on_click
self.cancel = None
if cancel:
self.cancel = Button(
ui.grid(16, n_x=4), res.load(ui.ICON_CANCEL), ButtonAbort
)
self.cancel.on_click = self.on_cancel
def _on_press_start(self) -> None:
self.loader.start()
def _on_press_end(self) -> None:
self.loader.stop()
def _on_loader_start(self) -> None:
# 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 - 58, ui.BG)
self.content.dispatch(ui.REPAINT, 0, 0)
def _on_click(self) -> None:
if self.loader.elapsed_ms() >= self.loader.target_ms:
self.on_confirm()
def dispatch(self, event: int, x: int, y: int) -> None:
if self.loader.start_ms is not None:
if utils.DISABLE_ANIMATION:
self.on_confirm()
self.loader.dispatch(event, x, y)
else:
self.content.dispatch(event, x, y)
self.confirm.dispatch(event, x, y)
if self.cancel:
self.cancel.dispatch(event, x, y)
def on_confirm(self) -> None:
raise ui.Result(CONFIRMED)
def on_cancel(self) -> None:
raise ui.Result(CANCELLED)
if __debug__:
def read_content(self) -> list[str]:
return self.content.read_content()
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
from apps.debug import confirm_signal
return super().create_tasks() + (confirm_signal(),)