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
+51
-63
@@ -20,6 +20,13 @@ namespace {
|
||||
/// `AdwDialog`, which is **not** a `GtkWindow`: it has no `::close-request`, so it clears on
|
||||
/// `::closed` and closes with `adw_dialog_close()`.
|
||||
GtkWidget* g_help = nullptr;
|
||||
/// The help window is a `.ui` file too, so its builder, the label one page's markup goes into and the
|
||||
/// column its screenshots go into are all kept beside the window's own pointer. All four are cleared by
|
||||
/// the window's own close handler: the page switch reads them, and it cannot run after the window is
|
||||
/// gone.
|
||||
GtkBuilder* g_help_ui = nullptr;
|
||||
GtkWidget* g_help_body = nullptr;
|
||||
GtkWidget* g_help_shots = nullptr;
|
||||
AdwDialog* g_about = nullptr;
|
||||
/// The overview is the one window here built from a `.ui` file, so its builder is held too: the window
|
||||
/// belongs to the builder, and a *destroyed* `GtkWindow` cannot be presented again -- which is why the
|
||||
@@ -27,6 +34,17 @@ AdwDialog* g_about = nullptr;
|
||||
GtkWidget* g_shortcuts = nullptr;
|
||||
GtkBuilder* g_shortcuts_ui = nullptr;
|
||||
|
||||
/// The help window's own close handler, rather than the shared one: this window also owns the label a
|
||||
/// page is written into and the column its screenshots are built in, and a page switch that ran after
|
||||
/// the window was gone would read both.
|
||||
gboolean on_help_close(GtkWindow* window, gpointer) {
|
||||
(void)window;
|
||||
g_help = nullptr;
|
||||
g_help_body = nullptr;
|
||||
g_help_shots = nullptr;
|
||||
return FALSE; // let the default handler destroy it
|
||||
}
|
||||
|
||||
gboolean on_window_close(GtkWindow* window, gpointer slot) {
|
||||
(void)window;
|
||||
*static_cast<GtkWidget**>(slot) = nullptr;
|
||||
@@ -107,20 +125,18 @@ std::string load_page(const HelpPage& page, bool* ok) {
|
||||
return body;
|
||||
}
|
||||
|
||||
void on_help_row_selected(GtkListBox*, GtkListBoxRow* row, gpointer body_label) {
|
||||
if (row == nullptr) return;
|
||||
void on_help_row_selected(GtkListBox*, GtkListBoxRow* row, gpointer) {
|
||||
if (row == nullptr || g_help_body == nullptr) return;
|
||||
const int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(row), "help-page"));
|
||||
if (idx < 0 || idx >= kPageCount) return;
|
||||
bool ok = false;
|
||||
const std::string body = load_page(kPages[idx], &ok);
|
||||
gtk_label_set_markup(GTK_LABEL(body_label), body.c_str());
|
||||
// The screenshots above it, if this page has any. The column is the label's previous sibling
|
||||
// inside the page's box, which is what lets this handler stay a matter of the label alone --
|
||||
// and refilling it is a page *switch*, not a frame, so rebuilding it here is cheap and is
|
||||
// also the only way to be sure the pictures are this page's and not the last one's.
|
||||
GtkWidget* label = GTK_WIDGET(body_label);
|
||||
GtkWidget* parent = gtk_widget_get_parent(label);
|
||||
GtkWidget* shots = parent != nullptr ? gtk_widget_get_first_child(parent) : nullptr;
|
||||
gtk_label_set_markup(GTK_LABEL(g_help_body), body.c_str());
|
||||
// The screenshots above it, if this page has any. The column comes from the `.ui` by id rather than
|
||||
// being found as the label's previous sibling: refilling it is a page *switch*, not a frame, so
|
||||
// rebuilding it here is cheap and is also the only way to be sure the pictures are this page's and
|
||||
// not the last one's.
|
||||
GtkWidget* shots = g_help_shots;
|
||||
if (shots != nullptr && GTK_IS_BOX(shots)) {
|
||||
while (GtkWidget* old = gtk_widget_get_first_child(shots)) {
|
||||
gtk_box_remove(GTK_BOX(shots), old);
|
||||
@@ -200,47 +216,31 @@ void show_help(GtkWindow* parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
GtkWidget* window = gtk_window_new();
|
||||
g_help = window;
|
||||
gtk_window_set_title(GTK_WINDOW(window), "udpscope help");
|
||||
gtk_window_set_default_size(GTK_WINDOW(window), 860, 640);
|
||||
gtk_window_set_transient_for(GTK_WINDOW(window), parent);
|
||||
// A builder per opening, as with every other `.ui` here: the window belongs to its object list and
|
||||
// `close-request` destroys the window, so the resource is parsed again rather than a dead window
|
||||
// fetched. This is where the previous one goes, and it is safe because that window has been closed
|
||||
// for as long as it takes someone to ask for help again.
|
||||
g_clear_object(&g_help_ui);
|
||||
g_help_ui = gtk_builder_new_from_resource("/org/udpscope/ui/help.ui");
|
||||
if (g_help_ui == nullptr) return;
|
||||
GObject* window = gtk_builder_get_object(g_help_ui, "help_window");
|
||||
GObject* list = gtk_builder_get_object(g_help_ui, "page_list");
|
||||
GObject* body = gtk_builder_get_object(g_help_ui, "body");
|
||||
GObject* shots = gtk_builder_get_object(g_help_ui, "shot_box");
|
||||
// A missing id is the one mistake the build and `GtkBuilder` both miss; every reader below would be a
|
||||
// null widget on the first page switch.
|
||||
if (window == nullptr || list == nullptr || body == nullptr || shots == nullptr) return;
|
||||
g_help = GTK_WIDGET(window);
|
||||
g_help_body = GTK_WIDGET(body);
|
||||
g_help_shots = GTK_WIDGET(shots);
|
||||
gtk_window_set_transient_for(GTK_WINDOW(g_help), parent);
|
||||
|
||||
// One label, width-locked by the scrolled window, holding one page's markup. The
|
||||
// sidebar swaps the markup rather than rebuilding anything.
|
||||
GtkWidget* body = gtk_label_new(nullptr);
|
||||
gtk_label_set_use_markup(GTK_LABEL(body), TRUE);
|
||||
gtk_label_set_wrap(GTK_LABEL(body), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(body), 0.0f);
|
||||
gtk_label_set_yalign(GTK_LABEL(body), 0.0f);
|
||||
gtk_label_set_selectable(GTK_LABEL(body), TRUE);
|
||||
gtk_widget_set_margin_start(body, 18);
|
||||
gtk_widget_set_margin_end(body, 18);
|
||||
gtk_widget_set_margin_top(body, 14);
|
||||
gtk_widget_set_margin_bottom(body, 14);
|
||||
|
||||
GtkWidget* body_scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(body_scroll), GTK_POLICY_NEVER,
|
||||
GTK_POLICY_AUTOMATIC);
|
||||
// The page's own column: its screenshots above its text. Built once and *refilled* by the page
|
||||
// switch, which is why no picture is made here -- one `GtkPicture` per image of the page.
|
||||
GtkWidget* shot_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_set_margin_start(shot_box, 18);
|
||||
gtk_widget_set_margin_end(shot_box, 18);
|
||||
gtk_widget_set_margin_top(shot_box, 14);
|
||||
gtk_widget_set_visible(shot_box, false);
|
||||
GtkWidget* page_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_box_append(GTK_BOX(page_box), shot_box);
|
||||
gtk_box_append(GTK_BOX(page_box), body);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(body_scroll), page_box);
|
||||
gtk_widget_set_hexpand(body_scroll, TRUE);
|
||||
|
||||
GtkWidget* list = gtk_list_box_new();
|
||||
gtk_list_box_set_selection_mode(GTK_LIST_BOX(list), GTK_SELECTION_SINGLE);
|
||||
// The rows are the pages, which is data and not template: a heading row per section, then one row per
|
||||
// page carrying its own index. A template cannot know the fourteen pages, and a sectioned list is not
|
||||
// what a GtkListView with one item factory can draw.
|
||||
const char* current_section = nullptr;
|
||||
for (int i = 0; i < kPageCount; ++i) {
|
||||
if (current_section == nullptr ||
|
||||
std::string(current_section) != kPages[i].section) {
|
||||
if (current_section == nullptr || std::string(current_section) != kPages[i].section) {
|
||||
current_section = kPages[i].section;
|
||||
GtkWidget* heading = gtk_label_new(nullptr);
|
||||
char markup[128];
|
||||
@@ -267,26 +267,14 @@ void show_help(GtkWindow* parent) {
|
||||
g_object_set_data(G_OBJECT(row), "help-page", GINT_TO_POINTER(i));
|
||||
gtk_list_box_append(GTK_LIST_BOX(list), row);
|
||||
}
|
||||
g_signal_connect(list, "row-selected", G_CALLBACK(on_help_row_selected), body);
|
||||
|
||||
GtkWidget* list_scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(list_scroll), GTK_POLICY_NEVER,
|
||||
GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(list_scroll), list);
|
||||
gtk_widget_set_size_request(list_scroll, 220, -1);
|
||||
|
||||
GtkWidget* split = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_box_append(GTK_BOX(split), list_scroll);
|
||||
gtk_box_append(GTK_BOX(split), gtk_separator_new(GTK_ORIENTATION_VERTICAL));
|
||||
gtk_box_append(GTK_BOX(split), body_scroll);
|
||||
gtk_window_set_child(GTK_WINDOW(window), split);
|
||||
g_signal_connect(list, "row-selected", G_CALLBACK(on_help_row_selected), nullptr);
|
||||
|
||||
// Select the first page so the window never opens blank.
|
||||
GtkListBoxRow* first = gtk_list_box_get_row_at_index(GTK_LIST_BOX(list), 1);
|
||||
if (first != nullptr) gtk_list_box_select_row(GTK_LIST_BOX(list), first);
|
||||
|
||||
g_signal_connect(window, "close-request", G_CALLBACK(on_window_close), &g_help);
|
||||
gtk_window_present(GTK_WINDOW(window));
|
||||
g_signal_connect(g_help, "close-request", G_CALLBACK(on_help_close), &g_help);
|
||||
gtk_window_present(GTK_WINDOW(g_help));
|
||||
}
|
||||
|
||||
void show_shortcuts(GtkWindow* parent) {
|
||||
|
||||
Reference in new issue
Block a user