Skip to content

Commit 90c6334

Browse files
authored
feat: splash screen support for Electron (#13505)
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.splashScreenOptions". Mandatory is the option "content" which specifies a relative path from the application root 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 f994214 commit 90c6334

File tree

6 files changed

+186
-10
lines changed

6 files changed

+186
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
- [application-manager] Generate Extension Info in server application to avoid empty About Dialog [#13590](https://github.com/eclipse-theia/theia/pull/13590) - contributed on behalf of STMicroelectronics
1010
- [application-package] bumped the default supported API from `1.87.2` to `1.88.1` [#13646](https://github.com/eclipse-theia/theia/pull/13646) - 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
- [scm] added support for dirty diff peek view [#13104](https://github.com/eclipse-theia/theia/pull/13104)
1213
- [test] stub VS Code `Test Coverage` API [#13631](https://github.com/eclipse-theia/theia/pull/13631)
1314

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,36 @@ export type ElectronFrontendApplicationConfig = RequiredRecursive<ElectronFronte
3333
export namespace ElectronFrontendApplicationConfig {
3434
export const DEFAULT: ElectronFrontendApplicationConfig = {
3535
windowOptions: {},
36-
showWindowEarly: true
36+
showWindowEarly: true,
37+
splashScreenOptions: {}
3738
};
39+
export interface SplashScreenOptions {
40+
/**
41+
* Initial width of the splash screen. Defaults to 640.
42+
*/
43+
width?: number;
44+
/**
45+
* Initial height of the splash screen. Defaults to 480.
46+
*/
47+
height?: number;
48+
/**
49+
* Minimum amount of time in milliseconds to show the splash screen before main window is shown.
50+
* Defaults to 0, i.e. the splash screen will be shown until the frontend application is ready.
51+
*/
52+
minDuration?: number;
53+
/**
54+
* Maximum amount of time in milliseconds before splash screen is removed and main window is shown.
55+
* Defaults to 30000.
56+
*/
57+
maxDuration?: number;
58+
/**
59+
* The content to load in the splash screen.
60+
* Will be resolved from application root.
61+
*
62+
* Mandatory attribute.
63+
*/
64+
content?: string;
65+
}
3866
export interface Partial {
3967

4068
/**
@@ -50,6 +78,13 @@ export namespace ElectronFrontendApplicationConfig {
5078
* Defaults to `true`.
5179
*/
5280
readonly showWindowEarly?: boolean;
81+
82+
/**
83+
* Configuration options for splash screen.
84+
*
85+
* Defaults to `{}` which results in no splash screen being displayed.
86+
*/
87+
readonly splashScreenOptions?: SplashScreenOptions;
5388
}
5489
}
5590

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+
"splashScreenOptions": {
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: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@ 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';
28-
import { Deferred } from '../common/promise-util';
28+
import { Deferred, timeout } from '../common/promise-util';
2929
import { MaybePromise } from '../common/types';
3030
import { ContributionProvider } from '../common/contribution-provider';
3131
import { ElectronSecurityTokenService } from './electron-security-token-service';
3232
import { ElectronSecurityToken } from '../electron-common/electron-token';
3333
import Storage = require('electron-store');
34-
import { Disposable, DisposableCollection, isOSX, isWindows } from '../common';
34+
import { CancellationTokenSource, Disposable, DisposableCollection, isOSX, isWindows } from '../common';
3535
import { DEFAULT_WINDOW_HASH, WindowSearchParams } from '../common/window';
3636
import { TheiaBrowserWindowOptions, TheiaElectronWindow, TheiaElectronWindowFactory } from './theia-electron-window';
3737
import { ElectronMainApplicationGlobals } from './electron-main-constants';
@@ -182,6 +182,7 @@ export class ElectronMainApplication {
182182
protected windows = new Map<number, TheiaElectronWindow>();
183183
protected restarting = false;
184184

185+
/** Used to temporarily store the reference to an early created main window */
185186
protected initialWindow?: BrowserWindow;
186187

187188
get config(): FrontendApplicationConfig {
@@ -287,18 +288,110 @@ export class ElectronMainApplication {
287288
return this.didUseNativeWindowFrameOnStart.get(webContents.id) ? 'native' : 'custom';
288289
}
289290

291+
protected async determineSplashScreenBounds(initialWindowBounds: { x: number, y: number, width: number, height: number }):
292+
Promise<{ x: number, y: number, width: number, height: number }> {
293+
const splashScreenOptions = this.getSplashScreenOptions();
294+
const width = splashScreenOptions?.width ?? 640;
295+
const height = splashScreenOptions?.height ?? 480;
296+
297+
// determine the screen on which to show the splash screen via the center of the window to show
298+
const windowCenterPoint = { x: initialWindowBounds.x + (initialWindowBounds.width / 2), y: initialWindowBounds.y + (initialWindowBounds.height / 2) };
299+
const { bounds } = screen.getDisplayNearestPoint(windowCenterPoint);
300+
301+
// place splash screen center of screen
302+
const screenCenterPoint = { x: bounds.x + (bounds.width / 2), y: bounds.y + (bounds.height / 2) };
303+
const x = screenCenterPoint.x - (width / 2);
304+
const y = screenCenterPoint.y - (height / 2);
305+
306+
return {
307+
x, y, width, height
308+
};
309+
}
310+
311+
protected isShowWindowEarly(): boolean {
312+
return !!this.config.electron.showWindowEarly &&
313+
!('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1');
314+
}
315+
290316
protected showInitialWindow(): void {
291-
if (this.config.electron.showWindowEarly &&
292-
!('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1')) {
293-
console.log('Showing main window early');
317+
if (this.isShowWindowEarly() || this.isShowSplashScreen()) {
294318
app.whenReady().then(async () => {
295319
const options = await this.getLastWindowOptions();
320+
// If we want to show a splash screen, don't auto open the main window
321+
if (this.isShowSplashScreen()) {
322+
options.preventAutomaticShow = true;
323+
}
296324
this.initialWindow = await this.createWindow({ ...options });
297-
this.initialWindow.show();
325+
326+
if (this.isShowSplashScreen()) {
327+
console.log('Showing splash screen');
328+
this.configureAndShowSplashScreen(this.initialWindow);
329+
}
330+
331+
// Show main window early if windows shall be shown early and splash screen is not configured
332+
if (this.isShowWindowEarly() && !this.isShowSplashScreen()) {
333+
console.log('Showing main window early');
334+
this.initialWindow.show();
335+
}
298336
});
299337
}
300338
}
301339

340+
protected async configureAndShowSplashScreen(mainWindow: BrowserWindow): Promise<BrowserWindow> {
341+
const splashScreenOptions = this.getSplashScreenOptions()!;
342+
console.debug('SplashScreen options', splashScreenOptions);
343+
344+
const splashScreenBounds = await this.determineSplashScreenBounds(mainWindow.getBounds());
345+
const splashScreenWindow = new BrowserWindow({
346+
...splashScreenBounds,
347+
frame: false,
348+
alwaysOnTop: true,
349+
show: false
350+
});
351+
352+
if (this.isShowWindowEarly()) {
353+
console.log('Showing splash screen early');
354+
splashScreenWindow.show();
355+
} else {
356+
splashScreenWindow.on('ready-to-show', () => {
357+
splashScreenWindow.show();
358+
});
359+
}
360+
361+
splashScreenWindow.loadFile(path.resolve(this.globals.THEIA_APP_PROJECT_PATH, splashScreenOptions.content!).toString());
362+
363+
// close splash screen and show main window once frontend is ready or a timeout is hit
364+
const cancelTokenSource = new CancellationTokenSource();
365+
const minTime = timeout(splashScreenOptions.minDuration ?? 0, cancelTokenSource.token);
366+
const maxTime = timeout(splashScreenOptions.maxDuration ?? 30000, cancelTokenSource.token);
367+
368+
const showWindowAndCloseSplashScreen = () => {
369+
cancelTokenSource.cancel();
370+
if (!mainWindow.isVisible()) {
371+
mainWindow.show();
372+
}
373+
splashScreenWindow.close();
374+
};
375+
TheiaRendererAPI.onApplicationStateChanged(mainWindow.webContents, state => {
376+
if (state === 'ready') {
377+
minTime.then(() => showWindowAndCloseSplashScreen());
378+
}
379+
});
380+
maxTime.then(() => showWindowAndCloseSplashScreen());
381+
return splashScreenWindow;
382+
}
383+
384+
protected isShowSplashScreen(): boolean {
385+
return typeof this.config.electron.splashScreenOptions === 'object' && !!this.config.electron.splashScreenOptions.content;
386+
}
387+
388+
protected getSplashScreenOptions(): ElectronFrontendApplicationConfig.SplashScreenOptions | undefined {
389+
if (this.isShowSplashScreen()) {
390+
return this.config.electron.splashScreenOptions;
391+
}
392+
return undefined;
393+
}
394+
302395
/**
303396
* Use this rather than creating `BrowserWindow` instances from scratch, since some security parameters need to be set, this method will do it.
304397
*
@@ -316,6 +409,7 @@ export class ElectronMainApplication {
316409
electronWindow.window.on('focus', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'focus'));
317410
this.attachSaveWindowState(electronWindow.window);
318411
this.configureNativeSecondaryWindowCreation(electronWindow.window);
412+
319413
return electronWindow.window;
320414
}
321415

packages/core/src/electron-main/theia-electron-window.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export interface TheiaBrowserWindowOptions extends BrowserWindowConstructorOptio
3737
* in which case we want to invalidate the stored options and use the default options instead.
3838
*/
3939
screenLayout?: string;
40+
/**
41+
* By default, the window will be shown as soon as the content is ready to render.
42+
* This can be prevented by handing over preventAutomaticShow: `true`.
43+
* Use this for fine-grained control over when to show the window, e.g. to coordinate with a splash screen.
44+
*/
45+
preventAutomaticShow?: boolean;
4046
}
4147

4248
export const TheiaBrowserWindowOptions = Symbol('TheiaBrowserWindowOptions');
@@ -76,7 +82,9 @@ export class TheiaElectronWindow {
7682
protected init(): void {
7783
this._window = new BrowserWindow(this.options);
7884
this._window.setMenuBarVisibility(false);
79-
this.attachReadyToShow();
85+
if (!this.options.preventAutomaticShow) {
86+
this.attachReadyToShow();
87+
}
8088
this.restoreMaximizedState();
8189
this.attachCloseListeners();
8290
this.trackApplicationState();

0 commit comments

Comments
 (0)