Bug 1885565 - Part 1: Add mozac_ic_avatar_circle_24 to ui-icons r=android-reviewers...
[gecko.git] / toolkit / modules / Preferences.sys.mjs
blob619087aa678afa380b144ac73947e798a7ae41e7
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
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 // The minimum and maximum integers that can be set as preferences.
6 // The range of valid values is narrower than the range of valid JS values
7 // because the native preferences code treats integers as NSPR PRInt32s,
8 // which are 32-bit signed integers on all platforms.
9 const MAX_INT = 0x7fffffff; // Math.pow(2, 31) - 1
10 const MIN_INT = -0x80000000;
12 export function Preferences(args) {
13   this._cachedPrefBranch = null;
14   if (isObject(args)) {
15     if (args.branch) {
16       this._branchStr = args.branch;
17     }
18     if (args.defaultBranch) {
19       this._defaultBranch = args.defaultBranch;
20     }
21     if (args.privacyContext) {
22       this._privacyContext = args.privacyContext;
23     }
24   } else if (args) {
25     this._branchStr = args;
26   }
29 /**
30  * Get the value of a pref, if any; otherwise return the default value.
31  *
32  * @param   prefName  {String|Array}
33  *          the pref to get, or an array of prefs to get
34  *
35  * @param   defaultValue
36  *          the default value, if any, for prefs that don't have one
37  *
38  * @param   valueType
39  *          the XPCOM interface of the pref's complex value type, if any
40  *
41  * @returns the value of the pref, if any; otherwise the default value
42  */
43 Preferences.get = function (prefName, defaultValue, valueType = null) {
44   if (Array.isArray(prefName)) {
45     return prefName.map(v => this.get(v, defaultValue));
46   }
48   return this._get(prefName, defaultValue, valueType);
51 Preferences._get = function (prefName, defaultValue, valueType) {
52   switch (this._prefBranch.getPrefType(prefName)) {
53     case Ci.nsIPrefBranch.PREF_STRING:
54       if (valueType) {
55         let ifaces = ["nsIFile", "nsIPrefLocalizedString"];
56         if (ifaces.includes(valueType.name)) {
57           return this._prefBranch.getComplexValue(prefName, valueType).data;
58         }
59       }
60       return this._prefBranch.getStringPref(prefName);
62     case Ci.nsIPrefBranch.PREF_INT:
63       return this._prefBranch.getIntPref(prefName);
65     case Ci.nsIPrefBranch.PREF_BOOL:
66       return this._prefBranch.getBoolPref(prefName);
68     case Ci.nsIPrefBranch.PREF_INVALID:
69       return defaultValue;
71     default:
72       // This should never happen.
73       throw new Error(
74         `Error getting pref ${prefName}; its value's type is ` +
75           `${this._prefBranch.getPrefType(prefName)}, which I don't ` +
76           `know how to handle.`
77       );
78   }
81 /**
82  * Set a preference to a value.
83  *
84  * You can set multiple prefs by passing an object as the only parameter.
85  * In that case, this method will treat the properties of the object
86  * as preferences to set, where each property name is the name of a pref
87  * and its corresponding property value is the value of the pref.
88  *
89  * @param   prefName  {String|Object}
90  *          the name of the pref to set; or an object containing a set
91  *          of prefs to set
92  *
93  * @param   prefValue {String|Number|Boolean}
94  *          the value to which to set the pref
95  *
96  * Note: Preferences cannot store non-integer numbers or numbers outside
97  * the signed 32-bit range -(2^31-1) to 2^31-1, If you have such a number,
98  * store it as a string by calling toString() on the number before passing
99  * it to this method, i.e.:
100  *   Preferences.set("pi", 3.14159.toString())
101  *   Preferences.set("big", Math.pow(2, 31).toString()).
102  */
103 Preferences.set = function (prefName, prefValue) {
104   if (isObject(prefName)) {
105     for (let [name, value] of Object.entries(prefName)) {
106       this.set(name, value);
107     }
108     return;
109   }
111   this._set(prefName, prefValue);
114 Preferences._set = function (prefName, prefValue) {
115   let prefType;
116   if (typeof prefValue != "undefined" && prefValue != null) {
117     prefType = prefValue.constructor.name;
118   }
120   switch (prefType) {
121     case "String":
122       this._prefBranch.setStringPref(prefName, prefValue);
123       break;
125     case "Number":
126       // We throw if the number is outside the range, since the result
127       // will never be what the consumer wanted to store, but we only warn
128       // if the number is non-integer, since the consumer might not mind
129       // the loss of precision.
130       if (prefValue > MAX_INT || prefValue < MIN_INT) {
131         throw new Error(
132           `you cannot set the ${prefName} pref to the number ` +
133             `${prefValue}, as number pref values must be in the signed ` +
134             `32-bit integer range -(2^31-1) to 2^31-1.  To store numbers ` +
135             `outside that range, store them as strings.`
136         );
137       }
138       this._prefBranch.setIntPref(prefName, prefValue);
139       if (prefValue % 1 != 0) {
140         console.error(
141           "Warning: setting the ",
142           prefName,
143           " pref to the non-integer number ",
144           prefValue,
145           " converted it " +
146             "to the integer number " +
147             this.get(prefName) +
148             "; to retain fractional precision, store non-integer " +
149             "numbers as strings."
150         );
151       }
152       break;
154     case "Boolean":
155       this._prefBranch.setBoolPref(prefName, prefValue);
156       break;
158     default:
159       throw new Error(
160         `can't set pref ${prefName} to value '${prefValue}'; ` +
161           `it isn't a String, Number, or Boolean`
162       );
163   }
167  * Whether or not the given pref has a value.  This is different from isSet
168  * because it returns true whether the value of the pref is a default value
169  * or a user-set value, while isSet only returns true if the value
170  * is a user-set value.
172  * @param   prefName  {String|Array}
173  *          the pref to check, or an array of prefs to check
175  * @returns {Boolean|Array}
176  *          whether or not the pref has a value; or, if the caller provided
177  *          an array of pref names, an array of booleans indicating whether
178  *          or not the prefs have values
179  */
180 Preferences.has = function (prefName) {
181   if (Array.isArray(prefName)) {
182     return prefName.map(this.has, this);
183   }
185   return (
186     this._prefBranch.getPrefType(prefName) != Ci.nsIPrefBranch.PREF_INVALID
187   );
191  * Whether or not the given pref has a user-set value.  This is different
192  * from |has| because it returns true only if the value of the pref is a user-
193  * set value, while |has| returns true if the value of the pref is a default
194  * value or a user-set value.
196  * @param   prefName  {String|Array}
197  *          the pref to check, or an array of prefs to check
199  * @returns {Boolean|Array}
200  *          whether or not the pref has a user-set value; or, if the caller
201  *          provided an array of pref names, an array of booleans indicating
202  *          whether or not the prefs have user-set values
203  */
204 Preferences.isSet = function (prefName) {
205   if (Array.isArray(prefName)) {
206     return prefName.map(this.isSet, this);
207   }
209   return this.has(prefName) && this._prefBranch.prefHasUserValue(prefName);
213  * Whether or not the given pref has a user-set value. Use isSet instead,
214  * which is equivalent.
215  * @deprecated
216  */
217 Preferences.modified = function (prefName) {
218   return this.isSet(prefName);
221 Preferences.reset = function (prefName) {
222   if (Array.isArray(prefName)) {
223     prefName.map(v => this.reset(v));
224     return;
225   }
227   this._prefBranch.clearUserPref(prefName);
231  * Lock a pref so it can't be changed.
233  * @param   prefName  {String|Array}
234  *          the pref to lock, or an array of prefs to lock
235  */
236 Preferences.lock = function (prefName) {
237   if (Array.isArray(prefName)) {
238     prefName.map(this.lock, this);
239   }
241   this._prefBranch.lockPref(prefName);
245  * Unlock a pref so it can be changed.
247  * @param   prefName  {String|Array}
248  *          the pref to lock, or an array of prefs to lock
249  */
250 Preferences.unlock = function (prefName) {
251   if (Array.isArray(prefName)) {
252     prefName.map(this.unlock, this);
253   }
255   this._prefBranch.unlockPref(prefName);
259  * Whether or not the given pref is locked against changes.
261  * @param   prefName  {String|Array}
262  *          the pref to check, or an array of prefs to check
264  * @returns {Boolean|Array}
265  *          whether or not the pref has a user-set value; or, if the caller
266  *          provided an array of pref names, an array of booleans indicating
267  *          whether or not the prefs have user-set values
268  */
269 Preferences.locked = function (prefName) {
270   if (Array.isArray(prefName)) {
271     return prefName.map(this.locked, this);
272   }
274   return this._prefBranch.prefIsLocked(prefName);
278  * Start observing a pref.
280  * The callback can be a function or any object that implements nsIObserver.
281  * When the callback is a function and thisObject is provided, it gets called
282  * as a method of thisObject.
284  * @param   prefName    {String}
285  *          the name of the pref to observe
287  * @param   callback    {Function|Object}
288  *          the code to notify when the pref changes;
290  * @param   thisObject  {Object}  [optional]
291  *          the object to use as |this| when calling a Function callback;
293  * @returns the wrapped observer
294  */
295 Preferences.observe = function (prefName, callback, thisObject) {
296   let fullPrefName = this._branchStr + (prefName || "");
298   let observer = new PrefObserver(fullPrefName, callback, thisObject);
299   Preferences._prefBranch.addObserver(fullPrefName, observer, true);
300   observers.push(observer);
302   return observer;
306  * Stop observing a pref.
308  * You must call this method with the same prefName, callback, and thisObject
309  * with which you originally registered the observer.  However, you don't have
310  * to call this method on the same exact instance of Preferences; you can call
311  * it on any instance.  For example, the following code first starts and then
312  * stops observing the "foo.bar.baz" preference:
314  *   let observer = function() {...};
315  *   Preferences.observe("foo.bar.baz", observer);
316  *   new Preferences("foo.bar.").ignore("baz", observer);
318  * @param   prefName    {String}
319  *          the name of the pref being observed
321  * @param   callback    {Function|Object}
322  *          the code being notified when the pref changes
324  * @param   thisObject  {Object}  [optional]
325  *          the object being used as |this| when calling a Function callback
326  */
327 Preferences.ignore = function (prefName, callback, thisObject) {
328   let fullPrefName = this._branchStr + (prefName || "");
330   // This seems fairly inefficient, but I'm not sure how much better we can
331   // make it.  We could index by fullBranch, but we can't index by callback
332   // or thisObject, as far as I know, since the keys to JavaScript hashes
333   // (a.k.a. objects) can apparently only be primitive values.
334   let [observer] = observers.filter(
335     v =>
336       v.prefName == fullPrefName &&
337       v.callback == callback &&
338       v.thisObject == thisObject
339   );
341   if (observer) {
342     Preferences._prefBranch.removeObserver(fullPrefName, observer);
343     observers.splice(observers.indexOf(observer), 1);
344   } else {
345     console.error(
346       `Attempt to stop observing a preference "${prefName}" that's not being observed`
347     );
348   }
351 Preferences.resetBranch = function (prefBranch = "") {
352   this.reset(this._prefBranch.getChildList(prefBranch));
356  * A string identifying the branch of the preferences tree to which this
357  * instance provides access.
358  * @private
359  */
360 Preferences._branchStr = "";
363  * The cached preferences branch object this instance encapsulates, or null.
364  * Do not use!  Use _prefBranch below instead.
365  * @private
366  */
367 Preferences._cachedPrefBranch = null;
370  * The preferences branch object for this instance.
371  * @private
372  */
373 Object.defineProperty(Preferences, "_prefBranch", {
374   get: function _prefBranch() {
375     if (!this._cachedPrefBranch) {
376       let prefSvc = Services.prefs;
377       this._cachedPrefBranch = this._defaultBranch
378         ? prefSvc.getDefaultBranch(this._branchStr)
379         : prefSvc.getBranch(this._branchStr);
380     }
381     return this._cachedPrefBranch;
382   },
383   enumerable: true,
384   configurable: true,
387 // Constructor-based access (Preferences.get(...) and set) is preferred over
388 // instance-based access (new Preferences().get(...) and set) and when using the
389 // root preferences branch, as it's desirable not to allocate the extra object.
390 // But both forms are acceptable.
391 Preferences.prototype = Preferences;
394  * A cache of pref observers.
396  * We use this to remove observers when a caller calls Preferences::ignore.
398  * All Preferences instances share this object, because we want callers to be
399  * able to remove an observer using a different Preferences object than the one
400  * with which they added it.  That means we have to identify the observers
401  * in this object by their complete pref name, not just their name relative to
402  * the root branch of the Preferences object with which they were created.
403  */
404 var observers = [];
406 function PrefObserver(prefName, callback, thisObject) {
407   this.prefName = prefName;
408   this.callback = callback;
409   this.thisObject = thisObject;
412 PrefObserver.prototype = {
413   QueryInterface: ChromeUtils.generateQI([
414     "nsIObserver",
415     "nsISupportsWeakReference",
416   ]),
418   observe(subject, topic, data) {
419     // The pref service only observes whole branches, but we only observe
420     // individual preferences, so we check here that the pref that changed
421     // is the exact one we're observing (and not some sub-pref on the branch).
422     if (data != this.prefName) {
423       return;
424     }
426     if (typeof this.callback == "function") {
427       let prefValue = Preferences.get(this.prefName);
429       if (this.thisObject) {
430         this.callback.call(this.thisObject, prefValue);
431       } else {
432         this.callback(prefValue);
433       }
434     } else {
435       // typeof this.callback == "object" (nsIObserver)
436       this.callback.observe(subject, topic, data);
437     }
438   },
441 function isObject(val) {
442   // We can't check for |val.constructor == Object| here, since the value
443   // might be from a different context whose Object constructor is not the same
444   // as ours, so instead we match based on the name of the constructor.
445   return (
446     typeof val != "undefined" &&
447     val != null &&
448     typeof val == "object" &&
449     val.constructor.name == "Object"
450   );