Adds some new icon variants, embedding them as PE resources, and registers them (with their about:settings metadata) in the CustomIconManager catalog. Differential Revision: https://phabricator.services.mozilla.com/D308650
474 lines
16 KiB
JavaScript
474 lines
16 KiB
JavaScript
/* 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/. */
|
|
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const PREF_ICON_ID = "browser.shell.customIcon.id";
|
|
|
|
/**
|
|
* Inlined catalog of selectable icons. Each entry has:
|
|
*
|
|
* iconResourceId: the Win32 resource ID of an icon embedded in firefox.exe
|
|
* at build time (declared in toolkit/xre/nsNativeAppSupportWin.h and
|
|
* browser/app/splash.rc). This is what gets applied: shortcuts reference
|
|
* it as firefox.exe,-<iconResourceId> and live windows load it directly.
|
|
* The catalog-id -> resource-id mapping is ABI: never remap or reuse an id
|
|
* once it has shipped, even if the icon is retired from the picker.
|
|
* preview: a chrome:// URI resolving to the same .ico shipped in omni.ja
|
|
* (see browser/components/shell/jar.mn). Used only to render a thumbnail in
|
|
* the about:settings picker; never used to apply the icon. PE resources are
|
|
* not addressable by a URL, hence the separate display asset.
|
|
* l10nId: Fluent id for the icon's about:settings label.
|
|
* gated: if true, the icon is a "Bonus" icon, offered only once the browser is
|
|
* both the default browser and pinned to the taskbar. This is purely an
|
|
* about:settings policy (the UI disables the option); CustomIconManager
|
|
* itself does not enforce it.
|
|
*
|
|
* Theme-aware icons (e.g. Minimal, a monochrome silhouette that's only legible
|
|
* against a matching background) instead carry a `variants` object keyed by
|
|
* color scheme ("dark"/"light"), each with its own iconResourceId + preview.
|
|
* `iconResourceId`/`preview` are read through resolveResourceId()/resolvePreview()
|
|
* so callers don't special-case theme-aware entries: the applied resource is
|
|
* chosen by the OS theme (the taskbar background), while the about:settings
|
|
* preview is chosen by the document's own color scheme (the surface it renders
|
|
* on). These differ only when the browser theme is overridden away from the OS.
|
|
*/
|
|
export const ICON_CATALOG = Object.freeze({
|
|
default: Object.freeze({
|
|
// The executable's own icon. "default" is the no-override state, selected by
|
|
// reverting (clearing the pref), so this resource id is never applied
|
|
// directly; the entry exists to give the picker a label and a preview. The
|
|
// preview is icon64 (rather than icon32) so it stays crisp on hi-dpi.
|
|
iconResourceId: 0,
|
|
preview: "chrome://branding/content/icon64.png",
|
|
l10nId: "appearance-browser-icon-default",
|
|
}),
|
|
retro2004: Object.freeze({
|
|
iconResourceId: 1100, // IDI_CUSTOM_RETRO2004
|
|
preview: "chrome://browser/content/icons/retro2004.ico",
|
|
l10nId: "appearance-browser-icon-retro2004",
|
|
}),
|
|
retro2017: Object.freeze({
|
|
iconResourceId: 1101, // IDI_CUSTOM_RETRO2017
|
|
preview: "chrome://browser/content/icons/retro2017.ico",
|
|
l10nId: "appearance-browser-icon-retro2017",
|
|
}),
|
|
pride: Object.freeze({
|
|
iconResourceId: 1106, // IDI_CUSTOM_PRIDE
|
|
preview: "chrome://browser/content/icons/pride.ico",
|
|
l10nId: "appearance-browser-icon-pride",
|
|
}),
|
|
minimal: Object.freeze({
|
|
l10nId: "appearance-browser-icon-minimal",
|
|
variants: Object.freeze({
|
|
dark: Object.freeze({
|
|
iconResourceId: 1102, // IDI_CUSTOM_MINIMAL_DARK
|
|
preview: "chrome://browser/content/icons/minimal-dark.ico",
|
|
}),
|
|
light: Object.freeze({
|
|
iconResourceId: 1103, // IDI_CUSTOM_MINIMAL_LIGHT
|
|
preview: "chrome://browser/content/icons/minimal-light.ico",
|
|
}),
|
|
}),
|
|
}),
|
|
kit: Object.freeze({
|
|
iconResourceId: 1107, // IDI_CUSTOM_KIT
|
|
preview: "chrome://browser/content/icons/kit.ico",
|
|
l10nId: "appearance-browser-icon-kit",
|
|
gated: true,
|
|
}),
|
|
pixelated: Object.freeze({
|
|
iconResourceId: 1104, // IDI_CUSTOM_PIXELATED
|
|
preview: "chrome://browser/content/icons/pixelated.ico",
|
|
l10nId: "appearance-browser-icon-pixelated",
|
|
gated: true,
|
|
}),
|
|
momo: Object.freeze({
|
|
iconResourceId: 1105, // IDI_CUSTOM_MOMO
|
|
preview: "chrome://browser/content/icons/momo.ico",
|
|
l10nId: "appearance-browser-icon-momo",
|
|
gated: true,
|
|
}),
|
|
});
|
|
|
|
/**
|
|
* Resolve a catalog entry's variant for a color scheme. Flat (theme-agnostic)
|
|
* entries are returned as-is; theme-aware entries return their dark/light
|
|
* variant.
|
|
*
|
|
* @param {object} entry A catalog entry.
|
|
* @param {"dark"|"light"} scheme
|
|
* @returns {object} An object with iconResourceId + preview.
|
|
*/
|
|
function resolveVariant(entry, scheme) {
|
|
return entry.variants ? entry.variants[scheme] : entry;
|
|
}
|
|
|
|
/**
|
|
* The embedded resource ID to apply for an entry, given the OS color scheme.
|
|
* Exported for tests; the manager itself resolves against the OS taskbar theme.
|
|
*
|
|
* @param {object} entry A catalog entry.
|
|
* @param {"dark"|"light"} scheme The OS color scheme.
|
|
* @returns {number}
|
|
*/
|
|
export function resolveResourceId(entry, scheme) {
|
|
return resolveVariant(entry, scheme).iconResourceId;
|
|
}
|
|
|
|
/**
|
|
* The preview URI to display for an entry, given a color scheme. Exported for
|
|
* the about:settings picker, which resolves against the document's own color
|
|
* scheme rather than the OS theme.
|
|
*
|
|
* @param {object} entry A catalog entry.
|
|
* @param {"dark"|"light"} scheme
|
|
* @returns {string}
|
|
*/
|
|
export function resolvePreview(entry, scheme) {
|
|
return resolveVariant(entry, scheme).preview;
|
|
}
|
|
|
|
const lazy = {};
|
|
|
|
XPCOMUtils.defineLazyServiceGetters(lazy, {
|
|
ShellService: [
|
|
"@mozilla.org/browser/shell-service;1",
|
|
Ci.nsIWindowsShellService,
|
|
],
|
|
WinTaskbar: ["@mozilla.org/windows-taskbar;1", Ci.nsIWinTaskbar],
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
|
|
return console.createInstance({
|
|
prefix: "CustomIconManager",
|
|
maxLogLevel: Services.prefs.getBoolPref(
|
|
"browser.shell.customIcon.log",
|
|
false
|
|
)
|
|
? "Debug"
|
|
: "Warn",
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Absolute path to the currently running browser executable. Custom icons are
|
|
* embedded as resources in this executable, so it is both the icon source for
|
|
* applying a custom icon and (with index 0) the default-icon source when
|
|
* reverting Windows shortcuts.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
function browserExePath() {
|
|
return Services.dirsvc.get("XREExeF", Ci.nsIFile).path;
|
|
}
|
|
|
|
/**
|
|
* Apply the given executable icon to every Windows .lnk file that this install
|
|
* owns (taskbar pin, per-user Desktop, per-user Start Menu).
|
|
*
|
|
* Per-shortcut failures are logged but do not abort the rest of the
|
|
* iteration; partial success is permitted.
|
|
*
|
|
* @param {string} iconPath Absolute path to the icon source (the executable).
|
|
* @param {number} iconResourceId Resource ID of the icon within iconPath (e.g.
|
|
* 1100 for IDI_CUSTOM_RETRO2004), or 0 for the executable's default icon.
|
|
* setShortcutsIcon owns the Win32 encoding of this reference.
|
|
* @returns {Promise<boolean>} True if at least one shortcut was updated.
|
|
*/
|
|
async function applyIconToWindowsShortcuts(iconPath, iconResourceId) {
|
|
let aumid = lazy.WinTaskbar.defaultGroupId;
|
|
let shortcuts = [];
|
|
try {
|
|
shortcuts = await lazy.ShellService.enumerateInstallShortcuts(aumid);
|
|
} catch (ex) {
|
|
lazy.logConsole.error("enumerateInstallShortcuts failed", ex);
|
|
return false;
|
|
}
|
|
|
|
lazy.logConsole.debug(
|
|
`enumerateInstallShortcuts(${aumid}) matched ${shortcuts.length} ` +
|
|
`shortcut(s): ${shortcuts.join(", ")}`
|
|
);
|
|
if (!shortcuts.length) {
|
|
lazy.logConsole.warn(
|
|
`No shortcuts matched this install (AUMID ${aumid}); nothing to update. ` +
|
|
`Only shortcuts created by Firefox carry this AUMID - hand-made ` +
|
|
`Explorer shortcuts are not modified.`
|
|
);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await lazy.ShellService.setShortcutsIcon(
|
|
shortcuts,
|
|
iconPath,
|
|
iconResourceId
|
|
);
|
|
} catch (ex) {
|
|
if (ex.result == Cr.NS_ERROR_NOT_AVAILABLE) {
|
|
lazy.logConsole.error("Could not update any shortcut icons.");
|
|
return false;
|
|
}
|
|
lazy.logConsole.error(
|
|
"Fatal error while attempting to update short icons:",
|
|
ex
|
|
);
|
|
return false;
|
|
}
|
|
|
|
lazy.logConsole.debug(
|
|
`Set icon resource ${iconResourceId} from "${iconPath}" on ` +
|
|
`${shortcuts.length} shortcut(s).`
|
|
);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Push the runtime icon override to every top-level Windows window in this
|
|
* process (excepting private browsing windows and web application windows).
|
|
* A resource ID of 0 reverts windows to the default executable icon.
|
|
*
|
|
* @param {number} iconResourceId Win32 resource ID of an icon embedded in the
|
|
* executable, or 0 to clear.
|
|
*/
|
|
function applyRuntimeWindowsIcon(iconResourceId) {
|
|
try {
|
|
lazy.WinTaskbar.setAllWindowIcons(iconResourceId);
|
|
} catch (ex) {
|
|
lazy.logConsole.error("setAllWindowIcons failed", ex);
|
|
}
|
|
}
|
|
|
|
// Values of the Personalize\SystemUsesLightTheme registry value (the OS "Windows
|
|
// mode" that governs the taskbar). Exported so tests can drive osColorScheme().
|
|
export const OS_LIGHT = 1;
|
|
export const OS_DARK = 0;
|
|
|
|
/**
|
|
* The OS color scheme that governs the taskbar / Start Menu background, where
|
|
* applied shortcut icons are shown. Read from the Windows "system" theme
|
|
* setting (Personalize\SystemUsesLightTheme) so it reflects the OS regardless
|
|
* of any browser theme override - the taskbar follows the OS, not Firefox.
|
|
* There is no JS API for this (LookAndFeel is C++-only; nsIXULRuntime exposes
|
|
* only the browser-derived chrome scheme), so we read the registry directly,
|
|
* as Gecko's own nsLookAndFeel does.
|
|
*
|
|
* @returns {"dark"|"light"}
|
|
*/
|
|
function osColorScheme() {
|
|
let key = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
|
|
Ci.nsIWindowsRegKey
|
|
);
|
|
try {
|
|
key.open(
|
|
Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
|
|
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
|
Ci.nsIWindowsRegKey.ACCESS_READ
|
|
);
|
|
// Absent on older Windows, which only had the light theme.
|
|
if (key.hasValue("SystemUsesLightTheme")) {
|
|
return key.readIntValue("SystemUsesLightTheme") === OS_DARK
|
|
? "dark"
|
|
: "light";
|
|
}
|
|
} catch (ex) {
|
|
lazy.logConsole.warn("Could not read OS theme; assuming light.", ex);
|
|
} finally {
|
|
try {
|
|
key.close();
|
|
} catch (ex) {}
|
|
}
|
|
return "light";
|
|
}
|
|
|
|
// The OS scheme used the last time the active icon was applied, so a
|
|
// look-and-feel change that doesn't flip the taskbar theme (font/color changes
|
|
// also fire the notification) is ignored.
|
|
let gLastAppliedScheme = null;
|
|
let gThemeObserverRegistered = false;
|
|
|
|
// When the OS taskbar theme flips, re-apply the active icon so a theme-aware
|
|
// icon (e.g. Minimal) swaps to the legible variant without user action. The
|
|
// observer is registered once at startup and unregisters itself at shutdown;
|
|
// observe() is a no-op unless a theme-aware icon is currently active.
|
|
const gThemeObserver = {
|
|
observe(subject, topic) {
|
|
if (topic === "xpcom-shutdown") {
|
|
Services.obs.removeObserver(this, "look-and-feel-changed");
|
|
Services.obs.removeObserver(this, "xpcom-shutdown");
|
|
return;
|
|
}
|
|
let entry = ICON_CATALOG[CustomIconManager.currentId];
|
|
if (entry?.variants && osColorScheme() !== gLastAppliedScheme) {
|
|
CustomIconManager.apply(CustomIconManager.currentId).catch(ex =>
|
|
lazy.logConsole.error("Re-applying icon after theme change failed", ex)
|
|
);
|
|
}
|
|
},
|
|
};
|
|
|
|
// Register the theme observer for the life of the process. Called once from
|
|
// startup (before any icon is chosen) so icons selected mid-session are also
|
|
// covered; the observer removes itself on xpcom-shutdown.
|
|
function registerThemeObserver() {
|
|
if (gThemeObserverRegistered) {
|
|
return;
|
|
}
|
|
gThemeObserverRegistered = true;
|
|
Services.obs.addObserver(gThemeObserver, "look-and-feel-changed");
|
|
Services.obs.addObserver(gThemeObserver, "xpcom-shutdown");
|
|
}
|
|
|
|
export const CustomIconManager = {
|
|
/**
|
|
* Make the icon identified by `id` the active custom icon for this
|
|
* install. On Windows this:
|
|
*
|
|
* 1. Updates every per-user .lnk this install owns to reference the
|
|
* embedded icon resource (firefox.exe,-<resource-id>).
|
|
* 2. Records the choice in a pref.
|
|
* 3. Pushes the icon to live windows via WM_SETICON.
|
|
*
|
|
* @param {string} id A key in ICON_CATALOG.
|
|
* @returns {Promise<void>}
|
|
* @throws {Error} If `id` is not in the catalog, the platform is not Windows,
|
|
* or this is an MSIX (packaged) build, where the feature is
|
|
* unsupported.
|
|
*/
|
|
async apply(id) {
|
|
if (AppConstants.platform !== "win") {
|
|
throw new Error("Custom icon is only supported on Windows.");
|
|
}
|
|
|
|
if (Services.sysinfo.getProperty("hasWinPackageId")) {
|
|
throw new Error(
|
|
"Custom browser icons are not supported on MSIX (packaged) builds."
|
|
);
|
|
}
|
|
|
|
let entry = ICON_CATALOG[id];
|
|
if (!entry) {
|
|
throw new Error(`Unknown icon id: ${id}`);
|
|
}
|
|
|
|
let scheme = osColorScheme();
|
|
let iconResourceId = resolveResourceId(entry, scheme);
|
|
|
|
let updated = await applyIconToWindowsShortcuts(
|
|
browserExePath(),
|
|
iconResourceId
|
|
);
|
|
if (!updated) {
|
|
lazy.logConsole.warn(
|
|
`apply("${id}"): no Windows shortcuts were updated. The running ` +
|
|
`window icon will change, but desktop/Start Menu/taskbar shortcuts ` +
|
|
`will not. See the log above for why.`
|
|
);
|
|
}
|
|
|
|
Services.prefs.setStringPref(PREF_ICON_ID, id);
|
|
applyRuntimeWindowsIcon(iconResourceId);
|
|
gLastAppliedScheme = scheme;
|
|
},
|
|
|
|
/**
|
|
* Revert all per-user shortcuts and the runtime icon for this process back
|
|
* to the default browser icon, and clear the pref.
|
|
*
|
|
* Safe to call when no custom icon is currently active.
|
|
*
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async revert() {
|
|
if (AppConstants.platform !== "win") {
|
|
return;
|
|
}
|
|
|
|
await applyIconToWindowsShortcuts(browserExePath(), 0);
|
|
|
|
Services.prefs.clearUserPref(PREF_ICON_ID);
|
|
applyRuntimeWindowsIcon(0);
|
|
},
|
|
|
|
/**
|
|
* Eagerly register the runtime icon override before any browser windows
|
|
* are created, so that the first window picks up the custom icon at
|
|
* construction time rather than flashing the default icon.
|
|
*
|
|
* Synchronous and does no I/O: reads the pref, resolves it against the
|
|
* catalog, and pushes the resource ID to WinTaskbar. If the id is no longer
|
|
* in the catalog, ensureAppliedOrRevert() reconciles later.
|
|
*
|
|
* Intended to be called from a browser-before-ui-startup hook.
|
|
*/
|
|
applyRuntimeOverrideForStartup() {
|
|
if (AppConstants.platform !== "win") {
|
|
return;
|
|
}
|
|
if (Services.sysinfo.getProperty("hasWinPackageId")) {
|
|
return;
|
|
}
|
|
// Register before the no-icon early-return so icons chosen later in the
|
|
// session are still re-applied when the OS theme flips.
|
|
registerThemeObserver();
|
|
let entry = ICON_CATALOG[this.currentId];
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
applyRuntimeWindowsIcon(resolveResourceId(entry, osColorScheme()));
|
|
},
|
|
|
|
/**
|
|
* Reconcile pref state with the shipped catalog at startup. If a custom icon
|
|
* is recorded in prefs and still exists in this build's catalog, push it to
|
|
* runtime windows. If the id is unknown (e.g. an older build that never
|
|
* shipped it, or an icon removed from the catalog), revert the .lnks to the
|
|
* default and clear the pref.
|
|
*
|
|
* Intended to be called from StartupOSIntegration once per process.
|
|
*
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async ensureAppliedOrRevert() {
|
|
if (AppConstants.platform !== "win") {
|
|
return;
|
|
}
|
|
|
|
if (Services.sysinfo.getProperty("hasWinPackageId")) {
|
|
return;
|
|
}
|
|
|
|
let id = this.currentId;
|
|
if (!id) {
|
|
return;
|
|
}
|
|
|
|
let entry = ICON_CATALOG[id];
|
|
if (!entry) {
|
|
lazy.logConsole.warn(`Custom icon id not in catalog, reverting: ${id}`);
|
|
await this.revert();
|
|
return;
|
|
}
|
|
|
|
if (entry.variants) {
|
|
// The OS theme may have flipped while the browser was closed, so re-apply
|
|
// the matching variant to shortcuts + runtime, and start watching for
|
|
// further changes.
|
|
await this.apply(id);
|
|
return;
|
|
}
|
|
|
|
applyRuntimeWindowsIcon(resolveResourceId(entry, osColorScheme()));
|
|
},
|
|
};
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
CustomIconManager,
|
|
"currentId",
|
|
PREF_ICON_ID,
|
|
""
|
|
);
|