From 148d1168d6b5ad812fdaf761696eab3855a77e9f Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Mon, 12 Oct 2026 16:27:56 +0200 Subject: [PATCH] ui: the events pane's frame is a .ui file src/ui/events-pane.ui holds the pane: the GtkRevealer that slides it in, the header with the way back to the live stream, the Compare and Accumulate actions, and the list inside its scroller. The rows stay in code -- one per latched event, rebuilt whenever the cache changes -- and their sensitivity stays with update_group_buttons(), which is why the file only starts the two actions insensitive. Two statements moved into the file along with the widgets, because they are about what the pane is for rather than about a default: multiple selection, and activation on *double* click. With single-click activation the plain click that builds a selection would replay the event instead. Read back as `events pane: compare=1 accumulate=1 list=1 revealer=1 nested=1`, where `nested` is the list being inside the revealer -- the one thing a lost id cannot show. The [events] probe is the other half and drives the pane end to end: recording, trigger, the history button, a row click and the replay, then Compare and Accumulate, so the file's buttons are read by being clicked. check_ui: 13 ok, 0 failed. --- docs/claude/probe-readings.md | 12 +++++ src/ui.cpp | 16 ++++++ src/ui/events-pane.ui | 90 ++++++++++++++++++++++++++++++++ src/ui/udpscope-ui.gresource.xml | 1 + src/ui_events.cpp | 87 +++++++----------------------- 5 files changed, 139 insertions(+), 67 deletions(-) create mode 100644 src/ui/events-pane.ui diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 27de394..81dd08a 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -1202,3 +1202,15 @@ button), and the list the channel rows go in. `buttons=0` is a `.ui` that lost t cannot add a channel, with nothing else complaining -- and `title=` is the file's own text read back, so a `.ui` that lost the label shows as `title='-'`. `[math-probe]` is the other half of the reading: it drives the add and the editor through the same code a user does. + +## `events pane:` + +``` +[udpscope] events pane: compare=1 accumulate=1 list=1 revealer=1 nested=1 +``` + +The events pane's frame read from the window: the two group actions, the list, and the revealer the `.ui` +builds, plus `nested` — the list being *inside* that revealer, which is the one thing a lost id cannot show +(a list parented elsewhere reads 1 everywhere else). `[events]` is the other half, and it drives the pane end +to end: recording off, recording on, the trigger, the history button that reveals it, a row click and the +replay, then Compare and Accumulate — so the file's buttons are read by being used. diff --git a/src/ui.cpp b/src/ui.cpp index fd566c4..763c9de 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -5446,6 +5446,22 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit buttons, app.math_list != nullptr ? 1 : 0, GTK_IS_LABEL(title) ? gtk_label_get_text(GTK_LABEL(title)) : "-"); } + { + // 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. + const auto inside = [](GtkWidget* w, GtkWidget* root) { + if (w == nullptr) return 0; + for (GtkWidget* p = w; p != nullptr; p = gtk_widget_get_parent(p)) { + if (p == root) return 1; + } + return 0; + }; + std::printf("[udpscope] events pane: compare=%d accumulate=%d list=%d revealer=%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)); + } 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 " "window_cap=%.1fs\n", diff --git a/src/ui/events-pane.ui b/src/ui/events-pane.ui new file mode 100644 index 0000000..bc3a04a --- /dev/null +++ b/src/ui/events-pane.ui @@ -0,0 +1,90 @@ + + + + + + + slide-right + + + vertical + 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 + + + + + + + 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 + + + + + + + 1 + never + + + 0 + multiple + + + + + + + + diff --git a/src/ui/udpscope-ui.gresource.xml b/src/ui/udpscope-ui.gresource.xml index 2f4c05c..7fd5c92 100644 --- a/src/ui/udpscope-ui.gresource.xml +++ b/src/ui/udpscope-ui.gresource.xml @@ -19,6 +19,7 @@ measures-panel.ui math-panel.ui math-editor.ui + events-pane.ui trigger-panel.ui signals-panel.ui diff --git a/src/ui_events.cpp b/src/ui_events.cpp index b69370a..a693bde 100644 --- a/src/ui_events.cpp +++ b/src/ui_events.cpp @@ -769,84 +769,37 @@ void on_event_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) { /// toolbar's history button, which is itself only there while trigger mode and recording are both /// on (`sync_events_availability()`). GtkWidget* build_events_pane(App& app) { - GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); - // A floor, not a width: the divider sets the width and this is what the pane needs to stay usable at - // its narrowest. - gtk_widget_set_size_request(box, 170, -1); - gtk_widget_set_margin_start(box, 6); - gtk_widget_set_margin_end(box, 6); - gtk_widget_set_margin_top(box, 6); + // 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: + // 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_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")); + if (app.events_revealer == 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, + // and `[udpscope] events pane:` reads the difference. + return app.events_revealer != nullptr ? app.events_revealer : gtk_revealer_new(); + } - GtkWidget* head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); - GtkWidget* title = gtk_label_new(nullptr); - gtk_label_set_markup(GTK_LABEL(title), "Events"); - gtk_widget_set_hexpand(title, TRUE); - gtk_widget_set_halign(title, GTK_ALIGN_START); - gtk_box_append(GTK_BOX(head), title); - - // The two group actions, under the header and above the list they read: what they act on is the - // selection, so they belong where the selection is. - GtkWidget* actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); - app.events_compare = gtk_button_new_with_label("Compare"); - gtk_widget_set_tooltip_text(app.events_compare, - "the two selected events in two plots side by side, with the axes " - "synchronised"); - gtk_widget_set_hexpand(app.events_compare, TRUE); - gtk_widget_set_sensitive(app.events_compare, FALSE); + // What the pane *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); - gtk_box_append(GTK_BOX(actions), app.events_compare); - app.events_accumulate = gtk_button_new_with_label("Accumulate"); - gtk_widget_set_tooltip_text(app.events_accumulate, - "up to four selected events overlaid in the same traces, translucent, " - "so where they agree the trace is brighter"); - gtk_widget_set_hexpand(app.events_accumulate, TRUE); - gtk_widget_set_sensitive(app.events_accumulate, FALSE); g_signal_connect(app.events_accumulate, "clicked", G_CALLBACK(on_group_clicked), &app); - gtk_box_append(GTK_BOX(actions), app.events_accumulate); - gtk_box_append(GTK_BOX(box), actions); - - GtkWidget* live = gtk_button_new_with_label("Live"); - gtk_widget_add_css_class(live, "flat"); - gtk_widget_set_tooltip_text(live, "go back to the live stream after replaying a stored event"); g_signal_connect(live, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) { go_live_again(*static_cast(user)); }), &app); - gtk_box_append(GTK_BOX(head), live); - gtk_box_append(GTK_BOX(box), head); - - app.events_list = gtk_list_box_new(); - // The decoded pictures live as long as the list does. - g_object_set_data_full(G_OBJECT(app.events_list), "udpscope-event-thumbs", new EventThumbCache, - [](gpointer p) { - auto* cache = static_cast(p); - for (const auto& entry : *cache) { - if (entry.second != nullptr) g_object_unref(entry.second); - } - delete cache; - }); - // Multiple, and activation on *double* click: the list's other job is choosing which events to - // compare or accumulate, and with single-click activation a plain click -- the gesture that builds a - // selection -- would replay the event instead. - gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(app.events_list), FALSE); - gtk_list_box_set_selection_mode(GTK_LIST_BOX(app.events_list), GTK_SELECTION_MULTIPLE); g_signal_connect(app.events_list, "row-activated", G_CALLBACK(on_event_row_activated), &app); - g_signal_connect(app.events_list, "selected-rows-changed", G_CALLBACK(+[](GtkListBox*, - gpointer user) { + g_signal_connect(app.events_list, "selected-rows-changed", + G_CALLBACK(+[](GtkListBox*, gpointer user) { update_group_buttons(*static_cast(user)); }), &app); - GtkWidget* scroll = gtk_scrolled_window_new(); - gtk_widget_set_vexpand(scroll, TRUE); - gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER, - GTK_POLICY_AUTOMATIC); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), app.events_list); - gtk_box_append(GTK_BOX(box), scroll); - - app.events_revealer = gtk_revealer_new(); - gtk_revealer_set_transition_type(GTK_REVEALER(app.events_revealer), - GTK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT); - gtk_revealer_set_child(GTK_REVEALER(app.events_revealer), box); return app.events_revealer; }