--- a/browser/components/extensions/.eslintrc.js
+++ b/browser/components/extensions/.eslintrc.js
@@ -8,15 +8,16 @@ module.exports = { // eslint-disable-li
"IconDetails": true,
"Tab": true,
"TabContext": true,
"Window": true,
"WindowEventManager": true,
"browserActionFor": true,
"getCookieStoreIdForTab": true,
"getDevToolsTargetForContext": true,
+ "getTargetTabIdForToolbox": true,
"makeWidgetId": true,
"pageActionFor": true,
"sidebarActionFor": true,
"tabTracker": true,
"windowTracker": true,
},
};
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-c-devtools-panels.js
@@ -0,0 +1,148 @@
+/* -*- 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/ExtensionUtils.jsm");
+
+const {
+ promiseDocumentLoaded,
+ SingletonEventManager,
+} = ExtensionUtils;
+
+const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
+
+/**
+ * Represents an addon devtools panel in the child process.
+ *
+ * @param {DevtoolsExtensionContext}
+ * A devtools extension context running in a child process.
+ * @param {object} panelOptions
+ * @param {string} panelOptions.id
+ * The id of the addon devtools panel registered in the main process.
+ */
+class ChildDevToolsPanel extends EventEmitter {
+ constructor(context, {id}) {
+ super();
+
+ this.context = context;
+ this.context.callOnClose(this);
+
+ this.id = id;
+ this._panelContext = null;
+
+ this.mm = context.messageManager;
+ this.mm.addMessageListener("Extension:DevToolsPanelShown", this);
+ this.mm.addMessageListener("Extension:DevToolsPanelHidden", this);
+ }
+
+ get panelContext() {
+ if (this._panelContext) {
+ return this._panelContext;
+ }
+
+ for (let view of this.context.extension.devtoolsViews) {
+ if (view.viewType === "devtools_panel" &&
+ view.devtoolsToolboxInfo.toolboxPanelId === this.id) {
+ this._panelContext = view;
+ return view;
+ }
+ }
+
+ return null;
+ }
+
+ receiveMessage({name, data}) {
+ // Filter out any message received while the panel context do not yet
+ // exist.
+ if (!this.panelContext || !this.panelContext.contentWindow) {
+ return;
+ }
+
+ // Filter out any message that is not related to the id of this
+ // toolbox panel.
+ if (!data || data.toolboxPanelId !== this.id) {
+ return;
+ }
+
+ switch (name) {
+ case "Extension:DevToolsPanelShown":
+ this.onParentPanelShown();
+ break;
+ case "Extension:DevToolsPanelHidden":
+ this.onParentPanelHidden();
+ break;
+ }
+ }
+
+ onParentPanelShown() {
+ const {document} = this.panelContext.contentWindow;
+
+ // Ensure that the onShown event is fired when the panel document has
+ // been fully loaded.
+ promiseDocumentLoaded(document).then(() => {
+ this.emit("shown", this.panelContext.contentWindow);
+ });
+ }
+
+ onParentPanelHidden() {
+ this.emit("hidden");
+ }
+
+ api() {
+ return {
+ onShown: new SingletonEventManager(
+ this.context, "devtoolsPanel.onShown", fire => {
+ const listener = (eventName, panelContentWindow) => {
+ fire.asyncWithoutClone(panelContentWindow);
+ };
+ this.on("shown", listener);
+ return () => {
+ this.off("shown", listener);
+ };
+ }).api(),
+
+ onHidden: new SingletonEventManager(
+ this.context, "devtoolsPanel.onHidden", fire => {
+ const listener = () => {
+ fire.async();
+ };
+ this.on("hidden", listener);
+ return () => {
+ this.off("hidden", listener);
+ };
+ }).api(),
+
+ // TODO(rpl): onSearch event and createStatusBarButton method
+ };
+ }
+
+ close() {
+ this.mm.removeMessageListener("Extension:DevToolsPanelShown", this);
+ this.mm.removeMessageListener("Extension:DevToolsPanelHidden", this);
+
+ this._panelContext = null;
+ this.context = null;
+ }
+}
+
+extensions.registerSchemaAPI("devtools.panels", "devtools_child", context => {
+ return {
+ devtools: {
+ panels: {
+ create(title, icon, url) {
+ return context.cloneScope.Promise.resolve().then(async () => {
+ const panelId = await context.childManager.callParentAsyncFunction(
+ "devtools.panels.create", [title, icon, url]);
+
+ const devtoolsPanel = new ChildDevToolsPanel(context, {id: panelId});
+
+ const devtoolsPanelAPI = Cu.cloneInto(devtoolsPanel.api(),
+ context.cloneScope,
+ {cloneFunctions: true});
+ return devtoolsPanelAPI;
+ });
+ },
+ },
+ },
+ };
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-devtools-panels.js
@@ -0,0 +1,248 @@
+/* -*- 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/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");
+ }
+
+ 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);
+ };
+ }
+
+ 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;
+ }
+}
+
+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);
+ },
+ },
+ },
+ };
+});
--- a/browser/components/extensions/extensions-browser.manifest
+++ b/browser/components/extensions/extensions-browser.manifest
@@ -3,45 +3,48 @@ category webextension-scripts bookmarks
category webextension-scripts browserAction chrome://browser/content/ext-browserAction.js
category webextension-scripts browsingData chrome://browser/content/ext-browsingData.js
category webextension-scripts commands chrome://browser/content/ext-commands.js
category webextension-scripts contextMenus chrome://browser/content/ext-contextMenus.js
category webextension-scripts desktop-runtime chrome://browser/content/ext-desktop-runtime.js
category webextension-scripts devtools chrome://browser/content/ext-devtools.js
category webextension-scripts devtools-inspectedWindow chrome://browser/content/ext-devtools-inspectedWindow.js
category webextension-scripts devtools-network chrome://browser/content/ext-devtools-network.js
+category webextension-scripts devtools-panels chrome://browser/content/ext-devtools-panels.js
category webextension-scripts history chrome://browser/content/ext-history.js
category webextension-scripts omnibox chrome://browser/content/ext-omnibox.js
category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
category webextension-scripts sessions chrome://browser/content/ext-sessions.js
category webextension-scripts sidebarAction chrome://browser/content/ext-sidebarAction.js
category webextension-scripts tabs chrome://browser/content/ext-tabs.js
category webextension-scripts theme chrome://browser/content/ext-theme.js
category webextension-scripts url-overrides chrome://browser/content/ext-url-overrides.js
category webextension-scripts utils chrome://browser/content/ext-utils.js
category webextension-scripts windows chrome://browser/content/ext-windows.js
# scripts specific for devtools extension contexts.
category webextension-scripts-devtools devtools-inspectedWindow chrome://browser/content/ext-c-devtools-inspectedWindow.js
+category webextension-scripts-devtools devtools-panels chrome://browser/content/ext-c-devtools-panels.js
# scripts that must run in the same process as addon code.
category webextension-scripts-addon contextMenus chrome://browser/content/ext-c-contextMenus.js
category webextension-scripts-addon omnibox chrome://browser/content/ext-c-omnibox.js
category webextension-scripts-addon tabs chrome://browser/content/ext-c-tabs.js
# schemas
category webextension-schemas bookmarks chrome://browser/content/schemas/bookmarks.json
category webextension-schemas browser_action chrome://browser/content/schemas/browser_action.json
category webextension-schemas browsing_data chrome://browser/content/schemas/browsing_data.json
category webextension-schemas commands chrome://browser/content/schemas/commands.json
category webextension-schemas context_menus chrome://browser/content/schemas/context_menus.json
category webextension-schemas context_menus_internal chrome://browser/content/schemas/context_menus_internal.json
category webextension-schemas devtools chrome://browser/content/schemas/devtools.json
category webextension-schemas devtools_inspected_window chrome://browser/content/schemas/devtools_inspected_window.json
category webextension-schemas devtools_network chrome://browser/content/schemas/devtools_network.json
+category webextension-schemas devtools_panels chrome://browser/content/schemas/devtools_panels.json
category webextension-schemas history chrome://browser/content/schemas/history.json
category webextension-schemas omnibox chrome://browser/content/schemas/omnibox.json
category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
category webextension-schemas sessions chrome://browser/content/schemas/sessions.json
category webextension-schemas sidebar_action chrome://browser/content/schemas/sidebar_action.json
category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
category webextension-schemas theme chrome://browser/content/schemas/theme.json
category webextension-schemas url_overrides chrome://browser/content/schemas/url_overrides.json
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -16,22 +16,24 @@ browser.jar:
content/browser/ext-browserAction.js
content/browser/ext-browsingData.js
content/browser/ext-commands.js
content/browser/ext-contextMenus.js
content/browser/ext-desktop-runtime.js
content/browser/ext-devtools.js
content/browser/ext-devtools-inspectedWindow.js
content/browser/ext-devtools-network.js
+ content/browser/ext-devtools-panels.js
content/browser/ext-history.js
content/browser/ext-omnibox.js
content/browser/ext-pageAction.js
content/browser/ext-sessions.js
content/browser/ext-sidebarAction.js
content/browser/ext-tabs.js
content/browser/ext-theme.js
content/browser/ext-url-overrides.js
content/browser/ext-utils.js
content/browser/ext-windows.js
content/browser/ext-c-contextMenus.js
content/browser/ext-c-devtools-inspectedWindow.js
+ content/browser/ext-c-devtools-panels.js
content/browser/ext-c-omnibox.js
content/browser/ext-c-tabs.js
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/schemas/devtools_panels.json
@@ -0,0 +1,407 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "devtools.panels",
+ "allowedContexts": ["devtools", "devtools_only"],
+ "defaultContexts": ["devtools", "devtools_only"],
+ "description": "Use the <code>chrome.devtools.panels</code> API to integrate your extension into Developer Tools window UI: create your own panels, access existing panels, and add sidebars.",
+ "nocompile": true,
+ "types": [
+ {
+ "id": "ElementsPanel",
+ "type": "object",
+ "description": "Represents the Elements panel.",
+ "events": [
+ {
+ "name": "onSelectionChanged",
+ "unsupported": true,
+ "description": "Fired when an object is selected in the panel."
+ }
+ ],
+ "functions": [
+ {
+ "name": "createSidebarPane",
+ "unsupported": true,
+ "type": "function",
+ "description": "Creates a pane within panel's sidebar.",
+ "parameters": [
+ {
+ "name": "title",
+ "type": "string",
+ "description": "Text that is displayed in sidebar caption."
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "description": "A callback invoked when the sidebar is created.",
+ "optional": true,
+ "parameters": [
+ {
+ "name": "result",
+ "description": "An ExtensionSidebarPane object for created sidebar pane.",
+ "$ref": "ExtensionSidebarPane"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "SourcesPanel",
+ "type": "object",
+ "description": "Represents the Sources panel.",
+ "events": [
+ {
+ "name": "onSelectionChanged",
+ "unsupported": true,
+ "description": "Fired when an object is selected in the panel."
+ }
+ ],
+ "functions": [
+ {
+ "name": "createSidebarPane",
+ "unsupported": true,
+ "type": "function",
+ "description": "Creates a pane within panel's sidebar.",
+ "parameters": [
+ {
+ "name": "title",
+ "type": "string",
+ "description": "Text that is displayed in sidebar caption."
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "description": "A callback invoked when the sidebar is created.",
+ "optional": true,
+ "parameters": [
+ {
+ "name": "result",
+ "description": "An ExtensionSidebarPane object for created sidebar pane.",
+ "$ref": "ExtensionSidebarPane"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "ExtensionPanel",
+ "type": "object",
+ "description": "Represents a panel created by extension.",
+ "functions": [
+ {
+ "name": "createStatusBarButton",
+ "unsupported": true,
+ "description": "Appends a button to the status bar of the panel.",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "iconPath",
+ "type": "string",
+ "description": "Path to the icon of the button. The file should contain a 64x24-pixel image composed of two 32x24 icons. The left icon is used when the button is inactive; the right icon is displayed when the button is pressed."
+ },
+ {
+ "name": "tooltipText",
+ "type": "string",
+ "description": "Text shown as a tooltip when user hovers the mouse over the button."
+ },
+ {
+ "name": "disabled",
+ "type": "boolean",
+ "description": "Whether the button is disabled."
+ }
+ ],
+ "returns": { "$ref": "Button" }
+ }
+ ],
+ "events": [
+ {
+ "name": "onSearch",
+ "unsupported": true,
+ "description": "Fired upon a search action (start of a new search, search result navigation, or search being canceled).",
+ "parameters": [
+ {
+ "name": "action",
+ "type": "string",
+ "description": "Type of search action being performed."
+ },
+ {
+ "name": "queryString",
+ "type": "string",
+ "optional": true,
+ "description": "Query string (only for 'performSearch')."
+ }
+ ]
+ },
+ {
+ "name": "onShown",
+ "type": "function",
+ "description": "Fired when the user switches to the panel.",
+ "parameters": [
+ {
+ "name": "window",
+ "type": "object",
+ "isInstanceOf": "global",
+ "additionalProperties": { "type": "any" },
+ "description": "The JavaScript <code>window</code> object of panel's page."
+ }
+ ]
+ },
+ {
+ "name": "onHidden",
+ "type": "function",
+ "description": "Fired when the user switches away from the panel."
+ }
+ ]
+ },
+ {
+ "id": "ExtensionSidebarPane",
+ "type": "object",
+ "description": "A sidebar created by the extension.",
+ "functions": [
+ {
+ "name": "setHeight",
+ "unsupported": true,
+ "type": "function",
+ "description": "Sets the height of the sidebar.",
+ "parameters": [
+ {
+ "name": "height",
+ "type": "string",
+ "description": "A CSS-like size specification, such as <code>'100px'</code> or <code>'12ex'</code>."
+ }
+ ]
+ },
+ {
+ "name": "setExpression",
+ "unsupported": true,
+ "type": "function",
+ "description": "Sets an expression that is evaluated within the inspected page. The result is displayed in the sidebar pane.",
+ "parameters": [
+ {
+ "name": "expression",
+ "type": "string",
+ "description": "An expression to be evaluated in context of the inspected page. JavaScript objects and DOM nodes are displayed in an expandable tree similar to the console/watch."
+ },
+ {
+ "name": "rootTitle",
+ "type": "string",
+ "optional": true,
+ "description": "An optional title for the root of the expression tree."
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "description": "A callback invoked after the sidebar pane is updated with the expression evaluation results."
+ }
+ ]
+ },
+ {
+ "name": "setObject",
+ "unsupported": true,
+ "type": "function",
+ "description": "Sets a JSON-compliant object to be displayed in the sidebar pane.",
+ "parameters": [
+ {
+ "name": "jsonObject",
+ "type": "string",
+ "description": "An object to be displayed in context of the inspected page. Evaluated in the context of the caller (API client)."
+ },
+ {
+ "name": "rootTitle",
+ "type": "string",
+ "optional": true,
+ "description": "An optional title for the root of the expression tree."
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "description": "A callback invoked after the sidebar is updated with the object."
+ }
+ ]
+ },
+ {
+ "name": "setPage",
+ "unsupported": true,
+ "type": "function",
+ "description": "Sets an HTML page to be displayed in the sidebar pane.",
+ "parameters": [
+ {
+ "name": "path",
+ "type": "string",
+ "description": "Relative path of an extension page to display within the sidebar."
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onShown",
+ "unsupported": true,
+ "type": "function",
+ "description": "Fired when the sidebar pane becomes visible as a result of user switching to the panel that hosts it.",
+ "parameters": [
+ {
+ "name": "window",
+ "type": "object",
+ "isInstanceOf": "global",
+ "additionalProperties": { "type": "any" },
+ "description": "The JavaScript <code>window</code> object of the sidebar page, if one was set with the <code>setPage()</code> method."
+ }
+ ]
+ },
+ {
+ "name": "onHidden",
+ "unsupported": true,
+ "type": "function",
+ "description": "Fired when the sidebar pane becomes hidden as a result of the user switching away from the panel that hosts the sidebar pane."
+ }
+ ]
+ },
+ {
+ "id": "Button",
+ "type": "object",
+ "description": "A button created by the extension.",
+ "functions": [
+ {
+ "name": "update",
+ "unsupported": true,
+ "type": "function",
+ "description": "Updates the attributes of the button. If some of the arguments are omitted or <code>null</code>, the corresponding attributes are not updated.",
+ "parameters": [
+ {
+ "name": "iconPath",
+ "type": "string",
+ "optional": true,
+ "description": "Path to the new icon of the button."
+ },
+ {
+ "name": "tooltipText",
+ "type": "string",
+ "optional": true,
+ "description": "Text shown as a tooltip when user hovers the mouse over the button."
+ },
+ {
+ "name": "disabled",
+ "type": "boolean",
+ "optional": true,
+ "description": "Whether the button is disabled."
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onClicked",
+ "unsupported": true,
+ "type": "function",
+ "description": "Fired when the button is clicked."
+ }
+ ]
+ }
+ ],
+ "properties": {
+ "elements": {
+ "$ref": "ElementsPanel",
+ "description": "Elements panel."
+ },
+ "sources": {
+ "$ref": "SourcesPanel",
+ "description": "Sources panel."
+ }
+ },
+ "functions": [
+ {
+ "name": "create",
+ "type": "function",
+ "description": "Creates an extension panel.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "title",
+ "type": "string",
+ "description": "Title that is displayed next to the extension icon in the Developer Tools toolbar."
+ },
+ {
+ "name": "iconPath",
+ "type": "string",
+ "description": "Path of the panel's icon relative to the extension directory."
+ },
+ {
+ "name": "pagePath",
+ "type": "string",
+ "description": "Path of the panel's HTML page relative to the extension directory."
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "description": "A function that is called when the panel is created.",
+ "parameters": [
+ {
+ "name": "panel",
+ "description": "An ExtensionPanel object representing the created panel.",
+ "$ref": "ExtensionPanel"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "setOpenResourceHandler",
+ "unsupported": true,
+ "type": "function",
+ "description": "Specifies the function to be called when the user clicks a resource link in the Developer Tools window. To unset the handler, either call the method with no parameters or pass null as the parameter.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "description": "A function that is called when the user clicks on a valid resource link in Developer Tools window. Note that if the user clicks an invalid URL or an XHR, this function is not called.",
+ "parameters": [
+ {
+ "name": "resource",
+ "$ref": "devtools.inspectedWindow.Resource",
+ "description": "A $(ref:devtools.inspectedWindow.Resource) object for the resource that was clicked."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "openResource",
+ "unsupported": true,
+ "type": "function",
+ "description": "Requests DevTools to open a URL in a Developer Tools panel.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "url",
+ "type": "string",
+ "description": "The URL of the resource to open."
+ },
+ {
+ "name": "lineNumber",
+ "type": "integer",
+ "description": "Specifies the line number to scroll to when the resource is loaded."
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "description": "A function that is called when the resource has been successfully loaded."
+ }
+ ]
+ }
+ ]
+ }
+]
--- a/browser/components/extensions/schemas/jar.mn
+++ b/browser/components/extensions/schemas/jar.mn
@@ -7,16 +7,17 @@ browser.jar:
content/browser/schemas/browser_action.json
content/browser/schemas/browsing_data.json
content/browser/schemas/commands.json
content/browser/schemas/context_menus.json
content/browser/schemas/context_menus_internal.json
content/browser/schemas/devtools.json
content/browser/schemas/devtools_inspected_window.json
content/browser/schemas/devtools_network.json
+ content/browser/schemas/devtools_panels.json
content/browser/schemas/history.json
content/browser/schemas/omnibox.json
content/browser/schemas/page_action.json
content/browser/schemas/sessions.json
content/browser/schemas/sidebar_action.json
content/browser/schemas/tabs.json
content/browser/schemas/theme.json
content/browser/schemas/url_overrides.json
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -50,16 +50,17 @@ support-files =
[browser_ext_contextMenus_radioGroups.js]
[browser_ext_contextMenus_uninstall.js]
[browser_ext_contextMenus_urlPatterns.js]
[browser_ext_currentWindow.js]
[browser_ext_devtools_inspectedWindow.js]
[browser_ext_devtools_inspectedWindow_reload.js]
[browser_ext_devtools_network.js]
[browser_ext_devtools_page.js]
+[browser_ext_devtools_panel.js]
[browser_ext_getViews.js]
[browser_ext_incognito_views.js]
[browser_ext_incognito_popup.js]
[browser_ext_lastError.js]
[browser_ext_omnibox.js]
[browser_ext_optionsPage_privileges.js]
[browser_ext_pageAction_context.js]
[browser_ext_pageAction_popup.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_panel.js
@@ -0,0 +1,145 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+ "resource://devtools/shared/Loader.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
+ "resource://devtools/client/framework/gDevTools.jsm");
+
+/**
+ * This test file ensures that:
+ *
+ * - ensures that devtools.panel.create is able to create a devtools panel
+ */
+
+add_task(function* test_devtools_page_panels_create() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
+
+ async function devtools_page() {
+ const result = {
+ devtoolsPageTabId: browser.devtools.inspectedWindow.tabId,
+ panelCreated: 0,
+ panelShown: 0,
+ panelHidden: 0,
+ };
+
+ try {
+ const panel = await browser.devtools.panels.create(
+ "Test Panel", "fake-icon.png", "devtools_panel.html"
+ );
+
+ result.panelCreated++;
+
+ panel.onShown.addListener(contentWindow => {
+ result.panelShown++;
+ browser.test.assertEq("complete", contentWindow.document.readyState,
+ "Got the expected 'complete' panel document readyState");
+ browser.test.assertEq("test_panel_global", contentWindow.TEST_PANEL_GLOBAL,
+ "Got the expected global in the panel contentWindow");
+ browser.test.sendMessage("devtools_panel_shown", result);
+ });
+
+ panel.onHidden.addListener(() => {
+ result.panelHidden++;
+
+ browser.test.sendMessage("devtools_panel_hidden", result);
+ });
+
+ browser.test.sendMessage("devtools_panel_created");
+ } catch (err) {
+ // Make the test able to fail fast when it is going to be a failure.
+ browser.test.sendMessage("devtools_panel_created");
+ throw err;
+ }
+ }
+
+ function devtools_panel() {
+ // Set a property in the global and check that it is defined
+ // and accessible from the devtools_page when the panel.onShown
+ // event has been received.
+ window.TEST_PANEL_GLOBAL = "test_panel_global";
+ browser.test.sendMessage("devtools_panel_inspectedWindow_tabId",
+ browser.devtools.inspectedWindow.tabId);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <script src="devtools_page.js"></script>
+ </body>
+ </html>`,
+ "devtools_page.js": devtools_page,
+ "devtools_panel.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ DEVTOOLS PANEL
+ <script src="devtools_panel.js"></script>
+ </body>
+ </html>`,
+ "devtools_panel.js": devtools_panel,
+ },
+ });
+
+ yield extension.startup();
+
+ let target = devtools.TargetFactory.forTab(tab);
+
+ const toolbox = yield gDevTools.showToolbox(target, "webconsole");
+ info("developer toolbox opened");
+
+ yield extension.awaitMessage("devtools_panel_created");
+
+ const toolboxAdditionalTools = toolbox.getAdditionalTools();
+
+ is(toolboxAdditionalTools.length, 1,
+ "Got the expected number of toolbox specific panel registered.");
+
+ const panelId = toolboxAdditionalTools[0].id;
+
+ yield gDevTools.showToolbox(target, panelId);
+ const {devtoolsPageTabId} = yield extension.awaitMessage("devtools_panel_shown");
+ const devtoolsPanelTabId = yield extension.awaitMessage("devtools_panel_inspectedWindow_tabId");
+ is(devtoolsPanelTabId, devtoolsPageTabId,
+ "Got the same devtools.inspectedWindow.tabId from devtools page and panel");
+ info("Addon Devtools Panel shown");
+
+ yield gDevTools.showToolbox(target, "webconsole");
+ const results = yield extension.awaitMessage("devtools_panel_hidden");
+ info("Addon Devtools Panel hidden");
+
+ is(results.panelCreated, 1, "devtools.panel.create callback has been called once");
+ is(results.panelShown, 1, "panel.onShown listener has been called once");
+ is(results.panelHidden, 1, "panel.onHidden listener has been called once");
+
+ yield gDevTools.showToolbox(target, panelId);
+ yield extension.awaitMessage("devtools_panel_shown");
+ info("Addon Devtools Panel shown - second cycle");
+
+ yield gDevTools.showToolbox(target, "webconsole");
+ const secondCycleResults = yield extension.awaitMessage("devtools_panel_hidden");
+ info("Addon Devtools Panel hidden - second cycle");
+
+ is(secondCycleResults.panelCreated, 1, "devtools.panel.create callback has been called once");
+ is(secondCycleResults.panelShown, 2, "panel.onShown listener has been called twice");
+ is(secondCycleResults.panelHidden, 2, "panel.onHidden listener has been called twice");
+
+ yield gDevTools.closeToolbox(target);
+
+ yield target.destroy();
+
+ yield extension.unload();
+
+ yield BrowserTestUtils.removeTab(tab);
+});