]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/prefs.jsm
Import 1.0 supporting Firefox up to 14.*
[dactyl.git] / common / modules / prefs.jsm
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 /* use strict */
8
9 try {
10
11 Components.utils.import("resource://dactyl/bootstrap.jsm");
12 defineModule("prefs", {
13     exports: ["Prefs", "localPrefs", "prefs"],
14     require: ["services", "util"]
15 }, this);
16
17 this.lazyRequire("messages", ["_"]);
18
19 var Prefs = Module("prefs", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
20     ORIGINAL: "extensions.dactyl.original.",
21     RESTORE: "extensions.dactyl.restore.",
22     SAVED: "extensions.dactyl.saved.",
23     INIT: {},
24
25     init: function init(branch, defaults) {
26         this._prefContexts = [];
27
28         this.branch = services.pref[defaults ? "getDefaultBranch" : "getBranch"](branch || "");
29         if ("nsIPrefBranch2" in Ci)
30             this.branch instanceof Ci.nsIPrefBranch2;
31
32         this.defaults = defaults ? this : this.constructor(branch, true);
33
34         this.branches = memoize({
35             __proto__: this,
36             get original() this.constructor(this.ORIGINAL + this.root),
37             get restore() this.constructor(this.RESTORE + this.root),
38             get saved() this.constructor(this.SAVED + this.root),
39         });
40
41         if (!defaults)
42             this.restore();
43
44         this._observers = {};
45     },
46
47     cleanup: function cleanup(reason) {
48         if (this.defaults != this)
49             this.defaults.cleanup(reason);
50
51         this._observers = {};
52         if (this.observe) {
53             this.branch.removeObserver("", this);
54             this.observe.unregister();
55             delete this.observe;
56         }
57
58         if (this == prefs) {
59             if (~["uninstall", "disable"].indexOf(reason)) {
60                 for (let name in values(this.branches.saved.getNames()))
61                     this.safeReset(name, null, true);
62
63                 this.branches.original.resetBranch();
64                 this.branches.saved.resetBranch();
65             }
66
67             if (reason == "uninstall")
68                 localPrefs.resetBranch();
69         }
70     },
71
72     /**
73      * Returns a new Prefs instance for the sub-branch *branch* of this
74      * branch.
75      *
76      * @param {string} branch The sub-branch to branch to.
77      * @returns {Prefs}
78      */
79     Branch: function Branch(branch) Prefs(this.root + branch),
80
81     /**
82      * Clears the entire branch.
83      *
84      * @param {string} name The name of the preference branch to delete.
85      */
86     clear: function clear(branch) {
87         this.branch.deleteBranch(branch || "");
88     },
89
90     /**
91      * Returns the full name of this object's preference branch.
92      */
93     get root() this.branch.root,
94
95     /**
96      * Returns the value of the preference *name*, or *defaultValue* if
97      * the preference does not exist.
98      *
99      * @param {string} name The name of the preference to return.
100      * @param {*} defaultValue The value to return if the preference has no value.
101      * @optional
102      */
103     get: function get(name, defaultValue) {
104         if (defaultValue == null)
105             defaultValue = null;
106         if (isArray(name))
107             name = name.join(".");
108
109         let type = this.branch.getPrefType(name);
110         try {
111             switch (type) {
112             case Ci.nsIPrefBranch.PREF_STRING:
113                 let value = this.branch.getComplexValue(name, Ci.nsISupportsString).data;
114                 // try in case it's a localized string (will throw an exception if not)
115                 if (!this.branch.prefIsLocked(name) && !this.branch.prefHasUserValue(name) &&
116                     RegExp("chrome://.+/locale/.+\\.properties").test(value))
117                         value = this.branch.getComplexValue(name, Ci.nsIPrefLocalizedString).data;
118                 return value;
119             case Ci.nsIPrefBranch.PREF_INT:
120                 return this.branch.getIntPref(name);
121             case Ci.nsIPrefBranch.PREF_BOOL:
122                 return this.branch.getBoolPref(name);
123             default:
124                 return defaultValue;
125             }
126         }
127         catch (e) {
128             return defaultValue;
129         }
130     },
131
132     getDefault: deprecated("Prefs#defaults.get", function getDefault(name, defaultValue) this.defaults.get(name, defaultValue)),
133
134     /**
135      * Returns an array of all preference names in this branch or the
136      * given sub-branch.
137      *
138      * @param {string} branch The sub-branch for which to return preferences.
139      * @optional
140      */
141     getNames: function getNames(branch) this.branch.getChildList(branch || "", { value: 0 }),
142
143     /**
144      * Returns true if the given preference exists in this branch.
145      *
146      * @param {string} name The name of the preference to check.
147      */
148     has: function has(name) this.branch.getPrefType(name) !== 0,
149
150     /**
151      * Returns true if the given preference is set to its default value.
152      *
153      * @param {string} name The name of the preference to check.
154      */
155     isDefault: function isDefault(name) !this.branch.prefHasUserValue(name),
156
157     _checkSafe: function _checkSafe(name, message, value) {
158         let curval = this.get(name, null);
159
160         if (this.branches.original.get(name) == null && !this.branches.saved.has(name))
161             this.branches.original.set(name, curval, true);
162
163         if (arguments.length > 2 && curval === value)
164             return;
165
166         let defval = this.defaults.get(name, null);
167         let saved  = this.branches.saved.get(name);
168
169         if (saved == null && curval != defval || saved != null && curval != saved) {
170             let msg = _("pref.safeSet.warnChanged", name);
171             if (message)
172                 msg = template.linkifyHelp(msg + " " + message);
173             util.dactyl.warn(msg);
174         }
175     },
176
177     /**
178      * Resets the preference *name* to *value* but warns the user if the value
179      * is changed from its default.
180      *
181      * @param {string} name The preference name.
182      * @param {value} value The new preference value.
183      * @param {boolean} silent Ignore errors.
184      */
185     safeReset: function safeReset(name, message, silent) {
186         this._checkSafe(name, message);
187         this.set(name, this.branches.original.get(name), silent);
188         this.branches.original.reset(name);
189         this.branches.saved.reset(name);
190     },
191
192     /**
193      * Sets the preference *name* to *value* but warns the user if the value is
194      * changed from its default.
195      *
196      * @param {string} name The preference name.
197      * @param {value} value The new preference value.
198      */
199     safeSet: function safeSet(name, value, message, skipSave) {
200         this._checkSafe(name, message, value);
201         this.set(name, value);
202         this.branches.saved[skipSave ? "reset" : "set"](name, value);
203     },
204
205     /**
206      * Sets the preference *name* to *value*. If the preference already
207      * exists, it must have the same type as the given value.
208      *
209      * @param {name} name The name of the preference to change.
210      * @param {string|number|boolean} value The value to set.
211      * @param {boolean} silent Ignore errors.
212      */
213     set: function set(name, value, silent) {
214         if (this._prefContexts.length)
215             this._prefContexts[this._prefContexts.length - 1][name] = this.get(name, null);
216
217         function assertType(needType)
218             util.assert(type === Ci.nsIPrefBranch.PREF_INVALID || type === needType,
219                 type === Ci.nsIPrefBranch.PREF_INT
220                                 ? /*L*/"E521: Number required after =: " + name + "=" + value
221                                 : /*L*/"E474: Invalid argument: " + name + "=" + value);
222
223         let type = this.branch.getPrefType(name);
224         try {
225             switch (typeof value) {
226             case "string":
227                 assertType(Ci.nsIPrefBranch.PREF_STRING);
228
229                 this.branch.setComplexValue(name, Ci.nsISupportsString, services.String(value));
230                 break;
231             case "number":
232                 assertType(Ci.nsIPrefBranch.PREF_INT);
233
234                 this.branch.setIntPref(name, value);
235                 break;
236             case "boolean":
237                 assertType(Ci.nsIPrefBranch.PREF_BOOL);
238
239                 this.branch.setBoolPref(name, value);
240                 break;
241             default:
242                 if (value == null && this != this.defaults)
243                     this.reset(name);
244                 else
245                     throw FailedAssertion("Unknown preference type: " + typeof value + " (" + name + "=" + value + ")");
246             }
247         }
248         catch (e if silent) {}
249         return value;
250     },
251
252     /**
253      * Saves the current value of a preference to be restored at next
254      * startup.
255      *
256      * @param {string} name The preference to save.
257      */
258     save: function save(name) {
259         let val = this.get(name);
260         this.set(this.RESTORE + name, val);
261         this.safeSet(name, val);
262     },
263
264     /**
265      * Restores saved preferences in the given branch.
266      *
267      * @param {string} branch The branch from which to restore
268      *      preferences. @optional
269      */
270     restore: function restore(branch) {
271         this.getNames(this.RESTORE + (branch || "")).forEach(function (pref) {
272             this.safeSet(pref.substr(this.RESTORE.length), this.get(pref), null, true);
273             this.reset(pref);
274         }, this);
275     },
276
277     /**
278      * Resets the preference *name* to its default value.
279      *
280      * @param {string} name The name of the preference to reset.
281      */
282     reset: function reset(name) {
283         if (this.branch.prefHasUserValue(name))
284             this.branch.clearUserPref(name);
285     },
286
287     /**
288      * Resets the preference branch *branch* to its default value.
289      *
290      * @param {string} branch The preference name. @optional
291      */
292     resetBranch: function resetBranch(branch) {
293         this.getNames(branch).forEach(this.closure.reset);
294     },
295
296     /**
297      * Toggles the value of the boolean preference *name*.
298      *
299      * @param {string} name The preference name.
300      */
301     toggle: function toggle(name) {
302         util.assert(this.branch.getPrefType(name) === Ci.nsIPrefBranch.PREF_BOOL,
303                     _("error.trailingCharacters", name + "!"));
304         this.set(name, !this.get(name));
305     },
306
307     /**
308      * Pushes a new preference context onto the context stack.
309      *
310      * @see #withContext
311      */
312     pushContext: function pushContext() {
313         this._prefContexts.push({});
314     },
315
316     /**
317      * Pops the top preference context from the stack.
318      *
319      * @see #withContext
320      */
321     popContext: function popContext() {
322         for (let [k, v] in Iterator(this._prefContexts.pop()))
323             this.set(k, v);
324     },
325
326     /**
327      * Executes *func* with a new preference context. When *func* returns, the
328      * context is popped and any preferences set via {@link #setPref} or
329      * {@link #invertPref} are restored to their previous values.
330      *
331      * @param {function} func The function to call.
332      * @param {Object} func The 'this' object with which to call *func*
333      * @see #pushContext
334      * @see #popContext
335      */
336     withContext: function withContext(func, self) {
337         try {
338             this.pushContext();
339             return func.call(self);
340         }
341         finally {
342             this.popContext();
343         }
344     },
345
346     observe: null,
347     observers: {
348         "nsPref:changed": function (subject, data) {
349             let observers = this._observers[data];
350             if (observers) {
351                 let value = this.get(data, false);
352                 this._observers[data] = observers.filter(function (callback) {
353                     if (!callback.get())
354                         return false;
355                     util.trapErrors(callback.get(), null, value);
356                     return true;
357                 });
358             }
359         }
360     },
361
362     /**
363      * Adds a new preference observer for the given preference.
364      *
365      * @param {string} pref The preference to observe.
366      * @param {function(object)} callback The callback, called with the
367      *    new value of the preference whenever it changes.
368      */
369     watch: function watch(pref, callback, strong) {
370         if (!this.observe) {
371             util.addObserver(this);
372             this.branch.addObserver("", this, false);
373         }
374
375         if (!this._observers[pref])
376             this._observers[pref] = [];
377         this._observers[pref].push(!strong ? util.weakReference(callback) : { get: function () callback });
378     },
379
380     /**
381      * Lists all preferences matching *filter* or only those with changed
382      * values if *onlyNonDefault* is specified.
383      *
384      * @param {boolean} onlyNonDefault Limit the list to prefs with a
385      *     non-default value.
386      * @param {string} filter The list filter. A null filter lists all
387      *     prefs.
388      * @optional
389      */
390     list: function list(onlyNonDefault, filter) {
391         if (!filter)
392             filter = "";
393
394         let prefArray = this.getNames();
395         prefArray.sort();
396         function prefs() {
397             for (let [, pref] in Iterator(prefArray)) {
398                 let userValue = services.pref.prefHasUserValue(pref);
399                 if (onlyNonDefault && !userValue || pref.indexOf(filter) == -1)
400                     continue;
401
402                 let value = this.get(pref);
403
404                 let option = {
405                     isDefault: !userValue,
406                     default:   this.defaults.get(pref, null),
407                     value:     <>={template.highlight(value, true, 100)}</>,
408                     name:      pref,
409                     pre:       "\u00a0\u00a0" // Unicode nonbreaking space.
410                 };
411
412                 yield option;
413             }
414         };
415
416         return template.options(_("pref.hostPreferences", config.host), prefs.call(this));
417     },
418 }, {
419 }, {
420     completion: function init_completion(dactyl, modules) {
421         modules.completion.preference = function preference(context) {
422             context.anchored = false;
423             context.title = [config.host + " Preference", "Value"];
424             context.keys = { text: function (item) item, description: function (item) prefs.get(item) };
425             context.completions = prefs.getNames();
426         };
427     },
428     javascript: function init_javascript(dactyl, modules) {
429         modules.JavaScript.setCompleter([this.get, this.safeSet, this.set, this.reset, this.toggle],
430                 [function (context) (context.anchored=false, this.getNames().map(function (pref) [pref, ""]))]);
431     }
432 });
433
434 var localPrefs = Prefs("extensions.dactyl.");
435 defineModule.modules.push(localPrefs);
436
437 endModule();
438
439 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
440
441 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: