browser/metro/base/content/input.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: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; js2-strict-trailing-comma-warning: nil -*-
michael@0 2 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 Components.utils.import("resource://gre/modules/Geometry.jsm");
michael@0 7
michael@0 8 /*
michael@0 9 * Drag scrolling related constants
michael@0 10 */
michael@0 11
michael@0 12 // maximum drag distance in inches while axis locking can still be reverted
michael@0 13 const kAxisLockRevertThreshold = 0.8;
michael@0 14
michael@0 15 // Same as NS_EVENT_STATE_ACTIVE from mozilla/EventStates.h
michael@0 16 const kStateActive = 0x00000001;
michael@0 17
michael@0 18 // After a drag begins, kinetic panning is stopped if the drag doesn't become
michael@0 19 // a pan in 300 milliseconds.
michael@0 20 const kStopKineticPanOnDragTimeout = 300;
michael@0 21
michael@0 22 // Min/max velocity of kinetic panning. This is in pixels/millisecond.
michael@0 23 const kMinVelocity = 0.4;
michael@0 24 const kMaxVelocity = 6;
michael@0 25
michael@0 26 /*
michael@0 27 * prefs
michael@0 28 */
michael@0 29
michael@0 30 // Display rects around selection ranges. Useful in debugging
michael@0 31 // selection problems.
michael@0 32 const kDebugSelectionDisplayPref = "metro.debug.selection.displayRanges";
michael@0 33 // Dump range rect data to the console. Very useful, but also slows
michael@0 34 // things down a lot.
michael@0 35 const kDebugSelectionDumpPref = "metro.debug.selection.dumpRanges";
michael@0 36 // Dump message manager event traffic for selection.
michael@0 37 const kDebugSelectionDumpEvents = "metro.debug.selection.dumpEvents";
michael@0 38 const kAsyncPanZoomEnabled = "layers.async-pan-zoom.enabled"
michael@0 39
michael@0 40 /**
michael@0 41 * TouchModule
michael@0 42 *
michael@0 43 * Handles all touch-related input such as dragging and tapping.
michael@0 44 *
michael@0 45 * The Fennec chrome DOM tree has elements that are augmented dynamically with
michael@0 46 * custom JS properties that tell the TouchModule they have custom support for
michael@0 47 * either dragging or clicking. These JS properties are JS objects that expose
michael@0 48 * an interface supporting dragging or clicking (though currently we only look
michael@0 49 * to drag scrollable elements).
michael@0 50 *
michael@0 51 * A custom dragger is a JS property that lives on a scrollable DOM element,
michael@0 52 * accessible as myElement.customDragger. The customDragger must support the
michael@0 53 * following interface: (The `scroller' argument is given for convenience, and
michael@0 54 * is the object reference to the element's scrollbox object).
michael@0 55 *
michael@0 56 * dragStart(cX, cY, target, scroller)
michael@0 57 * Signals the beginning of a drag. Coordinates are passed as
michael@0 58 * client coordinates. target is copied from the event.
michael@0 59 *
michael@0 60 * dragStop(dx, dy, scroller)
michael@0 61 * Signals the end of a drag. The dx, dy parameters may be non-zero to
michael@0 62 * indicate one last drag movement.
michael@0 63 *
michael@0 64 * dragMove(dx, dy, scroller, isKinetic)
michael@0 65 * Signals an input attempt to drag by dx, dy.
michael@0 66 *
michael@0 67 * There is a default dragger in case a scrollable element is dragged --- see
michael@0 68 * the defaultDragger prototype property.
michael@0 69 */
michael@0 70
michael@0 71 var TouchModule = {
michael@0 72 _debugEvents: false,
michael@0 73 _isCancelled: false,
michael@0 74 _isCancellable: false,
michael@0 75
michael@0 76 init: function init() {
michael@0 77 this._dragData = new DragData();
michael@0 78
michael@0 79 this._dragger = null;
michael@0 80
michael@0 81 this._targetScrollbox = null;
michael@0 82 this._targetScrollInterface = null;
michael@0 83
michael@0 84 this._kinetic = new KineticController(this._dragBy.bind(this),
michael@0 85 this._kineticStop.bind(this));
michael@0 86
michael@0 87 // capture phase events
michael@0 88 window.addEventListener("CancelTouchSequence", this, true);
michael@0 89 window.addEventListener("keydown", this, true);
michael@0 90 window.addEventListener("MozMouseHittest", this, true);
michael@0 91
michael@0 92 // bubble phase
michael@0 93 window.addEventListener("contextmenu", this, false);
michael@0 94 window.addEventListener("touchstart", this, false);
michael@0 95 window.addEventListener("touchmove", this, false);
michael@0 96 window.addEventListener("touchend", this, false);
michael@0 97
michael@0 98 Services.obs.addObserver(this, "Gesture:SingleTap", false);
michael@0 99 Services.obs.addObserver(this, "Gesture:DoubleTap", false);
michael@0 100 },
michael@0 101
michael@0 102 /*
michael@0 103 * Events
michael@0 104 */
michael@0 105
michael@0 106 handleEvent: function handleEvent(aEvent) {
michael@0 107 switch (aEvent.type) {
michael@0 108 case "contextmenu":
michael@0 109 this._onContextMenu(aEvent);
michael@0 110 break;
michael@0 111
michael@0 112 case "CancelTouchSequence":
michael@0 113 this.cancelPending();
michael@0 114 break;
michael@0 115
michael@0 116 default: {
michael@0 117 if (this._debugEvents) {
michael@0 118 if (aEvent.type != "touchmove")
michael@0 119 Util.dumpLn("TouchModule:", aEvent.type, aEvent.target);
michael@0 120 }
michael@0 121
michael@0 122 switch (aEvent.type) {
michael@0 123 case "touchstart":
michael@0 124 this._onTouchStart(aEvent);
michael@0 125 break;
michael@0 126 case "touchmove":
michael@0 127 this._onTouchMove(aEvent);
michael@0 128 break;
michael@0 129 case "touchend":
michael@0 130 this._onTouchEnd(aEvent);
michael@0 131 break;
michael@0 132 case "keydown":
michael@0 133 this._handleKeyDown(aEvent);
michael@0 134 break;
michael@0 135 case "MozMouseHittest":
michael@0 136 // Used by widget to hit test chrome vs content. Make sure the XUl scrollbars
michael@0 137 // are counted as "chrome". Since the XUL scrollbars have sub-elements we walk
michael@0 138 // the parent chain to ensure we catch all of those as well.
michael@0 139 let onScrollbar = false;
michael@0 140 for (let node = aEvent.originalTarget; node instanceof XULElement; node = node.parentNode) {
michael@0 141 if (node.tagName == 'scrollbar') {
michael@0 142 onScrollbar = true;
michael@0 143 break;
michael@0 144 }
michael@0 145 }
michael@0 146 if (onScrollbar || aEvent.target.ownerDocument == document) {
michael@0 147 aEvent.preventDefault();
michael@0 148 }
michael@0 149 aEvent.stopPropagation();
michael@0 150 break;
michael@0 151 }
michael@0 152 }
michael@0 153 }
michael@0 154 },
michael@0 155
michael@0 156 _handleKeyDown: function _handleKeyDown(aEvent) {
michael@0 157 const TABKEY = 9;
michael@0 158 if (aEvent.keyCode == TABKEY && !InputSourceHelper.isPrecise) {
michael@0 159 if (Util.isEditable(aEvent.target) &&
michael@0 160 aEvent.target.selectionStart != aEvent.target.selectionEnd) {
michael@0 161 SelectionHelperUI.closeEditSession(false);
michael@0 162 }
michael@0 163 setTimeout(function() {
michael@0 164 let element = Browser.selectedBrowser.contentDocument.activeElement;
michael@0 165 // We only want to attach monocles if we have an input, text area,
michael@0 166 // there is selection, and the target element changed.
michael@0 167 // Sometimes the target element won't change even though selection is
michael@0 168 // cleared because of focus outside the browser.
michael@0 169 if (Util.isEditable(element) &&
michael@0 170 !SelectionHelperUI.isActive &&
michael@0 171 element.selectionStart != element.selectionEnd &&
michael@0 172 // not e10s friendly
michael@0 173 aEvent.target != element) {
michael@0 174 let rect = element.getBoundingClientRect();
michael@0 175 SelectionHelperUI.attachEditSession(Browser.selectedBrowser,
michael@0 176 rect.left + rect.width / 2,
michael@0 177 rect.top + rect.height / 2);
michael@0 178 }
michael@0 179 }, 50);
michael@0 180 }
michael@0 181 },
michael@0 182
michael@0 183 observe: function BrowserUI_observe(aSubject, aTopic, aData) {
michael@0 184 switch (aTopic) {
michael@0 185 case "Gesture:SingleTap":
michael@0 186 case "Gesture:DoubleTap":
michael@0 187 Browser.selectedBrowser.messageManager.sendAsyncMessage(aTopic, JSON.parse(aData));
michael@0 188 break;
michael@0 189 }
michael@0 190 },
michael@0 191
michael@0 192
michael@0 193 sample: function sample(aTimeStamp) {
michael@0 194 this._waitingForPaint = false;
michael@0 195 },
michael@0 196
michael@0 197 /**
michael@0 198 * This gets invoked by the input handler if another module grabs. We should
michael@0 199 * reset our state or something here. This is probably doing the wrong thing
michael@0 200 * in its current form.
michael@0 201 */
michael@0 202 cancelPending: function cancelPending() {
michael@0 203 this._doDragStop();
michael@0 204
michael@0 205 // Kinetic panning may have already been active or drag stop above may have
michael@0 206 // made kinetic panning active.
michael@0 207 this._kinetic.end();
michael@0 208
michael@0 209 this._targetScrollbox = null;
michael@0 210 this._targetScrollInterface = null;
michael@0 211 },
michael@0 212
michael@0 213 _onContextMenu: function _onContextMenu(aEvent) {
michael@0 214 // bug 598965 - chrome UI should stop to be pannable once the
michael@0 215 // context menu has appeared.
michael@0 216 if (ContextMenuUI.popupState) {
michael@0 217 this.cancelPending();
michael@0 218 }
michael@0 219 },
michael@0 220
michael@0 221 /** Begin possible pan and send tap down event. */
michael@0 222 _onTouchStart: function _onTouchStart(aEvent) {
michael@0 223 if (aEvent.touches.length > 1)
michael@0 224 return;
michael@0 225
michael@0 226 this._isCancelled = false;
michael@0 227 this._isCancellable = true;
michael@0 228
michael@0 229 if (aEvent.defaultPrevented) {
michael@0 230 this._isCancelled = true;
michael@0 231 return;
michael@0 232 }
michael@0 233
michael@0 234 let dragData = this._dragData;
michael@0 235 if (dragData.dragging) {
michael@0 236 // Somehow a mouse up was missed.
michael@0 237 this._doDragStop();
michael@0 238 }
michael@0 239 dragData.reset();
michael@0 240 this.dX = 0;
michael@0 241 this.dY = 0;
michael@0 242
michael@0 243 // walk up the DOM tree in search of nearest scrollable ancestor. nulls are
michael@0 244 // returned if none found.
michael@0 245 let [targetScrollbox, targetScrollInterface, dragger]
michael@0 246 = ScrollUtils.getScrollboxFromElement(aEvent.originalTarget);
michael@0 247
michael@0 248 // stop kinetic panning if targetScrollbox has changed
michael@0 249 if (this._kinetic.isActive() && this._dragger != dragger)
michael@0 250 this._kinetic.end();
michael@0 251
michael@0 252 this._targetScrollbox = targetScrollInterface ? targetScrollInterface.element : targetScrollbox;
michael@0 253 this._targetScrollInterface = targetScrollInterface;
michael@0 254
michael@0 255 if (!this._targetScrollbox) {
michael@0 256 return;
michael@0 257 }
michael@0 258
michael@0 259 // Don't allow kinetic panning if APZC is enabled and the pan element is the deck
michael@0 260 let deck = document.getElementById("browsers");
michael@0 261 if (Services.prefs.getBoolPref(kAsyncPanZoomEnabled) &&
michael@0 262 this._targetScrollbox == deck) {
michael@0 263 return;
michael@0 264 }
michael@0 265
michael@0 266 // XXX shouldn't dragger always be valid here?
michael@0 267 if (dragger) {
michael@0 268 let draggable = dragger.isDraggable(targetScrollbox, targetScrollInterface);
michael@0 269 dragData.locked = !draggable.x || !draggable.y;
michael@0 270 if (draggable.x || draggable.y) {
michael@0 271 this._dragger = dragger;
michael@0 272 if (dragger.freeDrag)
michael@0 273 dragData.alwaysFreeDrag = dragger.freeDrag();
michael@0 274 this._doDragStart(aEvent, draggable);
michael@0 275 }
michael@0 276 }
michael@0 277 },
michael@0 278
michael@0 279 /** Send tap up event and any necessary full taps. */
michael@0 280 _onTouchEnd: function _onTouchEnd(aEvent) {
michael@0 281 if (aEvent.touches.length > 0 || this._isCancelled || !this._targetScrollbox)
michael@0 282 return;
michael@0 283
michael@0 284 // onMouseMove will not record the delta change if we are waiting for a
michael@0 285 // paint. Since this is the last input for this drag, we override the flag.
michael@0 286 this._waitingForPaint = false;
michael@0 287 this._onTouchMove(aEvent);
michael@0 288
michael@0 289 let dragData = this._dragData;
michael@0 290 this._doDragStop();
michael@0 291 },
michael@0 292
michael@0 293 /**
michael@0 294 * If we're in a drag, do what we have to do to drag on.
michael@0 295 */
michael@0 296 _onTouchMove: function _onTouchMove(aEvent) {
michael@0 297 if (aEvent.touches.length > 1)
michael@0 298 return;
michael@0 299
michael@0 300 if (this._isCancellable) {
michael@0 301 // only the first touchmove is cancellable.
michael@0 302 this._isCancellable = false;
michael@0 303 if (aEvent.defaultPrevented) {
michael@0 304 this._isCancelled = true;
michael@0 305 }
michael@0 306 }
michael@0 307
michael@0 308 if (this._isCancelled)
michael@0 309 return;
michael@0 310
michael@0 311 let touch = aEvent.changedTouches[0];
michael@0 312 if (!this._targetScrollbox) {
michael@0 313 return;
michael@0 314 }
michael@0 315
michael@0 316 let dragData = this._dragData;
michael@0 317
michael@0 318 if (dragData.dragging) {
michael@0 319 let oldIsPan = dragData.isPan();
michael@0 320 dragData.setDragPosition(touch.screenX, touch.screenY);
michael@0 321 dragData.setMousePosition(touch);
michael@0 322
michael@0 323 // Kinetic panning is sensitive to time. It is more stable if it receives
michael@0 324 // the mousemove events as they come. For dragging though, we only want
michael@0 325 // to call _dragBy if we aren't waiting for a paint (so we don't spam the
michael@0 326 // main browser loop with a bunch of redundant paints).
michael@0 327 //
michael@0 328 // Here, we feed kinetic panning drag differences for mouse events as
michael@0 329 // come; for dragging, we build up a drag buffer in this.dX/this.dY and
michael@0 330 // release it when we are ready to paint.
michael@0 331 //
michael@0 332 let [sX, sY] = dragData.panPosition();
michael@0 333 this.dX += dragData.prevPanX - sX;
michael@0 334 this.dY += dragData.prevPanY - sY;
michael@0 335
michael@0 336 if (dragData.isPan()) {
michael@0 337 // Only pan when mouse event isn't part of a click. Prevent jittering on tap.
michael@0 338 this._kinetic.addData(sX - dragData.prevPanX, sY - dragData.prevPanY);
michael@0 339
michael@0 340 // dragBy will reset dX and dY values to 0
michael@0 341 this._dragBy(this.dX, this.dY);
michael@0 342
michael@0 343 // Let everyone know when mousemove begins a pan
michael@0 344 if (!oldIsPan && dragData.isPan()) {
michael@0 345 let event = document.createEvent("Events");
michael@0 346 event.initEvent("PanBegin", true, false);
michael@0 347 this._targetScrollbox.dispatchEvent(event);
michael@0 348
michael@0 349 Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:PanBegin", {});
michael@0 350 }
michael@0 351 }
michael@0 352 }
michael@0 353 },
michael@0 354
michael@0 355 /**
michael@0 356 * Inform our dragger of a dragStart.
michael@0 357 */
michael@0 358 _doDragStart: function _doDragStart(aEvent, aDraggable) {
michael@0 359 let touch = aEvent.changedTouches[0];
michael@0 360 let dragData = this._dragData;
michael@0 361 dragData.setDragStart(touch.screenX, touch.screenY, aDraggable);
michael@0 362 this._kinetic.addData(0, 0);
michael@0 363 this._dragStartTime = Date.now();
michael@0 364 if (!this._kinetic.isActive()) {
michael@0 365 this._dragger.dragStart(touch.clientX, touch.clientY, touch.target, this._targetScrollInterface);
michael@0 366 }
michael@0 367 },
michael@0 368
michael@0 369 /** Finish a drag. */
michael@0 370 _doDragStop: function _doDragStop() {
michael@0 371 let dragData = this._dragData;
michael@0 372 if (!dragData.dragging)
michael@0 373 return;
michael@0 374
michael@0 375 dragData.endDrag();
michael@0 376
michael@0 377 // Note: it is possible for kinetic scrolling to be active from a
michael@0 378 // mousedown/mouseup event previous to this one. In this case, we
michael@0 379 // want the kinetic panner to tell our drag interface to stop.
michael@0 380
michael@0 381 if (dragData.isPan()) {
michael@0 382 if (Date.now() - this._dragStartTime > kStopKineticPanOnDragTimeout)
michael@0 383 this._kinetic._velocity.set(0, 0);
michael@0 384
michael@0 385 // Start kinetic pan if we aren't using async pan zoom or the scroll
michael@0 386 // element is not browsers.
michael@0 387 let deck = document.getElementById("browsers");
michael@0 388 if (!Services.prefs.getBoolPref(kAsyncPanZoomEnabled) ||
michael@0 389 this._targetScrollbox != deck) {
michael@0 390 this._kinetic.start();
michael@0 391 }
michael@0 392 } else {
michael@0 393 this._kinetic.end();
michael@0 394 if (this._dragger)
michael@0 395 this._dragger.dragStop(0, 0, this._targetScrollInterface);
michael@0 396 this._dragger = null;
michael@0 397 }
michael@0 398 },
michael@0 399
michael@0 400 /**
michael@0 401 * Used by _onTouchMove() above and by KineticController's timer to do the
michael@0 402 * actual dragMove signalling to the dragger. We'd put this in _onTouchMove()
michael@0 403 * but then KineticController would be adding to its own data as it signals
michael@0 404 * the dragger of dragMove()s.
michael@0 405 */
michael@0 406 _dragBy: function _dragBy(dX, dY, aIsKinetic) {
michael@0 407 let dragged = true;
michael@0 408 let dragData = this._dragData;
michael@0 409 if (!this._waitingForPaint || aIsKinetic) {
michael@0 410 let dragData = this._dragData;
michael@0 411 dragged = this._dragger.dragMove(dX, dY, this._targetScrollInterface, aIsKinetic,
michael@0 412 dragData._mouseX, dragData._mouseY);
michael@0 413 if (dragged && !this._waitingForPaint) {
michael@0 414 this._waitingForPaint = true;
michael@0 415 mozRequestAnimationFrame(this);
michael@0 416 }
michael@0 417 this.dX = 0;
michael@0 418 this.dY = 0;
michael@0 419 }
michael@0 420 if (!dragData.isPan())
michael@0 421 this._kinetic.pause();
michael@0 422
michael@0 423 return dragged;
michael@0 424 },
michael@0 425
michael@0 426 /** Callback for kinetic scroller. */
michael@0 427 _kineticStop: function _kineticStop() {
michael@0 428 // Kinetic panning could finish while user is panning, so don't finish
michael@0 429 // the pan just yet.
michael@0 430 let dragData = this._dragData;
michael@0 431 if (!dragData.dragging) {
michael@0 432 if (this._dragger)
michael@0 433 this._dragger.dragStop(0, 0, this._targetScrollInterface);
michael@0 434 this._dragger = null;
michael@0 435
michael@0 436 let event = document.createEvent("Events");
michael@0 437 event.initEvent("PanFinished", true, false);
michael@0 438 this._targetScrollbox.dispatchEvent(event);
michael@0 439 }
michael@0 440 },
michael@0 441
michael@0 442 toString: function toString() {
michael@0 443 return '[TouchModule] {'
michael@0 444 + '\n\tdragData=' + this._dragData + ', '
michael@0 445 + 'dragger=' + this._dragger + ', '
michael@0 446 + '\n\ttargetScroller=' + this._targetScrollInterface + '}';
michael@0 447 },
michael@0 448 };
michael@0 449
michael@0 450 var ScrollUtils = {
michael@0 451 // threshold in pixels for sensing a tap as opposed to a pan
michael@0 452 get tapRadius() {
michael@0 453 let dpi = Util.displayDPI;
michael@0 454 delete this.tapRadius;
michael@0 455 return this.tapRadius = Services.prefs.getIntPref("ui.dragThresholdX") / 240 * dpi;
michael@0 456 },
michael@0 457
michael@0 458 /**
michael@0 459 * Walk up (parentward) the DOM tree from elem in search of a scrollable element.
michael@0 460 * Return the element and its scroll interface if one is found, two nulls otherwise.
michael@0 461 *
michael@0 462 * This function will cache the pointer to the scroll interface on the element itself,
michael@0 463 * so it is safe to call it many times without incurring the same XPConnect overhead
michael@0 464 * as in the initial call.
michael@0 465 */
michael@0 466 getScrollboxFromElement: function getScrollboxFromElement(elem) {
michael@0 467 let scrollbox = null;
michael@0 468 let qinterface = null;
michael@0 469
michael@0 470 // if element is content or the startui page, get the browser scroll interface
michael@0 471 if (elem.ownerDocument == Browser.selectedBrowser.contentDocument) {
michael@0 472 elem = Browser.selectedBrowser;
michael@0 473 }
michael@0 474 for (; elem; elem = elem.parentNode) {
michael@0 475 try {
michael@0 476 if (elem.anonScrollBox) {
michael@0 477 scrollbox = elem.anonScrollBox;
michael@0 478 qinterface = scrollbox.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
michael@0 479 } else if (elem.scrollBoxObject) {
michael@0 480 scrollbox = elem;
michael@0 481 qinterface = elem.scrollBoxObject;
michael@0 482 break;
michael@0 483 } else if (elem.customDragger) {
michael@0 484 scrollbox = elem;
michael@0 485 break;
michael@0 486 } else if (elem.boxObject) {
michael@0 487 let qi = (elem._cachedSBO) ? elem._cachedSBO
michael@0 488 : elem.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
michael@0 489 if (qi) {
michael@0 490 scrollbox = elem;
michael@0 491 scrollbox._cachedSBO = qinterface = qi;
michael@0 492 break;
michael@0 493 }
michael@0 494 }
michael@0 495 } catch (e) { /* we aren't here to deal with your exceptions, we'll just keep
michael@0 496 traversing until we find something more well-behaved, as we
michael@0 497 prefer default behaviour to whiny scrollers. */ }
michael@0 498 }
michael@0 499 return [scrollbox, qinterface, (scrollbox ? (scrollbox.customDragger || this._defaultDragger) : null)];
michael@0 500 },
michael@0 501
michael@0 502 /** Determine if the distance moved can be considered a pan */
michael@0 503 isPan: function isPan(aPoint, aPoint2) {
michael@0 504 return (Math.abs(aPoint.x - aPoint2.x) > this.tapRadius ||
michael@0 505 Math.abs(aPoint.y - aPoint2.y) > this.tapRadius);
michael@0 506 },
michael@0 507
michael@0 508 /**
michael@0 509 * The default dragger object used by TouchModule when dragging a scrollable
michael@0 510 * element that provides no customDragger. Simply performs the expected
michael@0 511 * regular scrollBy calls on the scroller.
michael@0 512 */
michael@0 513 _defaultDragger: {
michael@0 514 isDraggable: function isDraggable(target, scroller) {
michael@0 515 let sX = {}, sY = {},
michael@0 516 pX = {}, pY = {};
michael@0 517 scroller.getPosition(pX, pY);
michael@0 518 scroller.getScrolledSize(sX, sY);
michael@0 519 let rect = target.getBoundingClientRect();
michael@0 520 return { x: (sX.value > rect.width || pX.value != 0),
michael@0 521 y: (sY.value > rect.height || pY.value != 0) };
michael@0 522 },
michael@0 523
michael@0 524 dragStart: function dragStart(cx, cy, target, scroller) {
michael@0 525 scroller.element.addEventListener("PanBegin", this._showScrollbars, false);
michael@0 526 },
michael@0 527
michael@0 528 dragStop: function dragStop(dx, dy, scroller) {
michael@0 529 scroller.element.removeEventListener("PanBegin", this._showScrollbars, false);
michael@0 530 return this.dragMove(dx, dy, scroller);
michael@0 531 },
michael@0 532
michael@0 533 dragMove: function dragMove(dx, dy, scroller) {
michael@0 534 if (scroller.getPosition) {
michael@0 535 try {
michael@0 536 let oldX = {}, oldY = {};
michael@0 537 scroller.getPosition(oldX, oldY);
michael@0 538
michael@0 539 scroller.scrollBy(dx, dy);
michael@0 540
michael@0 541 let newX = {}, newY = {};
michael@0 542 scroller.getPosition(newX, newY);
michael@0 543
michael@0 544 return (newX.value != oldX.value) || (newY.value != oldY.value);
michael@0 545
michael@0 546 } catch (e) { /* we have no time for whiny scrollers! */ }
michael@0 547 }
michael@0 548
michael@0 549 return false;
michael@0 550 },
michael@0 551
michael@0 552 _showScrollbars: function _showScrollbars(aEvent) {
michael@0 553 let scrollbox = aEvent.target;
michael@0 554 scrollbox.setAttribute("panning", "true");
michael@0 555
michael@0 556 let hideScrollbars = function() {
michael@0 557 scrollbox.removeEventListener("PanFinished", hideScrollbars, false);
michael@0 558 scrollbox.removeEventListener("CancelTouchSequence", hideScrollbars, false);
michael@0 559 scrollbox.removeAttribute("panning");
michael@0 560 }
michael@0 561
michael@0 562 // Wait for panning to be completely finished before removing scrollbars
michael@0 563 scrollbox.addEventListener("PanFinished", hideScrollbars, false);
michael@0 564 scrollbox.addEventListener("CancelTouchSequence", hideScrollbars, false);
michael@0 565 }
michael@0 566 }
michael@0 567 };
michael@0 568
michael@0 569 /**
michael@0 570 * DragData handles processing drags on the screen, handling both
michael@0 571 * locking of movement on one axis, and click detection.
michael@0 572 */
michael@0 573 function DragData() {
michael@0 574 this._domUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
michael@0 575 this._lockRevertThreshold = Util.displayDPI * kAxisLockRevertThreshold;
michael@0 576 this.reset();
michael@0 577 };
michael@0 578
michael@0 579 DragData.prototype = {
michael@0 580 reset: function reset() {
michael@0 581 this.dragging = false;
michael@0 582 this.sX = null;
michael@0 583 this.sY = null;
michael@0 584 this.locked = false;
michael@0 585 this.stayLocked = false;
michael@0 586 this.alwaysFreeDrag = false;
michael@0 587 this.lockedX = null;
michael@0 588 this.lockedY = null;
michael@0 589 this._originX = null;
michael@0 590 this._originY = null;
michael@0 591 this.prevPanX = null;
michael@0 592 this.prevPanY = null;
michael@0 593 this._isPan = false;
michael@0 594 },
michael@0 595
michael@0 596 /** Depending on drag data, locks sX,sY to X-axis or Y-axis of start position. */
michael@0 597 _lockAxis: function _lockAxis(sX, sY) {
michael@0 598 if (this.locked) {
michael@0 599 if (this.lockedX !== null)
michael@0 600 sX = this.lockedX;
michael@0 601 else if (this.lockedY !== null)
michael@0 602 sY = this.lockedY;
michael@0 603 return [sX, sY];
michael@0 604 }
michael@0 605 else {
michael@0 606 return [this._originX, this._originY];
michael@0 607 }
michael@0 608 },
michael@0 609
michael@0 610 setMousePosition: function setMousePosition(aEvent) {
michael@0 611 this._mouseX = aEvent.clientX;
michael@0 612 this._mouseY = aEvent.clientY;
michael@0 613 },
michael@0 614
michael@0 615 setDragPosition: function setDragPosition(sX, sY) {
michael@0 616 // Check if drag is now a pan.
michael@0 617 if (!this._isPan) {
michael@0 618 this._isPan = ScrollUtils.isPan(new Point(this._originX, this._originY), new Point(sX, sY));
michael@0 619 if (this._isPan) {
michael@0 620 this._resetActive();
michael@0 621 }
michael@0 622 }
michael@0 623
michael@0 624 // If now a pan, mark previous position where panning was.
michael@0 625 if (this._isPan) {
michael@0 626 let absX = Math.abs(this._originX - sX);
michael@0 627 let absY = Math.abs(this._originY - sY);
michael@0 628
michael@0 629 if (absX > this._lockRevertThreshold || absY > this._lockRevertThreshold)
michael@0 630 this.stayLocked = true;
michael@0 631
michael@0 632 // After the first lock, see if locking decision should be reverted.
michael@0 633 if (!this.stayLocked) {
michael@0 634 if (this.lockedX && absX > 3 * absY)
michael@0 635 this.lockedX = null;
michael@0 636 else if (this.lockedY && absY > 3 * absX)
michael@0 637 this.lockedY = null;
michael@0 638 }
michael@0 639
michael@0 640 if (!this.locked) {
michael@0 641 // look at difference from origin coord to lock movement, but only
michael@0 642 // do it if initial movement is sufficient to detect intent
michael@0 643
michael@0 644 // divide possibilty space into eight parts. Diagonals will allow
michael@0 645 // free movement, while moving towards a cardinal will lock that
michael@0 646 // axis. We pick a direction if you move more than twice as far
michael@0 647 // on one axis than another, which should be an angle of about 30
michael@0 648 // degrees from the axis
michael@0 649
michael@0 650 if (absX > 2.5 * absY)
michael@0 651 this.lockedY = sY;
michael@0 652 else if (absY > absX)
michael@0 653 this.lockedX = sX;
michael@0 654
michael@0 655 this.locked = true;
michael@0 656 }
michael@0 657 }
michael@0 658
michael@0 659 // Never lock if the dragger requests it
michael@0 660 if (this.alwaysFreeDrag) {
michael@0 661 this.lockedY = null;
michael@0 662 this.lockedX = null;
michael@0 663 }
michael@0 664
michael@0 665 // After pan lock, figure out previous panning position. Base it on last drag
michael@0 666 // position so there isn't a jump in panning.
michael@0 667 let [prevX, prevY] = this._lockAxis(this.sX, this.sY);
michael@0 668 this.prevPanX = prevX;
michael@0 669 this.prevPanY = prevY;
michael@0 670
michael@0 671 this.sX = sX;
michael@0 672 this.sY = sY;
michael@0 673 },
michael@0 674
michael@0 675 setDragStart: function setDragStart(screenX, screenY, aDraggable) {
michael@0 676 this.sX = this._originX = screenX;
michael@0 677 this.sY = this._originY = screenY;
michael@0 678 this.dragging = true;
michael@0 679
michael@0 680 // If the target area is pannable only in one direction lock it early
michael@0 681 // on the right axis
michael@0 682 this.lockedX = !aDraggable.x ? screenX : null;
michael@0 683 this.lockedY = !aDraggable.y ? screenY : null;
michael@0 684 this.stayLocked = this.lockedX || this.lockedY;
michael@0 685 this.locked = this.stayLocked;
michael@0 686 },
michael@0 687
michael@0 688 endDrag: function endDrag() {
michael@0 689 this._resetActive();
michael@0 690 this.dragging = false;
michael@0 691 },
michael@0 692
michael@0 693 /** Returns true if drag should pan scrollboxes.*/
michael@0 694 isPan: function isPan() {
michael@0 695 return this._isPan;
michael@0 696 },
michael@0 697
michael@0 698 /** Return true if drag should be parsed as a click. */
michael@0 699 isClick: function isClick() {
michael@0 700 return !this._isPan;
michael@0 701 },
michael@0 702
michael@0 703 /**
michael@0 704 * Returns the screen position for a pan. This factors in axis locking.
michael@0 705 * @return Array of screen X and Y coordinates
michael@0 706 */
michael@0 707 panPosition: function panPosition() {
michael@0 708 return this._lockAxis(this.sX, this.sY);
michael@0 709 },
michael@0 710
michael@0 711 /** dismiss the active state of the pan element */
michael@0 712 _resetActive: function _resetActive() {
michael@0 713 let target = document.documentElement;
michael@0 714 // If the target is active, toggle (turn off) the active flag. Otherwise do nothing.
michael@0 715 if (this._domUtils.getContentState(target) & kStateActive)
michael@0 716 this._domUtils.setContentState(target, kStateActive);
michael@0 717 },
michael@0 718
michael@0 719 toString: function toString() {
michael@0 720 return '[DragData] { sX,sY=' + this.sX + ',' + this.sY + ', dragging=' + this.dragging + ' }';
michael@0 721 }
michael@0 722 };
michael@0 723
michael@0 724
michael@0 725 /**
michael@0 726 * KineticController - a class to take drag position data and use it
michael@0 727 * to do kinetic panning of a scrollable object.
michael@0 728 *
michael@0 729 * aPanBy is a function that will be called with the dx and dy
michael@0 730 * generated by the kinetic algorithm. It should return true if the
michael@0 731 * object was panned, false if there was no movement.
michael@0 732 *
michael@0 733 * There are two complicated things done here. One is calculating the
michael@0 734 * initial velocity of the movement based on user input. Two is
michael@0 735 * calculating the distance to move every frame.
michael@0 736 */
michael@0 737 function KineticController(aPanBy, aEndCallback) {
michael@0 738 this._panBy = aPanBy;
michael@0 739 this._beforeEnd = aEndCallback;
michael@0 740
michael@0 741 // These are used to calculate the position of the scroll panes during kinetic panning. Think of
michael@0 742 // these points as vectors that are added together and multiplied by scalars.
michael@0 743 this._position = new Point(0, 0);
michael@0 744 this._velocity = new Point(0, 0);
michael@0 745 this._acceleration = new Point(0, 0);
michael@0 746 this._time = 0;
michael@0 747 this._timeStart = 0;
michael@0 748
michael@0 749 // How often do we change the position of the scroll pane? Too often and panning may jerk near
michael@0 750 // the end. Too little and panning will be choppy. In milliseconds.
michael@0 751 this._updateInterval = Services.prefs.getIntPref("browser.ui.kinetic.updateInterval");
michael@0 752 // Constants that affect the "friction" of the scroll pane.
michael@0 753 this._exponentialC = Services.prefs.getIntPref("browser.ui.kinetic.exponentialC");
michael@0 754 this._polynomialC = Services.prefs.getIntPref("browser.ui.kinetic.polynomialC") / 1000000;
michael@0 755 // Number of milliseconds that can contain a swipe. Movements earlier than this are disregarded.
michael@0 756 this._swipeLength = Services.prefs.getIntPref("browser.ui.kinetic.swipeLength");
michael@0 757
michael@0 758 this._reset();
michael@0 759 }
michael@0 760
michael@0 761 KineticController.prototype = {
michael@0 762 _reset: function _reset() {
michael@0 763 this._active = false;
michael@0 764 this._paused = false;
michael@0 765 this.momentumBuffer = [];
michael@0 766 this._velocity.set(0, 0);
michael@0 767 },
michael@0 768
michael@0 769 isActive: function isActive() {
michael@0 770 return this._active;
michael@0 771 },
michael@0 772
michael@0 773 _startTimer: function _startTimer() {
michael@0 774 let self = this;
michael@0 775
michael@0 776 let lastp = this._position; // track last position vector because pan takes deltas
michael@0 777 let v0 = this._velocity; // initial velocity
michael@0 778 let a = this._acceleration; // acceleration
michael@0 779 let c = this._exponentialC;
michael@0 780 let p = new Point(0, 0);
michael@0 781 let dx, dy, t, realt;
michael@0 782
michael@0 783 function calcP(v0, a, t) {
michael@0 784 // Important traits for this function:
michael@0 785 // p(t=0) is 0
michael@0 786 // p'(t=0) is v0
michael@0 787 //
michael@0 788 // We use exponential to get a smoother stop, but by itself exponential
michael@0 789 // is too smooth at the end. Adding a polynomial with the appropriate
michael@0 790 // weight helps to balance
michael@0 791 return v0 * Math.exp(-t / c) * -c + a * t * t + v0 * c;
michael@0 792 }
michael@0 793
michael@0 794 this._calcV = function(v0, a, t) {
michael@0 795 return v0 * Math.exp(-t / c) + 2 * a * t;
michael@0 796 }
michael@0 797
michael@0 798 let callback = {
michael@0 799 sample: function kineticHandleEvent(timeStamp) {
michael@0 800 // Someone called end() on us between timer intervals
michael@0 801 // or we are paused.
michael@0 802 if (!self.isActive() || self._paused)
michael@0 803 return;
michael@0 804
michael@0 805 // To make animation end fast enough but to keep smoothness, average the ideal
michael@0 806 // time frame (smooth animation) with the actual time lapse (end fast enough).
michael@0 807 // Animation will never take longer than 2 times the ideal length of time.
michael@0 808 realt = timeStamp - self._initialTime;
michael@0 809 self._time += self._updateInterval;
michael@0 810 t = (self._time + realt) / 2;
michael@0 811
michael@0 812 // Calculate new position.
michael@0 813 p.x = calcP(v0.x, a.x, t);
michael@0 814 p.y = calcP(v0.y, a.y, t);
michael@0 815 dx = Math.round(p.x - lastp.x);
michael@0 816 dy = Math.round(p.y - lastp.y);
michael@0 817
michael@0 818 // Test to see if movement is finished for each component.
michael@0 819 if (dx * a.x > 0) {
michael@0 820 dx = 0;
michael@0 821 lastp.x = 0;
michael@0 822 v0.x = 0;
michael@0 823 a.x = 0;
michael@0 824 }
michael@0 825 // Symmetric to above case.
michael@0 826 if (dy * a.y > 0) {
michael@0 827 dy = 0;
michael@0 828 lastp.y = 0;
michael@0 829 v0.y = 0;
michael@0 830 a.y = 0;
michael@0 831 }
michael@0 832
michael@0 833 if (v0.x == 0 && v0.y == 0) {
michael@0 834 self.end();
michael@0 835 } else {
michael@0 836 let panStop = false;
michael@0 837 if (dx != 0 || dy != 0) {
michael@0 838 try { panStop = !self._panBy(-dx, -dy, true); } catch (e) {}
michael@0 839 lastp.add(dx, dy);
michael@0 840 }
michael@0 841
michael@0 842 if (panStop)
michael@0 843 self.end();
michael@0 844 else
michael@0 845 mozRequestAnimationFrame(this);
michael@0 846 }
michael@0 847 }
michael@0 848 };
michael@0 849
michael@0 850 this._active = true;
michael@0 851 this._paused = false;
michael@0 852 mozRequestAnimationFrame(callback);
michael@0 853 },
michael@0 854
michael@0 855 start: function start() {
michael@0 856 function sign(x) {
michael@0 857 return x ? ((x > 0) ? 1 : -1) : 0;
michael@0 858 }
michael@0 859
michael@0 860 function clampFromZero(x, closerToZero, furtherFromZero) {
michael@0 861 if (x >= 0)
michael@0 862 return Math.max(closerToZero, Math.min(furtherFromZero, x));
michael@0 863 return Math.min(-closerToZero, Math.max(-furtherFromZero, x));
michael@0 864 }
michael@0 865
michael@0 866 let mb = this.momentumBuffer;
michael@0 867 let mblen = this.momentumBuffer.length;
michael@0 868
michael@0 869 let lastTime = mb[mblen - 1].t;
michael@0 870 let distanceX = 0;
michael@0 871 let distanceY = 0;
michael@0 872 let swipeLength = this._swipeLength;
michael@0 873
michael@0 874 // determine speed based on recorded input
michael@0 875 let me;
michael@0 876 for (let i = 0; i < mblen; i++) {
michael@0 877 me = mb[i];
michael@0 878 if (lastTime - me.t < swipeLength) {
michael@0 879 distanceX += me.dx;
michael@0 880 distanceY += me.dy;
michael@0 881 }
michael@0 882 }
michael@0 883
michael@0 884 let currentVelocityX = 0;
michael@0 885 let currentVelocityY = 0;
michael@0 886
michael@0 887 if (this.isActive()) {
michael@0 888 // If active, then we expect this._calcV to be defined.
michael@0 889 let currentTime = Date.now() - this._initialTime;
michael@0 890 currentVelocityX = Util.clamp(this._calcV(this._velocity.x, this._acceleration.x, currentTime), -kMaxVelocity, kMaxVelocity);
michael@0 891 currentVelocityY = Util.clamp(this._calcV(this._velocity.y, this._acceleration.y, currentTime), -kMaxVelocity, kMaxVelocity);
michael@0 892 }
michael@0 893
michael@0 894 if (currentVelocityX * this._velocity.x <= 0)
michael@0 895 currentVelocityX = 0;
michael@0 896 if (currentVelocityY * this._velocity.y <= 0)
michael@0 897 currentVelocityY = 0;
michael@0 898
michael@0 899 let swipeTime = Math.min(swipeLength, lastTime - mb[0].t);
michael@0 900 this._velocity.x = clampFromZero((distanceX / swipeTime) + currentVelocityX, Math.abs(currentVelocityX), kMaxVelocity);
michael@0 901 this._velocity.y = clampFromZero((distanceY / swipeTime) + currentVelocityY, Math.abs(currentVelocityY), kMaxVelocity);
michael@0 902
michael@0 903 if (Math.abs(this._velocity.x) < kMinVelocity)
michael@0 904 this._velocity.x = 0;
michael@0 905 if (Math.abs(this._velocity.y) < kMinVelocity)
michael@0 906 this._velocity.y = 0;
michael@0 907
michael@0 908 // Set acceleration vector to opposite signs of velocity
michael@0 909 this._acceleration.set(this._velocity.clone().map(sign).scale(-this._polynomialC));
michael@0 910
michael@0 911 this._position.set(0, 0);
michael@0 912 this._initialTime = mozAnimationStartTime;
michael@0 913 this._time = 0;
michael@0 914 this.momentumBuffer = [];
michael@0 915
michael@0 916 if (!this.isActive() || this._paused)
michael@0 917 this._startTimer();
michael@0 918
michael@0 919 return true;
michael@0 920 },
michael@0 921
michael@0 922 pause: function pause() {
michael@0 923 this._paused = true;
michael@0 924 },
michael@0 925
michael@0 926 end: function end() {
michael@0 927 if (this.isActive()) {
michael@0 928 if (this._beforeEnd)
michael@0 929 this._beforeEnd();
michael@0 930 this._reset();
michael@0 931 }
michael@0 932 },
michael@0 933
michael@0 934 addData: function addData(dx, dy) {
michael@0 935 let mbLength = this.momentumBuffer.length;
michael@0 936 let now = Date.now();
michael@0 937
michael@0 938 if (this.isActive()) {
michael@0 939 // Stop active movement when dragging in other direction.
michael@0 940 if (dx * this._velocity.x < 0 || dy * this._velocity.y < 0)
michael@0 941 this.end();
michael@0 942 }
michael@0 943
michael@0 944 this.momentumBuffer.push({'t': now, 'dx' : dx, 'dy' : dy});
michael@0 945 }
michael@0 946 };
michael@0 947
michael@0 948
michael@0 949 /*
michael@0 950 * Simple gestures support
michael@0 951 */
michael@0 952
michael@0 953 var GestureModule = {
michael@0 954 _debugEvents: false,
michael@0 955
michael@0 956 init: function init() {
michael@0 957 window.addEventListener("MozSwipeGesture", this, true);
michael@0 958 },
michael@0 959
michael@0 960 /*
michael@0 961 * Events
michael@0 962 *
michael@0 963 * Dispatch events based on the type of mouse gesture event. For now, make
michael@0 964 * sure to stop propagation of every gesture event so that web content cannot
michael@0 965 * receive gesture events.
michael@0 966 *
michael@0 967 * @param nsIDOMEvent information structure
michael@0 968 */
michael@0 969
michael@0 970 handleEvent: function handleEvent(aEvent) {
michael@0 971 try {
michael@0 972 aEvent.stopPropagation();
michael@0 973 aEvent.preventDefault();
michael@0 974 if (this._debugEvents) Util.dumpLn("GestureModule:", aEvent.type);
michael@0 975 switch (aEvent.type) {
michael@0 976 case "MozSwipeGesture":
michael@0 977 if (this._onSwipe(aEvent)) {
michael@0 978 let event = document.createEvent("Events");
michael@0 979 event.initEvent("CancelTouchSequence", true, true);
michael@0 980 aEvent.target.dispatchEvent(event);
michael@0 981 }
michael@0 982 break;
michael@0 983 }
michael@0 984 } catch (e) {
michael@0 985 Util.dumpLn("Error while handling gesture event", aEvent.type,
michael@0 986 "\nPlease report error at:", e);
michael@0 987 Cu.reportError(e);
michael@0 988 }
michael@0 989 },
michael@0 990
michael@0 991 _onSwipe: function _onSwipe(aEvent) {
michael@0 992 switch (aEvent.direction) {
michael@0 993 case Ci.nsIDOMSimpleGestureEvent.DIRECTION_LEFT:
michael@0 994 return this._tryCommand("cmd_forward");
michael@0 995 case Ci.nsIDOMSimpleGestureEvent.DIRECTION_RIGHT:
michael@0 996 return this._tryCommand("cmd_back");
michael@0 997 }
michael@0 998 return false;
michael@0 999 },
michael@0 1000
michael@0 1001 _tryCommand: function _tryCommand(aId) {
michael@0 1002 if (document.getElementById(aId).getAttribute("disabled") == "true")
michael@0 1003 return false;
michael@0 1004 CommandUpdater.doCommand(aId);
michael@0 1005 return true;
michael@0 1006 },
michael@0 1007 };
michael@0 1008
michael@0 1009 /**
michael@0 1010 * Helper to track when the user is using a precise pointing device (pen/mouse)
michael@0 1011 * versus an imprecise one (touch).
michael@0 1012 */
michael@0 1013 var InputSourceHelper = {
michael@0 1014 isPrecise: false,
michael@0 1015
michael@0 1016 init: function ish_init() {
michael@0 1017 Services.obs.addObserver(this, "metro_precise_input", false);
michael@0 1018 Services.obs.addObserver(this, "metro_imprecise_input", false);
michael@0 1019 },
michael@0 1020
michael@0 1021 _precise: function () {
michael@0 1022 if (!this.isPrecise) {
michael@0 1023 this.isPrecise = true;
michael@0 1024 this._fire("MozPrecisePointer");
michael@0 1025 }
michael@0 1026 },
michael@0 1027
michael@0 1028 _imprecise: function () {
michael@0 1029 if (this.isPrecise) {
michael@0 1030 this.isPrecise = false;
michael@0 1031 this._fire("MozImprecisePointer");
michael@0 1032 }
michael@0 1033 },
michael@0 1034
michael@0 1035 observe: function BrowserUI_observe(aSubject, aTopic, aData) {
michael@0 1036 switch (aTopic) {
michael@0 1037 case "metro_precise_input":
michael@0 1038 this._precise();
michael@0 1039 break;
michael@0 1040 case "metro_imprecise_input":
michael@0 1041 this._imprecise();
michael@0 1042 break;
michael@0 1043 }
michael@0 1044 },
michael@0 1045
michael@0 1046 fireUpdate: function fireUpdate() {
michael@0 1047 if (this.isPrecise) {
michael@0 1048 this._fire("MozPrecisePointer");
michael@0 1049 } else {
michael@0 1050 this._fire("MozImprecisePointer");
michael@0 1051 }
michael@0 1052 },
michael@0 1053
michael@0 1054 _fire: function (name) {
michael@0 1055 let event = document.createEvent("Events");
michael@0 1056 event.initEvent(name, true, true);
michael@0 1057 window.dispatchEvent(event);
michael@0 1058 }
michael@0 1059 };

mercurial