Bug 1433611 - Adjust the widths of the split box on toggling on the 3 pane inspector. r=pbro
authorGabriel Luong <gabriel.luong@gmail.com>
Thu, 05 Apr 2018 14:21:13 -0400
changeset 412028 8b549261f40d26868f8f26d7960fe3a79cbc5aea
parent 412027 620a4eba9389d4cfebb8b45c913d7a13921dd49a
child 412029 8cbc541afed7c033595841b0e61811507c66d4c4
push id33781
push userapavel@mozilla.com
push dateFri, 06 Apr 2018 14:39:11 +0000
treeherdermozilla-central@6792eee0c9c5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1433611
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1433611 - Adjust the widths of the split box on toggling on the 3 pane inspector. r=pbro When the inspector is in landscape mode, it will first try to toggle on the 3 pane mode by creating a middle/bottom-left panel with the same sidebar width, but if doubling the original sidebar width will be bigger than half of the toolbox's width, we will instead toggle on the 3 pane mode with all panels being of equal widths. When the inspector is in portrait mode, it will just toggle on its T shape pane such that the 2 bottom panes are equal widths. We also increased the breakpoint width of the toolbox's side view to 1000px so that it will keep its T shape in portrait mode until the toolbox's width is bigger than 1000px.
devtools/client/inspector/grids/grid-inspector.js
devtools/client/inspector/inspector.js
devtools/client/inspector/test/browser.ini
devtools/client/inspector/test/browser_inspector_pane-toggle-01.js
devtools/client/inspector/test/browser_inspector_pane-toggle-02.js
devtools/client/inspector/test/browser_inspector_pane-toggle-03.js
devtools/client/inspector/test/browser_inspector_pane-toggle-04.js
devtools/client/preferences/devtools.js
devtools/client/shared/components/splitter/SplitBox.js
devtools/client/themes/inspector.css
--- a/devtools/client/inspector/grids/grid-inspector.js
+++ b/devtools/client/inspector/grids/grid-inspector.js
@@ -78,17 +78,23 @@ class GridInspector {
    * Initializes the grid inspector by fetching the LayoutFront from the walker, loading
    * the highlighter settings and initalizing the SwatchColorPicker instance.
    */
   async init() {
     if (!this.inspector) {
       return;
     }
 
-    this.layoutInspector = await this.inspector.walker.getLayoutInspector();
+    try {
+      this.layoutInspector = await this.inspector.walker.getLayoutInspector();
+    } catch (e) {
+      // This call might fail if called asynchrously after the toolbox is finished
+      // closing.
+      return;
+    }
 
     this.loadHighlighterSettings();
 
     // Create a shared SwatchColorPicker instance to be reused by all GridItem components.
     this.swatchColorPickerTooltip = new SwatchColorPickerTooltip(
       this.inspector.toolbox.doc,
       this.inspector,
       {
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -7,16 +7,17 @@
 /* global window, BrowserLoader */
 
 "use strict";
 
 const Services = require("Services");
 const promise = require("promise");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {executeSoon} = require("devtools/shared/DevToolsUtils");
+const {Toolbox} = require("devtools/client/framework/toolbox");
 const {PrefObserver} = require("devtools/client/shared/prefs");
 const Telemetry = require("devtools/client/shared/telemetry");
 const HighlightersOverlay = require("devtools/client/inspector/shared/highlighters-overlay");
 const ReflowTracker = require("devtools/client/inspector/shared/reflow-tracker");
 const Store = require("devtools/client/inspector/store");
 const InspectorStyleChangeTracker = require("devtools/client/inspector/shared/style-change-tracker");
 
 // Use privileged promise in panel documents to prevent having them to freeze
@@ -33,30 +34,34 @@ loader.lazyRequireGetter(this, "nodeCons
 loader.lazyRequireGetter(this, "Menu", "devtools/client/framework/menu");
 loader.lazyRequireGetter(this, "MenuItem", "devtools/client/framework/menu-item");
 loader.lazyRequireGetter(this, "ExtensionSidebar", "devtools/client/inspector/extensions/extension-sidebar");
 loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
 loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
 
 const {LocalizationHelper, localizeMarkup} = require("devtools/shared/l10n");
 const INSPECTOR_L10N =
-      new LocalizationHelper("devtools/client/locales/inspector.properties");
+  new LocalizationHelper("devtools/client/locales/inspector.properties");
 loader.lazyGetter(this, "TOOLBOX_L10N", function() {
   return new LocalizationHelper("devtools/client/locales/toolbox.properties");
 });
 
 // Sidebar dimensions
 const INITIAL_SIDEBAR_SIZE = 350;
 
-// If the toolbox width is smaller than given amount of pixels,
-// the sidebar automatically switches from 'landscape' to 'portrait' mode.
-const PORTRAIT_MODE_WIDTH = 700;
+// If the toolbox's width is smaller than the given amount of pixels, the sidebar
+// automatically switches from 'landscape/horizontal' to 'portrait/vertical' mode.
+const PORTRAIT_MODE_WIDTH_THRESHOLD = 700;
+// If the toolbox's width docked to the side is smaller than the given amount of pixels,
+// the sidebar automatically switches from 'landscape/horizontal' to 'portrait/vertical'
+// mode.
+const SIDE_PORTAIT_MODE_WIDTH_THRESHOLD = 1000;
 
-const SHOW_SPLIT_SIDEBAR_TOGGLE_PREF = "devtools.inspector.split-sidebar-toggle";
-const SPLIT_RULE_VIEW_PREF = "devtools.inspector.split-rule-enabled";
+const SHOW_THREE_PANE_TOGGLE_PREF = "devtools.inspector.three-pane-toggle";
+const THREE_PANE_ENABLED_PREF = "devtools.inspector.three-pane-enabled";
 
 /**
  * Represents an open instance of the Inspector for a tab.
  * The inspector controls the breadcrumbs, the markup view, and the sidebar
  * (computed view, rule view, font view and animation inspector).
  *
  * Events:
  * - ready
@@ -108,19 +113,18 @@ function Inspector(toolbox) {
   this.styleChangeTracker = new InspectorStyleChangeTracker(this);
   this.store = Store();
   this.telemetry = new Telemetry();
 
   // Store the URL of the target page prior to navigation in order to ensure
   // telemetry counts in the Grid Inspector are not double counted on reload.
   this.previousURL = this.target.url;
 
-  this.showSplitSidebarToggle = Services.prefs.getBoolPref(
-    SHOW_SPLIT_SIDEBAR_TOGGLE_PREF);
-  this.isSplitRuleViewEnabled = Services.prefs.getBoolPref(SPLIT_RULE_VIEW_PREF);
+  this.show3PaneToggle = Services.prefs.getBoolPref(SHOW_THREE_PANE_TOGGLE_PREF);
+  this.is3PaneModeEnabled = Services.prefs.getBoolPref(THREE_PANE_ENABLED_PREF);
 
   this.nodeMenuTriggerInfo = null;
 
   this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
   this._onContextMenu = this._onContextMenu.bind(this);
   this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
   this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
   this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this);
@@ -450,17 +454,19 @@ Inspector.prototype = {
 
   /**
    * Check if the inspector should use the landscape mode.
    *
    * @return {Boolean} true if the inspector should be in landscape mode.
    */
   useLandscapeMode: function() {
     let { clientWidth } = this.panelDoc.getElementById("inspector-splitter-box");
-    return clientWidth > PORTRAIT_MODE_WIDTH;
+    return this.is3PaneModeEnabled && this.toolbox.hostType == Toolbox.HostType.SIDE ?
+      clientWidth > SIDE_PORTAIT_MODE_WIDTH_THRESHOLD :
+      clientWidth > PORTRAIT_MODE_WIDTH_THRESHOLD;
   },
 
   /**
    * Build Splitter located between the main and side area of
    * the Inspector panel.
    */
   setupSplitter: function() {
     let SplitBox = this.React.createFactory(this.browserRequire(
@@ -477,18 +483,18 @@ Inspector.prototype = {
       endPanelControl: true,
       startPanel: this.InspectorTabPanel({
         id: "inspector-main-content"
       }),
       endPanel: SplitBox({
         initialWidth: splitSidebarWidth,
         minSize: 10,
         maxSize: "80%",
-        splitterSize: this.isSplitRuleViewEnabled ? 1 : 0,
-        endPanelControl: false,
+        splitterSize: this.is3PaneModeEnabled ? 1 : 0,
+        endPanelControl: this.is3PaneModeEnabled,
         startPanel: this.InspectorTabPanel({
           id: "inspector-rules-container"
         }),
         endPanel: this.InspectorTabPanel({
           id: "inspector-sidebar-container"
         }),
         ref: splitbox => {
           this.sidebarSplitBox = splitbox;
@@ -536,17 +542,17 @@ Inspector.prototype = {
       height = Services.prefs.getIntPref("devtools.toolsidebar-height.inspector");
       splitSidebarWidth = Services.prefs.getIntPref(
         "devtools.toolsidebar-width.inspector.splitsidebar");
     } catch (e) {
       // Set width and height of the splitter. Only one
       // value is really useful at a time depending on the current
       // orientation (vertical/horizontal).
       // Having both is supported by the splitter component.
-      width = this.isSplitRuleViewEnabled ?
+      width = this.is3PaneModeEnabled ?
         INITIAL_SIDEBAR_SIZE * 2 : INITIAL_SIDEBAR_SIZE;
       height = INITIAL_SIDEBAR_SIZE;
       splitSidebarWidth = INITIAL_SIDEBAR_SIZE;
     }
 
     return { width, height, splitSidebarWidth };
   },
 
@@ -576,72 +582,131 @@ Inspector.prototype = {
 
   onSidebarShown: function() {
     let { width, height, splitSidebarWidth } = this.getSidebarSize();
     this.splitBox.setState({ width, height });
     this.sidebarSplitBox.setState({ width: splitSidebarWidth });
   },
 
   async onSidebarToggle() {
-    this.isSplitRuleViewEnabled = !this.isSplitRuleViewEnabled;
-    Services.prefs.setBoolPref(SPLIT_RULE_VIEW_PREF, this.isSplitRuleViewEnabled);
+    this.is3PaneModeEnabled = !this.is3PaneModeEnabled;
+    Services.prefs.setBoolPref(THREE_PANE_ENABLED_PREF, this.is3PaneModeEnabled);
 
     await this.setupToolbar();
     await this.addRuleView();
   },
 
   /**
-   * Adds the rule view to the main or split sidebar depending on whether or not it is
-   * split view mode. The default tab specifies whether or not the rule view should be
-   * selected. The defaultTab defaults to the rule view when the rule view is being merged
-   * back into the sidebar from the split sidebar. Otherwise, we specify the default tab
-   * when handling the sidebar setup.
+   * Adds the rule view to the middle (in landscape/horizontal mode) or bottom-left panel
+   * (in portrait/vertical mode) or inspector sidebar depending on whether or not it is 3
+   * pane mode. The default tab specifies whether or not the rule view should be selected.
+   * The defaultTab defaults to the rule view when reverting to the 2 pane mode and the
+   * rule view is being merged back into the inspector sidebar from middle/bottom-left
+   * panel. Otherwise, we specify the default tab when handling the sidebar setup.
    *
    * @params {String} defaultTab
    *         Thie id of the default tab for the sidebar.
    */
   async addRuleView(defaultTab = "ruleview") {
-    let ruleViewSidebar = this.sidebarSplitBox.startPanelContainer;
+    const ruleViewSidebar = this.sidebarSplitBox.startPanelContainer;
+    const toolboxWidth =
+      this.panelDoc.getElementById("inspector-splitter-box").clientWidth;
 
-    if (this.isSplitRuleViewEnabled) {
-      // Removes the rule view from the main sidebar and adds the rule view to the split
-      // sidebar.
+    if (this.is3PaneModeEnabled) {
+      // Convert to 3 pane mode by removing the rule view from the inspector sidebar
+      // and adding the rule view to the middle (in landscape/horizontal mode) or
+      // bottom-left (in portrait/vertical mode) panel.
+
       ruleViewSidebar.style.display = "block";
 
-      // Show the splitter inside the sidebar split box.
-      this.sidebarSplitBox.setState({ splitterSize: 1 });
+      // Get the inspector sidebar's (right panel in horizontal mode or bottom panel in
+      // vertical mode) width.
+      const sidebarWidth = this.splitBox.state.width;
+      // This variable represents the width of the right panel in horizontal mode or
+      // bottom-right panel in vertical mode width in 3 pane mode.
+      let sidebarSplitboxWidth;
+
+      if (this.useLandscapeMode()) {
+        // Whether or not doubling the inspector sidebar's (right panel in horizontal mode
+        // or bottom panel in vertical mode) width will be bigger than half of the
+        // toolbox's width.
+        const canDoubleSidebarWidth = (sidebarWidth * 2) < (toolboxWidth / 2);
+
+        // Resize the main split box's end panel that contains the middle and right panel.
+        // Attempts to resize the main split box's end panel to be double the size of the
+        // existing sidebar's width when switching to 3 pane mode. However, if the middle
+        // and right panel's width together is greater than half of the toolbox's width,
+        // split all 3 panels to be equally sized by resizing the end panel to be 2/3 of
+        // the current toolbox's width.
+        this.splitBox.setState({
+          width: canDoubleSidebarWidth ? sidebarWidth * 2 : toolboxWidth * 2 / 3,
+        });
+
+        // In landscape/horizontal mode, set the right panel back to its original
+        // inspector sidebar width if we can double the sidebar width. Otherwise, set
+        // the width of the right panel to be 1/3 of the toolbox's width since all 3
+        // panels will be equally sized.
+        sidebarSplitboxWidth = canDoubleSidebarWidth ? sidebarWidth : toolboxWidth / 3;
+      } else {
+        // In portrait/vertical mode, set the bottom-right panel to be 1/2 of the
+        // toolbox's width.
+        sidebarSplitboxWidth = toolboxWidth / 2;
+      }
+
+      // Show the splitter inside the sidebar split box. Sets the width of the inspector
+      // sidebar and specify that the end (right in horizontal or bottom-right in
+      // vertical) panel of the sidebar split box should be controlled when resizing.
+      this.sidebarSplitBox.setState({
+        endPanelControl: true,
+        splitterSize: 1,
+        width: sidebarSplitboxWidth,
+      });
 
       // Force the rule view panel creation by calling getPanel
       this.getPanel("ruleview");
 
       await this.sidebar.removeTab("ruleview");
 
       this.ruleViewSideBar.addExistingTab(
         "ruleview",
         INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
         true);
 
       this.ruleViewSideBar.show("ruleview");
     } else {
-      // Removes the rule view from the split sidebar and adds the rule view to the main
-      // sidebar.
+      // Removes the rule view from the 3 pane mode and adds the rule view to the main
+      // inspector sidebar.
+
       ruleViewSidebar.style.display = "none";
 
-      // Hide the splitter to prevent any drag events in the sidebar split box.
-      this.sidebarSplitBox.setState({ splitterSize: 0 });
+      // Set the width of the split box (right panel in horziontal mode and bottom panel
+      // in vertical mode) to be the width of the inspector sidebar.
+      this.splitBox.setState({
+        width: this.useLandscapeMode() ? this.sidebarSplitBox.state.width : toolboxWidth,
+      });
+
+      // Hide the splitter to prevent any drag events in the sidebar split box and
+      // specify that the end (right panel in horziontal mode or bottom panel in vertical
+      // mode) panel should be uncontrolled when resizing.
+      this.sidebarSplitBox.setState({
+        endPanelControl: false,
+        splitterSize: 0,
+      });
 
       this.ruleViewSideBar.hide();
       await this.ruleViewSideBar.removeTab("ruleview");
 
       this.sidebar.addExistingTab(
         "ruleview",
         INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
         defaultTab == "ruleview",
         0);
     }
+
+    this.emit("ruleview-added");
   },
 
   /**
    * Lazily get and create panel instances displayed in the sidebar
    */
   getPanel: function(id) {
     if (this._panels.has(id)) {
       return this._panels.get(id);
@@ -673,19 +738,19 @@ Inspector.prototype = {
 
   /**
    * Build the sidebar.
    */
   async setupSidebar() {
     let sidebar = this.panelDoc.getElementById("inspector-sidebar");
     let options = { showAllTabsMenu: true };
 
-    if (this.showSplitSidebarToggle) {
+    if (this.show3PaneToggle) {
       options.sidebarToggleButton = {
-        collapsed: !this.isSplitRuleViewEnabled,
+        collapsed: !this.is3PaneModeEnabled,
         collapsePaneTitle: INSPECTOR_L10N.getStr("inspector.hideSplitRulesView"),
         expandPaneTitle: INSPECTOR_L10N.getStr("inspector.showSplitRulesView"),
         onClick: this.onSidebarToggle,
       };
     }
 
     this.sidebar = new ToolSidebar(sidebar, this, "inspector", options);
 
@@ -693,27 +758,27 @@ Inspector.prototype = {
     this.ruleViewSideBar = new ToolSidebar(ruleSideBar, this, "inspector", {
       hideTabstripe: true
     });
 
     this.sidebar.on("select", this.onSidebarSelect);
 
     let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
 
-    if (this.isSplitRuleViewEnabled && defaultTab === "ruleview") {
+    if (this.is3PaneModeEnabled && defaultTab === "ruleview") {
       defaultTab = "computedview";
     }
 
     // Append all side panels
 
     await this.addRuleView(defaultTab);
 
     // If the 3 Pane Inspector feature is disabled, use the old order:
     // Rules, Computed, Layout, etc.
-    if (!this.showSplitSidebarToggle) {
+    if (!this.show3PaneToggle) {
       this.sidebar.addExistingTab(
         "computedview",
         INSPECTOR_L10N.getStr("inspector.sidebar.computedViewTitle"),
         defaultTab == "computedview");
     }
 
     // Inject a lazy loaded react tab by exposing a fake React object
     // with a lazy defined Tab thanks to `panel` being a function
@@ -736,17 +801,17 @@ Inspector.prototype = {
 
           return this.layoutview.provider;
         }
       },
       defaultTab == layoutId);
 
     // If the 3 Pane Inspector feature is enabled, use the new order:
     // Rules, Layout, Computed, etc.
-    if (this.showSplitSidebarToggle) {
+    if (this.show3PaneToggle) {
       this.sidebar.addExistingTab(
         "computedview",
         INSPECTOR_L10N.getStr("inspector.sidebar.computedViewTitle"),
         defaultTab == "computedview");
     }
 
     if (Services.prefs.getBoolPref("devtools.changesview.enabled")) {
       // Inject a lazy loaded react tab by exposing a fake React object
@@ -1300,24 +1365,25 @@ Inspector.prototype = {
     this.prefsObserver.destroy();
     this.reflowTracker.destroy();
     this.styleChangeTracker.destroy();
     this.search.destroy();
 
     this._toolbox = null;
     this.breadcrumbs = null;
     this.highlighters = null;
-    this.isSplitRuleViewEnabled = null;
+    this.is3PaneModeEnabled = null;
     this.panelDoc = null;
     this.panelWin.inspector = null;
     this.panelWin = null;
     this.prefsObserver = null;
     this.resultsLength = null;
     this.search = null;
     this.searchBox = null;
+    this.show3PaneToggle = null;
     this.sidebar = null;
     this.store = null;
     this.target = null;
 
     this._panelDestroyer = promise.all([
       cssPropertiesDestroyer,
       markupDestroyer,
       sidebarDestroyer,
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -156,16 +156,20 @@ skip-if = (os == 'linux' && bits == 32 &
 subsuite = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 [browser_inspector_menu-04-use-in-console.js]
 [browser_inspector_menu-05-attribute-items.js]
 [browser_inspector_menu-06-other.js]
 [browser_inspector_navigation.js]
 [browser_inspector_navigate_to_errors.js]
 [browser_inspector_open_on_neterror.js]
+[browser_inspector_pane-toggle-01.js]
+[browser_inspector_pane-toggle-02.js]
+[browser_inspector_pane-toggle-03.js]
+[browser_inspector_pane-toggle-04.js]
 [browser_inspector_picker-stop-on-destroy.js]
 [browser_inspector_picker-stop-on-tool-change.js]
 [browser_inspector_portrait_mode.js]
 [browser_inspector_pseudoclass-lock.js]
 [browser_inspector_pseudoclass-menu.js]
 [browser_inspector_reload-01.js]
 [browser_inspector_reload-02.js]
 [browser_inspector_remove-iframe-during-load.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-01.js
@@ -0,0 +1,28 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the inspector panel has a 3 pane toggle button, and that
+// this button is visible both in BOTTOM and SIDE hosts.
+
+add_task(async function() {
+  await pushPref("devtools.inspector.three-pane-toggle", true);
+
+  info("Open the inspector in a bottom toolbox host");
+  const { inspector, toolbox } = await openInspectorForURL("about:blank", "bottom");
+
+  const button = inspector.panelDoc.querySelector(".sidebar-toggle");
+  ok(button, "The toggle button exists in the DOM");
+  ok(button.getAttribute("title"), "The title tooltip has initial state");
+  ok(button.classList.contains("pane-collapsed"), "The button is in collapsed state");
+  ok(!!button.getClientRects().length, "The button is visible");
+
+  info("Switch the host to side type");
+  await toolbox.switchHost("side");
+
+  ok(!!button.getClientRects().length, "The button is still visible");
+  ok(button.classList.contains("pane-collapsed"),
+    "The button is still in collapsed state");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js
@@ -0,0 +1,49 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the 3 pane toggle button can toggle on and off the inspector's 3 pane mode,
+// and the 3 panes rendered are all of equal widths in the BOTTOM host.
+
+add_task(async function() {
+  await pushPref("devtools.inspector.three-pane-toggle", true);
+
+  const { inspector } = await openInspectorForURL("about:blank");
+  const { panelDoc: doc } = inspector;
+  const button = doc.querySelector(".sidebar-toggle");
+  const ruleViewSidebar = inspector.sidebarSplitBox.startPanelContainer;
+  const toolboxWidth = doc.getElementById("inspector-splitter-box").clientWidth;
+
+  ok(button.classList.contains("pane-collapsed"), "The button is in collapsed state");
+
+  info("Click on the toggle button to toggle ON 3 pane inspector");
+  let onRuleViewAdded = inspector.once("ruleview-added");
+  EventUtils.synthesizeMouseAtCenter(button, {}, inspector.panelDoc.defaultView);
+  await onRuleViewAdded;
+
+  info("Checking the state of the 3 pane inspector");
+  let sidebarWidth = inspector.splitBox.state.width;
+  let sidebarSplitBoxWidth = inspector.sidebarSplitBox.state.width;
+  ok(!button.classList.contains("pane-collapsed"), "The button is in expanded state");
+  ok(doc.getElementById("ruleview-panel"), "The rule view panel exist");
+  is(inspector.sidebar.getCurrentTabID(), "layoutview",
+    "Layout view is shown in the sidebar");
+  is(ruleViewSidebar.style.display, "block", "The split rule view sidebar is displayed");
+  is(sidebarWidth, toolboxWidth * 2 / 3, "Got correct main split box width");
+  is(sidebarSplitBoxWidth, toolboxWidth / 3, "Got correct sidebar split box width");
+
+  info("Click on the toggle button to toggle OFF the 3 pane inspector");
+  onRuleViewAdded = inspector.once("ruleview-added");
+  EventUtils.synthesizeMouseAtCenter(button, {}, inspector.panelDoc.defaultView);
+  await onRuleViewAdded;
+
+  info("Checking the state of the 2 pane inspector");
+  sidebarWidth = inspector.splitBox.state.width;
+  ok(button.classList.contains("pane-collapsed"), "The button is in collapsed state");
+  is(inspector.sidebar.getCurrentTabID(), "ruleview",
+    "Rule view is shown in the sidebar");
+  is(ruleViewSidebar.style.display, "none", "The split rule view sidebar is hidden");
+  is(sidebarWidth, sidebarSplitBoxWidth, "Got correct sidebar width");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js
@@ -0,0 +1,47 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the 3 pane inspector toggle can render the middle and right panels of equal
+// sizes when the original sidebar can be doubled in width and be smaller than half the
+// toolbox's width in the BOTTOM host.
+
+const SIDEBAR_WIDTH = 200;
+
+add_task(async function() {
+  await pushPref("devtools.inspector.three-pane-toggle", true);
+
+  const { inspector } = await openInspectorForURL("about:blank");
+  const { panelDoc: doc } = inspector;
+  const button = doc.querySelector(".sidebar-toggle");
+  const toolboxWidth = doc.getElementById("inspector-splitter-box").clientWidth;
+
+  if (toolboxWidth < 600) {
+    ok(true, "Can't run the full test because the toolbox width is too small.");
+  } else {
+    info("Set the sidebar width to 200px");
+    inspector.splitBox.setState({ width: SIDEBAR_WIDTH });
+
+    info("Click on the toggle button to toggle ON 3 pane inspector");
+    let onRuleViewAdded = inspector.once("ruleview-added");
+    EventUtils.synthesizeMouseAtCenter(button, {}, inspector.panelDoc.defaultView);
+    await onRuleViewAdded;
+
+    info("Checking the sizes of the 3 pane inspector");
+    let sidebarWidth = inspector.splitBox.state.width;
+    let sidebarSplitBoxWidth = inspector.sidebarSplitBox.state.width;
+    is(sidebarWidth, SIDEBAR_WIDTH * 2, "Got correct main split box width");
+    is(sidebarSplitBoxWidth, SIDEBAR_WIDTH, "Got correct sidebar split box width");
+
+    info("Click on the toggle button to toggle OFF the 3 pane inspector");
+    onRuleViewAdded = inspector.once("ruleview-added");
+    EventUtils.synthesizeMouseAtCenter(button, {}, inspector.panelDoc.defaultView);
+    await onRuleViewAdded;
+
+    info("Checking the sidebar size of the 2 pane inspector");
+    sidebarWidth = inspector.splitBox.state.width;
+    is(sidebarWidth, SIDEBAR_WIDTH, "Got correct sidebar width");
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-04.js
@@ -0,0 +1,39 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the 3 pane inspector toggle button can render the bottom-left and
+// bottom-right panels of equal sizes in the SIDE host.
+
+add_task(async function() {
+  await pushPref("devtools.inspector.three-pane-toggle", true);
+
+  const { inspector, toolbox } = await openInspectorForURL("about:blank");
+  const { panelDoc: doc } = inspector;
+
+  info("Switch the host to side type");
+  await toolbox.switchHost("side");
+
+  const button = doc.querySelector(".sidebar-toggle");
+  const toolboxWidth = doc.getElementById("inspector-splitter-box").clientWidth;
+
+  info("Click on the toggle button to toggle ON 3 pane inspector");
+  let onRuleViewAdded = inspector.once("ruleview-added");
+  EventUtils.synthesizeMouseAtCenter(button, {}, inspector.panelDoc.defaultView);
+  await onRuleViewAdded;
+
+  info("Checking the sizes of the 3 pane inspector");
+  let sidebarSplitBoxWidth = inspector.sidebarSplitBox.state.width;
+  is(sidebarSplitBoxWidth, toolboxWidth / 2, "Got correct sidebar split box width");
+
+  info("Click on the toggle button to toggle OFF the 3 pane inspector");
+  onRuleViewAdded = inspector.once("ruleview-added");
+  EventUtils.synthesizeMouseAtCenter(button, {}, inspector.panelDoc.defaultView);
+  await onRuleViewAdded;
+
+  info("Checking the sidebar size of the 2 pane inspector");
+  let sidebarWidth = inspector.splitBox.state.width;
+  is(sidebarWidth, toolboxWidth, "Got correct sidebar width");
+});
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -43,20 +43,20 @@ pref("devtools.command-button-measure.en
 pref("devtools.command-button-noautohide.enabled", false);
 
 // Inspector preferences
 // Enable the Inspector
 pref("devtools.inspector.enabled", true);
 // What was the last active sidebar in the inspector
 pref("devtools.inspector.activeSidebar", "ruleview");
 pref("devtools.inspector.remote", false);
-// Enable the split rule view sidebar toggle in the inspector
-pref("devtools.inspector.split-sidebar-toggle", false);
-// Enable the split rule view in the inspector
-pref("devtools.inspector.split-rule-enabled", false);
+// Enable the 3 pane mode toggle in the inspector
+pref("devtools.inspector.three-pane-toggle", false);
+// Enable the 3 pane mode in the inspector
+pref("devtools.inspector.three-pane-enabled", false);
 // Collapse pseudo-elements by default in the rule-view
 pref("devtools.inspector.show_pseudo_elements", false);
 // The default size for image preview tooltips in the rule-view/computed-view/markup-view
 pref("devtools.inspector.imagePreviewTooltipSize", 300);
 // Enable user agent style inspection in rule-view
 pref("devtools.inspector.showUserAgentStyles", false);
 // Show all native anonymous content (like controls in <video> tags)
 pref("devtools.inspector.showAllAnonymousContent", false);
--- a/devtools/client/shared/components/splitter/SplitBox.js
+++ b/devtools/client/shared/components/splitter/SplitBox.js
@@ -65,52 +65,66 @@ class SplitBox extends Component {
       endPanelControl: false
     };
   }
 
   constructor(props) {
     super(props);
 
     /**
-     * The state stores the current orientation (vertical or horizontal)
-     * and the current size (width/height). All these values can change
-     * during the component's life time.
+     * The state stores whether or not the end panel should be controlled, the current
+     * orientation (vertical or horizontal), the splitter size, and the current size
+     * (width/height). All these values can change during the component's life time.
      */
     this.state = {
+      // True if the right/bottom panel should be controlled.
+      endPanelControl: props.endPanelControl,
+      // True if the splitter bar is vertical (default is vertical).
       vert: props.vert,
+      // Size of the splitter handle bar.
       splitterSize: props.splitterSize,
+      // Width of controlled panel.
       width: props.initialWidth || props.initialSize,
+      // Height of controlled panel.
       height: props.initialHeight || props.initialSize
     };
 
     this.onStartMove = this.onStartMove.bind(this);
     this.onStopMove = this.onStopMove.bind(this);
     this.onMove = this.onMove.bind(this);
   }
 
   componentWillReceiveProps(nextProps) {
-    let { splitterSize, vert } = nextProps;
+    let {
+      endPanelControl,
+      splitterSize,
+      vert,
+    } = nextProps;
+
+    if (endPanelControl != this.props.endPanelControl) {
+      this.setState({ endPanelControl });
+    }
 
     if (splitterSize != this.props.splitterSize) {
       this.setState({ splitterSize });
     }
 
     if (vert !== this.props.vert) {
       this.setState({ vert });
     }
   }
 
   shouldComponentUpdate(nextProps, nextState) {
     return nextState.width != this.state.width ||
+      nextState.endPanelControl != this.props.endPanelControl ||
       nextState.height != this.state.height ||
       nextState.vert != this.state.vert ||
       nextState.splitterSize != this.state.splitterSize ||
       nextProps.startPanel != this.props.startPanel ||
       nextProps.endPanel != this.props.endPanel ||
-      nextProps.endPanelControl != this.props.endPanelControl ||
       nextProps.minSize != this.props.minSize ||
       nextProps.maxSize != this.props.maxSize;
   }
 
   componentDidUpdate(prevProps, prevState) {
     if (this.props.onControlledPanelResized && (prevState.width !== this.state.width ||
                                                 prevState.height !== this.state.height)) {
       this.props.onControlledPanelResized(this.state.width, this.state.height);
@@ -149,19 +163,19 @@ class SplitBox extends Component {
    * Adjust size of the controlled panel. Depending on the current
    * orientation we either remember the width or height of
    * the splitter box.
    */
   onMove(x, y) {
     const node = ReactDOM.findDOMNode(this);
 
     let size;
-    let { endPanelControl } = this.props;
+    let { endPanelControl, vert } = this.state;
 
-    if (this.state.vert) {
+    if (vert) {
       // Switch the control flag in case of RTL. Note that RTL
       // has impact on vertical splitter only.
       if (document.dir === "rtl") {
         endPanelControl = !endPanelControl;
       }
 
       size = endPanelControl ?
         (node.offsetLeft + node.offsetWidth) - x :
@@ -179,18 +193,18 @@ class SplitBox extends Component {
         height: size
       });
     }
   }
 
   // Rendering
 
   render() {
-    const { splitterSize, vert } = this.state;
-    const { startPanel, endPanel, endPanelControl, minSize, maxSize } = this.props;
+    const { endPanelControl, splitterSize, vert } = this.state;
+    const { startPanel, endPanel, minSize, maxSize } = this.props;
 
     let style = Object.assign({}, this.props.style);
 
     // Calculate class names list.
     let classNames = ["split-box"];
     classNames.push(vert ? "vert" : "horz");
     if (this.props.className) {
       classNames = classNames.concat(this.props.className.split(" "));
--- a/devtools/client/themes/inspector.css
+++ b/devtools/client/themes/inspector.css
@@ -58,16 +58,20 @@ window {
 .inspector-tabpanel {
   min-width: 200px;
 }
 
 #inspector-splitter-box .controlled.pane-collapsed {
   visibility: collapse;
 }
 
+#inspector-splitter-box .sidebar-toggle::before {
+  transform: unset;
+}
+
 /* Use flex layout for the Inspector toolbar. For now, it's done
    specifically for the Inspector toolbar since general rule applied
    on .devtools-toolbar breaks breadcrumbs and also toolbars in other
    panels (e.g. webconsole, debugger), these are not ready for HTML
    layout yet. */
 #inspector-toolbar.devtools-toolbar {
   display: flex;
 }