diff --git a/dev-packages/application-package/src/application-props.ts b/dev-packages/application-package/src/application-props.ts index a443a60df679d..efdc3deef3d76 100644 --- a/dev-packages/application-package/src/application-props.ts +++ b/dev-packages/application-package/src/application-props.ts @@ -33,8 +33,37 @@ export type ElectronFrontendApplicationConfig = RequiredRecursive; + maxTime: Promise; +} + export namespace ElectronMainProcessArgv { export interface ElectronMainProcess extends NodeJS.Process { readonly defaultApp: boolean; @@ -184,6 +190,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 +232,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,12 +296,69 @@ 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.config.electron.splashScreenOptions?.content) { + console.log('Showing splash screen'); + const splashScreenOptions = this.config.electron.splashScreenOptions; + const content = this.config.electron.splashScreenOptions.content; + console.debug('SplashScreen options', splashScreenOptions); + app.whenReady().then(() => { + const splashScreenBounds = this.determineSplashScreenBounds(); + const splashScreenWindow = new BrowserWindow({ + ...splashScreenBounds, + frame: false, + alwaysOnTop: true, + webPreferences: { + preload: content.endsWith('.js') ? path.resolve(this.globals.THEIA_APP_PROJECT_PATH, 'lib', 'frontend', content).toString() : undefined + } + }); + splashScreenWindow.show(); + if (!content.endsWith('.js')) { + splashScreenWindow.loadFile(path.resolve(this.globals.THEIA_APP_PROJECT_PATH, 'lib', 'frontend', 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.config.electron.splashScreenOptions; + 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 && - !('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1')) { + !('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1') && + !this.config.electron.splashScreenOptions?.content) { 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(); }); @@ -319,7 +385,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 +431,7 @@ export class ElectronMainApplication { preload: path.resolve(this.globals.THEIA_APP_PROJECT_PATH, 'lib', 'frontend', 'preload.js').toString() }, ...this.config.electron?.windowOptions || {}, + preventAutomaticShow: !!this.config.electron.splashScreenOptions?.content }; } @@ -376,20 +443,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();