diff --git a/src/sql/base/browser/ui/table/plugins/cellSelectionModel.plugin.ts b/src/sql/base/browser/ui/table/plugins/cellSelectionModel.plugin.ts index c3a6384a57..ac7a079de3 100644 --- a/src/sql/base/browser/ui/table/plugins/cellSelectionModel.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/cellSelectionModel.plugin.ts @@ -3,16 +3,20 @@ import { mixin } from 'vs/base/common/objects'; import { isUndefinedOrNull } from 'vs/base/common/types'; +import * as platform from 'vs/base/common/platform'; import { CellRangeSelector, ICellRangeSelector } from 'sql/base/browser/ui/table/plugins/cellRangeSelector'; export interface ICellSelectionModelOptions { cellRangeSelector?: any; - selectActiveCell?: boolean; + /** + * Whether the grid has a row selection column. Needs to take this into account to decide the cell click's selection range. + */ + hasRowSelector?: boolean, } const defaults: ICellSelectionModelOptions = { - selectActiveCell: true + hasRowSelector: false }; interface EventTargetWithClassName extends EventTarget { @@ -40,9 +44,8 @@ export class CellSelectionModel implements Slick.SelectionModel) { this.grid = grid; - this._handler.subscribe(this.grid.onClick, (e: DOMEvent, args: Slick.OnActiveCellChangedEventArgs) => this.handleActiveCellChange(e as MouseEvent, args)); this._handler.subscribe(this.grid.onKeyDown, (e: DOMEvent) => this.handleKeyDown(e as KeyboardEvent)); - this._handler.subscribe(this.grid.onClick, (e: DOMEvent, args: Slick.OnClickEventArgs) => this.handleIndividualCellSelection(e as MouseEvent, args)); + this._handler.subscribe(this.grid.onClick, (e: DOMEvent, args: Slick.OnClickEventArgs) => this.handleCellClick(e as MouseEvent, args)); this._handler.subscribe(this.grid.onHeaderClick, (e: DOMEvent, args: Slick.OnHeaderClickEventArgs) => this.handleHeaderClick(e as MouseEvent, args)); this.grid.registerPlugin(this.selector); this._handler.subscribe(this.selector.onCellRangeSelected, (e: Event, range: Slick.Range) => this.handleCellRangeSelected(e, range, false)); @@ -104,13 +107,8 @@ export class CellSelectionModel implements Slick.SelectionModel) { - if (this.options.selectActiveCell && !isUndefinedOrNull(args.row) && !isUndefinedOrNull(args.cell) && !e.ctrlKey) { - this.setSelectedRanges([new Slick.Range(args.row, args.cell)]); - } else if (!this.options.selectActiveCell) { - // clear the previous selection once the cell changes - this.setSelectedRanges([]); - } + private isMultiSelection(e: MouseEvent): boolean { + return platform.isMacintosh ? e.metaKey : e.ctrlKey; } private handleHeaderClick(e: MouseEvent, args: Slick.OnHeaderClickEventArgs) { @@ -118,17 +116,32 @@ export class CellSelectionModel implements Slick.SelectionModel; - if (e.ctrlKey) { - ranges = this.getSelectedRanges(); - ranges.push(new Slick.Range(0, columnIndex, this.grid.getDataLength() - 1, columnIndex)); - } else { - ranges = [new Slick.Range(0, columnIndex, this.grid.getDataLength() - 1, columnIndex)]; - } - this.grid.setActiveCell(this.grid.getViewport()?.top ?? 0, columnIndex); - this.setSelectedRanges(ranges); + const columnIndex = this.grid.getColumnIndex(args.column.id!); + const rowCount = this.grid.getDataLength(); + const columnCount = this.grid.getColumns().length; + const currentActiveCell = this.grid.getActiveCell(); + let newActiveCell: Slick.Cell; + if (this.options.hasRowSelector && columnIndex === 0) { + // When the row selector's header is clicked, all cells should be selected + this.setSelectedRanges([new Slick.Range(0, 1, rowCount - 1, columnCount - 1)]); + // The first data cell in the view should be selected. + newActiveCell = { row: this.grid.getViewport()?.top ?? 0, cell: 1 }; + } + else if (this.grid.canCellBeSelected(0, columnIndex)) { + // When SHIFT is pressed, all the columns between active cell's column and target column should be selected + const newlySelectedRange = (e.shiftKey && currentActiveCell) ? new Slick.Range(0, currentActiveCell.cell, rowCount - 1, columnIndex) + : new Slick.Range(0, columnIndex, rowCount - 1, columnIndex); + + // When CTRL is pressed, we need to merge the new selection with existing selections + const rangesToBeMerged: Slick.Range[] = this.isMultiSelection(e) ? this.getSelectedRanges() : []; + const result = this.insertIntoSelections(rangesToBeMerged, newlySelectedRange); + this.setSelectedRanges(result); + // The first data cell of the target column in the view should be selected. + newActiveCell = { row: this.grid.getViewport()?.top ?? 0, cell: columnIndex }; + } + + if (newActiveCell) { + this.grid.setActiveCell(newActiveCell.row, newActiveCell.cell); } } } @@ -228,29 +241,35 @@ export class CellSelectionModel implements Slick.SelectionModel) { - if (!e.ctrlKey) { - return; - } + private handleCellClick(e: MouseEvent, args: Slick.OnClickEventArgs) { + const activeCell = this.grid.getActiveCell(); + const columns = this.grid.getColumns(); + const isRowSelectorClicked: boolean = this.options.hasRowSelector && args.cell === 0; - let ranges: Array; - - ranges = this.getSelectedRanges(); - - let selectedRange: Slick.Range; - if (args.cell === 0) { - selectedRange = new Slick.Range(args.row, 1, args.row, args.grid.getColumns().length - 1); + let newlySelectedRange: Slick.Range; + // The selection is a range when there is an active cell and the SHIFT key is pressed. + if (activeCell !== undefined && e.shiftKey) { + // When the row selector cell is clicked, the new selection is all rows from current active row to target row. + // Otherwise, the new selection is the cells in the rectangle between current active cell and target cell. + newlySelectedRange = isRowSelectorClicked ? new Slick.Range(activeCell.row, columns.length - 1, args.row, 1) + : new Slick.Range(activeCell.row, activeCell.cell, args.row, args.cell); } else { - selectedRange = new Slick.Range(args.row, args.cell); - + // If the row selector cell is clicked, the new selection is all the cells in the target row. + // Otherwise, the new selection is the target cell + newlySelectedRange = isRowSelectorClicked ? new Slick.Range(args.row, 1, args.row, columns.length - 1) + : new Slick.Range(args.row, args.cell, args.row, args.cell); } - ranges = this.insertIntoSelections(ranges, selectedRange); - this.grid.setActiveCell(selectedRange.toRow, selectedRange.toCell); - this.setSelectedRanges(ranges); + // When the CTRL key is pressed, we need to merge the new selection with the existing selections. + const rangesToBeMerged: Slick.Range[] = this.isMultiSelection(e) ? this.getSelectedRanges() : []; + const result = this.insertIntoSelections(rangesToBeMerged, newlySelectedRange); + this.setSelectedRanges(result); - e.preventDefault(); - e.stopImmediatePropagation(); + // Find out the new active cell + // If the row selector is clicked, the first data cell in the row should be the new active cell, + // otherwise, the target cell should be the new active cell. + const newActiveCell: Slick.Cell = isRowSelectorClicked ? { cell: 1, row: args.row } : { cell: args.cell, row: args.row }; + this.grid.setActiveCell(newActiveCell.row, newActiveCell.cell); } private handleKeyDown(e: KeyboardEvent) { diff --git a/src/sql/base/browser/ui/table/plugins/rowNumberColumn.plugin.ts b/src/sql/base/browser/ui/table/plugins/rowNumberColumn.plugin.ts index 1c29930882..87183b13a8 100644 --- a/src/sql/base/browser/ui/table/plugins/rowNumberColumn.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/rowNumberColumn.plugin.ts @@ -4,17 +4,27 @@ *--------------------------------------------------------------------------------------------*/ import { FilterableColumn } from 'sql/base/browser/ui/table/interfaces'; +import { mixin } from 'vs/base/common/objects'; export interface IRowNumberColumnOptions { - numberOfRows: number; cssClass?: string; + /** + * Controls the cell selection behavior. If the value is true or not specified, the entire row will be selected when a cell is clicked, + * and when the header is clicked, the entire table will be selected. If the value is false, the auto selection will not happen. + */ + autoCellSelection?: boolean; } +const defaultOptions: IRowNumberColumnOptions = { + autoCellSelection: true +}; + export class RowNumberColumn implements Slick.Plugin { private handler = new Slick.EventHandler(); private grid!: Slick.Grid; - constructor(private options: IRowNumberColumnOptions) { + constructor(private options?: IRowNumberColumnOptions) { + this.options = mixin(this.options, defaultOptions, false); } public init(grid: Slick.Grid) { @@ -29,7 +39,7 @@ export class RowNumberColumn implements Slick.Plugin { } private handleClick(e: MouseEvent, args: Slick.OnClickEventArgs): void { - if (this.grid.getColumns()[args.cell].id === 'rowNumber') { + if (this.grid.getColumns()[args.cell].id === 'rowNumber' && this.options.autoCellSelection) { this.grid.setActiveCell(args.row, 1); if (this.grid.getSelectionModel()) { this.grid.setSelectedRows([args.row]); @@ -38,7 +48,7 @@ export class RowNumberColumn implements Slick.Plugin { } private handleHeaderClick(e: MouseEvent, args: Slick.OnHeaderClickEventArgs): void { - if (args.column.id === 'rowNumber') { + if (args.column.id === 'rowNumber' && this.options.autoCellSelection) { this.grid.setActiveCell(this.grid.getViewport()?.top ?? 0, 1); let selectionModel = this.grid.getSelectionModel(); if (selectionModel) { diff --git a/src/sql/workbench/contrib/editData/browser/editDataGridPanel.ts b/src/sql/workbench/contrib/editData/browser/editDataGridPanel.ts index 2c9027bf75..d15046cb30 100644 --- a/src/sql/workbench/contrib/editData/browser/editDataGridPanel.ts +++ b/src/sql/workbench/contrib/editData/browser/editDataGridPanel.ts @@ -392,7 +392,7 @@ export class EditDataGridPanel extends GridParentComponent { let maxHeight = this.getMaxHeight(resultSet.rowCount); let minHeight = this.getMinHeight(resultSet.rowCount); - let rowNumberColumn = new RowNumberColumn({ numberOfRows: resultSet.rowCount }); + let rowNumberColumn = new RowNumberColumn(); // Store the result set from the event let dataSet: IGridDataSet = { diff --git a/src/sql/workbench/contrib/executionPlan/browser/topOperationsTab.ts b/src/sql/workbench/contrib/executionPlan/browser/topOperationsTab.ts index f27ead4ba5..94f85f2e93 100644 --- a/src/sql/workbench/contrib/executionPlan/browser/topOperationsTab.ts +++ b/src/sql/workbench/contrib/executionPlan/browser/topOperationsTab.ts @@ -191,7 +191,7 @@ export class TopOperationsTabView extends Disposable implements IPanelView { topOperationContainer.appendChild(tableContainer); this._topOperationsContainers.push(topOperationContainer); - const rowNumberColumn = new RowNumberColumn({ numberOfRows: dataMap.length }); + const rowNumberColumn = new RowNumberColumn({ autoCellSelection: false }); columns.unshift(rowNumberColumn.getColumnDefinition()); let copyHandler = new CopyKeybind(); @@ -234,7 +234,7 @@ export class TopOperationsTabView extends Disposable implements IPanelView { }); })); - const selectionModel = new CellSelectionModel(); + const selectionModel = new CellSelectionModel({ hasRowSelector: true }); const table = this._register(new Table(tableContainer, { columns: columns, diff --git a/src/sql/workbench/contrib/query/browser/gridPanel.ts b/src/sql/workbench/contrib/query/browser/gridPanel.ts index d6e74955cc..1a068bd3b6 100644 --- a/src/sql/workbench/contrib/query/browser/gridPanel.ts +++ b/src/sql/workbench/contrib/query/browser/gridPanel.ts @@ -353,7 +353,7 @@ export abstract class GridTableBase extends Disposable implements IView { private table: Table; private actionBar: ActionBar; private container = document.createElement('div'); - private selectionModel = new CellSelectionModel(); + private selectionModel = new CellSelectionModel({ hasRowSelector: true }); private styles: ITableStyles; private currentHeight: number; private dataProvider: HybridDataProvider; @@ -498,7 +498,7 @@ export abstract class GridTableBase extends Disposable implements IView { collection.setCollectionChangedCallback((startIndex, count) => { this.renderGridDataRowsRange(startIndex, count); }); - this.rowNumberColumn = new RowNumberColumn({ numberOfRows: this.resultSet.rowCount }); + this.rowNumberColumn = new RowNumberColumn({ autoCellSelection: false }); this.columns.unshift(this.rowNumberColumn.getColumnDefinition()); let tableOptions: Slick.GridOptions = { rowHeight: this.rowHeight, diff --git a/src/sql/workbench/services/notebook/browser/outputs/tableRenderers.ts b/src/sql/workbench/services/notebook/browser/outputs/tableRenderers.ts index 0decf01ecd..ac1f2e7941 100644 --- a/src/sql/workbench/services/notebook/browser/outputs/tableRenderers.ts +++ b/src/sql/workbench/services/notebook/browser/outputs/tableRenderers.ts @@ -49,7 +49,7 @@ export function renderDataResource( // In order to show row numbers, we need to put the row number column // ahead of all of the other columns, and register the plugin below - let rowNumberColumn = new RowNumberColumn({ numberOfRows: source.length }); + let rowNumberColumn = new RowNumberColumn(); columnsTransformed.unshift(rowNumberColumn.getColumnDefinition()); let transformedData = transformData(sourceObject.data, columnsTransformed);