From 084c3e5d2bfbfc8eec4d3f89e7b1cfa508941902 Mon Sep 17 00:00:00 2001 From: ebenali <54529923+ebenali@users.noreply.github.com> Date: Sun, 21 Jun 2026 17:58:37 +0000 Subject: [PATCH 1/2] fix(wbconf): seed font pickers with current font; add a Reset split button Two wbconf UX fixes. - The font pickers were left empty for any place not present in rc.xml, so the font dialog opened with nothing selected and you couldn't just tweak the size. Seed each button with the effective default (Sans 10) when unset so the dialog pre-selects the current font. To avoid persisting that seeded default, the Save path now remembers the loaded settings and only writes a font the user actually changed (an untouched default-seeded place is left out of rc.xml). - Add a Reset control next to Close / Save & Apply: a split button whose main part resets the current page to defaults and whose dropdown offers "Reset This Page" and "Reset All to Defaults". (GTK core has no split-button widget, so it is a linked button + menu button with a popover.) Reset only updates the widgets; nothing is written until Save & Apply. Verified: full suite 27 green; clang + release clean; wbconf maps with the pickers pre-selected and the reset button, no GTK/Cairo errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wbconf/main.cpp | 134 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 128 insertions(+), 6 deletions(-) diff --git a/wbconf/main.cpp b/wbconf/main.cpp index e201500..730070c 100644 --- a/wbconf/main.cpp +++ b/wbconf/main.cpp @@ -160,6 +160,12 @@ struct Ui { /* Live theme preview (redrawn when the theme/fonts change). */ GtkWidget *preview = nullptr; + GtkNotebook *notebook = nullptr; /* for "reset this page" */ + + /* The settings as loaded, so Save only writes fonts the user changed + * (the font buttons are seeded with a default so the picker pre-selects a + * font, but we must not persist that default unless it was edited). */ + wb::WayboxSettings original; }; const char *const kPlacement[] = {"Smart", "Center", "UnderMouse", nullptr}; @@ -411,6 +417,7 @@ void preview_refresh(Ui *ui) { } void populate(Ui *ui, const wb::WayboxSettings &s) { + ui->original = s; /* remember the loaded state (for change detection) */ /* Theme list: every installed theme, current one guaranteed present and * selected (obconf shows the active theme highlighted in the list). */ ui->theme_names = wb::installed_theme_names(); @@ -467,11 +474,14 @@ void populate(Ui *ui, const wb::WayboxSettings &s) { gtk_switch_set_active(ui->switcher_osd, s.switcher_osd.value_or(true)); gtk_switch_set_active(ui->switcher_wrap, s.switcher_wrap.value_or(true)); - font_set(ui->font_active, s.font_active_window); - font_set(ui->font_inactive, s.font_inactive_window); - font_set(ui->font_menu_header, s.font_menu_header); - font_set(ui->font_menu_item, s.font_menu_item); - font_set(ui->font_osd, s.font_osd); + /* Always show a concrete font (the configured one, else waybox's effective + * default Sans 10) so the picker opens with the current font selected and + * the user can just tweak the size. */ + font_set(ui->font_active, s.font_active_window.value_or("Sans 10")); + font_set(ui->font_inactive, s.font_inactive_window.value_or("Sans 10")); + font_set(ui->font_menu_header, s.font_menu_header.value_or("Sans 10")); + font_set(ui->font_menu_item, s.font_menu_item.value_or("Sans 10")); + font_set(ui->font_osd, s.font_osd.value_or("Sans 10")); preview_refresh(ui); } @@ -511,6 +521,20 @@ wb::WayboxSettings collect(Ui *ui) { s.font_menu_header = font_value(ui->font_menu_header); s.font_menu_item = font_value(ui->font_menu_item); s.font_osd = font_value(ui->font_osd); + + /* The font buttons are seeded with the effective default so the picker + * pre-selects a font; don't persist that default unless the user actually + * changed it from what was loaded. */ + auto keep = [](std::optional &v, + const std::optional &orig) { + if (!orig.has_value() && v == std::optional("Sans 10")) + v = std::nullopt; + }; + keep(s.font_active_window, ui->original.font_active_window); + keep(s.font_inactive_window, ui->original.font_inactive_window); + keep(s.font_menu_header, ui->original.font_menu_header); + keep(s.font_menu_item, ui->original.font_menu_item); + keep(s.font_osd, ui->original.font_osd); return s; } @@ -548,7 +572,67 @@ void on_close(GtkButton *, gpointer data) { gtk_window_close(ui->window); } -/* ---- obconf-style tab pages ------------------------------------------ */ +/* ---- Reset to defaults ----------------------------------------------- */ + +/* The default values for one notebook page, set straight onto its widgets. + * Page order matches build_window: 0 Theme, 1 Appearance, 2 Fonts, 3 Windows, + * 4 Margins, 5 Menu, 6 Switcher. */ +void reset_page_widgets(Ui *ui, int page) { + switch (page) { + case 0: /* Theme: clear the selection (built-in default theme). */ + gtk_list_box_unselect_all(ui->theme_list); + break; + case 1: /* Appearance */ + gtk_spin_button_set_value(ui->pad_y, 2); + gtk_spin_button_set_value(ui->button_size, 0); + gtk_spin_button_set_value(ui->resize_grab, 8); + break; + case 2: /* Fonts */ + font_set(ui->font_active, "Sans 10"); + font_set(ui->font_inactive, "Sans 10"); + font_set(ui->font_menu_header, "Sans 10"); + font_set(ui->font_menu_item, "Sans 10"); + font_set(ui->font_osd, "Sans 10"); + break; + case 3: /* Windows */ + gtk_drop_down_set_selected(ui->placement, 0); /* Smart */ + break; + case 4: /* Margins */ + gtk_spin_button_set_value(ui->margin_top, 0); + gtk_spin_button_set_value(ui->margin_bottom, 0); + gtk_spin_button_set_value(ui->margin_left, 0); + gtk_spin_button_set_value(ui->margin_right, 0); + break; + case 5: /* Menu */ + gtk_editable_set_text(GTK_EDITABLE(ui->menu_source), "builtin"); + gtk_drop_down_set_selected(ui->submenu_open, 0); /* hover */ + gtk_spin_button_set_value(ui->hover_delay, 100); + gtk_switch_set_active(ui->menu_wrap, TRUE); + gtk_switch_set_active(ui->menu_icons, TRUE); + break; + case 6: /* Switcher */ + gtk_drop_down_set_selected(ui->switcher_order, 0); /* mru */ + gtk_switch_set_active(ui->switcher_osd, TRUE); + gtk_switch_set_active(ui->switcher_wrap, TRUE); + break; + } + preview_refresh(ui); +} + +void on_reset_page(GtkButton *, gpointer data) { + auto *ui = static_cast(data); + int page = gtk_notebook_get_current_page(ui->notebook); + reset_page_widgets(ui, page); + set_status(ui, _("Reset this page to defaults (not yet saved).")); +} + +void on_reset_all(GtkButton *, gpointer data) { + auto *ui = static_cast(data); + gint pages = gtk_notebook_get_n_pages(ui->notebook); + for (gint i = 0; i < pages; i++) + reset_page_widgets(ui, i); + set_status(ui, _("Reset all settings to defaults (not yet saved).")); +} GtkWidget *build_theme_page(Ui *ui) { GtkWidget *page = make_page(); @@ -673,6 +757,7 @@ void build_window(GtkApplication *app, Ui *ui) { GtkWidget *notebook = gtk_notebook_new(); gtk_widget_set_vexpand(notebook, TRUE); gtk_box_append(GTK_BOX(vbox), notebook); + ui->notebook = GTK_NOTEBOOK(notebook); add_tab(notebook, build_theme_page(ui), _("Theme")); add_tab(notebook, build_appearance_page(ui), _("Appearance")); @@ -694,6 +779,43 @@ void build_window(GtkApplication *app, Ui *ui) { ui->status = GTK_LABEL(status); gtk_box_append(GTK_BOX(actions), status); + /* Reset: a split button — the main part resets the current page, the arrow + * opens a menu with "Reset This Page" / "Reset All to Defaults". GTK core + * has no split-button widget, so compose a linked button + menu button. */ + GtkWidget *reset_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_add_css_class(reset_box, "linked"); + + GtkWidget *reset_primary = gtk_button_new_with_label(_("Reset Page")); + g_signal_connect(reset_primary, "clicked", G_CALLBACK(on_reset_page), ui); + gtk_box_append(GTK_BOX(reset_box), reset_primary); + + GtkWidget *reset_menu = gtk_menu_button_new(); + GtkWidget *pop = gtk_popover_new(); + GtkWidget *pbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); + gtk_widget_set_margin_top(pbox, 4); + gtk_widget_set_margin_bottom(pbox, 4); + gtk_widget_set_margin_start(pbox, 4); + gtk_widget_set_margin_end(pbox, 4); + + GtkWidget *rp = gtk_button_new_with_label(_("Reset This Page")); + gtk_button_set_has_frame(GTK_BUTTON(rp), FALSE); + gtk_widget_set_halign(rp, GTK_ALIGN_FILL); + g_signal_connect(rp, "clicked", G_CALLBACK(on_reset_page), ui); + g_signal_connect_swapped(rp, "clicked", G_CALLBACK(gtk_popover_popdown), pop); + gtk_box_append(GTK_BOX(pbox), rp); + + GtkWidget *ra = gtk_button_new_with_label(_("Reset All to Defaults")); + gtk_button_set_has_frame(GTK_BUTTON(ra), FALSE); + gtk_widget_set_halign(ra, GTK_ALIGN_FILL); + g_signal_connect(ra, "clicked", G_CALLBACK(on_reset_all), ui); + g_signal_connect_swapped(ra, "clicked", G_CALLBACK(gtk_popover_popdown), pop); + gtk_box_append(GTK_BOX(pbox), ra); + + gtk_popover_set_child(GTK_POPOVER(pop), pbox); + gtk_menu_button_set_popover(GTK_MENU_BUTTON(reset_menu), pop); + gtk_box_append(GTK_BOX(reset_box), reset_menu); + gtk_box_append(GTK_BOX(actions), reset_box); + GtkWidget *close = gtk_button_new_with_label(_("Close")); g_signal_connect(close, "clicked", G_CALLBACK(on_close), ui); gtk_box_append(GTK_BOX(actions), close); From 4b0bdf31a8a281477173af7be1d2e67d8a99ac2a Mon Sep 17 00:00:00 2001 From: ebenali <54529923+ebenali@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:04:37 +0000 Subject: [PATCH 2/2] fix(wbconf): replace GtkFontDialog with an instant custom font picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GtkFontDialogButton opened GTK's font chooser, which loads its font list asynchronously and only highlights/scrolls to the current font after a multi-second delay — so the currently-used font appeared unselected until you waited or scrolled. Seeding the button's font-desc did not change this; it is GTK's font-chooser internals. Replace it with a lightweight custom picker per place: a searchable family drop-down + a size spin button + a bold toggle, built from the Pango font map. It is plain widgets, so the current family/size/bold are shown immediately and you can just scale the size down. Reading/writing still uses the Pango "Family [Bold] Size" convention, so RcDocument and the change-detection are unchanged; the live preview updates on family/size/bold changes. Verified: full suite 27 green; clang + release clean; wbconf maps with the new pickers and no GTK/Pango errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wbconf/main.cpp | 177 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 125 insertions(+), 52 deletions(-) diff --git a/wbconf/main.cpp b/wbconf/main.cpp index 730070c..fa59796 100644 --- a/wbconf/main.cpp +++ b/wbconf/main.cpp @@ -117,6 +117,15 @@ int signal_waybox_reload() { return signalled; } +/* ---- A lightweight font picker: family + size + bold ----------------- */ + +struct FontPicker { + GtkDropDown *family = nullptr; + GtkSpinButton *size = nullptr; + GtkToggleButton *bold = nullptr; + std::vector families; /* parallel to the dropdown model */ +}; + /* ---- Widget set, bound to a WayboxSettings on save ------------------- */ struct Ui { @@ -151,12 +160,16 @@ struct Ui { GtkSwitch *switcher_osd = nullptr; GtkSwitch *switcher_wrap = nullptr; - /* Fonts (GtkFontDialogButton per place) */ - GtkFontDialogButton *font_active = nullptr; - GtkFontDialogButton *font_inactive = nullptr; - GtkFontDialogButton *font_menu_header = nullptr; - GtkFontDialogButton *font_menu_item = nullptr; - GtkFontDialogButton *font_osd = nullptr; + /* Fonts: a lightweight custom picker per place (family dropdown + size spin + * + bold toggle). We avoid GtkFontDialogButton because GTK's font chooser + * loads its list asynchronously and only highlights/scrolls to the current + * font after a multi-second delay; the custom picker is instant and always + * shows the current selection. */ + FontPicker font_active; + FontPicker font_inactive; + FontPicker font_menu_header; + FontPicker font_menu_item; + FontPicker font_osd; /* Live theme preview (redrawn when the theme/fonts change). */ GtkWidget *preview = nullptr; @@ -256,35 +269,100 @@ GtkDropDown *add_dropdown(GtkWidget *grid, int row, const char *label, /* ---- Fonts + live theme preview -------------------------------------- */ -GtkFontDialogButton *add_font(GtkWidget *grid, int row, const char *label) { +/* The installed font families, sorted and de-duplicated. Uses the default + * Cairo/Pango font map, so no widget is needed. */ +const std::vector &font_families() { + static const std::vector families = [] { + std::vector out; + PangoFontMap *fm = pango_cairo_font_map_get_default(); + PangoFontFamily **fams = nullptr; + int n = 0; + pango_font_map_list_families(fm, &fams, &n); + out.reserve(n); + for (int i = 0; i < n; i++) { + const char *name = pango_font_family_get_name(fams[i]); + if (name != nullptr) + out.emplace_back(name); + } + g_free(fams); + std::sort(out.begin(), out.end()); + out.erase(std::unique(out.begin(), out.end()), out.end()); + return out; + }(); + return families; +} + +/* A custom, instant font picker: a family dropdown, a size spin button and a + * bold toggle, laid out in one row. Stored in `out`. */ +void add_font(GtkWidget *grid, int row, const char *label, FontPicker *out, + GCallback on_change, gpointer user) { grid_label(grid, row, label); - GtkFontDialog *dialog = gtk_font_dialog_new(); - GtkWidget *btn = gtk_font_dialog_button_new(dialog); - gtk_widget_set_hexpand(btn, TRUE); - gtk_grid_attach(GTK_GRID(grid), btn, 1, row, 1, 1); - return GTK_FONT_DIALOG_BUTTON(btn); -} - -/* Read a font button as a Pango-style "Family [Bold] Size" string, or nullopt - * if it was never set. */ -std::optional font_value(GtkFontDialogButton *b) { - PangoFontDescription *d = gtk_font_dialog_button_get_font_desc(b); - if (d == nullptr) - return std::nullopt; - char *s = pango_font_description_to_string(d); - std::string out = s ? s : ""; - g_free(s); - if (out.empty()) + + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_set_hexpand(box, TRUE); + + out->families = font_families(); + std::vector items; + items.reserve(out->families.size() + 1); + for (const std::string &f : out->families) + items.push_back(f.c_str()); + items.push_back(nullptr); + GtkWidget *fam = gtk_drop_down_new_from_strings(items.data()); + gtk_widget_set_hexpand(fam, TRUE); + /* Make the long list searchable by typing. */ + gtk_drop_down_set_enable_search(GTK_DROP_DOWN(fam), TRUE); + out->family = GTK_DROP_DOWN(fam); + gtk_box_append(GTK_BOX(box), fam); + + GtkWidget *size = gtk_spin_button_new_with_range(4, 96, 1); + out->size = GTK_SPIN_BUTTON(size); + gtk_box_append(GTK_BOX(box), size); + + GtkWidget *bold = gtk_toggle_button_new_with_label(_("B")); + gtk_widget_set_tooltip_text(bold, _("Bold")); + out->bold = GTK_TOGGLE_BUTTON(bold); + gtk_box_append(GTK_BOX(box), bold); + + gtk_grid_attach(GTK_GRID(grid), box, 1, row, 1, 1); + + if (on_change != nullptr) { + g_signal_connect_swapped(fam, "notify::selected", on_change, user); + g_signal_connect_swapped(size, "value-changed", on_change, user); + g_signal_connect_swapped(bold, "toggled", on_change, user); + } +} + +/* Read a picker as a Pango-style "Family [Bold] Size" string. */ +std::optional font_value(const FontPicker *p) { + guint idx = gtk_drop_down_get_selected(p->family); + if (idx == GTK_INVALID_LIST_POSITION || idx >= p->families.size()) return std::nullopt; + std::string out = p->families[idx]; + if (gtk_toggle_button_get_active(p->bold)) + out += " Bold"; + out += " " + std::to_string(gtk_spin_button_get_value_as_int(p->size)); return out; } -void font_set(GtkFontDialogButton *b, const std::optional &v) { +/* Set a picker from a Pango-style "Family [Bold] Size" string. */ +void font_set(FontPicker *p, const std::optional &v) { if (!v) return; PangoFontDescription *d = pango_font_description_from_string(v->c_str()); - gtk_font_dialog_button_set_font_desc(b, d); + const char *fam = pango_font_description_get_family(d); + std::string family = fam ? fam : "Sans"; + int size = pango_font_description_get_size(d); + int size_pt = size > 0 ? size / PANGO_SCALE : 10; + bool bold = pango_font_description_get_weight(d) >= PANGO_WEIGHT_BOLD; pango_font_description_free(d); + + guint sel = 0; + for (guint i = 0; i < p->families.size(); i++) { + if (p->families[i] == family) { sel = i; break; } + } + gtk_drop_down_set_selected(p->family, sel); + gtk_spin_button_set_value(p->size, size_pt); + gtk_toggle_button_set_active(p->bold, bold); } /* Collect the current settings from all widgets (forward declaration so the @@ -477,11 +555,11 @@ void populate(Ui *ui, const wb::WayboxSettings &s) { /* Always show a concrete font (the configured one, else waybox's effective * default Sans 10) so the picker opens with the current font selected and * the user can just tweak the size. */ - font_set(ui->font_active, s.font_active_window.value_or("Sans 10")); - font_set(ui->font_inactive, s.font_inactive_window.value_or("Sans 10")); - font_set(ui->font_menu_header, s.font_menu_header.value_or("Sans 10")); - font_set(ui->font_menu_item, s.font_menu_item.value_or("Sans 10")); - font_set(ui->font_osd, s.font_osd.value_or("Sans 10")); + font_set(&ui->font_active, s.font_active_window.value_or("Sans 10")); + font_set(&ui->font_inactive, s.font_inactive_window.value_or("Sans 10")); + font_set(&ui->font_menu_header, s.font_menu_header.value_or("Sans 10")); + font_set(&ui->font_menu_item, s.font_menu_item.value_or("Sans 10")); + font_set(&ui->font_osd, s.font_osd.value_or("Sans 10")); preview_refresh(ui); } @@ -516,11 +594,11 @@ wb::WayboxSettings collect(Ui *ui) { s.switcher_osd = gtk_switch_get_active(ui->switcher_osd); s.switcher_wrap = gtk_switch_get_active(ui->switcher_wrap); - s.font_active_window = font_value(ui->font_active); - s.font_inactive_window = font_value(ui->font_inactive); - s.font_menu_header = font_value(ui->font_menu_header); - s.font_menu_item = font_value(ui->font_menu_item); - s.font_osd = font_value(ui->font_osd); + s.font_active_window = font_value(&ui->font_active); + s.font_inactive_window = font_value(&ui->font_inactive); + s.font_menu_header = font_value(&ui->font_menu_header); + s.font_menu_item = font_value(&ui->font_menu_item); + s.font_osd = font_value(&ui->font_osd); /* The font buttons are seeded with the effective default so the picker * pre-selects a font; don't persist that default unless the user actually @@ -588,11 +666,11 @@ void reset_page_widgets(Ui *ui, int page) { gtk_spin_button_set_value(ui->resize_grab, 8); break; case 2: /* Fonts */ - font_set(ui->font_active, "Sans 10"); - font_set(ui->font_inactive, "Sans 10"); - font_set(ui->font_menu_header, "Sans 10"); - font_set(ui->font_menu_item, "Sans 10"); - font_set(ui->font_osd, "Sans 10"); + font_set(&ui->font_active, "Sans 10"); + font_set(&ui->font_inactive, "Sans 10"); + font_set(&ui->font_menu_header, "Sans 10"); + font_set(&ui->font_menu_item, "Sans 10"); + font_set(&ui->font_osd, "Sans 10"); break; case 3: /* Windows */ gtk_drop_down_set_selected(ui->placement, 0); /* Smart */ @@ -666,17 +744,12 @@ GtkWidget *build_fonts_page(Ui *ui) { GtkWidget *page = make_page(); GtkWidget *content = section(page, _("Fonts")); GtkWidget *grid = section_grid(content); - ui->font_active = add_font(grid, 0, _("Active window title:")); - ui->font_inactive = add_font(grid, 1, _("Inactive window title:")); - ui->font_menu_header = add_font(grid, 2, _("Menu header:")); - ui->font_menu_item = add_font(grid, 3, _("Menu item:")); - ui->font_osd = add_font(grid, 4, _("On-screen display:")); - /* Refresh the preview whenever a font changes. */ - for (GtkFontDialogButton *b : {ui->font_active, ui->font_inactive, - ui->font_menu_header, ui->font_menu_item, ui->font_osd}) { - g_signal_connect_swapped(b, "notify::font-desc", - G_CALLBACK(preview_refresh), ui); - } + GCallback refresh = G_CALLBACK(preview_refresh); + add_font(grid, 0, _("Active window title:"), &ui->font_active, refresh, ui); + add_font(grid, 1, _("Inactive window title:"), &ui->font_inactive, refresh, ui); + add_font(grid, 2, _("Menu header:"), &ui->font_menu_header, refresh, ui); + add_font(grid, 3, _("Menu item:"), &ui->font_menu_item, refresh, ui); + add_font(grid, 4, _("On-screen display:"), &ui->font_osd, refresh, ui); return page; }