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
+64
-5
@@ -982,6 +982,18 @@ void rebuild_trigger_signals(App& app) {
|
||||
g_object_unref(list);
|
||||
|
||||
TriggerConfig& tc = app.model->trigger();
|
||||
// A session stores the trigger's channel by *name*: resolved here, where the acquired channels
|
||||
// exist. The pending name is cleared either way, so a name this stream does not have falls through
|
||||
// to the default below rather than being retried on every rebuild.
|
||||
if (!app.pending_trigger_channel.empty()) {
|
||||
for (std::size_t c = 0; c < channels.size(); ++c) {
|
||||
if (channels[c].name == app.pending_trigger_channel) {
|
||||
tc.channel = static_cast<int>(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
app.pending_trigger_channel.clear();
|
||||
}
|
||||
if (tc.channel < 0 && !channels.empty()) tc.channel = 0;
|
||||
const int sel = tc.channel;
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_signal_drop),
|
||||
@@ -4388,6 +4400,39 @@ void on_panel_switch_toggled(GObject* obj, GParamSpec*, gpointer user) {
|
||||
sync_panel_switches(*app);
|
||||
}
|
||||
|
||||
/// Both panes' modes, applied together: the trigger's body against its summary line, the two [edit]
|
||||
/// buttons against what they would do, and the signal list rebuilt because the lock decides what a row
|
||||
/// carries. Idempotent, so a caller only flips a flag and calls this.
|
||||
void apply_pane_lock(App& app) {
|
||||
if (app.trigger_body != nullptr) gtk_widget_set_visible(app.trigger_body, !app.trigger_locked);
|
||||
if (app.trigger_summary != nullptr) {
|
||||
gtk_widget_set_visible(app.trigger_summary, app.trigger_locked);
|
||||
}
|
||||
const auto edit_button = [](GtkWidget* button, bool locked, const char* what) {
|
||||
if (button == nullptr) return;
|
||||
gtk_button_set_icon_name(GTK_BUTTON(button),
|
||||
locked ? "emblem-system-symbolic" : "emblem-ok-symbolic");
|
||||
const std::string tip = std::string(locked ? "configure " : "keep this configuration of ") + what;
|
||||
gtk_widget_set_tooltip_text(button, tip.c_str());
|
||||
};
|
||||
edit_button(app.trigger_edit, app.trigger_locked, "the trigger");
|
||||
edit_button(app.signals_edit, app.signals_locked, "the signals");
|
||||
sync_trigger_summary(app);
|
||||
// The list, because the lock is what decides whether a row carries a tick, a menu and the `+`.
|
||||
if (app.signal_list != nullptr) rebuild_signal_panels(app);
|
||||
}
|
||||
|
||||
/// Either pane's [edit] button: one handler, because the two differ only in which flag they flip.
|
||||
void on_pane_edit_clicked(GtkButton* button, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
if (GTK_WIDGET(button) == app->trigger_edit) {
|
||||
app->trigger_locked = !app->trigger_locked;
|
||||
} else if (GTK_WIDGET(button) == app->signals_edit) {
|
||||
app->signals_locked = !app->signals_locked;
|
||||
}
|
||||
apply_pane_lock(*app);
|
||||
}
|
||||
|
||||
void run_app_action(App& app, AppAction action) {
|
||||
const auto toggle = [](GtkWidget* button) {
|
||||
if (button == nullptr) return;
|
||||
@@ -5677,13 +5722,23 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
// list the file gives it. A `.ui` that lost one of them reads here -- `labels=1` or `list=0` -- rather
|
||||
// than showing as a panel quietly missing its hint.
|
||||
GtkWidget* panel = app.signal_list != nullptr ? gtk_widget_get_parent(app.signal_list) : nullptr;
|
||||
// Counted over the panel's *subtree*, not its direct children: the heading lives in a heading row
|
||||
// beside the pane's [edit] button, and a count of children would read 1 whatever the file lost.
|
||||
int labels = 0;
|
||||
int lists = 0;
|
||||
for (GtkWidget* c = panel != nullptr ? gtk_widget_get_first_child(panel) : nullptr; c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
if (GTK_IS_LABEL(c)) ++labels;
|
||||
if (GTK_IS_LIST_BOX(c)) ++lists;
|
||||
}
|
||||
const std::function<void(GtkWidget*)> count = [&](GtkWidget* w) {
|
||||
// The list stops the walk: its rows are the code's, and this reading is about the file's frame.
|
||||
if (GTK_IS_LIST_BOX(w)) {
|
||||
++lists;
|
||||
return;
|
||||
}
|
||||
if (GTK_IS_LABEL(w)) ++labels;
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
count(c);
|
||||
}
|
||||
};
|
||||
if (panel != nullptr) count(panel);
|
||||
// The list's shape, beside the frame: `rows` is what the list holds, `signals` the streamed
|
||||
// ones, and the `MATH` divider plus the math channels account for the rest -- so
|
||||
// `rows == signals + 1 + math` is the invariant, and `divider=0` is a list that lost its `+`.
|
||||
@@ -5720,6 +5775,10 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
app.dbg_trigger_sensitive, app.dbg_switcher_visible,
|
||||
rows.cursors_visible() ? 1 : 0, rows.time_window(),
|
||||
app.window_limit_s);
|
||||
std::printf("[udpscope] panes: trigger=%s signals=%s body_visible=%d summary='%s'\n",
|
||||
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] 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",
|
||||
|
||||
Reference in new issue
Block a user