diff --git a/packages/code-studio/src/assets/svg/cursor-copy.svg b/packages/code-studio/src/assets/svg/cursor-copy.svg
new file mode 100644
index 0000000000..28421e86b4
--- /dev/null
+++ b/packages/code-studio/src/assets/svg/cursor-copy.svg
@@ -0,0 +1,15 @@
+
diff --git a/packages/code-studio/src/main/AppMainContainer.scss b/packages/code-studio/src/main/AppMainContainer.scss
index b45dd6e8ee..3524998f8f 100644
--- a/packages/code-studio/src/main/AppMainContainer.scss
+++ b/packages/code-studio/src/main/AppMainContainer.scss
@@ -248,6 +248,12 @@ $nav-space: 4px; // give a gap around some buttons for focus area that are in na
}
}
+.grid-cursor-copy {
+ cursor:
+ url('../assets/svg/cursor-copy.svg') 8 8,
+ copy;
+}
+
.grid-cursor-linker {
cursor:
url('../assets/svg/cursor-linker.svg') 8 8,
diff --git a/packages/components/src/context-actions/ContextActionUtils.ts b/packages/components/src/context-actions/ContextActionUtils.ts
index 4e76570dea..49c1872279 100644
--- a/packages/components/src/context-actions/ContextActionUtils.ts
+++ b/packages/components/src/context-actions/ContextActionUtils.ts
@@ -17,6 +17,9 @@ export interface ContextAction {
icon?: IconDefinition | React.ReactElement;
iconColor?: string;
shortcut?: Shortcut;
+
+ /* Display text for the shortcut if the shortcut is not wired up through the Shortcut class */
+ shortcutText?: string;
isGlobal?: boolean;
group?: number;
order?: number;
diff --git a/packages/components/src/context-actions/ContextMenuItem.tsx b/packages/components/src/context-actions/ContextMenuItem.tsx
index febf5c72ab..9af75cfef3 100644
--- a/packages/components/src/context-actions/ContextMenuItem.tsx
+++ b/packages/components/src/context-actions/ContextMenuItem.tsx
@@ -71,7 +71,8 @@ const ContextMenuItem = React.forwardRef(
'data-testid': dataTestId,
} = props;
- const displayShortcut = menuItem.shortcut?.getDisplayText();
+ const displayShortcut =
+ menuItem.shortcutText ?? menuItem.shortcut?.getDisplayText();
let icon: IconDefinition | React.ReactElement | null = null;
if (menuItem.icon) {
const menuItemIcon = menuItem.icon;
diff --git a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.scss b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.scss
index 457c74e1bb..e5235adc1d 100644
--- a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.scss
+++ b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.scss
@@ -31,3 +31,7 @@ $panel-message-overlay-top: 30px;
.grid-cursor-linker {
cursor: crosshair;
}
+
+.grid-cursor-copy {
+ cursor: copy;
+}
diff --git a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx
index 4101c6a68e..1b895ab508 100644
--- a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx
+++ b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx
@@ -1308,6 +1308,7 @@ export class IrisGridPanel extends PureComponent<
)}
columnAllowedCursor="linker"
columnNotAllowedCursor="linker-not-allowed"
+ copyCursor="copy"
customColumns={customColumns}
customColumnFormatMap={customColumnFormatMap}
columnSelectionValidator={this.isColumnSelectionValid}
diff --git a/packages/grid/src/Grid.test.tsx b/packages/grid/src/Grid.test.tsx
index e85dc2cd66..ffacabe39f 100644
--- a/packages/grid/src/Grid.test.tsx
+++ b/packages/grid/src/Grid.test.tsx
@@ -220,7 +220,8 @@ function mouseDoubleClick(
function keyDown(key: string, component: Grid, extraArgs?: KeyboardEventInit) {
const args = { key, ...extraArgs };
- component.handleKeyDown(
+ component.notifyKeyboardHandlers(
+ 'onDown',
new KeyboardEvent('keydown', args) as unknown as React.KeyboardEvent
);
}
diff --git a/packages/grid/src/Grid.tsx b/packages/grid/src/Grid.tsx
index fed4bd08c7..04cdbf2ef5 100644
--- a/packages/grid/src/Grid.tsx
+++ b/packages/grid/src/Grid.tsx
@@ -34,7 +34,10 @@ import {
GridTokenMouseHandler,
} from './mouse-handlers';
import './Grid.scss';
-import KeyHandler, { GridKeyboardEvent } from './KeyHandler';
+import KeyHandler, {
+ GridKeyHandlerFunctionName,
+ GridKeyboardEvent,
+} from './KeyHandler';
import {
EditKeyHandler,
PasteKeyHandler,
@@ -342,7 +345,9 @@ class Grid extends PureComponent {
this.handleEditCellChange = this.handleEditCellChange.bind(this);
this.handleEditCellCommit = this.handleEditCellCommit.bind(this);
this.handleDoubleClick = this.handleDoubleClick.bind(this);
+ this.notifyKeyboardHandlers = this.notifyKeyboardHandlers.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.handleKeyUp = this.handleKeyUp.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseDrag = this.handleMouseDrag.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
@@ -1715,14 +1720,20 @@ class Grid extends PureComponent {
}
/**
- * Handle a key down event from the keyboard. Pass the event to the registered keyboard handlers until one handles it.
- * @param event Keyboard event
+ * Notify all of the keyboard handlers for this grid of a keyboard event.
+ * @param functionName The name of the function in the keyboard handler to call
+ * @param event The keyboard event to notify
*/
- handleKeyDown(event: GridKeyboardEvent): void {
+ notifyKeyboardHandlers(
+ functionName: GridKeyHandlerFunctionName,
+ event: GridKeyboardEvent
+ ): void {
const keyHandlers = this.getKeyHandlers();
for (let i = 0; i < keyHandlers.length; i += 1) {
const keyHandler = keyHandlers[i];
- const result = keyHandler.onDown(event, this);
+ const result =
+ keyHandler[functionName] != null &&
+ keyHandler[functionName](event, this);
if (result !== false) {
const options = result as EventHandlerResultOptions;
if (options?.stopPropagation ?? true) event.stopPropagation();
@@ -1732,6 +1743,14 @@ class Grid extends PureComponent {
}
}
+ handleKeyDown(event: GridKeyboardEvent): void {
+ this.notifyKeyboardHandlers('onDown', event);
+ }
+
+ handleKeyUp(event: GridKeyboardEvent): void {
+ this.notifyKeyboardHandlers('onUp', event);
+ }
+
/**
* Notify all of the mouse handlers for this grid of a mouse event.
* @param functionName The name of the function in the mouse handler to call
@@ -2229,6 +2248,7 @@ class Grid extends PureComponent {
onContextMenu={this.handleContextMenu}
onDoubleClick={this.handleDoubleClick}
onKeyDown={this.handleKeyDown}
+ onKeyUp={this.handleKeyUp}
onMouseDown={this.handleMouseDown}
onMouseMove={this.handleMouseMove}
onMouseLeave={this.handleMouseLeave}
diff --git a/packages/grid/src/KeyHandler.ts b/packages/grid/src/KeyHandler.ts
index 8b4138f52d..f6216f5adf 100644
--- a/packages/grid/src/KeyHandler.ts
+++ b/packages/grid/src/KeyHandler.ts
@@ -15,6 +15,8 @@ import type Grid from './Grid';
*/
export type GridKeyboardEvent = KeyboardEvent | React.KeyboardEvent;
+export type GridKeyHandlerFunctionName = 'onDown' | 'onUp';
+
export class KeyHandler {
order: number;
@@ -33,6 +35,16 @@ export class KeyHandler {
onDown(event: GridKeyboardEvent, grid: Grid): EventHandlerResult {
return false;
}
+
+ /**
+ * Handle a keyup event on the grid.
+ * @param event The keyboard event
+ * @param grid The grid component the key press is on
+ * @returns Response indicating if the key was consumed
+ */
+ onUp(event: GridKeyboardEvent, grid: Grid): EventHandlerResult {
+ return false;
+ }
}
export default KeyHandler;
diff --git a/packages/iris-grid/src/IrisGrid.test.tsx b/packages/iris-grid/src/IrisGrid.test.tsx
index d255416fe8..059e8fb9ba 100644
--- a/packages/iris-grid/src/IrisGrid.test.tsx
+++ b/packages/iris-grid/src/IrisGrid.test.tsx
@@ -80,7 +80,10 @@ function makeComponent(
function keyDown(key, component, extraArgs?) {
const args = { key, ...extraArgs };
- component.grid.handleKeyDown(new KeyboardEvent('keydown', args));
+ component.grid.notifyKeyboardHandlers(
+ 'onDown',
+ new KeyboardEvent('keydown', args)
+ );
}
it('renders without crashing', () => {
diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx
index 7f7605097c..db33aa6db4 100644
--- a/packages/iris-grid/src/IrisGrid.tsx
+++ b/packages/iris-grid/src/IrisGrid.tsx
@@ -120,6 +120,7 @@ import {
IrisGridColumnSelectMouseHandler,
IrisGridColumnTooltipMouseHandler,
IrisGridContextMenuHandler,
+ IrisGridCopyCellMouseHandler,
IrisGridDataSelectMouseHandler,
IrisGridFilterMouseHandler,
IrisGridRowTreeMouseHandler,
@@ -310,6 +311,9 @@ export interface IrisGridProps {
// eslint-disable-next-line react/no-unused-prop-types
columnNotAllowedCursor: string;
+
+ // eslint-disable-next-line react/no-unused-prop-types
+ copyCursor: string;
name: string;
onlyFetchVisibleColumns: boolean;
@@ -484,6 +488,7 @@ export class IrisGrid extends Component {
columnSelectionValidator: null,
columnAllowedCursor: null,
columnNotAllowedCursor: null,
+ copyCursor: null,
name: 'table',
onlyFetchVisibleColumns: true,
showSearchBar: false,
@@ -616,6 +621,8 @@ export class IrisGrid extends Component {
this.gotoRowRef = React.createRef();
+ this.isCopying = false;
+
this.toggleFilterBarAction = {
action: () => this.toggleFilterBar(),
shortcut: SHORTCUTS.TABLE.TOGGLE_QUICK_FILTER,
@@ -693,15 +700,12 @@ export class IrisGrid extends Component {
columnHeaderGroups,
} = props;
+ const { dh } = model;
const keyHandlers: KeyHandler[] = [
new ReverseKeyHandler(this),
new ClearFilterKeyHandler(this),
];
- if (canCopy) {
- keyHandlers.push(new CopyKeyHandler(this));
- }
- const { dh } = model;
- const mouseHandlers = [
+ const mouseHandlers: GridMouseHandler[] = [
new IrisGridCellOverflowMouseHandler(this),
new IrisGridRowTreeMouseHandler(this),
new IrisGridTokenMouseHandler(this),
@@ -713,7 +717,10 @@ export class IrisGrid extends Component {
new IrisGridDataSelectMouseHandler(this),
new PendingMouseHandler(this),
];
-
+ if (canCopy) {
+ keyHandlers.push(new CopyKeyHandler(this));
+ mouseHandlers.push(new IrisGridCopyCellMouseHandler(this));
+ }
const movedColumns =
movedColumnsProp.length > 0
? movedColumnsProp
@@ -1009,6 +1016,8 @@ export class IrisGrid extends Component {
gotoRowRef: React.RefObject;
+ isCopying: boolean;
+
toggleFilterBarAction: Action;
toggleSearchBarAction: Action;
@@ -2018,6 +2027,25 @@ export class IrisGrid extends Component {
}
}
+ copyColumnHeader(columnIndex: GridRangeIndex, columnDepth = 0): void {
+ if (columnIndex === null) {
+ return;
+ }
+ const { canCopy } = this.props;
+ const { movedColumns } = this.state;
+
+ if (canCopy) {
+ const copyOperation = {
+ columnIndex,
+ columnDepth,
+ movedColumns,
+ };
+ this.setState({ copyOperation });
+ } else {
+ log.error('Attempted copyColumnHeader for user without copy permission.');
+ }
+ }
+
/**
* Copy the provided ranges to the clipboard
* @paramranges The ranges to copy
diff --git a/packages/iris-grid/src/IrisGridCopyHandler.test.tsx b/packages/iris-grid/src/IrisGridCopyHandler.test.tsx
index d8b9ebf546..cb4bab3d91 100644
--- a/packages/iris-grid/src/IrisGridCopyHandler.test.tsx
+++ b/packages/iris-grid/src/IrisGridCopyHandler.test.tsx
@@ -5,7 +5,12 @@ import { GridTestUtils } from '@deephaven/grid';
import { copyToClipboard } from '@deephaven/utils';
import dh from '@deephaven/jsapi-shim';
import IrisGridTestUtils from './IrisGridTestUtils';
-import IrisGridCopyHandler, { CopyOperation } from './IrisGridCopyHandler';
+import IrisGridCopyHandler, {
+ CopyOperation,
+ CopyHeaderOperation,
+ CopyRangesOperation,
+} from './IrisGridCopyHandler';
+import IrisGridProxyModel from './IrisGridProxyModel';
jest.mock('@deephaven/utils', () => ({
...jest.requireActual('@deephaven/utils'),
@@ -29,12 +34,12 @@ function makeSnapshotFn() {
return jest.fn(() => Promise.resolve(DEFAULT_EXPECTED_TEXT));
}
-function makeCopyOperation(
+function makeCopyRangesOperation(
ranges = GridTestUtils.makeRanges(),
includeHeaders = false,
movedColumns = [],
userColumnWidths = IrisGridTestUtils.makeUserColumnWidths()
-): CopyOperation {
+): CopyRangesOperation {
return {
ranges,
includeHeaders,
@@ -43,16 +48,29 @@ function makeCopyOperation(
};
}
+function makeCopyHeaderOperation(
+ columnIndex = 0,
+ columnDepth = 0,
+ movedColumns = []
+): CopyHeaderOperation {
+ return {
+ columnIndex,
+ columnDepth,
+ movedColumns,
+ };
+}
+
function makeModel() {
const model = irisGridTestUtils.makeModel();
model.textSnapshot = makeSnapshotFn();
+ model.textForColumnHeader = jest.fn((c: number) => c.toString());
return model;
}
function mountCopySelection({
model = makeModel(),
- copyOperation = makeCopyOperation(),
-} = {}) {
+ copyOperation = makeCopyRangesOperation(),
+}: { model?: IrisGridProxyModel; copyOperation?: CopyOperation } = {}) {
return render(
);
@@ -66,9 +84,20 @@ it('renders without crashing', () => {
mountCopySelection();
});
+it('copies column header', async () => {
+ const copyOperation = makeCopyHeaderOperation();
+ const model = makeModel();
+ mountCopySelection({ copyOperation, model });
+ screen.getByRole('progressbar', { hidden: true });
+ screen.getByText('Fetching header for clipboard...');
+ expect(model.textForColumnHeader).toHaveBeenCalled();
+
+ await waitFor(() => expect(copyToClipboard).toHaveBeenCalledWith('0'));
+});
+
it('copies immediately if less than 10,000 rows of data', async () => {
const ranges = GridTestUtils.makeRanges(1, 10000);
- const copyOperation = makeCopyOperation(ranges);
+ const copyOperation = makeCopyRangesOperation(ranges);
const model = makeModel();
mountCopySelection({ copyOperation, model });
screen.getByRole('progressbar', { hidden: true });
@@ -84,7 +113,7 @@ it('prompts to copy if more than 10,000 rows of data', async () => {
const user = userEvent.setup({ delay: null });
const model = makeModel();
const ranges = GridTestUtils.makeRanges(1, 10001);
- const copyOperation = makeCopyOperation(ranges);
+ const copyOperation = makeCopyRangesOperation(ranges);
mountCopySelection({ copyOperation, model });
const copyBtn = screen.getByText('Copy');
expect(copyBtn).toBeTruthy();
@@ -112,7 +141,7 @@ it('shows click to copy if async copy fails', async () => {
mockedCopyToClipboard.mockReturnValueOnce(Promise.reject(error));
const ranges = GridTestUtils.makeRanges();
- const copyOperation = makeCopyOperation(ranges);
+ const copyOperation = makeCopyRangesOperation(ranges);
mountCopySelection({ copyOperation });
await waitFor(() =>
@@ -138,7 +167,7 @@ it('shows click to copy if async copy fails', async () => {
it('retry option available if fetching fails', async () => {
const user = userEvent.setup({ delay: null });
const ranges = GridTestUtils.makeRanges();
- const copyOperation = makeCopyOperation(ranges);
+ const copyOperation = makeCopyRangesOperation(ranges);
const model = makeModel();
model.textSnapshot = jest.fn(() => Promise.reject());
@@ -166,7 +195,7 @@ it('shows an error if the copy fails permissions', async () => {
mockedCopyToClipboard.mockReturnValueOnce(Promise.reject(error));
const ranges = GridTestUtils.makeRanges();
- const copyOperation = makeCopyOperation(ranges);
+ const copyOperation = makeCopyRangesOperation(ranges);
mountCopySelection({ copyOperation });
await waitFor(() =>
diff --git a/packages/iris-grid/src/IrisGridCopyHandler.tsx b/packages/iris-grid/src/IrisGridCopyHandler.tsx
index f87b87b1af..0310b82ec6 100644
--- a/packages/iris-grid/src/IrisGridCopyHandler.tsx
+++ b/packages/iris-grid/src/IrisGridCopyHandler.tsx
@@ -27,15 +27,37 @@ type Values = T[keyof T];
type ButtonStateType = Values;
-export type CopyOperation = {
+type CommonCopyOperation = {
+ movedColumns: readonly MoveOperation[];
+ error?: string;
+};
+
+export type CopyRangesOperation = CommonCopyOperation & {
ranges: readonly GridRange[];
includeHeaders: boolean;
formatValues?: boolean;
- movedColumns: readonly MoveOperation[];
userColumnWidths: ModelSizeMap;
- error?: string;
};
+export type CopyHeaderOperation = CommonCopyOperation & {
+ columnIndex: number;
+ columnDepth: number;
+};
+
+export type CopyOperation = CopyRangesOperation | CopyHeaderOperation;
+
+function isCopyRangesOperation(
+ copyOperation: CopyOperation
+): copyOperation is CopyRangesOperation {
+ return (copyOperation as CopyRangesOperation).ranges != null;
+}
+
+function isCopyHeaderOperation(
+ copyOperation: CopyOperation
+): copyOperation is CopyHeaderOperation {
+ return (copyOperation as CopyHeaderOperation).columnIndex != null;
+}
+
interface IrisGridCopyHandlerProps {
model: IrisGridModel;
copyOperation: CopyOperation;
@@ -75,8 +97,11 @@ class IrisGridCopyHandler extends Component<
// Large copy operation, confirmation required
CONFIRMATION_REQUIRED: 'CONFIRMATION_REQUIRED',
- // Fetch is currently in progress
- FETCH_IN_PROGRESS: 'FETCH_IN_PROGRESS',
+ // Fetch is currently in progress for copy ranges operation
+ FETCH_RANGES_IN_PROGRESS: 'FETCH_RANGES_IN_PROGRESS',
+
+ // Fetch is currently in progress for copy header operation
+ FETCH_HEADER_IN_PROGRESS: 'FETCH_HEADER_IN_PROGRESS',
// There was an error fetching the data
FETCH_ERROR: 'FETCH_ERROR',
@@ -111,8 +136,10 @@ class IrisGridCopyHandler extends Component<
return `Fetched ${rowCount.toLocaleString()} rows!`;
case IrisGridCopyHandler.COPY_STATES.FETCH_ERROR:
return 'Unable to copy data.';
- case IrisGridCopyHandler.COPY_STATES.FETCH_IN_PROGRESS:
+ case IrisGridCopyHandler.COPY_STATES.FETCH_RANGES_IN_PROGRESS:
return `Fetching ${rowCount.toLocaleString()} rows for clipboard...`;
+ case IrisGridCopyHandler.COPY_STATES.FETCH_HEADER_IN_PROGRESS:
+ return 'Fetching header for clipboard...';
case IrisGridCopyHandler.COPY_STATES.DONE:
return 'Copied to Clipboard!';
default:
@@ -186,7 +213,7 @@ class IrisGridCopyHandler extends Component<
return;
}
- const { ranges, error } = copyOperation;
+ const { error } = copyOperation;
if (error != null) {
log.debug('Showing copy error', error);
this.setState({
@@ -198,18 +225,23 @@ class IrisGridCopyHandler extends Component<
return;
}
- const rowCount = GridRange.rowCount(ranges);
+ this.setState({ isShown: true, error: undefined });
- this.setState({ rowCount, isShown: true, error: undefined });
+ if (isCopyRangesOperation(copyOperation)) {
+ const { ranges } = copyOperation;
+ const rowCount = GridRange.rowCount(ranges);
+ this.setState({ rowCount });
- if (rowCount > IrisGridCopyHandler.NO_PROMPT_THRESHOLD) {
- this.setState({
- buttonState: IrisGridCopyHandler.BUTTON_STATES.COPY,
- copyState: IrisGridCopyHandler.COPY_STATES.CONFIRMATION_REQUIRED,
- });
- } else {
- this.startFetch();
+ if (rowCount > IrisGridCopyHandler.NO_PROMPT_THRESHOLD) {
+ this.setState({
+ buttonState: IrisGridCopyHandler.BUTTON_STATES.COPY,
+ copyState: IrisGridCopyHandler.COPY_STATES.CONFIRMATION_REQUIRED,
+ });
+ return;
+ }
}
+
+ this.startFetch();
}
stopCopy(): void {
@@ -276,41 +308,65 @@ class IrisGridCopyHandler extends Component<
async startFetch(): Promise {
this.stopFetch();
- this.setState({
- buttonState: IrisGridCopyHandler.BUTTON_STATES.FETCH_IN_PROGRESS,
- copyState: IrisGridCopyHandler.COPY_STATES.FETCH_IN_PROGRESS,
- });
-
const { model, copyOperation } = this.props;
- const {
- ranges,
- includeHeaders,
- userColumnWidths,
- movedColumns,
- formatValues,
- } = copyOperation;
- log.debug('startFetch', ranges);
-
- const hiddenColumns = IrisGridUtils.getHiddenColumns(userColumnWidths);
- let modelRanges = GridUtils.getModelRanges(ranges, movedColumns);
- if (hiddenColumns.length > 0) {
- const subtractRanges = hiddenColumns.map(GridRange.makeColumn);
- modelRanges = GridRange.subtractRangesFromRanges(
- modelRanges,
- subtractRanges
+
+ if (isCopyHeaderOperation(copyOperation)) {
+ const { columnIndex, columnDepth, movedColumns } = copyOperation;
+ log.debug('startFetch copyHeader', columnIndex, columnDepth);
+
+ this.setState({
+ buttonState: IrisGridCopyHandler.BUTTON_STATES.FETCH_IN_PROGRESS,
+ copyState: IrisGridCopyHandler.COPY_STATES.FETCH_HEADER_IN_PROGRESS,
+ });
+
+ const modelIndex = GridUtils.getModelIndex(columnIndex, movedColumns);
+ const copyText = model.textForColumnHeader(modelIndex, columnDepth);
+ if (copyText === undefined) {
+ this.fetchPromise = undefined;
+ this.setState({
+ error: 'Invalid column header selected.',
+ copyState: IrisGridCopyHandler.COPY_STATES.DONE,
+ });
+ return;
+ }
+ this.fetchPromise = PromiseUtils.makeCancelable(copyText);
+ } else {
+ const {
+ ranges,
+ includeHeaders,
+ userColumnWidths,
+ movedColumns,
+ formatValues,
+ } = copyOperation;
+ log.debug('startFetch copyRanges', ranges);
+
+ this.setState({
+ buttonState: IrisGridCopyHandler.BUTTON_STATES.FETCH_IN_PROGRESS,
+ copyState: IrisGridCopyHandler.COPY_STATES.FETCH_RANGES_IN_PROGRESS,
+ });
+
+ const hiddenColumns = IrisGridUtils.getHiddenColumns(userColumnWidths);
+ let modelRanges = GridUtils.getModelRanges(ranges, movedColumns);
+ if (hiddenColumns.length > 0) {
+ const subtractRanges = hiddenColumns.map(GridRange.makeColumn);
+ modelRanges = GridRange.subtractRangesFromRanges(
+ modelRanges,
+ subtractRanges
+ );
+ }
+
+ // Remove the hidden columns from the snapshot
+ const formatValue =
+ formatValues != null && formatValues
+ ? (value: unknown, column: Column) =>
+ model.displayString(value, column.type, column.name)
+ : (value: unknown) => `${value}`;
+
+ this.fetchPromise = PromiseUtils.makeCancelable(
+ model.textSnapshot(modelRanges, includeHeaders, formatValue)
);
}
- // Remove the hidden columns from the snapshot
- const formatValue =
- formatValues != null && formatValues
- ? (value: unknown, column: Column) =>
- model.displayString(value, column.type, column.name)
- : (value: unknown) => `${value}`;
-
- this.fetchPromise = PromiseUtils.makeCancelable(
- model.textSnapshot(modelRanges, includeHeaders, formatValue)
- );
try {
const text = await this.fetchPromise;
this.fetchPromise = undefined;
diff --git a/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.tsx b/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.tsx
index 5833f4096b..8762e0c074 100644
--- a/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.tsx
+++ b/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.tsx
@@ -337,6 +337,7 @@ class IrisGridContextMenuHandler extends GridMouseHandler {
actions.push({
title: 'Copy Column Name',
group: IrisGridContextMenuHandler.GROUP_COPY,
+ shortcutText: ContextActionUtils.isMacPlatform() ? '⌥Click' : 'Alt+Click',
action: () => {
copyToClipboard(model.textForColumnHeader(modelIndex) ?? '').catch(e =>
log.error('Unable to copy header', e)
@@ -632,6 +633,9 @@ class IrisGridContextMenuHandler extends GridMouseHandler {
actions.push({
title: 'Copy Cell',
group: IrisGridContextMenuHandler.GROUP_COPY,
+ shortcutText: ContextActionUtils.isMacPlatform()
+ ? '⌥Click'
+ : 'Alt+Click',
order: 10,
action: () => {
irisGrid.copyCell(columnIndex, rowIndex);
diff --git a/packages/iris-grid/src/mousehandlers/IrisGridCopyCellMouseHandler.ts b/packages/iris-grid/src/mousehandlers/IrisGridCopyCellMouseHandler.ts
new file mode 100644
index 0000000000..8359dd639c
--- /dev/null
+++ b/packages/iris-grid/src/mousehandlers/IrisGridCopyCellMouseHandler.ts
@@ -0,0 +1,66 @@
+import {
+ Grid,
+ GridMouseHandler,
+ GridPoint,
+ EventHandlerResult,
+ GridMouseEvent,
+ GridRange,
+} from '@deephaven/grid';
+import { ContextActionUtils } from '@deephaven/components';
+import IrisGrid from '../IrisGrid';
+
+class IrisGridCopyCellMouseHandler extends GridMouseHandler {
+ private irisGrid: IrisGrid;
+
+ constructor(irisGrid: IrisGrid) {
+ super(250);
+
+ this.irisGrid = irisGrid;
+ this.cursor = null;
+ }
+
+ onClick(
+ gridPoint: GridPoint,
+ grid: Grid,
+ event: GridMouseEvent
+ ): EventHandlerResult {
+ if (
+ event.altKey &&
+ !ContextActionUtils.isModifierKeyDown(event) &&
+ !event.shiftKey
+ ) {
+ this.cursor = null;
+ if (gridPoint.columnHeaderDepth !== undefined) {
+ this.irisGrid.copyColumnHeader(
+ gridPoint.column,
+ gridPoint.columnHeaderDepth
+ );
+ } else {
+ this.irisGrid.copyRanges([
+ GridRange.makeCell(gridPoint.column, gridPoint.row),
+ ]);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ onMove(
+ gridPoint: GridPoint,
+ _grid: Grid,
+ event: GridMouseEvent
+ ): EventHandlerResult {
+ if (
+ event.altKey &&
+ !ContextActionUtils.isModifierKeyDown(event) &&
+ !event.shiftKey &&
+ gridPoint.column != null &&
+ (gridPoint.row != null || gridPoint.columnHeaderDepth != null)
+ ) {
+ this.cursor = this.irisGrid.props.copyCursor;
+ return true;
+ }
+ return false;
+ }
+}
+export default IrisGridCopyCellMouseHandler;
diff --git a/packages/iris-grid/src/mousehandlers/index.ts b/packages/iris-grid/src/mousehandlers/index.ts
index 95c2faf112..a0bcb101a8 100644
--- a/packages/iris-grid/src/mousehandlers/index.ts
+++ b/packages/iris-grid/src/mousehandlers/index.ts
@@ -2,6 +2,7 @@ export { default as IrisGridCellOverflowMouseHandler } from './IrisGridCellOverf
export { default as IrisGridColumnSelectMouseHandler } from './IrisGridColumnSelectMouseHandler';
export { default as IrisGridColumnTooltipMouseHandler } from './IrisGridColumnTooltipMouseHandler';
export { default as IrisGridContextMenuHandler } from './IrisGridContextMenuHandler';
+export { default as IrisGridCopyCellMouseHandler } from './IrisGridCopyCellMouseHandler';
export { default as IrisGridDataSelectMouseHandler } from './IrisGridDataSelectMouseHandler';
export { default as IrisGridFilterMouseHandler } from './IrisGridFilterMouseHandler';
export { default as IrisGridRowTreeMouseHandler } from './IrisGridRowTreeMouseHandler';