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/. */
5 // This file is loaded into the browser window scope.
6 /* eslint-env mozilla/browser-window */
8 // Simple gestures support
10 // As per bug #412486, web content must not be allowed to receive any
11 // simple gesture events. Multi-touch gesture APIs are in their
12 // infancy and we do NOT want to be forced into supporting an API that
13 // will probably have to change in the future. (The current Mac OS X
14 // API is undocumented and was reverse-engineered.) Until support is
15 // implemented in the event dispatcher to keep these events as
16 // chrome-only, we must listen for the simple gesture events during
17 // the capturing phase and call stopPropagation on every event.
19 var gGestureSupport = {
22 _rotateMomentumThreshold: 0.75,
25 * Add or remove mouse gesture event listeners
28 * True to add/init listeners and false to remove/uninit
30 init: function GS_init(aAddListener) {
31 const gestureEvents = [
32 "SwipeGestureMayStart",
37 "MagnifyGestureStart",
38 "MagnifyGestureUpdate",
41 "RotateGestureUpdate",
47 for (let event of gestureEvents) {
49 gBrowser.tabbox.addEventListener("Moz" + event, this, true);
51 gBrowser.tabbox.removeEventListener("Moz" + event, this, true);
57 * Dispatch events based on the type of mouse gesture event. For now, make
58 * sure to stop propagation of every gesture event so that web content cannot
59 * receive gesture events.
62 * The gesture event to handle
64 handleEvent: function GS_handleEvent(aEvent) {
66 !Services.prefs.getBoolPref(
67 "dom.debug.propagate_gesture_events_through_content"
70 aEvent.stopPropagation();
73 // Create a preference object with some defaults
74 let def = (aThreshold, aLatched) => ({
75 threshold: aThreshold,
79 switch (aEvent.type) {
80 case "MozSwipeGestureMayStart":
81 if (this._shouldDoSwipeGesture(aEvent)) {
82 aEvent.preventDefault();
85 case "MozSwipeGestureStart":
86 aEvent.preventDefault();
87 this._setupSwipeGesture();
89 case "MozSwipeGestureUpdate":
90 aEvent.preventDefault();
91 this._doUpdate(aEvent);
93 case "MozSwipeGestureEnd":
94 aEvent.preventDefault();
97 case "MozSwipeGesture":
98 aEvent.preventDefault();
101 case "MozMagnifyGestureStart":
102 aEvent.preventDefault();
103 this._setupGesture(aEvent, "pinch", def(25, 0), "out", "in");
105 case "MozRotateGestureStart":
106 aEvent.preventDefault();
107 this._setupGesture(aEvent, "twist", def(25, 0), "right", "left");
109 case "MozMagnifyGestureUpdate":
110 case "MozRotateGestureUpdate":
111 aEvent.preventDefault();
112 this._doUpdate(aEvent);
114 case "MozTapGesture":
115 aEvent.preventDefault();
116 this._doAction(aEvent, ["tap"]);
118 case "MozRotateGesture":
119 aEvent.preventDefault();
120 this._doAction(aEvent, ["twist", "end"]);
122 /* case "MozPressTapGesture":
128 * Called at the start of "pinch" and "twist" gestures to setup all of the
129 * information needed to process the gesture
132 * The continual motion start event to handle
134 * Name of the gesture to handle
136 * Preference object with the names of preferences and defaults
138 * Command to trigger for increasing motion (without gesture name)
140 * Command to trigger for decreasing motion (without gesture name)
142 _setupGesture: function GS__setupGesture(
149 // Try to load user-set values from preferences
150 for (let [pref, def] of Object.entries(aPref)) {
151 aPref[pref] = this._getPref(aGesture + "." + pref, def);
154 // Keep track of the total deltas and latching behavior
156 let latchDir = aEvent.delta > 0 ? 1 : -1;
157 let isLatched = false;
159 // Create the update function here to capture closure state
160 this._doUpdate = function GS__doUpdate(updateEvent) {
161 // Update the offset with new event data
162 offset += updateEvent.delta;
164 // Check if the cumulative deltas exceed the threshold
165 if (Math.abs(offset) > aPref.threshold) {
166 // Trigger the action if we don't care about latching; otherwise, make
167 // sure either we're not latched and going the same direction of the
168 // initial motion; or we're latched and going the opposite way
169 let sameDir = (latchDir ^ offset) >= 0;
170 if (!aPref.latched || isLatched ^ sameDir) {
171 this._doAction(updateEvent, [aGesture, offset > 0 ? aInc : aDec]);
173 // We must be getting latched or leaving it, so just toggle
174 isLatched = !isLatched;
177 // Reset motion counter to prepare for more of the same gesture
182 // The start event also contains deltas, so handle an update right away
183 this._doUpdate(aEvent);
187 * Checks whether a swipe gesture event can navigate the browser history or
191 * The swipe gesture event.
192 * @return true if the swipe event may navigate the history, false othwerwise.
194 _swipeNavigatesHistory: function GS__swipeNavigatesHistory(aEvent) {
196 this._getCommand(aEvent, ["swipe", "left"]) ==
197 "Browser:BackOrBackDuplicate" &&
198 this._getCommand(aEvent, ["swipe", "right"]) ==
199 "Browser:ForwardOrForwardDuplicate"
204 * Checks whether we want to start a swipe for aEvent and sets
205 * aEvent.allowedDirections to the right values.
208 * The swipe gesture "MayStart" event.
209 * @return true if we're willing to start a swipe for this event, false
212 _shouldDoSwipeGesture: function GS__shouldDoSwipeGesture(aEvent) {
213 if (!this._swipeNavigatesHistory(aEvent)) {
217 let isVerticalSwipe = false;
218 if (aEvent.direction == aEvent.DIRECTION_UP) {
219 if (gMultiProcessBrowser || window.content.pageYOffset > 0) {
222 isVerticalSwipe = true;
223 } else if (aEvent.direction == aEvent.DIRECTION_DOWN) {
225 gMultiProcessBrowser ||
226 window.content.pageYOffset < window.content.scrollMaxY
230 isVerticalSwipe = true;
232 if (isVerticalSwipe) {
233 // Vertical overscroll has been temporarily disabled until bug 939480 is
238 let canGoBack = gHistorySwipeAnimation.canGoBack();
239 let canGoForward = gHistorySwipeAnimation.canGoForward();
240 let isLTR = gHistorySwipeAnimation.isLTR;
243 aEvent.allowedDirections |= isLTR
244 ? aEvent.DIRECTION_LEFT
245 : aEvent.DIRECTION_RIGHT;
248 aEvent.allowedDirections |= isLTR
249 ? aEvent.DIRECTION_RIGHT
250 : aEvent.DIRECTION_LEFT;
253 return canGoBack || canGoForward;
257 * Sets up swipe gestures. This includes setting up swipe animations for the
258 * gesture, if enabled.
261 * The swipe gesture start event.
262 * @return true if swipe gestures could successfully be set up, false
265 _setupSwipeGesture: function GS__setupSwipeGesture() {
266 gHistorySwipeAnimation.startAnimation();
268 this._doUpdate = function GS__doUpdate(aEvent) {
269 gHistorySwipeAnimation.updateAnimation(aEvent.delta);
272 this._doEnd = function GS__doEnd(aEvent) {
273 gHistorySwipeAnimation.swipeEndEventReceived();
275 this._doUpdate = function() {};
276 this._doEnd = function() {};
281 * Generator producing the powerset of the input array where the first result
282 * is the complete set and the last result (before StopIteration) is empty.
285 * Source array containing any number of elements
286 * @yield Array that is a subset of the input array from full set to empty
288 _power: function* GS__power(aArray) {
289 // Create a bitmask based on the length of the array
290 let num = 1 << aArray.length;
292 // Only select array elements where the current bit is set
293 yield aArray.reduce(function(aPrev, aCurr, aIndex) {
294 if (num & (1 << aIndex)) {
303 * Determine what action to do for the gesture based on which keys are
304 * pressed and which commands are set, and execute the command.
307 * The original gesture event to convert into a fake click event
309 * Array of gesture name parts (to be joined by periods)
310 * @return Name of the executed command. Returns null if no command is
313 _doAction: function GS__doAction(aEvent, aGesture) {
314 let command = this._getCommand(aEvent, aGesture);
315 return command && this._doCommand(aEvent, command);
319 * Determine what action to do for the gesture based on which keys are
320 * pressed and which commands are set
323 * The original gesture event to convert into a fake click event
325 * Array of gesture name parts (to be joined by periods)
327 _getCommand: function GS__getCommand(aEvent, aGesture) {
328 // Create an array of pressed keys in a fixed order so that a command for
329 // "meta" is preferred over "ctrl" when both buttons are pressed (and a
330 // command for both don't exist)
332 for (let key of ["shift", "alt", "ctrl", "meta"]) {
333 if (aEvent[key + "Key"]) {
338 // Try each combination of key presses in decreasing order for commands
339 for (let subCombo of this._power(keyCombos)) {
340 // Convert a gesture and pressed keys into the corresponding command
341 // action where the preference has the gesture before "shift" before
342 // "alt" before "ctrl" before "meta" all separated by periods
345 command = this._getPref(aGesture.concat(subCombo).join("."));
356 * Execute the specified command.
359 * The original gesture event to convert into a fake click event
361 * Name of the command found for the event's keys and gesture.
363 _doCommand: function GS__doCommand(aEvent, aCommand) {
364 let node = document.getElementById(aCommand);
366 if (node.getAttribute("disabled") != "true") {
367 let cmdEvent = document.createEvent("xulcommandevent");
368 cmdEvent.initCommandEvent(
380 aEvent.mozInputSource
382 node.dispatchEvent(cmdEvent);
385 goDoCommand(aCommand);
390 * Handle continual motion events. This function will be set by
391 * _setupGesture or _setupSwipe.
394 * The continual motion update event to handle
396 _doUpdate(aEvent) {},
399 * Handle gesture end events. This function will be set by _setupSwipe.
402 * The gesture end event to handle
407 * Convert the swipe gesture into a browser action based on the direction.
410 * The swipe event to handle
412 onSwipe: function GS_onSwipe(aEvent) {
413 // Figure out which one (and only one) direction was triggered
414 for (let dir of ["UP", "RIGHT", "DOWN", "LEFT"]) {
415 if (aEvent.direction == aEvent["DIRECTION_" + dir]) {
416 this._coordinateSwipeEventWithAnimation(aEvent, dir);
423 * Process a swipe event based on the given direction.
426 * The swipe event to handle
428 * The direction for the swipe event
430 processSwipeEvent: function GS_processSwipeEvent(aEvent, aDir) {
431 let dir = aDir.toLowerCase();
432 // This is a bit of a hack. Ideally we would like our pref names to not
433 // associate a direction (eg left) with a history action (eg back), and
434 // instead name them something like HistoryLeft/Right and then intercept
435 // that in this file and turn it into the back or forward command, but
436 // that involves sending whether we are in LTR or not into _doAction and
437 // _getCommand and then having them recognize that these command needs to
438 // be interpreted differently for rtl/ltr (but not other commands), which
439 // seems more brittle (have to keep all the places in sync) and more code.
440 // So we'll just live with presenting the wrong semantics in the prefs.
441 if (!gHistorySwipeAnimation.isLTR) {
442 if (dir == "right") {
444 } else if (dir == "left") {
448 this._doAction(aEvent, ["swipe", dir]);
452 * Coordinates the swipe event with the swipe animation, if any.
453 * If an animation is currently running, the swipe event will be
454 * processed once the animation stops. This will guarantee a fluid
455 * motion of the animation.
458 * The swipe event to handle
460 * The direction for the swipe event
462 _coordinateSwipeEventWithAnimation: function GS__coordinateSwipeEventWithAnimation(
466 gHistorySwipeAnimation.stopAnimation();
467 this.processSwipeEvent(aEvent, aDir);
471 * Get a gesture preference or use a default if it doesn't exist
474 * Name of the preference to load under the gesture branch
476 * Default value if the preference doesn't exist
478 _getPref: function GS__getPref(aPref, aDef) {
479 // Preferences branch under which all gestures preferences are stored
480 const branch = "browser.gesture.";
483 // Determine what type of data to load based on default value's type
484 let type = typeof aDef;
485 let getFunc = "Char";
486 if (type == "boolean") {
488 } else if (type == "number") {
491 return Services.prefs["get" + getFunc + "Pref"](branch + aPref);
498 * Perform rotation for ImageDocuments
501 * The MozRotateGestureUpdate event triggering this call
504 if (!ImageDocument.isInstance(window.content.document)) {
508 let contentElement = window.content.document.body.firstElementChild;
509 if (!contentElement) {
512 // If we're currently snapping, cancel that snap
513 if (contentElement.classList.contains("completeRotation")) {
514 this._clearCompleteRotation();
517 this.rotation = Math.round(this.rotation + aEvent.delta);
518 contentElement.style.transform = "rotate(" + this.rotation + "deg)";
519 this._lastRotateDelta = aEvent.delta;
523 * Perform a rotation end for ImageDocuments
526 if (!ImageDocument.isInstance(window.content.document)) {
530 let contentElement = window.content.document.body.firstElementChild;
531 if (!contentElement) {
535 let transitionRotation = 0;
537 // The reason that 360 is allowed here is because when rotating between
538 // 315 and 360, setting rotate(0deg) will cause it to rotate the wrong
539 // direction around--spinning wildly.
540 if (this.rotation <= 45) {
541 transitionRotation = 0;
542 } else if (this.rotation > 45 && this.rotation <= 135) {
543 transitionRotation = 90;
544 } else if (this.rotation > 135 && this.rotation <= 225) {
545 transitionRotation = 180;
546 } else if (this.rotation > 225 && this.rotation <= 315) {
547 transitionRotation = 270;
549 transitionRotation = 360;
552 // If we're going fast enough, and we didn't already snap ahead of rotation,
553 // then snap ahead of rotation to simulate momentum
555 this._lastRotateDelta > this._rotateMomentumThreshold &&
556 this.rotation > transitionRotation
558 transitionRotation += 90;
560 this._lastRotateDelta < -1 * this._rotateMomentumThreshold &&
561 this.rotation < transitionRotation
563 transitionRotation -= 90;
566 // Only add the completeRotation class if it is is necessary
567 if (transitionRotation != this.rotation) {
568 contentElement.classList.add("completeRotation");
569 contentElement.addEventListener(
571 this._clearCompleteRotation
575 contentElement.style.transform = "rotate(" + transitionRotation + "deg)";
576 this.rotation = transitionRotation;
580 * Gets the current rotation for the ImageDocument
583 return this._currentRotation;
587 * Sets the current rotation for the ImageDocument
590 * The new value to take. Can be any value, but it will be bounded to
591 * 0 inclusive to 360 exclusive.
594 this._currentRotation = aVal % 360;
595 if (this._currentRotation < 0) {
596 this._currentRotation += 360;
601 * When the location/tab changes, need to reload the current rotation for the
604 restoreRotationState() {
605 // Bug 1108553 - Cannot rotate images in stand-alone image documents with e10s
606 if (gMultiProcessBrowser) {
610 if (!ImageDocument.isInstance(window.content.document)) {
614 let contentElement = window.content.document.body.firstElementChild;
615 let transformValue = window.content.window.getComputedStyle(contentElement)
618 if (transformValue == "none") {
623 // transformValue is a rotation matrix--split it and do mathemagic to
624 // obtain the real rotation value
625 transformValue = transformValue
629 this.rotation = Math.round(
630 Math.atan2(transformValue[1], transformValue[0]) * (180 / Math.PI)
635 * Removes the transition rule by removing the completeRotation class
637 _clearCompleteRotation() {
639 window.content.document &&
640 ImageDocument.isInstance(window.content.document) &&
641 window.content.document.body &&
642 window.content.document.body.firstElementChild;
643 if (!contentElement) {
646 contentElement.classList.remove("completeRotation");
647 contentElement.removeEventListener(
649 this._clearCompleteRotation
654 // History Swipe Animation Support (bug 678392)
655 var gHistorySwipeAnimation = {
660 * Initializes the support for history swipe animations, if it is supported
661 * by the platform/configuration.
663 init: function HSA_init() {
664 this.isLTR = document.documentElement.matches(":-moz-locale-dir(ltr)");
665 this._isStoppingAnimation = false;
667 if (!this._isSupported()) {
671 this._icon = document.getElementById("swipe-nav-icon");
672 this._initPrefValues();
673 this._addPrefObserver();
678 * Uninitializes the support for history swipe animations.
680 uninit: function HSA_uninit() {
681 this._removePrefObserver();
689 * Starts the swipe animation.
691 * @param aIsVerticalSwipe
692 * Whether we're dealing with a vertical swipe or not.
694 startAnimation: function HSA_startAnimation() {
695 // old boxes can still be around (if completing fade out for example), we
696 // always want to remove them and recreate them because they can be
697 // attached to an old browser stack that's no longer in use.
699 this._isStoppingAnimation = false;
700 this._canGoBack = this.canGoBack();
701 this._canGoForward = this.canGoForward();
705 this.updateAnimation(0);
709 * Stops the swipe animation.
711 stopAnimation: function HSA_stopAnimation() {
712 if (!this.isAnimationRunning() || this._isStoppingAnimation) {
717 if (!this._prevBox.collapsed) {
719 } else if (!this._nextBox.collapsed) {
723 this._isStoppingAnimation = true;
724 box.style.transition = "opacity 0.35s cubic-bezier(.25,.1,0.25,1)";
725 box.addEventListener("transitionend", this, true);
726 box.style.opacity = 0;
728 this._isStoppingAnimation = false;
733 _willGoBack: function HSA_willGoBack(aVal) {
735 ((aVal > 0 && this.isLTR) || (aVal < 0 && !this.isLTR)) && this._canGoBack
739 _willGoForward: function HSA_willGoForward(aVal) {
741 ((aVal > 0 && !this.isLTR) || (aVal < 0 && this.isLTR)) &&
747 * Updates the animation between two pages in history.
750 * A floating point value that represents the progress of the
751 * swipe gesture. History navigation will be triggered if the absolute
752 * value of this `aVal` is greater than or equal to 0.25.
754 updateAnimation: function HSA_updateAnimation(aVal) {
755 if (!this.isAnimationRunning() || this._isStoppingAnimation) {
759 // Convert `aVal` into [0, 1] range.
760 // Note that absolute values of 0.25 (or greater) trigger history
761 // navigation, hence we multiply the value by 4 here.
762 const progress = Math.min(Math.abs(aVal) * 4, 1.0);
764 // Compute the icon position based on preferences.
766 this.translateStartPosition +
767 progress * (this.translateEndPosition - this.translateStartPosition);
769 translate = -translate;
772 // Compute the icon radius based on preferences.
774 this.minRadius + progress * (this.maxRadius - this.minRadius);
775 if (this._willGoBack(aVal)) {
776 this._prevBox.collapsed = false;
777 this._nextBox.collapsed = true;
778 this._prevBox.style.translate = `${translate}px 0px`;
781 .querySelectorAll("circle")[1]
782 .setAttribute("r", `${radius}`);
785 if (Math.abs(aVal) >= 0.25) {
786 // If `aVal` goes above 0.25, it means history navigation will be
787 // triggered once after the user lifts their fingers, it's time to
788 // trigger __indicator__ animations by adding `will-navigate` class.
789 this._prevBox.querySelector("svg").classList.add("will-navigate");
791 this._prevBox.querySelector("svg").classList.remove("will-navigate");
793 } else if (this._willGoForward(aVal)) {
794 // The intention is to go forward.
795 this._nextBox.collapsed = false;
796 this._prevBox.collapsed = true;
797 this._nextBox.style.translate = `${-translate}px 0px`;
800 .querySelectorAll("circle")[1]
801 .setAttribute("r", `${radius}`);
804 if (Math.abs(aVal) >= 0.25) {
805 // Same as above "go back" case.
806 this._nextBox.querySelector("svg").classList.add("will-navigate");
808 this._nextBox.querySelector("svg").classList.remove("will-navigate");
811 this._prevBox.collapsed = true;
812 this._nextBox.collapsed = true;
813 this._prevBox.style.translate = "none";
814 this._nextBox.style.translate = "none";
819 * Checks whether the history swipe animation is currently running or not.
821 * @return true if the animation is currently running, false otherwise.
823 isAnimationRunning: function HSA_isAnimationRunning() {
824 return !!this._container;
828 * Checks if there is a page in the browser history to go back to.
830 * @return true if there is a previous page in history, false otherwise.
832 canGoBack: function HSA_canGoBack() {
833 return gBrowser.webNavigation.canGoBack;
837 * Checks if there is a page in the browser history to go forward to.
839 * @return true if there is a next page in history, false otherwise.
841 canGoForward: function HSA_canGoForward() {
842 return gBrowser.webNavigation.canGoForward;
846 * Used to notify the history swipe animation that the OS sent a swipe end
847 * event and that we should navigate to the page that the user swiped to, if
848 * any. This will also result in the animation overlay to be torn down.
850 swipeEndEventReceived: function HSA_swipeEndEventReceived() {
851 this.stopAnimation();
855 * Checks to see if history swipe animations are supported by this
856 * platform/configuration.
858 * return true if supported, false otherwise.
860 _isSupported: function HSA__isSupported() {
861 return window.matchMedia("(-moz-swipe-animation-enabled)").matches;
864 handleEvent: function HSA_handleEvent(aEvent) {
865 switch (aEvent.type) {
866 case "transitionend":
867 this._completeFadeOut();
872 _completeFadeOut: function HSA__completeFadeOut(aEvent) {
873 if (!this._isStoppingAnimation) {
874 // The animation was restarted in the middle of our stopping fade out
875 // tranistion, so don't do anything.
878 this._isStoppingAnimation = false;
879 gHistorySwipeAnimation._removeBoxes();
883 * Adds the boxes that contain the arrows used during the swipe animation.
885 _addBoxes: function HSA__addBoxes() {
886 let browserStack = gBrowser.getPanel().querySelector(".browserStack");
887 this._container = this._createElement(
888 "historySwipeAnimationContainer",
891 browserStack.appendChild(this._container);
893 this._prevBox = this._createElement(
894 "historySwipeAnimationPreviousArrow",
897 this._prevBox.collapsed = true;
898 this._container.appendChild(this._prevBox);
899 let icon = this._icon.cloneNode(true);
900 icon.classList.add("swipe-nav-icon");
901 this._prevBox.appendChild(icon);
903 this._nextBox = this._createElement(
904 "historySwipeAnimationNextArrow",
907 this._nextBox.collapsed = true;
908 this._container.appendChild(this._nextBox);
909 icon = this._icon.cloneNode(true);
910 icon.classList.add("swipe-nav-icon");
911 this._nextBox.appendChild(icon);
917 _removeBoxes: function HSA__removeBoxes() {
918 this._prevBox = null;
919 this._nextBox = null;
920 if (this._container) {
921 this._container.remove();
923 this._container = null;
927 * Creates an element with a given identifier and tag name.
930 * An identifier to create the element with.
932 * The name of the tag to create the element for.
933 * @return the newly created element.
935 _createElement: function HSA__createElement(aID, aTagName) {
936 let element = document.createXULElement(aTagName);
941 observe(subj, topic, data) {
943 case "nsPref:changed":
944 this._initPrefValues();
948 _initPrefValues: function HSA__initPrefValues() {
949 this.translateStartPosition = Services.prefs.getIntPref(
950 "browser.swipe.navigation-icon-start-position",
953 this.translateEndPosition = Services.prefs.getIntPref(
954 "browser.swipe.navigation-icon-end-position",
957 this.minRadius = Services.prefs.getIntPref(
958 "browser.swipe.navigation-icon-min-radius",
961 this.maxRadius = Services.prefs.getIntPref(
962 "browser.swipe.navigation-icon-max-radius",
967 _addPrefObserver: function HSA__addPrefObserver() {
969 "browser.swipe.navigation-icon-start-position",
970 "browser.swipe.navigation-icon-end-position",
971 "browser.swipe.navigation-icon-min-radius",
972 "browser.swipe.navigation-icon-max-radius",
974 Services.prefs.addObserver(pref, this);
978 _removePrefObserver: function HSA__removePrefObserver() {
980 "browser.swipe.navigation-icon-start-position",
981 "browser.swipe.navigation-icon-end-position",
982 "browser.swipe.navigation-icon-min-radius",
983 "browser.swipe.navigation-icon-max-radius",
985 Services.prefs.removeObserver(pref, this);