xcraft-core-host
Version:
Multiple engines support for xcraft-server
622 lines (554 loc) • 17.5 kB
JavaScript
'use strict';
const WM_INSTANCE_KEY = Symbol.for('goblin-wm.window-manager');
const IS_DEV = process.env.NODE_ENV === 'development';
const Screen = require('./screen.js');
const moduleName = 'xcraft-core-host';
const xLog = require('xcraft-core-log')(moduleName);
class Window {
constructor(wm, options) {
const {BrowserWindow} = require('electron');
//copy config
this.wm = wm;
this.winOptions = {};
if (this.wm?.config?.windowOptions) {
this.winOptions = {...this.wm.config.windowOptions};
}
//Override with options
Object.entries(options).forEach(([option, value]) => {
if (value !== undefined) {
this.winOptions[option] = value;
}
});
// Default args for nodeIntegration and contextIsolation
this.winOptions.webPreferences = {
nodeIntegration: true,
contextIsolation: false,
// Override default args with options
...this.winOptions.webPreferences,
};
// Set the pid in the partition if it's defined.
// This action ensures that a second instance starts without delay,
// addressing the issue described in https://github.com/electron/electron/issues/22438
const partition = this.winOptions.webPreferences.partition;
if (partition) {
this.winOptions.webPreferences.partition = partition.replaceAll(
'$PROCESS_PID',
process.pid
);
}
//VIBRANCY SUPPORT
/*let vibrancy;
try {
vibrancy = require('windows-swca');
} catch (ex) {
if (ex.code !== 'MODULE_NOT_FOUND') {
throw ex;
} else if (this.wm.config.vibrancyOptions) {
console.warn(
'WM: electron-vibrancy not available and vibrancyOptions is set'
);
}
}
if (vibrancy && this.wm.config.vibrancyOptions !== null) {
this.winOptions.backgroundColor = '#00000000';
}*/
if (options.modal && options.parentId) {
this.winOptions.parent = wm[options.parentId];
this.winOptions.modal = true;
}
const mustHideWindow = this.winOptions.show === false;
//avoid display blank page
this.winOptions.show = false;
this.win = new BrowserWindow(this.winOptions);
this.win.setMenuBarVisibility(false);
this.win.autoHideMenuBar = true;
this.win.once('ready-to-show', () => {
if (!mustHideWindow) {
this.win.show();
}
});
//avoid title change for ex. when navigating
this.win.on('page-title-updated', (e) => {
e.preventDefault();
});
//ENABLE VIBRANCY
/*if (vibrancy && this.wm.config.vibrancyOptions !== null) {
this.win.setMenuBarVisibility(false);
vibrancy.SetWindowCompositionAttribute(
this.win.getNativeWindowHandle(),
vibrancy.ACCENT_STATE.ACCENT_ENABLE_BLURBEHIND,
0x01000000
);
}*/
if (options.openDevTools) {
this.win.webContents.on('did-frame-finish-load', () => {
this.win.webContents.openDevTools();
});
}
}
setState(windowState) {
if (windowState) {
//TODO: invalidate bad bounds
this.win.setBounds(
{
x: windowState.get('bounds.x'),
y: windowState.get('bounds.y'),
height: windowState.get('bounds.height'),
width: windowState.get('bounds.width'),
},
true
);
}
}
dispose(clearStorageData = false) {
this.win.close();
if (clearStorageData) {
this.win.webContents.session.clearStorageData();
}
if (!this.win.isDestroyed()) {
this.win.destroy();
}
}
}
class WindowManager {
constructor() {
console.log('WM init...');
const {Menu, app, shell} = require('electron');
if (!app) {
console.warn('WM init...[DISABLED]');
return;
}
this.app = app;
this._log = require('xcraft-core-log')('window-manager');
this._services = {};
this._windows = {};
this._windowIdStack = [];
this._currentFeeds = {};
const {splashWindowOptions} = require('xcraft-core-host');
this.config = {
splashWindowOptions,
};
const defaultMenu = require('electron-default-menu');
const menu = defaultMenu(app, shell)
.filter((menu) => menu.role !== 'help')
.map((menu) => {
if (menu.label === 'Window') {
delete menu.submenu[1].role;
menu.submenu[1].label = 'Close tab';
menu.submenu[1].click = function (menuItem, window) {
const busClient = require('xcraft-core-busclient').getGlobal();
const resp = busClient.newResponse('goblin-vm', 'token');
resp.events.send(`${window.id}.<close-tab-requested>`);
};
menu.submenu.push({
label: 'Close all tabs',
accelerator: 'CmdOrCtrl+Shift+W',
click: function (menuItem, window) {
const busClient = require('xcraft-core-busclient').getGlobal();
const resp = busClient.newResponse('goblin-vm', 'token');
resp.events.send(`${window.id}.<close-all-tabs-requested>`);
},
});
}
if (process.env.NODE_ENV !== 'development') {
menu.submenu = menu.submenu.filter(
(submenu) =>
!['CmdOrCtrl+R', 'Ctrl+Shift+I', 'Alt+Command+I'].includes(
submenu.accelerator
)
);
}
return menu;
});
app.applicationMenu = Menu.buildFromTemplate(menu);
}
loadConfig() {
const {splashWindowOptions} = require('xcraft-core-host');
this.config = require('xcraft-core-etc')().load('goblin-wm');
if (!this.config.splashWindowOptions && splashWindowOptions) {
this.config.splashWindowOptions = splashWindowOptions;
}
}
loadSplash() {
const appArgs = require('../lib/args-parsing.js')();
if (appArgs.splash !== false && this.config.disableSplash === false) {
this.displaySplash();
}
}
getDefaultWindowState() {
return {
bounds: Screen.getDefaultWindowBounds(),
maximized: false,
fullscreen: false,
};
}
getWindowState(window) {
if (window && window.isDestroyed()) {
return null;
}
let bounds = window.getNormalBounds();
return {
bounds,
maximized: window.isMaximized(),
fullscreen: window.isFullScreen(),
};
}
get currentWindow() {
const len = this._windowIdStack.length;
if (len > 0) {
return this._services[this._windowIdStack[len - 1]].win;
} else {
return null;
}
}
displayAuth(
authUrl,
takeWholeScreen = false,
onClose,
noParent = false,
windowId = '',
onLoaded = null,
windowOptions = null
) {
const authWindowId = `auth${windowId}`;
const existing = this.getWindowInstance(authWindowId);
if (existing) {
existing.show();
return (clearStorageData = false) => {
this.dispose(authWindowId, clearStorageData);
};
}
let parent = null;
if (!noParent) {
parent = this.currentWindow;
}
const {webPreferences, ...otherWindowOptions} = windowOptions || {};
const window = this.create(
authWindowId,
{
resizable: false,
minimizable: false,
maximizable: false,
fullscreenable: false,
show: true,
autoHideMenuBar: true,
frame: true,
backgroundColor: '#1e3d5b',
alwaysOnTop: false,
modal: false,
movable: true,
parent,
webPreferences: {
nodeIntegration: false,
...(webPreferences || {}),
},
...otherWindowOptions,
},
null
);
if (onClose) {
window.win.once('close', onClose);
}
let rect = Screen.getDefaultWindowBounds(800, 600);
if (!noParent) {
rect = parent.getBounds();
}
if (!takeWholeScreen) {
const height = parseInt((rect.height * 4) / 5);
const width = parseInt((height * 4) / 5);
const x = rect.x + parseInt(rect.width / 2 - width / 2);
const y = rect.y + parseInt(rect.height / 2 - height / 2);
window.win.setBounds({x, y, width, height});
} else {
window.win.setBounds({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
});
}
window.win.loadURL(authUrl);
window.win.webContents
.on('did-finish-load', () => {
if (onLoaded) {
onLoaded();
}
})
.on(
'did-fail-load',
(event, errorCode, errorDescription, validatedURL) => {
xLog.err(
`DisplayAuth: renderer load error (code: ${errorCode}, description: ${errorDescription}, url: ${validatedURL})`
);
if (onLoaded) {
onLoaded('did-fail-load', {
errorCode,
errorDescription,
validatedURL,
});
}
}
)
.on('render-process-gone', (event, details) => {
xLog.err(
`DisplayAuth: renderer process gone (reason: ${details.reason}, code: ${details.exitCode})`
);
if (onLoaded) {
onLoaded('render-process-gone', {details});
}
});
//return a disposer
return (clearStorageData = false) => {
this.dispose(authWindowId, clearStorageData);
};
}
async prompt({values}, listener) {
const getResult = new Promise((resolve, reject) => {
this.displaySplash(() => {
const {win} = this._services['splash'];
const {ipcMain} = require('electron');
ipcMain.once('prompt-selected', (event, value) => {
resolve(value);
});
win.webContents.send('prompt', {values});
});
});
const value = await getResult;
return value;
}
displaySplash(onLoaded) {
if (this._services.splash) {
return;
}
const {appVersion} = require('xcraft-core-host');
//SPLASH THE MAIN WINDOW
let options = {
width: 400,
height: 220,
center: true,
resizable: false,
minimizable: false,
maximizable: false,
fullscreenable: false,
show: true,
autoHideMenuBar: true,
title: this.app.getName(),
frame: false,
backgroundColor: '#1e3d5b',
alwaysOnTop: IS_DEV ? false : true,
};
if (this?.config?.splashWindowOptions) {
options = {...options, ...this.config.splashWindowOptions};
if (options.transparent) {
delete options.backgroundColor;
}
}
const bounds = Screen.getDefaultWindowBounds(options.width, options.height);
options = {...options, ...bounds};
const window = this.create('splash', options, null);
const path = require('path');
const {resourcesPath} = require('xcraft-core-host');
const fs = require('fs');
const customSplash = path.join(resourcesPath, 'splash.html');
if (fs.existsSync(customSplash)) {
window.win.loadFile(customSplash);
} else {
window.win.loadFile(path.join(__dirname, './splash.html'));
}
const locale = this.app.getLocale();
window.win.webContents
.on('did-finish-load', () => {
if (onLoaded) {
onLoaded();
}
window.win.webContents.send('program', {version: appVersion});
window.win.webContents.send('progress', {step: 'waiting', locale});
if (this._resp) {
this._resp.events.subscribe(`*::client.progressed`, (msg) => {
if (!window.win.isDestroyed()) {
window.win.webContents.send('progress', {step: msg.data, locale});
}
});
}
})
.on(
'did-fail-load',
(event, errorCode, errorDescription, validatedURL) => {
xLog.err(
`DisplaySplash: renderer load error (code: ${errorCode}, description: ${errorDescription}, url: ${validatedURL})`
);
if (onLoaded) {
onLoaded('did-fail-load', {
errorCode,
errorDescription,
validatedURL,
});
}
}
)
.on('render-process-gone', (event, details) => {
xLog.err(
`DisplaySplash: renderer process gone (reason: ${details.reason}, code: ${details.exitCode})`
);
if (onLoaded) {
onLoaded('render-process-gone', {details});
}
});
}
create(windowId, options) {
const {BrowserWindow} = require('electron');
//DEVELOPPER STUFF
const allBrowserWindow = BrowserWindow.getAllWindows();
// Close devtools in other windows
// prevent the fact that devtools work in only one window at a time...
allBrowserWindow.forEach((window) => {
window.webContents.closeDevTools();
});
const window = new Window(this, options);
this._windowIdStack.push(windowId);
this._services[windowId] = window;
//dispose splash when the next created window appear
if (windowId !== 'splash' && this._services.splash) {
window.win.once('ready-to-show', () => {
setTimeout(() => this.dispose('splash'), this.config?.splashDelay);
});
}
if (this.config?.windowOptions?.openLink === 'external') {
window.win.webContents.setWindowOpenHandler((details) => {
if (details.url && this._resp) {
const {url} = details;
const internalProtocols = Object.keys(this.xHost._xConfig.protocols);
const urlInfos = new URL(url);
const protocol = urlInfos.protocol.slice(0, -1);
if (internalProtocols.includes(protocol)) {
this.xHost._notifyProtocol({url}).catch((ex) => xLog.err(ex));
return {action: 'deny'};
}
this._resp.command.send('client.open-external', {url}, (err) => {
if (err) {
this._resp.log.err(err);
}
});
}
return {action: 'deny'};
});
}
if (this.config?.windowOptions?.contextMenu === true) {
const {Menu, MenuItem} = require('electron');
window.win.webContents.on('context-menu', (event, params) => {
const menu = new Menu();
if (params.isEditable) {
menu.append(new MenuItem({role: 'cut'}));
menu.append(new MenuItem({role: 'copy'}));
menu.append(new MenuItem({role: 'paste'}));
menu.append(
new MenuItem({
role: 'pasteAndMatchStyle',
})
);
menu.append(new MenuItem({role: 'selectAll'}));
menu.append(new MenuItem({type: 'separator'}));
menu.append(
new MenuItem({
type: 'checkbox',
role: 'toggleSpellChecker',
})
);
}
// Add each spelling suggestion
for (const suggestion of params.dictionarySuggestions) {
menu.append(
new MenuItem({
label: suggestion,
click: () =>
window.win.webContents.replaceMisspelling(suggestion),
})
);
}
// Allow users to add the misspelled word to the dictionary
if (params.misspelledWord) {
menu.append(
new MenuItem({
label: 'Add to dictionnary',
click: () =>
window.win.webContents.session.addWordToSpellCheckerDictionary(
params.misspelledWord
),
})
);
}
if (menu.items.length) {
menu.popup();
}
});
}
return window;
}
setWindowCurrentFeeds(windowId, desktopId, feeds) {
this._currentFeeds[windowId] = {desktopId, feeds};
}
init(xHost) {
const {session} = require('electron');
//force User-Agent with only Electron/version
session.defaultSession.webRequest.onBeforeSendHeaders(
(details, callback) => {
details.requestHeaders[
'User-Agent'
] = `Electron/${process.versions.electron}`;
callback({requestHeaders: details.requestHeaders});
}
);
this.xHost = xHost;
const busClient = require('xcraft-core-busclient').getGlobal();
this._resp = busClient.newResponse('goblin-vm', 'token');
}
focus() {
if (!this.currentWindow) {
return;
}
if (this.currentWindow.isMinimized()) {
this.currentWindow.restore();
}
this.currentWindow.focus();
}
getWindowInstance(windowId) {
const window = this._services[windowId];
if (window) {
return window.win;
}
return null;
}
getWindowOptions(windowId) {
return this._services[windowId].winOptions;
}
disposeAll() {
for (const windowId in this._services) {
this.dispose(windowId);
}
}
dispose(windowId, clearStorageData = false) {
const window = this._services[windowId];
if (window) {
window.dispose(clearStorageData);
}
delete this._services[windowId];
delete this._currentFeeds[windowId];
const idx = this._windowIdStack.indexOf(windowId);
if (idx !== -1) {
this._windowIdStack.splice(idx, 1);
}
}
}
const globalSymbols = Object.getOwnPropertySymbols(global);
const hasInstance = globalSymbols.indexOf(WM_INSTANCE_KEY) > -1;
if (!hasInstance) {
global[WM_INSTANCE_KEY] = new WindowManager();
}
const singleton = {};
Object.defineProperty(singleton, 'instance', {
get: function () {
return global[WM_INSTANCE_KEY];
},
});
Object.freeze(singleton);
module.exports = singleton;