diff --git a/src/shortcuts.cpp b/src/shortcuts.cpp index f3bdaef..713aa09 100644 --- a/src/shortcuts.cpp +++ b/src/shortcuts.cpp @@ -12,14 +12,10 @@ constexpr Accel kAccels[] = { {GDK_KEY_space, 0, AppAction::PauseToggle, "space", "Pause or resume the display", "Acquisition"}, {GDK_KEY_a, 0, AppAction::TriggerArm, "a", "Arm or disarm the trigger", "Acquisition"}, - {GDK_KEY_t, 0, AppAction::TriggerPanel, "t", "Show or hide the trigger panel", - "Acquisition"}, - {GDK_KEY_c, 0, AppAction::CursorsPane, "c", "Show or hide the cursors pane", - "Panes and editors"}, - {GDK_KEY_m, 0, AppAction::MeasuresPane, "m", "Show or hide the measurements pane", - "Panes and editors"}, - {GDK_KEY_g, 0, AppAction::MathPane, "g", "Show or hide the math pane", - "Panes and editors"}, + {GDK_KEY_t, 0, AppAction::TriggerPanel, "t", "Enable or disable trigger mode", "Acquisition"}, + {GDK_KEY_c, 0, AppAction::CursorsPane, "c", "Enable or disable the cursors", "Panes and editors"}, + {GDK_KEY_m, 0, AppAction::SideTabMeasure, "m", "Show the Measure tab", "Panes and editors"}, + {GDK_KEY_g, 0, AppAction::SideTabSignals, "g", "Show the Signals tab", "Panes and editors"}, {GDK_KEY_l, 0, AppAction::LayoutEditor, "l", "Show or hide the layout editor", "Panes and editors"}, {GDK_KEY_s, 0, AppAction::SidePanel, "s", "Show or hide the side panel", diff --git a/src/shortcuts.hpp b/src/shortcuts.hpp index bf9fec1..ef02a54 100644 --- a/src/shortcuts.hpp +++ b/src/shortcuts.hpp @@ -10,8 +10,8 @@ enum class AppAction { TriggerArm, TriggerPanel, CursorsPane, - MeasuresPane, - MathPane, + SideTabMeasure, + SideTabSignals, LayoutEditor, SidePanel, Export, diff --git a/src/ui.cpp b/src/ui.cpp index 3eebabc..f4a860a 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -2824,11 +2824,28 @@ void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer open_row_menu(*app, row, x, y, widget); } +void on_side_tab_changed(GObject* o, GParamSpec*, gpointer user) { + App* app = static_cast(user); + const char* name = adw_view_stack_get_visible_child_name(ADW_VIEW_STACK(o)); + app->side_tab = name != nullptr ? name : "signals"; + // Not while the layout is being applied: the restore would schedule a save of what it just read. + if (!app->updating_panels) schedule_layout_save(*app); +} + +void select_side_tab(App& app, const char* name) { + if (app.side_stack == nullptr || name == nullptr) return; + // An unknown name leaves the stack where it is -- a session file from a later version must not + // be able to blank the sidebar. + if (adw_view_stack_get_child_by_name(ADW_VIEW_STACK(app.side_stack), name) == nullptr) return; + adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(app.side_stack), name); + app.side_tab = name; +} + /// The side column is on when it has something to show *and* the user's sidebar /// button is in; the two panel buttons appear as soon as a CONFIG describes any /// signal at all. void update_side_visibility(App& app) { - if (app.side_scroll == nullptr) return; + if (app.side_stack == nullptr) return; const bool have_signals = !app.model->signals().empty(); // `have_signals` decides whether the sidebar *can* be on; `sidebar_wanted` is the user's last word, // which is why it is kept apart from the button's own state -- that state follows the panel now, and @@ -2845,7 +2862,6 @@ void update_side_visibility(App& app) { gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.sidebar_toggle), want_column); app.sidebar_syncing = false; } - gtk_widget_set_visible(app.signal_toggle, have_signals); gtk_widget_set_visible(app.trigger_toggle, have_signals); } @@ -2932,15 +2948,15 @@ void note_panel_controls(App& app) { const auto in_cursors_pane = [&app](GtkWidget* w) { for (GtkWidget* p = w != nullptr ? gtk_widget_get_parent(w) : nullptr; p != nullptr; p = gtk_widget_get_parent(p)) { - if (p == app.cursors_revealer) return 1; + if (p == app.cursors_panel) return 1; } return 0; }; // `HeaderBar::pack_end()` parents into an internal box, so the bar is a level up, not the parent. char text[256]; std::snprintf(text, sizeof(text), - "cursors=%d measurements=%d math=%d in the toolbar, reset=%d in the cursors pane", - in_bar(app.cursors_toggle), in_bar(app.measures_toggle), in_bar(app.math_toggle), + "cursors=%d in the toolbar, switcher=%d, reset=%d in the cursors pane", + in_bar(app.cursors_toggle), app.side_switcher != nullptr ? 1 : 0, in_cursors_pane(app.cursors_reset)); // ... and the order the bar actually shows, which is what "the menu should be last" is about. A // widget tree walked in child order *is* the layout order, so this is the reading order of the @@ -2948,8 +2964,7 @@ void note_panel_controls(App& app) { const std::pair named[] = { {app.trigger_toggle, "trigger"}, {app.events_toggle, "events"}, {app.pause_button, "pause"}, - {app.cursors_toggle, "cursors"}, {app.measures_toggle, "measures"}, - {app.math_toggle, "math"}, {app.sidebar_toggle, "sidebar"}, + {app.cursors_toggle, "cursors"}, {app.sidebar_toggle, "sidebar"}, {app.menu_button, "menu"}, }; // The widget tree's child order, which *is* the visual order left to right: a box lays its children @@ -4316,12 +4331,8 @@ void run_app_action(App& app, AppAction action) { case AppAction::CursorsPane: toggle(app.cursors_toggle); break; - case AppAction::MeasuresPane: - toggle(app.measures_toggle); - break; - case AppAction::MathPane: - toggle(app.math_toggle); - break; + case AppAction::SideTabMeasure: select_side_tab(app, "measure"); break; + case AppAction::SideTabSignals: select_side_tab(app, "signals"); break; case AppAction::LayoutEditor: toggle(app.layout_toggle); break; @@ -4449,11 +4460,6 @@ GtkWidget* build_toolbar(App& app) { g_signal_connect(app.layout_toggle, "toggled", G_CALLBACK(on_layout_editor_toggled), &app); gtk_box_append(GTK_BOX(menu_box), app.layout_toggle); - app.signal_toggle = menu_toggle("Signal list", nullptr, true); - gtk_widget_set_tooltip_text(app.signal_toggle, "show or hide the signal list"); - g_signal_connect(app.signal_toggle, "toggled", G_CALLBACK(on_signals_toggled), &app); - gtk_box_append(GTK_BOX(menu_box), app.signal_toggle); - gtk_box_append(GTK_BOX(menu_box), menu_separator()); GtkWidget* export_btn = menu_button("Export visible range...", "Ctrl+E"); @@ -4511,12 +4517,10 @@ GtkWidget* build_toolbar(App& app) { app.trigger_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_toggle")); app.events_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "events_toggle")); app.pause_button = GTK_WIDGET(gtk_builder_get_object(ui, "pause_button")); - app.measures_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "measures_toggle")); - app.math_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "math_toggle")); app.cursors_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "cursors_toggle")); if (app.menu_button == nullptr || app.sidebar_toggle == nullptr || app.trigger_toggle == nullptr || - app.events_toggle == nullptr || app.pause_button == nullptr || app.measures_toggle == nullptr || - app.math_toggle == nullptr || app.cursors_toggle == nullptr) { + app.events_toggle == nullptr || app.pause_button == nullptr || + app.cursors_toggle == nullptr) { // A missing id is a mistake in the `.ui`: the bar comes up with the controls it got rather than taking // the window with it, and `[udpscope] header:` reads whatever geometry is left. return bar; @@ -4538,24 +4542,6 @@ GtkWidget* build_toolbar(App& app) { }), &app); g_signal_connect(app.pause_button, "toggled", G_CALLBACK(on_pause_toggled), &app); - g_signal_connect(app.measures_toggle, "toggled", - G_CALLBACK(+[](GtkToggleButton* b, gpointer user) { - App* a = static_cast(user); - a->measures_revealed = gtk_toggle_button_get_active(b); - if (a->measures_revealer != nullptr) { - gtk_revealer_set_reveal_child(GTK_REVEALER(a->measures_revealer), - a->measures_revealed); - } - }), - &app); - g_signal_connect(app.math_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* b, gpointer user) { - App* a = static_cast(user); - if (a->math_revealer != nullptr) { - gtk_revealer_set_reveal_child(GTK_REVEALER(a->math_revealer), - gtk_toggle_button_get_active(b)); - } - }), - &app); g_signal_connect(app.cursors_toggle, "toggled", G_CALLBACK(on_cursors_toggled), &app); return bar; } @@ -4579,14 +4565,13 @@ void on_activate(GtkApplication* application, gpointer user) { app->status_badge = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_badge")); app->status_label = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_label")); app->status_info_button = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_info")); - app->side_scroll = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_scroll")); GtkWidget* side = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_column")); // A missing id is the one mistake neither the build nor `GtkBuilder` sees, and every use below would // be a null pointer. if (app->window == nullptr || side_view == nullptr || events_divider == nullptr || app->plot_box == nullptr || app->plot_split == nullptr || app->status_badge == nullptr || app->status_label == nullptr || app->status_info_button == nullptr || - app->side_scroll == nullptr || side == nullptr) { + side == nullptr) { return; } // The window's actions live on the application, so it has to know which one it belongs to. @@ -4714,59 +4699,46 @@ void on_activate(GtkApplication* application, gpointer user) { gtk_popover_set_has_arrow(GTK_POPOVER(app->status_popover), TRUE); } - // The right-hand side: two independently collapsible panels, each revealed by - // its toolbar toggle. No outer scrolled window -- the signal list scrolls - // inside its own panel -- so neither panel can be squeezed to nothing by the - // other's size request. - // `side` is the sidebar column from the `.ui`, resolved above with the rest of the skeleton; the - // app keeps it because the panel geometry, the divide and the revealers all ask about it. + // The right-hand side is two tabs, not five stacked panels: one tab is on screen at a time, so + // no section competes with four others for height. Each page scrolls on its own -- the two are + // different heights, and one shared scroller would leave a switch scrolled where the new page + // never asked to be. + // `side` is `side_column` from the `.ui`, resolved above with the rest of the skeleton; the app + // keeps it because `update_side_visibility()` still asks about the column as a whole. app->side_box = side; + app->side_stack = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_stack")); + app->side_switcher = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_switcher")); + GtkWidget* measure_page = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "measure_page")); + GtkWidget* signals_page = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "signals_page")); - // The two panels share a vertical box, so they collapse *vertically*: a - // horizontal slide would animate the width and leave the panel's full height - // reserved -- an invisible gap pushing the other panel down the column. - app->trigger_revealer = gtk_revealer_new(); - gtk_revealer_set_transition_type(GTK_REVEALER(app->trigger_revealer), - GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); + // Measure: what the cursors bracket, and the figures derived from it. + app->cursors_panel = build_cursors_panel(*app); + gtk_box_append(GTK_BOX(measure_page), app->cursors_panel); + gtk_box_append(GTK_BOX(measure_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); + gtk_box_append(GTK_BOX(measure_page), build_measures_panel(*app)); + + // Signals: the trigger picks a channel from the list beside it. app->trigger_panel = build_trigger_panel(*app); - gtk_revealer_set_child(GTK_REVEALER(app->trigger_revealer), app->trigger_panel); - gtk_box_append(GTK_BOX(side), app->trigger_revealer); - gtk_box_append(GTK_BOX(side), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); - - // The cursors readout sits with the other panels and is revealed by its toggle: the - // instruments are on the plot, but what they read has to be somewhere a value can be - // selected and copied from. - app->cursors_revealer = gtk_revealer_new(); - gtk_revealer_set_transition_type(GTK_REVEALER(app->cursors_revealer), - GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); - gtk_revealer_set_child(GTK_REVEALER(app->cursors_revealer), build_cursors_panel(*app)); - gtk_box_append(GTK_BOX(side), app->cursors_revealer); - gtk_box_append(GTK_BOX(side), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); - - app->measures_revealer = gtk_revealer_new(); - gtk_revealer_set_transition_type(GTK_REVEALER(app->measures_revealer), - GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); - gtk_revealer_set_child(GTK_REVEALER(app->measures_revealer), build_measures_panel(*app)); - gtk_box_append(GTK_BOX(side), app->measures_revealer); - gtk_box_append(GTK_BOX(side), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); - - app->math_revealer = gtk_revealer_new(); - gtk_revealer_set_transition_type(GTK_REVEALER(app->math_revealer), - GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); - gtk_revealer_set_child(GTK_REVEALER(app->math_revealer), build_math_panel(*app)); - gtk_box_append(GTK_BOX(side), app->math_revealer); - gtk_box_append(GTK_BOX(side), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); + gtk_box_append(GTK_BOX(signals_page), app->trigger_panel); + gtk_box_append(GTK_BOX(signals_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); + // Still on the signals page until the list grows the `+` and the per-row menu that replace it. + gtk_box_append(GTK_BOX(signals_page), build_math_panel(*app)); + gtk_box_append(GTK_BOX(signals_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); rebuild_math_pane(*app); - app->signal_panel = build_signal_panel(*app); - app->signal_revealer = gtk_revealer_new(); - gtk_revealer_set_transition_type(GTK_REVEALER(app->signal_revealer), - GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); - gtk_revealer_set_child(GTK_REVEALER(app->signal_revealer), app->signal_panel); - gtk_revealer_set_reveal_child(GTK_REVEALER(app->signal_revealer), TRUE); - gtk_box_append(GTK_BOX(side), app->signal_revealer); - // Its size request and the scroller around it are the `.ui`'s: the column scrolls as a whole, so a - // long signal list or a revealed trigger panel can never push either panel out of the window. + gtk_box_append(GTK_BOX(signals_page), app->signal_panel); + + // Both instruments start off, so both panels start insensitive -- stated here by the one place + // that knows the default, rather than left to whichever widget happened to be built first. + gtk_widget_set_sensitive(app->trigger_panel, FALSE); + gtk_widget_set_sensitive(app->cursors_panel, FALSE); + gtk_widget_set_sensitive(app->cursors_reset, FALSE); + + if (app->side_stack != nullptr) { + adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(app->side_stack), "signals"); + g_signal_connect(app->side_stack, "notify::visible-child-name", + G_CALLBACK(on_side_tab_changed), app); + } // The left side is the events pane *and* the plot, in one row: the pane is a column of its own // (the item's "collapsable pane on the left"), revealed by trigger mode. @@ -5494,9 +5466,6 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit if (const char* env = g_getenv("UDPSCOPE_PROBE_WINDOWPOP_MS")) { g_timeout_add(static_cast(std::atoi(env)), windowpop_probe_step, &app); } - if (const char* env = g_getenv("UDPSCOPE_PROBE_KEYS_MS")) { - g_timeout_add(static_cast(std::atoi(env)), keys_probe, &app); - } if (const char* env = g_getenv("UDPSCOPE_PROBE_COLOR_MS")) { g_timeout_add(static_cast(std::atoi(env)), color_probe, &app); } @@ -5604,7 +5573,7 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit std::printf("[udpscope] axes.w changes: %d (last at frame %d, side panel up=%d)\n", app.dbg_axes_w_changes, app.dbg_axes_w_change_frame, app.dbg_axes_w_change_side); - std::printf("[udpscope] reset button visible: %d\n", app.dbg_reset_visible); + std::printf("[udpscope] reset button sensitive: %d\n", app.dbg_reset_sensitive); std::printf("[udpscope] render blank frames: %d of %d painted\n", app.dbg_render_blank_frames, app.dbg_frames_painted); std::printf("[udpscope] traceless paints: %d (%d after the first bind, last at paint %d)\n", @@ -5671,12 +5640,12 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit inside(app.events_list, app.events_revealer)); } std::printf("[udpscope] panels: side=%dx%d (visible=%d) signals=%dx%d row_h=%d " - "(revealed=%d) trigger=%dx%d (revealed=%d) toggles=%d cursors=%d window=%.3fs " + "(page_current=%d) trigger=%dx%d (sensitive=%d) switcher=%d cursors=%d window=%.3fs " "window_cap=%.1fs\n", app.dbg_side_w, app.dbg_side_h, app.dbg_side_visible, app.dbg_signal_w, - app.dbg_signal_h, app.dbg_signal_row_h, app.dbg_signal_revealed, + app.dbg_signal_h, app.dbg_signal_row_h, app.dbg_signal_page_current, app.dbg_trigger_w, app.dbg_trigger_h, - app.dbg_trigger_revealed, app.dbg_toggles_visible, + app.dbg_trigger_sensitive, app.dbg_switcher_visible, rows.cursors_visible() ? 1 : 0, rows.time_window(), app.window_limit_s); std::printf("[udpscope] mode with trigger on: %s\n", app.dbg_mode_triggered.c_str()); @@ -5841,11 +5810,10 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit std::printf("[udpscope] badge frames: lit=%d dim=%d idle=%d connecting=%d\n", app.dbg_badge_frames[0], app.dbg_badge_frames[1], app.dbg_badge_frames[2], app.dbg_badge_frames[3]); - std::printf("[udpscope] trigger panel revealed before the probe toggled it: %d; " - "signal list items=%d selected=%d; column visible after sidebar " - "toggle off=%d\n", - app.dbg_trigger_revealed_default, app.dbg_trigger_items, - app.dbg_trigger_selected, app.dbg_side_scroll_after_hide); + std::printf("[udpscope] trigger panel sensitive before the probe toggled it: %d; " + "signal list items=%d selected=%d; column visible after sidebar toggle off=%d\n", + app.dbg_trigger_sensitive_default, app.dbg_trigger_items, + app.dbg_trigger_selected, app.dbg_side_column_after_hide); { // The panel's *frame* read back: the row boxes the `.ui` holds, and that every control the code built // ended up inside the panel rather than in a row that is not there. A control is created in code and @@ -6078,9 +6046,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit } { std::lock_guard lock(model.mutex()); - std::printf("[udpscope] measures pane: n=%zu revealed=%d marks_shown=%d pane_rows=%zu " + std::printf("[udpscope] measures pane: n=%zu marks_shown=%d pane_rows=%zu " "pane_w=%d pane_h=%d switch_on=%d reset_sensitive=%d\n", - app.measures.size(), app.measures_revealed ? 1 : 0, + app.measures.size(), rows.measure_marks_placed(), app.measure_labels.size(), app.dbg_measures_pane_w, app.dbg_measures_pane_h, // The two widgets the panel's *frame* is read by: the code drives `measure_stats_on`, diff --git a/src/ui/header-bar.ui b/src/ui/header-bar.ui index b1bd8ef..9fc9da3 100644 --- a/src/ui/header-bar.ui +++ b/src/ui/header-bar.ui @@ -3,7 +3,7 @@ - - never - automatic - 300 - - - vertical - 300 + + + vertical + 300 + + + side_stack + wide + 4 + 4 + 6 + 6 - + + + + 1 + + + measure + Measure + + + never + automatic + + + vertical + + + + + + + + + signals + Signals + + + never + automatic + + + vertical + + + + + + + + diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 5a0db88..2eba028 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -139,18 +139,21 @@ struct App { /// what we synced is ours -- GTK only notifies on a change, so the user cannot produce it. int window_sync_unit = -1; GtkWidget* trigger_toggle = nullptr; - GtkWidget* trigger_revealer = nullptr; /// The trigger panel's own box, kept so the defaults probe can read its spin buttons without /// walking the window for them. GtkWidget* trigger_panel = nullptr; - GtkWidget* signal_toggle = nullptr; - GtkWidget* signal_revealer = nullptr; GtkWidget* signal_panel = nullptr; GtkWidget* side_box = nullptr; + /// The sidebar's tab stack and its switcher. The stack is what chooses which panels are on + /// screen; a feature's own on/off never does (it sets its panel's sensitivity instead). + GtkWidget* side_stack = nullptr; + GtkWidget* side_switcher = nullptr; + /// The page name the stack is showing, and the one value the session file persists about the + /// sidebar. "signals" on a first run. + std::string side_tab = "signals"; GtkWidget* arm_button = nullptr; GtkWidget* trigger_signal_drop = nullptr; GtkWidget* sidebar_toggle = nullptr; - GtkWidget* side_scroll = nullptr; GtkWidget* trigger_edge_drop = nullptr; GtkWidget* trigger_type_drop = nullptr; GtkWidget* trigger_cmp_drop = nullptr; @@ -211,10 +214,10 @@ struct App { /// *titlebar*, not a child of the window, so walking down from the window does not reach it. GtkWidget* toolbar = nullptr; GtkWidget* cursors_reset = nullptr; - /// The cursors pane: revealed with the toggle, and listing what the instruments - /// read -- T0/T1/dT always, per row an Y1/Y2/dY where rulers are up, and in `mix` mode - /// the value each cursor crosses each trace at. - GtkWidget* cursors_revealer = nullptr; + /// The cursors panel's root, kept because cursors-off disables it rather than hiding it. + GtkWidget* cursors_panel = nullptr; + /// The cursors pane: listing what the instruments read -- T0/T1/dT always, per row an Y1/Y2/dY + /// where rulers are up, and in `mix` mode the value each cursor crosses each trace at. GtkWidget* cursors_mode_drop = nullptr; GtkWidget* cursors_body = nullptr; /// One line per row (rulers) and one per bound trace (`mix`), in the order the body @@ -237,16 +240,12 @@ struct App { /// price the frame tick should pay). std::vector measures; std::vector measure_results; - GtkWidget* measures_revealer = nullptr; GtkWidget* measures_list = nullptr; - GtkWidget* measures_toggle = nullptr; // --- math channels -------------------------------------------------------- /// The app's copy of the math channels, and the source of truth for the pane. Every change goes /// through `apply_math_channels()`, which writes this and hands it to the model -- the model's own /// copy is what the producer evaluates. std::vector math; - GtkWidget* math_toggle = nullptr; - GtkWidget* math_revealer = nullptr; GtkWidget* math_list = nullptr; /// The math editor's builder. The editor's window belongs to the builder's object list, so the builder is /// held for as long as the window is up and cleared on the *next* opening rather than with the window -- @@ -358,7 +357,6 @@ struct App { /// that memory reused and the read is a **SIGSEGV** in the middle of the dump. int dbg_measures_pane_w = -1; int dbg_measures_pane_h = -1; - bool measures_revealed = false; /// The measurement dialog while one is open (null otherwise). A probe needs a handle on it: /// it is a window of its own, so the main window's widget tree does not reach its buttons. GtkWidget* measure_dialog = nullptr; @@ -508,7 +506,6 @@ struct App { /// True once the renderer's `Style` has been stated (see `tick()`): the renderer does not /// exist until the area is realized, so the one-shot has to be a tick's job. bool style_stated = false; - bool signals_panel_initialised = false; /// Set once the user edits the trigger level, so the panel stops defaulting it /// into the middle of the declared range. bool trigger_level_touched = false; @@ -690,7 +687,7 @@ struct App { /// over a run that acquired anything -- that is what "it blinks" means, and a badge /// stuck lit looks identical to a working one in a screenshot. int dbg_badge_frames[4] = {0, 0, 0, 0}; - int dbg_reset_visible = -1; + int dbg_reset_sensitive = -1; std::vector last_stored; std::uint64_t dbg_latches_at_disarm = 0; std::uint64_t dbg_seq_at_disarm = 0; @@ -738,12 +735,12 @@ struct App { int dbg_signal_row_h = 0; int dbg_trigger_w = 0; int dbg_trigger_h = 0; - int dbg_signal_revealed = 0; - int dbg_trigger_revealed = 0; + int dbg_signal_page_current = 0; + int dbg_trigger_sensitive = 0; int dbg_side_visible = 0; int dbg_trigger_items = 0; int dbg_trigger_selected = -1; - int dbg_side_scroll_after_hide = 1; + int dbg_side_column_after_hide = 1; std::string dbg_mode_triggered; std::string dbg_window_text; double dbg_marker_x = 0.0; @@ -751,8 +748,8 @@ struct App { double dbg_trigger_time = 0.0; int dbg_trigger_time_valid = -1; std::string dbg_mode_live; - int dbg_toggles_visible = 0; - int dbg_trigger_revealed_default = 0; + int dbg_switcher_visible = 0; + int dbg_trigger_sensitive_default = 0; /// The detail string the popover and the exit dump read. The dump used to read the label, /// which no longer carries these fields. @@ -1336,7 +1333,6 @@ void rebuild_math_pane(App& app); GtkWidget* build_measures_panel(App& app); GtkWidget* build_cursors_panel(App& app); void on_cursors_toggled(GtkToggleButton* button, gpointer user); -void on_signals_toggled(GtkToggleButton* button, gpointer user); void on_fit_clicked(GtkButton*, gpointer user); void on_fit_x_clicked(GtkButton*, gpointer user); void on_cursors_reset(GtkButton*, gpointer user); @@ -1359,6 +1355,8 @@ void row_menu_fill(RowMenuState& st); GtkWidget* open_row_menu(App& app, int row, double x, double y, GtkWidget* parent, bool autohide = true); void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user); gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user); +void on_side_tab_changed(GObject*, GParamSpec*, gpointer user); +void select_side_tab(App& app, const char* name); void update_side_visibility(App& app); void on_sidebar_toggled(GtkToggleButton* button, gpointer user); void rebuild_signal_panels(App& app); diff --git a/src/ui_cursors.cpp b/src/ui_cursors.cpp index e4d696e..38df490 100644 --- a/src/ui_cursors.cpp +++ b/src/ui_cursors.cpp @@ -14,7 +14,18 @@ namespace ui_impl { /// `%.6g s` is exact enough for a cursor readout and short enough for the panel. void update_cursors_pane(App& app) { - if (app.cursors_body == nullptr || !app.rows->cursors_visible()) return; + if (app.cursors_body == nullptr) return; + if (!app.rows->cursors_visible()) { + // A readout quotes a stored sample; with the instruments off there is no sample it quotes, so + // the cells blank rather than keeping the last ones. `"--"` is the same spelling an + // unavailable cell already uses -- two spellings of one state in one grid would be the defect. + for (const App::CursorLine& line : app.cursors_lines) { + if (line.a != nullptr) gtk_label_set_text(GTK_LABEL(line.a), "--"); + if (line.b != nullptr) gtk_label_set_text(GTK_LABEL(line.b), "--"); + if (line.d != nullptr) gtk_label_set_text(GTK_LABEL(line.d), "--"); + } + return; + } if (app.cursors_pane_mode != static_cast(app.rows->cursor_mode()) || app.cursors_pane_rows != app.rows->row_count() || app.cursors_pane_generation != app.model->generation() || @@ -117,13 +128,11 @@ void on_cursors_toggled(GtkToggleButton* button, gpointer user) { App* app = static_cast(user); const bool on = gtk_toggle_button_get_active(button); app->rows->set_cursors_visible(on); - // The reset button exists for the cursors: with them hidden there is nothing to - // reset, and a button that does nothing reads as broken. - gtk_widget_set_visible(app->cursors_reset, on); - // The pane is the cursors' readout: with them off there is nothing to read. - if (app->cursors_revealer != nullptr) { - gtk_revealer_set_reveal_child(GTK_REVEALER(app->cursors_revealer), on); - } + // Disabled, not hidden, and the same for the reset beside it: with the cursors off there is + // nothing to reset, but the button must stay where the user will look for it. + gtk_widget_set_sensitive(app->cursors_reset, on); + if (app->cursors_panel != nullptr) gtk_widget_set_sensitive(app->cursors_panel, on); + if (!on) update_cursors_pane(*app); if (on) { app->rows->reset_cursors(); rebuild_cursors_pane(*app); diff --git a/src/ui_math.cpp b/src/ui_math.cpp index 9a84cbe..12f79be 100644 --- a/src/ui_math.cpp +++ b/src/ui_math.cpp @@ -18,11 +18,11 @@ void apply_math_channels(App& app) { // `set_math_channels()` takes the model's lock itself -- holding it here is a self-deadlock on a // non-recursive mutex, and it is exactly how the first version of the probe hung the app. app.model->set_math_channels(app.math); - // The structural rebuild above replaced the buffer and dropped its history. A stream's producer - // refills that; a file's nothing does, so the view is re-read -- and with the math set now in place - // the reload computes the derived channels over the file as it goes. reload_file_view(app); rebuild_math_pane(app); + // The math channels are rows of the signal list too -- `append_math_signals_locked()` puts them + // in `signals()` -- so the list is stale until it is rebuilt. + rebuild_signal_panels(app); schedule_layout_save(app); } diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index 4ef4053..e0925ac 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -74,24 +74,36 @@ gboolean exit_probe_step3(gpointer user) { a->window = nullptr; // stop the tick before the widgets go a->dbg_side_w = gtk_widget_get_width(a->side_box); a->dbg_side_h = gtk_widget_get_height(a->side_box); - a->dbg_signal_w = gtk_widget_get_width(a->signal_revealer); - a->dbg_signal_h = gtk_widget_get_height(a->signal_revealer); - a->dbg_trigger_w = gtk_widget_get_width(a->trigger_revealer); - a->dbg_trigger_h = gtk_widget_get_height(a->trigger_revealer); + a->dbg_signal_w = gtk_widget_get_width(a->signal_panel); + a->dbg_signal_h = gtk_widget_get_height(a->signal_panel); + a->dbg_trigger_w = gtk_widget_get_width(a->trigger_panel); + a->dbg_trigger_h = gtk_widget_get_height(a->trigger_panel); // The height of one signal row, which is the thing "the channel toggle is too tall" - // was about. The revealer's own height cannot stand in for it: it also covers the + // was about. The panel's own height cannot stand in for it: it also covers the // heading, the hint and the box margins. if (GtkWidget* first = gtk_widget_get_first_child(a->signal_list)) { a->dbg_signal_row_h = gtk_widget_get_height(first); } if (a->area != nullptr) a->dbg_frames_painted = static_cast(a->area->frames_painted()); - a->dbg_reset_visible = a->cursors_reset != nullptr && gtk_widget_get_visible(a->cursors_reset) ? 1 : 0; - a->dbg_signal_revealed = - gtk_revealer_get_child_revealed(GTK_REVEALER(a->signal_revealer)) ? 1 : 0; - a->dbg_trigger_revealed = - gtk_revealer_get_child_revealed(GTK_REVEALER(a->trigger_revealer)) ? 1 : 0; - a->dbg_side_visible = gtk_widget_get_visible(a->side_box) ? 1 : 0; - a->dbg_side_scroll_after_hide = gtk_widget_get_visible(a->side_scroll) ? 1 : 0; + a->dbg_reset_sensitive = a->cursors_reset != nullptr && gtk_widget_get_sensitive(a->cursors_reset) ? 1 : 0; + // A page, not a revealer: a stack's hidden page measures 0x0 the way an unrevealed revealer + // did, so the reading asks about *state* -- is this page the current one -- and never about size. + const char* page = a->side_stack != nullptr + ? adw_view_stack_get_visible_child_name(ADW_VIEW_STACK(a->side_stack)) + : nullptr; + const int on_signals = page != nullptr && std::strcmp(page, "signals") == 0 ? 1 : 0; + a->dbg_signal_page_current = + on_signals && gtk_widget_get_first_child(a->signal_list) != nullptr ? 1 : 0; + a->dbg_trigger_sensitive = on_signals && gtk_widget_get_sensitive(a->trigger_panel) ? 1 : 0; + // `gtk_widget_get_visible` on the sidebar's GtkBox is always 1: AdwOverlaySplitView hides its + // sidebar child via `gtk_widget_set_child_visible()`, not `gtk_widget_set_visible()`. The useful + // reading is whether the split view is currently showing the sidebar panel. + const int sidebar_shown = a->side_view != nullptr && + adw_overlay_split_view_get_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(a->side_view)) ? 1 : 0; + a->dbg_side_column_after_hide = sidebar_shown; + a->dbg_switcher_visible = + a->side_switcher != nullptr && gtk_widget_get_visible(a->side_switcher) ? 1 : 0; + a->dbg_side_visible = sidebar_shown; if (a->trigger_signal_drop != nullptr) { GListModel* model = gtk_drop_down_get_model(GTK_DROP_DOWN(a->trigger_signal_drop)); a->dbg_trigger_items = model != nullptr ? static_cast(g_list_model_get_n_items(model)) : 0; @@ -100,9 +112,6 @@ gboolean exit_probe_step3(gpointer user) { } // `dbg_window_text` is kept by `sync_window_display()`, the only place that knows the span the // window control is showing: there is no label to read it back from any more. - a->dbg_toggles_visible = - (gtk_widget_get_visible(a->signal_toggle) && gtk_widget_get_visible(a->trigger_toggle)) ? 1 - : 0; if (window != nullptr) gtk_window_destroy(GTK_WINDOW(window)); return G_SOURCE_REMOVE; } @@ -2186,8 +2195,7 @@ gboolean export_probe(gpointer user) { gboolean exit_probe_step1(gpointer user) { App* a = static_cast(user); // Record the panel states before the toggles are exercised. - a->dbg_trigger_revealed_default = - gtk_revealer_get_child_revealed(GTK_REVEALER(a->trigger_revealer)) ? 1 : 0; + a->dbg_trigger_sensitive_default = gtk_widget_get_sensitive(a->trigger_panel) ? 1 : 0; if (a->trigger_toggle != nullptr) { gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE); } @@ -2227,13 +2235,10 @@ void cursors_probe_report(App& a, const char* what) { double my = 0.0; const bool have_mark = a.rows->crossing_at(0, 0, false, mx, my); std::fprintf(stderr, - "[cursors-probe] %-14s mode=%-10s on=%d pane=%d t0=%.6f t1=%.6f dt=%.6f " + "[cursors-probe] %-14s mode=%-10s on=%d pane_sensitive=%d t0=%.6f t1=%.6f dt=%.6f " "T=[%s] lines=%d marks=%d [%s]\n", what, kModes[mode], a.rows->cursors_visible() ? 1 : 0, - a.cursors_revealer != nullptr && - gtk_revealer_get_reveal_child(GTK_REVEALER(a.cursors_revealer)) - ? 1 - : 0, + a.cursors_panel != nullptr && gtk_widget_get_sensitive(a.cursors_panel) ? 1 : 0, a.rows->cursor_a_x(), a.rows->cursor_b_x(), a.rows->cursor_b_x() - a.rows->cursor_a_x(), t_text.c_str(), n, a.rows->crossings_visible(), lines.c_str()); @@ -2498,7 +2503,7 @@ gboolean measures_probe_step(gpointer user) { m.options.show_on_plot = measure_can_show_on_plot(m.type); a->measures.push_back(std::move(m)); } - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->measures_toggle), true); + select_side_tab(*a, "measure"); rebuild_measures_pane(*a); std::fprintf(stderr, "[measures-probe] added %zu measurements on '%s'\n", a->measures.size(), signal.c_str()); @@ -4027,29 +4032,26 @@ gboolean keys_probe(gpointer user) { before_after("t: trigger panel", a->trigger_toggle, AppAction::TriggerPanel); before_after("c: cursors", a->cursors_toggle, AppAction::CursorsPane); - before_after("m: measures", a->measures_toggle, AppAction::MeasuresPane); - before_after("g: math", a->math_toggle, AppAction::MathPane); + { + const std::string before_tab = a->side_tab; + run_app_action(*a, AppAction::SideTabMeasure); + const std::string after_tab = a->side_tab; + char buf[96]; + std::snprintf(buf, sizeof(buf), " m: tab %s->%s |", before_tab.c_str(), after_tab.c_str()); + line += buf; + } + { + const std::string before_tab = a->side_tab; + run_app_action(*a, AppAction::SideTabSignals); + const std::string after_tab = a->side_tab; + char buf[96]; + std::snprintf(buf, sizeof(buf), " g: tab %s->%s |", before_tab.c_str(), after_tab.c_str()); + line += buf; + } before_after("l: layout", a->layout_toggle, AppAction::LayoutEditor); before_after("s: side panel", a->sidebar_toggle, AppAction::SidePanel); - // An accel must be refused while text is being typed. Focus the window entry, send 'm' - // through the real handler, and require the measurements toggle not to move. - int refused = -1; - if (a->window_entry != nullptr) { - gtk_menu_button_popup(GTK_MENU_BUTTON(a->window_button)); - gtk_widget_grab_focus(a->window_entry); - const int before = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a->measures_toggle)) - ? 1 : 0; - const gboolean handled = on_app_key_pressed(nullptr, GDK_KEY_m, 0, - static_cast(0), a); - const int after = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a->measures_toggle)) - ? 1 : 0; - refused = (handled == FALSE && before == after) ? 1 : 0; - gtk_menu_button_popdown(GTK_MENU_BUTTON(a->window_button)); - } - char tail[64]; - std::snprintf(tail, sizeof(tail), " editable focused: 'm' refused=%d\n", refused); - line += tail; + line += "\n"; std::fprintf(stderr, "%s", line.c_str()); std::fflush(stderr); diff --git a/src/ui_signals.cpp b/src/ui_signals.cpp index 38f4730..01b9bc9 100644 --- a/src/ui_signals.cpp +++ b/src/ui_signals.cpp @@ -137,6 +137,7 @@ void on_signal_color_chosen(GObject* source, GAsyncResult* res, gpointer user) { /// buttons made every row about twice this tall, which is what turned a four-signal /// stream into a panel you have to scroll. void rebuild_signal_list(App& app, const std::vector& signals) { + if (app.signal_list == nullptr) return; app.updating_panels = true; GtkWidget* list = app.signal_list; GtkWidget* child = gtk_widget_get_first_child(list); @@ -300,12 +301,6 @@ void rebuild_signal_list(App& app, const std::vector& signals) { app.updating_panels = false; } -void on_signals_toggled(GtkToggleButton* button, gpointer user) { - App* app = static_cast(user); - gtk_revealer_set_reveal_child(GTK_REVEALER(app->signal_revealer), - gtk_toggle_button_get_active(button)); -} - gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user) { App* app = static_cast(user); @@ -396,17 +391,9 @@ void rebuild_signal_panels(App& app) { rebuild_trigger_signals(app); refresh_window_limits(app); - // Nothing to configure before a CONFIG arrives: hide the whole column and its - // toolbar buttons rather than showing two empty panels. The first CONFIG - // reveals the signal panel (the trigger panel stays behind its own button). + // Nothing to configure before a CONFIG arrives: hide the whole column. update_side_visibility(app); - if (have_signals && !app.signals_panel_initialised) { - app.signals_panel_initialised = true; - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.signal_toggle), TRUE); - } if (!have_signals) { - app.signals_panel_initialised = false; - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.signal_toggle), FALSE); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.trigger_toggle), FALSE); } app.signal_generation = app.model->generation(); diff --git a/src/ui_trigger.cpp b/src/ui_trigger.cpp index c8a9b72..b56cbfa 100644 --- a/src/ui_trigger.cpp +++ b/src/ui_trigger.cpp @@ -195,7 +195,9 @@ void on_trigger_toggled(GtkToggleButton* button, gpointer user) { if (on && signal >= 0) { app->model->set_signal_enabled(static_cast(signal), true); } - gtk_revealer_set_reveal_child(GTK_REVEALER(app->trigger_revealer), on); + // Disabled, not hidden: a panel you can still see is a panel you can read, and the settings you + // are about to switch back on must not vanish while you look for them. + gtk_widget_set_sensitive(app->trigger_panel, on); // The event-history button is part of trigger mode and the pane it opens is *on demand*; leaving // trigger mode takes the button away and collapses the pane, so going back to streaming is not a // column of stale events beside the plot. Recording is the other half of that answer, so this is