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
36struct _GtkShortcutLabel
37{
38 GtkWidget parent_instance;
39 char *accelerator;
40 char *disabled_text;
41};
42
43struct _GtkShortcutLabelClass
44{
45 GtkWidgetClass parent_class;
46};
47
48G_DEFINE_TYPE (GtkShortcutLabel, gtk_shortcut_label, GTK_TYPE_WIDGET)
49
50enum {
51 PROP_0,
52 PROP_ACCELERATOR,
53 PROP_DISABLED_TEXT,
54 LAST_PROP
55};
56
57static GParamSpec *properties[LAST_PROP];
58
59static char *
60get_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
124static char **
125get_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++] = "&lt;";
157 break;
158 case '>':
159 labels[i++] = "&gt;";
160 break;
161 case '&':
162 labels[i++] = "&amp;";
163 break;
164 case '"':
165 labels[i++] = "&quot;";
166 break;
167 case '\'':
168 labels[i++] = "&apos;";
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
244static GtkWidget *
245dim_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
255static void
256display_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
284static gboolean
285parse_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
312static gboolean
313parse_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
335static gboolean
336parse_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
357static void
358clear_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
374static void
375gtk_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
407static void
408gtk_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
420static void
421gtk_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
443static void
444gtk_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
466static void
467gtk_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
505static void
506gtk_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 */
520GtkWidget *
521gtk_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 */
536const char *
537gtk_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 */
551void
552gtk_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 */
575const char *
576gtk_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 */
590void
591gtk_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

source code of gtk/gtk/gtkshortcutlabel.c