Bug 1101569 - Adds an allTabs menu to the sidebar widget; r=dcamp
authorPatrick Brosset <pbrosset@mozilla.com>
Thu, 15 Jan 2015 10:47:12 +0100
changeset 223845 e40dfb7e4ec71f540e6359316cc7001e28017e9e
parent 223844 d0b17fe68e831f166c467c3c57534b929ccf022b
child 223846 e19a51f9e09c46f39c913b446a65313fe5cc67f5
push id10831
push userpbrosset@mozilla.com
push dateThu, 15 Jan 2015 09:48:12 +0000
treeherderfx-team@e40dfb7e4ec7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdcamp
bugs1101569
milestone38.0a1
Bug 1101569 - Adds an allTabs menu to the sidebar widget; r=dcamp
browser/devtools/framework/sidebar.js
browser/devtools/framework/test/browser.ini
browser/devtools/framework/test/browser_toolbox_options_disable_js.js
browser/devtools/framework/test/browser_toolbox_sidebar.js
browser/devtools/framework/test/browser_toolbox_sidebar_existing_tabs.js
browser/devtools/framework/test/browser_toolbox_sidebar_overflow_menu.js
browser/devtools/framework/test/browser_toolbox_sidebar_tool.xul
browser/devtools/framework/test/head.js
browser/devtools/framework/test/helper_disable_cache.js
browser/devtools/inspector/inspector-panel.js
browser/devtools/webconsole/webconsole.js
browser/locales/en-US/chrome/browser/devtools/toolbox.properties
browser/themes/shared/devtools/dark-theme.css
browser/themes/shared/devtools/light-theme.css
browser/themes/shared/devtools/toolbars.inc.css
--- a/browser/devtools/framework/sidebar.js
+++ b/browser/devtools/framework/sidebar.js
@@ -1,289 +1,542 @@
 /* -*- 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/. */
 
 const {Cu} = require("chrome");
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 var {Promise: promise} = require("resource://gre/modules/Promise.jsm");
 var EventEmitter = require("devtools/toolkit/event-emitter");
 var Telemetry = require("devtools/shared/telemetry");
 
 const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 /**
  * ToolSidebar provides methods to register tabs in the sidebar.
  * It's assumed that the sidebar contains a xul:tabbox.
+ * Typically, you'll want the tabbox parameter to be a XUL tabbox like this:
+ *
+ * <tabbox id="inspector-sidebar" handleCtrlTab="false" class="devtools-sidebar-tabs">
+ *   <tabs/>
+ *   <tabpanels flex="1"/>
+ * </tabbox>
+ *
+ * The ToolSidebar API has a method to add new tabs, so the tabs and tabpanels
+ * nodes can be empty. But they can also already contain items before the
+ * ToolSidebar is created.
+ *
+ * Tabs added through the addTab method are only identified by an ID and a URL
+ * which is used as the href of an iframe node that is inserted in the newly
+ * created tabpanel.
+ * Tabs already present before the ToolSidebar is created may contain anything.
+ * However, these tabs must have ID attributes if it is required for the various
+ * methods that accept an ID as argument to work here.
  *
  * @param {Node} tabbox
  *  <tabbox> node;
  * @param {ToolPanel} panel
  *  Related ToolPanel instance;
  * @param {String} uid
  *  Unique ID
- * @param {Boolean} showTabstripe
- *  Show the tabs.
+ * @param {Object} options
+ *  - hideTabstripe: Should the tabs be hidden. Defaults to false
+ *  - showAllTabsMenu: Should a drop-down menu be displayed in case tabs
+ *    become hidden. Defaults to false.
+ *  - disableTelemetry: By default, switching tabs on and off in the sidebar
+ *    will record tool usage in telemetry, pass this option to true to avoid it.
+ *
+ * Events raised:
+ * - new-tab-registered : After a tab has been added via addTab. The tab ID
+ *   is passed with the event. This however, is raised before the tab iframe
+ *   is fully loaded.
+ * - <tabid>-ready : After the tab iframe has been loaded
+ * - <tabid>-selected : After tab <tabid> was selected
+ * - select : Same as above, but for any tab, the ID is passed with the event
+ * - <tabid>-unselected : After tab <tabid> is unselected
  */
-function ToolSidebar(tabbox, panel, uid, showTabstripe=true)
-{
+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;
+
+  this._onTabBoxOverflow = this._onTabBoxOverflow.bind(this);
+  this._onTabBoxUnderflow = this._onTabBoxUnderflow.bind(this);
 
   try {
     this._width = Services.prefs.getIntPref("devtools.toolsidebar-width." + this._uid);
   } catch(e) {}
 
-  this._telemetry = new Telemetry();
+  if (!options.disableTelemetry) {
+    this._telemetry = new Telemetry();
+  }
 
   this._tabbox.tabpanels.addEventListener("select", this, true);
 
   this._tabs = new Map();
 
-  if (!showTabstripe) {
+  // Check for existing tabs in the DOM and add them.
+  this.addExistingTabs();
+
+  if (this._options.hideTabstripe) {
     this._tabbox.setAttribute("hidetabs", "true");
   }
 
+  if (this._options.showAllTabsMenu) {
+    this.addAllTabsMenu();
+  }
+
   this._toolPanel.emit("sidebar-created", this);
 }
 
 exports.ToolSidebar = ToolSidebar;
 
 ToolSidebar.prototype = {
+  TAB_ID_PREFIX: "sidebar-tab-",
+
+  TABPANEL_ID_PREFIX: "sidebar-panel-",
+
+  /**
+   * Add a "…" button at the end of the tabstripe that toggles a dropdown menu
+   * containing the list of all tabs if any become hidden due to lack of room.
+   *
+   * If the ToolSidebar was created with the "showAllTabsMenu" option set to
+   * true, this is already done automatically. If not, you may call this
+   * function at any time to add the menu.
+   */
+  addAllTabsMenu: function() {
+    if (this._allTabsBtn) {
+      return;
+    }
+
+    let tabs = this._tabbox.tabs;
+
+    // Create a toolbar and insert it first in the tabbox
+    let allTabsToolbar = this._panelDoc.createElementNS(XULNS, "toolbar");
+    this._tabbox.insertBefore(allTabsToolbar, tabs);
+
+    // Move the tabs inside and make them flex
+    allTabsToolbar.appendChild(tabs);
+    tabs.setAttribute("flex", "1");
+
+    // Create the dropdown menu next to the tabs
+    this._allTabsBtn = this._panelDoc.createElementNS(XULNS, "toolbarbutton");
+    this._allTabsBtn.setAttribute("class", "devtools-sidebar-alltabs");
+    this._allTabsBtn.setAttribute("type", "menu");
+    this._allTabsBtn.setAttribute("label", l10n("sidebar.showAllTabs.label"));
+    this._allTabsBtn.setAttribute("tooltiptext", l10n("sidebar.showAllTabs.tooltip"));
+    this._allTabsBtn.setAttribute("hidden", "true");
+    allTabsToolbar.appendChild(this._allTabsBtn);
+
+    let menuPopup = this._panelDoc.createElementNS(XULNS, "menupopup");
+    this._allTabsBtn.appendChild(menuPopup);
+
+    // Listening to tabs overflow event to toggle the alltabs button
+    tabs.addEventListener("overflow", this._onTabBoxOverflow, false);
+    tabs.addEventListener("underflow", this._onTabBoxUnderflow, false);
+
+    // Add menuitems to the alltabs menu if there are already tabs in the
+    // sidebar
+    for (let [id, tab] of this._tabs) {
+      this._addItemToAllTabsMenu(id, tab, tab.hasAttribute("selected"));
+    }
+  },
+
+  removeAllTabsMenu: function() {
+    if (!this._allTabsBtn) {
+      return;
+    }
+
+    let tabs = this._tabbox.tabs;
+
+    tabs.removeEventListener("overflow", this._onTabBoxOverflow, false);
+    tabs.removeEventListener("underflow", this._onTabBoxUnderflow, false);
+
+    // Moving back the tabs as a first child of the tabbox
+    this._tabbox.insertBefore(tabs, this._tabbox.tabpanels);
+    this._tabbox.querySelector("toolbar").remove();
+
+    this._allTabsBtn = null;
+  },
+
+  _onTabBoxOverflow: function() {
+    this._allTabsBtn.removeAttribute("hidden");
+  },
+
+  _onTabBoxUnderflow: function() {
+    this._allTabsBtn.setAttribute("hidden", "true");
+  },
+
+  /**
+   * Add an item in the allTabs menu for a given tab.
+   */
+  _addItemToAllTabsMenu: function(id, tab, selected=false) {
+    if (!this._allTabsBtn) {
+      return;
+    }
+
+    let item = this._panelDoc.createElementNS(XULNS, "menuitem");
+    item.setAttribute("id", "sidebar-alltabs-item-" + id);
+    item.setAttribute("label", tab.getAttribute("label"));
+    item.setAttribute("type", "checkbox");
+    if (selected) {
+      item.setAttribute("checked", true);
+    }
+    // The auto-checking of menuitems in this menu doesn't work, so let's do
+    // it manually
+    item.setAttribute("autocheck", false);
+
+    this._allTabsBtn.querySelector("menupopup").appendChild(item);
+
+    item.addEventListener("click", () => {
+      this._tabbox.selectedTab = tab;
+    }, false);
+
+    tab.allTabsMenuItem = item;
+
+    return item;
+  },
+
   /**
    * 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
    */
-  addTab: function ToolSidebar_addTab(id, url, selected=false) {
+  addTab: function(id, url, selected=false) {
     let iframe = this._panelDoc.createElementNS(XULNS, "iframe");
     iframe.className = "iframe-" + id;
     iframe.setAttribute("flex", "1");
     iframe.setAttribute("src", url);
     iframe.tooltip = "aHTMLTooltip";
 
-    let tab = this._tabbox.tabs.appendItem();
+    // Creating the tab and adding it to the tabbox
+    let tab = this._panelDoc.createElementNS(XULNS, "tab");
+    this._tabbox.tabs.appendChild(tab);
     tab.setAttribute("label", ""); // Avoid showing "undefined" while the tab is loading
-    tab.setAttribute("id", "sidebar-tab-" + id);
+    tab.setAttribute("id", this.TAB_ID_PREFIX + id);
+
+    // Add the tab to the allTabs menu if exists
+    let allTabsItem = this._addItemToAllTabsMenu(id, tab, selected);
 
     let onIFrameLoaded = (event) => {
       let doc = event.target;
       let win = doc.defaultView;
       tab.setAttribute("label", doc.title);
 
+      if (allTabsItem) {
+        allTabsItem.setAttribute("label", doc.title);
+      }
+
       iframe.removeEventListener("load", onIFrameLoaded, true);
       if ("setPanel" in win) {
         win.setPanel(this._toolPanel, iframe);
       }
       this.emit(id + "-ready");
     };
 
     iframe.addEventListener("load", onIFrameLoaded, true);
 
     let tabpanel = this._panelDoc.createElementNS(XULNS, "tabpanel");
-    tabpanel.setAttribute("id", "sidebar-panel-" + id);
+    tabpanel.setAttribute("id", this.TABPANEL_ID_PREFIX + id);
     tabpanel.appendChild(iframe);
     this._tabbox.tabpanels.appendChild(tabpanel);
 
     this._tooltip = this._panelDoc.createElementNS(XULNS, "tooltip");
     this._tooltip.id = "aHTMLTooltip";
     tabpanel.appendChild(this._tooltip);
     this._tooltip.page = true;
 
-    tab.linkedPanel = "sidebar-panel-" + id;
+    tab.linkedPanel = this.TABPANEL_ID_PREFIX + id;
 
     // We store the index of this tab.
     this._tabs.set(id, tab);
 
     if (selected) {
       // For some reason I don't understand, if we call this.select in this
       // event loop (after inserting the tab), the tab will never get the
       // the "selected" attribute set to true.
       this._panelDoc.defaultView.setTimeout(() => {
         this.select(id);
       }, 10);
     }
 
     this.emit("new-tab-registered", id);
   },
 
+  untitledTabsIndex: 0,
+
+  /**
+   * Search for existing tabs in the markup that aren't know yet and add them.
+   */
+  addExistingTabs: function() {
+    let knownTabs = [...this._tabs.values()];
+
+    for (let tab of this._tabbox.tabs.querySelectorAll("tab")) {
+      if (knownTabs.indexOf(tab) !== -1) {
+        continue;
+      }
+
+      // Find an ID for this unknown tab
+      let id = tab.getAttribute("id") || "untitled-tab-" + (this.untitledTabsIndex++);
+
+      // Register the tab
+      this._tabs.set(id, tab);
+      this.emit("new-tab-registered", id);
+    }
+  },
+
   /**
    * 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*(id) {
-    let tab = this._tabbox.tabs.querySelector("tab#sidebar-tab-" + id);
+  removeTab: Task.async(function*(tabId, tabPanelId) {
+    // Remove the tab if it can be found
+    let tab = this.getTab(tabId);
     if (!tab) {
       return;
     }
 
-    let win = this.getWindowForTab(id);
-    if ("destroy" in win) {
+    let win = this.getWindowForTab(tabId);
+    if (win && ("destroy" in win)) {
       yield win.destroy();
     }
 
     tab.remove();
 
-    let panel = this.getTab(id);
+    // Also remove the tabpanel
+    let panel = this.getTabPanel(tabPanelId || tabId);
     if (panel) {
       panel.remove();
     }
 
-    this._tabs.delete(id);
+    this._tabs.delete(tabId);
+    this.emit("tab-unregistered", tabId);
+  }),
 
-    this.emit("tab-unregistered", id);
-  }),
+  /**
+   * Show or hide a specific tab
+   */
+  toggleTab: function(id, isVisible) {
+    let tab = this.getTab(id);
+    if (!tab) {
+      return;
+    }
+    tab.hidden = !isVisible;
+    if (this._allTabsBtn) {
+      this._allTabsBtn.querySelector("#sidebar-alltabs-item-" + id).hidden = !isVisible;
+    }
+  },
 
   /**
    * Select a specific tab.
    */
-  select: function ToolSidebar_select(id) {
-    let tab = this._tabs.get(id);
+  select: function(id) {
+    let tab = this.getTab(id);
     if (tab) {
       this._tabbox.selectedTab = tab;
     }
   },
 
   /**
    * Return the id of the selected tab.
    */
-  getCurrentTabID: function ToolSidebar_getCurrentTabID() {
+  getCurrentTabID: function() {
     let currentID = null;
     for (let [id, tab] of this._tabs) {
       if (this._tabbox.tabs.selectedItem == tab) {
         currentID = id;
         break;
       }
     }
     return currentID;
   },
 
   /**
-   * Returns the requested tab based on the id.
-   *
-   * @param String id
-   *        unique id of the requested tab.
+   * Returns the requested tab panel based on the id.
+   * @param {String} id
+   * @return {DOMNode}
    */
-  getTab: function ToolSidebar_getTab(id) {
-    return this._tabbox.tabpanels.querySelector("#sidebar-panel-" + id);
+  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._tabbox.tabpanels.querySelector("#" + this.TABPANEL_ID_PREFIX + id + ", #" + id);
+  },
+
+  /**
+   * Return the tab based on the provided id, if one was registered with this id.
+   * @param {String} id
+   * @return {DOMNode}
+   */
+  getTab: function(id) {
+    return this._tabs.get(id);
   },
 
   /**
    * Event handler.
    */
-  handleEvent: function ToolSidebar_eventHandler(event) {
-    if (event.type == "select") {
-      if (this._currentTool == this.getCurrentTabID()) {
-        // Tool hasn't changed.
-        return;
+  handleEvent: function(event) {
+    if (event.type !== "select" || this._destroyed) {
+      return;
+    }
+
+    if (this._currentTool == this.getCurrentTabID()) {
+      // Tool hasn't changed.
+      return;
+    }
+
+    let previousTool = this._currentTool;
+    this._currentTool = this.getCurrentTabID();
+    if (previousTool) {
+      if (this._telemetry) {
+        this._telemetry.toolClosed(previousTool);
       }
+      this.emit(previousTool + "-unselected");
+    }
 
-      let previousTool = this._currentTool;
-      this._currentTool = this.getCurrentTabID();
-      if (previousTool) {
-        this._telemetry.toolClosed(previousTool);
-        this.emit(previousTool + "-unselected");
+    if (this._telemetry) {
+      this._telemetry.toolOpened(this._currentTool);
+    }
+
+    this.emit(this._currentTool + "-selected");
+    this.emit("select", this._currentTool);
+
+    // Handlers for "select"/"...-selected"/"...-unselected" events might have
+    // destroyed the sidebar in the meantime.
+    if (this._destroyed) {
+      return;
+    }
+
+    // Handle menuitem selection if the allTabsMenu is there by unchecking all
+    // items except the selected one.
+    let tab = this._tabbox.selectedTab;
+    if (tab.allTabsMenuItem) {
+      for (let otherItem of this._allTabsBtn.querySelectorAll("menuitem")) {
+        otherItem.removeAttribute("checked");
       }
-
-      this._telemetry.toolOpened(this._currentTool);
-      this.emit(this._currentTool + "-selected");
-      this.emit("select", this._currentTool);
+      tab.allTabsMenuItem.setAttribute("checked", true);
     }
   },
 
   /**
    * Toggle sidebar's visibility state.
    */
-  toggle: function ToolSidebar_toggle() {
+  toggle: function() {
     if (this._tabbox.hasAttribute("hidden")) {
       this.show();
     } else {
       this.hide();
     }
   },
 
   /**
    * Show the sidebar.
    */
-  show: function ToolSidebar_show() {
+  show: function() {
     if (this._width) {
       this._tabbox.width = this._width;
     }
     this._tabbox.removeAttribute("hidden");
 
     this.emit("show");
   },
 
   /**
    * Show the sidebar.
    */
-  hide: function ToolSidebar_hide() {
+  hide: function() {
     Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, this._tabbox.width);
     this._tabbox.setAttribute("hidden", "true");
 
     this.emit("hide");
   },
 
   /**
    * Return the window containing the tab content.
    */
-  getWindowForTab: function ToolSidebar_getWindowForTab(id) {
+  getWindowForTab: function(id) {
     if (!this._tabs.has(id)) {
       return null;
     }
 
-    let panel = this._panelDoc.getElementById(this._tabs.get(id).linkedPanel);
+    // Get the tabpanel and make sure it contains an iframe
+    let panel = this.getTabPanel(id);
+    if (!panel || !panel.firstChild || !panel.firstChild.contentWindow) {
+      return;
+    }
     return panel.firstChild.contentWindow;
   },
 
   /**
    * Clean-up.
    */
   destroy: Task.async(function*() {
     if (this._destroyed) {
-      return promise.resolve(null);
+      return;
     }
     this._destroyed = true;
 
     Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, this._tabbox.width);
 
+    if (this._allTabsBtn) {
+      this.removeAllTabsMenu();
+    }
+
     this._tabbox.tabpanels.removeEventListener("select", this, true);
 
     // 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.
     while (this._tabbox.tabpanels && this._tabbox.tabpanels.hasChildNodes()) {
       let panel = this._tabbox.tabpanels.firstChild;
       let win = panel.firstChild.contentWindow;
-      if ("destroy" in win) {
+      if (win && ("destroy" in win)) {
         yield win.destroy();
       }
       panel.remove();
     }
 
     while (this._tabbox.tabs && this._tabbox.tabs.hasChildNodes()) {
       this._tabbox.tabs.removeChild(this._tabbox.tabs.firstChild);
     }
 
-    if (this._currentTool) {
+    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;
+  })
+}
 
-    return promise.resolve(null);
-  }),
-}
+XPCOMUtils.defineLazyGetter(this, "l10n", function() {
+  let bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
+  let l10n = function(aName, ...aArgs) {
+    try {
+      if (aArgs.length == 0) {
+        return bundle.GetStringFromName(aName);
+      } else {
+        return bundle.formatStringFromName(aName, aArgs, aArgs.length);
+      }
+    } catch (ex) {
+      Services.console.logStringMessage("Error reading '" + aName + "'");
+    }
+  };
+  return l10n;
+});
--- a/browser/devtools/framework/test/browser.ini
+++ b/browser/devtools/framework/test/browser.ini
@@ -1,14 +1,15 @@
 [DEFAULT]
 subsuite = devtools
 support-files =
   browser_toolbox_options_disable_js.html
   browser_toolbox_options_disable_js_iframe.html
   browser_toolbox_options_disable_cache.sjs
+  browser_toolbox_sidebar_tool.xul
   head.js
   helper_disable_cache.js
   doc_theme.css
 
 [browser_devtools_api.js]
 skip-if = e10s # Bug 1090340
 [browser_devtools_api_destroy.js]
 skip-if = e10s # Bug 1070837 - devtools/framework/toolbox.js |doc| getter not e10s friendly
@@ -34,16 +35,18 @@ skip-if = e10s # Bug 1030318
 skip-if = e10s # Bug 1070837 - devtools/framework/toolbox.js |doc| getter not e10s friendly
 # [browser_toolbox_raise.js] # Bug 962258
 # skip-if = os == "win"
 [browser_toolbox_ready.js]
 [browser_toolbox_select_event.js]
 skip-if = e10s # Bug 1069044 - destroyInspector may hang during shutdown
 [browser_toolbox_sidebar.js]
 [browser_toolbox_sidebar_events.js]
+[browser_toolbox_sidebar_existing_tabs.js]
+[browser_toolbox_sidebar_overflow_menu.js]
 [browser_toolbox_tabsswitch_shortcuts.js]
 [browser_toolbox_tool_ready.js]
 [browser_toolbox_tool_remote_reopen.js]
 [browser_toolbox_window_reload_target.js]
 [browser_toolbox_window_shortcuts.js]
 skip-if = os == "mac" && os_version == "10.8" || os == "win" && os_version == "5.1" # Bug 851129 - Re-enable browser_toolbox_window_shortcuts.js test after leaks are fixed
 [browser_toolbox_window_title_changes.js]
 [browser_toolbox_zoom.js]
--- a/browser/devtools/framework/test/browser_toolbox_options_disable_js.js
+++ b/browser/devtools/framework/test/browser_toolbox_options_disable_js.js
@@ -1,15 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that disabling JavaScript for a tab works as it should.
 
-const TEST_URI = "http://example.com/browser/browser/devtools/framework/" +
-                 "test/browser_toolbox_options_disable_js.html";
+const TEST_URI = URL_ROOT + "browser_toolbox_options_disable_js.html";
 
 let doc;
 let toolbox;
 
 function test() {
   gBrowser.selectedTab = gBrowser.addTab();
   let target = TargetFactory.forTab(gBrowser.selectedTab);
 
--- a/browser/devtools/framework/test/browser_toolbox_sidebar.js
+++ b/browser/devtools/framework/test/browser_toolbox_sidebar.js
@@ -106,17 +106,17 @@ function test() {
     let panels = panel.sidebar._tabbox.querySelectorAll("tabpanel");
     let label = 1;
     for (let tab of tabs) {
       is(tab.getAttribute("label"), label++, "Tab has the right title");
     }
 
     is(label, 4, "Found the right amount of tabs.");
     is(panel.sidebar._tabbox.selectedPanel, panels[0], "First tab is selected");
-    ok(panel.sidebar.getCurrentTabID(), "tab1", "getCurrentTabID() is correct");
+    is(panel.sidebar.getCurrentTabID(), "tab1", "getCurrentTabID() is correct");
 
     panel.sidebar.once("tab1-unselected", function() {
       ok(true, "received 'unselected' event");
       panel.sidebar.once("tab2-selected", function() {
         ok(true, "received 'selected' event");
         panel.sidebar.hide();
         is(panel.sidebar._tabbox.getAttribute("hidden"), "true", "Sidebar hidden");
         is(panel.sidebar.getWindowForTab("tab1").location.href, tab1URL, "Window is accessible");
@@ -149,16 +149,17 @@ function test() {
   function testWidth(panel) {
     let tabbox = panel.panelDoc.getElementById("sidebar");
     tabbox.width = 420;
     panel.sidebar.destroy().then(function() {
       tabbox.width = 0;
       panel.sidebar = new ToolSidebar(tabbox, panel, "testbug865688", true);
       panel.sidebar.show();
       is(panel.panelDoc.getElementById("sidebar").width, 420, "Width restored")
+
       finishUp(panel);
     });
   }
 
   function finishUp(panel) {
     panel.sidebar.destroy();
     gDevTools.unregisterTool(toolDefinition.id);
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_sidebar_existing_tabs.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the sidebar widget auto-registers existing tabs.
+
+const Cu = Components.utils;
+const {ToolSidebar} = devtools.require("devtools/framework/sidebar");
+
+const testToolURL = "data:text/xml;charset=utf8,<?xml version='1.0'?>" +
+                "<?xml-stylesheet href='chrome://browser/skin/devtools/common.css' type='text/css'?>" +
+                "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'>" +
+                "<hbox flex='1'><description flex='1'>test tool</description>" +
+                "<splitter class='devtools-side-splitter'/>" +
+                "<tabbox flex='1' id='sidebar' class='devtools-sidebar-tabs'>" +
+                "<tabs><tab id='tab1' label='tab 1'></tab><tab id='tab2' label='tab 2'></tab></tabs>" +
+                "<tabpanels flex='1'><tabpanel id='tabpanel1'>tab 1</tabpanel><tabpanel id='tabpanel2'>tab 2</tabpanel></tabpanels>" +
+                "</tabbox></hbox></window>";
+
+const testToolDefinition = {
+  id: "testTool",
+  url: testToolURL,
+  label: "Test Tool",
+  isTargetSupported: () => true,
+  build: (iframeWindow, toolbox) => {
+    return promise.resolve({
+      target: toolbox.target,
+      toolbox: toolbox,
+      isReady: true,
+      destroy: () => {},
+      panelDoc: iframeWindow.document,
+    });
+  }
+};
+
+add_task(function*() {
+  let tab = yield addTab("about:blank");
+
+  let target = TargetFactory.forTab(tab);
+
+  gDevTools.registerTool(testToolDefinition);
+  let toolbox = yield gDevTools.showToolbox(target, testToolDefinition.id);
+
+  let toolPanel = toolbox.getPanel(testToolDefinition.id);
+  let tabbox = toolPanel.panelDoc.getElementById("sidebar");
+
+  info("Creating the sidebar widget");
+  let sidebar = new ToolSidebar(tabbox, toolPanel, "bug1101569");
+  
+  info("Checking that existing tabs have been registered");
+  ok(sidebar.getTab("tab1"), "Existing tab 1 was found");
+  ok(sidebar.getTab("tab2"), "Existing tab 2 was found");
+  ok(sidebar.getTabPanel("tabpanel1"), "Existing tabpanel 1 was found");
+  ok(sidebar.getTabPanel("tabpanel2"), "Existing tabpanel 2 was found");
+
+  info("Checking that the sidebar API works with existing tabs");
+
+  sidebar.select("tab2");
+  is(tabbox.selectedTab, tabbox.querySelector("#tab2"),
+    "Existing tabs can be selected");
+
+  sidebar.select("tab1");
+  is(tabbox.selectedTab, tabbox.querySelector("#tab1"),
+    "Existing tabs can be selected");
+
+  is(sidebar.getCurrentTabID(), "tab1", "getCurrentTabID returns the expected id");
+
+  info("Removing a tab");
+  sidebar.removeTab("tab2", "tabpanel2");
+  ok(!sidebar.getTab("tab2"), "Tab 2 was removed correctly");
+  ok(!sidebar.getTabPanel("tabpanel2"), "Tabpanel 2 was removed correctly");
+
+  sidebar.destroy();
+  gDevTools.unregisterTool(testToolDefinition.id);
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_sidebar_overflow_menu.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the sidebar widget correctly displays the "all tabs..." button
+// when the tabs overflow.
+
+const {ToolSidebar} = devtools.require("devtools/framework/sidebar");
+
+const testToolDefinition = {
+  id: "testTool",
+  url: CHROME_URL_ROOT + "browser_toolbox_sidebar_tool.xul",
+  label: "Test Tool",
+  isTargetSupported: () => true,
+  build: (iframeWindow, toolbox) => {
+    return {
+      target: toolbox.target,
+      toolbox: toolbox,
+      isReady: true,
+      destroy: () => {},
+      panelDoc: iframeWindow.document,
+    };
+  }
+};
+
+add_task(function*() {
+  let tab = yield addTab("about:blank");
+  let target = TargetFactory.forTab(tab);
+
+  gDevTools.registerTool(testToolDefinition);
+  let toolbox = yield gDevTools.showToolbox(target, testToolDefinition.id);
+
+  let toolPanel = toolbox.getPanel(testToolDefinition.id);
+  let tabbox = toolPanel.panelDoc.getElementById("sidebar");
+
+  info("Creating the sidebar widget");
+  let sidebar = new ToolSidebar(tabbox, toolPanel, "bug1101569", {
+    showAllTabsMenu: true
+  });
+
+  let allTabsMenu = toolPanel.panelDoc.querySelector(".devtools-sidebar-alltabs");
+  ok(allTabsMenu, "The all-tabs menu is available");
+  is(allTabsMenu.getAttribute("hidden"), "true", "The menu is hidden for now");
+
+  info("Adding 10 tabs to the sidebar widget");
+  for (let nb = 0; nb < 10; nb ++) {
+    let url = `data:text/html;charset=utf8,<title>tab ${nb}</title><p>Test tab ${nb}</p>`;
+    sidebar.addTab("tab" + nb, url, nb === 0);
+  }
+
+  info("Fake an overflow event so that the all-tabs menu is visible");
+  sidebar._onTabBoxOverflow();
+  ok(!allTabsMenu.hasAttribute("hidden"), "The all-tabs menu is now shown");
+
+  info("Select each tab, one by one");
+  for (let nb = 0; nb < 10; nb ++) {
+    let id = "tab" + nb;
+
+    info("Found tab item nb " + nb);
+    let item = allTabsMenu.querySelector("#sidebar-alltabs-item-" + id);
+
+    info("Click on the tab");
+    EventUtils.sendMouseEvent({type: "click"}, item, toolPanel.panelDoc.defaultView);
+
+    is(tabbox.selectedTab.id, "sidebar-tab-" + id,
+      "The selected tab is now nb " + nb);
+  }
+
+  info("Fake an underflow event so that the all-tabs menu gets hidden");
+  sidebar._onTabBoxUnderflow();
+  is(allTabsMenu.getAttribute("hidden"), "true", "The all-tabs menu is hidden");
+
+  yield sidebar.destroy();
+  gDevTools.unregisterTool(testToolDefinition.id);
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_sidebar_tool.xul
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript;version=1.8" src="chrome://browser/content/devtools/theme-switching.js"/>
+  <box flex="1" class="devtools-responsive-container theme-body">
+    <vbox flex="1" class="devtools-main-content" id="content">test</vbox>
+    <splitter class="devtools-side-splitter"/>
+    <tabbox flex="1" id="sidebar" class="devtools-sidebar-tabs">
+      <tabs/>
+      <tabpanels flex="1"/>
+    </tabbox>
+  </box>
+</window>
--- a/browser/devtools/framework/test/head.js
+++ b/browser/devtools/framework/test/head.js
@@ -3,16 +3,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 let TargetFactory = gDevTools.TargetFactory;
 
 const { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
 const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 
+const URL_ROOT = "http://example.com/browser/browser/devtools/framework/test/";
+const CHROME_URL_ROOT = "chrome://mochitests/content/browser/browser/devtools/framework/test/";
+
 let TargetFactory = devtools.TargetFactory;
 
 // All test are asynchronous
 waitForExplicitFinish();
 
 // Uncomment this pref to dump all devtools emitted events to the console.
 // Services.prefs.setBoolPref("devtools.dump.emit", true);
 
--- a/browser/devtools/framework/test/helper_disable_cache.js
+++ b/browser/devtools/framework/test/helper_disable_cache.js
@@ -1,16 +1,15 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Common code shared by browser_toolbox_options_disable_cache-*.js
-const TEST_URI = "http://mochi.test:8888/browser/browser/devtools/framework/" +
-                 "test/browser_toolbox_options_disable_cache.sjs";
+const TEST_URI = URL_ROOT + "browser_toolbox_options_disable_cache.sjs";
 let tabs = [
 {
   title: "Tab 0",
   desc: "Toggles cache on.",
   startToolbox: true
 },
 {
   title: "Tab 1",
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -342,18 +342,16 @@ InspectorPanel.prototype = {
                         "layoutview" == defaultTab);
 
     if (this.target.form.animationsActor) {
       this.sidebar.addTab("animationinspector",
                           "chrome://browser/content/devtools/animationinspector/animation-inspector.xhtml",
                           "animationinspector" == defaultTab);
     }
 
-    let ruleViewTab = this.sidebar.getTab("ruleview");
-
     this.sidebar.show();
   },
 
   /**
    * Reset the inspector on new root mutation.
    */
   onNewRoot: function InspectorPanel_onNewRoot() {
     this._defaultNode = null;
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -3550,18 +3550,18 @@ JSTerm.prototype = {
   {
     let deferred = promise.defer();
 
     let onTabReady = () => {
       let window = this.sidebar.getWindowForTab("variablesview");
       deferred.resolve(window);
     };
 
-    let tab = this.sidebar.getTab("variablesview");
-    if (tab) {
+    let tabPanel = this.sidebar.getTabPanel("variablesview");
+    if (tabPanel) {
       if (this.sidebar.getCurrentTabID() == "variablesview") {
         onTabReady();
       }
       else {
         this.sidebar.once("variablesview-selected", onTabReady);
         this.sidebar.select("variablesview");
       }
     }
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.properties
@@ -66,16 +66,26 @@ scratchpad.keycode=VK_F4
 # Used for toggling the browser console from the detached toolbox window
 # Needs to match browserConsoleCmd.commandkey from browser.dtd
 browserConsoleCmd.commandkey=j
 
 # LOCALIZATION NOTE (pickButton.tooltip)
 # This is the tooltip of the pick button in the toolbox toolbar
 pickButton.tooltip=Pick an element from the page
 
+# LOCALIZATION NOTE (sidebar.showAllTabs.label)
+# This is the label shown in the show all tabs button in the tabbed side
+# bar, when there's no enough space to show all tabs at once
+sidebar.showAllTabs.label=…
+
+# LOCALIZATION NOTE (sidebar.showAllTabs.tooltip)
+# This is the tooltip shown when hover over the '…' button in the tabbed side
+# bar, when there's no enough space to show all tabs at once
+sidebar.showAllTabs.tooltip=All tabs
+
 # LOCALIZATION NOTE (options.darkTheme.label)
 # Used as a label for dark theme
 options.darkTheme.label=Dark theme
 
 # LOCALIZATION NOTE (options.lightTheme.label)
 # Used as a label for light theme
 options.lightTheme.label=Light theme
 
--- a/browser/themes/shared/devtools/dark-theme.css
+++ b/browser/themes/shared/devtools/dark-theme.css
@@ -186,17 +186,18 @@
 }
 
 .variable-or-property .token-domnode {
   font-weight: bold;
 }
 
 .theme-toolbar,
 .devtools-toolbar,
-.devtools-sidebar-tabs > tabs,
+.devtools-sidebar-tabs tabs,
+.devtools-sidebar-alltabs,
 .CodeMirror-dialog { /* General toolbar styling */
   color: var(--theme-body-color-alt);
   background-color: var(--theme-toolbar-background);
   border-color: hsla(210,8%,5%,.6);
 }
 
 .theme-fg-contrast { /* To be used for text on theme-bg-contrast */
   color: black;
--- a/browser/themes/shared/devtools/light-theme.css
+++ b/browser/themes/shared/devtools/light-theme.css
@@ -189,17 +189,18 @@
 }
 
 .theme-fg-contrast { /* To be used for text on theme-bg-contrast */
   color: black;
 }
 
 .theme-toolbar,
 .devtools-toolbar,
-.devtools-sidebar-tabs > tabs,
+.devtools-sidebar-tabs tabs,
+.devtools-sidebar-alltabs,
 .CodeMirror-dialog { /* General toolbar styling */
   color: var(--theme-body-color-alt);
   background-color: var(--theme-toolbar-background);
   border-color: var(--theme-splitter-color);
 }
 
 .ruleview-colorswatch,
 .computedview-colorswatch,
--- a/browser/themes/shared/devtools/toolbars.inc.css
+++ b/browser/themes/shared/devtools/toolbars.inc.css
@@ -6,17 +6,18 @@
 %filter substitution
 %define smallSeparatorDark linear-gradient(transparent 15%, #5a6169 15%, #5a6169 85%, transparent 85%)
 %define smallSeparatorLight linear-gradient(transparent 15%, #aaa 15%, #aaa 85%, transparent 85%)
 %define solidSeparatorDark linear-gradient(#2d5b7d, #2d5b7d)
 %define solidSeparatorLight linear-gradient(#aaa, #aaa)
 
 /* Toolbars */
 .devtools-toolbar,
-.devtools-sidebar-tabs > tabs {
+.devtools-sidebar-tabs tabs,
+.devtools-sidebar-alltabs {
   -moz-appearance: none;
   padding: 0;
   border-width: 0;
   border-bottom-width: 1px;
   border-style: solid;
   height: 24px;
   line-height: 24px;
   box-sizing: border-box;
@@ -411,29 +412,40 @@
   border: 0;
 }
 
 .theme-light .devtools-sidebar-tabs > tabpanels {
   background: var(--theme-sidebar-background);
   color: var(--theme-body-color);
 }
 
-.devtools-sidebar-tabs > tabs {
+.devtools-sidebar-tabs tabs {
   position: static;
   font: inherit;
   margin-bottom: 0;
   overflow: hidden;
 }
 
-.devtools-sidebar-tabs > tabs > .tabs-right,
-.devtools-sidebar-tabs > tabs > .tabs-left {
+.devtools-sidebar-alltabs {
+  margin: 0;
+  border-width: 0 0 1px 0;
+  -moz-border-start-width: 1px;
+  border-style: solid;
+}
+
+.devtools-sidebar-alltabs dropmarker {
   display: none;
 }
 
-.devtools-sidebar-tabs > tabs > tab {
+.devtools-sidebar-tabs tabs > .tabs-right,
+.devtools-sidebar-tabs tabs > .tabs-left {
+  display: none;
+}
+
+.devtools-sidebar-tabs tabs > tab {
   -moz-appearance: none;
   /* We want to match the height of a toolbar with a toolbarbutton
    * First, we need to replicated the padding of toolbar (4px),
    * then we need to take the border of the buttons into account (1px).
    */
   padding: 0 3px;
   margin: 0;
   min-width: 78px;
@@ -444,80 +456,80 @@
   border-width: 0;
   -moz-border-start-width: 1px;
   border-style: solid;
   border-radius: 0;
   position: static;
   text-shadow: none;
 }
 
-.devtools-sidebar-tabs > tabs > tab:first-child {
+.devtools-sidebar-tabs tabs > tab:first-child {
   -moz-border-start-width: 0;
 }
 
-.theme-dark .devtools-sidebar-tabs > tabs > tab {
+.theme-dark .devtools-sidebar-tabs tabs > tab {
   border-image: @smallSeparatorDark@ 1 1;
 }
 
-.theme-dark .devtools-sidebar-tabs > tabs > tab:hover {
+.theme-dark .devtools-sidebar-tabs tabs > tab:hover {
   background: hsla(206,37%,4%,.2);
   border-image: @smallSeparatorDark@ 1 1;
 }
 
-.theme-dark .devtools-sidebar-tabs > tabs > tab:hover:active {
+.theme-dark .devtools-sidebar-tabs tabs > tab:hover:active {
   background: hsla(206,37%,4%,.4);
   border-image: @smallSeparatorDark@ 1 1;
 }
 
-.theme-dark .devtools-sidebar-tabs > tabs > tab[selected] + tab {
+.theme-dark .devtools-sidebar-tabs tabs > tab[selected] + tab {
   border-image: @solidSeparatorDark@ 1 1;
 }
 
-.theme-dark .devtools-sidebar-tabs > tabs > tab[selected] + tab:hover {
+.theme-dark .devtools-sidebar-tabs tabs > tab[selected] + tab:hover {
   background: hsla(206,37%,4%,.2);
   border-image: @solidSeparatorDark@ 1 1;
 }
 
-.theme-dark .devtools-sidebar-tabs > tabs > tab[selected] + tab:hover:active {
+.theme-dark .devtools-sidebar-tabs tabs > tab[selected] + tab:hover:active {
   background: hsla(206,37%,4%,.4);
   border-image: @solidSeparatorDark@ 1 1;
 }
 
-.theme-dark .devtools-sidebar-tabs > tabs > tab[selected],
-.theme-dark .devtools-sidebar-tabs > tabs > tab[selected]:hover:active {
+.theme-dark .devtools-sidebar-tabs tabs > tab[selected],
+.theme-dark .devtools-sidebar-tabs tabs > tab[selected]:hover:active {
   color: var(--theme-selection-color);
   background: #1d4f73;
   border-image: @solidSeparatorDark@ 1 1;
 }
 
-.theme-light .devtools-sidebar-tabs > tabs > tab {
+.theme-light .devtools-sidebar-tabs tabs > tab {
   border-image: @smallSeparatorLight@ 1 1;
 }
 
-.theme-light .devtools-sidebar-tabs > tabs > tab:hover {
+.theme-light .devtools-sidebar-tabs tabs > tab:hover {
   background: #ddd;
   border-image: @smallSeparatorLight@ 1 1;
 }
 
-.theme-light .devtools-sidebar-tabs > tabs > tab:hover:active {
+.theme-light .devtools-sidebar-tabs tabs > tab:hover:active {
   background: #ddd;
   border-image: @smallSeparatorLight@ 1 1;
 }
 
-.theme-light .devtools-sidebar-tabs > tabs > tab[selected] + tab {
+.theme-light .devtools-sidebar-tabs tabs > tab[selected] + tab {
   border-image: @solidSeparatorLight@;
 }
 
-.theme-light .devtools-sidebar-tabs > tabs > tab[selected] + tab:hover {
+.theme-light .devtools-sidebar-tabs tabs > tab[selected] + tab:hover {
   background: #ddd;
   border-image: @solidSeparatorLight@;
 }
 
-.theme-light .devtools-sidebar-tabs > tabs > tab[selected],
-.theme-light .devtools-sidebar-tabs > tabs > tab[selected]:hover:active {
+.theme-light .devtools-sidebar-tabs tabs > tab[selected],
+.theme-light .devtools-sidebar-tabs tabs > tab[selected]:hover:active {
   color: var(--theme-selection-color);
   background: #4c9ed9;
   border-image: @solidSeparatorLight@;
 }
 
 /* Toolbox - moved from toolbox.css.
  * Rules that apply to the global toolbox like command buttons,
  * devtools tabs, docking buttons, etc. */