@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
374 lines (326 loc) • 15.2 kB
text/typescript
// *****************************************************************************
// 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-only 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,
CHANNEL_IS_MAXIMIZED,
CHANNEL_REQUEST_SECONDARY_CLOSE,
CHANNEL_SET_BACKGROUND_COLOR,
CHANNEL_WC_METADATA,
CHANNEL_ABOUT_TO_CLOSE,
CHANNEL_OPEN_WITH_SYSTEM_APP,
CHANNEL_OPEN_URL
} from '../electron-common/electron-api';
import { ElectronMainApplication, ElectronMainApplicationContribution } from './electron-main-application';
import { Disposable, DisposableCollection, isOSX, MaybePromise } from '../common';
import { createDisposableListener } from './event-utils';
()
export class TheiaMainApi implements ElectronMainApplicationContribution {
(ElectronSecurityToken)
protected electronSecurityToken: ElectronSecurityToken;
protected readonly openPopups = new Map<number, Menu>();
onStart(application: ElectronMainApplication): MaybePromise<void> {
ipcMain.on(CHANNEL_WC_METADATA, event => {
event.returnValue = event.sender.id.toString();
});
// electron security token
ipcMain.on(CHANNEL_GET_SECURITY_TOKEN, event => {
event.returnValue = 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,
sameSite: 'no_restriction'
}));
// 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, windowName?: string) => {
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);
let electronWindow: BrowserWindow | undefined;
if (windowName) {
electronWindow = BrowserWindow.getAllWindows().find(win => win.webContents.mainFrame.name === windowName);
} else {
electronWindow = BrowserWindow.fromWebContents(event.sender) || undefined;
}
popup.popup({
window: electronWindow,
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 = windowName
? BrowserWindow.getAllWindows().find(win => win.webContents.mainFrame.name === windowName)
: BrowserWindow.fromWebContents(event.sender);
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.on(CHANNEL_OPEN_WITH_SYSTEM_APP, (event, uri) => {
shell.openExternal(uri);
});
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_SET_BACKGROUND_COLOR, (event, backgroundColor) => application.setBackgroundColor(event.sender, backgroundColor));
ipcMain.on(CHANNEL_MINIMIZE, event => {
BrowserWindow.fromWebContents(event.sender)?.minimize();
});
ipcMain.on(CHANNEL_IS_MAXIMIZED, event => {
event.returnValue = BrowserWindow.fromWebContents(event.sender)?.isMaximized();
});
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);
}
});
}
private isASCI(accelerator: string | undefined): boolean {
if (typeof accelerator !== 'string') {
return false;
}
for (let i = 0; i < accelerator.length; i++) {
if (accelerator.charCodeAt(i) > 127) {
return false;
}
}
return true;
}
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: this.isASCI(dto.accelerator) ? dto.accelerator : undefined
};
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 openUrl(wc: WebContents, url: string): Promise<boolean> {
return new Promise<boolean>(resolve => {
const channelNr = nextReplyChannel++;
const replyChannel = `openUrl${channelNr}`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const l = createDisposableListener(ipcMain, replyChannel, (e, args: any[]) => {
l.dispose();
resolve(args[0]);
});
wc.send(CHANNEL_OPEN_URL, url, replyChannel);
});
}
export function sendAboutToClose(wc: WebContents): Promise<void> {
return new Promise<void>(resolve => {
const channelNr = nextReplyChannel++;
const replyChannel = `aboutToClose${channelNr}`;
const l = createDisposableListener(ipcMain, replyChannel, e => {
l.dispose();
resolve();
});
wc.send(CHANNEL_ABOUT_TO_CLOSE, replyChannel);
});
}
export function requestClose(wc: WebContents, stopReason: StopReason): Promise<boolean> {
const channelNr = nextReplyChannel++;
const confirmChannel = `confirm-${channelNr}`;
const cancelChannel = `cancel-${channelNr}`;
const disposables = new DisposableCollection();
return new Promise<boolean>(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 requestSecondaryClose(mainWindow: WebContents, secondaryWindow: WebContents): Promise<boolean> {
const channelNr = nextReplyChannel++;
const confirmChannel = `confirm-${channelNr}`;
const cancelChannel = `cancel-${channelNr}`;
const disposables = new DisposableCollection();
return new Promise<boolean>(resolve => {
mainWindow.send(CHANNEL_REQUEST_SECONDARY_CLOSE, secondaryWindow.mainFrame.name, 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, state => handler(state as FrontendApplicationState));
}
export function onIpcData(handler: (sender: WebContents, data: Uint8Array) => void): Disposable {
return createDisposableListener<IpcMainEvent>(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<IpcMainEvent>(ipcMain, channel, (event, ...args) => {
if (wc.id === event.sender.id) {
handler(...args);
}
});
}
}