- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 51 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 99 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 68 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 99 %
- : 68 %
- : 99 %
- : 99 %
- : 99 %
Source code
Revision control
Copy as Markdown
Other Tools
/* 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/. */
"use strict";
/* global windowTracker, EventManager, EventEmitter */
/* eslint-disable complexity */
ChromeUtils.defineESModuleGetters(this, {
LightweightThemeManager:
"resource://gre/modules/LightweightThemeManager.sys.mjs",
});
const onUpdatedEmitter = new EventEmitter();
// Represents an empty theme for convenience of use
const emptyTheme = {
details: { colors: null, images: null, properties: null },
};
let defaultTheme = emptyTheme;
// Map[BrowserWindow -> Theme instance]
let windowOverrides = new WeakMap();
/**
* Class representing either a global theme affecting all windows or an override on a specific window.
* Any extension updating the theme with a new global theme will replace the singleton defaultTheme.
*/
class Theme {
/**
* Creates a theme instance.
*
* @param {object} options
* @param {string} options.extension Extension that created the theme.
* @param {Integer} options.windowId The windowId where the theme is applied.
* @param {object} options.details
* @param {object} options.darkDetails
* @param {object} options.experiment
* @param {object} options.startupData startupData if this is a static theme.
*/
constructor({
extension,
details,
darkDetails,
windowId,
experiment,
startupData,
}) {
this.extension = extension;
this.details = details;
this.darkDetails = darkDetails;
this.windowId = windowId;
if (startupData?.lwtData) {
// Parsed theme from a previous load() already available in startupData
// of parsed theme. We assume that reparsing the theme will yield the same
// result, and therefore reuse the value of startupData. This is a minor
// optimization; the more important use of startupData is before startup,
// by Extension.sys.mjs for LightweightThemeManager.fallbackThemeData.
//
// Note: the assumption "yield the same result" is not obviously true: the
// startupData persists across application updates, so it is possible for
// a browser update to occur that interprets the static theme differently.
// In this case we would still be using the old interpretation instead of
// the new one, until the user disables and re-enables/installs the theme.
this.lwtData = startupData.lwtData;
this.experiment = startupData.lwtData.experiment;
} else {
// lwtData will be populated by load().
this.lwtData = null;
this.experiment = null;
if (experiment) {
if (extension.canUseThemeExperiment()) {
this.experiment = experiment;
} else {
const { logger } = this.extension;
logger.warn("This extension is not allowed to run theme experiments");
return;
}
}
}
this.load();
}
/**
* Loads a theme by reading the properties from the extension's manifest.
* This method will override any currently applied theme.
*/
load() {
// this.lwtData is usually null, unless populated from startupData.
if (!this.lwtData) {
this.lwtData = LightweightThemeManager.themeDataFrom(
this.details,
this.darkDetails,
this.experiment,
this.extension.baseURI,
this.extension.id,
this.extension.version,
this.extension.logger
);
if (this.extension.type === "theme") {
// Store the parsed theme in startupData, so it is available early at
// browser startup, to use as LightweightThemeManager.fallbackThemeData,
// which is assigned from Extension.sys.mjs to avoid having to wait for
// this ext-theme.js file to be loaded.
this.extension.startupData = {
lwtData: this.lwtData,
};
this.extension.saveStartupData();
}
}
if (this.windowId) {
let browserWindow = windowTracker.getWindow(this.windowId);
this.lwtData.window = browserWindow.docShell.outerWindowID;
windowOverrides.set(browserWindow, this);
} else {
windowOverrides = new WeakMap();
defaultTheme = this;
LightweightThemeManager.fallbackThemeData = this.lwtData;
}
onUpdatedEmitter.emit("theme-updated", this.details, this.windowId);
Services.obs.notifyObservers(
this.lwtData,
"lightweight-theme-styling-update"
);
}
static unload(browserWindow) {
let lwtData = {
theme: null,
};
if (browserWindow) {
lwtData.window = browserWindow.docShell?.outerWindowID;
windowOverrides.delete(browserWindow);
onUpdatedEmitter.emit(
"theme-updated",
{},
windowTracker.getId(browserWindow)
);
} else {
windowOverrides = new WeakMap();
defaultTheme = emptyTheme;
LightweightThemeManager.fallbackThemeData = null;
onUpdatedEmitter.emit("theme-updated", {});
}
Services.obs.notifyObservers(lwtData, "lightweight-theme-styling-update");
}
}
this.theme = class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
onUpdated({ fire, context }) {
let callback = (event, theme, windowId) => {
if (windowId) {
// Force access validation for incognito mode by getting the window.
if (windowTracker.getWindow(windowId, context, false)) {
fire.async({ theme, windowId });
}
} else {
fire.async({ theme });
}
};
onUpdatedEmitter.on("theme-updated", callback);
return {
unregister() {
onUpdatedEmitter.off("theme-updated", callback);
},
convert(_fire, _context) {
fire = _fire;
context = _context;
},
};
},
};
onManifestEntry() {
let { extension } = this;
let { manifest } = extension;
// Note: only static themes are processed here; extensions with the "theme"
// permission do not enter this code path.
defaultTheme = new Theme({
extension,
details: manifest.theme,
darkDetails: manifest.dark_theme,
experiment: manifest.theme_experiment,
startupData: extension.startupData,
});
}
onShutdown(isAppShutdown) {
if (isAppShutdown) {
return;
}
let { extension } = this;
for (let browserWindow of ChromeUtils.nondeterministicGetWeakMapKeys(
windowOverrides
)) {
let theme = windowOverrides.get(browserWindow);
if (theme.extension === extension) {
Theme.unload(browserWindow);
}
}
if (defaultTheme.extension === extension) {
Theme.unload();
}
}
getAPI(context) {
let { extension } = context;
return {
theme: {
getCurrent: windowId => {
// Take last focused window when no ID is supplied.
if (!windowId) {
windowId = windowTracker.getId(windowTracker.topWindow);
}
const browserWindow = windowTracker.getWindow(windowId, context);
if (windowOverrides.has(browserWindow)) {
return Promise.resolve(windowOverrides.get(browserWindow).details);
}
return Promise.resolve(defaultTheme.details);
},
update: (windowId, details) => {
if (windowId) {
const browserWindow = windowTracker.getWindow(windowId, context);
if (!browserWindow) {
return Promise.reject(`Invalid window ID: ${windowId}`);
}
}
new Theme({
extension,
details,
windowId,
experiment: this.extension.manifest.theme_experiment,
});
},
reset: windowId => {
if (windowId) {
const browserWindow = windowTracker.getWindow(windowId, context);
const theme = windowOverrides.get(browserWindow) || defaultTheme;
if (theme.extension === extension) {
Theme.unload(browserWindow);
}
return;
}
if (defaultTheme.extension === extension) {
Theme.unload();
}
},
onUpdated: new EventManager({
context,
module: "theme",
event: "onUpdated",
extensionApi: this,
}).api(),
},
};
}
};