Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/components/graph/selectionToolbox/MenuOptionItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
<div
v-else
role="button"
class="group flex cursor-pointer items-center gap-2 rounded px-3 py-1.5 text-left text-sm text-text-primary hover:bg-interface-menu-component-surface-hovered"
:class="[
'group flex items-center gap-2 rounded px-3 py-1.5 text-left text-sm',
option.disabled
? 'cursor-not-allowed pointer-events-none text-node-icon-disabled'
: 'cursor-pointer text-text-primary hover:bg-interface-menu-component-surface-hovered'
]"
@click="handleClick"
>
<i v-if="option.icon" :class="[option.icon, 'h-4 w-4']" />
Expand Down Expand Up @@ -57,6 +62,9 @@ const props = defineProps<Props>()
const emit = defineEmits<Emits>()

const handleClick = (event: Event) => {
if (props.option.disabled) {
return
}
emit('click', props.option, event)
}
</script>
16 changes: 13 additions & 3 deletions src/components/graph/selectionToolbox/SubmenuPopover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -83,6 +90,9 @@ defineExpose({
})

const handleSubmenuClick = (subOption: SubMenuOption) => {
if (subOption.disabled) {
return
}
emit('submenu-click', subOption)
}

Expand Down
139 changes: 139 additions & 0 deletions src/composables/graph/contextMenuConverter.ts
Original file line number Diff line number Diff line change
@@ -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<string>()
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
})
}
25 changes: 25 additions & 0 deletions src/composables/graph/useMoreOptionsMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -19,13 +21,15 @@ export interface MenuOption {
action?: () => void
submenu?: SubMenuOption[]
badge?: BadgeVariant
disabled?: boolean
}

export interface SubMenuOption {
label: string
icon?: string
action: () => void
color?: string
disabled?: boolean
}

export enum BadgeVariant {
Expand Down Expand Up @@ -91,6 +95,8 @@ export function useMoreOptionsMenu() {
computeSelectionFlags
} = useSelectionState()

const canvasStore = useCanvasStore()

const { getImageMenuOptions } = useImageMenuOptions()
const {
getNodeInfoOption,
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions src/composables/useContextMenuTranslation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand Down
Loading