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:
1 parent
cb098eff75
commit
8c866300d8
12 files changed
+131
-237
No files matched your search
+24
-43
@@ -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) "
|
||||||
|
|||||||
@@ -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,13 +15,10 @@
|
|||||||
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">
|
|
||||||
<property name="transition-type">slide-right</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox" id="events_box">
|
<object class="GtkBox" id="events_box">
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
@@ -85,6 +82,4 @@
|
|||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</interface>
|
</interface>
|
||||||
+3
-16
@@ -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>
|
||||||
|
|||||||
+6
-16
@@ -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,13 +67,6 @@
|
|||||||
</object>
|
</object>
|
||||||
</property>
|
</property>
|
||||||
<property name="content">
|
<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">
|
<object class="GtkBox" id="left_column">
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<property name="hexpand">1</property>
|
<property name="hexpand">1</property>
|
||||||
@@ -161,8 +153,6 @@
|
|||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</property>
|
</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
|||||||
+9
-16
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"
|
||||||
|
|||||||
Reference in new issue
Block a user