Gecko work for bug 823619: Dispatch spec-compliant mouse events when touch events are available. r=cjones,jlebar,schien,vingtetun a=blocking-basecamp
authorChris Peterson <cpeterson@mozilla.com>, Shih-Chiang Chien <schien@mozilla.com> and Chris Jones <jones.chris.g@gmail.com>
Fri, 04 Jan 2013 17:29:31 -0800
changeset 122497 9c07ef512e290129cc5186967bc9dc1fa1bd22d6
parent 122496 4f22d215a52ec664d70ccd0dc9784c64bb62f1cc
child 122498 64d3999a9272308a95f200e5be9cbd5c8d922ce4
push id1997
push userakeybl@mozilla.com
push dateMon, 07 Jan 2013 21:25:26 +0000
treeherdermozilla-beta@4baf45cdcf21 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerscjones, jlebar, schien, vingtetun, blocking-basecamp
bugs823619
milestone19.0a2
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
b2g/app/b2g.js
dom/browser-element/BrowserElementScrolling.js
dom/ipc/TabChild.cpp
dom/ipc/TabChild.h
gfx/layers/ipc/AsyncPanZoomController.cpp
gfx/layers/ipc/AsyncPanZoomController.h
layout/ipc/PRenderFrame.ipdl
layout/ipc/RenderFrameChild.cpp
layout/ipc/RenderFrameChild.h
layout/ipc/RenderFrameParent.cpp
layout/ipc/RenderFrameParent.h
modules/libpref/src/init/all.js
--- 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);