Add support for pasting images into the graph
It can be useful to paste images from the clipboard directly into the node graph. This commit modifies copy and paste handling to support this. When an image file is found in the clipboard, we check whether an image node is selected. If so, paste the image into that node. Otherwise, a new node is created. If no image data are found in the clipboard, we call the original Litegraph paste. To ensure that onCopy and onPaste events are fired, we override Litegraph's ctrl+c and ctrl+v handling. Try to detect whether the pasted image is a real file on disk, or just pixel data copied from e.g. Photoshop. Pasted pixel data will be called 'image.png' and have a creation time of now. If it is simply pasted data, we store it in the subfolder /input/clipboard/. This also adds support for the subfolder property in the IMAGEUPLOAD widget.
This commit is contained in:
parent
a74c5dbf37
commit
6f70227b8c
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue