The hand-rolled grid existed to avoid GTK's deprecated GtkShortcutsWindow; libadwaita 1.8's AdwShortcutsDialog is the supported replacement, so use the platform widget and get its search, sectioning and shortcut labels for free. The table strings become GTK accelerator syntax, since the shortcut label parses them. The six mouse rows cannot: no accelerator syntax names a wheel or a drag, so they carry an empty accelerator and prose in the new subtitle field. adw_init() is called in run_ui() and in choose_source() (replacing its gtk_init()), which means the whole app is drawn with libadwaita's stylesheet. Verified settled: viewport.w 870..870, axes.w changes 2, x-desync 0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
425 lines
18 KiB
C++
425 lines
18 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#include "source_dialog.hpp"
|
|
|
|
#include "layout_store.hpp"
|
|
|
|
#include <adwaita.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace udpscope {
|
|
namespace {
|
|
|
|
/// A chooser row's source becomes the connection to open. The interface comes along with the
|
|
/// group: it is the local address the join happens on, so storing one without the other would
|
|
/// reconnect to the right group on the wrong path.
|
|
void apply_source(Config& cfg, const KnownSource& src) {
|
|
cfg.server = src.host;
|
|
cfg.port = src.port;
|
|
cfg.multicast = src.multicast;
|
|
cfg.interface_addr = src.interface_addr;
|
|
}
|
|
|
|
/// One line per known source on stderr, in the style of the probes in `ui.cpp`.
|
|
void report_sources(const std::vector<KnownSource>& known) {
|
|
std::fprintf(stderr, "[sources] %zu known:", known.size());
|
|
for (const KnownSource& s : known) {
|
|
std::fprintf(stderr, " %s:%u plots=%d signals=%d", s.host.c_str(), s.port, s.rows, s.signals);
|
|
if (!s.multicast.empty()) std::fprintf(stderr, " multicast=%s", s.multicast.c_str());
|
|
if (!s.interface_addr.empty()) {
|
|
std::fprintf(stderr, " interface=%s", s.interface_addr.c_str());
|
|
}
|
|
std::fprintf(stderr, " |");
|
|
}
|
|
std::fprintf(stderr, "\n");
|
|
std::fflush(stderr);
|
|
}
|
|
|
|
/// The chooser's own state for the life of its nested loop. A plain local in `choose_source()`:
|
|
/// the loop runs inside that call, so nothing here outlives the stack frame and there is nothing
|
|
/// to free on a handler path.
|
|
struct Chooser {
|
|
Config* cfg = nullptr;
|
|
GMainLoop* loop = nullptr;
|
|
bool chosen = false;
|
|
GtkWidget* window = nullptr;
|
|
GtkWidget* list = nullptr;
|
|
std::vector<KnownSource> known;
|
|
/// The connection form, created on demand. Members of the chooser rather than a heap struct:
|
|
/// the chooser outlives the form by construction, so there is nothing to free on a close path
|
|
/// -- and a dialog whose context is freed by a handler is how this project earned a use-after
|
|
/// -free in the math editor.
|
|
GtkWidget* form = nullptr;
|
|
GtkWidget* form_server = nullptr;
|
|
GtkWidget* form_port = nullptr;
|
|
GtkWidget* form_group = nullptr;
|
|
GtkWidget* form_iface = nullptr;
|
|
GtkWidget* form_connect = nullptr;
|
|
/// `UDPSCOPE_PROBE_PICK_MS`'s timeout, kept so a pick made before it fires can cancel it. The
|
|
/// probe clears it when it runs: `g_source_remove()` on an id whose source already returned
|
|
/// `G_SOURCE_REMOVE` is a GLib critical, not a no-op.
|
|
guint pick_source = 0;
|
|
};
|
|
|
|
void chooser_finish(Chooser& c, bool chosen) {
|
|
c.chosen = chosen;
|
|
// Destroy the form first: any path that reaches here while the form is up -- a row click, the
|
|
// Escape key -- would otherwise leave a window whose four handlers are bound to a dead stack
|
|
// frame. `gtk_window_destroy()` rather than `gtk_window_close()`: the pointer is already
|
|
// cleared, so the `close-request` handler is not needed here, and `close()` on an unrealized
|
|
// window returns having done nothing.
|
|
if (c.form != nullptr) {
|
|
GtkWidget* w = c.form;
|
|
c.form = nullptr; // before the destroy, so the `close-request` handler cannot re-enter
|
|
gtk_window_destroy(GTK_WINDOW(w));
|
|
}
|
|
if (c.window != nullptr) {
|
|
GtkWidget* w = c.window;
|
|
c.window = nullptr; // before the destroy: `close-request` must not re-enter this
|
|
gtk_window_destroy(GTK_WINDOW(w));
|
|
}
|
|
if (c.loop != nullptr) g_main_loop_quit(c.loop);
|
|
}
|
|
|
|
void chooser_populate(Chooser& c);
|
|
|
|
void on_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
|
|
Chooser& c = *static_cast<Chooser*>(user);
|
|
const int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(row), "source-index"));
|
|
if (idx < 0 || static_cast<std::size_t>(idx) >= c.known.size()) return;
|
|
apply_source(*c.cfg, c.known[static_cast<std::size_t>(idx)]);
|
|
chooser_finish(c, true);
|
|
}
|
|
|
|
void on_forget_clicked(GtkButton* button, gpointer user) {
|
|
Chooser& c = *static_cast<Chooser*>(user);
|
|
const int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "source-index"));
|
|
if (idx < 0 || static_cast<std::size_t>(idx) >= c.known.size()) return;
|
|
forget_source(c.known[static_cast<std::size_t>(idx)]);
|
|
// Repopulate rather than remove one row: the rows carry their index into `known`, and dropping
|
|
// an entry renumbers every later one. Re-reading is also the honest answer to "did it go".
|
|
chooser_populate(c);
|
|
}
|
|
|
|
/// Fills the list box from `known_sources()`. Called again after a forget.
|
|
void chooser_populate(Chooser& c) {
|
|
while (GtkWidget* child = gtk_widget_get_first_child(c.list)) {
|
|
gtk_list_box_remove(GTK_LIST_BOX(c.list), child);
|
|
}
|
|
c.known = known_sources();
|
|
|
|
if (c.known.empty()) {
|
|
GtkWidget* empty = gtk_label_new("No saved sources yet.");
|
|
gtk_widget_add_css_class(empty, "dim-label");
|
|
gtk_widget_set_margin_top(empty, 24);
|
|
gtk_widget_set_margin_bottom(empty, 24);
|
|
// GTK4 auto-wraps any non-row widget appended to a `GtkListBox` in a row, so this is
|
|
// activatable -- but the wrapped row carries no `"source-index"` data, `g_object_get_data()`
|
|
// returns null, the index reads 0, and `on_row_activated`'s bounds check refuses it.
|
|
gtk_list_box_append(GTK_LIST_BOX(c.list), empty);
|
|
gtk_list_box_set_selection_mode(GTK_LIST_BOX(c.list), GTK_SELECTION_NONE);
|
|
return;
|
|
}
|
|
gtk_list_box_set_selection_mode(GTK_LIST_BOX(c.list), GTK_SELECTION_SINGLE);
|
|
|
|
for (std::size_t i = 0; i < c.known.size(); ++i) {
|
|
const KnownSource& s = c.known[i];
|
|
GtkWidget* row = gtk_list_box_row_new();
|
|
g_object_set_data(G_OBJECT(row), "source-index", GINT_TO_POINTER(static_cast<int>(i)));
|
|
|
|
GtkWidget* line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
|
|
gtk_widget_set_margin_start(line, 12);
|
|
gtk_widget_set_margin_end(line, 6);
|
|
gtk_widget_set_margin_top(line, 6);
|
|
gtk_widget_set_margin_bottom(line, 6);
|
|
|
|
GtkWidget* text = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
|
|
gtk_widget_set_hexpand(text, TRUE);
|
|
|
|
// The *server*, even for a multicast session: that is the host the file is named after and
|
|
// the one CONFIG is fetched from. Titling the row by the group would name it after something
|
|
// that is not its identity.
|
|
const std::string title = s.host + ":" + std::to_string(s.port);
|
|
GtkWidget* title_label = gtk_label_new(title.c_str());
|
|
gtk_widget_set_halign(title_label, GTK_ALIGN_START);
|
|
gtk_box_append(GTK_BOX(text), title_label);
|
|
|
|
std::string sub = std::to_string(s.rows) + (s.rows == 1 ? " plot, " : " plots, ") +
|
|
std::to_string(s.signals) + (s.signals == 1 ? " signal" : " signals");
|
|
if (!s.multicast.empty()) sub += ", multicast " + s.multicast;
|
|
GtkWidget* sub_label = gtk_label_new(sub.c_str());
|
|
gtk_widget_add_css_class(sub_label, "dim-label");
|
|
gtk_widget_set_halign(sub_label, GTK_ALIGN_START);
|
|
gtk_box_append(GTK_BOX(text), sub_label);
|
|
gtk_box_append(GTK_BOX(line), text);
|
|
|
|
GtkWidget* forget = gtk_button_new_from_icon_name("window-close-symbolic");
|
|
gtk_widget_set_tooltip_text(forget, "Forget this source (deletes its saved session)");
|
|
gtk_widget_add_css_class(forget, "flat");
|
|
gtk_widget_set_valign(forget, GTK_ALIGN_CENTER);
|
|
g_object_set_data(G_OBJECT(forget), "source-index", GINT_TO_POINTER(static_cast<int>(i)));
|
|
g_signal_connect(forget, "clicked", G_CALLBACK(on_forget_clicked), &c);
|
|
gtk_box_append(GTK_BOX(line), forget);
|
|
|
|
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), line);
|
|
gtk_list_box_append(GTK_LIST_BOX(c.list), row);
|
|
}
|
|
}
|
|
|
|
void form_close(Chooser& c) {
|
|
if (c.form == nullptr) return;
|
|
GtkWidget* w = c.form;
|
|
c.form = nullptr; // before the close, so the handler cannot re-enter
|
|
gtk_window_close(GTK_WINDOW(w));
|
|
}
|
|
|
|
void on_form_server_changed(GtkEditable* entry, gpointer user) {
|
|
Chooser& c = *static_cast<Chooser*>(user);
|
|
if (c.form_connect == nullptr) return;
|
|
// The only validation here. A *malformed* address is the acquisition layer's to report -- it
|
|
// already does, and the starvation notice covers a source that takes the socket and says
|
|
// nothing -- so checking it twice would be two expressions of one rule.
|
|
gtk_widget_set_sensitive(c.form_connect, gtk_entry_buffer_get_length(
|
|
gtk_entry_get_buffer(GTK_ENTRY(entry))) > 0);
|
|
}
|
|
|
|
void on_form_connect(GtkButton*, gpointer user) {
|
|
Chooser& c = *static_cast<Chooser*>(user);
|
|
const char* server = gtk_editable_get_text(GTK_EDITABLE(c.form_server));
|
|
if (server == nullptr || server[0] == '\0') return;
|
|
c.cfg->server = server;
|
|
c.cfg->port = static_cast<std::uint16_t>(
|
|
gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(c.form_port)));
|
|
c.cfg->multicast = gtk_editable_get_text(GTK_EDITABLE(c.form_group));
|
|
c.cfg->interface_addr = gtk_editable_get_text(GTK_EDITABLE(c.form_iface));
|
|
form_close(c);
|
|
chooser_finish(c, true);
|
|
}
|
|
|
|
/// A labelled row for the form's grid.
|
|
GtkWidget* form_field(GtkWidget* grid, int row, const char* label, GtkWidget* widget) {
|
|
GtkWidget* l = gtk_label_new(label);
|
|
gtk_widget_set_halign(l, GTK_ALIGN_END);
|
|
gtk_grid_attach(GTK_GRID(grid), l, 0, row, 1, 1);
|
|
gtk_widget_set_hexpand(widget, TRUE);
|
|
gtk_grid_attach(GTK_GRID(grid), widget, 1, row, 1, 1);
|
|
return widget;
|
|
}
|
|
|
|
void open_form(Chooser& c) {
|
|
if (c.form != nullptr) {
|
|
gtk_window_present(GTK_WINDOW(c.form));
|
|
return;
|
|
}
|
|
c.form = gtk_window_new();
|
|
gtk_window_set_title(GTK_WINDOW(c.form), "New host");
|
|
gtk_window_set_transient_for(GTK_WINDOW(c.form), GTK_WINDOW(c.window));
|
|
gtk_window_set_modal(GTK_WINDOW(c.form), TRUE);
|
|
|
|
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
|
|
gtk_widget_set_margin_start(box, 12);
|
|
gtk_widget_set_margin_end(box, 12);
|
|
gtk_widget_set_margin_top(box, 12);
|
|
gtk_widget_set_margin_bottom(box, 12);
|
|
gtk_window_set_child(GTK_WINDOW(c.form), box);
|
|
|
|
GtkWidget* grid = gtk_grid_new();
|
|
gtk_grid_set_row_spacing(GTK_GRID(grid), 6);
|
|
gtk_grid_set_column_spacing(GTK_GRID(grid), 12);
|
|
gtk_box_append(GTK_BOX(box), grid);
|
|
|
|
c.form_server = form_field(grid, 0, "Server", gtk_entry_new());
|
|
gtk_editable_set_text(GTK_EDITABLE(c.form_server), "127.0.0.1");
|
|
// A spin button holds no decimals by default, which is right for a port and wrong for every
|
|
// other numeric field in this project -- see the math dialog's fields.
|
|
c.form_port = form_field(grid, 1, "Port", gtk_spin_button_new_with_range(1, 65535, 1));
|
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_port), 8000);
|
|
c.form_group = form_field(grid, 2, "Multicast group", gtk_entry_new());
|
|
gtk_entry_set_placeholder_text(GTK_ENTRY(c.form_group), "empty = unicast");
|
|
c.form_iface = form_field(grid, 3, "Interface", gtk_entry_new());
|
|
gtk_entry_set_placeholder_text(GTK_ENTRY(c.form_iface), "empty = default route");
|
|
|
|
GtkWidget* actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
|
gtk_widget_set_halign(actions, GTK_ALIGN_END);
|
|
GtkWidget* cancel = gtk_button_new_with_label("Cancel");
|
|
g_signal_connect(cancel, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
|
form_close(*static_cast<Chooser*>(user));
|
|
}),
|
|
&c);
|
|
c.form_connect = gtk_button_new_with_label("Connect");
|
|
g_signal_connect(c.form_connect, "clicked", G_CALLBACK(on_form_connect), &c);
|
|
gtk_box_append(GTK_BOX(actions), cancel);
|
|
gtk_box_append(GTK_BOX(actions), c.form_connect);
|
|
gtk_box_append(GTK_BOX(box), actions);
|
|
|
|
g_signal_connect(c.form_server, "changed", G_CALLBACK(on_form_server_changed), &c);
|
|
// `close-request` clears the pointer, and only a *close* emits it: `gtk_window_destroy()` does
|
|
// not, which is how the math dialog once kept presenting a finalized window.
|
|
g_signal_connect(c.form, "close-request", G_CALLBACK(+[](GtkWindow*, gpointer user) {
|
|
static_cast<Chooser*>(user)->form = nullptr;
|
|
return FALSE;
|
|
}),
|
|
&c);
|
|
|
|
gtk_window_present(GTK_WINDOW(c.form));
|
|
gtk_widget_grab_focus(c.form_server);
|
|
}
|
|
|
|
/// What the pick probes report: the connection the chooser came back with. One helper rather than
|
|
/// the same four lines in each branch, since both probes assert against this exact line.
|
|
void report_pick(const Chooser& c) {
|
|
std::fprintf(stderr, "[sources] picked %s:%u multicast=%s interface=%s\n", c.cfg->server.c_str(),
|
|
c.cfg->port, c.cfg->multicast.c_str(), c.cfg->interface_addr.c_str());
|
|
std::fflush(stderr);
|
|
}
|
|
|
|
gboolean on_chooser_close(GtkWindow*, gpointer user) {
|
|
Chooser& c = *static_cast<Chooser*>(user);
|
|
c.window = nullptr; // GTK destroys it for us on the default handler
|
|
chooser_finish(c, false);
|
|
return FALSE;
|
|
}
|
|
|
|
gboolean on_chooser_key(GtkEventControllerKey*, guint keyval, guint, GdkModifierType,
|
|
gpointer user) {
|
|
if (keyval != GDK_KEY_Escape) return FALSE;
|
|
chooser_finish(*static_cast<Chooser*>(user), false);
|
|
return TRUE;
|
|
}
|
|
|
|
/// `UDPSCOPE_PROBE_PICK_MS`: activate the first row, the way a click does.
|
|
/// `UDPSCOPE_PROBE_NEWHOST=host:port` drives the form instead of a row. The crash surface of a
|
|
/// dialog built, filled and destroyed is as much the point as the values, the same way
|
|
/// `UDPSCOPE_PROBE_FIELDS_MS` is in `ui.cpp`.
|
|
gboolean pick_probe(gpointer user) {
|
|
Chooser& c = *static_cast<Chooser*>(user);
|
|
// Every path below returns `G_SOURCE_REMOVE`, so the source is gone the moment this returns and
|
|
// its id is stale. Clear it here rather than on each path: `choose_source()`'s cancel-the-pending
|
|
// -probe call would otherwise be a GLib critical on every run that let the probe fire.
|
|
c.pick_source = 0;
|
|
|
|
// `UDPSCOPE_PROBE_NEWHOST=host:port` drives the form instead of a row.
|
|
if (const char* spec = g_getenv("UDPSCOPE_PROBE_NEWHOST")) {
|
|
open_form(c);
|
|
const std::string s = spec;
|
|
const std::size_t colon = s.rfind(':');
|
|
gtk_editable_set_text(GTK_EDITABLE(c.form_server),
|
|
colon == std::string::npos ? s.c_str() : s.substr(0, colon).c_str());
|
|
if (colon != std::string::npos) {
|
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_port), std::atof(s.c_str() + colon + 1));
|
|
}
|
|
std::fprintf(stderr, "[sources] form open, connect_sensitive=%d\n",
|
|
gtk_widget_get_sensitive(c.form_connect) ? 1 : 0);
|
|
g_signal_emit_by_name(c.form_connect, "clicked");
|
|
report_pick(c);
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
// Check the source list rather than the row count: `gtk_list_box_append` auto-wraps any
|
|
// non-row widget in a `GtkListBoxRow`, so `gtk_list_box_get_row_at_index` returns non-null
|
|
// even when the only list child is the empty-state placeholder label.
|
|
if (c.known.empty()) {
|
|
std::fprintf(stderr, "[sources] picked nothing: the list is empty\n");
|
|
std::fflush(stderr);
|
|
chooser_finish(c, false);
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
GtkListBoxRow* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(c.list), 0);
|
|
// `row-activated`, not `gtk_widget_activate()`: this project has already learned that
|
|
// activating a widget takes a path a real click does not (a `GtkButton` holds for its 250 ms
|
|
// ACTIVATE_TIMEOUT and then emits into a widget a rebuild has destroyed).
|
|
g_signal_emit_by_name(c.list, "row-activated", row);
|
|
report_pick(c);
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool choose_source(Config& cfg) {
|
|
const std::vector<KnownSource> known = known_sources();
|
|
|
|
// The discovery half, checkable with no display at all: this returns before `gtk_init()`, so
|
|
// it asserts what the list found without needing an X server or a GL context. False means the
|
|
// caller exits 0, which is what "list and exit" wants.
|
|
if (g_getenv("UDPSCOPE_PROBE_SOURCES") != nullptr) {
|
|
report_sources(known);
|
|
return false;
|
|
}
|
|
|
|
// `adw_init()` rather than `gtk_init()`: it does everything the latter does and also
|
|
// installs libadwaita's stylesheet, which the shortcut overview's `AdwShortcutsDialog`
|
|
// is drawn with. Both are idempotent, so `run_ui()` calling it again costs nothing.
|
|
adw_init();
|
|
|
|
Chooser c;
|
|
c.cfg = &cfg;
|
|
c.window = gtk_window_new();
|
|
gtk_window_set_title(GTK_WINDOW(c.window), "udpscope \xe2\x80\x94 choose a source");
|
|
gtk_window_set_default_size(GTK_WINDOW(c.window), 420, 320);
|
|
|
|
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
|
gtk_window_set_child(GTK_WINDOW(c.window), box);
|
|
|
|
GtkWidget* scroller = gtk_scrolled_window_new();
|
|
gtk_widget_set_vexpand(scroller, TRUE);
|
|
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller), GTK_POLICY_NEVER,
|
|
GTK_POLICY_AUTOMATIC);
|
|
c.list = gtk_list_box_new();
|
|
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(c.list), TRUE);
|
|
g_signal_connect(c.list, "row-activated", G_CALLBACK(on_row_activated), &c);
|
|
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroller), c.list);
|
|
gtk_box_append(GTK_BOX(box), scroller);
|
|
|
|
GtkWidget* actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
|
gtk_widget_set_margin_start(actions, 12);
|
|
gtk_widget_set_margin_end(actions, 12);
|
|
gtk_widget_set_margin_top(actions, 12);
|
|
gtk_widget_set_margin_bottom(actions, 12);
|
|
gtk_widget_set_halign(actions, GTK_ALIGN_END);
|
|
GtkWidget* new_host = gtk_button_new_with_label("New host\xe2\x80\xa6");
|
|
g_signal_connect(new_host, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
|
open_form(*static_cast<Chooser*>(user));
|
|
}),
|
|
&c);
|
|
GtkWidget* cancel = gtk_button_new_with_label("Cancel");
|
|
g_signal_connect(cancel, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
|
chooser_finish(*static_cast<Chooser*>(user), false);
|
|
}),
|
|
&c);
|
|
gtk_box_append(GTK_BOX(actions), new_host);
|
|
gtk_box_append(GTK_BOX(actions), cancel);
|
|
gtk_box_append(GTK_BOX(box), actions);
|
|
|
|
chooser_populate(c);
|
|
if (c.known.empty()) gtk_widget_grab_focus(new_host);
|
|
|
|
g_signal_connect(c.window, "close-request", G_CALLBACK(on_chooser_close), &c);
|
|
GtkEventController* keys = gtk_event_controller_key_new();
|
|
g_signal_connect(keys, "key-pressed", G_CALLBACK(on_chooser_key), &c);
|
|
gtk_widget_add_controller(c.window, keys);
|
|
|
|
gtk_window_present(GTK_WINDOW(c.window));
|
|
|
|
// Keep the timeout id so it can be cancelled if the chooser finishes before it fires. A row
|
|
// clicked before UDPSCOPE_PROBE_PICK_MS elapses would otherwise leave an armed source on the
|
|
// default main context pointing at the now-gone stack frame.
|
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_PICK_MS")) {
|
|
c.pick_source = g_timeout_add(static_cast<guint>(std::atoi(env)), pick_probe, &c);
|
|
}
|
|
|
|
c.loop = g_main_loop_new(nullptr, FALSE);
|
|
g_main_loop_run(c.loop);
|
|
g_main_loop_unref(c.loop);
|
|
c.loop = nullptr;
|
|
if (c.pick_source != 0) g_source_remove(c.pick_source);
|
|
return c.chosen;
|
|
}
|
|
|
|
} // namespace udpscope
|