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: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/2389>
This commit is contained in:
Jonas Dreßler 2022-04-08 20:21:42 +02:00
parent 7dd37558b1
commit 63cc4da4f9
6 changed files with 406 additions and 47 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -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 */

View File

@ -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);
}

View File

@ -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

View File

@ -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)