7254f7b4d1
Build / Build (macos-latest) (push) Has been cancelled
Build / Build (ubuntu-latest) (push) Has been cancelled
Build / Build (windows-latest) (push) Has been cancelled
Build / Analyze (javascript) (push) Has been cancelled
Build / Analyze (python) (push) Has been cancelled
1109 lines
38 KiB
JavaScript
1109 lines
38 KiB
JavaScript
|
|
import * as base from './base.js';
|
|
import * as electron from 'electron';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import * as process from 'process';
|
|
import * as updater from 'electron-updater';
|
|
import * as url from 'url';
|
|
|
|
const app = {};
|
|
|
|
app.Application = class {
|
|
|
|
constructor() {
|
|
this._views = new app.ViewCollection(this);
|
|
this._configuration = new app.ConfigurationService();
|
|
this._menu = new app.MenuService(this._views);
|
|
this._openQueue = [];
|
|
this._package = {};
|
|
}
|
|
|
|
async start() {
|
|
const dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
const packageFile = path.join(path.dirname(dirname), 'package.json');
|
|
const packageContent = fs.readFileSync(packageFile, 'utf-8');
|
|
this._package = JSON.parse(packageContent);
|
|
|
|
electron.app.setAppUserModelId('com.lutzroeder.netron');
|
|
electron.app.allowRendererProcessReuse = true;
|
|
|
|
// Workaround electron/electron#50419 only available in Electron 43
|
|
if (process.platform === 'darwin') {
|
|
electron.app.commandLine.appendSwitch('use-mock-keychain');
|
|
} else if (process.platform === 'linux') {
|
|
electron.app.commandLine.appendSwitch('password-store', 'basic');
|
|
}
|
|
|
|
if (!electron.app.requestSingleInstanceLock()) {
|
|
electron.app.quit();
|
|
return;
|
|
}
|
|
|
|
electron.app.on('second-instance', (event, commandLine, workingDirectory) => {
|
|
const currentDirectory = process.cwd();
|
|
process.chdir(workingDirectory);
|
|
const open = this._parseCommandLine(commandLine);
|
|
process.chdir(currentDirectory);
|
|
if (!open && !this._views.empty) {
|
|
const view = this._views.first();
|
|
if (view) {
|
|
view.restore();
|
|
}
|
|
}
|
|
});
|
|
electron.ipcMain.on('get-environment', (event) => {
|
|
event.returnValue = this.environment;
|
|
});
|
|
electron.ipcMain.on('get-configuration', (event, obj) => {
|
|
event.returnValue = this._configuration.has(obj.name) ? this._configuration.get(obj.name) : undefined;
|
|
});
|
|
electron.ipcMain.on('set-configuration', (event, obj) => {
|
|
this._configuration.set(obj.name, obj.value);
|
|
this._configuration.save();
|
|
event.returnValue = null;
|
|
});
|
|
electron.ipcMain.on('delete-configuration', (event, obj) => {
|
|
this._configuration.delete(obj.name);
|
|
this._configuration.save();
|
|
event.returnValue = null;
|
|
});
|
|
electron.ipcMain.on('drop-paths', (event, data) => {
|
|
const paths = data.paths.filter((path) => {
|
|
if (fs.existsSync(path)) {
|
|
const stat = fs.statSync(path);
|
|
return stat.isFile() || stat.isDirectory();
|
|
}
|
|
return false;
|
|
});
|
|
this._dropPaths(event.sender, paths);
|
|
event.returnValue = null;
|
|
});
|
|
electron.ipcMain.on('update-recents', (event, data) => {
|
|
this._updateRecents(data.path);
|
|
event.returnValue = null;
|
|
});
|
|
electron.ipcMain.on('show-save-dialog', async (event, options) => {
|
|
const owner = event.sender.getOwnerBrowserWindow();
|
|
const argument = {};
|
|
try {
|
|
const { filePath, canceled } = await electron.dialog.showSaveDialog(owner, options);
|
|
argument.filePath = filePath;
|
|
argument.canceled = canceled;
|
|
} catch (error) {
|
|
argument.error = error.message;
|
|
}
|
|
event.sender.send('show-save-dialog-complete', argument);
|
|
});
|
|
electron.ipcMain.on('execute', async (event, data) => {
|
|
const owner = event.sender.getOwnerBrowserWindow();
|
|
const argument = {};
|
|
try {
|
|
argument.value = await this.execute(data.name, data.value || null, owner);
|
|
} catch (error) {
|
|
argument.error = error.message;
|
|
}
|
|
event.sender.send('execute-complete', argument);
|
|
});
|
|
|
|
electron.app.on('will-finish-launching', () => {
|
|
electron.app.on('open-file', (event, path) => {
|
|
this._openPath(path);
|
|
});
|
|
});
|
|
|
|
electron.app.on('ready', () => {
|
|
this._ready();
|
|
});
|
|
|
|
electron.app.on('window-all-closed', () => {
|
|
if (process.platform !== 'darwin') {
|
|
electron.app.quit();
|
|
}
|
|
});
|
|
|
|
electron.app.on('will-quit', () => {
|
|
this._configuration.save();
|
|
});
|
|
|
|
this._parseCommandLine(process.argv);
|
|
await this._checkForUpdates();
|
|
}
|
|
|
|
get environment() {
|
|
this._environment = this._environment || {
|
|
packaged: electron.app.isPackaged,
|
|
name: this._package.productName,
|
|
version: this._package.version,
|
|
date: this._package.date,
|
|
repository: `https://github.com/${this._package.repository}`,
|
|
platform: process.platform,
|
|
separator: path.sep,
|
|
titlebar: true // process.platform === 'darwin'
|
|
};
|
|
return this._environment;
|
|
}
|
|
|
|
_parseCommandLine(argv) {
|
|
let open = false;
|
|
if (argv.length > 1) {
|
|
for (const arg of argv.slice(1)) {
|
|
const dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
if (!arg.startsWith('-') && arg !== path.dirname(dirname)) {
|
|
const extension = path.extname(arg).toLowerCase();
|
|
if (extension !== '' && extension !== '.js' && fs.existsSync(arg)) {
|
|
const stat = fs.statSync(arg);
|
|
if (stat.isFile() || stat.isDirectory()) {
|
|
this._openPath(arg);
|
|
open = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return open;
|
|
}
|
|
|
|
_ready() {
|
|
this._configuration.open();
|
|
if (this._openQueue) {
|
|
const queue = this._openQueue;
|
|
this._openQueue = null;
|
|
while (queue.length > 0) {
|
|
const file = queue.shift();
|
|
this._openPath(file);
|
|
}
|
|
}
|
|
if (this._views.empty) {
|
|
this._views.openView();
|
|
}
|
|
this._updateMenu();
|
|
this._views.on('active-view-changed', () => {
|
|
this._menu.update();
|
|
});
|
|
this._views.on('active-view-updated', () => {
|
|
this._menu.update();
|
|
});
|
|
}
|
|
|
|
_open(path) {
|
|
let paths = path ? [path] : [];
|
|
if (paths.length === 0) {
|
|
const extensions = new base.Metadata().extensions;
|
|
const options = {
|
|
properties: ['openFile'],
|
|
filters: [{ name: 'All Model Files', extensions }]
|
|
};
|
|
const owner = electron.BrowserWindow.getFocusedWindow();
|
|
paths = electron.dialog.showOpenDialogSync(owner, options);
|
|
}
|
|
if (Array.isArray(paths) && paths.length > 0) {
|
|
for (const path of paths) {
|
|
this._openPath(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
_openPath(path) {
|
|
if (this._openQueue) {
|
|
this._openQueue.push(path);
|
|
return;
|
|
}
|
|
if (path && path.length > 0) {
|
|
const exists = fs.existsSync(path);
|
|
if (exists) {
|
|
const stat = fs.statSync(path);
|
|
if (stat.isFile() || stat.isDirectory()) {
|
|
const views = Array.from(this._views.views);
|
|
// find existing view for this file
|
|
let view = views.find((view) => view.match(path));
|
|
// find empty welcome window
|
|
if (!view) {
|
|
view = views.find((view) => view.match(null));
|
|
}
|
|
// create new window
|
|
if (!view) {
|
|
view = this._views.openView();
|
|
}
|
|
view.open(path);
|
|
}
|
|
}
|
|
this._updateRecents(exists ? path : undefined);
|
|
}
|
|
}
|
|
|
|
_dropPaths(sender, paths) {
|
|
const window = sender.getOwnerBrowserWindow();
|
|
let view = this._views.get(window);
|
|
for (const path of paths) {
|
|
if (view) {
|
|
view.open(path);
|
|
view = null;
|
|
} else {
|
|
this._openPath(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
async _export() {
|
|
const view = this._views.activeView;
|
|
if (view && view.path) {
|
|
let defaultPath = 'Untitled';
|
|
const file = view.path;
|
|
const lastIndex = file.lastIndexOf('.');
|
|
if (lastIndex !== -1) {
|
|
defaultPath = file.substring(0, lastIndex);
|
|
}
|
|
const owner = electron.BrowserWindow.getFocusedWindow();
|
|
const options = {
|
|
title: 'Export',
|
|
defaultPath,
|
|
buttonLabel: 'Export',
|
|
filters: [
|
|
{ name: 'PNG', extensions: ['png'] },
|
|
{ name: 'SVG', extensions: ['svg'] }
|
|
]
|
|
};
|
|
const { filePath, canceled } = await electron.dialog.showSaveDialog(owner, options);
|
|
if (filePath && !canceled) {
|
|
view.execute('export', { 'file': filePath });
|
|
}
|
|
}
|
|
}
|
|
|
|
async execute(command, value, window) {
|
|
switch (command) {
|
|
case 'open': this._open(value); break;
|
|
case 'export': await this._export(); break;
|
|
case 'close': window.close(); break;
|
|
case 'quit': electron.app.quit(); break;
|
|
case 'reload': this._reload(); break;
|
|
case 'report-issue': electron.shell.openExternal(`https://github.com/${this._package.repository}/issues/new`); break;
|
|
case 'about': this._about(); break;
|
|
default: {
|
|
const view = this._views.get(window) || this._views.activeView;
|
|
if (view && view.get(`${command}.enabled`) !== false) {
|
|
view.execute(command, value || {});
|
|
}
|
|
this._menu.update();
|
|
}
|
|
}
|
|
}
|
|
|
|
_reload() {
|
|
const view = this._views.activeView;
|
|
if (view && view.path) {
|
|
view.open(view.path);
|
|
}
|
|
}
|
|
|
|
async _checkForUpdates() {
|
|
if (!electron.app.isPackaged) {
|
|
return;
|
|
}
|
|
const autoUpdater = updater.default.autoUpdater;
|
|
if (autoUpdater.app && autoUpdater.app.appUpdateConfigPath && !fs.existsSync(autoUpdater.app.appUpdateConfigPath)) {
|
|
return;
|
|
}
|
|
const promise = autoUpdater.checkForUpdates();
|
|
if (promise) {
|
|
promise.catch((error) => {
|
|
/* eslint-disable no-console */
|
|
console.log(error.message);
|
|
/* eslint-enable no-console */
|
|
});
|
|
}
|
|
}
|
|
|
|
_about() {
|
|
let view = this._views.activeView;
|
|
if (!view) {
|
|
view = this._views.openView();
|
|
}
|
|
view.execute('about');
|
|
}
|
|
|
|
_updateRecents(path) {
|
|
let updated = false;
|
|
let recents = this._configuration.has('recents') ? this._configuration.get('recents') : [];
|
|
if (path && (recents.length === 0 || recents[0] !== path)) {
|
|
recents = recents.filter((recent) => path !== recent);
|
|
recents.unshift(path);
|
|
updated = true;
|
|
}
|
|
const value = [];
|
|
for (const recent of recents) {
|
|
if (value.length >= 9) {
|
|
updated = true;
|
|
break;
|
|
}
|
|
if (!fs.existsSync(recent)) {
|
|
updated = true;
|
|
continue;
|
|
}
|
|
const stat = fs.statSync(recent);
|
|
if (!stat.isFile() && !stat.isDirectory()) {
|
|
updated = true;
|
|
continue;
|
|
}
|
|
value.push(recent);
|
|
}
|
|
if (updated) {
|
|
this._configuration.set('recents', value);
|
|
this._updateMenu();
|
|
}
|
|
}
|
|
|
|
_updateMenu() {
|
|
|
|
let recents = [];
|
|
if (this._configuration.has('recents')) {
|
|
const value = this._configuration.get('recents');
|
|
recents = value.map((recent) => app.Application.location(recent));
|
|
}
|
|
|
|
if (this.environment.titlebar && recents.length > 0) {
|
|
for (const view of this._views.views) {
|
|
view.execute('recents', recents);
|
|
}
|
|
}
|
|
|
|
const darwin = process.platform === 'darwin';
|
|
if (!this.environment.titlebar || darwin) {
|
|
const menuRecentsTemplate = [];
|
|
for (let i = 0; i < recents.length; i++) {
|
|
const recent = recents[i];
|
|
menuRecentsTemplate.push({
|
|
path: recent.path,
|
|
label: recent.label,
|
|
accelerator: (darwin ? 'Cmd+' : 'Ctrl+') + (i + 1).toString(),
|
|
click: (item) => this._openPath(item.path)
|
|
});
|
|
}
|
|
|
|
const menuTemplate = [];
|
|
|
|
if (darwin) {
|
|
menuTemplate.unshift({
|
|
label: electron.app.name,
|
|
submenu: [
|
|
{
|
|
label: `About ${electron.app.name}`,
|
|
click: () => /* this.execute('about', null) */ this._about()
|
|
},
|
|
{ type: 'separator' },
|
|
{ role: 'hide' },
|
|
{ role: 'hideothers' },
|
|
{ role: 'unhide' },
|
|
{ type: 'separator' },
|
|
{ role: 'quit' }
|
|
]
|
|
});
|
|
}
|
|
|
|
const fileSubmenu = [
|
|
{
|
|
label: '&Open...',
|
|
accelerator: 'CmdOrCtrl+O',
|
|
click: () => this._open(null)
|
|
},
|
|
{
|
|
label: 'Open &Recent',
|
|
submenu: menuRecentsTemplate
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
id: 'file.export',
|
|
label: '&Export...',
|
|
accelerator: 'CmdOrCtrl+Shift+E',
|
|
click: async () => await this.execute('export', null)
|
|
},
|
|
{ type: 'separator' },
|
|
{ role: 'close' },
|
|
];
|
|
|
|
if (!darwin) {
|
|
fileSubmenu.push(
|
|
{ type: 'separator' },
|
|
{ role: 'quit' }
|
|
);
|
|
}
|
|
|
|
menuTemplate.push({
|
|
label: '&File',
|
|
submenu: fileSubmenu
|
|
});
|
|
|
|
if (darwin) {
|
|
electron.systemPreferences.setUserDefault('NSDisabledDictationMenuItem', 'boolean', true);
|
|
electron.systemPreferences.setUserDefault('NSDisabledCharacterPaletteMenuItem', 'boolean', true);
|
|
}
|
|
|
|
menuTemplate.push({
|
|
label: '&Edit',
|
|
submenu: [
|
|
{
|
|
id: 'edit.cut',
|
|
label: 'Cu&t',
|
|
accelerator: 'CmdOrCtrl+X',
|
|
click: async () => await this.execute('cut', null),
|
|
},
|
|
{
|
|
id: 'edit.copy',
|
|
label: '&Copy',
|
|
accelerator: 'CmdOrCtrl+C',
|
|
click: async () => await this.execute('copy', null),
|
|
},
|
|
{
|
|
id: 'edit.paste',
|
|
label: '&Paste',
|
|
accelerator: 'CmdOrCtrl+V',
|
|
click: async () => await this.execute('paste', null),
|
|
},
|
|
{
|
|
id: 'edit.select-all',
|
|
label: 'Select &All',
|
|
accelerator: 'CmdOrCtrl+A',
|
|
click: async () => await this.execute('selectall', null),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
id: 'edit.find',
|
|
label: '&Find...',
|
|
accelerator: 'CmdOrCtrl+F',
|
|
click: async () => await this.execute('find', null),
|
|
}
|
|
]
|
|
});
|
|
|
|
const viewTemplate = {
|
|
label: '&View',
|
|
submenu: [
|
|
{
|
|
id: 'view.toggle-attributes',
|
|
accelerator: 'CmdOrCtrl+D',
|
|
click: async () => await this.execute('toggle', 'attributes'),
|
|
},
|
|
{
|
|
id: 'view.toggle-weights',
|
|
accelerator: 'CmdOrCtrl+I',
|
|
click: async () => await this.execute('toggle', 'weights'),
|
|
},
|
|
{
|
|
id: 'view.toggle-names',
|
|
accelerator: 'CmdOrCtrl+U',
|
|
click: async () => await this.execute('toggle', 'names'),
|
|
},
|
|
{
|
|
id: 'view.toggle-direction',
|
|
accelerator: 'CmdOrCtrl+K',
|
|
click: async () => await this.execute('toggle', 'direction')
|
|
},
|
|
{
|
|
id: 'view.toggle-mousewheel',
|
|
accelerator: 'CmdOrCtrl+M',
|
|
click: async () => await this.execute('toggle', 'mousewheel'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
id: 'view.reload',
|
|
label: '&Reload',
|
|
accelerator: darwin ? 'Cmd+R' : 'F5',
|
|
click: async () => await this._reload(),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
id: 'view.zoom-reset',
|
|
label: 'Actual &Size',
|
|
accelerator: 'Shift+Backspace',
|
|
click: async () => await this.execute('zoom-reset', null),
|
|
},
|
|
{
|
|
id: 'view.zoom-in',
|
|
label: 'Zoom &In',
|
|
accelerator: 'Shift+Up',
|
|
click: async () => await this.execute('zoom-in', null),
|
|
},
|
|
{
|
|
id: 'view.zoom-out',
|
|
label: 'Zoom &Out',
|
|
accelerator: 'Shift+Down',
|
|
click: async () => await this.execute('zoom-out', null),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
id: 'view.show-properties',
|
|
label: '&Properties...',
|
|
accelerator: 'CmdOrCtrl+Enter',
|
|
click: async () => await this.execute('show-properties', null),
|
|
}
|
|
]
|
|
};
|
|
if (!electron.app.isPackaged) {
|
|
viewTemplate.submenu.push({ type: 'separator' });
|
|
viewTemplate.submenu.push({ role: 'toggledevtools' });
|
|
}
|
|
menuTemplate.push(viewTemplate);
|
|
|
|
if (darwin) {
|
|
menuTemplate.push({
|
|
role: 'window',
|
|
submenu: [
|
|
{ role: 'minimize' },
|
|
{ role: 'zoom' },
|
|
{ type: 'separator' },
|
|
{ role: 'front' }
|
|
]
|
|
});
|
|
}
|
|
|
|
const helpSubmenu = [
|
|
{
|
|
label: 'Report &Issue',
|
|
click: async () => await this.execute('report-issue', null)
|
|
}
|
|
];
|
|
|
|
if (!darwin) {
|
|
helpSubmenu.push({ type: 'separator' });
|
|
helpSubmenu.push({
|
|
label: `&About ${electron.app.name}`,
|
|
click: async () => await this.execute('about', null)
|
|
});
|
|
}
|
|
|
|
menuTemplate.push({
|
|
role: 'help',
|
|
submenu: helpSubmenu
|
|
});
|
|
|
|
const commandTable = new Map();
|
|
commandTable.set('file.export', {
|
|
enabled: (view) => view && view.path ? true : false
|
|
});
|
|
commandTable.set('edit.cut', {
|
|
enabled: (view) => view && view.path ? true : false
|
|
});
|
|
commandTable.set('edit.copy', {
|
|
enabled: (view) => view && (view.path || view.get('copy.enabled')) ? true : false
|
|
});
|
|
commandTable.set('edit.paste', {
|
|
enabled: (view) => view && view.path ? true : false
|
|
});
|
|
commandTable.set('edit.select-all', {
|
|
enabled: (view) => view && view.path ? true : false
|
|
});
|
|
commandTable.set('edit.find', {
|
|
enabled: (view) => view && view.path ? true : false
|
|
});
|
|
commandTable.set('view.toggle-attributes', {
|
|
enabled: (view) => view && view.path ? true : false,
|
|
label: (view) => !view || view.get('attributes') ? 'Hide &Attributes' : 'Show &Attributes'
|
|
});
|
|
commandTable.set('view.toggle-weights', {
|
|
enabled: (view) => view && view.path ? true : false,
|
|
label: (view) => !view || view.get('weights') ? 'Hide &Weights' : 'Show &Weights'
|
|
});
|
|
commandTable.set('view.toggle-names', {
|
|
enabled: (view) => view && view.path ? true : false,
|
|
label: (view) => !view || view.get('names') ? 'Hide &Names' : 'Show &Names'
|
|
});
|
|
commandTable.set('view.toggle-direction', {
|
|
enabled: (view) => view && view.path ? true : false,
|
|
label: (view) => !view || view.get('direction') === 'vertical' ? 'Show &Horizontal' : 'Show &Vertical'
|
|
});
|
|
commandTable.set('view.toggle-mousewheel', {
|
|
enabled: (view) => view && view.path ? true : false,
|
|
label: (view) => !view || view.get('mousewheel') === 'scroll' ? '&Mouse Wheel: Zoom' : '&Mouse Wheel: Scroll'
|
|
});
|
|
commandTable.set('view.reload', {
|
|
enabled: (view) => view && view.path ? true : false
|
|
});
|
|
commandTable.set('view.zoom-reset', {
|
|
enabled: (view) => view && view.path && view.get('zoom-reset.enabled') ? true : false
|
|
});
|
|
commandTable.set('view.zoom-in', {
|
|
enabled: (view) => view && view.path && view.get('zoom-in.enabled') ? true : false
|
|
});
|
|
commandTable.set('view.zoom-out', {
|
|
enabled: (view) => view && view.path && view.get('zoom-out.enabled') ? true : false
|
|
});
|
|
commandTable.set('view.show-properties', {
|
|
enabled: (view) => view && view.path ? true : false
|
|
});
|
|
|
|
this._menu.build(menuTemplate, commandTable);
|
|
this._menu.update();
|
|
}
|
|
}
|
|
|
|
static location(path) {
|
|
if (process.platform !== 'win32') {
|
|
const homeDir = os.homedir();
|
|
if (path.startsWith(homeDir)) {
|
|
return { path, label: `~${path.substring(homeDir.length)}` };
|
|
}
|
|
}
|
|
return { path, label: path };
|
|
}
|
|
};
|
|
|
|
app.View = class {
|
|
|
|
constructor(owner) {
|
|
this._owner = owner;
|
|
this._ready = false;
|
|
this._path = null;
|
|
this._properties = new Map();
|
|
this._dispatch = [];
|
|
const dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
const size = electron.screen.getPrimaryDisplay().workAreaSize;
|
|
const options = {
|
|
show: false,
|
|
title: electron.app.name,
|
|
backgroundColor: electron.nativeTheme.shouldUseDarkColors ? '#1e1e1e' : '#ececec',
|
|
icon: electron.nativeImage.createFromPath(path.join(dirname, 'icon.png')),
|
|
minWidth: 600,
|
|
minHeight: 600,
|
|
width: size.width > 1024 ? 1024 : size.width,
|
|
height: size.height > 768 ? 768 : size.height,
|
|
webPreferences: {
|
|
preload: path.join(dirname, 'desktop.mjs'),
|
|
contextIsolation: true,
|
|
nodeIntegration: true,
|
|
enableDeprecatedPaste: true
|
|
}
|
|
};
|
|
if (owner.application.environment.titlebar) {
|
|
options.frame = false;
|
|
options.thickFrame = true;
|
|
options.titleBarStyle = 'hiddenInset';
|
|
}
|
|
if (!this._owner.empty && app.View._position && app.View._position.length === 2) {
|
|
options.x = app.View._position[0] + 30;
|
|
options.y = app.View._position[1] + 30;
|
|
if (options.x + options.width > size.width) {
|
|
options.x = 0;
|
|
}
|
|
if (options.y + options.height > size.height) {
|
|
options.y = 0;
|
|
}
|
|
}
|
|
this._window = new electron.BrowserWindow(options);
|
|
app.View._position = this._window.getPosition();
|
|
this._window.on('close', () => this._owner.closeView(this));
|
|
this._window.on('focus', () => this.emit('activated'));
|
|
this._window.on('blur', () => this.emit('deactivated'));
|
|
this._window.on('minimize', () => this.state());
|
|
this._window.on('restore', () => this.state());
|
|
this._window.on('maximize', () => this.state());
|
|
this._window.on('unmaximize', () => this.state());
|
|
this._window.on('enter-full-screen', () => this.state('enter-full-screen'));
|
|
this._window.on('leave-full-screen', () => this.state('leave-full-screen'));
|
|
this._window.webContents.once('did-finish-load', () => {
|
|
this._didFinishLoad = true;
|
|
});
|
|
this._window.webContents.setWindowOpenHandler((detail) => {
|
|
const url = detail.url;
|
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
electron.shell.openExternal(url);
|
|
}
|
|
return { action: 'deny' };
|
|
});
|
|
this._window.once('ready-to-show', () => {
|
|
this._window.show();
|
|
});
|
|
if (owner.application.environment.titlebar && process.platform !== 'darwin') {
|
|
this._window.removeMenu();
|
|
}
|
|
const pathname = path.join(dirname, 'index.html');
|
|
let content = fs.readFileSync(pathname, 'utf-8');
|
|
content = content.replace(/<\s*script[^>]*>[\s\S]*?(<\s*\/script[^>]*>|$)/ig, '');
|
|
const data = `data:text/html;charset=utf-8,${encodeURIComponent(content)}`;
|
|
this._window.loadURL(data, {
|
|
baseURLForDataURL: url.pathToFileURL(pathname).toString()
|
|
});
|
|
}
|
|
|
|
get window() {
|
|
return this._window;
|
|
}
|
|
|
|
get path() {
|
|
return this._path;
|
|
}
|
|
|
|
async open(path) {
|
|
this._openPath = path;
|
|
const location = app.Application.location(path);
|
|
await new Promise((resolve) => {
|
|
if (this._didFinishLoad) {
|
|
resolve();
|
|
} else {
|
|
this._window.webContents.once('did-finish-load', resolve);
|
|
}
|
|
});
|
|
await new Promise((resolve) => {
|
|
if (this._window.isVisible()) {
|
|
resolve();
|
|
} else {
|
|
this._window.once('ready-to-show', resolve);
|
|
}
|
|
});
|
|
this._window.webContents.send('open', location);
|
|
}
|
|
|
|
restore() {
|
|
if (this._window) {
|
|
if (this._window.isMinimized()) {
|
|
this._window.restore();
|
|
}
|
|
this._window.show();
|
|
}
|
|
}
|
|
|
|
match(path) {
|
|
if (this._openPath) {
|
|
return this._openPath === path;
|
|
}
|
|
return this._path === path;
|
|
}
|
|
|
|
execute(command, data) {
|
|
if (this._dispatch) {
|
|
this._dispatch.push({ command, data });
|
|
} else if (this._window && this._window.webContents) {
|
|
const window = this._window;
|
|
const contents = window.webContents;
|
|
switch (command) {
|
|
case 'toggle-developer-tools':
|
|
if (contents.isDevToolsOpened()) {
|
|
contents.closeDevTools();
|
|
} else {
|
|
contents.openDevTools();
|
|
}
|
|
break;
|
|
case 'fullscreen':
|
|
window.setFullScreen(!window.isFullScreen());
|
|
break;
|
|
default:
|
|
contents.send(command, data);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
update(data) {
|
|
for (const [name, value] of Object.entries(data)) {
|
|
switch (name) {
|
|
case 'path': {
|
|
if (value) {
|
|
this._path = value;
|
|
const location = app.Application.location(this._path);
|
|
const title = process.platform === 'darwin' ? location.label : `${location.label} - ${electron.app.name}`;
|
|
this._window.setTitle(title);
|
|
this._window.focus();
|
|
}
|
|
delete this._openPath;
|
|
break;
|
|
}
|
|
default: {
|
|
this._properties.set(name, value);
|
|
}
|
|
}
|
|
}
|
|
this.emit('updated');
|
|
}
|
|
|
|
get(name) {
|
|
return this._properties.get(name);
|
|
}
|
|
|
|
on(event, callback) {
|
|
this._events = this._events || {};
|
|
this._events[event] = this._events[event] || [];
|
|
this._events[event].push(callback);
|
|
}
|
|
|
|
emit(event, data) {
|
|
if (this._events && this._events[event]) {
|
|
for (const callback of this._events[event]) {
|
|
callback(this, data);
|
|
}
|
|
}
|
|
}
|
|
|
|
state(event) {
|
|
let fullscreen = false;
|
|
switch (event) {
|
|
case 'enter-full-screen': fullscreen = true; break;
|
|
case 'leave-full-screen': fullscreen = false; break;
|
|
default: fullscreen = this._window.isFullScreen(); break;
|
|
}
|
|
this.execute('window-state', {
|
|
minimized: this._window.isMinimized(),
|
|
maximized: this._window.isMaximized(),
|
|
fullscreen
|
|
});
|
|
if (this._dispatch) {
|
|
const dispatch = this._dispatch;
|
|
delete this._dispatch;
|
|
for (const obj of dispatch) {
|
|
this.execute(obj.command, obj.data);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
app.ViewCollection = class {
|
|
|
|
constructor(application) {
|
|
this._application = application;
|
|
this._views = new Map();
|
|
electron.ipcMain.on('window-close', (event) => {
|
|
const window = event.sender.getOwnerBrowserWindow();
|
|
window.close();
|
|
event.returnValue = null;
|
|
});
|
|
electron.ipcMain.on('window-toggle', (event) => {
|
|
const window = event.sender.getOwnerBrowserWindow();
|
|
if (window.isFullScreen()) {
|
|
window.setFullScreen(false);
|
|
} else if (window.isMaximized()) {
|
|
window.unmaximize();
|
|
} else {
|
|
window.maximize();
|
|
}
|
|
event.returnValue = null;
|
|
});
|
|
electron.ipcMain.on('window-minimize', (event) => {
|
|
const window = event.sender.getOwnerBrowserWindow();
|
|
window.minimize();
|
|
event.returnValue = null;
|
|
});
|
|
electron.ipcMain.on('window-update', (event, data) => {
|
|
const window = event.sender.getOwnerBrowserWindow();
|
|
if (this._views.has(window)) {
|
|
const view = this._views.get(window);
|
|
view.update(data);
|
|
}
|
|
event.returnValue = null;
|
|
});
|
|
electron.ipcMain.on('update-window-state', (event) => {
|
|
const window = event.sender.getOwnerBrowserWindow();
|
|
if (this._views.has(window)) {
|
|
this._views.get(window).state();
|
|
}
|
|
event.returnValue = null;
|
|
});
|
|
}
|
|
|
|
get application() {
|
|
return this._application;
|
|
}
|
|
|
|
get views() {
|
|
return this._views.values();
|
|
}
|
|
|
|
get empty() {
|
|
return this._views.size === 0;
|
|
}
|
|
|
|
get(window) {
|
|
return this._views.get(window);
|
|
}
|
|
|
|
openView() {
|
|
const view = new app.View(this);
|
|
view.on('activated', (view) => {
|
|
this._activeView = view;
|
|
this.emit('active-view-changed', { activeView: this._activeView });
|
|
});
|
|
view.on('updated', () => {
|
|
this.emit('active-view-updated', { activeView: this._activeView });
|
|
});
|
|
view.on('deactivated', () => {
|
|
this._activeView = null;
|
|
this.emit('active-view-changed', { activeView: this._activeView });
|
|
});
|
|
this._views.set(view.window, view);
|
|
this._updateActiveView();
|
|
return view;
|
|
}
|
|
|
|
closeView(view) {
|
|
this._views.delete(view.window);
|
|
this._updateActiveView();
|
|
}
|
|
|
|
first() {
|
|
return this.empty ? null : this._views.values().next().value;
|
|
}
|
|
|
|
get activeView() {
|
|
return this._activeView;
|
|
}
|
|
|
|
on(event, callback) {
|
|
this._events = this._events || {};
|
|
this._events[event] = this._events[event] || [];
|
|
this._events[event].push(callback);
|
|
}
|
|
|
|
emit(event, data) {
|
|
if (this._events && this._events[event]) {
|
|
for (const callback of this._events[event]) {
|
|
callback(this, data);
|
|
}
|
|
}
|
|
}
|
|
|
|
_updateActiveView() {
|
|
const window = electron.BrowserWindow.getFocusedWindow();
|
|
const view = window && this._views.has(window) ? this._views.get(window) : null;
|
|
if (view !== this._activeView) {
|
|
this._activeView = view;
|
|
this.emit('active-view-changed', { activeView: this._activeView });
|
|
}
|
|
}
|
|
};
|
|
|
|
app.ConfigurationService = class {
|
|
|
|
constructor() {
|
|
this._content = { 'recents': [] };
|
|
const dir = electron.app.getPath('userData');
|
|
if (dir && dir.length > 0) {
|
|
this._file = path.join(dir, 'configuration.json');
|
|
}
|
|
}
|
|
|
|
open() {
|
|
if (this._file && fs.existsSync(this._file)) {
|
|
const data = fs.readFileSync(this._file, 'utf-8');
|
|
if (data) {
|
|
try {
|
|
const content = JSON.parse(data);
|
|
if (Array.isArray(content.recents)) {
|
|
content.recents = content.recents.map((recent) => typeof recent !== 'string' && recent && recent.path ? recent.path : recent);
|
|
}
|
|
this._content = content;
|
|
} catch {
|
|
// Silently ignore parsing errors and use empty config
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
save() {
|
|
if (this._content && this._file) {
|
|
const data = JSON.stringify(this._content, null, 2);
|
|
fs.writeFileSync(this._file, data);
|
|
}
|
|
}
|
|
|
|
has(name) {
|
|
return this._content && Object.prototype.hasOwnProperty.call(this._content, name);
|
|
}
|
|
|
|
set(name, value) {
|
|
this._content[name] = value;
|
|
}
|
|
|
|
get(name) {
|
|
return this._content[name];
|
|
}
|
|
|
|
delete(name) {
|
|
delete this._content[name];
|
|
}
|
|
};
|
|
|
|
app.MenuService = class {
|
|
|
|
constructor(views) {
|
|
this._views = views;
|
|
}
|
|
|
|
build(menuTemplate, commandTable) {
|
|
this._menuTemplate = menuTemplate;
|
|
this._commandTable = commandTable;
|
|
this._itemTable = new Map();
|
|
for (const menu of menuTemplate) {
|
|
for (const item of menu.submenu) {
|
|
if (item.id) {
|
|
if (!item.label) {
|
|
item.label = '';
|
|
}
|
|
this._itemTable.set(item.id, item);
|
|
}
|
|
}
|
|
}
|
|
this._rebuild();
|
|
}
|
|
|
|
update() {
|
|
if (!this._menu && !this._commandTable) {
|
|
return;
|
|
}
|
|
const view = this._views.activeView;
|
|
if (this._updateLabel(view)) {
|
|
this._rebuild();
|
|
}
|
|
this._updateEnabled(view);
|
|
}
|
|
|
|
_rebuild() {
|
|
if (process.platform === 'darwin') {
|
|
this._menu = electron.Menu.buildFromTemplate(this._menuTemplate);
|
|
electron.Menu.setApplicationMenu(this._menu);
|
|
} else if (!this._views.application.environment.titlebar) {
|
|
this._menu = electron.Menu.buildFromTemplate(this._menuTemplate);
|
|
for (const view of this._views.views) {
|
|
view.window.setMenu(this._menu);
|
|
}
|
|
}
|
|
}
|
|
|
|
_updateLabel(view) {
|
|
let rebuild = false;
|
|
for (const [name, command] of this._commandTable.entries()) {
|
|
if (this._menu) {
|
|
const item = this._menu.getMenuItemById(name);
|
|
if (command && command.label) {
|
|
const label = command.label(view);
|
|
if (label !== item.label) {
|
|
if (this._itemTable.has(name)) {
|
|
this._itemTable.get(name).label = label;
|
|
rebuild = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return rebuild;
|
|
}
|
|
|
|
_updateEnabled(view) {
|
|
for (const [name, command] of this._commandTable.entries()) {
|
|
if (this._menu) {
|
|
const item = this._menu.getMenuItemById(name);
|
|
if (item && command.enabled) {
|
|
item.enabled = command.enabled(view);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
try {
|
|
global.application = new app.Application();
|
|
await global.application.start();
|
|
} catch (error) {
|
|
/* eslint-disable no-console */
|
|
console.error(error.message);
|
|
/* eslint-enable no-console */
|
|
}
|