// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later #include "native_internal.h" static GHashTable *extensionPages; static GHashTable *extensionControls; static void extension_combo_changed(GtkComboBox *combo, gpointer data) { (void)data; prefs_describe_combo(GTK_WIDGET(combo), g_object_get_data(G_OBJECT(combo), "extension-label")); } static char *control_key(const char *page, const char *id) { return g_strconcat(page, "/", id, NULL); } static GtkWidget *extension_control(const char *page, const char *id) { char *key = control_key(page, id); GtkWidget *widget = extensionControls ? g_hash_table_lookup(extensionControls, key) : NULL; g_free(key); return widget; } void prefs_extension_add(const char *key, const char *title) { if (!extensionPages) { extensionPages = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); extensionControls = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); } GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); GtkWidget *grid = gtk_grid_new(); gtk_grid_set_row_spacing(GTK_GRID(grid), 8); gtk_grid_set_column_spacing(GTK_GRID(grid), 12); gtk_container_set_border_width(GTK_CONTAINER(grid), 12); gtk_container_add(GTK_CONTAINER(scroll), grid); g_object_set_data(G_OBJECT(scroll), "extension-grid", grid); gtk_notebook_append_page(GTK_NOTEBOOK(prefs_widget("notebook")), scroll, gtk_label_new(title)); g_hash_table_insert(extensionPages, g_strdup(key), scroll); gtk_widget_show_all(scroll); } void prefs_extension_remove(const char *key) { if (!extensionPages) return; GtkWidget *page = g_hash_table_lookup(extensionPages, key); if (!page) return; GtkNotebook *notebook = GTK_NOTEBOOK(prefs_widget("notebook")); int pageIndex = gtk_notebook_page_num(notebook, page); GtkWidget *focus = gtk_window_get_focus(GTK_WINDOW(prefs_widget("window"))); if (gtk_notebook_get_current_page(notebook) == pageIndex) { // Switch while the old page is still realized. GTK must not attempt to // restore a focus child from a page whose controls are being destroyed. gtk_notebook_set_current_page(notebook, 0); gtk_widget_grab_focus(GTK_WIDGET(notebook)); } else if (focus && gtk_widget_is_ancestor(focus, page)) { gtk_widget_grab_focus(GTK_WIDGET(notebook)); } GHashTableIter iterator; gpointer controlKey, widget; char *prefix = g_strconcat(key, "/", NULL); g_hash_table_iter_init(&iterator, extensionControls); while (g_hash_table_iter_next(&iterator, &controlKey, &widget)) { if (g_str_has_prefix(controlKey, prefix)) g_hash_table_iter_remove(&iterator); } g_free(prefix); gtk_notebook_remove_page(notebook, pageIndex); g_hash_table_remove(extensionPages, key); } void prefs_extensions_close(void) { g_clear_pointer(&extensionControls, g_hash_table_destroy); g_clear_pointer(&extensionPages, g_hash_table_destroy); } const char *prefs_extension_selected(void) { if (!extensionPages) return ""; GtkNotebook *notebook = GTK_NOTEBOOK(prefs_widget("notebook")); GtkWidget *current = gtk_notebook_get_nth_page(notebook, gtk_notebook_get_current_page(notebook)); GHashTableIter iterator; gpointer key, page; g_hash_table_iter_init(&iterator, extensionPages); while (g_hash_table_iter_next(&iterator, &key, &page)) { if (page == current) return key; } return ""; } void prefs_extension_select(const char *key) { if (!extensionPages) return; GtkWidget *page = g_hash_table_lookup(extensionPages, key); if (page) { GtkNotebook *notebook = GTK_NOTEBOOK(prefs_widget("notebook")); gtk_notebook_set_current_page(notebook, gtk_notebook_page_num(notebook, page)); } } void prefs_extension_control(const char *pageKey, const char *id, const char *label, int kind, int row, int maxLength, int disabled) { GtkWidget *page = g_hash_table_lookup(extensionPages, pageKey); GtkWidget *grid = g_object_get_data(G_OBJECT(page), "extension-grid"); GtkWidget *control; if (kind == 1) { control = gtk_combo_box_text_new(); // Focus the internal combo button so Tab and Shift+Tab leave it normally. gtk_widget_set_can_focus(control, FALSE); g_object_set_data_full(G_OBJECT(control), "extension-label", g_strdup(label), g_free); g_signal_connect(control, "changed", G_CALLBACK(extension_combo_changed), NULL); } else if (kind == 2) { control = gtk_entry_new(); gtk_entry_set_max_length(GTK_ENTRY(control), maxLength ? maxLength : 8192); } else { control = gtk_check_button_new_with_label(label); } atk_object_set_name(gtk_widget_get_accessible(control), label); gtk_widget_set_hexpand(control, TRUE); gtk_widget_set_sensitive(control, !disabled); if (kind != 3) { GtkWidget *name = gtk_label_new(label); gtk_label_set_xalign(GTK_LABEL(name), 0); gtk_label_set_mnemonic_widget(GTK_LABEL(name), control); gtk_grid_attach(GTK_GRID(grid), name, 0, row, 1, 1); gtk_grid_attach(GTK_GRID(grid), control, 1, row, 1, 1); } else { gtk_grid_attach(GTK_GRID(grid), control, 0, row, 2, 1); } g_hash_table_insert(extensionControls, control_key(pageKey, id), control); gtk_widget_show_all(grid); } void prefs_extension_option(const char *page, const char *id, const char *option, const char *label, int disabled) { // Disabled choices are omitted from native selection, preserving their IDs // in the host schema so an old/malicious draft is still rejected there. if (!disabled) gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(extension_control(page, id)), option, label); } void prefs_extension_set(const char *page, const char *id, int kind, const char *text, int checked) { GtkWidget *widget = extension_control(page, id); if (kind == 1) gtk_combo_box_set_active_id(GTK_COMBO_BOX(widget), text); else if (kind == 2) gtk_entry_set_text(GTK_ENTRY(widget), text); else gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), checked); } const char *prefs_extension_text(const char *page, const char *id, int kind) { GtkWidget *widget = extension_control(page, id); const char *value = kind == 1 ? gtk_combo_box_get_active_id(GTK_COMBO_BOX(widget)) : gtk_entry_get_text(GTK_ENTRY(widget)); return value ? value : ""; } int prefs_extension_checked(const char *page, const char *id) { return gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(extension_control(page, id))); }