merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 20 Jul 2016 11:17:45 +0200
changeset 389926 a6ca570852571ae98ef10902af8bc9a27543383c
parent 389908 3383b0da1a14340ec6096aca542eb73b0f7341d5 (current diff)
parent 389925 c0f1a96173fcbebe880279435cf62a428965bac0 (diff)
child 389959 e904e18d7dfcd8097f92d44104ca1462fc5d1335
push id23566
push usertkuo@mozilla.com
push dateWed, 20 Jul 2016 10:57:43 +0000
reviewersmerge
milestone50.0a1
merge fx-team to mozilla-central a=merge
browser/themes/shared/permissions.svg
browser/themes/shared/plugins/notification-pluginAlert.png
browser/themes/shared/plugins/notification-pluginAlert@2x.png
browser/themes/shared/plugins/notification-pluginBlocked.png
browser/themes/shared/plugins/notification-pluginBlocked@2x.png
browser/themes/shared/plugins/notification-pluginNormal.png
browser/themes/shared/plugins/notification-pluginNormal@2x.png
devtools/client/inspector/layout/test/browser_layout_update-after-navigation.js
toolkit/components/extensions/ExtensionManagement.jsm
toolkit/components/telemetry/Histograms.json
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -3780,21 +3780,16 @@
              * are hidden). This checks to make sure all conditions are
              * satisfied, and then records the tab switch as finished.
              */
             maybeFinishTabSwitch: function () {
               if (this.switchInProgress && this.requestedTab &&
                   this.getTabState(this.requestedTab) == this.STATE_LOADED) {
                 // After this point the tab has switched from the content thread's point of view.
                 // The changes will be visible after the next refresh driver tick + composite.
-                let event = new CustomEvent("TabSwitched", {
-                  bubbles: true,
-                  cancelable: true
-                });
-                this.tabbrowser.dispatchEvent(event);
                 let time = TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
                 if (time != -1) {
                   TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
                   this.log("DEBUG: tab switch time = " + time);
                   this.addMarker("AsyncTabSwitch:Finish");
                 }
                 this.switchInProgress = false;
               }
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -499,54 +499,51 @@ const CustomizableWidgets = [
       });
       return item;
     },
   }, {
     id: "privatebrowsing-button",
     shortcutId: "key_privatebrowsing",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(e) {
-      let win = e.target && e.target.ownerGlobal;
-      if (win && typeof win.OpenBrowserWindow == "function") {
+      let win = e.target.ownerGlobal;
+      if (typeof win.OpenBrowserWindow == "function") {
         win.OpenBrowserWindow({private: true});
       }
     }
   }, {
     id: "save-page-button",
     shortcutId: "key_savePage",
     tooltiptext: "save-page-button.tooltiptext3",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
-      let win = aEvent.target &&
-                aEvent.target.ownerGlobal;
-      if (win && typeof win.saveBrowser == "function") {
+      let win = aEvent.target.ownerGlobal;
+      if (typeof win.saveBrowser == "function") {
         win.saveBrowser(win.gBrowser.selectedBrowser);
       }
     }
   }, {
     id: "find-button",
     shortcutId: "key_find",
     tooltiptext: "find-button.tooltiptext3",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
-      let win = aEvent.target &&
-                aEvent.target.ownerGlobal;
-      if (win && win.gFindBar) {
+      let win = aEvent.target.ownerGlobal;
+      if (win.gFindBar) {
         win.gFindBar.onFindCommand();
       }
     }
   }, {
     id: "open-file-button",
     shortcutId: "openFileKb",
     tooltiptext: "open-file-button.tooltiptext3",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
-      let win = aEvent.target
-                && aEvent.target.ownerGlobal;
-      if (win && typeof win.BrowserOpenFileWindow == "function") {
+      let win = aEvent.target.ownerGlobal;
+      if (typeof win.BrowserOpenFileWindow == "function") {
         win.BrowserOpenFileWindow();
       }
     }
   }, {
     id: "sidebar-button",
     type: "view",
     viewId: "PanelUI-sidebar",
     tooltiptext: "sidebar-button.tooltiptext2",
@@ -612,19 +609,18 @@ const CustomizableWidgets = [
       return node;
     }
   }, {
     id: "add-ons-button",
     shortcutId: "key_openAddons",
     tooltiptext: "add-ons-button.tooltiptext3",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
-      let win = aEvent.target &&
-                aEvent.target.ownerGlobal;
-      if (win && typeof win.BrowserOpenAddonsMgr == "function") {
+      let win = aEvent.target.ownerGlobal;
+      if (typeof win.BrowserOpenAddonsMgr == "function") {
         win.BrowserOpenAddonsMgr();
       }
     }
   }, {
     id: "zoom-controls",
     type: "custom",
     tooltiptext: "zoom-controls.tooltiptext2",
     defaultArea: CustomizableUI.AREA_PANEL,
@@ -1149,19 +1145,18 @@ if (Services.prefs.getBoolPref("privacy.
     }
   });
 }
 
 let preferencesButton = {
   id: "preferences-button",
   defaultArea: CustomizableUI.AREA_PANEL,
   onCommand: function(aEvent) {
-    let win = aEvent.target &&
-              aEvent.target.ownerGlobal;
-    if (win && typeof win.openPreferences == "function") {
+    let win = aEvent.target.ownerGlobal;
+    if (typeof win.openPreferences == "function") {
       win.openPreferences();
     }
   }
 };
 if (AppConstants.platform == "win") {
   preferencesButton.label = "preferences-button.labelWin";
   preferencesButton.tooltiptext = "preferences-button.tooltipWin2";
 } else if (AppConstants.platform == "macosx") {
--- a/browser/components/extensions/ext-commands.js
+++ b/browser/components/extensions/ext-commands.js
@@ -72,20 +72,22 @@ CommandList.prototype = {
   loadCommandsFromManifest(manifest) {
     let commands = new Map();
     // For Windows, chrome.runtime expects 'win' while chrome.commands
     // expects 'windows'.  We can special case this for now.
     let os = PlatformInfo.os == "win" ? "windows" : PlatformInfo.os;
     for (let name of Object.keys(manifest.commands)) {
       let command = manifest.commands[name];
       let shortcut = command.suggested_key[os] || command.suggested_key.default;
-      commands.set(name, {
-        description: command.description,
-        shortcut: shortcut.replace(/\s+/g, ""),
-      });
+      if (shortcut) {
+        commands.set(name, {
+          description: command.description,
+          shortcut: shortcut.replace(/\s+/g, ""),
+        });
+      }
     }
     return commands;
   },
 
   /**
    * Registers the commands to a document.
    * @param {ChromeWindow} window The XUL window to insert the Keyset.
    */
@@ -119,17 +121,17 @@ CommandList.prototype = {
     // and it is currently ignored when set to the empty string.
     keyElement.setAttribute("oncommand", "//");
 
     /* eslint-disable mozilla/balanced-listeners */
     // We remove all references to the key elements when the extension is shutdown,
     // therefore the listeners for these elements will be garbage collected.
     keyElement.addEventListener("command", (event) => {
       if (name == "_execute_page_action") {
-        let win = event.target.ownerGlobal;
+        let win = event.target.ownerDocument.defaultView;
         pageActionFor(this.extension).triggerAction(win);
       } else {
         this.emit("command", name);
       }
     });
     /* eslint-enable mozilla/balanced-listeners */
 
     return keyElement;
--- a/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js
+++ b/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js
@@ -1,98 +1,254 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
 add_task(function* test_user_defined_commands() {
+  const testCommands = [
+    // Ctrl Shortcuts
+    {
+      name: "toggle-ctrl-a",
+      shortcut: "Ctrl+A",
+      key: "A",
+      modifiers: {
+        accelKey: true,
+      },
+    },
+    {
+      name: "toggle-ctrl-up",
+      shortcut: "Ctrl+Up",
+      key: "VK_UP",
+      modifiers: {
+        accelKey: true,
+      },
+    },
+    // Alt Shortcuts
+    {
+      name: "toggle-alt-1",
+      shortcut: "Alt+1",
+      key: "1",
+      modifiers: {
+        altKey: true,
+      },
+    },
+    {
+      name: "toggle-alt-a",
+      shortcut: "Alt+A",
+      key: "A",
+      modifiers: {
+        altKey: true,
+      },
+    },
+    {
+      name: "toggle-alt-down",
+      shortcut: "Alt+Down",
+      key: "VK_DOWN",
+      modifiers: {
+        altKey: true,
+      },
+    },
+    // Mac Shortcuts
+    {
+      name: "toggle-command-shift-page-up",
+      shortcutMac: "Command+Shift+PageUp",
+      key: "VK_PAGE_UP",
+      modifiers: {
+        accelKey: true,
+        shiftKey: true,
+      },
+    },
+    {
+      name: "toggle-mac-control-b",
+      shortcut: "Ctrl+B",
+      shortcutMac: "MacCtrl+B",
+      key: "B",
+      modifiers: {
+        ctrlKey: true,
+      },
+    },
+    // Ctrl+Shift Shortcuts
+    {
+      name: "toggle-ctrl-shift-1",
+      shortcut: "Ctrl+Shift+1",
+      key: "1",
+      modifiers: {
+        accelKey: true,
+        shiftKey: true,
+      },
+    },
+    {
+      name: "toggle-ctrl-shift-i",
+      shortcut: "Ctrl+Shift+I",
+      key: "I",
+      modifiers: {
+        accelKey: true,
+        shiftKey: true,
+      },
+    },
+    {
+      name: "toggle-ctrl-shift-left",
+      shortcut: "Ctrl+Shift+Left",
+      key: "VK_LEFT",
+      modifiers: {
+        accelKey: true,
+        shiftKey: true,
+      },
+    },
+    // Alt+Shift Shortcuts
+    {
+      name: "toggle-alt-shift-1",
+      shortcut: "Alt+Shift+1",
+      key: "1",
+      modifiers: {
+        altKey: true,
+        shiftKey: true,
+      },
+    },
+    {
+      name: "toggle-alt-shift-a",
+      shortcut: "Alt+Shift+A",
+      key: "A",
+      modifiers: {
+        altKey: true,
+        shiftKey: true,
+      },
+    },
+    {
+      name: "toggle-alt-shift-right",
+      shortcut: "Alt+Shift+Right",
+      key: "VK_RIGHT",
+      modifiers: {
+        altKey: true,
+        shiftKey: true,
+      },
+    },
+    // Misc Shortcuts
+    {
+      name: "unrecognized-property-name",
+      shortcut: "Alt+Shift+3",
+      key: "3",
+      modifiers: {
+        altKey: true,
+        shiftKey: true,
+      },
+      unrecognized_property: "with-a-random-value",
+    },
+    {
+      name: "spaces-in-shortcut-name",
+      shortcut: "  Alt + Shift + 2  ",
+      key: "2",
+      modifiers: {
+        altKey: true,
+        shiftKey: true,
+      },
+    },
+  ];
+
   // Create a window before the extension is loaded.
   let win1 = yield BrowserTestUtils.openNewBrowserWindow();
   yield BrowserTestUtils.loadURI(win1.gBrowser.selectedBrowser, "about:robots");
   yield BrowserTestUtils.browserLoaded(win1.gBrowser.selectedBrowser);
 
+  let commands = {};
+  let isMac = AppConstants.platform == "macosx";
+  let totalMacOnlyCommands = 0;
+
+  for (let testCommand of testCommands) {
+    let command = {
+      suggested_key: {},
+    };
+
+    if (testCommand.shortcut) {
+      command.suggested_key.default = testCommand.shortcut;
+    }
+
+    if (testCommand.shortcutMac) {
+      command.suggested_key.mac = testCommand.shortcutMac;
+    }
+
+    if (testCommand.shortcutMac && !testCommand.shortcut) {
+      totalMacOnlyCommands++;
+    }
+
+    if (testCommand.unrecognized_property) {
+      command.unrecognized_property = testCommand.unrecognized_property;
+    }
+
+    commands[testCommand.name] = command;
+  }
+
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
-      "commands": {
-        "toggle-feature-using-alt-shift-3": {
-          "suggested_key": {
-            "default": "Alt+Shift+3",
-          },
-        },
-        "toggle-feature-using-alt-shift-comma": {
-          "suggested_key": {
-            "default": "Alt+Shift+Comma",
-          },
-          "unrecognized_property": "with-a-random-value",
-        },
-        "toggle-feature-with-whitespace-in-suggested-key": {
-          "suggested_key": {
-            "default": "  Alt + Shift + 2  ",
-          },
-        },
-      },
+      "commands": commands,
     },
 
     background: function() {
       browser.commands.onCommand.addListener(commandName => {
         browser.test.sendMessage("oncommand", commandName);
       });
       browser.test.sendMessage("ready");
     },
   });
 
-
   SimpleTest.waitForExplicitFinish();
   let waitForConsole = new Promise(resolve => {
     SimpleTest.monitorConsole(resolve, [{
       message: /Reading manifest: Error processing commands.*.unrecognized_property: An unexpected property was found/,
     }]);
   });
 
   yield extension.startup();
   yield extension.awaitMessage("ready");
 
+  function* runTest() {
+    for (let testCommand of testCommands) {
+      if (testCommand.shortcutMac && !isMac) {
+        continue;
+      }
+      EventUtils.synthesizeKey(testCommand.key, testCommand.modifiers);
+      let message = yield extension.awaitMessage("oncommand");
+      is(message, testCommand.name, "Expected onCommand listener to fire with the correct command name");
+    }
+  }
+
   // Create another window after the extension is loaded.
   let win2 = yield BrowserTestUtils.openNewBrowserWindow();
   yield BrowserTestUtils.loadURI(win2.gBrowser.selectedBrowser, "about:config");
   yield BrowserTestUtils.browserLoaded(win2.gBrowser.selectedBrowser);
 
+  let totalTestCommands = Object.keys(testCommands).length;
+  let expectedCommandsRegistered = isMac ? totalTestCommands : totalTestCommands - totalMacOnlyCommands;
+
   // Confirm the keysets have been added to both windows.
   let keysetID = `ext-keyset-id-${makeWidgetId(extension.id)}`;
   let keyset = win1.document.getElementById(keysetID);
   ok(keyset != null, "Expected keyset to exist");
-  is(keyset.childNodes.length, 3, "Expected keyset to have 3 children");
+  is(keyset.childNodes.length, expectedCommandsRegistered, "Expected keyset to have the correct number of children");
 
   keyset = win2.document.getElementById(keysetID);
   ok(keyset != null, "Expected keyset to exist");
-  is(keyset.childNodes.length, 3, "Expected keyset to have 3 children");
+  is(keyset.childNodes.length, expectedCommandsRegistered, "Expected keyset to have the correct number of children");
 
   // Confirm that the commands are registered to both windows.
   yield focusWindow(win1);
-  EventUtils.synthesizeKey("3", {altKey: true, shiftKey: true});
-  let message = yield extension.awaitMessage("oncommand");
-  is(message, "toggle-feature-using-alt-shift-3", "Expected onCommand listener to fire with correct message");
+  yield runTest();
 
   yield focusWindow(win2);
-  EventUtils.synthesizeKey("VK_COMMA", {altKey: true, shiftKey: true});
-  message = yield extension.awaitMessage("oncommand");
-  is(message, "toggle-feature-using-alt-shift-comma", "Expected onCommand listener to fire with correct message");
-
-  EventUtils.synthesizeKey("2", {altKey: true, shiftKey: true});
-  message = yield extension.awaitMessage("oncommand");
-  is(message, "toggle-feature-with-whitespace-in-suggested-key", "Expected onCommand listener to fire with correct message");
+  yield runTest();
 
   yield extension.unload();
 
   // Confirm that the keysets have been removed from both windows after the extension is unloaded.
   keyset = win1.document.getElementById(keysetID);
   is(keyset, null, "Expected keyset to be removed from the window");
 
   keyset = win2.document.getElementById(keysetID);
   is(keyset, null, "Expected keyset to be removed from the window");
 
   yield BrowserTestUtils.closeWindow(win1);
   yield BrowserTestUtils.closeWindow(win2);
 
   SimpleTest.endMonitorConsole();
   yield waitForConsole;
 });
-
-
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -120,24 +120,23 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
                                   "resource://gre/modules/SimpleServiceDiscovery.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
                                   "resource:///modules/ContentSearch.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "TabCrashHandler",
                                   "resource:///modules/ContentCrashHandlers.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+                                  "resource://gre/modules/PluralForm.jsm");
 if (AppConstants.MOZ_CRASHREPORTER) {
   XPCOMUtils.defineLazyModuleGetter(this, "PluginCrashReporter",
                                     "resource:///modules/ContentCrashHandlers.jsm");
   XPCOMUtils.defineLazyModuleGetter(this, "CrashSubmit",
                                     "resource://gre/modules/CrashSubmit.jsm");
-  XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
-                                    "resource://gre/modules/PluralForm.jsm");
-
 }
 
 XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
   return Services.strings.createBundle('chrome://branding/locale/brand.properties');
 });
 
 XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
   return Services.strings.createBundle('chrome://browser/locale/browser.properties');
@@ -2477,18 +2476,22 @@ BrowserGlue.prototype = {
       if (URIs.length == 1) {
         title = bundle.GetStringFromName("tabArrivingNotification.title");
         const pageTitle = URIs[0].title || firstTab.linkedBrowser.contentTitle
                           || URIs[0].uri;
         body = bundle.formatStringFromName("tabArrivingNotification.body", [pageTitle, deviceName], 2);
       } else {
         title = bundle.GetStringFromName("tabsArrivingNotification.title");
         const tabArrivingBody = URIs.every(URI => URI.clientId == URIs[0].clientId) ?
-                                "tabsArrivingNotification.body" : "tabsArrivingNotificationMultiple.body";
-        body = bundle.formatStringFromName(tabArrivingBody, [URIs.length, deviceName], 2);
+                                "unnamedTabsArrivingNotification.body" :
+                                "unnamedTabsArrivingNotificationMultiple.body";
+        body = bundle.GetStringFromName(tabArrivingBody);
+        body = PluralForm.get(URIs.length, body);
+        body = body.replace("#1", URIs.length);
+        body = body.replace("#2", deviceName);
       }
 
       const clickCallback = (subject, topic, data) => {
         if (topic == "alertclickcallback") {
           win.gBrowser.selectedTab = firstTab;
         }
       }
       AlertsService.showAlertNotification(null, title, body, true, null, clickCallback);
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -365,21 +365,21 @@ var SessionHistoryListener = {
  */
 var ScrollPositionListener = {
   init: function () {
     addEventListener("scroll", this);
     gFrameTree.addObserver(this);
   },
 
   handleEvent: function (event) {
-    let frame = event.target && event.target.defaultView;
+    let frame = event.target.defaultView;
 
     // Don't collect scroll data for frames created at or after the load event
     // as SessionStore can't restore scroll data for those.
-    if (frame && gFrameTree.contains(frame)) {
+    if (gFrameTree.contains(frame)) {
       MessageQueue.push("scroll", () => this.collect());
     }
   },
 
   onFrameTreeCollected: function () {
     MessageQueue.push("scroll", () => this.collect());
   },
 
@@ -412,22 +412,21 @@ var ScrollPositionListener = {
 var FormDataListener = {
   init: function () {
     addEventListener("input", this, true);
     addEventListener("change", this, true);
     gFrameTree.addObserver(this);
   },
 
   handleEvent: function (event) {
-    let frame = event.target &&
-                event.target.ownerGlobal;
+    let frame = event.target.ownerGlobal;
 
     // Don't collect form data for frames created at or after the load event
     // as SessionStore can't restore form data for those.
-    if (frame && gFrameTree.contains(frame)) {
+    if (gFrameTree.contains(frame)) {
       MessageQueue.push("formdata", () => this.collect());
     }
   },
 
   onFrameTreeReset: function () {
     MessageQueue.push("formdata", () => null);
   },
 
--- a/browser/locales/en-US/chrome/browser/accounts.properties
+++ b/browser/locales/en-US/chrome/browser/accounts.properties
@@ -42,14 +42,18 @@ sendTabToAllDevices.menuitem = All Devic
 # LOCALIZATION NOTE (tabArrivingNotification.title, tabArrivingNotification.body,
 # tabsArrivingNotification.title, tabsArrivingNotification.body)
 # These strings are used in a notification shown when we're opening tab(s) another device sent us to display.
 tabArrivingNotification.title = Tab received
 # LOCALIZATION NOTE (tabArrivingNotification.body) %1 is the title of the tab and %2 is the device name.
 tabArrivingNotification.body = “%1$S” has arrived from %2$S.
 
 tabsArrivingNotification.title = Multiple tabs received
-# LOCALIZATION NOTE (tabsArrivingNotification.body) %1 is the number of tabs received and %2 is the device name.
-tabsArrivingNotification.body = %1$S tabs have arrived from %2$S.
-# LOCALIZATION NOTE (tabsArrivingNotificationMultiple.body)
-# This string is used in a notification shown when we're opening tab(s) that several devices sent us to display.
-# %S is the number of tabs received
-tabsArrivingNotificationMultiple.body = %S tabs have arrived from your connected devices.
+# LOCALIZATION NOTE (unnamedTabsArrivingNotification.body):
+# Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of tabs received and #2 is the device name.
+unnamedTabsArrivingNotification.body = #1 tab has arrived from #2.;#1 tabs have arrived from #2.
+# LOCALIZATION NOTE (unnamedTabsArrivingNotificationMultiple.body):
+# Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of tabs received.
+unnamedTabsArrivingNotificationMultiple.body = #1 tab has arrived from your connected devices.;#1 tabs have arrived from your connected devices.
--- a/browser/themes/shared/addons/addon-install-anchor.svg
+++ b/browser/themes/shared/addons/addon-install-anchor.svg
@@ -1,19 +1,11 @@
 <?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/. -->
 <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
      width="16" height="16" viewBox="0 0 16 16">
   <defs>
-    <style>
-      use:not(:target) {
-        display: none;
-      }
-      .style-icon-notification {
-        fill: #999;
-      }
-    </style>
     <path id="shape-notifications-addons" d="M10,15c0.5,0,1-0.4,1-1v-3c0,0,0-0.8,0.8-0.8c0.6,0,0.6,0.8,1.8,0.8c0.6,0,1.5-0.2,1.5-2c0-1.8-0.9-2-1.5-2 c-1.1,0-1.1,0.7-1.8,0.7C11,7.7,11,7,11,7V6c0-0.6-0.5-1-1-1H8c0,0-0.8,0-0.8-0.8C7.2,3.6,8,3.6,8,2.5C8,1.9,7.8,1,6,1 C4.2,1,4,1.9,4,2.5c0,1.1,0.8,1.1,0.8,1.8C4.8,5,4,5,4,5H2C1.5,5,1,5.4,1,6l0,1.5c0,0-0.1,1,1.1,1c0.8,0,0.9-1,1.9-1 C4.5,7.4,5,8,5,9c0,1-0.5,1.6-1,1.6c-1,0-1.1-1.1-1.9-1.1C0.9,9.5,1,10.8,1,10.8V14c0,0.6,0.5,1,1,1l2.6,0c0,0,1.1,0,1.1-1 c0-0.8-1-0.1-1-1.1c0-0.5,0.7-1.2,1.8-1.2s1.8,0.7,1.8,1.2c0,1-1.1,0.3-1.1,1.1c0,1,1.2,1,1.2,1H10z"/>
   </defs>
-  <use id="default" xlink:href="#shape-notifications-addons" class="style-icon-notification"/>
+  <use id="default" xlink:href="#shape-notifications-addons" />
 </svg>
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -61,17 +61,17 @@
   skin/classic/browser/heartbeat-star-lit.svg                  (../shared/heartbeat-star-lit.svg)
   skin/classic/browser/heartbeat-star-off.svg                  (../shared/heartbeat-star-off.svg)
   skin/classic/browser/identity-icon.svg                       (../shared/identity-block/identity-icon.svg)
   skin/classic/browser/identity-not-secure.svg                 (../shared/identity-block/identity-not-secure.svg)
   skin/classic/browser/identity-secure.svg                     (../shared/identity-block/identity-secure.svg)
   skin/classic/browser/identity-mixed-passive-loaded.svg       (../shared/identity-block/identity-mixed-passive-loaded.svg)
   skin/classic/browser/identity-mixed-active-loaded.svg        (../shared/identity-block/identity-mixed-active-loaded.svg)
   skin/classic/browser/info.svg                                (../shared/info.svg)
-  skin/classic/browser/permissions.svg                         (../shared/permissions.svg)
+  skin/classic/browser/notification-icons.svg                  (../shared/notification-icons.svg)
   skin/classic/browser/tracking-protection-16.svg              (../shared/identity-block/tracking-protection-16.svg)
   skin/classic/browser/tracking-protection-disabled-16.svg     (../shared/identity-block/tracking-protection-disabled-16.svg)
   skin/classic/browser/newtab/close.png                        (../shared/newtab/close.png)
   skin/classic/browser/newtab/controls.svg                     (../shared/newtab/controls.svg)
   skin/classic/browser/newtab/whimsycorn.png                   (../shared/newtab/whimsycorn.png)
   skin/classic/browser/preferences/in-content/favicon.ico      (../shared/incontentprefs/favicon.ico)
   skin/classic/browser/preferences/in-content/icons.svg        (../shared/incontentprefs/icons.svg)
   skin/classic/browser/preferences/in-content/search.css       (../shared/incontentprefs/search.css)
@@ -122,22 +122,16 @@
   skin/classic/browser/session-restore.svg                     (../shared/incontent-icons/session-restore.svg)
   skin/classic/browser/tab-crashed.svg                         (../shared/incontent-icons/tab-crashed.svg)
   skin/classic/browser/favicon-search-16.svg                   (../shared/favicon-search-16.svg)
   skin/classic/browser/icon-search-64.svg                      (../shared/incontent-icons/icon-search-64.svg)
   skin/classic/browser/welcome-back.svg                        (../shared/incontent-icons/welcome-back.svg)
   skin/classic/browser/reader-tour.png                         (../shared/reader/reader-tour.png)
   skin/classic/browser/reader-tour@2x.png                      (../shared/reader/reader-tour@2x.png)
   skin/classic/browser/readerMode.svg                          (../shared/reader/readerMode.svg)
-  skin/classic/browser/notification-pluginNormal.png           (../shared/plugins/notification-pluginNormal.png)
-  skin/classic/browser/notification-pluginNormal@2x.png        (../shared/plugins/notification-pluginNormal@2x.png)
-  skin/classic/browser/notification-pluginAlert.png            (../shared/plugins/notification-pluginAlert.png)
-  skin/classic/browser/notification-pluginAlert@2x.png         (../shared/plugins/notification-pluginAlert@2x.png)
-  skin/classic/browser/notification-pluginBlocked.png          (../shared/plugins/notification-pluginBlocked.png)
-  skin/classic/browser/notification-pluginBlocked@2x.png       (../shared/plugins/notification-pluginBlocked@2x.png)
   skin/classic/browser/webRTC-camera-white-16.png              (../shared/webrtc/camera-white-16.png)
   skin/classic/browser/webRTC-microphone-white-16.png          (../shared/webrtc/microphone-white-16.png)
   skin/classic/browser/webRTC-screen-white-16.png              (../shared/webrtc/screen-white-16.png)
   skin/classic/browser/panic-panel/header.png                  (../shared/panic-panel/header.png)
   skin/classic/browser/panic-panel/header@2x.png               (../shared/panic-panel/header@2x.png)
   skin/classic/browser/panic-panel/header-small.png            (../shared/panic-panel/header-small.png)
   skin/classic/browser/panic-panel/header-small@2x.png         (../shared/panic-panel/header-small@2x.png)
   skin/classic/browser/panic-panel/icons.png                   (../shared/panic-panel/icons.png)
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -36,27 +36,33 @@
 }
 
 .popup-notification-icon {
   width: 64px;
   height: 64px;
   margin-inline-end: 10px;
 }
 
+#notification-popup-box > .notification-anchor-icon:hover {
+  fill: #606060;
+}
+
 /* INDIVIDUAL NOTIFICATIONS */
 
 /* For the moment we apply the color filter only on the icons listed here.
    The first two selectors are used by socialchat.xml (bug 1275558). */
 .webRTC-sharingDevices-notification-icon,
 .webRTC-sharingMicrophone-notification-icon,
 .camera-icon,
 .geo-icon,
 .indexedDB-icon,
+.install-icon,
 .login-icon,
 .microphone-icon,
+.plugin-icon,
 .pointerLock-icon,
 .popup-icon,
 .screen-icon,
 .desktop-notification-icon,
 .popup-notification-icon[popupid="geolocation"],
 .popup-notification-icon[popupid="indexedDB-permissions-prompt"],
 .popup-notification-icon[popupid="password"],
 .popup-notification-icon[popupid="pointerLock"],
@@ -76,124 +82,124 @@
 .webRTC-sharingDevices-notification-icon,
 .webRTC-sharingMicrophone-notification-icon,
 .in-use {
   fill: #fea01b;
 }
 
 .popup-notification-icon[popupid="web-notifications"],
 .desktop-notification-icon {
-  list-style-image: url(chrome://browser/skin/permissions.svg#desktop-notification);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#desktop-notification);
 }
 
 .desktop-notification-icon.blocked {
-  list-style-image: url(chrome://browser/skin/permissions.svg#desktop-notification-blocked);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#desktop-notification-blocked);
 }
 
 .geo-icon {
 %ifdef XP_MACOSX
-  list-style-image: url(chrome://browser/skin/permissions.svg#geo-osx);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#geo-osx);
 %elif defined(MOZ_WIDGET_GTK)
-  list-style-image: url(chrome://browser/skin/permissions.svg#geo-linux);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#geo-linux);
 %else
-  list-style-image: url(chrome://browser/skin/permissions.svg#geo-windows);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#geo-windows);
 %endif
 }
 
 .geo-icon.blocked {
 %ifdef XP_MACOSX
-  list-style-image: url(chrome://browser/skin/permissions.svg#geo-osx-blocked);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#geo-osx-blocked);
 %elif defined(MOZ_WIDGET_GTK)
-  list-style-image: url(chrome://browser/skin/permissions.svg#geo-linux-blocked);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#geo-linux-blocked);
 %else
-  list-style-image: url(chrome://browser/skin/permissions.svg#geo-windows-blocked);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#geo-windows-blocked);
 %endif
 }
 
 .popup-notification-icon[popupid="geolocation"] {
 %ifdef XP_MACOSX
-  list-style-image: url(chrome://browser/skin/permissions.svg#geo-osx);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#geo-osx);
 %elif defined(MOZ_WIDGET_GTK)
-  list-style-image: url(chrome://browser/skin/permissions.svg#geo-linux-detailed);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#geo-linux-detailed);
 %else
-  list-style-image: url(chrome://browser/skin/permissions.svg#geo-windows-detailed);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#geo-windows-detailed);
 %endif
 }
 
 .popup-notification-icon[popupid="indexedDB-permissions-prompt"],
 .indexedDB-icon {
-  list-style-image: url(chrome://browser/skin/permissions.svg#indexedDB);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#indexedDB);
 }
 
 .indexedDB-icon.blocked {
-  list-style-image: url(chrome://browser/skin/permissions.svg#indexedDB-blocked);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#indexedDB-blocked);
 }
 
 .login-icon {
-  list-style-image: url(chrome://browser/skin/permissions.svg#login);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#login);
 }
 
 .popup-notification-icon[popupid="password"] {
-  list-style-image: url(chrome://browser/skin/permissions.svg#login-detailed);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#login-detailed);
 }
 
 #login-fill-notification-icon {
   /* Temporary solution until the capture and fill doorhangers are unified. */
   transform: scaleX(-1);
 }
 
 /* The first selector is used by socialchat.xml (bug 1275558). */
 .webRTC-sharingDevices-notification-icon,
 .camera-icon,
 .popup-notification-icon[popupid="webRTC-shareDevices"],
 .popup-notification-icon[popupid="webRTC-sharingDevices"] {
-  list-style-image: url(chrome://browser/skin/permissions.svg#camera);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#camera);
 }
 
 .camera-icon.blocked {
-  list-style-image: url(chrome://browser/skin/permissions.svg#camera-blocked);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#camera-blocked);
 }
 
 /* The first selector is used by socialchat.xml (bug 1275558). */
 .webRTC-sharingMicrophone-notification-icon,
 .microphone-icon {
-  list-style-image: url(chrome://browser/skin/permissions.svg#microphone);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#microphone);
 }
 
 .microphone-icon.blocked {
-  list-style-image: url(chrome://browser/skin/permissions.svg#microphone-blocked);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#microphone-blocked);
 }
 
 .popup-notification-icon[popupid="webRTC-shareMicrophone"],
 .popup-notification-icon[popupid="webRTC-sharingMicrophone"] {
-  list-style-image: url(chrome://browser/skin/permissions.svg#microphone-detailed);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#microphone-detailed);
 }
 
 .popup-notification-icon[popupid="webRTC-shareScreen"],
 .popup-notification-icon[popupid="webRTC-sharingScreen"],
 .screen-icon {
-  list-style-image: url(chrome://browser/skin/permissions.svg#screen);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#screen);
 }
 
 .screen-icon.blocked {
-  list-style-image: url(chrome://browser/skin/permissions.svg#screen-blocked);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#screen-blocked);
 }
 
 .popup-notification-icon[popupid="pointerLock"],
 .pointerLock-icon {
-  list-style-image: url(chrome://browser/skin/permissions.svg#pointerLock);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#pointerLock);
 }
 
 .pointerLock-icon.blocked {
-  list-style-image: url(chrome://browser/skin/permissions.svg#pointerLock-blocked);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#pointerLock-blocked);
 }
 
 /* This icon has a block sign in it, so we don't need a blocked version. */
 .popup-icon {
-  list-style-image: url("chrome://browser/skin/permissions.svg#popup");
+  list-style-image: url("chrome://browser/skin/notification-icons.svg#popup");
 }
 
 /* EME */
 
 .popup-notification-icon[popupid="drmContentPlaying"],
 .drm-icon {
   list-style-image: url("chrome://browser/skin/drm-icon.svg#chains");
 }
@@ -258,51 +264,24 @@
 .popup-notification-icon[popupid*="offline-app-requested"],
 .popup-notification-icon[popupid="offline-app-usage"] {
   list-style-image: url(chrome://global/skin/icons/question-64.png);
 }
 
 /* PLUGINS */
 
 .plugin-icon {
-  list-style-image: url(chrome://browser/skin/notification-pluginNormal.png);
-}
-
-.plugin-icon.plugin-hidden {
-  list-style-image: url(chrome://browser/skin/notification-pluginAlert.png);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#plugin);
 }
 
 .plugin-icon.plugin-blocked {
-  list-style-image: url(chrome://browser/skin/notification-pluginBlocked.png);
-}
-
-.plugin-icon {
-  -moz-image-region: rect(0, 16px, 16px, 0);
+  list-style-image: url(chrome://browser/skin/notification-icons.svg#plugin-blocked);
+  fill: #d92215;
 }
 
-%ifdef XP_MACOSX
-@media (min-resolution: 1.1dppx) {
-  .plugin-icon {
-    list-style-image: url(chrome://browser/skin/notification-pluginNormal@2x.png);
-  }
-
-  .plugin-icon.plugin-hidden {
-    list-style-image: url(chrome://browser/skin/notification-pluginAlert@2x.png);
-  }
-
-  .plugin-icon.plugin-blocked {
-    list-style-image: url(chrome://browser/skin/notification-pluginBlocked@2x.png);
-  }
-
-  .plugin-icon {
-    -moz-image-region: rect(0, 32px, 32px, 0);
-  }
-}
-%endif
-
 #notification-popup-box[hidden] {
   /* Override display:none to make the pluginBlockedNotification animation work
      when showing the notification repeatedly. */
   display: -moz-box;
   visibility: collapse;
 }
 
 #plugins-notification-icon.plugin-blocked[showing] {
rename from browser/themes/shared/permissions.svg
rename to browser/themes/shared/notification-icons.svg
--- a/browser/themes/shared/permissions.svg
+++ b/browser/themes/shared/notification-icons.svg
@@ -28,16 +28,17 @@
     <path id="geo-osx-icon" d="m 0,16 16,0 0,16 12,-28 z" />
     <path id="geo-windows-icon" d="m 2,14 0,4 2,0 a 12,12 0 0 0 10,10 l 0,2 4,0 0,-2 a 12,12 0 0 0 10,-10 l 2,0 0,-4 -2,0 a 12,12 0 0 0 -10,-10 l 0,-2 -4,0 0,2 a 12,12 0 0 0 -10,10 z m 4,1.9 a 10,10 0 1 1 0,0.2 z m 4,0 a 6,6 0 1 1 0,0.2 z" />
     <path id="geo-windows-detailed-icon" d="m 2,14.5 0,3 2,0.5 a 12,12 0 0 0 10,10 l 0.5,2 3,0 0.5,-2 a 12,12 0 0 0 10,-10 l 2,-0.5 0,-3 -2,-0.5 a 12,12 0 0 0 -10,-10 l -0.5,-2 -3,0 -0.5,2 a 12,12 0 0 0 -10,10 z m 4,1.4 a 10,10 0 1 1 0,0.2 z m 3,0 a 7,7 0 1 1 0,0.2 z" />
     <path id="indexedDB-icon" d="m 2,24 a 4,4 0 0 0 4,4 l 2,0 0,-4 -2,0 0,-16 20,0 0,16 -2,0 0,4 2,0 a 4,4 0 0 0 4,-4 l 0,-16 a 4,4 0 0 0 -4,-4 l -20,0 a 4,4 0 0 0 -4,4 z m 8,-2 6,7 6,-7 -4,0 0,-8 -4,0 0,8 z" />
     <path id="login-icon" d="m 2,26 0,4 6,0 0,-2 2,0 0,-2 1,0 0,-1 2,0 0,-3 2,0 2.5,-2.5 1.5,1.5 3,-3 a 8,8 0 1 0 -8,-8 l -3,3 2,2 z m 20,-18.1 a 2,2 0 1 1 0,0.2 z" />
     <path id="login-detailed-icon" d="m 1,27 0,3.5 a 0.5,0.5 0 0 0 0.5,0.5 l 5,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1.5 1.5,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1.5 1,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1 1,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-2 2,0 2.5,-2.5 q 0.5,-0.5 1,0 l 1,1 c 0.5,0.5 1,0.5 1.5,-0.5 l 1,-2 a 9,9 0 1 0 -8,-8 l -2,1 c -1,0.5 -1,1 -0.5,1.5 l 1.5,1.5 q 0.5,0.5 0,1 z m 21,-19.1 a 2,2 0 1 1 0,0.2 z" />
     <path id="microphone-icon" d="m 8,14 0,4 a 8,8 0 0 0 6,7.7 l 0,2.3 -2,0 a 2,2 0 0 0 -2,2 l 12,0 a 2,2 0 0 0 -2,-2 l -2,0 0,-2.3 a 8,8 0 0 0 6,-7.7 l 0,-4 -2,0 0,4 a 6,6 0 0 1 -12,0 l 0,-4 z m 4,4 a 4,4 0 0 0 8,0 l 0,-12 a 4,4 0 0 0 -8,0 z" />
     <path id="microphone-detailed-icon" d="m 8,18 a 8,8 0 0 0 6,7.7 l 0,2.3 -1,0 a 3,2 0 0 0 -3,2 l 12,0 a 3,2 0 0 0 -3,-2 l -1,0 0,-2.3 a 8,8 0 0 0 6,-7.7 l 0,-4 a 1,1 0 0 0 -2,0 l 0,4 a 6,6 0 0 1 -12,0 l 0,-4 a 1,1 0 0 0 -2,0 z m 4,0 a 4,4 0 0 0 8,0 l 0,-12 a 4,4 0 0 0 -8,0 z" />
+    <path id="plugin-icon" d="m 2,26 a 2,2 0 0 0 2,2 l 24,0 a 2,2 0 0 0 2,-2 l 0,-16 a 2,2 0 0 0 -2,-2 l -24,0 a 2,2 0 0 0 -2,2 z m 2,-20 10,0 0,-2 a 2,2 0 0 0 -2,-2 l -6,0 a 2,2 0 0 0 -2,2 z m 14,0 10,0 0,-2 a 2,2 0 0 0 -2,-2 l -6,0 a 2,2 0 0 0 -2,2 z" />
     <path id="pointerLock-icon" d="m 8,24 6,-5 5,10 4,-2 -5,-10 7,-1 -17,-14 z" />
     <path id="popup-icon" d="m 2,24 a 4,4 0 0 0 4,4 l 8,0 a 10,10 0 0 1 -2,-4 l -4,0 a 2,2 0 0 1 -2,-2 l 0,-12 18,0 0,2 a 10,10 0 0 1 4,2 l 0,-8 a 4,4 0 0 0 -4,-4 l -18,0 a 4,4 0 0 0 -4,4 z m 12,-2.1 a 8,8 0 1 1 0,0.2 m 10.7,-4.3 a 5,5 0 0 0 -6.9,6.9 z m -5.4,8.4 a 5,5 0 0 0 6.9,-6.9 z" />
     <path id="screen-icon" d="m 2,18 a 2,2 0 0 0 2,2 l 2,0 0,-6 a 4,4 0 0 1 4,-4 l 14,0 0,-6 a 2,2 0 0 0 -2,-2 l -18,0 a 2,2 0 0 0 -2,2 z m 6,10 a 2,2 0 0 0 2,2 l 18,0 a 2,2 0 0 0 2,-2 l 0,-14 a 2,2 0 0 0 -2,-2 l -18,0 a 2,2 0 0 0 -2,2 z" />
 
     <clipPath id="clip">
       <path d="m 0,0 0,31 31,-31 z m 6,32 26,0 0,-26 z"/>
     </clipPath>
   </defs>
@@ -56,16 +57,18 @@
   <use id="geo-windows-detailed" xlink:href="#geo-windows-detailed-icon" />
   <use id="indexedDB" xlink:href="#indexedDB-icon" />
   <use id="indexedDB-blocked" class="blocked" xlink:href="#indexedDB-icon" />
   <use id="login" xlink:href="#login-icon" />
   <use id="login-detailed" xlink:href="#login-detailed-icon" />
   <use id="microphone" xlink:href="#microphone-icon" />
   <use id="microphone-blocked" class="blocked" xlink:href="#microphone-icon" />
   <use id="microphone-detailed" xlink:href="#microphone-detailed-icon" />
+  <use id="plugin" xlink:href="#plugin-icon" />
+  <use id="plugin-blocked" class="blocked" xlink:href="#plugin-icon" />
   <use id="pointerLock" xlink:href="#pointerLock-icon" />
   <use id="pointerLock-blocked" class="blocked" xlink:href="#pointerLock-icon" />
   <use id="popup" xlink:href="#popup-icon" />
   <use id="screen" xlink:href="#screen-icon" />
   <use id="screen-blocked" class="blocked" xlink:href="#screen-icon" />
 
   <path id="strikeout" d="m 2,28 2,2 26,-26 -2,-2 z"/>
 </svg>
deleted file mode 100644
index 7492fdd8670488b9b510d913424dcc46f5e1ab57..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index f3359969b6f30fb090f424961024a72435e01b49..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index e2e9489004eb7cd0661d6ebf8d3a7be4869fddca..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 5126be01f08bfd06884278f6527e0e43f9029534..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 979e92b7f51d98aebc1ce4b6255954e6ee8a12dd..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index c081bbb475f9b60070fcae3817967da05f1b533b..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -109,25 +109,25 @@
 
 .tab-sharing-icon-overlay {
   /* 16px of the icon + 6px of margin-inline-end of .tab-icon-image */
   margin-inline-start: -22px;
   position: relative;
 }
 
 .tab-sharing-icon-overlay[sharing="camera"] {
-  list-style-image: url("chrome://browser/skin/permissions.svg#camera");
+  list-style-image: url("chrome://browser/skin/notification-icons.svg#camera");
 }
 
 .tab-sharing-icon-overlay[sharing="microphone"] {
-  list-style-image: url("chrome://browser/skin/permissions.svg#microphone");
+  list-style-image: url("chrome://browser/skin/notification-icons.svg#microphone");
 }
 
 .tab-sharing-icon-overlay[sharing="screen"] {
-  list-style-image: url("chrome://browser/skin/permissions.svg#screen");
+  list-style-image: url("chrome://browser/skin/notification-icons.svg#screen");
 }
 
 .tab-sharing-icon-overlay[sharing] {
   filter: url("chrome://browser/skin/filters.svg#fill");
   fill: rgb(224, 41, 29);
 }
 
 .tab-icon-overlay {
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1018,76 +1018,144 @@ toolbar[brighttext] .toolbarbutton-1 > .
    * padding (2 * 2px) + border (2 * 1px), but as a minimum because otherwise
    * increase in text sizes would break things...
    */
   min-height: 24px;
 }
 
 /* ::::: fullscreen window controls ::::: */
 
-#window-controls {
-  margin-inline-start: 4px;
-}
-
 #minimize-button,
 #restore-button,
 #close-button {
-  list-style-image: url("chrome://global/skin/icons/windowControls.png");
-  padding: 0;
+  -moz-appearance: none;
+  border: none;
+  margin: 0 !important;
+  padding: 6px 12px;
 }
 
 #minimize-button {
-  -moz-image-region: rect(0, 16px, 16px, 0);
+  list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize);
 }
-#minimize-button:hover {
-  -moz-image-region: rect(16px, 16px, 32px, 0);
+
+#restore-button {
+  list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore);
 }
-#minimize-button:hover:active {
-  -moz-image-region: rect(32px, 16px, 48px, 0);
+
+#minimize-button:hover,
+#restore-button:hover {
+  background-color: hsla(0, 0%, 0%, .12);
 }
-#restore-button {
-  -moz-image-region: rect(0, 32px, 16px, 16px);
-}
-#restore-button:hover {
-  -moz-image-region: rect(16px, 32px, 32px, 16px);
-}
+
+#minimize-button:hover:active,
 #restore-button:hover:active {
-  -moz-image-region: rect(32px, 32px, 48px, 16px);
+  background-color: hsla(0, 0%, 0%, .22);
 }
+
 #close-button {
-  -moz-image-region: rect(0, 48px, 16px, 32px);
-  -moz-appearance: none;
-  border-style: none;
-  margin: 2px;
+  list-style-image: url(chrome://browser/skin/caption-buttons.svg#close);
+}
+
+#close-button:hover {
+  background-color: hsl(355, 86%, 49%);
+  list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white);
+}
+
+#close-button:hover:active {
+  background-color: hsl(355, 82%, 69%);
+}
+
+toolbar[brighttext] #minimize-button {
+  list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-white);
+}
+
+toolbar[brighttext] #restore-button {
+  list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-white);
+}
+
+toolbar[brighttext] #close-button {
+  list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white);
 }
-#close-button:hover {
-  -moz-image-region: rect(16px, 48px, 32px, 32px);
+
+@media (-moz-os-version: windows-xp),
+       (-moz-os-version: windows-vista),
+       (-moz-os-version: windows-win7) {
+  #window-controls {
+    margin-inline-start: 4px;
+  }
+
+  #minimize-button,
+  #restore-button,
+  #close-button {
+    list-style-image: url("chrome://global/skin/icons/windowControls.png");
+    padding: 0;
+  }
+
+  #minimize-button {
+    -moz-image-region: rect(0, 16px, 16px, 0);
+  }
+
+  #minimize-button:hover {
+    -moz-image-region: rect(16px, 16px, 32px, 0);
+  }
+
+  #minimize-button:hover:active {
+    -moz-image-region: rect(32px, 16px, 48px, 0);
+  }
+
+  #restore-button {
+    -moz-image-region: rect(0, 32px, 16px, 16px);
+  }
+
+  #restore-button:hover {
+    -moz-image-region: rect(16px, 32px, 32px, 16px);
+  }
+
+  #restore-button:hover:active {
+    -moz-image-region: rect(32px, 32px, 48px, 16px);
+  }
+
+  #close-button {
+    -moz-image-region: rect(0, 48px, 16px, 32px);
+    -moz-appearance: none;
+    border-style: none;
+    margin: 2px;
+  }
+
+  #close-button:hover {
+    -moz-image-region: rect(16px, 48px, 32px, 32px);
+  }
+
+  #close-button:hover:active {
+    -moz-image-region: rect(32px, 48px, 48px, 32px);
+  }
 }
-#close-button:hover:active {
-  -moz-image-region: rect(32px, 48px, 48px, 32px);
-}
-
-@media not all and (-moz-os-version: windows-xp) {
+
+@media (-moz-os-version: windows-vista),
+       (-moz-os-version: windows-win7) {
   #window-controls {
     -moz-box-align: start;
   }
 
   #minimize-button,
   #restore-button,
   #close-button {
     -moz-appearance: none;
     border-style: none;
     margin: 0;
   }
+
   #close-button {
     -moz-image-region: rect(0, 49px, 16px, 32px);
   }
+
   #close-button:hover {
     -moz-image-region: rect(16px, 49px, 32px, 32px);
   }
+
   #close-button:hover:active {
     -moz-image-region: rect(32px, 49px, 48px, 32px);
   }
 
   #minimize-button:-moz-locale-dir(rtl),
   #restore-button:-moz-locale-dir(rtl),
   #close-button:-moz-locale-dir(rtl) {
     transform: scaleX(-1);
--- a/build/pgo/server-locations.txt
+++ b/build/pgo/server-locations.txt
@@ -176,16 +176,19 @@ http://itisatracker.org:80
 http://trackertest.org:80
 
 https://malware.example.com:443
 https://unwanted.example.com:443
 https://tracking.example.com:443
 https://not-tracking.example.com:443
 https://tracking.example.org:443
 
+# Bug 1281083
+http://bug1281083.example.com:80
+
 # Bug 483437, 484111
 https://www.bank1.com:443           privileged,cert=escapeattack1
 
 #
 # CONNECT for redirproxy results in a 302 redirect to
 # test1.example.com
 #
 https://redirproxy.example.com:443          privileged,redir=test1.example.com
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -1,18 +1,16 @@
 /* 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 Services = require("Services");
-
-loader.lazyGetter(this, "osString", () => Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS);
+const osString = Services.appinfo.OS;
 
 // Panels
 loader.lazyGetter(this, "OptionsPanel", () => require("devtools/client/framework/toolbox-options").OptionsPanel);
 loader.lazyGetter(this, "InspectorPanel", () => require("devtools/client/inspector/inspector-panel").InspectorPanel);
 loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/client/webconsole/panel").WebConsolePanel);
 loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/client/debugger/panel").DebuggerPanel);
 loader.lazyGetter(this, "StyleEditorPanel", () => require("devtools/client/styleeditor/styleeditor-panel").StyleEditorPanel);
 loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/client/shadereditor/panel").ShaderEditorPanel);
--- a/devtools/client/eyedropper/eyedropper.js
+++ b/devtools/client/eyedropper/eyedropper.js
@@ -27,20 +27,16 @@ loader.lazyGetter(this, "ioService", fun
   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, "XULRuntime", function () {
-  return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
-});
-
 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";
@@ -112,17 +108,17 @@ function Eyedropper(chromeWindow, opts =
   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 = XULRuntime.OS;
+  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
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -1,12 +1,13 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* 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/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
 
 "use strict";
 
 // This shared-head.js file is used for multiple mochitest test directories in
 // devtools.
 // It contains various common helper functions.
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr, Constructor: CC}
@@ -469,8 +470,22 @@ function waitForContextMenu(popup, butto
  * @return {Promise} resolves when the preferences have been updated
  */
 function pushPref(preferenceName, value) {
   return new Promise(resolve => {
     let options = {"set": [[preferenceName, value]]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 }
+
+/**
+ * Lookup the provided dotted path ("prop1.subprop2.myProp") in the provided object.
+ *
+ * @param {Object} obj
+ *        Object to expand.
+ * @param {String} path
+ *        Dotted path to use to expand the object.
+ * @return {?} anything that is found at the provided path in the object.
+ */
+function lookupPath(obj, path) {
+  let segments = path.split(".");
+  return segments.reduce((prev, current) => prev[current], obj);
+}
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -65,19 +65,16 @@ loader.lazyRequireGetter(this, "system",
   "devtools/shared/system");
 loader.lazyRequireGetter(this, "getPreferenceFront",
   "devtools/shared/fronts/preference", true);
 loader.lazyRequireGetter(this, "KeyShortcuts",
   "devtools/client/shared/key-shortcuts", true);
 loader.lazyRequireGetter(this, "ZoomKeys",
   "devtools/client/shared/zoom-keys");
 
-loader.lazyGetter(this, "osString", () => {
-  return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
-});
 loader.lazyGetter(this, "registerHarOverlay", () => {
   return require("devtools/client/netmonitor/har/toolbox-overlay").register;
 });
 
 // White-list buttons that can be toggled to prevent adding prefs for
 // addons that have manually inserted toolbarbuttons into DOM.
 // (By default, supported target is only local tab)
 const ToolboxButtons = exports.ToolboxButtons = [
@@ -490,17 +487,18 @@ Toolbox.prototype = {
   get ReactDOM() {
     return this.browserRequire("devtools/client/shared/vendor/react-dom");
   },
 
   _pingTelemetry: function () {
     this._telemetry.toolOpened("toolbox");
 
     this._telemetry.logOncePerBrowserVersion(OS_HISTOGRAM, system.getOSCPU());
-    this._telemetry.logOncePerBrowserVersion(OS_IS_64_BITS, system.is64Bit ? 1 : 0);
+    this._telemetry.logOncePerBrowserVersion(OS_IS_64_BITS,
+                                             Services.appinfo.is64Bit ? 1 : 0);
     this._telemetry.logOncePerBrowserVersion(SCREENSIZE_HISTOGRAM, system.getScreenDimensions());
   },
 
   /**
    * Because our panels are lazy loaded this is a good place to watch for
    * "pref-changed" events.
    * @param  {String} event
    *         The event type, "pref-changed".
--- a/devtools/client/inspector/computed/computed.js
+++ b/devtools/client/inspector/computed/computed.js
@@ -24,16 +24,18 @@ const {XPCOMUtils} = require("resource:/
 const {getCssProperties} = require("devtools/shared/fronts/css-properties");
 
 loader.lazyRequireGetter(this, "overlays",
   "devtools/client/inspector/shared/style-inspector-overlays");
 loader.lazyRequireGetter(this, "StyleInspectorMenu",
   "devtools/client/inspector/shared/style-inspector-menu");
 loader.lazyRequireGetter(this, "KeyShortcuts",
   "devtools/client/shared/key-shortcuts", true);
+loader.lazyRequireGetter(this, "LayoutView",
+  "devtools/client/inspector/layout/layout", true);
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
 
 XPCOMUtils.defineLazyGetter(CssComputedView, "_strings", function () {
   return Services.strings.createBundle(
     "chrome://devtools-shared/locale/styleinspector.properties");
 });
@@ -164,17 +166,16 @@ function CssComputedView(inspector, docu
   this._onCopy = this._onCopy.bind(this);
   this._onFilterStyles = this._onFilterStyles.bind(this);
   this._onClearSearch = this._onClearSearch.bind(this);
   this._onIncludeBrowserStyles = this._onIncludeBrowserStyles.bind(this);
   this._onFilterTextboxContextMenu =
     this._onFilterTextboxContextMenu.bind(this);
 
   let doc = this.styleDocument;
-  this.root = doc.getElementById("root");
   this.element = doc.getElementById("propertyContainer");
   this.searchField = doc.getElementById("computedview-searchbox");
   this.searchClearButton = doc.getElementById("computedview-searchinput-clear");
   this.includeBrowserStylesCheckbox =
     doc.getElementById("browser-style-checkbox");
 
   this.shortcuts = new KeyShortcuts({ window: this.styleWindow });
   this._onShortcut = this._onShortcut.bind(this);
@@ -772,17 +773,16 @@ CssComputedView.prototype = {
     this.searchField.removeEventListener("input", this._onFilterStyles);
     this.searchField.removeEventListener("contextmenu",
                                          this._onFilterTextboxContextMenu);
     this.searchClearButton.removeEventListener("click", this._onClearSearch);
     this.includeBrowserStylesCheckbox.removeEventListener("input",
       this._onIncludeBrowserStyles);
 
     // Nodes used in templating
-    this.root = null;
     this.element = null;
     this.panel = null;
     this.searchField = null;
     this.searchClearButton = null;
     this.includeBrowserStylesCheckbox = null;
 
     // Property views
     for (let propView of this.propertyViews) {
@@ -1402,85 +1402,86 @@ SelectorView.prototype = {
     });
   }
 };
 
 function ComputedViewTool(inspector, window) {
   this.inspector = inspector;
   this.document = window.document;
 
-  this.view = new CssComputedView(this.inspector, this.document,
+  this.computedView = new CssComputedView(this.inspector, this.document,
     this.inspector.pageStyle);
+  this.layoutView = new LayoutView(this.inspector, this.document);
 
   this.onSelected = this.onSelected.bind(this);
   this.refresh = this.refresh.bind(this);
   this.onPanelSelected = this.onPanelSelected.bind(this);
   this.onMutations = this.onMutations.bind(this);
   this.onResized = this.onResized.bind(this);
 
   this.inspector.selection.on("detached", this.onSelected);
   this.inspector.selection.on("new-node-front", this.onSelected);
   this.inspector.selection.on("pseudoclass", this.refresh);
   this.inspector.sidebar.on("computedview-selected", this.onPanelSelected);
   this.inspector.pageStyle.on("stylesheet-updated", this.refresh);
   this.inspector.walker.on("mutations", this.onMutations);
   this.inspector.walker.on("resize", this.onResized);
 
-  this.view.selectElement(null);
+  this.computedView.selectElement(null);
 
   this.onSelected();
 }
 
 ComputedViewTool.prototype = {
   isSidebarActive: function () {
-    if (!this.view) {
+    if (!this.computedView) {
       return false;
     }
     return this.inspector.sidebar.getCurrentTabID() == "computedview";
   },
 
   onSelected: function (event) {
     // Ignore the event if the view has been destroyed, or if it's inactive.
     // But only if the current selection isn't null. If it's been set to null,
     // let the update go through as this is needed to empty the view on
     // navigation.
-    if (!this.view) {
+    if (!this.computedView) {
       return;
     }
 
     let isInactive = !this.isSidebarActive() &&
                      this.inspector.selection.nodeFront;
     if (isInactive) {
       return;
     }
 
-    this.view.setPageStyle(this.inspector.pageStyle);
+    this.computedView.setPageStyle(this.inspector.pageStyle);
 
     if (!this.inspector.selection.isConnected() ||
         !this.inspector.selection.isElementNode()) {
-      this.view.selectElement(null);
+      this.computedView.selectElement(null);
       return;
     }
 
     if (!event || event == "new-node-front") {
       let done = this.inspector.updating("computed-view");
-      this.view.selectElement(this.inspector.selection.nodeFront).then(() => {
+      this.computedView.selectElement(this.inspector.selection.nodeFront).then(() => {
         done();
       });
     }
   },
 
   refresh: function () {
     if (this.isSidebarActive()) {
-      this.view.refreshPanel();
+      this.computedView.refreshPanel();
     }
   },
 
   onPanelSelected: function () {
-    if (this.inspector.selection.nodeFront === this.view._viewedElement) {
+    if (this.inspector.selection.nodeFront === this.computedView._viewedElement) {
       this.refresh();
     } else {
       this.onSelected();
     }
   },
 
   /**
    * When markup mutations occur, if an attribute of the selected node changes,
@@ -1511,17 +1512,18 @@ ComputedViewTool.prototype = {
     this.inspector.selection.off("pseudoclass", this.refresh);
     this.inspector.selection.off("new-node-front", this.onSelected);
     this.inspector.selection.off("detached", this.onSelected);
     this.inspector.sidebar.off("computedview-selected", this.onPanelSelected);
     if (this.inspector.pageStyle) {
       this.inspector.pageStyle.off("stylesheet-updated", this.refresh);
     }
 
-    this.view.destroy();
+    this.computedView.destroy();
+    this.layoutView.destroy();
 
-    this.view = this.document = this.inspector = null;
+    this.computedView = this.layoutView = this.document = this.inspector = null;
   }
 };
 
 exports.CssComputedView = CssComputedView;
 exports.ComputedViewTool = ComputedViewTool;
 exports.PropertyView = PropertyView;
--- a/devtools/client/inspector/computed/test/browser_computed_cycle_color.js
+++ b/devtools/client/inspector/computed/test/browser_computed_cycle_color.js
@@ -54,16 +54,17 @@ function* checkColorCycling(container, v
   for (let test of tests) {
     yield checkSwatchShiftClick(container, win, test.value, test.comment);
   }
 }
 
 function* checkSwatchShiftClick(container, win, expectedValue, comment) {
   let swatch = container.querySelector(".computedview-colorswatch");
   let valueNode = container.querySelector(".computedview-color");
+  swatch.scrollIntoView();
 
   let onUnitChange = swatch.once("unit-change");
   EventUtils.synthesizeMouseAtCenter(swatch, {
     type: "mousedown",
     shiftKey: true
   }, win);
   yield onUnitChange;
   is(valueNode.textContent, expectedValue, comment);
--- a/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js
+++ b/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js
@@ -20,16 +20,17 @@ add_task(function* () {
   let {inspector, view} = yield openComputedView();
   yield selectNode(".matches", inspector);
 
   let propView = getFirstVisiblePropertyView(view);
   let rulesTable = propView.matchedSelectorsContainer;
   let matchedExpander = propView.element;
 
   info("Focusing the property");
+  matchedExpander.scrollIntoView();
   let onMatchedExpanderFocus = once(matchedExpander, "focus", true);
   EventUtils.synthesizeMouseAtCenter(matchedExpander, {}, view.styleWindow);
   yield onMatchedExpanderFocus;
 
   yield checkToggleKeyBinding(view.styleWindow, "VK_SPACE", rulesTable,
                               inspector);
   yield checkToggleKeyBinding(view.styleWindow, "VK_RETURN", rulesTable,
                               inspector);
--- a/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js
+++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js
@@ -25,17 +25,17 @@ add_task(function* () {
   yield testExpandOnDblClick(view, inspector);
   yield testCollapseOnDblClick(view, inspector);
 });
 
 function* testExpandOnTwistyClick({styleDocument, styleWindow}, inspector) {
   info("Testing that a property expands on twisty click");
 
   info("Getting twisty element");
-  let twisty = styleDocument.querySelector(".expandable");
+  let twisty = styleDocument.querySelector("#propertyContainer .expandable");
   ok(twisty, "Twisty found");
 
   let onExpand = inspector.once("computed-view-property-expanded");
   info("Clicking on the twisty element");
   twisty.click();
 
   yield onExpand;
 
@@ -44,17 +44,17 @@ function* testExpandOnTwistyClick({style
   ok(div.childNodes.length > 0,
     "Matched selectors are expanded on twisty click");
 }
 
 function* testCollapseOnTwistyClick({styleDocument, styleWindow}, inspector) {
   info("Testing that a property collapses on twisty click");
 
   info("Getting twisty element");
-  let twisty = styleDocument.querySelector(".expandable");
+  let twisty = styleDocument.querySelector("#propertyContainer .expandable");
   ok(twisty, "Twisty found");
 
   let onCollapse = inspector.once("computed-view-property-collapsed");
   info("Clicking on the twisty element");
   twisty.click();
 
   yield onCollapse;
 
@@ -66,16 +66,18 @@ function* testCollapseOnTwistyClick({sty
 
 function* testExpandOnDblClick({styleDocument, styleWindow}, inspector) {
   info("Testing that a property expands on container dbl-click");
 
   info("Getting computed property container");
   let container = styleDocument.querySelector(".property-view");
   ok(container, "Container found");
 
+  container.scrollIntoView();
+
   let onExpand = inspector.once("computed-view-property-expanded");
   info("Dbl-clicking on the container");
   EventUtils.synthesizeMouseAtCenter(container, {clickCount: 2}, styleWindow);
 
   yield onExpand;
 
   // Expanded means the matchedselectors div is not empty
   let div = styleDocument.querySelector(".property-content .matchedselectors");
--- a/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js
+++ b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js
@@ -1,19 +1,17 @@
 /* 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";
 
 // Tests that properties can be selected and copied from the computed view.
 
-XPCOMUtils.defineLazyGetter(this, "osString", function () {
-  return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
-});
+const osString = Services.appinfo.OS;
 
 const TEST_URI = `
   <style type="text/css">
     span {
       font-variant-caps: small-caps;
       color: #000000;
     }
     .nomatches {
--- a/devtools/client/inspector/computed/test/head.js
+++ b/devtools/client/inspector/computed/test/head.js
@@ -127,17 +127,17 @@ function getComputedViewPropertyValue(vi
  *        The instance of the computed view panel
  * @param {Number} index
  *        The index of the property to be expanded
  * @return a promise that resolves when the property has been expanded, or
  * rejects if the property was not found
  */
 function expandComputedViewPropertyByIndex(view, index) {
   info("Expanding property " + index + " in the computed view");
-  let expandos = view.styleDocument.querySelectorAll(".expandable");
+  let expandos = view.styleDocument.querySelectorAll("#propertyContainer .expandable");
   if (!expandos.length || !expandos[index]) {
     return promise.reject();
   }
 
   let onExpand = view.inspector.once("computed-view-property-expanded");
   expandos[index].click();
   return onExpand;
 }
--- a/devtools/client/inspector/inspector-panel.js
+++ b/devtools/client/inspector/inspector-panel.js
@@ -26,17 +26,16 @@ const MenuItem = require("devtools/clien
 
 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);
 loader.lazyRequireGetter(this, "FontInspector", "devtools/client/inspector/fonts/fonts", true);
 loader.lazyRequireGetter(this, "HTMLBreadcrumbs", "devtools/client/inspector/breadcrumbs", true);
 loader.lazyRequireGetter(this, "InspectorSearch", "devtools/client/inspector/inspector-search", true);
-loader.lazyRequireGetter(this, "LayoutView", "devtools/client/inspector/layout/layout", true);
 loader.lazyRequireGetter(this, "MarkupView", "devtools/client/inspector/markup/markup", true);
 loader.lazyRequireGetter(this, "RuleViewTool", "devtools/client/inspector/rules/rules", true);
 loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/inspector/toolsidebar", true);
 loader.lazyRequireGetter(this, "ViewHelpers", "devtools/client/shared/widgets/view-helpers", true);
 
 loader.lazyGetter(this, "strings", () => {
   return Services.strings.createBundle("chrome://devtools/locale/inspector.properties");
 });
@@ -418,30 +417,24 @@ InspectorPanel.prototype = {
       strings.GetStringFromName("inspector.sidebar.ruleViewTitle"),
       defaultTab == "ruleview");
 
     this.sidebar.addExistingTab(
       "computedview",
       strings.GetStringFromName("inspector.sidebar.computedViewTitle"),
       defaultTab == "computedview");
 
-    this.sidebar.addExistingTab(
-      "layoutview",
-      strings.GetStringFromName("inspector.sidebar.layoutViewTitle"),
-      defaultTab == "layoutview");
-
     this._setDefaultSidebar = (event, toolId) => {
       Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
     };
 
     this.sidebar.on("select", this._setDefaultSidebar);
 
     this.ruleview = new RuleViewTool(this, this.panelWin);
     this.computedview = new ComputedViewTool(this, this.panelWin);
-    this.layoutview = new LayoutView(this, this.panelWin);
 
     if (this.target.form.animationsActor) {
       this.sidebar.addFrameTab(
         "animationinspector",
         strings.GetStringFromName("inspector.sidebar.animationInspectorTitle"),
         "chrome://devtools/content/animationinspector/animation-inspector.xhtml",
         defaultTab == "animationinspector");
     }
@@ -747,20 +740,16 @@ InspectorPanel.prototype = {
     if (this.computedview) {
       this.computedview.destroy();
     }
 
     if (this.fontInspector) {
       this.fontInspector.destroy();
     }
 
-    if (this.layoutview) {
-      this.layoutview.destroy();
-    }
-
     let cssPropertiesDestroyer = this._cssPropertiesLoaded.then(({front}) => {
       if (front) {
         front.destroy();
       }
     });
 
     this.sidebar.off("select", this._setDefaultSidebar);
     let sidebarDestroyer = this.sidebar.destroy();
--- a/devtools/client/inspector/inspector.xul
+++ b/devtools/client/inspector/inspector.xul
@@ -102,69 +102,74 @@
           </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="propertyContainer">
-        </html:div>
-
-        <html:div id="computedview-no-results" hidden="">
-          &noPropertiesFound;
-        </html:div>
-      </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="sidebar-panel-layoutview" class="devtools-monospace theme-sidebar inspector-tabpanel">
-        <html:div id="layout-wrapper">
-          <html:div id="layout-container">
-            <html:p id="layout-header">
-              <html:span id="layout-element-size"></html:span>
-              <html:section id="layout-position-group">
-                <html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
-                <html:span id="layout-element-position"></html:span>
-              </html:section>
-            </html:p>
-
-            <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: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-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: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-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:div style="display: none">
+                <html:p id="layout-dummy"></html:p>
+              </html:div>
+            </html:div>
+          </html:div>
 
-              <html:p class="layout-size"><html:span data-box="content" title="&content.tooltip;"></html:span></html:p>
-            </html:div>
+          <html:div id="propertyContainer" class="theme-separator" tabindex="0">
+          </html:div>
 
-            <html:div style="display: none">
-              <html:p id="layout-dummy"></html:p>
-            </html:div>
+          <html:div id="computedview-no-results" hidden="">
+            &noPropertiesFound;
           </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/layout/layout.js
+++ b/devtools/client/inspector/layout/layout.js
@@ -63,17 +63,17 @@ EditingSession.prototype = {
    * no style rules affect the property.
    *
    * @param property  The name of the property as a string
    */
   getProperty: function (property) {
     // Create a hidden element for getPropertyFromRule to use
     let div = this._doc.createElement("div");
     div.setAttribute("style", "display: none");
-    this._doc.getElementById("sidebar-panel-layoutview").appendChild(div);
+    this._doc.getElementById("sidebar-panel-computedview").appendChild(div);
     this._element = this._doc.createElement("p");
     div.appendChild(this._element);
 
     // As the rules are in order of priority we can just iterate until we find
     // the first that defines a value for the property and return that.
     for (let rule of this._rules) {
       let value = this.getPropertyFromRule(rule, property);
       if (value !== "") {
@@ -178,43 +178,52 @@ EditingSession.prototype = {
     this._doc = null;
     this._rules = null;
     this._modifications.clear();
   }
 };
 
 /**
  * The layout-view panel
- * @param {InspectorPanel} inspector An instance of the inspector-panel
- * currently loaded in the toolbox
- * @param {Window} win The window containing the panel
+ * @param {InspectorPanel} inspector
+ *        An instance of the inspector-panel currently loaded in the toolbox
+ * @param {Document} document
+ *        The document that will contain the layout view.
  */
-function LayoutView(inspector, win) {
+function LayoutView(inspector, document) {
   this.inspector = inspector;
-  this.doc = win.document;
+  this.doc = document;
+  this.wrapper = this.doc.getElementById("layout-wrapper");
+  this.container = this.doc.getElementById("layout-container");
+  this.expander = this.doc.getElementById("layout-expander");
   this.sizeLabel = this.doc.querySelector(".layout-size > span");
   this.sizeHeadingLabel = this.doc.getElementById("layout-element-size");
   this._geometryEditorHighlighter = null;
 
   this.init();
 }
 
 LayoutView.prototype = {
   init: function () {
     this.update = this.update.bind(this);
 
     this.onNewSelection = this.onNewSelection.bind(this);
     this.inspector.selection.on("new-node-front", this.onNewSelection);
 
     this.onNewNode = this.onNewNode.bind(this);
-    this.inspector.sidebar.on("layoutview-selected", this.onNewNode);
+    this.inspector.sidebar.on("computedview-selected", this.onNewNode);
 
     this.onSidebarSelect = this.onSidebarSelect.bind(this);
     this.inspector.sidebar.on("select", this.onSidebarSelect);
 
+    this.onToggleExpander = this.onToggleExpander.bind(this);
+    this.expander.addEventListener("click", this.onToggleExpander);
+    let header = this.doc.getElementById("layout-header");
+    header.addEventListener("dblclick", this.onToggleExpander);
+
     this.onPickerStarted = this.onPickerStarted.bind(this);
     this.onMarkupViewLeave = this.onMarkupViewLeave.bind(this);
     this.onMarkupViewNodeHover = this.onMarkupViewNodeHover.bind(this);
     this.onWillNavigate = this.onWillNavigate.bind(this);
 
     this.initBoxModelHighlighter();
 
     // Store for the different dimensions of the node.
@@ -308,17 +317,16 @@ LayoutView.prototype = {
     // Mark document as RTL or LTR:
     let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]
                     .getService(Ci.nsIXULChromeRegistry);
     let dir = chromeReg.isLocaleRTL("global");
     let container = this.doc.getElementById("layout-container");
     container.setAttribute("dir", dir ? "rtl" : "ltr");
 
     let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
-
     this.onGeometryButtonClick = this.onGeometryButtonClick.bind(this);
     nodeGeometry.addEventListener("click", this.onGeometryButtonClick);
   },
 
   initBoxModelHighlighter: function () {
     let highlightElts = this.doc.querySelectorAll("#layout-container *[title]");
     this.onHighlightMouseOver = this.onHighlightMouseOver.bind(this);
     this.onHighlightMouseOut = this.onHighlightMouseOut.bind(this);
@@ -410,17 +418,17 @@ LayoutView.prototype = {
   },
 
   /**
    * Is the layoutview visible in the sidebar.
    * @return {Boolean}
    */
   isViewVisible: function () {
     return this.inspector &&
-           this.inspector.sidebar.getCurrentTabID() == "layoutview";
+           this.inspector.sidebar.getCurrentTabID() == "computedview";
   },
 
   /**
    * Is the layoutview visible in the sidebar and is the current node valid to
    * be displayed in the view.
    * @return {Boolean}
    */
   isViewVisibleAndNodeValid: function () {
@@ -435,55 +443,62 @@ LayoutView.prototype = {
   destroy: function () {
     let highlightElts = this.doc.querySelectorAll("#layout-container *[title]");
 
     for (let element of highlightElts) {
       element.removeEventListener("mouseover", this.onHighlightMouseOver, true);
       element.removeEventListener("mouseout", this.onHighlightMouseOut, true);
     }
 
+    this.expander.removeEventListener("click", this.onToggleExpander);
+    let header = this.doc.getElementById("layout-header");
+    header.removeEventListener("dblclick", this.onToggleExpander);
+
     let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
     nodeGeometry.removeEventListener("click", this.onGeometryButtonClick);
 
     this.inspector.off("picker-started", this.onPickerStarted);
 
     // Inspector Panel will destroy `markup` object on "will-navigate" event,
     // therefore we have to check if it's still available in case LayoutView
     // is destroyed immediately after.
     if (this.inspector.markup) {
       this.inspector.markup.off("leave", this.onMarkupViewLeave);
       this.inspector.markup.off("node-hover", this.onMarkupViewNodeHover);
     }
 
-    this.inspector.sidebar.off("layoutview-selected", this.onNewNode);
+    this.inspector.sidebar.off("computedview-selected", this.onNewNode);
     this.inspector.selection.off("new-node-front", this.onNewSelection);
     this.inspector.sidebar.off("select", this.onSidebarSelect);
     this.inspector._target.off("will-navigate", this.onWillNavigate);
 
-    this.sizeHeadingLabel = null;
-    this.sizeLabel = null;
     this.inspector = null;
     this.doc = null;
+    this.wrapper = null;
+    this.container = null;
+    this.expander = null;
+    this.sizeLabel = null;
+    this.sizeHeadingLabel = null;
 
     if (this.reflowFront) {
       this.untrackReflows();
       this.reflowFront.destroy();
       this.reflowFront = null;
     }
   },
 
   onSidebarSelect: function (e, sidebar) {
-    this.setActive(sidebar === "layoutview");
+    this.setActive(sidebar === "computedview");
   },
 
   /**
    * Selection 'new-node-front' event handler.
    */
   onNewSelection: function () {
-    let done = this.inspector.updating("layoutview");
+    let done = this.inspector.updating("computed-view");
     this.onNewNode()
       .then(() => this.hideGeometryEditor())
       .then(done, (err) => {
         console.error(err);
         done();
       }).catch(console.error);
   },
 
@@ -521,16 +536,28 @@ LayoutView.prototype = {
       this.showGeometryEditor();
     }
   },
 
   onPickerStarted: function () {
     this.hideGeometryEditor();
   },
 
+  onToggleExpander: function () {
+    let isOpen = this.expander.hasAttribute("open");
+
+    if (isOpen) {
+      this.container.hidden = true;
+      this.expander.removeAttribute("open");
+    } else {
+      this.container.hidden = false;
+      this.expander.setAttribute("open", "");
+    }
+  },
+
   onMarkupViewLeave: function () {
     this.showGeometryEditor(true);
   },
 
   onMarkupViewNodeHover: function () {
     this.hideGeometryEditor(false);
   },
 
@@ -545,34 +572,33 @@ LayoutView.prototype = {
    * @param {Boolean} isActive
    */
   setActive: function (isActive) {
     if (isActive === this.isActive) {
       return;
     }
     this.isActive = isActive;
 
-    let panel = this.doc.getElementById("sidebar-panel-layoutview");
-    panel.classList.toggle("inactive", !isActive);
-
     if (isActive) {
       this.trackReflows();
     } else {
       this.untrackReflows();
     }
   },
 
   /**
    * Compute the dimensions of the node and update the values in
-   * the layoutview/view.xhtml document.
+   * the inspector.xul document.
    * @return a promise that will be resolved when complete.
    */
   update: function () {
     let lastRequest = Task.spawn((function* () {
       if (!this.isViewVisibleAndNodeValid()) {
+        this.wrapper.hidden = true;
+        this.inspector.emit("layoutview-updated");
         return null;
       }
 
       let node = this.inspector.selection.nodeFront;
       let layout = yield this.inspector.pageStyle.getLayout(node, {
         autoMargins: this.isActive
       });
       let styleEntries = yield this.inspector.pageStyle.getApplied(node, {});
@@ -588,22 +614,16 @@ LayoutView.prototype = {
       let width = layout.width;
       let height = layout.height;
       let newLabel = SHARED_L10N.getFormatStr("dimensions", width, height);
 
       if (this.sizeHeadingLabel.textContent != newLabel) {
         this.sizeHeadingLabel.textContent = newLabel;
       }
 
-      // If the view isn't active, no need to do anything more.
-      if (!this.isActive) {
-        this.inspector.emit("layoutview-updated");
-        return null;
-      }
-
       for (let i in this.map) {
         let property = this.map[i].property;
         if (!(property in layout)) {
           // Depending on the actor version, some properties
           // might be missing.
           continue;
         }
         let parsedValue = parseFloat(layout[property]);
@@ -651,18 +671,20 @@ LayoutView.prototype = {
 
       let newValue = width + "\u00D7" + height;
       if (this.sizeLabel.textContent != newValue) {
         this.sizeLabel.textContent = newValue;
       }
 
       this.elementRules = styleEntries.map(e => e.rule);
 
+      this.wrapper.hidden = false;
+
       this.inspector.emit("layoutview-updated");
-      return undefined;
+      return null;
     }).bind(this)).catch(console.error);
 
     this._lastRequest = lastRequest;
     return this._lastRequest;
   },
 
   /**
    * Update the text in the tooltip shown when hovering over a value to provide
--- a/devtools/client/inspector/layout/test/head.js
+++ b/devtools/client/inspector/layout/test/head.js
@@ -50,33 +50,33 @@ function selectAndHighlightNode(nodeOrSe
 
 /**
  * Open the toolbox, with the inspector tool visible, and the layout-view
  * sidebar tab selected.
  * @return a promise that resolves when the inspector is ready and the layout
  * view is visible and ready
  */
 function openLayoutView() {
-  return openInspectorSidebarTab("layoutview").then(data => {
+  return openInspectorSidebarTab("computedview").then(data => {
     // The actual highligher show/hide methods are mocked in layoutview tests.
     // The highlighter is tested in devtools/inspector/test.
     function mockHighlighter({highlighter}) {
       highlighter.showBoxModel = function () {
         return promise.resolve();
       };
       highlighter.hideBoxModel = function () {
         return promise.resolve();
       };
     }
     mockHighlighter(data.toolbox);
 
     return {
       toolbox: data.toolbox,
       inspector: data.inspector,
-      view: data.inspector.layoutview,
+      view: data.inspector.computedview.layoutView,
       testActor: data.testActor
     };
   });
 }
 
 /**
  * Wait for the layoutview-updated event.
  * @return a promise
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -48,16 +48,17 @@ const Heritage = require("sdk/core/herit
 const {parseAttribute} =
       require("devtools/client/shared/node-attribute-parser");
 const {Task} = require("devtools/shared/task");
 const {scrollIntoViewIfNeeded} = require("devtools/shared/layout/utils");
 const {PrefObserver} = require("devtools/client/styleeditor/utils");
 const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
 const {template} = require("devtools/shared/gcli/templater");
 const nodeConstants = require("devtools/shared/dom-node-constants");
+const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");
 const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
 
 loader.lazyRequireGetter(this, "CSS", "CSS");
 loader.lazyGetter(this, "AutocompletePopup", () => {
   return require("devtools/client/shared/autocomplete-popup").AutocompletePopup;
 });
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
@@ -627,24 +628,24 @@ MarkupView.prototype = {
 
   /**
    * Create a TreeWalker to find the next/previous
    * node for selection.
    */
   _selectionWalker: function (start) {
     let walker = this.doc.createTreeWalker(
       start || this._elt,
-      Ci.nsIDOMNodeFilter.SHOW_ELEMENT,
+      nodeFilterConstants.SHOW_ELEMENT,
       function (element) {
         if (element.container &&
             element.container.elt === element &&
             element.container.visible) {
-          return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
+          return nodeFilterConstants.FILTER_ACCEPT;
         }
-        return Ci.nsIDOMNodeFilter.FILTER_SKIP;
+        return nodeFilterConstants.FILTER_SKIP;
       }
     );
     walker.currentNode = this._selectedContainer.elt;
     return walker;
   },
 
   _onCopy: function (evt) {
     // Ignore copy events from editors
@@ -3188,16 +3189,17 @@ ElementEditor.prototype = {
       attr.remove();
     }
   },
 
   _createAttribute: function (attribute, before = null) {
     // Create the template editor, which will save some variables here.
     let data = {
       attrName: attribute.name,
+      tabindex: this.container.canFocus ? "0" : "-1",
     };
     this.template("attribute", data);
     let {attr, inner, name, val} = data;
 
     // Double quotes need to be handled specially to prevent DOMParser failing.
     // name="v"a"l"u"e" when editing -> name='v"a"l"u"e"'
     // name="v'a"l'u"e" when editing -> name="v'a&quot;l'u&quot;e"
     let editValueDisplayed = attribute.value || "";
--- a/devtools/client/inspector/markup/markup.xhtml
+++ b/devtools/client/inspector/markup/markup.xhtml
@@ -71,17 +71,17 @@
  --></span>
 
     <span id="template-attribute"
           save="${attr}"
           data-attr="${attrName}"
           data-value="${attrValue}"
           class="attreditor"
           style="display:none"> <!--
-   --><span class="editable" save="${inner}" tabindex="-1"><!--
+   --><span class="editable" save="${inner}" tabindex="${tabindex}"><!--
      --><span save="${name}" class="attr-name theme-fg-color2"></span><!--
      -->=&quot;<!--
      --><span save="${val}" class="attr-value theme-fg-color6"></span><!--
      -->&quot;<!--
    --></span><!--
  --></span>
 
     <span id="template-text" save="${elt}" class="editor text"><!--
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -29,16 +29,17 @@ support-files =
   doc_markup_toggle.html
   doc_markup_tooltip.png
   doc_markup_void_elements.html
   doc_markup_void_elements.xhtml
   doc_markup_xul.xul
   head.js
   helper_attributes_test_runner.js
   helper_events_test_runner.js
+  helper_markup_accessibility_navigation.js
   helper_outerhtml_test_runner.js
   helper_style_attr_test_runner.js
   lib_jquery_1.0.js
   lib_jquery_1.1.js
   lib_jquery_1.2_min.js
   lib_jquery_1.3_min.js
   lib_jquery_1.4_min.js
   lib_jquery_1.6_min.js
@@ -50,16 +51,18 @@ support-files =
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/shared/test/test-actor.js
   !/devtools/client/shared/test/test-actor-registry.js
 
 [browser_markup_accessibility_focus_blur.js]
 skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences
 [browser_markup_accessibility_navigation.js]
 skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences
+[browser_markup_accessibility_navigation_after_edit.js]
+skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences
 [browser_markup_accessibility_semantics.js]
 [browser_markup_anonymous_01.js]
 [browser_markup_anonymous_02.js]
 skip-if = e10s # scratchpad.xul is not loading in e10s window
 [browser_markup_anonymous_03.js]
 [browser_markup_anonymous_04.js]
 [browser_markup_copy_image_data.js]
 subsuite = clipboard
@@ -85,16 +88,17 @@ subsuite = clipboard
 [browser_markup_events_jquery_1.3.js]
 [browser_markup_events_jquery_1.4.js]
 [browser_markup_events_jquery_1.6.js]
 [browser_markup_events_jquery_1.7.js]
 [browser_markup_events_jquery_1.11.1.js]
 [browser_markup_events_jquery_2.1.1.js]
 [browser_markup_events-overflow.js]
 skip-if = true # Bug 1177550
+[browser_markup_events-windowed-host.js]
 [browser_markup_links_01.js]
 [browser_markup_links_02.js]
 [browser_markup_links_03.js]
 [browser_markup_links_04.js]
 subsuite = clipboard
 [browser_markup_links_05.js]
 [browser_markup_links_06.js]
 [browser_markup_links_07.js]
--- a/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.js
+++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation.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/. */
+/* import-globals-from helper_markup_accessibility_navigation.js */
 
 "use strict";
 
-/* global getContainerForSelector, openInspectorForURL */
+// Test keyboard navigation accessibility of inspector's markup view.
 
-// Test keyboard navigation accessibility of inspector's markup view.
+loadHelperScript("helper_markup_accessibility_navigation.js");
 
 /**
  * Test data has the format of:
  * {
  *   desc              {String}   description for better logging
  *   key               {String}   key event's key
  *   options           {?Object}  optional event data such as shiftKey, etc
  *   focused           {String}   path to expected focused element relative to
@@ -232,70 +233,45 @@ const TESTS = [
     focused: "docBody",
     activedescendant: "body.tagLine",
     key: "VK_UP",
     options: { },
     waitFor: "inspector-updated"
   },
 ];
 
+let containerID = 0;
 let elms = {};
-let containerID = 0;
 
 add_task(function* () {
   let { inspector } = yield openInspectorForURL(`data:text/html;charset=utf-8,
     <h1 id="some-id" class="some-class">foo<span>Child span<span></h1>`);
-  let markup = inspector.markup;
-  let doc = markup.doc;
-  let win = doc.defaultView;
 
   // Record containers that are created after inspector is initialized to be
   // useful in testing.
   inspector.on("container-created", memorizeContainer);
   registerCleanupFunction(() => {
     inspector.off("container-created", memorizeContainer);
   });
 
-  elms.docBody = doc.body;
-  elms.root = markup.getContainer(markup._rootNode);
+  elms.docBody = inspector.markup.doc.body;
+  elms.root = inspector.markup.getContainer(inspector.markup._rootNode);
   elms.header = yield getContainerForSelector("h1", inspector);
   elms.body = yield getContainerForSelector("body", inspector);
 
   // Initial focus is on root element and active descendant should be set on
   // body tag line.
-  testNavigationState(doc, elms.docBody, elms.body.tagLine);
+  testNavigationState(inspector, elms, elms.docBody, elms.body.tagLine);
 
   // Focus on the tree element.
   elms.root.elt.focus();
 
-  for (let {desc, waitFor, focused, activedescendant, key, options} of TESTS) {
-    info(desc);
-    let updated;
-    if (waitFor) {
-      updated = waitFor === "inspector-updated" ?
-        inspector.once(waitFor) : markup.once(waitFor);
-    } else {
-      updated = Promise.resolve();
-    }
+  for (let testData of TESTS) {
+    yield runAccessibilityNavigationTest(inspector, elms, testData);
+  }
 
-    EventUtils.synthesizeKey(key, options, win);
-    yield updated;
-    testNavigationState(doc, getElm(focused), getElm(activedescendant));
-  }
+  elms = null;
 });
 
 // Record all containers that are created dynamically into elms object.
 function memorizeContainer(event, container) {
   elms[`container-${containerID++}`] = container;
 }
-
-// Parse and lookup an element from elms object based on dotted path.
-function getElm(path) {
-  let segments = path.split(".");
-  return segments.reduce((prev, current) => prev[current], elms);
-}
-
-function testNavigationState(doc, focused, activedescendant) {
-  let id = activedescendant.getAttribute("id");
-  is(doc.activeElement, focused, `Keyboard focus should be set to ${focused}`);
-  is(elms.root.elt.getAttribute("aria-activedescendant"), id,
-    `Active descendant should be set to ${id}`);
-}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_accessibility_navigation_after_edit.js
@@ -0,0 +1,126 @@
+/* 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/. */
+/* import-globals-from helper_markup_accessibility_navigation.js */
+
+"use strict";
+
+// Test keyboard navigation accessibility is preserved after editing attributes.
+
+loadHelperScript("helper_markup_accessibility_navigation.js");
+
+const TEST_URI = '<div id="some-id" class="some-class"></div>';
+
+/**
+ * Test data has the format of:
+ * {
+ *   desc              {String}   description for better logging
+ *   key               {String}   key event's key
+ *   options           {?Object}  optional event data such as shiftKey, etc
+ *   focused           {String}   path to expected focused element relative to
+ *                                its container
+ *   activedescendant  {String}   path to expected aria-activedescendant element
+ *                                relative to its container
+ *   waitFor           {String}   optional event to wait for if keyboard actions
+ *                                result in asynchronous updates
+ * }
+ */
+const TESTS = [
+  {
+    desc: "Select header container",
+    focused: "root.elt",
+    activedescendant: "div.tagLine",
+    key: "VK_DOWN",
+    options: { },
+    waitFor: "inspector-updated"
+  },
+  {
+    desc: "Focus on header tag",
+    focused: "div.focusableElms.0",
+    activedescendant: "div.tagLine",
+    key: "VK_RETURN",
+    options: { }
+  },
+  {
+    desc: "Activate header tag editor",
+    focused: "div.editor.tag.inplaceEditor.input",
+    activedescendant: "div.tagLine",
+    key: "VK_RETURN",
+    options: { }
+  },
+  {
+    desc: "Activate header id attribute editor",
+    focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input",
+    activedescendant: "div.tagLine",
+    key: "VK_TAB",
+    options: { }
+  },
+  {
+    desc: "Deselect text in header id attribute editor",
+    focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input",
+    activedescendant: "div.tagLine",
+    key: "VK_TAB",
+    options: { }
+  },
+  {
+    desc: "Move the cursor to the left",
+    focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input",
+    activedescendant: "div.tagLine",
+    key: "VK_LEFT",
+    options: { }
+  },
+  {
+    desc: "Modify the attribute",
+    focused: "div.editor.attrList.children.0.children.1.inplaceEditor.input",
+    activedescendant: "div.tagLine",
+    key: "A",
+    options: { }
+  },
+  {
+    desc: "Commit the attribute change",
+    focused: "div.focusableElms.1",
+    activedescendant: "div.tagLine",
+    key: "VK_RETURN",
+    options: { },
+    waitFor: "inspector-updated"
+  },
+  {
+    desc: "Tab and focus on header class attribute",
+    focused: "div.focusableElms.2",
+    activedescendant: "div.tagLine",
+    key: "VK_TAB",
+    options: { }
+  },
+  {
+    desc: "Tab and focus on header new attribute node",
+    focused: "div.focusableElms.3",
+    activedescendant: "div.tagLine",
+    key: "VK_TAB",
+    options: { }
+  },
+];
+
+let elms = {};
+
+add_task(function* () {
+  let url = `data:text/html;charset=utf-8,${TEST_URI}`;
+  let { inspector } = yield openInspectorForURL(url);
+
+  elms.docBody = inspector.markup.doc.body;
+  elms.root = inspector.markup.getContainer(inspector.markup._rootNode);
+  elms.div = yield getContainerForSelector("div", inspector);
+  elms.body = yield getContainerForSelector("body", inspector);
+
+  // Initial focus is on root element and active descendant should be set on
+  // body tag line.
+  testNavigationState(inspector, elms, elms.docBody, elms.body.tagLine);
+
+  // Focus on the tree element.
+  elms.root.elt.focus();
+
+  for (let testData of TESTS) {
+    yield runAccessibilityNavigationTest(inspector, elms, testData);
+  }
+
+  elms = null;
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_events-windowed-host.js
@@ -0,0 +1,61 @@
+/* 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";
+
+/*
+ * Test that the event details tooltip can be hidden by clicking outside of the tooltip
+ * after switching hosts.
+ */
+
+const TEST_URL = URL_ROOT + "doc_markup_events-overflow.html";
+
+registerCleanupFunction(() => {
+  // Restore the default Toolbox host position after the test.
+  Services.prefs.clearUserPref("devtools.toolbox.host");
+});
+
+add_task(function* () {
+  let { inspector, toolbox } = yield openInspectorForURL(TEST_URL);
+  yield runTests(inspector);
+
+  yield toolbox.switchHost("window");
+  yield runTests(inspector);
+
+  yield toolbox.switchHost("bottom");
+  yield runTests(inspector);
+
+  yield toolbox.destroy();
+});
+
+function* runTests(inspector) {
+  let markupContainer = yield getContainerForSelector("#events", inspector);
+  let evHolder = markupContainer.elt.querySelector(".markupview-events");
+  let tooltip = inspector.markup.eventDetailsTooltip;
+
+  info("Clicking to open event tooltip.");
+
+  let onInspectorUpdated = inspector.once("inspector-updated");
+  let onTooltipShown = tooltip.once("shown");
+  EventUtils.synthesizeMouseAtCenter(evHolder, {}, inspector.markup.doc.defaultView);
+
+  yield onTooltipShown;
+  // New node is selected when clicking on the events bubble, wait for inspector-updated.
+  yield onInspectorUpdated;
+
+  ok(tooltip.isVisible(), "EventTooltip visible.");
+
+  onInspectorUpdated = inspector.once("inspector-updated");
+  let onTooltipHidden = tooltip.once("hidden");
+
+  info("Click on another tag to hide the event tooltip");
+  let h1 = yield getContainerForSelector("h1", inspector);
+  let tag = h1.elt.querySelector(".tag");
+  EventUtils.synthesizeMouseAtCenter(tag, {}, inspector.markup.doc.defaultView);
+
+  yield onTooltipHidden;
+  // New node is selected, wait for inspector-updated.
+  yield onInspectorUpdated;
+
+  ok(!tooltip.isVisible(), "EventTooltip hidden.");
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/helper_markup_accessibility_navigation.js
@@ -0,0 +1,70 @@
+/* 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/. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from head.js */
+"use strict";
+
+/**
+ * Execute a keyboard event and check that the state is as expected (focused element, aria
+ * attribute etc...).
+ *
+ * @param {InspectorPanel} inspector
+ *        Current instance of the inspector being tested.
+ * @param {Object} elms
+ *        Map of elements that will be used to retrieve live references to children
+ *        elements
+ * @param {Element} focused
+ *        Element expected to be focused
+ * @param {Element} activedescendant
+ *        Element expected to be the aria activedescendant of the root node
+ */
+function testNavigationState(inspector, elms, focused, activedescendant) {
+  let doc = inspector.markup.doc;
+  let id = activedescendant.getAttribute("id");
+  is(doc.activeElement, focused, `Keyboard focus should be set to ${focused}`);
+  is(elms.root.elt.getAttribute("aria-activedescendant"), id,
+    `Active descendant should be set to ${id}`);
+}
+
+/**
+ * Execute a keyboard event and check that the state is as expected (focused element, aria
+ * attribute etc...).
+ *
+ * @param {InspectorPanel} inspector
+ *        Current instance of the inspector being tested.
+ * @param {Object} elms
+ *        MarkupContainers/Elements that will be used to retrieve references to other
+ *        elements based on objects' paths.
+ * @param {Object} testData
+ *        - {String} desc: description for better logging.
+ *        - {String} key: keyboard event's key.
+ *        - {Object} options, optional: event data such as shiftKey, etc.
+ *        - {String} focused: path to expected focused element in elms map.
+ *        - {String} activedescendant: path to expected aria-activedescendant element in
+ *          elms map.
+ *        - {String} waitFor, optional: markupview event to wait for if keyboard actions
+ *          result in async updates. Also accepts the inspector event "inspector-updated".
+ */
+function* runAccessibilityNavigationTest(inspector, elms,
+  {desc, key, options, focused, activedescendant, waitFor}) {
+  info(desc);
+
+  let markup = inspector.markup;
+  let doc = markup.doc;
+  let win = doc.defaultView;
+
+  let updated;
+  if (waitFor) {
+    updated = waitFor === "inspector-updated" ?
+      inspector.once(waitFor) : markup.once(waitFor);
+  } else {
+    updated = Promise.resolve();
+  }
+  EventUtils.synthesizeKey(key, options, win);
+  yield updated;
+
+  let focusedElement = lookupPath(elms, focused);
+  let activeDescendantElement = lookupPath(elms, activedescendant);
+  testNavigationState(inspector, elms, focusedElement, activeDescendantElement);
+}
--- a/devtools/client/inspector/rules/models/rule.js
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -1,29 +1,25 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set 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";
 
-const {Cc, Ci} = require("chrome");
+const {Ci} = require("chrome");
 const promise = require("promise");
 const CssLogic = require("devtools/shared/inspector/css-logic");
 const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
 const {TextProperty} =
       require("devtools/client/inspector/rules/models/text-property");
 const {promiseWarn} = require("devtools/client/inspector/shared/utils");
 const {parseDeclarations} = require("devtools/shared/css-parsing-utils");
-const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "osString", function () {
-  return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
-});
+const Services = require("Services");
 
 /**
  * Rule is responsible for the following:
  *   Manages a single style declaration or rule.
  *   Applies changes to the properties in a rule.
  *   Maintains a list of TextProperty objects.
  *
  * @param {ElementStyle} elementStyle
@@ -655,17 +651,17 @@ Rule.prototype = {
   },
 
   /**
    * Return a string representation of the rule.
    */
   stringifyRule: function () {
     let selectorText = this.selectorText;
     let cssText = "";
-    let terminator = osString === "WINNT" ? "\r\n" : "\n";
+    let terminator = Services.appinfo.OS === "WINNT" ? "\r\n" : "\n";
 
     for (let textProp of this.textProps) {
       if (!textProp.invisible) {
         cssText += "\t" + textProp.stringifyProperty() + terminator;
       }
     }
 
     return selectorText + " {" + terminator + cssText + "}";
--- a/devtools/client/inspector/rules/test/browser_rules_copy_styles.js
+++ b/devtools/client/inspector/rules/test/browser_rules_copy_styles.js
@@ -4,19 +4,17 @@
 
 "use strict";
 
 /**
  * Tests the behaviour of the copy styles context menu items in the rule
  * view.
  */
 
-XPCOMUtils.defineLazyGetter(this, "osString", function () {
-  return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
-});
+const osString = Services.appinfo.OS;
 
 const TEST_URI = URL_ROOT + "doc_copystyles.html";
 
 add_task(function* () {
   yield addTab(TEST_URI);
   let { inspector, view } = yield openRuleView();
   yield selectNode("#testid", inspector);
 
--- a/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
+++ b/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
@@ -1,19 +1,17 @@
 /* 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";
 
 // Tests that properties can be selected and copied from the rule view
 
-XPCOMUtils.defineLazyGetter(this, "osString", function () {
-  return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
-});
+const osString = Services.appinfo.OS;
 
 const TEST_URI = `
   <style type="text/css">
     html {
       color: #000000;
     }
     span {
       font-variant: small-caps; color: #000000;
--- a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
@@ -19,17 +19,17 @@ add_task(function* () {
   yield testView("ruleview", inspector);
   yield testView("computedview", inspector);
 });
 
 function* testView(viewId, inspector) {
   info("Testing " + viewId);
 
   yield inspector.sidebar.select(viewId);
-  let view = inspector[viewId].view;
+  let view = inspector[viewId].view || inspector[viewId].computedView;
   yield selectNode("div", inspector);
 
   testIsColorValueNode(view);
   testIsColorPopupOnAllNodes(view);
   yield clearCurrentNodeSelection(inspector);
 }
 
 /**
--- a/devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js
@@ -15,17 +15,17 @@ add_task(function* () {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
 
   yield selectNode("#one", inspector);
 
   is(getRuleViewPropertyValue(view, "element", "color"), "red",
     "The rule-view shows the properties for test node one");
 
-  let cView = inspector.computedview.view;
+  let cView = inspector.computedview.computedView;
   let prop = getComputedViewProperty(cView, "color");
   ok(!prop, "The computed-view doesn't show the properties for test node one");
 
   info("Switching to the computed-view");
   let onComputedViewReady = inspector.once("computed-view-refreshed");
   selectComputedView(inspector);
   yield onComputedViewReady;
 
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -292,17 +292,17 @@ function openRuleView() {
  * view is visible and ready
  */
 function openComputedView() {
   return openInspectorSidebarTab("computedview").then(data => {
     return {
       toolbox: data.toolbox,
       inspector: data.inspector,
       testActor: data.testActor,
-      view: data.inspector.computedview.view
+      view: data.inspector.computedview.computedView
     };
   });
 }
 
 /**
  * Select the rule view sidebar tab on an already opened inspector panel.
  *
  * @param {InspectorPanel} inspector
@@ -318,17 +318,17 @@ function selectRuleView(inspector) {
  * Select the computed view sidebar tab on an already opened inspector panel.
  *
  * @param {InspectorPanel} inspector
  *        The opened inspector panel
  * @return {CssComputedView} the computed view
  */
 function selectComputedView(inspector) {
   inspector.sidebar.select("computedview");
-  return inspector.computedview.view;
+  return inspector.computedview.computedView;
 }
 
 /**
  * Get the NodeFront for a node that matches a given css selector, via the
  * protocol.
  * @param {String|NodeFront} selector
  * @param {InspectorPanel} inspector The instance of InspectorPanel currently
  * loaded in the toolbox
--- a/devtools/client/locales/en-US/layoutview.dtd
+++ b/devtools/client/locales/en-US/layoutview.dtd
@@ -11,16 +11,17 @@
   - You want to make that choice consistent across the developer tools.
   - A good criteria is the language in which you'd find the best
   - documentation on web development on the web. -->
 
 <!-- LOCALIZATION NOTE (*.tooltip): These tooltips are not regular tooltips.
   -  The text appears on the bottom right corner of the layout view when
   -  the corresponding box is hovered. -->
 
+<!ENTITY layoutViewTitle          "Box Model">
 <!ENTITY margin.tooltip           "margin">
 <!ENTITY border.tooltip           "border">
 <!ENTITY padding.tooltip          "padding">
 <!ENTITY content.tooltip          "content">
 
 <!-- LOCALIZATION NOTE: This label is displayed as a tooltip that appears when
   -  hovering over the button that allows users to edit the position of an
   -  element in the page. -->
--- a/devtools/client/netmonitor/har/har-builder.js
+++ b/devtools/client/netmonitor/har/har-builder.js
@@ -1,23 +1,20 @@
 /* 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 { Ci, Cc } = require("chrome");
 const { defer, all } = require("promise");
 const { LocalizationHelper } = require("devtools/client/shared/l10n");
+const Services = require("Services");
+const appInfo = Services.appinfo;
 
 loader.lazyRequireGetter(this, "NetworkHelper", "devtools/shared/webconsole/network-helper");
 
-loader.lazyGetter(this, "appInfo", () => {
-  return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
-});
-
 loader.lazyGetter(this, "L10N", () => {
   return new LocalizationHelper("chrome://devtools/locale/har.properties");
 });
 
 const HAR_VERSION = "1.1";
 
 /**
  * This object is responsible for building HAR file. See HAR spec:
--- a/devtools/client/performance/test/browser.ini
+++ b/devtools/client/performance/test/browser.ini
@@ -97,16 +97,17 @@ support-files =
 [browser_perf-telemetry-02.js]
 [browser_perf-telemetry-03.js]
 [browser_perf-telemetry-04.js]
 # [browser_perf-theme-toggle.js] TODO bug 1256350
 [browser_perf-tree-abstract-01.js]
 [browser_perf-tree-abstract-02.js]
 [browser_perf-tree-abstract-03.js]
 [browser_perf-tree-abstract-04.js]
+[browser_perf-tree-abstract-05.js]
 [browser_perf-tree-view-01.js]
 [browser_perf-tree-view-02.js]
 [browser_perf-tree-view-03.js]
 [browser_perf-tree-view-04.js]
 [browser_perf-tree-view-05.js]
 [browser_perf-tree-view-06.js]
 [browser_perf-tree-view-07.js]
 [browser_perf-tree-view-08.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-05.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the abstract tree base class for the profiler's tree view
+ * supports PageUp/PageDown/Home/End keys.
+ */
+
+const { appendAndWaitForPaint } = require("devtools/client/performance/test/helpers/dom-utils");
+const { synthesizeCustomTreeClass } = require("devtools/client/performance/test/helpers/synth-utils");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+  let { MyCustomTreeItem } = synthesizeCustomTreeClass();
+
+  let container = document.createElement("vbox");
+  container.style.height = '100%';
+  container.style.overflow = 'scroll';
+  yield appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+  let myDataSrc = {
+    label: "root",
+    children: []
+  };
+
+  for (let i = 0; i < 1000; i++) {
+    myDataSrc.children.push({
+      label: "child-" + i,
+      children: []
+    });
+  }
+
+  let treeRoot = new MyCustomTreeItem(myDataSrc, { parent: null });
+  treeRoot.attachTo(container);
+  treeRoot.focus();
+  treeRoot.expand();
+
+  is(document.commandDispatcher.focusedElement, treeRoot.target,
+    "The root node is focused.");
+
+  // Test HOME and END
+
+  key("VK_END");
+  is(document.commandDispatcher.focusedElement,
+    treeRoot.getChild(myDataSrc.children.length - 1).target,
+    "The last node is focused.");
+
+  key("VK_HOME");
+  is(document.commandDispatcher.focusedElement, treeRoot.target,
+    "The first (root) node is focused.");
+
+  // Test PageUp and PageDown
+
+  let nodesPerPageSize = treeRoot._getNodesPerPageSize();
+
+  key("VK_PAGE_DOWN");
+  is(document.commandDispatcher.focusedElement,
+    treeRoot.getChild(nodesPerPageSize - 1).target,
+    "The first node in the second page is focused.");
+
+  key("VK_PAGE_DOWN");
+  is(document.commandDispatcher.focusedElement,
+    treeRoot.getChild(nodesPerPageSize * 2 - 1).target,
+    "The first node in the third page is focused.");
+
+  key("VK_PAGE_UP");
+  is(document.commandDispatcher.focusedElement,
+    treeRoot.getChild(nodesPerPageSize - 1).target,
+    "The first node in the second page is focused.");
+
+  key("VK_PAGE_UP");
+  is(document.commandDispatcher.focusedElement, treeRoot.target,
+    "The first (root) node is focused.");
+
+  // Test PageUp in the middle of the first page
+
+  let middleIndex = Math.floor(nodesPerPageSize / 2);
+
+  treeRoot.getChild(middleIndex).target.focus();
+  is(document.commandDispatcher.focusedElement,
+    treeRoot.getChild(middleIndex).target,
+    "The middle node in the first page is focused.");
+
+  key("VK_PAGE_UP");
+  is(document.commandDispatcher.focusedElement, treeRoot.target,
+    "The first (root) node is focused.");
+
+  // Test PageDown in the middle of the last page
+
+  middleIndex = Math.ceil(myDataSrc.children.length - middleIndex);
+
+  treeRoot.getChild(middleIndex).target.focus();
+  is(document.commandDispatcher.focusedElement,
+    treeRoot.getChild(middleIndex).target,
+    "The middle node in the last page is focused.");
+
+  key("VK_PAGE_DOWN");
+  is(document.commandDispatcher.focusedElement,
+    treeRoot.getChild(myDataSrc.children.length - 1).target,
+    "The last node is focused.");
+
+  container.remove();
+});
--- a/devtools/client/responsivedesign/test/head.js
+++ b/devtools/client/responsivedesign/test/head.js
@@ -142,17 +142,17 @@ var openInspectorSideBar = Task.async(fu
   let {toolbox, inspector} = yield openInspector();
 
   info("Selecting the " + id + " sidebar");
   inspector.sidebar.select(id);
 
   return {
     toolbox: toolbox,
     inspector: inspector,
-    view: inspector[id].view
+    view: inspector[id].view || inspector[id].computedView
   };
 });
 
 /**
  * Checks whether the inspector's sidebar corresponding to the given id already
  * exists
  * @param {InspectorPanel}
  * @param {String}
--- a/devtools/client/shared/DOMHelpers.jsm
+++ b/devtools/client/shared/DOMHelpers.jsm
@@ -2,16 +2,18 @@
  * 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 Ci = Components.interfaces;
 const Cu = Components.utils;
 const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");
 
 this.EXPORTED_SYMBOLS = ["DOMHelpers"];
 
 /**
  * DOMHelpers
  * Makes DOM traversal easier. Goes through iframes.
  *
  * @constructor
@@ -98,17 +100,17 @@ DOMHelpers.prototype = {
         return child;
     }
 
     return null;  // we have no children worth showing.
   },
 
   getFirstChild: function Helpers_getFirstChild(node)
   {
-    let SHOW_ALL = Components.interfaces.nsIDOMNodeFilter.SHOW_ALL;
+    let SHOW_ALL = nodeFilterConstants.SHOW_ALL;
     this.treeWalker = node.ownerDocument.createTreeWalker(node,
       SHOW_ALL, null);
     return this.treeWalker.firstChild();
   },
 
   getNextSibling: function Helpers_getNextSibling(node)
   {
     let next = this.treeWalker.nextSibling();
--- a/devtools/client/shared/components/reps/array.js
+++ b/devtools/client/shared/components/reps/array.js
@@ -55,17 +55,17 @@ define(function (require, exports, modul
           items.push(ItemRep({
             object: exc,
             delim: delim,
             key: i
           }));
         }
       }
 
-      if (array.length > max + 1) {
+      if (array.length > max) {
         items.pop();
 
         let objectLink = this.props.objectLink || DOM.span;
         items.push(Caption({
           key: "more",
           object: objectLink({
             object: this.props.object
           }, "more…")
--- a/devtools/client/shared/components/reps/grip-array.js
+++ b/devtools/client/shared/components/reps/grip-array.js
@@ -82,17 +82,17 @@ define(function (require, exports, modul
           items.push(GripArrayItem(Object.assign({}, this.props, {
             object: exc,
             delim: delim,
             key: i}
           )));
         }
       }
 
-      if (array.length > max + 1) {
+      if (array.length > max) {
         items.pop();
         let objectLink = this.props.objectLink || span;
         items.push(Caption({
           key: "more",
           object: objectLink({
             object: this.props.object
           }, "more…")
         }));
--- a/devtools/client/shared/components/test/mochitest/test_reps_array.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_array.html
@@ -14,23 +14,28 @@ Test ArrayRep rep
 <pre id="test">
 <script src="head.js" type="application/javascript;version=1.8"></script>
 <script type="application/javascript;version=1.8">
 window.onload = Task.async(function* () {
   let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
   let { ArrayRep } = browserRequire("devtools/client/shared/components/reps/array");
 
   let componentUnderTest = ArrayRep;
+  const maxLength = {
+    short: 3,
+    long: 300
+  };
 
   try {
     yield testBasic();
 
     // Test property iterator
     yield testMaxProps();
-    yield testMoreThanMaxProps();
+    yield testMoreThanShortMaxProps();
+    yield testMoreThanLongMaxProps();
     yield testRecursiveArray();
 
     // Test that properties are rendered as expected by ItemRep
     yield testNested();
   } catch(e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
@@ -88,36 +93,63 @@ window.onload = Task.async(function* () 
         mode: "long",
         expectedOutput: defaultOutput,
       }
     ];
 
     testRepRenderModes(modeTests, "testMaxProps", componentUnderTest, stub);
   }
 
-  function testMoreThanMaxProps() {
-    const stub = Array(302).fill("foo");
-    const defaultOutput = `["foo", "foo", "foo", more…]`;
+  function testMoreThanShortMaxProps() {
+    const stub = Array(maxLength.short + 1).fill("foo");
+    const defaultShortOutput = `[${Array(maxLength.short).fill("\"foo\"").join(", ")}, more…]`;
 
     const modeTests = [
       {
         mode: undefined,
-        expectedOutput: defaultOutput,
+        expectedOutput: defaultShortOutput,
       },
       {
         mode: "tiny",
-        expectedOutput: `[302]`,
+        expectedOutput: `[${maxLength.short + 1}]`,
       },
       {
         mode: "short",
-        expectedOutput: defaultOutput,
+        expectedOutput: defaultShortOutput,
       },
       {
         mode: "long",
-        expectedOutput: `[${Array(300).fill("\"foo\"").join(", ")}, more…]`,
+        expectedOutput: `[${Array(maxLength.short + 1).fill("\"foo\"").join(", ")}]`,
+      }
+    ];
+
+    testRepRenderModes(modeTests, "testMoreThanMaxProps", componentUnderTest, stub);
+  }
+
+  function testMoreThanLongMaxProps() {
+    const stub = Array(maxLength.long + 1).fill("foo");
+    const defaultShortOutput = `[${Array(maxLength.short).fill("\"foo\"").join(", ")}, more…]`;
+    const defaultLongOutput = `[${Array(maxLength.long).fill("\"foo\"").join(", ")}, more…]`;
+
+    const modeTests = [
+      {
+        mode: undefined,
+        expectedOutput: defaultShortOutput,
+      },
+      {
+        mode: "tiny",
+        expectedOutput: `[${maxLength.long + 1}]`,
+      },
+      {
+        mode: "short",
+        expectedOutput: defaultShortOutput,
+      },
+      {
+        mode: "long",
+        expectedOutput: defaultLongOutput,
       }
     ];
 
     testRepRenderModes(modeTests, "testMoreThanMaxProps", componentUnderTest, stub);
   }
 
   function testRecursiveArray() {
     let stub = [1];
--- a/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
@@ -14,23 +14,28 @@ Test GripArray rep
 <pre id="test">
 <script src="head.js" type="application/javascript;version=1.8"></script>
 <script type="application/javascript;version=1.8">
 window.onload = Task.async(function* () {
   let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
   let { GripArray } = browserRequire("devtools/client/shared/components/reps/grip-array");
 
   let componentUnderTest = GripArray;
+  const maxLength = {
+    short: 3,
+    long: 300
+  };
 
   try {
     yield testBasic();
 
     // Test property iterator
     yield testMaxProps();
-    yield testMoreThanMaxProps();
+    yield testMoreThanShortMaxProps();
+    yield testMoreThanLongMaxProps();
     yield testRecursiveArray();
   } catch(e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testBasic() {
@@ -90,38 +95,67 @@ window.onload = Task.async(function* () 
         mode: "long",
         expectedOutput: defaultOutput,
       }
     ];
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
-  function testMoreThanMaxProps() {
-    // Test array = `["test string"…] //301 items`
-    const testName = "testMoreThanMaxProps";
+  function testMoreThanShortMaxProps() {
+    // Test array = `["test string"…] //4 items`
+    const testName = "testMoreThanShortMaxProps";
 
-    const defaultOutput = `[${Array(3).fill("\"test string\"").join(", ")}, more…]`;
+    const defaultOutput = `[${Array(maxLength.short).fill("\"test string\"").join(", ")}, more…]`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
       },
       {
         mode: "tiny",
-        expectedOutput: `[302]`,
+        expectedOutput: `[${maxLength.short + 1}]`,
       },
       {
         mode: "short",
         expectedOutput: defaultOutput,
       },
       {
         mode: "long",
-        expectedOutput: `[${Array(300).fill("\"test string\"").join(", ")}, more…]`,
+        expectedOutput: `[${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 modeTests = [
+      {
+        mode: undefined,
+        expectedOutput: defaultShortOutput,
+      },
+      {
+        mode: "tiny",
+        expectedOutput: `[${maxLength.long + 1}]`,
+      },
+      {
+        mode: "short",
+        expectedOutput: defaultShortOutput,
+      },
+      {
+        mode: "long",
+        expectedOutput: defaultLongOutput
       }
     ];
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
   function testRecursiveArray() {
     // @TODO This is not how this feature should actually work
@@ -195,39 +229,63 @@ window.onload = Task.async(function* () 
                 "frozen": false,
                 "sealed": false,
                 "ownPropertyLength": 0
               }
             ]
           }
         };
 
-      case "testMoreThanMaxProps":
-        let grip = {
+      case "testMoreThanShortMaxProps":
+        let shortArrayGrip = {
           "type": "object",
           "class": "Array",
           "actor": "server1.conn1.obj35",
           "extensible": true,
           "frozen": false,
           "sealed": false,
           "ownPropertyLength": 4,
           "preview": {
             "kind": "ArrayLike",
-            "length": 302,
+            "length": maxLength.short + 1,
             "items": []
           }
         };
 
-        // Generate 101 properties, which is more that the maximum
-        // limit in case of the 'long' mode.
-        for (let i = 0; i < 302; i++) {
-          grip.preview.items.push("test string");
+        // Generate array grip with length 4, which is more that the maximum
+        // limit in case of the 'short' mode.
+        for (let i = 0; i < maxLength.short + 1; i++) {
+          shortArrayGrip.preview.items.push("test string");
         }
 
-        return grip;
+        return shortArrayGrip;
+
+      case "testMoreThanLongMaxProps":
+        let longArrayGrip = {
+          "type": "object",
+          "class": "Array",
+          "actor": "server1.conn1.obj35",
+          "extensible": true,
+          "frozen": false,
+          "sealed": false,
+          "ownPropertyLength": 4,
+          "preview": {
+            "kind": "ArrayLike",
+            "length": maxLength.long + 1,
+            "items": []
+          }
+        };
+
+        // Generate array grip with length 301, which is more that the maximum
+        // limit in case of the 'long' mode.
+        for (let i = 0; i < maxLength.long + 1; i++) {
+          longArrayGrip.preview.items.push("test string");
+        }
+
+        return longArrayGrip;
 
       case "testRecursiveArray":
         return {
           "type": "object",
           "class": "Array",
           "actor": "server1.conn3.obj42",
           "extensible": true,
           "frozen": false,
--- a/devtools/client/shared/developer-toolbar.js
+++ b/devtools/client/shared/developer-toolbar.js
@@ -218,25 +218,20 @@ exports.CommandUtils = CommandUtils;
 /**
  * Due to a number of panel bugs we need a way to check if we are running on
  * Linux. See the comments for TooltipPanel and OutputPanel for further details.
  *
  * When bug 780102 is fixed all isLinux checks can be removed and we can revert
  * to using panels.
  */
 loader.lazyGetter(this, "isLinux", function () {
-  return OS == "Linux";
+  return Services.appinfo.OS == "Linux";
 });
 loader.lazyGetter(this, "isMac", function () {
-  return OS == "Darwin";
-});
-
-loader.lazyGetter(this, "OS", function () {
-  let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
-  return os;
+  return Services.appinfo.OS == "Darwin";
 });
 
 /**
  * A component to manage the global developer toolbar, which contains a GCLI
  * and buttons for various developer tools.
  * @param aChromeWindow The browser window to which this toolbar is attached
  */
 function DeveloperToolbar(aChromeWindow)
@@ -1005,17 +1000,17 @@ OutputPanel.prototype._resize = function
   // Set max panel width to match any content with a max of the width of the
   // browser window.
   let maxWidth = this._panel.ownerDocument.documentElement.clientWidth;
 
   // Adjust max width according to OS.
   // We'd like to put this in CSS but we can't:
   //   body { width: calc(min(-5px, max-content)); }
   //   #_panel { max-width: -5px; }
-  switch (OS) {
+  switch (Services.appinfo.OS) {
     case "Linux":
       maxWidth -= 5;
       break;
     case "Darwin":
       maxWidth -= 25;
       break;
     case "WINNT":
       maxWidth -= 5;
--- a/devtools/client/shared/shim/Services.js
+++ b/devtools/client/shared/shim/Services.js
@@ -460,16 +460,52 @@ PrefBranch.prototype = {
 
 const Services = {
   /**
    * An implementation of nsIPrefService that is based on local
    * storage.  Only the subset of nsIPrefService that is actually used
    * by devtools is implemented here.
    */
   prefs: new PrefBranch(null, "", ""),
+
+  /**
+   * An implementation of Services.appinfo that holds just the
+   * properties needed by devtools.
+   */
+  appinfo: {
+    get OS() {
+      const os = window.navigator.userAgent;
+      if (os) {
+        if (os.includes("Linux")) {
+          return "Linux";
+        } else if (os.includes("Windows")) {
+          return "WINNT";
+        } else if (os.includes("Mac")) {
+          return "Darwin";
+        }
+      }
+      return "Unknown";
+    },
+
+    // It's fine for this to be an approximation.
+    get name() {
+      return window.navigator.userAgent;
+    },
+
+    // It's fine for this to be an approximation.
+    get version() {
+      return window.navigator.appVersion;
+    },
+
+    // This is only used by telemetry, which is disabled for the
+    // content case.  So, being totally wrong is ok.
+    get is64Bit() {
+      return true;
+    },
+  },
 };
 
 /**
  * Create a new preference.  This is used during startup (see
  * devtools/client/preferences/devtools.js) to install the
  * default preferences.
  *
  * @param {String} name the name of the preference
--- a/devtools/client/shared/shim/test/mochitest.ini
+++ b/devtools/client/shared/shim/test/mochitest.ini
@@ -1,5 +1,6 @@
 [DEFAULT]
 support-files =
   prefs-wrapper.js
 
+[test_service_appinfo.html]
 [test_service_prefs.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/shim/test/test_service_appinfo.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1265802
+-->
+<head>
+  <title>Test for Bug 1265802 - replace Services.appinfo</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/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">
+  "use strict";
+  var exports = {};
+  </script>
+
+  <script type="application/javascript;version=1.8"
+	  src="resource://devtools/client/shared/shim/Services.js"></script>
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+is(Services.appinfo.OS, SpecialPowers.Services.appinfo.OS,
+   "check that Services.appinfo.OS shim matches platform");
+</script>
+</body>
--- a/devtools/client/shared/telemetry.js
+++ b/devtools/client/shared/telemetry.js
@@ -51,19 +51,17 @@ this.Telemetry = function () {
   this.logOncePerBrowserVersion = this.logOncePerBrowserVersion.bind(this);
   this.destroy = this.destroy.bind(this);
 
   this._timers = new Map();
 };
 
 module.exports = Telemetry;
 
-var {Cc, Ci, Cu} = require("chrome");
 var Services = require("Services");
-var {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
 
 Telemetry.prototype = {
   _histograms: {
     toolbox: {
       histogram: "DEVTOOLS_TOOLBOX_OPENED_COUNT",
       userHistogram: "DEVTOOLS_TOOLBOX_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS"
     },
@@ -92,21 +90,16 @@ Telemetry.prototype = {
       userHistogram: "DEVTOOLS_RULEVIEW_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_RULEVIEW_TIME_ACTIVE_SECONDS"
     },
     computedview: {
       histogram: "DEVTOOLS_COMPUTEDVIEW_OPENED_COUNT",
       userHistogram: "DEVTOOLS_COMPUTEDVIEW_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_COMPUTEDVIEW_TIME_ACTIVE_SECONDS"
     },
-    layoutview: {
-      histogram: "DEVTOOLS_LAYOUTVIEW_OPENED_COUNT",
-      userHistogram: "DEVTOOLS_LAYOUTVIEW_OPENED_PER_USER_FLAG",
-      timerHistogram: "DEVTOOLS_LAYOUTVIEW_TIME_ACTIVE_SECONDS"
-    },
     fontinspector: {
       histogram: "DEVTOOLS_FONTINSPECTOR_OPENED_COUNT",
       userHistogram: "DEVTOOLS_FONTINSPECTOR_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS"
     },
     animationinspector: {
       histogram: "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_COUNT",
       userHistogram: "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_PER_USER_FLAG",
@@ -357,17 +350,17 @@ Telemetry.prototype = {
   /**
    * Log info about usage once per browser version. This allows us to discover
    * how many individual users are using our tools for each browser version.
    *
    * @param  {String} perUserHistogram
    *         Histogram in which the data is to be stored.
    */
   logOncePerBrowserVersion: function (perUserHistogram, value) {
-    let currentVersion = appInfo.version;
+    let currentVersion = Services.appinfo.version;
     let latest = Services.prefs.getCharPref(TOOLS_OPENED_PREF);
     let latestObj = JSON.parse(latest);
 
     let lastVersionHistogramUpdated = latestObj[perUserHistogram];
 
     if (typeof lastVersionHistogramUpdated == "undefined" ||
         lastVersionHistogramUpdated !== currentVersion) {
       latestObj[perUserHistogram] = currentVersion;
@@ -378,12 +371,8 @@ Telemetry.prototype = {
   },
 
   destroy: function () {
     for (let histogramId of this._timers.keys()) {
       this.stopTimer(histogramId);
     }
   }
 };
-
-XPCOMUtils.defineLazyGetter(this, "appInfo", function () {
-  return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
-});
--- a/devtools/client/shared/test/browser_telemetry_sidebar.js
+++ b/devtools/client/shared/test/browser_telemetry_sidebar.js
@@ -23,17 +23,17 @@ add_task(function* () {
   gBrowser.removeCurrentTab();
 });
 
 function* testSidebar(toolbox) {
   info("Testing sidebar");
 
   let inspector = toolbox.getCurrentPanel();
   let sidebarTools = ["ruleview", "computedview", "fontinspector",
-                      "layoutview", "animationinspector"];
+                      "animationinspector"];
 
   // Concatenate the array with itself so that we can open each tool twice.
   sidebarTools.push.apply(sidebarTools, sidebarTools);
 
   return new Promise(resolve => {
     // See TOOL_DELAY for why we need setTimeout here
     setTimeout(function selectSidebarTab() {
       let tool = sidebarTools.pop();
--- a/devtools/client/shared/widgets/AbstractTreeItem.jsm
+++ b/devtools/client/shared/widgets/AbstractTreeItem.jsm
@@ -473,17 +473,58 @@ AbstractTreeItem.prototype = {
    * @param number delta
    *        The offset from this item to the target item.
    * @return nsIDOMNode
    *         The element displaying the target item at the specified offset.
    */
   _getSiblingAtDelta: function (delta) {
     let childNodes = this._containerNode.childNodes;
     let indexOfSelf = Array.indexOf(childNodes, this._targetNode);
-    return childNodes[indexOfSelf + delta];
+    if (indexOfSelf + delta >= 0) {
+      return childNodes[indexOfSelf + delta];
+    }
+    return undefined;
+  },
+
+  _getNodesPerPageSize: function() {
+    let childNodes = this._containerNode.childNodes;
+    let nodeHeight = this._getHeight(childNodes[childNodes.length - 1]);
+    let containerHeight = this.bounds.height;
+    return Math.ceil(containerHeight / nodeHeight);
+  },
+
+  _getHeight: function(elem) {
+    let win = this.document.defaultView;
+    let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIDOMWindowUtils);
+    return utils.getBoundsWithoutFlushing(elem).height;
+  },
+
+  /**
+   * Focuses the first item in this tree.
+   */
+  _focusFirstNode: function () {
+    let childNodes = this._containerNode.childNodes;
+    // The root node of the tree may be hidden in practice, so uses for-loop
+    // here to find the next visible node.
+    for (let i = 0; i < childNodes.length; i++) {
+      // The height will be 0 if an element is invisible.
+      if (this._getHeight(childNodes[i])) {
+        childNodes[i].focus();
+        return;
+      }
+    }
+  },
+
+  /**
+   * Focuses the last item in this tree.
+   */
+  _focusLastNode: function () {
+    let childNodes = this._containerNode.childNodes;
+    childNodes[childNodes.length - 1].focus();
   },
 
   /**
    * Focuses the next item in this tree.
    */
   _focusNextNode: function () {
     let nextElement = this._getSiblingAtDelta(1);
     if (nextElement) nextElement.focus(); // nsIDOMNode
@@ -565,16 +606,46 @@ AbstractTreeItem.prototype = {
 
       case e.DOM_VK_RIGHT:
         if (!this._expanded) {
           this.expand();
         } else {
           this._focusNextNode();
         }
         return;
+
+      case e.DOM_VK_PAGE_UP:
+        let pageUpElement =
+          this._getSiblingAtDelta(-this._getNodesPerPageSize());
+        // There's a chance that the root node is hidden. In this case, its
+        // height will be 0.
+        if (pageUpElement && this._getHeight(pageUpElement)) {
+          pageUpElement.focus();
+        } else {
+          this._focusFirstNode();
+        }
+        return;
+
+      case e.DOM_VK_PAGE_DOWN:
+        let pageDownElement =
+          this._getSiblingAtDelta(this._getNodesPerPageSize());
+        if (pageDownElement) {
+          pageDownElement.focus();
+        } else {
+          this._focusLastNode();
+        }
+        return;
+
+      case e.DOM_VK_HOME:
+        this._focusFirstNode();
+        return;
+
+      case e.DOM_VK_END:
+        this._focusLastNode();
+        return;
     }
   },
 
   /**
    * Handler for the "focus" event on the element displaying this tree item.
    */
   _onFocus: function (e) {
     this._rootItem.emit("focus", this);
--- a/devtools/client/shared/widgets/FlameGraph.js
+++ b/devtools/client/shared/widgets/FlameGraph.js
@@ -922,17 +922,19 @@ FlameGraph.prototype = {
   },
 
   /**
    * Listener for the "keydown" event on the graph's container.
    */
   _onKeyDown: function (e) {
     ViewHelpers.preventScrolling(e);
 
-    if (!this._keysPressed[e.keyCode]) {
+    const hasModifier = e.ctrlKey || e.shiftKey || e.altKey || e.metaKey;
+
+    if (!hasModifier && !this._keysPressed[e.keyCode]) {
       this._keysPressed[e.keyCode] = true;
       this._userInputStack++;
       this._shouldRedraw = true;
     }
   },
 
   /**
    * Listener for the "keyup" event on the graph's container.
--- a/devtools/client/shared/widgets/HTMLTooltip.js
+++ b/devtools/client/shared/widgets/HTMLTooltip.js
@@ -217,20 +217,21 @@ function HTMLTooltip(toolbox, {
   EventEmitter.decorate(this);
 
   this.doc = toolbox.doc;
   this.type = type;
   this.autofocus = autofocus;
   this.consumeOutsideClicks = consumeOutsideClicks;
   this.useXulWrapper = this._isXUL() && useXulWrapper;
 
-  this._position = null;
+  // The top window is used to attach click event listeners to close the tooltip if the
+  // user clicks on the content page.
+  this.topWindow = this._getTopWindow();
 
-  // Use the topmost window to listen for click events to close the tooltip
-  this.topWindow = this.doc.defaultView.top;
+  this._position = null;
 
   this._onClick = this._onClick.bind(this);
 
   this._toggle = new TooltipToggle(this);
   this.startTogglingOnHover = this._toggle.start.bind(this._toggle);
   this.stopTogglingOnHover = this._toggle.stop.bind(this._toggle);
 
   this.container = this._createContainer();
@@ -377,16 +378,18 @@ HTMLTooltip.prototype = {
     this.container.classList.add("tooltip-visible");
 
     // Keep a pointer on the focused element to refocus it when hiding the tooltip.
     this._focusedElement = this.doc.activeElement;
 
     this.doc.defaultView.clearTimeout(this.attachEventsTimer);
     this.attachEventsTimer = this.doc.defaultView.setTimeout(() => {
       this._maybeFocusTooltip();
+      // Updated the top window reference each time in case the host changes.
+      this.topWindow = this._getTopWindow();
       this.topWindow.addEventListener("click", this._onClick, true);
       this.emit("shown");
     }, 0);
   }),
 
   /**
    * Calculate the rect of the viewport that limits the tooltip dimensions. When using a
    * XUL panel wrapper, the viewport will be able to use the whole screen (excluding space
@@ -545,16 +548,20 @@ HTMLTooltip.prototype = {
     // http://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus .
     let focusableSelector = "a, button, iframe, input, select, textarea";
     let focusableElement = this.panel.querySelector(focusableSelector);
     if (this.autofocus && focusableElement) {
       focusableElement.focus();
     }
   },
 
+  _getTopWindow: function () {
+    return this.doc.defaultView.top;
+  },
+
   /**
    * Check if the tooltip's owner document is a XUL document.
    */
   _isXUL: function () {
     return this.doc.documentElement.namespaceURI === XUL_NS;
   },
 
   _createXulPanelWrapper: function () {
--- a/devtools/client/themes/computed.css
+++ b/devtools/client/themes/computed.css
@@ -27,21 +27,27 @@
   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 {
@@ -54,17 +60,19 @@
   flex-wrap: wrap;
 }
 
 .property-name-container {
   width: 202px;
 }
 
 .property-value-container {
-  width: 168px;
+  display: flex;
+  flex: 1 1 168px;
+  overflow: hidden;
 }
 
 .property-name-container > *,
 .property-value-container > * {
   display: inline-block;
   vertical-align: middle;
 }
 
--- a/devtools/client/themes/firebug-theme.css
+++ b/devtools/client/themes/firebug-theme.css
@@ -15,18 +15,20 @@
 /* Remove filters on firebug specific images */
 
 .theme-firebug .devtools-tabbar .devtools-button::before,
 .theme-firebug .devtools-option-toolbarbutton > image,
 .theme-firebug .command-button-invertable::before,
 .theme-firebug #sources-toolbar image,
 .theme-firebug [id$="pane-toggle"] > image,
 .theme-firebug [id$="pane-toggle"]::before,
-.theme-firebug #global-toolbar .devtools-button::before,
+.theme-firebug .sidebar-toggle::before,
 .theme-firebug #element-picker::before,
+.theme-firebug #rewind-timeline::before,
+.theme-firebug #pause-resume-timeline::before,
 .theme-firebug #debugger-controls .toolbarbutton-icon,
 .theme-firebug #filter-button .toolbarbutton-icon {
   filter: none !important;
 }
 
 /* CodeMirror Color Syntax */
 
 .theme-firebug .cm-keyword {color: BlueViolet; font-weight: bold;}
--- a/devtools/client/themes/layout.css
+++ b/devtools/client/themes/layout.css
@@ -1,72 +1,40 @@
 /* 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/ */
 
-#sidebar-panel-layoutview {
-  display: block;
-  overflow: auto;
-  height: 100%;
-}
-
-#layout-wrapper {
-  /* The sidebar-panel is not focusable, this wrapper will catch click events in
-     all the empty area around the layout-container */
-  height: 100%;
-}
-
 #layout-container {
   /* The view will grow bigger as the window gets resized, until 400px */
   max-width: 400px;
   margin: 0px auto;
   padding: 0;
-  /* "Contain" the absolutely positioned #layout-main element */
-  position: relative;
-}
-
-/* Header: contains the position and size of the element */
-
-#layout-header {
-  box-sizing: border-box;
-  width: 100%;
-  padding: 4px 14px;
-  display: -moz-box;
-  vertical-align: top;
-}
-
-#layout-header:dir(rtl) {
-  -moz-box-direction: reverse;
 }
 
-#layout-header > span {
-  display: -moz-box;
-}
+/* Header */
 
-#layout-element-size {
-  -moz-box-flex: 1;
+#layout-header,
+#layout-info {
+  display: flex;
+  align-items: center;
+  padding: 4px 17px;
 }
 
-#layout-element-size:dir(rtl) {
-  -moz-box-pack: end;
+#layout-geometry-editor {
+  visibility: hidden;
 }
 
-@media (max-height: 250px) {
-  #layout-header {
-    padding-top: 0;
-    padding-bottom: 0;
-    margin-top: 10px;
-    margin-bottom: 8px;
-  }
+#layout-geometry-editor::before {
+  background: url(images/geometry-editor.svg) no-repeat center center / 16px 16px;
 }
 
 /* Main: contains the box-model regions */
 
 #layout-main {
-  position: absolute;
+  position: relative;
   box-sizing: border-box;
   /* The regions are semi-transparent, so the white background is partly
      visible */
   background-color: white;
   color: var(--theme-selection-color);
   /* Make sure there is some space between the window's edges and the regions */
   margin: 0 14px 10px 14px;
   width: calc(100% - 2 * 14px);
@@ -311,17 +279,17 @@
     left: 16px;
   }
 
   .layout-border.layout-right {
     right: 17px;
   }
 }
 
-/* Legend, displayed inside regions */
+/* Legend: displayed inside regions */
 
 .layout-legend {
   position: absolute;
   margin: 5px 6px;
   z-index: 1;
 }
 
 .layout-legend[data-box="margin"] {
@@ -351,28 +319,18 @@
 }
 
 /* Make sure the content size doesn't appear as editable like the other sizes */
 
 .layout-size > span {
   cursor: default;
 }
 
-/* Hide all values when the view is inactive */
+/* Layout info: contains the position and size of the element */
 
-#layout-container.inactive > #layout-header > #layout-element-position,
-#layout-container.inactive > #layout-header > #layout-element-size,
-#layout-container.inactive > #layout-main > p {
-   visibility: hidden;
+#layout-element-size {
+  flex: 1;
 }
 
 #layout-position-group {
   display: flex;
   align-items: center;
 }
-
-#layout-geometry-editor {
-  visibility: hidden;
-}
-
-#layout-geometry-editor::before {
-  background: url(images/geometry-editor.svg) no-repeat center center / 16px 16px;
-}
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -40,17 +40,16 @@
 #ruleview-toolbar-container {
   display: flex;
   flex-direction: column;
   height: auto;
 }
 
 #ruleview-toolbar {
   display: flex;
-  height: 23px;
 }
 
 #ruleview-toolbar > .devtools-searchbox:first-child {
   padding-inline-start: 0px;
 }
 
 #ruleview-command-toolbar {
   display: flex;
@@ -251,21 +250,19 @@
 .theme-firebug .ruleview-overridden .ruleview-propertyname,
 .theme-firebug .ruleview-overridden .ruleview-propertyvalue {
   text-decoration: line-through;
 }
 
 .theme-firebug .ruleview-enableproperty:not([checked]) ~ .ruleview-namecontainer,
 .theme-firebug .ruleview-enableproperty:not([checked]) ~ .ruleview-namecontainer *,
 .theme-firebug .ruleview-enableproperty:not([checked]) ~ .ruleview-propertyvaluecontainer,
-.theme-firebug .ruleview-enableproperty:not([checked]) ~ .ruleview-propertyvaluecontainer * {
-  color: #CCCCCC;
-}
-
-.theme-firebug .ruleview-enableproperty:not([checked]) ~ .ruleview-computedlist * {
+.theme-firebug .ruleview-enableproperty:not([checked]) ~ .ruleview-propertyvaluecontainer *,
+.theme-firebug .ruleview-overridden > * > .ruleview-computed:not(.ruleview-overridden),
+.theme-firebug .ruleview-overridden > * > .ruleview-computed:not(.ruleview-overridden) * {
   color: #CCCCCC;
 }
 
 .ruleview-rule + .ruleview-rule {
   border-top-width: 1px;
   border-top-style: dotted;
 }
 
--- a/devtools/client/themes/toolbars.css
+++ b/devtools/client/themes/toolbars.css
@@ -385,16 +385,17 @@
 .devtools-filterinput .textbox-input::-moz-placeholder {
   font-style: normal;
 }
 
 /* Searchbox is a div container element for a search input element */
 .devtools-searchbox {
   display: flex;
   flex: 1;
+  height: 23px;
   position: relative;
   padding: 0 3px;
 }
 
 /* The spacing is accomplished with a padding on the searchbox */
 .devtools-searchbox > .devtools-textinput,
 .devtools-searchbox > .devtools-searchinput,
 .devtools-searchbox > .devtools-filterinput {
--- a/devtools/server/actors/inspector.js
+++ b/devtools/server/actors/inspector.js
@@ -71,16 +71,17 @@ const {
   isAnonymous,
   isNativeAnonymous,
   isXBLAnonymous,
   isShadowAnonymous,
   getFrameElement
 } = require("devtools/shared/layout/utils");
 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";
 const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
@@ -2763,23 +2764,23 @@ function isNodeDead(node) {
 }
 
 /**
  * Wrapper for inDeepTreeWalker.  Adds filtering to the traversal methods.
  * See inDeepTreeWalker for more information about the methods.
  *
  * @param {DOMNode} node
  * @param {Window} rootWin
- * @param {Int} whatToShow See Ci.nsIDOMNodeFilter / inIDeepTreeWalker for
+ * @param {Int} whatToShow See nodeFilterConstants / inIDeepTreeWalker for
  * options.
  * @param {Function} filter A custom filter function Taking in a DOMNode
  *        and returning an Int. See WalkerActor.nodeFilter for an example.
  */
 function DocumentWalker(node, rootWin,
-    whatToShow = Ci.nsIDOMNodeFilter.SHOW_ALL,
+    whatToShow = nodeFilterConstants.SHOW_ALL,
     filter = standardTreeWalkerFilter) {
   if (!rootWin.location) {
     throw new Error("Got an invalid root window in DocumentWalker");
   }
 
   this.walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"]
     .createInstance(Ci.inIDeepTreeWalker);
   this.walker.showAnonymousContent = true;
@@ -2788,17 +2789,17 @@ function DocumentWalker(node, rootWin,
   this.walker.init(rootWin.document, whatToShow);
   this.filter = filter;
 
   // Make sure that the walker knows about the initial node (which could
   // be skipped due to a filter).  Note that simply calling parentNode()
   // causes currentNode to be updated.
   this.walker.currentNode = node;
   while (node &&
-         this.filter(node) === Ci.nsIDOMNodeFilter.FILTER_SKIP) {
+         this.filter(node) === nodeFilterConstants.FILTER_SKIP) {
     node = this.walker.parentNode();
   }
 }
 
 DocumentWalker.prototype = {
   get node() {
     return this.walker.node;
   },
@@ -2819,64 +2820,64 @@ DocumentWalker.prototype = {
   nextNode: function () {
     let node = this.walker.currentNode;
     if (!node) {
       return null;
     }
 
     let nextNode = this.walker.nextNode();
     while (nextNode &&
-           this.filter(nextNode) === Ci.nsIDOMNodeFilter.FILTER_SKIP) {
+           this.filter(nextNode) === nodeFilterConstants.FILTER_SKIP) {
       nextNode = this.walker.nextNode();
     }
 
     return nextNode;
   },
 
   firstChild: function () {
     let node = this.walker.currentNode;
     if (!node) {
       return null;
     }
 
     let firstChild = this.walker.firstChild();
     while (firstChild &&
-           this.filter(firstChild) === Ci.nsIDOMNodeFilter.FILTER_SKIP) {
+           this.filter(firstChild) === nodeFilterConstants.FILTER_SKIP) {
       firstChild = this.walker.nextSibling();
     }
 
     return firstChild;
   },
 
   lastChild: function () {
     let node = this.walker.currentNode;
     if (!node) {
       return null;
     }
 
     let lastChild = this.walker.lastChild();
     while (lastChild &&
-           this.filter(lastChild) === Ci.nsIDOMNodeFilter.FILTER_SKIP) {
+           this.filter(lastChild) === nodeFilterConstants.FILTER_SKIP) {
       lastChild = this.walker.previousSibling();
     }
 
     return lastChild;
   },
 
   previousSibling: function () {
     let node = this.walker.previousSibling();
-    while (node && this.filter(node) === Ci.nsIDOMNodeFilter.FILTER_SKIP) {
+    while (node && this.filter(node) === nodeFilterConstants.FILTER_SKIP) {
       node = this.walker.previousSibling();
     }
     return node;
   },
 
   nextSibling: function () {
     let node = this.walker.nextSibling();
-    while (node && this.filter(node) === Ci.nsIDOMNodeFilter.FILTER_SKIP) {
+    while (node && this.filter(node) === nodeFilterConstants.FILTER_SKIP) {
       node = this.walker.nextSibling();
     }
     return node;
   }
 };
 
 function isInXULDocument(el) {
   let doc = nodeDocument(el);
@@ -2890,49 +2891,49 @@ function isInXULDocument(el) {
  * content with the exception of ::before and ::after and anonymous content
  * in XUL document (needed to show all elements in the browser toolbox).
  */
 function standardTreeWalkerFilter(node) {
   // ::before and ::after are native anonymous content, but we always
   // want to show them
   if (node.nodeName === "_moz_generated_content_before" ||
       node.nodeName === "_moz_generated_content_after") {
-    return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
+    return nodeFilterConstants.FILTER_ACCEPT;
   }
 
   // Ignore empty whitespace text nodes.
   if (node.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
       !/[^\s]/.exec(node.nodeValue)) {
-    return Ci.nsIDOMNodeFilter.FILTER_SKIP;
+    return nodeFilterConstants.FILTER_SKIP;
   }
 
   // Ignore all native and XBL anonymous content inside a non-XUL document
   if (!isInXULDocument(node) && (isXBLAnonymous(node) ||
                                   isNativeAnonymous(node))) {
     // Note: this will skip inspecting the contents of feedSubscribeLine since
     // that's XUL content injected in an HTML document, but we need to because
     // this also skips many other elements that need to be skipped - like form
     // controls, scrollbars, video controls, etc (see bug 1187482).
-    return Ci.nsIDOMNodeFilter.FILTER_SKIP;
+    return nodeFilterConstants.FILTER_SKIP;
   }
 
-  return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
+  return nodeFilterConstants.FILTER_ACCEPT;
 }
 
 /**
  * This DeepTreeWalker filter is like standardTreeWalkerFilter except that
  * it also includes all anonymous content (like internal form controls).
  */
 function allAnonymousContentTreeWalkerFilter(node) {
   // Ignore empty whitespace text nodes.
   if (node.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
       !/[^\s]/.exec(node.nodeValue)) {
-    return Ci.nsIDOMNodeFilter.FILTER_SKIP;
+    return nodeFilterConstants.FILTER_SKIP;
   }
-  return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
+  return nodeFilterConstants.FILTER_ACCEPT;
 }
 
 /**
  * Returns a promise that is settled once the given HTMLImageElement has
  * finished loading.
  *
  * @param {HTMLImageElement} image - The image element.
  * @param {Number} timeout - Maximum amount of time the image is allowed to load
--- a/devtools/server/tests/mochitest/test_inspector-anonymous.html
+++ b/devtools/server/tests/mochitest/test_inspector-anonymous.html
@@ -7,21 +7,22 @@ https://bugzilla.mozilla.org/show_bug.cg
   <meta charset="utf-8">
   <title>Test for Bug 777674</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 Ci = Components.interfaces;
   const {InspectorFront} =
     require("devtools/shared/fronts/inspector");
   const {_documentWalker} =
     require("devtools/server/actors/inspector");
+  const nodeFilterConstants =
+    require("devtools/shared/dom-node-filter-constants");
   const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
   SpecialPowers.pushPrefEnv({"set": [
     ["dom.webcomponents.enabled", true]
   ]});
   SimpleTest.waitForExplicitFinish();
 
   let gWalker = null;
@@ -71,19 +72,19 @@ window.onload = function() {
   addAsyncTest(function* testNativeAnonymousStartingNode() {
     info ("Tests attaching an element that a walker can't see.");
 
     let serverConnection = gWalker.conn._transport._serverConnection;
     let serverWalker = serverConnection.getActor(gWalker.actorID);
     let docwalker = new _documentWalker(
       gInspectee.querySelector("select"),
       gInspectee.defaultView,
-      Ci.nsIDOMNodeFilter.SHOW_ALL,
+      nodeFilterConstants.SHOW_ALL,
       () => {
-        return Ci.nsIDOMNodeFilter.FILTER_ACCEPT
+        return nodeFilterConstants.FILTER_ACCEPT
       }
     );
     let scrollbar = docwalker.lastChild();
     is (scrollbar.tagName, "scrollbar", "An anonymous child has been fetched");
 
     let node = yield serverWalker.attachElement(scrollbar);
 
     ok (node, "A response has arrived");
new file mode 100644
--- /dev/null
+++ b/devtools/shared/dom-node-filter-constants.js
@@ -0,0 +1,21 @@
+"use strict";
+
+module.exports = {
+  FILTER_ACCEPT: 1,
+  FILTER_REJECT: 2,
+  FILTER_SKIP: 3,
+
+  SHOW_ALL: 0xFFFFFFFF,
+  SHOW_ELEMENT: 0x00000001,
+  SHOW_ATTRIBUTE: 0x00000002,
+  SHOW_TEXT: 0x00000004,
+  SHOW_CDATA_SECTION: 0x00000008,
+  SHOW_ENTITY_REFERENCE: 0x00000010,
+  SHOW_ENTITY: 0x00000020,
+  SHOW_PROCESSING_INSTRUCTION: 0x00000040,
+  SHOW_COMMENT: 0x00000080,
+  SHOW_DOCUMENT: 0x00000100,
+  SHOW_DOCUMENT_TYPE: 0x00000200,
+  SHOW_DOCUMENT_FRAGMENT: 0x00000400,
+  SHOW_NOTATION: 0x00000800
+};
--- a/devtools/shared/inspector/css-logic.js
+++ b/devtools/shared/inspector/css-logic.js
@@ -35,17 +35,16 @@
  * CssLogic uses the standard DOM API, and the Gecko inIDOMUtils API to access
  * styling information in the page, and present this to the user in a way that
  * helps them understand:
  * - why their expectations may not have been fulfilled
  * - how browsers process CSS
  * @constructor
  */
 
-const { Cc, Ci } = require("chrome");
 const Services = require("Services");
 
 // This should be ok because none of the functions that use this should be used
 // on the worker thread, where Cu is not available.
 loader.lazyRequireGetter(this, "CSS", "CSS");
 
 loader.lazyRequireGetter(this, "CSSLexer", "devtools/shared/css-lexer");
 
@@ -139,17 +138,17 @@ const TAB_CHARS = "\t";
  * Prettify minified CSS text.
  * This prettifies CSS code where there is no indentation in usual places while
  * keeping original indentation as-is elsewhere.
  * @param string text The CSS source to prettify.
  * @return string Prettified CSS source
  */
 function prettifyCSS(text, ruleCount) {
   if (prettifyCSS.LINE_SEPARATOR == null) {
-    let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
+    let os = Services.appinfo.OS;
     prettifyCSS.LINE_SEPARATOR = (os === "WINNT" ? "\r\n" : "\n");
   }
 
   // remove initial and terminating HTML comments and surrounding whitespace
   text = text.replace(/(?:^\s*<!--[\r\n]*)|(?:\s*-->\s*$)/g, "");
   let originalText = text;
   text = text.trim();
 
--- a/devtools/shared/layout/utils.js
+++ b/devtools/shared/layout/utils.js
@@ -1,16 +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 { Ci, Cc } = require("chrome");
 const { memoize } = require("sdk/lang/functional");
+const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");
 
 loader.lazyRequireGetter(this, "setIgnoreLayoutChanges", "devtools/server/actors/layout", true);
 exports.setIgnoreLayoutChanges = (...args) =>
   this.setIgnoreLayoutChanges(...args);
 
 /**
  * Returns the `DOMWindowUtils` for the window given.
  *
@@ -372,17 +373,17 @@ function safelyGetContentWindow(frame) {
   if (frame.contentWindow) {
     return frame.contentWindow;
   }
 
   let walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"]
                .createInstance(Ci.inIDeepTreeWalker);
   walker.showSubDocuments = true;
   walker.showDocumentsAsNodes = true;
-  walker.init(frame, Ci.nsIDOMNodeFilter.SHOW_ALL);
+  walker.init(frame, nodeFilterConstants.SHOW_ALL);
   walker.currentNode = frame;
 
   let document = walker.nextNode();
   if (!document || !document.defaultView) {
     throw new Error("Couldn't get the content window inside frame " + frame);
   }
 
   return document.defaultView;
--- a/devtools/shared/moz.build
+++ b/devtools/shared/moz.build
@@ -44,16 +44,17 @@ DevToolsModules(
     'content-observer.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',
     'event-emitter.js',
     'event-parsers.js',
     'indentation.js',
     'Loader.jsm',
     'Parser.jsm',
     'path.js',
     'protocol.js',
     'system.js',
--- a/devtools/shared/system.js
+++ b/devtools/shared/system.js
@@ -326,15 +326,14 @@ function getSetting(name) {
       handleError: (error) => deferred.reject(error),
     });
   } else {
     deferred.reject(new Error("No settings service"));
   }
   return deferred.promise;
 }
 
-exports.is64Bit = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo).is64Bit;
 exports.getSystemInfo = Task.async(getSystemInfo);
 exports.getAppIniString = getAppIniString;
 exports.getSetting = getSetting;
 exports.getScreenDimensions = getScreenDimensions;
 exports.getOSCPU = getOSCPU;
 exports.constants = AppConstants;
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -71,32 +71,40 @@ ClientEngine.prototype = {
   get lastRecordUpload() {
     return Svc.Prefs.get(this.name + ".lastRecordUpload", 0);
   },
   set lastRecordUpload(value) {
     Svc.Prefs.set(this.name + ".lastRecordUpload", Math.floor(value));
   },
 
   get remoteClients() {
-    return Object.values(this._store._remoteClients);
+    // return all non-stale clients for external consumption.
+    return Object.values(this._store._remoteClients).filter(v => !v.stale);
+  },
+
+  remoteClientExists(id) {
+    let client = this._store._remoteClients[id];
+    return !!(client && !client.stale);
   },
 
   // Aggregate some stats on the composition of clients on this account
   get stats() {
     let stats = {
       hasMobile: this.localType == DEVICE_TYPE_MOBILE,
       names: [this.localName],
       numClients: 1,
     };
 
     for (let id in this._store._remoteClients) {
-      let {name, type} = this._store._remoteClients[id];
-      stats.hasMobile = stats.hasMobile || type == DEVICE_TYPE_MOBILE;
-      stats.names.push(name);
-      stats.numClients++;
+      let {name, type, stale} = this._store._remoteClients[id];
+      if (!stale) {
+        stats.hasMobile = stats.hasMobile || type == DEVICE_TYPE_MOBILE;
+        stats.names.push(name);
+        stats.numClients++;
+      }
     }
 
     return stats;
   },
 
   /**
    * Obtain information about device types.
    *
@@ -104,16 +112,19 @@ ClientEngine.prototype = {
    */
   get deviceTypes() {
     let counts = new Map();
 
     counts.set(this.localType, 1);
 
     for (let id in this._store._remoteClients) {
       let record = this._store._remoteClients[id];
+      if (record.stale) {
+        continue; // pretend "stale" records don't exist.
+      }
       let type = record.type;
       if (!counts.has(type)) {
         counts.set(type, 0);
       }
 
       counts.set(type, counts.get(type) + 1);
     }
 
@@ -152,20 +163,16 @@ ClientEngine.prototype = {
 
   get localType() {
     return Utils.getDeviceType();
   },
   set localType(value) {
     Svc.Prefs.set("client.type", value);
   },
 
-  remoteClientExists(id) {
-    return !!this._store._remoteClients[id];
-  },
-
   getClientName(id) {
     if (id == this.localID) {
       return this.localName;
     }
     let client = this._store._remoteClients[id];
     return client ? client.name : "";
   },
 
@@ -197,30 +204,32 @@ ClientEngine.prototype = {
       // tabs collection, since showing their list of tabs is confusing.
       for (let id in this._store._remoteClients) {
         if (!this._incomingClients[id]) {
           this._log.info(`Removing local state for deleted client ${id}`);
           this._removeRemoteClient(id);
         }
       }
       // Bug 1264498: Mobile clients don't remove themselves from the clients
-      // collection when the user disconnects Sync, so we filter out clients
+      // collection when the user disconnects Sync, so we mark as stale clients
       // with the same name that haven't synced in over a week.
+      // (Note we can't simply delete them, or we re-apply them next sync - see
+      // bug 1287687)
       delete this._incomingClients[this.localID];
       let names = new Set([this.localName]);
       for (let id in this._incomingClients) {
         let record = this._store._remoteClients[id];
         if (!names.has(record.name)) {
           names.add(record.name);
           continue;
         }
         let remoteAge = AsyncResource.serverTime - this._incomingClients[id];
         if (remoteAge > STALE_CLIENT_REMOTE_AGE) {
           this._log.info(`Hiding stale client ${id} with age ${remoteAge}`);
-          this._removeRemoteClient(id);
+          record.stale = true;
         }
       }
     } finally {
       this._incomingClients = null;
     }
   },
 
   _uploadOutgoing() {
@@ -340,16 +349,19 @@ ClientEngine.prototype = {
    */
   _sendCommandToClient: function sendCommandToClient(command, args, clientId) {
     this._log.trace("Sending " + command + " to " + clientId);
 
     let client = this._store._remoteClients[clientId];
     if (!client) {
       throw new Error("Unknown remote client ID: '" + clientId + "'.");
     }
+    if (client.stale) {
+      throw new Error("Stale remote client ID: '" + clientId + "'.");
+    }
 
     let action = {
       command: command,
       args: args,
     };
 
     if (!client.commands) {
       client.commands = [action];
@@ -449,18 +461,20 @@ ClientEngine.prototype = {
       this._log.error("Expected " + commandData.args + " args for '" +
                       command + "', but got " + args);
       return;
     }
 
     if (clientId) {
       this._sendCommandToClient(command, args, clientId);
     } else {
-      for (let id in this._store._remoteClients) {
-        this._sendCommandToClient(command, args, id);
+      for (let [id, record] in Iterator(this._store._remoteClients)) {
+        if (!record.stale) {
+          this._sendCommandToClient(command, args, id);
+        }
       }
     }
   },
 
   /**
    * Send a URI to another client for display.
    *
    * A side effect is the score is increased dramatically to incur an
@@ -604,16 +618,22 @@ ClientStore.prototype = {
       record.appPackage = Services.appinfo.ID;
       record.application = this.engine.brandName   // "Nightly"
 
       // We can't compute these yet.
       // record.device = "";            // Bug 1100723
       // record.formfactor = "";        // Bug 1100722
     } else {
       record.cleartext = this._remoteClients[id];
+      if (record.cleartext.stale) {
+        // It's almost certainly a logic error for us to upload a record we
+        // consider stale, so make log noise, but still remove the flag.
+        this._log.error(`Preparing to upload record ${id} that we consider stale`);
+        delete record.cleartext.stale;
+      }
     }
 
     return record;
   },
 
   itemExists(id) {
     return id in this.getAllIDs();
   },
--- a/services/sync/tests/unit/test_clients_engine.js
+++ b/services/sync/tests/unit/test_clients_engine.js
@@ -563,19 +563,51 @@ add_test(function test_filter_duplicate_
 
     _("First sync");
     strictEqual(engine.lastRecordUpload, 0);
     engine._sync();
     ok(engine.lastRecordUpload > 0);
     deepEqual(user.collection("clients").keys().sort(),
               [recentID, dupeID, oldID, engine.localID].sort(),
               "Our record should be uploaded on first sync");
+
     deepEqual(Object.keys(store.getAllIDs()).sort(),
-              [recentID, oldID, engine.localID].sort(),
-              "Fresh clients should be downloaded on first sync");
+              [recentID, dupeID, oldID, engine.localID].sort(),
+              "Duplicate ID should remain in getAllIDs");
+    ok(engine._store.itemExists(dupeID), "Dupe ID should be considered as existing for Sync methods.");
+    ok(!engine.remoteClientExists(dupeID), "Dupe ID should not be considered as existing for external methods.");
+
+    // dupe desktop should not appear in .deviceTypes.
+    equal(engine.deviceTypes.get("desktop"), 2);
+    equal(engine.deviceTypes.get("mobile"), 1);
+
+    // dupe desktop should not appear in stats
+    deepEqual(engine.stats, {
+      hasMobile: 1,
+      names: [engine.localName, "My Phone", "My old desktop"],
+      numClients: 3,
+    });
+
+    ok(engine.remoteClientExists(oldID), "non-dupe ID should exist.");
+    ok(!engine.remoteClientExists(dupeID), "dupe ID should not exist");
+    equal(engine.remoteClients.length, 2, "dupe should not be in remoteClients");
+
+    // Check that a subsequent Sync doesn't report anything as being processed.
+    let counts;
+    Svc.Obs.add("weave:engine:sync:applied", function observe(subject, data) {
+      Svc.Obs.remove("weave:engine:sync:applied", observe);
+      counts = subject;
+    });
+
+    engine._sync();
+    equal(counts.applied, 0); // We didn't report applying any records.
+    equal(counts.reconciled, 4); // We reported reconcilliation for all records
+    equal(counts.succeeded, 0);
+    equal(counts.failed, 0);
+    equal(counts.newFailed, 0);
 
     _("Broadcast logout to all clients");
     engine.sendCommand("logout", []);
     engine._sync();
 
     let collection = server.getCollection("foo", "clients");
     let recentPayload = JSON.parse(JSON.parse(collection.payload(recentID)).ciphertext);
     deepEqual(recentPayload.commands, [{ command: "logout", args: [] }],
@@ -600,16 +632,32 @@ add_test(function test_filter_duplicate_
     }), now - 10));
 
     _("Second sync.");
     engine._sync();
 
     deepEqual(Object.keys(store.getAllIDs()).sort(),
               [recentID, oldID, dupeID, engine.localID].sort(),
               "Stale client synced, so it should no longer be marked as a dupe");
+
+    ok(engine.remoteClientExists(dupeID), "Dupe ID should appear as it synced.");
+
+    // Recently synced dupe desktop should appear in .deviceTypes.
+    equal(engine.deviceTypes.get("desktop"), 3);
+
+    // Recently synced dupe desktop should now appear in stats
+    deepEqual(engine.stats, {
+      hasMobile: 1,
+      names: [engine.localName, "My Phone", engine.localName, "My old desktop"],
+      numClients: 4,
+    });
+
+    ok(engine.remoteClientExists(dupeID), "recently synced dupe ID should now exist");
+    equal(engine.remoteClients.length, 3, "recently synced dupe should now be in remoteClients");
+
   } finally {
     Svc.Prefs.resetBranch("");
     Service.recordManager.clearCache();
 
     try {
       server.deleteCollections("foo");
     } finally {
       server.stop(run_next_test);
--- a/testing/web-platform/meta/html/semantics/forms/textfieldselection/selection.html.ini
+++ b/testing/web-platform/meta/html/semantics/forms/textfieldselection/selection.html.ini
@@ -1,14 +1,17 @@
 [selection.html]
   type: testharness
   [test SelectionStart offset for input]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1287655
 
   [test SelectionStart offset for textarea]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1287655
 
   [test SelectionEnd offset for input]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1287655
 
   [test SelectionEnd offset for textarea]
     expected: FAIL
-
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1287655
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -6316,24 +6316,16 @@
   "DEVTOOLS_COMPUTEDVIEW_OPENED_COUNT": {
     "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
     "expires_in_version": "never",
     "kind": "count",
     "bug_numbers": [1247985],
     "description": "Number of times the DevTools Computed View has been opened.",
     "releaseChannelCollection": "opt-out"
   },
-  "DEVTOOLS_LAYOUTVIEW_OPENED_COUNT": {
-    "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
-    "expires_in_version": "never",
-    "kind": "count",
-    "bug_numbers": [1247985],
-    "description": "Number of times the DevTools Layout View has been opened.",
-    "releaseChannelCollection": "opt-out"
-  },
   "DEVTOOLS_FONTINSPECTOR_OPENED_COUNT": {
     "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
     "expires_in_version": "never",
     "kind": "count",
     "bug_numbers": [1247985],
     "description": "Number of times the DevTools Font Inspector has been opened.",
     "releaseChannelCollection": "opt-out"
   },
@@ -6597,21 +6589,16 @@
     "kind": "flag",
     "description": "Number of users that have opened the DevTools Rule View."
   },
   "DEVTOOLS_COMPUTEDVIEW_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "Number of users that have opened the DevTools Computed View."
   },
-  "DEVTOOLS_LAYOUTVIEW_OPENED_PER_USER_FLAG": {
-    "expires_in_version": "never",
-    "kind": "flag",
-    "description": "Number of users that have opened the DevTools Layout View."
-  },
   "DEVTOOLS_FONTINSPECTOR_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "Number of users that have opened the DevTools Font Inspector."
   },
   "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
@@ -6808,23 +6795,16 @@
   },
   "DEVTOOLS_COMPUTEDVIEW_TIME_ACTIVE_SECONDS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 10000000,
     "n_buckets": 100,
     "description": "How long has the computed view been active (seconds)"
   },
-  "DEVTOOLS_LAYOUTVIEW_TIME_ACTIVE_SECONDS": {
-    "expires_in_version": "never",
-    "kind": "exponential",
-    "high": 10000000,
-    "n_buckets": 100,
-    "description": "How long has the layout view been active (seconds)"
-  },
   "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 10000000,
     "n_buckets": 100,
     "description": "How long has the font inspector been active (seconds)"
   },
   "DEVTOOLS_ANIMATIONINSPECTOR_TIME_ACTIVE_SECONDS": {
--- a/toolkit/components/url-classifier/SafeBrowsing.jsm
+++ b/toolkit/components/url-classifier/SafeBrowsing.jsm
@@ -41,35 +41,38 @@ function getLists(prefName) {
   if (!pref) {
     return [];
   }
   return pref.split(",")
     .filter(function(value) { return value.indexOf("test-") == -1; })
     .map(function(value) { return value.trim(); });
 }
 
-// These may be a comma-separated lists of tables.
-const phishingLists = getLists("urlclassifier.phishTable");
-const malwareLists = getLists("urlclassifier.malwareTable");
-const downloadBlockLists = getLists("urlclassifier.downloadBlockTable");
-const downloadAllowLists = getLists("urlclassifier.downloadAllowTable");
-const trackingProtectionLists = getLists("urlclassifier.trackingTable");
-const trackingProtectionWhitelists = getLists("urlclassifier.trackingWhitelistTable");
-const blockedLists = getLists("urlclassifier.blockedTable");
+const tablePreferences = [
+  "urlclassifier.phishTable",
+  "urlclassifier.malwareTable",
+  "urlclassifier.downloadBlockTable",
+  "urlclassifier.downloadAllowTable",
+  "urlclassifier.trackingTable",
+  "urlclassifier.trackingWhitelistTable",
+  "urlclassifier.blockedTable"
+];
 
 this.SafeBrowsing = {
 
   init: function() {
     if (this.initialized) {
       log("Already initialized");
       return;
     }
 
     Services.prefs.addObserver("browser.safebrowsing", this.readPrefs.bind(this), false);
     Services.prefs.addObserver("privacy.trackingprotection", this.readPrefs.bind(this), false);
+    Services.prefs.addObserver("urlclassifier", this.readPrefs.bind(this), false);
+
     this.readPrefs();
     this.addMozEntries();
 
     this.controlUpdateChecking();
     this.initialized = true;
 
     log("init() finished");
   },
@@ -86,46 +89,54 @@ this.SafeBrowsing = {
       log("Check browser.safebrowsing.provider.[google/mozilla].lists");
       return;
     }
 
     listManager.registerTable(listname, providerName, provider.updateURL, provider.gethashURL);
   },
 
   registerTables: function() {
-    for (let i = 0; i < phishingLists.length; ++i) {
-      this.registerTableWithURLs(phishingLists[i]);
+    for (let i = 0; i < this.phishingLists.length; ++i) {
+      this.registerTableWithURLs(this.phishingLists[i]);
     }
-    for (let i = 0; i < malwareLists.length; ++i) {
-      this.registerTableWithURLs(malwareLists[i]);
+    for (let i = 0; i < this.malwareLists.length; ++i) {
+      this.registerTableWithURLs(this.malwareLists[i]);
     }
-    for (let i = 0; i < downloadBlockLists.length; ++i) {
-      this.registerTableWithURLs(downloadBlockLists[i]);
+    for (let i = 0; i < this.downloadBlockLists.length; ++i) {
+      this.registerTableWithURLs(this.downloadBlockLists[i]);
     }
-    for (let i = 0; i < downloadAllowLists.length; ++i) {
-      this.registerTableWithURLs(downloadAllowLists[i]);
+    for (let i = 0; i < this.downloadAllowLists.length; ++i) {
+      this.registerTableWithURLs(this.downloadAllowLists[i]);
     }
-    for (let i = 0; i < trackingProtectionLists.length; ++i) {
-      this.registerTableWithURLs(trackingProtectionLists[i]);
+    for (let i = 0; i < this.trackingProtectionLists.length; ++i) {
+      this.registerTableWithURLs(this.trackingProtectionLists[i]);
     }
-    for (let i = 0; i < trackingProtectionWhitelists.length; ++i) {
-      this.registerTableWithURLs(trackingProtectionWhitelists[i]);
+    for (let i = 0; i < this.trackingProtectionWhitelists.length; ++i) {
+      this.registerTableWithURLs(this.trackingProtectionWhitelists[i]);
     }
-    for (let i = 0; i < blockedLists.length; ++i) {
-      this.registerTableWithURLs(blockedLists[i]);
+    for (let i = 0; i < this.blockedLists.length; ++i) {
+      this.registerTableWithURLs(this.blockedLists[i]);
     }
   },
 
 
   initialized:      false,
   phishingEnabled:  false,
   malwareEnabled:   false,
   trackingEnabled:  false,
   blockedEnabled:   false,
 
+  phishingLists:                [],
+  malwareLists:                 [],
+  downloadBlockLists:           [],
+  downloadAllowLists:           [],
+  trackingProtectionLists:      [],
+  trackingProtectionWhitelists: [],
+  blockedLists:                 [],
+
   updateURL:             null,
   gethashURL:            null,
 
   reportURL:             null,
 
   getReportURL: function(kind, URI) {
     let pref;
     switch (kind) {
@@ -161,16 +172,25 @@ this.SafeBrowsing = {
   readPrefs: function() {
     log("reading prefs");
 
     this.debug = Services.prefs.getBoolPref("browser.safebrowsing.debug");
     this.phishingEnabled = Services.prefs.getBoolPref("browser.safebrowsing.phishing.enabled");
     this.malwareEnabled = Services.prefs.getBoolPref("browser.safebrowsing.malware.enabled");
     this.trackingEnabled = Services.prefs.getBoolPref("privacy.trackingprotection.enabled") || Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled");
     this.blockedEnabled = Services.prefs.getBoolPref("browser.safebrowsing.blockedURIs.enabled");
+
+    [this.phishingLists,
+     this.malwareLists,
+     this.downloadBlockLists,
+     this.downloadAllowLists,
+     this.trackingProtectionLists,
+     this.trackingProtectionWhitelists,
+     this.blockedLists] = tablePreferences.map(getLists);
+
     this.updateProviderURLs();
     this.registerTables();
 
     // XXX The listManager backend gets confused if this is called before the
     // lists are registered. So only call it here when a pref changes, and not
     // when doing initialization. I expect to refactor this later, so pardon the hack.
     if (this.initialized) {
       this.controlUpdateChecking();
@@ -242,63 +262,63 @@ this.SafeBrowsing = {
   controlUpdateChecking: function() {
     log("phishingEnabled:", this.phishingEnabled, "malwareEnabled:",
         this.malwareEnabled, "trackingEnabled:", this.trackingEnabled,
         "blockedEnabled:", this.blockedEnabled);
 
     let listManager = Cc["@mozilla.org/url-classifier/listmanager;1"].
                       getService(Ci.nsIUrlListManager);
 
-    for (let i = 0; i < phishingLists.length; ++i) {
+    for (let i = 0; i < this.phishingLists.length; ++i) {
       if (this.phishingEnabled) {
-        listManager.enableUpdate(phishingLists[i]);
+        listManager.enableUpdate(this.phishingLists[i]);
       } else {
-        listManager.disableUpdate(phishingLists[i]);
+        listManager.disableUpdate(this.phishingLists[i]);
       }
     }
-    for (let i = 0; i < malwareLists.length; ++i) {
+    for (let i = 0; i < this.malwareLists.length; ++i) {
       if (this.malwareEnabled) {
-        listManager.enableUpdate(malwareLists[i]);
+        listManager.enableUpdate(this.malwareLists[i]);
       } else {
-        listManager.disableUpdate(malwareLists[i]);
+        listManager.disableUpdate(this.malwareLists[i]);
       }
     }
-    for (let i = 0; i < downloadBlockLists.length; ++i) {
+    for (let i = 0; i < this.downloadBlockLists.length; ++i) {
       if (this.malwareEnabled) {
-        listManager.enableUpdate(downloadBlockLists[i]);
+        listManager.enableUpdate(this.downloadBlockLists[i]);
       } else {
-        listManager.disableUpdate(downloadBlockLists[i]);
+        listManager.disableUpdate(this.downloadBlockLists[i]);
       }
     }
-    for (let i = 0; i < downloadAllowLists.length; ++i) {
+    for (let i = 0; i < this.downloadAllowLists.length; ++i) {
       if (this.malwareEnabled) {
-        listManager.enableUpdate(downloadAllowLists[i]);
+        listManager.enableUpdate(this.downloadAllowLists[i]);
       } else {
-        listManager.disableUpdate(downloadAllowLists[i]);
+        listManager.disableUpdate(this.downloadAllowLists[i]);
       }
     }
-    for (let i = 0; i < trackingProtectionLists.length; ++i) {
+    for (let i = 0; i < this.trackingProtectionLists.length; ++i) {
       if (this.trackingEnabled) {
-        listManager.enableUpdate(trackingProtectionLists[i]);
+        listManager.enableUpdate(this.trackingProtectionLists[i]);
       } else {
-        listManager.disableUpdate(trackingProtectionLists[i]);
+        listManager.disableUpdate(this.trackingProtectionLists[i]);
       }
     }
-    for (let i = 0; i < trackingProtectionWhitelists.length; ++i) {
+    for (let i = 0; i < this.trackingProtectionWhitelists.length; ++i) {
       if (this.trackingEnabled) {
-        listManager.enableUpdate(trackingProtectionWhitelists[i]);
+        listManager.enableUpdate(this.trackingProtectionWhitelists[i]);
       } else {
-        listManager.disableUpdate(trackingProtectionWhitelists[i]);
+        listManager.disableUpdate(this.trackingProtectionWhitelists[i]);
       }
     }
-    for (let i = 0; i < blockedLists.length; ++i) {
+    for (let i = 0; i < this.blockedLists.length; ++i) {
       if (this.blockedEnabled) {
-        listManager.enableUpdate(blockedLists[i]);
+        listManager.enableUpdate(this.blockedLists[i]);
       } else {
-        listManager.disableUpdate(blockedLists[i]);
+        listManager.disableUpdate(this.blockedLists[i]);
       }
     }
     listManager.maybeToggleUpdateChecking();
   },
 
 
   addMozEntries: function() {
     // Add test entries to the DB.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/bug_1281083.html
@@ -0,0 +1,35 @@
+<html>
+<head>
+<title></title>
+
+<script type="text/javascript">
+
+var scriptItem = "untouched";
+
+function checkLoads() {
+  // Make sure the javascript did not load.
+  window.parent.is(scriptItem, "untouched", "Should not load bad javascript");
+
+  // Call parent.loadTestFrame again to test classification metadata in HTTP
+  // cache entries.
+  if (window.parent.firstLoad) {
+    window.parent.info("Reloading from cache...");
+    window.parent.firstLoad = false;
+    window.parent.loadTestFrame();
+    return;
+  }
+
+  // End (parent) test.
+  window.parent.SimpleTest.finish();
+}
+
+</script>
+
+<!-- Try loading from a malware javascript URI -->
+<script type="text/javascript" src="http://bug1281083.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.js"></script>
+
+</head>
+
+<body onload="checkLoads()">
+</body>
+</html>
--- a/toolkit/components/url-classifier/tests/mochitest/chrome.ini
+++ b/toolkit/components/url-classifier/tests/mochitest/chrome.ini
@@ -1,20 +1,22 @@
 [DEFAULT]
 skip-if = buildapp == 'b2g' || os == 'android'
 support-files =
   allowlistAnnotatedFrame.html
   classifiedAnnotatedFrame.html
   classifiedAnnotatedPBFrame.html
+  bug_1281083.html
 
 [test_lookup_system_principal.html]
 [test_classified_annotations.html]
 tags = trackingprotection
 skip-if = os == 'linux' && asan # Bug 1202548 
 [test_allowlisted_annotations.html]
 tags = trackingprotection
 [test_privatebrowsing_trackingprotection.html]
 tags = trackingprotection
 [test_trackingprotection_bug1157081.html]
 tags = trackingprotection
 [test_trackingprotection_whitelist.html]
 tags = trackingprotection
 [test_donottrack.html]
+[test_classifier_changetablepref.html]
--- a/toolkit/components/url-classifier/tests/mochitest/mochitest.ini
+++ b/toolkit/components/url-classifier/tests/mochitest/mochitest.ini
@@ -17,14 +17,15 @@ support-files =
   unwantedWorker.js
   vp9.webm
   whitelistFrame.html
   workerFrame.html
   ping.sjs
   basic.vtt
   dnt.html
   dnt.sjs
+  update.sjs
 
 [test_classifier.html]
 skip-if = (os == 'linux' && debug) #Bug 1199778
 [test_classifier_worker.html]
 [test_classify_ping.html]
 [test_classify_track.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_classifier_changetablepref.html
@@ -0,0 +1,149 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Bug 1281083 - Changing the urlclassifier.*Table prefs doesn't take effect before the next browser restart.</title>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="classifierHelper.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+const testTable = "moz-track-digest256";
+const UPDATE_URL = "http://mochi.test:8888/tests/toolkit/components/url-classifier/tests/mochitest/update.sjs";
+
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+
+var prefService = Cc["@mozilla.org/preferences-service;1"]
+                  .getService(Ci.nsIPrefService);
+
+var timer = Cc["@mozilla.org/timer;1"]
+            .createInstance(Ci.nsITimer);
+
+// If default preference contain the table we want to test,
+// We should change test table to a different one.
+var trackingTables = SpecialPowers.getCharPref("urlclassifier.trackingTable").split(",");
+ok(!trackingTables.includes(testTable), "test table should not be in the preference");
+
+var listmanager = Cc["@mozilla.org/url-classifier/listmanager;1"].
+                    getService(Ci.nsIUrlListManager);
+
+is(listmanager.getGethashUrl(testTable), "",
+   "gethash url for test table should be empty before setting to preference");
+
+function loadTestFrame() {
+  // gethash url of test table "moz-track-digest256" should be updated
+  // after setting preference.
+  var url = listmanager.getGethashUrl(testTable);
+  var expected = SpecialPowers.getCharPref("browser.safebrowsing.provider.mozilla.gethashURL");
+
+  is(url, expected, testTable + " matches its gethash url");
+
+  // Trigger update
+  listmanager.disableUpdate(testTable);
+  listmanager.enableUpdate(testTable);
+  listmanager.maybeToggleUpdateChecking();
+
+  // We wait until "nextupdattime" was set as a signal that update is complete.
+  waitForUpdateSuccess(function() {
+    document.getElementById("testFrame").src = "bug_1281083.html";
+  });
+}
+
+function waitForUpdateSuccess(callback) {
+  let nextupdatetime =
+    SpecialPowers.getCharPref("browser.safebrowsing.provider.mozilla.nextupdatetime");
+
+  if (nextupdatetime !== "1") {
+    callback();
+    return;
+  }
+
+  timer.initWithCallback(function() {
+    waitForUpdateSuccess(callback);
+  }, 10, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+}
+
+function addCompletionToServer(list, url) {
+  return new Promise(function(resolve, reject) {
+    var listParam = "list=" + list;
+    var fullhashParam = "fullhash=" + hash(url);
+
+    var xhr = new XMLHttpRequest;
+    xhr.open("PUT", UPDATE_URL + "?" +
+             listParam + "&" +
+             fullhashParam, true);
+    xhr.setRequestHeader("Content-Type", "text/plain");
+    xhr.onreadystatechange = function() {
+      if (this.readyState == this.DONE) {
+        resolve();
+      }
+    };
+    xhr.send();
+  });
+}
+
+function hash(str) {
+  function bytesFromString(str) {
+    var converter =
+      SpecialPowers.Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                       .createInstance(SpecialPowers.Ci.nsIScriptableUnicodeConverter);
+    converter.charset = "UTF-8";
+    return converter.convertToByteArray(str);
+  }
+
+  var hasher = SpecialPowers.Cc["@mozilla.org/security/hash;1"]
+                               .createInstance(SpecialPowers.Ci.nsICryptoHash);
+
+  var data = bytesFromString(str);
+  hasher.init(hasher.SHA256);
+  hasher.update(data, data.length);
+
+  return hasher.finish(true);
+}
+
+function runTest() {
+  /**
+   * In this test we try to modify only urlclassifier.*Table preference to see if
+   * url specified in the table will be blocked after update.
+   */
+  var pushPrefPromise = SpecialPowers.pushPrefEnv(
+    {"set" : [["urlclassifier.trackingTable", testTable]]});
+
+  // To make sure url is not blocked by an already blocked url.
+  // Here we use non-tracking.example.com as a tracked url.
+  // Since this table is only used in this bug, so it won't affect other testcases.
+  var addCompletePromise =
+    addCompletionToServer(testTable, "bug1281083.example.com/");
+
+  Promise.all([pushPrefPromise, addCompletePromise])
+    .then(() => {
+      loadTestFrame();
+    });
+}
+
+// Set nextupdatetime to 1 to trigger an update
+SpecialPowers.pushPrefEnv(
+  {"set" : [["privacy.trackingprotection.enabled", true],
+            ["channelclassifier.allowlist_example", true],
+            ["browser.safebrowsing.provider.mozilla.nextupdatetime", "1"],
+            ["browser.safebrowsing.provider.mozilla.lists", testTable],
+            ["browser.safebrowsing.provider.mozilla.updateURL", UPDATE_URL]]},
+  runTest);
+
+// Expected finish() call is in "bug_1281083.html".
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+<iframe id="testFrame" width="100%" height="100%" onload=""></iframe>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/update.sjs
@@ -0,0 +1,114 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+                             "nsIBinaryInputStream",
+                             "setInputStream");
+
+function handleRequest(request, response)
+{
+  var query = {};
+  request.queryString.split('&').forEach(function (val) {
+    var idx = val.indexOf('=');
+    query[val.slice(0, idx)] = unescape(val.slice(idx + 1));
+  });
+
+  // Store fullhash in the server side.
+  if ("list" in query && "fullhash" in query) {
+    // In the server side we will store:
+    // 1. All the full hashes for a given list
+    // 2. All the lists we have right now
+    // data is separate by '\n'
+    let list = query["list"];
+    let hashes = getState(list);
+
+    let hash = base64ToString(query["fullhash"]);
+    hashes += hash + "\n";
+    setState(list, hashes);
+
+    let lists = getState("lists");
+    if (lists.indexOf(list) == -1) {
+      lists += list + "\n";
+      setState("lists", lists);
+    }
+
+    return;
+  }
+
+  var body = new BinaryInputStream(request.bodyInputStream);
+  var avail;
+  var bytes = [];
+
+  while ((avail = body.available()) > 0) {
+    Array.prototype.push.apply(bytes, body.readByteArray(avail));
+  }
+
+  var responseBody = parseV2Request(bytes);
+
+  response.setHeader("Content-Type", "text/plain", false);
+  response.write(responseBody);
+}
+
+function parseV2Request(bytes) {
+  var table = String.fromCharCode.apply(this, bytes).slice(0,-2);
+
+  var ret = "";
+  getState("lists").split("\n").forEach(function(list) {
+    if (list == table) {
+      var completions = getState(list).split("\n");
+      ret += "n:1000\n"
+      ret += "i:" + list + "\n";
+      ret += "a:1:32:" + 32*(completions.length - 1) + "\n";
+
+      for (var completion of completions) {
+        ret += completion;
+      }
+    }
+  });
+
+  return ret;
+}
+
+/* Convert Base64 data to a string */
+const toBinaryTable = [
+    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+    -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+    52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+    -1, 0, 1, 2,  3, 4, 5, 6,  7, 8, 9,10, 11,12,13,14,
+    15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+    -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+    41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+];
+const base64Pad = '=';
+
+function base64ToString(data) {
+    var result = '';
+    var leftbits = 0; // number of bits decoded, but yet to be appended
+    var leftdata = 0; // bits decoded, but yet to be appended
+
+    // Convert one by one.
+    for (var i = 0; i < data.length; i++) {
+        var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
+        var padding = (data[i] == base64Pad);
+        // Skip illegal characters and whitespace
+        if (c == -1) continue;
+
+        // Collect data into leftdata, update bitcount
+        leftdata = (leftdata << 6) | c;
+        leftbits += 6;
+
+        // If we have 8 or more bits, append 8 bits to the result
+        if (leftbits >= 8) {
+            leftbits -= 8;
+            // Append if not padding.
+            if (!padding)
+                result += String.fromCharCode((leftdata >> leftbits) & 0xff);
+            leftdata &= (1 << leftbits) - 1;
+        }
+    }
+
+    // If there are any bits left, the base64 string was corrupted
+    if (leftbits)
+        throw Components.Exception('Corrupted base64 string');
+
+    return result;
+}
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/content/widgets/popup.xml
@@ -311,37 +311,45 @@
       <handler event="popuphiding"><![CDATA[
         try {
           this._currentFocus = document.commandDispatcher.focusedElement;
         } catch (e) {
           this._currentFocus = document.activeElement;
         }
       ]]></handler>
       <handler event="popuphidden"><![CDATA[
+        function doFocus() {
+          // Focus was set on an element inside this panel,
+          // so we need to move it back to where it was previously
+          try {
+            let fm = Components.classes["@mozilla.org/focus-manager;1"]
+                               .getService(Components.interfaces.nsIFocusManager);
+            fm.setFocus(prevFocus, fm.FLAG_NOSCROLL);
+          } catch(e) {
+            prevFocus.focus();
+          }
+        }
         var currentFocus = this._currentFocus;
         var prevFocus = this._prevFocus ? this._prevFocus.get() : null;
         this._currentFocus = null;
         this._prevFocus = null;
-        if (prevFocus && currentFocus && this.getAttribute("norestorefocus") != "true") {
+        if (prevFocus && this.getAttribute("norestorefocus") != "true") {
           // Try to restore focus
           try {
             if (document.commandDispatcher.focusedWindow != window)
               return; // Focus has already been set to a window outside of this panel
           } catch(ex) {}
+
+          if (!currentFocus) {
+            doFocus();
+            return;
+          }
           while (currentFocus) {
             if (currentFocus == this) {
-              // Focus was set on an element inside this panel,
-              // so we need to move it back to where it was previously
-              try {
-                let fm = Components.classes["@mozilla.org/focus-manager;1"]
-                                   .getService(Components.interfaces.nsIFocusManager);
-                fm.setFocus(prevFocus, fm.FLAG_NOSCROLL);
-              } catch(e) {
-                prevFocus.focus();
-              }
+              doFocus();
               return;
             }
             currentFocus = currentFocus.parentNode;
           }
         }
       ]]></handler>
     </handlers>
   </binding>