1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/metro/base/content/input.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1059 @@ 1.4 +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; js2-strict-trailing-comma-warning: nil -*- 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +Components.utils.import("resource://gre/modules/Geometry.jsm"); 1.10 + 1.11 +/* 1.12 + * Drag scrolling related constants 1.13 + */ 1.14 + 1.15 +// maximum drag distance in inches while axis locking can still be reverted 1.16 +const kAxisLockRevertThreshold = 0.8; 1.17 + 1.18 +// Same as NS_EVENT_STATE_ACTIVE from mozilla/EventStates.h 1.19 +const kStateActive = 0x00000001; 1.20 + 1.21 +// After a drag begins, kinetic panning is stopped if the drag doesn't become 1.22 +// a pan in 300 milliseconds. 1.23 +const kStopKineticPanOnDragTimeout = 300; 1.24 + 1.25 +// Min/max velocity of kinetic panning. This is in pixels/millisecond. 1.26 +const kMinVelocity = 0.4; 1.27 +const kMaxVelocity = 6; 1.28 + 1.29 +/* 1.30 + * prefs 1.31 + */ 1.32 + 1.33 +// Display rects around selection ranges. Useful in debugging 1.34 +// selection problems. 1.35 +const kDebugSelectionDisplayPref = "metro.debug.selection.displayRanges"; 1.36 +// Dump range rect data to the console. Very useful, but also slows 1.37 +// things down a lot. 1.38 +const kDebugSelectionDumpPref = "metro.debug.selection.dumpRanges"; 1.39 +// Dump message manager event traffic for selection. 1.40 +const kDebugSelectionDumpEvents = "metro.debug.selection.dumpEvents"; 1.41 +const kAsyncPanZoomEnabled = "layers.async-pan-zoom.enabled" 1.42 + 1.43 +/** 1.44 + * TouchModule 1.45 + * 1.46 + * Handles all touch-related input such as dragging and tapping. 1.47 + * 1.48 + * The Fennec chrome DOM tree has elements that are augmented dynamically with 1.49 + * custom JS properties that tell the TouchModule they have custom support for 1.50 + * either dragging or clicking. These JS properties are JS objects that expose 1.51 + * an interface supporting dragging or clicking (though currently we only look 1.52 + * to drag scrollable elements). 1.53 + * 1.54 + * A custom dragger is a JS property that lives on a scrollable DOM element, 1.55 + * accessible as myElement.customDragger. The customDragger must support the 1.56 + * following interface: (The `scroller' argument is given for convenience, and 1.57 + * is the object reference to the element's scrollbox object). 1.58 + * 1.59 + * dragStart(cX, cY, target, scroller) 1.60 + * Signals the beginning of a drag. Coordinates are passed as 1.61 + * client coordinates. target is copied from the event. 1.62 + * 1.63 + * dragStop(dx, dy, scroller) 1.64 + * Signals the end of a drag. The dx, dy parameters may be non-zero to 1.65 + * indicate one last drag movement. 1.66 + * 1.67 + * dragMove(dx, dy, scroller, isKinetic) 1.68 + * Signals an input attempt to drag by dx, dy. 1.69 + * 1.70 + * There is a default dragger in case a scrollable element is dragged --- see 1.71 + * the defaultDragger prototype property. 1.72 + */ 1.73 + 1.74 +var TouchModule = { 1.75 + _debugEvents: false, 1.76 + _isCancelled: false, 1.77 + _isCancellable: false, 1.78 + 1.79 + init: function init() { 1.80 + this._dragData = new DragData(); 1.81 + 1.82 + this._dragger = null; 1.83 + 1.84 + this._targetScrollbox = null; 1.85 + this._targetScrollInterface = null; 1.86 + 1.87 + this._kinetic = new KineticController(this._dragBy.bind(this), 1.88 + this._kineticStop.bind(this)); 1.89 + 1.90 + // capture phase events 1.91 + window.addEventListener("CancelTouchSequence", this, true); 1.92 + window.addEventListener("keydown", this, true); 1.93 + window.addEventListener("MozMouseHittest", this, true); 1.94 + 1.95 + // bubble phase 1.96 + window.addEventListener("contextmenu", this, false); 1.97 + window.addEventListener("touchstart", this, false); 1.98 + window.addEventListener("touchmove", this, false); 1.99 + window.addEventListener("touchend", this, false); 1.100 + 1.101 + Services.obs.addObserver(this, "Gesture:SingleTap", false); 1.102 + Services.obs.addObserver(this, "Gesture:DoubleTap", false); 1.103 + }, 1.104 + 1.105 + /* 1.106 + * Events 1.107 + */ 1.108 + 1.109 + handleEvent: function handleEvent(aEvent) { 1.110 + switch (aEvent.type) { 1.111 + case "contextmenu": 1.112 + this._onContextMenu(aEvent); 1.113 + break; 1.114 + 1.115 + case "CancelTouchSequence": 1.116 + this.cancelPending(); 1.117 + break; 1.118 + 1.119 + default: { 1.120 + if (this._debugEvents) { 1.121 + if (aEvent.type != "touchmove") 1.122 + Util.dumpLn("TouchModule:", aEvent.type, aEvent.target); 1.123 + } 1.124 + 1.125 + switch (aEvent.type) { 1.126 + case "touchstart": 1.127 + this._onTouchStart(aEvent); 1.128 + break; 1.129 + case "touchmove": 1.130 + this._onTouchMove(aEvent); 1.131 + break; 1.132 + case "touchend": 1.133 + this._onTouchEnd(aEvent); 1.134 + break; 1.135 + case "keydown": 1.136 + this._handleKeyDown(aEvent); 1.137 + break; 1.138 + case "MozMouseHittest": 1.139 + // Used by widget to hit test chrome vs content. Make sure the XUl scrollbars 1.140 + // are counted as "chrome". Since the XUL scrollbars have sub-elements we walk 1.141 + // the parent chain to ensure we catch all of those as well. 1.142 + let onScrollbar = false; 1.143 + for (let node = aEvent.originalTarget; node instanceof XULElement; node = node.parentNode) { 1.144 + if (node.tagName == 'scrollbar') { 1.145 + onScrollbar = true; 1.146 + break; 1.147 + } 1.148 + } 1.149 + if (onScrollbar || aEvent.target.ownerDocument == document) { 1.150 + aEvent.preventDefault(); 1.151 + } 1.152 + aEvent.stopPropagation(); 1.153 + break; 1.154 + } 1.155 + } 1.156 + } 1.157 + }, 1.158 + 1.159 + _handleKeyDown: function _handleKeyDown(aEvent) { 1.160 + const TABKEY = 9; 1.161 + if (aEvent.keyCode == TABKEY && !InputSourceHelper.isPrecise) { 1.162 + if (Util.isEditable(aEvent.target) && 1.163 + aEvent.target.selectionStart != aEvent.target.selectionEnd) { 1.164 + SelectionHelperUI.closeEditSession(false); 1.165 + } 1.166 + setTimeout(function() { 1.167 + let element = Browser.selectedBrowser.contentDocument.activeElement; 1.168 + // We only want to attach monocles if we have an input, text area, 1.169 + // there is selection, and the target element changed. 1.170 + // Sometimes the target element won't change even though selection is 1.171 + // cleared because of focus outside the browser. 1.172 + if (Util.isEditable(element) && 1.173 + !SelectionHelperUI.isActive && 1.174 + element.selectionStart != element.selectionEnd && 1.175 + // not e10s friendly 1.176 + aEvent.target != element) { 1.177 + let rect = element.getBoundingClientRect(); 1.178 + SelectionHelperUI.attachEditSession(Browser.selectedBrowser, 1.179 + rect.left + rect.width / 2, 1.180 + rect.top + rect.height / 2); 1.181 + } 1.182 + }, 50); 1.183 + } 1.184 + }, 1.185 + 1.186 + observe: function BrowserUI_observe(aSubject, aTopic, aData) { 1.187 + switch (aTopic) { 1.188 + case "Gesture:SingleTap": 1.189 + case "Gesture:DoubleTap": 1.190 + Browser.selectedBrowser.messageManager.sendAsyncMessage(aTopic, JSON.parse(aData)); 1.191 + break; 1.192 + } 1.193 + }, 1.194 + 1.195 + 1.196 + sample: function sample(aTimeStamp) { 1.197 + this._waitingForPaint = false; 1.198 + }, 1.199 + 1.200 + /** 1.201 + * This gets invoked by the input handler if another module grabs. We should 1.202 + * reset our state or something here. This is probably doing the wrong thing 1.203 + * in its current form. 1.204 + */ 1.205 + cancelPending: function cancelPending() { 1.206 + this._doDragStop(); 1.207 + 1.208 + // Kinetic panning may have already been active or drag stop above may have 1.209 + // made kinetic panning active. 1.210 + this._kinetic.end(); 1.211 + 1.212 + this._targetScrollbox = null; 1.213 + this._targetScrollInterface = null; 1.214 + }, 1.215 + 1.216 + _onContextMenu: function _onContextMenu(aEvent) { 1.217 + // bug 598965 - chrome UI should stop to be pannable once the 1.218 + // context menu has appeared. 1.219 + if (ContextMenuUI.popupState) { 1.220 + this.cancelPending(); 1.221 + } 1.222 + }, 1.223 + 1.224 + /** Begin possible pan and send tap down event. */ 1.225 + _onTouchStart: function _onTouchStart(aEvent) { 1.226 + if (aEvent.touches.length > 1) 1.227 + return; 1.228 + 1.229 + this._isCancelled = false; 1.230 + this._isCancellable = true; 1.231 + 1.232 + if (aEvent.defaultPrevented) { 1.233 + this._isCancelled = true; 1.234 + return; 1.235 + } 1.236 + 1.237 + let dragData = this._dragData; 1.238 + if (dragData.dragging) { 1.239 + // Somehow a mouse up was missed. 1.240 + this._doDragStop(); 1.241 + } 1.242 + dragData.reset(); 1.243 + this.dX = 0; 1.244 + this.dY = 0; 1.245 + 1.246 + // walk up the DOM tree in search of nearest scrollable ancestor. nulls are 1.247 + // returned if none found. 1.248 + let [targetScrollbox, targetScrollInterface, dragger] 1.249 + = ScrollUtils.getScrollboxFromElement(aEvent.originalTarget); 1.250 + 1.251 + // stop kinetic panning if targetScrollbox has changed 1.252 + if (this._kinetic.isActive() && this._dragger != dragger) 1.253 + this._kinetic.end(); 1.254 + 1.255 + this._targetScrollbox = targetScrollInterface ? targetScrollInterface.element : targetScrollbox; 1.256 + this._targetScrollInterface = targetScrollInterface; 1.257 + 1.258 + if (!this._targetScrollbox) { 1.259 + return; 1.260 + } 1.261 + 1.262 + // Don't allow kinetic panning if APZC is enabled and the pan element is the deck 1.263 + let deck = document.getElementById("browsers"); 1.264 + if (Services.prefs.getBoolPref(kAsyncPanZoomEnabled) && 1.265 + this._targetScrollbox == deck) { 1.266 + return; 1.267 + } 1.268 + 1.269 + // XXX shouldn't dragger always be valid here? 1.270 + if (dragger) { 1.271 + let draggable = dragger.isDraggable(targetScrollbox, targetScrollInterface); 1.272 + dragData.locked = !draggable.x || !draggable.y; 1.273 + if (draggable.x || draggable.y) { 1.274 + this._dragger = dragger; 1.275 + if (dragger.freeDrag) 1.276 + dragData.alwaysFreeDrag = dragger.freeDrag(); 1.277 + this._doDragStart(aEvent, draggable); 1.278 + } 1.279 + } 1.280 + }, 1.281 + 1.282 + /** Send tap up event and any necessary full taps. */ 1.283 + _onTouchEnd: function _onTouchEnd(aEvent) { 1.284 + if (aEvent.touches.length > 0 || this._isCancelled || !this._targetScrollbox) 1.285 + return; 1.286 + 1.287 + // onMouseMove will not record the delta change if we are waiting for a 1.288 + // paint. Since this is the last input for this drag, we override the flag. 1.289 + this._waitingForPaint = false; 1.290 + this._onTouchMove(aEvent); 1.291 + 1.292 + let dragData = this._dragData; 1.293 + this._doDragStop(); 1.294 + }, 1.295 + 1.296 + /** 1.297 + * If we're in a drag, do what we have to do to drag on. 1.298 + */ 1.299 + _onTouchMove: function _onTouchMove(aEvent) { 1.300 + if (aEvent.touches.length > 1) 1.301 + return; 1.302 + 1.303 + if (this._isCancellable) { 1.304 + // only the first touchmove is cancellable. 1.305 + this._isCancellable = false; 1.306 + if (aEvent.defaultPrevented) { 1.307 + this._isCancelled = true; 1.308 + } 1.309 + } 1.310 + 1.311 + if (this._isCancelled) 1.312 + return; 1.313 + 1.314 + let touch = aEvent.changedTouches[0]; 1.315 + if (!this._targetScrollbox) { 1.316 + return; 1.317 + } 1.318 + 1.319 + let dragData = this._dragData; 1.320 + 1.321 + if (dragData.dragging) { 1.322 + let oldIsPan = dragData.isPan(); 1.323 + dragData.setDragPosition(touch.screenX, touch.screenY); 1.324 + dragData.setMousePosition(touch); 1.325 + 1.326 + // Kinetic panning is sensitive to time. It is more stable if it receives 1.327 + // the mousemove events as they come. For dragging though, we only want 1.328 + // to call _dragBy if we aren't waiting for a paint (so we don't spam the 1.329 + // main browser loop with a bunch of redundant paints). 1.330 + // 1.331 + // Here, we feed kinetic panning drag differences for mouse events as 1.332 + // come; for dragging, we build up a drag buffer in this.dX/this.dY and 1.333 + // release it when we are ready to paint. 1.334 + // 1.335 + let [sX, sY] = dragData.panPosition(); 1.336 + this.dX += dragData.prevPanX - sX; 1.337 + this.dY += dragData.prevPanY - sY; 1.338 + 1.339 + if (dragData.isPan()) { 1.340 + // Only pan when mouse event isn't part of a click. Prevent jittering on tap. 1.341 + this._kinetic.addData(sX - dragData.prevPanX, sY - dragData.prevPanY); 1.342 + 1.343 + // dragBy will reset dX and dY values to 0 1.344 + this._dragBy(this.dX, this.dY); 1.345 + 1.346 + // Let everyone know when mousemove begins a pan 1.347 + if (!oldIsPan && dragData.isPan()) { 1.348 + let event = document.createEvent("Events"); 1.349 + event.initEvent("PanBegin", true, false); 1.350 + this._targetScrollbox.dispatchEvent(event); 1.351 + 1.352 + Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:PanBegin", {}); 1.353 + } 1.354 + } 1.355 + } 1.356 + }, 1.357 + 1.358 + /** 1.359 + * Inform our dragger of a dragStart. 1.360 + */ 1.361 + _doDragStart: function _doDragStart(aEvent, aDraggable) { 1.362 + let touch = aEvent.changedTouches[0]; 1.363 + let dragData = this._dragData; 1.364 + dragData.setDragStart(touch.screenX, touch.screenY, aDraggable); 1.365 + this._kinetic.addData(0, 0); 1.366 + this._dragStartTime = Date.now(); 1.367 + if (!this._kinetic.isActive()) { 1.368 + this._dragger.dragStart(touch.clientX, touch.clientY, touch.target, this._targetScrollInterface); 1.369 + } 1.370 + }, 1.371 + 1.372 + /** Finish a drag. */ 1.373 + _doDragStop: function _doDragStop() { 1.374 + let dragData = this._dragData; 1.375 + if (!dragData.dragging) 1.376 + return; 1.377 + 1.378 + dragData.endDrag(); 1.379 + 1.380 + // Note: it is possible for kinetic scrolling to be active from a 1.381 + // mousedown/mouseup event previous to this one. In this case, we 1.382 + // want the kinetic panner to tell our drag interface to stop. 1.383 + 1.384 + if (dragData.isPan()) { 1.385 + if (Date.now() - this._dragStartTime > kStopKineticPanOnDragTimeout) 1.386 + this._kinetic._velocity.set(0, 0); 1.387 + 1.388 + // Start kinetic pan if we aren't using async pan zoom or the scroll 1.389 + // element is not browsers. 1.390 + let deck = document.getElementById("browsers"); 1.391 + if (!Services.prefs.getBoolPref(kAsyncPanZoomEnabled) || 1.392 + this._targetScrollbox != deck) { 1.393 + this._kinetic.start(); 1.394 + } 1.395 + } else { 1.396 + this._kinetic.end(); 1.397 + if (this._dragger) 1.398 + this._dragger.dragStop(0, 0, this._targetScrollInterface); 1.399 + this._dragger = null; 1.400 + } 1.401 + }, 1.402 + 1.403 + /** 1.404 + * Used by _onTouchMove() above and by KineticController's timer to do the 1.405 + * actual dragMove signalling to the dragger. We'd put this in _onTouchMove() 1.406 + * but then KineticController would be adding to its own data as it signals 1.407 + * the dragger of dragMove()s. 1.408 + */ 1.409 + _dragBy: function _dragBy(dX, dY, aIsKinetic) { 1.410 + let dragged = true; 1.411 + let dragData = this._dragData; 1.412 + if (!this._waitingForPaint || aIsKinetic) { 1.413 + let dragData = this._dragData; 1.414 + dragged = this._dragger.dragMove(dX, dY, this._targetScrollInterface, aIsKinetic, 1.415 + dragData._mouseX, dragData._mouseY); 1.416 + if (dragged && !this._waitingForPaint) { 1.417 + this._waitingForPaint = true; 1.418 + mozRequestAnimationFrame(this); 1.419 + } 1.420 + this.dX = 0; 1.421 + this.dY = 0; 1.422 + } 1.423 + if (!dragData.isPan()) 1.424 + this._kinetic.pause(); 1.425 + 1.426 + return dragged; 1.427 + }, 1.428 + 1.429 + /** Callback for kinetic scroller. */ 1.430 + _kineticStop: function _kineticStop() { 1.431 + // Kinetic panning could finish while user is panning, so don't finish 1.432 + // the pan just yet. 1.433 + let dragData = this._dragData; 1.434 + if (!dragData.dragging) { 1.435 + if (this._dragger) 1.436 + this._dragger.dragStop(0, 0, this._targetScrollInterface); 1.437 + this._dragger = null; 1.438 + 1.439 + let event = document.createEvent("Events"); 1.440 + event.initEvent("PanFinished", true, false); 1.441 + this._targetScrollbox.dispatchEvent(event); 1.442 + } 1.443 + }, 1.444 + 1.445 + toString: function toString() { 1.446 + return '[TouchModule] {' 1.447 + + '\n\tdragData=' + this._dragData + ', ' 1.448 + + 'dragger=' + this._dragger + ', ' 1.449 + + '\n\ttargetScroller=' + this._targetScrollInterface + '}'; 1.450 + }, 1.451 +}; 1.452 + 1.453 +var ScrollUtils = { 1.454 + // threshold in pixels for sensing a tap as opposed to a pan 1.455 + get tapRadius() { 1.456 + let dpi = Util.displayDPI; 1.457 + delete this.tapRadius; 1.458 + return this.tapRadius = Services.prefs.getIntPref("ui.dragThresholdX") / 240 * dpi; 1.459 + }, 1.460 + 1.461 + /** 1.462 + * Walk up (parentward) the DOM tree from elem in search of a scrollable element. 1.463 + * Return the element and its scroll interface if one is found, two nulls otherwise. 1.464 + * 1.465 + * This function will cache the pointer to the scroll interface on the element itself, 1.466 + * so it is safe to call it many times without incurring the same XPConnect overhead 1.467 + * as in the initial call. 1.468 + */ 1.469 + getScrollboxFromElement: function getScrollboxFromElement(elem) { 1.470 + let scrollbox = null; 1.471 + let qinterface = null; 1.472 + 1.473 + // if element is content or the startui page, get the browser scroll interface 1.474 + if (elem.ownerDocument == Browser.selectedBrowser.contentDocument) { 1.475 + elem = Browser.selectedBrowser; 1.476 + } 1.477 + for (; elem; elem = elem.parentNode) { 1.478 + try { 1.479 + if (elem.anonScrollBox) { 1.480 + scrollbox = elem.anonScrollBox; 1.481 + qinterface = scrollbox.boxObject.QueryInterface(Ci.nsIScrollBoxObject); 1.482 + } else if (elem.scrollBoxObject) { 1.483 + scrollbox = elem; 1.484 + qinterface = elem.scrollBoxObject; 1.485 + break; 1.486 + } else if (elem.customDragger) { 1.487 + scrollbox = elem; 1.488 + break; 1.489 + } else if (elem.boxObject) { 1.490 + let qi = (elem._cachedSBO) ? elem._cachedSBO 1.491 + : elem.boxObject.QueryInterface(Ci.nsIScrollBoxObject); 1.492 + if (qi) { 1.493 + scrollbox = elem; 1.494 + scrollbox._cachedSBO = qinterface = qi; 1.495 + break; 1.496 + } 1.497 + } 1.498 + } catch (e) { /* we aren't here to deal with your exceptions, we'll just keep 1.499 + traversing until we find something more well-behaved, as we 1.500 + prefer default behaviour to whiny scrollers. */ } 1.501 + } 1.502 + return [scrollbox, qinterface, (scrollbox ? (scrollbox.customDragger || this._defaultDragger) : null)]; 1.503 + }, 1.504 + 1.505 + /** Determine if the distance moved can be considered a pan */ 1.506 + isPan: function isPan(aPoint, aPoint2) { 1.507 + return (Math.abs(aPoint.x - aPoint2.x) > this.tapRadius || 1.508 + Math.abs(aPoint.y - aPoint2.y) > this.tapRadius); 1.509 + }, 1.510 + 1.511 + /** 1.512 + * The default dragger object used by TouchModule when dragging a scrollable 1.513 + * element that provides no customDragger. Simply performs the expected 1.514 + * regular scrollBy calls on the scroller. 1.515 + */ 1.516 + _defaultDragger: { 1.517 + isDraggable: function isDraggable(target, scroller) { 1.518 + let sX = {}, sY = {}, 1.519 + pX = {}, pY = {}; 1.520 + scroller.getPosition(pX, pY); 1.521 + scroller.getScrolledSize(sX, sY); 1.522 + let rect = target.getBoundingClientRect(); 1.523 + return { x: (sX.value > rect.width || pX.value != 0), 1.524 + y: (sY.value > rect.height || pY.value != 0) }; 1.525 + }, 1.526 + 1.527 + dragStart: function dragStart(cx, cy, target, scroller) { 1.528 + scroller.element.addEventListener("PanBegin", this._showScrollbars, false); 1.529 + }, 1.530 + 1.531 + dragStop: function dragStop(dx, dy, scroller) { 1.532 + scroller.element.removeEventListener("PanBegin", this._showScrollbars, false); 1.533 + return this.dragMove(dx, dy, scroller); 1.534 + }, 1.535 + 1.536 + dragMove: function dragMove(dx, dy, scroller) { 1.537 + if (scroller.getPosition) { 1.538 + try { 1.539 + let oldX = {}, oldY = {}; 1.540 + scroller.getPosition(oldX, oldY); 1.541 + 1.542 + scroller.scrollBy(dx, dy); 1.543 + 1.544 + let newX = {}, newY = {}; 1.545 + scroller.getPosition(newX, newY); 1.546 + 1.547 + return (newX.value != oldX.value) || (newY.value != oldY.value); 1.548 + 1.549 + } catch (e) { /* we have no time for whiny scrollers! */ } 1.550 + } 1.551 + 1.552 + return false; 1.553 + }, 1.554 + 1.555 + _showScrollbars: function _showScrollbars(aEvent) { 1.556 + let scrollbox = aEvent.target; 1.557 + scrollbox.setAttribute("panning", "true"); 1.558 + 1.559 + let hideScrollbars = function() { 1.560 + scrollbox.removeEventListener("PanFinished", hideScrollbars, false); 1.561 + scrollbox.removeEventListener("CancelTouchSequence", hideScrollbars, false); 1.562 + scrollbox.removeAttribute("panning"); 1.563 + } 1.564 + 1.565 + // Wait for panning to be completely finished before removing scrollbars 1.566 + scrollbox.addEventListener("PanFinished", hideScrollbars, false); 1.567 + scrollbox.addEventListener("CancelTouchSequence", hideScrollbars, false); 1.568 + } 1.569 + } 1.570 +}; 1.571 + 1.572 +/** 1.573 + * DragData handles processing drags on the screen, handling both 1.574 + * locking of movement on one axis, and click detection. 1.575 + */ 1.576 +function DragData() { 1.577 + this._domUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); 1.578 + this._lockRevertThreshold = Util.displayDPI * kAxisLockRevertThreshold; 1.579 + this.reset(); 1.580 +}; 1.581 + 1.582 +DragData.prototype = { 1.583 + reset: function reset() { 1.584 + this.dragging = false; 1.585 + this.sX = null; 1.586 + this.sY = null; 1.587 + this.locked = false; 1.588 + this.stayLocked = false; 1.589 + this.alwaysFreeDrag = false; 1.590 + this.lockedX = null; 1.591 + this.lockedY = null; 1.592 + this._originX = null; 1.593 + this._originY = null; 1.594 + this.prevPanX = null; 1.595 + this.prevPanY = null; 1.596 + this._isPan = false; 1.597 + }, 1.598 + 1.599 + /** Depending on drag data, locks sX,sY to X-axis or Y-axis of start position. */ 1.600 + _lockAxis: function _lockAxis(sX, sY) { 1.601 + if (this.locked) { 1.602 + if (this.lockedX !== null) 1.603 + sX = this.lockedX; 1.604 + else if (this.lockedY !== null) 1.605 + sY = this.lockedY; 1.606 + return [sX, sY]; 1.607 + } 1.608 + else { 1.609 + return [this._originX, this._originY]; 1.610 + } 1.611 + }, 1.612 + 1.613 + setMousePosition: function setMousePosition(aEvent) { 1.614 + this._mouseX = aEvent.clientX; 1.615 + this._mouseY = aEvent.clientY; 1.616 + }, 1.617 + 1.618 + setDragPosition: function setDragPosition(sX, sY) { 1.619 + // Check if drag is now a pan. 1.620 + if (!this._isPan) { 1.621 + this._isPan = ScrollUtils.isPan(new Point(this._originX, this._originY), new Point(sX, sY)); 1.622 + if (this._isPan) { 1.623 + this._resetActive(); 1.624 + } 1.625 + } 1.626 + 1.627 + // If now a pan, mark previous position where panning was. 1.628 + if (this._isPan) { 1.629 + let absX = Math.abs(this._originX - sX); 1.630 + let absY = Math.abs(this._originY - sY); 1.631 + 1.632 + if (absX > this._lockRevertThreshold || absY > this._lockRevertThreshold) 1.633 + this.stayLocked = true; 1.634 + 1.635 + // After the first lock, see if locking decision should be reverted. 1.636 + if (!this.stayLocked) { 1.637 + if (this.lockedX && absX > 3 * absY) 1.638 + this.lockedX = null; 1.639 + else if (this.lockedY && absY > 3 * absX) 1.640 + this.lockedY = null; 1.641 + } 1.642 + 1.643 + if (!this.locked) { 1.644 + // look at difference from origin coord to lock movement, but only 1.645 + // do it if initial movement is sufficient to detect intent 1.646 + 1.647 + // divide possibilty space into eight parts. Diagonals will allow 1.648 + // free movement, while moving towards a cardinal will lock that 1.649 + // axis. We pick a direction if you move more than twice as far 1.650 + // on one axis than another, which should be an angle of about 30 1.651 + // degrees from the axis 1.652 + 1.653 + if (absX > 2.5 * absY) 1.654 + this.lockedY = sY; 1.655 + else if (absY > absX) 1.656 + this.lockedX = sX; 1.657 + 1.658 + this.locked = true; 1.659 + } 1.660 + } 1.661 + 1.662 + // Never lock if the dragger requests it 1.663 + if (this.alwaysFreeDrag) { 1.664 + this.lockedY = null; 1.665 + this.lockedX = null; 1.666 + } 1.667 + 1.668 + // After pan lock, figure out previous panning position. Base it on last drag 1.669 + // position so there isn't a jump in panning. 1.670 + let [prevX, prevY] = this._lockAxis(this.sX, this.sY); 1.671 + this.prevPanX = prevX; 1.672 + this.prevPanY = prevY; 1.673 + 1.674 + this.sX = sX; 1.675 + this.sY = sY; 1.676 + }, 1.677 + 1.678 + setDragStart: function setDragStart(screenX, screenY, aDraggable) { 1.679 + this.sX = this._originX = screenX; 1.680 + this.sY = this._originY = screenY; 1.681 + this.dragging = true; 1.682 + 1.683 + // If the target area is pannable only in one direction lock it early 1.684 + // on the right axis 1.685 + this.lockedX = !aDraggable.x ? screenX : null; 1.686 + this.lockedY = !aDraggable.y ? screenY : null; 1.687 + this.stayLocked = this.lockedX || this.lockedY; 1.688 + this.locked = this.stayLocked; 1.689 + }, 1.690 + 1.691 + endDrag: function endDrag() { 1.692 + this._resetActive(); 1.693 + this.dragging = false; 1.694 + }, 1.695 + 1.696 + /** Returns true if drag should pan scrollboxes.*/ 1.697 + isPan: function isPan() { 1.698 + return this._isPan; 1.699 + }, 1.700 + 1.701 + /** Return true if drag should be parsed as a click. */ 1.702 + isClick: function isClick() { 1.703 + return !this._isPan; 1.704 + }, 1.705 + 1.706 + /** 1.707 + * Returns the screen position for a pan. This factors in axis locking. 1.708 + * @return Array of screen X and Y coordinates 1.709 + */ 1.710 + panPosition: function panPosition() { 1.711 + return this._lockAxis(this.sX, this.sY); 1.712 + }, 1.713 + 1.714 + /** dismiss the active state of the pan element */ 1.715 + _resetActive: function _resetActive() { 1.716 + let target = document.documentElement; 1.717 + // If the target is active, toggle (turn off) the active flag. Otherwise do nothing. 1.718 + if (this._domUtils.getContentState(target) & kStateActive) 1.719 + this._domUtils.setContentState(target, kStateActive); 1.720 + }, 1.721 + 1.722 + toString: function toString() { 1.723 + return '[DragData] { sX,sY=' + this.sX + ',' + this.sY + ', dragging=' + this.dragging + ' }'; 1.724 + } 1.725 +}; 1.726 + 1.727 + 1.728 +/** 1.729 + * KineticController - a class to take drag position data and use it 1.730 + * to do kinetic panning of a scrollable object. 1.731 + * 1.732 + * aPanBy is a function that will be called with the dx and dy 1.733 + * generated by the kinetic algorithm. It should return true if the 1.734 + * object was panned, false if there was no movement. 1.735 + * 1.736 + * There are two complicated things done here. One is calculating the 1.737 + * initial velocity of the movement based on user input. Two is 1.738 + * calculating the distance to move every frame. 1.739 + */ 1.740 +function KineticController(aPanBy, aEndCallback) { 1.741 + this._panBy = aPanBy; 1.742 + this._beforeEnd = aEndCallback; 1.743 + 1.744 + // These are used to calculate the position of the scroll panes during kinetic panning. Think of 1.745 + // these points as vectors that are added together and multiplied by scalars. 1.746 + this._position = new Point(0, 0); 1.747 + this._velocity = new Point(0, 0); 1.748 + this._acceleration = new Point(0, 0); 1.749 + this._time = 0; 1.750 + this._timeStart = 0; 1.751 + 1.752 + // How often do we change the position of the scroll pane? Too often and panning may jerk near 1.753 + // the end. Too little and panning will be choppy. In milliseconds. 1.754 + this._updateInterval = Services.prefs.getIntPref("browser.ui.kinetic.updateInterval"); 1.755 + // Constants that affect the "friction" of the scroll pane. 1.756 + this._exponentialC = Services.prefs.getIntPref("browser.ui.kinetic.exponentialC"); 1.757 + this._polynomialC = Services.prefs.getIntPref("browser.ui.kinetic.polynomialC") / 1000000; 1.758 + // Number of milliseconds that can contain a swipe. Movements earlier than this are disregarded. 1.759 + this._swipeLength = Services.prefs.getIntPref("browser.ui.kinetic.swipeLength"); 1.760 + 1.761 + this._reset(); 1.762 +} 1.763 + 1.764 +KineticController.prototype = { 1.765 + _reset: function _reset() { 1.766 + this._active = false; 1.767 + this._paused = false; 1.768 + this.momentumBuffer = []; 1.769 + this._velocity.set(0, 0); 1.770 + }, 1.771 + 1.772 + isActive: function isActive() { 1.773 + return this._active; 1.774 + }, 1.775 + 1.776 + _startTimer: function _startTimer() { 1.777 + let self = this; 1.778 + 1.779 + let lastp = this._position; // track last position vector because pan takes deltas 1.780 + let v0 = this._velocity; // initial velocity 1.781 + let a = this._acceleration; // acceleration 1.782 + let c = this._exponentialC; 1.783 + let p = new Point(0, 0); 1.784 + let dx, dy, t, realt; 1.785 + 1.786 + function calcP(v0, a, t) { 1.787 + // Important traits for this function: 1.788 + // p(t=0) is 0 1.789 + // p'(t=0) is v0 1.790 + // 1.791 + // We use exponential to get a smoother stop, but by itself exponential 1.792 + // is too smooth at the end. Adding a polynomial with the appropriate 1.793 + // weight helps to balance 1.794 + return v0 * Math.exp(-t / c) * -c + a * t * t + v0 * c; 1.795 + } 1.796 + 1.797 + this._calcV = function(v0, a, t) { 1.798 + return v0 * Math.exp(-t / c) + 2 * a * t; 1.799 + } 1.800 + 1.801 + let callback = { 1.802 + sample: function kineticHandleEvent(timeStamp) { 1.803 + // Someone called end() on us between timer intervals 1.804 + // or we are paused. 1.805 + if (!self.isActive() || self._paused) 1.806 + return; 1.807 + 1.808 + // To make animation end fast enough but to keep smoothness, average the ideal 1.809 + // time frame (smooth animation) with the actual time lapse (end fast enough). 1.810 + // Animation will never take longer than 2 times the ideal length of time. 1.811 + realt = timeStamp - self._initialTime; 1.812 + self._time += self._updateInterval; 1.813 + t = (self._time + realt) / 2; 1.814 + 1.815 + // Calculate new position. 1.816 + p.x = calcP(v0.x, a.x, t); 1.817 + p.y = calcP(v0.y, a.y, t); 1.818 + dx = Math.round(p.x - lastp.x); 1.819 + dy = Math.round(p.y - lastp.y); 1.820 + 1.821 + // Test to see if movement is finished for each component. 1.822 + if (dx * a.x > 0) { 1.823 + dx = 0; 1.824 + lastp.x = 0; 1.825 + v0.x = 0; 1.826 + a.x = 0; 1.827 + } 1.828 + // Symmetric to above case. 1.829 + if (dy * a.y > 0) { 1.830 + dy = 0; 1.831 + lastp.y = 0; 1.832 + v0.y = 0; 1.833 + a.y = 0; 1.834 + } 1.835 + 1.836 + if (v0.x == 0 && v0.y == 0) { 1.837 + self.end(); 1.838 + } else { 1.839 + let panStop = false; 1.840 + if (dx != 0 || dy != 0) { 1.841 + try { panStop = !self._panBy(-dx, -dy, true); } catch (e) {} 1.842 + lastp.add(dx, dy); 1.843 + } 1.844 + 1.845 + if (panStop) 1.846 + self.end(); 1.847 + else 1.848 + mozRequestAnimationFrame(this); 1.849 + } 1.850 + } 1.851 + }; 1.852 + 1.853 + this._active = true; 1.854 + this._paused = false; 1.855 + mozRequestAnimationFrame(callback); 1.856 + }, 1.857 + 1.858 + start: function start() { 1.859 + function sign(x) { 1.860 + return x ? ((x > 0) ? 1 : -1) : 0; 1.861 + } 1.862 + 1.863 + function clampFromZero(x, closerToZero, furtherFromZero) { 1.864 + if (x >= 0) 1.865 + return Math.max(closerToZero, Math.min(furtherFromZero, x)); 1.866 + return Math.min(-closerToZero, Math.max(-furtherFromZero, x)); 1.867 + } 1.868 + 1.869 + let mb = this.momentumBuffer; 1.870 + let mblen = this.momentumBuffer.length; 1.871 + 1.872 + let lastTime = mb[mblen - 1].t; 1.873 + let distanceX = 0; 1.874 + let distanceY = 0; 1.875 + let swipeLength = this._swipeLength; 1.876 + 1.877 + // determine speed based on recorded input 1.878 + let me; 1.879 + for (let i = 0; i < mblen; i++) { 1.880 + me = mb[i]; 1.881 + if (lastTime - me.t < swipeLength) { 1.882 + distanceX += me.dx; 1.883 + distanceY += me.dy; 1.884 + } 1.885 + } 1.886 + 1.887 + let currentVelocityX = 0; 1.888 + let currentVelocityY = 0; 1.889 + 1.890 + if (this.isActive()) { 1.891 + // If active, then we expect this._calcV to be defined. 1.892 + let currentTime = Date.now() - this._initialTime; 1.893 + currentVelocityX = Util.clamp(this._calcV(this._velocity.x, this._acceleration.x, currentTime), -kMaxVelocity, kMaxVelocity); 1.894 + currentVelocityY = Util.clamp(this._calcV(this._velocity.y, this._acceleration.y, currentTime), -kMaxVelocity, kMaxVelocity); 1.895 + } 1.896 + 1.897 + if (currentVelocityX * this._velocity.x <= 0) 1.898 + currentVelocityX = 0; 1.899 + if (currentVelocityY * this._velocity.y <= 0) 1.900 + currentVelocityY = 0; 1.901 + 1.902 + let swipeTime = Math.min(swipeLength, lastTime - mb[0].t); 1.903 + this._velocity.x = clampFromZero((distanceX / swipeTime) + currentVelocityX, Math.abs(currentVelocityX), kMaxVelocity); 1.904 + this._velocity.y = clampFromZero((distanceY / swipeTime) + currentVelocityY, Math.abs(currentVelocityY), kMaxVelocity); 1.905 + 1.906 + if (Math.abs(this._velocity.x) < kMinVelocity) 1.907 + this._velocity.x = 0; 1.908 + if (Math.abs(this._velocity.y) < kMinVelocity) 1.909 + this._velocity.y = 0; 1.910 + 1.911 + // Set acceleration vector to opposite signs of velocity 1.912 + this._acceleration.set(this._velocity.clone().map(sign).scale(-this._polynomialC)); 1.913 + 1.914 + this._position.set(0, 0); 1.915 + this._initialTime = mozAnimationStartTime; 1.916 + this._time = 0; 1.917 + this.momentumBuffer = []; 1.918 + 1.919 + if (!this.isActive() || this._paused) 1.920 + this._startTimer(); 1.921 + 1.922 + return true; 1.923 + }, 1.924 + 1.925 + pause: function pause() { 1.926 + this._paused = true; 1.927 + }, 1.928 + 1.929 + end: function end() { 1.930 + if (this.isActive()) { 1.931 + if (this._beforeEnd) 1.932 + this._beforeEnd(); 1.933 + this._reset(); 1.934 + } 1.935 + }, 1.936 + 1.937 + addData: function addData(dx, dy) { 1.938 + let mbLength = this.momentumBuffer.length; 1.939 + let now = Date.now(); 1.940 + 1.941 + if (this.isActive()) { 1.942 + // Stop active movement when dragging in other direction. 1.943 + if (dx * this._velocity.x < 0 || dy * this._velocity.y < 0) 1.944 + this.end(); 1.945 + } 1.946 + 1.947 + this.momentumBuffer.push({'t': now, 'dx' : dx, 'dy' : dy}); 1.948 + } 1.949 +}; 1.950 + 1.951 + 1.952 +/* 1.953 + * Simple gestures support 1.954 + */ 1.955 + 1.956 +var GestureModule = { 1.957 + _debugEvents: false, 1.958 + 1.959 + init: function init() { 1.960 + window.addEventListener("MozSwipeGesture", this, true); 1.961 + }, 1.962 + 1.963 + /* 1.964 + * Events 1.965 + * 1.966 + * Dispatch events based on the type of mouse gesture event. For now, make 1.967 + * sure to stop propagation of every gesture event so that web content cannot 1.968 + * receive gesture events. 1.969 + * 1.970 + * @param nsIDOMEvent information structure 1.971 + */ 1.972 + 1.973 + handleEvent: function handleEvent(aEvent) { 1.974 + try { 1.975 + aEvent.stopPropagation(); 1.976 + aEvent.preventDefault(); 1.977 + if (this._debugEvents) Util.dumpLn("GestureModule:", aEvent.type); 1.978 + switch (aEvent.type) { 1.979 + case "MozSwipeGesture": 1.980 + if (this._onSwipe(aEvent)) { 1.981 + let event = document.createEvent("Events"); 1.982 + event.initEvent("CancelTouchSequence", true, true); 1.983 + aEvent.target.dispatchEvent(event); 1.984 + } 1.985 + break; 1.986 + } 1.987 + } catch (e) { 1.988 + Util.dumpLn("Error while handling gesture event", aEvent.type, 1.989 + "\nPlease report error at:", e); 1.990 + Cu.reportError(e); 1.991 + } 1.992 + }, 1.993 + 1.994 + _onSwipe: function _onSwipe(aEvent) { 1.995 + switch (aEvent.direction) { 1.996 + case Ci.nsIDOMSimpleGestureEvent.DIRECTION_LEFT: 1.997 + return this._tryCommand("cmd_forward"); 1.998 + case Ci.nsIDOMSimpleGestureEvent.DIRECTION_RIGHT: 1.999 + return this._tryCommand("cmd_back"); 1.1000 + } 1.1001 + return false; 1.1002 + }, 1.1003 + 1.1004 + _tryCommand: function _tryCommand(aId) { 1.1005 + if (document.getElementById(aId).getAttribute("disabled") == "true") 1.1006 + return false; 1.1007 + CommandUpdater.doCommand(aId); 1.1008 + return true; 1.1009 + }, 1.1010 +}; 1.1011 + 1.1012 +/** 1.1013 + * Helper to track when the user is using a precise pointing device (pen/mouse) 1.1014 + * versus an imprecise one (touch). 1.1015 + */ 1.1016 +var InputSourceHelper = { 1.1017 + isPrecise: false, 1.1018 + 1.1019 + init: function ish_init() { 1.1020 + Services.obs.addObserver(this, "metro_precise_input", false); 1.1021 + Services.obs.addObserver(this, "metro_imprecise_input", false); 1.1022 + }, 1.1023 + 1.1024 + _precise: function () { 1.1025 + if (!this.isPrecise) { 1.1026 + this.isPrecise = true; 1.1027 + this._fire("MozPrecisePointer"); 1.1028 + } 1.1029 + }, 1.1030 + 1.1031 + _imprecise: function () { 1.1032 + if (this.isPrecise) { 1.1033 + this.isPrecise = false; 1.1034 + this._fire("MozImprecisePointer"); 1.1035 + } 1.1036 + }, 1.1037 + 1.1038 + observe: function BrowserUI_observe(aSubject, aTopic, aData) { 1.1039 + switch (aTopic) { 1.1040 + case "metro_precise_input": 1.1041 + this._precise(); 1.1042 + break; 1.1043 + case "metro_imprecise_input": 1.1044 + this._imprecise(); 1.1045 + break; 1.1046 + } 1.1047 + }, 1.1048 + 1.1049 + fireUpdate: function fireUpdate() { 1.1050 + if (this.isPrecise) { 1.1051 + this._fire("MozPrecisePointer"); 1.1052 + } else { 1.1053 + this._fire("MozImprecisePointer"); 1.1054 + } 1.1055 + }, 1.1056 + 1.1057 + _fire: function (name) { 1.1058 + let event = document.createEvent("Events"); 1.1059 + event.initEvent(name, true, true); 1.1060 + window.dispatchEvent(event); 1.1061 + } 1.1062 +};