diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21929257f8b64..62ae28dc7fd6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
[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..7567698b5b610 100644
--- a/dev-packages/application-package/src/application-props.ts
+++ b/dev-packages/application-package/src/application-props.ts
@@ -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 {
/**
@@ -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;
}
}
diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts
index 2efa9fce419e2..d9204781ae3d8 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;
@@ -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.');
@@ -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);
@@ -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.
*
@@ -319,7 +402,7 @@ export class ElectronMainApplication {
return electronWindow.window;
}
- async getLastWindowOptions(): Promise {
+ getLastWindowOptions(): TheiaBrowserWindowOptions {
const previousWindowState: TheiaBrowserWindowOptions | undefined = this.electronStore.get('windowstate');
const windowState = previousWindowState?.screenLayout === this.getCurrentScreenLayout()
? previousWindowState
@@ -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()
};
}
@@ -376,20 +460,44 @@ 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;
+
+ // 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. */
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();