ui: the events list is a slide-over page; the left pane, its divider and the header toggle are gone

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Sonnet 4.6 committed 2026-11-03 07:07:57 +01:00
1 parent cb098eff75
commit 8c866300d8
12 files changed
+244 -350

No files matched your search

+24 -43
View File
@@ -74,7 +74,6 @@ namespace ui_impl {
/// fill, the bar does not.
// --- persisted layout -------------------------------------------------------
/// Defined with the events pane; the trigger toggle reveals it.
// Defined below; the configurations dialog opens a session with them, and it sits above all three.
@@ -3007,8 +3006,7 @@ void note_panel_controls(App& app) {
// widget tree walked in child order *is* the layout order, so this is the reading order of the
// controls the user sees -- the only way to check an arrangement without a screenshot.
const std::pair<GtkWidget*, const char*> named[] = {
{app.trigger_toggle, "trigger"}, {app.events_toggle, "events"},
{app.pause_button, "pause"},
{app.trigger_toggle, "trigger"}, {app.pause_button, "pause"},
{app.cursors_toggle, "cursors"}, {app.sidebar_toggle, "sidebar"},
{app.menu_button, "menu"},
};
@@ -3631,7 +3629,6 @@ gboolean tick(gpointer user) {
}
// Outside the lock: it reads and writes the two plots' own windows, not the model.
sync_compare_axes(*app);
sync_events_divider(*app);
// A file on screen is an event, and gets what a held capture gets: the axis pinned to the event's own
// `[-pre, +post]` and the trigger instant marked at x = 0. That *is* the constraint -- the time base is
// the event's, so no gesture can take the view outside it -- and the window control reads the event's
@@ -4932,12 +4929,10 @@ GtkWidget* build_toolbar(App& app) {
app.menu_button = GTK_WIDGET(gtk_builder_get_object(ui, "menu_button"));
app.sidebar_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "sidebar_toggle"));
app.trigger_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_toggle"));
app.events_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "events_toggle"));
app.pause_button = GTK_WIDGET(gtk_builder_get_object(ui, "pause_button"));
app.cursors_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "cursors_toggle"));
if (app.menu_button == nullptr || app.sidebar_toggle == nullptr || app.trigger_toggle == nullptr ||
app.events_toggle == nullptr || app.pause_button == nullptr ||
app.cursors_toggle == nullptr) {
app.pause_button == nullptr || app.cursors_toggle == nullptr) {
// A missing id is a mistake in the `.ui`: the bar comes up with the controls it got rather than taking
// the window with it, and `[udpscope] header:` reads whatever geometry is left.
return bar;
@@ -4947,17 +4942,6 @@ GtkWidget* build_toolbar(App& app) {
// What each control *does*, which a file cannot hold.
g_signal_connect(app.sidebar_toggle, "toggled", G_CALLBACK(on_sidebar_toggled), &app);
g_signal_connect(app.trigger_toggle, "toggled", G_CALLBACK(on_trigger_toggled), &app);
// The event history is a *trigger* thing, and one click from the mode that produces it.
g_signal_connect(app.events_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* b, gpointer user) {
App* a = static_cast<App*>(user);
const bool open = gtk_toggle_button_get_active(b);
gtk_revealer_set_reveal_child(GTK_REVEALER(a->events_revealer), open);
// Through the one function that decides *both*: revealing the child alone leaves an
// invisible pane with no width, which is a button that appears to do nothing.
sync_events_availability(*a);
if (open) refresh_events_pane(*a);
}),
&app);
g_signal_connect(app.pause_button, "toggled", G_CALLBACK(on_pause_toggled), &app);
g_signal_connect(app.cursors_toggle, "toggled", G_CALLBACK(on_cursors_toggled), &app);
return bar;
@@ -4969,7 +4953,7 @@ void on_activate(GtkApplication* application, gpointer user) {
install_css();
// The window's skeleton is a `.ui` file: the window, its two panes, the plot column with the status
// line under it, and the dock's scroller. Everything that depends on the
// stream -- the events pane, the two plot areas, the dock's panels -- is built below and put
// stream -- the events page, the two plot areas, the dock's panels -- is built below and put
// into the slots by id.
g_clear_object(&app->window_ui);
app->window_ui = gtk_builder_new_from_resource("/org/udpscope/ui/main-window.ui");
@@ -4977,7 +4961,6 @@ void on_activate(GtkApplication* application, gpointer user) {
app->window = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "main_window"));
GtkWidget* side_view = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_view"));
app->side_view = side_view;
GtkWidget* events_divider = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "events_divider"));
app->plot_box = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "plot_box"));
app->plot_split = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "plot_split"));
app->plot_overlay = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "plot_overlay"));
@@ -4995,7 +4978,7 @@ void on_activate(GtkApplication* application, gpointer user) {
app->badge_system = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "badge_system"));
// A missing id is the one mistake neither the build nor `GtkBuilder` sees, and every use below would
// be a null pointer.
if (app->window == nullptr || side_view == nullptr || events_divider == nullptr ||
if (app->window == nullptr || side_view == nullptr ||
app->plot_box == nullptr || app->plot_overlay == nullptr || app->plot_split == nullptr ||
app->status_badge == nullptr ||
app->status_label == nullptr || app->status_info_button == nullptr ||
@@ -5195,17 +5178,13 @@ void on_activate(GtkApplication* application, gpointer user) {
// `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.
// A *paned*: the pane's width is the user's to choose, and the plot takes what is left.
// The events pane is the one subtree of the inner divider that is built: whether it exists at all
// depends on trigger mode.
gtk_paned_set_start_child(GTK_PANED(events_divider), build_events_pane(*app));
// Once, now that both the button and the pane exist: trigger mode is off, so the pane is closed and
// its button is not on the bar -- stated by the one function that decides it rather than by whichever
// The full events list is the `events` page of the slide-over (the dock's Manage button opens it), so
// the plot is never squeezed by it. Once the page and the dock block both exist: nothing is cached yet,
// so both start insensitive -- stated by the one function that decides it rather than by whichever
// widget happened to be built with which default.
build_events_pane(*app);
slideover_add_page(*app, "events", "Events", app->events_box);
sync_events_availability(*app);
app->events_divider = events_divider;
// Two plots, in a paned so a comparison's divider can move: the primary takes everything while the
// second is hidden, and the width is the user's once it is not.
@@ -5715,14 +5694,17 @@ int group_visible_traces(App& app, int row) {
return visible;
}
/// The events block's availability as one reading: the rule, the dock block, the page, and the slide-over's
/// open page. Every step that changes recording, replay or the cache prints all of them.
void report_events_availability(App& a, const char* what) {
std::fprintf(stderr, "[events] %s: button_visible=%d pane=%d pane_w=%d tick=%d recording=%d\n",
what,
a.events_toggle != nullptr && gtk_widget_get_visible(a.events_toggle) ? 1 : 0,
a.events_revealer != nullptr &&
gtk_revealer_get_reveal_child(GTK_REVEALER(a.events_revealer)) ? 1 : 0,
a.events_divider != nullptr ? gtk_paned_get_position(GTK_PANED(a.events_divider)) : -1,
signal_tick_visible(a), a.event_recording ? 1 : 0);
// `available` is the rule `sync_events_availability()` states; the dock block and the page must both
// follow it, and `page` is the slide-over's open page.
const bool available = a.event_recording || a.replaying || a.events_cached;
std::fprintf(stderr, "[events] %s: available=%d dock_sensitive=%d page_sensitive=%d page=%s\n", what,
available ? 1 : 0,
a.dock_events_box != nullptr && gtk_widget_get_sensitive(a.dock_events_box) ? 1 : 0,
a.events_box != nullptr && gtk_widget_get_sensitive(a.events_box) ? 1 : 0,
slideover_page(a).empty() ? "none" : slideover_page(a).c_str());
}
@@ -6241,9 +6223,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
app.dbg_signal_empty_label);
}
{
// The events pane's frame, read from the window: the four widgets the `.ui` gives it, and the list
// being inside the revealer the file builds. A control that lost its id reads 0 here rather than
// showing as a pane missing a button, and `nested=0` is a list that is no longer in the pane at all.
// The events page's frame, read from the window: the widgets the `.ui` gives it, and the list
// being inside the box the file builds. A control that lost its id reads 0 here rather than
// showing as a page missing a button, and `nested=0` is a list that is no longer in the page at all.
const auto inside = [](GtkWidget* w, GtkWidget* root) {
if (w == nullptr) return 0;
for (GtkWidget* p = w; p != nullptr; p = gtk_widget_get_parent(p)) {
@@ -6251,10 +6233,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
}
return 0;
};
std::printf("[udpscope] events pane: compare=%d accumulate=%d list=%d revealer=%d nested=%d\n",
std::printf("[udpscope] events pane: compare=%d accumulate=%d list=%d nested=%d\n",
app.events_compare != nullptr ? 1 : 0, app.events_accumulate != nullptr ? 1 : 0,
app.events_list != nullptr ? 1 : 0, app.events_revealer != nullptr ? 1 : 0,
inside(app.events_list, app.events_revealer));
app.events_list != nullptr ? 1 : 0, inside(app.events_list, app.events_box));
}
std::printf("[udpscope] panels: side=%dx%d (visible=%d) signals=%dx%d row_h=%d "
"(page_current=%d) trigger=%dx%d (sensitive=%d) "
+53 -58
View File
@@ -2,8 +2,8 @@
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The events pane: the revealer that slides it in, the header with the way back to the live stream, the two
actions that read the selection, and the list inside its scroller.
The events list, the content of the slide-over's `events` page: the header with the way back to the live
stream, the two actions that read the selection, and the list inside its scroller.
The rows are not here. There is one per latched event in the cache, it carries the time, the duration, the
signals and the rate, and the whole list is rebuilt whenever the cache changes — so the file holds the
@@ -15,73 +15,68 @@
instead. Both actions start insensitive because a selection starts empty, and `update_group_buttons()`
owns them from there.
A width floor, not a width: the divider sets the pane's width, and this is what it needs to stay usable at
its narrowest.
A width floor, not a width: the slide-over sets the page's width, and this is what the list needs to stay
usable at its narrowest.
-->
<interface>
<object class="GtkRevealer" id="events_pane">
<property name="transition-type">slide-right</property>
<object class="GtkBox" id="events_box">
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<property name="width-request">170</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-top">6</property>
<child>
<object class="GtkBox" id="events_box">
<property name="orientation">vertical</property>
<object class="GtkBox" id="events_head">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<property name="width-request">170</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-top">6</property>
<child>
<object class="GtkBox" id="events_head">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="events_title">
<property name="label" translatable="yes">&lt;b&gt;Events&lt;/b&gt;</property>
<property name="use-markup">1</property>
<property name="hexpand">1</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkButton" id="events_live">
<property name="label" translatable="yes">Live</property>
<property name="css-classes">flat</property>
<property name="tooltip-text" translatable="yes">go back to the live stream after replaying a stored event</property>
</object>
</child>
<object class="GtkLabel" id="events_title">
<property name="label" translatable="yes">&lt;b&gt;Events&lt;/b&gt;</property>
<property name="use-markup">1</property>
<property name="hexpand">1</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkBox" id="events_actions">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton" id="events_compare">
<property name="label" translatable="yes">Compare</property>
<property name="tooltip-text" translatable="yes">the two selected events in two plots side by side, with the axes synchronised</property>
<property name="hexpand">1</property>
<property name="sensitive">0</property>
</object>
</child>
<child>
<object class="GtkButton" id="events_accumulate">
<property name="label" translatable="yes">Accumulate</property>
<property name="tooltip-text" translatable="yes">up to four selected events overlaid in the same traces, translucent, so where they agree the trace is brighter</property>
<property name="hexpand">1</property>
<property name="sensitive">0</property>
</object>
</child>
<object class="GtkButton" id="events_live">
<property name="label" translatable="yes">Live</property>
<property name="css-classes">flat</property>
<property name="tooltip-text" translatable="yes">go back to the live stream after replaying a stored event</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="events_actions">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton" id="events_compare">
<property name="label" translatable="yes">Compare</property>
<property name="tooltip-text" translatable="yes">the two selected events in two plots side by side, with the axes synchronised</property>
<property name="hexpand">1</property>
<property name="sensitive">0</property>
</object>
</child>
<child>
<object class="GtkScrolledWindow" id="events_scroll">
<property name="vexpand">1</property>
<property name="hscrollbar-policy">never</property>
<child>
<object class="GtkListBox" id="events_list">
<property name="activate-on-single-click">0</property>
<property name="selection-mode">multiple</property>
</object>
</child>
<object class="GtkButton" id="events_accumulate">
<property name="label" translatable="yes">Accumulate</property>
<property name="tooltip-text" translatable="yes">up to four selected events overlaid in the same traces, translucent, so where they agree the trace is brighter</property>
<property name="hexpand">1</property>
<property name="sensitive">0</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow" id="events_scroll">
<property name="vexpand">1</property>
<property name="hscrollbar-policy">never</property>
<child>
<object class="GtkListBox" id="events_list">
<property name="activate-on-single-click">0</property>
<property name="selection-mode">multiple</property>
</object>
</child>
</object>
+3 -16
View File
@@ -2,8 +2,7 @@
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The header bar: the time-window control's slot, the trigger, pause and events group with the separator that
marks where the *view's* controls end, and the three at the far end.
The header bar: the time-window control's slot, the trigger and pause group, and the three at the far end.
The bar carries only the controls you reach for *while watching a signal*; everything you set up once lives
in the overflow menu, which is code — its rows act on the `RowSet` directly and one of them is a toggle
@@ -17,8 +16,8 @@
from the buffer's limits), and `gtk_header_bar_pack_start()` appends *after* whatever a file has already
packed, so a control added by code would land to the right of this whole group instead of at its head.
The events toggle starts hidden: the event history is a *trigger* thing, and asking for it is one click
from the mode that produces it.
There is no events control here: the event list is the `events` page of the slide-over, reached by the dock's
Manage button.
-->
<interface>
<object class="GtkHeaderBar" id="header_bar">
@@ -39,18 +38,6 @@
<property name="tooltip-text" translatable="yes">pause acquisition</property>
</object>
</child>
<child type="start">
<object class="GtkSeparator">
<property name="orientation">vertical</property>
</object>
</child>
<child type="start">
<object class="GtkToggleButton" id="events_toggle">
<property name="icon-name">udpscope-events-symbolic</property>
<property name="tooltip-text" translatable="yes">the event history: the triggered acquisitions kept in the cache, with a picture of each</property>
<property name="visible">0</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton" id="menu_button">
<property name="icon-name">open-menu-symbolic</property>
+70 -80
View File
@@ -3,8 +3,8 @@
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The main window's skeleton: the window, the split view that puts the sidebar beside the plot, the
events pane's divider, the plot column (`plot_overlay` > `plot_box`, the slide-over panel being an overlay child added in code)
with the status line under it, and the sidebar, which is the dock.
plot column (`left_column`: `plot_overlay` > `plot_box`, the slide-over panel being an overlay child added
in code) with the badge bar and the status line under it, and the sidebar, which is the dock.
The sidebar is an `AdwOverlaySplitView`, which is what a utility sidebar is in GNOME (as against
`AdwNavigationSplitView`, which is for navigation lists). Three things follow from that, and they are
@@ -20,14 +20,13 @@
What is *not* here is as deliberate as what is. Everything this file nests is fixed — the shape of
the window does not depend on the stream, the trigger or the layout — while the subtrees that do are
built by code and put in place: the events pane, into `events_divider`'s start side; the two plot
areas, into the split inside `plot_box`; the dock's readouts, appended to `dock_page`; and the
status popover, attached to `status_info`.
built by code and put in place: the two plot areas, into the split inside `plot_box`; the dock's
readouts and events, appended to `dock_page`; the slide-over's pages, into the panel over `plot_overlay`;
and the status popover, attached to `status_info`.
The header bar is not here either: it is the window's own titlebar, built by `build_toolbar()`
because every button on it is wired to an action. The status line is not a bottom bar — it belongs to
the plot column, not to the window, so it stays inside `left_column` where the divider can move past
it.
the plot column, not to the window, so it stays inside `left_column` under the plot.
-->
<interface>
<object class="GtkApplicationWindow" id="main_window">
@@ -68,96 +67,87 @@
</object>
</property>
<property name="content">
<object class="GtkPaned" id="events_divider">
<property name="orientation">horizontal</property>
<!-- Shrinkable, or the pane's own minimum is reserved as an empty column even while it is
closed and the plot pays for a pane nobody opened. -->
<property name="shrink-start-child">1</property>
<property name="resize-start-child">0</property>
<child type="end">
<object class="GtkBox" id="left_column">
<property name="orientation">vertical</property>
<object class="GtkBox" id="left_column">
<property name="orientation">vertical</property>
<property name="hexpand">1</property>
<child>
<!-- The plot, and the slide-over panel that opens over its right edge. The panel is
one opaque overlay child placed by `halign=end` and a 360 px width, never a
full-area transparent child, which would be picked wherever its allocation is
and swallow the plot's pan, drop and hover (docs/claude/invariants-plot-and-layout.md).
The overlay is the only parent change: `plot_box` keeps its id and its content. -->
<object class="GtkOverlay" id="plot_overlay">
<property name="hexpand">1</property>
<property name="vexpand">1</property>
<child>
<!-- The plot, and the slide-over panel that opens over its right edge. The panel is
one opaque overlay child placed by `halign=end` and a 360 px width, never a
full-area transparent child, which would be picked wherever its allocation is
and swallow the plot's pan, drop and hover (docs/claude/invariants-plot-and-layout.md).
The overlay is the only parent change: `plot_box` keeps its id and its content. -->
<object class="GtkOverlay" id="plot_overlay">
<object class="GtkBox" id="plot_box">
<property name="orientation">vertical</property>
<property name="homogeneous">1</property>
<property name="spacing">4</property>
<property name="hexpand">1</property>
<property name="vexpand">1</property>
<child>
<object class="GtkBox" id="plot_box">
<property name="orientation">vertical</property>
<property name="homogeneous">1</property>
<property name="spacing">4</property>
<property name="hexpand">1</property>
<property name="vexpand">1</property>
<child>
<object class="GtkPaned" id="plot_split">
<property name="orientation">horizontal</property>
<property name="shrink-end-child">1</property>
</object>
</child>
<object class="GtkPaned" id="plot_split">
<property name="orientation">horizontal</property>
<property name="shrink-end-child">1</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<!-- Setup is reached by the object it configures: a badge per signal on the left
(step 2), the system badges (Trigger, Source) on the right. A fixed height, so
the plot above it never moves when badges come and go. -->
<object class="GtkBox" id="badge_bar">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<property name="height-request">36</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<child>
<!-- Setup is reached by the object it configures: a badge per signal on the left
(step 2), the system badges (Trigger, Source) on the right. A fixed height, so
the plot above it never moves when badges come and go. -->
<object class="GtkBox" id="badge_bar">
<object class="GtkBox" id="badge_signals">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<property name="height-request">36</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<child>
<object class="GtkBox" id="badge_signals">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<property name="hexpand">1</property>
</object>
</child>
<child>
<object class="GtkBox" id="badge_system">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<property name="halign">end</property>
</object>
</child>
<property name="hexpand">1</property>
</object>
</child>
<child>
<object class="GtkBox" id="status_row">
<object class="GtkBox" id="badge_system">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<property name="margin-top">2</property>
<property name="margin-bottom">2</property>
<property name="margin-start">6</property>
<child>
<object class="GtkLabel" id="status_badge"/>
</child>
<child>
<!-- The line is rewritten every frame and its counters grow, so it is
ellipsized: a label's minimum width is its text, and an unellipsized one
would push the divider around as the numbers change. -->
<object class="GtkLabel" id="status_label">
<property name="label">starting…</property>
<property name="ellipsize">end</property>
<property name="hexpand">1</property>
<property name="halign">fill</property>
</object>
</child>
<child>
<object class="GtkMenuButton" id="status_info">
<property name="icon-name">help-about-symbolic</property>
<property name="tooltip-text" translatable="yes">Stream and buffer detail</property>
<property name="valign">center</property>
</object>
</child>
<property name="halign">end</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="status_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<property name="margin-top">2</property>
<property name="margin-bottom">2</property>
<property name="margin-start">6</property>
<child>
<object class="GtkLabel" id="status_badge"/>
</child>
<child>
<!-- The line is rewritten every frame and its counters grow, so it is
ellipsized: a label's minimum width is its text, and an unellipsized one
would push the divider around as the numbers change. -->
<object class="GtkLabel" id="status_label">
<property name="label">starting…</property>
<property name="ellipsize">end</property>
<property name="hexpand">1</property>
<property name="halign">fill</property>
</object>
</child>
<child>
<object class="GtkMenuButton" id="status_info">
<property name="icon-name">help-about-symbolic</property>
<property name="tooltip-text" translatable="yes">Stream and buffer detail</property>
<property name="valign">center</property>
</object>
</child>
</object>
+9 -16
View File
@@ -789,26 +789,18 @@ struct App {
/// `event_recording` is the trigger panel's switch and defaults on -- the cache is only useful if
/// it was already on when the event you wanted happened.
std::unique_ptr<EventStore> event_store;
/// Whether the cache holds anything, refreshed once a second: what keeps the history button on the bar
/// for a source whose events are already written, even with recording switched off.
/// Whether the cache holds anything, refreshed once a second: what keeps the dock's Events block and the
/// `events` page live for a source whose events are already written, even with recording switched off.
bool events_cached = false;
bool event_recording = false;
/// The events pane: its list, the revealer that holds it, the recording switch (in the trigger
/// panel, where the trigger's options live), and `events_thumb` -- the picture of the newest
/// The `events` page of the slide-over: its content box, its list, the recording switch (in the
/// trigger panel, where the trigger's options live), and `events_thumb` -- the picture of the newest
/// event. Every row carries the picture of *its* event; this is the first one's, which is the one
/// the probe measures and which the pane does not put anywhere special.
GtkWidget* events_revealer = nullptr;
/// the probe measures and which the page does not put anywhere special.
GtkWidget* events_box = nullptr;
GtkWidget* events_list = nullptr;
GtkWidget* events_thumb = nullptr;
GtkWidget* events_record_switch = nullptr;
/// The toolbar's event-history toggle: the pane is opened *on demand* from here (the item's own
/// arrangement), and the toggle only exists while trigger mode does -- there are no events without
/// a trigger, and a button that can never have anything to show is a button spent on nothing.
GtkWidget* events_toggle = nullptr;
/// The paned that holds the events pane and the plot, and the width the divider was last dragged
/// to. 240 by default: enough for the preview and the numbers beside it, and not a plot's worth.
GtkWidget* events_divider = nullptr;
int events_pane_width = 240;
/// The Events block of the dock: the box, its list (compact rows, one per `EventRecord`, the record
/// attached to each row as `udpscope-event`) and the Manage button.
GtkWidget* dock_events_box = nullptr;
@@ -1228,6 +1220,9 @@ struct App {
/// it keeps for a second latch (it is counting retries, not time).
int dbg_events_step = 0;
int dbg_events_wait = 0;
/// The plot area's allocated width before the first event was latched (the events probe), compared with
/// its width after: the events list is a slide-over page and no longer a column beside the plot.
int dbg_events_plot_w = 0;
/// The event row the probe selected before it activated one, and how many rows were selected.
/// Both are read back *after* the activation: a row pointer that is no longer the list's row 0,
/// or a smaller count, says the activation rebuilt the list and so threw away the user's scroll
@@ -1753,8 +1748,6 @@ void on_pause_toggled(GtkToggleButton* button, gpointer user);
void search_jump(App& app, double t);
void update_search_panel(App& app);
void sync_events_availability(App& app);
/// Restates the events pane's divider position once a frame; see the definition.
void sync_events_divider(App& app);
/// The preferences dialog: one window, opened from the overflow menu. `apply_theme()` is separate
/// because the theme is the one preference that takes effect without a restart.
void open_preferences(App& app);
+9 -4
View File
@@ -11,7 +11,7 @@
//
// Below them, the Events block: one compact row per cached event (marker, title, time; no thumbnails),
// a Manage button for the full list, and one click on a row to replay it. Its rows are built from the
// same `event_store->list()` and the same `event_row_title()` as the events pane, so the two cannot
// same `event_store->list()` and the same `event_row_title()` as the events page, so the two cannot
// disagree.
#include "ui_app.hpp"
@@ -249,11 +249,16 @@ const char* dock_event_marker(const App& app, const EventRecord& r) {
return " ";
}
/// Opens the `events` page. The page's list is rebuilt only while the page is open (see `record_event()`),
/// so it is refreshed here, once, before the page shows: the slide-over has no on-open hook, and the list
/// is small. The button is not a child of either list, so the rebuild cannot free the widget it runs from.
void on_manage_events(GtkButton*, gpointer user) {
slideover_open(*static_cast<App*>(user), "events");
App& app = *static_cast<App*>(user);
refresh_events_pane(app);
slideover_open(app, "events");
}
/// One click on a row replays it, as the pane's activation does: `replay_event()` and then the in-place
/// One click on a row replays it, as the page list's activation does: `replay_event()` and then the in-place
/// re-mark. The record is copied first, because nothing it triggers may be allowed to free the row it
/// was read from.
///
@@ -394,7 +399,7 @@ void dock_rebuild_events(App& app, const std::vector<EventRecord>& records) {
gtk_widget_set_hexpand(name, true);
gtk_widget_set_tooltip_text(name, text.c_str());
gtk_box_append(GTK_BOX(line), name);
// The latch's sequence number, as the pane's third line ends with it: the title already carries the
// The latch's sequence number, as the page row's third line ends with it: the title already carries the
// clock, so this is the part of the row that is not in it.
const std::string seq = "#" + std::to_string(r.seq);
GtkWidget* num = fixed_label(seq.c_str(), kEventSeqChars, GTK_ALIGN_END);
+31 -80
View File
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
//
// The event cache, seen from the front end: the pane that lists it, the picture each row carries, the\n// replay, and the two group layouts (side-by-side comparison and accumulation) with the axis mirror\n// that keeps a comparison several plots in step.
// The event cache, seen from the front end: the slide-over page that lists it, the picture each row
// carries, the replay, and the two group layouts (side-by-side comparison and accumulation) with the
// axis mirror that keeps a comparison several plots in step.
#include "ui_app.hpp"
@@ -32,68 +34,19 @@ GtkWidget* make_compare_area(App& app) {
return widget;
}
/// Whether the event history is reachable at all, and the one place that decides it. It has exactly
/// two preconditions: trigger mode (there are no events without a trigger) and recording (a disabled
/// cache is empty, and an empty column beside the plot is what "off" must not look like). Both the
/// toolbar button and the pane follow from it, so they cannot disagree -- and turning recording off
/// while the pane is open takes the button and the pane with it rather than leaving a stale list.
/// Whether the event history is reachable at all, and the one place that decides it. Recording on (the
/// cache can still grow), an event being read (a replay), or something already in the cache (an event on
/// disk outlives the switch that wrote it) -- not trigger mode: a stored event being examined is a file,
/// and it has no trigger. The dock's Events block and the `events` page follow from it, so they cannot
/// disagree; both stay on screen and go insensitive ("disable, do not hide"), and turning recording off
/// while the page is open leaves the page up, greyed, rather than stale and live.
///
/// `events_cached` is refreshed once a second from the tick -- the store's list is a directory read, not
/// a per-frame question.
void sync_events_availability(App& app) {
if (app.events_toggle == nullptr) return;
// What the divider was last dragged to. Read here, on every change of state, because the close that
// follows is about to set the position to 0 and lose it.
if (app.events_divider != nullptr) {
const int at = gtk_paned_get_position(GTK_PANED(app.events_divider));
if (at > 1) app.events_pane_width = at;
}
// Available while the cache can still grow (recording on) *or* while it is being read (an event on
// screen). Not trigger mode: a stored event being examined is a file, and it has no trigger -- if the
// pane's life depended on the trigger, opening one would close the list it was opened from.
// ... or whenever the cache *has* something: an event on disk outlives the switch that wrote it, and
// the button that shows it must not go with the switch. `events_cached` is refreshed once a second
// from the tick -- the store's list is a directory read, not a per-frame question.
const bool available = app.event_recording || app.replaying || app.events_cached;
dock_events_set_sensitive(app, available);
gtk_widget_set_visible(app.events_toggle, available);
if (!available) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.events_toggle), FALSE);
gtk_revealer_set_reveal_child(GTK_REVEALER(app.events_revealer), FALSE);
}
const bool open =
available && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.events_toggle));
// Visibility, not only `reveal_child`: an unrevealed `GtkRevealer` *measures* 0x0 and its box still
// hands it the leftover width, so the collapsed pane reserved an empty column beside the plot.
gtk_widget_set_visible(app.events_revealer, open);
// The position is what gives the pane its room (or none): a paned hands its start child at least its
// minimum unless the position says otherwise.
if (app.events_divider != nullptr) {
gtk_paned_set_position(GTK_PANED(app.events_divider), open ? app.events_pane_width : 0);
}
}
/// Restates the divider's position, once a frame.
///
/// `gtk_paned_set_position()` is clamped against the allocation the paned has *now*, and a click on
/// the history button flips the toggle in the same instant the pane becomes visible -- so the position
/// is clamped against a child that still measures zero, the pane opens with no width, and only the
/// next call (a replay's, seconds later) gives it one. Restating it per frame is idempotent, and a
/// position the user dragged is *learned* rather than fought: the width is read back whenever the pane
/// is open and the position is not the one we would set.
void sync_events_divider(App& app) {
if (app.events_divider == nullptr) return;
const bool open =
app.events_revealer != nullptr && gtk_widget_get_visible(app.events_revealer);
const int at = gtk_paned_get_position(GTK_PANED(app.events_divider));
if (!open) {
if (at != 0) gtk_paned_set_position(GTK_PANED(app.events_divider), 0);
return;
}
if (at > 1 && at != app.events_pane_width) {
app.events_pane_width = at; // the user dragged it: that is the width now
return;
}
if (at != app.events_pane_width) {
gtk_paned_set_position(GTK_PANED(app.events_divider), app.events_pane_width);
}
if (app.events_box != nullptr) gtk_widget_set_sensitive(app.events_box, available);
}
/// Writes the held event into the cache: its table, a sidecar of what the list shows, and a
@@ -133,11 +86,10 @@ bool record_event(App& app) {
set_notice(app, "event cached: " + std::to_string(table.columns.size()) + " signals, " +
std::to_string(table.rows()) + " samples",
3);
// The dock follows every latch; the pane's rows (with pictures) only while the pane is on screen:
// rebuilding them per latch for a column nobody opened is work the tick owes nothing, and the toggle's
// own handler refreshes when it is asked for.
if (app.events_revealer != nullptr &&
gtk_revealer_get_child_revealed(GTK_REVEALER(app.events_revealer))) {
// The dock follows every latch; the page's rows (with pictures) only while the page is open:
// rebuilding them per latch for a list nobody opened is work the tick owes nothing, and the Manage
// button refreshes the list when it is asked for.
if (slideover_page(app) == "events") {
refresh_events_pane(app);
} else {
dock_rebuild_events(app);
@@ -269,7 +221,7 @@ bool replay_event(App& app, const EventRecord& record) {
// left alone afterwards -- restated here and never per frame.
// The axis is the event's own and the tick pins it there (`pin_event_window`): nothing to frame or
// home here beyond the window control's own reading, which the tick keeps in step.
// The pane is available through a replay, and this is what keeps it open.
// The events block is available through a replay, and this is what keeps it so.
sync_events_availability(app);
set_notice(app, "replaying " + record.stem + " (" +
std::to_string(record.signals) + " signals, " +
@@ -825,9 +777,9 @@ void mark_events_on_screen(App& app) {
}
}
/// The events pane's rows, newest first: each row is the picture of *that* event beside the
/// The events page's rows, newest first: each row is the picture of *that* event beside the
/// sidecar's numbers, because what tells one event from another is its shape, and one picture for
/// the whole pane describes whichever event happens to be newest rather than the one being read. The
/// the whole page describes whichever event happens to be newest rather than the one being read. The
/// numbers are one line each: a list that has to load the samples to describe an event is a list that
/// cannot show thirty of them.
static void rebuild_events_pane_list(App& app, std::vector<EventRecord>& records) {
@@ -934,7 +886,7 @@ static void rebuild_events_pane_list(App& app, std::vector<EventRecord>& records
gtk_widget_set_halign(name, GTK_ALIGN_START);
gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END);
// A cap, not only an ellipsis: `max-width-chars` bounds a label's *natural* width, and the row's
// natural width is the pane's minimum -- which is the floor the divider cannot go below.
// natural width is the page's minimum -- which is the floor the list cannot go below.
gtk_label_set_max_width_chars(GTK_LABEL(name), 22);
gtk_widget_add_css_class(name, "udpscope-event-title");
gtk_box_append(GTK_BOX(box), name);
@@ -1001,28 +953,27 @@ void on_event_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
mark_events_on_screen(app);
}
/// The pane itself: a collapsible column on the left, a header and the list of events -- each row
/// carrying its own picture. Built once with the rest of the window, and opened on demand from the
/// toolbar's history button, which is itself only there while trigger mode and recording are both
/// on (`sync_events_availability()`).
/// The events list: a header and the list of events, each row carrying its own picture. Built once with
/// the rest of the window and returned as the content of the slide-over's `events` page, which the
/// dock's Manage button opens.
GtkWidget* build_events_pane(App& app) {
// The pane's frame -- the revealer that slides it in, the header with the way back to the live stream, the
// two actions that read the selection, and the list inside its scroller -- is a UI file. The rows are not:
// The list's frame -- the header with the way back to the live stream, the two actions that read the
// selection, and the list inside its scroller -- is a UI file. The rows are not:
// there is one per latched event, and the whole list is rebuilt whenever the cache changes. The two
// buttons' sensitivity is `update_group_buttons()`'s, which is why the file only starts them insensitive.
GtkBuilder* ui = ui_load(app, "/org/udpscope/ui/events-pane.ui");
app.events_revealer = GTK_WIDGET(gtk_builder_get_object(ui, "events_pane"));
app.events_box = GTK_WIDGET(gtk_builder_get_object(ui, "events_box"));
app.events_compare = GTK_WIDGET(gtk_builder_get_object(ui, "events_compare"));
app.events_accumulate = GTK_WIDGET(gtk_builder_get_object(ui, "events_accumulate"));
GtkWidget* live = GTK_WIDGET(gtk_builder_get_object(ui, "events_live"));
app.events_list = GTK_WIDGET(gtk_builder_get_object(ui, "events_list"));
// The heading row, where the contextual help's "?" joins (see `ui_context_help.cpp`).
app.events_head = GTK_WIDGET(gtk_builder_get_object(ui, "events_head"));
if (app.events_revealer == nullptr || app.events_compare == nullptr ||
if (app.events_box == nullptr || app.events_compare == nullptr ||
app.events_accumulate == nullptr || live == nullptr || app.events_list == nullptr) {
// A missing id is a mistake in the `.ui`: the pane comes up empty rather than taking the window with it,
// A missing id is a mistake in the `.ui`: the page comes up empty rather than taking the window with it,
// and `[udpscope] events pane:` reads the difference.
return app.events_revealer != nullptr ? app.events_revealer : gtk_revealer_new();
return app.events_box != nullptr ? app.events_box : gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
}
// What the pane *does*, which a file cannot hold: the two group actions read the selection, the list
@@ -1039,7 +990,7 @@ GtkWidget* build_events_pane(App& app) {
update_group_buttons(*static_cast<App*>(user));
}),
&app);
return app.events_revealer;
return app.events_box;
}
/// The recording switch, which lives here because it is a trigger option and not a pane option: it
+1 -1
View File
@@ -377,7 +377,7 @@ void apply_trigger_layout(App& app, const LayoutState::Trigger& t) {
}
write_trigger_widgets(app);
// The state is applied by *pressing the same control a user would*: the handler is what arms, tells
// the events button, greys the body and syncs the pane's own switch. Deliberately outside the
// the dock's events block, greys the body and syncs the page's own switch. Deliberately outside the
// `updating_panels` window `apply_layout()` uses for the panels, because a toggle set inside it
// would emit `toggled` into a handler that returns at once and nothing would happen.
if (app.trigger_toggle != nullptr) {
+37 -42
View File
@@ -5653,12 +5653,15 @@ gboolean events_probe_step(gpointer user) {
if (a->window == nullptr) return G_SOURCE_REMOVE;
if (a->dbg_events_step == 0) {
// Before anything: streaming, no trigger, recording **off** (the default) -- so no button, no
// pane, and nothing written to disk that the user did not ask to have written.
// Before anything: streaming, no trigger, recording **off** (the default) -- so the dock's block and
// the page are insensitive (on screen, not hidden), and nothing is written to disk that the user did
// not ask to have written. The plot's width is taken here, with no event yet: `plot_w_stable` compares
// it with the width after the first latch.
a->dbg_events_plot_w = a->area_widget != nullptr ? gtk_widget_get_width(a->area_widget) : -1;
report_events_availability(*a, "live, recording off");
// The three clicks a user makes: recording on in the trigger panel, trigger mode on, then the
// history button (which is only there once the first two have happened -- that is the item's rule,
// and this is where it is asserted).
// The clicks a user makes: recording on in the trigger panel, trigger mode on, then the dock's
// Manage button (the `events` page is reachable at any time; it is insensitive until something is
// recorded or cached, and this is where that rule is asserted).
gtk_switch_set_active(GTK_SWITCH(a->events_record_switch), TRUE);
// An 80% pre-trigger share, so the event that latches is *not* symmetric: the window a click
// opens must be the event's own span, and with a symmetric event the live window would look
@@ -5671,27 +5674,19 @@ gboolean events_probe_step(gpointer user) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
}
report_events_availability(*a, "after trigger on");
if (a->events_toggle != nullptr) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->events_toggle), TRUE);
}
// Read straight after the click, before anything else can call the availability rule: this is the
// reading that says the *button* opens the pane, and it is the one a later replay used to mask.
report_events_availability(*a, "after history button");
// ... and again a few frames later, still with no replay in the way: this is the *click alone*
slideover_open(*a, "events");
// Read straight after the open, before anything else can call the availability rule.
report_events_availability(*a, "after events page opened");
// ... and again a few frames later, still with no replay in the way: this is the *open alone*
// path a user takes, which a later replay's own availability call used to mask.
events_probe_next(*a, 5, 500);
return G_SOURCE_REMOVE;
}
if (a->dbg_events_step == 5) {
GtkWidget* box = a->events_revealer != nullptr
? gtk_revealer_get_child(GTK_REVEALER(a->events_revealer))
: nullptr;
std::fprintf(stderr, "[events] click alone: pane=%d pane_w=%d pane_box=%d\n",
a->events_revealer != nullptr && gtk_widget_get_visible(a->events_revealer) ? 1 : 0,
a->events_divider != nullptr ? gtk_paned_get_position(GTK_PANED(a->events_divider))
: -1,
box != nullptr ? gtk_widget_get_width(box) : -1);
std::fprintf(stderr, "[events] open alone: page=%s page_box=%d\n",
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(),
a->events_box != nullptr ? gtk_widget_get_width(a->events_box) : -1);
events_probe_next(*a, 1, 2500);
return G_SOURCE_REMOVE;
}
@@ -5748,11 +5743,15 @@ gboolean events_probe_step(gpointer user) {
}
std::fprintf(stderr,
" newest: stem=%s dur=%.6f pre=%.6f post=%.6f edge=%.6f signals=%zu samples=%zu "
"rate=%.6g seq=%llu thumb_bytes=%ld recorded=%d pane=%d",
"rate=%.6g seq=%llu thumb_bytes=%ld recorded=%d page=%d",
r.stem.c_str(), r.duration_s, r.pre_s, r.post_s, r.edge, r.signals, r.samples,
r.rate_hz, static_cast<unsigned long long>(r.seq), thumb_bytes,
a->event_recording ? 1 : 0,
gtk_revealer_get_reveal_child(GTK_REVEALER(a->events_revealer)) ? 1 : 0);
a->event_recording ? 1 : 0, slideover_page(*a) == "events" ? 1 : 0);
// The first latch must not have moved the plot: the list is a slide-over page over the plot's
// edge, and the dock is a fixed column, so nothing about an event changes the plot's width.
const int plot_w_now = a->area_widget != nullptr ? gtk_widget_get_width(a->area_widget) : -1;
std::fprintf(stderr, " plot_w_stable=%d plot_w=%d/%d", plot_w_now == a->dbg_events_plot_w ? 1 : 0,
a->dbg_events_plot_w, plot_w_now);
}
std::fprintf(stderr, "\n");
std::fflush(stderr);
@@ -5799,18 +5798,14 @@ gboolean events_probe_step(gpointer user) {
const int selected_now = int(g_list_length(still));
g_list_free(still);
std::fprintf(stderr,
"[events] activated row0: replaying=%d pane=%d pane_w=%d pane_box=%d tick=%d "
"[events] activated row0: replaying=%d page=%s page_box=%d tick=%d "
"window=%.3f axis=[%.3f,%.3f] marker=%.3f "
"thumbnail=%d thumb_size=%dx%d thumb_visible=%d rows_kept=%d selected=%d/%d\n",
a->replaying ? 1 : 0,
a->events_revealer != nullptr && gtk_widget_get_visible(a->events_revealer) ? 1 : 0,
a->events_divider != nullptr ? gtk_paned_get_position(GTK_PANED(a->events_divider))
: -1,
// The pane's *rendered* width: the position is what we asked for, this is what the
// pane actually got -- a divider at 240 with a zero-width pane is still no pane.
a->events_revealer != nullptr && gtk_revealer_get_child(GTK_REVEALER(a->events_revealer)) != nullptr
? gtk_widget_get_width(gtk_revealer_get_child(GTK_REVEALER(a->events_revealer)))
: -1,
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(),
// The page's *rendered* width: what it actually got -- an open page with a zero-width
// list is still no list.
a->events_box != nullptr ? gtk_widget_get_width(a->events_box) : -1,
signal_tick_visible(*a), a->rows->time_window(),
a->rows->row(0).plot != nullptr
? a->rows->row(0).plot->x_range(a->rows->row(0).axis_x).min
@@ -5873,9 +5868,9 @@ gboolean events_probe_step(gpointer user) {
std::fprintf(stderr, "[events] live again: replaying=%d waveform=%d\n", a->replaying ? 1 : 0,
a->waveform_name.empty() ? 0 : 1);
report_live_restored(*a, "after replay");
// The pane's *other* precondition, through the real switch, in both trigger states: disabling
// recording has to take the button and the pane with it, and arming the trigger afterwards must not
// bring back a pane over an empty cache.
// The block's *other* precondition, through the real switch, in both trigger states: disabling
// recording with an empty cache has to grey the dock's block and the page, and arming the trigger
// afterwards must not wake them over an empty cache.
gtk_switch_set_active(GTK_SWITCH(a->events_record_switch), FALSE);
report_events_availability(*a, "recording off, trigger off");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
@@ -5901,11 +5896,11 @@ gboolean events_group_probe_step(gpointer user) {
return G_SOURCE_REMOVE;
}
a->dbg_events_wait = 0;
// Open the pane first: its list is only rebuilt while the pane is *up*, so a selection made against
// a collapsed pane selects rows that are not there.
if (a->events_toggle != nullptr &&
!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a->events_toggle))) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->events_toggle), TRUE);
// Open the page first: its list is only rebuilt while the page is *open* (or by the Manage button), so
// a selection made against a closed page selects rows that are not current.
if (slideover_page(*a) != "events") {
refresh_events_pane(*a);
slideover_open(*a, "events");
}
// The selection *is* the input, so select the two newest rows the way a user does and press the
// button: `on_group_clicked()` reads the list, not a parameter.
@@ -5937,13 +5932,13 @@ gboolean events_group_probe_step(gpointer user) {
std::lock_guard<std::mutex> lock(a->model->mutex());
std::fprintf(stderr,
"[events] compare: kind=%d rows=%d channels=%zu samples0=%llu traces0=%d traces1=%d "
"window=%.3f recording=%d pane=%d\n",
"window=%.3f recording=%d page=%s\n",
static_cast<int>(a->group_kind), a->rows->row_count(),
a->model->channels().size(),
static_cast<unsigned long long>(a->model->live().values(0).produced()),
group_visible_traces(*a, 0), group_visible_traces(*a, 1), a->rows->time_window(),
a->event_recording ? 1 : 0,
a->events_revealer != nullptr && gtk_widget_get_visible(a->events_revealer) ? 1 : 0);
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str());
report_dock_events(*a, "comparison up");
events_group_probe_next(*a, 46, 400);
return G_SOURCE_REMOVE;
-3
View File
@@ -656,9 +656,6 @@ GtkWidget* build_signal_panel(App& app) {
return box;
}
/// The pane's availability as one reading: the history button on the bar, the pane revealed, and the
/// switch that decides half of it. Both the button and the pane follow trigger mode *and* recording,
/// so every step that changes either prints all three.
/// Whether the signal list's first row still carries its acquire tick. A file view has nothing to
/// acquire -- the samples are stored -- so the tick goes; in live mode it is the control that decides
/// what is acquired at all.
+5 -5
View File
@@ -594,10 +594,10 @@ void on_trigger_toggled(GtkToggleButton* button, gpointer user) {
// unreachable exactly when a user wanted to change them.) What that costs is the reading: the
// `trigger panel sensitive` line is now a regression guard, and a `0` there means the
// configuration has been locked out again.
// The event-history button is part of trigger mode and the pane it opens is *on demand*; leaving
// trigger mode takes the button away and collapses the pane, so going back to streaming is not a
// column of stale events beside the plot. Recording is the other half of that answer, so this is
// `sync_events_availability()`'s call and not the trigger's alone.
// The dock's Events block and the `events` page follow what is recorded or cached, not trigger mode
// alone: leaving trigger mode does not empty the cache, and a replay is not in trigger mode at all.
// Recording is the other half of that answer, so this is `sync_events_availability()`'s call and not
// the trigger's alone.
sync_events_availability(*app);
// Arming is a *runtime* control: with trigger mode off there is no engine to arm, and the arm buttons
// are the one part of this pane that is not a setting. (The body itself stays live -- see above.) Both
@@ -721,7 +721,7 @@ GtkWidget* build_trigger_panel(App& app) {
gtk_switch_set_active(GTK_SWITCH(app.events_record_switch), app.event_recording);
gtk_widget_set_tooltip_text(app.events_record_switch,
"keep every triggered acquisition in the event cache, with a "
"picture of each; the history button goes with it");
"picture of each; the dock's Events block and page follow it");
g_signal_connect(app.events_record_switch, "notify::active",
G_CALLBACK(on_events_record_toggled), &app);
// Not `row_for()`: that expands its child, which is what a spin button wants and what a switch must not
+2 -2
View File
@@ -55,8 +55,8 @@ void install_css() {
"popover.udpscope-menu button label { min-height: 0; }\n"
"popover.udpscope-menu image { min-width: 12px; min-height: 0; }\n"
"popover.udpscope-menu separator { margin: 4px 0; }\n"
// The events pane: the row's time is what a person scans for, so it is the one thing in the
// column that is not dim; the thumbnail gets an edge, or a dark plot on a dark pane has none.
// The events page: the row's time is what a person scans for, so it is the one thing in the
// list that is not dim; the thumbnail gets an edge, or a dark plot on a dark panel has none.
".udpscope-event-title { font-weight: bold; }\n"
".udpscope-event-thumb { border: 1px solid alpha(currentColor, 0.15); border-radius: 4px; }\n"
"row.udpscope-compact button { min-height: 20px; min-width: 20px; padding: 0 3px; }\n"