Files
TRIMSP/main.js
T

528 lines
15 KiB
JavaScript

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();
}
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 });
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 });
});
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 : getDefaultDialogPath(),
//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) {
setImmediate(function() {
var focusedWindow = BrowserWindow.getFocusedWindow();
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 : getDefaultDialogPath(),
properties:["openDirectory"]}
).then(result => {
setImmediate(function() {
console.log(result.filePaths)
var focusedWindow = BrowserWindow.getFocusedWindow();
focusedWindow.webContents.send('selectFolder',result.filePaths);
});
}).catch(err => {
console.log(err);
})
}
},
{
label: 'Save',
accelerator: 'CmdOrCtrl+S',
click () {
setImmediate(function() {
var focusedWindow = BrowserWindow.getFocusedWindow();
focusedWindow.webContents.send('saveFile','');
});
}
},
{
label: 'Save As...',
accelerator: 'CmdOrCtrl+Shift+S',
click () {
dialog.showSaveDialog(win,
{ title : "Save configuration file",
defaultPath : path.join(getDefaultDialogPath(), 'TrimSP.cfg'),
filters :[
{name: 'Config file type', extensions: ['cfg']},
{name: 'All Files', extensions: ['*']}
],
properties: ['showOverwriteConfirmation']}
).then(result => {
if (result.canceled || !result.filePath) {
return;
}
setImmediate(function() {
var focusedWindow = BrowserWindow.getFocusedWindow();
focusedWindow.webContents.send('saveFile',result.filePath);
});
}).catch(err => {
console.log(err);
})
}
},
{
label: 'Print',
accelerator: 'CmdOrCtrl+P',
click () {
setImmediate(function() {
var focusedWindow = BrowserWindow.getFocusedWindow();
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",
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) {
setImmediate(function() {
var focusedWindow = BrowserWindow.getFocusedWindow();
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",
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) {
setImmediate(function() {
var focusedWindow = BrowserWindow.getFocusedWindow();
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",
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) {
setImmediate(function() {
var focusedWindow = BrowserWindow.getFocusedWindow();
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 : getDefaultDialogPath(),
properties:["openDirectory"]}
).then(result => {
console.log(result)
if (result.canceled || result.filePaths.length === 0) {
event.reply('browseFolderFallback');
return;
}
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);