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

+11
View File
@@ -218,6 +218,17 @@ Two lines of the self-check output are assertions in disguise:
floor are the two refusals, and because the probe emits `::clicked` straight at the
widget it reaches the handler even when the button is insensitive, so `add above 0` there
must leave `rows=` and `traces=` untouched rather than growing a ninth row.
- `header: window_button_w=W label_w=L bar_w=B` (stderr, once a second) — **`W` must be
a small constant and must not track `B`**: the window control is a header-bar child,
and an expanding one is measured into the title's centre box, so it stretched from the
label to the title (516 px at a 1270 px bar before the fix, 121 px after). A `W` that
grows with the window width is that bug back.
- **The session layout round trip** (`~/.config/udpscope/<host>_<port>.toml`) — run once
with a drop and a window change, then again with no interaction: the second run's dump
must read `layout loaded from ...` and `layout: rows=R window_s=W row0=[...]` with the
first run's values, and the signals it names must come back *acquired* (a `row 0 slot 0
-> channel ... samples=N` line). Delete the file before a run whose layout you did not
mean to inherit (`127.0.0.1_9540.toml` and friends accumulate otherwise).
- `badge frames: lit=L dim=D idle=I` — the activity badge's phase per frame. Over a run
that acquired anything **both `lit` and `dim` must be non-zero**: that is what "it
blinks" means, and a badge stuck lit is indistinguishable from a working one in a
+1
View File
@@ -40,6 +40,7 @@ add_executable(udpscope
src/acquisition.cpp
src/plot_host.cpp
src/export.cpp
src/layout_store.cpp
src/ui.cpp
)
+37
View File
@@ -46,6 +46,8 @@ src/acquisition.* the UDPStreamer client, the producer thread, frame decoding
src/trigger.* the trigger configuration and the producer-side state machine
src/plot_host.* the one plot::Figure, its rows' traces and cursors, the x link group
src/export.* the visible range as a table, and the .npy / .csv writers
src/layout_store.* the per-source session layout file (TOML), written and read back
src/event_series.hpp an immutable captured event as a plot::data::Series
src/ui.* the GTK4 window: toolbar, trigger panel, signal panel, plot box
```
@@ -56,6 +58,41 @@ frame and the GUI thread takes once per frame around binding the traces.
Samples cross the boundary through `plot::data::RingBuffer` (single producer,
single consumer), never through the lock.
## The session layout file
What you arranged is remembered per data source, in
`~/.config/udpscope/<host>_<port>.toml` (`$XDG_CONFIG_HOME` is honoured; the
host's unsafe characters are replaced with `_`). The file holds the display
window, the row layout and which signal each of a row's slots plots, and the
per-signal gain and offset:
```toml
window_s = 2
plots = 1
[[plot]]
signals = ["Ch1", "", "", ""]
[[calibration]]
name = "Ch1"
gain = 2
offset = 0.5
```
It is written (400 ms after the last change, and flushed on close) whenever the
window, the layout, a drop or a calibration changes, and applied once, when the
first CONFIG names the signals -- signal *names* are the identity, because a
CONFIG renumbers and an enable/disable rebuilds the channel list. Signals the
file plots are enabled on load, since a restored plot nobody acquires would come
back blank. It is plain TOML and safe to edit; a malformed file loads whatever
parsed rather than refusing to start. Writes go to a sibling temp file and are
renamed over the target, so a crash mid-write leaves the old layout.
The same reason the window control is a fixed-width label in the header bar
(see the DPI/relayout notes in `ui.cpp`) is why the bar's `window` control does
not expand: a header-bar child that expands is measured into the title's centre
box, so the control stretched to the title and every window resize moved it.
## How the spec maps onto the code
**One buffer of `N+1` slices, plus frozen event snapshots.** `ScopeModel`
+248
View File
@@ -0,0 +1,248 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include "layout_store.hpp"
#include <cstdint>
#include <cstdio>
#include <fstream>
#include <sstream>
#include <sys/stat.h>
#include <sys/types.h>
namespace udpscope {
namespace {
/// Anything outside a safe filename set becomes '_': the host is a dotted quad
/// today, but the CLI takes any string and a '/' or ':' in it must not escape
/// the directory.
std::string sanitize(const std::string& host) {
std::string out;
out.reserve(host.size());
for (char c : host) {
const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-';
out.push_back(ok ? c : '_');
}
return out.empty() ? "_" : out;
}
std::string config_dir() {
const char* xdg = std::getenv("XDG_CONFIG_HOME");
if (xdg != nullptr && xdg[0] != '\0') return std::string(xdg) + "/udpscope";
const char* home = std::getenv("HOME");
if (home == nullptr || home[0] == '\0') return "/tmp/udpscope-config";
return std::string(home) + "/.config/udpscope";
}
void mkdirs(const std::string& path) {
std::string cur;
for (const char c : path) {
cur.push_back(c);
if (c == '/' || cur.size() == path.size()) {
if (cur == "/" || cur.back() != '/') continue;
::mkdir(cur.c_str(), 0755);
}
}
}
std::string toml_escape(const std::string& s) {
std::string out = "\"";
for (char c : s) {
if (c == '"' || c == '\\') out.push_back('\\');
out.push_back(c);
}
return out + "\"";
}
bool unescape(std::string& s) {
std::string out;
for (std::size_t i = 0; i < s.size(); ++i) {
if (s[i] != '\\') {
out.push_back(s[i]);
continue;
}
if (++i >= s.size()) return false;
if (s[i] == '"' || s[i] == '\\') {
out.push_back(s[i]);
} else {
return false;
}
}
s = out;
return true;
}
std::string trim(const std::string& s) {
const auto a = s.find_first_not_of(" \t\r");
if (a == std::string::npos) return {};
const auto b = s.find_last_not_of(" \t\r");
return s.substr(a, b - a + 1);
}
/// `"a", "b"` (with the brackets already stripped) -> ["a", "b"].
bool parse_string_array(const std::string& in, std::vector<std::string>& out) {
out.clear();
std::size_t i = 0;
while (i < in.size()) {
while (i < in.size() && (in[i] == ' ' || in[i] == ',' || in[i] == '\t')) ++i;
if (i >= in.size()) break;
if (in[i] != '"') return false;
++i;
std::string tok;
while (i < in.size() && in[i] != '"') {
if (in[i] == '\\' && i + 1 < in.size()) {
tok.push_back(in[i]);
++i;
}
tok.push_back(in[i]);
++i;
}
if (i >= in.size()) return false;
++i; // closing quote
if (!unescape(tok)) return false;
out.push_back(tok);
}
return true;
}
} // namespace
std::string layout_path(const std::string& host, std::uint16_t port) {
const std::string dir = config_dir();
mkdirs(dir);
return dir + "/" + sanitize(host) + "_" + std::to_string(port) + ".toml";
}
bool load_layout(const std::string& path, LayoutState& out) {
std::ifstream in(path);
if (!in) return false;
LayoutState st;
st.window_s = 0.0;
st.rows.clear();
st.calibration.clear();
std::string line;
// What the last `[[...]]` header opened, so keys land in the right record.
enum class Section { Top, Plot, Calibration } section = Section::Top;
int plot_count = 0;
bool plot_section_seen = false;
while (std::getline(in, line)) {
const std::string t = trim(line);
if (t.empty() || t[0] == '#') continue;
if (t[0] == '[') {
if (t == "[[plot]]") {
section = Section::Plot;
plot_section_seen = true;
st.rows.emplace_back();
} else if (t == "[[calibration]]") {
section = Section::Calibration;
st.calibration.emplace_back();
} else {
section = Section::Top; // an unknown table: keep its keys out of ours
}
continue;
}
const auto eq = t.find('=');
if (eq == std::string::npos) continue;
const std::string key = trim(t.substr(0, eq));
const std::string value = trim(t.substr(eq + 1));
auto strip_quote = [](const std::string& v) {
if (v.size() >= 2 && v.front() == '"' && v.back() == '"') {
std::string s = v.substr(1, v.size() - 2);
std::string out;
for (std::size_t i = 0; i < s.size(); ++i) {
if (s[i] == '\\' && i + 1 < s.size()) ++i;
out.push_back(s[i]);
}
return out;
}
return v;
};
if (section == Section::Top) {
if (key == "window_s") {
try {
st.window_s = std::stod(value);
} catch (...) {
}
} else if (key == "plots") {
// Only a fallback for a file with no [[plot]] sections: the writer emits
// both, and honouring both appended the sections onto the pre-created
// rows (a 2-plot file loaded as 4).
try {
plot_count = std::max(0, std::stoi(value));
} catch (...) {
}
}
} else if (section == Section::Plot && !st.rows.empty()) {
if (key == "signals") {
std::vector<std::string> names;
if (value.size() >= 2 && value.front() == '[' && value.back() == ']') {
parse_string_array(value.substr(1, value.size() - 2), names);
}
st.rows.back().signals = names;
}
} else if (section == Section::Calibration && !st.calibration.empty()) {
if (key == "name") {
st.calibration.back().name = strip_quote(value);
} else if (key == "gain") {
try {
st.calibration.back().gain = std::stod(value);
} catch (...) {
}
} else if (key == "offset") {
try {
st.calibration.back().offset = std::stod(value);
} catch (...) {
}
}
}
}
if (!plot_section_seen && plot_count > 0) {
st.rows.assign(static_cast<std::size_t>(plot_count), {});
}
out = std::move(st);
return true;
}
bool save_layout(const std::string& path, const LayoutState& state) {
std::ostringstream o;
o << "# udpscope session layout (written by the app; safe to edit)\n";
if (state.window_s > 0.0) o << "window_s = " << state.window_s << "\n";
o << "plots = " << state.rows.size() << "\n";
for (const auto& row : state.rows) {
o << "\n[[plot]]\nsignals = [";
for (std::size_t i = 0; i < row.signals.size(); ++i) {
if (i != 0) o << ", ";
o << toml_escape(row.signals[i]);
}
o << "]\n";
}
for (const auto& cal : state.calibration) {
o << "\n[[calibration]]\nname = " << toml_escape(cal.name) << "\ngain = " << cal.gain
<< "\noffset = " << cal.offset << "\n";
}
// A sibling temp file + rename: a crash mid-write leaves the old layout, not a
// truncated one that would fail to parse next start.
const std::string tmp = path + ".tmp";
{
std::ofstream f(tmp, std::ios::trunc);
if (!f) return false;
f << o.str();
if (!f.good()) {
::remove(tmp.c_str());
return false;
}
}
if (std::rename(tmp.c_str(), path.c_str()) != 0) {
::remove(tmp.c_str());
return false;
}
return true;
}
} // namespace udpscope
+46
View File
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace udpscope {
/// The user-arranged part of a session, persisted per data source in
/// `~/.config/udpscope/<host>_<port>.toml`: the display window, the plot layout
/// (rows and which signal each of a row's slots holds) and the per-signal
/// calibration. Signal names are the stable identity (channel indexes are
/// renumbered by every enable/disable, see the model's own invariant).
struct LayoutState {
struct Row {
/// One entry per slot; an empty string is an empty slot.
std::vector<std::string> signals;
};
double window_s = 0.0; ///< 0 keeps the default window
std::vector<Row> rows;
struct Calibration {
std::string name;
double gain = 1.0;
double offset = 0.0;
};
std::vector<Calibration> calibration;
};
/// `~/.config/udpscope/<host>_<port>.toml`, with the directory created.
/// Characters unsafe in a filename are replaced in the host part.
std::string layout_path(const std::string& host, std::uint16_t port);
/// Parses the file's TOML subset. Returns false when the file is missing or
/// malformed; `out` is left usable either way (a malformed file loads as much as
/// parsed, which beats refusing a hand-edited file over one stray character).
bool load_layout(const std::string& path, LayoutState& out);
/// Writes `state` as TOML. Atomic in effect: writes a sibling temp file and
/// renames it over the target.
bool save_layout(const std::string& path, const LayoutState& state);
} // namespace udpscope
+1 -1
View File
@@ -30,7 +30,7 @@ int main(int argc, char** argv) {
return 1;
}
const int result = udpscope::run_ui(argc, argv, model, acquisition, rows);
const int result = udpscope::run_ui(argc, argv, cfg, model, acquisition, rows);
acquisition.stop();
return result;
}
+7
View File
@@ -184,6 +184,12 @@ void ScopeModel::rebuild_channels() {
ch.unit = sig.unit;
ch.elements = sig.elements();
ch.dt = sig.sampling_rate > 0.0 ? 1.0 / sig.sampling_rate : 0.0;
// A calibration the user already set for this name (before it was enabled, or
// before a CONFIG renumbered the channels) still applies.
if (auto it = calibration_.find(sig.name); it != calibration_.end()) {
ch.gain = it->second.first;
ch.offset = it->second.second;
}
channels_.push_back(std::move(ch));
}
}
@@ -277,6 +283,7 @@ void ScopeModel::set_signal_enabled(std::size_t signal, bool on) {
void ScopeModel::set_calibration(std::size_t signal, double gain, double offset) {
std::lock_guard<std::mutex> lock(mutex_);
if (signal < signals_.size()) calibration_[signals_[signal].name] = {gain, offset};
bool any = false;
for (ChannelInfo& c : channels_) {
if (c.signal != signal) continue;
+8
View File
@@ -11,6 +11,7 @@
#include <plot/data/ring_buffer.hpp>
#include <atomic>
#include <unordered_map>
#include <cstdint>
#include <memory>
#include <mutex>
@@ -183,6 +184,11 @@ class ScopeModel {
static bool is_time_signal(const std::vector<SignalInfo>& signals,
std::size_t signal);
void set_signal_enabled(std::size_t signal, bool on);
/// Stores the calibration for a signal's *name* and applies it to that signal's
/// live channels. Keyed by name, not index: a CONFIG renumbers and an
/// enable/disable rebuilds the channel list, and `rebuild_channels()` re-applies
/// from here -- so a calibration set before a signal was enabled, or surviving a
/// CONFIG, is not silently dropped.
void set_calibration(std::size_t signal, double gain, double offset);
std::size_t channel_count() const noexcept { return channels_.size(); }
@@ -352,6 +358,8 @@ class ScopeModel {
std::atomic<bool> trigger_pending_{false};
std::atomic<double> capture_edge_{0.0};
std::atomic<std::shared_ptr<const EventSnapshot>> event_; ///< the event on screen
/// Per-signal-name calibration (see `set_calibration`).
std::unordered_map<std::string, std::pair<double, double>> calibration_;
std::atomic<std::uint64_t> event_seq_{0};
std::atomic<double> display_epoch_{0.0};
std::atomic<bool> trigger_armed_{false};
+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);
+2 -1
View File
@@ -12,6 +12,7 @@ class RowSet;
/// Runs the GTK application. Never returns while the window is open; returns the
/// `g_application_run()` result once it closes.
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);
} // namespace udpscope