diff --git a/CHANGELOG.md b/CHANGELOG.md
index c46b143eece8e..4716142a42989 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
[Breaking Changes:](#breaking_changes_1.8.0)
- [file-search] Deprecate dependency on `@theia/process` and replaced its usage by node's `child_process` api.
+- [electron] Removed `attachWillPreventUnload` method from the Electron main application. The `confirmExit` logic is handled on the frontend. [#8732](https://github.com/eclipse-theia/theia/pull/8732)
## v1.7.0 - 29/10/2020
diff --git a/packages/core/src/browser/frontend-application.ts b/packages/core/src/browser/frontend-application.ts
index 25997c737a692..1d7c6a1ae870b 100644
--- a/packages/core/src/browser/frontend-application.ts
+++ b/packages/core/src/browser/frontend-application.ts
@@ -24,6 +24,7 @@ import { ShellLayoutRestorer, ApplicationShellLayoutMigrationError } from './she
import { FrontendApplicationStateService } from './frontend-application-state';
import { preventNavigation, parseCssTime, animationFrame } from './browser';
import { CorePreferences } from './core-preferences';
+import { WindowService } from './window/window-service';
/**
* Clients can implement to get a callback for contributing widgets to a shell on start.
@@ -96,6 +97,9 @@ export class FrontendApplication {
@inject(CorePreferences)
protected readonly corePreferences: CorePreferences;
+ @inject(WindowService)
+ protected readonly windowsService: WindowService;
+
constructor(
@inject(CommandRegistry) protected readonly commands: CommandRegistry,
@inject(MenuModelRegistry) protected readonly menus: MenuModelRegistry,
@@ -182,8 +186,7 @@ export class FrontendApplication {
*/
protected registerEventListeners(): void {
this.registerCompositionEventListeners(); /* Hotfix. See above. */
-
- window.addEventListener('beforeunload', () => {
+ this.windowsService.onUnload(() => {
this.stateService.state = 'closing_window';
this.layoutRestorer.storeLayout(this);
this.stopContributions();
diff --git a/packages/core/src/browser/window/default-window-service.ts b/packages/core/src/browser/window/default-window-service.ts
index a2948fb7a351d..bf8ce32e22a0d 100644
--- a/packages/core/src/browser/window/default-window-service.ts
+++ b/packages/core/src/browser/window/default-window-service.ts
@@ -15,6 +15,7 @@
********************************************************************************/
import { inject, injectable, named } from 'inversify';
+import { Event, Emitter } from '../../common';
import { CorePreferences } from '../core-preferences';
import { ContributionProvider } from '../../common/contribution-provider';
import { FrontendApplicationContribution, FrontendApplication } from '../frontend-application';
@@ -23,6 +24,9 @@ import { WindowService } from './window-service';
@injectable()
export class DefaultWindowService implements WindowService, FrontendApplicationContribution {
+ private didFireUnload = false;
+ private onUnloadEmitter = new Emitter();
+
protected frontendApplication: FrontendApplication;
@inject(CorePreferences)
@@ -38,7 +42,9 @@ export class DefaultWindowService implements WindowService, FrontendApplicationC
if (!this.canUnload()) {
return this.preventUnload(event);
}
+ this.onUnloadEmitter.fire();
});
+ this.registerUnloadListener();
}
openNewWindow(url: string): undefined {
@@ -61,6 +67,21 @@ export class DefaultWindowService implements WindowService, FrontendApplicationC
return confirmExit !== 'always';
}
+ protected registerUnloadListener(): void {
+ window.addEventListener('unload', this.fireUnload.bind(this));
+ }
+
+ protected fireUnload(): void {
+ if (!this.didFireUnload) {
+ this.didFireUnload = true;
+ this.onUnloadEmitter.fire();
+ }
+ }
+
+ get onUnload(): Event {
+ return this.onUnloadEmitter.event;
+ }
+
/**
* Ask the user to confirm if they want to unload the window. Prevent it if they do not.
* @param event The beforeunload event
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 a0662fcfde2a1..a3cca33908fa3 100644
--- a/packages/core/src/browser/window/test/mock-window-service.ts
+++ b/packages/core/src/browser/window/test/mock-window-service.ts
@@ -14,10 +14,12 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { injectable } from 'inversify';
+import { Event } from '../../../common/event';
import { WindowService } from '../window-service';
@injectable()
export class MockWindowService implements WindowService {
openNewWindow(): undefined { return undefined; }
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 1300b5af249c0..cec3b1e9a25f7 100644
--- a/packages/core/src/browser/window/window-service.ts
+++ b/packages/core/src/browser/window/window-service.ts
@@ -14,6 +14,8 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
+import { Event } from '../../common/event';
+
export interface NewWindowOptions {
readonly external?: boolean;
}
@@ -38,4 +40,10 @@ export interface WindowService {
*/
canUnload(): boolean;
+ /**
+ * Fires when the `window` unloads. The unload event is inevitable. On this event, the frontend application can save its state and release resource.
+ * Saving the state and releasing any resources must be a synchronous call. Any asynchronous calls invoked after emitting this event might be ignored.
+ */
+ readonly onUnload: 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 0bbda274987b9..c5a42a37cda46 100644
--- a/packages/core/src/electron-browser/window/electron-window-service.ts
+++ b/packages/core/src/electron-browser/window/electron-window-service.ts
@@ -15,6 +15,7 @@
********************************************************************************/
import { injectable, inject } from 'inversify';
+import { remote } from 'electron';
import { NewWindowOptions } from '../../browser/window/window-service';
import { DefaultWindowService } from '../../browser/window/default-window-service';
import { ElectronMainWindowService } from '../../electron-common/electron-main-window-service';
@@ -30,9 +31,28 @@ export class ElectronWindowService extends DefaultWindowService {
return undefined;
}
+ registerUnloadListener(): void {
+ // NOOP. The unload logic is handled in the `preventUnload` when running the app in electron env.
+ }
+
protected preventUnload(event: BeforeUnloadEvent): string | void {
- // The user will be shown a confirmation dialog by the will-prevent-unload handler in the Electron main script
- event.returnValue = false;
+ const electronWindow = remote.getCurrentWindow();
+ const response = remote.dialog.showMessageBoxSync(electronWindow, {
+ type: 'question',
+ buttons: ['Yes', 'No'],
+ title: 'Confirm',
+ message: 'Are you sure you want to quit?',
+ detail: 'Any unsaved changes will not be saved.'
+ });
+ if (response === 0) { // 'Yes', close the window.
+ this.fireUnload();
+ // The absence of a `returnValue` property on the event will guarantee the browser `unload` happens.
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload
+ delete event.returnValue;
+ } else {
+ event.preventDefault();
+ event.returnValue = true;
+ }
}
}
diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts
index e1a7194599c5c..dc7304c35c8e8 100644
--- a/packages/core/src/electron-main/electron-main-application.ts
+++ b/packages/core/src/electron-main/electron-main-application.ts
@@ -15,7 +15,7 @@
********************************************************************************/
import { inject, injectable, named } from 'inversify';
-import { session, screen, globalShortcut, app, BrowserWindow, BrowserWindowConstructorOptions, Event as ElectronEvent, dialog } from 'electron';
+import { session, screen, globalShortcut, app, BrowserWindow, BrowserWindowConstructorOptions, Event as ElectronEvent } from 'electron';
import * as path from 'path';
import { Argv } from 'yargs';
import { AddressInfo } from 'net';
@@ -213,7 +213,6 @@ export class ElectronMainApplication {
const electronWindow = new BrowserWindow(options);
this.attachReadyToShow(electronWindow);
this.attachSaveWindowState(electronWindow);
- this.attachWillPreventUnload(electronWindow);
this.attachGlobalShortcuts(electronWindow);
this.restoreMaximizedState(electronWindow, options);
return electronWindow;
@@ -339,26 +338,6 @@ export class ElectronMainApplication {
electronWindow.on('move', saveWindowStateDelayed);
}
- /**
- * Catch window closing event and display a confirmation window.
- */
- protected attachWillPreventUnload(electronWindow: BrowserWindow): void {
- // Fired when a beforeunload handler tries to prevent the page unloading
- electronWindow.webContents.on('will-prevent-unload', async event => {
- const { response } = await dialog.showMessageBox(electronWindow, {
- type: 'question',
- buttons: ['Yes', 'No'],
- title: 'Confirm',
- message: 'Are you sure you want to quit?',
- detail: 'Any unsaved changes will not be saved.'
- });
- if (response === 0) { // 'Yes'
- // This ignores the beforeunload callback, allowing the page to unload
- event.preventDefault();
- }
- });
- }
-
/**
* Catch certain keybindings to prevent reloading the window using keyboard shortcuts.
*/