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

+24
View File
@@ -9,6 +9,16 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
tick as the channel dump; the status line only formats what is already there. The
badge's markup is likewise rewritten on a change of phase (twice a second), never on
all sixty frames.
- **The trigger circle is the badge's sibling, in its alphabet and under its rules.** It sits
before the badge on the status row (`main-window.ui` `trigger_status`) and says what the
badge cannot: the *trigger's* state -- solid green while an event is held or a capture is
filling, flashing green (the lit and dim halves) while armed and waiting, red while the
trigger mode is on but disarmed, and **hidden while the trigger mode is off**, because a
light for a mode the user is not in is noise and the mode text beside it shows no trigger
words then either. Same width-locked glyph in every state, same half-second square for the
flash, markup rewritten on a change of the state key only (`trigger_circle_state`), and the
exit dump reads the tick's cached answer (`dbg_trigger_circle`) because it cannot ask the
widget.
- **A `notify::<prop>` handler takes three parameters, a `value-changed` handler two.**
`GObject::notify` passes `(GObject*, GParamSpec*, gpointer)` while
`GtkSpinButton::value-changed` passes `(GtkSpinButton*, gpointer)`, so one callback cannot
@@ -722,3 +732,17 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps
box drawn from that is a box over the wrong thing. Its numbers depend on the *window's* size, which
under the documented `xvfb-run` is the window's 756x894 minimum (a 640x480 screen), so a run on a
larger screen gives different rectangles for the same panels.
- **GTK4 has no `GtkWidget::destroy` signal; the lifecycle hook is `close-request`, and
`gtk_window_destroy()` never emits it.** A `g_signal_connect(window, "destroy", ...)` on a GTK4
widget is a connect to a signal that does not exist: it never fires (and its "signal not found"
warning is in the GObject domain, not GTK's, so the dump's `gtk warnings:` counter cannot see
it). That is why the dialogs use `gtk_window_close()` when their *lifetime* matters (a close
clears `App::measure_dialog`, which a destroy would not), and why the main window's own handler
-- the scope windows' teardown, the debounced layout-save flush and the application quit -- is
connected to **`close-request`** and returns `FALSE` so the default destroy still runs.
Everything that ends the main window programmatically (the exit probe's `exit_probe_step3b`)
closes it with `gtk_window_close()` for the same reason. The **scope windows go with it**:
each is a `GtkWindow` added to the application, and one left behind keeps the GApplication's
main loop alive around a window that is gone -- the hang where the exit run never reached its
dump.