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

+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.