Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/vs/base/browser/ui/table/table.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
position: relative;
padding-right: 14px;
}

.monaco-table-th.sort-ascending::after,
.monaco-table-th.sort-descending::after {
font-family: codicon;
font-size: 10px;
position: absolute;
right: 2px;
opacity: 0.8;
}

.monaco-table-th.sort-ascending::after {
content: '\eaa1'; /* codicon-arrow-up */
}

.monaco-table-th.sort-descending::after {
content: '\ea9a'; /* codicon-arrow-down */
}

.monaco-table-th,
Expand Down
10 changes: 10 additions & 0 deletions src/vs/base/browser/ui/table/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ export interface ITableTouchEvent<TRow> extends IListTouchEvent<TRow> { }
export interface ITableGestureEvent<TRow> extends IListGestureEvent<TRow> { }
export interface ITableContextMenuEvent<TRow> extends IListContextMenuEvent<TRow> { }

export const enum SortOrder {
Ascending,
Descending
}

export interface ITableSortState {
readonly columnIndex: number;
readonly sortOrder: SortOrder;
}

export class TableError extends Error {

constructor(user: string, message: string) {
Expand Down
32 changes: 30 additions & 2 deletions src/vs/base/browser/ui/table/tableWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { $, append, clearNode, getContentHeight, getContentWidth } from '../../dom.js';
import { $, addDisposableListener, append, clearNode, EventType, getContentHeight, getContentWidth } from '../../dom.js';
import { createStyleSheet } from '../../domStylesheets.js';
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
import { IListElementRenderDetails, IListRenderer, IListVirtualDelegate } from '../list/list.js';
import { IListOptions, IListOptionsUpdate, IListStyles, List, unthemedListStyles } from '../list/listWidget.js';
import { ISplitViewDescriptor, IView, Orientation, SplitView } from '../splitview/splitview.js';
import { ITableColumn, ITableContextMenuEvent, ITableEvent, ITableGestureEvent, ITableMouseEvent, ITableRenderer, ITableTouchEvent, ITableVirtualDelegate } from './table.js';
import { ITableColumn, ITableContextMenuEvent, ITableEvent, ITableGestureEvent, ITableMouseEvent, ITableRenderer, ITableSortState, ITableTouchEvent, ITableVirtualDelegate, SortOrder } from './table.js';
import { Emitter, Event } from '../../../common/event.js';
import { Disposable, DisposableStore, IDisposable } from '../../../common/lifecycle.js';
import { ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js';
Expand Down Expand Up @@ -129,6 +129,9 @@ class ColumnHeader<TRow, TCell> extends Disposable implements IView {
private _onDidLayout = this._register(new Emitter<[number, number]>());
readonly onDidLayout = this._onDidLayout.event;

private _onDidClick = this._register(new Emitter<number>());
readonly onDidClick = this._onDidClick.event;

constructor(readonly column: ITableColumn<TRow, TCell>, private index: number) {
super();

Expand All @@ -137,6 +140,8 @@ class ColumnHeader<TRow, TCell> extends Disposable implements IView {
if (column.tooltip) {
this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, column.tooltip));
}

this._register(addDisposableListener(this.element, EventType.CLICK, () => this._onDidClick.fire(this.index)));
}

layout(size: number): void {
Expand All @@ -156,12 +161,16 @@ export class Table<TRow> implements ISpliceable<TRow>, IDisposable {
readonly domNode: HTMLElement;
private splitview: SplitView;
private list: List<TRow>;
private columnHeaders: ColumnHeader<TRow, TCell>[];
private styleElement: HTMLStyleElement;
protected readonly disposables = new DisposableStore();

private cachedWidth: number = 0;
private cachedHeight: number = 0;

private _onDidClickColumn = this.disposables.add(new Emitter<number>());
readonly onDidClickColumn = this._onDidClickColumn.event;

get onDidChangeFocus(): Event<ITableEvent<TRow>> { return this.list.onDidChangeFocus; }
get onDidChangeSelection(): Event<ITableEvent<TRow>> { return this.list.onDidChangeSelection; }

Expand Down Expand Up @@ -201,6 +210,7 @@ export class Table<TRow> implements ISpliceable<TRow>, IDisposable {
this.domNode = append(container, $(`.monaco-table.${this.domId}`));

const headers = columns.map((c, i) => this.disposables.add(new ColumnHeader(c, i)));
this.columnHeaders = headers;
const descriptor: ISplitViewDescriptor = {
size: headers.reduce((a, b) => a + b.column.weight, 0),
views: headers.map(view => ({ size: view.column.weight, view }))
Expand All @@ -222,6 +232,9 @@ export class Table<TRow> implements ISpliceable<TRow>, IDisposable {
Event.any(...headers.map(h => h.onDidLayout))
(([index, size]) => renderer.layoutColumn(index, size), null, this.disposables);

Event.any(...headers.map(h => h.onDidClick))
(index => this._onDidClickColumn.fire(index), null, this.disposables);

this.splitview.onDidSashReset(index => {
const totalWeight = columns.reduce((r, c) => r + c.weight, 0);
const size = columns[index].weight / totalWeight * this.cachedWidth;
Expand All @@ -236,6 +249,21 @@ export class Table<TRow> implements ISpliceable<TRow>, IDisposable {
return this.columns.map(c => c.label);
}

setSortColumn(sortState: ITableSortState | undefined): void {
for (const header of this.columnHeaders) {
header.element.classList.remove('sort-ascending', 'sort-descending');
header.element.removeAttribute('aria-sort');
}
if (sortState) {
const header = this.columnHeaders[sortState.columnIndex];
if (header) {
const ascending = sortState.sortOrder === SortOrder.Ascending;
header.element.classList.add(ascending ? 'sort-ascending' : 'sort-descending');
header.element.setAttribute('aria-sort', ascending ? 'ascending' : 'descending');
}
}
}

resizeColumn(index: number, percentage: number): void {
const size = Math.round((percentage / 100.00) * this.cachedWidth);
this.splitview.resizeView(index, size);
Expand Down
49 changes: 47 additions & 2 deletions src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/lis
import { WORKBENCH_BACKGROUND } from '../../../common/theme.js';
import { IKeybindingItemEntry, IKeybindingsEditorPane } from '../../../services/preferences/common/preferences.js';
import { keybindingsRecordKeysIcon, keybindingsSortIcon, keybindingsAddIcon, preferencesClearInputIcon, keybindingsEditIcon } from './preferencesIcons.js';
import { ITableRenderer, ITableVirtualDelegate } from '../../../../base/browser/ui/table/table.js';
import { ITableRenderer, ITableSortState, ITableVirtualDelegate, SortOrder } from '../../../../base/browser/ui/table/table.js';
import { KeybindingsEditorInput } from '../../../services/preferences/browser/keybindingsEditorInput.js';
import { IEditorOptions } from '../../../../platform/editor/common/editor.js';
import { ToolBar } from '../../../../base/browser/ui/toolbar/toolbar.js';
Expand Down Expand Up @@ -112,6 +112,7 @@ export class KeybindingsEditor extends EditorPane<IKeybindingsEditorMemento> imp

private readonly sortByPrecedenceAction: Action;
private readonly recordKeysAction: Action;
private columnSortState: ITableSortState | undefined;

private ariaLabelElement!: HTMLElement;
readonly overflowWidgetsDomNode: HTMLElement;
Expand Down Expand Up @@ -535,6 +536,7 @@ export class KeybindingsEditor extends EditorPane<IKeybindingsEditorMemento> imp
}
)) as WorkbenchTable<IKeybindingItemEntry>;

this._register(this.keybindingsTable.onDidClickColumn(columnIndex => this.onColumnHeaderClick(columnIndex)));
this._register(this.keybindingsTable.onContextMenu(e => this.onContextMenu(e)));
this._register(this.keybindingsTable.onDidChangeFocus(e => this.onFocusChange()));
this._register(this.keybindingsTable.onDidFocus(() => {
Expand Down Expand Up @@ -608,7 +610,21 @@ export class KeybindingsEditor extends EditorPane<IKeybindingsEditorMemento> imp
private renderKeybindingsEntries(reset: boolean, preserveFocus?: boolean): void {
if (this.keybindingsEditorModel) {
const filter = this.searchWidget.getValue();
const keybindingsEntries: IKeybindingItemEntry[] = this.keybindingsEditorModel.fetch(filter, this.sortByPrecedenceAction.checked);
let keybindingsEntries: IKeybindingItemEntry[] = this.keybindingsEditorModel.fetch(filter, this.sortByPrecedenceAction.checked);
if (this.columnSortState) {
const direction = this.columnSortState.sortOrder === SortOrder.Ascending ? 1 : -1;
const selector = KeybindingsEditor.getColumnSortValue(this.columnSortState.columnIndex);
if (selector) {
keybindingsEntries = keybindingsEntries.slice().sort((a, b) => {
const valA = selector(a);
const valB = selector(b);
if (valA && !valB) { return -1 * direction; }
if (!valA && valB) { return 1 * direction; }
if (!valA && !valB) { return 0; }
return valA.localeCompare(valB) * direction;
});
}
}
const ariaLabel = this.getAriaLabel(keybindingsEntries);
this.accessibilityService.alert(ariaLabel);
this.ariaLabelElement.textContent = ariaLabel;
Expand Down Expand Up @@ -721,6 +737,35 @@ export class KeybindingsEditor extends EditorPane<IKeybindingsEditorMemento> imp
this.sortByPrecedenceAction.checked = !this.sortByPrecedenceAction.checked;
}

private static readonly SORTABLE_COLUMNS = new Set([1, 2, 3, 4]); // Command, Keybinding, When, Source

private static getColumnSortValue(columnIndex: number): ((entry: IKeybindingItemEntry) => string) | undefined {
switch (columnIndex) {
case 1: return e => e.keybindingItem.commandLabel || e.keybindingItem.command;
case 2: return e => e.keybindingItem.keybinding?.getAriaLabel() ?? '';
case 3: return e => e.keybindingItem.when;
case 4: return e => isString(e.keybindingItem.source) ? e.keybindingItem.source : e.keybindingItem.source.displayName ?? e.keybindingItem.source.identifier.value;
default: return undefined;
}
}

private onColumnHeaderClick(columnIndex: number): void {
if (!KeybindingsEditor.SORTABLE_COLUMNS.has(columnIndex)) {
return;
}
if (this.columnSortState?.columnIndex === columnIndex) {
if (this.columnSortState.sortOrder === SortOrder.Ascending) {
this.columnSortState = { columnIndex, sortOrder: SortOrder.Descending };
} else {
this.columnSortState = undefined;
}
} else {
this.columnSortState = { columnIndex, sortOrder: SortOrder.Ascending };
}
this.keybindingsTable.setSortColumn(this.columnSortState);
this.renderKeybindingsEntries(false);
}

private onContextMenu(e: IListContextMenuEvent<IKeybindingItemEntry>): void {
if (!e.element) {
return;
Expand Down