|
1 # This Source Code Form is subject to the terms of the Mozilla Public |
|
2 # License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
|
4 |
|
5 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this); |
|
6 |
|
7 // Simple gestures support |
|
8 // |
|
9 // As per bug #412486, web content must not be allowed to receive any |
|
10 // simple gesture events. Multi-touch gesture APIs are in their |
|
11 // infancy and we do NOT want to be forced into supporting an API that |
|
12 // will probably have to change in the future. (The current Mac OS X |
|
13 // API is undocumented and was reverse-engineered.) Until support is |
|
14 // implemented in the event dispatcher to keep these events as |
|
15 // chrome-only, we must listen for the simple gesture events during |
|
16 // the capturing phase and call stopPropagation on every event. |
|
17 |
|
18 let gGestureSupport = { |
|
19 _currentRotation: 0, |
|
20 _lastRotateDelta: 0, |
|
21 _rotateMomentumThreshold: .75, |
|
22 |
|
23 /** |
|
24 * Add or remove mouse gesture event listeners |
|
25 * |
|
26 * @param aAddListener |
|
27 * True to add/init listeners and false to remove/uninit |
|
28 */ |
|
29 init: function GS_init(aAddListener) { |
|
30 const gestureEvents = ["SwipeGestureStart", |
|
31 "SwipeGestureUpdate", "SwipeGestureEnd", "SwipeGesture", |
|
32 "MagnifyGestureStart", "MagnifyGestureUpdate", "MagnifyGesture", |
|
33 "RotateGestureStart", "RotateGestureUpdate", "RotateGesture", |
|
34 "TapGesture", "PressTapGesture"]; |
|
35 |
|
36 let addRemove = aAddListener ? window.addEventListener : |
|
37 window.removeEventListener; |
|
38 |
|
39 gestureEvents.forEach(function (event) addRemove("Moz" + event, this, true), |
|
40 this); |
|
41 }, |
|
42 |
|
43 /** |
|
44 * Dispatch events based on the type of mouse gesture event. For now, make |
|
45 * sure to stop propagation of every gesture event so that web content cannot |
|
46 * receive gesture events. |
|
47 * |
|
48 * @param aEvent |
|
49 * The gesture event to handle |
|
50 */ |
|
51 handleEvent: function GS_handleEvent(aEvent) { |
|
52 if (!Services.prefs.getBoolPref( |
|
53 "dom.debug.propagate_gesture_events_through_content")) { |
|
54 aEvent.stopPropagation(); |
|
55 } |
|
56 |
|
57 // Create a preference object with some defaults |
|
58 let def = function(aThreshold, aLatched) |
|
59 ({ threshold: aThreshold, latched: !!aLatched }); |
|
60 |
|
61 switch (aEvent.type) { |
|
62 case "MozSwipeGestureStart": |
|
63 if (this._setupSwipeGesture(aEvent)) { |
|
64 aEvent.preventDefault(); |
|
65 } |
|
66 break; |
|
67 case "MozSwipeGestureUpdate": |
|
68 aEvent.preventDefault(); |
|
69 this._doUpdate(aEvent); |
|
70 break; |
|
71 case "MozSwipeGestureEnd": |
|
72 aEvent.preventDefault(); |
|
73 this._doEnd(aEvent); |
|
74 break; |
|
75 case "MozSwipeGesture": |
|
76 aEvent.preventDefault(); |
|
77 this.onSwipe(aEvent); |
|
78 break; |
|
79 case "MozMagnifyGestureStart": |
|
80 aEvent.preventDefault(); |
|
81 #ifdef XP_WIN |
|
82 this._setupGesture(aEvent, "pinch", def(25, 0), "out", "in"); |
|
83 #else |
|
84 this._setupGesture(aEvent, "pinch", def(150, 1), "out", "in"); |
|
85 #endif |
|
86 break; |
|
87 case "MozRotateGestureStart": |
|
88 aEvent.preventDefault(); |
|
89 this._setupGesture(aEvent, "twist", def(25, 0), "right", "left"); |
|
90 break; |
|
91 case "MozMagnifyGestureUpdate": |
|
92 case "MozRotateGestureUpdate": |
|
93 aEvent.preventDefault(); |
|
94 this._doUpdate(aEvent); |
|
95 break; |
|
96 case "MozTapGesture": |
|
97 aEvent.preventDefault(); |
|
98 this._doAction(aEvent, ["tap"]); |
|
99 break; |
|
100 case "MozRotateGesture": |
|
101 aEvent.preventDefault(); |
|
102 this._doAction(aEvent, ["twist", "end"]); |
|
103 break; |
|
104 /* case "MozPressTapGesture": |
|
105 break; */ |
|
106 } |
|
107 }, |
|
108 |
|
109 /** |
|
110 * Called at the start of "pinch" and "twist" gestures to setup all of the |
|
111 * information needed to process the gesture |
|
112 * |
|
113 * @param aEvent |
|
114 * The continual motion start event to handle |
|
115 * @param aGesture |
|
116 * Name of the gesture to handle |
|
117 * @param aPref |
|
118 * Preference object with the names of preferences and defaults |
|
119 * @param aInc |
|
120 * Command to trigger for increasing motion (without gesture name) |
|
121 * @param aDec |
|
122 * Command to trigger for decreasing motion (without gesture name) |
|
123 */ |
|
124 _setupGesture: function GS__setupGesture(aEvent, aGesture, aPref, aInc, aDec) { |
|
125 // Try to load user-set values from preferences |
|
126 for (let [pref, def] in Iterator(aPref)) |
|
127 aPref[pref] = this._getPref(aGesture + "." + pref, def); |
|
128 |
|
129 // Keep track of the total deltas and latching behavior |
|
130 let offset = 0; |
|
131 let latchDir = aEvent.delta > 0 ? 1 : -1; |
|
132 let isLatched = false; |
|
133 |
|
134 // Create the update function here to capture closure state |
|
135 this._doUpdate = function GS__doUpdate(aEvent) { |
|
136 // Update the offset with new event data |
|
137 offset += aEvent.delta; |
|
138 |
|
139 // Check if the cumulative deltas exceed the threshold |
|
140 if (Math.abs(offset) > aPref["threshold"]) { |
|
141 // Trigger the action if we don't care about latching; otherwise, make |
|
142 // sure either we're not latched and going the same direction of the |
|
143 // initial motion; or we're latched and going the opposite way |
|
144 let sameDir = (latchDir ^ offset) >= 0; |
|
145 if (!aPref["latched"] || (isLatched ^ sameDir)) { |
|
146 this._doAction(aEvent, [aGesture, offset > 0 ? aInc : aDec]); |
|
147 |
|
148 // We must be getting latched or leaving it, so just toggle |
|
149 isLatched = !isLatched; |
|
150 } |
|
151 |
|
152 // Reset motion counter to prepare for more of the same gesture |
|
153 offset = 0; |
|
154 } |
|
155 }; |
|
156 |
|
157 // The start event also contains deltas, so handle an update right away |
|
158 this._doUpdate(aEvent); |
|
159 }, |
|
160 |
|
161 /** |
|
162 * Checks whether a swipe gesture event can navigate the browser history or |
|
163 * not. |
|
164 * |
|
165 * @param aEvent |
|
166 * The swipe gesture event. |
|
167 * @return true if the swipe event may navigate the history, false othwerwise. |
|
168 */ |
|
169 _swipeNavigatesHistory: function GS__swipeNavigatesHistory(aEvent) { |
|
170 return this._getCommand(aEvent, ["swipe", "left"]) |
|
171 == "Browser:BackOrBackDuplicate" && |
|
172 this._getCommand(aEvent, ["swipe", "right"]) |
|
173 == "Browser:ForwardOrForwardDuplicate"; |
|
174 }, |
|
175 |
|
176 /** |
|
177 * Sets up swipe gestures. This includes setting up swipe animations for the |
|
178 * gesture, if enabled. |
|
179 * |
|
180 * @param aEvent |
|
181 * The swipe gesture start event. |
|
182 * @return true if swipe gestures could successfully be set up, false |
|
183 * othwerwise. |
|
184 */ |
|
185 _setupSwipeGesture: function GS__setupSwipeGesture(aEvent) { |
|
186 if (!this._swipeNavigatesHistory(aEvent)) { |
|
187 return false; |
|
188 } |
|
189 |
|
190 let isVerticalSwipe = false; |
|
191 if (aEvent.direction == aEvent.DIRECTION_UP) { |
|
192 if (content.pageYOffset > 0) { |
|
193 return false; |
|
194 } |
|
195 isVerticalSwipe = true; |
|
196 } else if (aEvent.direction == aEvent.DIRECTION_DOWN) { |
|
197 if (content.pageYOffset < content.scrollMaxY) { |
|
198 return false; |
|
199 } |
|
200 isVerticalSwipe = true; |
|
201 } |
|
202 if (isVerticalSwipe) { |
|
203 // Vertical overscroll has been temporarily disabled until bug 939480 is |
|
204 // fixed. |
|
205 return false; |
|
206 } |
|
207 |
|
208 let canGoBack = gHistorySwipeAnimation.canGoBack(); |
|
209 let canGoForward = gHistorySwipeAnimation.canGoForward(); |
|
210 let isLTR = gHistorySwipeAnimation.isLTR; |
|
211 |
|
212 if (canGoBack) { |
|
213 aEvent.allowedDirections |= isLTR ? aEvent.DIRECTION_LEFT : |
|
214 aEvent.DIRECTION_RIGHT; |
|
215 } |
|
216 if (canGoForward) { |
|
217 aEvent.allowedDirections |= isLTR ? aEvent.DIRECTION_RIGHT : |
|
218 aEvent.DIRECTION_LEFT; |
|
219 } |
|
220 |
|
221 gHistorySwipeAnimation.startAnimation(isVerticalSwipe); |
|
222 |
|
223 this._doUpdate = function GS__doUpdate(aEvent) { |
|
224 gHistorySwipeAnimation.updateAnimation(aEvent.delta); |
|
225 }; |
|
226 |
|
227 this._doEnd = function GS__doEnd(aEvent) { |
|
228 gHistorySwipeAnimation.swipeEndEventReceived(); |
|
229 |
|
230 this._doUpdate = function (aEvent) {}; |
|
231 this._doEnd = function (aEvent) {}; |
|
232 } |
|
233 |
|
234 return true; |
|
235 }, |
|
236 |
|
237 /** |
|
238 * Generator producing the powerset of the input array where the first result |
|
239 * is the complete set and the last result (before StopIteration) is empty. |
|
240 * |
|
241 * @param aArray |
|
242 * Source array containing any number of elements |
|
243 * @yield Array that is a subset of the input array from full set to empty |
|
244 */ |
|
245 _power: function GS__power(aArray) { |
|
246 // Create a bitmask based on the length of the array |
|
247 let num = 1 << aArray.length; |
|
248 while (--num >= 0) { |
|
249 // Only select array elements where the current bit is set |
|
250 yield aArray.reduce(function (aPrev, aCurr, aIndex) { |
|
251 if (num & 1 << aIndex) |
|
252 aPrev.push(aCurr); |
|
253 return aPrev; |
|
254 }, []); |
|
255 } |
|
256 }, |
|
257 |
|
258 /** |
|
259 * Determine what action to do for the gesture based on which keys are |
|
260 * pressed and which commands are set, and execute the command. |
|
261 * |
|
262 * @param aEvent |
|
263 * The original gesture event to convert into a fake click event |
|
264 * @param aGesture |
|
265 * Array of gesture name parts (to be joined by periods) |
|
266 * @return Name of the executed command. Returns null if no command is |
|
267 * found. |
|
268 */ |
|
269 _doAction: function GS__doAction(aEvent, aGesture) { |
|
270 let command = this._getCommand(aEvent, aGesture); |
|
271 return command && this._doCommand(aEvent, command); |
|
272 }, |
|
273 |
|
274 /** |
|
275 * Determine what action to do for the gesture based on which keys are |
|
276 * pressed and which commands are set |
|
277 * |
|
278 * @param aEvent |
|
279 * The original gesture event to convert into a fake click event |
|
280 * @param aGesture |
|
281 * Array of gesture name parts (to be joined by periods) |
|
282 */ |
|
283 _getCommand: function GS__getCommand(aEvent, aGesture) { |
|
284 // Create an array of pressed keys in a fixed order so that a command for |
|
285 // "meta" is preferred over "ctrl" when both buttons are pressed (and a |
|
286 // command for both don't exist) |
|
287 let keyCombos = []; |
|
288 ["shift", "alt", "ctrl", "meta"].forEach(function (key) { |
|
289 if (aEvent[key + "Key"]) |
|
290 keyCombos.push(key); |
|
291 }); |
|
292 |
|
293 // Try each combination of key presses in decreasing order for commands |
|
294 for (let subCombo of this._power(keyCombos)) { |
|
295 // Convert a gesture and pressed keys into the corresponding command |
|
296 // action where the preference has the gesture before "shift" before |
|
297 // "alt" before "ctrl" before "meta" all separated by periods |
|
298 let command; |
|
299 try { |
|
300 command = this._getPref(aGesture.concat(subCombo).join(".")); |
|
301 } catch (e) {} |
|
302 |
|
303 if (command) |
|
304 return command; |
|
305 } |
|
306 return null; |
|
307 }, |
|
308 |
|
309 /** |
|
310 * Execute the specified command. |
|
311 * |
|
312 * @param aEvent |
|
313 * The original gesture event to convert into a fake click event |
|
314 * @param aCommand |
|
315 * Name of the command found for the event's keys and gesture. |
|
316 */ |
|
317 _doCommand: function GS__doCommand(aEvent, aCommand) { |
|
318 let node = document.getElementById(aCommand); |
|
319 if (node) { |
|
320 if (node.getAttribute("disabled") != "true") { |
|
321 let cmdEvent = document.createEvent("xulcommandevent"); |
|
322 cmdEvent.initCommandEvent("command", true, true, window, 0, |
|
323 aEvent.ctrlKey, aEvent.altKey, |
|
324 aEvent.shiftKey, aEvent.metaKey, aEvent); |
|
325 node.dispatchEvent(cmdEvent); |
|
326 } |
|
327 |
|
328 } |
|
329 else { |
|
330 goDoCommand(aCommand); |
|
331 } |
|
332 }, |
|
333 |
|
334 /** |
|
335 * Handle continual motion events. This function will be set by |
|
336 * _setupGesture or _setupSwipe. |
|
337 * |
|
338 * @param aEvent |
|
339 * The continual motion update event to handle |
|
340 */ |
|
341 _doUpdate: function(aEvent) {}, |
|
342 |
|
343 /** |
|
344 * Handle gesture end events. This function will be set by _setupSwipe. |
|
345 * |
|
346 * @param aEvent |
|
347 * The gesture end event to handle |
|
348 */ |
|
349 _doEnd: function(aEvent) {}, |
|
350 |
|
351 /** |
|
352 * Convert the swipe gesture into a browser action based on the direction. |
|
353 * |
|
354 * @param aEvent |
|
355 * The swipe event to handle |
|
356 */ |
|
357 onSwipe: function GS_onSwipe(aEvent) { |
|
358 // Figure out which one (and only one) direction was triggered |
|
359 for (let dir of ["UP", "RIGHT", "DOWN", "LEFT"]) { |
|
360 if (aEvent.direction == aEvent["DIRECTION_" + dir]) { |
|
361 this._coordinateSwipeEventWithAnimation(aEvent, dir); |
|
362 break; |
|
363 } |
|
364 } |
|
365 }, |
|
366 |
|
367 /** |
|
368 * Process a swipe event based on the given direction. |
|
369 * |
|
370 * @param aEvent |
|
371 * The swipe event to handle |
|
372 * @param aDir |
|
373 * The direction for the swipe event |
|
374 */ |
|
375 processSwipeEvent: function GS_processSwipeEvent(aEvent, aDir) { |
|
376 this._doAction(aEvent, ["swipe", aDir.toLowerCase()]); |
|
377 }, |
|
378 |
|
379 /** |
|
380 * Coordinates the swipe event with the swipe animation, if any. |
|
381 * If an animation is currently running, the swipe event will be |
|
382 * processed once the animation stops. This will guarantee a fluid |
|
383 * motion of the animation. |
|
384 * |
|
385 * @param aEvent |
|
386 * The swipe event to handle |
|
387 * @param aDir |
|
388 * The direction for the swipe event |
|
389 */ |
|
390 _coordinateSwipeEventWithAnimation: |
|
391 function GS__coordinateSwipeEventWithAnimation(aEvent, aDir) { |
|
392 if ((gHistorySwipeAnimation.isAnimationRunning()) && |
|
393 (aDir == "RIGHT" || aDir == "LEFT")) { |
|
394 gHistorySwipeAnimation.processSwipeEvent(aEvent, aDir); |
|
395 } |
|
396 else { |
|
397 this.processSwipeEvent(aEvent, aDir); |
|
398 } |
|
399 }, |
|
400 |
|
401 /** |
|
402 * Get a gesture preference or use a default if it doesn't exist |
|
403 * |
|
404 * @param aPref |
|
405 * Name of the preference to load under the gesture branch |
|
406 * @param aDef |
|
407 * Default value if the preference doesn't exist |
|
408 */ |
|
409 _getPref: function GS__getPref(aPref, aDef) { |
|
410 // Preferences branch under which all gestures preferences are stored |
|
411 const branch = "browser.gesture."; |
|
412 |
|
413 try { |
|
414 // Determine what type of data to load based on default value's type |
|
415 let type = typeof aDef; |
|
416 let getFunc = "get" + (type == "boolean" ? "Bool" : |
|
417 type == "number" ? "Int" : "Char") + "Pref"; |
|
418 return gPrefService[getFunc](branch + aPref); |
|
419 } |
|
420 catch (e) { |
|
421 return aDef; |
|
422 } |
|
423 }, |
|
424 |
|
425 /** |
|
426 * Perform rotation for ImageDocuments |
|
427 * |
|
428 * @param aEvent |
|
429 * The MozRotateGestureUpdate event triggering this call |
|
430 */ |
|
431 rotate: function(aEvent) { |
|
432 if (!(content.document instanceof ImageDocument)) |
|
433 return; |
|
434 |
|
435 let contentElement = content.document.body.firstElementChild; |
|
436 if (!contentElement) |
|
437 return; |
|
438 // If we're currently snapping, cancel that snap |
|
439 if (contentElement.classList.contains("completeRotation")) |
|
440 this._clearCompleteRotation(); |
|
441 |
|
442 this.rotation = Math.round(this.rotation + aEvent.delta); |
|
443 contentElement.style.transform = "rotate(" + this.rotation + "deg)"; |
|
444 this._lastRotateDelta = aEvent.delta; |
|
445 }, |
|
446 |
|
447 /** |
|
448 * Perform a rotation end for ImageDocuments |
|
449 */ |
|
450 rotateEnd: function() { |
|
451 if (!(content.document instanceof ImageDocument)) |
|
452 return; |
|
453 |
|
454 let contentElement = content.document.body.firstElementChild; |
|
455 if (!contentElement) |
|
456 return; |
|
457 |
|
458 let transitionRotation = 0; |
|
459 |
|
460 // The reason that 360 is allowed here is because when rotating between |
|
461 // 315 and 360, setting rotate(0deg) will cause it to rotate the wrong |
|
462 // direction around--spinning wildly. |
|
463 if (this.rotation <= 45) |
|
464 transitionRotation = 0; |
|
465 else if (this.rotation > 45 && this.rotation <= 135) |
|
466 transitionRotation = 90; |
|
467 else if (this.rotation > 135 && this.rotation <= 225) |
|
468 transitionRotation = 180; |
|
469 else if (this.rotation > 225 && this.rotation <= 315) |
|
470 transitionRotation = 270; |
|
471 else |
|
472 transitionRotation = 360; |
|
473 |
|
474 // If we're going fast enough, and we didn't already snap ahead of rotation, |
|
475 // then snap ahead of rotation to simulate momentum |
|
476 if (this._lastRotateDelta > this._rotateMomentumThreshold && |
|
477 this.rotation > transitionRotation) |
|
478 transitionRotation += 90; |
|
479 else if (this._lastRotateDelta < -1 * this._rotateMomentumThreshold && |
|
480 this.rotation < transitionRotation) |
|
481 transitionRotation -= 90; |
|
482 |
|
483 // Only add the completeRotation class if it is is necessary |
|
484 if (transitionRotation != this.rotation) { |
|
485 contentElement.classList.add("completeRotation"); |
|
486 contentElement.addEventListener("transitionend", this._clearCompleteRotation); |
|
487 } |
|
488 |
|
489 contentElement.style.transform = "rotate(" + transitionRotation + "deg)"; |
|
490 this.rotation = transitionRotation; |
|
491 }, |
|
492 |
|
493 /** |
|
494 * Gets the current rotation for the ImageDocument |
|
495 */ |
|
496 get rotation() { |
|
497 return this._currentRotation; |
|
498 }, |
|
499 |
|
500 /** |
|
501 * Sets the current rotation for the ImageDocument |
|
502 * |
|
503 * @param aVal |
|
504 * The new value to take. Can be any value, but it will be bounded to |
|
505 * 0 inclusive to 360 exclusive. |
|
506 */ |
|
507 set rotation(aVal) { |
|
508 this._currentRotation = aVal % 360; |
|
509 if (this._currentRotation < 0) |
|
510 this._currentRotation += 360; |
|
511 return this._currentRotation; |
|
512 }, |
|
513 |
|
514 /** |
|
515 * When the location/tab changes, need to reload the current rotation for the |
|
516 * image |
|
517 */ |
|
518 restoreRotationState: function() { |
|
519 // Bug 863514 - Make gesture support work in electrolysis |
|
520 if (gMultiProcessBrowser) |
|
521 return; |
|
522 |
|
523 if (!(content.document instanceof ImageDocument)) |
|
524 return; |
|
525 |
|
526 let contentElement = content.document.body.firstElementChild; |
|
527 let transformValue = content.window.getComputedStyle(contentElement, null) |
|
528 .transform; |
|
529 |
|
530 if (transformValue == "none") { |
|
531 this.rotation = 0; |
|
532 return; |
|
533 } |
|
534 |
|
535 // transformValue is a rotation matrix--split it and do mathemagic to |
|
536 // obtain the real rotation value |
|
537 transformValue = transformValue.split("(")[1] |
|
538 .split(")")[0] |
|
539 .split(","); |
|
540 this.rotation = Math.round(Math.atan2(transformValue[1], transformValue[0]) * |
|
541 (180 / Math.PI)); |
|
542 }, |
|
543 |
|
544 /** |
|
545 * Removes the transition rule by removing the completeRotation class |
|
546 */ |
|
547 _clearCompleteRotation: function() { |
|
548 let contentElement = content.document && |
|
549 content.document instanceof ImageDocument && |
|
550 content.document.body && |
|
551 content.document.body.firstElementChild; |
|
552 if (!contentElement) |
|
553 return; |
|
554 contentElement.classList.remove("completeRotation"); |
|
555 contentElement.removeEventListener("transitionend", this._clearCompleteRotation); |
|
556 }, |
|
557 }; |
|
558 |
|
559 // History Swipe Animation Support (bug 678392) |
|
560 let gHistorySwipeAnimation = { |
|
561 |
|
562 active: false, |
|
563 isLTR: false, |
|
564 |
|
565 /** |
|
566 * Initializes the support for history swipe animations, if it is supported |
|
567 * by the platform/configuration. |
|
568 */ |
|
569 init: function HSA_init() { |
|
570 if (!this._isSupported()) |
|
571 return; |
|
572 |
|
573 this.active = false; |
|
574 this.isLTR = document.documentElement.mozMatchesSelector( |
|
575 ":-moz-locale-dir(ltr)"); |
|
576 this._trackedSnapshots = []; |
|
577 this._startingIndex = -1; |
|
578 this._historyIndex = -1; |
|
579 this._boxWidth = -1; |
|
580 this._boxHeight = -1; |
|
581 this._maxSnapshots = this._getMaxSnapshots(); |
|
582 this._lastSwipeDir = ""; |
|
583 this._direction = "horizontal"; |
|
584 |
|
585 // We only want to activate history swipe animations if we store snapshots. |
|
586 // If we don't store any, we handle horizontal swipes without animations. |
|
587 if (this._maxSnapshots > 0) { |
|
588 this.active = true; |
|
589 gBrowser.addEventListener("pagehide", this, false); |
|
590 gBrowser.addEventListener("pageshow", this, false); |
|
591 gBrowser.addEventListener("popstate", this, false); |
|
592 gBrowser.addEventListener("DOMModalDialogClosed", this, false); |
|
593 gBrowser.tabContainer.addEventListener("TabClose", this, false); |
|
594 } |
|
595 }, |
|
596 |
|
597 /** |
|
598 * Uninitializes the support for history swipe animations. |
|
599 */ |
|
600 uninit: function HSA_uninit() { |
|
601 gBrowser.removeEventListener("pagehide", this, false); |
|
602 gBrowser.removeEventListener("pageshow", this, false); |
|
603 gBrowser.removeEventListener("popstate", this, false); |
|
604 gBrowser.removeEventListener("DOMModalDialogClosed", this, false); |
|
605 gBrowser.tabContainer.removeEventListener("TabClose", this, false); |
|
606 |
|
607 this.active = false; |
|
608 this.isLTR = false; |
|
609 }, |
|
610 |
|
611 /** |
|
612 * Starts the swipe animation and handles fast swiping (i.e. a swipe animation |
|
613 * is already in progress when a new one is initiated). |
|
614 * |
|
615 * @param aIsVerticalSwipe |
|
616 * Whether we're dealing with a vertical swipe or not. |
|
617 */ |
|
618 startAnimation: function HSA_startAnimation(aIsVerticalSwipe) { |
|
619 this._direction = aIsVerticalSwipe ? "vertical" : "horizontal"; |
|
620 |
|
621 if (this.isAnimationRunning()) { |
|
622 // If this is a horizontal scroll, or if this is a vertical scroll that |
|
623 // was started while a horizontal scroll was still running, handle it as |
|
624 // as a fast swipe. In the case of the latter scenario, this allows us to |
|
625 // start the vertical animation without first loading the final page, or |
|
626 // taking another snapshot. If vertical scrolls are initiated repeatedly |
|
627 // without prior horizontal scroll we skip this and restart the animation |
|
628 // from 0. |
|
629 if (this._direction == "horizontal" || this._lastSwipeDir != "") { |
|
630 gBrowser.stop(); |
|
631 this._lastSwipeDir = "RELOAD"; // just ensure that != "" |
|
632 this._canGoBack = this.canGoBack(); |
|
633 this._canGoForward = this.canGoForward(); |
|
634 this._handleFastSwiping(); |
|
635 } |
|
636 } |
|
637 else { |
|
638 this._startingIndex = gBrowser.webNavigation.sessionHistory.index; |
|
639 this._historyIndex = this._startingIndex; |
|
640 this._canGoBack = this.canGoBack(); |
|
641 this._canGoForward = this.canGoForward(); |
|
642 if (this.active) { |
|
643 this._addBoxes(); |
|
644 this._takeSnapshot(); |
|
645 this._installPrevAndNextSnapshots(); |
|
646 this._lastSwipeDir = ""; |
|
647 } |
|
648 } |
|
649 this.updateAnimation(0); |
|
650 }, |
|
651 |
|
652 /** |
|
653 * Stops the swipe animation. |
|
654 */ |
|
655 stopAnimation: function HSA_stopAnimation() { |
|
656 gHistorySwipeAnimation._removeBoxes(); |
|
657 this._historyIndex = gBrowser.webNavigation.sessionHistory.index; |
|
658 }, |
|
659 |
|
660 /** |
|
661 * Updates the animation between two pages in history. |
|
662 * |
|
663 * @param aVal |
|
664 * A floating point value that represents the progress of the |
|
665 * swipe gesture. |
|
666 */ |
|
667 updateAnimation: function HSA_updateAnimation(aVal) { |
|
668 if (!this.isAnimationRunning()) { |
|
669 return; |
|
670 } |
|
671 |
|
672 // We use the following value to decrease the bounce effect when scrolling |
|
673 // to the top or bottom of the page, or when swiping back/forward past the |
|
674 // browsing history. This value was determined experimentally. |
|
675 let dampValue = 4; |
|
676 if (this._direction == "vertical") { |
|
677 this._prevBox.collapsed = true; |
|
678 this._nextBox.collapsed = true; |
|
679 this._positionBox(this._curBox, -1 * aVal / dampValue); |
|
680 } else if ((aVal >= 0 && this.isLTR) || |
|
681 (aVal <= 0 && !this.isLTR)) { |
|
682 let tempDampValue = 1; |
|
683 if (this._canGoBack) { |
|
684 this._prevBox.collapsed = false; |
|
685 } else { |
|
686 tempDampValue = dampValue; |
|
687 this._prevBox.collapsed = true; |
|
688 } |
|
689 |
|
690 // The current page is pushed to the right (LTR) or left (RTL), |
|
691 // the intention is to go back. |
|
692 // If there is a page to go back to, it should show in the background. |
|
693 this._positionBox(this._curBox, aVal / tempDampValue); |
|
694 |
|
695 // The forward page should be pushed offscreen all the way to the right. |
|
696 this._positionBox(this._nextBox, 1); |
|
697 } else { |
|
698 // The intention is to go forward. If there is a page to go forward to, |
|
699 // it should slide in from the right (LTR) or left (RTL). |
|
700 // Otherwise, the current page should slide to the left (LTR) or |
|
701 // right (RTL) and the backdrop should appear in the background. |
|
702 // For the backdrop to be visible in that case, the previous page needs |
|
703 // to be hidden (if it exists). |
|
704 if (this._canGoForward) { |
|
705 this._nextBox.collapsed = false; |
|
706 let offset = this.isLTR ? 1 : -1; |
|
707 this._positionBox(this._curBox, 0); |
|
708 this._positionBox(this._nextBox, offset + aVal); |
|
709 } else { |
|
710 this._prevBox.collapsed = true; |
|
711 this._positionBox(this._curBox, aVal / dampValue); |
|
712 } |
|
713 } |
|
714 }, |
|
715 |
|
716 /** |
|
717 * Event handler for events relevant to the history swipe animation. |
|
718 * |
|
719 * @param aEvent |
|
720 * An event to process. |
|
721 */ |
|
722 handleEvent: function HSA_handleEvent(aEvent) { |
|
723 let browser = gBrowser.selectedBrowser; |
|
724 switch (aEvent.type) { |
|
725 case "TabClose": |
|
726 let browserForTab = gBrowser.getBrowserForTab(aEvent.target); |
|
727 this._removeTrackedSnapshot(-1, browserForTab); |
|
728 break; |
|
729 case "DOMModalDialogClosed": |
|
730 this.stopAnimation(); |
|
731 break; |
|
732 case "pageshow": |
|
733 if (aEvent.target == browser.contentDocument) { |
|
734 this.stopAnimation(); |
|
735 } |
|
736 break; |
|
737 case "popstate": |
|
738 if (aEvent.target == browser.contentDocument.defaultView) { |
|
739 this.stopAnimation(); |
|
740 } |
|
741 break; |
|
742 case "pagehide": |
|
743 if (aEvent.target == browser.contentDocument) { |
|
744 // Take and compress a snapshot of a page whenever it's about to be |
|
745 // navigated away from. We already have a snapshot of the page if an |
|
746 // animation is running, so we're left with compressing it. |
|
747 if (!this.isAnimationRunning()) { |
|
748 this._takeSnapshot(); |
|
749 } |
|
750 this._compressSnapshotAtCurrentIndex(); |
|
751 } |
|
752 break; |
|
753 } |
|
754 }, |
|
755 |
|
756 /** |
|
757 * Checks whether the history swipe animation is currently running or not. |
|
758 * |
|
759 * @return true if the animation is currently running, false otherwise. |
|
760 */ |
|
761 isAnimationRunning: function HSA_isAnimationRunning() { |
|
762 return !!this._container; |
|
763 }, |
|
764 |
|
765 /** |
|
766 * Process a swipe event based on the given direction. |
|
767 * |
|
768 * @param aEvent |
|
769 * The swipe event to handle |
|
770 * @param aDir |
|
771 * The direction for the swipe event |
|
772 */ |
|
773 processSwipeEvent: function HSA_processSwipeEvent(aEvent, aDir) { |
|
774 if (aDir == "RIGHT") |
|
775 this._historyIndex += this.isLTR ? 1 : -1; |
|
776 else if (aDir == "LEFT") |
|
777 this._historyIndex += this.isLTR ? -1 : 1; |
|
778 else |
|
779 return; |
|
780 this._lastSwipeDir = aDir; |
|
781 }, |
|
782 |
|
783 /** |
|
784 * Checks if there is a page in the browser history to go back to. |
|
785 * |
|
786 * @return true if there is a previous page in history, false otherwise. |
|
787 */ |
|
788 canGoBack: function HSA_canGoBack() { |
|
789 if (this.isAnimationRunning()) |
|
790 return this._doesIndexExistInHistory(this._historyIndex - 1); |
|
791 return gBrowser.webNavigation.canGoBack; |
|
792 }, |
|
793 |
|
794 /** |
|
795 * Checks if there is a page in the browser history to go forward to. |
|
796 * |
|
797 * @return true if there is a next page in history, false otherwise. |
|
798 */ |
|
799 canGoForward: function HSA_canGoForward() { |
|
800 if (this.isAnimationRunning()) |
|
801 return this._doesIndexExistInHistory(this._historyIndex + 1); |
|
802 return gBrowser.webNavigation.canGoForward; |
|
803 }, |
|
804 |
|
805 /** |
|
806 * Used to notify the history swipe animation that the OS sent a swipe end |
|
807 * event and that we should navigate to the page that the user swiped to, if |
|
808 * any. This will also result in the animation overlay to be torn down. |
|
809 */ |
|
810 swipeEndEventReceived: function HSA_swipeEndEventReceived() { |
|
811 if (this._lastSwipeDir != "" && this._historyIndex != this._startingIndex) |
|
812 this._navigateToHistoryIndex(); |
|
813 else |
|
814 this.stopAnimation(); |
|
815 }, |
|
816 |
|
817 /** |
|
818 * Checks whether a particular index exists in the browser history or not. |
|
819 * |
|
820 * @param aIndex |
|
821 * The index to check for availability for in the history. |
|
822 * @return true if the index exists in the browser history, false otherwise. |
|
823 */ |
|
824 _doesIndexExistInHistory: function HSA__doesIndexExistInHistory(aIndex) { |
|
825 try { |
|
826 gBrowser.webNavigation.sessionHistory.getEntryAtIndex(aIndex, false); |
|
827 } |
|
828 catch(ex) { |
|
829 return false; |
|
830 } |
|
831 return true; |
|
832 }, |
|
833 |
|
834 /** |
|
835 * Navigates to the index in history that is currently being tracked by |
|
836 * |this|. |
|
837 */ |
|
838 _navigateToHistoryIndex: function HSA__navigateToHistoryIndex() { |
|
839 if (this._doesIndexExistInHistory(this._historyIndex)) |
|
840 gBrowser.webNavigation.gotoIndex(this._historyIndex); |
|
841 else |
|
842 this.stopAnimation(); |
|
843 }, |
|
844 |
|
845 /** |
|
846 * Checks to see if history swipe animations are supported by this |
|
847 * platform/configuration. |
|
848 * |
|
849 * return true if supported, false otherwise. |
|
850 */ |
|
851 _isSupported: function HSA__isSupported() { |
|
852 return window.matchMedia("(-moz-swipe-animation-enabled)").matches; |
|
853 }, |
|
854 |
|
855 /** |
|
856 * Handle fast swiping (i.e. a swipe animation is already in |
|
857 * progress when a new one is initiated). This will swap out the snapshots |
|
858 * used in the previous animation with the appropriate new ones. |
|
859 */ |
|
860 _handleFastSwiping: function HSA__handleFastSwiping() { |
|
861 this._installCurrentPageSnapshot(null); |
|
862 this._installPrevAndNextSnapshots(); |
|
863 }, |
|
864 |
|
865 /** |
|
866 * Adds the boxes that contain the snapshots used during the swipe animation. |
|
867 */ |
|
868 _addBoxes: function HSA__addBoxes() { |
|
869 let browserStack = |
|
870 document.getAnonymousElementByAttribute(gBrowser.getNotificationBox(), |
|
871 "class", "browserStack"); |
|
872 this._container = this._createElement("historySwipeAnimationContainer", |
|
873 "stack"); |
|
874 browserStack.appendChild(this._container); |
|
875 |
|
876 this._prevBox = this._createElement("historySwipeAnimationPreviousPage", |
|
877 "box"); |
|
878 this._container.appendChild(this._prevBox); |
|
879 |
|
880 this._curBox = this._createElement("historySwipeAnimationCurrentPage", |
|
881 "box"); |
|
882 this._container.appendChild(this._curBox); |
|
883 |
|
884 this._nextBox = this._createElement("historySwipeAnimationNextPage", |
|
885 "box"); |
|
886 this._container.appendChild(this._nextBox); |
|
887 |
|
888 // Cache width and height. |
|
889 this._boxWidth = this._curBox.getBoundingClientRect().width; |
|
890 this._boxHeight = this._curBox.getBoundingClientRect().height; |
|
891 }, |
|
892 |
|
893 /** |
|
894 * Removes the boxes. |
|
895 */ |
|
896 _removeBoxes: function HSA__removeBoxes() { |
|
897 this._curBox = null; |
|
898 this._prevBox = null; |
|
899 this._nextBox = null; |
|
900 if (this._container) |
|
901 this._container.parentNode.removeChild(this._container); |
|
902 this._container = null; |
|
903 this._boxWidth = -1; |
|
904 this._boxHeight = -1; |
|
905 }, |
|
906 |
|
907 /** |
|
908 * Creates an element with a given identifier and tag name. |
|
909 * |
|
910 * @param aID |
|
911 * An identifier to create the element with. |
|
912 * @param aTagName |
|
913 * The name of the tag to create the element for. |
|
914 * @return the newly created element. |
|
915 */ |
|
916 _createElement: function HSA__createElement(aID, aTagName) { |
|
917 let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; |
|
918 let element = document.createElementNS(XULNS, aTagName); |
|
919 element.id = aID; |
|
920 return element; |
|
921 }, |
|
922 |
|
923 /** |
|
924 * Moves a given box to a given X coordinate position. |
|
925 * |
|
926 * @param aBox |
|
927 * The box element to position. |
|
928 * @param aPosition |
|
929 * The position (in X coordinates) to move the box element to. |
|
930 */ |
|
931 _positionBox: function HSA__positionBox(aBox, aPosition) { |
|
932 let transform = ""; |
|
933 |
|
934 if (this._direction == "vertical") |
|
935 transform = "translateY(" + this._boxHeight * aPosition + "px)"; |
|
936 else |
|
937 transform = "translateX(" + this._boxWidth * aPosition + "px)"; |
|
938 |
|
939 aBox.style.transform = transform; |
|
940 }, |
|
941 |
|
942 /** |
|
943 * Verifies that we're ready to take snapshots based on the global pref and |
|
944 * the current index in history. |
|
945 * |
|
946 * @return true if we're ready to take snapshots, false otherwise. |
|
947 */ |
|
948 _readyToTakeSnapshots: function HSA__readyToTakeSnapshots() { |
|
949 if ((this._maxSnapshots < 1) || |
|
950 (gBrowser.webNavigation.sessionHistory.index < 0)) { |
|
951 return false; |
|
952 } |
|
953 return true; |
|
954 }, |
|
955 |
|
956 /** |
|
957 * Takes a snapshot of the page the browser is currently on. |
|
958 */ |
|
959 _takeSnapshot: function HSA__takeSnapshot() { |
|
960 if (!this._readyToTakeSnapshots()) { |
|
961 return; |
|
962 } |
|
963 |
|
964 let canvas = null; |
|
965 |
|
966 TelemetryStopwatch.start("FX_GESTURE_TAKE_SNAPSHOT_OF_PAGE"); |
|
967 try { |
|
968 let browser = gBrowser.selectedBrowser; |
|
969 let r = browser.getBoundingClientRect(); |
|
970 canvas = document.createElementNS("http://www.w3.org/1999/xhtml", |
|
971 "canvas"); |
|
972 canvas.mozOpaque = true; |
|
973 let scale = window.devicePixelRatio; |
|
974 canvas.width = r.width * scale; |
|
975 canvas.height = r.height * scale; |
|
976 let ctx = canvas.getContext("2d"); |
|
977 let zoom = browser.markupDocumentViewer.fullZoom * scale; |
|
978 ctx.scale(zoom, zoom); |
|
979 ctx.drawWindow(browser.contentWindow, |
|
980 0, 0, canvas.width / zoom, canvas.height / zoom, "white", |
|
981 ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_VIEW | |
|
982 ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES | |
|
983 ctx.DRAWWINDOW_USE_WIDGET_LAYERS); |
|
984 } finally { |
|
985 TelemetryStopwatch.finish("FX_GESTURE_TAKE_SNAPSHOT_OF_PAGE"); |
|
986 } |
|
987 |
|
988 TelemetryStopwatch.start("FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE"); |
|
989 try { |
|
990 this._installCurrentPageSnapshot(canvas); |
|
991 this._assignSnapshotToCurrentBrowser(canvas); |
|
992 } finally { |
|
993 TelemetryStopwatch.finish("FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE"); |
|
994 } |
|
995 }, |
|
996 |
|
997 /** |
|
998 * Retrieves the maximum number of snapshots that should be kept in memory. |
|
999 * This limit is a global limit and is valid across all open tabs. |
|
1000 */ |
|
1001 _getMaxSnapshots: function HSA__getMaxSnapshots() { |
|
1002 return gPrefService.getIntPref("browser.snapshots.limit"); |
|
1003 }, |
|
1004 |
|
1005 /** |
|
1006 * Adds a snapshot to the list and initiates the compression of said snapshot. |
|
1007 * Once the compression is completed, it will replace the uncompressed |
|
1008 * snapshot in the list. |
|
1009 * |
|
1010 * @param aCanvas |
|
1011 * The snapshot to add to the list and compress. |
|
1012 */ |
|
1013 _assignSnapshotToCurrentBrowser: |
|
1014 function HSA__assignSnapshotToCurrentBrowser(aCanvas) { |
|
1015 let browser = gBrowser.selectedBrowser; |
|
1016 let currIndex = browser.webNavigation.sessionHistory.index; |
|
1017 |
|
1018 this._removeTrackedSnapshot(currIndex, browser); |
|
1019 this._addSnapshotRefToArray(currIndex, browser); |
|
1020 |
|
1021 if (!("snapshots" in browser)) |
|
1022 browser.snapshots = []; |
|
1023 let snapshots = browser.snapshots; |
|
1024 // Temporarily store the canvas as the compressed snapshot. |
|
1025 // This avoids a blank page if the user swipes quickly |
|
1026 // between pages before the compression could complete. |
|
1027 snapshots[currIndex] = { |
|
1028 image: aCanvas, |
|
1029 scale: window.devicePixelRatio |
|
1030 }; |
|
1031 }, |
|
1032 |
|
1033 /** |
|
1034 * Compresses the HTMLCanvasElement that's stored at the current history |
|
1035 * index in the snapshot array and stores the compressed image in its place. |
|
1036 */ |
|
1037 _compressSnapshotAtCurrentIndex: |
|
1038 function HSA__compressSnapshotAtCurrentIndex() { |
|
1039 if (!this._readyToTakeSnapshots()) { |
|
1040 // We didn't take a snapshot earlier because we weren't ready to, so |
|
1041 // there's nothing to compress. |
|
1042 return; |
|
1043 } |
|
1044 |
|
1045 TelemetryStopwatch.start("FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE"); |
|
1046 try { |
|
1047 let browser = gBrowser.selectedBrowser; |
|
1048 let snapshots = browser.snapshots; |
|
1049 let currIndex = browser.webNavigation.sessionHistory.index; |
|
1050 |
|
1051 // Kick off snapshot compression. |
|
1052 let canvas = snapshots[currIndex].image; |
|
1053 canvas.toBlob(function(aBlob) { |
|
1054 if (snapshots[currIndex]) { |
|
1055 snapshots[currIndex].image = aBlob; |
|
1056 } |
|
1057 }, "image/png" |
|
1058 ); |
|
1059 } finally { |
|
1060 TelemetryStopwatch.finish("FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE"); |
|
1061 } |
|
1062 }, |
|
1063 |
|
1064 /** |
|
1065 * Removes a snapshot identified by the browser and index in the array of |
|
1066 * snapshots for that browser, if present. If no snapshot could be identified |
|
1067 * the method simply returns without taking any action. If aIndex is negative, |
|
1068 * all snapshots for a particular browser will be removed. |
|
1069 * |
|
1070 * @param aIndex |
|
1071 * The index in history of the new snapshot, or negative value if all |
|
1072 * snapshots for a browser should be removed. |
|
1073 * @param aBrowser |
|
1074 * The browser the new snapshot was taken in. |
|
1075 */ |
|
1076 _removeTrackedSnapshot: function HSA__removeTrackedSnapshot(aIndex, aBrowser) { |
|
1077 let arr = this._trackedSnapshots; |
|
1078 let requiresExactIndexMatch = aIndex >= 0; |
|
1079 for (let i = 0; i < arr.length; i++) { |
|
1080 if ((arr[i].browser == aBrowser) && |
|
1081 (aIndex < 0 || aIndex == arr[i].index)) { |
|
1082 delete aBrowser.snapshots[arr[i].index]; |
|
1083 arr.splice(i, 1); |
|
1084 if (requiresExactIndexMatch) |
|
1085 return; // Found and removed the only element. |
|
1086 i--; // Make sure to revisit the index that we just removed an |
|
1087 // element at. |
|
1088 } |
|
1089 } |
|
1090 }, |
|
1091 |
|
1092 /** |
|
1093 * Adds a new snapshot reference for a given index and browser to the array |
|
1094 * of references to tracked snapshots. |
|
1095 * |
|
1096 * @param aIndex |
|
1097 * The index in history of the new snapshot. |
|
1098 * @param aBrowser |
|
1099 * The browser the new snapshot was taken in. |
|
1100 */ |
|
1101 _addSnapshotRefToArray: |
|
1102 function HSA__addSnapshotRefToArray(aIndex, aBrowser) { |
|
1103 let id = { index: aIndex, |
|
1104 browser: aBrowser }; |
|
1105 let arr = this._trackedSnapshots; |
|
1106 arr.unshift(id); |
|
1107 |
|
1108 while (arr.length > this._maxSnapshots) { |
|
1109 let lastElem = arr[arr.length - 1]; |
|
1110 delete lastElem.browser.snapshots[lastElem.index].image; |
|
1111 delete lastElem.browser.snapshots[lastElem.index]; |
|
1112 arr.splice(-1, 1); |
|
1113 } |
|
1114 }, |
|
1115 |
|
1116 /** |
|
1117 * Converts a compressed blob to an Image object. In some situations |
|
1118 * (especially during fast swiping) aBlob may still be a canvas, not a |
|
1119 * compressed blob. In this case, we simply return the canvas. |
|
1120 * |
|
1121 * @param aBlob |
|
1122 * The compressed blob to convert, or a canvas if a blob compression |
|
1123 * couldn't complete before this method was called. |
|
1124 * @return A new Image object representing the converted blob. |
|
1125 */ |
|
1126 _convertToImg: function HSA__convertToImg(aBlob) { |
|
1127 if (!aBlob) |
|
1128 return null; |
|
1129 |
|
1130 // Return aBlob if it's still a canvas and not a compressed blob yet. |
|
1131 if (aBlob instanceof HTMLCanvasElement) |
|
1132 return aBlob; |
|
1133 |
|
1134 let img = new Image(); |
|
1135 let url = ""; |
|
1136 try { |
|
1137 url = URL.createObjectURL(aBlob); |
|
1138 img.onload = function() { |
|
1139 URL.revokeObjectURL(url); |
|
1140 }; |
|
1141 } |
|
1142 finally { |
|
1143 img.src = url; |
|
1144 return img; |
|
1145 } |
|
1146 }, |
|
1147 |
|
1148 /** |
|
1149 * Scales the background of a given box element (which uses a given snapshot |
|
1150 * as background) based on a given scale factor. |
|
1151 * @param aSnapshot |
|
1152 * The snapshot that is used as background of aBox. |
|
1153 * @param aScale |
|
1154 * The scale factor to use. |
|
1155 * @param aBox |
|
1156 * The box element that uses aSnapshot as background. |
|
1157 */ |
|
1158 _scaleSnapshot: function HSA__scaleSnapshot(aSnapshot, aScale, aBox) { |
|
1159 if (aSnapshot && aScale != 1 && aBox) { |
|
1160 if (aSnapshot instanceof HTMLCanvasElement) { |
|
1161 aBox.style.backgroundSize = |
|
1162 aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px"; |
|
1163 } else { |
|
1164 // snapshot is instanceof HTMLImageElement |
|
1165 aSnapshot.addEventListener("load", function() { |
|
1166 aBox.style.backgroundSize = |
|
1167 aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px"; |
|
1168 }); |
|
1169 } |
|
1170 } |
|
1171 }, |
|
1172 |
|
1173 /** |
|
1174 * Sets the snapshot of the current page to the snapshot passed as parameter, |
|
1175 * or to the one previously stored for the current index in history if the |
|
1176 * parameter is null. |
|
1177 * |
|
1178 * @param aCanvas |
|
1179 * The snapshot to set the current page to. If this parameter is null, |
|
1180 * the previously stored snapshot for this index (if any) will be used. |
|
1181 */ |
|
1182 _installCurrentPageSnapshot: |
|
1183 function HSA__installCurrentPageSnapshot(aCanvas) { |
|
1184 let currSnapshot = aCanvas; |
|
1185 let scale = window.devicePixelRatio; |
|
1186 if (!currSnapshot) { |
|
1187 let snapshots = gBrowser.selectedBrowser.snapshots || {}; |
|
1188 let currIndex = this._historyIndex; |
|
1189 if (currIndex in snapshots) { |
|
1190 currSnapshot = this._convertToImg(snapshots[currIndex].image); |
|
1191 scale = snapshots[currIndex].scale; |
|
1192 } |
|
1193 } |
|
1194 this._scaleSnapshot(currSnapshot, scale, this._curBox ? this._curBox : |
|
1195 null); |
|
1196 document.mozSetImageElement("historySwipeAnimationCurrentPageSnapshot", |
|
1197 currSnapshot); |
|
1198 }, |
|
1199 |
|
1200 /** |
|
1201 * Sets the snapshots of the previous and next pages to the snapshots |
|
1202 * previously stored for their respective indeces. |
|
1203 */ |
|
1204 _installPrevAndNextSnapshots: |
|
1205 function HSA__installPrevAndNextSnapshots() { |
|
1206 let snapshots = gBrowser.selectedBrowser.snapshots || []; |
|
1207 let currIndex = this._historyIndex; |
|
1208 let prevIndex = currIndex - 1; |
|
1209 let prevSnapshot = null; |
|
1210 if (prevIndex in snapshots) { |
|
1211 prevSnapshot = this._convertToImg(snapshots[prevIndex].image); |
|
1212 this._scaleSnapshot(prevSnapshot, snapshots[prevIndex].scale, |
|
1213 this._prevBox); |
|
1214 } |
|
1215 document.mozSetImageElement("historySwipeAnimationPreviousPageSnapshot", |
|
1216 prevSnapshot); |
|
1217 |
|
1218 let nextIndex = currIndex + 1; |
|
1219 let nextSnapshot = null; |
|
1220 if (nextIndex in snapshots) { |
|
1221 nextSnapshot = this._convertToImg(snapshots[nextIndex].image); |
|
1222 this._scaleSnapshot(nextSnapshot, snapshots[nextIndex].scale, |
|
1223 this._nextBox); |
|
1224 } |
|
1225 document.mozSetImageElement("historySwipeAnimationNextPageSnapshot", |
|
1226 nextSnapshot); |
|
1227 }, |
|
1228 }; |