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