Bug 1349944 - Add browser.theme.getCurrent() to query the selected theme. r=jaws, mixedpuppy
MozReview-Commit-ID: Hzdm21riVlb
--- a/toolkit/components/extensions/ext-theme.js
+++ b/toolkit/components/extensions/ext-theme.js
@@ -23,34 +23,53 @@ class Theme {
* Creates a theme instance.
*
* @param {string} baseURI The base URI of the extension, used to
* resolve relative filepaths.
* @param {Object} logger Reference to the (console) logger that will be used
* to show manifest warnings to the theme author.
*/
constructor(baseURI, logger) {
- // A dictionary of light weight theme styles.
- this.lwtStyles = {
- icons: {},
- };
+ // The base theme applied to all windows.
+ this.baseProperties = {};
+
+ // Window-specific theme overrides.
+ this.windowOverrides = new WeakMap();
+
this.baseURI = baseURI;
this.logger = logger;
}
/**
+ * Gets the current theme for a specified window
+ *
+ * @param {Object} window
+ * @returns {Object} The theme of the specified window
+ */
+ getWindowTheme(window) {
+ if (this.windowOverrides.has(window)) {
+ return this.windowOverrides.get(window);
+ }
+ return this.baseProperties;
+ }
+
+ /**
* Loads a theme by reading the properties from the extension's manifest.
* This method will override any currently applied theme.
*
* @param {Object} details Theme part of the manifest. Supported
* properties can be found in the schema under ThemeType.
* @param {Object} targetWindow The window to apply the theme to. Omitting
* this parameter will apply the theme globally.
*/
load(details, targetWindow) {
+ this.lwtStyles = {
+ icons: {},
+ };
+
if (targetWindow) {
this.lwtStyles.window = getWinUtils(targetWindow).outerWindowID;
}
if (details.colors) {
this.loadColors(details.colors);
}
@@ -65,16 +84,21 @@ class Theme {
if (details.properties) {
this.loadProperties(details.properties);
}
// Lightweight themes require all properties to be defined.
if (this.lwtStyles.headerURL &&
this.lwtStyles.accentcolor &&
this.lwtStyles.textcolor) {
+ if (!targetWindow) {
+ this.baseProperties = details;
+ } else {
+ this.windowOverrides.set(targetWindow, details);
+ }
LightweightThemeManager.fallbackThemeData = this.lwtStyles;
Services.obs.notifyObservers(null,
"lightweight-theme-styling-update",
JSON.stringify(this.lwtStyles));
} else {
this.logger.warn("Your theme doesn't include one of the following required " +
"properties: 'headerURL', 'accentcolor' or 'textcolor'");
}
@@ -240,37 +264,41 @@ class Theme {
}
}
/**
* Unloads the currently applied theme.
* @param {Object} targetWindow The window the theme should be unloaded from
*/
unload(targetWindow) {
- let lwtStyles = {
+ this.lwtStyles = {
headerURL: "",
accentcolor: "",
additionalBackgrounds: "",
backgroundsAlignment: "",
backgroundsTiling: "",
textcolor: "",
icons: {},
};
if (targetWindow) {
- lwtStyles.window = getWinUtils(targetWindow).outerWindowID;
+ this.lwtStyles.window = getWinUtils(targetWindow).outerWindowID;
+ this.windowOverrides.set(targetWindow, {});
+ } else {
+ this.windowOverrides = new WeakMap();
+ this.baseProperties = {};
}
for (let icon of ICONS) {
- lwtStyles.icons[`--${icon}--icon`] = "";
+ this.lwtStyles.icons[`--${icon}--icon`] = "";
}
LightweightThemeManager.fallbackThemeData = null;
Services.obs.notifyObservers(null,
"lightweight-theme-styling-update",
- JSON.stringify(lwtStyles));
+ JSON.stringify(this.lwtStyles));
}
}
this.theme = class extends ExtensionAPI {
onManifestEntry(entryName) {
if (!gThemesEnabled) {
// Return early if themes are disabled.
return;
@@ -294,16 +322,33 @@ this.theme = class extends ExtensionAPI
}
}
getAPI(context) {
let {extension} = context;
return {
theme: {
+ getCurrent: (windowId) => {
+ // Return empty theme if none is applied.
+ if (!this.theme) {
+ return Promise.resolve({});
+ }
+
+ // Return theme applied on last focused window when no ID is supplied.
+ if (!windowId) {
+ return Promise.resolve(this.theme.getWindowTheme(windowTracker.topWindow));
+ }
+
+ const browserWindow = windowTracker.getWindow(windowId, context);
+ if (!browserWindow) {
+ return Promise.reject(`Invalid window ID: ${windowId}`);
+ }
+ return Promise.resolve(this.theme.getWindowTheme(browserWindow));
+ },
update: (windowId, details) => {
if (!gThemesEnabled) {
// Return early if themes are disabled.
return;
}
if (!this.theme) {
// WebExtensions using the Theme API will not have a theme defined
--- a/toolkit/components/extensions/schemas/theme.json
+++ b/toolkit/components/extensions/schemas/theme.json
@@ -454,20 +454,34 @@
]
},
{
"namespace": "theme",
"description": "The theme API allows customizing of visual elements of the browser.",
"permissions": ["theme"],
"functions": [
{
+ "name": "getCurrent",
+ "type": "function",
+ "async": true,
+ "description": "Returns the current theme for the specified window or the last focused window.",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "windowId",
+ "optional": true,
+ "description": "The window for which we want the theme."
+ }
+ ]
+ },
+ {
"name": "update",
"type": "function",
"async": true,
- "description": "Make complete or partial updates to the theme. Resolves when the update has completed.",
+ "description": "Make complete updates to the theme. Resolves when the update has completed.",
"parameters": [
{
"type": "integer",
"name": "windowId",
"optional": true,
"description": "The id of the window to update. No id updates all windows."
},
{
--- a/toolkit/components/extensions/test/browser/browser.ini
+++ b/toolkit/components/extensions/test/browser/browser.ini
@@ -1,12 +1,13 @@
[DEFAULT]
support-files =
head.js
[browser_ext_management_themes.js]
[browser_ext_themes_chromeparity.js]
+[browser_ext_themes_dynamic_getCurrent.js]
[browser_ext_themes_dynamic_updates.js]
[browser_ext_themes_lwtsupport.js]
[browser_ext_themes_multiple_backgrounds.js]
[browser_ext_themes_persistence.js]
[browser_ext_themes_toolbar_fields.js]
[browser_ext_themes_toolbars.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_getCurrent.js
@@ -0,0 +1,140 @@
+"use strict";
+
+// This test checks whether browser.theme.getCurrent() works correctly in different
+// configurations and with different parameter.
+
+// PNG image data for a simple red dot.
+const BACKGROUND_1 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+// PNG image data for the Mozilla dino head.
+const BACKGROUND_2 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
+
+add_task(async function test_get_current() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ const ACCENT_COLOR_1 = "#a14040";
+ const TEXT_COLOR_1 = "#fac96e";
+
+ const ACCENT_COLOR_2 = "#03fe03";
+ const TEXT_COLOR_2 = "#0ef325";
+
+ const theme1 = {
+ "images": {
+ "headerURL": "image1.png",
+ },
+ "colors": {
+ "accentcolor": ACCENT_COLOR_1,
+ "textcolor": TEXT_COLOR_1,
+ },
+ };
+
+ const theme2 = {
+ "images": {
+ "headerURL": "image2.png",
+ },
+ "colors": {
+ "accentcolor": ACCENT_COLOR_2,
+ "textcolor": TEXT_COLOR_2,
+ },
+ };
+
+ function testTheme1(returnedTheme) {
+ browser.test.assertTrue(returnedTheme.images.headerURL.includes("image1.png"),
+ "Theme 1 header URL should be applied");
+ browser.test.assertEq(ACCENT_COLOR_1, returnedTheme.colors.accentcolor,
+ "Theme 1 accent color should be applied");
+ browser.test.assertEq(TEXT_COLOR_1, returnedTheme.colors.textcolor,
+ "Theme 1 text color should be applied");
+ }
+
+ function testTheme2(returnedTheme) {
+ browser.test.assertTrue(returnedTheme.images.headerURL.includes("image2.png"),
+ "Theme 2 header URL should be applied");
+ browser.test.assertEq(ACCENT_COLOR_2, returnedTheme.colors.accentcolor,
+ "Theme 2 accent color should be applied");
+ browser.test.assertEq(TEXT_COLOR_2, returnedTheme.colors.textcolor,
+ "Theme 2 text color should be applied");
+ }
+
+ function testEmptyTheme(returnedTheme) {
+ browser.test.assertEq(0, Object.keys(returnedTheme).length, JSON.stringify(returnedTheme, null, 2));
+ }
+
+ browser.test.log("Testing getCurrent() with initial unthemed window");
+ const firstWin = await browser.windows.getCurrent();
+ testEmptyTheme(await browser.theme.getCurrent());
+ testEmptyTheme(await browser.theme.getCurrent(firstWin.id));
+
+ browser.test.log("Testing getCurrent() with after theme.update()");
+ await browser.theme.update(theme1);
+ testTheme1(await browser.theme.getCurrent());
+ testTheme1(await browser.theme.getCurrent(firstWin.id));
+
+ browser.test.log("Testing getCurrent() with after theme.update(windowId)");
+ const secondWin = await browser.windows.create();
+ await browser.theme.update(secondWin.id, theme2);
+ testTheme2(await browser.theme.getCurrent());
+ testTheme1(await browser.theme.getCurrent(firstWin.id));
+ testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+ browser.test.log("Testing getCurrent() after window focus change");
+ await browser.windows.update(firstWin.id, {focused: true});
+ testTheme1(await browser.theme.getCurrent());
+ testTheme1(await browser.theme.getCurrent(firstWin.id));
+ testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+ browser.test.log("Testing getCurrent() after another window focus change");
+ await browser.windows.update(secondWin.id, {focused: true});
+ testTheme2(await browser.theme.getCurrent());
+ testTheme1(await browser.theme.getCurrent(firstWin.id));
+ testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+ browser.test.log("Testing getCurrent() after theme.reset(windowId)");
+ await browser.theme.reset(firstWin.id);
+ testTheme2(await browser.theme.getCurrent());
+ testEmptyTheme(await browser.theme.getCurrent(firstWin.id));
+ testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+ browser.test.log("Testing getCurrent() after reset and window focus change");
+ await browser.windows.update(firstWin.id, {focused: true});
+ testEmptyTheme(await browser.theme.getCurrent());
+ testEmptyTheme(await browser.theme.getCurrent(firstWin.id));
+ testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+ browser.test.log("Testing getCurrent() after theme.update(windowId)");
+ await browser.theme.update(firstWin.id, theme1);
+ testTheme1(await browser.theme.getCurrent());
+ testTheme1(await browser.theme.getCurrent(firstWin.id));
+ testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+ browser.test.log("Testing getCurrent() after theme.reset()");
+ await browser.theme.reset();
+ testEmptyTheme(await browser.theme.getCurrent());
+ testEmptyTheme(await browser.theme.getCurrent(firstWin.id));
+ testEmptyTheme(await browser.theme.getCurrent(secondWin.id));
+
+ browser.test.log("Testing getCurrent() after closing a window");
+ await browser.windows.remove(secondWin.id);
+ testEmptyTheme(await browser.theme.getCurrent());
+ testEmptyTheme(await browser.theme.getCurrent(firstWin.id));
+
+ browser.test.log("Testing getCurrent() with invalid window ID");
+ await browser.test.assertRejects(
+ browser.theme.getCurrent(secondWin.id),
+ /Invalid window/,
+ "Invalid window should throw",
+ );
+ browser.test.notifyPass("get_current");
+ },
+ manifest: {
+ permissions: ["theme"],
+ },
+ files: {
+ "image1.png": BACKGROUND_1,
+ "image2.png": BACKGROUND_2,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("get_current");
+ await extension.unload();
+});