Files
orca/src/main/window/editable-context-menu.ts
T
mirror 34fa2cd5ca
Skill update round trip / roundtrip (false, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, latest) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
source snapshot 2026-09-02
2026-09-02 02:03:16 +08:00

279 lines
8.8 KiB
TypeScript

import {
richMarkdownContextMenuCommandChannel,
type RichMarkdownContextMenuCommand,
type RichMarkdownContextMenuCommandPayload,
type RichMarkdownContextMenuTableTarget
} from '../../shared/rich-markdown-context-menu'
import { translateMain } from '../i18n/main-i18n'
type EditableContextMenuWebContents = Pick<
Electron.WebContents,
'replaceMisspelling' | 'send' | 'session'
>
function markdownCommandItem(
label: string,
command: RichMarkdownContextMenuCommand,
webContents: EditableContextMenuWebContents,
point: { x: number; y: number },
tableTargetId?: string
): Electron.MenuItemConstructorOptions {
return {
label,
click: () => {
const payload: RichMarkdownContextMenuCommandPayload = {
command,
...point,
...(tableTargetId ? { tableTargetId } : {})
}
webContents.send(richMarkdownContextMenuCommandChannel, payload)
}
}
}
function editableContextPasteItem(
label: string,
webContents: EditableContextMenuWebContents,
options?: { plainTextOnly?: boolean }
): Electron.MenuItemConstructorOptions {
return {
label,
click: () => {
// Why: context-menu paste must share renderer ownership with keyboard and
// app-menu paste so large text controls can chunk and terminals cannot
// receive duplicate native paste.
webContents.send('ui:editableContextPaste', {
plainTextOnly: options?.plainTextOnly === true
})
}
}
}
function buildMarkdownMenuTemplate(
webContents: EditableContextMenuWebContents,
point: { x: number; y: number },
tableTarget: RichMarkdownContextMenuTableTarget | null
): Electron.MenuItemConstructorOptions[] {
return [
markdownCommandItem('Add link', 'add-link', webContents, point),
{ type: 'separator' },
{
label: 'Format',
submenu: [
markdownCommandItem('Bold', 'bold', webContents, point),
markdownCommandItem('Italic', 'italic', webContents, point),
markdownCommandItem('Strike', 'strike', webContents, point),
markdownCommandItem('Inline code', 'inline-code', webContents, point),
markdownCommandItem('Code block', 'code-block', webContents, point),
markdownCommandItem('Quote', 'blockquote', webContents, point)
]
},
{
label: 'Paragraph',
submenu: [
markdownCommandItem('Body text', 'paragraph', webContents, point),
markdownCommandItem('Heading 1', 'heading-1', webContents, point),
markdownCommandItem('Heading 2', 'heading-2', webContents, point),
markdownCommandItem('Heading 3', 'heading-3', webContents, point),
markdownCommandItem('Heading 4', 'heading-4', webContents, point),
markdownCommandItem('Heading 5', 'heading-5', webContents, point),
{ type: 'separator' },
markdownCommandItem('Bullet list', 'bullet-list', webContents, point),
markdownCommandItem('Numbered list', 'ordered-list', webContents, point),
markdownCommandItem('Checklist', 'task-list', webContents, point)
]
},
{
label: 'Insert',
submenu: [
markdownCommandItem('Link', 'add-link', webContents, point),
markdownCommandItem('Image', 'image', webContents, point),
markdownCommandItem('Divider', 'divider', webContents, point),
markdownCommandItem('Code block', 'code-block', webContents, point)
]
},
...(tableTarget
? [
{
label: translateMain('auto.main.window.editableContextMenu.table', 'Table'),
submenu: [
{
...markdownCommandItem(
translateMain(
'auto.main.window.editableContextMenu.insertRowAbove',
'Insert row above'
),
'insert-row-above',
webContents,
point,
tableTarget.targetId
),
enabled: tableTarget.cellType !== 'header'
},
markdownCommandItem(
translateMain(
'auto.main.window.editableContextMenu.insertRowBelow',
'Insert row below'
),
'insert-row-below',
webContents,
point,
tableTarget.targetId
),
{
...markdownCommandItem(
translateMain('auto.main.window.editableContextMenu.deleteRow', 'Delete row'),
'delete-row',
webContents,
point,
tableTarget.targetId
),
enabled: tableTarget.cellType !== 'header'
},
{ type: 'separator' as const },
markdownCommandItem(
translateMain(
'auto.main.window.editableContextMenu.insertColumnLeft',
'Insert column left'
),
'insert-column-left',
webContents,
point,
tableTarget.targetId
),
markdownCommandItem(
translateMain(
'auto.main.window.editableContextMenu.insertColumnRight',
'Insert column right'
),
'insert-column-right',
webContents,
point,
tableTarget.targetId
),
markdownCommandItem(
translateMain('auto.main.window.editableContextMenu.deleteColumn', 'Delete column'),
'delete-column',
webContents,
point,
tableTarget.targetId
),
{ type: 'separator' as const },
markdownCommandItem(
translateMain('auto.main.window.editableContextMenu.deleteTable', 'Delete table'),
'delete-table',
webContents,
point,
tableTarget.targetId
)
]
}
]
: []),
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
editableContextPasteItem('Paste', webContents),
editableContextPasteItem('Paste as plain text', webContents, { plainTextOnly: true }),
{ role: 'selectAll' }
]
}
function buildNativeEditMenuTemplate(
webContents: EditableContextMenuWebContents
): Electron.MenuItemConstructorOptions[] {
return [
{ role: 'cut' },
{ role: 'copy' },
editableContextPasteItem('Paste', webContents),
editableContextPasteItem('Paste as plain text', webContents, { plainTextOnly: true }),
{ role: 'selectAll' }
]
}
export function buildEditableContextMenuTemplate(
params: Electron.ContextMenuParams,
webContents: EditableContextMenuWebContents,
options?: { tableTarget?: RichMarkdownContextMenuTableTarget | null }
): Electron.MenuItemConstructorOptions[] {
if (!params.isEditable) {
return []
}
const suggestions = params.dictionarySuggestions.slice(0, 5)
const isRichMarkdownSurface = params.formControlType === 'none'
const template: Electron.MenuItemConstructorOptions[] = suggestions.map((suggestion) => ({
label: suggestion,
click: () => webContents.replaceMisspelling(suggestion)
}))
if (params.misspelledWord) {
if (template.length > 0) {
template.push({ type: 'separator' })
}
template.push({
label: 'Add to dictionary',
click: () => {
webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
}
})
}
if (template.length > 0) {
template.push({ type: 'separator' })
}
template.push(
...(isRichMarkdownSurface
? buildMarkdownMenuTemplate(
webContents,
{ x: params.x, y: params.y },
options?.tableTarget ?? null
)
: buildNativeEditMenuTemplate(webContents))
)
return template
}
export function parseRichMarkdownContextMenuTableTarget(
value: unknown
): RichMarkdownContextMenuTableTarget | null {
if (!value || typeof value !== 'object') {
return null
}
const target = value as Partial<RichMarkdownContextMenuTableTarget>
if (
(target.cellType !== 'body' && target.cellType !== 'header') ||
typeof target.targetId !== 'string' ||
target.targetId.length === 0 ||
typeof target.x !== 'number' ||
!Number.isFinite(target.x) ||
typeof target.y !== 'number' ||
!Number.isFinite(target.y)
) {
return null
}
return {
cellType: target.cellType,
targetId: target.targetId,
x: target.x,
y: target.y
}
}
export function matchingRichMarkdownContextMenuTableTarget(
params: Electron.ContextMenuParams,
target: RichMarkdownContextMenuTableTarget | null
): RichMarkdownContextMenuTableTarget | null {
if (
!target ||
!params.isEditable ||
params.formControlType !== 'none' ||
params.x !== target.x ||
params.y !== target.y
) {
return null
}
return target
}