ui: the sidebar's tabs become one dock; Measure and Signals by keyboard section; session keeps the dock's state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-11-02 20:37:55 +01:00
1 parent 31ff778e07
commit 4b65197f56
19 files changed
+421 -419

No files matched your search

+3 -3
View File
@@ -568,8 +568,8 @@ bool load_layout(const std::string& path, LayoutState& out) {
}
} else if (key == "measure_stats") {
st.measure_stats = value == "true";
} else if (key == "side_tab") {
st.side_tab = strip_quote(value);
} else if (key == "dock") {
st.dock_shown = value == "true";
} else if (key.rfind("trigger_", 0) == 0) {
trigger_key(st.trigger, key.substr(8), strip_quote(value));
} else if (key == "multicast") {
@@ -740,7 +740,7 @@ bool save_layout(const std::string& path, const LayoutState& state) {
o << "]\n";
}
o << "measure_stats = " << (state.measure_stats ? "true" : "false") << "\n";
o << "side_tab = \"" << state.side_tab << "\"\n";
o << "dock = " << (state.dock_shown ? "true" : "false") << "\n";
// The trigger, settings and state both. Written even when it is off: that is the point of it.
o << "trigger_enabled = " << (state.trigger.enabled ? "true" : "false") << "\n";
if (!state.trigger.channel.empty()) {
+4 -3
View File
@@ -73,9 +73,10 @@ struct LayoutState {
/// stream and mean nothing restored against another one.
bool measure_stats = false;
/// Which sidebar tab was showing: "measure" or "signals". The one thing the panel persists --
/// no revealer states remain, and the 320 px width is the `.ui`'s.
std::string side_tab = "signals";
/// The dock's shown state (the user's last word on the toolbar's Dock button). The one thing the dock
/// persists -- its 320 px width is the `.ui`'s. An older file's `side_tab` key is not read: the parser
/// ignores keys it does not know.
bool dock_shown = true;
/// The measurements pane, by signal *name* and with the type and options a row was built with.
std::vector<Measure> measures;
+2 -2
View File
@@ -14,8 +14,8 @@ constexpr Accel kAccels[] = {
{GDK_KEY_a, 0, AppAction::TriggerArm, "a", "Arm or disarm the trigger", "Acquisition"},
{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_m, 0, AppAction::ShowMeasure, "m", "Go to Measure", "Panes and editors"},
{GDK_KEY_g, 0, AppAction::ShowSignals, "g", "Go to Signals", "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,
SideTabMeasure,
SideTabSignals,
ShowMeasure,
ShowSignals,
LayoutEditor,
SidePanel,
Export,
+79 -109
View File
@@ -2864,35 +2864,38 @@ 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);
/// Scrolls the dock to a section. The dock is one scroller (`App::dock_scroller`) holding the signal list,
/// the cursors and the measurements top to bottom, so "measure" is the cursors panel's offset in the
/// page, and "signals" the top. Nothing is hidden or removed: both stay in the page. An unknown name is a
/// no-op, so a key bound to a section a later version drops cannot blank anything.
void dock_show_section(App& app, const char* section) {
if (app.dock_scroller == nullptr || app.dock_page == nullptr || section == nullptr) return;
GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(app.dock_scroller));
if (std::strcmp(section, "signals") == 0) {
// The list is the first section: its own offset in the page is the page's margin, and "go to Signals"
// means the top of the dock.
gtk_adjustment_set_value(adj, 0.0);
return;
}
if (std::strcmp(section, "measure") != 0 || app.cursors_panel == nullptr) return;
graphene_point_t zero = GRAPHENE_POINT_INIT(0.f, 0.f);
graphene_point_t at;
if (!gtk_widget_compute_point(app.cursors_panel, app.dock_page, &zero, &at)) return;
gtk_adjustment_set_value(adj, at.y);
}
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.
/// The dock is on when the user's sidebar button is in -- from the start, with or without signals; the
/// two panel buttons appear as soon as a CONFIG describes any signal at all.
void update_side_visibility(App& app) {
if (app.side_stack == nullptr) return;
if (app.side_box == 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
// the panel is off while there is nothing to show. A swipe that hides the sidebar fires the button's
// handler (the binding is bidirectional), so a gesture is a choice too.
gtk_widget_set_sensitive(app.sidebar_toggle, have_signals);
const bool want_column = have_signals && app.sidebar_wanted;
// `sidebar_wanted` is the user's last word, which is why it is kept apart from the button's own state:
// a swipe that hides the sidebar fires the button's handler (the binding is bidirectional), so a gesture
// is a choice too. "Disable, do not hide": the dock is on screen from the start and does nothing until
// the stream has signals, so the signals decide its *sensitivity* and never whether it is there.
gtk_widget_set_sensitive(app.side_box, have_signals);
gtk_widget_set_sensitive(app.sidebar_toggle, TRUE);
const bool want_column = app.sidebar_wanted;
// The split view owns the sidebar's visibility: it animates it, and when the window is too narrow it
// shows it as an overlay rather than letting the column be squeezed to nothing.
if (app.side_view != nullptr) {
@@ -2908,7 +2911,12 @@ void on_sidebar_toggled(GtkToggleButton* button, gpointer user) {
App* app = static_cast<App*>(user);
// Only a state we did not write is a wish: `update_side_visibility` sets this button (and the panel)
// from `sidebar_wanted`, so an unguarded change here is the user's.
if (!app->sidebar_syncing) app->sidebar_wanted = gtk_toggle_button_get_active(button) != FALSE;
if (!app->sidebar_syncing) {
app->sidebar_wanted = gtk_toggle_button_get_active(button) != FALSE;
// The wish is persisted (`SessionState::dock_shown`), so a change of it is a change of the session --
// but not one made while a layout is being applied, which would save what it just read.
if (!app->updating_panels) schedule_layout_save(*app);
}
update_side_visibility(*app);
}
@@ -2994,8 +3002,8 @@ void note_panel_controls(App& app) {
// `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 in the toolbar, switcher=%d, reset=%d in the cursors pane",
in_bar(app.cursors_toggle), app.side_switcher != nullptr ? 1 : 0,
"cursors=%d in the toolbar, dock=%d, reset=%d in the cursors pane",
in_bar(app.cursors_toggle), app.dock_page != 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
@@ -3051,42 +3059,6 @@ void note_panel_controls(App& app) {
" " +
icon_report(theme, "org.udpscope.app-symbolic",
"/org/udpscope/icons/scalable/apps/org.udpscope.app-symbolic.svg");
// What the switcher is drawing: the sidebar's tabs are words (`wide` policy, a title per page, no
// icon). The pills are `AdwViewSwitcher`'s own buttons, built when the stack is mapped, so this
// reads on the first tick. `mapped`, not `visible`: visibility is per widget, so a label whose
// *parent* is hidden still reads visible; `mapped` requires every ancestor, which is what "on
// screen" means -- and it is why the pill count is read the same way: if nothing is mapped the
// pill count reads 0 and the label count means nothing, rather than passing vacuously.
int pills = 0;
int drawn_labels = 0;
int drawn_images = 0;
const std::function<void(GtkWidget*)> count_drawn = [&](GtkWidget* w) {
if (GTK_IS_TOGGLE_BUTTON(w) && gtk_widget_get_mapped(w)) ++pills;
// A *mapped* image is an icon (or its missing-icon placeholder) on a tab: tabs are words only.
if (GTK_IS_IMAGE(w) && gtk_widget_get_mapped(w)) ++drawn_images;
// A *mapped* label with text: an empty label would be counted by "mapped" alone.
if (GTK_IS_LABEL(w) && gtk_widget_get_mapped(w) && gtk_label_get_text(GTK_LABEL(w))[0] != '\0') {
++drawn_labels;
}
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
count_drawn(c);
}
};
if (app.side_switcher != nullptr) count_drawn(app.side_switcher);
app.dbg_switcher_pills = pills;
app.dbg_switcher_labels = drawn_labels;
app.dbg_switcher_images = drawn_images;
// Walked through `get_pages()`, not the widget tree: an `AdwViewStackPage` is a plain `GObject`.
app.dbg_switcher_pages =
app.side_stack != nullptr
? static_cast<int>(g_list_model_get_n_items(
G_LIST_MODEL(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)))))
: 0;
if (app.side_switcher != nullptr) {
g_object_get(G_OBJECT(app.side_switcher), "policy", &app.dbg_switcher_policy, nullptr);
}
}
/// The event-zoom sweep's two halves: a gesture zoom, then the same walk through the window
@@ -4022,7 +3994,7 @@ gboolean tick(gpointer user) {
// follow a resize and the gutter settling without being told.
if (app->layout_editing) sync_layout_editor(*app);
// The one thing that has to be decided *per frame* about the sidebar: whether there is room for it
// The one thing that has to be decided *per frame* about the dock: whether there is room for it
// beside the plot. Collapsed, the split view puts it *over* the plot rather than giving it a share of
// a width that is not there -- so a small window shows fewer pixels of plot, never a panel squeezed
// to nothing. `kSidebarRoom` is the sidebar's 320 plus the width below which the plot stops being
@@ -4030,8 +4002,12 @@ gboolean tick(gpointer user) {
if (app->side_view != nullptr && app->window != nullptr) {
// The split view writes `show-sidebar` itself (at startup and when it collapses), so the wish is
// re-asserted here -- but only when they disagree, so a settled frame touches nothing. This is also
// why a hide *gesture* does not stick: the panel is where the button says it is.
const bool want = !app->model->signals().empty() && app->sidebar_wanted;
// why a hide *gesture* does not stick: the panel is where the button says it is. The wish alone decides
// whether the dock is there; having signals decides only whether it does anything ("disable, do not
// hide"): a dock that is on screen from the start does not jump in when the first CONFIG arrives.
const bool want = app->sidebar_wanted;
gtk_widget_set_sensitive(app->side_box, !app->model->signals().empty());
gtk_widget_set_sensitive(app->sidebar_toggle, TRUE);
if (adw_overlay_split_view_get_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(app->side_view)) != want) {
adw_overlay_split_view_set_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(app->side_view), want);
if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app->sidebar_toggle)) != want) {
@@ -4702,8 +4678,8 @@ void run_app_action(App& app, AppAction action) {
case AppAction::CursorsPane:
toggle(app.cursors_toggle);
break;
case AppAction::SideTabMeasure: select_side_tab(app, "measure"); break;
case AppAction::SideTabSignals: select_side_tab(app, "signals"); break;
case AppAction::ShowMeasure: dock_show_section(app, "measure"); break;
case AppAction::ShowSignals: dock_show_section(app, "signals"); break;
case AppAction::LayoutEditor:
toggle(app.layout_toggle);
break;
@@ -4745,6 +4721,9 @@ gboolean on_app_key_pressed(GtkEventControllerKey*, guint keyval, guint, GdkModi
tour_dismiss(*app);
return TRUE;
}
// ... and then the slide-over: Escape closes it, ahead of the editable check for the same reason -- the
// key that dismisses a panel must work from inside one of its own spin buttons.
if (keyval == GDK_KEY_Escape && slideover_escape(*app)) return TRUE;
GtkWidget* focus = gtk_window_get_focus(GTK_WINDOW(app->window));
if (focus != nullptr && GTK_IS_EDITABLE(focus)) return FALSE;
@@ -4998,8 +4977,8 @@ void on_activate(GtkApplication* application, gpointer user) {
install_css();
// The window's skeleton is a `.ui` file: the window, its two panes, the plot column with the status
// line under it, and the sidebar's tab switcher and two-page stack. Everything that depends on the
// stream -- the events pane, the two plot areas, the sidebar's panels -- is built below and put
// line under it, and the dock's scroller. Everything that depends on the
// stream -- the events pane, the two plot areas, the dock's panels -- is built below and put
// into the slots by id.
g_clear_object(&app->window_ui);
app->window_ui = gtk_builder_new_from_resource("/org/udpscope/ui/main-window.ui");
@@ -5018,10 +4997,8 @@ void on_activate(GtkApplication* application, gpointer user) {
// help's annotated figures label.
app->status_row = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_row"));
GtkWidget* side = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_column"));
GtkWidget* side_stack = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_stack"));
GtkWidget* 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"));
app->dock_scroller = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "dock_scroller"));
app->dock_page = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "dock_page"));
app->badge_bar = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "badge_bar"));
app->badge_signals = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "badge_signals"));
app->badge_system = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "badge_system"));
@@ -5033,8 +5010,7 @@ void on_activate(GtkApplication* application, gpointer user) {
app->status_label == nullptr || app->status_info_button == nullptr ||
app->status_row == nullptr || app->badge_bar == nullptr || app->badge_signals == nullptr ||
app->badge_system == nullptr ||
side == nullptr || side_stack == nullptr || side_switcher == nullptr ||
measure_page == nullptr || signals_page == nullptr) {
side == nullptr || app->dock_scroller == nullptr || app->dock_page == nullptr) {
return;
}
// The window's actions live on the application, so it has to know which one it belongs to.
@@ -5177,28 +5153,28 @@ void on_activate(GtkApplication* application, gpointer user) {
gtk_popover_set_has_arrow(GTK_POPOVER(app->status_popover), TRUE);
}
// 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` and `side_stack`/`side_switcher`/`measure_page`/`signals_page` are resolved above with
// the rest of the skeleton and checked by the null guard; assign the members here.
// The right-hand side is the dock: one scroller, the signal list first (it is what is dragged onto the
// plot), then the cursors and the measurements. Every section is always there -- "go to Measure" scrolls
// to it instead of switching a tab, so the measurements are as live as the list above them.
// `side` and `dock_page`/`dock_scroller` are resolved above with the rest of the skeleton and checked
// by the null guard; assign the member here.
app->side_box = side;
app->side_stack = side_stack;
app->side_switcher = side_switcher;
// Measure: what the cursors bracket, and the figures derived from it.
// Built in the order the panels have always been (cursors, measurements, trigger, signals) and placed in
// the dock's: the trigger form goes to the slide-over (reached by the Trigger badge).
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));
// The trigger form lives in the slide-over (reached by the Trigger badge); the signal list stays in the
// sidebar until the dock replaces it.
GtkWidget* measures_panel = build_measures_panel(*app);
app->trigger_panel = build_trigger_panel(*app);
// Top-aligned: the page's scroller gives the child the page's height, and a form that stretched to it
// would spread its rows (or draw its frame) over empty space under the last one.
gtk_widget_set_valign(app->trigger_panel, GTK_ALIGN_START);
slideover_add_page(*app, "trigger", "Trigger", app->trigger_panel);
app->signal_panel = build_signal_panel(*app);
gtk_box_append(GTK_BOX(signals_page), app->signal_panel);
gtk_box_append(GTK_BOX(app->dock_page), app->signal_panel);
gtk_box_append(GTK_BOX(app->dock_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
gtk_box_append(GTK_BOX(app->dock_page), app->cursors_panel);
gtk_box_append(GTK_BOX(app->dock_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
gtk_box_append(GTK_BOX(app->dock_page), measures_panel);
// The cursors start off, so their readout starts insensitive -- stated here by the one place that
// knows the default, and the *body* and not the panel, because the panel's heading carries the
@@ -5210,15 +5186,8 @@ void on_activate(GtkApplication* application, gpointer user) {
sync_arm_buttons(*app);
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 source kind's own tabs (a generator, digital I/O), added before the first tick so the
// pages-versus-pills reading below counts them too. The tabs are words, so the switcher's icons
// are hidden first and kept hidden for the pages this call adds.
keep_tabs_icon_free(*app);
// The source kind's own panels (a generator, digital I/O), pages of the slide-over, added before the
// first tick.
build_source_panels(*app);
// The badge bar reads `trigger_toggle` (made by `build_toolbar()` above) and the source's pages, which
// `build_source_panels()` has just registered: the Source badge opens the first of them.
@@ -5915,7 +5884,7 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
g_timeout_add(static_cast<guint>(std::atoi(env)), stats_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_SIDE_TAB_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), side_tab_probe, &app);
g_timeout_add(static_cast<guint>(std::atoi(env)), dock_section_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_SLIDEOVER_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), slideover_probe, &app);
@@ -6028,6 +5997,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
if (const char* env = g_getenv("UDPSCOPE_PROBE_MENUS_MS")) {
menus_probe_next(app, 0, static_cast<guint>(std::atoi(env)));
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_DOCK_SESSION_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), dock_session_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_KEYS_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), keys_probe, &app);
}
@@ -6237,12 +6209,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 "
"(page_current=%d) trigger=%dx%d (sensitive=%d) switcher=%d "
"(page_current=%d) trigger=%dx%d (sensitive=%d) "
"cursors=%d window=%.3fs window_cap=%.1fs\n",
app.dbg_side_w, app.dbg_side_h, app.dbg_side_column_after_hide, app.dbg_signal_w,
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_sensitive, app.dbg_switcher_visible,
app.dbg_trigger_sensitive,
rows.cursors_visible() ? 1 : 0, rows.time_window(),
app.window_limit_s);
{
@@ -6267,10 +6239,8 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
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 images=%d pages=%d size=%dx%d\n",
app.dbg_switcher_policy == 0 ? "narrow" : (app.dbg_switcher_policy == 1 ? "wide" : "?"),
app.dbg_switcher_pills, app.dbg_switcher_labels, app.dbg_switcher_images,
app.dbg_switcher_pages, app.dbg_switcher_w, app.dbg_switcher_h);
std::printf("[udpscope] dock: shown=%d sensitive=%d w=%d scroller_pages=%d\n", app.dbg_dock_shown,
app.dbg_dock_sensitive, app.dbg_dock_w, app.dbg_dock_scrollers);
// The help affordance layer: what the contextual-help toggle left on the surfaces, and where the
// guided tour stood when the last capture ran. Both are captured on the tick (`capture_help_state`),
// so neither asks a destroyed widget anything.
+13 -56
View File
@@ -4,7 +4,7 @@
<!--
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 (`plot_overlay` > `plot_box`, the slide-over panel being an overlay child added in code)
with the status line under it, and the sidebar's tab stack.
with the status line under it, and the sidebar, which is the dock.
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
@@ -21,7 +21,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 `measure_page` and `signals_page`; and the
areas, into the split inside `plot_box`; the dock's panels (signals, cursors, measurements), appended to `dock_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()`
@@ -49,66 +49,23 @@
<property name="min-sidebar-width">320</property>
<property name="max-sidebar-width">320</property>
<property name="sidebar">
<!-- 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.
The tabs are words: the policy is `wide` and every page has a title, no icon. (They
were icon-only glyphs once; the glyphs were not understandable enough.) The pages'
*names* are what identify them to `select_side_tab()` and the session file; a source
kind's own tabs are added in code with their `SourcePanel::title`. -->
<!-- The dock: one scroller holding the signal list, then the cursors and the measurements.
It replaces the tab switcher (four text tabs did not fit 320 px and a source can add
more); it is the temporary home of Measure and Signals until steps 2 and 3 turn them
into the badge bar's signals and the compact readouts. -->
<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>
</child>
<child>
<object class="AdwViewStack" id="side_stack">
<object class="GtkScrolledWindow" id="dock_scroller">
<property name="vexpand">1</property>
<child>
<object class="AdwViewStackPage">
<property name="name">measure</property>
<property name="title">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>
<property name="hscrollbar-policy">never</property>
<property name="vscrollbar-policy">automatic</property>
<property name="child">
<object class="GtkBox" id="dock_page">
<property name="orientation">vertical</property>
</object>
</child>
<child>
<object class="AdwViewStackPage">
<property name="name">signals</property>
<property name="title">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>
</property>
</object>
</child>
</object>
+2 -2
View File
@@ -73,14 +73,14 @@
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">m</property>
<property name="title" translatable="yes">Show the Measure tab</property>
<property name="title" translatable="yes">Go to Measure</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">g</property>
<property name="title" translatable="yes">Show the Signals tab</property>
<property name="title" translatable="yes">Go to Signals</property>
</object>
</child>
<child>
+22 -34
View File
@@ -241,13 +241,11 @@ struct App {
GtkWidget* trigger_panel = 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";
/// The dock: the sidebar's one scroller and the box inside it, which holds the signal list, the
/// cursors and the measurements (`dock_show_section()` scrolls it). A feature's own on/off never
/// removes a section (it sets its panel's sensitivity instead).
GtkWidget* dock_page = nullptr;
GtkWidget* dock_scroller = nullptr;
GtkWidget* arm_button = nullptr;
/// The Force button, beside the Arm/Disarm one: one acquisition now, with the configured pre/post
/// window. It carries no state of its own -- `sync_arm_buttons()` states the one it has
@@ -477,18 +475,6 @@ struct App {
/// works, and every other line of the dump still passes.
std::string dbg_toolbar_icons;
std::string dbg_app_icons;
/// What the switcher is: how many pills it holds, and how many labels it is *drawing*. The tabs
/// are words (`wide` policy, a title per page), so the label count must equal the page count.
int dbg_switcher_pills = -1;
int dbg_switcher_labels = -1;
/// How many `GtkImage`s the switcher has *mapped*: a tab is its title and nothing else, so this
/// must be 0 -- an icon, or the missing-icon placeholder GTK draws for a page with no `icon-name`.
int dbg_switcher_images = -1;
/// How many pages the sidebar stack holds: `pills` and `labels` must both equal it.
int dbg_switcher_pages = -1;
/// The switcher's own size, beside it: what the tab bar costs the column.
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;
@@ -504,7 +490,6 @@ struct App {
/// The Arm button, read with the trigger off: it is the pane's one runtime control, so it is the one
/// thing in the body that *is* disabled with the mode.
int dbg_arm_sensitive = -1;
int dbg_switcher_policy = -1;
/// The header bar, so the dump can report the order the controls are in: a header bar is a
/// *titlebar*, not a child of the window, so walking down from the window does not reach it.
GtkWidget* toolbar = nullptr;
@@ -881,7 +866,7 @@ struct App {
std::string source_kind = "udps";
SourceConfig source_opts;
std::string source_title;
/// The panels the source kind added to `side_stack`, in tab order. Empty for UDPS and in file mode.
/// The panels the source kind added to the slide-over, in page order. Empty for UDPS and in file mode.
std::vector<SourcePanelPage> source_panels;
bool source_has_datagrams = true; ///< the kind reports raw datagrams; false leaves the capture button disabled
guint layout_save_source = 0;
@@ -1139,7 +1124,14 @@ struct App {
int dbg_signal_row_h = 0;
int dbg_trigger_w = 0;
int dbg_trigger_h = 0;
/// The signal list has rows (the dock has no tab to be "current" on any more).
int dbg_signal_page_current = 0;
/// The dock as the exit probe found it: the split view showing the sidebar, the column sensitive (it is
/// not until the stream has signals), its width, and how many `GtkScrolledWindow`s it holds directly.
int dbg_dock_shown = -1;
int dbg_dock_sensitive = -1;
int dbg_dock_w = -1;
int dbg_dock_scrollers = -1;
int dbg_trigger_sensitive = 0;
int dbg_trigger_items = 0;
int dbg_trigger_selected = -1;
@@ -1161,7 +1153,6 @@ struct App {
double dbg_trigger_time = 0.0;
int dbg_trigger_time_valid = -1;
std::string dbg_mode_live;
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,
@@ -1572,9 +1563,9 @@ constexpr int kEventThumbH = 46;
constexpr int kEventThumbMaxH = 104;
inline int g_stats_phase = 0;
inline std::vector<std::uint64_t> g_stats_n_before;
/// The same for the tab-switch probe: the accumulator counts recorded while the Measure page was
/// still current, so the report can say whether any of them advanced afterwards.
inline std::vector<std::uint64_t> g_side_tab_n_before;
/// The same for the dock-section probe: the accumulator counts recorded while the Measure section was
/// still scrolled into view, so the report can say whether any of them advanced after it scrolled out.
inline std::vector<std::uint64_t> g_dock_section_n_before;
inline double g_trigrenum_edge = 0.0;
inline std::uint64_t g_trigrenum_latches = 0;
inline ConditionProbe g_condition;
@@ -1600,11 +1591,6 @@ void config_dialog_refresh(ConfigDialog& d);
void open_configurations(App& app);
void save_layout_now(App& app);
/// The sidebar's tabs are words: hide the icon every `AdwViewSwitcherButton` draws (the
/// missing-icon placeholder, for a page with a title and no `icon-name`), for the buttons that exist
/// and for any page added later. Call once, before `build_source_panels()`.
void keep_tabs_icon_free(App& app);
/// Capability panels (`src/ui_source.cpp`). `build_source_panels` adds the source kind's panels as
/// pages of the slide-over (`source-<id>`), once, before the first tick. `apply_source_panels` sets every panel's
/// sensitivity from `connected` -- disabled, never hidden.
@@ -1803,8 +1789,9 @@ 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);
/// Scrolls the dock to a section: "signals" (the top) or "measure" (the cursors panel, which the
/// measurements follow). An unknown name is a no-op.
void dock_show_section(App& app, const char* section);
/// 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);
@@ -1907,8 +1894,9 @@ gboolean measurepin_live_wait(gpointer user);
gboolean measure_trace(gpointer user);
gboolean stats_probe_report(gpointer user);
gboolean stats_probe(gpointer user);
gboolean side_tab_probe_report(gpointer user);
gboolean side_tab_probe(gpointer user);
gboolean dock_section_probe_report(gpointer user);
gboolean dock_section_probe(gpointer user);
gboolean dock_session_probe(gpointer user); // UDPSCOPE_PROBE_DOCK_SESSION_MS
/// The slide-over panel (`src/ui_slideover.cpp`): one panel at a time over the plot's right edge, a page
/// per setup surface, each page in a scroller of its own.
void build_slideover(App& app); // once, after the plot_overlay exists
+5 -2
View File
@@ -64,7 +64,7 @@ LayoutState collect_layout(App& app) {
st.math = app.math;
st.measures = app.measures;
st.measure_stats = app.measure_stats_on;
st.side_tab = app.side_tab;
st.dock_shown = app.sidebar_wanted;
// The trigger, its settings *and* whether it was on. The channel goes by name, like every identity
// in this file: every rebuild renumbers the channels.
{
@@ -427,7 +427,10 @@ void apply_layout(App& app, const LayoutState& st) {
app.measures = st.measures;
app.measure_stats_on = st.measure_stats;
app.updating_panels = true;
select_side_tab(app, st.side_tab.c_str());
// The dock's shown state is the wish `update_side_visibility()` re-asserts every frame; setting the
// toggle goes through `sidebar_syncing`, so the restore does not schedule a save of what it just read.
app.sidebar_wanted = st.dock_shown;
update_side_visibility(app);
if (app.measures_stats_check != nullptr) {
gtk_check_button_set_active(GTK_CHECK_BUTTON(app.measures_stats_check),
app.measure_stats_on);
+177 -62
View File
@@ -104,12 +104,6 @@ gboolean exit_probe_step3(gpointer user) {
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 switcher's size as allocated: read here and not on the first tick, where the window has not
// been allocated yet and every widget still measures 0x0.
if (a->side_switcher != nullptr) {
a->dbg_switcher_w = gtk_widget_get_width(a->side_switcher);
a->dbg_switcher_h = gtk_widget_get_height(a->side_switcher);
}
// How many rows of the signal list carry a drag source: dragging a signal onto a plot is how a plot is
// filled, so it has to survive both pane modes -- the locked list drops the tick, the menu and the `+`,
// and this is the reading that says it did not drop the drag with them. One row is the `MATH` divider,
@@ -187,14 +181,9 @@ gboolean exit_probe_step3(gpointer user) {
}
if (a->area != nullptr) a->dbg_frames_painted = static_cast<int>(a->area->frames_painted());
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;
// The signal list has rows: a reading about *state* (the dock has no page to be current on), never
// about size.
a->dbg_signal_page_current = gtk_widget_get_first_child(a->signal_list) != nullptr ? 1 : 0;
// The trigger form is no longer on a sidebar tab, so the reading is the body's own sensitivity.
a->dbg_trigger_sensitive = gtk_widget_get_sensitive(a->trigger_body) ? 1 : 0;
// `gtk_widget_get_visible` on the sidebar's GtkBox is always 1: AdwOverlaySplitView hides its
@@ -203,8 +192,17 @@ gboolean exit_probe_step3(gpointer user) {
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;
// The dock, read here with the other sizes (the dump runs after the window is gone). `sensitive` is the
// availability: the column is on screen whether or not the stream has signals, and does nothing until
// it does.
a->dbg_dock_shown = sidebar_shown;
a->dbg_dock_sensitive = gtk_widget_get_sensitive(a->side_box) ? 1 : 0;
a->dbg_dock_w = a->dbg_side_w;
a->dbg_dock_scrollers = 0;
for (GtkWidget* c = gtk_widget_get_first_child(a->side_box); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
if (GTK_IS_SCROLLED_WINDOW(c)) ++a->dbg_dock_scrollers;
}
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;
@@ -1795,11 +1793,24 @@ gboolean stats_probe(gpointer user) {
return G_SOURCE_REMOVE;
}
/// Phase two of the tab-switch probe: the hidden page's accumulator must have advanced. A statistics
/// readout is *history*, so min/max/avg would gain a gap for exactly as long as the trigger was on
/// screen -- which is the one defect the design's "what runs on a hidden page" rule is there to
/// prevent, and the reason no frame-rate code may be gated on which tab is current.
gboolean side_tab_probe_report(gpointer user) {
/// Which section the dock's scroller is showing at its top: "measure" once it has been scrolled to the
/// cursors panel or past it, "signals" above that. Read from the adjustment, so it is the state the user
/// would see and not the last call made.
static const char* dock_section_in_view(App& a) {
if (a.dock_scroller == nullptr || a.cursors_panel == nullptr || a.dock_page == nullptr) return "none";
graphene_point_t zero = GRAPHENE_POINT_INIT(0.f, 0.f);
graphene_point_t at;
if (!gtk_widget_compute_point(a.cursors_panel, a.dock_page, &zero, &at)) return "none";
GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(a.dock_scroller));
return gtk_adjustment_get_value(adj) > 0.5 * at.y ? "measure" : "signals";
}
/// Phase two of the dock-section probe: the accumulators must have advanced while the Measure section was
/// scrolled out of view. A statistics readout is *history*, so min/max/avg would gain a gap for exactly as
/// long as the section was off screen -- which is the one defect the design's "what runs on a hidden
/// page" rule is there to prevent, and the reason no frame-rate code may be gated on which section is in
/// view. (It was a tab once; the dock is one scroller, and the rule outlived the tabs.)
gboolean dock_section_probe_report(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
std::uint64_t advanced = 0;
@@ -1807,25 +1818,37 @@ gboolean side_tab_probe_report(gpointer user) {
bool first = true;
for (std::size_t i = 0; i < a->measure_stats.size(); ++i) {
const std::uint64_t n = a->measure_stats[i].n;
if (i < g_side_tab_n_before.size() && n > g_side_tab_n_before[i]) ++advanced;
if (i < g_dock_section_n_before.size() && n > g_dock_section_n_before[i]) ++advanced;
if (first || n < min_n) {
min_n = n;
first = false;
}
}
if (a->dock_page != nullptr) gtk_widget_set_size_request(a->dock_page, -1, -1);
std::fprintf(stderr,
"[side-tab-probe] page=%s rows=%zu accums=%zu advanced=%" G_GUINT64_FORMAT
"[side-tab-probe] section=%s rows=%zu accums=%zu advanced=%" G_GUINT64_FORMAT
" min_n=%" G_GUINT64_FORMAT "\n",
a->side_tab.c_str(), a->measures.size(), a->measure_stats.size(), advanced, min_n);
dock_section_in_view(*a), a->measures.size(), a->measure_stats.size(), advanced, min_n);
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
/// Phase one: four measurements on the first channel, statistics on, then switch *away* from the
/// Measure tab and come back in two seconds to read what ran while it was not on screen.
gboolean side_tab_probe(gpointer user) {
/// Phase one: four measurements on the first channel, statistics on, then scroll the dock *away* from the
/// Measure section and come back in two seconds to read what ran while it was not in view. (The probe
/// keeps its old environment variable, `UDPSCOPE_PROBE_SIDE_TAB_MS`, and its `[side-tab-probe]` tag.)
gboolean dock_section_probe(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
// A dock whose content fits has nothing to scroll away from, and a tall window does that. The first
// pass makes the page taller than any window (a probe-only minimum height, undone in the report) and
// comes back once it has been laid out, so "scrolled out of view" is a state this run really reaches.
static bool padded = false;
if (!padded && a->dock_page != nullptr) {
padded = true;
gtk_widget_set_size_request(a->dock_page, -1, 3000);
g_timeout_add(400, dock_section_probe, a);
return G_SOURCE_REMOVE;
}
// One signal on, and on a plot, the way `enable_all_probe` does it and a restored session arrives.
// Both halves matter here: a signal starts disabled (enabling is what starts acquisition), and a
// channel with no trace leaves the live window *still* -- and a still window is exactly the state
@@ -1863,18 +1886,23 @@ gboolean side_tab_probe(gpointer user) {
if (a->measures_stats_check != nullptr) {
gtk_check_button_set_active(GTK_CHECK_BUTTON(a->measures_stats_check), TRUE);
}
select_side_tab(*a, "measure");
dock_show_section(*a, "measure");
// Where the dock went: `scrolled` is its offset and `reach` the most it can scroll. A reach of 0 would
// mean the content fits and "away from the section" is not a state this run can reach.
GtkAdjustment* dock_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(a->dock_scroller));
const double scrolled = gtk_adjustment_get_value(dock_adj);
const double reach = gtk_adjustment_get_upper(dock_adj) - gtk_adjustment_get_page_size(dock_adj);
update_measures(*a, /*force=*/true, /*accumulate=*/true);
g_side_tab_n_before.clear();
for (const MeasureStatsAccum& s : a->measure_stats) g_side_tab_n_before.push_back(s.n);
// Away from the page the measurements are on: its widgets are now unrealized and measure 0x0, and
// the tick keeps calling `update_measures()` regardless.
select_side_tab(*a, "signals");
std::fprintf(stderr, "[side-tab-probe] page=%s accums=%zu baseline recorded\n",
a->side_tab.c_str(), g_side_tab_n_before.size());
g_dock_section_n_before.clear();
for (const MeasureStatsAccum& s : a->measure_stats) g_dock_section_n_before.push_back(s.n);
// Away from the section the measurements are in: they are scrolled out of the viewport (the widgets
// stay mapped, only clipped), and the tick keeps calling `update_measures()` regardless.
dock_show_section(*a, "signals");
std::fprintf(stderr, "[side-tab-probe] section=%s accums=%zu baseline recorded (measure at scroll %.0f of %.0f)\n",
dock_section_in_view(*a), g_dock_section_n_before.size(), scrolled, reach);
std::fflush(stderr);
// Long enough for several throttled recomputes (one per 100 ms while the window moves).
g_timeout_add(2000, side_tab_probe_report, a);
g_timeout_add(2000, dock_section_probe_report, a);
return G_SOURCE_REMOVE;
}
@@ -3573,6 +3601,63 @@ gboolean sidebar_probe(gpointer user) {
return G_SOURCE_REMOVE;
}
/// `UDPSCOPE_PROBE_DOCK_SESSION_MS`: the dock's shown state through the session file, both ways.
///
/// The Dock button's state is the one thing the dock persists (`dock = true|false`, replacing `side_tab`).
/// Leg 1 hides the dock through the button (a wish, so it must schedule a save), collects the session and
/// writes it, reads the file back and applies it; leg 2 does the same shown. Leg 3 loads a file written by
/// the previous version (`side_tab = "measure"`, no `dock`): it must load without a complaint and leave
/// `dock_shown` at its default, true. Read as `ok=1` on every line.
gboolean dock_session_probe(gpointer user) {
App& a = *static_cast<App*>(user);
if (a.window == nullptr || a.sidebar_toggle == nullptr) return G_SOURCE_REMOVE;
const std::string dir = g_get_tmp_dir();
const std::string path = dir + "/udpscope-dock-session-probe.toml";
const auto leg = [&](const char* name, bool shown) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a.sidebar_toggle), shown);
const bool wish = a.sidebar_wanted;
const bool save_pending = a.layout_save_source != 0;
LayoutState st = collect_layout(a);
const bool collected = st.dock_shown;
const bool saved = save_layout(path, st);
LayoutState back;
const bool loaded = load_layout(path, back);
// Apply the *opposite* first so the restore has something to undo, then the loaded state.
a.sidebar_wanted = !shown;
apply_layout(a, back);
const bool applied = a.sidebar_wanted == shown;
const bool split_follows =
(adw_overlay_split_view_get_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(a.side_view)) != FALSE) == shown;
std::fprintf(stderr,
"[dock-session] %s: wish=%d save_scheduled=%d collected=%d saved=%d loaded=%d read=%d "
"applied=%d split_follows=%d ok=%d\n",
name, wish ? 1 : 0, save_pending ? 1 : 0, collected ? 1 : 0, saved ? 1 : 0,
loaded ? 1 : 0, back.dock_shown ? 1 : 0, applied ? 1 : 0, split_follows ? 1 : 0,
(wish == shown && save_pending && collected == shown && saved && loaded &&
back.dock_shown == shown && applied)
? 1 : 0);
};
leg("hidden", false);
leg("shown", true);
{
std::FILE* f = std::fopen(path.c_str(), "w");
if (f != nullptr) {
std::fputs("side_tab = \"measure\"\nmeasure_stats = false\n", f);
std::fclose(f);
}
LayoutState old_file;
old_file.dock_shown = false; // anything but the default, so "left at the default" is a reading
const bool loaded = load_layout(path, old_file);
LayoutState fresh;
std::fprintf(stderr, "[dock-session] old file (side_tab only): loaded=%d dock_shown=%d default=%d ok=%d\n",
loaded ? 1 : 0, old_file.dock_shown ? 1 : 0, fresh.dock_shown ? 1 : 0,
loaded && fresh.dock_shown ? 1 : 0);
}
std::remove(path.c_str());
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
gboolean prefs_probe(gpointer user) {
App& a = *static_cast<App*>(user);
open_preferences(a);
@@ -4524,7 +4609,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));
}
select_side_tab(*a, "measure");
dock_show_section(*a, "measure");
rebuild_measures_pane(*a);
std::fprintf(stderr, "[measures-probe] added %zu measurements on '%s'\n", a->measures.size(),
signal.c_str());
@@ -4683,7 +4768,7 @@ gboolean bandavg_probe_step(gpointer user) {
m.options.show_on_plot = measure_can_show_on_plot(m.type);
a->measures.push_back(std::move(m));
}
select_side_tab(*a, "measure");
dock_show_section(*a, "measure");
rebuild_measures_pane(*a);
g_timeout_add(250, measure_trace, a);
bandavg_probe_next(*a, 1, 1500);
@@ -6247,6 +6332,16 @@ gboolean defaults_probe(gpointer user) {
gboolean keys_probe(gpointer user) {
App* a = static_cast<App*>(user);
// `m` scrolls the dock, and a dock whose content fits has nothing to scroll: a run on a tall window
// would read 0->0 and prove nothing. So the first pass only makes the page taller than any window
// (a probe-only minimum height, undone below) and comes back once it has been laid out.
static bool padded = false;
if (!padded && a->dock_page != nullptr) {
padded = true;
gtk_widget_set_size_request(a->dock_page, -1, 3000);
g_timeout_add(400, keys_probe, a);
return G_SOURCE_REMOVE;
}
std::string line = "[keys]";
const auto before_after = [&](const char* name, GtkWidget* button, AppAction action) {
@@ -6282,38 +6377,56 @@ gboolean keys_probe(gpointer user) {
before_after("t: trigger panel", a->trigger_toggle, AppAction::TriggerPanel);
before_after("c: cursors", a->cursors_toggle, AppAction::CursorsPane);
// `m` and `g` scroll the dock: the reading is the scroller's offset before and after, and `m` must land
// on the cursors panel's own y (clamped to what the page can scroll, which is why the reachable maximum
// is printed beside it), `g` back on 0.
const auto dock_adjustment = [a]() {
return gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(a->dock_scroller));
};
const auto dock_value = [&]() { return gtk_adjustment_get_value(dock_adjustment()); };
{
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());
const double before = dock_value();
run_app_action(*a, AppAction::ShowMeasure);
const double after = dock_value();
graphene_point_t zero = GRAPHENE_POINT_INIT(0.f, 0.f);
graphene_point_t at = GRAPHENE_POINT_INIT(-1.f, -1.f);
if (!gtk_widget_compute_point(a->cursors_panel, a->dock_page, &zero, &at)) at.y = -1.f;
GtkAdjustment* adj = dock_adjustment();
const double reach = gtk_adjustment_get_upper(adj) - gtk_adjustment_get_page_size(adj);
char buf[160];
std::snprintf(buf, sizeof(buf), " m: dock %.0f->%.0f (cursors_y=%.0f reach=%.0f at_cursors=%d) |", before,
after, static_cast<double>(at.y), reach,
std::fabs(after - std::min(static_cast<double>(at.y), reach)) < 1.0 ? 1 : 0);
line += buf;
}
{
const std::string before_tab = a->side_tab;
run_app_action(*a, AppAction::SideTabSignals);
const std::string after_tab = a->side_tab;
const double before = dock_value();
run_app_action(*a, AppAction::ShowSignals);
const double after = dock_value();
char buf[96];
std::snprintf(buf, sizeof(buf), " g: tab %s->%s |", before_tab.c_str(), after_tab.c_str());
std::snprintf(buf, sizeof(buf), " g: dock %.0f->%.0f at_top=%d |", before, after,
std::fabs(after) < 1.0 ? 1 : 0);
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 side tab not to change.
// through the real handler, and require the dock not to scroll.
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 std::string before_tab = a->side_tab;
// The dock has to be somewhere other than where `m` would take it, or "did not scroll" proves nothing.
gtk_adjustment_set_value(dock_adjustment(), 0.0);
const double before_dock = dock_value();
const gboolean handled = on_app_key_pressed(nullptr, GDK_KEY_m, 0,
static_cast<GdkModifierType>(0), a);
const std::string after_tab = a->side_tab;
refused = (handled == FALSE && before_tab == after_tab) ? 1 : 0;
const double after_dock = dock_value();
refused = (handled == FALSE && before_dock == after_dock) ? 1 : 0;
gtk_menu_button_popdown(GTK_MENU_BUTTON(a->window_button));
}
if (a->dock_page != nullptr) gtk_widget_set_size_request(a->dock_page, -1, -1);
char tail[64];
std::snprintf(tail, sizeof(tail), " editable focused: 'm' refused=%d\n", refused);
line += tail;
@@ -6570,10 +6683,10 @@ void tour_geometry_line(App& a, const char* probe, const char* what) {
p.origin.y < q.origin.y + q.size.height && q.origin.y < p.origin.y + p.size.height;
};
std::fprintf(stderr,
"[%s] %s=%d/%d visible=%d side_tab=%s spot=[%.0f,%.0f %.0fx%.0f] "
"[%s] %s=%d/%d visible=%d slideover=%s spot=[%.0f,%.0f %.0fx%.0f] "
"target=[%.0f,%.0f %.0fx%.0f] contains=%d card=[%.0f,%.0f %.0fx%.0f] clear=%d\n",
probe, what, tour_step_index(a) + 1, tour_step_count(a), tour_active(a) ? 1 : 0,
a.side_tab.c_str(), spot.origin.x, spot.origin.y, spot.size.width, spot.size.height,
(slideover_page(a).empty() ? "none" : slideover_page(a).c_str()), spot.origin.x, spot.origin.y, spot.size.width, spot.size.height,
target.origin.x, target.origin.y, target.size.width, target.size.height,
have_spot && have_target && contains(spot, target) ? 1 : 0, card.origin.x,
card.origin.y, card.size.width, card.size.height,
@@ -6818,10 +6931,10 @@ gboolean tour_surface_probe_step(gpointer user) {
case 0: {
// Where the run starts: a fresh config's sidebar is on Signals, so this is a *first* run's
// state. The auto-start's walk (if one began) would only confuse the reading; it goes first.
std::fprintf(stderr, "[toursurface] before: side_tab=%s signal_list_mapped=%d running=%d\n",
a->side_tab.c_str(), mapped(a->signal_list), tour_active(*a) ? 1 : 0);
std::fprintf(stderr, "[toursurface] before: slideover=%s signal_list_mapped=%d running=%d\n",
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(), mapped(a->signal_list), tour_active(*a) ? 1 : 0);
tour_dismiss(*a);
select_side_tab(*a, "measure");
slideover_open(*a, "trigger");
a->dbg_tour_surface_step = 1;
tour_surface_probe_next(*a, 500);
return G_SOURCE_REMOVE;
@@ -6830,8 +6943,9 @@ gboolean tour_surface_probe_step(gpointer user) {
// A frame after the switch, so the Measure page has been laid out and the list really is off
// screen. The tour is started *here*, from this state.
std::fprintf(stderr,
"[toursurface] on measure: side_tab=%s signal_list_mapped=%d side_stack_mapped=%d\n",
a->side_tab.c_str(), mapped(a->signal_list), mapped(a->side_stack));
"[toursurface] on trigger: slideover=%s signal_list_mapped=%d side_box_mapped=%d\n",
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(),
mapped(a->signal_list), mapped(a->side_box));
start_tour(*a);
std::fprintf(stderr, "[toursurface] tour: visible=%d steps=%d step=%d signals_stop=%d\n",
tour_active(*a) ? 1 : 0, tour_step_count(*a), tour_step_index(*a),
@@ -6848,8 +6962,9 @@ gboolean tour_surface_probe_step(gpointer user) {
}
GtkWidget* target = tour_target(*a);
const bool is_list = target != nullptr && target == a->signal_list;
std::fprintf(stderr, "[toursurface] step=%d/%d side_tab=%s target_mapped=%d%s\n",
tour_step_index(*a) + 1, tour_step_count(*a), a->side_tab.c_str(), mapped(target),
std::fprintf(stderr, "[toursurface] step=%d/%d slideover=%s target_mapped=%d%s\n",
tour_step_index(*a) + 1, tour_step_count(*a),
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(), mapped(target),
is_list ? " target=signal_list" : "");
if (is_list) {
// The frame after the switch: the page has been laid out and `update_tour()` has placed the
@@ -6876,9 +6991,9 @@ gboolean tour_surface_probe_step(gpointer user) {
default: {
start_tour(*a);
std::fprintf(stderr,
"[toursurface] collapsed: side_stack_mapped=%d steps=%d signals_stop=%d "
"[toursurface] collapsed: side_box_mapped=%d steps=%d signals_stop=%d "
"side_stop=%d\n",
mapped(a->side_stack), tour_step_count(*a),
mapped(a->side_box), tour_step_count(*a),
tour_plan_holds(*a, a->signal_list) ? 1 : 0,
tour_plan_holds(*a, a->side_box) ? 1 : 0);
tour_dismiss(*a);
-42
View File
@@ -11,48 +11,6 @@
namespace udpscope {
namespace ui_impl {
namespace {
/// Hide every `GtkImage` inside `w`, and keep it hidden. `AdwViewSwitcherButton` always builds an
/// icon -- in both of its layouts -- and a page with no `icon-name` gets `image-missing`, which is
/// the placeholder the sidebar's text tabs used to draw. There is no property or policy that turns
/// the icon off, so the image is hidden (a hidden child takes no space: the label gets the pill) and
/// a `notify::visible` handler puts it back to hidden if the button ever shows it again.
void hide_tab_icons(GtkWidget* w) {
if (GTK_IS_IMAGE(w)) {
if (g_object_get_data(G_OBJECT(w), "udpscope-icon-hidden") == nullptr) {
g_object_set_data(G_OBJECT(w), "udpscope-icon-hidden", GINT_TO_POINTER(1));
g_signal_connect(w, "notify::visible", G_CALLBACK(+[](GObject* o, GParamSpec*, gpointer) {
GtkWidget* img = GTK_WIDGET(o);
if (gtk_widget_get_visible(img)) gtk_widget_set_visible(img, FALSE);
}),
nullptr);
}
gtk_widget_set_visible(w, FALSE);
return;
}
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr; c = gtk_widget_get_next_sibling(c)) {
hide_tab_icons(c);
}
}
void on_tab_pages_changed(GListModel*, guint, guint, guint, gpointer sw) {
hide_tab_icons(GTK_WIDGET(sw));
}
} // namespace
void keep_tabs_icon_free(App& app) {
if (app.side_switcher == nullptr || app.side_stack == nullptr) return;
hide_tab_icons(app.side_switcher);
// A page added later gets a button when the model reports it; this handler is connected *after*
// the switcher's own, so the button already exists when it runs.
g_signal_connect_after(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)), "items-changed",
G_CALLBACK(on_tab_pages_changed), app.side_switcher);
// And once more when the switcher is (re)mapped, which also covers a rebuild of the buttons.
g_signal_connect_swapped(app.side_switcher, "map", G_CALLBACK(hide_tab_icons), app.side_switcher);
}
void build_source_panels(App& app) {
if (app.slideover.stack == nullptr || app.acq == nullptr || app.acq->source() == nullptr) return;
SourceKind* kind = find_kind(app.source_kind);
+4 -7
View File
@@ -422,11 +422,9 @@ void start_tour(App& app) {
app.settings.tour_seen = true;
(void)save_settings(app.settings);
}
// The sidebar's own stack, so a stop can name the page it stands on. Set here and not in
// `build_tour()`: the overlay is inserted above the window's content *before* `App::side_stack` is
// assigned, so the member does not exist yet at build time -- and the walk cannot start before the
// window is up anyway.
app.tour.page_stack = app.side_stack;
// The main window has no page stack any more (the dock is one scroller): `page_stack` stays null, so
// a stop's `page` selects nothing and a stop whose target is unmapped is dropped. The slide-over hook
// that lets a stop open the trigger page is the next step's.
// The whole table, in walk order: `tour_begin()` drops the stops whose surface is not on screen.
std::vector<TourStop> stops;
stops.reserve(kTourStepCount);
@@ -486,8 +484,7 @@ constexpr FigureSurface kFigureSurfaces[] = {
{"trigger_summary", &App::trigger_summary},
{"trigger_panel", &App::trigger_panel},
{"signal_list", &App::signal_list},
{"side_switcher", &App::side_switcher},
{"side_stack", &App::side_stack},
{"side_stack", &App::side_box},
{"window_button", &App::window_button},
{"status_label", &App::status_label},
{"header_bar", &App::toolbar},