-
Notifications
You must be signed in to change notification settings - Fork 2.6k
/
Copy paththeia-electron-window.ts
193 lines (168 loc) · 7.65 KB
/
theia-electron-window.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
// *****************************************************************************
// Copyright (C) 2022 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
// *****************************************************************************
import { FrontendApplicationConfig } from '@theia/application-package';
import { FrontendApplicationState } from '../common/frontend-application-state';
import { APPLICATION_STATE_CHANGE_SIGNAL, CLOSE_REQUESTED_SIGNAL, RELOAD_REQUESTED_SIGNAL, StopReason } from '../electron-common/messaging/electron-messages';
import { BrowserWindow, BrowserWindowConstructorOptions, ipcMain, IpcMainEvent } from '../../electron-shared/electron';
import { inject, injectable, postConstruct } from '../../shared/inversify';
import { ElectronMainApplicationGlobals } from './electron-main-constants';
import { DisposableCollection, Emitter, Event, isWindows } from '../common';
import { createDisposableListener } from './event-utils';
/**
* Theia tracks the maximized state of Electron Browser Windows.
*/
export interface TheiaBrowserWindowOptions extends BrowserWindowConstructorOptions {
isMaximized?: boolean;
isFullScreen?: boolean;
/**
* Represents the complete screen layout for all available displays.
* This field is used to determine if the layout was updated since the electron window was last opened,
* in which case we want to invalidate the stored options and use the default options instead.
*/
screenLayout?: string;
}
export const TheiaBrowserWindowOptions = Symbol('TheiaBrowserWindowOptions');
export const WindowApplicationConfig = Symbol('WindowApplicationConfig');
export type WindowApplicationConfig = FrontendApplicationConfig;
@injectable()
export class TheiaElectronWindow {
@inject(TheiaBrowserWindowOptions) protected readonly options: TheiaBrowserWindowOptions;
@inject(WindowApplicationConfig) protected readonly config: WindowApplicationConfig;
@inject(ElectronMainApplicationGlobals) protected readonly globals: ElectronMainApplicationGlobals;
protected onDidCloseEmitter = new Emitter<void>();
get onDidClose(): Event<void> {
return this.onDidCloseEmitter.event;
}
protected readonly toDispose = new DisposableCollection(this.onDidCloseEmitter);
protected _window: BrowserWindow;
get window(): BrowserWindow {
return this._window;
}
protected closeIsConfirmed = false;
protected applicationState: FrontendApplicationState = 'init';
@postConstruct()
protected init(): void {
this._window = new BrowserWindow(this.options);
this._window.setMenuBarVisibility(false);
this.attachReadyToShow();
this.restoreMaximizedState();
this.attachCloseListeners();
this.trackApplicationState();
this.attachReloadListener();
}
/**
* Only show the window when the content is ready.
*/
protected attachReadyToShow(): void {
this._window.once('ready-to-show', () => this._window.show());
}
protected attachCloseListeners(): void {
createDisposableListener<Electron.Event>(this._window, 'closed', () => {
this.onDidCloseEmitter.fire();
this.dispose();
}, this.toDispose);
createDisposableListener<Electron.Event>(this._window, 'close', async event => {
// User has already indicated that it is OK to close this window, or the window is being closed before it's ready.
if (this.closeIsConfirmed || this.applicationState !== 'ready') {
return;
}
event.preventDefault();
this.handleStopRequest(() => this.doCloseWindow(), StopReason.Close);
}, this.toDispose);
}
protected doCloseWindow(): void {
this.closeIsConfirmed = true;
this._window.close();
}
close(reason: StopReason = StopReason.Close): Promise<boolean> {
return this.handleStopRequest(() => this.doCloseWindow(), reason);
}
protected reload(): void {
this.handleStopRequest(() => {
this.applicationState = 'init';
this._window.reload();
}, StopReason.Reload);
}
protected async handleStopRequest(onSafeCallback: () => unknown, reason: StopReason): Promise<boolean> {
// Only confirm close to windows that have loaded our front end.
let currentUrl = this.window.webContents.getURL();
let frontendUri = this.globals.THEIA_FRONTEND_HTML_PATH;
// Since our resolved frontend HTML path might contain backward slashes on Windows, we normalize everything first.
if (isWindows) {
currentUrl = currentUrl.replace(/\\/g, '/');
frontendUri = frontendUri.replace(/\\/g, '/');
}
const safeToClose = !currentUrl.includes(frontendUri) || await this.checkSafeToStop(reason);
if (safeToClose) {
try {
await onSafeCallback();
return true;
} catch (e) {
console.warn(`Request ${StopReason[reason]} failed.`, e);
}
}
return false;
}
protected checkSafeToStop(reason: StopReason): Promise<boolean> {
const confirmChannel = `safe-to-close-${this._window.id}`;
const cancelChannel = `notSafeToClose-${this._window.id}`;
const temporaryDisposables = new DisposableCollection();
return new Promise<boolean>(resolve => {
this._window.webContents.send(CLOSE_REQUESTED_SIGNAL, { confirmChannel, cancelChannel, reason });
createDisposableListener(ipcMain, confirmChannel, (e: IpcMainEvent) => {
if (this.isSender(e)) {
resolve(true);
}
}, temporaryDisposables);
createDisposableListener(ipcMain, cancelChannel, (e: IpcMainEvent) => {
if (this.isSender(e)) {
resolve(false);
}
}, temporaryDisposables);
}).finally(() => temporaryDisposables.dispose());
}
protected restoreMaximizedState(): void {
if (this.options.isMaximized) {
this._window.maximize();
} else {
this._window.unmaximize();
}
}
protected trackApplicationState(): void {
createDisposableListener(ipcMain, APPLICATION_STATE_CHANGE_SIGNAL, (e: IpcMainEvent, state: FrontendApplicationState) => {
if (this.isSender(e)) {
this.applicationState = state;
}
}, this.toDispose);
}
protected attachReloadListener(): void {
createDisposableListener(ipcMain, RELOAD_REQUESTED_SIGNAL, (e: IpcMainEvent) => {
if (this.isSender(e)) {
this.reload();
}
}, this.toDispose);
}
protected isSender(e: IpcMainEvent): boolean {
return BrowserWindow.fromId(e.sender.id) === this._window;
}
dispose(): void {
this.toDispose.dispose();
}
}
export interface TheiaElectronWindowFactory {
(options: TheiaBrowserWindowOptions, config: FrontendApplicationConfig): TheiaElectronWindow;
}
export const TheiaElectronWindowFactory = Symbol('TheiaElectronWindowFactory');