diff options
Diffstat (limited to 'browser/base/content/browser-fullScreenAndPointerLock.js')
-rw-r--r-- | browser/base/content/browser-fullScreenAndPointerLock.js | 673 |
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"; +}); |