Wed, 31 Dec 2014 06:55:50 +0100
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 | }; |