dom/browser-element/BrowserElementPanning.js

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

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 };

mercurial