diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index 5553ac342..301bc3348 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -1,3 +1,4 @@ +import type { LGraphNode } from '@comfyorg/litegraph' import type { APIRequestContext, Locator, Page } from '@playwright/test' import { expect } from '@playwright/test' import { test as base } from '@playwright/test' @@ -646,6 +647,18 @@ export class ComfyPage { await this.nextFrame() } + async selectNodes(nodeTitles: string[]) { + await this.page.keyboard.down('Control') + for (const nodeTitle of nodeTitles) { + const nodes = await this.getNodeRefsByTitle(nodeTitle) + for (const node of nodes) { + await node.click('title') + } + } + await this.page.keyboard.up('Control') + await this.nextFrame() + } + async select2Nodes() { // Select 2 CLIP nodes. await this.page.keyboard.down('Control') @@ -835,12 +848,24 @@ export class ComfyPage { ( await this.page.evaluate((type) => { return window['app'].graph.nodes - .filter((n) => n.type === type) - .map((n) => n.id) + .filter((n: LGraphNode) => n.type === type) + .map((n: LGraphNode) => n.id) }, type) ).map((id: NodeId) => this.getNodeRefById(id)) ) } + async getNodeRefsByTitle(title: string): Promise { + return Promise.all( + ( + await this.page.evaluate((title) => { + return window['app'].graph.nodes + .filter((n: LGraphNode) => n.title === title) + .map((n: LGraphNode) => n.id) + }, title) + ).map((id: NodeId) => this.getNodeRefById(id)) + ) + } + async getFirstNodeRef(): Promise { const id = await this.page.evaluate(() => { return window['app'].graph.nodes[0]?.id @@ -896,9 +921,10 @@ export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({ try { await comfyPage.setupSettings({ 'Comfy.UseNewMenu': 'Disabled', - // Hide canvas menu/info by default. + // Hide canvas menu/info/selection toolbox by default. 'Comfy.Graph.CanvasInfo': false, 'Comfy.Graph.CanvasMenu': false, + 'Comfy.Canvas.SelectionToolbox': false, // Hide all badges by default. 'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None, 'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None, diff --git a/browser_tests/selectionToolbox.spec.ts b/browser_tests/selectionToolbox.spec.ts new file mode 100644 index 000000000..db47f9bd1 --- /dev/null +++ b/browser_tests/selectionToolbox.spec.ts @@ -0,0 +1,60 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture } from './fixtures/ComfyPage' + +const test = comfyPageFixture + +test.describe('Selection Toolbox', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) + }) + + test('shows/hides selection toolbox based on setting', async ({ + comfyPage + }) => { + // By default, selection toolbox should be enabled + expect( + await comfyPage.page.locator('.selection-overlay-container').isVisible() + ).toBe(false) + + // Select multiple nodes + await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) + + // Selection toolbox should be visible with multiple nodes selected + await expect( + comfyPage.page.locator('.selection-overlay-container') + ).toBeVisible() + await expect( + comfyPage.page.locator('.selection-overlay-container.show-border') + ).toBeVisible() + }) + + test('shows border only with multiple selections', async ({ comfyPage }) => { + // Select single node + await comfyPage.selectNodes(['KSampler']) + + // Selection overlay should be visible but without border + await expect( + comfyPage.page.locator('.selection-overlay-container') + ).toBeVisible() + await expect( + comfyPage.page.locator('.selection-overlay-container.show-border') + ).not.toBeVisible() + + // Select multiple nodes + await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) + + // Selection overlay should show border with multiple selections + await expect( + comfyPage.page.locator('.selection-overlay-container.show-border') + ).toBeVisible() + + // Deselect to single node + await comfyPage.selectNodes(['CLIP Text Encode (Prompt)']) + + // Border should be hidden again + await expect( + comfyPage.page.locator('.selection-overlay-container.show-border') + ).not.toBeVisible() + }) +}) diff --git a/package-lock.json b/package-lock.json index cfe6e163c..81f0a79a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.3.1", "@comfyorg/comfyui-electron-types": "^0.4.16", - "@comfyorg/litegraph": "^0.8.83", + "@comfyorg/litegraph": "^0.8.84", "@primevue/forms": "^4.2.5", "@primevue/themes": "^4.2.5", "@sentry/vue": "^8.48.0", @@ -1944,9 +1944,9 @@ "license": "GPL-3.0-only" }, "node_modules/@comfyorg/litegraph": { - "version": "0.8.83", - "resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.83.tgz", - "integrity": "sha512-4ZfRk0mBcCStY2yRERCrguwFf5v6WajD/6/JEmycD3HnF4OwYgyAspMYrscJcQ/R2MXfnedGe1gi8WXQ955vEQ==", + "version": "0.8.84", + "resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.84.tgz", + "integrity": "sha512-NjyWpBsccgFsNn81pMz8MA2n6Y6FS5Aw8sOWO7btHh/BvO+wFykLc9sal4bISyUFmVq2KDkoGDQUPsM46zad/g==", "license": "MIT" }, "node_modules/@cspotcode/source-map-support": { diff --git a/package.json b/package.json index 291251087..89b734e4c 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.3.1", "@comfyorg/comfyui-electron-types": "^0.4.16", - "@comfyorg/litegraph": "^0.8.83", + "@comfyorg/litegraph": "^0.8.84", "@primevue/forms": "^4.2.5", "@primevue/themes": "^4.2.5", "@sentry/vue": "^8.48.0", diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index e568b0bfb..5961070d5 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -28,9 +28,8 @@ class="w-full h-full touch-none" /> - - - + + @@ -45,10 +44,12 @@ import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue' import NodeBadge from '@/components/graph/NodeBadge.vue' import NodeTooltip from '@/components/graph/NodeTooltip.vue' import SelectionOverlay from '@/components/graph/SelectionOverlay.vue' +import SelectionToolbox from '@/components/graph/SelectionToolbox.vue' import TitleEditor from '@/components/graph/TitleEditor.vue' import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue' import SideToolbar from '@/components/sidebar/SideToolbar.vue' import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue' +import { useChainCallback } from '@/composables/functional/useChainCallback' import { useCanvasDrop } from '@/composables/useCanvasDrop' import { useContextMenuTranslation } from '@/composables/useContextMenuTranslation' import { useCopy } from '@/composables/useCopy' @@ -87,6 +88,9 @@ const canvasMenuEnabled = computed(() => settingStore.get('Comfy.Graph.CanvasMenu') ) const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips')) +const selectionToolboxEnabled = computed(() => + settingStore.get('Comfy.Canvas.SelectionToolbox') +) watchEffect(() => { nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated') @@ -192,6 +196,11 @@ onMounted(async () => { comfyAppReady.value = true + comfyApp.canvas.onSelectionChange = useChainCallback( + comfyApp.canvas.onSelectionChange, + () => canvasStore.updateSelectedItems() + ) + // Load color palette colorPaletteStore.customPalettes = settingStore.get( 'Comfy.CustomColorPalettes' diff --git a/src/components/graph/SelectionOverlay.vue b/src/components/graph/SelectionOverlay.vue index 0b9dc1c51..4a75e1599 100644 --- a/src/components/graph/SelectionOverlay.vue +++ b/src/components/graph/SelectionOverlay.vue @@ -2,6 +2,9 @@