Bug 597286, part 6: MouseModule generates tap events instead of using clunky clickers [r=mbrubeck]
authorBenjamin Stover <bstover@mozilla.com>
Wed, 22 Sep 2010 15:08:18 -0700
changeset 66685 216e061d35356622180e1592a7fc7eca7fc99662
parent 66684 0880959c0152925185e2001445612f6223cc0041
child 66686 0a0fdf83e137e07b8713d18401d929204bd93190
push id19389
push userffxbld
push dateWed, 06 Apr 2011 21:33:21 +0000
treeherdermozilla-central@8e9f90073a20 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmbrubeck
bugs597286
Bug 597286, part 6: MouseModule generates tap events instead of using clunky clickers [r=mbrubeck]
mobile/chrome/content/InputHandler.js
mobile/chrome/content/browser.js
--- a/mobile/chrome/content/InputHandler.js
+++ b/mobile/chrome/content/InputHandler.js
@@ -278,17 +278,17 @@ InputHandler.prototype = {
  *
  * The Fennec chrome DOM tree has elements that are augmented dynamically with
  * custom JS properties that tell the MouseModule they have custom support for
  * either dragging or clicking.  These JS properties are JS objects that expose
  * an interface supporting dragging or clicking (though currently we only look
  * to drag scrollable elements).
  *
  * The MouseModule grabs event focus of the input handler on mousedown, at which
- * point it will attempt to find such custom draggers/clickers by walking up the
+ * point it will attempt to find such custom draggers by walking up the
  * DOM tree from the event target.  It ungrabs event focus on mouseup.  It
  * redispatches the swallowed mousedown, mouseup events back to chrome, so that
  * chrome elements still get their events.
  *
  * The mousedown and mouseup events happening in the main context are
  * redispatched as soon as they get caught, contrary to events happening on web
  * content which are swallowed before being redispatched as a triple at the end
  * of the mouseup handling.
@@ -307,39 +307,28 @@ InputHandler.prototype = {
  *     indicate one last drag movement.
  *
  *   dragMove(dx, dy, scroller)
  *     Signals an input attempt to drag by dx, dy.
  *
  * Between mousedown and mouseup, MouseModule incrementally drags and updates
  * the dragger accordingly, and also determines whether a [double-]click occured
  * (based on whether the input moves have moved outside of a certain drag disk
- * centered at the mousedown position).  If a [double-]click happened, any
- * customClicker will be notified.  The customClicker must support the following
- * interface:
- *
- *   singleClick(cx, cy, modifiers)
- *     Signals a single (as opposed to double) click occured at client
- *     coordinates cx, cy.  Specify optional modifiers to include
- *     shift-keys with click.
- *
- *   doubleClick(cx1, cy1, cx2, cy2)
- *     Signals a doubleclick occured, with the first click at client coordinates
- *     cx1, cy1, and second click at client coordinates cx2, cy2.
+ * centered at the mousedown position). Custom touch events are dispatched
+ * accordingly.
  *
  * There is a default dragger in case a scrollable element is dragged --- see
- * the defaultDragger prototype property.  There is no default clicker.
+ * the defaultDragger prototype property.
  */
 function MouseModule(owner, browserViewContainer) {
   this._owner = owner;
   this._browserViewContainer = browserViewContainer;
   this._dragData = new DragData(kTapRadius);
 
   this._dragger = null;
-  this._clicker = null;
 
   this._downUpEvents = [];
   this._targetScrollInterface = null;
 
   var self = this;
   this._kinetic = new KineticController(this._dragBy.bind(this),
                                         this._kineticStop.bind(this));
 
@@ -394,25 +383,17 @@ MouseModule.prototype = {
       this._kinetic.end();
 
     this._dragData.reset();
     this._targetScrollInterface = null;
 
     this._cleanClickBuffer();
   },
 
-  /**
-   * Handle a mousedown by stopping any lingering kinetic drag, walking DOM tree
-   * in search of a scrollable element (and its custom dragger if available) and
-   * a clicker, and initiating a drag if we have said scrollable element.  The
-   * mousedown event is entirely swallowed but is saved for later redispatching,
-   * once we know right and proper what the input is trying to do to us.
-   *
-   * We grab() in here.
-   */
+  /** Begin possible pan and send tap down event. */
   _onMouseDown: function _onMouseDown(aEvent) {
     this._owner.allowClicks();
 
     let dragData = this._dragData;
     if (dragData.dragging) {
       // Somehow a mouse up was missed.
       let [sX, sY] = dragData.panPosition();
       this._doDragStop();
@@ -423,93 +404,83 @@ MouseModule.prototype = {
     // returned if none found.
     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();
 
-    let targetClicker = this.getClickerFromElement(aEvent.target);
-
     this._targetScrollInterface = targetScrollInterface;
     this._dragger = dragger;
-    this._clicker = (targetClicker) ? targetClicker.customClicker : null;
     this._target = aEvent.target;
 
-    if (this._clicker)
-      this._clicker.mouseDown(aEvent.clientX, aEvent.clientY);
+    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);
+
+      this._recordEvent(aEvent);
+    } else {
+      // cancel all pending content clicks
+      this._cleanClickBuffer();
+    }
 
     if (this._dragger) {
       let draggable = this._dragger.isDraggable(targetScrollbox, targetScrollInterface);
       if (draggable.x || draggable.y)
         this._doDragStart(aEvent);
     }
-
-    if (this._targetIsContent(aEvent)) {
-      this._recordEvent(aEvent);
-    }
-    else {
-      // cancel all pending content clicks
-      this._cleanClickBuffer();
-
-      if (this._dragger) {
-        // do not allow axis locking if panning is only possible in one direction
-        let draggable = this._dragger.isDraggable(targetScrollbox, targetScrollInterface);
-        dragData.locked = !draggable.x || !draggable.y;
-      }
-    }
   },
 
-  /**
-   * Handle a mouseup by swallowing the event (just as we did the mousedown) as
-   * well as the possible DOM click event that follows, making one last drag
-   * (which, do note, might just be the beginning of a kinetic drag that will
-   * linger long after we are gone), and recording the mousedown for later
-   * redispatching.
-   *
-   * We ungrab() in here.
-   */
+  /** Send tap up event and any necessary full taps. */
   _onMouseUp: function _onMouseUp(aEvent) {
     let dragData = this._dragData;
     let oldIsPan = dragData.isPan();
     if (dragData.dragging) {
       dragData.setDragPosition(aEvent.screenX, aEvent.screenY);
       let [sX, sY] = dragData.panPosition();
       this._doDragStop();
     }
 
     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 = this._clicker && dragData.isClick() && (this._downUpEvents.length > 1);
+      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, since there was no clicker
-        // to commit to.  when there is one, the path taken through
-        // _commitAnotherClick takes care of this.
+        // clean the click buffer ourselves
         this._cleanClickBuffer();
     }
     else if (dragData.isPan()) {
       // User was panning around or contextmenu was open, do not allow chrome 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();
-    }
 
-    let clicker = this._clicker;
-    if (clicker) {
-      // Let clicker know when mousemove begins a pan
-      if (!oldIsPan && dragData.isPan())
-        clicker.panBegin();
-      clicker.mouseUp(aEvent.clientX, aEvent.clientY);
+      // 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.
    */
@@ -522,20 +493,22 @@ MouseModule.prototype = {
       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 clicker know when mousemove begins a pan
-        let clicker = this._clicker;
-        if (!oldIsPan && clicker)
-          clicker.panBegin();
+        // Let everyone know when mousemove begins a pan
+        if (!oldIsPan && dragData.isPan()) {
+          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) {
@@ -611,55 +584,62 @@ MouseModule.prototype = {
       event.initEvent("PanFinished", true, false);
       document.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 click, and
-   * the timeout is cancelled.  Otherwise, the timeout issues a single click to
-   * the clicker.
+   * 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
       this._singleClickTimeout.clear();
       this._doDoubleClick();
     } else {
       this._singleClickTimeout.once(kDoubleClickInterval);
     }
   },
 
-  /**
-   * Endpoint of _commitAnotherClick().  Finalize a single click and tell the clicker.
-   */
+  /** Endpoint of _commitAnotherClick().  Finalize a single tap.  */
   _doSingleClick: function _doSingleClick() {
     let ev = this._downUpEvents[1];
     this._cleanClickBuffer();
 
     // borrowed from nsIDOMNSEvent.idl
     let modifiers =
       (ev.altKey   ? Ci.nsIDOMNSEvent.ALT_MASK     : 0) |
       (ev.ctrlKey  ? Ci.nsIDOMNSEvent.CONTROL_MASK : 0) |
       (ev.shiftKey ? Ci.nsIDOMNSEvent.SHIFT_MASK   : 0) |
       (ev.metaKey  ? Ci.nsIDOMNSEvent.META_MASK    : 0);
-    this._clicker.singleClick(ev.clientX, ev.clientY, modifiers);
+
+    let event = document.createEvent("Events");
+    event.initEvent("TapSingle", true, false);
+    event.clientX = ev.clientX;
+    event.clientY = ev.clientY;
+    event.modifiers = modifiers;
+    ev.target.dispatchEvent(event);
   },
 
-  /**
-   * Endpoint of _commitAnotherClick().  Finalize a double click and tell the clicker.
-   */
+  /** Endpoint of _commitAnotherClick().  Finalize a double tap.  */
   _doDoubleClick: function _doDoubleClick() {
     let mouseUp1 = this._downUpEvents[1];
     // sometimes the second press event is not dispatched at all
     let mouseUp2 = this._downUpEvents[Math.min(3, this._downUpEvents.length - 1)];
     this._cleanClickBuffer();
-    this._clicker.doubleClick(mouseUp1.clientX, mouseUp1.clientY,
-                              mouseUp2.clientX, mouseUp2.clientY);
+
+    let event = document.createEvent("Events");
+    event.initEvent("TapDouble", true, false);
+    event.clientX1 = mouseUp1.clientX;
+    event.clientY1 = mouseUp1.clientY;
+    event.clientX2 = mouseUp1.clientX;
+    event.clientY2 = mouseUp1.clientY;
+    mouseUp1.target.dispatchEvent(event);
   },
 
   /**
    * Record a mousedown/mouseup event for later redispatch via
    * _redispatchDownUpEvents()
    */
   _recordEvent: function _recordEvent(aEvent) {
     this._downUpEvents.push(aEvent);
@@ -754,33 +734,20 @@ MouseModule.prototype = {
         }
       } catch (e) { /* we aren't here to deal with your exceptions, we'll just keep
                        traversing until we find something more well-behaved, as we
                        prefer default behaviour to whiny scrollers. */ }
     }
     return [scrollbox, qinterface, (scrollbox ? (scrollbox.customDragger || this._defaultDragger) : null)];
   },
 
-  /**
-   * Walk up (parentward) the DOM tree from elem in search of an element with
-   * a customClicker.  Return the element if found, null elsewise.
-   */
-  getClickerFromElement: function getClickerFromElement(elem) {
-    for (; elem; elem = elem.parentNode)
-      if (elem.customClicker)
-        break;
-
-    return (elem) ? elem : null;
-  },
-
   toString: function toString() {
     return '[MouseModule] {'
       + '\n\tdragData=' + this._dragData + ', '
       + 'dragger=' + this._dragger + ', '
-      + 'clicker=' + this._clicker + ', '
       + '\n\tdownUpEvents=' + this._downUpEvents + ', '
       + 'length=' + this._downUpEvents.length + ', '
       + '\n\ttargetScroller=' + this._targetScrollInterface + ', '
       + '\n\tclickTimeout=' + this._clickTimeout + '\n  }';
   }
 };
 
 /**
@@ -1123,23 +1090,16 @@ KineticController.prototype = {
  * Input module for basic key input.
  */
 function KeyModule(owner, browserViewContainer) {
   this._owner = owner;
   this._browserViewContainer = browserViewContainer;
 }
 
 KeyModule.prototype = {
-  getClickerFromElement: function getClickerFromElement(elem) {
-    for (; elem; elem = elem.parentNode)
-      if (elem.customKeySender)
-        break;
-    return (elem) ? elem : null;
-  },
-
   handleEvent: function handleEvent(aEvent) {
     if (aEvent.type == "keydown" || aEvent.type == "keyup" || aEvent.type == "keypress") {
       let keyer = this._browserViewContainer.customKeySender;
       if (keyer) {
         keyer.dispatchKeyEvent(aEvent);
         aEvent.stopPropagation();
         aEvent.preventDefault();
       }
@@ -1284,20 +1244,16 @@ GestureModule.prototype = {
     // grab events during pinch, stopping other mouse events in their tracks
     this._owner.grab(this);
 
     if ((aEvent.target instanceof XULElement) || !Browser.selectedTab.allowZoom) {
       this._owner.ungrab(this);
       return;
     }
 
-    // hide element highlight
-    // XXX ugh, this is awful. None of this code should be in InputHandler.
-    document.getElementById("inputhandler-overlay").customClicker.panBegin();
-
     // create the AnimatedZoom object for fast arbitrary zooming
     this._pinchZoom = AnimatedZoom;
     this._pinchZoomRect = AnimatedZoom.getStartRect()
 
     // start from current zoom level
     this._pinchStartScale = this._pinchScale = getBrowser().scale;
     this._ignoreNextUpdate = true; // first update gives useless, huge delta
 
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -170,20 +170,21 @@ var Browser = {
       dump("###########" + e + "\n");
     }
 
     let container = document.getElementById("browsers");
     // XXX change
 
     /* handles dispatching clicks on browser into clicks in content or zooms */
     let inputHandlerOverlay = document.getElementById("inputhandler-overlay");
-    inputHandlerOverlay.customClicker = new ContentCustomClicker();
     inputHandlerOverlay.customKeySender = new ContentCustomKeySender();
     inputHandlerOverlay.customDragger = new Browser.MainDragger();
 
+    ContentTouchHandler.init();
+
     // Warning, total hack ahead. All of the real-browser related scrolling code
     // lies in a pretend scrollbox here. Let's not land this as-is. Maybe it's time
     // to redo all the dragging code.
     this.contentScrollbox = container;
     this.contentScrollboxScroller = {
       scrollBy: function(x, y) {
         getBrowser().scrollBy(x, y);
       },
@@ -1257,77 +1258,102 @@ const BrowserSearch = {
     return !BrowserSearch.engines.some(function(item) {
       return aEngine.title == item.name;
     });
   }
 };
 
 
 /** Watches for mouse events in chrome and sends them to content. */
-function ContentCustomClicker() {
-}
-
-ContentCustomClicker.prototype = {
+const ContentTouchHandler = {
+  init: function init() {
+    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) {
+    switch (ev.type) {
+      case "TapDown":
+        this.tapDown(ev.clientX, ev.clientY);
+        break;
+      case "TapUp":
+        thisTapSingletapUp(ev.clientX, ev.clientY);
+        break;
+      case "TapSingle":
+        this.tapSingle(ev.clientX, ev.clientY, ev.modifiers);
+        break;
+      case "TapDouble":
+        this.tapDouble(ev.clientX1, ev.clientY1, ev.clientX2, ev.clientY2);
+        break;
+      case "PanBegin":
+        this.panBegin();
+        break;
+    }
+  },
+
   _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 });
   },
 
-  mouseDown: function mouseDown(aX, aY) {
+  tapDown: function tapDown(aX, aY) {
     // Ensure that the content process has gets an activate event
     let browser = getBrowser();
     let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
     browser.focus();
     try {
       fl.activateRemoteFrame();
     } catch (e) {
     }
     this._dispatchMouseEvent("Browser:MouseDown", aX, aY);
   },
 
-  mouseUp: function mouseUp(aX, aY) {
+  tapUp: function tapUp(aX, aY) {
     TapHighlightHelper.hide(200);
   },
 
   panBegin: function panBegin() {
     TapHighlightHelper.hide(0);
 
     this._dispatchMouseEvent("Browser:MouseCancel");
   },
 
-  singleClick: function singleClick(aX, aY, aModifiers) {
+  tapSingle: function tapSingle(aX, aY, aModifiers) {
     TapHighlightHelper.hide(200);
 
     // Cancel the mouse click if we are showing a context menu
     if (!ContextHelper.popupState)
       this._dispatchMouseEvent("Browser:MouseUp", aX, aY, aModifiers);
     this._dispatchMouseEvent("Browser:MouseCancel");
   },
 
-  doubleClick: function doubleClick(aX1, aY1, aX2, aY2) {
+  tapDouble: function tapDouble(aX1, aY1, aX2, aY2) {
     TapHighlightHelper.hide();
 
     this._dispatchMouseEvent("Browser:MouseCancel");
 
     const kDoubleClickRadius = 100;
 
     let maxRadius = kDoubleClickRadius * getBrowser().scale;
     let dx = aX2 - aX1;
     let dy = aY1 - aY2;
 
     if (dx*dx + dy*dy < maxRadius*maxRadius)
       this._dispatchMouseEvent("Browser:ZoomToPoint", aX1, aY1);
   },
 
   toString: function toString() {
-    return "[ContentCustomClicker] { }";
+    return "[ContentTouchHandler] { }";
   }
 };
 
 
 /** Watches for mouse events in chrome and sends them to content. */
 function ContentCustomKeySender() {
 }