help: default the contextual layer off, and carry the help into the chooser

The contextual help was on by default, so a first run met a window with a "?"
beside every surface it had not asked to have explained; `contextual_help` now
defaults to false and the layer is asked for from the Help menu, from the
chooser's new help menu, or with Ctrl+H.

The source chooser runs before `App` exists, which makes it the window a user
who does not know how to start is looking at, so it now carries the same three
help affordances the window's Help view does: Documentation (the manual is
another process and needs nothing of the app), Enable contextual help (a row
whose check *is* the setting, written to the file the app reads a moment
later), and Guided tour. The tour is drawn on the app's own window, so the
chooser can only record the wish: it sets `Config::tour_pending` -- the value
`main()` owns and hands to both `choose_source()` and `run_ui()` -- and
`on_activate()` starts it once the window is up and laid out, through its own
callback so the readings can tell a request from the first-run auto-start.
The rows are the window's own `menu_button()`/`menu_toggle()`; `install_css()`
therefore runs from `choose_source()` (it is idempotent now) so the popover
gets the `.udpscope-menu` rules before it is shown.

Opening the manual could also fail with a message that blamed the handler
whatever was missing. `help_installed_path()` now resolves `help:<id>` the way
the specification does and the failure alert says which of the two it is: with
the set installed it names the viewer, without it it names the identifier, the
expected installed path and the install command. `show_help()` gained an
optional reporter, which `UDPSCOPE_PROBE_HELP_MS` uses for a new reading --
`[help] launch attempted= done= ok= error='' installed=` -- since a successful
launch leaves no window of ours to observe.

Tested: clean --clean-first build, three warnings as documented; test-ui 13/13
and test-help pass; no Gtk-CRITICAL in the help, helpui and chooser runs.
This commit is contained in:
Martino Ferrari committed 2026-10-14 17:22:03 +02:00
1 parent e140642ed0
commit d8a79b96ba
15 files changed
+461 -66

No files matched your search

+40 -12
View File
@@ -395,17 +395,21 @@ 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
- `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 requested=0` (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
on every opening, so it is not one of the window's own). The layer is **off by default**
(`contextual_help` in `settings.toml` is false for a missing file), so a fresh-config run reads
`contextual=off hints=0` and that line above is the state *after* the Help row was switched on;
`contextual=off` beside a non-zero count is the toggle not having removed its buttons — **off is
*gone*, not greyed**. **`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
is `running`/`idle` with the plan's `steps=` and the current `step=`; `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`.
against the same config directory reads `auto=0` — and `requested=1` is the **chooser's** "Guided tour"
(`Config::tour_pending`) having been carried into this run instead, which is `auto=0` because it is not
the auto-start.
- `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
@@ -634,7 +638,7 @@ Two lines of the self-check output are assertions in disguise:
so the panes the tick rewrites are measured directly. The `[width-probe]` lines are the
per-change detail behind the `axes.w changes` count, and a step over 100 px dumps the widget
tree's wide subtrees — that is what the documented, intermittent ~320 px step wants read.
- `[help] topics=14 index=1 figures=3 dir=/…/help/C/org.udpscope.app; shortcuts: app=14 plot=14 tables=28 ui=28 ui_accel=21 ui_icon=7 ui_bare=0; about version=… commit=… built=… settings=… icon=org.udpscope.app; closed ok` (stderr, `UDPSCOPE_PROBE_HELP_MS`) — the manual is a Mallard set on disk now, not a GResource in the binary, so this is a **file-system reading**: `topics` is the `.page` files other than `index.page`, `index` must be 1, and `figures` counts the screenshots under `figures/`. **`topics=0 index=0` means the manual is missing** from `dir` (the source directory the build was configured from, `UDPSCOPE_HELP_DIR`) — the finding the old `parsed != pages` used to name. The manual itself opens in Yelp, another process, which this probe does not and cannot exercise. The `shortcuts:` numbers are the shortcut overview four ways.
- `[help] topics=14 index=1 figures=3 dir=/…/help/C/org.udpscope.app; shortcuts: app=14 plot=14 tables=28 ui=28 ui_accel=21 ui_icon=7 ui_bare=0; about version=… commit=… built=… settings=… icon=org.udpscope.app; closed ok` (stderr, `UDPSCOPE_PROBE_HELP_MS`) — the manual is a Mallard set on disk now, not a GResource in the binary, so this is a **file-system reading**: `topics` is the `.page` files other than `index.page`, `index` must be 1, and `figures` counts the screenshots under `figures/`. **`topics=0 index=0` means the manual is missing** from `dir` (the source directory the build was configured from, `UDPSCOPE_HELP_DIR`) — the finding the old `parsed != pages` used to name. The manual itself opens in Yelp, another process, which the probe now launches and reports (the `[help] launch` line below). The `shortcuts:` numbers are the shortcut overview four ways.
`app` and `plot` are the rows in the C tables -- what the application actually *handles* -- and `tables`
is their sum. `ui`, `ui_accel`, `ui_icon` and `ui_bare` are what `src/ui/shortcuts.ui` *declares*, read
out of the resource with a markup parse rather than off the built widgets, because a tree walk counts
@@ -642,7 +646,8 @@ every row twice: `GtkShortcutsWindow` reaches each one from more than one place
**`ui` must equal `tables`** -- two lists of one thing, and a row in one but not the other is a gesture
that works and is not documented, or is documented and does nothing -- and **`ui_bare` must be zero**: a
row with no accelerator and no icon is a cell reading "No Shortcut". `ui_accel + ui_icon == ui`, so those
two also catch a row whose accelerator went missing. `settings=` must be a real path (the same one the About dialog shows). **`icon=` must read `org.udpscope.app`** — it is read back through `adw_about_dialog_get_application_icon()` while the dialog is open, so the dialog is asserted to *carry* the icon rather than merely to open (a `set_application_icon()` lost in an edit leaves a dialog that opens, looks almost right, and shows a generic placeholder). `closed ok` with **no `Gtk-CRITICAL`** is as much a crash test of the two dialogs built, presented and closed in one run as it is a state check: they are closed by **two different calls**, so a mismatched pair crashes or leaks here. One is a `GtkWindow` (the shortcut overview, `gtk_window_close()`/`::close-request`); the About dialog is an `AdwDialog` and closes with `adw_dialog_close()`, cleared on `::closed`. The manual is *not* one of them — it opens in Yelp, a separate process, so there is nothing here to open, read back or close.
two also catch a row whose accelerator went missing. `settings=` must be a real path (the same one the About dialog shows). **`icon=` must read `org.udpscope.app`** — it is read back through `adw_about_dialog_get_application_icon()` while the dialog is open, so the dialog is asserted to *carry* the icon rather than merely to open (a `set_application_icon()` lost in an edit leaves a dialog that opens, looks almost right, and shows a generic placeholder). `closed ok` with **no `Gtk-CRITICAL`** is as much a crash test of the two dialogs built, presented and closed in one run as it is a state check: they are closed by **two different calls**, so a mismatched pair crashes or leaks here. One is a `GtkWindow` (the shortcut overview, `gtk_window_close()`/`::close-request`); the About dialog is an `AdwDialog` and closes with `adw_dialog_close()`, cleared on `::closed`. The manual is *not* one of them — it opens in Yelp, a separate process, so there is no window of ours to read back or close; the line below is the reading of that path instead.
- `[help] launch attempted=1 done=1 ok=1 error='' installed=/home/…/.local/share/help/C/org.udpscope.app` (stderr, `UDPSCOPE_PROBE_HELP_MS`, a moment after the line above) — the **launch of the manual**, the one part of the help path no other reading reaches: a successful launch leaves no window of ours, so nothing else can say whether `show_help()` did anything at all. `attempted=` is set at the call (`0` would be a probe that never got as far as calling it); `done=` says the asynchronous callback ran, and **`done=0` is the reading for a launch that never called back**; `ok=1` is the `help:` URI handed to a viewer, `ok=0` with the launcher's `error=` text is a launch that failed. **`ok` is not "a window appeared"**: Yelp is another process, and under a headless X server it cannot map one — the launch is *this* process's half (the URI reached a registered handler), which is exactly what the desktop does when a user presses F1, and what an absent handler or an uninstalled viewer breaks. `installed=` is `help_installed_path()`'s answer — the `$XDG_DATA_HOME`/`$XDG_DATA_DIRS` directory the document was found in, or `(not found)` — and it is what a failing `ok=0` should be read beside, because the launch error alone cannot tell a missing document from a missing handler. On a machine with the set installed by `cmake --install build --prefix ~/.local` and `yelp` present, the line reads `ok=1 … installed=~/.local/share/help/C/org.udpscope.app`. A **failure** leaves the launcher's `GtkAlertDialog` up on purpose — that is what a user gets, and the probe must not report a failed path as a silence.
- `[menu] N 'label' accel='key' h=P label_x=X accel_right=R/W order=D ok=O`, one line per row,
then `[menu] rows=R expected=E order=D rows_bad=B popover_visible=V mapped=M button_mapped=B`
and `[menu] status pos=top bubble=WxH button_top=T button_right=R fits=F` (stderr,
@@ -671,7 +676,10 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
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.
only while the math editor window is open. **On a fresh config this reads `hints=0 ids={}`**, because
the layer is off by default; the example is the state after the row below was switched on, and a
fresh run's `hints=0` beside a `settings.toml` with no `contextual_help` key is the default being
read rather than assumed.
- `[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'`,
@@ -679,7 +687,9 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
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`.
`toggle=` being the contextual-help row's check, which *is* `settings.contextual_help`. It reads `0`
on a fresh config (the default) and `1` on one where the setting is on; the probe's next step drives
it both ways, which is why the example here is the `1` side.
- `[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
@@ -1088,7 +1098,10 @@ configuration that carries settings, is the defect this exists for — a saved h
## `[sources] waveforms` and the waveform load
```
[sources] chooser ui: page='sources' title='Sources' icon=yes page='waveforms' title='Waveforms' icon=yes visible='sources' switcher_wired=1 buttons=[prefs=1 new_host=1 open=1 cancel=1]
[sources] chooser ui: page='sources' title='Sources' icon=yes page='waveforms' title='Waveforms' icon=yes visible='sources' switcher_wired=1 buttons=[prefs=1 new_host=1 open=1 cancel=1] help=[button=1 docs=1 tour=1 toggle=1 on=0]
[sources] chooser help: tour requested
[sources] chooser help: tour_pending=1
[sources] chooser help: toggle=1 setting=1 file=/tmp/…/udpscope/settings.toml
[sources] waveforms: 1 recent /tmp/rt.npy(ok) page=waveforms
[sources] picked waveform /tmp/rt.npy
[udpscope] waveform loaded: /tmp/rt.npy signals=4 samples=20001 rows=20001 decim=1 span=2.000000s rate=10000 time=[-2.000000..0.000000]
@@ -1097,7 +1110,7 @@ configuration that carries settings, is the defect this exists for — a saved h
| field | value it must read | the defect a wrong reading names |
| --- | --- | --- |
| `chooser ui:` | both pages with a `title` and an `icon=yes`, `switcher_wired=1`, and all four buttons `1` | the chooser's `.ui` losing an id: the code names the missing one and refuses the window, and this line is the frame that *did* build -- a page with `icon=NO` is one the switcher cannot draw |
| `chooser ui:` | both pages with a `title` and an `icon=yes`, `switcher_wired=1`, all four buttons `1`, and `help=[button=1 docs=1 tour=1 toggle=1 on=0]` | the chooser's `.ui` losing an id: the code names the missing one and refuses the window, and this line is the frame that *did* build -- a page with `icon=NO` is one the switcher cannot draw, and a `help=` field at `0` is a help row (or the button that opens it) the dialog no longer carries |
| `waveforms:` | one entry per remembered export, with `(ok)` or `(missing)`, and `page=waveforms` once the page is shown | the Waveforms page not built, or a stale path offered as if it were openable |
| `picked waveform` | the selected path | a pick that carries no file: `--open` is what the mode reads |
| `signals=`, `rows=` | the export's `cols=` and `rows=` | a reader mis-slicing its records, or the columns bound to the wrong channels |
@@ -1106,6 +1119,21 @@ configuration that carries settings, is the defect this exists for — a saved h
| `span=`, `rate=`, `time=[…]` | the exported window and rate, `[-window, 0]` | the plan not covering the whole file (the tail arrives as the whole picture) or the display epoch not pinned to the file's last stamp |
| `frames=0 packets=0` beside `channels=N` | 0 — no acquisition | a socket opened for a file, which would also make the starvation notice live |
The chooser's **help menu** — `Documentation`, `Guided tour` and `Enable contextual help`, the three
affordances the window's Help view carries, moved in front of a user who has not started yet — has two
probe legs, both of which then fall through to the normal first-row pick so the run they affect is the
run they report:
- `UDPSCOPE_PROBE_CHOOSER_TOUR=1` emits `::clicked` on `Guided tour`, and `[sources] chooser help:
tour_pending=1` reads the request back out of the `Config` the run will be handed. **`tour_pending=1`
is the assertion**: the tour is drawn on a window that does not exist yet, so the entry can only work
by setting that field, and the reading it *started* is the exit dump's `help layer:` — `tour=running
… auto=0 requested=1`, `requested=1` being this path (see `help layer:` above).
- `UDPSCOPE_PROBE_CHOOSER_HELP=1` sets the `Enable contextual help` row active and reads the settings
file back: `toggle=1 setting=1 file=<settings.toml>`. **`setting=1` beside `toggle=1`** is the pair
that matters — the row *is* the setting, and the app is built from the file and not from the dialog,
so the file is where it has to have landed.
## `[drop-probe]`
```