Bug 1316630 - don't emit pref-changed event on gDevTools; r?jdescottes draft
authorTom Tromey <tom@tromey.com>
Mon, 21 Nov 2016 08:47:10 -0700
changeset 446631 b83c62f99861c02f51caf6e1fd6fa13155239831
parent 446630 3013c1b76d46c62a8bb5c5d0a03b9901044a3228
child 538822 1bb6fdf6b532717871a56de265bc38edae1bdd51
push id37833
push userbmo:ttromey@mozilla.com
push dateThu, 01 Dec 2016 17:04:55 +0000
reviewersjdescottes
bugs1316630
milestone53.0a1
Bug 1316630 - don't emit pref-changed event on gDevTools; r?jdescottes MozReview-Commit-ID: CCqAf8dBFSY
devtools/client/framework/devtools.js
devtools/client/framework/test/browser_toolbox_options.js
devtools/client/framework/toolbox-options.js
devtools/client/framework/toolbox.js
devtools/client/inspector/computed/computed.js
devtools/client/performance/performance-controller.js
devtools/client/shared/autocomplete-popup.js
devtools/client/shared/test/browser_theme.js
devtools/client/shared/theme.js
devtools/client/webaudioeditor/controller.js
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_timestamps.js
devtools/client/webconsole/test/browser_webconsole_expandable_timestamps.js
devtools/client/webconsole/webconsole.js
--- a/devtools/client/framework/devtools.js
+++ b/devtools/client/framework/devtools.js
@@ -313,24 +313,16 @@ DevTools.prototype = {
     // Reset the theme if an extension theme that's currently applied
     // is being removed.
     // Ignore shutdown since addons get disabled during that time.
     if (!Services.startup.shuttingDown &&
         !isCoreTheme &&
         theme.id == currTheme) {
       Services.prefs.setCharPref("devtools.theme", "light");
 
-      let data = {
-        pref: "devtools.theme",
-        newValue: "light",
-        oldValue: currTheme
-      };
-
-      this.emit("pref-changed", data);
-
       this.emit("theme-unregistered", theme);
     }
 
     this._themes.delete(themeId);
   },
 
   /**
    * Get a theme definition if it exists.
--- a/devtools/client/framework/test/browser_toolbox_options.js
+++ b/devtools/client/framework/test/browser_toolbox_options.js
@@ -7,16 +7,17 @@
 "use strict";
 
 // Tests that changing preferences in the options panel updates the prefs
 // and toggles appropriate things in the toolbox.
 
 var doc = null, toolbox = null, panelWin = null, modifiedPrefs = [];
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+const {PrefObserver} = require("devtools/client/shared/prefs");
 
 add_task(function* () {
   const URL = "data:text/html;charset=utf8,test for dynamically registering " +
               "and unregistering tools";
   registerNewTool();
   let tab = yield addTab(URL);
   let target = TargetFactory.forTab(tab);
   toolbox = yield gDevTools.showToolbox(target);
@@ -146,60 +147,63 @@ function* testSelect(select) {
   is(select.options[select.selectedIndex].value, GetPref(pref),
     "select starts out selected");
 
   for (let option of options) {
     if (options.indexOf(option) === select.selectedIndex) {
       continue;
     }
 
+    let observer = new PrefObserver("devtools.");
+
     let deferred = defer();
-    gDevTools.once("pref-changed", (event, data) => {
-      if (data.pref == pref) {
-        ok(true, "Correct pref was changed");
-        is(GetPref(pref), option.value, "Preference been switched for " + pref);
-      } else {
-        ok(false, "Pref " + pref + " was not changed correctly");
-      }
+    let changeSeen = false;
+    observer.once(pref, () => {
+      changeSeen = true;
+      is(GetPref(pref), option.value, "Preference been switched for " + pref);
       deferred.resolve();
     });
 
     select.selectedIndex = options.indexOf(option);
     let changeEvent = new Event("change");
     select.dispatchEvent(changeEvent);
 
     yield deferred.promise;
+
+    ok(changeSeen, "Correct pref was changed");
+    observer.destroy();
   }
 }
 
 function* testMouseClick(node, prefValue) {
   let deferred = defer();
 
+  let observer = new PrefObserver("devtools.");
+
   let pref = node.getAttribute("data-pref");
-  gDevTools.once("pref-changed", (event, data) => {
-    if (data.pref == pref) {
-      ok(true, "Correct pref was changed");
-      is(data.oldValue, prefValue, "Previous value is correct for " + pref);
-      is(data.newValue, !prefValue, "New value is correct for " + pref);
-    } else {
-      ok(false, "Pref " + pref + " was not changed correctly");
-    }
+  let changeSeen = false;
+  observer.once(pref, () => {
+    changeSeen = true;
+    is(GetPref(pref), !prefValue, "New value is correct for " + pref);
     deferred.resolve();
   });
 
   node.scrollIntoView();
 
   // We use executeSoon here to ensure that the element is in view and
   // clickable.
   executeSoon(function () {
     info("Click event synthesized for pref " + pref);
     EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
   });
 
   yield deferred.promise;
+
+  ok(changeSeen, "Correct pref was changed");
+  observer.destroy();
 }
 
 function* testToggleTools() {
   let toolNodes = panelWin.document.querySelectorAll(
     "#default-tools-box input[type=checkbox]:not([data-unsupported])," +
     "#additional-tools-box input[type=checkbox]:not([data-unsupported])");
   let enabledTools = [...toolNodes].filter(node => node.checked);
 
--- a/devtools/client/framework/toolbox-options.js
+++ b/devtools/client/framework/toolbox-options.js
@@ -270,17 +270,17 @@ OptionsPanel.prototype = {
 
     let createThemeOption = theme => {
       let inputLabel = this.panelDoc.createElement("label");
       let inputRadio = this.panelDoc.createElement("input");
       inputRadio.setAttribute("type", "radio");
       inputRadio.setAttribute("value", theme.id);
       inputRadio.setAttribute("name", "devtools-theme-item");
       inputRadio.addEventListener("change", function (e) {
-        setPrefAndEmit(themeBox.getAttribute("data-pref"),
+        SetPref(themeBox.getAttribute("data-pref"),
           e.target.value);
       });
 
       let inputSpanLabel = this.panelDoc.createElement("span");
       inputSpanLabel.textContent = theme.label;
       inputLabel.appendChild(inputRadio);
       inputLabel.appendChild(inputSpanLabel);
 
@@ -300,32 +300,32 @@ OptionsPanel.prototype = {
     let prefCheckboxes = this.panelDoc.querySelectorAll(
       "input[type=checkbox][data-pref]");
     for (let prefCheckbox of prefCheckboxes) {
       if (GetPref(prefCheckbox.getAttribute("data-pref"))) {
         prefCheckbox.setAttribute("checked", true);
       }
       prefCheckbox.addEventListener("change", function (e) {
         let checkbox = e.target;
-        setPrefAndEmit(checkbox.getAttribute("data-pref"), checkbox.checked);
+        SetPref(checkbox.getAttribute("data-pref"), checkbox.checked);
       });
     }
     // Themes radio inputs are handled in setupThemeList
     let prefRadiogroups = this.panelDoc.querySelectorAll(
       ".radiogroup[data-pref]:not(#devtools-theme-box)");
     for (let radioGroup of prefRadiogroups) {
       let selectedValue = GetPref(radioGroup.getAttribute("data-pref"));
 
       for (let radioInput of radioGroup.querySelectorAll("input[type=radio]")) {
         if (radioInput.getAttribute("value") == selectedValue) {
           radioInput.setAttribute("checked", true);
         }
 
         radioInput.addEventListener("change", function (e) {
-          setPrefAndEmit(radioGroup.getAttribute("data-pref"),
+          SetPref(radioGroup.getAttribute("data-pref"),
             e.target.value);
         });
       }
     }
     let prefSelects = this.panelDoc.querySelectorAll("select[data-pref]");
     for (let prefSelect of prefSelects) {
       let pref = GetPref(prefSelect.getAttribute("data-pref"));
       let options = [...prefSelect.options];
@@ -335,17 +335,17 @@ OptionsPanel.prototype = {
         if (value == pref) {
           prefSelect.selectedIndex = options.indexOf(option);
           return true;
         }
       });
 
       prefSelect.addEventListener("change", function (e) {
         let select = e.target;
-        setPrefAndEmit(select.getAttribute("data-pref"),
+        SetPref(select.getAttribute("data-pref"),
           select.options[select.selectedIndex].value);
       });
     }
 
     if (this.target.activeTab) {
       return this.target.client.attachTab(this.target.activeTab._actor)
         .then(([response, client]) => {
           this._origJavascriptEnabled = !response.javascriptEnabled;
@@ -417,22 +417,8 @@ OptionsPanel.prototype = {
       deferred.resolve();
     }
 
     this.panelWin = this.panelDoc = this.disableJSNode = this.toolbox = null;
 
     return this.destroyPromise;
   }
 };
-
-/* Set a pref and emit the pref-changed event if needed. */
-function setPrefAndEmit(prefName, newValue) {
-  let data = {
-    pref: prefName,
-    newValue: newValue
-  };
-  data.oldValue = GetPref(data.pref);
-  SetPref(data.pref, data.newValue);
-
-  if (data.newValue != data.oldValue) {
-    gDevTools.emit("pref-changed", data);
-  }
-}
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -108,17 +108,19 @@ function Toolbox(target, selectedTool, h
   this._toggleAutohide = this._toggleAutohide.bind(this);
   this.showFramesMenu = this.showFramesMenu.bind(this);
   this._updateFrames = this._updateFrames.bind(this);
   this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this);
   this.destroy = this.destroy.bind(this);
   this.highlighterUtils = getHighlighterUtils(this);
   this._highlighterReady = this._highlighterReady.bind(this);
   this._highlighterHidden = this._highlighterHidden.bind(this);
-  this._prefChanged = this._prefChanged.bind(this);
+  this._applyCacheSettings = this._applyCacheSettings.bind(this);
+  this._applyServiceWorkersTestingSettings =
+    this._applyServiceWorkersTestingSettings.bind(this);
   this._saveSplitConsoleHeight = this._saveSplitConsoleHeight.bind(this);
   this._onFocus = this._onFocus.bind(this);
   this._onBrowserMessage = this._onBrowserMessage.bind(this);
   this._showDevEditionPromo = this._showDevEditionPromo.bind(this);
   this._updateTextBoxMenuItems = this._updateTextBoxMenuItems.bind(this);
   this._onBottomHostMinimized = this._onBottomHostMinimized.bind(this);
   this._onBottomHostMaximized = this._onBottomHostMaximized.bind(this);
   this._onToolSelectWhileMinimized = this._onToolSelectWhileMinimized.bind(this);
@@ -362,17 +364,20 @@ Toolbox.prototype = {
       yield domReady.promise;
 
       this.isReady = true;
       let framesPromise = this._listFrames();
 
       this.closeButton = this.doc.getElementById("toolbox-close");
       this.closeButton.addEventListener("click", this.destroy, true);
 
-      gDevTools.on("pref-changed", this._prefChanged);
+      Services.prefs.addObserver("devtools.cache.disabled", this._applyCacheSettings,
+                                false);
+      Services.prefs.addObserver("devtools.serviceWorkers.testing.enabled",
+                                 this._applyServiceWorkersTestingSettings, false);
 
       let framesMenu = this.doc.getElementById("command-button-frames");
       framesMenu.addEventListener("click", this.showFramesMenu, false);
 
       let noautohideMenu = this.doc.getElementById("command-button-noautohide");
       noautohideMenu.addEventListener("click", this._toggleAutohide, true);
 
       this.textBoxContextMenuPopup =
@@ -480,39 +485,16 @@ Toolbox.prototype = {
     this._telemetry.logOncePerBrowserVersion(OS_HISTOGRAM, system.getOSCPU());
     this._telemetry.logOncePerBrowserVersion(OS_IS_64_BITS,
                                              Services.appinfo.is64Bit ? 1 : 0);
     this._telemetry.logOncePerBrowserVersion(SCREENSIZE_HISTOGRAM,
                                              system.getScreenDimensions());
     this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
   },
 
-  /**
-   * Because our panels are lazy loaded this is a good place to watch for
-   * "pref-changed" events.
-   * @param  {String} event
-   *         The event type, "pref-changed".
-   * @param  {Object} data
-   *         {
-   *           newValue: The new value
-   *           oldValue:  The old value
-   *           pref: The name of the preference that has changed
-   *         }
-   */
-  _prefChanged: function (event, data) {
-    switch (data.pref) {
-      case "devtools.cache.disabled":
-        this._applyCacheSettings();
-        break;
-      case "devtools.serviceWorkers.testing.enabled":
-        this._applyServiceWorkersTestingSettings();
-        break;
-    }
-  },
-
   _buildOptions: function () {
     let selectOptions = (name, event) => {
       // Flip back to the last used panel if we are already
       // on the options panel.
       if (this.currentToolId === "options" &&
           gDevTools.getToolDefinition(this.lastUsedToolId)) {
         this.selectTool(this.lastUsedToolId);
       } else {
@@ -2209,17 +2191,19 @@ Toolbox.prototype = {
     this._target.off("frame-update", this._updateFrames);
     this.off("select", this._refreshHostTitle);
     this.off("host-changed", this._refreshHostTitle);
     this.off("ready", this._showDevEditionPromo);
 
     gDevTools.off("tool-registered", this._toolRegistered);
     gDevTools.off("tool-unregistered", this._toolUnregistered);
 
-    gDevTools.off("pref-changed", this._prefChanged);
+    Services.prefs.removeObserver("devtools.cache.disabled", this._applyCacheSettings);
+    Services.prefs.removeObserver("devtools.serviceWorkers.testing.enabled",
+                                  this._applyServiceWorkersTestingSettings);
 
     this._lastFocusedElement = null;
     if (this._sourceMapService) {
       this._sourceMapService.destroy();
       this._sourceMapService = null;
     }
 
     if (this.webconsolePanel) {
--- a/devtools/client/inspector/computed/computed.js
+++ b/devtools/client/inspector/computed/computed.js
@@ -193,24 +193,23 @@ function CssComputedView(inspector, docu
   this.includeBrowserStylesCheckbox.addEventListener("input",
     this._onIncludeBrowserStyles);
 
   this.searchClearButton.hidden = true;
 
   // No results text.
   this.noResults = this.styleDocument.getElementById("computedview-no-results");
 
-  // Refresh panel when color unit changed.
+  // Refresh panel when color unit changed or pref for showing
+  // original sources changes.
   this._handlePrefChange = this._handlePrefChange.bind(this);
-  gDevTools.on("pref-changed", this._handlePrefChange);
-
-  // Refresh panel when pref for showing original sources changes
   this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this);
   this._prefObserver = new PrefObserver("devtools.");
   this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
+  this._prefObserver.on("devtools.defaultColorUnit", this._handlePrefChange);
 
   // The element that we're inspecting, and the document that it comes from.
   this._viewedElement = null;
 
   this.createStyleViews();
 
   this._contextmenu = new StyleInspectorMenu(this, { isRuleView: false });
 
@@ -257,18 +256,17 @@ CssComputedView.prototype = {
     this.pageStyle = pageStyle;
   },
 
   get includeBrowserStyles() {
     return this.includeBrowserStylesCheckbox.checked;
   },
 
   _handlePrefChange: function (event, data) {
-    if (this._computed && (data.pref === "devtools.defaultColorUnit" ||
-        data.pref === PREF_ORIG_SOURCES)) {
+    if (this._computed) {
       this.refreshPanel();
     }
   },
 
   /**
    * Update the view with a new selected element. The CssComputedView panel
    * will show the style information for the given element.
    *
@@ -595,16 +593,17 @@ CssComputedView.prototype = {
   refreshSourceFilter: function () {
     this._matchedProperties = null;
     this._sourceFilter = this.includeBrowserStyles ?
                                  CssLogic.FILTER.UA :
                                  CssLogic.FILTER.USER;
   },
 
   _onSourcePrefChanged: function () {
+    this._handlePrefChange();
     for (let propView of this.propertyViews) {
       propView.updateSourceLinks();
     }
     this.inspector.emit("computed-view-sourcelinks-updated");
   },
 
   /**
    * The CSS as displayed by the UI.
@@ -729,19 +728,18 @@ CssComputedView.prototype = {
 
   /**
    * Destructor for CssComputedView.
    */
   destroy: function () {
     this._viewedElement = null;
     this._outputParser = null;
 
-    gDevTools.off("pref-changed", this._handlePrefChange);
-
     this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
+    this._prefObserver.off("devtools.defaultColorUnit", this._handlePrefChange);
     this._prefObserver.destroy();
 
     // Cancel tree construction
     if (this._createViewsProcess) {
       this._createViewsProcess.cancel();
     }
     if (this._refreshProcess) {
       this._refreshProcess.cancel();
--- a/devtools/client/performance/performance-controller.js
+++ b/devtools/client/performance/performance-controller.js
@@ -11,17 +11,17 @@ var BrowserLoaderModule = {};
 Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
 var { loader, require } = BrowserLoaderModule.BrowserLoader({
   baseURI: "resource://devtools/client/performance/",
   window
 });
 var { Task } = require("devtools/shared/task");
 /* exported Heritage, ViewHelpers, WidgetMethods, setNamedTimeout, clearNamedTimeout */
 var { Heritage, ViewHelpers, WidgetMethods, setNamedTimeout, clearNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
-var { gDevTools } = require("devtools/client/framework/devtools");
+var { PrefObserver } = require("devtools/client/shared/prefs");
 
 // Events emitted by various objects in the panel.
 var EVENTS = require("devtools/client/performance/events");
 Object.defineProperty(this, "EVENTS", {
   value: EVENTS,
   enumerable: true,
   writable: false
 });
@@ -138,17 +138,18 @@ var PerformanceController = {
     PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
     PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
     PerformanceView.on(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
     RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     RecordingsView.on(EVENTS.UI_RECORDING_SELECTED, this._onRecordingSelectFromView);
     DetailsView.on(EVENTS.UI_DETAILS_VIEW_SELECTED, this._pipe);
 
-    gDevTools.on("pref-changed", this._onThemeChanged);
+    this._prefObserver = new PrefObserver("devtools.");
+    this._prefObserver.on("devtools.theme", this._onThemeChanged);
   }),
 
   /**
    * Remove events handled by the PerformanceController
    */
   destroy: function () {
     this._telemetry.destroy();
     this._prefs.off("pref-changed", this._onPrefChanged);
@@ -158,17 +159,18 @@ var PerformanceController = {
     PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
     PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
     PerformanceView.off(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
     RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     RecordingsView.off(EVENTS.UI_RECORDING_SELECTED, this._onRecordingSelectFromView);
     DetailsView.off(EVENTS.UI_DETAILS_VIEW_SELECTED, this._pipe);
 
-    gDevTools.off("pref-changed", this._onThemeChanged);
+    this._prefObserver.off("devtools.theme", this._onThemeChanged);
+    this._prefObserver.destroy();
   },
 
   /**
    * Enables front event listeners.
    *
    * The rationale behind this is given by the async intialization of all the
    * frontend components. Even though the panel is considered "open" only after
    * both the controller and the view are created, and even though their
@@ -397,24 +399,19 @@ var PerformanceController = {
    */
   _onPrefChanged: function (_, prefName, prefValue) {
     this.emit(EVENTS.PREF_CHANGED, prefName, prefValue);
   },
 
   /*
    * Called when the developer tools theme changes.
    */
-  _onThemeChanged: function (_, data) {
-    // Right now, gDevTools only emits `pref-changed` for the theme,
-    // but this could change in the future.
-    if (data.pref !== "devtools.theme") {
-      return;
-    }
-
-    this.emit(EVENTS.THEME_CHANGED, data.newValue);
+  _onThemeChanged: function () {
+    let newValue = Services.prefs.getCharPref("devtools.theme");
+    this.emit(EVENTS.THEME_CHANGED, newValue);
   },
 
   /**
    * Fired from the front on any event. Propagates to other handlers from here.
    */
   _onFrontEvent: function (eventName, ...data) {
     switch (eventName) {
       case "profiler-status":
--- a/devtools/client/shared/autocomplete-popup.js
+++ b/devtools/client/shared/autocomplete-popup.js
@@ -2,19 +2,19 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const Services = require("Services");
-const {gDevTools} = require("devtools/client/framework/devtools");
 const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
 const EventEmitter = require("devtools/shared/event-emitter");
+const {PrefObserver} = require("devtools/client/shared/prefs");
 
 let itemIdCounter = 0;
 /**
  * Autocomplete popup UI implementation.
  *
  * @constructor
  * @param {Document} toolboxDoc
  *        The toolbox document to attach the autocomplete popup panel.
@@ -42,17 +42,19 @@ function AutocompletePopup(toolboxDoc, o
   this.onClickCallback = options.onClick;
 
   // If theme is auto, use the devtools.theme pref
   if (theme === "auto") {
     theme = Services.prefs.getCharPref("devtools.theme");
     this.autoThemeEnabled = true;
     // Setup theme change listener.
     this._handleThemeChange = this._handleThemeChange.bind(this);
-    gDevTools.on("pref-changed", this._handleThemeChange);
+    this._prefObserver = new PrefObserver("devtools.");
+    this._prefObserver.on("devtools.theme", this._handleThemeChange);
+    this._currentTheme = theme;
   }
 
   // Create HTMLTooltip instance
   this._tooltip = new HTMLTooltip(this._document);
   this._tooltip.panel.classList.add(
     "devtools-autocomplete-popup",
     "devtools-monospace",
     theme + "-theme");
@@ -189,17 +191,18 @@ AutocompletePopup.prototype = {
   destroy: function () {
     if (this.isOpen) {
       this.hidePopup();
     }
 
     this._list.removeEventListener("click", this.onClick, false);
 
     if (this.autoThemeEnabled) {
-      gDevTools.off("pref-changed", this._handleThemeChange);
+      this._prefObserver.off("devtools.theme", this._handleThemeChange);
+      this._prefObserver.destroy();
     }
 
     this._list.remove();
     this._listClone.remove();
     this._tooltip.destroy();
     this._document = null;
     this._list = null;
     this._tooltip = null;
@@ -557,35 +560,27 @@ AutocompletePopup.prototype = {
   selectPreviousPageItem: function () {
     let prevPageIndex = this.selectedIndex - this._itemsPerPane - 1;
     this.selectedIndex = Math.max(prevPageIndex, 0);
     return this.selectedItem;
   },
 
   /**
    * Manages theme switching for the popup based on the devtools.theme pref.
-   *
-   * @private
-   *
-   * @param {String} event
-   *        The name of the event. In this case, "pref-changed".
-   * @param {Object} data
-   *        An object passed by the emitter of the event. In this case, the
-   *        object consists of three properties:
-   *        - pref {String} The name of the preference that was modified.
-   *        - newValue {Object} The new value of the preference.
-   *        - oldValue {Object} The old value of the preference.
    */
-  _handleThemeChange: function (event, data) {
-    if (data.pref === "devtools.theme") {
-      this._tooltip.panel.classList.toggle(data.oldValue + "-theme", false);
-      this._tooltip.panel.classList.toggle(data.newValue + "-theme", true);
-      this._list.classList.toggle(data.oldValue + "-theme", false);
-      this._list.classList.toggle(data.newValue + "-theme", true);
-    }
+  _handleThemeChange: function () {
+    const oldValue = this._currentTheme;
+    const newValue = Services.prefs.getCharPref("devtools.theme");
+
+    this._tooltip.panel.classList.toggle(oldValue + "-theme", false);
+    this._tooltip.panel.classList.toggle(newValue + "-theme", true);
+    this._list.classList.toggle(oldValue + "-theme", false);
+    this._list.classList.toggle(newValue + "-theme", true);
+
+    this._currentTheme = newValue;
   },
 
   /**
    * Used by tests.
    */
   get _panel() {
     return this._tooltip.panel;
   },
--- a/devtools/client/shared/test/browser_theme.js
+++ b/devtools/client/shared/test/browser_theme.js
@@ -2,16 +2,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Tests that theme utilities work
 
 const {getColor, getTheme, setTheme} = require("devtools/client/shared/theme");
+const {PrefObserver} = require("devtools/client/shared/prefs");
 
 add_task(function* () {
   testGetTheme();
   testSetTheme();
   testGetColor();
   testColorExistence();
 });
 
@@ -26,37 +27,40 @@ function testGetTheme() {
   is(getTheme(), "firebug", "getTheme() correctly returns firebug theme");
   Services.prefs.setCharPref("devtools.theme", "unknown");
   is(getTheme(), "unknown", "getTheme() correctly returns an unknown theme");
   Services.prefs.setCharPref("devtools.theme", originalTheme);
 }
 
 function testSetTheme() {
   let originalTheme = getTheme();
-  gDevTools.once("pref-changed", (_, { pref, oldValue, newValue }) => {
+
+  let prefObserver = new PrefObserver("devtools.");
+  prefObserver.once("devtools.theme", pref => {
     is(pref, "devtools.theme",
-      "The 'pref-changed' event triggered by setTheme has correct pref.");
-    is(oldValue, originalTheme,
-      "The 'pref-changed' event triggered by setTheme has correct oldValue.");
+      "A preference event triggered by setTheme has correct pref.");
+    let newValue = Services.prefs.getCharPref("devtools.theme");
     is(newValue, "dark",
-      "The 'pref-changed' event triggered by setTheme has correct newValue.");
+      "A preference event triggered by setTheme comes after the value is set.");
   });
   setTheme("dark");
   is(Services.prefs.getCharPref("devtools.theme"), "dark",
      "setTheme() correctly sets dark theme.");
   setTheme("light");
   is(Services.prefs.getCharPref("devtools.theme"), "light",
      "setTheme() correctly sets light theme.");
   setTheme("firebug");
   is(Services.prefs.getCharPref("devtools.theme"), "firebug",
      "setTheme() correctly sets firebug theme.");
   setTheme("unknown");
   is(Services.prefs.getCharPref("devtools.theme"), "unknown",
      "setTheme() correctly sets an unknown theme.");
   Services.prefs.setCharPref("devtools.theme", originalTheme);
+
+  prefObserver.destroy();
 }
 
 function testGetColor() {
   let BLUE_DARK = "#46afe3";
   let BLUE_LIGHT = "#0088cc";
   let BLUE_FIREBUG = "#3455db";
   let originalTheme = getTheme();
 
--- a/devtools/client/shared/theme.js
+++ b/devtools/client/shared/theme.js
@@ -5,17 +5,16 @@
 "use strict";
 
 /**
  * Colors for themes taken from:
  * https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors
  */
 
 const Services = require("Services");
-const { gDevTools } = require("devtools/client/framework/devtools");
 
 const variableFileContents = require("raw!devtools/client/themes/variables.css");
 
 const THEME_SELECTOR_STRINGS = {
   light: ":root.theme-light {",
   dark: ":root.theme-dark {",
   firebug: ":root.theme-firebug {"
 };
@@ -62,23 +61,14 @@ const getColor = exports.getColor = (typ
   let themeFile = getThemeFile(themeName);
   let match = themeFile.match(new RegExp("--theme-" + type + ": (.*);"));
 
   // Return the appropriate variable in the theme, or otherwise, null.
   return match ? match[1] : null;
 };
 
 /**
- * Mimics selecting the theme selector in the toolbox;
- * sets the preference and emits an event on gDevTools to trigger
- * the themeing.
+ * Set the theme preference.
  */
 const setTheme = exports.setTheme = (newTheme) => {
-  let oldTheme = getTheme();
-
   Services.prefs.setCharPref("devtools.theme", newTheme);
-  gDevTools.emit("pref-changed", {
-    pref: "devtools.theme",
-    newValue: newTheme,
-    oldValue: oldTheme
-  });
 };
 /* eslint-enable */
--- a/devtools/client/webaudioeditor/controller.js
+++ b/devtools/client/webaudioeditor/controller.js
@@ -1,12 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+const {PrefObserver} = require("devtools/client/shared/prefs");
+
 /**
  * A collection of `AudioNodeModel`s used throughout the editor
  * to keep track of audio nodes within the audio context.
  */
 var gAudioNodes = new AudioNodesCollection();
 
 /**
  * Initializes the web audio editor views
@@ -53,17 +55,19 @@ var WebAudioEditorController = {
     gFront.on("connect-param", this._onConnectParam);
     gFront.on("disconnect-node", this._onDisconnectNode);
     gFront.on("change-param", this._onChangeParam);
     gFront.on("destroy-node", this._onDestroyNode);
 
     // Hook into theme change so we can change
     // the graph's marker styling, since we can't do this
     // with CSS
-    gDevTools.on("pref-changed", this._onThemeChange);
+
+    this._prefObserver = new PrefObserver("");
+    this._prefObserver.on("devtools.theme", this._onThemeChange);
 
     // Store the AudioNode definitions from the WebAudioFront, if the method exists.
     // If not, get the JSON directly. Using the actor method is preferable so the client
     // knows exactly what methods are supported on the server.
     let actorHasDefinition = yield gTarget.actorHasMethod("webaudio", "getDefinition");
     if (actorHasDefinition) {
       AUDIO_NODE_DEFINITION = yield gFront.getDefinition();
     } else {
@@ -85,17 +89,18 @@ var WebAudioEditorController = {
     gTarget.off("navigate", this._onTabNavigated);
     gFront.off("start-context", this._onStartContext);
     gFront.off("create-node", this._onCreateNode);
     gFront.off("connect-node", this._onConnectNode);
     gFront.off("connect-param", this._onConnectParam);
     gFront.off("disconnect-node", this._onDisconnectNode);
     gFront.off("change-param", this._onChangeParam);
     gFront.off("destroy-node", this._onDestroyNode);
-    gDevTools.off("pref-changed", this._onThemeChange);
+    this._prefObserver.off("devtools.theme", this._onThemeChange);
+    this._prefObserver.destroy();
   },
 
   /**
    * Called when page is reloaded to show the reload notice and waiting
    * for an audio context notice.
    */
   reset: function () {
     $("#content").hidden = true;
@@ -124,18 +129,19 @@ var WebAudioEditorController = {
     return node;
   },
 
   /**
    * Fired when the devtools theme changes (light, dark, etc.)
    * so that the graph can update marker styling, as that
    * cannot currently be done with CSS.
    */
-  _onThemeChange: function (event, data) {
-    window.emit(EVENTS.THEME_CHANGE, data.newValue);
+  _onThemeChange: function () {
+    let newValue = Services.prefs.getCharPref("devtools.theme");
+    window.emit(EVENTS.THEME_CHANGE, newValue);
   },
 
   /**
    * Called for each location change in the debugged tab.
    */
   _onTabNavigated: Task.async(function* (event, {isFrameSwitching}) {
     switch (event) {
       case "will-navigate": {
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_timestamps.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_timestamps.js
@@ -3,50 +3,52 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test for the message timestamps option: check if the preference toggles the
 // display of messages in the console output. See bug 722267.
 
 "use strict";
 
+const {PrefObserver} = require("devtools/client/shared/prefs");
+
 const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
                  "bug 1307871 - preference for toggling timestamps in messages";
 const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
 
 add_task(function* () {
   let hud = yield openNewTabAndConsole(TEST_URI);
   let outputNode = hud.ui.experimentalOutputNode;
   let outputEl = outputNode.querySelector(".webconsole-output");
 
   testPrefDefaults(outputEl);
 
+  let observer = new PrefObserver("");
   let toolbox = gDevTools.getToolbox(hud.target);
   let optionsPanel = yield toolbox.selectTool("options");
-  yield togglePref(optionsPanel);
+  yield togglePref(optionsPanel, observer);
+  observer.destroy();
 
   yield testChangedPref(outputEl);
 
   Services.prefs.clearUserPref(PREF_MESSAGE_TIMESTAMP);
 });
 
 function testPrefDefaults(outputEl) {
   let prefValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP);
   ok(!prefValue, "Messages should have no timestamp by default (pref check)");
   ok(outputEl.classList.contains("hideTimestamps"),
      "Messages should have no timestamp (class name check)");
 }
 
-function* togglePref(panel) {
+function* togglePref(panel, observer) {
   info("Options panel opened");
 
   info("Changing pref");
-  let prefChanged = new Promise(resolve => {
-    gDevTools.once("pref-changed", resolve);
-  });
+  let prefChanged = observer.once(PREF_MESSAGE_TIMESTAMP, () => {});
   let checkbox = panel.panelDoc.getElementById("webconsole-timestamp-messages");
   checkbox.click();
 
   yield prefChanged;
 }
 
 function* testChangedPref(outputEl) {
   let prefValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP);
--- a/devtools/client/webconsole/test/browser_webconsole_expandable_timestamps.js
+++ b/devtools/client/webconsole/test/browser_webconsole_expandable_timestamps.js
@@ -3,49 +3,53 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test for the message timestamps option: check if the preference toggles the
 // display of messages in the console output. See bug 722267.
 
 "use strict";
 
+const {PrefObserver} = require("devtools/client/shared/prefs");
+
 const TEST_URI = "data:text/html;charset=utf-8,Web Console test for " +
                  "bug 722267 - preference for toggling timestamps in messages";
 const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
 var hud;
 
 add_task(function* () {
   yield loadTab(TEST_URI);
 
   hud = yield openConsole();
   let panel = yield consoleOpened();
 
-  yield onOptionsPanelSelected(panel);
+  let observer = new PrefObserver("");
+  yield onOptionsPanelSelected(panel, observer);
   onPrefChanged();
+  observer.destroy();
 
   Services.prefs.clearUserPref(PREF_MESSAGE_TIMESTAMP);
   hud = null;
 });
 
 function consoleOpened() {
   info("console opened");
   let prefValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP);
   ok(!prefValue, "messages have no timestamp by default (pref check)");
   ok(hud.outputNode.classList.contains("hideTimestamps"),
      "messages have no timestamp (class name check)");
 
   let toolbox = gDevTools.getToolbox(hud.target);
   return toolbox.selectTool("options");
 }
 
-function onOptionsPanelSelected(panel) {
+function onOptionsPanelSelected(panel, observer) {
   info("options panel opened");
 
-  let prefChanged = gDevTools.once("pref-changed", onPrefChanged);
+  let prefChanged = observer.once(PREF_MESSAGE_TIMESTAMP, () => {});
 
   let checkbox = panel.panelDoc.getElementById("webconsole-timestamp-messages");
   checkbox.click();
 
   return prefChanged;
 }
 
 function onPrefChanged() {
--- a/devtools/client/webconsole/webconsole.js
+++ b/devtools/client/webconsole/webconsole.js
@@ -12,16 +12,17 @@ const {Utils: WebConsoleUtils, CONSOLE_W
   require("devtools/client/webconsole/utils");
 const { getSourceNames } = require("devtools/client/shared/source-utils");
 const BrowserLoaderModule = {};
 Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
 
 const promise = require("promise");
 const Services = require("Services");
 const Telemetry = require("devtools/client/shared/telemetry");
+const {PrefObserver} = require("devtools/client/shared/prefs");
 
 loader.lazyServiceGetter(this, "clipboardHelper",
                          "@mozilla.org/widget/clipboardhelper;1",
                          "nsIClipboardHelper");
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
 loader.lazyRequireGetter(this, "ConsoleOutput", "devtools/client/webconsole/console-output", true);
 loader.lazyRequireGetter(this, "Messages", "devtools/client/webconsole/console-output", true);
 loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true);
@@ -631,21 +632,19 @@ WebConsoleFrame.prototype = {
           event.target.getAttribute("type").toLowerCase() === "search") {
         return;
       }
 
       this.jsterm.focus();
     });
 
     // Toggle the timestamp on preference change
-    gDevTools.on("pref-changed", this._onToolboxPrefChanged);
-    this._onToolboxPrefChanged("pref-changed", {
-      pref: PREF_MESSAGE_TIMESTAMP,
-      newValue: Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP),
-    });
+    this._prefObserver = new PrefObserver("");
+    this._prefObserver.on(PREF_MESSAGE_TIMESTAMP, this._onToolboxPrefChanged);
+    this._onToolboxPrefChanged();
 
     this._initShortcuts();
 
     // focus input node
     this.jsterm.focus();
   },
 
   /**
@@ -2686,35 +2685,26 @@ WebConsoleFrame.prototype = {
 
       this._startX = this._startY = undefined;
 
       callback.call(this, event);
     }, false);
   },
 
   /**
-   * Handler for the pref-changed event coming from the toolbox.
-   * Currently this function only handles the timestamps preferences.
-   *
-   * @private
-   * @param object event
-   *        This parameter is a string that holds the event name
-   *        pref-changed in this case.
-   * @param object data
-   *        This is the pref-changed data object.
-  */
-  _onToolboxPrefChanged: function (event, data) {
-    if (data.pref == PREF_MESSAGE_TIMESTAMP) {
-      if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
-        this.newConsoleOutput.dispatchTimestampsToggle(data.newValue);
-      } else if (data.newValue) {
-        this.outputNode.classList.remove("hideTimestamps");
-      } else {
-        this.outputNode.classList.add("hideTimestamps");
-      }
+   * Called when the message timestamp pref changes.
+   */
+  _onToolboxPrefChanged: function () {
+    let newValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP);
+    if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
+      this.newConsoleOutput.dispatchTimestampsToggle(newValue);
+    } else if (newValue) {
+      this.outputNode.classList.remove("hideTimestamps");
+    } else {
+      this.outputNode.classList.add("hideTimestamps");
     }
   },
 
   /**
    * Copies the selected items to the system clipboard.
    *
    * @param object options
    *        - linkOnly:
@@ -2813,17 +2803,18 @@ WebConsoleFrame.prototype = {
 
     this._destroyer = promise.defer();
 
     let toolbox = gDevTools.getToolbox(this.owner.target);
     if (toolbox) {
       toolbox.off("webconsole-selected", this._onPanelSelected);
     }
 
-    gDevTools.off("pref-changed", this._onToolboxPrefChanged);
+    this._prefObserver.off(PREF_MESSAGE_TIMESTAMP, this._onToolboxPrefChanged);
+    this._prefObserver.destroy();
     this.window.removeEventListener("resize", this.resize, true);
 
     this._repeatNodes = {};
     this._outputQueue.forEach(this._destroyItem, this);
     this._outputQueue = [];
     this._itemDestroyQueue.forEach(this._destroyItem, this);
     this._itemDestroyQueue = [];
     this._pruneCategoriesQueue = {};