Skip to content

Commit af8433f

Browse files
authored
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)
1 parent cabd08f commit af8433f

5 files changed

Lines changed: 77 additions & 45 deletions

File tree

src/composables/graph/useMoreOptionsMenu.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { computed, ref } from 'vue'
22
import type { Ref } from 'vue'
33

4-
import type { LGraphGroup } from '@/lib/litegraph/src/litegraph'
4+
import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
55
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
6+
import { getExtraOptionsForWidget } from '@/services/litegraphService'
67
import { isLGraphGroup } from '@/utils/litegraphUtil'
78

89
import {
@@ -45,6 +46,8 @@ export enum BadgeVariant {
4546
// Global singleton for NodeOptions component reference
4647
let nodeOptionsInstance: null | NodeOptionsInstance = null
4748

49+
const hoveredWidgetName = ref<string>()
50+
4851
/**
4952
* Toggle the node options popover
5053
* @param event - The trigger event
@@ -61,6 +64,13 @@ export function toggleNodeOptions(event: Event) {
6164
* @param event - The trigger event (must be MouseEvent for position)
6265
*/
6366
export function showNodeOptions(event: MouseEvent) {
67+
hoveredWidgetName.value = undefined
68+
const target = event.target
69+
if (target instanceof HTMLElement) {
70+
const widgetEl = target.closest('.lg-node-widget')
71+
if (widgetEl instanceof HTMLElement)
72+
hoveredWidgetName.value = widgetEl.dataset.widgetName
73+
}
6474
if (nodeOptionsInstance?.show) {
6575
nodeOptionsInstance.show(event)
6676
}
@@ -133,8 +143,8 @@ export function useMoreOptionsMenu() {
133143
} = useGroupMenuOptions()
134144
const {
135145
getBasicSelectionOptions,
136-
getSubgraphOptions,
137-
getMultipleNodesOptions
146+
getMultipleNodesOptions,
147+
getSubgraphOptions
138148
} = useSelectionMenuOptions()
139149

140150
const hasSubgraphs = hasSubgraphsComputed
@@ -164,13 +174,13 @@ export function useMoreOptionsMenu() {
164174

165175
// For single node selection, also get LiteGraph menu items to merge
166176
const litegraphOptions: MenuOption[] = []
177+
const node: LGraphNode | undefined = selectedNodes.value[0]
167178
if (
168179
selectedNodes.value.length === 1 &&
169180
!groupContext &&
170181
canvasStore.canvas
171182
) {
172183
try {
173-
const node = selectedNodes.value[0]
174184
const rawItems = canvasStore.canvas.getNodeMenuOptions(node)
175185
// Don't apply structuring yet - we'll do it after merging with Vue options
176186
litegraphOptions.push(
@@ -249,6 +259,18 @@ export function useMoreOptionsMenu() {
249259
options.push(...getImageMenuOptions(selectedNodes.value[0]))
250260
options.push({ type: 'divider' })
251261
}
262+
const rawName = hoveredWidgetName.value
263+
const widget = node?.widgets?.find((w) => w.name === rawName)
264+
if (widget) {
265+
const widgetOptions = convertContextMenuToOptions(
266+
getExtraOptionsForWidget(node, widget)
267+
)
268+
if (widgetOptions) {
269+
options.push(...widgetOptions)
270+
options.push({ type: 'divider' })
271+
}
272+
}
273+
252274
// Section 6 & 7: Extensions and Delete are handled by buildStructuredMenu
253275

254276
// Mark all Vue options with source

src/core/graph/subgraph/proxyWidgetUtils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,15 @@ export function addWidgetPromotionOptions(
105105
content: `Promote Widget: ${widget.label ?? widget.name}`,
106106
callback: () => {
107107
promoteWidget(node, widget, promotableParents)
108+
widget.callback?.(widget.value)
108109
}
109110
})
110111
else {
111112
options.unshift({
112113
content: `Un-Promote Widget: ${widget.label ?? widget.name}`,
113114
callback: () => {
114115
demoteWidget(node, widget, parents)
116+
widget.callback?.(widget.value)
115117
}
116118
})
117119
}

src/renderer/extensions/vueNodes/components/NodeWidgets.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
(!widget.simplified.options?.advanced || showAdvanced)
3131
"
3232
class="lg-node-widget group col-span-full grid grid-cols-subgrid items-stretch"
33+
:data-widget-name="widget.name"
3334
>
3435
<!-- Widget Input Slot Dot -->
3536
<div

src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ function updateValueBy(delta: number) {
138138
const dragValue = ref<number>()
139139
const dragDelta = ref(0)
140140
function handleMouseDown(e: PointerEvent) {
141+
if (e.button > 0) return
141142
if (props.widget.options?.disabled) return
142143
const { target } = e
143144
if (!(target instanceof HTMLElement)) return

src/services/litegraphService.ts

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
ISerialisableNodeOutput,
3030
ISerialisedNode
3131
} from '@/lib/litegraph/src/types/serialisation'
32+
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
3233
import { useSettingStore } from '@/platform/settings/settingStore'
3334
import { useToastStore } from '@/platform/updates/common/toastStore'
3435
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
@@ -71,6 +72,49 @@ export interface HasInitialMinSize {
7172
export const CONFIG = Symbol()
7273
export const GET_CONFIG = Symbol()
7374

75+
export function getExtraOptionsForWidget(
76+
node: LGraphNode,
77+
widget: IBaseWidget
78+
) {
79+
const options: IContextMenuValue[] = []
80+
const input = node.inputs.find((inp) => inp.widget?.name === widget.name)
81+
82+
if (input) {
83+
options.unshift({
84+
content: `${t('contextMenu.RenameWidget')}: ${widget.label ?? widget.name}`,
85+
callback: async () => {
86+
const newLabel = await useDialogService().prompt({
87+
title: t('g.rename'),
88+
message: t('g.enterNewName') + ':',
89+
defaultValue: widget.label,
90+
placeholder: widget.name
91+
})
92+
if (newLabel === null) return
93+
widget.label = newLabel || undefined
94+
input.label = newLabel || undefined
95+
widget.callback?.(widget.value)
96+
useCanvasStore().canvas?.setDirty(true)
97+
}
98+
})
99+
}
100+
101+
const favoritedWidgetsStore = useFavoritedWidgetsStore()
102+
const isFavorited = favoritedWidgetsStore.isFavorited(node, widget.name)
103+
options.unshift({
104+
content: isFavorited
105+
? `${t('contextMenu.UnfavoriteWidget')}: ${widget.label ?? widget.name}`
106+
: `${t('contextMenu.FavoriteWidget')}: ${widget.label ?? widget.name}`,
107+
callback: () => {
108+
favoritedWidgetsStore.toggleFavorite(node, widget.name)
109+
}
110+
})
111+
112+
if (node.graph && !node.graph.isRootGraph) {
113+
addWidgetPromotionOptions(options, widget, node)
114+
}
115+
return options
116+
}
117+
74118
/**
75119
* Service that augments litegraph with ComfyUI specific functionality.
76120
*/
@@ -678,47 +722,8 @@ export const useLitegraphService = () => {
678722
}
679723
const [x, y] = canvas.graph_mouse
680724
const overWidget = this.getWidgetOnPos(x, y, true)
681-
if (overWidget) {
682-
const input = this.inputs.find(
683-
(inp) => inp.widget?.name === overWidget.name
684-
)
685-
686-
if (input) {
687-
options.unshift({
688-
content: `${t('contextMenu.RenameWidget')}: ${overWidget.label ?? overWidget.name}`,
689-
callback: async () => {
690-
const newLabel = await useDialogService().prompt({
691-
title: t('g.rename'),
692-
message: t('g.enterNewName') + ':',
693-
defaultValue: overWidget.label,
694-
placeholder: overWidget.name
695-
})
696-
if (newLabel === null) return
697-
overWidget.label = newLabel || undefined
698-
input.label = newLabel || undefined
699-
useCanvasStore().canvas?.setDirty(true)
700-
}
701-
})
702-
}
703-
704-
const favoritedWidgetsStore = useFavoritedWidgetsStore()
705-
const isFavorited = favoritedWidgetsStore.isFavorited(
706-
this,
707-
overWidget.name
708-
)
709-
options.unshift({
710-
content: isFavorited
711-
? `${t('contextMenu.UnfavoriteWidget')}: ${overWidget.label ?? overWidget.name}`
712-
: `${t('contextMenu.FavoriteWidget')}: ${overWidget.label ?? overWidget.name}`,
713-
callback: () => {
714-
favoritedWidgetsStore.toggleFavorite(this, overWidget.name)
715-
}
716-
})
717-
718-
if (this.graph && !this.graph.isRootGraph) {
719-
addWidgetPromotionOptions(options, overWidget, this)
720-
}
721-
}
725+
if (overWidget)
726+
options.unshift(...getExtraOptionsForWidget(this, overWidget))
722727
return []
723728
}
724729
}
@@ -934,6 +939,7 @@ export const useLitegraphService = () => {
934939
addNodeOnGraph,
935940
addNodeInput,
936941
getCanvasCenter,
942+
getExtraOptionsForWidget,
937943
goToNode,
938944
resetView,
939945
fitView,

0 commit comments

Comments
 (0)