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 @@
+
+
+
+
+
+
+
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.uimath-panel.uimath-editor.ui
+ events-pane.uitrigger-panel.uisignals-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;
}