Bug 1330341 - Add support for dynamic updates r?mikedeboer
MozReview-Commit-ID: 8wA7J1oX2t
--- a/browser/components/extensions/ext-theme.js
+++ b/browser/components/extensions/ext-theme.js
@@ -1,69 +1,127 @@
-/* -*- 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, "Preferences",
"resource://gre/modules/Preferences.jsm");
-let themeExtensions = new WeakSet();
+// WeakMap[Extension -> Theme]
+let themeMap = new WeakMap();
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("manifest_theme", (type, directive, extension, manifest) => {
- let enabled = Preferences.get("extensions.webextensions.themes.enabled");
-
- if (!enabled || !manifest || !manifest.theme) {
- return;
+/** Class representing a theme. */
+class Theme {
+ /**
+ * Creates a theme instance.
+ */
+ constructor() {
+ // A dictionary of light weight theme styles.
+ this.lwtStyles = {};
}
- // Apply theme only if themes are enabled.
- let lwtStyles = {footerURL: ""};
- if (manifest.theme.colors) {
- let colors = manifest.theme.colors;
- for (let color of Object.getOwnPropertyNames(colors)) {
- let val = colors[color];
- // Since values are optional, they may be `null`.
- if (val === null) {
- continue;
- }
+
+ /**
+ * 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.
+ */
+ load(details) {
+ if (details.colors) {
+ this.loadColors(details.colors);
+ }
+
+ if (details.images) {
+ this.loadImages(details.images);
+ }
- if (color == "accentcolor") {
- lwtStyles.accentcolor = val;
- continue;
- }
- if (color == "textcolor") {
- lwtStyles.textcolor = val;
- }
+ // Lightweight themes require all properties to be defined.
+ if (this.lwtStyles.headerURL &&
+ this.lwtStyles.accentcolor &&
+ this.lwtStyles.textcolor) {
+ Services.obs.notifyObservers(null,
+ "lightweight-theme-styling-update",
+ JSON.stringify(this.lwtStyles));
+ }
+ }
+
+ /**
+ * Helper method for loading colors found in the extension's manifest.
+ *
+ * @param {Object} colors Dictionary mapping color properties to values.
+ */
+ loadColors(colors) {
+ let {accentcolor, textcolor} = colors;
+
+ if (accentcolor) {
+ this.lwtStyles.accentcolor = accentcolor;
+ }
+
+ if (textcolor) {
+ this.lwtStyles.textcolor = textcolor;
}
}
- if (manifest.theme.images) {
- let images = manifest.theme.images;
- for (let image of Object.getOwnPropertyNames(images)) {
- let val = images[image];
- if (val === null) {
- continue;
- }
+ /**
+ * Helper method for loading images found in the extension's manifest.
+ *
+ * @param {Object} images Dictionary mapping image properties to values.
+ */
+ loadImages(images) {
+ let {headerURL} = images;
- if (image == "headerURL") {
- lwtStyles.headerURL = val;
- }
+ if (headerURL) {
+ this.lwtStyles.headerURL = headerURL;
}
}
- if (lwtStyles.headerURL &&
- lwtStyles.accentcolor &&
- lwtStyles.textcolor) {
- themeExtensions.add(extension);
+ /**
+ * Unloads the currently applied theme.
+ */
+ unload() {
Services.obs.notifyObservers(null,
"lightweight-theme-styling-update",
- JSON.stringify(lwtStyles));
+ null);
}
+}
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("manifest_theme", (type, directive, extension, manifest) => {
+ if (!Preferences.get("extensions.webextensions.themes.enabled")) {
+ // Return early if themes are disabled.
+ return;
+ }
+
+ let theme = new Theme();
+ theme.load(manifest.theme);
+ themeMap.set(extension, theme);
});
extensions.on("shutdown", (type, extension) => {
- if (themeExtensions.has(extension)) {
- Services.obs.notifyObservers(null, "lightweight-theme-styling-update", null);
+ let theme = themeMap.get(extension);
+
+ // We won't have a theme if theme's aren't enabled.
+ if (!theme) {
+ return;
}
+
+ theme.unload();
});
/* eslint-enable mozilla/balanced-listeners */
+
+extensions.registerSchemaAPI("theme", "addon_parent", context => {
+ let {extension} = context;
+ return {
+ theme: {
+ update(details) {
+ let theme = themeMap.get(extension);
+
+ // We won't have a theme if theme's aren't enabled.
+ if (!theme) {
+ return;
+ }
+
+ theme.load(details);
+ },
+ },
+ };
+});
--- a/browser/components/extensions/schemas/theme.json
+++ b/browser/components/extensions/schemas/theme.json
@@ -41,10 +41,30 @@
"properties": {
"theme": {
"optional": true,
"$ref": "ThemeType"
}
}
}
]
+ },
+ {
+ "namespace": "theme",
+ "description": "The theme API allows customizing of visual elements of the browser.",
+ "permissions": ["manifest:theme"],
+ "functions": [
+ {
+ "name": "update",
+ "type": "function",
+ "async": true,
+ "description": "Make complete or partial updates to the theme. Resolves when the update has completed.",
+ "parameters": [
+ {
+ "name": "details",
+ "$ref": "manifest.ThemeType",
+ "description": "The properties of the theme to update."
+ }
+ ]
+ }
+ ]
}
]
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -102,16 +102,17 @@ support-files =
[browser_ext_tabs_query.js]
[browser_ext_tabs_reload.js]
[browser_ext_tabs_reload_bypass_cache.js]
[browser_ext_tabs_sendMessage.js]
[browser_ext_tabs_cookieStoreId.js]
[browser_ext_tabs_update.js]
[browser_ext_tabs_zoom.js]
[browser_ext_tabs_update_url.js]
+[browser_ext_themes_dynamic_updates.js]
[browser_ext_themes_lwtsupport.js]
[browser_ext_topwindowid.js]
[browser_ext_url_overrides.js]
[browser_ext_webRequest.js]
[browser_ext_webNavigation_frameId0.js]
[browser_ext_webNavigation_getFrames.js]
[browser_ext_webNavigation_urlbar_transitions.js]
[browser_ext_windows.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js
@@ -0,0 +1,87 @@
+"use strict";
+
+// PNG image data for a simple red dot.
+const BACKGROUND_1 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+const ACCENT_COLOR_1 = "#a14040";
+const TEXT_COLOR_1 = "#fac96e";
+
+// 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==";
+const ACCENT_COLOR_2 = "#03fe03";
+const TEXT_COLOR_2 = "#0ef325";
+
+function hexToRGB(hex) {
+ hex = parseInt((hex.indexOf("#") > -1 ? hex.substring(1) : hex), 16);
+ return [hex >> 16, (hex & 0x00FF00) >> 8, (hex & 0x0000FF)];
+}
+
+function validateTheme(backgroundImage, accentColor, textColor) {
+ let docEl = window.document.documentElement;
+ let style = window.getComputedStyle(docEl);
+
+ Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set");
+ Assert.equal(docEl.getAttribute("lwthemetextcolor"), "bright",
+ "LWT text color attribute should be set");
+
+ Assert.equal(style.backgroundImage, 'url("' + backgroundImage.replace(/"/g, '\\"') + '")',
+ "Expected correct background image");
+ Assert.equal(style.backgroundColor, "rgb(" + hexToRGB(accentColor).join(", ") + ")",
+ "Expected correct accent color");
+ Assert.equal(style.color, "rgb(" + hexToRGB(textColor).join(", ") + ")",
+ "Expected correct text color");
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["extensions.webextensions.themes.enabled", true]],
+ });
+});
+
+add_task(function* test_dynamic_theme_updates() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "theme": {
+ "images": {
+ "headerURL": BACKGROUND_1,
+ },
+ "colors": {
+ "accentcolor": ACCENT_COLOR_1,
+ "textcolor": TEXT_COLOR_1,
+ },
+ },
+ },
+ background() {
+ browser.test.onMessage.addListener((msg, details) => {
+ if (msg != "update-theme") {
+ browser.test.fail("expected 'update-theme' message");
+ }
+
+ browser.theme.update(details);
+ browser.test.sendMessage("theme-updated");
+ });
+ },
+ });
+
+ yield extension.startup();
+
+ validateTheme(BACKGROUND_1, ACCENT_COLOR_1, TEXT_COLOR_1);
+
+ extension.sendMessage("update-theme", {
+ "images": {
+ "headerURL": BACKGROUND_2,
+ },
+ "colors": {
+ "accentcolor": ACCENT_COLOR_2,
+ "textcolor": TEXT_COLOR_2,
+ },
+ });
+
+ yield extension.awaitMessage("theme-updated");
+
+ validateTheme(BACKGROUND_2, ACCENT_COLOR_2, TEXT_COLOR_2);
+
+ yield extension.unload();
+
+ let docEl = window.document.documentElement;
+ Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
+});
--- a/browser/components/extensions/test/browser/browser_ext_themes_lwtsupport.js
+++ b/browser/components/extensions/test/browser/browser_ext_themes_lwtsupport.js
@@ -1,87 +1,87 @@
"use strict";
-const kBackground = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
-const kAccentColor = "#a14040";
-const kTextColor = "#fac96e";
+const BACKGROUND = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+const ACCENT_COLOR = "#a14040";
+const TEXT_COLOR = "#fac96e";
function hexToRGB(hex) {
hex = parseInt((hex.indexOf("#") > -1 ? hex.substring(1) : hex), 16);
return [hex >> 16, (hex & 0x00FF00) >> 8, (hex & 0x0000FF)];
}
add_task(function* setup() {
yield SpecialPowers.pushPrefEnv({
set: [["extensions.webextensions.themes.enabled", true]],
});
});
-add_task(function* testSupportLWTProperties() {
+add_task(function* test_support_LWT_properties() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"theme": {
"images": {
- "headerURL": kBackground,
+ "headerURL": BACKGROUND,
},
"colors": {
- "accentcolor": kAccentColor,
- "textcolor": kTextColor,
+ "accentcolor": ACCENT_COLOR,
+ "textcolor": TEXT_COLOR,
},
},
},
});
yield extension.startup();
let docEl = window.document.documentElement;
let style = window.getComputedStyle(docEl);
Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set");
Assert.equal(docEl.getAttribute("lwthemetextcolor"), "bright",
"LWT text color attribute should be set");
- Assert.equal(style.backgroundImage, 'url("' + kBackground.replace(/"/g, '\\"') + '")',
+ Assert.equal(style.backgroundImage, 'url("' + BACKGROUND.replace(/"/g, '\\"') + '")',
"Expected background image");
- Assert.equal(style.backgroundColor, "rgb(" + hexToRGB(kAccentColor).join(", ") + ")",
+ Assert.equal(style.backgroundColor, "rgb(" + hexToRGB(ACCENT_COLOR).join(", ") + ")",
"Expected correct background color");
- Assert.equal(style.color, "rgb(" + hexToRGB(kTextColor).join(", ") + ")",
+ Assert.equal(style.color, "rgb(" + hexToRGB(TEXT_COLOR).join(", ") + ")",
"Expected correct text color");
yield extension.unload();
Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
});
-add_task(function* testLWTRequiresAllPropertiesDefinedImageOnly() {
+add_task(function* test_LWT_requires_all_properties_defined_image_only() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"theme": {
"images": {
- "headerURL": kBackground,
+ "headerURL": BACKGROUND,
},
},
},
});
yield extension.startup();
let docEl = window.document.documentElement;
Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
yield extension.unload();
Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
});
-add_task(function* testLWTRequiresAllPropertiesDefinedColorsOnly() {
+add_task(function* test_LWT_requires_all_properties_defined_colors_only() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"theme": {
"colors": {
- "accentcolor": kAccentColor,
- "textcolor": kTextColor,
+ "accentcolor": ACCENT_COLOR,
+ "textcolor": TEXT_COLOR,
},
},
},
});
yield extension.startup();
let docEl = window.document.documentElement;