@@ -22,7 +22,7 @@ import { AddressInfo } from 'net';
2222import { promises as fs } from 'fs' ;
2323import { existsSync , mkdirSync } from 'fs-extra' ;
2424import { fork , ForkOptions } from 'child_process' ;
25- import { DefaultTheme , FrontendApplicationConfig } from '@theia/application-package/lib/application-props' ;
25+ import { DefaultTheme , ElectronFrontendApplicationConfig , FrontendApplicationConfig } from '@theia/application-package/lib/application-props' ;
2626import URI from '../common/uri' ;
2727import { FileUri } from '../common/file-uri' ;
2828import { Deferred } from '../common/promise-util' ;
@@ -136,6 +136,16 @@ export class ElectronMainProcessArgv {
136136
137137}
138138
139+ interface SplashScreenState {
140+ splashScreenWindow ?: BrowserWindow ;
141+ minTime : Promise < void > ;
142+ maxTime : Promise < void > ;
143+ }
144+
145+ interface SplashScreenOptions extends ElectronFrontendApplicationConfig . SplashScreenOptions {
146+ content : string ;
147+ }
148+
139149export namespace ElectronMainProcessArgv {
140150 export interface ElectronMainProcess extends NodeJS . Process {
141151 readonly defaultApp : boolean ;
@@ -184,6 +194,8 @@ export class ElectronMainApplication {
184194
185195 protected initialWindow ?: BrowserWindow ;
186196
197+ protected splashScreenState ?: SplashScreenState ;
198+
187199 get config ( ) : FrontendApplicationConfig {
188200 if ( ! this . _config ) {
189201 throw new Error ( 'You have to start the application first.' ) ;
@@ -224,6 +236,7 @@ export class ElectronMainApplication {
224236 this . useNativeWindowFrame = this . getTitleBarStyle ( config ) === 'native' ;
225237 this . _config = config ;
226238 this . hookApplicationEvents ( ) ;
239+ this . showSplashScreen ( ) ;
227240 this . showInitialWindow ( ) ;
228241 const port = await this . startBackend ( ) ;
229242 this . _backendPort . resolve ( port ) ;
@@ -287,18 +300,87 @@ export class ElectronMainApplication {
287300 return this . didUseNativeWindowFrameOnStart . get ( webContents . id ) ? 'native' : 'custom' ;
288301 }
289302
303+ /**
304+ * Shows the splash screen, if it was configured. Otherwise does nothing.
305+ */
306+ protected showSplashScreen ( ) : void {
307+ if ( this . isShowSplashScreen ( ) ) {
308+ console . log ( 'Showing splash screen' ) ;
309+ const splashScreenOptions = this . getSplashScreenOptions ( ) ;
310+ if ( ! splashScreenOptions ) {
311+ // sanity check, should always exist here
312+ return ;
313+ }
314+ const content = splashScreenOptions . content ;
315+ console . debug ( 'SplashScreen options' , splashScreenOptions ) ;
316+ app . whenReady ( ) . then ( ( ) => {
317+ const splashScreenBounds = this . determineSplashScreenBounds ( ) ;
318+ const splashScreenWindow = new BrowserWindow ( {
319+ ...splashScreenBounds ,
320+ frame : false ,
321+ alwaysOnTop : true ,
322+ } ) ;
323+ splashScreenWindow . show ( ) ;
324+ splashScreenWindow . loadFile ( path . resolve ( this . globals . THEIA_APP_PROJECT_PATH , content ) . toString ( ) ) ;
325+ this . splashScreenState = {
326+ splashScreenWindow,
327+ minTime : new Promise ( resolve => setTimeout ( ( ) => resolve ( ) , splashScreenOptions . minDuration ?? 0 ) ) ,
328+ maxTime : new Promise ( resolve => setTimeout ( ( ) => resolve ( ) , splashScreenOptions . maxDuration ?? 60000 ) ) ,
329+ } ;
330+ } ) ;
331+ }
332+ }
333+
334+ protected determineSplashScreenBounds ( ) : { x : number , y : number , width : number , height : number } {
335+ const splashScreenOptions = this . getSplashScreenOptions ( ) ;
336+ const width = splashScreenOptions ?. width ?? 640 ;
337+ const height = splashScreenOptions ?. height ?? 480 ;
338+
339+ // determine the bounds of the screen on which Theia will be shown
340+ const lastWindowOptions = this . getLastWindowOptions ( ) ;
341+ const defaultWindowBounds = this . getDefaultTheiaWindowBounds ( ) ;
342+ const theiaPoint = typeof lastWindowOptions . x === 'number' && typeof lastWindowOptions . y === 'number' ?
343+ { x : lastWindowOptions . x , y : lastWindowOptions . y } :
344+ { x : defaultWindowBounds . x ! , y : defaultWindowBounds . y ! } ;
345+ const { bounds } = screen . getDisplayNearestPoint ( theiaPoint ) ;
346+
347+ // place splash screen center of screen
348+ const middlePoint = { x : bounds . x + bounds . width / 2 , y : bounds . y + bounds . height / 2 } ;
349+ const x = middlePoint . x - width / 2 ;
350+ const y = middlePoint . y - height / 2 ;
351+
352+ return {
353+ x, y, width, height
354+ } ;
355+ }
356+
290357 protected showInitialWindow ( ) : void {
291- if ( this . config . electron . showWindowEarly &&
358+ if ( this . isShowWindowEarly ( ) &&
292359 ! ( 'THEIA_ELECTRON_NO_EARLY_WINDOW' in process . env && process . env . THEIA_ELECTRON_NO_EARLY_WINDOW === '1' ) ) {
293360 console . log ( 'Showing main window early' ) ;
294361 app . whenReady ( ) . then ( async ( ) => {
295- const options = await this . getLastWindowOptions ( ) ;
362+ const options = this . getLastWindowOptions ( ) ;
296363 this . initialWindow = await this . createWindow ( { ...options } ) ;
297364 this . initialWindow . show ( ) ;
298365 } ) ;
299366 }
300367 }
301368
369+ protected isShowWindowEarly ( ) : boolean {
370+ return typeof this . config . electron . showWindowEarly === 'boolean' && this . config . electron . showWindowEarly ;
371+ }
372+
373+ protected isShowSplashScreen ( ) : boolean {
374+ return typeof this . config . electron . showWindowEarly === 'object' && ! ! this . config . electron . showWindowEarly . content ;
375+ }
376+
377+ protected getSplashScreenOptions ( ) : SplashScreenOptions | undefined {
378+ if ( this . isShowSplashScreen ( ) ) {
379+ return this . config . electron . showWindowEarly as SplashScreenOptions ;
380+ }
381+ return undefined ;
382+ }
383+
302384 /**
303385 * Use this rather than creating `BrowserWindow` instances from scratch, since some security parameters need to be set, this method will do it.
304386 *
@@ -319,7 +401,7 @@ export class ElectronMainApplication {
319401 return electronWindow . window ;
320402 }
321403
322- async getLastWindowOptions ( ) : Promise < TheiaBrowserWindowOptions > {
404+ getLastWindowOptions ( ) : TheiaBrowserWindowOptions {
323405 const previousWindowState : TheiaBrowserWindowOptions | undefined = this . electronStore . get ( 'windowstate' ) ;
324406 const windowState = previousWindowState ?. screenLayout === this . getCurrentScreenLayout ( )
325407 ? previousWindowState
@@ -365,6 +447,7 @@ export class ElectronMainApplication {
365447 preload : path . resolve ( this . globals . THEIA_APP_PROJECT_PATH , 'lib' , 'frontend' , 'preload.js' ) . toString ( )
366448 } ,
367449 ...this . config . electron ?. windowOptions || { } ,
450+ preventAutomaticShow : this . isShowSplashScreen ( )
368451 } ;
369452 }
370453
@@ -376,20 +459,44 @@ export class ElectronMainApplication {
376459 }
377460
378461 protected async openWindowWithWorkspace ( workspacePath : string ) : Promise < BrowserWindow > {
379- const options = await this . getLastWindowOptions ( ) ;
462+ const options = this . getLastWindowOptions ( ) ;
380463 const [ uri , electronWindow ] = await Promise . all ( [ this . createWindowUri ( ) , this . reuseOrCreateWindow ( options ) ] ) ;
381464 electronWindow . loadURL ( uri . withFragment ( encodeURI ( workspacePath ) ) . toString ( true ) ) ;
382465 return electronWindow ;
383466 }
384467
385468 protected async reuseOrCreateWindow ( asyncOptions : MaybePromise < TheiaBrowserWindowOptions > ) : Promise < BrowserWindow > {
386- if ( ! this . initialWindow ) {
387- return this . createWindow ( asyncOptions ) ;
388- }
469+ const windowPromise = this . initialWindow ? Promise . resolve ( this . initialWindow ) : this . createWindow ( asyncOptions ) ;
389470 // reset initial window after having it re-used once
390- const window = this . initialWindow ;
391471 this . initialWindow = undefined ;
392- return window ;
472+
473+ // hook ready listener to dispose splash screen as configured via min and maximum wait times
474+ if ( this . splashScreenState ) {
475+ windowPromise . then ( window => {
476+ TheiaRendererAPI . onApplicationStateChanged ( window . webContents , state => {
477+ if ( state === 'ready' ) {
478+ this . splashScreenState ?. minTime . then ( ( ) => {
479+ // sanity check (e.g. max time < min time)
480+ if ( this . splashScreenState ) {
481+ window . show ( ) ;
482+ this . splashScreenState . splashScreenWindow ?. close ( ) ;
483+ this . splashScreenState = undefined ;
484+ }
485+ } ) ;
486+ }
487+ } ) ;
488+ this . splashScreenState ?. maxTime . then ( ( ) => {
489+ // check whether splash screen was already disposed
490+ if ( this . splashScreenState ?. splashScreenWindow ) {
491+ window . show ( ) ;
492+ this . splashScreenState . splashScreenWindow ?. close ( ) ;
493+ this . splashScreenState = undefined ;
494+ }
495+ } ) ;
496+ } ) ;
497+ }
498+
499+ return windowPromise ;
393500 }
394501
395502 /** Configures native window creation, i.e. using window.open or links with target "_blank" in the frontend. */
0 commit comments