-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat: splash screen support for Electron #13505
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
50bdd62
51a5153
15b86cf
659bc13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<number, TheiaElectronWindow>(); | ||
| 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; | ||
tsmaeder marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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'); | ||
tsmaeder marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| 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<BrowserWindow> { | ||
| 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 => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this the right event or should we just wait until the browser window has finished loading it's content?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea is to wait until the frontend is ready to show real content. Otherwise the splash screen will just be shown less than 500ms before being replaced by the main window showing a loading spinner. You can see this in the video: The splash screen is shown and the main window is shown without any loading spinner (as it's already gone) |
||
| 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; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really need to prevent the main window from showing as soon as it's ready? I don't see the use case and it makes the window state graph more complicated.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not add an "open" call to
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The splash screen was intended as an alternative to the loading spinner of the main window. If we don't prevent the automatic show of the main window we will see the splash screen as well as the full main window at the same time.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sadly, I don't understand what you mean. What is the purpose of that open and when shall it be called?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea would be that the ElectronMainApplication controls visibility of the windows instead of passing flags to the window and letting the window manage it's own visiblity. |
||
| } | ||
|
|
||
| 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(); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.