fix: the trigger stays editable while it is off, and the locked pane says "stream"

Two things the lock got wrong, and they are the same mistake seen twice: trigger-off was treated as "the
pane is disabled".

**The configuration was unreachable exactly when it was wanted.** `on_trigger_toggled()` set the trigger
*body* insensitive with the mode — a rule carried over from the five-revealer sidebar, where a closed pane
had nothing to edit. 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 ordinary way to work. The body is now never disabled; only the
*cursors'* readout still is, which is the pane whose content has nothing to show with the instruments off.

**The locked pane described a trigger that was not running.** With trigger mode off the summary is one
line — `mode: stream (trigger off)` — because the four lines it used to show (signal, condition, level,
mode) describe a trigger that is not there. With the trigger on it is those four plus whatever is not at
its default (`noise band`, `HF reject`, `pre-trigger`, `hold-off`).

The reading changes with it: `trigger panel sensitive …=1` is now a **regression guard** rather than a
state — the trigger's body is always live, so a `0` there means the settings have been locked out again.

Verified in both states, from sessions whose only difference is `trigger_enabled`:

    panes: trigger=locked signals=locked body_visible=0 summary='mode: stream (trigger off)'
    panes: trigger=locked signals=locked body_visible=0 summary='signal: square | condition: window, rising | level: 0.3 .. 0.6 | mode: single | pre-trigger: 20 %'
    trigger panel sensitive before the probe toggled it: 1

The locked trigger pane is 72 px with the trigger off and 192 px with it on. Docs: the invariants' rule
for which body is disabled and why the trigger's is not, the `summary`'s two forms with what each means,
the README's deviation note, and the second `panes:` line renamed to say what it is — the line as the
probe found it, because only a run whose *session* has the trigger off shows the stream form.
This commit is contained in:
Martino Ferrari committed 2026-10-14 14:46:15 +02:00
1 parent 4f4851f4e3
commit 796dda4ad5
7 files changed
+48 -15

No files matched your search

+3 -2
View File
@@ -2412,8 +2412,9 @@ now — they are listed so they are not mistaken for anything else:
measured), so the pages carry an empty title and the pills are named in code. 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.
toggle hides a pane any more: cursors-off **disables** its readout and leaves it in place, and
trigger-off leaves the trigger panel 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.
- **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
+7 -3
View File
@@ -166,9 +166,13 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
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 pane's **body** insensitive, so the pane stays where it
is and the settings you are about to switch back on do not vanish while you look for them. The *body*
and not the pane, because each pane's heading carries its own switch (see the next bullet). The
**cursors'** toggle sets its pane's **body** insensitive -- the readout has nothing to show with the
instruments off -- and the *body* and not the pane, because the pane's heading carries its own switch
(see the next bullet). The **trigger's body is deliberately never disabled**: a trigger is configured
*before* it is switched on, so switching it off to change it, or to read what it is set to, is the
ordinary way to work. It used to go insensitive with the mode, which left the settings unreachable
exactly when a user wanted to change them; `trigger panel sensitive …=1` is the regression guard, and a
`0` there means the configuration has been locked out again. The
trap itself does not go away with the widget: a stack's hidden page measures 0x0 too and is still
given the leftover width, so **ask about state, not about size** carries over unchanged. Every
allocation read in the sidebar is a debug capture, and each reads 0 on the page that is *not*
+9
View File
@@ -1145,6 +1145,7 @@ library measures and draws the rect in tests of its own.
[udpscope] pane edits: locked=document-edit-symbolic(ok) config=object-select-symbolic(ok)
[udpscope] panes: trigger=locked signals=locked body_visible=0 summary='signal: square | condition: window, falling | level: 0.3 .. 0.6 | mode: single | noise band: 0.2 | pre-trigger: 20 % | hold-off: 1000 ms'
[udpscope] panes: trigger=config signals=config body_visible=1 summary=''
[udpscope] panes: the same line as the probe found it: 'mode: stream (trigger off)'
```
**`pane edits:`** is the two `[edit]` buttons in each mode, by name and verdict, and it exists because of
@@ -1160,6 +1161,14 @@ changes the configuration — and **config** is the full pane, which is what a *
session file that loaded is what "we have been here before" means. Each heading's `[edit]` button flips
its pane.
- **With trigger mode off the summary is one line**: `mode: stream (trigger off)`. The four lines below
describe a trigger that is not running, so quoting them would describe nothing. The dump carries the line
twice — once as the probe found it and once at the end — because only a run whose *session* has the
trigger off shows the stream form, and a run whose session has it on shows the settings.
- **`trigger panel sensitive …=1` is now a guard, not a state.** The trigger's body is never disabled —
a trigger is configured before it is switched on — so this reads `1` always, and a `0` means the
settings have been made unreachable again. The *cursors'* body is disabled with the cursors, and that
is the pane whose readout has nothing to show.
- **`summary`** is the locked trigger pane's whole content, as `key: value` pairs separated by ` |`. The
pane draws it as a small definition list — `signal`, `condition`, `level`, `mode` on four lines, and
then only the figures that are *not* at their defaults (`noise band`, `HF reject`, `pre-trigger`,
+6 -5
View File
@@ -4852,11 +4852,10 @@ void on_activate(GtkApplication* application, gpointer user) {
app->signal_panel = build_signal_panel(*app);
gtk_box_append(GTK_BOX(signals_page), app->signal_panel);
// Both instruments start off, so both *bodies* start insensitive -- stated here by the one place
// that knows the default, rather than left to whichever widget happened to be built first. The
// bodies and not the panels: each panel's heading carries the switch that turns it on, and a
// control inside an insensitive parent cannot be clicked.
gtk_widget_set_sensitive(app->trigger_body, FALSE);
// The cursors start off, so their readout starts insensitive -- stated here by the one place that
// knows the default, and the *body* and not the panel, because the panel's heading carries the
// switch that turns it back on. The trigger's body is deliberately *not* disabled: a trigger is
// configured before it is switched on, so its settings have to stay editable while it is off.
gtk_widget_set_sensitive(app->cursors_body, FALSE);
gtk_widget_set_sensitive(app->cursors_reset, FALSE);
@@ -5790,6 +5789,8 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
app.dbg_trigger_locked ? "locked" : "config",
app.dbg_signals_locked ? "locked" : "config", app.dbg_trigger_body_visible,
app.dbg_trigger_summary.c_str());
std::printf("[udpscope] panes: the same line as the probe found it: '%s'\n",
app.dbg_trigger_summary_early.c_str());
std::printf("[udpscope] panel switches: trigger=%d/%d cursors=%d/%d drove_toolbar=%d "
"(pane switch / header bar button; `drove_toolbar` is the pane's own switch having "
"moved the button)\n",
+4
View File
@@ -283,6 +283,10 @@ struct App {
/// The compact line the locked trigger pane shows. It is the pane's whole content in that mode, so it
/// is read rather than assumed: empty here is a locked pane showing nothing at all.
std::string dbg_trigger_summary;
/// The same line as the exit probe found it, captured before the probe touches the trigger. The locked
/// pane says one thing about a trigger that is running and another about one that is not, and only a run
/// whose session has it off shows the second -- so the dump carries both rather than assuming either.
std::string dbg_trigger_summary_early;
/// The two [edit] buttons' icons, read in each mode by clicking into it and back: a button whose icon
/// does not resolve is *blank*, and that is how the missing "done" icon was found.
std::string dbg_pane_edit_icon;
+1
View File
@@ -2340,6 +2340,7 @@ gboolean exit_probe_step1(gpointer user) {
a->trigger_body != nullptr && gtk_widget_get_visible(a->trigger_body) ? 1 : 0;
// The summary's text comes from the sync that wrote it: the container is a grid of key/value rows,
// and asking it for a label's text was itself a critical.
a->dbg_trigger_summary_early = a->dbg_trigger_summary; // as found, before this probe touches it
a->dbg_panel_switch_live =
a->trigger_switch != nullptr && gtk_widget_get_sensitive(a->trigger_switch) &&
a->cursors_switch != nullptr && gtk_widget_get_sensitive(a->cursors_switch)
+18 -5
View File
@@ -183,6 +183,18 @@ void sync_trigger_summary(App& app) {
channel = channels[static_cast<std::size_t>(tc.channel)].name;
}
}
// With trigger mode off the scope is streaming, and that is the whole of what this pane has to say:
// the four lines below describe a trigger that is not running.
if (!tc.enabled) {
put("mode", "stream (trigger off)");
for (int i = row; i < App::kTriggerSummaryRows; ++i) {
gtk_widget_set_visible(app.trigger_summary_keys[i], FALSE);
gtk_widget_set_visible(app.trigger_summary_values[i], FALSE);
}
app.dbg_trigger_summary = reading;
gtk_widget_set_visible(app.trigger_summary, app.trigger_locked);
return;
}
put("signal", channel.empty() ? std::string("none acquired") : channel);
// The condition: the shape, and whatever qualifies it. A pulse width carries its comparison and its
// width, which is what the shape *is*; a window carries its two levels on the line below.
@@ -386,11 +398,12 @@ void on_trigger_toggled(GtkToggleButton* button, gpointer user) {
if (on && signal >= 0) {
app->model->set_signal_enabled(static_cast<std::size_t>(signal), true);
}
// Disabled, not hidden: 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. The *body* is what goes
// insensitive and not the panel, because the panel's heading row carries the switch that turns
// this back on -- and a control inside an insensitive parent cannot be clicked.
gtk_widget_set_sensitive(app->trigger_body, on);
// The panel's body stays *live* whether or not the trigger is running: a trigger is configured
// before it is switched on, and switching it off to edit it -- or to check what it is set to --
// is the ordinary way to work. (It used to go insensitive with the mode, which left the settings
// unreachable exactly when a user wanted to change them.) What that costs is the reading: the
// `trigger panel sensitive` line is now a regression guard, and a `0` there means the
// configuration has been locked out again.
// The event-history button is part of trigger mode and the pane it opens is *on demand*; leaving
// trigger mode takes the button away and collapses the pane, so going back to streaming is not a
// column of stale events beside the plot. Recording is the other half of that answer, so this is