ui: the Source badge, and the source panels as slide-over pages

This commit is contained in:
Martino Ferrari committed 2026-11-02 20:19:47 +01:00
1 parent 5fb479bd69
commit 31ff778e07
6 files changed
+101 -28

No files matched your search

+11 -2
View File
@@ -439,6 +439,15 @@ precedent, not an exception.
a slide-over page**, so a probe that reads one of its widgets' *allocation* (a width, a height) must
`slideover_open(*a, "trigger")` and read after the 120 ms reveal, as `exit_probe_step1` and `force_probe` do;
a sensitivity, a visibility or a value needs no open page.
**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
`[badges]` line for the Source badge: `[badges] source name=<kind> state=<connected|disconnected> pages=<n>
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
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
(`second_ok=1` when it is that page), `can_target=0` with `pages=0` is the UDPS badge being a read-out and
`open_marked=-1` is that no-page case (there is nothing to open).
- `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
synthetic sine with one panel), so it is the only way a run sees that kind; its value is the delay in ms.
@@ -461,8 +470,8 @@ precedent, not an exception.
- `UDPSCOPE_PROBE_E1608_GEN=1500` with `UDPSCOPE_EXIT_AFTER_MS=5500`, run as `--source e1608 --source-opt
address=127.0.0.1 --source-opt port=54711 --source-opt ao0_init=0 --source-opt gen_rate=100` against
`tools/mock_e1608.py --port 54711 --log /tmp/e1608-gen100.log` -- the generator's update rate: it starts a Sine on
DAC 0 through the Analog out panel (and switches the side stack to that page, since the panel refreshes on its
frame clock, which does not tick for a page that is off screen), waits 2.5 s, reads the achieved rate and the
DAC 0 through the Analog out panel (and opens the `source-ao` slide-over page, since the panel refreshes on its
frame clock, which does not tick for a page that is off screen; the page is closed again at the end), waits 2.5 s, reads the achieved rate and the
panel's "Update rate" row, stops the generator and reads both again 400 ms later. Two lines, in
`probe-readings.md`. Then count the mock log's `AOUT` lines: about 250 with `gen_rate=100` (the generator ran
for about 2.5 s) and about 125 for a run without the option (default 50 Hz). Short and serial, the mock killed
+3 -2
View File
@@ -5072,8 +5072,6 @@ void on_activate(GtkApplication* application, gpointer user) {
}
// The slide-over panel: an overlay child of the plot, above the GL area.
build_slideover(*app);
// The badge bar reads `trigger_toggle`, which `build_toolbar()` made above.
build_badge_bar(*app);
// The size the window opens at, stated in code as well as in the `.ui`: `default-width` there is the
// size it *asks* for, and a window whose content has a larger minimum opens at that minimum instead --
@@ -5222,6 +5220,9 @@ void on_activate(GtkApplication* application, gpointer user) {
// are hidden first and kept hidden for the pages this call adds.
keep_tabs_icon_free(*app);
build_source_panels(*app);
// The badge bar reads `trigger_toggle` (made by `build_toolbar()` above) and the source's pages, which
// `build_source_panels()` has just registered: the Source badge opens the first of them.
build_badge_bar(*app);
// The left side is the events pane *and* the plot, in one row: the pane is a column of its own
// (the item's "collapsable pane on the left"), revealed by trigger mode.
+7 -7
View File
@@ -109,11 +109,11 @@ struct TourView {
void* done_user = nullptr;
};
/// One capability panel a source kind contributed to the side stack. `content` is the kind's own widget
/// One capability panel a source kind contributed to the slide-over. `content` is the kind's own widget
/// (made insensitive while the source is disconnected); the page wrapping it is named `name`.
struct SourcePanelPage {
std::string name; ///< stack page name: "source-" + the kind's panel id
std::string title; ///< the pill's tooltip and accessible name; the page's own title stays empty
std::string name; ///< slide-over page id: "source-" + the kind's panel id
std::string title; ///< the slide-over page's title, and the segmented switch's label
GtkWidget* content = nullptr;
};
@@ -143,7 +143,7 @@ struct BadgeBar {
GtkWidget* signals_slot = nullptr; ///< left; empty in step 1, step 2 fills it
GtkWidget* system_slot = nullptr; ///< right: Trigger, Source
Badge trigger;
Badge source; ///< task 3
Badge source; ///< opens the first source page; a read-out when the source has none
};
struct App {
@@ -1606,8 +1606,8 @@ void save_layout_now(App& app);
void keep_tabs_icon_free(App& app);
/// Capability panels (`src/ui_source.cpp`). `build_source_panels` adds the source kind's panels as
/// pages of `side_stack`, once, before the first tick. `apply_source_panels` sets every panel's
/// sensitivity from `connected` -- disabled, never hidden: the tab switcher owns visibility.
/// pages of the slide-over (`source-<id>`), once, before the first tick. `apply_source_panels` sets every panel's
/// sensitivity from `connected` -- disabled, never hidden.
/// `sync_source_panels` is the per-tick call: it applies the model's own connection flag.
void build_source_panels(App& app);
void apply_source_panels(App& app, bool connected);
@@ -1925,7 +1925,7 @@ gboolean slideover_probe(gpointer user); // UDPSCOPE_PROBE_SLIDEOVER_MS
/// The badge bar (`src/ui_badges.cpp`): setup is reached by the object it configures.
/// The Trigger badge's state word. Pure, so the probe can assert the whole table.
const char* trigger_badge_state(bool enabled, bool armed, bool pending, bool has_event);
void build_badge_bar(App& app); // after build_slideover and after the toolbar (it reads trigger_toggle)
void build_badge_bar(App& app); // after build_slideover, the toolbar (it reads trigger_toggle) and build_source_panels (the Source badge's page)
void sync_badge_bar(App& app); // per tick and on every slide-over open/close; only writes what changed
gboolean badges_probe(gpointer user); // UDPSCOPE_PROBE_BADGES_MS
gboolean source_kinds_probe_report(gpointer user);
+70 -1
View File
@@ -3,8 +3,12 @@
// The badge bar under the plot: setup is reached by the object it configures.
#include "source/registry.hpp"
#include "ui_app.hpp"
#include <adwaita.h>
#include <algorithm>
#include <cstdio>
#include <cstring>
@@ -37,7 +41,15 @@ void on_badge_clicked(GtkButton*, gpointer user) {
Badge* b = static_cast<Badge*>(user);
// The App is reached through the badge's button: the badges live inside App, so the pointer is stable.
App* app = static_cast<App*>(g_object_get_data(G_OBJECT(b->button), "udpscope-app"));
if (app != nullptr) slideover_toggle(*app, b->page);
if (app == nullptr) return;
// The Source badge opens the first source page; with another source page already open, a click on it
// closes the panel rather than jumping back to the first page (the switch inside the panel moves between
// them).
if (b == &app->badges.source && slideover_page(*app).rfind("source-", 0) == 0) {
slideover_close(*app);
return;
}
slideover_toggle(*app, b->page);
}
/// A badge: a flat pill, name on the left, state dimmed beside it. Words only.
@@ -66,6 +78,14 @@ void build_badge_bar(App& app) {
bb.signals_slot = app.badge_signals;
bb.system_slot = app.badge_system;
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
// badge is a read-out: not a target, not focusable, but still on screen.
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);
if (first.empty()) {
gtk_widget_set_can_target(bb.source.button, FALSE);
gtk_widget_set_focusable(bb.source.button, FALSE);
}
sync_badge_bar(app);
}
@@ -79,6 +99,21 @@ void sync_badge_bar(App& app) {
const bool has_event = app.model->event() != nullptr;
set_text_if_changed(bb.trigger.state, trigger_badge_state(enabled, armed, pending, has_event));
set_open(bb.trigger.button, slideover_page(app) == bb.trigger.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.
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";
}
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);
}
gboolean badges_probe(gpointer user) {
@@ -113,6 +148,40 @@ gboolean badges_probe(gpointer user) {
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,
marked_closed ? 1 : 0);
// The Source badge: its read-out, and the segmented switch of a source with several panels.
const std::string first = a->badges.source.page;
bool switch_visible = false;
bool second_ok = true;
std::string second_page;
if (!first.empty()) {
slideover_open(*a, first);
switch_visible = gtk_widget_get_visible(a->slideover.source_switch) != FALSE;
if (a->slideover.source_pages.size() >= 2) {
adw_toggle_group_set_active_name(ADW_TOGGLE_GROUP(a->slideover.source_switch),
a->slideover.source_pages[1].c_str());
second_page = slideover_page(*a);
second_ok = second_page == a->slideover.source_pages[1];
}
}
const bool marked = gtk_widget_has_css_class(a->badges.source.button, "udpscope-badge-open") != FALSE;
// The widest minimum width of any source panel: the slide-over's scroller never scrolls horizontally, so
// a panel whose minimum exceeds the page (360 px less the scroller's own chrome) would be clipped.
int widest = 0;
for (const SourcePanelPage& p : a->source_panels) {
int min_w = 0;
gtk_widget_measure(p.content, GTK_ORIENTATION_HORIZONTAL, -1, &min_w, nullptr, nullptr, nullptr);
widest = std::max(widest, min_w);
}
slideover_close(*a);
std::fprintf(stderr,
"[badges] source name=%s state=%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(),
switch_visible ? 1 : 0, gtk_widget_get_can_target(a->badges.source.button) ? 1 : 0,
second_page.empty() ? "-" : second_page.c_str(), second_ok ? 1 : 0,
first.empty() ? -1 : marked ? 1 : 0,
gtk_widget_has_css_class(a->badges.source.button, "udpscope-badge-open") ? 0 : 1, widest);
return G_SOURCE_REMOVE;
}
+6 -7
View File
@@ -1957,9 +1957,7 @@ gboolean source_kinds_probe(gpointer user) {
const SourcePanelPage* page = a->source_panels.empty() ? nullptr : &a->source_panels.front();
const auto report = [&](const char* state) {
const bool present =
page != nullptr && a->side_stack != nullptr &&
adw_view_stack_get_child_by_name(ADW_VIEW_STACK(a->side_stack), page->name.c_str()) !=
nullptr;
page != nullptr && slideover_has_page(*a, page->name);
std::fprintf(stderr, "[source-kinds] panel %s: panels=%zu present=%d sensitive=%d\n", state,
a->source_panels.size(), present ? 1 : 0,
page != nullptr && gtk_widget_get_sensitive(page->content) ? 1 : 0);
@@ -2399,8 +2397,7 @@ gboolean e1608_probe(gpointer user) {
const std::string page = std::string("source-") + id;
GtkWidget* content = source_panel_content(a, page.c_str());
const bool present =
content != nullptr && a->side_stack != nullptr &&
adw_view_stack_get_child_by_name(ADW_VIEW_STACK(a->side_stack), page.c_str()) != nullptr;
content != nullptr && slideover_has_page(*a, page);
std::fprintf(stderr, "[e1608] panel %s: present=%d sensitive=%d connected=%d\n", id,
present ? 1 : 0, content != nullptr && gtk_widget_get_sensitive(content) ? 1 : 0,
up ? 1 : 0);
@@ -2445,6 +2442,7 @@ gboolean e1608_gen_probe_stopped(gpointer user) {
std::fprintf(stderr, "[e1608] generator stopped: achieved=%.1f panel='%s' ok=%d\n", hz,
sub != nullptr ? sub : "", hz == 0.0 && off ? 1 : 0);
std::fflush(stderr);
slideover_close(*a);
return G_SOURCE_REMOVE;
}
@@ -2493,8 +2491,9 @@ gboolean e1608_gen_probe(gpointer user) {
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
// The panel refreshes on its frame clock, which only ticks while the page is on screen.
if (a->side_stack != nullptr) adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(a->side_stack), "source-ao");
// The panel refreshes on its frame clock, which only ticks while the page is on screen; the page stays
// open for the probe's reads and `e1608_gen_probe_stopped` closes it.
slideover_open(*a, "source-ao");
adw_combo_row_set_selected(ADW_COMBO_ROW(mode), 1); // Sine
adw_switch_row_set_active(ADW_SWITCH_ROW(run), TRUE);
g_timeout_add(2500, e1608_gen_probe_report, a);
+4 -9
View File
@@ -54,7 +54,7 @@ void keep_tabs_icon_free(App& app) {
}
void build_source_panels(App& app) {
if (app.side_stack == nullptr || app.acq == nullptr || app.acq->source() == nullptr) return;
if (app.slideover.stack == nullptr || app.acq == nullptr || app.acq->source() == nullptr) return;
SourceKind* kind = find_kind(app.source_kind);
if (kind == nullptr) return;
for (const SourcePanel& p : kind->make_panels(*app.acq->source())) {
@@ -63,15 +63,10 @@ void build_source_panels(App& app) {
p.widget == nullptr ? "no widget" : "an empty id");
continue;
}
// Each page scrolls on its own, like Measure and Signals: panels differ in height.
GtkWidget* scroller = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller), GTK_POLICY_NEVER,
GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroller), p.widget);
const std::string name = "source-" + p.id;
// The tab is its title: the sidebar's pills are words, not glyphs.
adw_view_stack_add_titled(ADW_VIEW_STACK(app.side_stack), scroller, name.c_str(),
p.title.c_str());
// The panel is a slide-over page now: the slide-over wraps it in its own scroller, and with two or
// more source pages it draws the segmented switch (the E-1608: Analog out, Digital I/O).
if (!slideover_add_page(app, name, p.title, p.widget)) continue;
app.source_panels.push_back({name, p.title, p.widget});
}
// Disconnected until the first Connected event says otherwise.