Skip to content

Commit 346018a

Browse files
author
Jackson Kearl
authored
Enum descriptions in new settings editor via SelectBox widget (#57050)
* Create initial implementation of details selectbox via SelectBox * Add markdown support * Remove click handling support
1 parent 3cbd552 commit 346018a

File tree

8 files changed

+146
-33
lines changed

8 files changed

+146
-33
lines changed

src/vs/base/browser/ui/selectBox/selectBox.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { IListStyles } from 'vs/base/browser/ui/list/listWidget';
1414
import { SelectBoxNative } from 'vs/base/browser/ui/selectBox/selectBoxNative';
1515
import { SelectBoxList } from 'vs/base/browser/ui/selectBox/selectBoxCustom';
1616
import { isMacintosh } from 'vs/base/common/platform';
17+
import { IContentActionHandler } from 'vs/base/browser/htmlContentRenderer';
1718

1819
// Public SelectBox interface - Calls routed to appropriate select implementation class
1920

@@ -24,6 +25,7 @@ export interface ISelectBoxDelegate {
2425
setOptions(options: string[], selected?: number, disabled?: number): void;
2526
select(index: number): void;
2627
setAriaLabel(label: string);
28+
setDetailsProvider(provider: (index: number) => { details: string, isMarkdown: boolean });
2729
focus(): void;
2830
blur(): void;
2931
dispose(): void;
@@ -37,13 +39,16 @@ export interface ISelectBoxDelegate {
3739
export interface ISelectBoxOptions {
3840
ariaLabel?: string;
3941
minBottomMargin?: number;
42+
hasDetails?: boolean;
43+
markdownActionHandler?: IContentActionHandler;
4044
}
4145

4246
export interface ISelectBoxStyles extends IListStyles {
4347
selectBackground?: Color;
4448
selectListBackground?: Color;
4549
selectForeground?: Color;
4650
selectBorder?: Color;
51+
selectListBorder?: Color;
4752
focusBorder?: Color;
4853
}
4954

@@ -68,7 +73,7 @@ export class SelectBox extends Widget implements ISelectBoxDelegate {
6873
mixin(this.styles, defaultStyles, false);
6974

7075
// Instantiate select implementation based on platform
71-
if (isMacintosh) {
76+
if (isMacintosh && !(selectBoxOptions && selectBoxOptions.hasDetails)) {
7277
this.selectBoxDelegate = new SelectBoxNative(options, selected, styles, selectBoxOptions);
7378
} else {
7479
this.selectBoxDelegate = new SelectBoxList(options, selected, contextViewProvider, styles, selectBoxOptions);
@@ -95,6 +100,10 @@ export class SelectBox extends Widget implements ISelectBoxDelegate {
95100
this.selectBoxDelegate.setAriaLabel(label);
96101
}
97102

103+
public setDetailsProvider(provider: (index: number) => { details: string, isMarkdown: boolean }): void {
104+
this.selectBoxDelegate.setDetailsProvider(provider);
105+
}
106+
98107
public focus(): void {
99108
this.selectBoxDelegate.focus();
100109
}

src/vs/base/browser/ui/selectBox/selectBoxCustom.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,28 @@
1616

1717
.monaco-select-box-dropdown-container {
1818
display: none;
19+
-webkit-box-sizing: border-box;
20+
-o-box-sizing: border-box;
21+
-moz-box-sizing: border-box;
22+
-ms-box-sizing: border-box;
23+
box-sizing: border-box;
24+
}
25+
26+
.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown * {
27+
margin: 0;
1928
}
2029

30+
.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown a:focus {
31+
outline: 1px solid -webkit-focus-ring-color;
32+
outline-offset: -1px;
33+
}
34+
35+
.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown code {
36+
line-height: 15px; /** For some reason, this is needed, otherwise <code> will take up 20px height */
37+
font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
38+
}
39+
40+
2141
.monaco-select-box-dropdown-container.visible {
2242
display: flex;
2343
flex-direction: column;
@@ -42,6 +62,10 @@
4262
box-sizing: border-box;
4363
}
4464

65+
.monaco-select-box-dropdown-container > .select-box-details-pane {
66+
padding: 5px;
67+
}
68+
4569
.hc-black .monaco-select-box-dropdown-container > .select-box-dropdown-list-container {
4670
padding-top: var(--dropdown-padding-top);
4771
padding-bottom: var(--dropdown-padding-bottom);

src/vs/base/browser/ui/selectBox/selectBoxCustom.ts

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import * as dom from 'vs/base/browser/dom';
1414
import * as arrays from 'vs/base/common/arrays';
1515
import { IContextViewProvider, AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
1616
import { List } from 'vs/base/browser/ui/list/listWidget';
17-
import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
17+
import { IVirtualDelegate, IRenderer, IListEvent } from 'vs/base/browser/ui/list/list';
1818
import { domEvent } from 'vs/base/browser/event';
1919
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
2020
import { ISelectBoxDelegate, ISelectBoxOptions, ISelectBoxStyles, ISelectData } from 'vs/base/browser/ui/selectBox/selectBox';
2121
import { isMacintosh } from 'vs/base/common/platform';
22+
import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer';
2223

2324
const $ = dom.$;
2425

@@ -103,6 +104,11 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
103104
private widthControlElement: HTMLElement;
104105
private _currentSelection: number;
105106
private _dropDownPosition: AnchorPosition;
107+
private detailsProvider: (index: number) => { details: string, isMarkdown: boolean };
108+
private selectionDetailsPane: HTMLElement;
109+
110+
111+
private _sticky: boolean = false; // for dev purposes only
106112

107113
constructor(options: string[], selected: number, contextViewProvider: IContextViewProvider, styles: ISelectBoxStyles, selectBoxOptions?: ISelectBoxOptions) {
108114

@@ -153,6 +159,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
153159
dom.addClass(this.selectDropDownContainer, 'monaco-select-box-dropdown-padding');
154160
// Setup list for drop-down select
155161
this.createSelectList(this.selectDropDownContainer);
162+
this.selectionDetailsPane = dom.append(this.selectDropDownContainer, $('.select-box-details-pane'));
156163

157164
// Create span flex box item/div we can measure and control
158165
let widthControlOuterDiv = dom.append(this.selectDropDownContainer, $('.select-box-dropdown-container-width-control'));
@@ -285,6 +292,10 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
285292
this.selectList.getHTMLElement().setAttribute('aria-label', this.selectBoxOptions.ariaLabel);
286293
}
287294

295+
public setDetailsProvider(provider: (index: number) => { details: string, isMarkdown: boolean }): void {
296+
this.detailsProvider = provider;
297+
}
298+
288299
public focus(): void {
289300
if (this.selectElement) {
290301
this.selectElement.focus();
@@ -320,6 +331,15 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
320331
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused:not(:hover) { color: ${this.styles.listFocusForeground} !important; }`);
321332
}
322333

334+
if (!this.styles.selectBorder.equals(this.styles.selectBackground)) {
335+
content.push(`.monaco-select-box-dropdown-container { border: 1px solid ${this.styles.selectBorder} } `);
336+
content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane { border-top: 1px solid ${this.styles.selectBorder} } `);
337+
}
338+
else if (this.styles.selectListBorder) {
339+
content.push(`.monaco-select-box-dropdown-container { border: 1px solid ${this.styles.selectListBorder} } `);
340+
content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane { border-top: 1px solid ${this.styles.selectListBorder} } `);
341+
}
342+
323343
// Hover foreground - ignore for disabled options
324344
if (this.styles.listHoverForeground) {
325345
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:hover { color: ${this.styles.listHoverForeground} !important; }`);
@@ -370,6 +390,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
370390

371391
let listBackground = this.styles.selectListBackground ? this.styles.selectListBackground.toString() : background;
372392
this.selectDropDownListContainer.style.backgroundColor = listBackground;
393+
this.selectionDetailsPane.style.backgroundColor = listBackground;
373394
const optionsBorder = this.styles.focusBorder ? this.styles.focusBorder.toString() : null;
374395
this.selectDropDownContainer.style.outlineColor = optionsBorder;
375396
this.selectDropDownContainer.style.outlineOffset = '-1px';
@@ -543,9 +564,6 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
543564
this.selectList.reveal(this.selectList.getFocus()[0] || 0);
544565
}
545566

546-
// Set final container height after adjustments
547-
this.selectDropDownContainer.style.height = (listHeight + verticalPadding) + 'px';
548-
549567
// Determine optimal width - min(longest option), opt(parent select, excluding margins), max(ContextView controlled)
550568
const selectWidth = this.selectElement.offsetWidth;
551569
const selectMinWidth = this.setWidthControlElement(this.widthControlElement);
@@ -556,7 +574,6 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
556574
// Maintain focus outline on parent select as well as list container - tabindex for focus
557575
this.selectDropDownListContainer.setAttribute('tabindex', '0');
558576
dom.toggleClass(this.selectElement, 'synthetic-focus', true);
559-
dom.toggleClass(this.selectDropDownContainer, 'synthetic-focus', true);
560577
return true;
561578
} else {
562579
return false;
@@ -626,7 +643,11 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
626643
.filter(() => this.selectList.length > 0)
627644
.on(e => this.onMouseUp(e), this, this.toDispose);
628645

629-
this.toDispose.push(this.selectList.onDidBlur(e => this.onListBlur()));
646+
this.toDispose.push(
647+
this.selectList.onDidBlur(e => this.onListBlur()),
648+
this.selectList.onMouseOver(e => this.selectList.setFocus([e.index])),
649+
this.selectList.onFocusChange(e => this.onListFocus(e))
650+
);
630651

631652
this.selectList.getHTMLElement().setAttribute('aria-expanded', 'true');
632653
}
@@ -672,7 +693,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
672693

673694
// List Exit - passive - implicit no selection change, hide drop-down
674695
private onListBlur(): void {
675-
696+
if (this._sticky) { return; }
676697
if (this.selected !== this._currentSelection) {
677698
// Reset selected to current if no change
678699
this.select(this._currentSelection);
@@ -681,6 +702,48 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
681702
this.hideSelectDropDown(false);
682703
}
683704

705+
706+
private renderDescriptionMarkdown(text: string): HTMLElement {
707+
const cleanRenderedMarkdown = (element: Node) => {
708+
for (let i = 0; i < element.childNodes.length; i++) {
709+
const child = element.childNodes.item(i);
710+
711+
const tagName = (<Element>child).tagName && (<Element>child).tagName.toLowerCase();
712+
if (tagName === 'img') {
713+
element.removeChild(child);
714+
} else {
715+
cleanRenderedMarkdown(child);
716+
}
717+
}
718+
};
719+
720+
const renderedMarkdown = renderMarkdown({ value: text }, {
721+
actionHandler: this.selectBoxOptions.markdownActionHandler
722+
});
723+
724+
renderedMarkdown.classList.add('select-box-description-markdown');
725+
cleanRenderedMarkdown(renderedMarkdown);
726+
727+
return renderedMarkdown;
728+
}
729+
730+
// List Focus Change - passive - update details pane with newly focused element's data
731+
private onListFocus(e: IListEvent<ISelectOptionItem>) {
732+
this.selectionDetailsPane.innerText = '';
733+
const selectedIndex = e.indexes[0];
734+
let description = this.detailsProvider ? this.detailsProvider(selectedIndex) : { details: '', isMarkdown: false };
735+
if (description.details) {
736+
if (description.isMarkdown) {
737+
this.selectionDetailsPane.appendChild(this.renderDescriptionMarkdown(description.details));
738+
} else {
739+
this.selectionDetailsPane.innerText = description.details;
740+
}
741+
this.selectionDetailsPane.style.display = 'block';
742+
} else {
743+
this.selectionDetailsPane.style.display = 'none';
744+
}
745+
}
746+
684747
// List keyboard controller
685748

686749
// List exit - active - hide ContextView dropdown, reset selection, return focus to parent select

src/vs/base/browser/ui/selectBox/selectBoxNative.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ export class SelectBoxNative implements ISelectBoxDelegate {
113113
this.selectElement.setAttribute('aria-label', label);
114114
}
115115

116+
public setDetailsProvider(provider: any): void {
117+
console.error('details are not available for native select boxes');
118+
}
119+
116120
public focus(): void {
117121
if (this.selectElement) {
118122
this.selectElement.focus();

src/vs/platform/theme/common/styler.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
'use strict';
77

88
import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService';
9-
import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground } from 'vs/platform/theme/common/colorRegistry';
9+
import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder } from 'vs/platform/theme/common/colorRegistry';
1010
import { IDisposable } from 'vs/base/common/lifecycle';
1111
import { Color } from 'vs/base/common/color';
1212
import { mixin } from 'vs/base/common/objects';
@@ -129,7 +129,8 @@ export function attachSelectBoxStyler(widget: IThemable, themeService: IThemeSer
129129
listFocusOutline: (style && style.listFocusOutline) || activeContrastBorder,
130130
listHoverBackground: (style && style.listHoverBackground) || listHoverBackground,
131131
listHoverForeground: (style && style.listHoverForeground) || listHoverForeground,
132-
listHoverOutline: (style && style.listFocusOutline) || activeContrastBorder
132+
listHoverOutline: (style && style.listFocusOutline) || activeContrastBorder,
133+
selectListBorder: (style && style.selectListBorder) || editorWidgetBorder
133134
} as ISelectBoxStyleOverrides, widget);
134135
}
135136

src/vs/workbench/parts/preferences/browser/settingsEditor2.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ export class SettingsEditor2 extends BaseEditor {
494494
}
495495

496496
private updateTreeScrollSync(): void {
497+
this.settingsTreeRenderer.cancelSuggesters();
497498
if (this.searchResultModel) {
498499
return;
499500
}

src/vs/workbench/parts/preferences/browser/settingsTree.ts

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler, attach
4141
import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
4242
import { ITOCEntry } from 'vs/workbench/parts/preferences/browser/settingsLayout';
4343
import { ISettingsEditorViewState, isExcludeSetting, SettingsTreeElement, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement, settingKeyToDisplayFormat } from 'vs/workbench/parts/preferences/browser/settingsTreeModels';
44-
import { ExcludeSettingWidget, IExcludeDataItem, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/parts/preferences/browser/settingsWidgets';
44+
import { ExcludeSettingWidget, IExcludeDataItem, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectListBorder, settingsSelectForeground, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/parts/preferences/browser/settingsWidgets';
4545
import { ISetting, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences';
4646

4747
const $ = DOM.$;
@@ -721,15 +721,32 @@ export class SettingsRenderer implements ITreeRenderer {
721721
return template;
722722
}
723723

724+
public cancelSuggesters() {
725+
this.contextViewService.hideContextView();
726+
}
727+
724728
private renderSettingEnumTemplate(tree: ITree, container: HTMLElement): ISettingEnumItemTemplate {
725729
const common = this.renderCommonTemplate(tree, container, 'enum');
726730

727-
const selectBox = new SelectBox([], undefined, this.contextViewService);
731+
const selectBox = new SelectBox([], undefined, this.contextViewService, undefined, {
732+
hasDetails: true, markdownActionHandler: {
733+
callback: (content: string) => {
734+
if (startsWith(content, '#')) {
735+
this._onDidClickSettingLink.fire(content.substr(1));
736+
} else {
737+
this.openerService.open(URI.parse(content)).then(void 0, onUnexpectedError);
738+
}
739+
},
740+
disposeables: common.toDispose
741+
}
742+
});
743+
728744
common.toDispose.push(selectBox);
729745
common.toDispose.push(attachSelectBoxStyler(selectBox, this.themeService, {
730746
selectBackground: settingsSelectBackground,
731747
selectForeground: settingsSelectForeground,
732-
selectBorder: settingsSelectBorder
748+
selectBorder: settingsSelectBorder,
749+
selectListBorder: settingsSelectListBorder
733750
}));
734751
selectBox.render(common.controlElement);
735752
const selectElement = common.controlElement.querySelector('select');
@@ -1016,6 +1033,13 @@ export class SettingsRenderer implements ITreeRenderer {
10161033
private renderEnum(dataElement: SettingsTreeSettingElement, template: ISettingEnumItemTemplate, onChange: (value: string) => void): void {
10171034
const displayOptions = getDisplayEnumOptions(dataElement.setting);
10181035
template.selectBox.setOptions(displayOptions);
1036+
const descriptions = dataElement.setting.enumDescriptions;
1037+
const descriptionsAreMarkdown = dataElement.setting.descriptionIsMarkdown;
1038+
template.selectBox.setDetailsProvider(index =>
1039+
({
1040+
details: descriptions && descriptions[index] && (descriptionsAreMarkdown ? fixSettingLinks(descriptions[index]) : descriptions[index]),
1041+
isMarkdown: descriptionsAreMarkdown
1042+
}));
10191043

10201044
const modifiedText = dataElement.isConfigured ? 'Modified' : '';
10211045
const label = ' ' + dataElement.displayCategory + ' ' + dataElement.displayLabel + ' combobox ' + modifiedText;
@@ -1033,24 +1057,6 @@ export class SettingsRenderer implements ITreeRenderer {
10331057
}
10341058

10351059
template.enumDescriptionElement.innerHTML = '';
1036-
// if (dataElement.setting.enumDescriptions && dataElement.setting.enum && dataElement.setting.enum.length < SettingsRenderer.MAX_ENUM_DESCRIPTIONS) {
1037-
// if (isSelected) {
1038-
// let enumDescriptionText = '\n' + dataElement.setting.enumDescriptions
1039-
// .map((desc, i) => {
1040-
// const displayEnum = escapeInvisibleChars(dataElement.setting.enum[i]);
1041-
// return desc ?
1042-
// ` - \`${displayEnum}\`: ${desc}` :
1043-
// ` - \`${dataElement.setting.enum[i]}\``;
1044-
// })
1045-
// .filter(desc => !!desc)
1046-
// .join('\n');
1047-
1048-
// const renderedMarkdown = this.renderDescriptionMarkdown(fixSettingLinks(enumDescriptionText), template.toDispose);
1049-
// template.enumDescriptionElement.appendChild(renderedMarkdown);
1050-
// }
1051-
1052-
// return { overflows: true };
1053-
// }
10541060
}
10551061

10561062
private renderText(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, onChange: (value: string) => void): void {
@@ -1245,7 +1251,7 @@ export class SettingsTreeController extends WorkbenchTreeController {
12451251
const isLink = eventish.target.tagName.toLowerCase() === 'a' ||
12461252
eventish.target.parentElement.tagName.toLowerCase() === 'a'; // <code> inside <a>
12471253

1248-
if (isLink && DOM.findParentWithClass(eventish.target, 'setting-item-description-markdown', tree.getHTMLElement())) {
1254+
if (isLink && (DOM.findParentWithClass(eventish.target, 'setting-item-description-markdown', tree.getHTMLElement()) || DOM.findParentWithClass(eventish.target, 'select-box-description-markdown'))) {
12491255
return true;
12501256
}
12511257

0 commit comments

Comments
 (0)