mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 15:52:17 +08:00
367 lines
11 KiB
JavaScript
367 lines
11 KiB
JavaScript
import fs from 'fs'
|
|
import path from 'path'
|
|
import { app, ipcMain, Menu } from 'electron'
|
|
import log from 'electron-log'
|
|
import { ensureDirSync, isDirectory, isFile } from 'common/filesystem'
|
|
import { isLinux } from '../config'
|
|
import { parseMenu } from '../keyboard/shortcutHandler'
|
|
import configureMenu, { configSettingMenu } from '../menu/templates'
|
|
|
|
export const MenuType = {
|
|
DEFAULT: 0,
|
|
EDITOR: 1,
|
|
SETTINGS: 2
|
|
}
|
|
|
|
class AppMenu {
|
|
/**
|
|
* @param {Preference} preferences The preferences instances.
|
|
* @param {Keybindings} keybindings The keybindings instances.
|
|
* @param {string} userDataPath The user data path.
|
|
*/
|
|
constructor (preferences, keybindings, userDataPath) {
|
|
const FILE_NAME = 'recently-used-documents.json'
|
|
this.MAX_RECENTLY_USED_DOCUMENTS = 12
|
|
|
|
this._preferences = preferences
|
|
this._keybindings = keybindings
|
|
this._userDataPath = userDataPath
|
|
|
|
this.RECENTS_PATH = path.join(userDataPath, FILE_NAME)
|
|
this.isOsxOrWindows = /darwin|win32/.test(process.platform)
|
|
this.isOsx = process.platform === 'darwin'
|
|
this.activeWindowId = -1
|
|
this.windowMenus = new Map()
|
|
|
|
this._listenForIpcMain()
|
|
}
|
|
|
|
addRecentlyUsedDocument (filePath) {
|
|
const { isOsxOrWindows, isOsx, MAX_RECENTLY_USED_DOCUMENTS, RECENTS_PATH } = this
|
|
|
|
if (isOsxOrWindows) app.addRecentDocument(filePath)
|
|
if (isOsx) return
|
|
|
|
const recentDocuments = this.getRecentlyUsedDocuments()
|
|
const index = recentDocuments.indexOf(filePath)
|
|
let needSave = index !== 0
|
|
if (index > 0) {
|
|
recentDocuments.splice(index, 1)
|
|
}
|
|
if (index !== 0) {
|
|
recentDocuments.unshift(filePath)
|
|
}
|
|
|
|
if (recentDocuments.length > MAX_RECENTLY_USED_DOCUMENTS) {
|
|
needSave = true
|
|
recentDocuments.splice(MAX_RECENTLY_USED_DOCUMENTS, recentDocuments.length - MAX_RECENTLY_USED_DOCUMENTS)
|
|
}
|
|
|
|
this.updateAppMenu(recentDocuments)
|
|
|
|
if (needSave) {
|
|
ensureDirSync(this._userDataPath)
|
|
const json = JSON.stringify(recentDocuments, null, 2)
|
|
fs.writeFileSync(RECENTS_PATH, json, 'utf-8')
|
|
}
|
|
}
|
|
|
|
getRecentlyUsedDocuments () {
|
|
const { RECENTS_PATH, MAX_RECENTLY_USED_DOCUMENTS } = this
|
|
if (!isFile(RECENTS_PATH)) {
|
|
return []
|
|
}
|
|
|
|
try {
|
|
const recentDocuments = JSON.parse(fs.readFileSync(RECENTS_PATH, 'utf-8'))
|
|
.filter(f => f && (isFile(f) || isDirectory(f)))
|
|
|
|
if (recentDocuments.length > MAX_RECENTLY_USED_DOCUMENTS) {
|
|
recentDocuments.splice(MAX_RECENTLY_USED_DOCUMENTS, recentDocuments.length - MAX_RECENTLY_USED_DOCUMENTS)
|
|
}
|
|
return recentDocuments
|
|
} catch (err) {
|
|
log.error(err)
|
|
return []
|
|
}
|
|
}
|
|
|
|
clearRecentlyUsedDocuments () {
|
|
const { isOsxOrWindows, isOsx, RECENTS_PATH } = this
|
|
if (isOsxOrWindows) app.clearRecentDocuments()
|
|
if (isOsx) return
|
|
|
|
const recentDocuments = []
|
|
this.updateAppMenu(recentDocuments)
|
|
const json = JSON.stringify(recentDocuments, null, 2)
|
|
ensureDirSync(this._userDataPath)
|
|
fs.writeFileSync(RECENTS_PATH, json, 'utf-8')
|
|
}
|
|
|
|
addDefaultMenu (windowId) {
|
|
const { windowMenus } = this
|
|
const menu = this.buildSettingMenu() // Setting menu is also the fallback menu.
|
|
windowMenus.set(windowId, menu)
|
|
}
|
|
|
|
addSettingMenu (window) {
|
|
const { windowMenus } = this
|
|
const menu = this.buildSettingMenu()
|
|
windowMenus.set(window.id, menu)
|
|
}
|
|
|
|
addEditorMenu (window, options = {}) {
|
|
const { windowMenus } = this
|
|
windowMenus.set(window.id, this.buildEditorMenu(true))
|
|
|
|
const { menu, shortcutMap } = windowMenus.get(window.id)
|
|
const currentMenu = Menu.getApplicationMenu() // the menu may be null
|
|
updateMenuItemSafe(currentMenu, menu, 'sourceCodeModeMenuItem', !!options.sourceCodeModeEnabled)
|
|
updateMenuItemSafe(currentMenu, menu, 'typewriterModeMenuItem', false)
|
|
|
|
// FIXME: Focus mode is being ignored when you open a new window - inconsistency.
|
|
// updateMenuItemSafe(currentMenu, menu, 'focusModeMenuItem', false)
|
|
|
|
const { checked: isSourceMode } = menu.getMenuItemById('sourceCodeModeMenuItem')
|
|
if (isSourceMode) {
|
|
// BUG: When opening a file `typewriterMode` and `focusMode` will be reset by editor.
|
|
// If source code mode is set the editor must not change the values.
|
|
const typewriterModeMenuItem = menu.getMenuItemById('typewriterModeMenuItem')
|
|
const focusModeMenuItem = menu.getMenuItemById('focusModeMenuItem')
|
|
typewriterModeMenuItem.enabled = false
|
|
focusModeMenuItem.enabled = false
|
|
}
|
|
this._keybindings.registerKeyHandlers(window, shortcutMap)
|
|
}
|
|
|
|
removeWindowMenu (windowId) {
|
|
// NOTE: Shortcut handler is automatically unregistered when window is closed.
|
|
const { activeWindowId } = this
|
|
this.windowMenus.delete(windowId)
|
|
if (activeWindowId === windowId) {
|
|
this.activeWindowId = -1
|
|
}
|
|
}
|
|
|
|
getWindowMenuById (windowId) {
|
|
const menu = this.windowMenus.get(windowId)
|
|
if (!menu) {
|
|
log.error(`getWindowMenuById: Cannot find window menu for id ${windowId}.`)
|
|
throw new Error(`Cannot find window menu for id ${windowId}.`)
|
|
}
|
|
return menu.menu
|
|
}
|
|
|
|
has (windowId) {
|
|
return this.windowMenus.has(windowId)
|
|
}
|
|
|
|
setActiveWindow (windowId) {
|
|
if (this.activeWindowId !== windowId) {
|
|
// Change application menu to the current window menu.
|
|
this._setApplicationMenu(this.getWindowMenuById(windowId))
|
|
this.activeWindowId = windowId
|
|
}
|
|
}
|
|
|
|
buildEditorMenu (createShortcutMap, recentUsedDocuments) {
|
|
if (!recentUsedDocuments) {
|
|
recentUsedDocuments = this.getRecentlyUsedDocuments()
|
|
}
|
|
|
|
const menuTemplate = configureMenu(this._keybindings, this._preferences, recentUsedDocuments)
|
|
const menu = Menu.buildFromTemplate(menuTemplate)
|
|
|
|
let shortcutMap = null
|
|
if (createShortcutMap) {
|
|
shortcutMap = parseMenu(menuTemplate)
|
|
}
|
|
|
|
return {
|
|
shortcutMap,
|
|
menu,
|
|
type: MenuType.EDITOR
|
|
}
|
|
}
|
|
|
|
buildSettingMenu () {
|
|
if (this.isOsx) {
|
|
const menuTemplate = configSettingMenu(this._keybindings)
|
|
const menu = Menu.buildFromTemplate(menuTemplate)
|
|
return { menu, type: MenuType.SETTINGS }
|
|
}
|
|
return { menu: null, type: MenuType.SETTINGS }
|
|
}
|
|
|
|
updateAppMenu (recentUsedDocuments) {
|
|
if (!recentUsedDocuments) {
|
|
recentUsedDocuments = this.getRecentlyUsedDocuments()
|
|
}
|
|
|
|
// "we don't support changing menu object after calling setMenu, the behavior
|
|
// is undefined if user does that." That mean we have to recreate the editor
|
|
// application menu each time.
|
|
|
|
// rebuild all window menus
|
|
this.windowMenus.forEach((value, key) => {
|
|
const { menu: oldMenu, type } = value
|
|
if (type !== MenuType.EDITOR) return
|
|
|
|
const { menu: newMenu } = this.buildEditorMenu(false, recentUsedDocuments)
|
|
|
|
// all other menu items are set automatically
|
|
updateMenuItem(oldMenu, newMenu, 'sourceCodeModeMenuItem')
|
|
updateMenuItem(oldMenu, newMenu, 'typewriterModeMenuItem')
|
|
updateMenuItem(oldMenu, newMenu, 'focusModeMenuItem')
|
|
updateMenuItem(oldMenu, newMenu, 'sideBarMenuItem')
|
|
updateMenuItem(oldMenu, newMenu, 'tabBarMenuItem')
|
|
|
|
// update window menu
|
|
value.menu = newMenu
|
|
|
|
// update application menu if necessary
|
|
const { activeWindowId } = this
|
|
if (activeWindowId === key) {
|
|
this._setApplicationMenu(newMenu)
|
|
}
|
|
})
|
|
}
|
|
|
|
updateLineEndingMenu (lineEnding) {
|
|
updateLineEndingMenu(lineEnding)
|
|
}
|
|
|
|
updateAlwaysOnTopMenu (flag) {
|
|
const menus = Menu.getApplicationMenu()
|
|
const menu = menus.getMenuItemById('alwaysOnTopMenuItem')
|
|
menu.checked = flag
|
|
}
|
|
|
|
updateThemeMenu = theme => {
|
|
this.windowMenus.forEach((value, key) => {
|
|
const { menu, type } = value
|
|
if (type !== MenuType.EDITOR) return
|
|
|
|
const themeMenus = menu.getMenuItemById('themeMenu')
|
|
if (!themeMenus) {
|
|
return
|
|
}
|
|
themeMenus.submenu.items.forEach(item => (item.checked = false))
|
|
themeMenus.submenu.items
|
|
.forEach(item => {
|
|
if (item.id && item.id === theme) {
|
|
item.checked = true
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
updateAutoSaveMenu = autoSave => {
|
|
this.windowMenus.forEach((value, key) => {
|
|
const { menu, type } = value
|
|
if (type !== MenuType.EDITOR) return
|
|
|
|
const autoSaveMenu = menu.getMenuItemById('autoSaveMenuItem')
|
|
if (!autoSaveMenu) {
|
|
return
|
|
}
|
|
autoSaveMenu.checked = autoSave
|
|
})
|
|
}
|
|
|
|
updateAidouMenu = bool => {
|
|
this.windowMenus.forEach((value, key) => {
|
|
const { menu, type } = value
|
|
if (type !== MenuType.EDITOR) return
|
|
|
|
const aidouMenu = menu.getMenuItemById('aidou')
|
|
if (!aidouMenu) {
|
|
return
|
|
}
|
|
aidouMenu.visible = bool
|
|
})
|
|
}
|
|
|
|
_setApplicationMenu (menu) {
|
|
if (isLinux && !menu) {
|
|
// WORKAROUND for Electron#16521: We cannot hide the (application) menu on Linux.
|
|
const dummyMenu = Menu.buildFromTemplate([])
|
|
Menu.setApplicationMenu(dummyMenu)
|
|
} else {
|
|
Menu.setApplicationMenu(menu)
|
|
}
|
|
}
|
|
|
|
_listenForIpcMain () {
|
|
ipcMain.on('mt::add-recently-used-document', (e, pathname) => {
|
|
this.addRecentlyUsedDocument(pathname)
|
|
})
|
|
|
|
ipcMain.on('menu-add-recently-used', pathname => {
|
|
this.addRecentlyUsedDocument(pathname)
|
|
})
|
|
ipcMain.on('menu-clear-recently-used', () => {
|
|
this.clearRecentlyUsedDocuments()
|
|
})
|
|
|
|
ipcMain.on('broadcast-preferences-changed', prefs => {
|
|
if (prefs.theme !== undefined) {
|
|
this.updateThemeMenu(prefs.theme)
|
|
}
|
|
if (prefs.autoSave !== undefined) {
|
|
this.updateAutoSaveMenu(prefs.autoSave)
|
|
}
|
|
if (prefs.aidou !== undefined) {
|
|
this.updateAidouMenu(prefs.aidou)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const updateMenuItem = (oldMenus, newMenus, id) => {
|
|
const oldItem = oldMenus.getMenuItemById(id)
|
|
const newItem = newMenus.getMenuItemById(id)
|
|
newItem.checked = oldItem.checked
|
|
}
|
|
|
|
const updateMenuItemSafe = (oldMenus, newMenus, id, defaultValue) => {
|
|
let checked = defaultValue
|
|
if (oldMenus) {
|
|
const oldItem = oldMenus.getMenuItemById(id)
|
|
if (oldItem) {
|
|
checked = oldItem.checked
|
|
}
|
|
}
|
|
const newItem = newMenus.getMenuItemById(id)
|
|
newItem.checked = checked
|
|
}
|
|
|
|
// ----------------------------------------------
|
|
|
|
// HACKY: We have one application menu per window and switch the menu when
|
|
// switching windows, so we can access and change the menu items via Electron.
|
|
|
|
/**
|
|
* Return the menu from the application menu.
|
|
*
|
|
* @param {string} menuId Menu ID
|
|
* @returns {Electron.Menu} Returns the menu or null.
|
|
*/
|
|
export const getMenuItemById = menuId => {
|
|
const menus = Menu.getApplicationMenu()
|
|
return menus.getMenuItemById(menuId)
|
|
}
|
|
|
|
export const updateLineEndingMenu = lineEnding => {
|
|
const menus = Menu.getApplicationMenu()
|
|
const crlfMenu = menus.getMenuItemById('crlfLineEndingMenuEntry')
|
|
const lfMenu = menus.getMenuItemById('lfLineEndingMenuEntry')
|
|
if (lineEnding === 'crlf') {
|
|
crlfMenu.checked = true
|
|
} else {
|
|
lfMenu.checked = true
|
|
}
|
|
}
|
|
|
|
export default AppMenu
|