ui: a signal's page shows gain and offset as aligned rows with a pencil each, replacing Calibrate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-11-03 11:36:58 +01:00
1 parent 6ff7dd786e
commit bbef7faf3d
9 files changed
+185 -30

No files matched your search

+20 -8
View File
@@ -865,15 +865,21 @@ void on_info_clicked(GtkButton* button, gpointer user) {
void on_calibrate_apply(GtkButton*, gpointer user) {
CalibContext* ctx = static_cast<CalibContext*>(user);
ctx->app->model->set_calibration(ctx->signal, gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->gain)),
gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->offset)));
// A single-field dialog has only its own spin; the other half is the model's record, unchanged.
std::pair<double, double> cal = ctx->app->model->calibration(ctx->signal);
if (ctx->gain != nullptr) cal.first = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->gain));
if (ctx->offset != nullptr) cal.second = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->offset));
ctx->app->model->set_calibration(ctx->signal, cal.first, cal.second);
schedule_layout_save(*ctx->app);
// The signal page quotes the calibration; refill it before the dialog goes.
signal_pages_sync(*ctx->app);
gtk_window_destroy(GTK_WINDOW(ctx->window));
}
void open_calibration(App& app, GtkWindow* parent, std::size_t signal) {
void open_calibration(App& app, GtkWindow* parent, std::size_t signal, CalibField field) {
const SignalInfo& sig = app.model->signals()[signal];
GtkWidget* window = dialog_window(GTK_WIDGET(parent), "Calibrate");
const char* heading = field == CalibField::Gain ? "Gain" : field == CalibField::Offset ? "Offset" : "Calibrate";
GtkWidget* window = dialog_window(GTK_WIDGET(parent), heading);
GtkWidget* box = gtk_window_get_child(GTK_WINDOW(window));
GtkWidget* title = gtk_label_new(sig.name.c_str());
gtk_widget_set_halign(title, GTK_ALIGN_START);
@@ -884,10 +890,16 @@ void open_calibration(App& app, GtkWindow* parent, std::size_t signal) {
// and the values are the model's record, so a signal that is not acquired still shows the
// calibration that will apply to it when it is.
const std::pair<double, double> cal = app.model->calibration(signal);
GtkWidget* gain = spin(-1.0e12, 1.0e12, 0.001, 6, cal.first);
GtkWidget* offset = spin(-1.0e12, 1.0e12, 0.001, 6, cal.second);
gtk_box_append(GTK_BOX(box), labeled_box("gain", gain));
gtk_box_append(GTK_BOX(box), labeled_box("offset", offset));
GtkWidget* gain = nullptr;
GtkWidget* offset = nullptr;
if (field != CalibField::Offset) {
gain = spin(-1.0e12, 1.0e12, 0.001, 6, cal.first);
gtk_box_append(GTK_BOX(box), labeled_box("gain", gain));
}
if (field != CalibField::Gain) {
offset = spin(-1.0e12, 1.0e12, 0.001, 6, cal.second);
gtk_box_append(GTK_BOX(box), labeled_box("offset", offset));
}
auto* ctx = new CalibContext{&app, signal, gain, offset, window};
GtkWidget* cancel = gtk_button_new_with_label("Cancel");
+3 -1
View File
@@ -1711,7 +1711,9 @@ void on_trigger_signal_changed(GObject* obj, GParamSpec*, gpointer user);
void show_info_dialog(GtkWindow* parent, const SignalInfo& sig);
void on_info_clicked(GtkButton* button, gpointer user);
void on_calibrate_apply(GtkButton*, gpointer user);
void open_calibration(App& app, GtkWindow* parent, std::size_t signal);
/// 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);
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);
+2 -2
View File
@@ -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, holds the calibration that maps "
"raw counts to a unit, 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 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 "
"signal nothing measures can be switched off there to stop acquiring it.",
"", &App::signals_note},
{"window", "The time window",
+143 -10
View File
@@ -2,13 +2,14 @@
// 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 and calibration (a math channel's editor instead). Opened from a row of the signal list;
// its information, gain and offset (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"
#include <gtk/gtk.h>
#include <cmath>
#include <cstdio>
#include <mutex>
#include <string>
@@ -31,6 +32,25 @@ std::size_t signal_index(App& app, const std::string& name) {
return std::string::npos;
}
/// A calibration number as written on the page: up to six decimals (what the editor's spin carries),
/// trailing zeros dropped, so the identity reads `1` and `0` and a trim of 0.000125 is not rounded away;
/// out of that range, three-to-six significant digits in exponent form.
std::string format_calibration(double v) {
if (!std::isfinite(v)) return "--";
char buf[64];
const double mag = std::fabs(v);
if (mag >= 1.0e9 || (mag > 0.0 && mag < 1.0e-6)) {
std::snprintf(buf, sizeof(buf), "%.6g", v);
return buf;
}
std::snprintf(buf, sizeof(buf), "%.6f", v);
std::string num = buf;
while (num.size() > 1 && num.back() == '0') num.pop_back();
if (num.back() == '.') num.pop_back();
if (num == "-0") num = "0";
return num;
}
GtkWidget* text_button(const char* label, GtkWidget* parent) {
GtkWidget* b = gtk_button_new_with_label(label);
gtk_box_append(GTK_BOX(parent), b);
@@ -173,7 +193,7 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
}),
&app);
// Information (a streamed signal's), calibration (ditto), and a math channel's editor.
// 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);
@@ -191,16 +211,65 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
GtkWidget* info = text_button("Information", more);
tag(info, index);
g_signal_connect(info, "clicked", G_CALLBACK(on_info_clicked), &app);
GtkWidget* cal = text_button("Calibrate...", more);
gtk_widget_set_tooltip_text(cal, "gain and offset, applied at ingest");
tag(cal, index);
g_signal_connect(cal, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
App* a = static_cast<App*>(user);
open_calibration(*a, GTK_WINDOW(a->window), tagged(b));
}),
&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);
}
}
} // namespace
@@ -437,6 +506,70 @@ 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.
const std::pair<double, double> cal0 = a.model->calibration(index);
const auto label_text = [&](const char* id) -> std::string {
auto it = a.signal_pages.find(name);
if (it == a.signal_pages.end()) return "?";
GtkWidget* l = find_descendant(it->second, [&](GtkWidget* w) {
return GTK_IS_LABEL(w) && std::string(gtk_widget_get_name(w)) == id;
});
return l == nullptr ? "?" : gtk_label_get_text(GTK_LABEL(l));
};
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;
{
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);
}
}
}
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] name=%s open=%d pages=%d enable_visible=%d plot_buttons=%d plot_on_row0=%d "
"plot_on_row0_disabled=%d "