diff --git a/src/vs/platform/layout/browser/layoutService.ts b/src/vs/platform/layout/browser/layoutService.ts index 652791e9b2ecd..3f9b354ae22e1 100644 --- a/src/vs/platform/layout/browser/layoutService.ts +++ b/src/vs/platform/layout/browser/layoutService.ts @@ -35,7 +35,7 @@ export interface ILayoutService { /** * An event that is emitted when any container is layed out. */ - readonly onDidLayoutContainer: Event<{ container: HTMLElement; dimension: IDimension }>; + readonly onDidLayoutContainer: Event<{ readonly container: HTMLElement; readonly dimension: IDimension }>; /** * An event that is emitted when the active container is layed out. @@ -46,7 +46,7 @@ export interface ILayoutService { * An event that is emitted when a new container is added. This * can happen in multi-window environments. */ - readonly onDidAddContainer: Event<{ container: HTMLElement; disposables: DisposableStore }>; + readonly onDidAddContainer: Event<{ readonly container: HTMLElement; readonly disposables: DisposableStore }>; /** * An event that is emitted when the active container changes. @@ -65,15 +65,6 @@ export interface ILayoutService { /** * Main container of the application. - * - * **NOTE**: In the standalone editor case, multiple editors can be created on a page. - * Therefore, in the standalone editor case, there are multiple containers, not just - * a single one. If you ship code that needs a "container" for the standalone editor, - * please use `activeContainer` to get the current focused code editor and use its - * container if necessary. You can also instantiate `EditorScopedLayoutService` - * which implements `ILayoutService` but is not a part of the service collection because - * it is code editor instance specific. - * */ readonly mainContainer: HTMLElement; diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 6c8aae29831a2..d0643bd67e84f 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -6,7 +6,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { MessageBoxOptions, MessageBoxReturnValue, MouseInputEvent, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'vs/base/parts/sandbox/common/electronTypes'; +import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'vs/base/parts/sandbox/common/electronTypes'; import { ISerializableCommandAction } from 'vs/platform/action/common/action'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -175,7 +175,6 @@ export interface ICommonNativeHostService { // Development openDevTools(options?: OpenDevToolsOptions): Promise; toggleDevTools(): Promise; - sendInputEvent(event: MouseInputEvent): Promise; // Perf Introspection profileRenderer(session: string, duration: number): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 062ce155e85cc..0929f47ea05d5 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -19,7 +19,6 @@ import { realpath } from 'vs/base/node/extpath'; import { virtualMachineHint } from 'vs/base/node/id'; import { Promises, SymlinkSupport } from 'vs/base/node/pfs'; import { findFreePort } from 'vs/base/node/ports'; -import { MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes'; import { localize } from 'vs/nls'; import { ISerializableCommandAction } from 'vs/platform/action/common/action'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; @@ -209,7 +208,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain window?.handleTitleDoubleClick(); } - async getCursorScreenPoint(firstArg: number | undefined): Promise { + async getCursorScreenPoint(windowId: number | undefined): Promise { return screen.getCursorScreenPoint(); } @@ -250,8 +249,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } } - async positionWindow(firstArg: number | undefined, position: IRectangle, options?: INativeOptions): Promise { - const window = this.windowById(options?.targetWindowId) ?? this.codeWindowById(firstArg); + async positionWindow(windowId: number | undefined, position: IRectangle, options?: INativeOptions): Promise { + const window = this.windowById(options?.targetWindowId, windowId); if (window?.win) { if (window.win.isFullScreen()) { const fullscreenLeftFuture = Event.toPromise(Event.once(Event.fromNodeEventEmitter(window.win, 'leave-full-screen'))); @@ -397,6 +396,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return { source, target }; } + //#endregion + //#region Dialog async showMessageBox(windowId: number | undefined, options: MessageBoxOptions): Promise { @@ -590,13 +591,13 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return virtualMachineHint.value(); } - public async getOSColorScheme(): Promise { + async getOSColorScheme(): Promise { return this.themeMainService.getColorScheme(); } // WSL - public async hasWSLFeatureInstalled(): Promise { + async hasWSLFeatureInstalled(): Promise { return isWindows && hasWSLFeatureInstalled(); } @@ -786,13 +787,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain window?.win?.webContents.toggleDevTools(); } - async sendInputEvent(windowId: number | undefined, event: MouseInputEvent): Promise { - const window = this.codeWindowById(windowId); - if (window?.win && (event.type === 'mouseDown' || event.type === 'mouseUp')) { - window.win.webContents.sendInputEvent(event); - } - } - //#endregion // #region Performance diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index ef796401b5a11..816cc9fa36e8c 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -1390,8 +1390,8 @@ export class AuxiliaryEditorPart extends EditorPart implements IAuxiliaryEditorP private static COUNTER = 1; - private readonly _onDidClose = this._register(new Emitter()); - readonly onDidClose = this._onDidClose.event; + private readonly _onWillClose = this._register(new Emitter()); + readonly onWillClose = this._onWillClose.event; constructor( editorPartsView: IEditorPartsView, @@ -1435,6 +1435,6 @@ export class AuxiliaryEditorPart extends EditorPart implements IAuxiliaryEditorP this.mergeAllGroups(this.editorPartsView.mainPart.activeGroup); } - this._onDidClose.fire(); + this._onWillClose.fire(); } } diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index f7ffa01b596d2..0626bb73af6b9 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -54,14 +54,16 @@ export class EditorParts extends Disposable implements IEditorGroupsService, IEd editorPart.create(partContainer, { restorePreviousState: false }); disposables.add(this.instantiationService.createInstance(WindowTitle, auxiliaryWindow.window, editorPart)); + const editorCloseListener = disposables.add(Event.once(editorPart.onWillClose)(() => auxiliaryWindow.window.close())); disposables.add(Event.once(auxiliaryWindow.onWillClose)(() => { if (disposables.isDisposed) { return; // the close happened as part of an earlier dispose call } + editorCloseListener.dispose(); editorPart.close(); + disposables.dispose(); })); - disposables.add(Event.once(editorPart.onDidClose)(() => disposables.dispose())); disposables.add(Event.once(this.lifecycleService.onDidShutdown)(() => disposables.dispose())); disposables.add(auxiliaryWindow.onDidLayout(dimension => editorPart.layout(dimension.width, dimension.height, 0, 0))); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 69109a59d2d6f..198ebc96fa42f 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -737,7 +737,7 @@ const registry = Registry.as(ConfigurationExtensions.Con 'default': (isWeb && !isStandalone()) ? 'keyboardOnly' : 'never', // on by default in web, unless PWA, never on desktop 'markdownDescription': isWeb ? localize('confirmBeforeCloseWeb', "Controls whether to show a confirmation dialog before closing the browser tab or window. Note that even if enabled, browsers may still decide to close a tab or window without confirmation and that this setting is only a hint that may not work in all cases.") : - localize('confirmBeforeClose', "Controls whether to show a confirmation dialog before closing the window or quitting the application."), + localize('confirmBeforeClose', "Controls whether to show a confirmation dialog before closing a window or quitting the application."), 'scope': ConfigurationScope.APPLICATION } } diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index dee6ef685170f..1ce14691032d6 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -487,7 +487,7 @@ export class NativeWindow extends BaseWindow { }); // Update setting if checkbox checked - if (res.checkboxChecked) { + if (res.confirmed && res.checkboxChecked) { await configurationService.updateValue('window.confirmBeforeClose', 'never'); } diff --git a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts index 78da11b7a1e5a..e2a1c27421189 100644 --- a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { mark } from 'vs/base/common/performance'; import { Emitter, Event } from 'vs/base/common/event'; -import { Dimension, EventHelper, EventType, addDisposableListener, cloneGlobalStylesheets, copyAttributes, createMetaElement, getActiveWindow, getClientArea, getWindowId, isGlobalStylesheet, position, registerWindow, sharedMutationObserver, size, trackAttributes } from 'vs/base/browser/dom'; +import { Dimension, EventHelper, EventType, ModifierKeyEmitter, addDisposableListener, cloneGlobalStylesheets, copyAttributes, createMetaElement, getActiveWindow, getClientArea, getWindowId, isGlobalStylesheet, position, registerWindow, sharedMutationObserver, size, trackAttributes } from 'vs/base/browser/dom'; import { CodeWindow, ensureCodeWindow, mainWindow } from 'vs/base/browser/window'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -18,6 +18,7 @@ import { IRectangle, WindowMinimumSize } from 'vs/platform/window/common/window' import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { BaseWindow } from 'vs/workbench/browser/window'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export const IAuxiliaryWindowService = createDecorator('auxiliaryWindowService'); @@ -52,7 +53,7 @@ export interface IAuxiliaryWindow extends IDisposable { layout(): void; } -class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { +export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { private readonly _onDidLayout = this._register(new Emitter()); readonly onDidLayout = this._onDidLayout.event; @@ -63,16 +64,19 @@ class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { private readonly _onWillDispose = this._register(new Emitter()); readonly onWillDispose = this._onWillDispose.event; - constructor(readonly window: CodeWindow, readonly container: HTMLElement) { + constructor( + readonly window: CodeWindow, + readonly container: HTMLElement, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { super(window); this.registerListeners(); } private registerListeners(): void { - this._register(addDisposableListener(this.window, 'beforeunload', () => { - this._onWillClose.fire(); - })); + this._register(addDisposableListener(this.window, EventType.BEFORE_UNLOAD, (e: BeforeUnloadEvent) => this.onBeforeUnload(e))); + this._register(addDisposableListener(this.window, EventType.UNLOAD, () => this._onWillClose.fire())); this._register(addDisposableListener(this.window, 'unhandledrejection', e => { onUnexpectedError(e.reason); @@ -99,6 +103,19 @@ class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { } } + private onBeforeUnload(e: BeforeUnloadEvent): void { + const confirmBeforeCloseSetting = this.configurationService.getValue<'always' | 'never' | 'keyboardOnly'>('window.confirmBeforeClose'); + const confirmBeforeClose = confirmBeforeCloseSetting === 'always' || (confirmBeforeCloseSetting === 'keyboardOnly' && ModifierKeyEmitter.getInstance().isModifierPressed); + if (confirmBeforeClose) { + this.confirmBeforeClose(e); + } + } + + protected confirmBeforeClose(e: BeforeUnloadEvent): void { + e.preventDefault(); + e.returnValue = localize('lifecycleVeto', "Changes that you made may not be saved. Please check press 'Cancel' and try again."); + } + layout(): void { this._onDidLayout.fire(getClientArea(this.container)); } @@ -129,7 +146,8 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili constructor( @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IDialogService private readonly dialogService: IDialogService + @IDialogService private readonly dialogService: IDialogService, + @IConfigurationService protected readonly configurationService: IConfigurationService ) { super(); } @@ -149,7 +167,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili const containerDisposables = new DisposableStore(); const container = this.createContainer(targetWindow, containerDisposables); - const auxiliaryWindow = new AuxiliaryWindow(targetWindow, container); + const auxiliaryWindow = this.createAuxiliaryWindow(targetWindow, container); const registryDisposables = new DisposableStore(); this.windows.set(targetWindow.vscodeWindowId, auxiliaryWindow); @@ -173,6 +191,10 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili return auxiliaryWindow; } + protected createAuxiliaryWindow(targetWindow: CodeWindow, container: HTMLElement): AuxiliaryWindow { + return new AuxiliaryWindow(targetWindow, container, this.configurationService); + } + private async openWindow(options?: IAuxiliaryWindowOpenOptions): Promise { const activeWindow = getActiveWindow(); diff --git a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts index 66427a0fe8c21..226ab13d04d8a 100644 --- a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts @@ -5,7 +5,7 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { BrowserAuxiliaryWindowService, IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; +import { AuxiliaryWindow, BrowserAuxiliaryWindowService, IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { ISandboxGlobals } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWindowsConfiguration } from 'vs/platform/window/common/window'; @@ -15,23 +15,56 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { getActiveWindow } from 'vs/base/browser/dom'; import { CodeWindow } from 'vs/base/browser/window'; import { mark } from 'vs/base/common/performance'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { NativeWindow } from 'vs/workbench/electron-sandbox/window'; +import { ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; -type NativeAuxiliaryWindow = CodeWindow & { +type NativeCodeWindow = CodeWindow & { readonly vscode: ISandboxGlobals; }; +export class NativeAuxiliaryWindow extends AuxiliaryWindow { + + private skipUnloadConfirmation = false; + + constructor( + window: CodeWindow, + container: HTMLElement, + @IConfigurationService configurationService: IConfigurationService, + @INativeHostService private readonly nativeHostService: INativeHostService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(window, container, configurationService); + } + + protected override async confirmBeforeClose(e: BeforeUnloadEvent): Promise { + if (this.skipUnloadConfirmation) { + return; + } + + e.preventDefault(); + + const confirmed = await this.instantiationService.invokeFunction(accessor => NativeWindow.confirmOnShutdown(accessor, ShutdownReason.CLOSE)); + if (confirmed) { + this.skipUnloadConfirmation = true; + this.nativeHostService.closeWindowById(this.window.vscodeWindowId); + } + } +} + export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService { constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, - @IConfigurationService private readonly configurationService: IConfigurationService, + @IConfigurationService configurationService: IConfigurationService, @INativeHostService private readonly nativeHostService: INativeHostService, - @IDialogService dialogService: IDialogService + @IDialogService dialogService: IDialogService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { - super(layoutService, dialogService); + super(layoutService, dialogService, configurationService); } - protected override async resolveWindowId(auxiliaryWindow: NativeAuxiliaryWindow): Promise { + protected override async resolveWindowId(auxiliaryWindow: NativeCodeWindow): Promise { mark('code/auxiliaryWindow/willResolveWindowId'); const windowId = await auxiliaryWindow.vscode.ipcRenderer.invoke('vscode:registerAuxiliaryWindow', this.nativeHostService.windowId); mark('code/auxiliaryWindow/didResolveWindowId'); @@ -39,7 +72,7 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService return windowId; } - protected override createContainer(auxiliaryWindow: NativeAuxiliaryWindow, disposables: DisposableStore): HTMLElement { + protected override createContainer(auxiliaryWindow: NativeCodeWindow, disposables: DisposableStore): HTMLElement { // Zoom level const windowConfig = this.configurationService.getValue(); @@ -49,7 +82,7 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService return super.createContainer(auxiliaryWindow, disposables); } - protected override patchMethods(auxiliaryWindow: NativeAuxiliaryWindow): void { + protected override patchMethods(auxiliaryWindow: NativeCodeWindow): void { super.patchMethods(auxiliaryWindow); // Enable `window.focus()` to work in Electron by @@ -65,6 +98,10 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService } }; } + + protected override createAuxiliaryWindow(targetWindow: CodeWindow, container: HTMLElement): AuxiliaryWindow { + return new NativeAuxiliaryWindow(targetWindow, container, this.configurationService, this.nativeHostService, this.instantiationService); + } } registerSingleton(IAuxiliaryWindowService, NativeAuxiliaryWindowService, InstantiationType.Delayed); diff --git a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts index 7f6a69d73504c..346b4d36cdc09 100644 --- a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts @@ -151,7 +151,6 @@ export class TestNativeHostService implements INativeHostService { async writeClipboardBuffer(format: string, buffer: VSBuffer, type?: 'selection' | 'clipboard' | undefined): Promise { } async readClipboardBuffer(format: string): Promise { return VSBuffer.wrap(Uint8Array.from([])); } async hasClipboard(format: string, type?: 'selection' | 'clipboard' | undefined): Promise { return false; } - async sendInputEvent(event: any): Promise { } async windowsGetStringRegKey(hive: 'HKEY_CURRENT_USER' | 'HKEY_LOCAL_MACHINE' | 'HKEY_CLASSES_ROOT' | 'HKEY_USERS' | 'HKEY_CURRENT_CONFIG', path: string, name: string): Promise { return undefined; } async profileRenderer(): Promise { throw new Error(); } }