1 | /* gtkshortcutlabel.c |
2 | * |
3 | * Copyright (C) 2015 Christian Hergert <christian@hergert.me> |
4 | * |
5 | * This library is free software; you can redistribute it and/or |
6 | * modify it under the terms of the GNU Library General Public License as |
7 | * published by the Free Software Foundation; either version 2 of the |
8 | * License, or (at your option) any later version. |
9 | * |
10 | * This library 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 GNU |
13 | * Library General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU Library General Public |
16 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
17 | */ |
18 | |
19 | #include "config.h" |
20 | |
21 | #include "gtkshortcutlabel.h" |
22 | #include "gtkboxlayout.h" |
23 | #include "gtklabel.h" |
24 | #include "gtkframe.h" |
25 | #include "gtkwidgetprivate.h" |
26 | #include "gtkintl.h" |
27 | |
28 | /** |
29 | * GtkShortcutLabel: |
30 | * |
31 | * `GtkShortcutLabel` displays a single keyboard shortcut or gesture. |
32 | * |
33 | * The main use case for `GtkShortcutLabel` is inside a [class@Gtk.ShortcutsWindow]. |
34 | */ |
35 | |
36 | struct _GtkShortcutLabel |
37 | { |
38 | GtkWidget parent_instance; |
39 | char *accelerator; |
40 | char *disabled_text; |
41 | }; |
42 | |
43 | struct _GtkShortcutLabelClass |
44 | { |
45 | GtkWidgetClass parent_class; |
46 | }; |
47 | |
48 | G_DEFINE_TYPE (GtkShortcutLabel, gtk_shortcut_label, GTK_TYPE_WIDGET) |
49 | |
50 | enum { |
51 | PROP_0, |
52 | PROP_ACCELERATOR, |
53 | PROP_DISABLED_TEXT, |
54 | LAST_PROP |
55 | }; |
56 | |
57 | static GParamSpec *properties[LAST_PROP]; |
58 | |
59 | static char * |
60 | get_modifier_label (guint key) |
61 | { |
62 | const char *subscript; |
63 | const char *label; |
64 | |
65 | switch (key) |
66 | { |
67 | case GDK_KEY_Shift_L: |
68 | case GDK_KEY_Control_L: |
69 | case GDK_KEY_Alt_L: |
70 | case GDK_KEY_Meta_L: |
71 | case GDK_KEY_Super_L: |
72 | case GDK_KEY_Hyper_L: |
73 | /* Translators: This string is used to mark left/right variants of modifier |
74 | * keys in the shortcut window (e.g. Control_L vs Control_R). Please keep |
75 | * this string very short, ideally just a single character, since it will |
76 | * be rendered as part of the key. |
77 | */ |
78 | subscript = C_("keyboard side marker" , "L" ); |
79 | break; |
80 | case GDK_KEY_Shift_R: |
81 | case GDK_KEY_Control_R: |
82 | case GDK_KEY_Alt_R: |
83 | case GDK_KEY_Meta_R: |
84 | case GDK_KEY_Super_R: |
85 | case GDK_KEY_Hyper_R: |
86 | /* Translators: This string is used to mark left/right variants of modifier |
87 | * keys in the shortcut window (e.g. Control_L vs Control_R). Please keep |
88 | * this string very short, ideally just a single character, since it will |
89 | * be rendered as part of the key. |
90 | */ |
91 | subscript = C_("keyboard side marker" , "R" ); |
92 | break; |
93 | default: |
94 | g_assert_not_reached (); |
95 | } |
96 | |
97 | switch (key) |
98 | { |
99 | case GDK_KEY_Shift_L: case GDK_KEY_Shift_R: |
100 | label = C_("keyboard label" , "Shift" ); |
101 | break; |
102 | case GDK_KEY_Control_L: case GDK_KEY_Control_R: |
103 | label = C_("keyboard label" , "Ctrl" ); |
104 | break; |
105 | case GDK_KEY_Alt_L: case GDK_KEY_Alt_R: |
106 | label = C_("keyboard label" , "Alt" ); |
107 | break; |
108 | case GDK_KEY_Meta_L: case GDK_KEY_Meta_R: |
109 | label = C_("keyboard label" , "Meta" ); |
110 | break; |
111 | case GDK_KEY_Super_L: case GDK_KEY_Super_R: |
112 | label = C_("keyboard label" , "Super" ); |
113 | break; |
114 | case GDK_KEY_Hyper_L: case GDK_KEY_Hyper_R: |
115 | label = C_("keyboard label" , "Hyper" ); |
116 | break; |
117 | default: |
118 | g_assert_not_reached (); |
119 | } |
120 | |
121 | return g_strdup_printf (format: "%s <small><b>%s</b></small>" , label, subscript); |
122 | } |
123 | |
124 | static char ** |
125 | get_labels (guint key, GdkModifierType modifier, guint *n_mods) |
126 | { |
127 | const char *labels[16]; |
128 | GList *freeme = NULL; |
129 | char key_label[6]; |
130 | const char *tmp; |
131 | gunichar ch; |
132 | int i = 0; |
133 | char **retval; |
134 | |
135 | if (modifier & GDK_SHIFT_MASK) |
136 | labels[i++] = C_("keyboard label" , "Shift" ); |
137 | if (modifier & GDK_CONTROL_MASK) |
138 | labels[i++] = C_("keyboard label" , "Ctrl" ); |
139 | if (modifier & GDK_ALT_MASK) |
140 | labels[i++] = C_("keyboard label" , "Alt" ); |
141 | if (modifier & GDK_SUPER_MASK) |
142 | labels[i++] = C_("keyboard label" , "Super" ); |
143 | if (modifier & GDK_HYPER_MASK) |
144 | labels[i++] = C_("keyboard label" , "Hyper" ); |
145 | if (modifier & GDK_META_MASK) |
146 | labels[i++] = C_("keyboard label" , "Meta" ); |
147 | |
148 | *n_mods = i; |
149 | |
150 | ch = gdk_keyval_to_unicode (keyval: key); |
151 | if (ch && ch < 0x80 && g_unichar_isgraph (c: ch)) |
152 | { |
153 | switch (ch) |
154 | { |
155 | case '<': |
156 | labels[i++] = "<" ; |
157 | break; |
158 | case '>': |
159 | labels[i++] = ">" ; |
160 | break; |
161 | case '&': |
162 | labels[i++] = "&" ; |
163 | break; |
164 | case '"': |
165 | labels[i++] = """ ; |
166 | break; |
167 | case '\'': |
168 | labels[i++] = "'" ; |
169 | break; |
170 | case '\\': |
171 | labels[i++] = C_("keyboard label" , "Backslash" ); |
172 | break; |
173 | default: |
174 | memset (s: key_label, c: 0, n: 6); |
175 | g_unichar_to_utf8 (c: g_unichar_toupper (c: ch), outbuf: key_label); |
176 | labels[i++] = key_label; |
177 | break; |
178 | } |
179 | } |
180 | else |
181 | { |
182 | switch (key) |
183 | { |
184 | case GDK_KEY_Shift_L: case GDK_KEY_Shift_R: |
185 | case GDK_KEY_Control_L: case GDK_KEY_Control_R: |
186 | case GDK_KEY_Alt_L: case GDK_KEY_Alt_R: |
187 | case GDK_KEY_Meta_L: case GDK_KEY_Meta_R: |
188 | case GDK_KEY_Super_L: case GDK_KEY_Super_R: |
189 | case GDK_KEY_Hyper_L: case GDK_KEY_Hyper_R: |
190 | freeme = g_list_prepend (list: freeme, data: get_modifier_label (key)); |
191 | labels[i++] = (const char *)freeme->data; |
192 | break; |
193 | case GDK_KEY_Left: |
194 | labels[i++] = "\xe2\x86\x90" ; |
195 | break; |
196 | case GDK_KEY_Up: |
197 | labels[i++] = "\xe2\x86\x91" ; |
198 | break; |
199 | case GDK_KEY_Right: |
200 | labels[i++] = "\xe2\x86\x92" ; |
201 | break; |
202 | case GDK_KEY_Down: |
203 | labels[i++] = "\xe2\x86\x93" ; |
204 | break; |
205 | case GDK_KEY_space: |
206 | labels[i++] = "\xe2\x90\xa3" ; |
207 | break; |
208 | case GDK_KEY_Return: |
209 | labels[i++] = "\xe2\x8f\x8e" ; |
210 | break; |
211 | case GDK_KEY_Page_Up: |
212 | labels[i++] = C_("keyboard label" , "Page_Up" ); |
213 | break; |
214 | case GDK_KEY_Page_Down: |
215 | labels[i++] = C_("keyboard label" , "Page_Down" ); |
216 | break; |
217 | default: |
218 | tmp = gdk_keyval_name (keyval: gdk_keyval_to_lower (keyval: key)); |
219 | if (tmp != NULL) |
220 | { |
221 | if (tmp[0] != 0 && tmp[1] == 0) |
222 | { |
223 | key_label[0] = g_ascii_toupper (c: tmp[0]); |
224 | key_label[1] = '\0'; |
225 | labels[i++] = key_label; |
226 | } |
227 | else |
228 | { |
229 | labels[i++] = g_dpgettext2 (GETTEXT_PACKAGE, context: "keyboard label" , msgid: tmp); |
230 | } |
231 | } |
232 | } |
233 | } |
234 | |
235 | labels[i] = NULL; |
236 | |
237 | retval = g_strdupv (str_array: (char **)labels); |
238 | |
239 | g_list_free_full (list: freeme, free_func: g_free); |
240 | |
241 | return retval; |
242 | } |
243 | |
244 | static GtkWidget * |
245 | dim_label (const char *text) |
246 | { |
247 | GtkWidget *label; |
248 | |
249 | label = gtk_label_new (str: text); |
250 | gtk_widget_add_css_class (widget: label, css_class: "dim-label" ); |
251 | |
252 | return label; |
253 | } |
254 | |
255 | static void |
256 | display_shortcut (GtkWidget *self, |
257 | guint key, |
258 | GdkModifierType modifier) |
259 | { |
260 | char **keys = NULL; |
261 | int i; |
262 | guint n_mods; |
263 | |
264 | keys = get_labels (key, modifier, n_mods: &n_mods); |
265 | for (i = 0; keys[i]; i++) |
266 | { |
267 | GtkWidget *disp; |
268 | |
269 | if (i > 0) |
270 | gtk_widget_set_parent (widget: dim_label (text: "+" ), parent: self); |
271 | |
272 | disp = gtk_label_new (str: keys[i]); |
273 | if (i < n_mods) |
274 | gtk_widget_set_size_request (widget: disp, width: 50, height: -1); |
275 | |
276 | gtk_widget_add_css_class (widget: disp, css_class: "keycap" ); |
277 | gtk_label_set_use_markup (GTK_LABEL (disp), TRUE); |
278 | |
279 | gtk_widget_set_parent (widget: disp, parent: self); |
280 | } |
281 | g_strfreev (str_array: keys); |
282 | } |
283 | |
284 | static gboolean |
285 | parse_combination (GtkShortcutLabel *self, |
286 | const char *str) |
287 | { |
288 | char **accels; |
289 | int k; |
290 | GdkModifierType modifier = 0; |
291 | guint key = 0; |
292 | gboolean retval = TRUE; |
293 | |
294 | accels = g_strsplit (string: str, delimiter: "&" , max_tokens: 0); |
295 | for (k = 0; accels[k]; k++) |
296 | { |
297 | if (!gtk_accelerator_parse (accelerator: accels[k], accelerator_key: &key, accelerator_mods: &modifier)) |
298 | { |
299 | retval = FALSE; |
300 | break; |
301 | } |
302 | if (k > 0) |
303 | gtk_widget_set_parent (widget: dim_label (text: "+" ), GTK_WIDGET (self)); |
304 | |
305 | display_shortcut (GTK_WIDGET (self), key, modifier); |
306 | } |
307 | g_strfreev (str_array: accels); |
308 | |
309 | return retval; |
310 | } |
311 | |
312 | static gboolean |
313 | parse_sequence (GtkShortcutLabel *self, |
314 | const char *str) |
315 | { |
316 | char **accels; |
317 | int k; |
318 | gboolean retval = TRUE; |
319 | |
320 | accels = g_strsplit (string: str, delimiter: "+" , max_tokens: 0); |
321 | for (k = 0; accels[k]; k++) |
322 | { |
323 | if (!parse_combination (self, str: accels[k])) |
324 | { |
325 | retval = FALSE; |
326 | break; |
327 | } |
328 | } |
329 | |
330 | g_strfreev (str_array: accels); |
331 | |
332 | return retval; |
333 | } |
334 | |
335 | static gboolean |
336 | parse_range (GtkShortcutLabel *self, |
337 | const char *str) |
338 | { |
339 | char *dots; |
340 | |
341 | dots = strstr (haystack: str, needle: "..." ); |
342 | if (!dots) |
343 | return parse_sequence (self, str); |
344 | |
345 | dots[0] = '\0'; |
346 | if (!parse_sequence (self, str)) |
347 | return FALSE; |
348 | |
349 | gtk_widget_set_parent (widget: dim_label (text: "⋯" ), GTK_WIDGET (self)); |
350 | |
351 | if (!parse_sequence (self, str: dots + 3)) |
352 | return FALSE; |
353 | |
354 | return TRUE; |
355 | } |
356 | |
357 | static void |
358 | clear_children (GtkShortcutLabel *self) |
359 | { |
360 | GtkWidget *child; |
361 | |
362 | child = gtk_widget_get_first_child (GTK_WIDGET (self)); |
363 | |
364 | while (child) |
365 | { |
366 | GtkWidget *next = gtk_widget_get_next_sibling (widget: child); |
367 | |
368 | gtk_widget_unparent (widget: child); |
369 | |
370 | child = next; |
371 | } |
372 | } |
373 | |
374 | static void |
375 | gtk_shortcut_label_rebuild (GtkShortcutLabel *self) |
376 | { |
377 | char **accels; |
378 | int k; |
379 | |
380 | clear_children (self); |
381 | |
382 | if (self->accelerator == NULL || self->accelerator[0] == '\0') |
383 | { |
384 | GtkWidget *label; |
385 | |
386 | label = dim_label (text: self->disabled_text); |
387 | |
388 | gtk_widget_set_parent (widget: label, GTK_WIDGET (self)); |
389 | return; |
390 | } |
391 | |
392 | accels = g_strsplit (string: self->accelerator, delimiter: " " , max_tokens: 0); |
393 | for (k = 0; accels[k]; k++) |
394 | { |
395 | if (k > 0) |
396 | gtk_widget_set_parent (widget: dim_label (text: "/" ), GTK_WIDGET (self)); |
397 | |
398 | if (!parse_range (self, str: accels[k])) |
399 | { |
400 | g_warning ("Failed to parse %s, part of accelerator '%s'" , accels[k], self->accelerator); |
401 | break; |
402 | } |
403 | } |
404 | g_strfreev (str_array: accels); |
405 | } |
406 | |
407 | static void |
408 | gtk_shortcut_label_finalize (GObject *object) |
409 | { |
410 | GtkShortcutLabel *self = (GtkShortcutLabel *)object; |
411 | |
412 | g_free (mem: self->accelerator); |
413 | g_free (mem: self->disabled_text); |
414 | |
415 | clear_children (self); |
416 | |
417 | G_OBJECT_CLASS (gtk_shortcut_label_parent_class)->finalize (object); |
418 | } |
419 | |
420 | static void |
421 | gtk_shortcut_label_get_property (GObject *object, |
422 | guint prop_id, |
423 | GValue *value, |
424 | GParamSpec *pspec) |
425 | { |
426 | GtkShortcutLabel *self = GTK_SHORTCUT_LABEL (object); |
427 | |
428 | switch (prop_id) |
429 | { |
430 | case PROP_ACCELERATOR: |
431 | g_value_set_string (value, v_string: gtk_shortcut_label_get_accelerator (self)); |
432 | break; |
433 | |
434 | case PROP_DISABLED_TEXT: |
435 | g_value_set_string (value, v_string: gtk_shortcut_label_get_disabled_text (self)); |
436 | break; |
437 | |
438 | default: |
439 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
440 | } |
441 | } |
442 | |
443 | static void |
444 | gtk_shortcut_label_set_property (GObject *object, |
445 | guint prop_id, |
446 | const GValue *value, |
447 | GParamSpec *pspec) |
448 | { |
449 | GtkShortcutLabel *self = GTK_SHORTCUT_LABEL (object); |
450 | |
451 | switch (prop_id) |
452 | { |
453 | case PROP_ACCELERATOR: |
454 | gtk_shortcut_label_set_accelerator (self, accelerator: g_value_get_string (value)); |
455 | break; |
456 | |
457 | case PROP_DISABLED_TEXT: |
458 | gtk_shortcut_label_set_disabled_text (self, disabled_text: g_value_get_string (value)); |
459 | break; |
460 | |
461 | default: |
462 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
463 | } |
464 | } |
465 | |
466 | static void |
467 | gtk_shortcut_label_class_init (GtkShortcutLabelClass *klass) |
468 | { |
469 | GObjectClass *object_class = G_OBJECT_CLASS (klass); |
470 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); |
471 | |
472 | object_class->finalize = gtk_shortcut_label_finalize; |
473 | object_class->get_property = gtk_shortcut_label_get_property; |
474 | object_class->set_property = gtk_shortcut_label_set_property; |
475 | |
476 | /** |
477 | * GtkShortcutLabel:accelerator: (attributes org.gtk.Property.get=gtk_shortcut_label_get_accelerator org.gtk.Property.set=gtk_shortcut_label_set_accelerator) |
478 | * |
479 | * The accelerator that @self displays. |
480 | * |
481 | * See [property@Gtk.ShortcutsShortcut:accelerator] |
482 | * for the accepted syntax. |
483 | */ |
484 | properties[PROP_ACCELERATOR] = |
485 | g_param_spec_string (name: "accelerator" , P_("Accelerator" ), P_("Accelerator" ), |
486 | NULL, |
487 | flags: (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); |
488 | |
489 | /** |
490 | * GtkShortcutLabel:disabled-text: (attributes org.gtk.Property.get=gtk_shortcut_label_get_disabled_text org.gtk.Property.set=gtk_shortcut_label_set_disabled_text) |
491 | * |
492 | * The text that is displayed when no accelerator is set. |
493 | */ |
494 | properties[PROP_DISABLED_TEXT] = |
495 | g_param_spec_string (name: "disabled-text" , P_("Disabled text" ), P_("Disabled text" ), |
496 | NULL, |
497 | flags: (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); |
498 | |
499 | g_object_class_install_properties (oclass: object_class, n_pspecs: LAST_PROP, pspecs: properties); |
500 | |
501 | gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BOX_LAYOUT); |
502 | gtk_widget_class_set_css_name (widget_class, I_("shortcut" )); |
503 | } |
504 | |
505 | static void |
506 | gtk_shortcut_label_init (GtkShortcutLabel *self) |
507 | { |
508 | /* Always use LTR so that modifiers are always left to the keyval */ |
509 | gtk_widget_set_direction (GTK_WIDGET (self), dir: GTK_TEXT_DIR_LTR); |
510 | } |
511 | |
512 | /** |
513 | * gtk_shortcut_label_new: |
514 | * @accelerator: the initial accelerator |
515 | * |
516 | * Creates a new `GtkShortcutLabel` with @accelerator set. |
517 | * |
518 | * Returns: a newly-allocated `GtkShortcutLabel` |
519 | */ |
520 | GtkWidget * |
521 | gtk_shortcut_label_new (const char *accelerator) |
522 | { |
523 | return g_object_new (GTK_TYPE_SHORTCUT_LABEL, |
524 | first_property_name: "accelerator" , accelerator, |
525 | NULL); |
526 | } |
527 | |
528 | /** |
529 | * gtk_shortcut_label_get_accelerator: (attributes org.gtk.Method.get_property=accelerator) |
530 | * @self: a `GtkShortcutLabel` |
531 | * |
532 | * Retrieves the current accelerator of @self. |
533 | * |
534 | * Returns: (transfer none)(nullable): the current accelerator. |
535 | */ |
536 | const char * |
537 | gtk_shortcut_label_get_accelerator (GtkShortcutLabel *self) |
538 | { |
539 | g_return_val_if_fail (GTK_IS_SHORTCUT_LABEL (self), NULL); |
540 | |
541 | return self->accelerator; |
542 | } |
543 | |
544 | /** |
545 | * gtk_shortcut_label_set_accelerator: (attributes org.gtk.Method.set_property=accelerator) |
546 | * @self: a `GtkShortcutLabel` |
547 | * @accelerator: the new accelerator |
548 | * |
549 | * Sets the accelerator to be displayed by @self. |
550 | */ |
551 | void |
552 | gtk_shortcut_label_set_accelerator (GtkShortcutLabel *self, |
553 | const char *accelerator) |
554 | { |
555 | g_return_if_fail (GTK_IS_SHORTCUT_LABEL (self)); |
556 | |
557 | if (g_strcmp0 (str1: accelerator, str2: self->accelerator) != 0) |
558 | { |
559 | g_free (mem: self->accelerator); |
560 | self->accelerator = g_strdup (str: accelerator); |
561 | gtk_shortcut_label_rebuild (self); |
562 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_ACCELERATOR]); |
563 | } |
564 | } |
565 | |
566 | /** |
567 | * gtk_shortcut_label_get_disabled_text: (attributes org.gtk.Method.get_property=disabled-text) |
568 | * @self: a `GtkShortcutLabel` |
569 | * |
570 | * Retrieves the text that is displayed when no accelerator is set. |
571 | * |
572 | * Returns: (transfer none)(nullable): the current text displayed when no |
573 | * accelerator is set. |
574 | */ |
575 | const char * |
576 | gtk_shortcut_label_get_disabled_text (GtkShortcutLabel *self) |
577 | { |
578 | g_return_val_if_fail (GTK_IS_SHORTCUT_LABEL (self), NULL); |
579 | |
580 | return self->disabled_text; |
581 | } |
582 | |
583 | /** |
584 | * gtk_shortcut_label_set_disabled_text: (attributes org.gtk.Method.set_property=disabled-text) |
585 | * @self: a `GtkShortcutLabel` |
586 | * @disabled_text: the text to be displayed when no accelerator is set |
587 | * |
588 | * Sets the text to be displayed by @self when no accelerator is set. |
589 | */ |
590 | void |
591 | gtk_shortcut_label_set_disabled_text (GtkShortcutLabel *self, |
592 | const char *disabled_text) |
593 | { |
594 | g_return_if_fail (GTK_IS_SHORTCUT_LABEL (self)); |
595 | |
596 | if (g_strcmp0 (str1: disabled_text, str2: self->disabled_text) != 0) |
597 | { |
598 | g_free (mem: self->disabled_text); |
599 | self->disabled_text = g_strdup (str: disabled_text); |
600 | gtk_shortcut_label_rebuild (self); |
601 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_DISABLED_TEXT]); |
602 | } |
603 | } |
604 | |