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