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

     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");
  1026   },
  1028   _imprecise: function () {
  1029     if (this.isPrecise) {
  1030       this.isPrecise = false;
  1031       this._fire("MozImprecisePointer");
  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;
  1044   },
  1046   fireUpdate: function fireUpdate() {
  1047     if (this.isPrecise) {
  1048       this._fire("MozPrecisePointer");
  1049     } else {
  1050       this._fire("MozImprecisePointer");
  1052   },
  1054   _fire: function (name) {
  1055     let event = document.createEvent("Events");
  1056     event.initEvent(name, true, true);
  1057     window.dispatchEvent(event);
  1059 };

mercurial