merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Sun, 17 Jul 2016 09:16:38 +0200
changeset 330197 ef5f932101e5b833b2429407cb0873471b4d764e
parent 330179 9ee862a030f350e481e3fa3a09d6dc28d4dbf591 (current diff)
parent 330196 fb5fa9e3a52bf056d922c36f38d4ab8a55380307 (diff)
child 330297 711963e8daa312ae06409f8ab5c06612cb0b8f7b
push id9858
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 14:37:10 +0000
treeherdermozilla-aurora@203106ef6cb6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone50.0a1
merge fx-team to mozilla-central a=merge
python/mozbuild/mozbuild/mach_commands.py
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1101,16 +1101,22 @@ pref("services.sync.prefs.sync.spellchec
 pref("services.sync.prefs.sync.xpinstall.whitelist.required", true);
 
 // A preference that controls whether we should show the icon for a remote tab.
 // This pref has no UI but exists because some people may be concerned that
 // fetching these icons to show remote tabs may leak information about that
 // user's tabs and bookmarks. Note this pref is also synced.
 pref("services.sync.syncedTabs.showRemoteIcons", true);
 
+#ifdef NIGHTLY_BUILD
+pref("services.sync.sendTabToDevice.enabled", true);
+#else
+pref("services.sync.sendTabToDevice.enabled", false);
+#endif
+
 // Developer edition preferences
 #ifdef MOZ_DEV_EDITION
 sticky_pref("lightweightThemes.selectedThemeID", "firefox-devedition@mozilla.org");
 #else
 sticky_pref("lightweightThemes.selectedThemeID", "");
 #endif
 
 // Whether the character encoding menu is under the main Firefox button. This
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -278,16 +278,24 @@
       <menuitem id="context-sharepage"
                 label="&sharePageCmd.label;"
                 accesskey="&sharePageCmd.accesskey;"
                 oncommand="SocialShare.sharePage();"/>
       <menuitem id="context-savepage"
                 label="&savePageCmd.label;"
                 accesskey="&savePageCmd.accesskey2;"
                 oncommand="gContextMenu.savePageAs();"/>
+      <menuseparator id="context-sep-sendpagetodevice" hidden="true"/>
+      <menu id="context-sendpagetodevice"
+                label="&sendPageToDevice.label;"
+                accesskey="&sendPageToDevice.accesskey;"
+                hidden="true">
+        <menupopup id="context-sendpagetodevice-popup"
+                   onpopupshowing="(() => { let browser = gBrowser || getPanelBrowser(); gFxAccounts.populateSendTabToDevicesMenu(event.target, browser.currentURI.spec, browser.contentTitle); })()"/>
+      </menu>
       <menu id="context-markpageMenu" label="&social.markpageMenu.label;"
             accesskey="&social.markpageMenu.accesskey;">
         <menupopup/>
       </menu>
       <menuseparator id="context-sep-viewbgimage"/>
       <menuitem id="context-viewbgimage"
                 label="&viewBGImageCmd.label;"
                 accesskey="&viewBGImageCmd.accesskey;"
@@ -321,16 +329,24 @@
                 command="cmd_selectAll"/>
       <menuseparator id="context-sep-selectall"/>
       <menuitem id="context-keywordfield"
                 label="&keywordfield.label;"
                 accesskey="&keywordfield.accesskey;"
                 oncommand="AddKeywordForSearchField();"/>
       <menuitem id="context-searchselect"
                 oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms);"/>
+      <menuseparator id="context-sep-sendlinktodevice" hidden="true"/>
+      <menu id="context-sendlinktodevice"
+                label="&sendLinkToDevice.label;"
+                accesskey="&sendLinkToDevice.accesskey;"
+                hidden="true">
+        <menupopup id="context-sendlinktodevice-popup"
+                   onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/>
+      </menu>
       <menuitem id="context-shareselect"
                 label="&shareSelect.label;"
                 accesskey="&shareSelect.accesskey;"
                 oncommand="gContextMenu.shareSelect();"/>
       <menuseparator id="frame-sep"/>
       <menu id="frame" label="&thisFrameMenu.label;" accesskey="&thisFrameMenu.accesskey;">
         <menupopup>
           <menuitem id="context-showonlythisframe"
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -73,16 +73,25 @@ var gFxAccounts = {
       return false;
     }
     // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
     // All other login failures are assumed to be transient and should go
     // away by themselves, so aren't reflected here.
     return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
   },
 
+  get sendTabToDeviceEnabled() {
+    return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled");
+  },
+
+  get remoteClients() {
+    return Weave.Service.clientsEngine.remoteClients
+           .sort((a, b) => a.name.localeCompare(b.name));
+  },
+
   init: function () {
     // Bail out if we're already initialized and for pop-up windows.
     if (this._initialized || !window.toolbar.visible) {
       return;
     }
 
     for (let topic of this.topics) {
       Services.obs.addObserver(this, topic, false);
@@ -356,16 +365,91 @@ var gFxAccounts = {
     switchToTabHavingURI(url, true, {
       replaceQueryString: true
     });
   },
 
   openSignInAgainPage: function (entryPoint) {
     this.openAccountsPage("reauth", { entrypoint: entryPoint });
   },
+
+  sendTabToDevice: function (url, clientId, title) {
+    Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title);
+  },
+
+  populateSendTabToDevicesMenu: function (devicesPopup, url, title) {
+    // remove existing menu items
+    while (devicesPopup.hasChildNodes()) {
+      devicesPopup.removeChild(devicesPopup.firstChild);
+    }
+
+    const fragment = document.createDocumentFragment();
+
+    const onTargetDeviceCommand = (event) => {
+      const clientId = event.target.getAttribute("clientId");
+      const clients = clientId
+                      ? [clientId]
+                      : this.remoteClients.map(client => client.id);
+
+      clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
+    }
+
+    function addTargetDevice(clientId, name) {
+      const targetDevice = document.createElement("menuitem");
+      targetDevice.addEventListener("command", onTargetDeviceCommand, true);
+      targetDevice.setAttribute("class", "sendtab-target");
+      targetDevice.setAttribute("clientId", clientId);
+      targetDevice.setAttribute("label", name);
+      fragment.appendChild(targetDevice);
+    }
+
+    const clients = this.remoteClients;
+    for (let client of clients) {
+      addTargetDevice(client.id, client.name);
+    }
+
+    // "All devices" menu item
+    const separator = document.createElement("menuseparator");
+    fragment.appendChild(separator);
+    const allDevicesLabel = this.strings.GetStringFromName("sendTabToAllDevices.menuitem");
+    addTargetDevice("", allDevicesLabel);
+
+    devicesPopup.appendChild(fragment);
+  },
+
+  updateTabContextMenu: function (aPopupMenu) {
+    if (!this.sendTabToDeviceEnabled) {
+      return;
+    }
+
+    const remoteClientPresent = this.remoteClients.length > 0;
+    ["context_sendTabToDevice", "context_sendTabToDevice_separator"]
+    .forEach(id => { document.getElementById(id).hidden = !remoteClientPresent });
+  },
+
+  initPageContextMenu: function (contextMenu) {
+    if (!this.sendTabToDeviceEnabled) {
+      return;
+    }
+
+    const remoteClientPresent = this.remoteClients.length > 0;
+    // showSendLink and showSendPage are mutually exclusive
+    const showSendLink = remoteClientPresent
+                         && (contextMenu.onSaveableLink || contextMenu.onPlainTextLink);
+    const showSendPage = !showSendLink && remoteClientPresent
+                         && !(contextMenu.isContentSelected ||
+                              contextMenu.onImage || contextMenu.onCanvas ||
+                              contextMenu.onVideo || contextMenu.onAudio ||
+                              contextMenu.onLink || contextMenu.onTextInput);
+
+    ["context-sendpagetodevice", "context-sep-sendpagetodevice"]
+    .forEach(id => contextMenu.showItem(id, showSendPage));
+    ["context-sendlinktodevice", "context-sep-sendlinktodevice"]
+    .forEach(id => contextMenu.showItem(id, showSendLink));
+  }
 };
 
 XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function () {
   return Cu.import("resource://gre/modules/FxAccountsCommon.js", {});
 });
 
 XPCOMUtils.defineLazyModuleGetter(gFxAccounts, "fxaMigrator",
   "resource://services-sync/FxaMigrator.jsm");
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -322,16 +322,46 @@ toolbarpaletteitem > toolbaritem[sdkstyl
 toolbarpaletteitem:-moz-any([place="palette"], [place="panel"]) > toolbaritem[sdkstylewidget="true"] > toolbarbutton {
   display: -moz-box;
 }
 
 toolbarpaletteitem > toolbaritem[sdkstylewidget="true"][cui-areatype="toolbar"] > .toolbarbutton-text {
   display: -moz-box;
 }
 
+@media not all and (min-resolution: 1.1dppx) {
+  .webextension-browser-action {
+    list-style-image: var(--webextension-toolbar-image);
+  }
+
+  .webextension-browser-action[cui-areatype="menu-panel"],
+  toolbarpaletteitem[place="palette"] > .webextension-browser-action {
+    list-style-image: var(--webextension-menupanel-image);
+  }
+
+  .webextension-page-action {
+    list-style-image: var(--webextension-urlbar-image);
+  }
+}
+
+@media (min-resolution: 1.1dppx) {
+  .webextension-browser-action {
+    list-style-image: var(--webextension-toolbar-image-2x);
+  }
+
+  .webextension-browser-action[cui-areatype="menu-panel"],
+  toolbarpaletteitem[place="palette"] > .webextension-browser-action {
+    list-style-image: var(--webextension-menupanel-image-2x);
+  }
+
+  .webextension-page-action {
+    list-style-image: var(--webextension-urlbar-image-2x);
+  }
+}
+
 toolbarpaletteitem[removable="false"] {
   opacity: 0.5;
   cursor: default;
 }
 
 %ifndef XP_MACOSX
 toolbarpaletteitem[place="palette"],
 toolbarpaletteitem[place="panel"],
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7616,16 +7616,18 @@ var TabContextMenu = {
       toggleMute.accessKey = gNavigatorBundle.getString("muteTab.accesskey");
     }
 
     this.contextTab.toggleMuteMenuItem = toggleMute;
     this._updateToggleMuteMenuItem(this.contextTab);
 
     this.contextTab.addEventListener("TabAttrModified", this, false);
     aPopupMenu.addEventListener("popuphiding", this, false);
+
+    gFxAccounts.updateTabContextMenu(aPopupMenu);
   },
   handleEvent(aEvent) {
     switch (aEvent.type) {
       case "popuphiding":
         gBrowser.removeEventListener("TabAttrModified", this);
         aEvent.target.removeEventListener("popuphiding", this);
         break;
       case "TabAttrModified":
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -99,16 +99,22 @@
                 tbattr="tabbrowser-multiple"
                 oncommand="gBrowser.replaceTabWithWindow(TabContextMenu.contextTab);"/>
 #ifdef E10S_TESTING_ONLY
       <menuitem id="context_openNonRemoteWindow" label="Open in new non-e10s window"
                 tbattr="tabbrowser-remote"
                 hidden="true"
                 oncommand="gBrowser.openNonRemoteWindow(TabContextMenu.contextTab);"/>
 #endif
+      <menuseparator id="context_sendTabToDevice_separator" hidden="true"/>
+      <menu id="context_sendTabToDevice" label="&sendTabToDevice.label;"
+            accesskey="&sendTabToDevice.accesskey;" hidden="true">
+        <menupopup id="context_sendTabToDevicePopupMenu"
+                   onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/>
+      </menu>
       <menuseparator/>
       <menuitem id="context_reloadAllTabs" label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;"
                 tbattr="tabbrowser-multiple-visible"
                 oncommand="gBrowser.reloadAllTabs();"/>
       <menuitem id="context_bookmarkAllTabs"
                 label="&bookmarkAllTabs.label;"
                 accesskey="&bookmarkAllTabs.accesskey;"
                 command="Browser:BookmarkAllTabs"/>
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -112,16 +112,17 @@ nsContextMenu.prototype = {
     this.initMiscItems();
     this.initSpellingItems();
     this.initSaveItems();
     this.initClipboardItems();
     this.initMediaPlayerItems();
     this.initLeaveDOMFullScreenItems();
     this.initClickToPlayItems();
     this.initPasswordManagerItems();
+    this.initSyncItems();
   },
 
   initPageMenuSeparator: function CM_initPageMenuSeparator() {
     this.showItem("page-menu-separator", this.hasPageMenu);
   },
 
   initOpenItems: function CM_initOpenItems() {
     var isMailtoInternal = false;
@@ -571,16 +572,20 @@ nsContextMenu.prototype = {
     if (!fragment) {
       return;
     }
     let popup = document.getElementById("fill-login-popup");
     let insertBeforeElement = document.getElementById("fill-login-no-logins");
     popup.insertBefore(fragment, insertBeforeElement);
   },
 
+  initSyncItems: function() {
+    gFxAccounts.initPageContextMenu(this);
+  },
+
   openPasswordManager: function() {
     LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
   },
 
   inspectNode: function() {
     let {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {});
     let gBrowser = this.browser.ownerDocument.defaultView.gBrowser;
     let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1915,16 +1915,19 @@
             // set the "nodefaultsrc" attribute that prevents a frameLoader
             // from being created as soon as the linked <browser> is inserted
             // into the DOM. We thus have to register the new outerWindowID
             // for non-remote browsers after we have called browser.loadURI().
             if (!remote) {
               this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser);
             }
 
+            var evt = new CustomEvent("TabBrowserCreated", { bubbles: true, detail: {} });
+            aTab.dispatchEvent(evt);
+
             return { usingPreloadedContent: usingPreloadedContent };
           ]]>
         </body>
       </method>
 
       <method name="addTab">
         <parameter name="aURI"/>
         <parameter name="aReferrerURI"/>
--- a/browser/base/content/test/general/browser_contextmenu.js
+++ b/browser/base/content/test/general/browser_contextmenu.js
@@ -847,16 +847,90 @@ add_task(function* test_input_spell_fals
      "---",                 null,
      "context-selectall",   true,
      "---",                 null,
      "spell-add-dictionaries-main",  true,
     ]
   );
 });
 
+const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
+
+add_task(function* test_plaintext_sendpagetodevice() {
+  if (!gFxAccounts.sendTabToDeviceEnabled) {
+    return;
+  }
+  const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
+
+  let plainTextItems = ["context-navigation",   null,
+                        ["context-back",         false,
+                         "context-forward",      false,
+                         "context-reload",       true,
+                         "context-bookmarkpage", true], null,
+                    "---",                  null,
+                    "context-savepage",     true,
+                    ...(hasPocket ? ["context-pocket", true] : []),
+                    "---",                  null,
+                    "context-sendpagetodevice", true,
+                      ["*Foo", true,
+                       "*Bar", true,
+                       "---", null,
+                       "*All Devices", true], null,
+                    "---",                  null,
+                    "context-viewbgimage",  false,
+                    "context-selectall",    true,
+                    "---",                  null,
+                    "context-viewsource",   true,
+                    "context-viewinfo",     true
+                   ];
+  yield test_contextmenu("#test-text", plainTextItems, {
+      onContextMenuShown() {
+        yield openMenuItemSubmenu("context-sendpagetodevice");
+      }
+    });
+
+  restoreRemoteClients(oldGetter);
+});
+
+add_task(function* test_link_sendlinktodevice() {
+  if (!gFxAccounts.sendTabToDeviceEnabled) {
+    return;
+  }
+  const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
+
+  yield test_contextmenu("#test-link",
+    ["context-openlinkintab", true,
+     ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+     // We need a blank entry here because the containers submenu is
+     // dynamically generated with no ids.
+     ...(hasContainers ? ["", null] : []),
+     "context-openlink",      true,
+     "context-openlinkprivate", true,
+     "---",                   null,
+     "context-bookmarklink",  true,
+     "context-savelink",      true,
+     ...(hasPocket ? ["context-savelinktopocket", true] : []),
+     "context-copylink",      true,
+     "context-searchselect",  true,
+     "---",                  null,
+     "context-sendlinktodevice", true,
+      ["*Foo", true,
+       "*Bar", true,
+       "---", null,
+       "*All Devices", true], null,
+    ],
+    {
+      onContextMenuShown() {
+        yield openMenuItemSubmenu("context-sendlinktodevice");
+      }
+    });
+
+  restoreRemoteClients(oldGetter);
+});
+
 add_task(function* test_cleanup() {
   gBrowser.removeCurrentTab();
 });
 
 /**
  * Selects the text of the element that matches the provided `selector`
  *
  * @param {String} selector
--- a/browser/base/content/test/general/browser_visibleTabs_contextMenu.js
+++ b/browser/base/content/test/general/browser_visibleTabs_contextMenu.js
@@ -1,24 +1,41 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
+
 add_task(function* test() {
   // There should be one tab when we start the test
   let [origTab] = gBrowser.visibleTabs;
   is(gBrowser.visibleTabs.length, 1, "there is one visible tab");
   let testTab = gBrowser.addTab();
   is(gBrowser.visibleTabs.length, 2, "there are now two visible tabs");
 
   // Check the context menu with two tabs
   updateTabContextMenu(origTab);
   is(document.getElementById("context_closeTab").disabled, false, "Close Tab is enabled");
   is(document.getElementById("context_reloadAllTabs").disabled, false, "Reload All Tabs is enabled");
 
+
+  if (gFxAccounts.sendTabToDeviceEnabled) {
+    // Check the send tab to device menu item
+    const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
+    yield updateTabContextMenu(origTab, function* () {
+      yield openMenuItemSubmenu("context_sendTabToDevice");
+    });
+    is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
+    let targets = document.getElementById("context_sendTabToDevicePopupMenu").childNodes;
+    is(targets[0].getAttribute("label"), "Foo", "Foo target is present");
+    is(targets[1].getAttribute("label"), "Bar", "Bar target is present");
+    is(targets[3].getAttribute("label"), "All Devices", "All Devices target is present");
+    restoreRemoteClients(oldGetter);
+  }
+
   // Hide the original tab.
   gBrowser.selectedTab = testTab;
   gBrowser.showOnlyTheseTabs([testTab]);
   is(gBrowser.visibleTabs.length, 1, "now there is only one visible tab");
   
   // Check the context menu with one tab.
   updateTabContextMenu(testTab);
   is(document.getElementById("context_closeTab").disabled, false, "Close Tab is enabled when more than one tab exists");
--- a/browser/base/content/test/general/contextmenu_common.js
+++ b/browser/base/content/test/general/contextmenu_common.js
@@ -36,69 +36,70 @@ function getVisibleMenuItems(aMenu, aDat
         var item = aMenu.childNodes[i];
         if (item.hidden)
             continue;
 
         var key = item.accessKey;
         if (key)
             key = key.toLowerCase();
 
-        var isGenerated = item.hasAttribute("generateditemid");
+        var isPageMenuItem = item.hasAttribute("generateditemid");
 
         if (item.nodeName == "menuitem") {
-            var isSpellSuggestion = item.className == "spell-suggestion";
-            if (isSpellSuggestion) {
-              is(item.id, "", "child menuitem #" + i + " is a spelling suggestion");
-            } else if (isGenerated) {
-              is(item.id, "", "child menuitem #" + i + " is a generated item");
+            var isGenerated = item.className == "spell-suggestion"
+                              || item.className == "sendtab-target";
+            if (isGenerated) {
+              is(item.id, "", "child menuitem #" + i + " is generated");
+            } else if (isPageMenuItem) {
+              is(item.id, "", "child menuitem #" + i + " is a generated page menu item");
             } else {
               ok(item.id, "child menuitem #" + i + " has an ID");
             }
             var label = item.getAttribute("label");
             ok(label.length, "menuitem " + item.id + " has a label");
-            if (isSpellSuggestion) {
-              is(key, "", "Spell suggestions shouldn't have an access key");
+            if (isGenerated) {
+              is(key, "", "Generated items shouldn't have an access key");
               items.push("*" + label);
-            } else if (isGenerated) {
+            } else if (isPageMenuItem) {
               items.push("+" + label);
             } else if (item.id.indexOf("spell-check-dictionary-") != 0 &&
                        item.id != "spell-no-suggestions" &&
                        item.id != "spell-add-dictionaries-main" &&
                        item.id != "context-savelinktopocket" &&
                        item.id != "fill-login-saved-passwords" &&
                        item.id != "fill-login-no-logins") {
               ok(key, "menuitem " + item.id + " has an access key");
               if (accessKeys[key])
                   ok(false, "menuitem " + item.id + " has same accesskey as " + accessKeys[key]);
               else
                   accessKeys[key] = item.id;
             }
-            if (!isSpellSuggestion && !isGenerated) {
+            if (!isGenerated && !isPageMenuItem) {
               items.push(item.id);
             }
-            if (isGenerated) {
+            if (isPageMenuItem) {
               var p = {};
               p.type = item.getAttribute("type");
               p.icon = item.getAttribute("image");
               p.checked = item.hasAttribute("checked");
               p.disabled = item.hasAttribute("disabled");
               items.push(p);
             } else {
               items.push(!item.disabled);
             }
         } else if (item.nodeName == "menuseparator") {
             ok(true, "--- seperator id is " + item.id);
             items.push("---");
             items.push(null);
         } else if (item.nodeName == "menu") {
-            if (isGenerated) {
+            if (isPageMenuItem) {
                 item.id = "generated-submenu-" + aData.generatedSubmenuId++;
             }
             ok(item.id, "child menu #" + i + " has an ID");
-            if (!isGenerated) {
+            if (!isPageMenuItem) {
                 ok(key, "menu has an access key");
                 if (accessKeys[key])
                     ok(false, "menu " + item.id + " has same accesskey as " + accessKeys[key]);
                 else
                     accessKeys[key] = item.id;
             }
             items.push(item.id);
             items.push(!item.disabled);
@@ -237,16 +238,17 @@ let lastElementSelector = null;
  *                 the element, optional
  *        offsetY: vertical mouse offset from the top-left corner of the
  *                 element, optional
  *        centered: if true, mouse position is centered in element, defaults
  *                  to true if offsetX and offsetY are not provided
  *        waitForSpellCheck: wait until spellcheck is initialized before
  *                           starting test
  *        preCheckContextMenuFn: callback to run before opening menu
+ *        onContextMenuShown: callback to run when the context menu is shown
  *        postCheckContextMenuFn: callback to run after opening menu
  * @return {Promise} resolved after the test finishes
  */
 function* test_contextmenu(selector, menuItems, options={}) {
   contextMenu = document.getElementById("contentAreaContextMenu");
   is(contextMenu.state, "closed", "checking if popup is closed");
 
   // Default to centered if no positioning is defined.
@@ -290,16 +292,21 @@ function* test_contextmenu(selector, men
       button: 2,
       shiftkey: options.shiftkey,
       centered: options.centered
     },
     gBrowser.selectedBrowser);
   yield awaitPopupShown;
   info("Popup Shown");
 
+  if (options.onContextMenuShown) {
+    yield options.onContextMenuShown();
+    info("Completed onContextMenuShown");
+  }
+
   if (menuItems) {
     if (Services.prefs.getBoolPref("devtools.inspector.enabled")) {
       let inspectItems = ["---", null,
                           "context-inspect", true];
       menuItems = menuItems.concat(inspectItems);
     }
 
     checkContextMenu(menuItems);
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -56,25 +56,33 @@ function whenDelayedStartupFinished(aWin
   Services.obs.addObserver(function observer(aSubject, aTopic) {
     if (aWindow == aSubject) {
       Services.obs.removeObserver(observer, aTopic);
       executeSoon(aCallback);
     }
   }, "browser-delayed-startup-finished", false);
 }
 
-function updateTabContextMenu(tab) {
+function updateTabContextMenu(tab, onOpened) {
   let menu = document.getElementById("tabContextMenu");
   if (!tab)
     tab = gBrowser.selectedTab;
   var evt = new Event("");
   tab.dispatchEvent(evt);
   menu.openPopup(tab, "end_after", 0, 0, true, false, evt);
   is(TabContextMenu.contextTab, tab, "TabContextMenu context is the expected tab");
-  menu.hidePopup();
+  const onFinished = () => menu.hidePopup();
+  if (onOpened) {
+    return Task.spawn(function*() {
+      yield onOpened();
+      onFinished();
+    });
+  } else {
+    onFinished();
+  }
 }
 
 function openToolbarCustomizationUI(aCallback, aBrowserWin) {
   if (!aBrowserWin)
     aBrowserWin = window;
 
   aBrowserWin.gCustomizeMode.enter();
 
@@ -1154,8 +1162,30 @@ function getCertExceptionDialog(aLocatio
 
       if (childDoc.location.href == aLocation) {
         return childDoc;
       }
     }
   }
 }
 
+function setupRemoteClientsFixture(fixture) {
+  let oldRemoteClientsGetter =
+    Object.getOwnPropertyDescriptor(gFxAccounts, "remoteClients").get;
+
+  Object.defineProperty(gFxAccounts, "remoteClients", {
+    get: function() { return fixture; }
+  });
+  return oldRemoteClientsGetter;
+}
+
+function restoreRemoteClients(getter) {
+  Object.defineProperty(gFxAccounts, "remoteClients", {
+    get: getter
+  });
+}
+
+function* openMenuItemSubmenu(id) {
+  let menuPopup = document.getElementById(id).menupopup;
+  let menuPopupPromise = BrowserTestUtils.waitForEvent(menuPopup, "popupshown");
+  menuPopup.showPopup();
+  yield menuPopupPromise;
+}
--- a/browser/base/content/web-panels.xul
+++ b/browser/base/content/web-panels.xul
@@ -19,16 +19,17 @@
 <page id="webpanels-window"
         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         onload="load()" onunload="unload()">
   <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
+  <script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/>
   <script type="application/javascript" src="chrome://global/content/inlineSpellCheckUI.js"/>
   <script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/>
   <script type="application/javascript" src="chrome://browser/content/web-panels.js"/>
 
   <stringbundleset id="stringbundleset"> 
     <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/>
   </stringbundleset>
 
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -74,16 +74,17 @@ BrowserAction.prototype = {
         let view = document.getElementById(this.viewId);
         if (view) {
           view.remove();
         }
       },
 
       onCreated: node => {
         node.classList.add("badged-button");
+        node.classList.add("webextension-browser-action");
         node.setAttribute("constrain-size", "true");
 
         this.updateButton(node, this.defaults);
       },
 
       onViewShowing: event => {
         let document = event.target.ownerDocument;
         let tabbrowser = document.defaultView.gBrowser;
@@ -145,32 +146,44 @@ BrowserAction.prototype = {
         color = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
       }
       badgeNode.style.backgroundColor = color || "";
     }
 
     const LEGACY_CLASS = "toolbarbutton-legacy-addon";
     node.classList.remove(LEGACY_CLASS);
 
-
-    let win = node.ownerDocument.defaultView;
-    let {icon, size} = IconDetails.getURL(tabData.icon, win, this.extension);
+    let baseSize = 16;
+    let {icon, size} = IconDetails.getPreferredIcon(tabData.icon, this.extension, baseSize);
 
     // If the best available icon size is not divisible by 16, check if we have
     // an 18px icon to fall back to, and trim off the padding instead.
     if (size % 16 && !icon.endsWith(".svg")) {
-      let result = IconDetails.getURL(tabData.icon, win, this.extension, 18);
+      let result = IconDetails.getPreferredIcon(tabData.icon, this.extension, 18);
 
       if (result.size % 18 == 0) {
+        baseSize = 18;
         icon = result.icon;
         node.classList.add(LEGACY_CLASS);
       }
     }
 
-    node.setAttribute("image", icon);
+    // These URLs should already be properly escaped, but make doubly sure CSS
+    // string escape characters are escaped here, since they could lead to a
+    // sandbox break.
+    let escape = str => str.replace(/[\\\s"]/g, encodeURIComponent);
+
+    let getIcon = size => escape(IconDetails.getPreferredIcon(tabData.icon, this.extension, size).icon);
+
+    node.setAttribute("style", `
+      --webextension-menupanel-image: url("${getIcon(32)}");
+      --webextension-menupanel-image-2x: url("${getIcon(64)}");
+      --webextension-toolbar-image: url("${escape(icon)}");
+      --webextension-toolbar-image-2x: url("${getIcon(baseSize * 2)}");
+    `);
   },
 
   // Update the toolbar button for a given window.
   updateWindow(window) {
     let widget = this.widget.forWindow(window);
     if (widget) {
       let tab = window.gBrowser.selectedTab;
       this.updateButton(widget.node, this.tabContext.get(tab));
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -49,17 +49,22 @@ var gMenuBuilder = {
       rootElement.setAttribute("ext-type", "top-level-menu");
       rootElement = this.removeTopLevelMenuIfNeeded(rootElement);
 
       // Display the extension icon on the root element.
       if (root.extension.manifest.icons) {
         let parentWindow = contextData.menu.ownerDocument.defaultView;
         let extension = root.extension;
 
-        let {icon} = IconDetails.getURL(extension.manifest.icons, parentWindow, extension, 16 /* size */);
+        let {icon} = IconDetails.getPreferredIcon(extension.manifest.icons, extension,
+                                                  16 * parentWindow.devicePixelRatio);
+
+        // The extension icons in the manifest are not pre-resolved, since
+        // they're sometimes used by the add-on manager when the extension is
+        // not enabled, and its URLs are not resolvable.
         let resolvedURL = root.extension.baseURI.resolve(icon);
 
         if (rootElement.localName == "menu") {
           rootElement.setAttribute("class", "menu-iconic");
         } else if (rootElement.localName == "menuitem") {
           rootElement.setAttribute("class", "menuitem-iconic");
         }
         rootElement.setAttribute("image", resolvedURL);
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -87,18 +87,29 @@ PageAction.prototype = {
 
     if (tabData.show) {
       // Update the title and icon only if the button is visible.
 
       let title = tabData.title || this.extension.name;
       button.setAttribute("tooltiptext", title);
       button.setAttribute("aria-label", title);
 
-      let {icon} = IconDetails.getURL(tabData.icon, window, this.extension);
-      button.setAttribute("src", icon);
+      // These URLs should already be properly escaped, but make doubly sure CSS
+      // string escape characters are escaped here, since they could lead to a
+      // sandbox break.
+      let escape = str => str.replace(/[\\\s"]/g, encodeURIComponent);
+
+      let getIcon = size => escape(IconDetails.getPreferredIcon(tabData.icon, this.extension, size).icon);
+
+      button.setAttribute("style", `
+        --webextension-urlbar-image: url("${getIcon(16)}");
+        --webextension-urlbar-image-2x: url("${getIcon(32)}");
+      `);
+
+      button.classList.add("webextension-page-action");
     }
 
     button.hidden = !tabData.show;
   },
 
   // Create an |image| node and add it to the |urlbar-icons|
   // container in the given window.
   addButton(window) {
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -92,17 +92,17 @@ function* runTests(options) {
 
   function checkDetails(details) {
     let button = document.getElementById(browserActionId);
 
     ok(button, "button exists");
 
     let title = details.title || options.manifest.name;
 
-    is(button.getAttribute("image"), details.icon, "icon URL is correct");
+    is(getListStyleImage(button), details.icon, "icon URL is correct");
     is(button.getAttribute("tooltiptext"), title, "image title is correct");
     is(button.getAttribute("label"), title, "image label is correct");
     is(button.getAttribute("badge"), details.badge, "badge text is correct");
     is(button.getAttribute("disabled") == "true", Boolean(details.disabled), "disabled state is correct");
 
     if (details.badge && details.badgeBackgroundColor) {
       let badge = button.ownerDocument.getAnonymousElementByAttribute(
         button, "class", "toolbarbutton-badge");
@@ -158,16 +158,21 @@ add_task(function* testTabSwitchContext(
           "description": "Popup",
         },
 
         "title": {
           "message": "Title",
           "description": "Title",
         },
       },
+
+      "default.png": imageBuffer,
+      "default-2.png": imageBuffer,
+      "1.png": imageBuffer,
+      "2.png": imageBuffer,
     },
 
     getTests(tabs, expectDefaults) {
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default Title",
          "badge": "",
@@ -317,16 +322,20 @@ add_task(function* testDefaultTitle() {
 
       "browser_action": {
         "default_icon": "icon.png",
       },
 
       "permissions": ["tabs"],
     },
 
+    files: {
+      "icon.png": imageBuffer,
+    },
+
     getTests(tabs, expectDefaults) {
       let details = [
         {"title": "Foo Extension",
          "popup": "",
          "badge": "",
          "badgeBackgroundColor": null,
          "icon": browser.runtime.getURL("icon.png")},
         {"title": "Foo Title",
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
@@ -47,16 +47,22 @@ add_task(function* testDetailsObjects() 
         resolutions: {
           "1": browser.runtime.getURL("data/a.png"),
           "2": browser.runtime.getURL("data/a.png")}},
       {details: {"path": {"19": "a.png", "38": "a-x2.png"}},
         resolutions: {
           "1": browser.runtime.getURL("data/a.png"),
           "2": browser.runtime.getURL("data/a-x2.png")}},
 
+      // Test that CSS strings are escaped properly.
+      {details: {"path": 'a.png#" \\'},
+        resolutions: {
+          "1": browser.runtime.getURL("data/a.png#%22%20%5C"),
+          "2": browser.runtime.getURL("data/a.png#%22%20%5C")}},
+
       // Only ImageData objects.
       {details: {"imageData": imageData.red.imageData},
         resolutions: {
           "1": imageData.red.url,
           "2": imageData.red.url}},
       {details: {"imageData": {"19": imageData.red.imageData}},
         resolutions: {
           "1": imageData.red.url,
@@ -131,26 +137,33 @@ add_task(function* testDetailsObjects() 
         "6": "6.png",
         "18": "18.png",
         "36": "36.png",
         "48": "48.png",
         "128": "128.png"}},
         legacy: true,
         resolutions: {
           "1": browser.runtime.getURL("data/18.png"),
-          "2": browser.runtime.getURL("data/36.png")}},
+          "2": browser.runtime.getURL("data/36.png")},
+        menuResolutions: {
+          "1": browser.runtime.getURL("data/36.png"),
+          "2": browser.runtime.getURL("data/128.png")}},
       {details: {"path": {
         "16": "16.png",
         "18": "18.png",
         "32": "32.png",
         "48": "48.png",
+        "64": "64.png",
         "128": "128.png"}},
         resolutions: {
           "1": browser.runtime.getURL("data/16.png"),
-          "2": browser.runtime.getURL("data/32.png")}},
+          "2": browser.runtime.getURL("data/32.png")},
+        menuResolutions: {
+          "1": browser.runtime.getURL("data/32.png"),
+          "2": browser.runtime.getURL("data/64.png")}},
       {details: {"path": {
         "18": "18.png",
         "32": "32.png",
         "48": "48.png",
         "128": "128.png"}},
         resolutions: {
           "1": browser.runtime.getURL("data/32.png"),
           "2": browser.runtime.getURL("data/32.png")}},
@@ -162,42 +175,44 @@ add_task(function* testDetailsObjects() 
     let tabId;
 
     browser.test.onMessage.addListener((msg, test) => {
       if (msg != "setIcon") {
         browser.test.fail("expecting 'setIcon' message");
       }
 
       let details = iconDetails[test.index];
-      let expectedURL = details.resolutions[test.resolution];
 
       let detailString = JSON.stringify(details);
-      browser.test.log(`Setting browerAction/pageAction to ${detailString} expecting URL ${expectedURL}`);
+      browser.test.log(`Setting browerAction/pageAction to ${detailString} expecting URLs ${JSON.stringify(details.resolutions)}`);
 
       browser.browserAction.setIcon(Object.assign({tabId}, details.details));
       browser.pageAction.setIcon(Object.assign({tabId}, details.details));
 
-      browser.test.sendMessage("imageURL", [expectedURL, !!details.legacy]);
+      browser.test.sendMessage("iconSet");
     });
 
     // Generate a list of tests and resolutions to send back to the test
     // context.
     //
     // This process is a bit convoluted, because the outer test context needs
     // to handle checking the button nodes and changing the screen resolution,
     // but it can't pass us icon definitions with ImageData objects. This
     // shouldn't be a problem, since structured clones should handle ImageData
     // objects without issue. Unfortunately, |cloneInto| implements a slightly
     // different algorithm than we use in web APIs, and does not handle them
     // correctly.
     let tests = [];
     for (let [idx, icon] of iconDetails.entries()) {
-      for (let res of Object.keys(icon.resolutions)) {
-        tests.push({index: idx, resolution: Number(res)});
-      }
+      tests.push({
+        index: idx,
+        legacy: !!icon.legacy,
+        menuResolutions: icon.menuResolutions,
+        resolutions: icon.resolutions,
+      });
     }
 
     // Sort by resolution, so we don't needlessly switch back and forth
     // between each test.
     tests.sort(test => test.resolution);
 
     browser.tabs.query({active: true, currentWindow: true}, tabs => {
       tabId = tabs[0].id;
@@ -214,45 +229,96 @@ add_task(function* testDetailsObjects() 
       "background": {
         "page": "data/background.html",
       }
     },
 
     files: {
       "data/background.html": `<script src="background.js"></script>`,
       "data/background.js": background,
+
+      "data/16.svg": imageBuffer,
+      "data/18.svg": imageBuffer,
+
+      "data/16.png": imageBuffer,
+      "data/18.png": imageBuffer,
+      "data/32.png": imageBuffer,
+      "data/36.png": imageBuffer,
+      "data/48.png": imageBuffer,
+      "data/64.png": imageBuffer,
+      "data/128.png": imageBuffer,
+
+      "a.png": imageBuffer,
+      "data/2.png": imageBuffer,
+      "data/100.png": imageBuffer,
+      "data/a.png": imageBuffer,
+      "data/a-x2.png": imageBuffer,
     },
   });
 
   const RESOLUTION_PREF = "layout.css.devPixelsPerPx";
-  registerCleanupFunction(() => {
-    SpecialPowers.clearUserPref(RESOLUTION_PREF);
-  });
+
+  let pageActionId = makeWidgetId(extension.id) + "-page-action";
+  let browserActionWidget = getBrowserActionWidget(extension);
 
-  let browserActionId = makeWidgetId(extension.id) + "-browser-action";
-  let pageActionId = makeWidgetId(extension.id) + "-page-action";
 
-  let [, tests] = yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+  yield extension.startup();
+  let tests = yield extension.awaitMessage("ready");
 
   for (let test of tests) {
-    SpecialPowers.setCharPref(RESOLUTION_PREF, String(test.resolution));
-    is(window.devicePixelRatio, test.resolution, "window has the required resolution");
+    extension.sendMessage("setIcon", test);
+    yield extension.awaitMessage("iconSet");
+
+    let browserActionButton = browserActionWidget.forWindow(window).node;
+    let pageActionImage = document.getElementById(pageActionId);
+
+
+    // Test icon sizes in the toolbar/urlbar.
+    for (let resolution of Object.keys(test.resolutions)) {
+      yield SpecialPowers.pushPrefEnv({set: [[RESOLUTION_PREF, resolution]]});
 
-    extension.sendMessage("setIcon", test);
+      is(window.devicePixelRatio, +resolution, "window has the required resolution");
+
+      let imageURL = test.resolutions[resolution];
+      is(getListStyleImage(browserActionButton), imageURL, `browser action has the correct image at ${resolution}x resolution`);
+      is(getListStyleImage(pageActionImage), imageURL, `page action has the correct image at ${resolution}x resolution`);
 
-    let [imageURL, legacy] = yield extension.awaitMessage("imageURL");
+      let isLegacy = browserActionButton.classList.contains("toolbarbutton-legacy-addon");
+      is(isLegacy, test.legacy, "Legacy class should be present?");
+
+      yield SpecialPowers.popPrefEnv();
+    }
 
-    let browserActionButton = document.getElementById(browserActionId);
-    is(browserActionButton.getAttribute("image"), imageURL, "browser action has the correct image");
+    if (!test.menuResolutions) {
+      continue;
+    }
+
+
+    // Test icon sizes in the menu panel.
+    CustomizableUI.addWidgetToArea(browserActionWidget.id,
+                                   CustomizableUI.AREA_PANEL);
+
+    yield showBrowserAction(extension);
+    browserActionButton = browserActionWidget.forWindow(window).node;
 
-    let isLegacy = browserActionButton.classList.contains("toolbarbutton-legacy-addon");
-    is(isLegacy, legacy, "Legacy class should be present?");
+    for (let resolution of Object.keys(test.menuResolutions)) {
+      yield SpecialPowers.pushPrefEnv({set: [[RESOLUTION_PREF, resolution]]});
+
+      is(window.devicePixelRatio, +resolution, "window has the required resolution");
+
+      let imageURL = test.menuResolutions[resolution];
+      is(getListStyleImage(browserActionButton), imageURL, `browser action has the correct menu image at ${resolution}x resolution`);
 
-    let pageActionImage = document.getElementById(pageActionId);
-    is(pageActionImage.src, imageURL, "page action has the correct image");
+      yield SpecialPowers.popPrefEnv();
+    }
+
+    yield closeBrowserAction(extension);
+
+    CustomizableUI.addWidgetToArea(browserActionWidget.id,
+                                   CustomizableUI.AREA_NAVBAR);
   }
 
   yield extension.unload();
 });
 
 // Test that an error is thrown when providing invalid icon sizes
 add_task(function* testInvalidIconSizes() {
   let extension = ExtensionTestUtils.loadExtension({
@@ -336,31 +402,36 @@ add_task(function* testDefaultDetails() 
       background: function() {
         browser.tabs.query({active: true, currentWindow: true}, tabs => {
           let tabId = tabs[0].id;
 
           browser.pageAction.show(tabId).then(() => {
             browser.test.sendMessage("ready");
           });
         });
-      }
+      },
+
+      files: {
+        "foo/bar.png": imageBuffer,
+        "baz/quux.png": imageBuffer,
+      },
     });
 
     yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
 
     let browserActionId = makeWidgetId(extension.id) + "-browser-action";
     let pageActionId = makeWidgetId(extension.id) + "-page-action";
 
     let browserActionButton = document.getElementById(browserActionId);
-    let image = browserActionButton.getAttribute("image");
+    let image = getListStyleImage(browserActionButton);
 
     ok(expectedURL.test(image), `browser action image ${image} matches ${expectedURL}`);
 
     let pageActionImage = document.getElementById(pageActionId);
-    image = pageActionImage.src;
+    image = getListStyleImage(pageActionImage);
 
     ok(expectedURL.test(image), `page action image ${image} matches ${expectedURL}`);
 
     yield extension.unload();
 
     let node = document.getElementById(pageActionId);
     is(node, null, "pageAction image removed from document");
   }
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -93,17 +93,17 @@ function* runTests(options) {
 
   function checkDetails(details) {
     let image = currentWindow.document.getElementById(pageActionId);
     if (details == null) {
       ok(image == null || image.hidden, "image is hidden");
     } else {
       ok(image, "image exists");
 
-      is(image.src, details.icon, "icon URL is correct");
+      is(getListStyleImage(image), details.icon, "icon URL is correct");
 
       let title = details.title || options.manifest.name;
       is(image.getAttribute("tooltiptext"), title, "image title is correct");
       is(image.getAttribute("aria-label"), title, "image aria-label is correct");
       // TODO: Popup URL.
     }
   }
 
@@ -172,16 +172,20 @@ add_task(function* testTabSwitchContext(
           "description": "Popup",
         },
 
         "title": {
           "message": "Title",
           "description": "Title",
         },
       },
+
+      "default.png": imageBuffer,
+      "1.png": imageBuffer,
+      "2.png": imageBuffer,
     },
 
     getTests(tabs) {
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default Title \u263a"},
         {"icon": browser.runtime.getURL("1.png"),
@@ -304,16 +308,20 @@ add_task(function* testDefaultTitle() {
 
       "page_action": {
         "default_icon": "icon.png",
       },
 
       "permissions": ["tabs"],
     },
 
+    files: {
+      "icon.png": imageBuffer,
+    },
+
     getTests(tabs) {
       let details = [
         {"title": "Foo Extension",
          "popup": "",
          "icon": browser.runtime.getURL("icon.png")},
         {"title": "Foo Title",
          "popup": "",
          "icon": browser.runtime.getURL("icon.png")},
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -5,16 +5,17 @@
 /* exported CustomizableUI makeWidgetId focusWindow forceGC
  *          getBrowserActionWidget
  *          clickBrowserAction clickPageAction
  *          getBrowserActionPopup getPageActionPopup
  *          closeBrowserAction closePageAction
  *          promisePopupShown promisePopupHidden
  *          openContextMenu closeContextMenu
  *          openExtensionContextMenu closeExtensionContextMenu
+ *          imageBuffer getListStyleImage
  */
 
 var {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm");
 var {CustomizableUI} = Cu.import("resource:///modules/CustomizableUI.jsm");
 
 // Bug 1239884: Our tests occasionally hit a long GC pause at unpredictable
 // times in debug builds, which results in intermittent timeouts. Until we have
 // a better solution, we force a GC after certain strategic tests, which tend to
@@ -42,16 +43,27 @@ var focusWindow = Task.async(function* f
       resolve();
     }, true);
   });
 
   win.focus();
   yield promise;
 });
 
+let img = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==";
+var imageBuffer = Uint8Array.from(atob(img), byte => byte.charCodeAt(0)).buffer;
+
+function getListStyleImage(button) {
+  let style = button.ownerDocument.defaultView.getComputedStyle(button);
+
+  let match = /^url\("(.*)"\)$/.exec(style.listStyleImage);
+
+  return match && match[1];
+}
+
 function promisePopupShown(popup) {
   return new Promise(resolve => {
     if (popup.state == "open") {
       resolve();
     } else {
       let onPopupShown = event => {
         popup.removeEventListener("popupshown", onPopupShown);
         resolve();
@@ -79,25 +91,31 @@ function getBrowserActionPopup(extension
   let group = getBrowserActionWidget(extension);
 
   if (group.areaType == CustomizableUI.TYPE_TOOLBAR) {
     return win.document.getElementById("customizationui-widget-panel");
   }
   return win.PanelUI.panel;
 }
 
-var clickBrowserAction = Task.async(function* (extension, win = window) {
+var showBrowserAction = Task.async(function* (extension, win = window) {
   let group = getBrowserActionWidget(extension);
   let widget = group.forWindow(win);
 
   if (group.areaType == CustomizableUI.TYPE_TOOLBAR) {
     ok(!widget.overflowed, "Expect widget not to be overflowed");
   } else if (group.areaType == CustomizableUI.TYPE_MENU_PANEL) {
     yield win.PanelUI.show();
   }
+});
+
+var clickBrowserAction = Task.async(function* (extension, win = window) {
+  yield showBrowserAction(extension, win);
+
+  let widget = getBrowserActionWidget(extension).forWindow(win);
 
   EventUtils.synthesizeMouseAtCenter(widget.node, {}, win);
 });
 
 function closeBrowserAction(extension, win = window) {
   let group = getBrowserActionWidget(extension);
 
   let node = win.document.getElementById(group.viewId);
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -120,17 +120,17 @@ const CLOSED_MESSAGES = new Set([
   "SessionStore:update",
 
   // For a description see above.
   "SessionStore:error",
 ]);
 
 // These are tab events that we listen to.
 const TAB_EVENTS = [
-  "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
+  "TabOpen", "TabBrowserCreated", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
   "TabUnpinned"
 ];
 
 const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 Cu.import("resource://gre/modules/Services.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", this);
@@ -889,17 +889,20 @@ var SessionStoreInternal = {
   /**
    * Implement nsIDOMEventListener for handling various window and tab events
    */
   handleEvent: function ssi_handleEvent(aEvent) {
     let win = aEvent.currentTarget.ownerDocument.defaultView;
     let target = aEvent.originalTarget;
     switch (aEvent.type) {
       case "TabOpen":
-        this.onTabAdd(win, target);
+        this.onTabAdd(win);
+        break;
+      case "TabBrowserCreated":
+        this.onTabBrowserCreated(win, target);
         break;
       case "TabClose":
         // `adoptedBy` will be set if the tab was closed because it is being
         // moved to a new window.
         if (!aEvent.detail.adoptedBy)
           this.onTabClose(win, target);
         this.onTabRemove(win, target);
         break;
@@ -981,17 +984,17 @@ var SessionStoreInternal = {
       this._windows[aWindow.__SSi]._restoring = true;
     if (!aWindow.toolbar.visible)
       this._windows[aWindow.__SSi].isPopup = true;
 
     let tabbrowser = aWindow.gBrowser;
 
     // add tab change listeners to all already existing tabs
     for (let i = 0; i < tabbrowser.tabs.length; i++) {
-      this.onTabAdd(aWindow, tabbrowser.tabs[i], true);
+      this.onTabBrowserCreated(aWindow, tabbrowser.tabs[i]);
     }
     // notification of tab add/remove/selection/show/hide
     TAB_EVENTS.forEach(function(aEvent) {
       tabbrowser.tabContainer.addEventListener(aEvent, this, true);
     }, this);
 
     // Keep track of a browser's latest frameLoader.
     aWindow.gBrowser.addEventListener("XULFrameLoaderCreated", this);
@@ -1675,36 +1678,39 @@ var SessionStoreInternal = {
       case "sessionstore.max_windows_undo":
         this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
         this._capClosedWindows();
         break;
     }
   },
 
   /**
+   * save state when new tab is added
+   * @param aWindow
+   *        Window reference
+   */
+  onTabAdd: function ssi_onTabAdd(aWindow) {
+    this.saveStateDelayed(aWindow);
+  },
+
+  /**
    * set up listeners for a new tab
    * @param aWindow
    *        Window reference
    * @param aTab
    *        Tab reference
-   * @param aNoNotification
-   *        bool Do not save state if we're updating an existing tab
    */
-  onTabAdd: function ssi_onTabAdd(aWindow, aTab, aNoNotification) {
+  onTabBrowserCreated: function ssi_onTabBrowserCreated(aWindow, aTab) {
     let browser = aTab.linkedBrowser;
     browser.addEventListener("SwapDocShells", this);
     browser.addEventListener("oop-browser-crashed", this);
 
     if (browser.frameLoader) {
       this._lastKnownFrameLoader.set(browser.permanentKey, browser.frameLoader);
     }
-
-    if (!aNoNotification) {
-      this.saveStateDelayed(aWindow);
-    }
   },
 
   /**
    * remove listeners for a tab
    * @param aWindow
    *        Window reference
    * @param aTab
    *        Tab reference
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,3 +1,3 @@
 This is the pdf.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 1.5.322
+Current extension version is: 1.5.337
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf', ['exports
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdf = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.322';
-var pdfjsBuild = 'b6826a4';
+var pdfjsVersion = '1.5.337';
+var pdfjsBuild = '11381cd';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -1217,20 +1217,17 @@ var StatTimer = (function StatTimerClosu
   };
   return StatTimer;
 })();
 
 var createBlob = function createBlob(data, contentType) {
   if (typeof Blob !== 'undefined') {
     return new Blob([data], { type: contentType });
   }
-  // Blob builder is deprecated in FF14 and removed in FF18.
-  var bb = new MozBlobBuilder();
-  bb.append(data);
-  return bb.getBlob(contentType);
+  warn('The "Blob" constructor is not supported.');
 };
 
 var createObjectURL = (function createObjectURLClosure() {
   // Blob/createObjectURL is not available, falling back to data schema.
   var digits =
     'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
 
   return function createObjectURL(data, contentType, forceDataSchema) {
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf.worker', ['
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdfWorker = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.322';
-var pdfjsBuild = 'b6826a4';
+var pdfjsVersion = '1.5.337';
+var pdfjsBuild = '11381cd';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -3238,20 +3238,17 @@ var StatTimer = (function StatTimerClosu
   };
   return StatTimer;
 })();
 
 var createBlob = function createBlob(data, contentType) {
   if (typeof Blob !== 'undefined') {
     return new Blob([data], { type: contentType });
   }
-  // Blob builder is deprecated in FF14 and removed in FF18.
-  var bb = new MozBlobBuilder();
-  bb.append(data);
-  return bb.getBlob(contentType);
+  warn('The "Blob" constructor is not supported.');
 };
 
 var createObjectURL = (function createObjectURLClosure() {
   // Blob/createObjectURL is not available, falling back to data schema.
   var digits =
     'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
 
   return function createObjectURL(data, contentType, forceDataSchema) {
@@ -27031,17 +27028,17 @@ function recoverGlyphName(name, glyphsUn
   var unicode = getUnicodeForGlyph(name, glyphsUnicodeMap);
   if (unicode !== -1) {
     for (var key in glyphsUnicodeMap) {
       if (glyphsUnicodeMap[key] === unicode) {
         return key;
       }
     }
   }
-  warn('Unable to recover a standard glyph name for: ' + name);
+  info('Unable to recover a standard glyph name for: ' + name);
   return name;
 }
 
 var Glyph = (function GlyphClosure() {
   function Glyph(fontChar, unicode, accent, width, vmetric, operatorListId,
                  isSpace, isInFont) {
     this.fontChar = fontChar;
     this.unicode = unicode;
index eb5ccb5ec3cdf7f20c344483958a2684e11e6df5..12bae83a910277a30f57e8e30a5a4ed2f17e2c37
GIT binary patch
literal 2417
zc$}S<`9Bkk1IOnaW{$8iGq>4>OiamK)66yGYVJF7geJondK^pQiyV=V%6;W3=Bm*1
zLLr${&Jgh+PjWs_&%f~fyk76m``6EpAEqtY3@iW>0001BbCT)h-!}RWKCa*Wpd;z~
zZ#CFjIb8Z}H8nL!Nl7g&Ee#C~adB~FWo0-VuC1+YXlRH;B4IF?rlzL6yu7TeEDD9f
z;c$wIiWe?i0E597FJ6Q|Ab339*w~mzB#MfPT3A?+NF-rlVGsxe1OlzBtoZr)O-)U?
zxw*+?va_?Zot>SBhX*Gor=z2zo12@xy*-6O@$&NW@$m@?3JMJk_4D(Kh=>RX2)KUz
z`pug+X*61JaBxaW3YAKYjg3Wa%={h<(4$@^+YA5SN$@C>1pxTYo0}Rt&^S^Gc=Gp1
zXKpDLjgL5{R-`iW*G{3cW?ZAl?Pn&3mE2V=aX~<X9-{``hy;ZRF>)}6(Fo0Q^RkVO
z#RwKhjTRXqs2}d>zRKIZHj62wbw`yC)xSSvXNE}?C^$cdKeUDs?p?Y!PSw66<ckBP
zb9DN>4)41*r72GWz+-?RgDcHrf<J$_8$fEg96LZ3QbAXYPF4_00^}RIKr}A09Bg48
zB)pm<Rt(hN6cKp$gb~)z9O!2p8y8tmJ0h@p9S~9KEYs@?0zS8PtHmYG%GO=Ld_HK?
z@29zzTE4lj!BOsjoaZUB8BMoi#S$kCA2n=?@^Vdx&>Clt-qI^(r9aE}-BB`HSdbu#
zjFgq%^1XXo9Jxk6Uqr!HvTsaxZq~fUPkmhJyJGM3pg@ZB)Eg3o8O(9D6>4Kn$25k(
zYcwK+9tVgb3Tq77jM9!4RpJrmpYw8D>&ggQN=a@@&`8TWwys>-j?$oUjMI>PXOltk
z#4v~&&XvonrGCsT9ox|K40LZ|z!Eqdx4-TvtsAS>SiH59S$a%OnQZvB>|F`?A9r5s
z(ASdG!GX5WV<{=^k`8+5KRSy5xBVxJnThl37|G*(mIW|0X{rv0?KE0AK+|Wvot#%K
z%UC`9u4a11XW)K*_gJ%w0+tjf&;U4T<9X%l^s<u3Be!kv{9JdHO-(@0vO&J?heQk-
z>$;FV6!-HG!OxLUTHxV>L}oZeJh0P$08+sSZqS}Vnm;9n0!C9MdVQ#>3f5Q8LQ+&_
zb26g35M=jU0le8>rBpAktROo0iYo^!irpG>_nbmD>Z)Tf@f#xcVR)6Gg8ydc1>Eh4
zH7lP`*(<6I%GFhXxL|P&|C;NHPHziEVoOy-6<SoLdJ=cZ_@MHoO9WG{$^pK&clU}v
z4b(m2K~3Ha|0v84zUoMg9#VPKh{+js+<u<>O{eTolA={p@;DjavkMeFlXdn>sYT+B
zC)2cUUQltM+$C~7ymNxN)GgT5m@xAY(=++B<c=h(wdP4^)8Dm1tp_$7Sz=HM?;mMR
z6ia=Ly~_FOyo~es{f0i9$>vAOl`Y|kVeWHoN>;)-$+;FYGB3g&EA!4Fj1-#^NVyx;
zo)ml!XW_4dh;ykZTXA>Z$Y*kB2iAM<6Eho?om#ElhR?U0isWA8DOa2rR3GxUd39M%
zWW5qnQZ9yX)xuuF#p3nHl83DAg!8lRlthfP!|J6uPtT3aT=)@SL3mlsZD2L>#(xbR
z(VJce_$-<mAM}1+#GMBp)BWc+r5l%`jM2^20s|GSDjS0m_`5H6mJMVkW|@j6Z73m|
z^hHj6jVlG)sMWC?1~mOI9km#gs6i&on9#ir&rTv=lPm~(Rqf>soHn7ngOI;6t)bJW
zAN>As-|%ed;NZgzXX!6abR{&xgx<6er)ym6v5=8&U#~>4CzSrWDeqw^5vrHZWY;q_
zD@4h9LxU6A>6n&lft5ASdGDSK9!OqQpNRS%i5s=Er2oakl32J$u3mMaYwsV0^Qi#j
zJ_iLQ;eB^B;M*R5r=sWwW)~O#&R5i=H6EtbRzK{-L+aNg&=@yonM|JJ-SGjKcEq<~
zMLN|G*{>dBW6y`q$l78QBYuff865tF-%Q%AM>%hsxzDLcW}bjl7xvkgmu}u&725X-
z5A^IPouCXK?z0ml;(q-p*DEckmaqBaq<4>6;qG-0ljcX>iN4i8*9t0f_H;QVE100G
z2K=Ex|E!g%vmZ1U_1mimCJmJ_1Bcrx>{USp!KmQ)FLl5_*-Wg<<h{P^AiGFg--ojf
zQQ>1EBB9AiS^Nykbdf10JH)rFe4(XdOI?0Gw}(F2VGkGe<@F-+VI^Hg=JwQ&B@m9@
zA=%qkGJ9c8hc65^87V_ox0BwkuQx_TtKm&6n4NrTsNMRa>D~`A-Q{WeV;f>pj}JBz
z7ocH!oY&WO_~YlXr|mRrvZx7x?-d9g2%p*A`0L|FPKf6((1#d`2STgg2IQx6Het&4
z2$K3%JVmKzPW9*M36aCBL_&LdMWxlM3G<|g08Fb!`932^T#hjC92cPlySV)vE9D6{
zlecWv{n|xkRNit}{sQU}EGA*;m0a}p42R^-^)Q8=?u7P?(-*aJ(s6k!jEohXuP`U^
z(fjosPbmb`uJ`VbmPQL@I0}D$C&qsoNKBI?N^5xye=^(aAfY!p8aTO*H$9=ai$o#a
zn>?lWpViggKGd>i*TP?cjbHmq>@jvAHKgah?aM69U6;A7=6>@ES%mo9M+YAywyc@n
zP7PT;C|Ub}eMFK-@q4VFkC?}woomcAB>s@HK0*d_;R@f6)(vcGbF<M_P)c#ZSboEp
z9CjXQx7XZYk$pQ4co(YLC$hmDw|Vt)6~2BOQpGDhwF*DX_4U#%nh@;fvCA=6><X(6
ztz7qznQ{Y1ckYzJEn^1xCFKc@f<`OXLfVs`V>x0a1J|~xn0QWio#?%GaBmD*9k9$f
z2QAR^?*9d9EmytYy*S$4fur{d4KE!<Wvi(jXKx8Q1ELpi<csp*nA2u)>_9BRt;s{@
z9PbxkR_wqjLT(p1b$oPS9>t%~*S`dWEA{d5HvO1s=h2f@XXU)}*nX|rBsOQ!t6F1I
z67lp|+`<GOjdUaUpPj6j){s_48h1SF*PHc#^Qt9EV~vEki~hsdPYzuC0>V51b@y4K
zm~KTLcOYqo6Ya#E<$%mHQ~da1(&H<v5Vxi846)7_T<Wm*P)sy**{#iI>2rN2_d(T<
Z7%JA_k&jlwe*aw&^GjsY1{1I3e*tEYGdlnP
index 306eb43b86861ffd83ab17541d64a4e8d240c86a..e50ca4eee46e25d92e06847004f323d616237f85
GIT binary patch
literal 107
zc%17D@N?(olHy`uVBq!ia0vp^>_9BQ!3HF6HKu+5QbwLGjv*C{$r2tP{{OG9Q;1_&
zbWt^;p+Z-{zWI{QiAmlEggExJ)R+qxP213Hu-B-8fx+-v^k(la_imtW22WQ%mvv4F
FO#tGFAX@+c
index b979e523e742027645e980d8a27c54d0060ebf21..4a5e2b8a3775facabbf7e93d8b9b1b30297632be
GIT binary patch
literal 859
zc$@)S1El<kP)<h;3K|Lk000e1NJLTq001BW001Be1ONa4*>kdg0009bNkl<Zcmdta
zLxAM~0E6MIZQHhO+qP}nwr$(C{l>O!qs?nO>`2G`bN+(=Af|T(S(@31n57R7<0pDb
zv*kV(PVYe$9;O?e1ONJb6Yc5BKP*8IY3C>D9Hdz{87<JX-$0k=bQGU6bIJ$Z_=Fv!
z`3X7&{u$mI`5>30PBZ^sbmXX%4tmg@4vsUTxntmOiRVB}nLn<lUGS4&B+ErR+Sos+
zQ(of7=@9s9L@(s{fOs27n{fSbB*$%OZGXRXqYqhy<FpI<F;wD?P;COO1HK0!Dc+sd
zv~;*Da~`8@z;~Gws$~g%LQA8=Mo5ZV(Zb<QnT8TSO6!1cu{;-Nu@ONqldzLkVVVg|
zCLj?PEotU(Tj57&74RjFi??axw@&8VL8}OZ9TSlaQ1fo01x@U3Df}=k0zMDx8N-EJ
zH1~Zwx}n6k(^AmeHw^_KrQ$TBk=;$13x{bQ@G&00NmJi9-8<LG&TTXo^iRw}K|p=b
zRWzlc{dJl15KRL<42t77XyW@y;ah1Y=$@L3!hrgq6OE~Ff2~ZzjhVTR#sTl*@#{45
zec4&zG!ZqY=Aa0mJ}53~sB3q%OhaU1XBw&aH5&Ln@6rj`gjF;aHRtE6I1nap4RwOw
zHEN1XzVO{n&5b#+{3`W*pUIrdXvDu)7NZ!{h0A-(zh5>Yi#??6;a}ZB3eP57p|1DS
zXl@|>xwZ%;kT8Pz`Hz3Db$te9qxmP|1)H#*aDh5Lj}^X@`uu%;NfI2egk?GY;;*~a
z@<CZq%hUWrBO3Fc=Y54Qp&ozUUx|`Ps^wUoPyEgw7IpF>dUdWZu{mdd@#VUTEfucA
z9}iccG?Fc|>vewjnSi)TqAu#895Qn;HT?c?WeWVQy!reZ_MEv1^(6j}Uth0B86;mF
zdF6c{@Vju11>@nYT*bfq@_HT0Bh@l39~}OgKg2Iw!R~CvPMjyc=5Ky}tMIgxcku}S
zhy2W6{6`&XB>Y8u|6sYo(^$Sr?Vhc2yC#0%M}7=@X>@SpAe2G6%8YVoT%t?Q{>}Sj
lZHt=7g$$J$Igkx$P79l;1h5EfFlGP%002ovPDHLkV1kE3p%MT9
index fb7db9383669cf734685ca7422bbab1032f486fd..a0208b41377f5e006b90f4c6f6432f0156c7cd0c
GIT binary patch
literal 219
zc$@*-03`p3P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80001@Nkl<ZcmeH}
zM;d`a5Cxxb7IxAJoR<S|7uiZ?CDTTH+evcQ_tmcJ&tah45s{j4W@$ZEp9CowSC-~M
zg8DSb0rqe<uup=Bqr{{C$OF&=I66u^dc6XlcL0;gq-lHk%xnuJuqQ=h+0s&Q|8XcH
zQZcqHt;Xt;AX($W(hNvYp9a~%9?pjKyaM_q)Sr>Ai1Yx4!(rbx9*stQkf2`I11D?x
VwzoOr^jH7@002ovPDHLkV1mA0TJZn?
index 1c8b9f7010cae8edd6ca3a1146c6f07f68f5931e..0496b3577555195f96498932403c030411769159
GIT binary patch
literal 143
zc%17D@N?(olHy`uVBq!ia0vp^oFL4>1|%O$WD@{VVV*9IAr*|t6(1k5>aAfpGT9*~
zXOfab4D%87#veSpl*$zRm>xL?oHRbsHKXB%c*k}H6$4SxhZAg?^X4iGtUtb-qb;)0
qg018G<FJ+kOb#)9&#VtUWMR17`s11Z)ixoZ4Gf;HelF{r5}E*k#W1e`
index 84279368d985d74ba324ea3433b08ea5974bcc62..6ad9ebcdf5df5a2aa8d39e01371fc8a5e4663db4
GIT binary patch
literal 167
zc%17D@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@RheJWm(LkP61+ijR+2cScBc{7$GT
z;z?kCWTIflbtL_S=?Sq8a|d1v=S7Yj`3fe<QZAFsPZ)DtmTuuydc%8&?co<TNw&nQ
zW})Uc4He3pni#)ISxsYnRF@^;a$@%v{|Od*y&D}{_IkfhYS~bpSi`{YeP_|4`&u?Y
Pn;AS^{an^LB{Ts58u&Pg
--- a/browser/extensions/pdfjs/content/web/viewer.js
+++ b/browser/extensions/pdfjs/content/web/viewer.js
@@ -6417,17 +6417,17 @@ var PDFViewer = (function pdfViewer() {
     },
 
     /**
      * @param val - The scale of the pages (in percent or predefined value).
      */
     set currentScaleValue(val) {
       if (!this.pdfDocument) {
         this._currentScale = isNaN(val) ? UNKNOWN_SCALE : val;
-        this._currentScaleValue = val;
+        this._currentScaleValue = val.toString();
         return;
       }
       this._setScale(val, false);
     },
 
     /**
      * @returns {number}
      */
@@ -6604,17 +6604,17 @@ var PDFViewer = (function pdfViewer() {
         presetValue: preset ? newValue : undefined
       };
       this.eventBus.dispatch('scalechanging', arg);
       this.eventBus.dispatch('scalechange', arg);
     },
 
     _setScaleUpdatePages: function pdfViewer_setScaleUpdatePages(
         newScale, newValue, noScroll, preset) {
-      this._currentScaleValue = newValue;
+      this._currentScaleValue = newValue.toString();
 
       if (isSameScale(this._currentScale, newScale)) {
         if (preset) {
           this._setScaleDispatchEvent(newScale, newValue, true);
         }
         return;
       }
 
@@ -7499,59 +7499,47 @@ var PDFViewerApplication = {
    * @param {string|TypedArray|ArrayBuffer} file - PDF location or binary data.
    * @param {Object} args - (optional) Additional arguments for the getDocument
    *                        call, e.g. HTTP headers ('httpHeaders') or
    *                        alternative data transport ('range').
    * @returns {Promise} - Returns the promise, which is resolved when document
    *                      is opened.
    */
   open: function pdfViewOpen(file, args) {
-    var scale = 0;
-    if (arguments.length > 2 || typeof args === 'number') {
-      console.warn('Call of open() with obsolete signature.');
-      if (typeof args === 'number') {
-        scale = args; // scale argument was found
-      }
-      args = arguments[4] || null;
-      if (arguments[3] && typeof arguments[3] === 'object') {
-        // The pdfDataRangeTransport argument is present.
-        args = Object.create(args);
-        args.range = arguments[3];
-      }
-      if (typeof arguments[2] === 'string') {
-        // The password argument is present.
-        args = Object.create(args);
-        args.password = arguments[2];
-      }
-    }
-
     if (this.pdfLoadingTask) {
       // We need to destroy already opened document.
       return this.close().then(function () {
         // Reload the preferences if a document was previously opened.
         Preferences.reload();
         // ... and repeat the open() call.
         return this.open(file, args);
       }.bind(this));
     }
 
-    var parameters = Object.create(null);
+    var parameters = Object.create(null), scale;
     if (typeof file === 'string') { // URL
       this.setTitleUsingUrl(file);
       parameters.url = file;
     } else if (file && 'byteLength' in file) { // ArrayBuffer
       parameters.data = file;
     } else if (file.url && file.originalUrl) {
       this.setTitleUsingUrl(file.originalUrl);
       parameters.url = file.url;
     }
     if (args) {
       for (var prop in args) {
         parameters[prop] = args[prop];
       }
+
+      if (args.scale) {
+        scale = args.scale;
+      }
+      if (args.length) {
+        this.pdfDocumentProperties.setFileSize(args.length);
+      }
     }
 
     var self = this;
     self.downloadComplete = false;
 
     var loadingTask = pdfjsLib.getDocument(parameters);
     this.pdfLoadingTask = loadingTask;
 
@@ -7562,17 +7550,17 @@ var PDFViewerApplication = {
 
     loadingTask.onProgress = function getDocumentProgress(progressData) {
       self.progress(progressData.loaded / progressData.total);
     };
 
     // Listen for unsupported features to trigger the fallback UI.
     loadingTask.onUnsupportedFeature = this.fallback.bind(this);
 
-    var result = loadingTask.promise.then(
+    return loadingTask.promise.then(
       function getDocumentCallback(pdfDocument) {
         self.load(pdfDocument, scale);
       },
       function getDocumentError(exception) {
         var message = exception && exception.message;
         var loadingErrorMessage = mozL10n.get('loading_error', null,
           'An error occurred while loading the PDF.');
 
@@ -7592,21 +7580,16 @@ var PDFViewerApplication = {
         var moreInfo = {
           message: message
         };
         self.error(loadingErrorMessage, moreInfo);
 
         throw new Error(loadingErrorMessage);
       }
     );
-
-    if (args && args.length) {
-      PDFViewerApplication.pdfDocumentProperties.setFileSize(args.length);
-    }
-    return result;
   },
 
   download: function pdfViewDownload() {
     function downloadByUrl() {
       downloadManager.downloadUrl(url, filename);
     }
 
     var url = this.url.split('#')[0];
@@ -7728,21 +7711,19 @@ var PDFViewerApplication = {
 
     this.pdfDocumentProperties.setDocumentAndUrl(pdfDocument, this.url);
 
     var downloadedPromise = pdfDocument.getDownloadInfo().then(function() {
       self.downloadComplete = true;
       self.loadingBar.hide();
     });
 
-    var pagesCount = pdfDocument.numPages;
-    var toolbarConfig = this.appConfig.toolbar;
-    toolbarConfig.numPages.textContent =
-      mozL10n.get('page_of', {pageCount: pagesCount}, 'of {{pageCount}}');
-    toolbarConfig.pageNumber.max = pagesCount;
+    this._updateUIToolbar({
+      resetNumPages: true,
+    });
 
     var id = this.documentFingerprint = pdfDocument.fingerprint;
     var store = this.store = new ViewHistory(id);
 
     var baseDocumentUrl = this.url.split('#')[0];
     this.pdfLinkService.setDocument(pdfDocument, baseDocumentUrl);
 
     var pdfViewer = this.pdfViewer;
@@ -7930,20 +7911,16 @@ var PDFViewerApplication = {
   },
 
   setInitialView: function pdfViewSetInitialView(storedHash, options) {
     var scale = options && options.scale;
     var sidebarView = options && options.sidebarView;
 
     this.isInitialViewSet = true;
 
-    // When opening a new file, when one is already loaded in the viewer,
-    // ensure that the 'pageNumber' element displays the correct value.
-    this.appConfig.toolbar.pageNumber.value = this.pdfViewer.currentPageNumber;
-
     this.pdfSidebar.setInitialView(this.preferenceSidebarViewOnLoad ||
                                    (sidebarView | 0));
 
     if (this.initialDestination) {
       this.pdfLinkService.navigateTo(this.initialDestination);
       this.initialDestination = null;
     } else if (this.initialBookmark) {
       this.pdfLinkService.setHash(this.initialBookmark);
@@ -8097,16 +8074,77 @@ var PDFViewerApplication = {
    */
   scrollPresentationMode: function pdfViewScrollPresentationMode(delta) {
     if (!this.pdfPresentationMode) {
       return;
     }
     this.pdfPresentationMode.mouseScroll(delta);
   },
 
+  /**
+   * @typedef UpdateUIToolbarParameters
+   * @property {number} pageNumber
+   * @property {string} scaleValue
+   * @property {scale} scale
+   * @property {boolean} resetNumPages
+   */
+
+  /**
+   * @param {Object} UpdateUIToolbarParameters
+   * @private
+   */
+  _updateUIToolbar: function (params) {
+    function selectScaleOption(value, scale) {
+      var options = toolbarConfig.scaleSelect.options;
+      var predefinedValueFound = false;
+      for (var i = 0, ii = options.length; i < ii; i++) {
+        var option = options[i];
+        if (option.value !== value) {
+          option.selected = false;
+          continue;
+        }
+        option.selected = true;
+        predefinedValueFound = true;
+      }
+      if (!predefinedValueFound) {
+        var customScale = Math.round(scale * 10000) / 100;
+        toolbarConfig.customScaleOption.textContent =
+          mozL10n.get('page_scale_percent', {scale: customScale}, '{{scale}}%');
+        toolbarConfig.customScaleOption.selected = true;
+      }
+    }
+
+    var pageNumber = params.pageNumber || this.pdfViewer.currentPageNumber;
+    var scaleValue = (params.scaleValue || params.scale ||
+      this.pdfViewer.currentScaleValue || DEFAULT_SCALE_VALUE).toString();
+    var scale = params.scale || this.pdfViewer.currentScale;
+    var resetNumPages = params.resetNumPages || false;
+
+    var toolbarConfig = this.appConfig.toolbar;
+    var pagesCount = this.pagesCount;
+
+    if (resetNumPages) {
+      toolbarConfig.numPages.textContent =
+        mozL10n.get('page_of', { pageCount: pagesCount }, 'of {{pageCount}}');
+      toolbarConfig.pageNumber.max = pagesCount;
+    }
+    toolbarConfig.pageNumber.value = pageNumber;
+
+    toolbarConfig.previous.disabled = (pageNumber <= 1);
+    toolbarConfig.next.disabled = (pageNumber >= pagesCount);
+
+    toolbarConfig.firstPage.disabled = (pageNumber <= 1);
+    toolbarConfig.lastPage.disabled = (pageNumber >= pagesCount);
+
+    toolbarConfig.zoomOut.disabled = (scale === MIN_SCALE);
+    toolbarConfig.zoomIn.disabled = (scale === MAX_SCALE);
+
+    selectScaleOption(scaleValue, scale);
+  },
+
   bindEvents: function pdfViewBindEvents() {
     var eventBus = this.eventBus;
 
     eventBus.on('resize', webViewerResize);
     eventBus.on('localized', webViewerLocalized);
     eventBus.on('hashchange', webViewerHashchange);
     eventBus.on('beforeprint', this.beforePrint.bind(this));
     eventBus.on('afterprint', this.afterPrint.bind(this));
@@ -8532,31 +8570,16 @@ function webViewerHashchange(e) {
       PDFViewerApplication.initialBookmark = hash;
     } else {
       PDFViewerApplication.pdfLinkService.setHash(hash);
     }
   }
 }
 
 
-function selectScaleOption(value) {
-  var options = PDFViewerApplication.appConfig.toolbar.scaleSelect.options;
-  var predefinedValueFound = false;
-  for (var i = 0, ii = options.length; i < ii; i++) {
-    var option = options[i];
-    if (option.value !== value) {
-      option.selected = false;
-      continue;
-    }
-    option.selected = true;
-    predefinedValueFound = true;
-  }
-  return predefinedValueFound;
-}
-
 window.addEventListener('localized', function localized(evt) {
   PDFViewerApplication.eventBus.dispatch('localized');
 });
 
 function webViewerLocalized() {
   document.getElementsByTagName('html')[0].dir = mozL10n.getDirection();
 
   PDFViewerApplication.animationStartedPromise.then(function() {
@@ -8632,52 +8655,37 @@ function webViewerFindFromUrlHash(e) {
     phraseSearch: e.phraseSearch,
     caseSensitive: false,
     highlightAll: true,
     findPrevious: false
   });
 }
 
 function webViewerScaleChanging(e) {
-  var appConfig = PDFViewerApplication.appConfig;
-  appConfig.toolbar.zoomOut.disabled = (e.scale === MIN_SCALE);
-  appConfig.toolbar.zoomIn.disabled = (e.scale === MAX_SCALE);
-
-  // Update the 'scaleSelect' DOM element.
-  var predefinedValueFound = selectScaleOption(e.presetValue ||
-                                               '' + e.scale);
-  if (!predefinedValueFound) {
-    var customScaleOption = appConfig.toolbar.customScaleOption;
-    var customScale = Math.round(e.scale * 10000) / 100;
-    customScaleOption.textContent =
-      mozL10n.get('page_scale_percent', { scale: customScale }, '{{scale}}%');
-    customScaleOption.selected = true;
-  }
+  PDFViewerApplication._updateUIToolbar({
+    scaleValue: e.presetValue,
+    scale: e.scale,
+  });
+
   if (!PDFViewerApplication.initialized) {
     return;
   }
   PDFViewerApplication.pdfViewer.update();
 }
 
 function webViewerPageChanging(e) {
   var page = e.pageNumber;
-  if (e.previousPageNumber !== page) {
-    PDFViewerApplication.appConfig.toolbar.pageNumber.value = page;
-
-    if (PDFViewerApplication.pdfSidebar.isThumbnailViewVisible) {
-      PDFViewerApplication.pdfThumbnailViewer.scrollThumbnailIntoView(page);
-    }
-  }
-  var numPages = PDFViewerApplication.pagesCount;
-
-  PDFViewerApplication.appConfig.toolbar.previous.disabled = (page <= 1);
-  PDFViewerApplication.appConfig.toolbar.next.disabled = (page >= numPages);
-
-  PDFViewerApplication.appConfig.toolbar.firstPage.disabled = (page <= 1);
-  PDFViewerApplication.appConfig.toolbar.lastPage.disabled = (page >= numPages);
+
+  PDFViewerApplication._updateUIToolbar({
+    pageNumber: page,
+  });
+  if (e.previousPageNumber !== page &&
+      PDFViewerApplication.pdfSidebar.isThumbnailViewVisible) {
+    PDFViewerApplication.pdfThumbnailViewer.scrollThumbnailIntoView(page);
+  }
 
   // we need to update stats
   if (pdfjsLib.PDFJS.pdfBug && Stats.enabled) {
     var pageView = PDFViewerApplication.pdfViewer.getPageView(page - 1);
     if (pageView.stats) {
       Stats.add(page, pageView.stats);
     }
   }
--- a/browser/locales/en-US/chrome/browser/accounts.properties
+++ b/browser/locales/en-US/chrome/browser/accounts.properties
@@ -28,8 +28,12 @@ verificationNotSentBody = We are unable 
 # These strings are used in a notification shown after Sync is connected.
 syncStartNotification.title = Sync enabled
 syncStartNotification.body = Firefox will begin syncing momentarily.
 
 # LOCALIZATION NOTE (deviceDisconnectedNotification.title, deviceDisconnectedNotification.body)
 # These strings are used in a notification shown after Sync was disconnected remotely.
 deviceDisconnectedNotification.title = Sync disconnected
 deviceDisconnectedNotification.body = This computer has been successfully disconnected from Firefox Sync.
+
+# LOCALIZATION NOTE (sendTabToAllDevices.menuitem)
+# Displayed in the Send Tabs context menu when right clicking a tab, a page or a link.
+sendTabToAllDevices.menuitem = All Devices
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -35,16 +35,22 @@ left instead of right. -->
 used as a metaphor for expressing the fact that these tabs are "pinned" to the
 left edge of the tabstrip. Really we just want the string to express the idea
 that this is a lightweight and reversible action that keeps your tab where you
 can reach it easily. -->
 <!ENTITY  pinTab.label                       "Pin Tab">
 <!ENTITY  pinTab.accesskey                   "P">
 <!ENTITY  unpinTab.label                     "Unpin Tab">
 <!ENTITY  unpinTab.accesskey                 "b">
+<!ENTITY  sendTabToDevice.label              "Send Tab to Device">
+<!ENTITY  sendTabToDevice.accesskey          "D">
+<!ENTITY  sendPageToDevice.label             "Send Page to Device">
+<!ENTITY  sendPageToDevice.accesskey         "D">
+<!ENTITY  sendLinkToDevice.label             "Send Link to Device">
+<!ENTITY  sendLinkToDevice.accesskey         "D">
 <!ENTITY  moveToNewWindow.label              "Move to New Window">
 <!ENTITY  moveToNewWindow.accesskey          "W">
 <!ENTITY  bookmarkAllTabs.label              "Bookmark All Tabs…">
 <!ENTITY  bookmarkAllTabs.accesskey          "T">
 <!ENTITY  undoCloseTab.label                 "Undo Close Tab">
 <!ENTITY  undoCloseTab.accesskey             "U">
 <!ENTITY  closeTab.label                     "Close Tab">
 <!ENTITY  closeTab.accesskey                 "c">
--- a/browser/modules/NetworkPrioritizer.jsm
+++ b/browser/modules/NetworkPrioritizer.jsm
@@ -22,17 +22,17 @@ Components.utils.import("resource://gre/
 
 // Lazy getters
 XPCOMUtils.defineLazyServiceGetter(this, "_focusManager",
                                    "@mozilla.org/focus-manager;1",
                                    "nsIFocusManager");
 
 
 // Constants
-const TAB_EVENTS = ["TabOpen", "TabSelect", "TabRemotenessChange"];
+const TAB_EVENTS = ["TabBrowserCreated", "TabSelect", "TabRemotenessChange"];
 const WINDOW_EVENTS = ["activate", "unload"];
 // lower value means higher priority
 const PRIORITY_DELTA = Ci.nsISupportsPriority.PRIORITY_NORMAL - Ci.nsISupportsPriority.PRIORITY_LOW;
 
 
 // Variables
 var _lastFocusedWindow = null;
 var _windows = [];
@@ -44,17 +44,17 @@ var _priorityBackup = new WeakMap();
 this.trackBrowserWindow = function trackBrowserWindow(aWindow) {
   WindowHelper.addWindow(aWindow);
 }
 
 
 // Global methods
 function _handleEvent(aEvent) {
   switch (aEvent.type) {
-    case "TabOpen":
+    case "TabBrowserCreated":
       BrowserHelper.onOpen(aEvent.target.linkedBrowser);
       break;
     case "TabSelect":
       BrowserHelper.onSelect(aEvent.target.linkedBrowser);
       break;
     case "activate":
       WindowHelper.onActivate(aEvent.target);
       break;
--- a/caps/nsScriptSecurityManager.cpp
+++ b/caps/nsScriptSecurityManager.cpp
@@ -777,27 +777,30 @@ nsScriptSecurityManager::CheckLoadURIWit
 
     //-- Some callers do not allow loading javascript:
     if ((aFlags & nsIScriptSecurityManager::DISALLOW_SCRIPT) &&
          targetScheme.EqualsLiteral("javascript"))
     {
        return NS_ERROR_DOM_BAD_URI;
     }
 
-    NS_NAMED_LITERAL_STRING(errorTag, "CheckLoadURIError");
-    bool reportErrors = !(aFlags & nsIScriptSecurityManager::DONT_REPORT_ERRORS);
-
     // Check for uris that are only loadable by principals that subsume them
     bool hasFlags;
     rv = NS_URIChainHasFlags(targetBaseURI,
                              nsIProtocolHandler::URI_LOADABLE_BY_SUBSUMERS,
                              &hasFlags);
     NS_ENSURE_SUCCESS(rv, rv);
 
     if (hasFlags) {
+        // check nothing else in the URI chain has flags that prevent
+        // access:
+        rv = CheckLoadURIFlags(sourceURI, aTargetURI, sourceBaseURI,
+                               targetBaseURI, aFlags);
+        NS_ENSURE_SUCCESS(rv, rv);
+        // Check the principal is allowed to load the target.
         return aPrincipal->CheckMayLoad(targetBaseURI, true, false);
     }
 
     //-- get the source scheme
     nsAutoCString sourceScheme;
     rv = sourceBaseURI->GetScheme(sourceScheme);
     if (NS_FAILED(rv)) return rv;
 
@@ -859,35 +862,60 @@ nsScriptSecurityManager::CheckLoadURIWit
                 !nsContentUtils::IsExactSitePermAllow(aPrincipal, WEBAPPS_PERM_NAME)) {
                 return NS_ERROR_DOM_BAD_URI;
             }
         }
         return NS_OK;
     }
 
     // If the schemes don't match, the policy is specified by the protocol
-    // flags on the target URI.  Note that the order of policy checks here is
-    // very important!  We start from most restrictive and work our way down.
-    // Note that since we're working with the innermost URI, we can just use
-    // the methods that work on chains of nested URIs and they will only look
-    // at the flags for our one URI.
+    // flags on the target URI.
+    return CheckLoadURIFlags(sourceURI, aTargetURI, sourceBaseURI,
+                             targetBaseURI, aFlags);
+}
+
+/**
+ * Helper method to check whether the target URI and its innermost ("base") URI
+ * has protocol flags that should stop it from being loaded by the source URI
+ * (and/or the source URI's innermost ("base") URI), taking into account any
+ * nsIScriptSecurityManager flags originally passed to
+ * CheckLoadURIWithPrincipal and friends.
+ *
+ * @return if success, access is allowed. Otherwise, deny access
+ */
+nsresult
+nsScriptSecurityManager::CheckLoadURIFlags(nsIURI *aSourceURI,
+                                           nsIURI *aTargetURI,
+                                           nsIURI *aSourceBaseURI,
+                                           nsIURI *aTargetBaseURI,
+                                           uint32_t aFlags)
+{
+    // Note that the order of policy checks here is very important!
+    // We start from most restrictive and work our way down.
+    bool reportErrors = !(aFlags & nsIScriptSecurityManager::DONT_REPORT_ERRORS);
+    NS_NAMED_LITERAL_STRING(errorTag, "CheckLoadURIError");
+
+    nsAutoCString targetScheme;
+    nsresult rv = aTargetBaseURI->GetScheme(targetScheme);
+    if (NS_FAILED(rv)) return rv;
 
     // Check for system target URI
     rv = DenyAccessIfURIHasFlags(aTargetURI,
                                  nsIProtocolHandler::URI_DANGEROUS_TO_LOAD);
     if (NS_FAILED(rv)) {
         // Deny access, since the origin principal is not system
         if (reportErrors) {
-            ReportError(nullptr, errorTag, sourceURI, aTargetURI);
+            ReportError(nullptr, errorTag, aSourceURI, aTargetURI);
         }
         return rv;
     }
 
     // Check for chrome target URI
-    rv = NS_URIChainHasFlags(targetBaseURI,
+    bool hasFlags = false;
+    rv = NS_URIChainHasFlags(aTargetBaseURI,
                              nsIProtocolHandler::URI_IS_UI_RESOURCE,
                              &hasFlags);
     NS_ENSURE_SUCCESS(rv, rv);
     if (hasFlags) {
         if (aFlags & nsIScriptSecurityManager::ALLOW_CHROME) {
 
             // For now, don't change behavior for resource:// or moz-icon:// and
             // just allow them.
@@ -899,85 +927,94 @@ nsScriptSecurityManager::CheckLoadURIWit
             // target if ALLOW_CHROME is set.
             //
             // ALLOW_CHROME is a flag that we pass on all loads _except_ docshell
             // loads (since docshell loads run the loaded content with its origin
             // principal). So we're effectively allowing resource://, chrome://,
             // and moz-icon:// source URIs to load resource://, chrome://, and
             // moz-icon:// files, so long as they're not loading it as a document.
             bool sourceIsUIResource;
-            rv = NS_URIChainHasFlags(sourceBaseURI,
+            rv = NS_URIChainHasFlags(aSourceBaseURI,
                                      nsIProtocolHandler::URI_IS_UI_RESOURCE,
                                      &sourceIsUIResource);
             NS_ENSURE_SUCCESS(rv, rv);
             if (sourceIsUIResource) {
                 return NS_OK;
             }
 
             // Allow the load only if the chrome package is whitelisted.
             nsCOMPtr<nsIXULChromeRegistry> reg(do_GetService(
                                                  NS_CHROMEREGISTRY_CONTRACTID));
             if (reg) {
                 bool accessAllowed = false;
-                reg->AllowContentToAccess(targetBaseURI, &accessAllowed);
+                reg->AllowContentToAccess(aTargetBaseURI, &accessAllowed);
                 if (accessAllowed) {
                     return NS_OK;
                 }
             }
         }
 
         // Special-case the hidden window: it's allowed to load
         // URI_IS_UI_RESOURCE no matter what.  Bug 1145470 tracks removing this.
         nsAutoCString sourceSpec;
-        if (NS_SUCCEEDED(sourceBaseURI->GetSpec(sourceSpec)) &&
+        if (NS_SUCCEEDED(aSourceBaseURI->GetSpec(sourceSpec)) &&
             sourceSpec.EqualsLiteral("resource://gre-resources/hiddenWindow.html")) {
             return NS_OK;
         }
 
         if (reportErrors) {
-            ReportError(nullptr, errorTag, sourceURI, aTargetURI);
+            ReportError(nullptr, errorTag, aSourceURI, aTargetURI);
         }
         return NS_ERROR_DOM_BAD_URI;
     }
 
     // Check for target URI pointing to a file
     rv = NS_URIChainHasFlags(aTargetURI,
                              nsIProtocolHandler::URI_IS_LOCAL_FILE,
                              &hasFlags);
     NS_ENSURE_SUCCESS(rv, rv);
     if (hasFlags) {
         // Allow domains that were whitelisted in the prefs. In 99.9% of cases,
         // this array is empty.
         for (size_t i = 0; i < mFileURIWhitelist.Length(); ++i) {
-            if (EqualOrSubdomain(sourceURI, mFileURIWhitelist[i])) {
+            if (EqualOrSubdomain(aSourceURI, mFileURIWhitelist[i])) {
                 return NS_OK;
             }
         }
 
         // Allow chrome://
-        if (sourceScheme.EqualsLiteral("chrome")) {
+        bool isChrome = false;
+        if (NS_SUCCEEDED(aSourceBaseURI->SchemeIs("chrome", &isChrome)) && isChrome) {
             return NS_OK;
         }
 
         // Nothing else.
         if (reportErrors) {
-            ReportError(nullptr, errorTag, sourceURI, aTargetURI);
+            ReportError(nullptr, errorTag, aSourceURI, aTargetURI);
         }
         return NS_ERROR_DOM_BAD_URI;
     }
 
     // OK, everyone is allowed to load this, since unflagged handlers are
     // deprecated but treated as URI_LOADABLE_BY_ANYONE.  But check whether we
     // need to warn.  At some point we'll want to make this warning into an
     // error and treat unflagged handlers as URI_DANGEROUS_TO_LOAD.
-    rv = NS_URIChainHasFlags(targetBaseURI,
+    rv = NS_URIChainHasFlags(aTargetBaseURI,
                              nsIProtocolHandler::URI_LOADABLE_BY_ANYONE,
                              &hasFlags);
     NS_ENSURE_SUCCESS(rv, rv);
-    if (!hasFlags) {
+    // NB: we also get here if the base URI is URI_LOADABLE_BY_SUBSUMERS,
+    // and none of the rest of the nested chain of URIs for aTargetURI
+    // prohibits the load, so avoid warning in that case:
+    bool hasSubsumersFlag = false;
+    rv = NS_URIChainHasFlags(aTargetBaseURI,
+                             nsIProtocolHandler::URI_LOADABLE_BY_SUBSUMERS,
+                             &hasSubsumersFlag);
+    NS_ENSURE_SUCCESS(rv, rv);
+    if (!hasFlags && !hasSubsumersFlag) {
         nsXPIDLString message;
         NS_ConvertASCIItoUTF16 ucsTargetScheme(targetScheme);
         const char16_t* formatStrings[] = { ucsTargetScheme.get() };
         rv = sStrBundle->
             FormatStringFromName(MOZ_UTF16("ProtocolFlagError"),
                                  formatStrings,
                                  ArrayLength(formatStrings),
                                  getter_Copies(message));
--- a/caps/nsScriptSecurityManager.h
+++ b/caps/nsScriptSecurityManager.h
@@ -115,16 +115,20 @@ private:
 
     // If aURI is a moz-extension:// URI, set mAddonId to the associated addon.
     nsresult MaybeSetAddonIdFromURI(mozilla::PrincipalOriginAttributes& aAttrs, nsIURI* aURI);
 
     nsresult GetChannelResultPrincipal(nsIChannel* aChannel,
                                        nsIPrincipal** aPrincipal,
                                        bool aIgnoreSandboxing);
 
+    nsresult
+    CheckLoadURIFlags(nsIURI* aSourceURI, nsIURI* aTargetURI, nsIURI* aSourceBaseURI,
+                      nsIURI* aTargetBaseURI, uint32_t aFlags);
+
     nsCOMPtr<nsIPrincipal> mSystemPrincipal;
     bool mPrefInitialized;
     bool mIsJavaScriptEnabled;
     nsTArray<nsCOMPtr<nsIURI>> mFileURIWhitelist;
 
     // This machinery controls new-style domain policies. The old-style
     // policy machinery will be removed soon.
     nsCOMPtr<nsIDomainPolicy> mDomainPolicy;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/components/inspector-tab-panel.css
@@ -0,0 +1,15 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.devtools-inspector-tab-frame {
+  border: none;
+  height: 100%;
+  width: 100%;
+}
+
+.devtools-inspector-tab-panel {
+  width: 100%;
+  height: 100%;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/components/inspector-tab-panel.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { DOM, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const { div } = DOM;
+
+/**
+ * Side panel for the Inspector panel.
+ * This side panel is using an existing DOM node as a content.
+ */
+var InspectorTabPanel = createClass({
+  displayName: "InspectorTabPanel",
+
+  propTypes: {
+    onMount: PropTypes.func,
+  },
+
+  componentDidMount: function () {
+    let doc = this.refs.content.ownerDocument;
+    let panel = doc.getElementById("sidebar-panel-" + this.props.id);
+
+    // Append existing DOM node into panel's content.
+    this.refs.content.appendChild(panel);
+
+    if (this.props.onMount) {
+      this.props.onMount(this.refs.content, this.props);
+    }
+  },
+
+  componentWillUnmount: function () {
+    let doc = this.refs.content.ownerDocument;
+    let panels = doc.getElementById("tabpanels");
+
+    // Move panel's content node back into list of tab panels.
+    panels.appendChild(this.refs.content.firstChild);
+  },
+
+  render: function () {
+    return (
+      div({
+        ref: "content",
+        className: "devtools-inspector-tab-panel",
+      })
+    );
+  }
+});
+
+module.exports = InspectorTabPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/components/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'inspector-tab-panel.css',
+    'inspector-tab-panel.js',
+)
--- a/devtools/client/inspector/computed/computed.js
+++ b/devtools/client/inspector/computed/computed.js
@@ -183,17 +183,17 @@ function CssComputedView(inspector, docu
   this.styleDocument.addEventListener("mousedown", this.focusWindow);
   this.element.addEventListener("click", this._onClick);
   this.element.addEventListener("copy", this._onCopy);
   this.element.addEventListener("contextmenu", this._onContextMenu);
   this.searchField.addEventListener("input", this._onFilterStyles);
   this.searchField.addEventListener("contextmenu",
                                     this._onFilterTextboxContextMenu);
   this.searchClearButton.addEventListener("click", this._onClearSearch);
-  this.includeBrowserStylesCheckbox.addEventListener("command",
+  this.includeBrowserStylesCheckbox.addEventListener("input",
     this._onIncludeBrowserStyles);
 
   this.searchClearButton.hidden = true;
 
   // No results text.
   this.noResults = this.styleDocument.getElementById("computedview-no-results");
 
   // Refresh panel when color unit changed.
@@ -768,18 +768,18 @@ CssComputedView.prototype = {
     this.styleDocument.removeEventListener("mousedown", this.focusWindow);
     this.element.removeEventListener("click", this._onClick);
     this.element.removeEventListener("copy", this._onCopy);
     this.element.removeEventListener("contextmenu", this._onContextMenu);
     this.searchField.removeEventListener("input", this._onFilterStyles);
     this.searchField.removeEventListener("contextmenu",
                                          this._onFilterTextboxContextMenu);
     this.searchClearButton.removeEventListener("click", this._onClearSearch);
-    this.includeBrowserStylesCheckbox.removeEventListener("command",
-      this.includeBrowserStylesChanged);
+    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;
--- a/devtools/client/inspector/inspector-panel.js
+++ b/devtools/client/inspector/inspector-panel.js
@@ -29,17 +29,17 @@ loader.lazyRequireGetter(this, "CSS", "C
 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/framework/sidebar", 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");
 });
 loader.lazyGetter(this, "toolboxStrings", () => {
   return Services.strings.createBundle("chrome://devtools/locale/toolbox.properties");
 });
@@ -407,42 +407,100 @@ InspectorPanel.prototype = {
 
     let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
 
     if (!Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
        defaultTab == "fontinspector") {
       defaultTab = "ruleview";
     }
 
+    // Append all side panels
+    this.sidebar.addExistingTab(
+      "ruleview",
+      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.addTab("animationinspector",
-                          "chrome://devtools/content/animationinspector/animation-inspector.xhtml",
-                          {selected: defaultTab == "animationinspector",
-                           insertBefore: "fontinspector"});
+      this.sidebar.addFrameTab(
+        "animationinspector",
+        strings.GetStringFromName("inspector.sidebar.animationInspectorTitle"),
+        "chrome://devtools/content/animationinspector/animation-inspector.xhtml",
+        defaultTab == "animationinspector");
     }
 
     if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
         this.canGetUsedFontFaces) {
+      this.sidebar.addExistingTab(
+        "fontinspector",
+        strings.GetStringFromName("inspector.sidebar.fontInspectorTitle"),
+        defaultTab == "fontinspector");
+
       this.fontInspector = new FontInspector(this, this.panelWin);
       this.sidebar.toggleTab(true, "fontinspector");
     }
 
+    this.setupSidebarToggle();
+    this.setupSidebarWidth();
+
     this.sidebar.show(defaultTab);
+  },
+
+  /**
+   * Sidebar width is currently driven by vbox.inspector-sidebar-container
+   * element, which is located at the left side of the side bar splitter.
+   * It's width is changed by the splitter and stored into preferences.
+   * As soon as bug 1260552 is fixed and new HTML based splitter in place
+   * the width can be driven by div.inspector-sidebar element. This element
+   * represents the ToolSidebar and so, the entire logic related to width
+   * persistence can be done inside the ToolSidebar.
+   */
+  setupSidebarWidth: function () {
+    let sidePaneContainer = this.panelDoc.querySelector(
+      "#inspector-sidebar-container");
 
-    this.setupSidebarToggle();
+    this.sidebar.on("show", () => {
+      try {
+        sidePaneContainer.width = Services.prefs.getIntPref(
+          "devtools.toolsidebar-width.inspector");
+      } catch (e) {
+        // The default width is the min-width set in CSS
+        // for #inspector-sidebar-container
+        sidePaneContainer.width = 450;
+      }
+    });
+
+    this.sidebar.on("hide", () => {
+      Services.prefs.setIntPref("devtools.toolsidebar-width.inspector",
+        sidePaneContainer.width);
+    });
+
+    this.sidebar.on("destroy", () => {
+      Services.prefs.setIntPref("devtools.toolsidebar-width.inspector",
+        sidePaneContainer.width);
+    });
   },
 
   /**
    * Add the expand/collapse behavior for the sidebar panel.
    */
   setupSidebarToggle: function () {
     let SidebarToggle = this.React.createFactory(this.browserRequire(
       "devtools/client/shared/components/sidebar-toggle"));
@@ -1165,42 +1223,49 @@ InspectorPanel.prototype = {
     }
   },
 
   /**
    * When the pane toggle button is clicked, toggle the pane, change the button
    * state and tooltip.
    */
   onPaneToggleButtonActivated: function (e) {
-    let sidePane = this.panelDoc.querySelector("#inspector-sidebar");
+    let sidePaneContainer = this.panelDoc.querySelector("#inspector-sidebar-container");
     let isVisible = !this._sidebarToggle.state.collapsed;
+    let sidePane = this.panelDoc.querySelector(
+      "#inspector-sidebar .devtools-sidebar-tabs");
 
     // Make sure the sidebar has width and height attributes before collapsing
     // because ViewHelpers needs it.
     if (isVisible) {
-      let rect = sidePane.getBoundingClientRect();
-      if (!sidePane.hasAttribute("width")) {
-        sidePane.setAttribute("width", rect.width);
+      let rect = sidePaneContainer.getBoundingClientRect();
+      if (!sidePaneContainer.hasAttribute("width")) {
+        sidePaneContainer.setAttribute("width", rect.width);
+        sidePane.style.width = rect.width + "px";
       }
       // always refresh the height attribute before collapsing, it could have
       // been modified by resizing the container.
-      sidePane.setAttribute("height", rect.height);
+      sidePaneContainer.setAttribute("height", rect.height);
+      sidePane.style.height = rect.height + "px";
     }
 
+    let onAnimationDone = () => {
+      if (isVisible) {
+        this._sidebarToggle.setState({collapsed: true});
+      } else {
+        this._sidebarToggle.setState({collapsed: false});
+      }
+    };
+
     ViewHelpers.togglePane({
       visible: !isVisible,
       animated: true,
-      delayed: true
-    }, sidePane);
-
-    if (isVisible) {
-      this._sidebarToggle.setState({collapsed: true});
-    } else {
-      this._sidebarToggle.setState({collapsed: false});
-    }
+      delayed: true,
+      callback: onAnimationDone
+    }, sidePaneContainer);
   },
 
   /**
    * Create a new node as the last child of the current selection, expand the
    * parent and select the new node.
    */
   addNode: Task.async(function* () {
     if (!this.canAddHTMLChild()) {
--- a/devtools/client/inspector/inspector.css
+++ b/devtools/client/inspector/inspector.css
@@ -1,19 +1,30 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-#inspector-sidebar {
-  min-width: 250px;
+/* Set the minimum width for the side bar so, all tabs are
+  properly visible. The value can be decreased when bug 1281789
+  is fixed and the all-tabs-menu is available again. */
+#inspector-sidebar-container {
+  overflow: hidden;
+  min-width: 450px;
+  position: relative;
 }
 
+#inspector-sidebar {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+}
+
+/* Override `-moz-user-focus:ignore;` from toolkit/content/minimal-xul.css */
 .inspector-tabpanel > * {
-  /*
-   * Override `-moz-user-focus:ignore;` from toolkit/content/minimal-xul.css
-   */
   -moz-user-focus: normal;
 }
 
 #inspector-sidebar-toggle-box {
   line-height: initial;
 }
--- a/devtools/client/inspector/inspector.xul
+++ b/devtools/client/inspector/inspector.xul
@@ -6,17 +6,22 @@
 <?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/content/inspector/inspector.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/inspector.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/rules.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/computed.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/fonts.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/animationinspector.css" type="text/css"?>
 <?xml-stylesheet href="resource://devtools/client/shared/components/sidebar-toggle.css" type="text/css"?>
+<?xml-stylesheet href="resource://devtools/client/shared/components/tabs/tabs.css" type="text/css"?>
+<?xml-stylesheet href="resource://devtools/client/shared/components/tabs/tabbar.css" type="text/css"?>
+<?xml-stylesheet href="resource://devtools/client/inspector/components/side-panel.css" type="text/css"?>
+<?xml-stylesheet href="resource://devtools/client/inspector/components/inspector-tab-panel.css" type="text/css"?>
 
 <!DOCTYPE window [
   <!ENTITY % inspectorDTD SYSTEM "chrome://devtools/locale/inspector.dtd"> %inspectorDTD;
   <!ENTITY % styleinspectorDTD SYSTEM "chrome://devtools/locale/styleinspector.dtd"> %styleinspectorDTD;
   <!ENTITY % fontinspectorDTD SYSTEM "chrome://devtools/locale/font-inspector.dtd"> %fontinspectorDTD;
   <!ENTITY % layoutviewDTD SYSTEM "chrome://devtools/locale/layoutview.dtd"> %layoutviewDTD;
 ]>
 
@@ -45,161 +50,159 @@
       </html:div>
       <vbox flex="1" id="markup-box">
       </vbox>
       <html:div id="inspector-breadcrumbs-toolbar" class="devtools-toolbar">
         <html:div id="inspector-breadcrumbs" class="breadcrumbs-widget-container"/>
       </html:div>
     </vbox>
     <splitter class="devtools-side-splitter"/>
-    <tabbox id="inspector-sidebar" handleCtrlTab="false" class="devtools-sidebar-tabs" hidden="true">
-      <tabs>
-        <tab id="sidebar-tab-ruleview"
-             label="&ruleViewTitle;"
-             crop="end"/>
-        <tab id="sidebar-tab-computedview"
-             label="&computedViewTitle;"
-             crop="end"/>
-        <tab id="sidebar-tab-layoutview"
-             label="&layoutViewTitle;"
-             crop="end"/>
-        <tab id="sidebar-tab-fontinspector"
-             label="&fontInspectorTitle;"
-             crop="end"
-             hidden="true"/>
-      </tabs>
-      <tabpanels flex="1">
-        <tabpanel id="sidebar-panel-ruleview" class="devtools-monospace theme-sidebar inspector-tabpanel">
-          <html:div id="ruleview-toolbar-container" class="devtools-toolbar">
-            <html:div id="ruleview-toolbar">
-              <html:div class="devtools-searchbox">
-                <html:input id="ruleview-searchbox"
-                            class="devtools-filterinput devtools-rule-searchbox"
-                            type="search"
-                            placeholder="&filterStylesPlaceholder;"/>
-                <html:button id="ruleview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
-              </html:div>
-              <html:div id="ruleview-command-toolbar">
-                <html:button id="ruleview-add-rule-button" title="&addRuleButtonTooltip;" class="devtools-button"></html:button>
-                <html:button id="pseudo-class-panel-toggle" title="&togglePseudoClassPanel;" class="devtools-button"></html:button>
-              </html:div>
-            </html:div>
-            <html:div id="pseudo-class-panel" hidden="true">
-              <html:label><html:input id="pseudo-hover-toggle" type="checkbox" value=":hover" tabindex="-1" />:hover</html:label>
-              <html:label><html:input id="pseudo-active-toggle" type="checkbox" value=":active" tabindex="-1" />:active</html:label>
-              <html:label><html:input id="pseudo-focus-toggle" type="checkbox" value=":focus" tabindex="-1" />:focus</html:label>
-          </html:div>
-          </html:div>
+    <vbox id="inspector-sidebar-container">
+      <!-- Specify the XHTML namespace explicitly
+        otherwise the layout is broken. -->
+      <div xmlns="http://www.w3.org/1999/xhtml"
+           id="inspector-sidebar"
+           hidden="true" />
+    </vbox>
 
-          <html:div id="ruleview-container" class="ruleview">
-          </html:div>
-        </tabpanel>
-
-        <tabpanel id="sidebar-panel-computedview" class="devtools-monospace theme-sidebar inspector-tabpanel">
-          <html:div class="devtools-toolbar">
+    <!-- Sidebar panel definitions -->
+    <html:div xmlns="http://www.w3.org/1999/xhtml" id="tabpanels" style="visibility:collapse">
+      <html:div id="sidebar-panel-ruleview" class="devtools-monospace theme-sidebar inspector-tabpanel">
+        <html:div id="ruleview-toolbar-container" class="devtools-toolbar">
+          <html:div id="ruleview-toolbar">
             <html:div class="devtools-searchbox">
-              <html:input id="computedview-searchbox"
+              <html:input id="ruleview-searchbox"
                           class="devtools-filterinput devtools-rule-searchbox"
                           type="search"
                           placeholder="&filterStylesPlaceholder;"/>
-              <html:button id="computedview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
+              <html:button id="ruleview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
+            </html:div>
+            <html:div id="ruleview-command-toolbar">
+              <html:button id="ruleview-add-rule-button" title="&addRuleButtonTooltip;" class="devtools-button"></html:button>
+              <html:button id="pseudo-class-panel-toggle" title="&togglePseudoClassPanel;" class="devtools-button"></html:button>
             </html:div>
-            <checkbox id="browser-style-checkbox"
-                      class="includebrowserstyles"
-                      checked="false"
-                      label="&browserStylesLabel;"/>
           </html:div>
+          <html:div id="pseudo-class-panel" hidden="true">
+            <html:label><html:input id="pseudo-hover-toggle" type="checkbox" value=":hover" tabindex="-1" />:hover</html:label>
+            <html:label><html:input id="pseudo-active-toggle" type="checkbox" value=":active" tabindex="-1" />:active</html:label>
+            <html:label><html:input id="pseudo-focus-toggle" type="checkbox" value=":focus" tabindex="-1" />:focus</html:label>
+        </html:div>
+        </html:div>
 
-          <html:div id="propertyContainer">
-          </html:div>
-
-          <html:div id="computedview-no-results" hidden="">
-            &noPropertiesFound;
-          </html:div>
-        </tabpanel>
+        <html:div id="ruleview-container" class="ruleview">
+        </html:div>
+      </html:div>
 
-        <tabpanel 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="sidebar-panel-computedview" class="devtools-monospace theme-sidebar inspector-tabpanel">
+        <html:div class="devtools-toolbar">
+          <html:div class="devtools-searchbox">
+            <html:input id="computedview-searchbox"
+                        class="devtools-filterinput devtools-rule-searchbox"
+                        type="search"
+                        placeholder="&filterStylesPlaceholder;"/>
+            <html:button id="computedview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
+          </html:div>
+          <html:label id="browser-style-checkbox-label" for="browser-style-checkbox">
+            <html:input id="browser-style-checkbox"
+                        type="checkbox"
+                        class="includebrowserstyles"
+                        label="&browserStylesLabel;"/>&browserStylesLabel;</html:label>
+        </html:div>
+
+        <html:div id="propertyContainer">
+        </html:div>
 
-              <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 id="computedview-no-results" hidden="">
+          &noPropertiesFound;
+        </html:div>
+      </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>
                   </html:div>
                 </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:p class="layout-padding layout-top"><html:span data-box="padding" class="layout-editable" title="padding-top"></html:span></html:p>
-                <html:p class="layout-padding layout-right"><html:span data-box="padding" class="layout-editable" title="padding-right"></html:span></html:p>
-                <html:p class="layout-padding layout-bottom"><html:span data-box="padding" class="layout-editable" title="padding-bottom"></html:span></html:p>
-                <html:p class="layout-padding layout-left"><html:span data-box="padding" class="layout-editable" title="padding-left"></html:span></html:p>
-
-                <html:p class="layout-size"><html:span data-box="content" title="&content.tooltip;"></html:span></html:p>
               </html:div>
 
-              <html:div style="display: none">
-                <html:p id="layout-dummy"></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:p class="layout-padding layout-top"><html:span data-box="padding" class="layout-editable" title="padding-top"></html:span></html:p>
+              <html:p class="layout-padding layout-right"><html:span data-box="padding" class="layout-editable" title="padding-right"></html:span></html:p>
+              <html:p class="layout-padding layout-bottom"><html:span data-box="padding" class="layout-editable" title="padding-bottom"></html:span></html:p>
+              <html:p class="layout-padding layout-left"><html:span data-box="padding" class="layout-editable" title="padding-left"></html:span></html:p>
+
+              <html:p class="layout-size"><html:span data-box="content" title="&content.tooltip;"></html:span></html:p>
+            </html:div>
+
+            <html:div style="display: none">
+              <html:p id="layout-dummy"></html:p>
             </html:div>
           </html:div>
-        </tabpanel>
+        </html:div>
+      </html:div>
 
-        <tabpanel 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"
-                          class="devtools-textinput"
-                          type="search"
-                          placeholder="&previewHint;"/>
-            </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"
+                        class="devtools-textinput"
+                        type="search"
+                        placeholder="&previewHint;"/>
           </html:div>
+        </html:div>
 
-          <html:div id="font-container">
-            <html:ul id="all-fonts"></html:ul>
-            <html:button id="font-showall">&showAllFonts;</html:button>
-          </html:div>
+        <html:div id="font-container">
+          <html:ul id="all-fonts"></html:ul>
+          <html:button id="font-showall">&showAllFonts;</html:button>
+        </html:div>
 
-          <html:div id="font-template">
-            <html:section class="font">
-              <html:div class="font-preview-container">
-                <html:img class="font-preview"></html:img>
-              </html:div>
-              <html:div class="font-info">
-                <html:h1 class="font-name"></html:h1>
-                <html:span class="font-is-local">&system;</html:span>
-                <html:span class="font-is-remote">&remote;</html:span>
-                <html:p class="font-format-url">
-                  <html:input readonly="readonly" class="font-url"></html:input>
-                  <html:span class="font-format"></html:span>
-                </html:p>
-                <html:p class="font-css">&usedAs; "<html:span class="font-css-name"></html:span>"</html:p>
-                <html:pre class="font-css-code"></html:pre>
-              </html:div>
-            </html:section>
-          </html:div>
-        </tabpanel>
-      </tabpanels>
-    </tabbox>
+        <html:div id="font-template">
+          <html:section class="font">
+            <html:div class="font-preview-container">
+              <html:img class="font-preview"></html:img>
+            </html:div>
+            <html:div class="font-info">
+              <html:h1 class="font-name"></html:h1>
+              <html:span class="font-is-local">&system;</html:span>
+              <html:span class="font-is-remote">&remote;</html:span>
+              <html:p class="font-format-url">
+                <html:input readonly="readonly" class="font-url"></html:input>
+                <html:span class="font-format"></html:span>
+              </html:p>
+              <html:p class="font-css">&usedAs; "<html:span class="font-css-name"></html:span>"</html:p>
+              <html:pre class="font-css-code"></html:pre>
+            </html:div>
+          </html:section>
+        </html:div>
+      </html:div>
+
+      <html:div id="sidebar-panel-animationinspector" class="devtools-monospace theme-sidebar inspector-tabpanel">
+        <html:iframe class="devtools-inspector-tab-frame" />
+      </html:div>
+    </html:div>
+
   </box>
 </window>
--- a/devtools/client/inspector/moz.build
+++ b/devtools/client/inspector/moz.build
@@ -1,21 +1,23 @@
 # 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/.
 
 DIRS += [
+    'components',
     'computed',
     'fonts',
     'layout',
     'markup',
     'rules',
     'shared'
 ]
 
 DevToolsModules(
     'breadcrumbs.js',
     'inspector-commands.js',
     'inspector-panel.js',
-    'inspector-search.js'
+    'inspector-search.js',
+    'toolsidebar.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js
@@ -6,17 +6,17 @@
 // Test that the inspector toggled panel is visible by default, is hidden after
 // clicking on the toggle button and remains expanded/collapsed when switching
 // hosts.
 
 add_task(function* () {
   info("Open the inspector in a side toolbox host");
   let {toolbox, inspector} = yield openInspectorForURL("about:blank", "side");
 
-  let panel = inspector.panelDoc.querySelector("#inspector-sidebar");
+  let panel = inspector.panelDoc.querySelector("#inspector-sidebar-container");
   let button = inspector.panelDoc.querySelector(".sidebar-toggle");
   ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");
 
   info("Listen to the end of the animation on the sidebar panel");
   let onTransitionEnd = once(panel, "transitionend");
 
   info("Click on the toggle button");
   EventUtils.synthesizeMouseAtCenter(button, {},
--- a/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js
@@ -5,17 +5,17 @@
 
 // Test that the toggle button can collapse and expand the inspector side/bottom
 // panel, and that the appropriate attributes are updated in the process.
 
 add_task(function* () {
   let {inspector} = yield openInspectorForURL("about:blank");
 
   let button = inspector.panelDoc.querySelector(".sidebar-toggle");
-  let panel = inspector.panelDoc.querySelector("#inspector-sidebar");
+  let panel = inspector.panelDoc.querySelector("#inspector-sidebar-container");
 
   ok(!button.classList.contains("pane-collapsed"), "The button is in expanded state");
 
   info("Listen to the end of the animation on the sidebar panel");
   let onTransitionEnd = once(panel, "transitionend");
 
   info("Click on the toggle button");
   EventUtils.synthesizeMouseAtCenter(button, {},
--- a/devtools/client/inspector/test/browser_inspector_pane-toggle-04.js
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-04.js
@@ -15,17 +15,17 @@ add_task(function* () {
     let options = {"set": [
       ["devtools.toolsidebar-width.inspector", 200]
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { inspector, toolbox } = yield openInspectorForURL("about:blank");
   let button = inspector.panelDoc.querySelector(".sidebar-toggle");
-  let panel = inspector.panelDoc.querySelector("#inspector-sidebar");
+  let panel = inspector.panelDoc.querySelector("#inspector-sidebar-container");
 
   info("Changing toolbox host to a window.");
   yield toolbox.switchHost(Toolbox.HostType.WINDOW);
 
   let hostWindow = toolbox._host._window;
   let originalWidth = hostWindow.outerWidth;
   let originalHeight = hostWindow.outerHeight;
 
--- a/devtools/client/inspector/test/browser_inspector_pane-toggle-05.js
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-05.js
@@ -5,17 +5,17 @@
 
 /**
 * Test the keyboard navigation for the pane toggle using
 * space and enter
 */
 
 add_task(function* () {
   let {inspector} = yield openInspectorForURL("about:blank", "side");
-  let panel = inspector.panelDoc.querySelector("#inspector-sidebar");
+  let panel = inspector.panelDoc.querySelector("#inspector-sidebar-container");
   let button = inspector.panelDoc.querySelector(".sidebar-toggle");
 
   ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");
 
   yield togglePane(button, "Press on the toggle button", panel, "VK_RETURN");
   ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state");
 
   yield togglePane(button, "Press on the toggle button to expand the panel again",
--- a/devtools/client/inspector/test/browser_inspector_sidebarstate.js
+++ b/devtools/client/inspector/test/browser_inspector_sidebarstate.js
@@ -13,16 +13,20 @@ add_task(function* () {
   inspector.sidebar.select("ruleview");
 
   is(inspector.sidebar.getCurrentTabID(), "ruleview",
      "Rule View is selected by default");
 
   info("Selecting computed view.");
   inspector.sidebar.select("computedview");
 
+  // Finish initialization of the computed panel before
+  // destroying the toolbox.
+  yield waitForTick();
+
   info("Closing inspector.");
   yield toolbox.destroy();
 
   info("Re-opening inspector.");
   inspector = (yield openInspector()).inspector;
 
   if (!inspector.sidebar.getCurrentTabID()) {
     info("Default sidebar still to be selected, adding select listener.");
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/toolsidebar.js
@@ -0,0 +1,320 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var Services = require("Services");
+var EventEmitter = require("devtools/shared/event-emitter");
+var Telemetry = require("devtools/client/shared/telemetry");
+var { Task } = require("devtools/shared/task");
+var { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * This object represents replacement for ToolSidebar
+ * implemented in devtools/client/framework/sidebar.js module
+ *
+ * This new component is part of devtools.html aimed at
+ * removing XUL and use HTML for entire DevTools UI.
+ * There are currently two implementation of the side bar since
+ * the `sidebar.js` module (mentioned above) is still used by
+ * other panels.
+ * As soon as all panels are using this HTML based
+ * implementation it can be removed.
+ */
+function ToolSidebar(tabbox, panel, uid, options = {}) {
+  EventEmitter.decorate(this);
+
+  this._tabbox = tabbox;
+  this._uid = uid;
+  this._panelDoc = this._tabbox.ownerDocument;
+  this._toolPanel = panel;
+  this._options = options;
+
+  if (!options.disableTelemetry) {
+    this._telemetry = new Telemetry();
+  }
+
+  this._tabs = [];
+
+  if (this._options.hideTabstripe) {
+    this._tabbox.setAttribute("hidetabs", "true");
+  }
+
+  this.render();
+
+  this._toolPanel.emit("sidebar-created", this);
+}
+
+exports.ToolSidebar = ToolSidebar;
+
+ToolSidebar.prototype = {
+  TABPANEL_ID_PREFIX: "sidebar-panel-",
+
+  // React
+
+  get React() {
+    return this._toolPanel.React;
+  },
+
+  get ReactDOM() {
+    return this._toolPanel.ReactDOM;
+  },
+
+  get browserRequire() {
+    return this._toolPanel.browserRequire;
+  },
+
+  get InspectorTabPanel() {
+    if (!this._InspectorTabPanel) {
+      this._InspectorTabPanel =
+        this.React.createFactory(this.browserRequire(
+        "devtools/client/inspector/components/inspector-tab-panel"));
+    }
+    return this._InspectorTabPanel;
+  },
+
+  // Rendering
+
+  render: function () {
+    let Tabbar = this.React.createFactory(this.browserRequire(
+      "devtools/client/shared/components/tabs/tabbar"));
+
+    let sidebar = Tabbar({
+      onSelect: this.handleSelectionChange.bind(this),
+    });
+
+    this._tabbar = this.ReactDOM.render(sidebar, this._tabbox);
+  },
+
+  addExistingTab: function (id, title, selected) {
+    this._tabbar.addTab(id, title, selected, this.InspectorTabPanel);
+
+    this.emit("new-tab-registered", id);
+  },
+
+  /**
+   * Register a tab. A tab is a document.
+   * The document must have a title, which will be used as the name of the tab.
+   *
+   * @param {string} tab uniq id
+   * @param {string} url
+   */
+  addFrameTab: function (id, title, url, selected) {
+    let panel = this.InspectorTabPanel({
+      id: id,
+      key: id,
+      title: title,
+      url: url,
+      onMount: this.onSidePanelMounted.bind(this),
+    });
+
+    this._tabbar.addTab(id, title, selected, panel);
+
+    this.emit("new-tab-registered", id);
+  },
+
+  onSidePanelMounted: function (content, props) {
+    let iframe = content.querySelector("iframe");
+    if (!iframe || iframe.getAttribute("src")) {
+      return;
+    }
+
+    let onIFrameLoaded = (event) => {
+      iframe.removeEventListener("load", onIFrameLoaded, true);
+
+      let doc = event.target;
+      let win = doc.defaultView;
+      if ("setPanel" in win) {
+        win.setPanel(this._toolPanel, iframe);
+      }
+      this.emit(props.id + "-ready");
+    };
+
+    iframe.addEventListener("load", onIFrameLoaded, true);
+    iframe.setAttribute("src", props.url);
+  },
+
+  /**
+   * Remove an existing tab.
+   * @param {String} tabId The ID of the tab that was used to register it, or
+   * the tab id attribute value if the tab existed before the sidebar
+   * got created.
+   * @param {String} tabPanelId Optional. If provided, this ID will be used
+   * instead of the tabId to retrieve and remove the corresponding <tabpanel>
+   */
+  removeTab: Task.async(function* (tabId, tabPanelId) {
+    this._tabbar.removeTab(tabId);
+
+    let win = this.getWindowForTab(tabId);
+    if (win && ("destroy" in win)) {
+      yield win.destroy();
+    }
+
+    this.emit("tab-unregistered", tabId);
+  }),
+
+  /**
+   * Show or hide a specific tab.
+   * @param {Boolean} isVisible True to show the tab/tabpanel, False to hide it.
+   * @param {String} id The ID of the tab to be hidden.
+   */
+  toggleTab: function (isVisible, id) {
+    this._tabbar.toggleTab(id, isVisible);
+  },
+
+  /**
+   * Select a specific tab.
+   */
+  select: function (id) {
+    this._tabbar.select(id);
+  },
+
+  /**
+   * Return the id of the selected tab.
+   */
+  getCurrentTabID: function () {
+    return this._currentTool;
+  },
+
+  /**
+   * Returns the requested tab panel based on the id.
+   * @param {String} id
+   * @return {DOMNode}
+   */
+  getTabPanel: function (id) {
+    // Search with and without the ID prefix as there might have been existing
+    // tabpanels by the time the sidebar got created
+    return this._panelDoc.querySelector("#" +
+      this.TABPANEL_ID_PREFIX + id + ", #" + id);
+  },
+
+  /**
+   * Event handler.
+   */
+  handleSelectionChange: function (id) {
+    if (this._destroyed) {
+      return;
+    }
+
+    let previousTool = this._currentTool;
+    if (previousTool) {
+      if (this._telemetry) {
+        this._telemetry.toolClosed(previousTool);
+      }
+      this.emit(previousTool + "-unselected");
+    }
+
+    this._currentTool = id;
+
+    if (this._telemetry) {
+      this._telemetry.toolOpened(this._currentTool);
+    }
+
+    this.emit(this._currentTool + "-selected");
+    this.emit("select", this._currentTool);
+  },
+
+  /**
+   * Show the sidebar.
+   *
+   * @param  {String} id
+   *         The sidebar tab id to select.
+   */
+  show: function (id) {
+    this._tabbox.removeAttribute("hidden");
+
+    // If an id is given, select the corresponding sidebar tab and record the
+    // tool opened.
+    if (id) {
+      this._currentTool = id;
+
+      if (this._telemetry) {
+        this._telemetry.toolOpened(this._currentTool);
+      }
+    }
+
+    this.emit("show");
+  },
+
+  /**
+   * Show the sidebar.
+   */
+  hide: function () {
+    this._tabbox.setAttribute("hidden", "true");
+
+    this.emit("hide");
+  },
+
+  /**
+   * Return the window containing the tab content.
+   */
+  getWindowForTab: function (id) {
+    // Get the tabpanel and make sure it contains an iframe
+    let panel = this.getTabPanel(id);
+    if (!panel || !panel.firstElementChild || !panel.firstElementChild.contentWindow) {
+      return null;
+    }
+
+    return panel.firstElementChild.contentWindow;
+  },
+
+  /**
+   * Clean-up.
+   */
+  destroy: Task.async(function* () {
+    if (this._destroyed) {
+      return;
+    }
+    this._destroyed = true;
+
+    this.emit("destroy");
+
+    // Note that we check for the existence of this._tabbox.tabpanels at each
+    // step as the container window may have been closed by the time one of the
+    // panel's destroy promise resolves.
+    let tabpanels = [...this._tabbox.querySelectorAll(".tab-panel-box")];
+    for (let panel of tabpanels) {
+      let iframe = panel.querySelector("iframe");
+      if (!iframe) {
+        continue;
+      }
+      let win = iframe.contentWindow;
+      if (win && ("destroy" in win)) {
+        yield win.destroy();
+      }
+      panel.remove();
+    }
+
+    if (this._currentTool && this._telemetry) {
+      this._telemetry.toolClosed(this._currentTool);
+    }
+
+    this._toolPanel.emit("sidebar-destroyed", this);
+
+    this._tabs = null;
+    this._tabbox = null;
+    this._panelDoc = null;
+    this._toolPanel = null;
+  })
+};
+
+XPCOMUtils.defineLazyGetter(this, "l10n", function () {
+  let bundle = Services.strings.createBundle(
+    "chrome://devtools/locale/toolbox.properties");
+
+  let l10n = function (name, ...args) {
+    try {
+      if (args.length == 0) {
+        return bundle.GetStringFromName(name);
+      }
+      return bundle.formatStringFromName(name, args, args.length);
+    } catch (err) {
+      console.error(err);
+    }
+    return null;
+  };
+  return l10n;
+});
--- a/devtools/client/jsonview/css/main.css
+++ b/devtools/client/jsonview/css/main.css
@@ -37,15 +37,21 @@
 
 /******************************************************************************/
 /* Theme Firebug */
 
 .theme-firebug .panelContent {
   height: calc(100% - 30px);
 }
 
+/* JSON View is using bigger font-size for the main tabs so,
+  let's overwrite the default value. */
+.theme-firebug .tabs .tabs-navigation {
+  font-size: 14px;
+}
+
 /******************************************************************************/
 /* Theme Light & Theme Dark*/
 
 .theme-dark .panelContent,
 .theme-light .panelContent {
   height: calc(100% - 27px);
 }
--- a/devtools/client/jsonview/json-viewer.js
+++ b/devtools/client/jsonview/json-viewer.js
@@ -24,17 +24,17 @@ define(function (require, exports, modul
   }
 
   // Application state object.
   let input = {
     jsonText: json.textContent,
     jsonPretty: null,
     json: jsonData,
     headers: JSON.parse(headers.textContent),
-    tabActive: 1,
+    tabActive: 0,
     prettified: false
   };
 
   json.remove();
   headers.remove();
 
   /**
    * Application actions/commands. This list implements all commands
--- a/devtools/client/locales/en-US/font-inspector.dtd
+++ b/devtools/client/locales/en-US/font-inspector.dtd
@@ -1,16 +1,15 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <!-- LOCALIZATION NOTE : FILE This file contains the Font Inspector strings.
   - The Font Inspector is the panel accessible in the Inspector sidebar. -->
 
-<!ENTITY fontInspectorTitle "Fonts">
 <!ENTITY showAllFonts "See all the fonts used in the page">
 <!ENTITY usedAs "Used as: ">
 <!ENTITY system "system">
 <!ENTITY remote "remote">
 
 <!-- LOCALIZATION NOTE (previewHint): This is the label shown as the
      placeholder in font inspector preview text box. -->
 <!ENTITY previewHint "Preview Text">
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -327,8 +327,35 @@ markupView.hide.key=h
 # LOCALIZATION NOTE (markupView.edit.key):
 # Key shortcut used to hide the selected node in the markup view.
 markupView.edit.key=F2
 
 # LOCALIZATION NOTE (markupView.scrollInto.key):
 # Key shortcut used to scroll the webpage in order to ensure the selected node
 # is visible
 markupView.scrollInto.key=s
+
+# LOCALIZATION NOTE (inspector.sidebar.fontInspectorTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying the list of fonts used in the page.
+inspector.sidebar.fontInspectorTitle=Fonts
+
+# LOCALIZATION NOTE (inspector.sidebar.ruleViewTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying the list of CSS rules used
+# in the page.
+inspector.sidebar.ruleViewTitle=Rules
+
+# LOCALIZATION NOTE (inspector.sidebar.computedViewTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying the list of computed CSS values
+# used in the page.
+inspector.sidebar.computedViewTitle=Computed
+
+# LOCALIZATION NOTE (inspector.sidebar.layoutViewTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying box model of the selected element.
+inspector.sidebar.layoutViewTitle=Box Model
+
+# LOCALIZATION NOTE (inspector.sidebar.animationInspectorTitle):
+# This is the title shown in a tab in the side panel of the Inspector panel
+# that corresponds to the tool displaying animations defined in the page.
+inspector.sidebar.animationInspectorTitle=Animations
--- a/devtools/client/locales/en-US/layoutview.dtd
+++ b/devtools/client/locales/en-US/layoutview.dtd
@@ -11,17 +11,16 @@
   - 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/locales/en-US/styleinspector.dtd
+++ b/devtools/client/locales/en-US/styleinspector.dtd
@@ -31,12 +31,8 @@
   -  shown when hovering over the `Toggle Pseudo Class Panel` button in the
   -  rule view toolbar. -->
 <!ENTITY togglePseudoClassPanel  "Toggle pseudo-classes">
 
 <!-- LOCALIZATION NOTE (noPropertiesFound): In the case where there are no CSS
   -  properties to display e.g. due to search criteria this message is
   -  displayed. -->
 <!ENTITY noPropertiesFound     "No CSS properties found.">
-
-<!-- FIXME: notes -->
-<!ENTITY computedViewTitle     "Computed">
-<!ENTITY ruleViewTitle         "Rules">
--- a/devtools/client/shared/components/reps/text-node.js
+++ b/devtools/client/shared/components/reps/text-node.js
@@ -6,19 +6,18 @@
 "use strict";
 
 // Make this available to both AMD and CJS environments
 define(function (require, exports, module) {
   // ReactJS
   const React = require("devtools/client/shared/vendor/react");
 
   // Reps
-  const { createFactories, isGrip } = require("./rep-utils");
+  const { createFactories, isGrip, cropMultipleLines } = require("./rep-utils");
   const { ObjectBox } = createFactories(require("./object-box"));
-  const { cropMultipleLines } = require("./string");
 
   // Shortcuts
   const DOM = React.DOM;
 
   /**
    * Renders DOM #text node.
    */
   let TextNode = React.createClass({
--- a/devtools/client/shared/components/tabs/moz.build
+++ b/devtools/client/shared/components/tabs/moz.build
@@ -1,10 +1,12 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
+    'tabbar.css',
+    'tabbar.js',
     'tabs.css',
     'tabs.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabbar.css
@@ -0,0 +1,54 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.tabs .tabs-navigation {
+  line-height: 15px;
+}
+
+.tabs .tabs-navigation {
+  height: 23px;
+}
+
+.tabs .tabs-menu-item:first-child {
+  border-inline-start-width: 0;
+}
+
+.tabs .tabs-navigation .tabs-menu-item:focus {
+  outline: var(--theme-focus-outline);
+  outline-offset: -2px;
+}
+
+.tabs .tabs-menu-item.is-active {
+  height: 23px;
+}
+
+/* Firebug theme is using slightly different height. */
+.theme-firebug .tabs .tabs-navigation {
+  height: 24px;
+}
+
+.tabs .tabs-menu-item a {
+  cursor: default;
+}
+
+/* The tab takes entire horizontal space and individual tabs
+  should stretch accordingly. Use flexbox for the behavior. */
+.tabs .tabs-navigation .tabs-menu {
+  display: flex;
+}
+
+.tabs .tabs-navigation .tabs-menu-item {
+  flex-grow: 1;
+}
+
+.tabs .tabs-navigation .tabs-menu-item a {
+  text-align: center;
+}
+
+/* Firebug theme doesn't stretch the tabs. */
+.theme-firebug .tabs .tabs-navigation .tabs-menu {
+  display: block;
+}
+
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabbar.js
@@ -0,0 +1,160 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { DOM, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const Tabs = createFactory(require("devtools/client/shared/components/tabs/tabs").Tabs);
+
+// Shortcuts
+const { div } = DOM;
+
+/**
+ * Renders Tabbar component.
+ */
+let Tabbar = createClass({
+  displayName: "Tabbar",
+
+  propTypes: {
+    onSelect: PropTypes.func,
+  },
+
+  getInitialState: function () {
+    return {
+      tabs: [],
+      activeTab: 0
+    };
+  },
+
+  // Public API
+
+  addTab: function (id, title, selected = false, panel, url) {
+    let tabs = this.state.tabs.slice();
+    tabs.push({id, title, panel, url});
+
+    let newState = Object.assign({}, this.state, {
+      tabs: tabs,
+    });
+
+    if (selected) {
+      newState.activeTab = tabs.length - 1;
+    }
+
+    this.setState(newState);
+  },
+
+  toggleTab: function (tabId, isVisible) {
+    let index = this.getTabIndex(tabId);
+    if (index < 0) {
+      return;
+    }
+
+    let tabs = this.state.tabs.slice();
+    tabs[index] = Object.assign({}, tabs[index], {
+      isVisible: isVisible
+    });
+
+    this.setState(Object.assign({}, this.state, {
+      tabs: tabs,
+    }));
+  },
+
+  removeTab: function (tabId) {
+    let index = this.getTabIndex(tabId);
+    if (index < 0) {
+      return;
+    }
+
+    let tabs = this.state.tabs.slice();
+    tabs.splice(index, 1);
+
+    this.setState(Object.assign({}, this.state, {
+      tabs: tabs,
+    }));
+  },
+
+  select: function (tabId) {
+    let index = this.getTabIndex(tabId);
+    if (index < 0) {
+      return;
+    }
+
+    let newState = Object.assign({}, this.state, {
+      activeTab: index,
+    });
+
+    this.setState(newState, () => {
+      if (this.props.onSelect) {
+        this.props.onSelect(tabId);
+      }
+    });
+  },
+
+  // Helpers
+
+  getTabIndex: function (tabId) {
+    let tabIndex = -1;
+    this.state.tabs.forEach((tab, index) => {
+      if (tab.id == tabId) {
+        tabIndex = index;
+      }
+    });
+    return tabIndex;
+  },
+
+  getTabId: function (index) {
+    return this.state.tabs[index].id;
+  },
+
+  getCurrentTabId: function () {
+    return this.state.tabs[this.state.activeTab].id;
+  },
+
+  // Event Handlers
+
+  onTabChanged: function (index) {
+    this.setState({
+      activeTab: index
+    });
+
+    if (this.props.onSelect) {
+      this.props.onSelect(this.state.tabs[index].id);
+    }
+  },
+
+  // Rendering
+
+  renderTab: function (tab) {
+    if (typeof tab.panel === "function") {
+      return tab.panel({
+        key: tab.id,
+        title: tab.title,
+        id: tab.id,
+        url: tab.url,
+      });
+    }
+
+    return tab.panel;
+  },
+
+  render: function () {
+    let tabs = this.state.tabs.map(tab => {
+      return this.renderTab(tab);
+    });
+
+    return (
+      div({className: "devtools-sidebar-tabs"},
+        Tabs({
+          tabActive: this.state.activeTab,
+          onAfterChange: this.onTabChanged},
+          tabs
+        )
+      )
+    );
+  },
+});
+
+module.exports = Tabbar;
--- a/devtools/client/shared/components/tabs/tabs.css
+++ b/devtools/client/shared/components/tabs/tabs.css
@@ -1,124 +1,65 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/******************************************************************************/
 /* Tabs General Styles */
 
 .tabs {
   height: 100%;
 }
 
-.tabs .tabs-navigation {
-  font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
-  font-size: 14px;
-  border-bottom-color: rgb(170, 188, 207);
-}
-
 .tabs .tabs-menu {
   display: table;
   list-style: none;
   padding: 0;
   margin: 0;
 }
 
 .tabs .tabs-menu-item {
   float: left;
 }
 
 .tabs .tabs-menu-item a {
   display: block;
   color: #A9A9A9;
   padding: 4px 8px;
-}
-
-.tabs .tabs-menu-item a {
   border: 1px solid transparent;
   text-decoration: none;
+  white-space: nowrap;
 }
 
-.tabs .tab-panel {
-  background-color: white;
+/* Make sure panel content takes entire vertical space.
+  (minus the height of the tab bar) */
+.tabs .panels {
+  height: calc(100% - 24px);
 }
 
-.tabs .tab-panel > DIV,
-.tabs .tab-panel > DIV > DIV {
+.tabs .tab-panel-box,
+.tabs .tab-panel {
   height: 100%;
 }
 
-/******************************************************************************/
-/* Firebug Theme */
-
-.theme-firebug .tabs {
-  background-color: rgb(219, 234, 249);
-  background-image: linear-gradient(rgba(253, 253, 253, 0.2), rgba(253, 253, 253, 0));
-}
-
-.theme-firebug .tabs .tabs-navigation {
-  padding-top: 3px;
-  padding-left: 3px;
-  height: 27px;
-  border-bottom: 1px solid rgb(170, 188, 207);
-}
-
-.theme-firebug .tabs .tab-panel {
-  height: calc(100% - 31px); /* minus the height of the tab bar */
-}
-
-.theme-firebug .tabs .tabs-menu-item {
-  position: relative;
-}
-
-.theme-firebug .tabs .tabs-menu-item a {
-  padding: 5px 8px 4px 8px;;
-  font-weight: bold;
-  color: #565656;
-  border-radius: 4px 4px 0 0;
-}
-
-.theme-firebug .tabs .tabs-menu-item.is-active a {
-  background-color: rgb(247, 251, 254);
-  border: 1px solid rgb(170, 188, 207);
-  border-bottom-color: transparent;
-}
-
-.theme-firebug .tabs .tabs-menu-item:hover a {
-  border: 1px solid #C8C8C8;
-  border-bottom: 1px solid transparent;
-  background-color: transparent !important;
-}
-
-.theme-firebug .tabs .tabs-menu-item.is-active:hover a {
-  border: 1px solid rgb(170, 188, 207) !important;
-  background-color: rgb(247, 251, 254) !important;
-  border-bottom-color: transparent !important;
-}
-
-/******************************************************************************/
 /* Light Theme */
 
 .theme-dark .tabs,
 .theme-light .tabs {
   background: var(--theme-tab-toolbar-background);
 }
 
 .theme-dark .tabs .tabs-navigation,
 .theme-light .tabs .tabs-navigation {
   border-bottom: 1px solid var(--theme-splitter-color);
-  box-shadow: 0 -2px 0 rgba(170, 170, 170,.1) inset;
-  height: 24px;
   font-size: 12px;
 }
 
-.theme-dark .tabs .tab-panel,
-.theme-light .tabs .tab-panel {
-  height: calc(100% - 24px); /* minus the height of the tab bar */
+.theme-firebug .tabs .tabs-navigation {
+  font-size: 11px;
 }
 
 .theme-dark .tabs .tabs-menu-item,
 .theme-light .tabs .tabs-menu-item {
   margin: 0;
   padding: 0;
   border-style: solid;
   border-width: 0;
@@ -130,19 +71,17 @@
 .theme-light .tabs .tabs-menu-item a {
   color: var(--theme-content-color1);
 }
 
 .theme-dark .tabs .tabs-menu-item a:hover,
 .theme-dark .tabs .tabs-menu-item a,
 .theme-light .tabs .tabs-menu-item a:hover,
 .theme-light .tabs .tabs-menu-item a {
-  border: none !important;
-  background-color: transparent !important;
-  padding: 5px 15px;
+  padding: 3px 15px;
 }
 
 .theme-dark .tabs .tabs-menu-item:hover,
 .theme-light .tabs .tabs-menu-item:hover {
   background-color: var(--toolbar-tab-hover);
 }
 
 .theme-dark .tabs .tabs-menu-item.is-active,
@@ -154,51 +93,72 @@
 
 .theme-dark .tabs .tabs-menu-item.is-active a,
 .theme-dark .tabs .tabs-menu-item.is-active:hover a,
 .theme-light .tabs .tabs-menu-item.is-active a,
 .theme-light .tabs .tabs-menu-item.is-active:hover a {
   color: var(--theme-selection-color);
 }
 
-.theme-dark .tabs .tabs-menu-item:active:hover,
-.theme-light .tabs .tabs-menu-item:active:hover {
-  background-color: var(--toolbar-tab-hover-active);
-}
-
-.theme-dark .tabs .tabs-menu-item.is-active,
-.theme-light .tabs .tabs-menu-item.is-active {
-  box-shadow: 0 2px 0 #d7f1ff inset,
-              0 8px 3px -5px #2b82bf inset,
-              0 -2px 0 rgba(0,0,0,.06) inset;
-}
-
-/******************************************************************************/
 /* Dark Theme */
 
-.theme-dark .tabs .tabs-navigation {
-  box-shadow: 0px -2px 0px rgba(0, 0, 0, 0.1) inset;
-}
-
 .theme-dark .tabs .tabs-menu-item a {
   color: #CED3D9;
 }
 
-.theme-dark .tabs .tabs-menu-item a:hover,
-.theme-dark .tabs .tabs-menu-item a {
-  border: none !important;
-  background-color: transparent !important;
-  padding: 5px 15px;
-}
-
 .theme-dark .tabs .tabs-menu-item:active:hover {
   background-color: hsla(206, 37%, 4%, .4); /* --toolbar-tab-hover-active */
 }
 
-.theme-dark .tabs .tabs-menu-item.is-active {
-  box-shadow: 0px 2px 0px #D7F1FF inset,
-   0px 8px 3px -5px #2B82BF inset,
-   0px -2px 0px rgba(0, 0, 0, 0.2) inset;
+/* Firebug Theme */
+
+.theme-firebug .tabs {
+  background-color: rgb(219, 234, 249);
+  background-image: linear-gradient(rgba(253, 253, 253, 0.2), rgba(253, 253, 253, 0));
+}
+
+.theme-firebug .tabs .tabs-navigation {
+  padding-top: 3px;
+  padding-left: 3px;
+  height: 27px;
+  border-bottom: 1px solid rgb(170, 188, 207);
+}
+
+.theme-firebug .tabs .tabs-menu-item.is-active,
+.theme-firebug .tabs .tabs-menu-item.is-active:hover {
+  background-color: transparent;
+}
+
+.theme-firebug .tabs .tabs-menu-item {
+  position: relative;
+  border-inline-start-width: 0;
 }
 
-.theme-dark .tabs .tab-panel {
-  background-color: var(--theme-body-background);
+.theme-firebug .tabs .tabs-menu-item a {
+  font-family: var(--proportional-font-family);
+  font-weight: bold;
+  color: var(--theme-body-color);
+  border-radius: 4px 4px 0 0;
+}
+
+.theme-firebug .tabs .tabs-menu-item:hover a {
+  border: 1px solid #C8C8C8;
+  border-bottom: 1px solid transparent;
+  background-color: transparent;
 }
+
+.theme-firebug .tabs .tabs-menu-item.is-active a,
+.theme-firebug .tabs .tabs-menu-item.is-active:hover a {
+  background-color: rgb(247, 251, 254);
+  border: 1px solid rgb(170, 188, 207);
+  border-bottom-color: transparent;
+  color: var(--theme-body-color);
+}
+
+.theme-firebug .tabs .tabs-menu-item a:hover,
+.theme-firebug .tabs .tabs-menu-item a {
+  border: 1px solid transparent;
+}
+
+.theme-firebug .tabs .tabs-menu-item a:hover,
+.theme-firebug .tabs .tabs-menu-item a {
+  padding: 4px 8px 4px 8px;
+}
--- a/devtools/client/shared/components/tabs/tabs.js
+++ b/devtools/client/shared/components/tabs/tabs.js
@@ -3,36 +3,37 @@
 /* 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";
 
 define(function (require, exports, module) {
   const React = require("devtools/client/shared/vendor/react");
-  const DOM = React.DOM;
+  const { DOM } = React;
+  const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
 
   /**
    * Renders simple 'tab' widget.
    *
    * Based on ReactSimpleTabs component
    * https://github.com/pedronauck/react-simpletabs
    *
    * Component markup (+CSS) example:
    *
    * <div class='tabs'>
    *  <nav class='tabs-navigation'>
    *    <ul class='tabs-menu'>
    *      <li class='tabs-menu-item is-active'>Tab #1</li>
    *      <li class='tabs-menu-item'>Tab #2</li>
    *    </ul>
    *  </nav>
-   *  <article class='tab-panel'>
+   *  <div class='tab-panel'>
    *    The content of active panel here
-   *  </article>
+   *  </div>
    * <div>
    */
   let Tabs = React.createClass({
     displayName: "Tabs",
 
     propTypes: {
       className: React.PropTypes.oneOfType([
         React.PropTypes.array,
@@ -46,124 +47,236 @@ define(function (require, exports, modul
       children: React.PropTypes.oneOfType([
         React.PropTypes.array,
         React.PropTypes.element
       ]).isRequired
     },
 
     getDefaultProps: function () {
       return {
-        tabActive: 1
+        tabActive: 0
       };
     },
 
     getInitialState: function () {
       return {
-        tabActive: this.props.tabActive
+        tabActive: this.props.tabActive,
+
+        // This array is used to store an information whether a tab
+        // at specific index has already been created (e.g. selected
+        // at least once).
+        // If yes, it's rendered even if not currently selected.
+        // This is because in some cases we don't want to re-create
+        // tab content when it's being unselected/selected.
+        // E.g. in case of an iframe being used as a tab-content
+        // we want the iframe to stay in the DOM.
+        created: [],
       };
     },
 
     componentDidMount: function () {
+      let node = findDOMNode(this);
+      node.addEventListener("keydown", this.onKeyDown, false);
+
       let index = this.state.tabActive;
       if (this.props.onMount) {
         this.props.onMount(index);
       }
     },
 
     componentWillReceiveProps: function (newProps) {
       if (newProps.tabActive) {
-        this.setState({tabActive: newProps.tabActive});
+        let created = [...this.state.created];
+        created[newProps.tabActive] = true;
+
+        this.setState(Object.assign({}, this.state, {
+          tabActive: newProps.tabActive,
+          created: created,
+        }));
       }
     },
 
-    setActive: function (index, e) {
+    componentWillUnmount: function () {
+      let node = findDOMNode(this);
+      node.removeEventListener("keydown", this.onKeyDown, false);
+    },
+
+    // DOM Events
+
+    onKeyDown: function (event) {
+      // Bail out if the focus isn't on a tab.
+      if (!event.target.closest(".tabs-menu-item")) {
+        return;
+      }
+
+      let tabActive = this.state.tabActive;
+      let tabCount = this.props.children.length;
+
+      switch (event.code) {
+        case "ArrowRight":
+          tabActive = Math.min(tabCount - 1, tabActive + 1);
+          break;
+        case "ArrowLeft":
+          tabActive = Math.max(0, tabActive - 1);
+          break;
+      }
+
+      if (this.state.tabActive != tabActive) {
+        this.setActive(tabActive);
+      }
+    },
+
+    onClickTab: function (index, event) {
+      this.setActive(index);
+      event.preventDefault();
+    },
+
+    // API
+
+    setActive: function (index) {
       let onAfterChange = this.props.onAfterChange;
       let onBeforeChange = this.props.onBeforeChange;
 
       if (onBeforeChange) {
         let cancel = onBeforeChange(index);
         if (cancel) {
           return;
         }
       }
 
-      let newState = {
-        tabActive: index
-      };
+      let created = [...this.state.created];
+      created[index] = true;
+
+      let newState = Object.assign({}, this.state, {
+        tabActive: index,
+        created: created
+      });
 
       this.setState(newState, () => {
+        // Properly set focus on selected tab.
+        let node = findDOMNode(this);
+        let selectedTab = node.querySelector(".is-active > a");
+        if (selectedTab) {
+          selectedTab.focus();
+        }
+
         if (onAfterChange) {
           onAfterChange(index);
         }
       });
-
-      e.preventDefault();
     },
 
-    getMenuItems: function () {
+    // Rendering
+
+    renderMenuItems: function () {
       if (!this.props.children) {
-        throw new Error("Tabs must contain at least one Panel");
+        throw new Error("There must be at least one Tab");
       }
 
       if (!Array.isArray(this.props.children)) {
         this.props.children = [this.props.children];
       }
 
-      let menuItems = this.props.children
-        .map(function (panel) {
-          return typeof panel === "function" ? panel() : panel;
-        }).filter(function (panel) {
-          return panel;
-        }).map(function (panel, index) {
-          let ref = ("tab-menu-" + (index + 1));
-          let title = panel.props.title;
-          let tabClassName = panel.props.className;
+      let tabs = this.props.children
+        .map(tab => {
+          return typeof tab === "function" ? tab() : tab;
+        }).filter(tab => {
+          return tab;
+        }).map((tab, index) => {
+          let ref = ("tab-menu-" + index);
+          let title = tab.props.title;
+          let tabClassName = tab.props.className;
 
           let classes = [
             "tabs-menu-item",
             tabClassName,
-            this.state.tabActive === (index + 1) && "is-active"
+            this.state.tabActive === index ? "is-active" : ""
           ].join(" ");
 
+          // Set tabindex to -1 (except the selected tab) so, it's focusable,
+          // but not reachable via sequential tab-key navigation.
+          // Changing selected tab (and so, moving focus) is done through
+          // left and right arrow keys.
+          // See also `onKeyDown()` event handler.
           return (
-            DOM.li({ref: ref, key: index, className: classes},
-              DOM.a({href: "#", onClick: this.setActive.bind(this, index + 1)},
+            DOM.li({
+              ref: ref,
+              key: index,
+              className: classes},
+              DOM.a({
+                href: "#",
+                tabIndex: this.state.tabActive === index ? 0 : -1,
+                onClick: this.onClickTab.bind(this, index)},
                 title
               )
             )
           );
-        }.bind(this));
+        });
 
       return (
         DOM.nav({className: "tabs-navigation"},
           DOM.ul({className: "tabs-menu"},
-            menuItems
+            tabs
           )
         )
       );
     },
 
-    getSelectedPanel: function () {
-      let index = this.state.tabActive - 1;
-      let panel = this.props.children[index];
+    renderPanels: function () {
+      if (!this.props.children) {
+        throw new Error("There must be at least one Tab");
+      }
+
+      if (!Array.isArray(this.props.children)) {
+        this.props.children = [this.props.children];
+      }
+
+      let selectedIndex = this.state.tabActive;
+
+      let panels = this.props.children
+        .map(tab => {
+          return typeof tab === "function" ? tab() : tab;
+        }).filter(tab => {
+          return tab;
+        }).map((tab, index) => {
+          let selected = selectedIndex == index;
+
+          // Use 'visibility:hidden' + 'width/height:0' for hiding
+          // content of non-selected tab. It's faster (not sure why)
+          // than display:none and visibility:collapse.
+          let style = {
+            visibility: selected ? "visible" : "hidden",
+            height: selected ? "100%" : "0",
+            width: selected ? "100%" : "0",
+          };
+
+          return (
+            DOM.div({
+              key: index,
+              style: style,
+              className: "tab-panel-box"},
+              (selected || this.state.created[index]) ? tab : null
+            )
+          );
+        });
 
       return (
-        DOM.article({ref: "tab-panel", className: "tab-panel"},
-          panel
+        DOM.div({className: "panels"},
+          panels
         )
       );
     },
 
     render: function () {
       let classNames = ["tabs", this.props.className].join(" ");
 
       return (
         DOM.div({className: classNames},
-          this.getMenuItems(),
-          this.getSelectedPanel()
+          this.renderMenuItems(),
+          this.renderPanels()
         )
       );
     },
   });
 
   /**
    * Renders simple tab 'panel'.
    */
@@ -174,17 +287,17 @@ define(function (require, exports, modul
       title: React.PropTypes.string.isRequired,
       children: React.PropTypes.oneOfType([
         React.PropTypes.array,
         React.PropTypes.element
       ]).isRequired
     },
 
     render: function () {
-      return DOM.div({},
+      return DOM.div({className: "tab-panel"},
         this.props.children
       );
     }
   });
 
   // Exports from this module
   exports.TabPanel = Panel;
   exports.Tabs = Tabs;
--- a/devtools/client/shared/components/test/mochitest/chrome.ini
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -15,16 +15,17 @@ support-files =
 [test_reps_grip-array.html]
 [test_reps_null.html]
 [test_reps_number.html]
 [test_reps_object.html]
 [test_reps_object-with-text.html]
 [test_reps_object-with-url.html]
 [test_reps_string.html]
 [test_reps_stylesheet.html]
+[test_reps_text-node.html]
 [test_reps_undefined.html]
 [test_reps_window.html]
 [test_sidebar_toggle.html]
 [test_tree_01.html]
 [test_tree_02.html]
 [test_tree_03.html]
 [test_tree_04.html]
 [test_tree_05.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_text-node.html
@@ -0,0 +1,60 @@
+
+<!DOCTYPE HTML>
+<html>
+<!--
+Test text-node rep
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Rep test - text-node</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">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+  try {
+    let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+    let { TextNode } = browserRequire("devtools/client/shared/components/reps/text-node");
+
+    let gripStub = {
+      "type": "object",
+      "class": "Text",
+      "actor": "server1.conn1.child1/obj50",
+      "extensible": true,
+      "frozen": false,
+      "sealed": false,
+      "ownPropertyLength": 0,
+      "preview": {
+        "kind": "DOMNode",
+        "nodeType": 3,
+        "nodeName": "#text",
+        "textContent": "hello world"
+      }
+    };
+
+    // Test that correct rep is chosen
+    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+    is(renderedRep.type, TextNode.rep,
+      `Rep correctly selects ${TextNode.rep.displayName}`);
+
+    // Test rendering
+    const renderedComponent = renderComponent(TextNode.rep, { object: gripStub });
+    is(renderedComponent.className, "objectBox objectBox-textNode",
+      "TextNode rep has expected class names");
+    is(renderedComponent.textContent, `"hello world"`,
+      "TextNode rep has expected text content");
+  } catch (e) {
+    ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+  } finally {
+    SimpleTest.finish();
+  }
+});
+</script>
+</pre>
+</body>
+</html>
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -100,16 +100,21 @@ body {
 }
 
 [timeline] #timeline-toolbar {
   display: flex;
 }
 
 /* The main animations container */
 
+#sidebar-panel-animationinspector {
+  height: 100%;
+  width: 100%;
+}
+
 #players {
   height: calc(100% - var(--toolbar-height));
   overflow-x: hidden;
   overflow-y: auto;
 }
 
 [empty] #players {
   display: none;
--- a/devtools/client/themes/computed.css
+++ b/devtools/client/themes/computed.css
@@ -3,35 +3,44 @@
  * 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-computedview {
   margin: 0;
   display : flex;
   flex-direction: column;
   width: 100%;
-  /* Bug 1243598 - Reduce the container height by the tab height to make room
-     for the tabs above. */
-  height: calc(100% - 24px);
-  position: absolute;
+  height: 100%;
 }
 
 #sidebar-panel-computedview > .devtools-toolbar {
   display: flex;
 }
 
 #browser-style-checkbox {
   /* Bug 1200073 - extra space before the browser styles checkbox so
-     they aren't squished together in a small window. */
+     they aren't squished together in a small window. Put also
+     an extra space after. */
   margin-inline-start: 5px;
+  margin-inline-end: 5px;
+}
+
+#browser-style-checkbox-label {
+  margin-right: 5px;
+
+  /* Vertically center the 'Browser styles' checkbox in the
+     Computed panel with its label. */
+  display: flex;
+  align-items: center;
 }
 
 #propertyContainer {
   -moz-user-select: text;
-  overflow: auto;
+  overflow-y: auto;
+  overflow-x: hidden;
   flex: auto;
 }
 
 .row-striped {
   background: var(--theme-body-background);
 }
 
 .property-view-hidden,
--- a/devtools/client/themes/fonts.css
+++ b/devtools/client/themes/fonts.css
@@ -3,20 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #sidebar-panel-fontinspector {
   margin: 0;
   display: flex;
   flex-direction: column;
   padding-bottom: 20px;
   width: 100%;
-  /* Bug 1243598 - Reduce the container height by the tab height to make room
-     for the tabs above. */
-  height: calc(100% - 24px);
-  position: absolute;
+  height: 100%;
 }
 
 #sidebar-panel-fontinspector > .devtools-toolbar {
   display: flex;
 }
 
 #font-container {
   overflow: auto;
--- a/devtools/client/themes/layout.css
+++ b/devtools/client/themes/layout.css
@@ -1,15 +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/ */
 
 #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%;
 }
 
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -27,20 +27,17 @@
   font-size: 11px;
 }
 
 #sidebar-panel-ruleview {
   margin: 0;
   display: flex;
   flex-direction: column;
   width: 100%;
-  /* Bug 1243598 - Reduce the container height by the tab height to make room
-     for the tabs above. */
-  height: calc(100% - 24px);
-  position: absolute;
+  height: 100%;
 }
 
 /* Rule View Toolbar */
 
 #ruleview-toolbar-container {
   display: flex;
   flex-direction: column;
   height: auto;
@@ -78,16 +75,17 @@
 }
 
 /* Rule View Container */
 
 #ruleview-container {
   -moz-user-select: text;
   overflow: auto;
   flex: auto;
+  height: 100%;
 }
 
 #ruleview-container.non-interactive {
   pointer-events: none;
   visibility: collapse;
   transition: visibility 0.25s;
 }
 
--- a/devtools/client/themes/toolbars.css
+++ b/devtools/client/themes/toolbars.css
@@ -501,16 +501,17 @@
 .devtools-filterinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear:hover {
   -moz-image-region: rect(0, 32px, 16px, 16px);
 }
 
 /* In-tools sidebar */
 .devtools-sidebar-tabs {
   -moz-appearance: none;
   margin: 0;
+  height: 100%;
 }
 
 .devtools-sidebar-tabs > tabpanels {
   -moz-appearance: none;
   background: transparent;
   padding: 0;
   border: 0;
 }
--- a/devtools/client/webconsole/new-console-output/components/grip-message-body.js
+++ b/devtools/client/webconsole/new-console-output/components/grip-message-body.js
@@ -3,28 +3,31 @@
 /* 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";
 
 // React
 const {
+  createFactory,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
 const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+const VariablesViewLink = createFactory(require("devtools/client/webconsole/new-console-output/components/variables-view-link").VariablesViewLink);
 const { Grip } = require("devtools/client/shared/components/reps/grip");
 
 GripMessageBody.displayName = "GripMessageBody";
 
 GripMessageBody.propTypes = {
   grip: PropTypes.object.isRequired,
 };
 
 function GripMessageBody(props) {
   return Rep({
     object: props.grip,
+    objectLink: VariablesViewLink,
     defaultRep: Grip
   });
 }
 
 module.exports.GripMessageBody = GripMessageBody;
--- a/devtools/client/webconsole/new-console-output/test/components/head.js
+++ b/devtools/client/webconsole/new-console-output/test/components/head.js
@@ -34,17 +34,17 @@ testCommands.set("console.log()", {
   commandType: "consoleAPICall",
   // @TODO should output: foobar test
   expectedText: "\"foobar\"\"test\""
 });
 testCommands.set("new Date()", {
   command: "new Date(448156800000)",
   commandType: "evaluationResult",
   // @TODO should output: Date 1984-03-15T00:00:00.000Z
-  expectedText: "1984-03-15T00:00:00.000Z"
+  expectedText: "Date1984-03-15T00:00:00.000Z"
 });
 testCommands.set("pageError", {
   command: null,
   commandType: "pageError",
   expectedText: "ReferenceError: asdf is not defined"
 });
 
 function* getPacket(command, type = "evaluationResult") {
--- a/devtools/server/actors/source.js
+++ b/devtools/server/actors/source.js
@@ -216,16 +216,17 @@ let SourceActor = ActorClassWithSpec(sou
       actor: this.actorID,
       generatedUrl: this.generatedSource ? this.generatedSource.url : null,
       url: this.url ? this.url.split(" -> ").pop() : null,
       addonID: this._addonID,
       addonPath: this._addonPath,
       isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url),
       isPrettyPrinted: this.threadActor.sources.isPrettyPrinted(this.url),
       isSourceMapped: this.isSourceMapped,
+      sourceMapURL: source ? source.sourceMapURL : null,
       introductionUrl: introductionUrl ? introductionUrl.split(" -> ").pop() : null,
       introductionType: source ? source.introductionType : null
     };
   },
 
   disconnect: function () {
     if (this.registeredPool && this.registeredPool.sourceActors) {
       delete this.registeredPool.sourceActors[this.actorID];
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -59,17 +59,18 @@ PageAction.prototype = {
 
     if (this.options.icon) {
       this.id = PageActions.add(this.options);
       return Promise.resolve();
     }
 
     this.shouldShow = true;
 
-    let {icon} = IconDetails.getURL(this.icons, context.contentWindow, this.extension, 18);
+    let {icon} = IconDetails.getPreferredIcon(this.icons, this.extension,
+                                              18 * context.contentWindow.devicePixelRatio);
 
     let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
     return IconDetails.convertImageURLToDataURL(icon, context, browserWindow).then(dataURI => {
       if (this.shouldShow) {
         this.options.icon = dataURI;
         this.id = PageActions.add(this.options);
       }
     }).catch(() => {
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -1055,16 +1055,18 @@ class RunProgram(MachCommandBase):
         help='Command-line arguments to be passed through to the program. Not specifying a --profile or -P option will result in a temporary profile being used.')
     @CommandArgumentGroup(prog_group)
     @CommandArgument('--remote', '-r', action='store_true', group=prog_group,
         help='Do not pass the --no-remote argument by default.')
     @CommandArgument('--background', '-b', action='store_true', group=prog_group,
         help='Do not pass the --foreground argument by default on Mac.')
     @CommandArgument('--noprofile', '-n', action='store_true', group=prog_group,
         help='Do not pass the --profile argument by default.')
+    @CommandArgument('--disable-e10s', action='store_true', group=prog_group,
+        help='Run the program with electrolysis disabled.')
 
     @CommandArgumentGroup('debugging')
     @CommandArgument('--debug', action='store_true', group='debugging',
         help='Enable the debugger. Not specifying a --debugger option will result in the default debugger being used.')
     @CommandArgument('--debugger', default=None, type=str, group='debugging',
         help='Name of debugger to use.')
     @CommandArgument('--debugparams', default=None, metavar='params', type=str,
         group='debugging',
@@ -1081,18 +1083,18 @@ class RunProgram(MachCommandBase):
     @CommandArgument('--dmd', action='store_true', group='DMD',
         help='Enable DMD. The following arguments have no effect without this.')
     @CommandArgument('--mode', choices=['live', 'dark-matter', 'cumulative', 'scan'], group='DMD',
          help='Profiling mode. The default is \'dark-matter\'.')
     @CommandArgument('--stacks', choices=['partial', 'full'], group='DMD',
         help='Allocation stack trace coverage. The default is \'partial\'.')
     @CommandArgument('--show-dump-stats', action='store_true', group='DMD',
         help='Show stats when doing dumps.')
-    def run(self, params, remote, background, noprofile, debug, debugger,
-        debugparams, slowscript, dmd, mode, stacks, show_dump_stats):
+    def run(self, params, remote, background, noprofile, disable_e10s, debug,
+        debugger, debugparams, slowscript, dmd, mode, stacks, show_dump_stats):
 
         if conditions.is_android(self):
             # Running Firefox for Android is completely different
             if dmd:
                 print("DMD is not supported for Firefox for Android")
                 return 1
             from mozrunner.devices.android_device import verify_android_device, run_firefox_for_android
             if not (debug or debugger or debugparams):
@@ -1127,16 +1129,18 @@ class RunProgram(MachCommandBase):
             if no_profile_option_given and not noprofile:
                 path = os.path.join(self.topobjdir, 'tmp', 'scratch_user')
                 if not os.path.isdir(path):
                     os.makedirs(path)
                 args.append('-profile')
                 args.append(path)
 
         extra_env = {'MOZ_CRASHREPORTER_DISABLE': '1'}
+        if disable_e10s:
+            extra_env['MOZ_FORCE_DISABLE_E10S'] = '1'
 
         if debug or debugger or debugparams:
             if 'INSIDE_EMACS' in os.environ:
                 self.log_manager.terminal_handler.setLevel(logging.WARNING)
 
             import mozdebug
             if not debugger:
                 # No debugger name was provided. Look for the default ones on
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -70,16 +70,20 @@ 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);
+  },
+
   // 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,
     };
 
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -469,21 +469,19 @@ let IconDetails = {
       extension.manifestError(`Invalid icon data: ${e}`);
     }
 
     return result;
   },
 
   // Returns the appropriate icon URL for the given icons object and the
   // screen resolution of the given window.
-  getURL(icons, window, extension, size = 16) {
+  getPreferredIcon(icons, extension = null, size = 16) {
     const DEFAULT = "chrome://browser/content/extension.svg";
 
-    size *= window.devicePixelRatio;
-
     let bestSize = null;
     if (icons[size]) {
       bestSize = size;
     } else if (icons[2 * size]) {
       bestSize = 2 * size;
     } else {
       let sizes = Object.keys(icons)
                         .map(key => parseInt(key, 10))
--- a/toolkit/themes/linux/global/findBar.css
+++ b/toolkit/themes/linux/global/findBar.css
@@ -80,19 +80,18 @@ findbar[noanim] {
   border-width: 1px;
   border-radius: 3px;
 }
 
 .findbar-find-previous,
 .findbar-find-next {
   margin-inline-start: 0;
   -moz-appearance: none;
-  background: linear-gradient(rgba(255,255,255,.9), rgba(255,255,255,.2));
+  background: linear-gradient(rgba(255,255,255,.8) 1px, rgba(255,255,255,.4) 1px, rgba(255,255,255,.1));
   border: 1px solid ThreeDShadow;
-  box-shadow: 0 1px #fff inset;
   padding: 5px 9px;
   line-height: 1em;
 }
 
 .findbar-find-previous:focus,
 .findbar-find-next:focus {
   border-color: Highlight;
   box-shadow: 0 0 1px 0 Highlight inset;
--- a/toolkit/themes/windows/global/findBar.css
+++ b/toolkit/themes/windows/global/findBar.css
@@ -78,19 +78,18 @@ findbar[noanim] {
 }
 
 /* Buttons */
 
 .findbar-find-previous,
 .findbar-find-next {
   margin-inline-start: 0;
   -moz-appearance: none;
-  background: linear-gradient(rgba(255,255,255,.9), rgba(255,255,255,.2));
+  background: linear-gradient(rgba(255,255,255,.8) 1px, rgba(255,255,255,.4) 1px, rgba(255,255,255,.1));
   border: 1px solid ThreeDShadow;
-  box-shadow: 0 1px #fff inset;
   padding: 1px 5px;
   line-height: 1em;
 }
 
 .findbar-find-previous:not([disabled]):active,
 .findbar-find-next:not([disabled]):active {
   background: rgba(23,50,76,.2);
   box-shadow: 0 1px 2px rgba(10,31,51,.2) inset;
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -4711,17 +4711,18 @@ mozilla::BrowserTabsRemoteAutostart()
   if (Preferences::GetBool(kForceEnableE10sPref, false)) {
     gBrowserTabsRemoteAutostart = true;
     prefEnabled = true;
     status = kE10sEnabledByUser;
   }
 
   // Uber override pref for emergency blocking
   if (gBrowserTabsRemoteAutostart &&
-      Preferences::GetBool(kForceDisableE10sPref, false)) {
+      (Preferences::GetBool(kForceDisableE10sPref, false) ||
+       EnvHasValue("MOZ_FORCE_DISABLE_E10S"))) {
     gBrowserTabsRemoteAutostart = false;
     status = kE10sForceDisabled;
   }
 
   gBrowserTabsRemoteStatus = status;
 
   mozilla::Telemetry::Accumulate(mozilla::Telemetry::E10S_STATUS, status);
   if (prefEnabled) {