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