mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 20:12:23 +08:00
474 lines
13 KiB
JavaScript
474 lines
13 KiB
JavaScript
import { app, BrowserWindow, ipcMain } from 'electron'
|
|
import EventEmitter from 'events'
|
|
import log from 'electron-log'
|
|
import Watcher, { WATCHER_STABILITY_THRESHOLD, WATCHER_STABILITY_POLL_INTERVAL } from '../filesystem/watcher'
|
|
import { WindowType } from '../windows/base'
|
|
import { Web3Storage, getFilesFromPath } from 'web3.storage'
|
|
|
|
class WindowActivityList {
|
|
constructor () {
|
|
// Oldest Newest
|
|
// <number>, ... , <number>
|
|
this._buf = []
|
|
}
|
|
|
|
getNewest () {
|
|
const { _buf } = this
|
|
if (_buf.length) {
|
|
return _buf[_buf.length - 1]
|
|
}
|
|
return null
|
|
}
|
|
|
|
getSecondNewest () {
|
|
const { _buf } = this
|
|
if (_buf.length >= 2) {
|
|
return _buf[_buf.length - 2]
|
|
}
|
|
return null
|
|
}
|
|
|
|
setNewest (id) {
|
|
// I think we do not need a linked list for only a few windows.
|
|
const { _buf } = this
|
|
const index = _buf.indexOf(id)
|
|
if (index !== -1) {
|
|
const lastIndex = _buf.length - 1
|
|
if (index === lastIndex) {
|
|
return
|
|
}
|
|
_buf.splice(index, 1)
|
|
}
|
|
_buf.push(id)
|
|
}
|
|
|
|
delete (id) {
|
|
const { _buf } = this
|
|
const index = _buf.indexOf(id)
|
|
if (index !== -1) {
|
|
_buf.splice(index, 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
class WindowManager extends EventEmitter {
|
|
/**
|
|
*
|
|
* @param {AppMenu} appMenu The application menu instance.
|
|
* @param {Preference} preferences The preference instance.
|
|
*/
|
|
constructor (appMenu, preferences) {
|
|
super()
|
|
|
|
this._appMenu = appMenu
|
|
|
|
this._activeWindowId = null
|
|
this._windows = new Map()
|
|
this._windowActivity = new WindowActivityList()
|
|
|
|
// TODO(need::refactor): Please see #1035.
|
|
this._watcher = new Watcher(preferences)
|
|
|
|
this._listenForIpcMain()
|
|
}
|
|
|
|
/**
|
|
* Add the given window to the window list.
|
|
*
|
|
* @param {IApplicationWindow} window The application window. We take ownership!
|
|
*/
|
|
add (window) {
|
|
const { id: windowId } = window
|
|
this._windows.set(windowId, window)
|
|
|
|
if (!this._appMenu.has(windowId)) {
|
|
this._appMenu.addDefaultMenu(windowId)
|
|
}
|
|
|
|
if (this.windowCount === 1) {
|
|
this.setActiveWindow(windowId)
|
|
}
|
|
|
|
window.on('window-focus', () => {
|
|
this.setActiveWindow(windowId)
|
|
})
|
|
window.on('window-closed', () => {
|
|
this.remove(windowId)
|
|
this._watcher.unwatchByWindowId(windowId)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Return the application window by id.
|
|
*
|
|
* @param {string} windowId The window id.
|
|
* @returns {BaseWindow} The application window or undefined.
|
|
*/
|
|
get (windowId) {
|
|
return this._windows.get(windowId)
|
|
}
|
|
|
|
/**
|
|
* Return the BrowserWindow by id.
|
|
*
|
|
* @param {string} windowId The window id.
|
|
* @returns {Electron.BrowserWindow} The window or undefined.
|
|
*/
|
|
getBrowserWindow (windowId) {
|
|
const window = this.get(windowId)
|
|
if (window) {
|
|
return window.browserWindow
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Remove the given window by id.
|
|
*
|
|
* NOTE: All window "window-focus" events listeners are removed!
|
|
*
|
|
* @param {string} windowId The window id.
|
|
* @returns {IApplicationWindow} Returns the application window. We no longer take ownership.
|
|
*/
|
|
remove (windowId) {
|
|
const { _windows } = this
|
|
const window = this.get(windowId)
|
|
if (window) {
|
|
window.removeAllListeners('window-focus')
|
|
|
|
this._windowActivity.delete(windowId)
|
|
const nextWindowId = this._windowActivity.getNewest()
|
|
this.setActiveWindow(nextWindowId)
|
|
|
|
_windows.delete(windowId)
|
|
}
|
|
return window
|
|
}
|
|
|
|
setActiveWindow (windowId) {
|
|
if (this._activeWindowId !== windowId) {
|
|
this._activeWindowId = windowId
|
|
this._windowActivity.setNewest(windowId)
|
|
if (windowId != null) {
|
|
// windowId is null when all windows are closed (e.g. when gracefully closed).
|
|
this._appMenu.setActiveWindow(windowId)
|
|
}
|
|
this.emit('activeWindowChanged', windowId)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the active window or null if no window is registered.
|
|
* @returns {BaseWindow|undefined}
|
|
*/
|
|
getActiveWindow () {
|
|
return this._windows.get(this._activeWindowId)
|
|
}
|
|
|
|
/**
|
|
* Returns the active window id or null if no window is registered.
|
|
* @returns {number|null}
|
|
*/
|
|
getActiveWindowId () {
|
|
return this._activeWindowId
|
|
}
|
|
|
|
/**
|
|
* Returns the (last) active editor window or null if no editor is registered.
|
|
* @returns {EditorWindow|undefined}
|
|
*/
|
|
getActiveEditor () {
|
|
let win = this.getActiveWindow()
|
|
if (win && win.type !== WindowType.EDITOR) {
|
|
win = this._windows.get(this._windowActivity.getSecondNewest())
|
|
if (win && win.type === WindowType.EDITOR) {
|
|
return win
|
|
}
|
|
return undefined
|
|
}
|
|
return win
|
|
}
|
|
|
|
/**
|
|
* Returns the (last) active editor window id or null if no editor is registered.
|
|
* @returns {number|null}
|
|
*/
|
|
getActiveEditorId () {
|
|
const win = this.getActiveEditor()
|
|
return win ? win.id : null
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {WindowType} type the WindowType one of ['base', 'editor', 'settings']
|
|
* @returns {{id: number, win: BaseWindow}[]} Return the windows of the given {type}
|
|
*/
|
|
getWindowsByType (type) {
|
|
if (!WindowType[type.toUpperCase()]) {
|
|
console.error(`"${type}" is not a valid window type.`)
|
|
}
|
|
const { windows } = this
|
|
const result = []
|
|
for (const [key, value] of windows) {
|
|
if (value.type === type) {
|
|
result.push({
|
|
id: key,
|
|
win: value
|
|
})
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Find the best window to open the files in.
|
|
*
|
|
* @param {string[]} fileList File full paths.
|
|
* @returns {{windowId: string, fileList: string[]}[]} An array of files mapped to a window id or null to open in a new window.
|
|
*/
|
|
findBestWindowToOpenIn (fileList) {
|
|
if (!fileList || !Array.isArray(fileList) || !fileList.length) return []
|
|
const { windows } = this
|
|
const lastActiveEditorId = this.getActiveEditorId() // editor id or null
|
|
|
|
if (this.windowCount <= 1) {
|
|
return [{ windowId: lastActiveEditorId, fileList }]
|
|
}
|
|
|
|
// Array of scores, same order like fileList.
|
|
let filePathScores = null
|
|
for (const window of windows.values()) {
|
|
if (window.type === WindowType.EDITOR) {
|
|
const scores = window.getCandidateScores(fileList)
|
|
if (!filePathScores) {
|
|
filePathScores = scores
|
|
} else {
|
|
const len = filePathScores.length
|
|
for (let i = 0; i < len; ++i) {
|
|
// Update score only if the file is not already opened.
|
|
if (filePathScores[i].score !== -1 && filePathScores[i].score < scores[i].score) {
|
|
filePathScores[i] = scores[i]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const buf = []
|
|
const len = filePathScores.length
|
|
for (let i = 0; i < len; ++i) {
|
|
let { id: windowId, score } = filePathScores[i]
|
|
|
|
if (score === -1) {
|
|
// Skip files that already opened.
|
|
continue
|
|
} else if (score === 0) {
|
|
// There is no best window to open the file(s) in.
|
|
windowId = lastActiveEditorId
|
|
}
|
|
|
|
let item = buf.find(w => w.windowId === windowId)
|
|
if (!item) {
|
|
item = { windowId, fileList: [] }
|
|
buf.push(item)
|
|
}
|
|
item.fileList.push(fileList[i])
|
|
}
|
|
return buf
|
|
}
|
|
|
|
get windows () {
|
|
return this._windows
|
|
}
|
|
|
|
get windowCount () {
|
|
return this._windows.size
|
|
}
|
|
|
|
// --- helper ---------------------------------
|
|
|
|
closeWatcher () {
|
|
this._watcher.close()
|
|
}
|
|
|
|
/**
|
|
* Closes the browser window and associated application window without asking to save documents.
|
|
*
|
|
* @param {Electron.BrowserWindow} browserWindow The browser window.
|
|
*/
|
|
forceClose (browserWindow) {
|
|
if (!browserWindow) {
|
|
return false
|
|
}
|
|
|
|
const { id: windowId } = browserWindow
|
|
const { _appMenu, _windows } = this
|
|
|
|
// Free watchers used by this window
|
|
this._watcher.unwatchByWindowId(windowId)
|
|
|
|
// Application clearup and remove listeners
|
|
_appMenu.removeWindowMenu(windowId)
|
|
const window = this.remove(windowId)
|
|
|
|
// Destroy window wrapper and browser window
|
|
if (window) {
|
|
window.destroy()
|
|
} else {
|
|
log.error('Something went wrong: Cannot find associated application window!')
|
|
browserWindow.destroy()
|
|
}
|
|
|
|
// Quit application on macOS if not windows are opened.
|
|
if (_windows.size === 0) {
|
|
app.quit()
|
|
}
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Closes the application window and associated browser window without asking to save documents.
|
|
*
|
|
* @param {number} windowId The application window or browser window id.
|
|
*/
|
|
forceCloseById (windowId) {
|
|
const browserWindow = this.getBrowserWindow(windowId)
|
|
if (browserWindow) {
|
|
return this.forceClose(browserWindow)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// --- private --------------------------------
|
|
|
|
_listenForIpcMain () {
|
|
// HACK: Don't use this event! Please see #1034 and #1035
|
|
ipcMain.on('mt::window-add-file-path', (e, filePath) => {
|
|
const win = BrowserWindow.fromWebContents(e.sender)
|
|
const editor = this.get(win.id)
|
|
if (!editor) {
|
|
log.error(`Cannot find window id "${win.id}" to add opened file.`)
|
|
return
|
|
}
|
|
editor.addToOpenedFiles(filePath)
|
|
})
|
|
|
|
// Force close a BrowserWindow
|
|
ipcMain.on('mt::close-window', e => {
|
|
const win = BrowserWindow.fromWebContents(e.sender)
|
|
this.forceClose(win)
|
|
})
|
|
|
|
ipcMain.on('mt::open-file', (e, filePath, options) => {
|
|
const win = BrowserWindow.fromWebContents(e.sender)
|
|
const editor = this.get(win.id)
|
|
if (!editor) {
|
|
log.error(`Cannot find window id "${win.id}" to open file.`)
|
|
return
|
|
}
|
|
editor.openTab(filePath, options, true)
|
|
})
|
|
|
|
ipcMain.on('mt::window-tab-closed', (e, pathname) => {
|
|
const win = BrowserWindow.fromWebContents(e.sender)
|
|
const editor = this.get(win.id)
|
|
if (editor) {
|
|
editor.removeFromOpenedFiles(pathname)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('mt::window-toggle-always-on-top', e => {
|
|
const win = BrowserWindow.fromWebContents(e.sender)
|
|
const flag = !win.isAlwaysOnTop()
|
|
win.setAlwaysOnTop(flag)
|
|
this._appMenu.updateAlwaysOnTopMenu(win.id, flag)
|
|
})
|
|
|
|
// --- local events ---------------
|
|
|
|
ipcMain.on('watcher-unwatch-all-by-id', windowId => {
|
|
this._watcher.unwatchByWindowId(windowId)
|
|
})
|
|
ipcMain.on('watcher-watch-file', (win, filePath) => {
|
|
this._watcher.watch(win, filePath, 'file')
|
|
})
|
|
ipcMain.on('watcher-watch-directory', (win, pathname) => {
|
|
this._watcher.watch(win, pathname, 'dir')
|
|
})
|
|
ipcMain.on('watcher-unwatch-file', (win, filePath) => {
|
|
this._watcher.unwatch(win, filePath, 'file')
|
|
})
|
|
ipcMain.on('watcher-unwatch-directory', (win, pathname) => {
|
|
this._watcher.unwatch(win, pathname, 'dir')
|
|
})
|
|
|
|
ipcMain.on('window-add-file-path', (windowId, filePath) => {
|
|
const editor = this.get(windowId)
|
|
if (!editor) {
|
|
log.error(`Cannot find window id "${windowId}" to add opened file.`)
|
|
return
|
|
}
|
|
editor.addToOpenedFiles(filePath)
|
|
})
|
|
ipcMain.on('window-change-file-path', (windowId, pathname, oldPathname) => {
|
|
const editor = this.get(windowId)
|
|
if (!editor) {
|
|
log.error(`Cannot find window id "${windowId}" to change file path.`)
|
|
return
|
|
}
|
|
editor.changeOpenedFilePath(pathname, oldPathname)
|
|
})
|
|
|
|
ipcMain.on('window-file-saved', (windowId, pathname) => {
|
|
// A changed event is emitted earliest after the stability threshold.
|
|
const duration = WATCHER_STABILITY_THRESHOLD + (WATCHER_STABILITY_POLL_INTERVAL * 2)
|
|
this._watcher.ignoreChangedEvent(windowId, pathname, duration)
|
|
})
|
|
|
|
ipcMain.on('add-file-to-ipfs', async (pathname) => {
|
|
// A changed event is emitted earliest after the stability threshold.
|
|
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjoweDU4ZDc1ZjYzN2Y5NDc2YzVkQmU1OGIxNzEyN0Q1MGU0NDgxMzUzQjQiLCJpc3MiOiJ3ZWIzLXN0b3JhZ2UiLCJpYXQiOjE2NjE0MDU2Mzc2MDQsIm5hbWUiOiJ4aW5taW5zdSJ9.sb1ATMTwOtsquSn6kTWQylCRUZjVDWrGUq5o6sLHlis'
|
|
const storage = new Web3Storage({ token })
|
|
|
|
const files = await getFilesFromPath(pathname)
|
|
const cid = await storage.put(files)
|
|
console.log('Content added with CID:', cid)
|
|
})
|
|
|
|
ipcMain.on('window-close-by-id', id => {
|
|
this.forceCloseById(id)
|
|
})
|
|
ipcMain.on('window-reload-by-id', id => {
|
|
const window = this.get(id)
|
|
if (window) {
|
|
window.reload()
|
|
}
|
|
})
|
|
ipcMain.on('window-toggle-always-on-top', win => {
|
|
const flag = !win.isAlwaysOnTop()
|
|
win.setAlwaysOnTop(flag)
|
|
this._appMenu.updateAlwaysOnTopMenu(win.id, flag)
|
|
})
|
|
|
|
ipcMain.on('broadcast-preferences-changed', prefs => {
|
|
// We can not dynamic change the title bar style, so do not need to send it to renderer.
|
|
if (typeof prefs.titleBarStyle !== 'undefined') {
|
|
delete prefs.titleBarStyle
|
|
}
|
|
if (Object.keys(prefs).length > 0) {
|
|
for (const { browserWindow } of this._windows.values()) {
|
|
browserWindow.webContents.send('mt::user-preference', prefs)
|
|
}
|
|
}
|
|
})
|
|
|
|
ipcMain.on('broadcast-user-data-changed', userData => {
|
|
for (const { browserWindow } of this._windows.values()) {
|
|
browserWindow.webContents.send('mt::user-preference', userData)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
export default WindowManager
|