diff --git a/README.md b/README.md index 77d979ac..f610f949 100644 --- a/README.md +++ b/README.md @@ -32,14 +32,28 @@ This ui will let you design and execute advanced stable diffusion pipelines usin Workflow examples can be found on the [Examples page](https://comfyanonymous.github.io/ComfyUI_examples/) ## Shortcuts -- **Ctrl + A** select all nodes -- **Ctrl + M** mute/unmute selected nodes -- **Delete** or **Backspace** delete selected nodes -- **Space** Holding space key while moving the cursor moves the canvas around. It works when holding the mouse button down so it is easier to connect different nodes when the canvas gets too large. -- **Ctrl/Shift + Click** Add clicked node to selection. -- **Ctrl + C/Ctrl + V** - Copy and paste selected nodes, without maintaining the connection to the outputs of unselected nodes. -- **Ctrl + C/Ctrl + Shift + V** - Copy and paste selected nodes, and maintaining the connection from the outputs of unselected nodes to the inputs of the newly pasted nodes. -- Holding **Shift** and drag selected nodes - Move multiple selected nodes at the same time. + +| Keybind | Explanation | +| - | - | +| Ctrl + Enter | Queue up current graph for generation | +| Ctrl + Shift + Enter | Queue up current graph as first for generation | +| Ctrl + S | Save workflow | +| Ctrl + O | Load workflow | +| Ctrl + A | Select all nodes | +| Ctrl + M | Mute/unmute selected nodes | +| Delete/Backspace | Delete selected nodes | +| Ctrl + Delete/Backspace | Delete the current graph | +| Space | Move the canvas around when held and moving the cursor | +| Ctrl/Shift + Click | Add clicked node to selection | +| Ctrl + C/Ctrl + V | Copy and paste selected nodes (without maintaining connections to outputs of unselected nodes) | +| Ctrl + C/Ctrl + Shift + V| Copy and paste selected nodes (maintaining connections from outputs of unselected nodes to inputs of pasted nodes) | +| Shift + Drag | Move multiple selected nodes at the same time | +| Ctrl + D | Load default graph | +| Q | Toggle visibility of the queue | +| H | Toggle visibility of history | +| R | Refresh graph | + +Ctrl can also be replaced with Cmd instead for MacOS users # Installing diff --git a/web/extensions/core/keybinds.js b/web/extensions/core/keybinds.js new file mode 100644 index 00000000..1825007a --- /dev/null +++ b/web/extensions/core/keybinds.js @@ -0,0 +1,76 @@ +import { app } from "/scripts/app.js"; + +const id = "Comfy.Keybinds"; +app.registerExtension({ + name: id, + init() { + const keybindListener = function(event) { + const target = event.composedPath()[0]; + + if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") { + return; + } + + const modifierPressed = event.ctrlKey || event.metaKey; + + // Queue prompt using ctrl or command + enter + if (modifierPressed && (event.key === "Enter" || event.keyCode === 13 || event.keyCode === 10)) { + app.queuePrompt(event.shiftKey ? -1 : 0); + return; + } + + const modifierKeyIdMap = { + "s": "#comfy-save-button", + 83: "#comfy-save-button", + "o": "#comfy-file-input", + 79: "#comfy-file-input", + "Backspace": "#comfy-clear-button", + 8: "#comfy-clear-button", + "Delete": "#comfy-clear-button", + 46: "#comfy-clear-button", + "d": "#comfy-load-default-button", + 68: "#comfy-load-default-button", + }; + + const modifierKeybindId = modifierKeyIdMap[event.key] || modifierKeyIdMap[event.keyCode]; + if (modifierPressed && modifierKeybindId) { + event.preventDefault(); + + const elem = document.querySelector(modifierKeybindId); + elem.click(); + return; + } + + // Finished Handling all modifier keybinds, now handle the rest + if (event.ctrlKey || event.altKey || event.metaKey) { + return; + } + + // Close out of modals using escape + if (event.key === "Escape" || event.keyCode === 27) { + const modals = document.querySelectorAll(".comfy-modal"); + const modal = Array.from(modals).find(modal => window.getComputedStyle(modal).getPropertyValue("display") !== "none"); + if (modal) { + modal.style.display = "none"; + } + } + + const keyIdMap = { + "q": "#comfy-view-queue-button", + 81: "#comfy-view-queue-button", + "h": "#comfy-view-history-button", + 72: "#comfy-view-history-button", + "r": "#comfy-refresh-button", + 82: "#comfy-refresh-button", + }; + + const buttonId = keyIdMap[event.key] || keyIdMap[event.keyCode]; + if (buttonId) { + const button = document.querySelector(buttonId); + button.click(); + } + } + + window.addEventListener("keydown", keybindListener, true); + } +}); diff --git a/web/scripts/app.js b/web/scripts/app.js index 1695dcae..f158f345 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -35,7 +35,6 @@ export class ComfyApp { */ this.nodeOutputs = {}; - /** * If the shift key on the keyboard is pressed * @type {boolean} @@ -713,11 +712,6 @@ export class ComfyApp { #addKeyboardHandler() { window.addEventListener("keydown", (e) => { this.shiftDown = e.shiftKey; - - // Queue prompt using ctrl or command + enter - if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.keyCode === 13 || e.keyCode === 10)) { - this.queuePrompt(e.shiftKey ? -1 : 0); - } }); window.addEventListener("keyup", (e) => { this.shiftDown = e.shiftKey; diff --git a/web/scripts/ui.js b/web/scripts/ui.js index 09861c44..f320f840 100644 --- a/web/scripts/ui.js +++ b/web/scripts/ui.js @@ -431,7 +431,15 @@ export class ComfyUI { defaultValue: true, }); + const promptFilename = this.settings.addSetting({ + id: "Comfy.PromptFilename", + name: "Prompt for filename when saving workflow", + type: "boolean", + defaultValue: true, + }); + const fileInput = $el("input", { + id: "comfy-file-input", type: "file", accept: ".json,image/png", style: { display: "none" }, @@ -448,6 +456,7 @@ export class ComfyUI { $el("button.comfy-settings-btn", { textContent: "⚙️", onclick: () => this.settings.show() }), ]), $el("button.comfy-queue-btn", { + id: "queue-button", textContent: "Queue Prompt", onclick: () => app.queuePrompt(0, this.batchCount), }), @@ -496,9 +505,10 @@ export class ComfyUI { ]), ]), $el("div.comfy-menu-btns", [ - $el("button", { textContent: "Queue Front", onclick: () => app.queuePrompt(-1, this.batchCount) }), + $el("button", { id: "queue-front-button", textContent: "Queue Front", onclick: () => app.queuePrompt(-1, this.batchCount) }), $el("button", { $: (b) => (this.queue.button = b), + id: "comfy-view-queue-button", textContent: "View Queue", onclick: () => { this.history.hide(); @@ -507,6 +517,7 @@ export class ComfyUI { }), $el("button", { $: (b) => (this.history.button = b), + id: "comfy-view-history-button", textContent: "View History", onclick: () => { this.queue.hide(); @@ -517,14 +528,23 @@ export class ComfyUI { this.queue.element, this.history.element, $el("button", { + id: "comfy-save-button", textContent: "Save", onclick: () => { + let filename = "workflow.json"; + if (promptFilename.value) { + filename = prompt("Save workflow as:", filename); + if (!filename) return; + if (!filename.toLowerCase().endsWith(".json")) { + filename += ".json"; + } + } const json = JSON.stringify(app.graph.serialize(), null, 2); // convert the data to a JSON string const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = $el("a", { href: url, - download: "workflow.json", + download: filename, style: { display: "none" }, parent: document.body, }); @@ -535,15 +555,15 @@ export class ComfyUI { }, 0); }, }), - $el("button", { textContent: "Load", onclick: () => fileInput.click() }), - $el("button", { textContent: "Refresh", onclick: () => app.refreshComboInNodes() }), - $el("button", { textContent: "Clear", onclick: () => { + $el("button", { id: "comfy-load-button", textContent: "Load", onclick: () => fileInput.click() }), + $el("button", { id: "comfy-refresh-button", textContent: "Refresh", onclick: () => app.refreshComboInNodes() }), + $el("button", { id: "comfy-clear-button", textContent: "Clear", onclick: () => { if (!confirmClear.value || confirm("Clear workflow?")) { app.clean(); app.graph.clear(); } }}), - $el("button", { textContent: "Load Default", onclick: () => { + $el("button", { id: "comfy-load-default-button", textContent: "Load Default", onclick: () => { if (!confirmClear.value || confirm("Load default workflow?")) { app.loadGraphData() }