From 46ca687047d1d0fb7e956c3b11c22714daff203d Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Tue, 10 Nov 2026 22:18:19 +0000 Subject: [PATCH] gui: Let Enter move on from every text field Enter in a field did nothing, so a card that "checks out" still needed a click on Continue. The CLI takes Enter as the answer to every prompt. _page now wires every text field it holds, once: Enter moves to the next shown field, and from the last presses the first page action that is switched on (the last action by default). Card entry falls back to "Suggest a repair", as the CLI offers a correction for a card that does not check out. Covers card entry, read-back, wallet record, new wallet, unlock, the YES gate and the custom layout numbers. Claude-Session: https://claude.ai/code/session_01N8jLK7DpgY81q3JWXK4Zvz --- src/codex32_gui/pages.py | 27 +++++++++++++++++++++++- tests/test_gui_boundaries.py | 2 +- tools/gui_walkthrough.py | 40 +++++++++++++++++++++++++++++------- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/codex32_gui/pages.py b/src/codex32_gui/pages.py index 6ed80813..57fd21ad 100644 --- a/src/codex32_gui/pages.py +++ b/src/codex32_gui/pages.py @@ -107,6 +107,7 @@ def _page( content: Gtk.Widget, *, actions: Gtk.Widget | None = None, + enter: Sequence[Gtk.Widget] = (), can_pop: bool = True, ) -> Adw.NavigationPage: scroller = Gtk.ScrolledWindow(hscrollbar_policy=Gtk.PolicyType.NEVER, vexpand=True) @@ -116,11 +117,35 @@ def _page( bars.set_content(scroller) if actions is not None: bars.add_bottom_bar(actions) + _on_enter(content, enter or (actions.get_last_child(),)) page = Adw.NavigationPage(title=title, child=bars) page.set_can_pop(can_pop) return page +def _texts(widget: Gtk.Widget) -> list[Gtk.Widget]: + found, child = [widget] if isinstance(widget, Gtk.Text) else [], widget.get_first_child() + while child is not None: + found, child = found + _texts(child), child.get_next_sibling() + return found + + +def _on_enter(content: Gtk.Widget, buttons: Sequence[Gtk.Widget]) -> None: + """Enter moves on as at a CLI prompt: to the next field, and from the last to the first button that can act.""" + texts = _texts(content) + + def press(text: Gtk.Widget) -> None: + later = [item for item in texts[texts.index(text) + 1 :] if item.get_mapped()] + ready = [button for button in buttons if button.is_sensitive()] + if later: + later[0].grab_focus() + elif ready: + ready[0].emit("clicked") + + for text in texts: + text.connect("activate", press) + + def _column(*children: Gtk.Widget) -> Gtk.Box: box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=14, margin_top=18, margin_bottom=18) for child in children: @@ -1310,7 +1335,7 @@ def following() -> Adw.NavigationPage: ) ) content = _column(_title(heading, body), *progress, field, status) - page = _page(title, content, actions=_actions(fix, go)) + page = _page(title, content, actions=_actions(fix, go), enter=(go, fix)) _forget_when_gone(view, page, field.clear) update() return page diff --git a/tests/test_gui_boundaries.py b/tests/test_gui_boundaries.py index 1d73c9c7..4c66465a 100644 --- a/tests/test_gui_boundaries.py +++ b/tests/test_gui_boundaries.py @@ -33,7 +33,7 @@ } ) CORE_ADAPTER = "codex32._bitcoin_core" -BUDGET = 2050 +BUDGET = 2070 def _package() -> Path: diff --git a/tools/gui_walkthrough.py b/tools/gui_walkthrough.py index b6b4a44f..705ae77b 100644 --- a/tools/gui_walkthrough.py +++ b/tools/gui_walkthrough.py @@ -20,7 +20,7 @@ gi.require_version("Adw", "1") gi.require_version("Gtk", "4.0") -from gi.repository import Gdk, GLib, Gtk +from gi.repository import Adw, Gdk, GLib, Gtk from codex32_gui import app, pages, reading, wallet_setup @@ -82,6 +82,12 @@ def press(page: Any, label: str) -> None: ) +def enter(field: Any) -> None: + """Press Enter in a text field, as its key binding does.""" + field.get_delegate().emit("activate") + settle() + + def labels(page: Any) -> list[str]: return [item.get_label() for item in walk(page) if isinstance(item, Gtk.Label) and item.get_label()] @@ -147,6 +153,7 @@ def do_activate(self) -> None: self.new_card_done, self.seed_entry, self.seed_shown, + self.form, ] GLib.timeout_add(300, self.pump) @@ -177,6 +184,8 @@ def typing(self) -> bool: item for item in walk(page) if isinstance(item, Gtk.Button) and item.get_label() == "Continue" ) check("nothing may be submitted yet", not go.get_sensitive()) + enter(field) + check("so Enter stays on the page", self.page() is page) field.set_text("ms12nameacd") settle() check("text is uppercased and grouped", field.get_text() == "MS12 NAME ACD", field.get_text()) @@ -199,7 +208,7 @@ def typing(self) -> bool: field.set_text(SHARE_C) settle() check("a valid card may be submitted", go.get_sensitive()) - go.emit("clicked") + enter(field) return True def intact(self) -> bool: @@ -229,7 +238,8 @@ def damaged(self) -> bool: if isinstance(item, Gtk.Button) and item.get_label() == "Suggest a repair" ) check("a repair is offered for an unreadable character", fix.get_sensitive()) - fix.emit("clicked") + enter(field) + check("and Enter asks for it", self.page() is not page) return True def candidate(self) -> bool: @@ -375,7 +385,7 @@ def read_back(self) -> bool: check("selecting a card does not publish it", _primary_is_empty(), _primary()) field.set_text(self.made[:-4] + "QQQQ") settle() - press(page, "Confirm card") + enter(field) check( "a mistyped group is named and refused", any("Group 12 does not match" in text for text in labels(page)), @@ -383,7 +393,7 @@ def read_back(self) -> bool: ) field.set_text(self.made.lower()) settle() - press(page, "Confirm card") + enter(field) return True def new_card_done(self) -> bool: @@ -406,14 +416,14 @@ def seed_entry(self) -> bool: page = self.page() field_of(page).set_text(text) settle() - press(page, "Continue") + enter(field_of(page)) return True def seed_shown(self) -> bool: page = self.page() if page.get_title() != "Master seed": return False - check("two cards recover the published secret", card(page) == SECRET_S, card(page)) + check("two cards recover the published secret by Enter alone", card(page) == SECRET_S, card(page)) check( "and the screen warns before anything else", any("take every coin" in text for text in labels(page)), @@ -421,6 +431,22 @@ def seed_shown(self) -> bool: ) return True + def form(self) -> bool: + pressed: list[bool] = [] + first, second, hidden = Adw.EntryRow(), Adw.PasswordEntryRow(), Adw.EntryRow(visible=False) + action = pages._button("Create", lambda: pressed.append(True)) + content = pages._column(first, hidden, second) + self.view.push(pages._page("Form", content, actions=pages._actions(action))) + settle() + enter(first) + check("Enter moves on to the next shown field", second.get_delegate().has_focus() and not pressed) + enter(second) + check("and in the last field presses the page's action", pressed == [True]) + action.set_sensitive(False) + enter(second) + check("but never an action that is switched off", pressed == [True]) + return True + def main() -> int: wallet_setup.connect = _connect # type: ignore[assignment]