|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | + * Licensed under the MIT License. See License.txt in the project root for license information. |
| 4 | + *--------------------------------------------------------------------------------------------*/ |
| 5 | + |
| 6 | +import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; |
| 7 | +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; |
| 8 | +import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; |
| 9 | +import { WorkbenchListFocusContextKey, WorkbenchListScrollAtBottomContextKey, WorkbenchListScrollAtTopContextKey } from 'vs/platform/list/browser/listService'; |
| 10 | +import { Event } from 'vs/base/common/event'; |
| 11 | +import { combinedDisposable, toDisposable, IDisposable, Disposable } from 'vs/base/common/lifecycle'; |
| 12 | +import { Registry } from 'vs/platform/registry/common/platform'; |
| 13 | +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; |
| 14 | +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; |
| 15 | + |
| 16 | +/** INavigatableContainer represents a logical container composed of widgets that can |
| 17 | + be navigated back and forth with key shortcuts */ |
| 18 | +interface INavigatableContainer { |
| 19 | + /** |
| 20 | + * The container may coomposed of multiple parts that share no DOM ancestor |
| 21 | + * (e.g., the main body and filter box of MarkersView may be separated). |
| 22 | + * To track the focus of container we must pass in focus/blur events of all parts |
| 23 | + * as `focusNotifiers`. |
| 24 | + * |
| 25 | + * Each element of `focusNotifiers` notifies the focus/blur event for a part of |
| 26 | + * the container. The container is considered focused if at least one part being |
| 27 | + * focused, and blurred if all parts being blurred. |
| 28 | + */ |
| 29 | + readonly focusNotifiers: readonly IFocusNotifier[]; |
| 30 | + focusPreviousWidget(): void; |
| 31 | + focusNextWidget(): void; |
| 32 | +} |
| 33 | + |
| 34 | +interface IFocusNotifier { |
| 35 | + readonly onDidFocus: Event<any>; |
| 36 | + readonly onDidBlur: Event<any>; |
| 37 | +} |
| 38 | + |
| 39 | +function handleFocusEventsGroup(group: readonly IFocusNotifier[], handler: (isFocus: boolean) => void): IDisposable { |
| 40 | + const focusedIndices = new Set<number>(); |
| 41 | + return combinedDisposable(...group.map((events, index) => combinedDisposable( |
| 42 | + events.onDidFocus(() => { |
| 43 | + if (!focusedIndices.size) { |
| 44 | + handler(true); |
| 45 | + } |
| 46 | + focusedIndices.add(index); |
| 47 | + }), |
| 48 | + events.onDidBlur(() => { |
| 49 | + focusedIndices.delete(index); |
| 50 | + if (!focusedIndices.size) { |
| 51 | + handler(false); |
| 52 | + } |
| 53 | + }), |
| 54 | + ))); |
| 55 | +} |
| 56 | + |
| 57 | +const NavigatableContainerFocusedContextKey = new RawContextKey<boolean>('navigatableContainerFocused', false); |
| 58 | + |
| 59 | +class NavigatableContainerManager implements IDisposable { |
| 60 | + private static INSTANCE: NavigatableContainerManager | undefined; |
| 61 | + |
| 62 | + private readonly containers = new Set<INavigatableContainer>(); |
| 63 | + private lastContainer: INavigatableContainer | undefined; |
| 64 | + private focused: IContextKey<boolean>; |
| 65 | + |
| 66 | + |
| 67 | + constructor(@IContextKeyService contextKeyService: IContextKeyService) { |
| 68 | + this.focused = NavigatableContainerFocusedContextKey.bindTo(contextKeyService); |
| 69 | + NavigatableContainerManager.INSTANCE = this; |
| 70 | + } |
| 71 | + |
| 72 | + dispose(): void { |
| 73 | + this.containers.clear(); |
| 74 | + this.focused.reset(); |
| 75 | + NavigatableContainerManager.INSTANCE = undefined; |
| 76 | + } |
| 77 | + |
| 78 | + static register(container: INavigatableContainer): IDisposable { |
| 79 | + const instance = this.INSTANCE; |
| 80 | + if (!instance) { |
| 81 | + return Disposable.None; |
| 82 | + } |
| 83 | + instance.containers.add(container); |
| 84 | + |
| 85 | + return combinedDisposable( |
| 86 | + handleFocusEventsGroup(container.focusNotifiers, (isFocus) => { |
| 87 | + if (isFocus) { |
| 88 | + instance.focused.set(true); |
| 89 | + instance.lastContainer = container; |
| 90 | + } else if (instance.lastContainer === container) { |
| 91 | + instance.focused.set(false); |
| 92 | + instance.lastContainer = undefined; |
| 93 | + } |
| 94 | + }), |
| 95 | + toDisposable(() => { |
| 96 | + instance.containers.delete(container); |
| 97 | + if (instance.lastContainer === container) { |
| 98 | + instance.focused.set(false); |
| 99 | + instance.lastContainer = undefined; |
| 100 | + } |
| 101 | + }) |
| 102 | + ); |
| 103 | + } |
| 104 | + |
| 105 | + static getActive(): INavigatableContainer | undefined { |
| 106 | + return this.INSTANCE?.lastContainer; |
| 107 | + } |
| 108 | +} |
| 109 | + |
| 110 | +export function registerNavigatableContainer(container: INavigatableContainer): IDisposable { |
| 111 | + return NavigatableContainerManager.register(container); |
| 112 | +} |
| 113 | + |
| 114 | +Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench) |
| 115 | + .registerWorkbenchContribution(NavigatableContainerManager, LifecyclePhase.Starting); |
| 116 | + |
| 117 | +KeybindingsRegistry.registerCommandAndKeybindingRule({ |
| 118 | + id: 'widgetNavigation.focusPrevious', |
| 119 | + weight: KeybindingWeight.WorkbenchContrib, |
| 120 | + when: ContextKeyExpr.and( |
| 121 | + NavigatableContainerFocusedContextKey, |
| 122 | + ContextKeyExpr.or( |
| 123 | + WorkbenchListFocusContextKey?.negate(), |
| 124 | + WorkbenchListScrollAtTopContextKey, |
| 125 | + ) |
| 126 | + ), |
| 127 | + primary: KeyMod.CtrlCmd | KeyCode.UpArrow, |
| 128 | + handler: () => { |
| 129 | + const activeContainer = NavigatableContainerManager.getActive(); |
| 130 | + activeContainer?.focusPreviousWidget(); |
| 131 | + } |
| 132 | +}); |
| 133 | + |
| 134 | +KeybindingsRegistry.registerCommandAndKeybindingRule({ |
| 135 | + id: 'widgetNavigation.focusNext', |
| 136 | + weight: KeybindingWeight.WorkbenchContrib, |
| 137 | + when: ContextKeyExpr.and( |
| 138 | + NavigatableContainerFocusedContextKey, |
| 139 | + ContextKeyExpr.or( |
| 140 | + WorkbenchListFocusContextKey?.negate(), |
| 141 | + WorkbenchListScrollAtBottomContextKey, |
| 142 | + ) |
| 143 | + ), |
| 144 | + primary: KeyMod.CtrlCmd | KeyCode.DownArrow, |
| 145 | + handler: () => { |
| 146 | + const activeContainer = NavigatableContainerManager.getActive(); |
| 147 | + activeContainer?.focusNextWidget(); |
| 148 | + } |
| 149 | +}); |
0 commit comments