diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 24056a6..e1bb145 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -104,6 +104,8 @@ export class CanvasInteractions { // Add a blur event listener to the window to reset key states window.addEventListener('blur', this.onBlur); document.addEventListener('paste', this.onPaste); + // Intercept Ctrl+V during capture phase to handle layer paste before ComfyUI + document.addEventListener('keydown', this.onKeyDown, { capture: true }); this.canvas.canvas.addEventListener('mouseenter', this.onMouseEnter); this.canvas.canvas.addEventListener('mouseleave', this.onMouseLeave); this.canvas.canvas.addEventListener('dragover', this.onDragOver); @@ -119,6 +121,8 @@ export class CanvasInteractions { this.canvas.canvas.removeEventListener('wheel', this.onWheel); this.canvas.canvas.removeEventListener('keydown', this.onKeyDown); this.canvas.canvas.removeEventListener('keyup', this.onKeyUp); + // Remove document-level capture listener + document.removeEventListener('keydown', this.onKeyDown, { capture: true }); window.removeEventListener('blur', this.onBlur); document.removeEventListener('paste', this.onPaste); this.canvas.canvas.removeEventListener('mouseenter', this.onMouseEnter); @@ -570,6 +574,12 @@ export class CanvasInteractions { this.canvas.canvasLayers.copySelectedLayers(); } break; + case 'v': + // Paste layers from internal clipboard + if (this.canvas.canvasLayers.internalClipboard.length > 0) { + this.canvas.canvasLayers.pasteLayers(); + } + break; default: handled = false; break; diff --git a/js/CanvasLayersPanel.js b/js/CanvasLayersPanel.js index 61c2b3e..015c039 100644 --- a/js/CanvasLayersPanel.js +++ b/js/CanvasLayersPanel.js @@ -285,6 +285,8 @@ export class CanvasLayersPanel { if (nameElement && nameElement.classList.contains('editing')) { return; } + // Prevent the layers panel from stealing focus + e.preventDefault(); this.handleLayerClick(e, layer, index); }); // --- PRAWY PRZYCISK: ODJAZNACZ LAYER --- @@ -329,6 +331,8 @@ export class CanvasLayersPanel { // Aktualizuj tylko wygląd (klasy CSS), bez niszczenia DOM this.updateSelectionAppearance(); this.updateButtonStates(); + // Focus the canvas so keyboard shortcuts (like Ctrl+C/V) work for layer operations + this.canvas.canvas.focus(); log.debug(`Layer clicked: ${layer.name}, selection count: ${this.canvas.canvasSelection.selectedLayers.length}`); } startEditingLayerName(nameElement, layer) { diff --git a/js/CanvasView.js b/js/CanvasView.js index a785f91..9146b60 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -1076,6 +1076,8 @@ app.registerExtension({ }, async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeType.comfyClass === "LayerForgeNode") { + // Map to track pending copy sources across node ID changes + const pendingCopySources = new Map(); const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { log.debug("CanvasNode onNodeCreated: Base widget setup."); @@ -1106,6 +1108,38 @@ app.registerExtension({ log.info(`Registered CanvasNode instance for ID: ${this.id}`); // Store the canvas widget on the node this.canvasWidget = canvasWidget; + // Check if this node has a pending copy source (from onConfigure) + // Check both the current ID and -1 (temporary ID during paste) + let sourceNodeId = pendingCopySources.get(this.id); + if (!sourceNodeId) { + sourceNodeId = pendingCopySources.get(-1); + if (sourceNodeId) { + // Transfer from -1 to the real ID and clear -1 + pendingCopySources.delete(-1); + } + } + if (sourceNodeId && sourceNodeId !== this.id) { + log.info(`Node ${this.id} will copy canvas state from node ${sourceNodeId}`); + // Clear the flag + pendingCopySources.delete(this.id); + // Copy the canvas state now that the widget is initialized + setTimeout(async () => { + try { + const { getCanvasState, setCanvasState } = await import('./db.js'); + const sourceState = await getCanvasState(String(sourceNodeId)); + if (!sourceState) { + log.debug(`No canvas state found for source node ${sourceNodeId}`); + return; + } + await setCanvasState(String(this.id), sourceState); + await canvasWidget.canvas.loadInitialState(); + log.info(`Canvas state copied successfully from node ${sourceNodeId} to node ${this.id}`); + } + catch (error) { + log.error(`Error copying canvas state:`, error); + } + }, 100); + } // Check if there are already connected inputs setTimeout(() => { if (this.inputs && this.inputs.length > 0) { @@ -1271,6 +1305,31 @@ app.registerExtension({ } return onRemoved?.apply(this, arguments); }; + // Handle copy/paste - save canvas state when copying + const originalSerialize = nodeType.prototype.serialize; + nodeType.prototype.serialize = function () { + const data = originalSerialize ? originalSerialize.apply(this) : {}; + // Store a reference to the source node ID so we can copy layer data + data.sourceNodeId = this.id; + log.debug(`Serializing node ${this.id} for copy`); + return data; + }; + // Handle copy/paste - load canvas state from source node when pasting + const originalConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = async function (data) { + if (originalConfigure) { + originalConfigure.apply(this, [data]); + } + // Store the source node ID in the map (persists across node ID changes) + // This will be picked up later in onAdded when the canvas widget is ready + if (data.sourceNodeId && data.sourceNodeId !== this.id) { + const existingSource = pendingCopySources.get(this.id); + if (!existingSource) { + pendingCopySources.set(this.id, data.sourceNodeId); + log.debug(`Stored pending copy source: ${data.sourceNodeId} for node ${this.id}`); + } + } + }; const originalGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; nodeType.prototype.getExtraMenuOptions = function (_, options) { // FIRST: Call original to let other extensions add their options diff --git a/src/CanvasInteractions.ts b/src/CanvasInteractions.ts index 3476166..dc1c4b0 100644 --- a/src/CanvasInteractions.ts +++ b/src/CanvasInteractions.ts @@ -176,6 +176,9 @@ export class CanvasInteractions { document.addEventListener('paste', this.onPaste as unknown as EventListener); + // Intercept Ctrl+V during capture phase to handle layer paste before ComfyUI + document.addEventListener('keydown', this.onKeyDown as EventListener, { capture: true }); + this.canvas.canvas.addEventListener('mouseenter', this.onMouseEnter as EventListener); this.canvas.canvas.addEventListener('mouseleave', this.onMouseLeave as EventListener); @@ -195,6 +198,9 @@ export class CanvasInteractions { this.canvas.canvas.removeEventListener('keydown', this.onKeyDown as EventListener); this.canvas.canvas.removeEventListener('keyup', this.onKeyUp as EventListener); + // Remove document-level capture listener + document.removeEventListener('keydown', this.onKeyDown as EventListener, { capture: true }); + window.removeEventListener('blur', this.onBlur); document.removeEventListener('paste', this.onPaste as unknown as EventListener); @@ -697,6 +703,12 @@ export class CanvasInteractions { this.canvas.canvasLayers.copySelectedLayers(); } break; + case 'v': + // Paste layers from internal clipboard + if (this.canvas.canvasLayers.internalClipboard.length > 0) { + this.canvas.canvasLayers.pasteLayers(); + } + break; default: handled = false; break; diff --git a/src/CanvasLayersPanel.ts b/src/CanvasLayersPanel.ts index 2ee7280..41b4e70 100644 --- a/src/CanvasLayersPanel.ts +++ b/src/CanvasLayersPanel.ts @@ -336,6 +336,8 @@ export class CanvasLayersPanel { if (nameElement && nameElement.classList.contains('editing')) { return; } + // Prevent the layers panel from stealing focus + e.preventDefault(); this.handleLayerClick(e, layer, index); }); @@ -383,11 +385,14 @@ export class CanvasLayersPanel { // Aktualizuj wewnętrzny stan zaznaczenia w obiekcie canvas // Ta funkcja NIE powinna już wywoływać onSelectionChanged w panelu. this.canvas.updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index); - + // Aktualizuj tylko wygląd (klasy CSS), bez niszczenia DOM this.updateSelectionAppearance(); this.updateButtonStates(); + // Focus the canvas so keyboard shortcuts (like Ctrl+C/V) work for layer operations + this.canvas.canvas.focus(); + log.debug(`Layer clicked: ${layer.name}, selection count: ${this.canvas.canvasSelection.selectedLayers.length}`); } diff --git a/src/CanvasView.ts b/src/CanvasView.ts index 13e16c6..64bb863 100644 --- a/src/CanvasView.ts +++ b/src/CanvasView.ts @@ -1239,6 +1239,9 @@ app.registerExtension({ async beforeRegisterNodeDef(nodeType: any, nodeData: any, app: ComfyApp) { if (nodeType.comfyClass === "LayerForgeNode") { + // Map to track pending copy sources across node ID changes + const pendingCopySources = new Map(); + const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function (this: ComfyNode) { log.debug("CanvasNode onNodeCreated: Base widget setup."); @@ -1271,10 +1274,47 @@ app.registerExtension({ const canvasWidget = await createCanvasWidget(this, null, app); canvasNodeInstances.set(this.id, canvasWidget); log.info(`Registered CanvasNode instance for ID: ${this.id}`); - + // Store the canvas widget on the node (this as any).canvasWidget = canvasWidget; + // Check if this node has a pending copy source (from onConfigure) + // Check both the current ID and -1 (temporary ID during paste) + let sourceNodeId = pendingCopySources.get(this.id); + if (!sourceNodeId) { + sourceNodeId = pendingCopySources.get(-1); + if (sourceNodeId) { + // Transfer from -1 to the real ID and clear -1 + pendingCopySources.delete(-1); + } + } + + if (sourceNodeId && sourceNodeId !== this.id) { + log.info(`Node ${this.id} will copy canvas state from node ${sourceNodeId}`); + + // Clear the flag + pendingCopySources.delete(this.id); + + // Copy the canvas state now that the widget is initialized + setTimeout(async () => { + try { + const { getCanvasState, setCanvasState } = await import('./db.js'); + const sourceState = await getCanvasState(String(sourceNodeId)); + + if (!sourceState) { + log.debug(`No canvas state found for source node ${sourceNodeId}`); + return; + } + + await setCanvasState(String(this.id), sourceState); + await canvasWidget.canvas.loadInitialState(); + log.info(`Canvas state copied successfully from node ${sourceNodeId} to node ${this.id}`); + } catch (error) { + log.error(`Error copying canvas state:`, error); + } + }, 100); + } + // Check if there are already connected inputs setTimeout(() => { if (this.inputs && this.inputs.length > 0) { @@ -1458,6 +1498,36 @@ app.registerExtension({ return onRemoved?.apply(this, arguments as any); }; + // Handle copy/paste - save canvas state when copying + const originalSerialize = nodeType.prototype.serialize; + nodeType.prototype.serialize = function (this: ComfyNode) { + const data = originalSerialize ? originalSerialize.apply(this) : {}; + + // Store a reference to the source node ID so we can copy layer data + data.sourceNodeId = this.id; + log.debug(`Serializing node ${this.id} for copy`); + + return data; + }; + + // Handle copy/paste - load canvas state from source node when pasting + const originalConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = async function (this: ComfyNode, data: any) { + if (originalConfigure) { + originalConfigure.apply(this, [data]); + } + + // Store the source node ID in the map (persists across node ID changes) + // This will be picked up later in onAdded when the canvas widget is ready + if (data.sourceNodeId && data.sourceNodeId !== this.id) { + const existingSource = pendingCopySources.get(this.id); + if (!existingSource) { + pendingCopySources.set(this.id, data.sourceNodeId); + log.debug(`Stored pending copy source: ${data.sourceNodeId} for node ${this.id}`); + } + } + }; + const originalGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; nodeType.prototype.getExtraMenuOptions = function (this: ComfyNode, _: any, options: any[]) { // FIRST: Call original to let other extensions add their options