Bug 1009733 - Rewrite much of the APZC input block handling code. r=botond

This commit is contained in:
Kartikaya Gupta 2014-07-16 08:33:50 -04:00
parent 9f8eabeb1c
commit e7190ff526
10 changed files with 722 additions and 358 deletions

View File

@ -401,49 +401,17 @@ child:
UpdateFrame(FrameMetrics frame);
/**
* Acknowledge the receipt of a scroll offset update from the content
* process. This is used to manage concurrent scroll updates from many
* sources.
*/
// The following methods correspond to functions on the GeckoContentController
// interface in gfx/layers/apz/public/GeckoContentController.h. Refer to documentation
// in that file for these functions.
AcknowledgeScrollUpdate(ViewID aScrollId, uint32_t aScrollGeneration);
/**
* Requests handling of a double tap. |point| is in CSS pixels, relative to
* the scroll offset. This message is expected to round-trip back to
* ZoomToRect() with a rect indicating where we should zoom to.
*/
HandleDoubleTap(CSSPoint point, ScrollableLayerGuid aGuid);
/**
* Requests handling of a single tap. |point| is in CSS pixels, relative to
* the scroll offset. This message is expected to send a "mousedown" and
* "mouseup" series of events at this point.
*/
HandleSingleTap(CSSPoint point, ScrollableLayerGuid aGuid);
/**
* Requests handling of a long tap. |point| is in CSS pixels, relative to
* the scroll offset. This message is expected to send a "contextmenu"
* events at this point.
*/
HandleLongTap(CSSPoint point, ScrollableLayerGuid aGuid);
/**
* Requests handling of releasing a long tap. |aPoint| is in CSS pixels,
* relative to the current scroll offset. In the case the "contextmenu"
* event generated by the preceding HandleLongTap call was not handled,
* this message is expected to generate a "mousedown" and "mouseup"
* series of events
*/
HandleLongTapUp(CSSPoint point, ScrollableLayerGuid aGuid);
/**
* Notifies the child about various APZ state changes.
* See GeckoContentController::NotifyAPZStateChange() for details.
*/
NotifyAPZStateChange(ViewID aViewId, APZStateChange aChange, int aArg);
/**
* Sending an activate message moves focus to the child.
*/

View File

@ -702,9 +702,7 @@ TabChild::TabChild(nsIContentChild* aManager, const TabContext& aContext, uint32
, mTriedBrowserInit(false)
, mOrientation(eScreenOrientation_PortraitPrimary)
, mUpdateHitRegion(false)
, mContextMenuHandled(false)
, mLongTapEventHandled(false)
, mWaitingTouchListeners(false)
, mPendingTouchPreventedResponse(false)
, mIgnoreKeyPressEvent(false)
, mActiveElementManager(new ActiveElementManager())
, mHasValidInnerSize(false)
@ -1805,23 +1803,23 @@ TabChild::RecvHandleLongTap(const CSSPoint& aPoint, const ScrollableLayerGuid& a
return true;
}
mContextMenuHandled =
bool eventHandled =
DispatchMouseEvent(NS_LITERAL_STRING("contextmenu"),
APZCCallbackHelper::ApplyCallbackTransform(aPoint, aGuid),
2, 1, 0, false,
nsIDOMMouseEvent::MOZ_SOURCE_TOUCH);
// If no one handle context menu, fire MOZLONGTAP event
if (!mContextMenuHandled) {
if (!eventHandled) {
LayoutDevicePoint currentPoint =
APZCCallbackHelper::ApplyCallbackTransform(aPoint, aGuid) * mWidget->GetDefaultScale();
int time = 0;
nsEventStatus status =
DispatchSynthesizedMouseEvent(NS_MOUSE_MOZLONGTAP, time, currentPoint, mWidget);
mLongTapEventHandled = (status == nsEventStatus_eConsumeNoDefault);
eventHandled = (status == nsEventStatus_eConsumeNoDefault);
}
SendContentReceivedTouch(aGuid, mContextMenuHandled || mLongTapEventHandled);
SendContentReceivedTouch(aGuid, eventHandled);
return true;
}
@ -1829,16 +1827,6 @@ TabChild::RecvHandleLongTap(const CSSPoint& aPoint, const ScrollableLayerGuid& a
bool
TabChild::RecvHandleLongTapUp(const CSSPoint& aPoint, const ScrollableLayerGuid& aGuid)
{
if (mContextMenuHandled) {
mContextMenuHandled = false;
return true;
}
if (mLongTapEventHandled) {
mLongTapEventHandled = false;
return true;
}
RecvHandleSingleTap(aPoint, aGuid);
return true;
}
@ -2095,23 +2083,21 @@ TabChild::RecvRealTouchEvent(const WidgetTouchEvent& aEvent,
mActiveElementManager->SetTargetElement(localEvent.touches[0]->GetTarget());
}
nsCOMPtr<nsPIDOMWindow> outerWindow = do_GetInterface(WebNavigation());
nsCOMPtr<nsPIDOMWindow> innerWindow = outerWindow ? outerWindow->GetCurrentInnerWindow() : nullptr;
if (!innerWindow || (!innerWindow->HasTouchEventListeners() &&
!innerWindow->MayHaveTouchCaret())) {
SendContentReceivedTouch(aGuid, false);
return true;
}
bool isTouchPrevented = nsIPresShell::gPreventMouseEvents ||
localEvent.mFlags.mMultipleActionsPrevented;
switch (aEvent.message) {
case NS_TOUCH_START: {
if (mPendingTouchPreventedResponse) {
// We can enter here if we get two TOUCH_STARTs in a row and didn't
// respond to the first one. Respond to it now.
SendContentReceivedTouch(mPendingTouchPreventedGuid, false);
mPendingTouchPreventedResponse = false;
}
if (isTouchPrevented) {
SendContentReceivedTouch(aGuid, isTouchPrevented);
} else {
mWaitingTouchListeners = true;
mPendingTouchPreventedResponse = true;
mPendingTouchPreventedGuid = aGuid;
}
break;
}
@ -2119,9 +2105,10 @@ TabChild::RecvRealTouchEvent(const WidgetTouchEvent& aEvent,
case NS_TOUCH_MOVE:
case NS_TOUCH_END:
case NS_TOUCH_CANCEL: {
if (mWaitingTouchListeners) {
SendContentReceivedTouch(aGuid, isTouchPrevented);
mWaitingTouchListeners = false;
if (mPendingTouchPreventedResponse) {
MOZ_ASSERT(aGuid == mPendingTouchPreventedGuid);
SendContentReceivedTouch(mPendingTouchPreventedGuid, isTouchPrevented);
mPendingTouchPreventedResponse = false;
}
break;
}

View File

@ -586,9 +586,8 @@ private:
bool mTriedBrowserInit;
ScreenOrientation mOrientation;
bool mUpdateHitRegion;
bool mContextMenuHandled;
bool mLongTapEventHandled;
bool mWaitingTouchListeners;
bool mPendingTouchPreventedResponse;
ScrollableLayerGuid mPendingTouchPreventedGuid;
void FireSingleTapEvent(LayoutDevicePoint aPoint);
bool mIgnoreKeyPressEvent;

View File

@ -68,7 +68,8 @@ public:
* relative to the current scroll offset. HandleLongTapUp will always be
* preceeded by HandleLongTap. However not all calls to HandleLongTap will
* be followed by a HandleLongTapUp (for example, if the user drags
* around between the long-tap and lifting their finger).
* around between the long-tap and lifting their finger, or if content
* notifies the APZ that the long-tap event was prevent-defaulted).
*/
virtual void HandleLongTapUp(const CSSPoint& aPoint,
int32_t aModifiers,

View File

@ -15,6 +15,7 @@
#include "FrameMetrics.h" // for FrameMetrics, etc
#include "GestureEventListener.h" // for GestureEventListener
#include "InputData.h" // for MultiTouchInput, etc
#include "TouchBlockState.h" // for TouchBlockState
#include "Units.h" // for CSSRect, CSSPoint, etc
#include "UnitTransforms.h" // for TransformTo
#include "base/message_loop.h" // for MessageLoop
@ -316,14 +317,6 @@ typedef GeckoContentController::APZStateChange APZStateChange;
* Units: ms
*/
/**
* Default touch behavior (is used when not touch behavior is set).
*/
static const uint32_t DefaultTouchBehavior = AllowedTouchBehavior::VERTICAL_PAN |
AllowedTouchBehavior::HORIZONTAL_PAN |
AllowedTouchBehavior::PINCH_ZOOM |
AllowedTouchBehavior::DOUBLE_TAP_ZOOM;
/**
* Angle from axis within which we stay axis-locked
*/
@ -697,7 +690,6 @@ AsyncPanZoomController::AsyncPanZoomController(uint64_t aLayersId,
mSharingFrameMetricsAcrossProcesses(false),
mMonitor("AsyncPanZoomController"),
mState(NOTHING),
mContentResponseTimeoutTask(nullptr),
mX(MOZ_THIS_IN_INITIALIZER_LIST()),
mY(MOZ_THIS_IN_INITIALIZER_LIST()),
mPanDirRestricted(false),
@ -707,7 +699,7 @@ AsyncPanZoomController::AsyncPanZoomController(uint64_t aLayersId,
mLastAsyncScrollOffset(0, 0),
mCurrentAsyncScrollOffset(0, 0),
mAsyncScrollTimeoutTask(nullptr),
mHandlingTouchQueue(false),
mTouchBlockBalance(0),
mTreeManager(aTreeManager),
mScrollParentId(FrameMetrics::NULL_SCROLL_ID),
mAPZCId(sAsyncPanZoomControllerCount++),
@ -759,6 +751,8 @@ AsyncPanZoomController::Destroy()
{
CancelAnimation();
mTouchBlockQueue.Clear();
{ // scope the lock
MonitorAutoLock lock(mRefPtrMonitor);
mGeckoContentController = nullptr;
@ -802,39 +796,50 @@ AsyncPanZoomController::GetTouchStartTolerance()
}
nsEventStatus AsyncPanZoomController::ReceiveInputEvent(const InputData& aEvent) {
if (aEvent.mInputType == MULTITOUCH_INPUT &&
aEvent.AsMultiTouchInput().mType == MultiTouchInput::MULTITOUCH_START) {
// Starting a new touch block, clear old touch block state.
mTouchBlockState = TouchBlockState();
if (aEvent.mInputType != MULTITOUCH_INPUT) {
return HandleInputEvent(aEvent);
}
// If we may have touch listeners and touch action property is enabled, we
// enable the machinery that allows touch listeners to preventDefault any touch inputs
// and also waits for the allowed touch behavior values to be received from the outside.
// This should not happen unless there are actually touch listeners and touch-action property
// enable as it introduces potentially unbounded lag because it causes a round-trip through
// content. Usually, if content is responding in a timely fashion, this only introduces a
// nearly constant few hundred ms of lag.
if ((mFrameMetrics.mMayHaveTouchListeners || mFrameMetrics.mMayHaveTouchCaret) &&
aEvent.mInputType == MULTITOUCH_INPUT &&
(mState == NOTHING || mState == TOUCHING || IsPanningState(mState))) {
const MultiTouchInput& multiTouchInput = aEvent.AsMultiTouchInput();
if (multiTouchInput.mType == MultiTouchInput::MULTITOUCH_START) {
SetState(WAITING_CONTENT_RESPONSE);
TouchBlockState* block = nullptr;
if (aEvent.AsMultiTouchInput().mType == MultiTouchInput::MULTITOUCH_START) {
block = StartNewTouchBlock(false);
APZC_LOG("%p started new touch block %p\n", this, block);
if (mFrameMetrics.mMayHaveTouchListeners || mFrameMetrics.mMayHaveTouchCaret) {
// Content may intercept the touch events and prevent-default them. So we schedule
// a timeout to give content time to do that.
ScheduleContentResponseTimeout();
} else {
// Content won't prevent-default this, so we can just pretend like we scheduled
// a timeout and it expired. Note that we will still receive a ContentReceivedTouch
// callback for this block, and so we need to make sure we adjust the touch balance.
APZC_LOG("%p not waiting for content response on block %p\n", this, block);
mTouchBlockBalance++;
block->TimeoutContentResponse();
}
} else if (mTouchBlockQueue.IsEmpty()) {
NS_WARNING("Received a non-start touch event while no touch blocks active!");
} else {
// this touch is part of the most-recently created block
block = mTouchBlockQueue.LastElement().get();
APZC_LOG("%p received new event in block %p\n", this, block);
}
if (mState == WAITING_CONTENT_RESPONSE || mHandlingTouchQueue) {
if (aEvent.mInputType == MULTITOUCH_INPUT) {
const MultiTouchInput& multiTouchInput = aEvent.AsMultiTouchInput();
mTouchQueue.AppendElement(multiTouchInput);
SetContentResponseTimer();
}
if (!block) {
return nsEventStatus_eIgnore;
}
return HandleInputEvent(aEvent);
if (block == CurrentTouchBlock() && block->IsReadyForHandling()) {
APZC_LOG("%p's current touch block is ready with preventdefault %d\n",
this, block->IsDefaultPrevented());
if (block->IsDefaultPrevented()) {
return nsEventStatus_eIgnore;
}
return HandleInputEvent(aEvent);
}
// Otherwise, add it to the queue for the touch block
block->AddEvent(aEvent.AsMultiTouchInput());
return nsEventStatus_eConsumeDoDefault;
}
nsEventStatus AsyncPanZoomController::HandleInputEvent(const InputData& aEvent) {
@ -957,7 +962,6 @@ nsEventStatus AsyncPanZoomController::OnTouchStart(const MultiTouchInput& aEvent
case CROSS_SLIDING_X:
case CROSS_SLIDING_Y:
case PINCHING:
case WAITING_CONTENT_RESPONSE:
NS_WARNING("Received impossible touch in OnTouchStart");
break;
default:
@ -992,9 +996,7 @@ nsEventStatus AsyncPanZoomController::OnTouchMove(const MultiTouchInput& aEvent)
return nsEventStatus_eIgnore;
}
if (gfxPrefs::TouchActionEnabled() &&
(GetTouchBehavior(0) & AllowedTouchBehavior::VERTICAL_PAN) &&
(GetTouchBehavior(0) & AllowedTouchBehavior::HORIZONTAL_PAN)) {
if (gfxPrefs::TouchActionEnabled() && CurrentTouchBlock()->TouchActionAllowsPanningXY()) {
// User tries to trigger a touch behavior. If allowed touch behavior is vertical pan
// + horizontal pan (touch-action value is equal to AUTO) we can return ConsumeNoDefault
// status immediately to trigger cancel event further. It should happen independent of
@ -1017,7 +1019,6 @@ nsEventStatus AsyncPanZoomController::OnTouchMove(const MultiTouchInput& aEvent)
NS_WARNING("Gesture listener should have handled pinching in OnTouchMove.");
return nsEventStatus_eIgnore;
case WAITING_CONTENT_RESPONSE:
case SNAP_BACK: // Should not receive a touch-move in the SNAP_BACK state
// as touch blocks that begin in an overscrolled state
// are ignored.
@ -1092,7 +1093,6 @@ nsEventStatus AsyncPanZoomController::OnTouchEnd(const MultiTouchInput& aEvent)
NS_WARNING("Gesture listener should have handled pinching in OnTouchEnd.");
return nsEventStatus_eIgnore;
case WAITING_CONTENT_RESPONSE:
case SNAP_BACK: // Should not receive a touch-move in the SNAP_BACK state
// as touch blocks that begin in an overscrolled state
// are ignored.
@ -1113,7 +1113,9 @@ nsEventStatus AsyncPanZoomController::OnTouchCancel(const MultiTouchInput& aEven
nsEventStatus AsyncPanZoomController::OnScaleBegin(const PinchGestureInput& aEvent) {
APZC_LOG("%p got a scale-begin in state %d\n", this, mState);
if (!TouchActionAllowPinchZoom()) {
// Note that there may not be a touch block at this point, if we received the
// PinchGestureEvent directly from widget code without any touch events.
if (HasReadyTouchBlock() && !CurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
return nsEventStatus_eIgnore;
}
@ -1130,7 +1132,7 @@ nsEventStatus AsyncPanZoomController::OnScaleBegin(const PinchGestureInput& aEve
nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) {
APZC_LOG("%p got a scale in state %d\n", this, mState);
if (!TouchActionAllowPinchZoom()) {
if (HasReadyTouchBlock() && !CurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
return nsEventStatus_eIgnore;
}
@ -1213,7 +1215,7 @@ nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) {
nsEventStatus AsyncPanZoomController::OnScaleEnd(const PinchGestureInput& aEvent) {
APZC_LOG("%p got a scale-end in state %d\n", this, mState);
if (!TouchActionAllowPinchZoom()) {
if (HasReadyTouchBlock() && !CurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
return nsEventStatus_eIgnore;
}
@ -1353,8 +1355,8 @@ nsEventStatus AsyncPanZoomController::OnLongPress(const TapGestureInput& aEvent)
int32_t modifiers = WidgetModifiersToDOMModifiers(aEvent.modifiers);
CSSPoint geckoScreenPoint;
if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) {
SetState(WAITING_CONTENT_RESPONSE);
SetContentResponseTimer();
StartNewTouchBlock(true);
ScheduleContentResponseTimeout();
controller->HandleLongTap(geckoScreenPoint, modifiers, GetGuid());
return nsEventStatus_eConsumeNoDefault;
}
@ -1391,7 +1393,7 @@ nsEventStatus AsyncPanZoomController::GenerateSingleTap(const ScreenIntPoint& aP
NewRunnableMethod(controller.get(), &GeckoContentController::HandleSingleTap,
geckoScreenPoint, modifiers, GetGuid()),
0);
mTouchBlockState.mSingleTapOccurred = true;
CurrentTouchBlock()->SetSingleTapOccurred();
return nsEventStatus_eConsumeNoDefault;
}
}
@ -1401,7 +1403,7 @@ nsEventStatus AsyncPanZoomController::GenerateSingleTap(const ScreenIntPoint& aP
void AsyncPanZoomController::OnTouchEndOrCancel() {
if (nsRefPtr<GeckoContentController> controller = GetGeckoContentController()) {
controller->NotifyAPZStateChange(
GetGuid(), APZStateChange::EndTouch, mTouchBlockState.mSingleTapOccurred);
GetGuid(), APZStateChange::EndTouch, CurrentTouchBlock()->SingleTapOccurred());
}
}
@ -1409,7 +1411,7 @@ nsEventStatus AsyncPanZoomController::OnSingleTapUp(const TapGestureInput& aEven
APZC_LOG("%p got a single-tap-up in state %d\n", this, mState);
// If mZoomConstraints.mAllowDoubleTapZoom is true we wait for a call to OnSingleTapConfirmed before
// sending event to content
if (!(mZoomConstraints.mAllowDoubleTapZoom && TouchActionAllowDoubleTapZoom())) {
if (!(mZoomConstraints.mAllowDoubleTapZoom && CurrentTouchBlock()->TouchActionAllowsDoubleTapZoom())) {
return GenerateSingleTap(aEvent.mPoint, aEvent.modifiers);
}
return nsEventStatus_eIgnore;
@ -1424,7 +1426,7 @@ nsEventStatus AsyncPanZoomController::OnDoubleTap(const TapGestureInput& aEvent)
APZC_LOG("%p got a double-tap in state %d\n", this, mState);
nsRefPtr<GeckoContentController> controller = GetGeckoContentController();
if (controller) {
if (mZoomConstraints.mAllowDoubleTapZoom && TouchActionAllowDoubleTapZoom()) {
if (mZoomConstraints.mAllowDoubleTapZoom && CurrentTouchBlock()->TouchActionAllowsDoubleTapZoom()) {
int32_t modifiers = WidgetModifiersToDOMModifiers(aEvent.modifiers);
CSSPoint geckoScreenPoint;
if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) {
@ -1451,10 +1453,10 @@ const ScreenPoint AsyncPanZoomController::GetVelocityVector() {
return ScreenPoint(mX.GetVelocity(), mY.GetVelocity());
}
void AsyncPanZoomController::HandlePanningWithTouchAction(double aAngle, TouchBehaviorFlags aBehavior) {
void AsyncPanZoomController::HandlePanningWithTouchAction(double aAngle) {
// Handling of cross sliding will need to be added in this method after touch-action released
// enabled by default.
if ((aBehavior & AllowedTouchBehavior::VERTICAL_PAN) && (aBehavior & AllowedTouchBehavior::HORIZONTAL_PAN)) {
if (CurrentTouchBlock()->TouchActionAllowsPanningXY()) {
if (mX.CanScrollNow() && mY.CanScrollNow()) {
if (IsCloseToHorizontal(aAngle, AXIS_LOCK_ANGLE)) {
mY.SetAxisLocked(true);
@ -1470,7 +1472,7 @@ void AsyncPanZoomController::HandlePanningWithTouchAction(double aAngle, TouchBe
} else {
SetState(NOTHING);
}
} else if (aBehavior & AllowedTouchBehavior::HORIZONTAL_PAN) {
} else if (CurrentTouchBlock()->TouchActionAllowsPanningX()) {
// Using bigger angle for panning to keep behavior consistent
// with IE.
if (IsCloseToHorizontal(aAngle, ALLOWED_DIRECT_PAN_ANGLE)) {
@ -1482,7 +1484,7 @@ void AsyncPanZoomController::HandlePanningWithTouchAction(double aAngle, TouchBe
// requires it.
SetState(NOTHING);
}
} else if (aBehavior & AllowedTouchBehavior::VERTICAL_PAN) {
} else if (CurrentTouchBlock()->TouchActionAllowsPanningY()) {
if (IsCloseToVertical(aAngle, ALLOWED_DIRECT_PAN_ANGLE)) {
mX.SetAxisLocked(true);
SetState(PANNING_LOCKED_Y);
@ -1561,7 +1563,7 @@ nsEventStatus AsyncPanZoomController::StartPanning(const MultiTouchInput& aEvent
angle = fabs(angle); // range [0, pi]
if (gfxPrefs::TouchActionEnabled()) {
HandlePanningWithTouchAction(angle, GetTouchBehavior(0));
HandlePanningWithTouchAction(angle);
} else {
if (GetAxisLockMode() == FREE) {
SetState(PANNING);
@ -2458,90 +2460,144 @@ void AsyncPanZoomController::ZoomToRect(CSSRect aRect) {
}
}
void AsyncPanZoomController::ContentReceivedTouch(bool aPreventDefault) {
mTouchBlockState.mPreventDefaultSet = true;
mTouchBlockState.mPreventDefault = aPreventDefault;
CheckContentResponse();
void
AsyncPanZoomController::ScheduleContentResponseTimeout() {
APZC_LOG("%p scheduling content response timeout\n", this);
PostDelayedTask(
NewRunnableMethod(this, &AsyncPanZoomController::ContentResponseTimeout),
gfxPrefs::APZContentResponseTimeout());
}
void AsyncPanZoomController::CheckContentResponse() {
bool canProceedToTouchState = true;
if (mFrameMetrics.mMayHaveTouchListeners ||
mFrameMetrics.mMayHaveTouchCaret) {
canProceedToTouchState &= mTouchBlockState.mPreventDefaultSet;
}
if (gfxPrefs::TouchActionEnabled()) {
canProceedToTouchState &= mTouchBlockState.mAllowedTouchBehaviorSet;
}
if (!canProceedToTouchState) {
return;
}
if (mContentResponseTimeoutTask) {
mContentResponseTimeoutTask->Cancel();
mContentResponseTimeoutTask = nullptr;
}
if (mState == WAITING_CONTENT_RESPONSE) {
if (!mTouchBlockState.mPreventDefault) {
SetState(NOTHING);
}
mHandlingTouchQueue = true;
while (!mTouchQueue.IsEmpty()) {
if (!mTouchBlockState.mPreventDefault) {
HandleInputEvent(mTouchQueue[0]);
}
if (mTouchQueue[0].mType == MultiTouchInput::MULTITOUCH_END ||
mTouchQueue[0].mType == MultiTouchInput::MULTITOUCH_CANCEL) {
mTouchQueue.RemoveElementAt(0);
void
AsyncPanZoomController::ContentResponseTimeout() {
mTouchBlockBalance++;
APZC_LOG("%p got a content response timeout; balance %d\n", this, mTouchBlockBalance);
if (mTouchBlockBalance > 0) {
// Find the first touch block in the queue that hasn't already received
// the content response timeout callback, and notify it.
bool found = false;
for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
if (mTouchBlockQueue[i]->TimeoutContentResponse()) {
found = true;
break;
}
mTouchQueue.RemoveElementAt(0);
}
mHandlingTouchQueue = false;
if (found) {
ProcessPendingInputBlocks();
} else {
NS_WARNING("APZC received more ContentResponseTimeout calls than it has unprocessed touch blocks\n");
}
}
}
bool AsyncPanZoomController::TouchActionAllowPinchZoom() {
if (!gfxPrefs::TouchActionEnabled()) {
return true;
}
// Pointer events specification implies all touch points to allow zoom
// to perform it.
for (size_t i = 0; i < mTouchBlockState.mAllowedTouchBehaviors.Length(); i++) {
if (!(mTouchBlockState.mAllowedTouchBehaviors[i] & AllowedTouchBehavior::PINCH_ZOOM)) {
return false;
void
AsyncPanZoomController::ContentReceivedTouch(bool aPreventDefault) {
mTouchBlockBalance--;
APZC_LOG("%p got a content response; balance %d\n", this, mTouchBlockBalance);
if (mTouchBlockBalance < 0) {
// Find the first touch block in the queue that hasn't already received
// its response from content, and notify it.
bool found = false;
for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
if (mTouchBlockQueue[i]->SetContentResponse(aPreventDefault)) {
found = true;
break;
}
}
if (found) {
ProcessPendingInputBlocks();
} else {
NS_WARNING("APZC received more ContentReceivedTouch calls than it has unprocessed touch blocks\n");
}
}
return true;
}
bool AsyncPanZoomController::TouchActionAllowDoubleTapZoom() {
if (!gfxPrefs::TouchActionEnabled()) {
return true;
}
for (size_t i = 0; i < mTouchBlockState.mAllowedTouchBehaviors.Length(); i++) {
if (!(mTouchBlockState.mAllowedTouchBehaviors[i] & AllowedTouchBehavior::DOUBLE_TAP_ZOOM)) {
return false;
void
AsyncPanZoomController::SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors) {
bool found = false;
for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
if (mTouchBlockQueue[i]->SetAllowedTouchBehaviors(aBehaviors)) {
found = true;
break;
}
}
return true;
if (found) {
ProcessPendingInputBlocks();
} else {
NS_WARNING("APZC received more SetAllowedTouchBehavior calls than it has unprocessed touch blocks\n");
}
}
AsyncPanZoomController::TouchBehaviorFlags
AsyncPanZoomController::GetTouchBehavior(uint32_t touchIndex) {
if (touchIndex < mTouchBlockState.mAllowedTouchBehaviors.Length()) {
return mTouchBlockState.mAllowedTouchBehaviors[touchIndex];
void
AsyncPanZoomController::ProcessPendingInputBlocks() {
while (true) {
TouchBlockState* curBlock = CurrentTouchBlock();
if (!curBlock->IsReadyForHandling()) {
break;
}
APZC_LOG("%p processing input block %p; preventDefault %d\n",
this, curBlock, curBlock->IsDefaultPrevented());
if (curBlock->IsDefaultPrevented()) {
SetState(NOTHING);
curBlock->DropEvents();
} else {
while (curBlock->HasEvents()) {
HandleInputEvent(curBlock->RemoveFirstEvent());
}
}
MOZ_ASSERT(!curBlock->HasEvents());
if (mTouchBlockQueue.Length() == 1) {
// If |curBlock| is the only touch block in the queue, then it is still
// active and we cannot remove it yet. We only know that a touch block is
// over when we start the next one. This block will be removed by the code
// in StartNewTouchBlock, where new touch blocks are added.
break;
}
// If we get here, we know there are more touch blocks in the queue after
// |curBlock|, so we can remove |curBlock| and try to process the next one.
APZC_LOG("%p discarding depleted touch block %p\n", this, curBlock);
mTouchBlockQueue.RemoveElementAt(0);
}
return DefaultTouchBehavior;
}
TouchBlockState*
AsyncPanZoomController::StartNewTouchBlock(bool aCopyAllowedTouchBehaviorFromCurrent)
{
TouchBlockState* newBlock = new TouchBlockState();
if (gfxPrefs::TouchActionEnabled() && aCopyAllowedTouchBehaviorFromCurrent) {
newBlock->CopyAllowedTouchBehaviorsFrom(*CurrentTouchBlock());
}
// We're going to start a new block, so clear out any depleted blocks at the head of the queue.
// See corresponding comment in ProcessPendingInputBlocks.
while (!mTouchBlockQueue.IsEmpty()) {
if (mTouchBlockQueue[0]->IsReadyForHandling() && !mTouchBlockQueue[0]->HasEvents()) {
APZC_LOG("%p discarding depleted touch block %p\n", this, mTouchBlockQueue[0]);
mTouchBlockQueue.RemoveElementAt(0);
} else {
break;
}
}
// Add the new block to the queue.
mTouchBlockQueue.AppendElement(newBlock);
return newBlock;
}
TouchBlockState*
AsyncPanZoomController::CurrentTouchBlock()
{
MOZ_ASSERT(!mTouchBlockQueue.IsEmpty());
return mTouchBlockQueue[0].get();
}
bool
AsyncPanZoomController::HasReadyTouchBlock()
{
return !mTouchBlockQueue.IsEmpty() && mTouchBlockQueue[0]->IsReadyForHandling();
}
AsyncPanZoomController::TouchBehaviorFlags
@ -2552,13 +2608,6 @@ AsyncPanZoomController::GetAllowedTouchBehavior(ScreenIntPoint& aPoint) {
return AllowedTouchBehavior::UNKNOWN;
}
void AsyncPanZoomController::SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors) {
mTouchBlockState.mAllowedTouchBehaviors.Clear();
mTouchBlockState.mAllowedTouchBehaviors.AppendElements(aBehaviors);
mTouchBlockState.mAllowedTouchBehaviorSet = true;
CheckContentResponse();
}
void AsyncPanZoomController::SetState(PanZoomState aNewState) {
PanZoomState oldState;
@ -2582,27 +2631,13 @@ void AsyncPanZoomController::SetState(PanZoomState aNewState) {
}
bool AsyncPanZoomController::IsTransformingState(PanZoomState aState) {
return !(aState == NOTHING || aState == TOUCHING || aState == WAITING_CONTENT_RESPONSE);
return !(aState == NOTHING || aState == TOUCHING);
}
bool AsyncPanZoomController::IsPanningState(PanZoomState aState) {
return (aState == PANNING || aState == PANNING_LOCKED_X || aState == PANNING_LOCKED_Y);
}
void AsyncPanZoomController::SetContentResponseTimer() {
if (!mContentResponseTimeoutTask) {
mContentResponseTimeoutTask =
NewRunnableMethod(this, &AsyncPanZoomController::TimeoutContentResponse);
PostDelayedTask(mContentResponseTimeoutTask, gfxPrefs::APZContentResponseTimeout());
}
}
void AsyncPanZoomController::TimeoutContentResponse() {
mContentResponseTimeoutTask = nullptr;
ContentReceivedTouch(false);
}
void AsyncPanZoomController::UpdateZoomConstraints(const ZoomConstraints& aConstraints) {
APZC_LOG("%p updating zoom constraints to %d %d %f %f\n", this, aConstraints.mAllowZoom,
aConstraints.mAllowDoubleTapZoom, aConstraints.mMinZoom.scale, aConstraints.mMaxZoom.scale);

View File

@ -15,6 +15,7 @@
#include "mozilla/Monitor.h"
#include "mozilla/ReentrantMonitor.h"
#include "mozilla/RefPtr.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Atomics.h"
#include "InputData.h"
#include "Axis.h"
@ -41,6 +42,7 @@ class PCompositorParent;
struct ViewTransform;
class AsyncPanZoomAnimation;
class FlingAnimation;
class TouchBlockState;
/**
* Controller for all panning and zooming logic. Any time a user input is
@ -111,6 +113,9 @@ public:
* based on what type of input it is. For example, a PinchGestureEvent will
* cause scaling. This should only be called externally to this class.
* HandleInputEvent() should be used internally.
* This function returns nsEventStatus_eIgnore for events that are ignored,
* and nsEventStatus_eConsumeDoDefault for events that are queued for
* processing pending a content response.
*/
nsEventStatus ReceiveInputEvent(const InputData& aEvent);
@ -121,14 +126,6 @@ public:
*/
void ZoomToRect(CSSRect aRect);
/**
* If we have touch listeners, this should always be called when we know
* definitively whether or not content has preventDefaulted any touch events
* that have come in. If |aPreventDefault| is true, any touch events in the
* queue will be discarded.
*/
void ContentReceivedTouch(bool aPreventDefault);
/**
* Updates any zoom constraints contained in the <meta name="viewport"> tag.
*/
@ -303,15 +300,6 @@ public:
*/
TouchBehaviorFlags GetAllowedTouchBehavior(ScreenIntPoint& aPoint);
/**
* Sets allowed touch behavior for current touch session.
* This method is invoked by the APZCTreeManager which in its turn invoked by
* the widget after performing touch-action values retrieving.
* Must be called after receiving the TOUCH_START even that started the
* touch session.
*/
void SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors);
/**
* Returns whether this APZC is for an element marked with the 'scrollgrab'
* attribute.
@ -347,10 +335,6 @@ protected:
PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */
ANIMATING_ZOOM, /* animated zoom to a new rect */
WAITING_CONTENT_RESPONSE, /* a state halfway between NOTHING and TOUCHING - the user has
put a finger down, but we don't yet know if a touch listener has
prevented the default actions yet and the allowed touch behavior
was not set yet. we still need to abort animations. */
SNAP_BACK, /* snap-back animation to relieve overscroll */
};
@ -487,7 +471,7 @@ protected:
/**
* Sets the panning state basing on the pan direction angle and current touch-action value.
*/
void HandlePanningWithTouchAction(double angle, TouchBehaviorFlags value);
void HandlePanningWithTouchAction(double angle);
/**
* Sets the panning state ignoring the touch action value.
@ -543,24 +527,6 @@ protected:
*/
const FrameMetrics& GetFrameMetrics() const;
/**
* Sets the timer for content response to a series of touch events, if it
* hasn't been already. This is to prevent us from batching up touch events
* indefinitely in the case that content doesn't respond with whether or not
* it wants to preventDefault. When the timer is fired, the touch event queue
* will be flushed.
*/
void SetContentResponseTimer();
/**
* Timeout function for content response. This should be called on a timer
* after we get our first touch event in a batch, under the condition that we
* waiting for response from content. If a notification comes indicating whether or not
* content preventDefaulted a series of touch events and touch behavior values are
* set before the timeout, the timeout should be cancelled.
*/
void TimeoutContentResponse();
/**
* Timeout function for mozbrowserasyncscroll event. Because we throttle
* mozbrowserasyncscroll events in some conditions, this function ensures
@ -570,62 +536,6 @@ protected:
void FireAsyncScrollOnTimeout();
private:
// State related to a single touch block. Does not persist across touch blocks.
struct TouchBlockState {
TouchBlockState()
: mAllowedTouchBehaviorSet(false),
mPreventDefault(false),
mPreventDefaultSet(false),
mSingleTapOccurred(false)
{}
// Values of allowed touch behavior for touch points of this touch block.
// Since there are maybe a few current active touch points per time (multitouch case)
// and each touch point should have its own value of allowed touch behavior- we're
// keeping an array of allowed touch behavior values, not the single value.
nsTArray<TouchBehaviorFlags> mAllowedTouchBehaviors;
// Specifies whether mAllowedTouchBehaviors is set for this touch events block.
bool mAllowedTouchBehaviorSet;
// Flag used to specify that content prevented the default behavior of this
// touch events block.
bool mPreventDefault;
// Specifies whether mPreventDefault property is set for this touch events block.
bool mPreventDefaultSet;
// Specifies whether a single tap event was generated during this touch block.
bool mSingleTapOccurred;
};
/*
* Returns whether current touch behavior values allow pinch-zooming.
*/
bool TouchActionAllowPinchZoom();
/*
* Returns whether current touch behavior values allow double-tap-zooming.
*/
bool TouchActionAllowDoubleTapZoom();
/*
* Returns allowed touch behavior from the mAllowedTouchBehavior array.
* In case apzc didn't receive touch behavior values within the timeout
* it returns default value.
*/
TouchBehaviorFlags GetTouchBehavior(uint32_t touchIndex);
/**
* To move from the WAITING_CONTENT_RESPONSE state to TOUCHING one we need two
* conditions set: get content listeners response (whether they called preventDefault)
* and get allowed touch behaviors.
* This method checks both conditions and changes (or not changes) state
* appropriately.
*/
void CheckContentResponse();
/**
* Helper to set the current state. Holds the monitor before actually setting
* it and fires content controller events based on state changes. Always set
@ -734,10 +644,6 @@ private:
// in a FIFO manner.
FrameMetrics mLastDispatchedPaintMetrics;
nsTArray<MultiTouchInput> mTouchQueue;
CancelableTask* mContentResponseTimeoutTask;
AxisX mX;
AxisY mY;
@ -771,19 +677,98 @@ private:
// ensures the last mozbrowserasyncscroll event is always been fired.
CancelableTask* mAsyncScrollTimeoutTask;
// Flag used to determine whether or not we should try to enter the
// WAITING_LISTENERS state. This is used in the case that we are processing a
// queued up event block. If set, this means that we are handling this queue
// and we don't want to queue the events back up again.
bool mHandlingTouchQueue;
// Stores information about the current touch block.
TouchBlockState mTouchBlockState;
nsRefPtr<AsyncPanZoomAnimation> mAnimation;
friend class Axis;
/* ===================================================================
* The functions and members in this section are used to manage
* blocks of touch events and the state needed to deal with content
* listeners.
*/
public:
/**
* This function is invoked by the APZCTreeManager which in turn is invoked
* by the widget when web content decides whether or not it wants to
* cancel a block of events. This automatically gets applied to the next
* block of events that has not yet been responded to. This function MUST
* be invoked exactly once for each touch block.
*/
void ContentReceivedTouch(bool aPreventDefault);
/**
* Sets allowed touch behavior for current touch session.
* This method is invoked by the APZCTreeManager which in its turn invoked by
* the widget after performing touch-action values retrieving.
* Must be called after receiving the TOUCH_START even that started the
* touch session.
*/
void SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors);
private:
void ScheduleContentResponseTimeout();
void ContentResponseTimeout();
/**
* Processes any pending input blocks that are ready for processing. There
* must be at least one input block in the queue when this function is called.
*/
void ProcessPendingInputBlocks();
TouchBlockState* StartNewTouchBlock(bool aCopyAllowedTouchBehaviorFromCurrent);
TouchBlockState* CurrentTouchBlock();
bool HasReadyTouchBlock();
private:
// The queue of touch blocks that have not yet been processed by this APZC.
nsTArray<UniquePtr<TouchBlockState>> mTouchBlockQueue;
// This variable requires some explanation. Strap yourself in.
//
// For each block of events, we do two things: (1) send the events to gecko and expect
// exactly one call to ContentReceivedTouch in return, and (2) kick off a timeout
// that triggers in case we don't hear from web content in a timely fashion.
// Since events are constantly coming in, we need to be able to handle more than one
// block of input events sitting in the queue.
//
// There are ordering restrictions on events that we can take advantage of, and that
// we need to abide by. Blocks of events in the queue will always be in the order that
// the user generated them. Responses we get from content will be in the same order as
// as the blocks of events in the queue. The timeout callbacks that have been posted
// will also fire in the same order as the blocks of events in the queue.
// HOWEVER, we may get multiple responses from content interleaved with multiple
// timeout expirations, and that interleaving is not predictable.
//
// Therefore, we need to make sure that for each block of events, we process the queued
// events exactly once, either when we get the response from content, or when the
// timeout expires (whichever happens first). There is no way to associate the timeout
// or response from content with a particular block of events other than via ordering.
//
// So, what we do to accomplish this is to track a "touch block balance", which is the
// number of timeout expirations that have fired, minus the number of content responses
// that have been received. (Think "balance" as in teeter-totter balance). This
// value is:
// - zero when we are in a state where the next content response we expect to receive
// and the next timeout expiration we expect to fire both correspond to the next
// unprocessed block of events in the queue.
// - negative when we are in a state where we have received more content responses than
// timeout expirations. This means that the next content repsonse we receive will
// correspond to the first unprocessed block, but the next n timeout expirations need
// to be ignored as they are for blocks we have already processed. (n is the absolute
// value of the balance.)
// - positive when we are in a state where we have received more timeout expirations
// than content responses. This means that the next timeout expiration that we will
// receive will correspond to the first unprocessed block, but the next n content
// responses need to be ignored as they are for blocks we have already processed.
// (n is the absolute value of the balance.)
//
// Note that each touch block internally carries flags that indicate whether or not it
// has received a content response and/or timeout expiration. However, we cannot rely
// on that alone to deliver these notifications to the right input block, because
// once an input block has been processed, it can potentially be removed from the queue.
// Therefore the information in that block is lost. An alternative approach would
// be to keep around those blocks until they have received both the content response
// and timeout expiration, but that involves a higher level of memory usage.
int32_t mTouchBlockBalance;
/* ===================================================================
* The functions and members in this section are used to manage

View File

@ -0,0 +1,219 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set sw=2 ts=8 et tw=80 : */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "TouchBlockState.h"
#include "mozilla/layers/APZCTreeManager.h" // for AllowedTouchBehavior
#include "gfxPrefs.h" // for gfxPrefs
#define TBS_LOG(...)
// #define TBS_LOG(...) printf_stderr("TBS: " __VA_ARGS__)
namespace mozilla {
namespace layers {
/**
* Default touch behavior (is used when no touch behavior is set).
*/
static const TouchBlockState::TouchBehaviorFlags kDefaultTouchBehavior =
AllowedTouchBehavior::VERTICAL_PAN |
AllowedTouchBehavior::HORIZONTAL_PAN |
AllowedTouchBehavior::PINCH_ZOOM |
AllowedTouchBehavior::DOUBLE_TAP_ZOOM;
TouchBlockState::TouchBlockState()
: mAllowedTouchBehaviorSet(false)
, mPreventDefault(false)
, mContentResponded(false)
, mContentResponseTimerExpired(false)
, mSingleTapOccurred(false)
{
TBS_LOG("Creating %p\n", this);
}
bool
TouchBlockState::SetContentResponse(bool aPreventDefault)
{
if (mContentResponded) {
return false;
}
TBS_LOG("%p got content response %d with timer expired %d\n",
this, aPreventDefault, mContentResponseTimerExpired);
if (!mContentResponseTimerExpired) {
mPreventDefault = aPreventDefault;
}
mContentResponded = true;
return true;
}
bool
TouchBlockState::TimeoutContentResponse()
{
if (mContentResponseTimerExpired) {
return false;
}
TBS_LOG("%p got content timer expired with response received %d\n",
this, mContentResponded);
if (!mContentResponded) {
mPreventDefault = false;
}
mContentResponseTimerExpired = true;
return true;
}
bool
TouchBlockState::SetAllowedTouchBehaviors(const nsTArray<TouchBehaviorFlags>& aBehaviors)
{
if (mAllowedTouchBehaviorSet) {
return false;
}
TBS_LOG("%p got allowed touch behaviours for %d points\n", this, aBehaviors.Length());
mAllowedTouchBehaviors.AppendElements(aBehaviors);
mAllowedTouchBehaviorSet = true;
return true;
}
bool
TouchBlockState::CopyAllowedTouchBehaviorsFrom(const TouchBlockState& aOther)
{
TBS_LOG("%p copying allowed touch behaviours from %p\n", this, &aOther);
MOZ_ASSERT(aOther.mAllowedTouchBehaviorSet);
return SetAllowedTouchBehaviors(aOther.mAllowedTouchBehaviors);
}
bool
TouchBlockState::IsReadyForHandling() const
{
// TODO: for long-tap blocks we probably don't need the touch behaviour?
if (gfxPrefs::TouchActionEnabled() && !mAllowedTouchBehaviorSet) {
return false;
}
if (!mContentResponded && !mContentResponseTimerExpired) {
return false;
}
return true;
}
bool
TouchBlockState::IsDefaultPrevented() const
{
MOZ_ASSERT(mContentResponded || mContentResponseTimerExpired);
return mPreventDefault;
}
void
TouchBlockState::SetSingleTapOccurred()
{
TBS_LOG("%p setting single-tap occurred\n", this);
mSingleTapOccurred = true;
}
bool
TouchBlockState::SingleTapOccurred() const
{
return mSingleTapOccurred;
}
bool
TouchBlockState::HasEvents() const
{
return !mEvents.IsEmpty();
}
void
TouchBlockState::AddEvent(const MultiTouchInput& aEvent)
{
TBS_LOG("%p adding event of type %d\n", this, aEvent.mType);
mEvents.AppendElement(aEvent);
}
void
TouchBlockState::DropEvents()
{
TBS_LOG("%p dropping %d events\n", this, mEvents.Length());
mEvents.Clear();
}
MultiTouchInput
TouchBlockState::RemoveFirstEvent()
{
MOZ_ASSERT(!mEvents.IsEmpty());
TBS_LOG("%p returning first of %d events\n", this, mEvents.Length());
MultiTouchInput event = mEvents[0];
mEvents.RemoveElementAt(0);
return event;
}
bool
TouchBlockState::TouchActionAllowsPinchZoom() const
{
if (!gfxPrefs::TouchActionEnabled()) {
return true;
}
// Pointer events specification requires that all touch points allow zoom.
for (size_t i = 0; i < mAllowedTouchBehaviors.Length(); i++) {
if (!(mAllowedTouchBehaviors[i] & AllowedTouchBehavior::PINCH_ZOOM)) {
return false;
}
}
return true;
}
bool
TouchBlockState::TouchActionAllowsDoubleTapZoom() const
{
if (!gfxPrefs::TouchActionEnabled()) {
return true;
}
for (size_t i = 0; i < mAllowedTouchBehaviors.Length(); i++) {
if (!(mAllowedTouchBehaviors[i] & AllowedTouchBehavior::DOUBLE_TAP_ZOOM)) {
return false;
}
}
return true;
}
bool
TouchBlockState::TouchActionAllowsPanningX() const
{
if (!gfxPrefs::TouchActionEnabled()) {
return true;
}
if (mAllowedTouchBehaviors.IsEmpty()) {
return false;
}
TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
return (flags & AllowedTouchBehavior::HORIZONTAL_PAN);
}
bool
TouchBlockState::TouchActionAllowsPanningY() const
{
if (!gfxPrefs::TouchActionEnabled()) {
return true;
}
if (mAllowedTouchBehaviors.IsEmpty()) {
return false;
}
TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
return (flags & AllowedTouchBehavior::VERTICAL_PAN);
}
bool
TouchBlockState::TouchActionAllowsPanningXY() const
{
if (!gfxPrefs::TouchActionEnabled()) {
return true;
}
if (mAllowedTouchBehaviors.IsEmpty()) {
return false;
}
TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
return (flags & AllowedTouchBehavior::HORIZONTAL_PAN)
&& (flags & AllowedTouchBehavior::VERTICAL_PAN);
}
} // namespace layers
} // namespace mozilla

View File

@ -0,0 +1,145 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set sw=2 ts=8 et tw=80 : */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_layers_TouchBlockState_h
#define mozilla_layers_TouchBlockState_h
#include "nsTArray.h" // for nsTArray
#include "InputData.h" // for MultiTouchInput
namespace mozilla {
namespace layers {
/**
* This class represents a single touch block. A touch block is
* a set of touch events that can be cancelled by web content via
* touch event listeners.
*
* Every touch-start event creates a new touch block. In this case, the
* touch block consists of the touch-start, followed by all touch events
* up to but not including the next touch-start (except in the case where
* a long-tap happens, see below). Note that in particular we cannot know
* when a touch block ends until the next one is started. Most touch
* blocks are created by receipt of a touch-start event.
*
* Every long-tap event also creates a new touch block, since it can also
* be consumed by web content. In this case, when the long-tap event is
* dispatched to web content, a new touch block is started to hold the remaining
* touch events, up to but not including the next touch start (or long-tap).
*
* Conceptually, each touch block can be cancelled by web content, and
* this information is stored in the mPreventDefault flag. Because web
* content runs on the Gecko main thread, we cannot always wait for web content's
* response. Instead, there is a timeout that sets this flag in the case
* where web content doesn't respond in time. The mContentResponded
* and mContentResponseTimerExpired flags indicate which of these scenarios
* occurred.
*
* Additionally, if touch-action is enabled, each touch block should
* have a set of allowed touch behavior flags; one for each touch point.
* This also requires running code on the Gecko main thread, and so may
* be populated with some latency. The mAllowedTouchBehaviorSet and
* mAllowedTouchBehaviors variables track this information.
*/
class TouchBlockState
{
public:
typedef uint32_t TouchBehaviorFlags;
TouchBlockState();
/**
* Record whether or not content cancelled this block of events.
* @param aPreventDefault true iff the block is cancelled.
* @return false if this block has already received a response from
* web content, true if not.
*/
bool SetContentResponse(bool aPreventDefault);
/**
* Record that content didn't respond in time.
* @return false if this block already timed out, true if not.
*/
bool TimeoutContentResponse();
/**
* Set the allowed touch behavior flags for this block.
* @return false if this block already has these flags set, true if not.
*/
bool SetAllowedTouchBehaviors(const nsTArray<TouchBehaviorFlags>& aBehaviors);
/**
* Copy the allowed touch behavior flags from another block.
* @return false if this block already has these flags set, true if not.
*/
bool CopyAllowedTouchBehaviorsFrom(const TouchBlockState& aOther);
/**
* @return true iff this block has received all the information needed
* to properly dispatch the events in the block.
*/
bool IsReadyForHandling() const;
/**
* @return true iff web content cancelled this block of events.
*/
bool IsDefaultPrevented() const;
/**
* Set a flag that indicates that this touch block triggered a single tap event.
*/
void SetSingleTapOccurred();
/**
* @return true iff SetSingleTapOccurred was previously called on this block.
*/
bool SingleTapOccurred() const;
/**
* @return true iff there are pending events in this touch block.
*/
bool HasEvents() const;
/**
* Add a new touch event to the queue of events in this input block.
*/
void AddEvent(const MultiTouchInput& aEvent);
/**
* Throw away all the events in this input block.
*/
void DropEvents();
/**
* @return the first event in the queue. The event is removed from the queue
* before it is returned.
*/
MultiTouchInput RemoveFirstEvent();
/**
* @return false iff touch-action is enabled and the allowed touch behaviors for
* this touch block do not allow pinch-zooming.
*/
bool TouchActionAllowsPinchZoom() const;
/**
* @return false iff touch-action is enabled and the allowed touch behaviors for
* this touch block do not allow double-tap zooming.
*/
bool TouchActionAllowsDoubleTapZoom() const;
/**
* @return false iff touch-action is enabled and the allowed touch behaviors for
* the first touch point do not allow panning in the specified direction(s).
*/
bool TouchActionAllowsPanningX() const;
bool TouchActionAllowsPanningY() const;
bool TouchActionAllowsPanningXY() const;
private:
nsTArray<TouchBehaviorFlags> mAllowedTouchBehaviors;
bool mAllowedTouchBehaviorSet;
bool mPreventDefault;
bool mContentResponded;
bool mContentResponseTimerExpired;
bool mSingleTapOccurred;
nsTArray<MultiTouchInput> mEvents;
};
} // namespace layers
} // namespace mozilla
#endif // mozilla_layers_TouchBlockState_h

View File

@ -235,6 +235,7 @@ UNIFIED_SOURCES += [
'apz/src/Axis.cpp',
'apz/src/GestureEventListener.cpp',
'apz/src/TaskThrottler.cpp',
'apz/src/TouchBlockState.cpp',
'apz/testutil/APZTestData.cpp',
'apz/util/ActiveElementManager.cpp',
'apz/util/APZCCallbackHelper.cpp',

View File

@ -349,10 +349,10 @@ ApzcPanAndCheckStatus(AsyncPanZoomController* aApzc,
ApzcPan(aApzc, aTreeManager, aTime, aTouchStartY, aTouchEndY, false, aAllowedTouchBehaviors, &statuses);
nsEventStatus touchStartStatus;
if (hasTouchListeners) {
if (hasTouchListeners || gfxPrefs::TouchActionEnabled()) {
// APZC shouldn't consume the start event now, instead queueing it up
// waiting for content's response.
touchStartStatus = nsEventStatus_eIgnore;
// waiting for content's response and/or allowed behavior.
touchStartStatus = nsEventStatus_eConsumeDoDefault;
} else {
// APZC should go into the touching state and therefore consume the event.
touchStartStatus = nsEventStatus_eConsumeNoDefault;
@ -360,7 +360,10 @@ ApzcPanAndCheckStatus(AsyncPanZoomController* aApzc,
EXPECT_EQ(touchStartStatus, statuses[0]);
nsEventStatus touchMoveStatus;
if (expectIgnoredPan) {
if (hasTouchListeners) {
// APZC will queue up this event while waiting for content's response.
touchMoveStatus = nsEventStatus_eConsumeDoDefault;
} else if (expectIgnoredPan) {
// APZC should ignore panning, be in TOUCHING state and therefore return eIgnore.
// The same applies to all consequent touch move events.
touchMoveStatus = nsEventStatus_eIgnore;
@ -1036,12 +1039,23 @@ protected:
int time = 0;
nsEventStatus status = ApzcDown(apzc, 10, 10, time);
EXPECT_EQ(nsEventStatus_eConsumeNoDefault, status);
if (gfxPrefs::TouchActionEnabled()) {
// If touch-action is enabled, then the event is queued until the
// allowed touch behavior is set.
EXPECT_EQ(nsEventStatus_eConsumeDoDefault, status);
} else {
// Otherwise, it is processed immediately.
EXPECT_EQ(nsEventStatus_eConsumeNoDefault, status);
}
// SetAllowedTouchBehavior() must be called after sending touch-start.
nsTArray<uint32_t> allowedTouchBehaviors;
allowedTouchBehaviors.AppendElement(aBehavior);
apzc->SetAllowedTouchBehavior(allowedTouchBehaviors);
if (gfxPrefs::TouchActionEnabled()) {
// SetAllowedTouchBehavior() must be called after sending touch-start.
nsTArray<uint32_t> allowedTouchBehaviors;
allowedTouchBehaviors.AppendElement(aBehavior);
apzc->SetAllowedTouchBehavior(allowedTouchBehaviors);
}
// Have content "respond" to the touchstart
apzc->ContentReceivedTouch(false);
MockFunction<void(std::string checkPointName)> check;
@ -1057,6 +1071,7 @@ protected:
EXPECT_CALL(check, Call("postHandleLongTapUp"));
}
// There is a longpress event scheduled on a timeout
mcc->CheckHasDelayedTask();
// Manually invoke the longpress while the touch is currently down.
@ -1066,24 +1081,23 @@ protected:
// 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.
// Dispatching the longpress event starts a new touch block, which
// needs a new content response and also has a pending timeout task
// in the queue. Deal with those here. We do the content response first
// with preventDefault=false, and then we run the timeout task which
// "loses the race" and does nothing.
apzc->ContentReceivedTouch(false);
mcc->CheckHasDelayedTask();
mcc->ClearDelayedTask();
apzc->ContentReceivedTouch(true);
mcc->RunDelayedTask();
time += 1000;
// Finally, simulate lifting the finger. Since the long-press wasn't
// prevent-defaulted, we should get a long-tap-up event.
check.Call("preHandleLongTapUp");
status = ApzcUp(apzc, 10, 10, time);
EXPECT_EQ(nsEventStatus_eIgnore, status);
// To get a LongTapUp event, we must kick APZC to flush its event queue. This
// would normally happen if we had a (Tab|RenderFrame)(Parent|Child)
// mechanism.
check.Call("preHandleLongTapUp");
apzc->ContentReceivedTouch(false);
check.Call("postHandleLongTapUp");
apzc->AssertStateIsReset();
@ -1101,12 +1115,23 @@ protected:
int time = 0;
nsEventStatus status = ApzcDown(apzc, touchX, touchStartY, time);
EXPECT_EQ(nsEventStatus_eConsumeNoDefault, status);
if (gfxPrefs::TouchActionEnabled()) {
// If touch-action is enabled, then the event is queued until the
// allowed touch behavior is set.
EXPECT_EQ(nsEventStatus_eConsumeDoDefault, status);
} else {
// Otherwise, it is processed immediately.
EXPECT_EQ(nsEventStatus_eConsumeNoDefault, status);
}
// SetAllowedTouchBehavior() must be called after sending touch-start.
nsTArray<uint32_t> allowedTouchBehaviors;
allowedTouchBehaviors.AppendElement(aBehavior);
apzc->SetAllowedTouchBehavior(allowedTouchBehaviors);
if (gfxPrefs::TouchActionEnabled()) {
// SetAllowedTouchBehavior() must be called after sending touch-start.
nsTArray<uint32_t> allowedTouchBehaviors;
allowedTouchBehaviors.AppendElement(aBehavior);
apzc->SetAllowedTouchBehavior(allowedTouchBehaviors);
}
// Have content "respond" to the touchstart
apzc->ContentReceivedTouch(false);
MockFunction<void(std::string checkPointName)> check;
@ -1127,11 +1152,15 @@ protected:
// 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.
mcc->ClearDelayedTask();
// There should be a TimeoutContentResponse task in the queue still,
// waiting for the response from the longtap event dispatched above.
// Send the signal that content has handled the long-tap, and then run
// the timeout task (it will be a no-op because the content "wins" the
// race. This takes the place of the "contextmenu" event.
apzc->ContentReceivedTouch(true);
mcc->CheckHasDelayedTask();
mcc->RunDelayedTask();
time += 1000;
@ -1144,11 +1173,6 @@ protected:
status = ApzcUp(apzc, touchX, touchEndY, time);
EXPECT_EQ(nsEventStatus_eIgnore, status);
// Flush the event queue. Once the "contextmenu" event is handled, any touch
// events that come from the same series of start->n*move->end events should
// be discarded, even if only the "contextmenu" event is preventDefaulted.
apzc->ContentReceivedTouch(false);
ScreenPoint pointOut;
ViewTransform viewTransformOut;
apzc->SampleContentTransformForFrame(testStartTime, &viewTransformOut, pointOut);