added loading screen

This commit is contained in:
Martino Ferrari committed 2026-10-14 00:38:24 +02:00
1 parent 881b52748e
commit 50a1ddca62
10 files changed
+249 -1

No files matched your search

+10 -1
View File
@@ -30,13 +30,22 @@ int main(int argc, char** argv) {
// declining must cost no buffer and no socket, and nothing below has to cope with "no source".
// `--open` is the other answer to that question, and the chooser knows it: its Waveforms page
// fills `open_waveform` in instead of the connection fields.
if (!cfg.server_specified && cfg.open_waveform.empty() && !udpscope::choose_source(cfg)) return 0;
const bool asked = !cfg.server_specified && cfg.open_waveform.empty();
if (asked && !udpscope::choose_source(cfg)) return 0;
// A source named on the command line skips the chooser, but its *stored* configuration still
// applies: the buffer size, the clock and the capture ring are decided here, before the model --
// which is the only moment they can be -- and a saved host whose buffer was ignored because the
// address came from the command line is an edit that looks like it never took effect.
if (cfg.server_specified) udpscope::apply_stored_settings(cfg);
// Everything from here to `gtk_window_present()` inside `run_ui()` runs with no main loop: the
// buffer is allocated, a recording is read, then the whole UI is built. The chooser's window is
// already gone, so without this the screen is empty for all of it -- "the dialog disappeared and
// nothing happened", which at a few gigabytes of buffer is ten seconds long. Only on the path
// that *asked*: a source named with `--server` had no window to lose, and the status line after
// the present is the right place to say what such a run is waiting for.
if (asked) udpscope::loading_show(cfg);
udpscope::ScopeModel model(cfg);
udpscope::RowSet rows;
rows.set_row_count(cfg.plots);
+80
View File
@@ -793,6 +793,86 @@ void apply_stored_settings(Config& cfg) {
}
}
namespace {
/// The loading window and its builder. File scope because its life spans two calls made from two
/// translation units -- `main()` raises it, `run_ui()` takes it down once the real window is up --
/// and there is exactly one of it: the gap it covers happens once per run.
GtkWidget* g_loading_window = nullptr;
GtkBuilder* g_loading_ui = nullptr;
/// Runs the default main context for `ms`, the chooser's nested-loop idiom again. Pumping pending
/// events with `g_main_context_iteration()` is not enough: a window needs a frame-clock cycle to be
/// mapped and drawn, and the frame clock only ticks while a loop runs.
void pump_main_context(unsigned ms) {
GMainLoop* loop = g_main_loop_new(nullptr, FALSE);
g_timeout_add(
ms,
+[](gpointer user) {
g_main_loop_quit(static_cast<GMainLoop*>(user));
return G_SOURCE_REMOVE;
},
loop);
g_main_loop_run(loop);
g_main_loop_unref(loop);
}
} // namespace
void loading_show(const Config& cfg) {
if (g_loading_window != nullptr) return;
g_loading_ui = gtk_builder_new_from_resource("/org/udpscope/ui/loading.ui");
g_loading_window = GTK_WIDGET(gtk_builder_get_object(g_loading_ui, "loading_window"));
GtkWidget* title = GTK_WIDGET(gtk_builder_get_object(g_loading_ui, "loading_title"));
GtkWidget* detail = GTK_WIDGET(gtk_builder_get_object(g_loading_ui, "loading_detail"));
if (g_loading_window == nullptr || title == nullptr || detail == nullptr) {
std::fprintf(stderr, "[sources] loading ui: loading.ui is missing an id this code reads\n");
std::fflush(stderr);
g_loading_window = nullptr;
g_clear_object(&g_loading_ui);
return;
}
// The wording names the *wait*, not the destination: "connecting" with nothing else said is what
// this window was accused of being, and the buffer size is the number that explains the ten
// seconds.
std::string head;
std::string sub;
if (cfg.open_waveform.empty()) {
head = "Connecting to " + cfg.server + ":" + std::to_string(cfg.port);
if (!cfg.multicast.empty()) head += " (" + cfg.multicast + ")";
char buf[128];
std::snprintf(buf, sizeof(buf), "planning the %.0f MB sample buffer", cfg.buffer_mb);
sub = buf;
} else {
const std::size_t slash = cfg.open_waveform.rfind('/');
head = "Opening " + (slash == std::string::npos ? cfg.open_waveform
: cfg.open_waveform.substr(slash + 1));
sub = "reading the recording";
}
gtk_label_set_text(GTK_LABEL(title), head.c_str());
gtk_label_set_text(GTK_LABEL(detail), sub.c_str());
gtk_window_present(GTK_WINDOW(g_loading_window));
// Painted *here*, before the caller blocks. Everything this window covers -- the buffer
// allocation, the first CONFIG, a recording read -- runs on a thread that never returns to a main
// loop, so a window presented and not yet drawn would be an empty rectangle for the whole wait,
// which is the defect this is meant to fix rather than restate.
pump_main_context(180);
std::fprintf(stderr, "[sources] loading: title='%s' detail='%s'\n", head.c_str(), sub.c_str());
std::fflush(stderr);
}
void loading_hide() {
if (g_loading_window == nullptr) return;
GtkWidget* w = g_loading_window;
g_loading_window = nullptr; // before the destroy, as the chooser's own close does
gtk_window_destroy(GTK_WINDOW(w));
// The window belonged to the builder's object list, so the builder goes after the destroy.
g_clear_object(&g_loading_ui);
std::fprintf(stderr, "[sources] loading: closed\n");
std::fflush(stderr);
}
bool choose_source(Config& cfg) {
const std::vector<KnownSource> known = known_sources();
+13
View File
@@ -26,4 +26,17 @@ bool choose_source(Config& cfg);
/// names signals, so it is loaded by the app once a CONFIG has described them.
void apply_stored_settings(Config& cfg);
/// Puts a small window on screen saying what `cfg` is about to be opened as, and paints it before
/// returning. Call it after the chooser has answered and *before* the buffer is planned or a
/// recording is read: both of those run with no main loop, so nothing drawn later than this would
/// appear until they are over.
///
/// Returns having pumped the main context long enough for the window to be mapped and drawn. The
/// caller owes a `loading_hide()`, which `run_ui()` does once the main window is presented.
void loading_show(const Config& cfg);
/// Takes the loading window down. A no-op when none is up, so the paths that never show one -- a
/// source named with `--server` -- need no notion of it.
void loading_hide();
} // namespace udpscope
+7
View File
@@ -33,6 +33,7 @@
#include "model.hpp"
#include "plot_host.hpp"
#include "search.hpp"
#include "source_dialog.hpp"
#include <adwaita.h>
#include <gtk/gtk.h>
@@ -4838,6 +4839,12 @@ void on_activate(GtkApplication* application, gpointer user) {
gtk_window_present(GTK_WINDOW(app->window));
// The loading window covered the gap from the chooser to here -- the buffer, the file read and
// the whole of the construction above, none of which returns to a main loop. It goes down only
// now, *after* the present: destroying it before would leave the screen empty again for the
// frame or two the real window takes to map. A no-op when there was no chooser.
loading_hide();
// What the window is waiting for, said *before* the first tick. The stream's first CONFIG plans
// the buffer on the producer thread, which holds the model's mutex for the whole of it -- at a few
// gigabytes that is around a second per gigabyte, and the frame tick cannot read the model while
+69
View File
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
What is on screen between the chooser closing and the main window appearing: the only window that
exists while the buffer is planned or a recording is read. Both of those run with no main loop, so
this window is painted once and then stays still — which is why it says what is happening in words
rather than only spinning, and why there is no progress bar: nothing can move it.
Deliberately undecorated (`deletable=0`, no title buttons): a close button that cannot be serviced
until the work finishes is a lie, and the work is not cancellable once the buffer is being planned.
-->
<interface>
<object class="AdwWindow" id="loading_window">
<property name="title" translatable="yes">udpscope</property>
<property name="default-width">420</property>
<property name="default-height">220</property>
<property name="resizable">0</property>
<property name="deletable">0</property>
<property name="content">
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<property name="show-start-title-buttons">0</property>
<property name="show-end-title-buttons">0</property>
<property name="title-widget">
<object class="AdwWindowTitle">
<property name="title" translatable="yes">udpscope</property>
</object>
</property>
</object>
</child>
<property name="content">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">14</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin-start">24</property>
<property name="margin-end">24</property>
<property name="margin-top">12</property>
<property name="margin-bottom">24</property>
<child>
<object class="GtkSpinner">
<property name="spinning">1</property>
<property name="width-request">36</property>
<property name="height-request">36</property>
</object>
</child>
<child>
<object class="GtkLabel" id="loading_title">
<property name="wrap">1</property>
<property name="justify">center</property>
<property name="css-classes">title-4</property>
</object>
</child>
<child>
<object class="GtkLabel" id="loading_detail">
<property name="wrap">1</property>
<property name="justify">center</property>
<property name="css-classes">dim-label</property>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</interface>
+1
View File
@@ -14,6 +14,7 @@
<file preprocess="xml-stripblanks">help.ui</file>
<file preprocess="xml-stripblanks">source-form.ui</file>
<file preprocess="xml-stripblanks">source-chooser.ui</file>
<file preprocess="xml-stripblanks">loading.ui</file>
<file preprocess="xml-stripblanks">main-window.ui</file>
<file preprocess="xml-stripblanks">cursors-panel.ui</file>
<file preprocess="xml-stripblanks">measures-panel.ui</file>