diff --git a/src/ui.cpp b/src/ui.cpp index 7dcb73d..8b479a8 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -74,7 +74,6 @@ namespace ui_impl { /// fill, the bar does not. // --- persisted layout ------------------------------------------------------- -/// Defined with the events pane; the trigger toggle reveals it. // Defined below; the configurations dialog opens a session with them, and it sits above all three. @@ -3007,8 +3006,7 @@ void note_panel_controls(App& app) { // widget tree walked in child order *is* the layout order, so this is the reading order of the // controls the user sees -- the only way to check an arrangement without a screenshot. const std::pair named[] = { - {app.trigger_toggle, "trigger"}, {app.events_toggle, "events"}, - {app.pause_button, "pause"}, + {app.trigger_toggle, "trigger"}, {app.pause_button, "pause"}, {app.cursors_toggle, "cursors"}, {app.sidebar_toggle, "sidebar"}, {app.menu_button, "menu"}, }; @@ -3631,7 +3629,6 @@ gboolean tick(gpointer user) { } // Outside the lock: it reads and writes the two plots' own windows, not the model. sync_compare_axes(*app); - sync_events_divider(*app); // A file on screen is an event, and gets what a held capture gets: the axis pinned to the event's own // `[-pre, +post]` and the trigger instant marked at x = 0. That *is* the constraint -- the time base is // the event's, so no gesture can take the view outside it -- and the window control reads the event's @@ -4932,12 +4929,10 @@ GtkWidget* build_toolbar(App& app) { app.menu_button = GTK_WIDGET(gtk_builder_get_object(ui, "menu_button")); app.sidebar_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "sidebar_toggle")); 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.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.cursors_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; @@ -4947,17 +4942,6 @@ GtkWidget* build_toolbar(App& app) { // What each control *does*, which a file cannot hold. g_signal_connect(app.sidebar_toggle, "toggled", G_CALLBACK(on_sidebar_toggled), &app); g_signal_connect(app.trigger_toggle, "toggled", G_CALLBACK(on_trigger_toggled), &app); - // The event history is a *trigger* thing, and one click from the mode that produces it. - g_signal_connect(app.events_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* b, gpointer user) { - App* a = static_cast(user); - const bool open = gtk_toggle_button_get_active(b); - gtk_revealer_set_reveal_child(GTK_REVEALER(a->events_revealer), open); - // Through the one function that decides *both*: revealing the child alone leaves an - // invisible pane with no width, which is a button that appears to do nothing. - sync_events_availability(*a); - if (open) refresh_events_pane(*a); - }), - &app); g_signal_connect(app.pause_button, "toggled", G_CALLBACK(on_pause_toggled), &app); g_signal_connect(app.cursors_toggle, "toggled", G_CALLBACK(on_cursors_toggled), &app); return bar; @@ -4969,7 +4953,7 @@ 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 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 + // stream -- the events page, 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"); @@ -4977,7 +4961,6 @@ void on_activate(GtkApplication* application, gpointer user) { app->window = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "main_window")); GtkWidget* side_view = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_view")); app->side_view = side_view; - GtkWidget* events_divider = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "events_divider")); app->plot_box = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "plot_box")); app->plot_split = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "plot_split")); app->plot_overlay = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "plot_overlay")); @@ -4995,7 +4978,7 @@ void on_activate(GtkApplication* application, gpointer user) { app->badge_system = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "badge_system")); // 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 || + if (app->window == nullptr || side_view == nullptr || app->plot_box == nullptr || app->plot_overlay == nullptr || app->plot_split == nullptr || app->status_badge == nullptr || app->status_label == nullptr || app->status_info_button == nullptr || @@ -5195,17 +5178,13 @@ void on_activate(GtkApplication* application, gpointer user) { // `build_source_panels()` has just registered: the Source badge opens the first of them. build_badge_bar(*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. - // A *paned*: the pane's width is the user's to choose, and the plot takes what is left. - // The events pane is the one subtree of the inner divider that is built: whether it exists at all - // depends on trigger mode. - gtk_paned_set_start_child(GTK_PANED(events_divider), build_events_pane(*app)); - // Once, now that both the button and the pane exist: trigger mode is off, so the pane is closed and - // its button is not on the bar -- stated by the one function that decides it rather than by whichever + // The full events list is the `events` page of the slide-over (the dock's Manage button opens it), so + // the plot is never squeezed by it. Once the page and the dock block both exist: nothing is cached yet, + // so both start insensitive -- stated by the one function that decides it rather than by whichever // widget happened to be built with which default. + build_events_pane(*app); + slideover_add_page(*app, "events", "Events", app->events_box); sync_events_availability(*app); - app->events_divider = events_divider; // Two plots, in a paned so a comparison's divider can move: the primary takes everything while the // second is hidden, and the width is the user's once it is not. @@ -5715,14 +5694,17 @@ int group_visible_traces(App& app, int row) { return visible; } +/// The events block's availability as one reading: the rule, the dock block, the page, and the slide-over's +/// open page. Every step that changes recording, replay or the cache prints all of them. void report_events_availability(App& a, const char* what) { - std::fprintf(stderr, "[events] %s: button_visible=%d pane=%d pane_w=%d tick=%d recording=%d\n", - what, - a.events_toggle != nullptr && gtk_widget_get_visible(a.events_toggle) ? 1 : 0, - a.events_revealer != nullptr && - gtk_revealer_get_reveal_child(GTK_REVEALER(a.events_revealer)) ? 1 : 0, - a.events_divider != nullptr ? gtk_paned_get_position(GTK_PANED(a.events_divider)) : -1, - signal_tick_visible(a), a.event_recording ? 1 : 0); + // `available` is the rule `sync_events_availability()` states; the dock block and the page must both + // follow it, and `page` is the slide-over's open page. + const bool available = a.event_recording || a.replaying || a.events_cached; + std::fprintf(stderr, "[events] %s: available=%d dock_sensitive=%d page_sensitive=%d page=%s\n", what, + available ? 1 : 0, + a.dock_events_box != nullptr && gtk_widget_get_sensitive(a.dock_events_box) ? 1 : 0, + a.events_box != nullptr && gtk_widget_get_sensitive(a.events_box) ? 1 : 0, + slideover_page(a).empty() ? "none" : slideover_page(a).c_str()); } @@ -6241,9 +6223,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit app.dbg_signal_empty_label); } { - // The events pane's frame, read from the window: the four widgets the `.ui` gives it, and the list - // being inside the revealer the file builds. A control that lost its id reads 0 here rather than - // showing as a pane missing a button, and `nested=0` is a list that is no longer in the pane at all. + // The events page's frame, read from the window: the widgets the `.ui` gives it, and the list + // being inside the box the file builds. A control that lost its id reads 0 here rather than + // showing as a page missing a button, and `nested=0` is a list that is no longer in the page at all. const auto inside = [](GtkWidget* w, GtkWidget* root) { if (w == nullptr) return 0; for (GtkWidget* p = w; p != nullptr; p = gtk_widget_get_parent(p)) { @@ -6251,10 +6233,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit } return 0; }; - std::printf("[udpscope] events pane: compare=%d accumulate=%d list=%d revealer=%d nested=%d\n", + std::printf("[udpscope] events pane: compare=%d accumulate=%d list=%d nested=%d\n", app.events_compare != nullptr ? 1 : 0, app.events_accumulate != nullptr ? 1 : 0, - app.events_list != nullptr ? 1 : 0, app.events_revealer != nullptr ? 1 : 0, - inside(app.events_list, app.events_revealer)); + app.events_list != nullptr ? 1 : 0, inside(app.events_list, app.events_box)); } std::printf("[udpscope] panels: side=%dx%d (visible=%d) signals=%dx%d row_h=%d " "(page_current=%d) trigger=%dx%d (sensitive=%d) " diff --git a/src/ui/events-pane.ui b/src/ui/events-pane.ui index bc3a04a..f72332a 100644 --- a/src/ui/events-pane.ui +++ b/src/ui/events-pane.ui @@ -2,8 +2,8 @@ - - slide-right + + vertical + 6 + 170 + 6 + 6 + 6 - - vertical + + horizontal 6 - 170 - 6 - 6 - 6 - - horizontal - 6 - - - <b>Events</b> - 1 - 1 - start - - - - - Live - flat - go back to the live stream after replaying a stored event - - + + <b>Events</b> + 1 + 1 + start - - horizontal - 6 - - - Compare - the two selected events in two plots side by side, with the axes synchronised - 1 - 0 - - - - - Accumulate - up to four selected events overlaid in the same traces, translucent, so where they agree the trace is brighter - 1 - 0 - - + + Live + flat + go back to the live stream after replaying a stored event + + + + + + + horizontal + 6 + + + Compare + the two selected events in two plots side by side, with the axes synchronised + 1 + 0 - - 1 - never - - - 0 - multiple - - + + Accumulate + up to four selected events overlaid in the same traces, translucent, so where they agree the trace is brighter + 1 + 0 + + + + + + + 1 + never + + + 0 + multiple diff --git a/src/ui/header-bar.ui b/src/ui/header-bar.ui index 1c21a9c..74248f7 100644 --- a/src/ui/header-bar.ui +++ b/src/ui/header-bar.ui @@ -2,8 +2,7 @@ @@ -39,18 +38,6 @@ pause acquisition - - - vertical - - - - - udpscope-events-symbolic - the event history: the triggered acquisitions kept in the cache, with a picture of each - 0 - - open-menu-symbolic diff --git a/src/ui/main-window.ui b/src/ui/main-window.ui index ba7d785..6817e15 100644 --- a/src/ui/main-window.ui +++ b/src/ui/main-window.ui @@ -3,8 +3,8 @@ @@ -68,96 +67,87 @@ - - horizontal - - 1 - 0 - - - vertical + + vertical + 1 + + + 1 + 1 - - + + vertical + 1 + 4 1 1 - - vertical - 1 - 4 - 1 - 1 - - - horizontal - 1 - - + + horizontal + 1 + + + + + + horizontal + 6 + 36 + 6 + 6 - - + horizontal 6 - 36 - 6 - 6 - - - horizontal - 6 - 1 - - - - - horizontal - 6 - end - - + 1 - + horizontal 6 - 2 - 2 - 6 - - - - - - - starting… - end - 1 - fill - - - - - help-about-symbolic - Stream and buffer detail - center - - + end + + + + + + + horizontal + 6 + 2 + 2 + 6 + + + + + + + starting… + end + 1 + fill + + + + + help-about-symbolic + Stream and buffer detail + center diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 540dd22..c419778 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -789,26 +789,18 @@ struct App { /// `event_recording` is the trigger panel's switch and defaults on -- the cache is only useful if /// it was already on when the event you wanted happened. std::unique_ptr event_store; - /// Whether the cache holds anything, refreshed once a second: what keeps the history button on the bar - /// for a source whose events are already written, even with recording switched off. + /// Whether the cache holds anything, refreshed once a second: what keeps the dock's Events block and the + /// `events` page live for a source whose events are already written, even with recording switched off. bool events_cached = false; bool event_recording = false; - /// The events pane: its list, the revealer that holds it, the recording switch (in the trigger - /// panel, where the trigger's options live), and `events_thumb` -- the picture of the newest + /// The `events` page of the slide-over: its content box, its list, the recording switch (in the + /// trigger panel, where the trigger's options live), and `events_thumb` -- the picture of the newest /// event. Every row carries the picture of *its* event; this is the first one's, which is the one - /// the probe measures and which the pane does not put anywhere special. - GtkWidget* events_revealer = nullptr; + /// the probe measures and which the page does not put anywhere special. + GtkWidget* events_box = nullptr; GtkWidget* events_list = nullptr; GtkWidget* events_thumb = nullptr; GtkWidget* events_record_switch = nullptr; - /// The toolbar's event-history toggle: the pane is opened *on demand* from here (the item's own - /// arrangement), and the toggle only exists while trigger mode does -- there are no events without - /// a trigger, and a button that can never have anything to show is a button spent on nothing. - GtkWidget* events_toggle = nullptr; - /// The paned that holds the events pane and the plot, and the width the divider was last dragged - /// to. 240 by default: enough for the preview and the numbers beside it, and not a plot's worth. - GtkWidget* events_divider = nullptr; - int events_pane_width = 240; /// The Events block of the dock: the box, its list (compact rows, one per `EventRecord`, the record /// attached to each row as `udpscope-event`) and the Manage button. GtkWidget* dock_events_box = nullptr; @@ -1228,6 +1220,9 @@ struct App { /// it keeps for a second latch (it is counting retries, not time). int dbg_events_step = 0; int dbg_events_wait = 0; + /// The plot area's allocated width before the first event was latched (the events probe), compared with + /// its width after: the events list is a slide-over page and no longer a column beside the plot. + int dbg_events_plot_w = 0; /// The event row the probe selected before it activated one, and how many rows were selected. /// Both are read back *after* the activation: a row pointer that is no longer the list's row 0, /// or a smaller count, says the activation rebuilt the list and so threw away the user's scroll @@ -1753,8 +1748,6 @@ void on_pause_toggled(GtkToggleButton* button, gpointer user); void search_jump(App& app, double t); void update_search_panel(App& app); void sync_events_availability(App& app); -/// Restates the events pane's divider position once a frame; see the definition. -void sync_events_divider(App& app); /// The preferences dialog: one window, opened from the overflow menu. `apply_theme()` is separate /// because the theme is the one preference that takes effect without a restart. void open_preferences(App& app); diff --git a/src/ui_dock.cpp b/src/ui_dock.cpp index 23827e6..4812eae 100644 --- a/src/ui_dock.cpp +++ b/src/ui_dock.cpp @@ -11,7 +11,7 @@ // // Below them, the Events block: one compact row per cached event (marker, title, time; no thumbnails), // a Manage button for the full list, and one click on a row to replay it. Its rows are built from the -// same `event_store->list()` and the same `event_row_title()` as the events pane, so the two cannot +// same `event_store->list()` and the same `event_row_title()` as the events page, so the two cannot // disagree. #include "ui_app.hpp" @@ -249,11 +249,16 @@ const char* dock_event_marker(const App& app, const EventRecord& r) { return " "; } +/// Opens the `events` page. The page's list is rebuilt only while the page is open (see `record_event()`), +/// so it is refreshed here, once, before the page shows: the slide-over has no on-open hook, and the list +/// is small. The button is not a child of either list, so the rebuild cannot free the widget it runs from. void on_manage_events(GtkButton*, gpointer user) { - slideover_open(*static_cast(user), "events"); + App& app = *static_cast(user); + refresh_events_pane(app); + slideover_open(app, "events"); } -/// One click on a row replays it, as the pane's activation does: `replay_event()` and then the in-place +/// One click on a row replays it, as the page list's activation does: `replay_event()` and then the in-place /// re-mark. The record is copied first, because nothing it triggers may be allowed to free the row it /// was read from. /// @@ -394,7 +399,7 @@ void dock_rebuild_events(App& app, const std::vector& records) { gtk_widget_set_hexpand(name, true); gtk_widget_set_tooltip_text(name, text.c_str()); gtk_box_append(GTK_BOX(line), name); - // The latch's sequence number, as the pane's third line ends with it: the title already carries the + // The latch's sequence number, as the page row's third line ends with it: the title already carries the // clock, so this is the part of the row that is not in it. const std::string seq = "#" + std::to_string(r.seq); GtkWidget* num = fixed_label(seq.c_str(), kEventSeqChars, GTK_ALIGN_END); diff --git a/src/ui_events.cpp b/src/ui_events.cpp index 596ee17..f719ca9 100644 --- a/src/ui_events.cpp +++ b/src/ui_events.cpp @@ -1,7 +1,9 @@ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari // -// The event cache, seen from the front end: the pane that lists it, the picture each row carries, the\n// replay, and the two group layouts (side-by-side comparison and accumulation) with the axis mirror\n// that keeps a comparison several plots in step. +// The event cache, seen from the front end: the slide-over page that lists it, the picture each row +// carries, the replay, and the two group layouts (side-by-side comparison and accumulation) with the +// axis mirror that keeps a comparison several plots in step. #include "ui_app.hpp" @@ -32,68 +34,19 @@ GtkWidget* make_compare_area(App& app) { return widget; } -/// Whether the event history is reachable at all, and the one place that decides it. It has exactly -/// two preconditions: trigger mode (there are no events without a trigger) and recording (a disabled -/// cache is empty, and an empty column beside the plot is what "off" must not look like). Both the -/// toolbar button and the pane follow from it, so they cannot disagree -- and turning recording off -/// while the pane is open takes the button and the pane with it rather than leaving a stale list. +/// Whether the event history is reachable at all, and the one place that decides it. Recording on (the +/// cache can still grow), an event being read (a replay), or something already in the cache (an event on +/// disk outlives the switch that wrote it) -- not trigger mode: a stored event being examined is a file, +/// and it has no trigger. The dock's Events block and the `events` page follow from it, so they cannot +/// disagree; both stay on screen and go insensitive ("disable, do not hide"), and turning recording off +/// while the page is open leaves the page up, greyed, rather than stale and live. +/// +/// `events_cached` is refreshed once a second from the tick -- the store's list is a directory read, not +/// a per-frame question. void sync_events_availability(App& app) { - if (app.events_toggle == nullptr) return; - // What the divider was last dragged to. Read here, on every change of state, because the close that - // follows is about to set the position to 0 and lose it. - if (app.events_divider != nullptr) { - const int at = gtk_paned_get_position(GTK_PANED(app.events_divider)); - if (at > 1) app.events_pane_width = at; - } - // Available while the cache can still grow (recording on) *or* while it is being read (an event on - // screen). Not trigger mode: a stored event being examined is a file, and it has no trigger -- if the - // pane's life depended on the trigger, opening one would close the list it was opened from. - // ... or whenever the cache *has* something: an event on disk outlives the switch that wrote it, and - // the button that shows it must not go with the switch. `events_cached` is refreshed once a second - // from the tick -- the store's list is a directory read, not a per-frame question. const bool available = app.event_recording || app.replaying || app.events_cached; dock_events_set_sensitive(app, available); - gtk_widget_set_visible(app.events_toggle, available); - if (!available) { - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.events_toggle), FALSE); - gtk_revealer_set_reveal_child(GTK_REVEALER(app.events_revealer), FALSE); - } - const bool open = - available && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.events_toggle)); - // Visibility, not only `reveal_child`: an unrevealed `GtkRevealer` *measures* 0x0 and its box still - // hands it the leftover width, so the collapsed pane reserved an empty column beside the plot. - gtk_widget_set_visible(app.events_revealer, open); - // The position is what gives the pane its room (or none): a paned hands its start child at least its - // minimum unless the position says otherwise. - if (app.events_divider != nullptr) { - gtk_paned_set_position(GTK_PANED(app.events_divider), open ? app.events_pane_width : 0); - } -} - -/// Restates the divider's position, once a frame. -/// -/// `gtk_paned_set_position()` is clamped against the allocation the paned has *now*, and a click on -/// the history button flips the toggle in the same instant the pane becomes visible -- so the position -/// is clamped against a child that still measures zero, the pane opens with no width, and only the -/// next call (a replay's, seconds later) gives it one. Restating it per frame is idempotent, and a -/// position the user dragged is *learned* rather than fought: the width is read back whenever the pane -/// is open and the position is not the one we would set. -void sync_events_divider(App& app) { - if (app.events_divider == nullptr) return; - const bool open = - app.events_revealer != nullptr && gtk_widget_get_visible(app.events_revealer); - const int at = gtk_paned_get_position(GTK_PANED(app.events_divider)); - if (!open) { - if (at != 0) gtk_paned_set_position(GTK_PANED(app.events_divider), 0); - return; - } - if (at > 1 && at != app.events_pane_width) { - app.events_pane_width = at; // the user dragged it: that is the width now - return; - } - if (at != app.events_pane_width) { - gtk_paned_set_position(GTK_PANED(app.events_divider), app.events_pane_width); - } + if (app.events_box != nullptr) gtk_widget_set_sensitive(app.events_box, available); } /// Writes the held event into the cache: its table, a sidecar of what the list shows, and a @@ -133,11 +86,10 @@ bool record_event(App& app) { set_notice(app, "event cached: " + std::to_string(table.columns.size()) + " signals, " + std::to_string(table.rows()) + " samples", 3); - // The dock follows every latch; the pane's rows (with pictures) only while the pane is on screen: - // rebuilding them per latch for a column nobody opened is work the tick owes nothing, and the toggle's - // own handler refreshes when it is asked for. - if (app.events_revealer != nullptr && - gtk_revealer_get_child_revealed(GTK_REVEALER(app.events_revealer))) { + // The dock follows every latch; the page's rows (with pictures) only while the page is open: + // rebuilding them per latch for a list nobody opened is work the tick owes nothing, and the Manage + // button refreshes the list when it is asked for. + if (slideover_page(app) == "events") { refresh_events_pane(app); } else { dock_rebuild_events(app); @@ -269,7 +221,7 @@ bool replay_event(App& app, const EventRecord& record) { // left alone afterwards -- restated here and never per frame. // The axis is the event's own and the tick pins it there (`pin_event_window`): nothing to frame or // home here beyond the window control's own reading, which the tick keeps in step. - // The pane is available through a replay, and this is what keeps it open. + // The events block is available through a replay, and this is what keeps it so. sync_events_availability(app); set_notice(app, "replaying " + record.stem + " (" + std::to_string(record.signals) + " signals, " + @@ -825,9 +777,9 @@ void mark_events_on_screen(App& app) { } } -/// The events pane's rows, newest first: each row is the picture of *that* event beside the +/// The events page's rows, newest first: each row is the picture of *that* event beside the /// sidecar's numbers, because what tells one event from another is its shape, and one picture for -/// the whole pane describes whichever event happens to be newest rather than the one being read. The +/// the whole page describes whichever event happens to be newest rather than the one being read. The /// numbers are one line each: a list that has to load the samples to describe an event is a list that /// cannot show thirty of them. static void rebuild_events_pane_list(App& app, std::vector& records) { @@ -934,7 +886,7 @@ static void rebuild_events_pane_list(App& app, std::vector& records gtk_widget_set_halign(name, GTK_ALIGN_START); gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END); // A cap, not only an ellipsis: `max-width-chars` bounds a label's *natural* width, and the row's - // natural width is the pane's minimum -- which is the floor the divider cannot go below. + // natural width is the page's minimum -- which is the floor the list cannot go below. gtk_label_set_max_width_chars(GTK_LABEL(name), 22); gtk_widget_add_css_class(name, "udpscope-event-title"); gtk_box_append(GTK_BOX(box), name); @@ -1001,28 +953,27 @@ void on_event_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) { mark_events_on_screen(app); } -/// The pane itself: a collapsible column on the left, a header and the list of events -- each row -/// carrying its own picture. Built once with the rest of the window, and opened on demand from the -/// toolbar's history button, which is itself only there while trigger mode and recording are both -/// on (`sync_events_availability()`). +/// The events list: a header and the list of events, each row carrying its own picture. Built once with +/// the rest of the window and returned as the content of the slide-over's `events` page, which the +/// dock's Manage button opens. GtkWidget* build_events_pane(App& app) { - // The pane's frame -- the revealer that slides it in, the header with the way back to the live stream, the - // two actions that read the selection, and the list inside its scroller -- is a UI file. The rows are not: + // The list's frame -- the header with the way back to the live stream, the two actions that read the + // selection, and the list inside its scroller -- is a UI file. The rows are not: // there is one per latched event, and the whole list is rebuilt whenever the cache changes. The two // buttons' sensitivity is `update_group_buttons()`'s, which is why the file only starts them insensitive. GtkBuilder* ui = ui_load(app, "/org/udpscope/ui/events-pane.ui"); - app.events_revealer = GTK_WIDGET(gtk_builder_get_object(ui, "events_pane")); + app.events_box = GTK_WIDGET(gtk_builder_get_object(ui, "events_box")); app.events_compare = GTK_WIDGET(gtk_builder_get_object(ui, "events_compare")); app.events_accumulate = GTK_WIDGET(gtk_builder_get_object(ui, "events_accumulate")); GtkWidget* live = GTK_WIDGET(gtk_builder_get_object(ui, "events_live")); app.events_list = GTK_WIDGET(gtk_builder_get_object(ui, "events_list")); // The heading row, where the contextual help's "?" joins (see `ui_context_help.cpp`). app.events_head = GTK_WIDGET(gtk_builder_get_object(ui, "events_head")); - if (app.events_revealer == nullptr || app.events_compare == nullptr || + if (app.events_box == nullptr || app.events_compare == nullptr || app.events_accumulate == nullptr || live == nullptr || app.events_list == nullptr) { - // A missing id is a mistake in the `.ui`: the pane comes up empty rather than taking the window with it, + // A missing id is a mistake in the `.ui`: the page comes up empty rather than taking the window with it, // and `[udpscope] events pane:` reads the difference. - return app.events_revealer != nullptr ? app.events_revealer : gtk_revealer_new(); + return app.events_box != nullptr ? app.events_box : gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); } // What the pane *does*, which a file cannot hold: the two group actions read the selection, the list @@ -1039,7 +990,7 @@ GtkWidget* build_events_pane(App& app) { update_group_buttons(*static_cast(user)); }), &app); - return app.events_revealer; + return app.events_box; } /// The recording switch, which lives here because it is a trigger option and not a pane option: it diff --git a/src/ui_layout.cpp b/src/ui_layout.cpp index 122082f..4ba5e04 100644 --- a/src/ui_layout.cpp +++ b/src/ui_layout.cpp @@ -377,7 +377,7 @@ void apply_trigger_layout(App& app, const LayoutState::Trigger& t) { } write_trigger_widgets(app); // The state is applied by *pressing the same control a user would*: the handler is what arms, tells - // the events button, greys the body and syncs the pane's own switch. Deliberately outside the + // the dock's events block, greys the body and syncs the page's own switch. Deliberately outside the // `updating_panels` window `apply_layout()` uses for the panels, because a toggle set inside it // would emit `toggled` into a handler that returns at once and nothing would happen. if (app.trigger_toggle != nullptr) { diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index 36e8d10..265084d 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -5653,12 +5653,15 @@ gboolean events_probe_step(gpointer user) { if (a->window == nullptr) return G_SOURCE_REMOVE; if (a->dbg_events_step == 0) { - // Before anything: streaming, no trigger, recording **off** (the default) -- so no button, no - // pane, and nothing written to disk that the user did not ask to have written. + // Before anything: streaming, no trigger, recording **off** (the default) -- so the dock's block and + // the page are insensitive (on screen, not hidden), and nothing is written to disk that the user did + // not ask to have written. The plot's width is taken here, with no event yet: `plot_w_stable` compares + // it with the width after the first latch. + a->dbg_events_plot_w = a->area_widget != nullptr ? gtk_widget_get_width(a->area_widget) : -1; report_events_availability(*a, "live, recording off"); - // The three clicks a user makes: recording on in the trigger panel, trigger mode on, then the - // history button (which is only there once the first two have happened -- that is the item's rule, - // and this is where it is asserted). + // The clicks a user makes: recording on in the trigger panel, trigger mode on, then the dock's + // Manage button (the `events` page is reachable at any time; it is insensitive until something is + // recorded or cached, and this is where that rule is asserted). gtk_switch_set_active(GTK_SWITCH(a->events_record_switch), TRUE); // An 80% pre-trigger share, so the event that latches is *not* symmetric: the window a click // opens must be the event's own span, and with a symmetric event the live window would look @@ -5671,27 +5674,19 @@ gboolean events_probe_step(gpointer user) { gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE); } report_events_availability(*a, "after trigger on"); - if (a->events_toggle != nullptr) { - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->events_toggle), TRUE); - } - // Read straight after the click, before anything else can call the availability rule: this is the - // reading that says the *button* opens the pane, and it is the one a later replay used to mask. - report_events_availability(*a, "after history button"); - // ... and again a few frames later, still with no replay in the way: this is the *click alone* + slideover_open(*a, "events"); + // Read straight after the open, before anything else can call the availability rule. + report_events_availability(*a, "after events page opened"); + // ... and again a few frames later, still with no replay in the way: this is the *open alone* // path a user takes, which a later replay's own availability call used to mask. events_probe_next(*a, 5, 500); return G_SOURCE_REMOVE; } if (a->dbg_events_step == 5) { - GtkWidget* box = a->events_revealer != nullptr - ? gtk_revealer_get_child(GTK_REVEALER(a->events_revealer)) - : nullptr; - std::fprintf(stderr, "[events] click alone: pane=%d pane_w=%d pane_box=%d\n", - a->events_revealer != nullptr && gtk_widget_get_visible(a->events_revealer) ? 1 : 0, - a->events_divider != nullptr ? gtk_paned_get_position(GTK_PANED(a->events_divider)) - : -1, - box != nullptr ? gtk_widget_get_width(box) : -1); + std::fprintf(stderr, "[events] open alone: page=%s page_box=%d\n", + slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(), + a->events_box != nullptr ? gtk_widget_get_width(a->events_box) : -1); events_probe_next(*a, 1, 2500); return G_SOURCE_REMOVE; } @@ -5748,11 +5743,15 @@ gboolean events_probe_step(gpointer user) { } std::fprintf(stderr, " newest: stem=%s dur=%.6f pre=%.6f post=%.6f edge=%.6f signals=%zu samples=%zu " - "rate=%.6g seq=%llu thumb_bytes=%ld recorded=%d pane=%d", + "rate=%.6g seq=%llu thumb_bytes=%ld recorded=%d page=%d", r.stem.c_str(), r.duration_s, r.pre_s, r.post_s, r.edge, r.signals, r.samples, r.rate_hz, static_cast(r.seq), thumb_bytes, - a->event_recording ? 1 : 0, - gtk_revealer_get_reveal_child(GTK_REVEALER(a->events_revealer)) ? 1 : 0); + a->event_recording ? 1 : 0, slideover_page(*a) == "events" ? 1 : 0); + // The first latch must not have moved the plot: the list is a slide-over page over the plot's + // edge, and the dock is a fixed column, so nothing about an event changes the plot's width. + const int plot_w_now = a->area_widget != nullptr ? gtk_widget_get_width(a->area_widget) : -1; + std::fprintf(stderr, " plot_w_stable=%d plot_w=%d/%d", plot_w_now == a->dbg_events_plot_w ? 1 : 0, + a->dbg_events_plot_w, plot_w_now); } std::fprintf(stderr, "\n"); std::fflush(stderr); @@ -5799,18 +5798,14 @@ gboolean events_probe_step(gpointer user) { const int selected_now = int(g_list_length(still)); g_list_free(still); std::fprintf(stderr, - "[events] activated row0: replaying=%d pane=%d pane_w=%d pane_box=%d tick=%d " + "[events] activated row0: replaying=%d page=%s page_box=%d tick=%d " "window=%.3f axis=[%.3f,%.3f] marker=%.3f " "thumbnail=%d thumb_size=%dx%d thumb_visible=%d rows_kept=%d selected=%d/%d\n", a->replaying ? 1 : 0, - a->events_revealer != nullptr && gtk_widget_get_visible(a->events_revealer) ? 1 : 0, - a->events_divider != nullptr ? gtk_paned_get_position(GTK_PANED(a->events_divider)) - : -1, - // The pane's *rendered* width: the position is what we asked for, this is what the - // pane actually got -- a divider at 240 with a zero-width pane is still no pane. - a->events_revealer != nullptr && gtk_revealer_get_child(GTK_REVEALER(a->events_revealer)) != nullptr - ? gtk_widget_get_width(gtk_revealer_get_child(GTK_REVEALER(a->events_revealer))) - : -1, + slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(), + // The page's *rendered* width: what it actually got -- an open page with a zero-width + // list is still no list. + a->events_box != nullptr ? gtk_widget_get_width(a->events_box) : -1, signal_tick_visible(*a), a->rows->time_window(), a->rows->row(0).plot != nullptr ? a->rows->row(0).plot->x_range(a->rows->row(0).axis_x).min @@ -5873,9 +5868,9 @@ gboolean events_probe_step(gpointer user) { std::fprintf(stderr, "[events] live again: replaying=%d waveform=%d\n", a->replaying ? 1 : 0, a->waveform_name.empty() ? 0 : 1); report_live_restored(*a, "after replay"); - // The pane's *other* precondition, through the real switch, in both trigger states: disabling - // recording has to take the button and the pane with it, and arming the trigger afterwards must not - // bring back a pane over an empty cache. + // The block's *other* precondition, through the real switch, in both trigger states: disabling + // recording with an empty cache has to grey the dock's block and the page, and arming the trigger + // afterwards must not wake them over an empty cache. gtk_switch_set_active(GTK_SWITCH(a->events_record_switch), FALSE); report_events_availability(*a, "recording off, trigger off"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE); @@ -5901,11 +5896,11 @@ gboolean events_group_probe_step(gpointer user) { return G_SOURCE_REMOVE; } a->dbg_events_wait = 0; - // Open the pane first: its list is only rebuilt while the pane is *up*, so a selection made against - // a collapsed pane selects rows that are not there. - if (a->events_toggle != nullptr && - !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a->events_toggle))) { - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->events_toggle), TRUE); + // Open the page first: its list is only rebuilt while the page is *open* (or by the Manage button), so + // a selection made against a closed page selects rows that are not current. + if (slideover_page(*a) != "events") { + refresh_events_pane(*a); + slideover_open(*a, "events"); } // The selection *is* the input, so select the two newest rows the way a user does and press the // button: `on_group_clicked()` reads the list, not a parameter. @@ -5937,13 +5932,13 @@ gboolean events_group_probe_step(gpointer user) { std::lock_guard lock(a->model->mutex()); std::fprintf(stderr, "[events] compare: kind=%d rows=%d channels=%zu samples0=%llu traces0=%d traces1=%d " - "window=%.3f recording=%d pane=%d\n", + "window=%.3f recording=%d page=%s\n", static_cast(a->group_kind), a->rows->row_count(), a->model->channels().size(), static_cast(a->model->live().values(0).produced()), group_visible_traces(*a, 0), group_visible_traces(*a, 1), a->rows->time_window(), a->event_recording ? 1 : 0, - a->events_revealer != nullptr && gtk_widget_get_visible(a->events_revealer) ? 1 : 0); + slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str()); report_dock_events(*a, "comparison up"); events_group_probe_next(*a, 46, 400); return G_SOURCE_REMOVE; diff --git a/src/ui_signals.cpp b/src/ui_signals.cpp index ddbc2d6..c2bf88c 100644 --- a/src/ui_signals.cpp +++ b/src/ui_signals.cpp @@ -656,9 +656,6 @@ GtkWidget* build_signal_panel(App& app) { return box; } -/// The pane's availability as one reading: the history button on the bar, the pane revealed, and the -/// switch that decides half of it. Both the button and the pane follow trigger mode *and* recording, -/// so every step that changes either prints all three. /// Whether the signal list's first row still carries its acquire tick. A file view has nothing to /// acquire -- the samples are stored -- so the tick goes; in live mode it is the control that decides /// what is acquired at all. diff --git a/src/ui_trigger.cpp b/src/ui_trigger.cpp index 67217de..64da317 100644 --- a/src/ui_trigger.cpp +++ b/src/ui_trigger.cpp @@ -594,10 +594,10 @@ void on_trigger_toggled(GtkToggleButton* button, gpointer user) { // unreachable exactly when a user wanted to change them.) What that costs is the reading: the // `trigger panel sensitive` line is now a regression guard, and a `0` there means the // configuration has been locked out again. - // 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 - // `sync_events_availability()`'s call and not the trigger's alone. + // The dock's Events block and the `events` page follow what is recorded or cached, not trigger mode + // alone: leaving trigger mode does not empty the cache, and a replay is not in trigger mode at all. + // Recording is the other half of that answer, so this is `sync_events_availability()`'s call and not + // the trigger's alone. sync_events_availability(*app); // Arming is a *runtime* control: with trigger mode off there is no engine to arm, and the arm buttons // are the one part of this pane that is not a setting. (The body itself stays live -- see above.) Both @@ -721,7 +721,7 @@ GtkWidget* build_trigger_panel(App& app) { gtk_switch_set_active(GTK_SWITCH(app.events_record_switch), app.event_recording); gtk_widget_set_tooltip_text(app.events_record_switch, "keep every triggered acquisition in the event cache, with a " - "picture of each; the history button goes with it"); + "picture of each; the dock's Events block and page follow it"); g_signal_connect(app.events_record_switch, "notify::active", G_CALLBACK(on_events_record_toggled), &app); // Not `row_for()`: that expands its child, which is what a spin button wants and what a switch must not diff --git a/src/ui_widgets.cpp b/src/ui_widgets.cpp index cb5931a..8bd5f54 100644 --- a/src/ui_widgets.cpp +++ b/src/ui_widgets.cpp @@ -55,8 +55,8 @@ void install_css() { "popover.udpscope-menu button label { min-height: 0; }\n" "popover.udpscope-menu image { min-width: 12px; min-height: 0; }\n" "popover.udpscope-menu separator { margin: 4px 0; }\n" - // The events pane: the row's time is what a person scans for, so it is the one thing in the - // column that is not dim; the thumbnail gets an edge, or a dark plot on a dark pane has none. + // The events page: the row's time is what a person scans for, so it is the one thing in the + // list that is not dim; the thumbnail gets an edge, or a dark plot on a dark panel has none. ".udpscope-event-title { font-weight: bold; }\n" ".udpscope-event-thumb { border: 1px solid alpha(currentColor, 0.15); border-radius: 4px; }\n" "row.udpscope-compact button { min-height: 20px; min-width: 20px; padding: 0 3px; }\n"