badges: source badge shows the name only; drop the signals lock; the signals page proposes what can be added

The link state is the status bar's, so the Source badge loses its state text. The signals have no lock
(the badges and signal pages are direct manipulation), and the signals page lists only unacquired
signals and uncomputed math channels (plus the acquired ones while the badge bar has overflowed).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-11-03 02:14:37 +01:00
1 parent dad424f1de
commit b98a3478ff
10 files changed
+164 -148

No files matched your search

+15 -22
View File
@@ -4595,9 +4595,8 @@ void on_panel_switch_toggled(GObject* obj, GParamSpec*, gpointer user) {
sync_panel_switches(*app); sync_panel_switches(*app);
} }
/// Both panes' modes, applied together: the trigger's body against its summary line, the two [edit] /// The trigger pane's mode: its body against its summary line and its [edit] button against what it would
/// buttons against what they would do, and the signal list rebuilt because the lock decides what a row /// do. Idempotent, so a caller only flips the flag and calls this.
/// carries. Idempotent, so a caller only flips a flag and calls this.
void apply_pane_lock(App& app) { void apply_pane_lock(App& app) {
if (app.trigger_body != nullptr) gtk_widget_set_visible(app.trigger_body, !app.trigger_locked); if (app.trigger_body != nullptr) gtk_widget_set_visible(app.trigger_body, !app.trigger_locked);
if (app.trigger_summary != nullptr) { if (app.trigger_summary != nullptr) {
@@ -4614,23 +4613,16 @@ void apply_pane_lock(App& app) {
gtk_widget_set_tooltip_text(button, tip.c_str()); gtk_widget_set_tooltip_text(button, tip.c_str());
}; };
edit_button(app.trigger_edit, app.trigger_locked, "the trigger"); edit_button(app.trigger_edit, app.trigger_locked, "the trigger");
edit_button(app.signals_edit, app.signals_locked, "the signals");
// The lock decides whether the compact line's arm control is the one on screen: with the body shown the // The lock decides whether the compact line's arm control is the one on screen: with the body shown the
// body's own button is there, and two arm buttons at once would be two controls for one state. // body's own button is there, and two arm buttons at once would be two controls for one state.
sync_arm_buttons(app); sync_arm_buttons(app);
sync_trigger_summary(app); sync_trigger_summary(app);
// The list, because the lock is what decides whether a row carries a tick, a menu and the `+`.
if (app.signal_list != nullptr) rebuild_signal_panels(app);
} }
/// Either pane's [edit] button: one handler, because the two differ only in which flag they flip. /// The trigger pane's [edit] button.
void on_pane_edit_clicked(GtkButton* button, gpointer user) { void on_pane_edit_clicked(GtkButton* button, gpointer user) {
App* app = static_cast<App*>(user); App* app = static_cast<App*>(user);
if (GTK_WIDGET(button) == app->trigger_edit) { if (GTK_WIDGET(button) == app->trigger_edit) app->trigger_locked = !app->trigger_locked;
app->trigger_locked = !app->trigger_locked;
} else if (GTK_WIDGET(button) == app->signals_edit) {
app->signals_locked = !app->signals_locked;
}
apply_pane_lock(*app); apply_pane_lock(*app);
} }
@@ -6150,12 +6142,12 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
static_cast<unsigned long long>(app.dbg_latches_at_disarm)); static_cast<unsigned long long>(app.dbg_latches_at_disarm));
std::printf("[udpscope] window control: '%s'\n", app.dbg_window_text.c_str()); std::printf("[udpscope] window control: '%s'\n", app.dbg_window_text.c_str());
{ {
// The signals panel's frame, read from the window rather than from the `.ui`: the two labels and the // The signals panel's frame, read from the window rather than from the `.ui`: the three labels (heading,
// list the file gives it. A `.ui` that lost one of them reads here -- `labels=1` or `list=0` -- rather // hint, and the "every signal is acquired" one, hidden or not) and the list the file gives it. A `.ui` that lost one of them reads here -- `labels=1` or `list=0` -- rather
// than showing as a panel quietly missing its hint. // than showing as a panel quietly missing its hint.
GtkWidget* panel = app.signal_list != nullptr ? gtk_widget_get_parent(app.signal_list) : nullptr; GtkWidget* panel = app.signal_list != nullptr ? gtk_widget_get_parent(app.signal_list) : nullptr;
// Counted over the panel's *subtree*, not its direct children: the heading lives in a heading row // Counted over the panel's *subtree*, not its direct children: the heading lives in a heading row
// beside the pane's [edit] button, and a count of children would read 1 whatever the file lost. // and the hint in a box of its own, and a count of children would read 1 whatever the file lost.
int labels = 0; int labels = 0;
int lists = 0; int lists = 0;
const std::function<void(GtkWidget*)> count = [&](GtkWidget* w) { const std::function<void(GtkWidget*)> count = [&](GtkWidget* w) {
@@ -6175,13 +6167,15 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
// ones, and the `MATH` divider plus the math channels account for the rest -- so // 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 `+`. // `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 " std::printf("[udpscope] signals panel: labels=%d list=%d rows=%d signals=%zu math=%zu "
"divider=%d drag_sources=%d\n", "divider=%d drag_sources=%d addable=%zu addable_ok=%d empty_label=%d\n",
labels, lists, app.dbg_signal_rows, app.dbg_signal_plain, app.math.size(), labels, lists, app.dbg_signal_rows, app.dbg_signal_plain, app.dbg_signal_math_rows,
app.dbg_signal_rows == app.dbg_signal_rows ==
static_cast<int>(app.dbg_signal_plain + 1 + app.math.size()) static_cast<int>(app.dbg_signal_plain + 1 + app.dbg_signal_math_rows)
? 1 ? 1
: 0, : 0,
app.dbg_signal_drag_sources); app.dbg_signal_drag_sources, app.dbg_signal_addable,
app.dbg_signal_plain + app.dbg_signal_math_rows == app.dbg_signal_addable ? 1 : 0,
app.dbg_signal_empty_label);
} }
{ {
// 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
@@ -6218,9 +6212,8 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
app.dbg_pane_view_icon.c_str(), app.dbg_pane_view_icon.c_str(),
gtk_icon_theme_has_icon(theme, app.dbg_pane_view_icon.c_str()) ? "ok" : "MISSING"); gtk_icon_theme_has_icon(theme, app.dbg_pane_view_icon.c_str()) ? "ok" : "MISSING");
} }
std::printf("[udpscope] panes: trigger=%s signals=%s body_visible=%d summary='%s'\n", std::printf("[udpscope] panes: trigger=%s body_visible=%d summary='%s'\n",
app.dbg_trigger_locked ? "locked" : "config", app.dbg_trigger_locked ? "locked" : "config", app.dbg_trigger_body_visible,
app.dbg_signals_locked ? "locked" : "config", app.dbg_trigger_body_visible,
app.dbg_trigger_summary.c_str()); app.dbg_trigger_summary.c_str());
std::printf("[udpscope] panes: the same line as the probe found it: '%s'\n", std::printf("[udpscope] panes: the same line as the probe found it: '%s'\n",
app.dbg_trigger_summary_early.c_str()); app.dbg_trigger_summary_early.c_str());
+20 -17
View File
@@ -2,11 +2,13 @@
<!-- SPDX-License-Identifier: MIT --> <!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari --> <!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!-- <!--
The signals panel: a heading, the hint that says what a signal is for, and the list they go in. The signals page: a heading, the hint that says what the page is for, the list, and the dim label that
replaces the list's signals when there is nothing left to add.
The rows are not here. There is one per signal in the stream, it carries the switch that decides whether The page proposes what can be *added*: the streamed signals that are not acquired and the math channels
that signal is acquired, its colour, its calibration and its drop targets, and the whole list is rebuilt that are not computed (an acquired signal has a badge in the bar). The rows are not here. There is one per
whenever the stream's signals change — so the file holds the frame and the code holds the rows. proposed signal, it carries the tick that acquires it and its menu, and the whole list is rebuilt whenever
the stream's signals or their state change — so the file holds the frame and the code holds the rows.
The list's size request is **width only**, deliberately. A height floor here made the list occupy 120 px The list's size request is **width only**, deliberately. A height floor here made the list occupy 120 px
whatever it held, which hid the compact rows' height behind it and wasted the space on an empty stream; whatever it held, which hid the compact rows' height behind it and wasted the space on an empty stream;
@@ -21,27 +23,17 @@
<property name="margin-start">8</property> <property name="margin-start">8</property>
<property name="margin-end">8</property> <property name="margin-end">8</property>
<child> <child>
<!-- The heading row, with the button that switches this pane between its two modes: locked, where <!-- The heading row (the contextual help's "?" joins it). -->
the list shows the signals that are on and nothing else, and configuration, which is the list
with every control on it. It is the code that swaps the icon and the tooltip, because the mode
is the code's. -->
<object class="GtkBox" id="signals_head"> <object class="GtkBox" id="signals_head">
<property name="orientation">horizontal</property> <property name="orientation">horizontal</property>
<property name="spacing">8</property> <property name="spacing">8</property>
<child> <child>
<object class="GtkLabel" id="signals_heading"> <object class="GtkLabel" id="signals_heading">
<property name="label" translatable="yes">Signals</property> <property name="label" translatable="yes">Add a signal</property>
<property name="halign">start</property> <property name="halign">start</property>
<property name="hexpand">1</property> <property name="hexpand">1</property>
</object> </object>
</child> </child>
<child>
<object class="GtkButton" id="signals_edit">
<property name="icon-name">document-edit-symbolic</property>
<property name="valign">center</property>
<property name="css-classes">flat</property>
</object>
</child>
</object> </object>
</child> </child>
<child> <child>
@@ -53,7 +45,7 @@
<property name="spacing">6</property> <property name="spacing">6</property>
<child> <child>
<object class="GtkLabel" id="signals_hint"> <object class="GtkLabel" id="signals_hint">
<property name="label" translatable="yes">click a signal for its page; drag its badge onto a plot</property> <property name="label" translatable="yes">tick a signal to acquire it; click one to open its page</property>
<property name="halign">start</property> <property name="halign">start</property>
<property name="xalign">0</property> <property name="xalign">0</property>
<property name="wrap">1</property> <property name="wrap">1</property>
@@ -63,6 +55,17 @@
</child> </child>
</object> </object>
</child> </child>
<child>
<!-- Shown by the code when every signal the stream declares is acquired. -->
<object class="GtkLabel" id="signals_empty">
<property name="label" translatable="yes">every signal is acquired</property>
<property name="halign">start</property>
<property name="xalign">0</property>
<property name="wrap">1</property>
<property name="visible">0</property>
<property name="css-classes">dim-label</property>
</object>
</child>
<child> <child>
<object class="GtkListBox" id="signal_list"> <object class="GtkListBox" id="signal_list">
<property name="selection-mode">none</property> <property name="selection-mode">none</property>
+20 -13
View File
@@ -535,13 +535,12 @@ struct App {
/// file stores a *name* and the trigger needs an index into the acquired channels, which do not /// file stores a *name* and the trigger needs an index into the acquired channels, which do not
/// exist until the stream has been described. Empty means "nothing pending". /// exist until the stream has been described. Empty means "nothing pending".
std::string pending_trigger_channel; std::string pending_trigger_channel;
/// The two panes' modes. *Locked* is the one a returning user gets: the trigger pane shows a line /// The trigger pane's mode. *Locked* is the one a returning user gets: the pane shows a line that
/// that says what it is set to, the signal list shows only the signals that are on, and neither /// says what it is set to and offers no control that changes the configuration. A first run -- no
/// offers a control that changes the configuration. A first run -- no session file for this source /// session file for this source -- starts in configuration mode, because that is what a first run is
/// -- starts in configuration mode, because that is what a first run is for. The flags last for the /// for. The flag lasts for the session: a lock is a mode, not a setting, so it is not persisted.
/// session: a lock is a mode, not a setting, so it is not persisted. /// (The signals have no lock: the badges and the signal pages are direct manipulation.)
bool trigger_locked = false; bool trigger_locked = false;
bool signals_locked = false;
/// The locked trigger pane's content: a small definition list, because one line cannot hold the /// The locked trigger pane's content: a small definition list, because one line cannot hold the
/// signal, the condition, the level and the mode, and those are what a user reads the pane for. /// signal, the condition, the level and the mode, and those are what a user reads the pane for.
GtkWidget* trigger_summary = nullptr; GtkWidget* trigger_summary = nullptr;
@@ -553,14 +552,14 @@ struct App {
/// pane is locked, which is the one combination where the body's own Arm button has gone with the body. /// pane is locked, which is the one combination where the body's own Arm button has gone with the body.
GtkWidget* trigger_summary_arm = nullptr; GtkWidget* trigger_summary_arm = nullptr;
GtkWidget* trigger_edit = nullptr; GtkWidget* trigger_edit = nullptr;
GtkWidget* signals_edit = nullptr; /// The dim label of the `signals` page that says nothing is left to add.
GtkWidget* signals_empty = nullptr;
int dbg_trigger_locked = -1; int dbg_trigger_locked = -1;
int dbg_summary_arm_off = -1; ///< the compact line's arm control with the trigger *off* int dbg_summary_arm_off = -1; ///< the compact line's arm control with the trigger *off*
int dbg_summary_arm_visible = -1; ///< the compact line's arm control, locked and with the trigger on int dbg_summary_arm_visible = -1; ///< the compact line's arm control, locked and with the trigger on
int dbg_summary_arm_sensitive = -1; int dbg_summary_arm_sensitive = -1;
std::string dbg_summary_arm_label; ///< its text, which is the armed state in the user's words std::string dbg_summary_arm_label; ///< its text, which is the armed state in the user's words
int dbg_summary_arm_width = -1; ///< its allocation a frame later: 0 means "not laid out" int dbg_summary_arm_width = -1; ///< its allocation a frame later: 0 means "not laid out"
int dbg_signals_locked = -1;
int dbg_trigger_body_visible = -1; int dbg_trigger_body_visible = -1;
/// The compact line the locked trigger pane shows. It is the pane's whole content in that mode, so it /// The compact line the locked trigger pane shows. It is the pane's whole content in that mode, so it
/// is read rather than assumed: empty here is a locked pane showing nothing at all. /// is read rather than assumed: empty here is a locked pane showing nothing at all.
@@ -1160,7 +1159,7 @@ struct App {
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 /// 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 /// of them are streamed signals. The `MATH` divider and the math rows account for the rest, and
/// `rows == signals + 1 + math` is the invariant the dump checks -- a list that lost its `+`. /// `rows == signals + 1 + math` is the invariant the dump checks -- a list that lost its `+`.
int dbg_signal_rows = 0; int dbg_signal_rows = 0;
/// How many rows of the list carry a `GtkDragSource`: 0 -- the signal badges are the drag handles now. /// How many rows of the list carry a `GtkDragSource`: 0 -- the signal badges are the drag handles now.
@@ -1169,6 +1168,11 @@ struct App {
/// -1 when there is no badge to drag. /// -1 when there is no badge to drag.
int dbg_drag_closed_slideover = -1; int dbg_drag_closed_slideover = -1;
std::size_t dbg_signal_plain = 0; std::size_t dbg_signal_plain = 0;
/// The math channels among those rows, and the number of signals the page should be proposing (counted from
/// the model, not from the rows built), and whether the "every signal is acquired" label was on screen.
std::size_t dbg_signal_math_rows = 0;
std::size_t dbg_signal_addable = 0;
int dbg_signal_empty_label = -1;
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;
@@ -1683,6 +1687,10 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals);
/// Every plottable signal once (the time base skipped, a group's layers folded to the first), in model order. /// Every plottable signal once (the time base skipped, a group's layers folded to the first), in model order.
/// The one rule the list and the badges share. /// The one rule the list and the badges share.
std::vector<std::size_t> listed_signals(const std::vector<SignalInfo>& signals); std::vector<std::size_t> listed_signals(const std::vector<SignalInfo>& signals);
/// Whether more signals are acquired than `kMaxSignalBadges`: the badge bar then shows one "N signals" badge,
/// and the `signals` page lists the acquired signals as well, since their rows are the only way to a page.
/// The one rule the bar and the page share.
bool signal_badges_overflowed(App& app, const std::vector<SignalInfo>& signals);
/// Puts `signal` (an index into `signals()`) on plot `row` in the first free slot, switching it on first if /// Puts `signal` (an index into `signals()`) on plot `row` in the first free slot, switching it on first if
/// need be. Says why not through `set_notice` and returns false when it cannot. The one path a drop and a /// need be. Says why not through `set_notice` and returns false when it cannot. The one path a drop and a
/// signal page's "Plot on" button share. /// signal page's "Plot on" button share.
@@ -1699,7 +1707,7 @@ bool signal_page_ensure(App& app, const std::string& name);
void signal_pages_sync(App& app); void signal_pages_sync(App& app);
gboolean signal_page_probe(gpointer user); // UDPSCOPE_PROBE_SIGNAL_PAGES_MS gboolean signal_page_probe(gpointer user); // UDPSCOPE_PROBE_SIGNAL_PAGES_MS
/// The `MATH` divider row in the one channel list, and the `+` it carries. /// The `MATH` divider row in the one channel list, and the `+` it carries.
GtkWidget* make_math_divider(App& app, bool with_add); GtkWidget* make_math_divider(App& app);
void on_math_add_clicked(GtkButton* button, gpointer user); 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`. /// 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); int math_index_for_signal(App& app, const std::string& name);
@@ -1849,9 +1857,8 @@ const char* trigger_mode_text(TriggerMode m);
TriggerMode trigger_mode_from_text(const std::string& s); TriggerMode trigger_mode_from_text(const std::string& s);
/// The panel's controls from the model, and the session's trigger into both. /// The panel's controls from the model, and the session's trigger into both.
void write_trigger_widgets(App& app); void write_trigger_widgets(App& app);
/// The compact line the trigger pane shows while it is locked, and the lock itself: the panes' /// The compact line the trigger pane shows while it is locked, and the lock itself: the pane's body and
/// bodies, their [edit] buttons and the signal list's rows all follow from `trigger_locked` and /// its [edit] button follow from `trigger_locked`.
/// `signals_locked`.
void sync_trigger_summary(App& app); void sync_trigger_summary(App& app);
/// Both arm buttons -- the panel's and the compact line's -- from the one armed state the producer keeps in /// Both arm buttons -- the panel's and the compact line's -- from the one armed state the producer keeps in
+9 -9
View File
@@ -181,6 +181,9 @@ void build_badge_bar(App& app) {
gtk_widget_set_can_target(bb.source.button, FALSE); gtk_widget_set_can_target(bb.source.button, FALSE);
gtk_widget_set_focusable(bb.source.button, FALSE); gtk_widget_set_focusable(bb.source.button, FALSE);
} }
// The link state is the status bar's to say; the badge names the source and opens its pages. The empty
// state label is hidden, as the "+" badge's is, rather than leaving a gap in the pill.
gtk_widget_set_visible(bb.source.state, FALSE);
// The signal badges sit in a scroller: a bar whose minimum width grew with the number of signals would // The signal badges sit in a scroller: a bar whose minimum width grew with the number of signals would
// widen the window (layout must not depend on content). EXTERNAL scrolls by wheel/touch with no bar. // widen the window (layout must not depend on content). EXTERNAL scrolls by wheel/touch with no bar.
GtkWidget* scroller = gtk_scrolled_window_new(); GtkWidget* scroller = gtk_scrolled_window_new();
@@ -227,7 +230,7 @@ void sync_signal_badges(App& app) {
bb.signal_badges.clear(); bb.signal_badges.clear();
bb.overflow = Badge{}; bb.overflow = Badge{};
const bool overflowed = shown.size() > kMaxSignalBadges; const bool overflowed = shown.size() > kMaxSignalBadges; // == signal_badges_overflowed()
if (overflowed) { if (overflowed) {
make_badge(app, bb.overflow, bb.signals_row, "", "signals"); make_badge(app, bb.overflow, bb.signals_row, "", "signals");
gtk_widget_set_visible(bb.overflow.state, FALSE); gtk_widget_set_visible(bb.overflow.state, FALSE);
@@ -275,18 +278,15 @@ void sync_badge_bar(App& app) {
set_open(sb->button, slideover_page(app) == sb->page); set_open(sb->button, slideover_page(app) == sb->page);
} }
if (bb.source.button == nullptr) return; if (bb.source.button == nullptr) return;
// The source's name and link state. A file has neither a link nor a kind worth naming. // The source's name; the link state is the status bar's. A file has no kind worth naming.
const bool file_mode = app.replaying || !app.model->config().open_waveform.empty(); const bool file_mode = app.replaying || !app.model->config().open_waveform.empty();
std::string name = "Source"; std::string name = "Source";
const char* state = "";
if (file_mode) { if (file_mode) {
name = "File"; name = "File";
} else { } else if (SourceKind* kind = find_kind(app.source_kind)) {
if (SourceKind* kind = find_kind(app.source_kind)) name = kind->display_name(); name = kind->display_name();
state = app.model->stats().connected.load(std::memory_order_relaxed) ? "connected" : "disconnected";
} }
set_text_if_changed(bb.source.name, name.c_str()); set_text_if_changed(bb.source.name, name.c_str());
set_text_if_changed(bb.source.state, state);
set_open(bb.source.button, slideover_page(app).rfind("source-", 0) == 0); set_open(bb.source.button, slideover_page(app).rfind("source-", 0) == 0);
} }
@@ -348,10 +348,10 @@ gboolean badges_probe(gpointer user) {
} }
slideover_close(*a); slideover_close(*a);
std::fprintf(stderr, std::fprintf(stderr,
"[badges] source name=%s state=%s pages=%zu switch_visible=%d can_target=%d second_page=%s " "[badges] source name=%s pages=%zu switch_visible=%d can_target=%d second_page=%s "
"second_ok=%d open_marked=%d closed_unmarked=%d panel_min_w=%d\n", "second_ok=%d open_marked=%d closed_unmarked=%d panel_min_w=%d\n",
gtk_label_get_text(GTK_LABEL(a->badges.source.name)), gtk_label_get_text(GTK_LABEL(a->badges.source.name)),
gtk_label_get_text(GTK_LABEL(a->badges.source.state)), a->slideover.source_pages.size(), a->slideover.source_pages.size(),
switch_visible ? 1 : 0, gtk_widget_get_can_target(a->badges.source.button) ? 1 : 0, switch_visible ? 1 : 0, gtk_widget_get_can_target(a->badges.source.button) ? 1 : 0,
second_page.empty() ? "-" : second_page.c_str(), second_ok, second_page.empty() ? "-" : second_page.c_str(), second_ok,
first.empty() ? -1 : marked ? 1 : 0, first.empty() ? -1 : marked ? 1 : 0,
+7 -6
View File
@@ -89,9 +89,10 @@ constexpr HelpHint kHelpHints[] = {
"arithmetic, wired together and named here. It is a channel like any other, so it can be " "arithmetic, wired together and named here. It is a channel like any other, so it can be "
"plotted, measured, searched and exported.", "plotted, measured, searched and exported.",
"", &App::math_head}, "", &App::math_head},
{"signals", "The signals", {"signals", "Add a signal",
"Every signal the source declares, each with the switch that acquires it. Nothing is buffered " "The signals the source declares that are not acquired yet, and the math channels not computed, "
"until it is switched on, and only acquired signals appear in the drop targets and the " "each with the tick that acquires it; an acquired signal has a badge in the bar instead. Nothing "
"is buffered until it is ticked, and only acquired signals appear in the drop targets and the "
"measurement editor. A signal's badge, dragged onto a plot, draws it there.", "measurement editor. A signal's badge, dragged onto a plot, draws it there.",
"G", &App::signals_head}, "G", &App::signals_head},
{"events", "The event history", {"events", "The event history",
@@ -116,9 +117,9 @@ constexpr HelpHint kHelpHints[] = {
"every row.", "every row.",
"", &App::trigger_search}, "", &App::trigger_search},
{"signals_config", "Configuring signals", {"signals_config", "Configuring signals",
"The list is locked until the pencil above opens the per-signal controls: the calibration that " "A signal's own page, opened by its badge or by its row here, holds the calibration that maps "
"maps raw counts to a unit, the colour a trace is drawn in, and which plot a signal is drawn " "raw counts to a unit, the colour a trace is drawn in, and which plot a signal is drawn on. A "
"on. A signal nothing measures can be switched off to stop acquiring it.", "signal nothing measures can be switched off there to stop acquiring it.",
"", &App::signals_note}, "", &App::signals_note},
{"window", "The time window", {"window", "The time window",
"How much time is on screen, counted back from the newest sample. Type a span, change its unit, " "How much time is on screen, counted back from the newest sample. Type a span, change its unit, "
+4 -5
View File
@@ -516,12 +516,11 @@ void apply_layout(App& app, const LayoutState& st) {
// above have built the channel list. // above have built the channel list.
apply_trigger_layout(app, st.trigger); apply_trigger_layout(app, st.trigger);
// A session that loaded is a user who has been here before, and that is what the lock means: the two // A session that loaded is a user who has been here before, and that is what the lock means: the trigger
// panes come up showing what they are set to rather than showing every control. A first run -- no // pane comes up showing what it is set to rather than showing every control. A first run -- no file for
// file for this source -- never reaches this function and stays in configuration mode, which is what // this source -- never reaches this function and stays in configuration mode, which is what a first run
// a first run is for. // is for.
app.trigger_locked = true; app.trigger_locked = true;
app.signals_locked = true;
apply_pane_lock(app); apply_pane_lock(app);
} }
+21 -19
View File
@@ -102,7 +102,7 @@ gboolean exit_probe_step3b(gpointer user) {
a->dbg_signal_w = gtk_widget_get_width(a->signal_panel); a->dbg_signal_w = gtk_widget_get_width(a->signal_panel);
a->dbg_signal_h = gtk_widget_get_height(a->signal_panel); a->dbg_signal_h = gtk_widget_get_height(a->signal_panel);
// How many rows of the signal list carry a drag source: none. Dragging a signal onto a plot is the signal // How many rows of the signal list carry a drag source: none. Dragging a signal onto a plot is the signal
// badge's job (and survives the pane lock, since the badges do not follow it); the list's rows only open // badge's job (the signals have no lock to survive); the list's rows only open
// the signal's page. The reading says the list did not grow a second, competing handle. // the signal's page. The reading says the list did not grow a second, competing handle.
if (a->signal_list != nullptr) { if (a->signal_list != nullptr) {
int sources = 0; int sources = 0;
@@ -3836,32 +3836,35 @@ gboolean enable_all_probe(gpointer user) {
std::fprintf(stderr, "[enable-all-probe] %zu channel(s) enabled and assigned to plots, notice='%s'\n", std::fprintf(stderr, "[enable-all-probe] %zu channel(s) enabled and assigned to plots, notice='%s'\n",
a->model->channel_count(), a->notice.c_str()); a->model->channel_count(), a->notice.c_str());
std::fflush(stderr); std::fflush(stderr);
// Then switch one off through its own switch in the signal list: a disabled signal must leave the plots, // Then switch one off through its own switch on the signal's page -- the user's path now that an acquired
// not sit there drawing nothing in a slot a live one could take. Late enough that a math probe armed behind // signal is not in the list: a disabled signal must leave the plots, not sit there drawing nothing in a slot
// this one has had its graphs installed -- the dependants are the point of the second line. // a live one could take. Late enough that a math probe armed behind this one has had its graphs installed --
// the dependants are the point of the second line.
g_timeout_add(2000, +[](gpointer u) -> gboolean { g_timeout_add(2000, +[](gpointer u) -> gboolean {
App* app = static_cast<App*>(u); App* app = static_cast<App*>(u);
if (app->model->channels().empty() || app->signal_list == nullptr) return G_SOURCE_REMOVE; if (app->model->channels().empty() || app->signal_list == nullptr) return G_SOURCE_REMOVE;
const std::size_t signal = app->model->channels().front().signal; const std::size_t signal = app->model->channels().front().signal;
const std::string name = app->model->channels().front().name; const std::string name = app->model->channels().front().name;
GtkWidget* tick = nullptr; GtkWidget* tick = nullptr;
for (GtkWidget* row_widget = gtk_widget_get_first_child(app->signal_list); std::string signal_name;
row_widget != nullptr && tick == nullptr; {
row_widget = gtk_widget_get_next_sibling(row_widget)) { std::lock_guard<std::mutex> lock(app->model->mutex());
// The tick sits a box inside the row, which is the shape `signal_tick_visible()` walks too. const std::vector<SignalInfo>& sigs = app->model->signals();
for (GtkWidget* box = gtk_widget_get_first_child(row_widget); box != nullptr && tick == nullptr; if (signal < sigs.size()) signal_name = sigs[signal].name;
box = gtk_widget_get_next_sibling(box)) { }
for (GtkWidget* w = gtk_widget_get_first_child(box); w != nullptr; if (!signal_name.empty() && signal_page_ensure(*app, signal_name)) {
w = gtk_widget_get_next_sibling(w)) { // The page's "Acquire this signal" check is the first check button in it.
if (!GTK_IS_CHECK_BUTTON(w)) continue; const std::function<GtkWidget*(GtkWidget*)> first_check = [&](GtkWidget* root) -> GtkWidget* {
if (GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(w), "udpscope-signal")) != signal) continue; for (GtkWidget* c = gtk_widget_get_first_child(root); c != nullptr; c = gtk_widget_get_next_sibling(c)) {
tick = w; if (GTK_IS_CHECK_BUTTON(c)) return c;
break; if (GtkWidget* deeper = first_check(c)) return deeper;
} }
} return nullptr;
};
tick = first_check(app->signal_pages[signal_name]);
} }
if (tick == nullptr) { if (tick == nullptr) {
std::fprintf(stderr, "[enable-all-probe] no acquire tick found for signal %zu\n", signal); std::fprintf(stderr, "[enable-all-probe] no acquire tick found on the page of signal %zu\n", signal);
std::fflush(stderr); std::fflush(stderr);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
@@ -4314,7 +4317,6 @@ gboolean exit_probe_step1(gpointer user) {
g_signal_emit_by_name(a->trigger_edit, "clicked"); // and back to the view g_signal_emit_by_name(a->trigger_edit, "clicked"); // and back to the view
} }
a->dbg_trigger_locked = a->trigger_locked ? 1 : 0; a->dbg_trigger_locked = a->trigger_locked ? 1 : 0;
a->dbg_signals_locked = a->signals_locked ? 1 : 0;
a->dbg_trigger_body_visible = a->dbg_trigger_body_visible =
a->trigger_body != nullptr && gtk_widget_get_visible(a->trigger_body) ? 1 : 0; a->trigger_body != nullptr && gtk_widget_get_visible(a->trigger_body) ? 1 : 0;
// The summary's text comes from the sync that wrote it: the container is a grid of key/value rows, // The summary's text comes from the sync that wrote it: the container is a grid of key/value rows,
+18 -26
View File
@@ -65,10 +65,9 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
const SignalInfo sig = all[index]; const SignalInfo sig = all[index];
const bool enabled = app.model->signal_enabled(index); const bool enabled = app.model->signal_enabled(index);
const bool plotted = signal_plotted(app, sig.name); const bool plotted = signal_plotted(app, sig.name);
const bool unlocked = !app.signals_locked;
// Back to the list: a page reached from a list row has no other way home. // Back to the list: a page reached from a list row has no other way home.
GtkWidget* back = text_button("All signals", page); GtkWidget* back = text_button("Back to signals", page);
gtk_widget_set_halign(back, GTK_ALIGN_START); gtk_widget_set_halign(back, GTK_ALIGN_START);
gtk_widget_add_css_class(back, "flat"); gtk_widget_add_css_class(back, "flat");
g_signal_connect(back, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) { g_signal_connect(back, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
@@ -96,8 +95,6 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
g_object_set_data(G_OBJECT(enable), "udpscope-signal", GSIZE_TO_POINTER(index)); g_object_set_data(G_OBJECT(enable), "udpscope-signal", GSIZE_TO_POINTER(index));
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_widget_set_visible(enable, sig.math || app.waveform_name.empty()); gtk_widget_set_visible(enable, sig.math || app.waveform_name.empty());
// The signal lock is the list's lock too: the page may not do what the locked list refuses.
gtk_widget_set_sensitive(enable, unlocked);
gtk_box_append(GTK_BOX(page), enable); gtk_box_append(GTK_BOX(page), enable);
// Colour. // Colour.
@@ -110,7 +107,7 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
GtkWidget* colour_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); GtkWidget* colour_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_box_append(GTK_BOX(colour_row), chip); gtk_box_append(GTK_BOX(colour_row), chip);
GtkWidget* pick = text_button("Change colour...", colour_row); GtkWidget* pick = text_button("Change colour...", colour_row);
gtk_widget_set_sensitive(pick, plotted && unlocked); gtk_widget_set_sensitive(pick, plotted);
gtk_widget_set_tooltip_text(pick, plotted ? "the colour this signal is plotted in" gtk_widget_set_tooltip_text(pick, plotted ? "the colour this signal is plotted in"
: "on no plot, so there is no trace to colour"); : "on no plot, so there is no trace to colour");
tag(pick, index); tag(pick, index);
@@ -119,7 +116,7 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
}), }),
&app); &app);
GtkWidget* reset = text_button("Default colour", colour_row); GtkWidget* reset = text_button("Default colour", colour_row);
gtk_widget_set_sensitive(reset, unlocked && app.rows->signal_colors().count(sig.name) != 0); gtk_widget_set_sensitive(reset, app.rows->signal_colors().count(sig.name) != 0);
tag(reset, index); tag(reset, index);
g_signal_connect(reset, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) { g_signal_connect(reset, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
outlive_the_rebuild(GTK_WIDGET(b)); outlive_the_rebuild(GTK_WIDGET(b));
@@ -184,12 +181,10 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
GtkWidget* more = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); GtkWidget* more = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
if (sig.math) { if (sig.math) {
GtkWidget* edit = text_button("Edit graph...", more); GtkWidget* edit = text_button("Edit graph...", more);
gtk_widget_set_sensitive(edit, unlocked);
tag(edit, index); tag(edit, index);
g_signal_connect(edit, "clicked", G_CALLBACK(on_math_edit_clicked), &app); g_signal_connect(edit, "clicked", G_CALLBACK(on_math_edit_clicked), &app);
GtkWidget* del = text_button("Delete", more); GtkWidget* del = text_button("Delete", more);
gtk_widget_add_css_class(del, "destructive-action"); gtk_widget_add_css_class(del, "destructive-action");
gtk_widget_set_sensitive(del, unlocked);
tag(del, index); tag(del, index);
g_signal_connect(del, "clicked", G_CALLBACK(on_math_delete_clicked), &app); g_signal_connect(del, "clicked", G_CALLBACK(on_math_delete_clicked), &app);
} else { } else {
@@ -197,7 +192,6 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
tag(info, index); tag(info, index);
g_signal_connect(info, "clicked", G_CALLBACK(on_info_clicked), &app); g_signal_connect(info, "clicked", G_CALLBACK(on_info_clicked), &app);
GtkWidget* cal = text_button("Calibrate...", more); GtkWidget* cal = text_button("Calibrate...", more);
gtk_widget_set_sensitive(cal, unlocked);
gtk_widget_set_tooltip_text(cal, "gain and offset, applied at ingest"); gtk_widget_set_tooltip_text(cal, "gain and offset, applied at ingest");
tag(cal, index); tag(cal, index);
g_signal_connect(cal, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) { g_signal_connect(cal, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
@@ -337,20 +331,6 @@ gboolean signal_page_probe(gpointer user) {
plot_on_row0_disabled = gtk_widget_get_sensitive(gtk_widget_get_first_child(rows_box_on)) ? 0 : 1; plot_on_row0_disabled = gtk_widget_get_sensitive(gtk_widget_get_first_child(rows_box_on)) ? 0 : 1;
} }
// The signal lock: with it on, the page's acquire tick is insensitive after the sync (the lock change
// re-runs it through rebuild_signal_panels); restored afterwards.
int locked_enable_sensitive = -1;
const bool was_locked = a.signals_locked;
a.signals_locked = true;
signal_pages_sync(a);
page_it = a.signal_pages.find(name);
if (page_it != a.signal_pages.end()) {
GtkWidget* enable_now = find_descendant(page_it->second, [](GtkWidget* w) { return GTK_IS_CHECK_BUTTON(w); });
if (enable_now != nullptr) locked_enable_sensitive = gtk_widget_get_sensitive(enable_now) ? 1 : 0;
}
a.signals_locked = was_locked;
signal_pages_sync(a);
int removed = -1; int removed = -1;
page_it = a.signal_pages.find(name); page_it = a.signal_pages.find(name);
GtkWidget* off = page_it == a.signal_pages.end() GtkWidget* off = page_it == a.signal_pages.end()
@@ -381,8 +361,15 @@ gboolean signal_page_probe(gpointer user) {
slideover_open(a, "signals"); slideover_open(a, "signals");
const int back_to_list = slideover_page(a) == "signals" ? 1 : 0; const int back_to_list = slideover_page(a) == "signals" ? 1 : 0;
// Activating the signal's row in the list opens its page (the path a click takes). // Activating the signal's row in the list opens its page (the path a click takes). The list proposes what
// can be added, so the signal has a row only while it is not acquired: switch it off for the leg and back
// on after it. (With more than six acquired signals the rows list the acquired ones too.)
slideover_close(a); slideover_close(a);
const bool was_enabled = a.model->signal_enabled(index);
if (was_enabled) {
a.model->set_signal_enabled(index, false);
rebuild_signal_panels(a);
}
int row_opens = -1; int row_opens = -1;
for (GtkWidget* r = gtk_widget_get_first_child(a.signal_list); r != nullptr; r = gtk_widget_get_next_sibling(r)) { for (GtkWidget* r = gtk_widget_get_first_child(a.signal_list); r != nullptr; r = gtk_widget_get_next_sibling(r)) {
if (GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(r), "udpscope-signal")) == index && GTK_IS_LIST_BOX_ROW(r)) { if (GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(r), "udpscope-signal")) == index && GTK_IS_LIST_BOX_ROW(r)) {
@@ -391,6 +378,11 @@ gboolean signal_page_probe(gpointer user) {
break; break;
} }
} }
if (was_enabled) {
slideover_close(a);
a.model->set_signal_enabled(index, true);
rebuild_signal_panels(a);
}
// Pruning: a math channel's page, open, then the channel deleted; the sync must close the panel and drop // Pruning: a math channel's page, open, then the channel deleted; the sync must close the panel and drop
// the page. Left at -1 when no channel can be added (nothing acquired to build it from). // the page. Left at -1 when no channel can be added (nothing acquired to build it from).
@@ -447,10 +439,10 @@ gboolean signal_page_probe(gpointer user) {
} }
std::fprintf(stderr, std::fprintf(stderr,
"[signalpage] name=%s open=%d pages=%d enable_visible=%d plot_buttons=%d plot_on_row0=%d " "[signalpage] name=%s open=%d pages=%d enable_visible=%d plot_buttons=%d plot_on_row0=%d "
"plot_on_row0_disabled=%d locked_enable_sensitive=%d " "plot_on_row0_disabled=%d "
"removed=%d btn_row0=%d back_to_list=%d row_opens=%d pruned=%d other_page_kept=%d rows=%d\n", "removed=%d btn_row0=%d back_to_list=%d row_opens=%d pruned=%d other_page_kept=%d rows=%d\n",
name.c_str(), open, pages, enable_visible, plot_buttons, plot_on_row0, plot_on_row0_disabled, name.c_str(), open, pages, enable_visible, plot_buttons, plot_on_row0, plot_on_row0_disabled,
locked_enable_sensitive, removed, btn_row0, back_to_list, row_opens, pruned, removed, btn_row0, back_to_list, row_opens, pruned,
other_page_kept, a.rows->row_count()); other_page_kept, a.rows->row_count());
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
+46 -28
View File
@@ -167,7 +167,7 @@ int math_index_for_signal(App& app, const std::string& name) {
/// The `MATH` boundary in the one channel list, carrying the `+` that creates a channel. Not /// 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 /// selectable and not activatable: it is a heading, and a heading that highlights under the
/// pointer reads as a row you can plot. /// pointer reads as a row you can plot.
GtkWidget* make_math_divider(App& app, bool with_add) { GtkWidget* make_math_divider(App& app) {
GtkWidget* row = gtk_list_box_row_new(); GtkWidget* row = gtk_list_box_row_new();
gtk_list_box_row_set_selectable(GTK_LIST_BOX_ROW(row), FALSE); gtk_list_box_row_set_selectable(GTK_LIST_BOX_ROW(row), FALSE);
gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(row), FALSE); gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(row), FALSE);
@@ -181,16 +181,12 @@ GtkWidget* make_math_divider(App& app, bool with_add) {
gtk_widget_set_halign(label, GTK_ALIGN_START); gtk_widget_set_halign(label, GTK_ALIGN_START);
gtk_widget_set_hexpand(label, TRUE); gtk_widget_set_hexpand(label, TRUE);
gtk_box_append(GTK_BOX(box), label); gtk_box_append(GTK_BOX(box), label);
// Locked: the heading stays, because the list it divides is the one being read, but the `+` that // The `+` that creates a channel: the "new math channel" entry of a page that proposes what can be added.
// creates a channel does not. A `+` on a locked list is a button that opens the editor the lock GtkWidget* add = gtk_button_new_from_icon_name("list-add-symbolic");
// exists to keep shut. gtk_widget_add_css_class(add, "flat");
if (with_add) { gtk_widget_set_tooltip_text(add, "add a math channel");
GtkWidget* add = gtk_button_new_from_icon_name("list-add-symbolic"); g_signal_connect(add, "clicked", G_CALLBACK(on_math_add_clicked), &app);
gtk_widget_add_css_class(add, "flat"); gtk_box_append(GTK_BOX(box), add);
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); gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box);
return row; return row;
} }
@@ -257,6 +253,14 @@ void on_math_delete_clicked(GtkButton* button, gpointer user) {
apply_math_channels(*a); apply_math_channels(*a);
} }
bool signal_badges_overflowed(App& app, const std::vector<SignalInfo>& signals) {
std::size_t enabled = 0;
for (std::size_t i : listed_signals(signals)) {
if (app.model->signal_enabled(i)) ++enabled;
}
return enabled > kMaxSignalBadges;
}
std::vector<std::size_t> listed_signals(const std::vector<SignalInfo>& signals) { std::vector<std::size_t> listed_signals(const std::vector<SignalInfo>& signals) {
std::vector<std::size_t> out; std::vector<std::size_t> out;
for (std::size_t i = 0; i < signals.size(); ++i) { for (std::size_t i = 0; i < signals.size(); ++i) {
@@ -310,12 +314,20 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
bool math_divider_done = false; bool math_divider_done = false;
// Streamed signals, counted as they are drawn: the dump reads `rows == signals + 1 + math`. // Streamed signals, counted as they are drawn: the dump reads `rows == signals + 1 + math`.
std::size_t plain = 0; std::size_t plain = 0;
std::size_t math_rows = 0;
// The page proposes what can be *added*: an acquired signal already has a badge, so it is not listed --
// unless the badges have overflowed into one "N signals" badge, when the rows are the only way to reach
// each signal's page.
const bool overflowed = signal_badges_overflowed(app, signals);
for (std::size_t i : listed_signals(signals)) { for (std::size_t i : listed_signals(signals)) {
// Locked: a row per signal that is *on*, which is the list a user reads rather than the list they if (!overflowed && app.model->signal_enabled(i)) continue;
// configure. Turning one on is what the pane's [edit] button is for.
if (app.signals_locked && !app.model->signal_enabled(i)) continue;
rows.push_back(i); rows.push_back(i);
} }
// Nothing addable: say so rather than leave the math entry alone under the hint. Not before the stream
// has declared any signal -- "every signal is acquired" would be untrue of no signals at all.
if (app.signals_empty != nullptr) {
gtk_widget_set_visible(app.signals_empty, rows.empty() && !listed_signals(signals).empty());
}
for (std::size_t i : rows) { for (std::size_t i : rows) {
const SignalInfo& sig = signals[i]; const SignalInfo& sig = signals[i];
@@ -323,9 +335,13 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
// `+` that creates the first one. // `+` that creates the first one.
if (sig.math && !math_divider_done) { if (sig.math && !math_divider_done) {
math_divider_done = true; math_divider_done = true;
gtk_list_box_append(GTK_LIST_BOX(list), make_math_divider(app, !app.signals_locked)); gtk_list_box_append(GTK_LIST_BOX(list), make_math_divider(app));
}
if (sig.math) {
++math_rows;
} else {
++plain;
} }
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);
@@ -339,7 +355,7 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
chip, !app.model->signal_enabled(i) ? "not acquired" chip, !app.model->signal_enabled(i) ? "not acquired"
: plotted ? "the colour this signal is plotted in -- click to change it" : plotted ? "the colour this signal is plotted in -- click to change it"
: "on no plot, so there is no trace to colour"); : "on no plot, so there is no trace to colour");
if (plotted && !app.signals_locked) { if (plotted) {
// A gesture on the label rather than a `GtkButton` around it: a button brings its own // A gesture on the label rather than a `GtkButton` around it: a button brings its own
// padding, and padding on this row is what made the list twice as tall once already (see // padding, and padding on this row is what made the list twice as tall once already (see
// this function's note). The chip also has to stay exactly one disc wide, or the names // this function's note). The chip also has to stay exactly one disc wide, or the names
@@ -373,9 +389,7 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
gtk_widget_set_visible(enable, sig.math || 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);
// Locked: the switch decides what is acquired, so it is a configuration control and it goes with gtk_box_append(GTK_BOX(box), enable);
// the rest of them.
if (!app.signals_locked) gtk_box_append(GTK_BOX(box), enable);
GtkWidget* name = gtk_label_new(sig.name.c_str()); GtkWidget* name = gtk_label_new(sig.name.c_str());
gtk_widget_set_hexpand(name, true); gtk_widget_set_hexpand(name, true);
@@ -459,9 +473,7 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
gtk_menu_button_set_popover(GTK_MENU_BUTTON(more), popover); gtk_menu_button_set_popover(GTK_MENU_BUTTON(more), popover);
gtk_widget_add_css_class(more, "flat"); gtk_widget_add_css_class(more, "flat");
gtk_widget_set_tooltip_text(more, "information and calibration"); gtk_widget_set_tooltip_text(more, "information and calibration");
// Locked: the menu holds calibration, deletion and a rename, which are exactly what this mode is gtk_box_append(GTK_BOX(box), more);
// not for. The popover is left unattached, so nothing can reach it.
if (!app.signals_locked) gtk_box_append(GTK_BOX(box), more);
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box); gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box);
// Activating the row opens the signal's page; the row resolves its identity the way its widgets do. // Activating the row opens the signal's page; the row resolves its identity the way its widgets do.
@@ -473,9 +485,18 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
// Appended when there are no math channels at all: the `+` is the only way to create the first // 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. // one, so it cannot be conditional on one already existing.
if (!math_divider_done) { if (!math_divider_done) {
gtk_list_box_append(GTK_LIST_BOX(list), make_math_divider(app, !app.signals_locked)); gtk_list_box_append(GTK_LIST_BOX(list), make_math_divider(app));
} }
app.dbg_signal_plain = plain; app.dbg_signal_plain = plain;
app.dbg_signal_math_rows = math_rows;
// The proposal as the model states it, to compare with the rows built: the signals not acquired, or every
// listed signal while the badges are overflowed.
app.dbg_signal_addable = 0;
for (std::size_t i : listed_signals(signals)) {
if (overflowed || !app.model->signal_enabled(i)) ++app.dbg_signal_addable;
}
app.dbg_signal_empty_label =
app.signals_empty != nullptr && gtk_widget_get_visible(app.signals_empty) != FALSE ? 1 : 0;
app.dbg_signal_rows = 0; app.dbg_signal_rows = 0;
for (GtkWidget* c = gtk_widget_get_first_child(list); c != nullptr; for (GtkWidget* c = gtk_widget_get_first_child(list); c != nullptr;
c = gtk_widget_get_next_sibling(c)) { c = gtk_widget_get_next_sibling(c)) {
@@ -608,10 +629,7 @@ GtkWidget* build_signal_panel(App& app) {
// The hint line under it, which is where the "signals_config" hint joins: the heading already // The hint line under it, which is where the "signals_config" hint joins: the heading already
// carries one, and the rule the layer follows is a hint beside the thing it explains. // carries one, and the rule the layer follows is a hint beside the thing it explains.
app.signals_note = GTK_WIDGET(gtk_builder_get_object(ui, "signals_note")); app.signals_note = GTK_WIDGET(gtk_builder_get_object(ui, "signals_note"));
app.signals_edit = GTK_WIDGET(gtk_builder_get_object(ui, "signals_edit")); app.signals_empty = GTK_WIDGET(gtk_builder_get_object(ui, "signals_empty"));
if (app.signals_edit != nullptr) {
g_signal_connect(app.signals_edit, "clicked", G_CALLBACK(on_pane_edit_clicked), &app);
}
if (box == nullptr || app.signal_list == nullptr) { if (box == nullptr || app.signal_list == nullptr) {
// A missing id is a mistake in the `.ui`: the panel comes up empty rather than taking the window with // A missing id is a mistake in the `.ui`: the panel comes up empty rather than taking the window with
// it, and `[udpscope] signals panel:` reads the difference. // it, and `[udpscope] signals panel:` reads the difference.
+4 -3
View File
@@ -458,9 +458,10 @@ constexpr TourStep kTourSteps[] = {
"The dock beside the plot: the cursors, then the measurements. The buttons in the bar above " "The dock beside the plot: the cursors, then the measurements. The buttons in the bar above "
"show and hide it.", "show and hide it.",
&App::side_box}, &App::side_box},
{"signals", "signals", "The signals", {"signals", "signals", "Adding a signal",
"Every signal the source declares, each with the switch that acquires it. The + badge opens " "What can be added: the signals the source declares that are not acquired yet, each with the "
"this list; a signal has to be switched on before it can be plotted, measured or searched.", "tick that acquires it. The + badge opens this list; a signal has to be acquired before it can "
"be plotted, measured or searched, and its badge then appears in the bar.",
&App::signal_list}, &App::signal_list},
{"status", nullptr, "The status line", {"status", nullptr, "The status line",
"The stream's own report, once a second: the display mode, the rate arriving, the window on " "The stream's own report, once a second: the display mode, the rate arriving, the window on "