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

+4 -8
View File
@@ -12,14 +12,10 @@ constexpr Accel kAccels[] = {
{GDK_KEY_space, 0, AppAction::PauseToggle, "space", "Pause or resume the display",
"Acquisition"},
{GDK_KEY_a, 0, AppAction::TriggerArm, "a", "Arm or disarm the trigger", "Acquisition"},
{GDK_KEY_t, 0, AppAction::TriggerPanel, "t", "Show or hide the trigger panel",
"Acquisition"},
{GDK_KEY_c, 0, AppAction::CursorsPane, "c", "Show or hide the cursors pane",
"Panes and editors"},
{GDK_KEY_m, 0, AppAction::MeasuresPane, "m", "Show or hide the measurements pane",
"Panes and editors"},
{GDK_KEY_g, 0, AppAction::MathPane, "g", "Show or hide the math pane",
"Panes and editors"},
{GDK_KEY_t, 0, AppAction::TriggerPanel, "t", "Enable or disable trigger mode", "Acquisition"},
{GDK_KEY_c, 0, AppAction::CursorsPane, "c", "Enable or disable the cursors", "Panes and editors"},
{GDK_KEY_m, 0, AppAction::SideTabMeasure, "m", "Show the Measure tab", "Panes and editors"},
{GDK_KEY_g, 0, AppAction::SideTabSignals, "g", "Show the Signals tab", "Panes and editors"},
{GDK_KEY_l, 0, AppAction::LayoutEditor, "l", "Show or hide the layout editor",
"Panes and editors"},
{GDK_KEY_s, 0, AppAction::SidePanel, "s", "Show or hide the side panel",
+2 -2
View File
@@ -10,8 +10,8 @@ enum class AppAction {
TriggerArm,
TriggerPanel,
CursorsPane,
MeasuresPane,
MathPane,
SideTabMeasure,
SideTabSignals,
LayoutEditor,
SidePanel,
Export,
+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`,
+2 -14
View File
@@ -3,7 +3,7 @@
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The header bar: the time-window control's slot, the trigger, pause and events group with the separator that
marks where the *view's* controls end, and the five at the far end.
marks where the *view's* controls end, and the three at the far end.
The bar carries only the controls you reach for *while watching a signal*; everything you set up once lives
in the overflow menu, which is code — its rows act on the `RowSet` directly and two of them are toggles
@@ -11,7 +11,7 @@
The order is the file's, and it is the one thing here that is not self-evident: `pack_end()` lays its
children out right to left, so a `<child type="end">` is added leftwards-from-the-right in document order
and the screen reads the reverse of it — cursors, measurements, math, sidebar, menu.
and the screen reads the reverse of it — cursors, sidebar, menu.
The first start child is a slot and not the control: the time-window control is code (its menu's rows come
from the buffer's limits), and `gtk_header_bar_pack_start()` appends *after* whatever a file has already
@@ -64,18 +64,6 @@
<property name="active">1</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="math_toggle">
<property name="icon-name">udpscope-math-symbolic</property>
<property name="tooltip-text" translatable="yes">channels derived from the others by a graph of operations: gain, absolute value, filters, sums and the rest. An enabled one is acquired, plotted and measured exactly like a signal</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="measures_toggle">
<property name="icon-name">udpscope-measure-symbolic</property>
<property name="tooltip-text" translatable="yes">compute a value over the visible window of a signal, and optionally mark it on the plot</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="cursors_toggle">
<property name="icon-name">udpscope-cursors-symbolic</property>
+58 -13
View File
@@ -3,7 +3,7 @@
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The main window's skeleton: the window, the split view that puts the sidebar beside the plot, the
events pane's divider, the plot column with the status line under it, and the sidebar's scroller.
events pane's divider, the plot column with the status line under it, and the sidebar's tab stack.
The sidebar is an `AdwOverlaySplitView`, which is what a utility sidebar is in GNOME (as against
`AdwNavigationSplitView`, which is for navigation lists). Three things follow from that, and they are
@@ -20,7 +20,7 @@
What is *not* here is as deliberate as what is. Everything this file nests is fixed — the shape of
the window does not depend on the stream, the trigger or the layout — while the subtrees that do are
built by code and put in place: the events pane, into `events_divider`'s start side; the two plot
areas, into the split inside `plot_box`; the sidebar's panels, appended to `side_column`; and the
areas, into the split inside `plot_box`; the sidebar's panels, appended to `measure_page` and `signals_page`; and the
status popover, attached to `status_info`.
The header bar is not here either: it is the window's own titlebar, built by `build_toolbar()`
@@ -48,18 +48,63 @@
<property name="min-sidebar-width">320</property>
<property name="max-sidebar-width">320</property>
<property name="sidebar">
<!-- 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. -->
<object class="GtkScrolledWindow" id="side_scroll">
<property name="hscrollbar-policy">never</property>
<property name="vscrollbar-policy">automatic</property>
<property name="min-content-width">300</property>
<property name="child">
<object class="GtkBox" id="side_column">
<property name="orientation">vertical</property>
<property name="width-request">300</property>
<!-- A switcher above a stack, and a scroller *per page*: the two pages are different
heights, so one shared scroller around the stack would leave a tab switch scrolled
to somewhere the new page never asked for. The switcher is the sibling above, not a
child of either scroller: the one control that chooses what you are looking at must
not be the first thing to scroll away. -->
<object class="GtkBox" id="side_column">
<property name="orientation">vertical</property>
<property name="width-request">300</property>
<child>
<object class="AdwViewSwitcher" id="side_switcher">
<property name="stack">side_stack</property>
<property name="policy">wide</property>
<property name="margin-top">4</property>
<property name="margin-bottom">4</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
</object>
</property>
</child>
<child>
<object class="AdwViewStack" id="side_stack">
<property name="vexpand">1</property>
<child>
<object class="AdwViewStackPage">
<property name="name">measure</property>
<property name="title" translatable="yes">Measure</property>
<property name="child">
<object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property>
<property name="vscrollbar-policy">automatic</property>
<property name="child">
<object class="GtkBox" id="measure_page">
<property name="orientation">vertical</property>
</object>
</property>
</object>
</property>
</object>
</child>
<child>
<object class="AdwViewStackPage">
<property name="name">signals</property>
<property name="title" translatable="yes">Signals</property>
<property name="child">
<object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property>
<property name="vscrollbar-policy">automatic</property>
<property name="child">
<object class="GtkBox" id="signals_page">
<property name="orientation">vertical</property>
</object>
</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
</property>
<property name="content">
+19 -21
View File
@@ -139,18 +139,21 @@ struct App {
/// what we synced is ours -- GTK only notifies on a change, so the user cannot produce it.
int window_sync_unit = -1;
GtkWidget* trigger_toggle = nullptr;
GtkWidget* trigger_revealer = nullptr;
/// The trigger panel's own box, kept so the defaults probe can read its spin buttons without
/// walking the window for them.
GtkWidget* trigger_panel = nullptr;
GtkWidget* signal_toggle = nullptr;
GtkWidget* signal_revealer = nullptr;
GtkWidget* signal_panel = nullptr;
GtkWidget* side_box = nullptr;
/// The sidebar's tab stack and its switcher. The stack is what chooses which panels are on
/// screen; a feature's own on/off never does (it sets its panel's sensitivity instead).
GtkWidget* side_stack = nullptr;
GtkWidget* side_switcher = nullptr;
/// The page name the stack is showing, and the one value the session file persists about the
/// sidebar. "signals" on a first run.
std::string side_tab = "signals";
GtkWidget* arm_button = nullptr;
GtkWidget* trigger_signal_drop = nullptr;
GtkWidget* sidebar_toggle = nullptr;
GtkWidget* side_scroll = nullptr;
GtkWidget* trigger_edge_drop = nullptr;
GtkWidget* trigger_type_drop = nullptr;
GtkWidget* trigger_cmp_drop = nullptr;
@@ -211,10 +214,10 @@ struct App {
/// *titlebar*, not a child of the window, so walking down from the window does not reach it.
GtkWidget* toolbar = nullptr;
GtkWidget* cursors_reset = nullptr;
/// The cursors pane: revealed with the toggle, and 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_revealer = nullptr;
/// The cursors panel's root, kept because cursors-off disables it rather than hiding it.
GtkWidget* cursors_panel = nullptr;
/// 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;
GtkWidget* cursors_body = nullptr;
/// One line per row (rulers) and one per bound trace (`mix`), in the order the body
@@ -237,16 +240,12 @@ struct App {
/// price the frame tick should pay).
std::vector<Measure> measures;
std::vector<MeasureResult> measure_results;
GtkWidget* measures_revealer = nullptr;
GtkWidget* measures_list = nullptr;
GtkWidget* measures_toggle = nullptr;
// --- math channels --------------------------------------------------------
/// The app's copy of the math channels, and the source of truth for the pane. Every change goes
/// through `apply_math_channels()`, which writes this and hands it to the model -- the model's own
/// copy is what the producer evaluates.
std::vector<MathChannel> math;
GtkWidget* math_toggle = nullptr;
GtkWidget* math_revealer = nullptr;
GtkWidget* math_list = nullptr;
/// The math editor's builder. The editor's window belongs to the builder's object list, so the builder is
/// held for as long as the window is up and cleared on the *next* opening rather than with the window --
@@ -358,7 +357,6 @@ struct App {
/// that memory reused and the read is a **SIGSEGV** in the middle of the dump.
int dbg_measures_pane_w = -1;
int dbg_measures_pane_h = -1;
bool measures_revealed = false;
/// The measurement dialog while one is open (null otherwise). A probe needs a handle on it:
/// it is a window of its own, so the main window's widget tree does not reach its buttons.
GtkWidget* measure_dialog = nullptr;
@@ -508,7 +506,6 @@ struct App {
/// True once the renderer's `Style` has been stated (see `tick()`): the renderer does not
/// exist until the area is realized, so the one-shot has to be a tick's job.
bool style_stated = false;
bool signals_panel_initialised = false;
/// Set once the user edits the trigger level, so the panel stops defaulting it
/// into the middle of the declared range.
bool trigger_level_touched = false;
@@ -690,7 +687,7 @@ struct App {
/// over a run that acquired anything -- that is what "it blinks" means, and a badge
/// stuck lit looks identical to a working one in a screenshot.
int dbg_badge_frames[4] = {0, 0, 0, 0};
int dbg_reset_visible = -1;
int dbg_reset_sensitive = -1;
std::vector<std::uint64_t> last_stored;
std::uint64_t dbg_latches_at_disarm = 0;
std::uint64_t dbg_seq_at_disarm = 0;
@@ -738,12 +735,12 @@ struct App {
int dbg_signal_row_h = 0;
int dbg_trigger_w = 0;
int dbg_trigger_h = 0;
int dbg_signal_revealed = 0;
int dbg_trigger_revealed = 0;
int dbg_signal_page_current = 0;
int dbg_trigger_sensitive = 0;
int dbg_side_visible = 0;
int dbg_trigger_items = 0;
int dbg_trigger_selected = -1;
int dbg_side_scroll_after_hide = 1;
int dbg_side_column_after_hide = 1;
std::string dbg_mode_triggered;
std::string dbg_window_text;
double dbg_marker_x = 0.0;
@@ -751,8 +748,8 @@ struct App {
double dbg_trigger_time = 0.0;
int dbg_trigger_time_valid = -1;
std::string dbg_mode_live;
int dbg_toggles_visible = 0;
int dbg_trigger_revealed_default = 0;
int dbg_switcher_visible = 0;
int dbg_trigger_sensitive_default = 0;
/// The detail string the popover and the exit dump read. The dump used to read the label,
/// which no longer carries these fields.
@@ -1336,7 +1333,6 @@ void rebuild_math_pane(App& app);
GtkWidget* build_measures_panel(App& app);
GtkWidget* build_cursors_panel(App& app);
void on_cursors_toggled(GtkToggleButton* button, gpointer user);
void on_signals_toggled(GtkToggleButton* button, gpointer user);
void on_fit_clicked(GtkButton*, gpointer user);
void on_fit_x_clicked(GtkButton*, gpointer user);
void on_cursors_reset(GtkButton*, gpointer user);
@@ -1359,6 +1355,8 @@ void row_menu_fill(RowMenuState& st);
GtkWidget* open_row_menu(App& app, int row, double x, double y, GtkWidget* parent, bool autohide = true);
void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user);
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);
void update_side_visibility(App& app);
void on_sidebar_toggled(GtkToggleButton* button, gpointer user);
void rebuild_signal_panels(App& app);
+17 -8
View File
@@ -14,7 +14,18 @@ namespace ui_impl {
/// `%.6g s` is exact enough for a cursor readout and short enough for the panel.
void update_cursors_pane(App& app) {
if (app.cursors_body == nullptr || !app.rows->cursors_visible()) return;
if (app.cursors_body == nullptr) return;
if (!app.rows->cursors_visible()) {
// A readout quotes a stored sample; with the instruments off there is no sample it quotes, so
// the cells blank rather than keeping the last ones. `"--"` is the same spelling an
// unavailable cell already uses -- two spellings of one state in one grid would be the defect.
for (const App::CursorLine& line : app.cursors_lines) {
if (line.a != nullptr) gtk_label_set_text(GTK_LABEL(line.a), "--");
if (line.b != nullptr) gtk_label_set_text(GTK_LABEL(line.b), "--");
if (line.d != nullptr) gtk_label_set_text(GTK_LABEL(line.d), "--");
}
return;
}
if (app.cursors_pane_mode != static_cast<int>(app.rows->cursor_mode()) ||
app.cursors_pane_rows != app.rows->row_count() ||
app.cursors_pane_generation != app.model->generation() ||
@@ -117,13 +128,11 @@ void on_cursors_toggled(GtkToggleButton* button, gpointer user) {
App* app = static_cast<App*>(user);
const bool on = gtk_toggle_button_get_active(button);
app->rows->set_cursors_visible(on);
// The reset button exists for the cursors: with them hidden there is nothing to
// reset, and a button that does nothing reads as broken.
gtk_widget_set_visible(app->cursors_reset, on);
// The pane is the cursors' readout: with them off there is nothing to read.
if (app->cursors_revealer != nullptr) {
gtk_revealer_set_reveal_child(GTK_REVEALER(app->cursors_revealer), 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.
gtk_widget_set_sensitive(app->cursors_reset, on);
if (app->cursors_panel != nullptr) gtk_widget_set_sensitive(app->cursors_panel, on);
if (!on) update_cursors_pane(*app);
if (on) {
app->rows->reset_cursors();
rebuild_cursors_pane(*app);
+3 -3
View File
@@ -18,11 +18,11 @@ void apply_math_channels(App& app) {
// `set_math_channels()` takes the model's lock itself -- holding it here is a self-deadlock on a
// non-recursive mutex, and it is exactly how the first version of the probe hung the app.
app.model->set_math_channels(app.math);
// The structural rebuild above replaced the buffer and dropped its history. A stream's producer
// refills that; a file's nothing does, so the view is re-read -- and with the math set now in place
// the reload computes the derived channels over the file as it goes.
reload_file_view(app);
rebuild_math_pane(app);
// The math channels are rows of the signal list too -- `append_math_signals_locked()` puts them
// in `signals()` -- so the list is stale until it is rebuilt.
rebuild_signal_panels(app);
schedule_layout_save(app);
}
+45 -43
View File
@@ -74,24 +74,36 @@ gboolean exit_probe_step3(gpointer user) {
a->window = nullptr; // stop the tick before the widgets go
a->dbg_side_w = gtk_widget_get_width(a->side_box);
a->dbg_side_h = gtk_widget_get_height(a->side_box);
a->dbg_signal_w = gtk_widget_get_width(a->signal_revealer);
a->dbg_signal_h = gtk_widget_get_height(a->signal_revealer);
a->dbg_trigger_w = gtk_widget_get_width(a->trigger_revealer);
a->dbg_trigger_h = gtk_widget_get_height(a->trigger_revealer);
a->dbg_signal_w = gtk_widget_get_width(a->signal_panel);
a->dbg_signal_h = gtk_widget_get_height(a->signal_panel);
a->dbg_trigger_w = gtk_widget_get_width(a->trigger_panel);
a->dbg_trigger_h = gtk_widget_get_height(a->trigger_panel);
// The height of one signal row, which is the thing "the channel toggle is too tall"
// was about. The revealer's own height cannot stand in for it: it also covers the
// was about. The panel's own height cannot stand in for it: it also covers the
// heading, the hint and the box margins.
if (GtkWidget* first = gtk_widget_get_first_child(a->signal_list)) {
a->dbg_signal_row_h = gtk_widget_get_height(first);
}
if (a->area != nullptr) a->dbg_frames_painted = static_cast<int>(a->area->frames_painted());
a->dbg_reset_visible = a->cursors_reset != nullptr && gtk_widget_get_visible(a->cursors_reset) ? 1 : 0;
a->dbg_signal_revealed =
gtk_revealer_get_child_revealed(GTK_REVEALER(a->signal_revealer)) ? 1 : 0;
a->dbg_trigger_revealed =
gtk_revealer_get_child_revealed(GTK_REVEALER(a->trigger_revealer)) ? 1 : 0;
a->dbg_side_visible = gtk_widget_get_visible(a->side_box) ? 1 : 0;
a->dbg_side_scroll_after_hide = gtk_widget_get_visible(a->side_scroll) ? 1 : 0;
a->dbg_reset_sensitive = a->cursors_reset != nullptr && gtk_widget_get_sensitive(a->cursors_reset) ? 1 : 0;
// A page, not a revealer: a stack's hidden page measures 0x0 the way an unrevealed revealer
// did, so the reading asks about *state* -- is this page the current one -- and never about size.
const char* page = a->side_stack != nullptr
? adw_view_stack_get_visible_child_name(ADW_VIEW_STACK(a->side_stack))
: nullptr;
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;
// `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.
const int sidebar_shown = a->side_view != nullptr &&
adw_overlay_split_view_get_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(a->side_view)) ? 1 : 0;
a->dbg_side_column_after_hide = sidebar_shown;
a->dbg_switcher_visible =
a->side_switcher != nullptr && gtk_widget_get_visible(a->side_switcher) ? 1 : 0;
a->dbg_side_visible = sidebar_shown;
if (a->trigger_signal_drop != nullptr) {
GListModel* model = gtk_drop_down_get_model(GTK_DROP_DOWN(a->trigger_signal_drop));
a->dbg_trigger_items = model != nullptr ? static_cast<int>(g_list_model_get_n_items(model)) : 0;
@@ -100,9 +112,6 @@ gboolean exit_probe_step3(gpointer user) {
}
// `dbg_window_text` is kept by `sync_window_display()`, the only place that knows the span the
// window control is showing: there is no label to read it back from any more.
a->dbg_toggles_visible =
(gtk_widget_get_visible(a->signal_toggle) && gtk_widget_get_visible(a->trigger_toggle)) ? 1
: 0;
if (window != nullptr) gtk_window_destroy(GTK_WINDOW(window));
return G_SOURCE_REMOVE;
}
@@ -2186,8 +2195,7 @@ 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_revealed_default =
gtk_revealer_get_child_revealed(GTK_REVEALER(a->trigger_revealer)) ? 1 : 0;
a->dbg_trigger_sensitive_default = gtk_widget_get_sensitive(a->trigger_panel) ? 1 : 0;
if (a->trigger_toggle != nullptr) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
}
@@ -2227,13 +2235,10 @@ void cursors_probe_report(App& a, const char* what) {
double my = 0.0;
const bool have_mark = a.rows->crossing_at(0, 0, false, mx, my);
std::fprintf(stderr,
"[cursors-probe] %-14s mode=%-10s on=%d pane=%d t0=%.6f t1=%.6f dt=%.6f "
"[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_revealer != nullptr &&
gtk_revealer_get_reveal_child(GTK_REVEALER(a.cursors_revealer))
? 1
: 0,
a.cursors_panel != nullptr && gtk_widget_get_sensitive(a.cursors_panel) ? 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());
@@ -2498,7 +2503,7 @@ gboolean measures_probe_step(gpointer user) {
m.options.show_on_plot = measure_can_show_on_plot(m.type);
a->measures.push_back(std::move(m));
}
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->measures_toggle), true);
select_side_tab(*a, "measure");
rebuild_measures_pane(*a);
std::fprintf(stderr, "[measures-probe] added %zu measurements on '%s'\n", a->measures.size(),
signal.c_str());
@@ -4027,29 +4032,26 @@ gboolean keys_probe(gpointer user) {
before_after("t: trigger panel", a->trigger_toggle, AppAction::TriggerPanel);
before_after("c: cursors", a->cursors_toggle, AppAction::CursorsPane);
before_after("m: measures", a->measures_toggle, AppAction::MeasuresPane);
before_after("g: math", a->math_toggle, AppAction::MathPane);
{
const std::string before_tab = a->side_tab;
run_app_action(*a, AppAction::SideTabMeasure);
const std::string after_tab = a->side_tab;
char buf[96];
std::snprintf(buf, sizeof(buf), " m: tab %s->%s |", before_tab.c_str(), after_tab.c_str());
line += buf;
}
{
const std::string before_tab = a->side_tab;
run_app_action(*a, AppAction::SideTabSignals);
const std::string after_tab = a->side_tab;
char buf[96];
std::snprintf(buf, sizeof(buf), " g: tab %s->%s |", before_tab.c_str(), after_tab.c_str());
line += buf;
}
before_after("l: layout", a->layout_toggle, AppAction::LayoutEditor);
before_after("s: side panel", a->sidebar_toggle, AppAction::SidePanel);
// An accel must be refused while text is being typed. Focus the window entry, send 'm'
// through the real handler, and require the measurements toggle not to move.
int refused = -1;
if (a->window_entry != nullptr) {
gtk_menu_button_popup(GTK_MENU_BUTTON(a->window_button));
gtk_widget_grab_focus(a->window_entry);
const int before = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a->measures_toggle))
? 1 : 0;
const gboolean handled = on_app_key_pressed(nullptr, GDK_KEY_m, 0,
static_cast<GdkModifierType>(0), a);
const int after = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a->measures_toggle))
? 1 : 0;
refused = (handled == FALSE && before == after) ? 1 : 0;
gtk_menu_button_popdown(GTK_MENU_BUTTON(a->window_button));
}
char tail[64];
std::snprintf(tail, sizeof(tail), " editable focused: 'm' refused=%d\n", refused);
line += tail;
line += "\n";
std::fprintf(stderr, "%s", line.c_str());
std::fflush(stderr);
+2 -15
View File
@@ -137,6 +137,7 @@ void on_signal_color_chosen(GObject* source, GAsyncResult* res, gpointer user) {
/// buttons made every row about twice this tall, which is what turned a four-signal
/// stream into a panel you have to scroll.
void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
if (app.signal_list == nullptr) return;
app.updating_panels = true;
GtkWidget* list = app.signal_list;
GtkWidget* child = gtk_widget_get_first_child(list);
@@ -300,12 +301,6 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
app.updating_panels = false;
}
void on_signals_toggled(GtkToggleButton* button, gpointer user) {
App* app = static_cast<App*>(user);
gtk_revealer_set_reveal_child(GTK_REVEALER(app->signal_revealer),
gtk_toggle_button_get_active(button));
}
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y,
gpointer user) {
App* app = static_cast<App*>(user);
@@ -396,17 +391,9 @@ void rebuild_signal_panels(App& app) {
rebuild_trigger_signals(app);
refresh_window_limits(app);
// Nothing to configure before a CONFIG arrives: hide the whole column and its
// toolbar buttons rather than showing two empty panels. The first CONFIG
// reveals the signal panel (the trigger panel stays behind its own button).
// Nothing to configure before a CONFIG arrives: hide the whole column.
update_side_visibility(app);
if (have_signals && !app.signals_panel_initialised) {
app.signals_panel_initialised = true;
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.signal_toggle), TRUE);
}
if (!have_signals) {
app.signals_panel_initialised = false;
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.signal_toggle), FALSE);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.trigger_toggle), FALSE);
}
app.signal_generation = app.model->generation();
+3 -1
View File
@@ -195,7 +195,9 @@ void on_trigger_toggled(GtkToggleButton* button, gpointer user) {
if (on && signal >= 0) {
app->model->set_signal_enabled(static_cast<std::size_t>(signal), true);
}
gtk_revealer_set_reveal_child(GTK_REVEALER(app->trigger_revealer), on);
// 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);
// 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