// @ts-check // 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). // https://github.com/electron/electron/issues/550#issuecomment-162037357 // https://github.com/eclipse-theia/theia/pull/3534#issuecomment-439689082 require('fix-path')(); // Workaround for https://github.com/electron/electron/issues/9225. Chrome has an issue where // in certain locales (e.g. PL), image metrics are wrongly computed. We explicitly set the // LC_NUMERIC to prevent this from happening (selects the numeric formatting category of the // C locale, http://en.cppreference.com/w/cpp/locale/LC_categories). if (process.env.LC_ALL) { process.env.LC_ALL = 'C'; } process.env.LC_NUMERIC = 'C'; const { v4 } = require('uuid'); const electron = require('electron'); const { join, resolve } = require('path'); const { fork } = require('child_process'); const { app, dialog, shell, BrowserWindow, ipcMain, Menu, globalShortcut } = electron; const { ElectronSecurityToken } = require('@theia/core/lib/electron-common/electron-token'); // Fix the window reloading issue, see: https://github.com/electron/electron/issues/22119 app.allowRendererProcessReuse = false; const applicationName = `Arduino Pro IDE`; const isSingleInstance = false; const disallowReloadKeybinding = false; const defaultWindowOptionsAdditions = {}; if (isSingleInstance && !app.requestSingleInstanceLock()) { // There is another instance running, exit now. The other instance will request focus. app.quit(); return; } const nativeKeymap = require('native-keymap'); const Storage = require('electron-store'); const electronStore = new Storage(); const electronSecurityToken = { value: v4(), }; // Make it easy for renderer process to fetch the ElectronSecurityToken: global[ElectronSecurityToken] = electronSecurityToken; app.on('ready', () => { // Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit") // See: https://github.com/electron-userland/electron-builder/issues/2468 app.setName(applicationName); const { screen } = electron; // Remove the default electron menus, waiting for the application to set its own. Menu.setApplicationMenu(Menu.buildFromTemplate([{ role: 'help', submenu: [{ role: 'toggleDevTools' }] }])); function createNewWindow(theUrl) { // We must center by hand because `browserWindow.center()` fails on multi-screen setups // See: https://github.com/electron/electron/issues/3490 const { bounds } = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()); const height = Math.floor(bounds.height * (2 / 3)); const width = Math.floor(bounds.width * (2 / 3)); const y = Math.floor(bounds.y + (bounds.height - height) / 2); const x = Math.floor(bounds.x + (bounds.width - width) / 2); const WINDOW_STATE = 'windowstate'; const windowState = electronStore.get(WINDOW_STATE, { width, height, x, y }); const persistedWindowOptionsAdditions = electronStore.get('windowOptions', {}); const windowOptionsAdditions = { ...defaultWindowOptionsAdditions, ...persistedWindowOptionsAdditions }; let windowOptions = { show: false, title: applicationName, width: windowState.width, height: windowState.height, minWidth: 200, minHeight: 120, x: windowState.x, y: windowState.y, isMaximized: windowState.isMaximized, ...windowOptionsAdditions, webPreferences: { nodeIntegration: true } }; // Always hide the window, we will show the window when it is ready to be shown in any case. const newWindow = new BrowserWindow(windowOptions); if (windowOptions.isMaximized) { newWindow.maximize(); } newWindow.on('ready-to-show', () => newWindow.show()); if (disallowReloadKeybinding) { newWindow.on('focus', event => { for (const accelerator of ['CmdOrCtrl+R', 'F5']) { globalShortcut.register(accelerator, () => { }); } }); newWindow.on('blur', event => globalShortcut.unregisterAll()); } // Prevent calls to "window.open" from opening an ElectronBrowser window, // and rather open in the OS default web browser. newWindow.webContents.on('new-window', (event, url) => { event.preventDefault(); shell.openExternal(url); }); // Save the window geometry state on every change const saveWindowState = () => { try { let bounds; if (newWindow.isMaximized()) { bounds = electronStore.get(WINDOW_STATE, {}); } else { bounds = newWindow.getBounds(); } electronStore.set(WINDOW_STATE, { isMaximized: newWindow.isMaximized(), width: bounds.width, height: bounds.height, x: bounds.x, y: bounds.y }); } catch (e) { console.error("Error while saving window state.", e); } }; let delayedSaveTimeout; const saveWindowStateDelayed = () => { if (delayedSaveTimeout) { clearTimeout(delayedSaveTimeout); } delayedSaveTimeout = setTimeout(saveWindowState, 1000); }; newWindow.on('close', saveWindowState); newWindow.on('resize', saveWindowStateDelayed); newWindow.on('move', saveWindowStateDelayed); // Fired when a beforeunload handler tries to prevent the page unloading newWindow.webContents.on('will-prevent-unload', async event => { const { response } = await dialog.showMessageBox(newWindow, { type: 'question', buttons: ['Yes', 'No'], title: 'Confirm', message: 'Are you sure you want to quit?', detail: 'Any unsaved changes will not be saved.' }); if (response === 0) { // 'Yes' // This ignores the beforeunload callback, allowing the page to unload event.preventDefault(); } }); // Notify the renderer process on keyboard layout change nativeKeymap.onDidChangeKeyboardLayout(() => { if (!newWindow.isDestroyed()) { const newLayout = { info: nativeKeymap.getCurrentKeyboardLayout(), mapping: nativeKeymap.getKeyMap() }; newWindow.webContents.send('keyboardLayoutChanged', newLayout); } }); if (!!theUrl) { newWindow.loadURL(theUrl); } return newWindow; } app.on('window-all-closed', () => { app.quit(); }); ipcMain.on('create-new-window', (event, url) => { createNewWindow(url); }); ipcMain.on('open-external', (event, url) => { shell.openExternal(url); }); ipcMain.on('set-window-options', (event, options) => { electronStore.set('windowOptions', options); }); ipcMain.on('get-persisted-window-options-additions', event => { event.returnValue = electronStore.get('windowOptions', {}); }); // Check whether we are in bundled application or development mode. // @ts-ignore const devMode = process.defaultApp || /node_modules[/]electron[/]/.test(process.execPath); // Check if we should run everything as one process. const noBackendFork = process.argv.includes('--no-cluster'); const mainWindow = createNewWindow(); if (isSingleInstance) { app.on('second-instance', (event, commandLine, workingDirectory) => { // Someone tried to run a second instance, we should focus our window. if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow.isMinimized()) { mainWindow.restore(); } mainWindow.focus() } }) } const setElectronSecurityToken = async port => { await electron.session.defaultSession.cookies.set({ url: `http://localhost:${port}/`, name: ElectronSecurityToken, value: JSON.stringify(electronSecurityToken), httpOnly: true }); }; const loadMainWindow = port => { if (!mainWindow.isDestroyed()) { mainWindow.loadURL('file://' + join(__dirname, '../../lib/index.html') + '?port=' + port); } }; // We cannot use the `process.cwd()` as the application project path (the location of the `package.json` in other words) // in a bundled electron application because it depends on the way we start it. For instance, on OS X, these are a differences: // https://github.com/eclipse-theia/theia/issues/3297#issuecomment-439172274 process.env.THEIA_APP_PROJECT_PATH = resolve(__dirname, '..', '..'); // Set the electron version for both the dev and the production mode. (https://github.com/eclipse-theia/theia/issues/3254) // Otherwise, the forked backend processes will not know that they're serving the electron frontend. // The forked backend should patch its `process.versions.electron` with this value if it is missing. process.env.THEIA_ELECTRON_VERSION = process.versions.electron; const mainPath = join(__dirname, '..', 'backend', 'main'); // We spawn a separate process for the backend for Express to not run in the Electron main process. // See: https://github.com/eclipse-theia/theia/pull/7361#issuecomment-601272212 // But when in debugging we want to run everything in the same process to make things easier. if (noBackendFork) { process.env[ElectronSecurityToken] = JSON.stringify(electronSecurityToken); require(mainPath).then(async (address) => { await setElectronSecurityToken(address.port); loadMainWindow(address.port); }).catch((error) => { console.error(error); app.exit(1); }); } else { // We want to pass flags passed to the Electron app to the backend process. // Quirk: When developing from sources, we execute Electron as `electron.exe electron-main.js ...args`, but when bundled, // the command looks like `bundled-application.exe ...args`. let args = process.argv.slice(devMode ? 2 : 1); if (process.platform === 'darwin') { // https://github.com/electron/electron/issues/3657 // https://stackoverflow.com/questions/10242115/os-x-strange-psn-command-line-parameter-when-launched-from-finder#comment102377986_10242200 // macOS appends an extra `-psn_0_someNumber` arg if a file is opened from Finder after downloading from the Internet. // "AppName" is an app downloaded from the Internet. Are you sure you want to open it? args = args.filter(arg => !arg.startsWith('-psn')); } const cp = fork(mainPath, args, { env: Object.assign({ [ElectronSecurityToken]: JSON.stringify(electronSecurityToken), }, process.env) }); cp.on('message', async (address) => { await setElectronSecurityToken(address.port); loadMainWindow(address.port); }); cp.on('error', (error) => { console.error(error); app.exit(1); }); app.on('quit', () => { // If we forked the process for the clusters, we need to manually terminate it. // See: https://github.com/eclipse-theia/theia/issues/835 process.kill(cp.pid); }); } });