mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-23 22:25:05 +00:00
feat: experimental flag to replace canvas Add Node menu with search box
Behind 'Comfy.NodeSearchBox.ReplaceCanvasMenu' (default off), the right-click canvas menu's 'Add Node' entry opens the V2 search box instead of the LiteGraph category submenu. The V2 search box already exposes subgraph blueprints, partner nodes, core nodes, and extensions, giving feature parity with the left-panel node library.
This commit is contained in:
@@ -146,6 +146,7 @@ import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||
import { useNodeBadge } from '@/composables/node/useNodeBadge'
|
||||
import { useCanvasDrop } from '@/composables/useCanvasDrop'
|
||||
import { useCanvasSearchBoxMenu } from '@/composables/useCanvasSearchBoxMenu'
|
||||
import { useContextMenuTranslation } from '@/composables/useContextMenuTranslation'
|
||||
import { useCopy } from '@/composables/useCopy'
|
||||
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
|
||||
@@ -459,6 +460,7 @@ useLitegraphSettings()
|
||||
useNodeBadge()
|
||||
|
||||
useGlobalLitegraph()
|
||||
useCanvasSearchBoxMenu()
|
||||
useContextMenuTranslation()
|
||||
useCopy()
|
||||
usePaste()
|
||||
|
||||
88
src/composables/useCanvasSearchBoxMenu.test.ts
Normal file
88
src/composables/useCanvasSearchBoxMenu.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useCanvasSearchBoxMenu } from '@/composables/useCanvasSearchBoxMenu'
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { createMockCanvas } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
describe('useCanvasSearchBoxMenu', () => {
|
||||
let originalGetCanvasMenuOptions: typeof LGraphCanvas.prototype.getCanvasMenuOptions
|
||||
let mockCanvas: LGraphCanvas
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
originalGetCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions
|
||||
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions =
|
||||
function (): (IContextMenuValue | null)[] {
|
||||
const items: (IContextMenuValue<string> | null)[] = [
|
||||
{
|
||||
content: 'Add Node',
|
||||
has_submenu: true,
|
||||
callback: LGraphCanvas.onMenuAdd
|
||||
},
|
||||
{ content: 'Add Group', callback: vi.fn() }
|
||||
]
|
||||
return items as (IContextMenuValue | null)[]
|
||||
}
|
||||
|
||||
mockCanvas = createMockCanvas({
|
||||
constructor: { prototype: LGraphCanvas.prototype } as typeof LGraphCanvas
|
||||
} as Partial<LGraphCanvas>)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = originalGetCanvasMenuOptions
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('leaves the default Add Node entry untouched when the setting is off', () => {
|
||||
vi.spyOn(useSettingStore(), 'get').mockImplementation((id) =>
|
||||
id === 'Comfy.NodeSearchBox.ReplaceCanvasMenu' ? false : undefined
|
||||
)
|
||||
|
||||
useCanvasSearchBoxMenu()
|
||||
const items = LGraphCanvas.prototype.getCanvasMenuOptions.call(mockCanvas)
|
||||
|
||||
const addNode = items.find((i) => i?.content === 'Add Node')
|
||||
expect(addNode?.callback).toBe(LGraphCanvas.onMenuAdd)
|
||||
expect(addNode?.has_submenu).toBe(true)
|
||||
})
|
||||
|
||||
it('replaces the Add Node callback with the search box trigger when the setting is on', () => {
|
||||
vi.spyOn(useSettingStore(), 'get').mockImplementation((id) =>
|
||||
id === 'Comfy.NodeSearchBox.ReplaceCanvasMenu' ? true : undefined
|
||||
)
|
||||
const toggleVisible = vi.spyOn(useSearchBoxStore(), 'toggleVisible')
|
||||
|
||||
useCanvasSearchBoxMenu()
|
||||
const items = LGraphCanvas.prototype.getCanvasMenuOptions.call(mockCanvas)
|
||||
const addNode = items.find((i) => i?.content === 'Add Node')
|
||||
|
||||
expect(addNode).toBeTruthy()
|
||||
expect(addNode?.has_submenu).toBe(false)
|
||||
expect(addNode?.callback).not.toBe(LGraphCanvas.onMenuAdd)
|
||||
|
||||
void addNode?.callback?.call(
|
||||
addNode as never,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined as never,
|
||||
undefined
|
||||
)
|
||||
expect(toggleVisible).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('preserves other canvas menu entries', () => {
|
||||
vi.spyOn(useSettingStore(), 'get').mockReturnValue(true)
|
||||
|
||||
useCanvasSearchBoxMenu()
|
||||
const items = LGraphCanvas.prototype.getCanvasMenuOptions.call(mockCanvas)
|
||||
|
||||
const contents = items.map((i) => i?.content)
|
||||
expect(contents).toEqual(['Add Node', 'Add Group'])
|
||||
})
|
||||
})
|
||||
67
src/composables/useCanvasSearchBoxMenu.ts
Normal file
67
src/composables/useCanvasSearchBoxMenu.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { legacyMenuCompat } from '@/lib/litegraph/src/contextMenuCompat'
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
|
||||
const REPLACE_SETTING_ID = 'Comfy.NodeSearchBox.ReplaceCanvasMenu'
|
||||
|
||||
/**
|
||||
* When the experimental "replace canvas menu" setting is enabled, the
|
||||
* right-click canvas menu's "Add Node" entry opens the Vue node search box
|
||||
* (which already includes blueprints, partner nodes, core nodes, and
|
||||
* extensions) instead of the legacy LiteGraph category submenu.
|
||||
*
|
||||
* The replacement is identified by callback identity against
|
||||
* {@link LGraphCanvas.onMenuAdd} so it remains stable across the translation
|
||||
* wrapper installed by {@link useContextMenuTranslation}.
|
||||
*/
|
||||
export const useCanvasSearchBoxMenu = () => {
|
||||
legacyMenuCompat.install(LGraphCanvas.prototype, 'getCanvasMenuOptions')
|
||||
|
||||
const originalGetCanvasMenuOptions =
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions
|
||||
|
||||
const wrapped = function (
|
||||
this: LGraphCanvas,
|
||||
...args: Parameters<typeof originalGetCanvasMenuOptions>
|
||||
) {
|
||||
const items = originalGetCanvasMenuOptions.apply(this, args)
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
if (!settingStore.get(REPLACE_SETTING_ID)) return items
|
||||
|
||||
return items.map((item) =>
|
||||
isLegacyAddNode(item) ? buildSearchBoxAddNode(item) : item
|
||||
)
|
||||
}
|
||||
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = wrapped
|
||||
legacyMenuCompat.registerWrapper(
|
||||
'getCanvasMenuOptions',
|
||||
wrapped,
|
||||
originalGetCanvasMenuOptions,
|
||||
LGraphCanvas.prototype
|
||||
)
|
||||
}
|
||||
|
||||
function isLegacyAddNode(
|
||||
item: IContextMenuValue | null
|
||||
): item is IContextMenuValue {
|
||||
return (
|
||||
!!item &&
|
||||
typeof item === 'object' &&
|
||||
item.callback === LGraphCanvas.onMenuAdd
|
||||
)
|
||||
}
|
||||
|
||||
function buildSearchBoxAddNode(original: IContextMenuValue): IContextMenuValue {
|
||||
return {
|
||||
...original,
|
||||
has_submenu: false,
|
||||
submenu: undefined,
|
||||
callback: () => {
|
||||
useSearchBoxStore().toggleVisible()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,6 +328,10 @@
|
||||
"name": "Show node frequency in search results",
|
||||
"tooltip": "Only applies to v1 (legacy)"
|
||||
},
|
||||
"Comfy_NodeSearchBox_ReplaceCanvasMenu": {
|
||||
"name": "Replace canvas right-click \"Add Node\" with search box",
|
||||
"tooltip": "When enabled, the right-click canvas menu opens the node search box instead of the LiteGraph category submenu. The search box includes blueprints, partner nodes, core nodes, and extensions."
|
||||
},
|
||||
"Comfy_NodeSuggestions_number": {
|
||||
"name": "Number of nodes suggestions",
|
||||
"tooltip": "Only for litegraph searchbox/context menu"
|
||||
|
||||
@@ -38,6 +38,17 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
options: ['default', 'v1 (legacy)', 'litegraph (legacy)'],
|
||||
defaultValue: 'default'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeSearchBox.ReplaceCanvasMenu',
|
||||
category: ['Comfy', 'Node Search Box', 'ReplaceCanvasMenu'],
|
||||
experimental: true,
|
||||
name: 'Replace canvas right-click "Add Node" with search box',
|
||||
tooltip:
|
||||
'When enabled, the right-click canvas menu opens the node search box instead of the LiteGraph category submenu. The search box includes blueprints, partner nodes, core nodes, and extensions.',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.46.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.LinkRelease.Action',
|
||||
category: ['LiteGraph', 'LinkRelease', 'Action'],
|
||||
|
||||
@@ -344,6 +344,7 @@ const zSettings = z.object({
|
||||
'Comfy.NodeSearchBoxImpl.ShowCategory': z.boolean(),
|
||||
'Comfy.NodeSearchBoxImpl.ShowIdName': z.boolean(),
|
||||
'Comfy.NodeSearchBoxImpl.ShowNodeFrequency': z.boolean(),
|
||||
'Comfy.NodeSearchBox.ReplaceCanvasMenu': z.boolean(),
|
||||
'Comfy.NodeSuggestions.number': z.number(),
|
||||
'Comfy.Node.BypassAllLinksOnDelete': z.boolean(),
|
||||
'Comfy.Node.Opacity': z.number(),
|
||||
|
||||
Reference in New Issue
Block a user