forked from mirrors/gecko-dev
MozReview-Commit-ID: H81qwCEoaf9 --HG-- extra : rebase_source : 8376f43d40739be337f983c34a9527137fb1c1c5
262 lines
8.5 KiB
JavaScript
262 lines
8.5 KiB
JavaScript
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set sts=2 sw=2 et tw=80: */
|
|
"use strict";
|
|
|
|
Cu.import("resource://gre/modules/AppConstants.jsm");
|
|
Cu.import("resource://gre/modules/ExtensionParent.jsm");
|
|
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
|
|
"resource:///modules/E10SUtils.jsm");
|
|
|
|
const {
|
|
watchExtensionProxyContextLoad,
|
|
} = ExtensionParent;
|
|
|
|
const {
|
|
IconDetails,
|
|
promiseEvent,
|
|
} = ExtensionUtils;
|
|
|
|
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
|
|
/**
|
|
* Represents an addon devtools panel in the main process.
|
|
*
|
|
* @param {ExtensionChildProxyContext} context
|
|
* A devtools extension proxy context running in a main process.
|
|
* @param {object} options
|
|
* @param {string} options.id
|
|
* The id of the addon devtools panel.
|
|
* @param {string} options.icon
|
|
* The icon of the addon devtools panel.
|
|
* @param {string} options.title
|
|
* The title of the addon devtools panel.
|
|
* @param {string} options.url
|
|
* The url of the addon devtools panel, relative to the extension base URL.
|
|
*/
|
|
class ParentDevToolsPanel {
|
|
constructor(context, panelOptions) {
|
|
const toolbox = context.devToolsToolbox;
|
|
if (!toolbox) {
|
|
// This should never happen when this constructor is called with a valid
|
|
// devtools extension context.
|
|
throw Error("Missing mandatory toolbox");
|
|
}
|
|
|
|
this.extension = context.extension;
|
|
this.viewType = "devtools_panel";
|
|
|
|
this.visible = false;
|
|
this.toolbox = toolbox;
|
|
|
|
this.context = context;
|
|
|
|
this.panelOptions = panelOptions;
|
|
|
|
this.context.callOnClose(this);
|
|
|
|
this.id = this.panelOptions.id;
|
|
|
|
this.onToolboxPanelSelect = this.onToolboxPanelSelect.bind(this);
|
|
this.onToolboxReady = this.onToolboxReady.bind(this);
|
|
|
|
this.panelAdded = false;
|
|
|
|
if (this.toolbox.isReady) {
|
|
this.onToolboxReady();
|
|
} else {
|
|
this.toolbox.once("ready", this.onToolboxReady);
|
|
}
|
|
|
|
this.waitTopLevelContext = new Promise(resolve => {
|
|
this._resolveTopLevelContext = resolve;
|
|
});
|
|
}
|
|
|
|
addPanel() {
|
|
const {icon, title} = this.panelOptions;
|
|
const extensionName = this.context.extension.name;
|
|
|
|
this.toolbox.addAdditionalTool({
|
|
id: this.id,
|
|
url: "about:blank",
|
|
icon: icon,
|
|
label: title,
|
|
tooltip: `DevTools Panel added by "${extensionName}" add-on.`,
|
|
invertIconForLightTheme: true,
|
|
visibilityswitch: `devtools.webext-${this.id}.enabled`,
|
|
isTargetSupported: target => target.isLocalTab,
|
|
build: (window, toolbox) => {
|
|
if (toolbox !== this.toolbox) {
|
|
throw new Error("Unexpected toolbox received on addAdditionalTool build property");
|
|
}
|
|
|
|
const destroy = this.buildPanel(window, toolbox);
|
|
|
|
return {toolbox, destroy};
|
|
},
|
|
});
|
|
}
|
|
|
|
buildPanel(window, toolbox) {
|
|
const {url} = this.panelOptions;
|
|
const {document} = window;
|
|
|
|
const browser = document.createElementNS(XUL_NS, "browser");
|
|
browser.setAttribute("type", "content");
|
|
browser.setAttribute("disableglobalhistory", "true");
|
|
browser.setAttribute("style", "width: 100%; height: 100%;");
|
|
browser.setAttribute("transparent", "true");
|
|
browser.setAttribute("class", "webextension-devtoolsPanel-browser");
|
|
browser.setAttribute("webextension-view-type", "devtools_panel");
|
|
browser.setAttribute("flex", "1");
|
|
|
|
this.browser = browser;
|
|
|
|
const {extension} = this.context;
|
|
|
|
let awaitFrameLoader = Promise.resolve();
|
|
if (extension.remote) {
|
|
browser.setAttribute("remote", "true");
|
|
browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE);
|
|
awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
|
|
} else if (!AppConstants.RELEASE_OR_BETA) {
|
|
// NOTE: Using a content iframe here breaks the devtools panel
|
|
// switching between docked and undocked mode,
|
|
// because of a swapFrameLoader exception (see bug 1075490).
|
|
browser.setAttribute("type", "chrome");
|
|
browser.setAttribute("forcemessagemanager", true);
|
|
}
|
|
|
|
let hasTopLevelContext = false;
|
|
|
|
// Listening to new proxy contexts.
|
|
const unwatchExtensionProxyContextLoad = watchExtensionProxyContextLoad(this, context => {
|
|
// Keep track of the toolbox and target associated to the context, which is
|
|
// needed by the API methods implementation.
|
|
context.devToolsToolbox = toolbox;
|
|
|
|
if (!hasTopLevelContext) {
|
|
hasTopLevelContext = true;
|
|
|
|
// Resolve the promise when the root devtools_panel context has been created.
|
|
awaitFrameLoader.then(() => this._resolveTopLevelContext(context));
|
|
}
|
|
});
|
|
|
|
document.body.setAttribute("style", "margin: 0; padding: 0;");
|
|
document.body.appendChild(browser);
|
|
|
|
extensions.emit("extension-browser-inserted", browser, {
|
|
devtoolsToolboxInfo: {
|
|
toolboxPanelId: this.id,
|
|
inspectedWindowTabId: getTargetTabIdForToolbox(toolbox),
|
|
},
|
|
});
|
|
|
|
browser.loadURI(url);
|
|
|
|
toolbox.on("select", this.onToolboxPanelSelect);
|
|
|
|
// Return a cleanup method that is when the panel is destroyed, e.g.
|
|
// - when addon devtool panel has been disabled by the user from the toolbox preferences,
|
|
// its ParentDevToolsPanel instance is still valid, but the built devtools panel is removed from
|
|
// the toolbox (and re-built again if the user re-enable it from the toolbox preferences panel)
|
|
// - when the creator context has been destroyed, the ParentDevToolsPanel close method is called,
|
|
// it remove the tool definition from the toolbox, which will call this destroy method.
|
|
return () => {
|
|
unwatchExtensionProxyContextLoad();
|
|
browser.remove();
|
|
toolbox.off("select", this.onToolboxPanelSelect);
|
|
|
|
// If the panel has been disabled from the toolbox preferences,
|
|
// we need to re-initialize the waitTopLevelContext Promise.
|
|
this.waitTopLevelContext = new Promise(resolve => {
|
|
this._resolveTopLevelContext = resolve;
|
|
});
|
|
};
|
|
}
|
|
|
|
onToolboxReady() {
|
|
if (!this.panelAdded) {
|
|
this.panelAdded = true;
|
|
this.addPanel();
|
|
}
|
|
}
|
|
|
|
onToolboxPanelSelect(what, id) {
|
|
if (!this.waitTopLevelContext || !this.panelAdded) {
|
|
return;
|
|
}
|
|
if (!this.visible && id === this.id) {
|
|
// Wait that the panel is fully loaded and emit show.
|
|
this.waitTopLevelContext.then(() => {
|
|
this.visible = true;
|
|
this.context.parentMessageManager.sendAsyncMessage("Extension:DevToolsPanelShown", {
|
|
toolboxPanelId: this.id,
|
|
});
|
|
});
|
|
} else if (this.visible && id !== this.id) {
|
|
this.visible = false;
|
|
this.context.parentMessageManager.sendAsyncMessage("Extension:DevToolsPanelHidden", {
|
|
toolboxPanelId: this.id,
|
|
});
|
|
}
|
|
}
|
|
|
|
close() {
|
|
const {toolbox} = this;
|
|
|
|
if (!toolbox) {
|
|
throw new Error("Unable to destroy a closed devtools panel");
|
|
}
|
|
|
|
toolbox.off("ready", this.onToolboxReady);
|
|
|
|
// Explicitly remove the panel if it is registered and the toolbox is not
|
|
// closing itself.
|
|
if (toolbox.isToolRegistered(this.id) && !toolbox._destroyer) {
|
|
toolbox.removeAdditionalTool(this.id);
|
|
}
|
|
|
|
this.context = null;
|
|
this.toolbox = null;
|
|
this.waitTopLevelContext = null;
|
|
this._resolveTopLevelContext = null;
|
|
}
|
|
}
|
|
|
|
extensions.registerSchemaAPI("devtools.panels", "devtools_parent", context => {
|
|
// An incremental "per context" id used in the generated devtools panel id.
|
|
let nextPanelId = 0;
|
|
|
|
return {
|
|
devtools: {
|
|
panels: {
|
|
create(title, icon, url) {
|
|
// Get a fallback icon from the manifest data.
|
|
if (icon === "" && context.extension.manifest.icons) {
|
|
const iconInfo = IconDetails.getPreferredIcon(context.extension.manifest.icons,
|
|
context.extension, 128);
|
|
icon = iconInfo ? iconInfo.icon : "";
|
|
}
|
|
|
|
icon = context.extension.baseURI.resolve(icon);
|
|
url = context.extension.baseURI.resolve(url);
|
|
|
|
const baseId = `${context.extension.id}-${context.contextId}-${nextPanelId++}`;
|
|
const id = `${makeWidgetId(baseId)}-devtools-panel`;
|
|
|
|
new ParentDevToolsPanel(context, {title, icon, url, id});
|
|
|
|
// Resolved to the devtools panel id into the child addon process,
|
|
// where it will be used to identify the messages related
|
|
// to the panel API onShown/onHidden events.
|
|
return Promise.resolve(id);
|
|
},
|
|
},
|
|
},
|
|
};
|
|
});
|