Bug 788073 - Use platform touch fluffing on Android. r=kats

This commit is contained in:
Wes Johnston 2013-08-06 09:37:27 -07:00
parent 3751cc02c9
commit 5669cbe8b0
2 changed files with 24 additions and 234 deletions

View File

@ -428,12 +428,19 @@ 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),
// 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
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);
// The percentage of the screen that needs to be scrolled before margins are exposed.
pref("browser.ui.show-margins-threshold", 20);

View File

@ -2044,12 +2044,8 @@ 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, 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);
// use the highlighted element if possible
let target = BrowserEventHandler._highlightElement;
if (!target)
return;
@ -2105,6 +2101,8 @@ 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) {
@ -4126,6 +4124,7 @@ 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)
@ -4138,19 +4137,11 @@ 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);
}
},
@ -4252,12 +4243,11 @@ var BrowserEventHandler = {
if (element) {
try {
let data = JSON.parse(aData);
let [x, y] = [data.x, data.y];
if (ElementTouchHelper.isElementClickable(element)) {
[x, y] = this._moveClickPoint(element, x, y);
element = ElementTouchHelper.anyElementFromPoint(x, y);
}
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
this._sendMouseEvent("mousemove", element, x, y);
this._sendMouseEvent("mousedown", element, x, y);
this._sendMouseEvent("mouseup", element, x, y);
@ -4514,35 +4504,6 @@ 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 {
@ -4638,184 +4599,6 @@ 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};