Skip to content

Commit 1acf13c

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
ATL-732: Support for static splash screen.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
1 parent cff2c95 commit 1acf13c

File tree

10 files changed

+318
-4
lines changed

10 files changed

+318
-4
lines changed

arduino-ide-extension/src/electron-browser/electron-window-service.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
1-
import { inject, injectable } from 'inversify';
1+
import { inject, injectable, postConstruct } from 'inversify';
22
import { remote } from 'electron';
3+
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
34
import { ConnectionStatus, ConnectionStatusService } from '@theia/core/lib/browser/connection-status-service';
45
import { ElectronWindowService as TheiaElectronWindowService } from '@theia/core/lib/electron-browser/window/electron-window-service';
6+
import { SplashService } from '../electron-common/splash-service';
57

68
@injectable()
79
export class ElectronWindowService extends TheiaElectronWindowService {
810

911
@inject(ConnectionStatusService)
1012
protected readonly connectionStatusService: ConnectionStatusService;
1113

14+
@inject(SplashService)
15+
protected readonly splashService: SplashService;
16+
17+
@inject(FrontendApplicationStateService)
18+
protected readonly appStateService: FrontendApplicationStateService;
19+
20+
@postConstruct()
21+
protected init(): void {
22+
this.appStateService.reachedAnyState('initialized_layout').then(() => this.splashService.requestClose());
23+
}
24+
1225
protected shouldUnload(): boolean {
1326
const offline = this.connectionStatusService.currentStatus === ConnectionStatus.OFFLINE;
1427
const detail = offline

arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { ContainerModule } from 'inversify';
22
import { WindowService } from '@theia/core/lib/browser/window/window-service';
33
import { ElectronMainMenuFactory as TheiaElectronMainMenuFactory } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory';
44
import { ElectronMenuContribution as TheiaElectronMenuContribution } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution'
5+
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
6+
import { SplashService, splashServicePath } from '../../../electron-common/splash-service';
57
import { MainMenuManager } from '../../../common/main-menu-manager';
68
import { ElectronWindowService } from '../../electron-window-service';
79
import { ElectronMainMenuFactory } from './electron-main-menu-factory';
@@ -15,4 +17,5 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
1517
rebind(TheiaElectronMainMenuFactory).toService(ElectronMainMenuFactory);
1618
bind(ElectronWindowService).toSelf().inSingletonScope()
1719
rebind(WindowService).toService(ElectronWindowService);
20+
bind(SplashService).toDynamicValue(context => ElectronIpcConnectionProvider.createProxy(context.container, splashServicePath)).inSingletonScope();
1821
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const splashServicePath = '/services/splash-service';
2+
export const SplashService = Symbol('SplashService');
3+
export interface SplashService {
4+
requestClose(): Promise<void>;
5+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import { ContainerModule } from 'inversify';
2+
import { JsonRpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory';
3+
import { ElectronConnectionHandler } from '@theia/core/lib/electron-common/messaging/electron-connection-handler';
24
import { ElectronMainApplication as TheiaElectronMainApplication } from '@theia/core/lib/electron-main/electron-main-application';
5+
import { SplashService, splashServicePath } from '../electron-common/splash-service';
6+
import { SplashServiceImpl } from './splash/splash-service-impl';
37
import { ElectronMainApplication } from './theia/electron-main-application';
48

59
export default new ContainerModule((bind, unbind, isBound, rebind) => {
610
bind(ElectronMainApplication).toSelf().inSingletonScope();
711
rebind(TheiaElectronMainApplication).toService(ElectronMainApplication);
12+
13+
bind(SplashServiceImpl).toSelf().inSingletonScope();
14+
bind(SplashService).toService(SplashServiceImpl);
15+
bind(ElectronConnectionHandler).toDynamicValue(context =>
16+
new JsonRpcConnectionHandler(splashServicePath, () => context.container.get(SplashService))).inSingletonScope();
817
});
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
MIT License
3+
4+
Copyright (c) 2017 Troy McKinnon
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
*/
24+
// Copied from https://raw.githubusercontent.com/trodi/electron-splashscreen/2f5052a133be021cbf9a438d0ef4719cd1796b75/index.ts
25+
26+
/**
27+
* Module handles configurable splashscreen to show while app is loading.
28+
*/
29+
30+
import { Event } from '@theia/core/lib/common/event';
31+
import { BrowserWindow } from "electron";
32+
33+
/**
34+
* When splashscreen was shown.
35+
* @ignore
36+
*/
37+
let splashScreenTimestamp: number = 0;
38+
/**
39+
* Splashscreen is loaded and ready to show.
40+
* @ignore
41+
*/
42+
let splashScreenReady = false;
43+
/**
44+
* Main window has been loading for a min amount of time.
45+
* @ignore
46+
*/
47+
let slowStartup = false;
48+
/**
49+
* True when expected work is complete and we've closed splashscreen, else user prematurely closed splashscreen.
50+
* @ignore
51+
*/
52+
let done = false;
53+
/**
54+
* Show splashscreen if criteria are met.
55+
* @ignore
56+
*/
57+
const showSplash = () => {
58+
if (splashScreen && splashScreenReady && slowStartup) {
59+
splashScreen.show();
60+
splashScreenTimestamp = Date.now();
61+
}
62+
};
63+
/**
64+
* Close splashscreen / show main screen. Ensure screen is visible for a min amount of time.
65+
* @ignore
66+
*/
67+
const closeSplashScreen = (main: Electron.BrowserWindow, min: number): void => {
68+
if (splashScreen) {
69+
const timeout = min - (Date.now() - splashScreenTimestamp);
70+
setTimeout(() => {
71+
done = true;
72+
if (splashScreen) {
73+
splashScreen.isDestroyed() || splashScreen.close(); // Avoid `Error: Object has been destroyed` (#19)
74+
splashScreen = null;
75+
}
76+
if (!main.isDestroyed()) {
77+
main.show();
78+
}
79+
}, timeout);
80+
}
81+
};
82+
/** `electron-splashscreen` config object. */
83+
export interface Config {
84+
/** Options for the window that is loading and having a splashscreen tied to. */
85+
windowOpts: Electron.BrowserWindowConstructorOptions;
86+
/**
87+
* URL to the splashscreen template. This is the path to an `HTML` or `SVG` file.
88+
* If you want to simply show a `PNG`, wrap it in an `HTML` file.
89+
*/
90+
templateUrl: string;
91+
92+
/**
93+
* Full set of browser window options for the splashscreen. We override key attributes to
94+
* make it look & feel like a splashscreen; the rest is up to you!
95+
*/
96+
splashScreenOpts: Electron.BrowserWindowConstructorOptions;
97+
/** Number of ms the window will load before splashscreen appears (default: 500ms). */
98+
delay?: number;
99+
/** Minimum ms the splashscreen will be visible (default: 500ms). */
100+
minVisible?: number;
101+
/** Close window that is loading if splashscreen is closed by user (default: true). */
102+
closeWindow?: boolean;
103+
}
104+
/**
105+
* The actual splashscreen browser window.
106+
* @ignore
107+
*/
108+
let splashScreen: Electron.BrowserWindow | null;
109+
/**
110+
* Initializes a splashscreen that will show/hide smartly (and handle show/hiding of main window).
111+
* @param config - Configures splashscreen
112+
* @returns {BrowserWindow} the main browser window ready for loading
113+
*/
114+
export const initSplashScreen = (config: Config, onCloseRequested?: Event<void>): BrowserWindow => {
115+
const xConfig: Required<Config> = {
116+
windowOpts: config.windowOpts,
117+
templateUrl: config.templateUrl,
118+
splashScreenOpts: config.splashScreenOpts,
119+
delay: config.delay ?? 500,
120+
minVisible: config.minVisible ?? 500,
121+
closeWindow: config.closeWindow ?? true
122+
};
123+
xConfig.splashScreenOpts.center = true;
124+
xConfig.splashScreenOpts.frame = false;
125+
xConfig.windowOpts.show = false;
126+
const window = new BrowserWindow(xConfig.windowOpts);
127+
splashScreen = new BrowserWindow(xConfig.splashScreenOpts);
128+
splashScreen.loadURL(`file://${xConfig.templateUrl}`);
129+
xConfig.closeWindow && splashScreen.on("close", () => {
130+
done || window.close();
131+
});
132+
// Splashscreen is fully loaded and ready to view.
133+
splashScreen.webContents.on("did-finish-load", () => {
134+
splashScreenReady = true;
135+
showSplash();
136+
});
137+
// Startup is taking enough time to show a splashscreen.
138+
setTimeout(() => {
139+
slowStartup = true;
140+
showSplash();
141+
}, xConfig.delay);
142+
if (onCloseRequested) {
143+
onCloseRequested(() => closeSplashScreen(window, xConfig.minVisible));
144+
} else {
145+
window.webContents.on('did-finish-load', () => {
146+
closeSplashScreen(window, xConfig.minVisible);
147+
});
148+
}
149+
window.on('closed', () => closeSplashScreen(window, 0)); // XXX: close splash when main window is closed
150+
return window;
151+
};
152+
/** Return object for `initDynamicSplashScreen()`. */
153+
export interface DynamicSplashScreen {
154+
/** The main browser window ready for loading */
155+
main: BrowserWindow;
156+
/** The splashscreen browser window so you can communicate with splashscreen in more complex use cases. */
157+
splashScreen: Electron.BrowserWindow;
158+
}
159+
/**
160+
* Initializes a splashscreen that will show/hide smartly (and handle show/hiding of main window).
161+
* Use this function if you need to send/receive info to the splashscreen (e.g., you want to send
162+
* IPC messages to the splashscreen to inform the user of the app's loading state).
163+
* @param config - Configures splashscreen
164+
* @returns {DynamicSplashScreen} the main browser window and the created splashscreen
165+
*/
166+
export const initDynamicSplashScreen = (config: Config): DynamicSplashScreen => {
167+
return {
168+
main: initSplashScreen(config),
169+
// initSplashScreen initializes splashscreen so this is a safe cast.
170+
splashScreen: splashScreen as Electron.BrowserWindow,
171+
};
172+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { injectable } from 'inversify';
2+
import { Event, Emitter } from '@theia/core/lib/common/event';
3+
import { SplashService } from '../../electron-common/splash-service';
4+
5+
@injectable()
6+
export class SplashServiceImpl implements SplashService {
7+
8+
protected requested = false;
9+
protected readonly onCloseRequestedEmitter = new Emitter<void>();
10+
11+
get onCloseRequested(): Event<void> {
12+
return this.onCloseRequestedEmitter.event;
13+
}
14+
15+
async requestClose(): Promise<void> {
16+
if (!this.requested) {
17+
this.requested = true;
18+
this.onCloseRequestedEmitter.fire()
19+
}
20+
}
21+
22+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<style>
6+
.container {
7+
width: auto;
8+
text-align: center;
9+
padding: 0px;
10+
}
11+
12+
img {
13+
max-width: 95%;
14+
height: auto;
15+
}
16+
</style>
17+
</head>
18+
19+
<body>
20+
<div class="container">
21+
<p><img src="splash.png"></p>
22+
</div>
23+
</body>
24+
25+
</html>
Loading

arduino-ide-extension/src/electron-main/theia/electron-main-application.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1-
import { injectable } from 'inversify';
2-
import { app } from 'electron';
1+
import { inject, injectable } from 'inversify';
2+
import { app, BrowserWindow, BrowserWindowConstructorOptions, screen } from 'electron';
33
import { fork } from 'child_process';
44
import { AddressInfo } from 'net';
5+
import { join } from 'path';
6+
import { initSplashScreen } from '../splash/splash-screen';
7+
import { MaybePromise } from '@theia/core/lib/common/types';
58
import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token';
69
import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
710
import { ElectronMainApplication as TheiaElectronMainApplication, TheiaBrowserWindowOptions } from '@theia/core/lib/electron-main/electron-main-application';
11+
import { SplashServiceImpl } from '../splash/splash-service-impl';
812

913
@injectable()
1014
export class ElectronMainApplication extends TheiaElectronMainApplication {
1115

16+
protected windows: BrowserWindow[] = [];
17+
18+
@inject(SplashServiceImpl)
19+
protected readonly splashService: SplashServiceImpl;
20+
1221
async start(config: FrontendApplicationConfig): Promise<void> {
1322
// Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit")
1423
// See: https://github.com/electron-userland/electron-builder/issues/2468
@@ -17,6 +26,62 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
1726
return super.start(config);
1827
}
1928

29+
/**
30+
* Use this rather than creating `BrowserWindow` instances from scratch, since some security parameters need to be set, this method will do it.
31+
*
32+
* @param options
33+
*/
34+
async createWindow(asyncOptions: MaybePromise<TheiaBrowserWindowOptions> = this.getDefaultBrowserWindowOptions()): Promise<BrowserWindow> {
35+
const options = await asyncOptions;
36+
let electronWindow: BrowserWindow | undefined;
37+
if (this.windows.length) {
38+
electronWindow = new BrowserWindow(options);
39+
} else {
40+
const { bounds } = screen.getDisplayNearestPoint(screen.getCursorScreenPoint());
41+
const splashHeight = 450;
42+
const splashWidth = 600;
43+
const splashY = Math.floor(bounds.y + (bounds.height - splashHeight) / 2);
44+
const splashX = Math.floor(bounds.x + (bounds.width - splashWidth) / 2);
45+
const splashScreenOpts: BrowserWindowConstructorOptions = {
46+
height: splashHeight,
47+
width: splashWidth,
48+
x: splashX,
49+
y: splashY,
50+
transparent: true,
51+
alwaysOnTop: true,
52+
focusable: false,
53+
minimizable: false,
54+
maximizable: false,
55+
hasShadow: false,
56+
resizable: false
57+
};
58+
electronWindow = initSplashScreen({
59+
windowOpts: options,
60+
templateUrl: join(__dirname, '..', '..', '..', 'src', 'electron-main', 'splash', 'static', 'splash.html'),
61+
delay: 0,
62+
minVisible: 2000,
63+
splashScreenOpts
64+
}, this.splashService.onCloseRequested);
65+
}
66+
this.windows.push(electronWindow);
67+
electronWindow.on('closed', () => {
68+
if (electronWindow) {
69+
const index = this.windows.indexOf(electronWindow);
70+
if (index === -1) {
71+
console.warn(`Could not dispose browser window: '${electronWindow.title}'.`);
72+
} else {
73+
this.windows.splice(index, 1);
74+
electronWindow = undefined;
75+
}
76+
}
77+
})
78+
this.attachReadyToShow(electronWindow);
79+
this.attachSaveWindowState(electronWindow);
80+
this.attachGlobalShortcuts(electronWindow);
81+
this.restoreMaximizedState(electronWindow, options);
82+
return electronWindow;
83+
}
84+
2085
protected async getDefaultBrowserWindowOptions(): Promise<TheiaBrowserWindowOptions> {
2186
const options = await super.getDefaultBrowserWindowOptions();
2287
return {

arduino-ide-extension/tslint.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"indent": [true, "spaces"],
88
"max-line-length": [true, 180],
99
"no-trailing-whitespace": false,
10-
"no-unused-expression": true,
10+
"no-unused-expression": false,
1111
"no-var-keyword": true,
1212
"one-line": [true,
1313
"check-open-brace",

0 commit comments

Comments
 (0)