Bug 1525762: Part 4 - Support automatic dark mode fallback for default theme again. r=aswan
authorKris Maglione <maglione.k@gmail.com>
Fri, 08 Feb 2019 15:36:04 -0800
changeset 525798 238bd73cdf0b759712c6b5ae8a73d77a930a0663
parent 525797 b9f524da2a6163d4c7cd10eae30e942fe59c5e07
child 525799 1b30a2c10d998efaf789db9cf66c14b5a965e13d
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaswan
bugs1525762
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1525762: Part 4 - Support automatic dark mode fallback for default theme again. r=aswan
toolkit/components/extensions/parent/ext-theme.js
toolkit/components/extensions/schemas/theme.json
toolkit/modules/LightweightThemeConsumer.jsm
toolkit/mozapps/extensions/LightweightThemeManager.jsm
toolkit/mozapps/extensions/default-theme/manifest.json
--- a/toolkit/components/extensions/parent/ext-theme.js
+++ b/toolkit/components/extensions/parent/ext-theme.js
@@ -33,24 +33,26 @@ let windowOverrides = new Map();
  */
 class Theme {
   /**
    * Creates a theme instance.
    *
    * @param {string} extension Extension that created the theme.
    * @param {Integer} windowId The windowId where the theme is applied.
    */
-  constructor({extension, details, windowId, experiment}) {
+  constructor({extension, details, darkDetails, windowId, experiment}) {
     this.extension = extension;
     this.details = details;
+    this.darkDetails = darkDetails;
     this.windowId = windowId;
 
     this.lwtStyles = {
       icons: {},
     };
+    this.lwtDarkStyles = null;
 
     if (experiment) {
       const canRunExperiment = AppConstants.MOZ_ALLOW_LEGACY_EXTENSIONS &&
         Services.prefs.getBoolPref("extensions.legacy.enabled");
       if (canRunExperiment) {
         this.lwtStyles.experimental = {
           colors: {},
           images: {},
@@ -73,101 +75,116 @@ class Theme {
   /**
    * 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() {
-    const {extension, details} = this;
-
-    if (details.colors) {
-      this.loadColors(details.colors);
-    }
-
-    if (details.images) {
-      this.loadImages(details.images);
+    this.loadDetails(this.details, this.lwtStyles);
+    if (this.darkDetails) {
+      this.lwtDarkStyles = {
+        icons: {},
+      };
+      this.loadDetails(this.darkDetails, this.lwtDarkStyles);
     }
 
-    if (details.icons) {
-      this.loadIcons(details.icons);
-    }
-
-    if (details.properties) {
-      this.loadProperties(details.properties);
-    }
-
-    this.loadMetadata(extension);
-
     let lwtData = {
       theme: this.lwtStyles,
+      darkTheme: this.lwtDarkStyles,
     };
 
+    if (this.experiment) {
+      lwtData.experiment = this.experiment;
+    }
+
     if (this.windowId) {
       lwtData.window =
         getWinUtils(windowTracker.getWindow(this.windowId)).outerWindowID;
       windowOverrides.set(this.windowId, this);
     } else {
       windowOverrides.clear();
       defaultTheme = this;
+      LightweightThemeManager.fallbackThemeData = lwtData;
     }
     onUpdatedEmitter.emit("theme-updated", this.details, this.windowId);
 
-    if (this.experiment) {
-      lwtData.experiment = this.experiment;
-    }
-    LightweightThemeManager.fallbackThemeData = this.lwtStyles;
     Services.obs.notifyObservers(null,
                                  "lightweight-theme-styling-update",
                                  JSON.stringify(lwtData));
   }
 
   /**
+   * @param {Object} details Details
+   * @param {Object} styles Styles object in which to store the colors.
+   */
+  loadDetails(details, styles) {
+    if (details.colors) {
+      this.loadColors(details.colors, styles);
+    }
+
+    if (details.images) {
+      this.loadImages(details.images, styles);
+    }
+
+    if (details.icons) {
+      this.loadIcons(details.icons, styles);
+    }
+
+    if (details.properties) {
+      this.loadProperties(details.properties, styles);
+    }
+
+    this.loadMetadata(this.extension, styles);
+  }
+
+  /**
    * Helper method for loading colors found in the extension's manifest.
    *
    * @param {Object} colors Dictionary mapping color properties to values.
+   * @param {Object} styles Styles object in which to store the colors.
    */
-  loadColors(colors) {
+  loadColors(colors, styles) {
     for (let color of Object.keys(colors)) {
       let val = colors[color];
 
       if (!val) {
         continue;
       }
 
       let cssColor = val;
       if (Array.isArray(val)) {
         cssColor = "rgb" + (val.length > 3 ? "a" : "") + "(" + val.join(",") + ")";
       }
 
       switch (color) {
         case "accentcolor":
         case "frame":
-          this.lwtStyles.accentcolor = cssColor;
+          styles.accentcolor = cssColor;
           break;
         case "frame_inactive":
-          this.lwtStyles.accentcolorInactive = cssColor;
+          styles.accentcolorInactive = cssColor;
           break;
         case "textcolor":
         case "tab_background_text":
-          this.lwtStyles.textcolor = cssColor;
+          styles.textcolor = cssColor;
           break;
         case "toolbar":
-          this.lwtStyles.toolbarColor = cssColor;
+          styles.toolbarColor = cssColor;
           break;
         case "toolbar_text":
         case "bookmark_text":
-          this.lwtStyles.toolbar_text = cssColor;
+          styles.toolbar_text = cssColor;
           break;
         case "icons":
-          this.lwtStyles.icon_color = cssColor;
+          styles.icon_color = cssColor;
           break;
         case "icons_attention":
-          this.lwtStyles.icon_attention_color = cssColor;
+          styles.icon_attention_color = cssColor;
           break;
         case "tab_background_separator":
         case "tab_loading":
         case "tab_text":
         case "tab_line":
         case "tab_selected":
         case "toolbar_field":
         case "toolbar_field_text":
@@ -190,75 +207,77 @@ class Theme {
         case "ntp_text":
         case "sidebar":
         case "sidebar_border":
         case "sidebar_text":
         case "sidebar_highlight":
         case "sidebar_highlight_text":
         case "toolbar_field_highlight":
         case "toolbar_field_highlight_text":
-          this.lwtStyles[color] = cssColor;
+          styles[color] = cssColor;
           break;
         default:
           if (this.experiment && this.experiment.colors && color in this.experiment.colors) {
-            this.lwtStyles.experimental.colors[color] = cssColor;
+            styles.experimental.colors[color] = cssColor;
           } else {
             const {logger} = this.extension;
             logger.warn(`Unrecognized theme property found: colors.${color}`);
           }
           break;
       }
     }
   }
 
   /**
    * Helper method for loading images found in the extension's manifest.
    *
    * @param {Object} images Dictionary mapping image properties to values.
+   * @param {Object} styles Styles object in which to store the colors.
    */
-  loadImages(images) {
+  loadImages(images, styles) {
     const {baseURI, logger} = this.extension;
 
     for (let image of Object.keys(images)) {
       let val = images[image];
 
       if (!val) {
         continue;
       }
 
       switch (image) {
         case "additional_backgrounds": {
           let backgroundImages = val.map(img => baseURI.resolve(img));
-          this.lwtStyles.additionalBackgrounds = backgroundImages;
+          styles.additionalBackgrounds = backgroundImages;
           break;
         }
         case "headerURL":
         case "theme_frame": {
           let resolvedURL = baseURI.resolve(val);
-          this.lwtStyles.headerURL = resolvedURL;
+          styles.headerURL = resolvedURL;
           break;
         }
         default: {
           if (this.experiment && this.experiment.images && image in this.experiment.images) {
-            this.lwtStyles.experimental.images[image] = baseURI.resolve(val);
+            styles.experimental.images[image] = baseURI.resolve(val);
           } else {
             logger.warn(`Unrecognized theme property found: images.${image}`);
           }
           break;
         }
       }
     }
   }
 
   /**
    * Helper method for loading icons found in the extension's manifest.
    *
    * @param {Object} icons Dictionary mapping icon properties to extension URLs.
+   * @param {Object} styles Styles object in which to store the colors.
    */
-  loadIcons(icons) {
+  loadIcons(icons, styles) {
     const {baseURI} = this.extension;
 
     if (!Services.prefs.getBoolPref("extensions.webextensions.themes.icons.enabled")) {
       // Return early if icons are disabled.
       return;
     }
 
     for (let icon of Object.getOwnPropertyNames(icons)) {
@@ -266,30 +285,31 @@ class Theme {
       // We also have to compare against the baseURI spec because
       // `val` might have been resolved already. Resolving "" against
       // the baseURI just produces that URI, so check for equality.
       if (!val || val == baseURI.spec || !ICONS.includes(icon)) {
         continue;
       }
       let variableName = `--${icon}-icon`;
       let resolvedURL = baseURI.resolve(val);
-      this.lwtStyles.icons[variableName] = resolvedURL;
+      styles.icons[variableName] = resolvedURL;
     }
   }
 
   /**
    * Helper method for preparing properties found in the extension's manifest.
    * Properties are commonly used to specify more advanced behavior of colors,
    * images or icons.
    *
    * @param {Object} properties Dictionary mapping properties to values.
+   * @param {Object} styles Styles object in which to store the colors.
    */
-  loadProperties(properties) {
-    let additionalBackgroundsCount = (this.lwtStyles.additionalBackgrounds &&
-      this.lwtStyles.additionalBackgrounds.length) || 0;
+  loadProperties(properties, styles) {
+    let additionalBackgroundsCount = (styles.additionalBackgrounds &&
+      styles.additionalBackgrounds.length) || 0;
     const assertValidAdditionalBackgrounds = (property, valueCount) => {
       const {logger} = this.extension;
       if (!additionalBackgroundsCount) {
         logger.warn(`The '${property}' property takes effect only when one ` +
           `or more additional background images are specified using the 'additional_backgrounds' property.`);
         return false;
       }
       if (additionalBackgroundsCount !== valueCount) {
@@ -308,86 +328,87 @@ class Theme {
       }
 
       switch (property) {
         case "additional_backgrounds_alignment": {
           if (!assertValidAdditionalBackgrounds(property, val.length)) {
             break;
           }
 
-          this.lwtStyles.backgroundsAlignment = val.join(",");
+          styles.backgroundsAlignment = val.join(",");
           break;
         }
         case "additional_backgrounds_tiling": {
           if (!assertValidAdditionalBackgrounds(property, val.length)) {
             break;
           }
 
           let tiling = [];
-          for (let i = 0, l = this.lwtStyles.additionalBackgrounds.length; i < l; ++i) {
+          for (let i = 0, l = styles.additionalBackgrounds.length; i < l; ++i) {
             tiling.push(val[i] || "no-repeat");
           }
-          this.lwtStyles.backgroundsTiling = tiling.join(",");
+          styles.backgroundsTiling = tiling.join(",");
           break;
         }
         default: {
           if (this.experiment && this.experiment.properties && property in this.experiment.properties) {
-            this.lwtStyles.experimental.properties[property] = val;
+            styles.experimental.properties[property] = val;
           } else {
             const {logger} = this.extension;
             logger.warn(`Unrecognized theme property found: properties.${property}`);
           }
           break;
         }
       }
     }
   }
 
   /**
    * Helper method for loading extension metadata required by downstream
    * consumers.
    *
    * @param {Object} extension Extension object.
+   * @param {Object} styles Styles object in which to store the colors.
    */
-  loadMetadata(extension) {
-    this.lwtStyles.id = extension.id;
-    this.lwtStyles.version = extension.version;
+  loadMetadata(extension, styles) {
+    styles.id = extension.id;
+    styles.version = extension.version;
   }
 
   static unload(windowId) {
     let lwtData = {
       theme: null,
     };
 
     if (windowId) {
       lwtData.window = getWinUtils(windowTracker.getWindow(windowId)).outerWindowID;
       windowOverrides.set(windowId, emptyTheme);
     } else {
       windowOverrides.clear();
       defaultTheme = emptyTheme;
+      LightweightThemeManager.fallbackThemeData = null;
     }
     onUpdatedEmitter.emit("theme-updated", {}, windowId);
 
-    LightweightThemeManager.fallbackThemeData = null;
     Services.obs.notifyObservers(null,
                                  "lightweight-theme-styling-update",
                                  JSON.stringify(lwtData));
   }
 }
 
 this.theme = class extends ExtensionAPI {
   onManifestEntry(entryName) {
     let {extension} = this;
     let {manifest} = extension;
-    let {theme, theme_experiment} = manifest;
 
     defaultTheme = new Theme({
       extension,
-      details: theme,
-      experiment: theme_experiment,
+      details: manifest.theme,
+      darkDetails: manifest.dark_theme,
+      experiment: manifest.theme_experiment,
     });
   }
 
   onShutdown(reason) {
     if (reason === "APP_SHUTDOWN") {
       return;
     }
 
--- a/toolkit/components/extensions/schemas/theme.json
+++ b/toolkit/components/extensions/schemas/theme.json
@@ -636,16 +636,20 @@
         "id": "ThemeManifest",
         "type": "object",
         "description": "Contents of manifest.json for a static theme",
         "$import": "manifest.ManifestBase",
         "properties": {
           "theme": {
             "$ref": "ThemeType"
           },
+          "dark_theme": {
+            "$ref": "ThemeType",
+            "optional": true
+          },
           "default_locale": {
             "type": "string",
             "optional": true
           },
           "theme_experiment": {
             "$ref": "ThemeExperiment",
             "optional": true
           },
--- a/toolkit/modules/LightweightThemeConsumer.jsm
+++ b/toolkit/modules/LightweightThemeConsumer.jsm
@@ -126,95 +126,95 @@ const toolkitVariableMap = [
 
 function LightweightThemeConsumer(aDocument) {
   this._doc = aDocument;
   this._win = aDocument.defaultView;
   this._winId = this._win.windowUtils.outerWindowID;
 
   Services.obs.addObserver(this, "lightweight-theme-styling-update");
 
-  ChromeUtils.import("resource://gre/modules/LightweightThemeManager.jsm", this);
-
   // We're responsible for notifying LightweightThemeManager when the OS is in
   // dark mode so it can activate the dark theme. We don't want this on Linux
   // as the default theme picks up the right colors from dark GTK themes.
   if (AppConstants.platform != "linux") {
-    this._darkThemeMediaQuery = this._win.matchMedia("(-moz-system-dark-theme)");
-    this._darkThemeMediaQuery.addListener(this.LightweightThemeManager);
-    this.LightweightThemeManager.systemThemeChanged(this._darkThemeMediaQuery);
+    this.darkThemeMediaQuery = this._win.matchMedia("(-moz-system-dark-theme)");
+    this.darkThemeMediaQuery.addListener(this);
   }
 
-  this._update(this.LightweightThemeManager.currentThemeWithPersistedData);
+  const {LightweightThemeManager} = ChromeUtils.import("resource://gre/modules/LightweightThemeManager.jsm");
+  this._update(LightweightThemeManager.themeData);
 
   this._win.addEventListener("resolutionchange", this);
   this._win.addEventListener("unload", this, { once: true });
 }
 
 LightweightThemeConsumer.prototype = {
   _lastData: null,
-  // Whether a lightweight theme is enabled.
-  _active: false,
 
   observe(aSubject, aTopic, aData) {
     if (aTopic != "lightweight-theme-styling-update")
       return;
 
     let parsedData = JSON.parse(aData);
     if (!parsedData) {
       parsedData = { theme: null, experiment: null };
     }
 
     if (parsedData.window && parsedData.window !== this._winId) {
       return;
     }
 
-    this._update(parsedData.theme, parsedData.experiment);
+    this._update(parsedData, parsedData.experiment);
   },
 
   handleEvent(aEvent) {
+    if (aEvent.media == "(-moz-system-dark-theme)") {
+      this._update(this._lastData);
+      return;
+    }
+
     switch (aEvent.type) {
       case "resolutionchange":
-        if (this._active) {
-          this._update(this._lastData);
-        }
+        this._update(this._lastData);
         break;
       case "unload":
         Services.obs.removeObserver(this, "lightweight-theme-styling-update");
         Services.ppmm.sharedData.delete(`theme/${this._winId}`);
         this._win.removeEventListener("resolutionchange", this);
         this._win = this._doc = null;
-        if (this._darkThemeMediaQuery) {
-          this._darkThemeMediaQuery.removeListener(this.LightweightThemeManager);
-          this._darkThemeMediaQuery = null;
+        if (this.darkThemeMediaQuery) {
+          this.darkThemeMediaQuery.removeListener(this);
+          this.darkThemeMediaQuery = null;
         }
         break;
     }
   },
 
-  _update(theme, experiment) {
-    this._lastData = theme;
-    if (theme) {
-      theme = LightweightThemeImageOptimizer.optimize(theme, this._win.screen);
+  get darkMode() {
+    return this.darkThemeMediaQuery && this.darkThemeMediaQuery.matches;
+  },
+
+  _update(themeData, experiment) {
+    this._lastData = themeData;
+
+    let theme = themeData.theme;
+    if (themeData.darkTheme && this.darkMode) {
+      theme = themeData.darkTheme;
     }
     if (!theme) {
       theme = { id: DEFAULT_THEME_ID };
     }
 
-    let active = this._active = (theme.id != DEFAULT_THEME_ID);
+    if (theme) {
+      theme = LightweightThemeImageOptimizer.optimize(theme, this._win.screen);
+    } else {
+      theme = {};
+    }
 
-    // The theme we're switching to can be different from the user-selected
-    // theme. E.g. if the default theme is selected and the OS is in dark mode,
-    // we'd silently activate the dark theme if available. We set an attribute
-    // in that case so stylesheets can differentiate this from the dark theme
-    // being selected explicitly by the user.
-    let isDefaultThemeInDarkMode =
-      theme.id == this.LightweightThemeManager.defaultDarkThemeID &&
-      this.LightweightThemeManager.selectedThemeID == DEFAULT_THEME_ID &&
-      this._darkThemeMediaQuery &&
-      this._darkThemeMediaQuery.matches;
+    let active = this._active = Object.keys(theme).length;
 
     let root = this._doc.documentElement;
 
     if (active && theme.headerURL) {
       root.setAttribute("lwtheme-image", "true");
     } else {
       root.removeAttribute("lwtheme-image");
     }
@@ -231,23 +231,23 @@ LightweightThemeConsumer.prototype = {
       _setImage(root, active, `--${icon}-icon`, value);
     }
 
     this._setExperiment(active, experiment, theme.experimental);
     _setImage(root, active, "--lwt-header-image", theme.headerURL);
     _setImage(root, active, "--lwt-additional-images", theme.additionalBackgrounds);
     _setProperties(root, active, theme);
 
-    if (active) {
+    if (theme.id != DEFAULT_THEME_ID || this.darkMode) {
       root.setAttribute("lwtheme", "true");
     } else {
       root.removeAttribute("lwtheme");
       root.removeAttribute("lwthemetextcolor");
     }
-    if (isDefaultThemeInDarkMode) {
+    if (theme.id == DEFAULT_THEME_ID && this.darkMode) {
       root.setAttribute("lwt-default-theme-in-dark-mode", "true");
     } else {
       root.removeAttribute("lwt-default-theme-in-dark-mode");
     }
 
     let contentThemeData = _getContentProperties(this._doc, active, theme);
     Services.ppmm.sharedData.set(`theme/${this._winId}`, contentThemeData);
   },
--- a/toolkit/mozapps/extensions/LightweightThemeManager.jsm
+++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm
@@ -16,37 +16,25 @@ var _fallbackThemeData = null;
 var LightweightThemeManager = {
   set fallbackThemeData(data) {
     if (data && Object.getOwnPropertyNames(data).length) {
       _fallbackThemeData = Object.assign({}, data);
       LightweightThemeImageOptimizer.purge();
     } else {
       _fallbackThemeData = null;
     }
-    return _fallbackThemeData;
   },
 
   /*
    * Returns the currently active theme, taking the fallback theme into account
    * if we'd be using the default theme otherwise.
    *
    * This will always return the original theme data and not make use of
    * locally persisted resources.
    */
   get currentThemeWithFallback() {
-    return _fallbackThemeData;
-  },
-
-  systemThemeChanged() {
+    return _fallbackThemeData && _fallbackThemeData.theme;
   },
 
-  /**
-   * Handles system theme changes.
-   *
-   * @param  aEvent
-   *         The MediaQueryListEvent associated with the system theme change.
-   */
-  handleEvent(aEvent) {
-    if (aEvent.media == "(-moz-system-dark-theme)") {
-      // Meh.
-    }
+  get themeData() {
+    return _fallbackThemeData || {theme: null};
   },
 };
--- a/toolkit/mozapps/extensions/default-theme/manifest.json
+++ b/toolkit/mozapps/extensions/default-theme/manifest.json
@@ -10,10 +10,33 @@
   "name": "Default",
   "description": "A theme with the operating system color scheme.",
   "author": "Mozilla",
   "version": "1.0",
 
   "icons": {"32": "icon.svg"},
 
   "theme": {
+  },
+
+  "dark_theme": {
+    "colors": {
+      "tab_background_text": "rgb(249, 249, 250)",
+      "icons": "rgb(249, 249, 250, 0.7)",
+      "frame": "hsl(240, 5%, 5%)",
+      "popup": "#4a4a4f",
+      "popup_text": "rgb(249, 249, 250)",
+      "popup_border": "#27272b",
+      "tab_line": "#0a84ff",
+      "toolbar": "hsl(240, 1%, 20%)",
+      "toolbar_bottom_separator": "hsl(240, 5%, 5%)",
+      "toolbar_field": "rgb(71, 71, 73)",
+      "toolbar_field_border": "rgba(249, 249, 250, 0.2)",
+      "toolbar_field_separator": "#5F6670",
+      "toolbar_field_text": "rgb(249, 249, 250)",
+      "ntp_background": "#2A2A2E",
+      "ntp_text": "rgb(249, 249, 250)",
+      "sidebar": "#38383D",
+      "sidebar_text": "rgb(249, 249, 250)",
+      "sidebar_border": "rgba(255, 255, 255, 0.1)"
+    }
   }
 }