1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/browser-element/BrowserElementPanning.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,860 @@ 1.4 +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 sw=2 sts=2 et: */ 1.6 + 1.7 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.8 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.9 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.10 + 1.11 +"use strict"; 1.12 +dump("############################### browserElementPanning.js loaded\n"); 1.13 + 1.14 +let { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; 1.15 +Cu.import("resource://gre/modules/Services.jsm"); 1.16 +Cu.import("resource://gre/modules/Geometry.jsm"); 1.17 + 1.18 +var global = this; 1.19 + 1.20 +const kObservedEvents = [ 1.21 + "BEC:ShownModalPrompt", 1.22 + "Activity:Success", 1.23 + "Activity:Error" 1.24 +]; 1.25 + 1.26 +const ContentPanning = { 1.27 + // Are we listening to touch or mouse events? 1.28 + watchedEventsType: '', 1.29 + 1.30 + // Are mouse events being delivered to this content along with touch 1.31 + // events, in violation of spec? 1.32 + hybridEvents: false, 1.33 + 1.34 + init: function cp_init() { 1.35 + // If APZ is enabled, we do active element handling in C++ 1.36 + // (see widget/xpwidgets/ActiveElementManager.h), and panning 1.37 + // itself in APZ, so we don't need to handle any touch events here. 1.38 + if (docShell.asyncPanZoomEnabled === false) { 1.39 + this._setupListenersForPanning(); 1.40 + } 1.41 + 1.42 + addEventListener("unload", 1.43 + this._unloadHandler.bind(this), 1.44 + /* useCapture = */ false, 1.45 + /* wantsUntrusted = */ false); 1.46 + 1.47 + addMessageListener("Viewport:Change", this._recvViewportChange.bind(this)); 1.48 + addMessageListener("Gesture:DoubleTap", this._recvDoubleTap.bind(this)); 1.49 + addEventListener("visibilitychange", this._handleVisibilityChange.bind(this)); 1.50 + kObservedEvents.forEach((topic) => { 1.51 + Services.obs.addObserver(this, topic, false); 1.52 + }); 1.53 + }, 1.54 + 1.55 + _setupListenersForPanning: function cp_setupListenersForPanning() { 1.56 + var events; 1.57 + try { 1.58 + content.document.createEvent('TouchEvent'); 1.59 + events = ['touchstart', 'touchend', 'touchmove']; 1.60 + this.watchedEventsType = 'touch'; 1.61 +#ifdef MOZ_WIDGET_GONK 1.62 + // The gonk widget backend does not deliver mouse events per 1.63 + // spec. Third-party content isn't exposed to this behavior, 1.64 + // but that behavior creates some extra work for us here. 1.65 + let appInfo = Cc["@mozilla.org/xre/app-info;1"]; 1.66 + let isParentProcess = 1.67 + !appInfo || appInfo.getService(Ci.nsIXULRuntime) 1.68 + .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; 1.69 + this.hybridEvents = isParentProcess; 1.70 +#endif 1.71 + } catch(e) { 1.72 + // Touch events aren't supported, so fall back on mouse. 1.73 + events = ['mousedown', 'mouseup', 'mousemove']; 1.74 + this.watchedEventsType = 'mouse'; 1.75 + } 1.76 + 1.77 + let els = Cc["@mozilla.org/eventlistenerservice;1"] 1.78 + .getService(Ci.nsIEventListenerService); 1.79 + 1.80 + events.forEach(function(type) { 1.81 + // Using the system group for mouse/touch events to avoid 1.82 + // missing events if .stopPropagation() has been called. 1.83 + els.addSystemEventListener(global, type, 1.84 + this.handleEvent.bind(this), 1.85 + /* useCapture = */ false); 1.86 + }.bind(this)); 1.87 + }, 1.88 + 1.89 + handleEvent: function cp_handleEvent(evt) { 1.90 + // Ignore events targeting a <iframe mozbrowser> since those will be 1.91 + // handle by the BrowserElementPanning.js instance of it. 1.92 + if (evt.target instanceof Ci.nsIMozBrowserFrame) { 1.93 + return; 1.94 + } 1.95 + 1.96 + if (evt.defaultPrevented || evt.multipleActionsPrevented) { 1.97 + // clean up panning state even if touchend/mouseup has been preventDefault. 1.98 + if(evt.type === 'touchend' || evt.type === 'mouseup') { 1.99 + if (this.dragging && 1.100 + (this.watchedEventsType === 'mouse' || 1.101 + this.findPrimaryPointer(evt.changedTouches))) { 1.102 + this._finishPanning(); 1.103 + } 1.104 + } 1.105 + return; 1.106 + } 1.107 + 1.108 + switch (evt.type) { 1.109 + case 'mousedown': 1.110 + case 'touchstart': 1.111 + this.onTouchStart(evt); 1.112 + break; 1.113 + case 'mousemove': 1.114 + case 'touchmove': 1.115 + this.onTouchMove(evt); 1.116 + break; 1.117 + case 'mouseup': 1.118 + case 'touchend': 1.119 + this.onTouchEnd(evt); 1.120 + break; 1.121 + case 'click': 1.122 + evt.stopPropagation(); 1.123 + evt.preventDefault(); 1.124 + 1.125 + let target = evt.target; 1.126 + let view = target.ownerDocument ? target.ownerDocument.defaultView 1.127 + : target; 1.128 + view.removeEventListener('click', this, true, true); 1.129 + break; 1.130 + } 1.131 + }, 1.132 + 1.133 + observe: function cp_observe(subject, topic, data) { 1.134 + this._resetHover(); 1.135 + }, 1.136 + 1.137 + position: new Point(0 , 0), 1.138 + 1.139 + findPrimaryPointer: function cp_findPrimaryPointer(touches) { 1.140 + if (!('primaryPointerId' in this)) 1.141 + return null; 1.142 + 1.143 + for (let i = 0; i < touches.length; i++) { 1.144 + if (touches[i].identifier === this.primaryPointerId) { 1.145 + return touches[i]; 1.146 + } 1.147 + } 1.148 + return null; 1.149 + }, 1.150 + 1.151 + onTouchStart: function cp_onTouchStart(evt) { 1.152 + let screenX, screenY; 1.153 + if (this.watchedEventsType == 'touch') { 1.154 + if ('primaryPointerId' in this || evt.touches.length >= 2) { 1.155 + this._resetActive(); 1.156 + return; 1.157 + } 1.158 + 1.159 + let firstTouch = evt.changedTouches[0]; 1.160 + this.primaryPointerId = firstTouch.identifier; 1.161 + this.pointerDownTarget = firstTouch.target; 1.162 + screenX = firstTouch.screenX; 1.163 + screenY = firstTouch.screenY; 1.164 + } else { 1.165 + this.pointerDownTarget = evt.target; 1.166 + screenX = evt.screenX; 1.167 + screenY = evt.screenY; 1.168 + } 1.169 + this.dragging = true; 1.170 + this.panning = false; 1.171 + 1.172 + let oldTarget = this.target; 1.173 + [this.target, this.scrollCallback] = this.getPannable(this.pointerDownTarget); 1.174 + 1.175 + // If we have a pointer down target, we may need to fill in for EventStateManager 1.176 + // in setting the active state on the target element. Set a timer to 1.177 + // ensure the pointer-down target is active. (If it's already 1.178 + // active, the timer is a no-op.) 1.179 + if (this.pointerDownTarget !== null) { 1.180 + // If there's no possibility this is a drag/pan, activate now. 1.181 + // Otherwise wait a little bit to see if the gesture isn't a 1.182 + // tap. 1.183 + if (this.target === null) { 1.184 + this.notify(this._activationTimer); 1.185 + } else { 1.186 + this._activationTimer.initWithCallback(this, 1.187 + this._activationDelayMs, 1.188 + Ci.nsITimer.TYPE_ONE_SHOT); 1.189 + } 1.190 + } 1.191 + 1.192 + // If there is a pan animation running (from a previous pan gesture) and 1.193 + // the user touch back the screen, stop this animation immediatly and 1.194 + // prevent the possible click action if the touch happens on the same 1.195 + // target. 1.196 + this.preventNextClick = false; 1.197 + if (KineticPanning.active) { 1.198 + KineticPanning.stop(); 1.199 + 1.200 + if (oldTarget && oldTarget == this.target) 1.201 + this.preventNextClick = true; 1.202 + } 1.203 + 1.204 + this.position.set(screenX, screenY); 1.205 + KineticPanning.reset(); 1.206 + KineticPanning.record(new Point(0, 0), evt.timeStamp); 1.207 + 1.208 + // We prevent start events to avoid sending a focus event at the end of this 1.209 + // touch series. See bug 889717. 1.210 + if ((this.panning || this.preventNextClick)) { 1.211 + evt.preventDefault(); 1.212 + } 1.213 + }, 1.214 + 1.215 + onTouchEnd: function cp_onTouchEnd(evt) { 1.216 + let touch = null; 1.217 + if (!this.dragging || 1.218 + (this.watchedEventsType == 'touch' && 1.219 + !(touch = this.findPrimaryPointer(evt.changedTouches)))) { 1.220 + return; 1.221 + } 1.222 + 1.223 + // !isPan() and evt.detail should always give the same answer here 1.224 + // since they use the same heuristics, but use the native gecko 1.225 + // computation when possible. 1.226 + // 1.227 + // NB: when we're using touch events, then !KineticPanning.isPan() 1.228 + // => this.panning, so we'll never attempt to block the click 1.229 + // event. That's OK however, because we won't fire a synthetic 1.230 + // click when we're using touch events and this touch series 1.231 + // wasn't a "tap" gesture. 1.232 + let click = (this.watchedEventsType == 'mouse') ? 1.233 + evt.detail : !KineticPanning.isPan(); 1.234 + // Additionally, if we're seeing non-compliant hybrid events, a 1.235 + // "real" click will be generated if we started and ended on the 1.236 + // same element. 1.237 + if (this.hybridEvents) { 1.238 + let target = 1.239 + content.document.elementFromPoint(touch.clientX, touch.clientY); 1.240 + click |= (target === this.pointerDownTarget); 1.241 + } 1.242 + 1.243 + if (this.target && click && (this.panning || this.preventNextClick)) { 1.244 + if (this.hybridEvents) { 1.245 + let target = this.target; 1.246 + let view = target.ownerDocument ? target.ownerDocument.defaultView 1.247 + : target; 1.248 + view.addEventListener('click', this, true, true); 1.249 + } else { 1.250 + // We prevent end events to avoid sending a focus event. See bug 889717. 1.251 + evt.preventDefault(); 1.252 + } 1.253 + } else if (this.target && click && !this.panning) { 1.254 + this.notify(this._activationTimer); 1.255 + } 1.256 + 1.257 + this._finishPanning(); 1.258 + 1.259 + // Now that we're done, avoid entraining the thing we just panned. 1.260 + this.pointerDownTarget = null; 1.261 + }, 1.262 + 1.263 + onTouchMove: function cp_onTouchMove(evt) { 1.264 + if (!this.dragging) 1.265 + return; 1.266 + 1.267 + let screenX, screenY; 1.268 + if (this.watchedEventsType == 'touch') { 1.269 + let primaryTouch = this.findPrimaryPointer(evt.changedTouches); 1.270 + if (evt.touches.length > 1 || !primaryTouch) 1.271 + return; 1.272 + screenX = primaryTouch.screenX; 1.273 + screenY = primaryTouch.screenY; 1.274 + } else { 1.275 + screenX = evt.screenX; 1.276 + screenY = evt.screenY; 1.277 + } 1.278 + 1.279 + let current = this.position; 1.280 + let delta = new Point(screenX - current.x, screenY - current.y); 1.281 + current.set(screenX, screenY); 1.282 + 1.283 + KineticPanning.record(delta, evt.timeStamp); 1.284 + 1.285 + let isPan = KineticPanning.isPan(); 1.286 + 1.287 + // If we've detected a pan gesture, cancel the active state of the 1.288 + // current target. 1.289 + if (!this.panning && isPan) { 1.290 + this._resetActive(); 1.291 + } 1.292 + 1.293 + // There's no possibility of us panning anything. 1.294 + if (!this.scrollCallback) { 1.295 + return; 1.296 + } 1.297 + 1.298 + // Scroll manually. 1.299 + this.scrollCallback(delta.scale(-1)); 1.300 + 1.301 + if (!this.panning && isPan) { 1.302 + this.panning = true; 1.303 + this._activationTimer.cancel(); 1.304 + } 1.305 + 1.306 + if (this.panning) { 1.307 + // Only do this when we're actually executing a pan gesture. 1.308 + // Otherwise synthetic mouse events will be canceled. 1.309 + evt.stopPropagation(); 1.310 + evt.preventDefault(); 1.311 + } 1.312 + }, 1.313 + 1.314 + // nsITimerCallback 1.315 + notify: function cp_notify(timer) { 1.316 + this._setActive(this.pointerDownTarget); 1.317 + }, 1.318 + 1.319 + onKineticBegin: function cp_onKineticBegin(evt) { 1.320 + }, 1.321 + 1.322 + onKineticPan: function cp_onKineticPan(delta) { 1.323 + return !this.scrollCallback(delta); 1.324 + }, 1.325 + 1.326 + onKineticEnd: function cp_onKineticEnd() { 1.327 + if (!this.dragging) 1.328 + this.scrollCallback = null; 1.329 + }, 1.330 + 1.331 + getPannable: function cp_getPannable(node) { 1.332 + let pannableNode = this._findPannable(node); 1.333 + if (pannableNode) { 1.334 + return [pannableNode, this._generateCallback(pannableNode)]; 1.335 + } 1.336 + 1.337 + return [null, null]; 1.338 + }, 1.339 + 1.340 + _findPannable: function cp_findPannable(node) { 1.341 + if (!(node instanceof Ci.nsIDOMHTMLElement) || node.tagName == 'HTML') { 1.342 + return null; 1.343 + } 1.344 + 1.345 + let nodeContent = node.ownerDocument.defaultView; 1.346 + while (!(node instanceof Ci.nsIDOMHTMLBodyElement)) { 1.347 + let style = nodeContent.getComputedStyle(node, null); 1.348 + 1.349 + let overflow = [style.getPropertyValue('overflow'), 1.350 + style.getPropertyValue('overflow-x'), 1.351 + style.getPropertyValue('overflow-y')]; 1.352 + 1.353 + let rect = node.getBoundingClientRect(); 1.354 + let isAuto = (overflow.indexOf('auto') != -1 && 1.355 + (rect.height < node.scrollHeight || 1.356 + rect.width < node.scrollWidth)); 1.357 + 1.358 + let isScroll = (overflow.indexOf('scroll') != -1); 1.359 + 1.360 + let isScrollableTextarea = (node.tagName == 'TEXTAREA' && 1.361 + (node.scrollHeight > node.clientHeight || 1.362 + node.scrollWidth > node.clientWidth || 1.363 + ('scrollLeftMax' in node && node.scrollLeftMax > 0) || 1.364 + ('scrollTopMax' in node && node.scrollTopMax > 0))); 1.365 + if (isScroll || isAuto || isScrollableTextarea) { 1.366 + return node; 1.367 + } 1.368 + 1.369 + node = node.parentNode; 1.370 + } 1.371 + 1.372 + if (nodeContent.scrollMaxX || nodeContent.scrollMaxY) { 1.373 + return nodeContent; 1.374 + } 1.375 + 1.376 + if (nodeContent.frameElement) { 1.377 + return this._findPannable(nodeContent.frameElement); 1.378 + } 1.379 + 1.380 + return null; 1.381 + }, 1.382 + 1.383 + _generateCallback: function cp_generateCallback(root) { 1.384 + let firstScroll = true; 1.385 + let target; 1.386 + let current; 1.387 + let win, doc, htmlNode, bodyNode; 1.388 + 1.389 + function doScroll(node, delta) { 1.390 + if (node instanceof Ci.nsIDOMHTMLElement) { 1.391 + return node.scrollByNoFlush(delta.x, delta.y); 1.392 + } else if (node instanceof Ci.nsIDOMWindow) { 1.393 + win = node; 1.394 + doc = win.document; 1.395 + 1.396 + // "overflow:hidden" on either the <html> or the <body> node should 1.397 + // prevent the user from scrolling the root viewport. 1.398 + if (doc instanceof Ci.nsIDOMHTMLDocument) { 1.399 + htmlNode = doc.documentElement; 1.400 + bodyNode = doc.body; 1.401 + if (win.getComputedStyle(htmlNode, null).overflowX == "hidden" || 1.402 + win.getComputedStyle(bodyNode, null).overflowX == "hidden") { 1.403 + delta.x = 0; 1.404 + } 1.405 + if (win.getComputedStyle(htmlNode, null).overflowY == "hidden" || 1.406 + win.getComputedStyle(bodyNode, null).overflowY == "hidden") { 1.407 + delta.y = 0; 1.408 + } 1.409 + } 1.410 + let oldX = node.scrollX; 1.411 + let oldY = node.scrollY; 1.412 + node.scrollBy(delta.x, delta.y); 1.413 + return (node.scrollX != oldX || node.scrollY != oldY); 1.414 + } 1.415 + // If we get here, |node| isn't an HTML element and it's not a window, 1.416 + // but findPannable apparently thought it was scrollable... What is it? 1.417 + return false; 1.418 + } 1.419 + 1.420 + function targetParent(node) { 1.421 + return node.parentNode || node.frameElement || null; 1.422 + } 1.423 + 1.424 + function scroll(delta) { 1.425 + current = root; 1.426 + firstScroll = true; 1.427 + while (current) { 1.428 + if (doScroll(current, delta)) { 1.429 + firstScroll = false; 1.430 + return true; 1.431 + } 1.432 + 1.433 + // TODO The current code looks for possible scrolling regions only if 1.434 + // this is the first scroll action but this should be more dynamic. 1.435 + if (!firstScroll) { 1.436 + return false; 1.437 + } 1.438 + 1.439 + current = ContentPanning._findPannable(targetParent(current)); 1.440 + } 1.441 + 1.442 + // There is nothing scrollable here. 1.443 + return false; 1.444 + } 1.445 + return scroll; 1.446 + }, 1.447 + 1.448 + get _domUtils() { 1.449 + delete this._domUtils; 1.450 + return this._domUtils = Cc['@mozilla.org/inspector/dom-utils;1'] 1.451 + .getService(Ci.inIDOMUtils); 1.452 + }, 1.453 + 1.454 + get _activationTimer() { 1.455 + delete this._activationTimer; 1.456 + return this._activationTimer = Cc["@mozilla.org/timer;1"] 1.457 + .createInstance(Ci.nsITimer); 1.458 + }, 1.459 + 1.460 + get _activationDelayMs() { 1.461 + let delay = Services.prefs.getIntPref('ui.touch_activation.delay_ms'); 1.462 + delete this._activationDelayMs; 1.463 + return this._activationDelayMs = delay; 1.464 + }, 1.465 + 1.466 + _resetActive: function cp_resetActive() { 1.467 + let elt = this.pointerDownTarget || this.target; 1.468 + let root = elt.ownerDocument || elt.document; 1.469 + this._setActive(root.documentElement); 1.470 + }, 1.471 + 1.472 + _resetHover: function cp_resetHover() { 1.473 + const kStateHover = 0x00000004; 1.474 + try { 1.475 + let element = content.document.createElement('foo'); 1.476 + this._domUtils.setContentState(element, kStateHover); 1.477 + } catch(e) {} 1.478 + }, 1.479 + 1.480 + _setActive: function cp_setActive(elt) { 1.481 + const kStateActive = 0x00000001; 1.482 + this._domUtils.setContentState(elt, kStateActive); 1.483 + }, 1.484 + 1.485 + _recvViewportChange: function(data) { 1.486 + let metrics = data.json; 1.487 + this._viewport = new Rect(metrics.x, metrics.y, 1.488 + metrics.viewport.width, 1.489 + metrics.viewport.height); 1.490 + this._cssCompositedRect = new Rect(metrics.x, metrics.y, 1.491 + metrics.cssCompositedRect.width, 1.492 + metrics.cssCompositedRect.height); 1.493 + this._cssPageRect = new Rect(metrics.cssPageRect.x, 1.494 + metrics.cssPageRect.y, 1.495 + metrics.cssPageRect.width, 1.496 + metrics.cssPageRect.height); 1.497 + }, 1.498 + 1.499 + _recvDoubleTap: function(data) { 1.500 + let data = data.json; 1.501 + 1.502 + // We haven't received a metrics update yet; don't do anything. 1.503 + if (this._viewport == null) { 1.504 + return; 1.505 + } 1.506 + 1.507 + let win = content; 1.508 + 1.509 + let element = ElementTouchHelper.anyElementFromPoint(win, data.x, data.y); 1.510 + if (!element) { 1.511 + this._zoomOut(); 1.512 + return; 1.513 + } 1.514 + 1.515 + while (element && !this._shouldZoomToElement(element)) 1.516 + element = element.parentNode; 1.517 + 1.518 + if (!element) { 1.519 + this._zoomOut(); 1.520 + } else { 1.521 + const margin = 15; 1.522 + let rect = ElementTouchHelper.getBoundingContentRect(element); 1.523 + 1.524 + let cssPageRect = this._cssPageRect; 1.525 + let viewport = this._viewport; 1.526 + let bRect = new Rect(Math.max(cssPageRect.x, rect.x - margin), 1.527 + rect.y, 1.528 + rect.w + 2 * margin, 1.529 + rect.h); 1.530 + // constrict the rect to the screen's right edge 1.531 + bRect.width = Math.min(bRect.width, cssPageRect.right - bRect.x); 1.532 + 1.533 + // if the rect is already taking up most of the visible area and is stretching the 1.534 + // width of the page, then we want to zoom out instead. 1.535 + if (this._isRectZoomedIn(bRect, this._cssCompositedRect)) { 1.536 + this._zoomOut(); 1.537 + return; 1.538 + } 1.539 + 1.540 + rect.x = Math.round(bRect.x); 1.541 + rect.y = Math.round(bRect.y); 1.542 + rect.w = Math.round(bRect.width); 1.543 + rect.h = Math.round(bRect.height); 1.544 + 1.545 + // if the block we're zooming to is really tall, and the user double-tapped 1.546 + // more than a screenful of height from the top of it, then adjust the y-coordinate 1.547 + // so that we center the actual point the user double-tapped upon. this prevents 1.548 + // flying to the top of a page when double-tapping to zoom in (bug 761721). 1.549 + // the 1.2 multiplier is just a little fuzz to compensate for bRect including horizontal 1.550 + // margins but not vertical ones. 1.551 + let cssTapY = viewport.y + data.y; 1.552 + if ((bRect.height > rect.h) && (cssTapY > rect.y + (rect.h * 1.2))) { 1.553 + rect.y = cssTapY - (rect.h / 2); 1.554 + } 1.555 + 1.556 + Services.obs.notifyObservers(docShell, 'browser-zoom-to-rect', JSON.stringify(rect)); 1.557 + } 1.558 + }, 1.559 + 1.560 + _handleVisibilityChange: function(evt) { 1.561 + if (!evt.target.hidden) 1.562 + return; 1.563 + 1.564 + this._resetHover(); 1.565 + }, 1.566 + 1.567 + _shouldZoomToElement: function(aElement) { 1.568 + let win = aElement.ownerDocument.defaultView; 1.569 + if (win.getComputedStyle(aElement, null).display == "inline") 1.570 + return false; 1.571 + if (aElement instanceof Ci.nsIDOMHTMLLIElement) 1.572 + return false; 1.573 + if (aElement instanceof Ci.nsIDOMHTMLQuoteElement) 1.574 + return false; 1.575 + return true; 1.576 + }, 1.577 + 1.578 + _zoomOut: function() { 1.579 + let rect = new Rect(0, 0, 0, 0); 1.580 + Services.obs.notifyObservers(docShell, 'browser-zoom-to-rect', JSON.stringify(rect)); 1.581 + }, 1.582 + 1.583 + _isRectZoomedIn: function(aRect, aViewport) { 1.584 + // This function checks to see if the area of the rect visible in the 1.585 + // viewport (i.e. the "overlapArea" variable below) is approximately 1.586 + // the max area of the rect we can show. 1.587 + let vRect = new Rect(aViewport.x, aViewport.y, aViewport.width, aViewport.height); 1.588 + let overlap = vRect.intersect(aRect); 1.589 + let overlapArea = overlap.width * overlap.height; 1.590 + let availHeight = Math.min(aRect.width * vRect.height / vRect.width, aRect.height); 1.591 + let showing = overlapArea / (aRect.width * availHeight); 1.592 + let ratioW = (aRect.width / vRect.width); 1.593 + let ratioH = (aRect.height / vRect.height); 1.594 + 1.595 + return (showing > 0.9 && (ratioW > 0.9 || ratioH > 0.9)); 1.596 + }, 1.597 + 1.598 + _finishPanning: function() { 1.599 + this.dragging = false; 1.600 + delete this.primaryPointerId; 1.601 + this._activationTimer.cancel(); 1.602 + 1.603 + // If there is a scroll action, let's do a manual kinetic panning action. 1.604 + if (this.panning) { 1.605 + KineticPanning.start(this); 1.606 + } 1.607 + }, 1.608 + 1.609 + _unloadHandler: function() { 1.610 + kObservedEvents.forEach((topic) => { 1.611 + Services.obs.removeObserver(this, topic); 1.612 + }); 1.613 + } 1.614 +}; 1.615 + 1.616 +// Min/max velocity of kinetic panning. This is in pixels/millisecond. 1.617 +const kMinVelocity = 0.2; 1.618 +const kMaxVelocity = 6; 1.619 + 1.620 +// Constants that affect the "friction" of the scroll pane. 1.621 +const kExponentialC = 1000; 1.622 +const kPolynomialC = 100 / 1000000; 1.623 + 1.624 +// How often do we change the position of the scroll pane? 1.625 +// Too often and panning may jerk near the end. 1.626 +// Too little and panning will be choppy. In milliseconds. 1.627 +const kUpdateInterval = 16; 1.628 + 1.629 +// The numbers of momentums to use for calculating the velocity of the pan. 1.630 +// Those are taken from the end of the action 1.631 +const kSamples = 5; 1.632 + 1.633 +const KineticPanning = { 1.634 + _position: new Point(0, 0), 1.635 + _velocity: new Point(0, 0), 1.636 + _acceleration: new Point(0, 0), 1.637 + 1.638 + get active() { 1.639 + return this.target !== null; 1.640 + }, 1.641 + 1.642 + target: null, 1.643 + start: function kp_start(target) { 1.644 + this.target = target; 1.645 + 1.646 + // Calculate the initial velocity of the movement based on user input 1.647 + let momentums = this.momentums; 1.648 + let flick = momentums[momentums.length - 1].time - momentums[0].time < 300; 1.649 + 1.650 + let distance = new Point(0, 0); 1.651 + momentums.forEach(function(momentum) { 1.652 + distance.add(momentum.dx, momentum.dy); 1.653 + }); 1.654 + 1.655 + function clampFromZero(x, min, max) { 1.656 + if (x >= 0) 1.657 + return Math.max(min, Math.min(max, x)); 1.658 + return Math.min(-min, Math.max(-max, x)); 1.659 + } 1.660 + 1.661 + let elapsed = momentums[momentums.length - 1].time - momentums[0].time; 1.662 + let velocityX = clampFromZero(distance.x / elapsed, 0, kMaxVelocity); 1.663 + let velocityY = clampFromZero(distance.y / elapsed, 0, kMaxVelocity); 1.664 + 1.665 + let velocity = this._velocity; 1.666 + if (flick) { 1.667 + // Very fast pan action that does not generate a click are very often pan 1.668 + // action. If this is a small gesture then it will not move the view a lot 1.669 + // and so it will be above the minimun threshold and not generate any 1.670 + // kinetic panning. This does not look on a device since this is often 1.671 + // a real gesture, so let's lower the velocity threshold for such moves. 1.672 + velocity.set(velocityX, velocityY); 1.673 + } else { 1.674 + velocity.set(Math.abs(velocityX) < kMinVelocity ? 0 : velocityX, 1.675 + Math.abs(velocityY) < kMinVelocity ? 0 : velocityY); 1.676 + } 1.677 + this.momentums = []; 1.678 + 1.679 + // Set acceleration vector to opposite signs of velocity 1.680 + function sign(x) { 1.681 + return x ? (x > 0 ? 1 : -1) : 0; 1.682 + } 1.683 + 1.684 + this._acceleration.set(velocity.clone().map(sign).scale(-kPolynomialC)); 1.685 + 1.686 + // Reset the position 1.687 + this._position.set(0, 0); 1.688 + 1.689 + this._startAnimation(); 1.690 + 1.691 + this.target.onKineticBegin(); 1.692 + }, 1.693 + 1.694 + stop: function kp_stop() { 1.695 + this.reset(); 1.696 + 1.697 + if (!this.target) 1.698 + return; 1.699 + 1.700 + this.target.onKineticEnd(); 1.701 + this.target = null; 1.702 + }, 1.703 + 1.704 + reset: function kp_reset() { 1.705 + this.momentums = []; 1.706 + this.distance.set(0, 0); 1.707 + }, 1.708 + 1.709 + momentums: [], 1.710 + record: function kp_record(delta, timestamp) { 1.711 + this.momentums.push({ 'time': this._getTime(timestamp), 1.712 + 'dx' : delta.x, 'dy' : delta.y }); 1.713 + 1.714 + // We only need to keep kSamples in this.momentums. 1.715 + if (this.momentums.length > kSamples) { 1.716 + this.momentums.shift(); 1.717 + } 1.718 + 1.719 + this.distance.add(delta.x, delta.y); 1.720 + }, 1.721 + 1.722 + _getTime: function kp_getTime(time) { 1.723 + // Touch events generated by the platform or hand-made are defined in 1.724 + // microseconds instead of milliseconds. Bug 77992 will fix this at the 1.725 + // platform level. 1.726 + if (time > Date.now()) { 1.727 + return Math.floor(time / 1000); 1.728 + } else { 1.729 + return time; 1.730 + } 1.731 + }, 1.732 + 1.733 + get threshold() { 1.734 + let dpi = content.QueryInterface(Ci.nsIInterfaceRequestor) 1.735 + .getInterface(Ci.nsIDOMWindowUtils) 1.736 + .displayDPI; 1.737 + 1.738 + let threshold = Services.prefs.getIntPref('ui.dragThresholdX') / 240 * dpi; 1.739 + 1.740 + delete this.threshold; 1.741 + return this.threshold = threshold; 1.742 + }, 1.743 + 1.744 + distance: new Point(0, 0), 1.745 + isPan: function cp_isPan() { 1.746 + return (Math.abs(this.distance.x) > this.threshold || 1.747 + Math.abs(this.distance.y) > this.threshold); 1.748 + }, 1.749 + 1.750 + _startAnimation: function kp_startAnimation() { 1.751 + let c = kExponentialC; 1.752 + function getNextPosition(position, v, a, t) { 1.753 + // Important traits for this function: 1.754 + // p(t=0) is 0 1.755 + // p'(t=0) is v0 1.756 + // 1.757 + // We use exponential to get a smoother stop, but by itself exponential 1.758 + // is too smooth at the end. Adding a polynomial with the appropriate 1.759 + // weight helps to balance 1.760 + position.set(v.x * Math.exp(-t / c) * -c + a.x * t * t + v.x * c, 1.761 + v.y * Math.exp(-t / c) * -c + a.y * t * t + v.y * c); 1.762 + } 1.763 + 1.764 + let startTime = content.mozAnimationStartTime; 1.765 + let elapsedTime = 0, targetedTime = 0, averageTime = 0; 1.766 + 1.767 + let velocity = this._velocity; 1.768 + let acceleration = this._acceleration; 1.769 + 1.770 + let position = this._position; 1.771 + let nextPosition = new Point(0, 0); 1.772 + let delta = new Point(0, 0); 1.773 + 1.774 + let callback = (function(timestamp) { 1.775 + if (!this.target) 1.776 + return; 1.777 + 1.778 + // To make animation end fast enough but to keep smoothness, average the 1.779 + // ideal time frame (smooth animation) with the actual time lapse 1.780 + // (end fast enough). 1.781 + // Animation will never take longer than 2 times the ideal length of time. 1.782 + elapsedTime = timestamp - startTime; 1.783 + targetedTime += kUpdateInterval; 1.784 + averageTime = (targetedTime + elapsedTime) / 2; 1.785 + 1.786 + // Calculate new position. 1.787 + getNextPosition(nextPosition, velocity, acceleration, averageTime); 1.788 + delta.set(Math.round(nextPosition.x - position.x), 1.789 + Math.round(nextPosition.y - position.y)); 1.790 + 1.791 + // Test to see if movement is finished for each component. 1.792 + if (delta.x * acceleration.x > 0) 1.793 + delta.x = position.x = velocity.x = acceleration.x = 0; 1.794 + 1.795 + if (delta.y * acceleration.y > 0) 1.796 + delta.y = position.y = velocity.y = acceleration.y = 0; 1.797 + 1.798 + if (velocity.equals(0, 0) || delta.equals(0, 0)) { 1.799 + this.stop(); 1.800 + return; 1.801 + } 1.802 + 1.803 + position.add(delta); 1.804 + if (this.target.onKineticPan(delta.scale(-1))) { 1.805 + this.stop(); 1.806 + return; 1.807 + } 1.808 + 1.809 + content.mozRequestAnimationFrame(callback); 1.810 + }).bind(this); 1.811 + 1.812 + content.mozRequestAnimationFrame(callback); 1.813 + } 1.814 +}; 1.815 + 1.816 +const ElementTouchHelper = { 1.817 + anyElementFromPoint: function(aWindow, aX, aY) { 1.818 + let cwu = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.819 + let elem = cwu.elementFromPoint(aX, aY, true, true); 1.820 + 1.821 + let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement; 1.822 + let HTMLFrameElement = Ci.nsIDOMHTMLFrameElement; 1.823 + while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) { 1.824 + let rect = elem.getBoundingClientRect(); 1.825 + aX -= rect.left; 1.826 + aY -= rect.top; 1.827 + cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.828 + elem = cwu.elementFromPoint(aX, aY, true, true); 1.829 + } 1.830 + 1.831 + return elem; 1.832 + }, 1.833 + 1.834 + getBoundingContentRect: function(aElement) { 1.835 + if (!aElement) 1.836 + return {x: 0, y: 0, w: 0, h: 0}; 1.837 + 1.838 + let document = aElement.ownerDocument; 1.839 + while (document.defaultView.frameElement) 1.840 + document = document.defaultView.frameElement.ownerDocument; 1.841 + 1.842 + let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.843 + let scrollX = {}, scrollY = {}; 1.844 + cwu.getScrollXY(false, scrollX, scrollY); 1.845 + 1.846 + let r = aElement.getBoundingClientRect(); 1.847 + 1.848 + // step out of iframes and frames, offsetting scroll values 1.849 + for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && frame != content; frame = frame.parent) { 1.850 + // adjust client coordinates' origin to be top left of iframe viewport 1.851 + let rect = frame.frameElement.getBoundingClientRect(); 1.852 + let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth; 1.853 + let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth; 1.854 + scrollX.value += rect.left + parseInt(left); 1.855 + scrollY.value += rect.top + parseInt(top); 1.856 + } 1.857 + 1.858 + return {x: r.left + scrollX.value, 1.859 + y: r.top + scrollY.value, 1.860 + w: r.width, 1.861 + h: r.height }; 1.862 + } 1.863 +};