--- a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-other-tabs.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-other-tabs.js
@@ -32,11 +32,10 @@ var test = Task.async(function* () {
callInTab(tab2, "testCase");
const packet = yield paused;
is(packet.why.type, "debuggerStatement",
"Should have stopped at the debugger statement, not the other tab's breakpoint");
is(packet.frame.where.line, 3,
"Should have stopped at line 3 (debugger statement), not line 2 (other tab's breakpoint)");
- yield teardown(panel1);
yield resumeDebuggerThenCloseAndFinish(panel2);
});
--- a/devtools/client/debugger/test/mochitest/browser_dbg_host-layout.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_host-layout.js
@@ -79,34 +79,34 @@ function getHost(host) {
if (host.indexOf("window") == 0) {
return "window";
}
return host;
}
function resizeToolboxWindow(panel, host) {
let sizeOption = host.split(":")[1];
- let win = panel._toolbox._host._window;
+ let win = panel._toolbox.win.parent;
// should be the same value as BREAKPOINT_SMALL_WINDOW_WIDTH in debugger-view.js
let breakpoint = 850;
if (sizeOption == "big" && win.outerWidth <= breakpoint) {
yield resizeAndWaitForLayoutChange(panel, breakpoint + 300);
} else if (sizeOption == "small" && win.outerWidth >= breakpoint) {
yield resizeAndWaitForLayoutChange(panel, breakpoint - 300);
} else {
info("Window resize unnecessary for host " + host);
}
}
function resizeAndWaitForLayoutChange(panel, width) {
info("Updating toolbox window width to " + width);
- let win = panel._toolbox._host._window;
+ let win = panel._toolbox.win.parent;
let gDebugger = panel.panelWin;
win.resizeTo(width, window.screen.availHeight);
yield waitEventOnce(gDebugger, gDebugger.EVENTS.LAYOUT_CHANGED);
}
function testHost(aPanel, aHostType, aLayoutType) {
let gDebugger = aPanel.panelWin;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
@@ -29,18 +29,27 @@ add_task(function* () {
let { workers } = yield listWorkers(tabClient);
let [, workerClient] = yield attachWorker(tabClient,
findWorker(workers, WORKER_URL));
let toolbox = yield gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
"jsdebugger",
Toolbox.HostType.WINDOW);
- is(toolbox._host.type, "window", "correct host");
- ok(toolbox._host._window.document.title.includes(WORKER_URL),
+ is(toolbox.hostType, "window", "correct host");
+
+ yield new Promise(done => {
+ toolbox.win.parent.addEventListener("message", function onmessage(event) {
+ if (event.data.name == "set-host-title") {
+ toolbox.win.parent.removeEventListener("message", onmessage);
+ done();
+ }
+ });
+ });
+ ok(toolbox.win.parent.document.title.includes(WORKER_URL),
"worker URL in host title");
let toolTabs = toolbox.doc.querySelectorAll(".devtools-tab");
let activeTools = [...toolTabs].map(tab=>tab.getAttribute("toolid"));
is(activeTools.join(","), "webconsole,jsdebugger,scratchpad,options",
"Correct set of tools supported by worker");
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -746,16 +746,19 @@ AddonDebugger.prototype = {
groupmap.get(group).get(label).url = source.url.split(" -> ").pop();
}
return groups;
}),
_onMessage: function (event) {
+ if (typeof(event.data) !== "string") {
+ return;
+ }
let json = JSON.parse(event.data);
switch (json.name) {
case "toolbox-title":
this.title = json.data.value;
break;
}
}
};
--- a/devtools/client/framework/devtools.js
+++ b/devtools/client/framework/devtools.js
@@ -5,24 +5,26 @@
"use strict";
const Services = require("Services");
const promise = require("promise");
const defer = require("devtools/shared/defer");
// Load gDevToolsBrowser toolbox lazily as they need gDevTools to be fully initialized
loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
+loader.lazyRequireGetter(this, "ToolboxHostManager", "devtools/client/framework/toolbox-host-manager", true);
loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
const {defaultTools: DefaultTools, defaultThemes: DefaultThemes} =
require("devtools/client/definitions");
const EventEmitter = require("devtools/shared/event-emitter");
const {JsonView} = require("devtools/client/jsonview/main");
const AboutDevTools = require("devtools/client/framework/about-devtools-toolbox");
const {when: unload} = require("sdk/system/unload");
+const {Task} = require("devtools/shared/task");
const FORBIDDEN_IDS = new Set(["toolbox", ""]);
const MAX_ORDINAL = 99;
/**
* DevTools is a class that represents a set of developer tools, it holds a
* set of tools and keeps track of open toolboxes in the browser.
*/
@@ -392,64 +394,52 @@ DevTools.prototype = {
* @param {Toolbox.HostType} hostType
* The type of host (bottom, window, side)
* @param {object} hostOptions
* Options for host specifically
*
* @return {Toolbox} toolbox
* The toolbox that was opened
*/
- showToolbox: function (target, toolId, hostType, hostOptions) {
- let deferred = defer();
-
+ showToolbox: Task.async(function* (target, toolId, hostType, hostOptions) {
let toolbox = this._toolboxes.get(target);
if (toolbox) {
- let hostPromise = (hostType != null && toolbox.hostType != hostType) ?
- toolbox.switchHost(hostType) :
- promise.resolve(null);
+ if (hostType != null && toolbox.hostType != hostType) {
+ yield toolbox.switchHost(hostType);
+ }
if (toolId != null && toolbox.currentToolId != toolId) {
- hostPromise = hostPromise.then(function () {
- return toolbox.selectTool(toolId);
- });
+ yield toolbox.selectTool(toolId);
}
- return hostPromise.then(function () {
- toolbox.raise();
- return toolbox;
- });
- }
- else {
- // No toolbox for target, create one
- toolbox = new Toolbox(target, toolId, hostType, hostOptions);
+ toolbox.raise();
+ } else {
+ let manager = new ToolboxHostManager(target, hostType, hostOptions);
+
+ toolbox = yield manager.create(toolId);
+ this._toolboxes.set(target, toolbox);
this.emit("toolbox-created", toolbox);
- this._toolboxes.set(target, toolbox);
-
toolbox.once("destroy", () => {
this.emit("toolbox-destroy", target);
});
toolbox.once("destroyed", () => {
this._toolboxes.delete(target);
this.emit("toolbox-destroyed", target);
});
- // If toolId was passed in, it will already be selected before the
- // open promise resolves.
- toolbox.open().then(() => {
- deferred.resolve(toolbox);
- this.emit("toolbox-ready", toolbox);
- });
+ yield toolbox.open();
+ this.emit("toolbox-ready", toolbox);
}
- return deferred.promise;
- },
+ return toolbox;
+ }),
/**
* Return the toolbox for a given target.
*
* @param {object} target
* Target value e.g. the target that owns this toolbox
*
* @return {Toolbox} toolbox
--- a/devtools/client/framework/moz.build
+++ b/devtools/client/framework/moz.build
@@ -20,13 +20,14 @@ DevToolsModules(
'menu-item.js',
'menu.js',
'selection.js',
'sidebar.js',
'source-map-service.js',
'target-from-url.js',
'target.js',
'toolbox-highlighter-utils.js',
+ 'toolbox-host-manager.js',
'toolbox-hosts.js',
'toolbox-options.js',
'toolbox.js',
'ToolboxProcess.jsm',
)
--- a/devtools/client/framework/test/browser_devtools_api.js
+++ b/devtools/client/framework/test/browser_devtools_api.js
@@ -66,17 +66,17 @@ function runTests1(aTab) {
toolbox.once(toolId1 + "-ready", (event, panel) => {
ok(panel, "panel argument available");
events["ready"] = true;
});
});
gDevTools.showToolbox(target, toolId1).then(function (toolbox) {
is(toolbox.target, target, "toolbox target is correct");
- is(toolbox._host.hostTab, gBrowser.selectedTab, "toolbox host is correct");
+ is(toolbox.target.tab, gBrowser.selectedTab, "targeted tab is correct");
ok(events["init"], "init event fired");
ok(events["ready"], "ready event fired");
gDevTools.unregisterTool(toolId1);
// Wait for unregisterTool to select the next tool before calling runTests2,
// otherwise we will receive the wrong select event when waiting for
@@ -134,17 +134,17 @@ function runTests2() {
toolbox.once(toolId2 + "-ready", (event, panel) => {
ok(panel, "panel argument available");
events["ready"] = true;
});
});
gDevTools.showToolbox(target, toolId2).then(function (toolbox) {
is(toolbox.target, target, "toolbox target is correct");
- is(toolbox._host.hostTab, gBrowser.selectedTab, "toolbox host is correct");
+ is(toolbox.target.tab, gBrowser.selectedTab, "targeted tab is correct");
ok(events["init"], "init event fired");
ok(events["build"], "build event fired");
ok(events["ready"], "ready event fired");
continueTests(toolbox);
});
}
--- a/devtools/client/framework/test/browser_toolbox_custom_host.js
+++ b/devtools/client/framework/test/browser_toolbox_custom_host.js
@@ -19,16 +19,19 @@ function test() {
target = TargetFactory.forTab(tab);
let options = {customIframe: iframe};
gDevTools.showToolbox(target, null, Toolbox.HostType.CUSTOM, options)
.then(testCustomHost, console.error)
.then(null, console.error);
});
function onMessage(event) {
+ if (typeof(event.data) !== "string") {
+ return;
+ }
info("onMessage: " + event.data);
let json = JSON.parse(event.data);
if (json.name == "toolbox-close") {
ok("Got the `toolbox-close` message");
window.removeEventListener("message", onMessage);
cleanup();
}
}
--- a/devtools/client/framework/test/browser_toolbox_raise.js
+++ b/devtools/client/framework/test/browser_toolbox_raise.js
@@ -37,35 +37,35 @@ function testBottomHost(aToolbox) {
function testWindowHost() {
// Make sure toolbox is not focused.
window.addEventListener("focus", onFocus, true);
// Need to wait for focus as otherwise window.focus() is overridden by
// toolbox window getting focused first on Linux and Mac.
let onToolboxFocus = () => {
- toolbox._host._window.removeEventListener("focus", onToolboxFocus, true);
+ toolbox.win.parent.removeEventListener("focus", onToolboxFocus, true);
info("focusing main window.");
window.focus();
};
// Need to wait for toolbox window to get focus.
- toolbox._host._window.addEventListener("focus", onToolboxFocus, true);
+ toolbox.win.parent.addEventListener("focus", onToolboxFocus, true);
}
function onFocus() {
info("Main window is focused before calling toolbox.raise()");
window.removeEventListener("focus", onFocus, true);
// Check if toolbox window got focus.
let onToolboxFocusAgain = () => {
- toolbox._host._window.removeEventListener("focus", onToolboxFocusAgain, false);
+ toolbox.win.parent.removeEventListener("focus", onToolboxFocusAgain, false);
ok(true, "Toolbox window is the focused window after calling toolbox.raise()");
cleanup();
};
- toolbox._host._window.addEventListener("focus", onToolboxFocusAgain, false);
+ toolbox.win.parent.addEventListener("focus", onToolboxFocusAgain, false);
// Now raise toolbox.
toolbox.raise();
}
function cleanup() {
Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM);
--- a/devtools/client/framework/test/browser_toolbox_toggle.js
+++ b/devtools/client/framework/test/browser_toolbox_toggle.js
@@ -77,17 +77,17 @@ function* testToggleDetachedToolbox(tab,
let onMainWindowFocus = once(window, "focus");
window.focus();
yield onMainWindowFocus;
ok(true, "Main window focused");
info("Verify windowed toolbox is focused instead of closed when using " +
"toggle key from the main window");
- let toolboxWindow = toolbox._host._window;
+ let toolboxWindow = toolbox.win.top;
let onToolboxWindowFocus = once(toolboxWindow, "focus", true);
EventUtils.synthesizeKey(key, modifiers);
yield onToolboxWindowFocus;
ok(true, "Toolbox focused and not destroyed");
info("Verify windowed toolbox is destroyed when using toggle key from its " +
"own window");
--- a/devtools/client/framework/test/browser_toolbox_window_title_changes.js
+++ b/devtools/client/framework/test/browser_toolbox_window_title_changes.js
@@ -23,55 +23,66 @@ function test() {
addTab(URL_1).then(function () {
let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, null, Toolbox.HostType.BOTTOM)
.then(function (aToolbox) { toolbox = aToolbox; })
.then(() => toolbox.selectTool(TOOL_ID_1))
// undock toolbox and check title
- .then(() => toolbox.switchHost(Toolbox.HostType.WINDOW))
+ .then(() => {
+ // We have to first switch the host in order to spawn the new top level window
+ // on which we are going to listen from title change event
+ return toolbox.switchHost(Toolbox.HostType.WINDOW)
+ .then(() => waitForTitleChange(toolbox));
+ })
.then(checkTitle.bind(null, NAME_1, URL_1, "toolbox undocked"))
// switch to different tool and check title
- .then(() => toolbox.selectTool(TOOL_ID_2))
+ .then(() => {
+ let onTitleChanged = waitForTitleChange(toolbox);
+ toolbox.selectTool(TOOL_ID_2);
+ return onTitleChanged;
+ })
.then(checkTitle.bind(null, NAME_1, URL_1, "tool changed"))
// navigate to different local url and check title
.then(function () {
- let deferred = defer();
- target.once("navigate", () => deferred.resolve());
+ let onTitleChanged = waitForTitleChange(toolbox);
gBrowser.loadURI(URL_2);
- return deferred.promise;
+ return onTitleChanged;
})
.then(checkTitle.bind(null, NAME_2, URL_2, "url changed"))
// navigate to a real url and check title
.then(() => {
- let deferred = defer();
- target.once("navigate", () => deferred.resolve());
+ let onTitleChanged = waitForTitleChange(toolbox);
gBrowser.loadURI(URL_3);
- return deferred.promise;
+ return onTitleChanged;
})
.then(checkTitle.bind(null, NAME_3, URL_3, "url changed"))
// destroy toolbox, create new one hosted in a window (with a
// different tool id), and check title
.then(function () {
// Give the tools a chance to handle the navigation event before
// destroying the toolbox.
executeSoon(function () {
toolbox.destroy()
.then(function () {
// After destroying the toolbox, a fresh target is required.
target = TargetFactory.forTab(gBrowser.selectedTab);
return gDevTools.showToolbox(target, null, Toolbox.HostType.WINDOW);
})
.then(function (aToolbox) { toolbox = aToolbox; })
- .then(() => toolbox.selectTool(TOOL_ID_1))
+ .then(() => {
+ let onTitleChanged = waitForTitleChange(toolbox);
+ toolbox.selectTool(TOOL_ID_1);
+ return onTitleChanged;
+ })
.then(checkTitle.bind(null, NAME_3, URL_3,
"toolbox destroyed and recreated"))
// clean up
.then(() => toolbox.destroy())
.then(function () {
toolbox = null;
gBrowser.removeCurrentTab();
--- a/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
+++ b/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
@@ -20,18 +20,24 @@ const IFRAME_URL = URL_ROOT + "browser_t
add_task(function* () {
Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true);
yield addTab(URL);
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = yield gDevTools.showToolbox(target, null,
Toolbox.HostType.BOTTOM);
+ let onTitleChanged = waitForTitleChange(toolbox);
yield toolbox.selectTool("inspector");
+ yield onTitleChanged;
+
yield toolbox.switchHost(Toolbox.HostType.WINDOW);
+ // Wait for title change event *after* switch host, in order to listen
+ // for the event on the WINDOW host window, which only exists after switchHost
+ yield waitForTitleChange(toolbox);
is(getTitle(), `Developer Tools - Page title - ${URL}`,
"Devtools title correct after switching to detached window host");
// Wait for tick to avoid unexpected 'popuphidden' event, which
// blocks the frame popup menu opened below. See also bug 1276873
yield waitForTick();
@@ -51,24 +57,27 @@ add_task(function* () {
let topFrameBtn = frames.filter(b => b.label == URL)[0];
let iframeBtn = frames.filter(b => b.label == IFRAME_URL)[0];
ok(topFrameBtn, "Got top level document in the list");
ok(iframeBtn, "Got iframe document in the list");
// Listen to will-navigate to check if the view is empty
let willNavigate = toolbox.target.once("will-navigate");
+ onTitleChanged = waitForTitleChange(toolbox);
+
// Only select the iframe after we are able to select an element from the top
// level document.
let newRoot = toolbox.getPanel("inspector").once("new-root");
info("Select the iframe");
iframeBtn.click();
yield willNavigate;
yield newRoot;
+ yield onTitleChanged;
info("Navigation to the iframe is done, the inspector should be back up");
is(getTitle(), `Developer Tools - Page title - ${URL}`,
"Devtools title was not updated after changing inspected frame");
info("Cleanup toolbox and test preferences.");
yield toolbox.destroy();
toolbox = null;
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -574,8 +574,22 @@ function emptyClipboard() {
}
/**
* Check if the current operating system is Windows.
*/
function isWindows() {
return Services.appinfo.OS === "WINNT";
}
+
+/**
+ * Wait for a given toolbox to get its title updated.
+ */
+function waitForTitleChange(toolbox) {
+ let deferred = defer();
+ toolbox.win.parent.addEventListener("message", function onmessage(event) {
+ if (event.data.name == "set-host-title") {
+ toolbox.win.parent.removeEventListener("message", onmessage);
+ deferred.resolve();
+ }
+ });
+ return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/toolbox-host-manager.js
@@ -0,0 +1,258 @@
+const Services = require("Services");
+const {Ci} = require("chrome");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/locale/toolbox.properties");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const {Task} = require("devtools/shared/task");
+
+loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
+loader.lazyRequireGetter(this, "Hosts", "devtools/client/framework/toolbox-hosts", true);
+
+/**
+ * Implement a wrapper on the chrome side to setup a Toolbox within Firefox UI.
+ *
+ * This component handles iframe creation within Firefox, in which we are loading
+ * the toolbox document. Then both the chrome and the toolbox document communicate
+ * via "message" events.
+ *
+ * Messages sent by the toolbox to the chrome:
+ * - switch-host:
+ * Order to display the toolbox in another host (side, bottom or window)
+ * - switch-to-previous-host:
+ * Order to display the toolbox in the previously used host
+ * - toggle-minimize-mode:
+ * When using the bottom host, the toolbox can be miximized to only display
+ * the tool titles
+ * - maximize-host:
+ * When using the bottom host in minimized mode, revert back to regular mode
+ * in order to see tool titles and the tools
+ * - raise-host:
+ * Focus the tools
+ * - set-host-title:
+ * When using the window host, update the window title
+ *
+ * Messages sent by the chrome to the toolbox:
+ * - host-minimized:
+ * The bottom host is done minimizing (after animation end)
+ * - host-maximized:
+ * The bottom host is done switching back to regular mode (after animation
+ * end)
+ * - switched-host:
+ * The `switch-host` command sent by the toolbox is done
+ */
+
+const LAST_HOST = "devtools.toolbox.host";
+const PREVIOUS_HOST = "devtools.toolbox.previousHost";
+let ID_COUNTER = 1;
+
+function ToolboxHostManager(target, hostType, hostOptions) {
+ this.target = target;
+
+ this.frameId = ID_COUNTER++;
+
+ if (!hostType) {
+ hostType = Services.prefs.getCharPref(LAST_HOST);
+ }
+ this.onHostMinimized = this.onHostMinimized.bind(this);
+ this.onHostMaximized = this.onHostMaximized.bind(this);
+ this.host = this.createHost(hostType, hostOptions);
+ this.hostType = hostType;
+}
+
+ToolboxHostManager.prototype = {
+ create: Task.async(function* (toolId) {
+ yield this.host.create();
+
+ this.host.frame.setAttribute("aria-label", L10N.getStr("toolbox.label"));
+ this.host.frame.ownerDocument.defaultView.addEventListener("message", this);
+ // We have to listen on capture as no event fires on bubble
+ this.host.frame.addEventListener("unload", this, true);
+
+ let toolbox = new Toolbox(this.target, toolId, this.host.type, this.host.frame.contentWindow, this.frameId);
+
+ // Prevent reloading the toolbox when loading the tools in a tab (e.g. from about:debugging)
+ if (!this.host.frame.contentWindow.location.href.startsWith("about:devtools-toolbox")) {
+ this.host.frame.setAttribute("src", "about:devtools-toolbox");
+ }
+
+ return toolbox;
+ }),
+
+ handleEvent(event) {
+ switch(event.type) {
+ case "message":
+ this.onMessage(event);
+ break;
+ case "unload":
+ // On unload, host iframe already lost its contentWindow attribute, so
+ // we can only compare against locations. Here we filter two very
+ // different cases: preliminary about:blank document as well as iframes
+ // like tool iframes.
+ if (!event.target.location.href.startsWith("about:devtools-toolbox")) {
+ break;
+ }
+ // Don't destroy the host during unload event (esp., don't remove the
+ // iframe from DOM!). Otherwise the unload event for the toolbox
+ // document doesn't fire within the toolbox *document*! This is
+ // the unload event that fires on the toolbox *iframe*.
+ DevToolsUtils.executeSoon(() => {
+ this.destroy();
+ });
+ break;
+ }
+ },
+
+ onMessage(event) {
+ if (!event.data) {
+ return;
+ }
+ // Toolbox document is still chrome and disallow identifying message
+ // origin via event.source as it is null. So use a custom id.
+ if (event.data.frameId != this.frameId) {
+ return;
+ }
+ switch (event.data.name) {
+ case "switch-host":
+ this.switchHost(event.data.hostType);
+ break;
+ case "switch-to-previous-host":
+ this.switchToPreviousHost();
+ break;
+ case "maximize-host":
+ this.host.maximize();
+ break;
+ case "raise-host":
+ this.host.raise();
+ break;
+ case "toggle-minimize-mode":
+ this.host.toggleMinimizeMode(event.data.toolbarHeight);
+ break;
+ case "set-host-title":
+ this.host.setTitle(event.data.title);
+ break;
+ }
+ },
+
+ postMessage(data) {
+ let window = this.host.frame.contentWindow;
+ window.postMessage(data, "*");
+ },
+
+ destroy() {
+ this.destroyHost();
+ this.host = null;
+ this.hostType = null;
+ this.target = null;
+ },
+
+ /**
+ * Create a host object based on the given host type.
+ *
+ * Warning: bottom and sidebar hosts require that the toolbox target provides
+ * a reference to the attached tab. Not all Targets have a tab property -
+ * make sure you correctly mix and match hosts and targets.
+ *
+ * @param {string} hostType
+ * The host type of the new host object
+ *
+ * @return {Host} host
+ * The created host object
+ */
+ createHost(hostType, options) {
+ if (!Hosts[hostType]) {
+ throw new Error("Unknown hostType: " + hostType);
+ }
+
+ let newHost = new Hosts[hostType](this.target.tab, options);
+ // Update the label and icon when the state changes.
+ newHost.on("minimized", this.onHostMinimized);
+ newHost.on("maximized", this.onHostMaximized);
+ return newHost;
+ },
+
+ onHostMinimized() {
+ this.postMessage({
+ name: "host-minimized"
+ });
+ },
+
+ onHostMaximized() {
+ this.postMessage({
+ name: "host-maximized"
+ });
+ },
+
+ /**
+ * Switch to the last used host for the toolbox UI.
+ * This is determined by the devtools.toolbox.previousHost pref.
+ */
+ switchToPreviousHost() {
+ let hostType = Services.prefs.getCharPref(PREVIOUS_HOST);
+
+ // Handle the case where the previous host happens to match the current
+ // host. If so, switch to bottom if it's not already used, and side if not.
+ if (hostType === this.hostType) {
+ if (hostType === Toolbox.HostType.BOTTOM) {
+ hostType = Toolbox.HostType.SIDE;
+ } else {
+ hostType = Toolbox.HostType.BOTTOM;
+ }
+ }
+
+ return this.switchHost(hostType);
+ },
+
+ switchHost: Task.async(function* (hostType) {
+ let iframe = this.host.frame;
+ let newHost = this.createHost(hostType);
+ let newIframe = yield newHost.create();
+ // change toolbox document's parent to the new host
+ newIframe.swapFrameLoaders(iframe);
+
+ // See bug 1022726, most probably because of swapFrameLoaders we need to
+ // first focus the window here, and then once again further from
+ // toolbox.js to make sure focus actually happens.
+ iframe.contentWindow.focus();
+
+ this.destroyHost();
+
+ if (this.hostType != Toolbox.HostType.CUSTOM) {
+ Services.prefs.setCharPref(PREVIOUS_HOST, this.hostType);
+ }
+
+ this.host = newHost;
+ this.hostType = hostType;
+ this.host.setTitle(this.host.frame.contentWindow.document.title);
+ this.host.frame.ownerDocument.defaultView.addEventListener("message", this);
+ this.host.frame.addEventListener("unload", this, true);
+
+ if (hostType != Toolbox.HostType.CUSTOM) {
+ Services.prefs.setCharPref(LAST_HOST, hostType);
+ }
+
+ // Tell the toolbox the host changed
+ this.postMessage({
+ name: "switched-host",
+ hostType
+ });
+ }),
+
+ /**
+ * Destroy the current host, and remove event listeners from its frame.
+ *
+ * @return {promise} to be resolved when the host is destroyed.
+ */
+ destroyHost() {
+ // When Firefox toplevel is closed, the frame may already be detached and
+ // the top level document gone
+ if (this.host.frame.ownerDocument.defaultView) {
+ this.host.frame.ownerDocument.defaultView.removeEventListener("message", this);
+ }
+ this.host.frame.removeEventListener("unload", this, true);
+
+ this.host.off("minimized", this.onHostMinimized);
+ this.host.off("maximized", this.onHostMaximized);
+ return this.host.destroy();
+ }
+};
+exports.ToolboxHostManager = ToolboxHostManager;
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -35,18 +35,16 @@ const { BrowserLoader } =
const {LocalizationHelper} = require("devtools/shared/l10n");
const L10N = new LocalizationHelper("devtools/locale/toolbox.properties");
loader.lazyRequireGetter(this, "CommandUtils",
"devtools/client/shared/developer-toolbar", true);
loader.lazyRequireGetter(this, "getHighlighterUtils",
"devtools/client/framework/toolbox-highlighter-utils", true);
-loader.lazyRequireGetter(this, "Hosts",
- "devtools/client/framework/toolbox-hosts", true);
loader.lazyRequireGetter(this, "Selection",
"devtools/client/framework/selection", true);
loader.lazyRequireGetter(this, "InspectorFront",
"devtools/shared/fronts/inspector", true);
loader.lazyRequireGetter(this, "flags",
"devtools/shared/flags");
loader.lazyRequireGetter(this, "showDoorhanger",
"devtools/client/shared/doorhanger", true);
@@ -98,22 +96,27 @@ const ToolboxButtons = exports.ToolboxBu
* the iframes where the tool panels will be living in.
*
* @param {object} target
* The object the toolbox is debugging.
* @param {string} selectedTool
* Tool to select initially
* @param {Toolbox.HostType} hostType
* Type of host that will host the toolbox (e.g. sidebar, window)
- * @param {object} hostOptions
- * Options for host specifically
+ * @param {DOMWindow} contentWindow
+ * The window object of the toolbox document
+ * @param {string} frameId
+ * A unique identifier to differentiate toolbox documents from the
+ * chrome codebase when passing DOM messages
*/
-function Toolbox(target, selectedTool, hostType, hostOptions) {
+function Toolbox(target, selectedTool, hostType, contentWindow, frameId) {
this._target = target;
- this._win = null;
+ this._win = contentWindow;
+ this.frameId = frameId;
+
this._toolPanels = new Map();
this._telemetry = new Telemetry();
if (Services.prefs.getBoolPref("devtools.sourcemap.locations.enabled")) {
this._sourceMapService = new SourceMapService(this._target);
}
this._initInspector = null;
this._inspector = null;
@@ -131,39 +134,36 @@ function Toolbox(target, selectedTool, h
this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this);
this.destroy = this.destroy.bind(this);
this.highlighterUtils = getHighlighterUtils(this);
this._highlighterReady = this._highlighterReady.bind(this);
this._highlighterHidden = this._highlighterHidden.bind(this);
this._prefChanged = this._prefChanged.bind(this);
this._saveSplitConsoleHeight = this._saveSplitConsoleHeight.bind(this);
this._onFocus = this._onFocus.bind(this);
+ this._onBrowserMessage = this._onBrowserMessage.bind(this);
this._showDevEditionPromo = this._showDevEditionPromo.bind(this);
this._updateTextBoxMenuItems = this._updateTextBoxMenuItems.bind(this);
this._onBottomHostMinimized = this._onBottomHostMinimized.bind(this);
this._onBottomHostMaximized = this._onBottomHostMaximized.bind(this);
this._onToolSelectWhileMinimized = this._onToolSelectWhileMinimized.bind(this);
this._onPerformanceFrontEvent = this._onPerformanceFrontEvent.bind(this);
this._onBottomHostWillChange = this._onBottomHostWillChange.bind(this);
this._toggleMinimizeMode = this._toggleMinimizeMode.bind(this);
this._onTabbarFocus = this._onTabbarFocus.bind(this);
this._onTabbarArrowKeypress = this._onTabbarArrowKeypress.bind(this);
this._target.on("close", this.destroy);
- if (!hostType) {
- hostType = Services.prefs.getCharPref(this._prefs.LAST_HOST);
- }
if (!selectedTool) {
selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
}
this._defaultToolId = selectedTool;
- this._hostOptions = hostOptions;
- this._host = this._createHost(hostType, hostOptions);
+ this._hostType = hostType;
EventEmitter.decorate(this);
this._target.on("navigate", this._refreshHostTitle);
this._target.on("frame-update", this._updateFrames);
this.on("host-changed", this._refreshHostTitle);
this.on("select", this._refreshHostTitle);
@@ -185,20 +185,18 @@ Toolbox.HostType = {
WINDOW: "window",
CUSTOM: "custom"
};
Toolbox.prototype = {
_URL: "about:devtools-toolbox",
_prefs: {
- LAST_HOST: "devtools.toolbox.host",
LAST_TOOL: "devtools.toolbox.selectedTool",
SIDE_ENABLED: "devtools.toolbox.sideEnabled",
- PREVIOUS_HOST: "devtools.toolbox.previousHost"
},
currentToolId: null,
lastUsedToolId: null,
/**
* Returns a *copy* of the _toolPanels collection.
*
@@ -265,17 +263,17 @@ Toolbox.prototype = {
return this._threadClient;
},
/**
* Get/alter the host of a Toolbox, i.e. is it in browser or in a separate
* tab. See HostType for more details.
*/
get hostType() {
- return this._host.type;
+ return this._hostType;
},
/**
* Shortcut to the window containing the toolbox UI
*/
get win() {
return this._win;
},
@@ -348,37 +346,28 @@ Toolbox.prototype = {
this.doc.querySelector("#toolbox-panel-iframe-webconsole").contentWindow;
},
/**
* Open the toolbox
*/
open: function () {
return Task.spawn(function* () {
- let iframe = yield this._host.create();
- this._win = iframe.contentWindow;
-
- let domReady = defer();
-
- // Prevent reloading the document when the toolbox is opened in a tab
- let location = iframe.contentWindow.location.href;
- if (!location.startsWith(this._URL)) {
- iframe.setAttribute("src", this._URL);
- } else {
- // Update the URL so that onceDOMReady watch for the right url.
- this._URL = location;
- }
-
this.browserRequire = BrowserLoader({
window: this.doc.defaultView,
useOnlyShared: true
}).require;
- iframe.setAttribute("aria-label", L10N.getStr("toolbox.label"));
- let domHelper = new DOMHelpers(iframe.contentWindow);
+ if (this.win.location.href.startsWith(this._URL)) {
+ // Update the URL so that onceDOMReady watch for the right url.
+ this._URL = this.win.location.href;
+ }
+
+ let domReady = defer();
+ let domHelper = new DOMHelpers(this.win);
domHelper.onceDOMReady(() => {
domReady.resolve();
}, this._URL);
// Optimization: fire up a few other things before waiting on
// the iframe being ready (makes startup faster)
// Load the toolbox-level actor fronts and utilities now
@@ -621,24 +610,48 @@ Toolbox.prototype = {
(name, event) => {
this.switchToPreviousHost();
event.preventDefault();
});
this.doc.addEventListener("keypress", this._splitConsoleOnKeypress, false);
this.doc.addEventListener("focus", this._onFocus, true);
this.win.addEventListener("unload", this.destroy);
+ this.win.addEventListener("message", this._onBrowserMessage, true);
},
_removeHostListeners: function () {
// The host iframe's contentDocument may already be gone.
if (this.doc) {
this.doc.removeEventListener("keypress", this._splitConsoleOnKeypress, false);
this.doc.removeEventListener("focus", this._onFocus, true);
this.win.removeEventListener("unload", this.destroy);
+ this.win.removeEventListener("message", this._onBrowserMessage, true);
+ }
+ },
+
+ // Called whenever the chrome send a message
+ _onBrowserMessage: function (event) {
+ if (!event.data) {
+ return;
+ }
+ switch (event.data.name) {
+ case "switched-host":
+ this._onSwitchedHost(event.data);
+ break;
+ case "host-minimized":
+ if (this.hostType == Toolbox.HostType.BOTTOM) {
+ this._onBottomHostMinimized();
+ }
+ break;
+ case "host-maximized":
+ if (this.hostType == Toolbox.HostType.BOTTOM) {
+ this._onBottomHostMaximized();
+ }
+ break;
}
},
_registerOverlays: function () {
registerHarOverlay(this);
},
_saveSplitConsoleHeight: function () {
@@ -792,19 +805,16 @@ Toolbox.prototype = {
the UI for it, and until bug 1173849 is fixed too. */
minimizeBtn.setAttribute("hidden", "true");
minimizeBtn.addEventListener("click", this._toggleMinimizeMode);
dockBox.appendChild(minimizeBtn);
// Show the button in its maximized state.
this._onBottomHostMaximized();
- // Update the label and icon when the state changes.
- this._host.on("minimized", this._onBottomHostMinimized);
- this._host.on("maximized", this._onBottomHostMaximized);
// Maximize again when a tool gets selected.
this.on("before-select", this._onToolSelectWhileMinimized);
// Maximize and stop listening before the host type changes.
this.once("host-will-change", this._onBottomHostWillChange);
}
if (this.hostType == Toolbox.HostType.WINDOW) {
this.closeButton.setAttribute("hidden", "true");
@@ -853,37 +863,53 @@ Toolbox.prototype = {
btn.className = "maximized";
btn.setAttribute("title",
L10N.getStr("toolboxDockButtons.bottom.minimize") + " " +
this._getMinimizeButtonShortcutTooltip());
},
_onToolSelectWhileMinimized: function () {
- this._host.maximize();
+ this.postMessage({
+ name: "maximize-host"
+ });
+ },
+
+ postMessage: function (msg) {
+ // We sometime try to send messages in middle of destroy(), where the
+ // toolbox iframe may already be detached and no longer have a parent.
+ if (this.win.parent) {
+ // Toolbox document is still chrome and disallow identifying message
+ // origin via event.source as it is null. So use a custom id.
+ msg.frameId = this.frameId;
+ this.win.parent.postMessage(msg, "*");
+ }
},
_onBottomHostWillChange: function () {
- this._host.maximize();
+ this.postMessage({
+ name: "maximize-host"
+ });
- this._host.off("minimized", this._onBottomHostMinimized);
- this._host.off("maximized", this._onBottomHostMaximized);
this.off("before-select", this._onToolSelectWhileMinimized);
},
_toggleMinimizeMode: function () {
if (this.hostType !== Toolbox.HostType.BOTTOM) {
return;
}
// Calculate the height to which the host should be minimized so the
// tabbar is still visible.
let toolbarHeight = this.tabbar.getBoxQuads({box: "content"})[0].bounds
.height;
- this._host.toggleMinimizeMode(toolbarHeight);
+ this.postMessage({
+ name: "toggle-minimize-mode",
+ toolbarHeight
+ });
},
/**
* Add tabs to the toolbox UI for registered tools
*/
_buildTabs: function () {
for (let definition of gDevTools.getToolDefinitionArray()) {
this._buildTabForTool(definition);
@@ -1606,31 +1632,36 @@ Toolbox.prototype = {
let tab = this.doc.getElementById("toolbox-tab-" + id);
tab && tab.removeAttribute("highlighted");
},
/**
* Raise the toolbox host.
*/
raise: function () {
- this._host.raise();
+ this.postMessage({
+ name: "raise-host"
+ });
},
/**
* Refresh the host's title.
*/
_refreshHostTitle: function () {
let title;
if (this.target.name && this.target.name != this.target.url) {
title = L10N.getFormatStr("toolbox.titleTemplate2", this.target.name,
this.target.url);
} else {
title = L10N.getFormatStr("toolbox.titleTemplate1", this.target.url);
}
- this._host.setTitle(title);
+ this.postMessage({
+ name: "set-host-title",
+ title
+ });
},
// Returns an instance of the preference actor
get _preferenceFront() {
return this.target.root.then(rootForm => {
return getPreferenceFront(this.target.client, rootForm);
});
},
@@ -1799,57 +1830,23 @@ Toolbox.prototype = {
// If non-top level frame is selected the toolbar button is
// marked as 'checked' indicating that a child frame is active.
if (!topFrameSelected && this.selectedFrameId) {
button.setAttribute("checked", "true");
}
},
/**
- * Create a host object based on the given host type.
- *
- * Warning: some hosts require that the toolbox target provides a reference to
- * the attached tab. Not all Targets have a tab property - make sure you
- * correctly mix and match hosts and targets.
- *
- * @param {string} hostType
- * The host type of the new host object
- *
- * @return {Host} host
- * The created host object
- */
- _createHost: function (hostType, options) {
- if (!Hosts[hostType]) {
- throw new Error("Unknown hostType: " + hostType);
- }
-
- // clean up the toolbox if its window is closed
- let newHost = new Hosts[hostType](this.target.tab, options);
- newHost.on("window-closed", this.destroy);
- return newHost;
- },
-
- /**
* Switch to the last used host for the toolbox UI.
- * This is determined by the devtools.toolbox.previousHost pref.
*/
switchToPreviousHost: function () {
- let hostType = Services.prefs.getCharPref(this._prefs.PREVIOUS_HOST);
-
- // Handle the case where the previous host happens to match the current
- // host. If so, switch to bottom if it's not already used, and side if not.
- if (hostType === this.hostType) {
- if (hostType === Toolbox.HostType.BOTTOM) {
- hostType = Toolbox.HostType.SIDE;
- } else {
- hostType = Toolbox.HostType.BOTTOM;
- }
- }
-
- return this.switchHost(hostType);
+ this.postMessage({
+ name: "switch-to-previous-host"
+ });
+ return this.once("host-changed");
},
/**
* Switch to a new host for the toolbox UI. E.g. bottom, sidebar, window,
* and focus the window when done.
*
* @param {string} hostType
* The host type of the new host object
@@ -1862,43 +1859,37 @@ Toolbox.prototype = {
this.emit("host-will-change", hostType);
// If we call swapFrameLoaders() when a tool if focused it leaves the
// browser in a state where it thinks that the tool is focused but in
// reality the content area is focused. Blurring the tool before calling
// swapFrameLoaders() works around this issue.
this.focusTool(this.currentToolId, false);
- let newHost = this._createHost(hostType);
- return newHost.create().then(iframe => {
- // change toolbox document's parent to the new host
- iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
- iframe.swapFrameLoaders(this._host.frame);
+ // Host code on the chrome side will send back a message once the host
+ // switched
+ this.postMessage({
+ name: "switch-host",
+ hostType
+ });
- this._host.off("window-closed", this.destroy);
- this.destroyHost();
-
- let prevHostType = this._host.type;
- this._host = newHost;
+ return this.once("host-changed");
+ },
- if (this.hostType != Toolbox.HostType.CUSTOM) {
- Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type);
- Services.prefs.setCharPref(this._prefs.PREVIOUS_HOST, prevHostType);
- }
+ _onSwitchedHost: function ({ hostType }) {
+ this._hostType = hostType;
- this._buildDockButtons();
- this._addKeysToWindow();
+ this._buildDockButtons();
+ this._addKeysToWindow();
- // Focus the tool to make sure keyboard shortcuts work straight away.
- this.focusTool(this.currentToolId, true);
+ // Focus the tool to make sure keyboard shortcuts work straight away.
+ this.focusTool(this.currentToolId, true);
- this.emit("host-changed");
-
- this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
- });
+ this.emit("host-changed");
+ this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
},
/**
* Return if the tool is available as a tab (i.e. if it's checked
* in the options panel). This is different from Toolbox.getPanel -
* a tool could be registered but not yet opened in which case
* isToolRegistered would return true but getPanel would return false.
*/
@@ -2061,26 +2052,16 @@ Toolbox.prototype = {
*
* @return The notification box component.
*/
getNotificationBox: function () {
return this.notificationBox;
},
/**
- * Destroy the current host, and remove event listeners from its frame.
- *
- * @return {promise} to be resolved when the host is destroyed.
- */
- destroyHost: function () {
- this._removeHostListeners();
- return this._host.destroy();
- },
-
- /**
* Remove all UI elements, detach from target and clear up
*/
destroy: function () {
// If several things call destroy then we give them all the same
// destruction promise so we're sure to destroy only once
if (this._destroyer) {
return this._destroyer;
}
@@ -2178,20 +2159,28 @@ Toolbox.prototype = {
this._telemetry.toolClosed("toolbox");
this._telemetry.destroy();
// Finish all outstanding tasks (which means finish destroying panels and
// then destroying the host, successfully or not) before destroying the
// target.
deferred.resolve(settleAll(outstanding)
.catch(console.error)
- .then(() => this.destroyHost())
- .catch(console.error)
.then(() => {
- this._win = null;
+ this._removeHostListeners();
+
+ // `location` may already be null if the toolbox document is already
+ // in process of destruction. Otherwise if it is still around, ensure
+ // releasing toolbox document and triggering cleanup thanks to unload
+ // event. We do that precisely here, before nullifying the target as
+ // various cleanup code depends on the target attribute to be still
+ // defined.
+ if (win.location) {
+ win.location.replace("about:blank");
+ }
// Targets need to be notified that the toolbox is being torn down.
// This is done after other destruction tasks since it may tear down
// fronts and the debugger transport which earlier destroy methods may
// require to complete.
if (!this._target) {
return null;
}
@@ -2201,16 +2190,17 @@ Toolbox.prototype = {
target.off("close", this.destroy);
return target.destroy();
}, console.error).then(() => {
this.emit("destroyed");
// Free _host after the call to destroyed in order to let a chance
// to destroyed listeners to still query toolbox attributes
this._host = null;
+ this._win = null;
this._toolPanels.clear();
// Force GC to prevent long GC pauses when running tests and to free up
// memory in general when the toolbox is closed.
if (flags.testing) {
win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.garbageCollect();
--- a/devtools/client/inspector/test/browser_inspector_breadcrumbs_visibility.js
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_visibility.js
@@ -38,17 +38,17 @@ add_task(function* () {
let { inspector, toolbox } = yield openInspectorForURL(TEST_URI);
// No way to wait for scrolling to end (Bug 1172171)
// Rather than wait a max time; limit test to instant scroll behavior
inspector.breadcrumbs.arrowScrollBox.scrollBehavior = "instant";
yield toolbox.switchHost(Toolbox.HostType.WINDOW);
- let hostWindow = toolbox._host._window;
+ let hostWindow = toolbox.win.parent;
let originalWidth = hostWindow.outerWidth;
let originalHeight = hostWindow.outerHeight;
hostWindow.resizeTo(640, 300);
info("Testing transitions ltr");
yield pushPref("intl.uidirection.en-US", "ltr");
yield testBreadcrumbTransitions(hostWindow, inspector);
--- a/devtools/client/inspector/test/browser_inspector_portrait_mode.js
+++ b/devtools/client/inspector/test/browser_inspector_portrait_mode.js
@@ -6,17 +6,17 @@
// Test that the inspector splitter is properly initialized in horizontal mode if the
// inspector starts in portrait mode.
add_task(function* () {
let { inspector, toolbox } = yield openInspectorForURL(
"data:text/html;charset=utf-8,<h1>foo</h1><span>bar</span>", "window");
- let hostWindow = toolbox._host._window;
+ let hostWindow = toolbox.win.parent;
let originalWidth = hostWindow.outerWidth;
let originalHeight = hostWindow.outerHeight;
let splitter = inspector.panelDoc.querySelector(".inspector-sidebar-splitter");
// If the inspector is not already in landscape mode.
if (!splitter.classList.contains("vert")) {
info("Resize toolbox window to force inspector to landscape mode");
@@ -41,17 +41,17 @@ add_task(function* () {
info("Reopen inspector");
({ inspector, toolbox } = yield openInspector("window"));
// Devtools window should still be 500px * 500px, inspector should still be in portrait.
splitter = inspector.panelDoc.querySelector(".inspector-sidebar-splitter");
ok(splitter.classList.contains("horz"), "Splitter is in horizontal mode");
info("Restore original window size");
- toolbox._host._window.resizeTo(originalWidth, originalHeight);
+ toolbox.win.parent.resizeTo(originalWidth, originalHeight);
});
/**
* Helper waiting for a class attribute mutation on the provided target. Returns a
* promise.
*
* @param {Node} target
* Node to observe
--- a/devtools/client/styleeditor/test/browser_styleeditor_sv_resize.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sv_resize.js
@@ -16,17 +16,17 @@ add_task(function* () {
is(ui.editors.length, 2, "There are 2 style sheets initially");
info("Changing toolbox host to a window.");
yield toolbox.switchHost(Toolbox.HostType.WINDOW);
let editor = yield ui.editors[0].getSourceEditor();
let originalSourceEditor = editor.sourceEditor;
- let hostWindow = toolbox._host._window;
+ let hostWindow = toolbox.win.parent;
let originalWidth = hostWindow.outerWidth;
let originalHeight = hostWindow.outerHeight;
// to check the caret is preserved
originalSourceEditor.setCursor(originalSourceEditor.getPosition(4));
info("Resizing window.");
hostWindow.resizeTo(120, 480);
--- a/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js
+++ b/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js
@@ -91,10 +91,10 @@ function onGetNameFetch(evt, view) {
}
function onExpandClosure(results) {
let prop = results[0].matchedProp;
ok(prop, "matched the name property in the variables view");
gVariablesView.window.focus();
gJSTerm.once("sidebar-closed", finishTest);
- EventUtils.synthesizeKey("VK_ESCAPE", {});
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, gVariablesView.window);
}