ui: a signal's details sit directly under its name; its unit is user-changeable and saved with the session
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
bbef7faf3d
commit
87f61ea364
14 files changed
+360
-135
No files matched your search
@@ -132,7 +132,7 @@ divider's `+` for a new math channel; when every signal is acquired a dim "every
|
||||
stands in for the list. While the bar has overflowed into the "N signals" badge the page lists the acquired
|
||||
signals too, since their rows are then the only way to each signal's page. There is no signal lock: a
|
||||
signal's page and the list always allow acquire, colour, calibration, the math editor and delete.
|
||||
A signal badge opens that signal's own page in the slide-over -- colour, the gain and offset (two aligned numbers, a pencil beside each editing just that one) and the *Plot on N*
|
||||
A signal badge opens that signal's own page in the slide-over -- the unit, gain and offset and the CONFIG's details directly under the name (aligned rows; a pencil beside the unit, gain and offset edits just that one -- a unit rename is stored per signal name, saved as `[[unit]]` and re-applied to every CONFIG), the colour and the *Plot on N*
|
||||
buttons -- and is also the drag handle: drag it onto a plot to draw the signal there, or onto another
|
||||
badge to reorder them. Past **six** signal badges the bar shows a single **"N signals"** badge in their
|
||||
place (it opens the list, and `+` is hidden): the E-1608's 8 analog inputs and counter would not fit
|
||||
|
||||
+5
-3
@@ -65,9 +65,11 @@ the badge bar. Drag the badge onto a plot row to add a trace; drag it again to a
|
||||
the same signal twice, or onto another badge to reorder the badges. The colour is per *signal*, so
|
||||
the same signal is the same colour wherever it appears.
|
||||
|
||||
Clicking a signal's badge opens its own page: the colour, **Plot on** buttons for the rows, and
|
||||
**Gain** and **Offset**, shown as two aligned numbers with a pencil button each that edits that one value.
|
||||
They are applied at ingest, so the measurements and the export carry the same numbers the plot shows.
|
||||
Clicking a signal's badge opens its own page. Directly under the name are its **Unit**, **Gain** and
|
||||
**Offset** — each with a pencil button that edits that one value — and then what the stream declared about
|
||||
it (type, size, range, sample rate, timing). Below come the colour and the **Plot on** buttons for the
|
||||
rows. Gain and offset are applied at ingest, so the measurements and the export carry the same numbers the
|
||||
plot shows; a renamed unit is used there too and is saved with the session.
|
||||
|
||||
## 4. The window and navigation
|
||||
|
||||
|
||||
@@ -23,7 +23,10 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
not follow the lock). `[sigbadges] drag_sources` is the reading, and `drag_sources` on the `signals panel:`
|
||||
line must be 0.
|
||||
- **The session is the layout.** `LayoutState` persists what makes a reopened scope look like the
|
||||
one that was closed: the window, the rows and their slot signals, the calibration, **the enabled
|
||||
one that was closed: the window, the rows and their slot signals, the calibration, **the unit renames**
|
||||
(`[[unit]]`, `name` + `unit`, only the overrides; `ScopeModel::set_signal_unit` re-applies them to every CONFIG,
|
||||
and `set_signals()` takes them off the incoming units before the "same stream" comparison so a reconnect still
|
||||
keeps its history), **the enabled
|
||||
signals**, **the chosen trace colours** (`[[color]]`, `name` + `rgb = "#RRGGBB"` — only the
|
||||
*overrides*, so a stream's new signals still take the positional palette and a saved session does
|
||||
not freeze a colour that is meant to follow the signal list), **the math channels with their
|
||||
|
||||
@@ -2761,11 +2761,11 @@ the slide-over closed and the page was dropped by `signal_pages_sync()`. `pruned
|
||||
channel removed) and the slide-over was still on `trigger`, both by `slideover_page()` and by the stack's visible child; `0` is `signal_pages_sync()` parking the stack on the
|
||||
signals list while the title and the badge marker still name `trigger`; `-1` when there is no `trigger` page or no math channel could be added.
|
||||
|
||||
A second line, `[signalpage] calibration identity=1/0 set=2.5/-1.25 pencils=2 pencil_edits_one=1`, reads the page's gain and offset rows
|
||||
A second line, `[signalpage] calibration identity=1/0 set=2.5/-1.25 pencils=3 pencil_edits_one=1 unit_edit=1 unit='V'`, reads the page's details grid (unit, gain, offset, then the CONFIG's read-only details) rows
|
||||
(`udpscope-gain` / `udpscope-offset` labels, one string rule: `format_calibration()`, up to six decimals with trailing zeros dropped): `identity` is what an
|
||||
uncalibrated signal shows, `set` what the page shows after `set_calibration(2.5, -1.25)` and `signal_pages_sync()` (the Apply path), `pencils=2` the two edit
|
||||
uncalibrated signal shows, `set` what the page shows after `set_calibration(2.5, -1.25)` and `signal_pages_sync()` (the Apply path), `pencils=3` the unit, gain and offset edit
|
||||
buttons, and `pencil_edits_one=1` that the gain pencil opens a dialog titled "Gain" with exactly one spin button and that Apply changed the gain only
|
||||
(offset stayed -1.25) and refilled the label. `-1` is no pencil / no dialog; `0` is a dialog that edits both or a label that did not follow the model.
|
||||
(offset stayed -1.25) and refilled the label. `unit_edit=1`: the unit pencil opens a dialog titled "Unit" with one entry, "kV" reached the model's signal and the page's `udpscope-unit` label, and an empty entry restored the declared unit (`unit=` prints it; it must be the mock's own). A rename bumps `generation()`, so the panels re-read the list and the measurement statistics restart. `-1` is no pencil / no dialog; `0` is a dialog that edits both or a label that did not follow the model.
|
||||
|
||||
## `[sigbadges]`
|
||||
|
||||
|
||||
@@ -30,9 +30,10 @@
|
||||
</item>
|
||||
<item>
|
||||
<p>Once a signal is acquired its badge appears. Click it to open that signal's page: its
|
||||
colour, <gui>Plot on</gui> buttons for the rows, <gui>Information</gui> for the signal's
|
||||
details, and its <gui>Gain</gui> and <gui>Offset</gui>, each with a pencil button that
|
||||
edits that one number.</p>
|
||||
details directly under its name — <gui>Unit</gui>, <gui>Gain</gui> and <gui>Offset</gui>,
|
||||
each with a pencil button that edits that one value, then the type, size, range, sample
|
||||
rate and timing the stream declared — its colour and <gui>Plot on</gui> buttons for the
|
||||
rows.</p>
|
||||
</item>
|
||||
</steps>
|
||||
</section>
|
||||
@@ -47,7 +48,9 @@
|
||||
through, so a trace already scaled by a gain of 2 reads 2. The pencil beside a number opens
|
||||
an editor for that number only, at its current value —
|
||||
pressing <gui>Apply</gui> confirms it rather than resetting it. A signal nobody
|
||||
calibrated shows the identity, 1 and 0.</p></item>
|
||||
calibrated shows the identity, 1 and 0. The unit can be renamed the same way (an empty
|
||||
entry restores the one the stream declares); the new name is used in the readouts, the
|
||||
measurements and the export, and is remembered with the session.</p></item>
|
||||
<item><p>Calibration is applied <em>at ingest</em>, so changing it re-plans the buffer
|
||||
and drops the history. So does enabling or disabling a signal, and so does a new
|
||||
CONFIG. An empty plot for a second after one of those is the ring refilling, not a
|
||||
|
||||
+14
-1
@@ -439,6 +439,7 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
||||
st.window_s = 0.0;
|
||||
st.rows.clear();
|
||||
st.calibration.clear();
|
||||
st.units.clear();
|
||||
st.colors.clear();
|
||||
st.enabled.clear();
|
||||
st.math.clear();
|
||||
@@ -446,7 +447,7 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
||||
|
||||
std::string line;
|
||||
// What the last `[[...]]` header opened, so keys land in the right record.
|
||||
enum class Section { Top, Plot, Calibration, Color, Math, Measure } section = Section::Top;
|
||||
enum class Section { Top, Plot, Calibration, Unit, Color, Math, Measure } section = Section::Top;
|
||||
int plot_count = 0;
|
||||
bool plot_section_seen = false;
|
||||
while (std::getline(in, line)) {
|
||||
@@ -460,6 +461,9 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
||||
} else if (t == "[[calibration]]") {
|
||||
section = Section::Calibration;
|
||||
st.calibration.emplace_back();
|
||||
} else if (t == "[[unit]]") {
|
||||
section = Section::Unit;
|
||||
st.units.emplace_back();
|
||||
} else if (t == "[[color]]") {
|
||||
section = Section::Color;
|
||||
st.colors.emplace_back();
|
||||
@@ -686,6 +690,12 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
} else if (section == Section::Unit && !st.units.empty()) {
|
||||
if (key == "name") {
|
||||
st.units.back().name = strip_quote(value);
|
||||
} else if (key == "unit") {
|
||||
st.units.back().unit = strip_quote(value);
|
||||
}
|
||||
} else if (section == Section::Color && !st.colors.empty()) {
|
||||
if (key == "name") {
|
||||
st.colors.back().name = strip_quote(value);
|
||||
@@ -785,6 +795,9 @@ bool save_layout(const std::string& path, const LayoutState& state) {
|
||||
o << "\n[[calibration]]\nname = " << toml_escape(cal.name) << "\ngain = " << cal.gain
|
||||
<< "\noffset = " << cal.offset << "\n";
|
||||
}
|
||||
for (const auto& u : state.units) {
|
||||
o << "\n[[unit]]\nname = " << toml_escape(u.name) << "\nunit = " << toml_escape(u.unit) << "\n";
|
||||
}
|
||||
for (const auto& color : state.colors) {
|
||||
char rgb[16];
|
||||
std::snprintf(rgb, sizeof(rgb), "\"#%06X\"", color.rgb & 0x00FFFFFFu);
|
||||
|
||||
@@ -36,6 +36,13 @@ struct LayoutState {
|
||||
};
|
||||
std::vector<Calibration> calibration;
|
||||
|
||||
/// The unit names the user gave streamed signals, by signal name. Only the overrides.
|
||||
struct Unit {
|
||||
std::string name;
|
||||
std::string unit;
|
||||
};
|
||||
std::vector<Unit> units;
|
||||
|
||||
/// The trace colours the user chose, `0xRRGGBB`. Only the *overrides*: a signal with no entry
|
||||
/// takes the positional palette entry, which is what keeps a stream's new signals coloured
|
||||
/// without a session having to name them. Saved by name like every identity here.
|
||||
|
||||
@@ -232,6 +232,13 @@ void ScopeModel::set_signals(std::vector<SignalInfo> signals) {
|
||||
signals.erase(std::remove_if(signals.begin(), signals.end(),
|
||||
[](const SignalInfo& s) { return s.math; }),
|
||||
signals.end());
|
||||
// The renames come off the stream's own units first, so that an identical CONFIG still compares
|
||||
// equal against `signals_` (which holds the renamed ones) and a reconnect keeps its history.
|
||||
declared_unit_.clear();
|
||||
for (SignalInfo& s : signals) {
|
||||
declared_unit_[s.name] = s.unit;
|
||||
if (const auto it = unit_override_.find(s.name); it != unit_override_.end()) s.unit = it->second;
|
||||
}
|
||||
if (!signals_.empty() && same_stream_signals(signals, signals_)) {
|
||||
// A reconnect's identical CONFIG: the honest recovery is *nothing* -- the ring keeps
|
||||
// its history, the trigger keeps its state and its held event, and the outage shows
|
||||
@@ -770,6 +777,38 @@ void ScopeModel::set_calibration(std::size_t signal, double gain, double offset)
|
||||
}
|
||||
}
|
||||
|
||||
void ScopeModel::set_signal_unit(std::size_t signal, const std::string& unit) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (signal >= signals_.size() || signals_[signal].math) return;
|
||||
const std::string name = signals_[signal].name;
|
||||
std::string shown = unit;
|
||||
if (unit.empty()) {
|
||||
unit_override_.erase(name);
|
||||
const auto it = declared_unit_.find(name);
|
||||
shown = it != declared_unit_.end() ? it->second : std::string();
|
||||
} else {
|
||||
unit_override_[name] = unit;
|
||||
}
|
||||
if (signals_[signal].unit == shown) return;
|
||||
signals_[signal].unit = shown;
|
||||
for (ChannelInfo& c : channels_) {
|
||||
if (c.signal == signal) c.unit = shown;
|
||||
}
|
||||
generation_.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
std::string ScopeModel::declared_unit(std::size_t signal) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (signal >= signals_.size()) return std::string();
|
||||
const auto it = declared_unit_.find(signals_[signal].name);
|
||||
return it != declared_unit_.end() ? it->second : signals_[signal].unit;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> ScopeModel::unit_overrides() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return {unit_override_.begin(), unit_override_.end()};
|
||||
}
|
||||
|
||||
double ScopeModel::channel_dt(std::size_t channel) const {
|
||||
return channel < channels_.size() ? channels_[channel].dt : 0.0;
|
||||
}
|
||||
|
||||
@@ -465,6 +465,17 @@ class ScopeModel {
|
||||
/// no channel to read it from, and the record is what a dialog must show.
|
||||
std::pair<double, double> calibration(std::size_t signal);
|
||||
|
||||
/// Renames the unit a streamed signal is shown, read and exported in. Stored by the signal's *name*
|
||||
/// like the calibration, and re-applied to every later CONFIG, so a rename survives a reconnect.
|
||||
/// An empty `unit` removes the override and restores the unit the stream declared. A math channel's
|
||||
/// unit belongs to its graph and is ignored here. Bumps `generation()` when it changed something,
|
||||
/// which is what makes the panels re-read the signal list.
|
||||
void set_signal_unit(std::size_t signal, const std::string& unit);
|
||||
/// The unit the stream declared for that signal, whatever the user renamed it to.
|
||||
std::string declared_unit(std::size_t signal);
|
||||
/// The renames in force, `{name, unit}`, for the session file.
|
||||
std::vector<std::pair<std::string, std::string>> unit_overrides();
|
||||
|
||||
std::size_t channel_count() const noexcept { return channels_.size(); }
|
||||
const Partition& partition() const noexcept { return part_; }
|
||||
|
||||
@@ -868,6 +879,9 @@ class ScopeModel {
|
||||
std::atomic<bool> paused_{false};
|
||||
/// Per-signal-name calibration (see `set_calibration`).
|
||||
std::unordered_map<std::string, std::pair<double, double>> calibration_;
|
||||
/// Per-signal-name unit renames (see `set_signal_unit`) and the units the stream declared.
|
||||
std::unordered_map<std::string, std::string> unit_override_;
|
||||
std::unordered_map<std::string, std::string> declared_unit_;
|
||||
std::atomic<std::uint64_t> event_seq_{0};
|
||||
std::atomic<double> display_epoch_{0.0};
|
||||
std::atomic<bool> trigger_armed_{false};
|
||||
|
||||
+71
@@ -918,6 +918,77 @@ void open_calibration(App& app, GtkWindow* parent, std::size_t signal, CalibFiel
|
||||
}
|
||||
|
||||
|
||||
/// The unit editor: one entry, seeded with the unit now shown, the stream's own unit as its placeholder.
|
||||
/// An empty entry restores the declared unit (see `ScopeModel::set_signal_unit`).
|
||||
void open_unit_editor(App& app, GtkWindow* parent, std::size_t signal) {
|
||||
std::string name;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
if (signal >= app.model->signals().size()) return;
|
||||
name = app.model->signals()[signal].name;
|
||||
}
|
||||
const std::string declared = app.model->declared_unit(signal);
|
||||
GtkWidget* window = dialog_window(GTK_WIDGET(parent), "Unit");
|
||||
GtkWidget* box = gtk_window_get_child(GTK_WINDOW(window));
|
||||
GtkWidget* title = gtk_label_new(name.c_str());
|
||||
gtk_widget_set_halign(title, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(box), title);
|
||||
GtkWidget* entry = gtk_entry_new();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
if (signal < app.model->signals().size()) {
|
||||
gtk_editable_set_text(GTK_EDITABLE(entry), app.model->signals()[signal].unit.c_str());
|
||||
}
|
||||
}
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(entry), declared.empty() ? "(none declared)" : declared.c_str());
|
||||
gtk_entry_set_activates_default(GTK_ENTRY(entry), true);
|
||||
gtk_box_append(GTK_BOX(box), labeled_box("unit", entry));
|
||||
GtkWidget* hint = gtk_label_new("Empty restores the unit the stream declares.");
|
||||
gtk_widget_add_css_class(hint, "dim-label");
|
||||
gtk_widget_set_halign(hint, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(box), hint);
|
||||
|
||||
struct UnitContext {
|
||||
App* app;
|
||||
std::string name;
|
||||
GtkWidget* entry;
|
||||
GtkWidget* window;
|
||||
};
|
||||
auto* ctx = new UnitContext{&app, name, entry, window};
|
||||
GtkWidget* cancel = gtk_button_new_with_label("Cancel");
|
||||
g_signal_connect_swapped(cancel, "clicked", G_CALLBACK(gtk_window_close), window);
|
||||
GtkWidget* apply = gtk_button_new_with_label("Apply");
|
||||
g_object_set_data_full(G_OBJECT(apply), "udpscope-ctx", ctx,
|
||||
[](gpointer p) { delete static_cast<UnitContext*>(p); });
|
||||
g_signal_connect(apply, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
UnitContext* c = static_cast<UnitContext*>(user);
|
||||
// Resolved by name at Apply: a CONFIG may have renumbered the signals while the dialog was open.
|
||||
std::size_t index = std::string::npos;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(c->app->model->mutex());
|
||||
const std::vector<SignalInfo>& s = c->app->model->signals();
|
||||
for (std::size_t i = 0; i < s.size(); ++i) {
|
||||
if (s[i].name == c->name) index = i;
|
||||
}
|
||||
}
|
||||
if (index != std::string::npos) {
|
||||
std::string text = gtk_editable_get_text(GTK_EDITABLE(c->entry));
|
||||
const auto b = text.find_first_not_of(" \t");
|
||||
const auto e = text.find_last_not_of(" \t");
|
||||
text = b == std::string::npos ? std::string() : text.substr(b, e - b + 1);
|
||||
c->app->model->set_signal_unit(index, text);
|
||||
schedule_layout_save(*c->app);
|
||||
signal_pages_sync(*c->app);
|
||||
}
|
||||
gtk_window_destroy(GTK_WINDOW(c->window));
|
||||
}),
|
||||
ctx);
|
||||
dialog_add_button(window, cancel);
|
||||
dialog_add_button(window, apply);
|
||||
gtk_window_present(GTK_WINDOW(window));
|
||||
}
|
||||
|
||||
|
||||
GdkContentProvider* on_drag_prepare(GtkDragSource*, double, double, gpointer user) {
|
||||
DragContext* ctx = static_cast<DragContext*>(user);
|
||||
// The payload is the signal index; the channel is resolved on drop.
|
||||
|
||||
@@ -1714,6 +1714,8 @@ void on_calibrate_apply(GtkButton*, gpointer user);
|
||||
/// Which of the calibration's two numbers a dialog edits; `Both` is the full dialog.
|
||||
enum class CalibField { Both, Gain, Offset };
|
||||
void open_calibration(App& app, GtkWindow* parent, std::size_t signal, CalibField field = CalibField::Both);
|
||||
/// The one-entry editor for a streamed signal's unit (an empty entry restores the declared one).
|
||||
void open_unit_editor(App& app, GtkWindow* parent, std::size_t signal);
|
||||
GdkContentProvider* on_drag_prepare(GtkDragSource*, double, double, gpointer user);
|
||||
int signal_color_index(const std::vector<SignalInfo>& signals, std::size_t signal);
|
||||
std::string color_hex(plot::Color c);
|
||||
|
||||
@@ -123,8 +123,8 @@ constexpr HelpHint kHelpHints[] = {
|
||||
"every row.",
|
||||
"", &App::trigger_search},
|
||||
{"signals_config", "Configuring signals",
|
||||
"A signal's own page, opened by its badge or by its row here, shows the gain and offset that map "
|
||||
"raw counts to a unit (a pencil edits each), the colour a trace is drawn in, and which plot a signal is drawn on. A "
|
||||
"A signal's own page, opened by its badge or by its row here, shows its unit, gain and offset (a pencil edits each) "
|
||||
"above what the stream declared about it, the colour a trace is drawn in, and which plot a signal is drawn on. A "
|
||||
"signal nothing measures can be switched off there to stop acquiring it.",
|
||||
"", &App::signals_note},
|
||||
{"window", "The time window",
|
||||
|
||||
@@ -42,6 +42,7 @@ LayoutState collect_layout(App& app) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const auto& [name, unit] : model.unit_overrides()) st.units.push_back({name, unit});
|
||||
// The *enable* state, not just what is plotted: a signal can be enabled only as a math channel's
|
||||
// source, and a session that came back with it off would come back with a NaN trace. "Enabled" is
|
||||
// read off the channels the model built -- it has no per-signal accessor and does not need one,
|
||||
@@ -492,6 +493,14 @@ void apply_layout(App& app, const LayoutState& st) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& u : st.units) {
|
||||
for (std::size_t si = 0; si < signals.size(); ++si) {
|
||||
if (signals[si].name != u.name) continue;
|
||||
model.set_signal_unit(si, u.unit);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Skipped for a name the stream no longer carries, like the calibration above: an override the
|
||||
// signal list cannot show is one `collect_layout()` would write back every session for ever.
|
||||
for (const auto& color : st.colors) {
|
||||
|
||||
+182
-120
@@ -2,7 +2,7 @@
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
//
|
||||
// The per-signal slide-over pages (`signal:<name>`): one signal's acquire switch, colour, the plot it is on,
|
||||
// its information, gain and offset (a math channel's editor instead). Opened from a row of the signal list;
|
||||
// its unit, gain, offset and details under its name (a math channel's editor instead). Opened from a row of the signal list;
|
||||
// kept in step with the model by `signal_pages_sync()`, which `rebuild_signal_panels()` ends with.
|
||||
|
||||
#include "ui_app.hpp"
|
||||
@@ -66,6 +66,98 @@ std::size_t tagged(GtkButton* b) {
|
||||
return static_cast<std::size_t>(GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(b), "udpscope-signal")));
|
||||
}
|
||||
|
||||
/// One row of the details grid: the name left, the value right-aligned in tabular figures, the value's
|
||||
/// unit in a column of its own (so the digits line up whatever the unit is), and, for a value the user can
|
||||
/// change, a pencil that edits just that one.
|
||||
struct DetailRow {
|
||||
const char* label;
|
||||
std::string value;
|
||||
std::string unit;
|
||||
const char* id; // widget name of the value label, which the probe reads; null for none
|
||||
const char* tip;
|
||||
int edit; // which editor the pencil opens: 0 unit, 1 gain, 2 offset; -1 for none
|
||||
};
|
||||
|
||||
/// Everything the page knows about a streamed signal, directly under its name: the unit, gain and offset
|
||||
/// (each with a pencil), then what the CONFIG declared about it.
|
||||
GtkWidget* build_info_grid(App& app, std::size_t index, const SignalInfo& sig,
|
||||
const std::vector<SignalInfo>& all) {
|
||||
const std::pair<double, double> cal = app.model->calibration(index);
|
||||
const char* time_modes[] = {"per packet", "full array", "first sample", "last sample"};
|
||||
const std::string time_mode =
|
||||
sig.time_mode < 4 ? time_modes[sig.time_mode] : std::to_string(int(sig.time_mode));
|
||||
const std::string time_signal =
|
||||
sig.time_signal_idx < all.size() ? all[sig.time_signal_idx].name : std::string("none");
|
||||
char size[48];
|
||||
std::snprintf(size, sizeof(size), "%u x %u", sig.rows, sig.cols);
|
||||
|
||||
const std::vector<DetailRow> rows = {
|
||||
{"Unit:", sig.unit.empty() ? "--" : sig.unit, "", "udpscope-unit",
|
||||
"edit the unit this signal is shown and exported in",
|
||||
0},
|
||||
{"Gain:", format_calibration(cal.first), "", "udpscope-gain", "edit the gain, applied at ingest",
|
||||
1},
|
||||
{"Offset:", format_calibration(cal.second), sig.unit, "udpscope-offset",
|
||||
"edit the offset, applied at ingest",
|
||||
2},
|
||||
{"Type:", std::to_string(int(sig.type_code)), "", nullptr, nullptr, -1},
|
||||
{"Quantisation:", std::to_string(int(sig.quant_type)), "", nullptr, nullptr, -1},
|
||||
{"Size:", size, "", nullptr, nullptr, -1},
|
||||
{"Range:", format_calibration(sig.range_min) + " .. " + format_calibration(sig.range_max), sig.unit,
|
||||
nullptr, nullptr, -1},
|
||||
{"Sample rate:", format_calibration(sig.sampling_rate), "Hz", nullptr, nullptr, -1},
|
||||
{"Time mode:", time_mode, "", nullptr, nullptr, -1},
|
||||
{"Time signal:", time_signal, "", nullptr, nullptr, -1},
|
||||
};
|
||||
|
||||
GtkWidget* grid = gtk_grid_new();
|
||||
gtk_widget_set_name(grid, "udpscope-calibration"); // the probe reads its labels and pencils
|
||||
gtk_grid_set_column_spacing(GTK_GRID(grid), 8);
|
||||
gtk_grid_set_row_spacing(GTK_GRID(grid), 4);
|
||||
int r = 0;
|
||||
for (const DetailRow& row : rows) {
|
||||
GtkWidget* name_label = gtk_label_new(row.label);
|
||||
gtk_widget_set_halign(name_label, GTK_ALIGN_START);
|
||||
gtk_grid_attach(GTK_GRID(grid), name_label, 0, r, 1, 1);
|
||||
GtkWidget* value = gtk_label_new(row.value.c_str());
|
||||
gtk_widget_add_css_class(value, "numeric");
|
||||
if (row.id != nullptr) gtk_widget_set_name(value, row.id);
|
||||
gtk_label_set_xalign(GTK_LABEL(value), 1.0f);
|
||||
gtk_label_set_width_chars(GTK_LABEL(value), 14);
|
||||
gtk_widget_set_hexpand(value, true);
|
||||
gtk_widget_set_halign(value, GTK_ALIGN_END);
|
||||
gtk_grid_attach(GTK_GRID(grid), value, 1, r, 1, 1);
|
||||
GtkWidget* unit = gtk_label_new(row.unit.c_str());
|
||||
gtk_widget_add_css_class(unit, "dim-label");
|
||||
gtk_label_set_xalign(GTK_LABEL(unit), 0.0f);
|
||||
gtk_label_set_width_chars(GTK_LABEL(unit), 4);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(unit), PANGO_ELLIPSIZE_END);
|
||||
gtk_grid_attach(GTK_GRID(grid), unit, 2, r, 1, 1);
|
||||
if (row.edit >= 0) {
|
||||
GtkWidget* edit = gtk_button_new_from_icon_name("document-edit-symbolic");
|
||||
gtk_widget_add_css_class(edit, "flat");
|
||||
gtk_widget_set_tooltip_text(edit, row.tip);
|
||||
gtk_accessible_update_property(GTK_ACCESSIBLE(edit), GTK_ACCESSIBLE_PROPERTY_LABEL, row.tip, -1);
|
||||
tag(edit, index);
|
||||
g_object_set_data(G_OBJECT(edit), "udpscope-edit", GINT_TO_POINTER(row.edit));
|
||||
g_signal_connect(edit, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
const int which = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), "udpscope-edit"));
|
||||
if (which == 0) {
|
||||
open_unit_editor(*a, GTK_WINDOW(a->window), tagged(b));
|
||||
} else {
|
||||
open_calibration(*a, GTK_WINDOW(a->window), tagged(b),
|
||||
which == 1 ? CalibField::Gain : CalibField::Offset);
|
||||
}
|
||||
}),
|
||||
&app);
|
||||
gtk_grid_attach(GTK_GRID(grid), edit, 3, r, 1, 1);
|
||||
}
|
||||
++r;
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
void fill(App& app, const std::string& name, GtkWidget* page) {
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(page); c != nullptr;) {
|
||||
GtkWidget* next = gtk_widget_get_next_sibling(c);
|
||||
@@ -100,11 +192,15 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
|
||||
gtk_widget_set_halign(title, GTK_ALIGN_START);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(title), PANGO_ELLIPSIZE_END);
|
||||
gtk_box_append(GTK_BOX(page), title);
|
||||
if (!sig.unit.empty()) {
|
||||
GtkWidget* unit = gtk_label_new(sig.unit.c_str());
|
||||
gtk_widget_add_css_class(unit, "dim-label");
|
||||
gtk_widget_set_halign(unit, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(page), unit);
|
||||
if (sig.math) {
|
||||
if (!sig.unit.empty()) {
|
||||
GtkWidget* unit = gtk_label_new(sig.unit.c_str());
|
||||
gtk_widget_add_css_class(unit, "dim-label");
|
||||
gtk_widget_set_halign(unit, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(page), unit);
|
||||
}
|
||||
} else {
|
||||
gtk_box_append(GTK_BOX(page), build_info_grid(app, index, sig, all));
|
||||
}
|
||||
|
||||
// Acquire / compute. The same handler as the list's tick, which reads the signal index off the widget.
|
||||
@@ -193,13 +289,13 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
|
||||
}),
|
||||
&app);
|
||||
|
||||
// Information (a streamed signal's), then its gain and offset below, and a math channel's editor.
|
||||
GtkWidget* more_head = gtk_label_new(sig.math ? "Math channel" : "Signal");
|
||||
gtk_widget_add_css_class(more_head, "heading");
|
||||
gtk_widget_set_halign(more_head, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(page), more_head);
|
||||
GtkWidget* more = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
// A math channel's editor. (A streamed signal's details are the grid under its name.)
|
||||
if (sig.math) {
|
||||
GtkWidget* more_head = gtk_label_new("Math channel");
|
||||
gtk_widget_add_css_class(more_head, "heading");
|
||||
gtk_widget_set_halign(more_head, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(page), more_head);
|
||||
GtkWidget* more = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
GtkWidget* edit = text_button("Edit graph...", more);
|
||||
tag(edit, index);
|
||||
g_signal_connect(edit, "clicked", G_CALLBACK(on_math_edit_clicked), &app);
|
||||
@@ -207,68 +303,7 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
|
||||
gtk_widget_add_css_class(del, "destructive-action");
|
||||
tag(del, index);
|
||||
g_signal_connect(del, "clicked", G_CALLBACK(on_math_delete_clicked), &app);
|
||||
} else {
|
||||
GtkWidget* info = text_button("Information", more);
|
||||
tag(info, index);
|
||||
g_signal_connect(info, "clicked", G_CALLBACK(on_info_clicked), &app);
|
||||
}
|
||||
gtk_box_append(GTK_BOX(page), more);
|
||||
|
||||
// Gain and offset, applied at ingest: two rows, labels left, numbers right-aligned in tabular figures, the
|
||||
// offset's unit in its own column so the digits line up, and a pencil that edits that one value.
|
||||
if (!sig.math) {
|
||||
const std::pair<double, double> cal = app.model->calibration(index);
|
||||
GtkWidget* grid = gtk_grid_new();
|
||||
gtk_widget_set_name(grid, "udpscope-calibration"); // the probe reads its labels
|
||||
gtk_grid_set_column_spacing(GTK_GRID(grid), 8);
|
||||
gtk_grid_set_row_spacing(GTK_GRID(grid), 4);
|
||||
const struct {
|
||||
const char* label;
|
||||
double value;
|
||||
const std::string unit;
|
||||
CalibField field;
|
||||
const char* tip;
|
||||
} rows[] = {
|
||||
{"Gain:", cal.first, "", CalibField::Gain, "edit the gain, applied at ingest"},
|
||||
{"Offset:", cal.second, sig.unit, CalibField::Offset, "edit the offset, applied at ingest"},
|
||||
};
|
||||
int r = 0;
|
||||
for (const auto& row : rows) {
|
||||
GtkWidget* name_label = gtk_label_new(row.label);
|
||||
gtk_widget_set_halign(name_label, GTK_ALIGN_START);
|
||||
gtk_grid_attach(GTK_GRID(grid), name_label, 0, r, 1, 1);
|
||||
GtkWidget* value = gtk_label_new(format_calibration(row.value).c_str());
|
||||
gtk_widget_add_css_class(value, "numeric");
|
||||
gtk_widget_set_name(value, row.field == CalibField::Gain ? "udpscope-gain" : "udpscope-offset");
|
||||
gtk_label_set_xalign(GTK_LABEL(value), 1.0f);
|
||||
gtk_label_set_width_chars(GTK_LABEL(value), 12);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(value), PANGO_ELLIPSIZE_START);
|
||||
gtk_widget_set_hexpand(value, true);
|
||||
gtk_widget_set_halign(value, GTK_ALIGN_END);
|
||||
gtk_grid_attach(GTK_GRID(grid), value, 1, r, 1, 1);
|
||||
GtkWidget* unit = gtk_label_new(row.unit.c_str());
|
||||
gtk_widget_add_css_class(unit, "dim-label");
|
||||
gtk_label_set_xalign(GTK_LABEL(unit), 0.0f);
|
||||
gtk_label_set_width_chars(GTK_LABEL(unit), 4);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(unit), PANGO_ELLIPSIZE_END);
|
||||
gtk_grid_attach(GTK_GRID(grid), unit, 2, r, 1, 1);
|
||||
GtkWidget* edit = gtk_button_new_from_icon_name("document-edit-symbolic");
|
||||
gtk_widget_add_css_class(edit, "flat");
|
||||
gtk_widget_set_tooltip_text(edit, row.tip);
|
||||
gtk_accessible_update_property(GTK_ACCESSIBLE(edit), GTK_ACCESSIBLE_PROPERTY_LABEL, row.tip, -1);
|
||||
tag(edit, index);
|
||||
g_object_set_data(G_OBJECT(edit), "udpscope-field", GINT_TO_POINTER(static_cast<int>(row.field)));
|
||||
g_signal_connect(edit, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
open_calibration(*a, GTK_WINDOW(a->window), tagged(b),
|
||||
static_cast<CalibField>(GPOINTER_TO_INT(
|
||||
g_object_get_data(G_OBJECT(b), "udpscope-field"))));
|
||||
}),
|
||||
&app);
|
||||
gtk_grid_attach(GTK_GRID(grid), edit, 3, r, 1, 1);
|
||||
++r;
|
||||
}
|
||||
gtk_box_append(GTK_BOX(page), grid);
|
||||
gtk_box_append(GTK_BOX(page), more);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,8 +541,9 @@ gboolean signal_page_probe(gpointer user) {
|
||||
}
|
||||
slideover_close(a);
|
||||
}
|
||||
// Gain and offset on the page: the labels follow the model, and a pencil edits just its own number.
|
||||
// The details under the name: the labels follow the model, and each pencil edits just its own value.
|
||||
const std::pair<double, double> cal0 = a.model->calibration(index);
|
||||
const std::string declared0 = a.model->declared_unit(index);
|
||||
const auto label_text = [&](const char* id) -> std::string {
|
||||
auto it = a.signal_pages.find(name);
|
||||
if (it == a.signal_pages.end()) return "?";
|
||||
@@ -516,60 +552,86 @@ gboolean signal_page_probe(gpointer user) {
|
||||
});
|
||||
return l == nullptr ? "?" : gtk_label_get_text(GTK_LABEL(l));
|
||||
};
|
||||
const auto pencil_n = [&](int n) -> GtkWidget* {
|
||||
auto it = a.signal_pages.find(name);
|
||||
GtkWidget* grid = it == a.signal_pages.end() ? nullptr : find_descendant(it->second, [](GtkWidget* w) {
|
||||
return std::string(gtk_widget_get_name(w)) == "udpscope-calibration";
|
||||
});
|
||||
int seen = 0;
|
||||
for (GtkWidget* c = grid == nullptr ? nullptr : gtk_widget_get_first_child(grid); c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
if (GTK_IS_BUTTON(c) && gtk_button_get_icon_name(GTK_BUTTON(c)) != nullptr && seen++ == n) return c;
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
// Click pencil `n`, find the dialog titled `title`, set its `Spin`/`Entry`, press Apply. Returns the number
|
||||
// of editing widgets the dialog had, or -1 when there was no dialog.
|
||||
const auto drive = [&](int n, const char* title, double number, const char* text) -> int {
|
||||
GtkWidget* pencil = pencil_n(n);
|
||||
if (pencil == nullptr) return -1;
|
||||
g_signal_emit_by_name(pencil, "clicked");
|
||||
int fields = -1;
|
||||
GListModel* windows = gtk_window_get_toplevels();
|
||||
for (guint i = 0; i < g_list_model_get_n_items(windows); ++i) {
|
||||
GtkWindow* w = GTK_WINDOW(g_list_model_get_item(windows, i));
|
||||
const char* t = gtk_window_get_title(w);
|
||||
if (t != nullptr && std::string(t) == title) {
|
||||
GtkWidget* body = gtk_window_get_child(w);
|
||||
fields = 0;
|
||||
find_descendant(body, [&](GtkWidget* x) {
|
||||
if (GTK_IS_SPIN_BUTTON(x) || GTK_IS_ENTRY(x)) ++fields;
|
||||
return false;
|
||||
});
|
||||
GtkWidget* spin_w = find_descendant(body, [](GtkWidget* x) { return GTK_IS_SPIN_BUTTON(x); });
|
||||
GtkWidget* entry_w = find_descendant(body, [](GtkWidget* x) { return GTK_IS_ENTRY(x) && !GTK_IS_SPIN_BUTTON(x); });
|
||||
GtkWidget* apply = find_descendant(body, [](GtkWidget* x) { return is_button_labelled(x, "Apply"); });
|
||||
if (apply != nullptr && (spin_w != nullptr || entry_w != nullptr)) {
|
||||
if (text != nullptr && entry_w != nullptr) gtk_editable_set_text(GTK_EDITABLE(entry_w), text);
|
||||
if (text == nullptr && spin_w != nullptr) gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin_w), number);
|
||||
g_signal_emit_by_name(apply, "clicked");
|
||||
} else {
|
||||
gtk_window_close(w);
|
||||
}
|
||||
}
|
||||
g_object_unref(w);
|
||||
}
|
||||
return fields;
|
||||
};
|
||||
|
||||
const std::string cal_identity = label_text("udpscope-gain") + "/" + label_text("udpscope-offset");
|
||||
a.model->set_calibration(index, 2.5, -1.25);
|
||||
signal_pages_sync(a);
|
||||
const std::string cal_set = label_text("udpscope-gain") + "/" + label_text("udpscope-offset");
|
||||
int pencils = 0;
|
||||
int pencil_edits_one = -1;
|
||||
while (pencil_n(pencils) != nullptr) ++pencils;
|
||||
const int gain_fields = drive(1, "Gain", 4.0, nullptr);
|
||||
const std::pair<double, double> after = a.model->calibration(index);
|
||||
const int pencil_edits_one =
|
||||
gain_fields == 1 && after.first == 4.0 && after.second == -1.25 && label_text("udpscope-gain") == "4" ? 1
|
||||
: 0;
|
||||
|
||||
// The unit: a pencil renames it, the rename reaches the model's signal and the page, and an empty entry
|
||||
// restores what the stream declared.
|
||||
const int unit_fields = drive(0, "Unit", 0.0, "kV");
|
||||
std::string model_unit;
|
||||
{
|
||||
auto it = a.signal_pages.find(name);
|
||||
GtkWidget* grid = it == a.signal_pages.end() ? nullptr : find_descendant(it->second, [](GtkWidget* w) {
|
||||
return std::string(gtk_widget_get_name(w)) == "udpscope-calibration";
|
||||
});
|
||||
GtkWidget* gain_pencil = nullptr;
|
||||
if (grid != nullptr) {
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(grid); c != nullptr; c = gtk_widget_get_next_sibling(c)) {
|
||||
if (GTK_IS_BUTTON(c) && gtk_button_get_icon_name(GTK_BUTTON(c)) != nullptr) {
|
||||
if (pencils++ == 0) gain_pencil = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (gain_pencil != nullptr) {
|
||||
g_signal_emit_by_name(gain_pencil, "clicked");
|
||||
GListModel* windows = gtk_window_get_toplevels();
|
||||
for (guint i = 0; i < g_list_model_get_n_items(windows); ++i) {
|
||||
GtkWindow* w = GTK_WINDOW(g_list_model_get_item(windows, i));
|
||||
const char* title = gtk_window_get_title(w);
|
||||
if (title != nullptr && std::string(title) == "Gain") {
|
||||
GtkWidget* body = gtk_window_get_child(w);
|
||||
int spins = 0;
|
||||
find_descendant(body, [&](GtkWidget* x) {
|
||||
if (GTK_IS_SPIN_BUTTON(x)) ++spins;
|
||||
return false;
|
||||
});
|
||||
GtkWidget* spin_w = find_descendant(body, [](GtkWidget* x) { return GTK_IS_SPIN_BUTTON(x); });
|
||||
GtkWidget* apply = find_descendant(body, [](GtkWidget* x) { return is_button_labelled(x, "Apply"); });
|
||||
if (spin_w != nullptr && apply != nullptr) {
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin_w), 4.0);
|
||||
g_signal_emit_by_name(apply, "clicked");
|
||||
const std::pair<double, double> after = a.model->calibration(index);
|
||||
pencil_edits_one = spins == 1 && after.first == 4.0 && after.second == -1.25 &&
|
||||
label_text("udpscope-gain") == "4"
|
||||
? 1
|
||||
: 0;
|
||||
} else {
|
||||
gtk_window_close(w);
|
||||
}
|
||||
}
|
||||
g_object_unref(w);
|
||||
}
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(a.model->mutex());
|
||||
model_unit = a.model->signals()[index].unit;
|
||||
}
|
||||
const std::string page_unit = label_text("udpscope-unit");
|
||||
drive(0, "Unit", 0.0, "");
|
||||
std::string restored;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a.model->mutex());
|
||||
restored = a.model->signals()[index].unit;
|
||||
}
|
||||
const int unit_edit = unit_fields == 1 && model_unit == "kV" && page_unit == "kV" && restored == declared0 ? 1 : 0;
|
||||
a.model->set_calibration(index, cal0.first, cal0.second);
|
||||
signal_pages_sync(a);
|
||||
std::fprintf(stderr, "[signalpage] calibration identity=%s set=%s pencils=%d pencil_edits_one=%d\n",
|
||||
cal_identity.c_str(), cal_set.c_str(), pencils, pencil_edits_one);
|
||||
std::fprintf(stderr,
|
||||
"[signalpage] calibration identity=%s set=%s pencils=%d pencil_edits_one=%d unit_edit=%d "
|
||||
"unit='%s'\n",
|
||||
cal_identity.c_str(), cal_set.c_str(), pencils, pencil_edits_one, unit_edit, restored.c_str());
|
||||
std::fprintf(stderr,
|
||||
"[signalpage] name=%s open=%d pages=%d enable_visible=%d plot_buttons=%d plot_on_row0=%d "
|
||||
"plot_on_row0_disabled=%d "
|
||||
|
||||
Reference in new issue
Block a user