/ src / main-process / atom-window.js
atom-window.js
  1  const { BrowserWindow, app, dialog, ipcMain } = require('electron');
  2  const getAppName = require('../get-app-name');
  3  const path = require('path');
  4  const url = require('url');
  5  const { EventEmitter } = require('events');
  6  const StartupTime = require('../startup-time');
  7  
  8  const ICON_PATH = path.resolve(__dirname, '..', '..', 'resources', 'atom.png');
  9  
 10  let includeShellLoadTime = true;
 11  let nextId = 0;
 12  
 13  module.exports = class AtomWindow extends EventEmitter {
 14    constructor(atomApplication, fileRecoveryService, settings = {}) {
 15      StartupTime.addMarker('main-process:atom-window:start');
 16  
 17      super();
 18  
 19      this.id = nextId++;
 20      this.atomApplication = atomApplication;
 21      this.fileRecoveryService = fileRecoveryService;
 22      this.isSpec = settings.isSpec;
 23      this.headless = settings.headless;
 24      this.safeMode = settings.safeMode;
 25      this.devMode = settings.devMode;
 26      this.resourcePath = settings.resourcePath;
 27  
 28      const locationsToOpen = settings.locationsToOpen || [];
 29  
 30      this.loadedPromise = new Promise(resolve => {
 31        this.resolveLoadedPromise = resolve;
 32      });
 33      this.closedPromise = new Promise(resolve => {
 34        this.resolveClosedPromise = resolve;
 35      });
 36  
 37      const options = {
 38        show: false,
 39        title: getAppName(),
 40        tabbingIdentifier: 'atom',
 41        webPreferences: {
 42          // Prevent specs from throttling when the window is in the background:
 43          // this should result in faster CI builds, and an improvement in the
 44          // local development experience when running specs through the UI (which
 45          // now won't pause when e.g. minimizing the window).
 46          backgroundThrottling: !this.isSpec,
 47          // Disable the `auxclick` feature so that `click` events are triggered in
 48          // response to a middle-click.
 49          // (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960)
 50          disableBlinkFeatures: 'Auxclick'
 51        }
 52      };
 53  
 54      // Don't set icon on Windows so the exe's ico will be used as window and
 55      // taskbar's icon. See https://github.com/atom/atom/issues/4811 for more.
 56      if (process.platform === 'linux') options.icon = ICON_PATH;
 57      if (this.shouldAddCustomTitleBar()) options.titleBarStyle = 'hidden';
 58      if (this.shouldAddCustomInsetTitleBar())
 59        options.titleBarStyle = 'hiddenInset';
 60      if (this.shouldHideTitleBar()) options.frame = false;
 61  
 62      const BrowserWindowConstructor =
 63        settings.browserWindowConstructor || BrowserWindow;
 64      this.browserWindow = new BrowserWindowConstructor(options);
 65  
 66      Object.defineProperty(this.browserWindow, 'loadSettingsJSON', {
 67        get: () =>
 68          JSON.stringify(
 69            Object.assign(
 70              {
 71                userSettings: !this.isSpec
 72                  ? this.atomApplication.configFile.get()
 73                  : null
 74              },
 75              this.loadSettings
 76            )
 77          )
 78      });
 79  
 80      this.handleEvents();
 81  
 82      this.loadSettings = Object.assign({}, settings);
 83      this.loadSettings.appVersion = app.getVersion();
 84      this.loadSettings.appName = getAppName();
 85      this.loadSettings.resourcePath = this.resourcePath;
 86      this.loadSettings.atomHome = process.env.ATOM_HOME;
 87      if (this.loadSettings.devMode == null) this.loadSettings.devMode = false;
 88      if (this.loadSettings.safeMode == null) this.loadSettings.safeMode = false;
 89      if (this.loadSettings.clearWindowState == null)
 90        this.loadSettings.clearWindowState = false;
 91  
 92      this.addLocationsToOpen(locationsToOpen);
 93  
 94      this.loadSettings.hasOpenFiles = locationsToOpen.some(
 95        location => location.pathToOpen && !location.isDirectory
 96      );
 97      this.loadSettings.initialProjectRoots = this.projectRoots;
 98  
 99      StartupTime.addMarker('main-process:atom-window:end');
100  
101      // Expose the startup markers to the renderer process, so we can have unified
102      // measures about startup time between the main process and the renderer process.
103      Object.defineProperty(this.browserWindow, 'startupMarkers', {
104        get: () => {
105          // We only want to make the main process startup data available once,
106          // so if the window is refreshed or a new window is opened, the
107          // renderer process won't use it again.
108          const timingData = StartupTime.exportData();
109          StartupTime.deleteData();
110  
111          return timingData;
112        }
113      });
114  
115      // Only send to the first non-spec window created
116      if (includeShellLoadTime && !this.isSpec) {
117        includeShellLoadTime = false;
118        if (!this.loadSettings.shellLoadTime) {
119          this.loadSettings.shellLoadTime = Date.now() - global.shellStartTime;
120        }
121      }
122  
123      if (!this.loadSettings.env) this.env = this.loadSettings.env;
124  
125      this.browserWindow.on('window:loaded', () => {
126        this.disableZoom();
127        this.emit('window:loaded');
128        this.resolveLoadedPromise();
129      });
130  
131      this.browserWindow.on('window:locations-opened', () => {
132        this.emit('window:locations-opened');
133      });
134  
135      this.browserWindow.on('enter-full-screen', () => {
136        this.browserWindow.webContents.send('did-enter-full-screen');
137      });
138  
139      this.browserWindow.on('leave-full-screen', () => {
140        this.browserWindow.webContents.send('did-leave-full-screen');
141      });
142  
143      this.browserWindow.loadURL(
144        url.format({
145          protocol: 'file',
146          pathname: `${this.resourcePath}/static/index.html`,
147          slashes: true
148        })
149      );
150  
151      this.browserWindow.showSaveDialog = this.showSaveDialog.bind(this);
152  
153      if (this.isSpec) this.browserWindow.focusOnWebView();
154  
155      const hasPathToOpen = !(
156        locationsToOpen.length === 1 && locationsToOpen[0].pathToOpen == null
157      );
158      if (hasPathToOpen && !this.isSpecWindow())
159        this.openLocations(locationsToOpen);
160    }
161  
162    hasProjectPaths() {
163      return this.projectRoots.length > 0;
164    }
165  
166    setupContextMenu() {
167      const ContextMenu = require('./context-menu');
168  
169      this.browserWindow.on('context-menu', menuTemplate => {
170        return new ContextMenu(menuTemplate, this);
171      });
172    }
173  
174    containsLocations(locations) {
175      return locations.every(location => this.containsLocation(location));
176    }
177  
178    containsLocation(location) {
179      if (!location.pathToOpen) return false;
180  
181      return this.projectRoots.some(projectPath => {
182        if (location.pathToOpen === projectPath) return true;
183        if (location.pathToOpen.startsWith(path.join(projectPath, path.sep))) {
184          if (!location.exists) return true;
185          if (!location.isDirectory) return true;
186        }
187        return false;
188      });
189    }
190  
191    handleEvents() {
192      this.browserWindow.on('close', async event => {
193        if (
194          (!this.atomApplication.quitting ||
195            this.atomApplication.quittingForUpdate) &&
196          !this.unloading
197        ) {
198          event.preventDefault();
199          this.unloading = true;
200          this.atomApplication.saveCurrentWindowOptions(false);
201          if (await this.prepareToUnload()) this.close();
202        }
203      });
204  
205      this.browserWindow.on('closed', () => {
206        this.fileRecoveryService.didCloseWindow(this);
207        this.atomApplication.removeWindow(this);
208        this.resolveClosedPromise();
209      });
210  
211      this.browserWindow.on('unresponsive', () => {
212        if (this.isSpec) return;
213        dialog.showMessageBox(
214          this.browserWindow,
215          {
216            type: 'warning',
217            buttons: ['Force Close', 'Keep Waiting'],
218            cancelId: 1, // Canceling should be the least destructive action
219            message: 'Editor is not responding',
220            detail:
221              'The editor is not responding. Would you like to force close it or just keep waiting?'
222          },
223          response => {
224            if (response === 0) this.browserWindow.destroy();
225          }
226        );
227      });
228  
229      this.browserWindow.webContents.on('crashed', async () => {
230        if (this.headless) {
231          console.log('Renderer process crashed, exiting');
232          this.atomApplication.exit(100);
233          return;
234        }
235  
236        await this.fileRecoveryService.didCrashWindow(this);
237        dialog.showMessageBox(
238          this.browserWindow,
239          {
240            type: 'warning',
241            buttons: ['Close Window', 'Reload', 'Keep It Open'],
242            cancelId: 2, // Canceling should be the least destructive action
243            message: 'The editor has crashed',
244            detail: 'Please report this issue to https://github.com/atom/atom'
245          },
246          response => {
247            switch (response) {
248              case 0:
249                return this.browserWindow.destroy();
250              case 1:
251                return this.browserWindow.reload();
252            }
253          }
254        );
255      });
256  
257      this.browserWindow.webContents.on('will-navigate', (event, url) => {
258        if (url !== this.browserWindow.webContents.getURL())
259          event.preventDefault();
260      });
261  
262      this.setupContextMenu();
263  
264      // Spec window's web view should always have focus
265      if (this.isSpec)
266        this.browserWindow.on('blur', () => this.browserWindow.focusOnWebView());
267    }
268  
269    async prepareToUnload() {
270      if (this.isSpecWindow()) return true;
271  
272      this.lastPrepareToUnloadPromise = new Promise(resolve => {
273        const callback = (event, result) => {
274          if (
275            BrowserWindow.fromWebContents(event.sender) === this.browserWindow
276          ) {
277            ipcMain.removeListener('did-prepare-to-unload', callback);
278            if (!result) {
279              this.unloading = false;
280              this.atomApplication.quitting = false;
281            }
282            resolve(result);
283          }
284        };
285        ipcMain.on('did-prepare-to-unload', callback);
286        this.browserWindow.webContents.send('prepare-to-unload');
287      });
288  
289      return this.lastPrepareToUnloadPromise;
290    }
291  
292    openPath(pathToOpen, initialLine, initialColumn) {
293      return this.openLocations([{ pathToOpen, initialLine, initialColumn }]);
294    }
295  
296    async openLocations(locationsToOpen) {
297      this.addLocationsToOpen(locationsToOpen);
298      await this.loadedPromise;
299      this.sendMessage('open-locations', locationsToOpen);
300    }
301  
302    didChangeUserSettings(settings) {
303      this.sendMessage('did-change-user-settings', settings);
304    }
305  
306    didFailToReadUserSettings(message) {
307      this.sendMessage('did-fail-to-read-user-settings', message);
308    }
309  
310    addLocationsToOpen(locationsToOpen) {
311      const roots = new Set(this.projectRoots || []);
312      for (const { pathToOpen, isDirectory } of locationsToOpen) {
313        if (isDirectory) {
314          roots.add(pathToOpen);
315        }
316      }
317  
318      this.projectRoots = Array.from(roots);
319      this.projectRoots.sort();
320    }
321  
322    replaceEnvironment(env) {
323      this.browserWindow.webContents.send('environment', env);
324    }
325  
326    sendMessage(message, detail) {
327      this.browserWindow.webContents.send('message', message, detail);
328    }
329  
330    sendCommand(command, ...args) {
331      if (this.isSpecWindow()) {
332        if (!this.atomApplication.sendCommandToFirstResponder(command)) {
333          switch (command) {
334            case 'window:reload':
335              return this.reload();
336            case 'window:toggle-dev-tools':
337              return this.toggleDevTools();
338            case 'window:close':
339              return this.close();
340          }
341        }
342      } else if (this.isWebViewFocused()) {
343        this.sendCommandToBrowserWindow(command, ...args);
344      } else if (!this.atomApplication.sendCommandToFirstResponder(command)) {
345        this.sendCommandToBrowserWindow(command, ...args);
346      }
347    }
348  
349    sendURIMessage(uri) {
350      this.browserWindow.webContents.send('uri-message', uri);
351    }
352  
353    sendCommandToBrowserWindow(command, ...args) {
354      const action =
355        args[0] && args[0].contextCommand ? 'context-command' : 'command';
356      this.browserWindow.webContents.send(action, command, ...args);
357    }
358  
359    getDimensions() {
360      const [x, y] = Array.from(this.browserWindow.getPosition());
361      const [width, height] = Array.from(this.browserWindow.getSize());
362      return { x, y, width, height };
363    }
364  
365    shouldAddCustomTitleBar() {
366      return (
367        !this.isSpec &&
368        process.platform === 'darwin' &&
369        this.atomApplication.config.get('core.titleBar') === 'custom'
370      );
371    }
372  
373    shouldAddCustomInsetTitleBar() {
374      return (
375        !this.isSpec &&
376        process.platform === 'darwin' &&
377        this.atomApplication.config.get('core.titleBar') === 'custom-inset'
378      );
379    }
380  
381    shouldHideTitleBar() {
382      return (
383        !this.isSpec &&
384        process.platform === 'darwin' &&
385        this.atomApplication.config.get('core.titleBar') === 'hidden'
386      );
387    }
388  
389    close() {
390      return this.browserWindow.close();
391    }
392  
393    focus() {
394      return this.browserWindow.focus();
395    }
396  
397    minimize() {
398      return this.browserWindow.minimize();
399    }
400  
401    maximize() {
402      return this.browserWindow.maximize();
403    }
404  
405    unmaximize() {
406      return this.browserWindow.unmaximize();
407    }
408  
409    restore() {
410      return this.browserWindow.restore();
411    }
412  
413    setFullScreen(fullScreen) {
414      return this.browserWindow.setFullScreen(fullScreen);
415    }
416  
417    setAutoHideMenuBar(autoHideMenuBar) {
418      return this.browserWindow.setAutoHideMenuBar(autoHideMenuBar);
419    }
420  
421    handlesAtomCommands() {
422      return !this.isSpecWindow() && this.isWebViewFocused();
423    }
424  
425    isFocused() {
426      return this.browserWindow.isFocused();
427    }
428  
429    isMaximized() {
430      return this.browserWindow.isMaximized();
431    }
432  
433    isMinimized() {
434      return this.browserWindow.isMinimized();
435    }
436  
437    isWebViewFocused() {
438      return this.browserWindow.isWebViewFocused();
439    }
440  
441    isSpecWindow() {
442      return this.isSpec;
443    }
444  
445    reload() {
446      this.loadedPromise = new Promise(resolve => {
447        this.resolveLoadedPromise = resolve;
448      });
449      this.prepareToUnload().then(canUnload => {
450        if (canUnload) this.browserWindow.reload();
451      });
452      return this.loadedPromise;
453    }
454  
455    showSaveDialog(options, callback) {
456      options = Object.assign(
457        {
458          title: 'Save File',
459          defaultPath: this.projectRoots[0]
460        },
461        options
462      );
463  
464      if (typeof callback === 'function') {
465        // Async
466        dialog.showSaveDialog(this.browserWindow, options, callback);
467      } else {
468        // Sync
469        return dialog.showSaveDialog(this.browserWindow, options);
470      }
471    }
472  
473    toggleDevTools() {
474      return this.browserWindow.toggleDevTools();
475    }
476  
477    openDevTools() {
478      return this.browserWindow.openDevTools();
479    }
480  
481    closeDevTools() {
482      return this.browserWindow.closeDevTools();
483    }
484  
485    setDocumentEdited(documentEdited) {
486      return this.browserWindow.setDocumentEdited(documentEdited);
487    }
488  
489    setRepresentedFilename(representedFilename) {
490      return this.browserWindow.setRepresentedFilename(representedFilename);
491    }
492  
493    setProjectRoots(projectRootPaths) {
494      this.projectRoots = projectRootPaths;
495      this.projectRoots.sort();
496      this.loadSettings.initialProjectRoots = this.projectRoots;
497      return this.atomApplication.saveCurrentWindowOptions();
498    }
499  
500    didClosePathWithWaitSession(path) {
501      this.atomApplication.windowDidClosePathWithWaitSession(this, path);
502    }
503  
504    copy() {
505      return this.browserWindow.copy();
506    }
507  
508    disableZoom() {
509      return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1);
510    }
511  
512    getLoadedPromise() {
513      return this.loadedPromise;
514    }
515  };