You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
629 lines
22 KiB
629 lines
22 KiB
/* ***** BEGIN LICENSE BLOCK ***** |
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1 |
|
* |
|
* The contents of this file are subject to the Mozilla Public License Version |
|
* 1.1 (the "License"); you may not use this file except in compliance with |
|
* the License. You may obtain a copy of the License at |
|
* http://www.mozilla.org/MPL/ |
|
* |
|
* Software distributed under the License is distributed on an "AS IS" basis, |
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License |
|
* for the specific language governing rights and limitations under the |
|
* License. |
|
* |
|
* The Original Code is Preferences. |
|
* |
|
* The Initial Developer of the Original Code is Mozilla. |
|
* Portions created by the Initial Developer are Copyright (C) 2008 |
|
* the Initial Developer. All Rights Reserved. |
|
* |
|
* Contributor(s): |
|
* Myk Melez <myk@mozilla.org> |
|
* Daniel Aquino <mr.danielaquino@gmail.com> |
|
* |
|
* Alternatively, the contents of this file may be used under the terms of |
|
* either the GNU General Public License Version 2 or later (the "GPL"), or |
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), |
|
* in which case the provisions of the GPL or the LGPL are applicable instead |
|
* of those above. If you wish to allow use of your version of this file only |
|
* under the terms of either the GPL or the LGPL, and not to allow others to |
|
* use your version of this file under the terms of the MPL, indicate your |
|
* decision by deleting the provisions above and replace them with the notice |
|
* and other provisions required by the GPL or the LGPL. If you do not delete |
|
* the provisions above, a recipient may use your version of this file under |
|
* the terms of any one of the MPL, the GPL or the LGPL. |
|
* |
|
* ***** END LICENSE BLOCK ***** */ |
|
|
|
let EXPORTED_SYMBOLS = ["Preferences"]; |
|
|
|
const Cc = Components.classes; |
|
const Ci = Components.interfaces; |
|
const Cr = Components.results; |
|
const Cu = Components.utils; |
|
|
|
Cu.import("resource://gre/modules/Services.jsm"); |
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
|
|
// The minimum and maximum integers that can be set as preferences. |
|
// The range of valid values is narrower than the range of valid JS values |
|
// because the native preferences code treats integers as NSPR PRInt32s, |
|
// which are 32-bit signed integers on all platforms. |
|
const MAX_INT = Math.pow(2, 31) - 1; |
|
const MIN_INT = -MAX_INT; |
|
|
|
function Preferences(args) { |
|
if (isObject(args)) { |
|
if (args.branch) |
|
this._prefBranch = args.branch; |
|
} |
|
else if (args) |
|
this._prefBranch = args; |
|
this.isDefaultBranch = false; |
|
} |
|
|
|
Preferences.prototype = { |
|
/** |
|
* Get the value of a pref, if any; otherwise return the default value. |
|
* |
|
* @param prefName {String|Array} |
|
* the pref to get, or an array of prefs to get |
|
* |
|
* @param defaultValue |
|
* the default value, if any, for prefs that don't have one |
|
* |
|
* @returns the value of the pref, if any; otherwise the default value |
|
*/ |
|
get: function(prefName, defaultValue) { |
|
if (isArray(prefName)) |
|
return prefName.map(v => this.get(v, defaultValue)); |
|
|
|
return this._get(prefName, defaultValue); |
|
}, |
|
|
|
// In all cases below, the preference might exist as a user pref, but not |
|
// have a default value. In those cases, get* throws. Return the default value. |
|
_get: function(prefName, defaultValue) { |
|
switch (this._prefSvc.getPrefType(prefName)) { |
|
case Ci.nsIPrefBranch.PREF_STRING: |
|
try { |
|
return this._prefSvc.getComplexValue(prefName, Ci.nsISupportsString).data; |
|
} catch (ex) { |
|
if (this.isDefaultBranch) |
|
return defaultValue; |
|
else |
|
return this._prefSvc.getCharPref(prefName); |
|
} |
|
|
|
case Ci.nsIPrefBranch.PREF_INT: |
|
try { |
|
return this._prefSvc.getIntPref(prefName); |
|
} catch (ex) { |
|
return defaultValue; |
|
} |
|
|
|
case Ci.nsIPrefBranch.PREF_BOOL: |
|
try { |
|
return this._prefSvc.getBoolPref(prefName); |
|
} catch (ex) { |
|
return defaultValue; |
|
} |
|
|
|
case Ci.nsIPrefBranch.PREF_INVALID: |
|
return defaultValue; |
|
|
|
default: |
|
// This should never happen. |
|
throw "Error getting pref " + prefName + "; its value's type is " + |
|
this._prefSvc.getPrefType(prefName) + ", which I don't know " + |
|
"how to handle."; |
|
} |
|
}, |
|
|
|
/** |
|
* Set a preference to a value. |
|
* |
|
* You can set multiple prefs by passing an object as the only parameter. |
|
* In that case, this method will treat the properties of the object |
|
* as preferences to set, where each property name is the name of a pref |
|
* and its corresponding property value is the value of the pref. |
|
* |
|
* @param prefName {String|Object} |
|
* the name of the pref to set; or an object containing a set |
|
* of prefs to set |
|
* |
|
* @param prefValue {String|Number|Boolean} |
|
* the value to which to set the pref |
|
* |
|
* Note: Preferences cannot store non-integer numbers or numbers outside |
|
* the signed 32-bit range -(2^31-1) to 2^31-1, If you have such a number, |
|
* store it as a string by calling toString() on the number before passing |
|
* it to this method, i.e.: |
|
* Preferences.set("pi", 3.14159.toString()) |
|
* Preferences.set("big", Math.pow(2, 31).toString()). |
|
*/ |
|
set: function(prefName, prefValue) { |
|
if (isObject(prefName)) { |
|
for (let [name, value] in Iterator(prefName)) |
|
this.set(name, value); |
|
return; |
|
} |
|
|
|
this._set(prefName, prefValue); |
|
}, |
|
|
|
_set: function(prefName, prefValue) { |
|
let prefType; |
|
if (typeof prefValue != "undefined" && prefValue != null) |
|
prefType = prefValue.constructor.name; |
|
|
|
var existingPrefType = this._prefSvc.getPrefType(prefName); |
|
if (existingPrefType != Ci.nsIPrefBranch.PREF_INVALID) |
|
{ |
|
// convert |
|
if (existingPrefType == Ci.nsIPrefBranch.PREF_INT && prefType == "String") |
|
{ |
|
prefValue = parseInt(prefValue); |
|
if (isNaN(prefValue)) |
|
throw "Incompatible pref value type - " + prefName; |
|
prefType = "Number"; |
|
} |
|
else if (existingPrefType == Ci.nsIPrefBranch.PREF_BOOL && prefType == "String") |
|
{ |
|
if (prefValue == "true") |
|
prefValue = true; |
|
else if (prefValue == "false") |
|
prefValue = false; |
|
else |
|
throw "Incompatible pref value type - " + prefName; |
|
prefType = "Boolean"; |
|
} |
|
else if (existingPrefType == Ci.nsIPrefBranch.PREF_BOOL && prefType == "Number") |
|
{ |
|
prefValue = prefValue != 0; |
|
prefType = "Boolean"; |
|
} |
|
} |
|
|
|
switch (prefType) { |
|
case "String": |
|
{ |
|
try { |
|
this._prefSvc.setStringPref(prefName, prefValue); |
|
} catch (e) { |
|
try { |
|
let string = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); |
|
string.data = prefValue; |
|
this._prefSvc.setComplexValue(prefName, Ci.nsISupportsString, string); |
|
} catch (e2) { |
|
Components.utils.reportError(e2); |
|
} |
|
} |
|
} |
|
break; |
|
|
|
case "Number": |
|
// We throw if the number is outside the range, since the result |
|
// will never be what the consumer wanted to store, but we only warn |
|
// if the number is non-integer, since the consumer might not mind |
|
// the loss of precision. |
|
if (prefValue > MAX_INT || prefValue < MIN_INT) |
|
throw("you cannot set the " + prefName + " pref to the number " + |
|
prefValue + ", as number pref values must be in the signed " + |
|
"32-bit integer range -(2^31-1) to 2^31-1. To store numbers " + |
|
"outside that range, store them as strings."); |
|
try { |
|
this._prefSvc.setIntPref(prefName, prefValue); |
|
} catch (e) { |
|
throw new Error(e.toString() + " - " + prefName); |
|
} |
|
if (prefValue % 1 != 0) |
|
Cu.reportError("Warning: setting the " + prefName + " pref to the " + |
|
"non-integer number " + prefValue + " converted it " + |
|
"to the integer number " + this.get(prefName) + |
|
"; to retain fractional precision, store non-integer " + |
|
"numbers as strings."); |
|
break; |
|
|
|
case "Boolean": |
|
this._prefSvc.setBoolPref(prefName, prefValue); |
|
break; |
|
|
|
default: |
|
throw "can't set pref " + prefName + " to value '" + prefValue + |
|
"'; it isn't a String, Number, or Boolean"; |
|
} |
|
}, |
|
|
|
/** |
|
* Whether or not the given pref has a value. This is different from isSet |
|
* because it returns true whether the value of the pref is a default value |
|
* or a user-set value, while isSet only returns true if the value |
|
* is a user-set value. |
|
* |
|
* @param prefName {String|Array} |
|
* the pref to check, or an array of prefs to check |
|
* |
|
* @returns {Boolean|Array} |
|
* whether or not the pref has a value; or, if the caller provided |
|
* an array of pref names, an array of booleans indicating whether |
|
* or not the prefs have values |
|
*/ |
|
has: function(prefName) { |
|
if (isArray(prefName)) |
|
return prefName.map(this.has, this); |
|
|
|
return this._has(prefName); |
|
}, |
|
|
|
_has: function(prefName) { |
|
return (this._prefSvc.getPrefType(prefName) != Ci.nsIPrefBranch.PREF_INVALID); |
|
}, |
|
|
|
/** |
|
* Whether or not the given pref has a user-set value. This is different |
|
* from |has| because it returns true only if the value of the pref is a user- |
|
* set value, while |has| returns true if the value of the pref is a default |
|
* value or a user-set value. |
|
* |
|
* @param prefName {String|Array} |
|
* the pref to check, or an array of prefs to check |
|
* |
|
* @returns {Boolean|Array} |
|
* whether or not the pref has a user-set value; or, if the caller |
|
* provided an array of pref names, an array of booleans indicating |
|
* whether or not the prefs have user-set values |
|
*/ |
|
isSet: function(prefName) { |
|
if (isArray(prefName)) |
|
return prefName.map(this.isSet, this); |
|
|
|
return (this.has(prefName) && this._prefSvc.prefHasUserValue(prefName)); |
|
}, |
|
|
|
/** |
|
* Whether or not the given pref has a user-set value. Use isSet instead, |
|
* which is equivalent. |
|
* @deprecated |
|
*/ |
|
modified: function(prefName) { return this.isSet(prefName) }, |
|
|
|
reset: function(prefName) { |
|
if (isArray(prefName)) { |
|
prefName.map(v => this.reset(v)); |
|
return; |
|
} |
|
|
|
this._reset(prefName); |
|
}, |
|
|
|
_reset: function(prefName) { |
|
try { |
|
this._prefSvc.clearUserPref(prefName); |
|
} |
|
catch(ex) { |
|
// The pref service throws NS_ERROR_UNEXPECTED when the caller tries |
|
// to reset a pref that doesn't exist or is already set to its default |
|
// value. This interface fails silently in those cases, so callers |
|
// can unconditionally reset a pref without having to check if it needs |
|
// resetting first or trap exceptions after the fact. It passes through |
|
// other exceptions, however, so callers know about them, since we don't |
|
// know what other exceptions might be thrown and what they might mean. |
|
if (ex.result != Cr.NS_ERROR_UNEXPECTED) |
|
throw ex; |
|
} |
|
}, |
|
|
|
/** |
|
* If you need to know the default values, without resetting the actual |
|
* user prefs, you can use this. |
|
* @returns {Preferences} a new Preferences object, which accesses |
|
* the defaults rather than the user prefs. |
|
* *Only* call get() on this. |
|
* If you call set(), you will modify the defaults, so don't do that! |
|
*/ |
|
get defaults() { |
|
// nsIPrefService |
|
let defaultBranch = Services.prefs. |
|
getDefaultBranch(this._prefBranch). |
|
QueryInterface(Ci.nsIPrefBranch); |
|
let prefs = new Preferences(this._prefBranch); |
|
// override. nasty, but this is internal, so OK. |
|
Object.defineProperty(prefs, "_prefSvc", { |
|
get: function() { |
|
return defaultBranch; |
|
} |
|
}); |
|
prefs.isDefaultBranch = true; |
|
return prefs; |
|
}, |
|
|
|
/** |
|
* Lock a pref so it can't be changed. |
|
* |
|
* @param prefName {String|Array} |
|
* the pref to lock, or an array of prefs to lock |
|
* @param prefValue {String} (optional) |
|
* default value of pref to lock only works if prefName isn't an array |
|
*/ |
|
lock: function(prefName, prefValue) { |
|
if (isArray(prefName)) |
|
prefName.map(this.lock, this); |
|
else if (typeof prefValue != "undefined") |
|
this.defaults.set(prefName, prefValue); |
|
|
|
this._prefSvc.lockPref(prefName); |
|
}, |
|
|
|
/** |
|
* Unlock a pref so it can be changed. |
|
* |
|
* @param prefName {String|Array} |
|
* the pref to lock, or an array of prefs to lock |
|
*/ |
|
unlock: function(prefName) { |
|
if (isArray(prefName)) |
|
prefName.map(this.unlock, this); |
|
|
|
this._prefSvc.unlockPref(prefName); |
|
}, |
|
|
|
/** |
|
* Whether or not the given pref is locked against changes and |
|
* if it is set to the passedi n value |
|
* |
|
* @param prefName {String|Array} |
|
* the pref to check, or an array of prefs to check |
|
* @param prefValue {String|Number|Boolean}} |
|
* the pref value to compare against |
|
* |
|
* @returns {Boolean|Array} |
|
* whether or not the pref is locked; or, if the caller |
|
* provided an array of pref names, an array of booleans indicating |
|
* whether or not the prefs are locked |
|
* If a pref value was specified returns whether or not the pref |
|
* was locked and equal to the passed in value. |
|
*/ |
|
locked: function(prefName, prefValue) { |
|
if (isArray(prefName)) |
|
return prefName.map(this.locked, this); |
|
|
|
if (prefValue) |
|
return this._prefSvc.prefIsLocked(prefName) && (this.get(prefName) == prefValue); |
|
else |
|
return this._prefSvc.prefIsLocked(prefName); |
|
}, |
|
|
|
/** |
|
* Start observing a pref. |
|
* |
|
* The callback can be a function or any object that implements nsIObserver. |
|
* When the callback is a function and thisObject is provided, it gets called |
|
* as a method of thisObject. |
|
* |
|
* @param prefName {String} |
|
* the name of the pref to observe |
|
* |
|
* @param callback {Function|Object} |
|
* the code to notify when the pref changes; |
|
* |
|
* @param thisObject {Object} [optional] |
|
* the object to use as |this| when calling a Function callback; |
|
* |
|
* @returns the wrapped observer |
|
*/ |
|
observe: function(prefName, callback, thisObject) { |
|
let fullPrefName = this._prefBranch + (prefName || ""); |
|
|
|
let observer = new PrefObserver(fullPrefName, callback, thisObject); |
|
Preferences._prefSvc.addObserver(fullPrefName, observer, true); |
|
observers.push(observer); |
|
|
|
return observer; |
|
}, |
|
|
|
/** |
|
* Stop observing a pref. |
|
* |
|
* You must call this method with the same prefName, callback, and thisObject |
|
* with which you originally registered the observer. However, you don't have |
|
* to call this method on the same exact instance of Preferences; you can call |
|
* it on any instance. For example, the following code first starts and then |
|
* stops observing the "foo.bar.baz" preference: |
|
* |
|
* let observer = function() {...}; |
|
* Preferences.observe("foo.bar.baz", observer); |
|
* new Preferences("foo.bar.").ignore("baz", observer); |
|
* |
|
* @param prefName {String} |
|
* the name of the pref being observed |
|
* |
|
* @param callback {Function|Object} |
|
* the code being notified when the pref changes |
|
* |
|
* @param thisObject {Object} [optional] |
|
* the object being used as |this| when calling a Function callback |
|
*/ |
|
ignore: function(prefName, callback, thisObject) { |
|
let fullPrefName = this._prefBranch + (prefName || ""); |
|
|
|
// This seems fairly inefficient, but I'm not sure how much better we can |
|
// make it. We could index by fullBranch, but we can't index by callback |
|
// or thisObject, as far as I know, since the keys to JavaScript hashes |
|
// (a.k.a. objects) can apparently only be primitive values. |
|
let [observer] = observers.filter(v => v.prefName == fullPrefName && |
|
v.callback == callback && |
|
v.thisObject == thisObject); |
|
|
|
if (observer) { |
|
Preferences._prefSvc.removeObserver(fullPrefName, observer); |
|
observers.splice(observers.indexOf(observer), 1); |
|
} |
|
}, |
|
|
|
/** |
|
* Same as observe(), but automatically unregisters itself when |
|
* the window closes, saving you from writing an unload handler and |
|
* calling ignore(). |
|
* @param win {nsIDOMWindow} your |window| |
|
*/ |
|
observeAuto: function(win, prefName, callback, thisObject) { |
|
if (!win instanceof Ci.nsIDOMWindow) |
|
throw "Need your |window| as first parameter"; |
|
this.observe(prefName, callback, thisObject); |
|
var self = this; |
|
win.addEventListener("unload", function() |
|
{ |
|
self.ignore(prefName, callback, thisObject); |
|
}, false); |
|
win = null; // don't let closure hold on to window unnecessarily |
|
}, |
|
|
|
resetBranch: function(prefBranch) { |
|
try { |
|
this._prefSvc.resetBranch(prefBranch); |
|
} |
|
catch(ex) { |
|
// The current implementation of nsIPrefBranch in Mozilla |
|
// doesn't implement resetBranch, so we do it ourselves. |
|
if (ex.result == Cr.NS_ERROR_NOT_IMPLEMENTED) |
|
this.reset(this._prefSvc.getChildList(prefBranch, [])); |
|
else |
|
throw ex; |
|
} |
|
}, |
|
|
|
/** |
|
* Returns all child prefs of this pref branch. |
|
* This equals nsIPrefBranch.getChildList(). |
|
* This allows you to do e.g. |
|
* var myPrefs = new Preferences("extensions.cooler."); |
|
* var contents = myPrefs.branch("contents."); |
|
* for each (let prefname in contents.childPrefNames()) |
|
* dump("have " + contents.get(prefname) + " " + prefname + "\n"); |
|
* |
|
* @returns {Array of String} The names of the children, |
|
* without the base pref branch, but with subbranch. |
|
*/ |
|
childPrefNames : function() { |
|
return this._prefSvc.getChildList("", []); |
|
}, |
|
|
|
/** |
|
* Returns an nsIPrefBranch for the pref branch that this object stands for. |
|
* You can use this to use functions that are not supported here. |
|
* @returns {nsIPrefBranch} |
|
*/ |
|
get mozillaPrefBranch() { |
|
return this._prefSvc; |
|
}, |
|
|
|
/** |
|
* Returns the base pref name that this object stands for. |
|
* E.g. "extensions.yourcooler."; |
|
* @returns {String} |
|
*/ |
|
get prefBranchName() { |
|
return this._prefBranch; |
|
}, |
|
|
|
/** |
|
* Returns an Preferences object for an sub pref branch |
|
* underneath the current pref branch. |
|
* @param subbranch {String} Will be appended to the |
|
* current pref branch. Don't forget the trailing dot, |
|
* where necessary. |
|
* E.g. "contents." |
|
* @returns {Preferences} |
|
*/ |
|
branch : function(subbranch) { |
|
return new Preferences(this._prefBranch + subbranch); |
|
}, |
|
|
|
/** |
|
* The branch of the preferences tree to which this instance provides access. |
|
* @private |
|
*/ |
|
_prefBranch: "", |
|
|
|
/** |
|
* Preferences Service |
|
* @private |
|
*/ |
|
get _prefSvc() { |
|
// nsIPrefService |
|
let prefSvc = Services.prefs. |
|
getBranch(this._prefBranch). |
|
QueryInterface(Ci.nsIPrefBranch); |
|
Object.defineProperty(this, "_prefSvc", { |
|
get: function() { |
|
return prefSvc; |
|
} |
|
}); |
|
return this._prefSvc; |
|
} |
|
|
|
}; |
|
|
|
// Give the constructor the same prototype as its instances, so users can access |
|
// preferences directly via the constructor without having to create an instance |
|
// first. |
|
Preferences.__proto__ = Preferences.prototype; |
|
|
|
/** |
|
* A cache of pref observers. |
|
* |
|
* We use this to remove observers when a caller calls Preferences::ignore. |
|
* |
|
* All Preferences instances share this object, because we want callers to be |
|
* able to remove an observer using a different Preferences object than the one |
|
* with which they added it. That means we have to identify the observers |
|
* in this object by their complete pref name, not just their name relative to |
|
* the root branch of the Preferences object with which they were created. |
|
*/ |
|
let observers = []; |
|
|
|
function PrefObserver(prefName, callback, thisObject) { |
|
this.prefName = prefName; |
|
this.callback = callback; |
|
this.thisObject = thisObject; |
|
} |
|
|
|
PrefObserver.prototype = { |
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), |
|
|
|
observe: function(subject, topic, data) { |
|
// The pref service only observes whole branches, but we only observe |
|
// individual preferences, so we check here that the pref that changed |
|
// is the exact one we're observing (and not some sub-pref on the branch). |
|
if (data != this.prefName) |
|
return; |
|
|
|
if (typeof this.callback == "function") { |
|
let prefValue = Preferences.get(this.prefName); |
|
|
|
if (this.thisObject) |
|
this.callback.call(this.thisObject, prefValue); |
|
else |
|
this.callback(prefValue); |
|
} |
|
else // typeof this.callback == "object" (nsIObserver) |
|
this.callback.observe(subject, topic, data); |
|
} |
|
}; |
|
|
|
function isArray(val) { |
|
// We can't check for |val.constructor == Array| here, since the value |
|
// might be from a different context whose Array constructor is not the same |
|
// as ours, so instead we match based on the name of the constructor. |
|
return (typeof val != "undefined" && val != null && typeof val == "object" && |
|
val.constructor.name == "Array"); |
|
} |
|
|
|
function isObject(val) { |
|
// We can't check for |val.constructor == Object| here, since the value |
|
// might be from a different context whose Object constructor is not the same |
|
// as ours, so instead we match based on the name of the constructor. |
|
return (typeof val != "undefined" && val != null && typeof val == "object" && |
|
val.constructor.name == "Object"); |
|
}
|
|
|