--- a/browser/components/extensions/ext-c-devtools-panels.js
+++ b/browser/components/extensions/ext-c-devtools-panels.js
@@ -1,14 +1,18 @@
/* -*- 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/Services.jsm");
+
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://gre/modules/EventEmitter.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChildDevToolsUtils",
+ "resource://gre/modules/ExtensionChildDevToolsUtils.jsm");
var {
promiseDocumentLoaded,
} = ExtensionUtils;
/**
* Represents an addon devtools panel in the child process.
*
@@ -127,29 +131,44 @@ class ChildDevToolsPanel extends EventEm
this._panelContext = null;
this.context = null;
}
}
this.devtools_panels = class extends ExtensionAPI {
getAPI(context) {
+ const themeChangeObserver = ExtensionChildDevToolsUtils.getThemeChangeObserver();
+
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;
});
},
+ get themeName() {
+ return themeChangeObserver.themeName;
+ },
+ onThemeChanged: new SingletonEventManager(
+ context, "devtools.panels.onThemeChanged", fire => {
+ const listener = (eventName, themeName) => {
+ fire.async(themeName);
+ };
+ themeChangeObserver.on("themeChanged", listener);
+ return () => {
+ themeChangeObserver.off("themeChanged", listener);
+ };
+ }).api(),
},
},
};
}
};
--- a/browser/components/extensions/ext-devtools.js
+++ b/browser/components/extensions/ext-devtools.js
@@ -133,16 +133,17 @@ class DevToolsPage extends HiddenExtensi
this.resolveTopLevelContext(context);
}
});
extensions.emit("extension-browser-inserted", this.browser, {
devtoolsToolboxInfo: {
inspectedWindowTabId: getTargetTabIdForToolbox(this.toolbox),
+ themeName: gDevTools.getTheme(),
},
});
this.browser.loadURI(this.url);
await this.waitForTopLevelContext;
}
@@ -195,39 +196,53 @@ class DevToolsPageDefinition {
this.url = url;
this.extension = extension;
// Map[TabTarget -> DevToolsPage]
this.devtoolsPageForTarget = new Map();
}
+ onThemeChanged(evt, themeName) {
+ Services.ppmm.broadcastAsyncMessage("Extension:DevToolsThemeChanged", {themeName});
+ }
+
buildForToolbox(toolbox) {
if (this.devtoolsPageForTarget.has(toolbox.target)) {
return Promise.reject(new Error("DevtoolsPage has been already created for this toolbox"));
}
const devtoolsPage = new DevToolsPage(this.extension, {
toolbox, url: this.url, devToolsPageDefinition: this,
});
+
+ // If this is the first DevToolsPage, subscribe to the theme-changed event
+ if (this.devtoolsPageForTarget.size === 0) {
+ gDevTools.on("theme-changed", this.onThemeChanged);
+ }
this.devtoolsPageForTarget.set(toolbox.target, devtoolsPage);
return devtoolsPage.build();
}
shutdownForTarget(target) {
if (this.devtoolsPageForTarget.has(target)) {
const devtoolsPage = this.devtoolsPageForTarget.get(target);
devtoolsPage.close();
// `devtoolsPage.close()` should remove the instance from the map,
// raise an exception if it is still there.
if (this.devtoolsPageForTarget.has(target)) {
throw new Error(`Leaked DevToolsPage instance for target "${target.toString()}"`);
}
+
+ // If this was the last DevToolsPage, unsubscribe from the theme-changed event
+ if (this.devtoolsPageForTarget.size === 0) {
+ gDevTools.off("theme-changed", this.onThemeChanged);
+ }
}
}
forgetForTarget(target) {
this.devtoolsPageForTarget.delete(target);
}
shutdown() {
--- a/browser/components/extensions/schemas/devtools_panels.json
+++ b/browser/components/extensions/schemas/devtools_panels.json
@@ -311,16 +311,20 @@
"properties": {
"elements": {
"$ref": "ElementsPanel",
"description": "Elements panel."
},
"sources": {
"$ref": "SourcesPanel",
"description": "Sources panel."
+ },
+ "themeName": {
+ "type": "string",
+ "description": "The name of the current devtools theme."
}
},
"functions": [
{
"name": "create",
"type": "function",
"description": "Creates an extension panel.",
"async": "callback",
@@ -397,11 +401,25 @@
{
"name": "callback",
"type": "function",
"optional": true,
"description": "A function that is called when the resource has been successfully loaded."
}
]
}
+ ],
+ "events": [
+ {
+ "name": "onThemeChanged",
+ "type": "function",
+ "description": "Fired when the devtools theme changes.",
+ "parameters": [
+ {
+ "name": "themeName",
+ "type": "string",
+ "description": "The name of the current devtools theme."
+ }
+ ]
+ }
]
}
]
--- a/browser/components/extensions/test/browser/browser_ext_devtools_panel.js
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_panel.js
@@ -1,23 +1,104 @@
/* -*- 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");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+
+const DEVTOOLS_THEME_PREF = "devtools.theme";
/**
* This test file ensures that:
*
- * - ensures that devtools.panel.create is able to create a devtools panel
+ * - devtools.panels.themeName returns the correct value,
+ * both from a page and a panel.
+ * - devtools.panels.onThemeChanged fires for theme changes,
+ * both from a page and a panel.
+ * - devtools.panels.create is able to create a devtools panel.
*/
+async function switchTheme(theme) {
+ const waitforThemeChanged = new Promise(resolve => gDevTools.once("theme-changed", resolve));
+ Preferences.set(DEVTOOLS_THEME_PREF, theme);
+ await waitforThemeChanged;
+}
+
+async function testThemeSwitching(extension, locations = ["page"]) {
+ for (let newTheme of ["dark", "light"]) {
+ await switchTheme(newTheme);
+ for (let location of locations) {
+ is(await extension.awaitMessage(`devtools_theme_changed_${location}`),
+ newTheme,
+ `The onThemeChanged event listener fired for the ${location}.`);
+ is(await extension.awaitMessage(`current_theme_${location}`),
+ newTheme,
+ `The current theme is reported as expected for the ${location}.`);
+ }
+ }
+}
+
+add_task(async function test_theme_name_no_panel() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
+
+ async function devtools_page() {
+ browser.devtools.panels.onThemeChanged.addListener(themeName => {
+ browser.test.sendMessage("devtools_theme_changed_page", themeName);
+ browser.test.sendMessage("current_theme_page", browser.devtools.panels.themeName);
+ });
+
+ browser.test.sendMessage("initial_theme", browser.devtools.panels.themeName);
+ }
+
+ 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,
+ },
+ });
+
+ // Ensure that the initial value of the devtools theme is "light".
+ await SpecialPowers.pushPrefEnv({set: [[DEVTOOLS_THEME_PREF, "light"]]});
+
+ await extension.startup();
+
+ let target = devtools.TargetFactory.forTab(tab);
+ await gDevTools.showToolbox(target, "webconsole");
+ info("developer toolbox opened");
+
+ is(await extension.awaitMessage("initial_theme"),
+ "light",
+ "The initial theme is reported as expected.");
+
+ await testThemeSwitching(extension);
+
+ await gDevTools.closeToolbox(target);
+ await target.destroy();
+
+ await extension.unload();
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
add_task(async function test_devtools_page_panels_create() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
async function devtools_page() {
const result = {
devtoolsPageTabId: browser.devtools.inspectedWindow.tabId,
panelCreated: 0,
panelShown: 0,
@@ -41,31 +122,44 @@ add_task(async function test_devtools_pa
});
panel.onHidden.addListener(() => {
result.panelHidden++;
browser.test.sendMessage("devtools_panel_hidden", result);
});
+ browser.devtools.panels.onThemeChanged.addListener(themeName => {
+ browser.test.sendMessage("devtools_theme_changed_page", themeName);
+ browser.test.sendMessage("current_theme_page", browser.devtools.panels.themeName);
+ });
+
browser.test.sendMessage("devtools_panel_created");
+ browser.test.sendMessage("initial_theme_page", browser.devtools.panels.themeName);
} 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.devtools.panels.onThemeChanged.addListener(themeName => {
+ browser.test.sendMessage("devtools_theme_changed_panel", themeName);
+ browser.test.sendMessage("current_theme_panel", browser.devtools.panels.themeName);
+ });
+
browser.test.sendMessage("devtools_panel_inspectedWindow_tabId",
browser.devtools.inspectedWindow.tabId);
+ browser.test.sendMessage("initial_theme_panel", browser.devtools.panels.themeName);
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
devtools_page: "devtools_page.html",
},
files: {
"devtools_page.html": `<!DOCTYPE html>
@@ -87,39 +181,56 @@ add_task(async function test_devtools_pa
DEVTOOLS PANEL
<script src="devtools_panel.js"></script>
</body>
</html>`,
"devtools_panel.js": devtools_panel,
},
});
+ registerCleanupFunction(function() {
+ Preferences.reset(DEVTOOLS_THEME_PREF);
+ });
+
+ // Ensure that the initial value of the devtools theme is "light".
+ Preferences.set(DEVTOOLS_THEME_PREF, "light");
+
await extension.startup();
let target = devtools.TargetFactory.forTab(tab);
const toolbox = await gDevTools.showToolbox(target, "webconsole");
info("developer toolbox opened");
await extension.awaitMessage("devtools_panel_created");
+ is(await extension.awaitMessage("initial_theme_page"),
+ "light",
+ "The initial theme is reported as expected from a devtools page.");
const toolboxAdditionalTools = toolbox.getAdditionalTools();
is(toolboxAdditionalTools.length, 1,
"Got the expected number of toolbox specific panel registered.");
+ await testThemeSwitching(extension);
+
const panelId = toolboxAdditionalTools[0].id;
await gDevTools.showToolbox(target, panelId);
const {devtoolsPageTabId} = await extension.awaitMessage("devtools_panel_shown");
const devtoolsPanelTabId = await extension.awaitMessage("devtools_panel_inspectedWindow_tabId");
is(devtoolsPanelTabId, devtoolsPageTabId,
"Got the same devtools.inspectedWindow.tabId from devtools page and panel");
+ is(await extension.awaitMessage("initial_theme_panel"),
+ "light",
+ "The initial theme is reported as expected from a devtools panel.");
info("Addon Devtools Panel shown");
+ await testThemeSwitching(extension, ["page", "panel"]);
+
await gDevTools.showToolbox(target, "webconsole");
const results = await 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");
@@ -165,31 +276,33 @@ add_task(async function test_devtools_pa
"The tool has been added on visibilityswitch set to true");
is(toolbox.visibleAdditionalTools.filter(toolId => toolId == panelId).length, 1,
"The tool is visible on visibilityswitch set to true");
// Test devtools panel is loaded correctly after being toggled and
// devtools panel events has been fired as expected.
await gDevTools.showToolbox(target, panelId);
await extension.awaitMessage("devtools_panel_shown");
+ is(await extension.awaitMessage("initial_theme_panel"),
+ "light",
+ "The initial theme is reported as expected from a devtools panel.");
info("Addon Devtools Panel shown - after visibilityswitch toggled");
info("Wait until the Addon Devtools Panel has been loaded - after visibilityswitch toggled");
const panelTabIdAfterToggle = await extension.awaitMessage("devtools_panel_inspectedWindow_tabId");
is(panelTabIdAfterToggle, devtoolsPageTabId,
"Got the same devtools.inspectedWindow.tabId from devtools panel after visibility toggled");
await gDevTools.showToolbox(target, "webconsole");
const toolToggledResults = await extension.awaitMessage("devtools_panel_hidden");
info("Addon Devtools Panel hidden - after visibilityswitch toggled");
is(toolToggledResults.panelCreated, 1, "devtools.panel.create callback has been called once");
is(toolToggledResults.panelShown, 3, "panel.onShown listener has been called three times");
is(toolToggledResults.panelHidden, 3, "panel.onHidden listener has been called three times");
await gDevTools.closeToolbox(target);
-
await target.destroy();
await extension.unload();
await BrowserTestUtils.removeTab(tab);
});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionChildDevToolsUtils.jsm
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * @fileOverview
+ * This module contains utilities for interacting with DevTools
+ * from the child process.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ExtensionChildDevToolsUtils"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource://gre/modules/EventEmitter.jsm");
+
+// Create a variable to hold the cached ThemeChangeObserver which does not
+// get created until a devtools context has been created.
+let themeChangeObserver;
+
+/**
+ * An observer that watches for changes to the devtools theme and provides
+ * that information to the devtools.panels.themeName API property, as well as
+ * emits events for the devtools.panels.onThemeChanged event. It also caches
+ * the current value of devtools.themeName.
+ */
+class ThemeChangeObserver extends EventEmitter {
+ constructor(themeName, onDestroyed) {
+ super();
+ this.themeName = themeName;
+ this.onDestroyed = onDestroyed;
+ this.contexts = new Set();
+
+ Services.cpmm.addMessageListener("Extension:DevToolsThemeChanged", this);
+ }
+
+ addContext(context) {
+ if (this.contexts.has(context)) {
+ throw new Error(
+ "addContext on the ThemeChangeObserver was called more than once" +
+ " for the context.");
+ }
+
+ context.callOnClose({
+ close: () => this.onContextClosed(context),
+ });
+
+ this.contexts.add(context);
+ }
+
+ onContextClosed(context) {
+ this.contexts.delete(context);
+
+ if (this.contexts.size === 0) {
+ this.destroy();
+ }
+ }
+
+ onThemeChanged(themeName) {
+ // Update the cached themeName and emit an event for the API.
+ this.themeName = themeName;
+ this.emit("themeChanged", themeName);
+ }
+
+ receiveMessage({name, data}) {
+ if (name === "Extension:DevToolsThemeChanged") {
+ this.onThemeChanged(data.themeName);
+ }
+ }
+
+ destroy() {
+ Services.cpmm.removeMessageListener("Extension:DevToolsThemeChanged", this);
+ this.onDestroyed();
+ this.onDestroyed = null;
+ this.contexts.clear();
+ this.contexts = null;
+ }
+}
+
+this.ExtensionChildDevToolsUtils = {
+ /**
+ * Creates an cached instance of the ThemeChangeObserver class and
+ * initializes it with the current themeName. This cached instance is
+ * destroyed when all of the contexts added to it are closed.
+ *
+ * @param {string} themeName The name of the current devtools theme.
+ * @param {DevToolsContextChild} context The newly created devtools page context.
+ */
+ initThemeChangeObserver(themeName, context) {
+ if (!themeChangeObserver) {
+ themeChangeObserver = new ThemeChangeObserver(
+ themeName,
+ function() { themeChangeObserver = null; }
+ );
+ }
+ themeChangeObserver.addContext(context);
+ },
+
+ /**
+ * Returns the cached instance of ThemeChangeObserver.
+ *
+ * @returns {ThemeChangeObserver} The cached instance of ThemeChangeObserver.
+ */
+ getThemeChangeObserver() {
+ if (!themeChangeObserver) {
+ throw new Error("A ThemeChangeObserver must be created before being retrieved.");
+ }
+ return themeChangeObserver;
+ },
+};
--- a/toolkit/components/extensions/ExtensionPageChild.jsm
+++ b/toolkit/components/extensions/ExtensionPageChild.jsm
@@ -16,16 +16,18 @@ this.EXPORTED_SYMBOLS = ["ExtensionPageC
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChildDevToolsUtils",
+ "resource://gre/modules/ExtensionChildDevToolsUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
"resource://gre/modules/ExtensionManagement.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
"resource://gre/modules/WebNavigationFrames.jsm");
const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
@@ -259,16 +261,18 @@ class DevToolsContextChild extends Exten
* @param {string} params.viewType One of "devtools_page" or "devtools_panel".
* @param {object} [params.devtoolsToolboxInfo] This devtools toolbox's information,
* used if viewType is "devtools_page" or "devtools_panel".
*/
constructor(extension, params) {
super(extension, Object.assign(params, {envType: "devtools_child"}));
this.devtoolsToolboxInfo = params.devtoolsToolboxInfo;
+ ExtensionChildDevToolsUtils.initThemeChangeObserver(
+ params.devtoolsToolboxInfo.themeName, this);
this.extension.devtoolsViews.add(this);
}
unload() {
super.unload();
this.extension.devtoolsViews.delete(this);
}
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -6,16 +6,17 @@
with Files('**'):
BUG_COMPONENT = ('Toolkit', 'WebExtensions: General')
EXTRA_JS_MODULES += [
'Extension.jsm',
'ExtensionAPI.jsm',
'ExtensionChild.jsm',
+ 'ExtensionChildDevToolsUtils.jsm',
'ExtensionCommon.jsm',
'ExtensionContent.jsm',
'ExtensionManagement.jsm',
'ExtensionPageChild.jsm',
'ExtensionParent.jsm',
'ExtensionPermissions.jsm',
'ExtensionPreferencesManager.jsm',
'ExtensionSettingsStore.jsm',