Backed out 2 changesets (bug 901151, bug 788073) for suspected robocop-2 orange.
authorRyan VanderMeulen <ryanvm@gmail.com>
Tue, 06 Aug 2013 15:28:46 -0400
changeset 149536 1fb5d14e8348b16014fafa5d746e0a8823df8187
parent 149535 0fba8893a8665e0725fa6dc672b4ebeebb4ecc9b
child 149537 2fcbc0a619f2d33123df6e73d8de82da7e69f942
child 149572 f1b9e3f2d4c5b17e53d2391156f4682034377bf1
child 149617 c3b23d6af64f79ae649cce603a57f9d4500f8a63
child 149638 a6e16bd50b96ab57916d96ecc60be2c9067ecde0
push idunknown
push userunknown
push dateunknown
bugs901151, 788073
milestone26.0a1
backs out6e7a45127e07da401d2fd048280a47580eab373b
bf050e52851f1fcd9037a0bf1700b829e92848a8
Backed out 2 changesets (bug 901151, bug 788073) for suspected robocop-2 orange. Backed out changeset 6e7a45127e07 (bug 901151) Backed out changeset bf050e52851f (bug 788073)
mobile/android/app/mobile.js
mobile/android/chrome/content/browser.js
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -423,29 +423,22 @@ pref("dom.max_script_run_time", 20);
 pref("devtools.errorconsole.enabled", false);
 
 pref("font.size.inflation.minTwips", 120);
 
 // When true, zooming will be enabled on all sites, even ones that declare user-scalable=no.
 pref("browser.ui.zoom.force-user-scalable", false);
 
 // Touch radius (area around the touch location to look for target elements),
-pref("ui.touch.radius.enabled", true);
-pref("ui.touch.radius.leftmm", 3);
-pref("ui.touch.radius.topmm", 5);
-pref("ui.touch.radius.rightmm", 3);
-pref("ui.touch.radius.bottommm", 2);
-pref("ui.touch.radius.visitedWeight", 120);
-
-pref("ui.mouse.radius.enabled", true);
-pref("ui.mouse.radius.leftmm", 3);
-pref("ui.mouse.radius.topmm", 5);
-pref("ui.mouse.radius.rightmm", 3);
-pref("ui.mouse.radius.bottommm", 2);
-pref("ui.mouse.radius.visitedWeight", 120);
+// in 1/240-inch pixels:
+pref("browser.ui.touch.left", 32);
+pref("browser.ui.touch.right", 32);
+pref("browser.ui.touch.top", 48);
+pref("browser.ui.touch.bottom", 16);
+pref("browser.ui.touch.weight.visited", 120); // percentage
 
 // The percentage of the screen that needs to be scrolled before margins are exposed.
 pref("browser.ui.show-margins-threshold", 20);
 
 // Maximum distance from the point where the user pressed where we still
 // look for text to select
 pref("browser.ui.selection.distance", 250);
 
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -2039,18 +2039,22 @@ var NativeWindow = {
       return null;
     },
 
     // Checks if there are context menu items to show, and if it finds them
     // sends a contextmenu event to content. We also send showing events to
     // any html5 context menus we are about to show
     _sendToContent: function(aX, aY) {
       // find and store the top most element this context menu is being shown for
-      // use the highlighted element if possible
-      let target = BrowserEventHandler._highlightElement;
+      // use the highlighted element if possible, otherwise look for nearby clickable elements
+      // If we still don't find one we fall back to using anything
+      let target = BrowserEventHandler._highlightElement || ElementTouchHelper.elementFromPoint(aX, aY);
+      if (!target)
+        target = ElementTouchHelper.anyElementFromPoint(aX, aY);
+
       if (!target)
         return;
 
       // store a weakref to the target to be used when the context menu event returns
       this._target = target;
 
       this.menuitems = [];
       let menuitemsSet = false;
@@ -2096,18 +2100,16 @@ var NativeWindow = {
         if (SelectionHandler.canSelect(target))
           SelectionHandler.startSelection(target, aX, aY);
       }
     },
 
     // Actually shows the native context menu by passing a list of context menu items to
     // show to the Java.
     _show: function(aEvent) {
-      BrowserEventHandler._cancelTapHighlight();
-
       let popupNode = this._target;
       this._target = null;
       if (aEvent.defaultPrevented || !popupNode) {
         return;
       }
       this._innerShow(popupNode, aEvent.clientX, aEvent.clientY);
     },
 
@@ -4119,34 +4121,41 @@ var BrowserEventHandler = {
   },
 
   _handleTouchStart: function(aEvent) {
     if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.touches.length > 1 || aEvent.defaultPrevented)
       return;
 
     let closest = aEvent.target;
 
-    // Touch event targets are already fluffed out to find targets that have registered for mouse events as well
     if (closest) {
       // If we've pressed a scrollable element, let Java know that we may
       // want to override the scroll behaviour (for document sub-frames)
       this._scrollableElement = this._findScrollableElement(closest, true);
       this._firstScrollEvent = true;
 
       if (this._scrollableElement != null) {
         // Discard if it's the top-level scrollable, we let Java handle this
         let doc = BrowserApp.selectedBrowser.contentDocument;
         if (this._scrollableElement != doc.body && this._scrollableElement != doc.documentElement)
           sendMessageToJava({ type: "Panning:Override" });
       }
-
+    }
+
+    if (!ElementTouchHelper.isElementClickable(closest, null, false))
+      closest = ElementTouchHelper.elementFromPoint(aEvent.changedTouches[0].screenX,
+                                                    aEvent.changedTouches[0].screenY);
+    if (!closest)
+      closest = aEvent.target;
+
+    if (closest) {
       let uri = this._getLinkURI(closest);
-      if (uri)
+      if (uri) {
         Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
-
+      }
       this._doTapHighlight(closest);
     }
   },
 
   _getLinkURI: function(aElement) {
     if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
         ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
         (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href))) {
@@ -4238,33 +4247,26 @@ var BrowserEventHandler = {
         this._cancelTapHighlight();
         break;
 
       case "Gesture:SingleTap": {
         let element = this._highlightElement;
         if (element) {
           try {
             let data = JSON.parse(aData);
-            let x = data.x;
-            let y = data.y;
-
-            // the target should already have been fluffed by the platform touch event code, but
-            // will be fluffed out again by the platform mouse event code as well
-            let win = element.ownerDocument.defaultView;
-            let cwu = win.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
-            // For now, all events are sent as if they came from a touch
-            let source = Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH;
-            try {
-              cwu.sendMouseEventToWindow("mousemove", x, y, 0, 1, 0, true, 1.0, source);
-              cwu.sendMouseEventToWindow("mousedown", x, y, 0, 1, 0, true, 1.0, source);
-              cwu.sendMouseEventToWindow("mouseup", x, y, 0, 1, 0, true, 1.0, source);
-            } catch(e) {
-              Cu.reportError(e);
+            let [x, y] = [data.x, data.y];
+            if (ElementTouchHelper.isElementClickable(element)) {
+              [x, y] = this._moveClickPoint(element, x, y);
+              element = ElementTouchHelper.anyElementFromPoint(x, y);
             }
 
+            this._sendMouseEvent("mousemove", element, x, y);
+            this._sendMouseEvent("mousedown", element, x, y);
+            this._sendMouseEvent("mouseup",   element, x, y);
+
             // See if its a input element
             if ((element instanceof HTMLInputElement && element.mozIsTextField(false)) ||
                 (element instanceof HTMLTextAreaElement))
                SelectionHandler.attachCaret(element);
 
             // scrollToFocusedInput does its own checks to find out if an element should be zoomed into
             BrowserApp.scrollToFocusedInput(BrowserApp.selectedBrowser);
           } catch(e) {
@@ -4507,16 +4509,55 @@ var BrowserEventHandler = {
   _updateLastPosition: function(x, y, dx, dy) {
     this.lastX = x;
     this.lastY = y;
     this.lastTime = Date.now();
 
     this.motionBuffer.push({ dx: dx, dy: dy, time: this.lastTime });
   },
 
+  _moveClickPoint: function(aElement, aX, aY) {
+    // the element can be out of the aX/aY point because of the touch radius
+    // if outside, we gracefully move the touch point to the edge of the element
+    if (!(aElement instanceof HTMLHtmlElement)) {
+      let isTouchClick = true;
+      let rects = ElementTouchHelper.getContentClientRects(aElement);
+      for (let i = 0; i < rects.length; i++) {
+        let rect = rects[i];
+        let inBounds =
+          (aX > rect.left && aX < (rect.left + rect.width)) &&
+          (aY > rect.top && aY < (rect.top + rect.height));
+        if (inBounds) {
+          isTouchClick = false;
+          break;
+        }
+      }
+
+      if (isTouchClick) {
+        let rect = rects[0];
+        // if either width or height is zero, we don't want to move the click to the edge of the element. See bug 757208
+        if (rect.width != 0 && rect.height != 0) {
+          aX = Math.min(Math.floor(rect.left + rect.width), Math.max(Math.ceil(rect.left), aX));
+          aY = Math.min(Math.floor(rect.top + rect.height), Math.max(Math.ceil(rect.top),  aY));
+        }
+      }
+    }
+    return [aX, aY];
+  },
+
+  _sendMouseEvent: function _sendMouseEvent(aName, aElement, aX, aY) {
+    let window = aElement.ownerDocument.defaultView;
+    try {
+      let cwu = window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+      cwu.sendMouseEventToWindow(aName, aX, aY, 0, 1, 0, true);
+    } catch(e) {
+      Cu.reportError(e);
+    }
+  },
+
   _hasScrollableOverflow: function(elem) {
     var win = elem.ownerDocument.defaultView;
     if (!win)
       return false;
     var computedStyle = win.getComputedStyle(elem);
     if (!computedStyle)
       return false;
     return computedStyle.overflowX == 'auto' || computedStyle.overflowX == 'scroll'
@@ -4592,16 +4633,194 @@ const ElementTouchHelper = {
       aY -= rect.top;
       cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
       elem = cwu.elementFromPoint(aX, aY, false, true);
     }
 
     return elem;
   },
 
+  /* Return the most appropriate clickable element (if any), starting from the given window
+     and drilling down through iframes as necessary. If no window is provided, the top-level
+     window of the currently selected tab is used. The coordinates provided should be CSS
+     pixels relative to the window's scroll position. The element returned may not actually
+     contain the coordinates passed in because of touch radius and clickability heuristics. */
+  elementFromPoint: function(aX, aY, aWindow) {
+    // browser's elementFromPoint expect browser-relative client coordinates.
+    // subtract browser's scroll values to adjust
+    let win = (aWindow ? aWindow : BrowserApp.selectedBrowser.contentWindow);
+    let cwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+    let elem = this.getClosest(cwu, aX, aY);
+
+    // step through layers of IFRAMEs and FRAMES to find innermost element
+    while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
+      // adjust client coordinates' origin to be top left of iframe viewport
+      let rect = elem.getBoundingClientRect();
+      aX -= rect.left;
+      aY -= rect.top;
+      cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+      elem = this.getClosest(cwu, aX, aY);
+    }
+
+    return elem;
+  },
+
+  /* Returns the touch radius in content px. */
+  getTouchRadius: function getTouchRadius() {
+    let dpiRatio = ViewportHandler.displayDPI / kReferenceDpi;
+    let zoom = BrowserApp.selectedTab._zoom;
+    return {
+      top: this.radius.top * dpiRatio / zoom,
+      right: this.radius.right * dpiRatio / zoom,
+      bottom: this.radius.bottom * dpiRatio / zoom,
+      left: this.radius.left * dpiRatio / zoom
+    };
+  },
+
+  /* Returns the touch radius in reference pixels. */
+  get radius() {
+    let prefs = Services.prefs;
+    delete this.radius;
+    return this.radius = { "top": prefs.getIntPref("browser.ui.touch.top"),
+                           "right": prefs.getIntPref("browser.ui.touch.right"),
+                           "bottom": prefs.getIntPref("browser.ui.touch.bottom"),
+                           "left": prefs.getIntPref("browser.ui.touch.left")
+                         };
+  },
+
+  get weight() {
+    delete this.weight;
+    return this.weight = { "visited": Services.prefs.getIntPref("browser.ui.touch.weight.visited") };
+  },
+
+  /* Retrieve the closest element to a point by looking at borders position */
+  getClosest: function getClosest(aWindowUtils, aX, aY) {
+    let target = aWindowUtils.elementFromPoint(aX, aY,
+                                               true,   /* ignore root scroll frame*/
+                                               false); /* don't flush layout */
+
+    // if this element is clickable we return quickly. also, if it isn't,
+    // use a cache to speed up future calls to isElementClickable in the
+    // loop below.
+    let unclickableCache = new Array();
+    if (this.isElementClickable(target, unclickableCache, false))
+      return target;
+
+    target = null;
+    let radius = this.getTouchRadius();
+    let nodes = aWindowUtils.nodesFromRect(aX, aY, radius.top, radius.right, radius.bottom, radius.left, true, false);
+
+    let threshold = Number.POSITIVE_INFINITY;
+    for (let i = 0; i < nodes.length; i++) {
+      let current = nodes[i];
+      if (!current.mozMatchesSelector || !this.isElementClickable(current, unclickableCache, true))
+        continue;
+
+      let rect = current.getBoundingClientRect();
+      let distance = this._computeDistanceFromRect(aX, aY, rect);
+
+      // increase a little bit the weight for already visited items
+      if (current && current.mozMatchesSelector("*:visited"))
+        distance *= (this.weight.visited / 100);
+
+      if (distance < threshold) {
+        target = current;
+        threshold = distance;
+      }
+    }
+
+    return target;
+  },
+
+  isElementClickable: function isElementClickable(aElement, aUnclickableCache, aAllowBodyListeners) {
+    const selector = "a,:link,:visited,[role=button],button,input,select,textarea";
+
+    let stopNode = null;
+    if (!aAllowBodyListeners && aElement && aElement.ownerDocument)
+      stopNode = aElement.ownerDocument.body;
+
+    for (let elem = aElement; elem && elem != stopNode; elem = elem.parentNode) {
+      if (aUnclickableCache && aUnclickableCache.indexOf(elem) != -1)
+        continue;
+      if (this._hasMouseListener(elem))
+        return true;
+      if (elem.mozMatchesSelector && elem.mozMatchesSelector(selector))
+        return true;
+      if (elem instanceof HTMLLabelElement && elem.control != null)
+        return true;
+      if (aUnclickableCache)
+        aUnclickableCache.push(elem);
+    }
+    return false;
+  },
+
+  _computeDistanceFromRect: function _computeDistanceFromRect(aX, aY, aRect) {
+    let x = 0, y = 0;
+    let xmost = aRect.left + aRect.width;
+    let ymost = aRect.top + aRect.height;
+
+    // compute horizontal distance from left/right border depending if X is
+    // before/inside/after the element's rectangle
+    if (aRect.left < aX && aX < xmost)
+      x = Math.min(xmost - aX, aX - aRect.left);
+    else if (aX < aRect.left)
+      x = aRect.left - aX;
+    else if (aX > xmost)
+      x = aX - xmost;
+
+    // compute vertical distance from top/bottom border depending if Y is
+    // above/inside/below the element's rectangle
+    if (aRect.top < aY && aY < ymost)
+      y = Math.min(ymost - aY, aY - aRect.top);
+    else if (aY < aRect.top)
+      y = aRect.top - aY;
+    if (aY > ymost)
+      y = aY - ymost;
+
+    return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
+  },
+
+  _els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService),
+  _clickableEvents: ["mousedown", "mouseup", "click"],
+  _hasMouseListener: function _hasMouseListener(aElement) {
+    let els = this._els;
+    let listeners = els.getListenerInfoFor(aElement, {});
+    for (let i = 0; i < listeners.length; i++) {
+      if (this._clickableEvents.indexOf(listeners[i].type) != -1)
+        return true;
+    }
+    return false;
+  },
+
+  getContentClientRects: function(aElement) {
+    let offset = { x: 0, y: 0 };
+
+    let nativeRects = aElement.getClientRects();
+    // step out of iframes and frames, offsetting scroll values
+    for (let frame = aElement.ownerDocument.defaultView; frame.frameElement; frame = frame.parent) {
+      // adjust client coordinates' origin to be top left of iframe viewport
+      let rect = frame.frameElement.getBoundingClientRect();
+      let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
+      let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
+      offset.x += rect.left + parseInt(left);
+      offset.y += rect.top + parseInt(top);
+    }
+
+    let result = [];
+    for (let i = nativeRects.length - 1; i >= 0; i--) {
+      let r = nativeRects[i];
+      result.push({ left: r.left + offset.x,
+                    top: r.top + offset.y,
+                    width: r.width,
+                    height: r.height
+                  });
+    }
+    return result;
+  },
+
   getBoundingContentRect: function(aElement) {
     if (!aElement)
       return {x: 0, y: 0, w: 0, h: 0};
 
     let document = aElement.ownerDocument;
     while (document.defaultView.frameElement)
       document = document.defaultView.frameElement.ownerDocument;