Support widget specific contextmenu options in vue (#8431)

<img width="614" height="485" alt="image"
src="https://github.com/user-attachments/assets/2a635dec-8bed-4fab-9881-5e6057d482e1"
/>

These options were defined in `litegraphService`. While the existing
code for defining options is reused (to ensure there's no implementation
drift) these extra widget options use the litegraph format for context
menu options and do not belong in `useSelectionMenuOptions`. They have
been moved out of `useLitegraphService` (good), but left in
`litegraphService` (not great)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8431-Support-widget-specific-contextmenu-options-in-vue-2f76d73d3650814fb20fca352dc81e3b)
by [Unito](https://www.unito.io)
This commit is contained in:
AustinMroz
2026-01-29 11:38:41 -08:00
committed by GitHub
parent cabd08f0ec
commit af8433fb3d
5 changed files with 77 additions and 45 deletions

View File

@@ -1,8 +1,9 @@
import { computed, ref } from 'vue'
import type { Ref } from 'vue'
import type { LGraphGroup } from '@/lib/litegraph/src/litegraph'
import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { getExtraOptionsForWidget } from '@/services/litegraphService'
import { isLGraphGroup } from '@/utils/litegraphUtil'
import {
@@ -45,6 +46,8 @@ export enum BadgeVariant {
// Global singleton for NodeOptions component reference
let nodeOptionsInstance: null | NodeOptionsInstance = null
const hoveredWidgetName = ref<string>()
/**
* Toggle the node options popover
* @param event - The trigger event
@@ -61,6 +64,13 @@ export function toggleNodeOptions(event: Event) {
* @param event - The trigger event (must be MouseEvent for position)
*/
export function showNodeOptions(event: MouseEvent) {
hoveredWidgetName.value = undefined
const target = event.target
if (target instanceof HTMLElement) {
const widgetEl = target.closest('.lg-node-widget')
if (widgetEl instanceof HTMLElement)
hoveredWidgetName.value = widgetEl.dataset.widgetName
}
if (nodeOptionsInstance?.show) {
nodeOptionsInstance.show(event)
}
@@ -133,8 +143,8 @@ export function useMoreOptionsMenu() {
} = useGroupMenuOptions()
const {
getBasicSelectionOptions,
getSubgraphOptions,
getMultipleNodesOptions
getMultipleNodesOptions,
getSubgraphOptions
} = useSelectionMenuOptions()
const hasSubgraphs = hasSubgraphsComputed
@@ -164,13 +174,13 @@ export function useMoreOptionsMenu() {
// 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 node = selectedNodes.value[0]
const rawItems = canvasStore.canvas.getNodeMenuOptions(node)
// Don't apply structuring yet - we'll do it after merging with Vue options
litegraphOptions.push(
@@ -249,6 +259,18 @@ export function useMoreOptionsMenu() {
options.push(...getImageMenuOptions(selectedNodes.value[0]))
options.push({ type: 'divider' })
}
const rawName = hoveredWidgetName.value
const widget = node?.widgets?.find((w) => w.name === rawName)
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

View File

@@ -105,6 +105,7 @@ export function addWidgetPromotionOptions(
content: `Promote Widget: ${widget.label ?? widget.name}`,
callback: () => {
promoteWidget(node, widget, promotableParents)
widget.callback?.(widget.value)
}
})
else {
@@ -112,6 +113,7 @@ export function addWidgetPromotionOptions(
content: `Un-Promote Widget: ${widget.label ?? widget.name}`,
callback: () => {
demoteWidget(node, widget, parents)
widget.callback?.(widget.value)
}
})
}

View File

@@ -30,6 +30,7 @@
(!widget.simplified.options?.advanced || showAdvanced)
"
class="lg-node-widget group col-span-full grid grid-cols-subgrid items-stretch"
:data-widget-name="widget.name"
>
<!-- Widget Input Slot Dot -->
<div

View File

@@ -138,6 +138,7 @@ function updateValueBy(delta: number) {
const dragValue = ref<number>()
const dragDelta = ref(0)
function handleMouseDown(e: PointerEvent) {
if (e.button > 0) return
if (props.widget.options?.disabled) return
const { target } = e
if (!(target instanceof HTMLElement)) return

View File

@@ -29,6 +29,7 @@ import type {
ISerialisableNodeOutput,
ISerialisedNode
} from '@/lib/litegraph/src/types/serialisation'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
@@ -71,6 +72,49 @@ export interface HasInitialMinSize {
export const CONFIG = Symbol()
export const GET_CONFIG = Symbol()
export function getExtraOptionsForWidget(
node: LGraphNode,
widget: IBaseWidget
) {
const options: IContextMenuValue[] = []
const input = node.inputs.find((inp) => inp.widget?.name === widget.name)
if (input) {
options.unshift({
content: `${t('contextMenu.RenameWidget')}: ${widget.label ?? widget.name}`,
callback: async () => {
const newLabel = await useDialogService().prompt({
title: t('g.rename'),
message: t('g.enterNewName') + ':',
defaultValue: widget.label,
placeholder: widget.name
})
if (newLabel === null) return
widget.label = newLabel || undefined
input.label = newLabel || undefined
widget.callback?.(widget.value)
useCanvasStore().canvas?.setDirty(true)
}
})
}
const favoritedWidgetsStore = useFavoritedWidgetsStore()
const isFavorited = favoritedWidgetsStore.isFavorited(node, widget.name)
options.unshift({
content: isFavorited
? `${t('contextMenu.UnfavoriteWidget')}: ${widget.label ?? widget.name}`
: `${t('contextMenu.FavoriteWidget')}: ${widget.label ?? widget.name}`,
callback: () => {
favoritedWidgetsStore.toggleFavorite(node, widget.name)
}
})
if (node.graph && !node.graph.isRootGraph) {
addWidgetPromotionOptions(options, widget, node)
}
return options
}
/**
* Service that augments litegraph with ComfyUI specific functionality.
*/
@@ -678,47 +722,8 @@ export const useLitegraphService = () => {
}
const [x, y] = canvas.graph_mouse
const overWidget = this.getWidgetOnPos(x, y, true)
if (overWidget) {
const input = this.inputs.find(
(inp) => inp.widget?.name === overWidget.name
)
if (input) {
options.unshift({
content: `${t('contextMenu.RenameWidget')}: ${overWidget.label ?? overWidget.name}`,
callback: async () => {
const newLabel = await useDialogService().prompt({
title: t('g.rename'),
message: t('g.enterNewName') + ':',
defaultValue: overWidget.label,
placeholder: overWidget.name
})
if (newLabel === null) return
overWidget.label = newLabel || undefined
input.label = newLabel || undefined
useCanvasStore().canvas?.setDirty(true)
}
})
}
const favoritedWidgetsStore = useFavoritedWidgetsStore()
const isFavorited = favoritedWidgetsStore.isFavorited(
this,
overWidget.name
)
options.unshift({
content: isFavorited
? `${t('contextMenu.UnfavoriteWidget')}: ${overWidget.label ?? overWidget.name}`
: `${t('contextMenu.FavoriteWidget')}: ${overWidget.label ?? overWidget.name}`,
callback: () => {
favoritedWidgetsStore.toggleFavorite(this, overWidget.name)
}
})
if (this.graph && !this.graph.isRootGraph) {
addWidgetPromotionOptions(options, overWidget, this)
}
}
if (overWidget)
options.unshift(...getExtraOptionsForWidget(this, overWidget))
return []
}
}
@@ -934,6 +939,7 @@ export const useLitegraphService = () => {
addNodeOnGraph,
addNodeInput,
getCanvasCenter,
getExtraOptionsForWidget,
goToNode,
resetView,
fitView,