diff --git a/src/components/graph/selectionToolbox/MenuOptionItem.vue b/src/components/graph/selectionToolbox/MenuOptionItem.vue index fb1f8c01a5..4de71b997b 100644 --- a/src/components/graph/selectionToolbox/MenuOptionItem.vue +++ b/src/components/graph/selectionToolbox/MenuOptionItem.vue @@ -6,7 +6,12 @@
@@ -57,6 +62,9 @@ const props = defineProps() const emit = defineEmits() const handleClick = (event: Event) => { + if (props.option.disabled) { + return + } emit('click', props.option, event) } diff --git a/src/components/graph/selectionToolbox/SubmenuPopover.vue b/src/components/graph/selectionToolbox/SubmenuPopover.vue index 12c1a8571a..2b4ccca149 100644 --- a/src/components/graph/selectionToolbox/SubmenuPopover.vue +++ b/src/components/graph/selectionToolbox/SubmenuPopover.vue @@ -19,9 +19,15 @@ v-for="subOption in option.submenu" :key="subOption.label" :class=" - isColorSubmenu - ? 'w-7 h-7 flex items-center justify-center hover:bg-smoke-100 dark-theme:hover:bg-zinc-700 rounded cursor-pointer' - : 'flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-smoke-100 dark-theme:hover:bg-zinc-700 rounded cursor-pointer' + cn( + 'flex items-center rounded', + isColorSubmenu + ? 'w-7 h-7 justify-center' + : 'gap-2 px-3 py-1.5 text-sm', + subOption.disabled + ? 'cursor-not-allowed pointer-events-none text-node-icon-disabled' + : 'hover:bg-smoke-100 dark-theme:hover:bg-zinc-700 cursor-pointer' + ) " :title="subOption.label" @click="handleSubmenuClick(subOption)" @@ -53,6 +59,7 @@ import type { SubMenuOption } from '@/composables/graph/useMoreOptionsMenu' import { useNodeCustomization } from '@/composables/graph/useNodeCustomization' +import { cn } from '@/utils/tailwindUtil' interface Props { option: MenuOption @@ -83,6 +90,9 @@ defineExpose({ }) const handleSubmenuClick = (subOption: SubMenuOption) => { + if (subOption.disabled) { + return + } emit('submenu-click', subOption) } diff --git a/src/composables/graph/contextMenuConverter.ts b/src/composables/graph/contextMenuConverter.ts new file mode 100644 index 0000000000..a988053bab --- /dev/null +++ b/src/composables/graph/contextMenuConverter.ts @@ -0,0 +1,139 @@ +import type { IContextMenuValue } from '@/lib/litegraph/src/litegraph' + +import type { MenuOption, SubMenuOption } from './useMoreOptionsMenu' + +/** + * Convert LiteGraph IContextMenuValue items to Vue MenuOption format + * Used to bridge LiteGraph context menus into Vue node menus + */ +export function convertContextMenuToOptions( + items: (IContextMenuValue | null)[] +): MenuOption[] { + const result: MenuOption[] = [] + + for (const item of items) { + // Null items are separators in LiteGraph + if (item === null) { + result.push({ type: 'divider' }) + continue + } + + // Skip items without content (shouldn't happen, but be safe) + if (!item.content) { + continue + } + + const option: MenuOption = { + label: item.content + } + + // Pass through disabled state + if (item.disabled) { + option.disabled = true + } + + // Handle callback (only if not disabled) + if (item.callback && !item.disabled) { + // Wrap the callback to match the () => void signature + option.action = () => { + try { + void item.callback?.call( + item as any, + item.value, + {}, + null as any, + null as any, + item as any + ) + } catch (error) { + console.error('Error executing context menu callback:', error) + } + } + } + + // Handle submenus + if (item.has_submenu && item.submenu?.options) { + option.hasSubmenu = true + option.submenu = convertSubmenuToOptions(item.submenu.options) + } + + result.push(option) + } + + return result +} + +/** + * Convert LiteGraph submenu items to Vue SubMenuOption format + */ +function convertSubmenuToOptions( + items: readonly (IContextMenuValue | string | null)[] +): SubMenuOption[] { + const result: SubMenuOption[] = [] + + for (const item of items) { + // Skip null separators and string items + if (!item || typeof item === 'string') continue + + if (!item.content) continue + + const subOption: SubMenuOption = { + label: item.content, + action: () => { + try { + void item.callback?.call( + item as any, + item.value, + {}, + null as any, + null as any, + item as any + ) + } catch (error) { + console.error('Error executing submenu callback:', error) + } + } + } + + // Pass through disabled state + if (item.disabled) { + subOption.disabled = true + } + + result.push(subOption) + } + + return result +} + +/** + * Check if a menu option already exists in the list by label + */ +export function menuOptionExists( + options: MenuOption[], + label: string +): boolean { + return options.some((opt) => opt.label === label) +} + +/** + * Filter out duplicate menu items based on label + * Keeps the first occurrence of each label + */ +export function removeDuplicateMenuOptions( + options: MenuOption[] +): MenuOption[] { + const seen = new Set() + return options.filter((opt) => { + // Always keep dividers + if (opt.type === 'divider') return true + + // Skip items without labels + if (!opt.label) return true + + // Filter duplicates + if (seen.has(opt.label)) return false + seen.add(opt.label) + return true + }) +} diff --git a/src/composables/graph/useMoreOptionsMenu.ts b/src/composables/graph/useMoreOptionsMenu.ts index fcf8ab8c7d..96d01cf2c8 100644 --- a/src/composables/graph/useMoreOptionsMenu.ts +++ b/src/composables/graph/useMoreOptionsMenu.ts @@ -2,8 +2,10 @@ import { computed, ref } from 'vue' import type { Ref } from 'vue' import type { LGraphGroup } from '@/lib/litegraph/src/litegraph' +import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { isLGraphGroup } from '@/utils/litegraphUtil' +import { convertContextMenuToOptions } from './contextMenuConverter' import { useGroupMenuOptions } from './useGroupMenuOptions' import { useImageMenuOptions } from './useImageMenuOptions' import { useNodeMenuOptions } from './useNodeMenuOptions' @@ -19,6 +21,7 @@ export interface MenuOption { action?: () => void submenu?: SubMenuOption[] badge?: BadgeVariant + disabled?: boolean } export interface SubMenuOption { @@ -26,6 +29,7 @@ export interface SubMenuOption { icon?: string action: () => void color?: string + disabled?: boolean } export enum BadgeVariant { @@ -91,6 +95,8 @@ export function useMoreOptionsMenu() { computeSelectionFlags } = useSelectionState() + const canvasStore = useCanvasStore() + const { getImageMenuOptions } = useImageMenuOptions() const { getNodeInfoOption, @@ -138,6 +144,25 @@ export function useMoreOptionsMenu() { ? selectedGroups[0] : null const hasSubgraphsSelected = hasSubgraphs.value + + // For single node selection, use LiteGraph menu as the primary source + if ( + selectedNodes.value.length === 1 && + !groupContext && + canvasStore.canvas + ) { + try { + const node = selectedNodes.value[0] + const rawItems = canvasStore.canvas.getNodeMenuOptions(node) + const options = convertContextMenuToOptions(rawItems) + return options + } catch (error) { + console.error('Error getting LiteGraph menu items:', error) + // Fall through to Vue menu as fallback + } + } + + // For other cases (groups, multiple selections), build Vue menu const options: MenuOption[] = [] // Section 1: Basic selection operations (Rename, Copy, Duplicate) diff --git a/src/composables/useContextMenuTranslation.ts b/src/composables/useContextMenuTranslation.ts index cfcf5c809f..009ca3992a 100644 --- a/src/composables/useContextMenuTranslation.ts +++ b/src/composables/useContextMenuTranslation.ts @@ -58,6 +58,9 @@ export const useContextMenuTranslation = () => { 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 ( @@ -73,11 +76,30 @@ export const useContextMenuTranslation = () => { 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 as ( + ...args: unknown[] + ) => IContextMenuValue[], + nodeMenuFn as (...args: unknown[]) => IContextMenuValue[], + LGraphCanvas.prototype + ) + function translateMenus( values: readonly (IContextMenuValue | string | null)[] | undefined, options: IContextMenuOptions