diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 56ea99c13f0..67f91e2f92d 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -377,6 +377,9 @@ class TypeFilterController implements IDisposable { private filterOnTypeDomNode: HTMLInputElement; private clearDomNode: HTMLElement; + private automaticKeyboardNavigation: boolean; + private triggered = false; + private enabledDisposables: IDisposable[] = []; private disposables: IDisposable[] = []; @@ -408,12 +411,10 @@ class TypeFilterController implements IDisposable { this.clearDomNode.title = localize('clear', "Clear"); model.onDidSplice(this.onDidSpliceModel, this, this.disposables); - - tree.onDidUpdateOptions(this.onDidUpdateTreeOptions, this, this.disposables); - this.onDidUpdateTreeOptions(tree.options); + this.updateOptions(tree.options); } - private onDidUpdateTreeOptions(options: IAbstractTreeOptions): void { + updateOptions(options: IAbstractTreeOptions): void { if (options.simpleKeyboardNavigation) { this.disable(); } else { @@ -421,10 +422,19 @@ class TypeFilterController implements IDisposable { } this.filterOnTypeDomNode.checked = !!options.filterOnType; + this.automaticKeyboardNavigation = typeof options.automaticKeyboardNavigation === 'undefined' ? true : options.automaticKeyboardNavigation; this.tree.refilter(); this.render(); } + toggle(): void { + this.triggered = !this.triggered; + + if (!this.triggered) { + this.onEventOrInput(''); + } + } + private enable(): void { if (this._enabled) { return; @@ -433,31 +443,22 @@ class TypeFilterController implements IDisposable { const isPrintableCharEvent = this.keyboardNavigationLabelProvider.mightProducePrintableCharacter ? (e: IKeyboardEvent) => this.keyboardNavigationLabelProvider.mightProducePrintableCharacter!(e) : (e: IKeyboardEvent) => mightProducePrintableCharacter(e); const onKeyDown = Event.chain(domEvent(this.view.getHTMLElement(), 'keydown')) .filter(e => !isInputElement(e.target as HTMLElement) || e.target === this.filterOnTypeDomNode) + .filter(() => this.automaticKeyboardNavigation || this.triggered) .map(e => new StandardKeyboardEvent(e)) - .filter(e => isPrintableCharEvent(e) || (this._pattern.length > 0 && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? e.altKey : e.ctrlKey)))) + .filter(e => isPrintableCharEvent(e) || ((this._pattern.length > 0 || this.triggered) && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? e.altKey : e.ctrlKey)))) .forEach(e => { e.stopPropagation(); e.preventDefault(); }) .event; const onClear = domEvent(this.clearDomNode, 'click'); - const onInput = Event.chain(Event.any(onKeyDown, onClear)) - .reduce((previous: string, e) => { - if (e instanceof MouseEvent || e.keyCode === KeyCode.Escape || (e.keyCode === KeyCode.Backspace && (isMacintosh ? e.altKey : e.ctrlKey))) { - return ''; - } - if (e.keyCode === KeyCode.Backspace) { - return previous.length === 0 ? '' : previous.substr(0, previous.length - 1); - } + Event.chain(Event.any(onKeyDown, onClear)) + .event(this.onEventOrInput, this, this.enabledDisposables); - return previous + e.browserEvent.key; - }, '') - .event; - - onInput(this.onInput, this, this.enabledDisposables); this.filter.pattern = ''; this.tree.refilter(); this.render(); this._enabled = true; + this.triggered = false; } private disable(): void { @@ -470,6 +471,19 @@ class TypeFilterController implements IDisposable { this.tree.refilter(); this.render(); this._enabled = false; + this.triggered = false; + } + + private onEventOrInput(e: MouseEvent | StandardKeyboardEvent | string): void { + if (typeof e === 'string') { + this.onInput(e); + } else if (e instanceof MouseEvent || e.keyCode === KeyCode.Escape || (e.keyCode === KeyCode.Backspace && (isMacintosh ? e.altKey : e.ctrlKey))) { + this.onInput(''); + } else if (e.keyCode === KeyCode.Backspace) { + this.onInput(this.pattern.length === 0 ? '' : this.pattern.substr(0, this.pattern.length - 1)); + } else { + this.onInput(this.pattern + e.browserEvent.key); + } } private onInput(pattern: string): void { @@ -498,6 +512,10 @@ class TypeFilterController implements IDisposable { } this.render(); + + if (!pattern) { + this.triggered = false; + } } private onDragStart(): void { @@ -631,6 +649,7 @@ function asTreeContextMenuEvent(event: IListContextMenuEvent implements IDisposable private focus = new Trait(); private selection = new Trait(); private eventBufferer = new EventBufferer(); + private typeFilterController?: TypeFilterController; protected disposables: IDisposable[] = []; - private _onDidUpdateOptions = new Emitter>(); - readonly onDidUpdateOptions = this._onDidUpdateOptions.event; - get onDidScroll(): Event { return this.view.onDidScroll; } readonly onDidChangeFocus: Event> = this.eventBufferer.wrapEvent(this.focus.onDidChange); @@ -852,9 +869,9 @@ export abstract class AbstractTree implements IDisposable } if (_options.keyboardNavigationLabelProvider) { - const typeFilterController = new TypeFilterController(this, this.model, this.view, filter!, _options.keyboardNavigationLabelProvider); + this.typeFilterController = new TypeFilterController(this, this.model, this.view, filter!, _options.keyboardNavigationLabelProvider); this.focusNavigationFilter = node => { - if (!typeFilterController.enabled || !typeFilterController.pattern) { + if (!this.typeFilterController!.enabled || !this.typeFilterController!.pattern) { return true; } @@ -864,7 +881,7 @@ export abstract class AbstractTree implements IDisposable return !FuzzyScore.isDefault(node.filterData as any as FuzzyScore); }; - this.disposables.push(typeFilterController); + this.disposables.push(this.typeFilterController!); } } @@ -876,7 +893,10 @@ export abstract class AbstractTree implements IDisposable } this.view.updateOptions({ enableKeyboardNavigation: this._options.simpleKeyboardNavigation }); - this._onDidUpdateOptions.fire(this._options); + + if (this.typeFilterController) { + this.typeFilterController.updateOptions(this._options); + } } get options(): IAbstractTreeOptions { @@ -983,6 +1003,14 @@ export abstract class AbstractTree implements IDisposable return this.model.isCollapsed(location); } + toggleKeyboardNavigation(): void { + if (!this.typeFilterController) { + return; + } + + this.typeFilterController.toggle(); + } + refilter(): void { this._onWillRefilter.fire(undefined); this.model.refilter(); diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index a4f7128419d..3b50d3ff5f8 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -478,6 +478,10 @@ export class AsyncDataTree implements IDisposable return this.tree.isCollapsed(this.getDataNode(element)); } + toggleKeyboardNavigation(): void { + this.tree.toggleKeyboardNavigation(); + } + refilter(): void { this.tree.refilter(); } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 776395ccbcf..529f63834ed 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -103,6 +103,8 @@ export const WorkbenchListFocusContextKey = ContextKeyExpr.and(RawWorkbenchListF export const WorkbenchListHasSelectionOrFocus = new RawContextKey('listHasSelectionOrFocus', false); export const WorkbenchListDoubleSelection = new RawContextKey('listDoubleSelection', false); export const WorkbenchListMultiSelection = new RawContextKey('listMultiSelection', false); +export const WorkbenchListAutomaticKeyboardNavigationKey = 'listAutomaticKeyboardNavigation'; +export const WorkbenchListAutomaticKeyboardNavigation = new RawContextKey(WorkbenchListAutomaticKeyboardNavigationKey, true); function createScopedContextKeyService(contextKeyService: IContextKeyService, widget: ListWidget): IContextKeyService { const result = contextKeyService.createScoped(widget.getHTMLElement()); @@ -920,6 +922,9 @@ export class WorkbenchObjectTree, TFilterData = void> @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService ) { + WorkbenchListAutomaticKeyboardNavigation.bindTo(contextKeyService); + + const automaticKeyboardNavigation = contextKeyService.getContextKeyValue(WorkbenchListAutomaticKeyboardNavigationKey); const keyboardNavigation = configurationService.getValue(keyboardNavigationSettingKey); const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); @@ -929,6 +934,7 @@ export class WorkbenchObjectTree, TFilterData = void> ...computeStyles(themeService.getTheme(), defaultListStyles), ...toWorkbenchListOptions(options, configurationService, keybindingService), indent: configurationService.getValue(treeIndentKey), + automaticKeyboardNavigation, simpleKeyboardNavigation: keyboardNavigation === 'simple', filterOnType: keyboardNavigation === 'filter', horizontalScrolling @@ -946,6 +952,9 @@ export class WorkbenchObjectTree, TFilterData = void> this._openOnSingleClick = useSingleClickToOpen(configurationService); this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); + const interestingContextKeys = new Set(); + interestingContextKeys.add(WorkbenchListAutomaticKeyboardNavigationKey); + this.disposables.push( this.contextKeyService, (listService as ListService).register(this), @@ -982,6 +991,14 @@ export class WorkbenchObjectTree, TFilterData = void> filterOnType: keyboardNavigation === 'filter' }); } + }), + this.contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(interestingContextKeys)) { + const automaticKeyboardNavigation = this.contextKeyService.getContextKeyValue(WorkbenchListAutomaticKeyboardNavigationKey); + this.updateOptions({ + automaticKeyboardNavigation + }); + } }) ); } @@ -1023,6 +1040,9 @@ export class WorkbenchDataTree extends DataTree(WorkbenchListAutomaticKeyboardNavigationKey); const keyboardNavigation = configurationService.getValue(keyboardNavigationSettingKey); const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); @@ -1032,6 +1052,7 @@ export class WorkbenchDataTree extends DataTree extends DataTree extends DataTree { + if (e.affectsSome(interestingContextKeys)) { + const automaticKeyboardNavigation = this.contextKeyService.getContextKeyValue(WorkbenchListAutomaticKeyboardNavigationKey); + this.updateOptions({ + automaticKeyboardNavigation + }); + } }) ); } @@ -1121,6 +1153,9 @@ export class WorkbenchAsyncDataTree extends Async @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService ) { + WorkbenchListAutomaticKeyboardNavigation.bindTo(contextKeyService); + + const automaticKeyboardNavigation = contextKeyService.getContextKeyValue(WorkbenchListAutomaticKeyboardNavigationKey); const keyboardNavigation = configurationService.getValue(keyboardNavigationSettingKey); const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); @@ -1130,6 +1165,7 @@ export class WorkbenchAsyncDataTree extends Async ...computeStyles(themeService.getTheme(), defaultListStyles), ...toWorkbenchListOptions(options, configurationService, keybindingService), indent: configurationService.getValue(treeIndentKey), + automaticKeyboardNavigation, simpleKeyboardNavigation: keyboardNavigation === 'simple', filterOnType: keyboardNavigation === 'filter', horizontalScrolling @@ -1147,6 +1183,9 @@ export class WorkbenchAsyncDataTree extends Async this._openOnSingleClick = useSingleClickToOpen(configurationService); this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); + const interestingContextKeys = new Set(); + interestingContextKeys.add(WorkbenchListAutomaticKeyboardNavigationKey); + this.disposables.push( this.contextKeyService, (listService as ListService).register(this), @@ -1183,6 +1222,14 @@ export class WorkbenchAsyncDataTree extends Async filterOnType: keyboardNavigation === 'filter' }); } + }), + this.contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(interestingContextKeys)) { + const automaticKeyboardNavigation = this.contextKeyService.getContextKeyValue(WorkbenchListAutomaticKeyboardNavigationKey); + this.updateOptions({ + automaticKeyboardNavigation + }); + } }) ); } diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index 5fbd81fae8e..123778a5809 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -7,7 +7,7 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { WorkbenchListFocusContextKey, IListService, WorkbenchListSupportsMultiSelectContextKey, ListWidget, WorkbenchListHasSelectionOrFocus } from 'vs/platform/list/browser/listService'; +import { WorkbenchListFocusContextKey, IListService, WorkbenchListSupportsMultiSelectContextKey, ListWidget, WorkbenchListHasSelectionOrFocus, WorkbenchListAutomaticKeyboardNavigationKey } from 'vs/platform/list/browser/listService'; import { PagedList } from 'vs/base/browser/ui/list/listPaging'; import { range } from 'vs/base/common/arrays'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -16,6 +16,7 @@ import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; function ensureDOMFocus(widget: ListWidget): void { // it can happen that one of the commands is executed while @@ -742,4 +743,22 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } } } -}); \ No newline at end of file +}); + +CommandsRegistry.registerCommand({ + id: 'list.toggleKeyboardNavigation', + handler: (accessor) => { + const focused = accessor.get(IListService).lastFocusedList; + + // List + if (focused instanceof List || focused instanceof PagedList) { + // TODO@joao + } + + // ObjectTree + else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { + const tree = focused; + tree.toggleKeyboardNavigation(); + } + } +});