/* PIKA - Photo and Image Kooker Application * a rebranding of The GNU Image Manipulation Program (created with heckimp) * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio * * Original copyright, applying to most contents (license remains unchanged): * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * pikaactionview.c * Copyright (C) 2004-2005 Michael Natterer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "config.h" #include #include #include #include #include "libpikabase/pikabase.h" #include "libpikawidgets/pikawidgets.h" #include "widgets-types.h" #include "core/pika.h" #include "pikaaction.h" #include "pikaactiongroup.h" #include "pikaactionview.h" #include "pikamessagebox.h" #include "pikamessagedialog.h" #include "pika-intl.h" /* local function prototypes */ static void pika_action_view_finalize (GObject *object); static void pika_action_view_select_path (PikaActionView *view, GtkTreePath *path); static void pika_action_view_accels_changed (PikaAction *action, gchar **accels, PikaActionView *view); static void pika_action_view_accel_edited (GtkCellRendererAccel *accel, const char *path_string, guint accel_key, GdkModifierType accel_mask, guint hardware_keycode, PikaActionView *view); static void pika_action_view_accel_cleared (GtkCellRendererAccel *accel, const char *path_string, PikaActionView *view); G_DEFINE_TYPE (PikaActionView, pika_action_view, GTK_TYPE_TREE_VIEW) #define parent_class pika_action_view_parent_class static void pika_action_view_class_init (PikaActionViewClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = pika_action_view_finalize; } static void pika_action_view_init (PikaActionView *view) { } static void pika_action_view_finalize (GObject *object) { PikaActionView *view = PIKA_ACTION_VIEW (object); if (view->pika) { if (view->show_shortcuts) { gchar **actions; actions = g_action_group_list_actions (G_ACTION_GROUP (view->pika->app)); for (gint i = 0; actions[i] != NULL; i++) { GAction *action; if (pika_action_is_gui_blacklisted (actions[i])) continue; action = g_action_map_lookup_action (G_ACTION_MAP (view->pika->app), actions[i]); g_signal_handlers_disconnect_by_func (action, pika_action_view_accels_changed, view); } g_strfreev (actions); } g_clear_object (&view->pika); } g_clear_pointer (&view->filter, g_free); G_OBJECT_CLASS (parent_class)->finalize (object); } static gint pika_action_view_name_compare (const void *name1, const void *name2) { return strcmp (*(gchar **) name1, *(gchar **) name2); } GtkWidget * pika_action_view_new (Pika *pika, const gchar *select_action, gboolean show_shortcuts) { gchar **actions; gchar *group_name = NULL; GtkTreeView *view; GtkTreeViewColumn *column; GtkCellRenderer *cell; GtkTreeStore *store; GtkTreeModel *filter; GtkTreePath *select_path = NULL; g_return_val_if_fail (PIKA_IS_PIKA (pika), NULL); store = gtk_tree_store_new (PIKA_ACTION_VIEW_N_COLUMNS, G_TYPE_BOOLEAN, /* COLUMN_VISIBLE */ PIKA_TYPE_ACTION, /* COLUMN_ACTION */ G_TYPE_STRING, /* COLUMN_ICON_NAME */ G_TYPE_STRING, /* COLUMN_LABEL */ G_TYPE_STRING, /* COLUMN_LABEL_CASEFOLD */ G_TYPE_STRING, /* COLUMN_NAME */ G_TYPE_UINT, /* COLUMN_ACCEL_KEY */ GDK_TYPE_MODIFIER_TYPE); /* COLUMN_ACCEL_MASK */ actions = g_action_group_list_actions (G_ACTION_GROUP (pika->app)); qsort (actions, g_strv_length (actions), sizeof (gchar *), pika_action_view_name_compare); for (gint i = 0; actions[i] != NULL; i++) { gchar **split_name; GtkTreeIter group_iter; GAction *action; const gchar *icon_name; gchar *label; gchar *label_casefold; guint accel_key = 0; GdkModifierType accel_mask = 0; GtkTreeIter action_iter; if (pika_action_is_gui_blacklisted (actions[i])) continue; split_name = g_strsplit (actions[i], "-", 2); if (group_name == NULL || g_strcmp0 (group_name, split_name[0]) != 0) { /* Since we sorted alphabetically and we use the first part of the * action name as group name, we ensure that we create each group only * once. */ g_free (group_name); group_name = g_strdup (split_name[0]); gtk_tree_store_append (store, &group_iter, NULL); gtk_tree_store_set (store, &group_iter, /* TODO: get back PikaActionGroup info? */ /*PIKA_ACTION_VIEW_COLUMN_ICON_NAME, group->icon_name,*/ /*PIKA_ACTION_VIEW_COLUMN_LABEL, group->label,*/ PIKA_ACTION_VIEW_COLUMN_LABEL, group_name, -1); } g_strfreev (split_name); action = g_action_map_lookup_action (G_ACTION_MAP (pika->app), actions[i]); g_return_val_if_fail (PIKA_IS_ACTION (action), NULL); icon_name = pika_action_get_icon_name (PIKA_ACTION (action)); label = pika_strip_uline (pika_action_get_label (PIKA_ACTION (action))); if (! (label && strlen (label))) { g_free (label); label = g_strdup (actions[i]); } label_casefold = g_utf8_casefold (label, -1); if (show_shortcuts) { const gchar **accels = NULL; accels = pika_action_get_accels (PIKA_ACTION (action)); /* TODO GAction: support multiple accelerators! */ if (accels && accels[0]) gtk_accelerator_parse (accels[0], &accel_key, &accel_mask); } gtk_tree_store_append (store, &action_iter, &group_iter); gtk_tree_store_set (store, &action_iter, PIKA_ACTION_VIEW_COLUMN_VISIBLE, TRUE, PIKA_ACTION_VIEW_COLUMN_ACTION, action, PIKA_ACTION_VIEW_COLUMN_ICON_NAME, icon_name, PIKA_ACTION_VIEW_COLUMN_LABEL, label, PIKA_ACTION_VIEW_COLUMN_LABEL_CASEFOLD, label_casefold, PIKA_ACTION_VIEW_COLUMN_NAME, actions[i], PIKA_ACTION_VIEW_COLUMN_ACCEL_KEY, accel_key, PIKA_ACTION_VIEW_COLUMN_ACCEL_MASK, accel_mask, -1); g_free (label); g_free (label_casefold); if (select_action && ! strcmp (select_action, actions[i])) select_path = gtk_tree_model_get_path (GTK_TREE_MODEL (store), &action_iter); } g_free (group_name); filter = gtk_tree_model_filter_new (GTK_TREE_MODEL (store), NULL); g_object_unref (store); view = g_object_new (PIKA_TYPE_ACTION_VIEW, "model", filter, "rules-hint", TRUE, NULL); g_object_unref (filter); gtk_tree_model_filter_set_visible_column (GTK_TREE_MODEL_FILTER (filter), PIKA_ACTION_VIEW_COLUMN_VISIBLE); PIKA_ACTION_VIEW (view)->pika = g_object_ref (pika); PIKA_ACTION_VIEW (view)->show_shortcuts = show_shortcuts; gtk_tree_view_set_search_column (GTK_TREE_VIEW (view), PIKA_ACTION_VIEW_COLUMN_LABEL); column = gtk_tree_view_column_new (); gtk_tree_view_column_set_title (column, _("Action")); cell = gtk_cell_renderer_pixbuf_new (); gtk_tree_view_column_pack_start (column, cell, FALSE); gtk_tree_view_column_set_attributes (column, cell, "icon-name", PIKA_ACTION_VIEW_COLUMN_ICON_NAME, NULL); cell = gtk_cell_renderer_text_new (); gtk_tree_view_column_pack_start (column, cell, TRUE); gtk_tree_view_column_set_attributes (column, cell, "text", PIKA_ACTION_VIEW_COLUMN_LABEL, NULL); gtk_tree_view_append_column (view, column); if (show_shortcuts) { for (gint i = 0; actions[i] != NULL; i++) { GAction *action; if (pika_action_is_gui_blacklisted (actions[i])) continue; action = g_action_map_lookup_action (G_ACTION_MAP (pika->app), actions[i]); g_signal_connect (action, "accels-changed", G_CALLBACK (pika_action_view_accels_changed), view); } column = gtk_tree_view_column_new (); gtk_tree_view_column_set_title (column, _("Shortcut")); cell = gtk_cell_renderer_accel_new (); g_object_set (cell, "mode", GTK_CELL_RENDERER_MODE_EDITABLE, "editable", TRUE, NULL); gtk_tree_view_column_pack_start (column, cell, TRUE); gtk_tree_view_column_set_attributes (column, cell, "accel-key", PIKA_ACTION_VIEW_COLUMN_ACCEL_KEY, "accel-mods", PIKA_ACTION_VIEW_COLUMN_ACCEL_MASK, NULL); g_signal_connect (cell, "accel-edited", G_CALLBACK (pika_action_view_accel_edited), view); g_signal_connect (cell, "accel-cleared", G_CALLBACK (pika_action_view_accel_cleared), view); gtk_tree_view_append_column (view, column); } g_strfreev (actions); column = gtk_tree_view_column_new (); gtk_tree_view_column_set_title (column, _("Name")); cell = gtk_cell_renderer_text_new (); gtk_tree_view_column_pack_start (column, cell, TRUE); gtk_tree_view_column_set_attributes (column, cell, "text", PIKA_ACTION_VIEW_COLUMN_NAME, NULL); gtk_tree_view_append_column (view, column); if (select_path) { pika_action_view_select_path (PIKA_ACTION_VIEW (view), select_path); gtk_tree_path_free (select_path); } return GTK_WIDGET (view); } void pika_action_view_set_filter (PikaActionView *view, const gchar *filter) { GtkTreeSelection *sel; GtkTreeModel *filtered_model; GtkTreeModel *model; GtkTreeIter iter; gboolean iter_valid; GtkTreeRowReference *selected_row = NULL; g_return_if_fail (PIKA_IS_ACTION_VIEW (view)); filtered_model = gtk_tree_view_get_model (GTK_TREE_VIEW (view)); model = gtk_tree_model_filter_get_model (GTK_TREE_MODEL_FILTER (filtered_model)); if (filter && ! strlen (filter)) filter = NULL; g_clear_pointer (&view->filter, g_free); if (filter) view->filter = g_utf8_casefold (filter, -1); sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (view)); if (gtk_tree_selection_get_selected (sel, NULL, &iter)) { GtkTreePath *path = gtk_tree_model_get_path (filtered_model, &iter); selected_row = gtk_tree_row_reference_new (filtered_model, path); } for (iter_valid = gtk_tree_model_get_iter_first (model, &iter); iter_valid; iter_valid = gtk_tree_model_iter_next (model, &iter)) { GtkTreeIter child_iter; gboolean child_valid; gint n_children = 0; for (child_valid = gtk_tree_model_iter_children (model, &child_iter, &iter); child_valid; child_valid = gtk_tree_model_iter_next (model, &child_iter)) { gboolean visible = TRUE; if (view->filter) { gchar *label; gchar *name; gtk_tree_model_get (model, &child_iter, PIKA_ACTION_VIEW_COLUMN_LABEL_CASEFOLD, &label, PIKA_ACTION_VIEW_COLUMN_NAME, &name, -1); visible = label && name && (strstr (label, view->filter) != NULL || strstr (name, view->filter) != NULL); g_free (label); g_free (name); } gtk_tree_store_set (GTK_TREE_STORE (model), &child_iter, PIKA_ACTION_VIEW_COLUMN_VISIBLE, visible, -1); if (visible) n_children++; } gtk_tree_store_set (GTK_TREE_STORE (model), &iter, PIKA_ACTION_VIEW_COLUMN_VISIBLE, n_children > 0, -1); } if (view->filter) gtk_tree_view_expand_all (GTK_TREE_VIEW (view)); else gtk_tree_view_collapse_all (GTK_TREE_VIEW (view)); gtk_tree_view_columns_autosize (GTK_TREE_VIEW (view)); if (selected_row) { if (gtk_tree_row_reference_valid (selected_row)) { GtkTreePath *path = gtk_tree_row_reference_get_path (selected_row); pika_action_view_select_path (view, path); gtk_tree_path_free (path); } gtk_tree_row_reference_free (selected_row); } } /* private functions */ static void pika_action_view_select_path (PikaActionView *view, GtkTreePath *path) { GtkTreeView *tv = GTK_TREE_VIEW (view); GtkTreePath *expand; expand = gtk_tree_path_copy (path); gtk_tree_path_up (expand); gtk_tree_view_expand_row (tv, expand, FALSE); gtk_tree_path_free (expand); gtk_tree_view_set_cursor (tv, path, NULL, FALSE); gtk_tree_view_scroll_to_cell (tv, path, NULL, TRUE, 0.5, 0.0); } static void pika_action_view_accels_changed (PikaAction *action, gchar **accels, PikaActionView *view) { GtkTreeModel *model, *tmpmodel; GtkTreeIter iter; gboolean iter_valid; gchar *pathstr = NULL; model = gtk_tree_view_get_model (GTK_TREE_VIEW (view)); if (! model) return; model = gtk_tree_model_filter_get_model (GTK_TREE_MODEL_FILTER (model)); if (! model) return; if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (GTK_TREE_VIEW (view)), &tmpmodel, &iter)) { GtkTreePath *path = gtk_tree_model_get_path (tmpmodel, &iter); pathstr = gtk_tree_path_to_string(path); } for (iter_valid = gtk_tree_model_get_iter_first (model, &iter); iter_valid; iter_valid = gtk_tree_model_iter_next (model, &iter)) { GtkTreeIter child_iter; gboolean child_valid; for (child_valid = gtk_tree_model_iter_children (model, &child_iter, &iter); child_valid; child_valid = gtk_tree_model_iter_next (model, &child_iter)) { PikaAction *it_action; gtk_tree_model_get (model, &child_iter, PIKA_ACTION_VIEW_COLUMN_ACTION, &it_action, -1); if (it_action) g_object_unref (it_action); if (it_action == action) { const gchar **accels; guint accel_key = 0; GdkModifierType accel_mask = 0; accels = pika_action_get_accels (action); if (accels && accels[0]) gtk_accelerator_parse (accels[0], &accel_key, &accel_mask); gtk_tree_store_set (GTK_TREE_STORE (model), &child_iter, PIKA_ACTION_VIEW_COLUMN_ACCEL_KEY, accel_key, PIKA_ACTION_VIEW_COLUMN_ACCEL_MASK, accel_mask, -1); gtk_tree_store_set (GTK_TREE_STORE (model), &iter, PIKA_ACTION_VIEW_COLUMN_VISIBLE, TRUE, -1); if (pathstr) { GtkTreePath *path = gtk_tree_path_new_from_string (pathstr); pika_action_view_select_path (view, path); gtk_tree_path_free (path); } g_free (pathstr); return; } } } g_free (pathstr); } typedef struct { Pika *pika; PikaAction *action; guint accel_key; GdkModifierType accel_mask; } ConfirmData; static void pika_action_view_conflict_response (GtkWidget *dialog, gint response_id, ConfirmData *confirm_data) { gtk_widget_destroy (dialog); if (response_id == GTK_RESPONSE_OK) { gchar **dup_actions; gchar *accel; accel = gtk_accelerator_name (confirm_data->accel_key, confirm_data->accel_mask); dup_actions = gtk_application_get_actions_for_accel (GTK_APPLICATION (confirm_data->pika->app), accel); for (gint i = 0; dup_actions[i] != NULL; i++) { GAction *conflict_action; gint start; gchar *left_paren_ptr = strchr (dup_actions[i], '('); if (left_paren_ptr) *left_paren_ptr = '\0'; /* ignore target part of detailed name */ start = g_str_has_prefix (dup_actions[i], "app.") ? 4 : 0; conflict_action = g_action_map_lookup_action (G_ACTION_MAP (confirm_data->pika->app), dup_actions[i] + start); g_return_if_fail (PIKA_IS_ACTION (conflict_action)); pika_action_set_accels (PIKA_ACTION (conflict_action), (const char*[]) { NULL }); } g_strfreev (dup_actions); pika_action_set_accels (confirm_data->action, (const char*[]) { accel, NULL }); } g_slice_free (ConfirmData, confirm_data); } static void pika_action_view_conflict_confirm (PikaActionView *view, PikaAction *action, PikaAction *edit_action, guint accel_key, GdkModifierType accel_mask) { PikaActionGroup *group; gchar *label; gchar *accel_string; ConfirmData *confirm_data; GtkWidget *dialog; PikaMessageBox *box; group = pika_action_get_group (action); label = pika_strip_uline (pika_action_get_label (action)); accel_string = gtk_accelerator_get_label (accel_key, accel_mask); confirm_data = g_slice_new (ConfirmData); confirm_data->pika = view->pika; confirm_data->action = edit_action; confirm_data->accel_key = accel_key; confirm_data->accel_mask = accel_mask; dialog = pika_message_dialog_new (_("Conflicting Shortcuts"), PIKA_ICON_DIALOG_WARNING, gtk_widget_get_toplevel (GTK_WIDGET (view)), 0, pika_standard_help_func, NULL, _("_Cancel"), GTK_RESPONSE_CANCEL, _("_Reassign Shortcut"), GTK_RESPONSE_OK, NULL); pika_dialog_set_alternative_button_order (GTK_DIALOG (dialog), GTK_RESPONSE_OK, GTK_RESPONSE_CANCEL, -1); g_signal_connect (dialog, "response", G_CALLBACK (pika_action_view_conflict_response), confirm_data); box = PIKA_MESSAGE_DIALOG (dialog)->box; pika_message_box_set_primary_text (box, _("Shortcut \"%s\" is already taken " "by \"%s\" from the \"%s\" group."), accel_string, label, group->label); pika_message_box_set_text (box, _("Reassigning the shortcut will cause it " "to be removed from \"%s\"."), label); g_free (label); g_free (accel_string); gtk_widget_show (dialog); } static void pika_action_view_get_accel_action (PikaActionView *view, const gchar *path_string, PikaAction **action_return, guint *action_accel_key, GdkModifierType *action_accel_mask) { GtkTreeModel *model; GtkTreePath *path; GtkTreeIter iter; model = gtk_tree_view_get_model (GTK_TREE_VIEW (view)); if (! model) return; path = gtk_tree_path_new_from_string (path_string); if (gtk_tree_model_get_iter (model, &iter, path)) { PikaAction *action; gtk_tree_model_get (model, &iter, PIKA_ACTION_VIEW_COLUMN_ACTION, &action, PIKA_ACTION_VIEW_COLUMN_ACCEL_KEY, action_accel_key, PIKA_ACTION_VIEW_COLUMN_ACCEL_MASK, action_accel_mask, -1); if (action) { g_object_unref (action); *action_return = action; } } gtk_tree_path_free (path); return; } static void pika_action_view_accel_edited (GtkCellRendererAccel *accel, const char *path_string, guint accel_key, GdkModifierType accel_mask, guint hardware_keycode, PikaActionView *view) { PikaAction *action; guint action_accel_key; GdkModifierType action_accel_mask; pika_action_view_get_accel_action (view, path_string, &action, &action_accel_key, &action_accel_mask); if (! action) return; if (accel_key == action_accel_key && accel_mask == action_accel_mask) return; if (! accel_key || /* Don't allow arrow keys, they are all swallowed by the canvas * and cannot be invoked anyway, the same applies to space. */ accel_key == GDK_KEY_Left || accel_key == GDK_KEY_Right || accel_key == GDK_KEY_Up || accel_key == GDK_KEY_Down || accel_key == GDK_KEY_space || accel_key == GDK_KEY_KP_Space) { pika_message_literal (view->pika, G_OBJECT (view), PIKA_MESSAGE_ERROR, _("Invalid shortcut.")); } else if (accel_key == GDK_KEY_F1 || action_accel_key == GDK_KEY_F1) { pika_message_literal (view->pika, G_OBJECT (view), PIKA_MESSAGE_ERROR, _("F1 cannot be remapped.")); } else if (accel_key >= GDK_KEY_0 && accel_key <= GDK_KEY_9 && accel_mask == GDK_MOD1_MASK) { pika_message (view->pika, G_OBJECT (view), PIKA_MESSAGE_ERROR, _("Alt+%d is used to switch to display %d and " "cannot be remapped."), accel_key - GDK_KEY_0, accel_key == GDK_KEY_0 ? 10 : accel_key - GDK_KEY_0); } else { gchar **dup_actions; gchar *accel = gtk_accelerator_name (accel_key, accel_mask); dup_actions = gtk_application_get_actions_for_accel (GTK_APPLICATION (view->pika->app), accel); if (dup_actions != NULL && dup_actions[0] != NULL) { PikaAction *conflict_action = NULL; gchar *left_paren_ptr0 = strchr (dup_actions[0], '('); if (left_paren_ptr0) *left_paren_ptr0 = '\0'; /* ignore target part of detailed name */ for (gint i = 0; dup_actions[i] != NULL; i++) { gint start; gchar *left_paren_ptr1 = strchr (dup_actions[i], '('); if (left_paren_ptr1) *left_paren_ptr1 = '\0'; /* ignore target part of detailed name */ start = g_str_has_prefix (dup_actions[i], "app.") ? 4 : 0; conflict_action = PIKA_ACTION (g_action_map_lookup_action (G_ACTION_MAP (view->pika->app), dup_actions[i] + start)); if (! conflict_action) continue; if (conflict_action != action) break; conflict_action = NULL; } if (conflict_action) pika_action_view_conflict_confirm (view, conflict_action, action, accel_key, accel_mask); else pika_message_literal (view->pika, G_OBJECT (view), PIKA_MESSAGE_ERROR, _("Changing shortcut failed.")); } else { pika_action_set_accels (action, (const char*[]) { accel, NULL }); } g_free (accel); g_strfreev (dup_actions); } } static void pika_action_view_accel_cleared (GtkCellRendererAccel *accel, const char *path_string, PikaActionView *view) { PikaAction *action; guint action_accel_key; GdkModifierType action_accel_mask; pika_action_view_get_accel_action (view, path_string, &action, &action_accel_key, &action_accel_mask); if (! action) return; if (action_accel_key == GDK_KEY_F1) { pika_message_literal (view->pika, G_OBJECT (view), PIKA_MESSAGE_ERROR, _("F1 cannot be remapped.")); return; } pika_action_set_accels (action, (const char*[]) { NULL }); }