Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

aux window - support confirm on close #198307

Merged
merged 9 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading