Skip to content

Commit

Permalink
aux window - support confirm on close (#198307)
Browse files Browse the repository at this point in the history
  • Loading branch information
bpasero authored Nov 16, 2023
1 parent 7688a7c commit ab66bab
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 49 deletions.
13 changes: 2 additions & 11 deletions src/vs/platform/layout/browser/layoutService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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;

Expand Down
3 changes: 1 addition & 2 deletions src/vs/platform/native/common/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -175,7 +175,6 @@ export interface ICommonNativeHostService {
// Development
openDevTools(options?: OpenDevToolsOptions): Promise<void>;
toggleDevTools(): Promise<void>;
sendInputEvent(event: MouseInputEvent): Promise<void>;

// Perf Introspection
profileRenderer(session: string, duration: number): Promise<IV8Profile>;
Expand Down
20 changes: 7 additions & 13 deletions src/vs/platform/native/electron-main/nativeHostMainService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -209,7 +208,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
window?.handleTitleDoubleClick();
}

async getCursorScreenPoint(firstArg: number | undefined): Promise<IPoint> {
async getCursorScreenPoint(windowId: number | undefined): Promise<IPoint> {
return screen.getCursorScreenPoint();
}

Expand Down Expand Up @@ -250,8 +249,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
}
}

async positionWindow(firstArg: number | undefined, position: IRectangle, options?: INativeOptions): Promise<void> {
const window = this.windowById(options?.targetWindowId) ?? this.codeWindowById(firstArg);
async positionWindow(windowId: number | undefined, position: IRectangle, options?: INativeOptions): Promise<void> {
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')));
Expand Down Expand Up @@ -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<MessageBoxReturnValue> {
Expand Down Expand Up @@ -590,13 +591,13 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
return virtualMachineHint.value();
}

public async getOSColorScheme(): Promise<IColorScheme> {
async getOSColorScheme(): Promise<IColorScheme> {
return this.themeMainService.getColorScheme();
}


// WSL
public async hasWSLFeatureInstalled(): Promise<boolean> {
async hasWSLFeatureInstalled(): Promise<boolean> {
return isWindows && hasWSLFeatureInstalled();
}

Expand Down Expand Up @@ -786,13 +787,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
window?.win?.webContents.toggleDevTools();
}

async sendInputEvent(windowId: number | undefined, event: MouseInputEvent): Promise<void> {
const window = this.codeWindowById(windowId);
if (window?.win && (event.type === 'mouseDown' || event.type === 'mouseUp')) {
window.win.webContents.sendInputEvent(event);
}
}

//#endregion

// #region Performance
Expand Down
6 changes: 3 additions & 3 deletions src/vs/workbench/browser/parts/editor/editorPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1390,8 +1390,8 @@ export class AuxiliaryEditorPart extends EditorPart implements IAuxiliaryEditorP

private static COUNTER = 1;

private readonly _onDidClose = this._register(new Emitter<void>());
readonly onDidClose = this._onDidClose.event;
private readonly _onWillClose = this._register(new Emitter<void>());
readonly onWillClose = this._onWillClose.event;

constructor(
editorPartsView: IEditorPartsView,
Expand Down Expand Up @@ -1435,6 +1435,6 @@ export class AuxiliaryEditorPart extends EditorPart implements IAuxiliaryEditorP
this.mergeAllGroups(this.editorPartsView.mainPart.activeGroup);
}

this._onDidClose.fire();
this._onWillClose.fire();
}
}
4 changes: 3 additions & 1 deletion src/vs/workbench/browser/parts/editor/editorParts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/browser/workbench.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,7 @@ const registry = Registry.as<IConfigurationRegistry>(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
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/electron-sandbox/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<IAuxiliaryWindowService>('auxiliaryWindowService');

Expand Down Expand Up @@ -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<Dimension>());
readonly onDidLayout = this._onDidLayout.event;
Expand All @@ -63,16 +64,19 @@ class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow {
private readonly _onWillDispose = this._register(new Emitter<void>());
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);
Expand All @@ -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));
}
Expand Down Expand Up @@ -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();
}
Expand All @@ -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);
Expand All @@ -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<Window | undefined> {
const activeWindow = getActiveWindow();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -15,31 +15,64 @@ 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<void> {
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<number> {
protected override async resolveWindowId(auxiliaryWindow: NativeCodeWindow): Promise<number> {
mark('code/auxiliaryWindow/willResolveWindowId');
const windowId = await auxiliaryWindow.vscode.ipcRenderer.invoke('vscode:registerAuxiliaryWindow', this.nativeHostService.windowId);
mark('code/auxiliaryWindow/didResolveWindowId');

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<IWindowsConfiguration>();
Expand All @@ -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
Expand All @@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ export class TestNativeHostService implements INativeHostService {
async writeClipboardBuffer(format: string, buffer: VSBuffer, type?: 'selection' | 'clipboard' | undefined): Promise<void> { }
async readClipboardBuffer(format: string): Promise<VSBuffer> { return VSBuffer.wrap(Uint8Array.from([])); }
async hasClipboard(format: string, type?: 'selection' | 'clipboard' | undefined): Promise<boolean> { return false; }
async sendInputEvent(event: any): Promise<void> { }
async windowsGetStringRegKey(hive: 'HKEY_CURRENT_USER' | 'HKEY_LOCAL_MACHINE' | 'HKEY_CLASSES_ROOT' | 'HKEY_USERS' | 'HKEY_CURRENT_CONFIG', path: string, name: string): Promise<string | undefined> { return undefined; }
async profileRenderer(): Promise<any> { throw new Error(); }
}
Expand Down

0 comments on commit ab66bab

Please sign in to comment.