mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 02:10:29 +08:00
259 lines
7.5 KiB
JavaScript
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()
|