Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
michael@0 | 1 | /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
michael@0 | 2 | /* vim: set ts=2 sw=2 sts=2 et: */ |
michael@0 | 3 | |
michael@0 | 4 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 6 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 7 | |
michael@0 | 8 | "use strict"; |
michael@0 | 9 | dump("############################### browserElementPanning.js loaded\n"); |
michael@0 | 10 | |
michael@0 | 11 | let { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; |
michael@0 | 12 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 13 | Cu.import("resource://gre/modules/Geometry.jsm"); |
michael@0 | 14 | |
michael@0 | 15 | var global = this; |
michael@0 | 16 | |
michael@0 | 17 | const kObservedEvents = [ |
michael@0 | 18 | "BEC:ShownModalPrompt", |
michael@0 | 19 | "Activity:Success", |
michael@0 | 20 | "Activity:Error" |
michael@0 | 21 | ]; |
michael@0 | 22 | |
michael@0 | 23 | const ContentPanning = { |
michael@0 | 24 | // Are we listening to touch or mouse events? |
michael@0 | 25 | watchedEventsType: '', |
michael@0 | 26 | |
michael@0 | 27 | // Are mouse events being delivered to this content along with touch |
michael@0 | 28 | // events, in violation of spec? |
michael@0 | 29 | hybridEvents: false, |
michael@0 | 30 | |
michael@0 | 31 | init: function cp_init() { |
michael@0 | 32 | // If APZ is enabled, we do active element handling in C++ |
michael@0 | 33 | // (see widget/xpwidgets/ActiveElementManager.h), and panning |
michael@0 | 34 | // itself in APZ, so we don't need to handle any touch events here. |
michael@0 | 35 | if (docShell.asyncPanZoomEnabled === false) { |
michael@0 | 36 | this._setupListenersForPanning(); |
michael@0 | 37 | } |
michael@0 | 38 | |
michael@0 | 39 | addEventListener("unload", |
michael@0 | 40 | this._unloadHandler.bind(this), |
michael@0 | 41 | /* useCapture = */ false, |
michael@0 | 42 | /* wantsUntrusted = */ false); |
michael@0 | 43 | |
michael@0 | 44 | addMessageListener("Viewport:Change", this._recvViewportChange.bind(this)); |
michael@0 | 45 | addMessageListener("Gesture:DoubleTap", this._recvDoubleTap.bind(this)); |
michael@0 | 46 | addEventListener("visibilitychange", this._handleVisibilityChange.bind(this)); |
michael@0 | 47 | kObservedEvents.forEach((topic) => { |
michael@0 | 48 | Services.obs.addObserver(this, topic, false); |
michael@0 | 49 | }); |
michael@0 | 50 | }, |
michael@0 | 51 | |
michael@0 | 52 | _setupListenersForPanning: function cp_setupListenersForPanning() { |
michael@0 | 53 | var events; |
michael@0 | 54 | try { |
michael@0 | 55 | content.document.createEvent('TouchEvent'); |
michael@0 | 56 | events = ['touchstart', 'touchend', 'touchmove']; |
michael@0 | 57 | this.watchedEventsType = 'touch'; |
michael@0 | 58 | #ifdef MOZ_WIDGET_GONK |
michael@0 | 59 | // The gonk widget backend does not deliver mouse events per |
michael@0 | 60 | // spec. Third-party content isn't exposed to this behavior, |
michael@0 | 61 | // but that behavior creates some extra work for us here. |
michael@0 | 62 | let appInfo = Cc["@mozilla.org/xre/app-info;1"]; |
michael@0 | 63 | let isParentProcess = |
michael@0 | 64 | !appInfo || appInfo.getService(Ci.nsIXULRuntime) |
michael@0 | 65 | .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; |
michael@0 | 66 | this.hybridEvents = isParentProcess; |
michael@0 | 67 | #endif |
michael@0 | 68 | } catch(e) { |
michael@0 | 69 | // Touch events aren't supported, so fall back on mouse. |
michael@0 | 70 | events = ['mousedown', 'mouseup', 'mousemove']; |
michael@0 | 71 | this.watchedEventsType = 'mouse'; |
michael@0 | 72 | } |
michael@0 | 73 | |
michael@0 | 74 | let els = Cc["@mozilla.org/eventlistenerservice;1"] |
michael@0 | 75 | .getService(Ci.nsIEventListenerService); |
michael@0 | 76 | |
michael@0 | 77 | events.forEach(function(type) { |
michael@0 | 78 | // Using the system group for mouse/touch events to avoid |
michael@0 | 79 | // missing events if .stopPropagation() has been called. |
michael@0 | 80 | els.addSystemEventListener(global, type, |
michael@0 | 81 | this.handleEvent.bind(this), |
michael@0 | 82 | /* useCapture = */ false); |
michael@0 | 83 | }.bind(this)); |
michael@0 | 84 | }, |
michael@0 | 85 | |
michael@0 | 86 | handleEvent: function cp_handleEvent(evt) { |
michael@0 | 87 | // Ignore events targeting a <iframe mozbrowser> since those will be |
michael@0 | 88 | // handle by the BrowserElementPanning.js instance of it. |
michael@0 | 89 | if (evt.target instanceof Ci.nsIMozBrowserFrame) { |
michael@0 | 90 | return; |
michael@0 | 91 | } |
michael@0 | 92 | |
michael@0 | 93 | if (evt.defaultPrevented || evt.multipleActionsPrevented) { |
michael@0 | 94 | // clean up panning state even if touchend/mouseup has been preventDefault. |
michael@0 | 95 | if(evt.type === 'touchend' || evt.type === 'mouseup') { |
michael@0 | 96 | if (this.dragging && |
michael@0 | 97 | (this.watchedEventsType === 'mouse' || |
michael@0 | 98 | this.findPrimaryPointer(evt.changedTouches))) { |
michael@0 | 99 | this._finishPanning(); |
michael@0 | 100 | } |
michael@0 | 101 | } |
michael@0 | 102 | return; |
michael@0 | 103 | } |
michael@0 | 104 | |
michael@0 | 105 | switch (evt.type) { |
michael@0 | 106 | case 'mousedown': |
michael@0 | 107 | case 'touchstart': |
michael@0 | 108 | this.onTouchStart(evt); |
michael@0 | 109 | break; |
michael@0 | 110 | case 'mousemove': |
michael@0 | 111 | case 'touchmove': |
michael@0 | 112 | this.onTouchMove(evt); |
michael@0 | 113 | break; |
michael@0 | 114 | case 'mouseup': |
michael@0 | 115 | case 'touchend': |
michael@0 | 116 | this.onTouchEnd(evt); |
michael@0 | 117 | break; |
michael@0 | 118 | case 'click': |
michael@0 | 119 | evt.stopPropagation(); |
michael@0 | 120 | evt.preventDefault(); |
michael@0 | 121 | |
michael@0 | 122 | let target = evt.target; |
michael@0 | 123 | let view = target.ownerDocument ? target.ownerDocument.defaultView |
michael@0 | 124 | : target; |
michael@0 | 125 | view.removeEventListener('click', this, true, true); |
michael@0 | 126 | break; |
michael@0 | 127 | } |
michael@0 | 128 | }, |
michael@0 | 129 | |
michael@0 | 130 | observe: function cp_observe(subject, topic, data) { |
michael@0 | 131 | this._resetHover(); |
michael@0 | 132 | }, |
michael@0 | 133 | |
michael@0 | 134 | position: new Point(0 , 0), |
michael@0 | 135 | |
michael@0 | 136 | findPrimaryPointer: function cp_findPrimaryPointer(touches) { |
michael@0 | 137 | if (!('primaryPointerId' in this)) |
michael@0 | 138 | return null; |
michael@0 | 139 | |
michael@0 | 140 | for (let i = 0; i < touches.length; i++) { |
michael@0 | 141 | if (touches[i].identifier === this.primaryPointerId) { |
michael@0 | 142 | return touches[i]; |
michael@0 | 143 | } |
michael@0 | 144 | } |
michael@0 | 145 | return null; |
michael@0 | 146 | }, |
michael@0 | 147 | |
michael@0 | 148 | onTouchStart: function cp_onTouchStart(evt) { |
michael@0 | 149 | let screenX, screenY; |
michael@0 | 150 | if (this.watchedEventsType == 'touch') { |
michael@0 | 151 | if ('primaryPointerId' in this || evt.touches.length >= 2) { |
michael@0 | 152 | this._resetActive(); |
michael@0 | 153 | return; |
michael@0 | 154 | } |
michael@0 | 155 | |
michael@0 | 156 | let firstTouch = evt.changedTouches[0]; |
michael@0 | 157 | this.primaryPointerId = firstTouch.identifier; |
michael@0 | 158 | this.pointerDownTarget = firstTouch.target; |
michael@0 | 159 | screenX = firstTouch.screenX; |
michael@0 | 160 | screenY = firstTouch.screenY; |
michael@0 | 161 | } else { |
michael@0 | 162 | this.pointerDownTarget = evt.target; |
michael@0 | 163 | screenX = evt.screenX; |
michael@0 | 164 | screenY = evt.screenY; |
michael@0 | 165 | } |
michael@0 | 166 | this.dragging = true; |
michael@0 | 167 | this.panning = false; |
michael@0 | 168 | |
michael@0 | 169 | let oldTarget = this.target; |
michael@0 | 170 | [this.target, this.scrollCallback] = this.getPannable(this.pointerDownTarget); |
michael@0 | 171 | |
michael@0 | 172 | // If we have a pointer down target, we may need to fill in for EventStateManager |
michael@0 | 173 | // in setting the active state on the target element. Set a timer to |
michael@0 | 174 | // ensure the pointer-down target is active. (If it's already |
michael@0 | 175 | // active, the timer is a no-op.) |
michael@0 | 176 | if (this.pointerDownTarget !== null) { |
michael@0 | 177 | // If there's no possibility this is a drag/pan, activate now. |
michael@0 | 178 | // Otherwise wait a little bit to see if the gesture isn't a |
michael@0 | 179 | // tap. |
michael@0 | 180 | if (this.target === null) { |
michael@0 | 181 | this.notify(this._activationTimer); |
michael@0 | 182 | } else { |
michael@0 | 183 | this._activationTimer.initWithCallback(this, |
michael@0 | 184 | this._activationDelayMs, |
michael@0 | 185 | Ci.nsITimer.TYPE_ONE_SHOT); |
michael@0 | 186 | } |
michael@0 | 187 | } |
michael@0 | 188 | |
michael@0 | 189 | // If there is a pan animation running (from a previous pan gesture) and |
michael@0 | 190 | // the user touch back the screen, stop this animation immediatly and |
michael@0 | 191 | // prevent the possible click action if the touch happens on the same |
michael@0 | 192 | // target. |
michael@0 | 193 | this.preventNextClick = false; |
michael@0 | 194 | if (KineticPanning.active) { |
michael@0 | 195 | KineticPanning.stop(); |
michael@0 | 196 | |
michael@0 | 197 | if (oldTarget && oldTarget == this.target) |
michael@0 | 198 | this.preventNextClick = true; |
michael@0 | 199 | } |
michael@0 | 200 | |
michael@0 | 201 | this.position.set(screenX, screenY); |
michael@0 | 202 | KineticPanning.reset(); |
michael@0 | 203 | KineticPanning.record(new Point(0, 0), evt.timeStamp); |
michael@0 | 204 | |
michael@0 | 205 | // We prevent start events to avoid sending a focus event at the end of this |
michael@0 | 206 | // touch series. See bug 889717. |
michael@0 | 207 | if ((this.panning || this.preventNextClick)) { |
michael@0 | 208 | evt.preventDefault(); |
michael@0 | 209 | } |
michael@0 | 210 | }, |
michael@0 | 211 | |
michael@0 | 212 | onTouchEnd: function cp_onTouchEnd(evt) { |
michael@0 | 213 | let touch = null; |
michael@0 | 214 | if (!this.dragging || |
michael@0 | 215 | (this.watchedEventsType == 'touch' && |
michael@0 | 216 | !(touch = this.findPrimaryPointer(evt.changedTouches)))) { |
michael@0 | 217 | return; |
michael@0 | 218 | } |
michael@0 | 219 | |
michael@0 | 220 | // !isPan() and evt.detail should always give the same answer here |
michael@0 | 221 | // since they use the same heuristics, but use the native gecko |
michael@0 | 222 | // computation when possible. |
michael@0 | 223 | // |
michael@0 | 224 | // NB: when we're using touch events, then !KineticPanning.isPan() |
michael@0 | 225 | // => this.panning, so we'll never attempt to block the click |
michael@0 | 226 | // event. That's OK however, because we won't fire a synthetic |
michael@0 | 227 | // click when we're using touch events and this touch series |
michael@0 | 228 | // wasn't a "tap" gesture. |
michael@0 | 229 | let click = (this.watchedEventsType == 'mouse') ? |
michael@0 | 230 | evt.detail : !KineticPanning.isPan(); |
michael@0 | 231 | // Additionally, if we're seeing non-compliant hybrid events, a |
michael@0 | 232 | // "real" click will be generated if we started and ended on the |
michael@0 | 233 | // same element. |
michael@0 | 234 | if (this.hybridEvents) { |
michael@0 | 235 | let target = |
michael@0 | 236 | content.document.elementFromPoint(touch.clientX, touch.clientY); |
michael@0 | 237 | click |= (target === this.pointerDownTarget); |
michael@0 | 238 | } |
michael@0 | 239 | |
michael@0 | 240 | if (this.target && click && (this.panning || this.preventNextClick)) { |
michael@0 | 241 | if (this.hybridEvents) { |
michael@0 | 242 | let target = this.target; |
michael@0 | 243 | let view = target.ownerDocument ? target.ownerDocument.defaultView |
michael@0 | 244 | : target; |
michael@0 | 245 | view.addEventListener('click', this, true, true); |
michael@0 | 246 | } else { |
michael@0 | 247 | // We prevent end events to avoid sending a focus event. See bug 889717. |
michael@0 | 248 | evt.preventDefault(); |
michael@0 | 249 | } |
michael@0 | 250 | } else if (this.target && click && !this.panning) { |
michael@0 | 251 | this.notify(this._activationTimer); |
michael@0 | 252 | } |
michael@0 | 253 | |
michael@0 | 254 | this._finishPanning(); |
michael@0 | 255 | |
michael@0 | 256 | // Now that we're done, avoid entraining the thing we just panned. |
michael@0 | 257 | this.pointerDownTarget = null; |
michael@0 | 258 | }, |
michael@0 | 259 | |
michael@0 | 260 | onTouchMove: function cp_onTouchMove(evt) { |
michael@0 | 261 | if (!this.dragging) |
michael@0 | 262 | return; |
michael@0 | 263 | |
michael@0 | 264 | let screenX, screenY; |
michael@0 | 265 | if (this.watchedEventsType == 'touch') { |
michael@0 | 266 | let primaryTouch = this.findPrimaryPointer(evt.changedTouches); |
michael@0 | 267 | if (evt.touches.length > 1 || !primaryTouch) |
michael@0 | 268 | return; |
michael@0 | 269 | screenX = primaryTouch.screenX; |
michael@0 | 270 | screenY = primaryTouch.screenY; |
michael@0 | 271 | } else { |
michael@0 | 272 | screenX = evt.screenX; |
michael@0 | 273 | screenY = evt.screenY; |
michael@0 | 274 | } |
michael@0 | 275 | |
michael@0 | 276 | let current = this.position; |
michael@0 | 277 | let delta = new Point(screenX - current.x, screenY - current.y); |
michael@0 | 278 | current.set(screenX, screenY); |
michael@0 | 279 | |
michael@0 | 280 | KineticPanning.record(delta, evt.timeStamp); |
michael@0 | 281 | |
michael@0 | 282 | let isPan = KineticPanning.isPan(); |
michael@0 | 283 | |
michael@0 | 284 | // If we've detected a pan gesture, cancel the active state of the |
michael@0 | 285 | // current target. |
michael@0 | 286 | if (!this.panning && isPan) { |
michael@0 | 287 | this._resetActive(); |
michael@0 | 288 | } |
michael@0 | 289 | |
michael@0 | 290 | // There's no possibility of us panning anything. |
michael@0 | 291 | if (!this.scrollCallback) { |
michael@0 | 292 | return; |
michael@0 | 293 | } |
michael@0 | 294 | |
michael@0 | 295 | // Scroll manually. |
michael@0 | 296 | this.scrollCallback(delta.scale(-1)); |
michael@0 | 297 | |
michael@0 | 298 | if (!this.panning && isPan) { |
michael@0 | 299 | this.panning = true; |
michael@0 | 300 | this._activationTimer.cancel(); |
michael@0 | 301 | } |
michael@0 | 302 | |
michael@0 | 303 | if (this.panning) { |
michael@0 | 304 | // Only do this when we're actually executing a pan gesture. |
michael@0 | 305 | // Otherwise synthetic mouse events will be canceled. |
michael@0 | 306 | evt.stopPropagation(); |
michael@0 | 307 | evt.preventDefault(); |
michael@0 | 308 | } |
michael@0 | 309 | }, |
michael@0 | 310 | |
michael@0 | 311 | // nsITimerCallback |
michael@0 | 312 | notify: function cp_notify(timer) { |
michael@0 | 313 | this._setActive(this.pointerDownTarget); |
michael@0 | 314 | }, |
michael@0 | 315 | |
michael@0 | 316 | onKineticBegin: function cp_onKineticBegin(evt) { |
michael@0 | 317 | }, |
michael@0 | 318 | |
michael@0 | 319 | onKineticPan: function cp_onKineticPan(delta) { |
michael@0 | 320 | return !this.scrollCallback(delta); |
michael@0 | 321 | }, |
michael@0 | 322 | |
michael@0 | 323 | onKineticEnd: function cp_onKineticEnd() { |
michael@0 | 324 | if (!this.dragging) |
michael@0 | 325 | this.scrollCallback = null; |
michael@0 | 326 | }, |
michael@0 | 327 | |
michael@0 | 328 | getPannable: function cp_getPannable(node) { |
michael@0 | 329 | let pannableNode = this._findPannable(node); |
michael@0 | 330 | if (pannableNode) { |
michael@0 | 331 | return [pannableNode, this._generateCallback(pannableNode)]; |
michael@0 | 332 | } |
michael@0 | 333 | |
michael@0 | 334 | return [null, null]; |
michael@0 | 335 | }, |
michael@0 | 336 | |
michael@0 | 337 | _findPannable: function cp_findPannable(node) { |
michael@0 | 338 | if (!(node instanceof Ci.nsIDOMHTMLElement) || node.tagName == 'HTML') { |
michael@0 | 339 | return null; |
michael@0 | 340 | } |
michael@0 | 341 | |
michael@0 | 342 | let nodeContent = node.ownerDocument.defaultView; |
michael@0 | 343 | while (!(node instanceof Ci.nsIDOMHTMLBodyElement)) { |
michael@0 | 344 | let style = nodeContent.getComputedStyle(node, null); |
michael@0 | 345 | |
michael@0 | 346 | let overflow = [style.getPropertyValue('overflow'), |
michael@0 | 347 | style.getPropertyValue('overflow-x'), |
michael@0 | 348 | style.getPropertyValue('overflow-y')]; |
michael@0 | 349 | |
michael@0 | 350 | let rect = node.getBoundingClientRect(); |
michael@0 | 351 | let isAuto = (overflow.indexOf('auto') != -1 && |
michael@0 | 352 | (rect.height < node.scrollHeight || |
michael@0 | 353 | rect.width < node.scrollWidth)); |
michael@0 | 354 | |
michael@0 | 355 | let isScroll = (overflow.indexOf('scroll') != -1); |
michael@0 | 356 | |
michael@0 | 357 | let isScrollableTextarea = (node.tagName == 'TEXTAREA' && |
michael@0 | 358 | (node.scrollHeight > node.clientHeight || |
michael@0 | 359 | node.scrollWidth > node.clientWidth || |
michael@0 | 360 | ('scrollLeftMax' in node && node.scrollLeftMax > 0) || |
michael@0 | 361 | ('scrollTopMax' in node && node.scrollTopMax > 0))); |
michael@0 | 362 | if (isScroll || isAuto || isScrollableTextarea) { |
michael@0 | 363 | return node; |
michael@0 | 364 | } |
michael@0 | 365 | |
michael@0 | 366 | node = node.parentNode; |
michael@0 | 367 | } |
michael@0 | 368 | |
michael@0 | 369 | if (nodeContent.scrollMaxX || nodeContent.scrollMaxY) { |
michael@0 | 370 | return nodeContent; |
michael@0 | 371 | } |
michael@0 | 372 | |
michael@0 | 373 | if (nodeContent.frameElement) { |
michael@0 | 374 | return this._findPannable(nodeContent.frameElement); |
michael@0 | 375 | } |
michael@0 | 376 | |
michael@0 | 377 | return null; |
michael@0 | 378 | }, |
michael@0 | 379 | |
michael@0 | 380 | _generateCallback: function cp_generateCallback(root) { |
michael@0 | 381 | let firstScroll = true; |
michael@0 | 382 | let target; |
michael@0 | 383 | let current; |
michael@0 | 384 | let win, doc, htmlNode, bodyNode; |
michael@0 | 385 | |
michael@0 | 386 | function doScroll(node, delta) { |
michael@0 | 387 | if (node instanceof Ci.nsIDOMHTMLElement) { |
michael@0 | 388 | return node.scrollByNoFlush(delta.x, delta.y); |
michael@0 | 389 | } else if (node instanceof Ci.nsIDOMWindow) { |
michael@0 | 390 | win = node; |
michael@0 | 391 | doc = win.document; |
michael@0 | 392 | |
michael@0 | 393 | // "overflow:hidden" on either the <html> or the <body> node should |
michael@0 | 394 | // prevent the user from scrolling the root viewport. |
michael@0 | 395 | if (doc instanceof Ci.nsIDOMHTMLDocument) { |
michael@0 | 396 | htmlNode = doc.documentElement; |
michael@0 | 397 | bodyNode = doc.body; |
michael@0 | 398 | if (win.getComputedStyle(htmlNode, null).overflowX == "hidden" || |
michael@0 | 399 | win.getComputedStyle(bodyNode, null).overflowX == "hidden") { |
michael@0 | 400 | delta.x = 0; |
michael@0 | 401 | } |
michael@0 | 402 | if (win.getComputedStyle(htmlNode, null).overflowY == "hidden" || |
michael@0 | 403 | win.getComputedStyle(bodyNode, null).overflowY == "hidden") { |
michael@0 | 404 | delta.y = 0; |
michael@0 | 405 | } |
michael@0 | 406 | } |
michael@0 | 407 | let oldX = node.scrollX; |
michael@0 | 408 | let oldY = node.scrollY; |
michael@0 | 409 | node.scrollBy(delta.x, delta.y); |
michael@0 | 410 | return (node.scrollX != oldX || node.scrollY != oldY); |
michael@0 | 411 | } |
michael@0 | 412 | // If we get here, |node| isn't an HTML element and it's not a window, |
michael@0 | 413 | // but findPannable apparently thought it was scrollable... What is it? |
michael@0 | 414 | return false; |
michael@0 | 415 | } |
michael@0 | 416 | |
michael@0 | 417 | function targetParent(node) { |
michael@0 | 418 | return node.parentNode || node.frameElement || null; |
michael@0 | 419 | } |
michael@0 | 420 | |
michael@0 | 421 | function scroll(delta) { |
michael@0 | 422 | current = root; |
michael@0 | 423 | firstScroll = true; |
michael@0 | 424 | while (current) { |
michael@0 | 425 | if (doScroll(current, delta)) { |
michael@0 | 426 | firstScroll = false; |
michael@0 | 427 | return true; |
michael@0 | 428 | } |
michael@0 | 429 | |
michael@0 | 430 | // TODO The current code looks for possible scrolling regions only if |
michael@0 | 431 | // this is the first scroll action but this should be more dynamic. |
michael@0 | 432 | if (!firstScroll) { |
michael@0 | 433 | return false; |
michael@0 | 434 | } |
michael@0 | 435 | |
michael@0 | 436 | current = ContentPanning._findPannable(targetParent(current)); |
michael@0 | 437 | } |
michael@0 | 438 | |
michael@0 | 439 | // There is nothing scrollable here. |
michael@0 | 440 | return false; |
michael@0 | 441 | } |
michael@0 | 442 | return scroll; |
michael@0 | 443 | }, |
michael@0 | 444 | |
michael@0 | 445 | get _domUtils() { |
michael@0 | 446 | delete this._domUtils; |
michael@0 | 447 | return this._domUtils = Cc['@mozilla.org/inspector/dom-utils;1'] |
michael@0 | 448 | .getService(Ci.inIDOMUtils); |
michael@0 | 449 | }, |
michael@0 | 450 | |
michael@0 | 451 | get _activationTimer() { |
michael@0 | 452 | delete this._activationTimer; |
michael@0 | 453 | return this._activationTimer = Cc["@mozilla.org/timer;1"] |
michael@0 | 454 | .createInstance(Ci.nsITimer); |
michael@0 | 455 | }, |
michael@0 | 456 | |
michael@0 | 457 | get _activationDelayMs() { |
michael@0 | 458 | let delay = Services.prefs.getIntPref('ui.touch_activation.delay_ms'); |
michael@0 | 459 | delete this._activationDelayMs; |
michael@0 | 460 | return this._activationDelayMs = delay; |
michael@0 | 461 | }, |
michael@0 | 462 | |
michael@0 | 463 | _resetActive: function cp_resetActive() { |
michael@0 | 464 | let elt = this.pointerDownTarget || this.target; |
michael@0 | 465 | let root = elt.ownerDocument || elt.document; |
michael@0 | 466 | this._setActive(root.documentElement); |
michael@0 | 467 | }, |
michael@0 | 468 | |
michael@0 | 469 | _resetHover: function cp_resetHover() { |
michael@0 | 470 | const kStateHover = 0x00000004; |
michael@0 | 471 | try { |
michael@0 | 472 | let element = content.document.createElement('foo'); |
michael@0 | 473 | this._domUtils.setContentState(element, kStateHover); |
michael@0 | 474 | } catch(e) {} |
michael@0 | 475 | }, |
michael@0 | 476 | |
michael@0 | 477 | _setActive: function cp_setActive(elt) { |
michael@0 | 478 | const kStateActive = 0x00000001; |
michael@0 | 479 | this._domUtils.setContentState(elt, kStateActive); |
michael@0 | 480 | }, |
michael@0 | 481 | |
michael@0 | 482 | _recvViewportChange: function(data) { |
michael@0 | 483 | let metrics = data.json; |
michael@0 | 484 | this._viewport = new Rect(metrics.x, metrics.y, |
michael@0 | 485 | metrics.viewport.width, |
michael@0 | 486 | metrics.viewport.height); |
michael@0 | 487 | this._cssCompositedRect = new Rect(metrics.x, metrics.y, |
michael@0 | 488 | metrics.cssCompositedRect.width, |
michael@0 | 489 | metrics.cssCompositedRect.height); |
michael@0 | 490 | this._cssPageRect = new Rect(metrics.cssPageRect.x, |
michael@0 | 491 | metrics.cssPageRect.y, |
michael@0 | 492 | metrics.cssPageRect.width, |
michael@0 | 493 | metrics.cssPageRect.height); |
michael@0 | 494 | }, |
michael@0 | 495 | |
michael@0 | 496 | _recvDoubleTap: function(data) { |
michael@0 | 497 | let data = data.json; |
michael@0 | 498 | |
michael@0 | 499 | // We haven't received a metrics update yet; don't do anything. |
michael@0 | 500 | if (this._viewport == null) { |
michael@0 | 501 | return; |
michael@0 | 502 | } |
michael@0 | 503 | |
michael@0 | 504 | let win = content; |
michael@0 | 505 | |
michael@0 | 506 | let element = ElementTouchHelper.anyElementFromPoint(win, data.x, data.y); |
michael@0 | 507 | if (!element) { |
michael@0 | 508 | this._zoomOut(); |
michael@0 | 509 | return; |
michael@0 | 510 | } |
michael@0 | 511 | |
michael@0 | 512 | while (element && !this._shouldZoomToElement(element)) |
michael@0 | 513 | element = element.parentNode; |
michael@0 | 514 | |
michael@0 | 515 | if (!element) { |
michael@0 | 516 | this._zoomOut(); |
michael@0 | 517 | } else { |
michael@0 | 518 | const margin = 15; |
michael@0 | 519 | let rect = ElementTouchHelper.getBoundingContentRect(element); |
michael@0 | 520 | |
michael@0 | 521 | let cssPageRect = this._cssPageRect; |
michael@0 | 522 | let viewport = this._viewport; |
michael@0 | 523 | let bRect = new Rect(Math.max(cssPageRect.x, rect.x - margin), |
michael@0 | 524 | rect.y, |
michael@0 | 525 | rect.w + 2 * margin, |
michael@0 | 526 | rect.h); |
michael@0 | 527 | // constrict the rect to the screen's right edge |
michael@0 | 528 | bRect.width = Math.min(bRect.width, cssPageRect.right - bRect.x); |
michael@0 | 529 | |
michael@0 | 530 | // if the rect is already taking up most of the visible area and is stretching the |
michael@0 | 531 | // width of the page, then we want to zoom out instead. |
michael@0 | 532 | if (this._isRectZoomedIn(bRect, this._cssCompositedRect)) { |
michael@0 | 533 | this._zoomOut(); |
michael@0 | 534 | return; |
michael@0 | 535 | } |
michael@0 | 536 | |
michael@0 | 537 | rect.x = Math.round(bRect.x); |
michael@0 | 538 | rect.y = Math.round(bRect.y); |
michael@0 | 539 | rect.w = Math.round(bRect.width); |
michael@0 | 540 | rect.h = Math.round(bRect.height); |
michael@0 | 541 | |
michael@0 | 542 | // if the block we're zooming to is really tall, and the user double-tapped |
michael@0 | 543 | // more than a screenful of height from the top of it, then adjust the y-coordinate |
michael@0 | 544 | // so that we center the actual point the user double-tapped upon. this prevents |
michael@0 | 545 | // flying to the top of a page when double-tapping to zoom in (bug 761721). |
michael@0 | 546 | // the 1.2 multiplier is just a little fuzz to compensate for bRect including horizontal |
michael@0 | 547 | // margins but not vertical ones. |
michael@0 | 548 | let cssTapY = viewport.y + data.y; |
michael@0 | 549 | if ((bRect.height > rect.h) && (cssTapY > rect.y + (rect.h * 1.2))) { |
michael@0 | 550 | rect.y = cssTapY - (rect.h / 2); |
michael@0 | 551 | } |
michael@0 | 552 | |
michael@0 | 553 | Services.obs.notifyObservers(docShell, 'browser-zoom-to-rect', JSON.stringify(rect)); |
michael@0 | 554 | } |
michael@0 | 555 | }, |
michael@0 | 556 | |
michael@0 | 557 | _handleVisibilityChange: function(evt) { |
michael@0 | 558 | if (!evt.target.hidden) |
michael@0 | 559 | return; |
michael@0 | 560 | |
michael@0 | 561 | this._resetHover(); |
michael@0 | 562 | }, |
michael@0 | 563 | |
michael@0 | 564 | _shouldZoomToElement: function(aElement) { |
michael@0 | 565 | let win = aElement.ownerDocument.defaultView; |
michael@0 | 566 | if (win.getComputedStyle(aElement, null).display == "inline") |
michael@0 | 567 | return false; |
michael@0 | 568 | if (aElement instanceof Ci.nsIDOMHTMLLIElement) |
michael@0 | 569 | return false; |
michael@0 | 570 | if (aElement instanceof Ci.nsIDOMHTMLQuoteElement) |
michael@0 | 571 | return false; |
michael@0 | 572 | return true; |
michael@0 | 573 | }, |
michael@0 | 574 | |
michael@0 | 575 | _zoomOut: function() { |
michael@0 | 576 | let rect = new Rect(0, 0, 0, 0); |
michael@0 | 577 | Services.obs.notifyObservers(docShell, 'browser-zoom-to-rect', JSON.stringify(rect)); |
michael@0 | 578 | }, |
michael@0 | 579 | |
michael@0 | 580 | _isRectZoomedIn: function(aRect, aViewport) { |
michael@0 | 581 | // This function checks to see if the area of the rect visible in the |
michael@0 | 582 | // viewport (i.e. the "overlapArea" variable below) is approximately |
michael@0 | 583 | // the max area of the rect we can show. |
michael@0 | 584 | let vRect = new Rect(aViewport.x, aViewport.y, aViewport.width, aViewport.height); |
michael@0 | 585 | let overlap = vRect.intersect(aRect); |
michael@0 | 586 | let overlapArea = overlap.width * overlap.height; |
michael@0 | 587 | let availHeight = Math.min(aRect.width * vRect.height / vRect.width, aRect.height); |
michael@0 | 588 | let showing = overlapArea / (aRect.width * availHeight); |
michael@0 | 589 | let ratioW = (aRect.width / vRect.width); |
michael@0 | 590 | let ratioH = (aRect.height / vRect.height); |
michael@0 | 591 | |
michael@0 | 592 | return (showing > 0.9 && (ratioW > 0.9 || ratioH > 0.9)); |
michael@0 | 593 | }, |
michael@0 | 594 | |
michael@0 | 595 | _finishPanning: function() { |
michael@0 | 596 | this.dragging = false; |
michael@0 | 597 | delete this.primaryPointerId; |
michael@0 | 598 | this._activationTimer.cancel(); |
michael@0 | 599 | |
michael@0 | 600 | // If there is a scroll action, let's do a manual kinetic panning action. |
michael@0 | 601 | if (this.panning) { |
michael@0 | 602 | KineticPanning.start(this); |
michael@0 | 603 | } |
michael@0 | 604 | }, |
michael@0 | 605 | |
michael@0 | 606 | _unloadHandler: function() { |
michael@0 | 607 | kObservedEvents.forEach((topic) => { |
michael@0 | 608 | Services.obs.removeObserver(this, topic); |
michael@0 | 609 | }); |
michael@0 | 610 | } |
michael@0 | 611 | }; |
michael@0 | 612 | |
michael@0 | 613 | // Min/max velocity of kinetic panning. This is in pixels/millisecond. |
michael@0 | 614 | const kMinVelocity = 0.2; |
michael@0 | 615 | const kMaxVelocity = 6; |
michael@0 | 616 | |
michael@0 | 617 | // Constants that affect the "friction" of the scroll pane. |
michael@0 | 618 | const kExponentialC = 1000; |
michael@0 | 619 | const kPolynomialC = 100 / 1000000; |
michael@0 | 620 | |
michael@0 | 621 | // How often do we change the position of the scroll pane? |
michael@0 | 622 | // Too often and panning may jerk near the end. |
michael@0 | 623 | // Too little and panning will be choppy. In milliseconds. |
michael@0 | 624 | const kUpdateInterval = 16; |
michael@0 | 625 | |
michael@0 | 626 | // The numbers of momentums to use for calculating the velocity of the pan. |
michael@0 | 627 | // Those are taken from the end of the action |
michael@0 | 628 | const kSamples = 5; |
michael@0 | 629 | |
michael@0 | 630 | const KineticPanning = { |
michael@0 | 631 | _position: new Point(0, 0), |
michael@0 | 632 | _velocity: new Point(0, 0), |
michael@0 | 633 | _acceleration: new Point(0, 0), |
michael@0 | 634 | |
michael@0 | 635 | get active() { |
michael@0 | 636 | return this.target !== null; |
michael@0 | 637 | }, |
michael@0 | 638 | |
michael@0 | 639 | target: null, |
michael@0 | 640 | start: function kp_start(target) { |
michael@0 | 641 | this.target = target; |
michael@0 | 642 | |
michael@0 | 643 | // Calculate the initial velocity of the movement based on user input |
michael@0 | 644 | let momentums = this.momentums; |
michael@0 | 645 | let flick = momentums[momentums.length - 1].time - momentums[0].time < 300; |
michael@0 | 646 | |
michael@0 | 647 | let distance = new Point(0, 0); |
michael@0 | 648 | momentums.forEach(function(momentum) { |
michael@0 | 649 | distance.add(momentum.dx, momentum.dy); |
michael@0 | 650 | }); |
michael@0 | 651 | |
michael@0 | 652 | function clampFromZero(x, min, max) { |
michael@0 | 653 | if (x >= 0) |
michael@0 | 654 | return Math.max(min, Math.min(max, x)); |
michael@0 | 655 | return Math.min(-min, Math.max(-max, x)); |
michael@0 | 656 | } |
michael@0 | 657 | |
michael@0 | 658 | let elapsed = momentums[momentums.length - 1].time - momentums[0].time; |
michael@0 | 659 | let velocityX = clampFromZero(distance.x / elapsed, 0, kMaxVelocity); |
michael@0 | 660 | let velocityY = clampFromZero(distance.y / elapsed, 0, kMaxVelocity); |
michael@0 | 661 | |
michael@0 | 662 | let velocity = this._velocity; |
michael@0 | 663 | if (flick) { |
michael@0 | 664 | // Very fast pan action that does not generate a click are very often pan |
michael@0 | 665 | // action. If this is a small gesture then it will not move the view a lot |
michael@0 | 666 | // and so it will be above the minimun threshold and not generate any |
michael@0 | 667 | // kinetic panning. This does not look on a device since this is often |
michael@0 | 668 | // a real gesture, so let's lower the velocity threshold for such moves. |
michael@0 | 669 | velocity.set(velocityX, velocityY); |
michael@0 | 670 | } else { |
michael@0 | 671 | velocity.set(Math.abs(velocityX) < kMinVelocity ? 0 : velocityX, |
michael@0 | 672 | Math.abs(velocityY) < kMinVelocity ? 0 : velocityY); |
michael@0 | 673 | } |
michael@0 | 674 | this.momentums = []; |
michael@0 | 675 | |
michael@0 | 676 | // Set acceleration vector to opposite signs of velocity |
michael@0 | 677 | function sign(x) { |
michael@0 | 678 | return x ? (x > 0 ? 1 : -1) : 0; |
michael@0 | 679 | } |
michael@0 | 680 | |
michael@0 | 681 | this._acceleration.set(velocity.clone().map(sign).scale(-kPolynomialC)); |
michael@0 | 682 | |
michael@0 | 683 | // Reset the position |
michael@0 | 684 | this._position.set(0, 0); |
michael@0 | 685 | |
michael@0 | 686 | this._startAnimation(); |
michael@0 | 687 | |
michael@0 | 688 | this.target.onKineticBegin(); |
michael@0 | 689 | }, |
michael@0 | 690 | |
michael@0 | 691 | stop: function kp_stop() { |
michael@0 | 692 | this.reset(); |
michael@0 | 693 | |
michael@0 | 694 | if (!this.target) |
michael@0 | 695 | return; |
michael@0 | 696 | |
michael@0 | 697 | this.target.onKineticEnd(); |
michael@0 | 698 | this.target = null; |
michael@0 | 699 | }, |
michael@0 | 700 | |
michael@0 | 701 | reset: function kp_reset() { |
michael@0 | 702 | this.momentums = []; |
michael@0 | 703 | this.distance.set(0, 0); |
michael@0 | 704 | }, |
michael@0 | 705 | |
michael@0 | 706 | momentums: [], |
michael@0 | 707 | record: function kp_record(delta, timestamp) { |
michael@0 | 708 | this.momentums.push({ 'time': this._getTime(timestamp), |
michael@0 | 709 | 'dx' : delta.x, 'dy' : delta.y }); |
michael@0 | 710 | |
michael@0 | 711 | // We only need to keep kSamples in this.momentums. |
michael@0 | 712 | if (this.momentums.length > kSamples) { |
michael@0 | 713 | this.momentums.shift(); |
michael@0 | 714 | } |
michael@0 | 715 | |
michael@0 | 716 | this.distance.add(delta.x, delta.y); |
michael@0 | 717 | }, |
michael@0 | 718 | |
michael@0 | 719 | _getTime: function kp_getTime(time) { |
michael@0 | 720 | // Touch events generated by the platform or hand-made are defined in |
michael@0 | 721 | // microseconds instead of milliseconds. Bug 77992 will fix this at the |
michael@0 | 722 | // platform level. |
michael@0 | 723 | if (time > Date.now()) { |
michael@0 | 724 | return Math.floor(time / 1000); |
michael@0 | 725 | } else { |
michael@0 | 726 | return time; |
michael@0 | 727 | } |
michael@0 | 728 | }, |
michael@0 | 729 | |
michael@0 | 730 | get threshold() { |
michael@0 | 731 | let dpi = content.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 732 | .getInterface(Ci.nsIDOMWindowUtils) |
michael@0 | 733 | .displayDPI; |
michael@0 | 734 | |
michael@0 | 735 | let threshold = Services.prefs.getIntPref('ui.dragThresholdX') / 240 * dpi; |
michael@0 | 736 | |
michael@0 | 737 | delete this.threshold; |
michael@0 | 738 | return this.threshold = threshold; |
michael@0 | 739 | }, |
michael@0 | 740 | |
michael@0 | 741 | distance: new Point(0, 0), |
michael@0 | 742 | isPan: function cp_isPan() { |
michael@0 | 743 | return (Math.abs(this.distance.x) > this.threshold || |
michael@0 | 744 | Math.abs(this.distance.y) > this.threshold); |
michael@0 | 745 | }, |
michael@0 | 746 | |
michael@0 | 747 | _startAnimation: function kp_startAnimation() { |
michael@0 | 748 | let c = kExponentialC; |
michael@0 | 749 | function getNextPosition(position, v, a, t) { |
michael@0 | 750 | // Important traits for this function: |
michael@0 | 751 | // p(t=0) is 0 |
michael@0 | 752 | // p'(t=0) is v0 |
michael@0 | 753 | // |
michael@0 | 754 | // We use exponential to get a smoother stop, but by itself exponential |
michael@0 | 755 | // is too smooth at the end. Adding a polynomial with the appropriate |
michael@0 | 756 | // weight helps to balance |
michael@0 | 757 | position.set(v.x * Math.exp(-t / c) * -c + a.x * t * t + v.x * c, |
michael@0 | 758 | v.y * Math.exp(-t / c) * -c + a.y * t * t + v.y * c); |
michael@0 | 759 | } |
michael@0 | 760 | |
michael@0 | 761 | let startTime = content.mozAnimationStartTime; |
michael@0 | 762 | let elapsedTime = 0, targetedTime = 0, averageTime = 0; |
michael@0 | 763 | |
michael@0 | 764 | let velocity = this._velocity; |
michael@0 | 765 | let acceleration = this._acceleration; |
michael@0 | 766 | |
michael@0 | 767 | let position = this._position; |
michael@0 | 768 | let nextPosition = new Point(0, 0); |
michael@0 | 769 | let delta = new Point(0, 0); |
michael@0 | 770 | |
michael@0 | 771 | let callback = (function(timestamp) { |
michael@0 | 772 | if (!this.target) |
michael@0 | 773 | return; |
michael@0 | 774 | |
michael@0 | 775 | // To make animation end fast enough but to keep smoothness, average the |
michael@0 | 776 | // ideal time frame (smooth animation) with the actual time lapse |
michael@0 | 777 | // (end fast enough). |
michael@0 | 778 | // Animation will never take longer than 2 times the ideal length of time. |
michael@0 | 779 | elapsedTime = timestamp - startTime; |
michael@0 | 780 | targetedTime += kUpdateInterval; |
michael@0 | 781 | averageTime = (targetedTime + elapsedTime) / 2; |
michael@0 | 782 | |
michael@0 | 783 | // Calculate new position. |
michael@0 | 784 | getNextPosition(nextPosition, velocity, acceleration, averageTime); |
michael@0 | 785 | delta.set(Math.round(nextPosition.x - position.x), |
michael@0 | 786 | Math.round(nextPosition.y - position.y)); |
michael@0 | 787 | |
michael@0 | 788 | // Test to see if movement is finished for each component. |
michael@0 | 789 | if (delta.x * acceleration.x > 0) |
michael@0 | 790 | delta.x = position.x = velocity.x = acceleration.x = 0; |
michael@0 | 791 | |
michael@0 | 792 | if (delta.y * acceleration.y > 0) |
michael@0 | 793 | delta.y = position.y = velocity.y = acceleration.y = 0; |
michael@0 | 794 | |
michael@0 | 795 | if (velocity.equals(0, 0) || delta.equals(0, 0)) { |
michael@0 | 796 | this.stop(); |
michael@0 | 797 | return; |
michael@0 | 798 | } |
michael@0 | 799 | |
michael@0 | 800 | position.add(delta); |
michael@0 | 801 | if (this.target.onKineticPan(delta.scale(-1))) { |
michael@0 | 802 | this.stop(); |
michael@0 | 803 | return; |
michael@0 | 804 | } |
michael@0 | 805 | |
michael@0 | 806 | content.mozRequestAnimationFrame(callback); |
michael@0 | 807 | }).bind(this); |
michael@0 | 808 | |
michael@0 | 809 | content.mozRequestAnimationFrame(callback); |
michael@0 | 810 | } |
michael@0 | 811 | }; |
michael@0 | 812 | |
michael@0 | 813 | const ElementTouchHelper = { |
michael@0 | 814 | anyElementFromPoint: function(aWindow, aX, aY) { |
michael@0 | 815 | let cwu = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); |
michael@0 | 816 | let elem = cwu.elementFromPoint(aX, aY, true, true); |
michael@0 | 817 | |
michael@0 | 818 | let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement; |
michael@0 | 819 | let HTMLFrameElement = Ci.nsIDOMHTMLFrameElement; |
michael@0 | 820 | while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) { |
michael@0 | 821 | let rect = elem.getBoundingClientRect(); |
michael@0 | 822 | aX -= rect.left; |
michael@0 | 823 | aY -= rect.top; |
michael@0 | 824 | cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); |
michael@0 | 825 | elem = cwu.elementFromPoint(aX, aY, true, true); |
michael@0 | 826 | } |
michael@0 | 827 | |
michael@0 | 828 | return elem; |
michael@0 | 829 | }, |
michael@0 | 830 | |
michael@0 | 831 | getBoundingContentRect: function(aElement) { |
michael@0 | 832 | if (!aElement) |
michael@0 | 833 | return {x: 0, y: 0, w: 0, h: 0}; |
michael@0 | 834 | |
michael@0 | 835 | let document = aElement.ownerDocument; |
michael@0 | 836 | while (document.defaultView.frameElement) |
michael@0 | 837 | document = document.defaultView.frameElement.ownerDocument; |
michael@0 | 838 | |
michael@0 | 839 | let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); |
michael@0 | 840 | let scrollX = {}, scrollY = {}; |
michael@0 | 841 | cwu.getScrollXY(false, scrollX, scrollY); |
michael@0 | 842 | |
michael@0 | 843 | let r = aElement.getBoundingClientRect(); |
michael@0 | 844 | |
michael@0 | 845 | // step out of iframes and frames, offsetting scroll values |
michael@0 | 846 | for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && frame != content; frame = frame.parent) { |
michael@0 | 847 | // adjust client coordinates' origin to be top left of iframe viewport |
michael@0 | 848 | let rect = frame.frameElement.getBoundingClientRect(); |
michael@0 | 849 | let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth; |
michael@0 | 850 | let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth; |
michael@0 | 851 | scrollX.value += rect.left + parseInt(left); |
michael@0 | 852 | scrollY.value += rect.top + parseInt(top); |
michael@0 | 853 | } |
michael@0 | 854 | |
michael@0 | 855 | return {x: r.left + scrollX.value, |
michael@0 | 856 | y: r.top + scrollY.value, |
michael@0 | 857 | w: r.width, |
michael@0 | 858 | h: r.height }; |
michael@0 | 859 | } |
michael@0 | 860 | }; |