// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later #include "native_internal.h" static GtkListStore *tentacleStore; static gboolean updating; static void name_toggle_cells(void) { AtkObject *accessible = gtk_widget_get_accessible(prefs_widget("tentaclesTree")); if (!ATK_IS_TABLE(accessible)) return; int rows = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(tentacleStore), NULL); for (int row = 0; row < rows; row++) { AtkObject *cell = atk_table_ref_at(ATK_TABLE(accessible), row, 0); if (!cell) continue; GtkTreeIter iter; gboolean enabled = FALSE; gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(tentacleStore), &iter, NULL, row); gtk_tree_model_get(GTK_TREE_MODEL(tentacleStore), &iter, 0, &enabled, -1); atk_object_set_name(cell, enabled ? "checked" : "unchecked"); g_object_unref(cell); } } static void tentacle_selected(GtkTreeSelection *selection, gpointer data) { (void)selection; (void)data; if (!updating) prefs_set_action(16); } static void tentacle_clicked(GtkButton *button, gpointer data) { (void)button; prefs_set_action(GPOINTER_TO_INT(data)); } static void tentacle_toggled(GtkCellRendererToggle *renderer, gchar *pathString, gpointer data) { (void)renderer; (void)data; if (updating) return; GtkTreePath *path = gtk_tree_path_new_from_string(pathString); gtk_tree_view_set_cursor(GTK_TREE_VIEW(prefs_widget("tentaclesTree")), path, NULL, FALSE); gtk_tree_path_free(path); // The accepted backend snapshot owns the check state; never check // optimistically before persistence or after an operation has failed. prefs_set_action(11); } static gboolean tentacle_key(GtkWidget *widget, GdkEventKey *event, gpointer data) { (void)widget; (void)data; if (event->keyval != GDK_KEY_space && event->keyval != GDK_KEY_KP_Space) return FALSE; prefs_set_action(11); return TRUE; } static void repository_switched(GtkNotebook *notebook, GtkWidget *page, guint index, gpointer data) { (void)notebook; (void)index; (void)data; GtkWidget *scroll = prefs_widget("tentacleTableScroll"); GtkWidget *parent = gtk_widget_get_parent(scroll); if (parent != page) { g_object_ref(scroll); gtk_container_remove(GTK_CONTAINER(parent), scroll); gtk_container_add(GTK_CONTAINER(page), scroll); g_object_unref(scroll); } if (!updating) prefs_set_action(17); } void prefs_init_tentacles(void) { updating = FALSE; tentacleStore = gtk_list_store_new(7, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); GtkTreeView *tree = GTK_TREE_VIEW(prefs_widget("tentaclesTree")); gtk_tree_view_set_model(tree, GTK_TREE_MODEL(tentacleStore)); GtkCellRenderer *toggle = gtk_cell_renderer_toggle_new(); g_signal_connect(toggle, "toggled", G_CALLBACK(tentacle_toggled), NULL); gtk_tree_view_append_column(tree, gtk_tree_view_column_new_with_attributes("Enabled", toggle, "active", 0, NULL)); const char *titles[] = {"Name", "Status", "Version", "Repository", "Details", "Shortcuts"}; for (unsigned i = 0; i < G_N_ELEMENTS(titles); 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 + 1, 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(tentacle_selected), NULL); g_signal_connect(tree, "key-press-event", G_CALLBACK(tentacle_key), NULL); g_signal_connect(prefs_widget("tentacleRepositories"), "switch-page", G_CALLBACK(repository_switched), NULL); const struct { const char *id; int action; } buttons[] = { {"tentacleNew", 9}, {"tentacleRetry", 13}, {"tentacleRemove", 14}, {"tentacleRefresh", 15}, {"tentacleRepositoryRemove", 12}, {"tentacleRepositoryRefresh", 18}, {"tentacleInstall", 19}, {"tentacleSettings", 20}, }; for (unsigned i = 0; i < G_N_ELEMENTS(buttons); i++) g_signal_connect(prefs_widget(buttons[i].id), "clicked", G_CALLBACK(tentacle_clicked), GINT_TO_POINTER(buttons[i].action)); atk_object_set_name(gtk_widget_get_accessible(GTK_WIDGET(tree)), "Tentacles"); atk_object_set_name(gtk_widget_get_accessible(prefs_widget("tentacleRepositoryURL")), "Repository URL"); atk_object_set_name(gtk_widget_get_accessible(prefs_widget("tentacleRepositories")), "Tentacle repositories"); g_object_unref(tentacleStore); // Owned by the tree. } void prefs_tentacles_clear(void) { updating = TRUE; gtk_list_store_clear(tentacleStore); } void prefs_tentacle_append(int enabled, const char *name, const char *state, const char *version, const char *repository, const char *message, const char *shortcuts) { GtkTreeIter iter; gtk_list_store_append(tentacleStore, &iter); gtk_list_store_set(tentacleStore, &iter, 0, enabled != 0, 1, name, 2, state, 3, version, 4, repository, 5, message, 6, shortcuts, -1); } int prefs_tentacle_selected(void) { GtkTreeModel *model; GtkTreeIter iter; GtkTreeView *tree = GTK_TREE_VIEW(prefs_widget("tentaclesTree")); if (!gtk_tree_selection_get_selected(gtk_tree_view_get_selection(tree), &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_tentacle_select(int row) { updating = TRUE; GtkTreeView *tree = GTK_TREE_VIEW(prefs_widget("tentaclesTree")); gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(tree)); if (row >= 0) { GtkTreePath *path = gtk_tree_path_new_from_indices(row, -1); if (gtk_widget_has_focus(GTK_WIDGET(tree))) gtk_tree_view_set_cursor(tree, path, NULL, FALSE); else gtk_tree_selection_select_path(gtk_tree_view_get_selection(tree), path); gtk_tree_path_free(path); } name_toggle_cells(); updating = FALSE; } void prefs_tentacle_controls(int available, int installed, int enabled, int repository) { gtk_widget_set_sensitive(prefs_widget("tentacleNew"), available); gtk_widget_set_sensitive(prefs_widget("tentacleRepositoryURL"), available); gtk_widget_set_sensitive(prefs_widget("tentacleRetry"), available && installed && enabled); gtk_widget_set_sensitive(prefs_widget("tentacleSettings"), available && installed); gtk_widget_set_sensitive(prefs_widget("tentacleRemove"), available && installed); gtk_widget_set_sensitive(prefs_widget("tentacleInstall"), available && !installed && prefs_tentacle_selected() >= 0); gtk_widget_set_sensitive(prefs_widget("tentacleRepositoryRefresh"), available && repository); gtk_widget_set_sensitive(prefs_widget("tentacleRepositoryRemove"), available && repository); } int prefs_tentacle_confirm(const char *message, const char *label) { GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(prefs_widget("window")), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", message); gtk_window_set_title(GTK_WINDOW(dialog), "Install tentacle"); gtk_dialog_add_button(GTK_DIALOG(dialog), "Cancel", GTK_RESPONSE_CANCEL); gtk_dialog_add_button(GTK_DIALOG(dialog), label, GTK_RESPONSE_ACCEPT); gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL); int response = gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); return response == GTK_RESPONSE_ACCEPT; } void prefs_tentacle_repositories_clear(void) { updating = TRUE; GtkNotebook *notebook = GTK_NOTEBOOK(prefs_widget("tentacleRepositories")); gtk_notebook_set_current_page(notebook, 0); while (gtk_notebook_get_n_pages(notebook) > 1) gtk_notebook_remove_page(notebook, -1); updating = FALSE; } void prefs_tentacle_repository_append(const char *label) { updating = TRUE; GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_notebook_append_page(GTK_NOTEBOOK(prefs_widget("tentacleRepositories")), page, gtk_label_new(label)); gtk_widget_show_all(page); updating = FALSE; } int prefs_tentacle_repository_selected(void) { return gtk_notebook_get_current_page(GTK_NOTEBOOK(prefs_widget("tentacleRepositories"))); } void prefs_tentacle_repository_select(int page) { updating = TRUE; gtk_notebook_set_current_page(GTK_NOTEBOOK(prefs_widget("tentacleRepositories")), page); updating = FALSE; }