diff --git a/packages/grid/src/GridMetricCalculator.ts b/packages/grid/src/GridMetricCalculator.ts index b007f1175..abe85f9e0 100644 --- a/packages/grid/src/GridMetricCalculator.ts +++ b/packages/grid/src/GridMetricCalculator.ts @@ -130,12 +130,18 @@ export class GridMetricCalculator { /** User set row heights */ protected userRowHeights: ModelSizeMap; - /** Calculated column widths based on cell contents */ + /** Calculated column widths based on cell contents and caching largest value */ protected calculatedColumnWidths: ModelSizeMap; - /** Calculated row heights based on cell contents */ + /** Calculated row heights based on cell contents and caching largest value */ protected calculatedRowHeights: ModelSizeMap; + /** Calculated column widths based on cell contents */ + protected contentColumnWidths: ModelSizeMap; + + /** Calculated row heights based on cell contents */ + protected contentRowHeights: ModelSizeMap; + /** Cache of fonts to estimated width of the smallest char */ protected fontWidthsLower: Map; @@ -162,6 +168,8 @@ export class GridMetricCalculator { userRowHeights = new Map(), calculatedColumnWidths = new Map(), calculatedRowHeights = new Map(), + contentColumnWidths = new Map(), + contentRowHeights = new Map(), fontWidthsLower = new Map(), fontWidthsUpper = new Map(), allCharWidths = new Map(), @@ -178,6 +186,8 @@ export class GridMetricCalculator { this.calculatedColumnWidths = calculatedColumnWidths; this.allCharWidths = allCharWidths; this.fontWidthsLower = fontWidthsLower; + this.contentColumnWidths = contentColumnWidths; + this.contentRowHeights = contentRowHeights; this.fontWidthsUpper = fontWidthsUpper; // Need to track the last moved rows/columns array so we know if we need to reset our models cache @@ -518,6 +528,8 @@ export class GridMetricCalculator { userRowHeights, calculatedRowHeights, calculatedColumnWidths, + contentColumnWidths, + contentRowHeights, } = this; return { @@ -608,6 +620,10 @@ export class GridMetricCalculator { visibleRowHeights, visibleColumnWidths, + // Map of the height/width of visible rows/columns without caching largest value + contentColumnWidths, + contentRowHeights, + // Array of floating rows/columns, by grid index floatingRows, floatingColumns, @@ -1647,8 +1663,12 @@ export class GridMetricCalculator { return rowHeight; } + // Not sure how to accurately get the height of text. For now just return the theme height. + this.contentRowHeights.set(modelRow, Math.ceil(rowHeight)); + trimMap(this.contentRowHeights); + const cachedValue = this.calculatedRowHeights.get(modelRow); - if (cachedValue != null) { + if (cachedValue != null && cachedValue > rowHeight) { return cachedValue; } @@ -1695,6 +1715,9 @@ export class GridMetricCalculator { let columnWidth = Math.ceil(Math.max(headerWidth, dataWidth)); columnWidth = Math.max(minColumnWidth, columnWidth); columnWidth = Math.min(maxColumnWidth, columnWidth); + this.contentColumnWidths.set(modelColumn, columnWidth); + trimMap(this.contentColumnWidths); + if (cachedValue != null && cachedValue > columnWidth) { columnWidth = cachedValue; } else { @@ -1768,7 +1791,8 @@ export class GridMetricCalculator { let columnWidth = 0; - const rowsPerPage = height / rowHeight; + const gridY = this.getGridY(state); + const rowsPerPage = Math.floor((height - gridY) / rowHeight); const bottom = Math.ceil(top + rowsPerPage); const cellPadding = cellHorizontalPadding * 2; GridUtils.iterateAllItems( @@ -1941,6 +1965,19 @@ export class GridMetricCalculator { this.userColumnWidths = userColumnWidths; } + /** + * Sets the calculated width for the specified column + * @param column The column model index to set + * @param size The size to set it to + */ + setCalculatedColumnWidth(column: ModelIndex, size: number): void { + // Always use a new instance of the map so any consumer of the metrics knows there has been a change + const calculatedColumnWidths = new Map(this.calculatedColumnWidths); + calculatedColumnWidths.set(column, Math.ceil(size)); + trimMap(calculatedColumnWidths); + this.calculatedColumnWidths = calculatedColumnWidths; + } + /** * Resets all the calculated column widths * Useful if the theme minimum column width changes @@ -1974,6 +2011,19 @@ export class GridMetricCalculator { this.calculatedRowHeights.delete(row); } + /** + * Sets the calculated height for the specified row + * @param row The column model index to set + * @param size The size to set it to + */ + setCalculatedRowHeight(row: ModelIndex, size: number): void { + // Always use a new instance of the map so any consumer of the metrics knows there has been a change + const calculatedRowHeights = new Map(this.calculatedRowHeights); + calculatedRowHeights.set(row, Math.ceil(size)); + trimMap(calculatedRowHeights); + this.calculatedColumnWidths = calculatedRowHeights; + } + /** * Resets all the calculated row heights * Useful if the theme row height changes diff --git a/packages/grid/src/GridMetrics.ts b/packages/grid/src/GridMetrics.ts index 03b35c090..a32c3ce89 100644 --- a/packages/grid/src/GridMetrics.ts +++ b/packages/grid/src/GridMetrics.ts @@ -173,6 +173,10 @@ export type GridMetrics = { calculatedRowHeights: ModelSizeMap; calculatedColumnWidths: ModelSizeMap; + // Map of calculated row/column height/width without caching largest value + contentColumnWidths: ModelSizeMap; + contentRowHeights: ModelSizeMap; + // Max depth of column headers. Depth of 1 for a table without column groups columnHeaderMaxDepth: number; }; diff --git a/packages/grid/src/mouse-handlers/GridSeparatorMouseHandler.ts b/packages/grid/src/mouse-handlers/GridSeparatorMouseHandler.ts index d27e0b03f..20a3426e7 100644 --- a/packages/grid/src/mouse-handlers/GridSeparatorMouseHandler.ts +++ b/packages/grid/src/mouse-handlers/GridSeparatorMouseHandler.ts @@ -277,16 +277,7 @@ abstract class GridSeparatorMouseHandler extends GridMouseHandler { const modelIndexes = metrics[this.modelIndexesProperty]; const modelIndex = getOrThrow(modelIndexes, separator.index); - const calculatedSize = - metrics[this.calculatedSizesProperty].get(modelIndex); - const defaultSize = - metricCalculator[this.initialSizesProperty].get(modelIndex); - - if (calculatedSize === defaultSize || calculatedSize == null) { - this.resetSize(metricCalculator, modelIndex); - } else { - this.setSize(metricCalculator, modelIndex, calculatedSize); - } + this.resetSize(metricCalculator, modelIndex); grid.forceUpdate(); diff --git a/packages/iris-grid/src/IrisGrid.test.tsx b/packages/iris-grid/src/IrisGrid.test.tsx index f6709a5ac..7e887507f 100644 --- a/packages/iris-grid/src/IrisGrid.test.tsx +++ b/packages/iris-grid/src/IrisGrid.test.tsx @@ -222,3 +222,140 @@ it('should set gotoValueSelectedColumnName to empty string if no columns are giv expect(component.state.gotoValueSelectedColumnName).toEqual(''); }); + +describe('handleResizeColumn', () => { + let irisGrid; + let metricCalculator; + + beforeAll(() => { + irisGrid = makeComponent( + irisGridTestUtils.makeModel( + irisGridTestUtils.makeTable({ + columns: irisGridTestUtils.makeColumns(1), + }) + ) + ); + metricCalculator = irisGrid.state.metricCalculator; + }); + + it('should set column width to content width if undefined user width', async () => { + const modelIndex = 0; + const mockMetricCalculator = { + ...metricCalculator, + userColumnWidths: new Map(), + setColumnWidth: jest.fn((column, size) => { + mockMetricCalculator.userColumnWidths.set(column, size); + }), + }; + Object.assign(irisGrid.state.metricCalculator, mockMetricCalculator); + const contentWidth = + irisGrid.state.metrics.contentColumnWidths.get(modelIndex); + expect(contentWidth).toBeDefined(); + + irisGrid.handleResizeColumn(modelIndex); + + expect(mockMetricCalculator.userColumnWidths.get(modelIndex)).toEqual( + contentWidth + ); + }); + + it('should reset user width & set calculated width to content width if column has defined user width', () => { + const modelIndex = 0; + const mockMetricCalculator = { + ...metricCalculator, + userColumnWidths: new Map([[modelIndex, 100]]), + setCalculatedColumnWidth: jest.fn((column, size) => { + mockMetricCalculator.calculatedColumnWidths.set(column, size); + }), + resetColumnWidth: jest.fn(() => { + mockMetricCalculator.userColumnWidths.delete(modelIndex); + }), + }; + Object.assign(irisGrid.state.metricCalculator, mockMetricCalculator); + const contentWidth = + irisGrid.state.metrics.contentColumnWidths.get(modelIndex); + expect(contentWidth).toBeDefined(); + + irisGrid.handleResizeColumn(modelIndex); + + expect( + mockMetricCalculator.userColumnWidths.get(modelIndex) + ).toBeUndefined(); + expect(mockMetricCalculator.calculatedColumnWidths.get(modelIndex)).toEqual( + contentWidth + ); + }); +}); + +// auto resize -> reset user width and set calculated width to content width +// manual resize -> set user width to content width +describe('handleResizeAllColumns', () => { + let irisGrid; + let metricCalculator; + + beforeAll(() => { + irisGrid = makeComponent( + irisGridTestUtils.makeModel( + irisGridTestUtils.makeTable({ + columns: irisGridTestUtils.makeColumns(3), + }) + ) + ); + metricCalculator = irisGrid.state.metricCalculator; + }); + + it('should auto resize all columns if all were manually sized', () => { + const mockMetricCalculator = { + ...metricCalculator, + userColumnWidths: new Map([ + [0, 100], + [1, 100], + [2, 100], + ]), + setCalculatedColumnWidth: jest.fn((column, size) => { + mockMetricCalculator.calculatedColumnWidths.set(column, size); + }), + resetColumnWidth: jest.fn(column => { + mockMetricCalculator.userColumnWidths.delete(column); + }), + }; + Object.assign(irisGrid.state.metricCalculator, mockMetricCalculator); + const contentWidths = irisGrid.state.metrics.contentColumnWidths; + + irisGrid.handleResizeAllColumns(); + + expect(mockMetricCalculator.userColumnWidths.size).toEqual(0); + + contentWidths.forEach((contentWidth, modelIndex) => { + expect( + mockMetricCalculator.calculatedColumnWidths.get(modelIndex) + ).toEqual(contentWidth); + }); + }); + + it('should manual resize all columns if not all were manually sized', () => { + const mockMetricCalculator = { + ...metricCalculator, + userColumnWidths: new Map([ + [0, 100], + [1, 100], + ]), + setColumnWidth: jest.fn((column, size) => { + mockMetricCalculator.userColumnWidths.set(column, size); + }), + resetColumnWidth: jest.fn(column => { + mockMetricCalculator.userColumnWidths.delete(column); + }), + }; + Object.assign(irisGrid.state.metricCalculator, mockMetricCalculator); + const contentWidths = irisGrid.state.metrics.contentColumnWidths; + + irisGrid.handleResizeAllColumns(); + + contentWidths.forEach((contentWidth, modelIndex) => { + expect(mockMetricCalculator.userColumnWidths.get(modelIndex)).toEqual( + contentWidth + ); + }); + }); +}); diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index 937eb7394..81a00c51b 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -3443,6 +3443,59 @@ class IrisGrid extends Component { this.grid?.forceUpdate(); } + handleResizeColumn(modelIndex: number): void { + const { metrics, metricCalculator } = this.state; + if (!metrics) throw new Error('Metrics not set'); + + const contentWidth = getOrThrow(metrics.contentColumnWidths, modelIndex); + + const userWidths = metricCalculator.getUserColumnWidths(); + if (userWidths.has(modelIndex)) { + metricCalculator.resetColumnWidth(modelIndex); + metricCalculator.setCalculatedColumnWidth(modelIndex, contentWidth); + } else { + metricCalculator.setColumnWidth(modelIndex, contentWidth); + } + + this.grid?.forceUpdate(); + } + + handleResizeAllColumns(): void { + const { metrics, metricCalculator } = this.state; + if (!metrics) throw new Error('Metrics not set'); + + const allColumns = [...metrics.allColumnWidths.entries()]; + const visibleColumns = allColumns + .filter(([_, width]) => width !== 0) + .map(([modelIndex]) => modelIndex); + + const contentWidths = metrics.contentColumnWidths; + const userWidths = metricCalculator.getUserColumnWidths(); + + const manualColumns = visibleColumns.filter(modelIndex => + userWidths.has(modelIndex) + ); + + if (visibleColumns.length === manualColumns.length) { + // All columns are manually sized, flip all to auto resize + for (let i = 0; i < visibleColumns.length; i += 1) { + const modelIndex = visibleColumns[i]; + const contentWidth = getOrThrow(contentWidths, modelIndex); + metricCalculator.resetColumnWidth(modelIndex); + metricCalculator.setCalculatedColumnWidth(modelIndex, contentWidth); + } + } else { + // Flip all to manual sized + for (let i = 0; i < visibleColumns.length; i += 1) { + const modelIndex = visibleColumns[i]; + const contentWidth = getOrThrow(contentWidths, modelIndex); + metricCalculator.setColumnWidth(modelIndex, contentWidth); + } + } + + this.grid?.forceUpdate(); + } + /** * User added, removed, or changed the order of aggregations, or position * @param aggregationSettings The new aggregation settings diff --git a/packages/iris-grid/src/IrisGridMetricCalculator.ts b/packages/iris-grid/src/IrisGridMetricCalculator.ts index e8ce35684..39acdc53c 100644 --- a/packages/iris-grid/src/IrisGridMetricCalculator.ts +++ b/packages/iris-grid/src/IrisGridMetricCalculator.ts @@ -53,6 +53,10 @@ export class IrisGridMetricCalculator extends GridMetricCalculator { getUserColumnWidths(): ModelSizeMap { return this.userColumnWidths; } + + getCalculatedColumnWidths(): ModelSizeMap { + return this.calculatedColumnWidths; + } } export default IrisGridMetricCalculator; diff --git a/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.tsx b/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.tsx index 8916c0092..f385c4c7f 100644 --- a/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.tsx +++ b/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.tsx @@ -231,7 +231,7 @@ class IrisGridContextMenuHandler extends GridMouseHandler { this.irisGrid.freezeColumnByColumnName(column.name); } }, - order: 10, + order: 30, }); actions.push({ title: 'Show All Columns', @@ -241,6 +241,22 @@ class IrisGridContextMenuHandler extends GridMouseHandler { }, disabled: !isColumnHidden, }); + actions.push({ + title: 'Resize Column', + group: IrisGridContextMenuHandler.GROUP_HIDE_COLUMNS, + action: () => { + this.irisGrid.handleResizeColumn(modelIndex); + }, + order: 10, + }); + actions.push({ + title: 'Resize All Columns', + group: IrisGridContextMenuHandler.GROUP_HIDE_COLUMNS, + action: () => { + this.irisGrid.handleResizeAllColumns(); + }, + order: 20, + }); actions.push({ title: 'Quick Filters', icon: vsRemove,