diff --git a/web/scripts/app.js b/web/scripts/app.js index 3b7483cd..b5114604 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -667,11 +667,40 @@ export class ComfyApp { } /** - * Adds a handler on paste that extracts and loads workflows from pasted JSON data + * Adds a handler on paste that extracts and loads images or workflows from pasted JSON data */ #addPasteHandler() { document.addEventListener("paste", (e) => { - let data = (e.clipboardData || window.clipboardData).getData("text/plain"); + let data = (e.clipboardData || window.clipboardData); + const items = data.items; + + // Look for image paste data + for (const item of items) { + if (item.type.startsWith('image/')) { + var imageNode = null; + + // If an image node is selected, paste into it + if (this.canvas.current_node && + this.canvas.current_node.is_selected && + ComfyApp.isImageNode(this.canvas.current_node)) { + imageNode = this.canvas.current_node; + } + + // No image node selected: add a new one + if (!imageNode) { + const newNode = LiteGraph.createNode("LoadImage"); + newNode.pos = [...this.canvas.graph_mouse]; + imageNode = this.graph.add(newNode); + this.graph.change(); + } + const blob = item.getAsFile(); + imageNode.pasteFile(blob); + return; + } + } + + // No image found. Look for node data + data = data.getData("text/plain"); let workflow; try { data = data.slice(data.indexOf("{")); @@ -687,9 +716,29 @@ export class ComfyApp { if (workflow && workflow.version && workflow.nodes && workflow.extra) { this.loadGraphData(workflow); } + else { + // Litegraph default paste + this.canvas.pasteFromClipboard(); + } + + }); } + + /** + * Adds a handler on copy that serializes selected nodes to JSON + */ + #addCopyHandler() { + document.addEventListener("copy", (e) => { + // copy + if (this.canvas.selected_nodes) { + this.canvas.copyToClipboard(); + } + }); + } + + /** * Handle mouse * @@ -745,12 +794,6 @@ export class ComfyApp { const self = this; const origProcessKey = LGraphCanvas.prototype.processKey; LGraphCanvas.prototype.processKey = function(e) { - const res = origProcessKey.apply(this, arguments); - - if (res === false) { - return res; - } - if (!this.graph) { return; } @@ -761,9 +804,10 @@ export class ComfyApp { return; } - if (e.type == "keydown") { + if (e.type == "keydown" && !e.repeat) { + // Ctrl + M mute/unmute - if (e.keyCode == 77 && e.ctrlKey) { + if (e.key === 'm' && e.ctrlKey) { if (this.selected_nodes) { for (var i in this.selected_nodes) { if (this.selected_nodes[i].mode === 2) { // never @@ -776,7 +820,8 @@ export class ComfyApp { block_default = true; } - if (e.keyCode == 66 && e.ctrlKey) { + // Ctrl + B bypass + if (e.key === 'b' && e.ctrlKey) { if (this.selected_nodes) { for (var i in this.selected_nodes) { if (this.selected_nodes[i].mode === 4) { // never @@ -788,6 +833,28 @@ export class ComfyApp { } block_default = true; } + + // Ctrl+C Copy + if ((e.key === 'c') && (e.metaKey || e.ctrlKey)) { + if (e.shiftKey) { + this.copyToClipboard(true); + block_default = true; + } + // Trigger default onCopy + return true; + } + + // Ctrl+V Paste + if ((e.key === 'v') && (e.metaKey || e.ctrlKey)) { + if (e.shiftKey) { + this.pasteFromClipboard(true); + block_default = true; + } + else { + // Trigger default onPaste + return true; + } + } } this.graph.change(); @@ -798,7 +865,8 @@ export class ComfyApp { return false; } - return res; + // Fall through to Litegraph defaults + return origProcessKey.apply(this, arguments); }; } @@ -1110,6 +1178,7 @@ export class ComfyApp { this.#addDrawGroupsHandler(); this.#addApiUpdateHandlers(); this.#addDropHandler(); + this.#addCopyHandler(); this.#addPasteHandler(); this.#addKeyboardHandler(); diff --git a/web/scripts/widgets.js b/web/scripts/widgets.js index 5a4644b1..45ac9b89 100644 --- a/web/scripts/widgets.js +++ b/web/scripts/widgets.js @@ -76,7 +76,7 @@ export function addValueControlWidget(node, targetWidget, defaultValue = "random targetWidget.value = max; } } - return valueControl; + return valueControl; }; function seedWidget(node, inputName, inputData, app) { @@ -387,11 +387,12 @@ export const ComfyWidgets = { } }); - async function uploadFile(file, updateNode) { + async function uploadFile(file, updateNode, pasted = false) { try { // Wrap file in formdata so it includes filename const body = new FormData(); body.append("image", file); + if (pasted) body.append("subfolder", "pasted"); const resp = await api.fetchApi("/upload/image", { method: "POST", body, @@ -399,15 +400,17 @@ export const ComfyWidgets = { if (resp.status === 200) { const data = await resp.json(); - // Add the file as an option and update the widget value - if (!imageWidget.options.values.includes(data.name)) { - imageWidget.options.values.push(data.name); + // Add the file to the dropdown list and update the widget value + let path = data.name; + if (data.subfolder) path = data.subfolder + "/" + path; + + if (!imageWidget.options.values.includes(path)) { + imageWidget.options.values.push(path); } if (updateNode) { - showImage(data.name); - - imageWidget.value = data.name; + showImage(path); + imageWidget.value = path; } } else { alert(resp.status + " - " + resp.statusText); @@ -460,6 +463,16 @@ export const ComfyWidgets = { return handled; }; + node.pasteFile = function(file) { + if (file.type.startsWith("image/")) { + const is_pasted = (file.name === "image.png") && + (file.lastModified - Date.now() < 2000); + uploadFile(file, true, is_pasted); + return true; + } + return false; + } + return { widget: uploadWidget }; }, };