diff --git a/CHANGELOG.md b/CHANGELOG.md index efee93467bcfd..521f57fb5b130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ [Breaking Changes:](#breaking_changes_1.36.0) - [plugin] renamed `TreeViewExtImpl#toTreeItem()` to `TreeViewExtImpl#toTreeElement()` +- [electron] enabled context isolation and disabled node integration in Electron renderer (https://github.com/eclipse-theia/theia/issues/2018) ## v1.35.0 - 02/23/2023 diff --git a/dev-packages/application-manager/src/generator/frontend-generator.ts b/dev-packages/application-manager/src/generator/frontend-generator.ts index a81d391ef3e3c..2b293c68ceeae 100644 --- a/dev-packages/application-manager/src/generator/frontend-generator.ts +++ b/dev-packages/application-manager/src/generator/frontend-generator.ts @@ -25,6 +25,7 @@ export class FrontendGenerator extends AbstractGenerator { const frontendModules = this.pck.targetFrontendModules; await this.write(this.pck.frontend('index.html'), this.compileIndexHtml(frontendModules)); await this.write(this.pck.frontend('index.js'), this.compileIndexJs(frontendModules)); + await this.write(this.pck.frontend('preload.js'), this.compilePreloadJs()); await this.write(this.pck.frontend('secondary-window.html'), this.compileSecondaryWindowHtml()); await this.write(this.pck.frontend('secondary-index.js'), this.compileSecondaryIndexJs(this.pck.secondaryWindowModules)); if (this.pck.isElectron()) { @@ -133,7 +134,6 @@ module.exports = preloader.preload().then(() => { return `// @ts-check require('reflect-metadata'); -require('@theia/electron/shared/@electron/remote/main').initialize(); // Useful for Electron/NW.js apps as GUI apps on macOS doesn't inherit the \`$PATH\` define // in your dotfiles (.bashrc/.bash_profile/.zshrc/etc). @@ -266,6 +266,17 @@ module.exports = Promise.resolve().then(() => { container.load(frontendApplicationModule); ${compiledModuleImports} }); +`; + } + + compilePreloadJs(): string { + const lines = Array.from(this.pck.preloadModules) + .map(([moduleName, path]) => `require('${path}').preload();`); + const imports = '\n' + lines.join('\n'); + + return `\ +// @ts-check +${imports} `; } } diff --git a/dev-packages/application-manager/src/generator/webpack-generator.ts b/dev-packages/application-manager/src/generator/webpack-generator.ts index a96cee368b91b..69f783b182dbf 100644 --- a/dev-packages/application-manager/src/generator/webpack-generator.ts +++ b/dev-packages/application-manager/src/generator/webpack-generator.ts @@ -103,7 +103,7 @@ module.exports = [{ devtoolModuleFilenameTemplate: 'webpack:///[resource-path]?[loaders]', globalObject: 'self' }, - target: '${this.ifBrowser('web', 'electron-renderer')}', + target: 'web', cache: staticCompression, module: { rules: [ @@ -252,7 +252,7 @@ module.exports = [{ devtoolModuleFilenameTemplate: 'webpack:///[resource-path]?[loaders]', globalObject: 'self' }, - target: 'electron-renderer', + target: 'web', cache: staticCompression, module: { rules: [ @@ -278,6 +278,24 @@ module.exports = [{ warnings: true, children: true } +}, { + mode, + devtool: 'source-map', + entry: { + "preload": path.resolve(__dirname, 'src-gen/frontend/preload.js'), + }, + output: { + filename: '[name].js', + path: outputPath, + devtoolModuleFilenameTemplate: 'webpack:///[resource-path]?[loaders]', + globalObject: 'self' + }, + target: 'electron-preload', + cache: staticCompression, + stats: { + warnings: true, + children: true + } }];`; } diff --git a/dev-packages/application-package/src/application-package.ts b/dev-packages/application-package/src/application-package.ts index c406de8176f0c..28906e16685c6 100644 --- a/dev-packages/application-package/src/application-package.ts +++ b/dev-packages/application-package/src/application-package.ts @@ -96,6 +96,7 @@ export class ApplicationPackage { protected _backendModules: Map | undefined; protected _backendElectronModules: Map | undefined; protected _electronMainModules: Map | undefined; + protected _preloadModules: Map | undefined; protected _extensionPackages: ReadonlyArray | undefined; /** @@ -176,6 +177,13 @@ export class ApplicationPackage { return this._electronMainModules; } + get preloadModules(): Map { + if (!this._preloadModules) { + this._preloadModules = this.computeModules('preload'); + } + return this._preloadModules; + } + protected computeModules

(primary: P, secondary?: S): Map { const result = new Map(); let moduleIndex = 1; diff --git a/dev-packages/application-package/src/extension-package.ts b/dev-packages/application-package/src/extension-package.ts index b654d174992f6..c5ec609d4efc8 100644 --- a/dev-packages/application-package/src/extension-package.ts +++ b/dev-packages/application-package/src/extension-package.ts @@ -26,6 +26,7 @@ export interface Extension { backend?: string; backendElectron?: string; electronMain?: string; + preload?: string; } export interface ExtensionPackageOptions { diff --git a/doc/Migration.md b/doc/Migration.md index ef28721041134..26b91954eadb7 100644 --- a/doc/Migration.md +++ b/doc/Migration.md @@ -20,6 +20,33 @@ For example: } ``` + +### v1.36.0 + +#### Disabled node integration and added context isolation flag in Electron renderer + +This also means that `electron-remote` can no longer be used in components in `electron-frontend` or `electron-common`. In order to use electron-related functionality from the browser, you need to expose an API via a preload script (see https://www.electronjs.org/docs/latest/tutorial/context-isolation). to achieve this from a Theia extension, you need to follow these steps: +1. Define the API interface and declare an api variable on the global `window` variable. See `packages/filesystem/electron-common/electron-api.ts` for an example +2. Write a preload script module that implements the API on the renderer ("browser") side and exposes the API via `exposeInMainWorld`. You'll need to expose the API in an exported function called `preload()`. See `packages/filesystem/electron-browser/preload.ts` for an example. +3. Declare a `theiaExtensions` entry pointing to the preload script like so: +``` +"theiaExtensions": [ + { + "preload": "lib/electron-browser/preload", +``` +See `/packages/filesystem/package.json` for an example + +4. Implement the API on the electron-main side by contributing a `ElectronMainApplicationContribution`. See `packages/filesystem/electron-main/electron-api-main.ts` for an example. If you don't have a module contributing to the electron-main application, you may have to declare it in your package.json. +``` +"theiaExtensions": [ + { + "preload": "lib/electron-browser/preload", + "electronMain": "lib/electron-main/electron-main-module" + } +``` + +If you are using nodejs API in your electron browser-side code you will also have to move the code outside of the renderer process, for exmaple +by setting up an API like described above, or, for example, by using a back-end service. ### v1.35.0 #### Drop support for `Node 14` diff --git a/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts b/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts index 9484a739698ea..2cd55bd0defed 100644 --- a/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts +++ b/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts @@ -15,9 +15,10 @@ // ***************************************************************************** import { injectable, ContainerModule } from '@theia/core/shared/inversify'; -import { CompoundMenuNode, MenuNode } from '@theia/core/lib/common/menu'; +import { MenuNode } from '@theia/core/lib/common/menu'; import { ElectronMainMenuFactory, ElectronMenuOptions } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory'; import { PlaceholderMenuNode } from '../../browser/menu/sample-menu-contribution'; +import { MenuDto } from '@theia/core/lib/electron-common/electron-api'; export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(ElectronMainMenuFactory).to(SampleElectronMainMenuFactory).inSingletonScope(); @@ -25,13 +26,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { @injectable() class SampleElectronMainMenuFactory extends ElectronMainMenuFactory { - protected override fillMenuTemplate( - parentItems: Electron.MenuItemConstructorOptions[], menuModel: MenuNode & CompoundMenuNode, args: unknown[] = [], options: ElectronMenuOptions - ): Electron.MenuItemConstructorOptions[] { - if (menuModel instanceof PlaceholderMenuNode) { - parentItems.push({ label: menuModel.label, enabled: false, visible: true }); + protected override fillMenuTemplate(parentItems: MenuDto[], + menu: MenuNode, + args: unknown[] = [], + options: ElectronMenuOptions + ): MenuDto[] { + if (menu instanceof PlaceholderMenuNode) { + parentItems.push({ label: menu.label, enabled: false, visible: true }); } else { - super.fillMenuTemplate(parentItems, menuModel, args, options); + super.fillMenuTemplate(parentItems, menu, args, options); } return parentItems; } diff --git a/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts b/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts index 5127e31375e89..5a917a3b52f1f 100644 --- a/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts +++ b/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts @@ -14,10 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import * as electronRemote from '@theia/core/electron-shared/@electron/remote'; -import { Menu, BrowserWindow } from '@theia/core/electron-shared/electron'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; -import { isOSX } from '@theia/core/lib/common/os'; import { CommonMenus } from '@theia/core/lib/browser'; import { Emitter, @@ -91,12 +88,8 @@ export class ElectronMenuUpdater { this.setMenu(); } - private setMenu(menu: Menu | null = this.factory.createElectronMenuBar(), electronWindow: BrowserWindow = electronRemote.getCurrentWindow()): void { - if (isOSX) { - electronRemote.Menu.setApplicationMenu(menu); - } else { - electronWindow.setMenu(menu); - } + private setMenu(): void { + window.electronTheiaCore.setMenu(this.factory.createElectronMenuBar()); } } diff --git a/packages/core/README.md b/packages/core/README.md index f69b2870b8606..c46c8bfed633c 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -69,8 +69,6 @@ export class SomeClass { ## Re-Exports - `@theia/core/electron-shared/...` - - `@electron/remote` (from [`@electron/remote@^2.0.1 <2.0.4 || >2.0.4`](https://www.npmjs.com/package/@electron/remote)) - - `@electron/remote/main` (from [`@electron/remote@^2.0.1 <2.0.4 || >2.0.4`](https://www.npmjs.com/package/@electron/remote)) - `native-keymap` (from [`native-keymap@^2.2.1`](https://www.npmjs.com/package/native-keymap)) - `electron` (from [`electron@^15.3.5`](https://www.npmjs.com/package/electron)) - `electron-store` (from [`electron-store@^8.0.0`](https://www.npmjs.com/package/electron-store)) diff --git a/packages/core/electron-shared/@electron/remote/index.d.ts b/packages/core/electron-shared/@electron/remote/index.d.ts deleted file mode 100644 index 146dfc4d862f5..0000000000000 --- a/packages/core/electron-shared/@electron/remote/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@theia/electron/shared/@electron/remote'; diff --git a/packages/core/electron-shared/@electron/remote/index.js b/packages/core/electron-shared/@electron/remote/index.js deleted file mode 100644 index 1a0318e4169e5..0000000000000 --- a/packages/core/electron-shared/@electron/remote/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@theia/electron/shared/@electron/remote'); diff --git a/packages/core/electron-shared/@electron/remote/main/index.d.ts b/packages/core/electron-shared/@electron/remote/main/index.d.ts deleted file mode 100644 index 283d7e0b4d57e..0000000000000 --- a/packages/core/electron-shared/@electron/remote/main/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@theia/electron/shared/@electron/remote/main'; diff --git a/packages/core/electron-shared/@electron/remote/main/index.js b/packages/core/electron-shared/@electron/remote/main/index.js deleted file mode 100644 index ec85b8feab3e2..0000000000000 --- a/packages/core/electron-shared/@electron/remote/main/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@theia/electron/shared/@electron/remote/main'); diff --git a/packages/core/package.json b/packages/core/package.json index 26bcba0c8a8ae..91ecac6265987 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -129,6 +129,9 @@ } }, "theiaExtensions": [ + { + "preload": "lib/electron-browser/preload" + }, { "frontend": "lib/browser/i18n/i18n-frontend-module", "backend": "lib/node/i18n/i18n-backend-module" diff --git a/packages/core/src/electron-browser/electron-clipboard-service.ts b/packages/core/src/electron-browser/electron-clipboard-service.ts index 40a53f6baa94b..79e5c728db7f1 100644 --- a/packages/core/src/electron-browser/electron-clipboard-service.ts +++ b/packages/core/src/electron-browser/electron-clipboard-service.ts @@ -15,7 +15,6 @@ // ***************************************************************************** // eslint-disable-next-line import/no-extraneous-dependencies -import { clipboard } from 'electron'; import { injectable } from 'inversify'; import { ClipboardService } from '../browser/clipboard-service'; @@ -23,11 +22,11 @@ import { ClipboardService } from '../browser/clipboard-service'; export class ElectronClipboardService implements ClipboardService { readText(): string { - return clipboard.readText(); + return window.electronTheiaCore.readClipboard(); } writeText(value: string): void { - clipboard.writeText(value); + window.electronTheiaCore.writeClipboard(value); } } diff --git a/packages/core/src/electron-browser/keyboard/electron-keyboard-layout-change-notifier.ts b/packages/core/src/electron-browser/keyboard/electron-keyboard-layout-change-notifier.ts index 7d38064c5e39c..651e2ecdecdf5 100644 --- a/packages/core/src/electron-browser/keyboard/electron-keyboard-layout-change-notifier.ts +++ b/packages/core/src/electron-browser/keyboard/electron-keyboard-layout-change-notifier.ts @@ -14,7 +14,6 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { ipcRenderer } from '@theia/electron/shared/electron'; import { postConstruct, injectable } from 'inversify'; import { KeyboardLayoutChangeNotifier, NativeKeyboardLayout } from '../../common/keyboard/keyboard-layout-provider'; import { Emitter, Event } from '../../common/event'; @@ -34,7 +33,7 @@ export class ElectronKeyboardLayoutChangeNotifier implements KeyboardLayoutChang @postConstruct() protected initialize(): void { - ipcRenderer.on('keyboardLayoutChanged', (event: Electron.IpcRendererEvent, newLayout: NativeKeyboardLayout) => this.nativeLayoutChanged.fire(newLayout)); + window.electronTheiaCore.onKeyboardLayoutChanged((newLayout: NativeKeyboardLayout) => this.nativeLayoutChanged.fire(newLayout)); } } diff --git a/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts index dad25dca2d646..93e6ed9892f49 100644 --- a/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts +++ b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts @@ -16,7 +16,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import * as electron from '../../../electron-shared/electron'; import { inject, injectable, postConstruct } from 'inversify'; import { ContextMenuRenderer, RenderContextMenuOptions, ContextMenuAccess, FrontendApplicationContribution, CommonCommands, coordinateFromAnchor, PreferenceService @@ -25,12 +24,11 @@ import { ElectronMainMenuFactory } from './electron-main-menu-factory'; import { ContextMenuContext } from '../../browser/menu/context-menu-context'; import { MenuPath, MenuContribution, MenuModelRegistry } from '../../common'; import { BrowserContextMenuRenderer } from '../../browser/menu/browser-context-menu-renderer'; -import { RequestTitleBarStyle, TitleBarStyleAtStartup } from '../../electron-common/messaging/electron-messages'; export class ElectronContextMenuAccess extends ContextMenuAccess { - constructor(readonly menu: electron.Menu) { + constructor(readonly menuHandle: Promise) { super({ - dispose: () => menu.closePopup() + dispose: () => menuHandle.then(handle => window.electronTheiaCore.closePopup(handle)) }); } } @@ -93,10 +91,7 @@ export class ElectronContextMenuRenderer extends BrowserContextMenuRenderer { @postConstruct() protected async init(): Promise { - electron.ipcRenderer.on(TitleBarStyleAtStartup, (_event, style: string) => { - this.useNativeStyle = style === 'native'; - }); - electron.ipcRenderer.send(RequestTitleBarStyle); + this.useNativeStyle = await window.electronTheiaCore.getTitleBarStyleAtStartup() === 'native'; } protected override doRender(options: RenderContextMenuOptions): ContextMenuAccess { @@ -104,17 +99,15 @@ export class ElectronContextMenuRenderer extends BrowserContextMenuRenderer { const { menuPath, anchor, args, onHide, context, contextKeyService } = options; const menu = this.electronMenuFactory.createElectronContextMenu(menuPath, args, context, contextKeyService); const { x, y } = coordinateFromAnchor(anchor); - const zoom = electron.webFrame.getZoomFactor(); - // TODO: Remove the offset once Electron fixes https://github.com/electron/electron/issues/31641 - const offset = process.platform === 'win32' ? 0 : 2; - // x and y values must be Ints or else there is a conversion error - menu.popup({ x: Math.round(x * zoom) + offset, y: Math.round(y * zoom) + offset }); + + const menuHandle = window.electronTheiaCore.popup(menu, x, y, () => { + if (onHide) { + onHide(); + } + }); // native context menu stops the event loop, so there is no keyboard events this.context.resetAltPressed(); - if (onHide) { - menu.once('menu-will-close', () => onHide()); - } - return new ElectronContextMenuAccess(menu); + return new ElectronContextMenuAccess(menuHandle); } else { return super.doRender(options); } diff --git a/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts b/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts index ab99319a32190..723070d0e5115 100644 --- a/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts +++ b/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts @@ -16,7 +16,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import * as electronRemote from '../../../electron-shared/@electron/remote'; import { inject, injectable, postConstruct } from 'inversify'; import { isOSX, MAIN_MENU_BAR, MenuPath, MenuNode, CommandMenuNode, CompoundMenuNode, CompoundMenuNodeRole } from '../../common'; import { Keybinding } from '../../common/keybinding'; @@ -24,7 +23,8 @@ import { PreferenceService, CommonCommands } from '../../browser'; import debounce = require('lodash.debounce'); import { MAXIMIZED_CLASS } from '../../browser/shell/theia-dock-panel'; import { BrowserMainMenuFactory } from '../../browser/menu/browser-menu-plugin'; -import { ContextMatcher } from 'src/browser/context-key-service'; +import { ContextMatcher } from '../../browser/context-key-service'; +import { MenuDto, MenuRole } from '../../electron-common/electron-api'; /** * Representation of possible electron menu options. @@ -68,7 +68,7 @@ export type ElectronMenuItemRole = ('undo' | 'redo' | 'cut' | 'copy' | 'paste' | @injectable() export class ElectronMainMenuFactory extends BrowserMainMenuFactory { - protected _menu?: Electron.Menu; + protected _menu?: MenuDto[]; protected _toggledCommands: Set = new Set(); @inject(PreferenceService) @@ -82,13 +82,13 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { this.setMenuBar(); } if (this._menu) { - for (const item of this._toggledCommands) { - const menuItem = this._menu.getMenuItemById(item); + for (const cmd of this._toggledCommands) { + const menuItem = this.findMenuById(this._menu, cmd); if (menuItem) { - menuItem.checked = this.commandRegistry.isToggled(item); + menuItem.checked = this.commandRegistry.isToggled(cmd); } } - electronRemote.getCurrentWindow().setMenu(this._menu); + window.electronTheiaCore.setMenu(this._menu); } }, 10) ); @@ -99,56 +99,49 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { async setMenuBar(): Promise { await this.preferencesService.ready; - if (isOSX) { - const createdMenuBar = this.createElectronMenuBar(); - electronRemote.Menu.setApplicationMenu(createdMenuBar); - } else if (this.preferencesService.get('window.titleBarStyle') === 'native') { - const createdMenuBar = this.createElectronMenuBar(); - electronRemote.getCurrentWindow().setMenu(createdMenuBar); - } + const createdMenuBar = this.createElectronMenuBar(); + window.electronTheiaCore.setMenu(createdMenuBar); } - createElectronMenuBar(): Electron.Menu | null { + createElectronMenuBar(): MenuDto[] | undefined { const preference = this.preferencesService.get('window.menuBarVisibility') || 'classic'; const maxWidget = document.getElementsByClassName(MAXIMIZED_CLASS); if (preference === 'visible' || (preference === 'classic' && maxWidget.length === 0)) { const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR); - const template = this.fillMenuTemplate([], menuModel, [], { rootMenuPath: MAIN_MENU_BAR }); + this._menu = this.fillMenuTemplate([], menuModel, [], { rootMenuPath: MAIN_MENU_BAR }); if (isOSX) { - template.unshift(this.createOSXMenu()); - } - const menu = electronRemote.Menu.buildFromTemplate(template); - if (!menu) { - throw new Error('menu is null'); + this._menu.unshift(this.createOSXMenu()); } - this._menu = menu; return this._menu; } this._menu = undefined; // eslint-disable-next-line no-null/no-null - return null; + return undefined; } - createElectronContextMenu(menuPath: MenuPath, args?: any[], context?: HTMLElement, contextKeyService?: ContextMatcher): Electron.Menu { + createElectronContextMenu(menuPath: MenuPath, args?: any[], context?: HTMLElement, contextKeyService?: ContextMatcher): MenuDto[] { const menuModel = this.menuProvider.getMenu(menuPath); - const template = this.fillMenuTemplate([], menuModel, args, { showDisabled: false, context, rootMenuPath: menuPath, contextKeyService }); - return electronRemote.Menu.buildFromTemplate(template); + return this.fillMenuTemplate([], menuModel, args, { showDisabled: false, context, rootMenuPath: menuPath, contextKeyService }); } - protected fillMenuTemplate(parentItems: Electron.MenuItemConstructorOptions[], + protected fillMenuTemplate(parentItems: MenuDto[], menu: MenuNode, args: unknown[] = [], options: ElectronMenuOptions - ): Electron.MenuItemConstructorOptions[] { + ): MenuDto[] { const showDisabled = options?.showDisabled !== false; if (CompoundMenuNode.is(menu) && menu.children.length && this.undefinedOrMatch(options.contextKeyService ?? this.contextKeyService, menu.when, options.context)) { const role = CompoundMenuNode.getRole(menu); - if (role === CompoundMenuNodeRole.Group && menu.id === 'inline') { return parentItems; } + if (role === CompoundMenuNodeRole.Group && menu.id === 'inline') { + return parentItems; + } const children = CompoundMenuNode.getFlatChildren(menu.children); - const myItems: Electron.MenuItemConstructorOptions[] = []; + const myItems: MenuDto[] = []; children.forEach(child => this.fillMenuTemplate(myItems, child, args, options)); - if (myItems.length === 0) { return parentItems; } + if (myItems.length === 0) { + return parentItems; + } if (role === CompoundMenuNodeRole.Submenu) { parentItems.push({ label: menu.label, submenu: myItems }); } else if (role === CompoundMenuNodeRole.Group && menu.id !== 'inline') { @@ -183,7 +176,7 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { const accelerator = bindings[0] && this.acceleratorFor(bindings[0]); - const menuItem: Electron.MenuItemConstructorOptions = { + const menuItem: MenuDto = { id: node.id, label: node.label, type: this.commandRegistry.getToggledHandler(commandId, ...args) ? 'checkbox' : 'normal', @@ -191,14 +184,14 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { enabled: true, // https://github.com/eclipse-theia/theia/issues/446 visible: true, accelerator, - click: () => this.execute(commandId, args, options.rootMenuPath) + execute: () => this.execute(commandId, args, options.rootMenuPath) }; if (isOSX) { const role = this.roleFor(node.id); if (role) { menuItem.role = role; - delete menuItem.click; + delete menuItem.execute; } } parentItems.push(menuItem); @@ -235,8 +228,8 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { return this.keybindingRegistry.acceleratorForKeyCode(keyCode, '+', true); } - protected roleFor(id: string): ElectronMenuItemRole | undefined { - let role: ElectronMenuItemRole | undefined; + protected roleFor(id: string): MenuRole | undefined { + let role: MenuRole | undefined; switch (id) { case CommonCommands.UNDO.id: role = 'undo'; @@ -262,18 +255,18 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { return role; } - protected async execute(command: string, args: any[], menuPath: MenuPath): Promise { + protected async execute(cmd: string, args: any[], menuPath: MenuPath): Promise { try { // This is workaround for https://github.com/eclipse-theia/theia/issues/446. // Electron menus do not update based on the `isEnabled`, `isVisible` property of the command. // We need to check if we can execute it. - if (this.menuCommandExecutor.isEnabled(menuPath, command, ...args)) { - await this.menuCommandExecutor.executeCommand(menuPath, command, ...args); - if (this._menu && this.menuCommandExecutor.isVisible(menuPath, command, ...args)) { - const item = this._menu.getMenuItemById(command); + if (this.menuCommandExecutor.isEnabled(menuPath, cmd, ...args)) { + await this.menuCommandExecutor.executeCommand(menuPath, cmd, ...args); + if (this._menu && this.menuCommandExecutor.isVisible(menuPath, cmd, ...args)) { + const item = this.findMenuById(this._menu, cmd); if (item) { - item.checked = this.menuCommandExecutor.isToggled(menuPath, command, ...args); - electronRemote.getCurrentWindow().setMenu(this._menu); + item.checked = this.menuCommandExecutor.isToggled(menuPath, cmd, ...args); + window.electronTheiaCore.setMenu(this._menu); } } } @@ -281,8 +274,22 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { // no-op } } + findMenuById(items: MenuDto[], id: string): MenuDto | undefined { + for (const item of items) { + if (item.id === id) { + return item; + } + if (item.submenu) { + const found = this.findMenuById(item.submenu, id); + if (found) { + return found; + } + } + } + return undefined; + } - protected createOSXMenu(): Electron.MenuItemConstructorOptions { + protected createOSXMenu(): MenuDto { return { label: 'Theia', submenu: [ diff --git a/packages/core/src/electron-browser/menu/electron-menu-contribution.ts b/packages/core/src/electron-browser/menu/electron-menu-contribution.ts index 590a136c449a1..484f9f3e88d3f 100644 --- a/packages/core/src/electron-browser/menu/electron-menu-contribution.ts +++ b/packages/core/src/electron-browser/menu/electron-menu-contribution.ts @@ -14,8 +14,6 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import * as electron from '../../../electron-shared/electron'; -import * as electronRemote from '../../../electron-shared/@electron/remote'; import { inject, injectable, postConstruct } from 'inversify'; import { Command, CommandContribution, CommandRegistry, isOSX, isWindows, MenuModelRegistry, MenuContribution, Disposable, nls } from '../../common'; import { @@ -25,13 +23,13 @@ import { import { ElectronMainMenuFactory } from './electron-main-menu-factory'; import { FrontendApplicationStateService, FrontendApplicationState } from '../../browser/frontend-application-state'; import { FrontendApplicationConfigProvider } from '../../browser/frontend-application-config-provider'; -import { RequestTitleBarStyle, Restart, TitleBarStyleAtStartup, TitleBarStyleChanged } from '../../electron-common/messaging/electron-messages'; import { ZoomLevel } from '../window/electron-window-preferences'; import { BrowserMenuBarContribution } from '../../browser/menu/browser-menu-plugin'; import { WindowService } from '../../browser/window/window-service'; import { WindowTitleService } from '../../browser/window/window-title-service'; import '../../../src/electron-browser/menu/electron-menu-style.css'; +import { MenuDto } from '../../electron-common/electron-api'; export namespace ElectronCommands { export const TOGGLE_DEVELOPER_TOOLS = Command.toDefaultLocalizedCommand({ @@ -131,38 +129,38 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme // OSX: Recreate the menus when changing windows. // OSX only has one menu bar for all windows, so we need to swap // between them as the user switches windows. - const targetWindow = electronRemote.getCurrentWindow(); - const callback = () => this.setMenu(app); - targetWindow.on('focus', callback); - window.addEventListener('unload', () => targetWindow.off('focus', callback)); + const disposeHandler = window.electronTheiaCore.onWindowEvent('focus', () => { + this.setMenu(app); + }); + window.addEventListener('unload', () => disposeHandler.dispose()); } protected attachMenuBarVisibilityListener(): void { this.preferenceService.onPreferenceChanged(e => { if (e.preferenceName === 'window.menuBarVisibility') { - const targetWindow = electronRemote.getCurrentWindow(); - this.handleFullScreen(targetWindow, e.newValue); + this.handleFullScreen(e.newValue); } }); } handleTitleBarStyling(app: FrontendApplication): void { this.hideTopPanel(app); - electron.ipcRenderer.on(TitleBarStyleAtStartup, (_event, style: string) => { + window.electronTheiaCore.getTitleBarStyleAtStartup().then(style => { this.titleBarStyle = style; this.setMenu(app); this.preferenceService.ready.then(() => { this.preferenceService.set('window.titleBarStyle', this.titleBarStyle, PreferenceScope.User); }); }); - electron.ipcRenderer.send(RequestTitleBarStyle); + this.preferenceService.ready.then(() => { - electronRemote.getCurrentWindow().setMenuBarVisibility(['classic', 'visible'].includes(this.preferenceService.get('window.menuBarVisibility', 'classic'))); + window.electronTheiaCore.setMenuBarVisible(['classic', 'visible'].includes(this.preferenceService.get('window.menuBarVisibility', 'classic'))); }); + this.preferenceService.onPreferenceChanged(change => { if (change.preferenceName === 'window.titleBarStyle') { - if (this.titleBarStyleChangeFlag && this.titleBarStyle !== change.newValue && electronRemote.getCurrentWindow().isFocused()) { - electron.ipcRenderer.send(TitleBarStyleChanged, change.newValue); + if (this.titleBarStyleChangeFlag && this.titleBarStyle !== change.newValue) { + window.electronTheiaCore.setTitleBarStyle(change.newValue); this.handleRequiredRestart(); } this.titleBarStyleChangeFlag = true; @@ -197,21 +195,18 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme } } - protected setMenu(app: FrontendApplication, electronMenu: electron.Menu | null = this.factory.createElectronMenuBar(), - electronWindow: electron.BrowserWindow = electronRemote.getCurrentWindow()): void { - if (isOSX) { - electronRemote.Menu.setApplicationMenu(electronMenu); - } else { + protected setMenu(app: FrontendApplication, electronMenu: MenuDto[] | undefined = this.factory.createElectronMenuBar()): void { + if (!isOSX) { this.hideTopPanel(app); if (this.titleBarStyle === 'custom' && !this.menuBar) { - this.createCustomTitleBar(app, electronWindow); + this.createCustomTitleBar(app); + return; } - // Unix/Windows: Set the per-window menus - electronWindow.setMenu(electronMenu); } + window.electronTheiaCore.setMenu(electronMenu); } - protected createCustomTitleBar(app: FrontendApplication, electronWindow: electron.BrowserWindow): void { + protected createCustomTitleBar(app: FrontendApplication): void { const dragPanel = new Widget(); dragPanel.id = 'theia-drag-panel'; app.shell.addWidget(dragPanel, { area: 'top' }); @@ -220,13 +215,13 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme const controls = document.createElement('div'); controls.id = 'window-controls'; controls.append( - this.createControlButton('minimize', () => electronWindow.minimize()), - this.createControlButton('maximize', () => electronWindow.maximize()), - this.createControlButton('restore', () => electronWindow.unmaximize()), - this.createControlButton('close', () => electronWindow.close()) + this.createControlButton('minimize', () => window.electronTheiaCore.minimize()), + this.createControlButton('maximize', () => window.electronTheiaCore.maximize()), + this.createControlButton('restore', () => window.electronTheiaCore.unMaximize()), + this.createControlButton('close', () => window.electronTheiaCore.close()) ); app.shell.topPanel.node.append(controls); - this.handleWindowControls(electronWindow); + this.handleWindowControls(); } protected createCustomTitleWidget(app: FrontendApplication): void { @@ -236,18 +231,14 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme } } - protected handleWindowControls(electronWindow: electron.BrowserWindow): void { - toggleControlButtons(); - electronWindow.on('maximize', toggleControlButtons); - electronWindow.on('unmaximize', toggleControlButtons); + protected handleWindowControls(): void { + window.electronTheiaCore.onWindowEvent('maximize', () => { + document.body.classList.add('maximized'); + }); - function toggleControlButtons(): void { - if (electronWindow.isMaximized()) { - document.body.classList.add('maximized'); - } else { - document.body.classList.remove('maximized'); - } - } + window.electronTheiaCore.onWindowEvent('unmaximize', () => { + document.body.classList.remove('maximized'); + }); } protected createControlButton(id: string, handler: () => void): HTMLElement { @@ -275,22 +266,15 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme }); if (await dialog.open()) { this.windowService.setSafeToShutDown(); - electron.ipcRenderer.send(Restart); + window.electronTheiaCore.restart(); } } registerCommands(registry: CommandRegistry): void { - const currentWindow = electronRemote.getCurrentWindow(); - registry.registerCommand(ElectronCommands.TOGGLE_DEVELOPER_TOOLS, { execute: () => { - const webContent = electronRemote.getCurrentWebContents(); - if (!webContent.isDevToolsOpened()) { - webContent.openDevTools(); - } else { - webContent.closeDevTools(); - } + window.electronTheiaCore.toggleDevTools(); } }); @@ -298,14 +282,14 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme execute: () => this.windowService.reload() }); registry.registerCommand(ElectronCommands.CLOSE_WINDOW, { - execute: () => currentWindow.close() + execute: () => window.electronTheiaCore.close() }); registry.registerCommand(ElectronCommands.ZOOM_IN, { - execute: () => { - const webContents = currentWindow.webContents; + execute: async () => { + const curentLevel = await window.electronTheiaCore.getZoomLevel(); // When starting at a level that is not a multiple of 0.5, increment by at most 0.5 to reach the next highest multiple of 0.5. - let zoomLevel = (Math.floor(webContents.zoomLevel / ZoomLevel.VARIATION) * ZoomLevel.VARIATION) + ZoomLevel.VARIATION; + let zoomLevel = (Math.floor(curentLevel / ZoomLevel.VARIATION) * ZoomLevel.VARIATION) + ZoomLevel.VARIATION; if (zoomLevel > ZoomLevel.MAX) { zoomLevel = ZoomLevel.MAX; return; @@ -314,10 +298,10 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme } }); registry.registerCommand(ElectronCommands.ZOOM_OUT, { - execute: () => { - const webContents = currentWindow.webContents; + execute: async () => { + const curentLevel = await window.electronTheiaCore.getZoomLevel(); // When starting at a level that is not a multiple of 0.5, decrement by at most 0.5 to reach the next lowest multiple of 0.5. - let zoomLevel = (Math.ceil(webContents.zoomLevel / ZoomLevel.VARIATION) * ZoomLevel.VARIATION) - ZoomLevel.VARIATION; + let zoomLevel = (Math.ceil(curentLevel / ZoomLevel.VARIATION) * ZoomLevel.VARIATION) - ZoomLevel.VARIATION; if (zoomLevel < ZoomLevel.MIN) { zoomLevel = ZoomLevel.MIN; return; @@ -328,10 +312,11 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme registry.registerCommand(ElectronCommands.RESET_ZOOM, { execute: () => this.preferenceService.set('window.zoomLevel', ZoomLevel.DEFAULT, PreferenceScope.User) }); + registry.registerCommand(ElectronCommands.TOGGLE_FULL_SCREEN, { - isEnabled: () => currentWindow.isFullScreenable(), - isVisible: () => currentWindow.isFullScreenable(), - execute: () => this.toggleFullScreen(currentWindow) + isEnabled: () => window.electronTheiaCore.isFullScreenable(), + isVisible: () => window.electronTheiaCore.isFullScreenable(), + execute: () => this.toggleFullScreen() }); } @@ -408,16 +393,16 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme }); } - protected toggleFullScreen(currentWindow: electron.BrowserWindow): void { - currentWindow.setFullScreen(!currentWindow.isFullScreen()); + protected toggleFullScreen(): void { + window.electronTheiaCore.toggleFullScreen(); const menuBarVisibility = this.preferenceService.get('window.menuBarVisibility', 'classic'); - this.handleFullScreen(currentWindow, menuBarVisibility); + this.handleFullScreen(menuBarVisibility); } - protected handleFullScreen(currentWindow: electron.BrowserWindow, menuBarVisibility: string): void { - const shouldShowTop = !currentWindow.isFullScreen() || menuBarVisibility === 'visible'; + protected handleFullScreen(menuBarVisibility: string): void { + const shouldShowTop = !window.electronTheiaCore.isFullScreen() || menuBarVisibility === 'visible'; if (this.titleBarStyle === 'native') { - currentWindow.menuBarVisible = shouldShowTop; + window.electronTheiaCore.setMenuBarVisible(shouldShowTop); } else if (shouldShowTop) { this.shell.topPanel.show(); } else { diff --git a/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts b/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts index 7e4410e6be60d..b5eb8f930c3f8 100644 --- a/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts +++ b/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts @@ -14,12 +14,10 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { Event as ElectronEvent, ipcRenderer } from '@theia/electron/shared/electron'; import { injectable, interfaces } from 'inversify'; import { JsonRpcProxy } from '../../common/messaging'; import { AbstractConnectionProvider } from '../../common/messaging/abstract-connection-provider'; -import { THEIA_ELECTRON_IPC_CHANNEL_NAME } from '../../electron-common/messaging/electron-connection-handler'; -import { AbstractChannel, Channel, Disposable, WriteBuffer } from '../../common'; +import { AbstractChannel, Channel, WriteBuffer } from '../../common'; import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '../../common/message-rpc/uint8-array-message-buffer'; export interface ElectronIpcOptions { @@ -50,16 +48,13 @@ export class ElectronIpcRendererChannel extends AbstractChannel { constructor() { super(); - const ipcMessageHandler = (_event: ElectronEvent, data: Uint8Array) => this.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(data)); - ipcRenderer.on(THEIA_ELECTRON_IPC_CHANNEL_NAME, ipcMessageHandler); - this.toDispose.push(Disposable.create(() => ipcRenderer.removeListener(THEIA_ELECTRON_IPC_CHANNEL_NAME, ipcMessageHandler))); + this.toDispose.push(window.electronTheiaCore.onData(data => this.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(data)))); + } getWriteBuffer(): WriteBuffer { const writer = new Uint8ArrayWriteBuffer(); - writer.onCommit(buffer => - ipcRenderer.send(THEIA_ELECTRON_IPC_CHANNEL_NAME, buffer) - ); + writer.onCommit(buffer => window.electronTheiaCore.sendData(buffer)); return writer; } diff --git a/packages/core/src/electron-browser/preload.ts b/packages/core/src/electron-browser/preload.ts new file mode 100644 index 0000000000000..6df8c195e4b56 --- /dev/null +++ b/packages/core/src/electron-browser/preload.ts @@ -0,0 +1,205 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics 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 { Disposable } from '../common/disposable'; +import { StopReason } from '../common/frontend-application-state'; +import { NativeKeyboardLayout } from '../common/keyboard/keyboard-layout-provider'; +import { + CHANNEL_ATTACH_SECURITY_TOKEN, + CHANNEL_FOCUS_WINDOW, CHANNEL_GET_SECURITY_TOKEN, CHANNEL_INVOKE_MENU, CHANNEL_SET_MENU, CHANNEL_OPEN_POPUP, CHANNEL_CLOSE_POPUP, + MenuDto, TheiaCoreAPI, CHANNEL_ON_CLOSE_POPUP, CHANNEL_GET_TITLE_STYLE_AT_STARTUP, WindowEvent, + CHANNEL_MINIMIZE, CHANNEL_MAXIMIZE, CHANNEL_UNMAXIMIZE, CHANNEL_CLOSE, CHANNEL_TOGGLE_DEVTOOLS, + CHANNEL_ON_WINDOW_EVENT, CHANNEL_GET_ZOOM_LEVEL, CHANNEL_SET_ZOOM_LEVEL, CHANNEL_IS_FULL_SCREENABLE, CHANNEL_TOGGLE_FULL_SCREEN, + CHANNEL_IS_FULL_SCREEN, CHANNEL_SET_MENU_BAR_VISIBLE, CHANNEL_REQUEST_CLOSE, CHANNEL_SET_TITLE_STYLE, CHANNEL_RESTART, + CHANNEL_REQUEST_RELOAD, CHANNEL_APP_STATE_CHANGED, CHANNEL_SHOW_ITEM_IN_FOLDER, CHANNEL_READ_CLIPBOARD, CHANNEL_WRITE_CLIPBOARD, + CHANNEL_KEYBOARD_LAYOUT_CHANGED, CHANNEL_IPC_CONNECTION, InternalMenuDto +} from '../electron-common/electron-api'; + +// eslint-disable-next-line import/no-extraneous-dependencies +const { ipcRenderer, contextBridge } = require('electron'); + +// a map of menuId => map handler> +const commandHandlers = new Map void>>(); +let nextHandlerId = 0; +const mainMenuId = 0; +let nextMenuId = mainMenuId + 1; + +function convertMenu(menu: MenuDto[] | undefined, handlerMap: Map void>): InternalMenuDto[] | undefined { + if (!menu) { + return undefined; + } + + return menu.map(item => { + let handlerId = undefined; + if (item.execute) { + handlerId = nextHandlerId++; + handlerMap.set(handlerId, item.execute); + } + + return { + id: item.id, + submenu: convertMenu(item.submenu, handlerMap), + accelerator: item.accelerator, + label: item.label, + handlerId: handlerId, + checked: item.checked, + enabled: item.enabled, + role: item.role, + type: item.type, + visible: item.visible + }; + }); +} + +const api: TheiaCoreAPI = { + setMenuBarVisible: (visible: boolean, windowName?: string) => ipcRenderer.send(CHANNEL_SET_MENU_BAR_VISIBLE, visible, windowName), + setMenu: (menu: MenuDto[] | undefined) => { + commandHandlers.delete(mainMenuId); + const handlers = new Map void>(); + commandHandlers.set(mainMenuId, handlers); + ipcRenderer.send(CHANNEL_SET_MENU, mainMenuId, convertMenu(menu, handlers)); + }, + getSecurityToken: () => ipcRenderer.invoke(CHANNEL_GET_SECURITY_TOKEN), + focusWindow: (name: string) => ipcRenderer.send(CHANNEL_FOCUS_WINDOW, name), + showItemInFolder: fsPath => { + ipcRenderer.send(CHANNEL_SHOW_ITEM_IN_FOLDER, fsPath); + }, + attachSecurityToken: (endpoint: string) => ipcRenderer.invoke(CHANNEL_ATTACH_SECURITY_TOKEN, endpoint), + + popup: async function (menu: MenuDto[], x: number, y: number, onClosed: () => void): Promise { + const menuId = nextMenuId++; + const handlers = new Map void>(); + commandHandlers.set(menuId, handlers); + const handle = await ipcRenderer.invoke(CHANNEL_OPEN_POPUP, menuId, convertMenu(menu, handlers), x, y); + const closeListener = () => { + ipcRenderer.removeListener(CHANNEL_ON_CLOSE_POPUP, closeListener); + commandHandlers.delete(menuId); + onClosed(); + }; + ipcRenderer.on(CHANNEL_ON_CLOSE_POPUP, closeListener); + return handle; + }, + closePopup: function (handle: number): void { + ipcRenderer.send(CHANNEL_CLOSE_POPUP, handle); + }, + getTitleBarStyleAtStartup: function (): Promise { + return ipcRenderer.invoke(CHANNEL_GET_TITLE_STYLE_AT_STARTUP); + }, + setTitleBarStyle: function (style): void { + ipcRenderer.send(CHANNEL_SET_TITLE_STYLE, style); + }, + minimize: function (): void { + ipcRenderer.send(CHANNEL_MINIMIZE); + }, + maximize: function (): void { + ipcRenderer.send(CHANNEL_MAXIMIZE); + }, + unMaximize: function (): void { + ipcRenderer.send(CHANNEL_UNMAXIMIZE); + }, + close: function (): void { + ipcRenderer.send(CHANNEL_CLOSE); + }, + onWindowEvent: function (event: WindowEvent, handler: () => void): Disposable { + const h = (_event: unknown, evt: WindowEvent) => { + if (event === evt) { + handler(); + } + }; + ipcRenderer.on(CHANNEL_ON_WINDOW_EVENT, h); + return Disposable.create(() => ipcRenderer.off(CHANNEL_ON_WINDOW_EVENT, h)); + }, + setCloseRequestHandler: function (handler: (stopReason: StopReason) => Promise): void { + ipcRenderer.on(CHANNEL_REQUEST_CLOSE, async (event, stopReason, confirmChannel, cancelChannel) => { + try { + if (await handler(stopReason)) { + event.sender.send(confirmChannel); + return; + }; + } catch (e) { + console.warn('exception in close handler ', e); + } + event.sender.send(confirmChannel); + }); + }, + + toggleDevTools: function (): void { + ipcRenderer.send(CHANNEL_TOGGLE_DEVTOOLS); + }, + getZoomLevel: function (): Promise { + return ipcRenderer.invoke(CHANNEL_GET_ZOOM_LEVEL); + }, + + setZoomLevel: function (desired: number): void { + ipcRenderer.send(CHANNEL_SET_ZOOM_LEVEL, desired); + }, + isFullScreenable: function (): boolean { + return ipcRenderer.sendSync(CHANNEL_IS_FULL_SCREENABLE); + }, + + isFullScreen: function (): boolean { + return ipcRenderer.sendSync(CHANNEL_IS_FULL_SCREEN); + + }, + toggleFullScreen: function (): void { + ipcRenderer.send(CHANNEL_TOGGLE_FULL_SCREEN); + }, + + requestReload: () => ipcRenderer.send(CHANNEL_REQUEST_RELOAD), + restart: () => ipcRenderer.send(CHANNEL_RESTART), + + applicationStateChanged: state => { + ipcRenderer.send(CHANNEL_APP_STATE_CHANGED, state); + }, + + readClipboard(): string { + return ipcRenderer.sendSync(CHANNEL_READ_CLIPBOARD); + }, + + writeClipboard(text): void { + ipcRenderer.send(CHANNEL_WRITE_CLIPBOARD, text); + }, + + onKeyboardLayoutChanged(handler): Disposable { + return createDisposableListener(CHANNEL_KEYBOARD_LAYOUT_CHANGED, (event, layout) => { handler(layout as NativeKeyboardLayout); }); + }, + + onData: handler => createDisposableListener(CHANNEL_IPC_CONNECTION, (event, data) => { handler(data as Uint8Array); }), + + sendData: data => { + ipcRenderer.send(CHANNEL_IPC_CONNECTION, data); + }, +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function createDisposableListener(channel: string, handler: (event: any, ...args: unknown[]) => any): Disposable { + ipcRenderer.on(channel, handler); + return Disposable.create(() => ipcRenderer.off(channel, handler)); +} + +export function preload(): void { + console.log('exposing theia core electron api'); + ipcRenderer.on(CHANNEL_INVOKE_MENU, (_, menuId: number, handlerId: number) => { + const map = commandHandlers.get(menuId); + if (map) { + const handler = map.get(handlerId); + if (handler) { + handler(); + } + } + }); + + contextBridge.exposeInMainWorld('electronTheiaCore', api); +} diff --git a/packages/core/src/electron-browser/token/electron-token-frontend-module.ts b/packages/core/src/electron-browser/token/electron-token-frontend-module.ts index 4061023ac503c..3dd4e1db83367 100644 --- a/packages/core/src/electron-browser/token/electron-token-frontend-module.ts +++ b/packages/core/src/electron-browser/token/electron-token-frontend-module.ts @@ -14,10 +14,9 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import * as electronRemote from '../../../electron-shared/@electron/remote'; import { ContainerModule } from 'inversify'; import { ElectronSecurityToken } from '../../electron-common/electron-token'; export default new ContainerModule(bind => { - bind(ElectronSecurityToken).toConstantValue(electronRemote.getGlobal(ElectronSecurityToken)); + bind(ElectronSecurityToken).toConstantValue(window.electronTheiaCore.getSecurityToken()); }); diff --git a/packages/core/src/electron-browser/window/electron-frontend-application-state.ts b/packages/core/src/electron-browser/window/electron-frontend-application-state.ts index 6d7b7a34fa13a..d56e80076481e 100644 --- a/packages/core/src/electron-browser/window/electron-frontend-application-state.ts +++ b/packages/core/src/electron-browser/window/electron-frontend-application-state.ts @@ -14,15 +14,13 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { ipcRenderer } from '../../../electron-shared/electron'; import { injectable } from 'inversify'; -import { APPLICATION_STATE_CHANGE_SIGNAL } from '../../electron-common/messaging/electron-messages'; import { FrontendApplicationState, FrontendApplicationStateService } from '../../browser/frontend-application-state'; @injectable() export class ElectronFrontendApplicationStateService extends FrontendApplicationStateService { protected override doSetState(state: FrontendApplicationState): void { super.doSetState(state); - ipcRenderer.send(APPLICATION_STATE_CHANGE_SIGNAL, state); + window.electronTheiaCore.applicationStateChanged(state); } } diff --git a/packages/core/src/electron-browser/window/electron-secondary-window-service.ts b/packages/core/src/electron-browser/window/electron-secondary-window-service.ts index 987420e056fd3..fb0aed2ef831a 100644 --- a/packages/core/src/electron-browser/window/electron-secondary-window-service.ts +++ b/packages/core/src/electron-browser/window/electron-secondary-window-service.ts @@ -14,44 +14,20 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { BrowserWindow } from '../../../electron-shared/electron'; -import * as electronRemote from '../../../electron-shared/@electron/remote'; import { injectable } from 'inversify'; import { DefaultSecondaryWindowService } from '../../browser/window/default-secondary-window-service'; @injectable() export class ElectronSecondaryWindowService extends DefaultSecondaryWindowService { - protected electronWindows: Map = new Map(); - - protected override doCreateSecondaryWindow(onClose?: (closedWin: Window) => void): Window | undefined { - const id = this.nextWindowId(); - electronRemote.getCurrentWindow().webContents.once('did-create-window', newElectronWindow => { - newElectronWindow.setMenuBarVisibility(false); - this.electronWindows.set(id, newElectronWindow); - newElectronWindow.on('closed', () => { - this.electronWindows.delete(id); - const browserWin = this.secondaryWindows.find(w => w.name === id); - if (browserWin) { - this.handleWindowClosed(browserWin, onClose); - } else { - console.warn(`Could not execute proper close handling for secondary window '${id}' because its frontend window could not be found.`); - }; - }); - }); - const win = window.open(DefaultSecondaryWindowService.SECONDARY_WINDOW_URL, id); - return win ?? undefined; + override focus(win: Window): void { + window.electronTheiaCore.focusWindow(win.name); } - override focus(win: Window): void { - // window.name is the target name given to the window.open call as the second parameter. - const electronWindow = this.electronWindows.get(win.name); - if (electronWindow) { - if (electronWindow.isMinimized()) { - electronWindow.restore(); - } - electronWindow.focus(); - } else { - console.warn(`There is no known secondary window '${win.name}'. Thus, the window could not be focussed.`); + protected override doCreateSecondaryWindow(onClose?: (closedWin: Window) => void): Window | undefined { + const w = super.doCreateSecondaryWindow(onClose); + if (w) { + window.electronTheiaCore.setMenuBarVisible(false, w.name); } + return w; } } diff --git a/packages/core/src/electron-browser/window/electron-window-service.ts b/packages/core/src/electron-browser/window/electron-window-service.ts index fd39ba39dfc33..b61a1c771914b 100644 --- a/packages/core/src/electron-browser/window/electron-window-service.ts +++ b/packages/core/src/electron-browser/window/electron-window-service.ts @@ -14,14 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import * as electronRemote from '../../../electron-shared/@electron/remote'; import { injectable, inject, postConstruct } from 'inversify'; -import * as electron from '../../../electron-shared/electron'; import { NewWindowOptions } from '../../common/window'; import { DefaultWindowService } from '../../browser/window/default-window-service'; import { ElectronMainWindowService } from '../../electron-common/electron-main-window-service'; import { ElectronWindowPreferences } from './electron-window-preferences'; -import { CloseRequestArguments, CLOSE_REQUESTED_SIGNAL, RELOAD_REQUESTED_SIGNAL, StopReason } from '../../electron-common/messaging/electron-messages'; @injectable() export class ElectronWindowService extends DefaultWindowService { @@ -62,37 +59,23 @@ export class ElectronWindowService extends DefaultWindowService { } protected override registerUnloadListeners(): void { - electron.ipcRenderer.on(CLOSE_REQUESTED_SIGNAL, (_event, closeRequestEvent: CloseRequestArguments) => this.handleCloseRequestedEvent(closeRequestEvent)); - window.addEventListener('unload', () => this.onUnloadEmitter.fire()); - } - - /** - * Run when ElectronMain detects a `close` event and emits a `close-requested` event. - * Should send an event to `electron.ipcRenderer` on the event's `confirmChannel` if it is safe to exit - * after running FrontendApplication `onWillStop` handlers or on the `cancelChannel` if it is not safe to exit. - */ - protected async handleCloseRequestedEvent(event: CloseRequestArguments): Promise { - const safeToClose = await this.isSafeToShutDown(event.reason); - if (safeToClose) { - console.debug(`Shutting down because of ${StopReason[event.reason]} request.`); - electron.ipcRenderer.send(event.confirmChannel); - } else { - electron.ipcRenderer.send(event.cancelChannel); - } + window.electronTheiaCore.setCloseRequestHandler(reason => this.isSafeToShutDown(reason)); + window.addEventListener('unload', () => { + this.onUnloadEmitter.fire(); + }); } /** * Updates the window zoom level based on the preference value. */ - protected updateWindowZoomLevel(): void { + protected async updateWindowZoomLevel(): Promise { const preferredZoomLevel = this.electronWindowPreferences['window.zoomLevel']; - const webContents = electronRemote.getCurrentWindow().webContents; - if (webContents.getZoomLevel() !== preferredZoomLevel) { - webContents.setZoomLevel(preferredZoomLevel); + if (await window.electronTheiaCore.getZoomLevel() !== preferredZoomLevel) { + window.electronTheiaCore.setZoomLevel(preferredZoomLevel); } } override reload(): void { - electron.ipcRenderer.send(RELOAD_REQUESTED_SIGNAL); + window.electronTheiaCore.requestReload(); } } diff --git a/packages/core/src/electron-common/electron-api.ts b/packages/core/src/electron-common/electron-api.ts new file mode 100644 index 0000000000000..72bc9b46b4096 --- /dev/null +++ b/packages/core/src/electron-common/electron-api.ts @@ -0,0 +1,131 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics 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 { NativeKeyboardLayout } from '../common/keyboard/keyboard-layout-provider'; +import { Disposable } from '../common'; +import { FrontendApplicationState, StopReason } from '../common/frontend-application-state'; + +export type MenuRole = ('undo' | 'redo' | 'cut' | 'copy' | 'paste' | 'selectAll' | 'about' | 'services' | 'hide' | 'hideOthers' | 'unhide' | 'quit'); + +export interface MenuDto { + id?: string, + label?: string, + submenu?: MenuDto[], + type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio'); + checked?: boolean, + enabled?: boolean, + visible?: boolean; + role?: MenuRole; + accelerator?: string, + execute?: () => void +} + +export type InternalMenuDto = Omit & { + submenu?: InternalMenuDto[], + handlerId?: number +}; + +export type WindowEvent = 'maximize' | 'unmaximize' | 'focus'; + +export interface TheiaCoreAPI { + getSecurityToken: () => Promise; + attachSecurityToken: (endpoint: string) => Promise; + + setMenuBarVisible(visible: boolean, windowName?: string): void; + setMenu(menu: MenuDto[] | undefined): void; + + popup(menu: MenuDto[], x: number, y: number, onClosed: () => void): Promise; + closePopup(handle: number): void; + + focusWindow(name: string): void; + + showItemInFolder(fsPath: string): void; + + getTitleBarStyleAtStartup(): Promise; + setTitleBarStyle(style: string): void; + minimize(): void; + maximize(): void; + unMaximize(): void; + close(): void; + onWindowEvent(event: WindowEvent, handler: () => void): Disposable; + setCloseRequestHandler(handler: (reason: StopReason) => Promise): void; + + toggleDevTools(): void; + getZoomLevel(): Promise; + setZoomLevel(desired: number): void; + + isFullScreenable(): boolean; // TODO: this should really be async, since it blocks the renderer process + isFullScreen(): boolean; // TODO: this should really be async, since it blocks the renderer process + toggleFullScreen(): void; + + requestReload(): void; + restart(): void; + + applicationStateChanged(state: FrontendApplicationState): void; + + readClipboard(): string; + writeClipboard(text: string): void; + + onKeyboardLayoutChanged(handler: (newLayout: NativeKeyboardLayout) => void): Disposable; + + sendData(data: Uint8Array): void; + onData(handler: (data: Uint8Array) => void): Disposable; +} + +declare global { + interface Window { + electronTheiaCore: TheiaCoreAPI + } +} + +export const CHANNEL_SET_MENU = 'SetMenu'; +export const CHANNEL_SET_MENU_BAR_VISIBLE = 'SetMenuBarVisible'; +export const CHANNEL_INVOKE_MENU = 'InvokeMenu'; +export const CHANNEL_OPEN_POPUP = 'OpenPopup'; +export const CHANNEL_ON_CLOSE_POPUP = 'OnClosePopup'; +export const CHANNEL_CLOSE_POPUP = 'ClosePopup'; +export const CHANNEL_GET_SECURITY_TOKEN = 'GetSecurityToken'; +export const CHANNEL_FOCUS_WINDOW = 'FocusWindow'; +export const CHANNEL_SHOW_OPEN = 'ShowOpenDialog'; +export const CHANNEL_SHOW_SAVE = 'ShowSaveDialog'; +export const CHANNEL_SHOW_ITEM_IN_FOLDER = 'ShowItemInFolder'; +export const CHANNEL_ATTACH_SECURITY_TOKEN = 'AttachSecurityToken'; + +export const CHANNEL_GET_TITLE_STYLE_AT_STARTUP = 'GetTitleStyleAtAtStartup'; +export const CHANNEL_SET_TITLE_STYLE = 'SetTitleStyle'; +export const CHANNEL_CLOSE = 'Close'; +export const CHANNEL_MINIMIZE = 'Minimize'; +export const CHANNEL_MAXIMIZE = 'Maximize'; +export const CHANNEL_UNMAXIMIZE = 'UnMaximize'; +export const CHANNEL_ON_WINDOW_EVENT = 'OnWindowEvent'; +export const CHANNEL_TOGGLE_DEVTOOLS = 'ToggleDevtools'; +export const CHANNEL_GET_ZOOM_LEVEL = 'GetZoomLevel'; +export const CHANNEL_SET_ZOOM_LEVEL = 'SetZoomLevel'; +export const CHANNEL_IS_FULL_SCREENABLE = 'IsFullScreenable'; +export const CHANNEL_IS_FULL_SCREEN = 'IsFullScreen'; +export const CHANNEL_TOGGLE_FULL_SCREEN = 'ToggleFullScreen'; + +export const CHANNEL_REQUEST_CLOSE = 'RequestClose'; +export const CHANNEL_REQUEST_RELOAD = 'RequestReload'; +export const CHANNEL_RESTART = 'Restart'; + +export const CHANNEL_APP_STATE_CHANGED = 'ApplicationStateChanged'; + +export const CHANNEL_READ_CLIPBOARD = 'ReadClipboard'; +export const CHANNEL_WRITE_CLIPBOARD = 'WriteClipboard'; + +export const CHANNEL_KEYBOARD_LAYOUT_CHANGED = 'KeyboardLayoutChanged'; +export const CHANNEL_IPC_CONNECTION = 'IpcConnection'; diff --git a/packages/core/src/electron-common/messaging/electron-connection-handler.ts b/packages/core/src/electron-common/messaging/electron-connection-handler.ts index a7fe3e9c92b81..f42791b93bf50 100644 --- a/packages/core/src/electron-common/messaging/electron-connection-handler.ts +++ b/packages/core/src/electron-common/messaging/electron-connection-handler.ts @@ -14,17 +14,8 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { ConnectionHandler } from '../../common/messaging/handler'; - -/** - * Name of the channel used with `ipcMain.on/emit`. - */ -export const THEIA_ELECTRON_IPC_CHANNEL_NAME = 'theia-electron-ipc'; - /** * Electron-IPC-specific connection handler. * Use this if you want to establish communication between the frontend and the electron-main process. */ export const ElectronConnectionHandler = Symbol('ElectronConnectionHandler'); -export interface ElectronConnectionHandler extends ConnectionHandler { -} diff --git a/packages/core/src/electron-common/messaging/electron-messages.ts b/packages/core/src/electron-common/messaging/electron-messages.ts deleted file mode 100644 index 53ec84e7edb6d..0000000000000 --- a/packages/core/src/electron-common/messaging/electron-messages.ts +++ /dev/null @@ -1,42 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2021 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 { StopReason } from '../../common/frontend-application-state'; -/** @deprecated @since 1.28 import from common/frontend-application-state instead */ -export { StopReason }; - -export const RequestTitleBarStyle = 'requestTitleBarStyle'; -export const TitleBarStyleChanged = 'titleBarStyleChanged'; -export const TitleBarStyleAtStartup = 'titleBarStyleAtStartup'; -export const Restart = 'restart'; -/** - * Emitted by main when close requested. - */ -export const CLOSE_REQUESTED_SIGNAL = 'close-requested'; -/** - * Emitted by window when a reload is requested. - */ -export const RELOAD_REQUESTED_SIGNAL = 'reload-requested'; -/** - * Emitted by the window when the application changes state - */ -export const APPLICATION_STATE_CHANGE_SIGNAL = 'application-state-changed'; - -export interface CloseRequestArguments { - confirmChannel: string; - cancelChannel: string; - reason: StopReason; -} diff --git a/packages/core/src/electron-main/electron-api-main.ts b/packages/core/src/electron-main/electron-api-main.ts new file mode 100644 index 0000000000000..e41a9b4ce3e60 --- /dev/null +++ b/packages/core/src/electron-main/electron-api-main.ts @@ -0,0 +1,286 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics 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 { + ipcMain, BrowserWindow, Menu, MenuItemConstructorOptions, webContents, WebContents, session, shell, clipboard, IpcMainEvent +} from '@theia/electron/shared/electron'; +import * as nativeKeymap from '@theia/electron/shared/native-keymap'; + +import { inject, injectable } from 'inversify'; +import { FrontendApplicationState, StopReason } from '../common/frontend-application-state'; +import { ElectronSecurityToken } from '../electron-common/electron-token'; +import { + CHANNEL_GET_SECURITY_TOKEN, CHANNEL_SET_MENU, MenuDto, CHANNEL_INVOKE_MENU, CHANNEL_FOCUS_WINDOW, + CHANNEL_ATTACH_SECURITY_TOKEN, CHANNEL_OPEN_POPUP, CHANNEL_ON_CLOSE_POPUP, CHANNEL_CLOSE_POPUP, + CHANNEL_GET_TITLE_STYLE_AT_STARTUP, + CHANNEL_MINIMIZE, + CHANNEL_MAXIMIZE, + CHANNEL_UNMAXIMIZE, + CHANNEL_CLOSE, + CHANNEL_ON_WINDOW_EVENT, + WindowEvent, + CHANNEL_TOGGLE_DEVTOOLS, + CHANNEL_SET_ZOOM_LEVEL, + CHANNEL_GET_ZOOM_LEVEL, + CHANNEL_IS_FULL_SCREENABLE, + CHANNEL_REQUEST_CLOSE, + CHANNEL_RESTART, + CHANNEL_SET_TITLE_STYLE, + CHANNEL_REQUEST_RELOAD, + CHANNEL_APP_STATE_CHANGED, + CHANNEL_SHOW_ITEM_IN_FOLDER, + CHANNEL_READ_CLIPBOARD, + CHANNEL_WRITE_CLIPBOARD, + CHANNEL_IPC_CONNECTION, + CHANNEL_IS_FULL_SCREEN, + InternalMenuDto, + CHANNEL_SET_MENU_BAR_VISIBLE, + CHANNEL_TOGGLE_FULL_SCREEN +} + from '../electron-common/electron-api'; +import { ElectronMainApplication, ElectronMainApplicationContribution } from './electron-main-application'; +import { Disposable, DisposableCollection, isOSX, MaybePromise } from '../common'; +import { createDisposableListener } from './event-utils'; + +@injectable() +export class TheiaMainApi implements ElectronMainApplicationContribution { + @inject(ElectronSecurityToken) + protected electronSecurityToken: ElectronSecurityToken; + + protected readonly openPopups = new Map(); + + onStart(application: ElectronMainApplication): MaybePromise { + // electron security token + ipcMain.handle(CHANNEL_GET_SECURITY_TOKEN, () => this.electronSecurityToken.value); + + ipcMain.handle(CHANNEL_ATTACH_SECURITY_TOKEN, (event, endpoint) => session.defaultSession.cookies.set({ + url: endpoint, + name: ElectronSecurityToken, + value: JSON.stringify(this.electronSecurityToken), + httpOnly: true + })); + + // application menu + ipcMain.on(CHANNEL_SET_MENU, (event, menuId: number, menu: MenuDto[]) => { + let electronMenu: Menu | null; + if (menu) { + electronMenu = Menu.buildFromTemplate(this.fromMenuDto(event.sender, menuId, menu)); + } else { + // eslint-disable-next-line no-null/no-null + electronMenu = null; + } + if (isOSX) { + Menu.setApplicationMenu(electronMenu); + } else { + BrowserWindow.fromWebContents(event.sender)?.setMenu(electronMenu); + } + }); + + ipcMain.on(CHANNEL_SET_MENU_BAR_VISIBLE, (event, visible: boolean, windowName: string | undefined) => { + let electronWindow; + if (windowName) { + electronWindow = BrowserWindow.getAllWindows().find(win => win.webContents.mainFrame.name === windowName); + } else { + electronWindow = BrowserWindow.fromWebContents(event.sender); + } + if (electronWindow) { + electronWindow.setMenuBarVisibility(visible); + } else { + console.warn(`There is no known secondary window '${windowName}'. Thus, the menu bar could not be made visible.`); + } + }); + + // popup menu + ipcMain.handle(CHANNEL_OPEN_POPUP, (event, menuId, menu, x, y) => { + const zoom = event.sender.getZoomFactor(); + // TODO: Remove the offset once Electron fixes https://github.com/electron/electron/issues/31641 + const offset = process.platform === 'win32' ? 0 : 2; + // x and y values must be Ints or else there is a conversion error + x = Math.round(x * zoom) + offset; + y = Math.round(y * zoom) + offset; + const popup = Menu.buildFromTemplate(this.fromMenuDto(event.sender, menuId, menu)); + this.openPopups.set(menuId, popup); + popup.popup({ + callback: () => { + this.openPopups.delete(menuId); + event.sender.send(CHANNEL_ON_CLOSE_POPUP, menuId); + } + }); + }); + + ipcMain.handle(CHANNEL_CLOSE_POPUP, (event, handle) => { + if (this.openPopups.has(handle)) { + this.openPopups.get(handle)!.closePopup(); + } + }); + + // focus windows for secondary window support + ipcMain.on(CHANNEL_FOCUS_WINDOW, (event, windowName) => { + const electronWindow = BrowserWindow.getAllWindows().find(win => win.webContents.mainFrame.name === windowName); + if (electronWindow) { + if (electronWindow.isMinimized()) { + electronWindow.restore(); + } + electronWindow.focus(); + } else { + console.warn(`There is no known secondary window '${windowName}'. Thus, the window could not be focussed.`); + } + }); + + ipcMain.on(CHANNEL_SHOW_ITEM_IN_FOLDER, (event, fsPath) => { + shell.showItemInFolder(fsPath); + }); + + ipcMain.handle(CHANNEL_GET_TITLE_STYLE_AT_STARTUP, event => application.getTitleBarStyleAtStartup(event.sender)); + + ipcMain.on(CHANNEL_SET_TITLE_STYLE, (event, style) => application.setTitleBarStyle(event.sender, style)); + + ipcMain.on(CHANNEL_MINIMIZE, event => { + BrowserWindow.fromWebContents(event.sender)?.minimize(); + }); + + ipcMain.on(CHANNEL_MAXIMIZE, event => { + BrowserWindow.fromWebContents(event.sender)?.maximize(); + }); + + ipcMain.on(CHANNEL_UNMAXIMIZE, event => { + BrowserWindow.fromWebContents(event.sender)?.unmaximize(); + }); + + ipcMain.on(CHANNEL_CLOSE, event => { + BrowserWindow.fromWebContents(event.sender)?.close(); + }); + + ipcMain.on(CHANNEL_RESTART, event => { + application.restart(event.sender); + }); + + ipcMain.on(CHANNEL_TOGGLE_DEVTOOLS, event => { + event.sender.toggleDevTools(); + }); + + ipcMain.on(CHANNEL_SET_ZOOM_LEVEL, (event, zoomLevel: number) => { + event.sender.setZoomLevel(zoomLevel); + }); + + ipcMain.handle(CHANNEL_GET_ZOOM_LEVEL, event => event.sender.getZoomLevel()); + + ipcMain.on(CHANNEL_TOGGLE_FULL_SCREEN, event => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + win.setFullScreen(!win.isFullScreen()); + } + }); + ipcMain.on(CHANNEL_IS_FULL_SCREENABLE, event => { + event.returnValue = BrowserWindow.fromWebContents(event.sender)?.isFullScreenable(); + }); + + ipcMain.on(CHANNEL_IS_FULL_SCREEN, event => { + event.returnValue = BrowserWindow.fromWebContents(event.sender)?.isFullScreen(); + }); + + ipcMain.on(CHANNEL_READ_CLIPBOARD, event => { + event.returnValue = clipboard.readText(); + }); + ipcMain.on(CHANNEL_WRITE_CLIPBOARD, (event, text) => { + clipboard.writeText(text); + }); + + nativeKeymap.onDidChangeKeyboardLayout(() => { + const newLayout = { + info: nativeKeymap.getCurrentKeyboardLayout(), + mapping: nativeKeymap.getKeyMap() + }; + for (const webContent of webContents.getAllWebContents()) { + webContent.send('keyboardLayoutChanged', newLayout); + } + }); + } + + fromMenuDto(sender: WebContents, menuId: number, menuDto: InternalMenuDto[]): MenuItemConstructorOptions[] { + return menuDto.map(dto => { + + const result: MenuItemConstructorOptions = { + id: dto.id, + label: dto.label, + type: dto.type, + checked: dto.checked, + enabled: dto.enabled, + visible: dto.visible, + role: dto.role, + accelerator: dto.accelerator + }; + if (dto.submenu) { + result.submenu = this.fromMenuDto(sender, menuId, dto.submenu); + } + if (dto.handlerId) { + result.click = () => { + sender.send(CHANNEL_INVOKE_MENU, menuId, dto.handlerId); + }; + } + return result; + }); + } +} + +let nextReplyChannel: number = 0; + +export namespace TheiaRendererAPI { + export function sendWindowEvent(wc: WebContents, event: WindowEvent): void { + wc.send(CHANNEL_ON_WINDOW_EVENT, event); + } + + export function requestClose(wc: WebContents, stopReason: StopReason): Promise { + const channelNr = nextReplyChannel++; + const confirmChannel = `confirm-${channelNr}`; + const cancelChannel = `confirm-${channelNr}`; + const disposables = new DisposableCollection(); + + return new Promise(resolve => { + wc.send(CHANNEL_REQUEST_CLOSE, stopReason, confirmChannel, cancelChannel); + createDisposableListener(ipcMain, confirmChannel, e => { + resolve(true); + }, disposables); + createDisposableListener(ipcMain, cancelChannel, e => { + resolve(false); + }, disposables); + }).finally(() => disposables.dispose()); + } + + export function onRequestReload(wc: WebContents, handler: () => void): Disposable { + return createWindowListener(wc, CHANNEL_REQUEST_RELOAD, handler); + } + + export function onApplicationStateChanged(wc: WebContents, handler: (state: FrontendApplicationState) => void): Disposable { + return createWindowListener(wc, CHANNEL_APP_STATE_CHANGED, (event, state) => handler(state as FrontendApplicationState)); + } + + export function onIpcData(handler: (sender: WebContents, data: Uint8Array) => void): Disposable { + return createDisposableListener(ipcMain, CHANNEL_IPC_CONNECTION, (event, data) => handler(event.sender, data as Uint8Array)); + } + + export function sendData(wc: WebContents, data: Uint8Array): void { + wc.send(CHANNEL_IPC_CONNECTION, data); + } + + function createWindowListener(wc: WebContents, channel: string, handler: (...args: unknown[]) => unknown): Disposable { + return createDisposableListener(ipcMain, channel, event => { + if (wc.id === event.sender.id) { + handler(); + } + }); + } +} diff --git a/packages/core/src/electron-main/electron-main-application-module.ts b/packages/core/src/electron-main/electron-main-application-module.ts index 1b8a52a7bb7f8..c41d640dba26c 100644 --- a/packages/core/src/electron-main/electron-main-application-module.ts +++ b/packages/core/src/electron-main/electron-main-application-module.ts @@ -27,7 +27,7 @@ import { ElectronMessagingService } from './messaging/electron-messaging-service import { ElectronConnectionHandler } from '../electron-common/messaging/electron-connection-handler'; import { ElectronSecurityTokenService } from './electron-security-token-service'; import { TheiaBrowserWindowOptions, TheiaElectronWindow, TheiaElectronWindowFactory, WindowApplicationConfig } from './theia-electron-window'; -import { ElectronNativeKeymap } from './electron-native-keymap'; +import { TheiaMainApi } from './electron-api-main'; const electronSecurityToken: ElectronSecurityToken = { value: v4() }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -44,6 +44,7 @@ export default new ContainerModule(bind => { bindContributionProvider(bind, ElectronMainApplicationContribution); bind(ElectronMainApplicationContribution).toService(ElectronMessagingContribution); + bind(ElectronMainApplicationContribution).to(TheiaMainApi).inSingletonScope(); bind(ElectronMainWindowService).to(ElectronMainWindowServiceImpl).inSingletonScope(); bind(ElectronConnectionHandler).toDynamicValue(context => @@ -60,7 +61,4 @@ export default new ContainerModule(bind => { child.bind(WindowApplicationConfig).toConstantValue(config); return child.get(TheiaElectronWindow); }); - - bind(ElectronNativeKeymap).toSelf().inSingletonScope(); - bind(ElectronMainApplicationContribution).toService(ElectronNativeKeymap); }); diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index 1a4d6d560fb92..b261a904ab228 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -15,8 +15,7 @@ // ***************************************************************************** import { inject, injectable, named } from 'inversify'; -import * as electronRemoteMain from '../../electron-shared/@electron/remote/main'; -import { screen, ipcMain, app, BrowserWindow, Event as ElectronEvent, BrowserWindowConstructorOptions, nativeImage } from '../../electron-shared/electron'; +import { screen, app, BrowserWindow, WebContents, Event as ElectronEvent, BrowserWindowConstructorOptions, nativeImage } from '../../electron-shared/electron'; import * as path from 'path'; import { Argv } from 'yargs'; import { AddressInfo } from 'net'; @@ -32,16 +31,12 @@ 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 { - RequestTitleBarStyle, - Restart, StopReason, - TitleBarStyleAtStartup, - TitleBarStyleChanged -} from '../electron-common/messaging/electron-messages'; import { DEFAULT_WINDOW_HASH } from '../common/window'; import { TheiaBrowserWindowOptions, TheiaElectronWindow, TheiaElectronWindowFactory } from './theia-electron-window'; import { ElectronMainApplicationGlobals } from './electron-main-constants'; import { createDisposableListener } from './event-utils'; +import { TheiaRendererAPI } from './electron-api-main'; +import { StopReason } from '../common/frontend-application-state'; export { ElectronMainApplicationGlobals }; @@ -156,7 +151,6 @@ export namespace ElectronMainProcessArgv { @injectable() export class ElectronMainApplication { - @inject(ContributionProvider) @named(ElectronMainApplicationContribution) protected readonly contributions: ContributionProvider; @@ -229,6 +223,24 @@ export class ElectronMainApplication { return isWindows ? 'custom' : 'native'; } + public setTitleBarStyle(webContents: WebContents, style: string): void { + this.useNativeWindowFrame = isOSX || style === 'native'; + const browserWindow = BrowserWindow.fromWebContents(webContents); + if (browserWindow) { + this.saveWindowState(browserWindow); + } else { + console.warn(`no BrowserWindow with id: ${webContents.id}`); + } + } + + /** + * @param id the id of the WebContents of the BrowserWindow in question + * @returns 'native' or 'custom' + */ + getTitleBarStyleAtStartup(webContents: WebContents): 'native' | 'custom' { + return this.didUseNativeWindowFrameOnStart.get(webContents.id) ? 'native' : 'custom'; + } + protected async launch(params: ElectronMainExecutionParams): Promise { createYargs(params.argv, params.cwd) .command('$0 [file]', false, @@ -247,11 +259,13 @@ export class ElectronMainApplication { let options = await asyncOptions; options = this.avoidOverlap(options); const electronWindow = this.windowFactory(options, this.config); - const { window: { id } } = electronWindow; + const id = electronWindow.window.webContents.id; this.windows.set(id, electronWindow); electronWindow.onDidClose(() => this.windows.delete(id)); + electronWindow.window.on('maximize', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'maximize')); + electronWindow.window.on('unmaximize', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'unmaximize')); + electronWindow.window.on('focus', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'focus')); this.attachSaveWindowState(electronWindow.window); - electronRemoteMain.enable(electronWindow.window.webContents); this.configureNativeSecondaryWindowCreation(electronWindow.window); return electronWindow.window; } @@ -292,12 +306,14 @@ export class ElectronMainApplication { minHeight: 120, webPreferences: { // `global` is undefined when `true`. - contextIsolation: false, + contextIsolation: true, + sandbox: false, // https://github.com/eclipse-theia/theia/issues/2018 - nodeIntegration: true, + nodeIntegration: false, // Setting the following option to `true` causes some features to break, somehow. // Issue: https://github.com/eclipse-theia/theia/issues/8577 nodeIntegrationInWorker: false, + preload: path.resolve(this.globals.THEIA_APP_PROJECT_PATH, 'lib/preload.js').toString() }, ...this.config.electron?.windowOptions || {}, }; @@ -418,8 +434,8 @@ export class ElectronMainApplication { }, windowStateListeners); createDisposableListener(electronWindow, 'resize', saveWindowStateDelayed, windowStateListeners); createDisposableListener(electronWindow, 'move', saveWindowStateDelayed, windowStateListeners); - windowStateListeners.push(Disposable.create(() => { try { this.didUseNativeWindowFrameOnStart.delete(electronWindow.id); } catch { } })); - this.didUseNativeWindowFrameOnStart.set(electronWindow.id, this.useNativeWindowFrame); + windowStateListeners.push(Disposable.create(() => { try { this.didUseNativeWindowFrameOnStart.delete(electronWindow.webContents.id); } catch { } })); + this.didUseNativeWindowFrameOnStart.set(electronWindow.webContents.id, this.useNativeWindowFrame); electronWindow.once('closed', () => windowStateListeners.dispose()); } @@ -532,24 +548,6 @@ export class ElectronMainApplication { app.on('will-quit', this.onWillQuit.bind(this)); app.on('second-instance', this.onSecondInstance.bind(this)); app.on('window-all-closed', this.onWindowAllClosed.bind(this)); - - ipcMain.on(TitleBarStyleChanged, ({ sender }, titleBarStyle: string) => { - this.useNativeWindowFrame = isOSX || titleBarStyle === 'native'; - const browserWindow = BrowserWindow.fromId(sender.id); - if (browserWindow) { - this.saveWindowState(browserWindow); - } else { - console.warn(`no BrowserWindow with id: ${sender.id}`); - } - }); - - ipcMain.on(Restart, ({ sender }) => { - this.restart(sender.id); - }); - - ipcMain.on(RequestTitleBarStyle, ({ sender }) => { - sender.send(TitleBarStyleAtStartup, this.didUseNativeWindowFrameOnStart.get(sender.id) ? 'native' : 'custom'); - }); } protected onWillQuit(event: ElectronEvent): void { @@ -573,10 +571,9 @@ export class ElectronMainApplication { } } - protected async restart(id: number): Promise { + public async restart(webContents: WebContents): Promise { this.restarting = true; - const window = BrowserWindow.fromId(id); - const wrapper = this.windows.get(window?.id as number); // If it's not a number, we won't get anything. + const wrapper = this.windows.get(webContents.id); if (wrapper) { const listener = wrapper.onDidClose(async () => { listener.dispose(); diff --git a/packages/core/src/electron-main/electron-native-keymap.ts b/packages/core/src/electron-main/electron-native-keymap.ts deleted file mode 100644 index 92e9323c5b628..0000000000000 --- a/packages/core/src/electron-main/electron-native-keymap.ts +++ /dev/null @@ -1,40 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2020 Ericsson 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 { webContents } from '@theia/electron/shared/electron'; -import * as nativeKeymap from '@theia/electron/shared/native-keymap'; -import { injectable } from 'inversify'; -import { ElectronMainApplication, ElectronMainApplicationContribution } from './electron-main-application'; - -@injectable() -export class ElectronNativeKeymap implements ElectronMainApplicationContribution { - - /** - * Notify all renderer processes on keyboard layout change. - */ - onStart(application: ElectronMainApplication): void { - nativeKeymap.onDidChangeKeyboardLayout(() => { - const newLayout = { - info: nativeKeymap.getCurrentKeyboardLayout(), - mapping: nativeKeymap.getKeyMap() - }; - for (const webContent of webContents.getAllWebContents()) { - webContent.send('keyboardLayoutChanged', newLayout); - } - }); - } - -} diff --git a/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts b/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts index b778598aea0e8..69f89add71132 100644 --- a/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts +++ b/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts @@ -14,16 +14,17 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { IpcMainEvent, ipcMain, WebContents } from '@theia/electron/shared/electron'; +import { WebContents } from '@theia/electron/shared/electron'; import { inject, injectable, named, postConstruct } from 'inversify'; import { ContributionProvider } from '../../common/contribution-provider'; import { MessagingContribution } from '../../node/messaging/messaging-contribution'; -import { ElectronConnectionHandler, THEIA_ELECTRON_IPC_CHANNEL_NAME } from '../../electron-common/messaging/electron-connection-handler'; +import { ElectronConnectionHandler } from '../../electron-common/messaging/electron-connection-handler'; import { ElectronMainApplicationContribution } from '../electron-main-application'; import { ElectronMessagingService } from './electron-messaging-service'; import { AbstractChannel, Channel, ChannelMultiplexer, MessageProvider } from '../../common/message-rpc/channel'; -import { Emitter, WriteBuffer } from '../../common'; +import { ConnectionHandler, Emitter, WriteBuffer } from '../../common'; import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '../../common/message-rpc/uint8-array-message-buffer'; +import { TheiaRendererAPI } from '../electron-api-main'; /** * This component replicates the role filled by `MessagingContribution` but for Electron. @@ -40,7 +41,7 @@ export class ElectronMessagingContribution implements ElectronMainApplicationCon protected readonly messagingContributions: ContributionProvider; @inject(ContributionProvider) @named(ElectronConnectionHandler) - protected readonly connectionHandlers: ContributionProvider; + protected readonly connectionHandlers: ContributionProvider; protected readonly channelHandlers = new MessagingContribution.ConnectionHandlers(); /** @@ -50,13 +51,10 @@ export class ElectronMessagingContribution implements ElectronMainApplicationCon @postConstruct() protected init(): void { - ipcMain.on(THEIA_ELECTRON_IPC_CHANNEL_NAME, (event: IpcMainEvent, data: Uint8Array) => { - this.handleIpcEvent(event, data); - }); + TheiaRendererAPI.onIpcData((sender, data) => this.handleIpcEvent(sender, data)); } - protected handleIpcEvent(event: IpcMainEvent, data: Uint8Array): void { - const sender = event.sender; + protected handleIpcEvent(sender: WebContents, data: Uint8Array): void { // Get the multiplexer for a given window id try { const windowChannelData = this.windowChannelMultiplexer.get(sender.id) ?? this.createWindowChannelData(sender); @@ -133,7 +131,7 @@ export class ElectronWebContentChannel extends AbstractChannel { writer.onCommit(buffer => { if (!this.sender.isDestroyed()) { - this.sender.send(THEIA_ELECTRON_IPC_CHANNEL_NAME, buffer); + TheiaRendererAPI.sendData(this.sender, buffer); } }); diff --git a/packages/core/src/electron-main/theia-electron-window.ts b/packages/core/src/electron-main/theia-electron-window.ts index 9ac8e7c9a2acd..a61be0ac2b5d8 100644 --- a/packages/core/src/electron-main/theia-electron-window.ts +++ b/packages/core/src/electron-main/theia-electron-window.ts @@ -15,15 +15,15 @@ // ***************************************************************************** import { FrontendApplicationConfig } from '@theia/application-package'; -import { FrontendApplicationState } from '../common/frontend-application-state'; -import { APPLICATION_STATE_CHANGE_SIGNAL, CLOSE_REQUESTED_SIGNAL, RELOAD_REQUESTED_SIGNAL, StopReason } from '../electron-common/messaging/electron-messages'; -import { BrowserWindow, BrowserWindowConstructorOptions, ipcMain, IpcMainEvent } from '../../electron-shared/electron'; +import { FrontendApplicationState, StopReason } from '../common/frontend-application-state'; +import { BrowserWindow, BrowserWindowConstructorOptions } from '../../electron-shared/electron'; import { inject, injectable, postConstruct } from '../../shared/inversify'; import { ElectronMainApplicationGlobals } from './electron-main-constants'; import { DisposableCollection, Emitter, Event } from '../common'; import { createDisposableListener } from './event-utils'; import { URI } from '../common/uri'; import { FileUri } from '../node/file-uri'; +import { TheiaRendererAPI } from './electron-api-main'; /** * Theia tracks the maximized state of Electron Browser Windows. @@ -138,22 +138,7 @@ export class TheiaElectronWindow { } protected checkSafeToStop(reason: StopReason): Promise { - const confirmChannel = `safe-to-close-${this._window.id}`; - const cancelChannel = `notSafeToClose-${this._window.id}`; - const temporaryDisposables = new DisposableCollection(); - return new Promise(resolve => { - this._window.webContents.send(CLOSE_REQUESTED_SIGNAL, { confirmChannel, cancelChannel, reason }); - createDisposableListener(ipcMain, confirmChannel, (e: IpcMainEvent) => { - if (this.isSender(e)) { - resolve(true); - } - }, temporaryDisposables); - createDisposableListener(ipcMain, cancelChannel, (e: IpcMainEvent) => { - if (this.isSender(e)) { - resolve(false); - } - }, temporaryDisposables); - }).finally(() => temporaryDisposables.dispose()); + return TheiaRendererAPI.requestClose(this.window.webContents, reason); } protected restoreMaximizedState(): void { @@ -165,23 +150,13 @@ export class TheiaElectronWindow { } protected trackApplicationState(): void { - createDisposableListener(ipcMain, APPLICATION_STATE_CHANGE_SIGNAL, (e: IpcMainEvent, state: FrontendApplicationState) => { - if (this.isSender(e)) { - this.applicationState = state; - } - }, this.toDispose); + this.toDispose.push(TheiaRendererAPI.onApplicationStateChanged(this.window.webContents, state => { + this.applicationState = state; + })); } protected attachReloadListener(): void { - createDisposableListener(ipcMain, RELOAD_REQUESTED_SIGNAL, (e: IpcMainEvent) => { - if (this.isSender(e)) { - this.reload(); - } - }, this.toDispose); - } - - protected isSender(e: IpcMainEvent): boolean { - return BrowserWindow.fromId(e.sender.id) === this._window; + this.toDispose.push(TheiaRendererAPI.onRequestReload(this.window.webContents, () => this.reload())); } dispose(): void { diff --git a/packages/electron/README.md b/packages/electron/README.md index 7b879f0056bdf..22374a2699c43 100644 --- a/packages/electron/README.md +++ b/packages/electron/README.md @@ -17,8 +17,6 @@ The `@theia/electron` extension bundles all Electron-specific dependencies and c ## Re-Exports - `@theia/electron/shared/...` - - `@electron/remote` (from [`@electron/remote@^2.0.1 <2.0.4 || >2.0.4`](https://www.npmjs.com/package/@electron/remote)) - - `@electron/remote/main` (from [`@electron/remote@^2.0.1 <2.0.4 || >2.0.4`](https://www.npmjs.com/package/@electron/remote)) - `native-keymap` (from [`native-keymap@^2.2.1`](https://www.npmjs.com/package/native-keymap)) - `electron` (from [`electron@^15.3.5`](https://www.npmjs.com/package/electron)) - `electron-store` (from [`electron-store@^8.0.0`](https://www.npmjs.com/package/electron-store)) diff --git a/packages/electron/package.json b/packages/electron/package.json index c1da5b17e5385..58a3a3c68390d 100644 --- a/packages/electron/package.json +++ b/packages/electron/package.json @@ -3,7 +3,6 @@ "version": "1.35.0", "description": "Theia - Electron utility package", "dependencies": { - "@electron/remote": "^2.0.1 <2.0.4 || >2.0.4", "electron-store": "^8.0.0", "fix-path": "^3.0.0", "native-keymap": "^2.2.1" @@ -18,8 +17,6 @@ "theiaReExports": { "shared": { "export *": [ - "@electron/remote", - "@electron/remote/main", "native-keymap" ], "export =": [ diff --git a/packages/electron/shared/@electron/remote/index.d.ts b/packages/electron/shared/@electron/remote/index.d.ts deleted file mode 100644 index 83b7353721aca..0000000000000 --- a/packages/electron/shared/@electron/remote/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@electron/remote'; diff --git a/packages/electron/shared/@electron/remote/index.js b/packages/electron/shared/@electron/remote/index.js deleted file mode 100644 index 4097d77b477dd..0000000000000 --- a/packages/electron/shared/@electron/remote/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@electron/remote'); diff --git a/packages/electron/shared/@electron/remote/main/index.d.ts b/packages/electron/shared/@electron/remote/main/index.d.ts deleted file mode 100644 index 05ede6b8da580..0000000000000 --- a/packages/electron/shared/@electron/remote/main/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@electron/remote/main'; diff --git a/packages/electron/shared/@electron/remote/main/index.js b/packages/electron/shared/@electron/remote/main/index.js deleted file mode 100644 index 3dec179bd4de4..0000000000000 --- a/packages/electron/shared/@electron/remote/main/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@electron/remote/main'); diff --git a/packages/filesystem/package.json b/packages/filesystem/package.json index 52c27a63790df..a6c62e53598ab 100644 --- a/packages/filesystem/package.json +++ b/packages/filesystem/package.json @@ -24,6 +24,10 @@ "access": "public" }, "theiaExtensions": [ + { + "preload": "lib/electron-browser/preload", + "electronMain": "lib/electron-main/electron-main-module" + }, { "frontend": "lib/browser/filesystem-frontend-module", "backend": "lib/node/filesystem-backend-module" diff --git a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts index 3eddafb47739e..8a19cbf89f20d 100644 --- a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts +++ b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts @@ -15,8 +15,6 @@ // ***************************************************************************** import { inject, injectable } from '@theia/core/shared/inversify'; -import { FileFilter, OpenDialogOptions, SaveDialogOptions } from '@theia/core/electron-shared/electron'; -import * as electronRemote from '@theia/core/electron-shared/@electron/remote'; import URI from '@theia/core/lib/common/uri'; import { isOSX, OS } from '@theia/core/lib/common/os'; import { MaybeArray } from '@theia/core/lib/common/types'; @@ -24,12 +22,6 @@ import { MessageService } from '@theia/core/lib/common/message-service'; import { FileStat } from '../../common/files'; import { FileAccess } from '../../common/filesystem'; import { DefaultFileDialogService, OpenFileDialogProps, SaveFileDialogProps } from '../../browser/file-dialog'; - -// See https://github.com/electron/electron/blob/v9.0.2/docs/api/dialog.md -// These properties get extended with newer versions of Electron -type DialogProperties = 'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles' | - 'createDirectory' | 'promptToCreate' | 'noResolveAliases' | 'treatPackageAsDirectory' | 'dontAddToRecent'; - // // We are OK to use this here because the electron backend and frontend are on the same host. // If required, we can move this single service (and its module) to a dedicated Theia extension, @@ -38,6 +30,7 @@ type DialogProperties = 'openFile' | 'openDirectory' | 'multiSelections' | 'show // // eslint-disable-next-line @theia/runtime-import-check import { FileUri } from '@theia/core/lib/node/file-uri'; +import { OpenDialogOptions, SaveDialogOptions } from '../../electron-common/electron-api'; @injectable() export class ElectronFileDialogService extends DefaultFileDialogService { @@ -49,10 +42,8 @@ export class ElectronFileDialogService extends DefaultFileDialogService { override async showOpenDialog(props: OpenFileDialogProps, folder?: FileStat): Promise | undefined> { const rootNode = await this.getRootNode(folder); if (rootNode) { - const { filePaths } = props.modal !== false ? - await electronRemote.dialog.showOpenDialog(electronRemote.getCurrentWindow(), this.toOpenDialogOptions(rootNode.uri, props)) : - await electronRemote.dialog.showOpenDialog(this.toOpenDialogOptions(rootNode.uri, props)); - if (filePaths.length === 0) { + const filePaths = await window.electronTheiaFilesystem.showOpenDialog(this.toOpenDialogOptions(rootNode.uri, props)); + if (!filePaths || filePaths.length === 0) { return undefined; } @@ -67,9 +58,8 @@ export class ElectronFileDialogService extends DefaultFileDialogService { override async showSaveDialog(props: SaveFileDialogProps, folder?: FileStat): Promise { const rootNode = await this.getRootNode(folder); if (rootNode) { - const { filePath } = props.modal !== false ? - await electronRemote.dialog.showSaveDialog(electronRemote.getCurrentWindow(), this.toSaveDialogOptions(rootNode.uri, props)) : - await electronRemote.dialog.showSaveDialog(this.toSaveDialogOptions(rootNode.uri, props)); + const filePath = await window.electronTheiaFilesystem.showSaveDialog(this.toSaveDialogOptions(rootNode.uri, props)); + if (!filePath) { return undefined; } @@ -110,120 +100,69 @@ export class ElectronFileDialogService extends DefaultFileDialogService { return unreadableResourcePaths.length === 0; } - protected toDialogOptions(uri: URI, props: SaveFileDialogProps | OpenFileDialogProps, dialogTitle: string): electron.FileDialogProps { - type Mutable = { -readonly [K in keyof T]: T[K] }; - const electronProps: Mutable = { - title: props.title || dialogTitle, - defaultPath: FileUri.fsPath(uri), - }; - const { - canSelectFiles = true, - canSelectFolders = false, - } = props as OpenFileDialogProps; - if (!isOSX && canSelectFiles && canSelectFolders) { - console.warn('canSelectFiles === true && canSelectFolders === true is only supported on OSX!'); + protected toOpenDialogOptions(uri: URI, props: OpenFileDialogProps): OpenDialogOptions { + if (!isOSX && props.canSelectFiles !== false && props.canSelectFolders === true) { + console.warn(`Cannot have 'canSelectFiles' and 'canSelectFolders' at the same time. Fallback to 'folder' dialog. \nProps was: ${JSON.stringify(props)}.`); + + // Given that both props are set, fallback to using a `folder` dialog. + props.canSelectFiles = false; + props.canSelectFolders = true; } - if ((isOSX && canSelectFiles) || !canSelectFolders) { - electronProps.filters = props.filters ? Object.entries(props.filters).map(([name, extensions]) => ({ name, extensions })) : []; - if (this.shouldAddAllFilesFilter(electronProps)) { - electronProps.filters.push({ name: 'All Files', extensions: ['*'] }); + + const result: OpenDialogOptions = { + path: FileUri.fsPath(uri) + }; + + result.title = props.title; + result.buttonLabel = props.openLabel; + result.maxWidth = props.maxWidth; + result.modal = props.modal; + result.openFiles = props.canSelectFiles; + result.openFolders = props.canSelectFolders; + result.selectMany = props.canSelectMany; + + if (props.filters) { + result.filters = []; + const filters = Object.entries(props.filters); + for (const [label, extensions] of filters) { + result.filters.push({ name: label, extensions: extensions }); } - } - return electronProps; - } - /** - * Specifies whether an _All Files_ filter should be added to the dialog. - * - * On Linux, the _All Files_ filter [hides](https://github.com/eclipse-theia/theia/issues/11321) files without an extension. - * The bug is resolved in Electron >=18. - */ - protected shouldAddAllFilesFilter(electronProps: electron.FileDialogProps): boolean { - const foundFilters = !!electronProps.filters && electronProps.filters.length > 0; - const isNotLinux = OS.type() !== OS.Type.Linux; - return isNotLinux || foundFilters; - } + // On Linux, the _All Files_ filter [hides](https://github.com/eclipse-theia/theia/issues/11321) files without an extension. + // The bug is resolved in Electron >=18. + if (props.canSelectFiles) { + if (OS.type() !== OS.Type.Linux || filters.length > 0) { + result.filters.push({ name: 'All Files', extensions: ['*'] }); + } + } + } - protected toOpenDialogOptions(uri: URI, props: OpenFileDialogProps): OpenDialogOptions { - const properties = electron.dialog.toDialogProperties(props); - const buttonLabel = props.openLabel; - return { ...this.toDialogOptions(uri, props, 'Open'), properties, buttonLabel }; + return result; } protected toSaveDialogOptions(uri: URI, props: SaveFileDialogProps): SaveDialogOptions { - const buttonLabel = props.saveLabel; if (props.inputValue) { uri = uri.resolve(props.inputValue); } - const defaultPath = FileUri.fsPath(uri); - return { ...this.toDialogOptions(uri, props, 'Save'), buttonLabel, defaultPath }; - } -} - -export namespace electron { - - /** - * Common "super" interface of the `electron.SaveDialogOptions` and `electron.OpenDialogOptions` types. - */ - export interface FileDialogProps { - - /** - * The dialog title. - */ - readonly title?: string; - - /** - * The default path, where the dialog opens. Requires an FS path. - */ - readonly defaultPath?: string; - - /** - * Resource filter. - */ - readonly filters?: FileFilter[]; + const result: SaveDialogOptions = { + path: FileUri.fsPath(uri) + }; - } + result.title = props.title; + result.buttonLabel = props.saveLabel; + result.maxWidth = props.maxWidth; + result.modal = props.modal; - export namespace dialog { - - /** - * Converts the Theia specific `OpenFileDialogProps` into an electron specific array. - * - * Note: On Windows and Linux an open dialog can not be both a file selector and a directory selector, - * so if you set properties to ['openFile', 'openDirectory'] on these platforms, a directory selector will be shown. - * - * See: https://github.com/electron/electron/issues/10252#issuecomment-322012159 - */ - export function toDialogProperties(props: OpenFileDialogProps): Array { - if (!isOSX && props.canSelectFiles !== false && props.canSelectFolders === true) { - console.warn(`Cannot have 'canSelectFiles' and 'canSelectFolders' at the same time. Fallback to 'folder' dialog. \nProps was: ${JSON.stringify(props)}.`); - - // Given that both props are set, fallback to using a `folder` dialog. - props.canSelectFiles = false; - props.canSelectFolders = true; + if (props.filters) { + result.filters = []; + const filters = Object.entries(props.filters); + for (const [label, extensions] of filters) { + result.filters.push({ name: label, extensions: extensions }); } - const properties: Array = []; - if (!isOSX) { - if (props.canSelectFiles !== false && props.canSelectFolders !== true) { - properties.push('openFile'); - } - if (props.canSelectFolders === true && props.canSelectFiles === false) { - properties.push('openDirectory'); - } - } else { - if (props.canSelectFiles !== false) { - properties.push('openFile'); - } - if (props.canSelectFolders === true) { - properties.push('openDirectory'); - properties.push('createDirectory'); - } - } - if (props.canSelectMany === true) { - properties.push('multiSelections'); - } - return properties; } + + return result; } + } diff --git a/packages/filesystem/src/electron-browser/preload.ts b/packages/filesystem/src/electron-browser/preload.ts new file mode 100644 index 0000000000000..718a33d36efbb --- /dev/null +++ b/packages/filesystem/src/electron-browser/preload.ts @@ -0,0 +1,31 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics 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 { CHANNEL_SHOW_OPEN, CHANNEL_SHOW_SAVE, OpenDialogOptions, SaveDialogOptions, TheiaFilesystemAPI } from '../electron-common/electron-api'; + +// eslint-disable-next-line import/no-extraneous-dependencies +const { ipcRenderer, contextBridge } = require('electron'); + +const api: TheiaFilesystemAPI = { + showOpenDialog: (options: OpenDialogOptions) => ipcRenderer.invoke(CHANNEL_SHOW_OPEN, options), + showSaveDialog: (options: SaveDialogOptions) => ipcRenderer.invoke(CHANNEL_SHOW_SAVE, options), + +}; + +export function preload(): void { + console.log('exposing theia filesystem electron api'); + + contextBridge.exposeInMainWorld('electronTheiaFilesystem', api); +} diff --git a/packages/filesystem/src/electron-common/electron-api.ts b/packages/filesystem/src/electron-common/electron-api.ts new file mode 100644 index 0000000000000..4441d1cb38bd7 --- /dev/null +++ b/packages/filesystem/src/electron-common/electron-api.ts @@ -0,0 +1,55 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics 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 +// ***************************************************************************** + +export interface FileFilter { + name: string; + extensions: string[]; +} + +export interface OpenDialogOptions { + title?: string, + maxWidth?: number, + path: string, + buttonLabel?: string, + modal?: boolean, + openFiles?: boolean, + openFolders?: boolean; + selectMany?: boolean; + filters?: FileFilter[]; +} + +export interface SaveDialogOptions { + title?: string, + maxWidth?: number, + path: string, + buttonLabel?: string, + modal?: boolean, + filters?: FileFilter[]; +} + +export interface TheiaFilesystemAPI { + showOpenDialog(options: OpenDialogOptions): Promise; + showSaveDialog(options: SaveDialogOptions): Promise; +} + +declare global { + interface Window { + electronTheiaFilesystem: TheiaFilesystemAPI + } +} + +export const CHANNEL_SHOW_OPEN = 'ShowOpenDialog'; +export const CHANNEL_SHOW_SAVE = 'ShowSaveDialog'; diff --git a/packages/filesystem/src/electron-main/electron-api-main.ts b/packages/filesystem/src/electron-main/electron-api-main.ts new file mode 100644 index 0000000000000..9a66f30ce9ef1 --- /dev/null +++ b/packages/filesystem/src/electron-main/electron-api-main.ts @@ -0,0 +1,78 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics 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 '@theia/core/shared/inversify'; + +import { ElectronMainApplication, ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application'; +import { MaybePromise } from '@theia/core'; +import { CHANNEL_SHOW_OPEN, CHANNEL_SHOW_SAVE, OpenDialogOptions, SaveDialogOptions } from '../electron-common/electron-api'; +import { ipcMain, OpenDialogOptions as ElectronOpenDialogOptions, SaveDialogOptions as ElectronSaveDialogOptions, BrowserWindow, dialog } + from '@theia/core/electron-shared/electron'; + +@injectable() +export class ElectronApi implements ElectronMainApplicationContribution { + onStart(application: ElectronMainApplication): MaybePromise { + // dialogs + ipcMain.handle(CHANNEL_SHOW_OPEN, async (event, options: OpenDialogOptions) => { + const properties: ElectronOpenDialogOptions['properties'] = []; + + // checking proper combination of file/dir opening is done on the renderer side + if (options.openFiles) { + properties.push('openFile'); + } + if (options.openFolders) { + properties.push('openDirectory'); + } + + if (options.selectMany === true) { + properties.push('multiSelections'); + } + + const dialogOpts: ElectronOpenDialogOptions = { + defaultPath: options.path, + buttonLabel: options.buttonLabel, + filters: options.filters, + title: options.title, + properties: properties + }; + + if (options.modal) { + const window = BrowserWindow.fromWebContents(event.sender); + if (window) { + return (await dialog.showOpenDialog(window, dialogOpts)).filePaths; + } + } + return (await dialog.showOpenDialog(dialogOpts)).filePaths; + }); + + ipcMain.handle(CHANNEL_SHOW_SAVE, async (event, options: SaveDialogOptions) => { + const dialogOpts: ElectronSaveDialogOptions = { + defaultPath: options.path, + buttonLabel: options.buttonLabel, + filters: options.filters, + title: options.title + }; + if (options.modal && window) { + const window = BrowserWindow.fromWebContents(event.sender); + if (window) { + return (await dialog.showSaveDialog(window, dialogOpts)).filePath; + } + } + return (await dialog.showSaveDialog(dialogOpts)).filePath; + }); + + } +} diff --git a/packages/filesystem/src/electron-main/electron-main-module.ts b/packages/filesystem/src/electron-main/electron-main-module.ts new file mode 100644 index 0000000000000..175e9c47916cc --- /dev/null +++ b/packages/filesystem/src/electron-main/electron-main-module.ts @@ -0,0 +1,22 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics 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 '@theia/core/shared/inversify'; +import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application'; +import { ElectronApi } from './electron-api-main'; + +export default new ContainerModule(bind => { + bind(ElectronMainApplicationContribution).to(ElectronApi).inSingletonScope(); +}); diff --git a/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts b/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts index 4fa8e0d27e170..001eb82820d10 100644 --- a/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts +++ b/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts @@ -15,11 +15,13 @@ // ***************************************************************************** import { Endpoint } from '@theia/core/lib/browser'; + import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token'; -import * as electronRemote from '@theia/core/electron-shared/@electron/remote'; import { inject, injectable } from '@theia/core/shared/inversify'; import { MiniBrowserEnvironment } from '../../browser/environment/mini-browser-environment'; +import { } from '@theia/core/lib/electron-common/electron-api'; + @injectable() export class ElectronMiniBrowserEnvironment extends MiniBrowserEnvironment { @@ -28,13 +30,8 @@ export class ElectronMiniBrowserEnvironment extends MiniBrowserEnvironment { override getEndpoint(uuid: string, hostname?: string): Endpoint { const endpoint = super.getEndpoint(uuid, hostname); - // Note: This call is async, but clients expect sync logic. - electronRemote.session.defaultSession.cookies.set({ - url: endpoint.getRestUrl().toString(true), - name: ElectronSecurityToken, - value: JSON.stringify(this.electronSecurityToken), - httpOnly: true, - }); + + window.electronTheiaCore.attachSecurityToken(endpoint.getRestUrl().toString(true)); return endpoint; } diff --git a/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts b/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts index 7c50c438a08a7..964b6163805a5 100644 --- a/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts +++ b/packages/navigator/src/electron-browser/electron-navigator-menu-contribution.ts @@ -17,15 +17,14 @@ import { Command, CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry, SelectionService } from '@theia/core'; import { CommonCommands, KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser'; import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; -import * as electron from '@theia/core/electron-shared/electron'; -import * as electronRemote from '@theia/core/electron-shared/@electron/remote'; import { inject, injectable } from '@theia/core/shared/inversify'; import { FileStatNode } from '@theia/filesystem/lib/browser'; import { FileNavigatorWidget, FILE_NAVIGATOR_ID } from '../browser'; import { NavigatorContextMenu, SHELL_TABBAR_CONTEXT_REVEAL } from '../browser/navigator-contribution'; import { isWindows, isOSX } from '@theia/core/lib/common/os'; -import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; +import { } from '@theia/core/lib/electron-common/electron-api'; export const OPEN_CONTAINING_FOLDER = Command.toDefaultLocalizedCommand({ id: 'revealFileInOS', @@ -50,10 +49,7 @@ export class ElectronNavigatorMenuContribution implements MenuContribution, Comm registerCommands(commands: CommandRegistry): void { commands.registerCommand(OPEN_CONTAINING_FOLDER, UriAwareCommandHandler.MonoSelect(this.selectionService, { execute: async uri => { - // workaround for https://github.com/electron/electron/issues/4349: - // use electron.remote.shell to open the window in the foreground on Windows - const shell = isWindows ? electronRemote.shell : electron.shell; - shell.showItemInFolder(uri['codeUri'].fsPath); + window.electronTheiaCore.showItemInFolder(uri['codeUri'].fsPath); }, isEnabled: uri => !!this.workspaceService.getWorkspaceRootUri(uri), isVisible: uri => !!this.workspaceService.getWorkspaceRootUri(uri), diff --git a/packages/plugin-ext/src/main/electron-browser/webview/electron-webview-widget-factory.ts b/packages/plugin-ext/src/main/electron-browser/webview/electron-webview-widget-factory.ts index fd491c2439ea7..d8fe8cd80a8eb 100644 --- a/packages/plugin-ext/src/main/electron-browser/webview/electron-webview-widget-factory.ts +++ b/packages/plugin-ext/src/main/electron-browser/webview/electron-webview-widget-factory.ts @@ -14,12 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import * as electronRemote from '@theia/core/electron-shared/@electron/remote'; -import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token'; import { WebviewWidgetFactory } from '../../browser/webview/webview-widget-factory'; import { WebviewWidgetIdentifier, WebviewWidget } from '../../browser/webview/webview'; import { CustomEditorWidgetFactory } from '../../browser/custom-editors/custom-editor-widget-factory'; import { CustomEditorWidget } from '../../browser/custom-editors/custom-editor-widget'; +import { } from '@theia/core/lib/electron-common/electron-api'; export class ElectronWebviewWidgetFactory extends WebviewWidgetFactory { @@ -34,13 +33,8 @@ export class ElectronWebviewWidgetFactory extends WebviewWidgetFactory { * * @param endpoint cookie's target url */ - protected async attachElectronSecurityCookie(endpoint: string): Promise { - await electronRemote.session.defaultSession.cookies.set({ - url: endpoint, - name: ElectronSecurityToken, - value: JSON.stringify(this.container.get(ElectronSecurityToken)), - httpOnly: true - }); + protected attachElectronSecurityCookie(endpoint: string): Promise { + return window.electronTheiaCore.attachSecurityToken(endpoint); } } @@ -59,12 +53,7 @@ export class ElectronCustomEditorWidgetFactory extends CustomEditorWidgetFactory * @param endpoint cookie's target url */ protected async attachElectronSecurityCookie(endpoint: string): Promise { - await electronRemote.session.defaultSession.cookies.set({ - url: endpoint, - name: ElectronSecurityToken, - value: JSON.stringify(this.container.get(ElectronSecurityToken)), - httpOnly: true - }); + return window.electronTheiaCore.attachSecurityToken(endpoint); } } diff --git a/yarn.lock b/yarn.lock index e940d884c9bdd..df9b89eb15a88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -964,11 +964,6 @@ optionalDependencies: global-agent "^3.0.0" -"@electron/remote@^2.0.1 <2.0.4 || >2.0.4": - version "2.0.9" - resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-2.0.9.tgz#092ff085407bc907f45b89a72c36faa773ccf2d9" - integrity sha512-LR0W0ID6WAKHaSs0x5LX9aiG+5pFBNAJL6eQAJfGkCuZPUa6nZz+czZLdlTDETG45CgF/0raSvCtYOYUpr6c+A== - "@eslint/eslintrc@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"