Files
ComfyUI_frontend/src/extensions/core/groupOptions.ts
Johnpaul Chiwetelu b3da6cf1b4 Contextmenu extension migration (#5993)
This pull request refactors how context menu items are contributed by
extensions in the LiteGraph-based canvas. The legacy monkey-patching
approach for adding context menu options is replaced by a new, explicit
API (`getCanvasMenuItems` and `getNodeMenuItems`) for extensions. A
compatibility layer is added to support legacy extensions and warn
developers about deprecated usage. The changes improve maintainability,
extension interoperability, and migration to the new context menu
system.

### Context Menu System Refactor

* Introduced a new API for extensions to contribute context menu items
via `getCanvasMenuItems` and `getNodeMenuItems` methods, replacing
legacy monkey-patching of `LGraphCanvas.prototype.getCanvasMenuOptions`.
Major extension files (`groupNode.ts`, `groupOptions.ts`,
`nodeTemplates.ts`) now use this new API.
[[1]](diffhunk://#diff-b29f141b89433027e7bb7cde57fad84f9e97ffbe5c58040d3e0fdb7905022917L1779-R1771)
[[2]](diffhunk://#diff-91169f3a27ff8974d5c8fc3346bd99c07bdfb5399984484630125fdd647ff02fL232-R239)
[[3]](diffhunk://#diff-04c18583d2dbfc013888e4c02fd432c250acbcecdef82bf7f6d9fd888e632a6eL447-R458)
* Added a compatibility layer (`legacyMenuCompat` in
`contextMenuCompat.ts`) to detect and warn when legacy monkey-patching
is used, and to extract legacy-added menu items for backward
compatibility.
[[1]](diffhunk://#diff-2b724cb107c04e290369fb927e2ae9fad03be9e617a7d4de2487deab89d0d018R2-R45)
[[2]](diffhunk://#diff-d3a8284ec16ae3f9512e33abe44ae653ed1aa45c9926485ef6270cc8d2b94ae6R1-R115)

### Extension Migration

* Refactored core extensions (`groupNode`, `groupOptions`, and
`nodeTemplates`) to implement the new context menu API, moving menu item
logic out of monkey-patched methods and into explicit extension methods.
[[1]](diffhunk://#diff-b29f141b89433027e7bb7cde57fad84f9e97ffbe5c58040d3e0fdb7905022917L1633-L1683)
[[2]](diffhunk://#diff-91169f3a27ff8974d5c8fc3346bd99c07bdfb5399984484630125fdd647ff02fL19-R77)
[[3]](diffhunk://#diff-04c18583d2dbfc013888e4c02fd432c250acbcecdef82bf7f6d9fd888e632a6eL366-R373)

### Type and Import Cleanup

* Updated imports for context menu types (`IContextMenuValue`) across
affected files for consistency with the new API.
[[1]](diffhunk://#diff-b29f141b89433027e7bb7cde57fad84f9e97ffbe5c58040d3e0fdb7905022917R4-L7)
[[2]](diffhunk://#diff-91169f3a27ff8974d5c8fc3346bd99c07bdfb5399984484630125fdd647ff02fL1-R11)
[[3]](diffhunk://#diff-04c18583d2dbfc013888e4c02fd432c250acbcecdef82bf7f6d9fd888e632a6eL2-R6)
[[4]](diffhunk://#diff-bde0dce9fe2403685d27b0e94a938c3d72824d02d01d1fd6167a0dddc6e585ddR10)

### Backward Compatibility and Migration Guidance

* The compatibility layer logs a deprecation warning to the console when
legacy monkey-patching is detected, helping developers migrate to the
new API.

---

These changes collectively modernize the context menu extension
mechanism, improve code clarity, and provide a migration path for legacy
extensions.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5993-Contextmenu-extension-migration-2876d73d3650813fae07c1141679637a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-10-28 04:02:28 +01:00

240 lines
6.5 KiB
TypeScript

import type {
IContextMenuValue,
Positionable
} from '@/lib/litegraph/src/interfaces'
import {
LGraphCanvas,
LGraphGroup,
type LGraphNode
} from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { ComfyExtension } from '@/types/comfy'
import { app } from '../../scripts/app'
function setNodeMode(node: LGraphNode, mode: number) {
node.mode = mode
node.graph?.change()
}
function addNodesToGroup(group: LGraphGroup, items: Iterable<Positionable>) {
const padding = useSettingStore().get('Comfy.GroupSelectedNodes.Padding')
group.resizeTo([...group.children, ...items], padding)
}
const ext: ComfyExtension = {
name: 'Comfy.GroupOptions',
getCanvasMenuItems(canvas: LGraphCanvas): IContextMenuValue[] {
const items: IContextMenuValue[] = []
// @ts-expect-error fixme ts strict error
const group = canvas.graph.getGroupOnPos(
canvas.graph_mouse[0],
canvas.graph_mouse[1]
)
if (!group) {
if (canvas.selectedItems.size > 0) {
items.push({
content: 'Add Group For Selected Nodes',
callback: () => {
const group = new LGraphGroup()
addNodesToGroup(group, canvas.selectedItems)
// @ts-expect-error fixme ts strict error
canvas.graph.add(group)
// @ts-expect-error fixme ts strict error
canvas.graph.change()
group.recomputeInsideNodes()
}
})
}
return items
}
// Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date
group.recomputeInsideNodes()
const nodesInGroup = group.nodes
items.push({
content: 'Add Selected Nodes To Group',
disabled: !canvas.selectedItems?.size,
callback: () => {
addNodesToGroup(group, canvas.selectedItems)
// @ts-expect-error fixme ts strict error
canvas.graph.change()
}
})
// No nodes in group, return default options
if (nodesInGroup.length === 0) {
return items
} else {
// Add a separator between the default options and the group options
// @ts-expect-error fixme ts strict error
items.push(null)
}
// Check if all nodes are the same mode
let allNodesAreSameMode = true
for (let i = 1; i < nodesInGroup.length; i++) {
if (nodesInGroup[i].mode !== nodesInGroup[0].mode) {
allNodesAreSameMode = false
break
}
}
items.push({
content: 'Fit Group To Nodes',
callback: () => {
group.recomputeInsideNodes()
const padding = useSettingStore().get(
'Comfy.GroupSelectedNodes.Padding'
)
group.resizeTo(group.children, padding)
// @ts-expect-error fixme ts strict error
canvas.graph.change()
}
})
items.push({
content: 'Select Nodes',
callback: () => {
canvas.selectNodes(nodesInGroup)
// @ts-expect-error fixme ts strict error
canvas.graph.change()
canvas.canvas.focus()
}
})
// Modes
// 0: Always
// 1: On Event
// 2: Never
// 3: On Trigger
// 4: Bypass
// If all nodes are the same mode, add a menu option to change the mode
if (allNodesAreSameMode) {
const mode = nodesInGroup[0].mode
switch (mode) {
case 0:
// All nodes are always, option to disable, and bypass
items.push({
content: 'Set Group Nodes to Never',
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 2)
}
}
})
items.push({
content: 'Bypass Group Nodes',
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 4)
}
}
})
break
case 2:
// All nodes are never, option to enable, and bypass
items.push({
content: 'Set Group Nodes to Always',
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 0)
}
}
})
items.push({
content: 'Bypass Group Nodes',
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 4)
}
}
})
break
case 4:
// All nodes are bypass, option to enable, and disable
items.push({
content: 'Set Group Nodes to Always',
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 0)
}
}
})
items.push({
content: 'Set Group Nodes to Never',
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 2)
}
}
})
break
default:
// All nodes are On Trigger or On Event(Or other?), option to disable, set to always, or bypass
items.push({
content: 'Set Group Nodes to Always',
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 0)
}
}
})
items.push({
content: 'Set Group Nodes to Never',
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 2)
}
}
})
items.push({
content: 'Bypass Group Nodes',
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 4)
}
}
})
break
}
} else {
// Nodes are not all the same mode, add a menu option to change the mode to always, never, or bypass
items.push({
content: 'Set Group Nodes to Always',
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 0)
}
}
})
items.push({
content: 'Set Group Nodes to Never',
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 2)
}
}
})
items.push({
content: 'Bypass Group Nodes',
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 4)
}
}
})
}
return items
}
}
app.registerExtension(ext)