/* 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/. */ "use strict"; const { Ci } = require("chrome"); const promise = require("promise"); const { Task } = require("devtools/shared/task"); const { tunnelToInnerBrowser } = require("./tunnel"); /** * Swap page content from an existing tab into a new browser within a container * page. Page state is preserved by using `swapFrameLoaders`, just like when * you move a tab to a new window. This provides a seamless transition for the * user since the page is not reloaded. * * See /devtools/docs/responsive-design-mode.md for a high level overview of how * this is used in RDM. The steps described there are copied into the code * below. * * For additional low level details about swapping browser content, * see /devtools/client/responsive.html/docs/browser-swap.md. * * @param tab * A browser tab with content to be swapped. * @param containerURL * URL to a page that holds an inner browser. * @param getInnerBrowser * Function that returns a Promise to the inner browser within the * container page. It is called with the outer browser that loaded the * container page. */ function swapToInnerBrowser({ tab, containerURL, getInnerBrowser }) { let gBrowser = tab.ownerDocument.defaultView.gBrowser; let innerBrowser; let tunnel; // Dispatch a custom event each time the _viewport content_ is swapped from one browser // to another. DevTools server code uses this to follow the content if there is an // active DevTools connection. While browser.xml does dispatch it's own SwapDocShells // event, this one is easier for DevTools to follow because it's only emitted once per // transition, instead of twice like SwapDocShells. let dispatchDevToolsBrowserSwap = (from, to) => { let CustomEvent = tab.ownerDocument.defaultView.CustomEvent; let event = new CustomEvent("DevTools:BrowserSwap", { detail: to, bubbles: true, }); from.dispatchEvent(event); }; return { start: Task.async(function* () { tab.isResponsiveDesignMode = true; // Freeze navigation temporarily to avoid "blinking" in the location bar. freezeNavigationState(tab); // 1. Create a temporary, hidden tab to load the tool UI. let containerTab = gBrowser.addTab("about:blank", { skipAnimation: true, forceNotRemote: true, }); gBrowser.hideTab(containerTab); let containerBrowser = containerTab.linkedBrowser; // Prevent the `containerURL` from ending up in the tab's history. containerBrowser.loadURIWithFlags(containerURL, { flags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, }); // Copy tab listener state flags to container tab. Each tab gets its own tab // listener and state flags which cache document loading progress. The state flags // are checked when switching tabs to update the browser UI. The later step of // `swapBrowsersAndCloseOther` will fold the state back into the main tab. let stateFlags = gBrowser._tabListeners.get(tab).mStateFlags; gBrowser._tabListeners.get(containerTab).mStateFlags = stateFlags; // 2. Mark the tool tab browser's docshell as active so the viewport frame // is created eagerly and will be ready to swap. // This line is crucial when the tool UI is loaded into a background tab. // Without it, the viewport browser's frame is created lazily, leading to // a multi-second delay before it would be possible to `swapFrameLoaders`. // Even worse than the delay, there appears to be no obvious event fired // after the frame is set lazily, so it's unclear how to know that work // has finished. containerBrowser.docShellIsActive = true; // 3. Create the initial viewport inside the tool UI. // The calling application will use container page loaded into the tab to // do whatever it needs to create the inner browser. yield tabLoaded(containerTab); innerBrowser = yield getInnerBrowser(containerBrowser); addXULBrowserDecorations(innerBrowser); if (innerBrowser.isRemoteBrowser != tab.linkedBrowser.isRemoteBrowser) { throw new Error("The inner browser's remoteness must match the " + "original tab."); } // 4. Swap tab content from the regular browser tab to the browser within // the viewport in the tool UI, preserving all state via // `gBrowser._swapBrowserDocShells`. dispatchDevToolsBrowserSwap(tab.linkedBrowser, innerBrowser); gBrowser._swapBrowserDocShells(tab, innerBrowser); // 5. Force the original browser tab to be non-remote since the tool UI // must be loaded in the parent process, and we're about to swap the // tool UI into this tab. gBrowser.updateBrowserRemoteness(tab.linkedBrowser, false); // 6. Swap the tool UI (with viewport showing the content) into the // original browser tab and close the temporary tab used to load the // tool via `swapBrowsersAndCloseOther`. gBrowser.swapBrowsersAndCloseOther(tab, containerTab); // 7. Start a tunnel from the tool tab's browser to the viewport browser // so that some browser UI functions, like navigation, are connected to // the content in the viewport, instead of the tool page. tunnel = tunnelToInnerBrowser(tab.linkedBrowser, innerBrowser); yield tunnel.start(); // Swapping browsers disconnects the find bar UI from the browser. // If the find bar has been initialized, reconnect it. if (gBrowser.isFindBarInitialized(tab)) { let findBar = gBrowser.getFindBar(tab); findBar.browser = tab.linkedBrowser; if (!findBar.hidden) { // Force the find bar to activate again, restoring the search string. findBar.onFindCommand(); } } // Force the browser UI to match the new state of the tab and browser. thawNavigationState(tab); gBrowser.setTabTitle(tab); gBrowser.updateCurrentBrowser(true); }), stop() { // 1. Stop the tunnel between outer and inner browsers. tunnel.stop(); tunnel = null; // 2. Create a temporary, hidden tab to hold the content. let contentTab = gBrowser.addTab("about:blank", { skipAnimation: true, }); gBrowser.hideTab(contentTab); let contentBrowser = contentTab.linkedBrowser; // 3. Mark the content tab browser's docshell as active so the frame // is created eagerly and will be ready to swap. contentBrowser.docShellIsActive = true; // 4. Swap tab content from the browser within the viewport in the tool UI // to the regular browser tab, preserving all state via // `gBrowser._swapBrowserDocShells`. dispatchDevToolsBrowserSwap(innerBrowser, contentBrowser); gBrowser._swapBrowserDocShells(contentTab, innerBrowser); innerBrowser = null; // Copy tab listener state flags to content tab. See similar comment in `start` // above for more details. let stateFlags = gBrowser._tabListeners.get(tab).mStateFlags; gBrowser._tabListeners.get(contentTab).mStateFlags = stateFlags; // 5. Force the original browser tab to be remote since web content is // loaded in the child process, and we're about to swap the content // into this tab. gBrowser.updateBrowserRemoteness(tab.linkedBrowser, true); // 6. Swap the content into the original browser tab and close the // temporary tab used to hold the content via // `swapBrowsersAndCloseOther`. dispatchDevToolsBrowserSwap(contentBrowser, tab.linkedBrowser); gBrowser.swapBrowsersAndCloseOther(tab, contentTab); // Swapping browsers disconnects the find bar UI from the browser. // If the find bar has been initialized, reconnect it. if (gBrowser.isFindBarInitialized(tab)) { let findBar = gBrowser.getFindBar(tab); findBar.browser = tab.linkedBrowser; if (!findBar.hidden) { // Force the find bar to activate again, restoring the search string. findBar.onFindCommand(); } } gBrowser = null; // The focus manager seems to get a little dizzy after all this swapping. If a // content element had been focused inside the viewport before stopping, it will // have lost focus. Activate the frame to restore expected focus. tab.linkedBrowser.frameLoader.activateRemoteFrame(); delete tab.isResponsiveDesignMode; }, }; } /** * Browser navigation properties we'll freeze temporarily to avoid "blinking" in the * location bar, etc. caused by the containerURL peeking through before the swap is * complete. */ const NAVIGATION_PROPERTIES = [ "currentURI", "contentTitle", "securityUI", ]; function freezeNavigationState(tab) { // Browser navigation properties we'll freeze temporarily to avoid "blinking" in the // location bar, etc. caused by the containerURL peeking through before the swap is // complete. for (let property of NAVIGATION_PROPERTIES) { let value = tab.linkedBrowser[property]; Object.defineProperty(tab.linkedBrowser, property, { get() { return value; }, configurable: true, enumerable: true, }); } } function thawNavigationState(tab) { // Thaw out the properties we froze at the beginning now that the swap is complete. for (let property of NAVIGATION_PROPERTIES) { delete tab.linkedBrowser[property]; } } /** * Browser elements that are passed to `gBrowser._swapBrowserDocShells` are * expected to have certain properties that currently exist only on * elements. In particular,