@theia/core 1.21.0-next.17 → 1.21.0-next.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/lib/browser/common-frontend-contribution.d.ts +5 -2
  2. package/lib/browser/common-frontend-contribution.d.ts.map +1 -1
  3. package/lib/browser/common-frontend-contribution.js +21 -4
  4. package/lib/browser/common-frontend-contribution.js.map +1 -1
  5. package/lib/browser/dialogs.d.ts +1 -0
  6. package/lib/browser/dialogs.d.ts.map +1 -1
  7. package/lib/browser/dialogs.js +11 -1
  8. package/lib/browser/dialogs.js.map +1 -1
  9. package/lib/browser/frontend-application.d.ts +15 -2
  10. package/lib/browser/frontend-application.d.ts.map +1 -1
  11. package/lib/browser/frontend-application.js +8 -1
  12. package/lib/browser/frontend-application.js.map +1 -1
  13. package/lib/browser/shell/application-shell-mouse-tracker.js +2 -2
  14. package/lib/browser/shell/application-shell-mouse-tracker.js.map +1 -1
  15. package/lib/browser/shell/application-shell.d.ts +0 -13
  16. package/lib/browser/shell/application-shell.d.ts.map +1 -1
  17. package/lib/browser/shell/application-shell.js +1 -16
  18. package/lib/browser/shell/application-shell.js.map +1 -1
  19. package/lib/browser/shell/shell-layout-restorer.d.ts +2 -0
  20. package/lib/browser/shell/shell-layout-restorer.d.ts.map +1 -1
  21. package/lib/browser/shell/shell-layout-restorer.js +13 -6
  22. package/lib/browser/shell/shell-layout-restorer.js.map +1 -1
  23. package/lib/browser/window/default-window-service.d.ts +19 -2
  24. package/lib/browser/window/default-window-service.d.ts.map +1 -1
  25. package/lib/browser/window/default-window-service.js +67 -12
  26. package/lib/browser/window/default-window-service.js.map +1 -1
  27. package/lib/browser/window/default-window-service.spec.js +3 -3
  28. package/lib/browser/window/default-window-service.spec.js.map +1 -1
  29. package/lib/browser/window/test/mock-window-service.d.ts +3 -1
  30. package/lib/browser/window/test/mock-window-service.d.ts.map +1 -1
  31. package/lib/browser/window/test/mock-window-service.js +3 -1
  32. package/lib/browser/window/test/mock-window-service.js.map +1 -1
  33. package/lib/browser/window/window-service.d.ts +20 -6
  34. package/lib/browser/window/window-service.d.ts.map +1 -1
  35. package/lib/electron-browser/menu/electron-menu-contribution.d.ts +2 -0
  36. package/lib/electron-browser/menu/electron-menu-contribution.d.ts.map +1 -1
  37. package/lib/electron-browser/menu/electron-menu-contribution.js +6 -0
  38. package/lib/electron-browser/menu/electron-menu-contribution.js.map +1 -1
  39. package/lib/electron-browser/window/electron-window-service.d.ts +7 -5
  40. package/lib/electron-browser/window/electron-window-service.d.ts.map +1 -1
  41. package/lib/electron-browser/window/electron-window-service.js +20 -42
  42. package/lib/electron-browser/window/electron-window-service.js.map +1 -1
  43. package/lib/electron-common/messaging/electron-messages.d.ts +27 -0
  44. package/lib/electron-common/messaging/electron-messages.d.ts.map +1 -1
  45. package/lib/electron-common/messaging/electron-messages.js +24 -1
  46. package/lib/electron-common/messaging/electron-messages.js.map +1 -1
  47. package/lib/electron-main/electron-main-application.d.ts +9 -0
  48. package/lib/electron-main/electron-main-application.d.ts.map +1 -1
  49. package/lib/electron-main/electron-main-application.js +57 -14
  50. package/lib/electron-main/electron-main-application.js.map +1 -1
  51. package/package.json +3 -3
  52. package/src/browser/common-frontend-contribution.ts +23 -6
  53. package/src/browser/dialogs.ts +9 -0
  54. package/src/browser/frontend-application.ts +19 -2
  55. package/src/browser/shell/application-shell-mouse-tracker.ts +2 -2
  56. package/src/browser/shell/application-shell.ts +1 -18
  57. package/src/browser/shell/shell-layout-restorer.ts +12 -6
  58. package/src/browser/window/default-window-service.spec.ts +3 -3
  59. package/src/browser/window/default-window-service.ts +69 -13
  60. package/src/browser/window/test/mock-window-service.ts +3 -1
  61. package/src/browser/window/window-service.ts +22 -8
  62. package/src/electron-browser/menu/electron-menu-contribution.ts +6 -1
  63. package/src/electron-browser/window/electron-window-service.ts +21 -41
  64. package/src/electron-common/messaging/electron-messages.ts +29 -0
  65. package/src/electron-main/electron-main-application.ts +74 -15
@@ -18,14 +18,16 @@ import { inject, injectable, named } from 'inversify';
18
18
  import { Event, Emitter } from '../../common';
19
19
  import { CorePreferences } from '../core-preferences';
20
20
  import { ContributionProvider } from '../../common/contribution-provider';
21
- import { FrontendApplicationContribution, FrontendApplication } from '../frontend-application';
21
+ import { FrontendApplicationContribution, FrontendApplication, OnWillStopAction } from '../frontend-application';
22
22
  import { WindowService } from './window-service';
23
23
  import { DEFAULT_WINDOW_HASH } from '../../common/window';
24
+ import { confirmExit } from '../dialogs';
24
25
 
25
26
  @injectable()
26
27
  export class DefaultWindowService implements WindowService, FrontendApplicationContribution {
27
28
 
28
29
  protected frontendApplication: FrontendApplication;
30
+ protected allowVetoes = true;
29
31
 
30
32
  protected onUnloadEmitter = new Emitter<void>();
31
33
  get onUnload(): Event<void> {
@@ -53,26 +55,40 @@ export class DefaultWindowService implements WindowService, FrontendApplicationC
53
55
  this.openNewWindow(`#${DEFAULT_WINDOW_HASH}`);
54
56
  }
55
57
 
56
- canUnload(): boolean {
57
- const confirmExit = this.corePreferences['application.confirmExit'];
58
- let preventUnload = confirmExit === 'always';
59
- for (const contribution of this.contributions.getContributions()) {
60
- if (contribution.onWillStop?.(this.frontendApplication)) {
61
- preventUnload = true;
58
+ /**
59
+ * Returns a list of actions that {@link FrontendApplicationContribution}s would like to take before shutdown
60
+ * It is expected that this will succeed - i.e. return an empty array - at most once per session. If no vetoes are received
61
+ * during any cycle, no further checks will be made. In that case, shutdown should proceed unconditionally.
62
+ */
63
+ protected collectContributionUnloadVetoes(): OnWillStopAction[] {
64
+ const vetoes = [];
65
+ if (this.allowVetoes) {
66
+ const shouldConfirmExit = this.corePreferences['application.confirmExit'];
67
+ for (const contribution of this.contributions.getContributions()) {
68
+ const veto = contribution.onWillStop?.(this.frontendApplication);
69
+ if (veto && shouldConfirmExit !== 'never') { // Ignore vetoes if we should not prompt the user on exit.
70
+ if (OnWillStopAction.is(veto)) {
71
+ vetoes.push(veto);
72
+ } else {
73
+ vetoes.push({ reason: 'No reason given', action: () => false });
74
+ }
75
+ }
76
+ }
77
+ if (vetoes.length === 0 && shouldConfirmExit === 'always') {
78
+ vetoes.push({ reason: 'application.confirmExit preference', action: () => confirmExit() });
79
+ }
80
+ if (vetoes.length === 0) {
81
+ this.allowVetoes = false;
62
82
  }
63
83
  }
64
- return confirmExit === 'never' || !preventUnload;
84
+ return vetoes;
65
85
  }
66
86
 
67
87
  /**
68
88
  * Implement the mechanism to detect unloading of the page.
69
89
  */
70
90
  protected registerUnloadListeners(): void {
71
- window.addEventListener('beforeunload', event => {
72
- if (!this.canUnload()) {
73
- return this.preventUnload(event);
74
- }
75
- });
91
+ window.addEventListener('beforeunload', event => this.handleBeforeUnloadEvent(event));
76
92
  // In a browser, `unload` is correctly fired when the page unloads, unlike Electron.
77
93
  // If `beforeunload` is cancelled, the user will be prompted to leave or stay.
78
94
  // If the user stays, the page won't be unloaded, so `unload` is not fired.
@@ -80,6 +96,43 @@ export class DefaultWindowService implements WindowService, FrontendApplicationC
80
96
  window.addEventListener('unload', () => this.onUnloadEmitter.fire());
81
97
  }
82
98
 
99
+ async isSafeToShutDown(): Promise<boolean> {
100
+ const vetoes = this.collectContributionUnloadVetoes();
101
+ if (vetoes.length === 0) {
102
+ return true;
103
+ }
104
+ console.debug('Shutdown prevented by', vetoes.map(({ reason }) => reason).join(', '));
105
+ const resolvedVetoes = await Promise.allSettled(vetoes.map(({ action }) => action()));
106
+ if (resolvedVetoes.every(resolution => resolution.status === 'rejected' || resolution.value === true)) {
107
+ console.debug('OnWillStop actions resolved; allowing shutdown');
108
+ this.allowVetoes = false;
109
+ return true;
110
+ } else {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ setSafeToShutDown(): void {
116
+ this.allowVetoes = false;
117
+ }
118
+
119
+ /**
120
+ * Called when the `window` is about to `unload` its resources.
121
+ * At this point, the `document` is still visible and the [`BeforeUnloadEvent`](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event)
122
+ * event will be canceled if the return value of this method is `false`.
123
+ *
124
+ * In Electron, handleCloseRequestEvent is is run instead.
125
+ */
126
+ protected handleBeforeUnloadEvent(event: BeforeUnloadEvent): string | void {
127
+ const vetoes = this.collectContributionUnloadVetoes();
128
+ if (vetoes.length) {
129
+ // In the browser, we don't call the functions because this has to finish in a single tick, so we treat any desired action as a veto.
130
+ console.debug('Shutdown prevented by', vetoes.map(({ reason }) => reason).join(', '));
131
+ return this.preventUnload(event);
132
+ }
133
+ console.debug('Shutdown will proceed.');
134
+ }
135
+
83
136
  /**
84
137
  * Notify the browser that we do not want to unload.
85
138
  *
@@ -95,4 +148,7 @@ export class DefaultWindowService implements WindowService, FrontendApplicationC
95
148
  return '';
96
149
  }
97
150
 
151
+ reload(): void {
152
+ window.location.reload();
153
+ }
98
154
  }
@@ -21,6 +21,8 @@ import { WindowService } from '../window-service';
21
21
  export class MockWindowService implements WindowService {
22
22
  openNewWindow(): undefined { return undefined; }
23
23
  openNewDefaultWindow(): void { }
24
- canUnload(): boolean { return true; }
24
+ reload(): void { }
25
+ isSafeToShutDown(): Promise<boolean> { return Promise.resolve(true); }
26
+ setSafeToShutDown(): void { }
25
27
  get onUnload(): Event<void> { return Event.None; }
26
28
  }
@@ -23,7 +23,6 @@ import { NewWindowOptions } from '../../common/window';
23
23
  export const WindowService = Symbol('WindowService');
24
24
 
25
25
  export interface WindowService {
26
-
27
26
  /**
28
27
  * Opens a new window and loads the content from the given URL.
29
28
  * In a browser, opening a new Theia tab or open a link is the same thing.
@@ -37,17 +36,32 @@ export interface WindowService {
37
36
  */
38
37
  openNewDefaultWindow(): void;
39
38
 
40
- /**
41
- * Called when the `window` is about to `unload` its resources.
42
- * At this point, the `document` is still visible and the [`BeforeUnloadEvent`](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event)
43
- * event will be canceled if the return value of this method is `false`.
44
- */
45
- canUnload(): boolean;
46
-
47
39
  /**
48
40
  * Fires when the `window` unloads. The unload event is inevitable. On this event, the frontend application can save its state and release resource.
49
41
  * Saving the state and releasing any resources must be a synchronous call. Any asynchronous calls invoked after emitting this event might be ignored.
50
42
  */
51
43
  readonly onUnload: Event<void>;
52
44
 
45
+ /**
46
+ * Checks `FrontendApplicationContribution#willStop` for impediments to shutdown and runs any actions returned.
47
+ * Can be used safely in browser and Electron when triggering reload or shutdown programmatically.
48
+ * Should _only_ be called before a shutdown - if this returns `true`, `FrontendApplicationContribution#willStop`
49
+ * will not be called again in the current session. I.e. if this return `true`, the shutdown should proceed without
50
+ * further condition.
51
+ */
52
+ isSafeToShutDown(): Promise<boolean>;
53
+
54
+ /**
55
+ * Will prevent subsequent checks of `FrontendApplicationContribution#willStop`. Should only be used after requesting
56
+ * user confirmation.
57
+ *
58
+ * This is primarily intended programmatic restarts due to e.g. change of display language. It allows for a single confirmation
59
+ * of intent, rather than one warning and then several warnings from other contributions.
60
+ */
61
+ setSafeToShutDown(): void;
62
+
63
+ /**
64
+ * Reloads the window according to platform.
65
+ */
66
+ reload(): void;
53
67
  }
@@ -22,7 +22,7 @@ import {
22
22
  } from '../../common';
23
23
  import {
24
24
  ApplicationShell, codicon, ConfirmDialog, KeybindingContribution, KeybindingRegistry,
25
- PreferenceScope, Widget, FrontendApplication, FrontendApplicationContribution, CommonMenus, CommonCommands, Dialog
25
+ PreferenceScope, Widget, FrontendApplication, FrontendApplicationContribution, CommonMenus, CommonCommands, Dialog,
26
26
  } from '../../browser';
27
27
  import { ElectronMainMenuFactory } from './electron-main-menu-factory';
28
28
  import { FrontendApplicationStateService, FrontendApplicationState } from '../../browser/frontend-application-state';
@@ -30,6 +30,7 @@ import { FrontendApplicationConfigProvider } from '../../browser/frontend-applic
30
30
  import { RequestTitleBarStyle, Restart, TitleBarStyleAtStartup, TitleBarStyleChanged } from '../../electron-common/messaging/electron-messages';
31
31
  import { ZoomLevel } from '../window/electron-window-preferences';
32
32
  import { BrowserMenuBarContribution } from '../../browser/menu/browser-menu-plugin';
33
+ import { WindowService } from '../../browser/window/window-service';
33
34
 
34
35
  import '../../../src/electron-browser/menu/electron-menu-style.css';
35
36
 
@@ -84,6 +85,9 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme
84
85
  @inject(FrontendApplicationStateService)
85
86
  protected readonly stateService: FrontendApplicationStateService;
86
87
 
88
+ @inject(WindowService)
89
+ protected readonly windowService: WindowService;
90
+
87
91
  protected titleBarStyleChangeFlag = false;
88
92
  protected titleBarStyle?: string;
89
93
 
@@ -241,6 +245,7 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme
241
245
  cancel: Dialog.CANCEL
242
246
  });
243
247
  if (await dialog.open()) {
248
+ this.windowService.setSafeToShutDown();
244
249
  electron.ipcRenderer.send(Restart);
245
250
  }
246
251
  }
@@ -15,11 +15,12 @@
15
15
  ********************************************************************************/
16
16
 
17
17
  import { injectable, inject, postConstruct } from 'inversify';
18
- import { remote } from '../../../shared/electron';
18
+ import * as electron from '../../../shared/electron';
19
19
  import { NewWindowOptions } from '../../common/window';
20
20
  import { DefaultWindowService } from '../../browser/window/default-window-service';
21
21
  import { ElectronMainWindowService } from '../../electron-common/electron-main-window-service';
22
22
  import { ElectronWindowPreferences } from './electron-window-preferences';
23
+ import { CloseRequestArguments, CLOSE_REQUESTED_SIGNAL, RELOAD_REQUESTED_SIGNAL, StopReason } from '../../electron-common/messaging/electron-messages';
23
24
 
24
25
  @injectable()
25
26
  export class ElectronWindowService extends DefaultWindowService {
@@ -59,49 +60,24 @@ export class ElectronWindowService extends DefaultWindowService {
59
60
  });
60
61
  }
61
62
 
62
- registerUnloadListeners(): void {
63
- window.addEventListener('beforeunload', event => {
64
- if (this.isUnloading) {
65
- // Unloading process ongoing, do nothing:
66
- return this.preventUnload(event);
67
- } else if (this.closeOnUnload || this.canUnload()) {
68
- // Let the window close and notify clients:
69
- delete event.returnValue;
70
- this.onUnloadEmitter.fire();
71
- return;
72
- } else {
73
- this.isUnloading = true;
74
- // Fix https://github.com/eclipse-theia/theia/issues/8186#issuecomment-742624480
75
- // On Electron/Linux doing `showMessageBoxSync` does not seems to block the closing
76
- // process long enough and closes the window no matter what you click on (yes/no).
77
- // Instead we'll prevent closing right away, ask for confirmation and finally close.
78
- setTimeout(() => {
79
- if (this.shouldUnload()) {
80
- this.closeOnUnload = true;
81
- window.close();
82
- }
83
- this.isUnloading = false;
84
- });
85
- return this.preventUnload(event);
86
- }
87
- });
63
+ protected registerUnloadListeners(): void {
64
+ electron.ipcRenderer.on(CLOSE_REQUESTED_SIGNAL, (_event, closeRequestEvent: CloseRequestArguments) => this.handleCloseRequestedEvent(closeRequestEvent));
65
+ window.addEventListener('unload', () => this.onUnloadEmitter.fire());
88
66
  }
89
67
 
90
68
  /**
91
- * When preventing `beforeunload` on Electron, no popup is shown.
92
- *
93
- * This method implements a modal to ask the user if he wants to quit the page.
69
+ * Run when ElectronMain detects a `close` event and emits a `close-requested` event.
70
+ * Should send an event to `electron.ipcRenderer` on the event's `confirmChannel` if it is safe to exit
71
+ * after running FrontentApplication `onWillStop` handlers or on the `cancelChannel` if it is not safe to exit.
94
72
  */
95
- protected shouldUnload(): boolean {
96
- const electronWindow = remote.getCurrentWindow();
97
- const response = remote.dialog.showMessageBoxSync(electronWindow, {
98
- type: 'question',
99
- buttons: ['Yes', 'No'],
100
- title: 'Confirm',
101
- message: 'Are you sure you want to quit?',
102
- detail: 'Any unsaved changes will not be saved.'
103
- });
104
- return response === 0; // 'Yes', close the window.
73
+ protected async handleCloseRequestedEvent(event: CloseRequestArguments): Promise<void> {
74
+ const safeToClose = await this.isSafeToShutDown();
75
+ if (safeToClose) {
76
+ console.debug(`Shutting down because of ${StopReason[event.reason]} request.`);
77
+ electron.ipcRenderer.send(event.confirmChannel);
78
+ } else {
79
+ electron.ipcRenderer.send(event.cancelChannel);
80
+ }
105
81
  }
106
82
 
107
83
  /**
@@ -109,9 +85,13 @@ export class ElectronWindowService extends DefaultWindowService {
109
85
  */
110
86
  protected updateWindowZoomLevel(): void {
111
87
  const preferredZoomLevel = this.electronWindowPreferences['window.zoomLevel'];
112
- const webContents = remote.getCurrentWindow().webContents;
88
+ const webContents = electron.remote.getCurrentWindow().webContents;
113
89
  if (webContents.getZoomLevel() !== preferredZoomLevel) {
114
90
  webContents.setZoomLevel(preferredZoomLevel);
115
91
  }
116
92
  }
93
+
94
+ reload(): void {
95
+ electron.ipcRenderer.send(RELOAD_REQUESTED_SIGNAL);
96
+ }
117
97
  }
@@ -18,3 +18,32 @@ export const RequestTitleBarStyle = 'requestTitleBarStyle';
18
18
  export const TitleBarStyleChanged = 'titleBarStyleChanged';
19
19
  export const TitleBarStyleAtStartup = 'titleBarStyleAtStartup';
20
20
  export const Restart = 'restart';
21
+ /**
22
+ * Emitted by main when close requested.
23
+ */
24
+ export const CLOSE_REQUESTED_SIGNAL = 'close-requested';
25
+ /**
26
+ * Emitted by window when a reload is requested.
27
+ */
28
+ export const RELOAD_REQUESTED_SIGNAL = 'reload-requested';
29
+
30
+ export enum StopReason {
31
+ /**
32
+ * Closing the window with no prospect of restart.
33
+ */
34
+ Close,
35
+ /**
36
+ * Reload without closing the window.
37
+ */
38
+ Reload,
39
+ /**
40
+ * Reload that includes closing the window.
41
+ */
42
+ Restart, // eslint-disable-line @typescript-eslint/no-shadow
43
+ }
44
+
45
+ export interface CloseRequestArguments {
46
+ confirmChannel: string;
47
+ cancelChannel: string;
48
+ reason: StopReason;
49
+ }
@@ -31,7 +31,14 @@ import { ElectronSecurityTokenService } from './electron-security-token-service'
31
31
  import { ElectronSecurityToken } from '../electron-common/electron-token';
32
32
  import Storage = require('electron-store');
33
33
  import { isOSX, isWindows } from '../common';
34
- import { RequestTitleBarStyle, Restart, TitleBarStyleAtStartup, TitleBarStyleChanged } from '../electron-common/messaging/electron-messages';
34
+ import {
35
+ CLOSE_REQUESTED_SIGNAL,
36
+ RELOAD_REQUESTED_SIGNAL,
37
+ RequestTitleBarStyle,
38
+ Restart, StopReason,
39
+ TitleBarStyleAtStartup,
40
+ TitleBarStyleChanged
41
+ } from '../electron-common/messaging/electron-messages';
35
42
  import { DEFAULT_WINDOW_HASH } from '../common/window';
36
43
 
37
44
  const createYargs: (argv?: string[], cwd?: string) => Argv = require('yargs/yargs');
@@ -192,6 +199,8 @@ export class ElectronMainApplication {
192
199
  protected useNativeWindowFrame: boolean = true;
193
200
  protected didUseNativeWindowFrameOnStart = new Map<number, boolean>();
194
201
  protected restarting = false;
202
+ protected closeIsConfirmed = new Set<number>();
203
+ protected closeRequested = 0;
195
204
 
196
205
  get config(): FrontendApplicationConfig {
197
206
  if (!this._config) {
@@ -256,6 +265,7 @@ export class ElectronMainApplication {
256
265
  this.attachSaveWindowState(electronWindow);
257
266
  this.attachGlobalShortcuts(electronWindow);
258
267
  this.restoreMaximizedState(electronWindow, options);
268
+ this.attachCloseListeners(electronWindow, options);
259
269
  return electronWindow;
260
270
  }
261
271
 
@@ -433,19 +443,20 @@ export class ElectronMainApplication {
433
443
  * Catch certain keybindings to prevent reloading the window using keyboard shortcuts.
434
444
  */
435
445
  protected attachGlobalShortcuts(electronWindow: BrowserWindow): void {
436
- if (this.config.electron?.disallowReloadKeybinding) {
437
- const accelerators = ['CmdOrCtrl+R', 'F5'];
438
- electronWindow.on('focus', () => {
439
- for (const accelerator of accelerators) {
440
- globalShortcut.register(accelerator, () => { });
441
- }
442
- });
443
- electronWindow.on('blur', () => {
444
- for (const accelerator of accelerators) {
445
- globalShortcut.unregister(accelerator);
446
- }
447
- });
448
- }
446
+ const handler = this.config.electron?.disallowReloadKeybinding
447
+ ? () => { }
448
+ : () => this.reload(electronWindow);
449
+ const accelerators = ['CmdOrCtrl+R', 'F5'];
450
+ electronWindow.on('focus', () => {
451
+ for (const accelerator of accelerators) {
452
+ globalShortcut.register(accelerator, handler);
453
+ }
454
+ });
455
+ electronWindow.on('blur', () => {
456
+ for (const accelerator of accelerators) {
457
+ globalShortcut.unregister(accelerator);
458
+ }
459
+ });
449
460
  }
450
461
 
451
462
  protected restoreMaximizedState(electronWindow: BrowserWindow, options: TheiaBrowserWindowOptions): void {
@@ -456,6 +467,43 @@ export class ElectronMainApplication {
456
467
  }
457
468
  }
458
469
 
470
+ protected attachCloseListeners(electronWindow: BrowserWindow, options: TheiaBrowserWindowOptions): void {
471
+ electronWindow.on('close', async event => {
472
+ // User has already indicated that it is OK to close this window.
473
+ if (this.closeIsConfirmed.has(electronWindow.id)) {
474
+ this.closeIsConfirmed.delete(electronWindow.id);
475
+ return;
476
+ }
477
+
478
+ event.preventDefault();
479
+ this.handleStopRequest(electronWindow, () => this.doCloseWindow(electronWindow), StopReason.Close);
480
+ });
481
+ }
482
+
483
+ protected doCloseWindow(electronWindow: BrowserWindow): void {
484
+ this.closeIsConfirmed.add(electronWindow.id);
485
+ electronWindow.close();
486
+ }
487
+
488
+ protected async handleStopRequest(electronWindow: BrowserWindow, onSafeCallback: () => unknown, reason: StopReason): Promise<void> {
489
+ // Only confirm close to windows that have loaded our front end.
490
+ const safeToClose = !electronWindow.webContents.getURL().includes(this.globals.THEIA_FRONTEND_HTML_PATH) || await this.checkSafeToStop(electronWindow, reason);
491
+ if (safeToClose) {
492
+ onSafeCallback();
493
+ }
494
+ }
495
+
496
+ protected checkSafeToStop(electronWindow: BrowserWindow, reason: StopReason): Promise<boolean> {
497
+ const closeRequest = this.closeRequested++;
498
+ const confirmChannel = `safeToClose-${electronWindow.id}-${closeRequest}`;
499
+ const cancelChannel = `notSafeToClose-${electronWindow.id}-${closeRequest}`;
500
+ return new Promise<boolean>(resolve => {
501
+ electronWindow.webContents.send(CLOSE_REQUESTED_SIGNAL, { confirmChannel, cancelChannel, reason });
502
+ ipcMain.once(confirmChannel, () => resolve(true));
503
+ ipcMain.once(cancelChannel, () => resolve(false));
504
+ });
505
+ }
506
+
459
507
  /**
460
508
  * Start the NodeJS backend server.
461
509
  *
@@ -541,6 +589,8 @@ export class ElectronMainApplication {
541
589
  this.restart(sender.id);
542
590
  });
543
591
 
592
+ ipcMain.on(RELOAD_REQUESTED_SIGNAL, event => this.handleReload(event));
593
+
544
594
  ipcMain.on(RequestTitleBarStyle, ({ sender }) => {
545
595
  sender.send(TitleBarStyleAtStartup, this.didUseNativeWindowFrameOnStart.get(sender.id) ? 'native' : 'custom');
546
596
  });
@@ -578,7 +628,16 @@ export class ElectronMainApplication {
578
628
  });
579
629
  this.restarting = false;
580
630
  });
581
- window.close();
631
+ this.handleStopRequest(window, () => this.doCloseWindow(window), StopReason.Restart);
632
+ }
633
+
634
+ protected async handleReload(event: Electron.IpcMainEvent): Promise<void> {
635
+ const window = BrowserWindow.fromId(event.sender.id);
636
+ this.reload(window);
637
+ }
638
+
639
+ protected reload(electronWindow: BrowserWindow): void {
640
+ this.handleStopRequest(electronWindow, () => electronWindow.reload(), StopReason.Reload);
582
641
  }
583
642
 
584
643
  protected async startContributions(): Promise<void> {