Bug 1615102 - Resize column to fit content. r=Harald,Honza,bomsy
☠☠ backed out by df43a7470ad3 ☠ ☠
authorFarooq AR <farooqbckk@gmail.com>
Fri, 13 Mar 2020 10:42:30 +0000
changeset 518563 2e588c4bccf11a9c8c2d297e7791665ecc8f88fe
parent 518562 d5eff66da2cdaecb0867143340b992365ce95198
child 518564 25ad22509b7809e464ca6602d8b821841a17deb8
push id110048
push userjodvarko@mozilla.com
push dateFri, 13 Mar 2020 10:43:13 +0000
treeherderautoland@2e588c4bccf1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHarald, Honza, bomsy
bugs1615102
milestone76.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1615102 - Resize column to fit content. r=Harald,Honza,bomsy Differential Revision: https://phabricator.services.mozilla.com/D65077
devtools/client/locales/en-US/netmonitor.properties
devtools/client/netmonitor/src/components/request-list/RequestListHeader.js
devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
devtools/client/netmonitor/test/browser.ini
devtools/client/netmonitor/test/browser_net_column-resize-fit.js
devtools/client/shared/components/splitter/Draggable.js
--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -948,16 +948,24 @@ netmonitor.toolbar.search=Search
 # LOCALIZATION NOTE (netmonitor.toolbar.resetColumns): This is the label
 # displayed in the network table header context menu.
 netmonitor.toolbar.resetColumns=Reset Columns
 
 # LOCALIZATION NOTE (netmonitor.toolbar.resetSorting): This is the label
 # displayed in the network table header context menu to reset sorting
 netmonitor.toolbar.resetSorting=Reset Sorting
 
+# LOCALIZATION NOTE (netmonitor.toolbar.resizeColumnToFitContent): This is the label
+# displayed in the network table header context menu to resize a column to fit its content
+netmonitor.toolbar.resizeColumnToFitContent=Resize Column To Fit Content
+
+# LOCALIZATION NOTE (netmonitor.toolbar.resizeColumnToFitContent.title): This is the title
+# tooltip displayed when draggable resizer in network table headers is hovered
+netmonitor.toolbar.resizeColumnToFitContent.title=Double-click to fit column to content
+
 # LOCALIZATION NOTE (netmonitor.toolbar.timings): This is the label
 # displayed in the network table header context menu for the timing submenu
 netmonitor.toolbar.timings=Timings
 
 # LOCALIZATION NOTE (netmonitor.toolbar.responseHeaders): This is the
 # label displayed in the network table header context menu for the
 # response headers submenu.
 netmonitor.toolbar.responseHeaders=Response Headers
--- a/devtools/client/netmonitor/src/components/request-list/RequestListHeader.js
+++ b/devtools/client/netmonitor/src/components/request-list/RequestListHeader.js
@@ -69,24 +69,26 @@ class RequestListHeader extends Componen
     this.requestListHeader = createRef();
 
     this.onContextMenu = this.onContextMenu.bind(this);
     this.drawBackground = this.drawBackground.bind(this);
     this.resizeWaterfall = this.resizeWaterfall.bind(this);
     this.waterfallDivisionLabels = this.waterfallDivisionLabels.bind(this);
     this.waterfallLabel = this.waterfallLabel.bind(this);
     this.onHeaderClick = this.onHeaderClick.bind(this);
+    this.resizeColumnToFitContent = this.resizeColumnToFitContent.bind(this);
   }
 
   componentWillMount() {
     const { resetColumns, resetSorting, toggleColumn } = this.props;
     this.contextMenu = new RequestListHeaderContextMenu({
       resetColumns,
       resetSorting,
       toggleColumn,
+      resizeColumnToFitContent: this.resizeColumnToFitContent,
     });
   }
 
   componentDidMount() {
     // Create the object that takes care of drawing the waterfall canvas background
     this.background = new WaterfallBackground(document);
     this.drawBackground();
     // When visible columns add up to less or more than 100% => update widths in prefs.
@@ -110,16 +112,109 @@ class RequestListHeader extends Componen
 
   componentWillUnmount() {
     this.background.destroy();
     this.background = null;
     window.removeEventListener("resize", this.resizeWaterfall);
     removeThemeObserver(this.drawBackground);
   }
 
+  /**
+   * Helper method to get the total width of cell's content.
+   * Used for resizing columns to fit their content.
+   */
+  totalCellWidth(cellEl) {
+    return [...cellEl.childNodes]
+      .map(cNode => {
+        if (cNode.nodeType === 3) {
+          // if it's text node
+          return Math.ceil(
+            cNode.getBoxQuads()[0].p2.x - cNode.getBoxQuads()[0].p1.x
+          );
+        }
+        return cNode.getBoundingClientRect().width;
+      })
+      .reduce((a, b) => a + b, 0);
+  }
+
+  /**
+   * Resize column to fit its content.
+   * Additionally, resize other columns (starting from last) to compensate.
+   */
+  resizeColumnToFitContent(name) {
+    const headerRef = this.refs[`${name}Header`];
+    const parentEl = headerRef.closest(".requests-list-table");
+    const width = headerRef.getBoundingClientRect().width;
+    const parentWidth = parentEl.getBoundingClientRect().width;
+    const items = parentEl.querySelectorAll(".request-list-item");
+    const columnIndex = headerRef.cellIndex;
+    const widths = [...items].map(item =>
+      this.totalCellWidth(item.children[columnIndex])
+    );
+
+    const minW = this.getMinWidth(name);
+
+    // Add 11 to account for cell padding (padding-right + padding-left = 9px), not accurate.
+    let maxWidth = 11 + Math.max.apply(null, widths);
+
+    if (maxWidth < minW) {
+      maxWidth = minW;
+    }
+
+    // Pixel value which, if added to this column's width, will fit its content.
+    let change = maxWidth - width;
+
+    // Max change we can do while taking other columns into account.
+    let maxAllowedChange = 0;
+    const visibleColumns = this.getVisibleColumns();
+    const newWidths = [];
+
+    // Calculate new widths for other columns to compensate.
+    // Start from the 2nd last column if last column is waterfall.
+    // This is done to comply with the existing resizing behavior.
+    const delta =
+      visibleColumns[visibleColumns.length - 1].name === "waterfall" ? 2 : 1;
+
+    for (let i = visibleColumns.length - delta; i > 0; i--) {
+      if (i !== columnIndex) {
+        const columnName = visibleColumns[i].name;
+        const columnHeaderRef = this.refs[`${columnName}Header`];
+        const columnWidth = columnHeaderRef.getBoundingClientRect().width;
+        const minWidth = this.getMinWidth(columnName);
+        const newWidth = columnWidth - change;
+
+        // If this column can compensate for all the remaining change.
+        if (newWidth >= minWidth) {
+          maxAllowedChange += change;
+          change = 0;
+          newWidths.push({
+            name: columnName,
+            width: this.px2percent(newWidth, parentWidth),
+          });
+          break;
+        } else {
+          // Max change we can do in this column.
+          let maxColumnChange = columnWidth - minWidth;
+          maxColumnChange = maxColumnChange > change ? change : maxColumnChange;
+          maxAllowedChange += maxColumnChange;
+          change -= maxColumnChange;
+          newWidths.push({
+            name: columnName,
+            width: this.px2percent(columnWidth - maxColumnChange, parentWidth),
+          });
+        }
+      }
+    }
+    newWidths.push({
+      name,
+      width: this.px2percent(width + maxAllowedChange, parentWidth),
+    });
+    this.props.setColumnsWidth(newWidths);
+  }
+
   onContextMenu(evt) {
     evt.preventDefault();
     this.contextMenu.open(evt, this.props.columns);
   }
 
   onHeaderClick(evt, headerName) {
     const { sortBy, resetSorting } = this.props;
     if (evt.button == 1) {
@@ -546,19 +641,21 @@ class RequestListHeader extends Componen
     }
     const columnStyle = {
       width: colWidth + "%",
     };
 
     // Support for columns resizing is currently hidden behind a pref.
     const draggable = Draggable({
       className: "column-resizer ",
+      title: L10N.getStr("netmonitor.toolbar.resizeColumnToFitContent.title"),
       onStart: () => this.onStartMove(),
       onStop: () => this.onStopMove(),
       onMove: x => this.onMove(name, x),
+      onDoubleClick: () => this.resizeColumnToFitContent(name),
     });
 
     return dom.th(
       {
         id: `requests-list-${boxName}-header-box`,
         className: `requests-list-column requests-list-${boxName}`,
         scope: "col",
         style: columnStyle,
@@ -567,16 +664,17 @@ class RequestListHeader extends Componen
         // Used to style the next column.
         "data-active": active,
       },
       button(
         {
           id: `requests-list-${name}-button`,
           className: `requests-list-header-button`,
           "data-sorted": sorted,
+          "data-name": name,
           title: sortedTitle ? `${label} (${sortedTitle})` : label,
           onClick: evt => this.onHeaderClick(evt, name),
         },
         name === "waterfall"
           ? this.waterfallLabel(waterfallWidth, scale, label)
           : div({ className: "button-text" }, label),
         div({ className: "button-icon" })
       ),
--- a/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
@@ -76,16 +76,24 @@ class RequestListHeaderContextMenu {
     });
 
     menu.push({
       id: "request-list-header-reset-sorting",
       label: L10N.getStr("netmonitor.toolbar.resetSorting"),
       click: () => this.props.resetSorting(),
     });
 
+    const columnName = event.target.getAttribute("data-name");
+
+    menu.push({
+      id: "request-list-header-resize-column-to-fit-content",
+      label: L10N.getStr("netmonitor.toolbar.resizeColumnToFitContent"),
+      click: () => this.props.resizeColumnToFitContent(columnName),
+    });
+
     showMenu(menu, {
       screenX: event.screenX,
       screenY: event.screenY,
     });
   }
 }
 
 module.exports = RequestListHeaderContextMenu;
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -99,16 +99,17 @@ skip-if = (verify && !debug && (os == 'l
 [browser_net_charts-02.js]
 [browser_net_charts-03.js]
 [browser_net_charts-04.js]
 [browser_net_charts-05.js]
 [browser_net_charts-06.js]
 [browser_net_charts-07.js]
 [browser_net_clear.js]
 [browser_net_column_headers_tooltips.js]
+[browser_net_column-resize-fit.js]
 [browser_net_columns_last_column.js]
 [browser_net_columns_pref.js]
 [browser_net_columns_reset.js]
 [browser_net_columns_showhide.js]
 [browser_net_columns_time.js]
 [browser_net_complex-params.js]
 skip-if = (verify && !debug && (os == 'win'))
 [browser_net_content-type.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_column-resize-fit.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests resizing of columns in NetMonitor.
+ */
+add_task(async function() {
+  // Reset visibleColumns so we only get the default ones
+  // and not all that are set in head.js
+  Services.prefs.clearUserPref("devtools.netmonitor.visibleColumns");
+  const visibleColumns = JSON.parse(
+    Services.prefs.getCharPref("devtools.netmonitor.visibleColumns")
+  );
+  // Init network monitor
+  const { tab, monitor } = await initNetMonitor(SIMPLE_URL);
+  info("Starting test... ");
+
+  const { document } = monitor.panelWin;
+
+  // Wait for network events (to have some requests in the table)
+  const wait = waitForNetworkEvents(monitor, 1);
+  tab.linkedBrowser.reload();
+  await wait;
+
+  info("Testing column resize to fit using double-click on draggable resizer");
+  const fileHeader = document.querySelector(`#requests-list-file-header-box`);
+  const fileColumnResizer = fileHeader.querySelector(".column-resizer");
+
+  EventUtils.sendMouseEvent({ type: "dblclick" }, fileColumnResizer);
+
+  // After resize - get fresh prefs for tests.
+  let columnsData = JSON.parse(
+    Services.prefs.getCharPref("devtools.netmonitor.columnsData")
+  );
+
+  // `File` column before resize: 25%, after resize: 11.25%
+  // `Transferred` column before resize: 10%, after resize: 10%
+  checkColumnsData(columnsData, "file", 12);
+  checkSumOfVisibleColumns(columnsData, visibleColumns);
+
+  info(
+    "Testing column resize to fit using context menu `Resize Column To Fit Content`"
+  );
+
+  // Resizing `transferred` column.
+  EventUtils.sendMouseEvent(
+    { type: "contextmenu" },
+    document.querySelector("#requests-list-transferred-button")
+  );
+
+  getContextMenuItem(
+    monitor,
+    "request-list-header-resize-column-to-fit-content"
+  ).click();
+
+  columnsData = JSON.parse(
+    Services.prefs.getCharPref("devtools.netmonitor.columnsData")
+  );
+
+  // `Transferred` column before resize: 10%, after resize: 2.97%
+  checkColumnsData(columnsData, "transferred", 3);
+  checkSumOfVisibleColumns(columnsData, visibleColumns);
+
+  // Done: clean up.
+  return teardown(monitor);
+});
+
+function checkColumnsData(columnsData, column, expectedWidth) {
+  const width = getWidthFromPref(columnsData, column);
+  const widthsDiff = Math.abs(width - expectedWidth);
+  ok(
+    widthsDiff < 2,
+    `Column ${column} has expected size. Got ${width}, Expected ${expectedWidth}`
+  );
+}
+
+function checkSumOfVisibleColumns(columnsData, visibleColumns) {
+  let sum = 0;
+  visibleColumns.forEach(column => {
+    sum += getWidthFromPref(columnsData, column);
+  });
+  sum = Math.round(sum);
+  is(sum, 100, "All visible columns cover 100%.");
+}
+
+function getWidthFromPref(columnsData, column) {
+  const widthInPref = columnsData.find(function(element) {
+    return element.name === column;
+  }).width;
+  return widthInPref;
+}
--- a/devtools/client/shared/components/splitter/Draggable.js
+++ b/devtools/client/shared/components/splitter/Draggable.js
@@ -7,46 +7,65 @@
 const { createRef, Component } = 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");
 
 class Draggable extends Component {
   static get propTypes() {
     return {
       onMove: PropTypes.func.isRequired,
+      onDoubleClick: PropTypes.func,
       onStart: PropTypes.func,
       onStop: PropTypes.func,
       style: PropTypes.object,
+      title: PropTypes.string,
       className: PropTypes.string,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.draggableEl = createRef();
 
     this.startDragging = this.startDragging.bind(this);
+    this.onDoubleClick = this.onDoubleClick.bind(this);
     this.onMove = this.onMove.bind(this);
     this.onUp = this.onUp.bind(this);
+    this.mouseX = 0;
+    this.mouseY = 0;
   }
+  startDragging(ev) {
+    const xDiff = Math.abs(this.mouseX - ev.clientX);
+    const yDiff = Math.abs(this.mouseY - ev.clientY);
 
-  startDragging(ev) {
+    // This allows for double-click.
+    if (xDiff + yDiff <= 1) {
+      return;
+    }
+    this.mouseX = ev.clientX;
+    this.mouseY = ev.clientY;
+
     if (this.isDragging) {
       return;
     }
     this.isDragging = true;
-
     ev.preventDefault();
     const doc = this.draggableEl.current.ownerDocument;
     doc.addEventListener("mousemove", this.onMove);
     doc.addEventListener("mouseup", this.onUp);
     this.props.onStart && this.props.onStart();
   }
 
+  onDoubleClick() {
+    if (this.props.onDoubleClick) {
+      this.props.onDoubleClick();
+    }
+  }
+
   onMove(ev) {
     if (!this.isDragging) {
       return;
     }
 
     ev.preventDefault();
     // Use viewport coordinates so, moving mouse over iframes
     // doesn't mangle (relative) coordinates.
@@ -66,15 +85,17 @@ class Draggable extends Component {
     this.props.onStop && this.props.onStop();
   }
 
   render() {
     return dom.div({
       ref: this.draggableEl,
       role: "presentation",
       style: this.props.style,
+      title: this.props.title,
       className: this.props.className,
       onMouseDown: this.startDragging,
+      onDoubleClick: this.onDoubleClick,
     });
   }
 }
 
 module.exports = Draggable;