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
85struct _GtkPadController {
86 GtkEventController parent_instance;
87 GActionGroup *action_group;
88 GdkDevice *pad;
89
90 GArray *action_entries;
91};
92
93struct _GtkPadControllerClass {
94 GtkEventControllerClass parent_class;
95};
96
97enum {
98 PROP_0,
99 PROP_ACTION_GROUP,
100 PROP_PAD,
101 N_PROPS
102};
103
104typedef struct
105{
106 GtkPadActionType type;
107 int index;
108 int mode;
109 char *label;
110 char *action_name;
111} ActionEntryData;
112
113static GParamSpec *pspecs[N_PROPS] = { NULL };
114
115G_DEFINE_TYPE (GtkPadController, gtk_pad_controller, GTK_TYPE_EVENT_CONTROLLER)
116
117static const ActionEntryData *
118gtk_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
144static void
145gtk_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
153static void
154gtk_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
163static void
164gtk_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
201static gboolean
202gtk_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
222static gboolean
223gtk_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
274static void
275gtk_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
284static void
285gtk_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
305static void
306gtk_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
326static void
327gtk_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
337static void
338gtk_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
356static void
357gtk_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
386static void
387gtk_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 */
414GtkPadController *
415gtk_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
429static int
430entry_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
447static void
448gtk_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 */
481void
482gtk_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 */
515void
516gtk_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

source code of gtk/gtk/gtkpadcontroller.c