forked from mirrors/gecko-dev
As far as I can tell nothing really looks at inFullscreen in other elements that are not the root, not in JS nor in CSS, so this code seems like it can go? Differential Revision: https://phabricator.services.mozilla.com/D207123
969 lines
31 KiB
JavaScript
969 lines
31 KiB
JavaScript
/* -*- 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,
|
|
});
|
|
|
|
document.documentElement.toggleAttribute("inFullscreen", enterFS);
|
|
document.documentElement.toggleAttribute(
|
|
"macOSNativeFullscreen",
|
|
enterFS &&
|
|
AppConstants.platform == "macosx" &&
|
|
(Services.prefs.getBoolPref(
|
|
"full-screen-api.macos-native-full-screen"
|
|
) ||
|
|
!document.fullscreenElement)
|
|
);
|
|
|
|
if (!document.fullscreenElement) {
|
|
ToolbarIconColor.inferFromText("fullscreen", 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);
|
|
},
|
|
};
|
|
|
|
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"])
|
|
);
|
|
});
|