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:
1 parent
b18b8a5b04
commit
54e840e590
1 file changed
+143
-4
+143
-4
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user