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:
1 parent
132041f79e
commit
a493737ca1
6 files changed
+186
-190
No files matched your search
+153
-20
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user