1 /* dzl-shortcut-context.c
2 *
3 * Copyright (C) 2017 Christian Hergert <chergert@redhat.com>
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
19 #define G_LOG_DOMAIN "dzl-shortcut-context"
20
21 #include "config.h"
22
23 #include <gobject/gvaluecollector.h>
24 #include <string.h>
25
26 #include "dzl-debug.h"
27
28 #include "shortcuts/dzl-shortcut-chord.h"
29 #include "shortcuts/dzl-shortcut-closure-chain.h"
30 #include "shortcuts/dzl-shortcut-context.h"
31 #include "shortcuts/dzl-shortcut-controller.h"
32 #include "shortcuts/dzl-shortcut-private.h"
33 #include "util/dzl-macros.h"
34
35 typedef struct
36 {
37 /* The name of the context, interned */
38 const gchar *name;
39
40 /* The table of entries in this context which maps to a shortcut.
41 * These need to be copied across when merging down to another
42 * context layer.
43 */
44 DzlShortcutChordTable *table;
45
46 /* If we should use binding sets. By default this is true, but
47 * we use a signed 2-bit int for -1 being "unset". That allows
48 * us to know when the value was set on a layer and merge that
49 * value upwards.
50 */
51 gint use_binding_sets : 2;
52 } DzlShortcutContextPrivate;
53
54 enum {
55 PROP_0,
56 PROP_NAME,
57 PROP_USE_BINDING_SETS,
58 N_PROPS
59 };
60
61 struct _DzlShortcutContext
62 {
63 GObject parent_instance;
64 };
65
G_DEFINE_TYPE_WITH_PRIVATE(DzlShortcutContext,dzl_shortcut_context,G_TYPE_OBJECT)66 G_DEFINE_TYPE_WITH_PRIVATE (DzlShortcutContext, dzl_shortcut_context, G_TYPE_OBJECT)
67
68 static GParamSpec *properties [N_PROPS];
69
70 static void
71 dzl_shortcut_context_finalize (GObject *object)
72 {
73 DzlShortcutContext *self = (DzlShortcutContext *)object;
74 DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self);
75
76 g_clear_pointer (&priv->table, dzl_shortcut_chord_table_free);
77
78 G_OBJECT_CLASS (dzl_shortcut_context_parent_class)->finalize (object);
79 }
80
81 static void
dzl_shortcut_context_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)82 dzl_shortcut_context_get_property (GObject *object,
83 guint prop_id,
84 GValue *value,
85 GParamSpec *pspec)
86 {
87 DzlShortcutContext *self = (DzlShortcutContext *)object;
88 DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self);
89
90 switch (prop_id)
91 {
92 case PROP_NAME:
93 g_value_set_string (value, priv->name);
94 break;
95
96 case PROP_USE_BINDING_SETS:
97 g_value_set_boolean (value, !!priv->use_binding_sets);
98 break;
99
100 default:
101 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
102 }
103 }
104
105 static void
dzl_shortcut_context_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)106 dzl_shortcut_context_set_property (GObject *object,
107 guint prop_id,
108 const GValue *value,
109 GParamSpec *pspec)
110 {
111 DzlShortcutContext *self = (DzlShortcutContext *)object;
112 DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self);
113
114 switch (prop_id)
115 {
116 case PROP_NAME:
117 priv->name = g_intern_string (g_value_get_string (value));
118 break;
119
120 case PROP_USE_BINDING_SETS:
121 priv->use_binding_sets = g_value_get_boolean (value);
122 break;
123
124 default:
125 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
126 }
127 }
128
129 static void
dzl_shortcut_context_class_init(DzlShortcutContextClass * klass)130 dzl_shortcut_context_class_init (DzlShortcutContextClass *klass)
131 {
132 GObjectClass *object_class = G_OBJECT_CLASS (klass);
133
134 object_class->finalize = dzl_shortcut_context_finalize;
135 object_class->get_property = dzl_shortcut_context_get_property;
136 object_class->set_property = dzl_shortcut_context_set_property;
137
138 properties [PROP_NAME] =
139 g_param_spec_string ("name",
140 "Name",
141 "Name",
142 NULL,
143 (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
144
145 properties [PROP_USE_BINDING_SETS] =
146 g_param_spec_boolean ("use-binding-sets",
147 "Use Binding Sets",
148 "If the context should allow activation using binding sets",
149 TRUE,
150 (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
151
152 g_object_class_install_properties (object_class, N_PROPS, properties);
153 }
154
155 static void
dzl_shortcut_context_init(DzlShortcutContext * self)156 dzl_shortcut_context_init (DzlShortcutContext *self)
157 {
158 DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self);
159
160 priv->use_binding_sets = -1;
161 }
162
163 DzlShortcutContext *
dzl_shortcut_context_new(const gchar * name)164 dzl_shortcut_context_new (const gchar *name)
165 {
166 return g_object_new (DZL_TYPE_SHORTCUT_CONTEXT,
167 "name", name,
168 NULL);
169 }
170
171 const gchar *
dzl_shortcut_context_get_name(DzlShortcutContext * self)172 dzl_shortcut_context_get_name (DzlShortcutContext *self)
173 {
174 DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self);
175
176 g_return_val_if_fail (DZL_IS_SHORTCUT_CONTEXT (self), NULL);
177
178 return priv->name;
179 }
180
181 gboolean
_dzl_shortcut_context_contains(DzlShortcutContext * self,const DzlShortcutChord * chord)182 _dzl_shortcut_context_contains (DzlShortcutContext *self,
183 const DzlShortcutChord *chord)
184 {
185 DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self);
186 gpointer data;
187
188 g_return_val_if_fail (DZL_IS_SHORTCUT_CONTEXT (self), FALSE);
189 g_return_val_if_fail (chord != NULL, FALSE);
190
191 return priv->table != NULL &&
192 dzl_shortcut_chord_table_lookup (priv->table, chord, &data) == DZL_SHORTCUT_MATCH_EQUAL;
193 }
194
195 DzlShortcutMatch
dzl_shortcut_context_activate(DzlShortcutContext * self,GtkWidget * widget,const DzlShortcutChord * chord)196 dzl_shortcut_context_activate (DzlShortcutContext *self,
197 GtkWidget *widget,
198 const DzlShortcutChord *chord)
199 {
200 DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self);
201 DzlShortcutMatch match = DZL_SHORTCUT_MATCH_NONE;
202 DzlShortcutClosureChain *chain = NULL;
203
204 DZL_ENTRY;
205
206 g_return_val_if_fail (DZL_IS_SHORTCUT_CONTEXT (self), DZL_SHORTCUT_MATCH_NONE);
207 g_return_val_if_fail (GTK_IS_WIDGET (widget), DZL_SHORTCUT_MATCH_NONE);
208 g_return_val_if_fail (chord != NULL, DZL_SHORTCUT_MATCH_NONE);
209
210 if (priv->table == NULL)
211 DZL_RETURN (DZL_SHORTCUT_MATCH_NONE);
212
213 #if 0
214 g_print ("Looking up %s in table %p (of size %u)\n",
215 dzl_shortcut_chord_to_string (chord),
216 priv->table,
217 dzl_shortcut_chord_table_size (priv->table));
218
219 dzl_shortcut_chord_table_printf (priv->table);
220 #endif
221
222 match = dzl_shortcut_chord_table_lookup (priv->table, chord, (gpointer *)&chain);
223
224 if (match == DZL_SHORTCUT_MATCH_EQUAL)
225 {
226 g_assert (chain != NULL);
227
228 /*
229 * If we got a full match, but it failed to activate, we could potentially
230 * have another partial match. However, that lands squarely in the land of
231 * undefined behavior. So instead we just assume there was no match.
232 */
233 if (!dzl_shortcut_closure_chain_execute (chain, widget))
234 match = DZL_SHORTCUT_MATCH_NONE;
235 }
236
237 DZL_TRACE_MSG ("%s: match = %d", priv->name, match);
238
239 DZL_RETURN (match);
240 }
241
242 static void
dzl_shortcut_context_add(DzlShortcutContext * self,const DzlShortcutChord * chord,DzlShortcutClosureChain * chain)243 dzl_shortcut_context_add (DzlShortcutContext *self,
244 const DzlShortcutChord *chord,
245 DzlShortcutClosureChain *chain)
246 {
247 DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self);
248 DzlShortcutClosureChain *head = NULL;
249 DzlShortcutMatch match;
250
251 g_assert (DZL_IS_SHORTCUT_CONTEXT (self));
252 g_assert (chord != NULL);
253 g_assert (chain != NULL);
254
255 if (priv->table == NULL)
256 {
257 priv->table = dzl_shortcut_chord_table_new ();
258 dzl_shortcut_chord_table_set_free_func (priv->table,
259 (GDestroyNotify)dzl_shortcut_closure_chain_free);
260 }
261
262 /*
263 * If we find that there is another entry for this shortcut, we chain onto
264 * the end of that item. This allows us to call multiple signals, or
265 * interleave signals and actions.
266 */
267
268 match = dzl_shortcut_chord_table_lookup (priv->table, chord, (gpointer *)&head);
269
270 if (match == DZL_SHORTCUT_MATCH_EQUAL)
271 dzl_shortcut_closure_chain_append (head, chain);
272 else
273 dzl_shortcut_chord_table_add (priv->table, chord, chain);
274 }
275
276 void
dzl_shortcut_context_add_action(DzlShortcutContext * self,const gchar * accel,const gchar * detailed_action_name)277 dzl_shortcut_context_add_action (DzlShortcutContext *self,
278 const gchar *accel,
279 const gchar *detailed_action_name)
280 {
281 g_autoptr(DzlShortcutChord) chord = NULL;
282 DzlShortcutClosureChain *chain;
283
284 g_return_if_fail (DZL_IS_SHORTCUT_CONTEXT (self));
285 g_return_if_fail (accel != NULL);
286 g_return_if_fail (detailed_action_name != NULL);
287
288 chord = dzl_shortcut_chord_new_from_string (accel);
289
290 if (chord == NULL)
291 {
292 g_warning ("Failed to parse accelerator “%s”", accel);
293 return;
294 }
295
296 chain = dzl_shortcut_closure_chain_append_action_string (NULL, detailed_action_name);
297
298 dzl_shortcut_context_add (self, chord, chain);
299 }
300
301 void
dzl_shortcut_context_add_command(DzlShortcutContext * self,const gchar * accel,const gchar * command)302 dzl_shortcut_context_add_command (DzlShortcutContext *self,
303 const gchar *accel,
304 const gchar *command)
305 {
306 g_autoptr(DzlShortcutChord) chord = NULL;
307 DzlShortcutClosureChain *chain;
308
309 g_return_if_fail (DZL_IS_SHORTCUT_CONTEXT (self));
310 g_return_if_fail (accel != NULL);
311 g_return_if_fail (command != NULL);
312
313 chord = dzl_shortcut_chord_new_from_string (accel);
314
315 if (chord == NULL)
316 {
317 g_warning ("Failed to parse accelerator “%s” for command “%s”",
318 accel, command);
319 return;
320 }
321
322 chain = dzl_shortcut_closure_chain_append_command (NULL, command);
323
324 dzl_shortcut_context_add (self, chord, chain);
325 }
326
327 void
dzl_shortcut_context_add_signal_va_list(DzlShortcutContext * self,const gchar * accel,const gchar * signal_name,guint n_args,va_list args)328 dzl_shortcut_context_add_signal_va_list (DzlShortcutContext *self,
329 const gchar *accel,
330 const gchar *signal_name,
331 guint n_args,
332 va_list args)
333 {
334 g_autoptr(DzlShortcutChord) chord = NULL;
335 DzlShortcutClosureChain *chain;
336
337 g_return_if_fail (DZL_IS_SHORTCUT_CONTEXT (self));
338 g_return_if_fail (accel != NULL);
339 g_return_if_fail (signal_name != NULL);
340
341 chord = dzl_shortcut_chord_new_from_string (accel);
342
343 if (chord == NULL)
344 {
345 g_warning ("Failed to parse accelerator \"%s\"", accel);
346 return;
347 }
348
349 chain = dzl_shortcut_closure_chain_append_signal (NULL, signal_name, n_args, args);
350
351 dzl_shortcut_context_add (self, chord, chain);
352 }
353
354 void
dzl_shortcut_context_add_signal(DzlShortcutContext * self,const gchar * accel,const gchar * signal_name,guint n_args,...)355 dzl_shortcut_context_add_signal (DzlShortcutContext *self,
356 const gchar *accel,
357 const gchar *signal_name,
358 guint n_args,
359 ...)
360 {
361 va_list args;
362
363 va_start (args, n_args);
364 dzl_shortcut_context_add_signal_va_list (self, accel, signal_name, n_args, args);
365 va_end (args);
366 }
367
368 /**
369 * dzl_shortcut_context_add_signalv:
370 * @self: a #DzlShortcutContext
371 * @accel: the accelerator for the shortcut
372 * @signal_name: the name of the signal
373 * @values: (element-type GObject.Value) (nullable) (transfer none): The
374 * values to use when calling the signal.
375 *
376 * This is similar to dzl_shortcut_context_add_signal() but is easier to use
377 * from language bindings.
378 */
379 void
dzl_shortcut_context_add_signalv(DzlShortcutContext * self,const gchar * accel,const gchar * signal_name,GArray * values)380 dzl_shortcut_context_add_signalv (DzlShortcutContext *self,
381 const gchar *accel,
382 const gchar *signal_name,
383 GArray *values)
384 {
385 g_autoptr(DzlShortcutChord) chord = NULL;
386 DzlShortcutClosureChain *chain;
387
388 g_return_if_fail (DZL_IS_SHORTCUT_CONTEXT (self));
389 g_return_if_fail (accel != NULL);
390 g_return_if_fail (signal_name != NULL);
391
392 chord = dzl_shortcut_chord_new_from_string (accel);
393
394 if (chord == NULL)
395 {
396 g_warning ("Failed to parse accelerator \"%s\"", accel);
397 return;
398 }
399
400 chain = dzl_shortcut_closure_chain_append_signalv (NULL, signal_name, values);
401
402 dzl_shortcut_context_add (self, chord, chain);
403 }
404
405 gboolean
dzl_shortcut_context_remove(DzlShortcutContext * self,const gchar * accel)406 dzl_shortcut_context_remove (DzlShortcutContext *self,
407 const gchar *accel)
408 {
409 DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self);
410 g_autoptr(DzlShortcutChord) chord = NULL;
411
412 g_return_val_if_fail (DZL_IS_SHORTCUT_CONTEXT (self), FALSE);
413 g_return_val_if_fail (accel != NULL, FALSE);
414
415 chord = dzl_shortcut_chord_new_from_string (accel);
416
417 if (chord != NULL && priv->table != NULL)
418 return dzl_shortcut_chord_table_remove (priv->table, chord);
419
420 return FALSE;
421 }
422
423 gboolean
dzl_shortcut_context_load_from_data(DzlShortcutContext * self,const gchar * data,gssize len,GError ** error)424 dzl_shortcut_context_load_from_data (DzlShortcutContext *self,
425 const gchar *data,
426 gssize len,
427 GError **error)
428 {
429 g_return_val_if_fail (DZL_IS_SHORTCUT_CONTEXT (self), FALSE);
430 g_return_val_if_fail (data != NULL, FALSE);
431
432 if (len < 0)
433 len = strlen (data);
434
435 g_set_error (error,
436 G_IO_ERROR,
437 G_IO_ERROR_INVALID_DATA,
438 "Failed to parse shortcut data");
439
440 return FALSE;
441 }
442
443 gboolean
dzl_shortcut_context_load_from_resource(DzlShortcutContext * self,const gchar * resource_path,GError ** error)444 dzl_shortcut_context_load_from_resource (DzlShortcutContext *self,
445 const gchar *resource_path,
446 GError **error)
447 {
448 g_autoptr(GBytes) bytes = NULL;
449 const gchar *endptr = NULL;
450 const gchar *data;
451 gsize len;
452
453 g_return_val_if_fail (DZL_IS_SHORTCUT_CONTEXT (self), FALSE);
454
455 if (NULL == (bytes = g_resources_lookup_data (resource_path, 0, error)))
456 return FALSE;
457
458 data = g_bytes_get_data (bytes, &len);
459
460 if (!g_utf8_validate (data, len, &endptr))
461 {
462 g_set_error (error,
463 G_IO_ERROR,
464 G_IO_ERROR_INVALID_DATA,
465 "Invalid UTF-8 at offset %u",
466 (guint)(endptr - data));
467 return FALSE;
468 }
469
470 return dzl_shortcut_context_load_from_data (self, data, len, error);
471 }
472
473 DzlShortcutChordTable *
_dzl_shortcut_context_get_table(DzlShortcutContext * self)474 _dzl_shortcut_context_get_table (DzlShortcutContext *self)
475 {
476 DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self);
477
478 g_return_val_if_fail (DZL_IS_SHORTCUT_CONTEXT (self), NULL);
479
480 return priv->table;
481 }
482
483 void
_dzl_shortcut_context_merge(DzlShortcutContext * self,DzlShortcutContext * layer)484 _dzl_shortcut_context_merge (DzlShortcutContext *self,
485 DzlShortcutContext *layer)
486 {
487 DzlShortcutContextPrivate *priv = dzl_shortcut_context_get_instance_private (self);
488 DzlShortcutContextPrivate *layer_priv = dzl_shortcut_context_get_instance_private (layer);
489 DzlShortcutChordTableIter iter;
490 const DzlShortcutChord *chord;
491 gpointer value;
492
493 g_return_if_fail (DZL_IS_SHORTCUT_CONTEXT (self));
494 g_return_if_fail (DZL_IS_SHORTCUT_CONTEXT (layer));
495 g_return_if_fail (layer != self);
496
497 if (layer_priv->use_binding_sets != -1)
498 priv->use_binding_sets = layer_priv->use_binding_sets;
499
500 _dzl_shortcut_chord_table_iter_init (&iter, layer_priv->table);
501
502 while (_dzl_shortcut_chord_table_iter_next (&iter, &chord, &value))
503 {
504 DzlShortcutClosureChain *chain = value;
505
506 /* Make sure this doesn't exist in the base layer anymore */
507 dzl_shortcut_chord_table_remove (priv->table, chord);
508
509 /* Now add it to our table of chords */
510 dzl_shortcut_context_add (self, chord, chain);
511
512 /* Now we can safely steal this from the upper layer */
513 _dzl_shortcut_chord_table_iter_steal (&iter);
514 }
515 }
516