chooser: pick the source type; each kind supplies its own connection form

This commit is contained in:
Martino Ferrari committed 2026-10-31 20:08:39 +01:00
1 parent 059b660586
commit 14f1421b2b
2 files changed
+172 -77

No files matched your search

+161 -43
View File
@@ -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 <unistd.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -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.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<SourceKind*> form_kinds; ///< dropdown order
ConfigForm form_cfg;
std::vector<GtkWidget*> 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<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.
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<Chooser*>(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<std::uint16_t>(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->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<Chooser*>(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<Chooser*>(user)->form = nullptr;
Chooser& ch = *static_cast<Chooser*>(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)) +
// 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))));
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()) {
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;
+11 -34
View File
@@ -2,7 +2,7 @@
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The form a source is added or edited with: one dialog, two groups, eight rows.
The form a source is added or edited with: one dialog, three groups, nine rows, the connection ones supplied by the chosen source kind.
A grid of labels and bare entries is what this was, and a grid is the wrong shape for it. Each row
here is a whole row — a title, a subtitle under it where the hint used to be a placeholder, and the
@@ -54,40 +54,17 @@
<object class="AdwPreferencesPage">
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwComboRow" id="kind_row">
<property name="title" translatable="yes">Source _type</property>
<property name="use-underline">True</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup" id="source_group">
<property name="title" translatable="yes">Connection</property>
<property name="description" translatable="yes">Leave the multicast group and the interface empty for unicast on the default route</property>
<child>
<object class="AdwEntryRow" id="server_row">
<property name="title" translatable="yes">_Server</property>
<property name="use-underline">True</property>
</object>
</child>
<child>
<object class="AdwSpinRow" id="port_row">
<property name="title" translatable="yes">_Port</property>
<property name="use-underline">True</property>
<property name="adjustment">
<object class="GtkAdjustment">
<property name="lower">1</property>
<property name="upper">65535</property>
<property name="step-increment">1</property>
<property name="value">8000</property>
</object>
</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="group_row">
<property name="title" translatable="yes">Multicast _group</property>
<property name="use-underline">True</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="iface_row">
<property name="title" translatable="yes">_Interface</property>
<property name="use-underline">True</property>
</object>
</child>
</object>
</child>
<child>