--- a/browser/components/extensions/ext-theme.js
+++ b/browser/components/extensions/ext-theme.js
@@ -59,16 +59,49 @@ const kThemePropertiesVarMap = new Map([
["space_above_tabbar", ["--space-above-tabbar", ":root"]],
["square_tabs", ["--tab-curve-width", "#TabsToolbar"]],
["toolbar_button_shadow", ["--toolbarbutton-active-boxshadow", ":root"]],
["toolbar_button_shadow_inactive", ["--toolbarbutton-hover-boxshadow", ":root"]],
["toolbar_navbar_overlap", ["--tab-toolbar-navbar-overlap", ":root"]],
["toolbar_navbar_highlight_overlap", ["--navbar-tab-toolbar-highlight-overlap", ":root"]],
["toolbar_text_shadow", ["--toolbarbutton-text-shadow", ":root"]],
]);
+const kIconsVarMap = new Map([
+ ["back", ["--back-icon", ":root"]],
+ ["forward", ["--forward-icon", ":root"]],
+ ["reload", ["--reload-icon", ":root"]],
+ ["stop", ["--stop-icon", ":root"]],
+ ["bookmark_star", ["--bookmark_star-icon", ":root"]],
+ ["bookmark_menu", ["--bookmark_menu-icon", ":root"]],
+ ["downloads", ["--downloads-icon", ":root"]],
+ ["home", ["--home-icon", ":root"]],
+ ["app_menu", ["--app_menu-icon", ":root"]],
+ ["cut", ["--cut-icon", ":root"]],
+ ["copy", ["--copy-icon", ":root"]],
+ ["paste", ["--paste-icon", ":root"]],
+ ["new_window", ["--new_window-icon", ":root"]],
+ ["new_private_window", ["--new_private_window-icon", ":root"]],
+ ["save_page", ["--save_page-icon", ":root"]],
+ ["print", ["--print-icon", ":root"]],
+ ["history", ["--history-icon", ":root"]],
+ ["full_screen", ["--full_screen-icon", ":root"]],
+ ["find", ["--find-icon", ":root"]],
+ ["options", ["--options-icon", ":root"]],
+ ["addons", ["--addons-icon", ":root"]],
+ ["developer", ["--developer-icon", ":root"]],
+ ["synced_tabs", ["--synced_tabs-icon", ":root"]],
+ ["open_file", ["--open_file-icon", ":root"]],
+ ["sidebars", ["--sidebars-icon", ":root"]],
+ ["share_page", ["--share_page-icon", ":root"]],
+ ["subscribe", ["--subscribe-icon", ":root"]],
+ ["text_encoding", ["--text_encoding-icon", ":root"]],
+ ["email_link", ["--email_link-icon", ":root"]],
+ ["forget", ["--forget-icon", ":root"]],
+ ["pocket", ["--pocket-icon", ":root"]],
+]);
// Some color settings or properties from the manifest need to trigger core browser
// CSS styles to be overridden in order to be visually applied.
// The static maps below provide the basic mechanism to do just that:
// key< 'property_name' > => value< [ 'css_selector', 'css_style_properties_bag' ] >
// ^-- if present in the ^-- An array that consists of
// manifest, this rule 0. The selector to which the following styles
// will be used should apply on, overriding the current theme.
@@ -148,16 +181,112 @@ const kBrowserOverridePropertiesVarMap =
["toolbar[brighttext] #downloads-indicator-counter", {"text-shadow": "--toolbarbutton-text-shadow"}],
]],
["space_above_tabbar", [
// Give some space to drag the window around while customizing (normal space
// to left and right of tabs doesn't work in this case)
["#main-window[tabsintitlebar][customizing]", {"--space-above-tabbar": "9px"}],
]],
]);
+const kBrowserOverrideIconsVarMap = new Map([
+ ["back", [
+ ["#back-button", {"list-style-image": "--back-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["forward", [
+ ["#forward-button", {"list-style-image": "--forward-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["reload", [
+ ["#urlbar-reload-button", {"list-style-image": "--reload-icon", "-moz-image-region": "rect(0, 14px, 14px, 0)"}]
+ ]],
+ ["stop", [
+ ["#urlbar-stop-button", {"list-style-image": "--stop-icon", "-moz-image-region": "rect(0, 14px, 14px, 0)"}]
+ ]],
+ ["bookmark_star", [
+ ["#bookmarks-menu-button", {"list-style-image": "--bookmark_star-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["bookmark_menu", [
+ ["#bookmarks-menu-button[cui-areatype='toolbar'] > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon", {"list-style-image": "--bookmark_menu-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["downloads", [
+ ["#downloads-button", {"list-style-image": "--downloads-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)", "background-image": "none"}]
+ ]],
+ ["home", [
+ ["#home-button", {"list-style-image": "--home-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["app_menu", [
+ ["#PanelUI-menu-button", {"list-style-image": "--app_menu-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["cut", [
+ ["#cut-button", {"list-style-image": "--cut-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["copy", [
+ ["#copy-button", {"list-style-image": "--copy-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["paste", [
+ ["#paste-button", {"list-style-image": "--paste-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["new_window", [
+ ["#new-window-button", {"list-style-image": "--new_window-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["new_private_window", [
+ ["#privatebrowsing-button", {"list-style-image": "--new_private_window-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["save_page", [
+ ["#save-page-button", {"list-style-image": "--save_page-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["print", [
+ ["#print-button", {"list-style-image": "--print-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["history", [
+ ["#history-panelmenu", {"list-style-image": "--history-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["full_screen", [
+ ["#fullscreen-button", {"list-style-image": "--full_screen-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["find", [
+ ["#find-button", {"list-style-image": "--find-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["options", [
+ ["#preferences-button", {"list-style-image": "--options-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["addons", [
+ ["#add-ons-button", {"list-style-image": "--addons-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["developer", [
+ ["#developer-button", {"list-style-image": "--developer-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["synced_tabs", [
+ ["#sync-button", {"list-style-image": "--synced_tabs-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["open_file", [
+ ["#open-file-button", {"list-style-image": "--open_file-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["sidebars", [
+ ["#sidebar-button", {"list-style-image": "--sidebars-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["share_page", [
+ ["#social-share-button", {"list-style-image": "--share_page-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["subscribe", [
+ ["#feed-button", {"list-style-image": "--subscribe-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["text_encoding", [
+ ["#characterencoding-button", {"list-style-image": "--text_encoding-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["email_link", [
+ ["#email-link-button", {"list-style-image": "--email_link-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["forget", [
+ ["#panic-button", {"list-style-image": "--forget-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+ ["pocket", [
+ ["#pocket-button", {"list-style-image": "--pocket-icon", "-moz-image-region": "rect(0, 18px, 18px, 0)"}]
+ ]],
+]);
+
const kTransparentGif = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
const kVideoElementID = "__WebExtThemeVideoElement__";
function hueToRgb(p, q, t) {
if (t < 0) {
t += 1;
}
if (t > 1) {
@@ -293,32 +422,35 @@ class Theme {
this.browserStyleOverrides = {};
this.cssVars = {};
this.ntp_video = null;
this.load(manifest.theme);
this.render();
}
load(theme) {
- // Order of sections matters here!
+ // Order of sections matters because gradients apply on top of colors.
if (theme.images) {
this.loadImages(theme.images);
}
if (theme.colors) {
this.loadColors(theme.colors);
}
if (theme.tints) {
this.loadTints(theme.tints);
}
if (theme.gradients) {
this.loadGradients(theme.gradients);
}
if (theme.properties) {
this.loadProperties(theme.properties);
}
+ if (theme.icons) {
+ this.loadIcons(theme.icons);
+ }
}
loadColors(colors) {
for (let color of Object.getOwnPropertyNames(colors)) {
let val = colors[color];
// Since values are optional, they may be `null`.
if (val === null) {
continue;
@@ -554,54 +686,72 @@ class Theme {
if (kBrowserOverrideTintsVarMap.has(tint)) {
addToBrowserOverrides(this.browserStyleOverrides, cssColor, kBrowserOverrideTintsVarMap.get(tint));
}
}
}
}
}
+ loadIcons(icons) {
+ for (let icon of Object.getOwnPropertyNames(icons || {})) {
+ let val = icons[icon];
+ if (!val) {
+ continue;
+ }
+ if (kIconsVarMap.has(icon)) {
+ // The icons in the manifest are not pre-resolved, and must be local.
+ let resolvedURL = this.baseURI.resolve(val);
+
+ let cssIcon = `url("${resolvedURL.replace(/"/g, '\\"')}")`;
+ let [varName, ...selectors] = kIconsVarMap.get(icon);
+ addToCSSVars.apply(null, [this.cssVars, cssIcon, varName].concat(selectors));
+ addToBrowserOverrides(this.browserStyleOverrides, resolvedURL, kBrowserOverrideIconsVarMap.get(icon));
+ }
+ }
+ }
+
receiveMessage(message) {
if (message.name != "Ext:Theme:RequestRender") {
return;
}
if (this.ntp_video) {
Services.mm.broadcastAsyncMessage("Ext:Theme:Render", {
video: this.ntp_video,
});
}
}
render({asUpdate} = {asUpdate: false}) {
let browserStyles = [`
- @-moz-document url("about:home"),
- url("about:newtab"),
- url("chrome://browser/content/abouthome/aboutHome.xhtml"),
- url("chrome://browser/content/newtab/newTab.xhtml") {`];
+ @-moz-document url("about:home"),\n
+ url("about:newtab"),\n
+ url("chrome://browser/content/abouthome/aboutHome.xhtml"),\n
+ url("chrome://browser/content/newtab/newTab.xhtml") {\n`];
for (let selector of Object.getOwnPropertyNames(this.aboutHomeCSSVars)) {
browserStyles.push(`
${selector} {
- ${this.aboutHomeCSSVars[selector].join(" !important;")} !important;
+ ${this.aboutHomeCSSVars[selector].join(" !important;\n")} !important;
}`);
}
if (this.ntp_video) {
browserStyles.push(`
body {
background-size: 100% !important;
}`);
}
// Close the '@-moz-document' block.
browserStyles.push(`
}`);
for (let selector of Object.getOwnPropertyNames(this.cssVars)) {
browserStyles.push(`
${selector} {
- ${this.cssVars[selector].join(" !important;")} !important;
+ ${this.cssVars[selector].join(" !important;\n")} !important;
}`);
}
for (let selector of Object.getOwnPropertyNames(this.browserStyleOverrides)) {
let styles = this.browserStyleOverrides[selector];
if (!styles.length) {
continue;
}
browserStyles.push(`
--- a/browser/components/extensions/schemas/theme.json
+++ b/browser/components/extensions/schemas/theme.json
@@ -183,24 +183,24 @@
},
"toolbar_bottom_separator": {
"type": "array",
"items": {
"type": "number"
},
"optional": true
},
- "toolbar_button_stroke": {
+ "toolbar_stroke": {
"type": "array",
"items": {
"type": "number"
},
"optional": true
},
- "toolbar_button_stroke_inactive": {
+ "toolbar_stroke_inactive": {
"type": "array",
"items": {
"type": "number"
},
"optional": true
},
"toolbar_input_control": {
"type": "array",
@@ -231,21 +231,21 @@
"optional": true
}
}
},
"gradients": {
"type": "object",
"optional": true,
"properties": {
- "toolbar_button": {
+ "toolbar": {
"type": "string",
"optional": true
},
- "toolbar_button_pressed": {
+ "toolbar_pressed": {
"type": "string",
"optional": true
}
}
},
"properties": {
"type": "object",
"optional": true,
@@ -284,21 +284,21 @@
"tab_curve_half_width": {
"type": "number",
"optional": true
},
"tab_curve_width": {
"type": "number",
"optional": true
},
- "toolbar_button_shadow": {
+ "toolbar_shadow": {
"type": "string",
"optional": true
},
- "toolbar_button_shadow_inactive": {
+ "toolbar_shadow_inactive": {
"type": "string",
"optional": true
},
"toolbar_navbar_highlight_overlap": {
"type": "number",
"optional": true
},
"toolbar_navbar_overlap": {
@@ -325,16 +325,146 @@
"buttons": {
"type": "array",
"items": {
"type": "number"
},
"optional": true
}
}
+ },
+ "icons": {
+ "type": "object",
+ "optional": true,
+ "properties": {
+ "back": {
+ "type": "string",
+ "optional": true
+ },
+ "forward": {
+ "type": "string",
+ "optional": true
+ },
+ "reload": {
+ "type": "string",
+ "optional": true
+ },
+ "stop": {
+ "type": "string",
+ "optional": true
+ },
+ "bookmark_star": {
+ "type": "string",
+ "optional": true
+ },
+ "bookmark_menu": {
+ "type": "string",
+ "optional": true
+ },
+ "downloads": {
+ "type": "string",
+ "optional": true
+ },
+ "home": {
+ "type": "string",
+ "optional": true
+ },
+ "app_menu": {
+ "type": "string",
+ "optional": true
+ },
+ "cut": {
+ "type": "string",
+ "optional": true
+ },
+ "copy": {
+ "type": "string",
+ "optional": true
+ },
+ "paste": {
+ "type": "string",
+ "optional": true
+ },
+ "new_window": {
+ "type": "string",
+ "optional": true
+ },
+ "new_private_window": {
+ "type": "string",
+ "optional": true
+ },
+ "save_page": {
+ "type": "string",
+ "optional": true
+ },
+ "print": {
+ "type": "string",
+ "optional": true
+ },
+ "history": {
+ "type": "string",
+ "optional": true
+ },
+ "full_screen": {
+ "type": "string",
+ "optional": true
+ },
+ "find": {
+ "type": "string",
+ "optional": true
+ },
+ "options": {
+ "type": "string",
+ "optional": true
+ },
+ "addons": {
+ "type": "string",
+ "optional": true
+ },
+ "developer": {
+ "type": "string",
+ "optional": true
+ },
+ "synced_tabs": {
+ "type": "string",
+ "optional": true
+ },
+ "open_file": {
+ "type": "string",
+ "optional": true
+ },
+ "sidebars": {
+ "type": "string",
+ "optional": true
+ },
+ "share_page": {
+ "type": "string",
+ "optional": true
+ },
+ "subscribe": {
+ "type": "string",
+ "optional": true
+ },
+ "text_encoding": {
+ "type": "string",
+ "optional": true
+ },
+ "email_link": {
+ "type": "string",
+ "optional": true
+ },
+ "forget": {
+ "type": "string",
+ "optional": true
+ },
+ "pocket": {
+ "type": "string",
+ "optional": true
+ }
+ }
}
}
},
{
"$extend": "WebExtensionManifest",
"properties": {
"theme": {
"optional": true,
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_theme_icons.js
@@ -0,0 +1,142 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const kBurgerIcon = "hamburger.svg";
+const kBurritoIcon = "burrito.svg";
+const kPoopIcon = "pile-of-poop.svg";
+
+function iconForButton(buttonName) {
+ let icon = kPoopIcon;
+ if (buttonName == "app_menu") {
+ icon = kBurgerIcon;
+ } else if (buttonName == "back") {
+ icon = kBurritoIcon;
+ }
+ return icon;
+}
+
+function shouldButtonHaveCustomStyling(selector, verifyFn, icon, message) {
+ try {
+ let element;
+ if (selector == "#bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon") {
+ if (message.includes("panel")) {
+ // The dropmarker isn't shown in the menupanel.
+ return;
+ }
+ element = document.querySelector("#bookmarks-menu-button");
+ element = document.getAnonymousElementByAttribute(element, "class", "toolbarbutton-menubutton-dropmarker");
+ element = document.getAnonymousElementByAttribute(element, "class", "dropmarker-icon");
+ } else {
+ element = document.querySelector(selector);
+ }
+
+ let iconFileName = iconForButton(icon);
+ let listStyleImage = getComputedStyle(element).listStyleImage;
+ info(`listStyleImage for ${iconFileName} is ${listStyleImage}`);
+ verifyFn(listStyleImage.includes(iconFileName), message);
+ } catch (ex) {
+ ok(false, `Unable to verify ${selector}: ${ex}`);
+ }
+}
+
+function verifyButtonWithoutCustomStyling(selector, icon, message) {
+ shouldButtonHaveCustomStyling(selector, (result, message) => ok(!result, message), icon, message);
+}
+
+function verifyButtonWithCustomStyling(selector, icon, message) {
+ shouldButtonHaveCustomStyling(selector, ok, icon, message);
+}
+
+add_task(function* testCustomizedIcons() {
+ let buttons = [
+ ["back", "#back-button"],
+ ["forward", "#forward-button"],
+ ["reload", "#urlbar-reload-button"],
+ ["stop", "#urlbar-stop-button"],
+ ["bookmark_star", "#bookmarks-menu-button", "bookmarks-menu-button"],
+ ["bookmark_menu", "#bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon"],
+ ["downloads", "#downloads-button", "downloads-button"],
+ ["home", "#home-button", "home-button"],
+ ["app_menu", "#PanelUI-menu-button"],
+ ["cut", "#cut-button", "edit-controls"],
+ ["copy", "#copy-button"],
+ ["paste", "#paste-button"],
+ ["new_window", "#new-window-button", "new-window-button"],
+ ["new_private_window", "#privatebrowsing-button", "privatebrowsing-button"],
+ ["save_page", "#save-page-button", "save-page-button"],
+ ["print", "#print-button", "print-button"],
+ ["history", "#history-panelmenu", "history-panelmenu"],
+ ["full_screen", "#fullscreen-button", "fullscreen-button"],
+ ["find", "#find-button", "find-button"],
+ ["options", "#preferences-button", "preferences-button"],
+ ["addons", "#add-ons-button", "add-ons-button"],
+ ["developer", "#developer-button", "developer-button"],
+ ["synced_tabs", "#sync-button", "sync-button"],
+ ["open_file", "#open-file-button", "open-file-button"],
+ ["sidebars", "#sidebar-button", "sidebar-button"],
+ ["share_page", "#social-share-button", "social-share-button"],
+ ["subscribe", "#feed-button", "feed-button"],
+ ["text_encoding", "#characterencoding-button", "characterencoding-button"],
+ ["email_link", "#email-link-button", "email-link-button"],
+ ["forget", "#panic-button", "panic-button"],
+ ["pocket", "#pocket-button", "pocket-button"]
+ ];
+
+ window.maximize();
+
+ try {
+ for (let button of buttons) {
+ if (button[2]) {
+ CustomizableUI.addWidgetToArea(button[2], CustomizableUI.AREA_NAVBAR);
+ }
+
+ verifyButtonWithoutCustomStyling(button[1], button[0],
+ `The ${button[1]} should not have it's icon customized when the test starts`);
+ }
+
+ let manifest = {
+ theme: {
+ icons: {}
+ }
+ };
+
+ for (let button of buttons) {
+ manifest.theme.icons[button[0]] = iconForButton(button[0]);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({manifest});
+
+ yield extension.startup();
+
+ for (let button of buttons) {
+ verifyButtonWithCustomStyling(button[1], button[0],
+ `The ${button[1]} should have it's icon customized in the toolbar`);
+ }
+
+ for (let button of buttons) {
+ if (button[2]) {
+ CustomizableUI.addWidgetToArea(button[2], CustomizableUI.AREA_PANEL);
+ }
+ yield PanelUI.show();
+ }
+
+ for (let button of buttons) {
+ verifyButtonWithCustomStyling(button[1], button[0],
+ `The ${button[1]} should have it's icon customized in the panel`);
+ }
+
+ yield PanelUI.hide();
+
+ yield extension.unload();
+
+ for (let button of buttons) {
+ verifyButtonWithoutCustomStyling(button[1], button[0],
+ `The ${button[1]} should not have it's icon customized when the theme is unloaded`);
+ }
+ } finally {
+ CustomizableUI.reset();
+ window.restore();
+ }
+
+});