1 | /* GTK - The GIMP Toolkit |
2 | * Copyright (C) 2016, Red Hat, Inc. |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General Public |
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
16 | * |
17 | * Author(s): Carlos Garnacho <carlosg@gnome.org> |
18 | */ |
19 | |
20 | /** |
21 | * GtkPadController: |
22 | * |
23 | * `GtkPadController` is an event controller for the pads found in drawing |
24 | * tablets. |
25 | * |
26 | * Pads are the collection of buttons and tactile sensors often found around |
27 | * the stylus-sensitive area. |
28 | * |
29 | * These buttons and sensors have no implicit meaning, and by default they |
30 | * perform no action. `GtkPadController` is provided to map those to |
31 | * [iface@Gio.Action] objects, thus letting the application give them a more |
32 | * semantic meaning. |
33 | * |
34 | * Buttons and sensors are not constrained to triggering a single action, |
35 | * some %GDK_SOURCE_TABLET_PAD devices feature multiple "modes". All these |
36 | * input elements have one current mode, which may determine the final action |
37 | * being triggered. |
38 | * |
39 | * Pad devices often divide buttons and sensors into groups. All elements |
40 | * in a group share the same current mode, but different groups may have |
41 | * different modes. See [method@Gdk.DevicePad.get_n_groups] and |
42 | * [method@Gdk.DevicePad.get_group_n_modes]. |
43 | * |
44 | * Each of the actions that a given button/strip/ring performs for a given mode |
45 | * is defined by a [struct@Gtk.PadActionEntry]. It contains an action name that |
46 | * will be looked up in the given [iface@Gio.ActionGroup] and activated whenever |
47 | * the specified input element and mode are triggered. |
48 | * |
49 | * A simple example of `GtkPadController` usage: Assigning button 1 in all |
50 | * modes and pad devices to an "invert-selection" action: |
51 | * |
52 | * ```c |
53 | * GtkPadActionEntry *pad_actions[] = { |
54 | * { GTK_PAD_ACTION_BUTTON, 1, -1, "Invert selection", "pad-actions.invert-selection" }, |
55 | * … |
56 | * }; |
57 | * |
58 | * … |
59 | * action_group = g_simple_action_group_new (); |
60 | * action = g_simple_action_new ("pad-actions.invert-selection", NULL); |
61 | * g_signal_connect (action, "activate", on_invert_selection_activated, NULL); |
62 | * g_action_map_add_action (G_ACTION_MAP (action_group), action); |
63 | * … |
64 | * pad_controller = gtk_pad_controller_new (action_group, NULL); |
65 | * ``` |
66 | * |
67 | * The actions belonging to rings/strips will be activated with a parameter |
68 | * of type %G_VARIANT_TYPE_DOUBLE bearing the value of the given axis, it |
69 | * is required that those are made stateful and accepting this `GVariantType`. |
70 | */ |
71 | |
72 | #include "config.h" |
73 | |
74 | #include "gtkeventcontrollerprivate.h" |
75 | #include "gtkpadcontroller.h" |
76 | #include "gtkwindow.h" |
77 | #include "gtkprivate.h" |
78 | #include "gtkintl.h" |
79 | |
80 | #ifdef GDK_WINDOWING_WAYLAND |
81 | #include <gdk/wayland/gdkwayland.h> |
82 | #include "gdk/wayland/gdkdevice-wayland-private.h" |
83 | #endif |
84 | |
85 | struct _GtkPadController { |
86 | GtkEventController parent_instance; |
87 | GActionGroup *action_group; |
88 | GdkDevice *pad; |
89 | |
90 | GArray *action_entries; |
91 | }; |
92 | |
93 | struct _GtkPadControllerClass { |
94 | GtkEventControllerClass parent_class; |
95 | }; |
96 | |
97 | enum { |
98 | PROP_0, |
99 | PROP_ACTION_GROUP, |
100 | PROP_PAD, |
101 | N_PROPS |
102 | }; |
103 | |
104 | typedef struct |
105 | { |
106 | GtkPadActionType type; |
107 | int index; |
108 | int mode; |
109 | char *label; |
110 | char *action_name; |
111 | } ActionEntryData; |
112 | |
113 | static GParamSpec *pspecs[N_PROPS] = { NULL }; |
114 | |
115 | G_DEFINE_TYPE (GtkPadController, gtk_pad_controller, GTK_TYPE_EVENT_CONTROLLER) |
116 | |
117 | static const ActionEntryData * |
118 | gtk_pad_action_find_match (GtkPadController *controller, |
119 | GtkPadActionType type, |
120 | int index, |
121 | int mode) |
122 | { |
123 | guint i; |
124 | |
125 | for (i = 0; i < controller->action_entries->len; i++) |
126 | { |
127 | const ActionEntryData *entry = &g_array_index (controller->action_entries, |
128 | ActionEntryData, i); |
129 | gboolean match_index = FALSE, match_mode = FALSE; |
130 | |
131 | if (entry->type != type) |
132 | continue; |
133 | |
134 | match_index = entry->index < 0 || entry->index == index; |
135 | match_mode = entry->mode < 0 || entry->mode == mode; |
136 | |
137 | if (match_index && match_mode) |
138 | return entry; |
139 | } |
140 | |
141 | return NULL; |
142 | } |
143 | |
144 | static void |
145 | gtk_pad_controller_activate_action (GtkPadController *controller, |
146 | const ActionEntryData *entry) |
147 | { |
148 | g_action_group_activate_action (action_group: controller->action_group, |
149 | action_name: entry->action_name, |
150 | NULL); |
151 | } |
152 | |
153 | static void |
154 | gtk_pad_controller_activate_action_with_axis (GtkPadController *controller, |
155 | const ActionEntryData *entry, |
156 | double value) |
157 | { |
158 | g_action_group_activate_action (action_group: controller->action_group, |
159 | action_name: entry->action_name, |
160 | parameter: g_variant_new_double (value)); |
161 | } |
162 | |
163 | static void |
164 | gtk_pad_controller_handle_mode_switch (GtkPadController *controller, |
165 | GdkDevice *pad, |
166 | guint group, |
167 | guint mode) |
168 | { |
169 | #ifdef GDK_WINDOWING_WAYLAND |
170 | if (GDK_IS_WAYLAND_DISPLAY (gdk_device_get_display (pad))) |
171 | { |
172 | const ActionEntryData *entry; |
173 | int elem, idx, n_features; |
174 | |
175 | for (elem = GTK_PAD_ACTION_BUTTON; elem <= GTK_PAD_ACTION_STRIP; elem++) |
176 | { |
177 | n_features = gdk_device_pad_get_n_features (GDK_DEVICE_PAD (pad), |
178 | feature: elem); |
179 | |
180 | for (idx = 0; idx < n_features; idx++) |
181 | { |
182 | if (gdk_device_pad_get_feature_group (GDK_DEVICE_PAD (pad), |
183 | feature: elem, feature_idx: idx) != group) |
184 | continue; |
185 | |
186 | entry = gtk_pad_action_find_match (controller, type: elem, index: idx, mode); |
187 | if (!entry) |
188 | continue; |
189 | if (!g_action_group_has_action (action_group: controller->action_group, |
190 | action_name: entry->action_name)) |
191 | continue; |
192 | |
193 | gdk_wayland_device_pad_set_feedback (device: pad, feature: elem, feature_idx: idx, |
194 | label: g_dgettext (NULL, msgid: entry->label)); |
195 | } |
196 | } |
197 | } |
198 | #endif |
199 | } |
200 | |
201 | static gboolean |
202 | gtk_pad_controller_filter_event (GtkEventController *controller, |
203 | GdkEvent *event) |
204 | { |
205 | GtkPadController *pad_controller = GTK_PAD_CONTROLLER (controller); |
206 | GdkEventType event_type = gdk_event_get_event_type (event); |
207 | |
208 | if (event_type != GDK_PAD_BUTTON_PRESS && |
209 | event_type != GDK_PAD_BUTTON_RELEASE && |
210 | event_type != GDK_PAD_RING && |
211 | event_type != GDK_PAD_STRIP && |
212 | event_type != GDK_PAD_GROUP_MODE) |
213 | return TRUE; |
214 | |
215 | if (pad_controller->pad && |
216 | gdk_event_get_device (event) != pad_controller->pad) |
217 | return TRUE; |
218 | |
219 | return FALSE; |
220 | } |
221 | |
222 | static gboolean |
223 | gtk_pad_controller_handle_event (GtkEventController *controller, |
224 | GdkEvent *event, |
225 | double x, |
226 | double y) |
227 | { |
228 | GtkPadController *pad_controller = GTK_PAD_CONTROLLER (controller); |
229 | GdkEventType event_type = gdk_event_get_event_type (event); |
230 | const ActionEntryData *entry; |
231 | GtkPadActionType type; |
232 | guint index, mode, group; |
233 | double value = 0; |
234 | |
235 | gdk_pad_event_get_group_mode (event, group: &group, mode: &mode); |
236 | if (event_type == GDK_PAD_GROUP_MODE) |
237 | { |
238 | gtk_pad_controller_handle_mode_switch (controller: pad_controller, |
239 | pad: gdk_event_get_device (event), |
240 | group, |
241 | mode); |
242 | return GDK_EVENT_PROPAGATE; |
243 | } |
244 | |
245 | switch ((guint) event_type) |
246 | { |
247 | case GDK_PAD_BUTTON_PRESS: |
248 | type = GTK_PAD_ACTION_BUTTON; |
249 | index = gdk_pad_event_get_button (event); |
250 | break; |
251 | case GDK_PAD_RING: |
252 | case GDK_PAD_STRIP: |
253 | type = event_type == GDK_PAD_RING ? |
254 | GTK_PAD_ACTION_RING : GTK_PAD_ACTION_STRIP; |
255 | gdk_pad_event_get_axis_value (event, index: &index, value: &value); |
256 | break; |
257 | default: |
258 | return GDK_EVENT_PROPAGATE; |
259 | } |
260 | |
261 | entry = gtk_pad_action_find_match (controller: pad_controller, |
262 | type, index, mode); |
263 | if (!entry) |
264 | return GDK_EVENT_PROPAGATE; |
265 | |
266 | if (event_type == GDK_PAD_RING || event_type == GDK_PAD_STRIP) |
267 | gtk_pad_controller_activate_action_with_axis (controller: pad_controller, entry, value); |
268 | else |
269 | gtk_pad_controller_activate_action (controller: pad_controller, entry); |
270 | |
271 | return GDK_EVENT_STOP; |
272 | } |
273 | |
274 | static void |
275 | gtk_pad_controller_set_pad (GtkPadController *controller, |
276 | GdkDevice *pad) |
277 | { |
278 | g_return_if_fail (!pad || GDK_IS_DEVICE (pad)); |
279 | g_return_if_fail (!pad || gdk_device_get_source (pad) == GDK_SOURCE_TABLET_PAD); |
280 | |
281 | g_set_object (&controller->pad, pad); |
282 | } |
283 | |
284 | static void |
285 | gtk_pad_controller_set_property (GObject *object, |
286 | guint prop_id, |
287 | const GValue *value, |
288 | GParamSpec *pspec) |
289 | { |
290 | GtkPadController *controller = GTK_PAD_CONTROLLER (object); |
291 | |
292 | switch (prop_id) |
293 | { |
294 | case PROP_ACTION_GROUP: |
295 | controller->action_group = g_value_dup_object (value); |
296 | break; |
297 | case PROP_PAD: |
298 | gtk_pad_controller_set_pad (controller, pad: g_value_get_object (value)); |
299 | break; |
300 | default: |
301 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
302 | } |
303 | } |
304 | |
305 | static void |
306 | gtk_pad_controller_get_property (GObject *object, |
307 | guint prop_id, |
308 | GValue *value, |
309 | GParamSpec *pspec) |
310 | { |
311 | GtkPadController *controller = GTK_PAD_CONTROLLER (object); |
312 | |
313 | switch (prop_id) |
314 | { |
315 | case PROP_ACTION_GROUP: |
316 | g_value_set_object (value, v_object: controller->action_group); |
317 | break; |
318 | case PROP_PAD: |
319 | g_value_set_object (value, v_object: controller->pad); |
320 | break; |
321 | default: |
322 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
323 | } |
324 | } |
325 | |
326 | static void |
327 | gtk_pad_controller_dispose (GObject *object) |
328 | { |
329 | GtkPadController *controller = GTK_PAD_CONTROLLER (object); |
330 | |
331 | g_clear_object (&controller->action_group); |
332 | g_clear_object (&controller->pad); |
333 | |
334 | G_OBJECT_CLASS (gtk_pad_controller_parent_class)->dispose (object); |
335 | } |
336 | |
337 | static void |
338 | gtk_pad_controller_finalize (GObject *object) |
339 | { |
340 | GtkPadController *controller = GTK_PAD_CONTROLLER (object); |
341 | guint i; |
342 | |
343 | for (i = 0; i < controller->action_entries->len; i++) |
344 | { |
345 | const ActionEntryData *entry = &g_array_index (controller->action_entries, |
346 | ActionEntryData, i); |
347 | |
348 | g_free (mem: entry->label); |
349 | g_free (mem: entry->action_name); |
350 | } |
351 | g_array_free (array: controller->action_entries, TRUE); |
352 | |
353 | G_OBJECT_CLASS (gtk_pad_controller_parent_class)->finalize (object); |
354 | } |
355 | |
356 | static void |
357 | gtk_pad_controller_class_init (GtkPadControllerClass *klass) |
358 | { |
359 | GObjectClass *object_class = G_OBJECT_CLASS (klass); |
360 | GtkEventControllerClass *controller_class = GTK_EVENT_CONTROLLER_CLASS (klass); |
361 | |
362 | controller_class->filter_event = gtk_pad_controller_filter_event; |
363 | controller_class->handle_event = gtk_pad_controller_handle_event; |
364 | |
365 | object_class->set_property = gtk_pad_controller_set_property; |
366 | object_class->get_property = gtk_pad_controller_get_property; |
367 | object_class->dispose = gtk_pad_controller_dispose; |
368 | object_class->finalize = gtk_pad_controller_finalize; |
369 | |
370 | pspecs[PROP_ACTION_GROUP] = |
371 | g_param_spec_object (name: "action-group" , |
372 | P_("Action group" ), |
373 | P_("Action group to launch actions from" ), |
374 | G_TYPE_ACTION_GROUP, |
375 | GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); |
376 | pspecs[PROP_PAD] = |
377 | g_param_spec_object (name: "pad" , |
378 | P_("Pad device" ), |
379 | P_("Pad device to control" ), |
380 | GDK_TYPE_DEVICE, |
381 | GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); |
382 | |
383 | g_object_class_install_properties (oclass: object_class, n_pspecs: N_PROPS, pspecs); |
384 | } |
385 | |
386 | static void |
387 | gtk_pad_controller_init (GtkPadController *controller) |
388 | { |
389 | controller->action_entries = g_array_new (FALSE, TRUE, element_size: sizeof (ActionEntryData)); |
390 | } |
391 | |
392 | /** |
393 | * gtk_pad_controller_new: |
394 | * @group: `GActionGroup` to trigger actions from |
395 | * @pad: (nullable): A %GDK_SOURCE_TABLET_PAD device, or %NULL to handle all pads |
396 | * |
397 | * Creates a new `GtkPadController` that will associate events from @pad to |
398 | * actions. |
399 | * |
400 | * A %NULL pad may be provided so the controller manages all pad devices |
401 | * generically, it is discouraged to mix `GtkPadController` objects with |
402 | * %NULL and non-%NULL @pad argument on the same toplevel window, as execution |
403 | * order is not guaranteed. |
404 | * |
405 | * The `GtkPadController` is created with no mapped actions. In order to |
406 | * map pad events to actions, use [method@Gtk.PadController.set_action_entries] |
407 | * or [method@Gtk.PadController.set_action]. |
408 | * |
409 | * Be aware that pad events will only be delivered to `GtkWindow`s, so adding |
410 | * a pad controller to any other type of widget will not have an effect. |
411 | * |
412 | * Returns: A newly created `GtkPadController` |
413 | */ |
414 | GtkPadController * |
415 | gtk_pad_controller_new (GActionGroup *group, |
416 | GdkDevice *pad) |
417 | { |
418 | g_return_val_if_fail (G_IS_ACTION_GROUP (group), NULL); |
419 | g_return_val_if_fail (!pad || GDK_IS_DEVICE (pad), NULL); |
420 | g_return_val_if_fail (!pad || gdk_device_get_source (pad) == GDK_SOURCE_TABLET_PAD, NULL); |
421 | |
422 | return g_object_new (GTK_TYPE_PAD_CONTROLLER, |
423 | first_property_name: "propagation-phase" , GTK_PHASE_CAPTURE, |
424 | "action-group" , group, |
425 | "pad" , pad, |
426 | NULL); |
427 | } |
428 | |
429 | static int |
430 | entry_compare_func (gconstpointer a, |
431 | gconstpointer b) |
432 | { |
433 | const GtkPadActionEntry *entry1 = a, *entry2 = b; |
434 | |
435 | if (entry1->mode > entry2->mode) |
436 | return -1; |
437 | else if (entry1->mode < entry2->mode) |
438 | return 1; |
439 | else if (entry1->index > entry2->index) |
440 | return -1; |
441 | else if (entry1->index < entry2->index) |
442 | return 1; |
443 | |
444 | return 0; |
445 | } |
446 | |
447 | static void |
448 | gtk_pad_controller_add_entry (GtkPadController *controller, |
449 | const GtkPadActionEntry *entry) |
450 | { |
451 | guint i; |
452 | const ActionEntryData new_entry = { |
453 | .type = entry->type, |
454 | .index = entry->index, |
455 | .mode = entry->mode, |
456 | .label= g_strdup (str: entry->label), |
457 | .action_name = g_strdup (str: entry->action_name) |
458 | }; |
459 | |
460 | for (i = 0; i < controller->action_entries->len; i++) |
461 | { |
462 | if (entry_compare_func (a: &new_entry, |
463 | b: &g_array_index (controller->action_entries, ActionEntryData, i)) == 0) |
464 | break; |
465 | } |
466 | |
467 | g_array_insert_val (controller->action_entries, i, new_entry); |
468 | } |
469 | |
470 | /** |
471 | * gtk_pad_controller_set_action_entries: |
472 | * @controller: a `GtkPadController` |
473 | * @entries: (array length=n_entries): the action entries to set on @controller |
474 | * @n_entries: the number of elements in @entries |
475 | * |
476 | * A convenience function to add a group of action entries on |
477 | * @controller. |
478 | * |
479 | * See [struct@Gtk.PadActionEntry] and [method@Gtk.PadController.set_action]. |
480 | */ |
481 | void |
482 | gtk_pad_controller_set_action_entries (GtkPadController *controller, |
483 | const GtkPadActionEntry *entries, |
484 | int n_entries) |
485 | { |
486 | int i; |
487 | |
488 | g_return_if_fail (GTK_IS_PAD_CONTROLLER (controller)); |
489 | g_return_if_fail (entries != NULL); |
490 | |
491 | for (i = 0; i < n_entries; i++) |
492 | gtk_pad_controller_add_entry (controller, entry: &entries[i]); |
493 | } |
494 | |
495 | /** |
496 | * gtk_pad_controller_set_action: |
497 | * @controller: a `GtkPadController` |
498 | * @type: the type of pad feature that will trigger this action |
499 | * @index: the 0-indexed button/ring/strip number that will trigger this action |
500 | * @mode: the mode that will trigger this action, or -1 for all modes. |
501 | * @label: Human readable description of this action, this string should |
502 | * be deemed user-visible. |
503 | * @action_name: action name that will be activated in the `GActionGroup` |
504 | * |
505 | * Adds an individual action to @controller. |
506 | * |
507 | * This action will only be activated if the given button/ring/strip number |
508 | * in @index is interacted while the current mode is @mode. -1 may be used |
509 | * for simple cases, so the action is triggered on all modes. |
510 | * |
511 | * The given @label should be considered user-visible, so internationalization |
512 | * rules apply. Some windowing systems may be able to use those for user |
513 | * feedback. |
514 | */ |
515 | void |
516 | gtk_pad_controller_set_action (GtkPadController *controller, |
517 | GtkPadActionType type, |
518 | int index, |
519 | int mode, |
520 | const char *label, |
521 | const char *action_name) |
522 | { |
523 | const GtkPadActionEntry entry = { type, index, mode, label, action_name }; |
524 | |
525 | g_return_if_fail (GTK_IS_PAD_CONTROLLER (controller)); |
526 | g_return_if_fail (type <= GTK_PAD_ACTION_STRIP); |
527 | |
528 | gtk_pad_controller_add_entry (controller, entry: &entry); |
529 | } |
530 | |