The native-menu handlers resolved the send target with BrowserWindow.getFocusedWindow(), which returns null once the OS file dialog closes and focus does not return to the app window. webContents was then read off null, throwing 'Cannot read properties of null (reading webContents)' in the main process and dropping the selected *.rge paths before they reached plotProfiles/plotFractions. Send to the window the dialog belongs to (win) instead, fixing Plot Profiles, Plot Fractions, Plot Mean, Open, Save Folder, Save, and Print.
528 lines
15 KiB
JavaScript
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, 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 : 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 = 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 : getDefaultDialogPath(),
|
|
properties:["openDirectory"]}
|
|
).then(result => {
|
|
setImmediate(function() {
|
|
console.log(result.filePaths)
|
|
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(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 = 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",
|
|
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 = 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",
|
|
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 = 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",
|
|
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 = 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 : 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);
|