diff options
Diffstat (limited to 'devtools/client/responsive.html/browser/swap.js')
-rw-r--r-- | devtools/client/responsive.html/browser/swap.js | 309 |
1 files changed, 309 insertions, 0 deletions
diff --git a/devtools/client/responsive.html/browser/swap.js b/devtools/client/responsive.html/browser/swap.js new file mode 100644 index 000000000..7ab028065 --- /dev/null +++ b/devtools/client/responsive.html/browser/swap.js @@ -0,0 +1,309 @@ +/* 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 + * <xul:browser> elements. In particular, <iframe mozbrowser> elements don't + * have them. + * + * Rather than duplicate the swapping code used by the browser to work around + * this, we stub out the missing properties needed for the swap to complete. + */ +function addXULBrowserDecorations(browser) { + if (browser.isRemoteBrowser == undefined) { + Object.defineProperty(browser, "isRemoteBrowser", { + get() { + return this.getAttribute("remote") == "true"; + }, + configurable: true, + enumerable: true, + }); + } + if (browser.messageManager == undefined) { + Object.defineProperty(browser, "messageManager", { + get() { + return this.frameLoader.messageManager; + }, + configurable: true, + enumerable: true, + }); + } + if (browser.outerWindowID == undefined) { + Object.defineProperty(browser, "outerWindowID", { + get() { + return browser._outerWindowID; + }, + configurable: true, + enumerable: true, + }); + } + + // It's not necessary for these to actually do anything. These properties are + // swapped between browsers in browser.xml's `swapDocShells`, and then their + // `swapBrowser` methods are called, so we define them here for that to work + // without errors. During the swap process above, these will move from the + // the new inner browser to the original tab's browser (step 4) and then to + // the temporary container tab's browser (step 7), which is then closed. + if (browser._remoteWebNavigationImpl == undefined) { + browser._remoteWebNavigationImpl = { + swapBrowser() {}, + }; + } + if (browser._remoteWebProgressManager == undefined) { + browser._remoteWebProgressManager = { + swapBrowser() {}, + }; + } +} + +function tabLoaded(tab) { + let deferred = promise.defer(); + + function handle(event) { + if (event.originalTarget != tab.linkedBrowser.contentDocument || + event.target.location.href == "about:blank") { + return; + } + tab.linkedBrowser.removeEventListener("load", handle, true); + deferred.resolve(event); + } + + tab.linkedBrowser.addEventListener("load", handle, true); + return deferred.promise; +} + +exports.swapToInnerBrowser = swapToInnerBrowser; |