From da732ab07c9f4c21511bf0c4ff9de037e7d28535 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 2 Jan 2019 17:16:56 -0800 Subject: [PATCH] Use new tree for settings editor --- src/vs/base/browser/ui/list/listView.ts | 4 + .../parts/preferences/browser/settingsTree.ts | 1627 ++++++++--------- .../preferences/browser/settingsWidgets.ts | 2 +- .../media/settingsEditor2.css | 39 +- .../electron-browser/settingsEditor2.ts | 268 ++- 5 files changed, 891 insertions(+), 1049 deletions(-) diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 486a975f47f..3cf4323966b 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -374,6 +374,10 @@ export class ListView implements ISpliceable, IDisposable { const item = this.items[index]; const renderer = this.renderers.get(item.templateId); + if (!item.row) { + return; + } + if (renderer.disposeElement) { renderer.disposeElement(item.element, index, item.row!.templateData); } diff --git a/src/vs/workbench/parts/preferences/browser/settingsTree.ts b/src/vs/workbench/parts/preferences/browser/settingsTree.ts index de3a9807169..b0f6eb9d770 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsTree.ts @@ -12,8 +12,12 @@ import { alert as ariaAlert } from 'vs/base/browser/ui/aria/aria'; import { Button } from 'vs/base/browser/ui/button/button'; import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; -import { SelectBox, ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IObjectTreeOptions, ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; +import { ITreeModel, ITreeNode, ITreeRenderer, ITreeFilter, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree'; import { Action, IAction } from 'vs/base/common/actions'; import * as arrays from 'vs/base/common/arrays'; import { Color, RGBA } from 'vs/base/common/color'; @@ -21,26 +25,24 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { ISpliceable } from 'vs/base/common/sequence'; import { escapeRegExpCharacters, startsWith } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -import { IAccessibilityProvider, IDataSource, IFilter, IRenderer as ITreeRenderer, ITree, ITreeConfiguration } from 'vs/base/parts/tree/browser/tree'; -import { DefaultTreestyler } from 'vs/base/parts/tree/browser/treeDefaults'; -import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; +import { ITree, IFilter, IAccessibilityProvider } from 'vs/base/parts/tree/browser/tree'; import { localize } from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { editorBackground, errorForeground, focusBorder, foreground, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder } from 'vs/platform/theme/common/colorRegistry'; +import { editorBackground, errorForeground, focusBorder, foreground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler, attachStyler } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ITOCEntry } from 'vs/workbench/parts/preferences/browser/settingsLayout'; -import { ISettingsEditorViewState, isExcludeSetting, settingKeyToDisplayFormat, SettingsTreeElement, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement } from 'vs/workbench/parts/preferences/browser/settingsTreeModels'; -import { ExcludeSettingWidget, IExcludeDataItem, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/parts/preferences/browser/settingsWidgets'; +import { ISettingsEditorViewState, settingKeyToDisplayFormat, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement } from 'vs/workbench/parts/preferences/browser/settingsTreeModels'; +import { ExcludeSettingWidget, IExcludeChangeEvent, IExcludeDataItem, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/parts/preferences/browser/settingsWidgets'; import { SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/parts/preferences/common/preferences'; import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; @@ -170,102 +172,6 @@ function getFlatSettings(settingsGroups: ISettingsGroup[]) { return result; } - -export class SettingsDataSource implements IDataSource { - - getId(tree: ITree, element: SettingsTreeElement): string { - return element.id; - } - - hasChildren(tree: ITree, element: SettingsTreeElement): boolean { - if (element instanceof SettingsTreeGroupElement) { - return true; - } - - return false; - } - - getChildren(tree: ITree, element: SettingsTreeElement): Promise { - return Promise.resolve(this._getChildren(element)); - } - - private _getChildren(element: SettingsTreeElement): SettingsTreeElement[] { - if (element instanceof SettingsTreeGroupElement) { - return element.children; - } else { - // No children... - return null; - } - } - - getParent(tree: ITree, element: SettingsTreeElement): Promise { - return Promise.resolve(element && element.parent); - } - - shouldAutoexpand(): boolean { - return true; - } -} - -export class SimplePagedDataSource implements IDataSource { - private static readonly SETTINGS_PER_PAGE = 30; - private static readonly BUFFER = 5; - - private loadedToIndex: number; - - constructor(private realDataSource: IDataSource) { - this.reset(); - } - - reset(): void { - this.loadedToIndex = SimplePagedDataSource.SETTINGS_PER_PAGE; - } - - pageTo(index: number, top = false): boolean { - const buffer = top ? SimplePagedDataSource.SETTINGS_PER_PAGE : SimplePagedDataSource.BUFFER; - - if (index > this.loadedToIndex - buffer) { - this.loadedToIndex = (Math.ceil(index / SimplePagedDataSource.SETTINGS_PER_PAGE) + 1) * SimplePagedDataSource.SETTINGS_PER_PAGE; - return true; - } else { - return false; - } - } - - getId(tree: ITree, element: any): string { - return this.realDataSource.getId(tree, element); - } - - hasChildren(tree: ITree, element: any): boolean { - return this.realDataSource.hasChildren(tree, element); - } - - getChildren(tree: ITree, element: SettingsTreeGroupElement): Promise { - return this.realDataSource.getChildren(tree, element).then(realChildren => { - return this._getChildren(realChildren); - }); - } - - _getChildren(realChildren: SettingsTreeElement[]): any[] { - const lastChild = realChildren[realChildren.length - 1]; - if (lastChild && lastChild.index > this.loadedToIndex) { - return realChildren.filter(child => { - return child.index < this.loadedToIndex; - }); - } else { - return realChildren; - } - } - - getParent(tree: ITree, element: any): Promise { - return this.realDataSource.getParent(tree, element); - } - - shouldAutoexpand(tree: ITree, element: any): boolean { - return this.realDataSource.shouldAutoexpand(tree, element); - } -} - interface IDisposableTemplate { toDispose: IDisposable[]; } @@ -325,7 +231,7 @@ const SETTINGS_BOOL_TEMPLATE_ID = 'settings.bool.template'; const SETTINGS_EXCLUDE_TEMPLATE_ID = 'settings.exclude.template'; const SETTINGS_COMPLEX_TEMPLATE_ID = 'settings.complex.template'; const SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID = 'settings.newExtensions.template'; -const SETTINGS_GROUP_ELEMENT_TEMPLATE_ID = 'settings.group.template'; +const SETTINGS_ELEMENT_TEMPLATE_ID = 'settings.group.template'; export interface ISettingChangeEvent { key: string; @@ -343,20 +249,22 @@ export interface ISettingOverrideClickEvent { targetKey: string; } -export class SettingsRenderer implements ITreeRenderer { +export abstract class AbstractSettingRenderer implements ITreeRenderer { + /** To override */ + templateId = undefined; public static readonly CONTROL_CLASS = 'setting-control-focus-target'; - public static readonly CONTROL_SELECTOR = '.' + SettingsRenderer.CONTROL_CLASS; + public static readonly CONTROL_SELECTOR = '.' + AbstractSettingRenderer.CONTROL_CLASS; public static readonly SETTING_KEY_ATTR = 'data-key'; private readonly _onDidClickOverrideElement = new Emitter(); public readonly onDidClickOverrideElement: Event = this._onDidClickOverrideElement.event; - private readonly _onDidChangeSetting = new Emitter(); + protected readonly _onDidChangeSetting = new Emitter(); public readonly onDidChangeSetting: Event = this._onDidChangeSetting.event; - private readonly _onDidOpenSettings = new Emitter(); + protected readonly _onDidOpenSettings = new Emitter(); public readonly onDidOpenSettings: Event = this._onDidOpenSettings.event; private readonly _onDidClickSettingLink = new Emitter(); @@ -365,248 +273,28 @@ export class SettingsRenderer implements ITreeRenderer { private readonly _onDidFocusSetting = new Emitter(); public readonly onDidFocusSetting: Event = this._onDidFocusSetting.event; - private descriptionMeasureContainer: HTMLElement; - private longestSingleLineDescription = 0; - - private rowHeightCache = new Map(); - private lastRenderedWidth: number; - - private settingActions: IAction[]; - + // Put common injections back here constructor( - _measureParent: HTMLElement, - @IThemeService private themeService: IThemeService, - @IContextViewService private contextViewService: IContextViewService, - @IOpenerService private readonly openerService: IOpenerService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @ICommandService private readonly commandService: ICommandService, - @IContextMenuService private contextMenuService: IContextMenuService, - @IKeybindingService private keybindingService: IKeybindingService, + private readonly settingActions: IAction[], + @IThemeService protected readonly _themeService: IThemeService, + @IContextViewService protected readonly _contextViewService: IContextViewService, + @IOpenerService protected readonly _openerService: IOpenerService, + @IInstantiationService protected readonly _instantiationService: IInstantiationService, + @ICommandService protected readonly _commandService: ICommandService, + @IContextMenuService protected readonly _contextMenuService: IContextMenuService, + @IKeybindingService protected readonly _keybindingService: IKeybindingService, ) { - this.descriptionMeasureContainer = $('.setting-item-description'); - DOM.append(_measureParent, - $('.setting-measure-container.monaco-tree.settings-editor-tree', undefined, - $('.monaco-scrollable-element', undefined, - $('.monaco-tree-wrapper', undefined, - $('.monaco-tree-rows', undefined, - $('.monaco-tree-row', undefined, - $('.setting-item', undefined, - this.descriptionMeasureContainer))))))); - - this.settingActions = [ - new Action('settings.resetSetting', localize('resetSettingLabel', "Reset Setting"), undefined, undefined, (context: SettingsTreeSettingElement) => { - if (context) { - this._onDidChangeSetting.fire({ key: context.setting.key, value: undefined, type: context.setting.type as SettingValueType }); - } - - return Promise.resolve(null); - }), - new Separator(), - this.instantiationService.createInstance(CopySettingIdAction), - this.instantiationService.createInstance(CopySettingAsJSONAction), - ]; - - } - - showContextMenu(element: SettingsTreeSettingElement, settingDOMElement: HTMLElement): void { - const toolbarElement: HTMLElement = settingDOMElement.querySelector('.toolbar-toggle-more'); - if (toolbarElement) { - this.contextMenuService.showContextMenu({ - getActions: () => this.settingActions, - getAnchor: () => toolbarElement, - getActionsContext: () => element - }); - } - } - - updateWidth(width: number): void { - if (this.lastRenderedWidth !== width) { - this.rowHeightCache = new Map(); - } - this.longestSingleLineDescription = 0; - - this.lastRenderedWidth = width; - } - - getHeight(tree: ITree, element: SettingsTreeElement): number { - if (this.rowHeightCache.has(element.id) && !(element instanceof SettingsTreeSettingElement && isExcludeSetting(element.setting))) { - return this.rowHeightCache.get(element.id); - } - - const h = this._getHeight(tree, element); - this.rowHeightCache.set(element.id, h); - return h; - } - - _getHeight(tree: ITree, element: SettingsTreeElement): number { - if (element instanceof SettingsTreeGroupElement) { - if (element.isFirstGroup) { - return 31; - } - - return 40 + (7 * element.level); - } - - if (element instanceof SettingsTreeSettingElement) { - if (isExcludeSetting(element.setting)) { - return this._getExcludeSettingHeight(element); - } else { - return this.measureSettingElementHeight(tree, element); - } - } - - if (element instanceof SettingsTreeNewExtensionsElement) { - return 40; - } - - return 0; - } - - _getExcludeSettingHeight(element: SettingsTreeSettingElement): number { - const displayValue = getExcludeDisplayValue(element); - return (displayValue.length + 1) * 22 + 66 + this.measureSettingDescription(element); - } - - private measureSettingElementHeight(tree: ITree, element: SettingsTreeSettingElement): number { - let heightExcludingDescription = 86; - - if (element.valueType === 'boolean') { - heightExcludingDescription = 60; - } - - return heightExcludingDescription + this.measureSettingDescription(element); - } - - private measureSettingDescription(element: SettingsTreeSettingElement): number { - if (element.description.length < this.longestSingleLineDescription * .8) { - // Most setting descriptions are one short line, so try to avoid measuring them. - // If the description is less than 80% of the longest single line description, assume this will also render to be one line. - return 18; - } - - const boolMeasureClass = 'measure-bool-description'; - if (element.valueType === 'boolean') { - this.descriptionMeasureContainer.classList.add(boolMeasureClass); - } else if (this.descriptionMeasureContainer.classList.contains(boolMeasureClass)) { - this.descriptionMeasureContainer.classList.remove(boolMeasureClass); - } - - const shouldRenderMarkdown = element.setting.descriptionIsMarkdown && element.description.indexOf('\n- ') >= 0; - - while (this.descriptionMeasureContainer.firstChild) { - this.descriptionMeasureContainer.removeChild(this.descriptionMeasureContainer.firstChild); - } - - if (shouldRenderMarkdown) { - const text = fixSettingLinks(element.description); - const rendered = renderMarkdown({ value: text }); - rendered.classList.add('setting-item-description-markdown'); - this.descriptionMeasureContainer.appendChild(rendered); - - return this.descriptionMeasureContainer.offsetHeight; - } else { - // Remove markdown links, setting links, backticks - const measureText = element.setting.descriptionIsMarkdown ? - fixSettingLinks(element.description) - .replace(/\[(.*)\]\(.*\)/g, '$1') - .replace(/`([^`]*)`/g, '$1') : - element.description; - - this.descriptionMeasureContainer.innerText = measureText; - const h = this.descriptionMeasureContainer.offsetHeight; - if (h < 20 && measureText.length > this.longestSingleLineDescription) { - this.longestSingleLineDescription = measureText.length; - } - - return h; - } - } - - getTemplateId(tree: ITree, element: SettingsTreeElement): string { - if (element instanceof SettingsTreeGroupElement) { - return SETTINGS_GROUP_ELEMENT_TEMPLATE_ID; - } - - if (element instanceof SettingsTreeSettingElement) { - if (element.valueType === 'boolean') { - return SETTINGS_BOOL_TEMPLATE_ID; - } - - if (element.valueType === 'integer' || element.valueType === 'number' || element.valueType === 'nullable-integer' || element.valueType === 'nullable-number') { - return SETTINGS_NUMBER_TEMPLATE_ID; - } - - if (element.valueType === 'string') { - return SETTINGS_TEXT_TEMPLATE_ID; - } - - if (element.valueType === 'enum') { - return SETTINGS_ENUM_TEMPLATE_ID; - } - - if (element.valueType === 'exclude') { - return SETTINGS_EXCLUDE_TEMPLATE_ID; - } - - return SETTINGS_COMPLEX_TEMPLATE_ID; - } - - if (element instanceof SettingsTreeNewExtensionsElement) { - return SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID; - } - - return ''; } - renderTemplate(tree: ITree, templateId: string, container: HTMLElement) { - if (templateId === SETTINGS_GROUP_ELEMENT_TEMPLATE_ID) { - return this.renderGroupTitleTemplate(container); - } - - if (templateId === SETTINGS_TEXT_TEMPLATE_ID) { - return this.renderSettingTextTemplate(tree, container); - } - - if (templateId === SETTINGS_NUMBER_TEMPLATE_ID) { - return this.renderSettingNumberTemplate(tree, container); - } - - if (templateId === SETTINGS_BOOL_TEMPLATE_ID) { - return this.renderSettingBoolTemplate(tree, container); - } - - if (templateId === SETTINGS_ENUM_TEMPLATE_ID) { - return this.renderSettingEnumTemplate(tree, container); - } - - if (templateId === SETTINGS_EXCLUDE_TEMPLATE_ID) { - return this.renderSettingExcludeTemplate(tree, container); - } - - if (templateId === SETTINGS_COMPLEX_TEMPLATE_ID) { - return this.renderSettingComplexTemplate(tree, container); - } - - if (templateId === SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID) { - return this.renderNewExtensionsTemplate(container); - } - - return null; + renderTemplate(container: HTMLElement): any { + throw new Error('to override'); } - private renderGroupTitleTemplate(container: HTMLElement): IGroupTitleTemplate { - DOM.addClass(container, 'group-title'); - - const toDispose: IDisposable[] = []; - const template: IGroupTitleTemplate = { - parent: container, - toDispose - }; - - return template; + renderElement(element: ITreeNode, index: number, templateData: any): void { + throw new Error('to override'); } - private renderCommonTemplate(tree: ITree, container: HTMLElement, typeClass: string): ISettingItemTemplate { + protected renderCommonTemplate(tree: any, container: HTMLElement, typeClass: string): ISettingItemTemplate { DOM.addClass(container, 'setting-item'); DOM.addClass(container, 'setting-item-' + typeClass); const titleElement = DOM.append(container, $('.setting-item-title')); @@ -647,17 +335,10 @@ export class SettingsRenderer implements ITreeRenderer { toDispose.push(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_ENTER, e => container.classList.add('mouseover'))); toDispose.push(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover'))); - toDispose.push(DOM.addStandardDisposableListener(valueElement, 'keydown', (e: StandardKeyboardEvent) => { - if (e.keyCode === KeyCode.Escape) { - tree.domFocus(); - e.browserEvent.stopPropagation(); - } - })); - return template; } - private addSettingElementFocusHandler(template: ISettingItemTemplate): void { + protected addSettingElementFocusHandler(template: ISettingItemTemplate): void { const focusTracker = DOM.trackFocus(template.containerElement); template.toDispose.push(focusTracker); focusTracker.onDidBlur(() => { @@ -675,76 +356,14 @@ export class SettingsRenderer implements ITreeRenderer { }); } - private renderSettingTextTemplate(tree: ITree, container: HTMLElement, type = 'text'): ISettingTextItemTemplate { - const common = this.renderCommonTemplate(tree, container, 'text'); - const validationErrorMessageElement = DOM.append(container, $('.setting-item-validation-message')); - - const inputBox = new InputBox(common.controlElement, this.contextViewService); - common.toDispose.push(inputBox); - common.toDispose.push(attachInputBoxStyler(inputBox, this.themeService, { - inputBackground: settingsTextInputBackground, - inputForeground: settingsTextInputForeground, - inputBorder: settingsTextInputBorder - })); - common.toDispose.push( - inputBox.onDidChange(e => { - if (template.onChange) { - template.onChange(e); - } - })); - common.toDispose.push(inputBox); - inputBox.inputElement.classList.add(SettingsRenderer.CONTROL_CLASS); - - const template: ISettingTextItemTemplate = { - ...common, - inputBox, - validationErrorMessageElement - }; - - this.addSettingElementFocusHandler(template); - - return template; - } - - private renderSettingNumberTemplate(tree: ITree, container: HTMLElement): ISettingNumberItemTemplate { - const common = this.renderCommonTemplate(tree, container, 'number'); - const validationErrorMessageElement = DOM.append(container, $('.setting-item-validation-message')); - - const inputBox = new InputBox(common.controlElement, this.contextViewService, { type: 'number' }); - common.toDispose.push(inputBox); - common.toDispose.push(attachInputBoxStyler(inputBox, this.themeService, { - inputBackground: settingsNumberInputBackground, - inputForeground: settingsNumberInputForeground, - inputBorder: settingsNumberInputBorder - })); - common.toDispose.push( - inputBox.onDidChange(e => { - if (template.onChange) { - template.onChange(e); - } - })); - common.toDispose.push(inputBox); - inputBox.inputElement.classList.add(SettingsRenderer.CONTROL_CLASS); - - const template: ISettingNumberItemTemplate = { - ...common, - inputBox, - validationErrorMessageElement - }; - - this.addSettingElementFocusHandler(template); - - return template; - } - - private renderSettingToolbar(container: HTMLElement): ToolBar { - const toggleMenuKeybinding = this.keybindingService.lookupKeybinding(SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU); + protected renderSettingToolbar(container: HTMLElement): ToolBar { + const toggleMenuKeybinding = this._keybindingService.lookupKeybinding(SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU); let toggleMenuTitle = localize('settingsContextMenuTitle', "More Actions... "); if (toggleMenuKeybinding) { toggleMenuTitle += ` (${toggleMenuKeybinding && toggleMenuKeybinding.getLabel()})`; } - const toolbar = new ToolBar(container, this.contextMenuService, { + const toolbar = new ToolBar(container, this._contextMenuService, { toggleMenuTitle }); toolbar.setActions([], this.settingActions)(); @@ -756,95 +375,418 @@ export class SettingsRenderer implements ITreeRenderer { return toolbar; } - private renderSettingBoolTemplate(tree: ITree, container: HTMLElement): ISettingBoolItemTemplate { - DOM.addClass(container, 'setting-item'); - DOM.addClass(container, 'setting-item-bool'); + protected renderSettingElement(node: ITreeNode, index: number, template: ISettingItemTemplate | ISettingBoolItemTemplate): void { + const element = node.element; + template.context = element; + template.toolbar.context = element; - const titleElement = DOM.append(container, $('.setting-item-title')); - const categoryElement = DOM.append(titleElement, $('span.setting-item-category')); - const labelElement = DOM.append(titleElement, $('span.setting-item-label')); - const otherOverridesElement = DOM.append(titleElement, $('span.setting-item-overrides')); + const setting = element.setting; - const descriptionAndValueElement = DOM.append(container, $('.setting-item-value-description')); - const controlElement = DOM.append(descriptionAndValueElement, $('.setting-item-bool-control')); - const descriptionElement = DOM.append(descriptionAndValueElement, $('.setting-item-description')); - const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); - modifiedIndicatorElement.title = localize('modified', "Modified"); + DOM.toggleClass(template.containerElement, 'is-configured', element.isConfigured); + DOM.toggleClass(template.containerElement, 'is-expanded', true); + template.containerElement.setAttribute(AbstractSettingRenderer.SETTING_KEY_ATTR, element.setting.key); + + const titleTooltip = setting.key + (element.isConfigured ? ' - Modified' : ''); + template.categoryElement.textContent = element.displayCategory && (element.displayCategory + ': '); + template.categoryElement.title = titleTooltip; + template.labelElement.textContent = element.displayLabel; + template.labelElement.title = titleTooltip; - const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); + template.descriptionElement.innerHTML = ''; + if (element.setting.descriptionIsMarkdown) { + const renderedDescription = this.renderDescriptionMarkdown(element, element.description, template.toDispose); + template.descriptionElement.appendChild(renderedDescription); + } else { + template.descriptionElement.innerText = element.description; + } + + const baseId = (element.displayCategory + '_' + element.displayLabel).replace(/ /g, '_').toLowerCase(); + template.descriptionElement.id = baseId + '_setting_description'; + + template.otherOverridesElement.innerHTML = ''; + + if (element.overriddenScopeList.length) { + const otherOverridesLabel = element.isConfigured ? + localize('alsoConfiguredIn', "Also modified in") : + localize('configuredIn', "Modified in"); + + DOM.append(template.otherOverridesElement, $('span', null, `(${otherOverridesLabel}: `)); + + for (let i = 0; i < element.overriddenScopeList.length; i++) { + let view = DOM.append(template.otherOverridesElement, $('a.modified-scope', null, element.overriddenScopeList[i])); + + if (i !== element.overriddenScopeList.length - 1) { + DOM.append(template.otherOverridesElement, $('span', null, ', ')); + } else { + DOM.append(template.otherOverridesElement, $('span', null, ')')); + } + + DOM.addStandardDisposableListener(view, DOM.EventType.CLICK, (e: IMouseEvent) => { + this._onDidClickOverrideElement.fire({ + targetKey: element.setting.key, + scope: element.overriddenScopeList[i] + }); + e.preventDefault(); + e.stopPropagation(); + }); + } + } + + const onChange = value => this._onDidChangeSetting.fire({ key: element.setting.key, value, type: template.context.valueType }); + template.deprecationWarningElement.innerText = element.setting.deprecationMessage || ''; + this.renderValue(element, template, onChange); + + // Remove tree attributes - sometimes overridden by tree - should be managed there + template.containerElement.parentElement.parentElement.removeAttribute('role'); + template.containerElement.parentElement.parentElement.removeAttribute('aria-level'); + template.containerElement.parentElement.parentElement.removeAttribute('aria-posinset'); + template.containerElement.parentElement.parentElement.removeAttribute('aria-setsize'); + } + + private renderDescriptionMarkdown(element: SettingsTreeSettingElement, text: string, disposeables: IDisposable[]): HTMLElement { + // Rewrite `#editor.fontSize#` to link format + text = fixSettingLinks(text); + + const renderedMarkdown = renderMarkdown({ value: text }, { + actionHandler: { + callback: (content: string) => { + if (startsWith(content, '#')) { + const e: ISettingLinkClickEvent = { + source: element, + targetKey: content.substr(1) + }; + this._onDidClickSettingLink.fire(e); + } else { + let uri: URI; + try { + uri = URI.parse(content); + } catch (err) { + // ignore + } + if (uri) { + this._openerService.open(uri).catch(onUnexpectedError); + } + } + }, + disposeables + } + }); + + renderedMarkdown.classList.add('setting-item-description-markdown'); + cleanRenderedMarkdown(renderedMarkdown); + return renderedMarkdown; + } + + protected abstract renderValue(dataElement: SettingsTreeSettingElement, template: ISettingItemTemplate, onChange: (value: any) => void): void; + + protected setElementAriaLabels(dataElement: SettingsTreeSettingElement, templateId: string, template: ISettingItemTemplate): string { + // Create base Id for element references + const baseId = (dataElement.displayCategory + '_' + dataElement.displayLabel).replace(/ /g, '_').toLowerCase(); + + const modifiedText = template.otherOverridesElement.textContent ? + template.otherOverridesElement.textContent : (dataElement.isConfigured ? localize('settings.Modified', ' Modified. ') : ''); + + let itemElement = null; + + // Use '.' as reader pause + let label = dataElement.displayCategory + ' ' + dataElement.displayLabel + '. '; + + // Setup and add ARIA attributes + // Create id and label for control/input element - parent is wrapper div + + if (templateId === SETTINGS_TEXT_TEMPLATE_ID) { + if (itemElement = (template).inputBox.inputElement) { + itemElement.setAttribute('role', 'textbox'); + label += modifiedText; + } + } else if (templateId === SETTINGS_NUMBER_TEMPLATE_ID) { + if (itemElement = (template).inputBox.inputElement) { + itemElement.setAttribute('role', 'textbox'); + label += ' number. ' + modifiedText; + } + } else if (templateId === SETTINGS_BOOL_TEMPLATE_ID) { + if (itemElement = (template).checkbox.domNode) { + itemElement.setAttribute('role', 'checkbox'); + label += modifiedText; + // Add checkbox target to description clickable and able to toggle checkbox + template.descriptionElement.setAttribute('checkbox_label_target_id', baseId + '_setting_item'); + } + } else if (templateId === SETTINGS_ENUM_TEMPLATE_ID) { + if (itemElement = template.controlElement.firstElementChild) { + itemElement.setAttribute('role', 'combobox'); + label += modifiedText; + } + } else { + // Don't change attributes if we don't know what we areFunctions + return ''; + } + + // We don't have control element, return empty label + if (!itemElement) { + return ''; + } + + // Labels will not be read on descendent input elements of the parent treeitem + // unless defined as roles for input items + // voiceover does not seem to use labeledby correctly, set labels directly on input elements + itemElement.id = baseId + '_setting_item'; + itemElement.setAttribute('aria-label', label); + itemElement.setAttribute('aria-describedby', baseId + '_setting_description settings_aria_more_actions_shortcut_label'); + + return label; + } + + disposeTemplate(template: IDisposableTemplate): void { + dispose(template.toDispose); + } +} + +export class SettingGroupRenderer implements ITreeRenderer { + templateId = SETTINGS_ELEMENT_TEMPLATE_ID; + + renderTemplate(container: HTMLElement): IGroupTitleTemplate { + DOM.addClass(container, 'group-title'); const toDispose: IDisposable[] = []; - const checkbox = new Checkbox({ actionClassName: 'setting-value-checkbox', isChecked: true, title: '', inputActiveOptionBorder: null }); - controlElement.appendChild(checkbox.domNode); - toDispose.push(checkbox); - toDispose.push(checkbox.onChange(() => { - if (template.onChange) { - template.onChange(checkbox.checked); + const template: IGroupTitleTemplate = { + parent: container, + toDispose + }; + + return template; + } + + renderElement(element: ITreeNode, index: number, templateData: IGroupTitleTemplate): void { + templateData.parent.innerHTML = ''; + const labelElement = DOM.append(templateData.parent, $('div.settings-group-title-label')); + labelElement.classList.add(`settings-group-level-${element.element.level}`); + labelElement.textContent = element.element.label; + + if (element.element.isFirstGroup) { + labelElement.classList.add('settings-group-first'); + } + } + + disposeTemplate(templateData: IGroupTitleTemplate): void { + } +} + +export class SettingNewExtensionsRenderer implements ITreeRenderer { + templateId = SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID; + + constructor( + @IThemeService private _themeService: IThemeService, + @ICommandService private _commandService: ICommandService, + ) { + } + + renderTemplate(container: HTMLElement): ISettingNewExtensionsTemplate { + const toDispose: IDisposable[] = []; + + container.classList.add('setting-item-new-extensions'); + + const button = new Button(container, { title: true, buttonBackground: null, buttonHoverBackground: null }); + toDispose.push(button); + toDispose.push(button.onDidClick(() => { + if (template.context) { + this._commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', template.context.extensionIds); } })); + button.label = localize('newExtensionsButtonLabel', "Show matching extensions"); + button.element.classList.add('settings-new-extensions-button'); + toDispose.push(attachButtonStyler(button, this._themeService)); - // Need to listen for mouse clicks on description and toggle checkbox - use target ID for safety - // Also have to ignore embedded links - too buried to stop propagation - toDispose.push(DOM.addDisposableListener(descriptionElement, DOM.EventType.MOUSE_DOWN, (e) => { - const targetElement = e.target; - const targetId = descriptionElement.getAttribute('checkbox_label_target_id'); + const template: ISettingNewExtensionsTemplate = { + button, + toDispose + }; - // Make sure we are not a link and the target ID matches - // Toggle target checkbox - if (targetElement.tagName.toLowerCase() !== 'a' && targetId === template.checkbox.domNode.id) { - template.checkbox.checked = template.checkbox.checked ? false : true; - template.onChange(checkbox.checked); - } - DOM.EventHelper.stop(e); + return template; + } + + renderElement(element: ITreeNode, index: number, templateData: ISettingNewExtensionsTemplate): void { + templateData.context = element.element; + } + + disposeTemplate(template: IDisposableTemplate): void { + dispose(template.toDispose); + } +} + +export class SettingComplexRenderer extends AbstractSettingRenderer implements ITreeRenderer { + templateId = SETTINGS_COMPLEX_TEMPLATE_ID; + + renderTemplate(container: HTMLElement): ISettingComplexItemTemplate { + const common = this.renderCommonTemplate(null, container, 'complex'); + + const openSettingsButton = new Button(common.controlElement, { title: true, buttonBackground: null, buttonHoverBackground: null }); + common.toDispose.push(openSettingsButton); + common.toDispose.push(openSettingsButton.onDidClick(() => template.onChange(null))); + openSettingsButton.label = localize('editInSettingsJson', "Edit in settings.json"); + openSettingsButton.element.classList.add('edit-in-settings-button'); + + common.toDispose.push(attachButtonStyler(openSettingsButton, this._themeService, { + buttonBackground: Color.transparent.toString(), + buttonHoverBackground: Color.transparent.toString(), + buttonForeground: 'foreground' })); + const template: ISettingComplexItemTemplate = { + ...common, + button: openSettingsButton + }; - checkbox.domNode.classList.add(SettingsRenderer.CONTROL_CLASS); - const toolbarContainer = DOM.append(container, $('.setting-toolbar-container')); - const toolbar = this.renderSettingToolbar(toolbarContainer); - toDispose.push(toolbar); + this.addSettingElementFocusHandler(template); - const template: ISettingBoolItemTemplate = { - toDispose, + return template; + } - containerElement: container, - categoryElement, - labelElement, - controlElement, - checkbox, - descriptionElement, - deprecationWarningElement, - otherOverridesElement, - toolbar + renderElement(element: ITreeNode, index: number, templateData: ISettingComplexItemTemplate): void { + super.renderSettingElement(element, index, templateData); + } + + protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingExcludeItemTemplate, onChange: (value: string) => void): void { + template.onChange = () => this._onDidOpenSettings.fire(dataElement.setting.key); + } +} + +export class SettingExcludeRenderer extends AbstractSettingRenderer implements ITreeRenderer { + templateId = SETTINGS_EXCLUDE_TEMPLATE_ID; + + renderTemplate(container: HTMLElement): ISettingExcludeItemTemplate { + const common = this.renderCommonTemplate(null, container, 'exclude'); + + const excludeWidget = this._instantiationService.createInstance(ExcludeSettingWidget, common.controlElement); + excludeWidget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + common.toDispose.push(excludeWidget); + + const template: ISettingExcludeItemTemplate = { + ...common, + excludeWidget }; this.addSettingElementFocusHandler(template); - // Prevent clicks from being handled by list - toDispose.push(DOM.addDisposableListener(controlElement, 'mousedown', e => e.stopPropagation())); + common.toDispose.push(excludeWidget.onDidChangeExclude(e => this.onDidChangeExclude(template, e))); + + return template; + } + + private onDidChangeExclude(template: ISettingExcludeItemTemplate, e: IExcludeChangeEvent): void { + if (template.context) { + const newValue = { ...template.context.scopeValue }; + + // first delete the existing entry, if present + if (e.originalPattern) { + if (e.originalPattern in template.context.defaultValue) { + // delete a default by overriding it + newValue[e.originalPattern] = false; + } else { + delete newValue[e.originalPattern]; + } + } + + // then add the new or updated entry, if present + if (e.pattern) { + if (e.pattern in template.context.defaultValue && !e.sibling) { + // add a default by deleting its override + delete newValue[e.pattern]; + } else { + newValue[e.pattern] = e.sibling ? { when: e.sibling } : true; + } + } + + const sortKeys = (obj) => { + const keyArray = Object.keys(obj) + .map(key => ({ key, val: obj[key] })) + .sort((a, b) => a.key.localeCompare(b.key)); + + const retVal = {}; + keyArray.forEach(pair => { + retVal[pair.key] = pair.val; + }); + return retVal; + }; + + this._onDidChangeSetting.fire({ + key: template.context.setting.key, + value: Object.keys(newValue).length === 0 ? undefined : sortKeys(newValue), + type: template.context.valueType + }); + } + } + + renderElement(element: ITreeNode, index: number, templateData: ISettingExcludeItemTemplate): void { + super.renderSettingElement(element, index, templateData); + } + + protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingExcludeItemTemplate, onChange: (value: string) => void): void { + const value = getExcludeDisplayValue(dataElement); + template.excludeWidget.setValue(value); + template.context = dataElement; + } +} + +export class SettingTextRenderer extends AbstractSettingRenderer implements ITreeRenderer { + templateId = SETTINGS_TEXT_TEMPLATE_ID; + + renderTemplate(container: HTMLElement): ISettingTextItemTemplate { + const common = this.renderCommonTemplate(null, container, 'text'); + const validationErrorMessageElement = DOM.append(container, $('.setting-item-validation-message')); + + const inputBox = new InputBox(common.controlElement, this._contextViewService); + common.toDispose.push(inputBox); + common.toDispose.push(attachInputBoxStyler(inputBox, this._themeService, { + inputBackground: settingsTextInputBackground, + inputForeground: settingsTextInputForeground, + inputBorder: settingsTextInputBorder + })); + common.toDispose.push( + inputBox.onDidChange(e => { + if (template.onChange) { + template.onChange(e); + } + })); + common.toDispose.push(inputBox); + inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + + const template: ISettingTextItemTemplate = { + ...common, + inputBox, + validationErrorMessageElement + }; - toDispose.push(DOM.addStandardDisposableListener(controlElement, 'keydown', (e: StandardKeyboardEvent) => { - if (e.keyCode === KeyCode.Escape) { - tree.domFocus(); - e.browserEvent.stopPropagation(); - } - })); + this.addSettingElementFocusHandler(template); return template; } - public cancelSuggesters() { - this.contextViewService.hideContextView(); + renderElement(element: ITreeNode, index: number, templateData: ISettingTextItemTemplate): void { + super.renderSettingElement(element, index, templateData); + } + + protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, onChange: (value: string) => void): void { + const label = this.setElementAriaLabels(dataElement, SETTINGS_TEXT_TEMPLATE_ID, template); + + template.onChange = null; + template.inputBox.value = dataElement.value; + template.onChange = value => { renderValidations(dataElement, template, false, label); onChange(value); }; + + renderValidations(dataElement, template, true, label); } +} + +export class SettingEnumRenderer extends AbstractSettingRenderer implements ITreeRenderer { + templateId = SETTINGS_ENUM_TEMPLATE_ID; - private renderSettingEnumTemplate(tree: ITree, container: HTMLElement): ISettingEnumItemTemplate { - const common = this.renderCommonTemplate(tree, container, 'enum'); + renderTemplate(container: HTMLElement): ISettingEnumItemTemplate { + const common = this.renderCommonTemplate(null, container, 'enum'); - const selectBox = new SelectBox([], undefined, this.contextViewService, undefined, { useCustomDrawn: true }); + const selectBox = new SelectBox([], undefined, this._contextViewService, undefined, { useCustomDrawn: true }); common.toDispose.push(selectBox); - common.toDispose.push(attachSelectBoxStyler(selectBox, this.themeService, { + common.toDispose.push(attachSelectBoxStyler(selectBox, this._themeService, { selectBackground: settingsSelectBackground, selectForeground: settingsSelectForeground, selectBorder: settingsSelectBorder, @@ -853,7 +795,7 @@ export class SettingsRenderer implements ITreeRenderer { selectBox.render(common.controlElement); const selectElement = common.controlElement.querySelector('select'); if (selectElement) { - selectElement.classList.add(SettingsRenderer.CONTROL_CLASS); + selectElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); } common.toDispose.push( @@ -876,85 +818,65 @@ export class SettingsRenderer implements ITreeRenderer { return template; } - private renderSettingExcludeTemplate(tree: ITree, container: HTMLElement): ISettingExcludeItemTemplate { - const common = this.renderCommonTemplate(tree, container, 'exclude'); - - const excludeWidget = this.instantiationService.createInstance(ExcludeSettingWidget, common.controlElement); - excludeWidget.domNode.classList.add(SettingsRenderer.CONTROL_CLASS); - common.toDispose.push(excludeWidget); - - const template: ISettingExcludeItemTemplate = { - ...common, - excludeWidget - }; - - this.addSettingElementFocusHandler(template); - - common.toDispose.push(excludeWidget.onDidChangeExclude(e => { - if (template.context) { - const newValue = { ...template.context.scopeValue }; + renderElement(element: ITreeNode, index: number, templateData: ISettingEnumItemTemplate): void { + super.renderSettingElement(element, index, templateData); + } - // first delete the existing entry, if present - if (e.originalPattern) { - if (e.originalPattern in template.context.defaultValue) { - // delete a default by overriding it - newValue[e.originalPattern] = false; - } else { - delete newValue[e.originalPattern]; - } - } + protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingEnumItemTemplate, onChange: (value: string) => void): void { + const enumDescriptions = dataElement.setting.enumDescriptions; + const enumDescriptionsAreMarkdown = dataElement.setting.enumDescriptionsAreMarkdown; - // then add the new or updated entry, if present - if (e.pattern) { - if (e.pattern in template.context.defaultValue && !e.sibling) { - // add a default by deleting its override - delete newValue[e.pattern]; - } else { - newValue[e.pattern] = e.sibling ? { when: e.sibling } : true; - } - } + let displayOptions = dataElement.setting.enum + .map(String) + .map(escapeInvisibleChars) + .map((data, index) => { + text: data, + description: (enumDescriptions && enumDescriptions[index] && (enumDescriptionsAreMarkdown ? fixSettingLinks(enumDescriptions[index], false) : enumDescriptions[index])), + descriptionIsMarkdown: enumDescriptionsAreMarkdown, + decoratorRight: (data === dataElement.defaultValue ? localize('settings.Default', "{0}", 'default') : '') + }); - const sortKeys = (obj) => { - const keyArray = Object.keys(obj) - .map(key => ({ key, val: obj[key] })) - .sort((a, b) => a.key.localeCompare(b.key)); + template.selectBox.setOptions(displayOptions); - const retVal = {}; - keyArray.forEach(pair => { - retVal[pair.key] = pair.val; - }); - return retVal; - }; + const label = this.setElementAriaLabels(dataElement, SETTINGS_ENUM_TEMPLATE_ID, template); + template.selectBox.setAriaLabel(label); - this._onDidChangeSetting.fire({ - key: template.context.setting.key, - value: Object.keys(newValue).length === 0 ? undefined : sortKeys(newValue), - type: template.context.valueType - }); - } - })); + const idx = dataElement.setting.enum.indexOf(dataElement.value); + template.onChange = null; + template.selectBox.select(idx); + template.onChange = idx => onChange(dataElement.setting.enum[idx]); - return template; + template.enumDescriptionElement.innerHTML = ''; } +} - private renderSettingComplexTemplate(tree: ITree, container: HTMLElement): ISettingComplexItemTemplate { - const common = this.renderCommonTemplate(tree, container, 'complex'); +export class SettingNumberRenderer extends AbstractSettingRenderer implements ITreeRenderer { + templateId = SETTINGS_NUMBER_TEMPLATE_ID; - const openSettingsButton = new Button(common.controlElement, { title: true, buttonBackground: null, buttonHoverBackground: null }); - common.toDispose.push(openSettingsButton); - common.toDispose.push(openSettingsButton.onDidClick(() => template.onChange(null))); - openSettingsButton.label = localize('editInSettingsJson', "Edit in settings.json"); - openSettingsButton.element.classList.add('edit-in-settings-button'); + renderTemplate(container: HTMLElement): ISettingNumberItemTemplate { + const common = super.renderCommonTemplate(null, container, 'number'); + const validationErrorMessageElement = DOM.append(container, $('.setting-item-validation-message')); - common.toDispose.push(attachButtonStyler(openSettingsButton, this.themeService, { - buttonBackground: Color.transparent.toString(), - buttonHoverBackground: Color.transparent.toString(), - buttonForeground: 'foreground' + const inputBox = new InputBox(common.controlElement, this._contextViewService, { type: 'number' }); + common.toDispose.push(inputBox); + common.toDispose.push(attachInputBoxStyler(inputBox, this._themeService, { + inputBackground: settingsNumberInputBackground, + inputForeground: settingsNumberInputForeground, + inputBorder: settingsNumberInputBorder })); + common.toDispose.push( + inputBox.onDidChange(e => { + if (template.onChange) { + template.onChange(e); + } + })); + common.toDispose.push(inputBox); + inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); - const template: ISettingComplexItemTemplate = { + const template: ISettingNumberItemTemplate = { ...common, - button: openSettingsButton + inputBox, + validationErrorMessageElement }; this.addSettingElementFocusHandler(template); @@ -962,198 +884,112 @@ export class SettingsRenderer implements ITreeRenderer { return template; } - private renderNewExtensionsTemplate(container: HTMLElement): ISettingNewExtensionsTemplate { - const toDispose: IDisposable[] = []; - - container.classList.add('setting-item-new-extensions'); - - const button = new Button(container, { title: true, buttonBackground: null, buttonHoverBackground: null }); - toDispose.push(button); - toDispose.push(button.onDidClick(() => { - if (template.context) { - this.commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', template.context.extensionIds); - } - })); - button.label = localize('newExtensionsButtonLabel', "Show matching extensions"); - button.element.classList.add('settings-new-extensions-button'); - toDispose.push(attachButtonStyler(button, this.themeService)); - - const template: ISettingNewExtensionsTemplate = { - button, - toDispose - }; - - // this.addSettingElementFocusHandler(template); - - return template; - } - - renderElement(tree: ITree, element: SettingsTreeElement, templateId: string, template: any): void { - if (templateId === SETTINGS_GROUP_ELEMENT_TEMPLATE_ID) { - return this.renderGroupElement(element, template); - } - - if (templateId === SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID) { - return this.renderNewExtensionsElement(element, template); - } - - return this.renderSettingElement(tree, element, templateId, template); - } - - private renderGroupElement(element: SettingsTreeGroupElement, template: IGroupTitleTemplate): void { - template.parent.innerHTML = ''; - const labelElement = DOM.append(template.parent, $('div.settings-group-title-label')); - labelElement.classList.add(`settings-group-level-${element.level}`); - labelElement.textContent = (element).label; - - if (element.isFirstGroup) { - labelElement.classList.add('settings-group-first'); - } + renderElement(element: ITreeNode, index: number, templateData: ISettingNumberItemTemplate): void { + super.renderSettingElement(element, index, templateData); } - private renderNewExtensionsElement(element: SettingsTreeNewExtensionsElement, template: ISettingNewExtensionsTemplate): void { - template.context = element; - } + protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingNumberItemTemplate, onChange: (value: number) => void): void { + const numParseFn = (dataElement.valueType === 'integer' || dataElement.valueType === 'nullable-integer') + ? parseInt : parseFloat; - public getSettingDOMElementForDOMElement(domElement: HTMLElement): HTMLElement { - const parent = DOM.findParentWithClass(domElement, 'setting-item'); - if (parent) { - return parent; - } + const nullNumParseFn = (dataElement.valueType === 'nullable-integer' || dataElement.valueType === 'nullable-number') + ? (v => v === '' ? null : numParseFn(v)) : numParseFn; - return null; - } + const label = this.setElementAriaLabels(dataElement, SETTINGS_NUMBER_TEMPLATE_ID, template); - public getDOMElementsForSettingKey(treeContainer: HTMLElement, key: string): NodeListOf { - return treeContainer.querySelectorAll(`[${SettingsRenderer.SETTING_KEY_ATTR}="${key}"]`); - } + template.onChange = null; + template.inputBox.value = dataElement.value; + template.onChange = value => { renderValidations(dataElement, template, false, label); onChange(nullNumParseFn(value)); }; - public getKeyForDOMElementInSetting(element: HTMLElement): string { - const settingElement = this.getSettingDOMElementForDOMElement(element); - return settingElement && settingElement.getAttribute(SettingsRenderer.SETTING_KEY_ATTR); + renderValidations(dataElement, template, true, label); } +} - private renderSettingElement(tree: ITree, element: SettingsTreeSettingElement, templateId: string, template: ISettingItemTemplate | ISettingBoolItemTemplate): void { - template.context = element; - template.toolbar.context = element; - - const setting = element.setting; - - DOM.toggleClass(template.containerElement, 'is-configured', element.isConfigured); - DOM.toggleClass(template.containerElement, 'is-expanded', true); - template.containerElement.setAttribute(SettingsRenderer.SETTING_KEY_ATTR, element.setting.key); - - const titleTooltip = setting.key + (element.isConfigured ? ' - Modified' : ''); - template.categoryElement.textContent = element.displayCategory && (element.displayCategory + ': '); - template.categoryElement.title = titleTooltip; - - template.labelElement.textContent = element.displayLabel; - template.labelElement.title = titleTooltip; +export class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRenderer { + templateId = SETTINGS_BOOL_TEMPLATE_ID; - template.descriptionElement.innerHTML = ''; - if (element.setting.descriptionIsMarkdown) { - const renderedDescription = this.renderDescriptionMarkdown(element, element.description, template.toDispose); - template.descriptionElement.appendChild(renderedDescription); - } else { - template.descriptionElement.innerText = element.description; - } + renderTemplate(container: HTMLElement): ISettingBoolItemTemplate { + DOM.addClass(container, 'setting-item'); + DOM.addClass(container, 'setting-item-bool'); - const baseId = (element.displayCategory + '_' + element.displayLabel).replace(/ /g, '_').toLowerCase(); - template.descriptionElement.id = baseId + '_setting_description'; + const titleElement = DOM.append(container, $('.setting-item-title')); + const categoryElement = DOM.append(titleElement, $('span.setting-item-category')); + const labelElement = DOM.append(titleElement, $('span.setting-item-label')); + const otherOverridesElement = DOM.append(titleElement, $('span.setting-item-overrides')); - template.otherOverridesElement.innerHTML = ''; + const descriptionAndValueElement = DOM.append(container, $('.setting-item-value-description')); + const controlElement = DOM.append(descriptionAndValueElement, $('.setting-item-bool-control')); + const descriptionElement = DOM.append(descriptionAndValueElement, $('.setting-item-description')); + const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); + modifiedIndicatorElement.title = localize('modified', "Modified"); - if (element.overriddenScopeList.length) { - const otherOverridesLabel = element.isConfigured ? - localize('alsoConfiguredIn', "Also modified in") : - localize('configuredIn', "Modified in"); - DOM.append(template.otherOverridesElement, $('span', null, `(${otherOverridesLabel}: `)); + const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); - for (let i = 0; i < element.overriddenScopeList.length; i++) { - let view = DOM.append(template.otherOverridesElement, $('a.modified-scope', null, element.overriddenScopeList[i])); + const toDispose: IDisposable[] = []; + const checkbox = new Checkbox({ actionClassName: 'setting-value-checkbox', isChecked: true, title: '', inputActiveOptionBorder: null }); + controlElement.appendChild(checkbox.domNode); + toDispose.push(checkbox); + toDispose.push(checkbox.onChange(() => { + if (template.onChange) { + template.onChange(checkbox.checked); + } + })); - if (i !== element.overriddenScopeList.length - 1) { - DOM.append(template.otherOverridesElement, $('span', null, ', ')); - } else { - DOM.append(template.otherOverridesElement, $('span', null, ')')); - } + // Need to listen for mouse clicks on description and toggle checkbox - use target ID for safety + // Also have to ignore embedded links - too buried to stop propagation + toDispose.push(DOM.addDisposableListener(descriptionElement, DOM.EventType.MOUSE_DOWN, (e) => { + const targetElement = e.target; + const targetId = descriptionElement.getAttribute('checkbox_label_target_id'); - DOM.addStandardDisposableListener(view, DOM.EventType.CLICK, (e: IMouseEvent) => { - this._onDidClickOverrideElement.fire({ - targetKey: element.setting.key, - scope: element.overriddenScopeList[i] - }); - e.preventDefault(); - e.stopPropagation(); - }); + // Make sure we are not a link and the target ID matches + // Toggle target checkbox + if (targetElement.tagName.toLowerCase() !== 'a' && targetId === template.checkbox.domNode.id) { + template.checkbox.checked = template.checkbox.checked ? false : true; + template.onChange(checkbox.checked); } + DOM.EventHelper.stop(e); + })); - } - - this.renderValue(element, templateId, template); - // Remove tree attributes - sometimes overridden by tree - should be managed there - template.containerElement.parentElement.removeAttribute('role'); - template.containerElement.parentElement.removeAttribute('aria-level'); - template.containerElement.parentElement.removeAttribute('aria-posinset'); - template.containerElement.parentElement.removeAttribute('aria-setsize'); - } + checkbox.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + const toolbarContainer = DOM.append(container, $('.setting-toolbar-container')); + const toolbar = this.renderSettingToolbar(toolbarContainer); + toDispose.push(toolbar); - private renderDescriptionMarkdown(element: SettingsTreeSettingElement, text: string, disposeables: IDisposable[]): HTMLElement { - // Rewrite `#editor.fontSize#` to link format - text = fixSettingLinks(text); + const template: ISettingBoolItemTemplate = { + toDispose, - const renderedMarkdown = renderMarkdown({ value: text }, { - actionHandler: { - callback: (content: string) => { - if (startsWith(content, '#')) { - const e: ISettingLinkClickEvent = { - source: element, - targetKey: content.substr(1) - }; - this._onDidClickSettingLink.fire(e); - } else { - let uri: URI; - try { - uri = URI.parse(content); - } catch (err) { - // ignore - } - if (uri) { - this.openerService.open(uri).catch(onUnexpectedError); - } - } - }, - disposeables + containerElement: container, + categoryElement, + labelElement, + controlElement, + checkbox, + descriptionElement, + deprecationWarningElement, + otherOverridesElement, + toolbar + }; + + this.addSettingElementFocusHandler(template); + + // Prevent clicks from being handled by list + toDispose.push(DOM.addDisposableListener(controlElement, 'mousedown', (e: IMouseEvent) => e.stopPropagation())); + + toDispose.push(DOM.addStandardDisposableListener(controlElement, 'keydown', (e: StandardKeyboardEvent) => { + if (e.keyCode === KeyCode.Escape) { + e.browserEvent.stopPropagation(); } - }); + })); - renderedMarkdown.classList.add('setting-item-description-markdown'); - cleanRenderedMarkdown(renderedMarkdown); - return renderedMarkdown; + return template; } - private renderValue(element: SettingsTreeSettingElement, templateId: string, template: ISettingItemTemplate | ISettingBoolItemTemplate): void { - const onChange = value => this._onDidChangeSetting.fire({ key: element.setting.key, value, type: template.context.valueType }); - template.deprecationWarningElement.innerText = element.setting.deprecationMessage || ''; - - if (templateId === SETTINGS_ENUM_TEMPLATE_ID) { - this.renderEnum(element, template, onChange); - } else if (templateId === SETTINGS_TEXT_TEMPLATE_ID) { - this.renderText(element, template, onChange); - } else if (templateId === SETTINGS_NUMBER_TEMPLATE_ID) { - this.renderNumber(element, template, onChange); - } else if (templateId === SETTINGS_BOOL_TEMPLATE_ID) { - this.renderBool(element, template, onChange); - } else if (templateId === SETTINGS_EXCLUDE_TEMPLATE_ID) { - this.renderExcludeSetting(element, template); - } else if (templateId === SETTINGS_COMPLEX_TEMPLATE_ID) { - this.renderComplexSetting(element, template); - } + renderElement(element: ITreeNode, index: number, templateData: ISettingBoolItemTemplate): void { + super.renderSettingElement(element, index, templateData); } - private renderBool(dataElement: SettingsTreeSettingElement, template: ISettingBoolItemTemplate, onChange: (value: boolean) => void): void { + protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingBoolItemTemplate, onChange: (value: boolean) => void): void { template.onChange = null; template.checkbox.checked = dataElement.value; template.onChange = onChange; @@ -1161,132 +997,99 @@ export class SettingsRenderer implements ITreeRenderer { // Setup and add ARIA attributes this.setElementAriaLabels(dataElement, SETTINGS_BOOL_TEMPLATE_ID, template); } +} - private renderEnum(dataElement: SettingsTreeSettingElement, template: ISettingEnumItemTemplate, onChange: (value: string) => void): void { - - const enumDescriptions = dataElement.setting.enumDescriptions; - const enumDescriptionsAreMarkdown = dataElement.setting.enumDescriptionsAreMarkdown; - - let displayOptions = dataElement.setting.enum - .map(String) - .map(escapeInvisibleChars) - .map((data, index) => { - text: data, - description: (enumDescriptions && enumDescriptions[index] && (enumDescriptionsAreMarkdown ? fixSettingLinks(enumDescriptions[index], false) : enumDescriptions[index])), - descriptionIsMarkdown: enumDescriptionsAreMarkdown, - decoratorRight: (data === dataElement.defaultValue ? localize('settings.Default', "{0}", 'default') : '') - }); - - template.selectBox.setOptions(displayOptions); - - const label = this.setElementAriaLabels(dataElement, SETTINGS_ENUM_TEMPLATE_ID, template); - template.selectBox.setAriaLabel(label); - - const idx = dataElement.setting.enum.indexOf(dataElement.value); - template.onChange = null; - template.selectBox.select(idx); - template.onChange = idx => onChange(dataElement.setting.enum[idx]); - - template.enumDescriptionElement.innerHTML = ''; - } +export class SettingTreeRenderers { + public readonly onDidClickOverrideElement: Event; - private renderText(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, onChange: (value: string) => void): void { + private readonly _onDidChangeSetting = new Emitter(); + public readonly onDidChangeSetting: Event; - const label = this.setElementAriaLabels(dataElement, SETTINGS_TEXT_TEMPLATE_ID, template); + public readonly onDidOpenSettings: Event; - template.onChange = null; - template.inputBox.value = dataElement.value; - template.onChange = value => { renderValidations(dataElement, template, false, label); onChange(value); }; + public readonly onDidClickSettingLink: Event; - renderValidations(dataElement, template, true, label); - } + public readonly onDidFocusSetting: Event; - private renderNumber(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, onChange: (value: number) => void): void { - const numParseFn = (dataElement.valueType === 'integer' || dataElement.valueType === 'nullable-integer') - ? parseInt : parseFloat; + public readonly allRenderers: ITreeRenderer[]; - const nullNumParseFn = (dataElement.valueType === 'nullable-integer' || dataElement.valueType === 'nullable-number') - ? (v => v === '' ? null : numParseFn(v)) : numParseFn; + private readonly settingActions: IAction[]; - const label = this.setElementAriaLabels(dataElement, SETTINGS_NUMBER_TEMPLATE_ID, template); + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextMenuService private readonly _contextMenuService: IContextMenuService, + @IContextViewService private readonly _contextViewService: IContextViewService + ) { + this.settingActions = [ + new Action('settings.resetSetting', localize('resetSettingLabel', "Reset Setting"), undefined, undefined, (context: SettingsTreeSettingElement) => { + if (context) { + this._onDidChangeSetting.fire({ key: context.setting.key, value: undefined, type: context.setting.type as SettingValueType }); + } - template.onChange = null; - template.inputBox.value = dataElement.value; - template.onChange = value => { renderValidations(dataElement, template, false, label); onChange(nullNumParseFn(value)); }; + return Promise.resolve(null); + }), + new Separator(), + this._instantiationService.createInstance(CopySettingIdAction), + this._instantiationService.createInstance(CopySettingAsJSONAction), + ]; - renderValidations(dataElement, template, true, label); - } + const settingRenderers = [ + this._instantiationService.createInstance(SettingBoolRenderer, this.settingActions), + this._instantiationService.createInstance(SettingNumberRenderer, this.settingActions), + this._instantiationService.createInstance(SettingBoolRenderer, this.settingActions), + this._instantiationService.createInstance(SettingComplexRenderer, this.settingActions), + this._instantiationService.createInstance(SettingTextRenderer, this.settingActions), + this._instantiationService.createInstance(SettingExcludeRenderer, this.settingActions), + this._instantiationService.createInstance(SettingEnumRenderer, this.settingActions), + ]; - private renderExcludeSetting(dataElement: SettingsTreeSettingElement, template: ISettingExcludeItemTemplate): void { - const value = getExcludeDisplayValue(dataElement); - template.excludeWidget.setValue(value); - template.context = dataElement; + this.onDidClickOverrideElement = Event.any(...settingRenderers.map(r => r.onDidClickOverrideElement)); + this.onDidChangeSetting = Event.any( + ...settingRenderers.map(r => r.onDidChangeSetting), + this._onDidChangeSetting.event + ); + this.onDidOpenSettings = Event.any(...settingRenderers.map(r => r.onDidOpenSettings)); + this.onDidClickSettingLink = Event.any(...settingRenderers.map(r => r.onDidClickSettingLink)); + this.onDidFocusSetting = Event.any(...settingRenderers.map(r => r.onDidFocusSetting)); + + this.allRenderers = [ + ...settingRenderers, + this._instantiationService.createInstance(SettingGroupRenderer), + this._instantiationService.createInstance(SettingNewExtensionsRenderer), + ]; } - private renderComplexSetting(dataElement: SettingsTreeSettingElement, template: ISettingComplexItemTemplate): void { - template.onChange = () => this._onDidOpenSettings.fire(dataElement.setting.key); + public cancelSuggesters() { + this._contextViewService.hideContextView(); } - - private setElementAriaLabels(dataElement: SettingsTreeSettingElement, templateId: string, template: ISettingItemTemplate): string { - // Create base Id for element references - const baseId = (dataElement.displayCategory + '_' + dataElement.displayLabel).replace(/ /g, '_').toLowerCase(); - - const modifiedText = template.otherOverridesElement.textContent ? - template.otherOverridesElement.textContent : (dataElement.isConfigured ? localize('settings.Modified', ' Modified. ') : ''); - - let itemElement = null; - - // Use '.' as reader pause - let label = dataElement.displayCategory + ' ' + dataElement.displayLabel + '. '; - - // Setup and add ARIA attributes - // Create id and label for control/input element - parent is wrapper div - - if (templateId === SETTINGS_TEXT_TEMPLATE_ID) { - if (itemElement = (template).inputBox.inputElement) { - itemElement.setAttribute('role', 'textbox'); - label += modifiedText; - } - } else if (templateId === SETTINGS_NUMBER_TEMPLATE_ID) { - if (itemElement = (template).inputBox.inputElement) { - itemElement.setAttribute('role', 'textbox'); - label += ' number. ' + modifiedText; - } - } else if (templateId === SETTINGS_BOOL_TEMPLATE_ID) { - if (itemElement = (template).checkbox.domNode) { - itemElement.setAttribute('role', 'checkbox'); - label += modifiedText; - // Add checkbox target to description clickable and able to toggle checkbox - template.descriptionElement.setAttribute('checkbox_label_target_id', baseId + '_setting_item'); - } - } else if (templateId === SETTINGS_ENUM_TEMPLATE_ID) { - if (itemElement = template.controlElement.firstElementChild) { - itemElement.setAttribute('role', 'combobox'); - label += modifiedText; - } - } else { - // Don't change attributes if we don't know what we areFunctions - return ''; + showContextMenu(element: SettingsTreeSettingElement, settingDOMElement: HTMLElement): void { + const toolbarElement: HTMLElement = settingDOMElement.querySelector('.toolbar-toggle-more'); + if (toolbarElement) { + this._contextMenuService.showContextMenu({ + getActions: () => this.settingActions, + getAnchor: () => toolbarElement, + getActionsContext: () => element + }); } + } - // We don't have control element, return empty label - if (!itemElement) { - return ''; + getSettingDOMElementForDOMElement(domElement: HTMLElement): HTMLElement { + const parent = DOM.findParentWithClass(domElement, 'setting-item'); + if (parent) { + return parent; } - // Labels will not be read on descendent input elements of the parent treeitem - // unless defined as roles for input items - // voiceover does not seem to use labeledby correctly, set labels directly on input elements - itemElement.id = baseId + '_setting_item'; - itemElement.setAttribute('aria-label', label); - itemElement.setAttribute('aria-describedby', baseId + '_setting_description settings_aria_more_actions_shortcut_label'); + return null; + } - return label; + getDOMElementsForSettingKey(treeContainer: HTMLElement, key: string): NodeListOf { + return treeContainer.querySelectorAll(`[${AbstractSettingRenderer.SETTING_KEY_ATTR}="${key}"]`); } - disposeTemplate(tree: ITree, templateId: string, template: IDisposableTemplate): void { - dispose(template.toDispose); + getKeyForDOMElementInSetting(element: HTMLElement): string { + const settingElement = this.getSettingDOMElementForDOMElement(element); + return settingElement && settingElement.getAttribute(AbstractSettingRenderer.SETTING_KEY_ATTR); } } @@ -1395,22 +1198,62 @@ export class SettingsTreeFilter implements IFilter { } } -export class SettingsTreeController extends WorkbenchTreeController { +export class SettingsTreeFilter2 implements ITreeFilter { constructor( - @IConfigurationService configurationService: IConfigurationService - ) { - super({}, configurationService); - } + private viewState: ISettingsEditorViewState, + ) { } + + filter(element: SettingsTreeElement, parentVisibility: TreeVisibility): TreeFilterResult { + // Filter during search + if (this.viewState.filterToCategory && element instanceof SettingsTreeSettingElement) { + if (!this.settingContainedInGroup(element.setting, this.viewState.filterToCategory)) { + return false; + } + } + + // Non-user scope selected + if (element instanceof SettingsTreeSettingElement && this.viewState.settingsTarget !== ConfigurationTarget.USER) { + if (!element.matchesScope(this.viewState.settingsTarget)) { + return false; + } + } + + // @modified or tag + if (element instanceof SettingsTreeSettingElement && this.viewState.tagFilters) { + if (!element.matchesAllTags(this.viewState.tagFilters)) { + return false; + } + } + + // Group with no visible children + if (element instanceof SettingsTreeGroupElement) { + if (typeof element.count === 'number') { + return element.count > 0; + } - protected onLeftClick(tree: ITree, element: any, eventish: IMouseEvent, origin?: string): boolean { - const isLink = eventish.target.tagName.toLowerCase() === 'a' || - eventish.target.parentElement.tagName.toLowerCase() === 'a'; // inside + return TreeVisibility.Recurse; + } - if (isLink && (DOM.findParentWithClass(eventish.target, 'setting-item-description-markdown', tree.getHTMLElement()) || DOM.findParentWithClass(eventish.target, 'select-box-description-markdown'))) { - return true; + // Filtered "new extensions" button + if (element instanceof SettingsTreeNewExtensionsElement) { + if ((this.viewState.tagFilters && this.viewState.tagFilters.size) || this.viewState.filterToCategory) { + return false; + } } - return false; + return true; + } + + private settingContainedInGroup(setting: ISetting, group: SettingsTreeGroupElement): boolean { + return group.children.some(child => { + if (child instanceof SettingsTreeGroupElement) { + return this.settingContainedInGroup(setting, child); + } else if (child instanceof SettingsTreeSettingElement) { + return child.setting.key === setting.key; + } else { + return false; + } + }); } } @@ -1435,112 +1278,96 @@ export class SettingsAccessibilityProvider implements IAccessibilityProvider { } } -class NonExpandableOrSelectableTree extends Tree { - expand(): Promise { - return Promise.resolve(null); - } - - collapse(): Promise { - return Promise.resolve(null); - } - - public setFocus(element?: any, eventPayload?: any): void { - return; - } +class SettingsTreeDelegate implements IListVirtualDelegate { + getHeight(element: SettingsTreeElement): number { + if (element instanceof SettingsTreeGroupElement) { + if (element.isFirstGroup) { + return 31; + } - public focusNext(count?: number, eventPayload?: any): void { - return; - } + return 40 + (7 * element.level); + } - public focusPrevious(count?: number, eventPayload?: any): void { - return; + return 78; } - public focusParent(eventPayload?: any): void { - return; - } + getTemplateId(element: SettingsTreeGroupElement | SettingsTreeSettingElement | SettingsTreeNewExtensionsElement): string { + if (element instanceof SettingsTreeGroupElement) { + return SETTINGS_ELEMENT_TEMPLATE_ID; + } - public focusFirstChild(eventPayload?: any): void { - return; - } + if (element instanceof SettingsTreeSettingElement) { + if (element.valueType === 'boolean') { + return SETTINGS_BOOL_TEMPLATE_ID; + } - public focusFirst(eventPayload?: any, from?: any): void { - return; - } + if (element.valueType === 'integer' || element.valueType === 'number' || element.valueType === 'nullable-integer' || element.valueType === 'nullable-number') { + return SETTINGS_NUMBER_TEMPLATE_ID; + } - public focusNth(index: number, eventPayload?: any): void { - return; - } + if (element.valueType === 'string') { + return SETTINGS_TEXT_TEMPLATE_ID; + } - public focusLast(eventPayload?: any, from?: any): void { - return; - } + if (element.valueType === 'enum') { + return SETTINGS_ENUM_TEMPLATE_ID; + } - public focusNextPage(eventPayload?: any): void { - return; - } + if (element.valueType === 'exclude') { + return SETTINGS_EXCLUDE_TEMPLATE_ID; + } - public focusPreviousPage(eventPayload?: any): void { - return; - } + return SETTINGS_COMPLEX_TEMPLATE_ID; + } - public select(element: any, eventPayload?: any): void { - return; - } + if (element instanceof SettingsTreeNewExtensionsElement) { + return SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID; + } - public selectRange(fromElement: any, toElement: any, eventPayload?: any): void { - return; + throw new Error('unknown element type: ' + element); } - public selectAll(elements: any[], eventPayload?: any): void { - return; + hasDynamicHeight(element: SettingsTreeGroupElement | SettingsTreeSettingElement | SettingsTreeNewExtensionsElement): boolean { + return !(element instanceof SettingsTreeGroupElement); } +} - public setSelection(elements: any[], eventPayload?: any): void { - return; +class NonCollapsibleObjectTreeModel extends ObjectTreeModel { + isCollapsible(element: T): boolean { + return false; } - public toggleSelection(element: any, eventPayload?: any): void { - return; + setCollapsed(element: T, collapsed?: boolean, recursive?: boolean): boolean { + return false; } } -export class SettingsTree extends NonExpandableOrSelectableTree { +export class SettingsTree extends ObjectTree { protected disposables: IDisposable[]; constructor( container: HTMLElement, viewState: ISettingsEditorViewState, - configuration: Partial, - @IThemeService themeService: IThemeService, - @IInstantiationService instantiationService: IInstantiationService + renderers: ITreeRenderer[], + @IThemeService themeService: IThemeService ) { const treeClass = 'settings-editor-tree'; - const controller = instantiationService.createInstance(SettingsTreeController); - const fullConfiguration = { - controller, - accessibilityProvider: instantiationService.createInstance(SettingsAccessibilityProvider), - filter: instantiationService.createInstance(SettingsTreeFilter, viewState), - styler: new DefaultTreestyler(DOM.createStyleSheet(container), treeClass), - - ...configuration - }; - - const options = { - ariaLabel: localize('treeAriaLabel', "Settings"), - showLoading: false, - indentPixels: 0, - twistiePixels: 20, // Actually for gear button - }; - super(container, - fullConfiguration, - options); + new SettingsTreeDelegate(), + renderers, + { + supportDynamicHeights: true, + ariaLabel: localize('treeAriaLabel', "Settings"), + identityProvider: { + getId(e) { + return e.id; + } + }, + filter: new SettingsTreeFilter2(viewState) + }); this.disposables = []; - this.disposables.push(controller); - this.disposables.push(registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const activeBorderColor = theme.getColor(focusBorder); if (activeBorderColor) { @@ -1609,6 +1436,10 @@ export class SettingsTree extends NonExpandableOrSelectableTree { })); } + protected createModel(view: ISpliceable>, options: IObjectTreeOptions): ITreeModel { + return new NonCollapsibleObjectTreeModel(view, options); + } + public dispose(): void { this.disposables = dispose(this.disposables); } diff --git a/src/vs/workbench/parts/preferences/browser/settingsWidgets.ts b/src/vs/workbench/parts/preferences/browser/settingsWidgets.ts index 819e6582f40..53cb44e642e 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsWidgets.ts @@ -191,7 +191,7 @@ export class ExcludeSettingListModel { } } -interface IExcludeChangeEvent { +export interface IExcludeChangeEvent { originalPattern: string; pattern?: string; sibling?: string; diff --git a/src/vs/workbench/parts/preferences/electron-browser/media/settingsEditor2.css b/src/vs/workbench/parts/preferences/electron-browser/media/settingsEditor2.css index 45fec74561c..5e165b5b319 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/media/settingsEditor2.css +++ b/src/vs/workbench/parts/preferences/electron-browser/media/settingsEditor2.css @@ -113,30 +113,35 @@ text-decoration: underline; } -.settings-editor.no-toc-search > .settings-body .settings-tree-container .monaco-tree-wrapper, -.settings-editor.narrow-width > .settings-body .settings-tree-container .monaco-tree-wrapper { +.settings-editor.no-toc-search > .settings-body .settings-tree-container .monaco-list-rows, +.settings-editor.narrow-width > .settings-body .settings-tree-container .monaco-list-rows { margin-left: 0px; } -.settings-editor > .settings-body .settings-tree-container .monaco-tree-wrapper { +.settings-editor > .settings-body .settings-tree-container .monaco-list-rows { max-width: 1000px; margin: auto; } -.settings-editor > .settings-body .settings-tree-container .monaco-tree-wrapper .monaco-tree-rows { +.settings-editor > .settings-body .settings-tree-container .monaco-list-row { + line-height: 1.4em !important; /* TODO */ padding-left: 208px; padding-right: 24px; - box-sizing: border-box; + /* box-sizing: border-box; */ +} + +.settings-editor > .settings-body .settings-tree-container .monaco-list-row .monaco-tl-row { + position: relative; } -.settings-editor.no-toc-search > .settings-body .settings-tree-container .monaco-tree-wrapper .monaco-tree-rows, -.settings-editor.narrow-width > .settings-body .settings-tree-container .monaco-tree-wrapper .monaco-tree-rows { +.settings-editor.no-toc-search > .settings-body .settings-tree-container .monaco-list-row, +.settings-editor.narrow-width > .settings-body .settings-tree-container .monaco-list-row { /* 3 margin + 20 padding + 2 border */ width: calc(100% - 25px); padding-left: 25px; } -.settings-editor > .settings-body .settings-tree-container .monaco-tree-row > .content::before { +.settings-editor > .settings-body .settings-tree-container .monaco-list-row .monaco-tl-twistie { /* Hide twisties */ display: none !important; } @@ -162,10 +167,10 @@ width: 26px; } -.settings-editor > .settings-body .settings-tree-container .monaco-tree-row .mouseover .setting-toolbar-container > .monaco-toolbar .toolbar-toggle-more, -.settings-editor > .settings-body .settings-tree-container .monaco-tree-row .setting-item.focused .setting-toolbar-container > .monaco-toolbar .toolbar-toggle-more, -.settings-editor > .settings-body .settings-tree-container .monaco-tree-row .setting-toolbar-container:hover > .monaco-toolbar .toolbar-toggle-more, -.settings-editor > .settings-body .settings-tree-container .monaco-tree-row .setting-toolbar-container > .monaco-toolbar .active .toolbar-toggle-more { +.settings-editor > .settings-body .settings-tree-container .monaco-list-row .mouseover .setting-toolbar-container > .monaco-toolbar .toolbar-toggle-more, +.settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-item.focused .setting-toolbar-container > .monaco-toolbar .toolbar-toggle-more, +.settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-toolbar-container:hover > .monaco-toolbar .toolbar-toggle-more, +.settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-toolbar-container > .monaco-toolbar .active .toolbar-toggle-more { opacity: 1; } @@ -261,7 +266,7 @@ padding-left: 31px; } -.settings-editor > .settings-body .settings-tree-container .monaco-tree-wrapper, +.settings-editor > .settings-body .settings-tree-container .monaco-list-rows, .settings-editor > .settings-body .settings-toc-wrapper { height: 100%; max-width: 1000px; @@ -273,7 +278,7 @@ margin-left: 0px; } -.settings-editor > .settings-body > .settings-tree-container .monaco-tree-row { +.settings-editor > .settings-body > .settings-tree-container .monaco-list-row { overflow: visible; /* so validation messages dont get clipped */ cursor: default; @@ -408,7 +413,7 @@ visibility: hidden; } -.settings-editor > .settings-body .settings-tree-container .setting-measure-container .monaco-tree-row { +.settings-editor > .settings-body .settings-tree-container .setting-measure-container .monaco-list-row { padding-left: 20px; } @@ -426,6 +431,10 @@ display: block; } +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-bool { + padding-bottom: 26px; +} + .settings-editor > .settings-body > .settings-tree-container .setting-item-bool .setting-item-value-description { display: flex; cursor: pointer; diff --git a/src/vs/workbench/parts/preferences/electron-browser/settingsEditor2.ts b/src/vs/workbench/parts/preferences/electron-browser/settingsEditor2.ts index d37a3d0b875..5ce685aa3d9 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/settingsEditor2.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/settingsEditor2.ts @@ -4,14 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import { ITreeElement } from 'vs/base/browser/ui/tree/tree'; import * as arrays from 'vs/base/common/arrays'; import { Delayer, ThrottledDelayer } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import * as collections from 'vs/base/common/collections'; import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors'; +import { Iterator } from 'vs/base/common/iterator'; import { isArray } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { collapseAll, expandAll } from 'vs/base/parts/tree/browser/treeUtils'; import 'vs/css!./media/settingsEditor2'; import { localize } from 'vs/nls'; @@ -32,8 +33,8 @@ import { IEditor, IEditorMemento } from 'vs/workbench/common/editor'; import { attachSuggestEnabledInputBoxStyler, SuggestEnabledInput } from 'vs/workbench/parts/codeEditor/electron-browser/suggestEnabledInput'; import { SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; import { commonlyUsedData, tocData } from 'vs/workbench/parts/preferences/browser/settingsLayout'; -import { ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveExtensionsSettings, resolveSettingsTree, SettingsDataSource, SettingsRenderer, SettingsTree, SimplePagedDataSource } from 'vs/workbench/parts/preferences/browser/settingsTree'; -import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/parts/preferences/browser/settingsTreeModels'; +import { AbstractSettingRenderer, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveExtensionsSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from 'vs/workbench/parts/preferences/browser/settingsTree'; +import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel } from 'vs/workbench/parts/preferences/browser/settingsTreeModels'; import { settingsTextInputBorder } from 'vs/workbench/parts/preferences/browser/settingsWidgets'; import { TOCRenderer, TOCTree, TOCTreeModel } from 'vs/workbench/parts/preferences/browser/tocTree'; import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, IPreferencesSearchService, ISearchProvider, MODIFIED_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/parts/preferences/common/preferences'; @@ -42,6 +43,19 @@ import { IPreferencesService, ISearchResult, ISettingsEditorModel, ISettingsEdit import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { Settings2EditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; +function createGroupIterator(group: SettingsTreeGroupElement): Iterator> { + const groupsIt = Iterator.fromArray(group.children); + + return Iterator.map(groupsIt, g => { + return { + element: g, + children: g instanceof SettingsTreeGroupElement ? + createGroupIterator(g) : + undefined + }; + }); +} + const $ = DOM.$; const SETTINGS_EDITOR_STATE_KEY = 'settingsEditorState'; @@ -76,9 +90,8 @@ export class SettingsEditor2 extends BaseEditor { private settingsTargetsWidget: SettingsTargetsWidget; private settingsTreeContainer: HTMLElement; - private settingsTree: Tree; - private settingsTreeRenderer: SettingsRenderer; - private settingsTreeDataSource: SimplePagedDataSource; + private settingsTree: SettingsTree; + private settingRenderers: SettingTreeRenderers; private tocTreeModel: TOCTreeModel; private settingsTreeModel: SettingsTreeModel; private noResultsMessage: HTMLElement; @@ -259,8 +272,8 @@ export class SettingsEditor2 extends BaseEditor { } layout(dimension: DOM.Dimension): void { - const firstEl = this.settingsTree.getFirstVisibleElement(); - const firstElTop = this.settingsTree.getRelativeTop(firstEl); + // const firstEl = this.settingsTree.getFirstVisibleElement(); + // const firstElTop = this.settingsTree.getRelativeTop(firstEl); this.layoutTrees(dimension); @@ -276,7 +289,7 @@ export class SettingsEditor2 extends BaseEditor { this.lastLayedoutWidth = dimension.width; this.delayRefreshOnLayout.trigger(() => { this.renderTree(undefined, true).then(() => { - this.settingsTree.reveal(firstEl, firstElTop); + // this.settingsTree.reveal(firstEl, firstElTop); }); }); } @@ -284,9 +297,9 @@ export class SettingsEditor2 extends BaseEditor { focus(): void { if (this.lastFocusedSettingElement) { - const elements = this.settingsTreeRenderer.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), this.lastFocusedSettingElement); + const elements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), this.lastFocusedSettingElement); if (elements.length) { - const control = elements[0].querySelector(SettingsRenderer.CONTROL_SELECTOR); + const control = elements[0].querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); if (control) { (control).focus(); return; @@ -307,26 +320,26 @@ export class SettingsEditor2 extends BaseEditor { } } - const firstFocusable = this.settingsTree.getHTMLElement().querySelector(SettingsRenderer.CONTROL_SELECTOR); + const firstFocusable = this.settingsTree.getHTMLElement().querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); if (firstFocusable) { (firstFocusable).focus(); } } showContextMenu(): void { - const settingDOMElement = this.settingsTreeRenderer.getSettingDOMElementForDOMElement(this.getActiveElementInSettingsTree()); + const settingDOMElement = this.settingRenderers.getSettingDOMElementForDOMElement(this.getActiveElementInSettingsTree()); if (!settingDOMElement) { return; } - const focusedKey = this.settingsTreeRenderer.getKeyForDOMElementInSetting(settingDOMElement); + const focusedKey = this.settingRenderers.getKeyForDOMElementInSetting(settingDOMElement); if (!focusedKey) { return; } const elements = this.currentSettingsModel.getElementsByName(focusedKey); if (elements && elements[0]) { - this.settingsTreeRenderer.showContextMenu(elements[0], settingDOMElement); + this.settingRenderers.showContextMenu(elements[0], settingDOMElement); } } @@ -414,9 +427,9 @@ export class SettingsEditor2 extends BaseEditor { this.settingsTree.reveal(elements[0], sourceTop); - const domElements = this.settingsTreeRenderer.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), evt.targetKey); + const domElements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), evt.targetKey); if (domElements && domElements[0]) { - const control = domElements[0].querySelector(SettingsRenderer.CONTROL_SELECTOR); + const control = domElements[0].querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); if (control) { (control).focus(); } @@ -483,57 +496,57 @@ export class SettingsEditor2 extends BaseEditor { this.createTOC(bodyContainer); - this.createFocusSink( - bodyContainer, - e => { - if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { - if (this.settingsTree.getScrollPosition() > 0) { - const firstElement = this.settingsTree.getFirstVisibleElement(); - this.settingsTree.reveal(firstElement, 0.1); - return true; - } - } else { - const firstControl = this.settingsTree.getHTMLElement().querySelector(SettingsRenderer.CONTROL_SELECTOR); - if (firstControl) { - (firstControl).focus(); - } - } - - return false; - }, - 'settings list focus helper'); + // this.createFocusSink( + // bodyContainer, + // e => { + // if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { + // if (this.settingsTree.getScrollPosition() > 0) { + // const firstElement = this.settingsTree.getFirstVisibleElement(); + // this.settingsTree.reveal(firstElement, 0.1); + // return true; + // } + // } else { + // const firstControl = this.settingsTree.getHTMLElement().querySelector(SettingsRenderer.CONTROL_SELECTOR); + // if (firstControl) { + // (firstControl).focus(); + // } + // } + + // return false; + // }, + // 'settings list focus helper'); this.createSettingsTree(bodyContainer); - this.createFocusSink( - bodyContainer, - e => { - if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { - if (this.settingsTree.getScrollPosition() < 1) { - const lastElement = this.settingsTree.getLastVisibleElement(); - this.settingsTree.reveal(lastElement, 0.9); - return true; - } - } - - return false; - }, - 'settings list focus helper' - ); + // this.createFocusSink( + // bodyContainer, + // e => { + // if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { + // if (this.settingsTree.getScrollPosition() < 1) { + // const lastElement = this.settingsTree.getLastVisibleElement(); + // this.settingsTree.reveal(lastElement, 0.9); + // return true; + // } + // } + + // return false; + // }, + // 'settings list focus helper' + // ); } - private createFocusSink(container: HTMLElement, callback: (e: any) => boolean, label: string): HTMLElement { - const listFocusSink = DOM.append(container, $('.settings-tree-focus-sink')); - listFocusSink.setAttribute('aria-label', label); - listFocusSink.tabIndex = 0; - this._register(DOM.addDisposableListener(listFocusSink, 'focus', (e: any) => { - if (e.relatedTarget && callback(e)) { - e.relatedTarget.focus(); - } - })); + // private createFocusSink(container: HTMLElement, callback: (e: any) => boolean, label: string): HTMLElement { + // const listFocusSink = DOM.append(container, $('.settings-tree-focus-sink')); + // listFocusSink.setAttribute('aria-label', label); + // listFocusSink.tabIndex = 0; + // this._register(DOM.addDisposableListener(listFocusSink, 'focus', (e: any) => { + // if (e.relatedTarget && callback(e)) { + // e.relatedTarget.focus(); + // } + // })); - return listFocusSink; - } + // return listFocusSink; + // } private createTOC(parent: HTMLElement): void { this.tocTreeModel = new TOCTreeModel(this.viewState); @@ -553,15 +566,9 @@ export class SettingsEditor2 extends BaseEditor { if (this.searchResultModel) { this.viewState.filterToCategory = element; this.renderTree(); - } - - if (element && (!e.payload || !e.payload.fromScroll)) { - let refreshP: Promise = Promise.resolve(null); - if (this.settingsTreeDataSource.pageTo(element.index, true)) { - refreshP = this.renderTree(); - } - - refreshP.then(() => this.settingsTree.reveal(element, 0)); + this.settingsTree.scrollTop = 0; + } else if (element && (!e.payload || !e.payload.fromScroll)) { + this.settingsTree.reveal(element, 0); } })); @@ -585,17 +592,17 @@ export class SettingsEditor2 extends BaseEditor { labelDiv.id = 'settings_aria_more_actions_shortcut_label'; labelDiv.setAttribute('aria-label', ''); - this.settingsTreeRenderer = this.instantiationService.createInstance(SettingsRenderer, this.settingsTreeContainer); - this._register(this.settingsTreeRenderer.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type))); - this._register(this.settingsTreeRenderer.onDidOpenSettings(settingKey => { + this.settingRenderers = this.instantiationService.createInstance(SettingTreeRenderers); + this._register(this.settingRenderers.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type))); + this._register(this.settingRenderers.onDidOpenSettings(settingKey => { this.openSettingsFile(settingKey); })); - this._register(this.settingsTreeRenderer.onDidClickSettingLink(settingName => this.onDidClickSetting(settingName))); - this._register(this.settingsTreeRenderer.onDidFocusSetting(element => { + this._register(this.settingRenderers.onDidClickSettingLink(settingName => this.onDidClickSetting(settingName))); + this._register(this.settingRenderers.onDidFocusSetting(element => { this.lastFocusedSettingElement = element.setting.key; this.settingsTree.reveal(element); })); - this._register(this.settingsTreeRenderer.onDidClickOverrideElement((element: ISettingOverrideClickEvent) => { + this._register(this.settingRenderers.onDidClickOverrideElement((element: ISettingOverrideClickEvent) => { if (ConfigurationTargetToString(ConfigurationTarget.WORKSPACE) === element.scope.toUpperCase()) { this.settingsTargetsWidget.updateTarget(ConfigurationTarget.WORKSPACE); } else if (ConfigurationTargetToString(ConfigurationTarget.USER) === element.scope.toUpperCase()) { @@ -605,25 +612,19 @@ export class SettingsEditor2 extends BaseEditor { this.searchWidget.setValue(element.targetKey); })); - this.settingsTreeDataSource = this.instantiationService.createInstance(SimplePagedDataSource, - this.instantiationService.createInstance(SettingsDataSource, this.viewState)); - this.settingsTree = this._register(this.instantiationService.createInstance(SettingsTree, this.settingsTreeContainer, this.viewState, - { - renderer: this.settingsTreeRenderer, - dataSource: this.settingsTreeDataSource - })); + this.settingRenderers.allRenderers)); this.settingsTree.getHTMLElement().attributes.removeNamedItem('tabindex'); // Have to redefine role of the tree widget to form for input elements // TODO:CDL make this an option for tree this.settingsTree.getHTMLElement().setAttribute('role', 'form'); - this._register(this.settingsTree.onDidScroll(() => { - this.updateTreeScrollSync(); - })); + // this._register(this.settingsTree.onDidScroll(() => { + // this.updateTreeScrollSync(); + // })); } private notifyNoSaveNeeded() { @@ -649,7 +650,7 @@ export class SettingsEditor2 extends BaseEditor { } private updateTreeScrollSync(): void { - this.settingsTreeRenderer.cancelSuggesters(); + this.settingRenderers.cancelSuggesters(); if (this.searchResultModel) { return; } @@ -657,36 +658,38 @@ export class SettingsEditor2 extends BaseEditor { if (!this.tocTree.getInput()) { return; } - this.updateTreePagingByScroll(); - const elementToSync = this.settingsTree.getFirstVisibleElement(); - const element = elementToSync instanceof SettingsTreeSettingElement ? elementToSync.parent : - elementToSync instanceof SettingsTreeGroupElement ? elementToSync : - null; + // this.updateTreePagingByScroll(); + + const element = this.tocTreeModel.children[0]; + // const elementToSync = this.settingsTree.getFirstVisibleElement(); + // const element = elementToSync instanceof SettingsTreeSettingElement ? elementToSync.parent : + // elementToSync instanceof SettingsTreeGroupElement ? elementToSync : + // null; if (element && this.tocTree.getSelection()[0] !== element) { - this.tocTree.reveal(element); - const elementTop = this.tocTree.getRelativeTop(element); - collapseAll(this.tocTree, element); - if (elementTop < 0 || elementTop > 1) { - this.tocTree.reveal(element); - } else { - this.tocTree.reveal(element, elementTop); - } + // this.tocTree.reveal(element); + // const elementTop = this.tocTree.getRelativeTop(element); + // collapseAll(this.tocTree, element); + // if (elementTop < 0 || elementTop > 1) { + // this.tocTree.reveal(element); + // } else { + // this.tocTree.reveal(element, elementTop); + // } - this.tocTree.expand(element); + // this.tocTree.expand(element); this.tocTree.setSelection([element]); this.tocTree.setFocus(element, { fromScroll: true }); } } - private updateTreePagingByScroll(): void { - const lastVisibleElement = this.settingsTree.getLastVisibleElement(); - if (lastVisibleElement && this.settingsTreeDataSource.pageTo(lastVisibleElement.index)) { - this.renderTree(); - } - } + // private updateTreePagingByScroll(): void { + // const lastVisibleElement = this.settingsTree.getLastVisibleElement(); + // if (lastVisibleElement && this.settingsTreeDataSource.pageTo(lastVisibleElement.index)) { + // this.renderTree(); + // } + // } private updateChangedSetting(key: string, value: any): Promise { // ConfigurationService displays the error if this fails. @@ -800,12 +803,6 @@ export class SettingsEditor2 extends BaseEditor { if (this.configurationService.getValue('workbench.settings.settingsSearchTocBehavior') === 'hide') { DOM.toggleClass(this.rootElement, 'no-toc-search', !!this.searchResultModel); } - - if (this.searchResultModel) { - this.settingsTreeDataSource.pageTo(Number.MAX_VALUE); - } else { - this.settingsTreeDataSource.reset(); - } } private scheduleRefresh(element: HTMLElement, key = ''): void { @@ -870,7 +867,7 @@ export class SettingsEditor2 extends BaseEditor { } else { this.settingsTreeModel = this.instantiationService.createInstance(SettingsTreeModel, this.viewState); this.settingsTreeModel.update(resolvedSettingsRoot); - this.settingsTree.setInput(this.settingsTreeModel.root); + this.refreshTree(); this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.root as SettingsTreeGroupElement; if (this.tocTree.getInput()) { @@ -914,11 +911,11 @@ export class SettingsEditor2 extends BaseEditor { } // If a setting control is currently focused, schedule a refresh for later - const focusedSetting = this.settingsTreeRenderer.getSettingDOMElementForDOMElement(this.getActiveElementInSettingsTree()); + const focusedSetting = this.settingRenderers.getSettingDOMElementForDOMElement(this.getActiveElementInSettingsTree()); if (focusedSetting && !force) { // If a single setting is being refreshed, it's ok to refresh now if that is not the focused setting if (key) { - const focusedKey = focusedSetting.getAttribute(SettingsRenderer.SETTING_KEY_ATTR); + const focusedKey = focusedSetting.getAttribute(AbstractSettingRenderer.SETTING_KEY_ATTR); if (focusedKey === key && !DOM.hasClass(focusedSetting, 'setting-item-exclude')) { // update `exclude`s live, as they have a separate "submit edit" step built in before this @@ -932,31 +929,31 @@ export class SettingsEditor2 extends BaseEditor { } } - let refreshP: Promise; if (key) { const elements = this.currentSettingsModel.getElementsByName(key); if (elements && elements.length) { // TODO https://github.com/Microsoft/vscode/issues/57360 - // refreshP = Promise.all(elements.map(e => this.settingsTree.refresh(e))); - refreshP = this.settingsTree.refresh(); + this.refreshTree(); } else { // Refresh requested for a key that we don't know about return Promise.resolve(null); } } else { - refreshP = this.settingsTree.refresh(); + this.refreshTree(); } - return refreshP.then(() => { - this.tocTreeModel.update(); - return this.tocTree.refresh(); - }).then(() => { }); + this.tocTreeModel.update(); + return this.tocTree.refresh(); + } + + private refreshTree(): void { + this.settingsTree.setChildren(null, createGroupIterator(this.currentSettingsModel.root)); } private updateModifiedLabelForKey(key: string): void { const dataElements = this.currentSettingsModel.getElementsByName(key); const isModified = dataElements && dataElements[0] && dataElements[0].isConfigured; // all elements are either configured or not - const elements = this.settingsTreeRenderer.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), key); + const elements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), key); if (elements && elements[0]) { DOM.toggleClass(elements[0], 'is-configured', isModified); } @@ -1012,15 +1009,17 @@ export class SettingsEditor2 extends BaseEditor { // Added a filter model this.tocTree.setSelection([]); expandAll(this.tocTree); - return this.settingsTree.setInput(this.searchResultModel.root).then(() => { - this.renderResultCountMessages(); - }); + this.refreshTree(); + this.renderResultCountMessages(); } else { // Leaving search mode collapseAll(this.tocTree); - return this.settingsTree.setInput(this.settingsTreeModel.root).then(() => this.renderResultCountMessages()); + this.refreshTree(); + this.renderResultCountMessages(); } } + + return Promise.resolve(null); } /** @@ -1138,7 +1137,7 @@ export class SettingsEditor2 extends BaseEditor { this.searchResultModel.setResult(type, result); this.tocTreeModel.currentSearchModel = this.searchResultModel; this.onSearchModeToggled(); - this.settingsTree.setInput(this.searchResultModel.root); + this.refreshTree(); } else { this.searchResultModel.setResult(type, result); this.tocTreeModel.update(); @@ -1153,7 +1152,7 @@ export class SettingsEditor2 extends BaseEditor { } private renderResultCountMessages() { - if (!this.settingsTree.getInput()) { + if (!this.currentSettingsModel) { return; } @@ -1201,13 +1200,12 @@ export class SettingsEditor2 extends BaseEditor { const listHeight = dimension.height - (76 + 11 /* header height + padding*/); const settingsTreeHeight = listHeight - 14; this.settingsTreeContainer.style.height = `${settingsTreeHeight}px`; - this.settingsTree.layout(settingsTreeHeight, 800); + this.settingsTree.layout(settingsTreeHeight); + this.settingsTree.layoutWidth(dimension.width); const tocTreeHeight = listHeight - 16; this.tocTreeContainer.style.height = `${tocTreeHeight}px`; this.tocTree.layout(tocTreeHeight, 175); - - this.settingsTreeRenderer.updateWidth(dimension.width); } protected saveState(): void { -- GitLab