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. /// fill, the bar does not.
// --- persisted layout ------------------------------------------------------- // --- 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. // 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 // 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. // controls the user sees -- the only way to check an arrangement without a screenshot.
const std::pair<GtkWidget*, const char*> named[] = { const std::pair<GtkWidget*, const char*> named[] = {
{app.trigger_toggle, "trigger"}, {app.events_toggle, "events"}, {app.trigger_toggle, "trigger"}, {app.pause_button, "pause"},
{app.pause_button, "pause"},
{app.cursors_toggle, "cursors"}, {app.sidebar_toggle, "sidebar"}, {app.cursors_toggle, "cursors"}, {app.sidebar_toggle, "sidebar"},
{app.menu_button, "menu"}, {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. // Outside the lock: it reads and writes the two plots' own windows, not the model.
sync_compare_axes(*app); 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 // 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 // `[-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 // 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.menu_button = GTK_WIDGET(gtk_builder_get_object(ui, "menu_button"));
app.sidebar_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "sidebar_toggle")); 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.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.pause_button = GTK_WIDGET(gtk_builder_get_object(ui, "pause_button"));
app.cursors_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "cursors_toggle")); 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 || if (app.menu_button == nullptr || app.sidebar_toggle == nullptr || app.trigger_toggle == nullptr ||
app.events_toggle == nullptr || app.pause_button == nullptr || app.pause_button == nullptr || app.cursors_toggle == 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 // 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. // the window with it, and `[udpscope] header:` reads whatever geometry is left.
return bar; return bar;
@@ -4947,17 +4942,6 @@ GtkWidget* build_toolbar(App& app) {
// What each control *does*, which a file cannot hold. // 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.sidebar_toggle, "toggled", G_CALLBACK(on_sidebar_toggled), &app);
g_signal_connect(app.trigger_toggle, "toggled", G_CALLBACK(on_trigger_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.pause_button, "toggled", G_CALLBACK(on_pause_toggled), &app);
g_signal_connect(app.cursors_toggle, "toggled", G_CALLBACK(on_cursors_toggled), &app); g_signal_connect(app.cursors_toggle, "toggled", G_CALLBACK(on_cursors_toggled), &app);
return bar; return bar;
@@ -4969,7 +4953,7 @@ void on_activate(GtkApplication* application, gpointer user) {
install_css(); install_css();
// The window's skeleton is a `.ui` file: the window, its two panes, the plot column with the status // 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 // 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. // into the slots by id.
g_clear_object(&app->window_ui); g_clear_object(&app->window_ui);
app->window_ui = gtk_builder_new_from_resource("/org/udpscope/ui/main-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")); 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")); GtkWidget* side_view = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_view"));
app->side_view = 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_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_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")); 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")); 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 // A missing id is the one mistake neither the build nor `GtkBuilder` sees, and every use below would
// be a null pointer. // 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->plot_box == nullptr || app->plot_overlay == nullptr || app->plot_split == nullptr ||
app->status_badge == nullptr || app->status_badge == nullptr ||
app->status_label == nullptr || app->status_info_button == 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_source_panels()` has just registered: the Source badge opens the first of them.
build_badge_bar(*app); 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 full events list is the `events` page of the slide-over (the dock's Manage button opens it), so
// (the item's "collapsable pane on the left"), revealed by trigger mode. // the plot is never squeezed by it. Once the page and the dock block both exist: nothing is cached yet,
// A *paned*: the pane's width is the user's to choose, and the plot takes what is left. // so both start insensitive -- stated by the one function that decides it rather than by whichever
// 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
// widget happened to be built with which default. // 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); 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 // 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. // 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; 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) { 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", // `available` is the rule `sync_events_availability()` states; the dock block and the page must both
what, // follow it, and `page` is the slide-over's open page.
a.events_toggle != nullptr && gtk_widget_get_visible(a.events_toggle) ? 1 : 0, const bool available = a.event_recording || a.replaying || a.events_cached;
a.events_revealer != nullptr && std::fprintf(stderr, "[events] %s: available=%d dock_sensitive=%d page_sensitive=%d page=%s\n", what,
gtk_revealer_get_reveal_child(GTK_REVEALER(a.events_revealer)) ? 1 : 0, available ? 1 : 0,
a.events_divider != nullptr ? gtk_paned_get_position(GTK_PANED(a.events_divider)) : -1, a.dock_events_box != nullptr && gtk_widget_get_sensitive(a.dock_events_box) ? 1 : 0,
signal_tick_visible(a), a.event_recording ? 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); app.dbg_signal_empty_label);
} }
{ {
// The events pane's frame, read from the window: the four widgets the `.ui` gives it, and the list // The events page's frame, read from the window: the 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 // being inside the box 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. // 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) { const auto inside = [](GtkWidget* w, GtkWidget* root) {
if (w == nullptr) return 0; if (w == nullptr) return 0;
for (GtkWidget* p = w; p != nullptr; p = gtk_widget_get_parent(p)) { 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; 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_compare != nullptr ? 1 : 0, app.events_accumulate != nullptr ? 1 : 0,
app.events_list != nullptr ? 1 : 0, app.events_revealer != nullptr ? 1 : 0, app.events_list != nullptr ? 1 : 0, inside(app.events_list, app.events_box));
inside(app.events_list, app.events_revealer));
} }
std::printf("[udpscope] panels: side=%dx%d (visible=%d) signals=%dx%d row_h=%d " std::printf("[udpscope] panels: side=%dx%d (visible=%d) signals=%dx%d row_h=%d "
"(page_current=%d) trigger=%dx%d (sensitive=%d) " "(page_current=%d) trigger=%dx%d (sensitive=%d) "
+53 -58
View File
@@ -2,8 +2,8 @@
<!-- SPDX-License-Identifier: MIT --> <!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari --> <!-- 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 The events list, the content of the slide-over's `events` page: the header with the way back to the live
actions that read the selection, and the list inside its scroller. 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 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 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()` instead. Both actions start insensitive because a selection starts empty, and `update_group_buttons()`
owns them from there. 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 A width floor, not a width: the slide-over sets the page's width, and this is what the list needs to stay
its narrowest. usable at its narrowest.
--> -->
<interface> <interface>
<object class="GtkRevealer" id="events_pane"> <object class="GtkBox" id="events_box">
<property name="transition-type">slide-right</property> <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> <child>
<object class="GtkBox" id="events_box"> <object class="GtkBox" id="events_head">
<property name="orientation">vertical</property> <property name="orientation">horizontal</property>
<property name="spacing">6</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> <child>
<object class="GtkBox" id="events_head"> <object class="GtkLabel" id="events_title">
<property name="orientation">horizontal</property> <property name="label" translatable="yes">&lt;b&gt;Events&lt;/b&gt;</property>
<property name="spacing">6</property> <property name="use-markup">1</property>
<child> <property name="hexpand">1</property>
<object class="GtkLabel" id="events_title"> <property name="halign">start</property>
<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> </object>
</child> </child>
<child> <child>
<object class="GtkBox" id="events_actions"> <object class="GtkButton" id="events_live">
<property name="orientation">horizontal</property> <property name="label" translatable="yes">Live</property>
<property name="spacing">6</property> <property name="css-classes">flat</property>
<child> <property name="tooltip-text" translatable="yes">go back to the live stream after replaying a stored event</property>
<object class="GtkButton" id="events_compare"> </object>
<property name="label" translatable="yes">Compare</property> </child>
<property name="tooltip-text" translatable="yes">the two selected events in two plots side by side, with the axes synchronised</property> </object>
<property name="hexpand">1</property> </child>
<property name="sensitive">0</property> <child>
</object> <object class="GtkBox" id="events_actions">
</child> <property name="orientation">horizontal</property>
<child> <property name="spacing">6</property>
<object class="GtkButton" id="events_accumulate"> <child>
<property name="label" translatable="yes">Accumulate</property> <object class="GtkButton" id="events_compare">
<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="label" translatable="yes">Compare</property>
<property name="hexpand">1</property> <property name="tooltip-text" translatable="yes">the two selected events in two plots side by side, with the axes synchronised</property>
<property name="sensitive">0</property> <property name="hexpand">1</property>
</object> <property name="sensitive">0</property>
</child>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkScrolledWindow" id="events_scroll"> <object class="GtkButton" id="events_accumulate">
<property name="vexpand">1</property> <property name="label" translatable="yes">Accumulate</property>
<property name="hscrollbar-policy">never</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>
<child> <property name="hexpand">1</property>
<object class="GtkListBox" id="events_list"> <property name="sensitive">0</property>
<property name="activate-on-single-click">0</property> </object>
<property name="selection-mode">multiple</property> </child>
</object> </object>
</child> </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> </object>
</child> </child>
</object> </object>
+3 -16
View File
@@ -2,8 +2,7 @@
<!-- SPDX-License-Identifier: MIT --> <!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari --> <!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!-- <!--
The header bar: the time-window control's slot, the trigger, pause and events group with the separator that The header bar: the time-window control's slot, the trigger and pause group, and the three at the far end.
marks where the *view's* controls end, 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 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 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 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. 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 There is no events control here: the event list is the `events` page of the slide-over, reached by the dock's
from the mode that produces it. Manage button.
--> -->
<interface> <interface>
<object class="GtkHeaderBar" id="header_bar"> <object class="GtkHeaderBar" id="header_bar">
@@ -39,18 +38,6 @@
<property name="tooltip-text" translatable="yes">pause acquisition</property> <property name="tooltip-text" translatable="yes">pause acquisition</property>
</object> </object>
</child> </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"> <child type="end">
<object class="GtkMenuButton" id="menu_button"> <object class="GtkMenuButton" id="menu_button">
<property name="icon-name">open-menu-symbolic</property> <property name="icon-name">open-menu-symbolic</property>
+70 -80
View File
@@ -3,8 +3,8 @@
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari --> <!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!-- <!--
The main window's skeleton: the window, the split view that puts the sidebar beside the plot, the 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) plot column (`left_column`: `plot_overlay` > `plot_box`, the slide-over panel being an overlay child added
with the status line under it, and the sidebar, which is the dock. 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 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 `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 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 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 built by code and put in place: the two plot areas, into the split inside `plot_box`; the dock's
areas, into the split inside `plot_box`; the dock's readouts, appended to `dock_page`; and the readouts and events, appended to `dock_page`; the slide-over's pages, into the panel over `plot_overlay`;
status popover, attached to `status_info`. 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()` 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 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 the plot column, not to the window, so it stays inside `left_column` under the plot.
it.
--> -->
<interface> <interface>
<object class="GtkApplicationWindow" id="main_window"> <object class="GtkApplicationWindow" id="main_window">
@@ -68,96 +67,87 @@
</object> </object>
</property> </property>
<property name="content"> <property name="content">
<object class="GtkPaned" id="events_divider"> <object class="GtkBox" id="left_column">
<property name="orientation">horizontal</property> <property name="orientation">vertical</property>
<!-- Shrinkable, or the pane's own minimum is reserved as an empty column even while it is <property name="hexpand">1</property>
closed and the plot pays for a pane nobody opened. --> <child>
<property name="shrink-start-child">1</property> <!-- The plot, and the slide-over panel that opens over its right edge. The panel is
<property name="resize-start-child">0</property> one opaque overlay child placed by `halign=end` and a 360 px width, never a
<child type="end"> full-area transparent child, which would be picked wherever its allocation is
<object class="GtkBox" id="left_column"> and swallow the plot's pan, drop and hover (docs/claude/invariants-plot-and-layout.md).
<property name="orientation">vertical</property> 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="hexpand">1</property>
<property name="vexpand">1</property>
<child> <child>
<!-- The plot, and the slide-over panel that opens over its right edge. The panel is <object class="GtkBox" id="plot_box">
one opaque overlay child placed by `halign=end` and a 360 px width, never a <property name="orientation">vertical</property>
full-area transparent child, which would be picked wherever its allocation is <property name="homogeneous">1</property>
and swallow the plot's pan, drop and hover (docs/claude/invariants-plot-and-layout.md). <property name="spacing">4</property>
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="hexpand">1</property>
<property name="vexpand">1</property> <property name="vexpand">1</property>
<child> <child>
<object class="GtkBox" id="plot_box"> <object class="GtkPaned" id="plot_split">
<property name="orientation">vertical</property> <property name="orientation">horizontal</property>
<property name="homogeneous">1</property> <property name="shrink-end-child">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> </object>
</child> </child>
</object> </object>
</child> </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> <child>
<!-- Setup is reached by the object it configures: a badge per signal on the left <object class="GtkBox" id="badge_signals">
(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="orientation">horizontal</property>
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="height-request">36</property> <property name="hexpand">1</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>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkBox" id="status_row"> <object class="GtkBox" id="badge_system">
<property name="orientation">horizontal</property> <property name="orientation">horizontal</property>
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="margin-top">2</property> <property name="halign">end</property>
<property name="margin-bottom">2</property> </object>
<property name="margin-start">6</property> </child>
<child> </object>
<object class="GtkLabel" id="status_badge"/> </child>
</child> <child>
<child> <object class="GtkBox" id="status_row">
<!-- The line is rewritten every frame and its counters grow, so it is <property name="orientation">horizontal</property>
ellipsized: a label's minimum width is its text, and an unellipsized one <property name="spacing">6</property>
would push the divider around as the numbers change. --> <property name="margin-top">2</property>
<object class="GtkLabel" id="status_label"> <property name="margin-bottom">2</property>
<property name="label">starting…</property> <property name="margin-start">6</property>
<property name="ellipsize">end</property> <child>
<property name="hexpand">1</property> <object class="GtkLabel" id="status_badge"/>
<property name="halign">fill</property> </child>
</object> <child>
</child> <!-- The line is rewritten every frame and its counters grow, so it is
<child> ellipsized: a label's minimum width is its text, and an unellipsized one
<object class="GtkMenuButton" id="status_info"> would push the divider around as the numbers change. -->
<property name="icon-name">help-about-symbolic</property> <object class="GtkLabel" id="status_label">
<property name="tooltip-text" translatable="yes">Stream and buffer detail</property> <property name="label">starting…</property>
<property name="valign">center</property> <property name="ellipsize">end</property>
</object> <property name="hexpand">1</property>
</child> <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> </object>
</child> </child>
</object> </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 /// `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. /// it was already on when the event you wanted happened.
std::unique_ptr<EventStore> event_store; std::unique_ptr<EventStore> event_store;
/// Whether the cache holds anything, refreshed once a second: what keeps the history button on the bar /// Whether the cache holds anything, refreshed once a second: what keeps the dock's Events block and the
/// for a source whose events are already written, even with recording switched off. /// `events` page live for a source whose events are already written, even with recording switched off.
bool events_cached = false; bool events_cached = false;
bool event_recording = false; bool event_recording = false;
/// The events pane: its list, the revealer that holds it, the recording switch (in the trigger /// The `events` page of the slide-over: its content box, its list, the recording switch (in the
/// panel, where the trigger's options live), and `events_thumb` -- the picture of the newest /// 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 /// 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. /// the probe measures and which the page does not put anywhere special.
GtkWidget* events_revealer = nullptr; GtkWidget* events_box = nullptr;
GtkWidget* events_list = nullptr; GtkWidget* events_list = nullptr;
GtkWidget* events_thumb = nullptr; GtkWidget* events_thumb = nullptr;
GtkWidget* events_record_switch = 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 /// 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. /// attached to each row as `udpscope-event`) and the Manage button.
GtkWidget* dock_events_box = nullptr; GtkWidget* dock_events_box = nullptr;
@@ -1228,6 +1220,9 @@ struct App {
/// it keeps for a second latch (it is counting retries, not time). /// it keeps for a second latch (it is counting retries, not time).
int dbg_events_step = 0; int dbg_events_step = 0;
int dbg_events_wait = 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. /// 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, /// 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 /// 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 search_jump(App& app, double t);
void update_search_panel(App& app); void update_search_panel(App& app);
void sync_events_availability(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 /// 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. /// because the theme is the one preference that takes effect without a restart.
void open_preferences(App& app); 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), // 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 // 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. // disagree.
#include "ui_app.hpp" #include "ui_app.hpp"
@@ -249,11 +249,16 @@ const char* dock_event_marker(const App& app, const EventRecord& r) {
return " "; 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) { 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 /// re-mark. The record is copied first, because nothing it triggers may be allowed to free the row it
/// was read from. /// 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_hexpand(name, true);
gtk_widget_set_tooltip_text(name, text.c_str()); gtk_widget_set_tooltip_text(name, text.c_str());
gtk_box_append(GTK_BOX(line), name); 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. // clock, so this is the part of the row that is not in it.
const std::string seq = "#" + std::to_string(r.seq); const std::string seq = "#" + std::to_string(r.seq);
GtkWidget* num = fixed_label(seq.c_str(), kEventSeqChars, GTK_ALIGN_END); GtkWidget* num = fixed_label(seq.c_str(), kEventSeqChars, GTK_ALIGN_END);
+31 -80
View File
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari // 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" #include "ui_app.hpp"
@@ -32,68 +34,19 @@ GtkWidget* make_compare_area(App& app) {
return widget; return widget;
} }
/// Whether the event history is reachable at all, and the one place that decides it. It has exactly /// Whether the event history is reachable at all, and the one place that decides it. Recording on (the
/// two preconditions: trigger mode (there are no events without a trigger) and recording (a disabled /// cache can still grow), an event being read (a replay), or something already in the cache (an event on
/// cache is empty, and an empty column beside the plot is what "off" must not look like). Both the /// disk outlives the switch that wrote it) -- not trigger mode: a stored event being examined is a file,
/// toolbar button and the pane follow from it, so they cannot disagree -- and turning recording off /// and it has no trigger. The dock's Events block and the `events` page follow from it, so they cannot
/// while the pane is open takes the button and the pane with it rather than leaving a stale list. /// 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) { 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; const bool available = app.event_recording || app.replaying || app.events_cached;
dock_events_set_sensitive(app, available); dock_events_set_sensitive(app, available);
gtk_widget_set_visible(app.events_toggle, available); if (app.events_box != nullptr) gtk_widget_set_sensitive(app.events_box, 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);
}
} }
/// Writes the held event into the cache: its table, a sidecar of what the list shows, and a /// 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, " + set_notice(app, "event cached: " + std::to_string(table.columns.size()) + " signals, " +
std::to_string(table.rows()) + " samples", std::to_string(table.rows()) + " samples",
3); 3);
// The dock follows every latch; the pane's rows (with pictures) only while the pane is on screen: // The dock follows every latch; the page's rows (with pictures) only while the page is open:
// rebuilding them per latch for a column nobody opened is work the tick owes nothing, and the toggle's // rebuilding them per latch for a list nobody opened is work the tick owes nothing, and the Manage
// own handler refreshes when it is asked for. // button refreshes the list when it is asked for.
if (app.events_revealer != nullptr && if (slideover_page(app) == "events") {
gtk_revealer_get_child_revealed(GTK_REVEALER(app.events_revealer))) {
refresh_events_pane(app); refresh_events_pane(app);
} else { } else {
dock_rebuild_events(app); 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. // 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 // 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. // 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); sync_events_availability(app);
set_notice(app, "replaying " + record.stem + " (" + set_notice(app, "replaying " + record.stem + " (" +
std::to_string(record.signals) + " signals, " + 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 /// 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 /// 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. /// cannot show thirty of them.
static void rebuild_events_pane_list(App& app, std::vector<EventRecord>& records) { 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_widget_set_halign(name, GTK_ALIGN_START);
gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END); 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 // 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_label_set_max_width_chars(GTK_LABEL(name), 22);
gtk_widget_add_css_class(name, "udpscope-event-title"); gtk_widget_add_css_class(name, "udpscope-event-title");
gtk_box_append(GTK_BOX(box), name); 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); mark_events_on_screen(app);
} }
/// The pane itself: a collapsible column on the left, a header and the list of events -- each row /// The events list: a header and the list of events, each row carrying its own picture. Built once with
/// carrying its own picture. Built once with the rest of the window, and opened on demand from the /// the rest of the window and returned as the content of the slide-over's `events` page, which the
/// toolbar's history button, which is itself only there while trigger mode and recording are both /// dock's Manage button opens.
/// on (`sync_events_availability()`).
GtkWidget* build_events_pane(App& app) { 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 // The list's frame -- the header with the way back to the live stream, the two actions that read the
// two actions that read the selection, and the list inside its scroller -- is a UI file. The rows are not: // 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 // 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. // 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"); 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_compare = GTK_WIDGET(gtk_builder_get_object(ui, "events_compare"));
app.events_accumulate = GTK_WIDGET(gtk_builder_get_object(ui, "events_accumulate")); app.events_accumulate = GTK_WIDGET(gtk_builder_get_object(ui, "events_accumulate"));
GtkWidget* live = GTK_WIDGET(gtk_builder_get_object(ui, "events_live")); GtkWidget* live = GTK_WIDGET(gtk_builder_get_object(ui, "events_live"));
app.events_list = GTK_WIDGET(gtk_builder_get_object(ui, "events_list")); 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`). // 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")); 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) { 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. // 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 // 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)); update_group_buttons(*static_cast<App*>(user));
}), }),
&app); &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 /// 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); write_trigger_widgets(app);
// The state is applied by *pressing the same control a user would*: the handler is what arms, tells // 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 // `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. // would emit `toggled` into a handler that returns at once and nothing would happen.
if (app.trigger_toggle != nullptr) { 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->window == nullptr) return G_SOURCE_REMOVE;
if (a->dbg_events_step == 0) { if (a->dbg_events_step == 0) {
// Before anything: streaming, no trigger, recording **off** (the default) -- so no button, no // Before anything: streaming, no trigger, recording **off** (the default) -- so the dock's block and
// pane, and nothing written to disk that the user did not ask to have written. // 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"); report_events_availability(*a, "live, recording off");
// The three clicks a user makes: recording on in the trigger panel, trigger mode on, then the // The clicks a user makes: recording on in the trigger panel, trigger mode on, then the dock's
// history button (which is only there once the first two have happened -- that is the item's rule, // Manage button (the `events` page is reachable at any time; it is insensitive until something is
// and this is where it is asserted). // recorded or cached, and this is where that rule is asserted).
gtk_switch_set_active(GTK_SWITCH(a->events_record_switch), TRUE); 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 // 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 // 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); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
} }
report_events_availability(*a, "after trigger on"); report_events_availability(*a, "after trigger on");
if (a->events_toggle != nullptr) { slideover_open(*a, "events");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->events_toggle), TRUE); // Read straight after the open, before anything else can call the availability rule.
} report_events_availability(*a, "after events page opened");
// Read straight after the click, before anything else can call the availability rule: this is the // ... and again a few frames later, still with no replay in the way: this is the *open alone*
// 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*
// path a user takes, which a later replay's own availability call used to mask. // path a user takes, which a later replay's own availability call used to mask.
events_probe_next(*a, 5, 500); events_probe_next(*a, 5, 500);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
if (a->dbg_events_step == 5) { if (a->dbg_events_step == 5) {
GtkWidget* box = a->events_revealer != nullptr std::fprintf(stderr, "[events] open alone: page=%s page_box=%d\n",
? gtk_revealer_get_child(GTK_REVEALER(a->events_revealer)) slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(),
: nullptr; a->events_box != nullptr ? gtk_widget_get_width(a->events_box) : -1);
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);
events_probe_next(*a, 1, 2500); events_probe_next(*a, 1, 2500);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
@@ -5748,11 +5743,15 @@ gboolean events_probe_step(gpointer user) {
} }
std::fprintf(stderr, std::fprintf(stderr,
" newest: stem=%s dur=%.6f pre=%.6f post=%.6f edge=%.6f signals=%zu samples=%zu " " 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.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, r.rate_hz, static_cast<unsigned long long>(r.seq), thumb_bytes,
a->event_recording ? 1 : 0, a->event_recording ? 1 : 0, slideover_page(*a) == "events" ? 1 : 0);
gtk_revealer_get_reveal_child(GTK_REVEALER(a->events_revealer)) ? 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::fprintf(stderr, "\n");
std::fflush(stderr); std::fflush(stderr);
@@ -5799,18 +5798,14 @@ gboolean events_probe_step(gpointer user) {
const int selected_now = int(g_list_length(still)); const int selected_now = int(g_list_length(still));
g_list_free(still); g_list_free(still);
std::fprintf(stderr, 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 " "window=%.3f axis=[%.3f,%.3f] marker=%.3f "
"thumbnail=%d thumb_size=%dx%d thumb_visible=%d rows_kept=%d selected=%d/%d\n", "thumbnail=%d thumb_size=%dx%d thumb_visible=%d rows_kept=%d selected=%d/%d\n",
a->replaying ? 1 : 0, a->replaying ? 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(),
a->events_divider != nullptr ? gtk_paned_get_position(GTK_PANED(a->events_divider)) // The page's *rendered* width: what it actually got -- an open page with a zero-width
: -1, // list is still no list.
// The pane's *rendered* width: the position is what we asked for, this is what the a->events_box != nullptr ? gtk_widget_get_width(a->events_box) : -1,
// 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,
signal_tick_visible(*a), a->rows->time_window(), signal_tick_visible(*a), a->rows->time_window(),
a->rows->row(0).plot != nullptr a->rows->row(0).plot != nullptr
? a->rows->row(0).plot->x_range(a->rows->row(0).axis_x).min ? 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, std::fprintf(stderr, "[events] live again: replaying=%d waveform=%d\n", a->replaying ? 1 : 0,
a->waveform_name.empty() ? 0 : 1); a->waveform_name.empty() ? 0 : 1);
report_live_restored(*a, "after replay"); report_live_restored(*a, "after replay");
// The pane's *other* precondition, through the real switch, in both trigger states: disabling // The block'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 // recording with an empty cache has to grey the dock's block and the page, and arming the trigger
// bring back a pane over an empty cache. // afterwards must not wake them over an empty cache.
gtk_switch_set_active(GTK_SWITCH(a->events_record_switch), FALSE); gtk_switch_set_active(GTK_SWITCH(a->events_record_switch), FALSE);
report_events_availability(*a, "recording off, trigger off"); report_events_availability(*a, "recording off, trigger off");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE); 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; return G_SOURCE_REMOVE;
} }
a->dbg_events_wait = 0; a->dbg_events_wait = 0;
// Open the pane first: its list is only rebuilt while the pane is *up*, so a selection made against // Open the page first: its list is only rebuilt while the page is *open* (or by the Manage button), so
// a collapsed pane selects rows that are not there. // a selection made against a closed page selects rows that are not current.
if (a->events_toggle != nullptr && if (slideover_page(*a) != "events") {
!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a->events_toggle))) { refresh_events_pane(*a);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->events_toggle), TRUE); slideover_open(*a, "events");
} }
// The selection *is* the input, so select the two newest rows the way a user does and press the // 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. // 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::lock_guard<std::mutex> lock(a->model->mutex());
std::fprintf(stderr, std::fprintf(stderr,
"[events] compare: kind=%d rows=%d channels=%zu samples0=%llu traces0=%d traces1=%d " "[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(), static_cast<int>(a->group_kind), a->rows->row_count(),
a->model->channels().size(), a->model->channels().size(),
static_cast<unsigned long long>(a->model->live().values(0).produced()), 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(), group_visible_traces(*a, 0), group_visible_traces(*a, 1), a->rows->time_window(),
a->event_recording ? 1 : 0, 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"); report_dock_events(*a, "comparison up");
events_group_probe_next(*a, 46, 400); events_group_probe_next(*a, 46, 400);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
-3
View File
@@ -656,9 +656,6 @@ GtkWidget* build_signal_panel(App& app) {
return box; 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 /// 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 /// acquire -- the samples are stored -- so the tick goes; in live mode it is the control that decides
/// what is acquired at all. /// 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 // 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 // `trigger panel sensitive` line is now a regression guard, and a `0` there means the
// configuration has been locked out again. // configuration has been locked out again.
// The event-history button is part of trigger mode and the pane it opens is *on demand*; leaving // The dock's Events block and the `events` page follow what is recorded or cached, not trigger mode
// trigger mode takes the button away and collapses the pane, so going back to streaming is not a // alone: leaving trigger mode does not empty the cache, and a replay is not in trigger mode at all.
// column of stale events beside the plot. Recording is the other half of that answer, so this is // Recording is the other half of that answer, so this is `sync_events_availability()`'s call and not
// `sync_events_availability()`'s call and not the trigger's alone. // the trigger's alone.
sync_events_availability(*app); sync_events_availability(*app);
// Arming is a *runtime* control: with trigger mode off there is no engine to arm, and the arm buttons // 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 // 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_switch_set_active(GTK_SWITCH(app.events_record_switch), app.event_recording);
gtk_widget_set_tooltip_text(app.events_record_switch, gtk_widget_set_tooltip_text(app.events_record_switch,
"keep every triggered acquisition in the event cache, with a " "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_signal_connect(app.events_record_switch, "notify::active",
G_CALLBACK(on_events_record_toggled), &app); 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 // 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 button label { min-height: 0; }\n"
"popover.udpscope-menu image { min-width: 12px; min-height: 0; }\n" "popover.udpscope-menu image { min-width: 12px; min-height: 0; }\n"
"popover.udpscope-menu separator { margin: 4px 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 // The events page: 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. // 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-title { font-weight: bold; }\n"
".udpscope-event-thumb { border: 1px solid alpha(currentColor, 0.15); border-radius: 4px; }\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" "row.udpscope-compact button { min-height: 20px; min-width: 20px; padding: 0 3px; }\n"