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