fune/browser/components/extensions/ext-devtools-panels.js
Luca Greco f30c630e4d Bug 1340529 - Fix shown/hidden events for a devtools_panel toggled from toolbox preferences. r=kmag
MozReview-Commit-ID: H81qwCEoaf9

--HG--
extra : rebase_source : 8376f43d40739be337f983c34a9527137fb1c1c5
2017-02-16 04:07:49 +01:00

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);
},
},
},
};
});