[New Feature] Selection Toolbox (#2608)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Chenlei Hu
2025-02-17 19:07:49 -05:00
committed by GitHub
parent f7556e0015
commit 79db202925
29 changed files with 220 additions and 15 deletions

View File

@@ -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<NodeReference[]> {
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<NodeReference | null> {
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,

View File

@@ -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()
})
})

8
package-lock.json generated
View File

@@ -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": {

View File

@@ -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",

View File

@@ -28,9 +28,8 @@
class="w-full h-full touch-none"
/>
<NodeSearchboxPopover />
<SelectionOverlay>
<!-- Placeholder for selection overlay testing. -->
<!-- <div class="w-full h-full bg-red-500"></div> -->
<SelectionOverlay v-if="selectionToolboxEnabled">
<SelectionToolbox />
</SelectionOverlay>
<NodeTooltip v-if="tooltipEnabled" />
<NodeBadge />
@@ -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'

View File

@@ -2,6 +2,9 @@
<template>
<div
class="selection-overlay-container pointer-events-none"
:class="{
'show-border': showBorder
}"
:style="style"
v-show="visible"
>
@@ -22,9 +25,12 @@ const canvasStore = useCanvasStore()
const { style, updatePosition } = useAbsolutePosition()
const visible = ref(false)
const showBorder = ref(false)
const positionSelectionOverlay = (canvas: LGraphCanvas) => {
const selectedItems = canvas.selectedItems
showBorder.value = selectedItems.size > 1
if (!selectedItems.size) {
visible.value = false
return
@@ -82,4 +88,8 @@ watch(
.selection-overlay-container > * {
pointer-events: auto;
}
.show-border {
@apply border-dashed rounded-md border-2 border-[var(--border-color)];
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<Panel
class="selection-toolbox absolute left-1/2"
:pt="{
header: 'hidden',
content: 'p-0 flex flex-row'
}"
>
<Button
severity="secondary"
text
icon="pi pi-thumbtack"
@click="() => commandStore.execute('Comfy.Canvas.ToggleSelected.Pin')"
/>
<Button
severity="danger"
text
icon="pi pi-trash"
@click="() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')"
/>
</Panel>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Panel from 'primevue/panel'
import { useCommandStore } from '@/stores/commandStore'
const commandStore = useCommandStore()
</script>
<style scoped>
.selection-toolbox {
transform: translateX(-50%) translateY(-120%);
}
</style>

View File

@@ -399,6 +399,7 @@ export function useCoreCommands(): ComfyCommand[] {
item.pin(!item.pinned)
}
}
app.canvas.setDirty(true, true)
}
},
{

View File

@@ -747,5 +747,13 @@ export const CORE_SETTINGS: SettingParams[] = [
},
defaultValue: 0.6,
versionAdded: '1.9.1'
},
{
id: 'Comfy.Canvas.SelectionToolbox',
category: ['LiteGraph', 'Canvas', 'SelectionToolbox'],
name: 'Show selection toolbox',
type: 'boolean',
defaultValue: true,
versionAdded: '1.10.5'
}
]

View File

@@ -35,6 +35,9 @@
"Comfy_BrowseTemplates": {
"label": "Browse Templates"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Delete Selected Items"
},
"Comfy_Canvas_FitView": {
"label": "Fit view to selected nodes"
},

View File

@@ -473,6 +473,7 @@
"Reinstall": "Reinstall",
"Restart": "Restart",
"Browse Templates": "Browse Templates",
"Delete Selected Items": "Delete Selected Items",
"Fit view to selected nodes": "Fit view to selected nodes",
"Reset View": "Reset View",
"Canvas Toggle Link Visibility": "Canvas Toggle Link Visibility",

View File

@@ -25,6 +25,9 @@
"custom": "custom"
}
},
"Comfy_Canvas_SelectionToolbox": {
"name": "Show selection toolbox"
},
"Comfy_ConfirmClear": {
"name": "Require confirmation when clearing workflow"
},

View File

@@ -35,6 +35,9 @@
"Comfy_BrowseTemplates": {
"label": "Parcourir les modèles"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Supprimer les éléments sélectionnés"
},
"Comfy_Canvas_FitView": {
"label": "Ajuster la vue aux nœuds sélectionnés"
},

View File

@@ -378,6 +378,7 @@
"ComfyUI Forum": "Forum ComfyUI",
"ComfyUI Issues": "Problèmes de ComfyUI",
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
"Delete Selected Items": "Supprimer les éléments sélectionnés",
"Desktop User Guide": "Guide de l'utilisateur de bureau",
"Duplicate Current Workflow": "Dupliquer le flux de travail actuel",
"Edit": "Éditer",

View File

@@ -25,6 +25,9 @@
},
"tooltip": "Choisissez l'option personnalisée pour masquer la barre de titre du système"
},
"Comfy_Canvas_SelectionToolbox": {
"name": "Afficher la boîte à outils de sélection"
},
"Comfy_ConfirmClear": {
"name": "Demander une confirmation lors de l'effacement du flux de travail"
},

View File

@@ -35,6 +35,9 @@
"Comfy_BrowseTemplates": {
"label": "テンプレートを参照"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "選択したアイテムを削除"
},
"Comfy_Canvas_FitView": {
"label": "選択したノードにビューを合わせる"
},

View File

@@ -378,6 +378,7 @@
"ComfyUI Forum": "ComfyUI フォーラム",
"ComfyUI Issues": "ComfyUIの問題",
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
"Delete Selected Items": "選択したアイテムを削除",
"Desktop User Guide": "デスクトップユーザーガイド",
"Duplicate Current Workflow": "現在のワークフローを複製",
"Edit": "編集",

View File

@@ -25,6 +25,9 @@
},
"tooltip": "システムタイトルバーを非表示にするにはカスタムオプションを選択してください"
},
"Comfy_Canvas_SelectionToolbox": {
"name": "選択ツールボックスを表示"
},
"Comfy_ConfirmClear": {
"name": "ワークフローをクリアする際に確認を要求する"
},

View File

@@ -35,6 +35,9 @@
"Comfy_BrowseTemplates": {
"label": "템플릿 탐색"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "선택한 항목 삭제"
},
"Comfy_Canvas_FitView": {
"label": "선택한 노드에 뷰 맞추기"
},

View File

@@ -378,6 +378,7 @@
"ComfyUI Forum": "ComfyUI 포럼",
"ComfyUI Issues": "ComfyUI 이슈 페이지",
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
"Delete Selected Items": "선택한 항목 삭제",
"Desktop User Guide": "데스크톱 사용자 가이드",
"Duplicate Current Workflow": "현재 워크플로 복제",
"Edit": "편집",

View File

@@ -25,6 +25,9 @@
},
"tooltip": "시스템 제목 표시 줄을 숨기려면 사용자 정의 옵션을 선택하세요"
},
"Comfy_Canvas_SelectionToolbox": {
"name": "선택 도구 상자 표시"
},
"Comfy_ConfirmClear": {
"name": "워크플로 비우기 시 확인 요구"
},

View File

@@ -35,6 +35,9 @@
"Comfy_BrowseTemplates": {
"label": "Просмотр шаблонов"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Удалить выбранные элементы"
},
"Comfy_Canvas_FitView": {
"label": "Подогнать вид к выбранным нодам"
},

View File

@@ -378,6 +378,7 @@
"ComfyUI Forum": "Форум ComfyUI",
"ComfyUI Issues": "Проблемы ComfyUI",
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
"Delete Selected Items": "Удалить выбранные элементы",
"Desktop User Guide": "Руководство пользователя для настольных ПК",
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
"Edit": "Редактировать",

View File

@@ -25,6 +25,9 @@
},
"tooltip": "Выберите пользовательский вариант, чтобы скрыть системную строку заголовка"
},
"Comfy_Canvas_SelectionToolbox": {
"name": "Показать панель инструментов выбора"
},
"Comfy_ConfirmClear": {
"name": "Требовать подтверждение при очистке рабочего процесса"
},

View File

@@ -35,6 +35,9 @@
"Comfy_BrowseTemplates": {
"label": "浏览模板"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "删除选定的项目"
},
"Comfy_Canvas_FitView": {
"label": "适应视图到选中节点"
},

View File

@@ -378,6 +378,7 @@
"ComfyUI Forum": "ComfyUI 论坛",
"ComfyUI Issues": "ComfyUI 问题",
"Convert selected nodes to group node": "将选中节点转换为组节点",
"Delete Selected Items": "删除选定的项目",
"Desktop User Guide": "桌面端用户指南",
"Duplicate Current Workflow": "复制当前工作流",
"Edit": "编辑",

View File

@@ -25,6 +25,9 @@
},
"tooltip": "选择自定义选项以隐藏系统标题栏"
},
"Comfy_Canvas_SelectionToolbox": {
"name": "显示选择工具箱"
},
"Comfy_ConfirmClear": {
"name": "清除工作流时需要确认"
},

View File

@@ -1,6 +1,7 @@
import { LGraphCanvas, LGraphGroup, LGraphNode } from '@comfyorg/litegraph'
import type { LGraphCanvas, LGraphGroup, LGraphNode } from '@comfyorg/litegraph'
import type { Positionable } from '@comfyorg/litegraph/dist/interfaces'
import { defineStore } from 'pinia'
import { shallowRef } from 'vue'
import { markRaw, ref, shallowRef } from 'vue'
export const useTitleEditorStore = defineStore('titleEditor', () => {
const titleEditorTarget = shallowRef<LGraphNode | LGraphGroup | null>(null)
@@ -17,8 +18,18 @@ export const useCanvasStore = defineStore('canvas', () => {
* The root LGraphCanvas object is shallow reactive.
*/
const canvas = shallowRef<LGraphCanvas | null>(null)
/**
* The selected items on the canvas. All stored items are raw.
*/
const selectedItems = ref<Positionable[]>([])
const updateSelectedItems = () => {
const items = Array.from(canvas.value?.selectedItems ?? [])
selectedItems.value = items.map((item) => markRaw(item))
}
return {
canvas
canvas,
selectedItems,
updateSelectedItems
}
})

View File

@@ -591,7 +591,8 @@ const zSettings = z.record(z.any()).and(
'LiteGraph.Canvas.MaximumFps': z.number(),
'Comfy.Workflow.ConfirmDelete': z.boolean(),
'Comfy.RerouteBeta': z.boolean(),
'LiteGraph.Canvas.LowQualityRenderingZoomThreshold': z.number()
'LiteGraph.Canvas.LowQualityRenderingZoomThreshold': z.number(),
'Comfy.Canvas.SelectionToolbox': z.boolean()
})
.optional()
)