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