1 /* - This Source Code Form is subject to the terms of the Mozilla Public
2 - License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 - You can obtain one at http://mozilla.org/MPL/2.0/. */
7 // We attach Preferences to the window object so other contexts (tests, JSMs)
9 const Preferences = (window.Preferences = (function () {
10 const { EventEmitter } = ChromeUtils.importESModule(
11 "resource://gre/modules/EventEmitter.sys.mjs"
15 ChromeUtils.defineESModuleGetters(lazy, {
16 DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
19 function getElementsByAttribute(name, value) {
20 // If we needed to defend against arbitrary values, we would escape
21 // double quotes (") and escape characters (\) in them, i.e.:
22 // ${value.replace(/["\\]/g, '\\$&')}
24 ? document.querySelectorAll(`[${name}="${value}"]`)
25 : document.querySelectorAll(`[${name}]`);
28 const domContentLoadedPromise = new Promise(resolve => {
29 window.addEventListener("DOMContentLoaded", resolve, {
39 if (this._all[prefInfo.id]) {
40 throw new Error(`preference with id '${prefInfo.id}' already added`);
42 const pref = new Preference(prefInfo);
43 this._all[pref.id] = pref;
44 domContentLoadedPromise.then(() => {
45 if (!this.updateQueued) {
46 pref.updateElements();
53 const pref = this._add(prefInfo);
58 prefInfos.map(prefInfo => this._add(prefInfo));
62 return this._all[id] || null;
66 return Object.values(this._all);
69 defaultBranch: Services.prefs.getDefaultBranch(""),
72 return document.documentElement.getAttribute("type") || "";
76 // The about:preferences page forces instantApply.
77 // TODO: Remove forceEnableInstantApply in favor of always applying in a
78 // parent and never applying in a child (bug 1775386).
79 if (this._instantApplyForceEnabled) {
83 // Dialogs of type="child" are never instantApply.
84 return this.type !== "child";
87 _instantApplyForceEnabled: false,
89 // Override the computed value of instantApply for this window.
90 forceEnableInstantApply() {
91 this._instantApplyForceEnabled = true;
94 observe(subject, topic, data) {
95 const pref = this._all[data];
97 pref.value = pref.valueFromPreferences;
103 queueUpdateOfAllElements() {
104 if (this.updateQueued) {
108 this.updateQueued = true;
110 Services.tm.dispatchToMainThread(() => {
111 let startTime = performance.now();
113 const elements = getElementsByAttribute("preference");
114 for (const element of elements) {
115 const id = element.getAttribute("preference");
116 let preference = this.get(id);
118 console.error(`Missing preference for ID ${id}`);
122 preference.setElementValue(element);
125 ChromeUtils.addProfilerMarker(
128 `updateAllElements: ${elements.length} preferences updated`
131 this.updateQueued = false;
136 Services.prefs.removeObserver("", this);
139 QueryInterface: ChromeUtils.generateQI(["nsITimerCallback", "nsIObserver"]),
141 _deferredValueUpdateElements: new Set(),
143 writePreferences(aFlushToDisk) {
144 // Write all values to preferences.
145 if (this._deferredValueUpdateElements.size) {
146 this._finalizeDeferredElements();
149 const preferences = Preferences.getAll();
150 for (const preference of preferences) {
151 preference.batching = true;
152 preference.valueFromPreferences = preference.value;
153 preference.batching = false;
156 Services.prefs.savePrefFile(null);
160 getPreferenceElement(aStartElement) {
161 let temp = aStartElement;
164 temp.nodeType == Node.ELEMENT_NODE &&
165 !temp.hasAttribute("preference")
167 temp = temp.parentNode;
169 return temp && temp.nodeType == Node.ELEMENT_NODE ? temp : aStartElement;
172 _deferredValueUpdate(aElement) {
173 delete aElement._deferredValueUpdateTask;
174 const prefID = aElement.getAttribute("preference");
175 const preference = Preferences.get(prefID);
176 const prefVal = preference.getElementValue(aElement);
177 preference.value = prefVal;
178 this._deferredValueUpdateElements.delete(aElement);
181 _finalizeDeferredElements() {
182 for (const el of this._deferredValueUpdateElements) {
183 if (el._deferredValueUpdateTask) {
184 el._deferredValueUpdateTask.finalize();
189 userChangedValue(aElement) {
190 const element = this.getPreferenceElement(aElement);
191 if (element.hasAttribute("preference")) {
192 if (element.getAttribute("delayprefsave") != "true") {
193 const preference = Preferences.get(
194 element.getAttribute("preference")
196 const prefVal = preference.getElementValue(element);
197 preference.value = prefVal;
199 if (!element._deferredValueUpdateTask) {
200 element._deferredValueUpdateTask = new lazy.DeferredTask(
201 this._deferredValueUpdate.bind(this, element),
204 this._deferredValueUpdateElements.add(element);
206 // Each time the preference is changed, restart the delay.
207 element._deferredValueUpdateTask.disarm();
209 element._deferredValueUpdateTask.arm();
215 // This "command" event handler tracks changes made to preferences by
216 // the user in this window.
217 if (event.sourceEvent) {
218 event = event.sourceEvent;
220 this.userChangedValue(event.target);
224 // This "change" event handler tracks changes made to preferences by
225 // the user in this window.
226 this.userChangedValue(event.target);
230 // This "input" event handler tracks changes made to preferences by
231 // the user in this window.
232 this.userChangedValue(event.target);
235 _fireEvent(aEventName, aTarget) {
237 const event = new CustomEvent(aEventName, {
241 return aTarget.dispatchEvent(event);
248 onDialogAccept(event) {
249 let dialog = document.querySelector("dialog");
250 if (!this._fireEvent("beforeaccept", dialog)) {
251 event.preventDefault();
254 this.writePreferences(true);
259 if (Preferences.instantApply) {
262 event.stopPropagation();
263 event.preventDefault();
267 switch (event.type) {
270 return this.onChange(event);
272 return this.onCommand(event);
274 return this.onDialogAccept(event);
276 return this.onInput(event);
278 return this.onUnload(event);
284 _syncFromPrefListeners: new WeakMap(),
285 _syncToPrefListeners: new WeakMap(),
287 addSyncFromPrefListener(aElement, callback) {
288 this._syncFromPrefListeners.set(aElement, callback);
289 if (this.updateQueued) {
292 // Make sure elements are updated correctly with the listener attached.
293 let elementPref = aElement.getAttribute("preference");
295 let pref = this.get(elementPref);
297 pref.updateElements();
302 addSyncToPrefListener(aElement, callback) {
303 this._syncToPrefListeners.set(aElement, callback);
304 if (this.updateQueued) {
307 // Make sure elements are updated correctly with the listener attached.
308 let elementPref = aElement.getAttribute("preference");
310 let pref = this.get(elementPref);
312 pref.updateElements();
317 removeSyncFromPrefListener(aElement) {
318 this._syncFromPrefListeners.delete(aElement);
321 removeSyncToPrefListener(aElement) {
322 this._syncToPrefListeners.delete(aElement);
326 Services.prefs.addObserver("", Preferences);
327 window.addEventListener("toggle", Preferences);
328 window.addEventListener("change", Preferences);
329 window.addEventListener("command", Preferences);
330 window.addEventListener("dialogaccept", Preferences);
331 window.addEventListener("input", Preferences);
332 window.addEventListener("select", Preferences);
333 window.addEventListener("unload", Preferences, { once: true });
335 class Preference extends EventEmitter {
336 constructor({ id, type, inverted }) {
338 this.on("change", this.onChange.bind(this));
341 this.readonly = false;
342 this._useDefault = false;
343 this.batching = false;
347 this.inverted = !!inverted;
349 // In non-instant apply mode, we must try and use the last saved state
350 // from any previous opens of a child dialog instead of the value from
351 // preferences, to pick up any edits a user may have made.
354 Preferences.type == "child" &&
356 window.opener.Preferences &&
357 window.opener.document.nodePrincipal.isSystemPrincipal
359 // Try to find the preference in the parent window.
360 const preference = window.opener.Preferences.get(this.id);
362 // Don't use the value setter here, we don't want updateElements to be
363 // prematurely fired.
364 this._value = preference ? preference.value : this.valueFromPreferences;
366 this._value = this.valueFromPreferences;
371 // defer reset until preference update
372 this.value = undefined;
375 _reportUnknownType() {
376 const msg = `Preference with id=${this.id} has unknown type ${this.type}.`;
377 Services.console.logStringMessage(msg);
380 setElementValue(aElement) {
382 aElement.disabled = true;
385 if (!this.isElementEditable(aElement)) {
391 if (Preferences._syncFromPrefListeners.has(aElement)) {
392 rv = Preferences._syncFromPrefListeners.get(aElement)(aElement);
395 if (val === undefined) {
396 val = Preferences.instantApply ? this.valueFromPreferences : this.value;
398 // if the preference is marked for reset, show default value in UI
399 if (val === undefined) {
400 val = this.defaultValue;
404 * Initialize a UI element property with a value. Handles the case
405 * where an element has not yet had a XBL binding attached for it and
406 * the property setter does not yet exist by setting the same attribute
407 * on the XUL element using DOM apis and assuming the element's
408 * constructor or property getters appropriately handle this state.
410 function setValue(element, attribute, value) {
411 if (attribute in element) {
412 element[attribute] = value;
413 } else if (attribute === "checked" || attribute === "pressed") {
414 // The "checked" attribute can't simply be set to the specified value;
415 // it has to be set if the value is true and removed if the value
416 // is false in order to be interpreted correctly by the element.
418 // In theory we can set it to anything; however xbl implementation
419 // of `checkbox` only works with "true".
420 element.setAttribute(attribute, "true");
422 element.removeAttribute(attribute);
425 element.setAttribute(attribute, value);
429 aElement.localName == "checkbox" ||
430 (aElement.localName == "input" && aElement.type == "checkbox")
432 setValue(aElement, "checked", val);
433 } else if (aElement.localName == "moz-toggle") {
434 setValue(aElement, "pressed", val);
436 setValue(aElement, "value", val);
440 getElementValue(aElement) {
441 if (Preferences._syncToPrefListeners.has(aElement)) {
443 const rv = Preferences._syncToPrefListeners.get(aElement)(aElement);
444 if (rv !== undefined) {
453 * Read the value of an attribute from an element, assuming the
454 * attribute is a property on the element's node API. If the property
455 * is not present in the API, then assume its value is contained in
456 * an attribute, as is the case before a binding has been attached.
458 function getValue(element, attribute) {
459 if (attribute in element) {
460 return element[attribute];
462 return element.getAttribute(attribute);
466 aElement.localName == "checkbox" ||
467 (aElement.localName == "input" && aElement.type == "checkbox")
469 value = getValue(aElement, "checked");
470 } else if (aElement.localName == "moz-toggle") {
471 value = getValue(aElement, "pressed");
473 value = getValue(aElement, "value");
478 return parseInt(value, 10) || 0;
480 return typeof value == "boolean" ? value : value == "true";
485 isElementEditable(aElement) {
486 switch (aElement.localName) {
499 let startTime = performance.now();
505 const elements = getElementsByAttribute("preference", this.id);
506 for (const element of elements) {
507 this.setElementValue(element);
510 ChromeUtils.addProfilerMarker(
512 { startTime, captureStack: true },
513 `updateElements for ${this.id}`
518 this.updateElements();
526 if (this.value !== val) {
528 if (Preferences.instantApply) {
529 this.valueFromPreferences = val;
536 return Services.prefs.prefIsLocked(this.id);
539 updateControlDisabledState(val) {
544 val = val || this.locked;
546 const elements = getElementsByAttribute("preference", this.id);
547 for (const element of elements) {
548 element.disabled = val;
550 const labels = getElementsByAttribute("control", element.id);
551 for (const label of labels) {
552 label.disabled = val;
559 Services.prefs.prefHasUserValue(this.id) && this.value !== undefined
564 this._useDefault = true;
565 const val = this.valueFromPreferences;
566 this._useDefault = false;
571 return this._useDefault ? Preferences.defaultBranch : Services.prefs;
574 get valueFromPreferences() {
576 // Force a resync of value with preferences.
579 return this._branch.getIntPref(this.id);
581 const val = this._branch.getBoolPref(this.id);
582 return this.inverted ? !val : val;
585 return this._branch.getComplexValue(
587 Ci.nsIPrefLocalizedString
591 return this._branch.getStringPref(this.id);
593 const family = this._branch.getStringPref(this.id);
594 const fontEnumerator = Cc[
595 "@mozilla.org/gfx/fontenumerator;1"
596 ].createInstance(Ci.nsIFontEnumerator);
597 return fontEnumerator.getStandardFamilyName(family);
600 const f = this._branch.getComplexValue(this.id, Ci.nsIFile);
604 this._reportUnknownType();
610 set valueFromPreferences(val) {
611 // Exit early if nothing to do.
612 if (this.readonly || this.valueFromPreferences == val) {
616 // The special value undefined means 'reset preference to default'.
617 if (val === undefined) {
618 Services.prefs.clearUserPref(this.id);
622 // Force a resync of preferences with value.
625 Services.prefs.setIntPref(this.id, val);
628 Services.prefs.setBoolPref(this.id, this.inverted ? !val : val);
631 const pls = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
632 Ci.nsIPrefLocalizedString
635 Services.prefs.setComplexValue(
637 Ci.nsIPrefLocalizedString,
645 Services.prefs.setStringPref(this.id, val);
649 if (typeof val == "string") {
650 lf = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
651 lf.persistentDescriptor = val;
653 lf.initWithPath(val);
656 lf = val.QueryInterface(Ci.nsIFile);
658 Services.prefs.setComplexValue(this.id, Ci.nsIFile, lf);
662 this._reportUnknownType();
664 if (!this.batching) {
665 Services.prefs.savePrefFile(null);