diff options
Diffstat (limited to 'browser/base/content/browser-fullScreenAndPointerLock.js')
-rw-r--r-- | browser/base/content/browser-fullScreenAndPointerLock.js | 992 |
1 files changed, 992 insertions, 0 deletions
diff --git a/browser/base/content/browser-fullScreenAndPointerLock.js b/browser/base/content/browser-fullScreenAndPointerLock.js new file mode 100644 index 0000000000..aae596a0f7 --- /dev/null +++ b/browser/base/content/browser-fullScreenAndPointerLock.js @@ -0,0 +1,992 @@ +/* -*- 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/. */ + +// This file is loaded into the browser window scope. +/* eslint-env mozilla/browser-window */ + +var PointerlockFsWarning = { + _element: null, + _origin: null, + + /** + * 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: class { + constructor(func, delay) { + this._id = 0; + this._func = func; + this._delay = delay; + } + start() { + this.cancel(); + this._id = setTimeout(() => this._handle(), this._delay); + } + cancel() { + if (this._id) { + clearTimeout(this._id); + this._id = 0; + } + } + _handle() { + this._id = 0; + this._func(); + } + get delay() { + return this._delay; + } + }, + + showPointerLock(aOrigin) { + if (!document.fullscreen) { + let timeout = Services.prefs.getIntPref( + "pointer-lock-api.warning.timeout" + ); + this.show(aOrigin, "pointerlock-warning", timeout, 0); + } + }, + + showFullScreen(aOrigin) { + let timeout = Services.prefs.getIntPref("full-screen-api.warning.timeout"); + let delay = Services.prefs.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(aOrigin, elementId, timeout, delay) { + if (!this._element) { + this._element = document.getElementById(elementId); + // Setup event listeners + this._element.addEventListener("transitionend", this); + this._element.addEventListener("transitioncancel", this); + window.addEventListener("mousemove", this, true); + // If the user explicitly disables the prompt, there's no need to detect + // activation. + if (timeout > 0) { + window.addEventListener("activate", this); + window.addEventListener("deactivate", this); + } + // The timeout to hide the warning box after a while. + this._timeoutHide = new this.Timeout(() => { + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); + 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 = Services.io.newURI(this._origin); + let host = null; + // Make an exception for PDF.js - we'll show "This document" instead. + if (this._origin != "resource://pdf.js") { + try { + host = uri.host; + } catch (e) {} + } + let textElem = this._element.querySelector( + ".pointerlockfswarning-domain-text" + ); + if (!host) { + textElem.hidden = true; + } else { + textElem.removeAttribute("hidden"); + // Document's principal's URI has a host. Display a warning including it. + let { DownloadUtils } = ChromeUtils.importESModule( + "resource://gre/modules/DownloadUtils.sys.mjs" + ); + let displayHost = DownloadUtils.getURIHost(uri.spec)[0]; + let l10nString = { + "fullscreen-warning": "fullscreen-warning-domain", + "pointerlock-warning": "pointerlock-warning-domain", + }[elementId]; + document.l10n.setAttributes(textElem, l10nString, { + domain: displayHost, + }); + } + + 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; + } + + if (Services.focus.activeWindow == window) { + this._state = "onscreen"; + this._timeoutHide.start(); + } + }, + + /** + * Close the full screen or pointerlock warning. + * @param {('fullscreen-warning'|'pointerlock-warning')} elementId - Id of the + * warning element to close. If the id does not match the currently shown + * warning this is a no-op. + */ + close(elementId) { + if (!elementId) { + throw new Error("Must pass id of warning element to close"); + } + if (!this._element || this._element.id != elementId) { + return; + } + // Cancel any pending timeout + this._timeoutHide.cancel(); + this._timeoutShow.cancel(); + // Reset state of the warning box + this._state = "hidden"; + // Reset state of the text so we don't persist or retranslate it. + this._element + .querySelector(".pointerlockfswarning-domain-text") + .removeAttribute("data-l10n-id"); + this._element.hidden = true; + // Remove all event listeners + this._element.removeEventListener("transitionend", this); + this._element.removeEventListener("transitioncancel", this); + window.removeEventListener("mousemove", this, true); + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); + // 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, ""); + } 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, ""); + } + }); + }); + } + } + }, + + handleEvent(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 if (state != "onscreen") { + 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": + case "transitioncancel": { + if (this._state == "hiding") { + this._element.hidden = true; + } + if (this._state == "onscreen") { + window.dispatchEvent(new CustomEvent("FullscreenWarningOnScreen")); + } + break; + } + case "activate": { + this._state = "onscreen"; + this._timeoutHide.start(); + break; + } + case "deactivate": { + this._state = "hidden"; + this._timeoutHide.cancel(); + break; + } + } + }, +}; + +var PointerLock = { + _isActive: false, + + /** + * @returns {boolean} - true if pointer lock is currently active for the + * associated window. + */ + get isActive() { + return this._isActive; + }, + + entered(originNoSuffix) { + this._isActive = true; + Services.obs.notifyObservers(null, "pointer-lock-entered"); + PointerlockFsWarning.showPointerLock(originNoSuffix); + }, + + exited() { + this._isActive = false; + PointerlockFsWarning.close("pointerlock-warning"); + }, +}; + +var FullScreen = { + init() { + XPCOMUtils.defineLazyPreferenceGetter( + this, + "permissionsFullScreenAllowed", + "permissions.fullscreen.allowed" + ); + + // Called when the Firefox window go into fullscreen. + addEventListener("fullscreen", this, true); + + // Called only when fullscreen is requested + // by the parent (eg: via the browser-menu). + // Should not be called when the request comes from + // the content. + addEventListener("willenterfullscreen", this, true); + addEventListener("willexitfullscreen", this, true); + addEventListener("MacFullscreenMenubarRevealUpdate", this, true); + + if (window.fullScreen) { + this.toggle(); + } + }, + + uninit() { + this.cleanup(); + }, + + willToggle(aWillEnterFullscreen) { + if (aWillEnterFullscreen) { + document.documentElement.setAttribute("inFullscreen", true); + } else { + document.documentElement.removeAttribute("inFullscreen"); + } + }, + + get fullScreenToggler() { + delete this.fullScreenToggler; + return (this.fullScreenToggler = + document.getElementById("fullscr-toggler")); + }, + + toggle() { + 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; + this.shiftMacToolbarDown(0); + } + + let fstoggler = this.fullScreenToggler; + fstoggler.addEventListener("mouseover", this._expandCallback); + fstoggler.addEventListener("dragenter", this._expandCallback); + fstoggler.addEventListener("touchmove", this._expandCallback, { + passive: true, + }); + + if (enterFS) { + gNavToolbox.setAttribute("inFullscreen", true); + document.documentElement.setAttribute("inFullscreen", true); + let alwaysUsesNativeFullscreen = + AppConstants.platform == "macosx" && + Services.prefs.getBoolPref("full-screen-api.macos-native-full-screen"); + if ( + (alwaysUsesNativeFullscreen || !document.fullscreenElement) && + AppConstants.platform == "macosx" + ) { + document.documentElement.setAttribute("macOSNativeFullscreen", true); + } + } else { + gNavToolbox.removeAttribute("inFullscreen"); + document.documentElement.removeAttribute("inFullscreen"); + document.documentElement.removeAttribute("macOSNativeFullscreen"); + } + + if (!document.fullscreenElement) { + this._updateToolbars(enterFS); + } + + if (enterFS) { + document.addEventListener("keypress", this._keyToggleCallback); + document.addEventListener("popupshown", this._setPopupOpen); + document.addEventListener("popuphidden", this._setPopupOpen); + gURLBar.controller.addQueryListener(this); + + // 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(); + } + this._toggleShortcutKeys(); + }, + + exitDomFullScreen() { + if (document.fullscreen) { + document.exitFullscreen(); + } + }, + + /** + * Shifts the browser toolbar down when it is moused over on macOS in + * fullscreen. + * @param {number} shiftSize + * A distance, in pixels, by which to shift the browser toolbar down. + */ + shiftMacToolbarDown(shiftSize) { + if (typeof shiftSize !== "number") { + console.error("Tried to shift the toolbar by a non-numeric distance."); + return; + } + + // shiftSize is sent from Cocoa widget code as a very precise double. We + // don't need that kind of precision in our CSS. + shiftSize = shiftSize.toFixed(2); + let toolbox = gNavToolbox; + if (shiftSize > 0) { + toolbox.style.setProperty("transform", `translateY(${shiftSize}px)`); + toolbox.style.setProperty("z-index", "2"); + + // If the mouse tracking missed our fullScreenToggler, then the toolbox + // might not have been shown before the menubar is animated down. Make + // sure it is shown now. + if (!this.fullScreenToggler.hidden) { + this.showNavToolbox(); + } + } else { + toolbox.style.removeProperty("transform"); + toolbox.style.removeProperty("z-index"); + } + }, + + handleEvent(event) { + switch (event.type) { + case "willenterfullscreen": + this.willToggle(true); + break; + case "willexitfullscreen": + this.willToggle(false); + break; + case "fullscreen": + this.toggle(); + break; + case "MacFullscreenMenubarRevealUpdate": + this.shiftMacToolbarDown(event.detail); + break; + } + }, + + _logWarningPermissionPromptFS(actionStringKey) { + let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance( + Ci.nsIScriptError + ); + let message = gBrowserBundle.GetStringFromName( + `permissions.fullscreen.${actionStringKey}` + ); + consoleMsg.initWithWindowID( + message, + gBrowser.currentURI.spec, + null, + 0, + 0, + Ci.nsIScriptError.warningFlag, + "FullScreen", + gBrowser.selectedBrowser.innerWindowID + ); + Services.console.logMessage(consoleMsg); + }, + + _handlePermPromptShow() { + if ( + !FullScreen.permissionsFullScreenAllowed && + window.fullScreen && + PopupNotifications.getNotification( + this._permissionNotificationIDs + ).filter(n => !n.dismissed).length + ) { + this.exitDomFullScreen(); + this._logWarningPermissionPromptFS("fullScreenCanceled"); + } + }, + + enterDomFullscreen(aBrowser, aActor) { + if (!document.fullscreenElement) { + aActor.requestOrigin = null; + return; + } + + // If we have a current pointerlock warning shown then hide it + // before transition. + PointerlockFsWarning.close("pointerlock-warning"); + + // 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. + // Additionally, in Fission world, we may need to notify the + // frames in the middle (content frames that embbed the oop iframe where + // the element requesting fullscreen lives) to enter fullscreen + // first. + // 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)) { + // The cached message recipient in actor is used for fullscreen state + // cleanup, we should not use it while entering fullscreen. + let [targetActor, inProcessBC] = this._getNextMsgRecipientActor( + aActor, + false /* aUseCache */ + ); + if (!targetActor) { + // If there is no appropriate actor to send the message we have + // no way to complete the transition and should abort by exiting + // fullscreen. + this._abortEnterFullscreen(); + return; + } + // Record that the actor is waiting for its child to enter + // fullscreen so that if it dies we can abort. + targetActor.waitingForChildEnterFullscreen = true; + targetActor.sendAsyncMessage("DOMFullscreen:Entered", { + remoteFrameBC: inProcessBC, + }); + + if (inProcessBC) { + // We aren't messaging the request origin yet, skip this time. + return; + } + } + + // 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._abortEnterFullscreen(); + return; + } + + // Remove permission prompts when entering full-screen. + if (!FullScreen.permissionsFullScreenAllowed) { + let notifications = PopupNotifications.getNotification( + this._permissionNotificationIDs + ).filter(n => !n.dismissed); + PopupNotifications.remove(notifications, true); + if (notifications.length) { + this._logWarningPermissionPromptFS("promptCanceled"); + } + } + document.documentElement.setAttribute("inDOMFullscreen", true); + + XULBrowserWindow.onEnterDOMFullscreen(); + + if (gFindBarInitialized) { + gFindBar.close(true); + } + + // Exit DOM full-screen mode when switching to a different tab. + gBrowser.tabContainer.addEventListener("TabSelect", this.exitDomFullScreen); + + // Addon installation should be cancelled when entering DOM fullscreen for security and usability reasons. + // Installation prompts in fullscreen can trick the user into installing unwanted addons. + // In fullscreen the notification box does not have a clear visual association with its parent anymore. + if (gXPInstallObserver.removeAllNotifications(aBrowser)) { + // If notifications have been removed, log a warning to the website console + gXPInstallObserver.logWarningFullScreenInstallBlocked(); + } + + PopupNotifications.panel.addEventListener( + "popupshowing", + () => this._handlePermPromptShow(), + true + ); + }, + + cleanup() { + if (!window.fullScreen) { + MousePosTracker.removeListener(this); + document.removeEventListener("keypress", this._keyToggleCallback); + document.removeEventListener("popupshown", this._setPopupOpen); + document.removeEventListener("popuphidden", this._setPopupOpen); + gURLBar.controller.removeQueryListener(this); + } + }, + + _toggleShortcutKeys() { + const kEnterKeyIds = [ + "key_enterFullScreen", + "key_enterFullScreen_old", + "key_enterFullScreen_compat", + ]; + const kExitKeyIds = [ + "key_exitFullScreen", + "key_exitFullScreen_old", + "key_exitFullScreen_compat", + ]; + for (let id of window.fullScreen ? kEnterKeyIds : kExitKeyIds) { + document.getElementById(id)?.setAttribute("disabled", "true"); + } + for (let id of window.fullScreen ? kExitKeyIds : kEnterKeyIds) { + document.getElementById(id)?.removeAttribute("disabled"); + } + }, + + /** + * Clean up full screen, starting from the request origin's first ancestor + * frame that is OOP. + * + * If there are OOP ancestor frames, we notify the first of those and then bail to + * be called again in that process when it has dealt with the change. This is + * repeated until all ancestor processes have been updated. Once that has happened + * we remove our handlers and attributes and notify the request origin to complete + * the cleanup. + */ + cleanupDomFullscreen(aActor) { + let needToWaitForChildExit = false; + // Use the message recipient cached in the actor if possible, especially for + // the case that actor is destroyed, which we are unable to find it by + // walking up the browsing context tree. + let [target, inProcessBC] = this._getNextMsgRecipientActor( + aActor, + true /* aUseCache */ + ); + if (target) { + needToWaitForChildExit = true; + // Record that the actor is waiting for its child to exit fullscreen so + // that if it dies we can continue cleanup. + target.waitingForChildExitFullscreen = true; + target.sendAsyncMessage("DOMFullscreen:CleanUp", { + remoteFrameBC: inProcessBC, + }); + if (inProcessBC) { + return needToWaitForChildExit; + } + } + + PopupNotifications.panel.removeEventListener( + "popupshowing", + () => this._handlePermPromptShow(), + true + ); + + PointerlockFsWarning.close("fullscreen-warning"); + gBrowser.tabContainer.removeEventListener( + "TabSelect", + this.exitDomFullScreen + ); + + document.documentElement.removeAttribute("inDOMFullscreen"); + + return needToWaitForChildExit; + }, + + _abortEnterFullscreen() { + // This function is called synchronously in fullscreen change, so + // we have to avoid calling exitFullscreen synchronously here. + // + // This could reject if we're not currently in fullscreen + // so just ignore rejection. + setTimeout(() => document.exitFullscreen().catch(() => {}), 0); + if (TelemetryStopwatch.running("FULLSCREEN_CHANGE_MS")) { + // Cancel the stopwatch for any fullscreen change to avoid + // errors if it is started again. + TelemetryStopwatch.cancel("FULLSCREEN_CHANGE_MS"); + } + }, + + /** + * Search for the first ancestor of aActor that lives in a different process. + * If found, that ancestor actor and the browsing context for its child which + * was in process are returned. Otherwise [request origin, null]. + * + * + * @param {JSWindowActorParent} aActor + * The actor that called this function. + * @param {bool} aUseCache + * Use the recipient cached in the aActor if available. + * + * @return {[JSWindowActorParent, BrowsingContext]} + * The parent actor which should be sent the next msg and the + * in process browsing context which is its child. Will be + * [null, null] if there is no OOP parent actor and request origin + * is unset. [null, null] is also returned if the intended actor or + * the calling actor has been destroyed or its associated + * WindowContext is in BFCache. + */ + _getNextMsgRecipientActor(aActor, aUseCache) { + // Walk up the cached nextMsgRecipient to find the next available actor if + // any. + if (aUseCache && aActor.nextMsgRecipient) { + let nextMsgRecipient = aActor.nextMsgRecipient; + while (nextMsgRecipient) { + let [actor] = nextMsgRecipient; + if ( + !actor.hasBeenDestroyed() && + actor.windowContext && + !actor.windowContext.isInBFCache + ) { + return nextMsgRecipient; + } + nextMsgRecipient = actor.nextMsgRecipient; + } + } + + if (aActor.hasBeenDestroyed()) { + return [null, null]; + } + + let childBC = aActor.browsingContext; + let parentBC = childBC.parent; + + // Walk up the browsing context tree from aActor's browsing context + // to find the first ancestor browsing context that's in a different process. + while (parentBC) { + if (!childBC.currentWindowGlobal || !parentBC.currentWindowGlobal) { + break; + } + let childPid = childBC.currentWindowGlobal.osPid; + let parentPid = parentBC.currentWindowGlobal.osPid; + + if (childPid == parentPid) { + childBC = parentBC; + parentBC = childBC.parent; + } else { + break; + } + } + + let target = null; + let inProcessBC = null; + + if (parentBC && parentBC.currentWindowGlobal) { + target = parentBC.currentWindowGlobal.getActor("DOMFullscreen"); + inProcessBC = childBC; + aActor.nextMsgRecipient = [target, inProcessBC]; + } else { + target = aActor.requestOrigin; + } + + if ( + !target || + target.hasBeenDestroyed() || + target.windowContext?.isInBFCache + ) { + return [null, null]; + } + return [target, inProcessBC]; + }, + + _isRemoteBrowser(aBrowser) { + return gMultiProcessBrowser && aBrowser.getAttribute("remote") == "true"; + }, + + getMouseTargetRect() { + return this._mouseTargetRect; + }, + + // Event callbacks + _expandCallback() { + FullScreen.showNavToolbox(); + }, + + onMouseEnter() { + this.hideNavToolbox(); + }, + + _keyToggleCallback(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(); + } else if (aEvent.keyCode == aEvent.DOM_VK_F6) { + // F6 is another shortcut to the address bar, but its not covered in OpenLocation() + FullScreen.showNavToolbox(); + } + }, + + // Checks whether we are allowed to collapse the chrome + _isPopupOpen: false, + _isChromeCollapsed: false, + + _setPopupOpen(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. + let target = aEvent.originalTarget; + if (target.localName == "tooltip") { + return; + } + if ( + aEvent.type == "popupshown" && + !FullScreen._isChromeCollapsed && + target.getAttribute("nopreventnavboxhide") != "true" + ) { + FullScreen._isPopupOpen = true; + } else if (aEvent.type == "popuphidden") { + FullScreen._isPopupOpen = false; + // Try again to hide toolbar when we close the popup. + FullScreen.hideNavToolbox(true); + } + }, + + // UrlbarController listener method + onViewOpen() { + if (!this._isChromeCollapsed) { + this._isPopupOpen = true; + } + }, + + // UrlbarController listener method + onViewClose() { + this._isPopupOpen = false; + this.hideNavToolbox(true); + }, + + get navToolboxHidden() { + return this._isChromeCollapsed; + }, + + // Autohide helpers for the context menu item + updateAutohideMenuitem(aItem) { + aItem.setAttribute( + "checked", + Services.prefs.getBoolPref("browser.fullscreen.autohide") + ); + }, + setAutohide() { + Services.prefs.setBoolPref( + "browser.fullscreen.autohide", + !Services.prefs.getBoolPref("browser.fullscreen.autohide") + ); + // Try again to hide toolbar when we change the pref. + FullScreen.hideNavToolbox(true); + }, + + showNavToolbox(trackMouse = true) { + if (BrowserHandler.kiosk) { + return; + } + this.fullScreenToggler.hidden = true; + gNavToolbox.removeAttribute("fullscreenShouldAnimate"); + gNavToolbox.style.marginTop = ""; + + if (!this._isChromeCollapsed) { + return; + } + + // Track whether mouse is near the toolbox + if (trackMouse) { + let rect = gBrowser.tabpanels.getBoundingClientRect(); + this._mouseTargetRect = { + top: rect.top + 50, + bottom: rect.bottom, + left: rect.left, + right: rect.right, + }; + MousePosTracker.addListener(this); + } + + this._isChromeCollapsed = false; + Services.obs.notifyObservers(null, "fullscreen-nav-toolbox", "shown"); + }, + + hideNavToolbox(aAnimate = false) { + if (this._isChromeCollapsed) { + return; + } + if (!Services.prefs.getBoolPref("browser.fullscreen.autohide")) { + return; + } + // a popup menu is open in chrome: don't collapse chrome + if (this._isPopupOpen) { + return; + } + + // a textbox in chrome is focused (location bar anyone?): don't collapse chrome + // unless we are kiosk mode + let focused = document.commandDispatcher.focusedElement; + if ( + focused && + focused.ownerDocument == document && + focused.localName == "input" && + !BrowserHandler.kiosk + ) { + // But try collapse the chrome again when anything happens which can make + // it lose the focus. We cannot listen on "blur" event on focused here + // because that event can be triggered by "mousedown", and hiding chrome + // would cause the content to move. This combination may split a single + // click into two actionless halves. + let retryHideNavToolbox = () => { + // Wait for at least a frame to give it a chance to be passed down to + // the content. + requestAnimationFrame(() => { + setTimeout(() => { + // In the meantime, it's possible that we exited fullscreen somehow, + // so only hide the toolbox if we're still in fullscreen mode. + if (window.fullScreen) { + this.hideNavToolbox(aAnimate); + } + }, 0); + }); + window.removeEventListener("keydown", retryHideNavToolbox); + window.removeEventListener("click", retryHideNavToolbox); + }; + window.addEventListener("keydown", retryHideNavToolbox); + window.addEventListener("click", retryHideNavToolbox); + return; + } + + if (!BrowserHandler.kiosk) { + this.fullScreenToggler.hidden = false; + } + + if ( + aAnimate && + window.matchMedia("(prefers-reduced-motion: no-preference)").matches && + !BrowserHandler.kiosk + ) { + gNavToolbox.setAttribute("fullscreenShouldAnimate", true); + } + + gNavToolbox.style.marginTop = + -gNavToolbox.getBoundingClientRect().height + "px"; + this._isChromeCollapsed = true; + Services.obs.notifyObservers(null, "fullscreen-nav-toolbox", "hidden"); + + MousePosTracker.removeListener(this); + }, + + _updateToolbars(aEnterFS) { + for (let el of document.querySelectorAll( + "toolbar[fullscreentoolbar=true]" + )) { + // Set the inFullscreen attribute to allow specific styling + // in fullscreen mode + if (aEnterFS) { + el.setAttribute("inFullscreen", true); + } else { + el.removeAttribute("inFullscreen"); + } + } + + ToolbarIconColor.inferFromText("fullscreen", aEnterFS); + }, +}; + +ChromeUtils.defineLazyGetter(FullScreen, "_permissionNotificationIDs", () => { + let { PermissionUI } = ChromeUtils.importESModule( + "resource:///modules/PermissionUI.sys.mjs" + ); + return ( + Object.values(PermissionUI) + .filter(value => { + let returnValue; + try { + returnValue = value.prototype.notificationID; + } catch (err) { + if (err.message === "Not implemented.") { + returnValue = false; + } else { + throw err; + } + } + return returnValue; + }) + .map(value => value.prototype.notificationID) + // Additionally include webRTC permission prompt which does not use PermissionUI + .concat(["webRTC-shareDevices"]) + ); +}); |