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:
parent
68d312934b
commit
0a54178723
@ -157,10 +157,22 @@ class CoordDialog(Adw.Dialog):
|
|||||||
y_flow, self._y_buttons = self._make_picker(
|
y_flow, self._y_buttons = self._make_picker(
|
||||||
range(0, 10), init_y, lambda i: setattr(self, "_y_val", i)
|
range(0, 10), init_y, lambda i: setattr(self, "_y_val", i)
|
||||||
)
|
)
|
||||||
|
Y_group = self._picker_group("Y (1–10)", Y_flow)
|
||||||
|
x_group = self._picker_group("x (0–9)", x_flow)
|
||||||
|
y_group = self._picker_group("y (0–9)", y_flow)
|
||||||
outer.append(self._picker_group("X (A–T)", X_flow))
|
outer.append(self._picker_group("X (A–T)", X_flow))
|
||||||
outer.append(self._picker_group("Y (1–10)", Y_flow))
|
outer.append(Y_group)
|
||||||
outer.append(self._picker_group("x (0–9)", x_flow))
|
outer.append(x_group)
|
||||||
outer.append(self._picker_group("y (0–9)", y_flow))
|
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_id = None
|
||||||
self.row_type = None
|
self.row_type = None
|
||||||
@ -180,6 +192,7 @@ class CoordDialog(Adw.Dialog):
|
|||||||
self.row_type.set_selected(list(TargetType).index(self._initial_type))
|
self.row_type.set_selected(list(TargetType).index(self._initial_type))
|
||||||
extra_group.add(self.row_type)
|
extra_group.add(self.row_type)
|
||||||
outer.append(extra_group)
|
outer.append(extra_group)
|
||||||
|
self._stage_widgets[3] = extra_group
|
||||||
|
|
||||||
submit = Gtk.Button(label="Set coordinates")
|
submit = Gtk.Button(label="Set coordinates")
|
||||||
submit.add_css_class("suggested-action")
|
submit.add_css_class("suggested-action")
|
||||||
@ -188,8 +201,33 @@ class CoordDialog(Adw.Dialog):
|
|||||||
submit.connect("clicked", self._on_submit_clicked)
|
submit.connect("clicked", self._on_submit_clicked)
|
||||||
outer.append(submit)
|
outer.append(submit)
|
||||||
|
|
||||||
scroller = Gtk.ScrolledWindow(child=outer)
|
self._exact_scroller = Gtk.ScrolledWindow(child=outer)
|
||||||
return scroller
|
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:
|
def _build_desc_tab(self) -> Gtk.Widget:
|
||||||
outer = Gtk.Box(
|
outer = Gtk.Box(
|
||||||
@ -253,6 +291,7 @@ class CoordDialog(Adw.Dialog):
|
|||||||
return False
|
return False
|
||||||
self._X_buttons[LARGE_X.index(letter)].set_active(True)
|
self._X_buttons[LARGE_X.index(letter)].set_active(True)
|
||||||
self._kb_stage = 1
|
self._kb_stage = 1
|
||||||
|
self._scroll_to_stage(0) # Y picker, up next
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if ch.isdigit():
|
if ch.isdigit():
|
||||||
@ -262,12 +301,15 @@ class CoordDialog(Adw.Dialog):
|
|||||||
if self._kb_stage == 1:
|
if self._kb_stage == 1:
|
||||||
self._Y_buttons[(10 if digit == 0 else digit) - 1].set_active(True)
|
self._Y_buttons[(10 if digit == 0 else digit) - 1].set_active(True)
|
||||||
self._kb_stage = 2
|
self._kb_stage = 2
|
||||||
|
self._scroll_to_stage(1) # x picker, up next
|
||||||
elif self._kb_stage == 2:
|
elif self._kb_stage == 2:
|
||||||
self._x_buttons[digit].set_active(True)
|
self._x_buttons[digit].set_active(True)
|
||||||
self._kb_stage = 3
|
self._kb_stage = 3
|
||||||
|
self._scroll_to_stage(2) # y picker, up next
|
||||||
elif self._kb_stage == 3:
|
elif self._kb_stage == 3:
|
||||||
self._y_buttons[digit].set_active(True)
|
self._y_buttons[digit].set_active(True)
|
||||||
self._kb_stage = 0
|
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
|
# Don't auto-submit when there's an id/type field still to
|
||||||
# fill in (Add spotter / Add target), the 4-digit sequence
|
# fill in (Add spotter / Add target), the 4-digit sequence
|
||||||
# only ever fills the coordinate, so submitting immediately
|
# only ever fills the coordinate, so submitting immediately
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user