Browse Source

support observing pref branches

Myk Melez 15 years ago
parent
commit
f836243004
2 changed files with 171 additions and 0 deletions
  1. 112 0
      Preferences.js
  2. 59 0
      test/unit/test_Preferences.js

+ 112 - 0
Preferences.js

@@ -42,6 +42,8 @@ const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
 function Preferences(prefBranch) {
   if (prefBranch)
     this._prefBranch = prefBranch;
@@ -152,12 +154,102 @@ Preferences.prototype = {
     }
   },
 
+
+  /**
+   * A cache of preference branch observers.
+   *
+   * We use this to remove observers when a caller calls |remove|.
+   *
+   * XXX This might result in reference cycles, causing memory leaks,
+   * if we hold a reference to an observer that holds a reference to us.
+   * Could we fix that by making this an independent top-level object
+   * rather than a property of the Preferences prototype?
+   *
+   * Note: all Preferences instances share this object, since all of them
+   * have the same prototype.  This is intentional, because we want callers
+   * to be able to remove an observer using a different Preferences object
+   * than the one with which they added it.  But it means we have to index
+   * the observers in this object by their complete pref branch, not just
+   * the branch relative to the root branch of any given Preferences object.
+   */
+  _observers: [],
+
+  /**
+   * Observe a pref branch.  The callback can be a function, a method
+   * (when thisObject is provided), or any object that implements nsIObserver.
+   * The pref branch can be any string and is appended to the root branch
+   * for the Preferences instance on which this method is called.
+   *
+   * For example, if the Preferences instance has root branch "foo.",
+   * and this method is called with branch "bar.", then the callback
+   * will observe the complete branch "foo.bar.". If the Preferences instance
+   * has the root branch "", and this method is called with branch "",
+   * then the callback will observe changes to all preferences.
+   *
+   * @param   branch      {String}  [optional]
+   *          the branch to observe
+   *
+   * @param   callback    {Object}
+   *          the callback to call when a pref on the branch changes;
+   *          a Function or an Object that implements nsIObserver
+   *
+   * @param   thisObject  {Object}  [optional]
+   *          the object to use as |this| when calling a Function callback;
+   *          allows the callback to behave like a method when observing changes
+   *
+   * @returns the wrapped observer
+   */
+  observe: function(branch, callback, thisObject) {
+    let fullBranch = this._prefBranch + (branch || "");
+
+    let observer = new PrefObserver(fullBranch, callback, thisObject);
+    Preferences._prefSvc.addObserver(fullBranch, observer, true);
+    Preferences._observers.push(observer);
+
+    return observer;
+  },
+
+  /**
+   * Stop observing a pref branch.  This method must be called with the same
+   * branch, callback, and thisObject with which the observer was originally
+   * registered.  However, you don't have to call this method on the same
+   * exact instance of Preferences.  You can call it on any instance.
+   *
+   * @param   branch      {String}  [optional]
+   *          the branch being observed
+   *
+   * @param   callback    {Object}
+   *          the callback doing the observing
+   *
+   * @param   thisObject  {Object}  [optional]
+   *          the object being used as |this| when calling a Function callback
+   */
+  ignore: function(branch, callback, thisObject) {
+    let fullBranch = this._prefBranch + (branch || "");
+
+    // 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] =
+      Preferences._observers.filter(function(v) v.branch     == fullBranch &&
+                                                v.callback   == callback &&
+                                                v.thisObject == thisObject);
+
+    if (observer) {
+      Preferences._prefSvc.removeObserver(fullBranch, observer);
+      Preferences._observers.splice(Preferences._observers.indexOf(observer), 1);
+    }
+  },
+
+
   // FIXME: make the methods below accept an array of pref names.
 
   has: function(prefName) {
     return (this._prefSvc.getPrefType(prefName) != Ci.nsIPrefBranch.PREF_INVALID);
   },
 
+  // FIXME: change this to isSet (for consistency with set and reset).
   modified: function(prefName) {
     return (this.has(prefName) && this._prefSvc.prefHasUserValue(prefName));
   },
@@ -195,6 +287,26 @@ Preferences.prototype = {
 // first.
 Preferences.__proto__ = Preferences.prototype;
 
+function PrefObserver(branch, callback, thisObject) {
+  this.branch = branch;
+  this.callback = callback;
+  this.thisObject = thisObject;
+}
+
+PrefObserver.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+  observe: function(subject, topic, data) {
+    if (typeof this.callback == "function") {
+      if (this.thisObject)
+        this.callback.call(this.thisObject);
+      else
+        this.callback();
+    }
+    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

+ 59 - 0
test/unit/test_Preferences.js

@@ -105,3 +105,62 @@ function test_reset_nonexistent_pref() {
 function test_reset_nonexistent_pref_branch() {
   Preferences.resetBranch("test_reset_nonexistent_pref_branch.");
 }
+
+function test_observe_prefs_function() {
+  let observed = false;
+  let observer = function() { observed = !observed };
+
+  Preferences.observe("test_observe_prefs_function", observer);
+  Preferences.set("test_observe_prefs_function", "something");
+  do_check_true(observed);
+
+  Preferences.ignore("test_observe_prefs_function", observer);
+  Preferences.set("test_observe_prefs_function", "something else");
+  do_check_true(observed);
+
+  // Clean up.
+  Preferences.reset("test_observe_prefs_function");
+}
+
+function test_observe_prefs_object() {
+  let observer = {
+    observed: false,
+    observe: function() {
+      this.observed = !this.observed;
+    }
+  };
+
+  Preferences.observe("test_observe_prefs_object", observer.observe, observer);
+  Preferences.set("test_observe_prefs_object", "something");
+  do_check_true(observer.observed);
+
+  Preferences.ignore("test_observe_prefs_object", observer.observe, observer);
+  Preferences.set("test_observe_prefs_object", "something else");
+  do_check_true(observer.observed);
+
+  // Clean up.
+  Preferences.reset("test_observe_prefs_object");
+}
+
+function test_observe_prefs_nsIObserver() {
+  let observer = {
+    observed: false,
+    observe: function(subject, topic, data) {
+      this.observed = !this.observed;
+      do_check_true(subject instanceof Ci.nsIPrefBranch2);
+      do_check_eq(topic, "nsPref:changed");
+      do_check_eq(data, "test_observe_prefs_nsIObserver");
+    }
+  };
+
+  Preferences.observe("test_observe_prefs_nsIObserver", observer);
+  Preferences.set("test_observe_prefs_nsIObserver", "something");
+  do_check_true(observer.observed);
+
+  Preferences.ignore("test_observe_prefs_nsIObserver", observer);
+  Preferences.set("test_observe_prefs_nsIObserver", "something else");
+  do_check_true(observer.observed);
+
+  // Clean up.
+  Preferences.reset("test_observe_prefs_nsIObserver");
+}