diff --git a/src/fenigma/coord_dialog.py b/src/fenigma/coord_dialog.py index 1978396..3ff4629 100644 --- a/src/fenigma/coord_dialog.py +++ b/src/fenigma/coord_dialog.py @@ -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 (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("Y (1–10)", Y_flow)) - outer.append(self._picker_group("x (0–9)", x_flow)) - outer.append(self._picker_group("y (0–9)", 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