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:
1 parent
e10a92fc52
commit
44d2fd35dc
11 files changed
+227
-232
No files matched your search
+72
-104
@@ -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`,
|
||||
|
||||
Reference in new issue
Block a user