// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later #include "native_internal.h" static GtkListStore *binding_store; static void binding_selected(GtkTreeSelection *selection, gpointer data) { (void)data; GtkTreeModel *model; GtkTreeIter iter; gboolean selected = gtk_tree_selection_get_selected(selection, &model, &iter); const char *controls[] = {"keyBindingEntry", "keyBindingSet", "keyBindingClear", "keyBindingReset"}; for (unsigned i = 0; i < G_N_ELEMENTS(controls); i++) gtk_widget_set_sensitive(prefs_widget(controls[i]), selected); gchar *keys = NULL; if (selected) gtk_tree_model_get(model, &iter, 2, &keys, -1); gtk_entry_set_text(GTK_ENTRY(prefs_widget("keyBindingEntry")), keys && strcmp(keys, "Unbound") != 0 ? keys : ""); g_free(keys); } static void binding_clicked(GtkButton *button, gpointer data) { (void)button; prefs_set_action(GPOINTER_TO_INT(data)); } void prefs_init_key_bindings(void) { binding_store = gtk_list_store_new(4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); GtkTreeView *tree = GTK_TREE_VIEW(prefs_widget("keyBindingsTreeview")); gtk_tree_view_set_model(tree, GTK_TREE_MODEL(binding_store)); const char *titles[] = {"Command", "Scope", "Binding", "Default binding"}; for (int i = 0; i < 4; i++) { GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); gtk_tree_view_append_column(tree, gtk_tree_view_column_new_with_attributes(titles[i], renderer, "text", i, NULL)); } gtk_tree_selection_set_mode(gtk_tree_view_get_selection(tree), GTK_SELECTION_SINGLE); g_signal_connect(gtk_tree_view_get_selection(tree), "changed", G_CALLBACK(binding_selected), NULL); const char *buttons[] = {"keyBindingSet", "keyBindingClear", "keyBindingReset", "keyBindingResetAll"}; for (int i = 0; i < 4; i++) g_signal_connect(prefs_widget(buttons[i]), "clicked", G_CALLBACK(binding_clicked), GINT_TO_POINTER(i + 4)); atk_object_set_name(gtk_widget_get_accessible(GTK_WIDGET(tree)), "Key bindings"); binding_selected(gtk_tree_view_get_selection(tree), NULL); g_object_unref(binding_store); // The tree owns the model. } void prefs_bindings_clear(void) { gtk_list_store_clear(binding_store); } void prefs_binding_append(const char *label, const char *scope, const char *keys, const char *defaults) { GtkTreeIter iter; gtk_list_store_append(binding_store, &iter); gtk_list_store_set(binding_store, &iter, 0, label, 1, scope, 2, keys, 3, defaults, -1); } int prefs_binding_selected(void) { GtkTreeModel *model; GtkTreeIter iter; if (!gtk_tree_selection_get_selected(gtk_tree_view_get_selection(GTK_TREE_VIEW(prefs_widget("keyBindingsTreeview"))), &model, &iter)) return -1; GtkTreePath *path = gtk_tree_model_get_path(model, &iter); int row = gtk_tree_path_get_indices(path)[0]; gtk_tree_path_free(path); return row; } void prefs_binding_select(int row) { if (row < 0) return; GtkTreePath *path = gtk_tree_path_new_from_indices(row, -1); // Setting the cursor grabs focus and can switch the notebook to this page // while loading preferences or changing the layout combo. Select the row // without moving keyboard focus out of the current control. GtkTreeView *tree = GTK_TREE_VIEW(prefs_widget("keyBindingsTreeview")); gtk_tree_selection_select_path(gtk_tree_view_get_selection(tree), path); gtk_tree_view_scroll_to_cell(tree, path, NULL, FALSE, 0, 0); gtk_tree_path_free(path); } static GtkListStore *modifierStore; static const struct { const char *id; const char *label; } modifierRows[] = { {"Insert", "Insert"}, {"KP_Insert", "Numpad Insert"}, {"Caps_Lock", "Caps Lock"}, }; static void name_modifier_cells(void) { AtkObject *accessible = gtk_widget_get_accessible(prefs_widget("cthulhuModifiersTreeview")); if (!ATK_IS_TABLE(accessible)) return; for (unsigned i = 0; i < G_N_ELEMENTS(modifierRows); i++) { AtkObject *cell = atk_table_ref_at(ATK_TABLE(accessible), i, 0); if (!cell) continue; gboolean enabled = prefs_modifier_get(modifierRows[i].id); const char *name = enabled ? "checked" : "unchecked"; if (g_strcmp0(atk_object_get_name(cell), name) != 0) atk_object_set_name(cell, name); g_object_unref(cell); } } void prefs_modifier_set(const char *id, int enabled) { for (unsigned i = 0; i < G_N_ELEMENTS(modifierRows); i++) { if (strcmp(id, modifierRows[i].id) != 0) continue; if (prefs_modifier_get(id) == (enabled != 0)) return; GtkTreeIter iter; if (gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(modifierStore), &iter, NULL, i)) gtk_list_store_set(modifierStore, &iter, 1, enabled != 0, -1); name_modifier_cells(); return; } } int prefs_modifier_get(const char *id) { for (unsigned i = 0; i < G_N_ELEMENTS(modifierRows); i++) { if (strcmp(id, modifierRows[i].id) != 0) continue; GtkTreeIter iter; gboolean enabled = FALSE; if (gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(modifierStore), &iter, NULL, i)) gtk_tree_model_get(GTK_TREE_MODEL(modifierStore), &iter, 1, &enabled, -1); return enabled; } return FALSE; } static void toggle_modifier_path(const char *pathString) { GtkTreeIter iter; if (!gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(modifierStore), &iter, pathString)) return; gboolean enabled; gtk_tree_model_get(GTK_TREE_MODEL(modifierStore), &iter, 1, &enabled, -1); gtk_list_store_set(modifierStore, &iter, 1, !enabled, -1); name_modifier_cells(); } static void modifier_toggled(GtkCellRendererToggle *renderer, gchar *pathString, gpointer data) { (void)renderer; (void)data; toggle_modifier_path(pathString); } static gboolean modifier_key_press(GtkWidget *view, GdkEventKey *event, gpointer data) { (void)data; if (event->keyval != GDK_KEY_space && event->keyval != GDK_KEY_KP_Space) return FALSE; GtkTreePath *path = NULL; gtk_tree_view_get_cursor(GTK_TREE_VIEW(view), &path, NULL); if (!path) return FALSE; gchar *pathString = gtk_tree_path_to_string(path); toggle_modifier_path(pathString); g_free(pathString); gtk_tree_path_free(path); return TRUE; } static void layout_modifiers(GtkToggleButton *button, gpointer data) { if (!gtk_toggle_button_get_active(button)) return; int laptop = GPOINTER_TO_INT(data); prefs_modifier_set("Insert", FALSE); prefs_modifier_set("KP_Insert", !laptop); prefs_modifier_set("Caps_Lock", laptop); } void prefs_init_modifiers(void) { modifierStore = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_BOOLEAN); for (unsigned i = 0; i < G_N_ELEMENTS(modifierRows); i++) { GtkTreeIter iter; gtk_list_store_append(modifierStore, &iter); gtk_list_store_set(modifierStore, &iter, 0, modifierRows[i].label, 1, FALSE, -1); } GtkTreeView *view = GTK_TREE_VIEW(prefs_widget("cthulhuModifiersTreeview")); gtk_tree_view_set_model(view, GTK_TREE_MODEL(modifierStore)); GtkCellRenderer *textRenderer = gtk_cell_renderer_text_new(); GtkCellRenderer *toggleRenderer = gtk_cell_renderer_toggle_new(); g_signal_connect(toggleRenderer, "toggled", G_CALLBACK(modifier_toggled), NULL); gtk_tree_view_append_column(view, gtk_tree_view_column_new_with_attributes("Enabled", toggleRenderer, "active", 1, NULL)); gtk_tree_view_append_column(view, gtk_tree_view_column_new_with_attributes("Cthulhu key", textRenderer, "text", 0, NULL)); g_signal_connect(view, "key-press-event", G_CALLBACK(modifier_key_press), NULL); AtkObject *accessible = gtk_widget_get_accessible(GTK_WIDGET(view)); atk_object_set_name(accessible, "Cthulhu modifier keys"); atk_object_set_description(accessible, "Use arrow keys to select Insert, Numpad Insert, or Caps Lock. Space toggles each key independently. Tab leaves the table."); g_signal_connect(prefs_widget("generalDesktopButton"), "toggled", G_CALLBACK(layout_modifiers), GINT_TO_POINTER(0)); g_signal_connect(prefs_widget("generalLaptopButton"), "toggled", G_CALLBACK(layout_modifiers), GINT_TO_POINTER(1)); name_modifier_cells(); g_object_unref(modifierStore); // The tree owns the model. }