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();