Skip to content

Commit f5ce793

Browse files
committed
feat: splash screen support for Electron
Enhances the ElectronMainApplication to optionally render a splash screen until the frontend is ready. The splash screen can be configured via the application config object "theia.frontend.config.electron.showWindowEarly". Mandatory is the option "content" which specifies a relative path from the frontend location to the content of the splash screen. Optionally "width", "height", "minDuration" and "maxDuration" can be handed over too. Configures the Electron example application to show a Theia logo splash screen. Implements #13410 Contributed on behalf of Pragmatiqu IT GmbH
1 parent 8ea1846 commit f5ce793

File tree

6 files changed

+198
-17
lines changed

6 files changed

+198
-17
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
## not yet released
88

99
- [application-package] bumped the default supported API from `1.86.2` to `1.87.2` [#13514](https://github.com/eclipse-theia/theia/pull/13514) - contributed on behalf of STMicroelectronics
10-
- [core] Fix quickpick problems found in IDE testing [#13451](https://github.com/eclipse-theia/theia/pull/13451) - contributed on behalf of STMicroelectronics
10+
- [core] Fix quickpick problems found in IDE testing [#13451](https://github.com/eclipse-theia/theia/pull/13451) - contributed on behalf of STMicroelectronics
11+
- [core] Splash Screen Support for Electron [#13505](https://github.com/eclipse-theia/theia/pull/13505) - contributed on behalf of Pragmatiqu IT GmbH
1112
- [plugin] Extend TextEditorLineNumbersStyle with Interval [#13458](https://github.com/eclipse-theia/theia/pull/13458) - contributed on behalf of STMicroelectronics
1213

1314
<a name="breaking_changes_not_yet_released">[Breaking Changes:](#breaking_changes_not_yet_released)</a>

dev-packages/application-package/src/application-props.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,33 @@ export namespace ElectronFrontendApplicationConfig {
3535
windowOptions: {},
3636
showWindowEarly: true
3737
};
38+
export interface SplashScreenOptions {
39+
/**
40+
* Initial width of the splash screen. Defaults to 640.
41+
*/
42+
width?: number;
43+
/**
44+
* Initial height of the splash screen. Defaults to 480.
45+
*/
46+
height?: number;
47+
/**
48+
* Minimum amount of time in milliseconds to show the splash screen before main window is shown.
49+
* Defaults to 0, i.e. the splash screen will be shown until the frontend application is ready.
50+
*/
51+
minDuration?: number;
52+
/**
53+
* Maximum amount of time in milliseconds before splash screen is removed and main window is shown.
54+
* Defaults to 60000.
55+
*/
56+
maxDuration?: number;
57+
/**
58+
* The content to load in the splash screen.
59+
* Will be resolved from application root.
60+
*
61+
* Mandatory attribute.
62+
*/
63+
content?: string;
64+
}
3865
export interface Partial {
3966

4067
/**
@@ -45,11 +72,11 @@ export namespace ElectronFrontendApplicationConfig {
4572
readonly windowOptions?: BrowserWindowConstructorOptions;
4673

4774
/**
48-
* Whether or not to show an empty Electron window as early as possible.
49-
*
50-
* Defaults to `true`.
75+
* Whether or not to show an empty Electron main window as early as possible.
76+
* Alternatively a splash screen can be configured which is shown until the
77+
* frontend is ready.
5178
*/
52-
readonly showWindowEarly?: boolean;
79+
readonly showWindowEarly?: boolean | SplashScreenOptions;
5380
}
5481
}
5582

examples/electron/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010
"frontend": {
1111
"config": {
1212
"applicationName": "Theia Electron Example",
13-
"reloadOnReconnect": true
13+
"reloadOnReconnect": true,
14+
"electron": {
15+
"showWindowEarly": {
16+
"content": "resources/theia-logo.svg",
17+
"height": 90
18+
}
19+
}
1420
}
1521
},
1622
"backend": {
Lines changed: 32 additions & 0 deletions
Loading

packages/core/src/electron-main/electron-main-application.ts

Lines changed: 117 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { AddressInfo } from 'net';
2222
import { promises as fs } from 'fs';
2323
import { existsSync, mkdirSync } from 'fs-extra';
2424
import { fork, ForkOptions } from 'child_process';
25-
import { DefaultTheme, FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
25+
import { DefaultTheme, ElectronFrontendApplicationConfig, FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
2626
import URI from '../common/uri';
2727
import { FileUri } from '../common/file-uri';
2828
import { Deferred } from '../common/promise-util';
@@ -136,6 +136,16 @@ export class ElectronMainProcessArgv {
136136

137137
}
138138

139+
interface SplashScreenState {
140+
splashScreenWindow?: BrowserWindow;
141+
minTime: Promise<void>;
142+
maxTime: Promise<void>;
143+
}
144+
145+
interface SplashScreenOptions extends ElectronFrontendApplicationConfig.SplashScreenOptions {
146+
content: string;
147+
}
148+
139149
export namespace ElectronMainProcessArgv {
140150
export interface ElectronMainProcess extends NodeJS.Process {
141151
readonly defaultApp: boolean;
@@ -184,6 +194,8 @@ export class ElectronMainApplication {
184194

185195
protected initialWindow?: BrowserWindow;
186196

197+
protected splashScreenState?: SplashScreenState;
198+
187199
get config(): FrontendApplicationConfig {
188200
if (!this._config) {
189201
throw new Error('You have to start the application first.');
@@ -224,6 +236,7 @@ export class ElectronMainApplication {
224236
this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native';
225237
this._config = config;
226238
this.hookApplicationEvents();
239+
this.showSplashScreen();
227240
this.showInitialWindow();
228241
const port = await this.startBackend();
229242
this._backendPort.resolve(port);
@@ -287,18 +300,87 @@ export class ElectronMainApplication {
287300
return this.didUseNativeWindowFrameOnStart.get(webContents.id) ? 'native' : 'custom';
288301
}
289302

303+
/**
304+
* Shows the splash screen, if it was configured. Otherwise does nothing.
305+
*/
306+
protected showSplashScreen(): void {
307+
if (this.isShowSplashScreen()) {
308+
console.log('Showing splash screen');
309+
const splashScreenOptions = this.getSplashScreenOptions();
310+
if (!splashScreenOptions) {
311+
// sanity check, should always exist here
312+
return;
313+
}
314+
const content = splashScreenOptions.content;
315+
console.debug('SplashScreen options', splashScreenOptions);
316+
app.whenReady().then(() => {
317+
const splashScreenBounds = this.determineSplashScreenBounds();
318+
const splashScreenWindow = new BrowserWindow({
319+
...splashScreenBounds,
320+
frame: false,
321+
alwaysOnTop: true,
322+
});
323+
splashScreenWindow.show();
324+
splashScreenWindow.loadFile(path.resolve(this.globals.THEIA_APP_PROJECT_PATH, content).toString());
325+
this.splashScreenState = {
326+
splashScreenWindow,
327+
minTime: new Promise(resolve => setTimeout(() => resolve(), splashScreenOptions.minDuration ?? 0)),
328+
maxTime: new Promise(resolve => setTimeout(() => resolve(), splashScreenOptions.maxDuration ?? 60000)),
329+
};
330+
});
331+
}
332+
}
333+
334+
protected determineSplashScreenBounds(): { x: number, y: number, width: number, height: number } {
335+
const splashScreenOptions = this.getSplashScreenOptions();
336+
const width = splashScreenOptions?.width ?? 640;
337+
const height = splashScreenOptions?.height ?? 480;
338+
339+
// determine the bounds of the screen on which Theia will be shown
340+
const lastWindowOptions = this.getLastWindowOptions();
341+
const defaultWindowBounds = this.getDefaultTheiaWindowBounds();
342+
const theiaPoint = typeof lastWindowOptions.x === 'number' && typeof lastWindowOptions.y === 'number' ?
343+
{ x: lastWindowOptions.x, y: lastWindowOptions.y } :
344+
{ x: defaultWindowBounds.x!, y: defaultWindowBounds.y! };
345+
const { bounds } = screen.getDisplayNearestPoint(theiaPoint);
346+
347+
// place splash screen center of screen
348+
const middlePoint = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
349+
const x = middlePoint.x - width / 2;
350+
const y = middlePoint.y - height / 2;
351+
352+
return {
353+
x, y, width, height
354+
};
355+
}
356+
290357
protected showInitialWindow(): void {
291-
if (this.config.electron.showWindowEarly &&
358+
if (this.isShowWindowEarly() &&
292359
!('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1')) {
293360
console.log('Showing main window early');
294361
app.whenReady().then(async () => {
295-
const options = await this.getLastWindowOptions();
362+
const options = this.getLastWindowOptions();
296363
this.initialWindow = await this.createWindow({ ...options });
297364
this.initialWindow.show();
298365
});
299366
}
300367
}
301368

369+
protected isShowWindowEarly(): boolean {
370+
return typeof this.config.electron.showWindowEarly === 'boolean' && this.config.electron.showWindowEarly;
371+
}
372+
373+
protected isShowSplashScreen(): boolean {
374+
return typeof this.config.electron.showWindowEarly === 'object' && !!this.config.electron.showWindowEarly.content;
375+
}
376+
377+
protected getSplashScreenOptions(): SplashScreenOptions | undefined {
378+
if (this.isShowSplashScreen()) {
379+
return this.config.electron.showWindowEarly as SplashScreenOptions;
380+
}
381+
return undefined;
382+
}
383+
302384
/**
303385
* Use this rather than creating `BrowserWindow` instances from scratch, since some security parameters need to be set, this method will do it.
304386
*
@@ -319,7 +401,7 @@ export class ElectronMainApplication {
319401
return electronWindow.window;
320402
}
321403

322-
async getLastWindowOptions(): Promise<TheiaBrowserWindowOptions> {
404+
getLastWindowOptions(): TheiaBrowserWindowOptions {
323405
const previousWindowState: TheiaBrowserWindowOptions | undefined = this.electronStore.get('windowstate');
324406
const windowState = previousWindowState?.screenLayout === this.getCurrentScreenLayout()
325407
? previousWindowState
@@ -365,6 +447,7 @@ export class ElectronMainApplication {
365447
preload: path.resolve(this.globals.THEIA_APP_PROJECT_PATH, 'lib', 'frontend', 'preload.js').toString()
366448
},
367449
...this.config.electron?.windowOptions || {},
450+
preventAutomaticShow: this.isShowSplashScreen()
368451
};
369452
}
370453

@@ -376,20 +459,44 @@ export class ElectronMainApplication {
376459
}
377460

378461
protected async openWindowWithWorkspace(workspacePath: string): Promise<BrowserWindow> {
379-
const options = await this.getLastWindowOptions();
462+
const options = this.getLastWindowOptions();
380463
const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.reuseOrCreateWindow(options)]);
381464
electronWindow.loadURL(uri.withFragment(encodeURI(workspacePath)).toString(true));
382465
return electronWindow;
383466
}
384467

385468
protected async reuseOrCreateWindow(asyncOptions: MaybePromise<TheiaBrowserWindowOptions>): Promise<BrowserWindow> {
386-
if (!this.initialWindow) {
387-
return this.createWindow(asyncOptions);
388-
}
469+
const windowPromise = this.initialWindow ? Promise.resolve(this.initialWindow) : this.createWindow(asyncOptions);
389470
// reset initial window after having it re-used once
390-
const window = this.initialWindow;
391471
this.initialWindow = undefined;
392-
return window;
472+
473+
// hook ready listener to dispose splash screen as configured via min and maximum wait times
474+
if (this.splashScreenState) {
475+
windowPromise.then(window => {
476+
TheiaRendererAPI.onApplicationStateChanged(window.webContents, state => {
477+
if (state === 'ready') {
478+
this.splashScreenState?.minTime.then(() => {
479+
// sanity check (e.g. max time < min time)
480+
if (this.splashScreenState) {
481+
window.show();
482+
this.splashScreenState.splashScreenWindow?.close();
483+
this.splashScreenState = undefined;
484+
}
485+
});
486+
}
487+
});
488+
this.splashScreenState?.maxTime.then(() => {
489+
// check whether splash screen was already disposed
490+
if (this.splashScreenState?.splashScreenWindow) {
491+
window.show();
492+
this.splashScreenState.splashScreenWindow?.close();
493+
this.splashScreenState = undefined;
494+
}
495+
});
496+
});
497+
}
498+
499+
return windowPromise;
393500
}
394501

395502
/** Configures native window creation, i.e. using window.open or links with target "_blank" in the frontend. */

0 commit comments

Comments
 (0)