diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index 7d54b83ba0e7..d207b4895072 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -428,19 +428,12 @@ pref("font.size.inflation.minTwips", 120); 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); diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 9f04b2ab10a1..5381da968b85 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -2044,8 +2044,12 @@ var NativeWindow = { // 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; @@ -2101,8 +2105,6 @@ var NativeWindow = { // 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) { @@ -4124,7 +4126,6 @@ var BrowserEventHandler = { 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) @@ -4137,11 +4138,19 @@ var BrowserEventHandler = { 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); } }, @@ -4243,23 +4252,16 @@ var BrowserEventHandler = { 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)) @@ -4512,6 +4514,45 @@ var BrowserEventHandler = { 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) @@ -4597,6 +4638,184 @@ const ElementTouchHelper = { 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};