feat: the host form, and the help window, are UI files
src/ui/source-form.ui is the form a source is added or edited with: an AdwDialog of AdwPreferencesGroup rows (AdwEntryRow, AdwSpinRow, AdwSwitchRow) with its action in the header bar, where the label says which mode the form is in -- Connect for a new host, Save for an existing configuration. The grid of labels and bare entries it replaces had its hints in placeholders, which vanish as soon as there is a value; they are the groups' descriptions now. src/ui/help.ui is the help window's chrome: an AdwWindow with a header bar and an AdwNavigationSplitView whose sidebar is the page list. The rows stay in C++ because the fourteen pages are data, not template. Four UI files now, 653 lines of template that used to be widget construction. Two libadwaita traps cost a run each and are in docs/claude/invariants-gtk-and-chrome.md: an AdwDialog has no content property (its child is a plain <child>, unlike an AdwToolbarView), and AdwEntryRow has no subtitle (it is an AdwPreferencesRow, not an AdwActionRow). The unused form_field() helper went with them. Every probe reading is unchanged -- [sources] form:, [sources] edit form:, [help] -- which is what refactoring a shape rather than a behaviour should look like.
This commit is contained in:
1 parent
4c56172005
commit
68743a7d7f
8 files changed
+388
-184
No files matched your search
+90
-119
@@ -79,19 +79,23 @@ struct Chooser {
|
||||
/// 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;
|
||||
AdwDialog* form = nullptr;
|
||||
/// The form is a `.ui` file (`src/ui/source-form.ui`), so its builder is held with it, and its rows
|
||||
/// 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;
|
||||
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
|
||||
/// existing one -- which is why the same form is used for both.
|
||||
GtkWidget* form_name = nullptr;
|
||||
GtkWidget* form_note = nullptr;
|
||||
GtkWidget* form_buffer = nullptr;
|
||||
GtkWidget* form_relative = nullptr;
|
||||
AdwEntryRow* form_name = nullptr;
|
||||
AdwEntryRow* form_note = nullptr;
|
||||
AdwSpinRow* form_buffer = nullptr;
|
||||
AdwSwitchRow* form_relative = nullptr;
|
||||
/// The configuration the form is editing: empty for a new host. Copied rather than referenced,
|
||||
/// because `chooser_populate()` rebuilds `known` under the form.
|
||||
std::string edit_host;
|
||||
@@ -119,9 +123,9 @@ void chooser_finish(Chooser& c, bool chosen) {
|
||||
// cleared, so the `close-request` handler is not needed here, and `close()` on an unrealized
|
||||
// window returns having done nothing.
|
||||
if (c.form != nullptr) {
|
||||
GtkWidget* w = c.form;
|
||||
c.form = nullptr; // before the destroy, so the `close-request` handler cannot re-enter
|
||||
gtk_window_destroy(GTK_WINDOW(w));
|
||||
AdwDialog* d = c.form;
|
||||
c.form = nullptr; // before the close, so the `closed` handler cannot re-enter
|
||||
adw_dialog_force_close(d);
|
||||
}
|
||||
// The preferences go first: they are presented *on* this window, and a dialog still up when its
|
||||
// window is destroyed is the lifetime this project has got wrong once already. `prefs_close()` is
|
||||
@@ -357,10 +361,9 @@ 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));
|
||||
// The form's own `closed` handler clears the pointer, so this is the only place that has to know how
|
||||
// an `AdwDialog` closes.
|
||||
if (c.form != nullptr) adw_dialog_close(c.form);
|
||||
}
|
||||
|
||||
void on_form_server_changed(GtkEditable* entry, gpointer user) {
|
||||
@@ -369,8 +372,8 @@ void on_form_server_changed(GtkEditable* entry, gpointer user) {
|
||||
// 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);
|
||||
const char* text = gtk_editable_get_text(entry);
|
||||
gtk_widget_set_sensitive(c.form_connect, text != nullptr && text[0] != '\0');
|
||||
}
|
||||
|
||||
void on_form_connect(GtkButton*, gpointer user) {
|
||||
@@ -378,14 +381,13 @@ void on_form_connect(GtkButton*, gpointer 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>(
|
||||
gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(c.form_port)));
|
||||
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));
|
||||
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 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(c.form_buffer));
|
||||
const bool relative = gtk_switch_get_active(GTK_SWITCH(c.form_relative));
|
||||
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;
|
||||
@@ -433,16 +435,6 @@ void on_form_connect(GtkButton*, gpointer user) {
|
||||
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;
|
||||
}
|
||||
|
||||
/// Opens the connection form: on a new host, or on an existing configuration (`edit_index`). One
|
||||
/// form for both, because the fields are the same and a configuration *is* a host plus its
|
||||
/// settings -- the only difference is where the values start and what the button is called.
|
||||
@@ -459,95 +451,74 @@ void open_form(Chooser& c, int edit_index = -1) {
|
||||
c.edit_port = src != nullptr ? src->port : 0;
|
||||
c.edit_config = src != nullptr ? src->config : std::string();
|
||||
|
||||
c.form = gtk_window_new();
|
||||
gtk_window_set_title(GTK_WINDOW(c.form), src != nullptr ? "Edit configuration" : "New host");
|
||||
gtk_window_set_transient_for(GTK_WINDOW(c.form), GTK_WINDOW(c.window));
|
||||
gtk_window_set_modal(GTK_WINDOW(c.form), TRUE);
|
||||
// A builder per opening, as with every other `.ui` here: the dialog belongs to its object list and
|
||||
// `closed` destroys it, so the resource is parsed again rather than a dead dialog fetched.
|
||||
g_clear_object(&c.form_ui);
|
||||
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_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"));
|
||||
c.form_relative = ADW_SWITCH_ROW(gtk_builder_get_object(c.form_ui, "form_relative_row"));
|
||||
c.form_title = ADW_WINDOW_TITLE(gtk_builder_get_object(c.form_ui, "form_title"));
|
||||
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 ||
|
||||
c.form_note == nullptr || c.form_buffer == nullptr || c.form_relative == nullptr ||
|
||||
c.form_connect == nullptr) {
|
||||
g_clear_object(&c.form_ui);
|
||||
return;
|
||||
}
|
||||
c.form = ADW_DIALOG(dialog);
|
||||
|
||||
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);
|
||||
// Which mode the form is in, said in the header bar: the title names it and the action's label says
|
||||
// what pressing it does.
|
||||
const bool editing = src != nullptr;
|
||||
if (c.form_title != nullptr) {
|
||||
adw_window_title_set_title(c.form_title, editing ? "Edit configuration" : "New host");
|
||||
}
|
||||
gtk_button_set_label(GTK_BUTTON(c.form_connect), editing ? "Save" : "Connect");
|
||||
|
||||
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),
|
||||
src != nullptr ? src->host.c_str() : "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), src != nullptr ? src->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");
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_group),
|
||||
src != nullptr ? src->multicast.c_str() : "");
|
||||
c.form_iface = form_field(grid, 3, "Interface", gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(c.form_iface), "empty = default route");
|
||||
// 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
|
||||
? src->buffer_mb
|
||||
: std::max(1.0, c.cfg->buffer_mb));
|
||||
adw_switch_row_set_active(c.form_relative,
|
||||
(src != nullptr ? src->relative_time : c.cfg->relative_time) ? TRUE
|
||||
: FALSE);
|
||||
|
||||
// The session half: what this source's configuration is called, and the settings that have to be
|
||||
// decided before the first socket is opened.
|
||||
GtkWidget* heading = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(heading), "<b>Configuration</b>");
|
||||
gtk_widget_set_halign(heading, GTK_ALIGN_START);
|
||||
gtk_widget_set_margin_top(heading, 8);
|
||||
gtk_grid_attach(GTK_GRID(grid), heading, 0, 4, 2, 1);
|
||||
|
||||
c.form_name = form_field(grid, 5, "Name", gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(c.form_name), "default");
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_name),
|
||||
src != nullptr ? src->config.c_str() : "default");
|
||||
c.form_note = form_field(grid, 6, "Note", gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(c.form_note), "what this configuration is for");
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_note),
|
||||
src != nullptr ? src->comment.c_str() : "");
|
||||
c.form_buffer = form_field(grid, 7, "Buffer MB", gtk_spin_button_new_with_range(1, 65536, 1));
|
||||
gtk_widget_set_tooltip_text(c.form_buffer,
|
||||
"sample buffer per source, applied when this source is opened");
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_buffer),
|
||||
src != nullptr && src->buffer_mb > 0.0
|
||||
? src->buffer_mb
|
||||
: std::max(1.0, c.cfg->buffer_mb));
|
||||
GtkWidget* relative = gtk_switch_new();
|
||||
gtk_widget_set_halign(relative, GTK_ALIGN_START);
|
||||
gtk_widget_set_tooltip_text(relative,
|
||||
"plot time from the packet counter and the declared rate, instead "
|
||||
"of the stream's own time signal (--relative-time)");
|
||||
gtk_switch_set_active(GTK_SWITCH(relative),
|
||||
src != nullptr ? src->relative_time : c.cfg->relative_time);
|
||||
c.form_relative = form_field(grid, 8, "Relative time", relative);
|
||||
|
||||
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(src != nullptr ? "Save" : "Connect");
|
||||
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));
|
||||
}),
|
||||
&c);
|
||||
}
|
||||
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) {
|
||||
// `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;
|
||||
return FALSE;
|
||||
}),
|
||||
&c);
|
||||
|
||||
gtk_window_present(GTK_WINDOW(c.form));
|
||||
gtk_widget_grab_focus(c.form_server);
|
||||
adw_dialog_present(c.form, GTK_WIDGET(c.window));
|
||||
gtk_widget_grab_focus(GTK_WIDGET(c.form_server));
|
||||
}
|
||||
|
||||
/// What the pick probes report: the connection the chooser came back with. One helper rather than
|
||||
@@ -665,19 +636,19 @@ gboolean pick_probe(gpointer user) {
|
||||
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));
|
||||
adw_spin_row_set_value(c.form_port, std::atof(s.c_str() + colon + 1));
|
||||
}
|
||||
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");
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_buffer), 32);
|
||||
gtk_switch_set_active(GTK_SWITCH(c.form_relative), TRUE);
|
||||
adw_spin_row_set_value(c.form_buffer, 32);
|
||||
adw_switch_row_set_active(c.form_relative, TRUE);
|
||||
std::fprintf(stderr,
|
||||
"[sources] form: name='%s' note='%s' buffer=%.6g relative=%d "
|
||||
"connect_sensitive=%d\n",
|
||||
gtk_editable_get_text(GTK_EDITABLE(c.form_name)),
|
||||
gtk_editable_get_text(GTK_EDITABLE(c.form_note)),
|
||||
gtk_spin_button_get_value(GTK_SPIN_BUTTON(c.form_buffer)),
|
||||
gtk_switch_get_active(GTK_SWITCH(c.form_relative)) ? 1 : 0,
|
||||
adw_spin_row_get_value(c.form_buffer),
|
||||
adw_switch_row_get_active(c.form_relative) ? 1 : 0,
|
||||
gtk_widget_get_sensitive(c.form_connect) ? 1 : 0);
|
||||
g_signal_emit_by_name(c.form_connect, "clicked");
|
||||
report_pick(c);
|
||||
@@ -708,12 +679,12 @@ gboolean pick_probe(gpointer user) {
|
||||
"[sources] edit form: name='%s' note='%s' buffer=%.6g relative=%d button='%s'\n",
|
||||
gtk_editable_get_text(GTK_EDITABLE(c.form_name)),
|
||||
gtk_editable_get_text(GTK_EDITABLE(c.form_note)),
|
||||
gtk_spin_button_get_value(GTK_SPIN_BUTTON(c.form_buffer)),
|
||||
gtk_switch_get_active(GTK_SWITCH(c.form_relative)) ? 1 : 0,
|
||||
adw_spin_row_get_value(c.form_buffer),
|
||||
adw_switch_row_get_active(c.form_relative) ? 1 : 0,
|
||||
gtk_button_get_label(GTK_BUTTON(c.form_connect)));
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_note), "edited by the probe");
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_buffer), 16);
|
||||
gtk_switch_set_active(GTK_SWITCH(c.form_relative), FALSE);
|
||||
adw_spin_row_set_value(c.form_buffer, 16);
|
||||
adw_switch_row_set_active(c.form_relative, FALSE);
|
||||
// `UDPSCOPE_PROBE_EDITNAME=new-name` also renames it, which is a *move*: the old file must not
|
||||
// survive, or the chooser lists the same session twice.
|
||||
if (const char* rename_to = g_getenv("UDPSCOPE_PROBE_EDITNAME")) {
|
||||
|
||||
Reference in new issue
Block a user