Bug 1461522 - Switch meatball menu to a doorhanger; r=jdescottes
authorBrian Birtles <birtles@gmail.com>
Thu, 28 Jun 2018 15:16:09 +0900
changeset 814159 cc6ec2789c9d50a686f4df5a70bf0f1173d1d32e
parent 814158 d6badfe124333064087ea2e878728faed94f8969
child 814160 2fafbc23c259ba61732ed064492271ec95248771
push id115123
push userjdescottes@mozilla.com
push dateWed, 04 Jul 2018 17:42:29 +0000
reviewersjdescottes
bugs1461522
milestone63.0a1
Bug 1461522 - Switch meatball menu to a doorhanger; r=jdescottes MozReview-Commit-ID: BHBJqiwOfHW
devtools/client/framework/components/MeatballMenu.js
devtools/client/framework/components/ToolboxToolbar.js
devtools/client/framework/components/moz.build
devtools/client/jar.mn
devtools/client/themes/images/command-console.svg
devtools/client/themes/images/command-noautohide.svg
devtools/client/themes/images/dock-bottom.svg
devtools/client/themes/images/dock-side-left.svg
devtools/client/themes/images/dock-side-right.svg
devtools/client/themes/images/dock-undock.svg
devtools/client/themes/images/tool-options-photon.svg
devtools/client/themes/toolbox.css
devtools/client/webconsole/test/mochitest/browser_webconsole_split.js
devtools/client/webconsole/test/mochitest/browser_webconsole_split_persist.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/components/MeatballMenu.js
@@ -0,0 +1,197 @@
+/* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const MenuItem = createFactory(
+  require("devtools/client/shared/components/menu/MenuItem")
+);
+const MenuList = createFactory(
+  require("devtools/client/shared/components/menu/MenuList")
+);
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { hr } = dom;
+const { openDocLink } = require("devtools/client/shared/link");
+
+class MeatballMenu extends PureComponent {
+  static get propTypes() {
+    return {
+      // The id of the currently selected tool, e.g. "inspector"
+      currentToolId: PropTypes.string,
+
+      // List of possible docking options.
+      hostTypes: PropTypes.arrayOf(
+        PropTypes.shape({
+          position: PropTypes.string.isRequired,
+          switchHost: PropTypes.func.isRequired,
+        })
+      ),
+
+      // Current docking type. Typically one of the position values in
+      // |hostTypes| but this is not always the case (e.g. when it is "custom").
+      currentHostType: PropTypes.string,
+
+      // Is the split console currently visible?
+      isSplitConsoleActive: PropTypes.bool,
+
+      // Are we disabling the behavior where pop-ups are automatically closed
+      // when clicking outside them?
+      //
+      // This is a tri-state value that may be true/false or undefined where
+      // undefined means that the option is not relevant in this context
+      // (i.e. we're not in a browser toolbox).
+      disableAutohide: PropTypes.bool,
+
+      // Function to turn the options panel on / off.
+      toggleOptions: PropTypes.func.isRequired,
+
+      // Function to turn the split console on / off.
+      toggleSplitConsole: PropTypes.func,
+
+      // Function to turn the disable pop-up autohide behavior on / off.
+      toggleNoAutohide: PropTypes.func,
+
+      // Localization interface.
+      L10N: PropTypes.object.isRequired,
+
+      // Callback function that will be invoked any time the component contents
+      // update in such a way that its bounding box might change.
+      onResize: PropTypes.func,
+    };
+  }
+
+  componentDidUpdate(prevProps) {
+    if (!this.props.onResize) {
+      return;
+    }
+
+    // We are only expecting the following kinds of dynamic changes when a popup
+    // is showing:
+    //
+    // - The "Disable pop-up autohide" menu item being added after the Browser
+    //   Toolbox is connected.
+    // - The split console label changing between "Show split console" and "Hide
+    //   split console".
+    // - The "Show/Hide split console" entry being added removed or removed.
+    //
+    // The latter two cases are only likely to be noticed when "Disable pop-up
+    // autohide" is active, but for completeness we handle them here.
+    const didChange =
+      typeof this.props.disableAutohide !== typeof prevProps.disableAutohide ||
+      this.props.currentToolId !== prevProps.currentToolId ||
+      this.props.isSplitConsoleActive !== prevProps.isSplitConsoleActive;
+
+    if (didChange) {
+      this.props.onResize();
+    }
+  }
+
+  render() {
+    const items = [];
+
+    // Dock options
+    for (const hostType of this.props.hostTypes) {
+      const l10nkey =
+        hostType.position === "window" ? "separateWindow" : hostType.position;
+      items.push(
+        MenuItem({
+          id: `toolbox-meatball-menu-dock-${hostType.position}`,
+          label: this.props.L10N.getStr(
+            `toolbox.meatballMenu.dock.${l10nkey}.label`
+          ),
+          onClick: () => hostType.switchHost(),
+          checked: hostType.position === this.props.currentHostType,
+          className: "iconic",
+        })
+      );
+    }
+
+    if (items.length) {
+      items.push(hr());
+    }
+
+    // Split console
+    if (this.props.currentToolId !== "webconsole") {
+      items.push(
+        MenuItem({
+          id: "toolbox-meatball-menu-splitconsole",
+          label: this.props.L10N.getStr(
+            `toolbox.meatballMenu.${
+              this.props.isSplitConsoleActive ? "hideconsole" : "splitconsole"
+            }.label`
+          ),
+          accelerator: "Esc",
+          onClick: this.props.toggleSplitConsole,
+          className: "iconic",
+        })
+      );
+    }
+
+    // Disable pop-up autohide
+    //
+    // If |disableAutohide| is undefined, it means this feature is not available
+    // in this context.
+    if (typeof this.props.disableAutohide !== "undefined") {
+      items.push(
+        MenuItem({
+          id: "toolbox-meatball-menu-noautohide",
+          label: this.props.L10N.getStr(
+            "toolbox.meatballMenu.noautohide.label"
+          ),
+          type: "checkbox",
+          checked: this.props.disableAutohide,
+          onClick: this.props.toggleNoAutohide,
+          className: "iconic",
+        })
+      );
+    }
+
+    // Settings
+    items.push(
+      MenuItem({
+        id: "toolbox-meatball-menu-settings",
+        label: this.props.L10N.getStr("toolbox.meatballMenu.settings.label"),
+        accelerator: this.props.L10N.getStr("toolbox.help.key"),
+        onClick: () => this.props.toggleOptions(),
+        className: "iconic",
+      })
+    );
+
+    items.push(hr());
+
+    // Getting started
+    items.push(
+      MenuItem({
+        id: "toolbox-meatball-menu-documentation",
+        label: this.props.L10N.getStr(
+          "toolbox.meatballMenu.documentation.label"
+        ),
+        onClick: () => {
+          openDocLink(
+            "https://developer.mozilla.org/docs/Tools?utm_source=devtools&utm_medium=tabbar-menu"
+          );
+        },
+      })
+    );
+
+    // Give feedback
+    items.push(
+      MenuItem({
+        id: "toolbox-meatball-menu-community",
+        label: this.props.L10N.getStr("toolbox.meatballMenu.community.label"),
+        onClick: () => {
+          openDocLink(
+            "https://discourse.mozilla.org/c/devtools?utm_source=devtools&utm_medium=tabbar-menu"
+          );
+        },
+      })
+    );
+
+    return MenuList({ id: "toolbox-meatball-menu" }, items);
+  }
+}
+
+module.exports = MeatballMenu;
--- a/devtools/client/framework/components/ToolboxToolbar.js
+++ b/devtools/client/framework/components/ToolboxToolbar.js
@@ -2,20 +2,19 @@
  * 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 { Component, createFactory } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const {div, button} = dom;
-const {openDocLink} = require("devtools/client/shared/link");
 
-const Menu = require("devtools/client/framework/menu");
-const MenuItem = require("devtools/client/framework/menu-item");
+const MeatballMenu = createFactory(require("devtools/client/framework/components/MeatballMenu"));
+const MenuButton = createFactory(require("devtools/client/shared/components/menu/MenuButton"));
 const ToolboxTabs = createFactory(require("devtools/client/framework/components/ToolboxTabs"));
 
 /**
  * This is the overall component for the toolbox toolbar. It is designed to not know how
  * the state is being managed, and attempts to be as pure as possible. The
  * ToolboxController component controls the changing state, and passes in everything as
  * props.
  */
@@ -104,17 +103,17 @@ class ToolboxToolbar extends Component {
       ? (
         div(
           {
             className: classnames.join(" ")
           },
           startButtons,
           ToolboxTabs(this.props),
           endButtons,
-          renderToolboxControls(this.props)
+          renderToolboxControls(this.props, this.refs)
         )
       )
       : div({ className: classnames.join(" ") });
   }
 }
 
 module.exports = ToolboxToolbar;
 
@@ -219,80 +218,94 @@ function renderToolboxButtons({focusedBu
  */
 function renderSeparator() {
   return div({className: "devtools-separator"});
 }
 
 /**
  * Render the toolbox control buttons. The following props are expected:
  *
- * @param {string} focusedButton
+ * @param {string} props.focusedButton
  *        The id of the focused button.
- * @param {Object[]} hostTypes
+ * @param {string} props.currentToolId
+ *        The id of the currently selected tool, e.g. "inspector".
+ * @param {Object[]} props.hostTypes
  *        Array of host type objects.
- * @param {string} hostTypes[].position
+ * @param {string} props.hostTypes[].position
  *        Position name.
- * @param {Function} hostTypes[].switchHost
+ * @param {Function} props.hostTypes[].switchHost
  *        Function to switch the host.
- * @param {string} currentHostType
+ * @param {string} props.currentHostType
  *        The current docking configuration.
- * @param {boolean} areDockOptionsEnabled
+ * @param {boolean} props.areDockOptionsEnabled
  *        They are not enabled in certain situations like when they are in the
  *        WebIDE.
- * @param {boolean} canCloseToolbox
+ * @param {boolean} props.canCloseToolbox
  *        Do we need to add UI for closing the toolbox? We don't when the
  *        toolbox is undocked, for example.
- * @param {boolean} isSplitConsoleActive
+ * @param {boolean} props.isSplitConsoleActive
  *         Is the split console currently visible?
  *        toolbox is undocked, for example.
- * @param {boolean|undefined} disableAutohide
+ * @param {boolean|undefined} props.disableAutohide
  *        Are we disabling the behavior where pop-ups are automatically
  *        closed when clicking outside them?
  *        (Only defined for the browser toolbox.)
- * @param {Function} selectTool
+ * @param {Function} props.selectTool
  *        Function to select a tool based on its id.
- * @param {Function} toggleOptions
+ * @param {Function} props.toggleOptions
  *        Function to turn the options panel on / off.
- * @param {Function} toggleSplitConsole
+ * @param {Function} props.toggleSplitConsole
  *        Function to turn the split console on / off.
- * @param {Function} toggleNoAutohide
+ * @param {Function} props.toggleNoAutohide
  *        Function to turn the disable pop-up autohide behavior on / off.
- * @param {Function} closeToolbox
+ * @param {Function} props.closeToolbox
  *        Completely close the toolbox.
- * @param {Function} focusButton
+ * @param {Function} props.focusButton
  *        Keep a record of the currently focused button.
- * @param {Object} L10N
+ * @param {Object} props.L10N
  *        Localization interface.
+ * @param {Object} props.toolbox
+ *        The devtools toolbox. Used by the MenuButton component to display
+ *        the menu popup.
+ * @param {Object} refs
+ *        The components refs object. Used to keep a reference to the MenuButton
+ *        for the meatball menu so that we can tell it to resize its contents
+ *        when they change.
  */
-function renderToolboxControls(props) {
+function renderToolboxControls(props, refs) {
   const {
     focusedButton,
+    canCloseToolbox,
     closeToolbox,
-    hostTypes,
     focusButton,
     L10N,
-    areDockOptionsEnabled,
-    canCloseToolbox,
+    toolbox,
   } = props;
 
   const meatballMenuButtonId = "toolbox-meatball-menu-button";
 
-  const meatballMenuButton = button({
-    id: meatballMenuButtonId,
-    onFocus: () => focusButton(meatballMenuButtonId),
-    className: "devtools-button",
-    title: L10N.getStr("toolbox.meatballMenu.button.tooltip"),
-    onClick: evt => {
-      showMeatballMenu(evt.target, {
-        ...props,
-        hostTypes: areDockOptionsEnabled ? hostTypes : [],
-      });
+  const meatballMenuButton = MenuButton(
+    {
+      id: meatballMenuButtonId,
+      menuId: meatballMenuButtonId + "-panel",
+      doc: toolbox.doc,
+      onFocus: () => focusButton(meatballMenuButtonId),
+      className: "devtools-button",
+      title: L10N.getStr("toolbox.meatballMenu.button.tooltip"),
+      tabIndex: focusedButton === meatballMenuButtonId ? "0" : "-1",
+      ref: "meatballMenuButton",
     },
-    tabIndex: focusedButton === meatballMenuButtonId ? "0" : "-1",
-  });
+    MeatballMenu({
+      ...props,
+      hostTypes: props.areDockOptionsEnabled ? props.hostTypes : [],
+      onResize: () => {
+        refs.meatballMenuButton.resizeContent();
+      },
+    })
+  );
 
   const closeButtonId = "toolbox-close";
 
   const closeButton = canCloseToolbox
     ? button({
       id: closeButtonId,
       onFocus: () => focusButton(closeButtonId),
       className: "devtools-button",
@@ -304,154 +317,8 @@ function renderToolboxControls(props) {
     })
     : null;
 
   return div({id: "toolbox-controls"},
     meatballMenuButton,
     closeButton
   );
 }
-
-/**
- * Display the "..." menu (affectionately known as the meatball menu).
- *
- * @param {Object} menuButton
- *        The <button> element from which the menu should pop out. The geometry
- *        of this element is used to position the menu.
- * @param {Object} props
- *        Properties as described below.
- * @param {string} props.currentToolId
- *        The id of the currently selected tool.
- * @param {Object[]} props.hostTypes
- *        Array of host type objects.
- *        This array will be empty if we shouldn't shouldn't show any dock
- *        options.
- * @param {string} props.hostTypes[].position
- *        Position name.
- * @param {Function} props.hostTypes[].switchHost
- *        Function to switch the host.
- * @param {string} props.currentHostType
- *        The current docking configuration.
- * @param {boolean} isSplitConsoleActive
- *        Is the split console currently visible?
- * @param {boolean|undefined} disableAutohide
- *        Are we disabling the behavior where pop-ups are automatically
- *        closed when clicking outside them.
- *        (Only defined for the browser toolbox.)
- * @param {Function} selectTool
- *        Function to select a tool based on its id.
- * @param {Function} toggleOptions
- *        Function to turn the options panel on / off.
- * @param {Function} toggleSplitConsole
- *        Function to turn the split console on / off.
- * @param {Function} toggleNoAutohide
- *        Function to turn the disable pop-up autohide behavior on / off.
- * @param {Object} props.L10N
- *        Localization interface.
- * @param {Object} props.toolbox
- *        The devtools toolbox. Used by the Menu component to determine which
- *        document to use.
- */
-function showMeatballMenu(
-  menuButton,
-  {
-    currentToolId,
-    hostTypes,
-    currentHostType,
-    isSplitConsoleActive,
-    disableAutohide,
-    toggleOptions,
-    toggleSplitConsole,
-    toggleNoAutohide,
-    L10N,
-    toolbox,
-  }
-) {
-  const menu = new Menu({ id: "toolbox-meatball-menu" });
-
-  // Dock options
-  for (const hostType of hostTypes) {
-    const l10nkey =
-      hostType.position === "window"
-        ? "separateWindow"
-        : hostType.position;
-    menu.append(
-      new MenuItem({
-        id: `toolbox-meatball-menu-dock-${hostType.position}`,
-        label: L10N.getStr(`toolbox.meatballMenu.dock.${l10nkey}.label`),
-        click: () => hostType.switchHost(),
-        type: "checkbox",
-        checked: hostType.position === currentHostType,
-      })
-    );
-  }
-
-  if (menu.items.length) {
-    menu.append(new MenuItem({ type: "separator" }));
-  }
-
-  // Split console
-  if (currentToolId !== "webconsole") {
-    menu.append(new MenuItem({
-      id: "toolbox-meatball-menu-splitconsole",
-      label: L10N.getStr(
-        `toolbox.meatballMenu.${
-          isSplitConsoleActive ? "hideconsole" : "splitconsole"
-        }.label`
-      ),
-      accelerator: "Esc",
-      click: toggleSplitConsole,
-    }));
-  }
-
-  // Disable pop-up autohide
-  //
-  // If |disableAutohide| is undefined, it means this feature is not available
-  // in this context.
-  if (typeof disableAutohide !== "undefined") {
-    menu.append(new MenuItem({
-      id: "toolbox-meatball-menu-noautohide",
-      label: L10N.getStr("toolbox.meatballMenu.noautohide.label"),
-      type: "checkbox",
-      checked: disableAutohide,
-      click: toggleNoAutohide,
-    }));
-  }
-
-  // Settings
-  menu.append(new MenuItem({
-    id: "toolbox-meatball-menu-settings",
-    label: L10N.getStr("toolbox.meatballMenu.settings.label"),
-    accelerator: L10N.getStr("toolbox.help.key"),
-    click: () => toggleOptions(),
-  }));
-
-  if (menu.items.length) {
-    menu.append(new MenuItem({ type: "separator" }));
-  }
-
-  // Getting started
-  menu.append(new MenuItem({
-    id: "toolbox-meatball-menu-documentation",
-    label: L10N.getStr("toolbox.meatballMenu.documentation.label"),
-    click: () => {
-      openDocLink(
-        "https://developer.mozilla.org/docs/Tools?utm_source=devtools&utm_medium=tabbar-menu");
-    },
-  }));
-
-  // Give feedback
-  menu.append(new MenuItem({
-    id: "toolbox-meatball-menu-community",
-    label: L10N.getStr("toolbox.meatballMenu.community.label"),
-    click: () => {
-      openDocLink(
-        "https://discourse.mozilla.org/c/devtools?utm_source=devtools&utm_medium=tabbar-menu");
-    },
-  }));
-
-  const rect = menuButton.getBoundingClientRect();
-  const screenX = menuButton.ownerDocument.defaultView.mozInnerScreenX;
-  const screenY = menuButton.ownerDocument.defaultView.mozInnerScreenY;
-
-  // Display the popup below the button.
-  menu.popupWithZoom(rect.left + screenX, rect.bottom + screenY, toolbox);
-}
--- a/devtools/client/framework/components/moz.build
+++ b/devtools/client/framework/components/moz.build
@@ -1,13 +1,14 @@
 # -*- Mode: python; 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(
+  'MeatballMenu.js',
   'ToolboxController.js',
   'ToolboxTab.js',
   'ToolboxTabs.js',
   'ToolboxToolbar.js',
 )
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -129,19 +129,21 @@ devtools.jar:
     skin/images/alerticon-warning@2x.png (themes/images/alerticon-warning@2x.png)
     skin/rules.css (themes/rules.css)
     skin/images/command-paintflashing.svg (themes/images/command-paintflashing.svg)
     skin/images/command-screenshot.svg (themes/images/command-screenshot.svg)
     skin/images/command-responsivemode.svg (themes/images/command-responsivemode.svg)
     skin/images/command-pick.svg (themes/images/command-pick.svg)
     skin/images/command-pick-accessibility.svg (themes/images/command-pick-accessibility.svg)
     skin/images/command-frames.svg (themes/images/command-frames.svg)
+    skin/images/command-console.svg (themes/images/command-console.svg)
     skin/images/command-eyedropper.svg (themes/images/command-eyedropper.svg)
     skin/images/command-rulers.svg (themes/images/command-rulers.svg)
     skin/images/command-measure.svg (themes/images/command-measure.svg)
+    skin/images/command-noautohide.svg (themes/images/command-noautohide.svg)
     skin/images/command-chevron.svg (themes/images/command-chevron.svg)
     skin/markup.css (themes/markup.css)
     skin/images/editor-error.png (themes/images/editor-error.png)
     skin/images/breakpoint.svg (themes/images/breakpoint.svg)
     skin/webconsole.css (themes/webconsole.css)
     skin/images/webconsole.svg (themes/images/webconsole.svg)
     skin/images/breadcrumbs-scrollbutton.svg (themes/images/breadcrumbs-scrollbutton.svg)
     skin/animation.css (themes/animation.css)
@@ -173,30 +175,35 @@ devtools.jar:
     skin/images/more.svg (themes/images/more.svg)
     skin/images/pause.svg (themes/images/pause.svg)
     skin/images/play.svg (themes/images/play.svg)
     skin/images/rewind.svg (themes/images/rewind.svg)
     skin/images/debugger-step-in.svg (themes/images/debugger-step-in.svg)
     skin/images/debugger-step-out.svg (themes/images/debugger-step-out.svg)
     skin/images/debugger-step-over.svg (themes/images/debugger-step-over.svg)
     skin/images/debugger-toggleBreakpoints.svg (themes/images/debugger-toggleBreakpoints.svg)
+    skin/images/dock-bottom.svg (themes/images/dock-bottom.svg)
+    skin/images/dock-side-left.svg (themes/images/dock-side-left.svg)
+    skin/images/dock-side-right.svg (themes/images/dock-side-right.svg)
+    skin/images/dock-undock.svg (themes/images/dock-undock.svg)
     skin/images/jump-definition.svg (themes/images/jump-definition.svg)
     skin/images/tracer-icon.png (themes/images/tracer-icon.png)
     skin/images/tracer-icon@2x.png (themes/images/tracer-icon@2x.png)
     skin/floating-scrollbars-dark-theme.css (themes/floating-scrollbars-dark-theme.css)
     skin/floating-scrollbars-responsive-design.css (themes/floating-scrollbars-responsive-design.css)
     skin/inspector.css (themes/inspector.css)
     skin/images/profiler-stopwatch.svg (themes/images/profiler-stopwatch.svg)
     skin/images/debugging-addons.svg (themes/images/debugging-addons.svg)
     skin/images/debugging-tabs.svg (themes/images/debugging-tabs.svg)
     skin/images/debugging-workers.svg (themes/images/debugging-workers.svg)
     skin/images/globe.svg (themes/images/globe.svg)
     skin/images/sad-face.svg (themes/images/sad-face.svg)
     skin/images/shape-swatch.svg (themes/images/shape-swatch.svg)
     skin/images/tool-options.svg (themes/images/tool-options.svg)
+    skin/images/tool-options-photon.svg (themes/images/tool-options-photon.svg)
     skin/images/tool-webconsole.svg (themes/images/tool-webconsole.svg)
     skin/images/tool-canvas.svg (themes/images/tool-canvas.svg)
     skin/images/tool-debugger.svg (themes/images/tool-debugger.svg)
     skin/images/tool-inspector.svg (themes/images/tool-inspector.svg)
     skin/images/tool-shadereditor.svg (themes/images/tool-shadereditor.svg)
     skin/images/tool-styleeditor.svg (themes/images/tool-styleeditor.svg)
     skin/images/tool-storage.svg (themes/images/tool-storage.svg)
     skin/images/tool-profiler.svg (themes/images/tool-profiler.svg)
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/command-console.svg
@@ -0,0 +1,11 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="context-fill #0b0b0b">
+  <path d="
+    M13 1a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-10a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h10z
+    M13 3h-10a1 1 0 0 0-1 1v1h12v-1a1 1 0 0 0-1-1z
+    M14 6h-12v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1z"/>
+  <path d="M4.5 7.5l2 2l-2 2" stroke="context-fill" stroke-width="1"
+    stroke-linecap="round" stroke-linejoin="round" fill="none"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/command-noautohide.svg
@@ -0,0 +1,9 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="context-fill #0b0b0b">
+  <path d="M1 5a2 2 0 0 1 2-2h1l2-2 2 2h5a2 2 0 0 1 2 2v8
+    a2 2 0 0 1-2 2h-10a2 2 0 0 1-2-2z"
+    stroke-width="2" stroke="context-fill"
+    stroke-linejoin="round" stroke-linecap="round" fill="none"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/dock-bottom.svg
@@ -0,0 +1,10 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
+  fill="context-fill #0b0b0b">
+  <path d="
+    M12 0a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3h-8a3 3 0 0 1-3-3v-10a3 3 0 0 1 3-3h8z
+    M12 2h-8a1 1 0 0 0-1 1v7h10v-7a1 1 0 0 0-1-1z
+    M13 11h-10v2a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/dock-side-left.svg
@@ -0,0 +1,10 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
+  fill="context-fill #0b0b0b">
+  <path d="
+    M0 4a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-10a3 3 0 0 1-3-3z
+    M2 4v8a1 1 0 0 0 1 1h2v-10h-2a1 1 0 0 0-1 1z
+    M6 3v10h7a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/dock-side-right.svg
@@ -0,0 +1,10 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
+  fill="context-fill #0b0b0b">
+  <path d="
+    M0 4a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-10a3 3 0 0 1-3-3z
+    M2 4v8a1 1 0 0 0 1 1h7v-10h-7a1 1 0 0 0-1 1z
+    M11 3v10h2a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/dock-undock.svg
@@ -0,0 +1,12 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
+  fill="context-fill #0b0b0b">
+  <path d="
+    M13 0a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3h-5a3 3 0 0 1-3-3v-5a3 3 0 0 1 3-3z
+    M13 2h-5a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1z"/>
+  <path d="M9 13v-1h2v1a3 3 0 0 1-3 3
+    h-5a3 3 0 0 1-3-3v-5a3 3 0 0 1 3-3h1v2h-1a1 1 0 0 0-1 1v5
+    a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/tool-options-photon.svg
@@ -0,0 +1,9 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"
+  fill="context-fill #0b0b0b">
+  <path d="M7.45 15.95a1.86 1.86 0 0 1-1.86-1.86v-.89c-.3-.14-.6-.3-.89-.5l-.77.43a1.87 1.87 0 0 1-2.54-.68l-.55-.96c-.51-.88-.21-2.02.68-2.54l.77-.44a5.8 5.8 0 0 1 0-1.02l-.77-.45A1.87 1.87 0 0 1 .84 4.5l.55-.95a1.87 1.87 0 0 1 2.54-.69l.77.45c.28-.2.58-.37.89-.52v-.88C5.58.88 6.42.05 7.45.05h1.1c1.03 0 1.86.83 1.86 1.86v.89c.3.14.6.3.89.5l.77-.44a1.86 1.86 0 0 1 2.54.69l.55.95c.52.9.21 2.03-.68 2.55l-.77.44c.03.34.03.68 0 1.02l.77.44c.89.52 1.2 1.66.68 2.55l-.55.95a1.87 1.87 0 0 1-2.54.69l-.77-.45c-.29.2-.58.38-.88.52v.88c0 1.03-.84 1.86-1.87 1.86h-1.1zM5.49 11.4c.26.2.53.35.82.48l.78.34v1.87c0 .2.17.35.36.35h1.1c.2 0 .36-.16.36-.35v-1.87l.78-.34c.3-.13.57-.29.82-.48l.69-.5 1.62.93a.36.36 0 0 0 .48-.13l.56-.96a.36.36 0 0 0-.13-.48l-1.62-.94.1-.85c.03-.31.03-.63 0-.95l-.1-.85 1.62-.93c.17-.1.22-.32.13-.49l-.56-.95a.36.36 0 0 0-.48-.13l-1.62.94-.69-.51a4.27 4.27 0 0 0-.82-.48l-.78-.34V1.91c0-.2-.16-.35-.36-.35h-1.1c-.2 0-.36.16-.36.35v1.87l-.78.34c-.3.13-.57.29-.82.48l-.69.5-1.62-.93a.36.36 0 0 0-.48.13l-.56.96c-.1.16-.03.38.13.48l1.62.94-.1.85c-.03.31-.03.63 0 .95l.1.84-1.62.94a.36.36 0 0 0-.13.48l.56.96c.06.1.18.18.3.18.07 0 .13-.02.18-.05l1.62-.94.69.51z"/>
+  <circle cx="8" cy="8" r="1.5" stroke-width="1"
+    stroke="context-fill" fill="none"/>
+</svg>
--- a/devtools/client/themes/toolbox.css
+++ b/devtools/client/themes/toolbox.css
@@ -1,17 +1,24 @@
 /* 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/. */
 
 :root {
   --close-button-image: url(chrome://devtools/skin/images/close.svg);
+  --dock-bottom-image: url(chrome://devtools/skin/images/dock-bottom.svg);
+  --dock-side-right-image: url(chrome://devtools/skin/images/dock-side-right.svg);
+  --dock-side-left-image: url(chrome://devtools/skin/images/dock-side-left.svg);
+  --dock-undock-image: url(chrome://devtools/skin/images/dock-undock.svg);
   --more-button-image: url(chrome://devtools/skin/images/more.svg);
+  --settings-image: url(chrome://devtools/skin/images/tool-options-photon.svg);
 
+  --command-noautohide-image: url(images/command-noautohide.svg);
+  --command-console-image: url(images/command-console.svg);
   --command-paintflashing-image: url(images/command-paintflashing.svg);
   --command-screenshot-image: url(images/command-screenshot.svg);
   --command-responsive-image: url(images/command-responsivemode.svg);
   --command-scratchpad-image: url(images/tool-scratchpad.svg);
   --command-pick-image: url(images/command-pick.svg);
   --command-pick-accessibility-image: url(images/command-pick-accessibility.svg);
   --command-frames-image: url(images/command-frames.svg);
   --command-rulers-image: url(images/command-rulers.svg);
@@ -214,16 +221,38 @@
   min-width: 24px;
 }
 
 #toolbox-meatball-menu-button::before {
   fill: var(--theme-toolbar-photon-icon-color);
   background-image: var(--more-button-image);
 }
 
+#toolbox-meatball-menu-dock-bottom > .label::before {
+  background-image: var(--dock-bottom-image);
+}
+#toolbox-meatball-menu-dock-left > .label::before {
+  background-image: var(--dock-side-left-image);
+}
+#toolbox-meatball-menu-dock-right > .label::before {
+  background-image: var(--dock-side-right-image);
+}
+#toolbox-meatball-menu-dock-window > .label::before {
+  background-image: var(--dock-undock-image);
+}
+#toolbox-meatball-menu-splitconsole > .label::before {
+  background-image: var(--command-console-image);
+}
+#toolbox-meatball-menu-noautohide > .label::before {
+  background-image: var(--command-noautohide-image);
+}
+#toolbox-meatball-menu-settings > .label::before {
+  background-image: var(--settings-image);
+}
+
 /* Command buttons */
 
 .command-button,
 #toolbox-controls > button {
   /* !important is needed to override .devtools-button rules in common.css */
   padding: 0 !important;
   margin: 0 !important;
   border: none !important;
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_split.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_split.js
@@ -99,19 +99,19 @@ add_task(async function() {
       EventUtils.sendMouseEvent({ type: "click" }, button);
 
       toolbox.doc.addEventListener("popupshown", () => {
         const menuItem =
           toolbox.doc.getElementById("toolbox-meatball-menu-splitconsole");
 
         // Return undefined if the menu item is not available
         let label;
-        if (menuItem) {
+        if (menuItem && menuItem.querySelector(".label")) {
           label =
-            menuItem.label ===
+            menuItem.querySelector(".label").textContent ===
             L10N.getStr("toolbox.meatballMenu.hideconsole.label")
               ? "hide"
               : "split";
         }
 
         // Wait for menu to close
         toolbox.doc.addEventListener("popuphidden", () => {
           resolve(label);
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_split_persist.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_split_persist.js
@@ -92,17 +92,18 @@ function doesMenuSayHide(toolbox) {
     EventUtils.sendMouseEvent({ type: "click" }, button);
 
     toolbox.doc.addEventListener("popupshown", () => {
       const menuItem =
         toolbox.doc.getElementById("toolbox-meatball-menu-splitconsole");
 
       const result =
         menuItem &&
-        menuItem.label ===
+        menuItem.querySelector(".label") &&
+        menuItem.querySelector(".label").textContent ===
           L10N.getStr("toolbox.meatballMenu.hideconsole.label");
 
       toolbox.doc.addEventListener("popuphidden", () => {
         resolve(result);
       },
       { once: true });
       EventUtils.synthesizeKey("KEY_Escape");
     },