summaryrefslogtreecommitdiffstats
path: root/devtools/client/responsive.html/browser/swap.js
blob: 7ab02806511f91050f106589a25780e734878ace (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
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;