Updated local storage

This commit is contained in:
Martino Ferrari committed 2026-10-02 11:30:54 +02:00
1 parent 30db44eaa0
commit 2218aed839
10 files changed
+548 -5

No files matched your search

+187 -3
View File
@@ -17,6 +17,7 @@
#include "acquisition.hpp"
#include "config.hpp"
#include "export.hpp"
#include "layout_store.hpp"
#include "model.hpp"
#include "plot_host.hpp"
@@ -121,6 +122,13 @@ struct App {
GtkWidget* cursors_toggle = nullptr;
GtkWidget* cursors_reset = nullptr;
/// The persisted-layout file this data source maps to (--server/--port), a
/// debounced save handle, and whether the file has been applied once the
/// CONFIG arrived.
std::string source_host;
std::uint16_t source_port = 0;
guint layout_save_source = 0;
bool layout_loaded = false;
unsigned signal_generation = ~0u;
bool updating_panels = false; ///< guards signal handlers during a rebuild
bool relayout_pending = false;
@@ -292,6 +300,121 @@ GtkWidget* labeled_box(const char* label, GtkWidget* child) {
return box;
}
/// Same, without expanding the child. Header-bar children expand through the
/// title's centre box (a start-packed expanding widget is measured into it), so
/// the window control there stretched all the way to the title; panels want the
/// fill, the bar does not.
// --- persisted layout -------------------------------------------------------
void apply_time_window(App& app, double seconds);
int signal_color_index(const std::vector<SignalInfo>& signals, std::size_t signal);
void rebuild_signal_panels(App& app);
/// Reads the session layout back out of the model and rows: the display window,
/// each row's slot signal names, and each signal's calibration.
LayoutState collect_layout(App& app) {
LayoutState st;
st.window_s = app.rows->time_window();
ScopeModel& model = *app.model;
for (int i = 0; i < app.rows->row_count(); ++i) {
LayoutState::Row row;
const RowSet::Row& r = app.rows->row(i);
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
row.signals.push_back(r.signal_name[static_cast<std::size_t>(s)]);
}
st.rows.push_back(std::move(row));
}
for (std::size_t si = 0; si < model.signals().size(); ++si) {
for (std::size_t c = 0; c < model.channels().size(); ++c) {
const ChannelInfo& ch = model.channels()[c];
if (ch.signal != si) continue;
st.calibration.push_back({model.signals()[si].name, ch.gain, ch.offset});
break;
}
}
return st;
}
/// Writes the layout file (a debounced schedule lands here once). Errors go to
/// stderr; success is silent -- a save per click must not narrate.
void save_layout_now(App& app) {
if (app.source_port == 0) return;
if (!save_layout(layout_path(app.source_host, app.source_port), collect_layout(app))) {
std::fprintf(stderr, "[udpscope] failed to write the layout file\n");
std::fflush(stderr);
}
}
/// One-shot debounce: a burst of changes (a drop, a row insert, a calibration
/// commit in the same second) writes once, 400 ms after the last one.
gboolean layout_save_tick(gpointer user) {
App* app = static_cast<App*>(user);
app->layout_save_source = 0;
save_layout_now(*app);
return G_SOURCE_REMOVE;
}
void schedule_layout_save(App& app) {
if (app.source_port == 0) return;
if (app.layout_save_source != 0) return;
app.layout_save_source = g_timeout_add(400, layout_save_tick, &app);
}
/// Applies a loaded layout: rows, slot assignments, calibration, window. Names
/// that no longer exist in the stream are skipped rather than freeing their slot
/// (a stream that renamed a signal must not blank the user's layout).
void apply_layout(App& app, const LayoutState& st) {
ScopeModel& model = *app.model;
const auto& signals = model.signals();
int rows = static_cast<int>(st.rows.size());
if (rows > RowSet::kMaxRows) rows = RowSet::kMaxRows;
if (rows > 0) app.rows->set_row_count(rows);
for (int i = 0; i < rows; ++i) {
int slot = 0;
for (const std::string& name : st.rows[static_cast<std::size_t>(i)].signals) {
if (slot >= RowSet::kSlotsPerRow) break;
if (name.empty()) continue;
for (std::size_t si = 0; si < signals.size(); ++si) {
if (signals[si].name != name) continue;
// Enabling is what starts acquisition (a loaded plot nobody acquires would
// come back blank), and it is what a drop does for the signal it places.
app.rows->assign(i, slot, name, name, signal_color_index(signals, si));
model.set_signal_enabled(si, true);
++slot;
break;
}
}
}
for (const auto& cal : st.calibration) {
for (std::size_t si = 0; si < signals.size(); ++si) {
if (signals[si].name != cal.name) continue;
model.set_calibration(si, cal.gain, cal.offset);
break;
}
}
if (st.window_s > 0.0) apply_time_window(app, st.window_s);
// set_row_count() rebuilt the figure (its row count is fixed at construction):
// re-point the area, as every other row-count change must, and refresh the
// signal panel's chips so they name the traces the file brought back.
if (app.area != nullptr) app.area->set_figure(&app.rows->figure());
app.relayout_pending = true;
rebuild_signal_panels(app);
}
GtkWidget* labeled_box_fixed(const char* label, GtkWidget* child) {
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget* l = gtk_label_new(label);
gtk_widget_set_halign(l, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(box), l);
gtk_box_append(GTK_BOX(box), child);
return box;
}
GtkWidget* spin(double min, double max, double step, int digits) {
GtkWidget* s = gtk_spin_button_new_with_range(min, max, step);
gtk_spin_button_set_digits(GTK_SPIN_BUTTON(s), digits);
@@ -646,6 +769,7 @@ void on_layout_row_remove(GtkButton* button, gpointer user) {
// The signal list's chips name the trace a signal is drawn as, and a removed row took
// its traces with it.
rebuild_signal_panels(*app);
schedule_layout_save(*app);
}
void on_layout_row_insert(GtkButton* button, gpointer user) {
@@ -659,6 +783,7 @@ void on_layout_row_insert(GtkButton* button, gpointer user) {
if (app->area != nullptr) app->area->set_figure(&app->rows->figure());
rebuild_layout_editor(*app);
rebuild_signal_panels(*app);
schedule_layout_save(*app);
}
/// Restates where the buttons sit, from the rects the last frame was laid out with.
@@ -875,6 +1000,7 @@ void on_calibrate_apply(GtkButton*, gpointer user) {
CalibContext* ctx = static_cast<CalibContext*>(user);
ctx->app->model->set_calibration(ctx->signal, gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->gain)),
gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->offset)));
schedule_layout_save(*ctx->app);
gtk_window_destroy(GTK_WINDOW(ctx->window));
}
@@ -1151,6 +1277,7 @@ void apply_time_window(App& app, double seconds) {
app.rows->set_time_window(clamped);
app.model->set_event_span(clamped);
sync_window_display(app, clamped);
schedule_layout_save(app);
}
/// Enter in the entry. A number that will not parse restores the current window instead of
@@ -1575,6 +1702,7 @@ gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y
const std::vector<SignalInfo>& signals = app->model->signals();
const SignalInfo& sig = signals[signal];
app->rows->assign(row, slot, sig.name, name, signal_color_index(signals, signal));
schedule_layout_save(*app);
app->relayout_pending = true;
rebuild_signal_panels(*app); // the row's colour chip now has a trace to name
cancel_drag_on(*app, row);
@@ -1656,8 +1784,37 @@ gboolean tick(gpointer user) {
app->relayout_pending = false;
relayout_areas(*app);
}
// Header geometry, once a second: the window control must be a fixed, small
// widget beside the title whatever the bar's width is (it used to expand through
// the title's centre box, i.e. window_button_w tracked the window width).
if (app->window_button != nullptr) {
static gint64 next_header_log = 0;
const gint64 now = g_get_monotonic_time();
if (now >= next_header_log) {
next_header_log = now + G_TIME_SPAN_SECOND;
std::fprintf(stderr, "[udpscope] header: window_button_w=%d label_w=%d bar_w=%d\n",
gtk_widget_get_width(app->window_button),
gtk_widget_get_width(app->window_label),
gtk_widget_get_width(app->window));
std::fflush(stderr);
}
}
if (model.generation() != app->signal_generation) rebuild_signal_panels(*app);
// The first CONFIG names the signals, which is what the persisted layout is
// written against: apply it once, here, and never again (a later CONFIG only
// renames or renumbers; the user's layout survives those untouched).
if (!app->layout_loaded && !model.signals().empty()) {
app->layout_loaded = true;
LayoutState st;
if (load_layout(layout_path(app->source_host, app->source_port), st)) {
apply_layout(*app, st);
std::fprintf(stderr, "[udpscope] layout loaded from %s\n",
layout_path(app->source_host, app->source_port).c_str());
std::fflush(stderr);
}
}
// What the display shows, decided once for the whole frame: the producer can open or
// close a capture between any two reads of the model, and the bind, the epoch, the pin
// and the status line all have to describe the same picture.
@@ -2255,7 +2412,7 @@ GtkWidget* build_toolbar(App& app) {
GtkWidget* bar = gtk_header_bar_new();
gtk_header_bar_pack_start(GTK_HEADER_BAR(bar),
labeled_box("window", build_window_control(app)));
labeled_box_fixed("window", build_window_control(app)));
refresh_window_limits(app);
// --- the overflow menu ----------------------------------------------------
@@ -2348,7 +2505,15 @@ void on_activate(GtkApplication* application, gpointer user) {
// A window closed by hand (a WM close button) must stop the frame tick: it holds
// pointers to widgets that are finalized with the window.
g_signal_connect(app->window, "destroy", G_CALLBACK(+[](GtkWidget*, gpointer user) {
static_cast<App*>(user)->window = nullptr;
App* a = static_cast<App*>(user);
// A pending debounced save will never fire now: flush it, so
// quitting right after a change keeps the change.
if (a->layout_save_source != 0) {
g_source_remove(a->layout_save_source);
a->layout_save_source = 0;
save_layout_now(*a);
}
a->window = nullptr;
}),
app);
gtk_window_set_title(GTK_WINDOW(app->window), "udpscope");
@@ -3078,12 +3243,16 @@ gboolean exit_probe_step1(gpointer user) {
} // namespace
int run_ui(int argc, char** argv, ScopeModel& model, Acquisition& acq, RowSet& rows) {
int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisition& acq,
RowSet& rows) {
(void)argc;
App app;
app.model = &model;
app.acq = &acq;
app.rows = &rows;
// Which data source this session talks to: the persisted-layout file's name.
app.source_host = cfg.server;
app.source_port = cfg.port;
GtkApplication* application =
gtk_application_new("org.udpscope.app", G_APPLICATION_DEFAULT_FLAGS);
@@ -3240,6 +3409,21 @@ int run_ui(int argc, char** argv, ScopeModel& model, Acquisition& acq, RowSet& r
tr.series->x_bounds().max);
}
}
std::printf("[udpscope] header: window_button_w=%d window_label_w=%d (bar_w=%d)\n",
app.window_button != nullptr ? gtk_widget_get_width(app.window_button) : -1,
app.window_label != nullptr ? gtk_widget_get_width(app.window_label) : -1,
app.window != nullptr ? gtk_widget_get_width(app.window) : -1);
std::printf("[udpscope] layout: rows=%d window_s=%.6g", rows.row_count(),
rows.time_window());
for (int r = 0; r < rows.row_count() && r < RowSet::kMaxRows; ++r) {
std::printf(" row%d=[", r);
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
if (s != 0) std::printf(",");
std::printf("'%s'", rows.row(r).signal_name[static_cast<std::size_t>(s)].c_str());
}
std::printf("]");
}
std::printf("\n");
for (int r = 0; r < rows.row_count() && r < RowSet::kMaxRows; ++r) {
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
const int ch = rows.channel_of(r, s);