From f4ca285d0737ee4fd0f8ced8aaf6f4b3003b190a Mon Sep 17 00:00:00 2001 From: Dante Date: Fri, 20 Feb 2026 19:34:45 +0900 Subject: [PATCH] feat: add Copy, Paste, Select All commands to Edit menu (#8954) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add Copy, Paste, and Select All commands to the Edit menu for mobile/touch users and accessibility - Menu-based copy uses LiteGraph internal clipboard; existing Ctrl+C/V behavior is unchanged ## Changes - `useCoreCommands.ts`: Register three new commands (`CopySelected`, `PasteFromClipboard`, `SelectAll`) - `coreMenuCommands.ts`: Add menu entries under Edit (between Undo/Redo and Clear Workflow) - `useCoreCommands.test.ts`: Add unit tests for the new commands ### AS IS 스크린샷 2026-02-18 오후 5 44 14 ### TO BE 스크린샷 2026-02-19 오후 5 07 28 ## Test plan - [x] Verify Copy/Paste/Select All appear in Edit menu - [x] Select nodes → Edit > Copy → Edit > Paste → nodes duplicated - [x] Edit > Select All → all canvas items selected - [x] Copy with no selection → no-op (no error) - [x] Existing Ctrl+C/V keyboard shortcuts still work Fixes #2892 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8954-feat-add-Copy-Paste-Select-All-commands-to-Edit-menu-30b6d73d365081ec9270ed2a562eaf0b) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 --- src/composables/useCoreCommands.test.ts | 53 ++++++++++++++++++++++++- src/composables/useCoreCommands.ts | 26 ++++++++++++ src/constants/coreMenuCommands.ts | 8 ++++ 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/composables/useCoreCommands.test.ts b/src/composables/useCoreCommands.test.ts index 925b0aac4..f5df599ae 100644 --- a/src/composables/useCoreCommands.test.ts +++ b/src/composables/useCoreCommands.test.ts @@ -23,7 +23,13 @@ vi.mock('vue-i18n', async () => { vi.mock('@/scripts/app', () => { const mockGraphClear = vi.fn() - const mockCanvas = { subgraph: undefined } + const mockCanvas = { + subgraph: undefined, + selectedItems: new Set(), + copyToClipboard: vi.fn(), + pasteFromClipboard: vi.fn(), + selectItems: vi.fn() + } return { app: { @@ -105,7 +111,8 @@ vi.mock('@/stores/subgraphStore', () => ({ vi.mock('@/renderer/core/canvas/canvasStore', () => ({ useCanvasStore: vi.fn(() => ({ - getCanvas: () => app.canvas + getCanvas: () => app.canvas, + canvas: app.canvas })), useTitleEditorStore: vi.fn(() => ({ titleEditorTarget: null @@ -300,6 +307,48 @@ describe('useCoreCommands', () => { }) }) + describe('Canvas clipboard commands', () => { + function findCommand(id: string) { + return useCoreCommands().find((cmd) => cmd.id === id)! + } + + beforeEach(() => { + app.canvas.selectedItems = new Set() + vi.mocked(app.canvas.copyToClipboard).mockClear() + vi.mocked(app.canvas.pasteFromClipboard).mockClear() + vi.mocked(app.canvas.selectItems).mockClear() + }) + + it('should copy selected items when selection exists', async () => { + app.canvas.selectedItems = new Set([ + {} + ]) as typeof app.canvas.selectedItems + + await findCommand('Comfy.Canvas.CopySelected').function() + + expect(app.canvas.copyToClipboard).toHaveBeenCalledWith() + }) + + it('should not copy when no items are selected', async () => { + await findCommand('Comfy.Canvas.CopySelected').function() + + expect(app.canvas.copyToClipboard).not.toHaveBeenCalled() + }) + + it('should paste from clipboard', async () => { + await findCommand('Comfy.Canvas.PasteFromClipboard').function() + + expect(app.canvas.pasteFromClipboard).toHaveBeenCalledWith() + }) + + it('should select all items', async () => { + await findCommand('Comfy.Canvas.SelectAll').function() + + // No arguments means "select all items on canvas" + expect(app.canvas.selectItems).toHaveBeenCalledWith() + }) + }) + describe('Subgraph metadata commands', () => { beforeEach(() => { mockSubgraph.extra = {} diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index 347aada4c..caab6316a 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -884,6 +884,32 @@ export function useCoreCommands(): ComfyCommand[] { window.open(staticUrls.forum, '_blank') } }, + { + id: 'Comfy.Canvas.CopySelected', + icon: 'icon-[lucide--copy]', + label: 'Copy', + function: () => { + if (app.canvas.selectedItems?.size) { + app.canvas.copyToClipboard() + } + } + }, + { + id: 'Comfy.Canvas.PasteFromClipboard', + icon: 'icon-[lucide--clipboard-paste]', + label: 'Paste', + function: () => { + app.canvas.pasteFromClipboard() + } + }, + { + id: 'Comfy.Canvas.SelectAll', + icon: 'icon-[lucide--lasso-select]', + label: 'Select All', + function: () => { + app.canvas.selectItems() + } + }, { id: 'Comfy.Canvas.DeleteSelectedItems', icon: 'pi pi-trash', diff --git a/src/constants/coreMenuCommands.ts b/src/constants/coreMenuCommands.ts index 6444b8309..1b1c52cbc 100644 --- a/src/constants/coreMenuCommands.ts +++ b/src/constants/coreMenuCommands.ts @@ -14,6 +14,14 @@ export const CORE_MENU_COMMANDS = [ ] ], [['Edit'], ['Comfy.Undo', 'Comfy.Redo']], + [ + ['Edit'], + [ + 'Comfy.Canvas.CopySelected', + 'Comfy.Canvas.PasteFromClipboard', + 'Comfy.Canvas.SelectAll' + ] + ], [['Edit'], ['Comfy.ClearWorkflow']], [['Edit'], ['Comfy.OpenClipspace']], [