diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 8df44ae6f681d321b70894930697829287250149..9c94e242e065a1066d99e449ccbd9f441bf481ab 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -105,6 +105,7 @@ export interface IWindowsService { onWindowBlur: Event; onWindowMaximize: Event; onWindowUnmaximize: Event; + onRecentlyOpenedChange: Event; // Dialogs pickFileFolderAndOpen(options: INativeOpenDialogOptions): TPromise; diff --git a/src/vs/platform/windows/common/windowsIpc.ts b/src/vs/platform/windows/common/windowsIpc.ts index 3cc5e90f6889e4c11bc6d65b2d850e7d2b066b2f..8875c10562107b78cabc859b10094db388e0808e 100644 --- a/src/vs/platform/windows/common/windowsIpc.ts +++ b/src/vs/platform/windows/common/windowsIpc.ts @@ -19,6 +19,7 @@ export interface IWindowsChannel extends IChannel { call(command: 'event:onWindowOpen'): TPromise; call(command: 'event:onWindowFocus'): TPromise; call(command: 'event:onWindowBlur'): TPromise; + call(command: 'event:onRecentlyOpenedChange'): TPromise; call(command: 'pickFileFolderAndOpen', arg: INativeOpenDialogOptions): TPromise; call(command: 'pickFileAndOpen', arg: INativeOpenDialogOptions): TPromise; call(command: 'pickFolderAndOpen', arg: INativeOpenDialogOptions): TPromise; @@ -77,6 +78,7 @@ export class WindowsChannel implements IWindowsChannel { private onWindowBlur: Event; private onWindowMaximize: Event; private onWindowUnmaximize: Event; + private onRecentlyOpenedChange: Event; constructor(private service: IWindowsService) { this.onWindowOpen = buffer(service.onWindowOpen, true); @@ -84,6 +86,7 @@ export class WindowsChannel implements IWindowsChannel { this.onWindowBlur = buffer(service.onWindowBlur, true); this.onWindowMaximize = buffer(service.onWindowMaximize, true); this.onWindowUnmaximize = buffer(service.onWindowUnmaximize, true); + this.onRecentlyOpenedChange = buffer(service.onRecentlyOpenedChange, true); } call(command: string, arg?: any): TPromise { @@ -93,6 +96,7 @@ export class WindowsChannel implements IWindowsChannel { case 'event:onWindowBlur': return eventToCall(this.onWindowBlur); case 'event:onWindowMaximize': return eventToCall(this.onWindowMaximize); case 'event:onWindowUnmaximize': return eventToCall(this.onWindowUnmaximize); + case 'event:onRecentlyOpenedChange': return eventToCall(this.onRecentlyOpenedChange); case 'pickFileFolderAndOpen': return this.service.pickFileFolderAndOpen(arg); case 'pickFileAndOpen': return this.service.pickFileAndOpen(arg); case 'pickFolderAndOpen': return this.service.pickFolderAndOpen(arg); @@ -181,6 +185,9 @@ export class WindowsChannelClient implements IWindowsService { private _onWindowUnmaximize: Event = eventFromCall(this.channel, 'event:onWindowUnmaximize'); get onWindowUnmaximize(): Event { return this._onWindowUnmaximize; } + private _onRecentlyOpenedChange: Event = eventFromCall(this.channel, 'event:onRecentlyOpenedChange'); + get onRecentlyOpenedChange(): Event { return this._onRecentlyOpenedChange; } + pickFileFolderAndOpen(options: INativeOpenDialogOptions): TPromise { return this.channel.call('pickFileFolderAndOpen', options); } diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index e9a6e08c6842b6af7d77e834f45e06cc4e7f8485..44df06d1f9fae8a2e4d16067d8b759083a770742 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -42,6 +42,8 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable readonly onWindowMaximize: Event = filterEvent(fromNodeEventEmitter(app, 'browser-window-maximize', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); readonly onWindowUnmaximize: Event = filterEvent(fromNodeEventEmitter(app, 'browser-window-unmaximize', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); + readonly onRecentlyOpenedChange: Event = this.historyService.onRecentlyOpenedChange; + constructor( private sharedProcess: ISharedProcess, @IWindowsMainService private windowsMainService: IWindowsMainService, diff --git a/src/vs/workbench/browser/parts/menubar/menubarPart.ts b/src/vs/workbench/browser/parts/menubar/menubarPart.ts index 2f287de3d415d63fcc2da6e1a5c25e69fb760133..af50fa982b4df3c156e9d16ac924dfb3c2972d28 100644 --- a/src/vs/workbench/browser/parts/menubar/menubarPart.ts +++ b/src/vs/workbench/browser/parts/menubar/menubarPart.ts @@ -13,9 +13,9 @@ import { Part } from 'vs/workbench/browser/part'; import { IMenubarService, IMenubarMenu, IMenubarMenuItemAction, IMenubarData } from 'vs/platform/menubar/common/menubar'; import { IMenuService, MenuId, IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWindowService, MenuBarVisibility } from 'vs/platform/windows/common/windows'; +import { IWindowService, MenuBarVisibility, IWindowsService } from 'vs/platform/windows/common/windows'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ActionRunner, IActionRunner, IAction } from 'vs/base/common/actions'; +import { ActionRunner, IActionRunner, IAction, Action } from 'vs/base/common/actions'; import { Builder, $ } from 'vs/base/browser/builder'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { EventType, Dimension, toggleClass } from 'vs/base/browser/dom'; @@ -30,6 +30,10 @@ import { Color } from 'vs/base/common/color'; import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { domEvent } from 'vs/base/browser/event'; +import { IRecentlyOpened } from 'vs/platform/history/common/history'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, getWorkspaceLabel } from 'vs/platform/workspaces/common/workspaces'; +import { getPathLabel } from 'vs/base/common/labels'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; interface CustomMenu { title: string; @@ -86,6 +90,7 @@ export class MenubarPart extends Part { private actionRunner: IActionRunner; private container: Builder; + private recentlyOpened: IRecentlyOpened; private _modifierKeyStatus: IModifierKeyStatus; private _isFocused: boolean; private _onVisibilityChange: Emitter; @@ -98,15 +103,19 @@ export class MenubarPart extends Part { menubarFontSize?: number; } = {}; + private static MAX_MENU_RECENT_ENTRIES = 5; + constructor( id: string, @IThemeService themeService: IThemeService, @IMenubarService private menubarService: IMenubarService, @IMenuService private menuService: IMenuService, @IWindowService private windowService: IWindowService, + @IWindowsService private windowsService: IWindowsService, @IContextKeyService private contextKeyService: IContextKeyService, @IKeybindingService private keybindingService: IKeybindingService, - @IConfigurationService private configurationService: IConfigurationService + @IConfigurationService private configurationService: IConfigurationService, + @IEnvironmentService private environmentService: IEnvironmentService ) { super(id, { hasTitle: false }, themeService); @@ -144,6 +153,10 @@ export class MenubarPart extends Part { this.isFocused = false; + this.windowService.getRecentlyOpened().then((recentlyOpened) => { + this.recentlyOpened = recentlyOpened; + }); + this.registerListeners(); } @@ -253,6 +266,13 @@ export class MenubarPart extends Part { } } + private onRecentlyOpenedChange(): void { + this.windowService.getRecentlyOpened().then(recentlyOpened => { + this.recentlyOpened = recentlyOpened; + this.setupMenubar(); + }); + } + private registerListeners(): void { browser.onDidChangeFullscreen(() => this.onDidChangeFullscreen()); @@ -262,6 +282,9 @@ export class MenubarPart extends Part { // Listen to update service // this.updateService.onStateChange(() => this.setupMenubar()); + // Listen for changes in recently opened menu + this.windowsService.onRecentlyOpenedChange(() => { this.onRecentlyOpenedChange(); }); + // Listen to keybindings change this.keybindingService.onDidUpdateKeybindings(() => this.setupMenubar()); @@ -342,6 +365,68 @@ export class MenubarPart extends Part { return this.currentEnableMenuBarMnemonics ? label : label.replace(/&&(.)/g, '$1'); } + private createOpenRecentMenuAction(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string, commandId: string, isFile: boolean): IAction { + + let label: string; + let path: string; + + if (isSingleFolderWorkspaceIdentifier(workspace) || typeof workspace === 'string') { + label = getPathLabel(workspace, this.environmentService); + path = workspace; + } else { + label = getWorkspaceLabel(workspace, this.environmentService, { verbose: true }); + path = workspace.configPath; + } + + return new Action(commandId, label, undefined, undefined, (event) => { + const openInNewWindow = event && ((!isMacintosh && (event.ctrlKey || event.shiftKey)) || (isMacintosh && (event.metaKey || event.altKey))); + + return this.windowService.openWindow([path], { + forceNewWindow: openInNewWindow, + forceOpenWorkspaceAsFile: isFile + }); + }); + } + + private getOpenRecentActions(): IAction[] { + if (!this.recentlyOpened) { + return []; + } + + const { workspaces, files } = this.recentlyOpened; + + const result: IAction[] = []; + + if (workspaces.length > 0) { + for (let i = 0; i < MenubarPart.MAX_MENU_RECENT_ENTRIES && i < workspaces.length; i++) { + result.push(this.createOpenRecentMenuAction(workspaces[i], 'openRecentWorkspace', false)); + } + + result.push(new Separator()); + } + + if (files.length > 0) { + for (let i = 0; i < MenubarPart.MAX_MENU_RECENT_ENTRIES && i < files.length; i++) { + result.push(this.createOpenRecentMenuAction(files[i], 'openRecentFile', false)); + } + + result.push(new Separator()); + } + + return result; + } + + private insertActionsBefore(nextAction: IAction, target: IAction[]): void { + switch (nextAction.id) { + case 'workbench.action.openRecent': + target.push(...this.getOpenRecentActions()); + break; + + default: + break; + } + } + private setupCustomMenubar(): void { this.container.empty(); this.container.attr('role', 'menubar'); @@ -381,6 +466,7 @@ export class MenubarPart extends Part { const [, actions] = group; for (let action of actions) { + this.insertActionsBefore(action, target); if (action instanceof SubmenuItemAction) { const submenu = this.menuService.createMenu(action.item.submenu, this.contextKeyService); const submenuActions = []; diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index b9e2dabcb7424508fbcbcc2f6ad52436aab6932d..a64e270d6ccb5ae2c9a0f60ad7afc3af2a14ad85 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -1123,6 +1123,7 @@ export class TestWindowsService implements IWindowsService { onWindowBlur: Event; onWindowMaximize: Event; onWindowUnmaximize: Event; + onRecentlyOpenedChange: Event; isFocused(windowId: number): TPromise { return TPromise.as(false);