/** * @file gnome-cmd-key-shortcuts-dialog.cc * @copyright (C) 2001-2006 Marcus Bjurman\n * @copyright (C) 2007-2012 Piotr Eljasiak\n * @copyright (C) 2013-2023 Uwe Scholz\n * * @copyright 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 2 of the License, or * (at your option) any later version. * * @copyright 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. * * @copyright You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "gnome-cmd-includes.h" #include "gnome-cmd-key-shortcuts-dialog.h" #include "eggcellrendererkeys.h" #include "gnome-cmd-data.h" #include "gnome-cmd-treeview.h" #include "gnome-cmd-hintbox.h" #include "dict.h" #include "utils.h" using namespace std; #define GNOME_CMD_TYPE_KEY_SHORTCUTS_DIALOG (gnome_cmd_key_shortcuts_dialog_get_type()) #define GNOME_CMD_KEY_SHORTCUTS_DIALOG(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GNOME_CMD_TYPE_KEY_SHORTCUTS_DIALOG, GnomeCmdKeyShortcutsDialog)) #define GNOME_CMD_KEY_SHORTCUTS_DIALOG_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GNOME_CMD_TYPE_KEY_SHORTCUTS_DIALOG, GnomeCmdKeyShortcutsDialogClass)) #define GNOME_CMD_IS_KEY_SHORTCUTS_DIALOG(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GNOME_CMD_TYPE_KEY_SHORTCUTS_DIALOG) GtkTreeViewColumn *create_new_accel_column (GtkTreeView *view, GtkCellRenderer *&renderer, gint COL_KEYS_ID, gint COL_MODS_ID, const gchar *title); GtkTreeViewColumn *create_new_combo_column (GtkTreeView *view, GtkTreeModel *model, GtkCellRenderer *&renderer, gint COL_ID, const gchar *title); gboolean conflict_confirm (GtkWidget *view, const gchar *action, guint accel_key, GdkModifierType accel_mask); gboolean equal_accel (GtkTreeModel *model, GtkTreeIter *i, guint key, GdkModifierType mask); gboolean find_accel (GtkTreeModel *model, GtkTreeIter *i, guint key, GdkModifierType mask); void set_accel (GtkTreeModel *model, GtkTreePath *path, guint accel_key, GdkModifierType accel_mask); struct GnomeCmdKeyShortcutsDialogPrivate { GnomeCmdKeyShortcutsDialogPrivate(); }; struct GnomeCmdKeyShortcutsDialog { GtkDialog parent; GnomeCmdKeyShortcutsDialogPrivate *priv; static GnomeCmdUserActions *user_actions; }; struct GnomeCmdKeyShortcutsDialogClass { GtkDialogClass parent_class; }; GnomeCmdKeyShortcutsDialogPrivate::GnomeCmdKeyShortcutsDialogPrivate() { } GnomeCmdUserActions *GnomeCmdKeyShortcutsDialog::user_actions = NULL; #if defined (__GNUC__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmissing-declarations" #endif G_DEFINE_TYPE (GnomeCmdKeyShortcutsDialog, gnome_cmd_key_shortcuts_dialog, GTK_TYPE_DIALOG) #if defined (__GNUC__) #pragma GCC diagnostic pop #endif static void gnome_cmd_key_shortcuts_dialog_finalize (GObject *object) { GnomeCmdKeyShortcutsDialog *dialog = GNOME_CMD_KEY_SHORTCUTS_DIALOG (object); delete dialog->priv; G_OBJECT_CLASS (gnome_cmd_key_shortcuts_dialog_parent_class)->finalize (object); } enum { COL_ACCEL_KEY, COL_ACCEL_MASK, COL_ACTION, COL_NAME, COL_OPTION, NUM_COLUMNS } ; static void response_callback (GnomeCmdKeyShortcutsDialog *dialog, int response_id, GtkWidget *view) { switch (response_id) { case GTK_RESPONSE_OK: if (dialog->user_actions) { dialog->user_actions->clear(); GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (view)); GtkTreeIter i; // copy model -> dialog->user_actions for (gboolean valid_iter=gtk_tree_model_get_iter_first (model, &i); valid_iter; valid_iter=gtk_tree_model_iter_next (model, &i)) { guint accel_key = 0; guint accel_mask = 0; gchar *name = NULL; gchar *options = NULL; gtk_tree_model_get (model, &i, COL_ACCEL_KEY, &accel_key, COL_ACCEL_MASK, &accel_mask, COL_NAME, &name, COL_OPTION, &options, -1); if (accel_key) dialog->user_actions->register_action(accel_mask, accel_key, name, g_strstrip (options)); g_free (name); g_free (options); } dialog->user_actions->unregister(GDK_F3); dialog->user_actions->unregister(GDK_F4); dialog->user_actions->unregister(GDK_F5); dialog->user_actions->unregister(GDK_F6); dialog->user_actions->unregister(GDK_F7); dialog->user_actions->unregister(GDK_F8); dialog->user_actions->unregister(GDK_F9); dialog->user_actions->register_action(GDK_F3, "file.view"); dialog->user_actions->register_action(GDK_F4, "file.edit"); dialog->user_actions->register_action(GDK_F5, "file.copy"); dialog->user_actions->register_action(GDK_F6, "file.rename"); dialog->user_actions->register_action(GDK_F7, "file.mkdir"); dialog->user_actions->register_action(GDK_F8, "file.delete"); dialog->user_actions->register_action(GDK_F9, "edit.search"); } break; case GTK_RESPONSE_NONE: case GTK_RESPONSE_DELETE_EVENT: case GTK_RESPONSE_CANCEL: break; case GTK_RESPONSE_HELP: gnome_cmd_help_display ("gnome-commander.xml", "gnome-commander-keyboard"); g_signal_stop_emission_by_name (dialog, "response"); break; default : g_assert_not_reached (); } } static void gnome_cmd_key_shortcuts_dialog_class_init (GnomeCmdKeyShortcutsDialogClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = gnome_cmd_key_shortcuts_dialog_finalize; } GtkWidget *create_view_and_model (GnomeCmdUserActions &user_actions); GtkTreeModel *create_and_fill_model (GnomeCmdUserActions &user_actions); static void accel_edited_callback (GtkCellRendererAccel *accel, const char *path_string, guint accel_key, GdkModifierType accel_mask, guint hardware_keycode, GtkWidget *view); static void cell_edited_callback (GtkCellRendererText *cell, gchar *path_string, gchar *new_text, GtkWidget *view); static void add_clicked_callback (GtkButton *button, GtkWidget *view); static void remove_clicked_callback (GtkButton *button, GtkWidget *view); static void gnome_cmd_key_shortcuts_dialog_init (GnomeCmdKeyShortcutsDialog *dialog) { dialog->priv = new GnomeCmdKeyShortcutsDialogPrivate; gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_CENTER); gtk_window_set_title (GTK_WINDOW (dialog), _("Keyboard Shortcuts")); gtk_dialog_set_has_separator (GTK_DIALOG (dialog), FALSE); gtk_container_set_border_width (GTK_CONTAINER (dialog), 5); gtk_box_set_spacing (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), 2); gtk_window_set_resizable (GTK_WINDOW (dialog), TRUE); GtkWidget *vbox = gtk_vbox_new (FALSE, 12); gtk_container_set_border_width (GTK_CONTAINER (vbox), 12); gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), vbox); gtk_widget_show (vbox); GtkWidget *hbox = gtk_hbox_new (FALSE, 12); gtk_container_add (GTK_CONTAINER (vbox), hbox); gtk_widget_show (hbox); GtkWidget *scrolled_window = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window), GTK_SHADOW_IN); gtk_box_pack_start (GTK_BOX (hbox), scrolled_window, TRUE, TRUE, 0); gtk_widget_show (scrolled_window); GtkWidget *view = create_view_and_model (*dialog->user_actions); gtk_widget_set_size_request (view, 600, 400); gtk_container_add (GTK_CONTAINER (scrolled_window), view); gtk_widget_show (view); GtkWidget *box = gnome_cmd_hint_box_new (_("To edit a shortcut key, click on the " "corresponding row and type a new " "accelerator, or press escape to " "cancel.")); gtk_box_pack_start (GTK_BOX (vbox), box, FALSE, FALSE, 0); gtk_widget_show (box); vbox = gtk_vbox_new (FALSE, 12); gtk_box_pack_start (GTK_BOX (hbox), vbox, FALSE, FALSE, 0); gtk_widget_show (vbox); GtkWidget *button = gtk_button_new_from_stock (GTK_STOCK_ADD); g_signal_connect (button, "clicked", G_CALLBACK (add_clicked_callback), view); gtk_box_pack_start (GTK_BOX (vbox), button, FALSE, FALSE, 0); gtk_widget_show (button); button = gtk_button_new_from_stock (GTK_STOCK_REMOVE); g_signal_connect (button, "clicked", G_CALLBACK (remove_clicked_callback), view); gtk_box_pack_start (GTK_BOX (vbox), button, FALSE, FALSE, 0); gtk_widget_show (button); gtk_dialog_add_buttons (GTK_DIALOG (dialog), GTK_STOCK_HELP, GTK_RESPONSE_HELP, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_OK, GTK_RESPONSE_OK, NULL); gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK); g_signal_connect (dialog, "response", G_CALLBACK (response_callback), view); gtk_widget_grab_focus (view); } gboolean gnome_cmd_key_shortcuts_dialog_new (GnomeCmdUserActions &user_actions) { GnomeCmdKeyShortcutsDialog::user_actions = &user_actions; // ugly hack, but can't come to any better method of passing data to gnome_cmd_key_shortcuts_dialog_init () GtkWidget *dialog = gtk_widget_new (GNOME_CMD_TYPE_KEY_SHORTCUTS_DIALOG, NULL); g_return_val_if_fail (dialog != NULL, FALSE); gint result = gtk_dialog_run (GTK_DIALOG (dialog)); gtk_widget_destroy (dialog); return result==GTK_RESPONSE_OK; } GtkTreeViewColumn *create_new_accel_column (GtkTreeView *view, GtkCellRenderer *&renderer, gint COL_KEYS_ID, gint COL_MODS_ID, const gchar *title=NULL) { renderer = egg_cell_renderer_keys_new (); g_object_set (renderer, "editable", TRUE, "accel-mode", GTK_CELL_RENDERER_ACCEL_MODE_OTHER, NULL); GtkTreeViewColumn *col = gtk_tree_view_column_new_with_attributes (title, renderer, "accel-key", COL_KEYS_ID, "accel-mods", COL_MODS_ID, NULL); g_object_set (col, "clickable", TRUE, "resizable", TRUE, NULL); // pack tree view column into tree view gtk_tree_view_append_column (GTK_TREE_VIEW (view), col); return col; } GtkTreeViewColumn *create_new_combo_column (GtkTreeView *view, GtkTreeModel *model, GtkCellRenderer *&renderer, gint COL_ID, const gchar *title=NULL) { renderer = gtk_cell_renderer_combo_new (); g_object_set (renderer, "model", model, "text-column", 2, "has-entry", FALSE, "editable", TRUE, NULL); GtkTreeViewColumn *col = gtk_tree_view_column_new_with_attributes (title, renderer, "text", COL_ID, NULL); g_object_set (col, "clickable", TRUE, "resizable", TRUE, NULL); g_object_set_data (G_OBJECT (renderer), "column", GINT_TO_POINTER (COL_ID)); // pack tree view column into tree view gtk_tree_view_append_column (GTK_TREE_VIEW (view), col); return col; } enum { SORTID_ACCEL, SORTID_ACTION, SORTID_OPTION }; GtkWidget *create_view_and_model (GnomeCmdUserActions &user_actions) { GtkWidget *view = gtk_tree_view_new (); g_object_set (view, "rules-hint", TRUE, "enable-search", TRUE, "search-column", COL_ACTION, NULL); GtkCellRenderer *renderer = NULL; GtkTreeViewColumn *col = NULL; col = create_new_accel_column (GTK_TREE_VIEW (view), renderer, COL_ACCEL_KEY, COL_ACCEL_MASK, _("Shortcut Key")); gtk_widget_set_tooltip_text (gtk_tree_view_column_get_button (col), _("Keyboard shortcut for selected action")); gtk_tree_view_column_set_sort_column_id (col, SORTID_ACCEL); g_signal_connect (renderer, "accel-edited", G_CALLBACK (accel_edited_callback), view); GtkTreeModel *combo_model = gnome_cmd_user_actions_create_model (); col = create_new_combo_column (GTK_TREE_VIEW (view), combo_model, renderer, COL_ACTION, _("Action")); gtk_widget_set_tooltip_text (gtk_tree_view_column_get_button (col), _("User action")); gtk_tree_view_column_set_sort_column_id (col, SORTID_ACTION); g_signal_connect (renderer, "edited", (GCallback) cell_edited_callback, view); g_object_unref (combo_model); // destroy model automatically with view col = gnome_cmd_treeview_create_new_text_column (GTK_TREE_VIEW (view), renderer, COL_OPTION, _("Options")); gtk_widget_set_tooltip_text (gtk_tree_view_column_get_button (col), _("Optional data")); gtk_tree_view_column_set_sort_column_id (col, SORTID_OPTION); g_signal_connect (renderer, "edited", (GCallback) cell_edited_callback, view); g_object_set (renderer, "editable", TRUE, "ellipsize-set", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL); GtkTreeModel *model = create_and_fill_model (user_actions); gtk_tree_view_set_model (GTK_TREE_VIEW (view), model); g_object_unref (model); // destroy model automatically with view GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view)); gtk_tree_selection_set_mode (selection, GTK_SELECTION_BROWSE); GtkTreeIter iter; if (gtk_tree_model_get_iter_first (gtk_tree_view_get_model (GTK_TREE_VIEW (view)), &iter)) // select the first row here... gtk_tree_selection_select_iter (selection, &iter); return view; } static gint sort_by_col (GtkTreeModel *model, GtkTreeIter *i1, GtkTreeIter *i2, gpointer COL) { gchar *s1; gchar *s2; gtk_tree_model_get (model, i1, GPOINTER_TO_UINT (COL), &s1, -1); gtk_tree_model_get (model, i2, GPOINTER_TO_UINT (COL), &s2, -1); gint retval = 0; if (!s1 && !s2) return retval; if (!s1) retval = 1; else if (!s2) retval = -1; else { // compare s1 and s2 in UTF8 aware way, case insensitive gchar *is1 = g_utf8_casefold (s1, -1); gchar *is2 = g_utf8_casefold (s2, -1); retval = g_utf8_collate (is1, is2); g_free (is1); g_free (is2); } g_free (s1); g_free (s2); return retval; } static gint sort_by_accel (GtkTreeModel *model, GtkTreeIter *i1, GtkTreeIter *i2, gpointer user_data) { guint key1, key2; GdkModifierType mask1, mask2; gtk_tree_model_get (model, i1, COL_ACCEL_KEY, &key1, COL_ACCEL_MASK, &mask1, -1); gtk_tree_model_get (model, i2, COL_ACCEL_KEY, &key2, COL_ACCEL_MASK, &mask2, -1); if (mask1mask2) return 1; return key1 - key2; } GtkTreeModel *create_and_fill_model (GnomeCmdUserActions &user_actions) { GtkListStore *store = gtk_list_store_new (NUM_COLUMNS, G_TYPE_UINT, // COL_ACCEL_KEY GDK_TYPE_MODIFIER_TYPE, // COL_ACCEL_MASK G_TYPE_STRING, // COL_ACTION G_TYPE_STRING, // COL_NAME G_TYPE_STRING); // COL_OPTION GtkTreeIter iter; for (GnomeCmdUserActions::const_iterator i=user_actions.begin(); i!=user_actions.end(); ++i) if (!ascii_isupper (*i)) // ignore lowercase keys as they duplicate uppercase ones { gtk_list_store_append (store, &iter); gtk_list_store_set (store, &iter, COL_ACCEL_KEY, i->first.keyval, COL_ACCEL_MASK, i->first.state, COL_ACTION, user_actions.description(i), COL_NAME, user_actions.name(i), COL_OPTION, user_actions.options(i), -1); } GtkTreeSortable *sortable = GTK_TREE_SORTABLE (store); gtk_tree_sortable_set_sort_func (sortable, SORTID_ACCEL, sort_by_accel, NULL, NULL); gtk_tree_sortable_set_sort_func (sortable, SORTID_ACTION, sort_by_col, GUINT_TO_POINTER (COL_ACTION), NULL); gtk_tree_sortable_set_sort_func (sortable, SORTID_OPTION, sort_by_col, GUINT_TO_POINTER (COL_OPTION), NULL); gtk_tree_sortable_set_sort_column_id (sortable, SORTID_ACTION, GTK_SORT_ASCENDING); // set initial sort order return GTK_TREE_MODEL (store); } gboolean conflict_confirm (GtkWidget *view, const gchar *action, guint accel_key, GdkModifierType accel_mask) { gchar *accel_string = egg_accelerator_get_label (accel_key, accel_mask); GtkWidget *dlg = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (view))), (GtkDialogFlags) (GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), GTK_MESSAGE_WARNING, GTK_BUTTONS_NONE, _("Shortcut “%s” is already taken by “%s”."), accel_string, action); gtk_dialog_add_buttons (GTK_DIALOG (dlg), GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, _("_Reassign shortcut"), GTK_RESPONSE_OK, NULL); gtk_dialog_set_default_response (GTK_DIALOG (dlg), GTK_RESPONSE_CANCEL); gtk_window_set_title (GTK_WINDOW (dlg), _("Conflicting Shortcuts")); gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dlg), _("Reassigning the shortcut will cause it " "to be removed from “%s”."), action); gtk_widget_show_all (dlg); gint response = gtk_dialog_run (GTK_DIALOG (dlg)); gtk_widget_destroy (dlg); g_free (accel_string); return response==GTK_RESPONSE_OK; } gboolean equal_accel (GtkTreeModel *model, GtkTreeIter *i, guint key, GdkModifierType mask) { guint accel_key = 0; GdkModifierType accel_mask = (GdkModifierType) 0; gtk_tree_model_get (model, i, COL_ACCEL_KEY, &accel_key, COL_ACCEL_MASK, &accel_mask, -1); return accel_key==key && accel_mask==mask; } gboolean find_accel (GtkTreeModel *model, GtkTreeIter *i, guint key, GdkModifierType mask) { gboolean valid_iter; for (valid_iter=gtk_tree_model_get_iter_first (model, i); valid_iter; valid_iter=gtk_tree_model_iter_next (model, i)) if (equal_accel (model, i, key, mask)) return TRUE; return FALSE; } void set_accel (GtkTreeModel *model, GtkTreePath *path, guint accel_key, GdkModifierType accel_mask) { GtkTreeIter iter; if (gtk_tree_model_get_iter (model, &iter, path)) gtk_list_store_set (GTK_LIST_STORE (model), &iter, COL_ACCEL_KEY, accel_key, COL_ACCEL_MASK, accel_mask, -1); } static void accel_edited_callback (GtkCellRendererAccel *accel, const char *path_string, guint accel_key, GdkModifierType accel_mask, guint hardware_keycode, GtkWidget *view) { DEBUG('u', "Key event: %s (%#x)\n", key2str(accel_mask,accel_key).c_str(), accel_key); if (!accel_key) gnome_cmd_show_message (NULL, _("Invalid shortcut.")); else { GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (view)); GtkTreePath *path = gtk_tree_path_new_from_string (path_string); GtkTreeIter iter; // do nothing if accelerators haven't changed... if (gtk_tree_model_get_iter (model, &iter, path) && equal_accel (model, &iter, accel_key, accel_mask)) return; if (!find_accel (model, &iter, accel_key, accel_mask)) // store new values if there isn't any duplicate... set_accel (model, path, accel_key, accel_mask); else { gchar *action; gtk_tree_model_get (model, &iter, COL_ACTION, &action, -1); // ...otherwise retrieve conflicting action... if (conflict_confirm (view, action, accel_key, accel_mask)) // ...and ask user for confirmation { set_accel (model, path, accel_key, accel_mask); gtk_list_store_remove (GTK_LIST_STORE (model), &iter); } g_free (action); } gtk_tree_path_free (path); } } static void cell_edited_callback (GtkCellRendererText *cell, gchar *path_string, gchar *new_text, GtkWidget *view) { GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (view)); GtkTreePath *path = gtk_tree_path_new_from_string (path_string); GtkTreeIter iter; gint col = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (cell), "column")); if (gtk_tree_model_get_iter (model, &iter, path)) { if (col==COL_ACTION && GnomeCmdKeyShortcutsDialog::user_actions) gtk_list_store_set (GTK_LIST_STORE (model), &iter, col, new_text, COL_NAME, GnomeCmdKeyShortcutsDialog::user_actions->name(new_text), -1); else gtk_list_store_set (GTK_LIST_STORE (model), &iter, col, new_text, -1); } gtk_tree_path_free (path); } static void remove_clicked_callback (GtkButton *button, GtkWidget *view) { GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view)); GtkTreeIter iter; if (gtk_tree_selection_get_selected (selection, NULL, &iter)) { GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (view)); gtk_list_store_remove (GTK_LIST_STORE (model), &iter); gtk_tree_selection_select_iter (selection, &iter); } } static void add_clicked_callback (GtkButton *button, GtkWidget *view) { GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (view)); GtkTreeIter iter; gtk_list_store_append (GTK_LIST_STORE (model), &iter); gtk_list_store_set (GTK_LIST_STORE (model), &iter, COL_ACTION, _("Do nothing"), COL_NAME, "no.action", -1); GtkTreePath *path = gtk_tree_model_get_path (model, &iter); gtk_widget_grab_focus (view); gtk_tree_view_set_cursor (GTK_TREE_VIEW (view), path, gtk_tree_view_get_column (GTK_TREE_VIEW (view),0), TRUE); gtk_tree_path_free(path); // start editing accelerator }