ui: the signal list is the signals slide-over page, opened by a + badge or g

This commit is contained in:
Martino Ferrari committed 2026-11-03 00:54:25 +01:00
1 parent e36f2a202b
commit 0dd235e41c
11 files changed
+98 -75

No files matched your search

+11 -16
View File
@@ -2864,19 +2864,13 @@ void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer
open_row_menu(*app, row, x, y, widget);
}
/// Scrolls the dock to a section. The dock is one scroller (`App::dock_scroller`) holding the signal list,
/// the cursors and the measurements top to bottom, so "measure" is the cursors panel's offset in the
/// page, and "signals" the top. Nothing is hidden or removed: both stay in the page. An unknown name is a
/// no-op, so a key bound to a section a later version drops cannot blank anything.
/// Scrolls the dock to a section. The dock is one scroller (`App::dock_scroller`) holding the cursors and
/// the measurements top to bottom, so "measure" is the cursors panel's offset in the page. Nothing is
/// hidden or removed: both stay in the page. An unknown name (the signal list is a slide-over page now,
/// not a dock section) is a no-op, so a key bound to a section a later version drops cannot blank anything.
void dock_show_section(App& app, const char* section) {
if (app.dock_scroller == nullptr || app.dock_page == nullptr || section == nullptr) return;
GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(app.dock_scroller));
if (std::strcmp(section, "signals") == 0) {
// The list is the first section: its own offset in the page is the page's margin, and "go to Signals"
// means the top of the dock.
gtk_adjustment_set_value(adj, 0.0);
return;
}
if (std::strcmp(section, "measure") != 0 || app.cursors_panel == nullptr) return;
graphene_point_t zero = GRAPHENE_POINT_INIT(0.f, 0.f);
graphene_point_t at;
@@ -4679,7 +4673,7 @@ void run_app_action(App& app, AppAction action) {
toggle(app.cursors_toggle);
break;
case AppAction::ShowMeasure: dock_show_section(app, "measure"); break;
case AppAction::ShowSignals: dock_show_section(app, "signals"); break;
case AppAction::ShowSignals: slideover_toggle(app, "signals"); break;
case AppAction::LayoutEditor:
toggle(app.layout_toggle);
break;
@@ -5153,9 +5147,9 @@ void on_activate(GtkApplication* application, gpointer user) {
gtk_popover_set_has_arrow(GTK_POPOVER(app->status_popover), TRUE);
}
// The right-hand side is the dock: one scroller, the signal list first (it is what is dragged onto the
// plot), then the cursors and the measurements. Every section is always there -- "go to Measure" scrolls
// to it instead of switching a tab, so the measurements are as live as the list above them.
// The right-hand side is the dock: one scroller, the cursors then the measurements. Every section is
// always there -- "go to Measure" scrolls to it instead of switching a tab, so the measurements are as
// live as the cursors above them. The signal list is a slide-over page (reached by the `+` badge).
// `side` and `dock_page`/`dock_scroller` are resolved above with the rest of the skeleton and checked
// by the null guard; assign the member here.
app->side_box = side;
@@ -5169,9 +5163,10 @@ void on_activate(GtkApplication* application, gpointer user) {
// would spread its rows (or draw its frame) over empty space under the last one.
gtk_widget_set_valign(app->trigger_panel, GTK_ALIGN_START);
slideover_add_page(*app, "trigger", "Trigger", app->trigger_panel);
// The signal list is the `signals` page, reached by the `+` badge or `g`; the panel is built here, after
// `build_slideover`, and registered once.
app->signal_panel = build_signal_panel(*app);
gtk_box_append(GTK_BOX(app->dock_page), app->signal_panel);
gtk_box_append(GTK_BOX(app->dock_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
slideover_add_page(*app, "signals", "Signals", app->signal_panel);
gtk_box_append(GTK_BOX(app->dock_page), app->cursors_panel);
gtk_box_append(GTK_BOX(app->dock_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
gtk_box_append(GTK_BOX(app->dock_page), measures_panel);
+3 -2
View File
@@ -156,6 +156,7 @@ struct BadgeBar {
GtkWidget* system_slot = nullptr; ///< right: Trigger, Source
Badge trigger;
Badge source; ///< opens the first source page; a read-out when the source has none
Badge plus; ///< the "+" in the signals slot: opens the `signals` page
};
struct App {
@@ -253,8 +254,8 @@ struct App {
GtkWidget* trigger_panel = nullptr;
GtkWidget* signal_panel = nullptr;
GtkWidget* side_box = nullptr;
/// The dock: the sidebar's one scroller and the box inside it, which holds the signal list, the
/// cursors and the measurements (`dock_show_section()` scrolls it). A feature's own on/off never
/// The dock: the sidebar's one scroller and the box inside it, which holds the cursors and
/// the measurements (`dock_show_section()` scrolls it). A feature's own on/off never
/// removes a section (it sets its panel's sensitivity instead).
GtkWidget* dock_page = nullptr;
GtkWidget* dock_scroller = nullptr;
+7
View File
@@ -86,6 +86,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 full signal list. Insensitive until the stream has declared signals ("disable, do not hide").
make_badge(app, bb.plus, bb.signals_slot, "+", "signals");
gtk_widget_set_tooltip_text(bb.plus.button, "all signals: switch more on, add a math channel");
sync_badge_bar(app);
}
@@ -100,6 +103,10 @@ void sync_badge_bar(App& app) {
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.plus.button != nullptr) {
gtk_widget_set_sensitive(bb.plus.button, !app.model->signals().empty());
set_open(bb.plus.button, slideover_page(app) == "signals");
}
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();
+3 -3
View File
@@ -92,7 +92,7 @@ constexpr HelpHint kHelpHints[] = {
{"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 "
"measurement editor. A signal that is dragged onto a plot is drawn there.",
"measurement editor. A signal's badge, dragged onto a plot, draws it there.",
"G", &App::signals_head},
{"events", "The event history",
"Every triggered acquisition the cache keeps, with a picture of each. Open one to read it as a "
@@ -117,8 +117,8 @@ constexpr HelpHint kHelpHints[] = {
"", &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 dragged "
"onto. A signal nothing measures can be switched off to stop acquiring it.",
"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.",
"", &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, "
+42 -24
View File
@@ -93,17 +93,14 @@ int on_render_probe(GtkGLArea*, GdkGLContext*, gpointer user) {
return FALSE; // same reason the adapter does: never stop the emission
}
gboolean exit_probe_step3(gpointer user) {
gboolean exit_probe_step3b(gpointer user) {
App* a = static_cast<App*>(user);
a->dbg_mode_live = a->status_detail;
GtkWidget* window = a->window;
a->window = nullptr; // stop the tick before the widgets go
a->dbg_side_w = gtk_widget_get_width(a->side_box);
a->dbg_side_h = gtk_widget_get_height(a->side_box);
// The signal list is the `signals` page, opened by step 3 300 ms ago: its allocation is only real once
// the page is open and the reveal has run.
a->dbg_signal_w = gtk_widget_get_width(a->signal_panel);
a->dbg_signal_h = gtk_widget_get_height(a->signal_panel);
a->dbg_trigger_w = gtk_widget_get_width(a->trigger_panel);
a->dbg_trigger_h = gtk_widget_get_height(a->trigger_panel);
// How many rows of the signal list carry a drag source: dragging a signal onto a plot is how a plot is
// filled, so it has to survive both pane modes -- the locked list drops the tick, the menu and the `+`,
// and this is the reading that says it did not drop the drag with them. One row is the `MATH` divider,
@@ -211,11 +208,25 @@ gboolean exit_probe_step3(gpointer user) {
}
// `dbg_window_text` is kept by `sync_window_display()`, the only place that knows the span the
// window control is showing: there is no label to read it back from any more.
slideover_close(*a); // opened by step 1 for the allocations read above
slideover_close(*a); // opened by step 3 for the signal list's allocations read above
if (window != nullptr) gtk_window_destroy(GTK_WINDOW(window));
return G_SOURCE_REMOVE;
}
/// Reads the sizes that need the `trigger` page open (step 1 opened it), then swaps it for the `signals`
/// page -- the slide-over holds one page -- and leaves step 3b the 300 ms the reveal needs.
gboolean exit_probe_step3(gpointer user) {
App* a = static_cast<App*>(user);
a->dbg_mode_live = a->status_detail;
a->dbg_side_w = gtk_widget_get_width(a->side_box);
a->dbg_side_h = gtk_widget_get_height(a->side_box);
a->dbg_trigger_w = gtk_widget_get_width(a->trigger_panel);
a->dbg_trigger_h = gtk_widget_get_height(a->trigger_panel);
slideover_open(*a, "signals");
g_timeout_add(300, exit_probe_step3b, a);
return G_SOURCE_REMOVE;
}
/// The frame after the pane was locked: read the compact arm control's *allocation*, then leave the
/// configuration mode and trigger mode as step 2 used to, and hand the chain back its 400 ms. The button
/// is created hidden and this is the first tick it is shown, so its width in the same tick would always
@@ -1794,15 +1805,16 @@ gboolean stats_probe(gpointer user) {
}
/// Which section the dock's scroller is showing at its top: "measure" once it has been scrolled to the
/// cursors panel or past it, "signals" above that. Read from the adjustment, so it is the state the user
/// would see and not the last call made.
/// measurements' heading or past it, "cursors" above that (the cursors panel is the dock's first section;
/// the signal list is a slide-over page). Read from the adjustment, so it is the state the user would see
/// and not the last call made.
static const char* dock_section_in_view(App& a) {
if (a.dock_scroller == nullptr || a.cursors_panel == nullptr || a.dock_page == nullptr) return "none";
if (a.dock_scroller == nullptr || a.measures_head == nullptr || a.dock_page == nullptr) return "none";
graphene_point_t zero = GRAPHENE_POINT_INIT(0.f, 0.f);
graphene_point_t at;
if (!gtk_widget_compute_point(a.cursors_panel, a.dock_page, &zero, &at)) return "none";
if (!gtk_widget_compute_point(a.measures_head, a.dock_page, &zero, &at)) return "none";
GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(a.dock_scroller));
return gtk_adjustment_get_value(adj) > 0.5 * at.y ? "measure" : "signals";
return gtk_adjustment_get_value(adj) > 0.5 * at.y ? "measure" : "cursors";
}
/// Phase two of the dock-section probe: the accumulators must have advanced while the Measure section was
@@ -1825,7 +1837,7 @@ gboolean dock_section_probe_report(gpointer user) {
}
}
if (a->dock_page != nullptr) gtk_widget_set_size_request(a->dock_page, -1, -1);
if (a->signal_panel != nullptr) gtk_widget_set_size_request(a->signal_panel, -1, -1);
if (a->cursors_panel != nullptr) gtk_widget_set_size_request(a->cursors_panel, -1, -1);
std::fprintf(stderr,
"[side-tab-probe] section=%s rows=%zu accums=%zu advanced=%" G_GUINT64_FORMAT
" min_n=%" G_GUINT64_FORMAT "\n",
@@ -1847,10 +1859,11 @@ gboolean dock_section_probe(gpointer user) {
if (!padded && a->dock_page != nullptr) {
padded = true;
gtk_widget_set_size_request(a->dock_page, -1, 3000);
// ... and the signal list taller than any window too, which is what pushes the Measure section below
// the viewport at offset 0 (padding the page's end only makes it scrollable, and on a window taller than
// the section's y the section would stay in view). Undone in the report.
if (a->signal_panel != nullptr) gtk_widget_set_size_request(a->signal_panel, -1, 3000);
// ... and the cursors panel (the dock's first section, above the measurements) taller than any window
// too, which is what pushes the measurements below the viewport at offset 0 (padding the page's end only
// makes it scrollable, and on a window taller than the section's y the section would stay in view).
// Undone in the report.
if (a->cursors_panel != nullptr) gtk_widget_set_size_request(a->cursors_panel, -1, 3000);
g_timeout_add(400, dock_section_probe, a);
return G_SOURCE_REMOVE;
}
@@ -1892,16 +1905,16 @@ gboolean dock_section_probe(gpointer user) {
gtk_check_button_set_active(GTK_CHECK_BUTTON(a->measures_stats_check), TRUE);
}
GtkAdjustment* dock_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(a->dock_scroller));
// From the top, is the measure section (the cursors panel, where `dock_show_section("measure")` lands)
// below the viewport? Only then is scrolling to it a move *into* view and back out a move *away*; a
// From the top, is the measure section (the measurements' heading) below the viewport? Only then is scrolling to it a move *into* view and back out a move *away*; a
// section already in view at offset 0 would make the whole probe a reading of nothing.
gtk_adjustment_set_value(dock_adj, 0.0);
graphene_point_t zero = GRAPHENE_POINT_INIT(0.f, 0.f);
graphene_point_t measure_at = GRAPHENE_POINT_INIT(-1.f, -1.f);
const bool located =
a->cursors_panel != nullptr && gtk_widget_compute_point(a->cursors_panel, a->dock_page, &zero, &measure_at);
a->measures_head != nullptr && gtk_widget_compute_point(a->measures_head, a->dock_page, &zero, &measure_at);
const bool outside_at_top = located && static_cast<double>(measure_at.y) >= gtk_adjustment_get_page_size(dock_adj);
dock_show_section(*a, "measure");
dock_show_section(*a, "measure"); // lands on the cursors panel, the dock's top: a no-op scroll
if (located) gtk_adjustment_set_value(dock_adj, measure_at.y); // ... so go to the measurements themselves
// Where the dock went: `scrolled` is its offset and `reach` the most it can scroll. A reach of 0 would
// mean the content fits and "away from the section" is not a state this run can reach.
const double scrolled = gtk_adjustment_get_value(dock_adj);
@@ -1911,7 +1924,7 @@ gboolean dock_section_probe(gpointer user) {
for (const MeasureStatsAccum& s : a->measure_stats) g_dock_section_n_before.push_back(s.n);
// Away from the section the measurements are in: they are scrolled out of the viewport (the widgets
// stay mapped, only clipped), and the tick keeps calling `update_measures()` regardless.
dock_show_section(*a, "signals");
gtk_adjustment_set_value(dock_adj, 0.0);
std::fprintf(stderr,
"[side-tab-probe] section=%s accums=%zu baseline recorded (measure at scroll %.0f of %.0f, "
"outside_at_top=%d)\n",
@@ -6427,12 +6440,17 @@ gboolean keys_probe(gpointer user) {
line += buf;
}
{
// `g` is the signal list's slide-over page now: it opens it, the dock does not move, and a second `g`
// closes it again.
const double before = dock_value();
run_app_action(*a, AppAction::ShowSignals);
const bool opened = slideover_page(*a) == "signals";
const double after = dock_value();
run_app_action(*a, AppAction::ShowSignals);
const bool closed = slideover_page(*a).empty();
char buf[96];
std::snprintf(buf, sizeof(buf), " g: dock %.0f->%.0f at_top=%d |", before, after,
std::fabs(after) < 1.0 ? 1 : 0);
std::snprintf(buf, sizeof(buf), " g: page=%d dock_still=%d closed=%d |", opened ? 1 : 0,
std::fabs(after - before) < 1.0 ? 1 : 0, closed ? 1 : 0);
line += buf;
}
before_after("l: layout", a->layout_toggle, AppAction::LayoutEditor);
+2 -1
View File
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
//
// The signal list: one row per signal with its colour chip and acquire tick, the panel around it,\n// and the drop that puts a signal on a row.
// The signal list: one row per signal with its colour chip and acquire tick, the panel around it,
// and the drop that puts a signal on a row.
#include "ui_app.hpp"
+5 -5
View File
@@ -454,12 +454,12 @@ constexpr TourStep kTourSteps[] = {
"the view then freezes on the first event that matches, however rare it is.",
&App::trigger_body},
{"side", nullptr, "The dock",
"The dock beside the plot: the list of signals, then the cursors and the measurements. The "
"buttons in the bar above show and hide it.",
"The dock beside the plot: the cursors, then the measurements. The buttons in the bar above "
"show and hide it.",
&App::side_box},
{"signals", nullptr, "The signals",
"Every signal the source declares, each with the switch that acquires it. A signal has to be "
"switched on before it can be plotted, measured or searched.",
{"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.",
&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 "