merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Mon, 25 Jul 2016 15:49:05 +0200
changeset 348440 e730a1dca1a41bd6b0824353305abc25284e2d52
parent 348395 f44bb9de08ade45299223de89953c6d0f4d003d1 (current diff)
parent 348439 1bb05a19ff9d70ed1d858d526fdbe27a8408f19f (diff)
child 348577 e23f2ec25e96ad6cdf64af446e81a0f0dfeb4b5c
push id1230
push userjlund@mozilla.com
push dateMon, 31 Oct 2016 18:13:35 +0000
treeherdermozilla-release@5e06e3766db2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
browser/base/content/browser.js
devtools/client/eyedropper/commands.js
devtools/client/eyedropper/crosshairs.css
devtools/client/eyedropper/eyedropper-child.js
devtools/client/eyedropper/eyedropper.js
devtools/client/eyedropper/eyedropper.xul
devtools/client/eyedropper/moz.build
devtools/client/eyedropper/nocursor.css
devtools/client/eyedropper/test/.eslintrc
devtools/client/eyedropper/test/browser.ini
devtools/client/eyedropper/test/browser_eyedropper_basic.js
devtools/client/eyedropper/test/browser_eyedropper_cmd.js
devtools/client/eyedropper/test/color-block.html
devtools/client/eyedropper/test/head.js
devtools/client/shared/components/reps/named-node-map.js
devtools/client/shared/css-color-db.js
devtools/client/shared/css-color.js
devtools/client/shared/output-parser.js
devtools/client/shared/test/browser_telemetry_button_eyedropper.js
devtools/shared/css-color.js
mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderClient.java
toolkit/components/extensions/Extension.jsm
--- a/.eslintignore
+++ b/.eslintignore
@@ -108,16 +108,18 @@ devtools/client/webconsole/**
 !devtools/client/webconsole/jsterm.js
 !devtools/client/webconsole/console-commands.js
 devtools/client/webide/**
 !devtools/client/webide/components/webideCli.js
 devtools/server/**
 !devtools/server/child.js
 !devtools/server/css-logic.js
 !devtools/server/main.js
+!devtools/server/actors/inspector.js
+!devtools/server/actors/highlighters/eye-dropper.js
 !devtools/server/actors/webbrowser.js
 !devtools/server/actors/styles.js
 !devtools/server/actors/string.js
 !devtools/server/actors/csscoverage.js
 devtools/shared/*.js
 !devtools/shared/css-lexer.js
 !devtools/shared/defer.js
 !devtools/shared/event-emitter.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1458,10 +1458,10 @@ pref("dom.mozBrowserFramesEnabled", true
 pref("extensions.pocket.enabled", true);
 
 pref("signon.schemeUpgrades", true);
 
 // Enable the "Simplify Page" feature in Print Preview
 pref("print.use_simplify_page", true);
 
 // Space separated list of URLS that are allowed to send objects (instead of
-// only strings) through webchannels.
+// only strings) through webchannels. This list is duplicated in mobile/android/app/mobile.js
 pref("webchannel.allowObject.urlWhitelist", "https://accounts.firefox.com https://content.cdn.mozilla.net https://hello.firefox.com https://input.mozilla.org https://support.mozilla.org https://install.mozilla.org");
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -402,20 +402,22 @@ var gFxAccounts = {
     }
 
     const clients = this.remoteClients;
     for (let client of clients) {
       addTargetDevice(client.id, client.name);
     }
 
     // "All devices" menu item
-    const separator = document.createElement("menuseparator");
-    fragment.appendChild(separator);
-    const allDevicesLabel = this.strings.GetStringFromName("sendTabToAllDevices.menuitem");
-    addTargetDevice("", allDevicesLabel);
+    if (clients.length > 1) {
+      const separator = document.createElement("menuseparator");
+      fragment.appendChild(separator);
+      const allDevicesLabel = this.strings.GetStringFromName("sendTabToAllDevices.menuitem");
+      addTargetDevice("", allDevicesLabel);
+    }
 
     devicesPopup.appendChild(fragment);
   },
 
   updateTabContextMenu: function (aPopupMenu) {
     if (!this.sendTabToDeviceEnabled) {
       return;
     }
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7756,31 +7756,16 @@ var ResponsiveUI = {
 };
 
 XPCOMUtils.defineLazyGetter(ResponsiveUI, "ResponsiveUIManager", function() {
   let tmp = {};
   Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", tmp);
   return tmp.ResponsiveUIManager;
 });
 
-function openEyedropper() {
-  var eyedropper = new this.Eyedropper(this, { context: "menu",
-                                               copyOnSelect: true });
-  eyedropper.open();
-}
-
-Object.defineProperty(this, "Eyedropper", {
-  get: function() {
-    let devtools = Cu.import("resource://devtools/shared/Loader.jsm", {}).devtools;
-    return devtools.require("devtools/client/eyedropper/eyedropper").Eyedropper;
-  },
-  configurable: true,
-  enumerable: true
-});
-
 XPCOMUtils.defineLazyGetter(window, "gShowPageResizers", function () {
   // Only show resizers on Windows 2000 and XP
   return AppConstants.isPlatformAndVersionAtMost("win", "5.9");
 });
 
 var MousePosTracker = {
   _listeners: new Set(),
   _x: 0,
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -37,73 +37,75 @@ function getSender(context, target, send
     // The message came from an ExtensionContext. In that case, it should
     // include a tabId property (which is filled in by the page-open
     // listener below).
     sender.tab = TabManager.convert(context.extension, TabManager.getTab(sender.tabId));
     delete sender.tabId;
   }
 }
 
-// WeakMap[ExtensionContext -> {tab, parentWindow}]
-var pageDataMap = new WeakMap();
+function getDocShellOwner(docShell) {
+  let browser = docShell.chromeEventHandler;
+
+  let xulWindow = browser.ownerGlobal;
+
+  let {gBrowser} = xulWindow;
+  if (gBrowser) {
+    let tab = gBrowser.getTabForBrowser(browser);
+
+    return {xulWindow, tab};
+  }
+
+  return {};
+}
 
 /* eslint-disable mozilla/balanced-listeners */
 // This listener fires whenever an extension page opens in a tab
 // (either initiated by the extension or the user). Its job is to fill
 // in some tab-specific details and keep data around about the
 // ExtensionContext.
-extensions.on("page-load", (type, page, params, sender, delegate) => {
+extensions.on("page-load", (type, context, params, sender, delegate) => {
   if (params.type == "tab" || params.type == "popup") {
-    let browser = params.docShell.chromeEventHandler;
+    let {xulWindow, tab} = getDocShellOwner(params.docShell);
 
-    let parentWindow = browser.ownerGlobal;
-    page.windowId = WindowManager.getId(parentWindow);
-
-    let tab = parentWindow.gBrowser.getTabForBrowser(browser);
+    // FIXME: Handle tabs being moved between windows.
+    context.windowId = WindowManager.getId(xulWindow);
     if (tab) {
       sender.tabId = TabManager.getId(tab);
-      page.tabId = TabManager.getId(tab);
+      context.tabId = TabManager.getId(tab);
     }
-
-    pageDataMap.set(page, {tab, parentWindow});
   }
 
   delegate.getSender = getSender;
 });
 
-extensions.on("page-unload", (type, page) => {
-  pageDataMap.delete(page);
-});
-
-extensions.on("page-shutdown", (type, page) => {
-  if (pageDataMap.has(page)) {
-    let {tab, parentWindow} = pageDataMap.get(page);
-    pageDataMap.delete(page);
-
+extensions.on("page-shutdown", (type, context) => {
+  if (context.type == "tab") {
+    let {xulWindow, tab} = getDocShellOwner(context.docShell);
     if (tab) {
-      parentWindow.gBrowser.removeTab(tab);
+      xulWindow.gBrowser.removeTab(tab);
     }
   }
 });
 
 extensions.on("fill-browser-data", (type, browser, data, result) => {
   let tabId = TabManager.getBrowserId(browser);
   if (tabId == -1) {
     result.cancel = true;
     return;
   }
 
   data.tabId = tabId;
 });
 /* eslint-enable mozilla/balanced-listeners */
 
 global.currentWindow = function(context) {
-  let pageData = pageDataMap.get(context);
-  if (pageData) {
-    return pageData.parentWindow;
+  let {xulWindow} = getDocShellOwner(context.docShell);
+  if (xulWindow) {
+    return xulWindow;
   }
   return WindowManager.topWindow;
 };
 
 let tabListener = {
   init() {
     if (this.initialized) {
       return;
--- a/browser/components/extensions/test/browser/browser_ext_contentscript_connect.js
+++ b/browser/components/extensions/test/browser/browser_ext_contentscript_connect.js
@@ -38,17 +38,20 @@ add_task(function* () {
 
           port_messages_received++;
           browser.test.assertEq(2, port_messages_received, "2 port messages received");
 
           browser.test.notifyPass("contentscript_connect.pass");
         });
       });
 
-      browser.tabs.executeScript({file: "script.js"});
+      browser.tabs.executeScript({file: "script.js"}).catch(e => {
+        browser.test.fail(`Error: ${e} :: ${e.stack}`);
+        browser.test.notifyFail("contentscript_connect.pass");
+      });
     },
 
     files: {
       "script.js": function() {
         let port = browser.runtime.connect();
         port.postMessage("port message");
       },
     },
--- a/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js
@@ -252,8 +252,71 @@ add_task(function* test_options_no_manif
         browser.test.notifyFail("options-no-manifest");
       });
     },
   });
 
   yield extension.awaitFinish("options-no-manifest");
   yield extension.unload();
 });
+
+add_task(function* test_inline_options_uninstall() {
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+
+  let extension = yield loadExtension({
+    manifest: {
+      "options_ui": {
+        "page": "options.html",
+      },
+    },
+
+    background: function() {
+      let _optionsPromise;
+      let awaitOptions = () => {
+        browser.test.assertFalse(_optionsPromise, "Should not be awaiting options already");
+
+        return new Promise(resolve => {
+          _optionsPromise = {resolve};
+        });
+      };
+
+      browser.runtime.onMessage.addListener((msg, sender) => {
+        if (msg == "options.html") {
+          if (_optionsPromise) {
+            _optionsPromise.resolve(sender.tab);
+            _optionsPromise = null;
+          } else {
+            browser.test.fail("Saw unexpected options page load");
+          }
+        }
+      });
+
+      let firstTab;
+      browser.tabs.query({currentWindow: true, active: true}).then(tabs => {
+        firstTab = tabs[0].id;
+
+        browser.test.log("Open options page. Expect fresh load.");
+        return Promise.all([
+          browser.runtime.openOptionsPage(),
+          awaitOptions(),
+        ]);
+      }).then(([, tab]) => {
+        browser.test.assertEq("about:addons", tab.url, "Tab contains AddonManager");
+        browser.test.assertTrue(tab.active, "Tab is active");
+        browser.test.assertTrue(tab.id != firstTab, "Tab is a new tab");
+
+        browser.test.sendMessage("options-ui-open");
+      }).catch(error => {
+        browser.test.fail(`Error: ${error} :: ${error.stack}`);
+      });
+    },
+  });
+
+  yield extension.awaitMessage("options-ui-open");
+  yield extension.unload();
+
+  is(gBrowser.selectedBrowser.currentURI.spec, "about:addons",
+     "Add-on manager tab should still be open");
+
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+  yield BrowserTestUtils.removeTab(tab);
+});
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,3 +1,3 @@
 This is the pdf.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 1.5.337
+Current extension version is: 1.5.345
--- a/browser/extensions/pdfjs/content/PdfJs.jsm
+++ b/browser/extensions/pdfjs/content/PdfJs.jsm
@@ -272,17 +272,17 @@ var PdfJs = {
       let PdfjsChromeUtils = Components.utils.import(jsm, {}).PdfjsChromeUtils;
       PdfjsChromeUtils.notifyChildOfSettingsChange();
     }
   },
 
   /**
    * pdf.js is only enabled if it is both selected as the pdf viewer and if the
    * global switch enabling it is true.
-   * @return {boolean} Wether or not it's enabled.
+   * @return {boolean} Whether or not it's enabled.
    */
   get enabled() {
     var disabled = getBoolPref(PREF_DISABLED, true);
     if (disabled) {
       return false;
     }
 
     // Check if the 'application/pdf' preview handler is configured properly.
--- a/browser/extensions/pdfjs/content/PdfStreamConverter.jsm
+++ b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm
@@ -329,17 +329,17 @@ ChromeActions.prototype = {
     try {
       // Lazy initialization of localizedStrings
       if (!('localizedStrings' in this)) {
         this.localizedStrings = getLocalizedStrings('viewer.properties');
       }
       var result = this.localizedStrings[data];
       return JSON.stringify(result || null);
     } catch (e) {
-      log('Unable to retrive localized strings: ' + e);
+      log('Unable to retrieve localized strings: ' + e);
       return 'null';
     }
   },
   supportsIntegratedFind: function() {
     // Integrated find is only supported when we're not in a frame
     if (this.domWindow.frameElement !== null) {
       return false;
     }
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf', ['exports
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdf = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.337';
-var pdfjsBuild = '11381cd';
+var pdfjsVersion = '1.5.345';
+var pdfjsBuild = '10f9f11';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -1075,17 +1075,17 @@ function isSpace(ch) {
   return (ch === 0x20 || ch === 0x09 || ch === 0x0D || ch === 0x0A);
 }
 
 /**
  * Promise Capability object.
  *
  * @typedef {Object} PromiseCapability
  * @property {Promise} promise - A promise object.
- * @property {function} resolve - Fullfills the promise.
+ * @property {function} resolve - Fulfills the promise.
  * @property {function} reject - Rejects the promise.
  */
 
 /**
  * Creates a promise capability object.
  * @alias createPromiseCapability
  *
  * @return {PromiseCapability} A capability object contains:
@@ -1098,18 +1098,18 @@ function createPromiseCapability() {
     capability.reject = reject;
   });
   return capability;
 }
 
 /**
  * Polyfill for Promises:
  * The following promise implementation tries to generally implement the
- * Promise/A+ spec. Some notable differences from other promise libaries are:
- * - There currently isn't a seperate deferred and promise object.
+ * Promise/A+ spec. Some notable differences from other promise libraries are:
+ * - There currently isn't a separate deferred and promise object.
  * - Unhandled rejections eventually show an error if they aren't handled.
  *
  * Based off of the work in:
  * https://bugzilla.mozilla.org/show_bug.cgi?id=810490
  */
 (function PromiseClosure() {
   if (globalScope.Promise) {
     // Promises existing in the DOM/Worker, checking presence of all/resolve
@@ -3610,17 +3610,17 @@ var createMeshCanvas = (function createM
   function createMeshCanvas(bounds, combinesScale, coords, colors, figures,
                             backgroundColor, cachedCanvases) {
     // we will increase scale on some weird factor to let antialiasing take
     // care of "rough" edges
     var EXPECTED_SCALE = 1.1;
     // MAX_PATTERN_SIZE is used to avoid OOM situation.
     var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough
     // We need to keep transparent border around our pattern for fill():
-    // createPattern with 'no-repeat' will bleed edges accross entire area.
+    // createPattern with 'no-repeat' will bleed edges across entire area.
     var BORDER_SIZE = 2;
 
     var offsetX = Math.floor(bounds[0]);
     var offsetY = Math.floor(bounds[1]);
     var boundsWidth = Math.ceil(bounds[2]) - offsetX;
     var boundsHeight = Math.ceil(bounds[3]) - offsetY;
 
     var width = Math.min(Math.ceil(Math.abs(boundsWidth * combinesScale[0] *
@@ -5683,17 +5683,17 @@ var CanvasGraphics = (function CanvasGra
       this.baseTransform = this.baseTransformStack.pop();
     },
 
     beginGroup: function CanvasGraphics_beginGroup(group) {
       this.save();
       var currentCtx = this.ctx;
       // TODO non-isolated groups - according to Rik at adobe non-isolated
       // group results aren't usually that different and they even have tools
-      // that ignore this setting. Notes from Rik on implmenting:
+      // that ignore this setting. Notes from Rik on implementing:
       // - When you encounter an transparency group, create a new canvas with
       // the dimensions of the bbox
       // - copy the content from the previous canvas to the new canvas
       // - draw as usual
       // - remove the backdrop alpha:
       // alphaNew = 1 - (1 - alpha)/(1 - alphaBackdrop) with 'alpha' the alpha
       // value of your transparency group and 'alphaBackdrop' the alpha of the
       // backdrop
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf.worker', ['
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdfWorker = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.337';
-var pdfjsBuild = '11381cd';
+var pdfjsVersion = '1.5.345';
+var pdfjsBuild = '10f9f11';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -3096,17 +3096,17 @@ function isSpace(ch) {
   return (ch === 0x20 || ch === 0x09 || ch === 0x0D || ch === 0x0A);
 }
 
 /**
  * Promise Capability object.
  *
  * @typedef {Object} PromiseCapability
  * @property {Promise} promise - A promise object.
- * @property {function} resolve - Fullfills the promise.
+ * @property {function} resolve - Fulfills the promise.
  * @property {function} reject - Rejects the promise.
  */
 
 /**
  * Creates a promise capability object.
  * @alias createPromiseCapability
  *
  * @return {PromiseCapability} A capability object contains:
@@ -3119,18 +3119,18 @@ function createPromiseCapability() {
     capability.reject = reject;
   });
   return capability;
 }
 
 /**
  * Polyfill for Promises:
  * The following promise implementation tries to generally implement the
- * Promise/A+ spec. Some notable differences from other promise libaries are:
- * - There currently isn't a seperate deferred and promise object.
+ * Promise/A+ spec. Some notable differences from other promise libraries are:
+ * - There currently isn't a separate deferred and promise object.
  * - Unhandled rejections eventually show an error if they aren't handled.
  *
  * Based off of the work in:
  * https://bugzilla.mozilla.org/show_bug.cgi?id=810490
  */
 (function PromiseClosure() {
   if (globalScope.Promise) {
     // Promises existing in the DOM/Worker, checking presence of all/resolve
@@ -4293,17 +4293,17 @@ var CFFParser = (function CFFParserClosu
               var left = bytes[pos++];
               for (var j = start; j <= start + left; j++) {
                 encoding[j] = gid++;
               }
             }
             break;
 
           default:
-            error('Unknow encoding format: ' + format + ' in CFF');
+            error('Unknown encoding format: ' + format + ' in CFF');
             break;
         }
         var dataEnd = pos;
         if (format & 0x80) {
           // The font sanitizer does not support CFF encoding with a
           // supplement, since the encoding is not really used to map
           // between gid to glyph, let's overwrite what is declared in
           // the top dictionary to let the sanitizer think the font use
@@ -4766,17 +4766,17 @@ var CFFCompiler = (function CFFCompilerC
       var topDictTracker = compiled.trackers[0];
 
       var stringIndex = this.compileStringIndex(cff.strings.strings);
       output.add(stringIndex);
 
       var globalSubrIndex = this.compileIndex(cff.globalSubrIndex);
       output.add(globalSubrIndex);
 
-      // Now start on the other entries that have no specfic order.
+      // Now start on the other entries that have no specific order.
       if (cff.encoding && cff.topDict.hasName('Encoding')) {
         if (cff.encoding.predefined) {
           topDictTracker.setEntryLocation('Encoding', [cff.encoding.format],
                                           output);
         } else {
           var encoding = this.compileEncoding(cff.encoding);
           topDictTracker.setEntryLocation('Encoding', [output.length], output);
           output.add(encoding);
@@ -12528,23 +12528,23 @@ var JpxImage = (function JpxImageClosure
           mu = spqcds[b].mu;
           epsilon = spqcds[b].epsilon;
           b++;
         }
 
         var subband = resolution.subbands[j];
         var gainLog2 = SubbandsGainLog2[subband.type];
 
-        // calulate quantization coefficient (Section E.1.1.1)
+        // calculate quantization coefficient (Section E.1.1.1)
         var delta = (reversible ? 1 :
           Math.pow(2, precision + gainLog2 - epsilon) * (1 + mu / 2048));
         var mb = (guardBits + epsilon - 1);
 
         // In the first resolution level, copyCoefficients will fill the
-        // whole array with coefficients. In the succeding passes,
+        // whole array with coefficients. In the succeeding passes,
         // copyCoefficients will consecutively fill in the values that belong
         // to the interleaved positions of the HL, LH, and HH coefficients.
         // The LL coefficients will then be interleaved in Transform.iterate().
         copyCoefficients(coefficients, width, height, subband, delta, mb,
                          reversible, segmentationSymbolUsed);
       }
       subbandCoefficients.push({
         width: width,
@@ -16373,17 +16373,17 @@ exports.getMetrics = getMetrics;
   {
     factory((root.pdfjsCoreMurmurHash3 = {}), root.pdfjsSharedUtil);
   }
 }(this, function (exports, sharedUtil) {
 
 var Uint32ArrayView = sharedUtil.Uint32ArrayView;
 
 var MurmurHash3_64 = (function MurmurHash3_64Closure (seed) {
-  // Workaround for missing math precison in JS.
+  // Workaround for missing math precision in JS.
   var MASK_HIGH = 0xffff0000;
   var MASK_LOW = 0xffff;
 
   function MurmurHash3_64 (seed) {
     var SEED = 0xc3d2e1f0;
     this.h1 = seed ? seed & 0xffffffff : SEED;
     this.h2 = seed ? seed & 0xffffffff : SEED;
   }
@@ -24776,17 +24776,17 @@ var Lexer = (function LexerClosure() {
             divideBy = 1;
           } else {
             // A number can have only one '.'
             break;
           }
         } else if (ch === 0x2D) { // '-'
           // ignore minus signs in the middle of numbers to match
           // Adobe's behavior
-          warn('Badly formated number');
+          warn('Badly formatted number');
         } else if (ch === 0x45 || ch === 0x65) { // 'E', 'e'
           // 'E' can be either a scientific notation or the beginning of a new
           // operator
           ch = this.peekChar();
           if (ch === 0x2B || ch === 0x2D) { // '+', '-'
             powerValueSign = (ch === 0x2D) ? -1 : 1;
             this.nextChar(); // Consume the sign character
           } else if (ch < 0x30 || ch > 0x39) { // '0' - '9'
@@ -25857,19 +25857,21 @@ exports.Type1Parser = Type1Parser;
   {
     factory((root.pdfjsCoreCMap = {}), root.pdfjsSharedUtil,
       root.pdfjsCorePrimitives, root.pdfjsCoreStream, root.pdfjsCoreParser);
   }
 }(this, function (exports, sharedUtil, corePrimitives, coreStream, coreParser) {
 
 var Util = sharedUtil.Util;
 var assert = sharedUtil.assert;
+var warn = sharedUtil.warn;
 var error = sharedUtil.error;
 var isInt = sharedUtil.isInt;
 var isString = sharedUtil.isString;
+var MissingDataException = sharedUtil.MissingDataException;
 var isName = corePrimitives.isName;
 var isCmd = corePrimitives.isCmd;
 var isStream = corePrimitives.isStream;
 var StringStream = coreStream.StringStream;
 var Lexer = coreParser.Lexer;
 var isEOF = coreParser.isEOF;
 
 var BUILT_IN_CMAPS = [
@@ -26707,64 +26709,71 @@ var CMapFactory = (function CMapFactoryC
       cMap.name = obj.name;
     }
   }
 
   function parseCMap(cMap, lexer, builtInCMapParams, useCMap) {
     var previous;
     var embededUseCMap;
     objLoop: while (true) {
-      var obj = lexer.getObj();
-      if (isEOF(obj)) {
-        break;
-      } else if (isName(obj)) {
-        if (obj.name === 'WMode') {
-          parseWMode(cMap, lexer);
-        } else if (obj.name === 'CMapName') {
-          parseCMapName(cMap, lexer);
-        }
-        previous = obj;
-      } else if (isCmd(obj)) {
-        switch (obj.cmd) {
-          case 'endcmap':
-            break objLoop;
-          case 'usecmap':
-            if (isName(previous)) {
-              embededUseCMap = previous.name;
-            }
-            break;
-          case 'begincodespacerange':
-            parseCodespaceRange(cMap, lexer);
-            break;
-          case 'beginbfchar':
-            parseBfChar(cMap, lexer);
-            break;
-          case 'begincidchar':
-            parseCidChar(cMap, lexer);
-            break;
-          case 'beginbfrange':
-            parseBfRange(cMap, lexer);
-            break;
-          case 'begincidrange':
-            parseCidRange(cMap, lexer);
-            break;
-        }
+      try {
+        var obj = lexer.getObj();
+        if (isEOF(obj)) {
+          break;
+        } else if (isName(obj)) {
+          if (obj.name === 'WMode') {
+            parseWMode(cMap, lexer);
+          } else if (obj.name === 'CMapName') {
+            parseCMapName(cMap, lexer);
+          }
+          previous = obj;
+        } else if (isCmd(obj)) {
+          switch (obj.cmd) {
+            case 'endcmap':
+              break objLoop;
+            case 'usecmap':
+              if (isName(previous)) {
+                embededUseCMap = previous.name;
+              }
+              break;
+            case 'begincodespacerange':
+              parseCodespaceRange(cMap, lexer);
+              break;
+            case 'beginbfchar':
+              parseBfChar(cMap, lexer);
+              break;
+            case 'begincidchar':
+              parseCidChar(cMap, lexer);
+              break;
+            case 'beginbfrange':
+              parseBfRange(cMap, lexer);
+              break;
+            case 'begincidrange':
+              parseCidRange(cMap, lexer);
+              break;
+          }
+        }
+      } catch (ex) {
+        if (ex instanceof MissingDataException) {
+          throw ex;
+        }
+        warn('Invalid cMap data: ' + ex);
+        continue;
       }
     }
 
     if (!useCMap && embededUseCMap) {
       // Load the usecmap definition from the file only if there wasn't one
       // specified.
       useCMap = embededUseCMap;
     }
     if (useCMap) {
       return extendCMap(cMap, builtInCMapParams, useCMap);
-    } else {
-      return Promise.resolve(cMap);
-    }
+    }
+    return Promise.resolve(cMap);
   }
 
   function extendCMap(cMap, builtInCMapParams, useCMap) {
     return createBuiltInCMap(useCMap, builtInCMapParams).then(
         function(newCMap) {
       cMap.useCMap = newCMap;
       // If there aren't any code space ranges defined clone all the parent ones
       // into this cMap.
@@ -26816,18 +26825,16 @@ var CMapFactory = (function CMapFactoryC
       request.onreadystatechange = function () {
         if (request.readyState === XMLHttpRequest.DONE) {
           if (request.status === 200 || request.status === 0) {
             var cMap = new CMap(true);
             var lexer = new Lexer(new StringStream(request.responseText));
             parseCMap(cMap, lexer, builtInCMapParams, null).then(
                 function (parsedCMap) {
               resolve(parsedCMap);
-            }).catch(function (e) {
-              reject(new Error({ message: 'Invalid CMap data', error: e }));
             });
           } else {
             reject(new Error('Unable to get cMap at: ' + url));
           }
         }
       };
       request.open('GET', url, true);
       request.send(null);
@@ -27285,16 +27292,17 @@ var ProblematicCharRanges = new Int32Arr
   // Control characters.
   0x0000, 0x0020,
   0x007F, 0x00A1,
   0x00AD, 0x00AE,
   // Chars that is used in complex-script shaping.
   0x0600, 0x0780,
   0x08A0, 0x10A0,
   0x1780, 0x1800,
+  0x1C00, 0x1C50,
   // General punctuation chars.
   0x2000, 0x2010,
   0x2011, 0x2012,
   0x2028, 0x2030,
   0x205F, 0x2070,
   0x25CC, 0x25CD,
   0x3000, 0x3001,
   // Chars that is used in complex-script shaping.
@@ -29465,17 +29473,17 @@ var Font = (function FontClosure() {
       // Maximum profile
       builder.addTable('maxp',
             '\x00\x00\x50\x00' + // Version number
             string16(numGlyphs)); // Num of glyphs
 
       // Naming tables
       builder.addTable('name', createNameTable(fontName));
 
-      // PostScript informations
+      // PostScript information
       builder.addTable('post', createPostTable(properties));
 
       return builder.toArray();
     },
 
     get spaceWidth() {
       if ('_shadowWidth' in this) {
         return this._shadowWidth;
@@ -29842,17 +29850,17 @@ var Type1Font = (function Type1FontClosu
     var pfbHeader = file.peekBytes(PFB_HEADER_SIZE);
     var pfbHeaderPresent = pfbHeader[0] === 0x80 && pfbHeader[1] === 0x01;
     if (pfbHeaderPresent) {
       file.skip(PFB_HEADER_SIZE);
       headerBlockLength = (pfbHeader[5] << 24) | (pfbHeader[4] << 16) |
                           (pfbHeader[3] << 8) | pfbHeader[2];
     }
 
-    // Get the data block containing glyphs and subrs informations
+    // Get the data block containing glyphs and subrs information
     var headerBlock = getHeaderBlock(file, headerBlockLength);
     headerBlockLength = headerBlock.length;
     var headerBlockParser = new Type1Parser(headerBlock.stream, false,
                                             SEAC_ANALYSIS_ENABLED);
     headerBlockParser.extractFontHeader(properties);
 
     if (pfbHeaderPresent) {
       pfbHeader = file.getBytes(PFB_HEADER_SIZE);
@@ -30709,17 +30717,17 @@ var PDFFunction = (function PDFFunctionC
           } else if (v < min) {
             v = min;
           }
           return v;
         };
 
         // clip to domain
         var v = clip(src[srcOffset], domain[0], domain[1]);
-        // calulate which bound the value is in
+        // calculate which bound the value is in
         for (var i = 0, ii = bounds.length; i < ii; ++i) {
           if (v < bounds[i]) {
             break;
           }
         }
 
         // encode value into domain of function
         var dmin = domain[0];
@@ -33152,17 +33160,17 @@ var PDFImage = (function PDFImageClosure
             output[i + 3] = (buf >> 4) & 1;
             output[i + 4] = (buf >> 3) & 1;
             output[i + 5] = (buf >> 2) & 1;
             output[i + 6] = (buf >> 1) & 1;
             output[i + 7] = buf & 1;
             i += 8;
           }
 
-          // handle remaing bits
+          // handle remaining bits
           if (i < loop2End) {
             buf = buffer[bufferPos++];
             mask = 128;
             while (i < loop2End) {
               output[i++] = +!!(buf & mask);
               mask >>= 1;
             }
           }
@@ -33220,17 +33228,17 @@ var PDFImage = (function PDFImageClosure
             alphaBuf[i] = 255 - alphaBuf[i];
           }
 
           if (sw !== width || sh !== height) {
             alphaBuf = resizeImageMask(alphaBuf, mask.bpc, sw, sh,
                                        width, height);
           }
         } else if (isArray(mask)) {
-          // Color key mask: if any of the compontents are outside the range
+          // Color key mask: if any of the components are outside the range
           // then they should be painted.
           alphaBuf = new Uint8Array(width * height);
           var numComps = this.numComps;
           for (i = 0, ii = width * height; i < ii; ++i) {
             var opacity = 0;
             var imageOffset = i * numComps;
             for (j = 0; j < numComps; ++j) {
               var color = image[imageOffset + j];
@@ -38053,17 +38061,17 @@ var PartialEvaluator = (function Partial
                                                           properties);
             return new Font(baseFontName, null, properties);
           }.bind(this));
         }
       }
 
       // According to the spec if 'FontDescriptor' is declared, 'FirstChar',
       // 'LastChar' and 'Widths' should exist too, but some PDF encoders seem
-      // to ignore this rule when a variant of a standart font is used.
+      // to ignore this rule when a variant of a standard font is used.
       // TODO Fill the width array depending on which of the base font this is
       // a variant.
       var firstChar = (dict.get('FirstChar') || 0);
       var lastChar = (dict.get('LastChar') || maxCharIndex);
 
       var fontName = descriptor.get('FontName');
       var baseFont = dict.get('BaseFont');
       // Some bad PDFs have a string as the font name.
--- a/browser/extensions/pdfjs/content/web/debugger.js
+++ b/browser/extensions/pdfjs/content/web/debugger.js
@@ -573,17 +573,17 @@ var PDFBug = (function PDFBugClosure() {
         panels.appendChild(panel);
         tool.panel = panel;
         tool.manager = this;
         if (tool.enabled) {
           tool.init(pdfjsLib);
         } else {
           panel.textContent = tool.name + ' is disabled. To enable add ' +
                               ' "' + tool.id + '" to the pdfBug parameter ' +
-                              'and refresh (seperate multiple by commas).';
+                              'and refresh (separate multiple by commas).';
         }
         buttons.push(panelButton);
       }
       this.selectPanel(0);
     },
     cleanup: function cleanup() {
       for (var i = 0, ii = this.tools.length; i < ii; i++) {
         if (this.tools[i].enabled) {
--- a/browser/extensions/pdfjs/content/web/viewer.js
+++ b/browser/extensions/pdfjs/content/web/viewer.js
@@ -5544,17 +5544,17 @@ var PDFPageView = (function PDFPageViewC
         };
 
         pdfPage.render(renderContext).promise.then(function() {
           // Tell the printEngine that rendering this canvas/page has finished.
           obj.done();
         }, function(error) {
           console.error(error);
           // Tell the printEngine that rendering this canvas/page has failed.
-          // This will make the print proces stop.
+          // This will make the print process stop.
           if ('abort' in obj) {
             obj.abort();
           } else {
             obj.done();
           }
         });
       };
     },
@@ -6341,32 +6341,44 @@ var PDFViewer = (function pdfViewer() {
     get pagesCount() {
       return this._pages.length;
     },
 
     getPageView: function (index) {
       return this._pages[index];
     },
 
+    /**
+     * @returns {number}
+     */
     get currentPageNumber() {
       return this._currentPageNumber;
     },
 
+    /**
+     * @param {number} val - The page number.
+     */
     set currentPageNumber(val) {
       if (!this.pdfDocument) {
         this._currentPageNumber = val;
         return;
       }
-      this._setCurrentPageNumber(val);
       // The intent can be to just reset a scroll position and/or scale.
-      this._resetCurrentPageView();
-    },
-
-    _setCurrentPageNumber: function pdfViewer_setCurrentPageNumber(val) {
+      this._setCurrentPageNumber(val, /* resetCurrentPageView = */ true);
+    },
+
+    /**
+     * @private
+     */
+    _setCurrentPageNumber:
+        function pdfViewer_setCurrentPageNumber(val, resetCurrentPageView) {
       if (this._currentPageNumber === val) {
+        if (resetCurrentPageView) {
+          this._resetCurrentPageView();
+        }
         return;
       }
       var arg;
       if (!(0 < val && val <= this.pagesCount)) {
         arg = {
           source: this,
           pageNumber: this._currentPageNumber,
           previousPageNumber: val
@@ -6379,16 +6391,20 @@ var PDFViewer = (function pdfViewer() {
       arg = {
         source: this,
         pageNumber: val,
         previousPageNumber: this._currentPageNumber
       };
       this._currentPageNumber = val;
       this.eventBus.dispatch('pagechanging', arg);
       this.eventBus.dispatch('pagechange', arg);
+
+      if (resetCurrentPageView) {
+        this._resetCurrentPageView();
+      }
     },
 
     /**
      * @returns {number}
      */
     get currentScale() {
       return this._currentScale !== UNKNOWN_SCALE ? this._currentScale :
                                                     DEFAULT_SCALE;
@@ -6686,16 +6702,17 @@ var PDFViewer = (function pdfViewer() {
             return;
         }
         this._setScaleUpdatePages(scale, value, noScroll, true);
       }
     },
 
     /**
      * Refreshes page view: scrolls to the current page and updates the scale.
+     * @private
      */
     _resetCurrentPageView: function () {
       if (this.isInPresentationMode) {
         // Fixes the case when PDF has different page sizes.
         this._setScale(this._currentScaleValue, true);
       }
 
       var pageView = this._pages[this._currentPageNumber - 1];
@@ -6710,18 +6727,17 @@ var PDFViewer = (function pdfViewer() {
      */
     scrollPageIntoView: function PDFViewer_scrollPageIntoView(pageNumber,
                                                               dest) {
       if (!this.pdfDocument) {
         return;
       }
 
       if (this.isInPresentationMode || !dest) {
-        this._setCurrentPageNumber(pageNumber);
-        this._resetCurrentPageView();
+        this._setCurrentPageNumber(pageNumber, /* resetCurrentPageView */ true);
         return;
       }
 
       var pageView = this._pages[pageNumber - 1];
       var x = 0, y = 0;
       var width = 0, height = 0, widthScale, heightScale;
       var changeOrientation = (pageView.rotation % 180 === 0 ? false : true);
       var pageWidth = (changeOrientation ? pageView.height : pageView.width) /
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -93,18 +93,17 @@ Tools.inspector = {
   panelLabel: l10n("inspector.panelLabel", inspectorStrings),
   get tooltip() {
     return l10n("inspector.tooltip2", inspectorStrings,
     (osString == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+") + this.key);
   },
   inMenu: true,
   commands: [
     "devtools/client/responsivedesign/resize-commands",
-    "devtools/client/inspector/inspector-commands",
-    "devtools/client/eyedropper/commands.js"
+    "devtools/client/inspector/inspector-commands"
   ],
 
   preventClosingOnKey: true,
   onkey: function (panel, toolbox) {
     toolbox.highlighterUtils.togglePicker();
   },
 
   isTargetSupported: function (target) {
deleted file mode 100644
--- a/devtools/client/eyedropper/commands.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-const l10n = require("gcli/l10n");
-const EventEmitter = require("devtools/shared/event-emitter");
-const eventEmitter = new EventEmitter();
-
-var { Eyedropper, EyedropperManager } = require("devtools/client/eyedropper/eyedropper");
-
-/**
- * 'eyedropper' command
- */
-exports.items = [{
-  item: "command",
-  runAt: "client",
-  name: "eyedropper",
-  description: l10n.lookup("eyedropperDesc"),
-  manual: l10n.lookup("eyedropperManual"),
-  buttonId: "command-button-eyedropper",
-  buttonClass: "command-button command-button-invertable",
-  tooltipText: l10n.lookup("eyedropperTooltip"),
-  state: {
-    isChecked: function (target) {
-      if (!target.tab) {
-        return false;
-      }
-      let chromeWindow = target.tab.ownerDocument.defaultView;
-      let dropper = EyedropperManager.getInstance(chromeWindow);
-      if (dropper) {
-        return true;
-      }
-      return false;
-    },
-    onChange: function (target, changeHandler) {
-      eventEmitter.on("changed", changeHandler);
-    },
-    offChange: function (target, changeHandler) {
-      eventEmitter.off("changed", changeHandler);
-    },
-  },
-  exec: function (args, context) {
-    let chromeWindow = context.environment.chromeWindow;
-    let target = context.environment.target;
-
-    let dropper = EyedropperManager.createInstance(chromeWindow,
-                                                   { context: "command",
-                                                     copyOnSelect: true });
-    dropper.open();
-
-    eventEmitter.emit("changed", { target: target });
-
-    dropper.once("destroy", () => {
-      eventEmitter.emit("changed", { target: target });
-    });
-  }
-}];
deleted file mode 100644
--- a/devtools/client/eyedropper/crosshairs.css
+++ /dev/null
@@ -1,3 +0,0 @@
-* {
-  cursor: crosshair !important;
-}
\ No newline at end of file
deleted file mode 100644
--- a/devtools/client/eyedropper/eyedropper-child.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/* 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 { interfaces: Ci } = Components;
-
-addMessageListener("Eyedropper:RequestContentScreenshot", sendContentScreenshot);
-
-function sendContentScreenshot() {
-  let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-  let scale = content.getInterface(Ci.nsIDOMWindowUtils).fullZoom;
-  let width = content.innerWidth;
-  let height = content.innerHeight;
-  canvas.width = width * scale;
-  canvas.height = height * scale;
-  canvas.mozOpaque = true;
-
-  let ctx = canvas.getContext("2d");
-
-  ctx.scale(scale, scale);
-  ctx.drawWindow(content, content.scrollX, content.scrollY, width, height, "#fff");
-
-  sendAsyncMessage("Eyedropper:Screenshot", canvas.toDataURL());
-}
deleted file mode 100644
--- a/devtools/client/eyedropper/eyedropper.js
+++ /dev/null
@@ -1,839 +0,0 @@
-/* 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 {Cc, Ci} = require("chrome");
-const {rgbToHsl, rgbToColorName} =
-      require("devtools/client/shared/css-color").colorUtils;
-const Telemetry = require("devtools/client/shared/telemetry");
-const EventEmitter = require("devtools/shared/event-emitter");
-const promise = require("promise");
-const defer = require("devtools/shared/defer");
-const Services = require("Services");
-
-loader.lazyGetter(this, "clipboardHelper", function () {
-  return Cc["@mozilla.org/widget/clipboardhelper;1"]
-    .getService(Ci.nsIClipboardHelper);
-});
-
-loader.lazyGetter(this, "ssService", function () {
-  return Cc["@mozilla.org/content/style-sheet-service;1"]
-    .getService(Ci.nsIStyleSheetService);
-});
-
-loader.lazyGetter(this, "ioService", function () {
-  return Cc["@mozilla.org/network/io-service;1"]
-    .getService(Ci.nsIIOService);
-});
-
-loader.lazyGetter(this, "DOMUtils", function () {
-  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
-});
-
-loader.lazyGetter(this, "l10n", () => Services.strings
-  .createBundle("chrome://devtools/locale/eyedropper.properties"));
-
-const EYEDROPPER_URL = "chrome://devtools/content/eyedropper/eyedropper.xul";
-const CROSSHAIRS_URL = "chrome://devtools/content/eyedropper/crosshairs.css";
-const NOCURSOR_URL = "chrome://devtools/content/eyedropper/nocursor.css";
-
-const ZOOM_PREF = "devtools.eyedropper.zoom";
-const FORMAT_PREF = "devtools.defaultColorUnit";
-
-const CANVAS_WIDTH = 96;
-const CANVAS_OFFSET = 3; // equals the border width of the canvas.
-const CLOSE_DELAY = 750;
-
-const HEX_BOX_WIDTH = CANVAS_WIDTH + CANVAS_OFFSET * 2;
-const HSL_BOX_WIDTH = 158;
-
-/**
- * Manage instances of eyedroppers for windows. Registering here isn't
- * necessary for creating an eyedropper, but can be used for testing.
- */
-var EyedropperManager = {
-  _instances: new WeakMap(),
-
-  getInstance: function (chromeWindow) {
-    return this._instances.get(chromeWindow);
-  },
-
-  createInstance: function (chromeWindow, options) {
-    let dropper = this.getInstance(chromeWindow);
-    if (dropper) {
-      return dropper;
-    }
-
-    dropper = new Eyedropper(chromeWindow, options);
-    this._instances.set(chromeWindow, dropper);
-
-    dropper.on("destroy", () => {
-      this.deleteInstance(chromeWindow);
-    });
-
-    return dropper;
-  },
-
-  deleteInstance: function (chromeWindow) {
-    this._instances.delete(chromeWindow);
-  }
-};
-
-exports.EyedropperManager = EyedropperManager;
-
-/**
- * Eyedropper widget. Once opened, shows zoomed area above current pixel and
- * displays the color value of the center pixel. Clicking on the window will
- * close the widget and fire a 'select' event. If 'copyOnSelect' is true, the color
- * will also be copied to the clipboard.
- *
- * let eyedropper = new Eyedropper(window);
- * eyedropper.open();
- *
- * eyedropper.once("select", (ev, color) => {
- *   console.log(color);  // "rgb(20, 50, 230)"
- * })
- *
- * @param {DOMWindow} chromeWindow
- *        window to inspect
- * @param {object} opts
- *        optional options object, with 'copyOnSelect', 'context'
- */
-function Eyedropper(chromeWindow, opts = { copyOnSelect: true, context: "other" }) {
-  this.copyOnSelect = opts.copyOnSelect;
-
-  this._onFirstMouseMove = this._onFirstMouseMove.bind(this);
-  this._onMouseMove = this._onMouseMove.bind(this);
-  this._onMouseDown = this._onMouseDown.bind(this);
-  this._onKeyDown = this._onKeyDown.bind(this);
-  this._onFrameLoaded = this._onFrameLoaded.bind(this);
-
-  this._chromeWindow = chromeWindow;
-  this._chromeDocument = chromeWindow.document;
-
-  this._OS = Services.appinfo.OS;
-
-  this._dragging = true;
-  this.loaded = false;
-
-  this._mouseMoveCounter = 0;
-
-  this.format = Services.prefs.getCharPref(FORMAT_PREF); // color value format
-  this.zoom = Services.prefs.getIntPref(ZOOM_PREF);      // zoom level - integer
-
-  this._zoomArea = {
-    x: 0,          // the left coordinate of the center of the inspected region
-    y: 0,          // the top coordinate of the center of the inspected region
-    width: CANVAS_WIDTH,      // width of canvas to draw zoomed area onto
-    height: CANVAS_WIDTH      // height of canvas
-  };
-
-  if (this._contentTab) {
-    let mm = this._contentTab.linkedBrowser.messageManager;
-    mm.loadFrameScript("resource://devtools/client/eyedropper/eyedropper-child.js", true);
-  }
-
-  // record if this was opened via the picker or standalone
-  var telemetry = new Telemetry();
-  if (opts.context == "command") {
-    telemetry.toolOpened("eyedropper");
-  }
-  else if (opts.context == "menu") {
-    telemetry.toolOpened("menueyedropper");
-  }
-  else if (opts.context == "picker") {
-    telemetry.toolOpened("pickereyedropper");
-  }
-
-  EventEmitter.decorate(this);
-}
-
-exports.Eyedropper = Eyedropper;
-
-Eyedropper.prototype = {
-  /**
-   * Get the number of cells (blown-up pixels) per direction in the grid.
-   */
-  get cellsWide() {
-    // Canvas will render whole "pixels" (cells) only, and an even
-    // number at that. Round up to the nearest even number of pixels.
-    let cellsWide = Math.ceil(this._zoomArea.width / this.zoom);
-    cellsWide += cellsWide % 2;
-
-    return cellsWide;
-  },
-
-  /**
-   * Get the size of each cell (blown-up pixel) in the grid.
-   */
-  get cellSize() {
-    return this._zoomArea.width / this.cellsWide;
-  },
-
-  /**
-   * Get index of cell in the center of the grid.
-   */
-  get centerCell() {
-    return Math.floor(this.cellsWide / 2);
-  },
-
-  /**
-   * Get color of center cell in the grid.
-   */
-  get centerColor() {
-    let x, y;
-    x = y = (this.centerCell * this.cellSize) + (this.cellSize / 2);
-    let rgb = this._ctx.getImageData(x, y, 1, 1).data;
-    return rgb;
-  },
-
-  get _contentTab() {
-    return this._chromeWindow.gBrowser && this._chromeWindow.gBrowser.selectedTab;
-  },
-
-  /**
-   * Fetch a screenshot of the content.
-   *
-   * @return {promise}
-   *         Promise that resolves with the screenshot as a dataURL
-   */
-  getContentScreenshot: function () {
-    if (!this._contentTab) {
-        return promise.resolve(null);
-    }
-
-    let deferred = defer();
-
-    let mm = this._contentTab.linkedBrowser.messageManager;
-    function onScreenshot(message) {
-      mm.removeMessageListener("Eyedropper:Screenshot", onScreenshot);
-      deferred.resolve(message.data);
-    }
-    mm.addMessageListener("Eyedropper:Screenshot", onScreenshot);
-    mm.sendAsyncMessage("Eyedropper:RequestContentScreenshot");
-
-    return deferred.promise;
-  },
-
-  /**
-   * Start the eyedropper. Add listeners for a mouse move in the window to
-   * show the eyedropper.
-   */
-  open: function () {
-    if (this.isOpen) {
-      // the eyedropper is aready open, don't create another panel.
-      return promise.resolve();
-    }
-
-    this.isOpen = true;
-
-    this._showCrosshairs();
-
-    // Get screenshot of content so we can inspect colors
-    return this.getContentScreenshot().then((dataURL) => {
-      // The data url may be null, e.g. if there is no content tab
-      if (dataURL) {
-        this._contentImage = new this._chromeWindow.Image();
-        this._contentImage.src = dataURL;
-
-        // Wait for screenshot to load
-        let imageLoaded = promise.defer();
-        this._contentImage.onload = imageLoaded.resolve
-        return imageLoaded.promise;
-      }
-    }).then(() => {
-      // Then start showing the eyedropper UI
-      this._chromeDocument.addEventListener("mousemove", this._onFirstMouseMove);
-      this.isStarted = true;
-      this.emit("started");
-    });
-  },
-
-  /**
-   * Called on the first mouse move over the window. Opens the eyedropper
-   * panel where the mouse is.
-   */
-  _onFirstMouseMove: function (event) {
-    this._chromeDocument.removeEventListener("mousemove", this._onFirstMouseMove);
-
-    this._panel = this._buildPanel();
-
-    let popupSet = this._chromeDocument.querySelector("#mainPopupSet");
-    popupSet.appendChild(this._panel);
-
-    let { panelX, panelY } = this._getPanelCoordinates(event);
-    this._panel.openPopupAtScreen(panelX, panelY);
-
-    this._setCoordinates(event);
-
-    this._addListeners();
-
-    // hide cursor as we'll be showing the panel over the mouse instead.
-    this._hideCrosshairs();
-    this._hideCursor();
-  },
-
-  /**
-   * Whether the coordinates are over the content or chrome.
-   *
-   * @param {number} clientX
-   *        x-coordinate of mouse relative to browser window.
-   * @param {number} clientY
-   *        y-coordinate of mouse relative to browser window.
-   */
-  _isInContent: function (clientX, clientY) {
-    let box = this._contentTab && this._contentTab.linkedBrowser.getBoundingClientRect();
-    if (box &&
-        clientX > box.left &&
-        clientX < box.right &&
-        clientY > box.top &&
-        clientY < box.bottom) {
-      return true;
-    }
-    return false;
-  },
-
-  /**
-   * Set the current coordinates to inspect from where a mousemove originated.
-   *
-   * @param {MouseEvent} event
-   *        Event for the mouse move.
-   */
-  _setCoordinates: function (event) {
-    let inContent = this._isInContent(event.clientX, event.clientY);
-    let win = this._chromeWindow;
-
-    // offset of mouse from browser window
-    let x = event.clientX;
-    let y = event.clientY;
-
-    if (inContent) {
-      // calculate the offset of the mouse from the content window
-      let box = this._contentTab.linkedBrowser.getBoundingClientRect();
-      x = x - box.left;
-      y = y - box.top;
-
-      this._zoomArea.contentWidth = box.width;
-      this._zoomArea.contentHeight = box.height;
-    }
-    this._zoomArea.inContent = inContent;
-
-    // don't let it inspect outside the browser window
-    x = Math.max(0, Math.min(x, win.outerWidth - 1));
-    y = Math.max(0, Math.min(y, win.outerHeight - 1));
-
-    this._zoomArea.x = x;
-    this._zoomArea.y = y;
-  },
-
-  /**
-   * Build and add a new eyedropper panel to the window.
-   *
-   * @return {Panel}
-   *         The XUL panel holding the eyedropper UI.
-   */
-  _buildPanel: function () {
-    let panel = this._chromeDocument.createElement("panel");
-    panel.setAttribute("noautofocus", true);
-    panel.setAttribute("noautohide", true);
-    panel.setAttribute("level", "floating");
-    panel.setAttribute("class", "devtools-eyedropper-panel");
-
-    let iframe = this._iframe = this._chromeDocument.createElement("iframe");
-    iframe.addEventListener("load", this._onFrameLoaded, true);
-    iframe.setAttribute("flex", "1");
-    iframe.setAttribute("transparent", "transparent");
-    iframe.setAttribute("allowTransparency", true);
-    iframe.setAttribute("class", "devtools-eyedropper-iframe");
-    iframe.setAttribute("src", EYEDROPPER_URL);
-    iframe.setAttribute("width", CANVAS_WIDTH);
-    iframe.setAttribute("height", CANVAS_WIDTH);
-
-    panel.appendChild(iframe);
-
-    return panel;
-  },
-
-  /**
-   * Event handler for the panel's iframe's load event. Emits
-   * a "load" event from this eyedropper object.
-   */
-  _onFrameLoaded: function () {
-    this._iframe.removeEventListener("load", this._onFrameLoaded, true);
-
-    this._iframeDocument = this._iframe.contentDocument;
-    this._colorPreview = this._iframeDocument.querySelector("#color-preview");
-    this._colorValue = this._iframeDocument.querySelector("#color-value");
-
-    // value box will be too long for hex values and too short for hsl
-    let valueBox = this._iframeDocument.querySelector("#color-value-box");
-    if (this.format == "hex") {
-      valueBox.style.width = HEX_BOX_WIDTH + "px";
-    }
-    else if (this.format == "hsl") {
-      valueBox.style.width = HSL_BOX_WIDTH + "px";
-    }
-
-    this._canvas = this._iframeDocument.querySelector("#canvas");
-    this._ctx = this._canvas.getContext("2d");
-
-    // so we preserve the clear pixel boundaries
-    this._ctx.mozImageSmoothingEnabled = false;
-
-    this._drawWindow();
-
-    this._addPanelListeners();
-    this._iframe.focus();
-
-    this.loaded = true;
-    this.emit("load");
-  },
-
-  /**
-   * Add key listeners to the panel.
-   */
-  _addPanelListeners: function () {
-    this._iframeDocument.addEventListener("keydown", this._onKeyDown);
-
-    let closeCmd = this._iframeDocument.getElementById("eyedropper-cmd-close");
-    closeCmd.addEventListener("command", this.destroy.bind(this), true);
-
-    let copyCmd = this._iframeDocument.getElementById("eyedropper-cmd-copy");
-    copyCmd.addEventListener("command", this.selectColor.bind(this), true);
-  },
-
-  /**
-   * Remove listeners from the panel.
-   */
-  _removePanelListeners: function () {
-    this._iframeDocument.removeEventListener("keydown", this._onKeyDown);
-  },
-
-  /**
-   * Add mouse event listeners to the document we're inspecting.
-   */
-  _addListeners: function () {
-    this._chromeDocument.addEventListener("mousemove", this._onMouseMove);
-    this._chromeDocument.addEventListener("mousedown", this._onMouseDown);
-  },
-
-  /**
-   * Remove mouse event listeners from the document we're inspecting.
-   */
-  _removeListeners: function () {
-    this._chromeDocument.removeEventListener("mousemove", this._onFirstMouseMove);
-    this._chromeDocument.removeEventListener("mousemove", this._onMouseMove);
-    this._chromeDocument.removeEventListener("mousedown", this._onMouseDown);
-  },
-
-  /**
-   * Hide the cursor.
-   */
-  _hideCursor: function () {
-    registerStyleSheet(NOCURSOR_URL);
-  },
-
-  /**
-   * Reset the cursor back to default.
-   */
-  _resetCursor: function () {
-    unregisterStyleSheet(NOCURSOR_URL);
-  },
-
-  /**
-   * Show a crosshairs as the mouse cursor
-   */
-  _showCrosshairs: function () {
-    registerStyleSheet(CROSSHAIRS_URL);
-  },
-
-  /**
-   * Reset cursor.
-   */
-  _hideCrosshairs: function () {
-    unregisterStyleSheet(CROSSHAIRS_URL);
-  },
-
-  /**
-   * Event handler for a mouse move over the page we're inspecting.
-   * Preview the area under the cursor, and move panel to be under the cursor.
-   *
-   * @param  {DOMEvent} event
-   *         MouseEvent for the mouse moving
-   */
-  _onMouseMove: function (event) {
-    if (!this._dragging || !this._panel || !this._canvas) {
-      return;
-    }
-
-    if (this._OS == "Linux" && ++this._mouseMoveCounter % 2 == 0) {
-      // skip every other mousemove to preserve performance.
-      return;
-    }
-
-    this._setCoordinates(event);
-    this._drawWindow();
-
-    let { panelX, panelY } = this._getPanelCoordinates(event);
-    this._movePanel(panelX, panelY);
-  },
-
-  /**
-   * Get coordinates of where the eyedropper panel should go based on
-   * the current coordinates of the mouse cursor.
-   *
-   * @param {MouseEvent} event
-   *        object with properties 'screenX' and 'screenY'
-   *
-   * @return {object}
-  *          object with properties 'panelX', 'panelY'
-   */
-  _getPanelCoordinates: function ({screenX, screenY}) {
-    let win = this._chromeWindow;
-    let offset = CANVAS_WIDTH / 2 + CANVAS_OFFSET;
-
-    let panelX = screenX - offset;
-    let windowX = win.screenX + (win.outerWidth - win.innerWidth);
-    let maxX = win.screenX + win.outerWidth - offset - 1;
-
-    let panelY = screenY - offset;
-    let windowY = win.screenY + (win.outerHeight - win.innerHeight);
-    let maxY = win.screenY + win.outerHeight - offset - 1;
-
-    // don't let the panel move outside the browser window
-    panelX = Math.max(windowX - offset, Math.min(panelX, maxX));
-    panelY = Math.max(windowY - offset, Math.min(panelY, maxY));
-
-    return { panelX: panelX, panelY: panelY };
-  },
-
-  /**
-   * Move the eyedropper panel to the given coordinates.
-   *
-   * @param  {number} screenX
-   *         left coordinate on the screen
-   * @param  {number} screenY
-   *         top coordinate
-   */
-  _movePanel: function (screenX, screenY) {
-    this._panelX = screenX;
-    this._panelY = screenY;
-
-    this._panel.moveTo(screenX, screenY);
-  },
-
-  /**
-   * Handler for the mouse down event on the inspected page. This means a
-   * click, so we'll select the color that's currently hovered.
-   *
-   * @param  {Event} event
-   *         DOM MouseEvent object
-   */
-  _onMouseDown: function (event) {
-    event.preventDefault();
-    event.stopPropagation();
-
-    this.selectColor();
-  },
-
-  /**
-   * Select the current color that's being previewed. Fire a
-   * "select" event with the color as an rgb string.
-   */
-  selectColor: function () {
-    if (this._isSelecting) {
-      return;
-    }
-    this._isSelecting = true;
-    this._dragging = false;
-
-    this.emit("select", this._colorValue.value);
-
-    if (this.copyOnSelect) {
-      this.copyColor(this.destroy.bind(this));
-    }
-    else {
-      this.destroy();
-    }
-  },
-
-  /**
-   * Copy the currently inspected color to the clipboard.
-   *
-   * @param  {Function} callback
-   *         Callback to be called when the color is in the clipboard.
-   */
-  copyColor: function (callback) {
-    clearTimeout(this._copyTimeout);
-
-    let color = this._colorValue.value;
-    clipboardHelper.copyString(color);
-
-    this._colorValue.classList.add("highlight");
-    this._colorValue.value = "✓ " + l10n.GetStringFromName("colorValue.copied");
-
-    this._copyTimeout = setTimeout(() => {
-      this._colorValue.classList.remove("highlight");
-      this._colorValue.value = color;
-
-      if (callback) {
-        callback();
-      }
-    }, CLOSE_DELAY);
-  },
-
-  /**
-   * Handler for the keydown event on the panel. Either copy the color
-   * or move the panel in a direction depending on the key pressed.
-   *
-   * @param  {Event} event
-   *         DOM KeyboardEvent object
-   */
-  _onKeyDown: function (event) {
-    if (event.metaKey && event.keyCode === event.DOM_VK_C) {
-      this.copyColor();
-      return;
-    }
-
-    let offsetX = 0;
-    let offsetY = 0;
-    let modifier = 1;
-
-    if (event.keyCode === event.DOM_VK_LEFT) {
-      offsetX = -1;
-    }
-    if (event.keyCode === event.DOM_VK_RIGHT) {
-      offsetX = 1;
-    }
-    if (event.keyCode === event.DOM_VK_UP) {
-      offsetY = -1;
-    }
-    if (event.keyCode === event.DOM_VK_DOWN) {
-      offsetY = 1;
-    }
-    if (event.shiftKey) {
-      modifier = 10;
-    }
-
-    offsetY *= modifier;
-    offsetX *= modifier;
-
-    if (offsetX !== 0 || offsetY !== 0) {
-      this._zoomArea.x += offsetX;
-      this._zoomArea.y += offsetY;
-
-      this._drawWindow();
-
-      this._movePanel(this._panelX + offsetX, this._panelY + offsetY);
-
-      event.preventDefault();
-    }
-  },
-
-  /**
-   * Draw the inspected area onto the canvas using the zoom level.
-   */
-  _drawWindow: function () {
-    let { width, height, x, y, inContent,
-          contentWidth, contentHeight } = this._zoomArea;
-
-    let zoomedWidth = width / this.zoom;
-    let zoomedHeight = height / this.zoom;
-
-    let leftX = x - (zoomedWidth / 2);
-    let topY = y - (zoomedHeight / 2);
-
-    // draw the portion of the window we're inspecting
-    if (inContent) {
-      // draw from content source image "s" to destination rect "d"
-      let sx = leftX;
-      let sy = topY;
-      let sw = zoomedWidth;
-      let sh = zoomedHeight;
-      let dx = 0;
-      let dy = 0;
-
-      // we're at the content edge, so we have to crop the drawing
-      if (leftX < 0) {
-        sx = 0;
-        sw = zoomedWidth + leftX;
-        dx = -leftX;
-      }
-      else if (leftX + zoomedWidth > contentWidth) {
-        sw = contentWidth - leftX;
-      }
-      if (topY < 0) {
-        sy = 0;
-        sh = zoomedHeight + topY;
-        dy = -topY;
-      }
-      else if (topY + zoomedHeight > contentHeight) {
-        sh = contentHeight - topY;
-      }
-      let dw = sw;
-      let dh = sh;
-
-      // we don't want artifacts when we're inspecting the edges of content
-      if (leftX < 0 || topY < 0 ||
-          leftX + zoomedWidth > contentWidth ||
-          topY + zoomedHeight > contentHeight) {
-        this._ctx.fillStyle = "white";
-        this._ctx.fillRect(0, 0, width, height);
-      }
-
-      // draw from the screenshot to the eyedropper canvas
-      this._ctx.drawImage(this._contentImage, sx, sy, sw,
-                          sh, dx, dy, dw, dh);
-    }
-    else {
-      // the mouse is over the chrome, so draw that instead of the content
-      this._ctx.drawWindow(this._chromeWindow, leftX, topY, zoomedWidth,
-                           zoomedHeight, "white");
-    }
-
-    // now scale it
-    this._ctx.drawImage(this._canvas, 0, 0, zoomedWidth, zoomedHeight,
-                                      0, 0, width, height);
-
-    let rgb = this.centerColor;
-    this._colorPreview.style.backgroundColor = toColorString(rgb, "rgb");
-    this._colorValue.value = toColorString(rgb, this.format);
-
-    if (this.zoom > 2) {
-      // grid at 2x is too busy
-      this._drawGrid();
-    }
-    this._drawCrosshair();
-  },
-
-  /**
-   * Draw a grid on the canvas representing pixel boundaries.
-   */
-  _drawGrid: function () {
-    let { width, height } = this._zoomArea;
-
-    this._ctx.lineWidth = 1;
-    this._ctx.strokeStyle = "rgba(143, 143, 143, 0.2)";
-
-    for (let i = 0; i < width; i += this.cellSize) {
-      this._ctx.beginPath();
-      this._ctx.moveTo(i - .5, 0);
-      this._ctx.lineTo(i - .5, height);
-      this._ctx.stroke();
-
-      this._ctx.beginPath();
-      this._ctx.moveTo(0, i - .5);
-      this._ctx.lineTo(width, i - .5);
-      this._ctx.stroke();
-    }
-  },
-
-  /**
-   * Draw a box on the canvas to highlight the center cell.
-   */
-  _drawCrosshair: function () {
-    let x, y;
-    x = y = this.centerCell * this.cellSize;
-
-    this._ctx.lineWidth = 1;
-    this._ctx.lineJoin = "miter";
-    this._ctx.strokeStyle = "rgba(0, 0, 0, 1)";
-    this._ctx.strokeRect(x - 1.5, y - 1.5, this.cellSize + 2, this.cellSize + 2);
-
-    this._ctx.strokeStyle = "rgba(255, 255, 255, 1)";
-    this._ctx.strokeRect(x - 0.5, y - 0.5, this.cellSize, this.cellSize);
-  },
-
-  /**
-   * Destroy the eyedropper and clean up. Emits a "destroy" event.
-   */
-  destroy: function () {
-    this._resetCursor();
-    this._hideCrosshairs();
-
-    if (this._panel) {
-      this._panel.hidePopup();
-      this._panel.remove();
-      this._panel = null;
-    }
-    this._removePanelListeners();
-    this._removeListeners();
-
-    this.isStarted = false;
-    this.isOpen = false;
-    this._isSelecting = false;
-
-    this.emit("destroy");
-  }
-};
-
-/**
- * Add a user style sheet that applies to all documents.
- */
-function registerStyleSheet(url) {
-  var uri = ioService.newURI(url, null, null);
-  if (!ssService.sheetRegistered(uri, ssService.AGENT_SHEET)) {
-    ssService.loadAndRegisterSheet(uri, ssService.AGENT_SHEET);
-  }
-}
-
-/**
- * Remove a user style sheet.
- */
-function unregisterStyleSheet(url) {
-  var uri = ioService.newURI(url, null, null);
-  if (ssService.sheetRegistered(uri, ssService.AGENT_SHEET)) {
-    ssService.unregisterSheet(uri, ssService.AGENT_SHEET);
-  }
-}
-
-/**
- * Get a formatted CSS color string from a color value.
- *
- * @param {array} rgb
- *        Rgb values of a color to format
- * @param {string} format
- *        Format of string. One of "hex", "rgb", "hsl", "name"
- *
- * @return {string}
- *        Formatted color value, e.g. "#FFF" or "hsl(20, 10%, 10%)"
- */
-function toColorString(rgb, format) {
-  let [r, g, b] = rgb;
-
-  switch (format) {
-    case "hex":
-      return hexString(rgb);
-    case "rgb":
-      return "rgb(" + r + ", " + g + ", " + b + ")";
-    case "hsl":
-      let [h, s, l] = rgbToHsl(rgb);
-      return "hsl(" + h + ", " + s + "%, " + l + "%)";
-    case "name":
-      let str;
-      try {
-        str = rgbToColorName(r, g, b);
-      } catch (e) {
-        str = hexString(rgb);
-      }
-      return str;
-    default:
-      return hexString(rgb);
-  }
-}
-
-/**
- * Produce a hex-formatted color string from rgb values.
- *
- * @param {array} rgb
- *        Rgb values of color to stringify
- *
- * @return {string}
- *        Hex formatted string for color, e.g. "#FFEE00"
- */
-function hexString([r, g, b]) {
-  let val = (1 << 24) + (r << 16) + (g << 8) + (b << 0);
-  return "#" + val.toString(16).substr(-6).toUpperCase();
-}
deleted file mode 100644
--- a/devtools/client/eyedropper/eyedropper.xul
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!-- 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/. -->
-
-<!DOCTYPE window []>
-
-<?xml-stylesheet href="chrome://devtools/skin/common.css" type="text/css"?>
-<?xml-stylesheet href="chrome://devtools/skin/eyedropper.css" type="text/css"?>
-
-<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        no-theme="true">
-  <script type="application/javascript;version=1.8"
-          src="chrome://devtools/content/shared/theme-switching.js"/>
-  <commandset id="eyedropper-commandset">
-    <command id="eyedropper-cmd-close"
-             oncommand="void(0);"/>
-    <command id="eyedropper-cmd-copy"
-             oncommand="void(0);"/>
-  </commandset>
-
-  <keyset id="eyedropper-keyset">
-    <key id="eyedropper-key-escape"
-         keycode="VK_ESCAPE"
-         command="eyedropper-cmd-close"/>
-    <key id="eyedropper-key-enter"
-         keycode="VK_RETURN"
-         command="eyedropper-cmd-copy"/>
-  </keyset>
-
-  <box id="canvas-overflow">
-    <canvas id="canvas" xmlns="http://www.w3.org/1999/xhtml" width="96" height="96">
-    </canvas>
-  </box>
-  <hbox id="color-value-container">
-    <hbox id="color-value-box">
-      <box id="color-preview">
-      </box>
-      <label id="color-value" class="devtools-monospace">
-      </label>
-    </hbox>
-  </hbox>
-</window>
\ No newline at end of file
deleted file mode 100644
--- a/devtools/client/eyedropper/moz.build
+++ /dev/null
@@ -1,13 +0,0 @@
-# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
-# vim: set filetype=python:
-# 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/.
-
-DevToolsModules(
-    'commands.js',
-    'eyedropper-child.js',
-    'eyedropper.js'
-)
-
-BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
deleted file mode 100644
--- a/devtools/client/eyedropper/nocursor.css
+++ /dev/null
@@ -1,3 +0,0 @@
-* {
-  cursor: none !important;
-}
\ No newline at end of file
deleted file mode 100644
--- a/devtools/client/eyedropper/test/.eslintrc
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  // Extend from the shared list of defined globals for mochitests.
-  "extends": "../../../.eslintrc.mochitests"
-}
deleted file mode 100644
--- a/devtools/client/eyedropper/test/browser.ini
+++ /dev/null
@@ -1,13 +0,0 @@
-[DEFAULT]
-tags = devtools
-subsuite = clipboard
-support-files =
-  color-block.html
-  head.js
-  !/devtools/client/commandline/test/helpers.js
-  !/devtools/client/framework/test/shared-head.js
-
-[browser_eyedropper_basic.js]
-skip-if = os == "win" && debug # bug 963492
-[browser_eyedropper_cmd.js]
-skip-if = true # bug 1278400
deleted file mode 100644
--- a/devtools/client/eyedropper/test/browser_eyedropper_basic.js
+++ /dev/null
@@ -1,80 +0,0 @@
-/* vim: set ts=2 et sw=2 tw=80: */
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-const TESTCASE_URI = CHROME_URL_ROOT + "color-block.html";
-const DIV_COLOR = "#0000FF";
-
-/**
- * Test basic eyedropper widget functionality:
- *  - Opening eyedropper and pressing ESC closes the eyedropper
- *  - Opening eyedropper and clicking copies the center color
- */
-add_task(function* () {
-  yield addTab(TESTCASE_URI);
-
-  info("added tab");
-
-  yield testEscape();
-
-  info("testing selecting a color");
-
-  yield testSelect();
-});
-
-function* testEscape() {
-  let dropper = new Eyedropper(window);
-
-  yield inspectPage(dropper, false);
-
-  let destroyed = dropper.once("destroy");
-  pressESC();
-  yield destroyed;
-
-  ok(true, "escape closed the eyedropper");
-}
-
-function* testSelect() {
-  let dropper = new Eyedropper(window);
-
-  let selected = dropper.once("select");
-  let copied = waitForClipboard(() => {}, DIV_COLOR);
-
-  yield inspectPage(dropper);
-
-  let color = yield selected;
-  is(color, DIV_COLOR, "correct color selected");
-
-  // wait for DIV_COLOR to be copied to the clipboard
-  yield copied;
-}
-
-/* Helpers */
-
-function* inspectPage(dropper, click = true) {
-  yield dropper.open();
-
-  info("dropper opened");
-
-  let target = document.documentElement;
-  let win = window;
-
-  // get location of the <div> in the content, offset from browser window
-  let box = gBrowser.selectedBrowser.getBoundingClientRect();
-  let x = box.left + 100;
-  let y = box.top + 100;
-
-  EventUtils.synthesizeMouse(target, x, y, { type: "mousemove" }, win);
-
-  yield dropperLoaded(dropper);
-
-  EventUtils.synthesizeMouse(target, x + 10, y + 10, { type: "mousemove" }, win);
-
-  if (click) {
-    EventUtils.synthesizeMouse(target, x + 10, y + 10, {}, win);
-  }
-}
-
-function pressESC() {
-  EventUtils.synthesizeKey("VK_ESCAPE", { });
-}
deleted file mode 100644
--- a/devtools/client/eyedropper/test/browser_eyedropper_cmd.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// Tests that the eyedropper command works
-
-const TESTCASE_URI = CHROME_URL_ROOT + "color-block.html";
-const DIV_COLOR = "#0000FF";
-
-function test() {
-  return Task.spawn(spawnTest).then(finish, helpers.handleError);
-}
-
-function* spawnTest() {
-  let options = yield helpers.openTab(TESTCASE_URI);
-  yield helpers.openToolbar(options);
-
-  yield helpers.audit(options, [
-    {
-      setup: "eyedropper",
-      check: {
-        input: "eyedropper"
-      },
-      exec: { output: "" }
-    },
-  ]);
-
-  yield inspectAndWaitForCopy();
-
-  yield helpers.closeToolbar(options);
-  yield helpers.closeTab(options);
-}
-
-function inspectAndWaitForCopy() {
-  let copied = waitForClipboard(() => {}, DIV_COLOR);
-  let ready = inspectPage(); // resolves once eyedropper is destroyed
-
-  return Promise.all([copied, ready]);
-}
-
-function inspectPage() {
-  let target = document.documentElement;
-  let win = window;
-
-  // get location of the <div> in the content, offset from browser window
-  let box = gBrowser.selectedBrowser.getBoundingClientRect();
-  let x = box.left + 100;
-  let y = box.top + 100;
-
-  let dropper = EyedropperManager.getInstance(window);
-
-  return dropperStarted(dropper).then(() => {
-    EventUtils.synthesizeMouse(target, x, y, { type: "mousemove" }, win);
-
-    return dropperLoaded(dropper).then(() => {
-      EventUtils.synthesizeMouse(target, x + 10, y + 10, { type: "mousemove" }, win);
-
-      EventUtils.synthesizeMouse(target, x + 10, y + 10, {}, win);
-      return dropper.once("destroy");
-    });
-  });
-}
deleted file mode 100644
--- a/devtools/client/eyedropper/test/color-block.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!doctype html>
-<html>
-<head>
-  <title>basic eyedropper test case</title>
-  <style type="text/css">
-  body {
-    background: #f99;
-  }
-
-  #test {
-    margin: 100px;
-    background-color: blue;
-    width: 20px;
-    height: 20px;
-  }
-  </style>
-</head>
-<body>
-  <div id="test">
-  </div>
-</body>
-</html>
deleted file mode 100644
--- a/devtools/client/eyedropper/test/head.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// shared-head.js handles imports, constants, and utility functions
-Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
-Services.scriptloader.loadSubScript(TEST_DIR + "../../../commandline/test/helpers.js", this);
-
-const { Eyedropper, EyedropperManager } = require("devtools/client/eyedropper/eyedropper");
-
-function waitForClipboard(setup, expected) {
-  let deferred = defer();
-  SimpleTest.waitForClipboard(expected, setup, deferred.resolve, deferred.reject);
-  return deferred.promise;
-}
-
-function dropperStarted(dropper) {
-  if (dropper.isStarted) {
-    return promise.resolve();
-  }
-  return dropper.once("started");
-}
-
-function dropperLoaded(dropper) {
-  if (dropper.loaded) {
-    return promise.resolve();
-  }
-  return dropper.once("load");
-}
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -88,17 +88,16 @@ const ToolboxButtons = exports.ToolboxBu
       return target.activeTab && target.activeTab.traits.frames;
     }
   },
   { id: "command-button-splitconsole",
     isTargetSupported: target => !target.isAddon },
   { id: "command-button-responsive" },
   { id: "command-button-paintflashing" },
   { id: "command-button-scratchpad" },
-  { id: "command-button-eyedropper" },
   { id: "command-button-screenshot" },
   { id: "command-button-rulers" },
   { id: "command-button-measure" },
   { id: "command-button-noautohide",
     isTargetSupported: target => target.chrome },
 ];
 
 /**
--- a/devtools/client/inspector/inspector-commands.js
+++ b/devtools/client/inspector/inspector-commands.js
@@ -1,17 +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/. */
 
 "use strict";
 
 const l10n = require("gcli/l10n");
-loader.lazyRequireGetter(this, "gDevTools",
-                         "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+const {EyeDropper, HighlighterEnvironment} = require("devtools/server/actors/highlighters");
+const Telemetry = require("devtools/client/shared/telemetry");
 
 exports.items = [{
   item: "command",
   runAt: "server",
   name: "inspect",
   description: l10n.lookup("inspectDesc"),
   manual: l10n.lookup("inspectManual"),
   params: [
@@ -23,9 +24,48 @@ exports.items = [{
     }
   ],
   exec: function (args, context) {
     let target = context.environment.target;
     return gDevTools.showToolbox(target, "inspector").then(toolbox => {
       toolbox.getCurrentPanel().selection.setNode(args.selector, "gcli");
     });
   }
+}, {
+  item: "command",
+  runAt: "client",
+  name: "eyedropper",
+  description: l10n.lookup("eyedropperDesc"),
+  manual: l10n.lookup("eyedropperManual"),
+  params: [{
+    // This hidden parameter is only set to true when the eyedropper browser menu item is
+    // used. It is useful to log a different telemetry event whether the tool was used
+    // from the menu, or from the gcli command line.
+    group: "hiddengroup",
+    params: [{
+      name: "frommenu",
+      type: "boolean",
+      hidden: true
+    }]
+  }],
+  exec: function (args, context) {
+    let telemetry = new Telemetry();
+    telemetry.toolOpened(args.frommenu ? "menueyedropper" : "eyedropper");
+    context.updateExec("eyedropper_server").catch(e => console.error(e));
+  }
+}, {
+  item: "command",
+  runAt: "server",
+  name: "eyedropper_server",
+  hidden: true,
+  exec: function (args, {environment}) {
+    let env = new HighlighterEnvironment();
+    env.initFromWindow(environment.window);
+    let eyeDropper = new EyeDropper(env);
+
+    eyeDropper.show(environment.document.documentElement, {copyOnSelect: true});
+
+    eyeDropper.once("hidden", () => {
+      eyeDropper.destroy();
+      env.destroy();
+    });
+  }
 }];
--- a/devtools/client/inspector/inspector-panel.js
+++ b/devtools/client/inspector/inspector-panel.js
@@ -15,16 +15,17 @@ var promise = require("promise");
 var defer = require("devtools/shared/defer");
 var EventEmitter = require("devtools/shared/event-emitter");
 var clipboard = require("sdk/clipboard");
 const {executeSoon} = require("devtools/shared/DevToolsUtils");
 var {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
 var {Task} = require("devtools/shared/task");
 const {initCssProperties} = require("devtools/shared/fronts/css-properties");
 const nodeConstants = require("devtools/shared/dom-node-constants");
+const Telemetry = require("devtools/client/shared/telemetry");
 
 const Menu = require("devtools/client/framework/menu");
 const MenuItem = require("devtools/client/framework/menu-item");
 
 loader.lazyRequireGetter(this, "CSS", "CSS");
 
 loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
 loader.lazyRequireGetter(this, "ComputedViewTool", "devtools/client/inspector/computed/computed", true);
@@ -84,36 +85,31 @@ loader.lazyGetter(this, "clipboardHelper
  */
 function InspectorPanel(iframeWindow, toolbox) {
   this._toolbox = toolbox;
   this._target = toolbox._target;
   this.panelDoc = iframeWindow.document;
   this.panelWin = iframeWindow;
   this.panelWin.inspector = this;
 
+  this.telemetry = new Telemetry();
+
   this.nodeMenuTriggerInfo = null;
 
   this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
   this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
   this.onNewRoot = this.onNewRoot.bind(this);
   this._onContextMenu = this._onContextMenu.bind(this);
   this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this);
   this.onNewSelection = this.onNewSelection.bind(this);
   this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this);
   this.onDetached = this.onDetached.bind(this);
   this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this);
   this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
 
-  let doc = this.panelDoc;
-
-  // Handle 'Add Node' toolbar button.
-  this.addNode = this.addNode.bind(this);
-  this.addNodeButton = doc.getElementById("inspector-element-add-button");
-  this.addNodeButton.addEventListener("click", this.addNode);
-
   this._target.on("will-navigate", this._onBeforeNavigate);
   this._detectingActorFeatures = this._detectActorFeatures();
 
   EventEmitter.decorate(this);
 }
 
 exports.InspectorPanel = InspectorPanel;
 
@@ -250,16 +246,17 @@ InspectorPanel.prototype = {
       this.markup.expandNode(this.selection.nodeFront);
 
       this.emit("ready");
       deferred.resolve(this);
     });
 
     this.setupSearchBox();
     this.setupSidebar();
+    this.setupToolbar();
 
     return deferred.promise;
   },
 
   _onBeforeNavigate: function () {
     this._defaultNode = null;
     this.selection.setNodeFront(null);
     this._destroyMarkup();
@@ -457,17 +454,16 @@ InspectorPanel.prototype = {
         "fontinspector",
         strings.GetStringFromName("inspector.sidebar.fontInspectorTitle"),
         defaultTab == "fontinspector");
 
       this.fontInspector = new FontInspector(this, this.panelWin);
       this.sidebar.toggleTab(true, "fontinspector");
     }
 
-    this.setupSidebarToggle();
     this.setupSidebarSize();
 
     this.sidebar.show(defaultTab);
   },
 
   /**
    * Sidebar size is currently driven by vbox.inspector-sidebar-container
    * element, which is located at the left/bottom side of the side bar splitter.
@@ -508,32 +504,60 @@ InspectorPanel.prototype = {
     this.sidebar.on("destroy", () => {
       Services.prefs.setIntPref("devtools.toolsidebar-width.inspector",
         sidePaneContainer.width);
       Services.prefs.setIntPref("devtools.toolsidebar-height.inspector",
         sidePaneContainer.height);
     });
   },
 
-  /**
-   * Add the expand/collapse behavior for the sidebar panel.
-   */
-  setupSidebarToggle: function () {
+  setupToolbar: function () {
+    // Setup the sidebar toggle button.
     let SidebarToggle = this.React.createFactory(this.browserRequire(
       "devtools/client/shared/components/sidebar-toggle"));
 
     let sidebarToggle = SidebarToggle({
       onClick: this.onPaneToggleButtonClicked,
       collapsed: false,
       expandPaneTitle: strings.GetStringFromName("inspector.expandPane"),
       collapsePaneTitle: strings.GetStringFromName("inspector.collapsePane"),
     });
 
     let parentBox = this.panelDoc.getElementById("inspector-sidebar-toggle-box");
     this._sidebarToggle = this.ReactDOM.render(sidebarToggle, parentBox);
+
+    // Setup the add-node button.
+    this.addNode = this.addNode.bind(this);
+    this.addNodeButton = this.panelDoc.getElementById("inspector-element-add-button");
+    this.addNodeButton.addEventListener("click", this.addNode);
+
+    // Setup the eye-dropper icon.
+    this.toolbox.target.actorHasMethod("inspector", "pickColorFromPage").then(value => {
+      if (!value) {
+        return;
+      }
+
+      this.onEyeDropperDone = this.onEyeDropperDone.bind(this);
+      this.onEyeDropperButtonClicked = this.onEyeDropperButtonClicked.bind(this);
+      this.eyeDropperButton = this.panelDoc.getElementById("inspector-eyedropper-toggle");
+      this.eyeDropperButton.style.display = "initial";
+      this.eyeDropperButton.addEventListener("click", this.onEyeDropperButtonClicked);
+    }, e => console.error(e));
+  },
+
+  teardownToolbar: function () {
+    if (this.addNodeButton) {
+      this.addNodeButton.removeEventListener("click", this.addNode);
+      this.addNodeButton = null;
+    }
+
+    if (this.eyeDropperButton) {
+      this.eyeDropperButton.removeEventListener("click", this.onEyeDropperButtonClicked);
+      this.eyeDropperButton = null;
+    }
   },
 
   /**
    * Reset the inspector on new root mutation.
    */
   onNewRoot: function () {
     this._defaultNode = null;
     this.selection.setNodeFront(null);
@@ -763,17 +787,17 @@ InspectorPanel.prototype = {
         front.destroy();
       }
     });
 
     this.sidebar.off("select", this._setDefaultSidebar);
     let sidebarDestroyer = this.sidebar.destroy();
     this.sidebar = null;
 
-    this.addNodeButton.removeEventListener("click", this.addNode);
+    this.teardownToolbar();
     this.breadcrumbs.destroy();
     this.selection.off("new-node-front", this.onNewSelection);
     this.selection.off("before-new-node", this.onBeforeNewSelection);
     this.selection.off("before-new-node-front", this.onBeforeNewSelection);
     this.selection.off("detached-front", this.onDetached);
     let markupDestroyer = this._destroyMarkup();
     this.panelWin.inspector = null;
     this.target = null;
@@ -1246,16 +1270,62 @@ InspectorPanel.prototype = {
     ViewHelpers.togglePane({
       visible: !isVisible,
       animated: true,
       delayed: true,
       callback: onAnimationDone
     }, sidePaneContainer);
   },
 
+  onEyeDropperButtonClicked: function () {
+    this.eyeDropperButton.hasAttribute("checked")
+      ? this.hideEyeDropper()
+      : this.showEyeDropper();
+  },
+
+  startEyeDropperListeners: function () {
+    this.inspector.once("color-pick-canceled", this.onEyeDropperDone);
+    this.inspector.once("color-picked", this.onEyeDropperDone);
+    this.walker.once("new-root", this.onEyeDropperDone);
+  },
+
+  stopEyeDropperListeners: function () {
+    this.inspector.off("color-pick-canceled", this.onEyeDropperDone);
+    this.inspector.off("color-picked", this.onEyeDropperDone);
+    this.walker.off("new-root", this.onEyeDropperDone);
+  },
+
+  onEyeDropperDone: function () {
+    this.eyeDropperButton.removeAttribute("checked");
+    this.stopEyeDropperListeners();
+  },
+
+  /**
+   * Show the eyedropper on the page.
+   * @return {Promise} resolves when the eyedropper is visible.
+   */
+  showEyeDropper: function () {
+    this.telemetry.toolOpened("toolbareyedropper");
+    this.eyeDropperButton.setAttribute("checked", "true");
+    this.startEyeDropperListeners();
+    return this.inspector.pickColorFromPage({copyOnSelect: true})
+                         .catch(e => console.error(e));
+  },
+
+  /**
+   * Hide the eyedropper.
+   * @return {Promise} resolves when the eyedropper is hidden.
+   */
+  hideEyeDropper: function () {
+    this.eyeDropperButton.removeAttribute("checked");
+    this.stopEyeDropperListeners();
+    return this.inspector.cancelPickColorFromPage()
+                         .catch(e => console.error(e));
+  },
+
   /**
    * Create a new node as the last child of the current selection, expand the
    * parent and select the new node.
    */
   addNode: Task.async(function* () {
     if (!this.canAddHTMLChild()) {
       return;
     }
--- a/devtools/client/inspector/inspector.xul
+++ b/devtools/client/inspector/inspector.xul
@@ -40,18 +40,21 @@
           class="devtools-button" />
         <html:div class="devtools-toolbar-spacer" />
         <html:span id="inspector-searchlabel" />
         <textbox id="inspector-searchbox"
           type="search"
           timeout="50"
           class="devtools-searchinput"
           placeholder="&inspectorSearchHTML.label3;"/>
+        <html:button id="inspector-eyedropper-toggle"
+          title="&inspectorEyeDropper.label;"
+          class="devtools-button command-button-invertable" />
         <div xmlns="http://www.w3.org/1999/xhtml"
-             id="inspector-sidebar-toggle-box" />
+          id="inspector-sidebar-toggle-box" />
       </html:div>
       <vbox flex="1" id="markup-box">
       </vbox>
       <html:div id="inspector-breadcrumbs-toolbar" class="devtools-toolbar">
         <html:div id="inspector-breadcrumbs" class="breadcrumbs-widget-container"/>
       </html:div>
     </vbox>
     <splitter class="devtools-side-splitter"/>
@@ -83,93 +86,97 @@
           <html:div id="pseudo-class-panel" hidden="true">
             <html:label><html:input id="pseudo-hover-toggle" type="checkbox" value=":hover" tabindex="-1" />:hover</html:label>
             <html:label><html:input id="pseudo-active-toggle" type="checkbox" value=":active" tabindex="-1" />:active</html:label>
             <html:label><html:input id="pseudo-focus-toggle" type="checkbox" value=":focus" tabindex="-1" />:focus</html:label>
         </html:div>
         </html:div>
 
         <html:div id="ruleview-container" class="ruleview">
+          <html:div id="ruleview-container-focusable" tabindex="-1">
+          </html:div>
         </html:div>
       </html:div>
 
       <html:div id="sidebar-panel-computedview" class="devtools-monospace theme-sidebar inspector-tabpanel">
-        <html:div class="devtools-toolbar">
-          <html:div class="devtools-searchbox">
-            <html:input id="computedview-searchbox"
-                        class="devtools-filterinput devtools-rule-searchbox"
-                        type="search"
-                        placeholder="&filterStylesPlaceholder;"/>
-            <html:button id="computedview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
-          </html:div>
-          <html:label id="browser-style-checkbox-label" for="browser-style-checkbox">
-            <html:input id="browser-style-checkbox"
-                        type="checkbox"
-                        class="includebrowserstyles"
-                        label="&browserStylesLabel;"/>&browserStylesLabel;</html:label>
-        </html:div>
+        <html:div id="computedview-container">
+          <html:div id="computedview-container-focusable" tabindex="-1">
+            <html:div id="layout-wrapper" tabindex="0">
+              <html:div id="layout-header">
+                <html:div id="layout-expander" class="expander theme-twisty expandable" open=""></html:div>
+                <html:span>&layoutViewTitle;</html:span>
+                <html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
+              </html:div>
 
-        <html:div id="computedview-container">
-          <html:div id="layout-wrapper" class="theme-separator" tabindex="0">
-            <html:div id="layout-header">
-              <html:div id="layout-expander" class="expander theme-twisty expandable" open=""></html:div>
-              <html:span>&layoutViewTitle;</html:span>
-              <html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
-            </html:div>
-
-            <html:div id="layout-container">
-              <html:div id="layout-main">
-                <html:span class="layout-legend" data-box="margin" title="&margin.tooltip;">&margin.tooltip;</html:span>
-                <html:div id="layout-margins" data-box="margin" title="&margin.tooltip;">
-                  <html:span class="layout-legend" data-box="border" title="&border.tooltip;">&border.tooltip;</html:span>
-                  <html:div id="layout-borders" data-box="border" title="&border.tooltip;">
-                    <html:span class="layout-legend" data-box="padding" title="&padding.tooltip;">&padding.tooltip;</html:span>
-                    <html:div id="layout-padding" data-box="padding" title="&padding.tooltip;">
-                      <html:div id="layout-content" data-box="content" title="&content.tooltip;">
+              <html:div id="layout-container">
+                <html:div id="layout-main">
+                  <html:span class="layout-legend" data-box="margin" title="&margin.tooltip;">&margin.tooltip;</html:span>
+                  <html:div id="layout-margins" data-box="margin" title="&margin.tooltip;">
+                    <html:span class="layout-legend" data-box="border" title="&border.tooltip;">&border.tooltip;</html:span>
+                    <html:div id="layout-borders" data-box="border" title="&border.tooltip;">
+                      <html:span class="layout-legend" data-box="padding" title="&padding.tooltip;">&padding.tooltip;</html:span>
+                      <html:div id="layout-padding" data-box="padding" title="&padding.tooltip;">
+                        <html:div id="layout-content" data-box="content" title="&content.tooltip;">
+                        </html:div>
                       </html:div>
                     </html:div>
                   </html:div>
+
+                  <html:p class="layout-margin layout-top"><html:span data-box="margin" class="layout-editable" title="margin-top"></html:span></html:p>
+                  <html:p class="layout-margin layout-right"><html:span data-box="margin" class="layout-editable" title="margin-right"></html:span></html:p>
+                  <html:p class="layout-margin layout-bottom"><html:span data-box="margin" class="layout-editable" title="margin-bottom"></html:span></html:p>
+                  <html:p class="layout-margin layout-left"><html:span data-box="margin" class="layout-editable" title="margin-left"></html:span></html:p>
+
+                  <html:p class="layout-border layout-top"><html:span data-box="border" class="layout-editable" title="border-top"></html:span></html:p>
+                  <html:p class="layout-border layout-right"><html:span data-box="border" class="layout-editable" title="border-right"></html:span></html:p>
+                  <html:p class="layout-border layout-bottom"><html:span data-box="border" class="layout-editable" title="border-bottom"></html:span></html:p>
+                  <html:p class="layout-border layout-left"><html:span data-box="border" class="layout-editable" title="border-left"></html:span></html:p>
+
+                  <html:p class="layout-padding layout-top"><html:span data-box="padding" class="layout-editable" title="padding-top"></html:span></html:p>
+                  <html:p class="layout-padding layout-right"><html:span data-box="padding" class="layout-editable" title="padding-right"></html:span></html:p>
+                  <html:p class="layout-padding layout-bottom"><html:span data-box="padding" class="layout-editable" title="padding-bottom"></html:span></html:p>
+                  <html:p class="layout-padding layout-left"><html:span data-box="padding" class="layout-editable" title="padding-left"></html:span></html:p>
+
+                  <html:p class="layout-size"><html:span data-box="content" title="&content.tooltip;"></html:span></html:p>
+                </html:div>
+
+                <html:div id="layout-info">
+                  <html:span id="layout-element-size"></html:span>
+                  <html:section id="layout-position-group">
+                    <html:span id="layout-element-position"></html:span>
+                  </html:section>
                 </html:div>
 
-                <html:p class="layout-margin layout-top"><html:span data-box="margin" class="layout-editable" title="margin-top"></html:span></html:p>
-                <html:p class="layout-margin layout-right"><html:span data-box="margin" class="layout-editable" title="margin-right"></html:span></html:p>
-                <html:p class="layout-margin layout-bottom"><html:span data-box="margin" class="layout-editable" title="margin-bottom"></html:span></html:p>
-                <html:p class="layout-margin layout-left"><html:span data-box="margin" class="layout-editable" title="margin-left"></html:span></html:p>
-
-                <html:p class="layout-border layout-top"><html:span data-box="border" class="layout-editable" title="border-top"></html:span></html:p>
-                <html:p class="layout-border layout-right"><html:span data-box="border" class="layout-editable" title="border-right"></html:span></html:p>
-                <html:p class="layout-border layout-bottom"><html:span data-box="border" class="layout-editable" title="border-bottom"></html:span></html:p>
-                <html:p class="layout-border layout-left"><html:span data-box="border" class="layout-editable" title="border-left"></html:span></html:p>
-
-                <html:p class="layout-padding layout-top"><html:span data-box="padding" class="layout-editable" title="padding-top"></html:span></html:p>
-                <html:p class="layout-padding layout-right"><html:span data-box="padding" class="layout-editable" title="padding-right"></html:span></html:p>
-                <html:p class="layout-padding layout-bottom"><html:span data-box="padding" class="layout-editable" title="padding-bottom"></html:span></html:p>
-                <html:p class="layout-padding layout-left"><html:span data-box="padding" class="layout-editable" title="padding-left"></html:span></html:p>
-
-                <html:p class="layout-size"><html:span data-box="content" title="&content.tooltip;"></html:span></html:p>
-              </html:div>
-
-              <html:div id="layout-info">
-                <html:span id="layout-element-size"></html:span>
-                <html:section id="layout-position-group">
-                  <html:span id="layout-element-position"></html:span>
-                </html:section>
-              </html:div>
-
-              <html:div style="display: none">
-                <html:p id="layout-dummy"></html:p>
+                <html:div style="display: none">
+                  <html:p id="layout-dummy"></html:p>
+                </html:div>
               </html:div>
             </html:div>
-          </html:div>
 
-          <html:div id="propertyContainer" class="theme-separator" tabindex="0">
-          </html:div>
+            <html:div id="computedview-toolbar" class="devtools-toolbar">
+              <html:div class="devtools-searchbox">
+                <html:input id="computedview-searchbox"
+                            class="devtools-filterinput devtools-rule-searchbox"
+                            type="search"
+                            placeholder="&filterStylesPlaceholder;"/>
+                <html:button id="computedview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
+              </html:div>
+              <html:label id="browser-style-checkbox-label" for="browser-style-checkbox">
+                <html:input id="browser-style-checkbox"
+                            type="checkbox"
+                            class="includebrowserstyles"
+                            label="&browserStylesLabel;"/>&browserStylesLabel;</html:label>
+            </html:div>
 
-          <html:div id="computedview-no-results" hidden="">
-            &noPropertiesFound;
+            <html:div id="propertyContainer" class="theme-separator" tabindex="0">
+            </html:div>
+
+            <html:div id="computedview-no-results" hidden="">
+              &noPropertiesFound;
+            </html:div>
           </html:div>
         </html:div>
       </html:div>
 
       <html:div id="sidebar-panel-fontinspector" class="devtools-monospace theme-sidebar inspector-tabpanel">
         <html:div class="devtools-toolbar">
           <html:div class="devtools-searchbox">
             <html:input id="font-preview-text-input"
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -169,17 +169,17 @@ function CssRuleView(inspector, document
   this._onFilterStyles = this._onFilterStyles.bind(this);
   this._onClearSearch = this._onClearSearch.bind(this);
   this._onFilterTextboxContextMenu =
     this._onFilterTextboxContextMenu.bind(this);
   this._onTogglePseudoClassPanel = this._onTogglePseudoClassPanel.bind(this);
   this._onTogglePseudoClass = this._onTogglePseudoClass.bind(this);
 
   let doc = this.styleDocument;
-  this.element = doc.getElementById("ruleview-container");
+  this.element = doc.getElementById("ruleview-container-focusable");
   this.addRuleButton = doc.getElementById("ruleview-add-rule-button");
   this.searchField = doc.getElementById("ruleview-searchbox");
   this.searchClearButton = doc.getElementById("ruleview-searchinput-clear");
   this.pseudoClassPanel = doc.getElementById("pseudo-class-panel");
   this.pseudoClassToggle = doc.getElementById("pseudo-class-panel-toggle");
   this.hoverCheckbox = doc.getElementById("pseudo-hover-toggle");
   this.activeCheckbox = doc.getElementById("pseudo-active-toggle");
   this.focusCheckbox = doc.getElementById("pseudo-focus-toggle");
--- a/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js
@@ -71,17 +71,17 @@ add_task(function* () {
   info("Enter new value and commit.");
   editor.input.value = newValue;
   EventUtils.synthesizeKey("VK_RETURN", {});
   yield onRuleViewChanged;
   ok(getRuleViewRule(view, newValue), "Rule with '" + newValue + " 'exists.");
 });
 
 function* clickOnRuleviewScrollbar(view) {
-  let container = view.element;
+  let container = view.element.parentNode;
   let onScroll = once(container, "scroll");
   let rect = container.getBoundingClientRect();
   // click 5 pixels before the bottom-right corner should hit the scrollbar
   EventUtils.synthesizeMouse(container, rect.width - 5, rect.height - 5,
     {}, view.styleWindow);
   yield onScroll;
 
   ok(true, "The rule view container scrolled after clicking on the scrollbar.");
--- a/devtools/client/inspector/rules/test/browser_rules_eyedropper.js
+++ b/devtools/client/inspector/rules/test/browser_rules_eyedropper.js
@@ -1,24 +1,15 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
-
 "use strict";
 
-// So we can test collecting telemetry on the eyedropper
-var oldCanRecord = Services.telemetry.canRecordExtended;
-Services.telemetry.canRecordExtended = true;
-registerCleanupFunction(function () {
-  Services.telemetry.canRecordExtended = oldCanRecord;
-});
-const EXPECTED_TELEMETRY = {
-  "DEVTOOLS_PICKER_EYEDROPPER_OPENED_COUNT": 2,
-  "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG": 1
-};
+// Test opening the eyedropper from the color picker. Pressing escape to close it, and
+// clicking the page to select a color.
 
 const TEST_URI = `
   <style type="text/css">
     body {
       background-color: white;
       padding: 0px
     }
 
@@ -38,151 +29,88 @@ const TEST_URI = `
   <body><div id="div1"></div><div id="div2"></div></body>
 `;
 
 // #f09
 const ORIGINAL_COLOR = "rgb(255, 0, 153)";
 // #ff5
 const EXPECTED_COLOR = "rgb(255, 255, 85)";
 
-// Test opening the eyedropper from the color picker. Pressing escape
-// to close it, and clicking the page to select a color.
-
 add_task(function* () {
-  // clear telemetry so we can get accurate counts
-  clearTelemetry();
-
+  info("Add the test tab, open the rule-view and select the test node");
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
-  let {inspector, view} = yield openRuleView();
+  let {testActor, inspector, view} = yield openRuleView();
   yield selectNode("#div2", inspector);
 
+  info("Get the background-color property from the rule-view");
   let property = getRuleViewProperty(view, "#div2", "background-color");
   let swatch = property.valueSpan.querySelector(".ruleview-colorswatch");
   ok(swatch, "Color swatch is displayed for the bg-color property");
 
-  let dropper = yield openEyedropper(view, swatch);
+  info("Open the eyedropper from the colorpicker tooltip");
+  yield openEyedropper(view, swatch);
 
   let tooltip = view.tooltips.colorPicker.tooltip;
-  ok(!tooltip.isVisible(),
-     "color picker tooltip is closed after opening eyedropper");
+  ok(!tooltip.isVisible(), "color picker tooltip is closed after opening eyedropper");
 
-  yield testESC(swatch, dropper);
+  info("Test that pressing escape dismisses the eyedropper");
+  yield testESC(swatch, inspector, testActor);
 
-  dropper = yield openEyedropper(view, swatch);
-
-  ok(dropper, "dropper opened");
+  info("Open the eyedropper again");
+  yield openEyedropper(view, swatch);
 
-  yield testSelect(view, swatch, dropper);
-
-  checkTelemetry();
+  info("Test that a color can be selected with the eyedropper");
+  yield testSelect(view, swatch, inspector, testActor);
 });
 
-function testESC(swatch, dropper) {
-  let deferred = defer();
-
-  dropper.once("destroy", () => {
-    let color = swatch.style.backgroundColor;
-    is(color, ORIGINAL_COLOR, "swatch didn't change after pressing ESC");
+function* testESC(swatch, inspector, testActor) {
+  info("Press escape");
+  let onCanceled = new Promise(resolve => {
+    inspector.inspector.once("color-pick-canceled", resolve);
+  });
+  yield testActor.synthesizeKey({key: "VK_ESCAPE", options: {}});
+  yield onCanceled;
 
-    deferred.resolve();
-  });
-
-  inspectPage(dropper, false).then(pressESC);
-
-  return deferred.promise;
+  let color = swatch.style.backgroundColor;
+  is(color, ORIGINAL_COLOR, "swatch didn't change after pressing ESC");
 }
 
-function* testSelect(view, swatch, dropper) {
-  let onDestroyed = dropper.once("destroy");
-  // the change to the content is done async after rule view change
+function* testSelect(view, swatch, inspector, testActor) {
+  info("Click at x:10px y:10px");
+  let onPicked = new Promise(resolve => {
+    inspector.inspector.once("color-picked", resolve);
+  });
+  // The change to the content is done async after rule view change
   let onRuleViewChanged = view.once("ruleview-changed");
 
-  inspectPage(dropper);
+  yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10,
+                                   options: {type: "mousemove"}});
+  yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10,
+                                   options: {type: "mousedown"}});
+  yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10,
+                                   options: {type: "mouseup"}});
 
-  yield onDestroyed;
+  yield onPicked;
   yield onRuleViewChanged;
 
   let color = swatch.style.backgroundColor;
   is(color, EXPECTED_COLOR, "swatch changed colors");
 
   is((yield getComputedStyleProperty("div", null, "background-color")),
      EXPECTED_COLOR,
      "div's color set to body color after dropper");
 }
 
-function clearTelemetry() {
-  for (let histogramId in EXPECTED_TELEMETRY) {
-    let histogram = Services.telemetry.getHistogramById(histogramId);
-    histogram.clear();
-  }
-}
-
-function checkTelemetry() {
-  for (let histogramId in EXPECTED_TELEMETRY) {
-    let expected = EXPECTED_TELEMETRY[histogramId];
-    let histogram = Services.telemetry.getHistogramById(histogramId);
-    let snapshot = histogram.snapshot();
-
-    is(snapshot.sum, expected,
-      "eyedropper telemetry value correct for " + histogramId);
-  }
-}
-
-/* Helpers */
-
-function openEyedropper(view, swatch) {
-  let deferred = defer();
-
+function* openEyedropper(view, swatch) {
   let tooltip = view.tooltips.colorPicker.tooltip;
 
-  tooltip.once("shown", () => {
-    let dropperButton = tooltip.doc.querySelector("#eyedropper-button");
-
-    tooltip.once("eyedropper-opened", (event, dropper) => {
-      deferred.resolve(dropper);
-    });
-    dropperButton.click();
-  });
-
+  info("Click on the swatch");
+  let onShown = tooltip.once("shown");
   swatch.click();
-  return deferred.promise;
-}
-
-function inspectPage(dropper, click = true) {
-  let target = document.documentElement;
-  let win = window;
-
-  // get location of the content, offset from browser window
-  let box = gBrowser.selectedBrowser.getBoundingClientRect();
-  let x = box.left + 1;
-  let y = box.top + 1;
-
-  return dropperStarted(dropper).then(() => {
-    EventUtils.synthesizeMouse(target, x, y, { type: "mousemove" }, win);
+  yield onShown;
 
-    return dropperLoaded(dropper).then(() => {
-      EventUtils.synthesizeMouse(target, x + 10, y + 10,
-        { type: "mousemove" }, win);
-
-      if (click) {
-        EventUtils.synthesizeMouse(target, x + 10, y + 10, {}, win);
-      }
-    });
-  });
-}
+  let dropperButton = tooltip.doc.querySelector("#eyedropper-button");
 
-function dropperStarted(dropper) {
-  if (dropper.isStarted) {
-    return promise.resolve();
-  }
-  return dropper.once("started");
+  info("Click on the eyedropper icon");
+  let onOpened = tooltip.once("eyedropper-opened");
+  dropperButton.click();
+  yield onOpened;
 }
-
-function dropperLoaded(dropper) {
-  if (dropper.loaded) {
-    return promise.resolve();
-  }
-  return dropper.once("load");
-}
-
-function pressESC() {
-  EventUtils.synthesizeKey("VK_ESCAPE", { });
-}
--- a/devtools/client/inspector/shared/style-inspector-overlays.js
+++ b/devtools/client/inspector/shared/style-inspector-overlays.js
@@ -288,17 +288,17 @@ TooltipsOverlay.prototype = {
     this.previewTooltip.startTogglingOnHover(this.view.element,
       this._onPreviewTooltipTargetHover.bind(this));
 
     // MDN CSS help tooltip
     this.cssDocs = new CssDocsTooltip(toolbox);
 
     if (this.isRuleView) {
       // Color picker tooltip
-      this.colorPicker = new SwatchColorPickerTooltip(toolbox);
+      this.colorPicker = new SwatchColorPickerTooltip(toolbox, this.view.inspector);
       // Cubic bezier tooltip
       this.cubicBezier = new SwatchCubicBezierTooltip(toolbox);
       // Filter editor tooltip
       this.filterEditor = new SwatchFilterTooltip(toolbox);
     }
 
     this._isStarted = true;
   },
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -58,16 +58,20 @@ skip-if = os == "mac" # Full keyboard na
 [browser_inspector_highlighter-02.js]
 [browser_inspector_highlighter-03.js]
 [browser_inspector_highlighter-04.js]
 [browser_inspector_highlighter-by-type.js]
 [browser_inspector_highlighter-comments.js]
 [browser_inspector_highlighter-csstransform_01.js]
 [browser_inspector_highlighter-csstransform_02.js]
 [browser_inspector_highlighter-embed.js]
+[browser_inspector_highlighter-eyedropper-clipboard.js]
+subsuite = clipboard
+[browser_inspector_highlighter-eyedropper-events.js]
+[browser_inspector_highlighter-eyedropper-show-hide.js]
 [browser_inspector_highlighter-geometry_01.js]
 [browser_inspector_highlighter-geometry_02.js]
 [browser_inspector_highlighter-geometry_03.js]
 [browser_inspector_highlighter-geometry_04.js]
 [browser_inspector_highlighter-geometry_05.js]
 [browser_inspector_highlighter-geometry_06.js]
 [browser_inspector_highlighter-hover_01.js]
 [browser_inspector_highlighter-hover_02.js]
@@ -129,13 +133,14 @@ skip-if = os == "mac" # Full keyboard na
 [browser_inspector_search-04.js]
 [browser_inspector_search-05.js]
 [browser_inspector_search-06.js]
 [browser_inspector_search-07.js]
 [browser_inspector_search-08.js]
 [browser_inspector_search_keyboard_trap.js]
 [browser_inspector_search-reserved.js]
 [browser_inspector_search-selection.js]
+[browser_inspector_search-sidebar.js]
 [browser_inspector_select-docshell.js]
 [browser_inspector_select-last-selected.js]
 [browser_inspector_search-navigation.js]
 [browser_inspector_sidebarstate.js]
 [browser_inspector_switch-to-inspector-on-pick.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js
@@ -0,0 +1,65 @@
+/* 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";
+
+// Test that the eyedropper can copy colors to the clipboard
+
+const HIGHLIGHTER_TYPE = "EyeDropper";
+const ID = "eye-dropper-";
+const TEST_URI = "data:text/html;charset=utf-8,<style>html{background:red}</style>";
+
+add_task(function* () {
+  let helper = yield openInspectorForURL(TEST_URI)
+               .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+  helper.prefix = ID;
+
+  let {show, synthesizeKey, finalize} = helper;
+
+  info("Show the eyedropper with the copyOnSelect option");
+  yield show("html", {copyOnSelect: true});
+
+  info("Make sure to wait until the eyedropper is done taking a screenshot of the page");
+  yield waitForElementAttributeSet("root", "drawn", helper);
+
+  yield waitForClipboard(() => {
+    info("Activate the eyedropper so the background color is copied");
+    let generateKey = synthesizeKey({key: "VK_RETURN", options: {}});
+    generateKey.next();
+  }, "#FF0000");
+
+  ok(true, "The clipboard contains the right value");
+
+  yield waitForElementAttributeRemoved("root", "drawn", helper);
+  yield waitForElementAttributeSet("root", "hidden", helper);
+  ok(true, "The eyedropper is now hidden");
+
+  finalize();
+});
+
+function* waitForElementAttributeSet(id, name, {getElementAttribute}) {
+  yield poll(function* () {
+    let value = yield getElementAttribute(id, name);
+    return !!value;
+  }, `Waiting for element ${id} to have attribute ${name} set`);
+}
+
+function* waitForElementAttributeRemoved(id, name, {getElementAttribute}) {
+  yield poll(function* () {
+    let value = yield getElementAttribute(id, name);
+    return !value;
+  }, `Waiting for element ${id} to have attribute ${name} removed`);
+}
+
+function* poll(check, desc) {
+  info(desc);
+
+  for (let i = 0; i < 10; i++) {
+    if (yield check()) {
+      return;
+    }
+    yield new Promise(resolve => setTimeout(resolve, 200));
+  }
+
+  throw new Error(`Timeout while: ${desc}`);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-events.js
@@ -0,0 +1,71 @@
+/* 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";
+
+// Test the eyedropper mouse and keyboard handling.
+
+const HIGHLIGHTER_TYPE = "EyeDropper";
+const ID = "eye-dropper-";
+
+const MOVE_EVENTS_DATA = [
+  {type: "mouse", x: 200, y: 100, expected: {x: 200, y: 100}},
+  {type: "mouse", x: 100, y: 200, expected: {x: 100, y: 200}},
+  {type: "keyboard", key: "VK_LEFT", expected: {x: 99, y: 200}},
+  {type: "keyboard", key: "VK_LEFT", shift: true, expected: {x: 89, y: 200}},
+  {type: "keyboard", key: "VK_RIGHT", expected: {x: 90, y: 200}},
+  {type: "keyboard", key: "VK_RIGHT", shift: true, expected: {x: 100, y: 200}},
+  {type: "keyboard", key: "VK_DOWN", expected: {x: 100, y: 201}},
+  {type: "keyboard", key: "VK_DOWN", shift: true, expected: {x: 100, y: 211}},
+  {type: "keyboard", key: "VK_UP", expected: {x: 100, y: 210}},
+  {type: "keyboard", key: "VK_UP", shift: true, expected: {x: 100, y: 200}},
+];
+
+add_task(function* () {
+  let helper = yield openInspectorForURL("data:text/html;charset=utf-8,eye-dropper test")
+               .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+  helper.prefix = ID;
+
+  yield helper.show("html");
+  yield respondsToMoveEvents(helper);
+  yield respondsToReturnAndEscape(helper);
+
+  helper.finalize();
+});
+
+function* respondsToMoveEvents(helper) {
+  info("Checking that the eyedropper responds to events from the mouse and keyboard");
+  let {mouse, synthesizeKey} = helper;
+
+  for (let {type, x, y, key, shift, expected} of MOVE_EVENTS_DATA) {
+    info(`Simulating a ${type} event to move to ${expected.x} ${expected.y}`);
+    if (type === "mouse") {
+      yield mouse.move(x, y);
+    } else if (type === "keyboard") {
+      let options = shift ? {shiftKey: true} : {};
+      yield synthesizeKey({key, options});
+    }
+    yield checkPosition(expected, helper);
+  }
+}
+
+function* checkPosition({x, y}, {getElementAttribute}) {
+  let style = yield getElementAttribute("root", "style");
+  is(style, `top:${y}px;left:${x}px;`,
+     `The eyedropper is at the expected ${x} ${y} position`);
+}
+
+function* respondsToReturnAndEscape({synthesizeKey, isElementHidden, show}) {
+  info("Simulating return to select the color and hide the eyedropper");
+
+  yield synthesizeKey({key: "VK_RETURN", options: {}});
+  let hidden = yield isElementHidden("root");
+  ok(hidden, "The eyedropper has been hidden");
+
+  info("Showing the eyedropper again and simulating escape to hide it");
+
+  yield show("html");
+  yield synthesizeKey({key: "VK_ESCAPE", options: {}});
+  hidden = yield isElementHidden("root");
+  ok(hidden, "The eyedropper has been hidden again");
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-show-hide.js
@@ -0,0 +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/. */
+"use strict";
+
+// Test the basic structure of the eye-dropper highlighter.
+
+const HIGHLIGHTER_TYPE = "EyeDropper";
+const ID = "eye-dropper-";
+
+add_task(function* () {
+  let helper = yield openInspectorForURL("data:text/html;charset=utf-8,eye-dropper test")
+               .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+  helper.prefix = ID;
+
+  yield isInitiallyHidden(helper);
+  yield canBeShownAndHidden(helper);
+
+  helper.finalize();
+});
+
+function* isInitiallyHidden({isElementHidden}) {
+  info("Checking that the eyedropper is hidden by default");
+
+  let hidden = yield isElementHidden("root");
+  ok(hidden, "The eyedropper is hidden by default");
+}
+
+function* canBeShownAndHidden({show, hide, isElementHidden, getElementAttribute}) {
+  info("Asking to show and hide the highlighter actually works");
+
+  yield show("html");
+  let hidden = yield isElementHidden("root");
+  ok(!hidden, "The eyedropper is now shown");
+
+  let style = yield getElementAttribute("root", "style");
+  is(style, "top:100px;left:100px;", "The eyedropper is correctly positioned");
+
+  yield hide();
+  hidden = yield isElementHidden("root");
+  ok(hidden, "The eyedropper is now hidden again");
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_search-sidebar.js
@@ -0,0 +1,74 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that depending where the user last clicked in the inspector, the right search
+// field is focused when ctrl+F is pressed.
+
+add_task(function* () {
+  let {inspector} = yield openInspectorForURL("data:text/html;charset=utf-8,Search!");
+
+  info("Check that by default, the inspector search field gets focused");
+  pressCtrlF();
+  isInInspectorSearchBox(inspector);
+
+  info("Click somewhere in the rule-view");
+  clickInRuleView(inspector);
+
+  info("Check that the rule-view search field gets focused");
+  pressCtrlF();
+  isInRuleViewSearchBox(inspector);
+
+  info("Click in the inspector again");
+  yield clickContainer("head", inspector);
+
+  info("Check that now we're back in the inspector, its search field gets focused");
+  pressCtrlF();
+  isInInspectorSearchBox(inspector);
+
+  info("Switch to the computed view, and click somewhere inside it");
+  selectComputedView(inspector);
+  clickInComputedView(inspector);
+
+  info("Check that the computed-view search field gets focused");
+  pressCtrlF();
+  isInComputedViewSearchBox(inspector);
+
+  info("Click in the inspector yet again");
+  yield clickContainer("body", inspector);
+
+  info("We're back in the inspector again, check the inspector search field focuses");
+  pressCtrlF();
+  isInInspectorSearchBox(inspector);
+});
+
+function pressCtrlF() {
+  EventUtils.synthesizeKey("f", {accelKey: true});
+}
+
+function clickInRuleView(inspector) {
+  let el = inspector.panelDoc.querySelector("#sidebar-panel-ruleview");
+  EventUtils.synthesizeMouseAtCenter(el, {}, inspector.panelDoc.defaultView);
+}
+
+function clickInComputedView(inspector) {
+  let el = inspector.panelDoc.querySelector("#sidebar-panel-computedview");
+  EventUtils.synthesizeMouseAtCenter(el, {}, inspector.panelDoc.defaultView);
+}
+
+function isInInspectorSearchBox(inspector) {
+  // Focus ends up in an anonymous child of the XUL textbox.
+  ok(inspector.panelDoc.activeElement.closest("#inspector-searchbox"),
+     "The inspector search field is focused when ctrl+F is pressed");
+}
+
+function isInRuleViewSearchBox(inspector) {
+  is(inspector.panelDoc.activeElement, inspector.ruleview.view.searchField,
+     "The rule-view search field is focused when ctrl+F is pressed");
+}
+
+function isInComputedViewSearchBox(inspector) {
+  is(inspector.panelDoc.activeElement, inspector.computedview.computedView.searchField,
+     "The computed-view search field is focused when ctrl+F is pressed");
+}
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -417,32 +417,33 @@ const getHighlighterHelperFor = (type) =
 
     // Highlighted node
     let highlightedNode = null;
 
     return {
       set prefix(value) {
         prefix = value;
       },
+
       get highlightedNode() {
         if (!highlightedNode) {
           return null;
         }
 
         return {
           getComputedStyle: function* (options = {}) {
             return yield inspector.pageStyle.getComputed(
               highlightedNode, options);
           }
         };
       },
 
-      show: function* (selector = ":root") {
+      show: function* (selector = ":root", options) {
         highlightedNode = yield getNodeFront(selector, inspector);
-        return yield highlighter.show(highlightedNode);
+        return yield highlighter.show(highlightedNode, options);
       },
 
       hide: function* () {
         yield highlighter.hide();
       },
 
       isElementHidden: function* (id) {
         return (yield testActor.getHighlighterNodeAttribute(
@@ -459,16 +460,20 @@ const getHighlighterHelperFor = (type) =
           prefix + id, name, highlighter);
       },
 
       synthesizeMouse: function* (options) {
         options = Object.assign({selector: ":root"}, options);
         yield testActor.synthesizeMouse(options);
       },
 
+      synthesizeKey: function* (options) {
+        yield testActor.synthesizeKey(options);
+      },
+
       // This object will synthesize any "mouse" prefixed event to the
       // `testActor`, using the name of method called as suffix for the
       // event's name.
       // If no x, y coords are given, the previous ones are used.
       //
       // For example:
       //   mouse.down(10, 20); // synthesize "mousedown" at 10,20
       //   mouse.move(20, 30); // synthesize "mousemove" at 20,30
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -126,19 +126,16 @@ devtools.jar:
     content/framework/connect/connect.xhtml (framework/connect/connect.xhtml)
     content/framework/connect/connect.css (framework/connect/connect.css)
     content/framework/connect/connect.js (framework/connect/connect.js)
     content/shared/widgets/graphs-frame.xhtml (shared/widgets/graphs-frame.xhtml)
     content/shared/widgets/cubic-bezier.css (shared/widgets/cubic-bezier.css)
     content/shared/widgets/mdn-docs.css (shared/widgets/mdn-docs.css)
     content/shared/widgets/filter-widget.css (shared/widgets/filter-widget.css)
     content/shared/widgets/spectrum.css (shared/widgets/spectrum.css)
-    content/eyedropper/eyedropper.xul (eyedropper/eyedropper.xul)
-    content/eyedropper/crosshairs.css (eyedropper/crosshairs.css)
-    content/eyedropper/nocursor.css (eyedropper/nocursor.css)
     content/aboutdebugging/aboutdebugging.xhtml (aboutdebugging/aboutdebugging.xhtml)
     content/aboutdebugging/aboutdebugging.css (aboutdebugging/aboutdebugging.css)
     content/aboutdebugging/initializer.js (aboutdebugging/initializer.js)
     content/responsive.html/index.xhtml (responsive.html/index.xhtml)
     content/responsive.html/index.js (responsive.html/index.js)
     content/dom/dom.html (dom/dom.html)
     content/dom/content/dom-view.css (dom/content/dom-view.css)
     content/dom/main.js (dom/main.js)
--- a/devtools/client/locales/en-US/inspector.dtd
+++ b/devtools/client/locales/en-US/inspector.dtd
@@ -10,8 +10,13 @@
      shown as the placeholder for the markup view search in the inspector. -->
 <!ENTITY inspectorSearchHTML.label3 "Search HTML">
 
 <!-- LOCALIZATION NOTE (inspectorAddNode.label): This is the label shown in
      the inspector toolbar for the button that lets users add elements to the
      DOM (as children of the currently selected element). -->
 <!ENTITY inspectorAddNode.label       "Create New Node">
 <!ENTITY inspectorAddNode.accesskey   "C">
+
+
+<!-- LOCALIZATION NOTE (inspectorEyeDropper.label): A string displayed as the tooltip of
+     a button in the inspector which toggles the Eyedropper tool -->
+<!ENTITY inspectorEyeDropper.label       "Grab a color from the page">
\ No newline at end of file
--- a/devtools/client/menus.js
+++ b/devtools/client/menus.js
@@ -32,17 +32,18 @@
  *   If true, the menuitem is prefixed by a checkbox and runtime code can
  *   toggle it.
  */
 
 const Services = require("Services");
 const isMac = Services.appinfo.OS === "Darwin";
 
 loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
-loader.lazyRequireGetter(this, "Eyedropper", "devtools/client/eyedropper/eyedropper", true);
+loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
+loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
 
 loader.lazyImporter(this, "BrowserToolboxProcess", "resource://devtools/client/framework/ToolboxProcess.jsm");
 loader.lazyImporter(this, "ResponsiveUIManager", "resource://devtools/client/responsivedesign/responsivedesign.jsm");
 loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
 
 exports.menuitems = [
   { id: "menu_devToolbox",
     l10nKey: "devToolboxMenuItem",
@@ -139,19 +140,23 @@ exports.menuitems = [
       keytext: true
     },
     checkbox: true
   },
   { id: "menu_eyedropper",
     l10nKey: "eyedropper",
     oncommand(event) {
       let window = event.target.ownerDocument.defaultView;
-      let eyedropper = new Eyedropper(window, { context: "menu",
-                                                copyOnSelect: true });
-      eyedropper.open();
+      let target = TargetFactory.forTab(window.gBrowser.selectedTab);
+
+      CommandUtils.createRequisition(target, {
+        environment: CommandUtils.createEnvironment({target})
+      }).then(requisition => {
+        requisition.updateExec("eyedropper --frommenu");
+      }, e => console.error(e));
     },
     checkbox: true
   },
   { id: "menu_scratchpad",
     l10nKey: "scratchpad",
     oncommand() {
       ScratchpadManager.openScratchpad();
     },
--- a/devtools/client/moz.build
+++ b/devtools/client/moz.build
@@ -8,17 +8,16 @@ include('../templates.mozbuild')
 
 DIRS += [
     'aboutdebugging',
     'animationinspector',
     'canvasdebugger',
     'commandline',
     'debugger',
     'dom',
-    'eyedropper',
     'framework',
     'inspector',
     'jsonview',
     'locales',
     'memory',
     'netmonitor',
     'performance',
     'preferences',
--- a/devtools/client/performance/modules/widgets/graphs.js
+++ b/devtools/client/performance/modules/widgets/graphs.js
@@ -13,17 +13,17 @@ const { Heritage } = require("devtools/c
 const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
 const BarGraphWidget = require("devtools/client/shared/widgets/BarGraphWidget");
 const MountainGraphWidget = require("devtools/client/shared/widgets/MountainGraphWidget");
 const { CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
 
 const promise = require("promise");
 const EventEmitter = require("devtools/shared/event-emitter");
 
-const { colorUtils } = require("devtools/client/shared/css-color");
+const { colorUtils } = require("devtools/shared/css-color");
 const { getColor } = require("devtools/client/shared/theme");
 const ProfilerGlobal = require("devtools/client/performance/modules/global");
 const { MarkersOverview } = require("devtools/client/performance/modules/widgets/markers-overview");
 const { createTierGraphDataFromFrameNode } = require("devtools/client/performance/modules/logic/jit");
 
 /**
  * For line graphs
  */
--- a/devtools/client/performance/modules/widgets/markers-overview.js
+++ b/devtools/client/performance/modules/widgets/markers-overview.js
@@ -8,17 +8,17 @@
  * the timeline data. Regions inside it may be selected, determining which
  * markers are visible in the "waterfall".
  */
 
 const { Cc, Ci, Cu, Cr } = require("chrome");
 const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
 const { AbstractCanvasGraph } = require("devtools/client/shared/widgets/Graphs");
 
-const { colorUtils } = require("devtools/client/shared/css-color");
+const { colorUtils } = require("devtools/shared/css-color");
 const { getColor } = require("devtools/client/shared/theme");
 const ProfilerGlobal = require("devtools/client/performance/modules/global");
 const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
 const { TickUtils } = require("devtools/client/performance/modules/widgets/waterfall-ticks");
 const { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
 
 const OVERVIEW_HEADER_HEIGHT = 14; // px
 const OVERVIEW_ROW_HEIGHT = 11; // px
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -25,30 +25,29 @@ pref("devtools.toolbar.visible", false);
 pref("devtools.webide.enabled", true);
 
 // Toolbox preferences
 pref("devtools.toolbox.footer.height", 250);
 pref("devtools.toolbox.sidebar.width", 500);
 pref("devtools.toolbox.host", "bottom");
 pref("devtools.toolbox.previousHost", "side");
 pref("devtools.toolbox.selectedTool", "webconsole");
-pref("devtools.toolbox.toolbarSpec", '["splitconsole", "paintflashing toggle","scratchpad","resize toggle","eyedropper","screenshot --fullpage", "rulers", "measure"]');
+pref("devtools.toolbox.toolbarSpec", '["splitconsole", "paintflashing toggle","scratchpad","resize toggle","screenshot --fullpage", "rulers", "measure"]');
 pref("devtools.toolbox.sideEnabled", true);
 pref("devtools.toolbox.zoomValue", "1");
 pref("devtools.toolbox.splitconsoleEnabled", false);
 pref("devtools.toolbox.splitconsoleHeight", 100);
 
 // Toolbox Button preferences
 pref("devtools.command-button-pick.enabled", true);
 pref("devtools.command-button-frames.enabled", true);
 pref("devtools.command-button-splitconsole.enabled", true);
 pref("devtools.command-button-paintflashing.enabled", false);
 pref("devtools.command-button-scratchpad.enabled", false);
 pref("devtools.command-button-responsive.enabled", true);
-pref("devtools.command-button-eyedropper.enabled", false);
 pref("devtools.command-button-screenshot.enabled", false);
 pref("devtools.command-button-rulers.enabled", false);
 pref("devtools.command-button-measure.enabled", false);
 pref("devtools.command-button-noautohide.enabled", false);
 
 // Inspector preferences
 // Enable the Inspector
 pref("devtools.inspector.enabled", true);
--- a/devtools/client/shared/components/reps/grip-array.js
+++ b/devtools/client/shared/components/reps/grip-array.js
@@ -30,18 +30,19 @@ define(function (require, exports, modul
       provider: React.PropTypes.object,
     },
 
     getLength: function (grip) {
       return grip.preview ? grip.preview.length : 0;
     },
 
     getTitle: function (object, context) {
-      if (this.props.objectLink) {
-        return this.props.objectLink({
+      let objectLink = this.props.objectLink || span;
+      if (this.props.mode != "tiny") {
+        return objectLink({
           object: object
         }, object.class);
       }
       return "";
     },
 
     arrayIterator: function (grip, max) {
       let items = [];
@@ -112,21 +113,22 @@ define(function (require, exports, modul
         let isEmpty = objectLength === 0;
         items = span({className: "length"}, isEmpty ? "" : objectLength);
       } else {
         let max = (mode == "short") ? 3 : 300;
         items = this.arrayIterator(object, max);
       }
 
       let objectLink = this.props.objectLink || span;
+      let title = this.getTitle(object);
 
       return (
         ObjectBox({
           className: "array"},
-          this.getTitle(object),
+          title,
           objectLink({
             className: "arrayLeftBracket",
             role: "presentation",
             object: object
           }, "["),
           items,
           objectLink({
             className: "arrayRightBracket",
--- a/devtools/client/shared/components/reps/moz.build
+++ b/devtools/client/shared/components/reps/moz.build
@@ -9,17 +9,16 @@ DevToolsModules(
     'attribute.js',
     'caption.js',
     'date-time.js',
     'document.js',
     'event.js',
     'function.js',
     'grip-array.js',
     'grip.js',
-    'named-node-map.js',
     'null.js',
     'number.js',
     'object-box.js',
     'object-link.js',
     'object-with-text.js',
     'object-with-url.js',
     'object.js',
     'prop-rep.js',
deleted file mode 100644
--- a/devtools/client/shared/components/reps/named-node-map.js
+++ /dev/null
@@ -1,180 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
-/* 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";
-
-// Make this available to both AMD and CJS environments
-define(function (require, exports, module) {
-  // ReactJS
-  const React = require("devtools/client/shared/vendor/react");
-
-  // Reps
-  const { createFactories, isGrip } = require("./rep-utils");
-  const { ObjectBox } = createFactories(require("./object-box"));
-  const { Caption } = createFactories(require("./caption"));
-
-  // Shortcuts
-  const { span } = React.DOM;
-
-  /**
-   * Used to render a map of values provided as a grip.
-   */
-  let NamedNodeMap = React.createClass({
-    displayName: "NamedNodeMap",
-
-    propTypes: {
-      object: React.PropTypes.object.isRequired,
-      mode: React.PropTypes.string,
-      provider: React.PropTypes.object,
-    },
-
-    getLength: function (object) {
-      return object.preview.length;
-    },
-
-    getTitle: function (object) {
-      if (this.props.objectLink && object.class) {
-        return this.props.objectLink({
-          object: object
-        }, object.class);
-      }
-      return object.class ? object.class : "";
-    },
-
-    getItems: function (array, max) {
-      let items = this.propIterator(array, max);
-
-      items = items.map(item => PropRep(item));
-
-      if (items.length > max + 1) {
-        items.pop();
-        let objectLink = this.props.objectLink || span;
-        items.push(Caption({
-          key: "more",
-          object: objectLink({
-            object: this.props.object
-          }, "more…")
-        }));
-      }
-
-      return items;
-    },
-
-    propIterator: function (grip, max) {
-      max = max || 3;
-
-      let props = [];
-
-      let provider = this.props.provider;
-      if (!provider) {
-        return props;
-      }
-
-      let ownProperties = grip.preview ? grip.preview.ownProperties : [];
-      for (let name in ownProperties) {
-        if (props.length > max) {
-          break;
-        }
-
-        let item = ownProperties[name];
-        let label = provider.getLabel(item);
-        let value = provider.getValue(item);
-
-        props.push(Object.assign({}, this.props, {
-          name: label,
-          object: value,
-          equal: ": ",
-          delim: ", ",
-        }));
-      }
-
-      return props;
-    },
-
-    render: function () {
-      let grip = this.props.object;
-      let mode = this.props.mode;
-
-      let items;
-      if (mode == "tiny") {
-        items = this.getLength(grip);
-      } else {
-        let max = (mode == "short") ? 3 : 100;
-        items = this.getItems(grip, max);
-      }
-
-      let objectLink = this.props.objectLink || span;
-
-      return (
-        ObjectBox({className: "NamedNodeMap"},
-          this.getTitle(grip),
-          objectLink({
-            className: "arrayLeftBracket",
-            role: "presentation",
-            object: grip
-          }, "["),
-          items,
-          objectLink({
-            className: "arrayRightBracket",
-            role: "presentation",
-            object: grip
-          }, "]")
-        )
-      );
-    },
-  });
-
-  /**
-   * Property for a grip object.
-   */
-  let PropRep = React.createFactory(React.createClass({
-    displayName: "PropRep",
-
-    propTypes: {
-      equal: React.PropTypes.string,
-      delim: React.PropTypes.string,
-    },
-
-    render: function () {
-      const { Rep } = createFactories(require("./rep"));
-
-      return (
-        span({},
-          span({
-            className: "nodeName"},
-            "$prop.name"
-          ),
-          span({
-            className: "objectEqual",
-            role: "presentation"},
-            this.props.equal
-          ),
-          Rep(this.props),
-          span({
-            className: "objectComma",
-            role: "presentation"},
-            this.props.delim
-          )
-        )
-      );
-    }
-  }));
-
-  // Registration
-
-  function supportsObject(grip, type) {
-    if (!isGrip(grip)) {
-      return false;
-    }
-
-    return (type == "NamedNodeMap" && grip.preview);
-  }
-
-  // Exports from this module
-  exports.NamedNodeMap = {
-    rep: NamedNodeMap,
-    supportsObject: supportsObject
-  };
-});
--- a/devtools/client/shared/components/reps/rep.js
+++ b/devtools/client/shared/components/reps/rep.js
@@ -22,17 +22,16 @@ define(function (require, exports, modul
   const { Obj } = require("./object");
 
   // DOM types (grips)
   const { Attribute } = require("./attribute");
   const { DateTime } = require("./date-time");
   const { Document } = require("./document");
   const { Event } = require("./event");
   const { Func } = require("./function");
-  const { NamedNodeMap } = require("./named-node-map");
   const { RegExp } = require("./regexp");
   const { StyleSheet } = require("./stylesheet");
   const { TextNode } = require("./text-node");
   const { Window } = require("./window");
   const { ObjectWithText } = require("./object-with-text");
   const { ObjectWithURL } = require("./object-with-url");
   const { GripArray } = require("./grip-array");
   const { Grip } = require("./grip");
@@ -41,17 +40,16 @@ define(function (require, exports, modul
   // XXX there should be a way for extensions to register a new
   // or modify an existing rep.
   let reps = [
     RegExp,
     StyleSheet,
     Event,
     DateTime,
     TextNode,
-    NamedNodeMap,
     Attribute,
     Func,
     ArrayRep,
     Document,
     Window,
     ObjectWithText,
     ObjectWithURL,
     GripArray,
--- a/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
@@ -27,33 +27,35 @@ window.onload = Task.async(function* () 
   try {
     yield testBasic();
 
     // Test property iterator
     yield testMaxProps();
     yield testMoreThanShortMaxProps();
     yield testMoreThanLongMaxProps();
     yield testRecursiveArray();
+
+    yield testNamedNodeMap();
   } catch(e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testBasic() {
     // Test array: `[]`
     const testName = "testBasic";
 
     // Test that correct rep is chosen
     const gripStub = getGripStub("testBasic");
     const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
     is(renderedRep.type, GripArray.rep, `Rep correctly selects ${GripArray.rep.displayName}`);
 
     // Test rendering
-    const defaultOutput = `[]`;
+    const defaultOutput = `Array[]`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
       },
       {
         mode: "tiny",
@@ -71,17 +73,17 @@ window.onload = Task.async(function* () 
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
   function testMaxProps() {
     // Test array: `[1, "foo", {}]`;
     const testName = "testMaxProps";
 
-    const defaultOutput = `[1, "foo", Object]`;
+    const defaultOutput = `Array[1, "foo", Object]`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
       },
       {
         mode: "tiny",
@@ -99,46 +101,46 @@ window.onload = Task.async(function* () 
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
   function testMoreThanShortMaxProps() {
     // Test array = `["test string"…] //4 items`
     const testName = "testMoreThanShortMaxProps";
 
-    const defaultOutput = `[${Array(maxLength.short).fill("\"test string\"").join(", ")}, more…]`;
+    const defaultOutput = `Array[${Array(maxLength.short).fill("\"test string\"").join(", ")}, more…]`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
       },
       {
         mode: "tiny",
         expectedOutput: `[${maxLength.short + 1}]`,
       },
       {
         mode: "short",
         expectedOutput: defaultOutput,
       },
       {
         mode: "long",
-        expectedOutput: `[${Array(maxLength.short + 1).fill("\"test string\"").join(", ")}]`,
+        expectedOutput: `Array[${Array(maxLength.short + 1).fill("\"test string\"").join(", ")}]`,
       }
     ];
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
   function testMoreThanLongMaxProps() {
     // Test array = `["test string"…] //301 items`
     const testName = "testMoreThanLongMaxProps";
 
-    const defaultShortOutput = `[${Array(maxLength.short).fill("\"test string\"").join(", ")}, more…]`;
-    const defaultLongOutput = `[${Array(maxLength.long).fill("\"test string\"").join(", ")}, more…]`;
+    const defaultShortOutput = `Array[${Array(maxLength.short).fill("\"test string\"").join(", ")}, more…]`;
+    const defaultLongOutput = `Array[${Array(maxLength.long).fill("\"test string\"").join(", ")}, more…]`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultShortOutput,
       },
       {
         mode: "tiny",
@@ -159,17 +161,17 @@ window.onload = Task.async(function* () 
 
   function testRecursiveArray() {
     // @TODO This is not how this feature should actually work
     // See Bug 1282465 - Reps: fix or remove recursive handling in grip-array
 
     // Test array = `let a = []; a = [a]`
     const testName = "testRecursiveArray";
 
-    const defaultOutput = `[[1]]`;
+    const defaultOutput = `Array[[1]]`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
       },
       {
         mode: "tiny",
@@ -183,16 +185,43 @@ window.onload = Task.async(function* () 
         mode: "long",
         expectedOutput: defaultOutput,
       }
     ];
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
+  function testNamedNodeMap() {
+    const testName = "testNamedNodeMap";
+
+    const defaultOutput = `NamedNodeMap[class="myclass", cellpadding="7", border="3"]`;
+
+    const modeTests = [
+      {
+        mode: undefined,
+        expectedOutput: defaultOutput,
+      },
+      {
+        mode: "tiny",
+        expectedOutput: `[3]`,
+      },
+      {
+        mode: "short",
+        expectedOutput: defaultOutput,
+      },
+      {
+        mode: "long",
+        expectedOutput: defaultOutput,
+      }
+    ];
+
+    testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+  }
+
   function getGripStub(functionName) {
     switch (functionName) {
       case "testBasic":
         return {
           "type": "object",
           "class": "Array",
           "actor": "server1.conn0.obj35",
           "extensible": true,
@@ -306,15 +335,77 @@ window.onload = Task.async(function* () 
                 "preview": {
                   "kind": "ArrayLike",
                   "length": 1
                 }
               }
             ]
           }
         };
+
+        case "testNamedNodeMap":
+          return {
+            "type": "object",
+            "class": "NamedNodeMap",
+            "actor": "server1.conn3.obj42",
+            "extensible": true,
+            "frozen": false,
+            "sealed": false,
+            "ownPropertyLength": 6,
+            "preview": {
+              "kind": "ArrayLike",
+              "length": 3,
+              "items": [
+                {
+                  "type": "object",
+                  "class": "Attr",
+                  "actor": "server1.conn3.obj43",
+                  "extensible": true,
+                  "frozen": false,
+                  "sealed": false,
+                  "ownPropertyLength": 0,
+                  "preview": {
+                    "kind": "DOMNode",
+                    "nodeType": 2,
+                    "nodeName": "class",
+                    "value": "myclass"
+                  }
+                },
+                {
+                  "type": "object",
+                  "class": "Attr",
+                  "actor": "server1.conn3.obj44",
+                  "extensible": true,
+                  "frozen": false,
+                  "sealed": false,
+                  "ownPropertyLength": 0,
+                  "preview": {
+                    "kind": "DOMNode",
+                    "nodeType": 2,
+                    "nodeName": "cellpadding",
+                    "value": "7"
+                  }
+                },
+                {
+                  "type": "object",
+                  "class": "Attr",
+                  "actor": "server1.conn3.obj44",
+                  "extensible": true,
+                  "frozen": false,
+                  "sealed": false,
+                  "ownPropertyLength": 0,
+                  "preview": {
+                    "kind": "DOMNode",
+                    "nodeType": 2,
+                    "nodeName": "border",
+                    "value": "3"
+                  }
+                }
+              ]
+            }
+          };
     }
   }
 });
 </script>
 </pre>
 </body>
 </html>
--- a/devtools/client/shared/moz.build
+++ b/devtools/client/shared/moz.build
@@ -15,18 +15,16 @@ DIRS += [
     'widgets',
 ]
 
 DevToolsModules(
     'AppCacheUtils.jsm',
     'autocomplete-popup.js',
     'browser-loader.js',
     'css-angle.js',
-    'css-color-db.js',
-    'css-color.js',
     'css-reload.js',
     'Curl.jsm',
     'demangle.js',
     'developer-toolbar.js',
     'devices.js',
     'devtools-file-watcher.js',
     'DOMHelpers.jsm',
     'doorhanger.js',
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -1,17 +1,17 @@
 /* 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 {Cc, Ci} = require("chrome");
 const {angleUtils} = require("devtools/client/shared/css-angle");
-const {colorUtils} = require("devtools/client/shared/css-color");
+const {colorUtils} = require("devtools/shared/css-color");
 const {getCSSLexer} = require("devtools/shared/css-lexer");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {
   ANGLE_TAKING_FUNCTIONS,
   BEZIER_KEYWORDS,
   COLOR_TAKING_FUNCTIONS,
   CSS_TYPES
 } = require("devtools/shared/css-properties-db");
--- a/devtools/client/shared/telemetry.js
+++ b/devtools/client/shared/telemetry.js
@@ -182,16 +182,20 @@ Telemetry.prototype = {
     menueyedropper: {
       histogram: "DEVTOOLS_MENU_EYEDROPPER_OPENED_COUNT",
       userHistogram: "DEVTOOLS_MENU_EYEDROPPER_OPENED_PER_USER_FLAG",
     },
     pickereyedropper: {
       histogram: "DEVTOOLS_PICKER_EYEDROPPER_OPENED_COUNT",
       userHistogram: "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG",
     },
+    toolbareyedropper: {
+      histogram: "DEVTOOLS_TOOLBAR_EYEDROPPER_OPENED_COUNT",
+      userHistogram: "DEVTOOLS_TOOLBAR_EYEDROPPER_OPENED_PER_USER_FLAG",
+    },
     developertoolbar: {
       histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_COUNT",
       userHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS"
     },
     aboutdebugging: {
       histogram: "DEVTOOLS_ABOUTDEBUGGING_OPENED_COUNT",
       userHistogram: "DEVTOOLS_ABOUTDEBUGGING_OPENED_PER_USER_FLAG",
--- a/devtools/client/shared/test/browser_css_color.js
+++ b/devtools/client/shared/test/browser_css_color.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_URI = "data:text/html;charset=utf-8,browser_css_color.js";
-var {colorUtils} = require("devtools/client/shared/css-color");
+var {colorUtils} = require("devtools/shared/css-color");
 var origColorUnit;
 
 add_task(function* () {
   yield addTab("about:blank");
   let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
   info("Creating a test canvas element to test colors");
   let canvas = createTestCanvas(doc);
--- a/devtools/client/shared/test/browser_tableWidget_mouse_interaction.js
+++ b/devtools/client/shared/test/browser_tableWidget_mouse_interaction.js
@@ -197,46 +197,50 @@ var testMouseInteraction = Task.async(fu
 
   is(table.menupopup.querySelectorAll("[disabled]").length, 1,
      "Only 1 menuitem is disabled");
   is(table.menupopup.querySelector("[disabled]"),
      table.menupopup.querySelector("[data-id='col1']"),
      "Which is the unique column");
   // popup should be open now
   // clicking on second column label
+  let onPopupHidden = once(table.menupopup, "popuphidden");
   event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
   node = table.menupopup.querySelector("[data-id='col2']");
   info("selecting to hide the second column");
   ok(!table.tbody.children[2].hasAttribute("hidden"),
      "Column is not hidden before hiding it");
   click(node);
   id = yield event;
+  yield onPopupHidden;
   is(id, "col2", "Correct column was triggered to be hidden");
   is(table.tbody.children[2].getAttribute("hidden"), "true",
      "Column is hidden after hiding it");
 
   // hiding third column
   // event listener for popupshown
   info("right clicking on the first column header");
   node = table.tbody.firstChild.firstChild.firstChild;
   onPopupShown = once(table.menupopup, "popupshown");
   click(node, 2);
   yield onPopupShown;
 
   is(table.menupopup.querySelectorAll("[disabled]").length, 1,
      "Only 1 menuitem is disabled");
   // popup should be open now
   // clicking on second column label
+  onPopupHidden = once(table.menupopup, "popuphidden");
   event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
   node = table.menupopup.querySelector("[data-id='col3']");
   info("selecting to hide the second column");
   ok(!table.tbody.children[4].hasAttribute("hidden"),
      "Column is not hidden before hiding it");
   click(node);
   id = yield event;
+  yield onPopupHidden;
   is(id, "col3", "Correct column was triggered to be hidden");
   is(table.tbody.children[4].getAttribute("hidden"), "true",
      "Column is hidden after hiding it");
 
   // opening again to see if 2 items are disabled now
   // event listener for popupshown
   info("right clicking on the first column header");
   node = table.tbody.firstChild.firstChild.firstChild;
@@ -251,44 +255,48 @@ var testMouseInteraction = Task.async(fu
      "First is the unique column");
   is(table.menupopup.querySelectorAll("[disabled]")[1],
      table.menupopup.querySelector("[data-id='col4']"),
      "Second is the last column");
 
   // showing back 2nd column
   // popup should be open now
   // clicking on second column label
+  onPopupHidden = once(table.menupopup, "popuphidden");
   event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
   node = table.menupopup.querySelector("[data-id='col2']");
   info("selecting to hide the second column");
   is(table.tbody.children[2].getAttribute("hidden"), "true",
      "Column is hidden before unhiding it");
   click(node);
   id = yield event;
+  yield onPopupHidden;
   is(id, "col2", "Correct column was triggered to be hidden");
   ok(!table.tbody.children[2].hasAttribute("hidden"),
      "Column is not hidden after unhiding it");
 
   // showing back 3rd column
   // event listener for popupshown
   info("right clicking on the first column header");
   node = table.tbody.firstChild.firstChild.firstChild;
   onPopupShown = once(table.menupopup, "popupshown");
   click(node, 2);
   yield onPopupShown;
 
   // popup should be open now
   // clicking on second column label
+  onPopupHidden = once(table.menupopup, "popuphidden");
   event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
   node = table.menupopup.querySelector("[data-id='col3']");
   info("selecting to hide the second column");
   is(table.tbody.children[4].getAttribute("hidden"), "true",
      "Column is hidden before unhiding it");
   click(node);
   id = yield event;
+  yield onPopupHidden;
   is(id, "col3", "Correct column was triggered to be hidden");
   ok(!table.tbody.children[4].hasAttribute("hidden"),
      "Column is not hidden after unhiding it");
 
   // reset table state
   table.clearSelection();
   table.sortBy("col1");
 });
--- a/devtools/client/shared/test/browser_telemetry_button_eyedropper.js
+++ b/devtools/client/shared/test/browser_telemetry_button_eyedropper.js
@@ -1,61 +1,55 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-const TEST_URI = "data:text/html;charset=utf-8," +
-  "<p>browser_telemetry_button_eyedropper.js</p><div>test</div>";
-
-var {EyedropperManager} = require("devtools/client/eyedropper/eyedropper");
-
-add_task(function* () {
-  yield addTab(TEST_URI);
-  let Telemetry = loadTelemetryAndRecordLogs();
-
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-  let toolbox = yield gDevTools.showToolbox(target, "inspector");
-  info("inspector opened");
-
-  info("testing the eyedropper button");
-  yield testButton(toolbox, Telemetry);
-
-  stopRecordingTelemetryLogs(Telemetry);
-  yield gDevTools.closeToolbox(target);
-  gBrowser.removeCurrentTab();
-});
-
-function* testButton(toolbox, Telemetry) {
-  let button = toolbox.doc.querySelector("#command-button-eyedropper");
-  ok(button, "Captain, we have the eyedropper button");
-
-  let clicked = toolbox._requisition.commandOutputManager.onOutput.once();
-
-  info("clicking the button to open the eyedropper");
-  button.click();
-
-  yield clicked;
-
-  checkResults("_EYEDROPPER_", Telemetry);
-}
-
-function checkResults(histIdFocus, Telemetry) {
-  let result = Telemetry.prototype.telemetryInfo;
-
-  for (let [histId, value] of Iterator(result)) {
-    if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
-        !histId.includes(histIdFocus)) {
-      // Inspector stats are tested in
-      // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
-      // because we only open the inspector once for this test.
-      continue;
-    }
-
-    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
-      ok(value.length === 1 && value[0] === true,
-         "Per user value " + histId + " has a single value of true");
-    } else if (histId.endsWith("OPENED_COUNT")) {
-      is(value.length, 1, histId + " has one entry");
-
-      let okay = value.every(element => element === true);
-      ok(okay, "All " + histId + " entries are === true");
-    }
-  }
-}
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+  "<p>browser_telemetry_button_eyedropper.js</p><div>test</div>";
+
+add_task(function* () {
+  yield addTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
+
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox = yield gDevTools.showToolbox(target, "inspector");
+  info("inspector opened");
+
+  info("testing the eyedropper button");
+  yield testButton(toolbox, Telemetry);
+
+  stopRecordingTelemetryLogs(Telemetry);
+  yield gDevTools.closeToolbox(target);
+  gBrowser.removeCurrentTab();
+});
+
+function* testButton(toolbox, Telemetry) {
+  info("Calling the eyedropper button's callback");
+  // We call the button callback directly because we don't need to test the UI here, we're
+  // only concerned about testing the telemetry probe.
+  yield toolbox.getPanel("inspector").showEyeDropper();
+
+  checkResults("_EYEDROPPER_", Telemetry);
+}
+
+function checkResults(histIdFocus, Telemetry) {
+  let result = Telemetry.prototype.telemetryInfo;
+
+  for (let [histId, value] of Iterator(result)) {
+    if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+        !histId.includes(histIdFocus)) {
+      // Inspector stats are tested in
+      // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+      // because we only open the inspector once for this test.
+      continue;
+    }
+
+    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+      ok(value.length === 1 && value[0] === true,
+         "Per user value " + histId + " has a single value of true");
+    } else if (histId.endsWith("OPENED_COUNT")) {
+      is(value.length, 1, histId + " has one entry");
+
+      let okay = value.every(element => element === true);
+      ok(okay, "All " + histId + " entries are === true");
+    }
+  }
+}
--- a/devtools/client/shared/test/unit/test_cssColor.js
+++ b/devtools/client/shared/test/unit/test_cssColor.js
@@ -5,17 +5,17 @@
 
 "use strict";
 
 var Cu = Components.utils;
 var Ci = Components.interfaces;
 var Cc = Components.classes;
 
 var {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
-const {colorUtils} = require("devtools/client/shared/css-color");
+const {colorUtils} = require("devtools/shared/css-color");
 
 loader.lazyGetter(this, "DOMUtils", function () {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
 
 const CLASSIFY_TESTS = [
   { input: "rgb(255,0,192)", output: "rgb" },
   { input: "RGB(255,0,192)", output: "rgb" },
--- a/devtools/client/shared/test/unit/test_cssColorDatabase.js
+++ b/devtools/client/shared/test/unit/test_cssColorDatabase.js
@@ -8,18 +8,18 @@
 var Cu = Components.utils;
 var Ci = Components.interfaces;
 var Cc = Components.classes;
 
 var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
 
 const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 
-const {colorUtils} = require("devtools/client/shared/css-color");
-const {cssColors} = require("devtools/client/shared/css-color-db");
+const {colorUtils} = require("devtools/shared/css-color");
+const {cssColors} = require("devtools/shared/css-color-db");
 
 function isValid(colorName) {
   ok(colorUtils.isValidCSSColor(colorName),
      colorName + " is valid in database");
   ok(DOMUtils.isValidCSSColor(colorName),
      colorName + " is valid in DOMUtils");
 }
 
--- a/devtools/client/shared/widgets/Tooltip.js
+++ b/devtools/client/shared/widgets/Tooltip.js
@@ -7,21 +7,18 @@
 const {Ci} = require("chrome");
 const defer = require("devtools/shared/defer");
 const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
 const {CubicBezierWidget} =
       require("devtools/client/shared/widgets/CubicBezierWidget");
 const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
 const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle");
 const EventEmitter = require("devtools/shared/event-emitter");
-const {colorUtils} = require("devtools/client/shared/css-color");
+const {colorUtils} = require("devtools/shared/css-color");
 const Heritage = require("sdk/core/heritage");
-const {Eyedropper} = require("devtools/client/eyedropper/eyedropper");
-const {gDevTools} = require("devtools/client/framework/devtools");
-const Services = require("Services");
 const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
 const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
 const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
 
 loader.lazyRequireGetter(this, "beautify", "devtools/shared/jsbeautify/beautify");
 loader.lazyRequireGetter(this, "setNamedTimeout", "devtools/client/shared/widgets/view-helpers", true);
 loader.lazyRequireGetter(this, "clearNamedTimeout", "devtools/client/shared/widgets/view-helpers", true);
 loader.lazyRequireGetter(this, "setNamedTimeout", "devtools/client/shared/widgets/view-helpers", true);
@@ -741,21 +738,25 @@ SwatchBasedEditorTooltip.prototype = {
  * The swatch color picker tooltip class is a specific class meant to be used
  * along with output-parser's generated color swatches.
  * It extends the parent SwatchBasedEditorTooltip class.
  * It just wraps a standard Tooltip and sets its content with an instance of a
  * color picker.
  *
  * @param {Toolbox} toolbox
  *        The devtools toolbox, needed to get the devtools main window.
+ * @param {InspectorPanel} inspector
+ *        The inspector panel, needed for the eyedropper.
  */
-function SwatchColorPickerTooltip(toolbox) {
+function SwatchColorPickerTooltip(toolbox, inspector) {
   let stylesheet = "chrome://devtools/content/shared/widgets/spectrum.css";
   SwatchBasedEditorTooltip.call(this, toolbox, stylesheet);
 
+  this.inspector = inspector;
+
   // Creating a spectrum instance. this.spectrum will always be a promise that
   // resolves to the spectrum instance
   this.spectrum = this.setColorPickerContent([0, 0, 0, 1]);
   this._onSpectrumColorChange = this._onSpectrumColorChange.bind(this);
   this._openEyeDropper = this._openEyeDropper.bind(this);
 }
 
 module.exports.SwatchColorPickerTooltip = SwatchColorPickerTooltip;
@@ -774,17 +775,17 @@ Heritage.extend(SwatchBasedEditorTooltip
     let spectrumNode = doc.createElementNS(XHTML_NS, "div");
     spectrumNode.id = "spectrum";
     container.appendChild(spectrumNode);
     let eyedropper = doc.createElementNS(XHTML_NS, "button");
     eyedropper.id = "eyedropper-button";
     eyedropper.className = "devtools-button";
     container.appendChild(eyedropper);
 
-    this.tooltip.setContent(container, { width: 210, height: 216 });
+    this.tooltip.setContent(container, { width: 218, height: 224 });
 
     let spectrum = new Spectrum(spectrumNode, color);
 
     // Wait for the tooltip to be shown before calling spectrum.show
     // as it expect to be visible in order to compute DOM element sizes.
     this.tooltip.once("shown", () => {
       spectrum.show();
     });
@@ -805,18 +806,26 @@ Heritage.extend(SwatchBasedEditorTooltip
       this._originalColor = this.currentSwatchColor.textContent;
       let color = this.activeSwatch.style.backgroundColor;
       this.spectrum.off("changed", this._onSpectrumColorChange);
       this.spectrum.rgb = this._colorToRgba(color);
       this.spectrum.on("changed", this._onSpectrumColorChange);
       this.spectrum.updateUI();
     }
 
-    let eyeButton = this.tooltip.doc.querySelector("#eyedropper-button");
-    eyeButton.addEventListener("click", this._openEyeDropper);
+    let {target} = this.inspector.toolbox;
+    target.actorHasMethod("inspector", "pickColorFromPage").then(value => {
+      let tooltipDoc = this.tooltip.doc;
+      let eyeButton = tooltipDoc.querySelector("#eyedropper-button");
+      if (value) {
+        eyeButton.addEventListener("click", this._openEyeDropper);
+      } else {
+        eyeButton.style.display = "none";
+      }
+    }, e => console.error(e));
   },
 
   _onSpectrumColorChange: function (event, rgba, cssColor) {
     this._selectColor(cssColor);
   },
 
   _selectColor: function (color) {
     if (this.activeSwatch) {
@@ -829,65 +838,53 @@ Heritage.extend(SwatchBasedEditorTooltip
 
       if (this.eyedropperOpen) {
         this.commit();
       }
     }
   },
 
   _openEyeDropper: function () {
-    let chromeWindow = this.tooltip.doc.defaultView.top;
-    let windowType = chromeWindow.document.documentElement
-                     .getAttribute("windowtype");
-    let toolboxWindow;
-    if (windowType != gDevTools.chromeWindowType) {
-      // this means the toolbox is in a seperate window. We need to make
-      // sure we'll be inspecting the browser window instead
-      toolboxWindow = chromeWindow;
-      chromeWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-      chromeWindow.focus();
-    }
-    let dropper = new Eyedropper(chromeWindow, { copyOnSelect: false,
-                                                 context: "picker" });
+    let {inspector, toolbox, telemetry} = this.inspector;
+    telemetry.toolOpened("pickereyedropper");
+    inspector.pickColorFromPage({copyOnSelect: false}).catch(e => console.error(e));
 
-    dropper.once("select", (event, color) => {
-      if (toolboxWindow) {
-        toolboxWindow.focus();
-      }
+    inspector.once("color-picked", color => {
+      toolbox.win.focus();
       this._selectColor(color);
     });
 
-    dropper.once("destroy", () => {
+    inspector.once("color-pick-canceled", () => {
       this.eyedropperOpen = false;
       this.activeSwatch = null;
     });
 
-    dropper.open();
     this.eyedropperOpen = true;
 
     // close the colorpicker tooltip so that only the eyedropper is open.
     this.hide();
 
-    this.tooltip.emit("eyedropper-opened", dropper);
+    this.tooltip.emit("eyedropper-opened");
   },
 
   _colorToRgba: function (color) {
     color = new colorUtils.CssColor(color);
     let rgba = color._getRGBATuple();
     return [rgba.r, rgba.g, rgba.b, rgba.a];
   },
 
   _toDefaultType: function (color) {
     let colorObj = new colorUtils.CssColor(color);
     colorObj.setAuthoredUnitFromColor(this._originalColor);
     return colorObj.toString();
   },
 
   destroy: function () {
     SwatchBasedEditorTooltip.prototype.destroy.call(this);
+    this.inspector = null;
     this.currentSwatchColor = null;
     this.spectrum.off("changed", this._onSpectrumColorChange);
     this.spectrum.destroy();
   }
 });
 
 /**
  * The swatch cubic-bezier tooltip class is a specific class meant to be used
--- a/devtools/client/shared/widgets/cubic-bezier.css
+++ b/devtools/client/shared/widgets/cubic-bezier.css
@@ -2,21 +2,22 @@
  * 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/. */
 
 /* Based on Lea Verou www.cubic-bezier.com
    See https://github.com/LeaVerou/cubic-bezier */
 
 .cubic-bezier-container {
   display: flex;
-  width: 500px;
+  width: 510px;
   height: 370px;
   flex-direction: row-reverse;
   overflow: hidden;
   padding: 5px;
+  box-sizing: border-box;
 }
 
 .display-wrap {
   width: 50%;
   height: 100%;
   text-align: center;
   overflow: hidden;
 }
@@ -25,61 +26,62 @@
 
 .coordinate-plane {
   width: 150px;
   height: 370px;
   margin: 0 auto;
   position: relative;
 }
 
-.theme-dark .coordinate-plane:before,
-.theme-dark .coordinate-plane:after {
-  border-color: #eee;
-}
-
 .control-point {
   position: absolute;
   z-index: 1;
   height: 10px;
   width: 10px;
   border: 0;
   background: #666;
   display: block;
   margin: -5px 0 0 -5px;
   outline: none;
   border-radius: 5px;
   padding: 0;
   cursor: pointer;
 }
 
 .display-wrap {
-  background: repeating-linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.05) 0, rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 15px) no-repeat, repeating-linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.05) 0, rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 15px) no-repeat;
+  background:
+  repeating-linear-gradient(0deg,
+    transparent,
+    var(--bezier-grid-color) 0,
+    var(--bezier-grid-color) 1px,
+    transparent 1px,
+    transparent 15px) no-repeat,
+  repeating-linear-gradient(90deg,
+    transparent,
+    var(--bezier-grid-color) 0,
+    var(--bezier-grid-color) 1px,
+    transparent 1px,
+    transparent 15px) no-repeat;
   background-size: 100% 100%, 100% 100%;
   background-position: -2px 5px, -2px 5px;
 
   -moz-user-select: none;
 }
 
-.theme-dark .display-wrap {
-  background: repeating-linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 15px) no-repeat, repeating-linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 15px) no-repeat;
-  background-size: 100% 100%, 100% 100%;
-  background-position: -2px 5px, -2px 5px;
-
-  -moz-user-select: none;
-}
 canvas.curve {
-  background: linear-gradient(-45deg, transparent 49.7%, rgba(0,0,0,.2) 49.7%, rgba(0,0,0,.2) 50.3%, transparent 50.3%) center no-repeat;
+  background:
+    linear-gradient(-45deg,
+      transparent 49.7%,
+      var(--bezier-diagonal-color) 49.7%,
+      var(--bezier-diagonal-color) 50.3%,
+      transparent 50.3%) center no-repeat;
   background-size: 100% 100%;
   background-position: 0 0;
 }
 
-.theme-dark canvas.curve {
-  background: linear-gradient(-45deg, transparent 49.7%, #eee 49.7%, #eee 50.3%, transparent 50.3%) center no-repeat;
-}
-
 /* Timing Function Preview Widget */
 
 .timing-function-preview {
   position: absolute;
   bottom: 20px;
   right: 45px;
   width: 150px;
 }
@@ -179,26 +181,22 @@ canvas.curve {
   cursor: pointer;
   width: 33.33%;
   margin: 5px 0px;
   text-align: center;
 }
 
 .preset canvas {
   display: block;
-  border: 1px solid #ccc;
+  border: 1px solid var(--theme-splitter-color);
   border-radius: 3px;
   background-color: var(--theme-body-background);
   margin: 0 auto;
 }
 
-.theme-dark .preset canvas {
-  border-color: #444e58;
-}
-
 .preset p {
   font-size: 80%;
   margin: 2px auto 0px auto;
   color: var(--theme-body-color-alt);
   text-transform: capitalize;
   text-overflow: ellipsis;
   overflow: hidden;
 }
@@ -206,13 +204,13 @@ canvas.curve {
 .active-preset p, .active-preset:hover p {
   color: var(--theme-body-color);
 }
 
 .preset:hover canvas {
   border-color: var(--theme-selection-background);
 }
 
-.active-preset canvas, .active-preset:hover canvas,
-.theme-dark .active-preset canvas, .theme-dark .preset:hover canvas {
+.active-preset canvas,
+.active-preset:hover canvas {
   background-color: var(--theme-selection-background-semitransparent);
   border-color: var(--theme-selection-background);
 }
--- a/devtools/client/shared/widgets/filter-widget.css
+++ b/devtools/client/shared/widgets/filter-widget.css
@@ -1,19 +1,21 @@
 /* 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/. */
 
 /* Main container: Displays the filters and presets in 2 columns */
 
 #filter-container {
-  height: 100%;
+  width: 510px;
+  height: 200px;
   display: flex;
   position: relative;
   padding: 5px;
+  box-sizing: border-box;
   /* when opened in a xul:panel, a gray color is applied to text */
   color: var(--theme-body-color);
 }
 
 #filter-container.dragging {
   -moz-user-select: none;
 }
 
@@ -133,22 +135,16 @@
 }
 
 /* Fix the size of inputs */
 /* Especially needed on Linux where input are bigger */
 input {
   width: 8em;
 }
 
-.theme-light .add,
-.theme-light .remove-button,
-.theme-light #toggle-presets {
-  filter: invert(1);
-}
-
 .preset {
   display: flex;
   margin-bottom: 10px;
   cursor: pointer;
   padding: 3px 5px;
 
   flex-direction: row;
   flex-wrap: wrap;
@@ -169,20 +165,16 @@ input {
 .preset:hover {
   background: var(--theme-selection-background);
 }
 
 .preset:hover label, .preset:hover span {
   color: var(--theme-selection-color);
 }
 
-.theme-light .preset:hover .remove-button {
-  filter: invert(0);
-}
-
 .preset .remove-button {
   order: 2;
 }
 
 .preset span {
   flex: 2 100%;
   white-space: nowrap;
   overflow: hidden;
@@ -233,11 +225,17 @@ input {
 .add {
   background: url(chrome://devtools/skin/images/add.svg);
 }
 
 #toggle-presets {
   background: url(chrome://devtools/skin/images/pseudo-class.svg);
 }
 
+.add,
+.remove-button,
+#toggle-presets {
+  filter: var(--icon-filter);
+}
+
 .show-presets #toggle-presets {
   filter: url(chrome://devtools/skin/images/filters.svg#checked-icon-state);
 }
--- a/devtools/client/shared/widgets/spectrum.css
+++ b/devtools/client/shared/widgets/spectrum.css
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #eyedropper-button {
   margin-inline-start: 5px;
   display: block;
 }
 
 #eyedropper-button::before {
-  background-image: url("chrome://devtools/skin/images/command-eyedropper.svg");
+  background-image: url(chrome://devtools/skin/images/command-eyedropper.svg);
 }
 
 /* Mix-in classes */
 
 .spectrum-checker {
   background-color: #eee;
   background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
     linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
@@ -32,16 +32,20 @@
 .spectrum-box {
   border: 1px solid rgba(0,0,0,0.2);
   border-radius: 2px;
   background-clip: content-box;
 }
 
 /* Elements */
 
+#spectrum-tooltip {
+  padding: 4px;
+}
+
 .spectrum-container {
   position: relative;
   display: none;
   top: 0;
   left: 0;
   border-radius: 0;
   width: 200px;
   padding: 5px;
--- a/devtools/client/sourceeditor/debugger.js
+++ b/devtools/client/sourceeditor/debugger.js
@@ -185,17 +185,25 @@ function addBreakpoint(ctx, line, cond) 
 function removeBreakpoints(ctx) {
   let { ed, cm } = ctx;
 
   let meta = dbginfo.get(ed);
   if (meta.breakpoints != null) {
     meta.breakpoints = {};
   }
 
-  cm.doc.iter((line) => { removeBreakpoint(ctx, line); });
+  cm.doc.iter((line) => {
+    // The hasBreakpoint is a slow operation: checks the line type, whether cm
+    // is initialized and creates several new objects. Inlining the line's
+    // wrapClass property check directly.
+    if (line.wrapClass == null || !line.wrapClass.includes("breakpoint")) {
+      return;
+    }
+    removeBreakpoint(ctx, line);
+  });
 }
 
 /**
  * Removes a visual breakpoint from a specified line and
  * makes Editor emit a breakpointRemoved event.
  */
 function removeBreakpoint(ctx, line) {
   if (!hasBreakpoint(ctx, line)) {
--- a/devtools/client/themes/computed.css
+++ b/devtools/client/themes/computed.css
@@ -6,17 +6,32 @@
 #sidebar-panel-computedview {
   margin: 0;
   display : flex;
   flex-direction: column;
   width: 100%;
   height: 100%;
 }
 
-#sidebar-panel-computedview > .devtools-toolbar {
+#computedview-container {
+  overflow: auto;
+  height: 100%;
+}
+
+/* This extra wrapper only serves as a way to get the content of the view focusable.
+   So that when the user reaches it either via keyboard or mouse, we know that the view
+   is focused and therefore can handle shortcuts.
+   However, for accessibility reasons, tabindex is set to -1 to avoid having to tab
+   through it, and the outline is hidden. */
+#computedview-container-focusable {
+  height: 100%;
+  outline: none;
+}
+
+#computedview-toolbar {
   display: flex;
 }
 
 #browser-style-checkbox {
   /* Bug 1200073 - extra space before the browser styles checkbox so
      they aren't squished together in a small window. Put also
      an extra space after. */
   margin-inline-start: 5px;
@@ -27,27 +42,21 @@
   margin-right: 5px;
 
   /* Vertically center the 'Browser styles' checkbox in the
      Computed panel with its label. */
   display: flex;
   align-items: center;
 }
 
-#computedview-container {
-  overflow: auto;
-}
-
 #propertyContainer {
   -moz-user-select: text;
   overflow-y: auto;
   overflow-x: hidden;
   flex: auto;
-  border-top-width: 1px;
-  border-top-style: dotted;
 }
 
 .row-striped {
   background: var(--theme-body-background);
 }
 
 .property-view-hidden,
 .property-content-hidden {
--- a/devtools/client/themes/inspector.css
+++ b/devtools/client/themes/inspector.css
@@ -1,13 +1,21 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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 {
+  --eyedropper-image: url(images/command-eyedropper.svg);
+}
+
+.theme-firebug {
+  --eyedropper-image: url(images/firebug/command-eyedropper.svg);
+}
+
 /* Use flex layout for the Inspector toolbar. For now, it's done
    specifically for the Inspector toolbar since general rule applied
    on .devtools-toolbar breaks breadcrubs and also toolbars in other
    panels (e.g. webconsole, debugger), these are not ready for HTML
    layout yet. */
 #inspector-toolbar.devtools-toolbar {
   display: flex;
 }
@@ -73,16 +81,27 @@
 }
 
 #inspector-breadcrumbs .breadcrumbs-widget-item {
   white-space: nowrap;
   flex-shrink: 0;
   font: message-box;
 }
 
+/* Eyedropper toolbar button */
+
+#inspector-eyedropper-toggle {
+  /* hidden by default, until we can check that the required highlighter exists */
+  display: none;
+}
+
+#inspector-eyedropper-toggle::before {
+  background-image: var(--eyedropper-image);
+}
+
 /* Add element toolbar button */
 #inspector-element-add-button::before {
   background-image: url("chrome://devtools/skin/images/add.svg");
   list-style-image: url("chrome://devtools/skin/images/add.svg");
   -moz-user-focus: normal;
 }
 
 /* "no results" warning message displayed in the ruleview and in the computed view */
--- a/devtools/client/themes/layout.css
+++ b/devtools/client/themes/layout.css
@@ -1,12 +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/ */
 
+#layout-wrapper {
+  border-bottom-style: solid;
+  border-bottom-width: 1px;
+  border-color: var(--theme-splitter-color);
+}
+
 #layout-container {
   /* The view will grow bigger as the window gets resized, until 400px */
   max-width: 400px;
   margin: 0px auto;
   padding: 0;
 }
 
 /* Header */
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -218,17 +218,16 @@
 .requests-menu-security-and-domain {
   width: 14vw;
 }
 
 .requests-security-state-icon {
   width: 16px;
   height: 16px;
   margin-inline-end: 4px;
-  cursor: pointer;
 }
 
 .security-state-insecure {
   list-style-image: url(chrome://devtools/skin/images/security-state-insecure.svg);
 }
 
 .security-state-secure {
   list-style-image: url(chrome://devtools/skin/images/security-state-secure.svg);
@@ -264,17 +263,16 @@
   font-size: 8px;
   font-weight: bold;
   line-height: 10px;
   border-radius: 3px;
   padding: 0 2px;
   margin: 0;
   margin-inline-end: 3px;
   -moz-user-select: none;
-  cursor: pointer;
 }
 
 .requests-menu-transferred {
   max-width: 8em;
   text-align: center;
   width: 8vw;
 }
 
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -77,16 +77,26 @@
 
 #ruleview-container {
   -moz-user-select: text;
   overflow: auto;
   flex: auto;
   height: 100%;
 }
 
+/* This extra wrapper only serves as a way to get the content of the view focusable.
+   So that when the user reaches it either via keyboard or mouse, we know that the view
+   is focused and therefore can handle shortcuts.
+   However, for accessibility reasons, tabindex is set to -1 to avoid having to tab
+   through it, and the outline is hidden. */
+#ruleview-container-focusable {
+  height: 100%;
+  outline: none;
+}
+
 #ruleview-container.non-interactive {
   pointer-events: none;
   visibility: collapse;
   transition: visibility 0.25s;
 }
 
 .ruleview-code {
   direction: ltr;
--- a/devtools/client/themes/toolbox.css
+++ b/devtools/client/themes/toolbox.css
@@ -12,17 +12,16 @@
   --command-paintflashing-image: url(images/command-paintflashing.svg);
   --command-screenshot-image: url(images/command-screenshot.svg);
   --command-responsive-image: url(images/command-responsivemode.svg);
   --command-scratchpad-image: url(images/command-scratchpad.svg);
   --command-pick-image: url(images/command-pick.svg);
   --command-frames-image: url(images/command-frames.svg);
   --command-splitconsole-image: url(images/command-console.svg);
   --command-noautohide-image: url(images/command-noautohide.svg);
-  --command-eyedropper-image: url(images/command-eyedropper.svg);
   --command-rulers-image: url(images/command-rulers.svg);
   --command-measure-image: url(images/command-measure.svg);
 }
 
 .theme-firebug {
   --close-button-image: url(chrome://devtools/skin/images/firebug/close.svg);
   --dock-bottom-image: url(chrome://devtools/skin/images/firebug/dock-bottom.svg);
   --dock-side-image: url(chrome://devtools/skin/images/firebug/dock-side.svg);
@@ -31,17 +30,16 @@
   --command-paintflashing-image: url(images/firebug/command-paintflashing.svg);
   --command-screenshot-image: url(images/firebug/command-screenshot.svg);
   --command-responsive-image: url(images/firebug/command-responsivemode.svg);
   --command-scratchpad-image: url(images/firebug/command-scratchpad.svg);
   --command-pick-image: url(images/firebug/command-pick.svg);
   --command-frames-image: url(images/firebug/command-frames.svg);
   --command-splitconsole-image: url(images/firebug/command-console.svg);
   --command-noautohide-image: url(images/firebug/command-noautohide.svg);
-  --command-eyedropper-image: url(images/firebug/command-eyedropper.svg);
   --command-rulers-image: url(images/firebug/command-rulers.svg);
   --command-measure-image: url(images/firebug/command-measure.svg);
 }
 
 /* Toolbox tabbar */
 
 .devtools-tabbar {
   -moz-appearance: none;
--- a/devtools/client/themes/tooltips.css
+++ b/devtools/client/themes/tooltips.css
@@ -1,13 +1,25 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
+/* Tooltip specific theme variables */
+
+.theme-dark {
+  --bezier-diagonal-color: #eee;
+  --bezier-grid-color: rgba(0, 0, 0, 0.2);
+}
+
+.theme-light {
+  --bezier-diagonal-color: rgba(0, 0, 0, 0.2);
+  --bezier-grid-color: rgba(0, 0, 0, 0.05);
+}
+
 /* Tooltip widget (see devtools/client/shared/widgets/Tooltip.js) */
 
 .devtools-tooltip .panel-arrowcontent {
   padding: 4px;
 }
 
 .devtools-tooltip .panel-arrowcontainer {
   /* Reseting the transition used when panels are shown */
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -395,8 +395,72 @@
 :-moz-native-anonymous .measuring-tool-highlighter-guide-top,
 :-moz-native-anonymous .measuring-tool-highlighter-guide-right,
 :-moz-native-anonymous .measuring-tool-highlighter-guide-bottom,
 :-moz-native-anonymous .measuring-tool-highlighter-guide-left {
   stroke: var(--highlighter-guide-color);
   stroke-dasharray: 5 3;
   shape-rendering: crispEdges;
 }
+
+/* Eye dropper */
+
+:-moz-native-anonymous .eye-dropper-root {
+  --magnifier-width: 96px;
+  --magnifier-height: 96px;
+  /* Width accounts for all color formats (hsl being the longest) */
+  --label-width: 160px;
+  --color: #e0e0e0;
+
+  position: absolute;
+  /* Tool start position. This should match the X/Y defines in JS */
+  top: 100px;
+  left: 100px;
+
+  /* Prevent interacting with the page when hovering and clicking */
+  pointer-events: auto;
+
+  /* Offset the UI so it is centered around the pointer */
+  transform: translate(
+    calc(var(--magnifier-width) / -2), calc(var(--magnifier-height) / -2));
+
+  filter: drop-shadow(0 0 1px rgba(0,0,0,.4));
+
+  /* We don't need the UI to be reversed in RTL locales, otherwise the # would appear
+     to the right of the hex code. Force LTR */
+  direction: ltr;
+}
+
+:-moz-native-anonymous .eye-dropper-canvas {
+  image-rendering: -moz-crisp-edges;
+  cursor: none;
+  width: var(--magnifier-width);
+  height: var(--magnifier-height);
+  border-radius: 50%;
+  box-shadow: 0 0 0 3px var(--color);
+  display: block;
+}
+
+:-moz-native-anonymous .eye-dropper-color-container {
+  background-color: var(--color);
+  border-radius: 2px;
+  width: var(--label-width);
+  transform: translateX(calc((var(--magnifier-width) - var(--label-width)) / 2));
+  position: relative;
+}
+
+:-moz-native-anonymous .eye-dropper-color-preview {
+  width: 16px;
+  height: 16px;
+  position: absolute;
+  offset-inline-start: 3px;
+  offset-block-start: 3px;
+  box-shadow: 0px 0px 0px black;
+  border: solid 1px #fff;
+}
+
+:-moz-native-anonymous .eye-dropper-color-value {
+  text-shadow: 1px 1px 1px #fff;
+  font: message-box;
+  font-size: 11px;
+  text-align: center;
+  padding: 4px 0;
+}
--- a/devtools/server/actors/highlighters.js
+++ b/devtools/server/actors/highlighters.js
@@ -674,8 +674,12 @@ exports.GeometryEditorHighlighter = Geom
 
 const { RulersHighlighter } = require("./highlighters/rulers");
 register(RulersHighlighter);
 exports.RulersHighlighter = RulersHighlighter;
 
 const { MeasuringToolHighlighter } = require("./highlighters/measuring-tool");
 register(MeasuringToolHighlighter);
 exports.MeasuringToolHighlighter = MeasuringToolHighlighter;
+
+const { EyeDropper } = require("./highlighters/eye-dropper");
+register(EyeDropper);
+exports.EyeDropper = EyeDropper;
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/highlighters/eye-dropper.js
@@ -0,0 +1,499 @@
+/* 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";
+
+// Eye-dropper tool. This is implemented as a highlighter so it can be displayed in the
+// content page.
+// It basically displays a magnifier that tracks mouse moves and shows a magnified version
+// of the page. On click, it samples the color at the pixel being hovered.
+
+const {Ci, Cc} = require("chrome");
+const {CanvasFrameAnonymousContentHelper, createNode} = require("./utils/markup");
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {rgbToHsl, rgbToColorName} = require("devtools/shared/css-color").colorUtils;
+const {getCurrentZoom, getFrameOffsets} = require("devtools/shared/layout/utils");
+
+loader.lazyGetter(this, "clipboardHelper",
+  () => Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper));
+loader.lazyGetter(this, "l10n",
+  () => Services.strings.createBundle("chrome://devtools/locale/eyedropper.properties"));
+
+const ZOOM_LEVEL_PREF = "devtools.eyedropper.zoom";
+const FORMAT_PREF = "devtools.defaultColorUnit";
+// Width of the canvas.
+const MAGNIFIER_WIDTH = 96;
+// Height of the canvas.
+const MAGNIFIER_HEIGHT = 96;
+// Start position, when the tool is first shown. This should match the top/left position
+// defined in CSS.
+const DEFAULT_START_POS_X = 100;
+const DEFAULT_START_POS_Y = 100;
+// How long to wait before closing after copy.
+const CLOSE_DELAY = 750;
+
+/**
+ * The EyeDropper is the class that draws the gradient line and
+ * color stops as an overlay on top of a linear-gradient background-image.
+ */
+function EyeDropper(highlighterEnv) {
+  EventEmitter.decorate(this);
+
+  this.highlighterEnv = highlighterEnv;
+  this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
+                                                      this._buildMarkup.bind(this));
+
+  // Get a couple of settings from prefs.
+  this.format = Services.prefs.getCharPref(FORMAT_PREF);
+  this.eyeDropperZoomLevel = Services.prefs.getIntPref(ZOOM_LEVEL_PREF);
+}
+
+EyeDropper.prototype = {
+  typeName: "EyeDropper",
+
+  ID_CLASS_PREFIX: "eye-dropper-",
+
+  get win() {
+    return this.highlighterEnv.window;
+  },
+
+  _buildMarkup() {
+    // Highlighter main container.
+    let container = createNode(this.win, {
+      attributes: {"class": "highlighter-container"}
+    });
+
+    // Wrapper element.
+    let wrapper = createNode(this.win, {
+      parent: container,
+      attributes: {
+        "id": "root",
+        "class": "root",
+        "hidden": "true"
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    // The magnifier canvas element.
+    createNode(this.win, {
+      parent: wrapper,
+      nodeType: "canvas",
+      attributes: {
+        "id": "canvas",
+        "class": "canvas",
+        "width": MAGNIFIER_WIDTH,
+        "height": MAGNIFIER_HEIGHT
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    // The color label element.
+    let colorLabelContainer = createNode(this.win, {
+      parent: wrapper,
+      attributes: {"class": "color-container"},
+      prefix: this.ID_CLASS_PREFIX
+    });
+    createNode(this.win, {
+      nodeType: "div",
+      parent: colorLabelContainer,
+      attributes: {"id": "color-preview", "class": "color-preview"},
+      prefix: this.ID_CLASS_PREFIX
+    });
+    createNode(this.win, {
+      nodeType: "div",
+      parent: colorLabelContainer,
+      attributes: {"id": "color-value", "class": "color-value"},
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    return container;
+  },
+
+  destroy() {
+    this.hide();
+    this.markup.destroy();
+  },
+
+  getElement(id) {
+    return this.markup.getElement(this.ID_CLASS_PREFIX + id);
+  },
+
+  /**
+   * Show the eye-dropper highlighter.
+   * @param {DOMNode} node The node which document the highlighter should be inserted in.
+   * @param {Object} options The options object may contain the following properties:
+   * - {Boolean} copyOnSelect Whether selecting a color should copy it to the clipboard.
+   */
+  show(node, options = {}) {
+    this.options = options;
+
+    // Get the page's current zoom level.
+    this.pageZoom = getCurrentZoom(this.win);
+
+    // Take a screenshot of the viewport. This needs to be done first otherwise the
+    // eyedropper UI will appear in the screenshot itself (since the UI is injected as
+    // native anonymous content in the page).
+    // Once the screenshot is ready, the magnified area will be drawn.
+    this.prepareImageCapture();
+
+    // Start listening for user events.
+    let {pageListenerTarget} = this.highlighterEnv;
+    pageListenerTarget.addEventListener("mousemove", this);
+    pageListenerTarget.addEventListener("click", this);
+    pageListenerTarget.addEventListener("keydown", this);
+    pageListenerTarget.addEventListener("DOMMouseScroll", this);
+    pageListenerTarget.addEventListener("FullZoomChange", this);
+
+    // Show the eye-dropper.
+    this.getElement("root").removeAttribute("hidden");
+
+    // Prepare the canvas context on which we're drawing the magnified page portion.
+    this.ctx = this.getElement("canvas").getCanvasContext();
+    this.ctx.mozImageSmoothingEnabled = false;
+
+    this.magnifiedArea = {width: MAGNIFIER_WIDTH, height: MAGNIFIER_HEIGHT,
+                          x: DEFAULT_START_POS_X, y: DEFAULT_START_POS_Y};
+
+    this.moveTo(DEFAULT_START_POS_X, DEFAULT_START_POS_Y);
+
+    // Focus the content so the keyboard can be used.
+    this.win.document.documentElement.focus();
+
+    return true;
+  },
+
+  /**
+   * Hide the eye-dropper highlighter.
+   */
+  hide() {
+    this.pageImage = null;
+
+    let {pageListenerTarget} = this.highlighterEnv;
+    pageListenerTarget.removeEventListener("mousemove", this);
+    pageListenerTarget.removeEventListener("click", this);
+    pageListenerTarget.removeEventListener("keydown", this);
+    pageListenerTarget.removeEventListener("DOMMouseScroll", this);
+    pageListenerTarget.removeEventListener("FullZoomChange", this);
+
+    this.getElement("root").setAttribute("hidden", "true");
+    this.getElement("root").removeAttribute("drawn");
+  },
+
+  prepareImageCapture() {
+    // Get the page as an image.
+    let imageData = getWindowAsImageData(this.win);
+    let image = new this.win.Image();
+    image.src = imageData;
+
+    // Wait for screenshot to load
+    image.onload = () => {
+      this.pageImage = image;
+      // We likely haven't drawn anything yet (no mousemove events yet), so start now.
+      this.draw();
+
+      // Set an attribute on the root element to be able to run tests after the first draw
+      // was done.
+      this.getElement("root").setAttribute("drawn", "true");
+    };
+  },
+
+  /**
+   * Get the number of cells (blown-up pixels) per direction in the grid.
+   */
+  get cellsWide() {
+    // Canvas will render whole "pixels" (cells) only, and an even number at that. Round
+    // up to the nearest even number of pixels.
+    let cellsWide = Math.ceil(this.magnifiedArea.width / this.eyeDropperZoomLevel);
+    cellsWide += cellsWide % 2;
+
+    return cellsWide;
+  },
+
+  /**
+   * Get the size of each cell (blown-up pixel) in the grid.
+   */
+  get cellSize() {
+    return this.magnifiedArea.width / this.cellsWide;
+  },
+
+  /**
+   * Get index of cell in the center of the grid.
+   */
+  get centerCell() {
+    return Math.floor(this.cellsWide / 2);
+  },
+
+  /**
+   * Get color of center cell in the grid.
+   */
+  get centerColor() {
+    let pos = (this.centerCell * this.cellSize) + (this.cellSize / 2);
+    let rgb = this.ctx.getImageData(pos, pos, 1, 1).data;
+    return rgb;
+  },
+
+  draw() {
+    // If the image of the page isn't ready yet, bail out, we'll draw later on mousemove.
+    if (!this.pageImage) {
+      return;
+    }
+
+    let {width, height, x, y} = this.magnifiedArea;
+
+    let zoomedWidth = width / this.eyeDropperZoomLevel;
+    let zoomedHeight = height / this.eyeDropperZoomLevel;
+
+    let sx = x - (zoomedWidth / 2);
+    let sy = y - (zoomedHeight / 2);
+    let sw = zoomedWidth;
+    let sh = zoomedHeight;
+
+    this.ctx.drawImage(this.pageImage, sx, sy, sw, sh, 0, 0, width, height);
+
+    // Draw the grid on top, but only at 3x or more, otherwise it's too busy.
+    if (this.eyeDropperZoomLevel > 2) {
+      this.drawGrid();
+    }
+
+    this.drawCrosshair();
+
+    // Update the color preview and value.
+    let rgb = this.centerColor;
+    this.getElement("color-preview").setAttribute("style",
+      `background-color:${toColorString(rgb, "rgb")};`);
+    this.getElement("color-value").setTextContent(toColorString(rgb, this.format));
+  },
+
+  /**
+   * Draw a grid on the canvas representing pixel boundaries.
+   */
+  drawGrid() {
+    let {width, height} = this.magnifiedArea;
+
+    this.ctx.lineWidth = 1;
+    this.ctx.strokeStyle = "rgba(143, 143, 143, 0.2)";
+
+    for (let i = 0; i < width; i += this.cellSize) {
+      this.ctx.beginPath();
+      this.ctx.moveTo(i - .5, 0);
+      this.ctx.lineTo(i - .5, height);
+      this.ctx.stroke();
+
+      this.ctx.beginPath();
+      this.ctx.moveTo(0, i - .5);
+      this.ctx.lineTo(width, i - .5);
+      this.ctx.stroke();
+    }
+  },
+
+  /**
+   * Draw a box on the canvas to highlight the center cell.
+   */
+  drawCrosshair() {
+    let pos = this.centerCell * this.cellSize;
+
+    this.ctx.lineWidth = 1;
+    this.ctx.lineJoin = "miter";
+    this.ctx.strokeStyle = "rgba(0, 0, 0, 1)";
+    this.ctx.strokeRect(pos - 1.5, pos - 1.5, this.cellSize + 2, this.cellSize + 2);
+
+    this.ctx.strokeStyle = "rgba(255, 255, 255, 1)";
+    this.ctx.strokeRect(pos - 0.5, pos - 0.5, this.cellSize, this.cellSize);
+  },
+
+  handleEvent(e) {
+    switch (e.type) {
+      case "mousemove":
+        // We might be getting an event from a child frame, so account for the offset.
+        let [xOffset, yOffset] = getFrameOffsets(this.win, e.target);
+        let x = xOffset + e.pageX - this.win.scrollX;
+        let y = yOffset + e.pageY - this.win.scrollY;
+        // Update the zoom area.
+        this.magnifiedArea.x = x * this.pageZoom;
+        this.magnifiedArea.y = y * this.pageZoom;
+        // Redraw the portion of the screenshot that is now under the mouse.
+        this.draw();
+        // And move the eye-dropper's UI so it follows the mouse.
+        this.moveTo(x, y);
+        break;
+      case "click":
+        this.selectColor();
+        break;
+      case "keydown":
+        this.handleKeyDown(e);
+        break;
+      case "DOMMouseScroll":
+        // Prevent scrolling. That's because we only took a screenshot of the viewport, so
+        // scrolling out of the viewport wouldn't draw the expected things. In the future
+        // we can take the screenshot again on scroll, but for now it doesn't seem
+        // important.
+        e.preventDefault();
+        break;
+      case "FullZoomChange":
+        this.hide();
+        this.show();
+        break;
+    }
+  },
+
+  moveTo(x, y) {
+    this.getElement("root").setAttribute("style", `top:${y}px;left:${x}px;`);
+  },
+
+  /**
+   * Select the current color that's being previewed. Depending on the current options,
+   * selecting might mean copying to the clipboard and closing the
+   */
+  selectColor() {
+    let onColorSelected = Promise.resolve();
+    if (this.options.copyOnSelect) {
+      onColorSelected = this.copyColor();
+    }
+
+    this.emit("selected", toColorString(this.centerColor, this.format));
+    onColorSelected.then(() => this.hide(), e => console.error(e));
+  },
+
+  /**
+   * Handler for the keydown event. Either select the color or move the panel in a
+   * direction depending on the key pressed.
+   */
+  handleKeyDown(e) {
+    if (e.keyCode === e.DOM_VK_RETURN) {
+      this.selectColor();
+      return;
+    }
+
+    if (e.keyCode === e.DOM_VK_ESCAPE) {
+      this.emit("canceled");
+      this.hide();
+      return;
+    }
+
+    let offsetX = 0;
+    let offsetY = 0;
+    let modifier = 1;
+
+    if (e.keyCode === e.DOM_VK_LEFT) {
+      offsetX = -1;
+    }
+    if (e.keyCode === e.DOM_VK_RIGHT) {
+      offsetX = 1;
+    }
+    if (e.keyCode === e.DOM_VK_UP) {
+      offsetY = -1;
+    }
+    if (e.keyCode === e.DOM_VK_DOWN) {
+      offsetY = 1;
+    }
+    if (e.shiftKey) {
+      modifier = 10;
+    }
+
+    offsetY *= modifier;
+    offsetX *= modifier;
+
+    if (offsetX !== 0 || offsetY !== 0) {
+      this.magnifiedArea.x += offsetX;
+      this.magnifiedArea.y += offsetY;
+
+      this.draw();
+
+      this.moveTo(this.magnifiedArea.x / this.pageZoom,
+                  this.magnifiedArea.y / this.pageZoom);
+    }
+
+    // Prevent all keyboard interaction with the page, except if a modifier is used to let
+    // keyboard shortcuts through.
+    let hasModifier = e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
+    if (!hasModifier) {
+      e.preventDefault();
+    }
+  },
+
+  /**
+   * Copy the currently inspected color to the clipboard.
+   * @return {Promise} Resolves when the copy has been done (after a delay that is used to
+   * let users know that something was copied).
+   */
+  copyColor() {
+    // Copy to the clipboard.
+    let color = toColorString(this.centerColor, this.format);
+    clipboardHelper.copyString(color);
+
+    // Provide some feedback.
+    this.getElement("color-value").setTextContent(
+      "✓ " + l10n.GetStringFromName("colorValue.copied"));
+
+    // Hide the tool after a delay.
+    clearTimeout(this._copyTimeout);
+    return new Promise(resolve => {
+      this._copyTimeout = setTimeout(resolve, CLOSE_DELAY);
+    });
+  }
+};
+
+exports.EyeDropper = EyeDropper;
+
+/**
+ * Get a content window as image data-url.
+ * @param {Window} win
+ * @return {String} The data-url
+ */
+function getWindowAsImageData(win) {
+  let canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+  let scale = getCurrentZoom(win);
+  let width = win.innerWidth;
+  let height = win.innerHeight;
+  canvas.width = width * scale;
+  canvas.height = height * scale;
+  canvas.mozOpaque = true;
+
+  let ctx = canvas.getContext("2d");
+
+  ctx.scale(scale, scale);
+  ctx.drawWindow(win, win.scrollX, win.scrollY, width, height, "#fff");
+
+  return canvas.toDataURL();
+}
+
+/**
+ * Get a formatted CSS color string from a color value.
+ * @param {array} rgb Rgb values of a color to format.
+ * @param {string} format Format of string. One of "hex", "rgb", "hsl", "name".
+ * @return {string} Formatted color value, e.g. "#FFF" or "hsl(20, 10%, 10%)".
+ */
+function toColorString(rgb, format) {
+  let [r, g, b] = rgb;
+
+  switch (format) {
+    case "hex":
+      return hexString(rgb);
+    case "rgb":
+      return "rgb(" + r + ", " + g + ", " + b + ")";
+    case "hsl":
+      let [h, s, l] = rgbToHsl(rgb);
+      return "hsl(" + h + ", " + s + "%, " + l + "%)";
+    case "name":
+      let str;
+      try {
+        str = rgbToColorName(r, g, b);
+      } catch (e) {
+        str = hexString(rgb);
+      }
+      return str;
+    default:
+      return hexString(rgb);
+  }
+}
+
+/**
+ * Produce a hex-formatted color string from rgb values.
+ * @param {array} rgb Rgb values of color to stringify.
+ * @return {string} Hex formatted string for color, e.g. "#FFEE00".
+ */
+function hexString([r, g, b]) {
+  let val = (1 << 24) + (r << 16) + (g << 8) + (b << 0);
+  return "#" + val.toString(16).substr(-6).toUpperCase();
+}
--- a/devtools/server/actors/highlighters/moz.build
+++ b/devtools/server/actors/highlighters/moz.build
@@ -7,15 +7,16 @@
 DIRS += [
     'utils',
 ]
 
 DevToolsModules(
     'auto-refresh.js',
     'box-model.js',
     'css-transform.js',
+    'eye-dropper.js',
     'geometry-editor.js',
     'measuring-tool.js',
     'rect.js',
     'rulers.js',
     'selector.js',
     'simple-outline.js'
 )
--- a/devtools/server/actors/highlighters/utils/markup.js
+++ b/devtools/server/actors/highlighters/utils/markup.js
@@ -320,16 +320,20 @@ CanvasFrameAnonymousContentHelper.protot
       this.content.removeAttributeForElement(id, name);
     }
   },
 
   hasAttributeForElement: function (id, name) {
     return typeof this.getAttributeForElement(id, name) === "string";
   },
 
+  getCanvasContext: function (id, type = "2d") {
+    return this.content ? this.content.getCanvasContext(id, type) : null;
+  },
+
   /**
    * Add an event listener to one of the elements inserted in the canvasFrame
    * native anonymous container.
    * Like other methods in this helper, this requires the ID of the element to
    * be passed in.
    *
    * Note that if the content page navigates, the event listeners won't be
    * added again.
@@ -455,16 +459,17 @@ CanvasFrameAnonymousContentHelper.protot
 
     return {
       getTextContent: () => this.getTextContentForElement(id),
       setTextContent: text => this.setTextContentForElement(id, text),
       setAttribute: (name, val) => this.setAttributeForElement(id, name, val),
       getAttribute: name => this.getAttributeForElement(id, name),
       removeAttribute: name => this.removeAttributeForElement(id, name),
       hasAttribute: name => this.hasAttributeForElement(id, name),
+      getCanvasContext: type => this.getCanvasContext(id, type),
       addEventListener: (type, handler) => {
         return this.addEventListenerForElement(id, type, handler);
       },
       removeEventListener: (type, handler) => {
         return this.removeEventListenerForElement(id, type, handler);
       },
       classList
     };
--- a/devtools/server/actors/inspector.js
+++ b/devtools/server/actors/inspector.js
@@ -48,39 +48,37 @@
  * So to be able to answer "all the children of a given node that we have
  * seen on the client side", we guarantee that every time we've seen a node,
  * we connect it up through its parents.
  */
 
 const {Cc, Ci, Cu} = require("chrome");
 const Services = require("Services");
 const protocol = require("devtools/shared/protocol");
-const {Arg, Option, method, RetVal, types} = protocol;
 const {LongStringActor} = require("devtools/server/actors/string");
 const promise = require("promise");
 const {Task} = require("devtools/shared/task");
-const object = require("sdk/util/object");
 const events = require("sdk/event/core");
-const {Class} = require("sdk/core/heritage");
 const {WalkerSearch} = require("devtools/server/actors/utils/walker-search");
 const {PageStyleActor, getFontPreviewData} = require("devtools/server/actors/styles");
 const {
   HighlighterActor,
   CustomHighlighterActor,
   isTypeRegistered,
+  HighlighterEnvironment
 } = require("devtools/server/actors/highlighters");
+const {EyeDropper} = require("devtools/server/actors/highlighters/eye-dropper");
 const {
   isAnonymous,
   isNativeAnonymous,
   isXBLAnonymous,
   isShadowAnonymous,
   getFrameElement
 } = require("devtools/shared/layout/utils");
-const {getLayoutChangesObserver, releaseLayoutChangesObserver} =
-  require("devtools/server/actors/layout");
+const {getLayoutChangesObserver, releaseLayoutChangesObserver} = require("devtools/server/actors/layout");
 const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");
 
 loader.lazyRequireGetter(this, "CSS", "CSS");
 
 const {EventParsers} = require("devtools/shared/event-parsers");
 const {nodeSpec, nodeListSpec, walkerSpec, inspectorSpec} = require("devtools/shared/specs/inspector");
 
 const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
@@ -437,44 +435,44 @@ var NodeActor = exports.NodeActor = prot
    * Gets event listeners and adds their information to the events array.
    *
    * @param  {Node} node
    *         Node for which we are to get listeners.
    */
   getEventListeners: function (node) {
     let parsers = this._eventParsers;
     let dbg = this.parent().tabActor.makeDebugger();
-    let events = [];
+    let listeners = [];
 
     for (let [, {getListeners, normalizeHandler}] of parsers) {
       try {
         let eventInfos = getListeners(node);
 
         if (!eventInfos) {
           continue;
         }
 
         for (let eventInfo of eventInfos) {
           if (normalizeHandler) {
             eventInfo.normalizeHandler = normalizeHandler;
           }
 
-          this.processHandlerForEvent(node, events, dbg, eventInfo);
+          this.processHandlerForEvent(node, listeners, dbg, eventInfo);
         }
       } catch (e) {
         // An object attached to the node looked like a listener but wasn't...
         // do nothing.
       }
     }
 
-    events.sort((a, b) => {
+    listeners.sort((a, b) => {
       return a.type.localeCompare(b.type);
     });
 
-    return events;
+    return listeners;
   },
 
   /**
    * Process a handler
    *
    * @param  {Node} node
    *         The node for which we want information.
    * @param  {Array} events
@@ -496,17 +494,17 @@ var NodeActor = exports.NodeActor = prot
    *             tags: tags,
    *             DOM0: true,
    *             capturing: true,
    *             hide: {
    *               dom0: true
    *             }
    *           }
    */
-  processHandlerForEvent: function (node, events, dbg, eventInfo) {
+  processHandlerForEvent: function (node, listeners, dbg, eventInfo) {
     let type = eventInfo.type || "";
     let handler = eventInfo.handler;
     let tags = eventInfo.tags || "";
     let hide = eventInfo.hide || {};
     let override = eventInfo.override || {};
     let global = Cu.getGlobalForObject(handler);
     let globalDO = dbg.addDebuggee(global);
     let listenerDO = globalDO.makeDebuggeeValue(handler);
@@ -587,17 +585,17 @@ var NodeActor = exports.NodeActor = prot
                            override.searchString : searchString,
       tags: tags,
       DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0,
       capturing: typeof override.capturing !== "undefined" ?
                         override.capturing : eventInfo.capturing,
       hide: hide
     };
 
-    events.push(eventObj);
+    listeners.push(eventObj);
 
     dbg.removeDebuggee(globalDO);
   },
 
   /**
    * Returns a LongStringActor with the node's value.
    */
   getNodeValue: function () {
@@ -1293,18 +1291,18 @@ var WalkerActor = protocol.ActorClassWit
     }
     let maxNodes = options.maxNodes || -1;
     if (maxNodes == -1) {
       maxNodes = Number.MAX_VALUE;
     }
 
     // We're going to create a few document walkers with the same filter,
     // make it easier.
-    let getFilteredWalker = node => {
-      return this.getDocumentWalker(node, options.whatToShow);
+    let getFilteredWalker = documentWalkerNode => {
+      return this.getDocumentWalker(documentWalkerNode, options.whatToShow);
     };
 
     // Need to know the first and last child.
     let rawNode = node.rawNode;
     let firstChild = getFilteredWalker(rawNode).firstChild();
     let lastChild = getFilteredWalker(rawNode).lastChild();
 
     if (!firstChild) {
@@ -2102,32 +2100,32 @@ var WalkerActor = protocol.ActorClassWit
 
   /**
    * Insert a node into the DOM.
    */
   insertBefore: function (node, parent, sibling) {
     if (isNodeDead(node) ||
         isNodeDead(parent) ||
         (sibling && isNodeDead(sibling))) {
-      return null;
+      return;
     }
 
     let rawNode = node.rawNode;
     let rawParent = parent.rawNode;
     let rawSibling = sibling ? sibling.rawNode : null;
 
     // Don't bother inserting a node if the document position isn't going
     // to change. This prevents needless iframes reloading and mutations.
     if (rawNode.parentNode === rawParent) {
       let currentNextSibling = this.nextSibling(node);
       currentNextSibling = currentNextSibling ? currentNextSibling.rawNode :
                                                 null;
 
       if (rawNode === rawSibling || currentNextSibling === rawSibling) {
-        return null;
+        return;
       }
     }
 
     rawParent.insertBefore(rawNode, rawSibling);
   },
 
   /**
    * Editing a node's tagname actually means creating a new node with the same
@@ -2145,32 +2143,32 @@ var WalkerActor = protocol.ActorClassWit
     // Create a new element with the same attributes as the current element and
     // prepare to replace the current node with it.
     let newNode;
     try {
       newNode = nodeDocument(oldNode).createElement(tagName);
     } catch (x) {
       // Failed to create a new element with that tag name, ignore the change,
       // and signal the error to the front.
-      return Promise.reject(new Error("Could not change node's tagName to " +
-        tagName));
+      return Promise.reject(new Error("Could not change node's tagName to " + tagName));
     }
 
     let attrs = oldNode.attributes;
     for (let i = 0; i < attrs.length; i++) {
       newNode.setAttribute(attrs[i].name, attrs[i].value);
     }
 
     // Insert the new node, and transfer the old node's children.
     oldNode.parentNode.insertBefore(newNode, oldNode);
     while (oldNode.firstChild) {
       newNode.appendChild(oldNode.firstChild);
     }
 
     oldNode.remove();
+    return null;
   },
 
   /**
    * Get any pending mutation records.  Must be called by the client after
    * the `new-mutations` notification is received.  Returns an array of
    * mutation records.
    *
    * Mutation records have a basic structure:
@@ -2575,25 +2573,31 @@ var WalkerActor = protocol.ActorClassWit
     return this.attachElement(obj);
   },
 });
 
 /**
  * Server side of the inspector actor, which is used to create
  * inspector-related actors, including the walker.
  */
-var InspectorActor = exports.InspectorActor = protocol.ActorClassWithSpec(inspectorSpec, {
+exports.InspectorActor = protocol.ActorClassWithSpec(inspectorSpec, {
   initialize: function (conn, tabActor) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this.tabActor = tabActor;
+
+    this._onColorPicked = this._onColorPicked.bind(this);
+    this._onColorPickCanceled = this._onColorPickCanceled.bind(this);
+    this.destroyEyeDropper = this.destroyEyeDropper.bind(this);
   },
 
   destroy: function () {
     protocol.Actor.prototype.destroy.call(this);
 
+    this.destroyEyeDropper();
+
     this._highlighterPromise = null;
     this._pageStylePromise = null;
     this._walkerPromise = null;
     this.walker = null;
     this.tabActor = null;
   },
 
   // Forces destruction of the actor and all its children
@@ -2731,16 +2735,76 @@ var InspectorActor = exports.InspectorAc
                    : nodeDocument(node.rawNode);
 
     if (!document) {
       return url;
     }
 
     let baseURI = Services.io.newURI(document.location.href, null, null);
     return Services.io.newURI(url, null, baseURI).spec;
+  },
+
+  /**
+   * Create an instance of the eye-dropper highlighter and store it on this._eyeDropper.
+   * Note that for now, a new instance is created every time to deal with page navigation.
+   */
+  createEyeDropper: function () {
+    this.destroyEyeDropper();
+    this._highlighterEnv = new HighlighterEnvironment();
+    this._highlighterEnv.initFromTabActor(this.tabActor);
+    this._eyeDropper = new EyeDropper(this._highlighterEnv);
+  },
+
+  /**
+   * Destroy the current eye-dropper highlighter instance.
+   */
+  destroyEyeDropper: function () {
+    if (this._eyeDropper) {
+      this.cancelPickColorFromPage();
+      this._eyeDropper.destroy();
+      this._eyeDropper = null;
+      this._highlighterEnv.destroy();
+      this._highlighterEnv = null;
+    }
+  },
+
+  /**
+   * Pick a color from the page using the eye-dropper. This method doesn't return anything
+   * but will cause events to be sent to the front when a color is picked or when the user
+   * cancels the picker.
+   * @param {Object} options
+   */
+  pickColorFromPage: function (options) {
+    this.createEyeDropper();
+    this._eyeDropper.show(this.window.document.documentElement, options);
+    this._eyeDropper.once("selected", this._onColorPicked);
+    this._eyeDropper.once("canceled", this._onColorPickCanceled);
+    events.once(this.tabActor, "will-navigate", this.destroyEyeDropper);
+  },
+
+  /**
+   * After the pickColorFromPage method is called, the only way to dismiss the eye-dropper
+   * highlighter is for the user to click in the page and select a color. If you need to
+   * dismiss the eye-dropper programatically instead, use this method.
+   */
+  cancelPickColorFromPage: function () {
+    if (this._eyeDropper) {
+      this._eyeDropper.hide();
+      this._eyeDropper.off("selected", this._onColorPicked);
+      this._eyeDropper.off("canceled", this._onColorPickCanceled);
+      events.off(this.tabActor, "will-navigate", this.destroyEyeDropper);
+    }
+  },
+
+  _onColorPicked: function (e, color) {
+    events.emit(this, "color-picked", color);
+  },
+
+  _onColorPickCanceled: function () {
+    events.emit(this, "color-pick-canceled");
   }
 });
 
 // Exported for test purposes.
 exports._documentWalker = DocumentWalker;
 
 function nodeDocument(node) {
   if (Cu.isDeadWrapper(node)) {
@@ -2752,16 +2816,17 @@ function nodeDocument(node) {
 
 function nodeDocshell(node) {
   let doc = node ? nodeDocument(node) : null;
   let win = doc ? doc.defaultView : null;
   if (win) {
     return win.QueryInterface(Ci.nsIInterfaceRequestor)
               .getInterface(Ci.nsIDocShell);
   }
+  return null;
 }
 
 function isNodeDead(node) {
   return !node || !node.rawNode || Cu.isDeadWrapper(node.rawNode);
 }
 
 /**
  * Wrapper for inDeepTreeWalker.  Adds filtering to the traversal methods.
@@ -2957,17 +3022,17 @@ function ensureImageLoaded(image, timeou
   let onLoad = AsyncUtils.listenOnce(image, "load");
 
   // Reject if loading fails.
   let onError = AsyncUtils.listenOnce(image, "error").then(() => {
     return promise.reject("Image '" + image.src + "' failed to load.");
   });
 
   // Don't timeout when testing. This is never settled.
-  let onAbort = new promise(() => {});
+  let onAbort = new Promise(() => {});
 
   if (!DevToolsUtils.testing) {
     // Tests are not running. Reject the promise after given timeout.
     onAbort = DevToolsUtils.waitForTime(timeout).then(() => {
       return promise.reject("Image '" + image.src + "' took too long to load.");
     });
   }
 
--- a/devtools/server/tests/mochitest/chrome.ini
+++ b/devtools/server/tests/mochitest/chrome.ini
@@ -6,16 +6,17 @@ support-files =
   Debugger.Source.prototype.element.js
   Debugger.Source.prototype.element-2.js
   Debugger.Source.prototype.element.html
   director-helpers.js
   hello-actor.js
   inspector_css-properties.html
   inspector_getImageData.html
   inspector-delay-image-response.sjs
+  inspector-eyedropper.html
   inspector-helpers.js
   inspector-search-data.html
   inspector-styles-data.css
   inspector-styles-data.html
   inspector-traversal-data.html
   large-image.jpg
   memory-helpers.js
   nonchrome_unsafeDereference.html
@@ -70,16 +71,17 @@ skip-if = buildapp == 'mulet'
 [test_inspector_getNodeFromActor.html]
 [test_inspector-hide.html]
 [test_inspector-insert.html]
 [test_inspector-mutations-attr.html]
 [test_inspector-mutations-events.html]
 [test_inspector-mutations-childlist.html]
 [test_inspector-mutations-frameload.html]
 [test_inspector-mutations-value.html]
+[test_inspector-pick-color.html]
 [test_inspector-pseudoclass-lock.html]
 [test_inspector-release.html]
 [test_inspector-reload.html]
 [test_inspector-remove.html]
 [test_inspector-resize.html]
 [test_inspector-resolve-url.html]
 [test_inspector-retain.html]
 [test_inspector-search.html]
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/mochitest/inspector-eyedropper.html
@@ -0,0 +1,18 @@
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Inspector Eyedropper tests</title>
+  <style>
+    html {
+      background: black;
+    }
+  </style>
+  <script type="text/javascript">
+    window.onload = function() {
+      window.opener.postMessage('ready', '*');
+    };
+  </script>
+</head>
+</body>
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/mochitest/test_inspector-pick-color.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the inspector actor has the pickColorFromPage and cancelPickColorFromPage
+methods and that when a color is picked the color-picked event is emitted and that when
+the eyedropper is dimissed, the color-pick-canceled event is emitted.
+https://bugzilla.mozilla.org/show_bug.cgi?id=1262439
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1262439</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
+  <script type="application/javascript;version=1.8">
+window.onload = function() {
+  const Cu = Components.utils;
+  Cu.import("resource://devtools/shared/Loader.jsm");
+  const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+  const {InspectorFront} = devtools.require("devtools/shared/fronts/inspector");
+  const {console} = Cu.import("resource://gre/modules/Console.jsm", {});
+
+  SimpleTest.waitForExplicitFinish();
+
+  let win = null;
+  let inspector = null;
+
+  addAsyncTest(function*() {
+    info("Setting up inspector actor");
+
+    let url = document.getElementById("inspectorContent").href;
+
+    yield new Promise(resolve => {
+      attachURL(url, function(err, client, tab, doc) {
+        win = doc.defaultView;
+        inspector = InspectorFront(client, tab);
+        resolve();
+      });
+    });
+
+    runNextTest();
+  });
+
+  addAsyncTest(function*() {
+    info("Start picking a color from the page");
+    yield inspector.pickColorFromPage();
+
+    info("Click in the page and make sure a color-picked event is received");
+    let onColorPicked = waitForEvent("color-picked");
+    win.document.body.click();
+    let color = yield onColorPicked;
+
+    is(color, "#000000", "The color-picked event was received with the right color");
+
+    runNextTest();
+  });
+
+  addAsyncTest(function*() {
+    info("Start picking a color from the page");
+    yield inspector.pickColorFromPage();
+
+    info("Use the escape key to dismiss the eyedropper");
+    let onPickCanceled = waitForEvent("color-pick-canceled");
+
+    let keyboardEvent = win.document.createEvent("KeyboardEvent");
+    keyboardEvent.initKeyEvent("keydown", true, true, win, false, false,
+                               false, false, 27, 0);
+    win.document.dispatchEvent(keyboardEvent);
+
+    yield onPickCanceled;
+    ok(true, "The color-pick-canceled event was received");
+
+    runNextTest();
+  });
+
+  addAsyncTest(function*() {
+    info("Start picking a color from the page");
+    yield inspector.pickColorFromPage();
+
+    info("And cancel the color picking");
+    yield inspector.cancelPickColorFromPage();
+
+    runNextTest();
+  });
+
+  function waitForEvent(name) {
+    return new Promise(resolve => inspector.once(name, resolve));
+  }
+
+  runNextTest();
+};
+  </script>
+</head>
+<body>
+<a id="inspectorContent" target="_blank" href="inspector-eyedropper.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test">
+</pre>
+</body>
+</html>
rename from devtools/client/shared/css-color-db.js
rename to devtools/shared/css-color-db.js
rename from devtools/client/shared/css-color.js
rename to devtools/shared/css-color.js
--- a/devtools/client/shared/css-color.js
+++ b/devtools/shared/css-color.js
@@ -2,17 +2,17 @@
  * 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 Services = require("Services");
 
 const {getCSSLexer} = require("devtools/shared/css-lexer");
-const {cssColors} = require("devtools/client/shared/css-color-db");
+const {cssColors} = require("devtools/shared/css-color-db");
 
 const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
 
 const SPECIALVALUES = new Set([
   "currentcolor",
   "initial",
   "inherit",
   "transparent",
--- a/devtools/shared/layout/utils.js
+++ b/devtools/shared/layout/utils.js
@@ -161,16 +161,17 @@ function getFrameOffsets(boundaryWindow,
     xOffset += frameRect.left + offsetLeft;
     yOffset += frameRect.top + offsetTop;
 
     frameWin = getParentWindow(frameWin);
   }
 
   return [xOffset * scale, yOffset * scale];
 }
+exports.getFrameOffsets = getFrameOffsets;
 
 /**
  * Get box quads adjusted for iframes and zoom level.
  *
  * @param {DOMWindow} boundaryWindow
  *        The window where to stop to iterate. If `null` is given, the top
  *        window is used.
  * @param {DOMNode} node
--- a/devtools/shared/locales/en-US/gclicommands.properties
+++ b/devtools/shared/locales/en-US/gclicommands.properties
@@ -305,20 +305,16 @@ inspectNodeManual=A CSS selector for use
 # string is designed to be shown in a menu alongside the command name, which
 # is why it should be as short as possible.
 eyedropperDesc=Grab a color from the page
 
 # LOCALIZATION NOTE (eyedropperManual) A fuller description of the 'eyedropper'
 # command, displayed when the user asks for help on what it does.
 eyedropperManual=Open a panel that magnifies an area of page to inspect pixels and copy color values
 
-# LOCALIZATION NOTE (eyedropperTooltip) A string displayed as the
-# tooltip of button in devtools toolbox which toggles the Eyedropper tool.
-eyedropperTooltip=Grab a color from the page
-
 # LOCALIZATION NOTE (debuggerClosed) Used in the output of several commands
 # to explain that the debugger must be opened first.
 debuggerClosed=The debugger must be opened before using this command
 
 # LOCALIZATION NOTE (debuggerStopped) Used in the output of several commands
 # to explain that the debugger must be opened first before setting breakpoints.
 debuggerStopped=The debugger must be opened before setting breakpoints
 
--- a/devtools/shared/moz.build
+++ b/devtools/shared/moz.build
@@ -37,16 +37,18 @@ XPCSHELL_TESTS_MANIFESTS += ['tests/unit
 
 JAR_MANIFESTS += ['jar.mn']
 
 DevToolsModules(
     'async-storage.js',
     'async-utils.js',
     'builtin-modules.js',
     'content-observer.js',
+    'css-color-db.js',
+    'css-color.js',
     'css-lexer.js',
     'css-parsing-utils.js',
     'css-properties-db.js',
     'defer.js',
     'deprecated-sync-thenables.js',
     'DevToolsUtils.js',
     'dom-node-constants.js',
     'dom-node-filter-constants.js',
--- a/devtools/shared/specs/inspector.js
+++ b/devtools/shared/specs/inspector.js
@@ -366,16 +366,26 @@ const walkerSpec = generateActorSpec({
   }
 });
 
 exports.walkerSpec = walkerSpec;
 
 const inspectorSpec = generateActorSpec({
   typeName: "inspector",
 
+  events: {
+    "color-picked": {
+      type: "colorPicked",
+      color: Arg(0, "string")
+    },
+    "color-pick-canceled": {
+      type: "colorPickCanceled"
+    }
+  },
+
   methods: {
     getWalker: {
       request: {
         options: Arg(0, "nullable:json")
       },
       response: {
         walker: RetVal("domwalker")
       }
@@ -404,13 +414,21 @@ const inspectorSpec = generateActorSpec(
     },
     getImageDataFromURL: {
       request: {url: Arg(0), maxDim: Arg(1, "nullable:number")},
       response: RetVal("imageData")
     },
     resolveRelativeURL: {
       request: {url: Arg(0, "string"), node: Arg(1, "nullable:domnode")},
       response: {value: RetVal("string")}
+    },
+    pickColorFromPage: {
+      request: {options: Arg(0, "nullable:json")},
+      response: {}
+    },
+    cancelPickColorFromPage: {
+      request: {},
+      response: {}
     }
   }
 });
 
 exports.inspectorSpec = inspectorSpec;
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -40,34 +40,48 @@ android {
         release {
             shrinkResources true
             minifyEnabled true
             proguardFile "${topsrcdir}/mobile/android/config/proguard/proguard.cfg"
         }
     }
 
     productFlavors {
-        // For fast local development.  If you have an API 21 device, you should
-        // use this.
+        // For API 21+ - with multi dex, this will be faster for local development.
         local {
-            // Setting `minSdkVersion 21` allows the Android gradle plugin to
+            // For multi dex, setting `minSdkVersion 21` allows the Android gradle plugin to
             // pre-DEX each module and produce an APK that can be tested on
             // Android Lollipop without time consuming DEX merging processes.
             minSdkVersion 21
             dexOptions {
                 preDexLibraries true
+                // We only call `MultiDex.install()` for the automation build flavor
+                // so this may not work. However, I don't think the multidex support
+                // library is necessary for 21+, so I expect that it will work.
                 multiDexEnabled true
             }
         }
-        // For local development on older devices.  Use this only if you only
-        // have a pre-API 21 device, or if you want to test API-specific things.
+        // For API < 21 - does not support multi dex because local development
+        // is slow in that case. Most builds will not require multi dex so this
+        // should not be an issue.
         localOld {
         }
         // Automation builds.
         automation {
+            dexOptions {
+                // As of FF48 on beta, the "test", "lint", etc. treeherder jobs fail because they
+                // exceed the method limit. Beta includes Adjust and its GPS dependencies, which
+                // increase the method count & explain the failures. Furthermore, this error only
+                // occurs on debug builds because we don't proguard.
+                //
+                // We enable multidex as an easy, quick-fix with minimal side effects but before we
+                // move to gradle for our production builds, we should re-evaluate this decision
+                // (bug 1286677).
+                multiDexEnabled true
+            }
         }
     }
 
     sourceSets {
         main {
             manifest.srcFile "${project.buildDir}/generated/source/preprocessed_manifest/AndroidManifest.xml"
 
             java {
@@ -166,16 +180,18 @@ android {
             // we have tests that start test servers and the bound ports
             // collide.  We'll fix this soon to have much faster test cycles.
             maxParallelForks 1
         }
     }
 }
 
 dependencies {
+    compile 'com.android.support:multidex:1.0.0'
+
     compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
     compile "com.android.support:appcompat-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
     compile "com.android.support:cardview-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
     compile "com.android.support:recyclerview-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
     compile "com.android.support:design:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
     compile "com.android.support:customtabs:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
 
     if (mozconfig.substs.MOZ_NATIVE_DEVICES) {
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -915,8 +915,12 @@ pref("identity.sync.tokenserver.uri", "h
 
 // Enable Presentation API
 pref("dom.presentation.enabled", true);
 pref("dom.presentation.discovery.enabled", true);
 pref("dom.presentation.discovery.legacy.enabled", true); // for TV 2.5 backward capability
 
 pref("dom.audiochannel.audioCompeting", true);
 pref("dom.audiochannel.mediaControl", true);
+
+// Space separated list of URLS that are allowed to send objects (instead of
+// only strings) through webchannels. This list is duplicated in browser/app/profile/firefox.js
+pref("webchannel.allowObject.urlWhitelist", "https://accounts.firefox.com https://content.cdn.mozilla.net https://hello.firefox.com https://input.mozilla.org https://support.mozilla.org https://install.mozilla.org");
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -274,16 +274,20 @@
                   android:theme="@style/Gecko.Preferences"
                   android:configChanges="orientation|screenSize|locale|layoutDirection"
                   android:excludeFromRecents="true"/>
 
         <provider android:name="org.mozilla.gecko.db.BrowserProvider"
                   android:authorities="@ANDROID_PACKAGE_NAME@.db.browser"
                   android:exported="false"/>
 
+        <provider android:name="org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy"
+                  android:authorities="@ANDROID_PACKAGE_NAME@.partnerbookmarks"
+                  android:exported="false"/>
+
         <!-- Share overlay activity
 
              Setting launchMode="singleTop" ensures onNewIntent is called when the Activity is
              reused. Ideally we create a new instance but Android L breaks this (bug 1137928). -->
         <activity android:name="org.mozilla.gecko.overlays.ui.ShareDialog"
                   android:label="@string/overlay_share_label"
                   android:theme="@style/OverlayActivity"
                   android:configChanges="keyboard|keyboardHidden|mcc|mnc|locale|layoutDirection"
--- a/mobile/android/base/AppConstants.java.in
+++ b/mobile/android/base/AppConstants.java.in
@@ -1,17 +1,21 @@
 //#filter substitution
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko;
 
+import android.content.Context;
 import android.os.Build;
+//#ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
+import android.support.multidex.MultiDex;
+//#endif
 
 /**
  * A collection of constants that pertain to the build and runtime state of the
  * application. Typically these are sourced from build-time definitions (see
  * Makefile.in). This is a Java-side substitute for nsIXULAppInfo, amongst
  * other things.
  *
  * See also SysInfo.java, which includes some of the values available from
@@ -338,9 +342,26 @@ public class AppConstants {
 //#ifdef MOZ_ANDROID_CUSTOM_TABS
     true;
 //#else
     false;
 //#endif
 
     // (bug 1266820) Temporarily disabled since no one is working on it.
     public static final boolean SCREENSHOTS_IN_BOOKMARKS_ENABLED = false;
+
+    /**
+     * Enables multidex depending on build flags. For more information,
+     * see `multiDexEnabled true` in mobile/android/app/build.gradle.
+     *
+     * As a method, this shouldn't be in AppConstants, but it's
+     * the only semi-relevant Java file that we pre-process.
+     */
+    public static void maybeInstallMultiDex(final Context context) {
+//#ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
+        if (BuildConfig.FLAVOR.equals("automation")) {
+            MultiDex.install(context);
+        }
+//#else
+        // Do nothing.
+//#endif
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
@@ -134,16 +134,22 @@ public class GeckoApplication extends Ap
         final Context applicationContext = getApplicationContext();
         GeckoBatteryManager.getInstance().start(applicationContext);
         GeckoNetworkManager.getInstance().start();
 
         mInBackground = false;
     }
 
     @Override
+    protected void attachBaseContext(Context base) {
+        super.attachBaseContext(base);
+        AppConstants.maybeInstallMultiDex(base);
+    }
+
+    @Override
     public void onCreate() {
         Log.i(LOG_TAG, "zerdatime " + SystemClock.uptimeMillis() + " - Fennec application start");
 
         mRefWatcher = LeakCanary.install(this);
 
         final Context context = getApplicationContext();
         HardwareUtils.init(context);
         Clipboard.init(context);
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderClient.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * 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/. */
-
-package org.mozilla.gecko.distribution;
-
-import android.content.ContentResolver;
-import android.database.Cursor;
-import android.net.Uri;
-
-import org.mozilla.gecko.db.BrowserContract;
-
-/**
- * Client for reading Android's PartnerBookmarksProvider.
- *
- * Note: This client is only invoked for distributions. Without a distribution the content provider
- *       will not be read and no bookmarks will be added to the UI.
- */
-public class PartnerBookmarksProviderClient {
-    /**
-     * The contract between the partner bookmarks provider and applications. Contains the definition
-     * for the supported URIs and columns.
-     */
-    private static class PartnerContract {
-        public static final Uri CONTENT_URI = Uri.parse("content://com.android.partnerbookmarks/bookmarks");
-
-        public static final int TYPE_BOOKMARK = 1;
-        public static final int TYPE_FOLDER = 2;
-
-        public static final int PARENT_ROOT_ID = 0;
-
-        public static final String ID = "_id";
-        public static final String TYPE = "type";
-        public static final String URL = "url";
-        public static final String TITLE = "title";
-        public static final String FAVICON = "favicon";
-        public static final String TOUCHICON = "touchicon";
-        public static final String PARENT = "parent";
-    }
-
-    public static Cursor getBookmarksInFolder(ContentResolver contentResolver, int folderId) {
-        // Use root folder id or transform negative id into actual (positive) folder id.
-        final long actualFolderId = folderId == BrowserContract.Bookmarks.FIXED_ROOT_ID
-                ? PartnerContract.PARENT_ROOT_ID
-                : BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - folderId;
-
-        return contentResolver.query(
-                PartnerContract.CONTENT_URI,
-                new String[] {
-                        // Transform ids into negative values starting with FAKE_PARTNER_BOOKMARKS_START.
-                        "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Bookmarks._ID,
-                        PartnerContract.TITLE + " as " + BrowserContract.Bookmarks.TITLE,
-                        PartnerContract.URL +  " as " + BrowserContract.Bookmarks.URL,
-                        // Transform parent ids to negative ids as well
-                        "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.PARENT + ") as " + BrowserContract.Bookmarks.PARENT,
-                        // Convert types (we use 0-1 and the partner provider 1-2)
-                        "(2 - " + PartnerContract.TYPE + ") as " + BrowserContract.Bookmarks.TYPE,
-                        // Use the ID of the entry as GUID
-                        PartnerContract.ID + " as " + BrowserContract.Bookmarks.GUID
-                },
-                PartnerContract.PARENT + " = ?"
-                        // Only select entries with valid type
-                        + " AND (" + BrowserContract.Bookmarks.TYPE + " = 1 OR " + BrowserContract.Bookmarks.TYPE + " = 2)"
-                        // Only select entries with non empty title
-                        + " AND " + BrowserContract.Bookmarks.TITLE + " <> ''",
-                new String[] { String.valueOf(actualFolderId) },
-                // Same order we use in our content provider (without position)
-                BrowserContract.Bookmarks.TYPE + " ASC, " + BrowserContract.Bookmarks._ID + " ASC");
-    }
-}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderProxy.java
@@ -0,0 +1,322 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+package org.mozilla.gecko.distribution;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.db.BrowserContract;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A proxy for the partner bookmarks provider. Bookmark and folder ids of the partner bookmarks providers
+ * will be transformed so that they do not overlap with the ids from the local database.
+ *
+ * Bookmarks in folder:
+ *   content://{PACKAGE_ID}.partnerbookmarks/bookmarks/{folderId}
+ * Icon of bookmark:
+ *   content://{PACKAGE_ID}.partnerbookmarks/icons/{bookmarkId}
+ */
+public class PartnerBookmarksProviderProxy extends ContentProvider {
+    /**
+     * The contract between the partner bookmarks provider and applications. Contains the definition
+     * for the supported URIs and columns.
+     */
+    public static class PartnerContract {
+        public static final Uri CONTENT_URI = Uri.parse("content://com.android.partnerbookmarks/bookmarks");
+
+        public static final int TYPE_BOOKMARK = 1;
+        public static final int TYPE_FOLDER = 2;
+
+        public static final int PARENT_ROOT_ID = 0;
+
+        public static final String ID = "_id";
+        public static final String TYPE = "type";
+        public static final String URL = "url";
+        public static final String TITLE = "title";
+        public static final String FAVICON = "favicon";
+        public static final String TOUCHICON = "touchicon";
+        public static final String PARENT = "parent";
+    }
+
+    private static final String AUTHORITY_PREFIX = ".partnerbookmarks";
+
+    private static final int URI_MATCH_BOOKMARKS = 1000;
+    private static final int URI_MATCH_ICON = 1001;
+    private static final int URI_MATCH_BOOKMARK = 1002;
+
+    private static final String PREF_DELETED_PARTNER_BOOKMARKS = "distribution.partner.bookmark.deleted";
+
+    /**
+     * Cursor wrapper for filtering empty folders.
+     */
+    private static class FilteredCursor extends CursorWrapper {
+        private HashSet<Integer> emptyFolderPositions;
+        private int count;
+
+        public FilteredCursor(PartnerBookmarksProviderProxy proxy, Cursor cursor) {
+            super(cursor);
+
+            emptyFolderPositions = new HashSet<>();
+            count = cursor.getCount();
+
+            for (int i = 0; i < cursor.getCount(); i++) {
+                cursor.moveToPosition(i);
+
+                final long id = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID));
+                final int type = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.TYPE));
+
+                if (type == BrowserContract.Bookmarks.TYPE_FOLDER && proxy.isFolderEmpty(id)) {
+                    // We do not support deleting folders. So at least hide partner folders that are
+                    // empty because all bookmarks inside it are deleted/hidden.
+                    // Note that this will still show folders with empty folders in them. But multi-level
+                    // partner bookmarks are very unlikely.
+
+                    count--;
+                    emptyFolderPositions.add(i);
+                }
+            }
+        }
+
+        @Override
+        public int getCount() {
+            return count;
+        }
+
+        @Override
+        public boolean moveToPosition(int position) {
+            final Cursor cursor = getWrappedCursor();
+            final int actualCount = cursor.getCount();
+
+            // Find the next position pointing to a bookmark or a non-empty folder
+            while (position < actualCount && emptyFolderPositions.contains(position)) {
+                position++;
+            }
+
+            return position < actualCount && cursor.moveToPosition(position);
+        }
+    }
+
+    private static String getAuthority(Context context) {
+        return context.getPackageName() + AUTHORITY_PREFIX;
+    }
+
+    public static Uri getUriForBookmarks(Context context, long folderId) {
+        return new Uri.Builder()
+                .scheme("content")
+                .authority(getAuthority(context))
+                .appendPath("bookmarks")
+                .appendPath(String.valueOf(folderId))
+                .build();
+    }
+
+    public static Uri getUriForIcon(Context context, long bookmarkId) {
+        return new Uri.Builder()
+                .scheme("content")
+                .authority(getAuthority(context))
+                .appendPath("icons")
+                .appendPath(String.valueOf(bookmarkId))
+                .build();
+    }
+
+    public static Uri getUriForBookmark(Context context, long bookmarkId) {
+        return new Uri.Builder()
+                .scheme("content")
+                .authority(getAuthority(context))
+                .appendPath("bookmark")
+                .appendPath(String.valueOf(bookmarkId))
+                .build();
+    }
+
+    private final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    @Override
+    public boolean onCreate() {
+        String authority = getAuthority(assertAndGetContext());
+
+        uriMatcher.addURI(authority, "bookmarks/*", URI_MATCH_BOOKMARKS);
+        uriMatcher.addURI(authority, "icons/*", URI_MATCH_ICON);
+        uriMatcher.addURI(authority, "bookmark/*", URI_MATCH_BOOKMARK);
+
+        return true;
+    }
+
+    @Override
+    public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+        final Context context = assertAndGetContext();
+        final int match = uriMatcher.match(uri);
+
+        final ContentResolver contentResolver = context.getContentResolver();
+
+        switch (match) {
+            case URI_MATCH_BOOKMARKS:
+                final long bookmarkId = ContentUris.parseId(uri);
+                if (bookmarkId == -1) {
+                    throw new IllegalArgumentException("Bookmark id is not a number");
+                }
+                final Cursor cursor = getBookmarksInFolder(contentResolver, bookmarkId);
+                cursor.setNotificationUri(context.getContentResolver(), uri);
+                return new FilteredCursor(this, cursor);
+
+            case URI_MATCH_ICON:
+                return getIcon(contentResolver, ContentUris.parseId(uri));
+
+            default:
+                throw new UnsupportedOperationException("Unknown URI " + uri.toString());
+        }
+    }
+
+    @Override
+    public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
+        final int match = uriMatcher.match(uri);
+
+        switch (match) {
+            case URI_MATCH_BOOKMARK:
+                rememberRemovedBookmark(ContentUris.parseId(uri));
+                notifyBookmarkChange();
+                return 1;
+
+            default:
+                throw new UnsupportedOperationException("Unknown URI " + uri.toString());
+        }
+    }
+
+    private void notifyBookmarkChange() {
+        final Context context = assertAndGetContext();
+
+        context.getContentResolver().notifyChange(
+                new Uri.Builder()
+                        .scheme("content")
+                        .authority(getAuthority(context))
+                        .appendPath("bookmarks")
+                        .build(),
+                null);
+    }
+
+    private synchronized void rememberRemovedBookmark(long bookmarkId) {
+        Set<String> deletedIds = getRemovedBookmarkIds();
+
+        deletedIds.add(String.valueOf(bookmarkId));
+
+        GeckoSharedPrefs.forProfile(assertAndGetContext())
+                .edit()
+                .putStringSet(PREF_DELETED_PARTNER_BOOKMARKS, deletedIds)
+                .apply();
+    }
+
+    private synchronized Set<String> getRemovedBookmarkIds() {
+        SharedPreferences preferences = GeckoSharedPrefs.forProfile(assertAndGetContext());
+        return preferences.getStringSet(PREF_DELETED_PARTNER_BOOKMARKS, new HashSet<String>());
+    }
+
+    private Cursor getBookmarksInFolder(ContentResolver contentResolver, long folderId) {
+        // Use root folder id or transform negative id into actual (positive) folder id.
+        final long actualFolderId = folderId == BrowserContract.Bookmarks.FIXED_ROOT_ID
+                ? PartnerContract.PARENT_ROOT_ID
+                : BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - folderId;
+
+        final String removedBookmarkIds = TextUtils.join(",", getRemovedBookmarkIds());
+
+        return contentResolver.query(
+                PartnerContract.CONTENT_URI,
+                new String[] {
+                        // Transform ids into negative values starting with FAKE_PARTNER_BOOKMARKS_START.
+                        "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Bookmarks._ID,
+                        "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Combined.BOOKMARK_ID,
+                        PartnerContract.TITLE + " as " + BrowserContract.Bookmarks.TITLE,
+                        PartnerContract.URL +  " as " + BrowserContract.Bookmarks.URL,
+                        // Transform parent ids to negative ids as well
+                        "(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.PARENT + ") as " + BrowserContract.Bookmarks.PARENT,
+                        // Convert types (we use 0-1 and the partner provider 1-2)
+                        "(2 - " + PartnerContract.TYPE + ") as " + BrowserContract.Bookmarks.TYPE,
+                        // Use the ID of the entry as GUID
+                        PartnerContract.ID + " as " + BrowserContract.Bookmarks.GUID
+                },
+                PartnerContract.PARENT + " = ?"
+                        // We only want to read bookmarks or folders from the content provider
+                        + " AND " + BrowserContract.Bookmarks.TYPE + " IN (?,?)"
+                        // Only select entries with non empty title
+                        + " AND " + BrowserContract.Bookmarks.TITLE + " <> ''"
+                        // Filter all "deleted" ids
+                        + " AND " + BrowserContract.Combined.BOOKMARK_ID + " NOT IN (" + removedBookmarkIds + ")",
+                new String[] {
+                        String.valueOf(actualFolderId),
+                        String.valueOf(PartnerContract.TYPE_BOOKMARK),
+                        String.valueOf(PartnerContract.TYPE_FOLDER)
+                },
+                // Same order we use in our content provider (without position)
+                BrowserContract.Bookmarks.TYPE + " ASC, " + BrowserContract.Bookmarks._ID + " ASC");
+    }
+
+    private boolean isFolderEmpty(long folderId) {
+        final Context context = assertAndGetContext();
+        final Cursor cursor = getBookmarksInFolder(context.getContentResolver(), folderId);
+
+        if (cursor == null) {
+            return true;
+        }
+
+        try {
+            return cursor.getCount() == 0;
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private Cursor getIcon(ContentResolver contentResolver, long bookmarkId) {
+        final long actualId = BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - bookmarkId;
+
+        return contentResolver.query(
+                PartnerContract.CONTENT_URI,
+                new String[] {
+                    PartnerContract.TOUCHICON,
+                    PartnerContract.FAVICON
+                },
+                PartnerContract.ID + " = ?",
+                new String[] {
+                    String.valueOf(actualId)
+                },
+                null);
+    }
+
+    private Context assertAndGetContext() {
+        final Context context = super.getContext();
+
+        if (context == null) {
+            throw new AssertionError("Context is null");
+        }
+
+        return context;
+    }
+
+    @Override
+    public String getType(@NonNull Uri uri) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Uri insert(@NonNull Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/favicons/LoadFaviconTask.java
+++ b/mobile/android/base/java/org/mozilla/gecko/favicons/LoadFaviconTask.java
@@ -1,22 +1,26 @@
 /* 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/. */
 
 package org.mozilla.gecko.favicons;
 
 import android.content.ContentResolver;
 import android.content.Context;
+import android.database.Cursor;
 import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
 import android.text.TextUtils;
 import android.util.Log;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
 import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
 import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
 import org.mozilla.gecko.util.GeckoJarReader;
 import org.mozilla.gecko.util.IOUtils;
 import org.mozilla.gecko.util.ProxySelector;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.io.IOException;
@@ -52,16 +56,22 @@ public class LoadFaviconTask {
     /**
      * Bypass all caches - this is used to directly retrieve the requested icon. Without this flag,
      * favicons will first be pushed into the memory cache (and possibly permanent cache if using FLAG_PERSIST),
      * where they will be downscaled to the maximum cache size, before being retrieved from the cache (resulting
      * in a possibly smaller icon size).
      */
     public static final int FLAG_BYPASS_CACHE_WHEN_DOWNLOADING_ICONS = 2;
 
+    /**
+     * If downloading from the favicon URL failed then do NOT try to guess the default URL and
+     * download from the default URL.
+     */
+    public static final int FLAG_NO_DOWNLOAD_FROM_GUESSED_DEFAULT_URL = 4;
+
     private static final int MAX_REDIRECTS_TO_FOLLOW = 5;
     // The default size of the buffer to use for downloading Favicons in the event no size is given
     // by the server.
     public static final int DEFAULT_FAVICON_BUFFER_SIZE = 25000;
 
     private static final AtomicInteger nextFaviconLoadId = new AtomicInteger(0);
     private final Context context;
     private final int id;
@@ -193,16 +203,83 @@ public class LoadFaviconTask {
                 // Just about anything could happen here.
                 Log.w(LOGTAG, "Error fetching favicon from JAR.", e);
                 return null;
             }
         }
         return null;
     }
 
+    /**
+     * Fetch icon from a content provider following the partner bookmarks provider contract.
+     */
+    private Bitmap fetchContentProviderFavicon(String uri, int targetWidthAndHeight) {
+        if (TextUtils.isEmpty(uri)) {
+            return null;
+        }
+
+        if (!uri.startsWith("content://")) {
+            return null;
+        }
+
+        Cursor cursor = context.getContentResolver().query(
+                Uri.parse(uri),
+                new String[] {
+                        PartnerBookmarksProviderProxy.PartnerContract.TOUCHICON,
+                        PartnerBookmarksProviderProxy.PartnerContract.FAVICON,
+                },
+                null,
+                null,
+                null
+        );
+
+        if (cursor == null) {
+            return null;
+        }
+
+        try {
+            if (!cursor.moveToFirst()) {
+                return null;
+            }
+
+            Bitmap icon = decodeFromCursor(cursor, PartnerBookmarksProviderProxy.PartnerContract.TOUCHICON, targetWidthAndHeight);
+            if (icon != null) {
+                return icon;
+            }
+
+            icon = decodeFromCursor(cursor, PartnerBookmarksProviderProxy.PartnerContract.FAVICON, targetWidthAndHeight);
+            if (icon != null) {
+                return icon;
+            }
+        } finally {
+            cursor.close();
+        }
+
+        return null;
+    }
+
+    private Bitmap decodeFromCursor(Cursor cursor, String column, int targetWidthAndHeight) {
+        final int index = cursor.getColumnIndex(column);
+        if (index == -1) {
+            return null;
+        }
+
+        if (cursor.isNull(index)) {
+            return null;
+        }
+
+        final byte[] data = cursor.getBlob(index);
+        LoadFaviconResult result = FaviconDecoder.decodeFavicon(data, 0, data.length);
+        if (result == null) {
+            return null;
+        }
+
+        return result.getBestBitmap(targetWidthAndHeight);
+    }
+
     // Runs in background thread.
     // Does not attempt to fetch from JARs.
     private LoadFaviconResult downloadFavicon(URI targetFaviconURI) {
         if (targetFaviconURI == null) {
             return null;
         }
 
         // Only get favicons for HTTP/HTTPS.
@@ -418,16 +495,24 @@ public class LoadFaviconTask {
         // Let's see if it's in a JAR.
         image = fetchJARFavicon(faviconURL);
         if (imageIsValid(image)) {
             // We don't want to put this into the DB.
             Favicons.putFaviconInMemCache(faviconURL, image);
             return image;
         }
 
+        // Download from a content provider
+        image = fetchContentProviderFavicon(faviconURL, targetWidthAndHeight);
+        if (imageIsValid(image)) {
+            // We don't want to put this into the DB.
+            Favicons.putFaviconInMemCache(faviconURL, image);
+            return image;
+        }
+
         try {
             loadedBitmaps = downloadFavicon(new URI(faviconURL));
         } catch (URISyntaxException e) {
             Log.e(LOGTAG, "The provided favicon URL is not valid");
             return null;
         } catch (Exception e) {
             Log.e(LOGTAG, "Couldn't download favicon.", e);
         }
@@ -435,40 +520,22 @@ public class LoadFaviconTask {
         if (loadedBitmaps != null) {
             if ((flags & FLAG_BYPASS_CACHE_WHEN_DOWNLOADING_ICONS) == 0) {
                 // Fetching bytes to store can fail. saveFaviconToDb will
                 // do the right thing, but we still choose to cache the
                 // downloaded icon in memory.
                 saveFaviconToDb(db, loadedBitmaps.getBytesForDatabaseStorage());
                 return pushToCacheAndGetResult(loadedBitmaps);
             } else {
-                final Map<Integer, Bitmap> iconMap = new HashMap<>();
-                final List<Integer> sizes = new ArrayList<>();
-
-                while (loadedBitmaps.getBitmaps().hasNext()) {
-                    final Bitmap b = loadedBitmaps.getBitmaps().next();
+                return loadedBitmaps.getBestBitmap(targetWidthAndHeight);
+            }
+        }
 
-                    // It's possible to receive null, most likely due to OOM or a zero-sized image,
-                    // from BitmapUtils.decodeByteArray(byte[], int, int, BitmapFactory.Options)
-                    if (b != null) {
-                        iconMap.put(b.getWidth(), b);
-                        sizes.add(b.getWidth());
-                    }
-                }
-
-                int bestSize = Favicons.selectBestSizeFromList(sizes, targetWidthAndHeight);
-
-                if (bestSize == -1) {
-                    // No icons found: this could occur if we weren't able to process any of the
-                    // supplied icons.
-                    return null;
-                }
-
-                return iconMap.get(bestSize);
-            }
+        if ((FLAG_NO_DOWNLOAD_FROM_GUESSED_DEFAULT_URL & flags) == FLAG_NO_DOWNLOAD_FROM_GUESSED_DEFAULT_URL) {
+            return null;
         }
 
         if (isUsingDefaultURL) {
             Favicons.putFaviconInFailedCache(faviconURL);
             return null;
         }
 
         if (isCancelled()) {
--- a/mobile/android/base/java/org/mozilla/gecko/favicons/decoders/LoadFaviconResult.java
+++ b/mobile/android/base/java/org/mozilla/gecko/favicons/decoders/LoadFaviconResult.java
@@ -1,19 +1,26 @@
 /* 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/. */
 
 package org.mozilla.gecko.favicons.decoders;
 
 import android.graphics.Bitmap;
 import android.util.Log;
+import android.util.SparseArray;
+
+import org.mozilla.gecko.favicons.Favicons;
 
 import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 
 /**
  * Class representing the result of loading a favicon.
  * This operation will produce either a collection of favicons, a single favicon, or no favicon.
  * It is necessary to model single favicons differently to a collection of one favicon (An entity
  * that may not exist with this scheme) since the in-database representation of these things differ.
  * (In particular, collections of favicons are stored in encoded ICO format, whereas single icons are
  * stored as decoded bitmap blobs.)
@@ -68,9 +75,34 @@ public class LoadFaviconResult {
         } catch (OutOfMemoryError e) {
             Log.w(LOGTAG, "Out of memory re-compressing favicon.");
         }
 
         Log.w(LOGTAG, "Favicon re-compression failed.");
         return null;
     }
 
+    public Bitmap getBestBitmap(int targetWidthAndHeight) {
+        final SparseArray<Bitmap> iconMap = new SparseArray<>();
+        final List<Integer> sizes = new ArrayList<>();
+
+        while (bitmapsDecoded.hasNext()) {
+            final Bitmap b = bitmapsDecoded.next();
+
+            // It's possible to receive null, most likely due to OOM or a zero-sized image,
+            // from BitmapUtils.decodeByteArray(byte[], int, int, BitmapFactory.Options)
+            if (b != null) {
+                iconMap.put(b.getWidth(), b);
+                sizes.add(b.getWidth());
+            }
+        }
+
+        int bestSize = Favicons.selectBestSizeFromList(sizes, targetWidthAndHeight);
+
+        if (bestSize == -1) {
+            // No icons found: this could occur if we weren't able to process any of the
+            // supplied icons.
+            return null;
+        }
+
+        return iconMap.get(bestSize);
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java
@@ -10,17 +10,17 @@ import java.util.LinkedList;
 import java.util.List;
 
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.distribution.PartnerBookmarksProviderClient;
+import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
 import org.mozilla.gecko.home.BookmarksListAdapter.FolderInfo;
 import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener;
 import org.mozilla.gecko.home.BookmarksListAdapter.RefreshType;
 import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 
 import android.app.Activity;
@@ -230,17 +230,17 @@ public class BookmarksPanel extends Home
 
             final ContentResolver contentResolver = getContext().getContentResolver();
 
             Cursor partnerCursor = null;
             Cursor userCursor = null;
 
             if (GeckoSharedPrefs.forProfile(getContext()).getBoolean(GeckoPreferences.PREFS_READ_PARTNER_BOOKMARKS_PROVIDER, false)
                     && (isRootFolder || mFolderInfo.id <= Bookmarks.FAKE_PARTNER_BOOKMARKS_START)) {
-                partnerCursor = PartnerBookmarksProviderClient.getBookmarksInFolder(contentResolver, mFolderInfo.id);
+                partnerCursor = contentResolver.query(PartnerBookmarksProviderProxy.getUriForBookmarks(getContext(), mFolderInfo.id), null, null, null, null, null);
             }
 
             if (isRootFolder || mFolderInfo.id > Bookmarks.FAKE_PARTNER_BOOKMARKS_START) {
                 userCursor = mDB.getBookmarksInFolder(contentResolver, mFolderInfo.id);
             }
 
 
             if (partnerCursor == null && userCursor == null) {
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeContextMenuInfo.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeContextMenuInfo.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.home;
 
+import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.util.StringUtils;
 
 import android.database.Cursor;
 import android.text.TextUtils;
 import android.view.View;
 import android.widget.AdapterView.AdapterContextMenuInfo;
 import android.widget.ExpandableListAdapter;
 import android.widget.ListAdapter;
@@ -38,18 +39,22 @@ public class HomeContextMenuInfo extends
     public boolean hasBookmarkId() {
         return bookmarkId > -1;
     }
 
     public boolean hasHistoryId() {
         return historyId > -1;
     }
 
+    public boolean hasPartnerBookmarkId() {
+        return bookmarkId <= BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START;
+    }
+
     public boolean canRemove() {
-        return hasBookmarkId() || hasHistoryId();
+        return hasBookmarkId() || hasHistoryId() || hasPartnerBookmarkId();
     }
 
     public String getDisplayTitle() {
         if (!TextUtils.isEmpty(title)) {
             return title;
         }
         return StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url, StringUtils.UrlFlags.STRIP_HTTPS));
     }
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
@@ -11,18 +11,20 @@ import org.mozilla.gecko.EditBookmarkDia
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoApplication;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.IntentHelper;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.SnackbarBuilder;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
+import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
 import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
 import org.mozilla.gecko.reader.SavedReaderViewHelper;
 import org.mozilla.gecko.reader.ReadingListHelper;
 import org.mozilla.gecko.restrictions.Restrictable;
 import org.mozilla.gecko.restrictions.Restrictions;
@@ -289,17 +291,21 @@ public abstract class HomeFragment exten
             new EditBookmarkDialog(context).show(info.url);
             return true;
         }
 
         if (itemId == R.id.home_remove) {
             // For Top Sites grid items, position is required in case item is Pinned.
             final int position = info instanceof TopSitesGridContextMenuInfo ? info.position : -1;
 
-            (new RemoveItemByUrlTask(context, info.url, info.itemType, position)).execute();
+            if (info.hasPartnerBookmarkId()) {
+                new RemovePartnerBookmarkTask(context, info.bookmarkId).execute();
+            } else {
+                new RemoveItemByUrlTask(context, info.url, info.itemType, position).execute();
+            }
             return true;
         }
 
         return false;
     }
 
     @Override
     public void setUserVisibleHint (boolean isVisibleToUser) {
@@ -437,9 +443,40 @@ public abstract class HomeFragment exten
         @Override
         public void onPostExecute(Void result) {
             SnackbarBuilder.builder((Activity) mContext)
                     .message(R.string.page_removed)
                     .duration(Snackbar.LENGTH_LONG)
                     .buildAndShow();
         }
     }
+
+    private static class RemovePartnerBookmarkTask extends UIAsyncTask.WithoutParams<Void> {
+        private Context context;
+        private long bookmarkId;
+
+        public RemovePartnerBookmarkTask(Context context, long bookmarkId) {
+            super(ThreadUtils.getBackgroundHandler());
+
+            this.context = context;
+            this.bookmarkId = bookmarkId;
+        }
+
+        @Override
+        protected Void doInBackground() {
+            context.getContentResolver().delete(
+                    PartnerBookmarksProviderProxy.getUriForBookmark(context, bookmarkId),
+                    null,
+                    null
+            );
+
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void aVoid) {
+            SnackbarBuilder.builder((Activity) context)
+                    .message(R.string.page_removed)
+                    .duration(Snackbar.LENGTH_LONG)
+                    .buildAndShow();
+        }
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
@@ -4,16 +4,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
 import java.lang.ref.WeakReference;
 
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
+import org.mozilla.gecko.favicons.LoadFaviconTask;
 import org.mozilla.gecko.reader.SavedReaderViewHelper;
 import org.mozilla.gecko.reader.ReaderModeUtils;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
@@ -265,18 +268,18 @@ public class TwoLinePageRow extends Line
      */
     public void update(String title, String url) {
         update(title, url, 0, false);
     }
 
     protected void update(String title, String url, long bookmarkId, boolean hasReaderCacheItem) {
         if (mShowIcons) {
             // The bookmark id will be 0 (null in database) when the url
-            // is not a bookmark.
-            final boolean isBookmark = bookmarkId != 0;
+            // is not a bookmark and negative for 'fake' bookmarks.
+            final boolean isBookmark = bookmarkId > 0;
 
             updateStatusIcon(isBookmark, hasReaderCacheItem);
         } else {
             updateStatusIcon(false, false);
         }
 
         // Use the URL instead of an empty title for consistency with the normal URL
         // bar view - this is the equivalent of getDisplayTitle() in Tab.java
@@ -288,19 +291,33 @@ public class TwoLinePageRow extends Line
         }
 
         // Blank the Favicon, so we don't show the wrong Favicon if we scroll and miss DB.
         mFavicon.clearImage();
         Favicons.cancelFaviconLoad(mLoadFaviconJobId);
 
         // Displayed RecentTabsPanel URLs may refer to pages opened in reader mode, so we
         // remove the about:reader prefix to ensure the Favicon loads properly.
-        final String pageURL = AboutPages.isAboutReader(url) ?
-            ReaderModeUtils.getUrlFromAboutReader(url) : url;
-        mLoadFaviconJobId = Favicons.getSizedFaviconForPageFromLocal(getContext(), pageURL, mFaviconListener);
+        final String pageURL = AboutPages.isAboutReader(url) ? ReaderModeUtils.getUrlFromAboutReader(url) : url;
+
+        if (bookmarkId < BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START) {
+            mLoadFaviconJobId = Favicons.getSizedFavicon(
+                    getContext(),
+                    pageURL,
+                    PartnerBookmarksProviderProxy.getUriForIcon(getContext(), bookmarkId).toString(),
+                    Favicons.LoadType.PRIVILEGED,
+                    Favicons.defaultFaviconSize,
+                    // We want to load the favicon from the content provider but we do not want the
+                    // favicon loader to fallback to loading a favicon from the web using a guessed
+                    // default URL.
+                    LoadFaviconTask.FLAG_NO_DOWNLOAD_FROM_GUESSED_DEFAULT_URL,
+                    mFaviconListener);
+        } else {
+            mLoadFaviconJobId = Favicons.getSizedFaviconForPageFromLocal(getContext(), pageURL, mFaviconListener);
+        }
 
         updateDisplayedUrl(url, hasReaderCacheItem);
     }
 
     /**
      * Update the data displayed by this row.
      * <p>
      * This method must be invoked on the UI thread.
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -263,17 +263,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'delegates/BrowserAppDelegate.java',
     'delegates/BrowserAppDelegateWithReference.java',
     'delegates/OfflineTabStatusDelegate.java',
     'delegates/ScreenshotDelegate.java',
     'delegates/TabsTrayVisibilityAwareDelegate.java',
     'DevToolsAuthHelper.java',
     'distribution/Distribution.java',
     'distribution/DistributionStoreCallback.java',
-    'distribution/PartnerBookmarksProviderClient.java',
+    'distribution/PartnerBookmarksProviderProxy.java',
     'distribution/PartnerBrowserCustomizationsClient.java',
     'distribution/ReferrerDescriptor.java',
     'distribution/ReferrerReceiver.java',
     'dlc/BaseAction.java',
     'dlc/catalog/DownloadContent.java',
     'dlc/catalog/DownloadContentBootstrap.java',
     'dlc/catalog/DownloadContentBuilder.java',
     'dlc/catalog/DownloadContentCatalog.java',
--- a/mobile/android/chrome/content/aboutAddons.js
+++ b/mobile/android/chrome/content/aboutAddons.js
@@ -20,16 +20,18 @@ XPCOMUtils.defineLazyGetter(window, "gCh
   return window.QueryInterface(Ci.nsIInterfaceRequestor)
            .getInterface(Ci.nsIWebNavigation)
            .QueryInterface(Ci.nsIDocShellTreeItem)
            .rootTreeItem
            .QueryInterface(Ci.nsIInterfaceRequestor)
            .getInterface(Ci.nsIDOMWindow)
            .QueryInterface(Ci.nsIDOMChromeWindow);
 });
+XPCOMUtils.defineLazyModuleGetter(window, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
 
 var ContextMenus = {
   target: null,
 
   init: function() {
     document.addEventListener("contextmenu", this, false);
 
     document.getElementById("contextmenu-enable").addEventListener("click", ContextMenus.enable.bind(this), false);
--- a/mobile/android/themes/core/aboutAddons.css
+++ b/mobile/android/themes/core/aboutAddons.css
@@ -254,20 +254,16 @@ select {
   background: #fff;
   border: 1px solid #ccc;
   border-radius: 4px;
   padding: 1em;
 }
 
 /* XBL bindings */
 
-settings {
-  -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#settings");
-}
-
 setting {
   display: none;
 }
 
 setting[type="bool"] {
   display: -moz-box;
   -moz-binding: url("chrome://browser/content/bindings/settings.xml#setting-fulltoggle-bool");
 }
--- a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-reportValidity.html.ini
+++ b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-reportValidity.html.ini
@@ -37,28 +37,16 @@
     expected: FAIL
 
   [[INPUT in EMAIL status\] suffering from being too long (in a form)]
     expected: FAIL
 
   [[INPUT in DATETIME status\] The datetime type must be supported.]
     expected: FAIL
 
-  [[INPUT in MONTH status\] suffering from an overflow]
-    expected: FAIL
-
-  [[INPUT in MONTH status\] suffering from an overflow (in a form)]
-    expected: FAIL
-
-  [[INPUT in MONTH status\] suffering from an underflow]
-    expected: FAIL
-
-  [[INPUT in MONTH status\] suffering from an underflow (in a form)]
-    expected: FAIL
-
   [[INPUT in MONTH status\] suffering from a step mismatch]
     expected: FAIL
 
   [[INPUT in MONTH status\] suffering from a step mismatch (in a form)]
     expected: FAIL
 
   [[INPUT in WEEK status\] The week type must be supported.]
     expected: FAIL
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -22,18 +22,16 @@ const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.importGlobalProperties(["TextEncoder"]);
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/ExtensionContent.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
-                                  "resource://devtools/shared/event-emitter.js");
 XPCOMUtils.defineLazyModuleGetter(this, "Locale",
                                   "resource://gre/modules/Locale.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Log",
                                   "resource://gre/modules/Log.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
                                   "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
                                   "resource://gre/modules/MatchPattern.jsm");
@@ -91,16 +89,17 @@ ExtensionManagement.registerSchema("chro
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/test.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/events.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/web_navigation.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/web_request.json");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   BaseContext,
+  EventEmitter,
   LocaleData,
   Messenger,
   injectAPI,
   instanceOf,
   extend,
   flushJarCache,
 } = ExtensionUtils;
 
@@ -223,17 +222,17 @@ var Management = {
 
   // The ext-*.js scripts can ask to be notified for certain hooks.
   on(hook, callback) {
     this.emitter.on(hook, callback);
   },
 
   // Ask to run all the callbacks that are registered for a given hook.
   emit(hook, ...args) {
-    this.emitter.emit(hook, ...args);
+    return this.emitter.emit(hook, ...args);
   },
 
   off(hook, callback) {
     this.emitter.off(hook, callback);
   },
 };
 
 // An extension page is an execution context for any extension content
@@ -274,16 +273,21 @@ ExtensionContext = class extends BaseCon
     let filter = {extensionId: extension.id};
     this.messenger = new Messenger(this, [Services.mm, Services.ppmm], sender, filter, delegate);
 
     if (this.externallyVisible) {
       this.extension.views.add(this);
     }
   }
 
+  get docShell() {
+    return this.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDocShell);
+  }
+
   get cloneScope() {
     return this.contentWindow;
   }
 
   get principal() {
     return this.contentWindow.document.nodePrincipal;
   }
 
@@ -678,26 +682,21 @@ GlobalManager = {
 
   _initializeBackgroundPage(contentWindow) {
     // Override the `alert()` method inside background windows;
     // we alias it to console.log().
     // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1203394
     let alertDisplayedWarning = false;
     let alertOverwrite = text => {
       if (!alertDisplayedWarning) {
-        let consoleWindow = Services.wm.getMostRecentWindow("devtools:webconsole");
-        if (!consoleWindow) {
-          let {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
-          require("devtools/client/framework/devtools-browser");
-          let hudservice = require("devtools/client/webconsole/hudservice");
-          hudservice.toggleBrowserConsole().catch(Cu.reportError);
-        } else {
-          // the Browser Console was already open
-          consoleWindow.focus();
-        }
+        let {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+        require("devtools/client/framework/devtools-browser");
+
+        let hudservice = require("devtools/client/webconsole/hudservice");
+        hudservice.openBrowserConsoleOrFocus();
 
         contentWindow.console.warn("alert() is not supported in background windows; please use console.log instead.");
 
         alertDisplayedWarning = true;
       }
 
       contentWindow.console.log(text);
     };
@@ -1220,28 +1219,31 @@ this.Extension.generateXPI = function(id
  * @param {nsIFile} file
  * @param {nsIURI} rootURI
  */
 function MockExtension(id, file, rootURI) {
   this.id = id;
   this.file = file;
   this.rootURI = rootURI;
 
-  this._extension = null;
-  this._extensionPromise = new Promise(resolve => {
+  let promiseEvent = eventName => new Promise(resolve => {
     let onstartup = (msg, extension) => {
       if (extension.id == this.id) {
-        Management.off("startup", onstartup);
+        Management.off(eventName, onstartup);
 
         this._extension = extension;
         resolve(extension);
       }
     };
-    Management.on("startup", onstartup);
+    Management.on(eventName, onstartup);
   });
+
+  this._extension = null;
+  this._extensionPromise = promiseEvent("startup");
+  this._readyPromise = promiseEvent("ready");
 }
 
 MockExtension.prototype = {
   testMessage(...args) {
     return this._extension.testMessage(...args);
   },
 
   on(...args) {
@@ -1254,17 +1256,17 @@ MockExtension.prototype = {
     this._extensionPromise.then(extension => {
       extension.off(...args);
     });
   },
 
   startup() {
     return AddonManager.installTemporaryAddon(this.file).then(addon => {
       this.addon = addon;
-      return this._extensionPromise;
+      return this._readyPromise;
     });
   },
 
   shutdown() {
     this.addon.uninstall(true);
     return this.cleanupGeneratedFile();
   },
 
@@ -1383,30 +1385,33 @@ Extension.prototype = extend(Object.crea
     // Strip leading slashes from web_accessible_resources.
     let strippedWebAccessibleResources = [];
     if (manifest.web_accessible_resources) {
       strippedWebAccessibleResources = manifest.web_accessible_resources.map(path => path.replace(/^\/+/, ""));
     }
 
     this.webAccessibleResources = new MatchGlobs(strippedWebAccessibleResources);
 
+    let promises = [];
     for (let directive in manifest) {
       if (manifest[directive] !== null) {
-        Management.emit("manifest_" + directive, directive, this, manifest);
+        promises.push(Management.emit(`manifest_${directive}`, directive, this, manifest));
       }
     }
 
     let data = Services.ppmm.initialProcessData;
     if (!data["Extension:Extensions"]) {
       data["Extension:Extensions"] = [];
     }
     let serial = this.serialize();
     data["Extension:Extensions"].push(serial);
 
-    return this.broadcast("Extension:Startup", serial);
+    return this.broadcast("Extension:Startup", serial).then(() => {
+      return Promise.all(promises);
+    });
   },
 
   callOnClose(obj) {
     this.onShutdown.add(obj);
   },
 
   forgetOnClose(obj) {
     this.onShutdown.delete(obj);
@@ -1458,16 +1463,18 @@ Extension.prototype = extend(Object.crea
         return;
       }
 
       GlobalManager.init(this);
 
       Management.emit("startup", this);
 
       return this.runManifest(this.manifest);
+    }).then(() => {
+      Management.emit("ready", this);
     }).catch(e => {
       dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
       Cu.reportError(e);
 
       if (started) {
         ExtensionManagement.shutdownExtension(this.uuid);
       }
 
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -533,16 +533,85 @@ let IconDetails = {
     canvas.width = imageData.width;
     canvas.height = imageData.height;
     canvas.getContext("2d").putImageData(imageData, 0, 0);
 
     return canvas.toDataURL("image/png");
   },
 };
 
+const LISTENERS = Symbol("listeners");
+
+class EventEmitter {
+  constructor() {
+    this[LISTENERS] = new Map();
+  }
+
+  /**
+   * Adds the given function as a listener for the given event.
+   *
+   * The listener function may optionally return a Promise which
+   * resolves when it has completed all operations which event
+   * dispatchers may need to block on.
+   *
+   * @param {string} event
+   *       The name of the event to listen for.
+   * @param {function(string, ...any)} listener
+   *        The listener to call when events are emitted.
+   */
+  on(event, listener) {
+    if (!this[LISTENERS].has(event)) {
+      this[LISTENERS].set(event, new Set());
+    }
+
+    this[LISTENERS].get(event).add(listener);
+  }
+
+  /**
+   * Removes the given function as a listener for the given event.
+   *
+   * @param {string} event
+   *       The name of the event to stop listening for.
+   * @param {function(string, ...any)} listener
+   *        The listener function to remove.
+   */
+  off(event, listener) {
+    if (this[LISTENERS].has(event)) {
+      let set = this[LISTENERS].get(event);
+
+      set.delete(listener);
+      if (!set.size) {
+        this[LISTENERS].delete(event);
+      }
+    }
+  }
+
+  /**
+   * Triggers all listeners for the given event, and returns a promise
+   * which resolves when all listeners have been called, and any
+   * promises they have returned have likewise resolved.
+   *
+   * @param {string} event
+   *       The name of the event to emit.
+   * @param {any} args
+   *        Arbitrary arguments to pass to the listener functions, after
+   *        the event name.
+   * @returns {Promise}
+   */
+  emit(event, ...args) {
+    let listeners = this[LISTENERS].get(event) || new Set();
+
+    let promises = Array.from(listeners, listener => {
+      return runSafeSyncWithoutClone(listener, event, ...args);
+    });
+
+    return Promise.all(promises);
+  }
+}
+
 function LocaleData(data) {
   this.defaultLocale = data.defaultLocale;
   this.selectedLocale = data.selectedLocale;
   this.locales = data.locales || new Map();
   this.warnedMissingKeys = new Set();
 
   // Map(locale-name -> Map(message-key -> localized-string))
   //
@@ -943,16 +1012,61 @@ function promiseDocumentReady(doc) {
       if (event.target === event.currentTarget) {
         doc.removeEventListener("DOMContentLoaded", onReady, true);
         resolve(doc);
       }
     }, true);
   });
 }
 
+/**
+ * Returns a Promise which resolves when the given document is fully
+ * loaded.
+ *
+ * @param {Document} doc The document to await the load of.
+ * @returns {Promise<Document>}
+ */
+function promiseDocumentLoaded(doc) {
+  if (doc.readyState == "complete") {
+    return Promise.resolve(doc);
+  }
+
+  return new Promise(resolve => {
+    doc.defaultView.addEventListener("load", function onReady(event) {
+      doc.defaultView.removeEventListener("load", onReady);
+      resolve(doc);
+    });
+  });
+}
+
+/**
+ * Returns a Promise which resolves the given observer topic has been
+ * observed.
+ *
+ * @param {string} topic
+ *        The topic to observe.
+ * @param {function(nsISupports, string)} [test]
+ *        An optional test function which, when called with the
+ *        observer's subject and data, should return true if this is the
+ *        expected notification, false otherwise.
+ * @returns {Promise<object>}
+ */
+function promiseObserved(topic, test = () => true) {
+  return new Promise(resolve => {
+    let observer = (subject, topic, data) => {
+      if (test(subject, data)) {
+        Services.obs.removeObserver(observer, topic);
+        resolve({subject, data});
+      }
+    };
+    Services.obs.addObserver(observer, topic, false);
+  });
+}
+
+
 /*
  * Messaging primitives.
  */
 
 let gNextPortId = 1;
 
 // Abstraction for a Port object in the extension API. Each port has a unique ID.
 function Port(context, messageManager, name, id, sender) {
@@ -1401,23 +1515,26 @@ function normalizeTime(date) {
 this.ExtensionUtils = {
   detectLanguage,
   extend,
   flushJarCache,
   ignoreEvent,
   injectAPI,
   instanceOf,
   normalizeTime,
+  promiseDocumentLoaded,
   promiseDocumentReady,
+  promiseObserved,
   runSafe,
   runSafeSync,
   runSafeSyncWithoutClone,
   runSafeWithoutClone,
   BaseContext,
   DefaultWeakMap,
+  EventEmitter,
   EventManager,
   IconDetails,
   LocaleData,
   Messenger,
   PlatformInfo,
   SingletonEventManager,
   SpreadArgs,
   ChildAPIManager,
--- a/toolkit/components/extensions/ext-backgroundPage.js
+++ b/toolkit/components/extensions/ext-backgroundPage.js
@@ -1,116 +1,143 @@
 "use strict";
 
 var {interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+const {
+  promiseDocumentLoaded,
+  promiseObserved,
+} = ExtensionUtils;
+
+const XUL_URL = "data:application/vnd.mozilla.xul+xml;charset=utf-8," + encodeURI(
+  `<?xml version="1.0"?>
+  <window id="documentElement"/>`);
+
 // WeakMap[Extension -> BackgroundPage]
 var backgroundPagesMap = new WeakMap();
 
 // Responsible for the background_page section of the manifest.
 function BackgroundPage(options, extension) {
   this.extension = extension;
   this.page = options.page || null;
   this.isGenerated = !!options.scripts;
   this.contentWindow = null;
-  this.chromeWebNav = null;
+  this.windowlessBrowser = null;
   this.webNav = null;
   this.context = null;
 }
 
 BackgroundPage.prototype = {
-  build() {
-    let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
-    this.chromeWebNav = chromeWebNav;
+  build: Task.async(function* () {
+    let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
+    this.windowlessBrowser = windowlessBrowser;
 
     let url;
     if (this.page) {
       url = this.extension.baseURI.resolve(this.page);
     } else if (this.isGenerated) {
       url = this.extension.baseURI.resolve("_generated_background_page.html");
     }
 
     if (!this.extension.isExtensionURL(url)) {
       this.extension.manifestError("Background page must be a file within the extension");
       url = this.extension.baseURI.resolve("_blank.html");
     }
 
     let system = Services.scriptSecurityManager.getSystemPrincipal();
 
-    let chromeShell = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor)
-                                  .getInterface(Ci.nsIDocShell);
-    chromeShell.createAboutBlankContentViewer(system);
+    // The windowless browser is a thin wrapper around a docShell that keeps
+    // its related resources alive. It implements nsIWebNavigation and
+    // forwards its methods to the underlying docShell, but cannot act as a
+    // docShell itself. Calling `getInterface(nsIDocShell)` gives us the
+    // underlying docShell, and `QueryInterface(nsIWebNavigation)` gives us
+    // access to the webNav methods that are already available on the
+    // windowless browser, but contrary to appearances, they are not the same
+    // object.
+    let chromeShell = windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor)
+                                       .getInterface(Ci.nsIDocShell)
+                                       .QueryInterface(Ci.nsIWebNavigation);
 
-    let chromeDoc = chromeWebNav.document;
-    const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-    let browser = chromeDoc.createElementNS(XUL_NS, "browser");
+    chromeShell.createAboutBlankContentViewer(system);
+    chromeShell.loadURI(XUL_URL, 0, null, null, null);
+
+
+    yield promiseObserved("chrome-document-global-created",
+                          win => win.document == chromeShell.document);
+
+    let chromeDoc = yield promiseDocumentLoaded(chromeShell.document);
+
+    let browser = chromeDoc.createElement("browser");
     browser.setAttribute("type", "content");
     browser.setAttribute("disableglobalhistory", "true");
     browser.setAttribute("webextension-view-type", "background");
-    chromeDoc.body.appendChild(browser);
+    browser.setAttribute("src", url);
+    chromeDoc.documentElement.appendChild(browser);
 
-    let frameLoader = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
-    let docShell = frameLoader.docShell;
 
-    let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
-    this.webNav = webNav;
+    yield new Promise(resolve => {
+      browser.addEventListener("load", function onLoad(event) {
+        if (event.target === browser.contentDocument) {
+          browser.removeEventListener("load", onLoad, true);
+          resolve();
+        }
+      }, true);
+    });
 
-    webNav.loadURI(url, 0, null, null, null);
+    this.webNav = browser.docShell.QueryInterface(Ci.nsIWebNavigation);
 
-    let window = webNav.document.defaultView;
+    let window = this.webNav.document.defaultView;
     this.contentWindow = window;
 
+
+    // Set the add-on's main debugger global, for use in the debugger
+    // console.
     if (this.extension.addonData.instanceID) {
       AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
                   .then(addon => addon.setDebugGlobal(window));
     }
 
-    // TODO: Right now we run onStartup after the background page
-    // finishes. See if this is what Chrome does.
     // TODO(robwu): This implementation of onStartup is wrong, see
-    // https://bugzilla.mozilla.org/show_bug.cgi?id=1247435#c1
-    let loadListener = event => {
-      if (event.target != window.document) {
-        return;
-      }
-      event.currentTarget.removeEventListener("load", loadListener, true);
-
-      if (this.extension.onStartup) {
-        this.extension.onStartup();
-      }
-    };
-    browser.addEventListener("load", loadListener, true);
-  },
+    // https://bugzil.la/1247435#c1
+    if (this.extension.onStartup) {
+      this.extension.onStartup();
+    }
+  }),
 
   shutdown() {
     // Navigate away from the background page to invalidate any
     // setTimeouts or other callbacks.
-    this.webNav.loadURI("about:blank", 0, null, null, null);
-    this.webNav = null;
+    if (this.webNav) {
+      this.webNav.loadURI("about:blank", 0, null, null, null);
+      this.webNav = null;
+    }
 
-    this.chromeWebNav.loadURI("about:blank", 0, null, null, null);
-    this.chromeWebNav.close();
-    this.chromeWebNav = null;
+    this.windowlessBrowser.loadURI("about:blank", 0, null, null, null);
+    this.windowlessBrowser.close();
+    this.windowlessBrowser = null;
 
     if (this.extension.addonData.instanceID) {
       AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
                   .then(addon => addon.setDebugGlobal(null));
     }
   },
 };
 
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("manifest_background", (type, directive, extension, manifest) => {
   let bgPage = new BackgroundPage(manifest.background, extension);
-  bgPage.build();
   backgroundPagesMap.set(extension, bgPage);
+  return bgPage.build();
 });
 
 extensions.on("shutdown", (type, extension) => {
   if (backgroundPagesMap.has(extension)) {
     backgroundPagesMap.get(extension).shutdown();
     backgroundPagesMap.delete(extension);
   }
 });
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -36,16 +36,17 @@ support-files =
   file_permission_xhr.html
   file_download.txt
 
 [test_ext_extension.html]
 [test_ext_inIncognitoContext_window.html]
 skip-if = os == 'android' # Android does not currently support windows.
 [test_ext_simple.html]
 [test_ext_geturl.html]
+[test_ext_background_canvas.html]
 [test_ext_content_security_policy.html]
 [test_ext_contentscript.html]
 skip-if = buildapp == 'b2g' # runat != document_idle is not supported.
 [test_ext_contentscript_api_injection.html]
 [test_ext_contentscript_create_iframe.html]
 [test_ext_contentscript_devtools_metadata.html]
 [test_ext_contentscript_css.html]
 [test_ext_downloads.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for background page canvas rendering</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_background_canvas() {
+  function background() {
+    try {
+      let canvas = document.createElement("canvas");
+
+      let context = canvas.getContext("2d");
+
+      // This ensures that we have a working PresShell, and can successfully
+      // calculate font metrics.
+      context.font = "8pt fixed";
+
+      browser.test.notifyPass("background-canvas");
+    } catch (e) {
+      browser.test.fail(`Error: ${e} :: ${e.stack}`);
+      browser.test.notifyFail("background-canvas");
+    }
+  }
+
+  let extensionData = {
+    background: `(${background})()`,
+  };
+
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+  yield extension.startup();
+  yield extension.awaitFinish("background-canvas");
+  yield extension.unload();
+});
+</script>
+
+</body>
+</html>
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html
@@ -88,17 +88,16 @@ add_task(function* test_contentscript() 
 
   let completePromise = new Promise(resolve => {
     extension.onMessage("script-run-complete", () => { completeCount++; resolve(); });
   });
 
   let chromeNamespacePromise = extension.awaitMessage("chrome-namespace-ok");
 
   yield extension.startup();
-  info("extension loaded");
 
   let win = window.open("file_sample.html");
 
   yield Promise.all([waitForLoad(win), completePromise, chromeNamespacePromise]);
   info("test page loaded");
 
   win.close();
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_geturl.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_geturl.html
@@ -50,17 +50,16 @@ let extensionData = {
   files: {
     "content_script.js": "(" + contentScript.toString() + ")()",
   },
 };
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
-  info("extension loaded");
 
   let win = window.open("file_sample.html");
 
   yield Promise.all([waitForLoad(win), extension.awaitFinish("geturl")]);
 
   win.close();
 
   yield extension.unload();
--- a/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
@@ -144,28 +144,25 @@ add_task(function* test_i18n() {
         if (msg == "assertEq") {
           browser.test.assertEq(...args);
         } else {
           browser.test.sendMessage(msg, ...args);
         }
       });
 
       runTests(browser.test.assertEq.bind(browser.test));
-
-      browser.test.notifyPass("l10n");
     } + `(${runTests})`,
   });
 
   yield extension.startup();
 
   let win = window.open("file_sample.html");
   yield extension.awaitMessage("content-script-finished");
   win.close();
 
-  yield extension.awaitFinish("l10n");
   yield extension.unload();
 });
 
 add_task(function* test_get_accept_languages() {
   function background() {
     function checkResults(source, results, expected) {
       browser.test.assertEq(
         expected.length,
--- a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html
@@ -61,17 +61,16 @@ let extensionData = {
   files: {
     "content_script.js": "(" + contentScript.toString() + ")()",
   },
 };
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
-  info("extension loaded");
 
   let win = window.open("file_sample.html");
 
   yield Promise.all([waitForLoad(win), extension.awaitFinish("runtime.connect")]);
 
   win.close();
 
   yield extension.unload();
--- a/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html
@@ -44,17 +44,16 @@ let extensionData = {
   files: {
     "content_script.js": "(" + contentScript.toString() + ")()",
   },
 };
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
-  info("extension loaded");
 
   let win = window.open("file_sample.html");
   yield Promise.all([waitForLoad(win), extension.awaitMessage("connected")]);
   win.close();
   yield extension.awaitMessage("disconnected");
 
   info("win.close() succeeded");
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html
@@ -39,17 +39,16 @@ let extensionData = {
   files: {
     "content_script.js": "(" + contentScript.toString() + ")()",
   },
 };
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
-  info("extension loaded");
 
   let win = window.open("file_sample.html");
 
   yield Promise.all([waitForLoad(win), extension.awaitFinish()]);
 
   win.close();
 
   yield extension.unload();
--- a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html
@@ -80,17 +80,16 @@ let extensionData = {
   files: {
     "content_script.js": "(" + contentScript.toString() + ")()",
   },
 };
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
-  info("extension loaded");
 
   let win = window.open("file_sample.html");
 
   yield Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_doublereply")]);
 
   win.close();
 
   yield extension.unload();
--- a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html
@@ -58,17 +58,16 @@ let extensionData = {
   files: {
     "content_script.js": "(" + contentScript.toString() + ")()",
   },
 };
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
-  info("extension loaded");
 
   let win = window.open("file_sample.html");
 
   yield Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_reply")]);
 
   win.close();
 
   yield extension.unload();
--- a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
@@ -18,36 +18,36 @@ function backgroundScript(token) {
     browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
 
     if (msg == "done") {
       browser.test.notifyPass("sendmessage_reply");
       return;
     }
 
     let tabId = sender.tab.id;
-    browser.tabs.sendMessage(tabId, token + "-tabMessage");
+    browser.tabs.sendMessage(tabId, `${token}-tabMessage`);
 
     browser.test.assertEq(msg, token, "token matches");
-    sendReply(token + "-done");
+    sendReply(`${token}-done`);
   });
 }
 
 function contentScript(token) {
   let gotTabMessage = false;
   let badTabMessage = false;
   browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
-    if (msg == token + "-tabMessage") {
+    if (msg == `${token}-tabMessage`) {
       gotTabMessage = true;
     } else {
       badTabMessage = true;
     }
   });
 
   browser.runtime.sendMessage(token, function(resp) {
-    if (resp != token + "-done" || !gotTabMessage || badTabMessage) {
+    if (resp != `${token}-done` || !gotTabMessage || badTabMessage) {
       return; // test failed
     }
     browser.runtime.sendMessage("done");
   });
 }
 
 function makeExtension() {
   let token = Math.random();
@@ -67,18 +67,18 @@ function makeExtension() {
     },
   };
   return extensionData;
 }
 
 add_task(function* test_contentscript() {
   let extension1 = ExtensionTestUtils.loadExtension(makeExtension());
   let extension2 = ExtensionTestUtils.loadExtension(makeExtension());
+
   yield Promise.all([extension1.startup(), extension2.startup()]);
-  info("extensions loaded");
 
   let win = window.open("file_sample.html");
 
   yield Promise.all([waitForLoad(win),
                      extension1.awaitFinish("sendmessage_reply"),
                      extension2.awaitFinish("sendmessage_reply")]);
 
   win.close();
--- a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
@@ -357,17 +357,18 @@ add_task(function* webnav_ordering() {
   extension.onMessage("received", ({url, event}) => {
     received.push({url, event});
 
     if (event == waitingEvent && url == waitingURL) {
       completedResolve();
     }
   });
 
-  yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+  yield extension.startup();
+  yield extension.awaitMessage("ready");
   info("webnavigation extension loaded");
 
   let win = window.open();
 
   yield loadAndWait(win, "onCompleted", URL, () => { win.location = URL; });
 
   function checkRequired(url) {
     for (let event of REQUIRED) {
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -21,16 +21,18 @@ Cu.import("resource://gre/modules/addons
 const CONSTANTS = {};
 Cu.import("resource://gre/modules/addons/AddonConstants.jsm", CONSTANTS);
 const SIGNING_REQUIRED = CONSTANTS.REQUIRE_SIGNING ?
                          true :
                          Services.prefs.getBoolPref("xpinstall.signatures.required");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Experiments",
   "resource:///modules/experiments/Experiments.jsm");
 
 const PREF_DISCOVERURL = "extensions.webservice.discoverURL";
 const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
 const PREF_XPI_ENABLED = "xpinstall.enabled";
 const PREF_MAXRESULTS = "extensions.getAddons.maxResults";
--- a/toolkit/mozapps/extensions/content/setting.xml
+++ b/toolkit/mozapps/extensions/content/setting.xml
@@ -10,23 +10,16 @@
 
 <!-- import-globals-from extensions.js -->
 
 <bindings xmlns="http://www.mozilla.org/xbl"
           xmlns:xbl="http://www.mozilla.org/xbl"
           xmlns:html="http://www.w3.org/1999/xhtml"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
-  <binding id="settings">
-    <content orient="vertical">
-      <xul:label class="settings-title" xbl:inherits="xbl:text=label" flex="1"/>
-      <children/>
-    </content>
-  </binding>
-
   <binding id="setting-base">
     <implementation>
       <constructor><![CDATA[
         this.preferenceChanged();
 
         this.addEventListener("keypress", function(event) {
           event.stopPropagation();
         }, false);
@@ -294,29 +287,25 @@
                      xbl:inherits="disabled,emptytext,type=inputtype,min,max,increment,hidespinbuttons,decimalplaces,wraparound"/>
       </xul:hbox>
     </content>
 
     <implementation>
       <method name="valueFromPreference">
         <body>
         <![CDATA[
-          const nsISupportsString = Components.interfaces.nsISupportsString;
-          this.value = Services.prefs.getComplexValue(this.pref, nsISupportsString).data;
+          this.value = Preferences.get(this.pref, "");
          ]]>
         </body>
       </method>
 
       <method name="valueToPreference">
         <body>
         <![CDATA[
-          const nsISupportsString = Components.interfaces.nsISupportsString;
-          let iss = Components.classes["@mozilla.org/supports-string;1"].createInstance(nsISupportsString);
-          iss.data = this.value;
-          Services.prefs.setComplexValue(this.pref, nsISupportsString, iss);
+          Preferences.set(this.pref, this.value);
         ]]>
         </body>
       </method>
     </implementation>
   </binding>
 
   <binding id="setting-color" extends="chrome://mozapps/content/extensions/setting.xml#setting-base">
     <content>
@@ -392,25 +381,25 @@
           }
         ]]>
         </body>
       </method>
 
       <method name="valueFromPreference">
         <body>
         <![CDATA[
-          this.value = Services.prefs.getCharPref(this.pref);
+          this.value = Preferences.get(this.pref, "");
         ]]>
         </body>
       </method>
 
       <method name="valueToPreference">
         <body>
         <![CDATA[
-          Services.prefs.setCharPref(this.pref, this.value);
+          Preferences.set(this.pref, this.value);
         ]]>
         </body>
       </method>
 
       <field name="_value"></field>
 
       <property name="value">
         <getter>
@@ -455,30 +444,17 @@
       <![CDATA[
         this.control.addEventListener("command", this.inputChanged.bind(this), false);
       ]]>
       </constructor>
 
       <method name="valueFromPreference">
         <body>
         <![CDATA[
-          let val;
-          switch (Services.prefs.getPrefType(this.pref)) {
-            case Ci.nsIPrefBranch.PREF_STRING:
-              val = Services.prefs.getCharPref(this.pref);
-              break;
-            case Ci.nsIPrefBranch.PREF_INT:
-              val = Services.prefs.getIntPref(this.pref);
-              break;
-            case Ci.nsIPrefBranch.PREF_BOOL:
-              val = Services.prefs.getBoolPref(this.pref).toString();
-              break;
-            default:
-              return;
-          }
+          let val = Preferences.get(this.pref, "").toString();
 
           if ("itemCount" in this.control) {
             for (let i = 0; i < this.control.itemCount; i++) {
               if (this.control.getItemAtIndex(i).value == val) {
                 this.control.selectedIndex = i;
                 break;
               }
             }
@@ -489,22 +465,22 @@
         </body>
       </method>
 
       <method name="valueToPreference">
         <body>
         <![CDATA[
           // We might not have a pref already set, so we guess the type from the value attribute
           let val = this.control.selectedItem.value;
-          if (val == "true" || val == "false")
-            Services.prefs.setBoolPref(this.pref, val == "true");
-          else if (/^-?\d+$/.test(val))
-            Services.prefs.setIntPref(this.pref, val);
-          else
-            Services.prefs.setCharPref(this.pref, val);
+          if (val == "true" || val == "false") {
+            val = val == "true";
+          } else if (/^-?\d+$/.test(val)) {
+            val = parseInt(val, 10);
+          }
+          Preferences.set(this.pref, val);
         ]]>
         </body>
       </method>
 
       <field name="control">this.getElementsByTagName(this.getAttribute("type") == "radio" ? "radiogroup" : "menulist")[0];</field>
     </implementation>
   </binding>
 </bindings>
--- a/toolkit/mozapps/extensions/test/browser/browser_inlinesettings.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_inlinesettings.js
@@ -1,13 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // Tests various aspects of the details view
+Components.utils.import("resource://gre/modules/Preferences.jsm");
 
 var gManagerWindow;
 var gCategoryUtilities;
 var gProvider;
 
 const SETTINGS_ROWS = 9;
 
 var MockFilePicker = SpecialPowers.MockFilePicker;
@@ -244,20 +245,21 @@ add_test(function() {
     Services.prefs.setCharPref("extensions.inlinesettings1.string", "foo");
     input = gManagerWindow.document.getAnonymousElementByAttribute(settings[3], "anonid", "input");
     is(input.value, "foo", "Text box should have initial value");
     input.select();
     EventUtils.synthesizeKey("b", {}, gManagerWindow);
     EventUtils.synthesizeKey("a", {}, gManagerWindow);
     EventUtils.synthesizeKey("r", {}, gManagerWindow);
     is(input.value, "bar", "Text box should have updated value");
+    input.value += "\u03DE"; // Cheat to add this non-ASCII character without typing it.
     EventUtils.synthesizeKey("/", {}, gManagerWindow);
-    is(input.value, "bar/", "Text box should have updated value");
+    is(input.value, "bar\u03DE/", "Text box should have updated value");
     is(gManagerWindow.document.getBindingParent(gManagerWindow.document.activeElement), input, "Search box should not have focus");
-    is(Services.prefs.getCharPref("extensions.inlinesettings1.string"), "bar/", "String pref should have been updated");
+    is(Preferences.get("extensions.inlinesettings1.string", "wrong"), "bar\u03DE/", "String pref should have been updated");
 
     ok(!settings[4].hasAttribute("first-row"), "Not the first row");
     input = settings[4].firstElementChild;
     is(input.value, "1", "Menulist should have initial value");
     input.focus();
     EventUtils.synthesizeKey("b", {}, gManagerWindow);
     is(input.value, "2", "Menulist should have updated value");
     is(gManagerWindow._testValue, "2", "Menulist oncommand handler should've updated the test value");
@@ -277,56 +279,57 @@ add_test(function() {
 
     try {
       ok(!settings[6].hasAttribute("first-row"), "Not the first row");
       var button = gManagerWindow.document.getAnonymousElementByAttribute(settings[6], "anonid", "button");
       input = gManagerWindow.document.getAnonymousElementByAttribute(settings[6], "anonid", "input");
       is(input.value, "", "Label value should be empty");
       is(input.tooltipText, "", "Label tooltip should be empty");
 
-      var profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+      var testFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+      testFile.append("\u2622");
       var curProcD = Services.dirsvc.get("CurProcD", Ci.nsIFile);
 
-      MockFilePicker.returnFiles = [profD];
+      MockFilePicker.returnFiles = [testFile];
       MockFilePicker.returnValue = Ci.nsIFilePicker.returnOK;
       EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
       is(MockFilePicker.mode, Ci.nsIFilePicker.modeOpen, "File picker mode should be open file");
-      is(input.value, profD.path, "Label value should match file chosen");
-      is(input.tooltipText, profD.path, "Label tooltip should match file chosen");
-      is(Services.prefs.getCharPref("extensions.inlinesettings1.file"), profD.path, "File pref should match file chosen");
+      is(input.value, testFile.path, "Label value should match file chosen");
+      is(input.tooltipText, testFile.path, "Label tooltip should match file chosen");
+      is(Preferences.get("extensions.inlinesettings1.file", "wrong"), testFile.path, "File pref should match file chosen");
 
       MockFilePicker.returnFiles = [curProcD];
       MockFilePicker.returnValue = Ci.nsIFilePicker.returnCancel;
       EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
       is(MockFilePicker.mode, Ci.nsIFilePicker.modeOpen, "File picker mode should be open file");
-      is(input.value, profD.path, "Label value should not have changed");
-      is(input.tooltipText, profD.path, "Label tooltip should not have changed");
-      is(Services.prefs.getCharPref("extensions.inlinesettings1.file"), profD.path, "File pref should not have changed");
+      is(input.value, testFile.path, "Label value should not have changed");
+      is(input.tooltipText, testFile.path, "Label tooltip should not have changed");
+      is(Preferences.get("extensions.inlinesettings1.file", "wrong"), testFile.path, "File pref should not have changed");
 
       ok(!settings[7].hasAttribute("first-row"), "Not the first row");
       button = gManagerWindow.document.getAnonymousElementByAttribute(settings[7], "anonid", "button");
       input = gManagerWindow.document.getAnonymousElementByAttribute(settings[7], "anonid", "input");
       is(input.value, "", "Label value should be empty");
       is(input.tooltipText, "", "Label tooltip should be empty");
 
-      MockFilePicker.returnFiles = [profD];
+      MockFilePicker.returnFiles = [testFile];
       MockFilePicker.returnValue = Ci.nsIFilePicker.returnOK;
       EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
       is(MockFilePicker.mode, Ci.nsIFilePicker.modeGetFolder, "File picker mode should be directory");
-      is(input.value, profD.path, "Label value should match file chosen");
-      is(input.tooltipText, profD.path, "Label tooltip should match file chosen");
-      is(Services.prefs.getCharPref("extensions.inlinesettings1.directory"), profD.path, "Directory pref should match file chosen");
+      is(input.value, testFile.path, "Label value should match file chosen");
+      is(input.tooltipText, testFile.path, "Label tooltip should match file chosen");
+      is(Preferences.get("extensions.inlinesettings1.directory", "wrong"), testFile.path, "Directory pref should match file chosen");
 
       MockFilePicker.returnFiles = [curProcD];
       MockFilePicker.returnValue = Ci.nsIFilePicker.returnCancel;
       EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
       is(MockFilePicker.mode, Ci.nsIFilePicker.modeGetFolder, "File picker mode should be directory");
-      is(input.value, profD.path, "Label value should not have changed");
-      is(input.tooltipText, profD.path, "Label tooltip should not have changed");
-      is(Services.prefs.getCharPref("extensions.inlinesettings1.directory"), profD.path, "Directory pref should not have changed");
+      is(input.value, testFile.path, "Label value should not have changed");
+      is(input.tooltipText, testFile.path, "Label tooltip should not have changed");
+      is(Preferences.get("extensions.inlinesettings1.directory", "wrong"), testFile.path, "Directory pref should not have changed");
 
       var unsizedInput = gManagerWindow.document.getAnonymousElementByAttribute(settings[2], "anonid", "input");
       var sizedInput = gManagerWindow.document.getAnonymousElementByAttribute(settings[8], "anonid", "input");
       is(unsizedInput.clientWidth > sizedInput.clientWidth, true, "Input with size attribute should be smaller than input without");
     } finally {
       button = gManagerWindow.document.getElementById("detail-prefs-btn");
       is_element_hidden(button, "Preferences button should not be visible");
 
@@ -375,19 +378,19 @@ add_test(function() {
 
     ok(!settings[2].hasAttribute("first-row"), "Not the first row");
     Services.prefs.setCharPref("extensions.inlinesettings3.radioString", "juliet");
     radios = settings[2].getElementsByTagName("radio");
     isnot(radios[0].selected, true, "Correct radio button should be selected");
     is(radios[1].selected, true, "Correct radio button should be selected");
     isnot(radios[2].selected, true, "Correct radio button should be selected");
     EventUtils.synthesizeMouseAtCenter(radios[0], { clickCount: 1 }, gManagerWindow);
-    is(Services.prefs.getCharPref("extensions.inlinesettings3.radioString"), "india", "Radio pref should have been updated");
+    is(Preferences.get("extensions.inlinesettings3.radioString", "wrong"), "india", "Radio pref should have been updated");
     EventUtils.synthesizeMouseAtCenter(radios[2], { clickCount: 1 }, gManagerWindow);
-    is(Services.prefs.getCharPref("extensions.inlinesettings3.radioString"), "kilo", "Radio pref should have been updated");
+    is(Preferences.get("extensions.inlinesettings3.radioString", "wrong"), "kilo \u338F", "Radio pref should have been updated");
 
     ok(!settings[3].hasAttribute("first-row"), "Not the first row");
     Services.prefs.setIntPref("extensions.inlinesettings3.menulist", 8);
     var input = settings[3].firstElementChild;
     is(input.value, "8", "Menulist should have initial value");
     input.focus();
     EventUtils.synthesizeKey("n", {}, gManagerWindow);
     is(input.value, "9", "Menulist should have updated value");
@@ -583,17 +586,17 @@ add_test(function() {
   observer.checkHidden("inlinesettings2@tests.mozilla.org");
 
   // Ensure these prefs are set. They should be set above, but somebody might
   // change the tests above.
   var profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
   Services.prefs.setBoolPref("extensions.inlinesettings1.bool", false);
   Services.prefs.setIntPref("extensions.inlinesettings1.boolint", 1);
   Services.prefs.setIntPref("extensions.inlinesettings1.integer", 12);
-  Services.prefs.setCharPref("extensions.inlinesettings1.string", "bar/");
+  Preferences.set("extensions.inlinesettings1.string", "bar\u03DE/");
   Services.prefs.setCharPref("extensions.inlinesettings1.color", "#FF9900");
   Services.prefs.setCharPref("extensions.inlinesettings1.file", profD.path);
   Services.prefs.setCharPref("extensions.inlinesettings1.directory", profD.path);
 
   var addon = get_addon_element(gManagerWindow, "inlinesettings1@tests.mozilla.org");
   addon.parentNode.ensureElementIsVisible(addon);
 
   var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
@@ -610,17 +613,17 @@ add_test(function() {
 
     input = gManagerWindow.document.getAnonymousElementByAttribute(settings[1], "anonid", "input");
     is(input.checked, true, "Checkbox should have initial value");
 
     input = gManagerWindow.document.getAnonymousElementByAttribute(settings[2], "anonid", "input");
     is(input.value, "12", "Number box should have initial value");
 
     input = gManagerWindow.document.getAnonymousElementByAttribute(settings[3], "anonid", "input");
-    is(input.value, "bar/", "Text box should have initial value");
+    is(input.value, "bar\u03DE/", "Text box should have initial value");
 
     input = gManagerWindow.document.getAnonymousElementByAttribute(settings[5], "anonid", "input");
     is(input.color, "#FF9900", "Color picker should have initial value");
 
     input = gManagerWindow.document.getAnonymousElementByAttribute(settings[6], "anonid", "input");
     is(input.value, profD.path, "Label should have initial value");
     is(input.tooltipText, profD.path, "Label tooltip should have initial value");
 
@@ -635,17 +638,17 @@ add_test(function() {
 // Tests bindings with existing prefs.
 add_test(function() {
   observer.checkHidden("inlinesettings1@tests.mozilla.org");
 
   // Ensure these prefs are set. They should be set above, but somebody might
   // change the tests above.
   Services.prefs.setBoolPref("extensions.inlinesettings3.radioBool", false);
   Services.prefs.setIntPref("extensions.inlinesettings3.radioInt", 6);
-  Services.prefs.setCharPref("extensions.inlinesettings3.radioString", "kilo");
+  Preferences.set("extensions.inlinesettings3.radioString", "kilo \u338F");
   Services.prefs.setIntPref("extensions.inlinesettings3.menulist", 9);
 
   var addon = get_addon_element(gManagerWindow, "inlinesettings3@tests.mozilla.org");
   addon.parentNode.ensureElementIsVisible(addon);
 
   var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
   EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
 
--- a/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_info.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_info.js
@@ -1,13 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // Tests various aspects of the details view
+Components.utils.import("resource://gre/modules/Preferences.jsm");
 
 var gManagerWindow;
 var gCategoryUtilities;
 var gProvider;
 
 const SETTINGS_ROWS = 8;
 
 var MockFilePicker = SpecialPowers.MockFilePicker;
@@ -371,17 +372,17 @@ add_test(function() {
     Services.prefs.setCharPref("extensions.inlinesettings3.radioString", "juliet");
     radios = settings[2].getElementsByTagName("radio");
     isnot(radios[0].selected, true, "Correct radio button should be selected");
     is(radios[1].selected, true, "Correct radio button should be selected");
     isnot(radios[2].selected, true, "Correct radio button should be selected");
     EventUtils.synthesizeMouseAtCenter(radios[0], { clickCount: 1 }, gManagerWindow);
     is(Services.prefs.getCharPref("extensions.inlinesettings3.radioString"), "india", "Radio pref should have been updated");
     EventUtils.synthesizeMouseAtCenter(radios[2], { clickCount: 1 }, gManagerWindow);
-    is(Services.prefs.getCharPref("extensions.inlinesettings3.radioString"), "kilo", "Radio pref should have been updated");
+    is(Preferences.get("extensions.inlinesettings3.radioString", "wrong"), "kilo \u338F", "Radio pref should have been updated");
 
     ok(!settings[3].hasAttribute("first-row"), "Not the first row");
     Services.prefs.setIntPref("extensions.inlinesettings3.menulist", 8);
     var input = settings[3].firstElementChild;
     is(input.value, "8", "Menulist should have initial value");
     input.focus();
     EventUtils.synthesizeKey("n", {}, gManagerWindow);
     is(input.value, "9", "Menulist should have updated value");
--- a/toolkit/mozapps/extensions/test/browser/more_options.xul
+++ b/toolkit/mozapps/extensions/test/browser/more_options.xul
@@ -12,17 +12,17 @@
       <radio label="Golf" value="5" />
       <radio label="Hotel" value="6" />
     </radiogroup>
   </setting>
   <setting pref="extensions.inlinesettings3.radioString" type="radio" title="Radio">
     <radiogroup>
       <radio label="India" value="india" />
       <radio label="Juliet" value="juliet" />
-      <radio label="Kilo" value="kilo" />
+      <radio label="Kilo &#x338F;" value="kilo &#x338F;" />
     </radiogroup>
   </setting>
   <setting pref="extensions.inlinesettings3.menulist" type="menulist" title="Menulist">
     <menulist sizetopopup="always">
       <menupopup>
         <menuitem label="Lima" value="7" />
         <menuitem label="Mike" value="8" />
         <menuitem label="November" value="9" />