2020-02-27 20:38:08 +02:00
|
|
|
const { BrowserWindow, Tray, screen } = require('electron');
|
2018-03-09 22:59:12 +02:00
|
|
|
const { shim } = require('lib/shim');
|
2019-07-30 09:35:42 +02:00
|
|
|
const url = require('url');
|
|
|
|
const path = require('path');
|
|
|
|
const { dirname } = require('lib/path-utils');
|
2018-03-09 22:59:12 +02:00
|
|
|
const fs = require('fs-extra');
|
2017-11-04 14:38:53 +02:00
|
|
|
|
|
|
|
class ElectronAppWrapper {
|
2018-03-09 22:59:12 +02:00
|
|
|
|
2018-04-23 21:50:29 +02:00
|
|
|
constructor(electronApp, env, profilePath) {
|
2017-11-04 14:38:53 +02:00
|
|
|
this.electronApp_ = electronApp;
|
2017-11-13 20:47:35 +02:00
|
|
|
this.env_ = env;
|
2018-04-23 21:50:29 +02:00
|
|
|
this.profilePath_ = profilePath;
|
2017-11-04 14:38:53 +02:00
|
|
|
this.win_ = null;
|
2017-11-17 20:05:25 +02:00
|
|
|
this.willQuitApp_ = false;
|
2018-01-31 22:10:32 +02:00
|
|
|
this.tray_ = null;
|
2018-02-06 15:11:59 +02:00
|
|
|
this.buildDir_ = null;
|
2017-11-04 15:23:15 +02:00
|
|
|
}
|
2017-11-04 14:38:53 +02:00
|
|
|
|
2017-11-04 15:23:15 +02:00
|
|
|
electronApp() {
|
|
|
|
return this.electronApp_;
|
|
|
|
}
|
|
|
|
|
|
|
|
setLogger(v) {
|
|
|
|
this.logger_ = v;
|
|
|
|
}
|
|
|
|
|
|
|
|
logger() {
|
|
|
|
return this.logger_;
|
2017-11-04 14:38:53 +02:00
|
|
|
}
|
|
|
|
|
2017-11-05 02:17:48 +02:00
|
|
|
window() {
|
|
|
|
return this.win_;
|
2017-11-04 21:46:37 +02:00
|
|
|
}
|
|
|
|
|
2019-12-18 13:49:44 +02:00
|
|
|
env() {
|
|
|
|
return this.env_;
|
|
|
|
}
|
|
|
|
|
2017-11-04 14:38:53 +02:00
|
|
|
createWindow() {
|
2019-12-17 14:09:57 +02:00
|
|
|
// Set to true to view errors if the application does not start
|
2020-02-08 19:15:22 +02:00
|
|
|
const debugEarlyBugs = this.env_ === 'dev';
|
2019-12-17 14:09:57 +02:00
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
const windowStateKeeper = require('electron-window-state');
|
2017-11-14 20:02:58 +02:00
|
|
|
|
2020-02-27 20:38:08 +02:00
|
|
|
|
2018-04-23 21:50:29 +02:00
|
|
|
const stateOptions = {
|
2020-02-27 20:38:08 +02:00
|
|
|
defaultWidth: Math.round(0.8*screen.getPrimaryDisplay().workArea.width),
|
|
|
|
defaultHeight: Math.round(0.8*screen.getPrimaryDisplay().workArea.height),
|
2019-09-19 23:51:18 +02:00
|
|
|
file: `window-state-${this.env_}.json`,
|
2019-07-30 09:35:42 +02:00
|
|
|
};
|
2018-04-23 21:50:29 +02:00
|
|
|
|
|
|
|
if (this.profilePath_) stateOptions.path = this.profilePath_;
|
|
|
|
|
|
|
|
// Load the previous state with fallback to defaults
|
|
|
|
const windowState = windowStateKeeper(stateOptions);
|
2017-11-14 20:02:58 +02:00
|
|
|
|
2017-12-15 09:31:57 +02:00
|
|
|
const windowOptions = {
|
2018-02-27 22:04:38 +02:00
|
|
|
x: windowState.x,
|
|
|
|
y: windowState.y,
|
|
|
|
width: windowState.width,
|
|
|
|
height: windowState.height,
|
2020-02-27 20:38:08 +02:00
|
|
|
minWidth: 100,
|
|
|
|
minHeight: 100,
|
2019-07-30 09:35:42 +02:00
|
|
|
backgroundColor: '#fff', // required to enable sub pixel rendering, can't be in css
|
2019-02-24 12:17:37 +02:00
|
|
|
webPreferences: {
|
|
|
|
nodeIntegration: true,
|
|
|
|
},
|
2019-12-17 14:09:57 +02:00
|
|
|
webviewTag: true,
|
2019-11-05 19:03:24 +02:00
|
|
|
// We start with a hidden window, which is then made visible depending on the showTrayIcon setting
|
|
|
|
// https://github.com/laurent22/joplin/issues/2031
|
2019-12-17 14:09:57 +02:00
|
|
|
show: debugEarlyBugs,
|
2017-12-15 09:31:57 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
// Linux icon workaround for bug https://github.com/electron-userland/electron-builder/issues/2098
|
|
|
|
// Fix: https://github.com/electron-userland/electron-builder/issues/2269
|
2019-01-09 19:05:28 +02:00
|
|
|
if (shim.isLinux()) windowOptions.icon = path.join(__dirname, '..', 'build/icons/128x128.png');
|
2017-12-15 09:31:57 +02:00
|
|
|
|
2018-06-26 01:52:46 +02:00
|
|
|
require('electron-context-menu')({
|
|
|
|
shouldShowMenu: (event, params) => {
|
2018-09-04 19:20:41 +02:00
|
|
|
// params.inputFieldType === 'none' when right-clicking the text editor. This is a bit of a hack to detect it because in this
|
|
|
|
// case we don't want to use the built-in context menu but a custom one.
|
|
|
|
return params.isEditable && params.inputFieldType !== 'none';
|
2018-06-26 01:52:46 +02:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2019-07-30 09:35:42 +02:00
|
|
|
this.win_ = new BrowserWindow(windowOptions);
|
2017-11-04 14:38:53 +02:00
|
|
|
|
2020-02-27 20:38:08 +02:00
|
|
|
if (!screen.getDisplayMatching(this.win_.getBounds())) {
|
|
|
|
const { width: windowWidth, height: windowHeight } = this.win_.getBounds();
|
|
|
|
const { width: primaryDisplayWidth, height: primaryDisplayHeight } = screen.getPrimaryDisplay().workArea;
|
|
|
|
this.win_.setPosition(primaryDisplayWidth/2 - windowWidth, primaryDisplayHeight/2 - windowHeight);
|
|
|
|
}
|
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
this.win_.loadURL(url.format({
|
|
|
|
pathname: path.join(__dirname, 'index.html'),
|
|
|
|
protocol: 'file:',
|
2019-07-30 09:35:42 +02:00
|
|
|
slashes: true,
|
|
|
|
}));
|
2017-11-04 14:38:53 +02:00
|
|
|
|
2019-12-17 14:09:57 +02:00
|
|
|
// Note that on Windows, calling openDevTools() too early results in a white window with no error message.
|
|
|
|
// Waiting for one of the ready events might work but they might not be triggered if there's an error, so
|
|
|
|
// the easiest is to use a timeout. Keep in mind that if you get a white window on Windows it might be due
|
|
|
|
// to this line though.
|
|
|
|
if (debugEarlyBugs) setTimeout(() => this.win_.webContents.openDevTools(), 3000);
|
2017-11-04 14:38:53 +02:00
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
this.win_.on('close', (event) => {
|
2018-01-31 22:10:32 +02:00
|
|
|
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
|
|
|
|
// otherwise the window is simply hidden, and will be re-open once the app is "activated" (which happens when the
|
|
|
|
// user clicks on the icon in the task bar).
|
|
|
|
|
|
|
|
// On Windows and Linux, the app is closed when the window is closed *except* if the tray icon is used. In which
|
2019-10-29 11:02:42 +02:00
|
|
|
// case the app must be explicitly closed with Ctrl+Q or by right-clicking on the tray icon and selecting "Exit".
|
2018-01-31 22:10:32 +02:00
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
if (process.platform === 'darwin') {
|
2018-01-31 22:10:32 +02:00
|
|
|
if (this.willQuitApp_) {
|
|
|
|
this.win_ = null;
|
|
|
|
} else {
|
|
|
|
event.preventDefault();
|
2018-02-27 23:54:40 +02:00
|
|
|
this.hide();
|
2018-01-31 22:10:32 +02:00
|
|
|
}
|
2017-11-17 20:05:25 +02:00
|
|
|
} else {
|
2018-01-31 22:10:32 +02:00
|
|
|
if (this.trayShown() && !this.willQuitApp_) {
|
|
|
|
event.preventDefault();
|
|
|
|
this.win_.hide();
|
|
|
|
} else {
|
|
|
|
this.win_ = null;
|
|
|
|
}
|
2017-11-17 20:05:25 +02:00
|
|
|
}
|
2019-07-30 09:35:42 +02:00
|
|
|
});
|
2017-11-14 20:02:58 +02:00
|
|
|
|
|
|
|
// Let us register listeners on the window, so we can update the state
|
|
|
|
// automatically (the listeners will be removed when the window is closed)
|
|
|
|
// and restore the maximized or full screen state
|
|
|
|
windowState.manage(this.win_);
|
2020-02-06 00:15:40 +02:00
|
|
|
|
2020-02-05 13:26:57 +02:00
|
|
|
// HACK: Ensure the window is hidden, as `windowState.manage` may make the window
|
|
|
|
// visible with isMaximized set to true in window-state-${this.env_}.json.
|
|
|
|
// https://github.com/laurent22/joplin/issues/2365
|
|
|
|
if (!windowOptions.show) {
|
|
|
|
this.win_.hide();
|
|
|
|
}
|
2017-11-04 14:38:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async waitForElectronAppReady() {
|
2017-11-04 15:23:15 +02:00
|
|
|
if (this.electronApp().isReady()) return Promise.resolve();
|
2017-11-04 14:38:53 +02:00
|
|
|
|
2019-09-13 00:16:42 +02:00
|
|
|
return new Promise((resolve) => {
|
2017-11-04 14:38:53 +02:00
|
|
|
const iid = setInterval(() => {
|
2017-11-04 15:23:15 +02:00
|
|
|
if (this.electronApp().isReady()) {
|
2017-11-04 14:38:53 +02:00
|
|
|
clearInterval(iid);
|
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
}, 10);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-03-07 21:11:55 +02:00
|
|
|
async quit() {
|
2017-11-04 15:23:15 +02:00
|
|
|
this.electronApp_.quit();
|
|
|
|
}
|
|
|
|
|
2018-03-07 21:11:55 +02:00
|
|
|
exit(errorCode = 0) {
|
|
|
|
this.electronApp_.exit(errorCode);
|
|
|
|
}
|
|
|
|
|
2018-01-31 22:10:32 +02:00
|
|
|
trayShown() {
|
|
|
|
return !!this.tray_;
|
|
|
|
}
|
|
|
|
|
2018-02-27 23:54:40 +02:00
|
|
|
// This method is used in macOS only to hide the whole app (and not just the main window)
|
2019-10-29 11:02:42 +02:00
|
|
|
// including the menu bar. This follows the macOS way of hiding an app.
|
2018-02-27 23:54:40 +02:00
|
|
|
hide() {
|
|
|
|
this.electronApp_.hide();
|
|
|
|
}
|
|
|
|
|
2018-02-06 00:19:21 +02:00
|
|
|
buildDir() {
|
2018-02-06 15:11:59 +02:00
|
|
|
if (this.buildDir_) return this.buildDir_;
|
2019-09-19 23:51:18 +02:00
|
|
|
let dir = `${__dirname}/build`;
|
2018-02-06 15:11:59 +02:00
|
|
|
if (!fs.pathExistsSync(dir)) {
|
2019-09-19 23:51:18 +02:00
|
|
|
dir = `${dirname(__dirname)}/build`;
|
2018-03-09 22:59:12 +02:00
|
|
|
if (!fs.pathExistsSync(dir)) throw new Error('Cannot find build dir');
|
2018-02-06 15:11:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
this.buildDir_ = dir;
|
|
|
|
return dir;
|
2018-02-06 00:19:21 +02:00
|
|
|
}
|
|
|
|
|
2018-02-20 02:41:52 +02:00
|
|
|
trayIconFilename_() {
|
2018-03-09 22:59:12 +02:00
|
|
|
let output = '';
|
2018-02-20 02:41:52 +02:00
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
if (process.platform === 'darwin') {
|
|
|
|
output = 'macos-16x16Template.png'; // Electron Template Image format
|
2018-02-20 02:41:52 +02:00
|
|
|
} else {
|
2018-03-09 22:59:12 +02:00
|
|
|
output = '16x16.png';
|
2018-02-20 02:41:52 +02:00
|
|
|
}
|
|
|
|
|
2019-07-30 09:35:42 +02:00
|
|
|
if (this.env_ === 'dev') output = '16x16-dev.png';
|
2018-02-20 02:41:52 +02:00
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
2018-01-31 22:10:32 +02:00
|
|
|
// Note: this must be called only after the "ready" event of the app has been dispatched
|
|
|
|
createTray(contextMenu) {
|
2018-02-06 15:11:59 +02:00
|
|
|
try {
|
2019-09-19 23:51:18 +02:00
|
|
|
this.tray_ = new Tray(`${this.buildDir()}/icons/${this.trayIconFilename_()}`);
|
2020-01-26 19:32:00 +02:00
|
|
|
this.tray_.setToolTip(this.electronApp_.name);
|
2019-07-30 09:35:42 +02:00
|
|
|
this.tray_.setContextMenu(contextMenu);
|
2018-02-06 15:11:59 +02:00
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
this.tray_.on('click', () => {
|
2018-02-06 15:11:59 +02:00
|
|
|
this.window().show();
|
|
|
|
});
|
|
|
|
} catch (error) {
|
2019-07-30 09:35:42 +02:00
|
|
|
console.error('Cannot create tray', error);
|
2018-02-06 15:11:59 +02:00
|
|
|
}
|
2018-01-31 22:10:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
destroyTray() {
|
|
|
|
if (!this.tray_) return;
|
|
|
|
this.tray_.destroy();
|
|
|
|
this.tray_ = null;
|
|
|
|
}
|
|
|
|
|
2018-02-23 19:51:23 +02:00
|
|
|
ensureSingleInstance() {
|
2018-03-09 22:59:12 +02:00
|
|
|
if (this.env_ === 'dev') return false;
|
2018-03-01 22:14:06 +02:00
|
|
|
|
2019-02-16 15:10:37 +02:00
|
|
|
const gotTheLock = this.electronApp_.requestSingleInstanceLock();
|
2018-02-23 19:51:23 +02:00
|
|
|
|
2019-02-16 15:10:37 +02:00
|
|
|
if (!gotTheLock) {
|
|
|
|
// Another instance is already running - exit
|
|
|
|
this.electronApp_.quit();
|
|
|
|
return true;
|
|
|
|
}
|
2018-02-23 19:51:23 +02:00
|
|
|
|
2019-02-16 15:10:37 +02:00
|
|
|
// Someone tried to open a second instance - focus our window instead
|
2019-09-13 00:16:42 +02:00
|
|
|
this.electronApp_.on('second-instance', () => {
|
2019-02-16 15:10:37 +02:00
|
|
|
const win = this.window();
|
|
|
|
if (!win) return;
|
|
|
|
if (win.isMinimized()) win.restore();
|
|
|
|
win.show();
|
|
|
|
win.focus();
|
2018-02-23 19:51:23 +02:00
|
|
|
});
|
2019-02-16 15:10:37 +02:00
|
|
|
|
|
|
|
return false;
|
2018-02-23 19:51:23 +02:00
|
|
|
}
|
|
|
|
|
2017-11-04 14:38:53 +02:00
|
|
|
async start() {
|
|
|
|
// Since we are doing other async things before creating the window, we might miss
|
|
|
|
// the "ready" event. So we use the function below to make sure that the app is ready.
|
|
|
|
await this.waitForElectronAppReady();
|
|
|
|
|
2019-02-16 15:10:37 +02:00
|
|
|
const alreadyRunning = this.ensureSingleInstance();
|
2019-07-30 09:35:42 +02:00
|
|
|
if (alreadyRunning) return;
|
2018-02-23 19:51:23 +02:00
|
|
|
|
2017-11-04 14:38:53 +02:00
|
|
|
this.createWindow();
|
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
this.electronApp_.on('before-quit', () => {
|
2017-11-17 20:05:25 +02:00
|
|
|
this.willQuitApp_ = true;
|
2019-07-30 09:35:42 +02:00
|
|
|
});
|
2017-11-17 20:05:25 +02:00
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
this.electronApp_.on('window-all-closed', () => {
|
2017-11-17 20:05:25 +02:00
|
|
|
this.electronApp_.quit();
|
2019-07-30 09:35:42 +02:00
|
|
|
});
|
2017-11-04 14:38:53 +02:00
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
this.electronApp_.on('activate', () => {
|
2017-11-17 20:05:25 +02:00
|
|
|
this.win_.show();
|
2019-07-30 09:35:42 +02:00
|
|
|
});
|
2017-11-04 14:38:53 +02:00
|
|
|
}
|
2018-03-09 22:59:12 +02:00
|
|
|
|
2017-11-04 14:38:53 +02:00
|
|
|
}
|
|
|
|
|
2019-01-09 19:05:28 +02:00
|
|
|
module.exports = { ElectronAppWrapper };
|