Bug 985541 - Turn GestureEventListener into Finite-state machine. r=kats,drs

This commit is contained in:
Dmitry Rozhkov 2014-03-27 14:04:39 +02:00
parent af6b166a2e
commit 5fb4778e0a
5 changed files with 500 additions and 438 deletions

View File

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

View File

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

View File

@ -8,12 +8,9 @@
#include <math.h> // for fabsf
#include <stddef.h> // 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);
}
}

View File

@ -7,10 +7,8 @@
#ifndef mozilla_layers_GestureEventListener_h
#define mozilla_layers_GestureEventListener_h
#include <stdint.h> // 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<AsyncPanZoomController> 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<SingleTouchData> 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();
};
}

View File

@ -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<Task*> 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.