Bug 1311722 - Add support for dynamic theme updates r?jaws draft
authorMatthew Wein <mwein@mozilla.com>
Fri, 21 Oct 2016 14:59:13 +0100
changeset 428746 471250cacc583496404db5341ecdd1500e112030
parent 428745 d6dc1f5435991946aa877bb05402d3de6922ed28
child 534830 8f5342d606b4825126c0b53ea143bb426d50e32c
push id33412
push usermwein@mozilla.com
push dateMon, 24 Oct 2016 15:46:56 +0000
reviewersjaws
bugs1311722
milestone52.0a1
Bug 1311722 - Add support for dynamic theme updates r?jaws MozReview-Commit-ID: 206GJJ3J9DL
browser/components/extensions/ext-theme.js
browser/components/extensions/schemas/theme.json
browser/components/extensions/test/browser/browser_ext_theme_abouthomebackground.js
--- a/browser/components/extensions/ext-theme.js
+++ b/browser/components/extensions/ext-theme.js
@@ -5,72 +5,93 @@ Cu.import("resource://gre/modules/Servic
 // WeakMap[Extension -> Theme]
 let themeMap = new WeakMap();
 
 let styleSheetService = Components.classes["@mozilla.org/content/style-sheet-service;1"]
                                   .getService(Components.interfaces.nsIStyleSheetService);
 let ioService = Components.classes["@mozilla.org/network/io-service;1"]
                           .getService(Components.interfaces.nsIIOService);
 
-function Theme(manifest) {
-  this.userSheetURI = null;
-  this.LWTStyles = null;
-
-  this.loadThemeFromManifest(manifest);
-}
+class Theme {
+  constructor(manifest) {
+    this.userSheetURI = null;
+    this.LWTStyles = {};
+    this.cssVars = {};
+    this.load(manifest.theme);
+    this.render();
+  }
 
-Theme.prototype = {
-  loadThemeFromManifest(manifest) {
-    // A temporary element is created to filter the CSS values that
-    // themes can provide.
-    let temp = WindowManager.topWindow.document.createElement("temp");
-    let cssVars = "";
-    let LWTStyles = this.LWTStyles = { headerURL: "", footerURL: "", textcolor: "", accentcolor: "" };
+  load(theme) {
+    if (theme.images) {
+      this.loadImages(theme.images);
+    }
+    if (theme.colors) {
+      this.loadColors(theme.colors);
+    }
+  }
 
-    for (let image of Object.getOwnPropertyNames(manifest.theme.images || {})) {
-      let cssURL = 'url("' + manifest.theme.images[image].replace(/"/g, '\\"') + '")';
-      if (image == "theme_ntp_background") {
-        temp.style.background = cssURL;
-        cssVars += `--page-background: ${temp.style.background} !important;`;
-      } else if (image == "theme_frame" || image == "headerURL") {
-        LWTStyles.headerURL = manifest.theme.images[image];
+  loadImages(images) {
+    // Use a temporary element to filter the CSS values that themes can provide.
+    if (WindowManager.topWindow) {
+      let temp = WindowManager.topWindow.document.createElement("temp");
+      for (let image of Object.getOwnPropertyNames(images)) {
+        if (images[image]) {
+          let cssURL = 'url("' + images[image].replace(/"/g, '\\"') + '")';
+          if (image == "theme_ntp_background") {
+            temp.style.background = cssURL;
+            this.cssVars["--page-background"] = `${temp.style.background} !important;`;
+          } else if (image == "theme_frame" || image == "headerURL") {
+            this.LWTStyles.headerURL = images[image];
+          }
+        }
       }
     }
+  }
 
-    for (let color of Object.getOwnPropertyNames(manifest.theme.colors || {})) {
-      let val = manifest.theme.colors[color];
+  loadColors(colors) {
+    for (let color of Object.getOwnPropertyNames(colors)) {
+      let val = details.theme.colors[color];
       let cssColor = Array.isArray(val) ?
         "rgb" + (val.length > 3 ? "a" : "") + "(" + val.join(",") + ")" : val;
       switch (color) {
         case "accentcolor":
         case "frame":
-          LWTStyles.accentcolor = cssColor;
+          this.LWTStyles.accentcolor = cssColor;
           break;
         case "tab_text":
         case "textcolor":
-          LWTStyles.textcolor = cssColor;
+          this.LWTStyles.textcolor = cssColor;
           break;
       }
     }
+  }
 
-    let aboutHomeStyles = `
-      @-moz-document url("about:home"),
-                     url("chrome://browser/content/abouthome/aboutHome.xhtml") {
-        :root {
-          ${cssVars}
-        }
-      }`;
-    let dataURL = `data:text/css,${aboutHomeStyles}`;
-    this.userSheetURI = ioService.newURI(dataURL, null, null);
-    styleSheetService.loadAndRegisterSheet(this.userSheetURI, styleSheetService.USER_SHEET);
+  render() {
+    if (this.cssVars) {
+      let aboutHomeStyles = `
+        @-moz-document url("about:home"),
+                       url("chrome://browser/content/abouthome/aboutHome.xhtml") {
+          :root {
+            --page-background: ${this.cssVars["--page-background"]}
+          }
+        }`;
+      let dataURL = `data:text/css,${aboutHomeStyles}`;
 
-    if (LWTStyles.headerURL) {
+      if (this.userSheetURI) {
+        styleSheetService.unregisterSheet(this.userSheetURI, styleSheetService.USER_SHEET);
+      }
+
+      this.userSheetURI = ioService.newURI(dataURL, null, null);
+      styleSheetService.loadAndRegisterSheet(this.userSheetURI, styleSheetService.USER_SHEET);
+    }
+
+    if (this.LWTStyles.headerURL) {
       Services.obs.notifyObservers(null, "lightweight-theme-styling-update", JSON.stringify(LWTStyles));
     }
-  },
+  }
 
   shutdown() {
     if (this.userSheetURI) {
       styleSheetService.unregisterSheet(this.userSheetURI, styleSheetService.USER_SHEET);
     }
     if (this.LWTStyles.headerURL) {
       Services.obs.notifyObservers(null, "lightweight-theme-styling-update", null);
     }
@@ -86,12 +107,19 @@ extensions.on("shutdown", (type, extensi
   if (themeMap.has(extension)) {
     themeMap.get(extension).shutdown();
     themeMap.delete(extension);
   }
 });
 /* eslint-enable mozilla/balanced-listeners */
 
 extensions.registerSchemaAPI("theme", "addon_parent", context => {
+  let {extension} = context;
   return {
-    theme: {},
+    theme: {
+      update(details) {
+        let theme = themeMap.get(extension);
+        theme.load(details);
+        theme.render();
+      }
+    },
   };
 });
--- a/browser/components/extensions/schemas/theme.json
+++ b/browser/components/extensions/schemas/theme.json
@@ -2,74 +2,91 @@
 // 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/.
 
 [
   {
     "namespace": "manifest",
     "types": [
       {
-        "$extend": "WebExtensionManifest",
+        "id": "ThemeType",
+        "type": "object",
         "properties": {
-          "theme": {
+          "images": {
+            "type": "object",
+            "optional": true,
+            "properties": {
+              "headerURL": {
+                "type": "string",
+                "optional": true
+              },
+              "theme_frame": {
+                "type": "string",
+                "optional": true
+              },
+              "theme_ntp_background": {
+                "type": "string",
+                "optional": true
+              }
+            }
+          },
+          "colors": {
             "type": "object",
             "optional": true,
             "properties": {
-              "images": {
-                "type": "object",
-                "optional": true,
-                "properties": {
-                  "headerURL": {
-                    "type": "string",
-                    "optional": true
-                  },
-                  "theme_frame": {
-                    "type": "string",
-                    "optional": true
-                  },
-                  "theme_ntp_background": {
-                    "type": "string",
-                    "optional": true
-                  }
-                }
+              "accentcolor": {
+                "type": "string",
+                "optional": true
+              },
+              "frame": {
+                "type": "array",
+                "items": {
+                  "type": "integer"
+                },
+                "optional": true
               },
-              "colors": {
-                "type": "object",
-                "optional": true,
-                "properties": {
-                  "accentcolor": {
-                    "type": "string",
-                    "optional": true
-                  },
-                  "frame": {
-                    "type": "array",
-                    "optional": true
-                  },
-                  "tab_text": {
-                    "type": "array",
-                    "optional": true
-                  },
-                  "textcolor": {
-                    "type": "string",
-                    "optional": true
-                  }
-                }
+              "tab_text": {
+                "type": "array",
+                "items": {
+                  "type": "integer"
+                },
+                "optional": true
+              },
+              "textcolor": {
+                "type": "string",
+                "optional": true
               }
             }
           }
         }
+      },
+      {
+        "$extend": "WebExtensionManifest",
+        "properties": {
+          "theme": {
+            "$ref": "ThemeType",
+            "optional": true
+          }
+        }
       }
     ]
   },
   {
     "namespace": "theme",
     "description": "The theme API allows customizing of visual elements of the browser.",
     "permissions": ["manifest:theme"],
     "functions": [
       {
-        "name": "dummyFunction",
+        "name": "update",
         "type": "function",
         "async": true,
-        "description": "Dummy function, necessary to register the 'theme' namespace."
+        "description": "Update the theme",
+        "parameters": [
+          {
+            "name": "details",
+            "$ref": "manifest.ThemeType",
+            "description": "Update the existing theme."
+          }
+        ]
       }
     ]
   }
 ]
--- a/browser/components/extensions/test/browser/browser_ext_theme_abouthomebackground.js
+++ b/browser/components/extensions/test/browser/browser_ext_theme_abouthomebackground.js
@@ -1,33 +1,55 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-const kBackground = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+const kBackgrounds = [
+  "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",
+  "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNOCwxMkwzLDcsNCw2bDQsNCw0LTQsMSwxWiIgZmlsbD0iIzZBNkE2QSIgLz4KPC9zdmc+Cg==",
+];
 
 add_task(function* testAboutHomeBackground() {
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", true);
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "theme": {
         "images": {
-          "theme_ntp_background": kBackground
+          "theme_ntp_background": kBackgrounds[0]
         }
       }
+    },
+    background() {
+      browser.test.onMessage.addListener((message, data) => {
+        if (message === "update-theme") {
+          browser.theme.update(data);
+          browser.test.sendMessage("theme-updated");
+        }
+      });
     }
   });
 
   yield extension.startup();
 
   let win = tab.linkedBrowser.contentWindow;
   let docEl = win.document.documentElement;
   let background = win.getComputedStyle(docEl).backgroundImage;
-  is(background, `url("` + kBackground + `")`, "Expected background image");
+  is(background, `url("` + kBackgrounds[0] + `")`, "Expected background image");
+
+  extension.sendMessage("update-theme", {
+    "images": {
+      "theme_ntp_background": kBackgrounds[1],
+    }
+  });
+
+  yield extension.awaitMessage("theme-updated");
+
+  background = win.getComputedStyle(docEl).backgroundImage;
+  is(background, `url("` + kBackgrounds[1] + `")`, "Expected background image");
 
   yield extension.unload();
 
   background = win.getComputedStyle(docEl).backgroundImage;
   is(background, "none", "The background image should be cleared when the extension is unloaded");
 
   yield BrowserTestUtils.removeTab(tab);
 });