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
import { Preferences } from "chrome://global/content/preferences/Preferences.mjs";
const { EventEmitter } = ChromeUtils.importESModule(
"resource://gre/modules/EventEmitter.sys.mjs"
);
/**
* @typedef {object} PreferenceConfigInfo
* @property {string} id
* @property {string} type
* @property {boolean} [inverted]
*/
/**
* @param {string} name
* @param {string} value
* @returns {NodeListOf<HTMLInputElement>}
*/
function getElementsByAttribute(name, value) {
// If we needed to defend against arbitrary values, we would escape
// double quotes (") and escape characters (\) in them, i.e.:
// ${value.replace(/["\\]/g, '\\$&')}
return document.querySelectorAll(`[${name}="${value}"]`);
}
export class Preference extends EventEmitter {
/**
* @type {string | undefined | null}
*/
_value;
/**
* @param {PreferenceConfigInfo} configInfo
*/
constructor({ id, type, inverted }) {
super();
this.on("change", this.onChange.bind(this));
this._value = null;
this.readonly = false;
this._useDefault = false;
this.batching = false;
this.id = id;
this.type = type;
this.inverted = !!inverted;
// In non-instant apply mode, we must try and use the last saved state
// from any previous opens of a child dialog instead of the value from
// preferences, to pick up any edits a user may have made.
if (
Preferences.type == "child" &&
window.opener &&
window.opener.Preferences &&
window.opener.document.nodePrincipal.isSystemPrincipal
) {
// Try to find the preference in the parent window.
const preference = window.opener.Preferences.get(this.id);
// Don't use the value setter here, we don't want updateElements to be
// prematurely fired.
this._value = preference ? preference.value : this.valueFromPreferences;
} else {
this._value = this.valueFromPreferences;
}
}
reset() {
// defer reset until preference update
this.value = undefined;
}
_reportUnknownType() {
const msg = `Preference with id=${this.id} has unknown type ${this.type}.`;
Services.console.logStringMessage(msg);
}
/**
* @param {HTMLInputElement} aElement
*/
setElementValue(aElement) {
if (this.locked) {
aElement.disabled = true;
}
if (aElement.labels?.length) {
for (let label of aElement.labels) {
label.toggleAttribute("disabled", this.locked);
}
}
if (!this.isElementEditable(aElement)) {
return;
}
let rv = undefined;
if (Preferences._syncFromPrefListeners.has(aElement)) {
rv = Preferences._syncFromPrefListeners.get(aElement)(aElement);
}
let val = rv;
if (val === undefined) {
val = Preferences.instantApply ? this.valueFromPreferences : this.value;
}
// if the preference is marked for reset, show default value in UI
if (val === undefined) {
val = this.defaultValue;
}
/**
* Initialize a UI element property with a value. Handles the case
* where an element has not yet had a XBL binding attached for it and
* the property setter does not yet exist by setting the same attribute
* on the XUL element using DOM apis and assuming the element's
* constructor or property getters appropriately handle this state.
*/
function setValue(element, attribute, value) {
if (attribute in element) {
element[attribute] = value;
} else if (attribute === "checked" || attribute === "pressed") {
// The "checked" attribute can't simply be set to the specified value;
// it has to be set if the value is true and removed if the value
// is false in order to be interpreted correctly by the element.
if (value) {
// In theory we can set it to anything; however xbl implementation
// of `checkbox` only works with "true".
element.setAttribute(attribute, "true");
} else {
element.removeAttribute(attribute);
}
} else {
element.setAttribute(attribute, value);
}
}
if (
aElement.localName == "checkbox" ||
aElement.localName == "moz-checkbox" ||
(aElement.localName == "input" && aElement.type == "checkbox")
) {
setValue(aElement, "checked", val);
} else if (aElement.localName == "moz-toggle") {
setValue(aElement, "pressed", val);
} else {
setValue(aElement, "value", val);
}
}
/**
* @param {HTMLElement} aElement
* @returns {string}
*/
getElementValue(aElement) {
if (Preferences._syncToPrefListeners.has(aElement)) {
try {
const rv = Preferences._syncToPrefListeners.get(aElement)(aElement);
if (rv !== undefined) {
return rv;
}
} catch (e) {
console.error(e);
}
}
/**
* Read the value of an attribute from an element, assuming the
* attribute is a property on the element's node API. If the property
* is not present in the API, then assume its value is contained in
* an attribute, as is the case before a binding has been attached.
*/
function getValue(element, attribute) {
if (attribute in element) {
return element[attribute];
}
return element.getAttribute(attribute);
}
let value;
if (
aElement.localName == "checkbox" ||
aElement.localName == "moz-checkbox" ||
(aElement.localName == "input" && aElement.type == "checkbox")
) {
value = getValue(aElement, "checked");
} else if (aElement.localName == "moz-toggle") {
value = getValue(aElement, "pressed");
} else {
value = getValue(aElement, "value");
}
switch (this.type) {
case "int":
return parseInt(value, 10) || 0;
case "bool":
return typeof value == "boolean" ? value : value == "true";
}
return value;
}
/**
* @param {HTMLElement} aElement
* @returns {boolean}
*/
isElementEditable(aElement) {
switch (aElement.localName) {
case "checkbox":
case "input":
case "radiogroup":
case "textarea":
case "menulist":
case "moz-toggle":
case "moz-checkbox":
return true;
}
return false;
}
updateElements() {
let startTime = ChromeUtils.now();
if (!this.id) {
return;
}
const elements = getElementsByAttribute("preference", this.id);
for (const element of elements) {
this.setElementValue(element);
}
ChromeUtils.addProfilerMarker(
"Preferences",
{ startTime, captureStack: true },
`updateElements for ${this.id}`
);
}
onChange() {
this.updateElements();
}
/**
* @type {Preference['_value']}
*/
get value() {
return this._value;
}
/**
* @param {string} val
*/
set value(val) {
if (this.value !== val) {
this._value = val;
if (Preferences.instantApply) {
this.valueFromPreferences = val;
}
this.emit("change");
}
}
/**
* @type {boolean}
*/
get locked() {
return Services.prefs.prefIsLocked(this.id);
}
/**
* @param {boolean | string} val
* @returns {void}
*/
updateControlDisabledState(val) {
if (!this.id) {
return;
}
val = val || this.locked;
const elements = getElementsByAttribute("preference", this.id);
for (const element of elements) {
element.disabled = val;
const labels = getElementsByAttribute("control", element.id);
for (const label of labels) {
label.disabled = val;
}
}
}
get hasUserValue() {
return Services.prefs.prefHasUserValue(this.id) && this.value !== undefined;
}
get defaultValue() {
this._useDefault = true;
const val = this.valueFromPreferences;
this._useDefault = false;
return val;
}
get _branch() {
return this._useDefault ? Preferences.defaultBranch : Services.prefs;
}
/**
* @type {string}
*/
get valueFromPreferences() {
try {
// Force a resync of value with preferences.
switch (this.type) {
case "int":
return this._branch.getIntPref(this.id);
case "bool": {
const val = this._branch.getBoolPref(this.id);
return this.inverted ? !val : val;
}
case "wstring":
return this._branch.getComplexValue(
this.id,
Ci.nsIPrefLocalizedString
).data;
case "string":
case "unichar":
return this._branch.getStringPref(this.id);
case "fontname": {
const family = this._branch.getStringPref(this.id);
const fontEnumerator = Cc[
"@mozilla.org/gfx/fontenumerator;1"
].createInstance(Ci.nsIFontEnumerator);
return fontEnumerator.getStandardFamilyName(family);
}
case "file": {
const f = this._branch.getComplexValue(this.id, Ci.nsIFile);
return f;
}
default:
this._reportUnknownType();
}
} catch (e) {}
return null;
}
/**
* @param {string} val
*/
set valueFromPreferences(val) {
// Exit early if nothing to do.
if (this.readonly || this.valueFromPreferences == val) {
return;
}
// The special value undefined means 'reset preference to default'.
if (val === undefined) {
Services.prefs.clearUserPref(this.id);
return;
}
// Force a resync of preferences with value.
switch (this.type) {
case "int":
Services.prefs.setIntPref(this.id, val);
break;
case "bool":
Services.prefs.setBoolPref(this.id, this.inverted ? !val : val);
break;
case "wstring": {
const pls = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
Ci.nsIPrefLocalizedString
);
pls.data = val;
Services.prefs.setComplexValue(this.id, Ci.nsIPrefLocalizedString, pls);
break;
}
case "string":
case "unichar":
case "fontname":
Services.prefs.setStringPref(this.id, val);
break;
case "file": {
let lf;
if (typeof val == "string") {
lf = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
lf.persistentDescriptor = val;
if (!lf.exists()) {
lf.initWithPath(val);
}
} else {
lf = val.QueryInterface(Ci.nsIFile);
}
Services.prefs.setComplexValue(this.id, Ci.nsIFile, lf);
break;
}
default:
this._reportUnknownType();
}
if (!this.batching) {
Services.prefs.savePrefFile(null);
}
}
}