|
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/. */ |
|
5 |
|
6 Components.utils.import("resource://gre/modules/Geometry.jsm"); |
|
7 |
|
8 /* |
|
9 * Drag scrolling related constants |
|
10 */ |
|
11 |
|
12 // maximum drag distance in inches while axis locking can still be reverted |
|
13 const kAxisLockRevertThreshold = 0.8; |
|
14 |
|
15 // Same as NS_EVENT_STATE_ACTIVE from mozilla/EventStates.h |
|
16 const kStateActive = 0x00000001; |
|
17 |
|
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; |
|
21 |
|
22 // Min/max velocity of kinetic panning. This is in pixels/millisecond. |
|
23 const kMinVelocity = 0.4; |
|
24 const kMaxVelocity = 6; |
|
25 |
|
26 /* |
|
27 * prefs |
|
28 */ |
|
29 |
|
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" |
|
39 |
|
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 */ |
|
70 |
|
71 var TouchModule = { |
|
72 _debugEvents: false, |
|
73 _isCancelled: false, |
|
74 _isCancellable: false, |
|
75 |
|
76 init: function init() { |
|
77 this._dragData = new DragData(); |
|
78 |
|
79 this._dragger = null; |
|
80 |
|
81 this._targetScrollbox = null; |
|
82 this._targetScrollInterface = null; |
|
83 |
|
84 this._kinetic = new KineticController(this._dragBy.bind(this), |
|
85 this._kineticStop.bind(this)); |
|
86 |
|
87 // capture phase events |
|
88 window.addEventListener("CancelTouchSequence", this, true); |
|
89 window.addEventListener("keydown", this, true); |
|
90 window.addEventListener("MozMouseHittest", this, true); |
|
91 |
|
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); |
|
97 |
|
98 Services.obs.addObserver(this, "Gesture:SingleTap", false); |
|
99 Services.obs.addObserver(this, "Gesture:DoubleTap", false); |
|
100 }, |
|
101 |
|
102 /* |
|
103 * Events |
|
104 */ |
|
105 |
|
106 handleEvent: function handleEvent(aEvent) { |
|
107 switch (aEvent.type) { |
|
108 case "contextmenu": |
|
109 this._onContextMenu(aEvent); |
|
110 break; |
|
111 |
|
112 case "CancelTouchSequence": |
|
113 this.cancelPending(); |
|
114 break; |
|
115 |
|
116 default: { |
|
117 if (this._debugEvents) { |
|
118 if (aEvent.type != "touchmove") |
|
119 Util.dumpLn("TouchModule:", aEvent.type, aEvent.target); |
|
120 } |
|
121 |
|
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 }, |
|
155 |
|
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 }, |
|
182 |
|
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 }, |
|
191 |
|
192 |
|
193 sample: function sample(aTimeStamp) { |
|
194 this._waitingForPaint = false; |
|
195 }, |
|
196 |
|
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(); |
|
204 |
|
205 // Kinetic panning may have already been active or drag stop above may have |
|
206 // made kinetic panning active. |
|
207 this._kinetic.end(); |
|
208 |
|
209 this._targetScrollbox = null; |
|
210 this._targetScrollInterface = null; |
|
211 }, |
|
212 |
|
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 }, |
|
220 |
|
221 /** Begin possible pan and send tap down event. */ |
|
222 _onTouchStart: function _onTouchStart(aEvent) { |
|
223 if (aEvent.touches.length > 1) |
|
224 return; |
|
225 |
|
226 this._isCancelled = false; |
|
227 this._isCancellable = true; |
|
228 |
|
229 if (aEvent.defaultPrevented) { |
|
230 this._isCancelled = true; |
|
231 return; |
|
232 } |
|
233 |
|
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; |
|
242 |
|
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); |
|
247 |
|
248 // stop kinetic panning if targetScrollbox has changed |
|
249 if (this._kinetic.isActive() && this._dragger != dragger) |
|
250 this._kinetic.end(); |
|
251 |
|
252 this._targetScrollbox = targetScrollInterface ? targetScrollInterface.element : targetScrollbox; |
|
253 this._targetScrollInterface = targetScrollInterface; |
|
254 |
|
255 if (!this._targetScrollbox) { |
|
256 return; |
|
257 } |
|
258 |
|
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 } |
|
265 |
|
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 }, |
|
278 |
|
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; |
|
283 |
|
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); |
|
288 |
|
289 let dragData = this._dragData; |
|
290 this._doDragStop(); |
|
291 }, |
|
292 |
|
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; |
|
299 |
|
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 } |
|
307 |
|
308 if (this._isCancelled) |
|
309 return; |
|
310 |
|
311 let touch = aEvent.changedTouches[0]; |
|
312 if (!this._targetScrollbox) { |
|
313 return; |
|
314 } |
|
315 |
|
316 let dragData = this._dragData; |
|
317 |
|
318 if (dragData.dragging) { |
|
319 let oldIsPan = dragData.isPan(); |
|
320 dragData.setDragPosition(touch.screenX, touch.screenY); |
|
321 dragData.setMousePosition(touch); |
|
322 |
|
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; |
|
335 |
|
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); |
|
339 |
|
340 // dragBy will reset dX and dY values to 0 |
|
341 this._dragBy(this.dX, this.dY); |
|
342 |
|
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); |
|
348 |
|
349 Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:PanBegin", {}); |
|
350 } |
|
351 } |
|
352 } |
|
353 }, |
|
354 |
|
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 }, |
|
368 |
|
369 /** Finish a drag. */ |
|
370 _doDragStop: function _doDragStop() { |
|
371 let dragData = this._dragData; |
|
372 if (!dragData.dragging) |
|
373 return; |
|
374 |
|
375 dragData.endDrag(); |
|
376 |
|
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. |
|
380 |
|
381 if (dragData.isPan()) { |
|
382 if (Date.now() - this._dragStartTime > kStopKineticPanOnDragTimeout) |
|
383 this._kinetic._velocity.set(0, 0); |
|
384 |
|
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 }, |
|
399 |
|
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(); |
|
422 |
|
423 return dragged; |
|
424 }, |
|
425 |
|
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; |
|
435 |
|
436 let event = document.createEvent("Events"); |
|
437 event.initEvent("PanFinished", true, false); |
|
438 this._targetScrollbox.dispatchEvent(event); |
|
439 } |
|
440 }, |
|
441 |
|
442 toString: function toString() { |
|
443 return '[TouchModule] {' |
|
444 + '\n\tdragData=' + this._dragData + ', ' |
|
445 + 'dragger=' + this._dragger + ', ' |
|
446 + '\n\ttargetScroller=' + this._targetScrollInterface + '}'; |
|
447 }, |
|
448 }; |
|
449 |
|
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 }, |
|
457 |
|
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; |
|
469 |
|
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 }, |
|
501 |
|
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 }, |
|
507 |
|
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 }, |
|
523 |
|
524 dragStart: function dragStart(cx, cy, target, scroller) { |
|
525 scroller.element.addEventListener("PanBegin", this._showScrollbars, false); |
|
526 }, |
|
527 |
|
528 dragStop: function dragStop(dx, dy, scroller) { |
|
529 scroller.element.removeEventListener("PanBegin", this._showScrollbars, false); |
|
530 return this.dragMove(dx, dy, scroller); |
|
531 }, |
|
532 |
|
533 dragMove: function dragMove(dx, dy, scroller) { |
|
534 if (scroller.getPosition) { |
|
535 try { |
|
536 let oldX = {}, oldY = {}; |
|
537 scroller.getPosition(oldX, oldY); |
|
538 |
|
539 scroller.scrollBy(dx, dy); |
|
540 |
|
541 let newX = {}, newY = {}; |
|
542 scroller.getPosition(newX, newY); |
|
543 |
|
544 return (newX.value != oldX.value) || (newY.value != oldY.value); |
|
545 |
|
546 } catch (e) { /* we have no time for whiny scrollers! */ } |
|
547 } |
|
548 |
|
549 return false; |
|
550 }, |
|
551 |
|
552 _showScrollbars: function _showScrollbars(aEvent) { |
|
553 let scrollbox = aEvent.target; |
|
554 scrollbox.setAttribute("panning", "true"); |
|
555 |
|
556 let hideScrollbars = function() { |
|
557 scrollbox.removeEventListener("PanFinished", hideScrollbars, false); |
|
558 scrollbox.removeEventListener("CancelTouchSequence", hideScrollbars, false); |
|
559 scrollbox.removeAttribute("panning"); |
|
560 } |
|
561 |
|
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 }; |
|
568 |
|
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 }; |
|
578 |
|
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 }, |
|
595 |
|
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 }, |
|
609 |
|
610 setMousePosition: function setMousePosition(aEvent) { |
|
611 this._mouseX = aEvent.clientX; |
|
612 this._mouseY = aEvent.clientY; |
|
613 }, |
|
614 |
|
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 } |
|
623 |
|
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); |
|
628 |
|
629 if (absX > this._lockRevertThreshold || absY > this._lockRevertThreshold) |
|
630 this.stayLocked = true; |
|
631 |
|
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 } |
|
639 |
|
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 |
|
643 |
|
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 |
|
649 |
|
650 if (absX > 2.5 * absY) |
|
651 this.lockedY = sY; |
|
652 else if (absY > absX) |
|
653 this.lockedX = sX; |
|
654 |
|
655 this.locked = true; |
|
656 } |
|
657 } |
|
658 |
|
659 // Never lock if the dragger requests it |
|
660 if (this.alwaysFreeDrag) { |
|
661 this.lockedY = null; |
|
662 this.lockedX = null; |
|
663 } |
|
664 |
|
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; |
|
670 |
|
671 this.sX = sX; |
|
672 this.sY = sY; |
|
673 }, |
|
674 |
|
675 setDragStart: function setDragStart(screenX, screenY, aDraggable) { |
|
676 this.sX = this._originX = screenX; |
|
677 this.sY = this._originY = screenY; |
|
678 this.dragging = true; |
|
679 |
|
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 }, |
|
687 |
|
688 endDrag: function endDrag() { |
|
689 this._resetActive(); |
|
690 this.dragging = false; |
|
691 }, |
|
692 |
|
693 /** Returns true if drag should pan scrollboxes.*/ |
|
694 isPan: function isPan() { |
|
695 return this._isPan; |
|
696 }, |
|
697 |
|
698 /** Return true if drag should be parsed as a click. */ |
|
699 isClick: function isClick() { |
|
700 return !this._isPan; |
|
701 }, |
|
702 |
|
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 }, |
|
710 |
|
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 }, |
|
718 |
|
719 toString: function toString() { |
|
720 return '[DragData] { sX,sY=' + this.sX + ',' + this.sY + ', dragging=' + this.dragging + ' }'; |
|
721 } |
|
722 }; |
|
723 |
|
724 |
|
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; |
|
740 |
|
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; |
|
748 |
|
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"); |
|
757 |
|
758 this._reset(); |
|
759 } |
|
760 |
|
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 }, |
|
768 |
|
769 isActive: function isActive() { |
|
770 return this._active; |
|
771 }, |
|
772 |
|
773 _startTimer: function _startTimer() { |
|
774 let self = this; |
|
775 |
|
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; |
|
782 |
|
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 } |
|
793 |
|
794 this._calcV = function(v0, a, t) { |
|
795 return v0 * Math.exp(-t / c) + 2 * a * t; |
|
796 } |
|
797 |
|
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; |
|
804 |
|
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; |
|
811 |
|
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); |
|
817 |
|
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 } |
|
832 |
|
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 } |
|
841 |
|
842 if (panStop) |
|
843 self.end(); |
|
844 else |
|
845 mozRequestAnimationFrame(this); |
|
846 } |
|
847 } |
|
848 }; |
|
849 |
|
850 this._active = true; |
|
851 this._paused = false; |
|
852 mozRequestAnimationFrame(callback); |
|
853 }, |
|
854 |
|
855 start: function start() { |
|
856 function sign(x) { |
|
857 return x ? ((x > 0) ? 1 : -1) : 0; |
|
858 } |
|
859 |
|
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 } |
|
865 |
|
866 let mb = this.momentumBuffer; |
|
867 let mblen = this.momentumBuffer.length; |
|
868 |
|
869 let lastTime = mb[mblen - 1].t; |
|
870 let distanceX = 0; |
|
871 let distanceY = 0; |
|
872 let swipeLength = this._swipeLength; |
|
873 |
|
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 } |
|
883 |
|
884 let currentVelocityX = 0; |
|
885 let currentVelocityY = 0; |
|
886 |
|
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 } |
|
893 |
|
894 if (currentVelocityX * this._velocity.x <= 0) |
|
895 currentVelocityX = 0; |
|
896 if (currentVelocityY * this._velocity.y <= 0) |
|
897 currentVelocityY = 0; |
|
898 |
|
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); |
|
902 |
|
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; |
|
907 |
|
908 // Set acceleration vector to opposite signs of velocity |
|
909 this._acceleration.set(this._velocity.clone().map(sign).scale(-this._polynomialC)); |
|
910 |
|
911 this._position.set(0, 0); |
|
912 this._initialTime = mozAnimationStartTime; |
|
913 this._time = 0; |
|
914 this.momentumBuffer = []; |
|
915 |
|
916 if (!this.isActive() || this._paused) |
|
917 this._startTimer(); |
|
918 |
|
919 return true; |
|
920 }, |
|
921 |
|
922 pause: function pause() { |
|
923 this._paused = true; |
|
924 }, |
|
925 |
|
926 end: function end() { |
|
927 if (this.isActive()) { |
|
928 if (this._beforeEnd) |
|
929 this._beforeEnd(); |
|
930 this._reset(); |
|
931 } |
|
932 }, |
|
933 |
|
934 addData: function addData(dx, dy) { |
|
935 let mbLength = this.momentumBuffer.length; |
|
936 let now = Date.now(); |
|
937 |
|
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 } |
|
943 |
|
944 this.momentumBuffer.push({'t': now, 'dx' : dx, 'dy' : dy}); |
|
945 } |
|
946 }; |
|
947 |
|
948 |
|
949 /* |
|
950 * Simple gestures support |
|
951 */ |
|
952 |
|
953 var GestureModule = { |
|
954 _debugEvents: false, |
|
955 |
|
956 init: function init() { |
|
957 window.addEventListener("MozSwipeGesture", this, true); |
|
958 }, |
|
959 |
|
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 */ |
|
969 |
|
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 }, |
|
990 |
|
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 }, |
|
1000 |
|
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 }; |
|
1008 |
|
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, |
|
1015 |
|
1016 init: function ish_init() { |
|
1017 Services.obs.addObserver(this, "metro_precise_input", false); |
|
1018 Services.obs.addObserver(this, "metro_imprecise_input", false); |
|
1019 }, |
|
1020 |
|
1021 _precise: function () { |
|
1022 if (!this.isPrecise) { |
|
1023 this.isPrecise = true; |
|
1024 this._fire("MozPrecisePointer"); |
|
1025 } |
|
1026 }, |
|
1027 |
|
1028 _imprecise: function () { |
|
1029 if (this.isPrecise) { |
|
1030 this.isPrecise = false; |
|
1031 this._fire("MozImprecisePointer"); |
|
1032 } |
|
1033 }, |
|
1034 |
|
1035 observe: function BrowserUI_observe(aSubject, aTopic, aData) { |
|
1036 switch (aTopic) { |
|
1037 case "metro_precise_input": |
|
1038 this._precise(); |
|
1039 break; |
|
1040 case "metro_imprecise_input": |
|
1041 this._imprecise(); |
|
1042 break; |
|
1043 } |
|
1044 }, |
|
1045 |
|
1046 fireUpdate: function fireUpdate() { |
|
1047 if (this.isPrecise) { |
|
1048 this._fire("MozPrecisePointer"); |
|
1049 } else { |
|
1050 this._fire("MozImprecisePointer"); |
|
1051 } |
|
1052 }, |
|
1053 |
|
1054 _fire: function (name) { |
|
1055 let event = document.createEvent("Events"); |
|
1056 event.initEvent(name, true, true); |
|
1057 window.dispatchEvent(event); |
|
1058 } |
|
1059 }; |