marktext/src/main/menu/index.js
2019-07-24 10:57:21 +08:00

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