Feature/source plugins #1

Merged
martino merged 130 commits from feature/source-plugins into main 2026-11-04 07:35:21 +01:00
5 changed files with 287 additions and 5 deletions
Showing only changes of commit baf1c73b21 - Show all commits

No files matched your search

+3 -1
View File
@@ -5172,8 +5172,10 @@ void on_activate(GtkApplication* application, gpointer user) {
gtk_widget_set_valign(app->measures_panel, GTK_ALIGN_START);
slideover_add_page(*app, "cursors", "Cursors", app->cursors_panel);
slideover_add_page(*app, "measure", "Measurements", app->measures_panel);
// The dock is the compact readouts alone (step 4 adds the events to it).
// The dock: the compact readouts, and under them the compact events list.
gtk_box_append(GTK_BOX(app->dock_page), build_readouts_dock(*app));
gtk_box_append(GTK_BOX(app->dock_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
gtk_box_append(GTK_BOX(app->dock_page), build_events_dock(*app));
// The cursors start off, so their readout starts insensitive -- stated here by the one place that
// knows the default, and the *body* and not the panel, because the panel's heading carries the
+19
View File
@@ -809,6 +809,11 @@ struct App {
/// 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;
GtkWidget* dock_events_list = nullptr;
GtkWidget* dock_events_manage = nullptr;
/// The comparison's second plot: its own placeholder, area and `RowSet`, and the paned that holds
/// the two side by side. `rows_b` exists only once the second area is built, and the area is hidden
/// (so the paned gives it no room and no handle) unless a comparison is up.
@@ -1786,6 +1791,20 @@ void dock_rebuild_measures(App& app);
void dock_cursor_cell(App& app, std::size_t line, const std::string& a, const std::string& b,
const std::string& d);
void dock_measure_value(App& app, std::size_t i, const std::string& text);
/// The Events block under the readouts (`src/ui_dock.cpp`): compact rows (marker, title, time; no
/// thumbnails) from `event_store->list()`, newest first, and a Manage button for the `events` page.
/// `dock_rebuild_events()` follows every change of what is *cached*, `dock_mark_events()` every change
/// of what is *on screen* (in place, no rebuild), `dock_events_set_sensitive()` is the one writer of the
/// block's sensitivity. All three are no-ops before the block is built.
GtkWidget* build_events_dock(App& app);
void dock_rebuild_events(App& app);
void dock_mark_events(App& app);
void dock_events_set_sensitive(App& app, bool available);
/// A row's first line, and the two questions its mark is made of (`ui_events.cpp`): the one function
/// for the pane and the dock, so the two cannot disagree about which event is on screen.
std::string event_row_title(App& app, const EventRecord& r);
bool event_on_screen(const App& app, const EventRecord& r);
bool event_in_group(const App& app, const EventRecord& r);
/// The signal chip of a measurement's row, in the colour every other view names that signal with.
std::string measure_chip_markup(App& app, const std::string& signal);
std::string row_unit(App& app, int row);
+163
View File
@@ -8,13 +8,20 @@
// once and hand the same variable to the page's label and to `dock_cursor_cell()` / `dock_measure_value()`,
// so the two views cannot disagree. Every label is ellipsized at a fixed `width-chars`, so the dock's
// layout does not depend on what a value reads; the full text is the label's tooltip.
//
// 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
// disagree.
#include "ui_app.hpp"
#include <gtk/gtk.h>
#include <cstring>
#include <ctime>
#include <string>
#include <vector>
namespace udpscope {
namespace ui_impl {
@@ -226,5 +233,161 @@ void dock_measure_value(App& app, std::size_t i, const std::string& text) {
set_value_text(app.dock_measure_labels[i], text);
}
namespace {
/// The widths of an event row's three cells, in characters. Fixed, so the Events block is as wide with
/// one event as with thirty (the marker is one cell wide even when it is empty).
constexpr int kEventTitleChars = 18;
constexpr int kEventTimeChars = 8;
/// The one-character mark of a row: filled for the event on screen, hollow for a member of the group on
/// screen, blank for the rest. Both questions are the ones `event_row_title()` asks (`ui_events.cpp`).
const char* dock_event_marker(const App& app, const EventRecord& r) {
if (event_on_screen(app, r)) return "\xE2\x97\x8F"; // U+25CF black circle
if (event_in_group(app, r)) return "\xE2\x97\x8B"; // U+25CB white circle
return " ";
}
void on_manage_events(GtkButton*, gpointer user) {
slideover_open(*static_cast<App*>(user), "events");
}
/// One click on a row replays it, as the pane'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.
void on_dock_event_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
App& app = *static_cast<App*>(user);
if (row == nullptr) return;
const auto* record =
static_cast<const EventRecord*>(g_object_get_data(G_OBJECT(row), "udpscope-event"));
if (record == nullptr) return;
const EventRecord copy = *record;
replay_event(app, copy);
mark_events_on_screen(app);
}
std::string event_clock(const EventRecord& r) {
const std::time_t t = static_cast<std::time_t>(r.wall_time);
std::tm tm{};
localtime_r(&t, &tm);
char clock[32];
std::strftime(clock, sizeof(clock), "%H:%M:%S", &tm);
return clock;
}
} // namespace
GtkWidget* build_events_dock(App& app) {
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
gtk_widget_set_margin_top(box, 6);
gtk_widget_set_margin_bottom(box, 6);
gtk_widget_set_margin_start(box, 6);
gtk_widget_set_margin_end(box, 6);
GtkWidget* head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget* title = gtk_label_new("Events");
gtk_widget_set_halign(title, GTK_ALIGN_START);
gtk_widget_set_hexpand(title, true);
gtk_widget_add_css_class(title, "heading");
gtk_box_append(GTK_BOX(head), title);
GtkWidget* manage = gtk_button_new_with_label("Manage");
gtk_widget_add_css_class(manage, "flat");
gtk_widget_set_tooltip_text(manage, "the events' full list and actions");
g_signal_connect(manage, "clicked", G_CALLBACK(on_manage_events), &app);
gtk_box_append(GTK_BOX(head), manage);
gtk_box_append(GTK_BOX(box), head);
GtkWidget* list = gtk_list_box_new();
gtk_list_box_set_selection_mode(GTK_LIST_BOX(list), GTK_SELECTION_NONE);
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(list), true);
gtk_widget_add_css_class(list, "boxed-list");
g_signal_connect(list, "row-activated", G_CALLBACK(on_dock_event_activated), &app);
// A fixed height (about four rows), so the block is as tall with no events as with thirty and the
// readouts above it do not move when one is latched.
GtkWidget* scroller = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(scroller), 112);
gtk_scrolled_window_set_max_content_height(GTK_SCROLLED_WINDOW(scroller), 112);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroller), list);
gtk_box_append(GTK_BOX(box), scroller);
app.dock_events_box = box;
app.dock_events_list = list;
app.dock_events_manage = manage;
// Insensitive until the first availability sync says otherwise: on screen and greyed, never hidden.
gtk_widget_set_sensitive(box, FALSE);
dock_rebuild_events(app);
return box;
}
void dock_rebuild_events(App& app) {
if (app.dock_events_list == nullptr || app.event_store == nullptr) return;
GtkListBox* list = GTK_LIST_BOX(app.dock_events_list);
gtk_list_box_remove_all(list);
const std::vector<EventRecord> records = app.event_store->list();
if (records.empty()) {
GtkWidget* none = gtk_label_new("no events yet");
gtk_widget_set_halign(none, GTK_ALIGN_START);
gtk_widget_add_css_class(none, "caption");
gtk_widget_add_css_class(none, "dim-label");
gtk_widget_set_margin_start(none, 6);
gtk_widget_set_margin_top(none, 4);
gtk_widget_set_margin_bottom(none, 4);
GtkWidget* row = gtk_list_box_row_new();
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), none);
gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(row), false);
gtk_list_box_append(list, row);
return;
}
for (const EventRecord& r : records) {
GtkWidget* row = gtk_list_box_row_new();
GtkWidget* line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_widget_set_margin_start(line, 6);
gtk_widget_set_margin_end(line, 6);
gtk_widget_set_margin_top(line, 3);
gtk_widget_set_margin_bottom(line, 3);
GtkWidget* marker = fixed_label(dock_event_marker(app, r), 1, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(line), marker);
const std::string text = event_row_title(app, r);
GtkWidget* name = fixed_label(text.c_str(), kEventTitleChars, GTK_ALIGN_START);
gtk_widget_set_hexpand(name, true);
gtk_widget_set_tooltip_text(name, text.c_str());
gtk_box_append(GTK_BOX(line), name);
const std::string clock = event_clock(r);
GtkWidget* when = fixed_label(clock.c_str(), kEventTimeChars, GTK_ALIGN_END);
gtk_widget_add_css_class(when, "dim-label");
gtk_box_append(GTK_BOX(line), when);
g_object_set_data(G_OBJECT(row), "udpscope-event-marker", marker);
g_object_set_data(G_OBJECT(row), "udpscope-event-title", name);
g_object_set_data_full(G_OBJECT(row), "udpscope-event", new EventRecord(r),
[](gpointer p) { delete static_cast<EventRecord*>(p); });
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), line);
gtk_list_box_append(list, row);
}
}
void dock_mark_events(App& app) {
if (app.dock_events_list == nullptr) return;
for (GtkWidget* row = gtk_widget_get_first_child(app.dock_events_list); row != nullptr;
row = gtk_widget_get_next_sibling(row)) {
const auto* record =
static_cast<const EventRecord*>(g_object_get_data(G_OBJECT(row), "udpscope-event"));
GtkWidget* marker = GTK_WIDGET(g_object_get_data(G_OBJECT(row), "udpscope-event-marker"));
GtkWidget* name = GTK_WIDGET(g_object_get_data(G_OBJECT(row), "udpscope-event-title"));
// The "no events yet" placeholder carries none of the three: skipped.
if (record == nullptr || marker == nullptr || name == nullptr) continue;
gtk_label_set_text(GTK_LABEL(marker), dock_event_marker(app, *record));
set_value_text(name, event_row_title(app, *record));
}
}
void dock_events_set_sensitive(App& app, bool available) {
if (app.dock_events_box == nullptr) return;
gtk_widget_set_sensitive(app.dock_events_box, available);
}
} // namespace ui_impl
} // namespace udpscope
+28 -4
View File
@@ -52,6 +52,7 @@ void sync_events_availability(App& app) {
// 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);
@@ -132,11 +133,14 @@ bool record_event(App& app) {
set_notice(app, "event cached: " + std::to_string(table.columns.size()) + " signals, " +
std::to_string(table.rows()) + " samples",
3);
// Only while the pane is on screen: rebuilding every row 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.
// 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))) {
refresh_events_pane(app);
} else {
dock_rebuild_events(app);
}
return true;
}
@@ -580,6 +584,7 @@ bool build_event_group(App& app, const std::vector<EventRecord>& records, EventG
// Fit on open, once: the group's own span, framed.
// As above: the tick pins the axis to the group's own span and marks the trigger.
sync_events_availability(app);
mark_events_on_screen(app); // the dock marks the group's members
return true;
}
@@ -781,6 +786,17 @@ void attach_preview_zoom(GtkWidget* frame, const std::string& path) {
/// screen. One function because two callers write it -- the row builder and the in-place
/// re-mark -- and a mark that reads differently depending on which of them last ran would be
/// worse than no mark.
bool event_on_screen(const App& app, const EventRecord& r) {
return std::string(app.waveform_name) == r.file;
}
/// Whether the event is one of the members of the comparison or accumulation on screen.
bool event_in_group(const App& app, const EventRecord& r) {
if (app.group_kind == EventGroupKind::None) return false;
return std::any_of(app.group.begin(), app.group.end(),
[&](const EventRecord& g) { return g.file == r.file; });
}
std::string event_row_title(App& app, const EventRecord& r) {
const std::time_t t = static_cast<std::time_t>(r.wall_time);
std::tm tm{};
@@ -789,11 +805,12 @@ std::string event_row_title(App& app, const EventRecord& r) {
std::strftime(clock, sizeof(clock), "%H:%M:%S", &tm);
char title[160];
std::snprintf(title, sizeof(title), "%s %.3f s%s", clock, r.duration_s,
std::string(app.waveform_name) == r.file ? " (on screen)" : "");
event_on_screen(app, r) ? " (on screen)" : "");
return title;
}
void mark_events_on_screen(App& app) {
dock_mark_events(app);
if (app.events_list == nullptr) return;
for (GtkWidget* row = gtk_widget_get_first_child(app.events_list); row != nullptr;
row = gtk_widget_get_next_sibling(row)) {
@@ -811,7 +828,7 @@ void mark_events_on_screen(App& app) {
/// the whole pane 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.
void refresh_events_pane(App& app) {
static void rebuild_events_pane_list(App& app) {
if (app.events_list == nullptr) return;
while (GtkWidget* child = gtk_widget_get_first_child(app.events_list)) {
gtk_list_box_remove(GTK_LIST_BOX(app.events_list), child);
@@ -955,6 +972,13 @@ void refresh_events_pane(App& app) {
}
}
/// 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.
void refresh_events_pane(App& app) {
rebuild_events_pane_list(app);
dock_rebuild_events(app);
}
/// 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.
+74
View File
@@ -5576,6 +5576,49 @@ void report_live_restored(App& a, const char* what) {
std::fflush(stderr);
}
/// The dock's Events block against the pane's list. `same_titles` compares each dock row's title label
/// with the pane row's, in order (both are rebuilt from the same list by `refresh_events_pane()`, and both
/// are marked by `mark_events_on_screen()`); `on_screen` counts the dock rows with the filled marker, and
/// `marked` is the index of the first one (-1 for none).
static void report_dock_events(App& a, const char* what) {
std::vector<std::string> dock_titles;
std::vector<std::string> pane_titles;
int on_screen = 0;
int marked = -1;
int members = 0;
const auto titles = [&members](GtkWidget* list, std::vector<std::string>* out, int* filled, int* first) {
if (list == nullptr) return;
int i = 0;
for (GtkWidget* row = gtk_widget_get_first_child(list); row != nullptr;
row = gtk_widget_get_next_sibling(row), ++i) {
if (g_object_get_data(G_OBJECT(row), "udpscope-event") == nullptr) continue;
GtkWidget* name = GTK_WIDGET(g_object_get_data(G_OBJECT(row), "udpscope-event-title"));
if (name == nullptr) continue;
out->push_back(gtk_label_get_text(GTK_LABEL(name)));
GtkWidget* marker = GTK_WIDGET(g_object_get_data(G_OBJECT(row), "udpscope-event-marker"));
if (filled != nullptr && marker != nullptr &&
std::strcmp(gtk_label_get_text(GTK_LABEL(marker)), "\xE2\x97\x8B") == 0) {
++members;
}
if (filled != nullptr && marker != nullptr &&
std::strcmp(gtk_label_get_text(GTK_LABEL(marker)), "\xE2\x97\x8F") == 0) {
++*filled;
if (first != nullptr && *first < 0) *first = i;
}
}
};
titles(a.dock_events_list, &dock_titles, &on_screen, &marked);
titles(a.events_list, &pane_titles, nullptr, nullptr);
std::fprintf(stderr,
"[events] dock events: rows=%zu pane_rows=%zu same_titles=%d sensitive=%d dock_w=%d "
"on_screen=%d marked=%d members=%d at=%s\n",
dock_titles.size(), pane_titles.size(), dock_titles == pane_titles ? 1 : 0,
a.dock_events_box != nullptr && gtk_widget_is_sensitive(a.dock_events_box) ? 1 : 0,
a.dock_events_box != nullptr ? gtk_widget_get_width(a.dock_events_box) : -1, on_screen,
marked, members, what);
std::fflush(stderr);
}
void events_probe_next(App& a, int step, guint delay_ms) {
a.dbg_events_step = step;
g_timeout_add(delay_ms, events_probe_step, &a);
@@ -5762,6 +5805,24 @@ gboolean events_probe_step(gpointer user) {
a->events_thumb != nullptr && gtk_widget_get_mapped(a->events_thumb) ? 1 : 0,
rows_kept ? 1 : 0, selected_now, a->dbg_events_selected);
std::fflush(stderr);
report_dock_events(*a, "after pane replay");
// One click on a dock row, the gesture: a *second* event when there is one, so the marker has to
// move. Through `row-activated`, which is what a single click emits.
{
GtkListBox* dock = GTK_LIST_BOX(a->dock_events_list);
GtkListBoxRow* row = gtk_list_box_get_row_at_index(dock, 1);
if (row == nullptr) row = gtk_list_box_get_row_at_index(dock, 0);
const bool real = row != nullptr && g_object_get_data(G_OBJECT(row), "udpscope-event") != nullptr;
const std::string before = a->waveform_name;
if (real) g_signal_emit_by_name(dock, "row-activated", row);
// Already replaying (the pane's click above), so `replaying` alone cannot tell the click from the
// state it found: `waveform_changed` says the click opened the row's own event (1 when it was a
// second event, 0 when there was only the one to click again).
std::fprintf(stderr, "[events] dock replay: clicked=%d replaying=%d waveform_changed=%d\n",
real ? 1 : 0, a->replaying ? 1 : 0, before != a->waveform_name ? 1 : 0);
std::fflush(stderr);
report_dock_events(*a, "after dock click");
}
events_probe_next(*a, 2, 1200);
return G_SOURCE_REMOVE;
}
@@ -5834,6 +5895,7 @@ gboolean events_group_probe_step(gpointer user) {
// The live settings this leg will be read against, taken *here* rather than inherited from the
// replay leg: a group is a second, independent way into a file view, and its way back has to put
// back what was on screen when the button was pressed.
report_dock_events(*a, "two events latched");
capture_live_expectation(*a);
if (a->events_compare != nullptr) g_signal_emit_by_name(a->events_compare, "clicked");
// Read one step later: a rebuilt row's traces are bound by the next *frame*, so a reading taken in
@@ -5853,6 +5915,7 @@ gboolean events_group_probe_step(gpointer user) {
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);
report_dock_events(*a, "comparison up");
events_group_probe_next(*a, 46, 400);
return G_SOURCE_REMOVE;
}
@@ -5985,6 +6048,17 @@ gboolean events_group_probe_step(gpointer user) {
if (a->dbg_events_step == 61) {
report_live_restored(*a, "after a group");
// A single click on the *second* dock row now that the app is live again: a different event than the
// last (there are several by now), so the filled marker has to move to that row.
{
GtkListBox* dock = GTK_LIST_BOX(a->dock_events_list);
GtkListBoxRow* row = gtk_list_box_get_row_at_index(dock, 1);
const bool real = row != nullptr && g_object_get_data(G_OBJECT(row), "udpscope-event") != nullptr;
if (real) g_signal_emit_by_name(dock, "row-activated", row);
std::fprintf(stderr, "[events] dock replay: clicked=%d replaying=%d (second click, live before)\n",
real ? 1 : 0, a->replaying ? 1 : 0);
report_dock_events(*a, "second dock click");
}
return G_SOURCE_REMOVE;
}