From 14f1421b2bb2668aa1072750972a05d71aced0a0 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Sat, 31 Oct 2026 20:08:39 +0100 Subject: [PATCH] chooser: pick the source type; each kind supplies its own connection form --- src/source_dialog.cpp | 224 ++++++++++++++++++++++++++++++++---------- src/ui/source-form.ui | 45 +++------ 2 files changed, 182 insertions(+), 87 deletions(-) diff --git a/src/source_dialog.cpp b/src/source_dialog.cpp index b2a9dd8..8bc7d32 100644 --- a/src/source_dialog.cpp +++ b/src/source_dialog.cpp @@ -8,6 +8,7 @@ #include "help_window.hpp" #include "layout_store.hpp" #include "settings.hpp" +#include "source/registry.hpp" // The app's shared chrome -- `ui_impl::menu_button()`/`menu_toggle()` and `install_css()` -- so the // chooser's help menu is the *same* menu the window's Help view is, not a second one that merely // looks like it. Heavy header, but a row shape copied here would be a row shape that drifts. @@ -19,6 +20,7 @@ #include +#include #include #include #include @@ -32,10 +34,16 @@ namespace { /// 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; + cfg.source_kind = src.kind; + if (src.kind == "udps") { + cfg.server = src.host; + cfg.port = src.port; + cfg.multicast = src.multicast; + cfg.interface_addr = src.interface_addr; + cfg.source_opts = SourceConfig(); + } else { + cfg.source_opts = src.opts; + } // The configuration's own startup settings. They are read *before* the model exists, which is the // only moment a buffer size can be applied at all -- and the reason they are stored per // configuration rather than only passed on the command line. @@ -105,10 +113,14 @@ struct Chooser { /// are typed: a switch is read with `adw_switch_row_get_active()` and not off a widget. GtkBuilder* form_ui = nullptr; AdwWindowTitle* form_title = nullptr; - AdwEntryRow* form_server = nullptr; - AdwSpinRow* form_port = nullptr; - AdwEntryRow* form_group = nullptr; - AdwEntryRow* form_iface = nullptr; + /// The source-type dropdown and the group the chosen kind's rows live in. The rows themselves, + /// and the closures that read and write them, are the kind's (`ConfigForm`); the chooser only + /// swaps them when the dropdown moves. + AdwComboRow* form_kind_row = nullptr; + AdwPreferencesGroup* form_source_group = nullptr; + std::vector form_kinds; ///< dropdown order + ConfigForm form_cfg; + std::vector form_cfg_rows; ///< the rows currently in `form_source_group` GtkWidget* form_connect = nullptr; /// The configuration half of the form: what a source's *session* is called and the settings that /// can only be applied before the model exists. A new host needs all of them, and so does an @@ -373,7 +385,11 @@ void chooser_populate(Chooser& c) { // 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) + " \u00b7 " + s.config; + // A non-UDPS session whose kind is not registered keeps its pseudo-host as the title. + SourceKind* sk = s.kind == "udps" ? nullptr : find_kind(s.kind); + const std::string where = sk != nullptr ? sk->display_name() + " " + sk->identity(s.opts) + : s.host + ":" + std::to_string(s.port); + const std::string title = where + " \u00b7 " + s.config; 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); @@ -432,34 +448,69 @@ void form_close(Chooser& c) { if (c.form != nullptr) adw_dialog_close(c.form); } -void on_form_server_changed(GtkEditable* entry, gpointer user) { +/// The kind selected in the dropdown, or null. +SourceKind* form_selected_kind(const Chooser& c) { + if (c.form_kind_row == nullptr) return nullptr; + const guint i = adw_combo_row_get_selected(c.form_kind_row); + return i < c.form_kinds.size() ? c.form_kinds[i] : nullptr; +} + +/// Swaps the Connection group's rows for `kind`'s form and fills them from `initial`. The +/// connect button follows the form's own `valid()`: the one validation here is the kind's. +void form_rebuild(Chooser& c, SourceKind& kind, const SourceConfig& initial) { + for (GtkWidget* r : c.form_cfg_rows) adw_preferences_group_remove(c.form_source_group, r); + c.form_cfg_rows.clear(); + c.form_cfg = kind.make_config_form(); + for (GtkWidget* r : c.form_cfg.rows) { + adw_preferences_group_add(c.form_source_group, r); + c.form_cfg_rows.push_back(r); + } + adw_preferences_group_set_description( + c.form_source_group, + c.form_cfg.description.empty() ? nullptr : c.form_cfg.description.c_str()); + c.form_cfg.write(initial); + c.form_cfg.on_change([&c] { + if (c.form_connect != nullptr) { + gtk_widget_set_sensitive(c.form_connect, c.form_cfg.valid() ? TRUE : FALSE); + } + }); + if (c.form_connect != nullptr) { + gtk_widget_set_sensitive(c.form_connect, c.form_cfg.valid() ? TRUE : FALSE); + } + if (c.form_cfg.focus != nullptr) gtk_widget_grab_focus(c.form_cfg.focus); +} + +void on_form_kind_changed(GObject*, GParamSpec*, 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. - const char* text = gtk_editable_get_text(entry); - gtk_widget_set_sensitive(c.form_connect, text != nullptr && text[0] != '\0'); + if (SourceKind* k = form_selected_kind(c)) form_rebuild(c, *k, SourceConfig()); } 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; - const std::string host = server; - const std::uint16_t port = static_cast(adw_spin_row_get_value(c.form_port)); - const std::string group = gtk_editable_get_text(GTK_EDITABLE(c.form_group)); - const std::string iface = gtk_editable_get_text(GTK_EDITABLE(c.form_iface)); + SourceKind* kind = form_selected_kind(c); + if (kind == nullptr || !c.form_cfg.valid()) return; + const SourceConfig scfg = c.form_cfg.read(); + const bool udps = kind->id() == "udps"; + const std::string host = session_host(kind->id(), scfg); + const std::uint16_t port = session_port(kind->id(), scfg); + const std::string group = udps ? scfg.get("multicast") : std::string(); + const std::string iface = udps ? scfg.get("interface") : std::string(); std::string name = gtk_editable_get_text(GTK_EDITABLE(c.form_name)); const std::string note = gtk_editable_get_text(GTK_EDITABLE(c.form_note)); const double buffer_mb = adw_spin_row_get_value(c.form_buffer); const bool relative = adw_switch_row_get_active(c.form_relative) != FALSE; if (name.empty()) name = "default"; - c.cfg->server = host; - c.cfg->port = port; - c.cfg->multicast = group; - c.cfg->interface_addr = iface; + c.cfg->source_kind = kind->id(); + if (udps) { + c.cfg->server = scfg.get("server"); + c.cfg->port = port; + c.cfg->multicast = group; + c.cfg->interface_addr = iface; + c.cfg->source_opts = SourceConfig(); + } else { + c.cfg->source_opts = scfg; + } // The startup settings. Applied here because this runs *before* `ScopeModel` exists, which is the // only moment a buffer size can be applied at all -- and stored, so the next run gets them back. c.cfg->buffer_mb = buffer_mb; @@ -476,6 +527,8 @@ void on_form_connect(GtkButton*, gpointer user) { st.relative_time = relative; st.multicast = group; st.interface_addr = iface; + st.source_kind = udps ? std::string() : kind->id(); + st.source_opts = udps ? SourceConfig() : scfg; const bool editing = !c.edit_config.empty(); const bool moved = @@ -527,10 +580,9 @@ void open_form(Chooser& c, int edit_index = -1) { c.form_ui = gtk_builder_new_from_resource("/org/udpscope/ui/source-form.ui"); if (c.form_ui == nullptr) return; GObject* dialog = gtk_builder_get_object(c.form_ui, "source_form"); - c.form_server = ADW_ENTRY_ROW(gtk_builder_get_object(c.form_ui, "server_row")); - c.form_port = ADW_SPIN_ROW(gtk_builder_get_object(c.form_ui, "port_row")); - c.form_group = ADW_ENTRY_ROW(gtk_builder_get_object(c.form_ui, "group_row")); - c.form_iface = ADW_ENTRY_ROW(gtk_builder_get_object(c.form_ui, "iface_row")); + c.form_kind_row = ADW_COMBO_ROW(gtk_builder_get_object(c.form_ui, "kind_row")); + c.form_source_group = + ADW_PREFERENCES_GROUP(gtk_builder_get_object(c.form_ui, "source_group")); c.form_name = ADW_ENTRY_ROW(gtk_builder_get_object(c.form_ui, "name_row")); c.form_note = ADW_ENTRY_ROW(gtk_builder_get_object(c.form_ui, "note_row")); c.form_buffer = ADW_SPIN_ROW(gtk_builder_get_object(c.form_ui, "buffer_row")); @@ -539,8 +591,8 @@ void open_form(Chooser& c, int edit_index = -1) { c.form_connect = GTK_WIDGET(gtk_builder_get_object(c.form_ui, "form_apply")); // A missing id is the one mistake neither the build nor `GtkBuilder` sees, and every read below would // be a null row. - if (dialog == nullptr || c.form_server == nullptr || c.form_port == nullptr || - c.form_group == nullptr || c.form_iface == nullptr || c.form_name == nullptr || + if (dialog == nullptr || c.form_kind_row == nullptr || c.form_source_group == nullptr || + c.form_name == nullptr || c.form_note == nullptr || c.form_buffer == nullptr || c.form_relative == nullptr || c.form_connect == nullptr) { g_clear_object(&c.form_ui); @@ -558,11 +610,6 @@ void open_form(Chooser& c, int edit_index = -1) { // The values: the configuration being edited, or what the command line asked for. The hints live in // the rows' subtitles, so an empty field still says what empty means. - gtk_editable_set_text(GTK_EDITABLE(c.form_server), src != nullptr ? src->host.c_str() : "127.0.0.1"); - adw_spin_row_set_value(c.form_port, src != nullptr ? src->port : 8000); - gtk_editable_set_text(GTK_EDITABLE(c.form_group), src != nullptr ? src->multicast.c_str() : ""); - gtk_editable_set_text(GTK_EDITABLE(c.form_iface), - src != nullptr ? src->interface_addr.c_str() : ""); gtk_editable_set_text(GTK_EDITABLE(c.form_name), src != nullptr ? src->config.c_str() : "default"); gtk_editable_set_text(GTK_EDITABLE(c.form_note), src != nullptr ? src->comment.c_str() : ""); adw_spin_row_set_value(c.form_buffer, src != nullptr && src->buffer_mb > 0.0 @@ -572,6 +619,37 @@ void open_form(Chooser& c, int edit_index = -1) { (src != nullptr ? src->relative_time : c.cfg->relative_time) ? TRUE : FALSE); + // The dropdown lists the registered kinds. A new host starts on the kind the command line named + // (UDPS by default); an edit starts on the configuration's own kind and cannot change it -- + // disabled, not hidden, like every other control that does not apply. + c.form_kinds = kinds(); + GtkStringList* names = gtk_string_list_new(nullptr); + guint selected = 0; + const std::string want_kind = src != nullptr ? src->kind : c.cfg->source_kind; + for (guint i = 0; i < c.form_kinds.size(); ++i) { + gtk_string_list_append(names, c.form_kinds[i]->display_name().c_str()); + if (c.form_kinds[i]->id() == want_kind) selected = i; + } + adw_combo_row_set_model(c.form_kind_row, G_LIST_MODEL(names)); + adw_combo_row_set_selected(c.form_kind_row, selected); + gtk_widget_set_sensitive(GTK_WIDGET(c.form_kind_row), src == nullptr); + g_object_unref(names); + + SourceConfig initial; + if (src != nullptr && src->kind == "udps") { + initial.set("server", src->host); + initial.set("port", std::to_string(src->port)); + if (!src->multicast.empty()) initial.set("multicast", src->multicast); + if (!src->interface_addr.empty()) initial.set("interface", src->interface_addr); + } else if (src != nullptr) { + initial = src->opts; + } else if (c.cfg->source_kind != "udps") { + initial = c.cfg->source_opts; + } + if (!c.form_kinds.empty()) form_rebuild(c, *c.form_kinds[selected], initial); + // Connected after the first rebuild: setting `selected` above must not rebuild a second time. + g_signal_connect(c.form_kind_row, "notify::selected", G_CALLBACK(on_form_kind_changed), &c); + if (GObject* cancel = gtk_builder_get_object(c.form_ui, "form_cancel"); cancel != nullptr) { g_signal_connect(cancel, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) { form_close(*static_cast(user)); @@ -579,16 +657,22 @@ void open_form(Chooser& c, int edit_index = -1) { &c); } g_signal_connect(c.form_connect, "clicked", G_CALLBACK(on_form_connect), &c); - g_signal_connect(c.form_server, "changed", G_CALLBACK(on_form_server_changed), &c); // `closed` clears the pointer. An `AdwDialog` emits that and not `close-request`, and it fires only // when the dialog really went away -- which is what the comment here used to be about. g_signal_connect(c.form, "closed", G_CALLBACK(+[](AdwDialog*, gpointer user) { - static_cast(user)->form = nullptr; + Chooser& ch = *static_cast(user); + ch.form = nullptr; + // The rows are gone with the dialog; dropping the closures that captured them + // keeps a stale `read()` from ever being callable. + ch.form_cfg = ConfigForm(); + ch.form_cfg_rows.clear(); + ch.form_kind_row = nullptr; + ch.form_source_group = nullptr; }), &c); adw_dialog_present(c.form, GTK_WIDGET(c.window)); - gtk_widget_grab_focus(GTK_WIDGET(c.form_server)); + if (c.form_cfg.focus != nullptr) gtk_widget_grab_focus(c.form_cfg.focus); } /// What the pick probes report: the connection the chooser came back with. One helper rather than @@ -614,6 +698,15 @@ void report_pick(const Chooser& c) { (c.saved_config.empty() ? (c.known.empty() ? "-" : c.known.front().comment) : c.saved_note) .c_str()); + if (c.cfg->source_kind != "udps") { + std::string opts; + for (const auto& kv : c.cfg->source_opts.items()) { + opts += (opts.empty() ? "" : ",") + kv.first + "=" + kv.second; + } + std::fprintf(stderr, "[sources] picked kind=%s opts=%s session=%s:%u\n", + c.cfg->source_kind.c_str(), opts.c_str(), c.cfg->session_host().c_str(), + c.cfg->session_port()); + } std::fflush(stderr); } @@ -1039,22 +1132,42 @@ gboolean pick_probe(gpointer user) { 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) { - adw_spin_row_set_value(c.form_port, std::atof(s.c_str() + colon + 1)); + SourceConfig sc; + if (const char* kspec = g_getenv("UDPSCOPE_PROBE_NEWHOST_KIND")) { + // "kind" or "kind:k=v,k=v": pick the kind in the dropdown (which rebuilds the form), then + // write the options into it. + std::string ks = kspec; + std::string opts; + if (const std::size_t sep = ks.find(':'); sep != std::string::npos) { + opts = ks.substr(sep + 1); + ks = ks.substr(0, sep); + } + for (guint i = 0; i < c.form_kinds.size(); ++i) { + if (c.form_kinds[i]->id() == ks) adw_combo_row_set_selected(c.form_kind_row, i); + } + for (std::size_t pos = 0; pos < opts.size();) { + const std::size_t end = std::min(opts.find(',', pos), opts.size()); + sc.parse_assignment(opts.substr(pos, end - pos)); + pos = end + 1; + } + } else { + sc.set("server", colon == std::string::npos ? s : s.substr(0, colon)); + if (colon != std::string::npos) sc.set("port", s.substr(colon + 1)); } + c.form_cfg.write(sc); gtk_editable_set_text(GTK_EDITABLE(c.form_name), "probe-host"); gtk_editable_set_text(GTK_EDITABLE(c.form_note), "made by the new-host probe"); adw_spin_row_set_value(c.form_buffer, 32); adw_switch_row_set_active(c.form_relative, TRUE); - // 8/8: the form's row titles are mnemonics (`_Server`, `_Port`, `Multicast _group`, ...). The two - // buttons already asked for it; the rows did not, and drew their underscores. - std::fprintf(stderr, "[sources] form mnemonics=%d/8\n", - (row_underlines(G_OBJECT(c.form_server)) + row_underlines(G_OBJECT(c.form_port)) + - row_underlines(G_OBJECT(c.form_group)) + row_underlines(G_OBJECT(c.form_iface)) + - row_underlines(G_OBJECT(c.form_name)) + row_underlines(G_OBJECT(c.form_note)) + - row_underlines(G_OBJECT(c.form_buffer)) + row_underlines(G_OBJECT(c.form_relative)))); + // 9/9 for UDPS: the form's row titles are mnemonics (`Source _type`, `_Server`, `_Port`, ...). The + // total follows the kind's row count: five rows are the chooser's, the rest the kind's own. + int underlines = row_underlines(G_OBJECT(c.form_kind_row)) + + row_underlines(G_OBJECT(c.form_name)) + row_underlines(G_OBJECT(c.form_note)) + + row_underlines(G_OBJECT(c.form_buffer)) + + row_underlines(G_OBJECT(c.form_relative)); + for (GtkWidget* r : c.form_cfg.rows) underlines += row_underlines(G_OBJECT(r)); + std::fprintf(stderr, "[sources] form mnemonics=%d/%zu\n", underlines, + 5 + c.form_cfg.rows.size()); std::fprintf(stderr, "[sources] form: name='%s' note='%s' buffer=%.6g relative=%d " "connect_sensitive=%d\n", @@ -1196,8 +1309,13 @@ void loading_show(const Config& cfg) { 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 + ")"; + SourceKind* kind = cfg.source_kind != "udps" ? find_kind(cfg.source_kind) : nullptr; + if (kind != nullptr) { + head = "Connecting to " + kind->display_name() + " " + kind->identity(cfg.source_config()); + } else { + 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; diff --git a/src/ui/source-form.ui b/src/ui/source-form.ui index c21c1ad..b0f2797 100644 --- a/src/ui/source-form.ui +++ b/src/ui/source-form.ui @@ -2,7 +2,7 @@