Files
udpscope/src/settings.cpp
T
Martino Ferrari bdf0ff7ddc scope: full-screen figures, second windows, gated measurements, per-configuration events
A batch of view and instrument work, with the probes and docs for each.

- **Full screen per figure row**: a toggle on each row's top-right corner, revealed by
  pointer proximity (`kMaximizeRevealPx` around its placed rect, per row, read from an
  overlay-level motion controller so a revealed button under the pointer cannot flicker);
  the plot menu offers the same as *Full screen this figure* / *Restore the layout*. The
  arrangement is saved and restored through `RowState`, so nothing plotted moves. A layout
  of one figure carries no toggle.
- **Second window**: `open_scope_window()` copies a figure (cells, width shares, signals in
  their colours) or opens an empty one, with its own `RowSet` and `PlotArea`; the main
  window's tick binds, acquires and repaints it, mirroring the time window and pin. Entries:
  the plot menu's *Open this plot in a new window* and the overflow menu's *New scope window*.
- **Layout editor from the plot**: the plot menu gains *Edit the layout...* / *Done editing
  the layout*, flipping the app's one toggle.
- **Measurements gate**: `MeasureOptions::gate` restricts a measurement to the held/pause
  snapshot (live falls back to the live ring), persisted with the session and shown in the
  measure dialog.
- **Crosshair axis badges**: a preference (`crosshair_axis_labels`, on by default) and the
  plotter's badges where the lines meet the axes, in the axes' own formats; the plot menu
  and the probe read them.
- **Event cache per configuration**: `EventStore` is keyed `<source>/<configuration>/`,
  `retarget_event_store()` follows every configuration switch, and a pre-configuration cache
  (loose files) is adopted into the configuration's folder on first start.
- **GTK4 lifecycle**: the main window's handlers move from the non-existent `"destroy"`
  signal to `close-request`; scope windows are closed with it and the application quits
  explicitly (the exit run's hang). The exit probe closes with `gtk_window_close()`.
- Probes: `[maximize-probe]` (single-figure rule and the corner reveal), `[scope-probe]`,
  `[split-probe]` cell crosshair leg, `[prefs-probe]` store keying, `[configs]` store key,
  `[rowmenu]` layout-editor entry; docs, help pages, README and AGENT.md updated.

Also fixes a probe-only use-after-free: `toggle_maximize_figure_row` replaces the figure
object, so the area must be re-pointed (the button's path does; the probe's raw call did not).
2026-11-05 17:58:59 +01:00

106 lines
4.2 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include "settings.hpp"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <glib.h>
#include "layout_store.hpp" // `mkdirs()`, so a first run's config directory is created with parents
namespace udpscope {
namespace {
/// One `key = value` per line, `#` for a comment: the same shape the layout files use, and parsed just
/// as leniently -- an unknown key, an empty line or a file written by an older version is not an error.
bool parse_bool(const std::string& v, bool fallback) {
if (v == "true" || v == "1" || v == "yes") return true;
if (v == "false" || v == "0" || v == "no") return false;
return fallback;
}
std::string unquote(const std::string& v) {
if (v.size() >= 2 && v.front() == '"' && v.back() == '"') return v.substr(1, v.size() - 2);
return v;
}
} // namespace
Settings default_settings() { return Settings{}; }
std::string settings_path() {
const std::string dir = std::string(g_get_user_config_dir()) + "/udpscope";
mkdirs(dir); // every component: `<config>` itself may not exist on a fresh install
return dir + "/settings.toml";
}
Settings load_settings() {
Settings s; // the defaults, so the file only has to mention what a user changed
std::ifstream in(settings_path());
if (!in) return s;
std::string line;
while (std::getline(in, line)) {
const std::size_t hash = line.find('#');
if (hash != std::string::npos) line = line.substr(0, hash);
const std::size_t eq = line.find('=');
if (eq == std::string::npos) continue;
std::string key = line.substr(0, eq);
std::string value = line.substr(eq + 1);
const auto trim = [](std::string& t) {
const std::size_t a = t.find_first_not_of(" \t\r\n");
const std::size_t b = t.find_last_not_of(" \t\r\n");
t = a == std::string::npos ? std::string() : t.substr(a, b - a + 1);
};
trim(key);
trim(value);
value = unquote(value);
if (value.empty()) continue;
if (key == "event_cache_dir") {
s.event_cache_dir = value;
} else if (key == "event_cache_max") {
s.event_cache_max = std::max(0, std::atoi(value.c_str()));
} else if (key == "relative_time") {
s.relative_time = parse_bool(value, s.relative_time);
} else if (key == "memory_limit_mb") {
const long mb = std::strtol(value.c_str(), nullptr, 10);
if (mb >= 16) s.memory_limit_mb = static_cast<int>(std::min<long>(mb, 1000000000L));
} else if (key == "theme") {
if (value == "auto" || value == "light" || value == "dark") s.theme = value;
} else if (key == "crosshair_axis_labels") {
s.crosshair_axis_labels = parse_bool(value, s.crosshair_axis_labels);
} else if (key == "contextual_help") {
s.contextual_help = parse_bool(value, s.contextual_help);
} else if (key == "tour_seen") {
s.tour_seen = parse_bool(value, s.tour_seen);
}
}
return s;
}
bool save_settings(const Settings& settings) {
std::ofstream out(settings_path(), std::ios::trunc);
if (!out) return false;
out << "# udpscope preferences -- written by the preferences dialog, read at start-up.\n";
out << "# A key that is absent means its default, so this file only has to mention what changed.\n";
out << "event_cache_dir = \"" << settings.event_cache_dir << "\"\n";
out << "event_cache_max = " << settings.event_cache_max << "\n";
out << "relative_time = " << (settings.relative_time ? "true" : "false") << "\n";
out << "memory_limit_mb = " << settings.memory_limit_mb << "\n";
out << "theme = \"" << settings.theme << "\"\n";
// The two help keys are the *app's* memory rather than the dialog's: the contextual-help rows --
// the Help menu's and the source chooser's own -- write the first, and the first-run tour writes the
// second. `read_rows()` in the preferences starts from `load_settings()`, so a dialog save carries
// them through untouched.
out << "contextual_help = " << (settings.contextual_help ? "true" : "false") << "\n";
out << "crosshair_axis_labels = " << (settings.crosshair_axis_labels ? "true" : "false") << "\n";
out << "tour_seen = " << (settings.tour_seen ? "true" : "false") << "\n";
out.flush();
return out.good();
}
} // namespace udpscope