Bug 1467572 - Part 9: Use native context menu instead of select elements in the reload condition menu of RDM. r=Honza
authorGabriel Luong <gabriel.luong@gmail.com>
Wed, 15 Aug 2018 17:27:41 -0400
changeset 431856 81d81c2f9098f782581869d6bafc09800e7601b9
parent 431855 aa2ea377f944d50885514d39d6568a510fe797e9
child 431857 b55503cf4ad4328e5bf5e1f9b590b97332789a66
push id34451
push userebalazs@mozilla.com
push dateThu, 16 Aug 2018 09:25:15 +0000
treeherdermozilla-central@161817e6d127 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHonza
bugs1467572
milestone63.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 1467572 - Part 9: Use native context menu instead of select elements in the reload condition menu of RDM. r=Honza - Converts the reload condition <select> to use native context menu - Converts the showMenu util to be reusable by both RDM and the Network Monitor - Renames getToplevelWindow to getTopLevelWindow - Renames the reload condition menu to be a setting menu in RDM according to the designs - Uses the new photon setting icon in RDM
devtools/client/jar.mn
devtools/client/locales/en-US/responsive.properties
devtools/client/netmonitor/src/components/Toolbar.js
devtools/client/netmonitor/src/utils/menu.js
devtools/client/netmonitor/src/utils/moz.build
devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
devtools/client/netmonitor/webpack.config.js
devtools/client/responsive.html/actions/screenshot.js
devtools/client/responsive.html/components/Browser.js
devtools/client/responsive.html/components/ReloadConditions.js
devtools/client/responsive.html/components/SettingsMenu.js
devtools/client/responsive.html/components/ToggleMenu.js
devtools/client/responsive.html/components/Toolbar.js
devtools/client/responsive.html/components/moz.build
devtools/client/responsive.html/index.css
devtools/client/responsive.html/utils/window.js
devtools/client/shared/components/menu/moz.build
devtools/client/shared/components/menu/utils.js
devtools/client/themes/images/settings.svg
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -260,16 +260,17 @@ devtools.jar:
     skin/images/diff.svg (themes/images/diff.svg)
     skin/images/import.svg (themes/images/import.svg)
     skin/images/pane-collapse.svg (themes/images/pane-collapse.svg)
     skin/images/pane-expand.svg (themes/images/pane-expand.svg)
     skin/images/help.svg (themes/images/help.svg)
     skin/images/read-only.svg (themes/images/read-only.svg)
     skin/images/reveal.svg (themes/images/reveal.svg)
     skin/images/select-arrow.svg (themes/images/select-arrow.svg)
+    skin/images/settings.svg (themes/images/settings.svg)
 
     # Debugger
     skin/images/debugger/angular.svg (themes/images/debugger/angular.svg)
     skin/images/debugger/arrow.svg (themes/images/debugger/arrow.svg)
     skin/images/debugger/back.svg (themes/images/debugger/back.svg)
     skin/images/debugger/blackBox.svg (themes/images/debugger/blackBox.svg)
     skin/images/debugger/breakpoint.svg (themes/images/debugger/breakpoint.svg)
     skin/images/debugger/close.svg (themes/images/debugger/close.svg)
--- a/devtools/client/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -119,24 +119,16 @@ responsive.deviceAdderSave=Save
 # device.  %4$S is the user agent of the device.  %5$S is a boolean value
 # noting whether touch input is supported.
 responsive.deviceDetails=Size: %1$S x %2$S\nDPR: %3$S\nUA: %4$S\nTouch: %5$S
 
 # LOCALIZATION NOTE (responsive.devicePixelRatioOption): UI option in a menu to configure
 # the device pixel ratio. %1$S is the devicePixelRatio value of the device.
 responsive.devicePixelRatioOption=DPR: %1$S
 
-# LOCALIZATION NOTE (responsive.reloadConditions.label): Label on button to open a menu
-# used to choose whether to reload the page automatically when certain actions occur.
-responsive.reloadConditions.label=Reload when…
-
-# LOCALIZATION NOTE (responsive.reloadConditions.title): Title on button to open a menu
-# used to choose whether to reload the page automatically when certain actions occur.
-responsive.reloadConditions.title=Choose whether to reload the page automatically when certain actions occur
-
 # LOCALIZATION NOTE (responsive.reloadConditions.touchSimulation): Label on checkbox used
 # to select whether to reload when touch simulation is toggled.
 responsive.reloadConditions.touchSimulation=Reload when touch simulation is toggled
 
 # LOCALIZATION NOTE (responsive.reloadConditions.userAgent): Label on checkbox used
 # to select whether to reload when user agent is changed.
 responsive.reloadConditions.userAgent=Reload when user agent is changed
 
--- a/devtools/client/netmonitor/src/components/Toolbar.js
+++ b/devtools/client/netmonitor/src/components/Toolbar.js
@@ -51,17 +51,17 @@ const ENABLE_PERSISTENT_LOGS_LABEL =
   L10N.getStr("netmonitor.toolbar.enablePersistentLogs.label");
 const DISABLE_CACHE_TOOLTIP = L10N.getStr("netmonitor.toolbar.disableCache.tooltip");
 const DISABLE_CACHE_LABEL = L10N.getStr("netmonitor.toolbar.disableCache.label");
 const NO_THROTTLING_LABEL = new LocalizationHelper(
   "devtools/client/locales/network-throttling.properties"
   ).getStr("responsive.noThrottling");
 
 // Menu
-loader.lazyRequireGetter(this, "showMenu", "devtools/client/netmonitor/src/utils/menu", true);
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
 loader.lazyRequireGetter(this, "HarMenuUtils", "devtools/client/netmonitor/src/har/har-menu-utils", true);
 
 // Throttling
 const Types = require("devtools/client/shared/components/throttling/types");
 const throttlingProfiles = require("devtools/client/shared/components/throttling/profiles");
 const { changeNetworkThrottling } = require("devtools/client/shared/components/throttling/actions");
 
 /**
--- a/devtools/client/netmonitor/src/utils/moz.build
+++ b/devtools/client/netmonitor/src/utils/moz.build
@@ -10,15 +10,14 @@ DIRS += [
 DevToolsModules(
     'filter-autocomplete-provider.js',
     'filter-predicates.js',
     'filter-text-utils.js',
     'format-utils.js',
     'headers-provider.js',
     'l10n.js',
     'mdn-utils.js',
-    'menu.js',
     'open-request-in-tab.js',
     'prefs.js',
     'request-utils.js',
     'sort-predicates.js',
     'sort-utils.js'
 )
--- a/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
@@ -12,17 +12,17 @@ const {
   getUrlQuery,
   getUrlBaseName,
   parseQueryString,
 } = require("../utils/request-utils");
 
 loader.lazyRequireGetter(this, "Curl", "devtools/client/shared/curl", true);
 loader.lazyRequireGetter(this, "saveAs", "devtools/client/shared/file-saver", true);
 loader.lazyRequireGetter(this, "copyString", "devtools/shared/platform/clipboard", true);
-loader.lazyRequireGetter(this, "showMenu", "devtools/client/netmonitor/src/utils/menu", true);
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
 loader.lazyRequireGetter(this, "openRequestInTab", "devtools/client/netmonitor/src/utils/firefox/open-request-in-tab", true);
 loader.lazyRequireGetter(this, "HarMenuUtils", "devtools/client/netmonitor/src/har/har-menu-utils", true);
 
 class RequestListContextMenu {
   constructor(props) {
     this.props = props;
   }
 
--- a/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
+const { showMenu } = require("devtools/client/shared/components/menu/utils");
 const { HEADERS } = require("../constants");
 const { L10N } = require("../utils/l10n");
 
 const stringMap = HEADERS
   .filter((header) => header.hasOwnProperty("label"))
   .reduce((acc, { name, label }) => Object.assign(acc, { [name]: label }), {});
 
 const subMenuMap = HEADERS
--- a/devtools/client/netmonitor/webpack.config.js
+++ b/devtools/client/netmonitor/webpack.config.js
@@ -68,17 +68,18 @@ const webpackConfig = {
       "node_modules",
     ],
     alias: {
       "Services": "devtools-modules/src/Services",
       "react": path.join(__dirname, "node_modules/react"),
 
       "devtools/client/framework/devtools": path.join(__dirname, "../../client/shared/webpack/shims/framework-devtools-shim"),
       "devtools/client/framework/menu": "devtools-modules/src/menu",
-      "devtools/client/netmonitor/src/utils/menu": "devtools-contextmenu",
+
+      "devtools/client/shared/components/menu/utils": "devtools-contextmenu",
 
       "devtools/client/shared/vendor/react": "react",
       "devtools/client/shared/vendor/react-dom": "react-dom",
       "devtools/client/shared/vendor/react-redux": "react-redux",
       "devtools/client/shared/vendor/redux": "redux",
       "devtools/client/shared/vendor/reselect": "reselect",
       "devtools/client/shared/vendor/jszip": "jszip",
 
--- a/devtools/client/responsive.html/actions/screenshot.js
+++ b/devtools/client/responsive.html/actions/screenshot.js
@@ -7,17 +7,17 @@
 "use strict";
 
 const {
   TAKE_SCREENSHOT_START,
   TAKE_SCREENSHOT_END,
 } = require("./index");
 
 const { getFormatStr } = require("../utils/l10n");
-const { getToplevelWindow } = require("../utils/window");
+const { getTopLevelWindow } = require("../utils/window");
 const e10s = require("../utils/e10s");
 const Services = require("Services");
 
 const CAMERA_AUDIO_URL = "resource://devtools/client/themes/audio/shutter.wav";
 
 const animationFrame = () => new Promise(resolve => {
   window.requestAnimationFrame(resolve);
 });
@@ -35,17 +35,17 @@ function getFileName() {
 
 function createScreenshotFor(node) {
   const mm = node.frameLoader.messageManager;
 
   return e10s.request(mm, "RequestScreenshot");
 }
 
 function saveToFile(data, filename) {
-  const chromeWindow = getToplevelWindow(window);
+  const chromeWindow = getTopLevelWindow(window);
   const chromeDocument = chromeWindow.document;
 
   // append .png extension to filename if it doesn't exist
   filename = filename.replace(/\.png$|$/i, ".png");
 
   chromeWindow.saveURL(data, filename, null,
                         true, true,
                         chromeDocument.documentURIObject, chromeDocument);
--- a/devtools/client/responsive.html/components/Browser.js
+++ b/devtools/client/responsive.html/components/Browser.js
@@ -9,17 +9,17 @@
 const Services = require("Services");
 const flags = require("devtools/shared/flags");
 const { PureComponent } = 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 e10s = require("../utils/e10s");
 const message = require("../utils/message");
-const { getToplevelWindow } = require("../utils/window");
+const { getTopLevelWindow } = require("../utils/window");
 
 const FRAME_SCRIPT = "resource://devtools/client/responsive.html/browser/content.js";
 
 class Browser extends PureComponent {
   /**
    * This component is not allowed to depend directly on frequently changing data (width,
    * height). Any changes in props would cause the <iframe> to be removed and added again,
    * throwing away the current state of the page.
@@ -109,17 +109,17 @@ class Browser extends PureComponent {
     // since it still needs to do async work before the content is actually
     // resized to match.
     e10s.on(mm, "OnContentResize", onContentResize);
 
     const ready = e10s.once(mm, "ChildScriptReady");
     mm.loadFrameScript(FRAME_SCRIPT, true);
     await ready;
 
-    const browserWindow = getToplevelWindow(window);
+    const browserWindow = getTopLevelWindow(window);
     const requiresFloatingScrollbars =
       !browserWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
 
     await e10s.request(mm, "Start", {
       requiresFloatingScrollbars,
       // Tests expect events on resize to wait for various size changes
       notifyOnResize: flags.testing,
     });
rename from devtools/client/responsive.html/components/ReloadConditions.js
rename to devtools/client/responsive.html/components/SettingsMenu.js
--- a/devtools/client/responsive.html/components/ReloadConditions.js
+++ b/devtools/client/responsive.html/components/SettingsMenu.js
@@ -1,50 +1,73 @@
 /* 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 { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
+const { PureComponent } = 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 ToggleMenu = createFactory(require("./ToggleMenu"));
-
 const { getStr } = require("../utils/l10n");
 const Types = require("../types");
 
-class ReloadConditions extends PureComponent {
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
+
+class SettingsMenu extends PureComponent {
   static get propTypes() {
     return {
       reloadConditions: PropTypes.shape(Types.reloadConditions).isRequired,
       onChangeReloadCondition: PropTypes.func.isRequired,
     };
   }
 
-  render() {
+  constructor(props) {
+    super(props);
+    this.onToggleSettingMenu = this.onToggleSettingMenu.bind(this);
+  }
+
+  onToggleSettingMenu(event) {
     const {
       reloadConditions,
       onChangeReloadCondition,
     } = this.props;
 
-    return ToggleMenu({
-      id: "reload-conditions-menu",
-      items: [
-        {
-          id: "touchSimulation",
-          label: getStr("responsive.reloadConditions.touchSimulation"),
-          checked: reloadConditions.touchSimulation,
+    const menuItems = [
+      {
+        id: "touchSimulation",
+        checked: reloadConditions.touchSimulation,
+        label: getStr("responsive.reloadConditions.touchSimulation"),
+        type: "checkbox",
+        click: () => {
+          onChangeReloadCondition("touchSimulation", !reloadConditions.touchSimulation);
+        },
+      },
+      {
+        id: "userAgent",
+        checked: reloadConditions.userAgent,
+        label: getStr("responsive.reloadConditions.userAgent"),
+        type: "checkbox",
+        click: () => {
+          onChangeReloadCondition("userAgent", !reloadConditions.userAgent);
         },
-        {
-          id: "userAgent",
-          label: getStr("responsive.reloadConditions.userAgent"),
-          checked: reloadConditions.userAgent,
-        },
-      ],
-      label: getStr("responsive.reloadConditions.label"),
-      title: getStr("responsive.reloadConditions.title"),
-      onChange: onChangeReloadCondition,
+      },
+    ];
+
+    showMenu(menuItems, {
+      button: event.target,
+      useTopLevelWindow: true,
     });
   }
+
+  render() {
+    return (
+      dom.button({
+        id: "settings-button",
+        className: "devtools-button",
+        onClick: this.onToggleSettingMenu,
+      })
+    );
+  }
 }
 
-module.exports = ReloadConditions;
+module.exports = SettingsMenu;
deleted file mode 100644
--- a/devtools/client/responsive.html/components/ToggleMenu.js
+++ /dev/null
@@ -1,130 +0,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/. */
-
-"use strict";
-
-const { PureComponent } = 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 MenuItem = {
-  id: PropTypes.string.isRequired,
-  label: PropTypes.string.isRequired,
-  checked: PropTypes.bool,
-};
-
-class ToggleMenu extends PureComponent {
-  static get propTypes() {
-    return {
-      id: PropTypes.string,
-      items: PropTypes.arrayOf(PropTypes.shape(MenuItem)).isRequired,
-      label: PropTypes.string,
-      title: PropTypes.string,
-      onChange: PropTypes.func.isRequired,
-    };
-  }
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      isOpen: false,
-    };
-
-    this.onItemChange = this.onItemChange.bind(this);
-    this.onToggleOpen = this.onToggleOpen.bind(this);
-  }
-
-  onItemChange({ target }) {
-    const {
-      onChange,
-    } = this.props;
-
-    // Close menu after changing an item
-    this.setState({
-      isOpen: false,
-    });
-
-    const id = target.name;
-    onChange(id, target.checked);
-  }
-
-  onToggleOpen() {
-    const {
-      isOpen,
-    } = this.state;
-
-    this.setState({
-      isOpen: !isOpen,
-    });
-  }
-
-  render() {
-    const {
-      id: menuID,
-      items,
-      label: toggleLabel,
-      title,
-    } = this.props;
-
-    const {
-      isOpen,
-    } = this.state;
-
-    const {
-      onItemChange,
-      onToggleOpen,
-    } = this;
-
-    const menuItems = items.map(({ id, label, checked }) => {
-      const inputID = `devtools-menu-item-${id}`;
-
-      return dom.div(
-        {
-          className: "devtools-menu-item",
-          key: id,
-        },
-        dom.input({
-          type: "checkbox",
-          id: inputID,
-          name: id,
-          checked,
-          onChange: onItemChange,
-        }),
-        dom.label({
-          htmlFor: inputID,
-        }, label)
-      );
-    });
-
-    let menuClass = "devtools-menu";
-    if (isOpen) {
-      menuClass += " opened";
-    }
-    const menu = dom.div(
-      {
-        className: menuClass,
-      },
-      menuItems
-    );
-
-    let buttonClass = "devtools-toggle-menu";
-    buttonClass += " toolbar-dropdown toolbar-button devtools-button";
-    if (isOpen || items.some(({ checked }) => checked)) {
-      buttonClass += " selected";
-    }
-    return dom.div(
-      {
-        id: menuID,
-        className: buttonClass,
-        title,
-        onClick: onToggleOpen,
-      },
-      toggleLabel,
-      menu
-    );
-  }
-}
-
-module.exports = ToggleMenu;
--- a/devtools/client/responsive.html/components/Toolbar.js
+++ b/devtools/client/responsive.html/components/Toolbar.js
@@ -6,17 +6,17 @@
 
 const { PureComponent, 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 DevicePixelRatioSelector = createFactory(require("./DevicePixelRatioSelector"));
 const DeviceSelector = createFactory(require("./DeviceSelector"));
 const NetworkThrottlingSelector = createFactory(require("devtools/client/shared/components/throttling/NetworkThrottlingSelector"));
-const ReloadConditions = createFactory(require("./ReloadConditions"));
+const SettingsMenu = createFactory(require("./SettingsMenu"));
 const ViewportDimension = createFactory(require("./ViewportDimension"));
 
 const { getStr } = require("../utils/l10n");
 const Types = require("../types");
 
 class Toolbar extends PureComponent {
   static get propTypes() {
     return {
@@ -121,17 +121,17 @@ class Toolbar extends PureComponent {
         { id: "toolbar-end-controls" },
         dom.button({
           id: "screenshot-button",
           className: "toolbar-button devtools-button",
           title: getStr("responsive.screenshot"),
           onClick: onScreenshot,
           disabled: screenshot.isCapturing,
         }),
-        ReloadConditions({
+        SettingsMenu({
           reloadConditions,
           onChangeReloadCondition,
         }),
         dom.div({ className: "devtools-separator" }),
         dom.button({
           id: "exit-button",
           className: "toolbar-button devtools-button",
           title: getStr("responsive.exit"),
--- a/devtools/client/responsive.html/components/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -6,15 +6,14 @@
 
 DevToolsModules(
     'App.js',
     'Browser.js',
     'DeviceAdder.js',
     'DeviceModal.js',
     'DevicePixelRatioSelector.js',
     'DeviceSelector.js',
-    'ReloadConditions.js',
     'ResizableViewport.js',
-    'ToggleMenu.js',
+    'SettingsMenu.js',
     'Toolbar.js',
     'ViewportDimension.js',
     'Viewports.js',
 )
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -128,47 +128,16 @@ select > option:hover {
 select > option.divider {
   border-top: 1px solid var(--theme-splitter-color);
   height: 0px;
   padding: 0;
   font-size: 0px;
 }
 
 /**
- * Toggle Menu
- */
-
-.devtools-toggle-menu {
-  position: relative;
-}
-
-.devtools-toggle-menu .devtools-menu {
-  display: none;
-  flex-direction: column;
-  align-items: start;
-  position: absolute;
-  right: 0;
-  top: 100%;
-  z-index: 1;
-  padding: 5px;
-  border-radius: 2px;
-  background-color: var(--theme-toolbar-background);
-  box-shadow: var(--rdm-box-shadow);
-}
-
-.devtools-toggle-menu .devtools-menu.opened {
-  display: flex;
-}
-
-.devtools-toggle-menu .devtools-menu-item {
-  display: flex;
-  align-items: center;
-}
-
-/**
  * Common background for dropdowns like select and toggle menu
  */
 
 .toolbar-dropdown,
 .toolbar-dropdown.devtools-button,
 .toolbar-dropdown.devtools-button:hover:not(:empty):not(:disabled):not(.checked) {
   background-color: var(--theme-toolbar-background);
   background-image: var(--select-arrow-image);
@@ -225,16 +194,20 @@ select > option.divider {
 #touch-simulation-button::before {
   background-image: url("./images/touch-events.svg");
 }
 
 #screenshot-button::before {
   background-image: url("./images/screenshot.svg");
 }
 
+#settings-button::before {
+  background-image: url("chrome://devtools/skin/images/settings.svg");
+}
+
 #exit-button::before {
   background-image: url("chrome://devtools/skin/images/close.svg");
 }
 
 #screenshot-button:disabled {
   filter: var(--theme-icon-checked-filter);
   opacity: 1 !important;
 }
--- a/devtools/client/responsive.html/utils/window.js
+++ b/devtools/client/responsive.html/utils/window.js
@@ -4,20 +4,20 @@
 
 "use strict";
 
 const Services = require("Services");
 
 /**
  * Returns the `nsIDOMWindow` toplevel window for any child/inner window
  */
-function getToplevelWindow(window) {
+function getTopLevelWindow(window) {
   return window.docShell.rootTreeItem.domWindow;
 }
-exports.getToplevelWindow = getToplevelWindow;
+exports.getTopLevelWindow = getTopLevelWindow;
 
 function getDOMWindowUtils(window) {
   return window.windowUtils;
 }
 exports.getDOMWindowUtils = getDOMWindowUtils;
 
 /**
  * Check if the given browser window has finished the startup.
--- a/devtools/client/shared/components/menu/moz.build
+++ b/devtools/client/shared/components/menu/moz.build
@@ -1,11 +1,12 @@
 # -*- 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(
-  'MenuButton.js',
-  'MenuItem.js',
-  'MenuList.js',
+    'MenuButton.js',
+    'MenuItem.js',
+    'MenuList.js',
+    'utils.js',
 )
rename from devtools/client/netmonitor/src/utils/menu.js
rename to devtools/client/shared/components/menu/utils.js
--- a/devtools/client/netmonitor/src/utils/menu.js
+++ b/devtools/client/shared/components/menu/utils.js
@@ -2,24 +2,33 @@
  * 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 Menu = require("devtools/client/framework/menu");
 const MenuItem = require("devtools/client/framework/menu-item");
 
+loader.lazyRequireGetter(this, "getTopLevelWindow", "devtools/client/responsive.html/utils/window", true);
+
 /**
  * Helper function for opening context menu.
  *
- * @param {Array} items List of menu items.
+ * @param {Array} items
+ *        List of menu items.
  * @param {Object} options:
- * @property {Number} screenX coordinate of the menu on the screen
- * @property {Number} screenY coordinate of the menu on the screen
- * @property {Object} button parent used to open the menu
+ * @property {Element} button
+ *           Button element used to open the menu.
+ * @property {Number} screenX
+ *           Screen x coordinate of the menu on the screen.
+ * @property {Number} screenY
+ *           Screen y coordinate of the menu on the screen.
+ * @property {Boolean} useTopLevelWindow
+ *           Whether or not the top level window needs to be fetched. This option is used
+ *           by RDM.
  */
 function showMenu(items, options) {
   if (items.length === 0) {
     return;
   }
 
   // Build the menu object from provided menu items.
   const menu = new Menu();
@@ -50,14 +59,21 @@ function showMenu(items, options) {
   if (options.button) {
     const button = options.button;
     const rect = button.getBoundingClientRect();
     const defaultView = button.ownerDocument.defaultView;
     screenX = rect.left + defaultView.mozInnerScreenX;
     screenY = rect.bottom + defaultView.mozInnerScreenY;
   }
 
-  menu.popup(screenX, screenY, { doc: window.parent.document });
+  let doc;
+  if (options.useTopLevelWindow) {
+    doc = getTopLevelWindow(window).document;
+  } else {
+    doc = window.parent.document;
+  }
+
+  menu.popup(screenX, screenY, { doc });
 }
 
 module.exports = {
   showMenu,
 };
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/settings.svg
@@ -0,0 +1,6 @@
+<!-- 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 width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+  <path fill="context-fill" fill-rule="evenodd" clip-rule="evenodd" d="M12 22a1 1 0 0 1-1-1v-3.083a5.966 5.966 0 0 1-2.477-1.026l-2.18 2.18a1 1 0 0 1-1.414-1.414l2.18-2.18A5.967 5.967 0 0 1 6.083 13H3a1 1 0 1 1 0-2h3.083a5.968 5.968 0 0 1 1.026-2.477l-2.18-2.18A1 1 0 0 1 6.343 4.93l2.18 2.18A5.968 5.968 0 0 1 11 6.083V3a1 1 0 1 1 2 0v3.083a5.967 5.967 0 0 1 2.476 1.026l2.18-2.18a1 1 0 1 1 1.415 1.414l-2.18 2.18A5.966 5.966 0 0 1 17.917 11H21a1 1 0 1 1 0 2h-3.083a5.966 5.966 0 0 1-1.026 2.476l2.18 2.18a1 1 0 0 1-1.414 1.415l-2.18-2.18A5.966 5.966 0 0 1 13 17.917V21a1 1 0 0 1-1 1zM8 12a4 4 0 1 1 8 0 4 4 0 0 1-8 0z"></path>
+</svg>