Gecko work for
bug 823619: Dispatch spec-compliant mouse events when touch events are available. r=cjones,jlebar,schien,vingtetun a=blocking-basecamp
This is a rollowup of the following patches
Bug 823619, part 1: Make TabChild dispatch spec-compliant compat mouse events. r=mwu
Bug 823619, part 2: Use touch event for scrolling if available. r=cjones,schien,vingtetun a=blocking-basecamp
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -277,20 +277,24 @@ pref("widget.ime.android.landscape_fulls
pref("widget.ime.android.fullscreen_threshold", 250); // in hundreths of inches
// optimize images' memory usage
pref("image.mem.decodeondraw", true);
pref("content.image.allow_locking", true);
pref("image.mem.min_discard_timeout_ms", 10000);
pref("image.mem.max_decoded_image_kb", 5120); /* 5MB */
+// XXX this isn't a good check for "are touch events supported", but
+// we don't really have a better one at the moment.
+#ifdef MOZ_WIDGET_GONK
// enable touch events interfaces
pref("dom.w3c_touch_events.enabled", 1);
pref("dom.w3c_touch_events.safetyX", 0); // escape borders in units of 1/240"
pref("dom.w3c_touch_events.safetyY", 120); // escape borders in units of 1/240"
+#endif
#ifdef MOZ_SAFE_BROWSING
// Safe browsing does nothing unless this pref is set
pref("browser.safebrowsing.enabled", true);
// Prevent loading of pages identified as malware
pref("browser.safebrowsing.malware.enabled", true);
--- a/dom/browser-element/BrowserElementScrolling.js
+++ b/dom/browser-element/BrowserElementScrolling.js
@@ -1,124 +1,276 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sw=2 sts=2 et: */
/* 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/. */
const ContentPanning = {
+ // Are we listening to touch or mouse events?
+ watchedEventsType: '',
+
+ // Are mouse events being delivered to this content along with touch
+ // events, in violation of spec?
+ hybridEvents: false,
+
init: function cp_init() {
- ['mousedown', 'mouseup', 'mousemove'].forEach(function(type) {
+ var events;
+ try {
+ content.document.createEvent('TouchEvent');
+ events = ['touchstart', 'touchend', 'touchmove'];
+ this.watchedEventsType = 'touch';
+#ifdef MOZ_WIDGET_GONK
+ // The gonk widget backend does not deliver mouse events per
+ // spec. Third-party content isn't exposed to this behavior,
+ // but that behavior creates some extra work for us here.
+ let appInfo = Cc["@mozilla.org/xre/app-info;1"];
+ let isParentProcess =
+ !appInfo || appInfo.getService(Ci.nsIXULRuntime)
+ .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+ this.hybridEvents = isParentProcess;
+#endif
+ } catch(e) {
+ // Touch events aren't supported, so fall back on mouse.
+ events = ['mousedown', 'mouseup', 'mousemove'];
+ this.watchedEventsType = 'mouse';
+ }
+ events.forEach(function(type) {
addEventListener(type, ContentPanning, false);
});
addMessageListener("Viewport:Change", this._recvViewportChange.bind(this));
addMessageListener("Gesture:DoubleTap", this._recvDoubleTap.bind(this));
},
handleEvent: function cp_handleEvent(evt) {
+ if (evt.defaultPrevented)
+ return;
+
switch (evt.type) {
case 'mousedown':
+ case 'touchstart':
this.onTouchStart(evt);
break;
case 'mousemove':
+ case 'touchmove':
this.onTouchMove(evt);
break;
case 'mouseup':
+ case 'touchend':
this.onTouchEnd(evt);
break;
case 'click':
evt.stopPropagation();
evt.preventDefault();
let target = evt.target;
let view = target.ownerDocument ? target.ownerDocument.defaultView
: target;
view.removeEventListener('click', this, true, true);
break;
}
},
position: new Point(0 , 0),
+ findPrimaryPointer: function cp_findPrimaryPointer(touches) {
+ if (!('primaryPointerId' in this))
+ return null;
+
+ for (let i = 0; i < touches.length; i++) {
+ if (touches[i].identifier === this.primaryPointerId) {
+ return touches[i];
+ }
+ }
+ return null;
+ },
+
onTouchStart: function cp_onTouchStart(evt) {
+ let screenX, screenY;
+ if (this.watchedEventsType == 'touch') {
+ if ('primaryPointerId' in this) {
+ return;
+ }
+
+ let firstTouch = evt.changedTouches[0];
+ this.primaryPointerId = firstTouch.identifier;
+ this.pointerDownTarget = firstTouch.target;
+ screenX = firstTouch.screenX;
+ screenY = firstTouch.screenY;
+ } else {
+ this.pointerDownTarget = evt.target;
+ screenX = evt.screenX;
+ screenY = evt.screenY;
+ }
this.dragging = true;
this.panning = false;
let oldTarget = this.target;
- [this.target, this.scrollCallback] = this.getPannable(evt.target);
+ [this.target, this.scrollCallback] = this.getPannable(this.pointerDownTarget);
// If we found a target, that means we have found a scrollable subframe. In
// this case, and if we are using async panning and zooming on the parent
// frame, inform the pan/zoom controller that it should not attempt to
// handle any touch events it gets until the next batch (meaning the next
// time we get a touch end).
- if (this.target != null && ContentPanning._asyncPanZoomForViewportFrame) {
+ if (this.target != null && this._asyncPanZoomForViewportFrame) {
+ this.detectingScrolling = true;
var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
- os.notifyObservers(docShell, 'cancel-default-pan-zoom', null);
+ os.notifyObservers(docShell, 'detect-scrollable-subframe', null);
+ }
+
+ // If we have a pointer down target and we're not async
+ // pan/zooming, we may need to fill in for EventStateManager in
+ // setting the active state on the target element. Set a timer to
+ // ensure the pointer-down target is active. (If it's already
+ // active, the timer is a no-op.)
+ if (this.pointerDownTarget !== null && !this.detectingScrolling) {
+ // If there's no possibility this is a drag/pan, activate now.
+ // Otherwise wait a little bit to see if the gesture isn't a
+ // tap.
+ if (this.target === null) {
+ this.notify(this._activationTimer);
+ } else {
+ this._activationTimer.initWithCallback(this,
+ this._activationDelayMs,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+ }
}
// If there is a pan animation running (from a previous pan gesture) and
// the user touch back the screen, stop this animation immediatly and
// prevent the possible click action if the touch happens on the same
// target.
this.preventNextClick = false;
if (KineticPanning.active) {
KineticPanning.stop();
if (oldTarget && oldTarget == this.target)
this.preventNextClick = true;
}
-
- this.position.set(evt.screenX, evt.screenY);
+ this.position.set(screenX, screenY);
KineticPanning.record(new Point(0, 0), evt.timeStamp);
},
onTouchEnd: function cp_onTouchEnd(evt) {
- if (!this.dragging)
+ let touch = null;
+ if (!this.dragging ||
+ (this.watchedEventsType == 'touch' &&
+ !(touch = this.findPrimaryPointer(evt.changedTouches)))) {
return;
- this.dragging = false;
+ }
- this.onTouchMove(evt);
+ // !isPan() and evt.detail should always give the same answer here
+ // since they use the same heuristics, but use the native gecko
+ // computation when possible.
+ //
+ // NB: when we're using touch events, then !KineticPanning.isPan()
+ // => this.panning, so we'll never attempt to block the click
+ // event. That's OK however, because we won't fire a synthetic
+ // click when we're using touch events and this touch series
+ // wasn't a "tap" gesture.
+ let click = (this.watchedEventsType == 'mouse') ?
+ evt.detail : !KineticPanning.isPan();
+ // Additionally, if we're seeing non-compliant hybrid events, a
+ // "real" click will be generated if we started and ended on the
+ // same element.
+ if (this.hybridEvents) {
+ let target =
+ content.document.elementFromPoint(touch.clientX, touch.clientY);
+ click |= (target === this.pointerDownTarget);
+ }
- let click = evt.detail;
if (this.target && click && (this.panning || this.preventNextClick)) {
let target = this.target;
let view = target.ownerDocument ? target.ownerDocument.defaultView
: target;
view.addEventListener('click', this, true, true);
}
- if (this.panning)
+ this._resetActive();
+ this.dragging = false;
+ this.detectingScrolling = false;
+ delete this.primaryPointerId;
+ this._activationTimer.cancel();
+
+ if (this.panning) {
KineticPanning.start(this);
+ }
},
+ // True when there's an async pan-zoom controll watching the
+ // outermost scrollable frame, and we're waiting to see whether
+ // we're going to take over from it and synchronously scroll an
+ // inner scrollable frame.
+ detectingScrolling: false,
+
onTouchMove: function cp_onTouchMove(evt) {
- if (!this.dragging || !this.scrollCallback)
+ if (!this.dragging)
return;
+ let screenX, screenY;
+ if (this.watchedEventsType == 'touch') {
+ let primaryTouch = this.findPrimaryPointer(evt.changedTouches);
+ if (evt.touches.length > 1 || !primaryTouch)
+ return;
+ screenX = primaryTouch.screenX;
+ screenY = primaryTouch.screenY;
+ } else {
+ screenX = evt.screenX;
+ screenY = evt.screenY;
+ }
+
let current = this.position;
- let delta = new Point(evt.screenX - current.x, evt.screenY - current.y);
- current.set(evt.screenX, evt.screenY);
+ let delta = new Point(screenX - current.x, screenY - current.y);
+ current.set(screenX, screenY);
KineticPanning.record(delta, evt.timeStamp);
+
+ // There's no possibility of us panning anything.
+ if (!this.scrollCallback) {
+ return;
+ }
+
+ let isPan = KineticPanning.isPan();
+ if (this.detectingScrolling) {
+ this.detectingScrolling = false;
+ // Stop async-pan-zooming if the user is panning the subframe.
+ if (isPan) {
+ // We're going to drive synchronously scrolling an inner frame.
+ Services.obs.notifyObservers(docShell, 'cancel-default-pan-zoom', null);
+ } else {
+ // Let AsyncPanZoomController handle the scrolling gesture.
+ this.scrollCallback = null;
+ return;
+ }
+ }
+
this.scrollCallback(delta.scale(-1));
// If a pan action happens, cancel the active state of the
// current target.
- if (!this.panning && KineticPanning.isPan()) {
+ if (!this.panning && isPan) {
this.panning = true;
this._resetActive();
+ this._activationTimer.cancel();
}
- evt.stopPropagation();
- evt.preventDefault();
+
+ if (this.panning) {
+ evt.stopPropagation();
+ evt.preventDefault();
+ }
},
+ // nsITimerCallback
+ notify: function cp_notify(timer) {
+ this._setActive(this.pointerDownTarget);
+ },
onKineticBegin: function cp_onKineticBegin(evt) {
},
onKineticPan: function cp_onKineticPan(delta) {
return !this.scrollCallback(delta);
},
@@ -244,21 +396,37 @@ const ContentPanning = {
},
get _domUtils() {
delete this._domUtils;
return this._domUtils = Cc['@mozilla.org/inspector/dom-utils;1']
.getService(Ci.inIDOMUtils);
},
- _resetActive: function cp_resetActive() {
- let root = this.target.ownerDocument || this.target.document;
+ get _activationTimer() {
+ delete this._activationTimer;
+ return this._activationTimer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ },
+ get _activationDelayMs() {
+ let delay = Services.prefs.getIntPref('ui.touch_activation.delay_ms');
+ delete this._activationDelayMs;
+ return this._activationDelayMs = delay;
+ },
+
+ _resetActive: function cp_resetActive() {
+ let elt = this.target || this.pointerDownTarget;
+ let root = elt.ownerDocument || elt.document;
+ this._setActive(root.documentElement);
+ },
+
+ _setActive: function cp_setActive(elt) {
const kStateActive = 0x00000001;
- this._domUtils.setContentState(root.documentElement, kStateActive);
+ this._domUtils.setContentState(elt, kStateActive);
},
get _asyncPanZoomForViewportFrame() {
return docShell.asyncPanZoomEnabled;
},
_recvViewportChange: function(data) {
let metrics = data.json;
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -91,16 +91,17 @@ using namespace mozilla::widget;
NS_IMPL_ISUPPORTS1(ContentListener, nsIDOMEventListener)
static const nsIntSize kDefaultViewportSize(980, 480);
static const char CANCEL_DEFAULT_PAN_ZOOM[] = "cancel-default-pan-zoom";
static const char BROWSER_ZOOM_TO_RECT[] = "browser-zoom-to-rect";
static const char BEFORE_FIRST_PAINT[] = "before-first-paint";
+static const char DETECT_SCROLLABLE_SUBFRAME[] = "detect-scrollable-subframe";
NS_IMETHODIMP
ContentListener::HandleEvent(nsIDOMEvent* aEvent)
{
RemoteDOMEvent remoteEvent;
remoteEvent.mEvent = do_QueryInterface(aEvent);
NS_ENSURE_STATE(remoteEvent.mEvent);
mTabChild->SendEvent(remoteEvent);
@@ -156,16 +157,18 @@ TabChild::Create(const TabContext &aCont
TabChild::TabChild(const TabContext& aContext, uint32_t aChromeFlags)
: TabContext(aContext)
, mRemoteFrame(nullptr)
, mTabChildGlobal(nullptr)
, mChromeFlags(aChromeFlags)
, mOuterRect(0, 0, 0, 0)
, mInnerSize(0, 0)
+ , mActivePointerId(-1)
+ , mTapHoldTimer(nullptr)
, mOldViewportWidth(0.0f)
, mLastBackgroundColor(NS_RGB(255, 255, 255))
, mDidFakeShow(false)
, mNotified(false)
, mContentDocumentIsDisplayed(false)
, mTriedBrowserInit(false)
, mOrientation(eScreenOrientation_PortraitPrimary)
{
@@ -237,16 +240,22 @@ TabChild::Observe(nsISupports *aSubject,
AsyncPanZoomController::CalculateResolution(mLastMetrics);
mLastMetrics.mScrollOffset = gfx::Point(0, 0);
utils->SetResolution(mLastMetrics.mResolution.width,
mLastMetrics.mResolution.height);
HandlePossibleViewportChange();
}
}
+ } else if (!strcmp(aTopic, DETECT_SCROLLABLE_SUBFRAME)) {
+ nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(aSubject));
+ nsCOMPtr<nsITabChild> tabChild(GetTabChildFrom(docShell));
+ if (tabChild == this) {
+ mRemoteFrame->DetectScrollableSubframe();
+ }
}
return NS_OK;
}
NS_IMETHODIMP
TabChild::OnStateChange(nsIWebProgress* aWebProgress,
nsIRequest* aRequest,
@@ -1340,67 +1349,160 @@ bool
TabChild::RecvMouseWheelEvent(const WheelEvent& event)
{
WheelEvent localEvent(event);
DispatchWidgetEvent(localEvent);
return true;
}
void
-TabChild::DispatchSynthesizedMouseEvent(const nsTouchEvent& aEvent)
+TabChild::DispatchSynthesizedMouseEvent(uint32_t aMsg, uint64_t aTime,
+ const nsIntPoint& aRefPoint)
{
- // Synthesize a phony mouse event.
- uint32_t msg;
- switch (aEvent.message) {
- case NS_TOUCH_START:
- msg = NS_MOUSE_BUTTON_DOWN;
- break;
- case NS_TOUCH_MOVE:
- msg = NS_MOUSE_MOVE;
- break;
- case NS_TOUCH_END:
- case NS_TOUCH_CANCEL:
- msg = NS_MOUSE_BUTTON_UP;
- break;
- default:
- MOZ_NOT_REACHED("Unknown touch event message");
- }
+ MOZ_ASSERT(aMsg == NS_MOUSE_MOVE || aMsg == NS_MOUSE_BUTTON_DOWN ||
+ aMsg == NS_MOUSE_BUTTON_UP);
- nsIntPoint refPoint(0, 0);
- if (aEvent.touches.Length()) {
- refPoint = aEvent.touches[0]->mRefPoint;
- }
-
- nsMouseEvent event(true, msg, NULL,
+ nsMouseEvent event(true, aMsg, NULL,
nsMouseEvent::eReal, nsMouseEvent::eNormal);
- event.refPoint = refPoint;
- event.time = aEvent.time;
+ event.refPoint = aRefPoint;
+ event.time = aTime;
event.button = nsMouseEvent::eLeftButton;
- if (msg != NS_MOUSE_MOVE) {
+ if (aMsg != NS_MOUSE_MOVE) {
event.clickCount = 1;
}
DispatchWidgetEvent(event);
}
+static nsDOMTouch*
+GetTouchForIdentifier(const nsTouchEvent& aEvent, int32_t aId)
+{
+ for (uint32_t i = 0; i < aEvent.touches.Length(); ++i) {
+ nsDOMTouch* touch = static_cast<nsDOMTouch*>(aEvent.touches[i].get());
+ if (touch->mIdentifier == aId) {
+ return touch;
+ }
+ }
+ return nullptr;
+}
+
+void
+TabChild::UpdateTapState(const nsTouchEvent& aEvent, nsEventStatus aStatus)
+{
+ static bool sHavePrefs;
+ static bool sClickHoldContextMenusEnabled;
+ static nsIntSize sDragThreshold;
+ static int32_t sContextMenuDelayMs;
+ if (!sHavePrefs) {
+ sHavePrefs = true;
+ Preferences::AddBoolVarCache(&sClickHoldContextMenusEnabled,
+ "ui.click_hold_context_menus", true);
+ Preferences::AddIntVarCache(&sDragThreshold.width,
+ "ui.dragThresholdX", 25);
+ Preferences::AddIntVarCache(&sDragThreshold.height,
+ "ui.dragThresholdY", 25);
+ Preferences::AddIntVarCache(&sContextMenuDelayMs,
+ "ui.click_hold_context_menus.delay", 500);
+ }
+
+ bool currentlyTrackingTouch = (mActivePointerId >= 0);
+ if (aEvent.message == NS_TOUCH_START) {
+ if (currentlyTrackingTouch || aEvent.touches.Length() > 1) {
+ // We're tracking a possible tap for another point, or we saw a
+ // touchstart for a later pointer after we canceled tracking of
+ // the first point. Ignore this one.
+ return;
+ }
+ if (aStatus == nsEventStatus_eConsumeNoDefault ||
+ nsIPresShell::gPreventMouseEvents) {
+ return;
+ }
+
+ nsDOMTouch* touch = static_cast<nsDOMTouch*>(aEvent.touches[0].get());
+ mGestureDownPoint = touch->mRefPoint;
+ mActivePointerId = touch->mIdentifier;
+ if (sClickHoldContextMenusEnabled) {
+ MOZ_ASSERT(!mTapHoldTimer);
+ mTapHoldTimer = NewRunnableMethod(this,
+ &TabChild::FireContextMenuEvent);
+ MessageLoop::current()->PostDelayedTask(FROM_HERE, mTapHoldTimer,
+ sContextMenuDelayMs);
+ }
+ return;
+ }
+
+ // If we're not tracking a touch or this event doesn't include the
+ // one we care about, bail.
+ if (!currentlyTrackingTouch) {
+ return;
+ }
+ nsDOMTouch* trackedTouch = GetTouchForIdentifier(aEvent, mActivePointerId);
+ if (!trackedTouch) {
+ return;
+ }
+
+ nsIntPoint currentPoint = trackedTouch->mRefPoint;
+ int64_t time = aEvent.time;
+ switch (aEvent.message) {
+ case NS_TOUCH_MOVE:
+ if (abs(currentPoint.x - mGestureDownPoint.x) > sDragThreshold.width ||
+ abs(currentPoint.y - mGestureDownPoint.y) > sDragThreshold.height) {
+ CancelTapTracking();
+ }
+ return;
+
+ case NS_TOUCH_END:
+ if (!nsIPresShell::gPreventMouseEvents) {
+ DispatchSynthesizedMouseEvent(NS_MOUSE_MOVE, time, currentPoint);
+ DispatchSynthesizedMouseEvent(NS_MOUSE_BUTTON_DOWN, time, currentPoint);
+ DispatchSynthesizedMouseEvent(NS_MOUSE_BUTTON_UP, time, currentPoint);
+ }
+ // fall through
+ case NS_TOUCH_CANCEL:
+ CancelTapTracking();
+ return;
+
+ default:
+ NS_WARNING("Unknown touch event type");
+ }
+}
+
+void
+TabChild::FireContextMenuEvent()
+{
+ MOZ_ASSERT(mTapHoldTimer && mActivePointerId >= 0);
+ RecvHandleLongTap(mGestureDownPoint);
+ CancelTapTracking();
+}
+
+void
+TabChild::CancelTapTracking()
+{
+ mActivePointerId = -1;
+ if (mTapHoldTimer) {
+ mTapHoldTimer->Cancel();
+ }
+ mTapHoldTimer = nullptr;
+}
+
bool
TabChild::RecvRealTouchEvent(const nsTouchEvent& aEvent)
{
nsTouchEvent localEvent(aEvent);
nsEventStatus status = DispatchWidgetEvent(localEvent);
if (IsAsyncPanZoomEnabled()) {
nsCOMPtr<nsPIDOMWindow> outerWindow = do_GetInterface(mWebNav);
nsCOMPtr<nsPIDOMWindow> innerWindow = outerWindow->GetCurrentInnerWindow();
if (innerWindow && innerWindow->HasTouchEventListeners()) {
SendContentReceivedTouch(nsIPresShell::gPreventMouseEvents);
}
- } else if (status != nsEventStatus_eConsumeNoDefault) {
- DispatchSynthesizedMouseEvent(aEvent);
+ } else {
+ UpdateTapState(aEvent, status);
}
return true;
}
bool
TabChild::RecvRealTouchMoveEvent(const nsTouchEvent& aEvent)
{
@@ -1671,16 +1773,17 @@ TabChild::RecvDestroy()
}
nsCOMPtr<nsIObserverService> observerService =
do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
observerService->RemoveObserver(this, CANCEL_DEFAULT_PAN_ZOOM);
observerService->RemoveObserver(this, BROWSER_ZOOM_TO_RECT);
observerService->RemoveObserver(this, BEFORE_FIRST_PAINT);
+ observerService->RemoveObserver(this, DETECT_SCROLLABLE_SUBFRAME);
const InfallibleTArray<PIndexedDBChild*>& idbActors =
ManagedPIndexedDBChild();
for (uint32_t i = 0; i < idbActors.Length(); ++i) {
static_cast<IndexedDBChild*>(idbActors[i])->Disconnect();
}
// XXX what other code in ~TabChild() should we be running here?
@@ -1806,16 +1909,19 @@ TabChild::InitRenderingState()
CANCEL_DEFAULT_PAN_ZOOM,
false);
observerService->AddObserver(this,
BROWSER_ZOOM_TO_RECT,
false);
observerService->AddObserver(this,
BEFORE_FIRST_PAINT,
false);
+ observerService->AddObserver(this,
+ DETECT_SCROLLABLE_SUBFRAME,
+ false);
}
return true;
}
void
TabChild::SetBackgroundColor(const nscolor& aColor)
{
--- a/dom/ipc/TabChild.h
+++ b/dom/ipc/TabChild.h
@@ -379,18 +379,28 @@ private:
// Wraps up a JSON object as a structured clone and sends it to the browser
// chrome script.
//
// XXX/bug 780335: Do the work the browser chrome script does in C++ instead
// so we don't need things like this.
void DispatchMessageManagerMessage(const nsAString& aMessageName,
const nsACString& aJSONData);
- // Sends a simulated mouse event from a touch event for compatibility.
- void DispatchSynthesizedMouseEvent(const nsTouchEvent& aEvent);
+ void DispatchSynthesizedMouseEvent(uint32_t aMsg, uint64_t aTime,
+ const nsIntPoint& aRefPoint);
+
+ // These methods are used for tracking synthetic mouse events
+ // dispatched for compatibility. On each touch event, we
+ // UpdateTapState(). If we've detected that the current gesture
+ // isn't a tap, then we CancelTapTracking(). In the meantime, we
+ // may detect a context-menu event, and if so we
+ // FireContextMenuEvent().
+ void FireContextMenuEvent();
+ void CancelTapTracking();
+ void UpdateTapState(const nsTouchEvent& aEvent, nsEventStatus aStatus);
nsresult
BrowserFrameProvideWindow(nsIDOMWindow* aOpener,
nsIURI* aURI,
const nsAString& aName,
const nsACString& aFeatures,
bool* aWindowIsNew,
nsIDOMWindow** aReturn);
@@ -406,16 +416,25 @@ private:
nsCOMPtr<nsIWidget> mWidget;
nsCOMPtr<nsIURI> mLastURI;
FrameMetrics mLastMetrics;
RenderFrameChild* mRemoteFrame;
nsRefPtr<TabChildGlobal> mTabChildGlobal;
uint32_t mChromeFlags;
nsIntRect mOuterRect;
nsIntSize mInnerSize;
+ // When we're tracking a possible tap gesture, this is the "down"
+ // point of the touchstart.
+ nsIntPoint mGestureDownPoint;
+ // The touch identifier of the active gesture.
+ int32_t mActivePointerId;
+ // A timer task that fires if the tap-hold timeout is exceeded by
+ // the touch we're tracking. That is, if touchend or a touchmove
+ // that exceeds the gesture threshold doesn't happen.
+ CancelableTask* mTapHoldTimer;
float mOldViewportWidth;
nscolor mLastBackgroundColor;
ScrollingBehavior mScrolling;
bool mDidFakeShow;
bool mNotified;
bool mContentDocumentIsDisplayed;
bool mTriedBrowserInit;
nsString mAppType;
--- a/gfx/layers/ipc/AsyncPanZoomController.cpp
+++ b/gfx/layers/ipc/AsyncPanZoomController.cpp
@@ -242,16 +242,36 @@ nsEventStatus AsyncPanZoomController::Ha
nsEventStatus rv = nsEventStatus_eIgnore;
if (mGestureEventListener && !mDisableNextTouchBatch) {
rv = mGestureEventListener->HandleInputEvent(aEvent);
if (rv == nsEventStatus_eConsumeNoDefault)
return rv;
}
+ if (mDelayPanning && aEvent.mInputType == MULTITOUCH_INPUT) {
+ const MultiTouchInput& multiTouchInput = aEvent.AsMultiTouchInput();
+ if (multiTouchInput.mType == MultiTouchInput::MULTITOUCH_MOVE) {
+ // Let BrowserElementScrolling perform panning gesture first.
+ SetState(WAITING_LISTENERS);
+ mTouchQueue.AppendElement(multiTouchInput);
+
+ if (!mTouchListenerTimeoutTask) {
+ mTouchListenerTimeoutTask =
+ NewRunnableMethod(this, &AsyncPanZoomController::TimeoutTouchListeners);
+
+ MessageLoop::current()->PostDelayedTask(
+ FROM_HERE,
+ mTouchListenerTimeoutTask,
+ TOUCH_LISTENER_TIMEOUT);
+ }
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ }
+
switch (aEvent.mInputType) {
case MULTITOUCH_INPUT: {
const MultiTouchInput& multiTouchInput = aEvent.AsMultiTouchInput();
switch (multiTouchInput.mType) {
case MultiTouchInput::MULTITOUCH_START: rv = OnTouchStart(multiTouchInput); break;
case MultiTouchInput::MULTITOUCH_MOVE: rv = OnTouchMove(multiTouchInput); break;
case MultiTouchInput::MULTITOUCH_END: rv = OnTouchEnd(multiTouchInput); break;
case MultiTouchInput::MULTITOUCH_CANCEL: rv = OnTouchCancel(multiTouchInput); break;
@@ -1141,16 +1161,17 @@ bool AsyncPanZoomController::SampleConte
void AsyncPanZoomController::NotifyLayersUpdated(const FrameMetrics& aViewportFrame, bool aIsFirstPaint) {
MonitorAutoLock monitor(mMonitor);
mPaintThrottler.TaskComplete();
mLastContentPaintMetrics = aViewportFrame;
+ mFrameMetrics.mMayHaveTouchListeners = aViewportFrame.mMayHaveTouchListeners;
if (mWaitingForContentToPaint) {
// Remove the oldest sample we have if adding a new sample takes us over our
// desired number of samples.
if (mPreviousPaintDurations.Length() >= NUM_PAINT_DURATION_SAMPLES) {
mPreviousPaintDurations.RemoveElementAt(0);
}
mPreviousPaintDurations.AppendElement(
@@ -1234,16 +1255,20 @@ void AsyncPanZoomController::UpdateCompo
void AsyncPanZoomController::CancelDefaultPanZoom() {
mDisableNextTouchBatch = true;
if (mGestureEventListener) {
mGestureEventListener->CancelGesture();
}
}
+void AsyncPanZoomController::DetectScrollableSubframe() {
+ mDelayPanning = true;
+}
+
void AsyncPanZoomController::ZoomToRect(const gfxRect& aRect) {
gfx::Rect zoomToRect(gfx::Rect(aRect.x, aRect.y, aRect.width, aRect.height));
SetState(ANIMATING_ZOOM);
{
MonitorAutoLock mon(mMonitor);
@@ -1320,34 +1345,43 @@ void AsyncPanZoomController::ZoomToRect(
mAnimationStartTime = TimeStamp::Now();
ScheduleComposite();
}
}
void AsyncPanZoomController::ContentReceivedTouch(bool aPreventDefault) {
- if (!mFrameMetrics.mMayHaveTouchListeners) {
+ if (!mFrameMetrics.mMayHaveTouchListeners && !mDelayPanning) {
mTouchQueue.Clear();
return;
}
if (mTouchListenerTimeoutTask) {
mTouchListenerTimeoutTask->Cancel();
mTouchListenerTimeoutTask = nullptr;
}
if (mState == WAITING_LISTENERS) {
if (!aPreventDefault) {
- SetState(NOTHING);
+ // Delayed scrolling gesture is pending at TOUCHING state.
+ if (mDelayPanning) {
+ SetState(TOUCHING);
+ } else {
+ SetState(NOTHING);
+ }
}
mHandlingTouchQueue = true;
while (!mTouchQueue.IsEmpty()) {
+ // we need to reset mDelayPanning before handling scrolling gesture.
+ if (mTouchQueue[0].mType == MultiTouchInput::MULTITOUCH_MOVE) {
+ mDelayPanning = false;
+ }
if (!aPreventDefault) {
HandleInputEvent(mTouchQueue[0]);
}
if (mTouchQueue[0].mType == MultiTouchInput::MULTITOUCH_END ||
mTouchQueue[0].mType == MultiTouchInput::MULTITOUCH_CANCEL) {
mTouchQueue.RemoveElementAt(0);
break;
--- a/gfx/layers/ipc/AsyncPanZoomController.h
+++ b/gfx/layers/ipc/AsyncPanZoomController.h
@@ -103,26 +103,32 @@ public:
* the frame this is tied to during composition onto, in device pixels. In
* general, this will just be:
* { x = 0, y = 0, width = surface.width, height = surface.height }, however
* there is no hard requirement for this.
*/
void UpdateCompositionBounds(const nsIntRect& aCompositionBounds);
/**
- * We have found a scrollable subframe, so disable our machinery until we hit
+ * We are scrolling a subframe, so disable our machinery until we hit
* a touch end or a new touch start. This prevents us from accidentally
* panning both the subframe and the parent frame.
*
* XXX/bug 775452: We should eventually be supporting async scrollable
* subframes.
*/
void CancelDefaultPanZoom();
/**
+ * We have found a scrollable subframe, so we need to delay the scrolling
+ * gesture executed and let subframe do the scrolling first.
+ */
+ void DetectScrollableSubframe();
+
+ /**
* Kicks an animation to zoom to a rect. This may be either a zoom out or zoom
* in. The actual animation is done on the compositor thread after being set
* up. |aRect| must be given in CSS pixels, relative to the document.
*/
void ZoomToRect(const gfxRect& aRect);
/**
* If we have touch listeners, this should always be called when we know
@@ -579,15 +585,21 @@ private:
bool mDisableNextTouchBatch;
// 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;
+ // Flag used to determine whether or not we should try scrolling by
+ // BrowserElementScrolling first. If set, we delay delivering
+ // touchmove events to GestureListener until BrowserElementScrolling
+ // decides whether it wants to handle panning for this touch series.
+ bool mDelayPanning;
+
friend class Axis;
};
}
}
#endif // mozilla_layers_PanZoomController_h
--- a/layout/ipc/PRenderFrame.ipdl
+++ b/layout/ipc/PRenderFrame.ipdl
@@ -37,16 +37,17 @@ parent:
* |id| is set to 0 in the "direct" case, and to a whole number
* in the "indirect" case.
*/
async PLayers();
async NotifyCompositorTransaction();
async CancelDefaultPanZoom();
+ async DetectScrollableSubframe();
async __delete__();
state EMPTY_OR_DIRECT_COMPOSITOR:
recv PLayers goto HAVE_CONTENT;
recv NotifyCompositorTransaction goto EMPTY_OR_DIRECT_COMPOSITOR;
recv __delete__;
--- a/layout/ipc/RenderFrameChild.cpp
+++ b/layout/ipc/RenderFrameChild.cpp
@@ -33,16 +33,22 @@ RenderFrameChild::Destroy()
}
void
RenderFrameChild::CancelDefaultPanZoom()
{
SendCancelDefaultPanZoom();
}
+void
+RenderFrameChild::DetectScrollableSubframe()
+{
+ SendDetectScrollableSubframe();
+}
+
PLayersChild*
RenderFrameChild::AllocPLayers()
{
return new ShadowLayersChild();
}
bool
RenderFrameChild::DeallocPLayers(PLayersChild* aLayers)
--- a/layout/ipc/RenderFrameChild.h
+++ b/layout/ipc/RenderFrameChild.h
@@ -15,16 +15,17 @@ namespace layout {
class RenderFrameChild : public PRenderFrameChild
{
public:
RenderFrameChild() {}
virtual ~RenderFrameChild() {}
void CancelDefaultPanZoom();
+ void DetectScrollableSubframe();
void Destroy();
protected:
virtual PLayersChild* AllocPLayers() MOZ_OVERRIDE;
virtual bool DeallocPLayers(PLayersChild* aLayers) MOZ_OVERRIDE;
};
--- a/layout/ipc/RenderFrameParent.cpp
+++ b/layout/ipc/RenderFrameParent.cpp
@@ -824,16 +824,25 @@ bool
RenderFrameParent::RecvCancelDefaultPanZoom()
{
if (mPanZoomController) {
mPanZoomController->CancelDefaultPanZoom();
}
return true;
}
+bool
+RenderFrameParent::RecvDetectScrollableSubframe()
+{
+ if (mPanZoomController) {
+ mPanZoomController->DetectScrollableSubframe();
+ }
+ return true;
+}
+
PLayersParent*
RenderFrameParent::AllocPLayers()
{
if (!mFrameLoader || mFrameLoaderDestroyed) {
return nullptr;
}
nsRefPtr<LayerManager> lm = GetFrom(mFrameLoader);
return new ShadowLayersParent(lm->AsShadowManager(), this, 0);
--- a/layout/ipc/RenderFrameParent.h
+++ b/layout/ipc/RenderFrameParent.h
@@ -104,16 +104,17 @@ public:
void UpdateZoomConstraints(bool aAllowZoom, float aMinZoom, float aMaxZoom);
protected:
void ActorDestroy(ActorDestroyReason why) MOZ_OVERRIDE;
virtual bool RecvNotifyCompositorTransaction() MOZ_OVERRIDE;
virtual bool RecvCancelDefaultPanZoom() MOZ_OVERRIDE;
+ virtual bool RecvDetectScrollableSubframe() MOZ_OVERRIDE;
virtual PLayersParent* AllocPLayers() MOZ_OVERRIDE;
virtual bool DeallocPLayers(PLayersParent* aLayers) MOZ_OVERRIDE;
private:
void BuildViewMap();
void TriggerRepaint();
void DispatchEventForPanZoomController(const InputEvent& aEvent);
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -3870,10 +3870,16 @@ pref("network.activity.blipIntervalMilli
// the scripts to interfere with each other. A restart is required for this
// to take effect.
pref("jsloader.reuseGlobal", false);
// When we're asked to take a screenshot, don't wait more than 2000ms for the
// event loop to become idle before actually taking the screenshot.
pref("dom.browserElement.maxScreenshotDelayMS", 2000);
+// If the user puts a finger down on an element and we think the user
+// might be executing a pan gesture, how long do we wait before
+// tentatively deciding the gesture is actually a tap and activating
+// the target element?
+pref("ui.touch_activation.delay_ms", 50);
+
// Whether we should show the placeholder when the element is focused but empty.
pref("dom.placeholder.show_on_focus", true);