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)