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.

Implements eclipse-theia#13410

Contributed on behalf of Pragmatiqu IT GmbH
  • Loading branch information
sdirix committed Mar 25, 2024
1 parent 0871e31 commit 5c933a2
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 16 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

## not yet released

- [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
128 changes: 118 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,88 @@ 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 {
// content must be handed over for splash screen to take effect
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 +402,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 +448,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 +460,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
10 changes: 9 additions & 1 deletion packages/core/src/electron-main/theia-electron-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export interface TheiaBrowserWindowOptions extends BrowserWindowConstructorOptio
* in which case we want to invalidate the stored options and use the default options instead.
*/
screenLayout?: string;
/**
* By default, the window will be shown as soon as the content is ready to render.
* This can be prevented by handing over preventAutomaticShow: `true`.
* Use this for fine-grained control over when to show the window, e.g. to coordinate with a splash screen.
*/
preventAutomaticShow?: boolean;
}

export const TheiaBrowserWindowOptions = Symbol('TheiaBrowserWindowOptions');
Expand Down Expand Up @@ -76,7 +82,9 @@ export class TheiaElectronWindow {
protected init(): void {
this._window = new BrowserWindow(this.options);
this._window.setMenuBarVisibility(false);
this.attachReadyToShow();
if (!this.options.preventAutomaticShow) {
this.attachReadyToShow();
}
this.restoreMaximizedState();
this.attachCloseListeners();
this.trackApplicationState();
Expand Down

0 comments on commit 5c933a2

Please sign in to comment.