Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Tue, 17 Sep 2013 22:47:28 -0400
changeset 147675 96d8a2bf056b4fb9c46c58b5f791055fc8e19c55
parent 147674 6bb1ea0648a284e7c289e69cd81d7e7221dd7ff8 (current diff)
parent 147622 ab4ccf3d6b6028d9c7dfdec85325e472370d3fba (diff)
child 147676 10065efeed2fc3ab87f73d61e31dc1e881bea13f
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
milestone27.0a1
Merge m-c to inbound.
docshell/base/nsDocShell.cpp
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -1042,16 +1042,17 @@ let RemoteDebugger = {
           DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
         }
         DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
         DebuggerServer.enableWebappsContentActor = true;
       }
       DebuggerServer.addActors('chrome://browser/content/dbg-browser-actors.js');
       DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webapps.js");
       DebuggerServer.registerModule("devtools/server/actors/device");
+      DebuggerServer.registerModule("devtools/server/actors/inspector")
 
 #ifdef MOZ_WIDGET_GONK
       DebuggerServer.onConnectionChange = function(what) {
         AdbController.updateState();
       }
 #endif
     }
 
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "ca3cef257ae1d19f25cd4b973f7ba2a713b8a7c2", 
+    "revision": "94340fb19f685f3cd6e5fa990281557ba810a10e", 
     "repo_path": "/integration/gaia-central"
 }
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1085,16 +1085,19 @@ pref("devtools.toolbox.zoomValue", "1");
 
 // Enable the Inspector
 pref("devtools.inspector.enabled", true);
 pref("devtools.inspector.activeSidebar", "ruleview");
 pref("devtools.inspector.markupPreview", false);
 pref("devtools.inspector.remote", false);
 pref("devtools.inspector.show_pseudo_elements", true);
 
+// DevTools default color unit
+pref("devtools.defaultColorUnit", "hex");
+
 // Enable the Responsive UI tool
 pref("devtools.responsiveUI.enabled", true);
 pref("devtools.responsiveUI.no-reload-notification", false);
 
 // Enable the Debugger
 pref("devtools.debugger.enabled", true);
 pref("devtools.debugger.chrome-enabled", true);
 pref("devtools.debugger.chrome-debugging-host", "localhost");
--- a/browser/base/content/browser-plugins.js
+++ b/browser/base/content/browser-plugins.js
@@ -1,18 +1,14 @@
 # -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 # 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/.
 
 var gPluginHandler = {
-  PLUGIN_SCRIPTED_STATE_NONE: 0,
-  PLUGIN_SCRIPTED_STATE_FIRED: 1,
-  PLUGIN_SCRIPTED_STATE_DONE: 2,
-
   PREF_NOTIFY_MISSING_FLASH: "plugins.notifyMissingFlash",
   PREF_SESSION_PERSIST_MINUTES: "plugin.sessionPermissionNow.intervalInMinutes",
   PREF_PERSISTENT_DAYS: "plugin.persistentPermissionAlways.intervalInDays",
 
   getPluginUI: function (plugin, anonid) {
     return plugin.ownerDocument.
            getAnonymousElementByAttribute(plugin, "anonid", anonid);
   },
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1430,17 +1430,16 @@ var gBrowserInit = {
       BrowserOffline.uninit();
       OfflineApps.uninit();
       IndexedDBPromptHelper.uninit();
       AddonManager.removeAddonListener(AddonsMgrListener);
       SocialUI.uninit();
     }
 
     // Final window teardown, do this last.
-    window.XULBrowserWindow.destroy();
     window.XULBrowserWindow = null;
     window.QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIWebNavigation)
           .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
           .QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIXULWindow)
           .XULBrowserWindow = null;
     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = null;
@@ -3685,24 +3684,16 @@ var XULBrowserWindow = {
   init: function () {
     this.throbberElement = document.getElementById("navigator-throbber");
 
     // Initialize the security button's state and tooltip text.
     var securityUI = gBrowser.securityUI;
     this.onSecurityChange(null, null, securityUI.state);
   },
 
-  destroy: function () {
-    // XXXjag to avoid leaks :-/, see bug 60729
-    delete this.throbberElement;
-    delete this.stopCommand;
-    delete this.reloadCommand;
-    delete this.statusText;
-  },
-
   setJSStatus: function () {
     // unsupported
   },
 
   setDefaultStatus: function (status) {
     this.defaultStatus = status;
     this.updateStatusField();
   },
@@ -4305,21 +4296,16 @@ var TabsProgressListener = {
     // longer exists)
     if (!Object.getOwnPropertyDescriptor(window, "PopupNotifications").get)
       PopupNotifications.locationChange(aBrowser);
 
     gBrowser.getNotificationBox(aBrowser).removeTransientNotifications();
 
     // Filter out location changes in sub documents.
     if (aWebProgress.isTopLevel) {
-      // Initialize the click-to-play state.
-      aBrowser._clickToPlayPluginsActivated = new Map();
-      aBrowser._clickToPlayAllPluginsActivated = false;
-      aBrowser._pluginScriptedState = gPluginHandler.PLUGIN_SCRIPTED_STATE_NONE;
-
       FullZoom.onLocationChange(aLocationURI, false, aBrowser);
     }
   },
 
   onRefreshAttempted: function (aBrowser, aWebProgress, aURI, aDelay, aSameURI) {
     if (gPrefService.getBoolPref("accessibility.blockautorefresh")) {
       let brandBundle = document.getElementById("bundle_brand");
       let brandShortName = brandBundle.getString("brandShortName");
--- a/browser/base/content/test/browser_sanitize-timespans.js
+++ b/browser/base/content/test/browser_sanitize-timespans.js
@@ -615,91 +615,91 @@ function setupDownloads() {
   let publicList = yield Downloads.getList(Downloads.PUBLIC);
 
   let download = yield Downloads.createDownload({
     source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
     target: "fakefile-10-minutes"
   });
   download.startTime = new Date(now_mSec - 10 * kMsecPerMin), // 10 minutes ago
   download.canceled = true;
-  publicList.add(download);
+  yield publicList.add(download);
 
   download = yield Downloads.createDownload({
     source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
     target: "fakefile-1-hour"
   });
   download.startTime = new Date(now_mSec - 45 * kMsecPerMin), // 45 minutes ago
   download.canceled = true;
-  publicList.add(download);
+  yield publicList.add(download);
 
   download = yield Downloads.createDownload({
     source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
     target: "fakefile-1-hour-10-minutes"
   });
   download.startTime = new Date(now_mSec - 70 * kMsecPerMin), // 70 minutes ago
   download.canceled = true;
-  publicList.add(download);
+  yield publicList.add(download);
 
   download = yield Downloads.createDownload({
     source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
     target: "fakefile-2-hour"
   });
   download.startTime = new Date(now_mSec - 90 * kMsecPerMin), // 90 minutes ago
   download.canceled = true;
-  publicList.add(download);
+  yield publicList.add(download);
 
   download = yield Downloads.createDownload({
     source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
     target: "fakefile-2-hour-10-minutes"
   });
   download.startTime = new Date(now_mSec - 130 * kMsecPerMin), // 130 minutes ago
   download.canceled = true;
-  publicList.add(download);
+  yield publicList.add(download);
 
   download = yield Downloads.createDownload({
     source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
     target: "fakefile-4-hour"
   });
   download.startTime = new Date(now_mSec - 180 * kMsecPerMin), // 180 minutes ago
   download.canceled = true;
-  publicList.add(download);
+  yield publicList.add(download);
 
   download = yield Downloads.createDownload({
     source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
     target: "fakefile-4-hour-10-minutes"
   });
   download.startTime = new Date(now_mSec - 250 * kMsecPerMin), // 250 minutes ago
   download.canceled = true;
-  publicList.add(download);
+  yield publicList.add(download);
 
   // Add "today" download
   let today = new Date();
   today.setHours(0);
   today.setMinutes(0);
   today.setSeconds(1);
 
   download = yield Downloads.createDownload({
     source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
     target: "fakefile-today"
   });
   download.startTime = today, // 12:00:01 AM this morning
   download.canceled = true;
-  publicList.add(download);
+  yield publicList.add(download);
   
   // Add "before today" download
   let lastYear = new Date();
   lastYear.setFullYear(lastYear.getFullYear() - 1);
 
   download = yield Downloads.createDownload({
     source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
     target: "fakefile-old"
   });
   download.startTime = lastYear,
   download.canceled = true;
-  publicList.add(download);
+  yield publicList.add(download);
   
   // Confirm everything worked
   let downloads = yield publicList.getAll();
   is(downloads.length, 9, "9 Pretend downloads added");
 
   ok((yield downloadExists(publicList, "fakefile-old")), "Pretend download for everything case should exist");
   ok((yield downloadExists(publicList, "fakefile-10-minutes")), "Pretend download for 10-minutes case should exist");
   ok((yield downloadExists(publicList, "fakefile-1-hour")), "Pretend download for 1-hour case should exist");
--- a/browser/base/content/test/browser_sanitizeDialog.js
+++ b/browser/base/content/test/browser_sanitizeDialog.js
@@ -982,17 +982,17 @@ function blankSlate() {
   // The promise is resolved only when removing both downloads and form history are done.
   let deferred = Promise.defer();
   let formHistoryDone = false, downloadsDone = false;
 
   Task.spawn(function deleteAllDownloads() {
     let publicList = yield Downloads.getList(Downloads.PUBLIC);
     let downloads = yield publicList.getAll();
     for (let download of downloads) {
-      publicList.remove(download);
+      yield publicList.remove(download);
       yield download.finalize(true);
     }
     downloadsDone = true;
     if (formHistoryDone) {
       deferred.resolve();
     }
   }).then(null, Components.utils.reportError);
 
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -146,16 +146,18 @@ const DownloadsPanel = {
     // Now that data loading has eventually started, load the required XUL
     // elements and initialize our views.
     DownloadsCommon.log("Ensuring DownloadsPanel overlay loaded.");
     DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay,
                                                function DP_I_callback() {
       DownloadsViewController.initialize();
       DownloadsCommon.log("Attaching DownloadsView...");
       DownloadsCommon.getData(window).addView(DownloadsView);
+      DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit)
+                     .addView(DownloadsSummary);
       DownloadsCommon.log("DownloadsView attached - the panel for this window",
                           "should now see download items come in.");
       DownloadsPanel._attachEventListeners();
       DownloadsCommon.log("DownloadsPanel initialized.");
       aCallback();
     });
   },
 
@@ -174,16 +176,18 @@ const DownloadsPanel = {
 
     window.removeEventListener("unload", this.onWindowUnload, false);
 
     // Ensure that the panel is closed before shutting down.
     this.hidePanel();
 
     DownloadsViewController.terminate();
     DownloadsCommon.getData(window).removeView(DownloadsView);
+    DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit)
+                   .removeView(DownloadsSummary);
     this._unattachEventListeners();
 
     this._state = this.kStateUninitialized;
 
     DownloadsSummary.active = false;
     DownloadsCommon.log("DownloadsPanel terminated.");
   },
 
@@ -1570,20 +1574,18 @@ const DownloadsSummary = {
    */
   set active(aActive)
   {
     if (aActive == this._active || !this._summaryNode) {
       return this._active;
     }
     if (aActive) {
       DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit)
-                     .addView(this);
+                     .refreshView(this);
     } else {
-      DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit)
-                     .removeView(this);
       DownloadsFooter.showingSummary = false;
     }
 
     return this._active = aActive;
   },
 
   /**
    * Returns the active state of the downloads summary.
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -403,17 +403,17 @@ let SessionStoreInternal = {
 
       // Start tracking the given (initial) browser window.
       if (!aWindow.closed) {
         this.onLoad(aWindow, initialState);
       }
 
       // Let everyone know we're done.
       this._deferredInitialized.resolve();
-    });
+    }, Cu.reportError);
   },
 
   initSession: function ssi_initSession() {
     let state;
     let ss = gSessionStartup;
 
     try {
       if (ss.doRestore() ||
@@ -1039,17 +1039,16 @@ let SessionStoreInternal = {
       return;
     this._lastSessionState = null;
     let openWindows = {};
     this._forEachBrowserWindow(function(aWindow) {
       Array.forEach(aWindow.gBrowser.tabs, function(aTab) {
         TabStateCache.delete(aTab);
         delete aTab.linkedBrowser.__SS_data;
         delete aTab.linkedBrowser.__SS_tabStillLoading;
-        delete aTab.linkedBrowser.__SS_formDataSaved;
         if (aTab.linkedBrowser.__SS_restoreState)
           this._resetTabRestoringState(aTab);
       }, this);
       openWindows[aWindow.__SSi] = true;
     });
     // also clear all data about closed tabs and windows
     for (let ix in this._windows) {
       if (ix in openWindows) {
@@ -1189,17 +1188,16 @@ let SessionStoreInternal = {
     let browser = aTab.linkedBrowser;
     browser.removeEventListener("load", this, true);
 
     let mm = browser.messageManager;
     MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
 
     delete browser.__SS_data;
     delete browser.__SS_tabStillLoading;
-    delete browser.__SS_formDataSaved;
 
     // If this tab was in the middle of restoring or still needs to be restored,
     // we need to reset that state. If the tab was restoring, we will attempt to
     // restore the next tab.
     let previousState = browser.__SS_restoreState;
     if (previousState) {
       this._resetTabRestoringState(aTab);
       if (previousState == TAB_STATE_RESTORING)
@@ -1268,36 +1266,31 @@ let SessionStoreInternal = {
         aBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
       return;
     }
 
     TabStateCache.delete(aBrowser);
 
     delete aBrowser.__SS_data;
     delete aBrowser.__SS_tabStillLoading;
-    delete aBrowser.__SS_formDataSaved;
     this.saveStateDelayed(aWindow);
 
     // attempt to update the current URL we send in a crash report
     this._updateCrashReportURL(aWindow);
   },
 
   /**
    * Called when a browser sends the "input" notification
    * @param aWindow
    *        Window reference
    * @param aBrowser
    *        Browser reference
    */
   onTabInput: function ssi_onTabInput(aWindow, aBrowser) {
-    // deleting __SS_formDataSaved will cause us to recollect form data
-    delete aBrowser.__SS_formDataSaved;
-
     TabStateCache.delete(aBrowser);
-
     this.saveStateDelayed(aWindow);
   },
 
   /**
    * When a tab is selected, save session data
    * @param aWindow
    *        Window reference
    */
@@ -2233,19 +2226,18 @@ let SessionStoreInternal = {
                             this._getSelectedPageStyle(browser.contentWindow);
     if (selectedPageStyle)
       aTabData.pageStyle = selectedPageStyle;
     else if (aTabData.pageStyle)
       delete aTabData.pageStyle;
 
     this._updateTextAndScrollDataForFrame(window, browser.contentWindow,
                                           aTabData.entries[tabIndex],
-                                          !browser.__SS_formDataSaved, includePrivateData,
+                                          includePrivateData,
                                           !!aTabData.pinned);
-    browser.__SS_formDataSaved = true;
     if (browser.currentURI.spec == "about:config")
       aTabData.entries[tabIndex].formdata = {
         id: {
           "textbox": browser.contentDocument.getElementById("textbox").value
         },
         xpath: {}
       };
       return true;
@@ -2255,53 +2247,49 @@ let SessionStoreInternal = {
    * go through all subframes and store all form data, the current
    * scroll positions and innerHTML content of WYSIWYG editors
    * @param aWindow
    *        Window reference
    * @param aContent
    *        frame reference
    * @param aData
    *        part of a tabData object to add the information to
-   * @param aUpdateFormData
-   *        update all form data for this tab
    * @param aIncludePrivateData
    *        always return privacy sensitive data (use with care)
    * @param aIsPinned
    *        the tab is pinned and should be treated differently for privacy
    */
   _updateTextAndScrollDataForFrame:
     function ssi_updateTextAndScrollDataForFrame(aWindow, aContent, aData,
-                                                 aUpdateFormData, aIncludePrivateData, aIsPinned) {
+                                                 aIncludePrivateData, aIsPinned) {
     for (var i = 0; i < aContent.frames.length; i++) {
       if (aData.children && aData.children[i])
         this._updateTextAndScrollDataForFrame(aWindow, aContent.frames[i],
-                                              aData.children[i], aUpdateFormData,
+                                              aData.children[i],
                                               aIncludePrivateData, aIsPinned);
     }
     var isHTTPS = this._getURIFromString((aContent.parent || aContent).
                                          document.location.href).schemeIs("https");
     let topURL = aContent.top.document.location.href;
     let isAboutSR = topURL == "about:sessionrestore" || topURL == "about:welcomeback";
     if (aIncludePrivateData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) {
-      if (aIncludePrivateData || aUpdateFormData) {
-        let formData = DocumentUtils.getFormData(aContent.document);
-
-        // We want to avoid saving data for about:sessionrestore as a string.
-        // Since it's stored in the form as stringified JSON, stringifying further
-        // causes an explosion of escape characters. cf. bug 467409
-        if (formData && isAboutSR) {
-          formData.id["sessionData"] = JSON.parse(formData.id["sessionData"]);
-        }
-
-        if (Object.keys(formData.id).length ||
-            Object.keys(formData.xpath).length) {
-          aData.formdata = formData;
-        } else if (aData.formdata) {
-          delete aData.formdata;
-        }
+      let formData = DocumentUtils.getFormData(aContent.document);
+
+      // We want to avoid saving data for about:sessionrestore as a string.
+      // Since it's stored in the form as stringified JSON, stringifying further
+      // causes an explosion of escape characters. cf. bug 467409
+      if (formData && isAboutSR) {
+        formData.id["sessionData"] = JSON.parse(formData.id["sessionData"]);
+      }
+
+      if (Object.keys(formData.id).length ||
+          Object.keys(formData.xpath).length) {
+        aData.formdata = formData;
+      } else if (aData.formdata) {
+        delete aData.formdata;
       }
 
       // designMode is undefined e.g. for XUL documents (as about:config)
       if ((aContent.document.designMode || "") == "on" && aContent.document.body)
         aData.innerHTML = aContent.document.body.innerHTML;
     }
 
     // get scroll position from nsIDOMWindowUtils, since it allows avoiding a
--- a/browser/components/sessionstore/src/_SessionFile.jsm
+++ b/browser/components/sessionstore/src/_SessionFile.jsm
@@ -29,16 +29,17 @@ const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/AsyncShutdown.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
   "resource://gre/modules/TelemetryStopwatch.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
@@ -144,16 +145,28 @@ let SessionFileInternal = {
   path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),
 
   /**
    * The path to sessionstore.bak
    */
   backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"),
 
   /**
+   * The promise returned by the latest call to |write|.
+   * We use it to ensure that AsyncShutdown.profileBeforeChange cannot
+   * interrupt a call to |write|.
+   */
+  _latestWrite: null,
+
+  /**
+   * |true| once we have decided to stop receiving write instructiosn
+   */
+  _isClosed: false,
+
+  /**
    * Utility function to safely read a file synchronously.
    * @param aPath
    *        A path to read the file from.
    * @returns string if successful, undefined otherwise.
    */
   readAuxSync: function (aPath) {
     let text;
     try {
@@ -205,41 +218,53 @@ let SessionFileInternal = {
   read: function () {
     return SessionWorker.post("read").then(msg => {
       this._recordTelemetry(msg.telemetry);
       return msg.ok;
     });
   },
 
   write: function (aData) {
+    if (this._isClosed) {
+      return Promise.reject(new Error("_SessionFile is closed"));
+    }
     let refObj = {};
-    return TaskUtils.spawn(function task() {
+    return this._latestWrite = TaskUtils.spawn(function task() {
       TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
 
       try {
         let promise = SessionWorker.post("write", [aData]);
         // At this point, we measure how long we stop the main thread
         TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
 
         // Now wait for the result and record how long the write took
         let msg = yield promise;
         this._recordTelemetry(msg.telemetry);
       } catch (ex) {
         TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
         Cu.reportError("Could not write session state file " + this.path
                        + ": " + ex);
       }
+      // At this stage, we are done writing. If shutdown has started,
+      // we will want to stop receiving write instructions.
+      if (Services.startup.shuttingDown) {
+        this._isClosed = true;
+      }
+      // In rare cases, we may already have other writes pending,
+      // which we need to flush before shutdown proceeds. AsyncShutdown
+      // uses _latestWrite to determine what needs to be flushed during
+      // shutdown.
     }.bind(this));
   },
 
   writeLoadStateOnceAfterStartup: function (aLoadState) {
     SessionWorker.post("writeLoadStateOnceAfterStartup", [aLoadState]).then(msg => {
       this._recordTelemetry(msg.telemetry);
       return msg;
-    });
+    }, Cu.reportError);
   },
 
   createBackupCopy: function (ext) {
     return SessionWorker.post("createBackupCopy", [ext]);
   },
 
   removeBackupCopy: function (ext) {
     return SessionWorker.post("removeBackupCopy", [ext]);
@@ -272,8 +297,16 @@ let SessionWorker = (function () {
           } else {
             throw error;
           }
         }
       );
     }
   };
 })();
+
+// Ensure that we can write sessionstore.js cleanly before the profile
+// becomes unaccessible.
+AsyncShutdown.profileBeforeChange.addBlocker(
+  "SessionFile: Finish writing the latest sessionstore.js",
+  function() {
+    return _SessionFile._latestWrite;
+  });
--- a/browser/components/sessionstore/src/nsSessionStartup.js
+++ b/browser/components/sessionstore/src/nsSessionStartup.js
@@ -76,17 +76,18 @@ SessionStartup.prototype = {
     // do not need to initialize anything in auto-started private browsing sessions
     if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
       this._initialized = true;
       gOnceInitializedDeferred.resolve();
       return;
     }
 
     _SessionFile.read().then(
-      this._onSessionFileRead.bind(this)
+      this._onSessionFileRead.bind(this),
+      Cu.reportError
     );
   },
 
   // Wrap a string as a nsISupports
   _createSupportsString: function ssfi_createSupportsString(aData) {
     let string = Cc["@mozilla.org/supports-string;1"]
                    .createInstance(Ci.nsISupportsString);
     string.data = aData;
--- a/browser/components/sessionstore/test/Makefile.in
+++ b/browser/components/sessionstore/test/Makefile.in
@@ -15,17 +15,17 @@ MOCHITEST_BROWSER_FILES = \
 	browser_form_restore_events.js \
 	browser_form_restore_events_sample.html \
 	browser_formdata_format.js \
 	browser_formdata_format_sample.html \
 	browser_input.js \
 	browser_input_sample.html \
 	browser_pageshow.js \
 	browser_sessionStorage.js \
-        browser_upgrade_backup.js \
+	browser_upgrade_backup.js \
 	browser_windowRestore_perwindowpb.js \
 	browser_248970_b_perwindowpb.js \
 	browser_248970_b_sample.html \
 	browser_339445.js \
 	browser_339445_sample.html \
 	browser_345898.js \
 	browser_346337.js \
 	browser_346337_sample.html \
@@ -130,16 +130,18 @@ MOCHITEST_BROWSER_FILES = \
 	browser_701377.js \
 	browser_705597.js \
 	browser_707862.js \
 	browser_739531.js \
 	browser_739531_sample.html \
 	browser_739805.js \
 	browser_819510_perwindowpb.js \
 	browser_833286_atomic_backup.js \
+	browser_916390_form_data_loss.js \
+	browser_916390_sample.html \
 	$(filter disabled-for-intermittent-failures--bug-766044, browser_459906_empty.html) \
 	$(filter disabled-for-intermittent-failures--bug-766044, browser_459906_sample.html) \
 	$(filter disabled-for-intermittent-failures--bug-765389, browser_461743_sample.html) \
 	$(NULL)
 
 # Disabled on Windows for frequent intermittent failures
 ifneq ($(OS_ARCH), WINNT)
 MOCHITEST_FILES += \
--- a/browser/components/sessionstore/test/browser_624727.js
+++ b/browser/components/sessionstore/test/browser_624727.js
@@ -1,19 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function test() {
-  waitForExplicitFinish();
+  TestRunner.run();
+}
 
-  let assertNumberOfTabs = function (num, msg) {
+function runTests() {
+  function assertNumberOfTabs(num, msg) {
     is(gBrowser.tabs.length, num, msg);
   }
 
-  let assertNumberOfPinnedTabs = function (num, msg) {
+  function assertNumberOfPinnedTabs(num, msg) {
     is(gBrowser._numPinnedTabs, num, msg);
   }
 
   // check prerequisites
   assertNumberOfTabs(1, "we start off with one tab");
   assertNumberOfPinnedTabs(0, "no pinned tabs so far");
 
   // setup
@@ -22,18 +24,18 @@ function test() {
 
   let [tab1, tab2] = gBrowser.tabs;
   let linkedBrowser = tab1.linkedBrowser;
   gBrowser.pinTab(tab1);
   gBrowser.pinTab(tab2);
   assertNumberOfPinnedTabs(2, "both tabs are now pinned");
 
   // run the test
-  waitForBrowserState(
+  yield waitForBrowserState(
     { windows: [{ tabs: [{ url: "about:blank" }] }] },
     function () {
       assertNumberOfTabs(1, "one tab left after setBrowserState()");
       assertNumberOfPinnedTabs(0, "there are no pinned tabs");
       is(gBrowser.tabs[0].linkedBrowser, linkedBrowser, "first tab's browser got re-used");
-      finish();
+      next();
     }
   );
 }
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_916390_form_data_loss.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "http://mochi.test:8888/browser/" +
+            "browser/components/sessionstore/test/browser_916390_sample.html";
+
+function test() {
+  TestRunner.run();
+}
+
+function runTests() {
+  // Create a tab with some form fields.
+  let tab = gBrowser.selectedTab = gBrowser.addTab(URL);
+  let browser = gBrowser.selectedBrowser;
+  yield waitForLoad(browser);
+
+  // Modify the text input field's state.
+  browser.contentDocument.getElementById("txt").focus();
+  EventUtils.synthesizeKey("m", {});
+  yield waitForInput();
+
+  // Check that we'll save the form data state correctly.
+  let state = JSON.parse(ss.getBrowserState());
+  let {formdata} = state.windows[0].tabs[1].entries[0];
+  is(formdata.id.txt, "m", "txt's value is correct");
+
+  // Change the number of session history entries and modify
+  // DOMSessionStorage data to invalidate the TabStateCache.
+  browser.loadURI(URL + "#");
+  browser.contentWindow.sessionStorage.foo = "bar";
+  yield waitForStorageChange();
+
+  // Check that we'll save the form data state correctly.
+  let state = JSON.parse(ss.getBrowserState());
+  let {formdata} = state.windows[0].tabs[1].entries[1];
+  is(formdata.id.txt, "m", "txt's value is correct");
+
+  // Clean up.
+  gBrowser.removeTab(tab);
+}
+
+function waitForLoad(aElement) {
+  aElement.addEventListener("load", function onLoad() {
+    aElement.removeEventListener("load", onLoad, true);
+    executeSoon(next);
+  }, true);
+}
+
+function waitForInput() {
+  let mm = gBrowser.selectedBrowser.messageManager;
+
+  mm.addMessageListener("SessionStore:input", function onInput() {
+    mm.removeMessageListener("SessionStore:input", onInput);
+    executeSoon(next);
+  });
+}
+
+function waitForStorageChange() {
+  let mm = gBrowser.selectedBrowser.messageManager;
+
+  mm.addMessageListener("SessionStore:MozStorageChanged", function onChanged() {
+    mm.removeMessageListener("SessionStore:MozStorageChanged", onChanged);
+    executeSoon(next);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_916390_sample.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+  <head>
+    <meta charset="utf-8">
+    <title>bug 916390</title>
+  </head>
+  <body>
+    <input id="txt" />
+  </body>
+</html>
--- a/browser/devtools/framework/toolbox-options.js
+++ b/browser/devtools/framework/toolbox-options.js
@@ -173,16 +173,37 @@ OptionsPanel.prototype = {
           pref: this.getAttribute("data-pref"),
           newValue: this.selectedItem.getAttribute("value")
         };
         data.oldValue = Services.prefs.getCharPref(data.pref);
         Services.prefs.setCharPref(data.pref, data.newValue);
         gDevTools.emit("pref-changed", data);
       }.bind(radiogroup));
     }
+    let prefMenulists = this.panelDoc.querySelectorAll("menulist[data-pref]");
+    for (let menulist of prefMenulists) {
+      let pref = Services.prefs.getCharPref(menulist.getAttribute("data-pref"));
+      let menuitems = menulist.querySelectorAll("menuitem");
+      for (let menuitem of menuitems) {
+        let value = menuitem.getAttribute("value");
+        if (value === pref) {
+          menulist.selectedItem = menuitem;
+          break;
+        }
+      }
+      menulist.addEventListener("command", function() {
+        let data = {
+          pref: this.getAttribute("data-pref"),
+          newValue: this.value
+        };
+        data.oldValue = Services.prefs.getCharPref(data.pref);
+        Services.prefs.setCharPref(data.pref, data.newValue);
+        gDevTools.emit("pref-changed", data);
+      }.bind(menulist));
+    }
   },
 
   /**
    * Handles checkbox click inside hbox with class "hidden-labels-box". The
    * labels inside the hbox are shown again when the user click on the checkbox
    * in the box.
    */
   prepareRestartPreferences: function() {
--- a/browser/devtools/framework/toolbox-options.xul
+++ b/browser/devtools/framework/toolbox-options.xul
@@ -26,16 +26,34 @@
         <label value="&options.selectDevToolsTheme.label;"/>
         <radiogroup id="devtools-theme-box"
                     class="options-groupbox"
                     data-pref="devtools.theme"
                     orient="horizontal">
           <radio value="light" label="&options.lightTheme.label;"/>
           <radio value="dark" label="&options.darkTheme.label;"/>
         </radiogroup>
+        <label value="&options.context.inspector;"/>
+        <vbox id="inspector-options" class="options-groupbox">
+          <hbox align="center">
+            <label value="&options.defaultColorUnit.label;"
+              control="defaultColorUnitMenuList"
+              accesskey="&options.defaultColorUnit.accesskey;"/>
+            <menulist id="defaultColorUnitMenuList"
+                      label="&options.defaultColorUnit.label;"
+                      data-pref="devtools.defaultColorUnit">
+              <menupopup>
+                <menuitem label="&options.defaultColorUnit.hex;" value="hex"/>
+                <menuitem label="&options.defaultColorUnit.hsl;" value="hsl"/>
+                <menuitem label="&options.defaultColorUnit.rgb;" value="rgb"/>
+                <menuitem label="&options.defaultColorUnit.name;" value="name"/>
+              </menupopup>
+            </menulist>
+          </hbox>
+        </vbox>
         <label value="&options.webconsole.label;"/>
         <vbox id="webconsole-options" class="options-groupbox">
           <checkbox label="&options.enablePersistentLogging.label;"
                     tooltiptext="&options.enablePersistentLogging.tooltip;"
                     data-pref="devtools.webconsole.persistlog"/>
         </vbox>
         <label value="&options.profiler.label;"/>
         <vbox id="profiler-options" class="options-groupbox">
--- a/browser/devtools/inspector/highlighter.js
+++ b/browser/devtools/inspector/highlighter.js
@@ -829,16 +829,17 @@ function BasicHighlighter(aTarget, aInsp
   this.highlight = this.highlight.bind(this);
   this.selection.on("new-node-front", this.highlight);
   EventEmitter.decorate(this);
   this.locked = true;
 }
 
 BasicHighlighter.prototype = {
   destroy: function() {
+    this.walker.highlight(null);
     this.selection.off("new-node-front", this.highlight);
     this.walker = null;
     this.selection = null;
   },
   toggleLockState: function() {
     this.locked = !this.locked;
     if (this.locked) {
       this.walker.cancelPick();
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -442,16 +442,23 @@ InspectorPanel.prototype = {
 
   /**
    * Destroy the inspector.
    */
   destroy: function InspectorPanel__destroy() {
     if (this._destroyPromise) {
       return this._destroyPromise;
     }
+
+    if (this.highlighter) {
+      this.highlighter.off("locked", this.onLockStateChanged);
+      this.highlighter.off("unlocked", this.onLockStateChanged);
+      this.highlighter.destroy();
+    }
+
     if (this.walker) {
       this.walker.off("new-root", this.onNewRoot);
       this._destroyPromise = this.walker.release().then(null, console.error);
       delete this.walker;
       delete this.pageStyle;
     } else {
       this._destroyPromise = promise.resolve(null);
     }
@@ -459,22 +466,16 @@ InspectorPanel.prototype = {
     this.cancelUpdate();
     this.cancelLayoutChange();
 
     if (this.browser) {
       this.browser.removeEventListener("resize", this.scheduleLayoutChange, true);
       this.browser = null;
     }
 
-    if (this.highlighter) {
-      this.highlighter.off("locked", this.onLockStateChanged);
-      this.highlighter.off("unlocked", this.onLockStateChanged);
-      this.highlighter.destroy();
-    }
-
     this.target.off("thread-paused", this.updateDebuggerPausedWarning);
     this.target.off("thread-resumed", this.updateDebuggerPausedWarning);
     this._toolbox.off("select", this.updateDebuggerPausedWarning);
 
     this._toolbox = null;
 
     this.sidebar.off("select", this._setDefaultSidebar);
     this.sidebar.destroy();
--- a/browser/devtools/inspector/test/browser_inspector_basic_highlighter.js
+++ b/browser/devtools/inspector/test/browser_inspector_basic_highlighter.js
@@ -1,57 +1,89 @@
 /* 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/. */
 
 
-function test()
-{
+function test() {
   let inspector, doc;
   let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
   let {require} = devtools;
+  let promise = require("sdk/core/promise");
+  let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 
   waitForExplicitFinish();
 
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.selectedBrowser.addEventListener("load", function onload() {
     gBrowser.selectedBrowser.removeEventListener("load", onload, true);
     doc = content.document;
     waitForFocus(setupTest, content);
   }, true);
 
   content.location = "data:text/html,<h1>foo<h1><h2>bar</h2>";
 
-  function setupTest()
-  {
+  function setupTest() {
     let h = require("devtools/inspector/highlighter");
     h._forceBasic.value = true;
     openInspector(runTests);
   }
 
-  function runTests(aInspector)
-  {
+  function runTests(aInspector) {
     inspector = aInspector;
-    let h1 = doc.querySelector("h1");
-    inspector.selection.once("new-node-front", () => executeSoon(testH1Selected));
-    inspector.selection.setNode(h1);
+
+    Task.spawn(function() {
+      yield selectH1();
+      yield verifyH1Selected();
+      yield deselect();
+      yield verifyNoNodeSelected();
+
+      yield selectH1();
+      yield verifyH1Selected();
+      yield destroyInspector();
+      yield verifyNoNodeSelected();
+
+      finishUp();
+    }).then(null, Cu.reportError);
   }
 
-  function testH1Selected() {
+  function selectH1() {
+    let deferred = promise.defer();
+    let h1 = doc.querySelector("h1");
+    inspector.selection.once("new-node-front", () => {
+      executeSoon(deferred.resolve);
+    });
+    inspector.selection.setNode(h1);
+    return deferred.promise;
+  }
+
+  function verifyH1Selected() {
     let h1 = doc.querySelector("h1");
     let nodes = doc.querySelectorAll(":-moz-devtools-highlighted");
     is(nodes.length, 1, "only one node selected");
     is(nodes[0], h1, "h1 selected");
-    inspector.selection.once("new-node-front", () => executeSoon(testNoNodeSelected));
-    inspector.selection.setNode(null);
+    return promise.resolve();
   }
 
-  function testNoNodeSelected() {
-    ok(doc.querySelectorAll(":-moz-devtools-highlighted").length, 0, "no node selected");
-    finishUp();
+  function deselect() {
+    let deferred = promise.defer();
+    inspector.selection.once("new-node-front", () => {
+      executeSoon(deferred.resolve);
+    });
+    inspector.selection.setNode(null);
+    return deferred.promise;
+  }
+
+  function destroyInspector() {
+    return inspector.destroy();
+  }
+
+  function verifyNoNodeSelected() {
+    ok(doc.querySelectorAll(":-moz-devtools-highlighted").length === 0, "no node selected");
+    return promise.resolve();
   }
 
   function finishUp() {
     let h = require("devtools/inspector/highlighter");
     h._forceBasic.value = false;
     gBrowser.removeCurrentTab();
     finish();
   }
--- a/browser/devtools/main.js
+++ b/browser/devtools/main.js
@@ -141,17 +141,17 @@ Tools.styleEditor = {
   id: "styleeditor",
   key: l10n("open.commandkey", styleEditorStrings),
   ordinal: 4,
   accesskey: l10n("open.accesskey", styleEditorStrings),
   modifiers: "shift",
   icon: "chrome://browser/skin/devtools/tool-styleeditor.png",
   url: "chrome://browser/content/devtools/styleeditor.xul",
   label: l10n("ToolboxStyleEditor.label", styleEditorStrings),
-  tooltip: l10n("ToolboxStyleEditor.tooltip", styleEditorStrings),
+  tooltip: l10n("ToolboxStyleEditor.tooltip2", styleEditorStrings),
 
   isTargetSupported: function(target) {
     return true;
   },
 
   build: function(iframeWindow, toolbox) {
     let panel = new StyleEditorPanel(iframeWindow, toolbox);
     return panel.open();
--- a/browser/devtools/markupview/markup-view.css
+++ b/browser/devtools/markupview/markup-view.css
@@ -1,15 +1,24 @@
 /* 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/. */
 
-#root-wrapper {
-  overflow: hidden;
-  min-width: 250px;
+/* Force height and width (possibly overflowing) from inline elements.
+ * This allows long overflows of text or input fields to still be styled with
+ * the container, rather than the background disappearing when scrolling */
+#root {
+  float: left;
+  min-width: 100%;
+}
+
+#root-wrapper:after {
+   content: "";
+   display: block;
+   clear: both;
 }
 
 .children {
   list-style: none;
   padding: 0;
   margin: 0;
 }
 
@@ -21,24 +30,23 @@
 .tag-line {
   min-height: 1.4em;
   line-height: 1.4em;
   position: relative;
 }
 
 /* Children are indented thanks to their parent's left padding, that means they
  * are not stretching from edge to edge, which is what we want.
- * So we insert a pseudo-element and make sure it covers the whole "line" */
+ * So we insert an element and make sure it covers the whole "line" */
 .tag-line .highlighter {
-    content: "";
-    position: absolute;
-    left: -1000em;
-    right: 0;
-    height: 100%;
-    z-index: -1;
+  position: absolute;
+  left: -1000em;
+  right: 0;
+  height: 100%;
+  z-index: -1;
 }
 
 .expander {
   display: inline-block;
   margin-left: -14px;
   vertical-align: middle;
 }
 
@@ -57,16 +65,20 @@
 .child.collapsed > .tag-line ~ .tag-line {
   display: none;
 }
 
 .child.collapsed .close {
   display: inline;
 }
 
+.closing-bracket {
+  pointer-events: none;
+}
+
 .newattr {
   display: inline-block;
   width: 1em;
   height: 1ex;
   margin-right: -1em;
   padding: 1px 0;
 }
 
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -11,16 +11,18 @@ const PAGE_SIZE = 10;
 const PREVIEW_AREA = 700;
 const DEFAULT_MAX_CHILDREN = 100;
 const COLLAPSE_ATTRIBUTE_LENGTH = 120;
 const COLLAPSE_DATA_URL_REGEX = /^data.+base64/;
 const COLLAPSE_DATA_URL_LENGTH = 60;
 
 const {UndoStack} = require("devtools/shared/undo");
 const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
+const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+const {colorUtils} = require("devtools/shared/css-color");
 const promise = require("sdk/core/promise");
 
 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 loader.lazyGetter(this, "DOMParser", function() {
  return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
@@ -70,28 +72,31 @@ function MarkupView(aInspector, aFrame, 
   this.popup = new AutocompletePopup(this.doc.defaultView.parent.document, options);
 
   this.undo = new UndoStack();
   this.undo.installController(aControllerWindow);
 
   this._containers = new WeakMap();
 
   this._boundMutationObserver = this._mutationObserver.bind(this);
-  this.walker.on("mutations", this._boundMutationObserver)
+  this.walker.on("mutations", this._boundMutationObserver);
 
   this._boundOnNewSelection = this._onNewSelection.bind(this);
   this._inspector.selection.on("new-node-front", this._boundOnNewSelection);
   this._onNewSelection();
 
   this._boundKeyDown = this._onKeyDown.bind(this);
   this._frame.contentWindow.addEventListener("keydown", this._boundKeyDown, false);
 
   this._boundFocus = this._onFocus.bind(this);
   this._frame.addEventListener("focus", this._boundFocus, false);
 
+  this._handlePrefChange = this._handlePrefChange.bind(this);
+  gDevTools.on("pref-changed", this._handlePrefChange);
+
   this._initPreview();
 }
 
 exports.MarkupView = MarkupView;
 
 MarkupView.prototype = {
   _selectedContainer: null,
 
@@ -107,16 +112,43 @@ MarkupView.prototype = {
    * Get the MarkupContainer object for a given node, or undefined if
    * none exists.
    */
   getContainer: function MT_getContainer(aNode)
   {
     return this._containers.get(aNode);
   },
 
+  _handlePrefChange: function(event, data) {
+    if (data.pref == "devtools.defaultColorUnit") {
+      this.update();
+    }
+  },
+
+  update: function() {
+    let updateChildren = function(node) {
+      this.getContainer(node).update();
+      for (let child of node.treeChildren()) {
+        updateChildren(child);
+      }
+    }.bind(this);
+
+    // Start with the documentElement
+    let documentElement;
+    for (let node of this._rootNode.treeChildren()) {
+      if (node.isDocumentElement === true) {
+        documentElement = node;
+        break;
+      }
+    }
+
+    // Recursively update each node starting with documentElement.
+    updateChildren(documentElement);
+  },
+
   /**
    * Highlight the inspector selected node.
    */
   _onNewSelection: function MT__onNewSelection()
   {
     let done = this._inspector.updating("markup-view");
     if (this._inspector.selection.isNode()) {
       this.showNode(this._inspector.selection.nodeFront, true).then(() => {
@@ -375,17 +407,17 @@ MarkupView.prototype = {
 
       let container = this._containers.get(target);
       if (!container) {
         // Container might not exist if this came from a load event for a node
         // we're not viewing.
         continue;
       }
       if (type === "attributes" || type === "characterData") {
-        container.update();
+        container.update(false);
       } else if (type === "childList") {
         container.childrenDirty = true;
         this._updateChildren(container);
       }
     }
     this._waitForChildren().then(() => {
       this._inspector.emit("markupmutation");
     });
@@ -692,16 +724,18 @@ MarkupView.prototype = {
     });
   },
 
   /**
    * Tear down the markup panel.
    */
   destroy: function MT_destroy()
   {
+    gDevTools.off("pref-changed", this._handlePrefChange);
+
     this.undo.destroy();
     delete this.undo;
 
     this.popup.destroy();
     delete this.popup;
 
     this._frame.removeEventListener("focus", this._boundFocus, false);
     delete this._boundFocus;
@@ -1015,19 +1049,19 @@ MarkupContainer.prototype = {
       this.highlighter.classList.remove("theme-selected");
     }
   },
 
   /**
    * Update the container's editor to the current state of the
    * viewed node.
    */
-  update: function() {
+  update: function(parseColors=true) {
     if (this.editor.update) {
-      this.editor.update();
+      this.editor.update(parseColors);
     }
   },
 
   /**
    * Try to put keyboard focus on the current editor.
    */
   focus: function() {
     let focusable = this.editor.elt.querySelector("[tabindex]");
@@ -1236,17 +1270,17 @@ function ElementEditor(aContainer, aNode
 
   this.update();
 }
 
 ElementEditor.prototype = {
   /**
    * Update the state of the editor from the node.
    */
-  update: function EE_update()
+  update: function EE_update(parseColors=true)
   {
     let attrs = this.node.attributes;
     if (!attrs) {
       return;
     }
 
     // Hide all the attribute editors, they'll be re-shown if they're
     // still applicable.  Don't update attributes that are being
@@ -1255,20 +1289,23 @@ ElementEditor.prototype = {
     for (let i = 0; i < attrEditors.length; i++) {
       if (!attrEditors[i].inplaceEditor) {
         attrEditors[i].style.display = "none";
       }
     }
 
     // Get the attribute editor for each attribute that exists on
     // the node and show it.
-    for (let i = 0; i < attrs.length; i++) {
-      let attr = this._createAttribute(attrs[i]);
-      if (!attr.inplaceEditor) {
-        attr.style.removeProperty("display");
+    for (let attr of attrs) {
+      if (parseColors && typeof attr.value !== "undefined") {
+        attr.value = colorUtils.processCSSString(attr.value);
+      }
+      let attribute = this._createAttribute(attr);
+      if (!attribute.inplaceEditor) {
+        attribute.style.removeProperty("display");
       }
     }
   },
 
   _startModifyingAttributes: function() {
     return this.node.startModifyingAttributes();
   },
 
--- a/browser/devtools/markupview/markup-view.xhtml
+++ b/browser/devtools/markupview/markup-view.xhtml
@@ -24,17 +24,17 @@
       <li id="template-container" save="${elt}" class="child collapsed">
         <div save="${tagLine}" class="tag-line"><span save="${highlighter}" class="highlighter"></span><span save="${expander}" class="theme-twisty expander"></span></div>
         <ul save="${children}" class="children"></ul>
       </li>
 
       <li id="template-more-nodes" class="more-nodes devtools-class-comment" save="${elt}"><span>${showing}</span> <button href="#" onclick="${allButtonClick}">${showAll}</button></li>
     </ul>
 
-    <span id="template-element" save="${elt}" class="editor"><span class="open">&lt;<span save="${tag}" class="tag theme-fg-color3" tabindex="0"></span><span save="${attrList}"></span><span save="${newAttr}" class="newattr" tabindex="0"></span>&gt;</span><span class="close">&lt;/<span save="${closeTag}" class="tag theme-fg-color3"></span>&gt;</span></span>
+    <span id="template-element" save="${elt}" class="editor"><span class="open">&lt;<span save="${tag}" class="tag theme-fg-color3" tabindex="0"></span><span save="${attrList}"></span><span save="${newAttr}" class="newattr" tabindex="0"></span><span class="closing-bracket">&gt;</span></span><span class="close">&lt;/<span save="${closeTag}" class="tag theme-fg-color3"></span>&gt;</span></span>
 
     <span id="template-attribute" save="${attr}" data-attr="${attrName}" class="attreditor" style="display:none"> <span class="editable" save="${inner}" tabindex="0"><span save="${name}" class="attr-name theme-fg-color2"></span>=&quot;<span save="${val}" class="attr-value theme-fg-color6"></span>&quot;</span></span>
 
     <span id="template-text" save="${elt}" class="editor text">
       <pre save="${value}" style="display:inline-block;" tabindex="0"></pre>
     </span>
 
     <span id="template-comment" save="${elt}" class="editor comment theme-comment"><span>&lt;!--</span><pre save="${value}" style="display:inline-block;" tabindex="0"></pre><span>--&gt;</span></span>
--- a/browser/devtools/markupview/test/Makefile.in
+++ b/browser/devtools/markupview/test/Makefile.in
@@ -1,13 +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/.
 
 MOCHITEST_BROWSER_FILES := \
+		browser_inspector_markup_colorconversion.js \
 		browser_inspector_markup_navigation.html \
 		browser_inspector_markup_navigation.js \
 		browser_inspector_markup_mutation.html \
 		browser_inspector_markup_mutation.js \
 		browser_inspector_markup_edit.html \
     browser_inspector_markup_edit.js \
     browser_inspector_markup_subset.html \
     browser_inspector_markup_subset.js \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_colorconversion.js
@@ -0,0 +1,90 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+  let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
+  let inspector;
+  let doc;
+
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    doc = content.document;
+    waitForFocus(createDocument, content);
+  }, true);
+
+  content.location = "data:text/html,browser_inspector_markupview_colorconversion.js";
+
+  function createDocument() {
+    doc.body.innerHTML = '' +
+    '<span style="color:red; border-radius:10px; ' +
+    'background-color:rgba(0, 255, 0, 1); display: inline-block;">' +
+    'Some styled text</span>';
+
+    doc.title = "Style Inspector key binding test";
+
+    setupTest();
+  }
+
+  function setupTest() {
+    let target = TargetFactory.forTab(gBrowser.selectedTab);
+    gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+      inspector = toolbox.getCurrentPanel();
+      inspector.once("inspector-updated", checkColors);
+    });
+  }
+
+  function checkColors() {
+    let node = content.document.querySelector("span");
+    assertAttributes(node, {
+      style: "color:#F00; border-radius:10px; background-color:#0F0; display: inline-block;"
+    }).then(() => {
+      finishUp();
+    });
+  }
+
+  // This version of assertAttributes is different from that in other markup
+  // view tests. This is because in most tests we are checking the node
+  // attributes but here we need the actual values displayed in the markup panel.
+  function assertAttributes(node, attributes) {
+    let deferred = promise.defer();
+    let attrsToCheck = Object.getOwnPropertyNames(attributes);
+    ok(node, "captain, we have the node");
+
+    let checkAttrs = function() {
+      let container = inspector.markup._selectedContainer;
+      let nodeAttrs = container.editor.attrs;
+
+      is(node.attributes.length, attrsToCheck.length,
+        "Node has the correct number of attributes");
+
+      for (let attr of attrsToCheck) {
+        ok(nodeAttrs[attr], "Node has a " + attr + "attribute");
+
+        let nodeAttributeText = nodeAttrs[attr].textContent;
+        [, nodeAttributeText] = nodeAttributeText.match(/^\s*[\w-]+\s*=\s*"(.*)"$/);
+        is(nodeAttributeText, attributes[attr],
+          "Node has the correct " + attr + " attribute value.");
+      }
+      deferred.resolve();
+    };
+
+    if (inspector.selection.node == node) {
+      checkAttrs();
+    } else {
+      inspector.once("inspector-updated", () => {
+        checkAttrs();
+      });
+      inspector.selection.setNode(node);
+    }
+    return deferred.promise;
+  }
+
+  function finishUp() {
+    doc = inspector = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
--- a/browser/devtools/responsivedesign/responsivedesign.jsm
+++ b/browser/devtools/responsivedesign/responsivedesign.jsm
@@ -175,16 +175,22 @@ function ResponsiveUI(aWindow, aTab)
   // Events
   this.tab.addEventListener("TabClose", this);
   this.tabContainer.addEventListener("TabSelect", this);
   this.mainWindow.document.addEventListener("keypress", this.bound_onKeypress, false);
 
   this.buildUI();
   this.checkMenus();
 
+  this.docShell = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                      .getInterface(Ci.nsIWebNavigation)
+                      .QueryInterface(Ci.nsIDocShell);
+
+  this.docShell.deviceSizeIsPageSize = true;
+
   try {
     if (Services.prefs.getBoolPref("devtools.responsiveUI.rotate")) {
       this.rotate();
     }
   } catch(e) {}
 
   if (this._floatingScrollbars)
     switchToFloatingScrollbars(this.tab);
@@ -247,16 +253,18 @@ ResponsiveUI.prototype = {
   close: function RUI_unload() {
     if (this.closing)
       return;
     this.closing = true;
 
     this.browser.removeEventListener("load", this.bound_onPageLoad, true);
     this.browser.removeEventListener("unload", this.bound_onPageUnload, true);
 
+    this.docShell.deviceSizeIsPageSize = false;
+
     if (this._floatingScrollbars)
       switchToNativeScrollbars(this.tab);
 
     this.unCheckMenus();
     // Reset style of the stack.
     let style = "max-width: none;" +
                 "min-width: 0;" +
                 "max-height: none;" +
@@ -283,16 +291,17 @@ ResponsiveUI.prototype = {
     this.stack.removeChild(this.resizer);
     this.stack.removeChild(this.resizeBarV);
     this.stack.removeChild(this.resizeBarH);
 
     // Unset the responsive mode.
     this.container.removeAttribute("responsivemode");
     this.stack.removeAttribute("responsivemode");
 
+    delete this.docShell;
     delete this.tab.__responsiveUI;
     if (this.touchEventHandler)
       this.touchEventHandler.stop();
     this._telemetry.toolClosed("responsive");
     ResponsiveUIManager.emit("off", this.tab, this);
   },
 
   /**
--- a/browser/devtools/responsivedesign/test/Makefile.in
+++ b/browser/devtools/responsivedesign/test/Makefile.in
@@ -4,11 +4,12 @@
 
 MOCHITEST_BROWSER_FILES := \
 		browser_responsiveui.js \
 		browser_responsiveuiaddcustompreset.js \
 		browser_responsiveruleview.js \
 		browser_responsive_cmd.js \
 		browser_responsivecomputedview.js \
 		browser_responsiveui_touch.js \
+		browser_responsive_devicewidth.js \
 		touch.html \
 		head.js \
 		$(NULL)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/responsivedesign/test/browser_responsive_devicewidth.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+  let instance;
+  let mgr = ResponsiveUI.ResponsiveUIManager;
+
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    waitForFocus(startTest, content);
+  }, true);
+
+  content.location = "data:text/html,mop";
+
+  function startTest() {
+    mgr.once("on", function() {executeSoon(onUIOpen)});
+    document.getElementById("Tools:ResponsiveUI").doCommand();
+  }
+
+  function onUIOpen() {
+    instance = gBrowser.selectedTab.__responsiveUI;
+    instance.stack.setAttribute("notransition", "true");
+    ok(instance, "instance of the module is attached to the tab.");
+
+    let mql = content.matchMedia("(max-device-width:100px)")
+
+    ok(!mql.matches, "media query doesn't match.");
+
+    mql.addListener(onMediaChange);
+    instance.setSize(90, 500);
+  }
+
+  function onMediaChange(mql) {
+    mql.removeListener(onMediaChange);
+    ok(mql.matches, "media query matches.");
+    ok(window.screen.width != content.screen.width, "screen.width is not the size of the screen.");
+    is(content.screen.width, 90, "screen.width is the width of the page.");
+    is(content.screen.height, 500, "screen.height is the height of the page.");
+
+
+    let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor)
+                          .getInterface(Ci.nsIWebNavigation)
+                          .QueryInterface(Ci.nsIDocShell);
+
+    mql.addListener(onMediaChange2);
+    docShell.deviceSizeIsPageSize = false;
+  }
+
+  function onMediaChange2(mql) {
+    mql.removeListener(onMediaChange);
+    ok(!mql.matches, "media query has been re-evaluated.");
+    ok(window.screen.width == content.screen.width, "screen.width is not the size of the screen.");
+    instance.stack.removeAttribute("notransition");
+    document.getElementById("Tools:ResponsiveUI").doCommand();
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/css-color.js
@@ -0,0 +1,398 @@
+/* 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 COLOR_UNIT_PREF = "devtools.defaultColorUnit";
+const {Cc, Ci, Cu} = require("chrome");
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+/**
+ * This module is used to convert between various color types.
+ *
+ * Usage:
+ *   let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+ *   let {colorUtils} = devtools.require("devtools/shared/css-color");
+ *
+ *   color.authored === "red"
+ *   color.hasAlpha === false
+ *   color.valid === true
+ *   color.transparent === false // transparent has a special status.
+ *   color.name === "red"        // returns hex or rgba when no name available.
+ *   color.hex === "#F00"        // returns shortHex when available else returns
+ *                                  longHex. If alpha channel is present then we
+ *                                  return this.rgba.
+ *   color.longHex === "#FF0000" // If alpha channel is present then we return
+ *                                  this.rgba.
+ *   color.rgb === "rgb(255, 0, 0)" // If alpha channel is present then we return
+ *                                     this.rgba.
+ *   color.rgba === "rgba(255, 0, 0, 1)"
+ *   color.hsl === "hsl(0, 100%, 50%)"
+ *   color.hsla === "hsla(0, 100%, 50%, 1)" // If alpha channel is present
+ *                                             then we return this.rgba.
+ *
+ *   color.toString() === "#F00"; // Outputs the color type determined in the
+ *                                   COLOR_UNIT_PREF constant (above).
+ *   // Color objects can be reused
+ *   color.newColor("green") === "#0F0"; // true
+ *
+ *   let processed = colorUtils.processCSSString("color:red; background-color:green;");
+ *   // Returns "color:#F00; background-color:#0F0;"
+ *
+ *   Valid values for COLOR_UNIT_PREF are contained in CssColor.COLORUNIT.
+ */
+
+function CssColor(colorValue) {
+  this.newColor(colorValue);
+}
+
+module.exports.colorUtils = {
+  CssColor: CssColor,
+  processCSSString: processCSSString
+};
+
+/**
+ * Values used in COLOR_UNIT_PREF
+ */
+CssColor.COLORUNIT = {
+  "authored": "authored",
+  "hex": "hex",
+  "name": "name",
+  "rgb": "rgb",
+  "hsl": "hsl"
+};
+
+CssColor.prototype = {
+  authored: null,
+
+  get hasAlpha() {
+    if (!this.valid || this.transparent) {
+      return false;
+    }
+    return this._getRGBATuple().a !== 1;
+  },
+
+  get valid() {
+    return this._validateColor(this.authored);
+  },
+
+  get transparent() {
+    try {
+      let tuple = this._getRGBATuple();
+      return tuple === "transparent";
+    } catch(e) {
+      return false;
+    }
+  },
+
+  get name() {
+    if (!this.valid) {
+      return "";
+    }
+    if (this.authored === "transparent") {
+      return "transparent";
+    }
+    try {
+      let tuple = this._getRGBATuple();
+
+      if (tuple === "transparent") {
+        return "transparent";
+      }
+      if (tuple.a !== 1) {
+        return this.rgb;
+      }
+      let {r, g, b} = tuple;
+      return DOMUtils.rgbToColorName(r, g, b);
+    } catch(e) {
+      return this.hex;
+    }
+  },
+
+  get hex() {
+    if (!this.valid) {
+      return "";
+    }
+    if (this.hasAlpha) {
+      return this.rgba;
+    }
+    if (this.transparent) {
+      return "transparent";
+    }
+
+    let hex = this.longHex;
+    if (hex.charAt(1) == hex.charAt(2) &&
+        hex.charAt(3) == hex.charAt(4) &&
+        hex.charAt(5) == hex.charAt(6)) {
+      hex = "#" + hex.charAt(1) + hex.charAt(3) + hex.charAt(5);
+    }
+    return hex;
+  },
+
+  get longHex() {
+    if (!this.valid) {
+      return "";
+    }
+    if (this.hasAlpha) {
+      return this.rgba;
+    }
+    if (this.transparent) {
+      return "transparent";
+    }
+    return this.rgb.replace(/\brgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)/gi, function(_, r, g, b) {
+      return "#" + ((1 << 24) + (r << 16) + (g << 8) + (b << 0)).toString(16).substr(-6).toUpperCase();
+    });
+  },
+
+  get rgb() {
+    if (!this.valid) {
+      return "";
+    }
+    if (this.transparent) {
+      return "transparent";
+    }
+    if (!this.hasAlpha) {
+      let tuple = this._getRGBATuple();
+      return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")";
+    }
+    return this.rgba;
+  },
+
+  get rgba() {
+    if (!this.valid) {
+      return "";
+    }
+    if (this.transparent) {
+      return "transparent";
+    }
+    let components = this._getRGBATuple();
+    return "rgba(" + components.r + ", " +
+                     components.g + ", " +
+                     components.b + ", " +
+                     components.a + ")";
+  },
+
+  get hsl() {
+    if (!this.valid) {
+      return "";
+    }
+    if (this.transparent) {
+      return "transparent";
+    }
+    if (this.hasAlpha) {
+      return this.hsla;
+    }
+    return this._hslNoAlpha();
+  },
+
+  get hsla() {
+    if (!this.valid) {
+      return "";
+    }
+    if (this.transparent) {
+      return "transparent";
+    }
+    // Because an hsla rbg roundtrip would lose accuracy we use the authored
+    // values if this is an hsla color.
+    if (this.authored.startsWith("hsla(")) {
+      let [, h, s, l, a] = /^\bhsla\(([\d.]+),\s*([\d.]+%),\s*([\d.]+%),\s*([\d.]+|0|1)\)$/gi.exec(this.authored);
+      return "hsla(" + h + ", " + s + ", " + l + ", " + a + ")";
+    }
+    if (this.hasAlpha) {
+      let a = this._getRGBATuple().a;
+      return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", " + a + ")");
+    }
+    return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", 1)");
+  },
+
+  /**
+   * Change color
+   *
+   * @param  {String} color
+   *         Any valid color string
+   */
+  newColor: function(color) {
+    this.authored = color.toLowerCase();
+    return this;
+  },
+
+  /**
+   * Return a string representing a color of type defined in COLOR_UNIT_PREF.
+   */
+  toString: function() {
+    let color;
+    let defaultUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF);
+    let unit = CssColor.COLORUNIT[defaultUnit];
+
+    switch(unit) {
+      case CssColor.COLORUNIT.authored:
+        color = this.authored;
+        break;
+      case CssColor.COLORUNIT.hex:
+        color = this.hex;
+        break;
+      case CssColor.COLORUNIT.hsl:
+        color = this.hsl;
+        break;
+      case CssColor.COLORUNIT.name:
+        color = this.name;
+        break;
+      case CssColor.COLORUNIT.rgb:
+        color = this.rgb;
+        break;
+      default:
+        color = this.rgb;
+    }
+    return color;
+  },
+
+  /**
+   * Returns a RGBA 4-Tuple representation of a color or transparent as
+   * appropriate.
+   */
+  _getRGBATuple: function() {
+    let win = Services.appShell.hiddenDOMWindow;
+    let doc = win.document;
+    let span = doc.createElement("span");
+    span.style.color = this.authored;
+    let computed = win.getComputedStyle(span).color;
+
+    if (computed === "transparent") {
+      return "transparent";
+    }
+
+    let rgba = /^rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+\.\d+|1|0)\)$/gi.exec(computed);
+
+    if (rgba) {
+      let [, r, g, b, a] = rgba;
+      return {r: r, g: g, b: b, a: a};
+    } else {
+      let rgb = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/gi.exec(computed);
+      let [, r, g, b] = rgb;
+
+      return {r: r, g: g, b: b, a: 1};
+    }
+  },
+
+  _hslNoAlpha: function() {
+    let {r, g, b} = this._getRGBATuple();
+
+    // Because an hsl rbg roundtrip would lose accuracy we use the authored
+    // values if this is an hsla color.
+    if (this.authored.startsWith("hsl(")) {
+      let [, h, s, l] = /^\bhsl\(([\d.]+),\s*([\d.]+%),\s*([\d.]+%)\)$/gi.exec(this.authored);
+      return "hsl(" + h + ", " + s + ", " + l + ")";
+    }
+
+    r = r / 255;
+    g = g / 255;
+    b = b / 255;
+
+    let max = Math.max(r, g, b);
+    let min = Math.min(r, g, b);
+    let h;
+    let s;
+    let l = (max + min) / 2;
+
+    if(max == min){
+      h = s = 0;
+    } else {
+      let d = max - min;
+      s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+
+      switch(max) {
+          case r:
+            h = ((g - b) / d) % 6;
+            break;
+          case g:
+            h = (b - r) / d + 2;
+            break;
+          case b:
+            h = (r - g) / d + 4;
+            break;
+      }
+      h *= 60;
+      if (h < 0) {
+        h += 360;
+      }
+    }
+    return "hsl(" + (Math.round(h * 1000)) / 1000 +
+           ", " + Math.round(s * 100) +
+           "%, " + Math.round(l * 100) + "%)";
+  },
+
+  /**
+   * This method allows comparison of CssColor objects using ===.
+   */
+  valueOf: function() {
+    return this.rgba;
+  },
+
+  _validateColor: function(color) {
+    if (typeof color !== "string" || color === "") {
+      return false;
+    }
+
+    let win = Services.appShell.hiddenDOMWindow;
+    let doc = win.document;
+
+    // Create a black span in a hidden window.
+    let span = doc.createElement("span");
+    span.style.color = "rgb(0, 0, 0)";
+
+    // Attempt to set the color. If the color is no longer black we know that
+    // color is valid.
+    span.style.color = color;
+    if (span.style.color !== "rgb(0, 0, 0)") {
+      return true;
+    }
+
+    // If the color is black then the above check will have failed. We change
+    // the span to white and attempt to reapply the color. If the span is not
+    // white then we know that the color is valid otherwise we return invalid.
+    span.style.color = "rgb(255, 255, 255)";
+    span.style.color = color;
+    return span.style.color !== "rgb(255, 255, 255)";
+  },
+};
+
+/**
+ * Process a CSS string
+ *
+ * @param  {String} value
+ *         CSS string e.g. "color:red; background-color:green;"
+ * @return {String}
+ *         Converted CSS String e.g. "color:#F00; background-color:#0F0;"
+ */
+function processCSSString(value) {
+  if (value && /^""$/.test(value)) {
+    return value;
+  }
+
+  // This regex matches:
+  //  - #F00
+  //  - #FF0000
+  //  - hsl()
+  //  - hsla()
+  //  - rgb()
+  //  - rgba()
+  //  - red
+  //
+  //  It also matches css keywords e.g. "background-color" otherwise
+  //  "background" would be replaced with #6363CE ("background" is a platform
+  //  color).
+  let colorPattern = /#[0-9a-fA-F]{3}\b|#[0-9a-fA-F]{6}\b|hsl\(.*?\)|hsla\(.*?\)|rgba?\(.*?\)|\b[a-zA-Z-]+\b/g;
+
+  value = value.replace(colorPattern, function(match) {
+    let color = new CssColor(match);
+    if (color.valid) {
+      return color;
+    }
+    return match;
+  });
+  return value;
+}
+
+loader.lazyGetter(this, "DOMUtils", function () {
+  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
--- a/browser/devtools/shared/inplace-editor.js
+++ b/browser/devtools/shared/inplace-editor.js
@@ -196,19 +196,21 @@ function InplaceEditor(aOptions, aEvent)
   if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) {
     this.input.select();
   }
   this.input.focus();
 
   this.input.addEventListener("blur", this._onBlur, false);
   this.input.addEventListener("keypress", this._onKeyPress, false);
   this.input.addEventListener("input", this._onInput, false);
-  this.input.addEventListener("mousedown", function(aEvt) {
-                                             aEvt.stopPropagation();
-                                           }, false);
+
+  this.input.addEventListener("dblclick",
+    (e) => { e.stopPropagation(); }, false);
+  this.input.addEventListener("mousedown",
+    (e) => { e.stopPropagation(); }, false);
 
   this.validate = aOptions.validate;
 
   if (this.validate) {
     this.input.addEventListener("keyup", this._onKeyup, false);
   }
 
   if (aOptions.start) {
--- a/browser/devtools/shared/test/Makefile.in
+++ b/browser/devtools/shared/test/Makefile.in
@@ -1,14 +1,15 @@
 #
 # 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/.
 
 MOCHITEST_BROWSER_FILES = \
+		browser_css_color.js \
 		browser_eventemitter_basic.js \
 		browser_observableobject.js \
 		browser_layoutHelpers.js \
 		browser_require_basic.js \
 		browser_telemetry_buttonsandsidebar.js \
 		browser_telemetry_toolboxtabs_inspector.js \
 		browser_telemetry_toolboxtabs_jsdebugger.js \
 		browser_telemetry_toolboxtabs_jsprofiler.js \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_css_color.js
@@ -0,0 +1,310 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
+
+let origColorUnit;
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+let {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+let {colorUtils} = devtools.require("devtools/shared/css-color");
+
+function test() {
+  // FIXME: Enable this test on Linux once bug 916544 is fixed
+  if (Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS === "Linux") {
+    Services = colorUtils.CssColor = Loader = null;
+    return;
+  }
+
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    waitForFocus(init, content);
+  }, true);
+
+  content.location = "data:text/html,browser_css_color.js";
+}
+
+function init() {
+  origColorUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF);
+  createDocument();
+}
+
+function createDocument()
+{
+  let doc = content.document;
+
+  let canvas = doc.createElement("canvas");
+  canvas.width = canvas.height = 10;
+  doc.body.appendChild(canvas);
+
+  testColorUtils();
+}
+
+function testColorUtils() {
+  let data = getTestData();
+
+  for (let {authored, name, hex, hsl, rgb} of data) {
+    let color = new colorUtils.CssColor(authored);
+
+    // Check all values.
+    is(color.name, name, "color.name === name");
+    is(color.hex, hex, "color.hex === hex");
+    is(color.hsl, hsl, "color.hsl === hsl");
+    is(color.rgb, rgb, "color.rgb === rgb");
+
+    testToString(color, name, hex, hsl, rgb);
+    testColorMatch(name, hex, hsl, rgb, color.rgba);
+  }
+  testProcessCSSString();
+  finishUp();
+}
+
+function testToString(color, name, hex, hsl, rgb) {
+  switchColorUnit(colorUtils.CssColor.COLORUNIT.name);
+  is(color.toString(), name, "toString() with authored type");
+
+  switchColorUnit(colorUtils.CssColor.COLORUNIT.hex);
+  is(color.toString(), hex, "toString() with hex type");
+
+  switchColorUnit(colorUtils.CssColor.COLORUNIT.hsl);
+  is(color.toString(), hsl, "toString() with hsl type");
+
+  switchColorUnit(colorUtils.CssColor.COLORUNIT.rgb);
+  is(color.toString(), rgb, "toString() with rgb type");
+
+}
+
+function switchColorUnit(unit) {
+  Services.prefs.setCharPref(COLOR_UNIT_PREF, unit);
+}
+
+function testColorMatch(name, hex, hsl, rgb, rgba) {
+  let target;
+
+  let canvas = content.document.querySelector("canvas");
+  let ctx = canvas.getContext("2d");
+
+  let clearCanvas = function() {
+    canvas.width = 1;
+  };
+  let setColor = function(aColor) {
+    ctx.fillStyle = aColor;
+    ctx.fillRect(0, 0, 1, 1);
+  };
+  let setTargetColor = function() {
+    clearCanvas();
+    // All colors have rgba so we can use this to compare against.
+    setColor(rgba);
+    let [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
+    target = {r: r, g: g, b: b, a: a};
+  };
+  let test = function(aColor, type) {
+    let tolerance = 3; // hsla -> rgba -> hsla produces inaccurate results so we
+                       // need some tolerence here.
+    clearCanvas();
+
+    setColor(aColor);
+    let [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
+
+    let rgbFail = Math.abs(r - target.r) > tolerance ||
+                  Math.abs(g - target.g) > tolerance ||
+                  Math.abs(b - target.b) > tolerance;
+    ok(!rgbFail, "color " + rgba + " matches target. Type: " + type);
+    if (rgbFail) {
+      info("target: " + (target.toSource()) + ", color: [r: " + r + ", g: " + g + ", b: " + b + ", a: " + a + "]");
+    }
+
+    let alphaFail = a !== target.a;
+    ok(!alphaFail, "color " + rgba + " alpha value matches target.");
+  };
+
+  setTargetColor();
+
+  test(name, "name");
+  test(hex, "hex");
+  test(hsl, "hsl");
+  test(rgb, "rgb");
+  switchColorUnit(origColorUnit);
+}
+
+function testProcessCSSString() {
+  let before = "border: 1px solid red; border-radius: 5px; " +
+               "color rgb(0, 255, 0); font-weight: bold; " +
+               "background-color: transparent; " +
+               "border-top-color: rgba(0, 0, 255, 0.5);";
+  let expected = "border: 1px solid #F00; border-radius: 5px; " +
+                 "color #0F0; font-weight: bold; " +
+                 "background-color: transparent; " +
+                 "border-top-color: rgba(0, 0, 255, 0.5);";
+  let after = colorUtils.processCSSString(before);
+
+  is(after, expected, "CSS string processed correctly");
+}
+
+function finishUp() {
+  Services = colorUtils.CssColor = Loader = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function getTestData() {
+  return [
+    {authored: "aliceblue", name: "aliceblue", hex: "#F0F8FF", hsl: "hsl(208, 100%, 97%)", rgb: "rgb(240, 248, 255)"},
+    {authored: "antiquewhite", name: "antiquewhite", hex: "#FAEBD7", hsl: "hsl(34.286, 78%, 91%)", rgb: "rgb(250, 235, 215)"},
+    {authored: "aqua", name: "aqua", hex: "#0FF", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)"},
+    {authored: "aquamarine", name: "aquamarine", hex: "#7FFFD4", hsl: "hsl(159.844, 100%, 75%)", rgb: "rgb(127, 255, 212)"},
+    {authored: "azure", name: "azure", hex: "#F0FFFF", hsl: "hsl(180, 100%, 97%)", rgb: "rgb(240, 255, 255)"},
+    {authored: "beige", name: "beige", hex: "#F5F5DC", hsl: "hsl(60, 56%, 91%)", rgb: "rgb(245, 245, 220)"},
+    {authored: "bisque", name: "bisque", hex: "#FFE4C4", hsl: "hsl(32.542, 100%, 88%)", rgb: "rgb(255, 228, 196)"},
+    {authored: "black", name: "black", hex: "#000", hsl: "hsl(0, 0%, 0%)", rgb: "rgb(0, 0, 0)"},
+    {authored: "blanchedalmond", name: "blanchedalmond", hex: "#FFEBCD", hsl: "hsl(36, 100%, 90%)", rgb: "rgb(255, 235, 205)"},
+    {authored: "blue", name: "blue", hex: "#00F", hsl: "hsl(240, 100%, 50%)", rgb: "rgb(0, 0, 255)"},
+    {authored: "blueviolet", name: "blueviolet", hex: "#8A2BE2", hsl: "hsl(271.148, 76%, 53%)", rgb: "rgb(138, 43, 226)"},
+    {authored: "brown", name: "brown", hex: "#A52A2A", hsl: "hsl(0, 59%, 41%)", rgb: "rgb(165, 42, 42)"},
+    {authored: "burlywood", name: "burlywood", hex: "#DEB887", hsl: "hsl(33.793, 57%, 70%)", rgb: "rgb(222, 184, 135)"},
+    {authored: "cadetblue", name: "cadetblue", hex: "#5F9EA0", hsl: "hsl(181.846, 25%, 50%)", rgb: "rgb(95, 158, 160)"},
+    {authored: "chartreuse", name: "chartreuse", hex: "#7FFF00", hsl: "hsl(90.118, 100%, 50%)", rgb: "rgb(127, 255, 0)"},
+    {authored: "chocolate", name: "chocolate", hex: "#D2691E", hsl: "hsl(25, 75%, 47%)", rgb: "rgb(210, 105, 30)"},
+    {authored: "coral", name: "coral", hex: "#FF7F50", hsl: "hsl(16.114, 100%, 66%)", rgb: "rgb(255, 127, 80)"},
+    {authored: "cornflowerblue", name: "cornflowerblue", hex: "#6495ED", hsl: "hsl(218.54, 79%, 66%)", rgb: "rgb(100, 149, 237)"},
+    {authored: "cornsilk", name: "cornsilk", hex: "#FFF8DC", hsl: "hsl(48, 100%, 93%)", rgb: "rgb(255, 248, 220)"},
+    {authored: "crimson", name: "crimson", hex: "#DC143C", hsl: "hsl(348, 83%, 47%)", rgb: "rgb(220, 20, 60)"},
+    {authored: "cyan", name: "aqua", hex: "#0FF", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)"},
+    {authored: "darkblue", name: "darkblue", hex: "#00008B", hsl: "hsl(240, 100%, 27%)", rgb: "rgb(0, 0, 139)"},
+    {authored: "darkcyan", name: "darkcyan", hex: "#008B8B", hsl: "hsl(180, 100%, 27%)", rgb: "rgb(0, 139, 139)"},
+    {authored: "darkgoldenrod", name: "darkgoldenrod", hex: "#B8860B", hsl: "hsl(42.659, 89%, 38%)", rgb: "rgb(184, 134, 11)"},
+    {authored: "darkgray", name: "darkgray", hex: "#A9A9A9", hsl: "hsl(0, 0%, 66%)", rgb: "rgb(169, 169, 169)"},
+    {authored: "darkgreen", name: "darkgreen", hex: "#006400", hsl: "hsl(120, 100%, 20%)", rgb: "rgb(0, 100, 0)"},
+    {authored: "darkgrey", name: "darkgray", hex: "#A9A9A9", hsl: "hsl(0, 0%, 66%)", rgb: "rgb(169, 169, 169)"},
+    {authored: "darkkhaki", name: "darkkhaki", hex: "#BDB76B", hsl: "hsl(55.61, 38%, 58%)", rgb: "rgb(189, 183, 107)"},
+    {authored: "darkmagenta", name: "darkmagenta", hex: "#8B008B", hsl: "hsl(300, 100%, 27%)", rgb: "rgb(139, 0, 139)"},
+    {authored: "darkolivegreen", name: "darkolivegreen", hex: "#556B2F", hsl: "hsl(82, 39%, 30%)", rgb: "rgb(85, 107, 47)"},
+    {authored: "darkorange", name: "darkorange", hex: "#FF8C00", hsl: "hsl(32.941, 100%, 50%)", rgb: "rgb(255, 140, 0)"},
+    {authored: "darkorchid", name: "darkorchid", hex: "#9932CC", hsl: "hsl(280.13, 61%, 50%)", rgb: "rgb(153, 50, 204)"},
+    {authored: "darkred", name: "darkred", hex: "#8B0000", hsl: "hsl(0, 100%, 27%)", rgb: "rgb(139, 0, 0)"},
+    {authored: "darksalmon", name: "darksalmon", hex: "#E9967A", hsl: "hsl(15.135, 72%, 70%)", rgb: "rgb(233, 150, 122)"},
+    {authored: "darkseagreen", name: "darkseagreen", hex: "#8FBC8F", hsl: "hsl(120, 25%, 65%)", rgb: "rgb(143, 188, 143)"},
+    {authored: "darkslateblue", name: "darkslateblue", hex: "#483D8B", hsl: "hsl(248.462, 39%, 39%)", rgb: "rgb(72, 61, 139)"},
+    {authored: "darkslategray", name: "darkslategray", hex: "#2F4F4F", hsl: "hsl(180, 25%, 25%)", rgb: "rgb(47, 79, 79)"},
+    {authored: "darkslategrey", name: "darkslategray", hex: "#2F4F4F", hsl: "hsl(180, 25%, 25%)", rgb: "rgb(47, 79, 79)"},
+    {authored: "darkturquoise", name: "darkturquoise", hex: "#00CED1", hsl: "hsl(180.861, 100%, 41%)", rgb: "rgb(0, 206, 209)"},
+    {authored: "darkviolet", name: "darkviolet", hex: "#9400D3", hsl: "hsl(282.085, 100%, 41%)", rgb: "rgb(148, 0, 211)"},
+    {authored: "deeppink", name: "deeppink", hex: "#FF1493", hsl: "hsl(327.574, 100%, 54%)", rgb: "rgb(255, 20, 147)"},
+    {authored: "deepskyblue", name: "deepskyblue", hex: "#00BFFF", hsl: "hsl(195.059, 100%, 50%)", rgb: "rgb(0, 191, 255)"},
+    {authored: "dimgray", name: "dimgray", hex: "#696969", hsl: "hsl(0, 0%, 41%)", rgb: "rgb(105, 105, 105)"},
+    {authored: "dodgerblue", name: "dodgerblue", hex: "#1E90FF", hsl: "hsl(209.6, 100%, 56%)", rgb: "rgb(30, 144, 255)"},
+    {authored: "firebrick", name: "firebrick", hex: "#B22222", hsl: "hsl(0, 68%, 42%)", rgb: "rgb(178, 34, 34)"},
+    {authored: "floralwhite", name: "floralwhite", hex: "#FFFAF0", hsl: "hsl(40, 100%, 97%)", rgb: "rgb(255, 250, 240)"},
+    {authored: "forestgreen", name: "forestgreen", hex: "#228B22", hsl: "hsl(120, 61%, 34%)", rgb: "rgb(34, 139, 34)"},
+    {authored: "fuchsia", name: "fuchsia", hex: "#F0F", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)"},
+    {authored: "gainsboro", name: "gainsboro", hex: "#DCDCDC", hsl: "hsl(0, 0%, 86%)", rgb: "rgb(220, 220, 220)"},
+    {authored: "ghostwhite", name: "ghostwhite", hex: "#F8F8FF", hsl: "hsl(240, 100%, 99%)", rgb: "rgb(248, 248, 255)"},
+    {authored: "gold", name: "gold", hex: "#FFD700", hsl: "hsl(50.588, 100%, 50%)", rgb: "rgb(255, 215, 0)"},
+    {authored: "goldenrod", name: "goldenrod", hex: "#DAA520", hsl: "hsl(42.903, 74%, 49%)", rgb: "rgb(218, 165, 32)"},
+    {authored: "gray", name: "gray", hex: "#808080", hsl: "hsl(0, 0%, 50%)", rgb: "rgb(128, 128, 128)"},
+    {authored: "green", name: "green", hex: "#008000", hsl: "hsl(120, 100%, 25%)", rgb: "rgb(0, 128, 0)"},
+    {authored: "greenyellow", name: "greenyellow", hex: "#ADFF2F", hsl: "hsl(83.654, 100%, 59%)", rgb: "rgb(173, 255, 47)"},
+    {authored: "grey", name: "gray", hex: "#808080", hsl: "hsl(0, 0%, 50%)", rgb: "rgb(128, 128, 128)"},
+    {authored: "honeydew", name: "honeydew", hex: "#F0FFF0", hsl: "hsl(120, 100%, 97%)", rgb: "rgb(240, 255, 240)"},
+    {authored: "hotpink", name: "hotpink", hex: "#FF69B4", hsl: "hsl(330, 100%, 71%)", rgb: "rgb(255, 105, 180)"},
+    {authored: "indianred", name: "indianred", hex: "#CD5C5C", hsl: "hsl(0, 53%, 58%)", rgb: "rgb(205, 92, 92)"},
+    {authored: "indigo", name: "indigo", hex: "#4B0082", hsl: "hsl(274.615, 100%, 25%)", rgb: "rgb(75, 0, 130)"},
+    {authored: "ivory", name: "ivory", hex: "#FFFFF0", hsl: "hsl(60, 100%, 97%)", rgb: "rgb(255, 255, 240)"},
+    {authored: "khaki", name: "khaki", hex: "#F0E68C", hsl: "hsl(54, 77%, 75%)", rgb: "rgb(240, 230, 140)"},
+    {authored: "lavender", name: "lavender", hex: "#E6E6FA", hsl: "hsl(240, 67%, 94%)", rgb: "rgb(230, 230, 250)"},
+    {authored: "lavenderblush", name: "lavenderblush", hex: "#FFF0F5", hsl: "hsl(340, 100%, 97%)", rgb: "rgb(255, 240, 245)"},
+    {authored: "lawngreen", name: "lawngreen", hex: "#7CFC00", hsl: "hsl(90.476, 100%, 49%)", rgb: "rgb(124, 252, 0)"},
+    {authored: "lemonchiffon", name: "lemonchiffon", hex: "#FFFACD", hsl: "hsl(54, 100%, 90%)", rgb: "rgb(255, 250, 205)"},
+    {authored: "lightblue", name: "lightblue", hex: "#ADD8E6", hsl: "hsl(194.737, 53%, 79%)", rgb: "rgb(173, 216, 230)"},
+    {authored: "lightcoral", name: "lightcoral", hex: "#F08080", hsl: "hsl(0, 79%, 72%)", rgb: "rgb(240, 128, 128)"},
+    {authored: "lightcyan", name: "lightcyan", hex: "#E0FFFF", hsl: "hsl(180, 100%, 94%)", rgb: "rgb(224, 255, 255)"},
+    {authored: "lightgoldenrodyellow", name: "lightgoldenrodyellow", hex: "#FAFAD2", hsl: "hsl(60, 80%, 90%)", rgb: "rgb(250, 250, 210)"},
+    {authored: "lightgray", name: "lightgray", hex: "#D3D3D3", hsl: "hsl(0, 0%, 83%)", rgb: "rgb(211, 211, 211)"},
+    {authored: "lightgreen", name: "lightgreen", hex: "#90EE90", hsl: "hsl(120, 73%, 75%)", rgb: "rgb(144, 238, 144)"},
+    {authored: "lightgrey", name: "lightgray", hex: "#D3D3D3", hsl: "hsl(0, 0%, 83%)", rgb: "rgb(211, 211, 211)"},
+    {authored: "lightpink", name: "lightpink", hex: "#FFB6C1", hsl: "hsl(350.959, 100%, 86%)", rgb: "rgb(255, 182, 193)"},
+    {authored: "lightsalmon", name: "lightsalmon", hex: "#FFA07A", hsl: "hsl(17.143, 100%, 74%)", rgb: "rgb(255, 160, 122)"},
+    {authored: "lightseagreen", name: "lightseagreen", hex: "#20B2AA", hsl: "hsl(176.712, 70%, 41%)", rgb: "rgb(32, 178, 170)"},
+    {authored: "lightskyblue", name: "lightskyblue", hex: "#87CEFA", hsl: "hsl(202.957, 92%, 75%)", rgb: "rgb(135, 206, 250)"},
+    {authored: "lightslategray", name: "lightslategray", hex: "#789", hsl: "hsl(210, 14%, 53%)", rgb: "rgb(119, 136, 153)"},
+    {authored: "lightslategrey", name: "lightslategray", hex: "#789", hsl: "hsl(210, 14%, 53%)", rgb: "rgb(119, 136, 153)"},
+    {authored: "lightsteelblue", name: "lightsteelblue", hex: "#B0C4DE", hsl: "hsl(213.913, 41%, 78%)", rgb: "rgb(176, 196, 222)"},
+    {authored: "lightyellow", name: "lightyellow", hex: "#FFFFE0", hsl: "hsl(60, 100%, 94%)", rgb: "rgb(255, 255, 224)"},
+    {authored: "lime", name: "lime", hex: "#0F0", hsl: "hsl(120, 100%, 50%)", rgb: "rgb(0, 255, 0)"},
+    {authored: "limegreen", name: "limegreen", hex: "#32CD32", hsl: "hsl(120, 61%, 50%)", rgb: "rgb(50, 205, 50)"},
+    {authored: "linen", name: "linen", hex: "#FAF0E6", hsl: "hsl(30, 67%, 94%)", rgb: "rgb(250, 240, 230)"},
+    {authored: "magenta", name: "fuchsia", hex: "#F0F", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)"},
+    {authored: "maroon", name: "maroon", hex: "#800000", hsl: "hsl(0, 100%, 25%)", rgb: "rgb(128, 0, 0)"},
+    {authored: "mediumaquamarine", name: "mediumaquamarine", hex: "#66CDAA", hsl: "hsl(159.612, 51%, 60%)", rgb: "rgb(102, 205, 170)"},
+    {authored: "mediumblue", name: "mediumblue", hex: "#0000CD", hsl: "hsl(240, 100%, 40%)", rgb: "rgb(0, 0, 205)"},
+    {authored: "mediumorchid", name: "mediumorchid", hex: "#BA55D3", hsl: "hsl(288.095, 59%, 58%)", rgb: "rgb(186, 85, 211)"},
+    {authored: "mediumpurple", name: "mediumpurple", hex: "#9370DB", hsl: "hsl(259.626, 60%, 65%)", rgb: "rgb(147, 112, 219)"},
+    {authored: "mediumseagreen", name: "mediumseagreen", hex: "#3CB371", hsl: "hsl(146.723, 50%, 47%)", rgb: "rgb(60, 179, 113)"},
+    {authored: "mediumslateblue", name: "mediumslateblue", hex: "#7B68EE", hsl: "hsl(248.507, 80%, 67%)", rgb: "rgb(123, 104, 238)"},
+    {authored: "mediumspringgreen", name: "mediumspringgreen", hex: "#00FA9A", hsl: "hsl(156.96, 100%, 49%)", rgb: "rgb(0, 250, 154)"},
+    {authored: "mediumturquoise", name: "mediumturquoise", hex: "#48D1CC", hsl: "hsl(177.81, 60%, 55%)", rgb: "rgb(72, 209, 204)"},
+    {authored: "mediumvioletred", name: "mediumvioletred", hex: "#C71585", hsl: "hsl(322.247, 81%, 43%)", rgb: "rgb(199, 21, 133)"},
+    {authored: "midnightblue", name: "midnightblue", hex: "#191970", hsl: "hsl(240, 64%, 27%)", rgb: "rgb(25, 25, 112)"},
+    {authored: "mintcream", name: "mintcream", hex: "#F5FFFA", hsl: "hsl(150, 100%, 98%)", rgb: "rgb(245, 255, 250)"},
+    {authored: "mistyrose", name: "mistyrose", hex: "#FFE4E1", hsl: "hsl(6, 100%, 94%)", rgb: "rgb(255, 228, 225)"},
+    {authored: "moccasin", name: "moccasin", hex: "#FFE4B5", hsl: "hsl(38.108, 100%, 85%)", rgb: "rgb(255, 228, 181)"},
+    {authored: "navajowhite", name: "navajowhite", hex: "#FFDEAD", hsl: "hsl(35.854, 100%, 84%)", rgb: "rgb(255, 222, 173)"},
+    {authored: "navy", name: "navy", hex: "#000080", hsl: "hsl(240, 100%, 25%)", rgb: "rgb(0, 0, 128)"},
+    {authored: "oldlace", name: "oldlace", hex: "#FDF5E6", hsl: "hsl(39.13, 85%, 95%)", rgb: "rgb(253, 245, 230)"},
+    {authored: "olive", name: "olive", hex: "#808000", hsl: "hsl(60, 100%, 25%)", rgb: "rgb(128, 128, 0)"},
+    {authored: "olivedrab", name: "olivedrab", hex: "#6B8E23", hsl: "hsl(79.626, 60%, 35%)", rgb: "rgb(107, 142, 35)"},
+    {authored: "orange", name: "orange", hex: "#FFA500", hsl: "hsl(38.824, 100%, 50%)", rgb: "rgb(255, 165, 0)"},
+    {authored: "orangered", name: "orangered", hex: "#FF4500", hsl: "hsl(16.235, 100%, 50%)", rgb: "rgb(255, 69, 0)"},
+    {authored: "orchid", name: "orchid", hex: "#DA70D6", hsl: "hsl(302.264, 59%, 65%)", rgb: "rgb(218, 112, 214)"},
+    {authored: "palegoldenrod", name: "palegoldenrod", hex: "#EEE8AA", hsl: "hsl(54.706, 67%, 80%)", rgb: "rgb(238, 232, 170)"},
+    {authored: "palegreen", name: "palegreen", hex: "#98FB98", hsl: "hsl(120, 93%, 79%)", rgb: "rgb(152, 251, 152)"},
+    {authored: "paleturquoise", name: "paleturquoise", hex: "#AFEEEE", hsl: "hsl(180, 65%, 81%)", rgb: "rgb(175, 238, 238)"},
+    {authored: "palevioletred", name: "palevioletred", hex: "#DB7093", hsl: "hsl(340.374, 60%, 65%)", rgb: "rgb(219, 112, 147)"},
+    {authored: "papayawhip", name: "papayawhip", hex: "#FFEFD5", hsl: "hsl(37.143, 100%, 92%)", rgb: "rgb(255, 239, 213)"},
+    {authored: "peachpuff", name: "peachpuff", hex: "#FFDAB9", hsl: "hsl(28.286, 100%, 86%)", rgb: "rgb(255, 218, 185)"},
+    {authored: "peru", name: "peru", hex: "#CD853F", hsl: "hsl(29.577, 59%, 53%)", rgb: "rgb(205, 133, 63)"},
+    {authored: "pink", name: "pink", hex: "#FFC0CB", hsl: "hsl(349.524, 100%, 88%)", rgb: "rgb(255, 192, 203)"},
+    {authored: "plum", name: "plum", hex: "#DDA0DD", hsl: "hsl(300, 47%, 75%)", rgb: "rgb(221, 160, 221)"},
+    {authored: "powderblue", name: "powderblue", hex: "#B0E0E6", hsl: "hsl(186.667, 52%, 80%)", rgb: "rgb(176, 224, 230)"},
+    {authored: "purple", name: "purple", hex: "#800080", hsl: "hsl(300, 100%, 25%)", rgb: "rgb(128, 0, 128)"},
+    {authored: "red", name: "red", hex: "#F00", hsl: "hsl(0, 100%, 50%)", rgb: "rgb(255, 0, 0)"},
+    {authored: "rosybrown", name: "rosybrown", hex: "#BC8F8F", hsl: "hsl(0, 25%, 65%)", rgb: "rgb(188, 143, 143)"},
+    {authored: "royalblue", name: "royalblue", hex: "#4169E1", hsl: "hsl(225, 73%, 57%)", rgb: "rgb(65, 105, 225)"},
+    {authored: "saddlebrown", name: "saddlebrown", hex: "#8B4513", hsl: "hsl(25, 76%, 31%)", rgb: "rgb(139, 69, 19)"},
+    {authored: "salmon", name: "salmon", hex: "#FA8072", hsl: "hsl(6.176, 93%, 71%)", rgb: "rgb(250, 128, 114)"},
+    {authored: "sandybrown", name: "sandybrown", hex: "#F4A460", hsl: "hsl(27.568, 87%, 67%)", rgb: "rgb(244, 164, 96)"},
+    {authored: "seagreen", name: "seagreen", hex: "#2E8B57", hsl: "hsl(146.452, 50%, 36%)", rgb: "rgb(46, 139, 87)"},
+    {authored: "seashell", name: "seashell", hex: "#FFF5EE", hsl: "hsl(24.706, 100%, 97%)", rgb: "rgb(255, 245, 238)"},
+    {authored: "sienna", name: "sienna", hex: "#A0522D", hsl: "hsl(19.304, 56%, 40%)", rgb: "rgb(160, 82, 45)"},
+    {authored: "silver", name: "silver", hex: "#C0C0C0", hsl: "hsl(0, 0%, 75%)", rgb: "rgb(192, 192, 192)"},
+    {authored: "skyblue", name: "skyblue", hex: "#87CEEB", hsl: "hsl(197.4, 71%, 73%)", rgb: "rgb(135, 206, 235)"},
+    {authored: "slateblue", name: "slateblue", hex: "#6A5ACD", hsl: "hsl(248.348, 53%, 58%)", rgb: "rgb(106, 90, 205)"},
+    {authored: "slategray", name: "slategray", hex: "#708090", hsl: "hsl(210, 13%, 50%)", rgb: "rgb(112, 128, 144)"},
+    {authored: "slategrey", name: "slategray", hex: "#708090", hsl: "hsl(210, 13%, 50%)", rgb: "rgb(112, 128, 144)"},
+    {authored: "snow", name: "snow", hex: "#FFFAFA", hsl: "hsl(0, 100%, 99%)", rgb: "rgb(255, 250, 250)"},
+    {authored: "springgreen", name: "springgreen", hex: "#00FF7F", hsl: "hsl(149.882, 100%, 50%)", rgb: "rgb(0, 255, 127)"},
+    {authored: "steelblue", name: "steelblue", hex: "#4682B4", hsl: "hsl(207.273, 44%, 49%)", rgb: "rgb(70, 130, 180)"},
+    {authored: "tan", name: "tan", hex: "#D2B48C", hsl: "hsl(34.286, 44%, 69%)", rgb: "rgb(210, 180, 140)"},
+    {authored: "teal", name: "teal", hex: "#008080", hsl: "hsl(180, 100%, 25%)", rgb: "rgb(0, 128, 128)"},
+    {authored: "thistle", name: "thistle", hex: "#D8BFD8", hsl: "hsl(300, 24%, 80%)", rgb: "rgb(216, 191, 216)"},
+    {authored: "tomato", name: "tomato", hex: "#FF6347", hsl: "hsl(9.13, 100%, 64%)", rgb: "rgb(255, 99, 71)"},
+    {authored: "turquoise", name: "turquoise", hex: "#40E0D0", hsl: "hsl(174, 72%, 56%)", rgb: "rgb(64, 224, 208)"},
+    {authored: "violet", name: "violet", hex: "#EE82EE", hsl: "hsl(300, 76%, 72%)", rgb: "rgb(238, 130, 238)"},
+    {authored: "wheat", name: "wheat", hex: "#F5DEB3", hsl: "hsl(39.091, 77%, 83%)", rgb: "rgb(245, 222, 179)"},
+    {authored: "white", name: "white", hex: "#FFF", hsl: "hsl(0, 0%, 100%)", rgb: "rgb(255, 255, 255)"},
+    {authored: "whitesmoke", name: "whitesmoke", hex: "#F5F5F5", hsl: "hsl(0, 0%, 96%)", rgb: "rgb(245, 245, 245)"},
+    {authored: "yellow", name: "yellow", hex: "#FF0", hsl: "hsl(60, 100%, 50%)", rgb: "rgb(255, 255, 0)"},
+    {authored: "yellowgreen", name: "yellowgreen", hex: "#9ACD32", hsl: "hsl(79.742, 61%, 50%)", rgb: "rgb(154, 205, 50)"},
+    {authored: "transparent", name: "transparent", hex: "transparent", hsl: "transparent", rgb: "transparent"},
+    {authored: "rgba(0, 0, 0, 0)", name: "transparent", hex: "transparent", hsl: "transparent", rgb: "transparent"},
+    {authored: "hsla(0, 0%, 0%, 0)", name: "transparent", hex: "transparent", hsl: "transparent", rgb: "transparent"},
+    {authored: "rgba(50, 60, 70, 0.5)", name: "rgba(50, 60, 70, 0.5)", hex: "rgba(50, 60, 70, 0.5)", hsl: "hsla(210, 17%, 24%, 0.5)", rgb: "rgba(50, 60, 70, 0.5)"},
+    {authored: "rgba(0, 0, 0, 0.3)", name: "rgba(0, 0, 0, 0.3)", hex: "rgba(0, 0, 0, 0.3)", hsl: "hsla(0, 0%, 0%, 0.3)", rgb: "rgba(0, 0, 0, 0.3)"},
+    {authored: "rgba(255, 255, 255, 0.7)", name: "rgba(255, 255, 255, 0.7)", hex: "rgba(255, 255, 255, 0.7)", hsl: "hsla(0, 0%, 100%, 0.7)", rgb: "rgba(255, 255, 255, 0.7)"},
+    {authored: "rgba(127, 89, 45, 1)", name: "#7F592D", hex: "#7F592D", hsl: "hsl(32.195, 48%, 34%)", rgb: "rgb(127, 89, 45)"},
+    {authored: "hsla(19.304, 56%, 40%, 1)", name: "#9F512C", hex: "#9F512C", hsl: "hsl(19.304, 57%, 40%)", rgb: "rgb(159, 81, 44)"},
+    {authored: "invalidColor", name: "", hex: "", hsl: "", rgb: ""}
+  ];
+}
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -6,23 +6,24 @@
 
 const {Cc, Ci, Cu} = require("chrome");
 
 let ToolDefinitions = require("main").Tools;
 let {CssLogic} = require("devtools/styleinspector/css-logic");
 let {ELEMENT_STYLE} = require("devtools/server/actors/styles");
 let promise = require("sdk/core/promise");
 let {EventEmitter} = require("devtools/shared/event-emitter");
+let {colorUtils} = require("devtools/shared/css-color");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PluralForm.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 
-Cu.import("resource:///modules/devtools/gDevTools.jsm");
+let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 
 const FILTER_CHANGED_TIMEOUT = 300;
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 /**
  * Helper for long-running processes that should yield occasionally to
@@ -147,16 +148,20 @@ function CssHtmlTree(aStyleInspector, aP
   // Nodes used in templating
   this.root = this.styleDocument.getElementById("root");
   this.templateRoot = this.styleDocument.getElementById("templateRoot");
   this.propertyContainer = this.styleDocument.getElementById("propertyContainer");
 
   // No results text.
   this.noResults = this.styleDocument.getElementById("noResults");
 
+  // Refresh panel when color unit changed.
+  this._handlePrefChange = this._handlePrefChange.bind(this);
+  gDevTools.on("pref-changed", this._handlePrefChange);
+
   CssHtmlTree.processTemplate(this.templateRoot, this.root, this);
 
   // The element that we're inspecting, and the document that it comes from.
   this.viewedElement = null;
   this.createStyleViews();
 }
 
 /**
@@ -239,29 +244,35 @@ CssHtmlTree.prototype = {
     this.pageStyle = pageStyle;
   },
 
   get includeBrowserStyles()
   {
     return this.includeBrowserStylesCheckbox.checked;
   },
 
+  _handlePrefChange: function(event, data) {
+    if (data.pref == "devtools.defaultColorUnit" && this._computed) {
+      this.refreshPanel();
+    }
+  },
+
   /**
    * Update the highlighted element. The CssHtmlTree panel will show the style
    * information for the given element.
    * @param {nsIDOMElement} aElement The highlighted node to get styles for.
    *
    * @returns a promise that will be resolved when highlighting is complete.
    */
   highlight: function(aElement) {
     if (!aElement) {
       if (this._refreshProcess) {
         this._refreshProcess.cancel();
       }
-      return promise.resolve(undefined)
+      return promise.resolve(undefined);
     }
 
     if (aElement === this.viewedElement) {
       return promise.resolve(undefined);
     }
 
     this.viewedElement = aElement;
 
@@ -490,16 +501,17 @@ CssHtmlTree.prototype = {
   destroy: function CssHtmlTree_destroy()
   {
     delete this.viewedElement;
 
     // Remove event listeners
     this.includeBrowserStylesCheckbox.removeEventListener("command",
       this.includeBrowserStylesChanged);
     this.searchField.removeEventListener("command", this.filterChanged);
+    gDevTools.off("pref-changed", this._handlePrefChange);
 
     // Cancel tree construction
     if (this._createViewsProcess) {
       this._createViewsProcess.cancel();
     }
     if (this._refreshProcess) {
       this._refreshProcess.cancel();
     }
@@ -548,18 +560,23 @@ CssHtmlTree.prototype = {
   },
 };
 
 function PropertyInfo(aTree, aName) {
   this.tree = aTree;
   this.name = aName;
 }
 PropertyInfo.prototype = {
-  get value() this.tree._computed ? this.tree._computed[this.name].value : ""
-}
+  get value() {
+    if (this.tree._computed) {
+      let value = this.tree._computed[this.name].value;
+      return colorUtils.processCSSString(value);
+    }
+  }
+};
 
 /**
  * A container to give easy access to property data from the template engine.
  *
  * @constructor
  * @param {CssHtmlTree} aTree the CssHtmlTree instance we are working with.
  * @param {string} aName the CSS property name for which this PropertyView
  * instance will render the rules.
@@ -930,17 +947,18 @@ SelectorView.prototype = {
   get sourceText()
   {
     return this.selectorInfo.sourceText;
   },
 
 
   get value()
   {
-    return this.selectorInfo.value;
+    let val = this.selectorInfo.value;
+    return colorUtils.processCSSString(val);
   },
 
   maybeOpenStyleEditor: function(aEvent)
   {
     let keyEvent = Ci.nsIDOMKeyEvent;
     if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) {
       this.openStyleEditor();
     }
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -7,16 +7,18 @@
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 const promise = require("sdk/core/promise");
 
 let {CssLogic} = require("devtools/styleinspector/css-logic");
 let {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
 let {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles");
+let {colorUtils} = require("devtools/shared/css-color");
+let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 /**
@@ -905,17 +907,17 @@ Rule.prototype = {
  * @param {string} aPriority
  *        The property's priority (either "important" or an empty string).
  *
  */
 function TextProperty(aRule, aName, aValue, aPriority)
 {
   this.rule = aRule;
   this.name = aName;
-  this.value = aValue;
+  this.value = colorUtils.processCSSString(aValue);
   this.priority = aPriority;
   this.enabled = true;
   this.updateComputed();
 }
 
 TextProperty.prototype = {
   /**
    * Update the editor associated with this text property,
@@ -995,17 +997,17 @@ TextProperty.prototype = {
   {
     this.rule.setPropertyEnabled(this, aValue);
     this.updateEditor();
   },
 
   remove: function TextProperty_remove()
   {
     this.rule.removeProperty(this);
-  }
+  },
 };
 
 
 /**
  * View hierarchy mostly follows the model hierarchy.
  *
  * CssRuleView:
  *   Owns an ElementStyle and creates a list of RuleEditors for its
@@ -1043,16 +1045,19 @@ function CssRuleView(aDoc, aStore, aPage
   this.pageStyle = aPageStyle;
   this.element = this.doc.createElementNS(HTML_NS, "div");
   this.element.className = "ruleview devtools-monospace";
   this.element.flex = 1;
 
   this._boundCopy = this._onCopy.bind(this);
   this.element.addEventListener("copy", this._boundCopy);
 
+  this._handlePrefChange = this._handlePrefChange.bind(this);
+  gDevTools.on("pref-changed", this._handlePrefChange);
+
   let options = {
     fixedWidth: true,
     autoSelect: true,
     theme: "auto"
   };
   this.popup = new AutocompletePopup(aDoc.defaultView.parent.document, options);
 
   this._showEmpty();
@@ -1070,20 +1075,30 @@ CssRuleView.prototype = {
 
   /**
    * Return {bool} true if the rule view currently has an input editor visible.
    */
   get isEditing() {
     return this.element.querySelectorAll(".styleinspector-propertyeditor").length > 0;
   },
 
+  _handlePrefChange: function(event, data) {
+    if (data.pref == "devtools.defaultColorUnit") {
+      let element = this._viewedElement;
+      this._viewedElement = null;
+      this.highlight(element);
+    }
+  },
+
   destroy: function CssRuleView_destroy()
   {
     this.clear();
 
+    gDevTools.off("pref-changed", this._handlePrefChange);
+
     this.element.removeEventListener("copy", this._boundCopy);
     delete this._boundCopy;
 
     if (this.element.parentNode) {
       this.element.parentNode.removeChild(this.element);
     }
 
     if (this.elementStyle) {
--- a/browser/devtools/styleinspector/test/browser_computedview_copy.js
+++ b/browser/devtools/styleinspector/test/browser_computedview_copy.js
@@ -66,17 +66,17 @@ function checkCopySelection()
   let range = document.createRange();
   range.setStart(props[0], 0);
   range.setEnd(props[3], 3);
   win.getSelection().addRange(range);
 
   info("Checking that cssHtmlTree.siBoundCopy() " +
        " returns the correct clipboard value");
 
-  let expectedPattern = "color: rgb\\(255, 255, 0\\);[\\r\\n]+" +
+  let expectedPattern = "color: #FF0;[\\r\\n]+" +
                  "font-family: helvetica,sans-serif;[\\r\\n]+" +
                  "font-size: 16px;[\\r\\n]+" +
                  "font-variant: small-caps;[\\r\\n]*";
 
   SimpleTest.waitForClipboard(function CS_boundCopyCheck() {
       return checkClipboardData(expectedPattern);
     },
     function() {fireCopyEvent(props[0])}, closeStyleInspector, function() {
--- a/browser/devtools/styleinspector/test/browser_ruleview_bug_902966_revert_value_on_ESC.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_bug_902966_revert_value_on_ESC.js
@@ -4,17 +4,17 @@
 
 // Test original value is correctly displayed when ESCaping out of the
 // inplace editor in the style inspector.
 
 let doc;
 let ruleWindow;
 let ruleView;
 let inspector;
-let originalValue = "blue";
+let originalValue = "#00F";
 
 // Test data format
 // {
 //  value: what char sequence to type,
 //  commitKey: what key to type to "commit" the change,
 //  modifiers: commitKey modifiers,
 //  expected: what value is expected as a result
 // }
@@ -49,17 +49,17 @@ function runTestData(index)
   if (index === testData.length) {
     finishTest();
     return;
   }
 
   let idRuleEditor = ruleView.element.children[1]._ruleEditor;
   let propEditor = idRuleEditor.rule.textProps[0].editor;
   waitForEditorFocus(propEditor.element, function(aEditor) {
-    is(inplaceEditor(propEditor.valueSpan), aEditor, "Focused editor should be the value.");
+    is(inplaceEditor(propEditor.valueSpan), aEditor, "Focused editor should be the value span.");
 
     for (let ch of testData[index].value) {
       EventUtils.sendChar(ch, ruleWindow);
     }
     EventUtils.synthesizeKey(testData[index].commitKey, testData[index].modifiers);
 
     is(propEditor.valueSpan.innerHTML, testData[index].expected);
 
--- a/browser/devtools/styleinspector/test/browser_ruleview_copy.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_copy.js
@@ -66,20 +66,20 @@ function checkCopySelection()
   let selection = win.getSelection();
   selection.addRange(range);
 
   info("Checking that _boundCopy() returns the correct " +
     "clipboard value");
   let expectedPattern = "    margin: 10em;[\\r\\n]+" +
                         "    font-size: 14pt;[\\r\\n]+" +
                         "    font-family: helvetica,sans-serif;[\\r\\n]+" +
-                        "    color: rgb\\(170, 170, 170\\);[\\r\\n]+" +
+                        "    color: #AAA;[\\r\\n]+" +
                         "}[\\r\\n]+" +
                         "html {[\\r\\n]+" +
-                        "    color: rgb\\(0, 0, 0\\);[\\r\\n]*";
+                        "    color: #000;[\\r\\n]*";
 
   SimpleTest.waitForClipboard(function IUI_boundCopyCheck() {
     return checkClipboardData(expectedPattern);
   },function() {fireCopyEvent(props[0])}, finishup, function() {
     failedClipboard(expectedPattern, finishup);
   });
 }
 
--- a/browser/devtools/styleinspector/test/browser_ruleview_manipulation.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_manipulation.js
@@ -1,73 +1,71 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let doc;
 
-
-
 function simpleOverride(aInspector, aRuleView)
 {
   doc.body.innerHTML = '<div id="testid">Styled Node</div>';
   let element = doc.getElementById("testid");
 
   aInspector.selection.setNode(element);
   aInspector.once("inspector-updated", () => {
     let elementStyle = aRuleView._elementStyle;
 
     let elementRule = elementStyle.rules[0];
     let firstProp = elementRule.createProperty("background-color", "green", "");
     let secondProp = elementRule.createProperty("background-color", "blue", "");
     is(elementRule.textProps[0], firstProp, "Rules should be in addition order.");
     is(elementRule.textProps[1], secondProp, "Rules should be in addition order.");
 
     promiseDone(elementRule._applyingModifications.then(() => {
-      is(element.style.getPropertyValue("background-color"), "blue", "Second property should have been used.");
+      is(element.style.getPropertyValue("background-color"), "rgb(0, 0, 255)", "Second property should have been used.");
 
       secondProp.remove();
       return elementRule._applyingModifications;
     }).then(() => {
-      is(element.style.getPropertyValue("background-color"), "green", "After deleting second property, first should be used.");
+      is(element.style.getPropertyValue("background-color"), "rgb(0, 128, 0)", "After deleting second property, first should be used.");
 
       secondProp = elementRule.createProperty("background-color", "blue", "");
       return elementRule._applyingModifications;
     }).then(() => {
-      is(element.style.getPropertyValue("background-color"), "blue", "New property should be used.");
+      is(element.style.getPropertyValue("background-color"), "rgb(0, 0, 255)", "New property should be used.");
 
       is(elementRule.textProps[0], firstProp, "Rules shouldn't have switched places.");
       is(elementRule.textProps[1], secondProp, "Rules shouldn't have switched places.");
 
       secondProp.setEnabled(false);
       return elementRule._applyingModifications;
     }).then(() => {
-      is(element.style.getPropertyValue("background-color"), "green", "After disabling second property, first value should be used");
+      is(element.style.getPropertyValue("background-color"), "rgb(0, 128, 0)", "After disabling second property, first value should be used");
 
       firstProp.setEnabled(false);
       return elementRule._applyingModifications;
     }).then(() => {
       is(element.style.getPropertyValue("background-color"), "", "After disabling both properties, value should be empty.");
 
       secondProp.setEnabled(true);
       return elementRule._applyingModifications;
     }).then(() => {
-      is(element.style.getPropertyValue("background-color"), "blue", "Value should be set correctly after re-enabling");
+      is(element.style.getPropertyValue("background-color"), "rgb(0, 0, 255)", "Value should be set correctly after re-enabling");
 
       firstProp.setEnabled(true);
       return elementRule._applyingModifications;
     }).then(() => {
-      is(element.style.getPropertyValue("background-color"), "blue", "Re-enabling an earlier property shouldn't make it override a later property.");
+      is(element.style.getPropertyValue("background-color"), "rgb(0, 0, 255)", "Re-enabling an earlier property shouldn't make it override a later property.");
       is(elementRule.textProps[0], firstProp, "Rules shouldn't have switched places.");
       is(elementRule.textProps[1], secondProp, "Rules shouldn't have switched places.");
 
       firstProp.setValue("purple", "");
       return elementRule._applyingModifications;
     }).then(() => {
-      is(element.style.getPropertyValue("background-color"), "blue", "Modifying an earlier property shouldn't override a later property.");
+      is(element.style.getPropertyValue("background-color"), "rgb(0, 0, 255)", "Modifying an earlier property shouldn't override a later property.");
       finishTest();
     }));
   });
 }
 
 function finishTest()
 {
   doc = null;
--- a/browser/devtools/styleinspector/test/browser_ruleview_pseudoelement.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_pseudoelement.js
@@ -62,17 +62,17 @@ function testTopLeft()
     let elementAfterRule = afterRules[0];
     let elementAfterRuleView = [].filter.call(view.element.children, (e) => {
       return e._ruleEditor && e._ruleEditor.rule === elementAfterRule;
     })[0]._ruleEditor;
 
     is
     (
       convertTextPropsToString(elementAfterRule.textProps),
-      "background: none repeat scroll 0% 0% red; content: \" \"; position: absolute; " +
+      "background: none repeat scroll 0% 0% #F00; content: \" \"; position: absolute; " +
       "border-radius: 50%; height: 32px; width: 32px; top: 50%; left: 50%; margin-top: -16px; margin-left: -16px",
       "TopLeft after properties are correct"
     );
 
     let elementBeforeRule = beforeRules[0];
     let elementBeforeRuleView = [].filter.call(view.element.children, (e) => {
       return e._ruleEditor && e._ruleEditor.rule === elementBeforeRule;
     })[0]._ruleEditor;
@@ -237,41 +237,41 @@ function testParagraph()
     let elementFirstLineRule = firstLineRules[0];
     let elementFirstLineRuleView = [].filter.call(view.element.children, (e) => {
       return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule;
     })[0]._ruleEditor;
 
     is
     (
       convertTextPropsToString(elementFirstLineRule.textProps),
-      "background: none repeat scroll 0% 0% blue",
+      "background: none repeat scroll 0% 0% #00F",
       "Paragraph first-line properties are correct"
     );
 
     let elementFirstLetterRule = firstLetterRules[0];
     let elementFirstLetterRuleView = [].filter.call(view.element.children, (e) => {
       return e._ruleEditor && e._ruleEditor.rule === elementFirstLetterRule;
     })[0]._ruleEditor;
 
     is
     (
       convertTextPropsToString(elementFirstLetterRule.textProps),
-      "color: red; font-size: 130%",
+      "color: #F00; font-size: 130%",
       "Paragraph first-letter properties are correct"
     );
 
     let elementSelectionRule = selectionRules[0];
     let elementSelectionRuleView = [].filter.call(view.element.children, (e) => {
       return e._ruleEditor && e._ruleEditor.rule === elementSelectionRule;
     })[0]._ruleEditor;
 
     is
     (
       convertTextPropsToString(elementSelectionRule.textProps),
-      "color: white; background: none repeat scroll 0% 0% black",
+      "color: #FFF; background: none repeat scroll 0% 0% #000",
       "Paragraph first-letter properties are correct"
     );
 
     testBody();
   });
 }
 
 function testBody() {
--- a/browser/locales/Makefile.in
+++ b/browser/locales/Makefile.in
@@ -63,17 +63,21 @@ UNINSTALLER_PACKAGE_HOOK = $(RM) -r $(ST
 
 STUB_HOOK = $(NSINSTALL) -D "$(_ABS_DIST)/$(PKG_INST_PATH)"; \
     $(RM) "$(_ABS_DIST)/$(PKG_INST_PATH)$(PKG_STUB_BASENAME).exe"; \
     cp ../installer/windows/l10ngen/stub.exe "$(_ABS_DIST)/$(PKG_INST_PATH)$(PKG_STUB_BASENAME).exe"; \
     chmod 0755 "$(_ABS_DIST)/$(PKG_INST_PATH)$(PKG_STUB_BASENAME).exe"; \
     $(NULL)
 endif
 
+ifeq ($(MOZ_WIDGET_TOOLKIT) $(DIST_SUBDIR),windows metro)
+SEARCHPLUGINS_NAMES = $(shell cat $(call MERGE_FILE,/searchplugins/metrolist.txt))
+else
 SEARCHPLUGINS_NAMES = $(shell cat $(call MERGE_FILE,/searchplugins/list.txt))
+endif
 SEARCHPLUGINS_PATH := $(FINAL_TARGET)/searchplugins
 SEARCHPLUGINS := $(addsuffix .xml,$(SEARCHPLUGINS_NAMES))
 PP_TARGETS += SEARCHPLUGINS
 
 # Required for l10n.mk - defines a list of app sub dirs that should
 # be included in langpack xpis.
 ifdef MOZ_METRO
 # metro build, include both app folders
--- a/browser/locales/en-US/chrome/browser/devtools/styleeditor.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/styleeditor.properties
@@ -60,24 +60,24 @@ open.commandkey=VK_F7
 # editor.
 open.accesskey=y
 
 # LOCALIZATION NOTE  (saveStyleSheet.commandkey): This the key to use in
 # conjunction with accel (Command on Mac or Ctrl on other platforms) to Save
 saveStyleSheet.commandkey=S
 
 # LOCALIZATION NOTE (ToolboxStyleEditor.label):
-# This string is displayed in the title of the tab when the debugger is
+# This string is displayed in the title of the tab when the style editor is
 # displayed inside the developer tools window and in the Developer Tools Menu.
 ToolboxStyleEditor.label=Style Editor
 
-# LOCALIZATION NOTE (ToolboxStyleEditor.tooltip):
-# This string is displayed in the tooltip of the tab when the debugger is
+# LOCALIZATION NOTE (ToolboxStyleEditor.tooltip2):
+# This string is displayed in the tooltip of the tab when the style editor is
 # displayed inside the developer tools window.
-ToolboxStyleEditor.tooltip=CSS Stylesheets Editor
+ToolboxStyleEditor.tooltip2=Stylesheet Editor (CSS)
 
 # LOCALIZATION NOTE (confirmNavigationAway): Shown in a notification box when
 # the user tries to navigate away from a web page.
 confirmNavigationAway.message=If you leave this page, unsaved changes in the Style Editor will be lost.
 confirmNavigationAway.buttonLeave=Leave Page
 confirmNavigationAway.buttonLeaveAccesskey=L
 confirmNavigationAway.buttonStay=Stay on Page
 confirmNavigationAway.buttonStayAccesskey=S
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
@@ -16,16 +16,47 @@
 <!ENTITY toolboxZoomIn.key2            "="> <!-- + is above this key on many keyboards -->
 <!ENTITY toolboxZoomOut.key            "-">
 <!ENTITY toolboxZoomReset.key          "0">
 
 <!-- LOCALIZATION NOTE (options.context.advancedSettings): This is the label for
   -  the heading of the advanced settings group in the options panel. -->
 <!ENTITY options.context.advancedSettings "Advanced settings">
 
+<!-- LOCALIZATION NOTE (options.context.inspector): This is the label for
+  -  the heading of the Inspector group in the options panel. -->
+<!ENTITY options.context.inspector "Inspector">
+
+<!-- LOCALIZATION NOTE (options.defaultColorUnit.label): This is the label for a
+  -  dropdown list that controls the default color unit used in the inspector.
+  -  This label is visible in the options panel. -->
+<!ENTITY options.defaultColorUnit.label "Default color unit">
+
+<!-- LOCALIZATION NOTE (options.defaultColorUnit.accesskey): This is the access
+  -  key for a dropdown list that controls the default color unit used in the
+  -  inspector. This is visible in the options panel. -->
+<!ENTITY options.defaultColorUnit.accesskey "U">
+
+<!-- LOCALIZATION NOTE (options.defaultColorUnit.hex): This is used in the
+  -  'Default color unit' dropdown list and is visible in the options panel. -->
+<!ENTITY options.defaultColorUnit.hex "Hex">
+
+<!-- LOCALIZATION NOTE (options.defaultColorUnit.hsl): This is used in the
+  -  'Default color unit' dropdown list and is visible in the options panel. -->
+<!ENTITY options.defaultColorUnit.hsl "HSL(A)">
+
+<!-- LOCALIZATION NOTE (options.defaultColorUnit.rgb): This is used in the
+  -  'Default color unit' dropdown list and is visible in the options panel. -->
+<!ENTITY options.defaultColorUnit.rgb "RGB(A)">
+
+<!-- LOCALIZATION NOTE (options.defaultColorUnit.name): This is used in
+  -  the 'Default color unit' dropdown list and is visible in the options panel.
+  -  -->
+<!ENTITY options.defaultColorUnit.name "Color Names">
+
 <!-- LOCALIZATION NOTE (options.context.requiresRestart2): This is the requires
   -  restart label at right of settings that require a browser restart to be
   -  effective. -->
 <!ENTITY options.context.requiresRestart2  "Requires browser restart">
 
 <!-- LOCALIZATION NOTE (options.restartButton.label): This is the label for the
   -  restart button next to options.context.requiresRestart2 label. -->
 <!ENTITY options.restartButton.label  "Restart now">
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/searchplugins/googlemetrofx.xml
@@ -0,0 +1,18 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Google</ShortName>
+<Description>Google Search</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/x-icon;base64,AAABAAIAEBAAAAAAAAB9AQAAJgAAACAgAAAAAAAA8gIAAKMBAACJUE5HDQoaCgAAAA1JSERSAAAAEAAAABAIBgAAAB/z/2EAAAFESURBVDjLpZNJSwNBEIXnt4lE4kHxovgT9BDwJHqPy0HEEOJBiAuCRg+KUdC4QS4KrpC4gCBGE3NQ48JsnZ6eZ3UOM6gjaePhQU93v6+qq2q0pqgeJj2S8EdJT1hr0OxBtKCD5iEd8QxDYpvhvOBAuMDKURX9C9aPu4GA1GEVkzvMg10UBfYveWAWgYAP00V01fa+R9M2bA51wJvhIn3qR+ybt3D3JNQBE5sMjCIOLFpoHzOwdsLRO22qA6R6kiZiWwxUvy/PUQZIhYZ1vFM9cvcOOsYNdcBgysISdSJBnZjJMlR0Fw8vAp0xoz5gao/h+NZBy4i/10XGwrPA+hmvDyhVRG2Avu/LwcrkFADZa16L1h330w1RNgc3DiJzCpPYRm1bpveXX11clQR28xwblHpk1vq1iP/5mcoS0CoXDZiL0vsJ+dzfl+3T/VYAAAAASUVORK5CYIKJUE5HDQoaCgAAAA1JSERSAAAAIAAAACAIBgAAAHN6evQAAAK5SURBVFjDxVfrSxRRFJ9/Jta/oyWjF5XQm6D6EkHRgygIIgjUTcueVgqVWSRRkppEUQYWWB8ye1iGWilWlo/Ude489s7M6Zw7D9dlt53dmd29cFiWvXvO77x+51xpaaUsoSxBaUWZQ4ECy5xji2xKZDyCMlMEw6lCNiOSgwZKJK1SkcKeSealfP64t0mBjl4Ow39MkDUL0p2RSROOtqhZdeUEYM1pBl39XCg/fEeFtWcY7G9W4csvUxjlBkCsQ4Nt9QyWVfvT6RsAKXw3aoDGATZeYIt+W1kjw7cJG0RctWDTRebbKd8A6h5pwsDb70ba3w/eUr3wt/cmwgfw6Yft4TNMQaY7o1P2ncm4FT4ANQH/jQBJ2xv7kqIXEADDql8eS3+n8bku7oxNm+EDIM/dU92upb3T/NJGeaNbDx/AsbsLRUY5Xn92caWXY5d8RV6gWllxSg4fAEnTC90DQW13BLlgXR2D3dcUeDVkwOthA1bXspxILWcm3HdThcfvufB26LcJpkOEAz9NKI/lzqpSEC7feol5EWnpSeSlIxCALUkApmULdjUqxQVAQnl3D/X/yQda4QBEq2TYc12By091MQ17Bg3R88nHKlQbVmHvj89awNBLYrwT9zXY2aBAxTkGFdiSxP/Jp6FLDw+AS7GfsdJTJ2EqSO5khD43nGfBARy/ZxOQgZHe7GPM1jzUvChUtmnBAXQPcKGMJp3fdFGq6NByEhiAO4b/YptFfQJwNyQ/bZkVQGcf90Ja25ndIyrKBOa/f8wIpwi3X1G8UcxNu7ozUS7tiH0jBswwS3RIaF1w6LYKU/ML2+8sGnjygQswtKrVIy/Qd9qQP6LnO64q4fPAKpxyZIymHo1jWk6p1ag2BsdNwQMHcC+M5kHFJX+YlPxpVlbCx2mZ5DzPI04k4kUwHHdskU3pH76iftG8yWlkAAAAAElFTkSuQmCC</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://www.google.com/complete/search?client=metrofirefox&amp;q={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.google.com/search">
+  <Param name="q" value="{searchTerms}"/>
+  <Param name="ie" value="utf-8"/>
+  <Param name="oe" value="utf-8"/>
+  <Param name="client" value="metrofirefox"/>
+</Url>
+<SearchForm>https://www.google.com/</SearchForm>
+</SearchPlugin>
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/searchplugins/metrolist.txt
@@ -0,0 +1,4 @@
+bing
+googlemetrofx
+wikipedia
+yahoo
--- a/browser/metro/base/content/ContextUI.js
+++ b/browser/metro/base/content/ContextUI.js
@@ -179,16 +179,17 @@ var ContextUI = {
   displayTabs: function () {
     this._clearDelayedTimeout();
     this._setIsExpanded(true);
   },
 
   // Dismiss the navbar if visible.
   dismissNavbar: function dismissNavbar() {
     if (!BrowserUI.isStartTabVisible) {
+      Elements.autocomplete.closePopup();
       Elements.navbar.dismiss();
       ContentAreaObserver.updateContentArea();
     }
   },
 
   // Dismiss the tabstray if visible.
   dismissTabs: function dimissTabs() {
     this._clearDelayedTimeout();
--- a/browser/metro/base/content/Util.js
+++ b/browser/metro/base/content/Util.js
@@ -171,17 +171,17 @@ let Util = {
   },
 
   /*
    * DownloadUtils.convertByteUnits returns [size, localized-unit-string]
    * so they are joined for a single download size string.
    */
   getDownloadSize: function dv__getDownloadSize (aSize) {
     let [size, units] = DownloadUtils.convertByteUnits(aSize);
-    if (size > 0)
+    if (aSize > 0)
       return size + units;
     else
       return Strings.browser.GetStringFromName("downloadsUnknownSize");
   },
 
   /*
    * URIs and schemes
    */
--- a/browser/metro/base/content/input.js
+++ b/browser/metro/base/content/input.js
@@ -1190,16 +1190,17 @@ var InputSourceHelper = {
   isPrecise: false,
   touchIsActive: false,
 
   init: function ish_init() {
     window.addEventListener("mousemove", this, true);
     window.addEventListener("mousedown", this, true);
     window.addEventListener("touchstart", this, true);
     window.addEventListener("touchend", this, true);
+    window.addEventListener("touchcancel", this, true);
   },
 
   _precise: function () {
     if (!this.isPrecise) {
       this.isPrecise = true;
       this._fire("MozPrecisePointer");
     }
   },
@@ -1213,16 +1214,17 @@ var InputSourceHelper = {
 
   handleEvent: function ish_handleEvent(aEvent) {
     switch(aEvent.type) {
       case "touchstart":
         this._imprecise();
         this.touchIsActive = true;
         break;
       case "touchend":
+      case "touchcancel":
         this.touchIsActive = false;
         break;
       default:
         // Ignore mouse movement when touch is active. Prevents both mouse scrollbars
         // and touch scrollbars from displaying at the same time. Also works around
         // odd win8 bug involving an erant mousemove event after a touch sequence
         // starts (bug 896017).
         if (this.touchIsActive) {
--- a/browser/metro/components/HelperAppDialog.js
+++ b/browser/metro/components/HelperAppDialog.js
@@ -39,17 +39,18 @@ HelperAppLauncherDialog.prototype = {
     } else {
       let wasClicked = false;
       this._showDownloadInfobar(aLauncher);
     }
   },
 
   _getDownloadSize: function dv__getDownloadSize (aSize) {
     let displaySize = DownloadUtils.convertByteUnits(aSize);
-    if (!isNaN(displaySize[0]) && displaySize[0] > 0) // [0] is size, [1] is units
+    // displaySize[0] is formatted size, displaySize[1] is units
+    if (aSize > 0)
       return displaySize.join("");
     else {
       let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
       return browserBundle.GetStringFromName("downloadsUnknownSize");
     }
   },
 
   _getChromeWindow: function (aWindow) {
--- a/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.cpp
+++ b/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.cpp
@@ -708,17 +708,17 @@ IFACEMETHODIMP CExecuteCommandVerb::Exec
 
   // Launch on the desktop
   if (mIsDesktopRequest) {
     LaunchDesktopBrowser();
     return S_OK;
   }
 
   CComPtr<IApplicationActivationManager> activateMgr;
-  if (!PrepareActivationManager(activateMgr)) {
+  if (FAILED(PrepareActivationManager(activateMgr))) {
       LaunchDesktopBrowser();
       return S_OK;
   }
 
   HRESULT hr;
   WCHAR appModelID[256];
   if (!GetDefaultBrowserAppModelID(appModelID)) {
     Log(L"GetDefaultBrowserAppModelID failed, launching on desktop.");
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -745,16 +745,17 @@ nsDocShell::nsDocShell():
     mObserveErrorPages(true),
     mAllowAuth(true),
     mAllowKeywordFixup(false),
     mIsOffScreenBrowser(false),
     mIsActive(true),
     mIsAppTab(false),
     mUseGlobalHistory(false),
     mInPrivateBrowsing(false),
+    mDeviceSizeIsPageSize(false),
     mFiredUnloadEvent(false),
     mEODForCurrentDocument(false),
     mURIResultedInDocument(false),
     mIsBeingDestroyed(false),
     mIsExecutingOnLoadHandler(false),
     mIsPrintingOrPP(false),
     mSavingOldViewer(false),
 #ifdef DEBUG
@@ -3927,16 +3928,37 @@ nsDocShell::GetCurrentSHEntry(nsISHEntry
         NS_ADDREF(*aEntry = mLSHE);
     } else if (mOSHE) {
         NS_ADDREF(*aEntry = mOSHE);
         *aOSHE = true;
     }
     return NS_OK;
 }
 
+NS_IMETHODIMP
+nsDocShell::SetDeviceSizeIsPageSize(bool aValue)
+{
+    if (mDeviceSizeIsPageSize != aValue) {
+      mDeviceSizeIsPageSize = aValue;
+      nsRefPtr<nsPresContext> presContext;
+      GetPresContext(getter_AddRefs(presContext));
+      if (presContext) {
+          presContext->MediaFeatureValuesChanged(presContext->eAlwaysRebuildStyle);
+      }
+    }
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetDeviceSizeIsPageSize(bool* aValue)
+{
+    *aValue = mDeviceSizeIsPageSize;
+    return NS_OK;
+}
+
 void
 nsDocShell::ClearFrameHistory(nsISHEntry* aEntry)
 {
   nsCOMPtr<nsISHContainer> shcontainer = do_QueryInterface(aEntry);
   nsCOMPtr<nsISHistory> rootSH;
   GetRootSessionHistory(getter_AddRefs(rootSH));
   nsCOMPtr<nsISHistoryInternal> history = do_QueryInterface(rootSH);
   if (!history || !shcontainer) {
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -804,16 +804,17 @@ protected:
     bool                       mObserveErrorPages;
     bool                       mAllowAuth;
     bool                       mAllowKeywordFixup;
     bool                       mIsOffScreenBrowser;
     bool                       mIsActive;
     bool                       mIsAppTab;
     bool                       mUseGlobalHistory;
     bool                       mInPrivateBrowsing;
+    bool                       mDeviceSizeIsPageSize;
 
     // This boolean is set to true right before we fire pagehide and generally
     // unset when we embed a new content viewer.  While it's true no navigation
     // is allowed in this docshell.
     bool                       mFiredUnloadEvent;
 
     // this flag is for bug #21358. a docshell may load many urls
     // which don't result in new documents being created (i.e. a new
--- a/docshell/base/nsIDocShell.idl
+++ b/docshell/base/nsIDocShell.idl
@@ -911,9 +911,17 @@ interface nsIDocShell : nsIDocShellTreeI
 
   /**
     * Cherry picked parts of nsIController.
     * They are here, because we want to call these functions
     * from JS.
     */
   boolean isCommandEnabled(in string command);
   void doCommand(in string command);
+
+  /**
+   * If deviceSizeIsPageSize is set to true, device-width/height media queries
+   * will be calculated from the page size, not the device size.
+   *
+   * Used by the Responsive Design View.
+   */
+  [infallible] attribute boolean deviceSizeIsPageSize;
 };
--- a/dom/base/nsScreen.cpp
+++ b/dom/base/nsScreen.cpp
@@ -380,16 +380,29 @@ nsScreen::MozUnlockOrientation()
 
 NS_IMETHODIMP
 nsScreen::SlowMozUnlockOrientation()
 {
   MozUnlockOrientation();
   return NS_OK;
 }
 
+bool
+nsScreen::IsDeviceSizePageSize()
+{
+  nsPIDOMWindow* owner = GetOwner();
+  if (owner) {
+    nsIDocShell* docShell = owner->GetDocShell();
+    if (docShell) {
+      return docShell->GetDeviceSizeIsPageSize();
+    }
+  }
+  return false;
+}
+
 /* virtual */
 JSObject*
 nsScreen::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aScope)
 {
   return ScreenBinding::Wrap(aCx, aScope, this);
 }
 
 NS_IMPL_ISUPPORTS1(nsScreen::FullScreenEventListener, nsIDOMEventListener)
--- a/dom/base/nsScreen.h
+++ b/dom/base/nsScreen.h
@@ -46,23 +46,41 @@ public:
     nsRect rect;
     aRv = GetRect(rect);
     return rect.x;
   }
 
   int32_t GetWidth(ErrorResult& aRv)
   {
     nsRect rect;
+    if (IsDeviceSizePageSize()) {
+      nsCOMPtr<nsPIDOMWindow> owner = GetOwner();
+      if (owner) {
+        int32_t innerWidth = 0;
+        aRv = owner->GetInnerWidth(&innerWidth);
+        return innerWidth;
+      }
+    }
+
     aRv = GetRect(rect);
     return rect.width;
   }
 
   int32_t GetHeight(ErrorResult& aRv)
   {
     nsRect rect;
+    if (IsDeviceSizePageSize()) {
+      nsCOMPtr<nsPIDOMWindow> owner = GetOwner();
+      if (owner) {
+        int32_t innerHeight = 0;
+        aRv = owner->GetInnerHeight(&innerHeight);
+        return innerHeight;
+      }
+    }
+
     aRv = GetRect(rect);
     return rect.height;
   }
 
   int32_t GetPixelDepth(ErrorResult& aRv);
   int32_t GetColorDepth(ErrorResult& aRv)
   {
     return GetPixelDepth(aRv);
@@ -132,12 +150,14 @@ private:
   enum LockPermission {
     LOCK_DENIED,
     FULLSCREEN_LOCK_ALLOWED,
     LOCK_ALLOWED
   };
 
   LockPermission GetLockOrientationPermission() const;
 
+  bool IsDeviceSizePageSize();
+
   nsRefPtr<FullScreenEventListener> mEventListener;
 };
 
 #endif /* nsScreen_h___ */
--- a/embedding/components/find/src/nsFind.cpp
+++ b/embedding/components/find/src/nsFind.cpp
@@ -31,16 +31,23 @@
 using namespace mozilla;
 
 // Yikes!  Casting a char to unichar can fill with ones!
 #define CHAR_TO_UNICHAR(c) ((PRUnichar)(const unsigned char)c)
 
 static NS_DEFINE_CID(kCContentIteratorCID, NS_CONTENTITERATOR_CID);
 static NS_DEFINE_CID(kCPreContentIteratorCID, NS_PRECONTENTITERATOR_CID);
 
+#define CH_QUOTE ((PRUnichar) 0x22)
+#define CH_APOSTROPHE ((PRUnichar) 0x27)
+#define CH_LEFT_SINGLE_QUOTE ((PRUnichar) 0x2018)
+#define CH_RIGHT_SINGLE_QUOTE ((PRUnichar) 0x2019)
+#define CH_LEFT_DOUBLE_QUOTE ((PRUnichar) 0x201C)
+#define CH_RIGHT_DOUBLE_QUOTE ((PRUnichar) 0x201D)
+
 #define CH_SHY ((PRUnichar) 0xAD)
 
 // nsFind::Find casts CH_SHY to char before calling StripChars
 // This works correctly if and only if CH_SHY <= 255
 PR_STATIC_ASSERT(CH_SHY <= 255);
 
 // -----------------------------------------------------------------------
 // nsFindContentIterator is a special iterator that also goes through
@@ -1096,19 +1103,42 @@ nsFind::Find(const PRUnichar *aPatText, 
     }
     if (!inWhitespace && IsSpace(patc))
       inWhitespace = true;
 
     // convert to lower case if necessary
     else if (!inWhitespace && !mCaseSensitive && IsUpperCase(c))
       c = ToLowerCase(c);
 
-    // ignore soft hyphens in the document
-    if (c == CH_SHY)
-      continue;
+    switch (c) {
+      // ignore soft hyphens in the document
+      case CH_SHY:
+        continue;
+      // treat curly and straight quotes as identical
+      case CH_LEFT_SINGLE_QUOTE:
+      case CH_RIGHT_SINGLE_QUOTE:
+        c = CH_APOSTROPHE;
+        break;
+      case CH_LEFT_DOUBLE_QUOTE:
+      case CH_RIGHT_DOUBLE_QUOTE:
+        c = CH_QUOTE;
+        break;
+    }
+
+    switch (patc) {
+      // treat curly and straight quotes as identical
+      case CH_LEFT_SINGLE_QUOTE:
+      case CH_RIGHT_SINGLE_QUOTE:
+        patc = CH_APOSTROPHE;
+        break;
+      case CH_LEFT_DOUBLE_QUOTE:
+      case CH_RIGHT_DOUBLE_QUOTE:
+        patc = CH_QUOTE;
+        break;
+    }
 
     // a '\n' between CJ characters is ignored
     if (pindex != (mFindBackward ? patLen : 0) && c != patc && !inWhitespace) {
       if (c == '\n' && t2b && IS_CJ_CHAR(prevChar)) {
         int32_t nindex = findex + incr;
         if (mFindBackward ? (nindex >= 0) : (nindex < fragLen)) {
           if (IS_CJ_CHAR(t2b[nindex]))
             continue;
--- a/embedding/test/test_nsFind.html
+++ b/embedding/test/test_nsFind.html
@@ -1,21 +1,23 @@
 <!DOCTYPE HTML>
 <html>
 <!--
 https://bugzilla.mozilla.org/show_bug.cgi?id=450048
 -->
 <head>
+  <meta charset="UTF-8">
   <title>Test for nsFind::Find()</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=450048">Mozilla Bug 450048</a>
 <p id="display">This is the text to search i<b>n&shy;t</b>o</p>
+<p id="quotes">"straight" and &ldquo;curly&rdquo; and &lsquo;didn't&rsquo; and 'doesn&rsquo;t'</p>
 <div id="content" style="display: none">
   
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 450048 **/
 
@@ -130,12 +132,94 @@ https://bugzilla.mozilla.org/show_bug.cg
   retRange = rf.Find(searchValue, searchRange, startPt, endPt);
   ok(!retRange, "\"" + searchValue + "\" found (forward)");
 
   rf.findBackwards = true;
 
   // searchValue = "the";
   retRange = rf.Find(searchValue, searchRange, startPt, endPt);
   ok(retRange, "\"" + searchValue + "\" not found (backward)");
+
+
+  // Curly quotes and straight quotes should match.
+
+  rf.caseSensitive = false;
+  rf.findBackwards = false;
+
+  function find(node, searchValue) {
+    var range = document.createRange();
+    range.setStart(node, 0);
+    range.setEnd(node, node.childNodes.length);
+    return rf.Find(searchValue, range, range, range);
+  }
+
+  function assertFound(node, searchValue) {
+    ok(find(node, searchValue), "\"" + searchValue + "\" not found");
+  }
+
+  function assertNotFound(node, searchValue) {
+    ok(!find(node, searchValue), "\"" + searchValue + "\" found");
+  }
+
+  var quotes = document.getElementById("quotes");
+
+  assertFound(quotes, "\"straight\"");
+  assertFound(quotes, "\u201Cstraight\u201D");
+
+  assertNotFound(quotes, "'straight'");
+  assertNotFound(quotes, "\u2018straight\u2019");
+  assertNotFound(quotes, "\u2019straight\u2018");
+  assertNotFound(quotes, ".straight.");
+
+  assertFound(quotes, "\"curly\"");
+  assertFound(quotes, "\u201Ccurly\u201D");
+
+  assertNotFound(quotes, "'curly'");
+  assertNotFound(quotes, "\u2018curly\u2019");
+  assertNotFound(quotes, ".curly.");
+
+  assertFound(quotes, "didn't");
+  assertFound(quotes, "didn\u2018t");
+  assertFound(quotes, "didn\u2019t");
+
+  assertNotFound(quotes, "didnt");
+  assertNotFound(quotes, "didn t");
+  assertNotFound(quotes, "didn.t");
+
+  assertFound(quotes, "'didn't'");
+  assertFound(quotes, "'didn\u2018t'");
+  assertFound(quotes, "'didn\u2019t'");
+  assertFound(quotes, "\u2018didn't\u2019");
+  assertFound(quotes, "\u2019didn't\u2018");
+  assertFound(quotes, "\u2018didn't\u2018");
+  assertFound(quotes, "\u2019didn't\u2019");
+  assertFound(quotes, "\u2018didn\u2019t\u2019");
+  assertFound(quotes, "\u2019didn\u2018t\u2019");
+  assertFound(quotes, "\u2018didn\u2019t\u2018");
+
+  assertNotFound(quotes, "\"didn't\"");
+  assertNotFound(quotes, "\u201Cdidn't\u201D");
+
+  assertFound(quotes, "doesn't");
+  assertFound(quotes, "doesn\u2018t");
+  assertFound(quotes, "doesn\u2019t");
+
+  assertNotFound(quotes, "doesnt");
+  assertNotFound(quotes, "doesn t");
+  assertNotFound(quotes, "doesn.t");
+
+  assertFound(quotes, "'doesn't'");
+  assertFound(quotes, "'doesn\u2018t'");
+  assertFound(quotes, "'doesn\u2019t'");
+  assertFound(quotes, "\u2018doesn't\u2019");
+  assertFound(quotes, "\u2019doesn't\u2018");
+  assertFound(quotes, "\u2018doesn't\u2018");
+  assertFound(quotes, "\u2019doesn't\u2019");
+  assertFound(quotes, "\u2018doesn\u2019t\u2019");
+  assertFound(quotes, "\u2019doesn\u2018t\u2019");
+  assertFound(quotes, "\u2018doesn\u2019t\u2018");
+
+  assertNotFound(quotes, "\"doesn't\"");
+  assertNotFound(quotes, "\u201Cdoesn't\u201D");
 </script>
 </pre>
 </body>
 </html>
--- a/layout/base/nsPresContext.cpp
+++ b/layout/base/nsPresContext.cpp
@@ -2667,16 +2667,27 @@ bool nsPresContext::GetPaintFlashing() c
       pref = Preferences::GetBool("nglayout.debug.paint_flashing_chrome");
     }
     mPaintFlashing = pref;
     mPaintFlashingInitialized = true;
   }
   return mPaintFlashing;
 }
 
+bool
+nsPresContext::IsDeviceSizePageSize()
+{
+  bool isDeviceSizePageSize = false;
+  nsCOMPtr<nsIDocShell> docShell(do_QueryReferent(mContainer));
+  if (docShell) {
+    isDeviceSizePageSize = docShell->GetDeviceSizeIsPageSize();
+  }
+  return isDeviceSizePageSize;
+}
+
 nsRootPresContext::nsRootPresContext(nsIDocument* aDocument,
                                      nsPresContextType aType)
   : nsPresContext(aDocument, aType),
     mDOMGeneration(0)
 {
 }
 
 nsRootPresContext::~nsRootPresContext()
--- a/layout/base/nsPresContext.h
+++ b/layout/base/nsPresContext.h
@@ -998,16 +998,18 @@ public:
   bool ExistThrottledUpdates() const {
     return mExistThrottledUpdates;
   }
 
   void SetExistThrottledUpdates(bool aExistThrottledUpdates) {
     mExistThrottledUpdates = aExistThrottledUpdates;
   }
 
+  bool IsDeviceSizePageSize();
+
 protected:
   friend class nsRunnableMethod<nsPresContext>;
   NS_HIDDEN_(void) ThemeChangedInternal();
   NS_HIDDEN_(void) SysColorChangedInternal();
   NS_HIDDEN_(void) UIResolutionChangedInternal();
 
   static NS_HIDDEN_(bool)
   UIResolutionChangedSubdocumentCallback(nsIDocument* aDocument, void* aData);
--- a/layout/style/nsMediaFeatures.cpp
+++ b/layout/style/nsMediaFeatures.cpp
@@ -106,24 +106,29 @@ GetDeviceContextFor(nsPresContext* aPres
   return aPresContext->DeviceContext();
 }
 
 // A helper for three features below.
 static nsSize
 GetDeviceSize(nsPresContext* aPresContext)
 {
     nsSize size;
-    if (aPresContext->IsRootPaginatedDocument())
+
+    if (aPresContext->IsDeviceSizePageSize()) {
+        size = GetSize(aPresContext);
+    } else if (aPresContext->IsRootPaginatedDocument()) {
         // We want the page size, including unprintable areas and margins.
         // XXX The spec actually says we want the "page sheet size", but
         // how is that different?
         size = aPresContext->GetPageSize();
-    else
+    } else {
         GetDeviceContextFor(aPresContext)->
             GetDeviceSurfaceDimensions(size.width, size.height);
+    }
+
     return size;
 }
 
 static nsresult
 GetDeviceWidth(nsPresContext* aPresContext, const nsMediaFeature*,
                nsCSSValue& aResult)
 {
     nsSize size = GetDeviceSize(aPresContext);
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -38,16 +38,17 @@ import android.app.AlertDialog;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.nfc.NdefMessage;
 import android.nfc.NdefRecord;
 import android.nfc.NfcAdapter;
 import android.nfc.NfcEvent;
 import android.os.Build;
 import android.os.Bundle;
@@ -63,16 +64,18 @@ import android.view.MotionEvent;
 import android.view.SubMenu;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.view.animation.Interpolator;
 import android.widget.RelativeLayout;
 import android.widget.Toast;
 
+import java.io.File;
+import java.io.FileNotFoundException;
 import java.net.URLEncoder;
 import java.util.EnumSet;
 import java.util.Vector;
 
 abstract public class BrowserApp extends GeckoApp
                                  implements TabsPanel.TabsLayoutChangeListener,
                                             PropertyAnimator.PropertyAnimationListener,
                                             View.OnKeyListener,
@@ -177,26 +180,17 @@ abstract public class BrowserApp extends
         switch(msg) {
             case LOCATION_CHANGE:
                 if (Tabs.getInstance().isSelectedTab(tab)) {
                     maybeCancelFaviconLoad(tab);
                 }
                 // fall through
             case SELECTED:
                 if (Tabs.getInstance().isSelectedTab(tab)) {
-                    if (isAboutHome(tab)) {
-                        showHomePager(tab.getAboutHomePage());
-
-                        if (isDynamicToolbarEnabled()) {
-                            // Show the toolbar.
-                            mLayerView.getLayerMarginsAnimator().showMargins(false);
-                        }
-                    } else {
-                        hideHomePager();
-                    }
+                    updateHomePagerForTab(tab);
 
                     if (mSiteIdentityPopup != null)
                         mSiteIdentityPopup.dismiss();
 
                     final TabsPanel.Panel panel = tab.isPrivate()
                                                 ? TabsPanel.Panel.PRIVATE_TABS
                                                 : TabsPanel.Panel.NORMAL_TABS;
                     // Delay calling showTabs so that it does not modify the mTabsChangedListeners
@@ -1503,16 +1497,37 @@ abstract public class BrowserApp extends
             hideBrowserSearch();
         } else {
             showBrowserSearch();
             mHomePager.setVisibility(View.INVISIBLE);
             mBrowserSearch.filter(searchTerm, handler);
         }
     }
 
+    /**
+     * Shows or hides the home pager for the given tab.
+     */
+    private void updateHomePagerForTab(Tab tab) {
+        // Don't change the visibility of the home pager if we're in editing mode.
+        if (mBrowserToolbar.isEditing()) {
+            return;
+        }
+
+        if (isAboutHome(tab)) {
+            showHomePager(tab.getAboutHomePage());
+
+            if (isDynamicToolbarEnabled()) {
+                // Show the toolbar.
+                mLayerView.getLayerMarginsAnimator().showMargins(false);
+            }
+        } else {
+            hideHomePager();
+        }
+    }
+
     private void showHomePager(HomePager.Page page) {
         showHomePagerWithAnimator(page, null);
     }
 
     private void showHomePagerWithAnimator(HomePager.Page page, PropertyAnimator animator) {
         if (isHomePagerVisible()) {
             return;
         }
@@ -1894,23 +1909,55 @@ abstract public class BrowserApp extends
                            scheme.equals("file") || scheme.equals("resource")));
 
         // Action providers are available only ICS+.
         if (Build.VERSION.SDK_INT >= 14) {
             GeckoActionProvider provider = (GeckoActionProvider) share.getActionProvider();
             if (provider != null) {
                 Intent shareIntent = provider.getIntent();
 
+                // For efficiency, the provider's intent is only set once
                 if (shareIntent == null) {
-                    shareIntent = GeckoAppShell.getShareIntent(this, url,
-                                                               "text/plain", tab.getDisplayTitle());
+                    shareIntent = new Intent(Intent.ACTION_SEND);
+                    shareIntent.setType("text/plain");
                     provider.setIntent(shareIntent);
-                } else {
-                    shareIntent.putExtra(Intent.EXTRA_TEXT, url);
-                    shareIntent.putExtra(Intent.EXTRA_SUBJECT, tab.getDisplayTitle());
+                }
+
+                // Replace the existing intent's extras
+                shareIntent.putExtra(Intent.EXTRA_TEXT, url);
+                shareIntent.putExtra(Intent.EXTRA_SUBJECT, tab.getDisplayTitle());
+                shareIntent.putExtra(Intent.EXTRA_TITLE, tab.getDisplayTitle());
+
+                // Clear the existing thumbnail extras so we don't share an old thumbnail.
+                shareIntent.removeExtra("share_screenshot");
+                shareIntent.removeExtra("share_screenshot_uri");
+
+                // Include the thumbnail of the page being shared.
+                BitmapDrawable drawable = tab.getThumbnail();
+                if (drawable != null) {
+                    Bitmap thumbnail = drawable.getBitmap();
+                    shareIntent.putExtra("share_screenshot", thumbnail);
+
+                    // Kobo uses a custom intent extra for sharing thumbnails.
+                    if (Build.MANUFACTURER.equals("Kobo")) {
+                        File cacheDir = getExternalCacheDir();
+
+                        if (cacheDir != null) {
+                            File outFile = new File(cacheDir, "thumbnail.png");
+
+                            try {
+                                java.io.FileOutputStream out = new java.io.FileOutputStream(outFile);
+                                thumbnail.compress(Bitmap.CompressFormat.PNG, 90, out);
+                            } catch (FileNotFoundException e) {
+                                Log.e(LOGTAG, "File not found", e);
+                            }
+
+                            shareIntent.putExtra("share_screenshot_uri", Uri.parse(outFile.getPath()));
+                        }
+                    }
                 }
             }
         }
 
         // Disable save as PDF for about:home and xul pages
         saveAsPDF.setEnabled(!(tab.getURL().equals("about:home") ||
                                tab.getContentType().equals("application/vnd.mozilla.xul+xml")));
 
@@ -2120,17 +2167,28 @@ abstract public class BrowserApp extends
 
         String action = intent.getAction();
 
         if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 10 && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
             String uri = intent.getDataString();
             GeckoAppShell.sendEventToGecko(GeckoEvent.createURILoadEvent(uri));
         }
 
-        if (!Intent.ACTION_MAIN.equals(action) || !mInitialized) {
+        if (!mInitialized) {
+            return;
+        }
+
+        // Dismiss editing mode if the user is loading a URL from an external app.
+        if (Intent.ACTION_VIEW.equals(action)) {
+            dismissEditingMode();
+            return;
+        }
+
+        // Only solicit feedback when the app has been launched from the icon shortcut.
+        if (!Intent.ACTION_MAIN.equals(action)) {
             return;
         }
 
         (new UiAsyncTask<Void, Void, Boolean>(ThreadUtils.getBackgroundHandler()) {
             @Override
             public synchronized Boolean doInBackground(Void... params) {
                 // Check to see how many times the app has been launched.
                 SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
--- a/mobile/android/base/Tab.java
+++ b/mobile/android/base/Tab.java
@@ -179,17 +179,17 @@ public class Tab {
     public String getBaseDomain() {
         return mBaseDomain;
     }
 
     public Bitmap getFavicon() {
         return mFavicon;
     }
 
-    public Drawable getThumbnail() {
+    public BitmapDrawable getThumbnail() {
         return mThumbnail;
     }
 
     public Bitmap getThumbnailBitmap(int width, int height) {
         if (mThumbnailBitmap != null) {
             // Bug 787318 - Honeycomb has a bug with bitmap caching, we can't
             // reuse the bitmap there.
             boolean honeycomb = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
--- a/mobile/android/base/tests/BaseTest.java.in
+++ b/mobile/android/base/tests/BaseTest.java.in
@@ -45,17 +45,17 @@ abstract class BaseTest extends Activity
     public static final int TEST_TALOS = 1;
 
     private static final String TARGET_PACKAGE_ID = "org.mozilla.gecko";
     private static final String LAUNCH_ACTIVITY_FULL_CLASSNAME ="@ANDROID_PACKAGE_NAME@.App";
     private static final int VERIFY_URL_TIMEOUT = 2000;
     private static final int MAX_LIST_ATTEMPTS = 3;
     private static final int MAX_WAIT_ENABLED_TEXT_MS = 10000;
     private static final int MAX_WAIT_HOME_PAGER_HIDDEN_MS = 10000;
-    public static final int MAX_WAIT_MS = 3000;
+    public static final int MAX_WAIT_MS = 4500;
 
     // IDs for UI views
     private static final String BROWSER_TOOLBAR_ID = "browser_toolbar";
     protected static final String URL_EDIT_TEXT_ID = "url_edit_text";
     protected static final String URL_BAR_TITLE_ID = "url_bar_title";
 
     private static Class<Activity> mLauncherActivityClass;
     private Activity mActivity;
--- a/mobile/android/chrome/content/aboutDownloads.js
+++ b/mobile/android/chrome/content/aboutDownloads.js
@@ -247,21 +247,21 @@ let Downloads = {
       displayState: this._getStateString(updatedState),
       state: updatedState
     });
     list = aDownload.isPrivate ? this._privateList : this._normalList;
     list.insertAdjacentHTML("afterbegin", item);
   },
 
   _getDownloadSize: function dl_getDownloadSize(aSize) {
-    let displaySize = DownloadUtils.convertByteUnits(aSize);
-    if (displaySize[0] > 0) // [0] is size, [1] is units
-      return displaySize.join("");
-    else
-      return gStrings.GetStringFromName("downloadState.unknownSize");
+    if (aSize > 0) {
+      let displaySize = DownloadUtils.convertByteUnits(aSize);
+      return displaySize.join(""); // [0] is size, [1] is units
+    }
+    return gStrings.GetStringFromName("downloadState.unknownSize");
   },
 
   // Not all states are displayed as-is on mobile, some are translated to a generic state
   _getState: function dl_getState(aState) {
     let str;
     switch (aState) {
       // Downloading and Scanning states show up as "Downloading"
       case this._dlmgr.DOWNLOAD_DOWNLOADING:
--- a/toolkit/components/alerts/resources/content/alert.css
+++ b/toolkit/components/alerts/resources/content/alert.css
@@ -3,16 +3,20 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #alertBox[animate] {
   animation-duration: 4s;
   animation-fill-mode: both;
   animation-name: alert-animation;
 }
 
+#alertBox[animate]:hover {
+  animation-play-state: paused;
+}
+
 .alertText {
   white-space: pre-wrap;
 }
 
 @keyframes alert-animation {
   to {
     visibility: hidden;
   }
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -1896,24 +1896,18 @@ DownloadLegacySaver.prototype = {
    */
   cancel: function DLS_cancel()
   {
     // We may be using a DownloadCopySaver to handle resuming.
     if (this.copySaver) {
       return this.copySaver.cancel.apply(this.copySaver, arguments);
     }
 
-    // Synchronously cancel the operation as soon as the object is connected.
+    // Cancel the operation as soon as the object is connected.
     this.deferCanceled.resolve();
-
-    // We don't necessarily receive status notifications after we call "cancel",
-    // but cancellation through nsICancelable should be synchronous, thus force
-    // the rejection of the execution promise immediately.
-    this.deferExecuted.reject(new DownloadError(Cr.NS_ERROR_FAILURE,
-                                                "Download canceled."));
   },
 
   /**
    * Implements "DownloadSaver.removePartialData".
    */
   removePartialData: function ()
   {
     // DownloadCopySaver and DownloadLeagcySaver use the same logic for removing
--- a/toolkit/components/jsdownloads/src/DownloadImport.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadImport.jsm
@@ -165,17 +165,17 @@ this.DownloadImport.prototype = {
             // Paused downloads that should not be auto-resumed are considered
             // in a "canceled" state.
             if (!resumeDownload) {
               downloadOptions.canceled = true;
             }
 
             let download = yield Downloads.createDownload(downloadOptions);
 
-            this.list.add(download);
+            yield this.list.add(download);
 
             if (resumeDownload) {
               download.start();
             } else {
               yield download.refresh();
             }
 
           } catch (ex) {
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -211,18 +211,21 @@ this.DownloadIntegration = {
         OS.File.remove(OS.Path.join(OS.Constants.Path.profileDir,
                                     "downloads.rdf"));
 
       }
 
       // After the list of persistent downloads has been loaded, add the
       // DownloadAutoSaveView and the DownloadHistoryObserver (even if the load
       // operation failed).  These objects are kept alive by the underlying
-      // DownloadList and by the history service respectively.
-      new DownloadAutoSaveView(aList, this._store);
+      // DownloadList and by the history service respectively.  We wait for a
+      // complete initialization of the view used for detecting changes to
+      // downloads to be persisted, before other callers get a chance to modify
+      // the list without being detected.
+      yield new DownloadAutoSaveView(aList, this._store).initialize();
       new DownloadHistoryObserver(aList);
     }.bind(this));
   },
 
   /**
    * Determines if a Download object from the list of persistent downloads
    * should be saved into a file, so that it can be restored across sessions.
    *
@@ -787,17 +790,18 @@ this.DownloadObserver = {
       },
       onDownloadRemoved: aDownload => {
         downloadsSet.delete(aDownload);
         // The download must also be removed from the canceled when offline set.
         this._canceledOfflineDownloads.delete(aDownload);
       }
     };
 
-    aList.addView(downloadsView);
+    // We register the view asynchronously.
+    aList.addView(downloadsView).then(null, Cu.reportError);
   },
 
   /**
    * Wrapper that handles the test mode before calling the prompt that display
    * a warning message box that informs that there are active downloads,
    * and asks whether the user wants to cancel them or not.
    *
    * @param aCancel
@@ -858,18 +862,19 @@ this.DownloadObserver = {
         this._confirmCancelDownloads(aSubject, downloadsCount, p,
                                      p.ON_LEAVE_PRIVATE_BROWSING);
         break;
       case "last-pb-context-exited":
         let deferred = Task.spawn(function() {
           let list = yield Downloads.getList(Downloads.PRIVATE);
           let downloads = yield list.getAll();
 
+          // We can remove the downloads and finalize them in parallel.
           for (let download of downloads) {
-            list.remove(download);
+            list.remove(download).then(null, Cu.reportError);
             download.finalize(true).then(null, Cu.reportError);
           }
         });
         // Handle test mode
         if (DownloadIntegration.testMode) {
           deferred.then((value) => { DownloadIntegration._deferTestClearPrivateList.resolve("success"); },
                         (error) => { DownloadIntegration._deferTestClearPrivateList.reject(error); });
         }
@@ -963,46 +968,63 @@ DownloadHistoryObserver.prototype = {
   onDeleteVisits: function () {},
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadAutoSaveView
 
 /**
  * This view can be added to a DownloadList object to trigger a save operation
- * in the given DownloadStore object when a relevant change occurs.
+ * in the given DownloadStore object when a relevant change occurs.  You should
+ * call the "initialize" method in order to register the view and load the
+ * current state from disk.
  *
  * You do not need to keep a reference to this object in order to keep it alive,
  * because the DownloadList object already keeps a strong reference to it.
  *
  * @param aList
  *        The DownloadList object on which the view should be registered.
  * @param aStore
  *        The DownloadStore object used for saving.
  */
 function DownloadAutoSaveView(aList, aStore) {
+  this._list = aList;
   this._store = aStore;
   this._downloadsMap = new Map();
-
-  // We set _initialized to true after adding the view, so that onDownloadAdded
-  // doesn't cause a save to occur.
-  aList.addView(this);
-  this._initialized = true;
 }
 
 DownloadAutoSaveView.prototype = {
   /**
+   * DownloadList object linked to this view.
+   */
+  _list: null,
+
+  /**
+   * The DownloadStore object used for saving.
+   */
+  _store: null,
+
+  /**
    * True when the initial state of the downloads has been loaded.
    */
   _initialized: false,
 
   /**
-   * The DownloadStore object used for saving.
+   * Registers the view and loads the current state from disk.
+   *
+   * @return {Promise}
+   * @resolves When the view has been registered.
+   * @rejects JavaScript exception.
    */
-  _store: null,
+  initialize: function ()
+  {
+    // We set _initialized to true after adding the view, so that
+    // onDownloadAdded doesn't cause a save to occur.
+    return this._list.addView(this).then(() => this._initialized = true);
+  },
 
   /**
    * This map contains only Download objects that should be saved to disk, and
    * associates them with the result of their getSerializationHash function, for
    * the purpose of detecting changes to the relevant properties.
    */
   _downloadsMap: null,
 
--- a/toolkit/components/jsdownloads/src/DownloadLegacy.js
+++ b/toolkit/components/jsdownloads/src/DownloadLegacy.js
@@ -90,16 +90,32 @@ DownloadLegacyTransfer.prototype = {
     if ((aStateFlags & Ci.nsIWebProgressListener.STATE_START) &&
         (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
       // The main request has just started.  Wait for the associated Download
       // object to be available before notifying.
       this._deferDownload.promise.then(download => {
         download.saver.onTransferStarted(
                          aRequest,
                          this._cancelable instanceof Ci.nsIHelperAppLauncher);
+
+        // To handle asynchronous cancellation properly, we should hook up the
+        // handler only after we have been notified that the main request
+        // started.  We will wait until the main request stopped before
+        // notifying that the download has been canceled.
+        return download.saver.deferCanceled.promise.then(() => {
+          // Only cancel if the object executing the download is still running.
+          if (this._cancelable && !this._componentFailed) {
+            this._cancelable.cancel(Cr.NS_ERROR_ABORT);
+            if (this._cancelable instanceof Ci.nsIWebBrowserPersist) {
+              // This component will not send the STATE_STOP notification.
+              download.saver.onTransferFinished(aRequest, Cr.NS_ERROR_ABORT);
+              this._cancelable = null;
+            }
+          }
+        });
       }).then(null, Cu.reportError);
     } else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
         (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
       // The last file has been received, or the download failed.  Wait for the
       // associated Download object to be available before notifying.
       this._deferDownload.promise.then(download => {
         download.saver.onTransferFinished(aRequest, aStatus);
       }).then(null, Cu.reportError);
@@ -191,24 +207,16 @@ DownloadLegacyTransfer.prototype = {
       source: { url: aSource.spec, isPrivate: aIsPrivate },
       target: { path: aTarget.QueryInterface(Ci.nsIFileURL).file.path,
                 partFilePath: aTempFile && aTempFile.path },
       saver: "legacy",
       launchWhenSucceeded: launchWhenSucceeded,
       contentType: contentType,
       launcherPath: launcherPath
     }).then(function DLT_I_onDownload(aDownload) {
-      // Now that the saver is available, hook up the cancellation handler.
-      aDownload.saver.deferCanceled.promise.then(() => {
-        // Only cancel if the object executing the download is still running.
-        if (!this._componentFailed) {
-          aCancelable.cancel(Cr.NS_ERROR_ABORT);
-        }
-      }).then(null, Cu.reportError);
-
       // Legacy components keep partial data when they use a ".part" file.
       if (aTempFile) {
         aDownload.tryToKeepPartialData = true;
       }
 
       // Start the download before allowing it to be controlled.
       aDownload.start().then(null, function () {
         // In case the operation failed, ensure we stop downloading data.
--- a/toolkit/components/jsdownloads/src/DownloadList.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadList.jsm
@@ -8,23 +8,27 @@
  * This file includes the following constructors and global objects:
  *
  * DownloadList
  * Represents a collection of Download objects that can be viewed and managed by
  * the user interface, and persisted across sessions.
  *
  * DownloadCombinedList
  * Provides a unified, unordered list combining public and private downloads.
+ *
+ * DownloadSummary
+ * Provides an aggregated view on the contents of a DownloadList.
  */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "DownloadList",
   "DownloadCombinedList",
+  "DownloadSummary",
 ];
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Globals
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
@@ -74,42 +78,54 @@ DownloadList.prototype = {
    * @note When a download is added to the list, its "onchange" event is
    *       registered by the list, thus it cannot be used to monitor the
    *       download.  To receive change notifications for downloads that are
    *       added to the list, use the addView method to register for
    *       onDownloadChanged notifications.
    *
    * @param aDownload
    *        The Download object to add.
+   *
+   * @return {Promise}
+   * @resolves When the download has been added.
+   * @rejects JavaScript exception.
    */
   add: function DL_add(aDownload) {
     this._downloads.push(aDownload);
     aDownload.onchange = this._change.bind(this, aDownload);
     this._notifyAllViews("onDownloadAdded", aDownload);
+
+    return Promise.resolve();
   },
 
   /**
    * Removes a download from the list.  If the download was already removed,
    * this method has no effect.
    *
    * This method does not change the state of the download, to allow adding it
    * to another list, or control it directly.  If you want to dispose of the
    * download object, you should cancel it afterwards, and remove any partially
    * downloaded data if needed.
    *
    * @param aDownload
    *        The Download object to remove.
+   *
+   * @return {Promise}
+   * @resolves When the download has been removed.
+   * @rejects JavaScript exception.
    */
   remove: function DL_remove(aDownload) {
     let index = this._downloads.indexOf(aDownload);
     if (index != -1) {
       this._downloads.splice(index, 1);
       aDownload.onchange = null;
       this._notifyAllViews("onDownloadRemoved", aDownload);
     }
+
+    return Promise.resolve();
   },
 
   /**
    * This function is called when "onchange" events of downloads occur.
    *
    * @param aDownload
    *        The Download object that changed.
    */
@@ -136,46 +152,54 @@ DownloadList.prototype = {
    *          onDownloadChanged: function (aDownload) {
    *            // Called after the properties of aDownload change.
    *          },
    *          onDownloadRemoved: function (aDownload) {
    *            // Called after aDownload is removed from the list.
    *          },
    *        }
    *
-   * @note The onDownloadAdded notifications are sent synchronously.  This
-   *       allows for a complete initialization of the view used for detecting
-   *       changes to downloads to be persisted, before other callers get a
-   *       chance to modify them.
+   * @return {Promise}
+   * @resolves When the view has been registered and all the onDownloadAdded
+   *           notifications for the existing downloads have been sent.
+   * @rejects JavaScript exception.
    */
   addView: function DL_addView(aView)
   {
     this._views.add(aView);
 
     if ("onDownloadAdded" in aView) {
       for (let download of this._downloads) {
         try {
           aView.onDownloadAdded(download);
         } catch (ex) {
           Cu.reportError(ex);
         }
       }
     }
+
+    return Promise.resolve();
   },
 
   /**
-   * Removes a view that was previously added using addView.  The removed view
-   * will not receive any more notifications after this method returns.
+   * Removes a view that was previously added using addView.
    *
    * @param aView
    *        The view object to remove.
+   *
+   * @return {Promise}
+   * @resolves When the view has been removed.  At this point, the removed view
+   *           will not receive any more notifications.
+   * @rejects JavaScript exception.
    */
   removeView: function DL_removeView(aView)
   {
     this._views.delete(aView);
+
+    return Promise.resolve();
   },
 
   /**
    * Notifies all the views of a download addition, change, or removal.
    *
    * @param aMethodName
    *        String containing the name of the method to call on the view.
    * @param aDownload
@@ -213,17 +237,17 @@ DownloadList.prototype = {
       for (let download of list) {
         // Remove downloads that have been canceled, even if the cancellation
         // operation hasn't completed yet so we don't check "stopped" here.
         // Failed downloads with partial data are also removed.
         if (download.stopped && (!download.hasPartialData || download.error) &&
             (!aFilterFn || aFilterFn(download))) {
           // Remove the download first, so that the views don't get the change
           // notifications that may occur during finalization.
-          this.remove(download);
+          yield this.remove(download);
           // Ensure that the download is stopped and no partial data is kept.
           // This works even if the download state has changed meanwhile.  We
           // don't need to wait for the procedure to be complete before
           // processing the other downloads in the list.
           download.finalize(true).then(null, Cu.reportError);
         }
       }
     }.bind(this)).then(null, Cu.reportError);
@@ -245,18 +269,18 @@ DownloadList.prototype = {
  * @param aPrivateList
  *        Underlying DownloadList containing private downloads.
  */
 function DownloadCombinedList(aPublicList, aPrivateList)
 {
   DownloadList.call(this);
   this._publicList = aPublicList;
   this._privateList = aPrivateList;
-  aPublicList.addView(this);
-  aPrivateList.addView(this);
+  aPublicList.addView(this).then(null, Cu.reportError);
+  aPrivateList.addView(this).then(null, Cu.reportError);
 }
 
 DownloadCombinedList.prototype = {
   __proto__: DownloadList.prototype,
 
   /**
    * Underlying DownloadList containing public downloads.
    */
@@ -273,44 +297,52 @@ DownloadCombinedList.prototype = {
    * @note When a download is added to the list, its "onchange" event is
    *       registered by the list, thus it cannot be used to monitor the
    *       download.  To receive change notifications for downloads that are
    *       added to the list, use the addView method to register for
    *       onDownloadChanged notifications.
    *
    * @param aDownload
    *        The Download object to add.
+   *
+   * @return {Promise}
+   * @resolves When the download has been added.
+   * @rejects JavaScript exception.
    */
   add: function (aDownload)
   {
     if (aDownload.source.isPrivate) {
-      this._privateList.add(aDownload);
+      return this._privateList.add(aDownload);
     } else {
-      this._publicList.add(aDownload);
+      return this._publicList.add(aDownload);
     }
   },
 
   /**
    * Removes a download from the list.  If the download was already removed,
    * this method has no effect.
    *
    * This method does not change the state of the download, to allow adding it
    * to another list, or control it directly.  If you want to dispose of the
    * download object, you should cancel it afterwards, and remove any partially
    * downloaded data if needed.
    *
    * @param aDownload
    *        The Download object to remove.
+   *
+   * @return {Promise}
+   * @resolves When the download has been removed.
+   * @rejects JavaScript exception.
    */
   remove: function (aDownload)
   {
     if (aDownload.source.isPrivate) {
-      this._privateList.remove(aDownload);
+      return this._privateList.remove(aDownload);
     } else {
-      this._publicList.remove(aDownload);
+      return this._publicList.remove(aDownload);
     }
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// DownloadList view
 
   onDownloadAdded: function (aDownload)
   {
@@ -327,8 +359,207 @@ DownloadCombinedList.prototype = {
   {
     let index = this._downloads.indexOf(aDownload);
     if (index != -1) {
       this._downloads.splice(index, 1);
     }
     this._notifyAllViews("onDownloadRemoved", aDownload);
   },
 };
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadSummary
+
+/**
+ * Provides an aggregated view on the contents of a DownloadList.
+ */
+function DownloadSummary() {
+  this._downloads = [];
+  this._views = new Set();
+}
+
+DownloadSummary.prototype = {
+  /**
+   * Array of Download objects that are currently part of the summary.
+   */
+  _downloads: null,
+
+  /**
+   * Underlying DownloadList whose contents should be summarized.
+   */
+  _list: null,
+
+  /**
+   * This method may be called once to bind this object to a DownloadList.
+   *
+   * Views on the summarized data can be registered before this object is bound
+   * to an actual list.  This allows the summary to be used without requiring
+   * the initialization of the DownloadList first.
+   *
+   * @param aList
+   *        Underlying DownloadList whose contents should be summarized.
+   *
+   * @return {Promise}
+   * @resolves When the view on the underlying list has been registered.
+   * @rejects JavaScript exception.
+   */
+  bindToList: function (aList)
+  {
+    if (this._list) {
+      throw new Error("bindToList may be called only once.");
+    }
+
+    return aList.addView(this).then(() => {
+      // Set the list reference only after addView has returned, so that we don't
+      // send a notification to our views for each download that is added.
+      this._list = aList;
+      this._onListChanged();
+    });
+  },
+
+  /**
+   * Set of currently registered views.
+   */
+  _views: null,
+
+  /**
+   * Adds a view that will be notified of changes to the summary.  The newly
+   * added view will receive an initial onSummaryChanged notification.
+   *
+   * @param aView
+   *        The view object to add.  The following methods may be defined:
+   *        {
+   *          onSummaryChanged: function () {
+   *            // Called after any property of the summary has changed.
+   *          },
+   *        }
+   *
+   * @return {Promise}
+   * @resolves When the view has been registered and the onSummaryChanged
+   *           notification has been sent.
+   * @rejects JavaScript exception.
+   */
+  addView: function (aView)
+  {
+    this._views.add(aView);
+
+    if ("onSummaryChanged" in aView) {
+      try {
+        aView.onSummaryChanged();
+      } catch (ex) {
+        Cu.reportError(ex);
+      }
+    }
+
+    return Promise.resolve();
+  },
+
+  /**
+   * Removes a view that was previously added using addView.
+   *
+   * @param aView
+   *        The view object to remove.
+   *
+   * @return {Promise}
+   * @resolves When the view has been removed.  At this point, the removed view
+   *           will not receive any more notifications.
+   * @rejects JavaScript exception.
+   */
+  removeView: function (aView)
+  {
+    this._views.delete(aView);
+
+    return Promise.resolve();
+  },
+
+  /**
+   * Indicates whether all the downloads are currently stopped.
+   */
+  allHaveStopped: true,
+
+  /**
+   * Indicates the total number of bytes to be transferred before completing all
+   * the downloads that are currently in progress.
+   *
+   * For downloads that do not have a known final size, the number of bytes
+   * currently transferred is reported as part of this property.
+   *
+   * This is zero if no downloads are currently in progress.
+   */
+  progressTotalBytes: 0,
+
+  /**
+   * Number of bytes currently transferred as part of all the downloads that are
+   * currently in progress.
+   *
+   * This is zero if no downloads are currently in progress.
+   */
+  progressCurrentBytes: 0,
+
+  /**
+   * This function is called when any change in the list of downloads occurs,
+   * and will recalculate the summary and notify the views in case the
+   * aggregated properties are different.
+   */
+  _onListChanged: function () {
+    let allHaveStopped = true;
+    let progressTotalBytes = 0;
+    let progressCurrentBytes = 0;
+
+    // Recalculate the aggregated state.  See the description of the individual
+    // properties for an explanation of the summarization logic.
+    for (let download of this._downloads) {
+      if (!download.stopped) {
+        allHaveStopped = false;
+        progressTotalBytes += download.hasProgress ? download.totalBytes
+                                                   : download.currentBytes;
+        progressCurrentBytes += download.currentBytes;
+      }
+    }
+
+    // Exit now if the properties did not change.
+    if (this.allHaveStopped == allHaveStopped &&
+        this.progressTotalBytes == progressTotalBytes &&
+        this.progressCurrentBytes == progressCurrentBytes) {
+      return;
+    }
+
+    this.allHaveStopped = allHaveStopped;
+    this.progressTotalBytes = progressTotalBytes;
+    this.progressCurrentBytes = progressCurrentBytes;
+
+    // Notify all the views that our properties changed.
+    for (let view of this._views) {
+      try {
+        if ("onSummaryChanged" in view) {
+          view.onSummaryChanged();
+        }
+      } catch (ex) {
+        Cu.reportError(ex);
+      }
+    }
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// DownloadList view
+
+  onDownloadAdded: function (aDownload)
+  {
+    this._downloads.push(aDownload);
+    if (this._list) {
+      this._onListChanged();
+    }
+  },
+
+  onDownloadChanged: function (aDownload)
+  {
+    this._onListChanged();
+  },
+
+  onDownloadRemoved: function (aDownload)
+  {
+    let index = this._downloads.indexOf(aDownload);
+    if (index != -1) {
+      this._downloads.splice(index, 1);
+    }
+    this._onListChanged();
+  },
+};
--- a/toolkit/components/jsdownloads/src/DownloadStore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadStore.jsm
@@ -127,17 +127,17 @@ DownloadStore.prototype = {
               // If the download was not in progress, try to update the current
               // progress from disk.  This is relevant in case we retained
               // partially downloaded data.
               yield download.refresh();
             }
           } finally {
             // Add the download to the list if we succeeded in creating it,
             // after we have updated its initial state.
-            this.list.add(download);
+            yield this.list.add(download);
           }
         } catch (ex) {
           // If an item is unrecognized, don't prevent others from being loaded.
           Cu.reportError(ex);
         }
       }
     }.bind(this));
   },
--- a/toolkit/components/jsdownloads/src/Downloads.jsm
+++ b/toolkit/components/jsdownloads/src/Downloads.jsm
@@ -26,16 +26,18 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/DownloadCore.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadCombinedList",
                                   "resource://gre/modules/DownloadList.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration",
                                   "resource://gre/modules/DownloadIntegration.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadList",
                                   "resource://gre/modules/DownloadList.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadSummary",
+                                  "resource://gre/modules/DownloadList.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
                                   "resource://gre/modules/DownloadUIHelper.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -125,17 +127,17 @@ this.Downloads = {
    *          isPrivate: Indicates whether the download originated from a
    *                     private window.
    *        }
    *
    * @return {Promise}
    * @resolves When the download has finished successfully.
    * @rejects JavaScript exception if the download failed.
    */
-  simpleDownload: function D_simpleDownload(aSource, aTarget, aOptions) {
+  fetch: function (aSource, aTarget, aOptions) {
     return this.createDownload({
       source: aSource,
       target: aTarget,
     }).then(function D_SD_onSuccess(aDownload) {
       if (aOptions && ("isPrivate" in aOptions)) {
         aDownload.source.isPrivate = aOptions.isPrivate;
       }
       return aDownload.start();
@@ -158,53 +160,96 @@ this.Downloads = {
    *        the Downloads.PRIVATE list based on their properties.
    *
    * @return {Promise}
    * @resolves The requested DownloadList or DownloadCombinedList object.
    * @rejects JavaScript exception.
    */
   getList: function (aType)
   {
+    if (!this._promiseListsInitialized) {
+      this._promiseListsInitialized = Task.spawn(function () {
+        let publicList = new DownloadList();
+        let privateList = new DownloadList();
+        let combinedList = new DownloadCombinedList(publicList, privateList);
+
+        try {
+          yield DownloadIntegration.addListObservers(publicList, false);
+          yield DownloadIntegration.addListObservers(privateList, true);
+          yield DownloadIntegration.initializePublicDownloadList(publicList);
+        } catch (ex) {
+          Cu.reportError(ex);
+        }
+
+        let publicSummary = yield this.getSummary(Downloads.PUBLIC);
+        let privateSummary = yield this.getSummary(Downloads.PRIVATE);
+        let combinedSummary = yield this.getSummary(Downloads.ALL);
+
+        yield publicSummary.bindToList(publicList);
+        yield privateSummary.bindToList(privateList);
+        yield combinedSummary.bindToList(combinedList);
+
+        this._lists[Downloads.PUBLIC] = publicList;
+        this._lists[Downloads.PRIVATE] = privateList;
+        this._lists[Downloads.ALL] = combinedList;
+      }.bind(this));
+    }
+
+    return this._promiseListsInitialized.then(() => this._lists[aType]);
+  },
+
+  /**
+   * Promise resolved when the initialization of the download lists has
+   * completed, or null if initialization has never been requested.
+   */
+  _promiseListsInitialized: null,
+
+  /**
+   * After initialization, this object is populated with one key for each type
+   * of download list that can be returned (Downloads.PUBLIC, Downloads.PRIVATE,
+   * or Downloads.ALL).  The values are the DownloadList objects.
+   */
+  _lists: {},
+
+  /**
+   * Retrieves the specified type of DownloadSummary object.  There is one
+   * download summary for each type, and this method always retrieves a
+   * reference to the same download summary when called with the same argument.
+   *
+   * Calling this function does not cause the list of public downloads to be
+   * reloaded from the previous session.  The summary will behave as if no
+   * downloads are present until the getList method is called.
+   *
+   * @param aType
+   *        This can be Downloads.PUBLIC, Downloads.PRIVATE, or Downloads.ALL.
+   *
+   * @return {Promise}
+   * @resolves The requested DownloadList or DownloadCombinedList object.
+   * @rejects JavaScript exception.
+   */
+  getSummary: function (aType)
+  {
     if (aType != Downloads.PUBLIC && aType != Downloads.PRIVATE &&
         aType != Downloads.ALL) {
       throw new Error("Invalid aType argument.");
     }
 
-    if (!(aType in this._listPromises)) {
-      this._listPromises[aType] = Task.spawn(function () {
-        let list;
-        if (aType == Downloads.ALL) {
-          list = new DownloadCombinedList(
-                       (yield this.getList(Downloads.PUBLIC)),
-                       (yield this.getList(Downloads.PRIVATE)));
-        } else {
-          list = new DownloadList();
-          try {
-            yield DownloadIntegration.addListObservers(
-                                        list, aType == Downloads.PRIVATE);
-            if (aType == Downloads.PUBLIC) {
-              yield DownloadIntegration.initializePublicDownloadList(list);
-            }
-          } catch (ex) {
-            Cu.reportError(ex);
-          }
-        }
-        throw new Task.Result(list);
-      }.bind(this));
+    if (!(aType in this._summaries)) {
+      this._summaries[aType] = new DownloadSummary();
     }
 
-    return this._listPromises[aType];
+    return Promise.resolve(this._summaries[aType]);
   },
 
   /**
-   * This object is populated by the getList method with one key for each type
-   * of object that can be returned (Downloads.PUBLIC, Downloads.PRIVATE, or
-   * Downloads.ALL).  The values are the promises returned by the method.
+   * This object is populated by the getSummary method with one key for each
+   * type of object that can be returned (Downloads.PUBLIC, Downloads.PRIVATE,
+   * or Downloads.ALL).  The values are the DownloadSummary objects.
    */
-  _listPromises: {},
+  _summaries: {},
 
   /**
    * Returns the system downloads directory asynchronously.
    *   Mac OSX:
    *     User downloads directory
    *   XP/2K:
    *     My Documents/Downloads
    *   Vista and others:
--- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -31,71 +31,16 @@ function promiseStartDownload(aSourceUrl
 
   return promiseNewDownload(aSourceUrl).then(download => {
     download.start();
     return download;
   });
 }
 
 /**
- * Waits for a download to reach half of its progress, in case it has not
- * reached the expected progress already.
- *
- * @param aDownload
- *        The Download object to wait upon.
- *
- * @return {Promise}
- * @resolves When the download has reached half of its progress.
- * @rejects Never.
- */
-function promiseDownloadMidway(aDownload) {
-  let deferred = Promise.defer();
-
-  // Wait for the download to reach half of its progress.
-  let onchange = function () {
-    if (!aDownload.stopped && !aDownload.canceled && aDownload.progress == 50) {
-      aDownload.onchange = null;
-      deferred.resolve();
-    }
-  };
-
-  // Register for the notification, but also call the function directly in
-  // case the download already reached the expected progress.
-  aDownload.onchange = onchange;
-  onchange();
-
-  return deferred.promise;
-}
-
-/**
- * Waits for a download to finish, in case it has not finished already.
- *
- * @param aDownload
- *        The Download object to wait upon.
- *
- * @return {Promise}
- * @resolves When the download has finished successfully.
- * @rejects JavaScript exception if the download failed.
- */
-function promiseDownloadStopped(aDownload) {
-  if (!aDownload.stopped) {
-    // The download is in progress, wait for the current attempt to finish and
-    // report any errors that may occur.
-    return aDownload.start();
-  }
-
-  if (aDownload.succeeded) {
-    return Promise.resolve();
-  }
-
-  // The download failed or was canceled.
-  return Promise.reject(aDownload.error || new Error("Download canceled."));
-}
-
-/**
  * Creates and starts a new download, configured to keep partial data, and
  * returns only when the first part of "interruptible_resumable.txt" has been
  * saved to disk.  You must call "continueResponses" to allow the interruptible
  * request to continue.
  *
  * This function uses either DownloadCopySaver or DownloadLegacySaver based on
  * the current test run.
  *
@@ -676,22 +621,16 @@ add_task(function test_cancel_midway_res
 /**
  * Cancels a download and restarts it from where it stopped.
  */
 add_task(function test_cancel_midway_restart_tryToKeepPartialData()
 {
   let download = yield promiseStartDownload_tryToKeepPartialData();
   yield download.cancel();
 
-  // This time-based solution is a workaround to avoid intermittent failures,
-  // and will be removed when bug 899102 is resolved.
-  if (gUseLegacySaver) {
-    yield promiseTimeout(250);
-  }
-
   do_check_true(download.stopped);
   do_check_true(download.hasPartialData);
 
   // The target file should not exist, but we should have kept the partial data.
   do_check_false(yield OS.File.exists(download.target.path));
   yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
 
   // Verify that the server sent the response from the start.
@@ -735,22 +674,16 @@ add_task(function test_cancel_midway_res
  * Cancels a download while keeping partially downloaded data, then removes the
  * data and restarts the download from the beginning.
  */
 add_task(function test_cancel_midway_restart_removePartialData()
 {
   let download = yield promiseStartDownload_tryToKeepPartialData();
   yield download.cancel();
 
-  // This time-based solution is a workaround to avoid intermittent failures,
-  // and will be removed when bug 899102 is resolved.
-  if (gUseLegacySaver) {
-    yield promiseTimeout(250);
-  }
-
   do_check_true(download.hasPartialData);
   yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
 
   yield download.removePartialData();
 
   do_check_false(download.hasPartialData);
   do_check_false(yield OS.File.exists(download.target.partFilePath));
 
@@ -772,22 +705,16 @@ add_task(function test_cancel_midway_res
  * data and restarts the download from the beginning without keeping the partial
  * data anymore.
  */
 add_task(function test_cancel_midway_restart_tryToKeepPartialData_false()
 {
   let download = yield promiseStartDownload_tryToKeepPartialData();
   yield download.cancel();
 
-  // This time-based solution is a workaround to avoid intermittent failures,
-  // and will be removed when bug 899102 is resolved.
-  if (gUseLegacySaver) {
-    yield promiseTimeout(250);
-  }
-
   download.tryToKeepPartialData = false;
 
   // The above property change does not affect existing partial data.
   do_check_true(download.hasPartialData);
   yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
 
   yield download.removePartialData();
   do_check_false(yield OS.File.exists(download.target.partFilePath));
@@ -800,22 +727,16 @@ add_task(function test_cancel_midway_res
   yield promisePartFileReady(download);
 
   // While the download is in progress, we should still have a ".part" file.
   do_check_false(download.hasPartialData);
   do_check_true(yield OS.File.exists(download.target.partFilePath));
 
   yield download.cancel();
 
-  // This time-based solution is a workaround to avoid intermittent failures,
-  // and will be removed when bug 899102 is resolved.
-  if (gUseLegacySaver) {
-    yield promiseTimeout(250);
-  }
-
   // The ".part" file should be deleted now that the download is canceled.
   do_check_false(download.hasPartialData);
   do_check_false(yield OS.File.exists(download.target.partFilePath));
 
   // The third time, we'll request and obtain the entire response again.
   continueResponses();
   yield download.start();
 
@@ -1012,38 +933,26 @@ add_task(function test_finalize()
  * Checks that the "finalize" method can remove partially downloaded data.
  */
 add_task(function test_finalize_tryToKeepPartialData()
 {
   // Check finalization without removing partial data.
   let download = yield promiseStartDownload_tryToKeepPartialData();
   yield download.finalize();
 
-  // This time-based solution is a workaround to avoid intermittent failures,
-  // and will be removed when bug 899102 is resolved.
-  if (gUseLegacySaver) {
-    yield promiseTimeout(250);
-  }
-
   do_check_true(download.hasPartialData);
   do_check_true(yield OS.File.exists(download.target.partFilePath));
 
   // Clean up.
   yield download.removePartialData();
 
   // Check finalization while removing partial data.
   download = yield promiseStartDownload_tryToKeepPartialData();
   yield download.finalize(true);
 
-  // This time-based solution is a workaround to avoid intermittent failures,
-  // and will be removed when bug 899102 is resolved.
-  if (gUseLegacySaver) {
-    yield promiseTimeout(250);
-  }
-
   do_check_false(download.hasPartialData);
   do_check_false(yield OS.File.exists(download.target.partFilePath));
 });
 
 /**
  * Checks that whenSucceeded returns a promise that is resolved after a restart.
  */
 add_task(function test_whenSucceeded_after_restart()
@@ -1262,18 +1171,18 @@ add_task(function test_public_and_privat
       testCount++;
     } else if (testCount == 2)  {
       // No cookies should exist for first private download.
       do_check_false(aRequest.hasHeader("Cookie"));
     }
   });
 
   let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
-  yield Downloads.simpleDownload(sourceUrl, targetFile);
-  yield Downloads.simpleDownload(sourceUrl, targetFile);
+  yield Downloads.fetch(sourceUrl, targetFile);
+  yield Downloads.fetch(sourceUrl, targetFile);
 
   if (!gUseLegacySaver) {
     let download = yield Downloads.createDownload({
       source: { url: sourceUrl, isPrivate: true },
       target: targetFile,
     });
     yield download.start();
   } else {
--- a/toolkit/components/jsdownloads/test/unit/head.js
+++ b/toolkit/components/jsdownloads/test/unit/head.js
@@ -364,26 +364,27 @@ function promiseStartLegacyDownload(aSou
 
   let deferred = Promise.defer();
 
   Downloads.getList(Downloads.ALL).then(function (aList) {
     // Temporarily register a view that will get notified when the download we
     // are controlling becomes visible in the list of downloads.
     aList.addView({
       onDownloadAdded: function (aDownload) {
-        aList.removeView(this);
+        aList.removeView(this).then(null, do_report_unexpected_exception);
 
         // Remove the download to keep the list empty for the next test.  This
         // also allows the caller to register the "onchange" event directly.
-        aList.remove(aDownload);
+        let promise = aList.remove(aDownload);
 
         // When the download object is ready, make it available to the caller.
-        deferred.resolve(aDownload);
+        promise.then(() => deferred.resolve(aDownload),
+                     do_report_unexpected_exception);
       },
-    });
+    }).then(null, do_report_unexpected_exception);
 
     let isPrivate = aOptions && aOptions.isPrivate;
 
     // Initialize the components so they reference each other.  This will cause
     // the Download object to be created and added to the public downloads.
     transfer.init(sourceURI, NetUtil.newURI(targetFile), null, mimeInfo, null,
                   null, persist, isPrivate);
     persist.progressListener = transfer;
@@ -416,26 +417,27 @@ function promiseStartExternalHelperAppSe
 
   let deferred = Promise.defer();
 
   Downloads.getList(Downloads.PUBLIC).then(function (aList) {
     // Temporarily register a view that will get notified when the download we
     // are controlling becomes visible in the list of downloads.
     aList.addView({
       onDownloadAdded: function (aDownload) {
-        aList.removeView(this);
+        aList.removeView(this).then(null, do_report_unexpected_exception);
 
         // Remove the download to keep the list empty for the next test.  This
         // also allows the caller to register the "onchange" event directly.
-        aList.remove(aDownload);
+        let promise = aList.remove(aDownload);
 
         // When the download object is ready, make it available to the caller.
-        deferred.resolve(aDownload);
+        promise.then(() => deferred.resolve(aDownload),
+                     do_report_unexpected_exception);
       },
-    });
+    }).then(null, do_report_unexpected_exception);
 
     let channel = NetUtil.newChannel(sourceURI);
 
     // Start the actual download process.
     channel.asyncOpen({
       contentListener: null,
 
       onStartRequest: function (aRequest, aContext)
@@ -459,40 +461,89 @@ function promiseStartExternalHelperAppSe
       },
     }, null);
   }.bind(this)).then(null, do_report_unexpected_exception);
 
   return deferred.promise;
 }
 
 /**
+ * Waits for a download to reach half of its progress, in case it has not
+ * reached the expected progress already.
+ *
+ * @param aDownload
+ *        The Download object to wait upon.
+ *
+ * @return {Promise}
+ * @resolves When the download has reached half of its progress.
+ * @rejects Never.
+ */
+function promiseDownloadMidway(aDownload) {
+  let deferred = Promise.defer();
+
+  // Wait for the download to reach half of its progress.
+  let onchange = function () {
+    if (!aDownload.stopped && !aDownload.canceled && aDownload.progress == 50) {
+      aDownload.onchange = null;
+      deferred.resolve();
+    }
+  };
+
+  // Register for the notification, but also call the function directly in
+  // case the download already reached the expected progress.
+  aDownload.onchange = onchange;
+  onchange();
+
+  return deferred.promise;
+}
+
+/**
+ * Waits for a download to finish, in case it has not finished already.
+ *
+ * @param aDownload
+ *        The Download object to wait upon.
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects JavaScript exception if the download failed.
+ */
+function promiseDownloadStopped(aDownload) {
+  if (!aDownload.stopped) {
+    // The download is in progress, wait for the current attempt to finish and
+    // report any errors that may occur.
+    return aDownload.start();
+  }
+
+  if (aDownload.succeeded) {
+    return Promise.resolve();
+  }
+
+  // The download failed or was canceled.
+  return Promise.reject(aDownload.error || new Error("Download canceled."));
+}
+
+/**
  * Returns a new public or private DownloadList object.
  *
  * @param aIsPrivate
  *        True for the private list, false or undefined for the public list.
  *
  * @return {Promise}
  * @resolves The newly created DownloadList object.
  * @rejects JavaScript exception.
  */
 function promiseNewList(aIsPrivate)
 {
-  let type = aIsPrivate ? Downloads.PRIVATE : Downloads.PUBLIC;
-
-  // Force the creation of a new list.
-  if (type in Downloads._listPromises) {
-    delete Downloads._listPromises[type];
-  }
+  // We need to clear all the internal state for the list and summary objects,
+  // since all the objects are interdependent internally.
+  Downloads._promiseListsInitialized = null;
+  Downloads._lists = {};
+  Downloads._summaries = {};
 
-  // Invalidate the combined list, if any.
-  if (Downloads.ALL in Downloads._listPromises) {
-    delete Downloads._listPromises[Downloads.ALL];
-  }
-
-  return Downloads.getList(type);
+  return Downloads.getList(aIsPrivate ? Downloads.PRIVATE : Downloads.PUBLIC);
 }
 
 /**
  * Ensures that the given file contents are equal to the given string.
  *
  * @param aPath
  *        String containing the path of the file whose contents should be
  *        verified.
--- a/toolkit/components/jsdownloads/test/unit/tail.js
+++ b/toolkit/components/jsdownloads/test/unit/tail.js
@@ -9,14 +9,17 @@
 
 "use strict";
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Termination functions common to all tests
 
 add_task(function test_common_terminate()
 {
+  // Ensure all the pending HTTP requests have a chance to finish.
+  continueResponses();
+
   // Stop the HTTP server.  We must do this inside a task in "tail.js" until the
   // xpcshell testing framework supports asynchronous termination functions.
   let deferred = Promise.defer();
   gHttpServer.stop(deferred.resolve);
   yield deferred.promise;
 });
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js
@@ -11,8 +11,14 @@
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Execution of common tests
 
 let gUseLegacySaver = false;
 
 let scriptFile = do_get_file("common_test_Download.js");
 Services.scriptloader.loadSubScript(NetUtil.newURI(scriptFile).spec);
+
+////////////////////////////////////////////////////////////////////////////////
+//// Termination
+
+let tailFile = do_get_file("tail.js");
+Services.scriptloader.loadSubScript(NetUtil.newURI(tailFile).spec);
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js
@@ -213,33 +213,33 @@ add_task(function test_notifications()
     let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
     let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
     let download3 = yield promiseNewDownload(httpUrl("interruptible.txt"));
     let promiseAttempt1 = download1.start();
     let promiseAttempt2 = download2.start();
     download3.start();
 
     // Add downloads to list.
-    list.add(download1);
-    list.add(download2);
-    list.add(download3);
+    yield list.add(download1);
+    yield list.add(download2);
+    yield list.add(download3);
     // Cancel third download
     yield download3.cancel();
 
     notifyPromptObservers(isPrivate, 2, 2);
 
     // Allow the downloads to complete.
     continueResponses();
     yield promiseAttempt1;
     yield promiseAttempt2;
 
     // Clean up.
-    list.remove(download1);
-    list.remove(download2);
-    list.remove(download3);
+    yield list.remove(download1);
+    yield list.remove(download2);
+    yield list.remove(download3);
   }
 });
 
 /**
  * Tests that notifications prompts observers are not notified if there are no
  * public or private active downloads.
  */
 add_task(function test_no_notifications()
@@ -249,60 +249,60 @@ add_task(function test_no_notifications(
   for (let isPrivate of [false, true]) {
     let list = yield promiseNewList(isPrivate);
     let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
     let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
     download1.start();
     download2.start();
 
     // Add downloads to list.
-    list.add(download1);
-    list.add(download2);
+    yield list.add(download1);
+    yield list.add(download2);
 
     yield download1.cancel();
     yield download2.cancel();
 
     notifyPromptObservers(isPrivate, 0, 0);
 
     // Clean up.
-    list.remove(download1);
-    list.remove(download2);
+    yield list.remove(download1);
+    yield list.remove(download2);
   }
 });
 
 /**
  * Tests notifications prompts when observers are notified if there are public
  * and private active downloads at the same time.
  */
 add_task(function test_mix_notifications()
 {
   enableObserversTestMode();
   mustInterruptResponses();
 
   let publicList = yield promiseNewList();
-  let privateList = yield promiseNewList(true);
+  let privateList = yield Downloads.getList(Downloads.PRIVATE);
   let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
   let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
   let promiseAttempt1 = download1.start();
   let promiseAttempt2 = download2.start();
 
   // Add downloads to lists.
-  publicList.add(download1);
-  privateList.add(download2);
+  yield publicList.add(download1);
+  yield privateList.add(download2);
 
   notifyPromptObservers(true, 2, 1);
 
   // Allow the downloads to complete.
   continueResponses();
   yield promiseAttempt1;
   yield promiseAttempt2;
 
   // Clean up.
-  publicList.remove(download1);
-  privateList.remove(download2);
+  yield publicList.remove(download1);
+  yield privateList.remove(download2);
 });
 
 /**
  * Tests suspending and resuming as well as going offline and then online again.
  * The downloads should stop when suspending and start again when resuming.
  */
 add_task(function test_suspend_resume()
 {
@@ -392,18 +392,18 @@ add_task(function test_exit_private_brow
 
   let privateList = yield promiseNewList(true);
   let download1 = yield promiseNewDownload(httpUrl("source.txt"));
   let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
   let promiseAttempt1 = download1.start();
   let promiseAttempt2 = download2.start();
 
   // Add downloads to list.
-  privateList.add(download1);
-  privateList.add(download2);
+  yield privateList.add(download1);
+  yield privateList.add(download2);
 
   // Complete the download.
   yield promiseAttempt1;
 
   do_check_eq((yield privateList.getAll()).length, 2);
 
   // Simulate exiting the private browsing.
   DownloadIntegration._deferTestClearPrivateList = Promise.defer();
@@ -411,8 +411,13 @@ add_task(function test_exit_private_brow
   let result = yield DownloadIntegration._deferTestClearPrivateList.promise;
 
   do_check_eq(result, "success");
   do_check_eq((yield privateList.getAll()).length, 0);
 
   continueResponses();
 });
 
+////////////////////////////////////////////////////////////////////////////////
+//// Termination
+
+let tailFile = do_get_file("tail.js");
+Services.scriptloader.loadSubScript(NetUtil.newURI(tailFile).spec);
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js
@@ -11,8 +11,14 @@
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Execution of common tests
 
 let gUseLegacySaver = true;
 
 let scriptFile = do_get_file("common_test_Download.js");
 Services.scriptloader.loadSubScript(NetUtil.newURI(scriptFile).spec);
+
+////////////////////////////////////////////////////////////////////////////////
+//// Termination
+
+let tailFile = do_get_file("tail.js");
+Services.scriptloader.loadSubScript(NetUtil.newURI(tailFile).spec);
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadList.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js
@@ -86,24 +86,24 @@ add_task(function test_construction()
 /**
  * Checks the methods to add and retrieve items from the list.
  */
 add_task(function test_add_getAll()
 {
   let list = yield promiseNewList();
 
   let downloadOne = yield promiseNewDownload();
-  list.add(downloadOne);
+  yield list.add(downloadOne);
 
   let itemsOne = yield list.getAll();
   do_check_eq(itemsOne.length, 1);
   do_check_eq(itemsOne[0], downloadOne);
 
   let downloadTwo = yield promiseNewDownload();
-  list.add(downloadTwo);
+  yield list.add(downloadTwo);
 
   let itemsTwo = yield list.getAll();
   do_check_eq(itemsTwo.length, 2);
   do_check_eq(itemsTwo[0], downloadOne);
   do_check_eq(itemsTwo[1], downloadTwo);
 
   // The first snapshot should not have been modified.
   do_check_eq(itemsOne.length, 1);
@@ -111,65 +111,65 @@ add_task(function test_add_getAll()
 
 /**
  * Checks the method to remove items from the list.
  */
 add_task(function test_remove()
 {
   let list = yield promiseNewList();
 
-  list.add(yield promiseNewDownload());
-  list.add(yield promiseNewDownload());
+  yield list.add(yield promiseNewDownload());
+  yield list.add(yield promiseNewDownload());
 
   let items = yield list.getAll();
-  list.remove(items[0]);
+  yield list.remove(items[0]);
 
   // Removing an item that was never added should not raise an error.
-  list.remove(yield promiseNewDownload());
+  yield list.remove(yield promiseNewDownload());
 
   items = yield list.getAll();
   do_check_eq(items.length, 1);
 });
 
 /**
  * Tests that the "add", "remove", and "getAll" methods on the global
  * DownloadCombinedList object combine the contents of the global DownloadList
  * objects for public and private downloads.
  */
 add_task(function test_DownloadCombinedList_add_remove_getAll()
 {
   let publicList = yield promiseNewList();
-  let privateList = yield promiseNewList(true);
+  let privateList = yield Downloads.getList(Downloads.PRIVATE);
   let combinedList = yield Downloads.getList(Downloads.ALL);
 
   let publicDownload = yield promiseNewDownload();
   let privateDownload = yield Downloads.createDownload({
     source: { url: httpUrl("source.txt"), isPrivate: true },
     target: getTempFile(TEST_TARGET_FILE_NAME).path,
   });
 
-  publicList.add(publicDownload);
-  privateList.add(privateDownload);
+  yield publicList.add(publicDownload);
+  yield privateList.add(privateDownload);
 
   do_check_eq((yield combinedList.getAll()).length, 2);
 
-  combinedList.remove(publicDownload);
-  combinedList.remove(privateDownload);
+  yield combinedList.remove(publicDownload);
+  yield combinedList.remove(privateDownload);
 
   do_check_eq((yield combinedList.getAll()).length, 0);
 
-  combinedList.add(publicDownload);
-  combinedList.add(privateDownload);
+  yield combinedList.add(publicDownload);
+  yield combinedList.add(privateDownload);
 
   do_check_eq((yield publicList.getAll()).length, 1);
   do_check_eq((yield privateList.getAll()).length, 1);
   do_check_eq((yield combinedList.getAll()).length, 2);
 
-  publicList.remove(publicDownload);
-  privateList.remove(privateDownload);
+  yield publicList.remove(publicDownload);
+  yield privateList.remove(privateDownload);
 
   do_check_eq((yield combinedList.getAll()).length, 0);
 });
 
 /**
  * Checks that views receive the download add and remove notifications, and that
  * adding and removing views works as expected, both for a normal and a combined
  * list.
@@ -183,59 +183,59 @@ add_task(function test_notifications_add
       list = yield Downloads.getList(Downloads.ALL);
     }
 
     let downloadOne = yield promiseNewDownload();
     let downloadTwo = yield Downloads.createDownload({
       source: { url: httpUrl("source.txt"), isPrivate: true },
       target: getTempFile(TEST_TARGET_FILE_NAME).path,
     });
-    list.add(downloadOne);
-    list.add(downloadTwo);
+    yield list.add(downloadOne);
+    yield list.add(downloadTwo);
 
     // Check that we receive add notifications for existing elements.
     let addNotifications = 0;
     let viewOne = {
       onDownloadAdded: function (aDownload) {
         // The first download to be notified should be the first that was added.
         if (addNotifications == 0) {
           do_check_eq(aDownload, downloadOne);
         } else if (addNotifications == 1) {
           do_check_eq(aDownload, downloadTwo);
         }
         addNotifications++;
       },
     };
-    list.addView(viewOne);
+    yield list.addView(viewOne);
     do_check_eq(addNotifications, 2);
 
     // Check that we receive add notifications for new elements.
-    list.add(yield promiseNewDownload());
+    yield list.add(yield promiseNewDownload());
     do_check_eq(addNotifications, 3);
 
     // Check that we receive remove notifications.
     let removeNotifications = 0;
     let viewTwo = {
       onDownloadRemoved: function (aDownload) {
         do_check_eq(aDownload, downloadOne);
         removeNotifications++;
       },
     };
-    list.addView(viewTwo);
-    list.remove(downloadOne);
+    yield list.addView(viewTwo);
+    yield list.remove(downloadOne);
     do_check_eq(removeNotifications, 1);
 
     // We should not receive remove notifications after the view is removed.
-    list.removeView(viewTwo);
-    list.remove(downloadTwo);
+    yield list.removeView(viewTwo);
+    yield list.remove(downloadTwo);
     do_check_eq(removeNotifications, 1);
 
     // We should not receive add notifications after the view is removed.
-    list.removeView(viewOne);
-    list.add(yield promiseNewDownload());
+    yield list.removeView(viewOne);
+    yield list.add(yield promiseNewDownload());
     do_check_eq(addNotifications, 3);
   }
 });
 
 /**
  * Checks that views receive the download change notifications, both for a
  * normal and a combined list.
  */
@@ -248,33 +248,33 @@ add_task(function test_notifications_cha
       list = yield Downloads.getList(Downloads.ALL);
     }
 
     let downloadOne = yield promiseNewDownload();
     let downloadTwo = yield Downloads.createDownload({
       source: { url: httpUrl("source.txt"), isPrivate: true },
       target: getTempFile(TEST_TARGET_FILE_NAME).path,
     });
-    list.add(downloadOne);
-    list.add(downloadTwo);
+    yield list.add(downloadOne);
+    yield list.add(downloadTwo);
 
     // Check that we receive change notifications.
     let receivedOnDownloadChanged = false;
-    list.addView({
+    yield list.addView({
       onDownloadChanged: function (aDownload) {
         do_check_eq(aDownload, downloadOne);
         receivedOnDownloadChanged = true;
       },
     });
     yield downloadOne.start();
     do_check_true(receivedOnDownloadChanged);
 
     // We should not receive change notifications after a download is removed.
     receivedOnDownloadChanged = false;
-    list.remove(downloadTwo);
+    yield list.remove(downloadTwo);
     yield downloadTwo.start();
     do_check_false(receivedOnDownloadChanged);
   }
 });
 
 /**
  * Checks that the reference to "this" is correct in the view callbacks.
  */
@@ -298,22 +298,22 @@ add_task(function test_notifications_thi
         receivedOnDownloadChanged = true;
       }
     },
     onDownloadRemoved: function () {
       do_check_eq(this, view);
       receivedOnDownloadRemoved = true;
     },
   };
-  list.addView(view);
+  yield list.addView(view);
 
   let download = yield promiseNewDownload();
-  list.add(download);
+  yield list.add(download);
   yield download.start();
-  list.remove(download);
+  yield list.remove(download);
 
   // Verify that we executed the checks.
   do_check_true(receivedOnDownloadAdded);
   do_check_true(receivedOnDownloadChanged);
   do_check_true(receivedOnDownloadRemoved);
 });
 
 /**
@@ -339,32 +339,32 @@ add_task(function test_history_expiratio
   let removeNotifications = 0;
   let downloadView = {
     onDownloadRemoved: function (aDownload) {
       if (++removeNotifications == 2) {
         deferred.resolve();
       }
     },
   };
-  list.addView(downloadView);
+  yield list.addView(downloadView);
 
   // Work with one finished download and one canceled download.
   yield downloadOne.start();
   downloadTwo.start();
   yield downloadTwo.cancel();
 
   // We must replace the visits added while executing the downloads with visits
   // that are older than 7 days, otherwise they will not be expired.
   yield promiseClearHistory();
   yield promiseExpirableDownloadVisit();
   yield promiseExpirableDownloadVisit(httpUrl("interruptible.txt"));
 
   // After clearing history, we can add the downloads to be removed to the list.
-  list.add(downloadOne);
-  list.add(downloadTwo);
+  yield list.add(downloadOne);
+  yield list.add(downloadTwo);
 
   // Force a history expiration.
   let expire = Cc["@mozilla.org/places/expiration;1"]
                  .getService(Ci.nsIObserver);
   expire.observe(null, "places-debug-start-expiration", -1);
 
   // Wait for both downloads to be removed.
   yield deferred.promise;
@@ -375,29 +375,29 @@ add_task(function test_history_expiratio
 /**
  * Checks all downloads are removed after clearing history.
  */
 add_task(function test_history_clear()
 {
   let list = yield promiseNewList();
   let downloadOne = yield promiseNewDownload();
   let downloadTwo = yield promiseNewDownload();
-  list.add(downloadOne);
-  list.add(downloadTwo);
+  yield list.add(downloadOne);
+  yield list.add(downloadTwo);
 
   let deferred = Promise.defer();
   let removeNotifications = 0;
   let downloadView = {
     onDownloadRemoved: function (aDownload) {
       if (++removeNotifications == 2) {
         deferred.resolve();
       }
     },
   };
-  list.addView(downloadView);
+  yield list.addView(downloadView);
 
   yield downloadOne.start();
   yield downloadTwo.start();
 
   yield promiseClearHistory();
 
   // Wait for the removal notifications that may still be pending.
   yield deferred.promise;
@@ -409,42 +409,165 @@ add_task(function test_history_clear()
  */
 add_task(function test_removeFinished()
 {
   let list = yield promiseNewList();
   let downloadOne = yield promiseNewDownload();
   let downloadTwo = yield promiseNewDownload();
   let downloadThree = yield promiseNewDownload();
   let downloadFour = yield promiseNewDownload();
-  list.add(downloadOne);
-  list.add(downloadTwo);
-  list.add(downloadThree);
-  list.add(downloadFour);
+  yield list.add(downloadOne);
+  yield list.add(downloadTwo);
+  yield list.add(downloadThree);
+  yield list.add(downloadFour);
 
   let deferred = Promise.defer();
   let removeNotifications = 0;
   let downloadView = {
     onDownloadRemoved: function (aDownload) {
       do_check_true(aDownload == downloadOne ||
                     aDownload == downloadTwo ||
                     aDownload == downloadThree);
       do_check_true(removeNotifications < 3);
       if (++removeNotifications == 3) {
         deferred.resolve();
       }
     },
   };
-  list.addView(downloadView);
+  yield list.addView(downloadView);
 
   // Start three of the downloads, but don't start downloadTwo, then set
   // downloadFour to have partial data. All downloads except downloadFour
   // should be removed.
   yield downloadOne.start();
   yield downloadThree.start();
   yield downloadFour.start();
   downloadFour.hasPartialData = true;
 
   list.removeFinished();
   yield deferred.promise;
 
   let downloads = yield list.getAll()
   do_check_eq(downloads.length, 1);
 });
+
+/**
+ * Tests the global DownloadSummary objects for the public, private, and
+ * combined download lists.
+ */
+add_task(function test_DownloadSummary()
+{
+  mustInterruptResponses();
+
+  let publicList = yield promiseNewList();
+  let privateList = yield Downloads.getList(Downloads.PRIVATE);
+
+  let publicSummary = yield Downloads.getSummary(Downloads.PUBLIC);
+  let privateSummary = yield Downloads.getSummary(Downloads.PRIVATE);
+  let combinedSummary = yield Downloads.getSummary(Downloads.ALL);
+
+  // Add a public download that has succeeded.
+  let succeededPublicDownload = yield promiseNewDownload();
+  yield succeededPublicDownload.start();
+  yield publicList.add(succeededPublicDownload);
+
+  // Add a public download that has been canceled midway.
+  let canceledPublicDownload =
+      yield promiseNewDownload(httpUrl("interruptible.txt"));
+  canceledPublicDownload.start();
+  yield promiseDownloadMidway(canceledPublicDownload);
+  yield canceledPublicDownload.cancel();
+  yield publicList.add(canceledPublicDownload);
+
+  // Add a public download that is in progress.
+  let inProgressPublicDownload =
+      yield promiseNewDownload(httpUrl("interruptible.txt"));
+  inProgressPublicDownload.start();
+  yield promiseDownloadMidway(inProgressPublicDownload);
+  yield publicList.add(inProgressPublicDownload);
+
+  // Add a private download that is in progress.
+  let inProgressPrivateDownload = yield Downloads.createDownload({
+    source: { url: httpUrl("interruptible.txt"), isPrivate: true },
+    target: getTempFile(TEST_TARGET_FILE_NAME).path,
+  });
+  inProgressPrivateDownload.start();
+  yield promiseDownloadMidway(inProgressPrivateDownload);
+  yield privateList.add(inProgressPrivateDownload);
+
+  // Verify that the summary includes the total number of bytes and the
+  // currently transferred bytes only for the downloads that are not stopped.
+  // For simplicity, we assume that after a download is added to the list, its
+  // current state is immediately propagated to the summary object, which is
+  // true in the current implementation, though it is not guaranteed as all the
+  // download operations may happen asynchronously.
+  do_check_false(publicSummary.allHaveStopped);
+  do_check_eq(publicSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
+  do_check_eq(publicSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
+
+  do_check_false(privateSummary.allHaveStopped);
+  do_check_eq(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
+  do_check_eq(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
+
+  do_check_false(combinedSummary.allHaveStopped);
+  do_check_eq(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 4);
+  do_check_eq(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length * 2);
+
+  yield inProgressPublicDownload.cancel();
+
+  // Stopping the download should have excluded it from the summary.
+  do_check_true(publicSummary.allHaveStopped);
+  do_check_eq(publicSummary.progressTotalBytes, 0);
+  do_check_eq(publicSummary.progressCurrentBytes, 0);
+
+  do_check_false(privateSummary.allHaveStopped);
+  do_check_eq(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
+  do_check_eq(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
+
+  do_check_false(combinedSummary.allHaveStopped);
+  do_check_eq(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
+  do_check_eq(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
+
+  yield inProgressPrivateDownload.cancel();
+
+  // All the downloads should be stopped now.
+  do_check_true(publicSummary.allHaveStopped);
+  do_check_eq(publicSummary.progressTotalBytes, 0);
+  do_check_eq(publicSummary.progressCurrentBytes, 0);
+
+  do_check_true(privateSummary.allHaveStopped);
+  do_check_eq(privateSummary.progressTotalBytes, 0);
+  do_check_eq(privateSummary.progressCurrentBytes, 0);
+
+  do_check_true(combinedSummary.allHaveStopped);
+  do_check_eq(combinedSummary.progressTotalBytes, 0);
+  do_check_eq(combinedSummary.progressCurrentBytes, 0);
+});
+
+/**
+ * Checks that views receive the summary change notification.  This is tested on
+ * the combined summary when adding a public download, as we assume that if we
+ * pass the test in this case we will also pass it in the others.
+ */
+add_task(function test_DownloadSummary_notifications()
+{
+  let list = yield promiseNewList();
+  let summary = yield Downloads.getSummary(Downloads.ALL);
+
+  let download = yield promiseNewDownload();
+  yield list.add(download);
+
+  // Check that we receive change notifications.
+  let receivedOnSummaryChanged = false;
+  yield summary.addView({
+    onSummaryChanged: function () {
+      receivedOnSummaryChanged = true;
+    },
+  });
+  yield download.start();
+  do_check_true(receivedOnSummaryChanged);
+});
+
+////////////////////////////////////////////////////////////////////////////////
+//// Termination
+
+let tailFile = do_get_file("tail.js");
+Services.scriptloader.loadSubScript(NetUtil.newURI(tailFile).spec);
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
@@ -285,8 +285,14 @@ add_task(function test_save_reload_unkno
               "download3target2");
 
   do_check_eq(Object.keys(itemsForLoad[2].saver._unknownProperties).length, 2);
   do_check_eq(itemsForLoad[2].saver._unknownProperties.saver1,
               "download3saver1");
   do_check_eq(itemsForLoad[2].saver._unknownProperties.saver2,
               "download3saver2");
 });
+
+////////////////////////////////////////////////////////////////////////////////
+//// Termination
+
+let tailFile = do_get_file("tail.js");
+Services.scriptloader.loadSubScript(NetUtil.newURI(tailFile).spec);
--- a/toolkit/components/jsdownloads/test/unit/test_Downloads.js
+++ b/toolkit/components/jsdownloads/test/unit/test_Downloads.js
@@ -56,50 +56,47 @@ add_task(function test_createDownload_pu
     source: { url: "about:blank" },
     target: { path: tempPath },
     saver: { type: "copy" }
   });
   do_check_false(download.source.isPrivate);
 });
 
 /**
- * Tests simpleDownload with nsIURI and nsIFile as arguments.
+ * Tests "fetch" with nsIURI and nsIFile as arguments.
  */
-add_task(function test_simpleDownload_uri_file_arguments()
+add_task(function test_fetch_uri_file_arguments()
 {
   let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
-  yield Downloads.simpleDownload(NetUtil.newURI(httpUrl("source.txt")),
-                                 targetFile);
+  yield Downloads.fetch(NetUtil.newURI(httpUrl("source.txt")), targetFile);
   yield promiseVerifyContents(targetFile.path, TEST_DATA_SHORT);
 });
 
 /**
- * Tests simpleDownload with DownloadSource and DownloadTarget as arguments.
+ * Tests "fetch" with DownloadSource and DownloadTarget as arguments.
  */
-add_task(function test_simpleDownload_object_arguments()
+add_task(function test_fetch_object_arguments()
 {
   let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
-  yield Downloads.simpleDownload({ url: httpUrl("source.txt") },
-                                 { path: targetPath });
+  yield Downloads.fetch({ url: httpUrl("source.txt") }, { path: targetPath });
   yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
 });
 
 /**
- * Tests simpleDownload with string arguments.
+ * Tests "fetch" with string arguments.
  */
-add_task(function test_simpleDownload_string_arguments()
+add_task(function test_fetch_string_arguments()
 {
   let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
-  yield Downloads.simpleDownload(httpUrl("source.txt"),
-                                 targetPath);
+  yield Downloads.fetch(httpUrl("source.txt"), targetPath);
   yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
 
   targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
-  yield Downloads.simpleDownload(new String(httpUrl("source.txt")),
-                                 new String(targetPath));
+  yield Downloads.fetch(new String(httpUrl("source.txt")),
+                        new String(targetPath));
   yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
 });
 
 /**
  * Tests that the getList function returns the same list when called multiple
  * times with the same argument, but returns different lists when called with
  * different arguments.  More detailed tests are implemented separately for the
  * DownloadList module.
@@ -114,16 +111,36 @@ add_task(function test_getList()
 
   do_check_eq(publicListOne, publicListTwo);
   do_check_eq(privateListOne, privateListTwo);
 
   do_check_neq(publicListOne, privateListOne);
 });
 
 /**
+ * Tests that the getSummary function returns the same summary when called
+ * multiple times with the same argument, but returns different summaries when
+ * called with different arguments.  More detailed tests are implemented
+ * separately for the DownloadSummary object in the DownloadList module.
+ */
+add_task(function test_getSummary()
+{
+  let publicSummaryOne = yield Downloads.getSummary(Downloads.PUBLIC);
+  let privateSummaryOne = yield Downloads.getSummary(Downloads.PRIVATE);
+
+  let publicSummaryTwo = yield Downloads.getSummary(Downloads.PUBLIC);
+  let privateSummaryTwo = yield Downloads.getSummary(Downloads.PRIVATE);
+
+  do_check_eq(publicSummaryOne, publicSummaryTwo);
+  do_check_eq(privateSummaryOne, privateSummaryTwo);
+
+  do_check_neq(publicSummaryOne, privateSummaryOne);
+});
+
+/**
  * Tests that the getSystemDownloadsDirectory returns a valid nsFile
  * download directory object.
  */
 add_task(function test_getSystemDownloadsDirectory()
 {
   let downloadDir = yield Downloads.getSystemDownloadsDirectory();
   do_check_true(downloadDir instanceof Ci.nsIFile);
 });
@@ -142,8 +159,14 @@ add_task(function test_getUserDownloadsD
  * Tests that the getTemporaryDownloadsDirectory returns a valid nsFile
  * download directory object.
  */
 add_task(function test_getTemporaryDownloadsDirectory()
 {
   let downloadDir = yield Downloads.getTemporaryDownloadsDirectory();
   do_check_true(downloadDir instanceof Ci.nsIFile);
 });
+
+////////////////////////////////////////////////////////////////////////////////
+//// Termination
+
+let tailFile = do_get_file("tail.js");
+Services.scriptloader.loadSubScript(NetUtil.newURI(tailFile).spec);
--- a/toolkit/components/jsdownloads/test/unit/xpcshell.ini
+++ b/toolkit/components/jsdownloads/test/unit/xpcshell.ini
@@ -1,10 +1,10 @@
 [DEFAULT]
 head = head.js
-tail = tail.js
+tail =
 
 [test_DownloadCore.js]
 [test_DownloadIntegration.js]
 [test_DownloadLegacy.js]
 [test_DownloadList.js]
 [test_Downloads.js]
 [test_DownloadStore.js]
--- a/toolkit/components/osfile/tests/mochi/main_test_osfile_async.js
+++ b/toolkit/components/osfile/tests/mochi/main_test_osfile_async.js
@@ -788,29 +788,30 @@ let test_system_shutdown = maketest("sys
             test.info("Got message: " + message);
             if (message.indexOf("TEST OS Controller WARNING") < 0) {
               return;
             }
             test.info("Got message: " + message + ", looking for resource " + resource);
             if (message.indexOf(resource) < 0) {
               return;
             }
+            test.info("Resource: " + resource + " found");
             setTimeout(deferred.resolve);
           } catch (ex) {
             setTimeout(function() {
               deferred.reject(ex);
             });
           }
         }};
         Services.console.registerListener(observer);
         Services.obs.notifyObservers(null, topic, null);
         setTimeout(function() {
           test.info("Timeout while waiting for resource: " + resource);
           deferred.reject("timeout");
-        }, 300);
+        }, 1000);
 
         let resolved = false;
         try {
           yield deferred.promise;
           resolved = true;
         } catch (ex) {
           if (ex != "timeout") {
             test.ok(false, "Error during 'test.osfile.web-workers-shutdown'" + ex);
--- a/toolkit/components/places/tests/unit/test_history.js
+++ b/toolkit/components/places/tests/unit/test_history.js
@@ -145,18 +145,18 @@ add_task(function test_execute()
   do_check_eq(result.root.getChild(0).uri, "http://google.com/");
   result.root.containerOpen = false;
 
   // By default history is enabled.
   do_check_true(!histsvc.historyDisabled);
 
   // test getPageTitle
   yield promiseAddVisits({ uri: uri("http://example.com"), title: "title" });
-  var title = histsvc.getPageTitle(uri("http://example.com"));
-  do_check_eq(title, "title");
+  let placeInfo = yield PlacesUtils.promisePlaceInfo(uri("http://example.com"));
+  do_check_eq(placeInfo.title, "title");
 
   // query for the visit
   do_check_true(uri_in_db(testURI));
 
   // test for schema changes in bug 373239
   // get direct db connection
   var db = histsvc.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
   var q = "SELECT id FROM moz_bookmarks";
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -918,25 +918,25 @@ var WalkerActor = protocol.ActorClass({
     }
     let nodes = this.rootDoc.querySelectorAll(HIGHLIGHTED_PSEUDO_CLASS);
     for (let node of nodes) {
       DOMUtils.removePseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
     }
   },
 
   highlight: method(function(node) {
-    this._installHelperSheet(node);
     this._unhighlight();
 
     if (!node ||
         !node.rawNode ||
          node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
       return;
     }
 
+    this._installHelperSheet(node);
     this.layoutHelpers.scrollIntoViewIfNeeded(node.rawNode);
     DOMUtils.addPseudoClassLock(node.rawNode, HIGHLIGHTED_PSEUDO_CLASS);
     this._highlightTimeout = setTimeout(this._unhighlight.bind(this), HIGHLIGHTED_TIMEOUT);
 
   }, { request: { node: Arg(0, "nullable:domnode") }}),
 
   /**
    * Watch the given document node for mutations using the DOM observer
--- a/toolkit/devtools/styleinspector/css-logic.js
+++ b/toolkit/devtools/styleinspector/css-logic.js
@@ -46,16 +46,18 @@ const RX_PSEUDO_CLASS_OR_ELT = /(:[\w-]+
 const RX_CONNECTORS = /\s*[\s>+~]\s*/g;
 const RX_ID = /\s*#\w+\s*/g;
 const RX_CLASS_OR_ATTRIBUTE = /\s*(?:\.\w+|\[.+?\])\s*/g;
 const RX_PSEUDO = /\s*:?:([\w-]+)(\(?\)?)\s*/g;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+let {colorUtils} = require("devtools/shared/css-color");
+
 function CssLogic()
 {
   // The cache of examined CSS properties.
   _propertyInfos: {};
 }
 
 exports.CssLogic = CssLogic;
 
@@ -1447,17 +1449,16 @@ CssPropertyInfo.prototype = {
       try {
         this._value = this._cssLogic._computedStyle.getPropertyValue(this.property);
       } catch (ex) {
         Services.console.logStringMessage('Error reading computed style for ' +
           this.property);
         Services.console.logStringMessage(ex);
       }
     }
-
     return this._value;
   },
 
   /**
    * Retrieve the number of matched rules holding the this.property style
    * property. Only rules that come from allowed stylesheets are counted.
    *
    * @return {number} the number of matched rules.
@@ -1596,19 +1597,18 @@ CssPropertyInfo.prototype = {
  * @param {string} aValue The property value from the CssRule that owns the selector.
  * @param {CssLogic.STATUS} aStatus The selector match status.
  * @constructor
  */
 function CssSelectorInfo(aSelector, aProperty, aValue, aStatus)
 {
   this.selector = aSelector;
   this.property = aProperty;
-  this.value = aValue;
   this.status = aStatus;
-
+  this.value = colorUtils.processCSSString(aValue);
   let priority = this.selector.cssRule.getPropertyPriority(this.property);
   this.important = (priority === "important");
 }
 
 CssSelectorInfo.prototype = {
   /**
    * Retrieve the CssSelector source, which is the source of the CssSheet owning
    * the selector.
--- a/toolkit/webapps/WebappOSUtils.jsm
+++ b/toolkit/webapps/WebappOSUtils.jsm
@@ -129,38 +129,47 @@ this.WebappOSUtils = {
       }
     }
 
     return exeFile;
 #endif
   },
 
   getInstallPath: function(aApp) {
-#ifdef XP_WIN
-    let execFile = this.getLaunchTarget(aApp);
-    if (!execFile) {
+    if (Services.appinfo.ID == "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}") {
+      // B2G
+      return aApp.basePath + "/" + aApp.id;
+    } else if (Services.appinfo.ID == "{aa3c5121-dab2-40e2-81ca-7ea25febc110}") {
+      // Android
+      return aApp.basePath + "/" + aApp.id;
+    } else if (Services.appinfo.ID == "{99bceaaa-e3c6-48c1-b981-ef9b46b67d60}") {
+      // Metro
       return null;
-    }
+    } else {
+      // Firefox, the Webapp Runtime and other Desktop products share the same
+      // directory naming scheme.
+#ifdef XP_WIN
+      let execFile = this.getLaunchTarget(aApp);
+      if (!execFile) {
+        return null;
+      }
 
-    return execFile.parent.path;
+      return execFile.parent.path;
 #elifdef XP_MACOSX
-    let [ bundleID, path ] = this.getLaunchTarget(aApp);
-    return path;
-#elifdef MOZ_B2G
-    return aApp.basePath + "/" + aApp.id;
-#elifdef MOZ_FENNEC
-    return aApp.basePath + "/" + aApp.id;
+      let [ bundleID, path ] = this.getLaunchTarget(aApp);
+      return path;
 #elifdef XP_UNIX
-    let execFile = this.getLaunchTarget(aApp);
-    if (!execFile) {
-      return null;
+      let execFile = this.getLaunchTarget(aApp);
+      if (!execFile) {
+        return null;
+      }
+
+      return execFile.parent.path;
+#endif
     }
-
-    return execFile.parent.path;
-#endif
   },
 
   launch: function(aApp) {
     let uniqueName = this.getUniqueName(aApp);
 
 #ifdef XP_WIN
     let initProcess = CC("@mozilla.org/process/util;1",
                          "nsIProcess", "init");
--- a/tools/mercurial/hgsetup/config.py
+++ b/tools/mercurial/hgsetup/config.py
@@ -1,28 +1,42 @@
 # 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/.
 
 from __future__ import unicode_literals
 
 from configobj import ConfigObj
 import re
+import os
 
 
 BUGZILLA_FINGERPRINT = '45:77:35:fd:6f:2c:1c:c2:90:4b:f7:b4:4d:60:c6:97:c5:5c:47:27'
 HG_FINGERPRINT = '10:78:e8:57:2d:95:de:7c:de:90:bd:22:e1:38:17:67:c5:a7:9c:14'
 
 
 class MercurialConfig(object):
     """Interface for manipulating a Mercurial config file."""
 
-    def __init__(self, infile=None):
+    def __init__(self, infiles=None):
         """Create a new instance, optionally from an existing hgrc file."""
 
+        if infiles:
+            # If multiple files were specified, figure out which file we're using:
+            if len(infiles) > 1:
+                picky_infiles = filter(os.path.isfile, infiles)
+                if picky_infiles:
+                    picky_infiles = [(os.path.getsize(path), path) for path in picky_infiles]
+                    infiles = [max(picky_infiles)[1]]
+
+            infile = infiles[0]
+            self.config_path = infile
+        else:
+            infile = None
+
         # write_empty_values is necessary to prevent built-in extensions (which
         # have no value) from being dropped on write.
         # list_values aren't needed by Mercurial and disabling them prevents
         # quotes from being added.
         self._c = ConfigObj(infile=infile, encoding='utf-8',
             write_empty_values=True, list_values=False)
 
     @property
--- a/tools/mercurial/hgsetup/wizard.py
+++ b/tools/mercurial/hgsetup/wizard.py
@@ -85,32 +85,32 @@ should be up to date!
 
 class MercurialSetupWizard(object):
     """Command-line wizard to help users configure Mercurial."""
 
     def __init__(self, state_dir):
         self.state_dir = state_dir
         self.ext_dir = os.path.join(state_dir, 'mercurial', 'extensions')
 
-    def run(self, config_path):
+    def run(self, config_paths):
         try:
             os.makedirs(self.ext_dir)
         except OSError as e:
             if e.errno != errno.EEXIST:
                 raise
 
         try:
             hg = which.which('hg')
         except which.WhichError as e:
             print(e)
             print('Try running |mach bootstrap| to ensure your environment is '
                 'up to date.')
             return 1
 
-        c = MercurialConfig(config_path)
+        c = MercurialConfig(config_paths)
 
         print(INITIAL_MESSAGE)
         raw_input()
 
         if not c.have_valid_username():
             print(MISSING_USERNAME)
             print('')
 
@@ -220,16 +220,17 @@ class MercurialSetupWizard(object):
 
         c.add_mozilla_host_fingerprints()
 
         b = StringIO()
         c.write(b)
         new_lines = [line.rstrip() for line in b.getvalue().splitlines()]
         old_lines = []
 
+        config_path = c.config_path
         if os.path.exists(config_path):
             with open(config_path, 'rt') as fh:
                 old_lines = [line.rstrip() for line in fh.readlines()]
 
         diff = list(difflib.unified_diff(old_lines, new_lines,
             'hgrc.old', 'hgrc.new'))
 
         if len(diff):
--- a/tools/mercurial/mach_commands.py
+++ b/tools/mercurial/mach_commands.py
@@ -21,17 +21,20 @@ class VersionControlCommands(object):
     @Command('mercurial-setup', category='devenv',
         description='Help configure Mercurial for optimal development.')
     def mercurial_bootstrap(self):
         sys.path.append(os.path.dirname(__file__))
 
         from hgsetup.wizard import MercurialSetupWizard
 
         wizard = MercurialSetupWizard(self._context.state_dir)
-        result = wizard.run(os.path.expanduser('~/.hgrc'))
+        config_paths = ['~/.hgrc']
+        if sys.platform in ('win32', 'cygwin'):
+          config_paths.insert(0, '~/mercurial.ini')
+        result = wizard.run(map(os.path.expanduser, config_paths))
 
         # Touch a file so we can periodically prompt to update extensions.
         state_path = os.path.join(self._context.state_dir,
             'mercurial/setup.lastcheck')
         with open(state_path, 'a'):
             os.utime(state_path, None)
 
         return result
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -1807,73 +1807,65 @@ nsExternalAppHandler::OnTargetChange(nsI
 {
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver *aSaver,
                                      nsresult aStatus)
 {
-  if (mCanceled)
-    return NS_OK;
-
-  // Save the hash
-  nsresult rv = mSaver->GetSha256Hash(mHash);
-  // Free the reference that the saver keeps on us, even if we couldn't get the
-  // hash.
-  mSaver = nullptr;
-
-  if (NS_FAILED(aStatus)) {
-    nsAutoString path;
-    mTempFile->GetPath(path);
-    SendStatusChange(kWriteError, aStatus, nullptr, path);
-    if (!mCanceled)
-      Cancel(aStatus);
-    return NS_OK;
+  if (!mCanceled) {
+    // Save the hash
+    (void)mSaver->GetSha256Hash(mHash);
+    // Free the reference that the saver keeps on us, even if we couldn't get
+    // the hash.
+    mSaver = nullptr;
+  
+    if (NS_FAILED(aStatus)) {
+      nsAutoString path;
+      mTempFile->GetPath(path);
+      SendStatusChange(kWriteError, aStatus, nullptr, path);
+      if (!mCanceled)
+        Cancel(aStatus);
+      return NS_OK;
+    }
   }
 
   // Notify the transfer object that we are done if the user has chosen an
   // action. If the user hasn't chosen an action, the progress listener
   // (nsITransfer) will be notified in CreateTransfer.
   if (mTransfer) {
-    rv = NotifyTransfer();
-    NS_ENSURE_SUCCESS(rv, rv);
+    NotifyTransfer(aStatus);
   }
 
   return NS_OK;
 }
 
-nsresult nsExternalAppHandler::NotifyTransfer()
+void nsExternalAppHandler::NotifyTransfer(nsresult aStatus)
 {
   MOZ_ASSERT(NS_IsMainThread(), "Must notify on main thread");
-  MOZ_ASSERT(!mCanceled, "Can't notify if canceled or action "
-             "hasn't been chosen");
   MOZ_ASSERT(mTransfer, "We must have an nsITransfer");
 
   LOG(("Notifying progress listener"));
 
-  nsresult rv = mTransfer->SetSha256Hash(mHash);
-  NS_ENSURE_SUCCESS(rv, rv);
+  if (NS_SUCCEEDED(aStatus)) {
+    (void)mTransfer->SetSha256Hash(mHash);
+    (void)mTransfer->OnProgressChange64(nullptr, nullptr, mProgress,
+      mContentLength, mProgress, mContentLength);
+  }
 
-  rv = mTransfer->OnProgressChange64(nullptr, nullptr, mProgress,
-    mContentLength, mProgress, mContentLength);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  rv = mTransfer->OnStateChange(nullptr, nullptr,
+  (void)mTransfer->OnStateChange(nullptr, nullptr,
     nsIWebProgressListener::STATE_STOP |
     nsIWebProgressListener::STATE_IS_REQUEST |
-    nsIWebProgressListener::STATE_IS_NETWORK, NS_OK);
-  NS_ENSURE_SUCCESS(rv, rv);
+    nsIWebProgressListener::STATE_IS_NETWORK, aStatus);
 
   // This nsITransfer object holds a reference to us (we are its observer), so
   // we need to release the reference to break a reference cycle (and therefore
-  // to prevent leaking)
+  // to prevent leaking).  We do this even if the previous calls failed.
   mTransfer = nullptr;
-
-  return NS_OK;
 }
 
 NS_IMETHODIMP nsExternalAppHandler::GetMIMEInfo(nsIMIMEInfo ** aMIMEInfo)
 {
   *aMIMEInfo = mMimeInfo;
   NS_ADDREF(*aMIMEInfo);
   return NS_OK;
 }
@@ -1961,17 +1953,17 @@ nsresult nsExternalAppHandler::CreateTra
   // Finally, save the transfer to mTransfer.
   mTransfer = transfer;
   transfer = nullptr;
 
   // While we were bringing up the progress dialog, we actually finished
   // processing the url. If that's the case then mStopRequestIssued will be
   // true and OnSaveComplete has been called.
   if (mStopRequestIssued && !mSaver && mTransfer) {
-    return NotifyTransfer();
+    NotifyTransfer(NS_OK);
   }
 
   return rv;
 }
 
 nsresult nsExternalAppHandler::SaveDestinationAvailable(nsIFile * aFile)
 {
   if (aFile)
@@ -2198,39 +2190,52 @@ NS_IMETHODIMP nsExternalAppHandler::Laun
     Cancel(rv);
   }
   return rv;
 }
 
 NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason)
 {
   NS_ENSURE_ARG(NS_FAILED(aReason));
-  // XXX should not ignore the reason
 
+  if (mCanceled) {
+    return NS_OK;
+  }
   mCanceled = true;
+
   if (mSaver) {
+    // We are still writing to the target file.  Give the saver a chance to
+    // close the target file, then notify the transfer object if necessary in
+    // the OnSaveComplete callback.
     mSaver->Finish(aReason);
     mSaver = nullptr;
-  } else if (mStopRequestIssued && mTempFile) {
-    // This branch can only happen when the user cancels the helper app dialog
-    // when the request has completed. The temp file has to be removed here,
-    // because mSaver has been released at that time with the temp file left.
-    (void)mTempFile->Remove(false);
+  } else {
+    if (mStopRequestIssued && mTempFile) {
+      // This branch can only happen when the user cancels the helper app dialog
+      // when the request has completed. The temp file has to be removed here,
+      // because mSaver has been released at that time with the temp file left.
+      (void)mTempFile->Remove(false);
+    }
+
+    // Notify the transfer object that the download has been canceled, if the
+    // user has already chosen an action and we didn't notify already.
+    if (mTransfer) {
+      NotifyTransfer(aReason);
+    }
   }
 
   // Break our reference cycle with the helper app dialog (set up in
   // OnStartRequest)
   mDialog = nullptr;
 
   mRequest = nullptr;
 
   // Release the listener, to break the reference cycle with it (we are the
   // observer of the listener).
   mDialogProgressListener = nullptr;
-  mTransfer = nullptr;
 
   return NS_OK;
 }
 
 void nsExternalAppHandler::ProcessAnyRefreshTags()
 {
    // one last thing, try to see if the original window context supports a refresh interface...
    // Sometimes, when you download content that requires an external handler, there is
--- a/uriloader/exthandler/nsExternalHelperAppService.h
+++ b/uriloader/exthandler/nsExternalHelperAppService.h
@@ -368,19 +368,24 @@ protected:
    * channel had a refresh url associated with it (which might point to a
    * "thank you for downloading" kind of page, then process that....It is safe
    * to invoke this method multiple times. We'll clear mOriginalChannel after
    * it's called and this ensures we won't call it again....
    */
   void ProcessAnyRefreshTags();
 
   /**
-   * Notify our nsITransfer object that we are done with the download.
+   * Notify our nsITransfer object that we are done with the download.  This is
+   * always called after the target file has been closed.
+   *
+   * @param aStatus
+   *        NS_OK for success, or a failure code if the download failed.
+   *        A partially downloaded file may still be available in this case.
    */
-  nsresult NotifyTransfer();
+  void NotifyTransfer(nsresult aStatus);
 
   /**
    * Helper routine that searches a pref string for a given mime type
    */
   bool GetNeverAskFlagFromPref(const char * prefName, const char * aContentType);
 
   /**
    * Helper routine to ensure mSuggestedFileName is "correct";