merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 20 Jul 2016 11:17:45 +0200
changeset 347681 a6ca570852571ae98ef10902af8bc9a27543383c
parent 347649 3383b0da1a14340ec6096aca542eb73b0f7341d5 (current diff)
parent 347680 c0f1a96173fcbebe880279435cf62a428965bac0 (diff)
child 347898 e904e18d7dfcd8097f92d44104ca1462fc5d1335
push id1230
push userjlund@mozilla.com
push dateMon, 31 Oct 2016 18:13:35 +0000
treeherdermozilla-release@5e06e3766db2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
browser/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..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index f3359969b6f30fb090f424961024a72435e01b49..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index e2e9489004eb7cd0661d6ebf8d3a7be4869fddca..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 5126be01f08bfd06884278f6527e0e43f9029534..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 979e92b7f51d98aebc1ce4b6255954e6ee8a12dd..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index c081bbb475f9b60070fcae3817967da05f1b533b..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
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>