Feature/source plugins #1
No files matched your search
+15
-22
@@ -4595,9 +4595,8 @@ void on_panel_switch_toggled(GObject* obj, GParamSpec*, gpointer user) {
|
||||
sync_panel_switches(*app);
|
||||
}
|
||||
|
||||
/// Both panes' modes, applied together: the trigger's body against its summary line, the two [edit]
|
||||
/// buttons against what they would do, and the signal list rebuilt because the lock decides what a row
|
||||
/// carries. Idempotent, so a caller only flips a flag and calls this.
|
||||
/// The trigger pane's mode: its body against its summary line and its [edit] button against what it would
|
||||
/// do. Idempotent, so a caller only flips the flag and calls this.
|
||||
void apply_pane_lock(App& app) {
|
||||
if (app.trigger_body != nullptr) gtk_widget_set_visible(app.trigger_body, !app.trigger_locked);
|
||||
if (app.trigger_summary != nullptr) {
|
||||
@@ -4614,23 +4613,16 @@ void apply_pane_lock(App& app) {
|
||||
gtk_widget_set_tooltip_text(button, tip.c_str());
|
||||
};
|
||||
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
|
||||
// body's own button is there, and two arm buttons at once would be two controls for one state.
|
||||
sync_arm_buttons(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) {
|
||||
App* app = static_cast<App*>(user);
|
||||
if (GTK_WIDGET(button) == app->trigger_edit) {
|
||||
app->trigger_locked = !app->trigger_locked;
|
||||
} else if (GTK_WIDGET(button) == app->signals_edit) {
|
||||
app->signals_locked = !app->signals_locked;
|
||||
}
|
||||
if (GTK_WIDGET(button) == app->trigger_edit) app->trigger_locked = !app->trigger_locked;
|
||||
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));
|
||||
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
|
||||
// list the file gives it. A `.ui` that lost one of them reads here -- `labels=1` or `list=0` -- rather
|
||||
// The signals panel's frame, read from the window rather than from the `.ui`: the three labels (heading,
|
||||
// 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.
|
||||
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
|
||||
// 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 lists = 0;
|
||||
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
|
||||
// `rows == signals + 1 + math` is the invariant, and `divider=0` is a list that lost its `+`.
|
||||
std::printf("[udpscope] signals panel: labels=%d list=%d rows=%d signals=%zu math=%zu "
|
||||
"divider=%d drag_sources=%d\n",
|
||||
labels, lists, app.dbg_signal_rows, app.dbg_signal_plain, app.math.size(),
|
||||
"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.dbg_signal_math_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
|
||||
: 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
|
||||
@@ -6218,9 +6212,8 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
app.dbg_pane_view_icon.c_str(),
|
||||
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",
|
||||
app.dbg_trigger_locked ? "locked" : "config",
|
||||
app.dbg_signals_locked ? "locked" : "config", app.dbg_trigger_body_visible,
|
||||
std::printf("[udpscope] panes: trigger=%s body_visible=%d summary='%s'\n",
|
||||
app.dbg_trigger_locked ? "locked" : "config", app.dbg_trigger_body_visible,
|
||||
app.dbg_trigger_summary.c_str());
|
||||
std::printf("[udpscope] panes: the same line as the probe found it: '%s'\n",
|
||||
app.dbg_trigger_summary_early.c_str());
|
||||
|
||||
+20
-17
@@ -2,11 +2,13 @@
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
<!-- 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
|
||||
that signal is acquired, its colour, its calibration and its drop targets, and the whole list is rebuilt
|
||||
whenever the stream's signals change — so the file holds the frame and the code holds the rows.
|
||||
The page proposes what can be *added*: the streamed signals that are not acquired and the math channels
|
||||
that are not computed (an acquired signal has a badge in the bar). The rows are not here. There is one per
|
||||
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
|
||||
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-end">8</property>
|
||||
<child>
|
||||
<!-- The heading row, with the button that switches this pane between its two modes: locked, where
|
||||
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. -->
|
||||
<!-- The heading row (the contextual help's "?" joins it). -->
|
||||
<object class="GtkBox" id="signals_head">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="spacing">8</property>
|
||||
<child>
|
||||
<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="hexpand">1</property>
|
||||
</object>
|
||||
</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>
|
||||
</child>
|
||||
<child>
|
||||
@@ -53,7 +45,7 @@
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<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="xalign">0</property>
|
||||
<property name="wrap">1</property>
|
||||
@@ -63,6 +55,17 @@
|
||||
</child>
|
||||
</object>
|
||||
</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>
|
||||
<object class="GtkListBox" id="signal_list">
|
||||
<property name="selection-mode">none</property>
|
||||
|
||||
+20
-13
@@ -535,13 +535,12 @@ struct App {
|
||||
/// 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".
|
||||
std::string pending_trigger_channel;
|
||||
/// The two panes' modes. *Locked* is the one a returning user gets: the trigger pane shows a line
|
||||
/// that says what it is set to, the signal list shows only the signals that are on, and neither
|
||||
/// offers a control that changes the configuration. A first run -- no session file for this source
|
||||
/// -- starts in configuration mode, because that is what a first run is for. The flags last for the
|
||||
/// session: a lock is a mode, not a setting, so it is not persisted.
|
||||
/// The trigger pane's mode. *Locked* is the one a returning user gets: the pane shows a line that
|
||||
/// says what it is set to and offers no control that changes the configuration. A first run -- no
|
||||
/// session file for this source -- starts in configuration mode, because that is what a first run is
|
||||
/// for. The flag lasts for the 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 signals_locked = false;
|
||||
/// 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.
|
||||
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.
|
||||
GtkWidget* trigger_summary_arm = 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_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_sensitive = -1;
|
||||
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_signals_locked = -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
|
||||
/// 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_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
|
||||
/// 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 `+`.
|
||||
int dbg_signal_rows = 0;
|
||||
/// 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.
|
||||
int dbg_drag_closed_slideover = -1;
|
||||
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;
|
||||
std::string dbg_mode_triggered;
|
||||
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.
|
||||
/// The one rule the list and the badges share.
|
||||
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
|
||||
/// 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.
|
||||
@@ -1699,7 +1707,7 @@ bool signal_page_ensure(App& app, const std::string& name);
|
||||
void signal_pages_sync(App& app);
|
||||
gboolean signal_page_probe(gpointer user); // UDPSCOPE_PROBE_SIGNAL_PAGES_MS
|
||||
/// 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);
|
||||
/// 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);
|
||||
@@ -1849,9 +1857,8 @@ const char* trigger_mode_text(TriggerMode m);
|
||||
TriggerMode trigger_mode_from_text(const std::string& s);
|
||||
/// The panel's controls from the model, and the session's trigger into both.
|
||||
void write_trigger_widgets(App& app);
|
||||
/// The compact line the trigger pane shows while it is locked, and the lock itself: the panes'
|
||||
/// bodies, their [edit] buttons and the signal list's rows all follow from `trigger_locked` and
|
||||
/// `signals_locked`.
|
||||
/// The compact line the trigger pane shows while it is locked, and the lock itself: the pane's body and
|
||||
/// its [edit] button follow from `trigger_locked`.
|
||||
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
|
||||
|
||||
+9
-9
@@ -181,6 +181,9 @@ void build_badge_bar(App& app) {
|
||||
gtk_widget_set_can_target(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
|
||||
// widen the window (layout must not depend on content). EXTERNAL scrolls by wheel/touch with no bar.
|
||||
GtkWidget* scroller = gtk_scrolled_window_new();
|
||||
@@ -227,7 +230,7 @@ void sync_signal_badges(App& app) {
|
||||
bb.signal_badges.clear();
|
||||
bb.overflow = Badge{};
|
||||
|
||||
const bool overflowed = shown.size() > kMaxSignalBadges;
|
||||
const bool overflowed = shown.size() > kMaxSignalBadges; // == signal_badges_overflowed()
|
||||
if (overflowed) {
|
||||
make_badge(app, bb.overflow, bb.signals_row, "", "signals");
|
||||
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);
|
||||
}
|
||||
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();
|
||||
std::string name = "Source";
|
||||
const char* state = "";
|
||||
if (file_mode) {
|
||||
name = "File";
|
||||
} else {
|
||||
if (SourceKind* kind = find_kind(app.source_kind)) name = kind->display_name();
|
||||
state = app.model->stats().connected.load(std::memory_order_relaxed) ? "connected" : "disconnected";
|
||||
} else if (SourceKind* kind = find_kind(app.source_kind)) {
|
||||
name = kind->display_name();
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -348,10 +348,10 @@ gboolean badges_probe(gpointer user) {
|
||||
}
|
||||
slideover_close(*a);
|
||||
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",
|
||||
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,
|
||||
second_page.empty() ? "-" : second_page.c_str(), second_ok,
|
||||
first.empty() ? -1 : marked ? 1 : 0,
|
||||
|
||||
@@ -89,9 +89,10 @@ constexpr HelpHint kHelpHints[] = {
|
||||
"arithmetic, wired together and named here. It is a channel like any other, so it can be "
|
||||
"plotted, measured, searched and exported.",
|
||||
"", &App::math_head},
|
||||
{"signals", "The signals",
|
||||
"Every signal the source declares, each with the switch that acquires it. Nothing is buffered "
|
||||
"until it is switched on, and only acquired signals appear in the drop targets and the "
|
||||
{"signals", "Add a signal",
|
||||
"The signals the source declares that are not acquired yet, and the math channels not computed, "
|
||||
"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.",
|
||||
"G", &App::signals_head},
|
||||
{"events", "The event history",
|
||||
@@ -116,9 +117,9 @@ constexpr HelpHint kHelpHints[] = {
|
||||
"every row.",
|
||||
"", &App::trigger_search},
|
||||
{"signals_config", "Configuring signals",
|
||||
"The list is locked until the pencil above opens the per-signal controls: the calibration that "
|
||||
"maps raw counts to a unit, the colour a trace is drawn in, and which plot a signal is drawn "
|
||||
"on. A signal nothing measures can be switched off to stop acquiring it.",
|
||||
"A signal's own page, opened by its badge or by its row here, holds the calibration that maps "
|
||||
"raw counts to a unit, the colour a trace is drawn in, and which plot a signal is drawn on. A "
|
||||
"signal nothing measures can be switched off there to stop acquiring it.",
|
||||
"", &App::signals_note},
|
||||
{"window", "The time window",
|
||||
"How much time is on screen, counted back from the newest sample. Type a span, change its unit, "
|
||||
|
||||
+4
-5
@@ -516,12 +516,11 @@ void apply_layout(App& app, const LayoutState& st) {
|
||||
// above have built the channel list.
|
||||
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
|
||||
// panes come up showing what they are set to rather than showing every control. A first run -- no
|
||||
// file for this source -- never reaches this function and stays in configuration mode, which is what
|
||||
// a first run is for.
|
||||
// A session that loaded is a user who has been here before, and that is what the lock means: the trigger
|
||||
// pane comes up showing what it is set to rather than showing every control. A first run -- no file for
|
||||
// this source -- never reaches this function and stays in configuration mode, which is what a first run
|
||||
// is for.
|
||||
app.trigger_locked = true;
|
||||
app.signals_locked = true;
|
||||
apply_pane_lock(app);
|
||||
}
|
||||
|
||||
|
||||
+21
-19
@@ -102,7 +102,7 @@ gboolean exit_probe_step3b(gpointer user) {
|
||||
a->dbg_signal_w = gtk_widget_get_width(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
|
||||
// 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.
|
||||
if (a->signal_list != nullptr) {
|
||||
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",
|
||||
a->model->channel_count(), a->notice.c_str());
|
||||
std::fflush(stderr);
|
||||
// Then switch one off through its own switch in the signal list: a disabled signal must leave the plots,
|
||||
// not sit there drawing nothing in a slot 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.
|
||||
// Then switch one off through its own switch on the signal's page -- the user's path now that an acquired
|
||||
// signal is not in the list: a disabled signal must leave the plots, not sit there drawing nothing in a slot
|
||||
// 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 {
|
||||
App* app = static_cast<App*>(u);
|
||||
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::string name = app->model->channels().front().name;
|
||||
GtkWidget* tick = nullptr;
|
||||
for (GtkWidget* row_widget = gtk_widget_get_first_child(app->signal_list);
|
||||
row_widget != nullptr && tick == nullptr;
|
||||
row_widget = gtk_widget_get_next_sibling(row_widget)) {
|
||||
// The tick sits a box inside the row, which is the shape `signal_tick_visible()` walks too.
|
||||
for (GtkWidget* box = gtk_widget_get_first_child(row_widget); box != nullptr && tick == nullptr;
|
||||
box = gtk_widget_get_next_sibling(box)) {
|
||||
for (GtkWidget* w = gtk_widget_get_first_child(box); w != nullptr;
|
||||
w = gtk_widget_get_next_sibling(w)) {
|
||||
if (!GTK_IS_CHECK_BUTTON(w)) continue;
|
||||
if (GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(w), "udpscope-signal")) != signal) continue;
|
||||
tick = w;
|
||||
break;
|
||||
std::string signal_name;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(app->model->mutex());
|
||||
const std::vector<SignalInfo>& sigs = app->model->signals();
|
||||
if (signal < sigs.size()) signal_name = sigs[signal].name;
|
||||
}
|
||||
if (!signal_name.empty() && signal_page_ensure(*app, signal_name)) {
|
||||
// The page's "Acquire this signal" check is the first check button in it.
|
||||
const std::function<GtkWidget*(GtkWidget*)> first_check = [&](GtkWidget* root) -> GtkWidget* {
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(root); c != nullptr; c = gtk_widget_get_next_sibling(c)) {
|
||||
if (GTK_IS_CHECK_BUTTON(c)) return c;
|
||||
if (GtkWidget* deeper = first_check(c)) return deeper;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
tick = first_check(app->signal_pages[signal_name]);
|
||||
}
|
||||
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);
|
||||
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
|
||||
}
|
||||
a->dbg_trigger_locked = a->trigger_locked ? 1 : 0;
|
||||
a->dbg_signals_locked = a->signals_locked ? 1 : 0;
|
||||
a->dbg_trigger_body_visible =
|
||||
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,
|
||||
|
||||
+18
-26
@@ -65,10 +65,9 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
|
||||
const SignalInfo sig = all[index];
|
||||
const bool enabled = app.model->signal_enabled(index);
|
||||
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.
|
||||
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_add_css_class(back, "flat");
|
||||
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_signal_connect(enable, "notify::active", G_CALLBACK(on_signal_switch), &app);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
gtk_box_append(GTK_BOX(colour_row), chip);
|
||||
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"
|
||||
: "on no plot, so there is no trace to colour");
|
||||
tag(pick, index);
|
||||
@@ -119,7 +116,7 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
|
||||
}),
|
||||
&app);
|
||||
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);
|
||||
g_signal_connect(reset, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||
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);
|
||||
if (sig.math) {
|
||||
GtkWidget* edit = text_button("Edit graph...", more);
|
||||
gtk_widget_set_sensitive(edit, unlocked);
|
||||
tag(edit, index);
|
||||
g_signal_connect(edit, "clicked", G_CALLBACK(on_math_edit_clicked), &app);
|
||||
GtkWidget* del = text_button("Delete", more);
|
||||
gtk_widget_add_css_class(del, "destructive-action");
|
||||
gtk_widget_set_sensitive(del, unlocked);
|
||||
tag(del, index);
|
||||
g_signal_connect(del, "clicked", G_CALLBACK(on_math_delete_clicked), &app);
|
||||
} else {
|
||||
@@ -197,7 +192,6 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
|
||||
tag(info, index);
|
||||
g_signal_connect(info, "clicked", G_CALLBACK(on_info_clicked), &app);
|
||||
GtkWidget* cal = text_button("Calibrate...", more);
|
||||
gtk_widget_set_sensitive(cal, unlocked);
|
||||
gtk_widget_set_tooltip_text(cal, "gain and offset, applied at ingest");
|
||||
tag(cal, index);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
page_it = a.signal_pages.find(name);
|
||||
GtkWidget* off = page_it == a.signal_pages.end()
|
||||
@@ -381,8 +361,15 @@ gboolean signal_page_probe(gpointer user) {
|
||||
slideover_open(a, "signals");
|
||||
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);
|
||||
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;
|
||||
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)) {
|
||||
@@ -391,6 +378,11 @@ gboolean signal_page_probe(gpointer user) {
|
||||
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
|
||||
// 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,
|
||||
"[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",
|
||||
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());
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
+46
-28
@@ -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
|
||||
/// 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, bool with_add) {
|
||||
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);
|
||||
@@ -181,16 +181,12 @@ GtkWidget* make_math_divider(App& app, bool with_add) {
|
||||
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||||
gtk_widget_set_hexpand(label, TRUE);
|
||||
gtk_box_append(GTK_BOX(box), label);
|
||||
// Locked: the heading stays, because the list it divides is the one being read, but the `+` that
|
||||
// creates a channel does not. A `+` on a locked list is a button that opens the editor the lock
|
||||
// exists to keep shut.
|
||||
if (with_add) {
|
||||
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);
|
||||
}
|
||||
// The `+` that creates a channel: the "new math channel" entry of a page that proposes what can be added.
|
||||
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;
|
||||
}
|
||||
@@ -257,6 +253,14 @@ void on_math_delete_clicked(GtkButton* button, gpointer user) {
|
||||
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> out;
|
||||
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;
|
||||
// Streamed signals, counted as they are drawn: the dump reads `rows == signals + 1 + math`.
|
||||
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)) {
|
||||
// Locked: a row per signal that is *on*, which is the list a user reads rather than the list they
|
||||
// configure. Turning one on is what the pane's [edit] button is for.
|
||||
if (app.signals_locked && !app.model->signal_enabled(i)) continue;
|
||||
if (!overflowed && app.model->signal_enabled(i)) continue;
|
||||
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) {
|
||||
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.
|
||||
if (sig.math && !math_divider_done) {
|
||||
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();
|
||||
gtk_widget_add_css_class(row, "udpscope-compact");
|
||||
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"
|
||||
: plotted ? "the colour this signal is plotted in -- click to change it"
|
||||
: "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
|
||||
// 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
|
||||
@@ -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());
|
||||
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);
|
||||
// Locked: the switch decides what is acquired, so it is a configuration control and it goes with
|
||||
// the rest of them.
|
||||
if (!app.signals_locked) gtk_box_append(GTK_BOX(box), enable);
|
||||
gtk_box_append(GTK_BOX(box), enable);
|
||||
|
||||
GtkWidget* name = gtk_label_new(sig.name.c_str());
|
||||
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_widget_add_css_class(more, "flat");
|
||||
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
|
||||
// not for. The popover is left unattached, so nothing can reach it.
|
||||
if (!app.signals_locked) gtk_box_append(GTK_BOX(box), more);
|
||||
gtk_box_append(GTK_BOX(box), more);
|
||||
|
||||
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.
|
||||
@@ -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
|
||||
// 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.signals_locked));
|
||||
gtk_list_box_append(GTK_LIST_BOX(list), make_math_divider(app));
|
||||
}
|
||||
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;
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(list); c != nullptr;
|
||||
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
|
||||
// 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_edit = GTK_WIDGET(gtk_builder_get_object(ui, "signals_edit"));
|
||||
if (app.signals_edit != nullptr) {
|
||||
g_signal_connect(app.signals_edit, "clicked", G_CALLBACK(on_pane_edit_clicked), &app);
|
||||
}
|
||||
app.signals_empty = GTK_WIDGET(gtk_builder_get_object(ui, "signals_empty"));
|
||||
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
|
||||
// it, and `[udpscope] signals panel:` reads the difference.
|
||||
|
||||
+4
-3
@@ -458,9 +458,10 @@ constexpr TourStep kTourSteps[] = {
|
||||
"The dock beside the plot: the cursors, then the measurements. The buttons in the bar above "
|
||||
"show and hide it.",
|
||||
&App::side_box},
|
||||
{"signals", "signals", "The signals",
|
||||
"Every signal the source declares, each with the switch that acquires it. The + badge opens "
|
||||
"this list; a signal has to be switched on before it can be plotted, measured or searched.",
|
||||
{"signals", "signals", "Adding a signal",
|
||||
"What can be added: the signals the source declares that are not acquired yet, each with the "
|
||||
"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},
|
||||
{"status", nullptr, "The status line",
|
||||
"The stream's own report, once a second: the display mode, the rate arriving, the window on "
|
||||
|
||||
Reference in new issue
Block a user