browser-fullScreenAndPointerLock.js (31210B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 var PointerlockFsWarning = { 7 _element: null, 8 _origin: null, 9 10 /** 11 * Timeout object for managing timeout request. If it is started when 12 * the previous call hasn't finished, it would automatically cancelled 13 * the previous one. 14 */ 15 Timeout: class { 16 constructor(func, delay) { 17 this._id = 0; 18 this._func = func; 19 this._delay = delay; 20 } 21 start() { 22 this.cancel(); 23 this._id = setTimeout(() => this._handle(), this._delay); 24 } 25 cancel() { 26 if (this._id) { 27 clearTimeout(this._id); 28 this._id = 0; 29 } 30 } 31 _handle() { 32 this._id = 0; 33 this._func(); 34 } 35 get delay() { 36 return this._delay; 37 } 38 }, 39 40 showPointerLock(aOrigin) { 41 if (!document.fullscreen) { 42 let timeout = Services.prefs.getIntPref( 43 "pointer-lock-api.warning.timeout" 44 ); 45 this.show(aOrigin, "pointerlock-warning", timeout, 0); 46 } 47 }, 48 49 showFullScreen(aOrigin) { 50 let timeout = Services.prefs.getIntPref("full-screen-api.warning.timeout"); 51 let delay = Services.prefs.getIntPref("full-screen-api.warning.delay"); 52 this.show(aOrigin, "fullscreen-warning", timeout, delay); 53 }, 54 55 // Shows a warning that the site has entered fullscreen or 56 // pointer lock for a short duration. 57 show(aOrigin, elementId, timeout, delay) { 58 if (!this._element) { 59 this._element = document.getElementById(elementId); 60 // Setup event listeners 61 this._element.addEventListener("transitionend", this); 62 this._element.addEventListener("transitioncancel", this); 63 window.addEventListener("mousemove", this, true); 64 // If the user explicitly disables the prompt, there's no need to detect 65 // activation. 66 if (timeout > 0) { 67 window.addEventListener("activate", this); 68 window.addEventListener("deactivate", this); 69 } 70 // The timeout to hide the warning box after a while. 71 this._timeoutHide = new this.Timeout(() => { 72 window.removeEventListener("activate", this); 73 window.removeEventListener("deactivate", this); 74 this._state = "hidden"; 75 }, timeout); 76 // The timeout to show the warning box when the pointer is at the top 77 this._timeoutShow = new this.Timeout(() => { 78 this._state = "ontop"; 79 this._timeoutHide.start(); 80 }, delay); 81 } 82 83 // Set the strings on the warning UI. 84 if (aOrigin) { 85 this._origin = aOrigin; 86 } 87 let uri = Services.io.newURI(this._origin); 88 let host = null; 89 // Make an exception for PDF.js - we'll show "This document" instead. 90 if (this._origin != "resource://pdf.js") { 91 try { 92 host = uri.host; 93 } catch (e) {} 94 } 95 let textElem = this._element.querySelector( 96 ".pointerlockfswarning-domain-text" 97 ); 98 if (!host) { 99 textElem.hidden = true; 100 } else { 101 textElem.removeAttribute("hidden"); 102 // Document's principal's URI has a host. Display a warning including it. 103 let displayHost = BrowserUtils.formatURIForDisplay(uri, { 104 onlyBaseDomain: true, 105 }); 106 let l10nString = { 107 "fullscreen-warning": "fullscreen-warning-domain", 108 "pointerlock-warning": "pointerlock-warning-domain", 109 }[elementId]; 110 document.l10n.setAttributes(textElem, l10nString, { 111 domain: displayHost, 112 }); 113 } 114 115 this._element.dataset.identity = 116 gIdentityHandler.pointerlockFsWarningClassName; 117 118 // User should be allowed to explicitly disable 119 // the prompt if they really want. 120 if (this._timeoutHide.delay <= 0) { 121 return; 122 } 123 124 if (Services.focus.activeWindow == window) { 125 this._state = "onscreen"; 126 this._timeoutHide.start(); 127 } 128 }, 129 130 /** 131 * Close the full screen or pointerlock warning. 132 * 133 * @param {('fullscreen-warning'|'pointerlock-warning')} elementId - Id of the 134 * warning element to close. If the id does not match the currently shown 135 * warning this is a no-op. 136 */ 137 close(elementId) { 138 if (!elementId) { 139 throw new Error("Must pass id of warning element to close"); 140 } 141 if (!this._element || this._element.id != elementId) { 142 return; 143 } 144 // Cancel any pending timeout 145 this._timeoutHide.cancel(); 146 this._timeoutShow.cancel(); 147 // Reset state of the warning box 148 this._state = "hidden"; 149 this._doHide(); 150 // Reset state of the text so we don't persist or retranslate it. 151 this._element 152 .querySelector(".pointerlockfswarning-domain-text") 153 .removeAttribute("data-l10n-id"); 154 // Remove all event listeners 155 this._element.removeEventListener("transitionend", this); 156 this._element.removeEventListener("transitioncancel", this); 157 window.removeEventListener("mousemove", this, true); 158 window.removeEventListener("activate", this); 159 window.removeEventListener("deactivate", this); 160 // Clear fields 161 this._element = null; 162 this._timeoutHide = null; 163 this._timeoutShow = null; 164 165 // Ensure focus switches away from the (now hidden) warning box. 166 // If the user clicked buttons in the warning box, it would have 167 // been focused, and any key events would be directed at the (now 168 // hidden) chrome document instead of the target document. 169 gBrowser.selectedBrowser.focus(); 170 }, 171 172 // State could be one of "onscreen", "ontop", "hiding", and 173 // "hidden". Setting the state to "onscreen" and "ontop" takes 174 // effect immediately, while setting it to "hidden" actually 175 // turns the state to "hiding" before the transition finishes. 176 _lastState: null, 177 _STATES: ["hidden", "ontop", "onscreen"], 178 get _state() { 179 for (let state of this._STATES) { 180 if (this._element.hasAttribute(state)) { 181 return state; 182 } 183 } 184 return "hiding"; 185 }, 186 187 _doHide() { 188 try { 189 this._element.hidePopover(); 190 } catch (e) {} 191 this._element.hidden = true; 192 }, 193 194 set _state(newState) { 195 let currentState = this._state; 196 if (currentState == newState) { 197 return; 198 } 199 if (currentState != "hiding") { 200 this._lastState = currentState; 201 this._element.removeAttribute(currentState); 202 } 203 if (currentState == "hidden") { 204 this._element.showPopover(); 205 } 206 // hidden is dealt with on transitionend or close(), see _doHide(). 207 if (newState != "hidden") { 208 this._element.setAttribute(newState, ""); 209 } 210 }, 211 212 handleEvent(event) { 213 switch (event.type) { 214 case "mousemove": { 215 let state = this._state; 216 if (state == "hidden") { 217 // If the warning box is currently hidden, show it after 218 // a short delay if the pointer is at the top. 219 if (event.clientY != 0) { 220 this._timeoutShow.cancel(); 221 } else if (this._timeoutShow.delay >= 0) { 222 this._timeoutShow.start(); 223 } 224 } else if (state != "onscreen") { 225 let elemRect = this._element.getBoundingClientRect(); 226 if (state == "hiding" && this._lastState != "hidden") { 227 // If we are on the hiding transition, and the pointer 228 // moved near the box, restore to the previous state. 229 if (event.clientY <= elemRect.bottom + 50) { 230 this._state = this._lastState; 231 this._timeoutHide.start(); 232 } 233 } else if (state == "ontop" || this._lastState != "hidden") { 234 // State being "ontop" or the previous state not being 235 // "hidden" indicates this current warning box is shown 236 // in response to user's action. Hide it immediately when 237 // the pointer leaves that area. 238 if (event.clientY > elemRect.bottom + 50) { 239 this._state = "hidden"; 240 this._timeoutHide.cancel(); 241 } 242 } 243 } 244 break; 245 } 246 case "transitionend": 247 case "transitioncancel": { 248 if (this._state == "hiding") { 249 this._doHide(); 250 } 251 if (this._state == "onscreen") { 252 window.dispatchEvent(new CustomEvent("FullscreenWarningOnScreen")); 253 } 254 break; 255 } 256 case "activate": { 257 this._state = "onscreen"; 258 this._timeoutHide.start(); 259 break; 260 } 261 case "deactivate": { 262 this._state = "hidden"; 263 this._timeoutHide.cancel(); 264 break; 265 } 266 } 267 }, 268 }; 269 270 var PointerLock = { 271 _isActive: false, 272 273 /** 274 * @returns {boolean} - true if pointer lock is currently active for the 275 * associated window. 276 */ 277 get isActive() { 278 return this._isActive; 279 }, 280 281 entered(originNoSuffix) { 282 this._isActive = true; 283 Services.obs.notifyObservers(null, "pointer-lock-entered"); 284 PointerlockFsWarning.showPointerLock(originNoSuffix); 285 }, 286 287 exited() { 288 this._isActive = false; 289 PointerlockFsWarning.close("pointerlock-warning"); 290 }, 291 }; 292 293 var FullScreen = { 294 init() { 295 XPCOMUtils.defineLazyPreferenceGetter( 296 this, 297 "permissionsFullScreenAllowed", 298 "permissions.fullscreen.allowed" 299 ); 300 301 let notificationExitButton = document.getElementById( 302 "fullscreen-exit-button" 303 ); 304 notificationExitButton.addEventListener("click", this.exitDomFullScreen); 305 306 // Called when the Firefox window go into fullscreen. 307 addEventListener("fullscreen", this, true); 308 309 // Called only when fullscreen is requested 310 // by the parent (eg: via the browser-menu). 311 // Should not be called when the request comes from 312 // the content. 313 addEventListener("willenterfullscreen", this, true); 314 addEventListener("willexitfullscreen", this, true); 315 addEventListener("MacFullscreenMenubarRevealUpdate", this, true); 316 317 if (window.fullScreen) { 318 this.toggle(); 319 } 320 }, 321 322 uninit() { 323 this.cleanup(); 324 }, 325 326 willToggle(aWillEnterFullscreen) { 327 if (aWillEnterFullscreen) { 328 document.documentElement.setAttribute("inFullscreen", true); 329 } else { 330 document.documentElement.removeAttribute("inFullscreen"); 331 } 332 }, 333 334 get fullScreenToggler() { 335 delete this.fullScreenToggler; 336 return (this.fullScreenToggler = 337 document.getElementById("fullscr-toggler")); 338 }, 339 340 toggle() { 341 var enterFS = window.fullScreen; 342 343 // Toggle the View:FullScreen command, which controls elements like the 344 // fullscreen menuitem, and menubars. 345 let fullscreenCommand = document.getElementById("View:FullScreen"); 346 fullscreenCommand.toggleAttribute("checked", enterFS); 347 348 if (AppConstants.platform == "macosx") { 349 // Make sure the menu items are adjusted. 350 document.getElementById("enterFullScreenItem").hidden = enterFS; 351 document.getElementById("exitFullScreenItem").hidden = !enterFS; 352 this.shiftMacToolbarDown(0); 353 } 354 355 let fstoggler = this.fullScreenToggler; 356 fstoggler.addEventListener("mouseover", this._expandCallback); 357 fstoggler.addEventListener("dragenter", this._expandCallback); 358 fstoggler.addEventListener("touchmove", this._expandCallback, { 359 passive: true, 360 }); 361 362 document.documentElement.toggleAttribute("inFullscreen", enterFS); 363 document.documentElement.toggleAttribute( 364 "macOSNativeFullscreen", 365 enterFS && 366 AppConstants.platform == "macosx" && 367 (Services.prefs.getBoolPref( 368 "full-screen-api.macos-native-full-screen" 369 ) || 370 !document.fullscreenElement) 371 ); 372 373 if (!document.fullscreenElement) { 374 ToolbarIconColor.inferFromText("fullscreen", enterFS); 375 } 376 377 if (enterFS) { 378 document.addEventListener("keypress", this._keyToggleCallback); 379 document.addEventListener("popupshown", this._setPopupOpen); 380 document.addEventListener("popuphidden", this._setPopupOpen); 381 gURLBar.controller.addListener(this); 382 383 // In DOM fullscreen mode, we hide toolbars with CSS 384 if (!document.fullscreenElement) { 385 this.hideNavToolbox(true); 386 } 387 } else { 388 this.showNavToolbox(false); 389 // This is needed if they use the context menu to quit fullscreen 390 this._isPopupOpen = false; 391 this.cleanup(); 392 } 393 this._toggleShortcutKeys(); 394 }, 395 396 exitDomFullScreen() { 397 // Don't use `this` here. It does not reliably refer to this object. 398 if (document.fullscreen) { 399 document.exitFullscreen(); 400 } 401 }, 402 403 _currentToolbarShift: 0, 404 405 /** 406 * Shifts the browser toolbar down when it is moused over on macOS in 407 * fullscreen. 408 * 409 * @param {number} shiftSize 410 * A distance, in pixels, by which to shift the browser toolbar down. 411 */ 412 shiftMacToolbarDown(shiftSize) { 413 if (typeof shiftSize !== "number") { 414 console.error("Tried to shift the toolbar by a non-numeric distance."); 415 return; 416 } 417 418 // shiftSize is sent from Cocoa widget code as a very precise double. We 419 // don't need that kind of precision in our CSS. 420 shiftSize = shiftSize.toFixed(2); 421 gNavToolbox.classList.toggle("fullscreen-with-menubar", shiftSize > 0); 422 423 let transform = shiftSize > 0 ? `translateY(${shiftSize}px)` : ""; 424 gNavToolbox.style.transform = transform; 425 gURLBar.style.transform = gURLBar.hasAttribute("breakout") ? transform : ""; 426 if (shiftSize > 0) { 427 // If the mouse tracking missed our fullScreenToggler, then the toolbox 428 // might not have been shown before the menubar is animated down. Make 429 // sure it is shown now. 430 if (!this.fullScreenToggler.hidden) { 431 this.showNavToolbox(); 432 } 433 } 434 435 this._currentToolbarShift = shiftSize; 436 }, 437 438 handleEvent(event) { 439 switch (event.type) { 440 case "willenterfullscreen": 441 this.willToggle(true); 442 break; 443 case "willexitfullscreen": 444 this.willToggle(false); 445 break; 446 case "fullscreen": 447 this.toggle(); 448 break; 449 case "MacFullscreenMenubarRevealUpdate": 450 this.shiftMacToolbarDown(event.detail); 451 break; 452 } 453 }, 454 455 _logWarningPermissionPromptFS(actionStringKey) { 456 let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance( 457 Ci.nsIScriptError 458 ); 459 let message = gBrowserBundle.GetStringFromName( 460 `permissions.fullscreen.${actionStringKey}` 461 ); 462 consoleMsg.initWithWindowID( 463 message, 464 gBrowser.currentURI.spec, 465 0, 466 0, 467 Ci.nsIScriptError.warningFlag, 468 "FullScreen", 469 gBrowser.selectedBrowser.innerWindowID 470 ); 471 Services.console.logMessage(consoleMsg); 472 }, 473 474 _handlePermPromptShow() { 475 if ( 476 !FullScreen.permissionsFullScreenAllowed && 477 window.fullScreen && 478 PopupNotifications.getNotification( 479 this._permissionNotificationIDs 480 ).filter(n => !n.dismissed).length 481 ) { 482 this.exitDomFullScreen(); 483 this._logWarningPermissionPromptFS("fullScreenCanceled"); 484 } 485 }, 486 487 enterDomFullscreen(aBrowser, aActor) { 488 if (!document.fullscreenElement) { 489 aActor.requestOrigin = null; 490 return; 491 } 492 493 // If we have a current pointerlock warning shown then hide it 494 // before transition. 495 PointerlockFsWarning.close("pointerlock-warning"); 496 497 // If it is a remote browser, send a message to ask the content 498 // to enter fullscreen state. We don't need to do so if it is an 499 // in-process browser, since all related document should have 500 // entered fullscreen state at this point. 501 // Additionally, in Fission world, we may need to notify the 502 // frames in the middle (content frames that embbed the oop iframe where 503 // the element requesting fullscreen lives) to enter fullscreen 504 // first. 505 // This should be done before the active tab check below to ensure 506 // that the content document handles the pending request. Doing so 507 // before the check is fine since we also check the activeness of 508 // the requesting document in content-side handling code. 509 if (this._isRemoteBrowser(aBrowser)) { 510 // The cached message recipient in actor is used for fullscreen state 511 // cleanup, we should not use it while entering fullscreen. 512 let [targetActor, inProcessBC] = this._getNextMsgRecipientActor( 513 aActor, 514 false /* aUseCache */ 515 ); 516 if (!targetActor) { 517 // If there is no appropriate actor to send the message we have 518 // no way to complete the transition and should abort by exiting 519 // fullscreen. 520 this._abortEnterFullscreen(aActor); 521 return; 522 } 523 // Record that the actor is waiting for its child to enter 524 // fullscreen so that if it dies we can abort. 525 targetActor.waitingForChildEnterFullscreen = true; 526 targetActor.sendAsyncMessage("DOMFullscreen:Entered", { 527 remoteFrameBC: inProcessBC, 528 }); 529 530 if (inProcessBC) { 531 // We aren't messaging the request origin yet, skip this time. 532 return; 533 } 534 } 535 536 // If we've received a fullscreen notification, we have to ensure that the 537 // element that's requesting fullscreen belongs to the browser that's currently 538 // active. If not, we exit fullscreen since the "full-screen document" isn't 539 // actually visible now. 540 if ( 541 !aBrowser || 542 gBrowser.selectedBrowser != aBrowser || 543 // The top-level window has lost focus since the request to enter 544 // full-screen was made. Cancel full-screen. 545 Services.focus.activeWindow != window 546 ) { 547 this._abortEnterFullscreen(aActor); 548 return; 549 } 550 551 // Remove permission prompts when entering full-screen. 552 if (!FullScreen.permissionsFullScreenAllowed) { 553 let notifications = PopupNotifications.getNotification( 554 this._permissionNotificationIDs 555 ).filter(n => !n.dismissed); 556 PopupNotifications.remove(notifications, true); 557 if (notifications.length) { 558 this._logWarningPermissionPromptFS("promptCanceled"); 559 } 560 } 561 document.documentElement.setAttribute("inDOMFullscreen", true); 562 563 XULBrowserWindow.onEnterDOMFullscreen(); 564 565 if (gFindBarInitialized) { 566 gFindBar.close(true); 567 } 568 569 // Exit DOM full-screen mode when switching to a different tab. 570 gBrowser.tabContainer.addEventListener("TabSelect", this.exitDomFullScreen); 571 572 // Addon installation should be cancelled when entering DOM fullscreen for security and usability reasons. 573 // Installation prompts in fullscreen can trick the user into installing unwanted addons. 574 // In fullscreen the notification box does not have a clear visual association with its parent anymore. 575 if (gXPInstallObserver.removeAllNotifications(aBrowser)) { 576 // If notifications have been removed, log a warning to the website console 577 gXPInstallObserver.logWarningFullScreenInstallBlocked(); 578 } 579 580 PopupNotifications.panel.addEventListener( 581 "popupshowing", 582 () => this._handlePermPromptShow(), 583 true 584 ); 585 }, 586 587 cleanup() { 588 if (!window.fullScreen) { 589 MousePosTracker.removeListener(this); 590 document.removeEventListener("keypress", this._keyToggleCallback); 591 document.removeEventListener("popupshown", this._setPopupOpen); 592 document.removeEventListener("popuphidden", this._setPopupOpen); 593 gURLBar.controller.removeListener(this); 594 } 595 }, 596 597 _toggleShortcutKeys() { 598 const kEnterKeyIds = [ 599 "key_enterFullScreen", 600 "key_enterFullScreen_old", 601 "key_enterFullScreen_compat", 602 ]; 603 const kExitKeyIds = [ 604 "key_exitFullScreen", 605 "key_exitFullScreen_old", 606 "key_exitFullScreen_compat", 607 ]; 608 for (let id of window.fullScreen ? kEnterKeyIds : kExitKeyIds) { 609 document.getElementById(id)?.setAttribute("disabled", "true"); 610 } 611 for (let id of window.fullScreen ? kExitKeyIds : kEnterKeyIds) { 612 document.getElementById(id)?.removeAttribute("disabled"); 613 } 614 }, 615 616 /** 617 * Clean up full screen, starting from the request origin's first ancestor 618 * frame that is OOP. 619 * 620 * If there are OOP ancestor frames, we notify the first of those and then bail to 621 * be called again in that process when it has dealt with the change. This is 622 * repeated until all ancestor processes have been updated. Once that has happened 623 * we remove our handlers and attributes and notify the request origin to complete 624 * the cleanup. 625 */ 626 cleanupDomFullscreen(aActor) { 627 let needToWaitForChildExit = false; 628 // Use the message recipient cached in the actor if possible, especially for 629 // the case that actor is destroyed, which we are unable to find it by 630 // walking up the browsing context tree. 631 let [target, inProcessBC] = this._getNextMsgRecipientActor( 632 aActor, 633 true /* aUseCache */ 634 ); 635 if (target) { 636 needToWaitForChildExit = true; 637 // Record that the actor is waiting for its child to exit fullscreen so 638 // that if it dies we can continue cleanup. 639 target.waitingForChildExitFullscreen = true; 640 target.sendAsyncMessage("DOMFullscreen:CleanUp", { 641 remoteFrameBC: inProcessBC, 642 }); 643 if (inProcessBC) { 644 return needToWaitForChildExit; 645 } 646 } 647 648 PopupNotifications.panel.removeEventListener( 649 "popupshowing", 650 () => this._handlePermPromptShow(), 651 true 652 ); 653 654 PointerlockFsWarning.close("fullscreen-warning"); 655 gBrowser.tabContainer.removeEventListener( 656 "TabSelect", 657 this.exitDomFullScreen 658 ); 659 660 document.documentElement.removeAttribute("inDOMFullscreen"); 661 662 return needToWaitForChildExit; 663 }, 664 665 _abortEnterFullscreen(aActor) { 666 // This function is called synchronously in fullscreen change, so 667 // we have to avoid calling exitFullscreen synchronously here. 668 // 669 // This could reject if we're not currently in fullscreen 670 // so just ignore rejection. 671 setTimeout(() => document.exitFullscreen().catch(() => {}), 0); 672 if (aActor.timerId) { 673 // Cancel the stopwatch for any fullscreen change to avoid 674 // errors if it is started again. 675 Glean.fullscreen.change.cancel(aActor.timerId); 676 aActor.timerId = null; 677 } 678 }, 679 680 /** 681 * Search for the first ancestor of aActor that lives in a different process. 682 * If found, that ancestor actor and the browsing context for its child which 683 * was in process are returned. Otherwise [request origin, null]. 684 * 685 * @param {JSWindowActorParent} aActor 686 * The actor that called this function. 687 * @param {bool} aUseCache 688 * Use the recipient cached in the aActor if available. 689 * 690 * @return {[JSWindowActorParent, BrowsingContext]} 691 * The parent actor which should be sent the next msg and the 692 * in process browsing context which is its child. Will be 693 * [null, null] if there is no OOP parent actor and request origin 694 * is unset. [null, null] is also returned if the intended actor or 695 * the calling actor has been destroyed or its associated 696 * WindowContext is in BFCache. 697 */ 698 _getNextMsgRecipientActor(aActor, aUseCache) { 699 // Walk up the cached nextMsgRecipient to find the next available actor if 700 // any. 701 if (aUseCache && aActor.nextMsgRecipient) { 702 let nextMsgRecipient = aActor.nextMsgRecipient; 703 while (nextMsgRecipient) { 704 let [actor] = nextMsgRecipient; 705 if ( 706 !actor.hasBeenDestroyed() && 707 actor.windowContext && 708 !actor.windowContext.isInBFCache 709 ) { 710 return nextMsgRecipient; 711 } 712 nextMsgRecipient = actor.nextMsgRecipient; 713 } 714 } 715 716 if (aActor.hasBeenDestroyed()) { 717 return [null, null]; 718 } 719 720 let childBC = aActor.browsingContext; 721 let parentBC = childBC.parent; 722 723 // Walk up the browsing context tree from aActor's browsing context 724 // to find the first ancestor browsing context that's in a different process. 725 while (parentBC) { 726 if (!childBC.currentWindowGlobal || !parentBC.currentWindowGlobal) { 727 break; 728 } 729 let childPid = childBC.currentWindowGlobal.osPid; 730 let parentPid = parentBC.currentWindowGlobal.osPid; 731 732 if (childPid == parentPid) { 733 childBC = parentBC; 734 parentBC = childBC.parent; 735 } else { 736 break; 737 } 738 } 739 740 let target = null; 741 let inProcessBC = null; 742 743 if (parentBC && parentBC.currentWindowGlobal) { 744 target = parentBC.currentWindowGlobal.getActor("DOMFullscreen"); 745 inProcessBC = childBC; 746 aActor.nextMsgRecipient = [target, inProcessBC]; 747 } else { 748 target = aActor.requestOrigin; 749 } 750 751 if ( 752 !target || 753 target.hasBeenDestroyed() || 754 target.windowContext?.isInBFCache 755 ) { 756 return [null, null]; 757 } 758 return [target, inProcessBC]; 759 }, 760 761 _isRemoteBrowser(aBrowser) { 762 return gMultiProcessBrowser && aBrowser.getAttribute("remote") == "true"; 763 }, 764 765 getMouseTargetRect() { 766 return this._mouseTargetRect; 767 }, 768 769 // Event callbacks 770 _expandCallback() { 771 FullScreen.showNavToolbox(); 772 }, 773 774 onMouseEnter() { 775 this.hideNavToolbox(); 776 }, 777 778 _keyToggleCallback(aEvent) { 779 // if we can use the keyboard (eg Ctrl+L or Ctrl+E) to open the toolbars, we 780 // should provide a way to collapse them too. 781 if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) { 782 FullScreen.hideNavToolbox(); 783 } else if (aEvent.keyCode == aEvent.DOM_VK_F6) { 784 // F6 is another shortcut to the address bar, but its not covered in OpenLocation() 785 FullScreen.showNavToolbox(); 786 } 787 }, 788 789 // Checks whether we are allowed to collapse the chrome 790 _isPopupOpen: false, 791 _isChromeCollapsed: false, 792 793 _setPopupOpen(aEvent) { 794 // Popups should only veto chrome collapsing if they were opened when the chrome was not collapsed. 795 // Otherwise, they would not affect chrome and the user would expect the chrome to go away. 796 // e.g. we wouldn't want the autoscroll icon firing this event, so when the user 797 // toggles chrome when moving mouse to the top, it doesn't go away again. 798 let target = aEvent.originalTarget; 799 if (target.localName == "tooltip" || target.id == "tab-preview-panel") { 800 return; 801 } 802 if ( 803 aEvent.type == "popupshown" && 804 !FullScreen._isChromeCollapsed && 805 target.getAttribute("nopreventnavboxhide") != "true" 806 ) { 807 FullScreen._isPopupOpen = true; 808 } else if (aEvent.type == "popuphidden") { 809 FullScreen._isPopupOpen = false; 810 // Try again to hide toolbar when we close the popup. 811 FullScreen.hideNavToolbox(true); 812 } 813 }, 814 815 // UrlbarController listener method 816 onViewOpen() { 817 if (!this._isChromeCollapsed) { 818 this._isPopupOpen = true; 819 } 820 }, 821 822 // UrlbarController listener method 823 onViewClose() { 824 this._isPopupOpen = false; 825 this.hideNavToolbox(true); 826 }, 827 828 get navToolboxHidden() { 829 return this._isChromeCollapsed; 830 }, 831 832 // Autohide helpers for the context menu item 833 updateAutohideMenuitem(aItem) { 834 aItem.toggleAttribute( 835 "checked", 836 Services.prefs.getBoolPref("browser.fullscreen.autohide") 837 ); 838 }, 839 setAutohide() { 840 Services.prefs.setBoolPref( 841 "browser.fullscreen.autohide", 842 !Services.prefs.getBoolPref("browser.fullscreen.autohide") 843 ); 844 // Try again to hide toolbar when we change the pref. 845 FullScreen.hideNavToolbox(true); 846 }, 847 848 showNavToolbox(trackMouse = true) { 849 if (BrowserHandler.kiosk) { 850 return; 851 } 852 this.fullScreenToggler.hidden = true; 853 gNavToolbox.removeAttribute("fullscreenShouldAnimate"); 854 gNavToolbox.style.marginTop = ""; 855 856 if (!this._isChromeCollapsed) { 857 return; 858 } 859 860 // Track whether mouse is near the toolbox 861 if (trackMouse) { 862 let rect = gBrowser.tabpanels.getBoundingClientRect(); 863 this._mouseTargetRect = { 864 top: rect.top + 50, 865 bottom: rect.bottom, 866 left: rect.left, 867 right: rect.right, 868 }; 869 MousePosTracker.addListener(this); 870 } 871 872 this._isChromeCollapsed = false; 873 Services.obs.notifyObservers( 874 gNavToolbox, 875 "fullscreen-nav-toolbox", 876 "shown" 877 ); 878 }, 879 880 hideNavToolbox(aAnimate = false) { 881 if (this._isChromeCollapsed) { 882 return; 883 } 884 if (!Services.prefs.getBoolPref("browser.fullscreen.autohide")) { 885 return; 886 } 887 // a popup menu is open in chrome: don't collapse chrome 888 if (this._isPopupOpen) { 889 return; 890 } 891 892 // a textbox in chrome is focused (location bar anyone?): don't collapse chrome 893 // unless we are kiosk mode 894 let focused = document.commandDispatcher.focusedElement; 895 if ( 896 focused && 897 focused.ownerDocument == document && 898 focused.localName == "input" && 899 !BrowserHandler.kiosk 900 ) { 901 // But try collapse the chrome again when anything happens which can make 902 // it lose the focus. We cannot listen on "blur" event on focused here 903 // because that event can be triggered by "mousedown", and hiding chrome 904 // would cause the content to move. This combination may split a single 905 // click into two actionless halves. 906 let retryHideNavToolbox = () => { 907 // Wait for at least a frame to give it a chance to be passed down to 908 // the content. 909 requestAnimationFrame(() => { 910 setTimeout(() => { 911 // In the meantime, it's possible that we exited fullscreen somehow, 912 // so only hide the toolbox if we're still in fullscreen mode. 913 if (window.fullScreen) { 914 this.hideNavToolbox(aAnimate); 915 } 916 }, 0); 917 }); 918 window.removeEventListener("keydown", retryHideNavToolbox); 919 window.removeEventListener("click", retryHideNavToolbox); 920 }; 921 window.addEventListener("keydown", retryHideNavToolbox); 922 window.addEventListener("click", retryHideNavToolbox); 923 return; 924 } 925 926 if (!BrowserHandler.kiosk) { 927 this.fullScreenToggler.hidden = false; 928 } 929 930 if ( 931 aAnimate && 932 window.matchMedia("(prefers-reduced-motion: no-preference)").matches && 933 !BrowserHandler.kiosk 934 ) { 935 gNavToolbox.setAttribute("fullscreenShouldAnimate", true); 936 } 937 938 gNavToolbox.style.marginTop = 939 -gNavToolbox.getBoundingClientRect().height + "px"; 940 this._isChromeCollapsed = true; 941 Services.obs.notifyObservers( 942 gNavToolbox, 943 "fullscreen-nav-toolbox", 944 "hidden" 945 ); 946 947 MousePosTracker.removeListener(this); 948 }, 949 }; 950 951 ChromeUtils.defineLazyGetter(FullScreen, "_permissionNotificationIDs", () => { 952 let { PermissionUI } = ChromeUtils.importESModule( 953 "resource:///modules/PermissionUI.sys.mjs" 954 ); 955 return ( 956 Object.values(PermissionUI) 957 .filter(value => { 958 let returnValue; 959 try { 960 returnValue = value.prototype.notificationID; 961 } catch (err) { 962 if (err.message === "Not implemented.") { 963 returnValue = false; 964 } else { 965 throw err; 966 } 967 } 968 return returnValue; 969 }) 970 .map(value => value.prototype.notificationID) 971 // Additionally include webRTC permission prompt which does not use PermissionUI 972 .concat(["webRTC-shareDevices"]) 973 ); 974 });