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:
1 parent
31ff778e07
commit
4b65197f56
19 files changed
+421
-419
No files matched your search
@@ -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()) {
|
||||
|
||||
@@ -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
@@ -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
@@ -10,8 +10,8 @@ enum class AppAction {
|
||||
TriggerArm,
|
||||
TriggerPanel,
|
||||
CursorsPane,
|
||||
SideTabMeasure,
|
||||
SideTabSignals,
|
||||
ShowMeasure,
|
||||
ShowSignals,
|
||||
LayoutEditor,
|
||||
SidePanel,
|
||||
Export,
|
||||
|
||||
+79
-109
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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},
|
||||
|
||||
Reference in new issue
Block a user