dom/browser-element/BrowserElementPanning.js

changeset 0
6474c204b198
     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 +};

mercurial