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