diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index 9eb5b9a3acd..cd41e673bc3 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -19,6 +19,7 @@ import { } from '@/lib/litegraph/src/litegraph' import type { Point } from '@/lib/litegraph/src/litegraph' import { useBillingContext } from '@/composables/billing/useBillingContext' +import { useCompactModeStore } from '@/stores/compactModeStore' import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog' import { createModelNodeFromAsset } from '@/platform/assets/utils/createModelNodeFromAsset' import { useSettingStore } from '@/platform/settings/settingStore' @@ -90,6 +91,7 @@ export function useCoreCommands(): ComfyCommand[] { const dialogStore = useDialogStore() const maskEditorStore = useMaskEditorStore() + const compactModeStore = useCompactModeStore() const { getSelectedNodes, toggleSelectedNodesMode } = useSelectedLiteGraphItems() @@ -106,6 +108,7 @@ export function useCoreCommands(): ComfyCommand[] { const moveSelectedNodes = ( positionUpdater: (pos: Point, gridSize: number) => Point ) => { + if (compactModeStore.isCompactMode) return const selectedNodes = getSelectedNodes() if (selectedNodes.length === 0) return @@ -594,6 +597,7 @@ export function useCoreCommands(): ComfyCommand[] { versionAdded: '1.3.7', category: 'essentials' as const, function: () => { + if (compactModeStore.isCompactMode) return const { canvas } = app if (!canvas.selectedItems?.size) { toastStore.add({ @@ -898,6 +902,7 @@ export function useCoreCommands(): ComfyCommand[] { icon: 'icon-[lucide--clipboard-paste]', label: 'Paste', function: () => { + if (compactModeStore.isCompactMode) return app.canvas.pasteFromClipboard() } }, @@ -906,6 +911,7 @@ export function useCoreCommands(): ComfyCommand[] { icon: 'icon-[lucide--clipboard-paste]', label: () => t('Paste with Connect'), function: () => { + if (compactModeStore.isCompactMode) return app.canvas.pasteFromClipboard({ connectInputs: true }) } }, @@ -923,6 +929,7 @@ export function useCoreCommands(): ComfyCommand[] { label: 'Delete Selected Items', versionAdded: '1.10.5', function: () => { + if (compactModeStore.isCompactMode) return if (app.canvas.selectedItems.size === 0) { app.canvas.canvas.dispatchEvent( new CustomEvent('litegraph:no-items-selected', { bubbles: true }) @@ -1034,6 +1041,7 @@ export function useCoreCommands(): ComfyCommand[] { versionAdded: '1.20.1', category: 'essentials' as const, function: () => { + if (compactModeStore.isCompactMode) return const canvas = canvasStore.getCanvas() const graph = canvas.subgraph ?? canvas.graph if (!graph) throw new TypeError('Canvas has no graph or subgraph set.') @@ -1349,6 +1357,35 @@ export function useCoreCommands(): ComfyCommand[] { if (newMode) useTelemetry()?.trackEnterLinear({ source }) canvasStore.linearMode = newMode } + }, + { + id: 'Comfy.ToggleCompactMode', + icon: 'pi pi-th-large', + label: 'Toggle Compact Mode', + category: 'view-controls' as const, + active: () => compactModeStore.isCompactMode, + function: async () => { + compactModeStore.toggle() + if (compactModeStore.isCompactMode) { + compactModeStore.savedLinkRenderMode = settingStore.get( + 'Comfy.LinkRenderMode' + ) + await settingStore.set('Comfy.LinkRenderMode', LiteGraph.HIDDEN_LINK) + return + } + const linksAreStillHidden = + settingStore.get('Comfy.LinkRenderMode') === LiteGraph.HIDDEN_LINK + if ( + linksAreStillHidden && + compactModeStore.savedLinkRenderMode != null + ) { + await settingStore.set( + 'Comfy.LinkRenderMode', + compactModeStore.savedLinkRenderMode + ) + } + compactModeStore.savedLinkRenderMode = null + } } ] diff --git a/src/composables/useWorkflowActionsMenu.test.ts b/src/composables/useWorkflowActionsMenu.test.ts index e935b14de3a..6aaa3376b22 100644 --- a/src/composables/useWorkflowActionsMenu.test.ts +++ b/src/composables/useWorkflowActionsMenu.test.ts @@ -61,6 +61,10 @@ const mockFeatureFlags = vi.hoisted(() => ({ flags: { linearToggleEnabled: false } })) +const mockSettingStore = vi.hoisted(() => ({ + get: vi.fn<(key: string) => unknown>(() => undefined) +})) + vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({ useWorkflowStore: vi.fn(() => mockWorkflowStore), useWorkflowBookmarkStore: vi.fn(() => mockBookmarkStore) @@ -92,6 +96,10 @@ vi.mock('@/composables/useFeatureFlags', () => ({ useFeatureFlags: vi.fn(() => mockFeatureFlags) })) +vi.mock('@/platform/settings/settingStore', () => ({ + useSettingStore: vi.fn(() => mockSettingStore) +})) + type MenuItems = ReturnType['menuItems']['value'] function actionItems(items: MenuItems): WorkflowMenuAction[] { @@ -120,6 +128,9 @@ describe('useWorkflowActionsMenu', () => { mockFeatureFlags.flags.linearToggleEnabled = false mockAppModeStore.selectedInputs.length = 0 mockAppModeStore.selectedOutputs.length = 0 + mockSettingStore.get.mockImplementation((key: string) => + key === 'Comfy.VueNodes.Enabled' ? true : undefined + ) mockWorkflowStore.activeWorkflow = { path: 'test.json', isPersisted: true @@ -375,6 +386,25 @@ describe('useWorkflowActionsMenu', () => { expect(bookmark.disabled).toBe(true) }) + it('shows the Compact Mode item when Vue Nodes is enabled', () => { + const { menuItems } = useWorkflowActionsMenu(vi.fn(), { isRoot: true }) + const labels = menuLabels(menuItems.value) + + expect(labels).toContain('breadcrumbsMenu.enterCompactMode') + }) + + it('hides the Compact Mode item when Vue Nodes is disabled', () => { + mockSettingStore.get.mockImplementation((key: string) => + key === 'Comfy.VueNodes.Enabled' ? false : undefined + ) + + const { menuItems } = useWorkflowActionsMenu(vi.fn(), { isRoot: true }) + const labels = menuLabels(menuItems.value) + + expect(labels).not.toContain('breadcrumbsMenu.enterCompactMode') + expect(labels).not.toContain('breadcrumbsMenu.exitCompactMode') + }) + it('switches to custom workflow before executing rename', async () => { const customWorkflow = ref({ path: 'other.json', diff --git a/src/composables/useWorkflowActionsMenu.ts b/src/composables/useWorkflowActionsMenu.ts index 6990bd0a1c8..254e7ee4ec8 100644 --- a/src/composables/useWorkflowActionsMenu.ts +++ b/src/composables/useWorkflowActionsMenu.ts @@ -12,7 +12,9 @@ import { useWorkflowBookmarkStore, useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' +import { useSettingStore } from '@/platform/settings/settingStore' import { useCommandStore } from '@/stores/commandStore' +import { useCompactModeStore } from '@/stores/compactModeStore' import { useMenuItemStore } from '@/stores/menuItemStore' import { useSubgraphStore } from '@/stores/subgraphStore' import { useAppModeStore } from '@/stores/appModeStore' @@ -56,6 +58,8 @@ export function useWorkflowActionsMenu( const { flags } = useFeatureFlags() const appModeStore = useAppModeStore() const { enterBuilder, pruneLinearData } = appModeStore + const compactModeStore = useCompactModeStore() + const settingStore = useSettingStore() const targetWorkflow = computed( () => workflow?.value ?? workflowStore.activeWorkflow @@ -220,6 +224,23 @@ export function useWorkflowActionsMenu( prependSeparator: true }) + const isVueNodesEnabled = + settingStore.get('Comfy.VueNodes.Enabled') ?? false + addItem({ + id: 'toggle-compact-mode', + label: compactModeStore.isCompactMode + ? t('breadcrumbsMenu.exitCompactMode') + : t('breadcrumbsMenu.enterCompactMode'), + icon: 'icon-[lucide--minimize-2]', + command: async () => { + await ensureWorkflowActive(targetWorkflow.value) + await commandStore.execute('Comfy.ToggleCompactMode') + }, + visible: !isLinearMode && isVueNodesEnabled, + prependSeparator: !showAppModeItems, + isNew: true + }) + const isActive = workflow === workflowStore.activeWorkflow const rawLd = isActive ? { diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 156c2fa1fa9..1fd36ddc2d1 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -1421,6 +1421,7 @@ "Canvas Performance": "Canvas Performance", "Help Center": "Help Center", "Toggle App Mode": "Toggle App Mode", + "Toggle Compact Mode": "Toggle Compact Mode", "Toggle Queue Panel V2": "Toggle Queue Panel V2", "Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)", "Undo": "Undo", @@ -2742,6 +2743,8 @@ "duplicate": "Duplicate", "enterAppMode": "Enter app mode", "exitAppMode": "Exit app mode", + "enterCompactMode": "Enter compact mode", + "exitCompactMode": "Exit compact mode", "enterBuilderMode": "Build app", "editBuilderMode": "Edit app", "workflowActions": "Workflow actions", diff --git a/src/renderer/extensions/vueNodes/components/ImagePreview.vue b/src/renderer/extensions/vueNodes/components/ImagePreview.vue index 5548f55d40b..5bcc967fc75 100644 --- a/src/renderer/extensions/vueNodes/components/ImagePreview.vue +++ b/src/renderer/extensions/vueNodes/components/ImagePreview.vue @@ -114,7 +114,7 @@
1 ? 'grid' : 'gallery' } +const compactModeStore = useCompactModeStore() const currentIndex = ref(0) const viewMode = ref(defaultViewMode(imageUrls)) const galleryPanelEl = ref() diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index 3933314f204..5010960b67c 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -34,7 +34,9 @@ }" :inert="isGhostPlacing" v-bind="remainingPointerHandlers" + @pointerdown.capture="onCompactCtrlClickCapture" @pointerdown="nodeOnPointerdown" + @click.capture="onCompactCtrlClickCapture" @wheel="handleWheel" @contextmenu="handleContextMenu" @dragover.prevent="handleDragOver" @@ -139,7 +141,20 @@
+
+
+
- + @@ -183,7 +202,7 @@ :image-url="latestPreviewUrl" />
{ return selectedNodeIds.value.has(nodeData.id) }) @@ -377,7 +402,13 @@ const showErrorsTabEnabled = computed(() => settingStore.get('Comfy.RightSidePanel.ShowErrorsTab') ) -const displayHeader = computed(() => nodeData.titleMode !== TitleMode.NO_TITLE) +const displayHeader = computed( + () => + !compactModeStore.isCompactMode && nodeData.titleMode !== TitleMode.NO_TITLE +) +const isOutputNode = computed( + () => !!lgraphNode.value?.constructor?.nodeData?.output_node +) const isRerouteNode = computed(() => nodeData.type === 'Reroute') @@ -422,7 +453,35 @@ const { onPointerdown, ...remainingPointerHandlers } = pointerHandlers const { startDrag } = useNodeDrag() const badges = usePartitionedBadges(nodeData) +function isCompactCtrlClickExecute(event: PointerEvent | MouseEvent): boolean { + return ( + compactModeStore.isCompactMode && + (event.ctrlKey || event.metaKey) && + (event as MouseEvent).button === 0 && + isOutputNode.value && + !!lgraphNode.value + ) +} + +async function onCompactCtrlClickCapture(event: PointerEvent | MouseEvent) { + if (!isCompactCtrlClickExecute(event)) return + event.preventDefault() + event.stopPropagation() + if (event.type !== 'pointerdown') return + if (!lgraphNode.value) return + app.canvas.select(lgraphNode.value) + canvasStore.updateSelectedItems() + await commandStore.execute('Comfy.QueueSelectedOutputNodes', { + metadata: { trigger_source: 'compact_mode_ctrl_click' } + }) +} + async function nodeOnPointerdown(event: PointerEvent) { + if (isCompactCtrlClickExecute(event)) return + if (compactModeStore.isCompactMode) { + onPointerdown(event) + return + } if (event.altKey && lgraphNode.value) { const result = LGraphCanvas.cloneNodes([lgraphNode.value]) if (result?.created?.length) { @@ -442,6 +501,8 @@ const handleContextMenu = (event: MouseEvent) => { event.preventDefault() event.stopPropagation() + if (compactModeStore.isCompactMode) return + // First handle the standard right-click behavior (selection) handleNodeRightClick(event as PointerEvent, nodeData.id) @@ -529,6 +590,7 @@ const handleResizePointerDown = ( ) => { if (event.button !== 0) return if (!shouldHandleNodePointerEvents.value) return + if (compactModeStore.isCompactMode) return if (nodeData.flags?.pinned) return if (nodeData.resizable === false) return startResize(event, corner) @@ -571,6 +633,16 @@ const cursorClass = computed(() => { }) const bodyRoundingClass = computed(() => { + if (compactModeStore.isCompactMode) { + switch (nodeData.shape) { + case RenderShape.BOX: + return '' + case RenderShape.CARD: + return 'rounded-tl-2xl rounded-br-2xl' + default: + return 'rounded-2xl' + } + } switch (nodeData.shape) { case RenderShape.BOX: return '' diff --git a/src/renderer/extensions/vueNodes/components/NodeFooter.test.ts b/src/renderer/extensions/vueNodes/components/NodeFooter.test.ts index 5bebf4352ff..e7fafceb535 100644 --- a/src/renderer/extensions/vueNodes/components/NodeFooter.test.ts +++ b/src/renderer/extensions/vueNodes/components/NodeFooter.test.ts @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/vue' import userEvent from '@testing-library/user-event' +import { createPinia } from 'pinia' import { beforeEach, describe, expect, it } from 'vitest' import { createI18n } from 'vue-i18n' @@ -44,7 +45,7 @@ const baseProps: Props = { function renderFooter(overrides: Partial = {}) { return render(NodeFooter, { - global: { plugins: [i18n] }, + global: { plugins: [i18n, createPinia()] }, props: { ...baseProps, ...overrides } }) } diff --git a/src/renderer/extensions/vueNodes/components/NodeFooter.vue b/src/renderer/extensions/vueNodes/components/NodeFooter.vue index c5c963f2968..e24c8bf3a1e 100644 --- a/src/renderer/extensions/vueNodes/components/NodeFooter.vue +++ b/src/renderer/extensions/vueNodes/components/NodeFooter.vue @@ -1,7 +1,12 @@