mirror of
https://github.com/atom/atom.git
synced 2026-01-23 22:08:08 -05:00
445 lines
13 KiB
JavaScript
445 lines
13 KiB
JavaScript
const {BrowserWindow, app, dialog, ipcMain} = require('electron')
|
|
const path = require('path')
|
|
const fs = require('fs')
|
|
const url = require('url')
|
|
const {EventEmitter} = require('events')
|
|
|
|
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 = {}) {
|
|
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
|
|
|
|
let {pathToOpen, locationsToOpen} = settings
|
|
if (!locationsToOpen && pathToOpen) locationsToOpen = [{pathToOpen}]
|
|
if (!locationsToOpen) locationsToOpen = []
|
|
|
|
this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve })
|
|
this.closedPromise = new Promise(resolve => { this.resolveClosedPromise = resolve })
|
|
|
|
const options = {
|
|
show: false,
|
|
title: 'Atom',
|
|
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'
|
|
}
|
|
}
|
|
|
|
// 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 = ICON_PATH
|
|
if (this.shouldAddCustomTitleBar()) options.titleBarStyle = 'hidden'
|
|
if (this.shouldAddCustomInsetTitleBar()) options.titleBarStyle = 'hidden-inset'
|
|
if (this.shouldHideTitleBar()) options.frame = false
|
|
this.browserWindow = new BrowserWindow(options)
|
|
|
|
Object.defineProperty(this.browserWindow, 'loadSettingsJSON', {
|
|
get: () => JSON.stringify(Object.assign({
|
|
userSettings: this.atomApplication.configFile.get()
|
|
}, this.loadSettings)),
|
|
configurable: true
|
|
})
|
|
|
|
this.handleEvents()
|
|
|
|
this.loadSettings = Object.assign({}, settings)
|
|
this.loadSettings.appVersion = app.getVersion()
|
|
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
|
|
|
|
if (!this.loadSettings.initialPaths) {
|
|
this.loadSettings.initialPaths = []
|
|
for (const {pathToOpen, stat} of locationsToOpen) {
|
|
if (!pathToOpen) continue
|
|
if (stat && stat.isDirectory()) {
|
|
this.loadSettings.initialPaths.push(pathToOpen)
|
|
} else {
|
|
const parentDirectory = path.dirname(pathToOpen)
|
|
if (stat && stat.isFile() || fs.existsSync(parentDirectory)) {
|
|
this.loadSettings.initialPaths.push(parentDirectory)
|
|
} else {
|
|
this.loadSettings.initialPaths.push(pathToOpen)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.loadSettings.initialPaths.sort()
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
this.representedDirectoryPaths = this.loadSettings.initialPaths
|
|
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)
|
|
}
|
|
|
|
hasProjectPath () {
|
|
return this.representedDirectoryPaths.length > 0
|
|
}
|
|
|
|
setupContextMenu () {
|
|
const ContextMenu = require('./context-menu')
|
|
|
|
this.browserWindow.on('context-menu', menuTemplate => {
|
|
return new ContextMenu(menuTemplate, this)
|
|
})
|
|
}
|
|
|
|
containsPaths (paths) {
|
|
return paths.every(p => this.containsPath(p))
|
|
}
|
|
|
|
containsPath (pathToCheck) {
|
|
if (!pathToCheck) return false
|
|
let stat
|
|
return this.representedDirectoryPaths.some(projectPath => {
|
|
if (pathToCheck === projectPath) return true
|
|
if (!pathToCheck.startsWith(path.join(projectPath, path.sep))) return false
|
|
if (stat === undefined) stat = fs.statSyncNoException(pathToCheck)
|
|
return !stat || !stat.isDirectory()
|
|
})
|
|
}
|
|
|
|
handleEvents () {
|
|
this.browserWindow.on('close', async event => {
|
|
if (!this.atomApplication.quitting && !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', () => {
|
|
if (this.isSpec) return
|
|
dialog.showMessageBox(this.browserWindow, {
|
|
type: 'warning',
|
|
buttons: ['Force Close', 'Keep Waiting'],
|
|
message: 'Editor is not responding',
|
|
detail:
|
|
'The editor is not responding. Would you like to force close it or just keep waiting?'
|
|
}, response => { if (response === 0) this.browserWindow.destroy() })
|
|
})
|
|
|
|
this.browserWindow.webContents.on('crashed', async () => {
|
|
if (this.headless) {
|
|
console.log('Renderer process crashed, exiting')
|
|
this.atomApplication.exit(100)
|
|
return
|
|
}
|
|
|
|
await this.fileRecoveryService.didCrashWindow(this)
|
|
dialog.showMessageBox(this.browserWindow, {
|
|
type: 'warning',
|
|
buttons: ['Close Window', 'Reload', 'Keep It Open'],
|
|
message: 'The editor has crashed',
|
|
detail: 'Please report this issue to https://github.com/atom/atom'
|
|
}, response => {
|
|
switch (response) {
|
|
case 0: return this.browserWindow.destroy()
|
|
case 1: return this.browserWindow.reload()
|
|
}
|
|
})
|
|
})
|
|
|
|
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) {
|
|
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)
|
|
}
|
|
|
|
replaceEnvironment (env) {
|
|
this.browserWindow.webContents.send('environment', env)
|
|
}
|
|
|
|
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}
|
|
}
|
|
|
|
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 &&
|
|
process.platform === 'darwin' &&
|
|
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.representedDirectoryPaths[0]
|
|
}, options)
|
|
|
|
if (typeof callback === 'function') {
|
|
// Async
|
|
dialog.showSaveDialog(this.browserWindow, options, callback)
|
|
} else {
|
|
// Sync
|
|
return dialog.showSaveDialog(this.browserWindow, options)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
setRepresentedDirectoryPaths (representedDirectoryPaths) {
|
|
this.representedDirectoryPaths = representedDirectoryPaths
|
|
this.representedDirectoryPaths.sort()
|
|
this.loadSettings.initialPaths = this.representedDirectoryPaths
|
|
return this.atomApplication.saveCurrentWindowOptions()
|
|
}
|
|
|
|
didClosePathWithWaitSession (path) {
|
|
this.atomApplication.windowDidClosePathWithWaitSession(this, path)
|
|
}
|
|
|
|
copy () {
|
|
return this.browserWindow.copy()
|
|
}
|
|
|
|
disableZoom () {
|
|
return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1)
|
|
}
|
|
}
|