toolkit/modules/LightweightThemeConsumer.jsm
author Sebastian Hengst <archaeopteryx@coole-files.de>
Sat, 03 Feb 2018 02:24:11 +0200
changeset 457294 f53bac4e3d94b4a954dd3d05b82bb0bb4cba0896
parent 457290 111e574ef42a2b0fc4664d1606acb13112f4197c
child 457344 749425381bbad8f39af8f7c8053bd23600d09b2a
permissions -rw-r--r--
Backed out changeset 1687250b0dd9 (bug 1431189) for eslint failures in toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_colors.js

/* This Source Code Form is subject to the terms of the Mozilla Public
 * 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/. */

this.EXPORTED_SYMBOLS = ["LightweightThemeConsumer"];

const {utils: Cu, interfaces: Ci, classes: Cc} = Components;

ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/AppConstants.jsm");

ChromeUtils.defineModuleGetter(this, "LightweightThemeImageOptimizer",
  "resource://gre/modules/addons/LightweightThemeImageOptimizer.jsm");

const kCSSVarsMap = new Map([
  ["--lwt-accent-color-inactive", "accentcolorInactive"],
  ["--lwt-background-alignment", "backgroundsAlignment"],
  ["--lwt-background-tiling", "backgroundsTiling"],
  ["--tab-loading-fill", "tab_loading"],
  ["--lwt-tab-text", "tab_text"],
  ["--toolbar-bgcolor", "toolbarColor"],
  ["--toolbar-color", "toolbar_text"],
  ["--url-and-searchbar-background-color", "toolbar_field"],
  ["--url-and-searchbar-color", "toolbar_field_text"],
  ["--lwt-toolbar-field-border-color", "toolbar_field_border"],
  ["--urlbar-separator-color", "toolbar_field_separator"],
  ["--tabs-border-color", "toolbar_top_separator"],
  ["--lwt-toolbar-vertical-separator", "toolbar_vertical_separator"],
  ["--toolbox-border-bottom-color", "toolbar_bottom_separator"],
  ["--lwt-toolbarbutton-icon-fill", "icon_color"],
  ["--lwt-toolbarbutton-icon-fill-attention", "icon_attention_color"],
]);

this.LightweightThemeConsumer =
 function LightweightThemeConsumer(aDocument) {
  this._doc = aDocument;
  this._win = aDocument.defaultView;

  let screen = this._win.screen;
  this._lastScreenWidth = screen.width;
  this._lastScreenHeight = screen.height;

  Services.obs.addObserver(this, "lightweight-theme-styling-update");

  var temp = {};
  ChromeUtils.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
  this._update(temp.LightweightThemeManager.currentThemeForDisplay);
  this._win.addEventListener("resize", this);
};

LightweightThemeConsumer.prototype = {
  _lastData: null,
  _lastScreenWidth: null,
  _lastScreenHeight: null,
  // Whether the active lightweight theme should be shown on the window.
  _enabled: true,
  // Whether a lightweight theme is enabled.
  _active: false,

  enable() {
    this._enabled = true;
    this._update(this._lastData);
  },

  disable() {
    // Dance to keep the data, but reset the applied styles:
    let lastData = this._lastData;
    this._update(null);
    this._enabled = false;
    this._lastData = lastData;
  },

  getData() {
    return this._enabled ? Cu.cloneInto(this._lastData, this._win) : null;
  },

  observe(aSubject, aTopic, aData) {
    if (aTopic != "lightweight-theme-styling-update")
      return;

    const { outerWindowID } = this._win
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIDOMWindowUtils);

    const parsedData = JSON.parse(aData);
    if (parsedData && parsedData.window && parsedData.window !== outerWindowID) {
      return;
    }

    this._update(parsedData);
  },

  handleEvent(aEvent) {
    let {width, height} = this._win.screen;

    if (this._lastScreenWidth != width || this._lastScreenHeight != height) {
      this._lastScreenWidth = width;
      this._lastScreenHeight = height;
      if (!this._active)
        return;
      this._update(this._lastData);
      Services.obs.notifyObservers(this._win, "lightweight-theme-optimized",
                                   JSON.stringify(this._lastData));
    }
  },

  destroy() {
    Services.obs.removeObserver(this, "lightweight-theme-styling-update");

    this._win.removeEventListener("resize", this);

    this._win = this._doc = null;
  },

  _update(aData) {
    if (!aData) {
      aData = { headerURL: "", footerURL: "", textcolor: "", accentcolor: "" };
      this._lastData = aData;
    } else {
      this._lastData = aData;
      aData = LightweightThemeImageOptimizer.optimize(aData, this._win.screen);
    }
    if (!this._enabled)
      return;

    let root = this._doc.documentElement;
    let active = !!aData.headerURL;

    // We need to clear these either way: either because the theme is being removed,
    // or because we are applying a new theme and the data might be bogus CSS,
    // so if we don't reset first, it'll keep the old value.
    root.style.removeProperty("--lwt-text-color");
    root.style.removeProperty("--lwt-accent-color");
    let textcolor = aData.textcolor || "black";
    _setProperty(root, active, "--lwt-text-color", textcolor);
    _setProperty(root, active, "--lwt-accent-color", this._sanitizeCSSColor(aData.accentcolor) || "white");

    if (active) {
      let dummy = this._doc.createElement("dummy");
      dummy.style.color = textcolor;
      let [r, g, b] = _parseRGB(this._win.getComputedStyle(dummy).color);
      let luminance = 0.2125 * r + 0.7154 * g + 0.0721 * b;
      root.setAttribute("lwthemetextcolor", luminance <= 110 ? "dark" : "bright");
      root.setAttribute("lwtheme", "true");
    } else {
      root.removeAttribute("lwthemetextcolor");
      root.removeAttribute("lwtheme");
    }

    this._active = active;

    if (aData.icons) {
      let activeIcons = active ? Object.keys(aData.icons).join(" ") : "";
      root.setAttribute("lwthemeicons", activeIcons);
      for (let [name, value] of Object.entries(aData.icons)) {
        _setImage(root, active, name, value);
      }
    } else {
      root.removeAttribute("lwthemeicons");
    }

    _setImage(root, active, "--lwt-header-image", aData.headerURL);
    _setImage(root, active, "--lwt-footer-image", aData.footerURL);
    _setImage(root, active, "--lwt-additional-images", aData.additionalBackgrounds);
    _setProperties(root, active, aData);

    if (active && aData.footerURL)
      root.setAttribute("lwthemefooter", "true");
    else
      root.removeAttribute("lwthemefooter");

    Services.obs.notifyObservers(this._win, "lightweight-theme-window-updated",
                                 JSON.stringify(aData));
  },

  _sanitizeCSSColor(cssColor) {
    // style.color normalizes color values and rejects invalid ones, so a
    // simple round trip gets us a sanitized color value.
    let span = this._doc.createElementNS("http://www.w3.org/1999/xhtml", "span");
    span.style.color = cssColor;
    cssColor = this._win.getComputedStyle(span).color;
    if (cssColor == "rgba(0, 0, 0, 0)" ||
        !cssColor) {
      return "";
    }
    // Remove alpha channel from color
    return `rgb(${_parseRGB(cssColor).join(", ")})`;
  }
};

function _setImage(aRoot, aActive, aVariableName, aURLs) {
  if (aURLs && !Array.isArray(aURLs)) {
    aURLs = [aURLs];
  }
  _setProperty(aRoot, aActive, aVariableName, aURLs && aURLs.map(v => `url("${v.replace(/"/g, '\\"')}")`).join(","));
}

function _setProperty(root, active, variableName, value) {
  if (active && value) {
    root.style.setProperty(variableName, value);
  } else {
    root.style.removeProperty(variableName);
  }
}

function _setProperties(root, active, vars) {
  for (let [cssVarName, varsKey] of kCSSVarsMap) {
    _setProperty(root, active, cssVarName, vars[varsKey]);
  }
}

function _parseRGB(aColorString) {
  var rgb = aColorString.match(/^rgba?\((\d+), (\d+), (\d+)/);
  rgb.shift();
  return rgb.map(x => parseInt(x));
}