Feature/source plugins #1

Merged
martino merged 130 commits from feature/source-plugins into main 2026-11-04 07:35:21 +01:00
11 changed files with 205 additions and 88 deletions
Showing only changes of commit ce3cc97b91 - Show all commits

No files matched your search

+51 -25
View File
@@ -146,10 +146,21 @@ panel laid over the plot's right edge, opaque, with its own title, a *Close* but
page. The Trigger badge opens the trigger form; the Source badge (when shown) opens the source kind's own panels page. The Trigger badge opens the trigger form; the Source badge (when shown) opens the source kind's own panels
(the E-1608's *Analog out* and *Digital I/O*, behind a segmented switch at the top of the panel). It (the E-1608's *Analog out* and *Digital I/O*, behind a segmented switch at the top of the panel). It
closes with a second click on its badge, with `Esc`, with *Close*, or with a click on the plot; it never closes with a second click on its badge, with `Esc`, with *Close*, or with a click on the plot; it never
resizes the plot. The right-hand **dock** (the existing sidebar, 320 px) holds what is left of the old resizes the plot. The right-hand **dock** (the sidebar, **280 px**, fixed) holds the **readouts**: a
tabs in one scroller: the cursors and the measurements, shown from the start and insensitive until compact, read-only copy of the cursors table (a header row `A B delta`, then one line per cursor
the stream has signals (the signal list moved to the `signals` slide-over page). It is temporary -- see line) and of the measurements (one line each: the signal's colour chip, the type, the value). It is
"Review findings not (yet) implemented" for what the redesign still has to do with it. shown from the start and insensitive until the stream has signals. Each of the two blocks has an
**Edit** button, which opens the full panel as a slide-over page -- `cursors` or `measure` -- over the
plot; `m` toggles the `measure` page, and a click on a measurement's line in the dock opens that
measurement's dialog. The header's **Cursors** toggle only switches the cursors on and off (it
mirrors the switch inside the `cursors` page); with the cursors off the dock's cursor lines are
insensitive, not hidden. Both pages are insensitive until the stream has signals, like the dock. The dock's
cells are a fixed number of characters wide and ellipsized, so the dock's width and height do not
depend on what a value reads: the **who** cell of a cursor line is 5 characters (the full text is its
tooltip), a value cell 9 (its tooltip is the full text). The dock and the pages show **one string**:
`update_cursors_pane()` and `update_measures()` format each value once and hand that variable to the
page's label and to the dock's, so the two cannot disagree. The remaining part of the redesign is
listed under "Review findings not (yet) implemented".
Threading: one producer thread owns the data source (the UDPStreamer client is the built-in Threading: one producer thread owns the data source (the UDPStreamer client is the built-in
one) and calls `poll()`; the GTK main thread owns every widget and the GL contexts. one) and calls `poll()`; the GTK main thread owns every widget and the GL contexts.
@@ -1229,7 +1240,11 @@ decides which steps exist at all: a stop on a slide-over page is always kept, be
reveals it, while one whose *pane is collapsed* (the dock, in a window too narrow to put it beside the reveals it, while one whose *pane is collapsed* (the dock, in a window too narrow to put it beside the
plot) is dropped, because it is unmapped with its pane (a hidden page keeps its own *visible* flag plot) is dropped, because it is unmapped with its pane (a hidden page keeps its own *visible* flag
true -- the widget's own flag, which is all `is_visible()` walks -- so the old test walked over true -- the widget's own flag, which is all `is_visible()` walks -- so the old test walked over
surfaces that were not on screen); a narrow window therefore walks four steps. The chooser's surfaces that were not on screen); a narrow window therefore walks five steps. The dock's stop
describes the readouts and the **Edit** buttons; it names no slide-over page, so it closes the panel
like the plot's. The cursors and measurements pages have no stop of their own, and their contextual
help "(i)" hints are attached to the pages' headings the way the trigger's and the signals' are: they
are drawn while the page is open, and the dock has none. The chooser's
tour works the same way over its own two pages: its tabs step shows the *Waveforms* page and tour works the same way over its own two pages: its tabs step shows the *Waveforms* page and
stepping off it -- in either direction -- brings the *Sources* page back. stepping off it -- in either direction -- brings the *Sources* page back.
@@ -1631,9 +1646,10 @@ it holds more than the window the bound became the whole ring, and a zoom-out wa
view past the window into data the window was never set to show. A partly filled window view past the window into data the window was never set to show. A partly filled window
simply has an empty left-hand side. simply has an empty left-hand side.
**The panel switches are on the toolbar, and the panes own their own controls.** Cursors, **The panel switches are on the toolbar, and the panes own their own controls.** (Since the
Measurements and Math channels are icon toggles in the header bar rather than rows in the overflow slide-over redesign the toolbar keeps only the **Cursors** toggle of the three icons named here, and it
menu, and **Reset cursors** sits in the cursors pane beside the mode it resets (it acts on that only enables the cursors; the measurements are reached by `m` or the dock's Edit button, and the math
editor from the signal list.) **Reset cursors** sits in the cursors page beside the mode it resets (it acts on that
pane's instruments and is only visible while they exist). The rows read, left to right: **window control, trigger, pause** — the trigger and the pause are pane's instruments and is only visible while they exist). The rows read, left to right: **window control, trigger, pause** — the trigger and the pause are
read against the span on screen, so they sit with the control that sets it — then, on the right, read against the span on screen, so they sit with the control that sets it — then, on the right,
**cursors, measurements, math, sidebar** and **the menu last**. With `HeaderBar::pack_end()` the **cursors, measurements, math, sidebar** and **the menu last**. With `HeaderBar::pack_end()` the
@@ -2849,7 +2865,7 @@ reaches every widget). The full key table:
| `Space` | pause / resume | | `Space` | pause / resume |
| `t` | enable / disable trigger mode | | `t` | enable / disable trigger mode |
| `c` | enable / disable the cursors | | `c` | enable / disable the cursors |
| `m` | go to Measure (scroll the dock to the cursors and the measurements) | | `m` | open or close the Measurements page (the `measure` slide-over page; the dock's Edit button for the cursors opens the `cursors` page) |
| `g` | go to Signals (open or close the signal list, the `signals` slide-over page, also reached by the `+` badge) | | `g` | go to Signals (open or close the signal list, the `signals` slide-over page, also reached by the `+` badge) |
| `l` | show / hide the layout editor | | `l` | show / hide the layout editor |
| `s` | show / hide the dock | | `s` | show / hide the dock |
@@ -2970,9 +2986,10 @@ now — they are listed so they are not mistaken for anything else:
signal list -- in one scrolling column; with four of them open in a 320 px sidebar every signal list -- in one scrolling column; with four of them open in a 320 px sidebar every
section was short of room, and finding one meant scrolling past the others. The sidebar was then two section was short of room, and finding one meant scrolling past the others. The sidebar was then two
text tabs (a **Measure** tab and a **Signals** tab, with a source kind's own tabs beside them), and four text tabs (a **Measure** tab and a **Signals** tab, with a source kind's own tabs beside them), and four
tabs did not fit 320 px. It is **one dock** now, with no tab switcher: the cursors and the tabs did not fit 320 px. It is **one dock** now, with no tab switcher: the **readouts** (a compact,
measurements in one scroller, shown from the start and insensitive until the stream has signals; `m` read-only copy of the cursors and of the measurements, each block with an **Edit** button), shown from
scrolls it to the cursors; the **signal list** is the `signals` slide-over page (`g`, or the `+` badge). The **trigger form** and a **source's own the start and insensitive until the stream has signals; the full cursors and measurements panels are the
`cursors` and `measure` slide-over pages (`m` toggles `measure`); the **signal list** is the `signals` slide-over page (`g`, or the `+` badge). The **trigger form** and a **source's own
controls** are pages of the slide-over, reached from the badge bar. The trigger pane also has a **locked** mode, which is what a returning user gets: it controls** are pages of the slide-over, reached from the badge bar. The trigger pane also has a **locked** mode, which is what a returning user gets: it
shows one line saying what it is set to and offers no control that changes the configuration -- its shows one line saying what it is set to and offers no control that changes the configuration -- its
heading has an `[edit]` button to switch to the full pane, and back. The signals have no lock: the heading has an `[edit]` button to switch to the full pane, and back. The signals have no lock: the
@@ -2987,19 +3004,28 @@ now — they are listed so they are not mistaken for anything else:
toggle hides a pane any more: cursors-off **disables** its readout and leaves it in place, and toggle hides a pane any more: cursors-off **disables** its readout and leaves it in place, and
trigger-off leaves the trigger form fully editable -- a trigger is configured *before* it is trigger-off leaves the trigger form fully editable -- a trigger is configured *before* it is
switched on, and the locked view says `stream (trigger off)` instead of the settings -- and the measurements' and math panes' toolbar toggles are gone with the panes. switched on, and the locked view says `stream (trigger off)` instead of the settings -- and the measurements' and math panes' toolbar toggles are gone with the panes.
- **The badge bar / slide-over / dock redesign is in progress; this is its first step, and it deviates - **The badge bar / slide-over / dock redesign is in progress; steps 1-3 are done, and they deviate
from `docs/superpowers/specs/2026-11-02-ui-badges-dock-design.md` in three known ways.** from `docs/superpowers/specs/2026-11-02-ui-badges-dock-design.md` in these known ways.**
(1) The dock keeps the sidebar's **320 px**, not the spec's ~280: the panels it holds were built for (1) The session file gains the dock's *shown* state only, not a width -- the dock's width is
320, and the width changes when they become compact readouts (step 3). (2) The session file gains the fixed (**280 px**, as the spec says), so there is nothing to persist -- and does not store the open
dock's *shown* state only, not a width -- the dock's width is fixed, so there is nothing to persist slide-over page. (2) The **Trigger badge** shows `off`, `armed` and `triggered` as the spec says, and
-- and does not store the open slide-over page. (3) The **Trigger badge** shows `off`, `armed` and two more words, `capturing` (a post-trigger window is filling) and `disarmed` (trigger on, not
`triggered` as the spec says, and two more words, `capturing` (a post-trigger window is filling) and armed), so the label never claims a state the engine is not in. (3) The **`cursors` and `measure`
`disarmed` (trigger on, not armed), so the label never claims a state the engine is not in. The signal badges, the pages hold the existing panels unchanged** (the cursors table with its mode, its Reset and its
signal pages and the drag (step 2) are done. Still to switch; the measurement list with its `+`, statistics and per-row edit), so the pages carry the
come, in the spec's order: the compact readouts (3), the events panels' own controls, not a new form. (4) The **measurement editor stays a dialog**: the spec has the
section in the dock, the deletion of the left pane and the Events toggle, and `USER-GUIDE.md` and the measurement add/edit/remove inside the `measure` page, but the dialog (`open_measure_dialog()`) is
manual's figures (4). Until then the events pane, the header's Events toggle and the manual still what the `+`, a row's edit button and a click on a dock line all open; the page lists the measurements
describe the previous layout. and hosts the dialog's entry points. (5) A **dock cursor line carries the full who / A / B / delta
cells** (not one value per line as the spec sketches), in a 5 + 3 x 9 character fixed-width grid: the
**who** cell ellipsizes at **5 characters** -- the width the 280 px pin allows; with 8 the readouts'
minimum width was 304 px and widened the sidebar -- and its tooltip carries the full text. (6) The dock and
the pages **share one string** for every value (see `docs/claude/invariants-readouts-and-cursors.md`).
The signal badges, the signal pages and the drag (step 2) are done. Still to come, in the spec's
order: the events section in the dock, the deletion of the left pane and the Events toggle, and
`USER-GUIDE.md` and the manual's figures (4). Until then the events pane, the header's Events toggle
and the manual (which still describes the side panel's tabs, and the cursors and measurements as
panes of it) still describe the previous layout.
- **A plot holds at most eight signals** (`RowSet::kSlotsPerRow`), because a `Plot` - **A plot holds at most eight signals** (`RowSet::kSlotsPerRow`), because a `Plot`
cannot give up a trace and the slots are therefore created up front. cannot give up a trace and the slots are therefore created up front.
- **One toggle drives both cursor orientations.** The vertical markers and the - **One toggle drives both cursor orientations.** The vertical markers and the
+17 -5
View File
@@ -101,8 +101,11 @@ touching the data path.
a `slideover_add_page(app, id, title, widget)`: the widget is wrapped in a `GtkScrolledWindow` of its a `slideover_add_page(app, id, title, widget)`: the widget is wrapped in a `GtkScrolledWindow` of its
own and registered under its id (`trigger`, `source-<id>`, `signals` (the signal list) and own and registered under its id (`trigger`, `source-<id>`, `signals` (the signal list) and
`signal:<name>` (one signal's page, `src/ui_signal_page.cpp`, created on demand by `signal_page_ensure()` `signal:<name>` (one signal's page, `src/ui_signal_page.cpp`, created on demand by `signal_page_ensure()`
and pruned by `signal_pages_sync()`, which `rebuild_signal_panels()` ends with); `measure`, `cursors` and pruned by `signal_pages_sync()`, which `rebuild_signal_panels()` ends with), `cursors` (the cursors
and `events` are reserved for later steps). One page is open at a time and `App::slideover.page` panel, unchanged) and `measure` (the measurements panel, unchanged; `m` toggles it); `events` is reserved
for a later step). Both of those are registered whether or not they are open (a closed page's widgets
exist, unmapped), and both are insensitive until the stream has signals, by the same
`update_signal_availability()` call that sets the dock's sensitivity. One page is open at a time and `App::slideover.page`
is its id, empty when closed. The open page is **not** a session value. Esc, *Close*, a second click on the is its id, empty when closed. The open page is **not** a session value. Esc, *Close*, a second click on the
badge and a click on the plot close it; the click-away is a capture-phase observer (see badge and a click on the plot close it; the click-away is a capture-phase observer (see
[invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)). With two or more `source-*` pages, an [invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)). With two or more `source-*` pages, an
@@ -116,9 +119,18 @@ touching the data path.
file) leaves it **hidden** (the user asked for hide here, not the usual disable): `sync_badge_bar()` sets its visibility, file) leaves it **hidden** (the user asked for hide here, not the usual disable): `sync_badge_bar()` sets its visibility,
pick and focus from `slideover.source_pages` being non-empty every tick, so anything that indexes the badge pick and focus from `slideover.source_pages` being non-empty every tick, so anything that indexes the badge
(the tour's `badges` step targets the whole bar, `[badges]`) must tolerate it being absent. (the tour's `badges` step targets the whole bar, `[badges]`) must tolerate it being absent.
- **The dock** is the `AdwOverlaySplitView` sidebar (`side_box` > `dock_scroller` > `dock_page`): the - **The dock** is the `AdwOverlaySplitView` sidebar (`side_box` > `dock_scroller` > `dock_page`), **280 px**
cursors and the measurements only, in one scroller (the signal list is the `signals` page). It holds no tab switcher and no and fixed. It holds one thing, the **Readouts** box (`src/ui_dock.cpp`, `build_readouts_dock()`): a
revealer, and it does not hold a source kind's panels (those are slide-over pages). **Cursors** block (heading, an **Edit** button that opens the `cursors` page, a `GtkGrid` of a header row
`A B delta` and one line per page line) and a **Measurements** block (heading, **Edit** opening the
`measure` page, one line per measurement: chip, type, value; a click on a line opens that measurement's
dialog; no measurements shows one dim "no measurements" line). It is read-only and **owns no formatting**:
`update_cursors_pane()` and `update_measures()` hand the string they wrote to the page's label to
`dock_cursor_cell()` / `dock_measure_value()` as well, and `rebuild_cursors_pane()` /
`rebuild_measures_pane()` end by rebuilding the dock's lines to match (`dock_rebuild_cursors()`,
`dock_rebuild_measures()`). It holds no tab switcher, no revealer and none of the full panels, nor a source
kind's panels (those are slide-over pages). The window's measurement editor is a dialog
(`open_measure_dialog()`), not part of the `measure` page.
**When each is built and when it is read.** `build_slideover()` runs in `on_activate` right after the tour **When each is built and when it is read.** `build_slideover()` runs in `on_activate` right after the tour
overlay is built (the tour's overlay wraps the whole window, so its dim rectangles cover the slide-over overlay is built (the tour's overlay wraps the whole window, so its dim rectangles cover the slide-over
+24 -7
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 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* 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 (Fit, Export, Configurations) and the one editor that is not a pane (the layout editor): the signal list
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. is the `signals` slide-over page, 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 - **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 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 measurement ruler, and the stock names those buttons used to carry said something else
@@ -225,13 +225,15 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
take room has to be hidden and not merely unrevealed; the revealer keeps the slide while the pane 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. *is* open. The allocated width is how this was found -- the revealer's own measure said 0.
**The sidebar is one dock now, and has no `GtkRevealer` left in it**: a single `GtkScrolledWindow` **The sidebar is one dock now, and has no `GtkRevealer` left in it**: a single `GtkScrolledWindow`
(`dock_scroller`) holding the cursors and the measurements one under another. It (`dock_scroller`) holding only the compact **readouts** (`src/ui_dock.cpp`). It
replaced an `AdwViewSwitcher` over an `AdwViewStack`, whose four text tabs did not fit 320 px (and a source 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 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 this trap teaches: full panels competing for one narrow column each left every one of them short of room.
a scroller gives each its natural height. `m` scrolls the dock to the cursors (`dock_show_section()`); the signal list is The full cursors and measurements panels are now the `cursors` and `measure` **slide-over pages**, reached
not a dock section but the `signals` slide-over page, which `g` and the `+` badge open. A *feature's* on/off is no longer a visibility at all -- the by the readouts' **Edit** buttons, and `m` toggles `measure` (it no longer scrolls the dock;
**cursors'** toggle sets its pane's **body** insensitive -- the readout has nothing to show with the `dock_show_section()` is gone); the signal list is the `signals` slide-over page, which `g` and the `+` badge
open. The header's Cursors toggle no longer opens anything: it only enables the cursors. A *feature's* on/off is no longer a visibility at all -- the
**cursors'** toggle sets its pane's **body** insensitive (and the dock's cursor grid beside it) -- 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 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 (see the next bullet). The **trigger's body is deliberately never disabled**: a trigger is configured
*before* it is switched on, so switching it off to change it, or to read what it is set to, is the *before* it is switched on, so switching it off to change it, or to read what it is set to, is the
@@ -243,7 +245,10 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
- **The dock is shown but insensitive until the stream has signals ("disable, do not hide").** - **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 `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 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 sensitive throughout. The `cursors` and `measure` **pages follow the same condition** through
`update_signal_availability()` (called from `update_side_visibility()` and the per-frame block), so a
page opened by `m` before the stream has signals is insensitive, not refused. `[readouts] ... pages_sensitive=`
reads it. The wish is persisted (`dock = true|false` in the session file) and a change of it
schedules a save. `dock:` in the dump reads the result. schedules a save. `dock:` in the dump reads the result.
- **The signal badge is the drag handle, and a drop handler only moves widgets.** Dragging a signal onto a plot - **The signal badge is the drag handle, and a drop handler only moves widgets.** Dragging a signal onto a plot
starts on its badge in the badge bar (a `GtkDragSource` on the badge's button, payload the signal index as a starts on its badge in the badge bar (a `GtkDragSource` on the badge's button, payload the signal index as a
@@ -584,6 +589,18 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps
`ui_impl::hint_button()` reached either by the table's `context_help_sync()` or by `ui_impl::hint_button()` reached either by the table's `context_help_sync()` or by
`context_help_button()` for a window with no `App` (the source chooser) -- one button, two callers, `context_help_button()` for a window with no `App` (the source chooser) -- one button, two callers,
so the two windows cannot drift into two affordances. so the two windows cannot drift into two affordances.
- **A hint on a slide-over page's heading is attached all the time and drawn only while the page is open.**
The trigger's, signals', cursors' and measurements' headings (`trigger_head`, `signals_head`,
`cursors_head`, `measures_head`) live on pages that exist, unmapped, whether or not they are open, so
`context_help_sync()` attaches their "(i)" like any other and the count (`hints=12`) does not depend on
which page is up. There is **no second code path** and the dock's readouts carry no hint of their own: the
pages' hints say what the dock shows too (the cursors' and measurements' bodies mention the read-only copy
and the Edit button). A probe that must *see* one drawn needs its page open, like every other reading off a page.
- **The tour has one stop for the readouts and none for the pages behind them.** The dock's stop
(`side`, target `side_box`, no `page`, so it closes the panel) describes the readouts, the **Edit**
buttons and the click on a measurement; the `cursors` and `measure` pages are reached from there rather than
walked. A stop for either would be one more `TourStop::page` (see the next bullet), and the plan's
length is asserted (`steps=6`, `steps=5` with the dock put away) -- a new stop changes both.
- **The guided tour takes no grab, and that is the whole reason it is an overlay.** Four dim - **The guided tour takes no grab, and that is the whole reason it is an overlay.** Four dim
rectangles around the hole are ordinary widgets that swallow the clicks that land on them and rectangles around the hole are ordinary widgets that swallow the clicks that land on them and
nothing else; `Skip`, `Finish` and `Escape` hide them, and there is no popover, no gesture and no nothing else; `Skip`, `Finish` and `Escape` hide them, and there is no popover, no gesture and no
+13 -6
View File
@@ -12,9 +12,9 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
from the paned's own width, re-parenting the column, the property form of its children, size requests): from the paned's own width, re-parenting the column, the property form of its children, size requests):
the *mechanism* was the fault. It is an **`AdwOverlaySplitView`** now — what a utility sidebar is in the *mechanism* was the fault. It is an **`AdwOverlaySplitView`** now — what a utility sidebar is in
GNOME, as against `AdwNavigationSplitView`, which is for navigation lists — with `min-sidebar-width` = GNOME, as against `AdwNavigationSplitView`, which is for navigation lists — with `min-sidebar-width` =
`max-sidebar-width` = 320 in **pixels**, so the width is its own at any window size, and `collapsed` (set `max-sidebar-width` = 280 in **pixels**, so the width is its own at any window size, and `collapsed` (set
from the window's width in `tick()`) to *overlay* the plot when there is no room beside it rather than from the window's width in `tick()`) to *overlay* the plot when there is no room beside it rather than
being squeezed. `[sidebar-probe]` reads both states: `window=756 sidebar=320` under a 2x scale is exactly being squeezed. `[sidebar-probe]` reads both states: `window=756 sidebar=280` under a 2x scale is exactly
the case that used to be zero. the case that used to be zero.
- **The sidebar's button is the only input; `AdwOverlaySplitView:show-sidebar` is an output.** The split - **The sidebar's button is the only input; `AdwOverlaySplitView:show-sidebar` is an output.** The split
view writes that property *itself* — at startup, and when it collapses — with no user involved, so reading view writes that property *itself* — at startup, and when it collapses — with no user involved, so reading
@@ -29,7 +29,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
no-signals case must leave the wish alone, or a source connecting later finds a sidebar the user never closed. 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. 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 - **`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 `sidebar-width-unit` says.** Setting a pixel count there (280) 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 falls back to 0.25 — which reads as "the sidebar is mysteriously 560 px wide" on a wide window with no
error anywhere the build would catch. Pin the width with `min-sidebar-width`/`max-sidebar-width`. error anywhere the build would catch. Pin the width with `min-sidebar-width`/`max-sidebar-width`.
- **A `GtkPaned`'s position is stated after both its sides are filled**, and that is a fact about the - **A `GtkPaned`'s position is stated after both its sides are filled**, and that is a fact about the
@@ -130,9 +130,16 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
`bar_min_w` reads the same with one signal, nine signals and the overflow (244 on the E-1608, 124 on UDPS: `bar_min_w` reads the same with one signal, nine signals and the overflow (244 on the E-1608, 124 on UDPS:
the Source badge is on screen for the E-1608 and hidden for UDPS, and its text differs by kind). A change that makes it grow with the signal count has the Source badge is on screen for the E-1608 and hidden for UDPS, and its text differs by kind). A change that makes it grow with the signal count has
taken the scroller out of the measurement. taken the scroller out of the measurement.
- **The dock's width is fixed too** (`side_column` `width-request` 300, the split view's 320 px floor - **The dock's width is fixed too, and it is the readouts' width that holds it there** (`side_column`
and cap), and it holds one scroller with the cursors and the measurements. A `width-request` 260, the split view's 280 px floor and cap). The dock holds only the compact readouts, so a
section's visibility is never what makes the dock taller; the scroller is what absorbs the extra. section's visibility is never what makes it taller; the scroller absorbs the rest. **The readouts' minimum
width is the sum of their cells' character widths, and it must stay under 280** (the Readouts box is 268:
280 less its 6 px margins): a cursor line is the `who` cell (5 characters) plus three value cells (9 each),
every one ellipsized at that width (`width-chars` = `max-width-chars`), so a long value is cut and its
tooltip carries the text and the layout never depends on what a value reads. The `who` cell was 8
characters, which made the readouts' minimum width 304 px; that pushed the sidebar to 304 px
(`dock_w=304`) and defeated the 280 pin. Widening any cell is the same
trap -- read `[readouts] ... dock_w=` (must be 280) and the `dock cursors:` / `dock measures:` `fits=`.
- **Trace slots are identified by signal name, not channel index.** Enabling a signal - **Trace slots are identified by signal name, not channel index.** Enabling a signal
or a new CONFIG renumbers the channel list, so an index would silently re-point a or a new CONFIG renumbers the channel list, so an index would silently re-point a
trace at another signal. trace at another signal.
+17 -1
View File
@@ -41,6 +41,22 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
window's **first sample** and never from history before the window, so a measurement never depends on data window's **first sample** and never from history before the window, so a measurement never depends on data
outside the window it reports on: the left-edge step transient is removed by the seeding, not by priming outside the window it reports on: the left-edge step transient is removed by the seeding, not by priming
from earlier samples. from earlier samples.
- **The dock's readouts and the pages' panels are one string, never two.** The dock (`src/ui_dock.cpp`)
shows a read-only copy of the cursors table and of the measurement values, and it must not format
anything: `update_cursors_pane()` and `update_measures()` format each value **once**, into a local, and
hand that same variable to the page's label **and** to `dock_cursor_cell()` / `dock_measure_value()` --
on every branch, including the `"--"` blanking one (cursors off, no sample). A second formatter in the
dock would be two spellings of one reading that drift the first time `format_measure()` changes, and
the dock exists so that the two can be read side by side. The `who` cell is copied from the page's label
verbatim (markup chip and name), and the dock's lines are rebuilt from the page's whenever the page
rebuilds (`dock_rebuild_cursors()`, `dock_rebuild_measures()`), so the line counts agree by
construction. A cell is **ellipsized at a fixed character count** (who 5, values 9, measurement type 12) with
the full text in its tooltip: the dock is 280 px wide and its layout must not depend on what a value
reads, so a long value is cut and quoted whole in the tooltip rather than widening the column (the
`who` cell was 8 characters, which made the readouts 304 px wide and pushed the sidebar past its
pin). `[cursors-probe] ... dock cursors:` and `[measures-probe] ... dock measures:` assert it: `same_text=1`
compares `gtk_label_get_text()` (the untruncated text) of every dock label with the page's, and
`fits=1` that the Readouts box is no wider than the dock.
- **`format_measure()` is the one formatter for both panes, and its SI prefix does not depend on - **`format_measure()` is the one formatter for both panes, and its SI prefix does not depend on
there being a unit.** The cursors pane's values and the measurements pane's `text=` go through there being a unit.** The cursors pane's values and the measurements pane's `text=` go through
it, so a change reaches both. The prefix ladder used to be inside `if (!unit.empty())`, which it, so a change reaches both. The prefix ladder used to be inside `if (!unit.empty())`, which
@@ -55,7 +71,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
unit (`636.62 mV`). Below `1e-12` the ladder runs out, and `%.2f` would print `0.00` again, so unit (`636.62 mV`). Below `1e-12` the ladder runs out, and `%.2f` would print `0.00` again, so
that range falls back to `%.3g`. that range falls back to `%.3g`.
- **With the instruments off the readout blanks, and the panel stays on screen.** Cursors-off makes the - **With the instruments off the readout blanks, and the panel stays on screen.** Cursors-off makes the
pane *insensitive* rather than hiding it -- a panel you can still see is a panel you can read, and the pane *insensitive* (the page's body, and the dock's cursor grid beside it) rather than hiding it -- a panel you can still see is a panel you can read, and the
settings you are about to switch back on must not vanish while you look for them -- and every cell in settings you are about to switch back on must not vanish while you look for them -- and every cell in
it is written `"--"`, the spelling an unavailable cell already uses. A readout quotes a stored sample; it is written `"--"`, the spelling an unavailable cell already uses. A readout quotes a stored sample;
with nothing bracketing one there is no sample to quote, and keeping the last values would be quoting with nothing bracketing one there is no sample to quote, and keeping the last values would be quoting
+1 -1
View File
@@ -31,7 +31,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
toggle; written as a bare top-level boolean (`measure_stats = true/false`), defaults to `false` toggle; written as a bare top-level boolean (`measure_stats = true/false`), defaults to `false`
when absent, and **`dock`** -- `LayoutState::dock_shown`, the Dock button's state (the user's last word on it, when absent, and **`dock`** -- `LayoutState::dock_shown`, the Dock button's state (the user's last word on it,
`App::sidebar_wanted`): a bare top-level boolean written where `side_tab` used to be, default `true`. `App::sidebar_wanted`): a bare top-level boolean written where `side_tab` used to be, default `true`.
It is the **only** thing the dock persists -- its 320 px width is fixed, so there is nothing to store -- It is the **only** thing the dock persists -- its 280 px width is fixed, so there is nothing to store --
and **the open slide-over page is deliberately not persisted** (a panel that opens by itself at start-up and **the open slide-over page is deliberately not persisted** (a panel that opens by itself at start-up
is a surprise, and a page id from another source kind may not exist). The parser ignores unknown keys is a surprise, and a page id from another source kind may not exist). The parser ignores unknown keys
(its key chain has no erroring final `else`), so a file written before the tabs became a dock (its key chain has no erroring final `else`), so a file written before the tabs became a dock
+51 -24
View File
@@ -29,7 +29,7 @@ Two lines of the self-check output are assertions in disguise:
window slides over a round number. The companion `axes.w changes: N (last at frame F, window slides over a round number. The companion `axes.w changes: N (last at frame F,
side panel up=S)` line separates the two readings: `0`, or a handful near the start, side panel up=S)` line separates the two readings: `0`, or a handful near the start,
is a settled layout; a count near the frame count is the twitch. Runs far longer than is a settled layout; a count near the frame count is the twitch. Runs far longer than
the documented probes have shown one ~320 px step — the side panel's width — that the documented probes have shown one ~320 px step — the side panel's width, when it was 320 px — that
reverts; it is intermittent and unexplained, so read `changes` before calling a range reverts; it is intermittent and unexplained, so read `changes` before calling a range
the twitch bug. the twitch bug.
- `[trig-probe] ... trigger=on ... display=... epoch=...` (stderr, - `[trig-probe] ... trigger=on ... display=... epoch=...` (stderr,
@@ -425,8 +425,8 @@ Two lines of the self-check output are assertions in disguise:
— that is how it tells a gesture from its own echo — so a fit that wrote the group alone reads — that is how it tells a gesture from its own echo — so a fit that wrote the group alone reads
back as the zoomed range one frame later. back as the zoomed range one frame later.
- `[layout-probe] <action> rows=N buttons=B done=D done_box=[x,y,WxH] rows_y=[..] close=[..] add=[..] traces=[..]` - `[layout-probe] <action> rows=N buttons=B done=D done_box=[x,y,WxH] rows_y=[..] close=[..] add=[..] traces=[..]`
`side_w` is the sidebar column's width and **must be its own request (320), never 0**: it read 0 while `side_w` is the sidebar column's width and **must be its own request (280), never 0**: it read 0 while
the divider sat at the window's right edge, which is a panel that is not there. It was 320 during the the divider sat at the window's right edge, which is a panel that is not there. It was 280 during the
first frames and 0 once the window had settled, so read it from a `placed` line rather than the first. first frames and 0 once the window had settled, so read it from a `placed` line rather than the first.
(stderr, `UDPSCOPE_PROBE_LAYOUT_MS`) — **`buttons` must be `2N+1`** while the editor is (stderr, `UDPSCOPE_PROBE_LAYOUT_MS`) — **`buttons` must be `2N+1`** while the editor is
up (one `×` per row, one `+` per boundary) and **0 on the `editor off` line**: the up (one `×` per row, one `+` per boundary) and **0 on the `editor off` line**: the
@@ -479,16 +479,30 @@ 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 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 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. is the only check that it names something that exists.
- `dock: shown=1 sensitive=1 w=320 scroller_pages=1` (in the exit dump, beside `panels:`) -- the sidebar's dock - `dock: shown=1 sensitive=1 w=280 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 as the last tick found it. There is no tab switcher: the sidebar is one scroller (`dock_scroller`) holding the
one scroller (`dock_scroller`) holding the signal list, the cursors and the measurements. **`shown=1` **readouts box only** -- a compact, read-only copy of the cursors and of the measurements, each with an Edit
button (`build_readouts_dock()` in `src/ui_dock.cpp`). The signal list, the full cursors table, the measurements
and the trigger are slide-over pages. **`shown=1`
always** (the dock is on screen from the start; `shown=0` only when the user hid it with the Dock button, 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 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`; 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 `w=` is the sidebar's own width (**280**); **`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: 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 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. are slide-over pages (`source-<id>`), so the line reads the same on the E-1608 and the probe kind.
- `[readouts] pages cursors=1 measure=1 dock_children=1 dock_w=280 pages_sensitive=1 signals=1` (in the exit
dump; `dock_children` counts the direct children of the dock page) -- the two pages and the dock against the
stream. **`cursors=1 measure=1`**: both slide-over pages are registered, always and from the start (not when
the stream has signals); `0` is a page that was never added, and then the dock's Edit buttons and the `m` key
open nothing. **`dock_w=280`** is the sidebar's width, as for `dock:`. **`pages_sensitive` must equal
`signals`**: both pages are insensitive until the stream has signals ("disable, do not hide";
`update_signal_availability()` writes them with the dock), so a run against a port with no sender reads
`pages_sensitive=0 signals=0`, a normal run `pages_sensitive=1 signals=1`. `pages_sensitive=1` with `signals=0`
is a page that can be opened and read with nothing to read; `pages_sensitive=0` with `signals=1` is a
stream whose pages never came back after the signal list arrived. **`dock_children=1`**: the dock
page holds the readouts box and nothing else (a second child is a section that crept back into the dock; 0 is a
dock that was built empty).
- `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=6 step=0 auto=1 requested=0` (in the exit - `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=6 step=0 auto=1 requested=0` (in the exit
dump, beside `dock:`) — 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 the number of ids**, and the ids are the surfaces the contextual help is on: eleven of the window
@@ -988,7 +1002,7 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
as a placement bug, on the plot stop (the target is the whole content) and on the trigger stop at the as a placement bug, on the plot stop (the target is the whole content) and on the trigger stop at the
documented 756 px window (the panel is 360 px of it, so the card has no side wide enough and goes flush documented 756 px window (the panel is 360 px of it, so the card has no side wide enough and goes flush
left over the plot, overlapping the panel's edge by a few pixels), and on the `signals` stop for the same left over the plot, overlapping the panel's edge by a few pixels), and on the `signals` stop for the same
reason (the dock beside it is 320 px and the card has no side wide enough). reason (the dock beside it is 280 px and the card has no side wide enough).
- `[toursurface] finished: running=0 slideover=none` -- the last Next ended the walk and **the panel is - `[toursurface] finished: running=0 slideover=none` -- the last Next ended the walk and **the panel is
closed with it**. closed with it**.
- `[toursurface] restarted at trigger: running=1 slideover=trigger` then - `[toursurface] restarted at trigger: running=1 slideover=trigger` then
@@ -999,8 +1013,8 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
is how a probe puts the dock away for good): the dock's own stop is dropped, **and the trigger and is how a probe puts the dock away for good): the dock's own stop is dropped, **and the trigger and
signals stops are kept** because the slide-over does not depend on the dock -- five stops remain (plot, signals stops are kept** because the slide-over does not depend on the dock -- five stops remain (plot,
badges, trigger, signals, status). badges, trigger, signals, status).
- `[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: page=1 dock_still=1 closed=1 | … | esc: opened=1 handled=1 closed=1 idle_handled=0 | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** (`g` is not a pair: it opens the `signals` slide-over page and a second press closes it), **`refused` must be 1**, and the `esc:` reading must be `opened=1 handled=1 closed=1 idle_handled=0`. - `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | c: cursors: 0->1 | m: page=1 dock_still=1 closed=1 | g: page=1 dock_still=1 closed=1 | … | esc: opened=1 handled=1 closed=1 idle_handled=0 | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** (`m` and `g` are not pairs: each opens a slide-over page, `measure` and `signals`, and a second press closes it), **`refused` must be 1**, and the `esc:` reading must be `opened=1 handled=1 closed=1 idle_handled=0`.
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 read `page=1 dock_still=1 closed=1`** (the `signals` page opened, the dock's scroll untouched, a second press closing it), which is what says `m` scrolls to a section and `g` opens the list's page 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 `esc:` reading goes through `on_app_key_pressed()` with `GDK_KEY_Escape`**, the handler the window's key controller calls, and not through `slideover_escape()`: the probe opens the `trigger` slide-over page (`opened=1`), sends the key (`handled=1`: it was consumed, and `closed=1`: the page is gone), then sends it again with nothing open (`idle_handled=0`: Escape is left alone when there is nothing for it to close, so a dialog or popover under it still gets it). `handled=0` or `closed=0` is the slide-over's Escape never reaching the panel from the window's key path -- a handler that returns before the slide-over test, or a controller in the wrong phase -- which the `[slideover] esc_closes=1` reading (a direct call to `slideover_escape()`) cannot see. **Limit of the `m`/`g` readings:** the padding is 3000 px at the end of the page, so `reach` is non-zero on any window shorter than that and the key has somewhere to scroll, but it does not push the cursors panel out of the viewport -- on a window whose dock viewport is taller than `cursors_y` (a tall window) the section is already in view at offset 0 and `m` still scrolls to it; `at_cursors=1` is the key landing the section's top at the viewport's top, which is the reading, not "the section was hidden before". 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). The `m`/`g` readings are `page=P dock_still=D closed=C`: **all three must be 1 for both keys** -- `P` the page the key opens (`measure` for `m`, `signals` for `g`) is the slide-over's open page after the first press, `D` the dock scroller's vertical adjustment did not move (the dock holds only the readouts, so a key must not scroll it), and `C` a second press closes the page. This is what says `m` opens the measurements' page rather than scrolling the dock to a section (the old `m: dock 0->N (cursors_y=… at_cursors=1)` reading is gone with the dock sections), and that neither key toggles a pane. `m` does *not* turn the measurements on or off: the header Cursors toggle only enables cursors, and `m` only opens the page. `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 `esc:` reading goes through `on_app_key_pressed()` with `GDK_KEY_Escape`**, the handler the window's key controller calls, and not through `slideover_escape()`: the probe opens the `trigger` slide-over page (`opened=1`), sends the key (`handled=1`: it was consumed, and `closed=1`: the page is gone), then sends it again with nothing open (`idle_handled=0`: Escape is left alone when there is nothing for it to close, so a dialog or popover under it still gets it). `handled=0` or `closed=0` is the slide-over's Escape never reaching the panel from the window's key path -- a handler that returns before the slide-over test, or a controller in the wrong phase -- which the `[slideover] esc_closes=1` reading (a direct call to `slideover_escape()`) cannot see. 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. - `[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 - `[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` gaps_refused=N open_holes=N bands=N bands_repaired=N nan_slots=N scanned=N trigger_in_hole=D`
@@ -1791,11 +1805,26 @@ disagree. The two lines are the claim, in both directions:
- `trigger panel sensitive …=0` is the *panel body*, not the panel: the heading row, with its switch, - `trigger panel sensitive …=0` is the *panel body*, not the panel: the heading row, with its switch,
stays live. `1` here while trigger mode is off is the pane not being disabled at all. stays live. `1` here while trigger mode is off is the pane not being disabled at all.
## `dock cursors:` and `dock measures:`
```
[cursors-probe] <what> dock cursors: lines=1 page_lines=1 same_text=1 dock_w=268 side_w=280 fits=1 dock_sensitive=1
[measures-probe] <what> dock measures: lines=13 page_rows=13 same_text=1 dock_w=268 side_w=280 fits=1
```
These ride on the `[cursors-probe]` readings (every state line gets a `dock cursors:` beside it) and on the `[measures-probe]` readings (`added`, `settled`, `final`), and read the dock's compact copy against the page it copies. The dock never formats a value: `update_cursors_pane()` and `update_measures()` hand the same string to the page label and the dock label.
- **`same_text=1` is the assertion**: every dock label (the cursors' who, A, B, delta cells; the measurements' values) reads the same text as the page's label in the same position. `same_text=0` is a second formatter, or a value written to one view only; the two panes must never disagree. The probe compares `gtk_label_get_text()`, so the who cell compares as the text a user reads, markup stripped.
- **`lines` must equal `page_lines` / `page_rows`**: one dock line per page line. A shorter dock is a rebuild that was not mirrored (`dock_rebuild_cursors()` / `dock_rebuild_measures()` run from the pages' rebuilds); `same_text` reads 0 with it, so this names the cause.
- **`fits=1`** (`dock_w <= side_w`, `side_w > 0`): the readouts box fits inside the sidebar scroller. `dock_w` reads **268** inside the 280 px sidebar (the box's own margins and the fixed label widths: who 5 characters, each value 9, a measurement's type 12). **A who cell at 8 characters made the box 304 px and `fits=0`**, which is the defeat of the pin: the fixed-width ellipsis is what makes the dock's layout independent of what a value reads, so a cell given more characters widens the dock for every run, not only for a long name. `side_w=-1` or `dock_w=-1` is a probe that ran before the dock was built.
- **`dock_sensitive=1`** (cursors only): the dock's cursor grid is live whenever the stream has signals; `0` before them ("disable, do not hide"), `0` with signals is the dock never coming back.
- On a session with no cursors line, or with no measurements, `lines=0 page_lines=0 same_text=1` is correct: the dock then shows `no measurements` for the measurements (one non-value label, not counted in `lines`).
## `[sidebar-probe]` ## `[sidebar-probe]`
``` ```
[sidebar-probe] beside window=756 sidebar=320 collapsed=1 shown=1 button=1(sens=1) [sidebar-probe] beside window=756 sidebar=280 collapsed=1 shown=1 button=1(sens=1)
[sidebar-probe] regained window=756 sidebar=320 collapsed=1 shown=1 button=1(sens=1) [sidebar-probe] regained window=756 sidebar=280 collapsed=1 shown=1 button=1(sens=1)
``` ```
`UDPSCOPE_PROBE_SIDEBAR_MS` reads the sidebar in the two states a window can put it in, and against a hide `UDPSCOPE_PROBE_SIDEBAR_MS` reads the sidebar in the two states a window can put it in, and against a hide
@@ -1806,7 +1835,7 @@ and `regained` reads 400 ms later.
- **`sidebar` must never be 0.** It was, for every window narrower than the 940 px the divider used to sit - **`sidebar` must never be 0.** It was, for every window narrower than the 940 px the divider used to sit
at, and the pane could only be found by dragging the divider back — the report this reading exists for. at, and the pane could only be found by dragging the divider back — the report this reading exists for.
It must read its own width (320) in *both* states: `collapsed` changes where the sidebar is, never how It must read its own width (280) in *both* states: `collapsed` changes where the sidebar is, never how
wide it is. wide it is.
- **`shown` and `button` must agree, always.** They are the panel and the toolbar button, and a button that - **`shown` and `button` must agree, always.** They are the panel and the toolbar button, and a button that
says "shown" over a hidden panel is the report this reading exists for. `shown=1 button=1` on `regained` says "shown" over a hidden panel is the report this reading exists for. `shown=1 button=1` on `regained`
@@ -1822,24 +1851,22 @@ and `regained` reads 400 ms later.
## `[side-tab-probe]` ## `[side-tab-probe]`
``` ```
[side-tab-probe] section=cursors accums=4 baseline recorded (measure at scroll 2732 of 2732, outside_at_top=1) [side-tab-probe] section=none accums=4 baseline recorded (measure page was_open=1, now closed=1)
[side-tab-probe] section=cursors rows=4 accums=4 advanced=4 min_n=18 [side-tab-probe] section=none rows=4 accums=4 advanced=4 min_n=18
``` ```
`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 `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 measurements' page is closed**. The measurements are a slide-over page (`measure`), not a dock section, so the old scroll-the-dock set-up is gone. The probe opens the page, forces one pass so there is a baseline, *closes* the page (its widgets stay in the tree, unmapped) and reads again two seconds later. It re-arms once after 400 ms so the first reading has the layout. **Run it against a config that has seen the first-run tour**, which would close the page it opens (`tour_seen = true`).
while the Measure section is out of view**. It scrolls the dock *to* the measurements (`dock_show_section("measure")` lands on the cursors panel, the dock's top, so the probe then moves to the measurements' heading itself), forces one pass so there is a baseline, then scrolls back to the top (the cursors) and reads again two seconds later. To make the scroll observable the probe pads `dock_page` **and the cursors panel** to 3000 px each (probe only, both undone in the report), and it re-arms once after 400 ms so the first reading has the layout.
- **`outside_at_top=1` is the precondition**: with the dock at offset 0, the Measure section (the measurements' heading, below the padded cursors panel) starts at or below the viewport's bottom edge, so the "away from the section" state really has the measurements out of view and `advanced=4` says something. `outside_at_top=0` means the probe's own set-up failed and the run proves nothing -- unpadded, the section's top sits a short way down and the dock's viewport is taller than that. **The limit is the 3000 px:** the guarantee holds for a dock viewport shorter than the padding (any real window); a viewport taller than the cursors panel's padding, about 3000 logical px, would read `0` again. - **`was_open=1 closed=1` is the precondition**: the page really was open for the baseline and really is closed for the report, so `advanced=4` says something. `was_open=0` is the page never opening (the tour closed it, or the page is insensitive because the stream has no signals) and `closed=0` is `slideover_close()` not taking effect; either way the run proves nothing. `section=` is the slide-over's open page and **reads `none` on both lines**, because the page is closed.
- **`advanced=4` is the assertion**: every accumulator gained passes while the page was closed, 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 `min_n` (the smallest `n` seen) must be non-zero too. **`advanced=0` is the defect**: something gated the
accumulation on the visible section, so the readout would gain a gap in its min/max/avg for exactly as long as accumulation on the page being open, 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 the page was shut. A `Gtk-CRITICAL` here is the other half of the same rule -- a label or an
allocation read on the unrealized page. allocation read on the unmapped page.
- The probe enables a signal **and assigns it to a plot** itself, both deliberately. A signal starts disabled - The probe enables a signal **and assigns it to a plot** itself, both deliberately. A signal starts disabled
(enabling is what starts acquisition), and a channel with no trace leaves the live window *still* -- and a (enabling is what starts acquisition), and a channel with no trace leaves the live window *still* -- and a
still window is the state `update_measures()` is *supposed* to skip, so a probe that left it that way would still window is the state `update_measures()` is *supposed* to skip, so a probe that left it that way would
read `advanced=0` for a reason that has nothing to do with the tab. read `advanced=0` for a reason that has nothing to do with the page.
- `rows` is the measurement count and `accums` the accumulator count; the two must agree, because one - `rows` is the measurement count and `accums` the accumulator count; the two must agree, because one
accumulator per measurement row is what makes `advanced` a statement about every row. accumulator per measurement row is what makes `advanced` a statement about every row.
+19 -10
View File
@@ -423,15 +423,24 @@ precedent, not an exception.
pauses, reports again with `dn=` per measurement, then resets and reports a third time. Three 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 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_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 in the - `UDPSCOPE_PROBE_SIDE_TAB_MS=3000` with `UDPSCOPE_EXIT_AFTER_MS=11000` — the measurements while the
dock's Measure section, a forced pass, then the dock scrolled back to its top (the `measure` page is **closed**: it adds four measurements, opens the page for a forced pass (the baseline),
measurements out of view) and read again two seconds later. The probe pads the dock page and the cursors panel to closes it, and reads the accumulators again two seconds later. **`advanced=<rows>` is the assertion**
3000 px each so the Measure section really starts below the viewport (`outside_at_top=1`) on any real window. **`advanced=4` (every accumulator gained passes while the page was off screen); `advanced=0` is the defect where the
is the assertion** (every accumulator gained passes while its section was out of view); `advanced=0` is the accumulation is gated on what is visible, which would leave a gap in min/max/avg for exactly as long as the
defect where the accumulation is gated on the visible section, which would leave a gap in min/max/avg for page was shut. It enables *and plots* its own signal, so it needs no other probe ahead of it, and it
exactly as long as the trigger was on screen. It enables *and plots* its own signal, so it needs no other re-arms once after 400 ms (a buffer rebuild between baseline and report reads as a gated pass). It needs a
probe ahead of it. The reading's own entry is in `probe-readings.md`. (Probe name `dock_section_probe`; the config that has seen the first-run tour, which would close the page it opens (see the next-but-one bullet).
variable and the `[side-tab-probe]` tag keep their old names so existing scripts keep working.) 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. The measurements used to be a
dock section the probe scrolled out of view, with padding; the dock now holds only readouts, so there is
no scroll and no padding.)
- `UDPSCOPE_PROBE_CURSORS_MS` and `UDPSCOPE_PROBE_MEASURES_MS` also print the dock's copy of what they read:
a `dock cursors:` line after every `[cursors-probe]` report and a `dock measures:` line at the
measures probe's added / settled / final stops. **`same_text=1` on every one** is the assertion that the
dock and the page show one string; `fits=1` that the Readouts box is no wider than the dock. Entries in
`probe-readings.md`. The exit chain also prints one `[readouts]` line (pages registered, the dock's one
child, the dock's width, the pages' sensitivity, whether the stream has signals) on every exit-dump run.
- `UDPSCOPE_PROBE_DOCK_SESSION_MS=2500` with `UDPSCOPE_EXIT_AFTER_MS=8000` -- the session file's `dock` key, - `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 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`; (`side_tab = "measure"`, no `dock`) loaded without an error. Three `[dock-session]` lines, each `ok=1`;
@@ -447,7 +456,7 @@ precedent, not an exception.
read `click_inside_closes=0 still_open=1 click_outside_closes=1 esc_closes=1 closed=1`. For a screenshot read `click_inside_closes=0 still_open=1 click_outside_closes=1 esc_closes=1 closed=1`. For a screenshot
add `UDPSCOPE_PROBE_SLIDEOVER_HOLD=1`, which leaves the page open after line 1 (run with `GSK_RENDERER=cairo` add `UDPSCOPE_PROBE_SLIDEOVER_HOLD=1`, which leaves the page open after line 1 (run with `GSK_RENDERER=cairo`
and `import -window root`): the panel must be drawn over the traces, opaque. and `import -window root`): the panel must be drawn over the traces, opaque.
- **Slide-over and page probes (`SLIDEOVER`, `BADGES`, `SIGNAL_PAGES`, `SIGNAL_BADGES`) need a config that has already seen - **Slide-over and page probes (`SLIDEOVER`, `BADGES`, `SIGNAL_PAGES`, `SIGNAL_BADGES`, `SIDE_TAB`, `KEYS`) need a config that has already seen
the first-run tour**: a fresh `XDG_CONFIG_HOME` auto-starts the tour at about 1.5 s, and a tour stop with no page closes the the first-run tour**: a fresh `XDG_CONFIG_HOME` auto-starts the tour at about 1.5 s, and a tour stop with no page closes the
slide-over under the probe. Seed it with `mkdir -p $D/udpscope && printf 'tour_seen = true\n' > $D/udpscope/settings.toml` slide-over under the probe. Seed it with `mkdir -p $D/udpscope && printf 'tour_seen = true\n' > $D/udpscope/settings.toml`
(or one short throw-away run, which writes the key when the tour starts) and run with `XDG_CONFIG_HOME=$D`. Delete the (or one short throw-away run, which writes the key when the tour starts) and run with `XDG_CONFIG_HOME=$D`. Delete the
+3 -2
View File
@@ -25,8 +25,9 @@
<p>Press the cursors button in the toolbar, or <key>c</key>.</p> <p>Press the cursors button in the toolbar, or <key>c</key>.</p>
</item> </item>
<item> <item>
<p>The pane below the plot shows the readings and owns a <gui>Reset</gui> button. <p>The readings are in the side panel (a read-only copy) and in the cursors panel, which
Drag a marker to move it.</p> <gui>Edit</gui> beside <gui>Cursors</gui> opens over the plot and which owns a
<gui>Reset</gui> button. Drag a marker to move it.</p>
</item> </item>
<item> <item>
<p>The mode dropdown offers four modes: <gui>vertical</gui> (the shared time pair), <p>The mode dropdown offers four modes: <gui>vertical</gui> (the shared time pair),
+2 -2
View File
@@ -28,10 +28,10 @@
<title>How to reach it</title> <title>How to reach it</title>
<steps> <steps>
<item> <item>
<p>Press the measurements button in the toolbar, or <key>m</key>.</p> <p>Press <key>m</key>, or <gui>Edit</gui> beside <gui>Measurements</gui> in the side panel.</p>
</item> </item>
<item> <item>
<p>The pane's <gui>+</gui> adds one; clicking a row opens the dialog where the signal, <p>The panel's <gui>+</gui> adds one; clicking a row opens the dialog where the signal,
the type, the threshold share or shares and the on-plot marks are set, with the type, the threshold share or shares and the on-plot marks are set, with
<gui>Save</gui> and <gui>Delete</gui>.</p> <gui>Save</gui> and <gui>Delete</gui>.</p>
</item> </item>
+7 -5
View File
@@ -88,11 +88,13 @@
</item> </item>
<item> <item>
<title>The side panel</title> <title>The side panel</title>
<p>Two tabs. <gui>Measure</gui> holds the cursors readout and the measurements <p>A read-only copy of the cursors and of the measurements; the <gui>Edit</gui> button
derived from it; <gui>Signals</gui> holds the trigger and the channel list, where a beside each heading opens that instrument's full panel over the plot. The channel
math channel is a row of that one list under a <gui>MATH</gui> heading whose list and the trigger are panels of their own, opened from the badge bar and the
<gui>+</gui> creates one. One row per channel: its colour, an enable switch, its name toolbar, where a math channel is a row of the one list under a <gui>MATH</gui>
and its unit. A channel's badge in the badge bar is dragged onto a plot row.</p> heading whose <gui>+</gui> creates one. One row per channel: its colour, an enable
switch, its name and its unit. A channel's badge in the badge bar is dragged onto a
plot row.</p>
</item> </item>
<item> <item>
<title>The plot</title> <title>The plot</title>