From 90c6334f86688a4b53141dc911e41c2dae79584b Mon Sep 17 00:00:00 2001 From: Stefan Dirix Date: Mon, 29 Apr 2024 11:23:11 +0200 Subject: [PATCH] feat: splash screen support for Electron (#13505) 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 --- CHANGELOG.md | 1 + .../src/application-props.ts | 37 +++++- examples/electron/package.json | 8 +- examples/electron/resources/theia-logo.svg | 32 ++++++ .../electron-main-application.ts | 108 ++++++++++++++++-- .../electron-main/theia-electron-window.ts | 10 +- 6 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 examples/electron/resources/theia-logo.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b928620a1162..6493c519bac06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) 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 9c0f410fba9ba..e97114df94c8c 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -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'; @@ -182,6 +182,7 @@ export class ElectronMainApplication { protected windows = new Map(); protected restarting = false; + /** Used to temporarily store the reference to an early created main window */ protected initialWindow?: BrowserWindow; get config(): FrontendApplicationConfig { @@ -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 { + 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. * @@ -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; } 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();