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 141585 1fb5d14e8348b16014fafa5d746e0a8823df8187
parent 141584 0fba8893a8665e0725fa6dc672b4ebeebb4ecc9b
child 141586 f1b9e3f2d4c5b17e53d2391156f4682034377bf1
child 141684 2fcbc0a619f2d33123df6e73d8de82da7e69f942
child 141690 c3b23d6af64f79ae649cce603a57f9d4500f8a63
child 141711 a6e16bd50b96ab57916d96ecc60be2c9067ecde0
child 155691 eba98a54c087b7c0d71550fc5c797ef827685d73
push id32143
push userryanvm@gmail.com
push dateTue, 06 Aug 2013 21:07:36 +0000
treeherdermozilla-inbound@f1b9e3f2d4c5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs901151, 788073
milestone26.0a1
backs out6e7a45127e07da401d2fd048280a47580eab373b
bf050e52851f1fcd9037a0bf1700b829e92848a8
first release with
nightly linux32
1fb5d14e8348 / 26.0a1 / 20130807030216 / files
nightly linux64
1fb5d14e8348 / 26.0a1 / 20130807030216 / files
nightly mac
1fb5d14e8348 / 26.0a1 / 20130807030216 / files
nightly win32
1fb5d14e8348 / 26.0a1 / 20130807030216 / files
nightly win64
1fb5d14e8348 / 26.0a1 / 20130807030216 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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;