Feature/source plugins #1

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

No files matched your search

+5
View File
@@ -1797,7 +1797,12 @@ void dock_measure_value(App& app, std::size_t i, const std::string& text);
/// 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);
/// The dock shows at most this many events (the newest), then one dim "... N more, see Manage" row.
inline constexpr std::size_t kDockEventsMax = 24;
/// The first lists the store itself; the second takes a list the caller already holds (so one refresh
/// reads the cache directory once, not twice). Both keep the list's vertical scroll across the rebuild.
void dock_rebuild_events(App& app);
void dock_rebuild_events(App& app, const std::vector<EventRecord>& records);
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
+74 -16
View File
@@ -18,6 +18,7 @@
#include <gtk/gtk.h>
#include <algorithm>
#include <cstring>
#include <ctime>
#include <string>
@@ -238,7 +239,7 @@ 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;
constexpr int kEventSeqChars = 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`).
@@ -255,6 +256,11 @@ void on_manage_events(GtkButton*, gpointer user) {
/// 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.
///
/// Safety depends on `replay_event()` never rebuilding the dock's list (it must go through
/// `dock_mark_events()`, which rewrites labels in place): a rebuild here would free this very row, and
/// `row`, `record` and the list box's own emission of `row-activated` would all be dangling. The copy
/// guards the record only, not the row. If a replay ever has to refresh the cache's view, defer it.
void on_dock_event_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
App& app = *static_cast<App*>(user);
if (row == nullptr) return;
@@ -266,15 +272,6 @@ void on_dock_event_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
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) {
@@ -323,9 +320,47 @@ GtkWidget* build_events_dock(App& app) {
void dock_rebuild_events(App& app) {
if (app.dock_events_list == nullptr || app.event_store == nullptr) return;
dock_rebuild_events(app, app.event_store->list());
}
namespace {
/// Puts the list's vertical scroll back after a refill. The adjustment's bounds are only updated at the
/// next allocation, so the value set at once is clamped against the old ones; an idle (which runs after the
/// frame that laid the new rows out) sets it again against the new bounds, where the clamp is exact.
void restore_dock_scroll(GtkWidget* list, double value) {
GtkWidget* scroller = gtk_widget_get_ancestor(list, GTK_TYPE_SCROLLED_WINDOW);
if (scroller == nullptr) return;
GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroller));
gtk_adjustment_set_value(adj, value);
struct Pending {
GtkAdjustment* adj;
double value;
};
auto* p = new Pending{GTK_ADJUSTMENT(g_object_ref(adj)), value};
g_idle_add_full(
G_PRIORITY_DEFAULT_IDLE,
[](gpointer d) -> gboolean {
auto* q = static_cast<Pending*>(d);
gtk_adjustment_set_value(q->adj, q->value);
g_object_unref(q->adj);
delete q;
return G_SOURCE_REMOVE;
},
p, nullptr);
}
} // namespace
void dock_rebuild_events(App& app, const std::vector<EventRecord>& records) {
if (app.dock_events_list == nullptr) return;
GtkListBox* list = GTK_LIST_BOX(app.dock_events_list);
GtkWidget* scroller = gtk_widget_get_ancestor(app.dock_events_list, GTK_TYPE_SCROLLED_WINDOW);
const double scroll =
scroller != nullptr
? gtk_adjustment_get_value(gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroller)))
: 0.0;
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);
@@ -340,7 +375,11 @@ void dock_rebuild_events(App& app) {
gtk_list_box_append(list, row);
return;
}
for (const EventRecord& r : records) {
// The newest `kDockEventsMax` only: the list is newest first, and a block that builds four widgets per
// cached event is a cost that grows with a cache the user may have set to unbounded.
const std::size_t shown = std::min(records.size(), kDockEventsMax);
for (std::size_t i = 0; i < shown; ++i) {
const EventRecord& r = records[i];
GtkWidget* row = gtk_list_box_row_new();
GtkWidget* line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_widget_set_margin_start(line, 6);
@@ -355,10 +394,13 @@ void dock_rebuild_events(App& app) {
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);
// The latch's sequence number, as the pane's third line ends with it: the title already carries the
// clock, so this is the part of the row that is not in it.
const std::string seq = "#" + std::to_string(r.seq);
GtkWidget* num = fixed_label(seq.c_str(), kEventSeqChars, GTK_ALIGN_END);
gtk_widget_set_tooltip_text(num, ("latch " + seq).c_str());
gtk_widget_add_css_class(num, "dim-label");
gtk_box_append(GTK_BOX(line), num);
g_object_set_data(G_OBJECT(row), "udpscope-event-marker", marker);
g_object_set_data(G_OBJECT(row), "udpscope-event-title", name);
@@ -367,6 +409,22 @@ void dock_rebuild_events(App& app) {
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), line);
gtk_list_box_append(list, row);
}
if (records.size() > shown) {
// A last row with no record (like the placeholder: `dock_mark_events()` skips it, and it does nothing
// when activated). Fixed text and the same ellipsis as the rest, so it cannot widen the dock.
const std::string more = "\xE2\x80\xA6 " + std::to_string(records.size() - shown) + " more, see Manage";
GtkWidget* label = fixed_label(more.c_str(), kEventTitleChars + kEventSeqChars, GTK_ALIGN_START);
gtk_widget_add_css_class(label, "dim-label");
gtk_widget_set_tooltip_text(label, more.c_str());
gtk_widget_set_margin_start(label, 6);
gtk_widget_set_margin_top(label, 4);
gtk_widget_set_margin_bottom(label, 4);
GtkWidget* row = gtk_list_box_row_new();
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), label);
gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(row), false);
gtk_list_box_append(list, row);
}
restore_dock_scroll(app.dock_events_list, scroll);
}
void dock_mark_events(App& app) {
+15 -9
View File
@@ -782,10 +782,8 @@ void attach_preview_zoom(GtkWidget* frame, const std::string& path) {
GConnectFlags(0));
}
/// A row's first line: when the event was latched, how long it is, and whether it is the one on
/// 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.
/// Whether the event is the one on screen: the one question the title's "(on screen)" and the dock's
/// filled mark both ask.
bool event_on_screen(const App& app, const EventRecord& r) {
return std::string(app.waveform_name) == r.file;
}
@@ -797,6 +795,10 @@ bool event_in_group(const App& app, const EventRecord& r) {
[&](const EventRecord& g) { return g.file == r.file; });
}
/// A row's first line: when the event was latched, how long it is, and whether it is the one on
/// 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.
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{};
@@ -828,7 +830,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.
static void rebuild_events_pane_list(App& app) {
static void rebuild_events_pane_list(App& app, std::vector<EventRecord>& records) {
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);
@@ -838,8 +840,8 @@ static void rebuild_events_pane_list(App& app) {
// the one that finds out.
app.events_thumb = nullptr;
if (app.event_store == nullptr) return;
// Not const: a record whose preview had to be redrawn learns its thumbnail's path here.
std::vector<EventRecord> records = app.event_store->list();
// `records` is the caller's list, not const: a record whose preview had to be redrawn learns its
// thumbnail's path here.
if (records.empty()) {
GtkWidget* empty = gtk_label_new("no events yet -- arm the trigger");
gtk_widget_add_css_class(empty, "dim-label");
@@ -975,8 +977,12 @@ static void rebuild_events_pane_list(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);
// One read of the cache directory for both views (none without a store, which the pane's rebuild
// and the dock's both treat as nothing to list).
std::vector<EventRecord> records;
if (app.event_store != nullptr) records = app.event_store->list();
rebuild_events_pane_list(app, records);
if (app.event_store != nullptr) dock_rebuild_events(app, records);
}
/// Clicking a row replays it: the same loader `--open` uses, on the event's file. The acquisition is
+39 -9
View File
@@ -5579,14 +5579,19 @@ void report_live_restored(App& a, const char* what) {
/// 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) {
/// `marked` is the index of the first one (-1 for none). `max_ok` is the cap: no more than
/// `kDockEventsMax` event rows and one "more" row. `expected_marked` is read after a replay of a record
/// the caller names by file (`expected_file`): 1 when exactly one dock row is filled and its record is that
/// one (record identity, not a count), 0 otherwise, -1 when the caller names none.
static void report_dock_events(App& a, const char* what, const std::string& expected_file = std::string()) {
std::vector<std::string> dock_titles;
std::string filled_file;
int dock_children = 0;
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) {
const auto titles = [&members, &filled_file](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;
@@ -5603,22 +5608,41 @@ static void report_dock_events(App& a, const char* what) {
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;
if (first != nullptr && *first < 0) {
*first = i;
filled_file = static_cast<const EventRecord*>(g_object_get_data(G_OBJECT(row), "udpscope-event"))->file;
}
}
}
};
for (GtkWidget* row = a.dock_events_list != nullptr ? gtk_widget_get_first_child(a.dock_events_list) : nullptr;
row != nullptr; row = gtk_widget_get_next_sibling(row)) {
++dock_children;
}
titles(a.dock_events_list, &dock_titles, &on_screen, &marked);
titles(a.events_list, &pane_titles, nullptr, nullptr);
// The dock shows the newest `kDockEventsMax` only: its titles are the pane's first ones, in order.
if (pane_titles.size() > kDockEventsMax) pane_titles.resize(kDockEventsMax);
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",
"on_screen=%d marked=%d members=%d max_ok=%d expected_marked=%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);
marked, members, dock_children <= static_cast<int>(kDockEventsMax) + 1 ? 1 : 0,
expected_file.empty() ? -1 : (on_screen == 1 && filled_file == expected_file ? 1 : 0), what);
std::fflush(stderr);
}
/// The file of the record the Nth row of a list carries ("" for a row without one).
static std::string listed_file(GtkWidget* list, int index) {
if (list == nullptr) return std::string();
GtkListBoxRow* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(list), index);
if (row == nullptr) return std::string();
const auto* r = static_cast<const EventRecord*>(g_object_get_data(G_OBJECT(row), "udpscope-event"));
return r != nullptr ? r->file : std::string();
}
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);
@@ -5805,7 +5829,9 @@ 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");
// The pane replayed its own row 0; the dock's list is the same list, so the dock's filled marker has to
// be on the row whose record is that one.
report_dock_events(*a, "after pane replay", listed_file(a->events_list, 0));
// 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.
{
@@ -5813,6 +5839,9 @@ gboolean events_probe_step(gpointer user) {
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 clicked_file =
real ? static_cast<const EventRecord*>(g_object_get_data(G_OBJECT(row), "udpscope-event"))->file
: std::string();
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
@@ -5821,7 +5850,7 @@ gboolean events_probe_step(gpointer user) {
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");
report_dock_events(*a, "after dock click", clicked_file);
}
events_probe_next(*a, 2, 1200);
return G_SOURCE_REMOVE;
@@ -6054,10 +6083,11 @@ gboolean events_group_probe_step(gpointer user) {
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;
const std::string clicked_file = real ? listed_file(a->dock_events_list, 1) : std::string();
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");
report_dock_events(*a, "second dock click", clicked_file);
}
return G_SOURCE_REMOVE;
}