Skip to content

Commit

Permalink
feat: splash screen support for Electron (#13505)
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.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
  • Loading branch information
sdirix authored Apr 29, 2024
1 parent f994214 commit 90c6334
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- [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
- [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
- [core] Splash Screen Support for Electron [#13505](https://github.com/eclipse-theia/theia/pull/13505) - contributed on behalf of Pragmatiqu IT GmbH
- [scm] added support for dirty diff peek view [#13104](https://github.com/eclipse-theia/theia/pull/13104)
- [test] stub VS Code `Test Coverage` API [#13631](https://github.com/eclipse-theia/theia/pull/13631)

Expand Down
37 changes: 36 additions & 1 deletion dev-packages/application-package/src/application-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,36 @@ export type ElectronFrontendApplicationConfig = RequiredRecursive<ElectronFronte
export namespace ElectronFrontendApplicationConfig {
export const DEFAULT: ElectronFrontendApplicationConfig = {
windowOptions: {},
showWindowEarly: true
showWindowEarly: true,
splashScreenOptions: {}
};
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 30000.
*/
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 @@ -50,6 +78,13 @@ export namespace ElectronFrontendApplicationConfig {
* Defaults to `true`.
*/
readonly showWindowEarly?: boolean;

/**
* Configuration options for splash screen.
*
* Defaults to `{}` which results in no splash screen being displayed.
*/
readonly splashScreenOptions?: 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": {
"splashScreenOptions": {
"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.
108 changes: 101 additions & 7 deletions packages/core/src/electron-main/electron-main-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ 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';
import { Deferred, timeout } from '../common/promise-util';
import { MaybePromise } from '../common/types';
import { ContributionProvider } from '../common/contribution-provider';
import { ElectronSecurityTokenService } from './electron-security-token-service';
import { ElectronSecurityToken } from '../electron-common/electron-token';
import Storage = require('electron-store');
import { Disposable, DisposableCollection, isOSX, isWindows } from '../common';
import { CancellationTokenSource, Disposable, DisposableCollection, isOSX, isWindows } from '../common';
import { DEFAULT_WINDOW_HASH, WindowSearchParams } from '../common/window';
import { TheiaBrowserWindowOptions, TheiaElectronWindow, TheiaElectronWindowFactory } from './theia-electron-window';
import { ElectronMainApplicationGlobals } from './electron-main-constants';
Expand Down Expand Up @@ -182,6 +182,7 @@ export class ElectronMainApplication {
protected windows = new Map<number, TheiaElectronWindow>();
protected restarting = false;

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

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

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

// determine the screen on which to show the splash screen via the center of the window to show
const windowCenterPoint = { x: initialWindowBounds.x + (initialWindowBounds.width / 2), y: initialWindowBounds.y + (initialWindowBounds.height / 2) };
const { bounds } = screen.getDisplayNearestPoint(windowCenterPoint);

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

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

protected isShowWindowEarly(): boolean {
return !!this.config.electron.showWindowEarly &&
!('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1');
}

protected showInitialWindow(): void {
if (this.config.electron.showWindowEarly &&
!('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1')) {
console.log('Showing main window early');
if (this.isShowWindowEarly() || this.isShowSplashScreen()) {
app.whenReady().then(async () => {
const options = await this.getLastWindowOptions();
// If we want to show a splash screen, don't auto open the main window
if (this.isShowSplashScreen()) {
options.preventAutomaticShow = true;
}
this.initialWindow = await this.createWindow({ ...options });
this.initialWindow.show();

if (this.isShowSplashScreen()) {
console.log('Showing splash screen');
this.configureAndShowSplashScreen(this.initialWindow);
}

// Show main window early if windows shall be shown early and splash screen is not configured
if (this.isShowWindowEarly() && !this.isShowSplashScreen()) {
console.log('Showing main window early');
this.initialWindow.show();
}
});
}
}

protected async configureAndShowSplashScreen(mainWindow: BrowserWindow): Promise<BrowserWindow> {
const splashScreenOptions = this.getSplashScreenOptions()!;
console.debug('SplashScreen options', splashScreenOptions);

const splashScreenBounds = await this.determineSplashScreenBounds(mainWindow.getBounds());
const splashScreenWindow = new BrowserWindow({
...splashScreenBounds,
frame: false,
alwaysOnTop: true,
show: false
});

if (this.isShowWindowEarly()) {
console.log('Showing splash screen early');
splashScreenWindow.show();
} else {
splashScreenWindow.on('ready-to-show', () => {
splashScreenWindow.show();
});
}

splashScreenWindow.loadFile(path.resolve(this.globals.THEIA_APP_PROJECT_PATH, splashScreenOptions.content!).toString());

// close splash screen and show main window once frontend is ready or a timeout is hit
const cancelTokenSource = new CancellationTokenSource();
const minTime = timeout(splashScreenOptions.minDuration ?? 0, cancelTokenSource.token);
const maxTime = timeout(splashScreenOptions.maxDuration ?? 30000, cancelTokenSource.token);

const showWindowAndCloseSplashScreen = () => {
cancelTokenSource.cancel();
if (!mainWindow.isVisible()) {
mainWindow.show();
}
splashScreenWindow.close();
};
TheiaRendererAPI.onApplicationStateChanged(mainWindow.webContents, state => {
if (state === 'ready') {
minTime.then(() => showWindowAndCloseSplashScreen());
}
});
maxTime.then(() => showWindowAndCloseSplashScreen());
return splashScreenWindow;
}

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

protected getSplashScreenOptions(): ElectronFrontendApplicationConfig.SplashScreenOptions | undefined {
if (this.isShowSplashScreen()) {
return this.config.electron.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 @@ -316,6 +409,7 @@ export class ElectronMainApplication {
electronWindow.window.on('focus', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'focus'));
this.attachSaveWindowState(electronWindow.window);
this.configureNativeSecondaryWindowCreation(electronWindow.window);

return electronWindow.window;
}

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 90c6334

Please sign in to comment.