Refactor IPC selection messages (#1833)

This commit is contained in:
Felix Häusler 2019-12-31 16:00:04 +01:00 committed by Ran Luo
parent 3f2e3340b6
commit 593ca5f83b
5 changed files with 187 additions and 55 deletions

View File

@ -5,7 +5,7 @@ const MENU_ID_FORMAT_MAP = {
strikeMenuItem: 'del', strikeMenuItem: 'del',
hyperlinkMenuItem: 'link', hyperlinkMenuItem: 'link',
imageMenuItem: 'image', imageMenuItem: 'image',
mathMenuItem: 'inline_math' inlineMathMenuItem: 'inline_math'
} }
export const format = (win, type) => { export const format = (win, type) => {
@ -19,12 +19,18 @@ export const format = (win, type) => {
// NOTE: Don't use static `getMenuItemById` here, instead request the menu by // NOTE: Don't use static `getMenuItemById` here, instead request the menu by
// window id from `AppMenu` manager. // window id from `AppMenu` manager.
/**
* Update format menu entires from given state.
*
* @param {Electron.MenuItem} applicationMenu The application menu instance.
* @param {Object.<string, boolean>} formats A object map with selected formats.
*/
export const updateFormatMenu = (applicationMenu, formats) => { export const updateFormatMenu = (applicationMenu, formats) => {
const formatMenuItem = applicationMenu.getMenuItemById('formatMenuItem') const formatMenuItem = applicationMenu.getMenuItemById('formatMenuItem')
formatMenuItem.submenu.items.forEach(item => (item.checked = false)) formatMenuItem.submenu.items.forEach(item => (item.checked = false))
formatMenuItem.submenu.items formatMenuItem.submenu.items
.forEach(item => { .forEach(item => {
if (item.id && formats.some(format => format.type === MENU_ID_FORMAT_MAP[item.id])) { if (item.id && formats[MENU_ID_FORMAT_MAP[item.id]]) {
item.checked = true item.checked = true
} }
}) })

View File

@ -17,13 +17,15 @@ const MENU_ID_MAP = {
heading6MenuItem: 'h6', heading6MenuItem: 'h6',
tableMenuItem: 'figure', tableMenuItem: 'figure',
codeFencesMenuItem: 'pre', codeFencesMenuItem: 'pre',
htmlBlockMenuItem: 'html',
mathBlockMenuItem: 'multiplemath',
quoteBlockMenuItem: 'blockquote', quoteBlockMenuItem: 'blockquote',
orderListMenuItem: 'ol', orderListMenuItem: 'ol',
bulletListMenuItem: 'ul', bulletListMenuItem: 'ul',
taskListMenuItem: 'ul', // taskListMenuItem: 'ul',
paragraphMenuItem: 'p', paragraphMenuItem: 'p',
horizontalLineMenuItem: 'hr', horizontalLineMenuItem: 'hr',
frontMatterMenuItem: 'pre' frontMatterMenuItem: 'frontmatter' // 'pre'
} }
export const paragraph = (win, type) => { export const paragraph = (win, type) => {
@ -50,76 +52,95 @@ const setMultipleStatus = (applicationMenu, list, status) => {
.forEach(item => (item.enabled = status)) .forEach(item => (item.enabled = status))
} }
const setCheckedMenuItem = (applicationMenu, affiliation) => { const setCheckedMenuItem = (applicationMenu, { affiliation, isTable, isLooseListItem, isTaskList }) => {
const paragraphMenuItem = applicationMenu.getMenuItemById('paragraphMenuEntry') const paragraphMenuItem = applicationMenu.getMenuItemById('paragraphMenuEntry')
paragraphMenuItem.submenu.items.forEach(item => (item.checked = false)) paragraphMenuItem.submenu.items.forEach(item => (item.checked = false))
paragraphMenuItem.submenu.items.forEach(item => { paragraphMenuItem.submenu.items.forEach(item => {
if (!item.id) { if (!item.id) {
return false return false
} else if (item.id === 'looseListItemMenuItem') { } else if (item.id === 'looseListItemMenuItem') {
let checked = false item.checked = !!isLooseListItem
if (affiliation.length >= 1 && /ul|ol/.test(affiliation[0].type)) { } else if (Object.keys(affiliation).some(b => {
checked = affiliation[0].children[0].isLooseListItem if (b === 'ul' && isTaskList) {
} else if (affiliation.length >= 3 && affiliation[1].type === 'li') { if (item.id === 'taskListMenuItem') {
checked = affiliation[1].isLooseListItem return true
} }
item.checked = checked return false
} else if (affiliation.some(b => { } else if (isTable && item.id === 'tableMenuItem') {
if (b.type === 'ul') { return true
if (b.listType === 'bullet') { } else if (item.id === 'codeFencesMenuItem' && /code$/.test(b)) {
return item.id === 'bulletListMenuItem' return true
} else {
return item.id === 'taskListMenuItem'
}
} else if (b.type === 'pre' && b.functionType) {
if (b.functionType === 'frontmatter') {
return item.id === 'frontMatterMenuItem'
} else if (/code$/.test(b.functionType)) {
return item.id === 'codeFencesMenuItem'
} else if (b.functionType === 'html') {
return item.id === 'htmlBlockMenuItem'
} else if (b.functionType === 'multiplemath') {
return item.id === 'mathBlockMenuItem'
}
} else if (b.type === 'figure' && b.functionType) {
if (b.functionType === 'table') {
return item.id === 'tableMenuItem'
}
} else {
return b.type === MENU_ID_MAP[item.id]
} }
return b === MENU_ID_MAP[item.id]
})) { })) {
item.checked = true item.checked = true
} }
}) })
} }
export const updateSelectionMenus = (applicationMenu, { start, end, affiliation }) => { /**
// format menu * Update paragraph menu entires from given state.
*
* @param {Electron.MenuItem} applicationMenu The application menu instance.
* @param {*} state The selection information.
*/
export const updateSelectionMenus = (applicationMenu, state) => {
const {
// Key/boolean object like "ul: true" of block elements that are selected.
// This may be an empty object when multiple block elements are selected.
affiliation,
isDisabled,
isMultiline,
isCodeFences,
isCodeContent
} = state
// Reset format menu.
const formatMenuItem = applicationMenu.getMenuItemById('formatMenuItem') const formatMenuItem = applicationMenu.getMenuItemById('formatMenuItem')
formatMenuItem.submenu.items.forEach(item => (item.enabled = true)) formatMenuItem.submenu.items.forEach(item => (item.enabled = true))
// handle menu checked
setCheckedMenuItem(applicationMenu, affiliation)
// handle disable
setParagraphMenuItemStatus(applicationMenu, true)
if ( // Handle menu checked.
(start.block.functionType === 'cellContent' && end.block.functionType === 'cellContent') || setCheckedMenuItem(applicationMenu, state)
(start.type === 'span' && start.block.functionType === 'codeContent') ||
(end.type === 'span' && end.block.functionType === 'codeContent') // Reset paragraph menu.
) { setParagraphMenuItemStatus(applicationMenu, !isDisabled)
if (isDisabled) {
return
}
if (isCodeFences) {
setParagraphMenuItemStatus(applicationMenu, false) setParagraphMenuItemStatus(applicationMenu, false)
if (start.block.functionType === 'codeContent' || end.block.functionType === 'codeContent') { // A code line is selected.
setMultipleStatus(applicationMenu, ['codeFencesMenuItem'], true) if (isCodeContent) {
formatMenuItem.submenu.items.forEach(item => (item.enabled = false)) formatMenuItem.submenu.items.forEach(item => (item.enabled = false))
// TODO: Allow to transform to paragraph for other code blocks too but
// currently not supported by Muya.
// // Allow to transform to paragraph.
// if (affiliation.frontmatter) {
// setMultipleStatus(applicationMenu, ['frontMatterMenuItem'], true)
// } else if (affiliation.html) {
// setMultipleStatus(applicationMenu, ['htmlBlockMenuItem'], true)
// } else if (affiliation.multiplemath) {
// setMultipleStatus(applicationMenu, ['mathBlockMenuItem'], true)
// } else {
// setMultipleStatus(applicationMenu, ['codeFencesMenuItem'], true)
// }
if (Object.keys(affiliation).some(b => /code$/.test(b))) {
setMultipleStatus(applicationMenu, ['codeFencesMenuItem'], true)
}
} }
} else if (start.key !== end.key) { } else if (isMultiline) {
formatMenuItem.submenu.items formatMenuItem.submenu.items
.filter(item => item.id && DISABLE_LABELS.includes(item.id)) .filter(item => item.id && DISABLE_LABELS.includes(item.id))
.forEach(item => (item.enabled = false)) .forEach(item => (item.enabled = false))
setMultipleStatus(applicationMenu, DISABLE_LABELS, false) setMultipleStatus(applicationMenu, DISABLE_LABELS, false)
} else if (!affiliation.slice(0, 3).some(p => /ul|ol/.test(p.type))) { }
// Disable loose list item.
if (!affiliation.ul && !affiliation.ol) {
setMultipleStatus(applicationMenu, ['looseListItemMenuItem'], false) setMultipleStatus(applicationMenu, ['looseListItemMenuItem'], false)
} }
} }

View File

@ -148,6 +148,7 @@ const formatCtrl = ContentState => {
if (!start || !end) { if (!start || !end) {
return { formats: [], tokens: [], neighbors: [] } return { formats: [], tokens: [], neighbors: [] }
} }
const startBlock = this.getBlock(start.key) const startBlock = this.getBlock(start.key)
const formats = [] const formats = []
const neighbors = [] const neighbors = []

View File

@ -113,6 +113,8 @@ const paragraphCtrl = ContentState => {
} }
} }
// TODO: New created nestled list items missing "listType" key and value.
ContentState.prototype.handleListMenu = function (paraType, insertMode) { ContentState.prototype.handleListMenu = function (paraType, insertMode) {
const { start, end, affiliation } = this.selectionChange(this.cursor) const { start, end, affiliation } = this.selectionChange(this.cursor)
const { orderListDelimiter, bulletListMarker, preferLooseListItem } = this.muya.options const { orderListDelimiter, bulletListMarker, preferLooseListItem } = this.muya.options

View File

@ -995,16 +995,13 @@ const actions = {
}) })
} }
// TODO: We should only send a map of booleans to improve performance and not send
// the full change with all block elements with every change.
const { windowId } = global.marktext.env const { windowId } = global.marktext.env
ipcRenderer.send('mt::editor-selection-changed', windowId, changes) ipcRenderer.send('mt::editor-selection-changed', windowId, createApplicationMenuState(changes))
}, },
SELECTION_FORMATS (_, formats) { SELECTION_FORMATS (_, formats) {
const { windowId } = global.marktext.env const { windowId } = global.marktext.env
ipcRenderer.send('mt::update-format-menu', windowId, formats) ipcRenderer.send('mt::update-format-menu', windowId, createSelectionFormatState(formats))
}, },
EXPORT ({ state }, { type, content, pageOptions }) { EXPORT ({ state }, { type, content, pageOptions }) {
@ -1241,4 +1238,109 @@ const trimTrailingNewlines = text => {
return text.replace(/[\r?\n]+$/, '') return text.replace(/[\r?\n]+$/, '')
} }
/**
* Creates a object that contains the application menu state.
*
* @param {*} selection The selection.
* @returns A object that represents the application menu state.
*/
const createApplicationMenuState = ({ start, end, affiliation }) => {
const state = {
isDisabled: false,
// Whether multiple lines are selected.
isMultiline: start.key !== end.key,
// List information - a list must be selected.
isLooseListItem: false,
isTaskList: false,
// Whether the selection is code block like (math, html or code block).
isCodeFences: false,
// Whether a code block line is selected.
isCodeContent: false,
// Whether the selection contains a table.
isTable: false,
// Contains keys about the selection type(s) (string, boolean) like "ul: true".
affiliation: {}
}
const { isMultiline } = state
// Get code block information from selection.
if (
(start.block.functionType === 'cellContent' && end.block.functionType === 'cellContent') ||
(start.type === 'span' && start.block.functionType === 'codeContent') ||
(end.type === 'span' && end.block.functionType === 'codeContent')
) {
// A code block like block is selected (code, math, ...).
state.isCodeFences = true
// A code block line is selected.
if (start.block.functionType === 'codeContent' || end.block.functionType === 'codeContent') {
state.isCodeContent = true
}
}
// Query list information.
if (affiliation.length >= 1 && /ul|ol/.test(affiliation[0].type)) {
const listBlock = affiliation[0]
state.affiliation[listBlock.type] = true
state.isLooseListItem = listBlock.children[0].isLooseListItem
state.isTaskList = listBlock.listType === 'task'
} else if (affiliation.length >= 3 && affiliation[1].type === 'li') {
const listItem = affiliation[1]
const listType = listItem.listItemType === 'order' ? 'ol' : 'ul'
state.affiliation[listType] = true
state.isLooseListItem = listItem.isLooseListItem
state.isTaskList = listItem.listItemType === 'task'
}
// Search with block depth 3 (e.g. "ul -> li -> p" where p is the actually paragraph inside the list (item)).
for (const b of affiliation.slice(0, 3)) {
if (b.type === 'pre' && b.functionType) {
if (/frontmatter|html|multiplemath|code$/.test(b.functionType)) {
state.isCodeFences = true
state.affiliation[b.functionType] = true
}
break
} else if (b.type === 'figure' && b.functionType) {
if (b.functionType === 'table') {
state.isTable = true
state.isDisabled = true
}
break
} else if (isMultiline && /^h{1,6}$/.test(b.type)) {
// Multiple block elements are selected.
state.affiliation = {}
break
} else {
if (!state.affiliation[b.type]) {
state.affiliation[b.type] = true
}
}
}
// Clean up
if (Object.getOwnPropertyNames(state.affiliation).length >= 2 && state.affiliation.p) {
delete state.affiliation.p
}
if ((state.affiliation.ul || state.affiliation.ol) && state.affiliation.li) {
delete state.affiliation.li
}
return state
}
/**
* Creates a object that contains the formats selection state.
*
* @param {*} selection The selection.
* @returns A object that represents the formats menu state.
*/
const createSelectionFormatState = formats => {
// NOTE: Normally only one format can be selected but the selection is
// given as array by Muya.
const state = {}
for (const item of formats) {
state[item.type] = true
}
return state
}
export default { state, mutations, actions } export default { state, mutations, actions }