Beside each figure row's full-screen toggle -- same corner, same show-on-approach reveal, same
one-figure/never-maximized rules -- a **detach** button moves the figure into a window of its
own: copied into a new scope window (titled "detached") and removed from the main window's rows,
which then save the change. The detached window is the one scope window with an **attach**
control: an overlay wrapping its GL area, a button in the top-right corner revealed by the same
pointer-proximity rule from the overlay's own motion controller, and one click appends the
figure back to the main window's rows (cells, weights, every slot's signal in its colour) and
closes the window.
`detach_figure()` / `attach_scope_window()`; the reveal and placement share the maximize
buttons' geometry and pointer state, per row. A detached window's rows participate everywhere a
scope window's do (drops, the signal page's *Plot on* groups), which the previous commit gave
them.
Probe: `[maximize-probe]` now counts the detach buttons (`detach_single=0` for one figure,
`detach=2` for two, absent while maximized) and reads their corner reveal
(`detach_near=1 detach_other=0`, the pointer parked on the detach button's own centre -- the
strictest zone of the pair). Real-pointer smoke: hover reveals the pair, clicking detach opens
the window, clicking attach closes it with the figure back. Docs: README, the Mallard layout
page, the probe readings, the layout invariant, and the tour's plot stop.
The second windows were read-only by accident: nothing accepted a drop on them, and the
signal page's *Plot on* buttons knew only the main window's rows.
- **Drop target per scope window**: the same string payload a badge drags (the signal index),
handled by `on_scope_drop_channel()` -- the target carries its own window, the row is that
window's cell under the pointer, and `assign_signal_to_scope()` lands the signal in the
window's own row set (enabling an unacquired signal first, exactly as the main drop does).
The rebuild and the area's re-point are the window's next frame in the tick: a drop arrives
from inside the very widget it rearranges, and a synchronous rebuild would reset the drag
controller under it.
- **Plot on, per window**: the signal page carries one named group per scope window ("In
\"plot 2\""), one button per plot of that window, sensitive on that window's own rows. A
window closing refreshes the pages in its destroy handler, before the deferred delete could
dangle a button's scope pointer.
- Probe: `[scope-probe]` drops through the wired handler (`drop=1 slot1='square'`) and counts
the page's groups (`page_groups=2`); the section documents that it must not run with
`UDPSCOPE_ENABLE_ALL_MS` -- the enable-all probe's second leg clears the front plot, which
is the slot this probe copies. README, help and the layout invariant follow.
A batch of view and instrument work, with the probes and docs for each.
- **Full screen per figure row**: a toggle on each row's top-right corner, revealed by
pointer proximity (`kMaximizeRevealPx` around its placed rect, per row, read from an
overlay-level motion controller so a revealed button under the pointer cannot flicker);
the plot menu offers the same as *Full screen this figure* / *Restore the layout*. The
arrangement is saved and restored through `RowState`, so nothing plotted moves. A layout
of one figure carries no toggle.
- **Second window**: `open_scope_window()` copies a figure (cells, width shares, signals in
their colours) or opens an empty one, with its own `RowSet` and `PlotArea`; the main
window's tick binds, acquires and repaints it, mirroring the time window and pin. Entries:
the plot menu's *Open this plot in a new window* and the overflow menu's *New scope window*.
- **Layout editor from the plot**: the plot menu gains *Edit the layout...* / *Done editing
the layout*, flipping the app's one toggle.
- **Measurements gate**: `MeasureOptions::gate` restricts a measurement to the held/pause
snapshot (live falls back to the live ring), persisted with the session and shown in the
measure dialog.
- **Crosshair axis badges**: a preference (`crosshair_axis_labels`, on by default) and the
plotter's badges where the lines meet the axes, in the axes' own formats; the plot menu
and the probe read them.
- **Event cache per configuration**: `EventStore` is keyed `<source>/<configuration>/`,
`retarget_event_store()` follows every configuration switch, and a pre-configuration cache
(loose files) is adopted into the configuration's folder on first start.
- **GTK4 lifecycle**: the main window's handlers move from the non-existent `"destroy"`
signal to `close-request`; scope windows are closed with it and the application quits
explicitly (the exit run's hang). The exit probe closes with `gtk_window_close()`.
- Probes: `[maximize-probe]` (single-figure rule and the corner reveal), `[scope-probe]`,
`[split-probe]` cell crosshair leg, `[prefs-probe]` store keying, `[configs]` store key,
`[rowmenu]` layout-editor entry; docs, help pages, README and AGENT.md updated.
Also fixes a probe-only use-after-free: `toggle_maximize_figure_row` replaces the figure
object, so the area must be re-pointed (the button's path does; the probe's raw call did not).
The help was 14 Pango-markup `.txt` bodies in a second GResource, drawn into a paged `GtkWindow`
built in `help_window.cpp`. That made F1 a viewer of ours with no search index, no cross-references
and no translation, and it kept the manual in the *binary* when every other desktop application's is
a document on disk. It is now a real Mallard set:
help/C/org.udpscope.app/ index.page + 14 topic pages + figures/*.png
and `show_help()` hands `help:org.udpscope.app` to whatever the desktop registered for the `help:`
scheme -- Yelp on GNOME -- through a `GtkUriLauncher`, reporting a launch failure with a
`GtkAlertDialog` rather than a click that silently does nothing. The paged window's builder, the
page table and `help_self_check()`'s markup validation are gone with the resource.
The document identifier is the desktop file's own basename (its desktop-file id), derived in
CMakeLists from `data/org.udpscope.app.desktop.in`, so the same string is the `.desktop` name, the
GApplication id and the install directory at once. The set is installed by a new `install()` rule to
`<datadir>/help/C/org.udpscope.app/` -- where the freedesktop Help System Specification's `help:`
URI resolves `help:<id>` -- so the manual is read from a datadir by another process even though the
binary has no install step. `UDPSCOPE_HELP_NAME` and `UDPSCOPE_HELP_DIR` are compiled in from that
one place.
The 14 pages were migrated fact for fact, but restructured the Mallard way: a `<desc>` per page
(what a search result shows), `<steps>` for procedures, `<terms>` for option lists,
`<gui>`/`<key>`/`<keyseq>` for interface words and keys, `<note>` for the asides, and a topic graph
via `<link type="guide" xref="index#main"/>`. The figures are annotated screenshots produced by
`tools/make-help-shots.sh`, which drives the app headlessly against `tools/mock_udpstreamer.py` and
draws the boxes with ImageMagick; the side panel's box is measured from the app's own `side=NNNx`
dump reading, the header and status bands come from a documented table, and a crop whose standard
deviation is 0 fails the script.
`cmake --build build --target test-help` checks the set the way `test-ui` checks the `.ui` files:
`yelp-check`'s offline subcommands (links, media, orphans, ids) and a `yelp-build html` render.
`yelp-check validate` is deliberately absent -- it fetches the Mallard RelaxNG schema over the
network and fails offline for that reason, not for a fault in the pages.
The `[help]` probe reading changes with the resource it was written for. `parsed == pages` over
in-binary markup cannot exist now, so `help_self_check()` scans `UDPSCOPE_HELP_DIR` and the line
reads `topics=14 index=1 figures=3`; `topics=0 index=0` is the manual missing from the tree, which
is the finding the old count named.
Verified: clean build with the documented three warnings (the help bundle's `-Woverlength-strings`
is gone, so the baseline moves from four to three); test-ui 14/14; test-help clean (the four
subcommands and 15 pages rendered); a temp-prefix install lands 15 `.page` files and 3 figures at
`share/help/C/org.udpscope.app/`; the headless run reads `[help] topics=14 index=1 figures=3 dir=...`
with 0 Gtk-CRITICAL. Yelp's own window could not be exercised on this box -- it starts under Xvfb,
activates its portals and never maps, the same WebKit/portal limitation that stops a grab -- so its
rendering and its search index are unverified here; the content pipeline was (`yelp-build` renders
the whole set).
Docs: the manual's build/install/open story in `build-and-run.md` and README; the warning baseline
in `verification.md`/CLAUDE.md; the help bullets in `invariants-gtk-and-chrome.md`; the `[help]`
line in `probe-readings.md`.