# HG changeset patch # User J. Ryan Stinnett # Date 1518124817 21600 # Node ID dc1bd6cc0ade6954d74e5e0b3f58c33857b3d1c7 # Parent eb10c415f7ca04e123a1c4d8b6739c6c6215d54e 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 diff --git a/devtools/client/locales/en-US/responsive.properties b/devtools/client/locales/en-US/responsive.properties --- 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 diff --git a/devtools/client/preferences/devtools.js b/devtools/client/preferences/devtools.js --- a/devtools/client/preferences/devtools.js +++ b/devtools/client/preferences/devtools.js @@ -349,8 +349,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); diff --git a/devtools/client/responsive.html/actions/index.js b/devtools/client/responsive.html/actions/index.js --- 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", diff --git a/devtools/client/responsive.html/actions/moz.build b/devtools/client/responsive.html/actions/moz.build --- 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', ) diff --git a/devtools/client/responsive.html/actions/reload-conditions.js b/devtools/client/responsive.html/actions/reload-conditions.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 }); + }; + }, + +}; diff --git a/devtools/client/responsive.html/app.js b/devtools/client/responsive.html/app.js --- 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, diff --git a/devtools/client/responsive.html/components/DevicePixelRatioSelector.js b/devtools/client/responsive.html/components/DevicePixelRatioSelector.js --- 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"); diff --git a/devtools/client/responsive.html/components/DeviceSelector.js b/devtools/client/responsive.html/components/DeviceSelector.js --- 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) { diff --git a/devtools/client/responsive.html/components/GlobalToolbar.js b/devtools/client/responsive.html/components/GlobalToolbar.js --- 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({ diff --git a/devtools/client/responsive.html/components/NetworkThrottlingSelector.js b/devtools/client/responsive.html/components/NetworkThrottlingSelector.js --- 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"); } diff --git a/devtools/client/responsive.html/components/ReloadConditions.js b/devtools/client/responsive.html/components/ReloadConditions.js 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; diff --git a/devtools/client/responsive.html/components/ToggleMenu.js b/devtools/client/responsive.html/components/ToggleMenu.js 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; diff --git a/devtools/client/responsive.html/components/moz.build b/devtools/client/responsive.html/components/moz.build --- 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', ) diff --git a/devtools/client/responsive.html/index.css b/devtools/client/responsive.html/index.css --- 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"); } diff --git a/devtools/client/responsive.html/index.js b/devtools/client/responsive.html/index.js --- 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); diff --git a/devtools/client/responsive.html/manager.js b/devtools/client/responsive.html/manager.js --- 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"); }, /** diff --git a/devtools/client/responsive.html/reducers.js b/devtools/client/responsive.html/reducers.js --- 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"); diff --git a/devtools/client/responsive.html/reducers/moz.build b/devtools/client/responsive.html/reducers/moz.build --- 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', ) diff --git a/devtools/client/responsive.html/reducers/reload-conditions.js b/devtools/client/responsive.html/reducers/reload-conditions.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); +}; diff --git a/devtools/client/responsive.html/test/browser/browser_device_change.js b/devtools/client/responsive.html/test/browser/browser_device_change.js --- 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"), diff --git 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 --- 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, }); diff --git a/devtools/client/responsive.html/test/browser/browser_touch_device.js b/devtools/client/responsive.html/test/browser/browser_touch_device.js --- 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); diff --git a/devtools/client/responsive.html/test/browser/browser_touch_simulation.js b/devtools/client/responsive.html/test/browser/browser_touch_simulation.js --- 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"); diff --git a/devtools/client/responsive.html/test/browser/head.js b/devtools/client/responsive.html/test/browser/head.js --- 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); +} diff --git a/devtools/client/responsive.html/types.js b/devtools/client/responsive.html/types.js --- 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