fix: the guided tour selects the page a stop's target lives on
A stop whose target is on another page of a stack was entered without switching to that page, so the spotlight aimed at a widget that was not on screen. A stop now declares the page it stands on (`TourStop::page`) and the tour selects it as the step is entered -- the window's trigger and signals stops put the sidebar on Signals, the chooser's tabs stop shows the Waveforms page and stepping off it brings Sources back. The stop filter moved into `tour_begin()` and now runs on `gtk_widget_get_mapped()`: GTK 4.24's `is_visible()` ignores `child_visible`, so it kept a stop on another page *and* one in a collapsed pane alike. A wrong page is kept and selected; a collapsed pane, whose stack is unmapped too, is still dropped.
This commit is contained in:
1 parent
829176248a
commit
d549223889
9 files changed
+385
-89
No files matched your search
@@ -587,6 +587,28 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps
|
||||
the view's `on_done`; the window's passes null, the chooser's sets `Config::tour_pending`, which is
|
||||
how the two halves are one tour. `App` carries its own `TourView` (`app.tour`), so the machinery is
|
||||
compiled once and the probe reads either window's state by the same fields.
|
||||
- **A stop declares the page it stands on, and the tour selects that page as the stop is entered.** A
|
||||
stop whose target lives on another page of a stack -- the window's `signals` (and `trigger`) stops
|
||||
on the sidebar's `signals` page, the chooser's "Two kinds of input" stop on its `waveforms` page --
|
||||
names that page in `TourStop::page`, and `show_step()` selects it from `TourView::page_stack`
|
||||
before anything is measured. The page switch is **not** followed by a one-shot measure: `show_step()`
|
||||
selects the page and `tour_update()`, which already runs every frame, fills the spotlight in on the
|
||||
frame *after*, when the new page has been laid out. A stack allocates the new page's widgets on the
|
||||
frame after the switch, so a measurement taken in the switch's own tick reads zero (the allocation
|
||||
bullet in [invariants-trigger-and-pinning.md](invariants-trigger-and-pinning.md)) -- and reading the
|
||||
page *name* back is fine immediately, because that is state and not geometry.
|
||||
- **The stop filter runs on `get_mapped()`, not `is_visible()`, and that is what separates a wrong
|
||||
page from a collapsed pane.** GTK 4.24's `gtk_widget_is_visible()` walks only the widgets' own
|
||||
`visible` flags up the parents; it does **not** consult `child_visible`. Both a `GtkStack`'s
|
||||
non-current pages and `AdwOverlaySplitView`'s hidden sidebar are hidden with
|
||||
`gtk_widget_set_child_visible()`, so a widget on another page, *and* one in a collapsed pane, both
|
||||
read `is_visible() == TRUE` (verified against 4.24.1) -- the old filter kept a stop it meant to
|
||||
drop and left the spotlight on a target with no allocation. `stop_is_walkable()` keeps a stop when
|
||||
its target is **mapped**, or when it names a page and the stack that page belongs to is mapped: a
|
||||
wrong page leaves the stack mapped, so the stop is kept and the page switch reveals it; a collapsed
|
||||
pane leaves the stack unmapped too, so no page can reveal it and the stop is dropped. The filter
|
||||
lives in `tour_begin()`, not in each window's `start_tour()`, both so the two windows cannot drift
|
||||
into two rules and because the rule needs the view's own `page_stack`.
|
||||
- **The chooser's tour needs an overlay over its *whole* window, and `AdwWindow`'s content is
|
||||
replaced, not wrapped.** Two of the chooser's stops name header-bar buttons, and an overlay inside
|
||||
the `AdwToolbarView`'s `content` would give them negative coordinates and clamp the hole to the top
|
||||
@@ -605,7 +627,7 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps
|
||||
window uses and re-synced by its Help menu's toggle.
|
||||
- **The hole is kept from the last frame that could measure the target.** `gtk_widget_compute_bounds()`
|
||||
answers nothing on a frame that lands between two layout passes, and letting the hole empty there
|
||||
made the spotlight and the card jump to the corner and back; a *target that is not visible* is the
|
||||
made the spotlight and the card jump to the corner and back; a *target that is not mapped* is the
|
||||
different case (a genuinely absent surface) and is what empties it. The placement is also
|
||||
re-measured on **every frame**, never once at the start: a widget's allocation is 0 on the frame it
|
||||
is first shown, and a window resize moves it under a one-shot measurement.
|
||||
|
||||
@@ -516,17 +516,28 @@ Two lines of the self-check output are assertions in disguise:
|
||||
|
||||
- `UDPSCOPE_PROBE_CHOOSER_TOUR=1` emits `::clicked` on the help menu's **Guided tour** row, which
|
||||
now starts *this* walk rather than only requesting the window's: `[sources] chooser help: tour
|
||||
started in the chooser`, then `[sources] chooser tour: visible=1 steps=5 step=0`, then a walking
|
||||
`[sources] chooser tour: next -> step=N/5` per press, then `[sources] chooser tour: finished 5
|
||||
stops -- the window continues it (tour_pending=1)`. **`tour_pending=1` and `visible=0` is the
|
||||
handoff assertion** — the walk is over here and the request rides on the one `Config` value
|
||||
`main()` hands to both windows.
|
||||
- `UDPSCOPE_PROBE_CHOOSER_TOUR_MS=<ms>` schedules the same walk on a timer, for reading a tour
|
||||
**the run itself started** (the first-run auto-start) rather than one the probe started; it must
|
||||
be later than 1500 ms. Paired with `UDPSCOPE_PROBE_PICK_MS` a few hundred ms later, the run then
|
||||
shows the other half: the application's `help layer:` reads `tour=running … auto=0 requested=1`,
|
||||
the window's tour having been started by `tour_start_requested()` rather than by the first-run
|
||||
timer.
|
||||
started in the chooser`, then `[sources] chooser tour: visible=1 steps=5 step=0`, then
|
||||
`[sources] chooser tour: finished 5 stops -- the window continues it (tour_pending=1)` and
|
||||
`[sources] chooser tour: after finish visible=0 tour_pending=1`. **`tour_pending=1` and
|
||||
`visible=0` is the handoff assertion** — the walk is over here and the request rides on the one
|
||||
`Config` value `main()` hands to both windows. The walk is driven in one tick (it is the handoff
|
||||
being asserted, not the geometry), so the pages it selects are not readable here; the timed
|
||||
`_MS` form below walks a stop per tick and reads them.
|
||||
- `UDPSCOPE_PROBE_CHOOSER_TOUR_MS=<ms>` walks the tour a stop per **tick** (250 ms), the cadence
|
||||
the window's own probe uses and for its reason: a page a stop selects is allocated on the frame
|
||||
after the switch. It starts the walk itself when one is not already running, so it does not
|
||||
depend on the config directory being a first run. Every tick prints the page the stack shows and
|
||||
the page the stop *stands on*:
|
||||
`[sources] chooser tour: step=N/5 page=<shown> stop_page=<declared> target_mapped=D`. **The
|
||||
"Two kinds of input" stop stands on `waveforms`**: `step=2/5 page=waveforms stop_page=waveforms`
|
||||
is the switch. Then it steps **Back** off that stop and reads the page again —
|
||||
`[sources] chooser tour: back off the waveforms stop: step=1/5 page=sources` — which is the other
|
||||
direction, and the previous stop stands on `sources`. It then walks to the end:
|
||||
`[sources] chooser tour: finished 5 stops -- the window continues it (tour_pending=1)` and
|
||||
`after finish visible=0 page=sources tour_pending=1`. Paired with `UDPSCOPE_PROBE_PICK_MS` after
|
||||
the walk, the run shows the other half: the application's `help layer:` reads `tour=running …
|
||||
auto=0 requested=1`, the window's tour having been started by `tour_start_requested()` rather
|
||||
than by the first-run timer.
|
||||
- **The session layout round trip** (`~/.config/udpscope/<host>_<port>.toml`) — run once
|
||||
with a drop and a window change, then again with no interaction: the second run's dump
|
||||
must read `layout loaded from ...` and `layout: rows=R window_s=W row0=[...]` with the
|
||||
@@ -766,15 +777,19 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
|
||||
removing them. The count comes back to the same eleven, which is the other half — a toggle that
|
||||
removed and could not restore is a one-way switch.
|
||||
- `[helpui] tour started from the probe: visible=1 steps=5 step=0` — the tour started the way
|
||||
`Ctrl+T` starts it. `steps=5` is the plan: the table's five steps whose target was on screen
|
||||
(`gtk_widget_is_visible()`, so a collapsed pane is not walked over).
|
||||
- `[helpui] step=1/5 visible=1 spot=[x,y WxH] target=[…] contains=1 card=[…] clear=C` — **in window
|
||||
coordinates**, the frame a screenshot is cropped in. **`contains` must be 1** on every step: the
|
||||
spotlight is the target's own rectangle grown three pixels. `clear` is the card missing the target,
|
||||
and it is 1 wherever the window has room for the card beside or above/below the hole — at the
|
||||
documented run's window (see `help figures:` below) the plot's step is *the whole content*, because
|
||||
at 756 px the split view overlays the side panel rather than putting it beside the plot, so
|
||||
`clear=0` there is the geometry and not a placement bug. The side panel's step reads `clear=1`.
|
||||
`Ctrl+T` starts it. `steps=5` is the plan: the table's five steps whose surface is on screen or
|
||||
selectable into it (`gtk_widget_get_mapped()` -- a wrong *page* is kept, because selecting its
|
||||
page reveals it, while a *collapsed pane* is still dropped; see the surface bullets in
|
||||
[invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)).
|
||||
- `[helpui] step=1/5 visible=1 side_tab=signals spot=[x,y WxH] target=[…] contains=1 card=[…] clear=C`
|
||||
— **in window coordinates**, the frame a screenshot is cropped in, with the sidebar's page beside
|
||||
it (a stop the walk had to change the page for is readable as such; see `[toursurface]` below).
|
||||
**`contains` must be 1** on every step: the spotlight is the target's own rectangle grown three
|
||||
pixels. `clear` is the card missing the target, and it is 1 wherever the window has room for the
|
||||
card beside or above/below the hole — at the documented run's window (see `help figures:` below)
|
||||
the plot's step is *the whole content*, because at 756 px the split view overlays the side panel
|
||||
rather than putting it beside the plot, so `clear=0` there is the geometry and not a placement
|
||||
bug. The side panel's step reads `clear=1`.
|
||||
- `[helpui] after Next: step=2/5 visible=1`, `[helpui] after Back: step=1/5 visible=1`,
|
||||
`[helpui] after Skip: visible=0 dims_hidden=1 spot_hidden=1 card_hidden=1 controllers=12 (was 12)
|
||||
click_gestures=2` — the three controls. **`controllers=` equal to `(was N)` is the "no lingering
|
||||
@@ -789,6 +804,32 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
|
||||
rather than leaving them to find the mode. `trigger_panel`'s height differs between them (508 px in
|
||||
configuration mode against 72 locked — the body is replaced by the summary), which is the check
|
||||
that the mode really moved.
|
||||
- `[toursurface] …` (stderr, `UDPSCOPE_PROBE_TOUR_SURFACE_MS`) — a stop that stands on a page it is
|
||||
not on, entered from the page it is not on. It puts the sidebar on **Measure**, starts the window's
|
||||
tour, and walks it a stop per tick (400 ms), because a stack allocates the new page's widgets on
|
||||
the frame *after* the switch — so each reading is of the page the step left standing, not of the
|
||||
one the switch has just left behind. The lines:
|
||||
|
||||
- `[toursurface] before: side_tab=signals signal_list_mapped=1 running=1` — the state a first run
|
||||
starts in (a fresh config's sidebar is on Signals), then the auto-started walk is dismissed.
|
||||
- `[toursurface] on measure: side_tab=measure signal_list_mapped=0 side_stack_mapped=1` — a frame
|
||||
after the switch: **`signal_list_mapped=0` beside `side_stack_mapped=1`** is the whole premise,
|
||||
the list is genuinely off screen while the stack that holds it is on screen.
|
||||
- `[toursurface] tour: visible=1 steps=5 step=0 signals_stop=1` — **`signals_stop=1` is the filter's
|
||||
reading**: a stop whose target is *not mapped* is still in the plan because it names a page the
|
||||
stack can select. `steps=5` is the full table.
|
||||
- `[toursurface] step=N/5 side_tab=<page> target_mapped=D[ target=signal_list]` — one per stop. The
|
||||
page moves from `measure` to `signals` at the `trigger` stop (the first stop whose target is on
|
||||
the Signals page) and stays there; **the `signals` stop reads `target_mapped=1`**.
|
||||
- `[toursurface] signals stop=4/5 visible=1 side_tab=signals spot=[…] target=[…] contains=1
|
||||
card=[…] clear=1` — the `tour_geometry_line()` the helpui probe uses, read on the frame *after*
|
||||
the switch: **`side_tab=signals` with `contains=1` is the assertion** that the spotlight and the
|
||||
card are on the list, not on the page the walk came from.
|
||||
- `[toursurface] collapsed: side_stack_mapped=0 steps=2 signals_stop=0 side_stop=0` — the other half
|
||||
of the filter, read: with `sidebar_wanted` off (`update_side_visibility()` re-asserts it, so this
|
||||
is how a probe puts the sidebar away for good) the split view unmaps the column, the stack with
|
||||
it, and the two stops that need the sidebar are dropped while the page-less ones stay. This is
|
||||
what says a *wrong page* and a *collapsed pane* are not the same case.
|
||||
- `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | c: cursors: 0->1 | m: tab signals->measure | g: tab measure->signals | … | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** and **`refused` must be 1**.
|
||||
The `m`/`g` pairs are the *tab name* before and after, which is what says those two keys select a tab rather than toggling a pane; `t`/`c` read the toolbar toggle's own state, because those two enable a feature and the button is where that state is visible. The refusal is the whole reason the controller is in the bubble phase: every bare-letter accel is also a character, so an accel that fires while a `GtkEditable` has focus makes the window entry unusable. A pair reading `0->0` is an action whose dispatch does not reach the handler a click reaches. Note the format is `<key>: <widget-description>: <before>-><after>` — the name passed to `before_after()` includes the widget's description, so `t: trigger panel: 0->1` is the correct form (not `t: trigger panel 0->1` without the colon before the values).
|
||||
- `[color] '<name>' at row R slot S: trace=#RRGGBB chip_agrees=1 (want 1)`, then `[color] override: trace=#123456 chip_agrees=1`, `[color] session: entries=1 rgb=#123456` and `[color] default: trace=#RRGGBB chip_agrees=1` (stderr, `UDPSCOPE_PROBE_COLOR_MS`) — **all three `chip_agrees` must be 1**, the override line's `trace` must be `#123456`, the session line must read `entries=1 rgb=#123456`, and the default line's `trace` must be back to the first line's. Run it with `UDPSCOPE_ENABLE_ALL_MS` ahead of it, or there is no bound trace and the probe prints `no bound trace to recolour` instead of asserting anything; on the standard mock the first line reads `'sine' at row 0 slot 0: trace=#4FA3FF`. Two things make this an assertion rather than a tautology. The colours are read out of **`plot::Plot::trace().color`** — what the renderer will stroke, reached through `Row::plot` *after* `relayout_areas()`, since a new colour is a new figure — and out of the chip's own `<span foreground="…">` markup, not by asking `RowSet::signal_color()` twice, which would pass against a chip that never reached the trace. And the override is `#123456`, deliberately in no palette slot: an override equal to the entry it replaces passes against an implementation that ignores overrides entirely. The session leg round-trips `collect_layout()` → `save_layout()` → `load_layout()` on a temp path and deletes it, because a probe must not leave a colour in the real session file for a later run to inherit.
|
||||
|
||||
@@ -323,6 +323,14 @@ as distinct from the fault injection below, which exercises ingest:
|
||||
bullet) — so a probe run against a fresh config directory will have it on screen for the first
|
||||
second and a half, and that start *writes* `tour_seen` to whichever config directory the run is
|
||||
using, which is why the second run against the same one reads `auto=0`.
|
||||
- `UDPSCOPE_PROBE_TOUR_SURFACE_MS` — the tour's *surface* switch: it puts the sidebar on **Measure**,
|
||||
starts the window's tour, and walks it a stop per tick to the signals stop, reading the sidebar's
|
||||
page and the spotlight/card where the walk arrives. It then puts the sidebar *away* and starts the
|
||||
tour again, which is the same filter's other case (a collapsed pane drops the stops a page switch
|
||||
could have revealed). `[toursurface] signals stop=4/5 … side_tab=signals … contains=1` and
|
||||
`[toursurface] collapsed: side_stack_mapped=0 steps=2 signals_stop=0` are the two assertions; its
|
||||
lines are in [probe-readings.md](probe-readings.md). Run it alone with
|
||||
`UDPSCOPE_EXIT_AFTER_MS=9000 UDPSCOPE_EXIT_DUMP=1` against a live source.
|
||||
- `UDPSCOPE_PROBE_DEFAULTS_MS` — opens **Calibrate** and reads every spin button in it and in the
|
||||
trigger panel: `[defaults] calibrate: 1 0 model=1 0 | trigger: 0.001 0.002 0 0.001 0 50 0.01`. A
|
||||
`GtkSpinButton` starts at its range's *minimum*, so a `-1e12` here is an unseeded button the
|
||||
@@ -341,10 +349,16 @@ as distinct from the fault injection below, which exercises ingest:
|
||||
on, `hints=[0 {}] hint=0x0` with it off. `UDPSCOPE_PROBE_CHOOSER_HELP=1` drives the **Enable
|
||||
contextual help** row and reads the file back; `UDPSCOPE_PROBE_CHOOSER_TOUR=1` drives the **Guided
|
||||
tour** row and walks the chooser's own tour to its last stop, printing `[sources] chooser tour: …`
|
||||
and the handoff (`tour_pending=1`); `UDPSCOPE_PROBE_CHOOSER_TOUR_MS=<ms>` schedules the same walk,
|
||||
which is how the **first-run** auto-start is read (schedule it after 1500 ms, and pair it with
|
||||
`UDPSCOPE_PROBE_PICK_MS` a little later so the application's half follows). Its lines and what each
|
||||
asserts are in [probe-readings.md](probe-readings.md). A legacy file
|
||||
and the handoff (`tour_pending=1`); `UDPSCOPE_PROBE_CHOOSER_TOUR_MS=<ms>` walks it a stop per tick
|
||||
instead, printing the page the stack shows at every stop and stepping **Back** off the stop that
|
||||
stands on the waveforms page — the reading for the surface a stop changes. It starts the walk
|
||||
itself when one is not running, so it does not need a first-run config; pair it with
|
||||
`UDPSCOPE_PROBE_PICK_MS` after the walk so the application's half follows. A working run: seed a
|
||||
source once (`./build/udpscope --server 127.0.0.1 --port N` against a mock, under the throwaway
|
||||
`XDG_CONFIG_HOME`), then run bare with
|
||||
`UDPSCOPE_PROBE_CHOOSER_TOUR_MS=2000 UDPSCOPE_PROBE_PICK_MS=4500 UDPSCOPE_EXIT_AFTER_MS=12000
|
||||
UDPSCOPE_EXIT_DUMP=1`. Its lines and what each asserts are in
|
||||
[probe-readings.md](probe-readings.md). A legacy file
|
||||
(`<host>_<port>.toml`) produces `layout loaded from …/<host>_<port>/default.toml`, an `active`
|
||||
holding `default`, and the old file renamed `.imported`.
|
||||
- **The loading window covers the gap, and only on the path that asked.** The same
|
||||
|
||||
Reference in new issue
Block a user