diff --git a/.travis.yml b/.travis.yml index d0ca55fa95fcb..2efad7e16f948 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ cache: - dev-packages/cli/node_modules - examples/api-samples/node_modules - examples/browser/node_modules + - examples/electron-api-samples/node_modules - examples/electron/node_modules - node_modules - packages/callhierarchy/node_modules diff --git a/configs/root-compilation.tsconfig.json b/configs/root-compilation.tsconfig.json index 313e5e3971fb5..a8557d34ed2fe 100644 --- a/configs/root-compilation.tsconfig.json +++ b/configs/root-compilation.tsconfig.json @@ -141,6 +141,9 @@ }, { "path": "../packages/electron/compile.tsconfig.json" + }, + { + "path": "../examples/electron-api-samples/compile.tsconfig.json" } ] } diff --git a/examples/electron-api-samples/.eslintrc.js b/examples/electron-api-samples/.eslintrc.js new file mode 100644 index 0000000000000..be9cf1a1b3dff --- /dev/null +++ b/examples/electron-api-samples/.eslintrc.js @@ -0,0 +1,10 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: [ + '../../configs/build.eslintrc.json' + ], + parserOptions: { + tsconfigRootDir: __dirname, + project: 'compile.tsconfig.json' + } +}; diff --git a/examples/electron-api-samples/README.md b/examples/electron-api-samples/README.md new file mode 100644 index 0000000000000..10d4ad9658aaa --- /dev/null +++ b/examples/electron-api-samples/README.md @@ -0,0 +1,7 @@ +# Theia - Electron API Examples + +This extension contains examples of how to customize the electron application. + +## License +- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/) +- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp) diff --git a/examples/electron-api-samples/compile.tsconfig.json b/examples/electron-api-samples/compile.tsconfig.json new file mode 100644 index 0000000000000..182372a5b2e9f --- /dev/null +++ b/examples/electron-api-samples/compile.tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../packages/core/compile.tsconfig.json" + }, + { + "path": "../../packages/electron/compile.tsconfig.json" + } + ] +} diff --git a/examples/electron-api-samples/package.json b/examples/electron-api-samples/package.json new file mode 100644 index 0000000000000..7838b9b1baac0 --- /dev/null +++ b/examples/electron-api-samples/package.json @@ -0,0 +1,43 @@ +{ + "private": true, + "name": "@theia/electron-api-samples", + "version": "1.2.0", + "description": "Theia - Example to demonstrate how to customize the electron application", + "dependencies": { + "@theia/core": "^1.2.0", + "@theia/electron": "^1.2.0" + }, + "theiaExtensions": [ + { + "electronMain": "lib/electron-main/update/sample-updater-main-module" + }, + { + "frontendElectron": "lib/electron-browser/updater/sample-updater-frontend-module" + } + ], + "keywords": [ + "theia-extension" + ], + "license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-theia/theia.git" + }, + "bugs": { + "url": "https://github.com/eclipse-theia/theia/issues" + }, + "homepage": "https://github.com/eclipse-theia/theia", + "files": [ + "lib", + "src" + ], + "scripts": { + "lint": "theiaext lint", + "build": "theiaext build", + "watch": "theiaext watch", + "clean": "theiaext clean" + }, + "devDependencies": { + "@theia/ext-scripts": "^1.2.0" + } +} diff --git a/examples/electron-api-samples/src/common/updater/sample-updater.ts b/examples/electron-api-samples/src/common/updater/sample-updater.ts new file mode 100644 index 0000000000000..92daf919dcaea --- /dev/null +++ b/examples/electron-api-samples/src/common/updater/sample-updater.ts @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; + +export enum UpdateStatus { + InProgress = 'in-progress', + Available = 'available', + NotAvailable = 'not-available' +} + +export const SampleUpdaterPath = '/services/sample-updater'; +export const SampleUpdater = Symbol('SampleUpdater'); +export interface SampleUpdater extends JsonRpcServer { + checkForUpdates(): Promise<{ status: UpdateStatus }>; + onRestartToUpdateRequested(): void; + disconnectClient(client: SampleUpdaterClient): void; + + setUpdateAvailable(available: boolean): Promise; // Mock +} + +export const SampleUpdaterClient = Symbol('SampleUpdaterClient'); +export interface SampleUpdaterClient { + notifyReadyToInstall(): void; +} diff --git a/examples/electron-api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts b/examples/electron-api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts new file mode 100644 index 0000000000000..c129c2133f4d0 --- /dev/null +++ b/examples/electron-api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts @@ -0,0 +1,182 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { remote, Menu, BrowserWindow } from 'electron'; +import { inject, injectable, postConstruct } from 'inversify'; +import { isOSX } from '@theia/core/lib/common/os'; +import { CommonMenus } from '@theia/core/lib/browser'; +import { + Emitter, + Command, + MenuPath, + MessageService, + MenuModelRegistry, + MenuContribution, + CommandRegistry, + CommandContribution +} from '@theia/core/lib/common'; +import { ElectronMainMenuFactory } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory'; +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +import { SampleUpdater, UpdateStatus, SampleUpdaterClient } from '../../common/updater/sample-updater'; + +export namespace SampleUpdaterCommands { + + const category = 'Electron Updater Sample'; + + export const CHECK_FOR_UPDATES: Command = { + id: 'electron-sample:check-for-updates', + label: 'Check for Updates...', + category + }; + + export const RESTART_TO_UPDATE: Command = { + id: 'electron-sample:restart-to-update', + label: 'Restart to Update', + category + }; + + // Mock + export const MOCK_UPDATE_AVAILABLE: Command = { + id: 'electron-sample:mock-update-available', + label: 'Mock - Available', + category + }; + + export const MOCK_UPDATE_NOT_AVAILABLE: Command = { + id: 'electron-sample:mock-update-not-available', + label: 'Mock - Not Available', + category + }; + +} + +export namespace SampleUpdaterMenu { + export const MENU_PATH: MenuPath = [...CommonMenus.FILE_SETTINGS_SUBMENU, '3_settings_submenu_update']; +} + +@injectable() +export class SampleUpdaterClientImpl implements SampleUpdaterClient { + + protected readonly onReadyToInstallEmitter = new Emitter(); + readonly onReadyToInstall = this.onReadyToInstallEmitter.event; + + notifyReadyToInstall(): void { + this.onReadyToInstallEmitter.fire(); + } + +} + +// Dynamic menus aren't yet supported by electron: https://github.com/eclipse-theia/theia/issues/446 +@injectable() +export class ElectronMenuUpdater { + + @inject(ElectronMainMenuFactory) + protected readonly factory: ElectronMainMenuFactory; + + public update(): void { + this.setMenu(); + } + + private setMenu(menu: Menu = this.factory.createMenuBar(), electronWindow: BrowserWindow = remote.getCurrentWindow()): void { + if (isOSX) { + remote.Menu.setApplicationMenu(menu); + } else { + electronWindow.setMenu(menu); + } + } + +} + +@injectable() +export class SampleUpdaterFrontendContribution implements CommandContribution, MenuContribution { + + @inject(MessageService) + protected readonly messageService: MessageService; + + @inject(ElectronMenuUpdater) + protected readonly menuUpdater: ElectronMenuUpdater; + + @inject(SampleUpdater) + protected readonly updater: SampleUpdater; + + @inject(SampleUpdaterClientImpl) + protected readonly updaterClient: SampleUpdaterClientImpl; + + protected readyToUpdate = false; + + @postConstruct() + protected init(): void { + this.updaterClient.onReadyToInstall(async () => { + this.readyToUpdate = true; + this.menuUpdater.update(); + this.handleUpdatesAvailable(); + }); + } + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(SampleUpdaterCommands.CHECK_FOR_UPDATES, { + execute: async () => { + const { status } = await this.updater.checkForUpdates(); + switch (status) { + case UpdateStatus.Available: { + this.handleUpdatesAvailable(); + break; + } + case UpdateStatus.NotAvailable: { + const { applicationName } = FrontendApplicationConfigProvider.get(); + this.messageService.info(`[Not Available]: You’re all good. You’ve got the latest version of ${applicationName}.`, { timeout: 3000 }); + break; + } + case UpdateStatus.InProgress: { + this.messageService.warn('[Downloading]: Work in progress...', { timeout: 3000 }); + break; + } + default: throw new Error(`Unexpected status: ${status}`); + } + }, + isEnabled: () => !this.readyToUpdate, + isVisible: () => !this.readyToUpdate + }); + registry.registerCommand(SampleUpdaterCommands.RESTART_TO_UPDATE, { + execute: () => this.updater.onRestartToUpdateRequested(), + isEnabled: () => this.readyToUpdate, + isVisible: () => this.readyToUpdate + }); + registry.registerCommand(SampleUpdaterCommands.MOCK_UPDATE_AVAILABLE, { + execute: () => this.updater.setUpdateAvailable(true) + }); + registry.registerCommand(SampleUpdaterCommands.MOCK_UPDATE_NOT_AVAILABLE, { + execute: () => this.updater.setUpdateAvailable(false) + }); + } + + registerMenus(registry: MenuModelRegistry): void { + registry.registerMenuAction(SampleUpdaterMenu.MENU_PATH, { + commandId: SampleUpdaterCommands.CHECK_FOR_UPDATES.id + }); + registry.registerMenuAction(SampleUpdaterMenu.MENU_PATH, { + commandId: SampleUpdaterCommands.RESTART_TO_UPDATE.id + }); + } + + protected async handleUpdatesAvailable(): Promise { + const answer = await this.messageService.info('[Available]: Found updates, do you want update now?', 'No', 'Yes'); + if (answer === 'Yes') { + this.updater.onRestartToUpdateRequested(); + } + } + +} diff --git a/examples/electron-api-samples/src/electron-browser/updater/sample-updater-frontend-module.ts b/examples/electron-api-samples/src/electron-browser/updater/sample-updater-frontend-module.ts new file mode 100644 index 0000000000000..14e16af65f753 --- /dev/null +++ b/examples/electron-api-samples/src/electron-browser/updater/sample-updater-frontend-module.ts @@ -0,0 +1,34 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { ContainerModule } from 'inversify'; +import { ElectronIpcConnectionProvider } from '@theia/electron/lib/electron-browser/messaging/electron-ipc-connection-provider'; +import { CommandContribution, MenuContribution } from '@theia/core/lib/common'; +import { SampleUpdater, SampleUpdaterPath, SampleUpdaterClient } from '../../common/updater/sample-updater'; +import { SampleUpdaterFrontendContribution, ElectronMenuUpdater, SampleUpdaterClientImpl } from './sample-updater-frontend-contribution'; + +export default new ContainerModule(bind => { + bind(ElectronMenuUpdater).toSelf().inSingletonScope(); + bind(SampleUpdaterClientImpl).toSelf().inSingletonScope(); + bind(SampleUpdaterClient).toService(SampleUpdaterClientImpl); + bind(SampleUpdater).toDynamicValue(context => { + const client = context.container.get(SampleUpdaterClientImpl); + return ElectronIpcConnectionProvider.createProxy(context.container, SampleUpdaterPath, client); + }).inSingletonScope(); + bind(SampleUpdaterFrontendContribution).toSelf().inSingletonScope(); + bind(MenuContribution).toService(SampleUpdaterFrontendContribution); + bind(CommandContribution).toService(SampleUpdaterFrontendContribution); +}); diff --git a/examples/electron-api-samples/src/electron-main/update/sample-updater-impl.ts b/examples/electron-api-samples/src/electron-main/update/sample-updater-impl.ts new file mode 100644 index 0000000000000..9c3bc5875daff --- /dev/null +++ b/examples/electron-api-samples/src/electron-main/update/sample-updater-impl.ts @@ -0,0 +1,87 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable } from 'inversify'; +import { ElectronMainContribution } from '@theia/electron/lib/electron-main/electron-application'; +import { SampleUpdater, SampleUpdaterClient, UpdateStatus } from '../../common/updater/sample-updater'; + +@injectable() +export class SampleUpdaterImpl implements SampleUpdater, ElectronMainContribution { + + protected clients: Array = []; + protected inProgressTimer: NodeJS.Timer | undefined; + protected available = false; + + async checkForUpdates(): Promise<{ status: UpdateStatus }> { + if (this.inProgressTimer) { + return { status: UpdateStatus.InProgress }; + } + return { status: this.available ? UpdateStatus.Available : UpdateStatus.NotAvailable }; + } + + onRestartToUpdateRequested(): void { + console.info('Update to Restart was requested from the client.'); + console.info('Here comes your install and restart implementation. For example: `autoUpdater.quitAndInstall();`'); + } + + async setUpdateAvailable(available: boolean): Promise { + if (this.inProgressTimer) { + clearTimeout(this.inProgressTimer); + } + if (!available) { + this.inProgressTimer = undefined; + this.available = false; + } else { + this.inProgressTimer = setTimeout(() => { + this.inProgressTimer = undefined; + this.available = true; + for (const client of this.clients) { + client.notifyReadyToInstall(); + } + }, 5000); + } + } + + onStart(): void { + console.log('SAMPLE UPDATER: onStart'); + } + + setClient(client: SampleUpdaterClient | undefined): void { + if (client) { + this.clients.push(client); + console.info('Registered a new sample updater client.'); + } else { + console.warn("Couldn't register undefined client."); + } + } + + disconnectClient(client: SampleUpdaterClient): void { + const index = this.clients.indexOf(client); + if (index !== -1) { + this.clients.splice(index, 1); + console.info('Disposed a sample updater client.'); + } else { + console.warn("Couldn't dispose client; it was not registered."); + } + } + + dispose(): void { + console.info('>>> Disposing sample updater service...'); + this.clients.forEach(this.disconnectClient.bind(this)); + console.info('>>> Disposed sample updater service.'); + } + +} diff --git a/examples/electron-api-samples/src/electron-main/update/sample-updater-main-module.ts b/examples/electron-api-samples/src/electron-main/update/sample-updater-main-module.ts new file mode 100644 index 0000000000000..bd0324b8a293e --- /dev/null +++ b/examples/electron-api-samples/src/electron-main/update/sample-updater-main-module.ts @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { ContainerModule } from 'inversify'; +import { JsonRpcConnectionHandler } from '@theia/core/lib/common'; +import { ElectronMainContribution } from '@theia/electron/lib/electron-main/electron-application'; +import { ElectronConnectionHandler } from '@theia/electron/lib/electron-common/messaging/electron-connection-handler'; +import { SampleUpdaterPath, SampleUpdater, SampleUpdaterClient } from '../../common/updater/sample-updater'; +import { SampleUpdaterImpl } from './sample-updater-impl'; + +export default new ContainerModule(bind => { + bind(SampleUpdaterImpl).toSelf().inSingletonScope(); + bind(SampleUpdater).toService(SampleUpdaterImpl); + bind(ElectronMainContribution).toService(SampleUpdater); + bind(ElectronConnectionHandler).toDynamicValue(context => + new JsonRpcConnectionHandler(SampleUpdaterPath, client => { + const server = context.container.get(SampleUpdater); + server.setClient(client); + client.onDidCloseConnection(() => { + console.info('>>> Disconnecting updater client...'); + server.disconnectClient(client); + console.info('<<< Disconnected updater client.'); + }); + return server; + }) + ).inSingletonScope(); +}); diff --git a/examples/electron/compile.tsconfig.json b/examples/electron/compile.tsconfig.json index bf6e50a22c079..faf081a6c365d 100644 --- a/examples/electron/compile.tsconfig.json +++ b/examples/electron/compile.tsconfig.json @@ -127,6 +127,9 @@ }, { "path": "../../packages/electron/compile.tsconfig.json" + }, + { + "path": "../electron-api-samples/compile.tsconfig.json" } ] } diff --git a/examples/electron/package.json b/examples/electron/package.json index e2dddb1908728..405bbf2b15c70 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -22,6 +22,7 @@ "@theia/editor": "^1.2.0", "@theia/editor-preview": "^1.2.0", "@theia/electron": "^1.2.0", + "@theia/electron-api-samples": "^1.2.0", "@theia/file-search": "^1.2.0", "@theia/filesystem": "^1.2.0", "@theia/getting-started": "^1.2.0", diff --git a/packages/electron/src/electron-main/electron-application-module.ts b/packages/electron/src/electron-main/electron-application-module.ts index 16357c038b9cd..c2786c448a19e 100644 --- a/packages/electron/src/electron-main/electron-application-module.ts +++ b/packages/electron/src/electron-main/electron-application-module.ts @@ -33,7 +33,7 @@ const electronSecurityToken: ElectronSecurityToken = { value: v4() }; export default new ContainerModule(bind => { bind(ElectronApplication).toSelf().inSingletonScope(); bind(ElectronMessagingContribution).toSelf().inSingletonScope(); - bind(ElectronSecurityToken).toConstantValue(electronSecurityToken); + bind(ElectronSecurityToken).toConstantValue(electronSecurityToken); bindContributionProvider(bind, ElectronConnectionHandler); bindContributionProvider(bind, ElectronMessagingService.Contribution); diff --git a/tsconfig.json b/tsconfig.json index b89e10cfdc3ad..16b2c8d866cd3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -167,6 +167,9 @@ ], "@theia/electron/lib/*": [ "packages/electron/src/*" + ], + "@theia/electron-api-samples/lib/*": [ + "examples/electron-api-samples/src/*" ] } }