Skip to content

Commit 4aa581b

Browse files
Introduce group-level selection and navigation for table (#140)
## What it does - Disable row and cell navigation and selection - Introduce custom selection and navigation handling based on groups - Select a group by hitting 'Enter' - Edit a group by hitting 'Space' (only available for data groups) - Support copying data from the groups using Ctrl+C and Ctrl+X - Only allow single selection for now ## PR Feedback - Improve keyboard navigation to skip over non-existing groups - Copy should always use the focused group and not the selected one - Fix issue of losing focus when submitting a data edit - Support pasting values in data cells without going into edit mode - Improve styling in all themes -- Avoid orange focus outline and use same as tree for visibility -- Selected cells get their colors overwritten -- Slightly increase padding between two data groups ## PR Feedback - Ensure shortcuts work on MacOS
1 parent b20f07c commit 4aa581b

16 files changed

Lines changed: 845 additions & 139 deletions

media/index.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@
2020
@import "./multi-select.css";
2121
@import "./options-widget.css";
2222
@import "./memory-table.css";
23+
@import "./variable-decorations.css";

media/memory-table.css

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@
2727
white-space: nowrap;
2828
}
2929

30-
.memory-inspector-table .column-data .byte-group.editable:hover {
31-
border-bottom: 1px dotted var(--vscode-editorHoverWidget-border);
30+
.p-datatable :focus-visible {
31+
outline-style: dotted;
32+
outline-offset: -1px;
3233
}
3334

3435
/* == MoreMemorySelect == */
@@ -161,37 +162,85 @@
161162
color: var(--vscode-debugTokenExpression-name);
162163
}
163164

164-
/* == Data Edit == */
165+
/* Cell Styles */
165166

166-
.byte-group {
167-
font-family: var(--vscode-editor-font-family);
168-
margin-right: 4px;
169-
padding: 0 1px; /* we use this padding to balance out the 2px that are needed for the editing */
167+
.p-datatable .p-datatable-tbody > tr > td[data-column="address"][role="cell"],
168+
.p-datatable .p-datatable-tbody > tr > td[data-column="ascii"][role="cell"] {
169+
padding: 0;
170+
}
171+
172+
.p-datatable .p-datatable-tbody > tr > td[data-column="data"][role="cell"],
173+
.p-datatable .p-datatable-tbody > tr > td[data-column="variables"][role="cell"] {
174+
padding: 0 12px;
175+
vertical-align: middle;
170176
}
171177

172-
.byte-group:last-child {
173-
margin-right: 0px;
178+
/* Group Styles */
179+
180+
[role='group']:hover {
181+
border-bottom: 0px;
182+
outline: 1px solid var(--vscode-list-focusOutline);
183+
}
184+
185+
[role='group'][data-group-selected='true'] {
186+
background: var(--vscode-list-activeSelectionBackground);
187+
color: var(--vscode-list-activeSelectionForeground);
188+
outline: 1px solid var(--vscode-list-activeSelectionBackground);
174189
}
175190

176-
.byte-group:has(> .data-edit) {
191+
[role='group']:focus-visible,
192+
[role='group']:focus {
193+
outline: 1px solid var(--vscode-list-focusOutline);
194+
}
195+
196+
[data-column="address"][role="group"],
197+
[data-column="ascii"][role="group"] {
198+
padding: 4px 12px;
199+
display: flex;
200+
outline-offset: -1px;
201+
}
202+
203+
[data-column="data"][role="group"],
204+
[data-column="variables"][role="group"] {
205+
padding: 4px 1px;
206+
line-height: 23.5px;
207+
outline-offset: -1px;
208+
}
209+
210+
/* Data Column */
211+
212+
[data-column="data"][role="group"] {
213+
padding: 4px 2px; /* left-padding should match text-indent of data-edit */
214+
}
215+
216+
/* == Data Edit == */
217+
218+
[data-column="data"][role="group"]:has(> .data-edit) {
177219
outline: 1px solid var(--vscode-inputOption-activeBorder);
178-
outline-offset: 1px;
179-
padding: 0px; /* editing takes two more pixels cause the input field will cut off the characters otherwise. */
220+
background: transparent;
221+
padding-left: 0px; /* editing takes two more pixels cause the input field will cut off the characters otherwise. */
222+
padding-right: 0px; /* editing takes two more pixels cause the input field will cut off the characters otherwise. */
180223
}
181224

182225
.data-edit {
183226
padding: 0;
184227
outline: 0;
185228
border: none;
186-
text-indent: 1px;
229+
text-indent: 2px;
187230
min-height: unset;
188231
height: 2ex;
189232
background: unset;
190233
margin: 0;
234+
color: var(--vscode-editor-foreground) !important;
191235
}
192236

193237
.data-edit:enabled:focus {
194238
outline: none;
195239
border: none;
196-
text-indent: 1px;
240+
text-indent: 2px;
197241
}
242+
243+
.p-datatable .p-datatable-tbody > tr > td.p-highlight:has(>.selected) {
244+
background: transparent;
245+
outline: none;
246+
}

media/variable-decorations.css

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/********************************************************************************
2+
* Copyright (C) 2024 Ericsson and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the Eclipse
10+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
* with the GNU Classpath Exception which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
********************************************************************************/
16+
17+
/* Color wheel of variable decorations with 5 non-HC colors */
18+
.variable-0 {
19+
color: var(--vscode-terminal-ansiBlue);
20+
}
21+
22+
.variable-1 {
23+
color: var(--vscode-terminal-ansiGreen);
24+
}
25+
26+
.variable-2 {
27+
color: var(--vscode-terminal-ansiBrightRed);
28+
}
29+
30+
.variable-3 {
31+
color: var(--vscode-terminal-ansiYellow);
32+
}
33+
34+
.variable-4 {
35+
color: var(--vscode-terminal-ansiMagenta);
36+
}
37+
38+
[role='group'][data-group-selected='true'] .variable-0,
39+
[role='group'][data-group-selected='true'] .variable-1,
40+
[role='group'][data-group-selected='true'] .variable-2,
41+
[role='group'][data-group-selected='true'] .variable-3,
42+
[role='group'][data-group-selected='true'] .variable-4 {
43+
color: inherit;
44+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
},
3333
"dependencies": {
3434
"@vscode/codicons": "^0.0.32",
35+
"deepmerge": "^4.3.1",
3536
"fast-deep-equal": "^3.1.3",
3637
"formik": "^2.4.5",
3738
"lodash": "^4.17.21",

src/common/os.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/********************************************************************************
2+
* Copyright (C) 2017 TypeFox and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the Eclipse
10+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
* with the GNU Classpath Exception which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
********************************************************************************/
16+
17+
// from https://github.com/eclipse-theia/theia/blob/266fa0b2a9cf2649ed9b34c8b71b786806e787b4/packages/core/src/common/os.ts#L4
18+
19+
function is(userAgent: string, platform: string): boolean {
20+
if (typeof navigator !== 'undefined') {
21+
if (navigator.userAgent && navigator.userAgent.indexOf(userAgent) >= 0) {
22+
return true;
23+
}
24+
}
25+
if (typeof process !== 'undefined') {
26+
return (process.platform === platform);
27+
}
28+
return false;
29+
}
30+
31+
export const isWindows = is('Windows', 'win32');
32+
export const isOSX = is('Mac', 'darwin');
33+
export const isLinux = !isWindows && !isOSX;

src/webview/columns/address-column.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
********************************************************************************/
1616

1717
import React, { ReactNode } from 'react';
18-
import { Memory } from '../../common/memory';
19-
import { BigIntMemoryRange, getAddressString, getRadixMarker } from '../../common/memory-range';
20-
import { ColumnContribution, ColumnFittingType, TableRenderOptions } from './column-contribution-service';
18+
import { getAddressString, getRadixMarker } from '../../common/memory-range';
19+
import { MemoryRowData } from '../components/memory-table';
20+
import { ColumnContribution, ColumnFittingType, ColumnRenderProps } from './column-contribution-service';
21+
import { createDefaultSelection, groupAttributes, SelectionProps } from './table-group';
2122

2223
export class AddressColumn implements ColumnContribution {
2324
static ID = 'address';
@@ -28,10 +29,16 @@ export class AddressColumn implements ColumnContribution {
2829

2930
fittingType: ColumnFittingType = 'content-width';
3031

31-
render(range: BigIntMemoryRange, _: Memory, options: TableRenderOptions): ReactNode {
32-
return <span className='memory-start-address hoverable' data-column='address'>
33-
{options.showRadixPrefix && <span className='radix-prefix'>{getRadixMarker(options.addressRadix)}</span>}
34-
<span className='address'>{getAddressString(range.startAddress, options.addressRadix, options.effectiveAddressLength)}</span>
32+
render(columnIndex: number, row: MemoryRowData, config: ColumnRenderProps): ReactNode {
33+
const selectionProps: SelectionProps = {
34+
createSelection: (event, position) => createDefaultSelection(event, position, AddressColumn.ID, row),
35+
getSelection: () => config.selection,
36+
setSelection: config.setSelection
37+
};
38+
const groupProps = groupAttributes({ columnIndex, rowIndex: row.rowIndex, groupIndex: 0, maxGroupIndex: 0 }, selectionProps);
39+
return <span className='memory-start-address hoverable' data-column='address' {...groupProps}>
40+
{config.tableConfig.showRadixPrefix && <span className='radix-prefix'>{getRadixMarker(config.tableConfig.addressRadix)}</span>}
41+
<span className='address'>{getAddressString(row.startAddress, config.tableConfig.addressRadix, config.tableConfig.effectiveAddressLength)}</span>
3542
</span>;
3643
}
3744
}
Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616

17-
import { ReactNode } from 'react';
17+
import React, { ReactNode } from 'react';
1818
import * as manifest from '../../common/manifest';
19-
import { Memory } from '../../common/memory';
20-
import { BigIntMemoryRange, toOffset } from '../../common/memory-range';
21-
import { ColumnContribution, TableRenderOptions } from './column-contribution-service';
19+
import { toOffset } from '../../common/memory-range';
20+
import { MemoryRowData } from '../components/memory-table';
21+
import { ColumnContribution, ColumnRenderProps } from './column-contribution-service';
22+
import { createDefaultSelection, groupAttributes, SelectionProps } from './table-group';
2223

2324
function isPrintableAsAscii(input: number): boolean {
2425
return input >= 32 && input < (128 - 1);
@@ -30,17 +31,25 @@ function getASCIIForSingleByte(byte: number | undefined): string {
3031
}
3132

3233
export class AsciiColumn implements ColumnContribution {
33-
readonly id = manifest.CONFIG_SHOW_ASCII_COLUMN;
34+
static ID = manifest.CONFIG_SHOW_ASCII_COLUMN;
35+
readonly id = AsciiColumn.ID;
3436
readonly label = 'ASCII';
3537
readonly priority = 3;
36-
render(range: BigIntMemoryRange, memory: Memory, options: TableRenderOptions): ReactNode {
37-
const mauSize = options.bytesPerMau * 8;
38-
const startOffset = toOffset(memory.address, range.startAddress, mauSize);
39-
const endOffset = toOffset(memory.address, range.endAddress, mauSize);
38+
39+
render(columnIndex: number, row: MemoryRowData, config: ColumnRenderProps): ReactNode {
40+
const selectionProps: SelectionProps = {
41+
createSelection: (event, position) => createDefaultSelection(event, position, AsciiColumn.ID, row),
42+
getSelection: () => config.selection,
43+
setSelection: config.setSelection
44+
};
45+
const groupProps = groupAttributes({ columnIndex, rowIndex: row.rowIndex, groupIndex: 0, maxGroupIndex: 0 }, selectionProps);
46+
const mauSize = config.tableConfig.bytesPerMau * 8;
47+
const startOffset = toOffset(config.memory.address, row.startAddress, mauSize);
48+
const endOffset = toOffset(config.memory.address, row.endAddress, mauSize);
4049
let result = '';
4150
for (let i = startOffset; i < endOffset; i++) {
42-
result += getASCIIForSingleByte(memory.bytes[i]);
51+
result += getASCIIForSingleByte(config.memory.bytes[i]);
4352
}
44-
return result;
53+
return <span data-column='ascii' className='ascii' {...groupProps}>{result}</span>;
4554
}
4655
}

src/webview/columns/column-contribution-service.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,23 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616

17+
import { ColumnPassThroughOptions } from 'primereact/column';
1718
import type * as React from 'react';
1819
import { Memory } from '../../common/memory';
19-
import { BigIntMemoryRange } from '../../common/memory-range';
2020
import { ReadMemoryArguments } from '../../common/messaging';
21+
import { MemoryRowData, MemoryTableSelection, MemoryTableState } from '../components/memory-table';
2122
import type { Disposable, MemoryState, SerializedTableRenderOptions, UpdateExecutor } from '../utils/view-types';
2223

2324
export type ColumnFittingType = 'content-width';
2425

26+
export interface ColumnRenderProps {
27+
memory: Memory;
28+
tableConfig: TableRenderOptions;
29+
groupsPerRowToRender: number;
30+
selection?: MemoryTableSelection;
31+
setSelection: (selection?: MemoryTableSelection) => void;
32+
}
33+
2534
export interface ColumnContribution {
2635
readonly id: string;
2736
readonly className?: string;
@@ -30,7 +39,8 @@ export interface ColumnContribution {
3039
fittingType?: ColumnFittingType;
3140
/** Sorted low to high. If omitted, sorted alphabetically by ID after all contributions with numbers. */
3241
priority?: number;
33-
render(range: BigIntMemoryRange, memory: Memory, options: TableRenderOptions): React.ReactNode
42+
pt?(columnIndex: number, state: MemoryTableState): ColumnPassThroughOptions;
43+
render(columnIdx: number, row: MemoryRowData, config: ColumnRenderProps): React.ReactNode
3444
/** Called when fetching new memory or when activating the column. */
3545
fetchData?(currentViewParameters: ReadMemoryArguments): Promise<void>;
3646
/** Called when the user reveals the column */

0 commit comments

Comments
 (0)