From a493737ca1676ab21e886a87236762e8b5b54dd4 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Wed, 14 Oct 2026 12:56:31 +0200 Subject: [PATCH] ui: one channel list, and the math pane is gone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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`. --- src/ui.cpp | 106 +++---------------- src/ui/math-panel.ui | 49 --------- src/ui/udpscope-ui.gresource.xml | 1 - src/ui_app.hpp | 26 +++-- src/ui_math.cpp | 21 ---- src/ui_signals.cpp | 173 +++++++++++++++++++++++++++---- 6 files changed, 186 insertions(+), 190 deletions(-) delete mode 100644 src/ui/math-panel.ui diff --git a/src/ui.cpp b/src/ui.cpp index c480f6d..d390583 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -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(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(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(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(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(std::atoi(env)), stats_probe, &app); } + if (const char* env = g_getenv("UDPSCOPE_PROBE_SIDE_TAB_MS")) { + g_timeout_add(static_cast(std::atoi(env)), side_tab_probe, &app); + } if (const char* env = g_getenv("UDPSCOPE_PROBE_SEARCH_MS")) { g_timeout_add(static_cast(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(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 diff --git a/src/ui/math-panel.ui b/src/ui/math-panel.ui deleted file mode 100644 index 9738111..0000000 --- a/src/ui/math-panel.ui +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - vertical - 4 - 6 - 6 - 6 - 6 - - - horizontal - 8 - - - Math channels - start - 1 - - - - - list-add-symbolic - flat - add a math channel - - - - - - - vertical - 2 - - - - diff --git a/src/ui/udpscope-ui.gresource.xml b/src/ui/udpscope-ui.gresource.xml index 2c70961..a38b3f3 100644 --- a/src/ui/udpscope-ui.gresource.xml +++ b/src/ui/udpscope-ui.gresource.xml @@ -18,7 +18,6 @@ main-window.ui cursors-panel.ui measures-panel.ui - math-panel.ui math-editor.ui events-pane.ui header-bar.ui diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 9b5f625..97000fd 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -242,11 +242,10 @@ struct App { std::vector 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 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 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 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& 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& 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); diff --git a/src/ui_math.cpp b/src/ui_math.cpp index 122f023..3004ca0 100644 --- a/src/ui_math.cpp +++ b/src/ui_math.cpp @@ -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(user)); - }), - &app); - return box; -} - } // namespace ui_impl } // namespace udpscope diff --git a/src/ui_signals.cpp b/src/ui_signals.cpp index 994b864..32f9e05 100644 --- a/src/ui_signals.cpp +++ b/src/ui_signals.cpp @@ -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(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(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(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(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 lock(app.model->mutex()); + const std::vector& 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(user); + const std::size_t index = static_cast( + 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(user); + const std::size_t index = static_cast( + 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& 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 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& 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& 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& 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& 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(user); - const std::size_t idx = static_cast( - 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(user); + const std::size_t idx = static_cast( + 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& 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; }