From 6492bb910f92410284140eed9750d19a447c6003 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Wed, 14 Oct 2026 12:56:43 +0200 Subject: [PATCH] test: a probe reads the hidden tab's accumulator, and the docs follow the tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `UDPSCOPE_PROBE_SIDE_TAB_MS` is the one reading for the design's §5 rule -- the statistics accumulator keeps running on the hidden page -- which nothing tested before. It switches to Measure, forces one pass so there is a baseline, switches away to Signals and reads again two seconds later: `advanced=4` is the assertion, and `advanced=0` is the defect where the accumulation is gated on the visible tab, which would leave a gap in min/max/avg for exactly as long as the trigger was on screen. Two things the probe has to do for itself, and getting either wrong makes `advanced=0` a lie about the tab: - it **enables its own signal**, because a signal starts disabled (enabling is what starts acquisition) and a probe that waited for one to be on would be reading another probe's side effects -- and `UDPSCOPE_ENABLE_ALL_MS` is not part of this run; - it **assigns that channel to a plot**, because a channel with no trace leaves the live window *still*, and a still window is precisely the state `update_measures()` is supposed to skip. Measured the hard way: without the assignment the probe read `advanced=0 min_n=0` with the view pinned and only four passes in twelve seconds, none of them the tab's doing. Docs: `probe-readings.md` gains the probe's entry and the signals panel's new shape (`rows == signals + 1 + math`, `divider=0` a list that lost its `+`), loses the whole `math panel:` entry, and follows `pane_sensitive=`, the gone `revealed=` field, the six toolbar controls and the tab-reading `m`/`g` key pairs. `verification.md` gains the probe's bullet and the re-measured warning figures. The invariants record the sidebar as a switcher above a stack with a scroller per page, the hidden-page trap that replaces the unrevealed-revealer one for it, that a feature's on/off is sensitivity rather than visibility, that a math channel is a row of the signal list, and that a cursors-off readout blanks to `--` with the panel left on screen. `shortcuts.ui` and the help page's window and sidebar bullets stop describing five revealers. The README records the deviation from `SPECS.md` that names the panes as separate collapsible sections. Verified: `[side-tab-probe] page=signals rows=4 accums=4 advanced=4 min_n=16`; `signals panel: labels=2 list=1 rows=9 signals=4 math=4 divider=1`; `panels: … (page_current=1) (sensitive=0) switcher=1`; `toolbar icons:` six controls with every name `(ok)`, with the Preferences dialog opened in the same run; zero `Gtk-CRITICAL` and no missing icon. --- README.md | 10 +++ docs/claude/invariants-gtk-and-chrome.md | 24 +++++- docs/claude/invariants-math-channels.md | 11 +++ .../claude/invariants-readouts-and-cursors.md | 6 ++ docs/claude/probe-readings.md | 69 +++++++++------ docs/claude/verification.md | 14 +++- src/help/01-overview.txt | 13 +-- src/ui/shortcuts.ui | 8 +- src/ui_probe.cpp | 83 +++++++++++++++++++ 9 files changed, 197 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 74ddaa6..f7d6813 100644 --- a/README.md +++ b/README.md @@ -2388,6 +2388,16 @@ now — they are listed so they are not mistaken for anything else: progress. What the spec was protecting still holds — a *held* event is bound to its snapshot alone, and the axis is the event's own window from the edge onwards rather than a sliding live view. +- **The sidebar is two tabs, and a math channel is a row of the signal list.** `SPECS.md` + names the panes as separate collapsible sections — trigger, cursors, measurements, math, + 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 built + sidebar is a **Measure** tab (the cursors readout and the measurements) and a **Signals** + tab (the trigger and the channels), with the math list folded into the signal list under a + `MATH` divider whose `+` creates a channel — a derived channel already *is* a channel in + the model, so the two lists were the same list drawn twice. A tab is one-of-*N*, so no + toggle hides a pane any more: trigger-off and cursors-off **disable** their pane and leave + it in place, and the measurements' and math panes' toolbar toggles are gone with the panes. - **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. - **One toggle drives both cursor orientations.** The vertical markers and the diff --git a/docs/claude/invariants-gtk-and-chrome.md b/docs/claude/invariants-gtk-and-chrome.md index 9f86eee..803d507 100644 --- a/docs/claude/invariants-gtk-and-chrome.md +++ b/docs/claude/invariants-gtk-and-chrome.md @@ -49,13 +49,17 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear one of them with reality): it is computed on the first *tick*, because the dump 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) and the two editors that are not panes (Layout, Signal list). The Arm/Disarm button in the trigger panel still has text. + (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. - **Seven 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 entirely: a *record dot* for the trigger and three different **magnifiers** for cursors, - measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,math,events}-symbolic.svg` - are the five action icons; beside them in `src/icons/scalable/apps/` are the two + measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,events,resize-rows}-symbolic.svg` + are the five action icons. `udpscope-math-symbolic` left with the toolbar toggle that was its last + user; `udpscope-measure-symbolic` **stayed**, because the Preferences window's Plot page carries it -- + an icon with a live user is not an orphan, whatever the toggle that introduced it did. Beside them in + `src/icons/scalable/apps/` are the two **application** icons, `org.udpscope.app.svg` (128px colour artwork) and `org.udpscope.app-symbolic.svg` (16px). The `apps` context is where an icon theme keeps an application's own icon and is where the **in-process** window icon resolves from; it is also @@ -63,7 +67,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear into `$XDG_DATA_HOME/icons/hicolor/scalable/apps` and `.../symbolic/apps` — those installed copies are what makes `Icon=org.udpscope.app` in the desktop entry resolve (a desktop shell cannot read a GResource compiled into the binary). - All six are bundled by `src/icons/udpscope.gresource.xml` (CMake runs + All seven are bundled by `src/icons/udpscope.gresource.xml` (CMake runs `glib-compile-resources`) and reached through **one** `gtk_icon_theme_add_resource_path()` in `run_ui()`, right after `adw_init()` — not at the top of `build_toolbar()`, where it used to be: there are two consumers now (the window's own icon, @@ -179,6 +183,18 @@ 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 + trigger's and the cursors' toggles set their panel's **sensitivity**, so the panel stays where it + is and the settings you are about to switch back on do not vanish while you look for them. 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. - **A list box outlives the children of the pane it sits in, and the teardown order is not yours to choose.** Tearing the window down finalizes the events pane's buttons, then the list box, and the list's own `selected-rows-changed` then reaches a handler holding the freed pointers: four diff --git a/docs/claude/invariants-math-channels.md b/docs/claude/invariants-math-channels.md index 8b0778c..f8c8794 100644 --- a/docs/claude/invariants-math-channels.md +++ b/docs/claude/invariants-math-channels.md @@ -301,6 +301,17 @@ with at most the transient a re-start costs. The exit dump's `math '': fin the assertion: `finite` must be about `M` minus the hole share, and `finite=0` beside non-zero `hole_slots` is this defect returned. +- **A math channel is a row of the *signal* list, under a `MATH` divider, and there is no math pane.** + `append_math_signals_locked()` already puts every channel in `signals()`, so the pane was the same + list drawn twice; the divider is what the pane uniquely contributed. It is non-selectable and + non-activatable -- a heading that highlighted under the pointer would read as a row you can plot -- + and it carries the `+` that creates a channel, appended even when there are none, because a `+` that + waited for a channel to exist could never create the first one. A math row's menu is *Edit graph…*, + *Default colour*, *Delete*: no Information and no Calibrate, because calibration is applied at + ingest and a derived channel has no stream to describe, and no rename, because the node editor owns + a channel's name and unit and a second place to change them would be a second answer to the same + question. A row's tick is the channel's own switch and must reach `App::math` -- the copy the + session file is written from -- or the list and the file disagree about what "enabled" was. - **A graph *edit* does not move the plan; adding or removing a channel does.** The plan is a partition over the channels, so `set_math_channels()` takes a surgical path whenever the *set* is unchanged -- same channels, names and enables: the store is never touched, every other channel keeps its samples **and** its runtimes, and diff --git a/docs/claude/invariants-readouts-and-cursors.md b/docs/claude/invariants-readouts-and-cursors.md index 1ea179e..45a9e9c 100644 --- a/docs/claude/invariants-readouts-and-cursors.md +++ b/docs/claude/invariants-readouts-and-cursors.md @@ -54,6 +54,12 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear unit and a spaced `m` alone reads as metres; with one it stays spaced and the prefix joins the 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`. +- **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 + 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; + with nothing bracketing one there is no sample to quote, and keeping the last values would be quoting + a sample the cursors no longer name. Two spellings of one state in one grid would be the defect. - **The cursors are read back from the plots only while a gesture is on that row.** `Cursor::x` is the *free* pointer position while a drag runs and the snapped value otherwise -- which is what the app itself wrote a frame earlier, or the snap of it. A tick that reads a row on diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 01224ec..7d9fafc 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -161,10 +161,12 @@ Two lines of the self-check output are assertions in disguise: *adoption* half -- a gesture that does move a cursor, and the shared value reaching every row -- is asserted in the `check_host` harness, which can call `sync_cursors(row)` directly; a synthetic press here cannot reach the adapter's own event pump.) -- `[cursors-probe] mode=.. on=D pane=D t0=.. t1=.. dt=.. T=[..] lines=N [..]` +- `[cursors-probe] mode=.. on=D pane_sensitive=D t0=.. t1=.. dt=.. T=[..] lines=N [..]` (stderr, `UDPSCOPE_PROBE_CURSORS_MS`) — walks `vertical` → `horizontal` → `both` → `mix`, then moves a marker *through the plot* (`Plot::set_cursor_x(..., Free)`, which is - what a drag does) and reports again. **The times must survive every mode change**: they + what a drag does) and reports again. `pane_sensitive` is the cursors *panel's* sensitivity, not a + revealer's child: with the cursors off the panel stays exactly where it is and its cells blank to `--`, + because a readout whose instruments are off has no sample to quote. **The times must survive every mode change**: they are the `RowSet`'s one pair, and a mode that parks the markers must not read them back (a parked cursor reports a real number, so a read-back would adopt it as a gesture — the bug that made `T0` read 0). **`lines`** must be 0 for `vertical`, one per row for @@ -195,7 +197,7 @@ Two lines of the self-check output are assertions in disguise: numbers and the plot's state agreeing is the difference between "the app placed them" and "the app placed them and they are on screen". - `[measures-probe]` + the dump's `measure N: 'sig' type thr=..% above=D show=D -> ok|invalid value text=[..] - [duty=..] [marks=..]` and `measures pane: n=N revealed=D marks_shown=M pane_rows=R pane_w=W + [duty=..] [marks=..]` and `measures pane: n=N marks_shown=M pane_rows=R pane_w=W pane_h=H switch_on=X reset_sensitive=Y` (stderr, `UDPSCOPE_PROBE_MEASURES_MS`) — adds one measurement of every type on the first plottable signal and lets the tick compute them, so what it prints is what a user reads. @@ -345,9 +347,9 @@ Two lines of the self-check output are assertions in disguise: and an expanding one is measured into the title's centre box, so it stretched from the label to the title (516 px at a 1270 px bar before the fix, 121 px after). A `W` that grows with the window width is that bug back. -- `toolbar icons: trigger=NAME(ok) pause=... cursors=... measures=... math=... sidebar=... - menu=...` (in the exit dump, beside `panel controls:`) — the icon name every toolbar button - carries, with `gtk_icon_theme_has_icon()`'s verdict. **Every entry must read `(ok)`.** Four +- `toolbar icons: trigger=NAME(ok) events=... pause=... cursors=... sidebar=... menu=...` (in the + exit dump, beside `panel controls:`) — the icon name every toolbar button + carries, with `gtk_icon_theme_has_icon()`'s verdict. **Every entry must read `(ok)`.** Three of the names are this app's own (`udpscope-trigger-symbolic` and friends, a `GResource` compiled into the binary — see the toolbar-icon invariant in [invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)), and a `(MISSING)` on one of @@ -598,7 +600,7 @@ two also catch a row whose accelerator went missing. `settings=` must be a real every row then reports `h=0`. It turns autohide off for its own measurement — a popover dismisses itself when it loses the grab, and `xvfb-run` has no window manager to hand it one — and restores it afterwards. -- `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | … | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** and **`refused` must be 1**. 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 `: : ->` — 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). +- `[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 `: : ->` — 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] '' 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 `` markup, not by asking `RowSet::signal_color()` twice, which would pass against a chip that never reached the trace. And the override is `#123456`, deliberately in no palette slot: an override equal to the entry it replaces passes against an implementation that ignores overrides entirely. The session leg round-trips `collect_layout()` → `save_layout()` → `load_layout()` on a temp path and deletes it, because a probe must not leave a colour in the real session file for a later run to inherit. - `[reseq-probe] reseq=N late=N stale=N overflows=N holes=N hole_slots=N backfilled=N open_holes=N bands=N bands_repaired=N nan_slots=N scanned=N trigger_in_hole=D` (stderr, @@ -1144,6 +1146,30 @@ and `regained` reads 400 ms later. - `collapsed=1` means the window is narrower than `kSidebarRoom` (900), which is the expected reading on a scaled display, where the logical width is half what the screen looks like. +## `[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 +``` + +`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. + +- **`advanced=4` is the assertion**: every accumulator gained passes while its page was off screen, 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 + 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 + (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 + read `advanced=0` for a reason that has nothing to do with the tab. +- `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. + ## `[prefs-probe]` ``` @@ -1252,25 +1278,20 @@ the two lines together read what the file gave and what the code put in it. ## `signals panel:` ``` -[udpscope] signals panel: labels=2 list=1 +[udpscope] signals panel: labels=2 list=1 rows=5 signals=4 math=0 divider=1 +[udpscope] signals panel: labels=2 list=1 rows=9 signals=4 math=4 divider=1 ``` -The signals panel's frame read from the window: the heading and the hint the `.ui` gives it, and the list -the rows go in. `labels=1` is a `.ui` that lost one of the two, `list=0` one that lost the list -- both of -which otherwise show as a panel quietly missing a piece. `panels: … signals=WxH row_h=26` is the other half: -the list's own geometry and the height a row really takes, which is what the compact-row CSS rule is for. - -## `math panel:` - -``` -[udpscope] math panel: head=2 buttons=1 list=1 title='Math channels' -``` - -The math panel's frame read from the window: the head row the `.ui` gives it (the title and the add -button), and the list the channel rows go in. `buttons=0` is a `.ui` that lost the `+` -- a panel that -cannot add a channel, with nothing else complaining -- and `title=` is the file's own text read back, so a -`.ui` that lost the label shows as `title='-'`. `[math-probe]` is the other half of the reading: it drives -the add and the editor through the same code a user does. +The signals panel's frame read from the window, and in the same line the *shape* of the one channel list: +the heading and the hint the `.ui` gives it, the list the rows go in, how many rows that list holds +(`rows`), how many of them are streamed signals (`signals`), how many math channels the app has (`math`), +and `divider`, which is 1 when `rows == signals + 1 + math` and 0 otherwise. `labels=1` is a `.ui` that lost +one of the two labels, `list=0` one that lost the list -- both of which otherwise show as a panel quietly +missing a piece -- and **`divider=0` is the merged list that lost its `+`**, the one control that can create +a math channel. The first line above is a run with no math channels at all: the divider is there anyway, +because a `+` that only appeared once a channel existed could never create the first one. `panels: … +signals=WxH row_h=26` is the other half: the list's own geometry and the height a row really takes, which +is what the compact-row CSS rule is for. ## `events pane:` diff --git a/docs/claude/verification.md b/docs/claude/verification.md index 4b3935b..4f9344d 100644 --- a/docs/claude/verification.md +++ b/docs/claude/verification.md @@ -15,9 +15,11 @@ xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \ Two baselines every run is read against. A clean build emits **four** warnings and no more: two `-Wmissing-field-initializers` on `MathNode::source` and `-Woverlength-strings` on **both** -generated GResource `.c` files — the help pages' and the icons', whose blob crossed C99's -4095-byte string-literal limit when the two app icons joined the four action ones (a note about a -generated file, which GCC itself supports). The fifth warning that used to appear here was an unused +generated GResource `.c` files — the help pages' (**514532** bytes) and the icons', whose blob is +**8613** against C99's 4095-byte string-literal limit with the five action icons and the two app icons +that remain (a note about a generated file, which GCC itself supports) -- re-measured when +`udpscope-math-symbolic` was deleted, which did not move the count; re-measure by touching the two +`*.gresource.xml` files and rebuilding. The fifth warning that used to appear here was an unused `output_built` in `math_probe()`; it was **deleted** as dead code — redundant with the `output_after_add` already printed on the same line — and that deletion is what dropped the baseline from five to four. Nothing started reading it. And an exit-dump run emits **no** `Gtk-CRITICAL` at @@ -205,6 +207,12 @@ 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 + 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`. - `UDPSCOPE_PROBE_SEARCH_MS` — runs a buffer search for the armed condition, cross-checks the live trigger's own `t0` against the nearest hit (`agree=1` is the load-bearing reading), and jumps to hit 0. Reports the resulting `mode=`, `axis=` and `marks placed=`. diff --git a/src/help/01-overview.txt b/src/help/01-overview.txt index 22d24fd..7bb6131 100644 --- a/src/help/01-overview.txt +++ b/src/help/01-overview.txt @@ -31,13 +31,14 @@ panel is working correctly, not failing to connect. The window • The header bar -- left to right: the time window (2 s), the trigger and the - pause, then the pane switches -- signal list, cursors, measurements, math, side panel -- and - the menu, which holds Fit to data, the layout editor, Export, Keyboard shortcuts, Help and - About. + pause, the events, cursors and side-panel switches, and the menu, which holds Fit to data, the + layout editor, Export, Keyboard shortcuts, Help and About. -• The side panel -- the trigger, cursors, measurements and math panes at the top, then - the signal list: one row per signal with its colour, an enable switch, its name and its unit. - Signals are dragged from here onto a plot row. +• The side panel -- two tabs. Measure holds the cursors readout and the + measurements derived from it; Signals holds the trigger and the channel list, where a + math channel is a row of that one list under a MATH heading whose + creates + one. One row per channel: its colour, an enable switch, its name and its unit. Channels are + dragged from here onto a plot row. • The plot -- up to 8 rows, each with its own y axis and up to 4 traces, all on one shared time axis. Right-click a row for its own menu (fit, add a signal, remove a trace). diff --git a/src/ui/shortcuts.ui b/src/ui/shortcuts.ui index ebd828a..04d4882 100644 --- a/src/ui/shortcuts.ui +++ b/src/ui/shortcuts.ui @@ -46,7 +46,7 @@ 1 t - Show or hide the trigger panel + Enable or disable trigger mode @@ -66,21 +66,21 @@ 1 c - Show or hide the cursors pane + Enable or disable the cursors 1 m - Show or hide the measurements pane + Show the Measure tab 1 g - Show or hide the math pane + Show the Signals tab diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index 83be65e..40784e7 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -1124,6 +1124,89 @@ gboolean stats_probe(gpointer user) { return G_SOURCE_REMOVE; } +/// Phase two of the tab-switch probe: the hidden page's accumulator must have advanced. A statistics +/// readout is *history*, so min/max/avg would gain a gap for exactly as long as the trigger was on +/// screen -- which is the one defect the design's "what runs on a hidden page" rule is there to +/// prevent, and the reason no frame-rate code may be gated on which tab is current. +gboolean side_tab_probe_report(gpointer user) { + App* a = static_cast(user); + if (a->window == nullptr) return G_SOURCE_REMOVE; + std::uint64_t advanced = 0; + std::uint64_t min_n = 0; + bool first = true; + for (std::size_t i = 0; i < a->measure_stats.size(); ++i) { + const std::uint64_t n = a->measure_stats[i].n; + if (i < g_side_tab_n_before.size() && n > g_side_tab_n_before[i]) ++advanced; + if (first || n < min_n) { + min_n = n; + first = false; + } + } + std::fprintf(stderr, + "[side-tab-probe] page=%s rows=%zu accums=%zu advanced=%" G_GUINT64_FORMAT + " min_n=%" G_GUINT64_FORMAT "\n", + a->side_tab.c_str(), a->measures.size(), a->measure_stats.size(), advanced, min_n); + std::fflush(stderr); + return G_SOURCE_REMOVE; +} + +/// Phase one: four measurements on the first channel, statistics on, then switch *away* from the +/// Measure tab and come back in two seconds to read what ran while it was not on screen. +gboolean side_tab_probe(gpointer user) { + App* a = static_cast(user); + if (a->window == nullptr) return G_SOURCE_REMOVE; + // One signal on, and on a plot, the way `enable_all_probe` does it and a restored session arrives. + // Both halves matter here: a signal starts disabled (enabling is what starts acquisition), and a + // channel with no trace leaves the live window *still* -- and a still window is exactly the state + // the tick is supposed to skip, so a probe that left it that way would be testing nothing. This + // probe is about the case where the window does move while the page is somewhere else. + std::size_t pick = a->model->signals().size(); + { + const std::vector& signals = a->model->signals(); + for (std::size_t i = 0; i < signals.size(); ++i) { + if (ScopeModel::is_time_signal(signals, i)) continue; // the time base is not acquired + pick = i; + break; + } + } + if (pick >= a->model->signals().size()) return G_SOURCE_REMOVE; + a->model->set_signals_enabled({pick}, true); + std::string name; + { + const std::vector& channels = a->model->channels(); + if (channels.empty()) return G_SOURCE_REMOVE; + name = channels.front().name; + a->rows->assign(0, 0, channels.front().name, channels.front().name, + signal_color_index(a->model->signals(), channels.front().signal)); + } + a->relayout_pending = true; + if (a->measures.empty()) { + for (const MeasureType t : {MeasureType::Average, MeasureType::Top, MeasureType::PeakToPeak, + MeasureType::Amplitude}) { + MeasureOptions options; + options.show_on_plot = measure_can_show_on_plot(t); + a->measures.push_back({name, t, options}); + } + rebuild_measures_pane(*a); + } + if (a->measures_stats_check != nullptr) { + gtk_check_button_set_active(GTK_CHECK_BUTTON(a->measures_stats_check), TRUE); + } + select_side_tab(*a, "measure"); + update_measures(*a, /*force=*/true, /*accumulate=*/true); + g_side_tab_n_before.clear(); + for (const MeasureStatsAccum& s : a->measure_stats) g_side_tab_n_before.push_back(s.n); + // Away from the page the measurements are on: its widgets are now unrealized and measure 0x0, and + // the tick keeps calling `update_measures()` regardless. + select_side_tab(*a, "signals"); + std::fprintf(stderr, "[side-tab-probe] page=%s accums=%zu baseline recorded\n", + a->side_tab.c_str(), g_side_tab_n_before.size()); + std::fflush(stderr); + // Long enough for several throttled recomputes (one per 100 ms while the window moves). + g_timeout_add(2000, side_tab_probe_report, a); + return G_SOURCE_REMOVE; +} + gboolean pan_probe(gpointer user) { App* a = static_cast(user); if (a->window == nullptr || a->rows->row_count() <= 0) return G_SOURCE_REMOVE;