From 247590ec7da12980d9420974e1272f2ff6b84093 Mon Sep 17 00:00:00 2001 From: Matthew Wein Date: Wed, 25 Jan 2017 15:11:20 -0800 Subject: [PATCH] Bug 1330341 - Add support for dynamic updates r=mikedeboer MozReview-Commit-ID: 8wA7J1oX2t --HG-- extra : rebase_source : e244e9803644676398d952057d416f2d9558e833 --- browser/components/extensions/ext-theme.js | 139 +++++++++++++----- .../components/extensions/schemas/theme.json | 20 +++ .../test/browser/browser-common.ini | 1 + .../browser_ext_themes_dynamic_updates.js | 87 +++++++++++ .../browser/browser_ext_themes_lwtsupport.js | 30 ++-- 5 files changed, 225 insertions(+), 52 deletions(-) create mode 100644 browser/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js diff --git a/browser/components/extensions/ext-theme.js b/browser/components/extensions/ext-theme.js index 2e9a72018258..6198232282da 100644 --- a/browser/components/extensions/ext-theme.js +++ b/browser/components/extensions/ext-theme.js @@ -5,58 +5,123 @@ Cu.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Preferences", "resource://gre/modules/Preferences.jsm"); -/* eslint-disable mozilla/balanced-listeners */ -extensions.on("manifest_theme", (type, directive, extension, manifest) => { - let enabled = Preferences.get("extensions.webextensions.themes.enabled"); +// WeakMap[Extension -> Theme] +let themeMap = new WeakMap(); - 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; - } - if (color == "accentcolor") { - lwtStyles.accentcolor = val; - continue; - } - if (color == "textcolor") { - lwtStyles.textcolor = val; - } + /** + * 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); + } + + // 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)); } } - 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 colors found in the extension's manifest. + * + * @param {Object} colors Dictionary mapping color properties to values. + */ + loadColors(colors) { + let {accentcolor, textcolor} = colors; - if (image == "headerURL") { - lwtStyles.headerURL = val; - } + if (accentcolor) { + this.lwtStyles.accentcolor = accentcolor; + } + + if (textcolor) { + this.lwtStyles.textcolor = textcolor; } } - if (lwtStyles.headerURL && - lwtStyles.accentcolor && - lwtStyles.textcolor) { + /** + * 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 (headerURL) { + this.lwtStyles.headerURL = headerURL; + } + } + + /** + * 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) => { - 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); + }, + }, + }; +}); diff --git a/browser/components/extensions/schemas/theme.json b/browser/components/extensions/schemas/theme.json index a915dcd227b5..4795b02179d5 100644 --- a/browser/components/extensions/schemas/theme.json +++ b/browser/components/extensions/schemas/theme.json @@ -46,5 +46,25 @@ } } ] + }, + { + "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." + } + ] + } + ] } ] diff --git a/browser/components/extensions/test/browser/browser-common.ini b/browser/components/extensions/test/browser/browser-common.ini index b83a4caa04fc..b6e3fba4642f 100644 --- a/browser/components/extensions/test/browser/browser-common.ini +++ b/browser/components/extensions/test/browser/browser-common.ini @@ -107,6 +107,7 @@ support-files = [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] diff --git a/browser/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js b/browser/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js new file mode 100644 index 000000000000..dff8c63068f7 --- /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"); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_themes_lwtsupport.js b/browser/components/extensions/test/browser/browser_ext_themes_lwtsupport.js index d538c61f1e4c..5e2e0a351fd4 100644 --- a/browser/components/extensions/test/browser/browser_ext_themes_lwtsupport.js +++ b/browser/components/extensions/test/browser/browser_ext_themes_lwtsupport.js @@ -1,8 +1,8 @@ "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); @@ -15,16 +15,16 @@ add_task(function* setup() { }); }); -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, }, }, }, @@ -39,11 +39,11 @@ add_task(function* testSupportLWTProperties() { 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(); @@ -51,12 +51,12 @@ add_task(function* testSupportLWTProperties() { 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, }, }, }, @@ -70,13 +70,13 @@ add_task(function* testLWTRequiresAllPropertiesDefinedImageOnly() { 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, }, }, },