Updated local storage
This commit is contained in:
1 parent
30db44eaa0
commit
2218aed839
10 files changed
+548
-5
No files matched your search
+187
-3
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user