Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion src/codex32_gui/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/test_gui_boundaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
}
)
CORE_ADAPTER = "codex32._bitcoin_core"
BUDGET = 2050
BUDGET = 2070


def _package() -> Path:
Expand Down
40 changes: 33 additions & 7 deletions tools/gui_walkthrough.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()]

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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())
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -375,15 +385,15 @@ 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)),
[text for text in labels(page) if "Group" in text],
)
field.set_text(self.made.lower())
settle()
press(page, "Confirm card")
enter(field)
return True

def new_card_done(self) -> bool:
Expand All @@ -406,21 +416,37 @@ 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)),
[text for text in labels(page) if "coin" in text],
)
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]
Expand Down
Loading