Skip to content

Commit

Permalink
feat: splash screen support for Electron
Browse files Browse the repository at this point in the history
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 eclipse-theia#13410

Contributed on behalf of Pragmatiqu IT GmbH
  • Loading branch information
sdirix committed Mar 26, 2024
1 parent 8ea1846 commit f5ce793
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 17 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
## not yet released

- [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
- [core] Fix quickpick problems found in IDE testing [#13451](https://github.com/eclipse-theia/theia/pull/13451) - contributed on behalf of STMicroelectronics
- [core] Fix quickpick problems found in IDE testing [#13451](https://github.com/eclipse-theia/theia/pull/13451) - contributed on behalf of STMicroelectronics
- [core] Splash Screen Support for Electron [#13505](https://github.com/eclipse-theia/theia/pull/13505) - contributed on behalf of Pragmatiqu IT GmbH
- [plugin] Extend TextEditorLineNumbersStyle with Interval [#13458](https://github.com/eclipse-theia/theia/pull/13458) - contributed on behalf of STMicroelectronics

<a name="breaking_changes_not_yet_released">[Breaking Changes:](#breaking_changes_not_yet_released)</a>
Expand Down
35 changes: 31 additions & 4 deletions dev-packages/application-package/src/application-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,33 @@ export namespace ElectronFrontendApplicationConfig {
windowOptions: {},
showWindowEarly: true
};
export interface SplashScreenOptions {
/**
* Initial width of the splash screen. Defaults to 640.
*/
width?: number;
/**
* Initial height of the splash screen. Defaults to 480.
*/
height?: number;
/**
* Minimum amount of time in milliseconds to show the splash screen before main window is shown.
* Defaults to 0, i.e. the splash screen will be shown until the frontend application is ready.
*/
minDuration?: number;
/**
* Maximum amount of time in milliseconds before splash screen is removed and main window is shown.
* Defaults to 60000.
*/
maxDuration?: number;
/**
* The content to load in the splash screen.
* Will be resolved from application root.
*
* Mandatory attribute.
*/
content?: string;
}
export interface Partial {

/**
Expand All @@ -45,11 +72,11 @@ export namespace ElectronFrontendApplicationConfig {
readonly windowOptions?: BrowserWindowConstructorOptions;

/**
* Whether or not to show an empty Electron window as early as possible.
*
* Defaults to `true`.
* Whether or not to show an empty Electron main window as early as possible.
* Alternatively a splash screen can be configured which is shown until the
* frontend is ready.
*/
readonly showWindowEarly?: boolean;
readonly showWindowEarly?: boolean | SplashScreenOptions;
}
}

Expand Down
8 changes: 7 additions & 1 deletion examples/electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
"frontend": {
"config": {
"applicationName": "Theia Electron Example",
"reloadOnReconnect": true
"reloadOnReconnect": true,
"electron": {
"showWindowEarly": {
"content": "resources/theia-logo.svg",
"height": 90
}
}
}
},
"backend": {
Expand Down
32 changes: 32 additions & 0 deletions examples/electron/resources/theia-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
127 changes: 117 additions & 10 deletions packages/core/src/electron-main/electron-main-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { AddressInfo } from 'net';
import { promises as fs } from 'fs';
import { existsSync, mkdirSync } from 'fs-extra';
import { fork, ForkOptions } from 'child_process';
import { DefaultTheme, FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
import { DefaultTheme, ElectronFrontendApplicationConfig, FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
import URI from '../common/uri';
import { FileUri } from '../common/file-uri';
import { Deferred } from '../common/promise-util';
Expand Down Expand Up @@ -136,6 +136,16 @@ export class ElectronMainProcessArgv {

}

interface SplashScreenState {
splashScreenWindow?: BrowserWindow;
minTime: Promise<void>;
maxTime: Promise<void>;
}

interface SplashScreenOptions extends ElectronFrontendApplicationConfig.SplashScreenOptions {
content: string;
}

export namespace ElectronMainProcessArgv {
export interface ElectronMainProcess extends NodeJS.Process {
readonly defaultApp: boolean;
Expand Down Expand Up @@ -184,6 +194,8 @@ export class ElectronMainApplication {

protected initialWindow?: BrowserWindow;

protected splashScreenState?: SplashScreenState;

get config(): FrontendApplicationConfig {
if (!this._config) {
throw new Error('You have to start the application first.');
Expand Down Expand Up @@ -224,6 +236,7 @@ export class ElectronMainApplication {
this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native';
this._config = config;
this.hookApplicationEvents();
this.showSplashScreen();
this.showInitialWindow();
const port = await this.startBackend();
this._backendPort.resolve(port);
Expand Down Expand Up @@ -287,18 +300,87 @@ export class ElectronMainApplication {
return this.didUseNativeWindowFrameOnStart.get(webContents.id) ? 'native' : 'custom';
}

/**
* Shows the splash screen, if it was configured. Otherwise does nothing.
*/
protected showSplashScreen(): void {
if (this.isShowSplashScreen()) {
console.log('Showing splash screen');
const splashScreenOptions = this.getSplashScreenOptions();
if (!splashScreenOptions) {
// sanity check, should always exist here
return;
}
const content = splashScreenOptions.content;
console.debug('SplashScreen options', splashScreenOptions);
app.whenReady().then(() => {
const splashScreenBounds = this.determineSplashScreenBounds();
const splashScreenWindow = new BrowserWindow({
...splashScreenBounds,
frame: false,
alwaysOnTop: true,
});
splashScreenWindow.show();
splashScreenWindow.loadFile(path.resolve(this.globals.THEIA_APP_PROJECT_PATH, content).toString());
this.splashScreenState = {
splashScreenWindow,
minTime: new Promise(resolve => setTimeout(() => resolve(), splashScreenOptions.minDuration ?? 0)),
maxTime: new Promise(resolve => setTimeout(() => resolve(), splashScreenOptions.maxDuration ?? 60000)),
};
});
}
}

protected determineSplashScreenBounds(): { x: number, y: number, width: number, height: number } {
const splashScreenOptions = this.getSplashScreenOptions();
const width = splashScreenOptions?.width ?? 640;
const height = splashScreenOptions?.height ?? 480;

// determine the bounds of the screen on which Theia will be shown
const lastWindowOptions = this.getLastWindowOptions();
const defaultWindowBounds = this.getDefaultTheiaWindowBounds();
const theiaPoint = typeof lastWindowOptions.x === 'number' && typeof lastWindowOptions.y === 'number' ?
{ x: lastWindowOptions.x, y: lastWindowOptions.y } :
{ x: defaultWindowBounds.x!, y: defaultWindowBounds.y! };
const { bounds } = screen.getDisplayNearestPoint(theiaPoint);

// place splash screen center of screen
const middlePoint = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
const x = middlePoint.x - width / 2;
const y = middlePoint.y - height / 2;

return {
x, y, width, height
};
}

protected showInitialWindow(): void {
if (this.config.electron.showWindowEarly &&
if (this.isShowWindowEarly() &&
!('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1')) {
console.log('Showing main window early');
app.whenReady().then(async () => {
const options = await this.getLastWindowOptions();
const options = this.getLastWindowOptions();
this.initialWindow = await this.createWindow({ ...options });
this.initialWindow.show();
});
}
}

protected isShowWindowEarly(): boolean {
return typeof this.config.electron.showWindowEarly === 'boolean' && this.config.electron.showWindowEarly;
}

protected isShowSplashScreen(): boolean {
return typeof this.config.electron.showWindowEarly === 'object' && !!this.config.electron.showWindowEarly.content;
}

protected getSplashScreenOptions(): SplashScreenOptions | undefined {
if (this.isShowSplashScreen()) {
return this.config.electron.showWindowEarly as SplashScreenOptions;
}
return undefined;
}

/**
* Use this rather than creating `BrowserWindow` instances from scratch, since some security parameters need to be set, this method will do it.
*
Expand All @@ -319,7 +401,7 @@ export class ElectronMainApplication {
return electronWindow.window;
}

async getLastWindowOptions(): Promise<TheiaBrowserWindowOptions> {
getLastWindowOptions(): TheiaBrowserWindowOptions {
const previousWindowState: TheiaBrowserWindowOptions | undefined = this.electronStore.get('windowstate');
const windowState = previousWindowState?.screenLayout === this.getCurrentScreenLayout()
? previousWindowState
Expand Down Expand Up @@ -365,6 +447,7 @@ export class ElectronMainApplication {
preload: path.resolve(this.globals.THEIA_APP_PROJECT_PATH, 'lib', 'frontend', 'preload.js').toString()
},
...this.config.electron?.windowOptions || {},
preventAutomaticShow: this.isShowSplashScreen()
};
}

Expand All @@ -376,20 +459,44 @@ export class ElectronMainApplication {
}

protected async openWindowWithWorkspace(workspacePath: string): Promise<BrowserWindow> {
const options = await this.getLastWindowOptions();
const options = this.getLastWindowOptions();
const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.reuseOrCreateWindow(options)]);
electronWindow.loadURL(uri.withFragment(encodeURI(workspacePath)).toString(true));
return electronWindow;
}

protected async reuseOrCreateWindow(asyncOptions: MaybePromise<TheiaBrowserWindowOptions>): Promise<BrowserWindow> {
if (!this.initialWindow) {
return this.createWindow(asyncOptions);
}
const windowPromise = this.initialWindow ? Promise.resolve(this.initialWindow) : this.createWindow(asyncOptions);
// reset initial window after having it re-used once
const window = this.initialWindow;
this.initialWindow = undefined;
return window;

// hook ready listener to dispose splash screen as configured via min and maximum wait times
if (this.splashScreenState) {
windowPromise.then(window => {
TheiaRendererAPI.onApplicationStateChanged(window.webContents, state => {
if (state === 'ready') {
this.splashScreenState?.minTime.then(() => {
// sanity check (e.g. max time < min time)
if (this.splashScreenState) {
window.show();
this.splashScreenState.splashScreenWindow?.close();
this.splashScreenState = undefined;
}
});
}
});
this.splashScreenState?.maxTime.then(() => {
// check whether splash screen was already disposed
if (this.splashScreenState?.splashScreenWindow) {
window.show();
this.splashScreenState.splashScreenWindow?.close();
this.splashScreenState = undefined;
}
});
});
}

return windowPromise;
}

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

0 comments on commit f5ce793

Please sign in to comment.