Bug 1426041 - Fix toolbar layout in Portrait mode; r=davidwalsh draft
authorFred Lin <gasolin@gmail.com>
Thu, 04 Jan 2018 11:40:27 +0800
changeset 784193 935463d90781d7dbdd9bc9743aa00cc332716c2c
parent 783966 789e30ff2e3d6e1fcfce1a373c1e5635488d24da
push id106875
push userjodvarko@mozilla.com
push dateWed, 18 Apr 2018 08:40:47 +0000
reviewersdavidwalsh
bugs1426041
milestone61.0a1
Bug 1426041 - Fix toolbar layout in Portrait mode; r=davidwalsh MozReview-Commit-ID: 8yHx0qAmq09
devtools/client/netmonitor/src/assets/styles/Toolbar.css
devtools/client/netmonitor/src/assets/styles/variables.css
devtools/client/netmonitor/src/components/MonitorPanel.js
devtools/client/netmonitor/src/components/Toolbar.js
devtools/client/shared/components/SearchBox.js
--- a/devtools/client/netmonitor/src/assets/styles/Toolbar.css
+++ b/devtools/client/netmonitor/src/assets/styles/Toolbar.css
@@ -1,34 +1,56 @@
 /* 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/. */
 
 /* Toolbar */
 
 .devtools-toolbar {
   display: flex;
+  line-height: 23px;
 }
 
 .devtools-toolbar-container {
   height: auto;
   flex-wrap: wrap;
   justify-content: space-between;
 }
 
 .devtools-toolbar-group {
   display: flex;
   flex: 0 0 auto;
   flex-wrap: nowrap;
   align-items: center;
 }
 
+.devtools-toolbar-group .devtools-separator {
+  height: 24px;
+}
+
+.devtools-toolbar-two-rows-1,
+.devtools-toolbar-two-rows-2,
+.devtools-toolbar-single-row {
+  flex-grow: 1;
+  min-height: var(--primary-toolbar-height);
+}
+
+.devtools-toolbar-two-rows-1 {
+  width: -moz-available;
+}
+
+.devtools-toolbar-two-rows-2 {
+  justify-content: space-between;
+  border-top: 1px solid var(--theme-splitter-color);
+}
+
 .requests-list-filter-buttons {
   display: flex;
   flex-wrap: wrap;
+  margin: 0 2px;
 }
 
 .devtools-button.devtools-pause-icon::before {
   background-image: var(--pause-icon-url);
 }
 
 .devtools-button.devtools-play-icon::before {
   background-image: var(--play-icon-url);
@@ -39,9 +61,10 @@
   vertical-align: middle;
   bottom: 1px;
 }
 
 .devtools-checkbox-label {
   margin-inline-start: 10px;
   margin-inline-end: 3px;
   white-space: nowrap;
+  margin-top: 1px;
 }
--- a/devtools/client/netmonitor/src/assets/styles/variables.css
+++ b/devtools/client/netmonitor/src/assets/styles/variables.css
@@ -30,16 +30,18 @@
   --timing-wait-color: rgba(95, 136, 176, 0.8); /* blue grey */
   --timing-receive-color: rgba(44, 187, 15, 0.8); /* green */
 
   --sort-ascending-image: url(chrome://devtools/skin/images/sort-ascending-arrow.svg);
   --sort-descending-image: url(chrome://devtools/skin/images/sort-descending-arrow.svg);
 }
 
 :root {
+  --primary-toolbar-height: 29px;
+
   /* Icons */
   --play-icon-url: url("chrome://devtools/content/netmonitor/src/assets/icons/play.svg");
   --pause-icon-url: url("chrome://devtools/skin/images/pause.svg");
 
   /* HTTP status codes */
   --status-code-color-1xx: var(--theme-highlight-blue);
   --status-code-color-2xx: var(--theme-highlight-green);
   --status-code-color-3xx: transparent;
--- a/devtools/client/netmonitor/src/components/MonitorPanel.js
+++ b/devtools/client/netmonitor/src/components/MonitorPanel.js
@@ -22,17 +22,23 @@ const {
 const SplitBox = createFactory(require("devtools/client/shared/components/splitter/SplitBox"));
 const RequestList = createFactory(require("./RequestList"));
 const Toolbar = createFactory(require("./Toolbar"));
 
 loader.lazyGetter(this, "NetworkDetailsPanel", function() {
   return createFactory(require("./NetworkDetailsPanel"));
 });
 
-const MediaQueryList = window.matchMedia("(min-width: 700px)");
+// MediaQueryList object responsible for switching sidebar splitter
+// between landscape and portrait mode (depending on browser window size).
+const MediaQueryVert = window.matchMedia("(min-width: 700px)");
+
+// MediaQueryList object responsible for switching the toolbar
+// between single and 2-rows layout (depending on browser window size).
+const MediaQuerySingleRow = window.matchMedia("(min-width: 920px)");
 
 /**
  * Monitor panel component
  * The main panel for displaying various network request information
  */
 class MonitorPanel extends Component {
   static get propTypes() {
     return {
@@ -48,62 +54,66 @@ class MonitorPanel extends Component {
       updateRequest: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
-      isVerticalSpliter: MediaQueryList.matches,
+      isSingleRow: MediaQuerySingleRow.matches,
+      isVerticalSpliter: MediaQueryVert.matches,
     };
 
     this.onLayoutChange = this.onLayoutChange.bind(this);
     this.onNetworkDetailsResized = this.onNetworkDetailsResized.bind(this);
   }
 
   componentDidMount() {
-    MediaQueryList.addListener(this.onLayoutChange);
+    MediaQuerySingleRow.addListener(this.onLayoutChange);
+    MediaQueryVert.addListener(this.onLayoutChange);
   }
 
   componentWillReceiveProps(nextProps) {
     updateFormDataSections(nextProps);
   }
 
   componentDidUpdate() {
     let { selectedRequestVisible, openNetworkDetails } = this.props;
     if (!selectedRequestVisible) {
       openNetworkDetails(false);
     }
   }
 
   componentWillUnmount() {
-    MediaQueryList.removeListener(this.onLayoutChange);
+    MediaQuerySingleRow.removeListener(this.onLayoutChange);
+    MediaQueryVert.removeListener(this.onLayoutChange);
 
     let { clientWidth, clientHeight } = findDOMNode(this.refs.endPanel) || {};
 
     if (this.state.isVerticalSpliter && clientWidth) {
       Services.prefs.setIntPref(
         "devtools.netmonitor.panes-network-details-width", clientWidth);
     }
     if (!this.state.isVerticalSpliter && clientHeight) {
       Services.prefs.setIntPref(
         "devtools.netmonitor.panes-network-details-height", clientHeight);
     }
   }
 
   onLayoutChange() {
     this.setState({
-      isVerticalSpliter: MediaQueryList.matches,
+      isSingleRow: MediaQuerySingleRow.matches,
+      isVerticalSpliter: MediaQueryVert.matches,
     });
   }
 
   onNetworkDetailsResized(width, height) {
-   // Cleaning width and height parameters, because SplitBox passes ALWAYS two values,
-   // while depending on orientation ONLY ONE dimension is managed by it at a time.
+    // Cleaning width and height parameters, because SplitBox passes ALWAYS two values,
+    // while depending on orientation ONLY ONE dimension is managed by it at a time.
     let { isVerticalSpliter }  = this.state;
     return this.props.onNetworkDetailsResized(
       isVerticalSpliter ? width : null,
       isVerticalSpliter ? null : height
     );
   }
 
   render() {
@@ -111,23 +121,26 @@ class MonitorPanel extends Component {
       connector,
       isEmpty,
       networkDetailsOpen,
       openLink,
       sourceMapService,
     } = this.props;
 
     let initialWidth = Services.prefs.getIntPref(
-        "devtools.netmonitor.panes-network-details-width");
+      "devtools.netmonitor.panes-network-details-width");
     let initialHeight = Services.prefs.getIntPref(
-        "devtools.netmonitor.panes-network-details-height");
+      "devtools.netmonitor.panes-network-details-height");
 
     return (
       div({ className: "monitor-panel" },
-        Toolbar({ connector }),
+        Toolbar({
+          connector,
+          singleRow: this.state.isSingleRow,
+        }),
         SplitBox({
           className: "devtools-responsive-container",
           initialWidth: initialWidth,
           initialHeight: initialHeight,
           minSize: "50px",
           maxSize: "80%",
           splitterSize: 1,
           startPanel: RequestList({ isEmpty, connector }),
--- a/devtools/client/netmonitor/src/components/Toolbar.js
+++ b/devtools/client/netmonitor/src/components/Toolbar.js
@@ -61,16 +61,19 @@ class Toolbar extends Component {
       enablePersistentLogs: PropTypes.func.isRequired,
       togglePersistentLogs: PropTypes.func.isRequired,
       persistentLogsEnabled: PropTypes.bool.isRequired,
       disableBrowserCache: PropTypes.func.isRequired,
       toggleBrowserCache: PropTypes.func.isRequired,
       browserCacheDisabled: PropTypes.bool.isRequired,
       toggleRequestFilterType: PropTypes.func.isRequired,
       filteredRequests: PropTypes.array.isRequired,
+      // Set to true if there is enough horizontal space
+      // and the toolbar needs just one row.
+      singleRow: PropTypes.bool.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
     this.autocompleteProvider = this.autocompleteProvider.bind(this);
     this.onSearchBoxFocus = this.onSearchBoxFocus.bind(this);
     this.toggleRequestFilterType = this.toggleRequestFilterType.bind(this);
@@ -84,16 +87,17 @@ class Toolbar extends Component {
     Services.prefs.addObserver(DEVTOOLS_DISABLE_CACHE_PREF,
                                this.updateBrowserCacheDisabled);
   }
 
   shouldComponentUpdate(nextProps) {
     return this.props.persistentLogsEnabled !== nextProps.persistentLogsEnabled
     || this.props.browserCacheDisabled !== nextProps.browserCacheDisabled
     || this.props.recording !== nextProps.recording
+    || this.props.singleRow !== nextProps.singleRow
     || !Object.is(this.props.requestFilterTypes, nextProps.requestFilterTypes)
 
     // Filtered requests are useful only when searchbox is focused
     || !!(this.refs.searchbox && this.refs.searchbox.focused);
   }
 
   componentWillUnmount() {
     Services.prefs.removeObserver(DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF,
@@ -130,29 +134,61 @@ class Toolbar extends Component {
     filteredRequests.forEach((request) => {
       fetchNetworkUpdatePacket(connector.requestData, request, [
         "responseCookies",
         "responseHeaders",
       ]);
     });
   }
 
-  render() {
-    let {
-      toggleRecording,
-      clearRequests,
-      requestFilterTypes,
-      setRequestFilterText,
-      togglePersistentLogs,
-      persistentLogsEnabled,
-      toggleBrowserCache,
-      browserCacheDisabled,
-      recording,
-    } = this.props;
+  /**
+   * Render a separator.
+   */
+  renderSeparator() {
+    return span({ className: "devtools-separator" });
+  }
+
+  /**
+   * Render a clear button.
+   */
+  renderClearButton(clearRequests) {
+    return (
+      button({
+        className: "devtools-button devtools-clear-icon requests-list-clear-button",
+        title: TOOLBAR_CLEAR,
+        onClick: clearRequests,
+      })
+    );
+  }
 
+  /**
+   * Render a ToggleRecording button.
+   */
+  renderToggleRecordingButton(recording, toggleRecording) {
+    // Calculate class-list for toggle recording button.
+    // The button has two states: pause/play.
+    let toggleRecordingButtonClass = [
+      "devtools-button",
+      "requests-list-pause-button",
+      recording ? "devtools-pause-icon" : "devtools-play-icon",
+    ].join(" ");
+
+    return (
+      button({
+        className: toggleRecordingButtonClass,
+        title: TOOLBAR_TOGGLE_RECORDING,
+        onClick: toggleRecording,
+      })
+    );
+  }
+
+  /**
+   * Render filter buttons.
+   */
+  renderFilterButtons(requestFilterTypes) {
     // Render list of filter-buttons.
     let buttons = Object.entries(requestFilterTypes).map(([type, checked]) => {
       let classList = ["devtools-button", `requests-list-filter-${type}-button`];
       checked && classList.push("checked");
 
       return (
         button({
           className: classList.join(" "),
@@ -162,79 +198,127 @@ class Toolbar extends Component {
           "aria-pressed": checked,
           "data-key": type,
         },
           TOOLBAR_FILTER_LABELS[type]
         )
       );
     });
 
-    // Calculate class-list for toggle recording button. The button
-    // has two states: pause/play.
-    let toggleRecordingButtonClass = [
-      "devtools-button",
-      "requests-list-pause-button",
-      recording ? "devtools-pause-icon" : "devtools-play-icon",
-    ].join(" ");
+    return div({ className: "requests-list-filter-buttons" }, buttons);
+  }
+
+  /**
+   * Render a Persistlog checkbox.
+   */
+  renderPersistlogCheckbox(persistentLogsEnabled, togglePersistentLogs) {
+    return (
+      label(
+        {
+          className: "devtools-checkbox-label",
+          title: ENABLE_PERSISTENT_LOGS_TOOLTIP,
+        },
+        input({
+          id: "devtools-persistlog-checkbox",
+          className: "devtools-checkbox",
+          type: "checkbox",
+          checked: persistentLogsEnabled,
+          onChange: togglePersistentLogs,
+        }),
+        ENABLE_PERSISTENT_LOGS_LABEL,
+      )
+    );
+  }
+
+  /**
+   * Render a Cache checkbox.
+   */
+  renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache) {
+    return (
+      label(
+        {
+          className: "devtools-checkbox-label",
+          title: DISABLE_CACHE_TOOLTIP,
+        },
+        input({
+          id: "devtools-cache-checkbox",
+          className: "devtools-checkbox",
+          type: "checkbox",
+          checked: browserCacheDisabled,
+          onChange: toggleBrowserCache,
+        }),
+        DISABLE_CACHE_LABEL,
+      )
+    );
+  }
+
+  /**
+   * Render filter Searchbox.
+   */
+  renderFilterBox(setRequestFilterText) {
+    return (
+      SearchBox({
+        delay: FILTER_SEARCH_DELAY,
+        keyShortcut: SEARCH_KEY_SHORTCUT,
+        placeholder: SEARCH_PLACE_HOLDER,
+        plainStyle: true,
+        type: "filter",
+        ref: "searchbox",
+        onChange: setRequestFilterText,
+        onFocus: this.onSearchBoxFocus,
+        autocompleteProvider: this.autocompleteProvider,
+      })
+    );
+  }
+
+  render() {
+    let {
+      toggleRecording,
+      clearRequests,
+      requestFilterTypes,
+      setRequestFilterText,
+      togglePersistentLogs,
+      persistentLogsEnabled,
+      toggleBrowserCache,
+      browserCacheDisabled,
+      recording,
+      singleRow,
+    } = this.props;
 
     // Render the entire toolbar.
-    return (
+    // dock at bottom or dock at side has different layout
+    return singleRow ? (
       span({ className: "devtools-toolbar devtools-toolbar-container" },
-        span({ className: "devtools-toolbar-group" },
-          button({
-            className: toggleRecordingButtonClass,
-            title: TOOLBAR_TOGGLE_RECORDING,
-            onClick: toggleRecording,
-          }),
-          button({
-            className: "devtools-button devtools-clear-icon requests-list-clear-button",
-            title: TOOLBAR_CLEAR,
-            onClick: clearRequests,
-          }),
-          div({ className: "requests-list-filter-buttons" }, buttons),
-          label(
-            {
-              className: "devtools-checkbox-label",
-              title: ENABLE_PERSISTENT_LOGS_TOOLTIP,
-            },
-            input({
-              id: "devtools-persistlog-checkbox",
-              className: "devtools-checkbox",
-              type: "checkbox",
-              checked: persistentLogsEnabled,
-              onChange: togglePersistentLogs,
-            }),
-            ENABLE_PERSISTENT_LOGS_LABEL
-          ),
-          label(
-            {
-              className: "devtools-checkbox-label",
-              title: DISABLE_CACHE_TOOLTIP,
-            },
-            input({
-              id: "devtools-cache-checkbox",
-              className: "devtools-checkbox",
-              type: "checkbox",
-              checked: browserCacheDisabled,
-              onChange: toggleBrowserCache,
-            }),
-            DISABLE_CACHE_LABEL,
-          ),
+        span({ className: "devtools-toolbar-group devtools-toolbar-single-row" },
+          this.renderClearButton(clearRequests),
+          this.renderSeparator(),
+          this.renderFilterBox(setRequestFilterText),
+          this.renderSeparator(),
+          this.renderToggleRecordingButton(recording, toggleRecording),
+          this.renderSeparator(),
+          this.renderFilterButtons(requestFilterTypes),
+          this.renderSeparator(),
+          this.renderPersistlogCheckbox(persistentLogsEnabled, togglePersistentLogs),
+          this.renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache),
+        )
+      )
+    ) : (
+      span({ className: "devtools-toolbar devtools-toolbar-container" },
+        span({ className: "devtools-toolbar-group devtools-toolbar-two-rows-1" },
+          this.renderClearButton(clearRequests),
+          this.renderSeparator(),
+          this.renderFilterBox(setRequestFilterText),
+          this.renderSeparator(),
+          this.renderToggleRecordingButton(recording, toggleRecording),
+          this.renderSeparator(),
+          this.renderPersistlogCheckbox(persistentLogsEnabled, togglePersistentLogs),
+          this.renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache),
         ),
-        span({ className: "devtools-toolbar-group" },
-          SearchBox({
-            delay: FILTER_SEARCH_DELAY,
-            keyShortcut: SEARCH_KEY_SHORTCUT,
-            placeholder: SEARCH_PLACE_HOLDER,
-            type: "filter",
-            ref: "searchbox",
-            onChange: setRequestFilterText,
-            onFocus: this.onSearchBoxFocus,
-            autocompleteProvider: this.autocompleteProvider,
-          })
+        span({ className: "devtools-toolbar-group devtools-toolbar-two-rows-2" },
+          this.renderFilterButtons(requestFilterTypes)
         )
       )
     );
   }
 }
 
 module.exports = connect(
   (state) => ({
--- a/devtools/client/shared/components/SearchBox.js
+++ b/devtools/client/shared/components/SearchBox.js
@@ -17,16 +17,17 @@ class SearchBox extends Component {
     return {
       delay: PropTypes.number,
       keyShortcut: PropTypes.string,
       onChange: PropTypes.func,
       onFocus: PropTypes.func,
       onBlur: PropTypes.func,
       onKeyDown: PropTypes.func,
       placeholder: PropTypes.string,
+      plainStyle: PropTypes.bool,
       type: PropTypes.string,
       autocompleteProvider: PropTypes.func,
     };
   }
 
   constructor(props) {
     super(props);
 
@@ -155,20 +156,24 @@ class SearchBox extends Component {
     }
   }
 
   render() {
     let {
       type = "search",
       placeholder,
       autocompleteProvider,
+      plainStyle,
     } = this.props;
     let { value } = this.state;
     let divClassList = ["devtools-searchbox", "has-clear-btn"];
     let inputClassList = [`devtools-${type}input`];
+    if (plainStyle) {
+      inputClassList.push("devtools-plaininput");
+    }
     let showAutocomplete = autocompleteProvider && this.state.focused && value !== "";
 
     if (value !== "") {
       inputClassList.push("filled");
     }
     return dom.div(
       { className: divClassList.join(" ") },
       dom.input({