diff --git a/gfx/layers/ipc/AsyncPanZoomController.cpp b/gfx/layers/ipc/AsyncPanZoomController.cpp index 0ff4e4bc023..8620e3b9f3e 100644 --- a/gfx/layers/ipc/AsyncPanZoomController.cpp +++ b/gfx/layers/ipc/AsyncPanZoomController.cpp @@ -594,6 +594,18 @@ nsEventStatus AsyncPanZoomController::HandleInputEvent(const InputData& aEvent) } break; } + default: NS_WARNING("Unhandled input event"); break; + } + + mLastEventTime = aEvent.mTime; + return rv; +} + +nsEventStatus AsyncPanZoomController::HandleGestureEvent(const InputData& aEvent) +{ + nsEventStatus rv = nsEventStatus_eIgnore; + + switch (aEvent.mInputType) { case PINCHGESTURE_INPUT: { const PinchGestureInput& pinchGestureInput = aEvent.AsPinchGestureInput(); switch (pinchGestureInput.mType) { diff --git a/gfx/layers/ipc/AsyncPanZoomController.h b/gfx/layers/ipc/AsyncPanZoomController.h index 0a7143f6ba4..532d0f68721 100644 --- a/gfx/layers/ipc/AsyncPanZoomController.h +++ b/gfx/layers/ipc/AsyncPanZoomController.h @@ -255,6 +255,15 @@ public: */ nsEventStatus HandleInputEvent(const InputData& aEvent); + /** + * Handler for gesture events. + * Currently some gestures are detected in GestureEventListener that calls + * APZC back through this handler in order to avoid recursive calls to + * APZC::HandleInputEvent() which is supposed to do the work for + * ReceiveInputEvent(). + */ + nsEventStatus HandleGestureEvent(const InputData& aEvent); + /** * Populates the provided object (if non-null) with the scrollable guid of this apzc. */ diff --git a/gfx/layers/ipc/GestureEventListener.cpp b/gfx/layers/ipc/GestureEventListener.cpp index a144e716bcf..5fc136e479e 100644 --- a/gfx/layers/ipc/GestureEventListener.cpp +++ b/gfx/layers/ipc/GestureEventListener.cpp @@ -8,12 +8,9 @@ #include // for fabsf #include // for size_t #include "AsyncPanZoomController.h" // for AsyncPanZoomController -#include "mozilla/layers/APZCTreeManager.h" // for APZCTreeManager #include "base/task.h" // for CancelableTask, etc #include "gfxPrefs.h" // for gfxPrefs -#include "mozilla/gfx/BasePoint.h" // for BasePoint -#include "mozilla/mozalloc.h" // for operator new -#include "nsDebug.h" // for NS_WARN_IF_FALSE +#include "nsDebug.h" // for NS_WARNING #include "nsMathUtils.h" // for NS_hypot namespace mozilla { @@ -34,12 +31,26 @@ static const uint32_t MAX_TAP_TIME = 300; */ static const float PINCH_START_THRESHOLD = 35.0f; +ScreenPoint GetCurrentFocus(const MultiTouchInput& aEvent) +{ + const ScreenIntPoint& firstTouch = aEvent.mTouches[0].mScreenPoint, + secondTouch = aEvent.mTouches[1].mScreenPoint; + return ScreenPoint(firstTouch + secondTouch) / 2; +} + +float GetCurrentSpan(const MultiTouchInput& aEvent) +{ + const ScreenIntPoint& firstTouch = aEvent.mTouches[0].mScreenPoint, + secondTouch = aEvent.mTouches[1].mScreenPoint; + ScreenIntPoint delta = secondTouch - firstTouch; + return float(NS_hypot(delta.x, delta.y)); +} + GestureEventListener::GestureEventListener(AsyncPanZoomController* aAsyncPanZoomController) : mAsyncPanZoomController(aAsyncPanZoomController), mState(GESTURE_NONE), mSpanChange(0.0f), - mTapStartTime(0), - mLastTapEndTime(0), + mPreviousSpan(0.0f), mLastTouchInput(MultiTouchInput::MULTITOUCH_START, 0, 0) { } @@ -50,352 +61,401 @@ GestureEventListener::~GestureEventListener() nsEventStatus GestureEventListener::HandleInputEvent(const MultiTouchInput& aEvent) { + nsEventStatus rv = nsEventStatus_eIgnore; + // Cache the current event since it may become the single or long tap that we // send. mLastTouchInput = aEvent; - switch (aEvent.mType) - { + switch (aEvent.mType) { case MultiTouchInput::MULTITOUCH_START: - case MultiTouchInput::MULTITOUCH_ENTER: { + case MultiTouchInput::MULTITOUCH_ENTER: + mTouches.Clear(); + for (size_t i = 0; i < aEvent.mTouches.Length(); i++) { + mTouches.AppendElement(aEvent.mTouches[i]); + } + + if (aEvent.mTouches.Length() == 1) { + rv = HandleInputTouchSingleStart(); + } else { + rv = HandleInputTouchMultiStart(); + } + break; + case MultiTouchInput::MULTITOUCH_MOVE: + rv = HandleInputTouchMove(); + break; + case MultiTouchInput::MULTITOUCH_END: + case MultiTouchInput::MULTITOUCH_LEAVE: for (size_t i = 0; i < aEvent.mTouches.Length(); i++) { - bool foundAlreadyExistingTouch = false; for (size_t j = 0; j < mTouches.Length(); j++) { - if (mTouches[j].mIdentifier == aEvent.mTouches[i].mIdentifier) { - foundAlreadyExistingTouch = true; + if (aEvent.mTouches[i].mIdentifier == mTouches[j].mIdentifier) { + mTouches.RemoveElementAt(j); break; } } - - // If we didn't find a touch in our list that matches this, then add it. - if (!foundAlreadyExistingTouch) { - mTouches.AppendElement(aEvent.mTouches[i]); - } - } - - size_t length = mTouches.Length(); - if (length == 1) { - mTapStartTime = aEvent.mTime; - mTouchStartPosition = aEvent.mTouches[0].mScreenPoint; - if (mState == GESTURE_NONE) { - mState = GESTURE_WAITING_SINGLE_TAP; - - mLongTapTimeoutTask = - NewRunnableMethod(this, &GestureEventListener::TimeoutLongTap); - - mAsyncPanZoomController->PostDelayedTask( - mLongTapTimeoutTask, - gfxPrefs::UiClickHoldContextMenusDelay()); - } - } else if (length == 2) { - // Another finger has been added; it can't be a tap anymore. - HandleTapCancel(aEvent); } + rv = HandleInputTouchEnd(); break; - } - case MultiTouchInput::MULTITOUCH_MOVE: { - // If we move too much, bail out of the tap. - ScreenIntPoint delta = aEvent.mTouches[0].mScreenPoint - mTouchStartPosition; - if (mTouches.Length() == 1 && - NS_hypot(delta.x, delta.y) > AsyncPanZoomController::GetTouchStartTolerance()) - { - HandleTapCancel(aEvent); - } - - size_t eventTouchesMatched = 0; - for (size_t i = 0; i < mTouches.Length(); i++) { - bool isTouchRemoved = true; - for (size_t j = 0; j < aEvent.mTouches.Length(); j++) { - if (mTouches[i].mIdentifier == aEvent.mTouches[j].mIdentifier) { - eventTouchesMatched++; - isTouchRemoved = false; - mTouches[i] = aEvent.mTouches[j]; - } - } - if (isTouchRemoved) { - // this touch point was lifted, so remove it from our list - mTouches.RemoveElementAt(i); - i--; - } - } - - NS_WARN_IF_FALSE(eventTouchesMatched == aEvent.mTouches.Length(), "Touch moved, but not in list"); - - break; - } - case MultiTouchInput::MULTITOUCH_END: - case MultiTouchInput::MULTITOUCH_LEAVE: { - for (size_t i = 0; i < aEvent.mTouches.Length(); i++) { - bool foundAlreadyExistingTouch = false; - for (size_t j = 0; j < mTouches.Length() && !foundAlreadyExistingTouch; j++) { - if (aEvent.mTouches[i].mIdentifier == mTouches[j].mIdentifier) { - foundAlreadyExistingTouch = true; - mTouches.RemoveElementAt(j); - } - } - NS_WARN_IF_FALSE(foundAlreadyExistingTouch, "Touch ended, but not in list"); - } - - if (mState == GESTURE_WAITING_DOUBLE_TAP) { - CancelDoubleTapTimeoutTask(); - if (mTapStartTime - mLastTapEndTime > MAX_TAP_TIME || - aEvent.mTime - mTapStartTime > MAX_TAP_TIME) { - // Either the time between taps or the last tap took too long - // confirm previous tap and handle current tap seperately - TimeoutDoubleTap(); - mState = GESTURE_WAITING_SINGLE_TAP; - } else { - // We were waiting for a double tap and it has arrived. - HandleDoubleTap(aEvent); - mState = GESTURE_NONE; - } - } - - if (mState == GESTURE_LONG_TAP_UP) { - HandleLongTapUpEvent(aEvent); - mState = GESTURE_NONE; - } else if (mState == GESTURE_WAITING_SINGLE_TAP && - aEvent.mTime - mTapStartTime > MAX_TAP_TIME) { - // Extended taps are immediately dispatched as single taps - CancelLongTapTimeoutTask(); - HandleSingleTapConfirmedEvent(aEvent); - mState = GESTURE_NONE; - } else if (mState == GESTURE_WAITING_SINGLE_TAP) { - CancelLongTapTimeoutTask(); - nsEventStatus tapupEvent = HandleSingleTapUpEvent(aEvent); - - if (tapupEvent == nsEventStatus_eIgnore) { - // We were not waiting for anything but a single tap has happened that - // may turn into a double tap. Wait a while and if it doesn't turn into - // a double tap, send a single tap instead. - mState = GESTURE_WAITING_DOUBLE_TAP; - - mDoubleTapTimeoutTask = - NewRunnableMethod(this, &GestureEventListener::TimeoutDoubleTap); - - mAsyncPanZoomController->PostDelayedTask( - mDoubleTapTimeoutTask, - MAX_TAP_TIME); - - } else if (tapupEvent == nsEventStatus_eConsumeNoDefault) { - // We sent the tapup into content without waiting for a double tap - mState = GESTURE_NONE; - } - } - - mLastTapEndTime = aEvent.mTime; - - if (!mTouches.Length()) { - mSpanChange = 0.0f; - } - - break; - } case MultiTouchInput::MULTITOUCH_CANCEL: - // FIXME: we should probably clear a bunch of gesture state here - break; - } - - return HandlePinchGestureEvent(aEvent); -} - -nsEventStatus GestureEventListener::HandlePinchGestureEvent(const MultiTouchInput& aEvent) -{ - nsEventStatus rv = nsEventStatus_eIgnore; - - if (aEvent.mType == MultiTouchInput::MULTITOUCH_CANCEL) { mTouches.Clear(); - mState = GESTURE_NONE; - return rv; - } - - if (mTouches.Length() > 1) { - const ScreenIntPoint& firstTouch = mTouches[0].mScreenPoint, - secondTouch = mTouches[1].mScreenPoint; - ScreenPoint focusPoint = ScreenPoint(firstTouch + secondTouch) / 2; - ScreenIntPoint delta = secondTouch - firstTouch; - float currentSpan = float(NS_hypot(delta.x, delta.y)); - - switch (mState) { - case GESTURE_NONE: - mPreviousSpan = currentSpan; - mState = GESTURE_WAITING_PINCH; - // Deliberately fall through. If the user pinched and took their fingers - // off the screen such that they still had 1 left on it, we want there to - // be no resistance. We should only reset |mSpanChange| once all fingers - // are off the screen. - case GESTURE_WAITING_PINCH: { - mSpanChange += fabsf(currentSpan - mPreviousSpan); - if (mSpanChange > PINCH_START_THRESHOLD) { - PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_START, - aEvent.mTime, - focusPoint, - currentSpan, - currentSpan, - aEvent.modifiers); - - mAsyncPanZoomController->HandleInputEvent(pinchEvent); - - mState = GESTURE_PINCH; - } - - break; - } - case GESTURE_PINCH: { - PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_SCALE, - aEvent.mTime, - focusPoint, - currentSpan, - mPreviousSpan, - aEvent.modifiers); - - mAsyncPanZoomController->HandleInputEvent(pinchEvent); - break; - } - default: - // What? - break; - } - - mPreviousSpan = currentSpan; - - rv = nsEventStatus_eConsumeNoDefault; - } else if (mState == GESTURE_PINCH) { - PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_END, - aEvent.mTime, - ScreenPoint(), - 1.0f, - 1.0f, - aEvent.modifiers); - mAsyncPanZoomController->HandleInputEvent(pinchEvent); - - mState = GESTURE_NONE; - - // If the user left a finger on the screen, spoof a touch start event and - // send it to APZC so that they can continue panning from that point. - if (mTouches.Length() == 1) { - MultiTouchInput touchEvent(MultiTouchInput::MULTITOUCH_START, - aEvent.mTime, - aEvent.modifiers); - touchEvent.mTouches.AppendElement(mTouches[0]); - mAsyncPanZoomController->HandleInputEvent(touchEvent); - - // The spoofed touch start will get back to GEL and make us enter the - // GESTURE_WAITING_SINGLE_TAP state, but this isn't a new touch, so there - // is no condition under which this touch should turn into any tap. - mState = GESTURE_NONE; - } - - rv = nsEventStatus_eConsumeNoDefault; - } else if (mState == GESTURE_WAITING_PINCH) { - mState = GESTURE_NONE; + rv = HandleInputTouchCancel(); + break; } return rv; } -nsEventStatus GestureEventListener::HandleSingleTapUpEvent(const MultiTouchInput& aEvent) +nsEventStatus GestureEventListener::HandleInputTouchSingleStart() { - TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_UP, aEvent.mTime, - aEvent.mTouches[0].mScreenPoint, aEvent.modifiers); - return mAsyncPanZoomController->HandleInputEvent(tapEvent); -} + switch (mState) { + case GESTURE_NONE: + SetState(GESTURE_FIRST_SINGLE_TOUCH_DOWN); + mTouchStartPosition = mLastTouchInput.mTouches[0].mScreenPoint; -nsEventStatus GestureEventListener::HandleSingleTapConfirmedEvent(const MultiTouchInput& aEvent) -{ - TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_CONFIRMED, aEvent.mTime, - aEvent.mTouches[0].mScreenPoint, aEvent.modifiers); - return mAsyncPanZoomController->HandleInputEvent(tapEvent); -} - -nsEventStatus GestureEventListener::HandleLongTapEvent(const MultiTouchInput& aEvent) -{ - TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_LONG, aEvent.mTime, - aEvent.mTouches[0].mScreenPoint, aEvent.modifiers); - return mAsyncPanZoomController->HandleInputEvent(tapEvent); -} - -nsEventStatus GestureEventListener::HandleLongTapUpEvent(const MultiTouchInput& aEvent) -{ - TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_LONG_UP, aEvent.mTime, - aEvent.mTouches[0].mScreenPoint, aEvent.modifiers); - return mAsyncPanZoomController->HandleInputEvent(tapEvent); -} - -nsEventStatus GestureEventListener::HandleTapCancel(const MultiTouchInput& aEvent) -{ - mTapStartTime = 0; - - switch (mState) - { - case GESTURE_WAITING_SINGLE_TAP: - CancelLongTapTimeoutTask(); - mState = GESTURE_NONE; + CreateLongTapTimeoutTask(); + CreateMaxTapTimeoutTask(); break; - - case GESTURE_WAITING_DOUBLE_TAP: - case GESTURE_LONG_TAP_UP: - mState = GESTURE_NONE; + case GESTURE_FIRST_SINGLE_TOUCH_UP: + SetState(GESTURE_SECOND_SINGLE_TOUCH_DOWN); break; default: + NS_WARNING("Unhandled state upon single touch start"); + SetState(GESTURE_NONE); break; } - return nsEventStatus_eConsumeDoDefault; + return nsEventStatus_eIgnore; } -nsEventStatus GestureEventListener::HandleDoubleTap(const MultiTouchInput& aEvent) +nsEventStatus GestureEventListener::HandleInputTouchMultiStart() { - TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_DOUBLE, aEvent.mTime, - aEvent.mTouches[0].mScreenPoint, aEvent.modifiers); - return mAsyncPanZoomController->HandleInputEvent(tapEvent); + nsEventStatus rv = nsEventStatus_eIgnore; + + switch (mState) { + case GESTURE_NONE: + SetState(GESTURE_MULTI_TOUCH_DOWN); + break; + case GESTURE_FIRST_SINGLE_TOUCH_DOWN: + CancelLongTapTimeoutTask(); + CancelMaxTapTimeoutTask(); + SetState(GESTURE_MULTI_TOUCH_DOWN); + // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event + rv = nsEventStatus_eConsumeNoDefault; + break; + case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN: + CancelLongTapTimeoutTask(); + SetState(GESTURE_MULTI_TOUCH_DOWN); + // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event + rv = nsEventStatus_eConsumeNoDefault; + break; + case GESTURE_FIRST_SINGLE_TOUCH_UP: + // Cancel wait for double tap + CancelMaxTapTimeoutTask(); + SetState(GESTURE_MULTI_TOUCH_DOWN); + // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event + rv = nsEventStatus_eConsumeNoDefault; + break; + case GESTURE_SECOND_SINGLE_TOUCH_DOWN: + // Cancel wait for single tap + CancelMaxTapTimeoutTask(); + SetState(GESTURE_MULTI_TOUCH_DOWN); + // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event + rv = nsEventStatus_eConsumeNoDefault; + break; + case GESTURE_LONG_TOUCH_DOWN: + SetState(GESTURE_MULTI_TOUCH_DOWN); + break; + case GESTURE_MULTI_TOUCH_DOWN: + case GESTURE_PINCH: + // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event + rv = nsEventStatus_eConsumeNoDefault; + break; + default: + NS_WARNING("Unhandled state upon multitouch start"); + SetState(GESTURE_NONE); + break; + } + + return rv; } -void GestureEventListener::TimeoutDoubleTap() +nsEventStatus GestureEventListener::HandleInputTouchMove() { - mDoubleTapTimeoutTask = nullptr; - // If we haven't gotten another tap by now, reset the state and treat it as a - // single tap. It couldn't have been a double tap. - if (mState == GESTURE_WAITING_DOUBLE_TAP) { - mState = GESTURE_NONE; + nsEventStatus rv = nsEventStatus_eIgnore; - HandleSingleTapConfirmedEvent(mLastTouchInput); + switch (mState) { + case GESTURE_NONE: + case GESTURE_LONG_TOUCH_DOWN: + // Ignore this input signal as the corresponding events get handled by APZC + break; + + case GESTURE_FIRST_SINGLE_TOUCH_DOWN: + case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN: + case GESTURE_SECOND_SINGLE_TOUCH_DOWN: { + // If we move too much, bail out of the tap. + ScreenIntPoint delta = mLastTouchInput.mTouches[0].mScreenPoint - mTouchStartPosition; + if (NS_hypot(delta.x, delta.y) > AsyncPanZoomController::GetTouchStartTolerance()) { + CancelLongTapTimeoutTask(); + CancelMaxTapTimeoutTask(); + SetState(GESTURE_NONE); + } + break; } + + case GESTURE_MULTI_TOUCH_DOWN: { + if (mLastTouchInput.mTouches.Length() < 2) { + NS_WARNING("Wrong input: less than 2 moving points in GESTURE_MULTI_TOUCH_DOWN state"); + break; + } + + float currentSpan = GetCurrentSpan(mLastTouchInput); + + mSpanChange += fabsf(currentSpan - mPreviousSpan); + if (mSpanChange > PINCH_START_THRESHOLD) { + SetState(GESTURE_PINCH); + PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_START, + mLastTouchInput.mTime, + GetCurrentFocus(mLastTouchInput), + currentSpan, + currentSpan, + mLastTouchInput.modifiers); + + mAsyncPanZoomController->HandleGestureEvent(pinchEvent); + } + rv = nsEventStatus_eConsumeNoDefault; + mPreviousSpan = currentSpan; + break; + } + + case GESTURE_PINCH: { + if (mLastTouchInput.mTouches.Length() < 2) { + NS_WARNING("Wrong input: less than 2 moving points in GESTURE_PINCH state"); + // Prevent APZC::OnTouchMove() from handling this wrong input + rv = nsEventStatus_eConsumeNoDefault; + break; + } + + float currentSpan = GetCurrentSpan(mLastTouchInput); + + PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_SCALE, + mLastTouchInput.mTime, + GetCurrentFocus(mLastTouchInput), + currentSpan, + mPreviousSpan, + mLastTouchInput.modifiers); + + mAsyncPanZoomController->HandleGestureEvent(pinchEvent); + rv = nsEventStatus_eConsumeNoDefault; + mPreviousSpan = currentSpan; + + break; + } + + default: + NS_WARNING("Unhandled state upon touch move"); + SetState(GESTURE_NONE); + break; + } + + return rv; } -void GestureEventListener::CancelDoubleTapTimeoutTask() { - if (mDoubleTapTimeoutTask) { - mDoubleTapTimeoutTask->Cancel(); - mDoubleTapTimeoutTask = nullptr; +nsEventStatus GestureEventListener::HandleInputTouchEnd() +{ + nsEventStatus rv = nsEventStatus_eIgnore; + + switch (mState) { + case GESTURE_NONE: + // GEL doesn't have a dedicated state for PANNING handled in APZC thus ignore. + break; + + case GESTURE_FIRST_SINGLE_TOUCH_DOWN: { + CancelLongTapTimeoutTask(); + CancelMaxTapTimeoutTask(); + TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_UP, + mLastTouchInput.mTime, + mLastTouchInput.mTouches[0].mScreenPoint, + mLastTouchInput.modifiers); + nsEventStatus tapupStatus = mAsyncPanZoomController->HandleGestureEvent(tapEvent); + if (tapupStatus == nsEventStatus_eIgnore) { + SetState(GESTURE_FIRST_SINGLE_TOUCH_UP); + CreateMaxTapTimeoutTask(); + } else { + // We sent the tapup into content without waiting for a double tap + SetState(GESTURE_NONE); + } + break; } + + case GESTURE_SECOND_SINGLE_TOUCH_DOWN: { + CancelMaxTapTimeoutTask(); + SetState(GESTURE_NONE); + TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_DOUBLE, + mLastTouchInput.mTime, + mLastTouchInput.mTouches[0].mScreenPoint, + mLastTouchInput.modifiers); + mAsyncPanZoomController->HandleGestureEvent(tapEvent); + break; + } + + case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN: + CancelLongTapTimeoutTask(); + SetState(GESTURE_NONE); + TriggerSingleTapConfirmedEvent(); + break; + + case GESTURE_LONG_TOUCH_DOWN: { + SetState(GESTURE_NONE); + TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_LONG_UP, + mLastTouchInput.mTime, + mLastTouchInput.mTouches[0].mScreenPoint, + mLastTouchInput.modifiers); + mAsyncPanZoomController->HandleGestureEvent(tapEvent); + break; + } + + case GESTURE_MULTI_TOUCH_DOWN: + if (mTouches.Length() < 2) { + SetState(GESTURE_NONE); + } + break; + + case GESTURE_PINCH: + if (mTouches.Length() < 2) { + SetState(GESTURE_NONE); + PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_END, + mLastTouchInput.mTime, + ScreenPoint(), + 1.0f, + 1.0f, + mLastTouchInput.modifiers); + mAsyncPanZoomController->HandleGestureEvent(pinchEvent); + } + rv = nsEventStatus_eConsumeNoDefault; + break; + + default: + NS_WARNING("Unhandled state upon touch end"); + SetState(GESTURE_NONE); + break; + } + + return rv; } -void GestureEventListener::TimeoutLongTap() +nsEventStatus GestureEventListener::HandleInputTouchCancel() +{ + SetState(GESTURE_NONE); + return nsEventStatus_eIgnore; +} + +void GestureEventListener::HandleInputTimeoutLongTap() { mLongTapTimeoutTask = nullptr; - // If the tap has not been released, this is a long press. - if (mState == GESTURE_WAITING_SINGLE_TAP) { - mState = GESTURE_LONG_TAP_UP; - HandleLongTapEvent(mLastTouchInput); + switch (mState) { + case GESTURE_FIRST_SINGLE_TOUCH_DOWN: + // just in case MAX_TAP_TIME > ContextMenuDelay cancel MAX_TAP timer + // and fall through + CancelMaxTapTimeoutTask(); + case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN: { + SetState(GESTURE_LONG_TOUCH_DOWN); + TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_LONG, + mLastTouchInput.mTime, + mLastTouchInput.mTouches[0].mScreenPoint, + mLastTouchInput.modifiers); + mAsyncPanZoomController->HandleGestureEvent(tapEvent); + break; + } + default: + NS_WARNING("Unhandled state upon long tap timeout"); + SetState(GESTURE_NONE); + break; } } -void GestureEventListener::CancelLongTapTimeoutTask() { +void GestureEventListener::HandleInputTimeoutMaxTap() +{ + mMaxTapTimeoutTask = nullptr; + + if (mState == GESTURE_FIRST_SINGLE_TOUCH_DOWN) { + SetState(GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN); + } else if (mState == GESTURE_FIRST_SINGLE_TOUCH_UP || + mState == GESTURE_SECOND_SINGLE_TOUCH_DOWN) { + SetState(GESTURE_NONE); + TriggerSingleTapConfirmedEvent(); + } else { + NS_WARNING("Unhandled state upon MAX_TAP timeout"); + SetState(GESTURE_NONE); + } +} + +void GestureEventListener::TriggerSingleTapConfirmedEvent() +{ + TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_CONFIRMED, + mLastTouchInput.mTime, + mLastTouchInput.mTouches[0].mScreenPoint, + mLastTouchInput.modifiers); + mAsyncPanZoomController->HandleGestureEvent(tapEvent); +} + +void GestureEventListener::SetState(GestureState aState) +{ + mState = aState; + + if (mState == GESTURE_NONE) { + mSpanChange = 0.0f; + mPreviousSpan = 0.0f; + } else if (mState == GESTURE_MULTI_TOUCH_DOWN) { + mPreviousSpan = GetCurrentSpan(mLastTouchInput); + } +} + +void GestureEventListener::CancelLongTapTimeoutTask() +{ + if (mState == GESTURE_SECOND_SINGLE_TOUCH_DOWN) { + // being in this state means the task has been canceled already + return; + } + if (mLongTapTimeoutTask) { mLongTapTimeoutTask->Cancel(); mLongTapTimeoutTask = nullptr; } } -AsyncPanZoomController* GestureEventListener::GetAsyncPanZoomController() { - return mAsyncPanZoomController; +void GestureEventListener::CreateLongTapTimeoutTask() +{ + mLongTapTimeoutTask = + NewRunnableMethod(this, &GestureEventListener::HandleInputTimeoutLongTap); + + mAsyncPanZoomController->PostDelayedTask( + mLongTapTimeoutTask, + gfxPrefs::UiClickHoldContextMenusDelay()); } -void GestureEventListener::CancelGesture() { - mTouches.Clear(); - mState = GESTURE_NONE; +void GestureEventListener::CancelMaxTapTimeoutTask() +{ + if (mState == GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN) { + // being in this state means the timer has just been triggered + return; + } + + if (mMaxTapTimeoutTask) { + mMaxTapTimeoutTask->Cancel(); + mMaxTapTimeoutTask = nullptr; + } +} + +void GestureEventListener::CreateMaxTapTimeoutTask() +{ + mMaxTapTimeoutTask = + NewRunnableMethod(this, &GestureEventListener::HandleInputTimeoutMaxTap); + + mAsyncPanZoomController->PostDelayedTask( + mMaxTapTimeoutTask, + MAX_TAP_TIME); } } diff --git a/gfx/layers/ipc/GestureEventListener.h b/gfx/layers/ipc/GestureEventListener.h index e8b21477258..703898be31d 100644 --- a/gfx/layers/ipc/GestureEventListener.h +++ b/gfx/layers/ipc/GestureEventListener.h @@ -7,10 +7,8 @@ #ifndef mozilla_layers_GestureEventListener_h #define mozilla_layers_GestureEventListener_h -#include // for uint64_t #include "InputData.h" // for MultiTouchInput, etc #include "Units.h" // for ScreenIntPoint -#include "mozilla/Assertions.h" // for MOZ_ASSERT_HELPER2 #include "mozilla/EventForwards.h" // for nsEventStatus #include "nsAutoPtr.h" // for nsRefPtr #include "nsISupportsImpl.h" @@ -56,118 +54,95 @@ public: */ nsEventStatus HandleInputEvent(const MultiTouchInput& aEvent); - /** - * Cancels any currently active gesture. May not properly handle situations - * that require extra work at the gesture's end, like a pinch which only - * requests a repaint once it has ended. - */ - void CancelGesture(); - - /** - * Returns the AsyncPanZoomController stored on this class and used for - * callbacks. - */ - AsyncPanZoomController* GetAsyncPanZoomController(); - protected: + + /** + * States of GEL finite-state machine. + */ enum GestureState { - // There's no gesture going on, and we don't think we're about to enter one. + // This is the initial and final state of any gesture. + // In this state there's no gesture going on, and we don't think we're + // about to enter one. + // Allowed next states: GESTURE_FIRST_SINGLE_TOUCH_DOWN, GESTURE_MULTI_TOUCH_DOWN. GESTURE_NONE, + + // A touch start with a single touch point has just happened. + // After having gotten into this state we start timers for MAX_TAP_TIME and + // gfxPrefs::UiClickHoldContextMenusDelay(). + // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE, + // GESTURE_FIRST_SINGLE_TOUCH_UP, GESTURE_LONG_TOUCH_DOWN, + // GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN. + GESTURE_FIRST_SINGLE_TOUCH_DOWN, + + // While in GESTURE_FIRST_SINGLE_TOUCH_DOWN state a MAX_TAP_TIME timer got + // triggered. Now we'll trigger either a single tap if a user lifts her + // finger or a long tap if gfxPrefs::UiClickHoldContextMenusDelay() happens + // first. + // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE, + // GESTURE_LONG_TOUCH_DOWN. + GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN, + + // A user put her finger down and lifted it up quickly enough. + // After having gotten into this state we clear the timer for MAX_TAP_TIME. + // Allowed next states: GESTURE_SECOND_SINGLE_TOUCH_DOWN, GESTURE_NONE, + // GESTURE_MULTI_TOUCH_DOWN. + GESTURE_FIRST_SINGLE_TOUCH_UP, + + // A user put down her finger again right after a single tap thus the + // gesture can't be a single tap, but rather a double tap. But we're + // still not sure about that until the user lifts her finger again. + // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE. + GESTURE_SECOND_SINGLE_TOUCH_DOWN, + + // A long touch has happened, but the user still keeps her finger down. + // We'll trigger a "long tap up" event when the finger is up. + // Allowed next states: GESTURE_NONE, GESTURE_MULTI_TOUCH_DOWN. + GESTURE_LONG_TOUCH_DOWN, + // We have detected that two or more fingers are on the screen, but there // hasn't been enough movement yet to make us start actually zooming the // screen. - GESTURE_WAITING_PINCH, + // Allowed next states: GESTURE_PINCH, GESTURE_NONE + GESTURE_MULTI_TOUCH_DOWN, + // There are two or more fingers on the screen, and the user has already // pinched enough for us to start zooming the screen. - GESTURE_PINCH, - // A touch start has happened and it may turn into a tap. We use this - // because, if we put down two fingers and then lift them very quickly, this - // may be mistaken for a tap. - GESTURE_WAITING_SINGLE_TAP, - // A single tap has happened for sure, and we're waiting for a second tap. - GESTURE_WAITING_DOUBLE_TAP, - // A long tap has happened, wait for the tap to be released in case we need - // to fire a click event in the case the long tap was not handled. - GESTURE_LONG_TAP_UP + // Allowed next states: GESTURE_NONE + GESTURE_PINCH }; /** - * Attempts to handle the event as a pinch event. If it is not a pinch event, - * then we simply tell the next consumer to consume the event instead. + * These HandleInput* functions comprise input alphabet of the GEL + * finite-state machine triggering state transitions. */ - nsEventStatus HandlePinchGestureEvent(const MultiTouchInput& aEvent); + nsEventStatus HandleInputTouchSingleStart(); + nsEventStatus HandleInputTouchMultiStart(); + nsEventStatus HandleInputTouchEnd(); + nsEventStatus HandleInputTouchMove(); + nsEventStatus HandleInputTouchCancel(); + void HandleInputTimeoutLongTap(); + void HandleInputTimeoutMaxTap(); + + void TriggerSingleTapConfirmedEvent(); /** - * Attempts to handle the event as a single tap event, which highlights links - * before opening them. In general, this will not attempt to block the touch - * event from being passed along to AsyncPanZoomController since APZC needs to - * know about touches ending (and we only know if a touch was a tap once it - * ends). + * Do actual state transition and reset substates. */ - nsEventStatus HandleSingleTapUpEvent(const MultiTouchInput& aEvent); - - /** - * Attempts to handle a single tap confirmation. This is what will actually - * open links, etc. In general, this will not attempt to block the touch event - * from being passed along to AsyncPanZoomController since APZC needs to know - * about touches ending (and we only know if a touch was a tap once it ends). - */ - nsEventStatus HandleSingleTapConfirmedEvent(const MultiTouchInput& aEvent); - - /** - * Attempts to handle a long tap confirmation. This is what will use - * for context menu. - */ - nsEventStatus HandleLongTapEvent(const MultiTouchInput& aEvent); - - /** - * Attempts to handle release of long tap. This is used to fire click - * events in the case the context menu was not invoked. - */ - nsEventStatus HandleLongTapUpEvent(const MultiTouchInput& aEvent); - - /** - * Attempts to handle a tap event cancellation. This happens when we think - * something was a tap but it actually wasn't. In general, this will not - * attempt to block the touch event from being passed along to - * AsyncPanZoomController since APZC needs to know about touches ending (and - * we only know if a touch was a tap once it ends). - */ - nsEventStatus HandleTapCancel(const MultiTouchInput& aEvent); - - /** - * Attempts to handle a double tap. This happens when we get two single taps - * within a short time. In general, this will not attempt to block the touch - * event from being passed along to AsyncPanZoomController since APZC needs to - * know about touches ending (and we only know if a touch was a double tap - * once it ends). - */ - nsEventStatus HandleDoubleTap(const MultiTouchInput& aEvent); - - /** - * Times out a single tap we think may be turned into a double tap. This will - * also send a single tap if we're still in the "GESTURE_WAITING_DOUBLE_TAP" - * state when this is called. This should be called a short time after a - * single tap is detected, and the delay on it should be enough that the user - * has time to tap again (to make a double tap). - */ - void TimeoutDoubleTap(); - /** - * Times out a long tap. This should be called a 'long' time after a single - * tap is detected. - */ - void TimeoutLongTap(); + void SetState(GestureState aState); nsRefPtr mAsyncPanZoomController; /** * Array containing all active touches. When a touch happens it, gets added to * this array, even if we choose not to handle it. When it ends, we remove it. + * We need to maintain this array in order to detect the end of the + * "multitouch" states because touch start events contain all current touches, + * but touch end events contain only those touches that have gone. */ nsTArray mTouches; /** - * Current gesture we're dealing with. + * Current state we're dealing with. */ GestureState mState; @@ -186,56 +161,49 @@ protected: float mPreviousSpan; /** - * Stores the time a touch started, used for detecting a tap gesture. Only - * valid when there's exactly one touch in mTouches. This is the time that the - * first touch was inserted into the array. This is a uint64_t because it is - * initialized from interactions with InputData, which stores its timestamps as - * a uint64_t. - */ - uint64_t mTapStartTime; - - /** - * Stores the time the last tap ends (finger leaves the screen). This is used - * when mDoubleTapTimeoutTask cannot be scheduled in time and consecutive - * taps are falsely regarded as double taps. - */ - uint64_t mLastTapEndTime; - - /** - * Cached copy of the last touch input, only valid when in the - * "GESTURE_WAITING_DOUBLE_TAP" state. This is used to forward along to - * AsyncPanZoomController if a single tap needs to be sent (since it is sent - * shortly after the user actually taps, since we need to wait for a double - * tap). + * Cached copy of the last touch input. */ MultiTouchInput mLastTouchInput; /** - * Task used to timeout a double tap. This gets posted to the UI thread such - * that it runs a short time after a single tap happens. We cache it so that - * we can cancel it if a double tap actually comes in. - * CancelDoubleTapTimeoutTask: Cancel the mDoubleTapTimeoutTask and also set - * it to null. + * Position of the last touch starting. This is only valid during an attempt + * to determine if a touch is a tap. If a touch point moves away from + * mTouchStartPosition to the distance greater than + * AsyncPanZoomController::GetTouchStartTolerance() while in + * GESTURE_FIRST_SINGLE_TOUCH_DOWN, GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN + * or GESTURE_SECOND_SINGLE_TOUCH_DOWN then we're certain the gesture is + * not tap. */ - CancelableTask *mDoubleTapTimeoutTask; - inline void CancelDoubleTapTimeoutTask(); + ScreenIntPoint mTouchStartPosition; /** * Task used to timeout a long tap. This gets posted to the UI thread such * that it runs a time when a single tap happens. We cache it so that * we can cancel it if any other touch event happens. + * + * The task is supposed to be non-null if in GESTURE_FIRST_SINGLE_TOUCH_DOWN + * and GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN states. + * * CancelLongTapTimeoutTask: Cancel the mLongTapTimeoutTask and also set * it to null. */ CancelableTask *mLongTapTimeoutTask; - inline void CancelLongTapTimeoutTask(); + void CancelLongTapTimeoutTask(); + void CreateLongTapTimeoutTask(); /** - * Position of the last touch starting. This is only valid during an attempt - * to determine if a touch is a tap. This means that it is used in both the - * "GESTURE_WAITING_SINGLE_TAP" and "GESTURE_WAITING_DOUBLE_TAP" states. + * Task used to timeout a single tap or a double tap. + * + * The task is supposed to be non-null if in GESTURE_FIRST_SINGLE_TOUCH_DOWN, + * GESTURE_FIRST_SINGLE_TOUCH_UP and GESTURE_SECOND_SINGLE_TOUCH_DOWN states. + * + * CancelMaxTapTimeoutTask: Cancel the mMaxTapTimeoutTask and also set + * it to null. */ - ScreenIntPoint mTouchStartPosition; + CancelableTask *mMaxTapTimeoutTask; + void CancelMaxTapTimeoutTask(); + void CreateMaxTapTimeoutTask(); + }; } diff --git a/gfx/tests/gtest/TestAsyncPanZoomController.cpp b/gfx/tests/gtest/TestAsyncPanZoomController.cpp index a07fcd2e172..e5f1580385f 100644 --- a/gfx/tests/gtest/TestAsyncPanZoomController.cpp +++ b/gfx/tests/gtest/TestAsyncPanZoomController.cpp @@ -65,39 +65,38 @@ public: class MockContentControllerDelayed : public MockContentController { public: MockContentControllerDelayed() - : mCurrentTask(nullptr) { } void PostDelayedTask(Task* aTask, int aDelayMs) { - // Ensure we're not clobbering an existing task - EXPECT_TRUE(nullptr == mCurrentTask); - mCurrentTask = aTask; + mTaskQueue.AppendElement(aTask); } void CheckHasDelayedTask() { - EXPECT_TRUE(nullptr != mCurrentTask); + EXPECT_TRUE(mTaskQueue.Length() > 0); } void ClearDelayedTask() { - mCurrentTask = nullptr; + mTaskQueue.RemoveElementAt(0); + } + + void DestroyOldestTask() { + delete mTaskQueue[0]; + mTaskQueue.RemoveElementAt(0); } // Note that deleting mCurrentTask is important in order to // release the reference to the callee object. Without this // that object might be leaked. This is also why we don't - // expose mCurrentTask to any users of MockContentControllerDelayed. + // expose mTaskQueue to any users of MockContentControllerDelayed. void RunDelayedTask() { - // Running mCurrentTask may call PostDelayedTask, so we should - // keep a local copy of mCurrentTask and operate on that - Task* local = mCurrentTask; - mCurrentTask = nullptr; - local->Run(); - delete local; + mTaskQueue[0]->Run(); + delete mTaskQueue[0]; + mTaskQueue.RemoveElementAt(0); } private: - Task *mCurrentTask; + nsTArray mTaskQueue; }; @@ -284,19 +283,19 @@ void DoPanTest(bool aShouldTriggerScroll, bool aShouldUseTouchAction, uint32_t a static void ApzcPinch(AsyncPanZoomController* aApzc, int aFocusX, int aFocusY, float aScale) { - aApzc->HandleInputEvent(PinchGestureInput(PinchGestureInput::PINCHGESTURE_START, + aApzc->HandleGestureEvent(PinchGestureInput(PinchGestureInput::PINCHGESTURE_START, 0, ScreenPoint(aFocusX, aFocusY), 10.0, 10.0, 0)); - aApzc->HandleInputEvent(PinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, + aApzc->HandleGestureEvent(PinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, 0, ScreenPoint(aFocusX, aFocusY), 10.0 * aScale, 10.0, 0)); - aApzc->HandleInputEvent(PinchGestureInput(PinchGestureInput::PINCHGESTURE_END, + aApzc->HandleGestureEvent(PinchGestureInput(PinchGestureInput::PINCHGESTURE_END, 0, ScreenPoint(aFocusX, aFocusY), // note: negative values here tell APZC @@ -324,8 +323,10 @@ static nsEventStatus ApzcTap(AsyncPanZoomController* apzc, int aX, int aY, int& aTime, int aTapLength, MockContentControllerDelayed* mcc = nullptr) { nsEventStatus status = ApzcDown(apzc, aX, aY, aTime); if (mcc != nullptr) { - // There will be a delayed task posted for the long-tap timeout, but - // if we were provided a non-null mcc we want to clear it. + // There will be delayed tasks posted for the long-tap and MAX_TAP timeouts, but + // if we were provided a non-null mcc we want to clear them. + mcc->CheckHasDelayedTask(); + mcc->ClearDelayedTask(); mcc->CheckHasDelayedTask(); mcc->ClearDelayedTask(); } @@ -776,6 +777,16 @@ DoLongPressTest(bool aShouldUseTouchAction, uint32_t aBehavior) { mcc->RunDelayedTask(); check.Call("postHandleLongTap"); + // Destroy pending MAX_TAP timeout task + mcc->DestroyOldestTask(); + // There should be a TimeoutContentResponse task in the queue still + // Clear the waiting-for-content timeout task, then send the signal that + // content has handled this long tap. This takes the place of the + // "contextmenu" event. + mcc->CheckHasDelayedTask(); + mcc->ClearDelayedTask(); + apzc->ContentReceivedTouch(true); + time += 1000; status = ApzcUp(apzc, 10, 10, time); @@ -835,6 +846,8 @@ TEST_F(AsyncPanZoomControllerTester, LongPressPreventDefault) { mcc->RunDelayedTask(); check.Call("postHandleLongTap"); + // Destroy pending MAX_TAP timeout task + mcc->DestroyOldestTask(); // Clear the waiting-for-content timeout task, then send the signal that // content has handled this long tap. This takes the place of the // "contextmenu" event.