diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c2d3bb4353fe..53865bfdddee6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
[Breaking Changes:](#breaking_changes_not_yet_released)
diff --git a/dev-packages/application-package/src/application-props.ts b/dev-packages/application-package/src/application-props.ts
index a443a60df679d..91de09fc319b7 100644
--- a/dev-packages/application-package/src/application-props.ts
+++ b/dev-packages/application-package/src/application-props.ts
@@ -33,8 +33,36 @@ export type ElectronFrontendApplicationConfig = RequiredRecursive
+
+
+
diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts
index 2efa9fce419e2..dda80aa3b4149 100644
--- a/packages/core/src/electron-main/electron-main-application.ts
+++ b/packages/core/src/electron-main/electron-main-application.ts
@@ -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';
@@ -136,6 +136,16 @@ export class ElectronMainProcessArgv {
}
+interface SplashScreenState {
+ splashScreenWindow?: BrowserWindow;
+ minTime?: Promise;
+ maxTime?: Promise;
+}
+
+interface SplashScreenOptions extends ElectronFrontendApplicationConfig.SplashScreenOptions {
+ content: string;
+}
+
export namespace ElectronMainProcessArgv {
export interface ElectronMainProcess extends NodeJS.Process {
readonly defaultApp: boolean;
@@ -182,7 +192,10 @@ export class ElectronMainApplication {
protected windows = new Map();
protected restarting = false;
+ /** Used to temporarily store the reference to an early created main window, in case the app is configured with `showWindowEarly` and without `splashScreenOptions` */
protected initialWindow?: BrowserWindow;
+ /** Used to temporarily store the splash screen state, in case one is rendered. Will be reset once splash screen handling is over. */
+ protected splashScreenState?: SplashScreenState;
get config(): FrontendApplicationConfig {
if (!this._config) {
@@ -224,6 +237,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);
@@ -287,18 +301,102 @@ 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
+ console.error('Splash screen options not available although they should be. Will not show a splash screen.');
+ return;
+ }
+ const content = splashScreenOptions.content;
+ console.debug('SplashScreen options', splashScreenOptions);
+ // indicate to render a splash screen
+ this.splashScreenState = {};
+ app.whenReady().then(() => {
+ this.determineSplashScreenBounds().then(splashScreenBounds => {
+ const splashScreenWindow = new BrowserWindow({
+ ...splashScreenBounds,
+ frame: false,
+ alwaysOnTop: true,
+ show: this.isShowWindowEarly()
+ });
+ this.splashScreenState = {
+ splashScreenWindow,
+ minTime: new Promise(resolve => setTimeout(() => resolve(), splashScreenOptions.minDuration ?? 0)),
+ maxTime: new Promise(resolve => setTimeout(() => resolve(), splashScreenOptions.maxDuration ?? 30000)),
+ };
+ if (!this.isShowWindowEarly()) {
+ splashScreenWindow.on('ready-to-show', () => {
+ this.splashScreenState?.splashScreenWindow?.show();
+ });
+ }
+ splashScreenWindow.loadFile(path.resolve(this.globals.THEIA_APP_PROJECT_PATH, content).toString());
+ });
+ });
+ }
+ }
+
+ protected async determineSplashScreenBounds(): 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 bounds of the Theia main application
+ const lastWindowOptions = await this.getLastWindowOptions();
+ const defaultWindowBounds = this.getDefaultTheiaWindowBounds();
+ const theiaBounds = typeof lastWindowOptions.x === 'number' &&
+ typeof lastWindowOptions.y === 'number' &&
+ typeof lastWindowOptions.width === 'number' &&
+ typeof lastWindowOptions.height === 'number' ?
+ { x: lastWindowOptions.x, y: lastWindowOptions.y, width: lastWindowOptions.width, height: lastWindowOptions.height } :
+ { x: defaultWindowBounds.x!, y: defaultWindowBounds.y!, width: lastWindowOptions.width!, height: lastWindowOptions.height! };
+
+ // determine the screen on which to show the splash screen via the center of the Theia window to show
+ const theiaCenterPoint = { x: theiaBounds.x + theiaBounds.width / 2, y: theiaBounds.y + theiaBounds.height / 2 };
+ const { bounds } = screen.getDisplayNearestPoint(theiaCenterPoint);
+
+ // 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 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')) {
+ if (this.isShowWindowEarly() && !this.isShowSplashScreen()) {
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 isShowSplashScreen(): boolean {
+ return typeof this.config.electron.splashScreenOptions === 'object' && !!this.config.electron.splashScreenOptions.content;
+ }
+
+ protected getSplashScreenOptions(): SplashScreenOptions | undefined {
+ if (this.isShowSplashScreen()) {
+ return this.config.electron.splashScreenOptions 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.
*
@@ -307,6 +405,10 @@ export class ElectronMainApplication {
async createWindow(asyncOptions: MaybePromise = this.getDefaultTheiaWindowOptions()): Promise {
let options = await asyncOptions;
options = this.avoidOverlap(options);
+ if (this.splashScreenState) {
+ // in case we show a splash screen, do not automatically open created windows
+ options = { ...options, preventAutomaticShow: true };
+ }
const electronWindow = this.windowFactory(options, this.config);
const id = electronWindow.window.webContents.id;
this.windows.set(id, electronWindow);
@@ -316,9 +418,40 @@ export class ElectronMainApplication {
electronWindow.window.on('focus', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'focus'));
this.attachSaveWindowState(electronWindow.window);
this.configureNativeSecondaryWindowCreation(electronWindow.window);
+ this.configureShowOnSplashScreenClose(electronWindow.window);
+
return electronWindow.window;
}
+ /**
+ * In case we show a splash screen, this will configure the window to show and the splash screen to close
+ * depending on the frontend ready state and user configuration.
+ */
+ protected configureShowOnSplashScreenClose(window: BrowserWindow): void {
+ if (this.splashScreenState) {
+ const showWindowAndCloseSplashScreen = () => {
+ if (!window.isVisible()) {
+ window.show();
+ }
+ this.splashScreenState?.splashScreenWindow?.close();
+ this.splashScreenState = undefined;
+ };
+
+ TheiaRendererAPI.onApplicationStateChanged(window.webContents, state => {
+ if (state === 'ready') {
+ if (this.splashScreenState?.minTime) {
+ this.splashScreenState.minTime.then(() => showWindowAndCloseSplashScreen());
+ } else {
+ showWindowAndCloseSplashScreen();
+ }
+ }
+ });
+ // maxTime should always exist here, sanity fallback
+ const maxTime = this.splashScreenState.maxTime ?? new Promise(resolve => setTimeout(() => resolve(), 30000));
+ maxTime.then(() => showWindowAndCloseSplashScreen());
+ }
+ }
+
async getLastWindowOptions(): Promise {
const previousWindowState: TheiaBrowserWindowOptions | undefined = this.electronStore.get('windowstate');
const windowState = previousWindowState?.screenLayout === this.getCurrentScreenLayout()
@@ -364,7 +497,7 @@ export class ElectronMainApplication {
nodeIntegrationInWorker: false,
preload: path.resolve(this.globals.THEIA_APP_PROJECT_PATH, 'lib', 'frontend', 'preload.js').toString()
},
- ...this.config.electron?.windowOptions || {},
+ ...this.config.electron?.windowOptions || {}
};
}
@@ -376,20 +509,18 @@ export class ElectronMainApplication {
}
protected async openWindowWithWorkspace(workspacePath: string): Promise {
- 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): Promise {
- 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;
+
+ return windowPromise;
}
/** Configures native window creation, i.e. using window.open or links with target "_blank" in the frontend. */
diff --git a/packages/core/src/electron-main/theia-electron-window.ts b/packages/core/src/electron-main/theia-electron-window.ts
index 17c82fa9a4511..985fc01220fbc 100644
--- a/packages/core/src/electron-main/theia-electron-window.ts
+++ b/packages/core/src/electron-main/theia-electron-window.ts
@@ -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');
@@ -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();