ui: one channel list, and the math pane is gone

`rebuild_signal_list()` is handed `model.signals()`, and `append_math_signals_locked()` already
appends one synthetic entry per math channel -- "they go through the ordinary signal path on
purpose: a derived channel is a channel" -- so **every math channel was already a row of the
signal list**, with its tick, chip, name, unit and menu. The math pane listed the same channels a
second time, so this is mostly a deletion: `build_math_panel()`, `rebuild_math_pane()`,
`src/ui/math-panel.ui` and its resource entry, `App::math_list`, and the `math panel:` dump line.

What the one list gains:

- a non-selectable `MATH` divider before the first math row, carrying the `+` that creates a
  channel -- appended even when there are no math channels at all, because a `+` conditional on
  one existing could never create the first one;
- a math row's own menu: *Edit graph…*, *Default colour*, *Delete*. No Information and no
  Calibrate (calibration is applied at ingest, and a derived channel has no stream to describe),
  and no rename, because the node editor owns a channel's name and unit -- a second place to
  change them would be a second answer to the same question. An item dismisses the row's popover
  before opening or destroying anything from it;
- `App::math` mirrored from a math row's tick: `enable_locked()` mirrors it into the model, but
  `App::math` is the copy the session file is written from. The mirror sits *before* the
  `apply_math_channels()` in the `!on` path, which would otherwise push a stale `enabled = true`
  straight back into the model;
- a tick visible in a file view too: a derived channel is computed, and a file view has no
  acquisition to start, which is the other tick's question.

`rebuild_signal_list()` also publishes the list's own shape -- `rows`, `signals`, `math` -- and
the dump prints `divider=1` when `rows == signals + 1 + math`: the reading for a list that lost
its `+`.

Verified: `make test-ui` 14 files (was 15); `signals panel: labels=2 list=1 rows=9 signals=4
math=4 divider=1`; `[math-probe] 3 math channels`, every other line unchanged; no `math panel:`
line, and zero `Gtk-CRITICAL`.
This commit is contained in:
Martino Ferrari committed 2026-10-14 12:56:31 +02:00
1 parent 132041f79e
commit a493737ca1
6 files changed
+186 -190

No files matched your search

+13 -93
View File
@@ -2248,76 +2248,6 @@ void open_math_dialog(App& app, int edit) {
}
void rebuild_math_pane(App& app) {
if (app.math_list == nullptr) return;
while (GtkWidget* child = gtk_widget_get_first_child(app.math_list)) {
gtk_box_remove(GTK_BOX(app.math_list), child);
}
if (app.math.empty()) {
GtkWidget* none = gtk_label_new("— none —");
gtk_widget_add_css_class(none, "dim-label");
gtk_widget_set_halign(none, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(app.math_list), none);
return;
}
for (std::size_t i = 0; i < app.math.size(); ++i) {
const MathChannel& m = app.math[i];
GtkWidget* row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget* on = gtk_check_button_new();
gtk_check_button_set_active(GTK_CHECK_BUTTON(on), m.enabled);
gtk_widget_set_tooltip_text(on, "enable this math channel: off means it is not acquired, not\n"
"plotted and not measured, exactly like a signal's switch");
const std::size_t index = i;
g_signal_connect(on, "toggled", G_CALLBACK(+[](GtkToggleButton* b, gpointer user) {
App* a = static_cast<App*>(user);
const std::size_t k = GPOINTER_TO_SIZE(
g_object_get_data(G_OBJECT(b), "udpscope-math-index"));
if (k >= a->math.size()) return;
a->math[k].enabled = gtk_toggle_button_get_active(b);
apply_math_channels(*a);
}),
&app);
g_object_set_data(G_OBJECT(on), "udpscope-math-index", GSIZE_TO_POINTER(index));
gtk_box_append(GTK_BOX(row), on);
std::string text = m.name;
if (!m.unit.empty()) text += " [" + m.unit + "]";
GtkWidget* label = gtk_label_new(text.c_str());
gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_MIDDLE);
gtk_widget_set_hexpand(label, true);
gtk_widget_set_halign(label, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(row), label);
GtkWidget* edit = gtk_button_new_from_icon_name("document-edit-symbolic");
gtk_widget_add_css_class(edit, "flat");
gtk_widget_set_tooltip_text(edit, "edit this math channel's graph");
g_object_set_data(G_OBJECT(edit), "udpscope-math-index", GSIZE_TO_POINTER(index));
g_signal_connect(edit, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
App* a = static_cast<App*>(user);
const std::size_t k = GPOINTER_TO_SIZE(
g_object_get_data(G_OBJECT(b), "udpscope-math-index"));
open_math_dialog(*a, static_cast<int>(k));
}),
&app);
gtk_box_append(GTK_BOX(row), edit);
GtkWidget* del = gtk_button_new_from_icon_name("window-close-symbolic");
gtk_widget_add_css_class(del, "flat");
gtk_widget_set_tooltip_text(del, "delete this math channel");
g_object_set_data(G_OBJECT(del), "udpscope-math-index", GSIZE_TO_POINTER(index));
g_signal_connect(del, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
App* a = static_cast<App*>(user);
const std::size_t k = GPOINTER_TO_SIZE(
g_object_get_data(G_OBJECT(b), "udpscope-math-index"));
if (k < a->math.size()) a->math.erase(a->math.begin() + k);
apply_math_channels(*a);
}),
&app);
gtk_box_append(GTK_BOX(row), del);
gtk_box_append(GTK_BOX(app.math_list), row);
}
}
GtkWidget* build_measures_panel(App& app) {
// The panel's frame is a UI file; the readings are not, because each row is one measurement and they are
// rebuilt whenever the set or the mode changes. `make test-ui` checks the file's types against the GIR;
@@ -4724,10 +4654,6 @@ void on_activate(GtkApplication* application, gpointer user) {
app->trigger_panel = build_trigger_panel(*app);
gtk_box_append(GTK_BOX(signals_page), app->trigger_panel);
gtk_box_append(GTK_BOX(signals_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
// Still on the signals page until the list grows the `+` and the per-row menu that replace it.
gtk_box_append(GTK_BOX(signals_page), build_math_panel(*app));
gtk_box_append(GTK_BOX(signals_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
rebuild_math_pane(*app);
app->signal_panel = build_signal_panel(*app);
gtk_box_append(GTK_BOX(signals_page), app->signal_panel);
@@ -5376,6 +5302,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
if (const char* env = g_getenv("UDPSCOPE_PROBE_STATS_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), stats_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_SIDE_TAB_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), side_tab_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_SEARCH_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), search_probe, &app);
}
@@ -5606,25 +5535,16 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
if (GTK_IS_LABEL(c)) ++labels;
if (GTK_IS_LIST_BOX(c)) ++lists;
}
std::printf("[udpscope] signals panel: labels=%d list=%d\n", labels, lists);
}
{
// The math panel's frame, read from the window: the head row with the title and the add button, and the
// list. `buttons=0` is a `.ui` that lost the `+` -- otherwise a panel that cannot add anything, with
// nothing else complaining -- and the title is read out to show the file's own text arrived.
GtkWidget* panel = app.math_list != nullptr ? gtk_widget_get_parent(app.math_list) : nullptr;
GtkWidget* head = panel != nullptr ? gtk_widget_get_first_child(panel) : nullptr;
GtkWidget* title = head != nullptr ? gtk_widget_get_first_child(head) : nullptr;
int head_children = 0;
int buttons = 0;
for (GtkWidget* c = head != nullptr ? gtk_widget_get_first_child(head) : nullptr; c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
++head_children;
if (GTK_IS_BUTTON(c)) ++buttons;
}
std::printf("[udpscope] math panel: head=%d buttons=%d list=%d title='%s'\n", head_children,
buttons, app.math_list != nullptr ? 1 : 0,
GTK_IS_LABEL(title) ? gtk_label_get_text(GTK_LABEL(title)) : "-");
// The list's shape, beside the frame: `rows` is what the list holds, `signals` the streamed
// ones, and the `MATH` divider plus the math channels account for the rest -- so
// `rows == signals + 1 + math` is the invariant, and `divider=0` is a list that lost its `+`.
std::printf("[udpscope] signals panel: labels=%d list=%d rows=%d signals=%zu math=%zu "
"divider=%d\n",
labels, lists, app.dbg_signal_rows, app.dbg_signal_plain, app.math.size(),
app.dbg_signal_rows ==
static_cast<int>(app.dbg_signal_plain + 1 + app.math.size())
? 1
: 0);
}
{
// The events pane's frame, read from the window: the four widgets the `.ui` gives it, and the list
-49
View File
@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The math panel: a heading row with the button that adds a channel, and the list they go in.
The rows are not here. A math channel's row carries its name, its unit, its expression, its colour and its
own edit and remove buttons, all of which follow the channel — so the file holds the frame and the code
holds the rows, as in the measurements and signals panels beside it.
The heading takes the slack instead of the button, which is what keeps the `+` at the row's end however
long the title or the panel is.
-->
<interface>
<object class="GtkBox" id="math_panel">
<property name="orientation">vertical</property>
<property name="spacing">4</property>
<property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<child>
<object class="GtkBox" id="math_head">
<property name="orientation">horizontal</property>
<property name="spacing">8</property>
<child>
<object class="GtkLabel" id="math_title">
<property name="label" translatable="yes">Math channels</property>
<property name="halign">start</property>
<property name="hexpand">1</property>
</object>
</child>
<child>
<object class="GtkButton" id="math_add">
<property name="icon-name">list-add-symbolic</property>
<property name="css-classes">flat</property>
<property name="tooltip-text" translatable="yes">add a math channel</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="math_list">
<property name="orientation">vertical</property>
<property name="spacing">2</property>
</object>
</child>
</object>
</interface>
-1
View File
@@ -18,7 +18,6 @@
<file preprocess="xml-stripblanks">main-window.ui</file>
<file preprocess="xml-stripblanks">cursors-panel.ui</file>
<file preprocess="xml-stripblanks">measures-panel.ui</file>
<file preprocess="xml-stripblanks">math-panel.ui</file>
<file preprocess="xml-stripblanks">math-editor.ui</file>
<file preprocess="xml-stripblanks">events-pane.ui</file>
<file preprocess="xml-stripblanks">header-bar.ui</file>
+20 -6
View File
@@ -242,11 +242,10 @@ struct App {
std::vector<MeasureResult> measure_results;
GtkWidget* measures_list = nullptr;
// --- math channels --------------------------------------------------------
/// The app's copy of the math channels, and the source of truth for the pane. Every change goes
/// through `apply_math_channels()`, which writes this and hands it to the model -- the model's own
/// copy is what the producer evaluates.
/// The app's copy of the math channels: what the signal list's ticks mirror into and what the
/// session file is written from. Every change goes through `apply_math_channels()`, which hands it
/// to the model -- the model's own copy is what the producer evaluates.
std::vector<MathChannel> math;
GtkWidget* math_list = nullptr;
/// The math editor's builder. The editor's window belongs to the builder's object list, so the builder is
/// held for as long as the window is up and cleared on the *next* opening rather than with the window --
/// freeing it under a live window frees the window with it. That is the source form's pattern.
@@ -739,6 +738,11 @@ struct App {
int dbg_trigger_sensitive = 0;
int dbg_trigger_items = 0;
int dbg_trigger_selected = -1;
/// The signal list's shape, captured where the list is built: how many rows it holds, and how many
/// of them are streamed signals. The `MATH` divider and the math channels account for the rest, and
/// `rows == signals + 1 + math` is the invariant the dump checks -- a list that lost its `+`.
int dbg_signal_rows = 0;
std::size_t dbg_signal_plain = 0;
int dbg_side_column_after_hide = 1;
std::string dbg_mode_triggered;
std::string dbg_window_text;
@@ -1147,6 +1151,9 @@ constexpr int kEventThumbH = 46;
constexpr int kEventThumbMaxH = 104;
inline int g_stats_phase = 0;
inline std::vector<std::uint64_t> g_stats_n_before;
/// The same for the tab-switch probe: the accumulator counts recorded while the Measure page was
/// still current, so the report can say whether any of them advanced afterwards.
inline std::vector<std::uint64_t> g_side_tab_n_before;
inline double g_trigrenum_edge = 0.0;
inline std::uint64_t g_trigrenum_latches = 0;
inline ConditionProbe g_condition;
@@ -1229,6 +1236,13 @@ std::string signal_chip_markup(App& app, const std::vector<SignalInfo>& signals,
void on_signal_color_chosen(GObject* source, GAsyncResult* res, gpointer user);
void open_signal_color(App& app, std::size_t signal);
void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals);
/// The `MATH` divider row in the one channel list, and the `+` it carries.
GtkWidget* make_math_divider(App& app);
void on_math_add_clicked(GtkButton* button, gpointer user);
/// The index in `App::math` of the channel a synthetic signal is named after, or `-1`.
int math_index_for_signal(App& app, const std::string& name);
void on_math_edit_clicked(GtkButton* button, gpointer user);
void on_math_delete_clicked(GtkButton* button, gpointer user);
void rebuild_trigger_signals(App& app);
double window_limit(const App& app);
void sync_window_display(App& app, double seconds);
@@ -1327,8 +1341,6 @@ void math_dialog_sync(MathDialog* d);
void on_math_apply(GtkButton*, gpointer user);
GtkWidget* math_row(const char* label, GtkWidget* widget, GtkWidget** row_out);
void open_math_dialog(App& app, int edit);
GtkWidget* build_math_panel(App& app);
void rebuild_math_pane(App& app);
GtkWidget* build_measures_panel(App& app);
GtkWidget* build_cursors_panel(App& app);
void on_cursors_toggled(GtkToggleButton* button, gpointer user);
@@ -1415,6 +1427,8 @@ gboolean measure_probe(gpointer user);
gboolean measure_trace(gpointer user);
gboolean stats_probe_report(gpointer user);
gboolean stats_probe(gpointer user);
gboolean side_tab_probe_report(gpointer user);
gboolean side_tab_probe(gpointer user);
gboolean pan_probe(gpointer user);
gboolean arm_probe(gpointer user);
gboolean trigrenum_probe_step3(gpointer user);
-21
View File
@@ -22,7 +22,6 @@ void apply_math_channels(App& app) {
// refills that; a file's nothing does, so the view is re-read -- and with the math set now in place
// the reload computes the derived channels over the file as it goes.
reload_file_view(app);
rebuild_math_pane(app);
// The math channels are rows of the signal list too -- `append_math_signals_locked()` puts them
// in `signals()` -- so the list is stale until it is rebuilt.
rebuild_signal_panels(app);
@@ -1044,25 +1043,5 @@ GtkWidget* math_row(const char* label, GtkWidget* widget, GtkWidget** row_out) {
return row;
}
GtkWidget* build_math_panel(App& app) {
// The panel's frame -- the heading row with its add button, and the list -- is a UI file; the rows are
// not, because each one is a channel and the whole list is rebuilt whenever the set changes. `make test-ui`
// checks the file's types against the GIR; the ids below are what keeps the code and the file agreeing.
GtkBuilder* ui = ui_load(app, "/org/udpscope/ui/math-panel.ui");
GtkWidget* box = GTK_WIDGET(gtk_builder_get_object(ui, "math_panel"));
GtkWidget* add = GTK_WIDGET(gtk_builder_get_object(ui, "math_add"));
app.math_list = GTK_WIDGET(gtk_builder_get_object(ui, "math_list"));
if (box == nullptr || add == nullptr || app.math_list == nullptr) {
// A missing id is a mistake in the `.ui`: the panel comes up empty rather than taking the window with
// it, and `[udpscope] math panel:` reads the difference.
return box != nullptr ? box : gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
}
g_signal_connect(add, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
add_math_channel(*static_cast<App*>(user));
}),
&app);
return box;
}
} // namespace ui_impl
} // namespace udpscope
+153 -20
View File
@@ -34,6 +34,13 @@ void on_signal_switch(GObject* obj, GParamSpec*, gpointer user) {
// an idle keeps the instance alive until after `set_active` has emitted everything and returned.
g_object_ref(obj);
app->model->set_signal_enabled(index, on);
// `App::math` is the copy the session file is written from, so a math row's tick has to reach it.
// Here and not after the `if (!on)` block below: `apply_math_channels()` down there pushes
// `App::math` into the model, and a stale `enabled = true` would switch the channel straight back on.
{
const int k = math_index_for_signal(*app, name);
if (k >= 0) app->math[static_cast<std::size_t>(k)].enabled = on;
}
if (!on) {
// A trace of a signal nobody acquires draws nothing, and holds the slot a live one could take.
int gone = remove_signal_from_plots(*app, name);
@@ -146,6 +153,91 @@ void on_signal_color_chosen(GObject* source, GAsyncResult* res, gpointer user) {
schedule_layout_save(app);
}
/// `App::math` is indexed by position and `signals()` by name, and the mapping between them is
/// the name: `append_math_signals_locked()` names each synthetic signal after its channel and
/// `enable_locked()` matches it back the same way.
int math_index_for_signal(App& app, const std::string& name) {
for (std::size_t i = 0; i < app.math.size(); ++i) {
if (app.math[i].name == name) return static_cast<int>(i);
}
return -1;
}
/// The `MATH` boundary in the one channel list, carrying the `+` that creates a channel. Not
/// selectable and not activatable: it is a heading, and a heading that highlights under the
/// pointer reads as a row you can plot.
GtkWidget* make_math_divider(App& app) {
GtkWidget* row = gtk_list_box_row_new();
gtk_list_box_row_set_selectable(GTK_LIST_BOX_ROW(row), FALSE);
gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(row), FALSE);
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_widget_set_margin_top(box, 6);
gtk_widget_set_margin_start(box, 6);
gtk_widget_set_margin_end(box, 6);
GtkWidget* label = gtk_label_new("MATH");
gtk_widget_add_css_class(label, "dim-label");
gtk_widget_add_css_class(label, "caption-heading");
gtk_widget_set_halign(label, GTK_ALIGN_START);
gtk_widget_set_hexpand(label, TRUE);
gtk_box_append(GTK_BOX(box), label);
GtkWidget* add = gtk_button_new_from_icon_name("list-add-symbolic");
gtk_widget_add_css_class(add, "flat");
gtk_widget_set_tooltip_text(add, "add a math channel");
g_signal_connect(add, "clicked", G_CALLBACK(on_math_add_clicked), &app);
gtk_box_append(GTK_BOX(box), add);
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box);
return row;
}
/// Create a channel, then open the editor on it: a new channel is a single `Source` node with a
/// default name, which is of no use until it is given a graph.
void on_math_add_clicked(GtkButton*, gpointer user) {
App* a = static_cast<App*>(user);
add_math_channel(*a);
if (a->math.empty()) return;
// `add_math_channel()` appended, so the new channel is the last one; `apply_math_channels()`
// inside it has already rebuilt this list, which is why the index is taken after the call.
open_math_dialog(*a, static_cast<int>(a->math.size()) - 1);
}
/// The row's menu items resolve their identity the one way the list already does: the widget
/// carries its *signal* index, and the name is read off the model under the lock.
static std::string signal_name_at(App& app, const std::size_t index) {
std::lock_guard<std::mutex> lock(app.model->mutex());
const std::vector<SignalInfo>& signals = app.model->signals();
return index < signals.size() ? signals[index].name : std::string();
}
/// Close the row's popover before anything is opened or destroyed from it: a dialog that takes
/// focus over an open popover leaves it hanging over the new window, and `Delete` removes the row
/// the popover belongs to, which cannot be left pointing at it.
static void dismiss_row_menu(GtkButton* button) {
GtkWidget* popover = gtk_widget_get_ancestor(GTK_WIDGET(button), GTK_TYPE_POPOVER);
if (popover != nullptr) gtk_widget_set_visible(popover, FALSE);
}
void on_math_edit_clicked(GtkButton* button, gpointer user) {
App* a = static_cast<App*>(user);
const std::size_t index = static_cast<std::size_t>(
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(button), "udpscope-signal")));
const int k = math_index_for_signal(*a, signal_name_at(*a, index));
if (k < 0) return;
dismiss_row_menu(button);
open_math_dialog(*a, k);
}
void on_math_delete_clicked(GtkButton* button, gpointer user) {
App* a = static_cast<App*>(user);
const std::size_t index = static_cast<std::size_t>(
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(button), "udpscope-signal")));
const int k = math_index_for_signal(*a, signal_name_at(*a, index));
if (k < 0) return;
dismiss_row_menu(button);
a->math.erase(a->math.begin() + k);
// Rebuilds the model's signal list, the plots and this list, and schedules the save.
apply_math_channels(*a);
}
/// One line per signal: colour chip, checkbox, name, unit, and a menu for the things
/// you touch once (information, calibration). A `GtkSwitch` and two always-visible
/// buttons made every row about twice this tall, which is what turned a four-signal
@@ -165,6 +257,12 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
// that showed both would name one "sine (A)" and the other "sine (B)" as if they were different
// things, and give them different colours. What the row toggles, colours and drags is the signal.
std::vector<std::size_t> rows;
// The `MATH` divider goes in before the first math row, and once only: `append_math_signals_locked()`
// has already put every math channel in `signals()`, so the boundary is the one thing the deleted
// math pane contributed that the list does not already have.
bool math_divider_done = false;
// Streamed signals, counted as they are drawn: the dump reads `rows == signals + 1 + math`.
std::size_t plain = 0;
for (std::size_t i = 0; i < signals.size(); ++i) {
if (ScopeModel::is_time_signal(signals, i)) continue; // the time base is not plottable
const std::string base = layer_base_name(signals[i].name);
@@ -178,6 +276,13 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
for (std::size_t i : rows) {
const SignalInfo& sig = signals[i];
// One list, with a boundary in it: a derived channel *is* a channel, and the divider carries the
// `+` that creates the first one.
if (sig.math && !math_divider_done) {
math_divider_done = true;
gtk_list_box_append(GTK_LIST_BOX(list), make_math_divider(app));
}
if (!sig.math) ++plain;
GtkWidget* row = gtk_list_box_row_new();
gtk_widget_add_css_class(row, "udpscope-compact");
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
@@ -216,11 +321,13 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
GtkWidget* enable = gtk_check_button_new();
gtk_check_button_set_active(GTK_CHECK_BUTTON(enable), app.model->signal_enabled(i));
gtk_widget_set_tooltip_text(enable, "acquire this signal");
// A derived channel is computed here, not streamed: its tick is its own switch, and it has one
// in every view -- a file view has no acquisition to start, which is the other tick's question.
gtk_widget_set_tooltip_text(enable, sig.math ? "compute this channel" : "acquire this signal");
// A file view has nothing to acquire: an event's or a file's samples are already stored, and
// "acquire this signal" is a stream's question. Hidden rather than disabled -- a greyed tick invites
// the user to look for the reason, and the reason is that this mode has no acquisition at all.
gtk_widget_set_visible(enable, app.waveform_name.empty());
gtk_widget_set_visible(enable, sig.math || app.waveform_name.empty());
g_object_set_data(G_OBJECT(enable), "udpscope-signal", GSIZE_TO_POINTER(i));
g_signal_connect(enable, "notify::active", G_CALLBACK(on_signal_switch), &app);
gtk_box_append(GTK_BOX(box), enable);
@@ -245,11 +352,6 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
gtk_widget_set_margin_start(menu_box, 4);
gtk_widget_set_margin_end(menu_box, 4);
GtkWidget* info = menu_button("Information", nullptr);
g_object_set_data(G_OBJECT(info), "udpscope-signal", GSIZE_TO_POINTER(i));
g_signal_connect(info, "clicked", G_CALLBACK(on_info_clicked), &app);
gtk_box_append(GTK_BOX(menu_box), info);
// The way back from a chosen colour. The chooser itself cannot carry a "default" button, and a
// setting with no undo is a trap; it lives here rather than on the chip because a second
// gesture on a one-glyph target is not something a user would find.
@@ -275,20 +377,42 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
schedule_layout_save(*a);
}),
&app);
gtk_box_append(GTK_BOX(menu_box), recolor);
if (sig.math) {
// A math row carries its own menu. Information and Calibrate... are a *streamed*
// signal's: calibration is applied at ingest, and a derived channel has no stream to
// describe. No rename either -- the node editor owns a math channel's name and its unit,
// and a second place to change them would be a second answer to the same question.
GtkWidget* edit = menu_button("Edit graph...", nullptr);
g_object_set_data(G_OBJECT(edit), "udpscope-signal", GSIZE_TO_POINTER(i));
g_signal_connect(edit, "clicked", G_CALLBACK(on_math_edit_clicked), &app);
gtk_box_append(GTK_BOX(menu_box), edit);
gtk_box_append(GTK_BOX(menu_box), recolor);
GtkWidget* del = menu_button("Delete", nullptr);
gtk_widget_set_tooltip_text(del, "delete this math channel");
g_object_set_data(G_OBJECT(del), "udpscope-signal", GSIZE_TO_POINTER(i));
g_signal_connect(del, "clicked", G_CALLBACK(on_math_delete_clicked), &app);
gtk_box_append(GTK_BOX(menu_box), del);
} else {
GtkWidget* info = menu_button("Information", nullptr);
g_object_set_data(G_OBJECT(info), "udpscope-signal", GSIZE_TO_POINTER(i));
g_signal_connect(info, "clicked", G_CALLBACK(on_info_clicked), &app);
gtk_box_append(GTK_BOX(menu_box), info);
GtkWidget* cal = menu_button("Calibrate...", nullptr);
gtk_widget_set_tooltip_text(cal, "gain and offset, applied at ingest");
g_object_set_data(G_OBJECT(cal), "udpscope-signal", GSIZE_TO_POINTER(i));
g_signal_connect(cal, "clicked",
G_CALLBACK(+[](GtkButton* b, gpointer user) {
App* a = static_cast<App*>(user);
const std::size_t idx = static_cast<std::size_t>(
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(b), "udpscope-signal")));
open_calibration(*a, GTK_WINDOW(a->window), idx);
}),
&app);
gtk_box_append(GTK_BOX(menu_box), cal);
gtk_box_append(GTK_BOX(menu_box), recolor);
GtkWidget* cal = menu_button("Calibrate...", nullptr);
gtk_widget_set_tooltip_text(cal, "gain and offset, applied at ingest");
g_object_set_data(G_OBJECT(cal), "udpscope-signal", GSIZE_TO_POINTER(i));
g_signal_connect(cal, "clicked",
G_CALLBACK(+[](GtkButton* b, gpointer user) {
App* a = static_cast<App*>(user);
const std::size_t idx = static_cast<std::size_t>(
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(b), "udpscope-signal")));
open_calibration(*a, GTK_WINDOW(a->window), idx);
}),
&app);
gtk_box_append(GTK_BOX(menu_box), cal);
}
GtkWidget* popover = gtk_popover_new();
gtk_widget_add_css_class(popover, "udpscope-menu");
@@ -312,6 +436,15 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
gtk_list_box_append(GTK_LIST_BOX(list), row);
}
// Appended when there are no math channels at all: the `+` is the only way to create the first
// one, so it cannot be conditional on one already existing.
if (!math_divider_done) gtk_list_box_append(GTK_LIST_BOX(list), make_math_divider(app));
app.dbg_signal_plain = plain;
app.dbg_signal_rows = 0;
for (GtkWidget* c = gtk_widget_get_first_child(list); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
++app.dbg_signal_rows;
}
app.updating_panels = false;
}