summaryrefslogtreecommitdiffstats
path: root/browser/base/content/browser-fullScreenAndPointerLock.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/browser-fullScreenAndPointerLock.js')
-rw-r--r--browser/base/content/browser-fullScreenAndPointerLock.js673
1 files changed, 673 insertions, 0 deletions
diff --git a/browser/base/content/browser-fullScreenAndPointerLock.js b/browser/base/content/browser-fullScreenAndPointerLock.js
new file mode 100644
index 000000000..497e51121
--- /dev/null
+++ b/browser/base/content/browser-fullScreenAndPointerLock.js
@@ -0,0 +1,673 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var PointerlockFsWarning = {
+
+ _element: null,
+ _origin: null,
+
+ init: function() {
+ this.Timeout.prototype = {
+ start: function() {
+ this.cancel();
+ this._id = setTimeout(() => this._handle(), this._delay);
+ },
+ cancel: function() {
+ if (this._id) {
+ clearTimeout(this._id);
+ this._id = 0;
+ }
+ },
+ _handle: function() {
+ this._id = 0;
+ this._func();
+ },
+ get delay() {
+ return this._delay;
+ }
+ };
+ },
+
+ /**
+ * Timeout object for managing timeout request. If it is started when
+ * the previous call hasn't finished, it would automatically cancelled
+ * the previous one.
+ */
+ Timeout: function(func, delay) {
+ this._id = 0;
+ this._func = func;
+ this._delay = delay;
+ },
+
+ showPointerLock: function(aOrigin) {
+ if (!document.fullscreen) {
+ let timeout = gPrefService.getIntPref("pointer-lock-api.warning.timeout");
+ this.show(aOrigin, "pointerlock-warning", timeout, 0);
+ }
+ },
+
+ showFullScreen: function(aOrigin) {
+ let timeout = gPrefService.getIntPref("full-screen-api.warning.timeout");
+ let delay = gPrefService.getIntPref("full-screen-api.warning.delay");
+ this.show(aOrigin, "fullscreen-warning", timeout, delay);
+ },
+
+ // Shows a warning that the site has entered fullscreen or
+ // pointer lock for a short duration.
+ show: function(aOrigin, elementId, timeout, delay) {
+
+ if (!this._element) {
+ this._element = document.getElementById(elementId);
+ // Setup event listeners
+ this._element.addEventListener("transitionend", this);
+ window.addEventListener("mousemove", this, true);
+ // The timeout to hide the warning box after a while.
+ this._timeoutHide = new this.Timeout(() => {
+ this._state = "hidden";
+ }, timeout);
+ // The timeout to show the warning box when the pointer is at the top
+ this._timeoutShow = new this.Timeout(() => {
+ this._state = "ontop";
+ this._timeoutHide.start();
+ }, delay);
+ }
+
+ // Set the strings on the warning UI.
+ if (aOrigin) {
+ this._origin = aOrigin;
+ }
+ let uri = BrowserUtils.makeURI(this._origin);
+ let host = null;
+ try {
+ host = uri.host;
+ } catch (e) { }
+ let textElem = this._element.querySelector(".pointerlockfswarning-domain-text");
+ if (!host) {
+ textElem.setAttribute("hidden", true);
+ } else {
+ textElem.removeAttribute("hidden");
+ let hostElem = this._element.querySelector(".pointerlockfswarning-domain");
+ // Document's principal's URI has a host. Display a warning including it.
+ let utils = {};
+ Cu.import("resource://gre/modules/DownloadUtils.jsm", utils);
+ hostElem.textContent = utils.DownloadUtils.getURIHost(uri.spec)[0];
+ }
+
+ this._element.dataset.identity =
+ gIdentityHandler.pointerlockFsWarningClassName;
+
+ // User should be allowed to explicitly disable
+ // the prompt if they really want.
+ if (this._timeoutHide.delay <= 0) {
+ return;
+ }
+
+ // Explicitly set the last state to hidden to avoid the warning
+ // box being hidden immediately because of mousemove.
+ this._state = "onscreen";
+ this._lastState = "hidden";
+ this._timeoutHide.start();
+ },
+
+ close: function() {
+ if (!this._element) {
+ return;
+ }
+ // Cancel any pending timeout
+ this._timeoutHide.cancel();
+ this._timeoutShow.cancel();
+ // Reset state of the warning box
+ this._state = "hidden";
+ this._element.setAttribute("hidden", true);
+ // Remove all event listeners
+ this._element.removeEventListener("transitionend", this);
+ window.removeEventListener("mousemove", this, true);
+ // Clear fields
+ this._element = null;
+ this._timeoutHide = null;
+ this._timeoutShow = null;
+
+ // Ensure focus switches away from the (now hidden) warning box.
+ // If the user clicked buttons in the warning box, it would have
+ // been focused, and any key events would be directed at the (now
+ // hidden) chrome document instead of the target document.
+ gBrowser.selectedBrowser.focus();
+ },
+
+ // State could be one of "onscreen", "ontop", "hiding", and
+ // "hidden". Setting the state to "onscreen" and "ontop" takes
+ // effect immediately, while setting it to "hidden" actually
+ // turns the state to "hiding" before the transition finishes.
+ _lastState: null,
+ _STATES: ["hidden", "ontop", "onscreen"],
+ get _state() {
+ for (let state of this._STATES) {
+ if (this._element.hasAttribute(state)) {
+ return state;
+ }
+ }
+ return "hiding";
+ },
+ set _state(newState) {
+ let currentState = this._state;
+ if (currentState == newState) {
+ return;
+ }
+ if (currentState != "hiding") {
+ this._lastState = currentState;
+ this._element.removeAttribute(currentState);
+ }
+ if (newState != "hidden") {
+ if (currentState != "hidden") {
+ this._element.setAttribute(newState, true);
+ } else {
+ // When the previous state is hidden, the display was none,
+ // thus no box was constructed. We need to wait for the new
+ // display value taking effect first, otherwise, there won't
+ // be any transition. Since requestAnimationFrame callback is
+ // generally triggered before any style flush and layout, we
+ // should wait for the second animation frame.
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ if (this._element) {
+ this._element.setAttribute(newState, true);
+ }
+ });
+ });
+ }
+ }
+ },
+
+ handleEvent: function(event) {
+ switch (event.type) {
+ case "mousemove": {
+ let state = this._state;
+ if (state == "hidden") {
+ // If the warning box is currently hidden, show it after
+ // a short delay if the pointer is at the top.
+ if (event.clientY != 0) {
+ this._timeoutShow.cancel();
+ } else if (this._timeoutShow.delay >= 0) {
+ this._timeoutShow.start();
+ }
+ } else {
+ let elemRect = this._element.getBoundingClientRect();
+ if (state == "hiding" && this._lastState != "hidden") {
+ // If we are on the hiding transition, and the pointer
+ // moved near the box, restore to the previous state.
+ if (event.clientY <= elemRect.bottom + 50) {
+ this._state = this._lastState;
+ this._timeoutHide.start();
+ }
+ } else if (state == "ontop" || this._lastState != "hidden") {
+ // State being "ontop" or the previous state not being
+ // "hidden" indicates this current warning box is shown
+ // in response to user's action. Hide it immediately when
+ // the pointer leaves that area.
+ if (event.clientY > elemRect.bottom + 50) {
+ this._state = "hidden";
+ this._timeoutHide.cancel();
+ }
+ }
+ }
+ break;
+ }
+ case "transitionend": {
+ if (this._state == "hiding") {
+ this._element.setAttribute("hidden", true);
+ }
+ break;
+ }
+ }
+ }
+};
+
+var PointerLock = {
+
+ init: function() {
+ window.messageManager.addMessageListener("PointerLock:Entered", this);
+ window.messageManager.addMessageListener("PointerLock:Exited", this);
+ },
+
+ receiveMessage: function(aMessage) {
+ switch (aMessage.name) {
+ case "PointerLock:Entered": {
+ PointerlockFsWarning.showPointerLock(aMessage.data.originNoSuffix);
+ break;
+ }
+ case "PointerLock:Exited": {
+ PointerlockFsWarning.close();
+ break;
+ }
+ }
+ }
+};
+
+var FullScreen = {
+ _MESSAGES: [
+ "DOMFullscreen:Request",
+ "DOMFullscreen:NewOrigin",
+ "DOMFullscreen:Exit",
+ "DOMFullscreen:Painted",
+ ],
+
+ init: function() {
+ // called when we go into full screen, even if initiated by a web page script
+ window.addEventListener("fullscreen", this, true);
+ window.addEventListener("MozDOMFullscreen:Entered", this,
+ /* useCapture */ true,
+ /* wantsUntrusted */ false);
+ window.addEventListener("MozDOMFullscreen:Exited", this,
+ /* useCapture */ true,
+ /* wantsUntrusted */ false);
+ for (let type of this._MESSAGES) {
+ window.messageManager.addMessageListener(type, this);
+ }
+
+ if (window.fullScreen)
+ this.toggle();
+ },
+
+ uninit: function() {
+ for (let type of this._MESSAGES) {
+ window.messageManager.removeMessageListener(type, this);
+ }
+ this.cleanup();
+ },
+
+ toggle: function () {
+ var enterFS = window.fullScreen;
+
+ // Toggle the View:FullScreen command, which controls elements like the
+ // fullscreen menuitem, and menubars.
+ let fullscreenCommand = document.getElementById("View:FullScreen");
+ if (enterFS) {
+ fullscreenCommand.setAttribute("checked", enterFS);
+ } else {
+ fullscreenCommand.removeAttribute("checked");
+ }
+
+ if (AppConstants.platform == "macosx") {
+ // Make sure the menu items are adjusted.
+ document.getElementById("enterFullScreenItem").hidden = enterFS;
+ document.getElementById("exitFullScreenItem").hidden = !enterFS;
+ }
+
+ if (!this._fullScrToggler) {
+ this._fullScrToggler = document.getElementById("fullscr-toggler");
+ this._fullScrToggler.addEventListener("mouseover", this._expandCallback, false);
+ this._fullScrToggler.addEventListener("dragenter", this._expandCallback, false);
+ this._fullScrToggler.addEventListener("touchmove", this._expandCallback, {passive: true});
+ }
+
+ if (enterFS) {
+ gNavToolbox.setAttribute("inFullscreen", true);
+ document.documentElement.setAttribute("inFullscreen", true);
+ if (!document.fullscreenElement && this.useLionFullScreen)
+ document.documentElement.setAttribute("OSXLionFullscreen", true);
+ } else {
+ gNavToolbox.removeAttribute("inFullscreen");
+ document.documentElement.removeAttribute("inFullscreen");
+ document.documentElement.removeAttribute("OSXLionFullscreen");
+ }
+
+ if (!document.fullscreenElement)
+ this._updateToolbars(enterFS);
+
+ if (enterFS) {
+ document.addEventListener("keypress", this._keyToggleCallback, false);
+ document.addEventListener("popupshown", this._setPopupOpen, false);
+ document.addEventListener("popuphidden", this._setPopupOpen, false);
+ // In DOM fullscreen mode, we hide toolbars with CSS
+ if (!document.fullscreenElement)
+ this.hideNavToolbox(true);
+ }
+ else {
+ this.showNavToolbox(false);
+ // This is needed if they use the context menu to quit fullscreen
+ this._isPopupOpen = false;
+ this.cleanup();
+ // In TabsInTitlebar._update(), we cancel the appearance update on
+ // resize event for exiting fullscreen, since that happens before we
+ // change the UI here in the "fullscreen" event. Hence we need to
+ // call it here to ensure the appearance is properly updated. See
+ // TabsInTitlebar._update() and bug 1173768.
+ TabsInTitlebar.updateAppearance(true);
+ }
+
+ if (enterFS && !document.fullscreenElement) {
+ Services.telemetry.getHistogramById("FX_BROWSER_FULLSCREEN_USED")
+ .add(1);
+ }
+ },
+
+ exitDomFullScreen : function() {
+ document.exitFullscreen();
+ },
+
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "fullscreen":
+ this.toggle();
+ break;
+ case "MozDOMFullscreen:Entered": {
+ // The event target is the element which requested the DOM
+ // fullscreen. If we were entering DOM fullscreen for a remote
+ // browser, the target would be `gBrowser` and the original
+ // target would be the browser which was the parameter of
+ // `remoteFrameFullscreenChanged` call. If the fullscreen
+ // request was initiated from an in-process browser, we need
+ // to get its corresponding browser here.
+ let browser;
+ if (event.target == gBrowser) {
+ browser = event.originalTarget;
+ } else {
+ let topWin = event.target.ownerGlobal.top;
+ browser = gBrowser.getBrowserForContentWindow(topWin);
+ }
+ TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS");
+ this.enterDomFullscreen(browser);
+ break;
+ }
+ case "MozDOMFullscreen:Exited":
+ TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS");
+ this.cleanupDomFullscreen();
+ break;
+ }
+ },
+
+ receiveMessage: function(aMessage) {
+ let browser = aMessage.target;
+ switch (aMessage.name) {
+ case "DOMFullscreen:Request": {
+ this._windowUtils.remoteFrameFullscreenChanged(browser);
+ break;
+ }
+ case "DOMFullscreen:NewOrigin": {
+ // Don't show the warning if we've already exited fullscreen.
+ if (document.fullscreen) {
+ PointerlockFsWarning.showFullScreen(aMessage.data.originNoSuffix);
+ }
+ break;
+ }
+ case "DOMFullscreen:Exit": {
+ this._windowUtils.remoteFrameFullscreenReverted();
+ break;
+ }
+ case "DOMFullscreen:Painted": {
+ Services.obs.notifyObservers(window, "fullscreen-painted", "");
+ TelemetryStopwatch.finish("FULLSCREEN_CHANGE_MS");
+ break;
+ }
+ }
+ },
+
+ enterDomFullscreen : function(aBrowser) {
+
+ if (!document.fullscreenElement) {
+ return;
+ }
+
+ // If we have a current pointerlock warning shown then hide it
+ // before transition.
+ PointerlockFsWarning.close();
+
+ // If it is a remote browser, send a message to ask the content
+ // to enter fullscreen state. We don't need to do so if it is an
+ // in-process browser, since all related document should have
+ // entered fullscreen state at this point.
+ // This should be done before the active tab check below to ensure
+ // that the content document handles the pending request. Doing so
+ // before the check is fine since we also check the activeness of
+ // the requesting document in content-side handling code.
+ if (this._isRemoteBrowser(aBrowser)) {
+ aBrowser.messageManager.sendAsyncMessage("DOMFullscreen:Entered");
+ }
+
+ // If we've received a fullscreen notification, we have to ensure that the
+ // element that's requesting fullscreen belongs to the browser that's currently
+ // active. If not, we exit fullscreen since the "full-screen document" isn't
+ // actually visible now.
+ if (!aBrowser || gBrowser.selectedBrowser != aBrowser ||
+ // The top-level window has lost focus since the request to enter
+ // full-screen was made. Cancel full-screen.
+ Services.focus.activeWindow != window) {
+ // This function is called synchronously in fullscreen change, so
+ // we have to avoid calling exitFullscreen synchronously here.
+ setTimeout(() => document.exitFullscreen(), 0);
+ return;
+ }
+
+ document.documentElement.setAttribute("inDOMFullscreen", true);
+
+ if (gFindBarInitialized) {
+ gFindBar.close(true);
+ }
+
+ // Exit DOM full-screen mode upon open, close, or change tab.
+ gBrowser.tabContainer.addEventListener("TabOpen", this.exitDomFullScreen);
+ gBrowser.tabContainer.addEventListener("TabClose", this.exitDomFullScreen);
+ gBrowser.tabContainer.addEventListener("TabSelect", this.exitDomFullScreen);
+
+ // Add listener to detect when the fullscreen window is re-focused.
+ // If a fullscreen window loses focus, we show a warning when the
+ // fullscreen window is refocused.
+ window.addEventListener("activate", this);
+ },
+
+ cleanup: function () {
+ if (!window.fullScreen) {
+ MousePosTracker.removeListener(this);
+ document.removeEventListener("keypress", this._keyToggleCallback, false);
+ document.removeEventListener("popupshown", this._setPopupOpen, false);
+ document.removeEventListener("popuphidden", this._setPopupOpen, false);
+ }
+ },
+
+ cleanupDomFullscreen: function () {
+ window.messageManager
+ .broadcastAsyncMessage("DOMFullscreen:CleanUp");
+
+ PointerlockFsWarning.close();
+ gBrowser.tabContainer.removeEventListener("TabOpen", this.exitDomFullScreen);
+ gBrowser.tabContainer.removeEventListener("TabClose", this.exitDomFullScreen);
+ gBrowser.tabContainer.removeEventListener("TabSelect", this.exitDomFullScreen);
+ window.removeEventListener("activate", this);
+
+ document.documentElement.removeAttribute("inDOMFullscreen");
+ },
+
+ _isRemoteBrowser: function (aBrowser) {
+ return gMultiProcessBrowser && aBrowser.getAttribute("remote") == "true";
+ },
+
+ get _windowUtils() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ },
+
+ getMouseTargetRect: function()
+ {
+ return this._mouseTargetRect;
+ },
+
+ // Event callbacks
+ _expandCallback: function()
+ {
+ FullScreen.showNavToolbox();
+ },
+ onMouseEnter: function()
+ {
+ FullScreen.hideNavToolbox();
+ },
+ _keyToggleCallback: function(aEvent)
+ {
+ // if we can use the keyboard (eg Ctrl+L or Ctrl+E) to open the toolbars, we
+ // should provide a way to collapse them too.
+ if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
+ FullScreen.hideNavToolbox();
+ }
+ // F6 is another shortcut to the address bar, but its not covered in OpenLocation()
+ else if (aEvent.keyCode == aEvent.DOM_VK_F6)
+ FullScreen.showNavToolbox();
+ },
+
+ // Checks whether we are allowed to collapse the chrome
+ _isPopupOpen: false,
+ _isChromeCollapsed: false,
+ _safeToCollapse: function () {
+ if (!gPrefService.getBoolPref("browser.fullscreen.autohide"))
+ return false;
+
+ // a popup menu is open in chrome: don't collapse chrome
+ if (this._isPopupOpen)
+ return false;
+
+ // On OS X Lion we don't want to hide toolbars.
+ if (this.useLionFullScreen)
+ return false;
+
+ // a textbox in chrome is focused (location bar anyone?): don't collapse chrome
+ if (document.commandDispatcher.focusedElement &&
+ document.commandDispatcher.focusedElement.ownerDocument == document &&
+ document.commandDispatcher.focusedElement.localName == "input") {
+ return false;
+ }
+
+ return true;
+ },
+
+ _setPopupOpen: function(aEvent)
+ {
+ // Popups should only veto chrome collapsing if they were opened when the chrome was not collapsed.
+ // Otherwise, they would not affect chrome and the user would expect the chrome to go away.
+ // e.g. we wouldn't want the autoscroll icon firing this event, so when the user
+ // toggles chrome when moving mouse to the top, it doesn't go away again.
+ if (aEvent.type == "popupshown" && !FullScreen._isChromeCollapsed &&
+ aEvent.target.localName != "tooltip" && aEvent.target.localName != "window")
+ FullScreen._isPopupOpen = true;
+ else if (aEvent.type == "popuphidden" && aEvent.target.localName != "tooltip" &&
+ aEvent.target.localName != "window") {
+ FullScreen._isPopupOpen = false;
+ // Try again to hide toolbar when we close the popup.
+ FullScreen.hideNavToolbox(true);
+ }
+ },
+
+ // Autohide helpers for the context menu item
+ getAutohide: function(aItem)
+ {
+ aItem.setAttribute("checked", gPrefService.getBoolPref("browser.fullscreen.autohide"));
+ },
+ setAutohide: function()
+ {
+ gPrefService.setBoolPref("browser.fullscreen.autohide", !gPrefService.getBoolPref("browser.fullscreen.autohide"));
+ // Try again to hide toolbar when we change the pref.
+ FullScreen.hideNavToolbox(true);
+ },
+
+ showNavToolbox: function(trackMouse = true) {
+ this._fullScrToggler.hidden = true;
+ gNavToolbox.removeAttribute("fullscreenShouldAnimate");
+ gNavToolbox.style.marginTop = "";
+
+ if (!this._isChromeCollapsed) {
+ return;
+ }
+
+ // Track whether mouse is near the toolbox
+ if (trackMouse && !this.useLionFullScreen) {
+ let rect = gBrowser.mPanelContainer.getBoundingClientRect();
+ this._mouseTargetRect = {
+ top: rect.top + 50,
+ bottom: rect.bottom,
+ left: rect.left,
+ right: rect.right
+ };
+ MousePosTracker.addListener(this);
+ }
+
+ this._isChromeCollapsed = false;
+ },
+
+ hideNavToolbox: function (aAnimate = false) {
+ if (this._isChromeCollapsed || !this._safeToCollapse())
+ return;
+
+ this._fullScrToggler.hidden = false;
+
+ if (aAnimate && gPrefService.getBoolPref("browser.fullscreen.animate")) {
+ gNavToolbox.setAttribute("fullscreenShouldAnimate", true);
+ // Hide the fullscreen toggler until the transition ends.
+ let listener = () => {
+ gNavToolbox.removeEventListener("transitionend", listener, true);
+ if (this._isChromeCollapsed)
+ this._fullScrToggler.hidden = false;
+ };
+ gNavToolbox.addEventListener("transitionend", listener, true);
+ this._fullScrToggler.hidden = true;
+ }
+
+ gNavToolbox.style.marginTop =
+ -gNavToolbox.getBoundingClientRect().height + "px";
+ this._isChromeCollapsed = true;
+ MousePosTracker.removeListener(this);
+ },
+
+ _updateToolbars: function (aEnterFS) {
+ for (let el of document.querySelectorAll("toolbar[fullscreentoolbar=true]")) {
+ if (aEnterFS) {
+ // Give the main nav bar and the tab bar the fullscreen context menu,
+ // otherwise remove context menu to prevent breakage
+ el.setAttribute("saved-context", el.getAttribute("context"));
+ if (el.id == "nav-bar" || el.id == "TabsToolbar")
+ el.setAttribute("context", "autohide-context");
+ else
+ el.removeAttribute("context");
+
+ // Set the inFullscreen attribute to allow specific styling
+ // in fullscreen mode
+ el.setAttribute("inFullscreen", true);
+ } else {
+ if (el.hasAttribute("saved-context")) {
+ el.setAttribute("context", el.getAttribute("saved-context"));
+ el.removeAttribute("saved-context");
+ }
+ el.removeAttribute("inFullscreen");
+ }
+ }
+
+ ToolbarIconColor.inferFromText();
+
+ // For Lion fullscreen, all fullscreen controls are hidden, don't
+ // bother to touch them. If we don't stop here, the following code
+ // could cause the native fullscreen button be shown unexpectedly.
+ // See bug 1165570.
+ if (this.useLionFullScreen) {
+ return;
+ }
+
+ var fullscreenctls = document.getElementById("window-controls");
+ var navbar = document.getElementById("nav-bar");
+ var ctlsOnTabbar = window.toolbar.visible;
+ if (fullscreenctls.parentNode == navbar && ctlsOnTabbar) {
+ fullscreenctls.removeAttribute("flex");
+ document.getElementById("TabsToolbar").appendChild(fullscreenctls);
+ }
+ else if (fullscreenctls.parentNode.id == "TabsToolbar" && !ctlsOnTabbar) {
+ fullscreenctls.setAttribute("flex", "1");
+ navbar.appendChild(fullscreenctls);
+ }
+ fullscreenctls.hidden = !aEnterFS;
+ }
+};
+XPCOMUtils.defineLazyGetter(FullScreen, "useLionFullScreen", function() {
+ // We'll only use OS X Lion full screen if we're
+ // * on OS X
+ // * on Lion or higher (Darwin 11+)
+ // * have fullscreenbutton="true"
+ return AppConstants.isPlatformAndVersionAtLeast("macosx", 11) &&
+ document.documentElement.getAttribute("fullscreenbutton") == "true";
+});