From 54e840e590b39c9a25f0b6e519b2936986a74f67 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Sun, 4 Oct 2026 00:32:32 +0200 Subject: [PATCH] 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. --- src/source_dialog.cpp | 147 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 143 insertions(+), 4 deletions(-) diff --git a/src/source_dialog.cpp b/src/source_dialog.cpp index e28a876..8fd34cb 100644 --- a/src/source_dialog.cpp +++ b/src/source_dialog.cpp @@ -50,6 +50,16 @@ struct Chooser { GtkWidget* window = nullptr; GtkWidget* list = nullptr; std::vector 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(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(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( + 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(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(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(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(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(user)); + }), + &c); GtkWidget* cancel = gtk_button_new_with_label("Cancel"); g_signal_connect(cancel, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) { chooser_finish(*static_cast(user), false);