ui: the sidebar is two tabs, and a toggle disables its pane

Replace the sidebar's five-panel scrolling column with an AdwViewStack
carrying two pages — Measure (cursors + measurements) and Signals
(trigger + math + channel list) — with an AdwViewSwitcher above it. The
trigger and cursors toggles change from "reveal/hide my pane" to
"enable/disable my feature, leaving the pane insensitive", following the
disable-not-hide principle. The keys probe loses a duplicate registration
that was toggling the sidebar back on before the exit probe captured its
state; the exit-probe captures now read adw_overlay_split_view_get_show_
sidebar() instead of gtk_widget_get_visible(), which AdwOverlaySplitView
never changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Sonnet 4.6 committed 2026-10-14 11:11:21 +02:00
1 parent e10a92fc52
commit 44d2fd35dc
11 files changed
+227 -232

No files matched your search

+72 -104
View File
@@ -2824,11 +2824,28 @@ void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer
open_row_menu(*app, row, x, y, widget);
}
void on_side_tab_changed(GObject* o, GParamSpec*, gpointer user) {
App* app = static_cast<App*>(user);
const char* name = adw_view_stack_get_visible_child_name(ADW_VIEW_STACK(o));
app->side_tab = name != nullptr ? name : "signals";
// Not while the layout is being applied: the restore would schedule a save of what it just read.
if (!app->updating_panels) schedule_layout_save(*app);
}
void select_side_tab(App& app, const char* name) {
if (app.side_stack == nullptr || name == nullptr) return;
// An unknown name leaves the stack where it is -- a session file from a later version must not
// be able to blank the sidebar.
if (adw_view_stack_get_child_by_name(ADW_VIEW_STACK(app.side_stack), name) == nullptr) return;
adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(app.side_stack), name);
app.side_tab = name;
}
/// The side column is on when it has something to show *and* the user's sidebar
/// button is in; the two panel buttons appear as soon as a CONFIG describes any
/// signal at all.
void update_side_visibility(App& app) {
if (app.side_scroll == nullptr) return;
if (app.side_stack == nullptr) return;
const bool have_signals = !app.model->signals().empty();
// `have_signals` decides whether the sidebar *can* be on; `sidebar_wanted` is the user's last word,
// which is why it is kept apart from the button's own state -- that state follows the panel now, and
@@ -2845,7 +2862,6 @@ void update_side_visibility(App& app) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.sidebar_toggle), want_column);
app.sidebar_syncing = false;
}
gtk_widget_set_visible(app.signal_toggle, have_signals);
gtk_widget_set_visible(app.trigger_toggle, have_signals);
}
@@ -2932,15 +2948,15 @@ void note_panel_controls(App& app) {
const auto in_cursors_pane = [&app](GtkWidget* w) {
for (GtkWidget* p = w != nullptr ? gtk_widget_get_parent(w) : nullptr; p != nullptr;
p = gtk_widget_get_parent(p)) {
if (p == app.cursors_revealer) return 1;
if (p == app.cursors_panel) return 1;
}
return 0;
};
// `HeaderBar::pack_end()` parents into an internal box, so the bar is a level up, not the parent.
char text[256];
std::snprintf(text, sizeof(text),
"cursors=%d measurements=%d math=%d in the toolbar, reset=%d in the cursors pane",
in_bar(app.cursors_toggle), in_bar(app.measures_toggle), in_bar(app.math_toggle),
"cursors=%d in the toolbar, switcher=%d, reset=%d in the cursors pane",
in_bar(app.cursors_toggle), app.side_switcher != nullptr ? 1 : 0,
in_cursors_pane(app.cursors_reset));
// ... and the order the bar actually shows, which is what "the menu should be last" is about. A
// widget tree walked in child order *is* the layout order, so this is the reading order of the
@@ -2948,8 +2964,7 @@ void note_panel_controls(App& app) {
const std::pair<GtkWidget*, const char*> named[] = {
{app.trigger_toggle, "trigger"}, {app.events_toggle, "events"},
{app.pause_button, "pause"},
{app.cursors_toggle, "cursors"}, {app.measures_toggle, "measures"},
{app.math_toggle, "math"}, {app.sidebar_toggle, "sidebar"},
{app.cursors_toggle, "cursors"}, {app.sidebar_toggle, "sidebar"},
{app.menu_button, "menu"},
};
// The widget tree's child order, which *is* the visual order left to right: a box lays its children
@@ -4316,12 +4331,8 @@ void run_app_action(App& app, AppAction action) {
case AppAction::CursorsPane:
toggle(app.cursors_toggle);
break;
case AppAction::MeasuresPane:
toggle(app.measures_toggle);
break;
case AppAction::MathPane:
toggle(app.math_toggle);
break;
case AppAction::SideTabMeasure: select_side_tab(app, "measure"); break;
case AppAction::SideTabSignals: select_side_tab(app, "signals"); break;
case AppAction::LayoutEditor:
toggle(app.layout_toggle);
break;
@@ -4449,11 +4460,6 @@ GtkWidget* build_toolbar(App& app) {
g_signal_connect(app.layout_toggle, "toggled", G_CALLBACK(on_layout_editor_toggled), &app);
gtk_box_append(GTK_BOX(menu_box), app.layout_toggle);
app.signal_toggle = menu_toggle("Signal list", nullptr, true);
gtk_widget_set_tooltip_text(app.signal_toggle, "show or hide the signal list");
g_signal_connect(app.signal_toggle, "toggled", G_CALLBACK(on_signals_toggled), &app);
gtk_box_append(GTK_BOX(menu_box), app.signal_toggle);
gtk_box_append(GTK_BOX(menu_box), menu_separator());
GtkWidget* export_btn = menu_button("Export visible range...", "Ctrl+E");
@@ -4511,12 +4517,10 @@ GtkWidget* build_toolbar(App& app) {
app.trigger_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_toggle"));
app.events_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "events_toggle"));
app.pause_button = GTK_WIDGET(gtk_builder_get_object(ui, "pause_button"));
app.measures_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "measures_toggle"));
app.math_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "math_toggle"));
app.cursors_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "cursors_toggle"));
if (app.menu_button == nullptr || app.sidebar_toggle == nullptr || app.trigger_toggle == nullptr ||
app.events_toggle == nullptr || app.pause_button == nullptr || app.measures_toggle == nullptr ||
app.math_toggle == nullptr || app.cursors_toggle == nullptr) {
app.events_toggle == nullptr || app.pause_button == nullptr ||
app.cursors_toggle == nullptr) {
// A missing id is a mistake in the `.ui`: the bar comes up with the controls it got rather than taking
// the window with it, and `[udpscope] header:` reads whatever geometry is left.
return bar;
@@ -4538,24 +4542,6 @@ GtkWidget* build_toolbar(App& app) {
}),
&app);
g_signal_connect(app.pause_button, "toggled", G_CALLBACK(on_pause_toggled), &app);
g_signal_connect(app.measures_toggle, "toggled",
G_CALLBACK(+[](GtkToggleButton* b, gpointer user) {
App* a = static_cast<App*>(user);
a->measures_revealed = gtk_toggle_button_get_active(b);
if (a->measures_revealer != nullptr) {
gtk_revealer_set_reveal_child(GTK_REVEALER(a->measures_revealer),
a->measures_revealed);
}
}),
&app);
g_signal_connect(app.math_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* b, gpointer user) {
App* a = static_cast<App*>(user);
if (a->math_revealer != nullptr) {
gtk_revealer_set_reveal_child(GTK_REVEALER(a->math_revealer),
gtk_toggle_button_get_active(b));
}
}),
&app);
g_signal_connect(app.cursors_toggle, "toggled", G_CALLBACK(on_cursors_toggled), &app);
return bar;
}
@@ -4579,14 +4565,13 @@ void on_activate(GtkApplication* application, gpointer user) {
app->status_badge = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_badge"));
app->status_label = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_label"));
app->status_info_button = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_info"));
app->side_scroll = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_scroll"));
GtkWidget* side = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_column"));
// A missing id is the one mistake neither the build nor `GtkBuilder` sees, and every use below would
// be a null pointer.
if (app->window == nullptr || side_view == nullptr || events_divider == nullptr ||
app->plot_box == nullptr || app->plot_split == nullptr || app->status_badge == nullptr ||
app->status_label == nullptr || app->status_info_button == nullptr ||
app->side_scroll == nullptr || side == nullptr) {
side == nullptr) {
return;
}
// The window's actions live on the application, so it has to know which one it belongs to.
@@ -4714,59 +4699,46 @@ void on_activate(GtkApplication* application, gpointer user) {
gtk_popover_set_has_arrow(GTK_POPOVER(app->status_popover), TRUE);
}
// The right-hand side: two independently collapsible panels, each revealed by
// its toolbar toggle. No outer scrolled window -- the signal list scrolls
// inside its own panel -- so neither panel can be squeezed to nothing by the
// other's size request.
// `side` is the sidebar column from the `.ui`, resolved above with the rest of the skeleton; the
// app keeps it because the panel geometry, the divide and the revealers all ask about it.
// The right-hand side is two tabs, not five stacked panels: one tab is on screen at a time, so
// no section competes with four others for height. Each page scrolls on its own -- the two are
// different heights, and one shared scroller would leave a switch scrolled where the new page
// never asked to be.
// `side` is `side_column` from the `.ui`, resolved above with the rest of the skeleton; the app
// keeps it because `update_side_visibility()` still asks about the column as a whole.
app->side_box = side;
app->side_stack = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_stack"));
app->side_switcher = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_switcher"));
GtkWidget* measure_page = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "measure_page"));
GtkWidget* signals_page = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "signals_page"));
// The two panels share a vertical box, so they collapse *vertically*: a
// horizontal slide would animate the width and leave the panel's full height
// reserved -- an invisible gap pushing the other panel down the column.
app->trigger_revealer = gtk_revealer_new();
gtk_revealer_set_transition_type(GTK_REVEALER(app->trigger_revealer),
GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
// Measure: what the cursors bracket, and the figures derived from it.
app->cursors_panel = build_cursors_panel(*app);
gtk_box_append(GTK_BOX(measure_page), app->cursors_panel);
gtk_box_append(GTK_BOX(measure_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
gtk_box_append(GTK_BOX(measure_page), build_measures_panel(*app));
// Signals: the trigger picks a channel from the list beside it.
app->trigger_panel = build_trigger_panel(*app);
gtk_revealer_set_child(GTK_REVEALER(app->trigger_revealer), app->trigger_panel);
gtk_box_append(GTK_BOX(side), app->trigger_revealer);
gtk_box_append(GTK_BOX(side), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
// The cursors readout sits with the other panels and is revealed by its toggle: the
// instruments are on the plot, but what they read has to be somewhere a value can be
// selected and copied from.
app->cursors_revealer = gtk_revealer_new();
gtk_revealer_set_transition_type(GTK_REVEALER(app->cursors_revealer),
GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
gtk_revealer_set_child(GTK_REVEALER(app->cursors_revealer), build_cursors_panel(*app));
gtk_box_append(GTK_BOX(side), app->cursors_revealer);
gtk_box_append(GTK_BOX(side), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
app->measures_revealer = gtk_revealer_new();
gtk_revealer_set_transition_type(GTK_REVEALER(app->measures_revealer),
GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
gtk_revealer_set_child(GTK_REVEALER(app->measures_revealer), build_measures_panel(*app));
gtk_box_append(GTK_BOX(side), app->measures_revealer);
gtk_box_append(GTK_BOX(side), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
app->math_revealer = gtk_revealer_new();
gtk_revealer_set_transition_type(GTK_REVEALER(app->math_revealer),
GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
gtk_revealer_set_child(GTK_REVEALER(app->math_revealer), build_math_panel(*app));
gtk_box_append(GTK_BOX(side), app->math_revealer);
gtk_box_append(GTK_BOX(side), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
gtk_box_append(GTK_BOX(signals_page), app->trigger_panel);
gtk_box_append(GTK_BOX(signals_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
// Still on the signals page until the list grows the `+` and the per-row menu that replace it.
gtk_box_append(GTK_BOX(signals_page), build_math_panel(*app));
gtk_box_append(GTK_BOX(signals_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
rebuild_math_pane(*app);
app->signal_panel = build_signal_panel(*app);
app->signal_revealer = gtk_revealer_new();
gtk_revealer_set_transition_type(GTK_REVEALER(app->signal_revealer),
GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
gtk_revealer_set_child(GTK_REVEALER(app->signal_revealer), app->signal_panel);
gtk_revealer_set_reveal_child(GTK_REVEALER(app->signal_revealer), TRUE);
gtk_box_append(GTK_BOX(side), app->signal_revealer);
// Its size request and the scroller around it are the `.ui`'s: the column scrolls as a whole, so a
// long signal list or a revealed trigger panel can never push either panel out of the window.
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);
gtk_widget_set_sensitive(app->cursors_reset, FALSE);
if (app->side_stack != nullptr) {
adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(app->side_stack), "signals");
g_signal_connect(app->side_stack, "notify::visible-child-name",
G_CALLBACK(on_side_tab_changed), app);
}
// The left side is the events pane *and* the plot, in one row: the pane is a column of its own
// (the item's "collapsable pane on the left"), revealed by trigger mode.
@@ -5494,9 +5466,6 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
if (const char* env = g_getenv("UDPSCOPE_PROBE_WINDOWPOP_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), windowpop_probe_step, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_KEYS_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), keys_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_COLOR_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), color_probe, &app);
}
@@ -5604,7 +5573,7 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
std::printf("[udpscope] axes.w changes: %d (last at frame %d, side panel up=%d)\n",
app.dbg_axes_w_changes, app.dbg_axes_w_change_frame,
app.dbg_axes_w_change_side);
std::printf("[udpscope] reset button visible: %d\n", app.dbg_reset_visible);
std::printf("[udpscope] reset button sensitive: %d\n", app.dbg_reset_sensitive);
std::printf("[udpscope] render blank frames: %d of %d painted\n",
app.dbg_render_blank_frames, app.dbg_frames_painted);
std::printf("[udpscope] traceless paints: %d (%d after the first bind, last at paint %d)\n",
@@ -5671,12 +5640,12 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
inside(app.events_list, app.events_revealer));
}
std::printf("[udpscope] panels: side=%dx%d (visible=%d) signals=%dx%d row_h=%d "
"(revealed=%d) trigger=%dx%d (revealed=%d) toggles=%d cursors=%d window=%.3fs "
"(page_current=%d) trigger=%dx%d (sensitive=%d) switcher=%d cursors=%d window=%.3fs "
"window_cap=%.1fs\n",
app.dbg_side_w, app.dbg_side_h, app.dbg_side_visible, app.dbg_signal_w,
app.dbg_signal_h, app.dbg_signal_row_h, app.dbg_signal_revealed,
app.dbg_signal_h, app.dbg_signal_row_h, app.dbg_signal_page_current,
app.dbg_trigger_w, app.dbg_trigger_h,
app.dbg_trigger_revealed, app.dbg_toggles_visible,
app.dbg_trigger_sensitive, app.dbg_switcher_visible,
rows.cursors_visible() ? 1 : 0, rows.time_window(),
app.window_limit_s);
std::printf("[udpscope] mode with trigger on: %s\n", app.dbg_mode_triggered.c_str());
@@ -5841,11 +5810,10 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
std::printf("[udpscope] badge frames: lit=%d dim=%d idle=%d connecting=%d\n",
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 revealed before the probe toggled it: %d; "
"signal list items=%d selected=%d; column visible after sidebar "
"toggle off=%d\n",
app.dbg_trigger_revealed_default, app.dbg_trigger_items,
app.dbg_trigger_selected, app.dbg_side_scroll_after_hide);
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",
app.dbg_trigger_sensitive_default, app.dbg_trigger_items,
app.dbg_trigger_selected, app.dbg_side_column_after_hide);
{
// 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
@@ -6078,9 +6046,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
}
{
std::lock_guard<std::mutex> lock(model.mutex());
std::printf("[udpscope] measures pane: n=%zu revealed=%d marks_shown=%d pane_rows=%zu "
std::printf("[udpscope] measures pane: n=%zu marks_shown=%d pane_rows=%zu "
"pane_w=%d pane_h=%d switch_on=%d reset_sensitive=%d\n",
app.measures.size(), app.measures_revealed ? 1 : 0,
app.measures.size(),
rows.measure_marks_placed(), app.measure_labels.size(),
app.dbg_measures_pane_w, app.dbg_measures_pane_h,
// The two widgets the panel's *frame* is read by: the code drives `measure_stats_on`,