marktext/src/main/window.js
2019-04-06 14:50:40 +08:00

259 lines
7.5 KiB
JavaScript

import { app, BrowserWindow, screen, ipcMain } from 'electron'
import windowStateKeeper from 'electron-window-state'
import { getOsLineEndingName, loadMarkdownFile, getDefaultTextDirection } from './utils/filesystem'
import appMenu from './menu'
import Watcher from './watcher'
import { isMarkdownFile, isDirectory, normalizeAndResolvePath, log } from './utils'
import { TITLE_BAR_HEIGHT, defaultWinOptions, isLinux } from './config'
import userPreference from './preference'
import { newTab } from './actions/file'
class AppWindow {
constructor () {
this.focusedWindowId = -1
this.windows = new Map()
this.watcher = new Watcher()
this.listen()
}
listen () {
// listen for file watch from renderer process eg
// 1. click file in folder.
// 2. new tab and save it.
// 3. close tab(s) need unwatch.
ipcMain.on('AGANI::file-watch', (e, { pathname, watch }) => {
const win = BrowserWindow.fromWebContents(e.sender)
if (watch) {
// listen for file `change` and `unlink`
this.watcher.watch(win, pathname, 'file')
} else {
// unlisten for file `change` and `unlink`
this.watcher.unWatch(win, pathname, 'file')
}
})
}
ensureWindowPosition (mainWindowState) {
// "workArea" doesn't work on Linux
const { bounds, workArea } = screen.getPrimaryDisplay()
const screenArea = isLinux ? bounds : workArea
let { x, y, width, height } = mainWindowState
let center = false
if (x === undefined || y === undefined) {
center = true
// First app start; check whether window size is larger than screen size
if (screenArea.width < width) width = screenArea.width
if (screenArea.height < height) height = screenArea.height
} else {
center = !screen.getAllDisplays().map(display =>
x >= display.bounds.x && x <= display.bounds.x + display.bounds.width &&
y >= display.bounds.y && y <= display.bounds.y + display.bounds.height)
.some(display => display)
}
if (center) {
// win.center() doesn't work on Linux
x = Math.max(0, Math.ceil(screenArea.x + (screenArea.width - width) / 2))
y = Math.max(0, Math.ceil(screenArea.y + (screenArea.height - height) / 2))
}
return {
x,
y,
width,
height
}
}
/**
* Creates a new editor window.
*
* @param {string} [pathname] Path to a file, directory or link.
* @param {string} [markdown] Markdown content.
* @param {*} [options] BrowserWindow options.
*/
createWindow (pathname = null, markdown = '', options = {}) {
// Ensure path is normalized
if (pathname) {
pathname = normalizeAndResolvePath(pathname)
}
const { windows } = this
const mainWindowState = windowStateKeeper({
defaultWidth: 1200,
defaultHeight: 800
})
const { x, y, width, height } = this.ensureWindowPosition(mainWindowState)
const winOpt = Object.assign({ x, y, width, height }, defaultWinOptions, options)
// Enable native or custom window
const { titleBarStyle } = userPreference.getAll()
if (titleBarStyle === 'custom') {
winOpt.titleBarStyle = ''
} else if (titleBarStyle === 'native') {
winOpt.frame = true
winOpt.titleBarStyle = ''
}
const win = new BrowserWindow(winOpt)
windows.set(win.id, { win })
// create a menu for the current window
appMenu.addWindowMenuWithListener(win)
if (windows.size === 1) {
appMenu.setActiveWindow(win.id)
}
win.once('ready-to-show', async () => {
mainWindowState.manage(win)
win.show()
// open single markdown file
if (pathname && isMarkdownFile(pathname)) {
appMenu.addRecentlyUsedDocument(pathname)
try {
this.openFile(win, pathname)
} catch (err) {
log(err)
}
// open directory / folder
} else if (pathname && isDirectory(pathname)) {
appMenu.addRecentlyUsedDocument(pathname)
this.openFolder(win, pathname)
// open a window but do not open a file or directory
} else {
const lineEnding = getOsLineEndingName()
const textDirection = getDefaultTextDirection()
win.webContents.send('AGANI::open-blank-window', {
lineEnding,
markdown
})
appMenu.updateLineEndingnMenu(lineEnding)
appMenu.updateTextDirectionMenu(textDirection)
}
})
win.on('focus', () => {
win.webContents.send('AGANI::window-active-status', { status: true })
if (win.id !== this.focusedWindowId) {
this.focusedWindowId = win.id
win.webContents.send('AGANI::req-update-line-ending-menu')
win.webContents.send('AGANI::request-for-view-layout')
win.webContents.send('AGANI::req-update-text-direction-menu')
// update application menu
appMenu.setActiveWindow(win.id)
}
})
win.on('blur', () => {
win.webContents.send('AGANI::window-active-status', { status: false })
})
win.on('close', event => { // before closed
event.preventDefault()
win.webContents.send('AGANI::ask-for-close')
})
// set renderer arguments
const { codeFontFamily, codeFontSize, theme } = userPreference.getAll()
// wow, this can be accessesed in renderer process.
win.stylePrefs = {
codeFontFamily,
codeFontSize,
theme
}
const winURL = process.env.NODE_ENV === 'development'
? `http://localhost:9080`
: `file://${__dirname}/index.html`
win.loadURL(winURL)
win.setSheetOffset(TITLE_BAR_HEIGHT)
return win
}
openFile = async (win, filePath) => {
const data = await loadMarkdownFile(filePath)
const {
markdown,
filename,
pathname,
isUtf8BomEncoded,
lineEnding,
adjustLineEndingOnSave,
isMixedLineEndings,
textDirection
} = data
appMenu.updateLineEndingnMenu(lineEnding)
appMenu.updateTextDirectionMenu(textDirection)
win.webContents.send('AGANI::open-single-file', {
markdown,
filename,
pathname,
options: {
isUtf8BomEncoded,
lineEnding,
adjustLineEndingOnSave
}
})
// listen for file `change` and `unlink`
this.watcher.watch(win, filePath, 'file')
// Notify user about mixed endings
if (isMixedLineEndings) {
win.webContents.send('AGANI::show-notification', {
title: 'Mixed Line Endings',
type: 'error',
message: `The document has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`,
time: 20000
})
}
}
newTab (win, filePath) {
this.watcher.watch(win, filePath, 'file')
loadMarkdownFile(filePath).then(rawDocument => {
appMenu.addRecentlyUsedDocument(filePath)
newTab(win, rawDocument)
}).catch(err => {
// TODO: Handle error --> create a end-user error handler.
console.error('[ERROR] Cannot open file or directory.')
log(err)
})
}
openFolder (win, pathname) {
this.watcher.watch(win, pathname, 'dir')
try {
win.webContents.send('AGANI::open-project', pathname)
} catch (err) {
log(err)
}
}
forceClose (win) {
if (!win) return
const { windows } = this
if (windows.has(win.id)) {
this.watcher.unWatchWin(win)
windows.delete(win.id)
}
appMenu.removeWindowMenu(win.id)
win.destroy() // if use win.close(), it will cause a endless loop.
if (windows.size === 0) {
app.quit()
}
}
clear () {
this.watcher.clear()
}
}
export default new AppWindow()