Skip to content

Commit

Permalink
Introduce group-level selection and navigation for table (#140)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
martin-fleck-at authored Aug 8, 2024
1 parent b20f07c commit 4aa581b
Show file tree
Hide file tree
Showing 16 changed files with 845 additions and 139 deletions.
1 change: 1 addition & 0 deletions media/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
@import "./multi-select.css";
@import "./options-widget.css";
@import "./memory-table.css";
@import "./variable-decorations.css";
77 changes: 63 additions & 14 deletions media/memory-table.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@
white-space: nowrap;
}

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

/* == MoreMemorySelect == */
Expand Down Expand Up @@ -161,37 +162,85 @@
color: var(--vscode-debugTokenExpression-name);
}

/* == Data Edit == */
/* Cell Styles */

.byte-group {
font-family: var(--vscode-editor-font-family);
margin-right: 4px;
padding: 0 1px; /* we use this padding to balance out the 2px that are needed for the editing */
.p-datatable .p-datatable-tbody > tr > td[data-column="address"][role="cell"],
.p-datatable .p-datatable-tbody > tr > td[data-column="ascii"][role="cell"] {
padding: 0;
}

.p-datatable .p-datatable-tbody > tr > td[data-column="data"][role="cell"],
.p-datatable .p-datatable-tbody > tr > td[data-column="variables"][role="cell"] {
padding: 0 12px;
vertical-align: middle;
}

.byte-group:last-child {
margin-right: 0px;
/* Group Styles */

[role='group']:hover {
border-bottom: 0px;
outline: 1px solid var(--vscode-list-focusOutline);
}

[role='group'][data-group-selected='true'] {
background: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
outline: 1px solid var(--vscode-list-activeSelectionBackground);
}

.byte-group:has(> .data-edit) {
[role='group']:focus-visible,
[role='group']:focus {
outline: 1px solid var(--vscode-list-focusOutline);
}

[data-column="address"][role="group"],
[data-column="ascii"][role="group"] {
padding: 4px 12px;
display: flex;
outline-offset: -1px;
}

[data-column="data"][role="group"],
[data-column="variables"][role="group"] {
padding: 4px 1px;
line-height: 23.5px;
outline-offset: -1px;
}

/* Data Column */

[data-column="data"][role="group"] {
padding: 4px 2px; /* left-padding should match text-indent of data-edit */
}

/* == Data Edit == */

[data-column="data"][role="group"]:has(> .data-edit) {
outline: 1px solid var(--vscode-inputOption-activeBorder);
outline-offset: 1px;
padding: 0px; /* editing takes two more pixels cause the input field will cut off the characters otherwise. */
background: transparent;
padding-left: 0px; /* editing takes two more pixels cause the input field will cut off the characters otherwise. */
padding-right: 0px; /* editing takes two more pixels cause the input field will cut off the characters otherwise. */
}

.data-edit {
padding: 0;
outline: 0;
border: none;
text-indent: 1px;
text-indent: 2px;
min-height: unset;
height: 2ex;
background: unset;
margin: 0;
color: var(--vscode-editor-foreground) !important;
}

.data-edit:enabled:focus {
outline: none;
border: none;
text-indent: 1px;
text-indent: 2px;
}

.p-datatable .p-datatable-tbody > tr > td.p-highlight:has(>.selected) {
background: transparent;
outline: none;
}
44 changes: 44 additions & 0 deletions media/variable-decorations.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/********************************************************************************
* Copyright (C) 2024 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

/* Color wheel of variable decorations with 5 non-HC colors */
.variable-0 {
color: var(--vscode-terminal-ansiBlue);
}

.variable-1 {
color: var(--vscode-terminal-ansiGreen);
}

.variable-2 {
color: var(--vscode-terminal-ansiBrightRed);
}

.variable-3 {
color: var(--vscode-terminal-ansiYellow);
}

.variable-4 {
color: var(--vscode-terminal-ansiMagenta);
}

[role='group'][data-group-selected='true'] .variable-0,
[role='group'][data-group-selected='true'] .variable-1,
[role='group'][data-group-selected='true'] .variable-2,
[role='group'][data-group-selected='true'] .variable-3,
[role='group'][data-group-selected='true'] .variable-4 {
color: inherit;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
},
"dependencies": {
"@vscode/codicons": "^0.0.32",
"deepmerge": "^4.3.1",
"fast-deep-equal": "^3.1.3",
"formik": "^2.4.5",
"lodash": "^4.17.21",
Expand Down
33 changes: 33 additions & 0 deletions src/common/os.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/********************************************************************************
* Copyright (C) 2017 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

// from https://github.com/eclipse-theia/theia/blob/266fa0b2a9cf2649ed9b34c8b71b786806e787b4/packages/core/src/common/os.ts#L4

function is(userAgent: string, platform: string): boolean {
if (typeof navigator !== 'undefined') {
if (navigator.userAgent && navigator.userAgent.indexOf(userAgent) >= 0) {
return true;
}
}
if (typeof process !== 'undefined') {
return (process.platform === platform);
}
return false;
}

export const isWindows = is('Windows', 'win32');
export const isOSX = is('Mac', 'darwin');
export const isLinux = !isWindows && !isOSX;
21 changes: 14 additions & 7 deletions src/webview/columns/address-column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
********************************************************************************/

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

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

fittingType: ColumnFittingType = 'content-width';

render(range: BigIntMemoryRange, _: Memory, options: TableRenderOptions): ReactNode {
return <span className='memory-start-address hoverable' data-column='address'>
{options.showRadixPrefix && <span className='radix-prefix'>{getRadixMarker(options.addressRadix)}</span>}
<span className='address'>{getAddressString(range.startAddress, options.addressRadix, options.effectiveAddressLength)}</span>
render(columnIndex: number, row: MemoryRowData, config: ColumnRenderProps): ReactNode {
const selectionProps: SelectionProps = {
createSelection: (event, position) => createDefaultSelection(event, position, AddressColumn.ID, row),
getSelection: () => config.selection,
setSelection: config.setSelection
};
const groupProps = groupAttributes({ columnIndex, rowIndex: row.rowIndex, groupIndex: 0, maxGroupIndex: 0 }, selectionProps);
return <span className='memory-start-address hoverable' data-column='address' {...groupProps}>
{config.tableConfig.showRadixPrefix && <span className='radix-prefix'>{getRadixMarker(config.tableConfig.addressRadix)}</span>}
<span className='address'>{getAddressString(row.startAddress, config.tableConfig.addressRadix, config.tableConfig.effectiveAddressLength)}</span>
</span>;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

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

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

export class AsciiColumn implements ColumnContribution {
readonly id = manifest.CONFIG_SHOW_ASCII_COLUMN;
static ID = manifest.CONFIG_SHOW_ASCII_COLUMN;
readonly id = AsciiColumn.ID;
readonly label = 'ASCII';
readonly priority = 3;
render(range: BigIntMemoryRange, memory: Memory, options: TableRenderOptions): ReactNode {
const mauSize = options.bytesPerMau * 8;
const startOffset = toOffset(memory.address, range.startAddress, mauSize);
const endOffset = toOffset(memory.address, range.endAddress, mauSize);

render(columnIndex: number, row: MemoryRowData, config: ColumnRenderProps): ReactNode {
const selectionProps: SelectionProps = {
createSelection: (event, position) => createDefaultSelection(event, position, AsciiColumn.ID, row),
getSelection: () => config.selection,
setSelection: config.setSelection
};
const groupProps = groupAttributes({ columnIndex, rowIndex: row.rowIndex, groupIndex: 0, maxGroupIndex: 0 }, selectionProps);
const mauSize = config.tableConfig.bytesPerMau * 8;
const startOffset = toOffset(config.memory.address, row.startAddress, mauSize);
const endOffset = toOffset(config.memory.address, row.endAddress, mauSize);
let result = '';
for (let i = startOffset; i < endOffset; i++) {
result += getASCIIForSingleByte(memory.bytes[i]);
result += getASCIIForSingleByte(config.memory.bytes[i]);
}
return result;
return <span data-column='ascii' className='ascii' {...groupProps}>{result}</span>;
}
}
14 changes: 12 additions & 2 deletions src/webview/columns/column-contribution-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,23 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

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

export type ColumnFittingType = 'content-width';

export interface ColumnRenderProps {
memory: Memory;
tableConfig: TableRenderOptions;
groupsPerRowToRender: number;
selection?: MemoryTableSelection;
setSelection: (selection?: MemoryTableSelection) => void;
}

export interface ColumnContribution {
readonly id: string;
readonly className?: string;
Expand All @@ -30,7 +39,8 @@ export interface ColumnContribution {
fittingType?: ColumnFittingType;
/** Sorted low to high. If omitted, sorted alphabetically by ID after all contributions with numbers. */
priority?: number;
render(range: BigIntMemoryRange, memory: Memory, options: TableRenderOptions): React.ReactNode
pt?(columnIndex: number, state: MemoryTableState): ColumnPassThroughOptions;
render(columnIdx: number, row: MemoryRowData, config: ColumnRenderProps): React.ReactNode
/** Called when fetching new memory or when activating the column. */
fetchData?(currentViewParameters: ReadMemoryArguments): Promise<void>;
/** Called when the user reveals the column */
Expand Down
Loading

0 comments on commit 4aa581b

Please sign in to comment.