Bug 1428816 - Add RDM UI to control whether we reload. r=ochameau draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Thu, 08 Feb 2018 15:20:17 -0600
changeset 758577 6249f7f609e2d79222c74735ce37d0bbcc7ba14b
parent 758576 23fb2bb002b4464ee01244bf9934236bcc404f3c
child 758578 dba08ff6fedada12d4ed77d2fc6759961dfa32d2
push id100108
push userbmo:jryans@gmail.com
push dateThu, 22 Feb 2018 18:02:07 +0000
reviewersochameau
bugs1428816
milestone60.0a1
Bug 1428816 - Add RDM UI to control whether we reload. r=ochameau This adds a menu to the RDM global toolbar to allow the user to control whether the page reloads in response to various state changes. This also changes the default behavior to _not_ reload, so that we avoid losing changes that might have been made in DevTools. MozReview-Commit-ID: 86h5cB5dify
devtools/client/locales/en-US/responsive.properties
devtools/client/preferences/devtools.js
devtools/client/responsive.html/actions/index.js
devtools/client/responsive.html/actions/moz.build
devtools/client/responsive.html/actions/reload-conditions.js
devtools/client/responsive.html/app.js
devtools/client/responsive.html/components/DevicePixelRatioSelector.js
devtools/client/responsive.html/components/DeviceSelector.js
devtools/client/responsive.html/components/GlobalToolbar.js
devtools/client/responsive.html/components/NetworkThrottlingSelector.js
devtools/client/responsive.html/components/ReloadConditions.js
devtools/client/responsive.html/components/ToggleMenu.js
devtools/client/responsive.html/components/moz.build
devtools/client/responsive.html/index.css
devtools/client/responsive.html/index.js
devtools/client/responsive.html/manager.js
devtools/client/responsive.html/reducers.js
devtools/client/responsive.html/reducers/moz.build
devtools/client/responsive.html/reducers/reload-conditions.js
devtools/client/responsive.html/test/browser/browser_device_change.js
devtools/client/responsive.html/test/browser/browser_device_pixel_ratio_change.js
devtools/client/responsive.html/test/browser/browser_touch_device.js
devtools/client/responsive.html/test/browser/browser_touch_simulation.js
devtools/client/responsive.html/test/browser/head.js
devtools/client/responsive.html/types.js
--- a/devtools/client/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -133,8 +133,24 @@ responsive.deviceAdderSave=Save
 # %2$S is the height of the device.  %3$S is the device pixel ratio value of the
 # 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
\ No newline at end of file
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -352,8 +352,13 @@ pref("devtools.editor.autoclosebrackets"
 pref("devtools.editor.detectindentation", true);
 pref("devtools.editor.enableCodeFolding", true);
 pref("devtools.editor.autocomplete", true);
 
 // Pref to store the browser version at the time of a telemetry ping for an
 // opened developer tool. This allows us to ping telemetry just once per browser
 // version for each user.
 pref("devtools.telemetry.tools.opened.version", "{}");
+
+// Whether to reload when touch simulation is toggled
+pref("devtools.responsive.reloadConditions.touchSimulation", false);
+// Whether to reload when user agent is changed
+pref("devtools.responsive.reloadConditions.userAgent", false);
--- a/devtools/client/responsive.html/actions/index.js
+++ b/devtools/client/responsive.html/actions/index.js
@@ -36,28 +36,34 @@ createEnum([
   // Change the network throttling profile.
   "CHANGE_NETWORK_THROTTLING",
 
   // The pixel ratio of the viewport has changed. This may be triggered by the user
   // when changing the device displayed in the viewport, or when a pixel ratio is
   // selected from the device pixel ratio dropdown.
   "CHANGE_PIXEL_RATIO",
 
+  // Change one of the reload conditions.
+  "CHANGE_RELOAD_CONDITION",
+
   // Change the touch simulation state.
   "CHANGE_TOUCH_SIMULATION",
 
-  // Indicates that the device list is being loaded
+  // Indicates that the device list is being loaded.
   "LOAD_DEVICE_LIST_START",
 
-  // Indicates that the device list loading action threw an error
+  // Indicates that the device list loading action threw an error.
   "LOAD_DEVICE_LIST_ERROR",
 
-  // Indicates that the device list has been loaded successfully
+  // Indicates that the device list has been loaded successfully.
   "LOAD_DEVICE_LIST_END",
 
+  // Indicates that the reload conditions have been loaded successfully.
+  "LOAD_RELOAD_CONDITIONS_END",
+
   // Remove a device.
   "REMOVE_DEVICE",
 
   // Remove the viewport's device assocation.
   "REMOVE_DEVICE_ASSOCIATION",
 
   // Resize the viewport.
   "RESIZE_VIEWPORT",
--- a/devtools/client/responsive.html/actions/moz.build
+++ b/devtools/client/responsive.html/actions/moz.build
@@ -5,12 +5,13 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'devices.js',
     'display-pixel-ratio.js',
     'index.js',
     'location.js',
     'network-throttling.js',
+    'reload-conditions.js',
     'screenshot.js',
     'touch-simulation.js',
     'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/actions/reload-conditions.js
@@ -0,0 +1,52 @@
+/* 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 {
+  CHANGE_RELOAD_CONDITION,
+  LOAD_RELOAD_CONDITIONS_END,
+} = require("./index");
+
+const Types = require("../types");
+const Services = require("Services");
+
+const PREF_PREFIX = "devtools.responsive.reloadConditions.";
+
+module.exports = {
+
+  changeReloadCondition(id, value) {
+    return dispatch => {
+      let pref = PREF_PREFIX + id;
+      Services.prefs.setBoolPref(pref, value);
+      dispatch({
+        type: CHANGE_RELOAD_CONDITION,
+        id,
+        value,
+      });
+    };
+  },
+
+  loadReloadConditions() {
+    return dispatch => {
+      // Loop over the conditions and load their values from prefs.
+      for (let id in Types.reloadConditions) {
+        // Skip over the loading state of the list.
+        if (id == "state") {
+          return;
+        }
+        let pref = PREF_PREFIX + id;
+        let value = Services.prefs.getBoolPref(pref, false);
+        dispatch({
+          type: CHANGE_RELOAD_CONDITION,
+          id,
+          value,
+        });
+      }
+
+      dispatch({ type: LOAD_RELOAD_CONDITIONS_END });
+    };
+  },
+
+};
--- a/devtools/client/responsive.html/app.js
+++ b/devtools/client/responsive.html/app.js
@@ -14,16 +14,17 @@ const { connect } = require("devtools/cl
 const {
   addCustomDevice,
   removeCustomDevice,
   updateDeviceDisplayed,
   updateDeviceModal,
   updatePreferredDevices,
 } = require("./actions/devices");
 const { changeNetworkThrottling } = require("./actions/network-throttling");
+const { changeReloadCondition } = require("./actions/reload-conditions");
 const { takeScreenshot } = require("./actions/screenshot");
 const { changeTouchSimulation } = require("./actions/touch-simulation");
 const {
   changeDevice,
   changePixelRatio,
   removeDeviceAssociation,
   resizeViewport,
   rotateViewport,
@@ -35,29 +36,31 @@ const Types = require("./types");
 
 class App extends Component {
   static get propTypes() {
     return {
       devices: PropTypes.shape(Types.devices).isRequired,
       dispatch: PropTypes.func.isRequired,
       displayPixelRatio: Types.pixelRatio.value.isRequired,
       networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
+      reloadConditions: PropTypes.shape(Types.reloadConditions).isRequired,
       screenshot: PropTypes.shape(Types.screenshot).isRequired,
       touchSimulation: PropTypes.shape(Types.touchSimulation).isRequired,
       viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
     };
   }
 
   constructor(props) {
     super(props);
     this.onAddCustomDevice = this.onAddCustomDevice.bind(this);
     this.onBrowserMounted = this.onBrowserMounted.bind(this);
     this.onChangeDevice = this.onChangeDevice.bind(this);
     this.onChangeNetworkThrottling = this.onChangeNetworkThrottling.bind(this);
     this.onChangePixelRatio = this.onChangePixelRatio.bind(this);
+    this.onChangeReloadCondition = this.onChangeReloadCondition.bind(this);
     this.onChangeTouchSimulation = this.onChangeTouchSimulation.bind(this);
     this.onContentResize = this.onContentResize.bind(this);
     this.onDeviceListUpdate = this.onDeviceListUpdate.bind(this);
     this.onExit = this.onExit.bind(this);
     this.onRemoveCustomDevice = this.onRemoveCustomDevice.bind(this);
     this.onRemoveDeviceAssociation = this.onRemoveDeviceAssociation.bind(this);
     this.onResizeViewport = this.onResizeViewport.bind(this);
     this.onRotateViewport = this.onRotateViewport.bind(this);
@@ -99,16 +102,20 @@ class App extends Component {
   onChangePixelRatio(pixelRatio) {
     window.postMessage({
       type: "change-pixel-ratio",
       pixelRatio,
     }, "*");
     this.props.dispatch(changePixelRatio(0, pixelRatio));
   }
 
+  onChangeReloadCondition(id, value) {
+    this.props.dispatch(changeReloadCondition(id, value));
+  }
+
   onChangeTouchSimulation(enabled) {
     window.postMessage({
       type: "change-touch-simulation",
       enabled,
     }, "*");
     this.props.dispatch(changeTouchSimulation(enabled));
   }
 
@@ -160,27 +167,29 @@ class App extends Component {
     this.props.dispatch(updateDeviceModal(isOpen, modalOpenedFromViewport));
   }
 
   render() {
     let {
       devices,
       displayPixelRatio,
       networkThrottling,
+      reloadConditions,
       screenshot,
       touchSimulation,
       viewports,
     } = this.props;
 
     let {
       onAddCustomDevice,
       onBrowserMounted,
       onChangeDevice,
       onChangeNetworkThrottling,
       onChangePixelRatio,
+      onChangeReloadCondition,
       onChangeTouchSimulation,
       onContentResize,
       onDeviceListUpdate,
       onExit,
       onRemoveCustomDevice,
       onRemoveDeviceAssociation,
       onResizeViewport,
       onRotateViewport,
@@ -205,22 +214,24 @@ class App extends Component {
     return dom.div(
       {
         id: "app",
       },
       GlobalToolbar({
         devices,
         displayPixelRatio,
         networkThrottling,
+        reloadConditions,
         screenshot,
         selectedDevice,
         selectedPixelRatio,
         touchSimulation,
         onChangeNetworkThrottling,
         onChangePixelRatio,
+        onChangeReloadCondition,
         onChangeTouchSimulation,
         onExit,
         onScreenshot,
       }),
       Viewports({
         devices,
         screenshot,
         viewports,
--- a/devtools/client/responsive.html/components/DevicePixelRatioSelector.js
+++ b/devtools/client/responsive.html/components/DevicePixelRatioSelector.js
@@ -88,17 +88,17 @@ class DevicePixelRatioSelector extends P
     }
 
     if (!PIXEL_RATIO_PRESET.includes(displayPixelRatio)) {
       hiddenOptions.push(displayPixelRatio);
     }
 
     let state = devices.listState;
     let isDisabled = (state !== Types.loadableState.LOADED) || (selectedDevice !== "");
-    let selectorClass = "";
+    let selectorClass = "toolbar-dropdown";
     let title;
 
     if (isDisabled) {
       selectorClass += " disabled";
       title = getFormatStr("responsive.devicePixelRatio.auto", selectedDevice);
     } else {
       title = getStr("responsive.changeDevicePixelRatio");
 
--- a/devtools/client/responsive.html/components/DeviceSelector.js
+++ b/devtools/client/responsive.html/components/DeviceSelector.js
@@ -67,17 +67,17 @@ class DeviceSelector extends PureCompone
         }
       }
     }
 
     options.sort(function (a, b) {
       return a.name.localeCompare(b.name);
     });
 
-    let selectClass = "viewport-device-selector";
+    let selectClass = "viewport-device-selector toolbar-dropdown";
     if (selectedDevice) {
       selectClass += " selected";
     }
 
     let state = devices.listState;
     let listContent;
 
     if (state == Types.loadableState.LOADED) {
--- a/devtools/client/responsive.html/components/GlobalToolbar.js
+++ b/devtools/client/responsive.html/components/GlobalToolbar.js
@@ -7,46 +7,51 @@
 const { PureComponent, createFactory } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 
 const { getStr } = require("../utils/l10n");
 const Types = require("../types");
 const DevicePixelRatioSelector = createFactory(require("./DevicePixelRatioSelector"));
 const NetworkThrottlingSelector = createFactory(require("./NetworkThrottlingSelector"));
+const ReloadConditions = createFactory(require("./ReloadConditions"));
 
 class GlobalToolbar extends PureComponent {
   static get propTypes() {
     return {
       devices: PropTypes.shape(Types.devices).isRequired,
       displayPixelRatio: Types.pixelRatio.value.isRequired,
       networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
+      reloadConditions: PropTypes.shape(Types.reloadConditions).isRequired,
       screenshot: PropTypes.shape(Types.screenshot).isRequired,
       selectedDevice: PropTypes.string.isRequired,
       selectedPixelRatio: PropTypes.shape(Types.pixelRatio).isRequired,
       touchSimulation: PropTypes.shape(Types.touchSimulation).isRequired,
       onChangeNetworkThrottling: PropTypes.func.isRequired,
       onChangePixelRatio: PropTypes.func.isRequired,
+      onChangeReloadCondition: PropTypes.func.isRequired,
       onChangeTouchSimulation: PropTypes.func.isRequired,
       onExit: PropTypes.func.isRequired,
       onScreenshot: PropTypes.func.isRequired,
     };
   }
 
   render() {
     let {
       devices,
       displayPixelRatio,
       networkThrottling,
+      reloadConditions,
       screenshot,
       selectedDevice,
       selectedPixelRatio,
       touchSimulation,
       onChangeNetworkThrottling,
       onChangePixelRatio,
+      onChangeReloadCondition,
       onChangeTouchSimulation,
       onExit,
       onScreenshot,
     } = this.props;
 
     let touchButtonClass = "toolbar-button devtools-button";
     if (touchSimulation.enabled) {
       touchButtonClass += " checked";
@@ -69,16 +74,20 @@ class GlobalToolbar extends PureComponen
       }),
       DevicePixelRatioSelector({
         devices,
         displayPixelRatio,
         selectedDevice,
         selectedPixelRatio,
         onChangePixelRatio,
       }),
+      ReloadConditions({
+        reloadConditions,
+        onChangeReloadCondition,
+      }),
       dom.button({
         id: "global-touch-simulation-button",
         className: touchButtonClass,
         title: (touchSimulation.enabled ?
           getStr("responsive.disableTouch") : getStr("responsive.enableTouch")),
         onClick: () => onChangeTouchSimulation(!touchSimulation.enabled),
       }),
       dom.button({
--- a/devtools/client/responsive.html/components/NetworkThrottlingSelector.js
+++ b/devtools/client/responsive.html/components/NetworkThrottlingSelector.js
@@ -43,17 +43,17 @@ class NetworkThrottlingSelector extends 
     }
   }
 
   render() {
     let {
       networkThrottling,
     } = this.props;
 
-    let selectClass = "";
+    let selectClass = "toolbar-dropdown";
     let selectedProfile;
     if (networkThrottling.enabled) {
       selectClass += " selected";
       selectedProfile = networkThrottling.profile;
     } else {
       selectedProfile = getStr("responsive.noThrottling");
     }
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/components/ReloadConditions.js
@@ -0,0 +1,49 @@
+/* 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, createFactory } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+const Types = require("../types");
+const { getStr } = require("../utils/l10n");
+const ToggleMenu = createFactory(require("./ToggleMenu"));
+
+class ReloadConditions extends PureComponent {
+  static get propTypes() {
+    return {
+      reloadConditions: PropTypes.shape(Types.reloadConditions).isRequired,
+      onChangeReloadCondition: PropTypes.func.isRequired,
+    };
+  }
+
+  render() {
+    let {
+      reloadConditions,
+      onChangeReloadCondition,
+    } = this.props;
+
+    return ToggleMenu({
+      id: "global-reload-conditions-menu",
+      items: [
+        {
+          id: "touchSimulation",
+          label: getStr("responsive.reloadConditions.touchSimulation"),
+          checked: reloadConditions.touchSimulation,
+        },
+        {
+          id: "userAgent",
+          label: getStr("responsive.reloadConditions.userAgent"),
+          checked: reloadConditions.userAgent,
+        },
+      ],
+      label: getStr("responsive.reloadConditions.label"),
+      title: getStr("responsive.reloadConditions.title"),
+      onChange: onChangeReloadCondition,
+    });
+  }
+}
+
+module.exports = ReloadConditions;
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/components/ToggleMenu.js
@@ -0,0 +1,129 @@
+/* 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 dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+let 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 }) {
+    let {
+      onChange,
+    } = this.props;
+
+    // Close menu after changing an item
+    this.setState({
+      isOpen: false,
+    });
+
+    let id = target.name;
+    onChange(id, target.checked);
+  }
+
+  onToggleOpen() {
+    let {
+      isOpen,
+    } = this.state;
+
+    this.setState({
+      isOpen: !isOpen,
+    });
+  }
+
+  render() {
+    let {
+      id: menuID,
+      items,
+      label: toggleLabel,
+      title,
+    } = this.props;
+
+    let {
+      isOpen,
+    } = this.state;
+
+    let {
+      onItemChange,
+      onToggleOpen,
+    } = this;
+
+    let menuItems = items.map(({ id, label, checked }) => {
+      let inputID = `devtools-menu-item-${id}`;
+
+      return dom.div(
+        {
+          className: "devtools-menu-item",
+        },
+        dom.input({
+          type: "checkbox",
+          id: inputID,
+          name: id,
+          checked,
+          onChange: onItemChange,
+        }),
+        dom.label({
+          htmlFor: inputID,
+        }, label)
+      );
+    });
+
+    let menuClass = "devtools-menu";
+    if (isOpen) {
+      menuClass += " opened";
+    }
+    let 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/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -7,14 +7,16 @@
 DevToolsModules(
     'Browser.js',
     'DeviceAdder.js',
     'DeviceModal.js',
     'DevicePixelRatioSelector.js',
     'DeviceSelector.js',
     'GlobalToolbar.js',
     'NetworkThrottlingSelector.js',
+    'ReloadConditions.js',
     'ResizableViewport.js',
+    'ToggleMenu.js',
     'Viewport.js',
     'ViewportDimension.js',
     'Viewports.js',
     'ViewportToolbar.js',
 )
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -62,38 +62,42 @@ body,
   background-color: var(--theme-toolbar-background);
   border: 1px solid var(--theme-splitter-color);
 }
 
 .toolbar-button {
   margin: 0;
   padding: 0;
   border: none;
+  color: var(--viewport-color);
 }
 
 .toolbar-button:empty:hover:not(:disabled),
-.toolbar-button:empty:-moz-any(:hover:active, .checked):not(:disabled) {
+.toolbar-button:empty:-moz-any(:hover:active, .checked):not(:disabled),
+.toolbar-button:not(:empty),
+.toolbar-button:hover:not(:empty):not(:disabled):not(.checked) {
   /* Reset background from .devtools-button */
   background: none;
 }
 
 .toolbar-button:active::before {
   filter: var(--theme-icon-checked-filter);
 }
 
+.toolbar-button.selected {
+  color: var(--viewport-active-color);
+}
+
+.toolbar-button:not(:disabled):hover {
+  color: var(--viewport-hover-color);
+}
+
 select {
   -moz-appearance: none;
-  background-color: var(--theme-toolbar-background);
-  background-image: var(--viewport-selection-arrow);
-  -moz-context-properties: fill;
-  fill: currentColor;
   color: var(--viewport-color);
-  background-position: 100% 50%;
-  background-repeat: no-repeat;
-  background-size: 7px;
   border: none;
   height: 100%;
   padding: 0 8px;
   text-align: center;
   text-overflow: ellipsis;
 }
 
 select.selected {
@@ -125,26 +129,73 @@ 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;
+  left: 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(--viewport-selection-arrow);
+  background-position: 100% 50%;
+  background-repeat: no-repeat;
+  background-size: 7px;
+  -moz-context-properties: fill;
+  fill: currentColor;
+}
+
+/**
  * Global Toolbar
  */
 
 #global-toolbar {
   color: var(--theme-body-color-alt);
   border-radius: 2px;
   box-shadow: var(--rdm-box-shadow);
   margin: 0 0 15px 0;
   padding: 4px 5px;
   display: inline-flex;
+  align-items: center;
   -moz-user-select: none;
 }
 
 #global-toolbar > .title {
   border-right: 1px solid var(--theme-splitter-color);
   padding: 1px 6px 0 2px;
 }
 
@@ -152,16 +203,23 @@ select > option.divider {
   margin-inline-start: 8px;
 }
 
 #global-toolbar > .toolbar-button::before {
   width: 12px;
   height: 12px;
 }
 
+#global-toolbar .toolbar-dropdown {
+  background-position-x: right 5px;
+  border-right: 1px solid var(--theme-splitter-color);
+  padding-right: 15px;
+  /* padding-left: 0; */
+}
+
 #global-touch-simulation-button::before {
   background-image: url("./images/touch-events.svg");
 }
 
 #global-screenshot-button::before {
   background-image: url("./images/screenshot.svg");
 }
 
--- a/devtools/client/responsive.html/index.js
+++ b/devtools/client/responsive.html/index.js
@@ -18,20 +18,21 @@ const { loadAgentSheet } = require("./ut
 const { createFactory, createElement } =
   require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
 const message = require("./utils/message");
 const App = createFactory(require("./app"));
 const Store = require("./store");
-const { changeLocation } = require("./actions/location");
+const { loadDevices } = require("./actions/devices");
 const { changeDisplayPixelRatio } = require("./actions/display-pixel-ratio");
+const { changeLocation } = require("./actions/location");
+const { loadReloadConditions } = require("./actions/reload-conditions");
 const { addViewport, resizeViewport } = require("./actions/viewports");
-const { loadDevices } = require("./actions/devices");
 
 // Exposed for use by tests
 window.require = require;
 
 let bootstrap = {
 
   telemetry: new Telemetry(),
 
@@ -74,17 +75,20 @@ let bootstrap = {
 
 };
 
 // manager.js sends a message to signal init
 message.wait(window, "init").then(() => bootstrap.init());
 
 // manager.js sends a message to signal init is done, which can be used for delayed
 // startup work that shouldn't block initial load
-message.wait(window, "post-init").then(() => bootstrap.dispatch(loadDevices()));
+message.wait(window, "post-init").then(() => {
+  bootstrap.dispatch(loadDevices());
+  bootstrap.dispatch(loadReloadConditions());
+});
 
 window.addEventListener("unload", function () {
   bootstrap.destroy();
 }, {once: true});
 
 // Allows quick testing of actions from the console
 window.dispatch = action => bootstrap.dispatch(action);
 
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -1,16 +1,17 @@
 /* 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 { Ci } = require("chrome");
 const promise = require("promise");
+const Services = require("Services");
 const EventEmitter = require("devtools/shared/old-event-emitter");
 
 const TOOL_URL = "chrome://devtools/content/responsive.html/index.xhtml";
 
 loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/debugger-client", true);
 loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
 loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
 loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
@@ -22,16 +23,18 @@ loader.lazyRequireGetter(this, "startup"
   "devtools/client/responsive.html/utils/window", true);
 loader.lazyRequireGetter(this, "message",
   "devtools/client/responsive.html/utils/message");
 loader.lazyRequireGetter(this, "getStr",
   "devtools/client/responsive.html/utils/l10n", true);
 loader.lazyRequireGetter(this, "EmulationFront",
   "devtools/shared/fronts/emulation", true);
 
+const RELOAD_CONDITION_PREF_PREFIX = "devtools.responsive.reloadConditions.";
+
 function debug(msg) {
   // console.log(`RDM manager: ${msg}`);
 }
 
 /**
  * ResponsiveUIManager is the external API for the browser UI, etc. to use when
  * opening and closing the responsive UI.
  */
@@ -400,20 +403,22 @@ ResponsiveUI.prototype = {
       // Notify the inner browser to stop the frame script
       await message.request(this.toolWindow, "stop-frame-script");
     }
 
     // Ensure the tab is reloaded if required when exiting RDM so that no emulated
     // settings are left in a customized state.
     if (!isTabContentDestroying) {
       let reloadNeeded = false;
-      reloadNeeded |= await this.updateDPPX();
-      reloadNeeded |= await this.updateNetworkThrottling();
-      reloadNeeded |= await this.updateUserAgent();
-      reloadNeeded |= await this.updateTouchSimulation();
+      await this.updateDPPX();
+      await this.updateNetworkThrottling();
+      reloadNeeded |= await this.updateUserAgent() &&
+                      this.reloadOnChange("userAgent");
+      reloadNeeded |= await this.updateTouchSimulation() &&
+                      this.reloadOnChange("touchSimulation");
       if (reloadNeeded) {
         this.getViewportBrowser().reload();
       }
     }
 
     // Destroy local state
     let swap = this.swap;
     this.browserWindow = null;
@@ -445,16 +450,21 @@ ResponsiveUI.prototype = {
     DebuggerServer.init();
     DebuggerServer.registerAllActors();
     this.client = new DebuggerClient(DebuggerServer.connectPipe());
     await this.client.connect();
     let { tab } = await this.client.getTab();
     this.emulationFront = EmulationFront(this.client, tab);
   },
 
+  reloadOnChange(id) {
+    let pref = RELOAD_CONDITION_PREF_PREFIX + id;
+    return Services.prefs.getBoolPref(pref, false);
+  },
+
   handleEvent(event) {
     let { browserWindow, tab } = this;
 
     switch (event.type) {
       case "message":
         this.handleMessage(event);
         break;
       case "BeforeTabRemotenessChange":
@@ -494,20 +504,22 @@ ResponsiveUI.prototype = {
       case "remove-device-association":
         this.onRemoveDeviceAssociation(event);
         break;
     }
   },
 
   async onChangeDevice(event) {
     let { userAgent, pixelRatio, touch } = event.data.device;
-    // Bug 1428799: Should we reload on UA change as well?
-    await this.updateUserAgent(userAgent);
+    let reloadNeeded = false;
     await this.updateDPPX(pixelRatio);
-    let reloadNeeded = await this.updateTouchSimulation(touch);
+    reloadNeeded |= await this.updateUserAgent(userAgent) &&
+                    this.reloadOnChange("userAgent");
+    reloadNeeded |= await this.updateTouchSimulation(touch) &&
+                    this.reloadOnChange("touchSimulation");
     if (reloadNeeded) {
       this.getViewportBrowser().reload();
     }
     // Used by tests
     this.emit("device-changed");
   },
 
   async onChangeNetworkThrottling(event) {
@@ -519,17 +531,18 @@ ResponsiveUI.prototype = {
 
   onChangePixelRatio(event) {
     let { pixelRatio } = event.data;
     this.updateDPPX(pixelRatio);
   },
 
   async onChangeTouchSimulation(event) {
     let { enabled } = event.data;
-    let reloadNeeded = await this.updateTouchSimulation(enabled);
+    let reloadNeeded = await this.updateTouchSimulation(enabled) &&
+                       this.reloadOnChange("touchSimulation");
     if (reloadNeeded) {
       this.getViewportBrowser().reload();
     }
     // Used by tests
     this.emit("touch-simulation-changed");
   },
 
   onContentResize(event) {
@@ -541,20 +554,22 @@ ResponsiveUI.prototype = {
   },
 
   onExit() {
     let { browserWindow, tab } = this;
     ResponsiveUIManager.closeIfNeeded(browserWindow, tab);
   },
 
   async onRemoveDeviceAssociation(event) {
-    // Bug 1428799: Should we reload on UA change as well?
-    await this.updateUserAgent();
+    let reloadNeeded = false;
     await this.updateDPPX();
-    let reloadNeeded = await this.updateTouchSimulation();
+    reloadNeeded |= await this.updateUserAgent() &&
+                    this.reloadOnChange("userAgent");
+    reloadNeeded |= await this.updateTouchSimulation() &&
+                    this.reloadOnChange("touchSimulation");
     if (reloadNeeded) {
       this.getViewportBrowser().reload();
     }
     // Used by tests
     this.emit("device-association-removed");
   },
 
   /**
--- a/devtools/client/responsive.html/reducers.js
+++ b/devtools/client/responsive.html/reducers.js
@@ -3,11 +3,12 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 exports.devices = require("./reducers/devices");
 exports.displayPixelRatio = require("./reducers/display-pixel-ratio");
 exports.location = require("./reducers/location");
 exports.networkThrottling = require("./reducers/network-throttling");
+exports.reloadConditions = require("./reducers/reload-conditions");
 exports.screenshot = require("./reducers/screenshot");
 exports.touchSimulation = require("./reducers/touch-simulation");
 exports.viewports = require("./reducers/viewports");
--- a/devtools/client/responsive.html/reducers/moz.build
+++ b/devtools/client/responsive.html/reducers/moz.build
@@ -4,12 +4,13 @@
 # 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(
     'devices.js',
     'display-pixel-ratio.js',
     'location.js',
     'network-throttling.js',
+    'reload-conditions.js',
     'screenshot.js',
     'touch-simulation.js',
     'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/reload-conditions.js
@@ -0,0 +1,42 @@
+/* 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 {
+  CHANGE_RELOAD_CONDITION,
+  LOAD_RELOAD_CONDITIONS_END,
+} = require("../actions/index");
+
+const Types = require("../types");
+
+const INITIAL_RELOAD_CONDITIONS = {
+  touchSimulation: false,
+  userAgent: false,
+  state: Types.loadableState.INITIALIZED,
+};
+
+let reducers = {
+
+  [CHANGE_RELOAD_CONDITION](conditions, { id, value }) {
+    return Object.assign({}, conditions, {
+      [id]: value,
+    });
+  },
+
+  [LOAD_RELOAD_CONDITIONS_END](conditions) {
+    return Object.assign({}, conditions, {
+      state: Types.loadableState.LOADED,
+    });
+  },
+
+};
+
+module.exports = function (conditions = INITIAL_RELOAD_CONDITIONS, action) {
+  let reducer = reducers[action.type];
+  if (!reducer) {
+    return conditions;
+  }
+  return reducer(conditions, action);
+};
--- a/devtools/client/responsive.html/test/browser/browser_device_change.js
+++ b/devtools/client/responsive.html/test/browser/browser_device_change.js
@@ -26,16 +26,18 @@ const testDevice = {
 };
 
 // Add the new device to the list
 addDeviceForTest(testDevice);
 
 addRDMTask(TEST_URL, async function ({ ui }) {
   let { store } = ui.toolWindow;
 
+  reloadOnUAChange(true);
+
   // Wait until the viewport has been added and the device list has been loaded
   await waitUntilState(store, state => state.viewports.length == 1
     && state.devices.listState == Types.loadableState.LOADED);
 
   // Test defaults
   testViewportDimensions(ui, 320, 480);
   info("Should have default UA at the start of the test");
   await testUserAgent(ui, DEFAULT_UA);
@@ -67,24 +69,28 @@ addRDMTask(TEST_URL, async function ({ u
 
   // Test device with generic properties
   await selectDevice(ui, "Laptop (1366 x 768)");
   await waitForViewportResizeTo(ui, 1366, 768);
   info("Should have default UA when using device without specific UA");
   await testUserAgent(ui, DEFAULT_UA);
   await testDevicePixelRatio(ui, 1);
   await testTouchEventsOverride(ui, false);
+
+  reloadOnUAChange(false);
 });
 
 add_task(async function () {
   const tab = await addTab(TEST_URL);
   const { ui } = await openRDM(tab);
 
   let { store } = ui.toolWindow;
 
+  reloadOnUAChange(true);
+
   // Wait until the viewport has been added and the device list has been loaded
   await waitUntilState(store, state => state.viewports.length == 1
     && state.devices.listState == Types.loadableState.LOADED);
 
   // Select device with custom UA
   let reloaded = waitForViewportLoad(ui);
   await selectDevice(ui, "Fake Phone RDM Test");
   await reloaded;
@@ -97,16 +103,18 @@ add_task(async function () {
   await closeRDM(tab);
   await reloaded;
 
   // Ensure UA is reset to default after closing RDM
   info("Should have default UA after closing RDM");
   await testUserAgentFromBrowser(tab.linkedBrowser, DEFAULT_UA);
 
   await removeTab(tab);
+
+  reloadOnUAChange(false);
 });
 
 function testViewportDimensions(ui, w, h) {
   let viewport = ui.toolWindow.document.querySelector(".viewport-content");
 
   is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"),
      `${w}px`, `Viewport should have width of ${w}px`);
   is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"),
--- a/devtools/client/responsive.html/test/browser/browser_device_pixel_ratio_change.js
+++ b/devtools/client/responsive.html/test/browser/browser_device_pixel_ratio_change.js
@@ -51,37 +51,34 @@ async function testDefaults(ui) {
     disabled: false,
   });
   testViewportDeviceSelectLabel(ui, "no device selected");
 }
 
 async function testChangingDevice(ui) {
   info("Test Changing Device");
 
-  let reloaded = waitForViewportLoad(ui);
   await selectDevice(ui, testDevice.name);
-  await reloaded;
   await waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
   let dppx = await waitForDevicePixelRatio(ui, testDevice.pixelRatio);
   is(dppx, testDevice.pixelRatio, "Content has expected devicePixelRatio");
   testViewportDevicePixelRatioSelect(ui, {
     value: testDevice.pixelRatio,
     disabled: true,
   });
   testViewportDeviceSelectLabel(ui, testDevice.name);
 }
 
 async function testResetWhenResizingViewport(ui) {
   info("Test reset when resizing the viewport");
 
   let deviceRemoved = once(ui, "device-association-removed");
-  let reloaded = waitForViewportLoad(ui);
   await testViewportResize(ui, ".viewport-vertical-resize-handle",
     [-10, -10], [testDevice.width, testDevice.height - 10], [0, -10], ui);
-  await Promise.all([ deviceRemoved, reloaded ]);
+  await deviceRemoved;
 
   let dppx = await waitForDevicePixelRatio(ui, DEFAULT_DPPX);
   is(dppx, DEFAULT_DPPX, "Content has expected devicePixelRatio");
 
   testViewportDevicePixelRatioSelect(ui, {
     value: DEFAULT_DPPX,
     disabled: false,
   });
--- a/devtools/client/responsive.html/test/browser/browser_touch_device.js
+++ b/devtools/client/responsive.html/test/browser/browser_touch_device.js
@@ -18,24 +18,28 @@ const testDevice = {
   "os": "custom",
   "featured": true,
 };
 
 // Add the new device to the list
 addDeviceForTest(testDevice);
 
 addRDMTask(TEST_URL, async function ({ ui, manager }) {
+  reloadOnTouchChange(true);
+
   await waitStartup(ui);
 
   await testDefaults(ui);
   await testChangingDevice(ui);
   await testResizingViewport(ui, true, false);
   await testEnableTouchSimulation(ui);
   await testResizingViewport(ui, false, true);
   await testDisableTouchSimulation(ui);
+
+  reloadOnTouchChange(false);
 });
 
 async function waitStartup(ui) {
   let { store } = ui.toolWindow;
 
   // Wait until the viewport has been added and the device list has been loaded
   await waitUntilState(store, state => state.viewports.length == 1
     && state.devices.listState == Types.loadableState.LOADED);
--- a/devtools/client/responsive.html/test/browser/browser_touch_simulation.js
+++ b/devtools/client/responsive.html/test/browser/browser_touch_simulation.js
@@ -4,25 +4,29 @@
 "use strict";
 
 // Test global touch simulation button
 
 const TEST_URL = `${URL_ROOT}touch.html`;
 const PREF_DOM_META_VIEWPORT_ENABLED = "dom.meta-viewport.enabled";
 
 addRDMTask(TEST_URL, async function ({ ui }) {
+  reloadOnTouchChange(true);
+
   await injectEventUtilsInContentTask(ui.getViewportBrowser());
 
   await waitBootstrap(ui);
   await testWithNoTouch(ui);
   await toggleTouchSimulation(ui);
   await testWithTouch(ui);
   await testWithMetaViewportEnabled(ui);
   await testWithMetaViewportDisabled(ui);
   testTouchButton(ui);
+
+  reloadOnTouchChange(false);
 });
 
 async function testWithNoTouch(ui) {
   await ContentTask.spawn(ui.getViewportBrowser(), {}, async function () {
     let div = content.document.querySelector("div");
     let x = 0, y = 0;
 
     info("testWithNoTouch: Initial test parameter and mouse mouse outside div");
--- a/devtools/client/responsive.html/test/browser/head.js
+++ b/devtools/client/responsive.html/test/browser/head.js
@@ -29,38 +29,39 @@ Services.scriptloader.loadSubScript(
 // Import helpers for the inspector that are also shared with others
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
   this);
 
 const E10S_MULTI_ENABLED = Services.prefs.getIntPref("dom.ipc.processCount") > 1;
 const TEST_URI_ROOT = "http://example.com/browser/devtools/client/responsive.html/test/browser/";
 const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";
+const RELOAD_CONDITION_PREF_PREFIX = "devtools.responsive.reloadConditions.";
 
 const { _loadPreferredDevices } = require("devtools/client/responsive.html/actions/devices");
 const asyncStorage = require("devtools/shared/async-storage");
 const { addDevice, removeDevice, removeLocalDevices } = require("devtools/client/shared/devices");
 
 SimpleTest.requestCompleteLog();
 SimpleTest.waitForExplicitFinish();
 
 // Toggling the RDM UI involves several docShell swap operations, which are somewhat slow
 // on debug builds. Usually we are just barely over the limit, so a blanket factor of 2
 // should be enough.
 requestLongerTimeout(2);
 
 flags.testing = true;
-Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
-Services.prefs.setCharPref("devtools.devices.url",
-  TEST_URI_ROOT + "devices.json");
+Services.prefs.setCharPref("devtools.devices.url", TEST_URI_ROOT + "devices.json");
 
 registerCleanupFunction(async () => {
   flags.testing = false;
   Services.prefs.clearUserPref("devtools.devices.url");
   Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
+  Services.prefs.clearUserPref("devtools.responsive.reloadConditions.touchSimulation");
+  Services.prefs.clearUserPref("devtools.responsive.reloadConditions.userAgent");
   await asyncStorage.removeItem("devtools.devices.url_cache");
   await removeLocalDevices();
 });
 
 loader.lazyRequireGetter(this, "ResponsiveUIManager", "devtools/client/responsive.html/manager", true);
 
 /**
  * Open responsive design mode for the given tab.
@@ -414,8 +415,18 @@ function addDeviceInModal(ui, device) {
   let existingCustomDevices = store.getState().devices.custom.length;
   let adderSave = document.querySelector("#device-adder-save");
   let saved = waitUntilState(store, state =>
     state.devices.custom.length == existingCustomDevices + 1
   );
   Simulate.click(adderSave);
   return saved;
 }
+
+function reloadOnUAChange(enabled) {
+  let pref = RELOAD_CONDITION_PREF_PREFIX + "userAgent";
+  Services.prefs.setBoolPref(pref, enabled);
+}
+
+function reloadOnTouchChange(enabled) {
+  let pref = RELOAD_CONDITION_PREF_PREFIX + "touchSimulation";
+  Services.prefs.setBoolPref(pref, enabled);
+}
--- a/devtools/client/responsive.html/types.js
+++ b/devtools/client/responsive.html/types.js
@@ -5,23 +5,51 @@
 "use strict";
 
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { createEnum } = require("devtools/client/shared/enum");
 
 // React PropTypes are used to describe the expected "shape" of various common
 // objects that get passed down as props to components.
 
+/* ENUMS */
+
+/**
+ * An enum containing the possible states for loadable things.
+ */
+exports.loadableState = createEnum([
+  "INITIALIZED",
+  "LOADING",
+  "LOADED",
+  "ERROR",
+]);
+
 /* GLOBAL */
 
 /**
  * The location of the document displayed in the viewport(s).
  */
 exports.location = PropTypes.string;
 
+/**
+ * Whether to reload the page automatically when certain actions occur.
+ */
+exports.reloadConditions = {
+
+  // Whether to reload when touch simulation is toggled
+  touchSimulation: PropTypes.bool,
+
+  // Whether to reload when user agent is changed
+  userAgent: PropTypes.bool,
+
+  // Loaded state of these conditions
+  state: PropTypes.oneOf(Object.keys(exports.loadableState)),
+
+};
+
 /* DEVICE */
 
 /**
  * A single device that can be displayed in the viewport.
  */
 const device = {
 
   // The name of the device
@@ -46,26 +74,16 @@ const device = {
   os: PropTypes.String,
 
   // Whether or not the device is displayed in the device selector
   displayed: PropTypes.bool,
 
 };
 
 /**
- * An enum containing the possible states for loadable things.
- */
-exports.loadableState = createEnum([
-  "INITIALIZED",
-  "LOADING",
-  "LOADED",
-  "ERROR",
-]);
-
-/**
  * A list of devices and their types that can be displayed in the viewport.
  */
 exports.devices = {
 
   // An array of device types
   types: PropTypes.arrayOf(PropTypes.string),
 
   // An array of phone devices