michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: this.EXPORTED_SYMBOLS = ["Preferences"]; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: // The minimum and maximum integers that can be set as preferences. michael@0: // The range of valid values is narrower than the range of valid JS values michael@0: // because the native preferences code treats integers as NSPR PRInt32s, michael@0: // which are 32-bit signed integers on all platforms. michael@0: const MAX_INT = Math.pow(2, 31) - 1; michael@0: const MIN_INT = -MAX_INT; michael@0: michael@0: this.Preferences = michael@0: function Preferences(args) { michael@0: if (isObject(args)) { michael@0: if (args.branch) michael@0: this._prefBranch = args.branch; michael@0: if (args.defaultBranch) michael@0: this._defaultBranch = args.defaultBranch; michael@0: if (args.privacyContext) michael@0: this._privacyContext = args.privacyContext; michael@0: } michael@0: else if (args) michael@0: this._prefBranch = args; michael@0: } michael@0: michael@0: Preferences.prototype = { michael@0: /** michael@0: * Get the value of a pref, if any; otherwise return the default value. michael@0: * michael@0: * @param prefName {String|Array} michael@0: * the pref to get, or an array of prefs to get michael@0: * michael@0: * @param defaultValue michael@0: * the default value, if any, for prefs that don't have one michael@0: * michael@0: * @returns the value of the pref, if any; otherwise the default value michael@0: */ michael@0: get: function(prefName, defaultValue) { michael@0: if (Array.isArray(prefName)) michael@0: return prefName.map(function(v) this.get(v, defaultValue), this); michael@0: michael@0: return this._get(prefName, defaultValue); michael@0: }, michael@0: michael@0: _get: function(prefName, defaultValue) { michael@0: switch (this._prefSvc.getPrefType(prefName)) { michael@0: case Ci.nsIPrefBranch.PREF_STRING: michael@0: return this._prefSvc.getComplexValue(prefName, Ci.nsISupportsString).data; michael@0: michael@0: case Ci.nsIPrefBranch.PREF_INT: michael@0: return this._prefSvc.getIntPref(prefName); michael@0: michael@0: case Ci.nsIPrefBranch.PREF_BOOL: michael@0: return this._prefSvc.getBoolPref(prefName); michael@0: michael@0: case Ci.nsIPrefBranch.PREF_INVALID: michael@0: return defaultValue; michael@0: michael@0: default: michael@0: // This should never happen. michael@0: throw "Error getting pref " + prefName + "; its value's type is " + michael@0: this._prefSvc.getPrefType(prefName) + ", which I don't know " + michael@0: "how to handle."; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Set a preference to a value. michael@0: * michael@0: * You can set multiple prefs by passing an object as the only parameter. michael@0: * In that case, this method will treat the properties of the object michael@0: * as preferences to set, where each property name is the name of a pref michael@0: * and its corresponding property value is the value of the pref. michael@0: * michael@0: * @param prefName {String|Object} michael@0: * the name of the pref to set; or an object containing a set michael@0: * of prefs to set michael@0: * michael@0: * @param prefValue {String|Number|Boolean} michael@0: * the value to which to set the pref michael@0: * michael@0: * Note: Preferences cannot store non-integer numbers or numbers outside michael@0: * the signed 32-bit range -(2^31-1) to 2^31-1, If you have such a number, michael@0: * store it as a string by calling toString() on the number before passing michael@0: * it to this method, i.e.: michael@0: * Preferences.set("pi", 3.14159.toString()) michael@0: * Preferences.set("big", Math.pow(2, 31).toString()). michael@0: */ michael@0: set: function(prefName, prefValue) { michael@0: if (isObject(prefName)) { michael@0: for (let [name, value] in Iterator(prefName)) michael@0: this.set(name, value); michael@0: return; michael@0: } michael@0: michael@0: this._set(prefName, prefValue); michael@0: }, michael@0: michael@0: _set: function(prefName, prefValue) { michael@0: let prefType; michael@0: if (typeof prefValue != "undefined" && prefValue != null) michael@0: prefType = prefValue.constructor.name; michael@0: michael@0: switch (prefType) { michael@0: case "String": michael@0: { michael@0: let string = Cc["@mozilla.org/supports-string;1"]. michael@0: createInstance(Ci.nsISupportsString); michael@0: string.data = prefValue; michael@0: this._prefSvc.setComplexValue(prefName, Ci.nsISupportsString, string); michael@0: } michael@0: break; michael@0: michael@0: case "Number": michael@0: // We throw if the number is outside the range, since the result michael@0: // will never be what the consumer wanted to store, but we only warn michael@0: // if the number is non-integer, since the consumer might not mind michael@0: // the loss of precision. michael@0: if (prefValue > MAX_INT || prefValue < MIN_INT) michael@0: throw("you cannot set the " + prefName + " pref to the number " + michael@0: prefValue + ", as number pref values must be in the signed " + michael@0: "32-bit integer range -(2^31-1) to 2^31-1. To store numbers " + michael@0: "outside that range, store them as strings."); michael@0: this._prefSvc.setIntPref(prefName, prefValue); michael@0: if (prefValue % 1 != 0) michael@0: Cu.reportError("Warning: setting the " + prefName + " pref to the " + michael@0: "non-integer number " + prefValue + " converted it " + michael@0: "to the integer number " + this.get(prefName) + michael@0: "; to retain fractional precision, store non-integer " + michael@0: "numbers as strings."); michael@0: break; michael@0: michael@0: case "Boolean": michael@0: this._prefSvc.setBoolPref(prefName, prefValue); michael@0: break; michael@0: michael@0: default: michael@0: throw "can't set pref " + prefName + " to value '" + prefValue + michael@0: "'; it isn't a String, Number, or Boolean"; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Whether or not the given pref has a value. This is different from isSet michael@0: * because it returns true whether the value of the pref is a default value michael@0: * or a user-set value, while isSet only returns true if the value michael@0: * is a user-set value. michael@0: * michael@0: * @param prefName {String|Array} michael@0: * the pref to check, or an array of prefs to check michael@0: * michael@0: * @returns {Boolean|Array} michael@0: * whether or not the pref has a value; or, if the caller provided michael@0: * an array of pref names, an array of booleans indicating whether michael@0: * or not the prefs have values michael@0: */ michael@0: has: function(prefName) { michael@0: if (Array.isArray(prefName)) michael@0: return prefName.map(this.has, this); michael@0: michael@0: return this._has(prefName); michael@0: }, michael@0: michael@0: _has: function(prefName) { michael@0: return (this._prefSvc.getPrefType(prefName) != Ci.nsIPrefBranch.PREF_INVALID); michael@0: }, michael@0: michael@0: /** michael@0: * Whether or not the given pref has a user-set value. This is different michael@0: * from |has| because it returns true only if the value of the pref is a user- michael@0: * set value, while |has| returns true if the value of the pref is a default michael@0: * value or a user-set value. michael@0: * michael@0: * @param prefName {String|Array} michael@0: * the pref to check, or an array of prefs to check michael@0: * michael@0: * @returns {Boolean|Array} michael@0: * whether or not the pref has a user-set value; or, if the caller michael@0: * provided an array of pref names, an array of booleans indicating michael@0: * whether or not the prefs have user-set values michael@0: */ michael@0: isSet: function(prefName) { michael@0: if (Array.isArray(prefName)) michael@0: return prefName.map(this.isSet, this); michael@0: michael@0: return (this.has(prefName) && this._prefSvc.prefHasUserValue(prefName)); michael@0: }, michael@0: michael@0: /** michael@0: * Whether or not the given pref has a user-set value. Use isSet instead, michael@0: * which is equivalent. michael@0: * @deprecated michael@0: */ michael@0: modified: function(prefName) { return this.isSet(prefName) }, michael@0: michael@0: reset: function(prefName) { michael@0: if (Array.isArray(prefName)) { michael@0: prefName.map(function(v) this.reset(v), this); michael@0: return; michael@0: } michael@0: michael@0: this._prefSvc.clearUserPref(prefName); michael@0: }, michael@0: michael@0: /** michael@0: * Lock a pref so it can't be changed. michael@0: * michael@0: * @param prefName {String|Array} michael@0: * the pref to lock, or an array of prefs to lock michael@0: */ michael@0: lock: function(prefName) { michael@0: if (Array.isArray(prefName)) michael@0: prefName.map(this.lock, this); michael@0: michael@0: this._prefSvc.lockPref(prefName); michael@0: }, michael@0: michael@0: /** michael@0: * Unlock a pref so it can be changed. michael@0: * michael@0: * @param prefName {String|Array} michael@0: * the pref to lock, or an array of prefs to lock michael@0: */ michael@0: unlock: function(prefName) { michael@0: if (Array.isArray(prefName)) michael@0: prefName.map(this.unlock, this); michael@0: michael@0: this._prefSvc.unlockPref(prefName); michael@0: }, michael@0: michael@0: /** michael@0: * Whether or not the given pref is locked against changes. michael@0: * michael@0: * @param prefName {String|Array} michael@0: * the pref to check, or an array of prefs to check michael@0: * michael@0: * @returns {Boolean|Array} michael@0: * whether or not the pref has a user-set value; or, if the caller michael@0: * provided an array of pref names, an array of booleans indicating michael@0: * whether or not the prefs have user-set values michael@0: */ michael@0: locked: function(prefName) { michael@0: if (Array.isArray(prefName)) michael@0: return prefName.map(this.locked, this); michael@0: michael@0: return this._prefSvc.prefIsLocked(prefName); michael@0: }, michael@0: michael@0: /** michael@0: * Start observing a pref. michael@0: * michael@0: * The callback can be a function or any object that implements nsIObserver. michael@0: * When the callback is a function and thisObject is provided, it gets called michael@0: * as a method of thisObject. michael@0: * michael@0: * @param prefName {String} michael@0: * the name of the pref to observe michael@0: * michael@0: * @param callback {Function|Object} michael@0: * the code to notify when the pref changes; michael@0: * michael@0: * @param thisObject {Object} [optional] michael@0: * the object to use as |this| when calling a Function callback; michael@0: * michael@0: * @returns the wrapped observer michael@0: */ michael@0: observe: function(prefName, callback, thisObject) { michael@0: let fullPrefName = this._prefBranch + (prefName || ""); michael@0: michael@0: let observer = new PrefObserver(fullPrefName, callback, thisObject); michael@0: Preferences._prefSvc.addObserver(fullPrefName, observer, true); michael@0: observers.push(observer); michael@0: michael@0: return observer; michael@0: }, michael@0: michael@0: /** michael@0: * Stop observing a pref. michael@0: * michael@0: * You must call this method with the same prefName, callback, and thisObject michael@0: * with which you originally registered the observer. However, you don't have michael@0: * to call this method on the same exact instance of Preferences; you can call michael@0: * it on any instance. For example, the following code first starts and then michael@0: * stops observing the "foo.bar.baz" preference: michael@0: * michael@0: * let observer = function() {...}; michael@0: * Preferences.observe("foo.bar.baz", observer); michael@0: * new Preferences("foo.bar.").ignore("baz", observer); michael@0: * michael@0: * @param prefName {String} michael@0: * the name of the pref being observed michael@0: * michael@0: * @param callback {Function|Object} michael@0: * the code being notified when the pref changes michael@0: * michael@0: * @param thisObject {Object} [optional] michael@0: * the object being used as |this| when calling a Function callback michael@0: */ michael@0: ignore: function(prefName, callback, thisObject) { michael@0: let fullPrefName = this._prefBranch + (prefName || ""); michael@0: michael@0: // This seems fairly inefficient, but I'm not sure how much better we can michael@0: // make it. We could index by fullBranch, but we can't index by callback michael@0: // or thisObject, as far as I know, since the keys to JavaScript hashes michael@0: // (a.k.a. objects) can apparently only be primitive values. michael@0: let [observer] = observers.filter(function(v) v.prefName == fullPrefName && michael@0: v.callback == callback && michael@0: v.thisObject == thisObject); michael@0: michael@0: if (observer) { michael@0: Preferences._prefSvc.removeObserver(fullPrefName, observer); michael@0: observers.splice(observers.indexOf(observer), 1); michael@0: } michael@0: }, michael@0: michael@0: resetBranch: function(prefBranch = "") { michael@0: try { michael@0: this._prefSvc.resetBranch(prefBranch); michael@0: } michael@0: catch(ex) { michael@0: // The current implementation of nsIPrefBranch in Mozilla michael@0: // doesn't implement resetBranch, so we do it ourselves. michael@0: if (ex.result == Cr.NS_ERROR_NOT_IMPLEMENTED) michael@0: this.reset(this._prefSvc.getChildList(prefBranch, [])); michael@0: else michael@0: throw ex; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The branch of the preferences tree to which this instance provides access. michael@0: * @private michael@0: */ michael@0: _prefBranch: "", michael@0: michael@0: /** michael@0: * Preferences Service michael@0: * @private michael@0: */ michael@0: get _prefSvc() { michael@0: let prefSvc = Cc["@mozilla.org/preferences-service;1"] michael@0: .getService(Ci.nsIPrefService); michael@0: if (this._defaultBranch) { michael@0: prefSvc = prefSvc.getDefaultBranch(this._prefBranch); michael@0: } else { michael@0: prefSvc = prefSvc.getBranch(this._prefBranch); michael@0: } michael@0: michael@0: this.__defineGetter__("_prefSvc", function() prefSvc); michael@0: return this._prefSvc; michael@0: }, michael@0: michael@0: /** michael@0: * IO Service michael@0: * @private michael@0: */ michael@0: get _ioSvc() { michael@0: let ioSvc = Cc["@mozilla.org/network/io-service;1"]. michael@0: getService(Ci.nsIIOService); michael@0: this.__defineGetter__("_ioSvc", function() ioSvc); michael@0: return this._ioSvc; michael@0: } michael@0: michael@0: }; michael@0: michael@0: // Give the constructor the same prototype as its instances, so users can access michael@0: // preferences directly via the constructor without having to create an instance michael@0: // first. michael@0: Preferences.__proto__ = Preferences.prototype; michael@0: michael@0: /** michael@0: * A cache of pref observers. michael@0: * michael@0: * We use this to remove observers when a caller calls Preferences::ignore. michael@0: * michael@0: * All Preferences instances share this object, because we want callers to be michael@0: * able to remove an observer using a different Preferences object than the one michael@0: * with which they added it. That means we have to identify the observers michael@0: * in this object by their complete pref name, not just their name relative to michael@0: * the root branch of the Preferences object with which they were created. michael@0: */ michael@0: let observers = []; michael@0: michael@0: function PrefObserver(prefName, callback, thisObject) { michael@0: this.prefName = prefName; michael@0: this.callback = callback; michael@0: this.thisObject = thisObject; michael@0: } michael@0: michael@0: PrefObserver.prototype = { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), michael@0: michael@0: observe: function(subject, topic, data) { michael@0: // The pref service only observes whole branches, but we only observe michael@0: // individual preferences, so we check here that the pref that changed michael@0: // is the exact one we're observing (and not some sub-pref on the branch). michael@0: if (data.indexOf(this.prefName) != 0) michael@0: return; michael@0: michael@0: if (typeof this.callback == "function") { michael@0: let prefValue = Preferences.get(data); michael@0: michael@0: if (this.thisObject) michael@0: this.callback.call(this.thisObject, prefValue); michael@0: else michael@0: this.callback(prefValue); michael@0: } michael@0: else // typeof this.callback == "object" (nsIObserver) michael@0: this.callback.observe(subject, topic, data); michael@0: } michael@0: }; michael@0: michael@0: function isObject(val) { michael@0: // We can't check for |val.constructor == Object| here, since the value michael@0: // might be from a different context whose Object constructor is not the same michael@0: // as ours, so instead we match based on the name of the constructor. michael@0: return (typeof val != "undefined" && val != null && typeof val == "object" && michael@0: val.constructor.name == "Object"); michael@0: }