the new-host connection form

Server, port, multicast group and interface, and nothing else: --buffer-mb and
friends are tuning rather than which source, and a form that asks for them is
one a user has to read first. The form's widgets live on the chooser, which
outlives them, so no close path frees a live context.
This commit is contained in:
Martino Ferrari committed 2026-10-04 00:32:32 +02:00
1 parent b18b8a5b04
commit 54e840e590
1 file changed
+143 -4
+143 -4
View File
@@ -50,6 +50,16 @@ struct Chooser {
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;
};
void chooser_finish(Chooser& c, bool chosen) {
@@ -145,6 +155,113 @@ void chooser_populate(Chooser& c) {
}
}
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
@@ -160,8 +277,29 @@ gboolean on_chooser_key(GtkEventControllerKey*, guint keyval, guint, GdkModifier
}
/// `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);
// `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.
@@ -182,10 +320,7 @@ gboolean pick_probe(gpointer user) {
// 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);
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);
report_pick(c);
return G_SOURCE_REMOVE;
}
@@ -230,6 +365,10 @@ bool choose_source(Config& cfg) {
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);