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) {
269 return this.onChange(event);
271 return this.onCommand(event);
273 return this.onDialogAccept(event);
275 return this.onInput(event);
277 return this.onUnload(event);
283 _syncFromPrefListeners: new WeakMap(),
284 _syncToPrefListeners: new WeakMap(),
286 addSyncFromPrefListener(aElement, callback) {
287 this._syncFromPrefListeners.set(aElement, callback);
288 if (this.updateQueued) {
291 // Make sure elements are updated correctly with the listener attached.
292 let elementPref = aElement.getAttribute("preference");
294 let pref = this.get(elementPref);
296 pref.updateElements();
301 addSyncToPrefListener(aElement, callback) {
302 this._syncToPrefListeners.set(aElement, callback);
303 if (this.updateQueued) {
306 // Make sure elements are updated correctly with the listener attached.
307 let elementPref = aElement.getAttribute("preference");
309 let pref = this.get(elementPref);
311 pref.updateElements();
316 removeSyncFromPrefListener(aElement) {
317 this._syncFromPrefListeners.delete(aElement);
320 removeSyncToPrefListener(aElement) {
321 this._syncToPrefListeners.delete(aElement);
325 Services.prefs.addObserver("", Preferences);
326 window.addEventListener("change", Preferences);
327 window.addEventListener("command", Preferences);
328 window.addEventListener("dialogaccept", Preferences);
329 window.addEventListener("input", Preferences);
330 window.addEventListener("select", Preferences);
331 window.addEventListener("unload", Preferences, { once: true });
333 class Preference extends EventEmitter {
334 constructor({ id, type, inverted }) {
336 this.on("change", this.onChange.bind(this));
339 this.readonly = false;
340 this._useDefault = false;
341 this.batching = false;
345 this.inverted = !!inverted;
347 // In non-instant apply mode, we must try and use the last saved state
348 // from any previous opens of a child dialog instead of the value from
349 // preferences, to pick up any edits a user may have made.
352 Preferences.type == "child" &&
354 window.opener.Preferences &&
355 window.opener.document.nodePrincipal.isSystemPrincipal
357 // Try to find the preference in the parent window.
358 const preference = window.opener.Preferences.get(this.id);
360 // Don't use the value setter here, we don't want updateElements to be
361 // prematurely fired.
362 this._value = preference ? preference.value : this.valueFromPreferences;
364 this._value = this.valueFromPreferences;
369 // defer reset until preference update
370 this.value = undefined;
373 _reportUnknownType() {
374 const msg = `Preference with id=${this.id} has unknown type ${this.type}.`;
375 Services.console.logStringMessage(msg);
378 setElementValue(aElement) {
380 aElement.disabled = true;
383 if (!this.isElementEditable(aElement)) {
389 if (Preferences._syncFromPrefListeners.has(aElement)) {
390 rv = Preferences._syncFromPrefListeners.get(aElement)(aElement);
393 if (val === undefined) {
394 val = Preferences.instantApply ? this.valueFromPreferences : this.value;
396 // if the preference is marked for reset, show default value in UI
397 if (val === undefined) {
398 val = this.defaultValue;
402 * Initialize a UI element property with a value. Handles the case
403 * where an element has not yet had a XBL binding attached for it and
404 * the property setter does not yet exist by setting the same attribute
405 * on the XUL element using DOM apis and assuming the element's
406 * constructor or property getters appropriately handle this state.
408 function setValue(element, attribute, value) {
409 if (attribute in element) {
410 element[attribute] = value;
411 } else if (attribute === "checked") {
412 // The "checked" attribute can't simply be set to the specified value;
413 // it has to be set if the value is true and removed if the value
414 // is false in order to be interpreted correctly by the element.
416 // In theory we can set it to anything; however xbl implementation
417 // of `checkbox` only works with "true".
418 element.setAttribute(attribute, "true");
420 element.removeAttribute(attribute);
423 element.setAttribute(attribute, value);
427 aElement.localName == "checkbox" ||
428 (aElement.localName == "input" && aElement.type == "checkbox")
430 setValue(aElement, "checked", val);
432 setValue(aElement, "value", val);
436 getElementValue(aElement) {
437 if (Preferences._syncToPrefListeners.has(aElement)) {
439 const rv = Preferences._syncToPrefListeners.get(aElement)(aElement);
440 if (rv !== undefined) {
449 * Read the value of an attribute from an element, assuming the
450 * attribute is a property on the element's node API. If the property
451 * is not present in the API, then assume its value is contained in
452 * an attribute, as is the case before a binding has been attached.
454 function getValue(element, attribute) {
455 if (attribute in element) {
456 return element[attribute];
458 return element.getAttribute(attribute);
462 aElement.localName == "checkbox" ||
463 (aElement.localName == "input" && aElement.type == "checkbox")
465 value = getValue(aElement, "checked");
467 value = getValue(aElement, "value");
472 return parseInt(value, 10) || 0;
474 return typeof value == "boolean" ? value : value == "true";
479 isElementEditable(aElement) {
480 switch (aElement.localName) {
492 let startTime = performance.now();
498 const elements = getElementsByAttribute("preference", this.id);
499 for (const element of elements) {
500 this.setElementValue(element);
503 ChromeUtils.addProfilerMarker(
505 { startTime, captureStack: true },
506 `updateElements for ${this.id}`
511 this.updateElements();
519 if (this.value !== val) {
521 if (Preferences.instantApply) {
522 this.valueFromPreferences = val;
529 return Services.prefs.prefIsLocked(this.id);
532 updateControlDisabledState(val) {
537 val = val || this.locked;
539 const elements = getElementsByAttribute("preference", this.id);
540 for (const element of elements) {
541 element.disabled = val;
543 const labels = getElementsByAttribute("control", element.id);
544 for (const label of labels) {
545 label.disabled = val;
552 Services.prefs.prefHasUserValue(this.id) && this.value !== undefined
557 this._useDefault = true;
558 const val = this.valueFromPreferences;
559 this._useDefault = false;
564 return this._useDefault ? Preferences.defaultBranch : Services.prefs;
567 get valueFromPreferences() {
569 // Force a resync of value with preferences.
572 return this._branch.getIntPref(this.id);
574 const val = this._branch.getBoolPref(this.id);
575 return this.inverted ? !val : val;
578 return this._branch.getComplexValue(
580 Ci.nsIPrefLocalizedString
584 return this._branch.getStringPref(this.id);
586 const family = this._branch.getStringPref(this.id);
587 const fontEnumerator = Cc[
588 "@mozilla.org/gfx/fontenumerator;1"
589 ].createInstance(Ci.nsIFontEnumerator);
590 return fontEnumerator.getStandardFamilyName(family);
593 const f = this._branch.getComplexValue(this.id, Ci.nsIFile);
597 this._reportUnknownType();
603 set valueFromPreferences(val) {
604 // Exit early if nothing to do.
605 if (this.readonly || this.valueFromPreferences == val) {
609 // The special value undefined means 'reset preference to default'.
610 if (val === undefined) {
611 Services.prefs.clearUserPref(this.id);
615 // Force a resync of preferences with value.
618 Services.prefs.setIntPref(this.id, val);
621 Services.prefs.setBoolPref(this.id, this.inverted ? !val : val);
624 const pls = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
625 Ci.nsIPrefLocalizedString
628 Services.prefs.setComplexValue(
630 Ci.nsIPrefLocalizedString,
638 Services.prefs.setStringPref(this.id, val);
642 if (typeof val == "string") {
643 lf = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
644 lf.persistentDescriptor = val;
646 lf.initWithPath(val);
649 lf = val.QueryInterface(Ci.nsIFile);
651 Services.prefs.setComplexValue(this.id, Ci.nsIFile, lf);
655 this._reportUnknownType();
657 if (!this.batching) {
658 Services.prefs.savePrefFile(null);