feat: the help is a submenu, with a contextual-help layer and a guided tour
The hamburger menu's help rows were three flat entries and a window. They are a **`Help` submenu** now, which is where every GNOME menu keeps them, with the four things a first run cannot find: - Keyboard shortcuts Ctrl+/ (the existing overview, `show_shortcuts()`) - Documentation F1 (the existing manual, `show_help()`) - Guided tour Ctrl+T (new) - Enable contextual help (new, a stateful row: its check *is* the setting) The submenu *replaces* the menu's rows rather than floating beside them, the same shape and the same reason as the plot's right-click menu: a submenu torn out of the menu it belongs to is a second thing to dismiss, and the `GtkMenuButton` that owned one toggled itself out of sync the moment anything else closed its popover. It is the pages of a `GtkStack`, not a rebuild of the box and not a swap of the popover's child: the main view holds `app.layout_toggle`, which the tick's `sync_layout_editor()` and the `L` accel read, so rebuilding would leave that pointer on a destroyed widget, and a swapped child would leave the view that is not showing without an owner. **The contextual help is one table and a "?" per surface** (`src/ui_context_help.cpp`): the plot, the trigger, the cursors, the measurements, the math channels, the signals, the event history, the time window, the status line, and the math editor's identity row. Each row names the *heading box* its button joins -- a sibling of the title, never an overlay child on a corner, which is what would cover the switch or the [edit] button already there -- so adding a surface is a row and a widget pointer. Off means *gone*, not greyed: the buttons are removed, and they are found by a data key rather than a list of pointers because the math editor's heading is built afresh on every opening and a list would go stale one dialog later. `contextual_help` and `tour_seen` are keys of `settings.toml`, written through the same `Settings`/`load_settings()`/`save_settings()` the Preferences dialog uses, whose own rows start from `load_settings()` and so carry them through untouched. **The guided tour is five steps on a `GtkOverlay`** above the window's content: the plot, the side panel, the trigger, the signals and the status line. There is no popover in it and therefore no pointer grab to leak -- four dim rectangles around the hole are what make it modal -- and the probe asserts that as a reading (`controllers=` against the count taken before it opened) rather than as a claim. Everything is placed from `gtk_widget_compute_bounds()` re-read on every frame, because a widget's allocation is 0 on the frame it is first shown and a window resize moves it under a one-shot measurement; the hole is kept from the last frame that could measure the target, because a frame that lands between two layout passes would otherwise empty it and make the spotlight jump. `Skip`, `Finish` and `Escape` all hide every child and leave nothing behind. A first run gets the tour once, decided by `tour_seen` and not by "the settings file did not exist" (a user who changed the theme has a file and has still never seen it), and the flag is written when the tour *starts* rather than when it finishes: a user who skips it in the first second has still seen it. `UDPSCOPE_PROBE_HELPUI_MS` drives all of it -- the auto-start, the menu's seven rows and the submenu's five, the toggle's effect read as a count of affordances on either side of it, and the tour started, stepped forwards and back and dismissed -- and the exit dump gains `help layer:` and `help figures:`, the latter being the ten surfaces' window-coordinate rectangles that the manual's annotated screenshots crop their boxes from. Two of them are what the D-Bus-free headless run can still not decide, and both are limitations rather than defects: export carries no "?" (it is a menu action with no surface of its own, and a popover inside the menu's popover is the nested-popover trap the invariants already warn about), and the tour card cannot miss its target when the target is the whole content -- which is what the plot is at the window's 756 px minimum, where the split view overlays the side panel instead of putting it beside the plot. The probe prints both rectangles rather than asserting a placement, so that case reads as the geometry it is. Also fixed while it was found: `gtk_widget_measure()` includes the widget's own margins (measured: a 280 px box with `margin-start 453` reports 733), so a measure that is used to *place* the widget is a feedback loop -- that is what made the tour card walk between the corner and the target from frame to frame.
This commit is contained in:
1 parent
c1460a6977
commit
1baa58b8fd
20 files changed
+1558
-30
No files matched your search
@@ -395,6 +395,30 @@ Two lines of the self-check output are assertions in disguise:
|
||||
on the first tick for the same reason the toolbar's are. **Both must read `(ok)`**: the page titles are
|
||||
empty on purpose, so an icon that does not resolve leaves a *blank* pill, and a blank pill looks
|
||||
exactly like a page with nothing in it.
|
||||
- `help layer: contextual=on hints=8 ids={events plot status cursors measures trigger signals window} icon=dialog-question-symbolic(ok); tour=idle steps=5 step=0 auto=1` (in the exit
|
||||
dump, beside `switcher:`) — the help affordance layer as the last tick found it. **`hints=` must equal
|
||||
the number of ids**, and the ids are the surfaces the contextual help is on: the eight of the window
|
||||
itself, and `math` only while the math editor is open (its heading lives in a builder that is rebuilt
|
||||
on every opening, so it is not one of the window's own). `contextual=off` beside `hints=0` is the
|
||||
toggle's other state — **off is *gone*, not greyed**, so a non-zero count with `off` is a row that hid
|
||||
its buttons instead of removing them. **`icon=` must read `(ok)`**: a `?"` whose name Adwaita lacks is a
|
||||
*blank* control that still works, the failure the pane [edit] buttons already paid for once. `tour=`
|
||||
is `running`/`idle` with the plan's `steps=` and the current `step=`, and `auto=1` is the first-run
|
||||
auto-start having fired *in this process* — the flag is `tour_seen` in `settings.toml`, so a second run
|
||||
against the same config directory reads `auto=0`.
|
||||
- `help figures: trigger_head=X,Y,W,H trigger_summary=… trigger_panel=… signal_list=… side_switcher=… side_stack=… window_button=… status_label=… header_bar=… plot=…` (in the exit
|
||||
dump, beside `help layer:`) — the **window-coordinate** rectangle of each surface the help's annotated
|
||||
figures label, `x,y,width,height`, or `-1,-1,0,0` for a surface that is **not on screen**. It is
|
||||
measured on the tick while the window is alive (nothing in the dump may call `gtk_widget_*` — see
|
||||
[verification.md](verification.md)) and with `gtk_widget_is_visible()`, not `get_visible()`: a widget
|
||||
under a hidden parent — `trigger_summary` while the pane is in *configuration* mode, the Measure page
|
||||
while the Signals tab is showing — keeps its own flag true and reads `0x0`, and a crop box drawn from
|
||||
that is a box over the wrong thing. The figures are produced by cropping a real screenshot, so these
|
||||
numbers **are** the crop boxes; their values depend on the window's size, which under the documented
|
||||
`xvfb-run` is the window's 756x894 minimum (the 640x480 screen is smaller than the content), so a run
|
||||
on a larger screen reads different numbers for the same panels. `trigger_summary` reading
|
||||
`-1,-1,0,0` is the *configuration* pane rather than a defect: it is the locked pane's content, and
|
||||
`UDPSCOPE_PROBE_HELPUI_MS` locks the pane for that leg.
|
||||
- `app icons: org.udpscope.app(ok 128x128) org.udpscope.app-symbolic(ok 16x16)` (in the exit dump,
|
||||
beside `toolbar icons:`) — the application icon, named and present at the size its own root
|
||||
`<svg>` element declares. **Both entries must read `ok` at those sizes.** `ok` is
|
||||
@@ -638,7 +662,55 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
|
||||
every row then reports `h=0`. It turns autohide off for its own measurement — a popover dismisses
|
||||
itself when it loses the grab, and `xvfb-run` has no window manager to hand it one — and restores
|
||||
it afterwards.
|
||||
- `[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).
|
||||
- `[helpui] auto_started=1 visible=1 steps=5 step=0 (first run)` (stderr, `UDPSCOPE_PROBE_HELPUI_MS`) —
|
||||
the help affordance layer end to end, in ten steps 300-400 ms apart (every reading is an
|
||||
*allocation*, and a step that read its own predecessor's geometry would read a rectangle from before
|
||||
the frame that placed it). The line above is the **first-run auto-start**, read before the probe
|
||||
touches anything: `auto_started=1 visible=1` is the tour that started itself, and the parenthetical is
|
||||
`settings.tour_seen` as the process found it — `tour_seen already set` with `auto=0` is the *second*
|
||||
run against the same config directory, which is the assertion that the flag persisted. Then:
|
||||
- `[helpui] contextual on: hints=8 ids={events plot status cursors measures trigger signals window}` —
|
||||
the eight "?" affordances, by id. The layer's ninth surface, `math`, is not here: its host exists
|
||||
only while the math editor window is open.
|
||||
- `[helpui] main view: rows=7 help_row=1` and one `[helpui] entry N '<label>' accel='<key>' order=O`
|
||||
per row of the **Help view** — `entry 0 'Back'` with no accelerator and then the four the submenu
|
||||
promises: `entry 1 'Keyboard shortcuts' accel='Ctrl+/'`, `entry 2 'Documentation' accel='F1'`,
|
||||
`entry 3 'Guided tour' accel='Ctrl+T'`, `entry 4 'Enable contextual help' accel=''`. **`order=1` on
|
||||
every row** is the assertion; the view is entered by emitting `::clicked` on the menu's own `Help`
|
||||
row, so the row's handler is exercised and not just the stack.
|
||||
- `[helpui] help view: rows=5 expected=5 rows_bad=0 back=1 toggle=1 entries='…'` — the summary, with
|
||||
`toggle=1` being the contextual-help row's check, which *is* `settings.contextual_help`.
|
||||
- `[helpui] toggle: off -> hints=0 ids={}; on -> hints=8 ids={…}; setting=on` — the toggle's effect
|
||||
read both ways through the row itself. **`hints=0` with `off` is the assertion**: off means the
|
||||
affordances are *gone*, not greyed, so anything else is a row that hid its buttons instead of
|
||||
removing them. The count comes back to the same eight, 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`.
|
||||
- `[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
|
||||
input grab" reading**: a tour of ordinary overlay widgets takes no grab at all, and a count that
|
||||
grew would be one somebody added.
|
||||
- `[helpui] layer: …` — `capture_help_state()`'s line at the end of the run, the same text the exit
|
||||
dump prints as `help layer:`.
|
||||
- `[helpui] figures locked: …` and `[helpui] figures config: …` — `help figures:` read in both of the
|
||||
trigger pane's modes, by clicking its [edit] button. This exists because one of the help's figures
|
||||
is the *locked* pane's summary grid (`trigger_summary`), which in configuration mode is not on
|
||||
screen at all and reads `-1,-1,0,0`; the two lines give the geometry agent both states' numbers
|
||||
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.
|
||||
- `[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.
|
||||
- `[reseq-probe] reseq=N late=N stale=N overflows=N holes=N hole_slots=N backfilled=N
|
||||
open_holes=N bands=N bands_repaired=N nan_slots=N scanned=N trigger_in_hole=D` (stderr,
|
||||
|
||||
Reference in new issue
Block a user