ui: the sidebar's tabs become one dock; Measure and Signals by keyboard section; session keeps the dock's state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-11-02 20:37:55 +01:00
1 parent 31ff778e07
commit 4b65197f56
19 files changed
+421 -419

No files matched your search

+1 -1
View File
@@ -36,7 +36,7 @@ What feeds `Acquisition` is a `DataSource`, one of the registered *kinds*.
`src/source/udps_*.cpp` (`grep -rn "udps_" src` outside those files is a leak of the old coupling).
- The frame is **single-timebase**. A multi-timebase source (ABScope) will change the interface and
the data path together; do not bend one frame to carry two clocks.
- A kind's capability panels are pages of `side_stack`, named `source-<id>`. A disconnected source
- A kind's capability panels are pages of the slide-over (`slideover_add_page`), named `source-<id>`. A disconnected source
leaves them **insensitive, not hidden** (`apply_source_panels` in `src/ui_source.cpp`).
- Session file naming for a non-UDPS kind: the pseudo-host is `<kind>-<identity>` (characters outside
`[A-Za-z0-9._-]` become `_`) on port **1** (0 is rejected by the session-file parser), the kind is
+20 -26
View File
@@ -50,7 +50,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
runs after the window is destroyed and asking GTK about a widget then is four criticals, which is
how the first version of that line earned them. What stays in the menu is what acts on the *data*
(Fit, Export, Configurations) and the one editor that is not a pane (the layout editor): the signal list
is a *tab* of the sidebar, so there is nothing left to reveal or hide about it. The Arm/Disarm button in the trigger panel still has text.
is a section of the sidebar's dock, so there is nothing left to reveal or hide about it. The Arm/Disarm button in the trigger panel still has text.
- **Five of this app's icons are its own, and they are a `GResource` compiled into the
binary.** No icon theme has a glyph for an oscilloscope trigger, a pair of cursors or a
measurement ruler, and the stock names those buttons used to carry said something else
@@ -61,9 +61,9 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
an icon with a live user is not an orphan, whatever the toggle that introduced it did -- and it is
the Preferences window's icon.
The sidebar's **tabs have no icons any more**: two glyphs (`udpscope-tab-measure-symbolic`,
The sidebar has **no tabs any more** (it is one dock): two tab glyphs (`udpscope-tab-measure-symbolic`,
`udpscope-tab-signals-symbolic`, each a 32x16 pair) were drawn for them and judged not understandable
enough, so the tabs are text and both SVGs were deleted. The *trigger's* own icon was redrawn to the
enough, so the tabs were text, and both SVGs were deleted. The *trigger's* own icon was redrawn to the
Signals glyph's picture at that time and kept it.
- **A shortcut table row's `accel` is GTK accelerator syntax, and a pointer gesture has
none.** `src/shortcuts.cpp`'s strings are handed to `AdwShortcutsItem`, which parses them
@@ -229,11 +229,13 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
instead of across. `gtk_widget_set_visible()` is what a layout honours, so anything that must not
take room has to be hidden and not merely unrevealed; the revealer keeps the slide while the pane
*is* open. The allocated width is how this was found -- the revealer's own measure said 0.
**The sidebar is a tab stack now, and has no `GtkRevealer` left in it**: an `AdwViewSwitcher`
above an `AdwViewStack`, each page a `GtkScrolledWindow` of its own (the two pages are different
heights, so one shared scroller would leave a switch scrolled where the new page never asked to
be). The reason is the one this trap teaches: five sections competing for one 320 px column left
every one of them short of room. A *feature's* on/off is no longer a visibility at all -- the
**The sidebar is one dock now, and has no `GtkRevealer` left in it**: a single `GtkScrolledWindow`
(`dock_scroller`) holding the signal list, the cursors and the measurements one under another. It
replaced an `AdwViewSwitcher` over an `AdwViewStack`, whose four text tabs did not fit 320 px (and a source
can add more); a source kind's own panels are slide-over pages, not dock sections. The reason is the one
this trap teaches: sections competing for one 320 px column each left every one of them short of room, and
a scroller gives each its natural height. `m` and `g` scroll the dock to the cursors and to the signal list
(`dock_show_section()`). A *feature's* on/off is no longer a visibility at all -- the
**cursors'** toggle sets its pane's **body** insensitive -- the readout has nothing to show with the
instruments off -- and the *body* and not the pane, because the pane's heading carries its own switch
(see the next bullet). The **trigger's body is deliberately never disabled**: a trigger is configured
@@ -241,23 +243,15 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
ordinary way to work. It used to go insensitive with the mode, which left the settings unreachable
exactly when a user wanted to change them; `trigger panel sensitive …=1` is the regression guard, and a
`0` there means the configuration has been locked out again. The
trap itself does not go away with the widget: a stack's hidden page measures 0x0 too and is still
given the leftover width, so **ask about state, not about size** carries over unchanged. Every
allocation read in the sidebar is a debug capture, and each reads 0 on the page that is *not*
current -- a change in what the reading means, not a defect, and no functional code may depend on
those numbers.
- **The sidebar's tabs are text: `AdwViewSwitcherPolicy.wide`, a title on every page, no icon.** The
built-in pages get their titles in the `.ui` and a source kind's pages from `SourcePanel::title`
(`adw_view_stack_add_titled`); the page's *name* still identifies it to `select_side_tab()` and the
session file. An icon-only version existed (policy `narrow`, empty titles, the pills named in code by
`name_side_tabs()`, because `narrow` still draws a label beside an icon); it was reversed because the
glyphs were not understandable. A page added with an empty title is a blank pill, and
`switcher: … pills=N labels=N images=0 pages=N` reads it as `labels` below `pills`. **No icon at all**
is not what "no `icon-name`" gives you: `AdwViewSwitcherButton` always builds a `GtkImage` and falls
back to `image-missing` (the `[x]` placeholder, which also squeezed the title to "Meas..."), and no
property turns it off. `keep_tabs_icon_free()` (`src/ui_source.cpp`) hides every image in the switcher,
re-hides one that is shown again, and re-runs on the stack's `items-changed` (connected *after* the
switcher's own, so the new button exists) and on the switcher's `map`; `images=0` is the reading.
trap itself does not go away with the widget: a section scrolled out of view is still laid out, but
**ask about state, not about size** still holds for anything a tick reads.
- **The dock is shown but insensitive until the stream has signals ("disable, do not hide").**
`update_side_visibility()` and the tick's per-frame block set the column's visibility from the user's wish
alone (`sidebar_wanted`) and the dock's *sensitivity* from `have_signals`; the toolbar's Dock button stays
sensitive throughout. The wish is persisted (`dock = true|false` in the session file) and a change of it
schedules a save. There is no tab icon to keep clear any more: the `keep_tabs_icon_free()` workaround for
`AdwViewSwitcherButton`'s placeholder `[x]` image went with the switcher, and `dock:` in the dump
replaces the `switcher:` line.
- **An *overlaid* sidebar covers the plot, so a drag out of it has nowhere to land.** `collapsed` in an
`AdwOverlaySplitView` means the sidebar sits *on* the plot rather than beside it: the pointer is over the
list, never over a drop target, and a signal can be dragged but not dropped. `row_drag_sidebar()` hides
@@ -278,7 +272,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
`sync_panel_switches()` re-reads each pair from the toolbar's state after any change — so clicking
either end moves both, and `panel switches: trigger=1/1 cursors=1/1` is the reading. Two traps:
a `GtkSwitch` emits `notify::active` for a *programmatic* write as well, so the sync holds a guard
(`App::panel_switch_sync`) or the program's own write comes back as a click; and the switcher must
(`App::panel_switch_sync`) or the program's own write comes back as a click; and the switch must
stay *out* of what the pane disables, because a control inside an insensitive parent cannot be
clicked — a switch that greyed out with its own pane could never turn it back on. That is why the
sensitive target is the pane's **body** (`trigger_body`, `cursors_body`) and not the pane.
+3 -2
View File
@@ -23,10 +23,11 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
toggle is the same mistake in one line, and it cannot be told from a real change by a "is this our own
write" flag either, since the toggle's signal may fire after the write returns. Instead `App::sidebar_wanted`
holds the wish (written only by `on_sidebar_toggled`, guarded by `App::sidebar_syncing` around our own
`set_active`), `update_side_visibility()` is the single place that turns that wish plus "are there signals"
into the panel's *and* the button's state together, and `tick()` re-asserts them when the widget disagrees —
`set_active`), `update_side_visibility()` is the single place that turns that wish into the panel's visibility
and "are there signals" into the dock's *sensitivity* (the button stays sensitive), and `tick()` re-asserts both when the widget disagrees —
which is also why a hide *gesture* does not stick: the panel stays where the button says it is. The
no-signals case must leave the wish alone, or a source connecting later finds a sidebar the user never closed.
The wish is the session's `dock` key: a change of it (not a program's own write, and not a restore) schedules a save.
- **`AdwOverlaySplitView:sidebar-width-fraction` is a 0..1 fraction of the window, whatever
`sidebar-width-unit` says.** Setting a pixel count there (320) is a `GObject` critical, and it silently
falls back to 0.25 — which reads as "the sidebar is mysteriously 560 px wide" on a wide window with no
+4 -1
View File
@@ -30,7 +30,10 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
not freeze a colour that is meant to follow the signal list), **the math channels with their
graphs**, **the measurements pane**, and **`measure_stats`** — the measurements statistics
toggle; written as a bare top-level boolean (`measure_stats = true/false`), defaults to `false`
when absent. Signal identity
when absent, and **`dock`** -- the Dock button's state (the user's last word on it, `sidebar_wanted`),
a bare top-level boolean written where the sidebar's `side_tab` used to be, default `true`. The parser
ignores unknown keys, so a file written before the tabs became a dock (`side_tab = "measure"`) still loads
and simply opens with the dock shown; nothing needs to read the old key. Signal identity
is the *name* throughout (a CONFIG renumbers channels), 'enabled' is read off the channels the
model built -- an enabled signal is exactly one that produced a channel -- and `apply_layout()`
installs the graphs **before** assigning the rows, since a row may hold a math channel's name and
+61 -55
View File
@@ -479,22 +479,18 @@ Two lines of the self-check output are assertions in disguise:
substitutes a missing-image placeholder, the button still works, and every other line of the
dump still passes. Changing an icon name is a one-line change in `build_toolbar()` and this
is the only check that it names something that exists.
- `switcher: policy=wide pills=4 labels=4 images=0 pages=4 size=308x34` (in the exit dump, beside `panels:`) — the
sidebar tabs as the switcher actually draws them, and they are **words**: `labels` counts *mapped labels
with text*, so `pills=labels=pages` says every page has a visible title. **`images` must be `0`**: it
counts *mapped* `GtkImage`s inside the switcher. `AdwViewSwitcherButton` builds an icon in every tab, and
a page with a title and no `icon-name` gets `image-missing` -- the `[x]` placeholder; the tabs hide it
(`keep_tabs_icon_free()`, which also covers pages `build_source_panels()` adds). A nonzero `images`
names a tab still drawing an icon or its placeholder (`images=pages` is the unfixed state), and
`pills`/`labels` never saw it. Read it on a source with extra pages (`--source probe` under
`UDPSCOPE_PROBE_SOURCE_KINDS`) as well as on UDPS. `pages` is read from the stack
(2 for UDPS, 3 for the probe source, 4 for the E-1608 with its Analog out and Digital I/O tabs). A
`labels` below `pills` is a page added with an empty title; `pills=0` means nothing is mapped, which
makes the rest of the line meaningless rather than a pass. `policy=wide` is read rather than assumed.
`size=` is the bar's own size: a text tab costs about 34 px of height, and a width of 308 shows the
four tabs fit the 320 px column.
- `dock: shown=1 sensitive=1 w=320 scroller_pages=1` (in the exit dump, beside `panels:`) -- the sidebar's dock
as the last tick found it. It replaces the old `switcher:` line: there is no tab switcher, the sidebar is
one scroller (`dock_scroller`) holding the signal list, the cursors and the measurements. **`shown=1`
always** (the dock is on screen from the start; `shown=0` only when the user hid it with the Dock button,
which a probe does deliberately); **`sensitive=0` only before the stream has signals** ("disable, do not
hide") -- a run against a port with no sender reads `shown=1 sensitive=0`, a normal run `sensitive=1`;
`w=` is the sidebar's own width (320); **`scroller_pages=1`** counts the scrollers directly under the dock
column and says there is one scroller and no stack. These are captured on the tick or at the exit probe:
the dump runs after the window is destroyed. A source with extra panels adds *no* dock section: its panels
are slide-over pages (`source-<id>`), so the line reads the same on the E-1608 and the probe kind.
- `help layer: contextual=on hints=12 ids={events plot status cursors measures trigger trigger_advanced events_record trigger_search signals signals_config window} icon=dialog-information-symbolic(ok) hint=12x12; 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
dump, beside `dock:`) — 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: eleven 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). Four of the eleven are not a pane's *heading*
@@ -538,7 +534,7 @@ Two lines of the self-check output are assertions in disguise:
[invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)). The writer chains to
`g_log_writer_default()`, so every message still reaches stderr exactly as before and the documented
baselines are read the same way.
- `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
- `help figures: trigger_head=X,Y,W,H trigger_summary=… trigger_panel=… signal_list=… 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
@@ -919,9 +915,9 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
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).
- `[helpui] step=1/5 visible=1 slideover=none spot=[x,y WxH] target=[…] contains=1 card=[…] clear=C`
— **in window coordinates**, the frame a screenshot is cropped in, with the slide-over's open page
(`none`, or a page id) beside it; 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)
@@ -949,34 +945,30 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
read `mapped=0` (and `0x0`), which is exactly why the header box exists (`search_head` in
`src/ui/trigger-panel.ui`) and why the hint joins it as its last child. `mapped_header=` is the
header box beside it, and `hint=WxH` is the same 12x12 the layer's own line reads.
- `[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] …` (stderr, `UDPSCOPE_PROBE_TOUR_SURFACE_MS`) -- a stop that stands on a surface it is
not on, entered from the state it is not in. Since the sidebar's tabs became the dock the probe opens the
`trigger` slide-over page instead of switching a tab, starts the window's tour and walks it a stop per tick
(400 ms). **The tour's own rework for the slide-over is Task 5's**: until its page hook exists the tour
has no page stack, so a stop whose target is on a slide-over page is dropped, and the lines below read the
dock only. 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).
- `[toursurface] before: slideover=none signal_list_mapped=1 running=1` -- the state a first run starts in
(the dock is on screen), then the auto-started walk is dismissed.
- `[toursurface] on trigger: slideover=trigger signal_list_mapped=1 side_box_mapped=1` -- a frame after the
page was opened: the dock is still on screen under the slide-over (they are not alternatives).
- `[toursurface] tour: visible=1 steps=N step=0 signals_stop=1` -- **`signals_stop=1` is the filter's
reading**: the signal list is in the dock, which is mapped, so its stop is in the plan.
- `[toursurface] step=N/M slideover=<page> target_mapped=D[ target=signal_list]` -- one per stop;
**the `signals` stop reads `target_mapped=1`**.
- `[toursurface] signals stop=N/M visible=1 slideover=… spot=[…] target=[…] contains=1 card=[…] clear=1` --
the `tour_geometry_line()` the helpui probe uses: **`contains=1` is the assertion** that the spotlight
and the card are on the list.
- `[toursurface] collapsed: side_box_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 and the stops that need the
dock are dropped while the page-less ones stay. This is what says a *collapsed pane* is dropped.
- `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | c: cursors: 0->1 | m: dock 0->243 (cursors_y=243 reach=2256 at_cursors=1) | g: dock 243->0 at_top=1 | … | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** (`g` returns, so its pair reads `243->0`) and **`refused` must be 1**.
The `m`/`g` readings are the dock scroller's vertical adjustment before and after (the probe pads the dock to 3000 px so there is something to scroll, and undoes it at the end): **`m` must land on `cursors_y`, the cursors panel's y in the dock (`at_cursors=1`), and `g` must return to 0 (`at_top=1`)**, which is what says those two keys scroll to a section rather than toggling a pane; a `reach` of 0 would mean the dock fit its viewport and the probe's padding was lost; `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
gaps_refused=N open_holes=N bands=N bands_repaired=N nan_slots=N scanned=N trigger_in_hole=D`
@@ -1796,18 +1788,16 @@ and `regained` reads 400 ms later.
## `[side-tab-probe]`
```
[side-tab-probe] page=signals accums=4 baseline recorded
[side-tab-probe] page=signals rows=4 accums=4 advanced=4 min_n=18
[side-tab-probe] section=measure accums=4 baseline recorded (measure at scroll 243 of 2256)
[side-tab-probe] section=signals rows=4 accums=4 advanced=4 min_n=18
```
`UDPSCOPE_PROBE_SIDE_TAB_MS` is the one reading for the rule that **the statistics accumulator keeps running
on the hidden page**. It switches *to* the Measure tab, forces one pass so there is a baseline, then switches
away to Signals and reads again two seconds later -- with the measurements' page not current, so its widgets
are unrealized and measure 0x0.
`UDPSCOPE_PROBE_SIDE_TAB_MS` (the variable and the tag keep their pre-dock names; the function is `dock_section_probe`) is the one reading for the rule that **the statistics accumulator keeps running
while the Measure section is out of view**. It scrolls the dock *to* the Measure section (`dock_show_section("measure")`), forces one pass so there is a baseline, then scrolls back to Signals and reads again two seconds later. To make the scroll observable on a short window the probe pads `dock_page` to 3000 px (probe only), and it re-arms once after 400 ms so the first reading has the layout.
- **`advanced=4` is the assertion**: every accumulator gained passes while its page was off screen, and
- **`advanced=4` is the assertion**: every accumulator gained passes while its section was out of view, and
`min_n` (the smallest `n` seen) must be non-zero too. **`advanced=0` is the defect**: something gated the
accumulation on the visible tab, so the readout would gain a gap in its min/max/avg for exactly as long as
accumulation on the visible section, so the readout would gain a gap in its min/max/avg for exactly as long as
the trigger was on screen. A `Gtk-CRITICAL` here is the other half of the same rule -- a label or an
allocation read on the unrealized page.
- The probe enables a signal **and assigns it to a plot** itself, both deliberately. A signal starts disabled
@@ -1849,7 +1839,7 @@ kind, so a run without it never shows that kind. The `form` line appears once pe
`drained=0` is a queue that `poll()` does not drain on some path; `after` greater than `before + 1` is a
command run twice.
The UDPS run has no `source-` page in `side_stack`, because the UDPS kind has no panels.
The UDPS run has no `source-` slide-over page, because the UDPS kind has no panels.
## `[e1608]`
@@ -2591,3 +2581,19 @@ The same probe then puts five measurements on the same spiked `sine` as **two wi
- **`measure windows=2`** -- the five legs ask for corners `0, 50, 0, 0, 50` on one channel, which is two
distinct windows. **`windows=1` means the corner is not part of the key**: the five collapsed onto one walk
and two of them read a signal they did not ask for.
## `[dock-session]`
```
[dock-session] hidden: wish=0 save_scheduled=1 collected=0 saved=1 loaded=1 read=0 applied=1 split_follows=1 ok=1
[dock-session] shown: wish=1 save_scheduled=1 collected=1 saved=1 loaded=1 read=1 applied=1 split_follows=1 ok=1
[dock-session] old file (side_tab only): loaded=1 dock_shown=1 default=1 ok=1
```
`UDPSCOPE_PROBE_DOCK_SESSION_MS` reads the one thing the dock persists, the Dock button's state, as `dock = true|false` in the
session file (it replaced `side_tab`). Each of the first two lines drives the toggle, then follows the wish through: `save_scheduled=1` is
`on_sidebar_toggled` having scheduled a save (a user's wish that was not saved is lost at the next start), `collected` is
`collect_layout()`'s `dock_shown`, `saved`/`loaded` the file round trip on a temp path, `read` the loaded value, `applied`
the wish after `apply_layout()`, and `split_follows=1` the split view following it. **`ok=1` on all three lines is the assertion.** The third line
loads a file as the previous version wrote it (`side_tab = "measure"`): it must load with no error and leave `dock_shown` at its default
(`true`) -- the parser ignores unknown keys, and this is what says the old key still does not break a session. The probe writes only to a temp path.
+15 -8
View File
@@ -412,12 +412,19 @@ precedent, not an exception.
pauses, reports again with `dn=` per measurement, then resets and reports a third time. Three
phases: `phase=0`, `phase=1` (after pause) and `phase=2` (after Reset). Needs
`UDPSCOPE_ENABLE_ALL_MS` ahead of it and `UDPSCOPE_PROBE_MEASURE_MS` to add measurements first.
- `UDPSCOPE_PROBE_SIDE_TAB_MS=3000` with `UDPSCOPE_EXIT_AFTER_MS=11000` — the measurements on the
Measure tab, a forced pass, then the tab switched away and read again two seconds later. **`advanced=4`
is the assertion** (every accumulator gained passes while its page was off screen); `advanced=0` is the
defect where the accumulation is gated on the visible tab, which would leave a gap in min/max/avg for
- `UDPSCOPE_PROBE_SIDE_TAB_MS=3000` with `UDPSCOPE_EXIT_AFTER_MS=11000` — the measurements in the
dock's Measure section, a forced pass, then the dock scrolled back to the signal list (the Measure
section out of view) and read again two seconds later. The probe pads the dock to 3000 px so a scroll is
observable on a short window. **`advanced=4`
is the assertion** (every accumulator gained passes while its section was out of view); `advanced=0` is the
defect where the accumulation is gated on the visible section, which would leave a gap in min/max/avg for
exactly as long as the trigger was on screen. It enables *and plots* its own signal, so it needs no other
probe ahead of it. The reading's own entry is in `probe-readings.md`.
probe ahead of it. The reading's own entry is in `probe-readings.md`. (Probe name `dock_section_probe`; the
variable and the `[side-tab-probe]` tag keep their old names so existing scripts keep working.)
- `UDPSCOPE_PROBE_DOCK_SESSION_MS=2500` with `UDPSCOPE_EXIT_AFTER_MS=8000` -- the session file's `dock` key,
end to end: the dock hidden, saved and reloaded; shown again; and a file from the previous version
(`side_tab = "measure"`, no `dock`) loaded without an error. Three `[dock-session]` lines, each `ok=1`;
entries in `probe-readings.md`.
- `UDPSCOPE_PROBE_SLIDEOVER_MS=1500` with `UDPSCOPE_EXIT_AFTER_MS=5000` (and `UDPSCOPE_ENABLE_ALL_MS=500`) -- the
slide-over panel over the plot's right edge, opened on a throw-away `probe` page, read 0.5 s later. Two
`[slideover]` lines. Line 1 must read `panel=360x<overlay height> ... edge_flush=1 full_height=1
@@ -631,12 +638,12 @@ 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**,
- `UDPSCOPE_PROBE_TOUR_SURFACE_MS` — the tour's *surface* switch: it opens a slide-over page (`trigger`),
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
could have revealed). `[toursurface] signals stop=4/5 … slideover=none … contains=1` and
`[toursurface] collapsed: side_box_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