Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { BrowserFeatures } from '../../../../base/browser/canIUse.js';
7
import * as DOM from '../../../../base/browser/dom.js';
8
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
9
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
10
import { Button } from '../../../../base/browser/ui/button/button.js';
11
import { applyDragImage } from '../../../../base/browser/ui/dnd/dnd.js';
12
import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js';
13
import { SelectBox } from '../../../../base/browser/ui/selectBox/selectBox.js';
14
import { Toggle, unthemedToggleStyles } from '../../../../base/browser/ui/toggle/toggle.js';
15
import { IAction } from '../../../../base/common/actions.js';
16
import { disposableTimeout } from '../../../../base/common/async.js';
17
import { Codicon } from '../../../../base/common/codicons.js';
18
import { Emitter, Event } from '../../../../base/common/event.js';
19
import { MarkdownString } from '../../../../base/common/htmlContent.js';
20
import { KeyCode } from '../../../../base/common/keyCodes.js';
21
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
22
import { isIOS } from '../../../../base/common/platform.js';
23
import { ThemeIcon } from '../../../../base/common/themables.js';
24
import { isDefined, isUndefinedOrNull } from '../../../../base/common/types.js';
25
import { localize } from '../../../../nls.js';
26
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
27
import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
28
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
29
import { defaultButtonStyles, getInputBoxStyle, getSelectBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';
30
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
31
import { hasNativeContextMenu } from '../../../../platform/window/common/window.js';
32
import { SettingValueType } from '../../../services/preferences/common/preferences.js';
33
import { settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js';
34
import './media/settingsWidgets.css';
35
import { settingsDiscardIcon, settingsEditIcon, settingsRemoveIcon } from './preferencesIcons.js';
36
37
const $ = DOM.$;
38
39
type EditKey = 'none' | 'create' | number;
40
41
type RowElementGroup = {
42
rowElement: HTMLElement;
43
keyElement: HTMLElement;
44
valueElement?: HTMLElement;
45
};
46
47
type IListViewItem<TDataItem extends object> = TDataItem & {
48
editing?: boolean;
49
selected?: boolean;
50
};
51
52
export class ListSettingListModel<TDataItem extends object> {
53
protected _dataItems: TDataItem[] = [];
54
private _editKey: EditKey | null = null;
55
private _selectedIdx: number | null = null;
56
private _newDataItem: TDataItem;
57
58
get items(): IListViewItem<TDataItem>[] {
59
const items = this._dataItems.map((item, i) => {
60
const editing = typeof this._editKey === 'number' && this._editKey === i;
61
return {
62
...item,
63
editing,
64
selected: i === this._selectedIdx || editing
65
};
66
});
67
68
if (this._editKey === 'create') {
69
items.push({
70
editing: true,
71
selected: true,
72
...this._newDataItem,
73
});
74
}
75
76
return items;
77
}
78
79
constructor(newItem: TDataItem) {
80
this._newDataItem = newItem;
81
}
82
83
setEditKey(key: EditKey): void {
84
this._editKey = key;
85
}
86
87
setValue(listData: TDataItem[]): void {
88
this._dataItems = listData;
89
}
90
91
select(idx: number | null): void {
92
this._selectedIdx = idx;
93
}
94
95
getSelected(): number | null {
96
return this._selectedIdx;
97
}
98
99
selectNext(): void {
100
if (typeof this._selectedIdx === 'number') {
101
this._selectedIdx = Math.min(this._selectedIdx + 1, this._dataItems.length - 1);
102
} else {
103
this._selectedIdx = 0;
104
}
105
}
106
107
selectPrevious(): void {
108
if (typeof this._selectedIdx === 'number') {
109
this._selectedIdx = Math.max(this._selectedIdx - 1, 0);
110
} else {
111
this._selectedIdx = 0;
112
}
113
}
114
}
115
116
export interface ISettingListChangeEvent<TDataItem extends object> {
117
type: 'change';
118
originalItem: TDataItem;
119
newItem: TDataItem;
120
targetIndex: number;
121
}
122
123
export interface ISettingListAddEvent<TDataItem extends object> {
124
type: 'add';
125
newItem: TDataItem;
126
targetIndex: number;
127
}
128
129
export interface ISettingListMoveEvent<TDataItem extends object> {
130
type: 'move';
131
originalItem: TDataItem;
132
newItem: TDataItem;
133
targetIndex: number;
134
sourceIndex: number;
135
}
136
137
export interface ISettingListRemoveEvent<TDataItem extends object> {
138
type: 'remove';
139
originalItem: TDataItem;
140
targetIndex: number;
141
}
142
143
export interface ISettingListResetEvent<TDataItem extends object> {
144
type: 'reset';
145
originalItem: TDataItem;
146
targetIndex: number;
147
}
148
149
export type SettingListEvent<TDataItem extends object> = ISettingListChangeEvent<TDataItem> | ISettingListAddEvent<TDataItem> | ISettingListMoveEvent<TDataItem> | ISettingListRemoveEvent<TDataItem> | ISettingListResetEvent<TDataItem>;
150
151
export abstract class AbstractListSettingWidget<TDataItem extends object> extends Disposable {
152
private listElement: HTMLElement;
153
private rowElements: HTMLElement[] = [];
154
155
protected readonly _onDidChangeList = this._register(new Emitter<SettingListEvent<TDataItem>>());
156
protected readonly model = new ListSettingListModel<TDataItem>(this.getEmptyItem());
157
protected readonly listDisposables = this._register(new DisposableStore());
158
159
readonly onDidChangeList: Event<SettingListEvent<TDataItem>> = this._onDidChangeList.event;
160
161
get domNode(): HTMLElement {
162
return this.listElement;
163
}
164
165
get items(): TDataItem[] {
166
return this.model.items;
167
}
168
169
protected get isReadOnly(): boolean {
170
return false;
171
}
172
173
constructor(
174
private container: HTMLElement,
175
@IThemeService protected readonly themeService: IThemeService,
176
@IContextViewService protected readonly contextViewService: IContextViewService,
177
@IConfigurationService protected readonly configurationService: IConfigurationService,
178
) {
179
super();
180
181
this.listElement = DOM.append(container, $('div'));
182
this.listElement.setAttribute('role', 'list');
183
this.getContainerClasses().forEach(c => this.listElement.classList.add(c));
184
DOM.append(container, this.renderAddButton());
185
this.renderList();
186
187
this._register(DOM.addDisposableListener(this.listElement, DOM.EventType.POINTER_DOWN, e => this.onListClick(e)));
188
this._register(DOM.addDisposableListener(this.listElement, DOM.EventType.DBLCLICK, e => this.onListDoubleClick(e)));
189
190
this._register(DOM.addStandardDisposableListener(this.listElement, 'keydown', (e: StandardKeyboardEvent) => {
191
if (e.equals(KeyCode.UpArrow)) {
192
this.selectPreviousRow();
193
} else if (e.equals(KeyCode.DownArrow)) {
194
this.selectNextRow();
195
} else {
196
return;
197
}
198
199
e.preventDefault();
200
e.stopPropagation();
201
}));
202
}
203
204
setValue(listData: TDataItem[]): void {
205
this.model.setValue(listData);
206
this.renderList();
207
}
208
209
abstract isItemNew(item: TDataItem): boolean;
210
protected abstract getEmptyItem(): TDataItem;
211
protected abstract getContainerClasses(): string[];
212
protected abstract getActionsForItem(item: TDataItem, idx: number): IAction[];
213
protected abstract renderItem(item: TDataItem, idx: number): RowElementGroup;
214
protected abstract renderEdit(item: TDataItem, idx: number): HTMLElement;
215
protected abstract addTooltipsToRow(rowElement: RowElementGroup, item: TDataItem): void;
216
protected abstract getLocalizedStrings(): {
217
deleteActionTooltip: string;
218
editActionTooltip: string;
219
addButtonLabel: string;
220
};
221
222
protected renderHeader(): HTMLElement | undefined {
223
return;
224
}
225
226
protected isAddButtonVisible(): boolean {
227
return true;
228
}
229
230
protected renderList(): void {
231
const focused = DOM.isAncestorOfActiveElement(this.listElement);
232
233
DOM.clearNode(this.listElement);
234
this.listDisposables.clear();
235
236
const newMode = this.model.items.some(item => !!(item.editing && this.isItemNew(item)));
237
this.container.classList.toggle('setting-list-hide-add-button', !this.isAddButtonVisible() || newMode);
238
239
if (this.model.items.length) {
240
this.listElement.tabIndex = 0;
241
} else {
242
this.listElement.removeAttribute('tabIndex');
243
}
244
245
const header = this.renderHeader();
246
247
if (header) {
248
this.listElement.appendChild(header);
249
}
250
251
this.rowElements = this.model.items.map((item, i) => this.renderDataOrEditItem(item, i, focused));
252
this.rowElements.forEach(rowElement => this.listElement.appendChild(rowElement));
253
254
}
255
256
protected createBasicSelectBox(value: IObjectEnumData): SelectBox {
257
const selectBoxOptions = value.options.map(({ value, description }) => ({ text: value, description }));
258
const selected = value.options.findIndex(option => value.data === option.value);
259
260
const styles = getSelectBoxStyles({
261
selectBackground: settingsSelectBackground,
262
selectForeground: settingsSelectForeground,
263
selectBorder: settingsSelectBorder,
264
selectListBorder: settingsSelectListBorder
265
});
266
267
268
const selectBox = new SelectBox(selectBoxOptions, selected, this.contextViewService, styles, {
269
useCustomDrawn: !hasNativeContextMenu(this.configurationService) || !(isIOS && BrowserFeatures.pointerEvents)
270
});
271
return selectBox;
272
}
273
274
protected editSetting(idx: number): void {
275
this.model.setEditKey(idx);
276
this.renderList();
277
}
278
279
public cancelEdit(): void {
280
this.model.setEditKey('none');
281
this.renderList();
282
}
283
284
protected handleItemChange(originalItem: TDataItem, changedItem: TDataItem, idx: number) {
285
this.model.setEditKey('none');
286
287
if (this.isItemNew(originalItem)) {
288
this._onDidChangeList.fire({
289
type: 'add',
290
newItem: changedItem,
291
targetIndex: idx,
292
});
293
} else {
294
this._onDidChangeList.fire({
295
type: 'change',
296
originalItem,
297
newItem: changedItem,
298
targetIndex: idx,
299
});
300
}
301
302
this.renderList();
303
}
304
305
protected renderDataOrEditItem(item: IListViewItem<TDataItem>, idx: number, listFocused: boolean): HTMLElement {
306
const rowElement = item.editing ?
307
this.renderEdit(item, idx) :
308
this.renderDataItem(item, idx, listFocused);
309
310
rowElement.setAttribute('role', 'listitem');
311
312
return rowElement;
313
}
314
315
private renderDataItem(item: IListViewItem<TDataItem>, idx: number, listFocused: boolean): HTMLElement {
316
const rowElementGroup = this.renderItem(item, idx);
317
const rowElement = rowElementGroup.rowElement;
318
319
rowElement.setAttribute('data-index', idx + '');
320
rowElement.setAttribute('tabindex', item.selected ? '0' : '-1');
321
rowElement.classList.toggle('selected', item.selected);
322
323
const actionBar = new ActionBar(rowElement);
324
this.listDisposables.add(actionBar);
325
326
actionBar.push(this.getActionsForItem(item, idx), { icon: true, label: true });
327
this.addTooltipsToRow(rowElementGroup, item);
328
329
if (item.selected && listFocused) {
330
disposableTimeout(() => rowElement.focus(), undefined, this.listDisposables);
331
}
332
333
this.listDisposables.add(DOM.addDisposableListener(rowElement, 'click', (e) => {
334
// There is a parent list widget, which is the one that holds the list of settings.
335
// Prevent the parent widget from trying to interpret this click event.
336
e.stopPropagation();
337
}));
338
339
return rowElement;
340
}
341
342
private renderAddButton(): HTMLElement {
343
const rowElement = $('.setting-list-new-row');
344
345
const startAddButton = this._register(new Button(rowElement, defaultButtonStyles));
346
startAddButton.label = this.getLocalizedStrings().addButtonLabel;
347
startAddButton.element.classList.add('setting-list-addButton');
348
349
this._register(startAddButton.onDidClick(() => {
350
this.model.setEditKey('create');
351
this.renderList();
352
}));
353
354
return rowElement;
355
}
356
357
private onListClick(e: PointerEvent): void {
358
const targetIdx = this.getClickedItemIndex(e);
359
if (targetIdx < 0) {
360
return;
361
}
362
363
e.preventDefault();
364
e.stopImmediatePropagation();
365
if (this.model.getSelected() === targetIdx) {
366
return;
367
}
368
369
this.selectRow(targetIdx);
370
}
371
372
private onListDoubleClick(e: MouseEvent): void {
373
const targetIdx = this.getClickedItemIndex(e);
374
if (targetIdx < 0) {
375
return;
376
}
377
378
if (this.isReadOnly) {
379
return;
380
}
381
382
const item = this.model.items[targetIdx];
383
if (item) {
384
this.editSetting(targetIdx);
385
e.preventDefault();
386
e.stopPropagation();
387
}
388
}
389
390
private getClickedItemIndex(e: MouseEvent): number {
391
if (!e.target) {
392
return -1;
393
}
394
395
const actionbar = DOM.findParentWithClass(e.target as HTMLElement, 'monaco-action-bar');
396
if (actionbar) {
397
// Don't handle doubleclicks inside the action bar
398
return -1;
399
}
400
401
const element = DOM.findParentWithClass(e.target as HTMLElement, 'setting-list-row');
402
if (!element) {
403
return -1;
404
}
405
406
const targetIdxStr = element.getAttribute('data-index');
407
if (!targetIdxStr) {
408
return -1;
409
}
410
411
const targetIdx = parseInt(targetIdxStr);
412
return targetIdx;
413
}
414
415
private selectRow(idx: number): void {
416
this.model.select(idx);
417
this.rowElements.forEach(row => row.classList.remove('selected'));
418
419
const selectedRow = this.rowElements[this.model.getSelected()!];
420
421
selectedRow.classList.add('selected');
422
selectedRow.focus();
423
}
424
425
private selectNextRow(): void {
426
this.model.selectNext();
427
this.selectRow(this.model.getSelected()!);
428
}
429
430
private selectPreviousRow(): void {
431
this.model.selectPrevious();
432
this.selectRow(this.model.getSelected()!);
433
}
434
}
435
436
interface IListSetValueOptions {
437
showAddButton: boolean;
438
keySuggester?: IObjectKeySuggester;
439
}
440
441
export interface IListDataItem {
442
value: ObjectKey;
443
sibling?: string;
444
}
445
446
interface ListSettingWidgetDragDetails<TListDataItem extends IListDataItem> {
447
element: HTMLElement;
448
item: TListDataItem;
449
itemIndex: number;
450
}
451
452
export class ListSettingWidget<TListDataItem extends IListDataItem> extends AbstractListSettingWidget<TListDataItem> {
453
private keyValueSuggester: IObjectKeySuggester | undefined;
454
private showAddButton: boolean = true;
455
456
override setValue(listData: TListDataItem[], options?: IListSetValueOptions) {
457
this.keyValueSuggester = options?.keySuggester;
458
this.showAddButton = options?.showAddButton ?? true;
459
super.setValue(listData);
460
}
461
462
constructor(
463
container: HTMLElement,
464
@IThemeService themeService: IThemeService,
465
@IContextViewService contextViewService: IContextViewService,
466
@IHoverService protected readonly hoverService: IHoverService,
467
@IConfigurationService configurationService: IConfigurationService,
468
) {
469
super(container, themeService, contextViewService, configurationService);
470
}
471
472
protected getEmptyItem(): TListDataItem {
473
// eslint-disable-next-line local/code-no-dangerous-type-assertions
474
return {
475
value: {
476
type: 'string',
477
data: ''
478
}
479
} as TListDataItem;
480
}
481
482
protected override isAddButtonVisible(): boolean {
483
return this.showAddButton;
484
}
485
486
protected getContainerClasses(): string[] {
487
return ['setting-list-widget'];
488
}
489
490
protected getActionsForItem(item: TListDataItem, idx: number): IAction[] {
491
if (this.isReadOnly) {
492
return [];
493
}
494
return [
495
{
496
class: ThemeIcon.asClassName(settingsEditIcon),
497
enabled: true,
498
id: 'workbench.action.editListItem',
499
tooltip: this.getLocalizedStrings().editActionTooltip,
500
run: () => this.editSetting(idx)
501
},
502
{
503
class: ThemeIcon.asClassName(settingsRemoveIcon),
504
enabled: true,
505
id: 'workbench.action.removeListItem',
506
tooltip: this.getLocalizedStrings().deleteActionTooltip,
507
run: () => this._onDidChangeList.fire({ type: 'remove', originalItem: item, targetIndex: idx })
508
}
509
] as IAction[];
510
}
511
512
private dragDetails: ListSettingWidgetDragDetails<TListDataItem> | undefined;
513
514
protected renderItem(item: TListDataItem, idx: number): RowElementGroup {
515
const rowElement = $('.setting-list-row');
516
const valueElement = DOM.append(rowElement, $('.setting-list-value'));
517
const siblingElement = DOM.append(rowElement, $('.setting-list-sibling'));
518
519
valueElement.textContent = item.value.data.toString();
520
if (item.sibling) {
521
siblingElement.textContent = `when: ${item.sibling}`;
522
} else {
523
siblingElement.textContent = null;
524
valueElement.classList.add('no-sibling');
525
}
526
527
this.addDragAndDrop(rowElement, item, idx);
528
return { rowElement, keyElement: valueElement, valueElement: siblingElement };
529
}
530
531
protected addDragAndDrop(rowElement: HTMLElement, item: TListDataItem, idx: number) {
532
if (this.model.items.every(item => !item.editing)) {
533
rowElement.draggable = true;
534
rowElement.classList.add('draggable');
535
} else {
536
rowElement.draggable = false;
537
rowElement.classList.remove('draggable');
538
}
539
540
this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_START, (ev) => {
541
this.dragDetails = {
542
element: rowElement,
543
item,
544
itemIndex: idx
545
};
546
547
applyDragImage(ev, rowElement, item.value.data);
548
}));
549
this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_OVER, (ev) => {
550
if (!this.dragDetails) {
551
return false;
552
}
553
ev.preventDefault();
554
if (ev.dataTransfer) {
555
ev.dataTransfer.dropEffect = 'move';
556
}
557
return true;
558
}));
559
let counter = 0;
560
this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_ENTER, (ev) => {
561
counter++;
562
rowElement.classList.add('drag-hover');
563
}));
564
this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_LEAVE, (ev) => {
565
counter--;
566
if (!counter) {
567
rowElement.classList.remove('drag-hover');
568
}
569
}));
570
this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DROP, (ev) => {
571
// cancel the op if we dragged to a completely different setting
572
if (!this.dragDetails) {
573
return false;
574
}
575
ev.preventDefault();
576
counter = 0;
577
if (this.dragDetails.element !== rowElement) {
578
this._onDidChangeList.fire({
579
type: 'move',
580
originalItem: this.dragDetails.item,
581
sourceIndex: this.dragDetails.itemIndex,
582
newItem: item,
583
targetIndex: idx
584
});
585
}
586
return true;
587
}));
588
this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_END, (ev) => {
589
counter = 0;
590
rowElement.classList.remove('drag-hover');
591
ev.dataTransfer?.clearData();
592
if (this.dragDetails) {
593
this.dragDetails = undefined;
594
}
595
}));
596
}
597
598
protected renderEdit(item: TListDataItem, idx: number): HTMLElement {
599
const rowElement = $('.setting-list-edit-row');
600
let valueInput: InputBox | SelectBox;
601
let currentDisplayValue: string;
602
let currentEnumOptions: IObjectEnumOption[] | undefined;
603
604
if (this.keyValueSuggester) {
605
const enumData = this.keyValueSuggester(this.model.items.map(({ value: { data } }) => data), idx);
606
item = {
607
...item,
608
value: {
609
type: 'enum',
610
data: item.value.data,
611
options: enumData ? enumData.options : []
612
}
613
};
614
}
615
616
switch (item.value.type) {
617
case 'string':
618
valueInput = this.renderInputBox(item.value, rowElement);
619
break;
620
case 'enum':
621
valueInput = this.renderDropdown(item.value, rowElement);
622
currentEnumOptions = item.value.options;
623
if (item.value.options.length) {
624
currentDisplayValue = this.isItemNew(item) ?
625
currentEnumOptions[0].value : item.value.data;
626
}
627
break;
628
}
629
630
const updatedInputBoxItem = (): TListDataItem => {
631
const inputBox = valueInput as InputBox;
632
// eslint-disable-next-line local/code-no-dangerous-type-assertions
633
return {
634
value: {
635
type: 'string',
636
data: inputBox.value
637
},
638
sibling: siblingInput?.value
639
} as TListDataItem;
640
};
641
const updatedSelectBoxItem = (selectedValue: string): TListDataItem => {
642
// eslint-disable-next-line local/code-no-dangerous-type-assertions
643
return {
644
value: {
645
type: 'enum',
646
data: selectedValue,
647
options: currentEnumOptions ?? []
648
}
649
} as TListDataItem;
650
};
651
const onKeyDown = (e: StandardKeyboardEvent) => {
652
if (e.equals(KeyCode.Enter)) {
653
this.handleItemChange(item, updatedInputBoxItem(), idx);
654
} else if (e.equals(KeyCode.Escape)) {
655
this.cancelEdit();
656
e.preventDefault();
657
}
658
rowElement?.focus();
659
};
660
661
if (item.value.type !== 'string') {
662
const selectBox = valueInput as SelectBox;
663
this.listDisposables.add(
664
selectBox.onDidSelect(({ selected }) => {
665
currentDisplayValue = selected;
666
})
667
);
668
} else {
669
const inputBox = valueInput as InputBox;
670
this.listDisposables.add(
671
DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, onKeyDown)
672
);
673
}
674
675
let siblingInput: InputBox | undefined;
676
if (!isUndefinedOrNull(item.sibling)) {
677
siblingInput = new InputBox(rowElement, this.contextViewService, {
678
placeholder: this.getLocalizedStrings().siblingInputPlaceholder,
679
inputBoxStyles: getInputBoxStyle({
680
inputBackground: settingsTextInputBackground,
681
inputForeground: settingsTextInputForeground,
682
inputBorder: settingsTextInputBorder
683
})
684
});
685
siblingInput.element.classList.add('setting-list-siblingInput');
686
this.listDisposables.add(siblingInput);
687
siblingInput.value = item.sibling;
688
689
this.listDisposables.add(
690
DOM.addStandardDisposableListener(siblingInput.inputElement, DOM.EventType.KEY_DOWN, onKeyDown)
691
);
692
} else if (valueInput instanceof InputBox) {
693
valueInput.element.classList.add('no-sibling');
694
}
695
696
const okButton = this.listDisposables.add(new Button(rowElement, defaultButtonStyles));
697
okButton.label = localize('okButton', "OK");
698
okButton.element.classList.add('setting-list-ok-button');
699
700
this.listDisposables.add(okButton.onDidClick(() => {
701
if (item.value.type === 'string') {
702
this.handleItemChange(item, updatedInputBoxItem(), idx);
703
} else {
704
this.handleItemChange(item, updatedSelectBoxItem(currentDisplayValue), idx);
705
}
706
}));
707
708
const cancelButton = this.listDisposables.add(new Button(rowElement, { secondary: true, ...defaultButtonStyles }));
709
cancelButton.label = localize('cancelButton', "Cancel");
710
cancelButton.element.classList.add('setting-list-cancel-button');
711
712
this.listDisposables.add(cancelButton.onDidClick(() => this.cancelEdit()));
713
714
this.listDisposables.add(
715
disposableTimeout(() => {
716
valueInput.focus();
717
if (valueInput instanceof InputBox) {
718
valueInput.select();
719
}
720
})
721
);
722
723
return rowElement;
724
}
725
726
override isItemNew(item: TListDataItem): boolean {
727
return item.value.data === '';
728
}
729
730
protected addTooltipsToRow(rowElementGroup: RowElementGroup, { value, sibling }: TListDataItem) {
731
const title = isUndefinedOrNull(sibling)
732
? localize('listValueHintLabel', "List item `{0}`", value.data)
733
: localize('listSiblingHintLabel', "List item `{0}` with sibling `${1}`", value.data, sibling);
734
735
const { rowElement } = rowElementGroup;
736
this.listDisposables.add(this.hoverService.setupDelayedHover(rowElement, { content: title }));
737
rowElement.setAttribute('aria-label', title);
738
}
739
740
protected getLocalizedStrings() {
741
return {
742
deleteActionTooltip: localize('removeItem', "Remove Item"),
743
editActionTooltip: localize('editItem', "Edit Item"),
744
addButtonLabel: localize('addItem', "Add Item"),
745
inputPlaceholder: localize('itemInputPlaceholder', "Item..."),
746
siblingInputPlaceholder: localize('listSiblingInputPlaceholder', "Sibling..."),
747
};
748
}
749
750
private renderInputBox(value: ObjectValue, rowElement: HTMLElement): InputBox {
751
const valueInput = new InputBox(rowElement, this.contextViewService, {
752
placeholder: this.getLocalizedStrings().inputPlaceholder,
753
inputBoxStyles: getInputBoxStyle({
754
inputBackground: settingsTextInputBackground,
755
inputForeground: settingsTextInputForeground,
756
inputBorder: settingsTextInputBorder
757
})
758
});
759
760
valueInput.element.classList.add('setting-list-valueInput');
761
this.listDisposables.add(valueInput);
762
valueInput.value = value.data.toString();
763
764
return valueInput;
765
}
766
767
private renderDropdown(value: ObjectKey, rowElement: HTMLElement): SelectBox {
768
if (value.type !== 'enum') {
769
throw new Error('Valuetype must be enum.');
770
}
771
const selectBox = this.createBasicSelectBox(value);
772
773
const wrapper = $('.setting-list-object-list-row');
774
selectBox.render(wrapper);
775
rowElement.appendChild(wrapper);
776
777
return selectBox;
778
}
779
}
780
781
export class ExcludeSettingWidget extends ListSettingWidget<IIncludeExcludeDataItem> {
782
protected override getContainerClasses() {
783
return ['setting-list-include-exclude-widget'];
784
}
785
786
protected override addDragAndDrop(rowElement: HTMLElement, item: IIncludeExcludeDataItem, idx: number) {
787
return;
788
}
789
790
protected override addTooltipsToRow(rowElementGroup: RowElementGroup, item: IIncludeExcludeDataItem): void {
791
let title = isUndefinedOrNull(item.sibling)
792
? localize('excludePatternHintLabel', "Exclude files matching `{0}`", item.value.data)
793
: localize('excludeSiblingHintLabel', "Exclude files matching `{0}`, only when a file matching `{1}` is present", item.value.data, item.sibling);
794
795
if (item.source) {
796
title += localize('excludeIncludeSource', ". Default value provided by `{0}`", item.source);
797
}
798
799
const markdownTitle = new MarkdownString().appendMarkdown(title);
800
801
const { rowElement } = rowElementGroup;
802
this.listDisposables.add(this.hoverService.setupDelayedHover(rowElement, { content: markdownTitle }));
803
rowElement.setAttribute('aria-label', title);
804
}
805
806
protected override getLocalizedStrings() {
807
return {
808
deleteActionTooltip: localize('removeExcludeItem', "Remove Exclude Item"),
809
editActionTooltip: localize('editExcludeItem', "Edit Exclude Item"),
810
addButtonLabel: localize('addPattern', "Add Pattern"),
811
inputPlaceholder: localize('excludePatternInputPlaceholder', "Exclude Pattern..."),
812
siblingInputPlaceholder: localize('excludeSiblingInputPlaceholder', "When Pattern Is Present..."),
813
};
814
}
815
}
816
817
export class IncludeSettingWidget extends ListSettingWidget<IIncludeExcludeDataItem> {
818
protected override getContainerClasses() {
819
return ['setting-list-include-exclude-widget'];
820
}
821
822
protected override addDragAndDrop(rowElement: HTMLElement, item: IIncludeExcludeDataItem, idx: number) {
823
return;
824
}
825
826
protected override addTooltipsToRow(rowElementGroup: RowElementGroup, item: IIncludeExcludeDataItem): void {
827
let title = isUndefinedOrNull(item.sibling)
828
? localize('includePatternHintLabel', "Include files matching `{0}`", item.value.data)
829
: localize('includeSiblingHintLabel', "Include files matching `{0}`, only when a file matching `{1}` is present", item.value.data, item.sibling);
830
831
if (item.source) {
832
title += localize('excludeIncludeSource', ". Default value provided by `{0}`", item.source);
833
}
834
835
const markdownTitle = new MarkdownString().appendMarkdown(title);
836
837
const { rowElement } = rowElementGroup;
838
this.listDisposables.add(this.hoverService.setupDelayedHover(rowElement, { content: markdownTitle }));
839
rowElement.setAttribute('aria-label', title);
840
}
841
842
protected override getLocalizedStrings() {
843
return {
844
deleteActionTooltip: localize('removeIncludeItem', "Remove Include Item"),
845
editActionTooltip: localize('editIncludeItem', "Edit Include Item"),
846
addButtonLabel: localize('addPattern', "Add Pattern"),
847
inputPlaceholder: localize('includePatternInputPlaceholder', "Include Pattern..."),
848
siblingInputPlaceholder: localize('includeSiblingInputPlaceholder', "When Pattern Is Present..."),
849
};
850
}
851
}
852
853
interface IObjectStringData {
854
type: 'string';
855
data: string;
856
}
857
858
export interface IObjectEnumOption {
859
value: string;
860
description?: string;
861
}
862
863
interface IObjectEnumData {
864
type: 'enum';
865
data: string;
866
options: IObjectEnumOption[];
867
}
868
869
interface IObjectBoolData {
870
type: 'boolean';
871
data: boolean;
872
}
873
874
type ObjectKey = IObjectStringData | IObjectEnumData;
875
export type ObjectValue = IObjectStringData | IObjectEnumData | IObjectBoolData;
876
type ObjectWidget = InputBox | SelectBox;
877
878
export interface IObjectDataItem {
879
key: ObjectKey;
880
value: ObjectValue;
881
keyDescription?: string;
882
source?: string;
883
removable: boolean;
884
resetable: boolean;
885
}
886
887
export interface IIncludeExcludeDataItem {
888
value: ObjectKey;
889
elementType: SettingValueType;
890
sibling?: string;
891
source?: string;
892
}
893
894
export interface IObjectValueSuggester {
895
(key: string): ObjectValue | undefined;
896
}
897
898
export interface IObjectKeySuggester {
899
(existingKeys: string[], idx?: number): IObjectEnumData | undefined;
900
}
901
902
interface IObjectSetValueOptions {
903
settingKey: string;
904
showAddButton: boolean;
905
isReadOnly?: boolean;
906
keySuggester?: IObjectKeySuggester;
907
valueSuggester?: IObjectValueSuggester;
908
}
909
910
interface IObjectRenderEditWidgetOptions {
911
isKey: boolean;
912
idx: number;
913
readonly originalItem: IObjectDataItem;
914
readonly changedItem: IObjectDataItem;
915
update(keyOrValue: ObjectKey | ObjectValue): void;
916
}
917
918
export class ObjectSettingDropdownWidget extends AbstractListSettingWidget<IObjectDataItem> {
919
private editable: boolean = true;
920
private currentSettingKey: string = '';
921
private showAddButton: boolean = true;
922
private keySuggester: IObjectKeySuggester = () => undefined;
923
private valueSuggester: IObjectValueSuggester = () => undefined;
924
925
constructor(
926
container: HTMLElement,
927
@IThemeService themeService: IThemeService,
928
@IContextViewService contextViewService: IContextViewService,
929
@IHoverService private readonly hoverService: IHoverService,
930
@IConfigurationService configurationService: IConfigurationService,
931
) {
932
super(container, themeService, contextViewService, configurationService);
933
}
934
935
override setValue(listData: IObjectDataItem[], options?: IObjectSetValueOptions): void {
936
this.editable = !options?.isReadOnly;
937
this.showAddButton = options?.showAddButton ?? this.showAddButton;
938
this.keySuggester = options?.keySuggester ?? this.keySuggester;
939
this.valueSuggester = options?.valueSuggester ?? this.valueSuggester;
940
941
if (isDefined(options) && options.settingKey !== this.currentSettingKey) {
942
this.model.setEditKey('none');
943
this.model.select(null);
944
this.currentSettingKey = options.settingKey;
945
}
946
947
super.setValue(listData);
948
}
949
950
override isItemNew(item: IObjectDataItem): boolean {
951
return item.key.data === '' && item.value.data === '';
952
}
953
954
protected override isAddButtonVisible(): boolean {
955
return this.showAddButton;
956
}
957
958
protected override get isReadOnly(): boolean {
959
return !this.editable;
960
}
961
962
protected getEmptyItem(): IObjectDataItem {
963
return {
964
key: { type: 'string', data: '' },
965
value: { type: 'string', data: '' },
966
removable: true,
967
resetable: false
968
};
969
}
970
971
protected getContainerClasses() {
972
return ['setting-list-object-widget'];
973
}
974
975
protected getActionsForItem(item: IObjectDataItem, idx: number): IAction[] {
976
if (this.isReadOnly) {
977
return [];
978
}
979
980
const actions: IAction[] = [
981
{
982
class: ThemeIcon.asClassName(settingsEditIcon),
983
enabled: true,
984
id: 'workbench.action.editListItem',
985
label: '',
986
tooltip: this.getLocalizedStrings().editActionTooltip,
987
run: () => this.editSetting(idx)
988
},
989
];
990
991
if (item.resetable) {
992
actions.push({
993
class: ThemeIcon.asClassName(settingsDiscardIcon),
994
enabled: true,
995
id: 'workbench.action.resetListItem',
996
label: '',
997
tooltip: this.getLocalizedStrings().resetActionTooltip,
998
run: () => this._onDidChangeList.fire({ type: 'reset', originalItem: item, targetIndex: idx })
999
});
1000
}
1001
1002
if (item.removable) {
1003
actions.push({
1004
class: ThemeIcon.asClassName(settingsRemoveIcon),
1005
enabled: true,
1006
id: 'workbench.action.removeListItem',
1007
label: '',
1008
tooltip: this.getLocalizedStrings().deleteActionTooltip,
1009
run: () => this._onDidChangeList.fire({ type: 'remove', originalItem: item, targetIndex: idx })
1010
});
1011
}
1012
1013
return actions;
1014
}
1015
1016
protected override renderHeader() {
1017
const header = $('.setting-list-row-header');
1018
const keyHeader = DOM.append(header, $('.setting-list-object-key'));
1019
const valueHeader = DOM.append(header, $('.setting-list-object-value'));
1020
const { keyHeaderText, valueHeaderText } = this.getLocalizedStrings();
1021
1022
keyHeader.textContent = keyHeaderText;
1023
valueHeader.textContent = valueHeaderText;
1024
1025
return header;
1026
}
1027
1028
protected renderItem(item: IObjectDataItem, idx: number): RowElementGroup {
1029
const rowElement = $('.setting-list-row');
1030
rowElement.classList.add('setting-list-object-row');
1031
1032
const keyElement = DOM.append(rowElement, $('.setting-list-object-key'));
1033
const valueElement = DOM.append(rowElement, $('.setting-list-object-value'));
1034
1035
keyElement.textContent = item.key.data;
1036
valueElement.textContent = item.value.data.toString();
1037
1038
return { rowElement, keyElement, valueElement };
1039
}
1040
1041
protected renderEdit(item: IObjectDataItem, idx: number): HTMLElement {
1042
const rowElement = $('.setting-list-edit-row.setting-list-object-row');
1043
1044
const changedItem = { ...item };
1045
const onKeyChange = (key: ObjectKey) => {
1046
changedItem.key = key;
1047
okButton.enabled = key.data !== '';
1048
1049
const suggestedValue = this.valueSuggester(key.data) ?? item.value;
1050
1051
if (this.shouldUseSuggestion(item.value, changedItem.value, suggestedValue)) {
1052
onValueChange(suggestedValue);
1053
renderLatestValue();
1054
}
1055
};
1056
const onValueChange = (value: ObjectValue) => {
1057
changedItem.value = value;
1058
};
1059
1060
let keyWidget: ObjectWidget | undefined;
1061
let keyElement: HTMLElement;
1062
1063
if (this.showAddButton) {
1064
if (this.isItemNew(item)) {
1065
const suggestedKey = this.keySuggester(this.model.items.map(({ key: { data } }) => data));
1066
1067
if (isDefined(suggestedKey)) {
1068
changedItem.key = suggestedKey;
1069
const suggestedValue = this.valueSuggester(changedItem.key.data);
1070
onValueChange(suggestedValue ?? changedItem.value);
1071
}
1072
}
1073
1074
const { widget, element } = this.renderEditWidget(changedItem.key, {
1075
idx,
1076
isKey: true,
1077
originalItem: item,
1078
changedItem,
1079
update: onKeyChange,
1080
});
1081
keyWidget = widget;
1082
keyElement = element;
1083
} else {
1084
keyElement = $('.setting-list-object-key');
1085
keyElement.textContent = item.key.data;
1086
}
1087
1088
let valueWidget: ObjectWidget;
1089
const valueContainer = $('.setting-list-object-value-container');
1090
1091
const renderLatestValue = () => {
1092
const { widget, element } = this.renderEditWidget(changedItem.value, {
1093
idx,
1094
isKey: false,
1095
originalItem: item,
1096
changedItem,
1097
update: onValueChange,
1098
});
1099
1100
valueWidget = widget;
1101
1102
DOM.clearNode(valueContainer);
1103
valueContainer.append(element);
1104
};
1105
1106
renderLatestValue();
1107
1108
rowElement.append(keyElement, valueContainer);
1109
1110
const okButton = this.listDisposables.add(new Button(rowElement, defaultButtonStyles));
1111
okButton.enabled = changedItem.key.data !== '';
1112
okButton.label = localize('okButton', "OK");
1113
okButton.element.classList.add('setting-list-ok-button');
1114
1115
this.listDisposables.add(okButton.onDidClick(() => this.handleItemChange(item, changedItem, idx)));
1116
1117
const cancelButton = this.listDisposables.add(new Button(rowElement, { secondary: true, ...defaultButtonStyles }));
1118
cancelButton.label = localize('cancelButton', "Cancel");
1119
cancelButton.element.classList.add('setting-list-cancel-button');
1120
1121
this.listDisposables.add(cancelButton.onDidClick(() => this.cancelEdit()));
1122
1123
this.listDisposables.add(
1124
disposableTimeout(() => {
1125
const widget = keyWidget ?? valueWidget;
1126
1127
widget.focus();
1128
1129
if (widget instanceof InputBox) {
1130
widget.select();
1131
}
1132
})
1133
);
1134
1135
return rowElement;
1136
}
1137
1138
private renderEditWidget(
1139
keyOrValue: ObjectKey | ObjectValue,
1140
options: IObjectRenderEditWidgetOptions,
1141
) {
1142
switch (keyOrValue.type) {
1143
case 'string':
1144
return this.renderStringEditWidget(keyOrValue, options);
1145
case 'enum':
1146
return this.renderEnumEditWidget(keyOrValue, options);
1147
case 'boolean':
1148
return this.renderEnumEditWidget(
1149
{
1150
type: 'enum',
1151
data: keyOrValue.data.toString(),
1152
options: [{ value: 'true' }, { value: 'false' }],
1153
},
1154
options,
1155
);
1156
}
1157
}
1158
1159
private renderStringEditWidget(
1160
keyOrValue: IObjectStringData,
1161
{ idx, isKey, originalItem, changedItem, update }: IObjectRenderEditWidgetOptions,
1162
) {
1163
const wrapper = $(isKey ? '.setting-list-object-input-key' : '.setting-list-object-input-value');
1164
const inputBox = new InputBox(wrapper, this.contextViewService, {
1165
placeholder: isKey
1166
? localize('objectKeyInputPlaceholder', "Key")
1167
: localize('objectValueInputPlaceholder', "Value"),
1168
inputBoxStyles: getInputBoxStyle({
1169
inputBackground: settingsTextInputBackground,
1170
inputForeground: settingsTextInputForeground,
1171
inputBorder: settingsTextInputBorder
1172
})
1173
});
1174
1175
inputBox.element.classList.add('setting-list-object-input');
1176
1177
this.listDisposables.add(inputBox);
1178
inputBox.value = keyOrValue.data;
1179
1180
this.listDisposables.add(inputBox.onDidChange(value => update({ ...keyOrValue, data: value })));
1181
1182
const onKeyDown = (e: StandardKeyboardEvent) => {
1183
if (e.equals(KeyCode.Enter)) {
1184
this.handleItemChange(originalItem, changedItem, idx);
1185
} else if (e.equals(KeyCode.Escape)) {
1186
this.cancelEdit();
1187
e.preventDefault();
1188
}
1189
};
1190
1191
this.listDisposables.add(
1192
DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, onKeyDown)
1193
);
1194
1195
return { widget: inputBox, element: wrapper };
1196
}
1197
1198
private renderEnumEditWidget(
1199
keyOrValue: IObjectEnumData,
1200
{ isKey, changedItem, update }: IObjectRenderEditWidgetOptions,
1201
) {
1202
const selectBox = this.createBasicSelectBox(keyOrValue);
1203
1204
const changedKeyOrValue = isKey ? changedItem.key : changedItem.value;
1205
this.listDisposables.add(
1206
selectBox.onDidSelect(({ selected }) =>
1207
update(
1208
changedKeyOrValue.type === 'boolean'
1209
? { ...changedKeyOrValue, data: selected === 'true' ? true : false }
1210
: { ...changedKeyOrValue, data: selected },
1211
)
1212
)
1213
);
1214
1215
const wrapper = $('.setting-list-object-input');
1216
wrapper.classList.add(
1217
isKey ? 'setting-list-object-input-key' : 'setting-list-object-input-value',
1218
);
1219
1220
selectBox.render(wrapper);
1221
1222
// Switch to the first item if the user set something invalid in the json
1223
const selected = keyOrValue.options.findIndex(option => keyOrValue.data === option.value);
1224
if (selected === -1 && keyOrValue.options.length) {
1225
update(
1226
changedKeyOrValue.type === 'boolean'
1227
? { ...changedKeyOrValue, data: true }
1228
: { ...changedKeyOrValue, data: keyOrValue.options[0].value }
1229
);
1230
} else if (changedKeyOrValue.type === 'boolean') {
1231
// https://github.com/microsoft/vscode/issues/129581
1232
update({ ...changedKeyOrValue, data: keyOrValue.data === 'true' });
1233
}
1234
1235
return { widget: selectBox, element: wrapper };
1236
}
1237
1238
private shouldUseSuggestion(originalValue: ObjectValue, previousValue: ObjectValue, newValue: ObjectValue): boolean {
1239
// suggestion is exactly the same
1240
if (newValue.type !== 'enum' && newValue.type === previousValue.type && newValue.data === previousValue.data) {
1241
return false;
1242
}
1243
1244
// item is new, use suggestion
1245
if (originalValue.data === '') {
1246
return true;
1247
}
1248
1249
if (previousValue.type === newValue.type && newValue.type !== 'enum') {
1250
return false;
1251
}
1252
1253
// check if all enum options are the same
1254
if (previousValue.type === 'enum' && newValue.type === 'enum') {
1255
const previousEnums = new Set(previousValue.options.map(({ value }) => value));
1256
newValue.options.forEach(({ value }) => previousEnums.delete(value));
1257
1258
// all options are the same
1259
if (previousEnums.size === 0) {
1260
return false;
1261
}
1262
}
1263
1264
return true;
1265
}
1266
1267
protected addTooltipsToRow(rowElementGroup: RowElementGroup, item: IObjectDataItem): void {
1268
const { keyElement, valueElement, rowElement } = rowElementGroup;
1269
1270
let accessibleDescription;
1271
if (item.source) {
1272
accessibleDescription = localize('objectPairHintLabelWithSource', "The property `{0}` is set to `{1}` by `{2}`.", item.key.data, item.value.data, item.source);
1273
} else {
1274
accessibleDescription = localize('objectPairHintLabel', "The property `{0}` is set to `{1}`.", item.key.data, item.value.data);
1275
}
1276
1277
const markdownString = new MarkdownString().appendMarkdown(accessibleDescription);
1278
1279
const keyDescription: string | MarkdownString = this.getEnumDescription(item.key) ?? item.keyDescription ?? markdownString;
1280
this.listDisposables.add(this.hoverService.setupDelayedHover(keyElement, { content: keyDescription }));
1281
1282
const valueDescription: string | MarkdownString = this.getEnumDescription(item.value) ?? markdownString;
1283
this.listDisposables.add(this.hoverService.setupDelayedHover(valueElement!, { content: valueDescription }));
1284
1285
rowElement.setAttribute('aria-label', accessibleDescription);
1286
}
1287
1288
private getEnumDescription(keyOrValue: ObjectKey | ObjectValue): string | undefined {
1289
const enumDescription = keyOrValue.type === 'enum'
1290
? keyOrValue.options.find(({ value }) => keyOrValue.data === value)?.description
1291
: undefined;
1292
return enumDescription;
1293
}
1294
1295
protected getLocalizedStrings() {
1296
return {
1297
deleteActionTooltip: localize('removeItem', "Remove Item"),
1298
resetActionTooltip: localize('resetItem', "Reset Item"),
1299
editActionTooltip: localize('editItem', "Edit Item"),
1300
addButtonLabel: localize('addItem', "Add Item"),
1301
keyHeaderText: localize('objectKeyHeader', "Item"),
1302
valueHeaderText: localize('objectValueHeader', "Value"),
1303
};
1304
}
1305
}
1306
1307
interface IBoolObjectSetValueOptions {
1308
settingKey: string;
1309
}
1310
1311
export interface IBoolObjectDataItem {
1312
key: IObjectStringData;
1313
value: IObjectBoolData;
1314
keyDescription?: string;
1315
source?: string;
1316
removable: false;
1317
resetable: boolean;
1318
}
1319
1320
export class ObjectSettingCheckboxWidget extends AbstractListSettingWidget<IBoolObjectDataItem> {
1321
private currentSettingKey: string = '';
1322
1323
constructor(
1324
container: HTMLElement,
1325
@IThemeService themeService: IThemeService,
1326
@IContextViewService contextViewService: IContextViewService,
1327
@IHoverService private readonly hoverService: IHoverService,
1328
@IConfigurationService configurationService: IConfigurationService,
1329
) {
1330
super(container, themeService, contextViewService, configurationService);
1331
}
1332
1333
override setValue(listData: IBoolObjectDataItem[], options?: IBoolObjectSetValueOptions): void {
1334
if (isDefined(options) && options.settingKey !== this.currentSettingKey) {
1335
this.model.setEditKey('none');
1336
this.model.select(null);
1337
this.currentSettingKey = options.settingKey;
1338
}
1339
1340
super.setValue(listData);
1341
}
1342
1343
override isItemNew(item: IBoolObjectDataItem): boolean {
1344
return !item.key.data && !item.value.data;
1345
}
1346
1347
protected getEmptyItem(): IBoolObjectDataItem {
1348
return {
1349
key: { type: 'string', data: '' },
1350
value: { type: 'boolean', data: false },
1351
removable: false,
1352
resetable: true
1353
};
1354
}
1355
1356
protected getContainerClasses() {
1357
return ['setting-list-object-widget'];
1358
}
1359
1360
protected getActionsForItem(item: IBoolObjectDataItem, idx: number): IAction[] {
1361
return [];
1362
}
1363
1364
protected override isAddButtonVisible(): boolean {
1365
return false;
1366
}
1367
1368
protected override renderHeader() {
1369
return undefined;
1370
}
1371
1372
protected override renderDataOrEditItem(item: IListViewItem<IBoolObjectDataItem>, idx: number, listFocused: boolean): HTMLElement {
1373
const rowElement = this.renderEdit(item, idx);
1374
rowElement.setAttribute('role', 'listitem');
1375
return rowElement;
1376
}
1377
1378
protected renderItem(item: IBoolObjectDataItem, idx: number): RowElementGroup {
1379
// Return just the containers, since we always render in edit mode anyway
1380
const rowElement = $('.blank-row');
1381
const keyElement = $('.blank-row-key');
1382
return { rowElement, keyElement };
1383
}
1384
1385
protected renderEdit(item: IBoolObjectDataItem, idx: number): HTMLElement {
1386
const rowElement = $('.setting-list-edit-row.setting-list-object-row.setting-item-bool');
1387
1388
const changedItem = { ...item };
1389
const onValueChange = (newValue: boolean) => {
1390
changedItem.value.data = newValue;
1391
this.handleItemChange(item, changedItem, idx);
1392
};
1393
const checkboxDescription = item.keyDescription ? `${item.keyDescription} (${item.key.data})` : item.key.data;
1394
const { element, widget: checkbox } = this.renderEditWidget((changedItem.value as IObjectBoolData).data, checkboxDescription, onValueChange);
1395
rowElement.appendChild(element);
1396
1397
const valueElement = DOM.append(rowElement, $('.setting-list-object-value'));
1398
valueElement.textContent = checkboxDescription;
1399
1400
// We add the tooltips here, because the method is not called by default
1401
// for widgets in edit mode
1402
const rowElementGroup = { rowElement, keyElement: valueElement, valueElement: checkbox.domNode };
1403
this.addTooltipsToRow(rowElementGroup, item);
1404
1405
this._register(DOM.addDisposableListener(valueElement, DOM.EventType.MOUSE_DOWN, e => {
1406
const targetElement = <HTMLElement>e.target;
1407
if (targetElement.tagName.toLowerCase() !== 'a') {
1408
checkbox.checked = !checkbox.checked;
1409
onValueChange(checkbox.checked);
1410
}
1411
DOM.EventHelper.stop(e);
1412
}));
1413
1414
return rowElement;
1415
}
1416
1417
private renderEditWidget(
1418
value: boolean,
1419
checkboxDescription: string,
1420
onValueChange: (newValue: boolean) => void
1421
) {
1422
const checkbox = new Toggle({
1423
icon: Codicon.check,
1424
actionClassName: 'setting-value-checkbox',
1425
isChecked: value,
1426
title: checkboxDescription,
1427
...unthemedToggleStyles
1428
});
1429
1430
this.listDisposables.add(checkbox);
1431
1432
const wrapper = $('.setting-list-object-input');
1433
wrapper.classList.add('setting-list-object-input-key-checkbox');
1434
checkbox.domNode.classList.add('setting-value-checkbox');
1435
wrapper.appendChild(checkbox.domNode);
1436
1437
this._register(DOM.addDisposableListener(wrapper, DOM.EventType.MOUSE_DOWN, e => {
1438
checkbox.checked = !checkbox.checked;
1439
onValueChange(checkbox.checked);
1440
1441
// Without this line, the settings editor assumes
1442
// we lost focus on this setting completely.
1443
e.stopImmediatePropagation();
1444
}));
1445
1446
return { widget: checkbox, element: wrapper };
1447
}
1448
1449
protected addTooltipsToRow(rowElementGroup: RowElementGroup, item: IBoolObjectDataItem): void {
1450
const accessibleDescription = localize('objectPairHintLabel', "The property `{0}` is set to `{1}`.", item.key.data, item.value.data);
1451
const title = item.keyDescription ?? accessibleDescription;
1452
const { rowElement, keyElement, valueElement } = rowElementGroup;
1453
1454
this.listDisposables.add(this.hoverService.setupDelayedHover(keyElement, { content: title }));
1455
valueElement!.setAttribute('aria-label', accessibleDescription);
1456
rowElement.setAttribute('aria-label', accessibleDescription);
1457
}
1458
1459
protected getLocalizedStrings() {
1460
return {
1461
deleteActionTooltip: localize('removeItem', "Remove Item"),
1462
resetActionTooltip: localize('resetItem', "Reset Item"),
1463
editActionTooltip: localize('editItem', "Edit Item"),
1464
addButtonLabel: localize('addItem', "Add Item"),
1465
keyHeaderText: localize('objectKeyHeader', "Item"),
1466
valueHeaderText: localize('objectValueHeader', "Value"),
1467
};
1468
}
1469
}
1470
1471