mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-23 06:10:32 +00:00
Group Nodes are a legacy feature superseded by Subgraphs. This removes all UI entry points for creating new Group Nodes, while keeping the loading, ungrouping, and management code intact so existing workflows that contain Group Nodes continue to load and can still be unpacked or managed. Removed entry points: - 'Convert selected nodes to group node' command - Alt+G keybinding - 'Convert to Group Node (Deprecated)' canvas and node context menu items - 'Convert to Group Node' option in the Vue selection menu - Associated en locale strings - Browser tests that exercised the creation flow
316 lines
9.0 KiB
TypeScript
316 lines
9.0 KiB
TypeScript
import { computed, ref } from 'vue'
|
|
import type { Ref } from 'vue'
|
|
|
|
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
|
import type {
|
|
LGraphGroup,
|
|
LGraphNode,
|
|
NodeId
|
|
} from '@/lib/litegraph/src/litegraph'
|
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
|
import { getExtraOptionsForWidget } from '@/services/litegraphService'
|
|
import { isLGraphGroup } from '@/utils/litegraphUtil'
|
|
|
|
import {
|
|
buildStructuredMenu,
|
|
convertContextMenuToOptions
|
|
} from './contextMenuConverter'
|
|
import { useGroupMenuOptions } from './useGroupMenuOptions'
|
|
import { useImageMenuOptions } from './useImageMenuOptions'
|
|
import { useNodeMenuOptions } from './useNodeMenuOptions'
|
|
import { useSelectionMenuOptions } from './useSelectionMenuOptions'
|
|
import { useSelectionState } from './useSelectionState'
|
|
|
|
export interface MenuOption {
|
|
label?: string
|
|
icon?: string
|
|
shortcut?: string
|
|
hasSubmenu?: boolean
|
|
type?: 'divider' | 'category'
|
|
action?: () => void
|
|
submenu?: SubMenuOption[]
|
|
badge?: BadgeVariant
|
|
disabled?: boolean
|
|
source?: 'litegraph' | 'vue'
|
|
isColorPicker?: boolean
|
|
isShapePicker?: boolean
|
|
}
|
|
|
|
export interface SubMenuOption {
|
|
label: string
|
|
icon?: string
|
|
action: () => void
|
|
color?: string
|
|
disabled?: boolean
|
|
}
|
|
|
|
export enum BadgeVariant {
|
|
NEW = 'new'
|
|
}
|
|
|
|
// Global singleton for NodeOptions component reference
|
|
let nodeOptionsInstance: null | NodeOptionsInstance = null
|
|
|
|
const hoveredWidget = ref<[string, NodeId | undefined]>()
|
|
|
|
/**
|
|
* Toggle the node options popover
|
|
* @param event - The trigger event
|
|
*/
|
|
export function toggleNodeOptions(event: Event) {
|
|
if (nodeOptionsInstance?.toggle) {
|
|
nodeOptionsInstance.toggle(event)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show the node options popover (always shows, doesn't toggle)
|
|
* Use this for contextmenu events where we always want to show at the new position
|
|
* @param event - The trigger event (must be MouseEvent for position)
|
|
*/
|
|
export function showNodeOptions(
|
|
event: MouseEvent,
|
|
widgetName?: string,
|
|
nodeId?: NodeId
|
|
) {
|
|
hoveredWidget.value = widgetName ? [widgetName, nodeId] : undefined
|
|
if (nodeOptionsInstance?.show) {
|
|
nodeOptionsInstance.show(event)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if the node options menu is currently open
|
|
*/
|
|
export function isNodeOptionsOpen(): boolean {
|
|
return nodeOptionsInstance?.isOpen.value ?? false
|
|
}
|
|
|
|
interface NodeOptionsInstance {
|
|
toggle: (event: Event) => void
|
|
show: (event: MouseEvent) => void
|
|
hide: () => void
|
|
isOpen: Ref<boolean>
|
|
}
|
|
|
|
/**
|
|
* Register the NodeOptions component instance
|
|
* @param instance - The NodeOptions component instance
|
|
*/
|
|
export function registerNodeOptionsInstance(
|
|
instance: null | NodeOptionsInstance
|
|
) {
|
|
nodeOptionsInstance = instance
|
|
}
|
|
|
|
/**
|
|
* Mark menu options as coming from Vue hardcoded menu
|
|
*/
|
|
function markAsVueOptions(options: MenuOption[]): MenuOption[] {
|
|
return options.map((opt) => {
|
|
// Don't mark dividers or category labels
|
|
if (opt.type === 'divider' || opt.type === 'category') {
|
|
return opt
|
|
}
|
|
return { ...opt, source: 'vue' }
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Composable for managing the More Options menu configuration
|
|
* Refactored to use smaller, focused composables for better maintainability
|
|
*/
|
|
export function useMoreOptionsMenu() {
|
|
const {
|
|
selectedItems,
|
|
selectedNodes,
|
|
canOpenNodeInfo,
|
|
openNodeInfo,
|
|
hasSubgraphs: hasSubgraphsComputed,
|
|
hasImageNode,
|
|
hasOutputNodesSelected,
|
|
hasMultipleSelection,
|
|
computeSelectionFlags
|
|
} = useSelectionState()
|
|
|
|
const canvasStore = useCanvasStore()
|
|
|
|
const { getImageMenuOptions } = useImageMenuOptions()
|
|
const {
|
|
getNodeInfoOption,
|
|
getNodeVisualOptions,
|
|
getPinOption,
|
|
getBypassOption,
|
|
getRunBranchOption
|
|
} = useNodeMenuOptions()
|
|
const {
|
|
getFitGroupToNodesOption,
|
|
getGroupColorOptions,
|
|
getGroupModeOptions
|
|
} = useGroupMenuOptions()
|
|
const {
|
|
getBasicSelectionOptions,
|
|
getMultipleNodesOptions,
|
|
getSubgraphOptions
|
|
} = useSelectionMenuOptions()
|
|
|
|
const hasSubgraphs = hasSubgraphsComputed
|
|
const hasMultipleNodes = hasMultipleSelection
|
|
|
|
// Internal version to force menu rebuild after state mutations
|
|
const optionsVersion = ref(0)
|
|
const bump = () => {
|
|
optionsVersion.value++
|
|
}
|
|
|
|
const menuOptions = computed((): MenuOption[] => {
|
|
// Reference selection flags to ensure re-computation when they change
|
|
void optionsVersion.value
|
|
const states = computeSelectionFlags()
|
|
|
|
// Detect single group selection context (and no nodes explicitly selected)
|
|
const selectedGroups = selectedItems.value.filter(
|
|
isLGraphGroup
|
|
) as LGraphGroup[]
|
|
const groupContext: LGraphGroup | null =
|
|
selectedGroups.length === 1 && selectedNodes.value.length === 0
|
|
? selectedGroups[0]
|
|
: null
|
|
const hasSubgraphsSelected = hasSubgraphs.value
|
|
|
|
// For single node selection, also get LiteGraph menu items to merge
|
|
const litegraphOptions: MenuOption[] = []
|
|
const node: LGraphNode | undefined = selectedNodes.value[0]
|
|
if (
|
|
selectedNodes.value.length === 1 &&
|
|
!groupContext &&
|
|
canvasStore.canvas
|
|
) {
|
|
try {
|
|
const rawItems = canvasStore.canvas.getNodeMenuOptions(node)
|
|
// Don't apply structuring yet - we'll do it after merging with Vue options
|
|
litegraphOptions.push(
|
|
...convertContextMenuToOptions(rawItems, node, false)
|
|
)
|
|
} catch (error) {
|
|
console.error('Error getting LiteGraph menu items:', error)
|
|
}
|
|
}
|
|
|
|
const options: MenuOption[] = []
|
|
|
|
// Section 1: Basic selection operations (Rename, Copy, Duplicate)
|
|
const basicOps = getBasicSelectionOptions()
|
|
options.push(...basicOps)
|
|
options.push({ type: 'divider' })
|
|
|
|
// Section 2: Node actions (Run Branch, Pin, Bypass, Mute)
|
|
if (hasOutputNodesSelected.value) {
|
|
const runBranch = getRunBranchOption()
|
|
options.push(runBranch)
|
|
}
|
|
if (!groupContext) {
|
|
const pin = getPinOption(states, bump)
|
|
const bypass = getBypassOption(states, bump)
|
|
options.push(pin)
|
|
options.push(bypass)
|
|
}
|
|
if (groupContext) {
|
|
const groupModes = getGroupModeOptions(groupContext, bump)
|
|
options.push(...groupModes)
|
|
}
|
|
options.push({ type: 'divider' })
|
|
|
|
// Section 3: Structure operations (Convert to Subgraph, Frame selection, Minimize Node)
|
|
options.push(
|
|
...getSubgraphOptions({
|
|
hasSubgraphs: hasSubgraphsSelected,
|
|
hasMultipleSelection: hasMultipleNodes.value
|
|
})
|
|
)
|
|
if (hasMultipleNodes.value) {
|
|
options.push(...getMultipleNodesOptions())
|
|
}
|
|
if (groupContext) {
|
|
options.push(getFitGroupToNodesOption(groupContext))
|
|
} else {
|
|
// Node context: Expand/Minimize
|
|
const visualOptions = getNodeVisualOptions(states, bump)
|
|
if (visualOptions.length > 0) {
|
|
options.push(visualOptions[0]) // Expand/Minimize (index 0)
|
|
}
|
|
}
|
|
options.push({ type: 'divider' })
|
|
|
|
// Section 4: Node properties (Node Info, Shape, Color)
|
|
if (canOpenNodeInfo.value) {
|
|
options.push(getNodeInfoOption(openNodeInfo))
|
|
}
|
|
if (groupContext) {
|
|
options.push(getGroupColorOptions(groupContext, bump))
|
|
} else {
|
|
// Add shape and color options
|
|
const visualOptions = getNodeVisualOptions(states, bump)
|
|
if (visualOptions.length > 1) {
|
|
options.push(visualOptions[1]) // Shape (index 1)
|
|
}
|
|
if (visualOptions.length > 2) {
|
|
options.push(visualOptions[2]) // Color (index 2)
|
|
}
|
|
}
|
|
options.push({ type: 'divider' })
|
|
|
|
// Section 5: Image operations (if image node)
|
|
if (hasImageNode.value && selectedNodes.value.length > 0) {
|
|
options.push(...getImageMenuOptions(selectedNodes.value[0]))
|
|
options.push({ type: 'divider' })
|
|
}
|
|
const [widgetName, nodeId] = hoveredWidget.value ?? []
|
|
const widget =
|
|
nodeId !== undefined
|
|
? node?.widgets?.find(
|
|
(w) =>
|
|
isPromotedWidgetView(w) &&
|
|
w.sourceWidgetName === widgetName &&
|
|
w.sourceNodeId === nodeId
|
|
)
|
|
: node?.widgets?.find((w) => w.name === widgetName)
|
|
if (widget) {
|
|
const widgetOptions = convertContextMenuToOptions(
|
|
getExtraOptionsForWidget(node, widget)
|
|
)
|
|
if (widgetOptions) {
|
|
options.push(...widgetOptions)
|
|
options.push({ type: 'divider' })
|
|
}
|
|
}
|
|
|
|
// Section 6 & 7: Extensions and Delete are handled by buildStructuredMenu
|
|
|
|
// Mark all Vue options with source
|
|
const markedVueOptions = markAsVueOptions(options)
|
|
|
|
if (litegraphOptions.length > 0) {
|
|
// Merge: LiteGraph options first, then Vue options (Vue will win in dedup)
|
|
const merged = [...litegraphOptions, ...markedVueOptions]
|
|
return buildStructuredMenu(merged)
|
|
}
|
|
// For other cases, structure the Vue options
|
|
const result = buildStructuredMenu(markedVueOptions)
|
|
return result
|
|
})
|
|
|
|
// Computed property to get only menu items with submenus
|
|
const menuOptionsWithSubmenu = computed(() =>
|
|
menuOptions.value.filter((option) => option.hasSubmenu && option.submenu)
|
|
)
|
|
|
|
return {
|
|
menuOptions,
|
|
menuOptionsWithSubmenu,
|
|
bump,
|
|
hasSubgraphs,
|
|
registerNodeOptionsInstance
|
|
}
|
|
}
|