Line | Branch | Exec | Source |
---|---|---|---|
1 | /* | ||
2 | * Copyright (C) 2010 Intel, Inc | ||
3 | * Copyright (C) 2014 Red Hat, Inc | ||
4 | * | ||
5 | * This program is free software; you can redistribute it and/or modify | ||
6 | * it under the terms of the GNU General Public License as published by | ||
7 | * the Free Software Foundation; either version 2 of the License, or | ||
8 | * (at your option) any later version. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU General Public License | ||
16 | * along with this program; if not, see <http://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * Authors: Thomas Wood <thomas.wood@intel.com> | ||
19 | * Rodrigo Moya <rodrigo@gnome.org> | ||
20 | * Christophe Fergeau <cfergeau@redhat.com> | ||
21 | */ | ||
22 | |||
23 | #include <config.h> | ||
24 | |||
25 | #include <glib/gi18n.h> | ||
26 | |||
27 | #include "keyboard-shortcuts.h" | ||
28 | |||
29 | #define CUSTOM_KEYS_BASENAME "/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings" | ||
30 | |||
31 | static char * | ||
32 | ✗ | replace_pictures_folder (const char *description) | |
33 | { | ||
34 | ✗ | g_autoptr(GRegex) pictures_regex = NULL; | |
35 | const char *path; | ||
36 | ✗ | g_autofree gchar *dirname = NULL; | |
37 | ✗ | g_autofree gchar *ret = NULL; | |
38 | |||
39 | ✗ | if (description == NULL) | |
40 | ✗ | return NULL; | |
41 | |||
42 | ✗ | if (strstr (description, "$PICTURES") == NULL) | |
43 | ✗ | return g_strdup (description); | |
44 | |||
45 | ✗ | pictures_regex = g_regex_new ("\\$PICTURES", 0, 0, NULL); | |
46 | ✗ | path = g_get_user_special_dir (G_USER_DIRECTORY_PICTURES); | |
47 | ✗ | dirname = g_filename_display_basename (path); | |
48 | ✗ | ret = g_regex_replace (pictures_regex, description, -1, | |
49 | 0, dirname, 0, NULL); | ||
50 | |||
51 | ✗ | if (ret == NULL) | |
52 | ✗ | return g_strdup (description); | |
53 | |||
54 | ✗ | return g_steal_pointer (&ret); | |
55 | } | ||
56 | |||
57 | static void | ||
58 | ✗ | parse_start_tag (GMarkupParseContext *ctx, | |
59 | const gchar *element_name, | ||
60 | const gchar **attr_names, | ||
61 | const gchar **attr_values, | ||
62 | gpointer user_data, | ||
63 | GError **error) | ||
64 | { | ||
65 | ✗ | KeyList *keylist = (KeyList *) user_data; | |
66 | KeyListEntry key; | ||
67 | const char *name, *schema, *description, *package, *context, *orig_description, *reverse_entry; | ||
68 | gboolean is_reversed, hidden; | ||
69 | |||
70 | ✗ | name = NULL; | |
71 | ✗ | schema = NULL; | |
72 | ✗ | package = NULL; | |
73 | ✗ | context = NULL; | |
74 | |||
75 | /* The top-level element, names the section in the tree */ | ||
76 | ✗ | if (g_str_equal (element_name, "KeyListEntries")) | |
77 | { | ||
78 | ✗ | const char *wm_name = NULL; | |
79 | ✗ | const char *group = NULL; | |
80 | |||
81 | ✗ | while (*attr_names && *attr_values) | |
82 | { | ||
83 | ✗ | if (g_str_equal (*attr_names, "name")) | |
84 | { | ||
85 | ✗ | if (**attr_values) | |
86 | ✗ | name = *attr_values; | |
87 | ✗ | } else if (g_str_equal (*attr_names, "group")) { | |
88 | ✗ | if (**attr_values) | |
89 | ✗ | group = *attr_values; | |
90 | ✗ | } else if (g_str_equal (*attr_names, "wm_name")) { | |
91 | ✗ | if (**attr_values) | |
92 | ✗ | wm_name = *attr_values; | |
93 | ✗ | } else if (g_str_equal (*attr_names, "schema")) { | |
94 | ✗ | if (**attr_values) | |
95 | ✗ | schema = *attr_values; | |
96 | ✗ | } else if (g_str_equal (*attr_names, "package")) { | |
97 | ✗ | if (**attr_values) | |
98 | ✗ | package = *attr_values; | |
99 | } | ||
100 | ✗ | ++attr_names; | |
101 | ✗ | ++attr_values; | |
102 | } | ||
103 | |||
104 | ✗ | if (name) | |
105 | { | ||
106 | ✗ | if (keylist->name) | |
107 | ✗ | g_warning ("Duplicate section name"); | |
108 | ✗ | g_free (keylist->name); | |
109 | ✗ | keylist->name = g_strdup (name); | |
110 | } | ||
111 | ✗ | if (wm_name) | |
112 | { | ||
113 | ✗ | if (keylist->wm_name) | |
114 | ✗ | g_warning ("Duplicate window manager name"); | |
115 | ✗ | g_free (keylist->wm_name); | |
116 | ✗ | keylist->wm_name = g_strdup (wm_name); | |
117 | } | ||
118 | ✗ | if (package) | |
119 | { | ||
120 | ✗ | if (keylist->package) | |
121 | ✗ | g_warning ("Duplicate gettext package name"); | |
122 | ✗ | g_free (keylist->package); | |
123 | ✗ | keylist->package = g_strdup (package); | |
124 | ✗ | bind_textdomain_codeset (keylist->package, "UTF-8"); | |
125 | } | ||
126 | ✗ | if (group) | |
127 | { | ||
128 | ✗ | if (keylist->group) | |
129 | ✗ | g_warning ("Duplicate group"); | |
130 | ✗ | g_free (keylist->group); | |
131 | ✗ | keylist->group = g_strdup (group); | |
132 | } | ||
133 | ✗ | if (schema) | |
134 | { | ||
135 | ✗ | if (keylist->schema) | |
136 | ✗ | g_warning ("Duplicate schema"); | |
137 | ✗ | g_free (keylist->schema); | |
138 | ✗ | keylist->schema = g_strdup (schema); | |
139 | } | ||
140 | ✗ | return; | |
141 | } | ||
142 | |||
143 | ✗ | if (!g_str_equal (element_name, "KeyListEntry") | |
144 | ✗ | || attr_names == NULL | |
145 | ✗ | || attr_values == NULL) | |
146 | ✗ | return; | |
147 | |||
148 | ✗ | schema = NULL; | |
149 | ✗ | description = NULL; | |
150 | ✗ | context = NULL; | |
151 | ✗ | orig_description = NULL; | |
152 | ✗ | reverse_entry = NULL; | |
153 | ✗ | is_reversed = FALSE; | |
154 | ✗ | hidden = FALSE; | |
155 | |||
156 | ✗ | while (*attr_names && *attr_values) | |
157 | { | ||
158 | ✗ | if (g_str_equal (*attr_names, "name")) | |
159 | { | ||
160 | /* skip if empty */ | ||
161 | ✗ | if (**attr_values) | |
162 | ✗ | name = *attr_values; | |
163 | ✗ | } else if (g_str_equal (*attr_names, "schema")) { | |
164 | ✗ | if (**attr_values) { | |
165 | ✗ | schema = *attr_values; | |
166 | } | ||
167 | ✗ | } else if (g_str_equal (*attr_names, "description")) { | |
168 | ✗ | if (**attr_values) | |
169 | ✗ | orig_description = *attr_values; | |
170 | ✗ | } else if (g_str_equal (*attr_names, "msgctxt")) { | |
171 | ✗ | if (**attr_values) | |
172 | ✗ | context = *attr_values; | |
173 | ✗ | } else if (g_str_equal (*attr_names, "reverse-entry")) { | |
174 | ✗ | if (**attr_values) | |
175 | ✗ | reverse_entry = *attr_values; | |
176 | ✗ | } else if (g_str_equal (*attr_names, "is-reversed")) { | |
177 | ✗ | if (g_str_equal (*attr_values, "true")) | |
178 | ✗ | is_reversed = TRUE; | |
179 | ✗ | } else if (g_str_equal (*attr_names, "hidden")) { | |
180 | ✗ | if (g_str_equal (*attr_values, "true")) | |
181 | ✗ | hidden = TRUE; | |
182 | } | ||
183 | |||
184 | ✗ | ++attr_names; | |
185 | ✗ | ++attr_values; | |
186 | } | ||
187 | |||
188 | ✗ | if (name == NULL) | |
189 | ✗ | return; | |
190 | |||
191 | ✗ | if (schema == NULL && | |
192 | ✗ | keylist->schema == NULL) { | |
193 | ✗ | g_debug ("Ignored GConf keyboard shortcut '%s'", name); | |
194 | ✗ | return; | |
195 | } | ||
196 | |||
197 | ✗ | if (context != NULL) | |
198 | ✗ | description = g_dpgettext2 (keylist->package, context, orig_description); | |
199 | else | ||
200 | ✗ | description = dgettext (keylist->package, orig_description); | |
201 | |||
202 | ✗ | key.name = g_strdup (name); | |
203 | ✗ | key.type = CC_KEYBOARD_ITEM_TYPE_GSETTINGS; | |
204 | ✗ | key.description = replace_pictures_folder (description); | |
205 | ✗ | key.schema = schema ? g_strdup (schema) : g_strdup (keylist->schema); | |
206 | ✗ | key.reverse_entry = g_strdup (reverse_entry); | |
207 | ✗ | key.is_reversed = is_reversed; | |
208 | ✗ | key.hidden = hidden; | |
209 | ✗ | g_array_append_val (keylist->entries, key); | |
210 | } | ||
211 | |||
212 | static const guint forbidden_keyvals[] = { | ||
213 | /* Navigation keys */ | ||
214 | GDK_KEY_Home, | ||
215 | GDK_KEY_Left, | ||
216 | GDK_KEY_Up, | ||
217 | GDK_KEY_Right, | ||
218 | GDK_KEY_Down, | ||
219 | GDK_KEY_Page_Up, | ||
220 | GDK_KEY_Page_Down, | ||
221 | GDK_KEY_End, | ||
222 | GDK_KEY_Tab, | ||
223 | |||
224 | /* Return */ | ||
225 | GDK_KEY_KP_Enter, | ||
226 | GDK_KEY_Return, | ||
227 | |||
228 | GDK_KEY_Mode_switch | ||
229 | }; | ||
230 | |||
231 | static gboolean | ||
232 | ✗ | keyval_is_forbidden (guint keyval) | |
233 | { | ||
234 | guint i; | ||
235 | |||
236 | ✗ | for (i = 0; i < G_N_ELEMENTS(forbidden_keyvals); i++) { | |
237 | ✗ | if (keyval == forbidden_keyvals[i]) | |
238 | ✗ | return TRUE; | |
239 | } | ||
240 | |||
241 | ✗ | return FALSE; | |
242 | } | ||
243 | |||
244 | gboolean | ||
245 | ✗ | is_valid_binding (const CcKeyCombo *combo) | |
246 | { | ||
247 | ✗ | if ((combo->mask == 0 || combo->mask == GDK_SHIFT_MASK) && combo->keycode != 0) | |
248 | { | ||
249 | ✗ | guint keyval = combo->keyval; | |
250 | |||
251 | ✗ | if ((keyval >= GDK_KEY_a && keyval <= GDK_KEY_z) | |
252 | ✗ | || (keyval >= GDK_KEY_A && keyval <= GDK_KEY_Z) | |
253 | ✗ | || (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9) | |
254 | ✗ | || (keyval >= GDK_KEY_kana_fullstop && keyval <= GDK_KEY_semivoicedsound) | |
255 | ✗ | || (keyval >= GDK_KEY_Arabic_comma && keyval <= GDK_KEY_Arabic_sukun) | |
256 | ✗ | || (keyval >= GDK_KEY_Serbian_dje && keyval <= GDK_KEY_Cyrillic_HARDSIGN) | |
257 | ✗ | || (keyval >= GDK_KEY_Greek_ALPHAaccent && keyval <= GDK_KEY_Greek_omega) | |
258 | ✗ | || (keyval >= GDK_KEY_hebrew_doublelowline && keyval <= GDK_KEY_hebrew_taf) | |
259 | ✗ | || (keyval >= GDK_KEY_Thai_kokai && keyval <= GDK_KEY_Thai_lekkao) | |
260 | ✗ | || (keyval >= GDK_KEY_Hangul_Kiyeog && keyval <= GDK_KEY_Hangul_J_YeorinHieuh) | |
261 | ✗ | || (keyval == GDK_KEY_space && combo->mask == 0) | |
262 | ✗ | || keyval_is_forbidden (keyval)) { | |
263 | ✗ | return FALSE; | |
264 | } | ||
265 | } | ||
266 | ✗ | return TRUE; | |
267 | } | ||
268 | |||
269 | gboolean | ||
270 | ✗ | is_empty_binding (const CcKeyCombo *combo) | |
271 | { | ||
272 | ✗ | if (combo->keyval == 0 && | |
273 | ✗ | combo->mask == 0 && | |
274 | ✗ | combo->keycode == 0) | |
275 | ✗ | return TRUE; | |
276 | ✗ | return FALSE; | |
277 | } | ||
278 | |||
279 | gboolean | ||
280 | ✗ | is_valid_accel (const CcKeyCombo *combo) | |
281 | { | ||
282 | /* Unlike gtk_accelerator_valid(), we want to allow Tab when combined | ||
283 | * with some modifiers (Alt+Tab and friends) | ||
284 | */ | ||
285 | ✗ | return gtk_accelerator_valid (combo->keyval, combo->mask) || | |
286 | ✗ | (combo->keyval == GDK_KEY_Tab && combo->mask != 0); | |
287 | } | ||
288 | |||
289 | gchar* | ||
290 | ✗ | find_free_settings_path (GSettings *settings) | |
291 | { | ||
292 | ✗ | g_auto(GStrv) used_names = NULL; | |
293 | ✗ | g_autofree gchar *dir = NULL; | |
294 | int i, num, n_names; | ||
295 | |||
296 | ✗ | used_names = g_settings_get_strv (settings, "custom-keybindings"); | |
297 | ✗ | n_names = g_strv_length (used_names); | |
298 | |||
299 | ✗ | for (num = 0; dir == NULL; num++) | |
300 | { | ||
301 | ✗ | g_autofree gchar *tmp = NULL; | |
302 | ✗ | gboolean found = FALSE; | |
303 | |||
304 | ✗ | tmp = g_strdup_printf ("%s/custom%d/", CUSTOM_KEYS_BASENAME, num); | |
305 | ✗ | for (i = 0; i < n_names && !found; i++) | |
306 | ✗ | found = strcmp (used_names[i], tmp) == 0; | |
307 | |||
308 | ✗ | if (!found) | |
309 | ✗ | dir = g_steal_pointer (&tmp); | |
310 | } | ||
311 | |||
312 | ✗ | return g_steal_pointer (&dir); | |
313 | } | ||
314 | |||
315 | KeyList* | ||
316 | ✗ | parse_keylist_from_file (const gchar *path) | |
317 | { | ||
318 | KeyList *keylist; | ||
319 | ✗ | g_autoptr(GError) err = NULL; | |
320 | ✗ | g_autofree gchar *buf = NULL; | |
321 | gsize buf_len; | ||
322 | guint i; | ||
323 | |||
324 | ✗ | g_autoptr(GMarkupParseContext) ctx = NULL; | |
325 | ✗ | GMarkupParser parser = { parse_start_tag, NULL, NULL, NULL, NULL }; | |
326 | |||
327 | /* Parse file */ | ||
328 | ✗ | if (!g_file_get_contents (path, &buf, &buf_len, &err)) | |
329 | ✗ | return NULL; | |
330 | |||
331 | ✗ | keylist = g_new0 (KeyList, 1); | |
332 | ✗ | keylist->entries = g_array_new (FALSE, TRUE, sizeof (KeyListEntry)); | |
333 | ✗ | ctx = g_markup_parse_context_new (&parser, 0, keylist, NULL); | |
334 | |||
335 | ✗ | if (!g_markup_parse_context_parse (ctx, buf, buf_len, &err)) | |
336 | { | ||
337 | ✗ | g_warning ("Failed to parse '%s': '%s'", path, err->message); | |
338 | ✗ | g_free (keylist->name); | |
339 | ✗ | g_free (keylist->package); | |
340 | ✗ | g_free (keylist->wm_name); | |
341 | |||
342 | ✗ | for (i = 0; i < keylist->entries->len; i++) | |
343 | ✗ | g_free (((KeyListEntry *) &(keylist->entries->data[i]))->name); | |
344 | |||
345 | ✗ | g_array_free (keylist->entries, TRUE); | |
346 | ✗ | g_free (keylist); | |
347 | ✗ | return NULL; | |
348 | } | ||
349 | |||
350 | ✗ | return keylist; | |
351 | } | ||
352 | |||
353 | /* | ||
354 | * Stolen from GtkCellRendererAccel: | ||
355 | * https://git.gnome.org/browse/gtk+/tree/gtk/gtkcellrendereraccel.c#n261 | ||
356 | */ | ||
357 | gchar* | ||
358 | ✗ | convert_keysym_state_to_string (const CcKeyCombo *combo) | |
359 | { | ||
360 | gchar *name; | ||
361 | |||
362 | ✗ | if (combo->keyval == 0 && combo->keycode == 0) | |
363 | { | ||
364 | /* This label is displayed in a treeview cell displaying | ||
365 | * a disabled accelerator key combination. | ||
366 | */ | ||
367 | ✗ | name = g_strdup (_("Disabled")); | |
368 | } | ||
369 | else | ||
370 | { | ||
371 | ✗ | name = gtk_accelerator_get_label_with_keycode (NULL, combo->keyval, combo->keycode, combo->mask); | |
372 | |||
373 | ✗ | if (name == NULL) | |
374 | ✗ | name = gtk_accelerator_name_with_keycode (NULL, combo->keyval, combo->keycode, combo->mask); | |
375 | } | ||
376 | |||
377 | ✗ | return name; | |
378 | } | ||
379 | |||
380 | /* This adjusts the keyval and modifiers such that it matches how | ||
381 | * gnome-shell detects shortcuts, which works as follows: | ||
382 | * First for the non-modifier key, the keycode that generates this | ||
383 | * keyval at the lowest shift level is determined, which might be a | ||
384 | * level > 0, such as for numbers in the num-row in AZERTY. | ||
385 | * Next it checks if all the specified modifiers were pressed. | ||
386 | */ | ||
387 | void | ||
388 | 24 | normalize_keyval_and_mask (guint keycode, | |
389 | GdkModifierType mask, | ||
390 | guint group, | ||
391 | guint *out_keyval, | ||
392 | GdkModifierType *out_mask) | ||
393 | { | ||
394 | guint unmodified_keyval; | ||
395 | guint shifted_keyval; | ||
396 | GdkModifierType explicit_modifiers; | ||
397 | GdkModifierType used_modifiers; | ||
398 | |||
399 | /* We want shift to always be included as explicit modifier for | ||
400 | * gnome-shell shortcuts. That's because users usually think of | ||
401 | * shortcuts as including the shift key rather than being defined | ||
402 | * for the shifted keyval. | ||
403 | * This helps with num-row keys which have different keyvals on | ||
404 | * different layouts for example, but also with keys that have | ||
405 | * explicit key codes at shift level 0, that gnome-shell would prefer | ||
406 | * over shifted ones, such the DOLLAR key. | ||
407 | */ | ||
408 | 24 | explicit_modifiers = gtk_accelerator_get_default_mod_mask () | GDK_SHIFT_MASK; | |
409 | 24 | used_modifiers = mask & explicit_modifiers; | |
410 | |||
411 | /* Find the base keyval of the pressed key without the explicit | ||
412 | * modifiers. */ | ||
413 | 24 | gdk_display_translate_key (gdk_display_get_default (), | |
414 | keycode, | ||
415 | 24 | mask & ~explicit_modifiers, | |
416 | group, | ||
417 | &unmodified_keyval, | ||
418 | NULL, | ||
419 | NULL, | ||
420 | NULL); | ||
421 | |||
422 | /* Normalize num-row keys to the number value. This allows these | ||
423 | * shortcuts to work when switching between AZERTY and layouts where | ||
424 | * the numbers are at shift level 0. */ | ||
425 | 24 | gdk_display_translate_key (gdk_display_get_default (), | |
426 | keycode, | ||
427 | 24 | GDK_SHIFT_MASK | (mask & ~explicit_modifiers), | |
428 | group, | ||
429 | &shifted_keyval, | ||
430 | NULL, | ||
431 | NULL, | ||
432 | NULL); | ||
433 | |||
434 |
4/4✓ Branch 0 taken 15 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 13 times.
|
24 | if (shifted_keyval >= GDK_KEY_0 && shifted_keyval <= GDK_KEY_9) |
435 | 2 | unmodified_keyval = shifted_keyval; | |
436 | |||
437 | /* Normalise <Tab> */ | ||
438 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
|
24 | if (unmodified_keyval == GDK_KEY_ISO_Left_Tab) |
439 | ✗ | unmodified_keyval = GDK_KEY_Tab; | |
440 | |||
441 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
24 | if (unmodified_keyval == GDK_KEY_Sys_Req && (used_modifiers & GDK_ALT_MASK) != 0) |
442 | { | ||
443 | /* HACK: we don't want to use SysRq as a keybinding (but we do | ||
444 | * want Alt+Print), so we avoid translation from Alt+Print to SysRq */ | ||
445 | ✗ | unmodified_keyval = GDK_KEY_Print; | |
446 | } | ||
447 | |||
448 | 24 | *out_keyval = unmodified_keyval; | |
449 | 24 | *out_mask = used_modifiers; | |
450 | 24 | } | |
451 |