feat: locked panes for a returning user, and the trigger is persisted
Two panes, two modes. **Locked** is what a returning user gets: the trigger pane shows one line saying
what it is set to, the signal list shows only the signals that are on, and neither offers a control that
changes the configuration — each heading has an `[edit]` button (the Adwaita `emblem-system-symbolic`,
swapping to `emblem-ok-symbolic` in configuration mode) that flips its pane. A **first run** — no session
file for that source — never reaches `apply_layout()` and comes up in configuration mode, because
configuring the host is what a first run is for. The mode is a mode and not a setting, so it is
deliberately not persisted.
The locked signal list drops the rows of every signal that is off, which is the whole of "show active
signals": the list already filters on nothing, so the lock is one `continue`. With the rows go the acquire
tick, the overflow menu (Information, Calibrate…, Delete, rename) and the `MATH` divider's `+` — and the
colour chip stops being clickable, because choosing a colour is configuration too. Dragging a signal onto
a plot stays: that is which view shows what, not what the scope is set up to do.
**The trigger is persisted, settings and state both** — the settings *even while it is off*, which is the
point: a trigger configured once and switched off is still the trigger you come back to. `LayoutState`
gains fifteen flat `trigger_*` keys; the channel is stored by *name* (every rebuild renumbers the
channels, the model's own rule) and the two enumerations as **the words the panel's dropdowns show**,
because an index ties the file to a declaration order — the lesson `math_op_at()` already carries. The
words are shared with the panel now (`trigger_type_text()` and friends build the dropdown arrays), so the
two cannot drift.
Two ordering traps, both found by running it:
- **The channel name resolves twice over.** `rebuild_trigger_signals()` picks up
`App::pending_trigger_channel` when the stream arrives *after* the load, and `apply_trigger_layout()`
resolves it itself when the channels are already there — without the second path the name stayed
pending past the one rebuild that would have used it, and the trigger silently sat on channel 0
(`summary='pulse width · sine …'` where the session said `square`).
- **The trigger's state is applied by pressing the control, not by setting a flag**: the toggle is set
*outside* the `updating_panels` window `apply_layout()` uses, because a toggle set inside it emits
`toggled` into a handler that returns at once and nothing would happen — no arming, no events button,
no sensitivity.
Verified, in both modes and both directions:
panes: trigger=locked signals=locked body_visible=0 summary='window · square · 0.3…0.6 · 20 % pre'
panes: trigger=config signals=config body_visible=1 summary=''
signals panel: labels=2 list=1 rows=7 signals=2 math=4 divider=1
The locked trigger pane is 42 px tall against 580 unlocked. A hand-written session round-trips: every
value it named came back in the panel's own controls, `trigger=1/1` (the state arrived and the pane's
switch followed it), and the file the app wrote back carried all fifteen keys with the untouched ones at
their defaults. `[side-tab-probe] … advanced=4` on its documented recipe; `make test-ui` 14 files; zero
`Gtk-CRITICAL` and no missing icon.
Docs: the `panes:` reading with what each field means, the corrected `labels=` note (the panel's frame is
counted above the list now, because the heading sits in a heading row), the invariants for both the
persistence and the lock, the README's deviation note, and the removal of its claim that the trigger has
never been persisted — it is now, and the locked view is built from exactly that.
This commit is contained in:
1 parent
cc8335c514
commit
121abbefae
12 files changed
+574
-28
No files matched your search
@@ -476,6 +476,48 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
||||
return v;
|
||||
};
|
||||
|
||||
/// One `trigger_*` key, by its name after the prefix. Text for the two enumerations and for the
|
||||
/// signal, numbers for the rest; `value` arrives unquoted.
|
||||
const auto trigger_key = [](LayoutState::Trigger& t, const std::string& k, const std::string& v) {
|
||||
const auto num = [&](double& out) {
|
||||
try {
|
||||
out = std::stod(v);
|
||||
} catch (...) {
|
||||
}
|
||||
};
|
||||
if (k == "enabled") {
|
||||
t.enabled = v == "true";
|
||||
} else if (k == "signal") {
|
||||
t.channel = v;
|
||||
} else if (k == "type") {
|
||||
t.type = v;
|
||||
} else if (k == "edge") {
|
||||
t.edge = v;
|
||||
} else if (k == "cmp") {
|
||||
t.cmp = v;
|
||||
} else if (k == "mode") {
|
||||
t.mode = v;
|
||||
} else if (k == "level") {
|
||||
num(t.level);
|
||||
t.level_given = true;
|
||||
} else if (k == "level2") {
|
||||
num(t.level2);
|
||||
} else if (k == "width") {
|
||||
num(t.width_s);
|
||||
} else if (k == "width2") {
|
||||
num(t.width2_s);
|
||||
} else if (k == "timeout") {
|
||||
num(t.timeout_s);
|
||||
} else if (k == "pre") {
|
||||
num(t.pretrigger_pct);
|
||||
} else if (k == "holdoff") {
|
||||
num(t.holdoff_s);
|
||||
} else if (k == "band") {
|
||||
num(t.noise_band);
|
||||
} else if (k == "hf") {
|
||||
num(t.hf_fc);
|
||||
}
|
||||
};
|
||||
if (section == Section::Top) {
|
||||
if (key == "window_s") {
|
||||
try {
|
||||
@@ -498,6 +540,8 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
||||
st.measure_stats = value == "true";
|
||||
} else if (key == "side_tab") {
|
||||
st.side_tab = strip_quote(value);
|
||||
} else if (key.rfind("trigger_", 0) == 0) {
|
||||
trigger_key(st.trigger, key.substr(8), strip_quote(value));
|
||||
} else if (key == "multicast") {
|
||||
st.multicast = strip_quote(value);
|
||||
} else if (key == "interface") {
|
||||
@@ -648,6 +692,24 @@ bool save_layout(const std::string& path, const LayoutState& state) {
|
||||
}
|
||||
o << "measure_stats = " << (state.measure_stats ? "true" : "false") << "\n";
|
||||
o << "side_tab = \"" << state.side_tab << "\"\n";
|
||||
// The trigger, settings and state both. Written even when it is off: that is the point of it.
|
||||
o << "trigger_enabled = " << (state.trigger.enabled ? "true" : "false") << "\n";
|
||||
if (!state.trigger.channel.empty()) {
|
||||
o << "trigger_signal = " << toml_escape(state.trigger.channel) << "\n";
|
||||
}
|
||||
o << "trigger_type = " << toml_escape(state.trigger.type) << "\n";
|
||||
o << "trigger_edge = " << toml_escape(state.trigger.edge) << "\n";
|
||||
o << "trigger_cmp = " << toml_escape(state.trigger.cmp) << "\n";
|
||||
o << "trigger_mode = " << toml_escape(state.trigger.mode) << "\n";
|
||||
o << "trigger_level = " << state.trigger.level << "\n";
|
||||
o << "trigger_level2 = " << state.trigger.level2 << "\n";
|
||||
o << "trigger_width = " << state.trigger.width_s << "\n";
|
||||
o << "trigger_width2 = " << state.trigger.width2_s << "\n";
|
||||
o << "trigger_timeout = " << state.trigger.timeout_s << "\n";
|
||||
o << "trigger_pre = " << state.trigger.pretrigger_pct << "\n";
|
||||
o << "trigger_holdoff = " << state.trigger.holdoff_s << "\n";
|
||||
o << "trigger_band = " << state.trigger.noise_band << "\n";
|
||||
o << "trigger_hf = " << state.trigger.hf_fc << "\n";
|
||||
if (!state.comment.empty()) o << "comment = " << toml_escape(state.comment) << "\n";
|
||||
if (state.buffer_mb > 0.0) o << "buffer_mb = " << state.buffer_mb << "\n";
|
||||
o << "relative_time = " << (state.relative_time ? "true" : "false") << "\n";
|
||||
|
||||
Reference in new issue
Block a user