469 lines
14 KiB
JavaScript
469 lines
14 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 {
|
|
AsyncSetting,
|
|
AsyncSettingHandler,
|
|
} from "chrome://global/content/preferences/AsyncSetting.mjs";
|
|
import { Preferences } from "chrome://global/content/preferences/Preferences.mjs";
|
|
|
|
/**
|
|
* @import { type Preference } from "chrome://global/content/preferences/Preference.mjs"
|
|
* @import { SettingControlConfig } from "chrome://browser/content/preferences/widgets/setting-control.mjs"
|
|
* @import { ExtensionSettingsStore } from "resource://gre/modules/ExtensionSettingsStore.sys.mjs"
|
|
* @import { AddonManager } from "resource://gre/modules/AddonManager.sys.mjs"
|
|
* @import { Management } from "resource://gre/modules/Extension.sys.mjs"
|
|
*/
|
|
|
|
/**
|
|
* A map of Setting instances (values) along with their IDs
|
|
* (keys) so that the dependencies of a setting can
|
|
* be easily looked up by just their ID.
|
|
*
|
|
* @typedef {Record<string, Setting>} SettingDeps
|
|
*/
|
|
|
|
/**
|
|
* @typedef {string | boolean | number | nsIFile | null} SettingValue
|
|
*/
|
|
|
|
/**
|
|
* @callback SettingVisibleCallback
|
|
* @param {SettingDeps} deps
|
|
* @param {Setting} setting
|
|
* @returns {any} If truthy shows the setting in the UI, or hides it if not
|
|
*/
|
|
|
|
/**
|
|
* Gets the value of a {@link Setting}.
|
|
*
|
|
* @callback SettingGetCallback
|
|
* @param {any} val - The value that was retrieved from the preferences backend
|
|
* @param {SettingDeps} deps
|
|
* @param {Setting} setting
|
|
* @returns {any} - The value to set onto the setting
|
|
*/
|
|
|
|
/**
|
|
* Sets the value of a {@link Setting}. If the SettingConfig has {@link SettingConfig.pref} set
|
|
* use this callback to transform the control's value to the literal pref value, this
|
|
* would likely be the inverse of {@link SettingConfig.get}. If {@link SettingConfig.pref} is not set,
|
|
* this should store the value manually in a fashion that can be retrieved in {@link SettingConfig.get}.
|
|
*
|
|
* @callback SettingSetCallback
|
|
* @param {SettingValue} val - The value/pressed/checked from the input (control) associated with the setting
|
|
* @param {SettingDeps} deps
|
|
* @param {Setting} setting
|
|
* @returns {SettingValue}
|
|
*/
|
|
|
|
/**
|
|
* @callback SettingOnUserChangeCallback
|
|
* @param {SettingValue} val - The value/pressed/checked from the input of the control associated with the setting
|
|
* @param {SettingDeps} deps
|
|
* @param {Setting} setting
|
|
* @returns {void}
|
|
*/
|
|
|
|
/**
|
|
* @callback SettingDisabledCallback
|
|
* @param {SettingDeps} deps
|
|
* @param {Setting} setting
|
|
* @returns {any}
|
|
*/
|
|
|
|
/**
|
|
* @callback SettingGetControlConfigCallback
|
|
* @param {SettingControlConfig} config
|
|
* @param {SettingDeps} deps
|
|
* @param {Setting} setting
|
|
* @returns {SettingControlConfig}
|
|
*/
|
|
|
|
/**
|
|
* @callback SettingTeardownCallback
|
|
* @returns {void}
|
|
*/
|
|
|
|
/**
|
|
* @callback SettingEmitChange
|
|
*/
|
|
|
|
/**
|
|
* @callback SettingSetupCallback
|
|
* @param {SettingEmitChange} emitChange Notify listeners of a change to this setting.
|
|
* @param {SettingDeps} deps
|
|
* @param {Setting} setting
|
|
* @returns {SettingTeardownCallback | void}
|
|
*/
|
|
|
|
/**
|
|
* @callback SettingOnUserClickCallback
|
|
* @param {MouseEvent} event
|
|
* @param {SettingDeps} deps
|
|
* @param {Setting} setting
|
|
* @returns {void}
|
|
*/
|
|
|
|
/**
|
|
* @callback SettingOnMessageBarDismissCallback
|
|
* @param {CustomEvent} event - The dismiss event
|
|
* @param {SettingDeps} deps
|
|
* @param {Setting} setting
|
|
* @returns {void}
|
|
*/
|
|
|
|
/**
|
|
* @callback SettingOnUserReorderCallback
|
|
* @param {CustomEvent} event - The reorder event with detail containing draggedElement, targetElement, position, draggedIndex, targetIndex
|
|
* @param {SettingDeps} deps
|
|
* @param {Setting} setting
|
|
* @returns {void}
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} SettingControllingExtensionInfo
|
|
* @property {string} storeId The ExtensionSettingsStore id that controls this setting.
|
|
* @property {string} l10nId A fluent id to show in a controlled by extension message.
|
|
* @property {string} [name] The controlling extension's name.
|
|
* @property {string} [id] The controlling extension's id.
|
|
* @property {string} [supportPage] A support page to show in the message.
|
|
* @property {boolean} [allowControl] If the control should be enabled while controlled.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} SettingConfig
|
|
* @property {string} id - The ID for the Setting, this should match the layout id
|
|
* @property {string} [pref] - A {@link Services.prefs} id that will be used as the backend if it is provided
|
|
* @property {string[]} [deps] - An array of setting IDs that this setting depends on, when these settings change this setting will emit a change event to update the UI
|
|
* @property {Pick<SettingControllingExtensionInfo, "storeId" | "l10nId" | "allowControl" | "supportPage">} [controllingExtensionInfo] Data related to the setting being controlled by an extension.
|
|
* @property {SettingVisibleCallback} [visible] - Function to determine if a setting is visible in the UI
|
|
* @property {SettingGetCallback} [get] - Function to get the value of the setting. Optional if {@link SettingConfig#pref} is set.
|
|
* @property {SettingSetCallback} [set] - See {@link SettingSetCallback}
|
|
* @property {SettingGetControlConfigCallback} [getControlConfig] - Function that allows the setting to modify its layout, this is intended to be used to provide the options, {@link SettingConfig#l10nId} or {@link SettingConfig#l10nArgs} data if necessary, but technically it can change anything (that doesn't mean it will have any effect though).
|
|
* @property {SettingOnUserChangeCallback} [onUserChange] - A function that will be called when the setting
|
|
* has been modified by the user, it is passed the value/pressed/checked from its input. NOTE: This should be used for
|
|
* additional work that needs to happen, such as recording telemetry.
|
|
* If you want to set the value of the Setting then use the {@link SettingConfig.set} function.
|
|
* @property {SettingSetupCallback} [setup] - A function to be called to register listeners for
|
|
* the setting. It should return a {@link SettingTeardownCallback} function to
|
|
* remove the listeners if necessary. This should emit change events when the setting has changed to
|
|
* ensure the UI stays in sync if possible.
|
|
* @property {SettingDisabledCallback} [disabled] - A function to determine if a setting should be disabled
|
|
* @property {SettingOnUserClickCallback} [onUserClick] - A function that will be called when a setting has been
|
|
* clicked, the element name must be included in the CLICK_HANDLERS array
|
|
* in {@link file://./../../browser/components/preferences/widgets/setting-group/setting-group.mjs}. This should be
|
|
* used for controls that aren't regular form controls but instead perform an action when clicked, like a button or link.
|
|
* @property {SettingOnMessageBarDismissCallback} [onMessageBarDismiss] - A function that will be called when a message bar has been
|
|
* dismissed. This should be used for moz-message-bar to override the default behavior.
|
|
* @property {SettingOnUserReorderCallback} [onUserReorder] - A function that will be called when items in a
|
|
* reorderable list have been reordered. This should be used to update the underlying data when the user
|
|
* reorders items, such as updating preference values.
|
|
*/
|
|
|
|
const { EventEmitter } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/EventEmitter.sys.mjs"
|
|
);
|
|
|
|
/** @type {{ ExtensionSettingsStore: typeof ExtensionSettingsStore, AddonManager: typeof AddonManager, Management: typeof Management }} */
|
|
// @ts-expect-error bug 1996860
|
|
const lazy = {};
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
ExtensionSettingsStore:
|
|
"resource://gre/modules/ExtensionSettingsStore.sys.mjs",
|
|
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
|
Management: "resource://gre/modules/Extension.sys.mjs",
|
|
});
|
|
|
|
class PreferenceNotAddedError extends Error {
|
|
/**
|
|
* @param {string} settingId
|
|
* @param {string} prefId
|
|
*/
|
|
constructor(settingId, prefId) {
|
|
super(
|
|
`Setting "${settingId}" was unable to find Preference "${prefId}". Did you register it with Preferences.add/addAll?`
|
|
);
|
|
this.name = "PreferenceNotAddedError";
|
|
this.settingId = settingId;
|
|
this.prefId = prefId;
|
|
}
|
|
}
|
|
|
|
export class Setting extends EventEmitter {
|
|
/**
|
|
* @type {Preference}
|
|
*/
|
|
_pref;
|
|
|
|
/**
|
|
* Keeps a cache of each dep's Setting so that
|
|
* it can be easily looked up by its ID.
|
|
*
|
|
* @type {SettingDeps}
|
|
*/
|
|
_deps;
|
|
|
|
/**
|
|
* @type {SettingConfig | AsyncSettingHandler}
|
|
*/
|
|
config;
|
|
|
|
get pref() {
|
|
return this._pref;
|
|
}
|
|
|
|
set pref(newPref) {
|
|
if (this._pref) {
|
|
this._pref.off("change", this.onChange);
|
|
}
|
|
|
|
this._pref = newPref;
|
|
|
|
if (this._pref) {
|
|
this._pref.on("change", this.onChange);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {SettingConfig['id']} id
|
|
* @param {SettingConfig | typeof AsyncSetting} config
|
|
* @throws {Error} Will throw an error (PreferenceNotAddedError) if
|
|
* config.pref was not registered
|
|
*/
|
|
constructor(id, config) {
|
|
super();
|
|
|
|
/** @type {SettingConfig | AsyncSettingHandler} */
|
|
let configObj;
|
|
|
|
if (Object.getPrototypeOf(config) == AsyncSetting) {
|
|
configObj = new AsyncSettingHandler(
|
|
id,
|
|
/** @type {typeof AsyncSetting} */ (config)
|
|
);
|
|
} else {
|
|
configObj = config;
|
|
}
|
|
|
|
this.id = id;
|
|
this.config = configObj;
|
|
this.pref = configObj.pref && Preferences.get(configObj.pref);
|
|
if (configObj.pref && !this.pref) {
|
|
throw new PreferenceNotAddedError(id, configObj.pref);
|
|
}
|
|
this._emitting = false;
|
|
|
|
/** @type {SettingControllingExtensionInfo} */
|
|
this.controllingExtensionInfo = {
|
|
...this.config.controllingExtensionInfo,
|
|
};
|
|
if (this.config.controllingExtensionInfo?.storeId) {
|
|
this._checkForControllingExtension();
|
|
this.watchExtensionPrefChange();
|
|
}
|
|
if (typeof this.config.setup === "function") {
|
|
this._teardown = this.config.setup(this.onChange, this.deps, this);
|
|
}
|
|
}
|
|
|
|
onChange = () => {
|
|
if (this._emitting) {
|
|
return;
|
|
}
|
|
this._emitting = true;
|
|
this.emit("change");
|
|
this._emitting = false;
|
|
};
|
|
|
|
/**
|
|
* A map of each dep and it's associated {@link Setting} instance.
|
|
*
|
|
* @type {SettingDeps}
|
|
*/
|
|
get deps() {
|
|
if (this._deps) {
|
|
return this._deps;
|
|
}
|
|
/**
|
|
* @type {SettingDeps}
|
|
*/
|
|
const deps = {};
|
|
|
|
if (this.config.deps) {
|
|
for (let id of this.config.deps) {
|
|
const setting = Preferences.getSetting(id);
|
|
if (setting) {
|
|
deps[id] = setting;
|
|
}
|
|
}
|
|
}
|
|
this._deps = deps;
|
|
|
|
for (const setting of Object.values(this._deps)) {
|
|
setting.on("change", this.onChange);
|
|
}
|
|
|
|
return this._deps;
|
|
}
|
|
|
|
/**
|
|
* @type {SettingValue}
|
|
*/
|
|
get value() {
|
|
let prefVal = this.pref?.value;
|
|
if (this.config.get) {
|
|
return this.config.get(prefVal, this.deps, this);
|
|
}
|
|
return prefVal;
|
|
}
|
|
|
|
/**
|
|
* @param {SettingValue} val
|
|
*/
|
|
set value(val) {
|
|
let newVal = this.config.set ? this.config.set(val, this.deps, this) : val;
|
|
if (this.pref && !(newVal instanceof Object && "then" in newVal)) {
|
|
this.pref.value = newVal;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @type {boolean}
|
|
*/
|
|
get locked() {
|
|
return this.pref?.locked ?? false;
|
|
}
|
|
|
|
get visible() {
|
|
return this.config.visible ? this.config.visible(this.deps, this) : true;
|
|
}
|
|
|
|
get disabled() {
|
|
return this.config.disabled ? this.config.disabled(this.deps, this) : false;
|
|
}
|
|
|
|
/**
|
|
* @param {SettingControlConfig} config
|
|
* @returns {SettingControlConfig}
|
|
*/
|
|
getControlConfig(config) {
|
|
if (this.config.getControlConfig) {
|
|
return this.config.getControlConfig(config, this.deps, this);
|
|
}
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* @param {MouseEvent} event
|
|
*/
|
|
userClick(event) {
|
|
if (this.config.onUserClick) {
|
|
this.config.onUserClick(event, this.deps, this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {CustomEvent} event
|
|
*/
|
|
messageBarDismiss(event) {
|
|
if (this.config.onMessageBarDismiss) {
|
|
this.config.onMessageBarDismiss(event, this.deps, this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {CustomEvent} event
|
|
*/
|
|
userReorder(event) {
|
|
if (this.config.onUserReorder) {
|
|
this.config.onUserReorder(event, this.deps, this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} val
|
|
*/
|
|
userChange(val) {
|
|
this.value = val;
|
|
if (this.config.onUserChange) {
|
|
this.config.onUserChange(val, this.deps, this);
|
|
}
|
|
}
|
|
|
|
async disableControllingExtension() {
|
|
if (
|
|
this.controllingExtensionInfo.name &&
|
|
this.controllingExtensionInfo.id
|
|
) {
|
|
await lazy.ExtensionSettingsStore.initialize();
|
|
let { id } = lazy.ExtensionSettingsStore.getSetting(
|
|
"prefs",
|
|
this.controllingExtensionInfo.storeId
|
|
);
|
|
if (id) {
|
|
let addon = await lazy.AddonManager.getAddonByID(id);
|
|
await addon.disable();
|
|
}
|
|
}
|
|
}
|
|
|
|
_checkForControllingExtension = async () => {
|
|
// Make sure all settings API modules are loaded
|
|
// and the extension controlling settings metadata
|
|
// loaded from the ExtensionSettingsStore backend.
|
|
await lazy.Management.asyncLoadSettingsModules();
|
|
await lazy.ExtensionSettingsStore.initialize();
|
|
// Retrieve the extension controlled settings info
|
|
// for the given setting storeId.
|
|
let info = lazy.ExtensionSettingsStore.getSetting(
|
|
"prefs",
|
|
this.config.controllingExtensionInfo?.storeId
|
|
);
|
|
if (info && info.id) {
|
|
let addon = await lazy.AddonManager.getAddonByID(info.id);
|
|
if (addon) {
|
|
this.controllingExtensionInfo.name = addon.name;
|
|
this.controllingExtensionInfo.id = info.id;
|
|
this.emit("change");
|
|
return;
|
|
}
|
|
}
|
|
this._clearControllingExtensionInfo();
|
|
};
|
|
|
|
_clearControllingExtensionInfo() {
|
|
delete this.controllingExtensionInfo.id;
|
|
delete this.controllingExtensionInfo.name;
|
|
delete this.controllingExtensionInfo.supportPage;
|
|
// Request an update to the setting control so the UI is in the correct state
|
|
this.onChange();
|
|
}
|
|
|
|
watchExtensionPrefChange() {
|
|
lazy.Management.on(
|
|
`extension-setting-changed:${this.config.controllingExtensionInfo?.storeId}`,
|
|
this._checkForControllingExtension
|
|
);
|
|
}
|
|
|
|
destroy() {
|
|
if (typeof this._teardown === "function") {
|
|
this._teardown();
|
|
this._teardown = null;
|
|
}
|
|
|
|
if (this.pref) {
|
|
this.pref.off("change", this.onChange);
|
|
}
|
|
|
|
if (this.config.controllingExtensionInfo?.storeId) {
|
|
lazy.Management.off(
|
|
`extension-setting-changed:${this.config.controllingExtensionInfo?.storeId}`,
|
|
this._checkForControllingExtension
|
|
);
|
|
}
|
|
}
|
|
}
|