diff --git a/common/protob/messages-common.proto b/common/protob/messages-common.proto index 2790cf2bc0..8e3ed56a13 100644 --- a/common/protob/messages-common.proto +++ b/common/protob/messages-common.proto @@ -45,7 +45,13 @@ message Failure { * @next ButtonAck */ message ButtonRequest { - optional ButtonRequestType code = 1; + optional ButtonRequestType code = 1; // enum identifier of the screen + optional uint32 pages = 2; // if the screen is paginated, number of pages + optional uint32 page_number = 3; // if the screen is paginated, current page (1-based) + /* Rationale: both fields are optional, and neither field can have 0 as a valid + value. So both testing `if pages` and `if page_number` do the right thing. + Also the following is always true: `page_is_last = (page_number == pages)` */ + /** * Type of button request */ diff --git a/core/.changelog.d/1671.added b/core/.changelog.d/1671.added new file mode 100644 index 0000000000..f3e9142f55 --- /dev/null +++ b/core/.changelog.d/1671.added @@ -0,0 +1 @@ +ButtonRequest is sent also after every screen of a multi-page view. diff --git a/core/src/apps/common/confirm.py b/core/src/apps/common/confirm.py index b56b8e7991..987f762d60 100644 --- a/core/src/apps/common/confirm.py +++ b/core/src/apps/common/confirm.py @@ -31,7 +31,6 @@ async def confirm( cancel_style: ButtonStyleType = Confirm.DEFAULT_CANCEL_STYLE, major_confirm: bool = False, ) -> bool: - await button_request(ctx, code=code) if content.__class__.__name__ == "Paginated": # The following works because asserts are omitted in non-debug builds. @@ -46,13 +45,15 @@ async def confirm( cancel_style, major_confirm, ) - dialog: ui.Layout = content + result = await content.interact(ctx, code=code) else: + await button_request(ctx, code=code) dialog = Confirm( content, confirm, confirm_style, cancel, cancel_style, major_confirm ) + result = await ctx.wait(dialog) - return await ctx.wait(dialog) is CONFIRMED + return result is CONFIRMED async def info_confirm( @@ -92,21 +93,21 @@ async def hold_to_confirm( loader_style: LoaderStyleType = HoldToConfirm.DEFAULT_LOADER_STYLE, cancel: bool = True, ) -> bool: - await button_request(ctx, code=code) if content.__class__.__name__ == "Paginated": # The following works because asserts are omitted in non-debug builds. # IOW if the assert runs, that means __debug__ is True and Paginated is imported assert isinstance(content, Paginated) - content.pages[-1] = HoldToConfirm( content.pages[-1], confirm, confirm_style, loader_style, cancel ) - dialog: ui.Layout = content + result = await content.interact(ctx, code=code) else: + await button_request(ctx, code=code) dialog = HoldToConfirm(content, confirm, confirm_style, loader_style, cancel) + result = await ctx.wait(dialog) - return await ctx.wait(dialog) is CONFIRMED + return result is CONFIRMED async def require_confirm(*args: Any, **kwargs: Any) -> None: diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 0685adfe61..d4834a4c95 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -282,11 +282,15 @@ if TYPE_CHECKING: class ButtonRequest(protobuf.MessageType): code: ButtonRequestType | None + pages: int | None + page_number: int | None def __init__( self, *, code: ButtonRequestType | None = None, + pages: int | None = None, + page_number: int | None = None, ) -> None: pass diff --git a/core/src/trezor/ui/components/tt/scroll.py b/core/src/trezor/ui/components/tt/scroll.py index 657941f0df..eaebd072cd 100644 --- a/core/src/trezor/ui/components/tt/scroll.py +++ b/core/src/trezor/ui/components/tt/scroll.py @@ -1,6 +1,8 @@ from micropython import const -from trezor import loop, res, ui, utils +from trezor import loop, res, ui, utils, wire, workflow +from trezor.enums import ButtonRequestType +from trezor.messages import ButtonAck, ButtonRequest from .button import Button, ButtonCancel, ButtonConfirm, ButtonDefault from .confirm import CANCELLED, CONFIRMED, Confirm @@ -9,6 +11,12 @@ from .text import TEXT_MAX_LINES, Span, Text _PAGINATED_LINE_WIDTH = const(204) +WAS_PAGED = object() + + +if False: + from typing import Any + def render_scrollbar(pages: int, page: int) -> None: BBOX = const(220) @@ -46,20 +54,19 @@ def render_swipe_text() -> None: class Paginated(ui.Layout): - def __init__( - self, pages: list[ui.Component], page: int = 0, one_by_one: bool = False - ): + def __init__(self, pages: list[ui.Component], page: int = 0): super().__init__() self.pages = pages self.page = page - self.one_by_one = one_by_one def dispatch(self, event: int, x: int, y: int) -> None: pages = self.pages page = self.page pages[page].dispatch(event, x, y) - if event is ui.RENDER: + if event is ui.REPAINT: + self.repaint = True + elif event is ui.RENDER: length = len(pages) if page < length - 1: render_swipe_icon() @@ -89,15 +96,24 @@ class Paginated(ui.Layout): elif swipe is SWIPE_DOWN: self.page = max(self.page - 1, 0) - self.pages[self.page].dispatch(ui.REPAINT, 0, 0) - self.repaint = True - - if __debug__: - from apps.debug import notify_layout_change - - notify_layout_change(self) - self.on_change() + raise ui.Result(WAS_PAGED) + + async def interact( + self, + ctx: wire.GenericContext, + code: ButtonRequestType = ButtonRequestType.Other, + ) -> Any: + workflow.close_others() + result = WAS_PAGED + while result is WAS_PAGED: + br = ButtonRequest( + code=code, pages=len(self.pages), page_number=self.page + 1 + ) + await ctx.call(br, ButtonAck) + result = await self + + return result def create_tasks(self) -> tuple[loop.Task, ...]: tasks: tuple[loop.Task, ...] = ( @@ -118,8 +134,7 @@ class Paginated(ui.Layout): return tasks def on_change(self) -> None: - if self.one_by_one: - raise ui.Result(self.page) + pass if __debug__: diff --git a/core/src/trezor/ui/layouts/common.py b/core/src/trezor/ui/layouts/common.py index 586f2ece71..4b11dc0082 100644 --- a/core/src/trezor/ui/layouts/common.py +++ b/core/src/trezor/ui/layouts/common.py @@ -8,6 +8,10 @@ if False: LayoutType = Awaitable[Any] +if __debug__: + from ..components.tt.scroll import Paginated + + async def interact( ctx: wire.GenericContext, layout: LayoutType, @@ -15,6 +19,10 @@ async def interact( brcode: ButtonRequestType = ButtonRequestType.Other, ) -> Any: log.debug(__name__, "ButtonRequest.type={}".format(brtype)) - workflow.close_others() - await ctx.call(ButtonRequest(code=brcode), ButtonAck) - return await ctx.wait(layout) + if layout.__class__.__name__ == "Paginated": + assert isinstance(layout, Paginated) + return await layout.interact(ctx, code=brcode) + else: + workflow.close_others() + await ctx.call(ButtonRequest(code=brcode), ButtonAck) + return await ctx.wait(layout) diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 9bf7e0b76d..89196d1575 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -700,14 +700,20 @@ class ButtonRequest(protobuf.MessageType): MESSAGE_WIRE_TYPE = 26 FIELDS = { 1: protobuf.Field("code", ButtonRequestType, repeated=False, required=False), + 2: protobuf.Field("pages", "uint32", repeated=False, required=False), + 3: protobuf.Field("page_number", "uint32", repeated=False, required=False), } def __init__( self, *, code: Optional[ButtonRequestType] = None, + pages: Optional[int] = None, + page_number: Optional[int] = None, ) -> None: self.code = code + self.pages = pages + self.page_number = page_number class ButtonAck(protobuf.MessageType):