feat: initial release of LinkModeToggle (hotkeys + toolbar button)

This commit is contained in:
mikesimone
2025-11-06 23:34:25 -08:00
commit 5ce0309c81
6 changed files with 346 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules/
__pycache__/
*.pyc
.DS_Store
Thumbs.db
+9
View File
@@ -0,0 +1,9 @@
# LinkModeToggle – minimal server side stub so ComfyUI serves /web
# Place in: D:\AI\ComfyUI\custom_nodes\LinkModeToggle\__init__.py
# ComfyUI loads this file and, seeing WEB_DIRECTORY, mounts /web automatically.
WEB_DIRECTORY = "./web"
# No Python nodes here; purely a web extension.
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
+195
View File
@@ -0,0 +1,195 @@
// LinkModeToggle — front-end extension for ComfyUI
// Adds: F8/Ctrl+K hotkeys + a small toolbar button that cycles SPLINE/LINEAR/STRAIGHT.
(() => {
const ORDER = ["SPLINE","LINEAR","STRAIGHT"];
const KEY = "LinkModeToggle.mode";
// Robustly get the LiteGraph canvas object and the DOM <canvas> elements
const getCanvasObj = () => window.app?.canvas || window.app?.graphcanvas || null;
// Cycle & persistence
const setMode = (name) => {
const c = getCanvasObj();
if (!c) return false;
const modes = { STRAIGHT:0, SPLINE:1, LINEAR:2 };
const ok =
(typeof c.setLinkRenderMode === "function" && (c.setLinkRenderMode(modes[name]), true)) ||
("links_render_mode" in c && (() => {
const K = c.constructor;
const KM = {
STRAIGHT: K?.LINK_RENDER_MODE_STRAIGHT ?? K?.STRAIGHT_LINKS ?? 0,
SPLINE: K?.LINK_RENDER_MODE_SPLINE ?? K?.SPLINE_LINKS ?? 1,
LINEAR: K?.LINK_RENDER_MODE_LINEAR ?? K?.LINEAR_LINKS ?? 2,
};
c.links_render_mode = KM[name] ?? modes[name];
c.dirty_canvas = true; c.draw(true, true);
return true;
})()) ||
("render_curved_links" in c && (() => {
c.render_curved_links = (name === "SPLINE");
c.dirty_canvas = true; c.draw(true, true);
return true;
})()) ||
("render_link_curved" in c && (() => {
c.render_link_curved = (name === "SPLINE");
c.redraw?.(true,true); c.draw?.(true,true);
return true;
})());
if (ok) localStorage.setItem(KEY, name);
updateButtonUI(name, ok);
return ok;
};
const cycle = () => {
const cur = localStorage.getItem(KEY) || "SPLINE";
const idx = Math.max(0, ORDER.indexOf(cur));
const next = ORDER[(idx + 1) % ORDER.length];
setMode(next);
};
// Button
let btn, badge;
const updateButtonUI = (mode, ok) => {
if (!btn) return;
btn.dataset.mode = mode;
btn.title = `Link Mode: ${mode} (F8 / Ctrl+K to toggle)`;
if (badge) badge.textContent = mode[0]; // S/L/S
btn.classList.toggle("lmt-failed", !ok);
};
// --- build a Prime/Comfy-styled button like the others in the toolbar ---
const buildButton = () => {
if (btn) return btn;
btn = document.createElement("button");
// match existing toolbar buttons
btn.className = [
"p-button", "p-component", "p-button-icon-only", "p-button-secondary",
"h-8", "w-8", "bg-interface-panel-surface", "p-0", "hover:bg-button-hover-surface!"
].join(" ");
btn.type = "button";
btn.setAttribute("aria-label", "Cycle Link Mode (F8 / Ctrl+K)");
btn.setAttribute("data-pc-name", "button");
btn.setAttribute("data-p-disabled", "false");
btn.setAttribute("data-p-severity", "secondary");
btn.style.borderRadius = "8px";
btn.style.border = "none";
// icon (uses lucide class system like other buttons)
const icon = document.createElement("i");
// looks like a curve; feel free to change to: lucide--bezier-curve / pen-tool / route
icon.className = "icon-[lucide--bezier-curve] h-4 w-4";
btn.appendChild(icon);
// tiny badge (S/L/…) in the corner so you know current mode at a glance
badge = document.createElement("span");
badge.className = "lmt-badge";
badge.textContent = (localStorage.getItem(KEY) || "SPLINE")[0];
btn.appendChild(badge);
btn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); cycle(); });
return btn;
};
// --- find the bottom-right group and dock the button there ---
const attachButton = () => {
const b = buildButton();
// Find the toolbar group you pasted (bottom-right palette)
// We anchor off a known child button to be resilient to class churn.
const group =
document.querySelector('[data-testid="toggle-link-visibility-button"]')
?.closest('span.p-buttongroup[role="group"]')
|| document.querySelector('span.p-buttongroup[role="group"].right-0.bottom-0');
if (group) {
// Optional: put a divider before our button for symmetry
const divider = document.createElement("div");
divider.className = "h-[27px] w-[1px] self-center bg-node-divider";
// If we don’t already exist, append divider + button to the group
if (!group.contains(b)) {
group.appendChild(divider);
group.appendChild(b);
}
b.classList.remove("lmt-floating", "lmt-bottom-right", "lmt-in-toolbar");
// We’re in the toolbar now
return true;
}
// Fallback: float at bottom-right (won’t block nodes; z-index is high)
if (!document.body.contains(b)) document.body.appendChild(b);
b.classList.add("lmt-floating", "lmt-bottom-right");
return false;
};
// Keep hotkeys the same; just add a MutationObserver so we reattach if UI changes
const observeUI = () => {
const obs = new MutationObserver(() => {
// if button got removed (UI re-render), re-attach
if (!document.querySelector(".lmt-btn")) attachButton();
});
obs.observe(document.body, { childList: true, subtree: true });
return obs;
};
// Hotkeys
const keyHandler = (e) => {
const ctrlK = e.code === "KeyK" && e.ctrlKey && !e.altKey && !e.shiftKey;
const f8 = e.code === "F8" && !e.ctrlKey && !e.altKey && !e.shiftKey;
if (!(ctrlK || f8)) return;
const t = e.target;
const tag = (t && t.tagName) || "";
if (tag === "INPUT" || tag === "TEXTAREA" || t?.isContentEditable) return;
e.preventDefault();
e.stopPropagation();
cycle();
};
const bindHotkeys = () => {
const cobj = getCanvasObj();
const targets = [window, document, document.body, cobj?.canvas, cobj?.bgcanvas]
.filter(t => t && typeof t.addEventListener === "function");
targets.forEach(t => t.addEventListener("keydown", keyHandler, { capture: true }));
console.log("[LinkModeToggle] Hotkeys ready (F8 / Ctrl+K). Bound on:", targets.length, "targets.");
};
// Boot sequence with retries until app + canvas are ready
const boot = () => {
const tryInit = (attempt = 0) => {
const ready = window.app && getCanvasObj();
if (!ready) {
if (attempt < 60) return void setTimeout(() => tryInit(attempt + 1), 250);
console.warn("[LinkModeToggle] Timed out waiting for ComfyUI app/canvas.");
return;
}
setMode(localStorage.getItem(KEY) || "SPLINE");
attachButton();
bindHotkeys();
observeUI();
};
tryInit();
};
// Use Comfy’s extension API if present
if (window.app?.registerExtension) {
app.registerExtension({
name: "LinkModeToggle",
setup() { boot(); },
});
} else {
// Older frontends: best-effort boot
document.addEventListener("DOMContentLoaded", boot);
boot();
}
})();
+79
View File
@@ -0,0 +1,79 @@
/* Small, theme-friendly button */
.lmt-btn {
display: inline-flex;
align-items: center;
gap: .4rem;
padding: .25rem .5rem;
border: none;
border-radius: 6px;
cursor: pointer;
font: inherit;
line-height: 1;
opacity: .9;
}
.lmt-btn:hover {
opacity: 1;
}
/* tiny icon + mode badge */
.lmt-icon {
font-weight: 700;
}
/* subtle corner badge on the toolbar icon */
.lmt-badge {
position: absolute;
right: -2px;
bottom: -2px;
font-size: 9px;
line-height: 1;
padding: 1px 3px;
border-radius: 6px;
background: var(--node-border, #444);
color: var(--descrip-text, #ccc);
pointer-events: none;
}
/* fallback floating style (only used if toolbar not found) */
.lmt-floating.lmt-bottom-right {
position: fixed;
right: 10px;
bottom: 10px;
z-index: 99999;
}
/* when we can’t find a toolbar, float it bottom-right */
.lmt-floating {
position: fixed;
right: 12px;
bottom: 12px;
z-index: 99999;
pointer-events: auto;
}
/* if we docked into a toolbar, keep it tidy */
.lmt-in-toolbar {
margin-left: .5rem;
}
/* visual hint if API failed (should be rare) */
.lmt-failed {
outline: 2px solid #c33;
}
/* subtle corner badge on the toolbar icon */
.lmt-badge {
position: absolute;
right: -2px;
bottom: -2px;
font-size: 9px;
line-height: 1;
padding: 1px 3px;
border-radius: 6px;
background: var(--node-border, #444);
color: var(--descrip-text, #ccc);
pointer-events: none;
}
+5
View File
@@ -0,0 +1,5 @@
// Entry point for LinkModeToggle (ComfyUI custom node)
// Loads CSS and the actual extension code.
import "./index.css";
import "./extensions/linkmode-toggle.js"; // your extension lives here
+53
View File
@@ -0,0 +1,53 @@
/* Small, unobtrusive button that matches ComfyUI style */
.lmt-btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 2px 8px;
border-radius: 8px;
font-size: 12px;
line-height: 16px;
cursor: pointer;
opacity: 0.85;
}
.lmt-btn:hover {
opacity: 1;
}
.lmt-icon {
font-weight: 700;
font-size: 14px;
line-height: 14px;
}
.lmt-badge {
font-weight: 700;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
}
/* When we can’t find the toolbar, dock to bottom-left without covering the canvas */
.lmt-floating {
position: fixed;
right: 12px; /* bottom-right so it never hides nodes */
bottom: 12px;
z-index: 99999; /* sit above overlays */
pointer-events: auto;
}
.lmt-bottom-right {
right: 12px;
bottom: 12px;
}
/* When placed in toolbar, make it visually consistent */
.lmt-in-toolbar {
margin-left: 6px;
}
/* Fail state (should basically never show) */
.lmt-failed {
outline: 1px solid #e66;
box-shadow: 0 0 0 2px rgba(230,102,102,.2) inset;
}