ui: each pane carries its own switch, the same one the header bar has

The trigger pane and the cursors pane now have a `GtkSwitch` in their heading, and it is *one control in
two places* with the header bar's button: both run the same action (`run_app_action()`), and
`sync_panel_switches()` re-reads each pair from the toolbar's state after any change, so clicking either
end moves both. A pane whose feature is off is disabled, which is exactly the state you need the switch
in, so it belongs *in* the pane rather than only in the toolbar.

Three things it needed:

- **The sensitive target moved to the pane's body.** A control inside an insensitive parent cannot be
  clicked, so a switch that greyed out with its own pane could never turn it back on. `on_trigger_toggled`
  and `on_cursors_toggled` now disable `trigger_body` / `cursors_body`, and so does the startup default;
  the headings, with their switches, stay live.
- **The trigger's heading moved out of `trigger-panel.ui`** and is built in `build_trigger_panel()`,
  which returns the heading row and the panel together. It has to be a sibling of the body for the switch
  to stay usable, and keeping it in the file would have meant the file owning a widget the code must
  arrange. The file's own panel is unchanged otherwise, so the `[trigger panel:]` counts still read.
- **A guard on the sync.** Setting a `GtkSwitch` emits `notify::active` for a programmatic write just as
  it does for a click, so without `App::panel_switch_sync` the program's own write would come back as a
  click and flip the feature again.

Readings, both directions in one line:

    trigger panel sensitive before the probe toggled it: 0; … ; pane switches live while their panes are off=1
    panel switches: trigger=1/1 cursors=1/1 drove_toolbar=1  (pane switch / header bar button)

`pane switches live …=1` is the design's point and is read before anything turns either pane on; `0` would
be a switch drawn, greyed and useless. The trigger pair is moved from the *toolbar* end and the cursors
pair from the *pane* end, so the line carries both halves of the sync, and `drove_toolbar=1` is the pane's
own switch having turned the cursors on with the button following. `trigger panel sensitive …=0` is the
body, not the panel.

Verified: the two lines above; `[side-tab-probe] … advanced=4`; `make test-ui` 14 files; zero
`Gtk-CRITICAL` and no missing icon. Docs: the new reading in `probe-readings.md` with what a `0` means,
the invariants bullet for the one-control-in-two-places rule and its two traps, the sidebar bullet's
sensitive target, and the README note on why the switch is in the pane.
This commit is contained in:
Martino Ferrari committed 2026-10-14 14:13:49 +02:00
1 parent d9287ea826
commit cc8335c514
10 files changed
+201 -26

No files matched your search

+4 -1
View File
@@ -2397,7 +2397,10 @@ now — they are listed so they are not mistaken for anything else:
cursors and the ruler for Measure, a sine crossing its trigger level and the channel list
for Signals (the same picture the toolbar's trigger button carries), drawn as a
32x16 pair so both motifs are legible at a glance — with the words kept as each pill's tooltip and
accessible name. The design spec had the switcher showing *titles* and neither page an
accessible name. Each pane's heading also carries its **own switch**, the same control as the
header bar's button — clicking either moves both — because a pane that is *disabled* while its
feature is off is a pane whose controls you would otherwise have to leave to turn it back on. The
design spec had the switcher showing *titles* and neither page an
`icon-name`; `narrow` alone does not do it (libadwaita still draws a label beside the icon,
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
+13 -2
View File
@@ -166,8 +166,9 @@ 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 panel's **sensitivity**, so the panel stays where it
is and the settings you are about to switch back on do not vanish while you look for them. 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
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*
@@ -183,6 +184,16 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
**tooltip and accessible name** from the page's name, then hides the empty label that would otherwise
leave the icon off-centre. An icon-only pill is still a named control for a screen reader, and
`switcher: … pills=2 labels=0 names=2` is the reading for all three facts at once.
- **A pane's heading switch and the header bar's button are one control in two places.** Each pane's
heading (`build_trigger_panel()` builds the trigger's, `cursors-panel.ui` has the cursors' in its
`head`) carries a `GtkSwitch` that runs the *same action* the toolbar button runs, and
`sync_panel_switches()` re-reads each pair from the toolbar's state after any change — so clicking
either end moves both, and `panel switches: trigger=1/1 cursors=1/1` is the reading. Two traps:
a `GtkSwitch` emits `notify::active` for a *programmatic* write as well, so the sync holds a guard
(`App::panel_switch_sync`) or the program's own write comes back as a click; and the switcher must
stay *out* of what the pane disables, because a control inside an insensitive parent cannot be
clicked — a switch that greyed out with its own pane could never turn it back on. That is why the
sensitive target is the pane's **body** (`trigger_body`, `cursors_body`) and not the pane.
- **A list box outlives the children of the pane it sits in, and the teardown order is not yours to
choose.** Tearing the window down finalizes the events pane's buttons, then the list box, and the
list's own `selected-rows-changed` then reaches a handler holding the freed pointers: four
+24 -1
View File
@@ -164,7 +164,7 @@ Two lines of the self-check output are assertions in disguise:
- `[cursors-probe] <action> mode=.. on=D pane_sensitive=D t0=.. t1=.. dt=.. T=[..] lines=N [..]`
(stderr, `UDPSCOPE_PROBE_CURSORS_MS`) — walks `vertical` → `horizontal` → `both` →
`mix`, then moves a marker *through the plot* (`Plot::set_cursor_x(..., Free)`, which is
what a drag does) and reports again. `pane_sensitive` is the cursors *panel's* sensitivity, not a
what a drag does) and reports again. `pane_sensitive` is the cursors *panel body's* sensitivity, not a
revealer's child: with the cursors off the panel stays exactly where it is and its cells blank to `--`,
because a readout whose instruments are off has no sample to quote. **The times must survive every mode change**: they
are the `RowSet`'s one pair, and a mode that parks the markers must not read them back
@@ -1139,6 +1139,29 @@ library measures and draws the rect in tests of its own.
defect this reading exists for, and it was invisible from the frame alone: the file view pinning its
axis from the ring each frame is what swallowed the gesture.
## `panel switches:` and the trigger panel's default sensitivity
```
[udpscope] trigger panel sensitive before the probe toggled it: 0; signal list items=1 selected=0; column visible after sidebar toggle off=1; pane switches live while their panes are off=1
[udpscope] panel switches: trigger=1/1 cursors=1/1 drove_toolbar=1 (pane switch / header bar button; `drove_toolbar` is the pane's own switch having moved the button)
```
Each pane's heading carries its own switch, and it is the *same* control as the header bar's button:
both run the same action and both are re-read from the toolbar's state afterwards, so they cannot
disagree. The two lines are the claim, in both directions:
- **`pane switches live while their panes are off=1`** — read before anything turns either on, because
it is the whole point of putting the switches in the headings: a control inside an insensitive parent
cannot be clicked, so a switch that went insensitive *with* its own pane could never turn it back on.
**`0` is that defect**: the switch is drawn, greyed, and useless.
- **`trigger=` and `cursors=` are `pane switch / header bar button`**, and the pairs must agree. The
trigger's is moved from the *toolbar* end (these steps toggle that button to arm the trigger) and the
cursors' from the *pane* end, so one line carries both directions of the sync. **`drove_toolbar=1`**
is the second direction's assertion: the pane's own switch turned the cursors on and the header bar's
button followed.
- `trigger panel sensitive …=0` is the *panel body*, not the panel: the heading row, with its switch,
stays live. `1` here while trigger mode is off is the pane not being disabled at all.
## `[sidebar-probe]`
```
+49 -6
View File
@@ -4355,6 +4355,39 @@ void on_window_popover_press(GtkGestureClick*, int, double x, double y, gpointer
/// gtk_toggle_button_set_active(), which emits ::toggled and so runs exactly the handler a
/// click would; the non-toggles call their click handlers with a null button, which they
/// already ignore. Split out of the key handler so the probe can fire a row directly.
/// The pane's switches and the header bar's buttons are one control with two places to click, so a
/// change in either re-reads the other. `panel_switch_sync` is the guard: setting a `GtkSwitch` emits
/// its own `notify::active`, and without it the program's write would come back as a click.
void sync_panel_switches(App& app) {
if (app.panel_switch_sync) return;
app.panel_switch_sync = true;
const auto mirror = [](GtkWidget* panel_switch, GtkWidget* toolbar_toggle) {
if (panel_switch == nullptr || toolbar_toggle == nullptr) return;
gtk_switch_set_active(GTK_SWITCH(panel_switch),
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toolbar_toggle)) == TRUE);
};
mirror(app.trigger_switch, app.trigger_toggle);
mirror(app.cursors_switch, app.cursors_toggle);
app.panel_switch_sync = false;
}
/// A pane's own switch drives the *same action* as the header bar's button, so either place reaches
/// the feature down one path, and the sync above is what keeps the two showing the same thing.
void on_panel_switch_toggled(GObject* obj, GParamSpec*, gpointer user) {
App* app = static_cast<App*>(user);
if (app->panel_switch_sync) return;
const bool on = gtk_switch_get_active(GTK_SWITCH(obj)) != FALSE;
const bool is_trigger = app->trigger_switch != nullptr && obj == G_OBJECT(app->trigger_switch);
GtkWidget* toggle = is_trigger ? app->trigger_toggle : app->cursors_toggle;
if (toggle == nullptr) return;
// The action toggles the toolbar button, so it is run only when the two actually disagree -- which
// is what makes this a *set* rather than a second toggle of something already in that state.
if ((gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle)) != FALSE) != on) {
run_app_action(*app, is_trigger ? AppAction::TriggerPanel : AppAction::CursorsPane);
}
sync_panel_switches(*app);
}
void run_app_action(App& app, AppAction action) {
const auto toggle = [](GtkWidget* button) {
if (button == nullptr) return;
@@ -4771,10 +4804,12 @@ 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 panels start insensitive -- stated here by the one place
// that knows the default, rather than left to whichever widget happened to be built first.
gtk_widget_set_sensitive(app->trigger_panel, FALSE);
gtk_widget_set_sensitive(app->cursors_panel, FALSE);
// 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);
gtk_widget_set_sensitive(app->cursors_body, FALSE);
gtk_widget_set_sensitive(app->cursors_reset, FALSE);
if (app->side_stack != nullptr) {
@@ -5685,6 +5720,12 @@ 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] 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",
app.dbg_panel_switch_trigger, app.dbg_toolbar_switch_trigger,
app.dbg_panel_switch_cursors, app.dbg_toolbar_switch_cursors,
app.dbg_panel_drove_toolbar);
std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d names=%d icon=%dx%d "
"size=%dx%d\n",
app.dbg_switcher_policy == 0 ? "narrow" : (app.dbg_switcher_policy == 1 ? "wide" : "?"),
@@ -5855,9 +5896,11 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
app.dbg_badge_frames[0], app.dbg_badge_frames[1], app.dbg_badge_frames[2],
app.dbg_badge_frames[3]);
std::printf("[udpscope] trigger panel sensitive before the probe toggled it: %d; "
"signal list items=%d selected=%d; column visible after sidebar toggle off=%d\n",
"signal list items=%d selected=%d; column visible after sidebar toggle off=%d; "
"pane switches live while their panes are off=%d\n",
app.dbg_trigger_sensitive_default, app.dbg_trigger_items,
app.dbg_trigger_selected, app.dbg_side_column_after_hide);
app.dbg_trigger_selected, app.dbg_side_column_after_hide,
app.dbg_panel_switch_live);
{
// The panel's *frame* read back: the row boxes the `.ui` holds, and that every control the code built
// ended up inside the panel rather than in a row that is not there. A control is created in code and
+9
View File
@@ -38,6 +38,15 @@
<property name="css-classes">flat</property>
</object>
</child>
<child>
<!-- The pane's own switch, the same one the header bar carries: a feature whose pane is
disabled while it is off needs its control *in* the pane, or turning it back on means
finding the toolbar button that greyed it out. `body` is what goes insensitive, not the
panel, so this stays live. -->
<object class="GtkSwitch" id="cursors_switch">
<property name="valign">center</property>
</object>
</child>
</object>
</child>
<child>
+4 -8
View File
@@ -2,8 +2,10 @@
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The trigger panel's frame: the heading, one labelled row per setting, the two buttons that run it, and
the search block.
The trigger panel's frame: one labelled row per setting, the two buttons that run it, and the search
block. Its *heading* is not here: the heading row carries the switch that turns trigger mode on and
off, and this panel's body is what goes insensitive when it is off, so the heading has to be its
sibling rather than a child of it (`build_trigger_panel()` returns the two together).
Every row here is a label and a place for a control, and the control itself is built in code. A
dropdown's model and a spin button's range are the *application's* (the condition list, the level
@@ -24,12 +26,6 @@
<property name="margin-bottom">8</property>
<property name="margin-start">8</property>
<property name="margin-end">8</property>
<child>
<object class="GtkLabel" id="trigger_heading">
<property name="label" translatable="yes">Trigger</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkBox" id="signal_row">
<property name="orientation">horizontal</property>
+26
View File
@@ -227,6 +227,18 @@ struct App {
/// is what it costs.
int dbg_switcher_w = -1;
int dbg_switcher_h = -1;
/// The panes' own switches and the header bar's buttons, read together by the exit probe: the two
/// must agree, and `dbg_panel_drove_toolbar` is the pane's switch having moved the button.
int dbg_panel_switch_trigger = -1;
int dbg_toolbar_switch_trigger = -1;
int dbg_panel_switch_cursors = -1;
int dbg_toolbar_switch_cursors = -1;
int dbg_panel_drove_toolbar = -1;
/// Both panes' switches being *sensitive* while their panes are off -- read before anything turns
/// one on. It is the whole point of putting them in the heading: a control inside an insensitive
/// parent cannot be clicked, so a switch that went insensitive with its own pane could never turn
/// it back on.
int dbg_panel_switch_live = -1;
int dbg_switcher_policy = -1;
/// Whether `name_side_tabs()` has named the pills: it can only run once the switcher is mapped.
bool side_tabs_named = false;
@@ -236,6 +248,16 @@ struct App {
GtkWidget* cursors_reset = nullptr;
/// The cursors panel's root, kept because cursors-off disables it rather than hiding it.
GtkWidget* cursors_panel = nullptr;
/// The panes' *own* switches, in their header rows, and the panel bodies those switches disable.
/// The switch and the header bar's button are one control with two places to click: each runs the
/// same action, and `sync_panel_switches()` re-reads both from the toolbar's state after any
/// change, so they cannot disagree.
GtkWidget* trigger_switch = nullptr;
GtkWidget* cursors_switch = nullptr;
GtkWidget* trigger_body = nullptr;
/// True while `sync_panel_switches()` is writing the switches: setting a `GtkSwitch` emits its own
/// `notify::active`, and without this the program's own write would come back as a click.
bool panel_switch_sync = false;
/// The cursors pane: listing what the instruments read -- T0/T1/dT always, per row an Y1/Y2/dY
/// where rulers are up, and in `mix` mode the value each cursor crosses each trace at.
GtkWidget* cursors_mode_drop = nullptr;
@@ -1388,6 +1410,10 @@ void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user);
void on_side_tab_changed(GObject*, GParamSpec*, gpointer user);
void select_side_tab(App& app, const char* name);
/// Reads each pane's own switch back off the header bar's button, and the other way round: the two are
/// one control in two places, and `panel_switch_sync` keeps the write from looking like a click.
void sync_panel_switches(App& app);
void on_panel_switch_toggled(GObject* obj, GParamSpec*, gpointer user);
void update_side_visibility(App& app);
void on_sidebar_toggled(GtkToggleButton* button, gpointer user);
void rebuild_signal_panels(App& app);
+12 -2
View File
@@ -96,6 +96,12 @@ GtkWidget* build_cursors_panel(App& app) {
app.cursors_mode_drop = GTK_WIDGET(gtk_builder_get_object(ui, "mode_drop"));
app.cursors_reset = GTK_WIDGET(gtk_builder_get_object(ui, "reset"));
app.cursors_body = GTK_WIDGET(gtk_builder_get_object(ui, "body"));
app.cursors_switch = GTK_WIDGET(gtk_builder_get_object(ui, "cursors_switch"));
if (app.cursors_switch != nullptr) {
gtk_widget_set_tooltip_text(app.cursors_switch,
"the cursors, the same switch the header bar carries");
g_signal_connect(app.cursors_switch, "notify::active", G_CALLBACK(on_panel_switch_toggled), &app);
}
if (box == nullptr || app.cursors_mode_drop == nullptr || app.cursors_reset == nullptr ||
app.cursors_body == nullptr) {
// A missing id is a mistake in the `.ui`. The panel comes up empty rather than taking the window
@@ -129,15 +135,19 @@ void on_cursors_toggled(GtkToggleButton* button, gpointer user) {
const bool on = gtk_toggle_button_get_active(button);
app->rows->set_cursors_visible(on);
// Disabled, not hidden, and the same for the reset beside it: with the cursors off there is
// nothing to reset, but the button must stay where the user will look for it.
// nothing to reset, but the button must stay where the user will look for it. The *body* goes
// insensitive and not the panel, because the panel's header carries the switch that turns the
// cursors back on -- and a control inside an insensitive parent cannot be clicked.
gtk_widget_set_sensitive(app->cursors_reset, on);
if (app->cursors_panel != nullptr) gtk_widget_set_sensitive(app->cursors_panel, on);
if (app->cursors_body != nullptr) gtk_widget_set_sensitive(app->cursors_body, on);
if (!on) update_cursors_pane(*app);
if (on) {
app->rows->reset_cursors();
rebuild_cursors_pane(*app);
update_cursors_pane(*app);
}
// The pane's own switch shows the same state, whichever of the two was clicked.
sync_panel_switches(*app);
}
void on_cursors_reset(GtkButton*, gpointer user) {
+29 -3
View File
@@ -85,6 +85,28 @@ gboolean exit_probe_step3(gpointer user) {
a->dbg_switcher_w = gtk_widget_get_width(a->side_switcher);
a->dbg_switcher_h = gtk_widget_get_height(a->side_switcher);
}
// The panes' own switches, and *both* directions of the sync: the header bar's trigger button is
// flipped here and the pane's switch has to follow, then the cursors' switch is driven from the
// pane and the button is what has to follow. Both are read back after the fact, so the two pairs in
// the dump are the claim: they agree, having been moved from opposite ends.
if (a->trigger_toggle != nullptr) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
}
a->dbg_panel_switch_trigger =
a->trigger_switch != nullptr && gtk_switch_get_active(GTK_SWITCH(a->trigger_switch)) ? 1 : 0;
a->dbg_toolbar_switch_trigger =
a->trigger_toggle != nullptr &&
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a->trigger_toggle))
? 1 : 0;
if (a->cursors_switch != nullptr) {
gtk_switch_set_active(GTK_SWITCH(a->cursors_switch), TRUE);
a->dbg_panel_switch_cursors = gtk_switch_get_active(GTK_SWITCH(a->cursors_switch)) ? 1 : 0;
a->dbg_toolbar_switch_cursors =
a->cursors_toggle != nullptr &&
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a->cursors_toggle))
? 1 : 0;
a->dbg_panel_drove_toolbar = a->dbg_toolbar_switch_cursors;
}
for (GtkWidget* pill = a->side_switcher != nullptr ? gtk_widget_get_first_child(a->side_switcher)
: nullptr;
pill != nullptr; pill = gtk_widget_get_next_sibling(pill)) {
@@ -116,7 +138,7 @@ gboolean exit_probe_step3(gpointer user) {
const int on_signals = page != nullptr && std::strcmp(page, "signals") == 0 ? 1 : 0;
a->dbg_signal_page_current =
on_signals && gtk_widget_get_first_child(a->signal_list) != nullptr ? 1 : 0;
a->dbg_trigger_sensitive = on_signals && gtk_widget_get_sensitive(a->trigger_panel) ? 1 : 0;
a->dbg_trigger_sensitive = on_signals && gtk_widget_get_sensitive(a->trigger_body) ? 1 : 0;
// `gtk_widget_get_visible` on the sidebar's GtkBox is always 1: AdwOverlaySplitView hides its
// sidebar child via `gtk_widget_set_child_visible()`, not `gtk_widget_set_visible()`. The useful
// reading is whether the split view is currently showing the sidebar panel.
@@ -2299,7 +2321,11 @@ gboolean export_probe(gpointer user) {
gboolean exit_probe_step1(gpointer user) {
App* a = static_cast<App*>(user);
// Record the panel states before the toggles are exercised.
a->dbg_trigger_sensitive_default = gtk_widget_get_sensitive(a->trigger_panel) ? 1 : 0;
a->dbg_trigger_sensitive_default = gtk_widget_get_sensitive(a->trigger_body) ? 1 : 0;
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)
? 1 : 0;
if (a->trigger_toggle != nullptr) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
}
@@ -2342,7 +2368,7 @@ void cursors_probe_report(App& a, const char* what) {
"[cursors-probe] %-14s mode=%-10s on=%d pane_sensitive=%d t0=%.6f t1=%.6f dt=%.6f "
"T=[%s] lines=%d marks=%d [%s]\n",
what, kModes[mode], a.rows->cursors_visible() ? 1 : 0,
a.cursors_panel != nullptr && gtk_widget_get_sensitive(a.cursors_panel) ? 1 : 0,
a.cursors_body != nullptr && gtk_widget_get_sensitive(a.cursors_body) ? 1 : 0,
a.rows->cursor_a_x(), a.rows->cursor_b_x(),
a.rows->cursor_b_x() - a.rows->cursor_a_x(), t_text.c_str(), n,
a.rows->crossings_visible(), lines.c_str());
+31 -3
View File
@@ -196,14 +196,18 @@ void on_trigger_toggled(GtkToggleButton* button, gpointer user) {
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.
gtk_widget_set_sensitive(app->trigger_panel, on);
// 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 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
// `sync_events_availability()`'s call and not the trigger's alone.
sync_events_availability(*app);
gtk_button_set_label(GTK_BUTTON(app->arm_button), on ? "Disarm" : "Arm");
// The pane's own switch shows the same state, whichever of the two was clicked.
sync_panel_switches(*app);
}
/// The way out of trigger mode, from inside the panel that froze the view: it is
@@ -236,6 +240,8 @@ GtkWidget* build_trigger_panel(App& app) {
GtkBuilder* ui = ui_load(app, "/org/udpscope/ui/trigger-panel.ui");
GtkWidget* box = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_panel"));
if (box == nullptr) return gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
// The *body* is what trigger-off disables; this panel's index into it is `trigger_body`.
app.trigger_body = box;
app.trigger_signal_drop = gtk_drop_down_new(G_LIST_MODEL(gtk_string_list_new(nullptr)), nullptr);
g_signal_connect(app.trigger_signal_drop, "notify::selected",
@@ -422,7 +428,29 @@ GtkWidget* build_trigger_panel(App& app) {
&app);
}
sync_trigger_rows(app);
return box;
// The heading row, and the switch that is the *same* control as the header bar's button. Built here
// rather than in the file because it must not go insensitive with the body: turning trigger mode
// back on would otherwise mean finding the toolbar button that greyed the panel out.
GtkWidget* head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_margin_top(head, 8);
gtk_widget_set_margin_start(head, 8);
gtk_widget_set_margin_end(head, 8);
GtkWidget* title = gtk_label_new("Trigger");
gtk_widget_set_halign(title, GTK_ALIGN_START);
gtk_widget_set_hexpand(title, TRUE);
gtk_box_append(GTK_BOX(head), title);
app.trigger_switch = gtk_switch_new();
gtk_widget_set_valign(app.trigger_switch, GTK_ALIGN_CENTER);
gtk_widget_set_tooltip_text(app.trigger_switch,
"trigger mode, the same switch the header bar carries");
g_signal_connect(app.trigger_switch, "notify::active", G_CALLBACK(on_panel_switch_toggled), &app);
gtk_box_append(GTK_BOX(head), app.trigger_switch);
GtkWidget* outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_box_append(GTK_BOX(outer), head);
gtk_box_append(GTK_BOX(outer), box);
return outer;
}
} // namespace ui_impl