mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-08 09:00:05 +00:00
## Summary - Add `contextMenuConverter.ts` with utilities for converting LiteGraph context menu items to Vue menu format - Improve `contextMenuCompat.ts` with set-based diffing for more reliable legacy extension detection - Extend `MenuOption`/`SubMenuOption` types with `source`, `disabled`, `isColorPicker`, and `category` type fields - Add unit tests for converter functions ## Context This is foundational work for migrating the node context menu from a custom Popover-based component to PrimeVue ContextMenu. The converter provides: - Menu ordering and section grouping (core items first, then extensions) - Deduplication with preference for Vue-native items over LiteGraph items - Extension categorization with labeled section - Support for disabled states and color picker submenus ## Test plan - [x] Unit tests pass for `buildStructuredMenu` (9 tests) - [x] Unit tests pass for `convertContextMenuToOptions` (7 tests) - [x] Typecheck passes - [x] Lint passes - [x] Knip passes (no unused exports) ## Related This is PR 1 of 2 for the node context menu migration. PR 2 will wire up the UI component. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7113-feat-Add-context-menu-converter-infrastructure-2be6d73d3650816ca6c9d2cf50f10159) by [Unito](https://www.unito.io)
178 lines
5.2 KiB
TypeScript
178 lines
5.2 KiB
TypeScript
import { st, te } from '@/i18n'
|
|
import { legacyMenuCompat } from '@/lib/litegraph/src/contextMenuCompat'
|
|
import type {
|
|
IContextMenuOptions,
|
|
IContextMenuValue,
|
|
INodeInputSlot,
|
|
IWidget
|
|
} from '@/lib/litegraph/src/litegraph'
|
|
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
|
import { app } from '@/scripts/app'
|
|
import { normalizeI18nKey } from '@/utils/formatUtil'
|
|
|
|
/**
|
|
* Add translation for litegraph context menu.
|
|
*/
|
|
export const useContextMenuTranslation = () => {
|
|
// Install compatibility layer BEFORE any extensions load
|
|
legacyMenuCompat.install(LGraphCanvas.prototype, 'getCanvasMenuOptions')
|
|
|
|
const { getCanvasMenuOptions } = LGraphCanvas.prototype
|
|
const getCanvasCenterMenuOptions = function (
|
|
this: LGraphCanvas,
|
|
...args: Parameters<typeof getCanvasMenuOptions>
|
|
) {
|
|
const res: (IContextMenuValue | null)[] = getCanvasMenuOptions.apply(
|
|
this,
|
|
args
|
|
)
|
|
|
|
// Add items from new extension API
|
|
const newApiItems = app.collectCanvasMenuItems(this)
|
|
for (const item of newApiItems) {
|
|
res.push(item)
|
|
}
|
|
|
|
// Add legacy monkey-patched items
|
|
const legacyItems = legacyMenuCompat.extractLegacyItems(
|
|
'getCanvasMenuOptions',
|
|
this,
|
|
...args
|
|
)
|
|
for (const item of legacyItems) {
|
|
res.push(item)
|
|
}
|
|
|
|
// Translate all items
|
|
for (const item of res) {
|
|
if (item?.content) {
|
|
item.content = st(`contextMenu.${item.content}`, item.content)
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
LGraphCanvas.prototype.getCanvasMenuOptions = getCanvasCenterMenuOptions
|
|
|
|
legacyMenuCompat.registerWrapper(
|
|
'getCanvasMenuOptions',
|
|
getCanvasCenterMenuOptions,
|
|
getCanvasMenuOptions,
|
|
LGraphCanvas.prototype
|
|
)
|
|
|
|
// Install compatibility layer for getNodeMenuOptions
|
|
legacyMenuCompat.install(LGraphCanvas.prototype, 'getNodeMenuOptions')
|
|
|
|
// Wrap getNodeMenuOptions to add new API items
|
|
const nodeMenuFn = LGraphCanvas.prototype.getNodeMenuOptions
|
|
const getNodeMenuOptionsWithExtensions = function (
|
|
this: LGraphCanvas,
|
|
...args: Parameters<typeof nodeMenuFn>
|
|
) {
|
|
const res = nodeMenuFn.apply(this, args) as (IContextMenuValue | null)[]
|
|
|
|
// Add items from new extension API
|
|
const node = args[0]
|
|
const newApiItems = app.collectNodeMenuItems(node)
|
|
for (const item of newApiItems) {
|
|
res.push(item)
|
|
}
|
|
|
|
// Add legacy monkey-patched items
|
|
const legacyItems = legacyMenuCompat.extractLegacyItems(
|
|
'getNodeMenuOptions',
|
|
this,
|
|
...args
|
|
)
|
|
for (const item of legacyItems) {
|
|
res.push(item)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
LGraphCanvas.prototype.getNodeMenuOptions = getNodeMenuOptionsWithExtensions
|
|
|
|
legacyMenuCompat.registerWrapper(
|
|
'getNodeMenuOptions',
|
|
getNodeMenuOptionsWithExtensions,
|
|
nodeMenuFn,
|
|
LGraphCanvas.prototype
|
|
)
|
|
|
|
function translateMenus(
|
|
values: readonly (IContextMenuValue | string | null)[] | undefined,
|
|
options: IContextMenuOptions
|
|
) {
|
|
if (!values) return
|
|
const reInput = /Convert (.*) to input/
|
|
const reWidget = /Convert (.*) to widget/
|
|
const cvt = st('contextMenu.Convert ', 'Convert ')
|
|
const tinp = st('contextMenu. to input', ' to input')
|
|
const twgt = st('contextMenu. to widget', ' to widget')
|
|
for (const value of values) {
|
|
if (typeof value === 'string') continue
|
|
|
|
translateMenus(value?.submenu?.options, options)
|
|
if (!value?.content) {
|
|
continue
|
|
}
|
|
if (te(`contextMenu.${value.content}`)) {
|
|
value.content = st(`contextMenu.${value.content}`, value.content)
|
|
}
|
|
|
|
// for capture translation text of input and widget
|
|
const extraInfo: any = options.extra || options.parentMenu?.options?.extra
|
|
// widgets and inputs
|
|
const matchInput = value.content?.match(reInput)
|
|
if (matchInput) {
|
|
let match = matchInput[1]
|
|
extraInfo?.inputs?.find((i: INodeInputSlot) => {
|
|
if (i.name != match) return false
|
|
match = i.label ? i.label : i.name
|
|
})
|
|
extraInfo?.widgets?.find((i: IWidget) => {
|
|
if (i.name != match) return false
|
|
match = i.label ? i.label : i.name
|
|
})
|
|
value.content = cvt + match + tinp
|
|
continue
|
|
}
|
|
const matchWidget = value.content?.match(reWidget)
|
|
if (matchWidget) {
|
|
let match = matchWidget[1]
|
|
extraInfo?.inputs?.find((i: INodeInputSlot) => {
|
|
if (i.name != match) return false
|
|
match = i.label ? i.label : i.name
|
|
})
|
|
extraInfo?.widgets?.find((i: IWidget) => {
|
|
if (i.name != match) return false
|
|
match = i.label ? i.label : i.name
|
|
})
|
|
value.content = cvt + match + twgt
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
const OriginalContextMenu = LiteGraph.ContextMenu
|
|
function ContextMenu(
|
|
values: (IContextMenuValue | string)[],
|
|
options: IContextMenuOptions
|
|
) {
|
|
if (options.title) {
|
|
options.title = st(
|
|
`nodeDefs.${normalizeI18nKey(options.title)}.display_name`,
|
|
options.title
|
|
)
|
|
}
|
|
translateMenus(values, options)
|
|
const ctx = new OriginalContextMenu(values, options)
|
|
return ctx
|
|
}
|
|
|
|
LiteGraph.ContextMenu = ContextMenu as unknown as typeof LiteGraph.ContextMenu
|
|
LiteGraph.ContextMenu.prototype = OriginalContextMenu.prototype
|
|
}
|