CoordDialog: auto-scroll the next picker group into view during keyboard entry

The quick keyboard-entry sequence (type a letter then three digits to
fill X/Y/x/y in one go) is the whole point of the Exact tab, but the
picker buttons are deliberately non-focusable (keyboard focus stays on
the dialog itself, see _make_picker's comment), so nothing was
auto-scrolling the group you're about to type into view the way a
real focused field would, on a dialog taller than its visible area
you'd end up typing digits blind past the fold. _scroll_to_stage() now
scrolls the upcoming picker group (or the Identity group, once all
four digits are in) to the top of the scroll area after each
character, and hands real keyboard focus to the id field once there's
nothing left for the digit sequence to fill.

Hit and fixed a real crash while wiring this up: translate_coordinates()
actually returns a plain (x, y) tuple on success in this PyGObject
version (not the documented (bool, x, y)) and a falsy value on
failure, unconditionally unpacking three values crashed immediately on
the very first keystroke. Verified directly against a live widget
before trusting the fix, not just against the docstring.

Verified with a GTK smoke test driving the real key-press handler
through a full X/Y/x/y sequence, confirming the scroll position
advances monotonically at each stage.
This commit is contained in:
Dominik Moritz Roth 2026-08-09 20:36:15 +02:00
parent 68d312934b
commit 0a54178723

View File

@ -157,10 +157,22 @@ class CoordDialog(Adw.Dialog):
y_flow, self._y_buttons = self._make_picker(
range(0, 10), init_y, lambda i: setattr(self, "_y_val", i)
)
Y_group = self._picker_group("Y (110)", Y_flow)
x_group = self._picker_group("x (09)", x_flow)
y_group = self._picker_group("y (09)", y_flow)
outer.append(self._picker_group("X (AT)", X_flow))
outer.append(self._picker_group("Y (110)", Y_flow))
outer.append(self._picker_group("x (09)", x_flow))
outer.append(self._picker_group("y (09)", y_flow))
outer.append(Y_group)
outer.append(x_group)
outer.append(y_group)
# Index-matched to _kb_stage (0->just typed X, waiting on Y;
# 1->just typed Y, waiting on x; 2->just typed x, waiting on y;
# 3->all four digits in, whatever comes next: the Identity
# group if there is one, otherwise nothing left to scroll to).
# See _scroll_to_stage(), keyboard-only entry means the picker
# group for the digit you're about to type can easily be
# scrolled out of view with nothing to auto-follow it the way
# a real focusable field would.
self._stage_widgets = [Y_group, x_group, y_group, None]
self.row_id = None
self.row_type = None
@ -180,6 +192,7 @@ class CoordDialog(Adw.Dialog):
self.row_type.set_selected(list(TargetType).index(self._initial_type))
extra_group.add(self.row_type)
outer.append(extra_group)
self._stage_widgets[3] = extra_group
submit = Gtk.Button(label="Set coordinates")
submit.add_css_class("suggested-action")
@ -188,8 +201,33 @@ class CoordDialog(Adw.Dialog):
submit.connect("clicked", self._on_submit_clicked)
outer.append(submit)
scroller = Gtk.ScrolledWindow(child=outer)
return scroller
self._exact_scroller = Gtk.ScrolledWindow(child=outer)
self._exact_content = outer
return self._exact_scroller
def _scroll_to_stage(self, stage: int) -> None:
"""Scroll the picker group for the digit about to be typed (or
the Identity group, once all four are in) to the top of the
dialog's visible area. The picker buttons are deliberately non-
focusable (see _make_picker) so nothing else auto-scrolls this
for us as the keyboard-only entry sequence advances."""
widget = self._stage_widgets[stage] if 0 <= stage < len(self._stage_widgets) else None
if widget is None:
return
# Despite the C API being (bool, dest_x, dest_y), this
# PyGObject version's translate_coordinates() actually returns
# a plain (x, y) tuple on success and a falsy value (None) on
# failure (e.g. widget not yet realized/allocated), verified
# directly rather than trusting the documented signature.
result = widget.translate_coordinates(self._exact_content, 0, 0)
if result:
_x, y = result
self._exact_scroller.get_vadjustment().set_value(y)
if stage == 3 and self.row_id is not None:
# Nothing left to type via the digit-sequence controller,
# hand real keyboard focus to the id field so continuing to
# type just works.
self.row_id.grab_focus()
def _build_desc_tab(self) -> Gtk.Widget:
outer = Gtk.Box(
@ -253,6 +291,7 @@ class CoordDialog(Adw.Dialog):
return False
self._X_buttons[LARGE_X.index(letter)].set_active(True)
self._kb_stage = 1
self._scroll_to_stage(0) # Y picker, up next
return True
if ch.isdigit():
@ -262,12 +301,15 @@ class CoordDialog(Adw.Dialog):
if self._kb_stage == 1:
self._Y_buttons[(10 if digit == 0 else digit) - 1].set_active(True)
self._kb_stage = 2
self._scroll_to_stage(1) # x picker, up next
elif self._kb_stage == 2:
self._x_buttons[digit].set_active(True)
self._kb_stage = 3
self._scroll_to_stage(2) # y picker, up next
elif self._kb_stage == 3:
self._y_buttons[digit].set_active(True)
self._kb_stage = 0
self._scroll_to_stage(3) # Identity group, if there is one
# Don't auto-submit when there's an id/type field still to
# fill in (Add spotter / Add target), the 4-digit sequence
# only ever fills the coordinate, so submitting immediately