From bc16b70bdef76d118f055c023279a4b0d4ce16a7 Mon Sep 17 00:00:00 2001 From: Karun Date: Sun, 16 Apr 2023 01:25:11 -0400 Subject: [PATCH] Adds several keybinds that interact with ComfyUI (#491) * adds keybinds that interact w/ comfy menu * adds remaining keybinds * adds keybinds to readme and converts to table * ctrl s and o save and open workflow * moves keybinds to sep file, update readme * remap load default, support keycodes * update keybinds table, prepends comfy to ids * escape exits out of modals * modifier keys also use map * adds setting for filename prompt * better handle filename prompt Co-authored-by: missionfloyd --- README.md | 30 +++++++++---- web/extensions/core/keybinds.js | 76 +++++++++++++++++++++++++++++++++ web/scripts/app.js | 6 --- web/scripts/ui.js | 32 +++++++++++--- 4 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 web/extensions/core/keybinds.js diff --git a/README.md b/README.md index 77d979a..f610f94 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 0000000..1825007 --- /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 1695dca..f158f34 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 09861c4..f320f84 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() }