/* 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 { 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} */ 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); } } }