|
1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 sw=2 sts=2 et: */ |
|
3 |
|
4 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
5 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
6 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
7 |
|
8 "use strict"; |
|
9 dump("############################### browserElementPanning.js loaded\n"); |
|
10 |
|
11 let { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; |
|
12 Cu.import("resource://gre/modules/Services.jsm"); |
|
13 Cu.import("resource://gre/modules/Geometry.jsm"); |
|
14 |
|
15 var global = this; |
|
16 |
|
17 const kObservedEvents = [ |
|
18 "BEC:ShownModalPrompt", |
|
19 "Activity:Success", |
|
20 "Activity:Error" |
|
21 ]; |
|
22 |
|
23 const ContentPanning = { |
|
24 // Are we listening to touch or mouse events? |
|
25 watchedEventsType: '', |
|
26 |
|
27 // Are mouse events being delivered to this content along with touch |
|
28 // events, in violation of spec? |
|
29 hybridEvents: false, |
|
30 |
|
31 init: function cp_init() { |
|
32 // If APZ is enabled, we do active element handling in C++ |
|
33 // (see widget/xpwidgets/ActiveElementManager.h), and panning |
|
34 // itself in APZ, so we don't need to handle any touch events here. |
|
35 if (docShell.asyncPanZoomEnabled === false) { |
|
36 this._setupListenersForPanning(); |
|
37 } |
|
38 |
|
39 addEventListener("unload", |
|
40 this._unloadHandler.bind(this), |
|
41 /* useCapture = */ false, |
|
42 /* wantsUntrusted = */ false); |
|
43 |
|
44 addMessageListener("Viewport:Change", this._recvViewportChange.bind(this)); |
|
45 addMessageListener("Gesture:DoubleTap", this._recvDoubleTap.bind(this)); |
|
46 addEventListener("visibilitychange", this._handleVisibilityChange.bind(this)); |
|
47 kObservedEvents.forEach((topic) => { |
|
48 Services.obs.addObserver(this, topic, false); |
|
49 }); |
|
50 }, |
|
51 |
|
52 _setupListenersForPanning: function cp_setupListenersForPanning() { |
|
53 var events; |
|
54 try { |
|
55 content.document.createEvent('TouchEvent'); |
|
56 events = ['touchstart', 'touchend', 'touchmove']; |
|
57 this.watchedEventsType = 'touch'; |
|
58 #ifdef MOZ_WIDGET_GONK |
|
59 // The gonk widget backend does not deliver mouse events per |
|
60 // spec. Third-party content isn't exposed to this behavior, |
|
61 // but that behavior creates some extra work for us here. |
|
62 let appInfo = Cc["@mozilla.org/xre/app-info;1"]; |
|
63 let isParentProcess = |
|
64 !appInfo || appInfo.getService(Ci.nsIXULRuntime) |
|
65 .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; |
|
66 this.hybridEvents = isParentProcess; |
|
67 #endif |
|
68 } catch(e) { |
|
69 // Touch events aren't supported, so fall back on mouse. |
|
70 events = ['mousedown', 'mouseup', 'mousemove']; |
|
71 this.watchedEventsType = 'mouse'; |
|
72 } |
|
73 |
|
74 let els = Cc["@mozilla.org/eventlistenerservice;1"] |
|
75 .getService(Ci.nsIEventListenerService); |
|
76 |
|
77 events.forEach(function(type) { |
|
78 // Using the system group for mouse/touch events to avoid |
|
79 // missing events if .stopPropagation() has been called. |
|
80 els.addSystemEventListener(global, type, |
|
81 this.handleEvent.bind(this), |
|
82 /* useCapture = */ false); |
|
83 }.bind(this)); |
|
84 }, |
|
85 |
|
86 handleEvent: function cp_handleEvent(evt) { |
|
87 // Ignore events targeting a <iframe mozbrowser> since those will be |
|
88 // handle by the BrowserElementPanning.js instance of it. |
|
89 if (evt.target instanceof Ci.nsIMozBrowserFrame) { |
|
90 return; |
|
91 } |
|
92 |
|
93 if (evt.defaultPrevented || evt.multipleActionsPrevented) { |
|
94 // clean up panning state even if touchend/mouseup has been preventDefault. |
|
95 if(evt.type === 'touchend' || evt.type === 'mouseup') { |
|
96 if (this.dragging && |
|
97 (this.watchedEventsType === 'mouse' || |
|
98 this.findPrimaryPointer(evt.changedTouches))) { |
|
99 this._finishPanning(); |
|
100 } |
|
101 } |
|
102 return; |
|
103 } |
|
104 |
|
105 switch (evt.type) { |
|
106 case 'mousedown': |
|
107 case 'touchstart': |
|
108 this.onTouchStart(evt); |
|
109 break; |
|
110 case 'mousemove': |
|
111 case 'touchmove': |
|
112 this.onTouchMove(evt); |
|
113 break; |
|
114 case 'mouseup': |
|
115 case 'touchend': |
|
116 this.onTouchEnd(evt); |
|
117 break; |
|
118 case 'click': |
|
119 evt.stopPropagation(); |
|
120 evt.preventDefault(); |
|
121 |
|
122 let target = evt.target; |
|
123 let view = target.ownerDocument ? target.ownerDocument.defaultView |
|
124 : target; |
|
125 view.removeEventListener('click', this, true, true); |
|
126 break; |
|
127 } |
|
128 }, |
|
129 |
|
130 observe: function cp_observe(subject, topic, data) { |
|
131 this._resetHover(); |
|
132 }, |
|
133 |
|
134 position: new Point(0 , 0), |
|
135 |
|
136 findPrimaryPointer: function cp_findPrimaryPointer(touches) { |
|
137 if (!('primaryPointerId' in this)) |
|
138 return null; |
|
139 |
|
140 for (let i = 0; i < touches.length; i++) { |
|
141 if (touches[i].identifier === this.primaryPointerId) { |
|
142 return touches[i]; |
|
143 } |
|
144 } |
|
145 return null; |
|
146 }, |
|
147 |
|
148 onTouchStart: function cp_onTouchStart(evt) { |
|
149 let screenX, screenY; |
|
150 if (this.watchedEventsType == 'touch') { |
|
151 if ('primaryPointerId' in this || evt.touches.length >= 2) { |
|
152 this._resetActive(); |
|
153 return; |
|
154 } |
|
155 |
|
156 let firstTouch = evt.changedTouches[0]; |
|
157 this.primaryPointerId = firstTouch.identifier; |
|
158 this.pointerDownTarget = firstTouch.target; |
|
159 screenX = firstTouch.screenX; |
|
160 screenY = firstTouch.screenY; |
|
161 } else { |
|
162 this.pointerDownTarget = evt.target; |
|
163 screenX = evt.screenX; |
|
164 screenY = evt.screenY; |
|
165 } |
|
166 this.dragging = true; |
|
167 this.panning = false; |
|
168 |
|
169 let oldTarget = this.target; |
|
170 [this.target, this.scrollCallback] = this.getPannable(this.pointerDownTarget); |
|
171 |
|
172 // If we have a pointer down target, we may need to fill in for EventStateManager |
|
173 // in setting the active state on the target element. Set a timer to |
|
174 // ensure the pointer-down target is active. (If it's already |
|
175 // active, the timer is a no-op.) |
|
176 if (this.pointerDownTarget !== null) { |
|
177 // If there's no possibility this is a drag/pan, activate now. |
|
178 // Otherwise wait a little bit to see if the gesture isn't a |
|
179 // tap. |
|
180 if (this.target === null) { |
|
181 this.notify(this._activationTimer); |
|
182 } else { |
|
183 this._activationTimer.initWithCallback(this, |
|
184 this._activationDelayMs, |
|
185 Ci.nsITimer.TYPE_ONE_SHOT); |
|
186 } |
|
187 } |
|
188 |
|
189 // If there is a pan animation running (from a previous pan gesture) and |
|
190 // the user touch back the screen, stop this animation immediatly and |
|
191 // prevent the possible click action if the touch happens on the same |
|
192 // target. |
|
193 this.preventNextClick = false; |
|
194 if (KineticPanning.active) { |
|
195 KineticPanning.stop(); |
|
196 |
|
197 if (oldTarget && oldTarget == this.target) |
|
198 this.preventNextClick = true; |
|
199 } |
|
200 |
|
201 this.position.set(screenX, screenY); |
|
202 KineticPanning.reset(); |
|
203 KineticPanning.record(new Point(0, 0), evt.timeStamp); |
|
204 |
|
205 // We prevent start events to avoid sending a focus event at the end of this |
|
206 // touch series. See bug 889717. |
|
207 if ((this.panning || this.preventNextClick)) { |
|
208 evt.preventDefault(); |
|
209 } |
|
210 }, |
|
211 |
|
212 onTouchEnd: function cp_onTouchEnd(evt) { |
|
213 let touch = null; |
|
214 if (!this.dragging || |
|
215 (this.watchedEventsType == 'touch' && |
|
216 !(touch = this.findPrimaryPointer(evt.changedTouches)))) { |
|
217 return; |
|
218 } |
|
219 |
|
220 // !isPan() and evt.detail should always give the same answer here |
|
221 // since they use the same heuristics, but use the native gecko |
|
222 // computation when possible. |
|
223 // |
|
224 // NB: when we're using touch events, then !KineticPanning.isPan() |
|
225 // => this.panning, so we'll never attempt to block the click |
|
226 // event. That's OK however, because we won't fire a synthetic |
|
227 // click when we're using touch events and this touch series |
|
228 // wasn't a "tap" gesture. |
|
229 let click = (this.watchedEventsType == 'mouse') ? |
|
230 evt.detail : !KineticPanning.isPan(); |
|
231 // Additionally, if we're seeing non-compliant hybrid events, a |
|
232 // "real" click will be generated if we started and ended on the |
|
233 // same element. |
|
234 if (this.hybridEvents) { |
|
235 let target = |
|
236 content.document.elementFromPoint(touch.clientX, touch.clientY); |
|
237 click |= (target === this.pointerDownTarget); |
|
238 } |
|
239 |
|
240 if (this.target && click && (this.panning || this.preventNextClick)) { |
|
241 if (this.hybridEvents) { |
|
242 let target = this.target; |
|
243 let view = target.ownerDocument ? target.ownerDocument.defaultView |
|
244 : target; |
|
245 view.addEventListener('click', this, true, true); |
|
246 } else { |
|
247 // We prevent end events to avoid sending a focus event. See bug 889717. |
|
248 evt.preventDefault(); |
|
249 } |
|
250 } else if (this.target && click && !this.panning) { |
|
251 this.notify(this._activationTimer); |
|
252 } |
|
253 |
|
254 this._finishPanning(); |
|
255 |
|
256 // Now that we're done, avoid entraining the thing we just panned. |
|
257 this.pointerDownTarget = null; |
|
258 }, |
|
259 |
|
260 onTouchMove: function cp_onTouchMove(evt) { |
|
261 if (!this.dragging) |
|
262 return; |
|
263 |
|
264 let screenX, screenY; |
|
265 if (this.watchedEventsType == 'touch') { |
|
266 let primaryTouch = this.findPrimaryPointer(evt.changedTouches); |
|
267 if (evt.touches.length > 1 || !primaryTouch) |
|
268 return; |
|
269 screenX = primaryTouch.screenX; |
|
270 screenY = primaryTouch.screenY; |
|
271 } else { |
|
272 screenX = evt.screenX; |
|
273 screenY = evt.screenY; |
|
274 } |
|
275 |
|
276 let current = this.position; |
|
277 let delta = new Point(screenX - current.x, screenY - current.y); |
|
278 current.set(screenX, screenY); |
|
279 |
|
280 KineticPanning.record(delta, evt.timeStamp); |
|
281 |
|
282 let isPan = KineticPanning.isPan(); |
|
283 |
|
284 // If we've detected a pan gesture, cancel the active state of the |
|
285 // current target. |
|
286 if (!this.panning && isPan) { |
|
287 this._resetActive(); |
|
288 } |
|
289 |
|
290 // There's no possibility of us panning anything. |
|
291 if (!this.scrollCallback) { |
|
292 return; |
|
293 } |
|
294 |
|
295 // Scroll manually. |
|
296 this.scrollCallback(delta.scale(-1)); |
|
297 |
|
298 if (!this.panning && isPan) { |
|
299 this.panning = true; |
|
300 this._activationTimer.cancel(); |
|
301 } |
|
302 |
|
303 if (this.panning) { |
|
304 // Only do this when we're actually executing a pan gesture. |
|
305 // Otherwise synthetic mouse events will be canceled. |
|
306 evt.stopPropagation(); |
|
307 evt.preventDefault(); |
|
308 } |
|
309 }, |
|
310 |
|
311 // nsITimerCallback |
|
312 notify: function cp_notify(timer) { |
|
313 this._setActive(this.pointerDownTarget); |
|
314 }, |
|
315 |
|
316 onKineticBegin: function cp_onKineticBegin(evt) { |
|
317 }, |
|
318 |
|
319 onKineticPan: function cp_onKineticPan(delta) { |
|
320 return !this.scrollCallback(delta); |
|
321 }, |
|
322 |
|
323 onKineticEnd: function cp_onKineticEnd() { |
|
324 if (!this.dragging) |
|
325 this.scrollCallback = null; |
|
326 }, |
|
327 |
|
328 getPannable: function cp_getPannable(node) { |
|
329 let pannableNode = this._findPannable(node); |
|
330 if (pannableNode) { |
|
331 return [pannableNode, this._generateCallback(pannableNode)]; |
|
332 } |
|
333 |
|
334 return [null, null]; |
|
335 }, |
|
336 |
|
337 _findPannable: function cp_findPannable(node) { |
|
338 if (!(node instanceof Ci.nsIDOMHTMLElement) || node.tagName == 'HTML') { |
|
339 return null; |
|
340 } |
|
341 |
|
342 let nodeContent = node.ownerDocument.defaultView; |
|
343 while (!(node instanceof Ci.nsIDOMHTMLBodyElement)) { |
|
344 let style = nodeContent.getComputedStyle(node, null); |
|
345 |
|
346 let overflow = [style.getPropertyValue('overflow'), |
|
347 style.getPropertyValue('overflow-x'), |
|
348 style.getPropertyValue('overflow-y')]; |
|
349 |
|
350 let rect = node.getBoundingClientRect(); |
|
351 let isAuto = (overflow.indexOf('auto') != -1 && |
|
352 (rect.height < node.scrollHeight || |
|
353 rect.width < node.scrollWidth)); |
|
354 |
|
355 let isScroll = (overflow.indexOf('scroll') != -1); |
|
356 |
|
357 let isScrollableTextarea = (node.tagName == 'TEXTAREA' && |
|
358 (node.scrollHeight > node.clientHeight || |
|
359 node.scrollWidth > node.clientWidth || |
|
360 ('scrollLeftMax' in node && node.scrollLeftMax > 0) || |
|
361 ('scrollTopMax' in node && node.scrollTopMax > 0))); |
|
362 if (isScroll || isAuto || isScrollableTextarea) { |
|
363 return node; |
|
364 } |
|
365 |
|
366 node = node.parentNode; |
|
367 } |
|
368 |
|
369 if (nodeContent.scrollMaxX || nodeContent.scrollMaxY) { |
|
370 return nodeContent; |
|
371 } |
|
372 |
|
373 if (nodeContent.frameElement) { |
|
374 return this._findPannable(nodeContent.frameElement); |
|
375 } |
|
376 |
|
377 return null; |
|
378 }, |
|
379 |
|
380 _generateCallback: function cp_generateCallback(root) { |
|
381 let firstScroll = true; |
|
382 let target; |
|
383 let current; |
|
384 let win, doc, htmlNode, bodyNode; |
|
385 |
|
386 function doScroll(node, delta) { |
|
387 if (node instanceof Ci.nsIDOMHTMLElement) { |
|
388 return node.scrollByNoFlush(delta.x, delta.y); |
|
389 } else if (node instanceof Ci.nsIDOMWindow) { |
|
390 win = node; |
|
391 doc = win.document; |
|
392 |
|
393 // "overflow:hidden" on either the <html> or the <body> node should |
|
394 // prevent the user from scrolling the root viewport. |
|
395 if (doc instanceof Ci.nsIDOMHTMLDocument) { |
|
396 htmlNode = doc.documentElement; |
|
397 bodyNode = doc.body; |
|
398 if (win.getComputedStyle(htmlNode, null).overflowX == "hidden" || |
|
399 win.getComputedStyle(bodyNode, null).overflowX == "hidden") { |
|
400 delta.x = 0; |
|
401 } |
|
402 if (win.getComputedStyle(htmlNode, null).overflowY == "hidden" || |
|
403 win.getComputedStyle(bodyNode, null).overflowY == "hidden") { |
|
404 delta.y = 0; |
|
405 } |
|
406 } |
|
407 let oldX = node.scrollX; |
|
408 let oldY = node.scrollY; |
|
409 node.scrollBy(delta.x, delta.y); |
|
410 return (node.scrollX != oldX || node.scrollY != oldY); |
|
411 } |
|
412 // If we get here, |node| isn't an HTML element and it's not a window, |
|
413 // but findPannable apparently thought it was scrollable... What is it? |
|
414 return false; |
|
415 } |
|
416 |
|
417 function targetParent(node) { |
|
418 return node.parentNode || node.frameElement || null; |
|
419 } |
|
420 |
|
421 function scroll(delta) { |
|
422 current = root; |
|
423 firstScroll = true; |
|
424 while (current) { |
|
425 if (doScroll(current, delta)) { |
|
426 firstScroll = false; |
|
427 return true; |
|
428 } |
|
429 |
|
430 // TODO The current code looks for possible scrolling regions only if |
|
431 // this is the first scroll action but this should be more dynamic. |
|
432 if (!firstScroll) { |
|
433 return false; |
|
434 } |
|
435 |
|
436 current = ContentPanning._findPannable(targetParent(current)); |
|
437 } |
|
438 |
|
439 // There is nothing scrollable here. |
|
440 return false; |
|
441 } |
|
442 return scroll; |
|
443 }, |
|
444 |
|
445 get _domUtils() { |
|
446 delete this._domUtils; |
|
447 return this._domUtils = Cc['@mozilla.org/inspector/dom-utils;1'] |
|
448 .getService(Ci.inIDOMUtils); |
|
449 }, |
|
450 |
|
451 get _activationTimer() { |
|
452 delete this._activationTimer; |
|
453 return this._activationTimer = Cc["@mozilla.org/timer;1"] |
|
454 .createInstance(Ci.nsITimer); |
|
455 }, |
|
456 |
|
457 get _activationDelayMs() { |
|
458 let delay = Services.prefs.getIntPref('ui.touch_activation.delay_ms'); |
|
459 delete this._activationDelayMs; |
|
460 return this._activationDelayMs = delay; |
|
461 }, |
|
462 |
|
463 _resetActive: function cp_resetActive() { |
|
464 let elt = this.pointerDownTarget || this.target; |
|
465 let root = elt.ownerDocument || elt.document; |
|
466 this._setActive(root.documentElement); |
|
467 }, |
|
468 |
|
469 _resetHover: function cp_resetHover() { |
|
470 const kStateHover = 0x00000004; |
|
471 try { |
|
472 let element = content.document.createElement('foo'); |
|
473 this._domUtils.setContentState(element, kStateHover); |
|
474 } catch(e) {} |
|
475 }, |
|
476 |
|
477 _setActive: function cp_setActive(elt) { |
|
478 const kStateActive = 0x00000001; |
|
479 this._domUtils.setContentState(elt, kStateActive); |
|
480 }, |
|
481 |
|
482 _recvViewportChange: function(data) { |
|
483 let metrics = data.json; |
|
484 this._viewport = new Rect(metrics.x, metrics.y, |
|
485 metrics.viewport.width, |
|
486 metrics.viewport.height); |
|
487 this._cssCompositedRect = new Rect(metrics.x, metrics.y, |
|
488 metrics.cssCompositedRect.width, |
|
489 metrics.cssCompositedRect.height); |
|
490 this._cssPageRect = new Rect(metrics.cssPageRect.x, |
|
491 metrics.cssPageRect.y, |
|
492 metrics.cssPageRect.width, |
|
493 metrics.cssPageRect.height); |
|
494 }, |
|
495 |
|
496 _recvDoubleTap: function(data) { |
|
497 let data = data.json; |
|
498 |
|
499 // We haven't received a metrics update yet; don't do anything. |
|
500 if (this._viewport == null) { |
|
501 return; |
|
502 } |
|
503 |
|
504 let win = content; |
|
505 |
|
506 let element = ElementTouchHelper.anyElementFromPoint(win, data.x, data.y); |
|
507 if (!element) { |
|
508 this._zoomOut(); |
|
509 return; |
|
510 } |
|
511 |
|
512 while (element && !this._shouldZoomToElement(element)) |
|
513 element = element.parentNode; |
|
514 |
|
515 if (!element) { |
|
516 this._zoomOut(); |
|
517 } else { |
|
518 const margin = 15; |
|
519 let rect = ElementTouchHelper.getBoundingContentRect(element); |
|
520 |
|
521 let cssPageRect = this._cssPageRect; |
|
522 let viewport = this._viewport; |
|
523 let bRect = new Rect(Math.max(cssPageRect.x, rect.x - margin), |
|
524 rect.y, |
|
525 rect.w + 2 * margin, |
|
526 rect.h); |
|
527 // constrict the rect to the screen's right edge |
|
528 bRect.width = Math.min(bRect.width, cssPageRect.right - bRect.x); |
|
529 |
|
530 // if the rect is already taking up most of the visible area and is stretching the |
|
531 // width of the page, then we want to zoom out instead. |
|
532 if (this._isRectZoomedIn(bRect, this._cssCompositedRect)) { |
|
533 this._zoomOut(); |
|
534 return; |
|
535 } |
|
536 |
|
537 rect.x = Math.round(bRect.x); |
|
538 rect.y = Math.round(bRect.y); |
|
539 rect.w = Math.round(bRect.width); |
|
540 rect.h = Math.round(bRect.height); |
|
541 |
|
542 // if the block we're zooming to is really tall, and the user double-tapped |
|
543 // more than a screenful of height from the top of it, then adjust the y-coordinate |
|
544 // so that we center the actual point the user double-tapped upon. this prevents |
|
545 // flying to the top of a page when double-tapping to zoom in (bug 761721). |
|
546 // the 1.2 multiplier is just a little fuzz to compensate for bRect including horizontal |
|
547 // margins but not vertical ones. |
|
548 let cssTapY = viewport.y + data.y; |
|
549 if ((bRect.height > rect.h) && (cssTapY > rect.y + (rect.h * 1.2))) { |
|
550 rect.y = cssTapY - (rect.h / 2); |
|
551 } |
|
552 |
|
553 Services.obs.notifyObservers(docShell, 'browser-zoom-to-rect', JSON.stringify(rect)); |
|
554 } |
|
555 }, |
|
556 |
|
557 _handleVisibilityChange: function(evt) { |
|
558 if (!evt.target.hidden) |
|
559 return; |
|
560 |
|
561 this._resetHover(); |
|
562 }, |
|
563 |
|
564 _shouldZoomToElement: function(aElement) { |
|
565 let win = aElement.ownerDocument.defaultView; |
|
566 if (win.getComputedStyle(aElement, null).display == "inline") |
|
567 return false; |
|
568 if (aElement instanceof Ci.nsIDOMHTMLLIElement) |
|
569 return false; |
|
570 if (aElement instanceof Ci.nsIDOMHTMLQuoteElement) |
|
571 return false; |
|
572 return true; |
|
573 }, |
|
574 |
|
575 _zoomOut: function() { |
|
576 let rect = new Rect(0, 0, 0, 0); |
|
577 Services.obs.notifyObservers(docShell, 'browser-zoom-to-rect', JSON.stringify(rect)); |
|
578 }, |
|
579 |
|
580 _isRectZoomedIn: function(aRect, aViewport) { |
|
581 // This function checks to see if the area of the rect visible in the |
|
582 // viewport (i.e. the "overlapArea" variable below) is approximately |
|
583 // the max area of the rect we can show. |
|
584 let vRect = new Rect(aViewport.x, aViewport.y, aViewport.width, aViewport.height); |
|
585 let overlap = vRect.intersect(aRect); |
|
586 let overlapArea = overlap.width * overlap.height; |
|
587 let availHeight = Math.min(aRect.width * vRect.height / vRect.width, aRect.height); |
|
588 let showing = overlapArea / (aRect.width * availHeight); |
|
589 let ratioW = (aRect.width / vRect.width); |
|
590 let ratioH = (aRect.height / vRect.height); |
|
591 |
|
592 return (showing > 0.9 && (ratioW > 0.9 || ratioH > 0.9)); |
|
593 }, |
|
594 |
|
595 _finishPanning: function() { |
|
596 this.dragging = false; |
|
597 delete this.primaryPointerId; |
|
598 this._activationTimer.cancel(); |
|
599 |
|
600 // If there is a scroll action, let's do a manual kinetic panning action. |
|
601 if (this.panning) { |
|
602 KineticPanning.start(this); |
|
603 } |
|
604 }, |
|
605 |
|
606 _unloadHandler: function() { |
|
607 kObservedEvents.forEach((topic) => { |
|
608 Services.obs.removeObserver(this, topic); |
|
609 }); |
|
610 } |
|
611 }; |
|
612 |
|
613 // Min/max velocity of kinetic panning. This is in pixels/millisecond. |
|
614 const kMinVelocity = 0.2; |
|
615 const kMaxVelocity = 6; |
|
616 |
|
617 // Constants that affect the "friction" of the scroll pane. |
|
618 const kExponentialC = 1000; |
|
619 const kPolynomialC = 100 / 1000000; |
|
620 |
|
621 // How often do we change the position of the scroll pane? |
|
622 // Too often and panning may jerk near the end. |
|
623 // Too little and panning will be choppy. In milliseconds. |
|
624 const kUpdateInterval = 16; |
|
625 |
|
626 // The numbers of momentums to use for calculating the velocity of the pan. |
|
627 // Those are taken from the end of the action |
|
628 const kSamples = 5; |
|
629 |
|
630 const KineticPanning = { |
|
631 _position: new Point(0, 0), |
|
632 _velocity: new Point(0, 0), |
|
633 _acceleration: new Point(0, 0), |
|
634 |
|
635 get active() { |
|
636 return this.target !== null; |
|
637 }, |
|
638 |
|
639 target: null, |
|
640 start: function kp_start(target) { |
|
641 this.target = target; |
|
642 |
|
643 // Calculate the initial velocity of the movement based on user input |
|
644 let momentums = this.momentums; |
|
645 let flick = momentums[momentums.length - 1].time - momentums[0].time < 300; |
|
646 |
|
647 let distance = new Point(0, 0); |
|
648 momentums.forEach(function(momentum) { |
|
649 distance.add(momentum.dx, momentum.dy); |
|
650 }); |
|
651 |
|
652 function clampFromZero(x, min, max) { |
|
653 if (x >= 0) |
|
654 return Math.max(min, Math.min(max, x)); |
|
655 return Math.min(-min, Math.max(-max, x)); |
|
656 } |
|
657 |
|
658 let elapsed = momentums[momentums.length - 1].time - momentums[0].time; |
|
659 let velocityX = clampFromZero(distance.x / elapsed, 0, kMaxVelocity); |
|
660 let velocityY = clampFromZero(distance.y / elapsed, 0, kMaxVelocity); |
|
661 |
|
662 let velocity = this._velocity; |
|
663 if (flick) { |
|
664 // Very fast pan action that does not generate a click are very often pan |
|
665 // action. If this is a small gesture then it will not move the view a lot |
|
666 // and so it will be above the minimun threshold and not generate any |
|
667 // kinetic panning. This does not look on a device since this is often |
|
668 // a real gesture, so let's lower the velocity threshold for such moves. |
|
669 velocity.set(velocityX, velocityY); |
|
670 } else { |
|
671 velocity.set(Math.abs(velocityX) < kMinVelocity ? 0 : velocityX, |
|
672 Math.abs(velocityY) < kMinVelocity ? 0 : velocityY); |
|
673 } |
|
674 this.momentums = []; |
|
675 |
|
676 // Set acceleration vector to opposite signs of velocity |
|
677 function sign(x) { |
|
678 return x ? (x > 0 ? 1 : -1) : 0; |
|
679 } |
|
680 |
|
681 this._acceleration.set(velocity.clone().map(sign).scale(-kPolynomialC)); |
|
682 |
|
683 // Reset the position |
|
684 this._position.set(0, 0); |
|
685 |
|
686 this._startAnimation(); |
|
687 |
|
688 this.target.onKineticBegin(); |
|
689 }, |
|
690 |
|
691 stop: function kp_stop() { |
|
692 this.reset(); |
|
693 |
|
694 if (!this.target) |
|
695 return; |
|
696 |
|
697 this.target.onKineticEnd(); |
|
698 this.target = null; |
|
699 }, |
|
700 |
|
701 reset: function kp_reset() { |
|
702 this.momentums = []; |
|
703 this.distance.set(0, 0); |
|
704 }, |
|
705 |
|
706 momentums: [], |
|
707 record: function kp_record(delta, timestamp) { |
|
708 this.momentums.push({ 'time': this._getTime(timestamp), |
|
709 'dx' : delta.x, 'dy' : delta.y }); |
|
710 |
|
711 // We only need to keep kSamples in this.momentums. |
|
712 if (this.momentums.length > kSamples) { |
|
713 this.momentums.shift(); |
|
714 } |
|
715 |
|
716 this.distance.add(delta.x, delta.y); |
|
717 }, |
|
718 |
|
719 _getTime: function kp_getTime(time) { |
|
720 // Touch events generated by the platform or hand-made are defined in |
|
721 // microseconds instead of milliseconds. Bug 77992 will fix this at the |
|
722 // platform level. |
|
723 if (time > Date.now()) { |
|
724 return Math.floor(time / 1000); |
|
725 } else { |
|
726 return time; |
|
727 } |
|
728 }, |
|
729 |
|
730 get threshold() { |
|
731 let dpi = content.QueryInterface(Ci.nsIInterfaceRequestor) |
|
732 .getInterface(Ci.nsIDOMWindowUtils) |
|
733 .displayDPI; |
|
734 |
|
735 let threshold = Services.prefs.getIntPref('ui.dragThresholdX') / 240 * dpi; |
|
736 |
|
737 delete this.threshold; |
|
738 return this.threshold = threshold; |
|
739 }, |
|
740 |
|
741 distance: new Point(0, 0), |
|
742 isPan: function cp_isPan() { |
|
743 return (Math.abs(this.distance.x) > this.threshold || |
|
744 Math.abs(this.distance.y) > this.threshold); |
|
745 }, |
|
746 |
|
747 _startAnimation: function kp_startAnimation() { |
|
748 let c = kExponentialC; |
|
749 function getNextPosition(position, v, a, t) { |
|
750 // Important traits for this function: |
|
751 // p(t=0) is 0 |
|
752 // p'(t=0) is v0 |
|
753 // |
|
754 // We use exponential to get a smoother stop, but by itself exponential |
|
755 // is too smooth at the end. Adding a polynomial with the appropriate |
|
756 // weight helps to balance |
|
757 position.set(v.x * Math.exp(-t / c) * -c + a.x * t * t + v.x * c, |
|
758 v.y * Math.exp(-t / c) * -c + a.y * t * t + v.y * c); |
|
759 } |
|
760 |
|
761 let startTime = content.mozAnimationStartTime; |
|
762 let elapsedTime = 0, targetedTime = 0, averageTime = 0; |
|
763 |
|
764 let velocity = this._velocity; |
|
765 let acceleration = this._acceleration; |
|
766 |
|
767 let position = this._position; |
|
768 let nextPosition = new Point(0, 0); |
|
769 let delta = new Point(0, 0); |
|
770 |
|
771 let callback = (function(timestamp) { |
|
772 if (!this.target) |
|
773 return; |
|
774 |
|
775 // To make animation end fast enough but to keep smoothness, average the |
|
776 // ideal time frame (smooth animation) with the actual time lapse |
|
777 // (end fast enough). |
|
778 // Animation will never take longer than 2 times the ideal length of time. |
|
779 elapsedTime = timestamp - startTime; |
|
780 targetedTime += kUpdateInterval; |
|
781 averageTime = (targetedTime + elapsedTime) / 2; |
|
782 |
|
783 // Calculate new position. |
|
784 getNextPosition(nextPosition, velocity, acceleration, averageTime); |
|
785 delta.set(Math.round(nextPosition.x - position.x), |
|
786 Math.round(nextPosition.y - position.y)); |
|
787 |
|
788 // Test to see if movement is finished for each component. |
|
789 if (delta.x * acceleration.x > 0) |
|
790 delta.x = position.x = velocity.x = acceleration.x = 0; |
|
791 |
|
792 if (delta.y * acceleration.y > 0) |
|
793 delta.y = position.y = velocity.y = acceleration.y = 0; |
|
794 |
|
795 if (velocity.equals(0, 0) || delta.equals(0, 0)) { |
|
796 this.stop(); |
|
797 return; |
|
798 } |
|
799 |
|
800 position.add(delta); |
|
801 if (this.target.onKineticPan(delta.scale(-1))) { |
|
802 this.stop(); |
|
803 return; |
|
804 } |
|
805 |
|
806 content.mozRequestAnimationFrame(callback); |
|
807 }).bind(this); |
|
808 |
|
809 content.mozRequestAnimationFrame(callback); |
|
810 } |
|
811 }; |
|
812 |
|
813 const ElementTouchHelper = { |
|
814 anyElementFromPoint: function(aWindow, aX, aY) { |
|
815 let cwu = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); |
|
816 let elem = cwu.elementFromPoint(aX, aY, true, true); |
|
817 |
|
818 let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement; |
|
819 let HTMLFrameElement = Ci.nsIDOMHTMLFrameElement; |
|
820 while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) { |
|
821 let rect = elem.getBoundingClientRect(); |
|
822 aX -= rect.left; |
|
823 aY -= rect.top; |
|
824 cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); |
|
825 elem = cwu.elementFromPoint(aX, aY, true, true); |
|
826 } |
|
827 |
|
828 return elem; |
|
829 }, |
|
830 |
|
831 getBoundingContentRect: function(aElement) { |
|
832 if (!aElement) |
|
833 return {x: 0, y: 0, w: 0, h: 0}; |
|
834 |
|
835 let document = aElement.ownerDocument; |
|
836 while (document.defaultView.frameElement) |
|
837 document = document.defaultView.frameElement.ownerDocument; |
|
838 |
|
839 let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); |
|
840 let scrollX = {}, scrollY = {}; |
|
841 cwu.getScrollXY(false, scrollX, scrollY); |
|
842 |
|
843 let r = aElement.getBoundingClientRect(); |
|
844 |
|
845 // step out of iframes and frames, offsetting scroll values |
|
846 for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && frame != content; frame = frame.parent) { |
|
847 // adjust client coordinates' origin to be top left of iframe viewport |
|
848 let rect = frame.frameElement.getBoundingClientRect(); |
|
849 let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth; |
|
850 let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth; |
|
851 scrollX.value += rect.left + parseInt(left); |
|
852 scrollY.value += rect.top + parseInt(top); |
|
853 } |
|
854 |
|
855 return {x: r.left + scrollX.value, |
|
856 y: r.top + scrollY.value, |
|
857 w: r.width, |
|
858 h: r.height }; |
|
859 } |
|
860 }; |