scope: full-screen figures, second windows, gated measurements, per-configuration events

A batch of view and instrument work, with the probes and docs for each.

- **Full screen per figure row**: a toggle on each row's top-right corner, revealed by
  pointer proximity (`kMaximizeRevealPx` around its placed rect, per row, read from an
  overlay-level motion controller so a revealed button under the pointer cannot flicker);
  the plot menu offers the same as *Full screen this figure* / *Restore the layout*. The
  arrangement is saved and restored through `RowState`, so nothing plotted moves. A layout
  of one figure carries no toggle.
- **Second window**: `open_scope_window()` copies a figure (cells, width shares, signals in
  their colours) or opens an empty one, with its own `RowSet` and `PlotArea`; the main
  window's tick binds, acquires and repaints it, mirroring the time window and pin. Entries:
  the plot menu's *Open this plot in a new window* and the overflow menu's *New scope window*.
- **Layout editor from the plot**: the plot menu gains *Edit the layout...* / *Done editing
  the layout*, flipping the app's one toggle.
- **Measurements gate**: `MeasureOptions::gate` restricts a measurement to the held/pause
  snapshot (live falls back to the live ring), persisted with the session and shown in the
  measure dialog.
- **Crosshair axis badges**: a preference (`crosshair_axis_labels`, on by default) and the
  plotter's badges where the lines meet the axes, in the axes' own formats; the plot menu
  and the probe read them.
- **Event cache per configuration**: `EventStore` is keyed `<source>/<configuration>/`,
  `retarget_event_store()` follows every configuration switch, and a pre-configuration cache
  (loose files) is adopted into the configuration's folder on first start.
- **GTK4 lifecycle**: the main window's handlers move from the non-existent `"destroy"`
  signal to `close-request`; scope windows are closed with it and the application quits
  explicitly (the exit run's hang). The exit probe closes with `gtk_window_close()`.
- Probes: `[maximize-probe]` (single-figure rule and the corner reveal), `[scope-probe]`,
  `[split-probe]` cell crosshair leg, `[prefs-probe]` store keying, `[configs]` store key,
  `[rowmenu]` layout-editor entry; docs, help pages, README and AGENT.md updated.

Also fixes a probe-only use-after-free: `toggle_maximize_figure_row` replaces the figure
object, so the area must be re-pointed (the button's path does; the probe's raw call did not).
This commit is contained in:
Martino Ferrari committed 2026-11-05 17:58:59 +01:00
1 parent bfe13630f9
commit bdf0ff7ddc
39 files changed
+2267 -136

No files matched your search

+10
View File
@@ -63,6 +63,16 @@
<item><p>A marker past the newest sample reads <code>--</code>. With only one marker
over data the row reads <code>partial</code>: a difference between a number and nothing
is not a number.</p></item>
<item><p>The hover crosshair also writes its two coordinates where the eye already
looks: the value where its horizontal line meets the y axis, and the time where its
vertical line meets the x axis — each in a small box among that axis' own tick labels,
in the axis' own format. Each box is centred on the line it names, and its background
covers what is behind it, so a tick label underneath never prints through the number.
The preference's <gui>Crosshair axis labels</gui> turns the pair off.</p></item>
<item><p>In a split row the horizontal line runs across <em>every</em> plot of the row,
not just the one under the pointer: the plots share one y range, so it is one value
along the whole row. It stops at the row's edges — another row has a scale of its
own.</p></item>
<item><p>In <gui>mix</gui> each marker also places a crossing annotation per trace,
which is what lets you see on the plot which sample a number came from. It is <em>on
that sample</em>, so when the marker falls between two it sits a little off its own
+24 -2
View File
@@ -29,7 +29,8 @@
add a trace.</p>
</item>
<item>
<p>Press <key>l</key>, or pick <gui>Layout editor</gui> from the overflow menu, to add
<p>Press <key>l</key>, pick <gui>Layout editor</gui> from the overflow menu, or pick
<gui>Edit the layout…</gui> from a plot's right-click menu, to add
and remove rows: a <gui>×</gui> appears at the top left of each plot and a <gui>+</gui>
on each boundary between rows, including above the first and below the last.</p>
</item>
@@ -58,12 +59,33 @@
<p>All the plots of a row, and of the whole figure, share one time window: zooming or
panning along time in any of them moves all of them, and the vertical cursors are drawn in
every plot. The plots of a split row also share one y range: the y labels are drawn at the
left of the first plot only, the horizontal cursors sit at the same values in every plot, and a
left of the first plot only, the horizontal cursors sit at the same values in every plot, the
hover crosshair's horizontal line spans all of them, and a
zoom, pan, fit or fixed range in any of them applies to the whole row. The time labels
sit under the bottom row, and under any row above it whose columns differ from the row below. A row holds at most 4 plots and the figure at most 8
in total; the <gui>+</gui> buttons go grey at the limit.</p>
</section>
<section id="fullscreen">
<title>A row full screen, and a second window</title>
<list>
<item><p>Every row carries a <gui>full-screen</gui> button in its top-right corner: it comes
out of hiding when the pointer moves near that corner and hides again when it leaves, so the
plot is not decorated by a control you are not reaching for. One click makes that row fill the plot area — the
other rows step aside with everything they were plotting — and the same button (now the
restore icon) brings the whole layout back exactly as it was. It is a view, not an edit:
nothing is saved, nothing is dropped, and the rows' traces, colours and cursors come back
where they were. The plot's right-click menu offers the same as <gui>Full screen this
figure</gui> (and <gui>Restore the layout</gui> while one is enlarged). A layout of a single
figure has nothing to enlarge *to*, so it carries no button.</p></item>
<item><p>A plot's right-click menu has <gui>Open this plot in a new window</gui>: a second
window showing the same stream through a copy of that figure (its cells, their widths and
every trace), with its own axes following the main window's time window. The overflow menu's
<gui>New scope window</gui> opens the same kind of window with an empty figure. Closing one
closes that view only; the main window keeps acquiring.</p></item>
</list>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
+31 -1
View File
@@ -80,8 +80,19 @@
shorter than a level's own resolution, or one read through <gui>hf</gui> — where
<gui>top</gui>/<gui>bottom</gui> fall back to the mean of each side of the split and
<gui>amplitude</gui> to the window's peak to peak.</p></item>
<item><p>Each row names the <em>channel</em> it measures: its number among the active
channels (1., 2., … the same number the signal's badge in the bar carries), its name, and
the type — <code>1. sine · average</code>. The measurement dialog's signal dropdown
numbers its entries the same way. A channel list that changes (a signal switched off, a
new one enabled) renumbers, because the number is the answer to "which active channel is
this".</p></item>
<item><p>The on-plot marks come in two kinds and the mix is the reading: a <em>level</em>
mark is a ruler at a value, a <em>time</em> mark is a marker at a position. An average
mark is a ruler at a value — its value written in a small box on the ruler, the way the
axis writes its own ticks — and a <em>time</em> mark is a marker at a position, with the
position's own badge beside it. A level ruler's badge carries its number; a time
marker's carries its time. Marks whose badges meet share one panel with one line each,
so a column of close readings is one boxed readout rather than a smear of texts; and a
box over a noisy trace is what keeps the number readable. An average
draws one level mark; a max draws a level mark <em>and</em> a time mark at the first
sample that reached it; <gui>peak to peak</gui> draws a ruler at each extreme and
<gui>amplitude</gui> a ruler at each of its two flat levels — the pair is the span, so
@@ -136,6 +147,25 @@
</list>
</section>
<section id="gate">
<title>The gate</title>
<p>A measurement's dialog carries <gui>Gate enabled</gui>. Every measurement
with it set reads only the samples inside one shared band — violet, labelled <em>gate</em>,
carried by its own two draggable edges on the plots, which are not the cursors (those keep
meaning what the cursors pane reads). Measurements without it are untouched: they read the
whole window and share the walks they always shared.</p>
<p>The band is shown exactly while some measurement is gated — gate one and it appears,
gate or delete the last one and it goes — because it is one area that all of the gated
measurements share, not a property of any one of them. Drag an edge to resize it, grab the
middle to move it.</p>
<p>The band lives in the same relative time the axis labels use, so it stays where it was
put in both modes: live, a band at 300 ms to 100 ms ago stays put on screen while the data
streams beneath it — the gated read of the newest data a bench scope gives — and on a held
event it names a fixed slice of the frozen frame. A band with no samples in it — dragged
outside the view, or over a gap — reads <code>--</code> like any empty window. The band's
position and each measurement's gate flag are saved with the session.</p>
</section>
<section id="example">
<title>Example</title>
<p>On a 1 Hz ±1 sine: <code>average=0</code>, <code>top=0.957988</code>,
+5
View File
@@ -29,6 +29,11 @@
not a working badge. Of the two steady colours, <em>grey</em> is no stream at all or a
pipeline you stopped (the tooltip says which), and <em>yellow</em> is a source that
answered but has sent no configuration.</p></item>
<item><p>Left of the badge is the <em>trigger circle</em>, and it is the trigger's own
light: solid green while the trigger has fired (an event is held, or a capture is
filling), <em>flashing</em> green while armed and waiting for an event, red while the
trigger mode is on but disarmed, and hidden entirely while the trigger mode is off — a
light for a mode you are not in is noise. Its tooltip names the state in words.</p></item>
<item><p>The state field is one word from a fixed set: <code>connecting</code>,
<code>live</code>, <code>paused</code>, <code>armed</code>, <code>capturing</code>,
<code>event held</code>, <code>disarmed</code>. Two states add one field each: in trigger
+3 -1
View File
@@ -169,7 +169,9 @@
<section id="cache">
<title>The event cache, replay and compare</title>
<p><em>Every triggered acquisition is kept.</em> Latches are written to the event cache
(<file>~/.cache/udpscope/events/&lt;source&gt;/</file>) as a <file>.npy</file> of the
(<file>~/.cache/udpscope/events/&lt;source&gt;/&lt;configuration&gt;/</file> -- one folder per
source and configuration, so two configurations of one source keep their events apart) as a
<file>.npy</file> of the
event, a <file>.txt</file> of what it was, and a <file>.bmp</file> picture drawn from the
samples. The <gui>Events</gui> block at the foot of the dock lists them, newest first, one
short row each: a marker for the event on screen, its title and its number. It is dimmed