diff --git a/arduino-ide-extension/src/electron-browser/electron-window-service.ts b/arduino-ide-extension/src/electron-browser/electron-window-service.ts index 199a71a2..e0462206 100644 --- a/arduino-ide-extension/src/electron-browser/electron-window-service.ts +++ b/arduino-ide-extension/src/electron-browser/electron-window-service.ts @@ -1,7 +1,9 @@ -import { inject, injectable } from 'inversify'; +import { inject, injectable, postConstruct } from 'inversify'; import { remote } from 'electron'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { ConnectionStatus, ConnectionStatusService } from '@theia/core/lib/browser/connection-status-service'; import { ElectronWindowService as TheiaElectronWindowService } from '@theia/core/lib/electron-browser/window/electron-window-service'; +import { SplashService } from '../electron-common/splash-service'; @injectable() export class ElectronWindowService extends TheiaElectronWindowService { @@ -9,6 +11,17 @@ export class ElectronWindowService extends TheiaElectronWindowService { @inject(ConnectionStatusService) protected readonly connectionStatusService: ConnectionStatusService; + @inject(SplashService) + protected readonly splashService: SplashService; + + @inject(FrontendApplicationStateService) + protected readonly appStateService: FrontendApplicationStateService; + + @postConstruct() + protected init(): void { + this.appStateService.reachedAnyState('initialized_layout').then(() => this.splashService.requestClose()); + } + protected shouldUnload(): boolean { const offline = this.connectionStatusService.currentStatus === ConnectionStatus.OFFLINE; const detail = offline diff --git a/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts b/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts index 3a07da16..7762c2cb 100644 --- a/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts +++ b/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts @@ -2,6 +2,8 @@ import { ContainerModule } from 'inversify'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { ElectronMainMenuFactory as TheiaElectronMainMenuFactory } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory'; import { ElectronMenuContribution as TheiaElectronMenuContribution } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution' +import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider'; +import { SplashService, splashServicePath } from '../../../electron-common/splash-service'; import { MainMenuManager } from '../../../common/main-menu-manager'; import { ElectronWindowService } from '../../electron-window-service'; import { ElectronMainMenuFactory } from './electron-main-menu-factory'; @@ -15,4 +17,5 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(TheiaElectronMainMenuFactory).toService(ElectronMainMenuFactory); bind(ElectronWindowService).toSelf().inSingletonScope() rebind(WindowService).toService(ElectronWindowService); + bind(SplashService).toDynamicValue(context => ElectronIpcConnectionProvider.createProxy(context.container, splashServicePath)).inSingletonScope(); }); diff --git a/arduino-ide-extension/src/electron-common/splash-service.ts b/arduino-ide-extension/src/electron-common/splash-service.ts new file mode 100644 index 00000000..04a2b671 --- /dev/null +++ b/arduino-ide-extension/src/electron-common/splash-service.ts @@ -0,0 +1,5 @@ +export const splashServicePath = '/services/splash-service'; +export const SplashService = Symbol('SplashService'); +export interface SplashService { + requestClose(): Promise; +} diff --git a/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts b/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts index f8d37ed4..5f4a43e0 100644 --- a/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts +++ b/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts @@ -1,8 +1,17 @@ import { ContainerModule } from 'inversify'; +import { JsonRpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory'; +import { ElectronConnectionHandler } from '@theia/core/lib/electron-common/messaging/electron-connection-handler'; import { ElectronMainApplication as TheiaElectronMainApplication } from '@theia/core/lib/electron-main/electron-main-application'; +import { SplashService, splashServicePath } from '../electron-common/splash-service'; +import { SplashServiceImpl } from './splash/splash-service-impl'; import { ElectronMainApplication } from './theia/electron-main-application'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ElectronMainApplication).toSelf().inSingletonScope(); rebind(TheiaElectronMainApplication).toService(ElectronMainApplication); + + bind(SplashServiceImpl).toSelf().inSingletonScope(); + bind(SplashService).toService(SplashServiceImpl); + bind(ElectronConnectionHandler).toDynamicValue(context => + new JsonRpcConnectionHandler(splashServicePath, () => context.container.get(SplashService))).inSingletonScope(); }); diff --git a/arduino-ide-extension/src/electron-main/splash/splash-screen.ts b/arduino-ide-extension/src/electron-main/splash/splash-screen.ts new file mode 100644 index 00000000..6f395acf --- /dev/null +++ b/arduino-ide-extension/src/electron-main/splash/splash-screen.ts @@ -0,0 +1,172 @@ +/* +MIT License + +Copyright (c) 2017 Troy McKinnon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +// Copied from https://raw.githubusercontent.com/trodi/electron-splashscreen/2f5052a133be021cbf9a438d0ef4719cd1796b75/index.ts + +/** + * Module handles configurable splashscreen to show while app is loading. + */ + +import { Event } from '@theia/core/lib/common/event'; +import { BrowserWindow } from "electron"; + +/** + * When splashscreen was shown. + * @ignore + */ +let splashScreenTimestamp: number = 0; +/** + * Splashscreen is loaded and ready to show. + * @ignore + */ +let splashScreenReady = false; +/** + * Main window has been loading for a min amount of time. + * @ignore + */ +let slowStartup = false; +/** + * True when expected work is complete and we've closed splashscreen, else user prematurely closed splashscreen. + * @ignore + */ +let done = false; +/** + * Show splashscreen if criteria are met. + * @ignore + */ +const showSplash = () => { + if (splashScreen && splashScreenReady && slowStartup) { + splashScreen.show(); + splashScreenTimestamp = Date.now(); + } +}; +/** + * Close splashscreen / show main screen. Ensure screen is visible for a min amount of time. + * @ignore + */ +const closeSplashScreen = (main: Electron.BrowserWindow, min: number): void => { + if (splashScreen) { + const timeout = min - (Date.now() - splashScreenTimestamp); + setTimeout(() => { + done = true; + if (splashScreen) { + splashScreen.isDestroyed() || splashScreen.close(); // Avoid `Error: Object has been destroyed` (#19) + splashScreen = null; + } + if (!main.isDestroyed()) { + main.show(); + } + }, timeout); + } +}; +/** `electron-splashscreen` config object. */ +export interface Config { + /** Options for the window that is loading and having a splashscreen tied to. */ + windowOpts: Electron.BrowserWindowConstructorOptions; + /** + * URL to the splashscreen template. This is the path to an `HTML` or `SVG` file. + * If you want to simply show a `PNG`, wrap it in an `HTML` file. + */ + templateUrl: string; + + /** + * Full set of browser window options for the splashscreen. We override key attributes to + * make it look & feel like a splashscreen; the rest is up to you! + */ + splashScreenOpts: Electron.BrowserWindowConstructorOptions; + /** Number of ms the window will load before splashscreen appears (default: 500ms). */ + delay?: number; + /** Minimum ms the splashscreen will be visible (default: 500ms). */ + minVisible?: number; + /** Close window that is loading if splashscreen is closed by user (default: true). */ + closeWindow?: boolean; +} +/** + * The actual splashscreen browser window. + * @ignore + */ +let splashScreen: Electron.BrowserWindow | null; +/** + * Initializes a splashscreen that will show/hide smartly (and handle show/hiding of main window). + * @param config - Configures splashscreen + * @returns {BrowserWindow} the main browser window ready for loading + */ +export const initSplashScreen = (config: Config, onCloseRequested?: Event): BrowserWindow => { + const xConfig: Required = { + windowOpts: config.windowOpts, + templateUrl: config.templateUrl, + splashScreenOpts: config.splashScreenOpts, + delay: config.delay ?? 500, + minVisible: config.minVisible ?? 500, + closeWindow: config.closeWindow ?? true + }; + xConfig.splashScreenOpts.center = true; + xConfig.splashScreenOpts.frame = false; + xConfig.windowOpts.show = false; + const window = new BrowserWindow(xConfig.windowOpts); + splashScreen = new BrowserWindow(xConfig.splashScreenOpts); + splashScreen.loadURL(`file://${xConfig.templateUrl}`); + xConfig.closeWindow && splashScreen.on("close", () => { + done || window.close(); + }); + // Splashscreen is fully loaded and ready to view. + splashScreen.webContents.on("did-finish-load", () => { + splashScreenReady = true; + showSplash(); + }); + // Startup is taking enough time to show a splashscreen. + setTimeout(() => { + slowStartup = true; + showSplash(); + }, xConfig.delay); + if (onCloseRequested) { + onCloseRequested(() => closeSplashScreen(window, xConfig.minVisible)); + } else { + window.webContents.on('did-finish-load', () => { + closeSplashScreen(window, xConfig.minVisible); + }); + } + window.on('closed', () => closeSplashScreen(window, 0)); // XXX: close splash when main window is closed + return window; +}; +/** Return object for `initDynamicSplashScreen()`. */ +export interface DynamicSplashScreen { + /** The main browser window ready for loading */ + main: BrowserWindow; + /** The splashscreen browser window so you can communicate with splashscreen in more complex use cases. */ + splashScreen: Electron.BrowserWindow; +} +/** + * Initializes a splashscreen that will show/hide smartly (and handle show/hiding of main window). + * Use this function if you need to send/receive info to the splashscreen (e.g., you want to send + * IPC messages to the splashscreen to inform the user of the app's loading state). + * @param config - Configures splashscreen + * @returns {DynamicSplashScreen} the main browser window and the created splashscreen + */ +export const initDynamicSplashScreen = (config: Config): DynamicSplashScreen => { + return { + main: initSplashScreen(config), + // initSplashScreen initializes splashscreen so this is a safe cast. + splashScreen: splashScreen as Electron.BrowserWindow, + }; +}; diff --git a/arduino-ide-extension/src/electron-main/splash/splash-service-impl.ts b/arduino-ide-extension/src/electron-main/splash/splash-service-impl.ts new file mode 100644 index 00000000..d7d095af --- /dev/null +++ b/arduino-ide-extension/src/electron-main/splash/splash-service-impl.ts @@ -0,0 +1,22 @@ +import { injectable } from 'inversify'; +import { Event, Emitter } from '@theia/core/lib/common/event'; +import { SplashService } from '../../electron-common/splash-service'; + +@injectable() +export class SplashServiceImpl implements SplashService { + + protected requested = false; + protected readonly onCloseRequestedEmitter = new Emitter(); + + get onCloseRequested(): Event { + return this.onCloseRequestedEmitter.event; + } + + async requestClose(): Promise { + if (!this.requested) { + this.requested = true; + this.onCloseRequestedEmitter.fire() + } + } + +} diff --git a/arduino-ide-extension/src/electron-main/splash/static/splash.html b/arduino-ide-extension/src/electron-main/splash/static/splash.html new file mode 100644 index 00000000..e372bae3 --- /dev/null +++ b/arduino-ide-extension/src/electron-main/splash/static/splash.html @@ -0,0 +1,25 @@ + + + + + + + + +
+

+
+ + + diff --git a/arduino-ide-extension/src/electron-main/splash/static/splash.png b/arduino-ide-extension/src/electron-main/splash/static/splash.png new file mode 100644 index 00000000..1328005a Binary files /dev/null and b/arduino-ide-extension/src/electron-main/splash/static/splash.png differ diff --git a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts index 7b8d7f42..c096daf6 100644 --- a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts +++ b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts @@ -1,14 +1,23 @@ -import { injectable } from 'inversify'; -import { app } from 'electron'; +import { inject, injectable } from 'inversify'; +import { app, BrowserWindow, BrowserWindowConstructorOptions, screen } from 'electron'; import { fork } from 'child_process'; import { AddressInfo } from 'net'; +import { join } from 'path'; +import { initSplashScreen } from '../splash/splash-screen'; +import { MaybePromise } from '@theia/core/lib/common/types'; import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token'; import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props'; import { ElectronMainApplication as TheiaElectronMainApplication, TheiaBrowserWindowOptions } from '@theia/core/lib/electron-main/electron-main-application'; +import { SplashServiceImpl } from '../splash/splash-service-impl'; @injectable() export class ElectronMainApplication extends TheiaElectronMainApplication { + protected windows: BrowserWindow[] = []; + + @inject(SplashServiceImpl) + protected readonly splashService: SplashServiceImpl; + async start(config: FrontendApplicationConfig): Promise { // Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit") // See: https://github.com/electron-userland/electron-builder/issues/2468 @@ -17,6 +26,62 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { return super.start(config); } + /** + * Use this rather than creating `BrowserWindow` instances from scratch, since some security parameters need to be set, this method will do it. + * + * @param options + */ + async createWindow(asyncOptions: MaybePromise = this.getDefaultBrowserWindowOptions()): Promise { + const options = await asyncOptions; + let electronWindow: BrowserWindow | undefined; + if (this.windows.length) { + electronWindow = new BrowserWindow(options); + } else { + const { bounds } = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()); + const splashHeight = 450; + const splashWidth = 600; + const splashY = Math.floor(bounds.y + (bounds.height - splashHeight) / 2); + const splashX = Math.floor(bounds.x + (bounds.width - splashWidth) / 2); + const splashScreenOpts: BrowserWindowConstructorOptions = { + height: splashHeight, + width: splashWidth, + x: splashX, + y: splashY, + transparent: true, + alwaysOnTop: true, + focusable: false, + minimizable: false, + maximizable: false, + hasShadow: false, + resizable: false + }; + electronWindow = initSplashScreen({ + windowOpts: options, + templateUrl: join(__dirname, '..', '..', '..', 'src', 'electron-main', 'splash', 'static', 'splash.html'), + delay: 0, + minVisible: 2000, + splashScreenOpts + }, this.splashService.onCloseRequested); + } + this.windows.push(electronWindow); + electronWindow.on('closed', () => { + if (electronWindow) { + const index = this.windows.indexOf(electronWindow); + if (index === -1) { + console.warn(`Could not dispose browser window: '${electronWindow.title}'.`); + } else { + this.windows.splice(index, 1); + electronWindow = undefined; + } + } + }) + this.attachReadyToShow(electronWindow); + this.attachSaveWindowState(electronWindow); + this.attachGlobalShortcuts(electronWindow); + this.restoreMaximizedState(electronWindow, options); + return electronWindow; + } + protected async getDefaultBrowserWindowOptions(): Promise { const options = await super.getDefaultBrowserWindowOptions(); return { diff --git a/arduino-ide-extension/tslint.json b/arduino-ide-extension/tslint.json index 55b00628..9e8ab4d7 100644 --- a/arduino-ide-extension/tslint.json +++ b/arduino-ide-extension/tslint.json @@ -7,7 +7,7 @@ "indent": [true, "spaces"], "max-line-length": [true, 180], "no-trailing-whitespace": false, - "no-unused-expression": true, + "no-unused-expression": false, "no-var-keyword": true, "one-line": [true, "check-open-brace",