badges: hide the Source badge when the source has no panel of its own

A Source pill that opens nothing (UDPS, Flat UDP, files) was noise. It is now shown only while the slide-over has a source page; the [badges] source line gains visible=.

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:38:12 +01:00
1 parent d8f82bd610
commit 616cde5b13
8 files changed
+41 -24

No files matched your search

+4 -3
View File
@@ -118,10 +118,11 @@ place (it opens the list, and `+` is hidden): the E-1608's 8 analog inputs and c
otherwise. The signal badges sit in a scroller, so the bar's minimum width, and with it the window's, otherwise. The signal badges sit in a scroller, so the bar's minimum width, and with it the window's,
never depends on how many signals there are. The badge order is kept **for the session only**; it is not never depends on how many signals there are. The badge order is kept **for the session only**; it is not
written to the session file. The right-hand end carries two words-only badges: **Trigger** (`off`, `armed`, `capturing`, `triggered` written to the session file. The right-hand end carries two words-only badges: **Trigger** (`off`, `armed`, `capturing`, `triggered`
or `disarmed`) and **Source** (the kind's name only -- the link state is the status bar's; a read-out, not a button, or `disarmed`) and **Source** (the kind's name only -- the link state is the status bar's). **Source is shown only when the source
for a source with no controls of its own, and for a file). A badge opens the **slide-over**: a 360 px has panels of its own** (the E-1608's); for UDPS, Flat UDP and a file there is nothing for it to open, so it is hidden
(the bar keeps its height; the kind's name is in the status bar). A badge opens the **slide-over**: a 360 px
panel laid over the plot's right edge, opaque, with its own title, a *Close* button and one scroller per panel laid over the plot's right edge, opaque, with its own title, a *Close* button and one scroller per
page. The Trigger badge opens the trigger form; the Source badge opens the source kind's own panels page. The Trigger badge opens the trigger form; the Source badge (when shown) opens the source kind's own panels
(the E-1608's *Analog out* and *Digital I/O*, behind a segmented switch at the top of the panel). It (the E-1608's *Analog out* and *Digital I/O*, behind a segmented switch at the top of the panel). It
closes with a second click on its badge, with `Esc`, with *Close*, or with a click on the plot; it never closes with a second click on its badge, with `Esc`, with *Close*, or with a click on the plot; it never
resizes the plot. The right-hand **dock** (the existing sidebar, 320 px) holds what is left of the old resizes the plot. The right-hand **dock** (the existing sidebar, 320 px) holds what is left of the old
+3 -1
View File
@@ -113,7 +113,9 @@ touching the data path.
**Source** badges). `sync_signal_badges()` rebuilds the signal badges (from `rebuild_signal_panels()`); **Source** badges). `sync_signal_badges()` rebuilds the signal badges (from `rebuild_signal_panels()`);
their order is `App::badge_order`, in memory only. A badge is a flat button whose `page` is the slide-over page it their order is `App::badge_order`, in memory only. A badge is a flat button whose `page` is the slide-over page it
toggles; the Source badge's page is the first `source-*` page, and a kind with none (UDPS, Flat UDP, a toggles; the Source badge's page is the first `source-*` page, and a kind with none (UDPS, Flat UDP, a
file) leaves it a read-out with `can_target` off. file) leaves it **hidden** (the user asked for hide here, not the usual disable): `sync_badge_bar()` sets its visibility,
pick and focus from `slideover.source_pages` being non-empty every tick, so anything that indexes the badge
(the tour's `badges` step targets the whole bar, `[badges]`) must tolerate it being absent.
- **The dock** is the `AdwOverlaySplitView` sidebar (`side_box` > `dock_scroller` > `dock_page`): the - **The dock** is the `AdwOverlaySplitView` sidebar (`side_box` > `dock_scroller` > `dock_page`): the
cursors and the measurements only, in one scroller (the signal list is the `signals` page). It holds no tab switcher and no cursors and the measurements only, in one scroller (the signal list is the `signals` page). It holds no tab switcher and no
revealer, and it does not hold a source kind's panels (those are slide-over pages). revealer, and it does not hold a source kind's panels (those are slide-over pages).
+3 -3
View File
@@ -117,7 +117,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
(`height-request`) between `plot_overlay` and the status line, so the plot above it never moves when a (`height-request`) between `plot_overlay` and the status line, so the plot above it never moves when a
badge's text changes (`bar_h` = `bar_h_after` in `[badges]`). This is "layout must not depend on badge's text changes (`bar_h` = `bar_h_after` in `[badges]`). This is "layout must not depend on
content" for the new row: the Trigger badge's state is a word from a short fixed set and the Source badge content" for the new row: the Trigger badge's state is a word from a short fixed set and the Source badge
is a kind's display name only (no connection state: that is the status bar's), so what changes with the stream is the row's *width* (the bar has room to (shown only when the kind has source pages; hidden otherwise) is a kind's display name only (no connection state: that is the status bar's), so what changes with the stream is the row's *width* (the bar has room to
spare on the right) and never its height. The badge labels are not ellipsized: a name long enough to spare on the right) and never its height. The badge labels are not ellipsized: a name long enough to
need it would be a defect of the kind's `display_name()`. Rows that appear by trigger condition appear need it would be a defect of the kind's `display_name()`. Rows that appear by trigger condition appear
inside the slide-over's scroller, never in the bar. inside the slide-over's scroller, never in the bar.
@@ -127,8 +127,8 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
(the E-1608: eight inputs and the counter) would push the window's minimum width past a laptop screen. (the E-1608: eight inputs and the counter) would push the window's minimum width past a laptop screen.
Past `kMaxSignalBadges` (6) the row is replaced by one "N signals" badge, which also stands for the `+`. Past `kMaxSignalBadges` (6) the row is replaced by one "N signals" badge, which also stands for the `+`.
The bar's height stays 36 px in every one of these states (`bar_h_fixed` in `[sigbadges]`), and The bar's height stays 36 px in every one of these states (`bar_h_fixed` in `[sigbadges]`), and
`bar_min_w` reads the same with one signal, nine signals and the overflow (244 on the E-1608, 251 on UDPS: `bar_min_w` reads the same with one signal, nine signals and the overflow (244 on the E-1608, 124 on UDPS:
the Source badge of each kind reads differently). A change that makes it grow with the signal count has the Source badge is on screen for the E-1608 and hidden for UDPS, and its text differs by kind). A change that makes it grow with the signal count has
taken the scroller out of the measurement. taken the scroller out of the measurement.
- **The dock's width is fixed too** (`side_column` `width-request` 300, the split view's 320 px floor - **The dock's width is fixed too** (`side_column` `width-request` 300, the split view's 320 px floor
and cap), and it holds one scroller with the cursors and the measurements. A and cap), and it holds one scroller with the cursors and the measurements. A
+6 -5
View File
@@ -2663,14 +2663,14 @@ signals list while the title and the badge marker still name `trigger`; `-1` whe
## `[sigbadges]` ## `[sigbadges]`
``` ```
[sigbadges] enabled=4 badges=4 overflow=0 overflow_text=- plus_visible=1 dot_plotted=4 bar_h=36 bar_h_fixed=1 bar_min_w=251 opens=1 plus_opens=1 [sigbadges] enabled=4 badges=4 overflow=0 overflow_text=- plus_visible=1 dot_plotted=4 bar_h=36 bar_h_fixed=1 bar_min_w=124 opens=1 plus_opens=1
``` ```
`UDPSCOPE_PROBE_SIGNAL_BADGES_MS` reads the signal badges. `badges` must equal `enabled` (the enabled, listed signals) while `enabled` is at most 6 `UDPSCOPE_PROBE_SIGNAL_BADGES_MS` reads the signal badges. `badges` must equal `enabled` (the enabled, listed signals) while `enabled` is at most 6
(`kMaxSignalBadges`); beyond that `badges=0 overflow=1 overflow_text=<N> signals plus_visible=0` (the "N signals" badge replaces the badges and the "+"). `dot_plotted` is the number of (`kMaxSignalBadges`); beyond that `badges=0 overflow=1 overflow_text=<N> signals plus_visible=0` (the "N signals" badge replaces the badges and the "+"). `dot_plotted` is the number of
badges whose signal is on a plot (its dot is then the trace colour, not the unplotted grey). `bar_h` is the bar's allocated height and `bar_h_fixed=1` that neither it nor its natural badges whose signal is on a plot (its dot is then the trace colour, not the unplotted grey). `bar_h` is the bar's allocated height and `bar_h_fixed=1` that neither it nor its natural
height moved while one signal was switched off and on again (`rebuild_signal_panels()` each time), and that the badge count followed; `0` is a bar whose height depends on its content. height moved while one signal was switched off and on again (`rebuild_signal_panels()` each time), and that the badge count followed; `0` is a bar whose height depends on its content.
`bar_min_w` is only a record: the scroller must keep it the same however many signals there are (it reads 251 with the UDPS mock's 4 and with the overflow forced on, and 244 against the E-1608 mock with one signal and with nine -- the overflow, `enabled=9 badges=0 overflow=1 overflow_text=9 signals plus_visible=0 bar_h=36 bar_h_fixed=1`; a value that grows with the signal count is a scroller that has stopped constraining the bar). `opens=1`: clicking the `bar_min_w` is only a record: the scroller must keep it the same however many signals there are (it reads 124 with the UDPS mock's 3 (the Source badge being hidden there) and with the overflow forced on, and 244 against the E-1608 mock with one signal and with nine -- the overflow, `enabled=9 badges=0 overflow=1 overflow_text=9 signals plus_visible=0 bar_h=36 bar_h_fixed=1`; a value that grows with the signal count is a scroller that has stopped constraining the bar). `opens=1`: clicking the
first signal badge opens `signal:<name>` with the badge marked open and a second click closes it (`-1` when overflowed, there being no signal badges); `plus_opens=1` the same for the "+" first signal badge opens `signal:<name>` with the badge marked open and a second click closes it (`-1` when overflowed, there being no signal badges); `plus_opens=1` the same for the "+"
(the overflow badge when overflowed) and the `signals` page. (the overflow badge when overflowed) and the `signals` page.
@@ -2691,7 +2691,7 @@ gesture's threshold, the slide-over closing under it and the pointer-driven drop
``` ```
[badges] table_bad=0 trigger_state=off bar_h=36 bar_h_after=36 fixed=1 open_marked=1 page_open=1 closed_unmarked=1 [badges] table_bad=0 trigger_state=off bar_h=36 bar_h_after=36 fixed=1 open_marked=1 page_open=1 closed_unmarked=1
[badges] source name=UDPStreamer state=connected pages=0 switch_visible=0 can_target=0 second_page=- second_ok=-1 open_marked=-1 closed_unmarked=1 panel_min_w=0 [badges] source name=UDPStreamer pages=0 visible=0 switch_visible=0 can_target=0 second_page=- second_ok=-1 open_marked=-1 closed_unmarked=1 panel_min_w=0
``` ```
`UDPSCOPE_PROBE_BADGES_MS` reads the badge bar under the plot. **Line 1** is the Trigger badge: `table_bad=0` (the rows of the 16-input `UDPSCOPE_PROBE_BADGES_MS` reads the badge bar under the plot. **Line 1** is the Trigger badge: `table_bad=0` (the rows of the 16-input
@@ -2700,12 +2700,13 @@ the same, and at least 36 px, before and after the `trigger` page was opened and
moves), `open_marked=1` / `page_open=1` / `closed_unmarked=1` (the `udpscope-badge-open` class follows the slide-over's page, both ways; `0` on moves), `open_marked=1` / `page_open=1` / `closed_unmarked=1` (the `udpscope-badge-open` class follows the slide-over's page, both ways; `0` on
any is a badge that does not say its panel is open, or one that stays marked after it closed). any is a badge that does not say its panel is open, or one that stays marked after it closed).
**Line 2** is the Source badge, and every field has a value it must read: **Line 2** is the Source badge, which is **hidden when the source has no pages** (UDPS, Flat UDP, a file), and every field has a value it must read:
- `name=` the source kind's name, as the badge shows it. The badge carries no link state (the status bar says it); there is no `state=` field. - `name=` the source kind's name, as the badge shows it. The badge carries no link state (the status bar says it); there is no `state=` field.
- `pages=` the number of slide-over pages the source kind contributes (`source-<id>`): `0` for UDPS, `2` for the E-1608 (`source-ao`, `source-dio`). - `pages=` the number of slide-over pages the source kind contributes (`source-<id>`): `0` for UDPS, `2` for the E-1608 (`source-ao`, `source-dio`).
`can_target=0` with `pages=0` is the UDPS badge being a read-out rather than a button; `can_target=1` with `pages>0` is the button being live. `can_target=0` with `pages=0` is the UDPS badge being untargetable, which goes with it being hidden; `can_target=1` with `pages>0` is the button being live.
`can_target=1` with `pages=0` is a dead button that opens nothing. `can_target=1` with `pages=0` is a dead button that opens nothing.
- `visible=` the badge being on screen: **`0` with `pages=0`** (hidden, by design: nothing to open), `1` with `pages>0`. `visible=1` with `pages=0` is a dead pill that was supposed to be hidden; `visible=0` with pages is a source panel nothing can open.
- `switch_visible=` the segmented switch being on screen after the first source page was opened: `1` with two or more pages, `0` with one or none. - `switch_visible=` the segmented switch being on screen after the first source page was opened: `1` with two or more pages, `0` with one or none.
A switch shown for a single page is a control that cannot change anything; hidden with two, the second panel is unreachable. A switch shown for a single page is a control that cannot change anything; hidden with two, the second panel is unreachable.
- `second_page=` / `second_ok=` what `slideover_page()` read after the switch selected the second page, and whether it is that page. **`second_ok=1` with - `second_page=` / `second_ok=` what `slideover_page()` read after the switch selected the second page, and whether it is that page. **`second_ok=1` with
+4 -3
View File
@@ -473,12 +473,13 @@ precedent, not an exception.
**The source kind's panels are slide-over pages too** (`source-<id>`: the E-1608's `source-ao` and **The source kind's panels are slide-over pages too** (`source-<id>`: the E-1608's `source-ao` and
`source-dio`, Flat UDP's, the probe kind's), so the same rule holds for them. The probe appends a second `source-dio`, Flat UDP's, the probe kind's), so the same rule holds for them. The probe appends a second
`[badges]` line for the Source badge: `[badges] source name=<kind> pages=<n> `[badges]` line for the Source badge: `[badges] source name=<kind> pages=<n>
switch_visible=<0|1> can_target=<0|1> second_page=<id|-> second_ok=<0|1> open_marked=<0|1|-1> visible=<0|1> switch_visible=<0|1> can_target=<0|1> second_page=<id|-> second_ok=<0|1> open_marked=<0|1|-1>
closed_unmarked=1`. `pages` is the number of source pages, `switch_visible` is the segmented switch closed_unmarked=1`. `pages` is the number of source pages, `switch_visible` is the segmented switch
being on screen after the first source page was opened (`1` for the E-1608's two pages, `0` for one page being on screen after the first source page was opened (`1` for the E-1608's two pages, `0` for one page
or none), `second_page` is what `slideover_page()` read after the switch selected the second page or none), `second_page` is what `slideover_page()` read after the switch selected the second page
(`second_ok=1` when it is that page), `can_target=0` with `pages=0` is the UDPS badge being a read-out and (`second_ok=1` when it is that page), `visible=0` with `pages=0` is the UDPS badge being **hidden** (a kind with no source page
`open_marked=-1` is that no-page case (there is nothing to open). has nothing for it to open; `can_target=0` goes with it) and `open_marked=-1` is that no-page case (there is
nothing to open); `visible=1` with `pages>0`.
- `UDPSCOPE_PROBE_SOURCE_KINDS=2000` with `UDPSCOPE_EXIT_AFTER_MS=5000`, run as `--source probe --source-opt rate=500` - `UDPSCOPE_PROBE_SOURCE_KINDS=2000` with `UDPSCOPE_EXIT_AFTER_MS=5000`, run as `--source probe --source-opt rate=500`
-- the source-plugin path end to end. The variable's presence also registers the test-only `probe` kind (a -- the source-plugin path end to end. The variable's presence also registers the test-only `probe` kind (a
synthetic sine with one panel), so it is the only way a run sees that kind; its value is the delay in ms. synthetic sine with one panel), so it is the only way a run sees that kind; its value is the delay in ms.
+1 -1
View File
@@ -160,7 +160,7 @@ struct BadgeBar {
GtkWidget* signals_slot = nullptr; ///< left; empty in step 1, step 2 fills it GtkWidget* signals_slot = nullptr; ///< left; empty in step 1, step 2 fills it
GtkWidget* system_slot = nullptr; ///< right: Trigger, Source GtkWidget* system_slot = nullptr; ///< right: Trigger, Source
Badge trigger; Badge trigger;
Badge source; ///< opens the first source page; a read-out when the source has none Badge source; ///< opens the first source page; hidden when the source has none
GtkWidget* signals_row = nullptr; ///< the box inside the scroller: signal badges, then the "+" GtkWidget* signals_row = nullptr; ///< the box inside the scroller: signal badges, then the "+"
/// One per shown enabled signal; empty when overflowed. `unique_ptr`: each button's "clicked" handler holds /// One per shown enabled signal; empty when overflowed. `unique_ptr`: each button's "clicked" handler holds
/// a pointer to its `Badge`, which must not move when the vector does. /// a pointer to its `Badge`, which must not move when the vector does.
+17 -5
View File
@@ -173,13 +173,16 @@ void build_badge_bar(App& app) {
bb.signals_slot = app.badge_signals; bb.signals_slot = app.badge_signals;
bb.system_slot = app.badge_system; bb.system_slot = app.badge_system;
make_badge(app, bb.trigger, bb.system_slot, "Trigger", "trigger"); make_badge(app, bb.trigger, bb.system_slot, "Trigger", "trigger");
// The first source page is where the badge goes; a source with none (UDPS) has nothing to open, so its // The first source page is where the badge goes; a source with none (UDPS, Flat UDP, a file) has nothing to
// badge is a read-out: not a target, not focusable, but still on screen. // open, so its badge is hidden (`sync_badge_bar` keeps it in step with the slide-over's source pages) and, for
// as long as it is, neither a target nor focusable. Hidden, not insensitive: a dead "Source" pill that does
// nothing was only noise. The bar's height is fixed, so the row loses width and never height.
const std::string first = app.source_panels.empty() ? std::string() : app.source_panels.front().name; const std::string first = app.source_panels.empty() ? std::string() : app.source_panels.front().name;
make_badge(app, bb.source, bb.system_slot, "Source", first); make_badge(app, bb.source, bb.system_slot, "Source", first);
if (first.empty()) { if (first.empty()) {
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);
gtk_widget_set_visible(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 // 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. // state label is hidden, as the "+" badge's is, rather than leaving a gap in the pill.
@@ -278,6 +281,14 @@ 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;
// Shown only while there is a source page to open. The slide-over's page list is the truth, so a page that is
// registered later brings the badge back, and its focus and pick with it.
const bool has_source_pages = !app.slideover.source_pages.empty();
if ((gtk_widget_get_visible(bb.source.button) != FALSE) != has_source_pages) {
gtk_widget_set_visible(bb.source.button, has_source_pages);
gtk_widget_set_can_target(bb.source.button, has_source_pages);
gtk_widget_set_focusable(bb.source.button, has_source_pages);
}
// The source's name; the link state is the status bar's. A file has no 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";
@@ -322,7 +333,8 @@ gboolean badges_probe(gpointer user) {
bad, gtk_label_get_text(GTK_LABEL(a->badges.trigger.state)), h_closed, h_after, bad, gtk_label_get_text(GTK_LABEL(a->badges.trigger.state)), h_closed, h_after,
h_closed == h_after && h_closed >= 36 ? 1 : 0, marked_open ? 1 : 0, page_open ? 1 : 0, h_closed == h_after && h_closed >= 36 ? 1 : 0, marked_open ? 1 : 0, page_open ? 1 : 0,
marked_closed ? 1 : 0); marked_closed ? 1 : 0);
// The Source badge: its read-out, and the segmented switch of a source with several panels. // The Source badge: shown only with a source page (`visible=`; hidden, the page-dependent fields read -1 or 0),
// and the segmented switch of a source with several panels.
const std::string first = a->badges.source.page; const std::string first = a->badges.source.page;
bool switch_visible = false; bool switch_visible = false;
int second_ok = -1; // -1: there is no second page to select (one source page, or none) int second_ok = -1; // -1: there is no second page to select (one source page, or none)
@@ -348,11 +360,11 @@ gboolean badges_probe(gpointer user) {
} }
slideover_close(*a); slideover_close(*a);
std::fprintf(stderr, std::fprintf(stderr,
"[badges] source name=%s pages=%zu switch_visible=%d can_target=%d second_page=%s " "[badges] source name=%s pages=%zu visible=%d 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)),
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, gtk_widget_get_visible(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,
gtk_widget_has_css_class(a->badges.source.button, "udpscope-badge-open") ? 0 : 1, widest); gtk_widget_has_css_class(a->badges.source.button, "udpscope-badge-open") ? 0 : 1, widest);
+3 -3
View File
@@ -446,9 +446,9 @@ constexpr TourStep kTourSteps[] = {
&App::area_overlay}, &App::area_overlay},
{"badges", nullptr, "The badges", {"badges", nullptr, "The badges",
"Setup is reached by the thing it configures. A badge per signal opens that signal's colour, " "Setup is reached by the thing it configures. A badge per signal opens that signal's colour, "
"calibration and plot; the + opens the full list; Trigger and Source open their own forms. Drag a " "calibration and plot; the + opens the full list; Trigger opens its form, and so does Source when "
"signal badge onto a plot to draw it there, or onto another badge to reorder. Click again, press " "the source has controls of its own. Drag a signal badge onto a plot to draw it there, or onto "
"Esc or click the plot to close the panel.", "another badge to reorder. Click again, press Esc or click the plot to close the panel.",
&App::badge_bar}, &App::badge_bar},
{"trigger", "trigger", "The trigger", {"trigger", "trigger", "The trigger",
"Describe an event -- a signal, a condition, a level and the window around it -- and arm it: " "Describe an event -- a signal, a condition, a level and the window around it -- and arm it: "