From f4eba67933ccf05bb1afc44e2c95c7e618ea8126 Mon Sep 17 00:00:00 2001 From: rgthree Date: Wed, 15 May 2024 22:04:15 -0400 Subject: [PATCH] Enhance bookmarks to allow for multiple-key shortcuts, and make the key check exclusive (except for shift). Helps #227 --- README.md | 5 +- src_web/comfyui/bookmark.ts | 57 ++++++++--------- src_web/comfyui/rgthree.ts | 118 +++++++++++++++++++++++++++--------- web/comfyui/bookmark.js | 31 +++++----- web/comfyui/rgthree.js | 52 +++++++++++++--- 5 files changed, 184 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 2f6dffb..dff03ac 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,11 @@ Note, you can right-click on a bunch of the rgthree-comfy nodes and select `🛟 >
> â„šī¸ See More Information > -> - Define the `sahortvut_key` to press to go right to that bookmark node, anchored in the top left +> - Define the `shortcut_key` to press to go right to that bookmark node, anchored in the top left. > - You can also define the zoom level as well! +> - Pro tip: `shortcut_key` can be multiple keys. For instance "alt + shift + !" would require +> pressing the alt key, the shift key, and the "!" (as in the "1" key, but with shift pressed) +> in order to trigger. >
diff --git a/src_web/comfyui/bookmark.ts b/src_web/comfyui/bookmark.ts index 06be5a5..b931b13 100644 --- a/src_web/comfyui/bookmark.ts +++ b/src_web/comfyui/bookmark.ts @@ -2,15 +2,15 @@ import { RgthreeBaseVirtualNodeConstructor } from "typings/rgthree.js"; // @ts-ignore import { app } from "../../scripts/app.js"; import { RgthreeBaseVirtualNode } from "./base_node.js"; +import { rgthree } from "./rgthree.js"; import { NodeTypesString } from "./constants.js"; import type { LGraph as TLGraph, LiteGraph as TLiteGraph, LGraphCanvas as TLGraphCanvas, - ISliderWidget, INumberWidget, - Vector2, } from "typings/litegraph.js"; +import { getClosestOrSelf } from "rgthree/common/utils_dom.js"; declare const LiteGraph: typeof TLiteGraph; @@ -19,7 +19,6 @@ declare const LiteGraph: typeof TLiteGraph; * navigate to that node, with it in the top-left corner. */ export class Bookmark extends RgthreeBaseVirtualNode { - static override type = NodeTypesString.BOOKMARK; static override title = NodeTypesString.BOOKMARK; override comfyClass = NodeTypesString.BOOKMARK; @@ -32,7 +31,6 @@ export class Bookmark extends RgthreeBaseVirtualNode { // override it with a setter and re-set it measured exactly as we want. ___collapsed_width: number = 0; - override isVirtualNode = true; override serialize_widgets = true; @@ -43,10 +41,10 @@ export class Bookmark extends RgthreeBaseVirtualNode { override set _collapsed_width(width: number) { const canvas = app.canvas as TLGraphCanvas; - const ctx = canvas.canvas.getContext('2d')!; + const ctx = canvas.canvas.getContext("2d")!; const oldFont = ctx.font; ctx.font = canvas.title_text_font; - this.___collapsed_width = 40 + ctx.measureText(this.title).width; + this.___collapsed_width = 40 + ctx.measureText(this.title).width; ctx.font = oldFont; } @@ -54,14 +52,18 @@ export class Bookmark extends RgthreeBaseVirtualNode { constructor(title = Bookmark.title) { super(title); - this.addWidget('text', 'shortcut_key', '1', (value: string, ...args) => { - value = value.trim()[0] || '1'; - },{ - y: 8, - }); - this.addWidget('number', 'zoom', 1, (value: number) => { - - }, { + this.addWidget( + "text", + "shortcut_key", + "1", + (value: string, ...args) => { + value = value.trim()[0] || "1"; + }, + { + y: 8, + }, + ); + this.addWidget("number", "zoom", 1, (value: number) => {}, { y: 8 + LiteGraph.NODE_WIDGET_HEIGHT + 4, max: 2, min: 0.5, @@ -78,28 +80,29 @@ export class Bookmark extends RgthreeBaseVirtualNode { // } get shortcutKey(): string { - return this.widgets[0]?.value?.toLocaleLowerCase() ?? ''; + return this.widgets[0]?.value?.toLocaleLowerCase() ?? ""; } override onAdded(graph: TLGraph): void { - window.addEventListener("keydown", this.keypressBound); + rgthree.addEventListener("keydown", this.keypressBound as EventListener); } override onRemoved(): void { - window.removeEventListener("keydown", this.keypressBound); + rgthree.removeEventListener("keydown", this.keypressBound as EventListener); } - async onKeypress(event: KeyboardEvent) { - const target = (event.target as HTMLElement)!; - // Span because the properties panel uses a contenteditable - if (['input','textarea', 'span'].includes(target.localName)) { + onKeypress(event: CustomEvent<{ originalEvent: KeyboardEvent }>) { + const originalEvent = event.detail.originalEvent; + const target = (originalEvent.target as HTMLElement)!; + if (getClosestOrSelf(target, 'input,textarea,[contenteditable="true"]')) { return; } - if (event.ctrlKey || event.metaKey || event.altKey) { - return; - } - if (event.key.toLocaleLowerCase() === this.shortcutKey) { + + // Only the shortcut keys are held down, otionally including "shift". + if (rgthree.areOnlyKeysDown(this.widgets[0]!.value, true)) { this.canvasToBookmark(); + originalEvent.preventDefault(); + originalEvent.stopPropagation(); } } @@ -108,8 +111,8 @@ export class Bookmark extends RgthreeBaseVirtualNode { // ComfyUI seemed to break us again, but couldn't repro. No reason to not check, I guess. // https://github.com/rgthree/rgthree-comfy/issues/71 if (canvas?.ds?.offset) { - canvas.ds.offset[0] = -this.pos[0] + 16; - canvas.ds.offset[1] = -this.pos[1] + 40; + canvas.ds.offset[0] = -this.pos[0] + 16; + canvas.ds.offset[1] = -this.pos[1] + 40; } if (canvas?.ds?.scale != null) { canvas.ds.scale = Number(this.widgets[1]!.value || 1); diff --git a/src_web/comfyui/rgthree.ts b/src_web/comfyui/rgthree.ts index c7e39d2..9311694 100644 --- a/src_web/comfyui/rgthree.ts +++ b/src_web/comfyui/rgthree.ts @@ -22,7 +22,7 @@ import { NodeTypesString } from "./constants.js"; import { RgthreeProgressBar } from "rgthree/common/progress_bar.js"; import { RgthreeConfigDialog } from "./config.js"; import { iconGear, iconReplace, iconStarFilled, logoRgthree } from "rgthree/common/media/svgs.js"; -import type { Bookmark } from './bookmark'; +import type { Bookmark } from "./bookmark"; declare const LiteGraph: typeof TLiteGraph; declare const LGraphCanvas: typeof TLGraphCanvas; @@ -121,14 +121,12 @@ class Logger { } } - /** * A log session, with the name as the prefix. A new session will stack prefixes. */ class LogSession { - readonly logger = new Logger(); - readonly logsCache: {[key: string]: {lastShownTime: number}} = {}; + readonly logsCache: { [key: string]: { lastShownTime: number } } = {}; constructor(readonly name?: string) {} @@ -139,16 +137,17 @@ class LogSession { * const [logMethod, logArgs] = logger.logParts(LogLevel.DEBUG, message, ...args); * console[logMethod]?.(...logArgs); */ - logParts(level: LogLevel, message?: string, ...args: any[]) : [ConsoleLogFns, any[]] { + logParts(level: LogLevel, message?: string, ...args: any[]): [ConsoleLogFns, any[]] { message = `${this.name || ""}${message ? " " + message : ""}`; - return this.logger.logParts( - level, - message, - ...args, - ); + return this.logger.logParts(level, message, ...args); } - logPartsOnceForTime(level: LogLevel, time: number, message?: string, ...args: any[]) : [ConsoleLogFns, any[]] { + logPartsOnceForTime( + level: LogLevel, + time: number, + message?: string, + ...args: any[] + ): [ConsoleLogFns, any[]] { message = `${this.name || ""}${message ? " " + message : ""}`; const cacheKey = `${level}:${message}`; const cacheEntry = this.logsCache[cacheKey]; @@ -156,17 +155,12 @@ class LogSession { if (cacheEntry && cacheEntry.lastShownTime + time > now) { return ["none" as "info", []]; } - const parts = this.logger.logParts( - level, - message, - ...args, - ); + const parts = this.logger.logParts(level, message, ...args); if (console[parts[0]]) { - this.logsCache[cacheKey] = this.logsCache[cacheKey] || {} as {lastShownTime: number}; + this.logsCache[cacheKey] = this.logsCache[cacheKey] || ({} as { lastShownTime: number }); this.logsCache[cacheKey]!.lastShownTime = now; } return parts; - } debugParts(message?: string, ...args: any[]) { @@ -251,6 +245,18 @@ class Rgthree extends EventTarget { this.handleKeyup(e); }); + // If we get a visibilitychange, then clear the keys since we can't listen for keys up/down when + // not visible. + document.addEventListener("visibilitychange", (e) => { + this.clearKeydowns(); + }); + + // If we get a blur, then also clear the keys since we can't listen for keys up/down when + // blurred. This can happen w/o a visibilitychange, like a browser alert. + window.addEventListener("blur", (e) => { + this.clearKeydowns(); + }); + this.initializeGraphAndCanvasHooks(); this.initializeComfyUIHooks(); this.initializeContextMenu(); @@ -852,8 +858,19 @@ class Rgthree extends EventTarget { container && (container.innerHTML = ""); } + private clearKeydowns() { + this.ctrlKey = false; + this.altKey = false; + this.metaKey = false; + this.shiftKey = false; + for (const key in this.downKeys) delete this.downKeys[key]; + } + /** - * Handle keydown. Pulled out because sometimes a node will get a keydown before rgthree. + * Handle keydown. Pulled out because sometimes a node will get a keydown before rgthree and call + * into this.. + * + * Note: ComfyUI blocks Space, Esc, Delete, and Backspace. */ handleKeydown(e: KeyboardEvent) { this.ctrlKey = !!e.ctrlKey; @@ -861,33 +878,78 @@ class Rgthree extends EventTarget { this.metaKey = !!e.metaKey; this.shiftKey = !!e.shiftKey; this.downKeys[e.key.toLocaleUpperCase()] = true; - this.downKeys["^" + e.key.toLocaleUpperCase()] = true; + this.dispatchCustomEvent("keydown", { originalEvent: e }); } /** - * Handle keyup. Pulled out because sometimes a node will get a keyup before rgthree. + * Handle keyup. Pulled out because sometimes a node will get a keyup before rgthree and call + * into this. */ handleKeyup(e: KeyboardEvent) { this.ctrlKey = !!e.ctrlKey; this.altKey = !!e.altKey; this.metaKey = !!e.metaKey; this.shiftKey = !!e.shiftKey; - this.downKeys[e.key.toLocaleUpperCase()] = false; - this.downKeys["^" + e.key.toLocaleUpperCase()] = false; + delete this.downKeys[e.key.toLocaleUpperCase()]; + this.dispatchCustomEvent("keyup", { originalEvent: e }); + } + + /** + * Parses a shortcut string. + * + * - 's' => ['S'] + * - 'shift + c' => ['SHIFT', 'C'] + * - 'shift + meta + @' => ['SHIFT', 'META', '@'] + * - 'shift + + + @' => ['SHIFT', '__PLUS__', '='] + * - '+ + p' => ['__PLUS__', 'P'] + */ + private getKeysFromShortcut(shortcut: string | string[]) { + let keys; + if (typeof shortcut === "string") { + // Rip all spaces out. Note, Comfy swallows space, so we don't have to handle it. Otherwise, + // we would require space to be fed as "Space" or "Spacebar" instead of " ". + shortcut = shortcut.replace(/\s/g, ""); + // Change a real "+" to something we can encode. + shortcut = shortcut.replace(/^\+/, "__PLUS__").replace(/\+\+/, "+__PLUS__"); + keys = shortcut.split("+").map((i) => i.replace("__PLUS__", "+")); + } else { + keys = [...shortcut]; + } + return keys.map((k) => k.toLocaleUpperCase()); } /** * Checks if all keys passed in are down. */ - areAllKeysDown(keys: string[], caseSensitive = false) { + areAllKeysDown(keys: string | string[]) { + keys = this.getKeysFromShortcut(keys); return keys.every((k) => { - if (caseSensitive) { - return rgthree.downKeys["^" + k.trim()]; - } - return rgthree.downKeys[k.trim().toUpperCase()]; + return rgthree.downKeys[k]; }); } + /** + * Checks if only the keys passed in are down; optionally and additionally allowing "shift" key. + */ + areOnlyKeysDown(keys: string | string[], alsoAllowShift = false) { + keys = this.getKeysFromShortcut(keys); + const allKeysDown = this.areAllKeysDown(keys); + const downKeysLength = Object.values(rgthree.downKeys).length; + // All keys are down and they're the only ones. + if (allKeysDown && keys.length === downKeysLength) { + return true; + } + // Special case allowing the shift key in addition to the shortcut keys. This helps when a user + // may had originally defined "$" as a shortcut, but needs to press "shift + $" since it's an + // upper key character, etc. + if (alsoAllowShift && !keys.includes("SHIFT") && keys.length === downKeysLength - 1) { + // If we're holding down shift, have one extra key held down, and the original keys don't + // include shift, then we're good to go. + return this.areAllKeysDown(["SHIFT"]); + } + return false; + } + /** * Injects the rgthree.css file into the app. */ diff --git a/web/comfyui/bookmark.js b/web/comfyui/bookmark.js index cfad7ec..7d2c338 100644 --- a/web/comfyui/bookmark.js +++ b/web/comfyui/bookmark.js @@ -1,13 +1,15 @@ import { app } from "../../scripts/app.js"; import { RgthreeBaseVirtualNode } from "./base_node.js"; +import { rgthree } from "./rgthree.js"; import { NodeTypesString } from "./constants.js"; +import { getClosestOrSelf } from "../../rgthree/common/utils_dom.js"; export class Bookmark extends RgthreeBaseVirtualNode { get _collapsed_width() { return this.___collapsed_width; } set _collapsed_width(width) { const canvas = app.canvas; - const ctx = canvas.canvas.getContext('2d'); + const ctx = canvas.canvas.getContext("2d"); const oldFont = ctx.font; ctx.font = canvas.title_text_font; this.___collapsed_width = 40 + ctx.measureText(this.title).width; @@ -19,13 +21,12 @@ export class Bookmark extends RgthreeBaseVirtualNode { this.___collapsed_width = 0; this.isVirtualNode = true; this.serialize_widgets = true; - this.addWidget('text', 'shortcut_key', '1', (value, ...args) => { - value = value.trim()[0] || '1'; + this.addWidget("text", "shortcut_key", "1", (value, ...args) => { + value = value.trim()[0] || "1"; }, { y: 8, }); - this.addWidget('number', 'zoom', 1, (value) => { - }, { + this.addWidget("number", "zoom", 1, (value) => { }, { y: 8 + LiteGraph.NODE_WIDGET_HEIGHT + 4, max: 2, min: 0.5, @@ -36,24 +37,24 @@ export class Bookmark extends RgthreeBaseVirtualNode { } get shortcutKey() { var _a, _b, _c; - return (_c = (_b = (_a = this.widgets[0]) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.toLocaleLowerCase()) !== null && _c !== void 0 ? _c : ''; + return (_c = (_b = (_a = this.widgets[0]) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.toLocaleLowerCase()) !== null && _c !== void 0 ? _c : ""; } onAdded(graph) { - window.addEventListener("keydown", this.keypressBound); + rgthree.addEventListener("keydown", this.keypressBound); } onRemoved() { - window.removeEventListener("keydown", this.keypressBound); + rgthree.removeEventListener("keydown", this.keypressBound); } - async onKeypress(event) { - const target = event.target; - if (['input', 'textarea', 'span'].includes(target.localName)) { + onKeypress(event) { + const originalEvent = event.detail.originalEvent; + const target = originalEvent.target; + if (getClosestOrSelf(target, 'input,textarea,[contenteditable="true"]')) { return; } - if (event.ctrlKey || event.metaKey || event.altKey) { - return; - } - if (event.key.toLocaleLowerCase() === this.shortcutKey) { + if (rgthree.areOnlyKeysDown(this.widgets[0].value, true)) { this.canvasToBookmark(); + originalEvent.preventDefault(); + originalEvent.stopPropagation(); } } canvasToBookmark() { diff --git a/web/comfyui/rgthree.js b/web/comfyui/rgthree.js index 3c9e5f5..9a980c0 100644 --- a/web/comfyui/rgthree.js +++ b/web/comfyui/rgthree.js @@ -138,6 +138,12 @@ class Rgthree extends EventTarget { window.addEventListener("keyup", (e) => { this.handleKeyup(e); }); + document.addEventListener("visibilitychange", (e) => { + this.clearKeydowns(); + }); + window.addEventListener("blur", (e) => { + this.clearKeydowns(); + }); this.initializeGraphAndCanvasHooks(); this.initializeComfyUIHooks(); this.initializeContextMenu(); @@ -584,30 +590,60 @@ class Rgthree extends EventTarget { let container = document.querySelector(".rgthree-top-messages-container"); container && (container.innerHTML = ""); } + clearKeydowns() { + this.ctrlKey = false; + this.altKey = false; + this.metaKey = false; + this.shiftKey = false; + for (const key in this.downKeys) + delete this.downKeys[key]; + } handleKeydown(e) { this.ctrlKey = !!e.ctrlKey; this.altKey = !!e.altKey; this.metaKey = !!e.metaKey; this.shiftKey = !!e.shiftKey; this.downKeys[e.key.toLocaleUpperCase()] = true; - this.downKeys["^" + e.key.toLocaleUpperCase()] = true; + this.dispatchCustomEvent("keydown", { originalEvent: e }); } handleKeyup(e) { this.ctrlKey = !!e.ctrlKey; this.altKey = !!e.altKey; this.metaKey = !!e.metaKey; this.shiftKey = !!e.shiftKey; - this.downKeys[e.key.toLocaleUpperCase()] = false; - this.downKeys["^" + e.key.toLocaleUpperCase()] = false; + delete this.downKeys[e.key.toLocaleUpperCase()]; + this.dispatchCustomEvent("keyup", { originalEvent: e }); } - areAllKeysDown(keys, caseSensitive = false) { + getKeysFromShortcut(shortcut) { + let keys; + if (typeof shortcut === "string") { + shortcut = shortcut.replace(/\s/g, ""); + shortcut = shortcut.replace(/^\+/, "__PLUS__").replace(/\+\+/, "+__PLUS__"); + keys = shortcut.split("+").map((i) => i.replace("__PLUS__", "+")); + } + else { + keys = [...shortcut]; + } + return keys.map((k) => k.toLocaleUpperCase()); + } + areAllKeysDown(keys) { + keys = this.getKeysFromShortcut(keys); return keys.every((k) => { - if (caseSensitive) { - return rgthree.downKeys["^" + k.trim()]; - } - return rgthree.downKeys[k.trim().toUpperCase()]; + return rgthree.downKeys[k]; }); } + areOnlyKeysDown(keys, alsoAllowShift = false) { + keys = this.getKeysFromShortcut(keys); + const allKeysDown = this.areAllKeysDown(keys); + const downKeysLength = Object.values(rgthree.downKeys).length; + if (allKeysDown && keys.length === downKeysLength) { + return true; + } + if (alsoAllowShift && !keys.includes("SHIFT") && keys.length === downKeysLength - 1) { + return this.areAllKeysDown(["SHIFT"]); + } + return false; + } injectRgthreeCss() { let link = document.createElement("link"); link.rel = "stylesheet";