diff --git a/src/ui.cpp b/src/ui.cpp index 8b479a8..46cb210 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -4187,7 +4187,7 @@ gboolean tick(gpointer user) { next_channels = now_c + G_TIME_SPAN_SECOND; app->last_channels_meas = now_c; // The cache's own question, on the same cadence as the channel dump: a directory read per second, - // and the button's availability re-decided only when the answer changed. + // and the dock block's and the events page's availability re-decided only when the answer changed. if (app->event_store != nullptr) { const bool cached = !app->event_store->list().empty(); if (cached != app->events_cached) { @@ -5708,8 +5708,7 @@ void report_events_availability(App& a, const char* what) { } -/// Walks the whole event-cache loop the way a user does: trigger mode on (the real toggle, which is -/// what reveals the pane), a latch recorded automatically, the cache listed, the newest event +/// Walks the whole event-cache loop the way a user does: trigger mode on (the real toggle), a latch recorded automatically, the cache listed, the newest event /// replayed through the loader `--open` uses, and *Live* taken back. What it reports is what each /// step must be able to do -- a cache nothing can replay is a directory of files, and a replay with /// no way back is a one-way door. diff --git a/src/ui_app.hpp b/src/ui_app.hpp index c419778..9a94b0d 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -1945,6 +1945,12 @@ GdkTexture* scale_texture(GdkTexture* source, int width, int height); EventThumbCache* event_thumb_cache(App& app); GdkTexture* event_preview_texture(App& app, const std::string& path); void refresh_events_pane(App& app); +/// Opens the `events` page. **Every route that opens that page must use this** and never `slideover_open()` +/// directly: the page's list is rebuilt only while the page is open (see `record_event()`), and the +/// slide-over has no on-open hook, so the list is refreshed here, once, before the page shows. The refresh +/// is skipped when the page is already open -- it is current then, and a rebuild would drop the +/// Compare/Accumulate selection and the scroll position. Returns what `slideover_open()` returns. +bool open_events_page(App& app); /// Re-marks which row is the event currently on screen, **without rebuilding the list**. /// /// The mark is the only thing about a row that a replay changes -- the picture and the numbers diff --git a/src/ui_dock.cpp b/src/ui_dock.cpp index 4812eae..b68ab6b 100644 --- a/src/ui_dock.cpp +++ b/src/ui_dock.cpp @@ -249,13 +249,10 @@ 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. +/// Opens the `events` page through `open_events_page()` (which refreshes the list only if it is not +/// already open). 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) { - App& app = *static_cast(user); - refresh_events_pane(app); - slideover_open(app, "events"); + open_events_page(*static_cast(user)); } /// One click on a row replays it, as the page list's activation does: `replay_event()` and then the in-place diff --git a/src/ui_events.cpp b/src/ui_events.cpp index f719ca9..6703e41 100644 --- a/src/ui_events.cpp +++ b/src/ui_events.cpp @@ -622,7 +622,7 @@ EventThumbCache* event_thumb_cache(App& app) { // Created on first use and owned by the list. Nothing used to create it, and a missing cache is // not a slow path here but a *blank* one: `event_preview_texture()` answers from the cache or not // at all, so every row came up without a picture -- which is what the previews were reported for. - // Owned by the list because nothing else has the pane's lifetime: textures of a list that no + // Owned by the list because nothing else has the events page's lifetime: textures of a list that no // longer exists are so much held memory. cache = new EventThumbCache(); g_object_set_data_full(G_OBJECT(app.events_list), "udpscope-event-thumbs", cache, @@ -707,7 +707,7 @@ GdkTexture* event_preview_texture(App& app, const std::string& path) { /// /// Through GTK's own tooltip rather than a popover of ours, for lifetime: a tooltip is shown, placed, /// dismissed and destroyed by GTK, and it takes no pointer grab -- a popover per row would be a -/// parented widget to unparent on every rebuild of the pane, and a grab to lose (see the GTK +/// parented widget to unparent on every rebuild of the page, and a grab to lose (see the GTK /// invariants on both). The texture is decoded in the handler and dropped with the tooltip's picture, /// so a hover costs nothing once it is over and the row cache keeps holding only the small one. gboolean on_preview_query_tooltip(GtkWidget*, int, int, gboolean, GtkTooltip* tooltip, gpointer user) { @@ -842,7 +842,7 @@ static void rebuild_events_pane_list(App& app, std::vector& records // to grow and the text keeps the rest of the width. gtk_widget_set_size_request(frame, kEventThumbW, kEventThumbH); // `halign` START, not the default FILL: a filling widget is given whatever the row has left over, - // which made the picture as wide as the pane and cropped by the height it was pinned to. + // which made the picture as wide as the page and cropped by the height it was pinned to. gtk_widget_set_halign(frame, GTK_ALIGN_START); gtk_widget_set_valign(frame, GTK_ALIGN_CENTER); gtk_widget_add_css_class(frame, "udpscope-event-thumb"); @@ -870,7 +870,7 @@ static void rebuild_events_pane_list(App& app, std::vector& records ++unreadable; } gtk_box_append(GTK_BOX(line), frame); - // The newest event is the first row: what the probe measures, and nothing the pane treats + // The newest event is the first row: what the probe measures, and nothing the page treats // specially. if (app.events_thumb == nullptr) app.events_thumb = shot; @@ -926,10 +926,11 @@ static void rebuild_events_pane_list(App& app, std::vector& records } } -/// Both views of the cache, rebuilt from the same list: the pane's rows (with their pictures) and the -/// dock's compact ones. The dock follows even when the pane's list does not exist. +/// Both views of the cache, rebuilt from the same list: the events page's rows (with their pictures) and the +/// dock block's compact ones. The dock follows even when the page's list does not exist. To *open* the page +/// use `open_events_page()`, which calls this only when the page is not already open. void refresh_events_pane(App& app) { - // One read of the cache directory for both views (none without a store, which the pane's rebuild + // One read of the cache directory for both views (none without a store, which the page's rebuild // and the dock's both treat as nothing to list). std::vector records; if (app.event_store != nullptr) records = app.event_store->list(); @@ -937,9 +938,14 @@ void refresh_events_pane(App& app) { if (app.event_store != nullptr) dock_rebuild_events(app, records); } +bool open_events_page(App& app) { + if (slideover_page(app) != "events") refresh_events_pane(app); + return slideover_open(app, "events"); +} + /// Clicking a row replays it: the same loader `--open` uses, on the event's file. The acquisition is /// stopped by `replay_event()` -- a live producer would overwrite the samples within a frame -- so the -/// pane's own *Live* button is the way back. +/// page's own *Live* button is the way back. void on_event_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) { App& app = *static_cast(user); if (row == nullptr) return; @@ -976,7 +982,7 @@ GtkWidget* build_events_pane(App& app) { 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 + // What the page *does*, which a file cannot hold: the two group actions read the selection, the list // replays an event on a double click, and the header's button goes back to the live stream. g_signal_connect(app.events_compare, "clicked", G_CALLBACK(on_group_clicked), &app); g_signal_connect(app.events_accumulate, "clicked", G_CALLBACK(on_group_clicked), &app); @@ -993,9 +999,10 @@ GtkWidget* build_events_pane(App& app) { return app.events_box; } -/// The recording switch, which lives here because it is a trigger option and not a pane option: it -/// decides whether latches are written at all, and the pane that lists them must not be the only -/// place to reach the control that stops them. Flipping it re-decides the pane's availability. +/// The recording switch, which lives here because it is a trigger option and not an events-page option: it +/// decides whether latches are written at all, and the page that lists them must not be the only +/// place to reach the control that stops them. Flipping it re-decides the availability of the dock's +/// Events block and of the page. void on_events_record_toggled(GObject* o, GParamSpec*, gpointer user) { App& app = *static_cast(user); app.event_recording = gtk_switch_get_active(GTK_SWITCH(o)); diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index 265084d..8b884a7 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -5674,7 +5674,7 @@ 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"); - slideover_open(*a, "events"); + open_events_page(*a); // 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* @@ -5747,11 +5747,15 @@ gboolean events_probe_step(gpointer user) { 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, 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. + } + // 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. Both + // widths must be real (> 0): two unrealised -1s are equal and would read as stable. + { 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, " plot_w_stable=%d plot_w=%d/%d", + (plot_w_now > 0 && 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); @@ -5898,10 +5902,7 @@ gboolean events_group_probe_step(gpointer user) { a->dbg_events_wait = 0; // 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"); - } + open_events_page(*a); // 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. GtkListBox* list = GTK_LIST_BOX(a->events_list);