src/main-process/atom-window.js
const {
BrowserWindow,
app,
dialog,
ipcMain,
nativeImage
} = require('electron');
const getAppName = require('../get-app-name');
const path = require('path');
const url = require('url');
const { EventEmitter } = require('events');
const StartupTime = require('../startup-time');
const ICON_PATH = path.resolve(__dirname, '..', '..', 'resources', 'atom.png');
let includeShellLoadTime = true;
let nextId = 0;
module.exports = class AtomWindow extends EventEmitter {
constructor(atomApplication, fileRecoveryService, settings = {}) {
StartupTime.addMarker('main-process:atom-window:start');
super();
this.id = nextId++;
this.atomApplication = atomApplication;
this.fileRecoveryService = fileRecoveryService;
this.isSpec = settings.isSpec;
this.headless = settings.headless;
this.safeMode = settings.safeMode;
this.devMode = settings.devMode;
this.resourcePath = settings.resourcePath;
const locationsToOpen = settings.locationsToOpen || [];
this.loadedPromise = new Promise(resolve => {
this.resolveLoadedPromise = resolve;
});
this.closedPromise = new Promise(resolve => {
this.resolveClosedPromise = resolve;
});
const options = {
show: false,
title: getAppName(),
tabbingIdentifier: 'atom',
webPreferences: {
// Prevent specs from throttling when the window is in the background:
// this should result in faster CI builds, and an improvement in the
// local development experience when running specs through the UI (which
// now won't pause when e.g. minimizing the window).
backgroundThrottling: !this.isSpec,
// Disable the `auxclick` feature so that `click` events are triggered in
// response to a middle-click.
// (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960)
disableBlinkFeatures: 'Auxclick',
nodeIntegration: true,
webviewTag: true,
// TodoElectronIssue: remote module is deprecated https://www.electronjs.org/docs/breaking-changes#default-changed-enableremotemodule-defaults-to-false
enableRemoteModule: true,
// node support in threads
nodeIntegrationInWorker: true
},
simpleFullscreen: this.getSimpleFullscreen()
};
// Don't set icon on Windows so the exe's ico will be used as window and
// taskbar's icon. See https://github.com/atom/atom/issues/4811 for more.
if (process.platform === 'linux')
options.icon = nativeImage.createFromPath(ICON_PATH);
if (this.shouldAddCustomTitleBar()) options.titleBarStyle = 'hidden';
if (this.shouldAddCustomInsetTitleBar())
options.titleBarStyle = 'hiddenInset';
if (this.shouldHideTitleBar()) options.frame = false;
const BrowserWindowConstructor =
settings.browserWindowConstructor || BrowserWindow;
this.browserWindow = new BrowserWindowConstructor(options);
Object.defineProperty(this.browserWindow, 'loadSettingsJSON', {
get: () =>
JSON.stringify(
Object.assign(
{
userSettings: !this.isSpec
? this.atomApplication.configFile.get()
: null
},
this.loadSettings
)
)
});
this.handleEvents();
this.loadSettings = Object.assign({}, settings);
this.loadSettings.appVersion = app.getVersion();
this.loadSettings.appName = getAppName();
this.loadSettings.resourcePath = this.resourcePath;
this.loadSettings.atomHome = process.env.ATOM_HOME;
if (this.loadSettings.devMode == null) this.loadSettings.devMode = false;
if (this.loadSettings.safeMode == null) this.loadSettings.safeMode = false;
if (this.loadSettings.clearWindowState == null)
this.loadSettings.clearWindowState = false;
this.addLocationsToOpen(locationsToOpen);
this.loadSettings.hasOpenFiles = locationsToOpen.some(
location => location.pathToOpen && !location.isDirectory
);
this.loadSettings.initialProjectRoots = this.projectRoots;
StartupTime.addMarker('main-process:atom-window:end');
// Expose the startup markers to the renderer process, so we can have unified
// measures about startup time between the main process and the renderer process.
Object.defineProperty(this.browserWindow, 'startupMarkers', {
get: () => {
// We only want to make the main process startup data available once,
// so if the window is refreshed or a new window is opened, the
// renderer process won't use it again.
const timingData = StartupTime.exportData();
StartupTime.deleteData();
return timingData;
}
});
// Only send to the first non-spec window created
if (includeShellLoadTime && !this.isSpec) {
includeShellLoadTime = false;
if (!this.loadSettings.shellLoadTime) {
this.loadSettings.shellLoadTime = Date.now() - global.shellStartTime;
}
}
if (!this.loadSettings.env) this.env = this.loadSettings.env;
this.browserWindow.on('window:loaded', () => {
this.disableZoom();
this.emit('window:loaded');
this.resolveLoadedPromise();
});
this.browserWindow.on('window:locations-opened', () => {
this.emit('window:locations-opened');
});
this.browserWindow.on('enter-full-screen', () => {
this.browserWindow.webContents.send('did-enter-full-screen');
});
this.browserWindow.on('leave-full-screen', () => {
this.browserWindow.webContents.send('did-leave-full-screen');
});
this.browserWindow.loadURL(
url.format({
protocol: 'file',
pathname: `${this.resourcePath}/static/index.html`,
slashes: true
})
);
this.browserWindow.showSaveDialog = this.showSaveDialog.bind(this);
if (this.isSpec) this.browserWindow.focusOnWebView();
const hasPathToOpen = !(
locationsToOpen.length === 1 && locationsToOpen[0].pathToOpen == null
);
if (hasPathToOpen && !this.isSpecWindow())
this.openLocations(locationsToOpen);
}
hasProjectPaths() {
return this.projectRoots.length > 0;
}
setupContextMenu() {
const ContextMenu = require('./context-menu');
this.browserWindow.on('context-menu', menuTemplate => {
return new ContextMenu(menuTemplate, this);
});
}
containsLocations(locations) {
return locations.every(location => this.containsLocation(location));
}
containsLocation(location) {
if (!location.pathToOpen) return false;
return this.projectRoots.some(projectPath => {
if (location.pathToOpen === projectPath) return true;
if (location.pathToOpen.startsWith(path.join(projectPath, path.sep))) {
if (!location.exists) return true;
if (!location.isDirectory) return true;
}
return false;
});
}
handleEvents() {
this.browserWindow.on('close', async event => {
if (
(!this.atomApplication.quitting ||
this.atomApplication.quittingForUpdate) &&
!this.unloading
) {
event.preventDefault();
this.unloading = true;
this.atomApplication.saveCurrentWindowOptions(false);
if (await this.prepareToUnload()) this.close();
}
});
this.browserWindow.on('closed', () => {
this.fileRecoveryService.didCloseWindow(this);
this.atomApplication.removeWindow(this);
this.resolveClosedPromise();
});
this.browserWindow.on('unresponsive', async () => {
if (this.isSpec) return;
const result = await dialog.showMessageBox(this.browserWindow, {
type: 'warning',
buttons: ['Force Close', 'Keep Waiting'],
cancelId: 1, // Canceling should be the least destructive action
message: 'Editor is not responding',
detail:
'The editor is not responding. Would you like to force close it or just keep waiting?'
});
if (result.response === 0) this.browserWindow.destroy();
});
this.browserWindow.webContents.on('render-process-gone', async () => {
if (this.headless) {
console.log('Renderer process crashed, exiting');
this.atomApplication.exit(100);
return;
}
await this.fileRecoveryService.didCrashWindow(this);
const result = await dialog.showMessageBox(this.browserWindow, {
type: 'warning',
buttons: ['Close Window', 'Reload', 'Keep It Open'],
cancelId: 2, // Canceling should be the least destructive action
message: 'The editor has crashed',
detail: 'Please report this issue to https://github.com/atom/atom'
});
switch (result.response) {
case 0:
this.browserWindow.destroy();
break;
case 1:
this.browserWindow.reload();
break;
}
});
this.browserWindow.webContents.on('will-navigate', (event, url) => {
if (url !== this.browserWindow.webContents.getURL())
event.preventDefault();
});
this.setupContextMenu();
// Spec window's web view should always have focus
if (this.isSpec)
this.browserWindow.on('blur', () => this.browserWindow.focusOnWebView());
}
async prepareToUnload() {
if (this.isSpecWindow()) return true;
this.lastPrepareToUnloadPromise = new Promise(resolve => {
const callback = (event, result) => {
if (
BrowserWindow.fromWebContents(event.sender) === this.browserWindow
) {
ipcMain.removeListener('did-prepare-to-unload', callback);
if (!result) {
this.unloading = false;
this.atomApplication.quitting = false;
}
resolve(result);
}
};
ipcMain.on('did-prepare-to-unload', callback);
this.browserWindow.webContents.send('prepare-to-unload');
});
return this.lastPrepareToUnloadPromise;
}
openPath(pathToOpen, initialLine, initialColumn) {
return this.openLocations([{ pathToOpen, initialLine, initialColumn }]);
}
async openLocations(locationsToOpen) {
this.addLocationsToOpen(locationsToOpen);
await this.loadedPromise;
this.sendMessage('open-locations', locationsToOpen);
}
didChangeUserSettings(settings) {
this.sendMessage('did-change-user-settings', settings);
}
didFailToReadUserSettings(message) {
this.sendMessage('did-fail-to-read-user-settings', message);
}
addLocationsToOpen(locationsToOpen) {
const roots = new Set(this.projectRoots || []);
for (const { pathToOpen, isDirectory } of locationsToOpen) {
if (isDirectory) {
roots.add(pathToOpen);
}
}
this.projectRoots = Array.from(roots);
this.projectRoots.sort();
}
replaceEnvironment(env) {
const {
NODE_ENV,
NODE_PATH,
ATOM_HOME,
ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT
} = env;
this.browserWindow.webContents.send('environment', {
NODE_ENV,
NODE_PATH,
ATOM_HOME,
ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT
});
}
sendMessage(message, detail) {
this.browserWindow.webContents.send('message', message, detail);
}
sendCommand(command, ...args) {
if (this.isSpecWindow()) {
if (!this.atomApplication.sendCommandToFirstResponder(command)) {
switch (command) {
case 'window:reload':
return this.reload();
case 'window:toggle-dev-tools':
return this.toggleDevTools();
case 'window:close':
return this.close();
}
}
} else if (this.isWebViewFocused()) {
this.sendCommandToBrowserWindow(command, ...args);
} else if (!this.atomApplication.sendCommandToFirstResponder(command)) {
this.sendCommandToBrowserWindow(command, ...args);
}
}
sendURIMessage(uri) {
this.browserWindow.webContents.send('uri-message', uri);
}
sendCommandToBrowserWindow(command, ...args) {
const action =
args[0] && args[0].contextCommand ? 'context-command' : 'command';
this.browserWindow.webContents.send(action, command, ...args);
}
getDimensions() {
const [x, y] = Array.from(this.browserWindow.getPosition());
const [width, height] = Array.from(this.browserWindow.getSize());
return { x, y, width, height };
}
getSimpleFullscreen() {
return this.atomApplication.config.get('core.simpleFullScreenWindows');
}
shouldAddCustomTitleBar() {
return (
!this.isSpec &&
process.platform === 'darwin' &&
this.atomApplication.config.get('core.titleBar') === 'custom'
);
}
shouldAddCustomInsetTitleBar() {
return (
!this.isSpec &&
process.platform === 'darwin' &&
this.atomApplication.config.get('core.titleBar') === 'custom-inset'
);
}
shouldHideTitleBar() {
return (
!this.isSpec &&
this.atomApplication.config.get('core.titleBar') === 'hidden'
);
}
close() {
return this.browserWindow.close();
}
focus() {
return this.browserWindow.focus();
}
minimize() {
return this.browserWindow.minimize();
}
maximize() {
return this.browserWindow.maximize();
}
unmaximize() {
return this.browserWindow.unmaximize();
}
restore() {
return this.browserWindow.restore();
}
setFullScreen(fullScreen) {
return this.browserWindow.setFullScreen(fullScreen);
}
setAutoHideMenuBar(autoHideMenuBar) {
return this.browserWindow.setAutoHideMenuBar(autoHideMenuBar);
}
handlesAtomCommands() {
return !this.isSpecWindow() && this.isWebViewFocused();
}
isFocused() {
return this.browserWindow.isFocused();
}
isMaximized() {
return this.browserWindow.isMaximized();
}
isMinimized() {
return this.browserWindow.isMinimized();
}
isWebViewFocused() {
return this.browserWindow.isWebViewFocused();
}
isSpecWindow() {
return this.isSpec;
}
reload() {
this.loadedPromise = new Promise(resolve => {
this.resolveLoadedPromise = resolve;
});
this.prepareToUnload().then(canUnload => {
if (canUnload) this.browserWindow.reload();
});
return this.loadedPromise;
}
showSaveDialog(options, callback) {
options = Object.assign(
{
title: 'Save File',
defaultPath: this.projectRoots[0]
},
options
);
let promise = dialog.showSaveDialog(this.browserWindow, options);
if (typeof callback === 'function') {
promise = promise.then(({ filePath, bookmark }) => {
callback(filePath, bookmark);
});
}
return promise;
}
toggleDevTools() {
return this.browserWindow.toggleDevTools();
}
openDevTools() {
return this.browserWindow.openDevTools();
}
closeDevTools() {
return this.browserWindow.closeDevTools();
}
setDocumentEdited(documentEdited) {
return this.browserWindow.setDocumentEdited(documentEdited);
}
setRepresentedFilename(representedFilename) {
return this.browserWindow.setRepresentedFilename(representedFilename);
}
setProjectRoots(projectRootPaths) {
this.projectRoots = projectRootPaths;
this.projectRoots.sort();
this.loadSettings.initialProjectRoots = this.projectRoots;
return this.atomApplication.saveCurrentWindowOptions();
}
didClosePathWithWaitSession(path) {
this.atomApplication.windowDidClosePathWithWaitSession(this, path);
}
copy() {
return this.browserWindow.copy();
}
disableZoom() {
return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1);
}
getLoadedPromise() {
return this.loadedPromise;
}
};