Bug 597286, part 7: Separate panning from tapping and MouseModule no longer cares about whether something is content [r=mbrubeck]
authorBenjamin Stover <bstover@mozilla.com>
Wed, 22 Sep 2010 15:09:04 -0700
changeset 66686 0a0fdf83e137e07b8713d18401d929204bd93190
parent 66685 216e061d35356622180e1592a7fc7eca7fc99662
child 66687 ab521981f5a432600e4821cb0054139ec823f7f5
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmbrubeck
bugs597286
Bug 597286, part 7: Separate panning from tapping and MouseModule no longer cares about whether something is content [r=mbrubeck]
mobile/chrome/content/InputHandler.js
mobile/chrome/content/browser.js
--- a/mobile/chrome/content/InputHandler.js
+++ b/mobile/chrome/content/InputHandler.js
@@ -40,16 +40,19 @@
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 // Maximum delay in ms between the two taps of a double-tap
 const kDoubleClickInterval = 400;
 
+// Amount of time to wait before tap becomes long tap
+const kLongTapWait = 700;
+
 // If a tap lasts longer than this duration in ms, treat it as a single-tap
 // immediately instead of waiting for a possible double tap.
 const kDoubleClickThreshold = 200;
 
 // threshold in pixels for sensing a tap as opposed to a pan
 const kTapRadius = Services.prefs.getIntPref("ui.dragThresholdX");
 
 // maximum drag distance in pixels while axis locking can still be reverted
@@ -122,17 +125,16 @@ function InputHandler(browserViewContain
   /* when set to true, next click won't be dispatched */
   this._suppressNextClick = false;
 
   /* these handle dragging of both chrome elements and content */
   window.addEventListener("mousedown", this, true);
   window.addEventListener("mouseup", this, true);
   window.addEventListener("mousemove", this, true);
   window.addEventListener("click", this, true);
-  window.addEventListener("contextmenu", this, false);
   window.addEventListener("MozSwipeGesture", this, true);
   window.addEventListener("MozMagnifyGestureStart", this, true);
   window.addEventListener("MozMagnifyGestureUpdate", this, true);
   window.addEventListener("MozMagnifyGesture", this, true);
 
   /* these handle key strokes in the browser view (where page content appears) */
   browserViewContainer.addEventListener("keypress", this, false);
   browserViewContainer.addEventListener("keyup", this, false);
@@ -215,16 +217,24 @@ InputHandler.prototype = {
    * for the module to do so via the InputHandler.  Hopefully the module is doing
    * this under grab (that is, hopefully the module was grabbing while the mousedown
    * and mouseup events came in, *not* just grabbing for making this call).
    */
   suppressNextClick: function suppressNextClick() {
     this._suppressNextClick = true;
   },
 
+  /** Cancels all pending input sequences */
+  cancelPending: function cancelPending() {
+    let mods = this._modules;
+    for (let i = 0, len = mods.length; i < len; ++i)
+      if (mods[i])
+        mods[i].cancelPending();
+  },
+
   /**
    * Undoes any suppression caused by calling suppressNextClick(), unless the click
    * already happened.
    */
   allowClicks: function allowClicks() {
     this._suppressNextClick = false;
   },
 
@@ -328,56 +338,44 @@ function MouseModule(owner, browserViewC
   this._downUpEvents = [];
   this._targetScrollInterface = null;
 
   var self = this;
   this._kinetic = new KineticController(this._dragBy.bind(this),
                                         this._kineticStop.bind(this));
 
   this._singleClickTimeout = new Util.Timeout(this._doSingleClick.bind(this));
-
-  messageManager.addMessageListener("Browser:ContextMenu", this);
+  this._longClickTimeout = new Util.Timeout(this._doLongClick.bind(this));
 }
 
 
 MouseModule.prototype = {
   handleEvent: function handleEvent(aEvent) {
     if (aEvent.button !== 0 && aEvent.type != "contextmenu" && aEvent.type != "MozBeforePaint")
       return;
 
     switch (aEvent.type) {
       case "mousedown":
         this._onMouseDown(aEvent);
         break;
       case "mousemove":
+        aEvent.stopPropagation();
+        aEvent.preventDefault();
         this._onMouseMove(aEvent);
         break;
       case "mouseup":
         this._onMouseUp(aEvent);
         break;
-      case "contextmenu":
-        if (ContextHelper.popupState)
-          this.cancelPending();
-        break;
       case "MozBeforePaint":
         this._waitingForPaint = false;
         removeEventListener("MozBeforePaint", this, false);
         break;
     }
   },
 
-  receiveMessage: function receiveMessage(aMessage) {
-    // TODO: Make "contextmenu" a first class part of InputHandler
-    // Bug 554639
-    if (aMessage.name != "Browser:ContextMenu" || !ContextHelper.popupState)
-      return;
-
-    this.cancelPending();
-  },
-
   /**
    * This gets invoked by the input handler if another module grabs.  We should
    * reset our state or something here.  This is probably doing the wrong thing
    * in its current form.
    */
   cancelPending: function cancelPending() {
     if (this._kinetic.isActive())
       this._kinetic.end();
@@ -405,123 +403,114 @@ MouseModule.prototype = {
     let [targetScrollbox, targetScrollInterface, dragger]
       = this.getScrollboxFromElement(aEvent.target);
 
     // stop kinetic panning if targetScrollbox has changed
     if (this._kinetic.isActive() && this._dragger != dragger)
       this._kinetic.end();
 
     this._targetScrollInterface = targetScrollInterface;
-    this._dragger = dragger;
-    this._target = aEvent.target;
 
-    if (this._targetIsContent(aEvent)) {
-      let event = document.createEvent("Events");
-      event.initEvent("TapDown", true, false);
-      event.clientX = aEvent.clientX;
-      event.clientY = aEvent.clientY;
-      aEvent.target.dispatchEvent(event);
-
+    // Do tap
+    let event = document.createEvent("Events");
+    event.initEvent("TapDown", true, true);
+    event.clientX = aEvent.clientX;
+    event.clientY = aEvent.clientY;
+    let success = aEvent.target.dispatchEvent(event);
+    if (success) {
       this._recordEvent(aEvent);
+      this._target = aEvent.target;
+      this._longClickTimeout.once(kLongTapWait);
     } else {
       // cancel all pending content clicks
       this._cleanClickBuffer();
     }
 
-    if (this._dragger) {
-      let draggable = this._dragger.isDraggable(targetScrollbox, targetScrollInterface);
-      if (draggable.x || draggable.y)
+    // Do pan
+    if (dragger) {
+      let draggable = dragger.isDraggable(targetScrollbox, targetScrollInterface);
+      dragData.locked = !draggable.x || !draggable.y;
+      if (draggable.x || draggable.y) {
+        this._dragger = dragger;
         this._doDragStart(aEvent);
+      }
     }
+
+    this._owner.grab(this);
   },
 
   /** Send tap up event and any necessary full taps. */
   _onMouseUp: function _onMouseUp(aEvent) {
+    this._onMouseMove(aEvent);
+
     let dragData = this._dragData;
-    let oldIsPan = dragData.isPan();
-    if (dragData.dragging) {
-      dragData.setDragPosition(aEvent.screenX, aEvent.screenY);
-      let [sX, sY] = dragData.panPosition();
+    if (dragData.dragging)
       this._doDragStop();
+
+    // Do tap
+    if (this._target) {
+      let event = document.createEvent("Events");
+      event.initEvent("TapUp", true, true);
+      event.clientX = aEvent.clientX
+      event.clientY = aEvent.clientY;
+      let success = aEvent.target.dispatchEvent(event);
+      if (!success) {
+        this._cleanClickBuffer();
+      } else {
+        this._recordEvent(aEvent);
+        let commitToClicker = dragData.isClick() && (this._downUpEvents.length > 1);
+        if (commitToClicker)
+          // commit this click to the doubleclick timewait buffer
+          this._commitAnotherClick();
+        else
+          // clean the click buffer ourselves
+          this._cleanClickBuffer();
+      }
     }
 
-    if (this._targetIsContent(aEvent)) {
-      if (this._dragger) {
-        let event = document.createEvent("Events");
-        event.initEvent("TapUp", true, false);
-        event.clientX = aEvent.clientX
-        event.clientY = aEvent.clientY;
-        aEvent.target.dispatchEvent(event);
-      }
-
-      // User possibly clicked on something in content
-      this._recordEvent(aEvent);
-      let commitToClicker = dragData.isClick() && (this._downUpEvents.length > 1);
-      if (commitToClicker)
-        // commit this click to the doubleclick timewait buffer
-        this._commitAnotherClick();
-      else
-        // clean the click buffer ourselves
-        this._cleanClickBuffer();
-    }
-    else if (dragData.isPan()) {
-      // User was panning around or contextmenu was open, do not allow chrome click
+    // Do pan
+    if (dragData.isPan()) {
+      // User was panning around, do not allow click
       // XXX Instead of having suppressNextClick, we could grab until click is seen
       // and THEN ungrab so that owner does not need to know anything about clicking.
       let generatesClick = aEvent.detail;
       if (generatesClick)
         this._owner.suppressNextClick();
-
-      // Pan has begun
-      if (!oldIsPan) {
-        let event = document.createEvent("Events");
-        event.initEvent("PanBegin", true, false);
-        document.dispatchEvent(event);
-      }
     }
 
     this._owner.ungrab(this);
   },
 
   /**
    * If we're in a drag, do what we have to do to drag on.
    */
   _onMouseMove: function _onMouseMove(aEvent) {
     let dragData = this._dragData;
 
     if (dragData.dragging && !this._waitingForPaint) {
       let oldIsPan = dragData.isPan();
       dragData.setDragPosition(aEvent.screenX, aEvent.screenY);
-      aEvent.stopPropagation();
-      aEvent.preventDefault();
       if (dragData.isPan()) {
-        this._owner.grab(this);
         // Only pan when mouse event isn't part of a click. Prevent jittering on tap.
         let [sX, sY] = dragData.panPosition();
         this._doDragMove();
 
         // Let everyone know when mousemove begins a pan
         if (!oldIsPan && dragData.isPan()) {
+          this._longClickTimeout.clear();
+
           let event = document.createEvent("Events");
           event.initEvent("PanBegin", true, false);
           aEvent.target.dispatchEvent(event);
         }
       }
     }
   },
 
   /**
-   * Check if the event concern the browser content
-   */
-  _targetIsContent: function _targetIsContent(aEvent) {
-    let target = aEvent.target;
-    return target && target.id == "inputhandler-overlay";
-  },
-
-  /**
    * Inform our dragger of a dragStart.
    */
   _doDragStart: function _doDragStart(event) {
     let dragData = this._dragData;
     dragData.setDragStart(event.screenX, event.screenY);
     this._kinetic.addData(0, 0);
     if (!this._kinetic.isActive())
       this._dragger.dragStart(event.clientX, event.clientY, event.target, this._targetScrollInterface);
@@ -573,24 +562,36 @@ MouseModule.prototype = {
     }
     return dragged;
   },
 
   /** Callback for kinetic scroller. */
   _kineticStop: function _kineticStop() {
     // Kinetic panning could finish while user is panning, so don't finish
     // the pan just yet.
-    if (!dragData.dragging && !dragData.isPan()) {
+    let dragData = this._dragData;
+    if (!dragData.dragging) {
       this._dragger.dragStop(0, 0, this._targetScrollInterface);
       let event = document.createEvent("Events");
       event.initEvent("PanFinished", true, false);
       document.dispatchEvent(event);
     }
   },
 
+  /** Called when tap down times out and becomes a long tap. */
+  _doLongClick: function _doLongClick() {
+    let ev = this._downUpEvents[0];
+
+    let event = document.createEvent("Events");
+    event.initEvent("LongTap", true, false);
+    event.clientX = ev.clientX;
+    event.clientY = ev.clientY;
+    ev.target.dispatchEvent(event);
+  },
+
   /**
    * Commit another click event to our click buffer.  The `click buffer' is a
    * timeout initiated by the first click.  If the timeout is still alive when
    * another click is committed, then the click buffer forms a double tap, and
    * the timeout is cancelled.  Otherwise, the timeout issues a single tap.
    */
   _commitAnotherClick: function _commitAnotherClick() {
     if (this._singleClickTimeout.isPending()) {   // we're waiting for a second click for double
@@ -647,16 +648,17 @@ MouseModule.prototype = {
 
   /**
    * Clean out the click buffer.  Should be called after a single, double, or
    * non-click has been processed and all relevant (re)dispatches of events in
    * the recorded down/up event queue have been issued out.
    */
   _cleanClickBuffer: function _cleanClickBuffer() {
     this._singleClickTimeout.clear();
+    this._longClickTimeout.clear();
     this._downUpEvents.splice(0);
   },
 
   /**
    * The default dragger object used by MouseModule when dragging a scrollable
    * element that provides no customDragger.  Simply performs the expected
    * regular scrollBy calls on the scroller.
    */
@@ -740,18 +742,17 @@ MouseModule.prototype = {
   },
 
   toString: function toString() {
     return '[MouseModule] {'
       + '\n\tdragData=' + this._dragData + ', '
       + 'dragger=' + this._dragger + ', '
       + '\n\tdownUpEvents=' + this._downUpEvents + ', '
       + 'length=' + this._downUpEvents.length + ', '
-      + '\n\ttargetScroller=' + this._targetScrollInterface + ', '
-      + '\n\tclickTimeout=' + this._clickTimeout + '\n  }';
+      + '\n\ttargetScroller=' + this._targetScrollInterface + '}';
   }
 };
 
 /**
  * DragData handles processing drags on the screen, handling both
  * locking of movement on one axis, and click detection.
  */
 function DragData(dragRadius) {
@@ -1204,22 +1205,16 @@ GestureModule.prototype = {
           else
             this._pinchUpdate(aEvent);
           break;
 
         case "MozMagnifyGesture":
           consume = true;
           this._pinchEnd(aEvent);
           break;
-
-        case "contextmenu":
-          // prevent context menu while pinching
-          if (this._pinchZoom)
-            consume = true;
-          break;
       }
       if (consume) {
         // prevent sending of event to content
         aEvent.stopPropagation();
         aEvent.preventDefault();
       }
     }
     catch (e) {
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -1268,16 +1268,22 @@ const ContentTouchHandler = {
     document.addEventListener("TapDown", this, false);
     document.addEventListener("TapUp", this, false);
     document.addEventListener("TapSingle", this, false);
     document.addEventListener("TapDouble", this, false);
     document.addEventListener("PanBegin", this, false);
   },
 
   handleEvent: function handleEvent(ev) {
+    if (!this._targetIsContent(ev)) {
+      TapHighlightHelper.hide();
+      this._dispatchMouseEvent("Browser:MouseCancel");
+      return;
+    }
+
     switch (ev.type) {
       case "TapDown":
         this.tapDown(ev.clientX, ev.clientY);
         break;
       case "TapUp":
         thisTapSingletapUp(ev.clientX, ev.clientY);
         break;
       case "TapSingle":
@@ -1287,16 +1293,24 @@ const ContentTouchHandler = {
         this.tapDouble(ev.clientX1, ev.clientY1, ev.clientX2, ev.clientY2);
         break;
       case "PanBegin":
         this.panBegin();
         break;
     }
   },
 
+  /**
+   * Check if the event concern the browser content
+   */
+  _targetIsContent: function _targetIsContent(aEvent) {
+    let target = aEvent.target;
+    return target && target.id == "inputhandler-overlay";
+  },
+
   _dispatchMouseEvent: function _dispatchMouseEvent(aName, aX, aY, aModifiers) {
     let aX = aX || 0;
     let aY = aY || 0;
     let aModifiers = aModifiers || null;
     let browser = getBrowser();
     let [x, y] = Browser.transformClientToBrowser(aX, aY);
     browser.messageManager.sendAsyncMessage(aName, { x: x, y: y, modifiers: aModifiers });
   },
@@ -1314,17 +1328,16 @@ const ContentTouchHandler = {
   },
 
   tapUp: function tapUp(aX, aY) {
     TapHighlightHelper.hide(200);
   },
 
   panBegin: function panBegin() {
     TapHighlightHelper.hide(0);
-
     this._dispatchMouseEvent("Browser:MouseCancel");
   },
 
   tapSingle: function tapSingle(aX, aY, aModifiers) {
     TapHighlightHelper.hide(200);
 
     // Cancel the mouse click if we are showing a context menu
     if (!ContextHelper.popupState)