From 63cc4da4f97a81b7676658f0876b933ae661657e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Dre=C3=9Fler?= Date: Fri, 8 Apr 2022 20:21:42 +0200 Subject: [PATCH] clutter/gesture: Cancel other gestures when moving to RECOGNIZING Quite often there are situations where multiple gestures try to recognize, keeping track of the same set of points (for example an edge drag gesture on the stage and a click gesture somewhere in the scenegraph). Usually what's wanted here is that the first gesture to move to RECOGNIZING wins over all other active gestures and "claims" the point for itself. We implement this by introducing a concept called "influencing". It works by making all gestures operating on a shared set of points aware of each other using ClutterAction->register_sequence(). ClutterGesture uses this vfunc to keep track of all other ClutterGestures that are potentially conflicting, and keeps a list (priv->cancel_on_recognizing) of those. As soon as the move to RECOGNIZING happens, all gestures inside this list get moved to CANCELLED. To allow fine-grained control over this behavior, two APIs are introduced: 1) on the implementation level (should_influence() and should_be_influenced_by()): This is a vfunc that gets called as soon as a potential conflict is detected. It's helpful when a specific gesture always behaves the same towards another gesture, for example to make sure a LongPress gesture never cancels a DragGesture. 2) on the gesture user level, clutter_gesture_can_not_cancel() is introduced: This allows control for the user of a gesture to specify that a specific instance of a gesture won't cancel another gesture. Calling this twice so that both gestures can't cancel each other allows for things like simultaneous recognition of a pinch-to-zoom and rotate gesture. Part-of: --- clutter/clutter/clutter-action-private.h | 8 + clutter/clutter/clutter-action.c | 26 ++ clutter/clutter/clutter-action.h | 8 + clutter/clutter/clutter-gesture.c | 351 ++++++++++++++++++++--- clutter/clutter/clutter-gesture.h | 18 ++ clutter/clutter/clutter-stage.c | 42 +++ 6 files changed, 406 insertions(+), 47 deletions(-) diff --git a/clutter/clutter/clutter-action-private.h b/clutter/clutter/clutter-action-private.h index 7490bd5a2..bcb969e2f 100644 --- a/clutter/clutter/clutter-action-private.h +++ b/clutter/clutter/clutter-action-private.h @@ -42,4 +42,12 @@ void clutter_action_sequence_cancelled (ClutterAction *action, ClutterInputDevice *device, ClutterEventSequence *sequence); +gboolean clutter_action_register_sequence (ClutterAction *self, + const ClutterEvent *event); + +int clutter_action_setup_sequence_relationship (ClutterAction *action_1, + ClutterAction *action_2, + ClutterInputDevice *device, + ClutterEventSequence *sequence); + G_END_DECLS diff --git a/clutter/clutter/clutter-action.c b/clutter/clutter/clutter-action.c index 4244721c6..62148dea7 100644 --- a/clutter/clutter/clutter-action.c +++ b/clutter/clutter/clutter-action.c @@ -118,3 +118,29 @@ clutter_action_sequence_cancelled (ClutterAction *action, if (action_class->sequence_cancelled) action_class->sequence_cancelled (action, device, sequence); } + +gboolean +clutter_action_register_sequence (ClutterAction *self, + const ClutterEvent *event) +{ + ClutterActionClass *action_class = CLUTTER_ACTION_GET_CLASS (self); + + if (action_class->register_sequence) + return action_class->register_sequence (self, event); + + return TRUE; +} + +int +clutter_action_setup_sequence_relationship (ClutterAction *action_1, + ClutterAction *action_2, + ClutterInputDevice *device, + ClutterEventSequence *sequence) +{ + ClutterActionClass *action_class = CLUTTER_ACTION_GET_CLASS (action_1); + + if (action_class->setup_sequence_relationship) + return action_class->setup_sequence_relationship (action_1, action_2, device, sequence); + + return 0; +} diff --git a/clutter/clutter/clutter-action.h b/clutter/clutter/clutter-action.h index 5c155292e..dd5d59011 100644 --- a/clutter/clutter/clutter-action.h +++ b/clutter/clutter/clutter-action.h @@ -54,6 +54,14 @@ struct _ClutterActionClass void (* sequence_cancelled) (ClutterAction *action, ClutterInputDevice *device, ClutterEventSequence *sequence); + + gboolean (* register_sequence) (ClutterAction *self, + const ClutterEvent *event); + + int (* setup_sequence_relationship) (ClutterAction *action_1, + ClutterAction *action_2, + ClutterInputDevice *device, + ClutterEventSequence *sequence); }; /* ClutterActor API */ diff --git a/clutter/clutter/clutter-gesture.c b/clutter/clutter/clutter-gesture.c index 78bf85233..44d3fca52 100644 --- a/clutter/clutter/clutter-gesture.c +++ b/clutter/clutter/clutter-gesture.c @@ -70,6 +70,15 @@ * (and immediately) enter the requested state. To deal with this, never * assume the state has changed after calling clutter_gesture_set_state(), * and react to state changes by implementing the state_changed() vfunc. + * + * ## Relationships of gestures + * + * By default, when multiple gestures try to recognize while sharing one or + * more points, the first gesture to move to RECOGNIZING wins, and implicitly + * moves all conflicting gestures to state CANCELLED. This behavior can be + * prohibited by using the clutter_gesture_can_not_cancel() API or by + * implementing the should_influence() or should_be_influenced_by() vfuncs + * in your #ClutterGesture subclass. */ #include "config.h" @@ -116,6 +125,12 @@ struct _ClutterGesturePrivate unsigned int latest_index; ClutterGestureState state; + + GHashTable *in_relationship_with; + + GPtrArray *cancel_on_recognizing; + + GHashTable *can_not_cancel; }; enum @@ -477,7 +492,27 @@ set_state (ClutterGesture *self, } if (new_state == CLUTTER_GESTURE_STATE_WAITING) - g_array_set_size (priv->sequences, 0); + { + GHashTableIter iter; + ClutterGesture *other_gesture; + + g_array_set_size (priv->sequences, 0); + + g_hash_table_iter_init (&iter, priv->in_relationship_with); + while (g_hash_table_iter_next (&iter, (gpointer *) &other_gesture, NULL)) + { + ClutterGesturePrivate *other_priv = + clutter_gesture_get_instance_private (other_gesture); + gboolean removed; + + removed = g_hash_table_remove (other_priv->in_relationship_with, self); + g_assert (removed); + + g_hash_table_iter_remove (&iter); + } + + g_ptr_array_set_size (priv->cancel_on_recognizing, 0); + } priv->state = new_state; @@ -524,11 +559,50 @@ maybe_move_to_waiting (ClutterGesture *self) set_state (self, CLUTTER_GESTURE_STATE_WAITING); } +static void +maybe_influence_other_gestures (ClutterGesture *self) +{ + ClutterGesturePrivate *priv = clutter_gesture_get_instance_private (self); + + if (priv->state == CLUTTER_GESTURE_STATE_RECOGNIZING || + priv->state == CLUTTER_GESTURE_STATE_COMPLETED) + { + unsigned int i; + + for (i = 0; i < priv->cancel_on_recognizing->len; i++) + { + ClutterGesture *other_gesture = priv->cancel_on_recognizing->pdata[i]; + ClutterGesturePrivate *other_priv = + clutter_gesture_get_instance_private (other_gesture); + + if (!g_hash_table_contains (priv->in_relationship_with, other_gesture)) + continue; + + g_assert (other_priv->state != CLUTTER_GESTURE_STATE_WAITING); + + if (other_priv->state == CLUTTER_GESTURE_STATE_CANCELLED || + other_priv->state == CLUTTER_GESTURE_STATE_COMPLETED) + continue; + + set_state (other_gesture, CLUTTER_GESTURE_STATE_CANCELLED); + maybe_move_to_waiting (other_gesture); + } + } +} + void set_state_authoritative (ClutterGesture *self, ClutterGestureState new_state) { + ClutterGesturePrivate *priv = clutter_gesture_get_instance_private (self); + ClutterGestureState old_state = priv->state; + set_state (self, new_state); + + if (priv->state == CLUTTER_GESTURE_STATE_RECOGNIZING || + (old_state != CLUTTER_GESTURE_STATE_RECOGNIZING && + priv->state == CLUTTER_GESTURE_STATE_COMPLETED)) + maybe_influence_other_gestures (self); maybe_move_to_waiting (self); } @@ -656,52 +730,6 @@ clutter_gesture_handle_event (ClutterAction *action, if (clutter_event_get_flags (event) & CLUTTER_EVENT_FLAG_SYNTHETIC) return CLUTTER_EVENT_PROPAGATE; - if (priv->state == CLUTTER_GESTURE_STATE_CANCELLED || - priv->state == CLUTTER_GESTURE_STATE_COMPLETED) - return CLUTTER_EVENT_PROPAGATE; - - if (event_type == CLUTTER_BUTTON_PRESS || - event_type == CLUTTER_TOUCH_BEGIN) - { - ClutterInputDevice *source_device = - clutter_event_get_source_device (event); - gboolean retval; - - if (priv->sequences->len > 0) - { - unsigned int i; - - for (i = 0; i < priv->sequences->len; i++) - { - GestureSequenceData *iter = &g_array_index (priv->sequences, GestureSequenceData, i); - ClutterInputDevice *iter_source_device; - - if (iter->ended) - continue; - - iter_source_device = clutter_event_get_source_device (iter->begin_event); - - if (iter_source_device != source_device) - return CLUTTER_EVENT_PROPAGATE; - - break; - } - } - - g_signal_emit (self, obj_signals[SHOULD_HANDLE_SEQUENCE], 0, - event, &retval); - if (!retval) - return CLUTTER_EVENT_PROPAGATE; - - if (priv->state == CLUTTER_GESTURE_STATE_WAITING) - { - set_state_authoritative (self, CLUTTER_GESTURE_STATE_POSSIBLE); - g_assert (priv->state == CLUTTER_GESTURE_STATE_POSSIBLE); - } - - register_sequence (self, event); - } - if ((seq_data = get_sequence_data (self, device, sequence, &seq_index)) == NULL) return CLUTTER_EVENT_PROPAGATE; @@ -799,6 +827,11 @@ clutter_gesture_handle_event (ClutterAction *action, if (stage) clutter_stage_notify_action_implicit_grab (stage, device, sequence); } + + debug_message (self, + "Cancelling other gestures on newly added point automatically"); + + maybe_influence_other_gestures (self); } return CLUTTER_EVENT_PROPAGATE; @@ -812,6 +845,156 @@ clutter_gesture_sequence_cancelled (ClutterAction *action, cancel_point (CLUTTER_GESTURE (action), device, sequence); } +static gboolean +clutter_gesture_register_sequence (ClutterAction *action, + const ClutterEvent *sequence_begin_event) +{ + ClutterGesture *self = CLUTTER_GESTURE (action); + ClutterGesturePrivate *priv = clutter_gesture_get_instance_private (self); + ClutterInputDevice *source_device = clutter_event_get_source_device (sequence_begin_event); + gboolean retval; + + if (priv->state == CLUTTER_GESTURE_STATE_CANCELLED || + priv->state == CLUTTER_GESTURE_STATE_COMPLETED) + return FALSE; + + if (priv->sequences->len > 0) + { + unsigned int i; + + for (i = 0; i < priv->sequences->len; i++) + { + GestureSequenceData *iter = &g_array_index (priv->sequences, GestureSequenceData, i); + ClutterInputDevice *iter_source_device; + + if (iter->ended) + continue; + + iter_source_device = clutter_event_get_source_device (iter->begin_event); + + if (iter_source_device != source_device) + return FALSE; + + break; + } + } + + g_signal_emit (self, obj_signals[SHOULD_HANDLE_SEQUENCE], 0, + sequence_begin_event, &retval); + if (!retval) + return FALSE; + + if (priv->state == CLUTTER_GESTURE_STATE_WAITING) + { + set_state_authoritative (self, CLUTTER_GESTURE_STATE_POSSIBLE); + g_assert (priv->state == CLUTTER_GESTURE_STATE_POSSIBLE); + } + + register_sequence (self, sequence_begin_event); + + return TRUE; +} + +static void +setup_influence_on_other_gesture (ClutterGesture *self, + ClutterGesture *other_gesture, + gboolean *cancel_other_gesture_on_recognizing) +{ + ClutterGesturePrivate *priv = clutter_gesture_get_instance_private (self); + ClutterGestureClass *gesture_class = CLUTTER_GESTURE_GET_CLASS (self); + ClutterGestureClass *other_gesture_class = CLUTTER_GESTURE_GET_CLASS (other_gesture); + + /* The default: We cancel other gestures when we recognize */ + gboolean cancel = TRUE; + + /* First check with the implementation specific APIs */ + if (gesture_class->should_influence) + gesture_class->should_influence (self, other_gesture, &cancel); + + if (other_gesture_class->should_be_influenced_by) + other_gesture_class->should_be_influenced_by (other_gesture, self, &cancel); + + /* Then apply overrides made using the public methods */ + if (priv->can_not_cancel && + g_hash_table_contains (priv->can_not_cancel, other_gesture)) + cancel = FALSE; + + *cancel_other_gesture_on_recognizing = cancel; +} + +static int +clutter_gesture_setup_sequence_relationship (ClutterAction *action_1, + ClutterAction *action_2, + ClutterInputDevice *device, + ClutterEventSequence *sequence) +{ + if (!CLUTTER_IS_GESTURE (action_1) || !CLUTTER_IS_GESTURE (action_2)) + return 0; + + ClutterGesture *gesture_1 = CLUTTER_GESTURE (action_1); + ClutterGesture *gesture_2 = CLUTTER_GESTURE (action_2); + ClutterGesturePrivate *priv_1 = clutter_gesture_get_instance_private (gesture_1); + ClutterGesturePrivate *priv_2 = clutter_gesture_get_instance_private (gesture_2); + gboolean cancel_1_on_recognizing; + gboolean cancel_2_on_recognizing; + + /* When CANCELLED or COMPLETED, we refuse to accept new points in + * register_sequence(). Also when WAITING it's impossible to have points, + * that leaves only two states, POSSIBLE and RECOGNIZING. + */ + g_assert (priv_1->state == CLUTTER_GESTURE_STATE_POSSIBLE || + priv_1->state == CLUTTER_GESTURE_STATE_RECOGNIZING); + g_assert (priv_2->state == CLUTTER_GESTURE_STATE_POSSIBLE || + priv_2->state == CLUTTER_GESTURE_STATE_RECOGNIZING); + + g_assert (get_sequence_data (gesture_1, device, sequence, NULL) != NULL && + get_sequence_data (gesture_2, device, sequence, NULL) != NULL); + + /* If gesture 1 knows gesture 2 (this implies vice-versa), everything's + * figured out already, we won't negotiate again for any new shared sequences! + */ + if (g_hash_table_contains (priv_1->in_relationship_with, gesture_2)) + { + cancel_1_on_recognizing = g_ptr_array_find (priv_2->cancel_on_recognizing, gesture_1, NULL); + cancel_2_on_recognizing = g_ptr_array_find (priv_1->cancel_on_recognizing, gesture_2, NULL); + } + else + { + setup_influence_on_other_gesture (gesture_1, gesture_2, + &cancel_2_on_recognizing); + + setup_influence_on_other_gesture (gesture_2, gesture_1, + &cancel_1_on_recognizing); + + CLUTTER_NOTE (GESTURES, + "Setting up relation between \"<%s> [<%s>:%p]\" (cancel: %d) " + "and \"<%s> [<%s>:%p]\" (cancel: %d)", + clutter_actor_meta_get_name (CLUTTER_ACTOR_META (gesture_1)), + G_OBJECT_TYPE_NAME (gesture_1), gesture_1, + cancel_1_on_recognizing, + clutter_actor_meta_get_name (CLUTTER_ACTOR_META (gesture_2)), + G_OBJECT_TYPE_NAME (gesture_2), gesture_2, + cancel_2_on_recognizing); + + g_hash_table_add (priv_1->in_relationship_with, g_object_ref (gesture_2)); + g_hash_table_add (priv_2->in_relationship_with, g_object_ref (gesture_1)); + + if (cancel_2_on_recognizing) + g_ptr_array_add (priv_1->cancel_on_recognizing, gesture_2); + + if (cancel_1_on_recognizing) + g_ptr_array_add (priv_2->cancel_on_recognizing, gesture_1); + } + + if (cancel_2_on_recognizing && !cancel_1_on_recognizing) + return -1; + + if (!cancel_2_on_recognizing && cancel_1_on_recognizing) + return 1; + + return 0; +} + static void clutter_gesture_set_actor (ClutterActorMeta *meta, ClutterActor *actor) @@ -869,6 +1052,28 @@ clutter_gesture_get_property (GObject *gobject, } } +static void +other_gesture_disposed (gpointer user_data, + GObject *finalized_gesture) +{ + GHashTable *hashtable = user_data; + + g_hash_table_remove (hashtable, finalized_gesture); +} + +static void +destroy_weak_ref_hashtable (GHashTable *hashtable) +{ + GHashTableIter iter; + GObject *key; + + g_hash_table_iter_init (&iter, hashtable); + while (g_hash_table_iter_next (&iter, (gpointer *) &key, NULL)) + g_object_weak_unref (key, other_gesture_disposed, hashtable); + + g_hash_table_destroy (hashtable); +} + static void clutter_gesture_finalize (GObject *gobject) { @@ -889,6 +1094,15 @@ clutter_gesture_finalize (GObject *gobject) g_array_unref (priv->sequences); + g_assert (g_hash_table_size (priv->in_relationship_with) == 0); + g_hash_table_destroy (priv->in_relationship_with); + + g_assert (priv->cancel_on_recognizing->len == 0); + g_ptr_array_free (priv->cancel_on_recognizing, TRUE); + + if (priv->can_not_cancel) + destroy_weak_ref_hashtable (priv->can_not_cancel); + G_OBJECT_CLASS (clutter_gesture_parent_class)->finalize (gobject); } @@ -906,6 +1120,8 @@ clutter_gesture_class_init (ClutterGestureClass *klass) action_class->handle_event = clutter_gesture_handle_event; action_class->sequence_cancelled = clutter_gesture_sequence_cancelled; + action_class->register_sequence = clutter_gesture_register_sequence; + action_class->setup_sequence_relationship = clutter_gesture_setup_sequence_relationship; meta_class->set_actor = clutter_gesture_set_actor; meta_class->set_enabled = clutter_gesture_set_enabled; @@ -1054,6 +1270,11 @@ clutter_gesture_init (ClutterGesture *self) priv->state = CLUTTER_GESTURE_STATE_WAITING; + priv->in_relationship_with = g_hash_table_new_full (NULL, NULL, (GDestroyNotify) g_object_unref, NULL); + + priv->cancel_on_recognizing = g_ptr_array_new (); + + priv->can_not_cancel = NULL; } /** @@ -1420,3 +1641,39 @@ clutter_gesture_get_point_event (ClutterGesture *self, return seq_data->latest_event; } + +/** + * clutter_gesture_can_not_cancel: + * @self: a #ClutterGesture + * @other_gesture: the other #ClutterGesture + * + * In case @self and @other_gesture are operating on the same points, calling + * this function will make sure that @self does not cancel @other_gesture + * when @self moves to state RECOGNIZING. + * + * To allow two gestures to recognize simultaneously using the same set of + * points (for example a zoom and a rotate gesture on the same actor), call + * clutter_gesture_can_not_cancel() twice, so that both gestures can not + * cancel each other. + */ +void +clutter_gesture_can_not_cancel (ClutterGesture *self, + ClutterGesture *other_gesture) +{ + ClutterGesturePrivate *priv; + + g_return_if_fail (CLUTTER_IS_GESTURE (self)); + g_return_if_fail (CLUTTER_IS_GESTURE (other_gesture)); + + priv = clutter_gesture_get_instance_private (self); + + if (!priv->can_not_cancel) + priv->can_not_cancel = g_hash_table_new (NULL, NULL); + + if (!g_hash_table_add (priv->can_not_cancel, other_gesture)) + return; + + g_object_weak_ref (G_OBJECT (other_gesture), + (GWeakNotify) other_gesture_disposed, + priv->can_not_cancel); +} diff --git a/clutter/clutter/clutter-gesture.h b/clutter/clutter/clutter-gesture.h index d11f9a5a1..f461ce809 100644 --- a/clutter/clutter/clutter-gesture.h +++ b/clutter/clutter/clutter-gesture.h @@ -88,6 +88,20 @@ struct _ClutterGestureClass * ClutterGestureClass::may_recognize: (skip) */ gboolean (* may_recognize) (ClutterGesture *self); + + /** + * ClutterGestureClass::should_influence: (skip) + */ + void (* should_influence) (ClutterGesture *self, + ClutterGesture *other_gesture, + gboolean *cancel_on_recognizing); + + /** + * ClutterGestureClass::should_be_influenced_by: (skip) + */ + void (* should_be_influenced_by) (ClutterGesture *self, + ClutterGesture *other_gesture, + gboolean *cancelled_on_recognizing); }; CLUTTER_EXPORT @@ -144,4 +158,8 @@ CLUTTER_EXPORT const ClutterEvent * clutter_gesture_get_point_event (ClutterGesture *self, int point_index); +CLUTTER_EXPORT +void clutter_gesture_can_not_cancel (ClutterGesture *self, + ClutterGesture *other_gesture); + G_END_DECLS diff --git a/clutter/clutter/clutter-stage.c b/clutter/clutter/clutter-stage.c index c13c548dc..dae5ca47e 100644 --- a/clutter/clutter/clutter-stage.c +++ b/clutter/clutter/clutter-stage.c @@ -4292,6 +4292,47 @@ clutter_stage_maybe_lost_implicit_grab (ClutterStage *self, cleanup_implicit_grab (entry); } +static void +setup_sequence_actions (GArray *emission_chain, + const ClutterEvent *sequence_begin_event) +{ + ClutterInputDevice *device = clutter_event_get_device (sequence_begin_event); + ClutterEventSequence *sequence = clutter_event_get_event_sequence (sequence_begin_event); + unsigned int i, j; + + for (i = 0; i < emission_chain->len; i++) + { + EventReceiver *receiver = &g_array_index (emission_chain, EventReceiver, i); + + if (!receiver->action) + continue; + + if (!clutter_action_register_sequence (receiver->action, sequence_begin_event)) + g_clear_object (&receiver->action); + } + + for (i = 0; i < emission_chain->len; i++) + { + EventReceiver *receiver_1 = &g_array_index (emission_chain, EventReceiver, i); + + if (!receiver_1->action) + continue; + + for (j = i + 1; j < emission_chain->len; j++) + { + EventReceiver *receiver_2 = &g_array_index (emission_chain, EventReceiver, j); + + if (!receiver_2->action) + continue; + + clutter_action_setup_sequence_relationship (receiver_1->action, + receiver_2->action, + device, + sequence); + } + } +} + void clutter_stage_emit_event (ClutterStage *self, const ClutterEvent *event) @@ -4389,6 +4430,7 @@ clutter_stage_emit_event (ClutterStage *self, clutter_actor_set_implicitly_grabbed (entry->implicit_grab_actor, TRUE); create_event_emission_chain (self, entry->event_emission_chain, seat_grab_actor, target_actor); + setup_sequence_actions (entry->event_emission_chain, event); } if (entry && entry->press_count)