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; }