diff --git a/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/src/vs/platform/keybinding/common/abstractKeybindingService.ts index 78d71449bec3c280a5717049b10823684fe479d6..94ad74d8bd082bff7c6ab6b5ec893bcd773a2f03 100644 --- a/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -152,10 +152,20 @@ export abstract class AbstractKeybindingService extends Disposable implements IK this._currentChord = null; } + public dispatchByUserSettingsLabel(userSettingsLabel: string, target: IContextKeyServiceTarget): void { + const keybindings = this.resolveUserBinding(userSettingsLabel); + if (keybindings.length === 1) { + this._doDispatch(keybindings[0], target); + } + } + protected _dispatch(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean { + return this._doDispatch(this.resolveKeyboardEvent(e), target); + } + + private _doDispatch(keybinding: ResolvedKeybinding, target: IContextKeyServiceTarget): boolean { let shouldPreventDefault = false; - const keybinding = this.resolveKeyboardEvent(e); if (keybinding.isChord()) { console.warn('Unexpected keyboard event mapped to a chord'); return false; diff --git a/src/vs/platform/keybinding/common/keybinding.ts b/src/vs/platform/keybinding/common/keybinding.ts index 078cea23edef348e0a97f8cab81d37308e9d6ec3..59ce959636b6ebb300b430fb931657e01e5aaecf 100644 --- a/src/vs/platform/keybinding/common/keybinding.ts +++ b/src/vs/platform/keybinding/common/keybinding.ts @@ -57,6 +57,8 @@ export interface IKeybindingService { */ softDispatch(keyboardEvent: IKeyboardEvent, target: IContextKeyServiceTarget): IResolveResult | null; + dispatchByUserSettingsLabel(userSettingsLabel: string, target: IContextKeyServiceTarget): void; + /** * Look up keybindings for a command. * Use `lookupKeybinding` if you are interested in the preferred keybinding. diff --git a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts index 99893ecbff8e14ee3242813ce5bd3a486fb19a72..690f1fdaf9a768bf9029ff707cd56483c11aad5a 100644 --- a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts +++ b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts @@ -120,8 +120,7 @@ export class MockKeybindingService implements IKeybindingService { return null; } - dispatchEvent(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean { - return false; + dispatchByUserSettingsLabel(userSettingsLabel: string, target: IContextKeyServiceTarget): void { } mightProducePrintableCharacter(e: IKeyboardEvent): boolean { diff --git a/src/vs/platform/menubar/common/menubar.ts b/src/vs/platform/menubar/common/menubar.ts index 0e90daf9bede267bbde7e789d3d4e1c54b61dbf2..59fcaf1f1027de94633bbf9e3c1485b58017a2aa 100644 --- a/src/vs/platform/menubar/common/menubar.ts +++ b/src/vs/platform/menubar/common/menubar.ts @@ -25,6 +25,7 @@ export interface IMenubarMenu { export interface IMenubarKeybinding { label: string; + userSettingsLabel: string; isNative?: boolean; // Assumed true if missing } diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 3232086675a47fec1e65a50d6571f1fac5e98adf..9cf9e9f76ad3216ccd4f1355cdeca99ce0108a94 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { isMacintosh, language } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { app, shell, Menu, MenuItem, BrowserWindow } from 'electron'; -import { OpenContext, IRunActionInWindowRequest, getTitleBarStyle } from 'vs/platform/windows/common/windows'; +import { OpenContext, IRunActionInWindowRequest, getTitleBarStyle, IRunKeybindingInWindowRequest } from 'vs/platform/windows/common/windows'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUpdateService, StateType } from 'vs/platform/update/common/update'; @@ -31,6 +31,15 @@ interface IMenuItemClickHandler { inNoWindow: () => void; } +type IMenuItemInvocation = ( + { type: 'commandId'; commandId: string; } + | { type: 'keybinding'; userSettingsLabel: string; } +); + +interface IMenuItemWithKeybinding { + userSettingsLabel?: string; +} + export class Menubar { private static readonly MAX_MENU_RECENT_ENTRIES = 10; @@ -621,17 +630,49 @@ export class Menubar { } } + private static _menuItemIsTriggeredViaKeybinding(event: Electron.Event, userSettingsLabel: string): boolean { + // The event coming in from Electron will inform us only about the modifier keys pressed. + // The strategy here is to check if the modifier keys match those of the keybinding, + // since it is highly unlikely to use modifier keys when clicking with the mouse + if (!userSettingsLabel) { + // There is no keybinding + return false; + } + + let ctrlRequired = /ctrl/.test(userSettingsLabel); + let shiftRequired = /shift/.test(userSettingsLabel); + let altRequired = /alt/.test(userSettingsLabel); + let metaRequired = /cmd/.test(userSettingsLabel) || /super/.test(userSettingsLabel); + + if (!ctrlRequired && !shiftRequired && !altRequired && !metaRequired) { + // This keybinding does not use any modifier keys, so we cannot use this heuristic + return false; + } + + return ( + ctrlRequired === event.ctrlKey + && shiftRequired === event.shiftKey + && altRequired === event.altKey + && metaRequired === event.metaKey + ); + } + private createMenuItem(label: string, commandId: string | string[], enabled?: boolean, checked?: boolean): Electron.MenuItem; private createMenuItem(label: string, click: () => void, enabled?: boolean, checked?: boolean): Electron.MenuItem; private createMenuItem(arg1: string, arg2: any, arg3?: boolean, arg4?: boolean): Electron.MenuItem { const label = this.mnemonicLabel(arg1); - const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: Electron.MenuItem, win: Electron.BrowserWindow, event: Electron.Event) => { + const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: Electron.MenuItem & IMenuItemWithKeybinding, win: Electron.BrowserWindow, event: Electron.Event) => { + const userSettingsLabel = menuItem.userSettingsLabel; let commandId = arg2; if (Array.isArray(arg2)) { commandId = this.isOptionClick(event) ? arg2[1] : arg2[0]; // support alternative action if we got multiple action Ids and the option key was pressed while invoking } - this.runActionInRenderer(commandId); + if (isMacintosh && userSettingsLabel && Menubar._menuItemIsTriggeredViaKeybinding(event, userSettingsLabel)) { + this.runActionInRenderer({ type: 'keybinding', userSettingsLabel }); + } else { + this.runActionInRenderer({ type: 'commandId', commandId }); + } }; const enabled = typeof arg3 === 'boolean' ? arg3 : this.windowsMainService.getWindowCount() > 0; const checked = typeof arg4 === 'boolean' ? arg4 : false; @@ -704,7 +745,7 @@ export class Menubar { }; } - private runActionInRenderer(id: string): void { + private runActionInRenderer(invocation: IMenuItemInvocation): void { // We make sure to not run actions when the window has no focus, this helps // for https://github.com/Microsoft/vscode/issues/25907 and specifically for // https://github.com/Microsoft/vscode/issues/11928 @@ -719,18 +760,21 @@ export class Menubar { } if (activeWindow) { - if (!activeWindow.isReady && isMacintosh && id === 'workbench.action.toggleDevTools' && !this.environmentService.isBuilt) { - // prevent this action from running twice on macOS (https://github.com/Microsoft/vscode/issues/62719) - // we already register a keybinding in bootstrap-window.js for opening developer tools in case something - // goes wrong and that keybinding is only removed when the application has loaded (= window ready). - return; + if (invocation.type === 'commandId') { + if (!activeWindow.isReady && isMacintosh && invocation.commandId === 'workbench.action.toggleDevTools' && !this.environmentService.isBuilt) { + // prevent this action from running twice on macOS (https://github.com/Microsoft/vscode/issues/62719) + // we already register a keybinding in bootstrap-window.js for opening developer tools in case something + // goes wrong and that keybinding is only removed when the application has loaded (= window ready). + return; + } + this.windowsMainService.sendToFocused('vscode:runAction', { id: invocation.commandId, from: 'menu' } as IRunActionInWindowRequest); + } else { + this.windowsMainService.sendToFocused('vscode:runKeybinding', { userSettingsLabel: invocation.userSettingsLabel } as IRunKeybindingInWindowRequest); } - - this.windowsMainService.sendToFocused('vscode:runAction', { id, from: 'menu' } as IRunActionInWindowRequest); } } - private withKeybinding(commandId: string | undefined, options: Electron.MenuItemConstructorOptions): Electron.MenuItemConstructorOptions { + private withKeybinding(commandId: string | undefined, options: Electron.MenuItemConstructorOptions & IMenuItemWithKeybinding): Electron.MenuItemConstructorOptions { const binding = typeof commandId === 'string' ? this.keybindings[commandId] : undefined; // Apply binding if there is one @@ -739,6 +783,7 @@ export class Menubar { // if the binding is native, we can just apply it if (binding.isNative !== false) { options.accelerator = binding.label; + options.userSettingsLabel = binding.userSettingsLabel; } // the keybinding is not native so we cannot show it as part of the accelerator of @@ -768,6 +813,7 @@ export class Menubar { const originalClick = options.click; options.click = (item, window, event) => { + console.log(`444444`); this.reportMenuActionTelemetry(commandId); if (originalClick) { originalClick(item, window, event); diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index f100efedb5031b0bcd7ca6fc8b86d0e3339ce939..7bcdef3e9dfa5c744eb77b2120bfac40345528b1 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -408,6 +408,10 @@ export interface IRunActionInWindowRequest { args?: any[]; } +export interface IRunKeybindingInWindowRequest { + userSettingsLabel: string; +} + export class ActiveWindowManager implements IDisposable { private disposables: IDisposable[] = []; diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 8363e7a905723af03451aa5cb760bbf6077c573a..c18d455f9d5be445dda8ff0e42cc109599e23992 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -980,13 +980,13 @@ export class MenubarControl extends Disposable { // first try to resolve a native accelerator const electronAccelerator = binding.getElectronAccelerator(); if (electronAccelerator) { - return { label: electronAccelerator }; + return { label: electronAccelerator, userSettingsLabel: binding.getUserSettingsLabel() }; } // we need this fallback to support keybindings that cannot show in electron menus (e.g. chords) const acceleratorLabel = binding.getLabel(); if (acceleratorLabel) { - return { label: acceleratorLabel, isNative: false }; + return { label: acceleratorLabel, isNative: false, userSettingsLabel: binding.getUserSettingsLabel() }; } return null; diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index ea348f3a0409ad1d5793891f21c00864d0a3cf3c..3ea5f2f27d3220ef2dc1b3856da8667f951bd820 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -15,7 +15,7 @@ import { toResource, IUntitledResourceInput } from 'vs/workbench/common/editor'; import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/common/editorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; -import { IWindowsService, IWindowService, IWindowSettings, IOpenFileRequest, IWindowsConfiguration, IAddFoldersRequest, IRunActionInWindowRequest, IPathData } from 'vs/platform/windows/common/windows'; +import { IWindowsService, IWindowService, IWindowSettings, IOpenFileRequest, IWindowsConfiguration, IAddFoldersRequest, IRunActionInWindowRequest, IPathData, IRunKeybindingInWindowRequest } from 'vs/platform/windows/common/windows'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; import { IWorkbenchThemeService, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -38,6 +38,7 @@ import { AccessibilitySupport, isRootUser, isWindows, isMacintosh } from 'vs/bas import product from 'vs/platform/node/product'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; const TextInputActions: IAction[] = [ new Action('undo', nls.localize('undo', "Undo"), null, true, () => document.execCommand('undo') && Promise.resolve(true)), @@ -71,6 +72,7 @@ export class ElectronWindow extends Themable { @IWorkbenchThemeService protected themeService: IWorkbenchThemeService, @INotificationService private notificationService: INotificationService, @ICommandService private commandService: ICommandService, + @IKeybindingService private keybindingService: IKeybindingService, @IContextMenuService private contextMenuService: IContextMenuService, @ITelemetryService private telemetryService: ITelemetryService, @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService, @@ -133,6 +135,11 @@ export class ElectronWindow extends Themable { }); }); + // Support runKeybinding event + ipc.on('vscode:runKeybinding', (event: any, request: IRunKeybindingInWindowRequest) => { + this.keybindingService.dispatchByUserSettingsLabel(request.userSettingsLabel, document.activeElement); + }); + // Error reporting from main ipc.on('vscode:reportError', (event: any, error: string) => { if (error) {