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
|
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
|
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.
|
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
|
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
|
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
|
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 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.
|
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
|
Clicking a signal's badge opens its own page. Directly under the name are its **Unit**, **Gain** and
|
||||||
**Gain** and **Offset**, shown as two aligned numbers with a pencil button each that edits that one value.
|
**Offset** — each with a pencil button that edits that one value — and then what the stream declared about
|
||||||
They are applied at ingest, so the measurements and the export carry the same numbers the plot shows.
|
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
|
## 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:`
|
not follow the lock). `[sigbadges] drag_sources` is the reading, and `drag_sources` on the `signals panel:`
|
||||||
line must be 0.
|
line must be 0.
|
||||||
- **The session is the layout.** `LayoutState` persists what makes a reopened scope look like the
|
- **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
|
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
|
*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
|
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
|
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.
|
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
|
(`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
|
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]`
|
## `[sigbadges]`
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,10 @@
|
|||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<p>Once a signal is acquired its badge appears. Click it to open that signal's page: its
|
<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 directly under its name — <gui>Unit</gui>, <gui>Gain</gui> and <gui>Offset</gui>,
|
||||||
details, and its <gui>Gain</gui> and <gui>Offset</gui>, each with a pencil button that
|
each with a pencil button that edits that one value, then the type, size, range, sample
|
||||||
edits that one number.</p>
|
rate and timing the stream declared — its colour and <gui>Plot on</gui> buttons for the
|
||||||
|
rows.</p>
|
||||||
</item>
|
</item>
|
||||||
</steps>
|
</steps>
|
||||||
</section>
|
</section>
|
||||||
@@ -47,7 +48,9 @@
|
|||||||
through, so a trace already scaled by a gain of 2 reads 2. The pencil beside a number opens
|
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 —
|
an editor for that number only, at its current value —
|
||||||
pressing <gui>Apply</gui> confirms it rather than resetting it. A signal nobody
|
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
|
<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
|
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
|
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.window_s = 0.0;
|
||||||
st.rows.clear();
|
st.rows.clear();
|
||||||
st.calibration.clear();
|
st.calibration.clear();
|
||||||
|
st.units.clear();
|
||||||
st.colors.clear();
|
st.colors.clear();
|
||||||
st.enabled.clear();
|
st.enabled.clear();
|
||||||
st.math.clear();
|
st.math.clear();
|
||||||
@@ -446,7 +447,7 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
|||||||
|
|
||||||
std::string line;
|
std::string line;
|
||||||
// What the last `[[...]]` header opened, so keys land in the right record.
|
// 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;
|
int plot_count = 0;
|
||||||
bool plot_section_seen = false;
|
bool plot_section_seen = false;
|
||||||
while (std::getline(in, line)) {
|
while (std::getline(in, line)) {
|
||||||
@@ -460,6 +461,9 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
|||||||
} else if (t == "[[calibration]]") {
|
} else if (t == "[[calibration]]") {
|
||||||
section = Section::Calibration;
|
section = Section::Calibration;
|
||||||
st.calibration.emplace_back();
|
st.calibration.emplace_back();
|
||||||
|
} else if (t == "[[unit]]") {
|
||||||
|
section = Section::Unit;
|
||||||
|
st.units.emplace_back();
|
||||||
} else if (t == "[[color]]") {
|
} else if (t == "[[color]]") {
|
||||||
section = Section::Color;
|
section = Section::Color;
|
||||||
st.colors.emplace_back();
|
st.colors.emplace_back();
|
||||||
@@ -686,6 +690,12 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
|||||||
} catch (...) {
|
} 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()) {
|
} else if (section == Section::Color && !st.colors.empty()) {
|
||||||
if (key == "name") {
|
if (key == "name") {
|
||||||
st.colors.back().name = strip_quote(value);
|
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
|
o << "\n[[calibration]]\nname = " << toml_escape(cal.name) << "\ngain = " << cal.gain
|
||||||
<< "\noffset = " << cal.offset << "\n";
|
<< "\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) {
|
for (const auto& color : state.colors) {
|
||||||
char rgb[16];
|
char rgb[16];
|
||||||
std::snprintf(rgb, sizeof(rgb), "\"#%06X\"", color.rgb & 0x00FFFFFFu);
|
std::snprintf(rgb, sizeof(rgb), "\"#%06X\"", color.rgb & 0x00FFFFFFu);
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ struct LayoutState {
|
|||||||
};
|
};
|
||||||
std::vector<Calibration> calibration;
|
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
|
/// 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
|
/// 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.
|
/// 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(),
|
signals.erase(std::remove_if(signals.begin(), signals.end(),
|
||||||
[](const SignalInfo& s) { return s.math; }),
|
[](const SignalInfo& s) { return s.math; }),
|
||||||
signals.end());
|
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_)) {
|
if (!signals_.empty() && same_stream_signals(signals, signals_)) {
|
||||||
// A reconnect's identical CONFIG: the honest recovery is *nothing* -- the ring keeps
|
// 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
|
// 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 {
|
double ScopeModel::channel_dt(std::size_t channel) const {
|
||||||
return channel < channels_.size() ? channels_[channel].dt : 0.0;
|
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.
|
/// no channel to read it from, and the record is what a dialog must show.
|
||||||
std::pair<double, double> calibration(std::size_t signal);
|
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(); }
|
std::size_t channel_count() const noexcept { return channels_.size(); }
|
||||||
const Partition& partition() const noexcept { return part_; }
|
const Partition& partition() const noexcept { return part_; }
|
||||||
|
|
||||||
@@ -868,6 +879,9 @@ class ScopeModel {
|
|||||||
std::atomic<bool> paused_{false};
|
std::atomic<bool> paused_{false};
|
||||||
/// Per-signal-name calibration (see `set_calibration`).
|
/// Per-signal-name calibration (see `set_calibration`).
|
||||||
std::unordered_map<std::string, std::pair<double, double>> 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<std::uint64_t> event_seq_{0};
|
||||||
std::atomic<double> display_epoch_{0.0};
|
std::atomic<double> display_epoch_{0.0};
|
||||||
std::atomic<bool> trigger_armed_{false};
|
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) {
|
GdkContentProvider* on_drag_prepare(GtkDragSource*, double, double, gpointer user) {
|
||||||
DragContext* ctx = static_cast<DragContext*>(user);
|
DragContext* ctx = static_cast<DragContext*>(user);
|
||||||
// The payload is the signal index; the channel is resolved on drop.
|
// 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.
|
/// Which of the calibration's two numbers a dialog edits; `Both` is the full dialog.
|
||||||
enum class CalibField { Both, Gain, Offset };
|
enum class CalibField { Both, Gain, Offset };
|
||||||
void open_calibration(App& app, GtkWindow* parent, std::size_t signal, CalibField field = CalibField::Both);
|
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);
|
GdkContentProvider* on_drag_prepare(GtkDragSource*, double, double, gpointer user);
|
||||||
int signal_color_index(const std::vector<SignalInfo>& signals, std::size_t signal);
|
int signal_color_index(const std::vector<SignalInfo>& signals, std::size_t signal);
|
||||||
std::string color_hex(plot::Color c);
|
std::string color_hex(plot::Color c);
|
||||||
|
|||||||
@@ -123,8 +123,8 @@ constexpr HelpHint kHelpHints[] = {
|
|||||||
"every row.",
|
"every row.",
|
||||||
"", &App::trigger_search},
|
"", &App::trigger_search},
|
||||||
{"signals_config", "Configuring signals",
|
{"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 "
|
"A signal's own page, opened by its badge or by its row here, shows its unit, gain and offset (a pencil edits each) "
|
||||||
"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 "
|
"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.",
|
"signal nothing measures can be switched off there to stop acquiring it.",
|
||||||
"", &App::signals_note},
|
"", &App::signals_note},
|
||||||
{"window", "The time window",
|
{"window", "The time window",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ LayoutState collect_layout(App& app) {
|
|||||||
break;
|
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
|
// 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
|
// 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,
|
// 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
|
// 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.
|
// signal list cannot show is one `collect_layout()` would write back every session for ever.
|
||||||
for (const auto& color : st.colors) {
|
for (const auto& color : st.colors) {
|
||||||
|
|||||||
+182
-120
@@ -2,7 +2,7 @@
|
|||||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
//
|
//
|
||||||
// The per-signal slide-over pages (`signal:<name>`): one signal's acquire switch, colour, the plot it is on,
|
// 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.
|
// kept in step with the model by `signal_pages_sync()`, which `rebuild_signal_panels()` ends with.
|
||||||
|
|
||||||
#include "ui_app.hpp"
|
#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")));
|
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) {
|
void fill(App& app, const std::string& name, GtkWidget* page) {
|
||||||
for (GtkWidget* c = gtk_widget_get_first_child(page); c != nullptr;) {
|
for (GtkWidget* c = gtk_widget_get_first_child(page); c != nullptr;) {
|
||||||
GtkWidget* next = gtk_widget_get_next_sibling(c);
|
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_widget_set_halign(title, GTK_ALIGN_START);
|
||||||
gtk_label_set_ellipsize(GTK_LABEL(title), PANGO_ELLIPSIZE_END);
|
gtk_label_set_ellipsize(GTK_LABEL(title), PANGO_ELLIPSIZE_END);
|
||||||
gtk_box_append(GTK_BOX(page), title);
|
gtk_box_append(GTK_BOX(page), title);
|
||||||
if (!sig.unit.empty()) {
|
if (sig.math) {
|
||||||
GtkWidget* unit = gtk_label_new(sig.unit.c_str());
|
if (!sig.unit.empty()) {
|
||||||
gtk_widget_add_css_class(unit, "dim-label");
|
GtkWidget* unit = gtk_label_new(sig.unit.c_str());
|
||||||
gtk_widget_set_halign(unit, GTK_ALIGN_START);
|
gtk_widget_add_css_class(unit, "dim-label");
|
||||||
gtk_box_append(GTK_BOX(page), unit);
|
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.
|
// 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);
|
&app);
|
||||||
|
|
||||||
// Information (a streamed signal's), then its gain and offset below, and a math channel's editor.
|
// A math channel's editor. (A streamed signal's details are the grid under its name.)
|
||||||
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);
|
|
||||||
if (sig.math) {
|
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);
|
GtkWidget* edit = text_button("Edit graph...", more);
|
||||||
tag(edit, index);
|
tag(edit, index);
|
||||||
g_signal_connect(edit, "clicked", G_CALLBACK(on_math_edit_clicked), &app);
|
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");
|
gtk_widget_add_css_class(del, "destructive-action");
|
||||||
tag(del, index);
|
tag(del, index);
|
||||||
g_signal_connect(del, "clicked", G_CALLBACK(on_math_delete_clicked), &app);
|
g_signal_connect(del, "clicked", G_CALLBACK(on_math_delete_clicked), &app);
|
||||||
} else {
|
gtk_box_append(GTK_BOX(page), more);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,8 +541,9 @@ gboolean signal_page_probe(gpointer user) {
|
|||||||
}
|
}
|
||||||
slideover_close(a);
|
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::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 {
|
const auto label_text = [&](const char* id) -> std::string {
|
||||||
auto it = a.signal_pages.find(name);
|
auto it = a.signal_pages.find(name);
|
||||||
if (it == a.signal_pages.end()) return "?";
|
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));
|
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");
|
const std::string cal_identity = label_text("udpscope-gain") + "/" + label_text("udpscope-offset");
|
||||||
a.model->set_calibration(index, 2.5, -1.25);
|
a.model->set_calibration(index, 2.5, -1.25);
|
||||||
signal_pages_sync(a);
|
signal_pages_sync(a);
|
||||||
const std::string cal_set = label_text("udpscope-gain") + "/" + label_text("udpscope-offset");
|
const std::string cal_set = label_text("udpscope-gain") + "/" + label_text("udpscope-offset");
|
||||||
int pencils = 0;
|
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);
|
std::lock_guard<std::mutex> lock(a.model->mutex());
|
||||||
GtkWidget* grid = it == a.signal_pages.end() ? nullptr : find_descendant(it->second, [](GtkWidget* w) {
|
model_unit = a.model->signals()[index].unit;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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);
|
a.model->set_calibration(index, cal0.first, cal0.second);
|
||||||
signal_pages_sync(a);
|
signal_pages_sync(a);
|
||||||
std::fprintf(stderr, "[signalpage] calibration identity=%s set=%s pencils=%d pencil_edits_one=%d\n",
|
std::fprintf(stderr,
|
||||||
cal_identity.c_str(), cal_set.c_str(), pencils, pencil_edits_one);
|
"[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,
|
std::fprintf(stderr,
|
||||||
"[signalpage] name=%s open=%d pages=%d enable_visible=%d plot_buttons=%d plot_on_row0=%d "
|
"[signalpage] name=%s open=%d pages=%d enable_visible=%d plot_buttons=%d plot_on_row0=%d "
|
||||||
"plot_on_row0_disabled=%d "
|
"plot_on_row0_disabled=%d "
|
||||||
|
|||||||
Reference in new issue
Block a user