diff --git a/src/ui.cpp b/src/ui.cpp index 701b09e..d8d8555 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -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(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(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 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(app.dbg_signal_plain + 1 + app.math.size()) + static_cast(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()); diff --git a/src/ui/signals-panel.ui b/src/ui/signals-panel.ui index 33c775a..3cd8090 100644 --- a/src/ui/signals-panel.ui +++ b/src/ui/signals-panel.ui @@ -2,11 +2,13 @@ + horizontal 8 - Signals + Add a signal start 1 - - - document-edit-symbolic - center - flat - - @@ -53,7 +45,7 @@ 6 - click a signal for its page; drag its badge onto a plot + tick a signal to acquire it; click one to open its page start 0 1 @@ -63,6 +55,17 @@ + + + + every signal is acquired + start + 0 + 1 + 0 + dim-label + + none diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 277728a..a9322ee 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -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& 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 listed_signals(const std::vector& 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& 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 diff --git a/src/ui_badges.cpp b/src/ui_badges.cpp index fbdffe5..485a84b 100644 --- a/src/ui_badges.cpp +++ b/src/ui_badges.cpp @@ -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, diff --git a/src/ui_context_help.cpp b/src/ui_context_help.cpp index 459725b..4ca19a9 100644 --- a/src/ui_context_help.cpp +++ b/src/ui_context_help.cpp @@ -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, " diff --git a/src/ui_layout.cpp b/src/ui_layout.cpp index db631af..e3c66d6 100644 --- a/src/ui_layout.cpp +++ b/src/ui_layout.cpp @@ -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); } diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index 72c0fcd..228ffdd 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -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(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 lock(app->model->mutex()); + const std::vector& 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 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, diff --git a/src/ui_signal_page.cpp b/src/ui_signal_page.cpp index 9e29707..4b09670 100644 --- a/src/ui_signal_page.cpp +++ b/src/ui_signal_page.cpp @@ -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; } diff --git a/src/ui_signals.cpp b/src/ui_signals.cpp index 5b7c392..ddbc2d6 100644 --- a/src/ui_signals.cpp +++ b/src/ui_signals.cpp @@ -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& 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 listed_signals(const std::vector& signals) { std::vector out; for (std::size_t i = 0; i < signals.size(); ++i) { @@ -310,12 +314,20 @@ void rebuild_signal_list(App& app, const std::vector& 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& 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& 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& 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& 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& 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. diff --git a/src/ui_tour.cpp b/src/ui_tour.cpp index 9085d63..d698849 100644 --- a/src/ui_tour.cpp +++ b/src/ui_tour.cpp @@ -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 "