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
+13
-93
@@ -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) {
|
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
|
// 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;
|
// 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);
|
app->trigger_panel = build_trigger_panel(*app);
|
||||||
gtk_box_append(GTK_BOX(signals_page), app->trigger_panel);
|
gtk_box_append(GTK_BOX(signals_page), app->trigger_panel);
|
||||||
gtk_box_append(GTK_BOX(signals_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
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);
|
app->signal_panel = build_signal_panel(*app);
|
||||||
gtk_box_append(GTK_BOX(signals_page), app->signal_panel);
|
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")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_STATS_MS")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), stats_probe, &app);
|
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")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_SEARCH_MS")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), search_probe, &app);
|
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_LABEL(c)) ++labels;
|
||||||
if (GTK_IS_LIST_BOX(c)) ++lists;
|
if (GTK_IS_LIST_BOX(c)) ++lists;
|
||||||
}
|
}
|
||||||
std::printf("[udpscope] signals panel: labels=%d list=%d\n", labels, lists);
|
// 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 `+`.
|
||||||
// The math panel's frame, read from the window: the head row with the title and the add button, and the
|
std::printf("[udpscope] signals panel: labels=%d list=%d rows=%d signals=%zu math=%zu "
|
||||||
// list. `buttons=0` is a `.ui` that lost the `+` -- otherwise a panel that cannot add anything, with
|
"divider=%d\n",
|
||||||
// nothing else complaining -- and the title is read out to show the file's own text arrived.
|
labels, lists, app.dbg_signal_rows, app.dbg_signal_plain, app.math.size(),
|
||||||
GtkWidget* panel = app.math_list != nullptr ? gtk_widget_get_parent(app.math_list) : nullptr;
|
app.dbg_signal_rows ==
|
||||||
GtkWidget* head = panel != nullptr ? gtk_widget_get_first_child(panel) : nullptr;
|
static_cast<int>(app.dbg_signal_plain + 1 + app.math.size())
|
||||||
GtkWidget* title = head != nullptr ? gtk_widget_get_first_child(head) : nullptr;
|
? 1
|
||||||
int head_children = 0;
|
: 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 events pane's frame, read from the window: the four widgets the `.ui` gives it, and the list
|
// The events pane's frame, read from the window: the four widgets the `.ui` gives it, and the list
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
<file preprocess="xml-stripblanks">main-window.ui</file>
|
<file preprocess="xml-stripblanks">main-window.ui</file>
|
||||||
<file preprocess="xml-stripblanks">cursors-panel.ui</file>
|
<file preprocess="xml-stripblanks">cursors-panel.ui</file>
|
||||||
<file preprocess="xml-stripblanks">measures-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">math-editor.ui</file>
|
||||||
<file preprocess="xml-stripblanks">events-pane.ui</file>
|
<file preprocess="xml-stripblanks">events-pane.ui</file>
|
||||||
<file preprocess="xml-stripblanks">header-bar.ui</file>
|
<file preprocess="xml-stripblanks">header-bar.ui</file>
|
||||||
|
|||||||
+20
-6
@@ -242,11 +242,10 @@ struct App {
|
|||||||
std::vector<MeasureResult> measure_results;
|
std::vector<MeasureResult> measure_results;
|
||||||
GtkWidget* measures_list = nullptr;
|
GtkWidget* measures_list = nullptr;
|
||||||
// --- math channels --------------------------------------------------------
|
// --- math channels --------------------------------------------------------
|
||||||
/// The app's copy of the math channels, and the source of truth for the pane. Every change goes
|
/// The app's copy of the math channels: what the signal list's ticks mirror into and what the
|
||||||
/// through `apply_math_channels()`, which writes this and hands it to the model -- the model's own
|
/// session file is written from. Every change goes through `apply_math_channels()`, which hands it
|
||||||
/// copy is what the producer evaluates.
|
/// to the model -- the model's own copy is what the producer evaluates.
|
||||||
std::vector<MathChannel> math;
|
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
|
/// 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 --
|
/// 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.
|
/// 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_sensitive = 0;
|
||||||
int dbg_trigger_items = 0;
|
int dbg_trigger_items = 0;
|
||||||
int dbg_trigger_selected = -1;
|
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;
|
int dbg_side_column_after_hide = 1;
|
||||||
std::string dbg_mode_triggered;
|
std::string dbg_mode_triggered;
|
||||||
std::string dbg_window_text;
|
std::string dbg_window_text;
|
||||||
@@ -1147,6 +1151,9 @@ constexpr int kEventThumbH = 46;
|
|||||||
constexpr int kEventThumbMaxH = 104;
|
constexpr int kEventThumbMaxH = 104;
|
||||||
inline int g_stats_phase = 0;
|
inline int g_stats_phase = 0;
|
||||||
inline std::vector<std::uint64_t> g_stats_n_before;
|
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 double g_trigrenum_edge = 0.0;
|
||||||
inline std::uint64_t g_trigrenum_latches = 0;
|
inline std::uint64_t g_trigrenum_latches = 0;
|
||||||
inline ConditionProbe g_condition;
|
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 on_signal_color_chosen(GObject* source, GAsyncResult* res, gpointer user);
|
||||||
void open_signal_color(App& app, std::size_t signal);
|
void open_signal_color(App& app, std::size_t signal);
|
||||||
void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals);
|
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);
|
void rebuild_trigger_signals(App& app);
|
||||||
double window_limit(const App& app);
|
double window_limit(const App& app);
|
||||||
void sync_window_display(App& app, double seconds);
|
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);
|
void on_math_apply(GtkButton*, gpointer user);
|
||||||
GtkWidget* math_row(const char* label, GtkWidget* widget, GtkWidget** row_out);
|
GtkWidget* math_row(const char* label, GtkWidget* widget, GtkWidget** row_out);
|
||||||
void open_math_dialog(App& app, int edit);
|
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_measures_panel(App& app);
|
||||||
GtkWidget* build_cursors_panel(App& app);
|
GtkWidget* build_cursors_panel(App& app);
|
||||||
void on_cursors_toggled(GtkToggleButton* button, gpointer user);
|
void on_cursors_toggled(GtkToggleButton* button, gpointer user);
|
||||||
@@ -1415,6 +1427,8 @@ gboolean measure_probe(gpointer user);
|
|||||||
gboolean measure_trace(gpointer user);
|
gboolean measure_trace(gpointer user);
|
||||||
gboolean stats_probe_report(gpointer user);
|
gboolean stats_probe_report(gpointer user);
|
||||||
gboolean stats_probe(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 pan_probe(gpointer user);
|
||||||
gboolean arm_probe(gpointer user);
|
gboolean arm_probe(gpointer user);
|
||||||
gboolean trigrenum_probe_step3(gpointer user);
|
gboolean trigrenum_probe_step3(gpointer user);
|
||||||
|
|||||||
@@ -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
|
// 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.
|
// the reload computes the derived channels over the file as it goes.
|
||||||
reload_file_view(app);
|
reload_file_view(app);
|
||||||
rebuild_math_pane(app);
|
|
||||||
// The math channels are rows of the signal list too -- `append_math_signals_locked()` puts them
|
// 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.
|
// in `signals()` -- so the list is stale until it is rebuilt.
|
||||||
rebuild_signal_panels(app);
|
rebuild_signal_panels(app);
|
||||||
@@ -1044,25 +1043,5 @@ GtkWidget* math_row(const char* label, GtkWidget* widget, GtkWidget** row_out) {
|
|||||||
return row;
|
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 ui_impl
|
||||||
} // namespace udpscope
|
} // namespace udpscope
|
||||||
+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.
|
// an idle keeps the instance alive until after `set_active` has emitted everything and returned.
|
||||||
g_object_ref(obj);
|
g_object_ref(obj);
|
||||||
app->model->set_signal_enabled(index, on);
|
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) {
|
if (!on) {
|
||||||
// A trace of a signal nobody acquires draws nothing, and holds the slot a live one could take.
|
// 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);
|
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);
|
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
|
/// 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
|
/// 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
|
/// 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
|
// 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.
|
// things, and give them different colours. What the row toggles, colours and drags is the signal.
|
||||||
std::vector<std::size_t> rows;
|
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) {
|
for (std::size_t i = 0; i < signals.size(); ++i) {
|
||||||
if (ScopeModel::is_time_signal(signals, i)) continue; // the time base is not plottable
|
if (ScopeModel::is_time_signal(signals, i)) continue; // the time base is not plottable
|
||||||
const std::string base = layer_base_name(signals[i].name);
|
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) {
|
for (std::size_t i : rows) {
|
||||||
const SignalInfo& sig = signals[i];
|
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();
|
GtkWidget* row = gtk_list_box_row_new();
|
||||||
gtk_widget_add_css_class(row, "udpscope-compact");
|
gtk_widget_add_css_class(row, "udpscope-compact");
|
||||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
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();
|
GtkWidget* enable = gtk_check_button_new();
|
||||||
gtk_check_button_set_active(GTK_CHECK_BUTTON(enable), app.model->signal_enabled(i));
|
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
|
// 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
|
// "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.
|
// 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_object_set_data(G_OBJECT(enable), "udpscope-signal", GSIZE_TO_POINTER(i));
|
||||||
g_signal_connect(enable, "notify::active", G_CALLBACK(on_signal_switch), &app);
|
g_signal_connect(enable, "notify::active", G_CALLBACK(on_signal_switch), &app);
|
||||||
gtk_box_append(GTK_BOX(box), enable);
|
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_start(menu_box, 4);
|
||||||
gtk_widget_set_margin_end(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
|
// 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
|
// 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.
|
// 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);
|
schedule_layout_save(*a);
|
||||||
}),
|
}),
|
||||||
&app);
|
&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_box_append(GTK_BOX(menu_box), recolor);
|
||||||
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));
|
GtkWidget* cal = menu_button("Calibrate...", nullptr);
|
||||||
g_signal_connect(cal, "clicked",
|
gtk_widget_set_tooltip_text(cal, "gain and offset, applied at ingest");
|
||||||
G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
g_object_set_data(G_OBJECT(cal), "udpscope-signal", GSIZE_TO_POINTER(i));
|
||||||
App* a = static_cast<App*>(user);
|
g_signal_connect(cal, "clicked",
|
||||||
const std::size_t idx = static_cast<std::size_t>(
|
G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||||
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(b), "udpscope-signal")));
|
App* a = static_cast<App*>(user);
|
||||||
open_calibration(*a, GTK_WINDOW(a->window), idx);
|
const std::size_t idx = static_cast<std::size_t>(
|
||||||
}),
|
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(b), "udpscope-signal")));
|
||||||
&app);
|
open_calibration(*a, GTK_WINDOW(a->window), idx);
|
||||||
gtk_box_append(GTK_BOX(menu_box), cal);
|
}),
|
||||||
|
&app);
|
||||||
|
gtk_box_append(GTK_BOX(menu_box), cal);
|
||||||
|
}
|
||||||
|
|
||||||
GtkWidget* popover = gtk_popover_new();
|
GtkWidget* popover = gtk_popover_new();
|
||||||
gtk_widget_add_css_class(popover, "udpscope-menu");
|
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);
|
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;
|
app.updating_panels = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user