diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index aa0cbc50e3af6..4a3296369f00d 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -105,6 +105,7 @@ import { import { QuickAccessContribution } from './quick-input/quick-access'; import { QuickCommandService } from './quick-input/quick-command-service'; import { SidebarBottomMenuWidget } from './shell/sidebar-bottom-menu-widget'; +import { WindowContribution } from './window-contribution'; export { bindResourceProvider, bindMessageService, bindPreferenceService }; @@ -356,4 +357,8 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo bind(CredentialsService).to(CredentialsServiceImpl); bind(ContributionFilterRegistry).to(ContributionFilterRegistryImpl).inSingletonScope(); + bind(WindowContribution).toSelf().inSingletonScope(); + for (const contribution of [CommandContribution, KeybindingContribution, MenuContribution]) { + bind(contribution).toService(WindowContribution); + } }); diff --git a/packages/core/src/browser/window-contribution.ts b/packages/core/src/browser/window-contribution.ts new file mode 100644 index 0000000000000..29f377be9c478 --- /dev/null +++ b/packages/core/src/browser/window-contribution.ts @@ -0,0 +1,64 @@ +/******************************************************************************** + * Copyright (C) 2021 Ericsson and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable } from 'inversify'; +import { Command, CommandContribution, CommandRegistry, environment } from '../common'; +import { WindowService } from './window/window-service'; +import { KeybindingContribution, KeybindingRegistry } from './keybinding'; +import { MenuContribution, MenuModelRegistry } from '../common/menu'; +import { CommonMenus } from '../browser/common-frontend-contribution'; + +export namespace WindowCommands { + + export const NEW_WINDOW: Command = { + id: 'workbench.action.newWindow', + label: 'New Window' + }; +} + +@injectable() +export class WindowContribution implements CommandContribution, KeybindingContribution, MenuContribution { + + @inject(WindowService) + protected windowService: WindowService; + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(WindowCommands.NEW_WINDOW, { + execute: () => { + this.windowService.openNewDefaultWindow(); + } + }); + } + + registerKeybindings(registry: KeybindingRegistry): void { + registry.registerKeybindings({ + command: WindowCommands.NEW_WINDOW.id, + keybinding: this.isElectron() ? 'ctrlcmd+shift+n' : 'alt+shift+n' + }); + } + + registerMenus(registry: MenuModelRegistry): void { + registry.registerMenuAction(CommonMenus.FILE_NEW, { + commandId: WindowCommands.NEW_WINDOW.id, + order: 'c' + }); + } + + private isElectron(): boolean { + return environment.electron.is(); + } + +} diff --git a/packages/core/src/browser/window/default-window-service.ts b/packages/core/src/browser/window/default-window-service.ts index af33bcf16f367..1e9a4b029230e 100644 --- a/packages/core/src/browser/window/default-window-service.ts +++ b/packages/core/src/browser/window/default-window-service.ts @@ -20,6 +20,7 @@ import { CorePreferences } from '../core-preferences'; import { ContributionProvider } from '../../common/contribution-provider'; import { FrontendApplicationContribution, FrontendApplication } from '../frontend-application'; import { WindowService } from './window-service'; +import { DEFAULT_WINDOW_HASH } from './window-service'; @injectable() export class DefaultWindowService implements WindowService, FrontendApplicationContribution { @@ -48,6 +49,10 @@ export class DefaultWindowService implements WindowService, FrontendApplicationC return undefined; } + openNewDefaultWindow(): void { + this.openNewWindow(DEFAULT_WINDOW_HASH); + } + canUnload(): boolean { const confirmExit = this.corePreferences['application.confirmExit']; let preventUnload = confirmExit === 'always'; diff --git a/packages/core/src/browser/window/test/mock-window-service.ts b/packages/core/src/browser/window/test/mock-window-service.ts index a3cca33908fa3..94c0da87dbb61 100644 --- a/packages/core/src/browser/window/test/mock-window-service.ts +++ b/packages/core/src/browser/window/test/mock-window-service.ts @@ -20,6 +20,7 @@ import { WindowService } from '../window-service'; @injectable() export class MockWindowService implements WindowService { openNewWindow(): undefined { return undefined; } + openNewDefaultWindow(): void { } canUnload(): boolean { return true; } get onUnload(): Event { return Event.None; } } diff --git a/packages/core/src/browser/window/window-service.ts b/packages/core/src/browser/window/window-service.ts index cec3b1e9a25f7..e06d903e7c67a 100644 --- a/packages/core/src/browser/window/window-service.ts +++ b/packages/core/src/browser/window/window-service.ts @@ -24,6 +24,12 @@ export interface NewWindowOptions { * Service for opening new browser windows. */ export const WindowService = Symbol('WindowService'); + +/** + * The window hash value that is used to spawn a new default window. + */ +export const DEFAULT_WINDOW_HASH: string = '#!empty'; + export interface WindowService { /** @@ -33,6 +39,12 @@ export interface WindowService { */ openNewWindow(url: string, options?: NewWindowOptions): undefined; + /** + * Opens a new default window. + * - In electron and in the browser it will open the default window without a pre-defined content. + */ + openNewDefaultWindow(): void; + /** * Called when the `window` is about to `unload` its resources. * At this point, the `document` is still visible and the [`BeforeUnloadEvent`](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event) diff --git a/packages/core/src/electron-browser/window/electron-window-service.ts b/packages/core/src/electron-browser/window/electron-window-service.ts index e290cadf8ddf9..3869913200acd 100644 --- a/packages/core/src/electron-browser/window/electron-window-service.ts +++ b/packages/core/src/electron-browser/window/electron-window-service.ts @@ -45,6 +45,10 @@ export class ElectronWindowService extends DefaultWindowService { return undefined; } + openNewDefaultWindow(): void { + this.delegate.openNewDefaultWindow(); + } + @postConstruct() protected init(): void { // Update the default zoom level on startup when the preferences event is fired. diff --git a/packages/core/src/electron-common/electron-main-window-service.ts b/packages/core/src/electron-common/electron-main-window-service.ts index 2efc7f8f3ff3c..66aa1cd12ecc1 100644 --- a/packages/core/src/electron-common/electron-main-window-service.ts +++ b/packages/core/src/electron-common/electron-main-window-service.ts @@ -20,4 +20,5 @@ export const electronMainWindowServicePath = '/services/electron-window'; export const ElectronMainWindowService = Symbol('ElectronMainWindowService'); export interface ElectronMainWindowService { openNewWindow(url: string, options?: NewWindowOptions): undefined; + openNewDefaultWindow(): void; } diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index 6d0da0cced7a5..c27efcc1053ec 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -30,6 +30,7 @@ import { ContributionProvider } from '../common/contribution-provider'; import { ElectronSecurityTokenService } from './electron-security-token-service'; import { ElectronSecurityToken } from '../electron-common/electron-token'; import Storage = require('electron-store'); +import { DEFAULT_WINDOW_HASH } from '../browser/window/window-service'; const createYargs: (argv?: string[], cwd?: string) => Argv = require('yargs/yargs'); /** @@ -268,10 +269,9 @@ export class ElectronMainApplication { }; } - protected async openDefaultWindow(): Promise { - const options = await this.getLastWindowOptions(); - const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.createWindow(options)]); - electronWindow.loadURL(uri.toString(true)); + async openDefaultWindow(): Promise { + const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.createWindow()]); + electronWindow.loadURL(uri.withFragment(DEFAULT_WINDOW_HASH).toString(true)); return electronWindow; } diff --git a/packages/core/src/electron-main/electron-main-window-service-impl.ts b/packages/core/src/electron-main/electron-main-window-service-impl.ts index 7b14b0148f046..b2dfa6f095d61 100644 --- a/packages/core/src/electron-main/electron-main-window-service-impl.ts +++ b/packages/core/src/electron-main/electron-main-window-service-impl.ts @@ -37,4 +37,8 @@ export class ElectronMainWindowServiceImpl implements ElectronMainWindowService return undefined; } + openNewDefaultWindow(): void { + this.app.openDefaultWindow(); + } + } diff --git a/packages/workspace/src/browser/workspace-commands.ts b/packages/workspace/src/browser/workspace-commands.ts index 9fb1c10d16410..7b65e498bdec1 100644 --- a/packages/workspace/src/browser/workspace-commands.ts +++ b/packages/workspace/src/browser/workspace-commands.ts @@ -147,10 +147,12 @@ export class FileMenuContribution implements MenuContribution { registerMenus(registry: MenuModelRegistry): void { registry.registerMenuAction(CommonMenus.FILE_NEW, { - commandId: WorkspaceCommands.NEW_FILE.id + commandId: WorkspaceCommands.NEW_FILE.id, + order: 'a' }); registry.registerMenuAction(CommonMenus.FILE_NEW, { - commandId: WorkspaceCommands.NEW_FOLDER.id + commandId: WorkspaceCommands.NEW_FOLDER.id, + order: 'b' }); const downloadUploadMenu = [...CommonMenus.FILE, '4_downloadupload']; registry.registerMenuAction(downloadUploadMenu, { diff --git a/packages/workspace/src/browser/workspace-service.ts b/packages/workspace/src/browser/workspace-service.ts index 1b80e5b66cc83..607a24c827157 100644 --- a/packages/workspace/src/browser/workspace-service.ts +++ b/packages/workspace/src/browser/workspace-service.ts @@ -17,7 +17,7 @@ import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; import { WorkspaceServer, THEIA_EXT, VSCODE_EXT, getTemporaryWorkspaceFileUri } from '../common'; -import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { DEFAULT_WINDOW_HASH, WindowService } from '@theia/core/lib/browser/window/window-service'; import { FrontendApplicationContribution, PreferenceServiceImpl, PreferenceScope, PreferenceSchemaProvider, LabelProvider } from '@theia/core/lib/browser'; @@ -128,6 +128,13 @@ export class WorkspaceService implements FrontendApplicationContribution { } protected async doGetDefaultWorkspaceUri(): Promise { + + // If an empty window is explicitly requested do not restore a previous workspace. + if (window.location.hash === DEFAULT_WINDOW_HASH) { + window.location.hash = ''; + return undefined; + } + // Prefer the workspace path specified as the URL fragment, if present. if (window.location.hash.length > 1) { // Remove the leading # and decode the URI.