const path = require('path'); const fs = require('fs'); const { spawnSync } = require('child_process'); const { app, BrowserWindow, Menu, clipboard, dialog, ipcMain, nativeImage } = require('electron'); let currentWorkPath = process.cwd(); const startupOptions = parseStartupOptions(process.argv); let quickStartWindow = null; // Packaged builds retain the class expected by installed desktop launchers. // A distinct development class prevents npm start from inheriting the icon // cached for an older system installation of TrimSP. app.setName(app.isPackaged ? 'TrimSP' : 'TrimSPDev'); if (process.platform === 'linux' && !app.isPackaged) { // KDE resolves taskbar icons through matching desktop entries and ignores // Electron's development-window icon. Keep a hidden per-user entry tied // to this checkout so npm start uses the current repository artwork. try { const dataHome = process.env.XDG_DATA_HOME || path.join(app.getPath('home'), '.local', 'share'); const applicationsDir = path.join(dataHome, 'applications'); const desktopFile = path.join(applicationsDir, 'trimsp-dev.desktop'); const executable = process.execPath.replace(/"/g, '\\"'); const projectPath = __dirname.replace(/"/g, '\\"'); const iconPath = path.join(__dirname, 'icon.png'); const desktopContent = [ '[Desktop Entry]', 'Type=Application', 'Name=TRIMSP-NL Workbench (Development)', `Exec="${executable}" "${projectPath}"`, `Icon=${iconPath}`, `X-TrimSP-Icon-MTime=${fs.statSync(iconPath).mtimeMs}`, 'StartupWMClass=TrimSPDev', 'NoDisplay=true', 'Categories=Science;', '', ].join('\n'); fs.mkdirSync(applicationsDir, {recursive: true}); if (!fs.existsSync(desktopFile) || fs.readFileSync(desktopFile, 'utf8') !== desktopContent) { fs.writeFileSync(desktopFile, desktopContent, 'utf8'); // KDE caches desktop identities and icons; refresh that user cache only // when this entry changes. Other desktops safely ignore ENOENT here. spawnSync('kbuildsycoca6', ['--noincremental'], {stdio: 'ignore'}); } app.setDesktopName('trimsp-dev.desktop'); } catch (error) { console.warn('Could not register the development desktop entry:', error); } } function parseStartupOptions(argv) { const options = { configPaths: [], autoRun: false, }; for (const arg of argv.slice(2)) { if (arg === '--run') { options.autoRun = true; } else if (!arg.startsWith('--')) { let candidates = [path.resolve(arg)]; if (/[*?]/.test(arg)) { const resolvedPattern = path.resolve(arg); const parsed = path.parse(resolvedPattern); const relativePattern = resolvedPattern.slice(parsed.root.length); const segments = relativePattern.split(path.sep).filter(Boolean); candidates = [parsed.root || '.']; for (const segment of segments) { const nextCandidates = []; let matcher = null; if (/[*?]/.test(segment)) { let pattern = '^'; for (const char of segment) { if (char === '*') { pattern += '.*'; } else if (char === '?') { pattern += '.'; } else { pattern += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&'); } } pattern += '$'; matcher = new RegExp(pattern); } for (const candidate of candidates) { if (!matcher) { const nextPath = path.join(candidate, segment); if (typeof nextPath === 'string' && fs.existsSync(nextPath)) { nextCandidates.push(nextPath); } continue; } if (typeof candidate !== 'string' || !fs.existsSync(candidate) || !fs.statSync(candidate).isDirectory()) { continue; } const entries = fs.readdirSync(candidate).sort(); for (const entry of entries) { if (matcher.test(entry)) { nextCandidates.push(path.join(candidate, entry)); } } } candidates = nextCandidates; if (candidates.length === 0) { break; } } candidates = candidates .filter((candidate) => typeof candidate === 'string' && fs.existsSync(candidate) && fs.statSync(candidate).isFile()) .sort(); } for (const candidate of candidates) { if ( typeof candidate === 'string' && path.extname(candidate).toLowerCase() === '.cfg' && fs.existsSync(candidate) && fs.statSync(candidate).isFile() && !options.configPaths.includes(candidate) ) { options.configPaths.push(candidate); } } } } return options; } function showAboutDialog(parentWindow) { const detail = [ 'Graphical workbench for low-energy muon and ion implantation simulations.', `Workbench version: ${app.getVersion()}`, 'License: GNU General Public License v3 or later (GPL-3.0-or-later).', 'Maintained by the Low Energy Muons group at the Paul Scherrer Institute.', 'Main contributor: Zaher Salman.', '', 'TRIM.SP-NL is the generalized multilayer Fortran transport engine.', 'TRIMSP-NL Workbench combines the engine with the GUI and scan workflow.', '', 'If you use this software in published work, please cite the relevant', 'TRIM.SP references and the TRIMSP-NL Workbench paper.', '', 'Key references:', 'Z. Salman, R. M. L. McFadden, and T. Prokscha, Modernization and', 'Statistical Validation of a Multilayer TRIM.SP Code for Low-Energy Muon', 'and Ion Implantation.', 'J. P. Biersack and W. Eckstein, Appl. Phys. A 34, 73-94 (1984).', 'W. Eckstein, Computer Simulation of Ion-Solid Interactions (1991).', 'E. Morenzoni et al., NIM B 192, 245-266 (2002).', '', 'Project repository:', 'Gitea: gitea.psi.ch/LMU/TRIMSP', 'Web application: musruser.psi.ch/TRIMSP-NL/' ].join('\n'); dialog.showMessageBox(parentWindow, { type: 'info', title: 'About TRIMSP-NL Workbench', message: 'TRIMSP-NL Workbench', detail: detail, buttons: ['OK'], icon: parentWindow ? undefined : undefined }); } // Some Linux/remote desktop setups expose a broken GPU context to Electron. // Fall back to software rendering instead of crashing on startup. app.disableHardwareAcceleration(); if (process.platform === 'linux') { // The XDG portal dialog backend ignores defaultPath on some systems and // may cancel folder selection entirely in X11/SSH sessions. app.commandLine.appendSwitch('xdg-portal-required-version', '999'); } function getDefaultDialogPath() { return currentWorkPath || process.cwd(); } // Remember the folder in which the last file dialog opened or saved, so the // dialogs reopen where the user actually works rather than at an OS default // (or always at process.cwd()). Shared by every dialog type, stored in the // app's userData dir, and durable across restarts; falls back to the current // work path. const DIALOG_FOLDERS_FILE = path.join(app.getPath('userData'), 'dialog-folders.json'); let dialogFolders = null; function loadDialogFolders() { if (dialogFolders === null) { try { dialogFolders = JSON.parse(fs.readFileSync(DIALOG_FOLDERS_FILE, 'utf8')); } catch (err) { dialogFolders = {}; } } return dialogFolders; } function rememberLastDialogFolder(folder) { if (!folder) return; loadDialogFolders(); dialogFolders.lastUsed = folder; try { fs.writeFileSync(DIALOG_FOLDERS_FILE, JSON.stringify(dialogFolders, null, 2)); } catch (err) { console.log('could not save dialog folder memory:', err); } } function rememberLastDialogFolderFromFiles(filePaths) { if (!filePaths || filePaths.length === 0) return; rememberLastDialogFolder(path.dirname(filePaths[0])); } function lastDialogFolder() { const fallback = getDefaultDialogPath(); const saved = loadDialogFolders().lastUsed; if (saved && fs.existsSync(saved)) return saved; return fallback; } function getWindowIconPath() { if (app.isPackaged) { return path.join(process.resourcesPath, 'app.asar', 'icon.png'); } return path.join(__dirname, 'icon.png'); } function showQuickStartWindow(sectionId = 'helpLayers') { if (quickStartWindow && !quickStartWindow.isDestroyed()) { quickStartWindow.show(); quickStartWindow.focus(); quickStartWindow.webContents.send('quickStartSection', { sectionId: sectionId, version: app.getVersion() }); return; } quickStartWindow = new BrowserWindow({ title: 'TRIMSP-NL Quick Start', width: 720, height: 680, minWidth: 480, minHeight: 360, resizable: true, autoHideMenuBar: true, icon: nativeImage.createFromPath(getWindowIconPath()), webPreferences: { contextIsolation: false, nodeIntegration: true, } }); quickStartWindow.webContents.once('did-finish-load', () => { quickStartWindow.webContents.send('quickStartSection', { sectionId: sectionId, version: app.getVersion() }); }); quickStartWindow.loadFile('quick-start.html'); quickStartWindow.on('closed', () => { quickStartWindow = null; }); } function createWindow () { const windowIcon = nativeImage.createFromPath(getWindowIconPath()); const win = new BrowserWindow({ width: 1000, height: 610, icon: windowIcon, webPreferences: { contextIsolation: false, nodeIntegration: true, nativeWindowOpen: true, enableRemoteModule: true, } }) // Pass a native image explicitly for Linux panels that can use the window // icon directly. KDE resolves the development icon through the matching // desktop entry registered above. if (process.platform === 'linux' && !windowIcon.isEmpty()) { win.setIcon(windowIcon); } let template = [ { label: 'File', submenu: [ { label: 'Open', id : 'openItem', accelerator: 'CmdOrCtrl+O', click () { dialog.showOpenDialog(win, { title : "Load configuration file", defaultPath : lastDialogFolder(), //buttonLabel : "Custom button", filters :[ {name: 'Config file type', extensions: ['cfg']}, ], properties: ['openFile']} ).then(result => { console.log(result.canceled); console.log(result.filePaths); if (!result.canceled) { rememberLastDialogFolderFromFiles(result.filePaths); setImmediate(function() { var focusedWindow = win; focusedWindow.webContents.send('openFile',result.filePaths); }); } }).catch(err => { console.log(err); }) } }, { label: 'Save Folder...', accelerator: 'CmdOrCtrl+F', click () { dialog.showOpenDialog(win, { title: "Select folder", defaultPath : lastDialogFolder(), properties:["openDirectory"]} ).then(result => { setImmediate(function() { console.log(result.filePaths) if (!result.canceled) { rememberLastDialogFolder(result.filePaths[0]); } var focusedWindow = win; focusedWindow.webContents.send('selectFolder',result.filePaths); }); }).catch(err => { console.log(err); }) } }, { label: 'Save', accelerator: 'CmdOrCtrl+S', click () { setImmediate(function() { var focusedWindow = win; focusedWindow.webContents.send('saveFile',''); }); } }, { label: 'Save As...', accelerator: 'CmdOrCtrl+Shift+S', click () { dialog.showSaveDialog(win, { title : "Save configuration file", defaultPath : path.join(lastDialogFolder(), 'TrimSP.cfg'), filters :[ {name: 'Config file type', extensions: ['cfg']}, {name: 'All Files', extensions: ['*']} ], properties: ['showOverwriteConfirmation']} ).then(result => { if (result.canceled || !result.filePath) { return; } rememberLastDialogFolder(path.dirname(result.filePath)); setImmediate(function() { var focusedWindow = win; focusedWindow.webContents.send('saveFile',result.filePath); }); }).catch(err => { console.log(err); }) } }, { label: 'Print', accelerator: 'CmdOrCtrl+P', click () { setImmediate(function() { var focusedWindow = win; const options = {}; focusedWindow.webContents.print(options, (success, errorType) => { if (!success) console.log(errorType) }); }); } }, { role: 'quit' } ] }, { role: 'editMenu' }, { label: 'Plot', submenu: [ { label: 'Plot Profiles', //accelerator: 'CmdOrCtrl+P P', click () { dialog.showOpenDialog(win, { title : "Select rge files", defaultPath : lastDialogFolder(), filters :[ {name: 'Profile file type', extensions: ['rge']}, {name: 'All Files', extensions: ['*']} ], properties: ['openFile', 'multiSelections']} ).then(result => { console.log(result.canceled); console.log(result.filePaths); if (!result.canceled) { rememberLastDialogFolderFromFiles(result.filePaths); setImmediate(function() { var focusedWindow = win; focusedWindow.webContents.send('plotProf',result.filePaths); }); } }).catch(err => { console.log(err); }) } }, { label: 'Plot Fractions', //accelerator: 'CmdOrCtrl+P F', click () { dialog.showOpenDialog(win, { title : "Select sequence file", defaultPath : lastDialogFolder(), filters :[ {name: 'Sequence file type', extensions: ['dat']}, {name: 'All Files', extensions: ['*']} ], properties: ['openFile']} ).then(result => { console.log(result.canceled); console.log(result.filePaths); if (!result.canceled) { rememberLastDialogFolderFromFiles(result.filePaths); setImmediate(function() { var focusedWindow = win; focusedWindow.webContents.send('plotFrac',result.filePaths); }); } }).catch(err => { console.log(err); }) } }, { label: 'Plot Mean', //accelerator: 'CmdOrCtrl+P M', click () { dialog.showOpenDialog(win, { title : "Select sequence file", defaultPath : lastDialogFolder(), filters :[ {name: 'Sequence file type', extensions: ['dat']}, {name: 'All Files', extensions: ['*']} ], properties: ['openFile']} ).then(result => { console.log(result.canceled); console.log(result.filePaths); if (!result.canceled) { rememberLastDialogFolderFromFiles(result.filePaths); setImmediate(function() { var focusedWindow = win; focusedWindow.webContents.send('plotMean',result.filePaths); }); } }).catch(err => { console.log(err); }) } } ] }, { role: 'viewMenu' }, { role: 'help', submenu: [ { label: 'Quick Start', accelerator: 'F1', click () { showQuickStartWindow(); } }, { label: 'About TRIMSP-NL Workbench', click () { showAboutDialog(win); } } ] } ] let menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) win.loadFile('TrimSP.html'); // Comment the following line to start without Dev // win.openDevTools(); } app.whenReady().then(createWindow) global.path = app.getAppPath(); //console.log("From main: ",global.path); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } }) app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow() } }) // Reply to calls from browser button ipcMain.on('browseFolder', (event, args) => { console.log('received a message: '+args); const senderWindow = BrowserWindow.fromWebContents(event.sender); dialog.showOpenDialog(senderWindow, { title: "Select folder", defaultPath : lastDialogFolder(), properties:["openDirectory"]} ).then(result => { console.log(result) if (result.canceled || result.filePaths.length === 0) { event.reply('browseFolderFallback'); return; } rememberLastDialogFolder(result.filePaths[0]); event.reply('browseFolder', result.filePaths); app.setPath('temp',result.filePaths[0]); }).catch(err => { console.log(err); }) }); ipcMain.on('updateWorkPath', (event, folder) => { if (folder) { currentWorkPath = folder; } }); ipcMain.handle('getStartupOptions', async () => { return startupOptions; }); ipcMain.on('openQuickStartWindow', (event, options) => { const sectionId = options && options.sectionId ? options.sectionId : 'helpLayers'; showQuickStartWindow(sectionId); }); ipcMain.handle('copyConfigurationLink', async (event, url) => { if (typeof url !== 'string' || url.length > 500000 || !url.startsWith('http://musruser.psi.ch/TRIMSP-NL/')) { throw new Error('Invalid configuration link'); } clipboard.writeText(url); return true; }); //ipcMain.on('browseFolder-send', (event, args) => { // dialog.showOpenDialog(null, args).then(filePaths => { // event.sender.dend('browseFolder', filePaths); // }).catch(err => { // console.log(err); // }) //}); //menuItem = menu.getMenuItemById('openItem'); //console.log(menuItem);