1 /*
2 * Copyright © 2001, 2002 Havoc Pennington, Red Hat Inc.
3 * Copyright © 2008 Christian Persch
4 * Copyright (C) 2012-2021 MATE Developers
5 *
6 * Mate-terminal is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * Mate-terminal is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include <config.h>
21
22 #include <string.h>
23
24 #include <gdk/gdkkeysyms.h>
25
26 #include "terminal-accels.h"
27 #include "terminal-app.h"
28 #include "terminal-debug.h"
29 #include "terminal-intl.h"
30 #include "terminal-profile.h"
31 #include "terminal-util.h"
32
33 /* NOTES
34 *
35 * There are two sources of keybindings changes, from GSettings and from
36 * the accel map (happens with in-place menu editing).
37 *
38 * When a keybinding GSettins key changes, we propagate that into the
39 * accel map.
40 * When the accel map changes, we queue a sync to GSettings.
41 *
42 * To avoid infinite loops, we short-circuit in both directions
43 * if the value is unchanged from last known.
44 *
45 * In the keybinding editor, when editing or clearing an accel, we write
46 * the change directly to GSettings and rely on the GSettings callback to
47 * actually apply the change to the accel map.
48 */
49
50 #define ACCEL_PATH_ROOT "<Actions>/Main/"
51 #define ACCEL_PATH_NEW_TAB ACCEL_PATH_ROOT "FileNewTab"
52 #define ACCEL_PATH_NEW_WINDOW ACCEL_PATH_ROOT "FileNewWindow"
53 #define ACCEL_PATH_NEW_PROFILE ACCEL_PATH_ROOT "FileNewProfile"
54 #define ACCEL_PATH_SAVE_CONTENTS ACCEL_PATH_ROOT "FileSaveContents"
55 #define ACCEL_PATH_CLOSE_TAB ACCEL_PATH_ROOT "FileCloseTab"
56 #define ACCEL_PATH_CLOSE_WINDOW ACCEL_PATH_ROOT "FileCloseWindow"
57 #define ACCEL_PATH_COPY ACCEL_PATH_ROOT "EditCopy"
58 #define ACCEL_PATH_PASTE ACCEL_PATH_ROOT "EditPaste"
59 #define ACCEL_PATH_SELECT_ALL ACCEL_PATH_ROOT "EditSelectAll"
60 #define ACCEL_PATH_SEARCH_FIND ACCEL_PATH_ROOT "SearchFind"
61 #define ACCEL_PATH_SEARCH_FIND_NEXT ACCEL_PATH_ROOT "SearchFindNext"
62 #define ACCEL_PATH_SEARCH_FIND_PREVIOUS ACCEL_PATH_ROOT "SearchFindPrevious"
63 #define ACCEL_PATH_TOGGLE_MENUBAR ACCEL_PATH_ROOT "ViewMenubar"
64 #define ACCEL_PATH_FULL_SCREEN ACCEL_PATH_ROOT "ViewFullscreen"
65 #define ACCEL_PATH_RESET ACCEL_PATH_ROOT "TerminalReset"
66 #define ACCEL_PATH_RESET_AND_CLEAR ACCEL_PATH_ROOT "TerminalResetClear"
67 #define ACCEL_PATH_PREV_PROFILE ACCEL_PATH_ROOT "ProfilePrevious"
68 #define ACCEL_PATH_NEXT_PROFILE ACCEL_PATH_ROOT "ProfileNext"
69 #define ACCEL_PATH_PREV_TAB ACCEL_PATH_ROOT "TabsPrevious"
70 #define ACCEL_PATH_NEXT_TAB ACCEL_PATH_ROOT "TabsNext"
71 #define ACCEL_PATH_SET_TERMINAL_TITLE ACCEL_PATH_ROOT "TerminalSetTitle"
72 #define ACCEL_PATH_HELP ACCEL_PATH_ROOT "HelpContents"
73 #define ACCEL_PATH_ZOOM_IN ACCEL_PATH_ROOT "ViewZoomIn"
74 #define ACCEL_PATH_ZOOM_OUT ACCEL_PATH_ROOT "ViewZoomOut"
75 #define ACCEL_PATH_ZOOM_NORMAL ACCEL_PATH_ROOT "ViewZoom100"
76 #define ACCEL_PATH_MOVE_TAB_LEFT ACCEL_PATH_ROOT "TabsMoveLeft"
77 #define ACCEL_PATH_MOVE_TAB_RIGHT ACCEL_PATH_ROOT "TabsMoveRight"
78 #define ACCEL_PATH_DETACH_TAB ACCEL_PATH_ROOT "TabsDetach"
79 #define ACCEL_PATH_SWITCH_TAB_PREFIX ACCEL_PATH_ROOT "TabsSwitch"
80
81 #define KEY_CLOSE_TAB "close-tab"
82 #define KEY_CLOSE_WINDOW "close-window"
83 #define KEY_COPY "copy"
84 #define KEY_DETACH_TAB "detach-tab"
85 #define KEY_FULL_SCREEN "full-screen"
86 #define KEY_HELP "help"
87 #define KEY_MOVE_TAB_LEFT "move-tab-left"
88 #define KEY_MOVE_TAB_RIGHT "move-tab-right"
89 #define KEY_NEW_PROFILE "new-profile"
90 #define KEY_NEW_TAB "new-tab"
91 #define KEY_NEW_WINDOW "new-window"
92 #define KEY_NEXT_PROFILE "next-profile"
93 #define KEY_NEXT_TAB "next-tab"
94 #define KEY_PASTE "paste"
95 #define KEY_PREV_PROFILE "prev-profile"
96 #define KEY_PREV_TAB "prev-tab"
97 #define KEY_RESET_AND_CLEAR "reset-and-clear"
98 #define KEY_RESET "reset"
99 #define KEY_SEARCH_FIND "search-find"
100 #define KEY_SEARCH_FIND_NEXT "search-find-next"
101 #define KEY_SEARCH_FIND_PREVIOUS "search-find-previous"
102 #define KEY_SELECT_ALL "select-all"
103 #define KEY_SAVE_CONTENTS "save-contents"
104 #define KEY_SET_TERMINAL_TITLE "set-terminal-title"
105 #define KEY_TOGGLE_MENUBAR "toggle-menubar"
106 #define KEY_ZOOM_IN "zoom-in"
107 #define KEY_ZOOM_NORMAL "zoom-normal"
108 #define KEY_ZOOM_OUT "zoom-out"
109 #define KEY_SWITCH_TAB_PREFIX "switch-to-tab-"
110
111 #if 1
112 /*
113 * We don't want to enable content saving until vte supports it async.
114 * So we disable this code for stable versions.
115 */
116 #include "terminal-version.h"
117
118 #if (TERMINAL_MINOR_VERSION & 1) != 0
119 #define ENABLE_SAVE
120 #else
121 #undef ENABLE_SAVE
122 #endif
123 #endif
124
125 typedef struct
126 {
127 const char *user_visible_name;
128 const char *gsettings_key;
129 const char *accel_path;
130 /* last values received from GSettings */
131 GdkModifierType gsettings_mask;
132 guint gsettings_keyval;
133 GClosure *closure;
134 /* have gotten a notification from gtk */
135 gboolean needs_gsettings_sync;
136 gboolean accel_path_unlocked;
137 } KeyEntry;
138
139 typedef struct
140 {
141 KeyEntry *key_entry;
142 guint n_elements;
143 const char *user_visible_name;
144 } KeyEntryList;
145
146 static KeyEntry file_entries[] =
147 {
148 {
149 N_("New Tab"),
150 KEY_NEW_TAB, ACCEL_PATH_NEW_TAB, GDK_SHIFT_MASK | GDK_CONTROL_MASK, GDK_KEY_t, NULL, FALSE, TRUE
151 },
152 {
153 N_("New Window"),
154 KEY_NEW_WINDOW, ACCEL_PATH_NEW_WINDOW, GDK_SHIFT_MASK | GDK_CONTROL_MASK, GDK_KEY_n, NULL, FALSE, TRUE
155 },
156 {
157 N_("New Profile"),
158 KEY_NEW_PROFILE, ACCEL_PATH_NEW_PROFILE, 0, 0, NULL, FALSE, TRUE
159 },
160 #ifdef ENABLE_SAVE
161 {
162 N_("Save Contents"),
163 KEY_SAVE_CONTENTS, ACCEL_PATH_SAVE_CONTENTS, 0, 0, NULL, FALSE, TRUE
164 },
165 #endif
166 {
167 N_("Close Tab"),
168 KEY_CLOSE_TAB, ACCEL_PATH_CLOSE_TAB, GDK_SHIFT_MASK | GDK_CONTROL_MASK, GDK_KEY_w, NULL, FALSE, TRUE
169 },
170 {
171 N_("Close Window"),
172 KEY_CLOSE_WINDOW, ACCEL_PATH_CLOSE_WINDOW, GDK_SHIFT_MASK | GDK_CONTROL_MASK, GDK_KEY_q, NULL, FALSE, TRUE
173 },
174 };
175
176 static KeyEntry edit_entries[] =
177 {
178 {
179 N_("Copy"),
180 KEY_COPY, ACCEL_PATH_COPY, GDK_SHIFT_MASK | GDK_CONTROL_MASK, GDK_KEY_c, NULL, FALSE, TRUE
181 },
182 {
183 N_("Paste"),
184 KEY_PASTE, ACCEL_PATH_PASTE, GDK_SHIFT_MASK | GDK_CONTROL_MASK, GDK_KEY_v, NULL, FALSE, TRUE
185 },
186 {
187 N_("Select All"),
188 KEY_SELECT_ALL, ACCEL_PATH_SELECT_ALL, GDK_SHIFT_MASK | GDK_CONTROL_MASK, GDK_KEY_a, NULL, FALSE, TRUE
189 }
190 };
191
192 static KeyEntry view_entries[] =
193 {
194 {
195 N_("Hide and Show menubar"),
196 KEY_TOGGLE_MENUBAR, ACCEL_PATH_TOGGLE_MENUBAR, 0, 0, NULL, FALSE, TRUE
197 },
198 {
199 N_("Full Screen"),
200 KEY_FULL_SCREEN, ACCEL_PATH_FULL_SCREEN, 0, GDK_KEY_F11, NULL, FALSE, TRUE
201 },
202 {
203 N_("Zoom In"),
204 KEY_ZOOM_IN, ACCEL_PATH_ZOOM_IN, GDK_CONTROL_MASK, GDK_KEY_plus, NULL, FALSE, TRUE
205 },
206 {
207 N_("Zoom Out"),
208 KEY_ZOOM_OUT, ACCEL_PATH_ZOOM_OUT, GDK_CONTROL_MASK, GDK_KEY_minus, NULL, FALSE, TRUE
209 },
210 {
211 N_("Normal Size"),
212 KEY_ZOOM_NORMAL, ACCEL_PATH_ZOOM_NORMAL, GDK_CONTROL_MASK, GDK_KEY_0, NULL, FALSE, TRUE
213 }
214 };
215
216 static KeyEntry search_entries[] =
217 {
218 {
219 N_("Find"),
220 KEY_SEARCH_FIND, ACCEL_PATH_SEARCH_FIND, GDK_SHIFT_MASK | GDK_CONTROL_MASK, GDK_KEY_f, NULL, FALSE, TRUE
221 },
222 {
223 N_("Find Next"),
224 KEY_SEARCH_FIND_NEXT, ACCEL_PATH_SEARCH_FIND_NEXT, GDK_SHIFT_MASK | GDK_CONTROL_MASK, GDK_KEY_h, NULL, FALSE, TRUE
225 },
226 {
227 N_("Find Previous"),
228 KEY_SEARCH_FIND_PREVIOUS, ACCEL_PATH_SEARCH_FIND_PREVIOUS, GDK_SHIFT_MASK | GDK_CONTROL_MASK, GDK_KEY_g, NULL, FALSE, TRUE
229 }
230 };
231
232 static KeyEntry terminal_entries[] =
233 {
234 {
235 N_("Set Title"),
236 KEY_SET_TERMINAL_TITLE, ACCEL_PATH_SET_TERMINAL_TITLE, 0, 0, NULL, FALSE, TRUE
237 },
238 {
239 N_("Reset"),
240 KEY_RESET, ACCEL_PATH_RESET, 0, 0, NULL, FALSE, TRUE
241 },
242 {
243 N_("Reset and Clear"),
244 KEY_RESET_AND_CLEAR, ACCEL_PATH_RESET_AND_CLEAR, 0, 0, NULL, FALSE, TRUE
245 },
246 {
247 N_("Switch to Previous Profile"),
248 KEY_PREV_PROFILE, ACCEL_PATH_PREV_PROFILE, GDK_MOD1_MASK, GDK_KEY_Page_Up, NULL, FALSE, TRUE
249 },
250 {
251 N_("Switch to Next Profile"),
252 KEY_NEXT_PROFILE, ACCEL_PATH_NEXT_PROFILE, GDK_MOD1_MASK, GDK_KEY_Page_Down, NULL, FALSE, TRUE
253 },
254 };
255
256 static KeyEntry tabs_entries[] =
257 {
258 {
259 N_("Switch to Previous Tab"),
260 KEY_PREV_TAB, ACCEL_PATH_PREV_TAB, GDK_CONTROL_MASK, GDK_KEY_Page_Up, NULL, FALSE, TRUE
261 },
262 {
263 N_("Switch to Next Tab"),
264 KEY_NEXT_TAB, ACCEL_PATH_NEXT_TAB, GDK_CONTROL_MASK, GDK_KEY_Page_Down, NULL, FALSE, TRUE
265 },
266 {
267 N_("Move Tab to the Left"),
268 KEY_MOVE_TAB_LEFT, ACCEL_PATH_MOVE_TAB_LEFT, GDK_SHIFT_MASK | GDK_CONTROL_MASK, GDK_KEY_Page_Up, NULL, FALSE, TRUE
269 },
270 {
271 N_("Move Tab to the Right"),
272 KEY_MOVE_TAB_RIGHT, ACCEL_PATH_MOVE_TAB_RIGHT, GDK_SHIFT_MASK | GDK_CONTROL_MASK, GDK_KEY_Page_Down, NULL, FALSE, TRUE
273 },
274 {
275 N_("Detach Tab"),
276 KEY_DETACH_TAB, ACCEL_PATH_DETACH_TAB, 0, 0, NULL, FALSE, TRUE
277 },
278 {
279 N_("Switch to Tab 1"),
280 KEY_SWITCH_TAB_PREFIX "1",
281 ACCEL_PATH_SWITCH_TAB_PREFIX "1", GDK_MOD1_MASK, GDK_KEY_1, NULL, FALSE, TRUE
282 },
283 {
284 N_("Switch to Tab 2"),
285 KEY_SWITCH_TAB_PREFIX "2",
286 ACCEL_PATH_SWITCH_TAB_PREFIX "2", GDK_MOD1_MASK, GDK_KEY_2, NULL, FALSE, TRUE
287 },
288 {
289 N_("Switch to Tab 3"),
290 KEY_SWITCH_TAB_PREFIX "3",
291 ACCEL_PATH_SWITCH_TAB_PREFIX "3", GDK_MOD1_MASK, GDK_KEY_3, NULL, FALSE, TRUE
292 },
293 {
294 N_("Switch to Tab 4"),
295 KEY_SWITCH_TAB_PREFIX "4",
296 ACCEL_PATH_SWITCH_TAB_PREFIX "4", GDK_MOD1_MASK, GDK_KEY_4, NULL, FALSE, TRUE
297 },
298 {
299 N_("Switch to Tab 5"),
300 KEY_SWITCH_TAB_PREFIX "5",
301 ACCEL_PATH_SWITCH_TAB_PREFIX "5", GDK_MOD1_MASK, GDK_KEY_5, NULL, FALSE, TRUE
302 },
303 {
304 N_("Switch to Tab 6"),
305 KEY_SWITCH_TAB_PREFIX "6",
306 ACCEL_PATH_SWITCH_TAB_PREFIX "6", GDK_MOD1_MASK, GDK_KEY_6, NULL, FALSE, TRUE
307 },
308 {
309 N_("Switch to Tab 7"),
310 KEY_SWITCH_TAB_PREFIX "7",
311 ACCEL_PATH_SWITCH_TAB_PREFIX "7", GDK_MOD1_MASK, GDK_KEY_7, NULL, FALSE, TRUE
312 },
313 {
314 N_("Switch to Tab 8"),
315 KEY_SWITCH_TAB_PREFIX "8",
316 ACCEL_PATH_SWITCH_TAB_PREFIX "8", GDK_MOD1_MASK, GDK_KEY_8, NULL, FALSE, TRUE
317 },
318 {
319 N_("Switch to Tab 9"),
320 KEY_SWITCH_TAB_PREFIX "9",
321 ACCEL_PATH_SWITCH_TAB_PREFIX "9", GDK_MOD1_MASK, GDK_KEY_9, NULL, FALSE, TRUE
322 },
323 {
324 N_("Switch to Tab 10"),
325 KEY_SWITCH_TAB_PREFIX "10",
326 ACCEL_PATH_SWITCH_TAB_PREFIX "10", GDK_MOD1_MASK, GDK_KEY_0, NULL, FALSE, TRUE
327 },
328 {
329 N_("Switch to Tab 11"),
330 KEY_SWITCH_TAB_PREFIX "11",
331 ACCEL_PATH_SWITCH_TAB_PREFIX "11", 0, 0, NULL, FALSE, TRUE
332 },
333 {
334 N_("Switch to Tab 12"),
335 KEY_SWITCH_TAB_PREFIX "12",
336 ACCEL_PATH_SWITCH_TAB_PREFIX "12", 0, 0, NULL, FALSE, TRUE
337 }
338 };
339
340 static KeyEntry help_entries[] =
341 {
342 { N_("Contents"), KEY_HELP, ACCEL_PATH_HELP, 0, GDK_KEY_F1, NULL, FALSE, TRUE }
343 };
344
345 static KeyEntryList all_entries[] =
346 {
347 { file_entries, G_N_ELEMENTS (file_entries), N_("File") },
348 { edit_entries, G_N_ELEMENTS (edit_entries), N_("Edit") },
349 { view_entries, G_N_ELEMENTS (view_entries), N_("View") },
350 { search_entries, G_N_ELEMENTS (search_entries), N_("Search") },
351 { terminal_entries, G_N_ELEMENTS (terminal_entries), N_("Terminal") },
352 { tabs_entries, G_N_ELEMENTS (tabs_entries), N_("Tabs") },
353 { help_entries, G_N_ELEMENTS (help_entries), N_("Help") }
354 };
355
356 enum
357 {
358 ACTION_COLUMN,
359 KEYVAL_COLUMN,
360 N_COLUMNS
361 };
362
363 static void keys_change_notify (GSettings *settings,
364 const gchar *key,
365 gpointer user_data);
366
367 static void accel_changed_callback (GtkAccelGroup *accel_group,
368 guint keyval,
369 GdkModifierType modifier,
370 GClosure *accel_closure,
371 gpointer data);
372
373 static gboolean binding_from_string (const char *str,
374 guint *accelerator_key,
375 GdkModifierType *accelerator_mods);
376
377 static gboolean binding_from_value (GVariant *value,
378 guint *accelerator_key,
379 GdkModifierType *accelerator_mods);
380
381 static gboolean sync_idle_cb (gpointer data);
382
383 static guint sync_idle_id = 0;
384 static GtkAccelGroup *notification_group = NULL;
385 /* never set GSettings keys in response to receiving a GSettings notify. */
386 static int inside_gsettings_notify = 0;
387 static GtkWidget *edit_keys_dialog = NULL;
388 static GtkTreeStore *edit_keys_store = NULL;
389 static GHashTable *gsettings_key_to_entry;
390 static GSettings *settings_keybindings;
391
392 static char*
binding_name(guint keyval,GdkModifierType mask)393 binding_name (guint keyval,
394 GdkModifierType mask)
395 {
396 if (keyval != 0)
397 return gtk_accelerator_name (keyval, mask);
398
399 return g_strdup ("disabled");
400 }
401
402 static char*
binding_display_name(guint keyval,GdkModifierType mask)403 binding_display_name (guint keyval,
404 GdkModifierType mask)
405 {
406 if (keyval != 0)
407 return gtk_accelerator_get_label (keyval, mask);
408
409 return g_strdup (_("Disabled"));
410 }
411
412 void
terminal_accels_init(void)413 terminal_accels_init (void)
414 {
415 guint i, j;
416
417 settings_keybindings = g_settings_new (CONF_KEYS_SCHEMA);
418
419 g_signal_connect (settings_keybindings,
420 "changed",
421 G_CALLBACK(keys_change_notify),
422 NULL);
423
424 gsettings_key_to_entry = g_hash_table_new (g_str_hash, g_str_equal);
425
426 notification_group = gtk_accel_group_new ();
427
428 for (i = 0; i < G_N_ELEMENTS (all_entries); ++i)
429 {
430 for (j = 0; j < all_entries[i].n_elements; ++j)
431 {
432 KeyEntry *key_entry;
433
434 key_entry = &(all_entries[i].key_entry[j]);
435
436 g_hash_table_insert (gsettings_key_to_entry,
437 (gpointer) key_entry->gsettings_key,
438 key_entry);
439
440 key_entry->closure = g_closure_new_simple (sizeof (GClosure), key_entry);
441
442 g_closure_ref (key_entry->closure);
443 g_closure_sink (key_entry->closure);
444
445 gtk_accel_group_connect_by_path (notification_group,
446 I_(key_entry->accel_path),
447 key_entry->closure);
448 keys_change_notify (settings_keybindings, key_entry->gsettings_key, NULL);
449 }
450 }
451
452 g_signal_connect (notification_group, "accel-changed",
453 G_CALLBACK (accel_changed_callback), NULL);
454 }
455
456 void
terminal_accels_shutdown(void)457 terminal_accels_shutdown (void)
458 {
459
460 g_signal_handlers_disconnect_by_func (settings_keybindings,
461 G_CALLBACK(keys_change_notify),
462 NULL);
463 g_object_unref (settings_keybindings);
464
465 if (sync_idle_id != 0)
466 {
467 g_source_remove (sync_idle_id);
468 sync_idle_id = 0;
469
470 sync_idle_cb (NULL);
471 }
472
473 g_hash_table_destroy (gsettings_key_to_entry);
474 gsettings_key_to_entry = NULL;
475
476 g_object_unref (notification_group);
477 notification_group = NULL;
478 }
479
480 static gboolean
update_model_foreach(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer data)481 update_model_foreach (GtkTreeModel *model,
482 GtkTreePath *path,
483 GtkTreeIter *iter,
484 gpointer data)
485 {
486 KeyEntry *key_entry = NULL;
487
488 gtk_tree_model_get (model, iter,
489 KEYVAL_COLUMN, &key_entry,
490 -1);
491
492 if (key_entry == (KeyEntry *) data)
493 {
494 gtk_tree_model_row_changed (model, path, iter);
495 return TRUE;
496 }
497 return FALSE;
498 }
499
500 static void
keys_change_notify(GSettings * settings,const gchar * key,gpointer user_data)501 keys_change_notify (GSettings *settings,
502 const gchar *key,
503 gpointer user_data)
504 {
505 GVariant *val;
506 KeyEntry *key_entry;
507 GdkModifierType mask;
508 guint keyval;
509
510 _terminal_debug_print (TERMINAL_DEBUG_ACCELS,
511 "key %s changed\n",
512 key);
513
514 val = g_settings_get_value (settings, key);
515
516 #ifdef MATE_ENABLE_DEBUG
517 _TERMINAL_DEBUG_IF (TERMINAL_DEBUG_ACCELS)
518 {
519 if (val == NULL)
520 _terminal_debug_print (TERMINAL_DEBUG_ACCELS, " changed to be unset\n");
521 else if (!g_variant_is_of_type (val, G_VARIANT_TYPE_STRING))
522 _terminal_debug_print (TERMINAL_DEBUG_ACCELS, " changed to non-string value\n");
523 else
524 _terminal_debug_print (TERMINAL_DEBUG_ACCELS,
525 " changed to \"%s\"\n",
526 g_variant_get_string (val, NULL));
527 }
528 #endif
529
530 key_entry = g_hash_table_lookup (gsettings_key_to_entry, key);
531 if (!key_entry)
532 {
533 /* shouldn't really happen, but let's be safe */
534 _terminal_debug_print (TERMINAL_DEBUG_ACCELS,
535 " WARNING: KeyEntry for changed key not found, bailing out\n");
536 return;
537 }
538
539 if (!binding_from_value (val, &keyval, &mask))
540 {
541 const char *str = g_variant_is_of_type (val, G_VARIANT_TYPE_STRING) ? g_variant_get_string (val, NULL) : NULL;
542 g_printerr ("The value \"%s\" of configuration key %s is not a valid accelerator\n",
543 str ? str : "(null)",
544 key_entry->gsettings_key);
545 return;
546 }
547 key_entry->gsettings_keyval = keyval;
548 key_entry->gsettings_mask = mask;
549
550 /* Unlock the path, so we can change its accel */
551 if (!key_entry->accel_path_unlocked)
552 gtk_accel_map_unlock_path (key_entry->accel_path);
553
554 /* sync over to GTK */
555 _terminal_debug_print (TERMINAL_DEBUG_ACCELS,
556 "changing path %s to %s\n",
557 key_entry->accel_path,
558 binding_name (keyval, mask)); /* memleak */
559 inside_gsettings_notify += 1;
560 /* Note that this may return FALSE, e.g. when the entry was already set correctly. */
561 gtk_accel_map_change_entry (key_entry->accel_path,
562 keyval, mask,
563 TRUE);
564 inside_gsettings_notify -= 1;
565
566 /* Lock the path if the GSettings key isn't writable */
567 key_entry->accel_path_unlocked = g_settings_is_writable (settings, key);
568 if (!key_entry->accel_path_unlocked)
569 gtk_accel_map_lock_path (key_entry->accel_path);
570
571 /* This seems necessary to update the tree model, since sometimes the
572 * notification on the notification_group seems not to be emitted correctly.
573 * Without this change, when trying to set an accel to e.g. Alt-T (while the main
574 * menu in the terminal windows is _Terminal with Alt-T mnemonic) only displays
575 * the accel change after a re-expose of the row.
576 * FIXME: Find out *why* the accel-changed signal is wrong here!
577 */
578 if (edit_keys_store)
579 gtk_tree_model_foreach (GTK_TREE_MODEL (edit_keys_store), update_model_foreach, key_entry);
580
581 g_variant_unref(val);
582 }
583
584 static void
accel_changed_callback(GtkAccelGroup * accel_group,guint keyval,GdkModifierType modifier,GClosure * accel_closure,gpointer data)585 accel_changed_callback (GtkAccelGroup *accel_group,
586 guint keyval,
587 GdkModifierType modifier,
588 GClosure *accel_closure,
589 gpointer data)
590 {
591 /* FIXME because GTK accel API is so nonsensical, we get
592 * a notify for each closure, on both the added and the removed
593 * accelerator. We just use the accel closure to find our
594 * accel entry, then update the value of that entry.
595 * We use an idle function to avoid setting the entry
596 * in GSettings when the accelerator gets removed and then
597 * setting it again when it gets added.
598 */
599 KeyEntry *key_entry;
600
601 _terminal_debug_print (TERMINAL_DEBUG_ACCELS,
602 "Changed accel %s closure %p\n",
603 binding_name (keyval, modifier), /* memleak */
604 accel_closure);
605
606 if (inside_gsettings_notify)
607 {
608 _terminal_debug_print (TERMINAL_DEBUG_ACCELS,
609 "Ignoring change from gtk because we're inside a GSettings notify\n");
610 return;
611 }
612
613 key_entry = accel_closure->data;
614 g_assert (key_entry);
615
616 key_entry->needs_gsettings_sync = TRUE;
617
618 if (sync_idle_id == 0)
619 sync_idle_id = g_idle_add (sync_idle_cb, NULL);
620 }
621
622 static gboolean
binding_from_string(const char * str,guint * accelerator_key,GdkModifierType * accelerator_mods)623 binding_from_string (const char *str,
624 guint *accelerator_key,
625 GdkModifierType *accelerator_mods)
626 {
627 if (str == NULL ||
628 strcmp (str, "disabled") == 0)
629 {
630 *accelerator_key = 0;
631 *accelerator_mods = 0;
632 return TRUE;
633 }
634
635 gtk_accelerator_parse (str, accelerator_key, accelerator_mods);
636 if (*accelerator_key == 0 &&
637 *accelerator_mods == 0)
638 return FALSE;
639
640 return TRUE;
641 }
642
643 static gboolean
binding_from_value(GVariant * value,guint * accelerator_key,GdkModifierType * accelerator_mods)644 binding_from_value (GVariant *value,
645 guint *accelerator_key,
646 GdkModifierType *accelerator_mods)
647 {
648 if (value == NULL)
649 {
650 /* unset */
651 *accelerator_key = 0;
652 *accelerator_mods = 0;
653 return TRUE;
654 }
655
656 if (!g_variant_is_of_type (value, G_VARIANT_TYPE_STRING))
657 return FALSE;
658
659 return binding_from_string (g_variant_get_string (value,NULL),
660 accelerator_key,
661 accelerator_mods);
662 }
663
664 static void
add_key_entry_to_changeset(gpointer key,KeyEntry * key_entry,GSettings * changeset)665 add_key_entry_to_changeset (gpointer key,
666 KeyEntry *key_entry,
667 GSettings *changeset)
668 {
669 GtkAccelKey gtk_key;
670
671 if (!key_entry->needs_gsettings_sync)
672 return;
673
674 key_entry->needs_gsettings_sync = FALSE;
675
676 if (gtk_accel_map_lookup_entry (key_entry->accel_path, >k_key) &&
677 (gtk_key.accel_key != key_entry->gsettings_keyval ||
678 gtk_key.accel_mods != key_entry->gsettings_mask))
679 {
680 char *accel_name;
681
682 accel_name = binding_name (gtk_key.accel_key, gtk_key.accel_mods);
683 g_settings_set_string (changeset, key_entry->gsettings_key, accel_name);
684 g_free (accel_name);
685 }
686 }
687
688 static gboolean
sync_idle_cb(gpointer data)689 sync_idle_cb (gpointer data)
690 {
691 GSettings *changeset;
692
693 _terminal_debug_print (TERMINAL_DEBUG_ACCELS,
694 "GSettings sync handler\n");
695
696 sync_idle_id = 0;
697
698 changeset = g_settings_new (CONF_KEYS_SCHEMA);
699 g_settings_delay (changeset);
700
701 g_hash_table_foreach (gsettings_key_to_entry, (GHFunc) add_key_entry_to_changeset, changeset);
702 g_settings_apply(changeset);
703
704 g_object_unref (changeset);
705
706 return FALSE;
707 }
708
709 /* We have the same KeyEntry* in both columns;
710 * we only have two columns because we want to be able
711 * to sort by either one of them.
712 */
713
714 static void
accel_set_func(GtkTreeViewColumn * tree_column,GtkCellRenderer * cell,GtkTreeModel * model,GtkTreeIter * iter,gpointer data)715 accel_set_func (GtkTreeViewColumn *tree_column,
716 GtkCellRenderer *cell,
717 GtkTreeModel *model,
718 GtkTreeIter *iter,
719 gpointer data)
720 {
721 KeyEntry *ke;
722
723 gtk_tree_model_get (model, iter,
724 KEYVAL_COLUMN, &ke,
725 -1);
726
727 if (ke == NULL)
728 /* This is a title row */
729 g_object_set (cell,
730 "visible", FALSE,
731 NULL);
732 else
733 g_object_set (cell,
734 "visible", TRUE,
735 "sensitive", ke->accel_path_unlocked,
736 "editable", ke->accel_path_unlocked,
737 "accel-key", ke->gsettings_keyval,
738 "accel-mods", ke->gsettings_mask,
739 NULL);
740 }
741
742 static int
accel_compare_func(GtkTreeModel * model,GtkTreeIter * a,GtkTreeIter * b,gpointer user_data)743 accel_compare_func (GtkTreeModel *model,
744 GtkTreeIter *a,
745 GtkTreeIter *b,
746 gpointer user_data)
747 {
748 KeyEntry *ke_a;
749 KeyEntry *ke_b;
750 char *name_a;
751 char *name_b;
752 int result;
753
754 gtk_tree_model_get (model, a,
755 KEYVAL_COLUMN, &ke_a,
756 -1);
757 if (ke_a == NULL)
758 {
759 gtk_tree_model_get (model, a,
760 ACTION_COLUMN, &name_a,
761 -1);
762 }
763 else
764 {
765 name_a = binding_display_name (ke_a->gsettings_keyval,
766 ke_a->gsettings_mask);
767 }
768
769 gtk_tree_model_get (model, b,
770 KEYVAL_COLUMN, &ke_b,
771 -1);
772 if (ke_b == NULL)
773 {
774 gtk_tree_model_get (model, b,
775 ACTION_COLUMN, &name_b,
776 -1);
777 }
778 else
779 {
780 name_b = binding_display_name (ke_b->gsettings_keyval,
781 ke_b->gsettings_mask);
782 }
783
784 result = g_utf8_collate (name_a, name_b);
785
786 g_free (name_a);
787 g_free (name_b);
788
789 return result;
790 }
791
792 static void
treeview_accel_changed_cb(GtkAccelGroup * accel_group,guint keyval,GdkModifierType modifier,GClosure * accel_closure,GtkTreeModel * model)793 treeview_accel_changed_cb (GtkAccelGroup *accel_group,
794 guint keyval,
795 GdkModifierType modifier,
796 GClosure *accel_closure,
797 GtkTreeModel *model)
798 {
799 gtk_tree_model_foreach (model, update_model_foreach, accel_closure->data);
800 }
801
802 static void
accel_edited_callback(GtkCellRendererAccel * cell,gchar * path_string,guint keyval,GdkModifierType mask,guint hardware_keycode,GtkTreeView * view)803 accel_edited_callback (GtkCellRendererAccel *cell,
804 gchar *path_string,
805 guint keyval,
806 GdkModifierType mask,
807 guint hardware_keycode,
808 GtkTreeView *view)
809 {
810 GtkTreeModel *model;
811 GtkTreePath *path;
812 GtkTreeIter iter;
813 KeyEntry *ke;
814 GtkAccelGroupEntry *entries;
815 guint n_entries;
816 char *str;
817
818 model = gtk_tree_view_get_model (view);
819
820 path = gtk_tree_path_new_from_string (path_string);
821 if (!path)
822 return;
823
824 if (!gtk_tree_model_get_iter (model, &iter, path))
825 {
826 gtk_tree_path_free (path);
827 return;
828 }
829 gtk_tree_path_free (path);
830
831 gtk_tree_model_get (model, &iter, KEYVAL_COLUMN, &ke, -1);
832
833 /* sanity check */
834 if (ke == NULL)
835 return;
836
837 /* Check if we already have an entry using this accel */
838 entries = gtk_accel_group_query (notification_group, keyval, mask, &n_entries);
839 if (n_entries > 0)
840 {
841 if (entries[0].accel_path_quark != g_quark_from_string (ke->accel_path))
842 {
843 GtkWidget *dialog;
844 char *name;
845 KeyEntry *other_key;
846
847 name = gtk_accelerator_get_label (keyval, mask);
848 other_key = entries[0].closure->data;
849 g_assert (other_key);
850
851 dialog =
852 gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (view))),
853 GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
854 GTK_MESSAGE_WARNING,
855 GTK_BUTTONS_OK,
856 _("The shortcut key “%s” is already bound to the “%s” action"),
857 name,
858
859 other_key->user_visible_name ? _(other_key->user_visible_name) : other_key->gsettings_key);
860 g_free (name);
861
862 g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
863 gtk_window_present (GTK_WINDOW (dialog));
864 }
865
866 return;
867 }
868
869 str = binding_name (keyval, mask);
870
871 _terminal_debug_print (TERMINAL_DEBUG_ACCELS,
872 "Edited path %s keyval %s, setting GSettings to %s\n",
873 ke->accel_path,
874 gdk_keyval_name (keyval) ? gdk_keyval_name (keyval) : "null",
875 str);
876 #ifdef MATE_ENABLE_DEBUG
877 _TERMINAL_DEBUG_IF (TERMINAL_DEBUG_ACCELS)
878 {
879 GtkAccelKey old_key;
880
881 if (gtk_accel_map_lookup_entry (ke->accel_path, &old_key))
882 {
883 _terminal_debug_print (TERMINAL_DEBUG_ACCELS,
884 " Old entry of path %s is keyval %s mask %x\n",
885 ke->accel_path, gdk_keyval_name (old_key.accel_key), old_key.accel_mods);
886 }
887 else
888 {
889 _terminal_debug_print (TERMINAL_DEBUG_ACCELS,
890 " Failed to look up the old entry of path %s\n",
891 ke->accel_path);
892 }
893 }
894 #endif
895
896 g_settings_set_string (settings_keybindings,
897 ke->gsettings_key,
898 str);
899 g_free (str);
900 }
901
902 static void
accel_cleared_callback(GtkCellRendererAccel * cell,gchar * path_string,GtkTreeView * view)903 accel_cleared_callback (GtkCellRendererAccel *cell,
904 gchar *path_string,
905 GtkTreeView *view)
906 {
907 GtkTreeModel *model;
908 GtkTreePath *path;
909 GtkTreeIter iter;
910 KeyEntry *ke;
911 char *str;
912
913 model = gtk_tree_view_get_model (view);
914
915 path = gtk_tree_path_new_from_string (path_string);
916 if (!path)
917 return;
918
919 if (!gtk_tree_model_get_iter (model, &iter, path))
920 {
921 gtk_tree_path_free (path);
922 return;
923 }
924 gtk_tree_path_free (path);
925
926 gtk_tree_model_get (model, &iter, KEYVAL_COLUMN, &ke, -1);
927
928 /* sanity check */
929 if (ke == NULL)
930 return;
931
932 ke->gsettings_keyval = 0;
933 ke->gsettings_mask = 0;
934 ke->needs_gsettings_sync = TRUE;
935
936 str = binding_name (0, 0);
937
938 _terminal_debug_print (TERMINAL_DEBUG_ACCELS,
939 "Cleared keybinding for GSettings %s",
940 ke->gsettings_key);
941
942 g_settings_set_string (settings_keybindings,
943 ke->gsettings_key,
944 str);
945 g_free (str);
946 }
947
948 static void
edit_keys_dialog_destroy_cb(GtkWidget * widget,gpointer user_data)949 edit_keys_dialog_destroy_cb (GtkWidget *widget,
950 gpointer user_data)
951 {
952 g_signal_handlers_disconnect_by_func (notification_group, G_CALLBACK (treeview_accel_changed_cb), user_data);
953 edit_keys_dialog = NULL;
954 edit_keys_store = NULL;
955 }
956
957 static void
edit_keys_dialog_response_cb(GtkWidget * editor,int response,gpointer use_data)958 edit_keys_dialog_response_cb (GtkWidget *editor,
959 int response,
960 gpointer use_data)
961 {
962 if (response == GTK_RESPONSE_HELP)
963 {
964 terminal_util_show_help ("mate-terminal-shortcuts", GTK_WINDOW (editor));
965 return;
966 }
967
968 gtk_widget_destroy (editor);
969 }
970
971 #ifdef MATE_ENABLE_DEBUG
972 static void
row_changed(GtkTreeModel * tree_model,GtkTreePath * path,GtkTreeIter * iter,gpointer user_data)973 row_changed (GtkTreeModel *tree_model,
974 GtkTreePath *path,
975 GtkTreeIter *iter,
976 gpointer user_data)
977 {
978 _terminal_debug_print (TERMINAL_DEBUG_ACCELS,
979 "ROW-CHANGED [%s]\n", gtk_tree_path_to_string (path) /* leak */);
980 }
981 #endif
982
983 void
terminal_edit_keys_dialog_show(GtkWindow * transient_parent)984 terminal_edit_keys_dialog_show (GtkWindow *transient_parent)
985 {
986 TerminalApp *app;
987 GtkWidget *dialog, *tree_view, *disable_mnemonics_button, *disable_menu_accel_button;
988 GtkTreeViewColumn *column;
989 GtkCellRenderer *cell_renderer;
990 GtkTreeStore *tree;
991 guint i;
992
993 if (edit_keys_dialog != NULL)
994 goto done;
995
996 if (!terminal_util_load_builder_resource (TERMINAL_RESOURCES_PATH_PREFIX G_DIR_SEPARATOR_S "ui/keybinding-editor.ui",
997 "keybindings-dialog", &dialog,
998 "disable-mnemonics-checkbutton", &disable_mnemonics_button,
999 "disable-menu-accel-checkbutton", &disable_menu_accel_button,
1000 "accelerators-treeview", &tree_view,
1001 NULL))
1002 return;
1003
1004 app = terminal_app_get ();
1005 terminal_util_bind_object_property_to_widget (G_OBJECT (app), TERMINAL_APP_ENABLE_MNEMONICS,
1006 disable_mnemonics_button, 0);
1007 terminal_util_bind_object_property_to_widget (G_OBJECT (app), TERMINAL_APP_ENABLE_MENU_BAR_ACCEL,
1008 disable_menu_accel_button, 0);
1009
1010 /* Column 1 */
1011 cell_renderer = gtk_cell_renderer_text_new ();
1012 column = gtk_tree_view_column_new_with_attributes (_("_Action"),
1013 cell_renderer,
1014 "text", ACTION_COLUMN,
1015 NULL);
1016 gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), column);
1017 gtk_tree_view_column_set_sort_column_id (column, ACTION_COLUMN);
1018
1019 /* Column 2 */
1020 cell_renderer = gtk_cell_renderer_accel_new ();
1021 g_object_set (cell_renderer,
1022 "editable", TRUE,
1023 "accel-mode", GTK_CELL_RENDERER_ACCEL_MODE_GTK,
1024 NULL);
1025 g_signal_connect (cell_renderer, "accel-edited",
1026 G_CALLBACK (accel_edited_callback), tree_view);
1027 g_signal_connect (cell_renderer, "accel-cleared",
1028 G_CALLBACK (accel_cleared_callback), tree_view);
1029
1030 column = gtk_tree_view_column_new ();
1031 gtk_tree_view_column_set_title (column, _("Shortcut _Key"));
1032 gtk_tree_view_column_pack_start (column, cell_renderer, TRUE);
1033 gtk_tree_view_column_set_cell_data_func (column, cell_renderer, accel_set_func, NULL, NULL);
1034 gtk_tree_view_column_set_sort_column_id (column, KEYVAL_COLUMN);
1035 gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), column);
1036
1037 /* Add the data */
1038
1039 tree = edit_keys_store = gtk_tree_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_POINTER);
1040
1041 #ifdef MATE_ENABLE_DEBUG
1042 _TERMINAL_DEBUG_IF (TERMINAL_DEBUG_ACCELS)
1043 g_signal_connect (tree, "row-changed", G_CALLBACK (row_changed), NULL);
1044 #endif
1045
1046 for (i = 0; i < G_N_ELEMENTS (all_entries); ++i)
1047 {
1048 GtkTreeIter parent_iter;
1049 guint j;
1050
1051 gtk_tree_store_append (tree, &parent_iter, NULL);
1052 gtk_tree_store_set (tree, &parent_iter,
1053 ACTION_COLUMN, _(all_entries[i].user_visible_name),
1054 -1);
1055
1056 for (j = 0; j < all_entries[i].n_elements; ++j)
1057 {
1058 KeyEntry *key_entry = &(all_entries[i].key_entry[j]);
1059 GtkTreeIter iter;
1060
1061 gtk_tree_store_insert_with_values (tree, &iter, &parent_iter, -1,
1062 ACTION_COLUMN, _(key_entry->user_visible_name),
1063 KEYVAL_COLUMN, key_entry,
1064 -1);
1065 }
1066 }
1067
1068 gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (tree),
1069 KEYVAL_COLUMN, accel_compare_func,
1070 NULL, NULL);
1071 gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (tree), ACTION_COLUMN,
1072 GTK_SORT_ASCENDING);
1073
1074 gtk_tree_view_set_model (GTK_TREE_VIEW (tree_view), GTK_TREE_MODEL (tree));
1075 g_object_unref (tree);
1076
1077 gtk_tree_view_expand_all (GTK_TREE_VIEW (tree_view));
1078
1079 g_signal_connect (notification_group, "accel-changed",
1080 G_CALLBACK (treeview_accel_changed_cb), tree);
1081
1082 edit_keys_dialog = dialog;
1083 g_signal_connect (dialog, "destroy",
1084 G_CALLBACK (edit_keys_dialog_destroy_cb), tree);
1085 g_signal_connect (dialog, "response",
1086 G_CALLBACK (edit_keys_dialog_response_cb),
1087 NULL);
1088 gtk_window_set_default_size (GTK_WINDOW (dialog), -1, 350);
1089
1090 done:
1091 gtk_window_set_transient_for (GTK_WINDOW (edit_keys_dialog), transient_parent);
1092 gtk_window_present (GTK_WINDOW (edit_keys_dialog));
1093 }
1094