314 lines
10 KiB
JavaScript
314 lines
10 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
|
|
const PANEL_TYPE = "SolidlimeMultiBoolPanel";
|
|
const ROW_HEIGHT = 23;
|
|
const HEADER_PAD = 6;
|
|
const MAX_ITEMS = 20;
|
|
const DEBOUNCE_MS = 120;
|
|
|
|
// ── helpers ──────────────────────────────────────────────
|
|
|
|
function getModeWidget(node) {
|
|
return (node.widgets || []).find((w) => w.name === "mode");
|
|
}
|
|
|
|
function getPanelMode(node) {
|
|
return getModeWidget(node)?.value ?? "widgets";
|
|
}
|
|
|
|
/** Resolve target nodes whose title starts with "<panelTitle>:" and item part is non-empty. */
|
|
function resolveTargets(panelNode) {
|
|
const prefix = panelNode.title;
|
|
if (!prefix) return [];
|
|
const needle = prefix + ":";
|
|
return (app.graph._nodes || []).filter((n) => {
|
|
if (n.id === panelNode.id) return false;
|
|
if (!n.title || !n.title.startsWith(needle)) return false;
|
|
const item = n.title.slice(needle.length).trim();
|
|
return item.length > 0;
|
|
});
|
|
}
|
|
|
|
/** Extract item name (part after ":") from a target node title. */
|
|
function itemName(panelNode, targetNode) {
|
|
const prefix = panelNode.title + ":";
|
|
return targetNode.title.slice(prefix.length).trim();
|
|
}
|
|
|
|
/** Get current real state of a target node for a given panel mode. */
|
|
function currentRealValue(targetNode, mode) {
|
|
if (mode === "bypass") return targetNode.mode === 2;
|
|
if (mode === "mute") return targetNode.mode === 4;
|
|
// widgets: first boolean widget value
|
|
const bw = (targetNode.widgets || []).find(
|
|
(w) => w.type === "BOOLEAN" || w.type === "toggle" || (w.options && w.options.type === "boolean")
|
|
);
|
|
return bw ? !!bw.value : false;
|
|
}
|
|
|
|
/** Check if target node has a boolean widget. */
|
|
function hasBooleanWidget(targetNode) {
|
|
return (targetNode.widgets || []).some(
|
|
(w) => w.type === "BOOLEAN" || w.type === "toggle" || (w.options && w.options.type === "boolean")
|
|
);
|
|
}
|
|
|
|
/** Apply a toggle value to a target node. */
|
|
function applyValue(targetNode, mode, value) {
|
|
if (mode === "bypass") {
|
|
targetNode.mode = value ? 2 : 0;
|
|
} else if (mode === "mute") {
|
|
targetNode.mode = value ? 4 : 0;
|
|
} else {
|
|
// widgets
|
|
const bw = (targetNode.widgets || []).find(
|
|
(w) => w.type === "BOOLEAN" || w.type === "toggle" || (w.options && w.options.type === "boolean")
|
|
);
|
|
if (bw) {
|
|
bw.value = value;
|
|
if (typeof bw.callback === "function") {
|
|
try { bw.callback(value); } catch (_) { /* ignore */ }
|
|
}
|
|
}
|
|
}
|
|
app.graph.setDirtyCanvas(true, true);
|
|
}
|
|
|
|
// ── panel widget builder ─────────────────────────────────
|
|
|
|
function buildPanel(panelNode) {
|
|
// Ensure properties container
|
|
if (!panelNode.properties.switches) {
|
|
panelNode.properties.switches = {};
|
|
}
|
|
|
|
const container = document.createElement("div");
|
|
container.style.cssText =
|
|
"font-family:monospace;font-size:12px;color:#ccc;overflow-y:auto;user-select:none;";
|
|
|
|
const listEl = document.createElement("div");
|
|
container.appendChild(listEl);
|
|
|
|
// addDOMWidget — the new frontend (comfyui_frontend_package) drops
|
|
// options.computeSize, so sizing must be attached to the widget instance.
|
|
// Provide both computeLayoutSize (new) and computeSize (legacy) paths.
|
|
let rowsHeight = 60;
|
|
const domWidget = panelNode.addDOMWidget("multi_bool_panel", "multi_bool_panel", container, {
|
|
serialize: false,
|
|
});
|
|
domWidget.computeLayoutSize = () => ({
|
|
minHeight: rowsHeight,
|
|
minWidth: 150,
|
|
maxHeight: rowsHeight,
|
|
});
|
|
domWidget.computeSize = (nodeWidth) => [nodeWidth, rowsHeight];
|
|
|
|
// ── render ────────────────────────────────────────────
|
|
|
|
let debounceTimer = null;
|
|
|
|
function scheduleRefresh() {
|
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(() => render(), DEBOUNCE_MS);
|
|
}
|
|
|
|
function render() {
|
|
const mode = getPanelMode(panelNode);
|
|
const switches = panelNode.properties.switches;
|
|
const targets = resolveTargets(panelNode);
|
|
|
|
// Sort case-insensitively by item name
|
|
targets.sort((a, b) => {
|
|
const na = itemName(panelNode, a).toLowerCase();
|
|
const nb = itemName(panelNode, b).toLowerCase();
|
|
return na.localeCompare(nb);
|
|
});
|
|
|
|
// Build rows data
|
|
let rows = targets.map((t) => {
|
|
const name = itemName(panelNode, t);
|
|
return { node: t, name };
|
|
});
|
|
|
|
// Limit
|
|
const overflow = rows.length > MAX_ITEMS ? rows.length - MAX_ITEMS : 0;
|
|
if (overflow > 0) rows = rows.slice(0, MAX_ITEMS);
|
|
|
|
// Clear list
|
|
listEl.innerHTML = "";
|
|
|
|
// Empty state hint
|
|
if (rows.length === 0 && overflow === 0) {
|
|
const hint = document.createElement("div");
|
|
hint.style.cssText =
|
|
"padding:6px 4px;color:#888;font-size:11px;line-height:1.4;text-align:center;";
|
|
hint.textContent =
|
|
'このノードの名前を「A」に変えると、タイトルが「A:名前」のノードを操作対象にできる';
|
|
listEl.appendChild(hint);
|
|
resizeNode();
|
|
return;
|
|
}
|
|
|
|
// Render rows
|
|
rows.forEach(({ node: t, name }) => {
|
|
const row = document.createElement("div");
|
|
row.style.cssText =
|
|
"display:flex;align-items:center;justify-content:space-between;" +
|
|
"padding:2px 6px;border-bottom:1px solid #2a2a2a;cursor:default;height:" +
|
|
ROW_HEIGHT + "px;box-sizing:border-box;";
|
|
|
|
// Determine enabled state
|
|
let disabled = false;
|
|
if (mode === "widgets" && !hasBooleanWidget(t)) {
|
|
disabled = true;
|
|
}
|
|
|
|
// Label
|
|
const label = document.createElement("span");
|
|
label.style.cssText =
|
|
"flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" +
|
|
"font-size:12px;" + (disabled ? "color:#555;" : "color:#ccc;");
|
|
label.textContent = name;
|
|
if (disabled) {
|
|
label.title = "booleanウィジェットがありません";
|
|
}
|
|
row.appendChild(label);
|
|
|
|
// Toggle
|
|
const toggle = createToggle(disabled);
|
|
const saved = switches[name];
|
|
let isOn;
|
|
if (saved !== undefined) {
|
|
isOn = !!saved;
|
|
} else {
|
|
isOn = currentRealValue(t, mode);
|
|
}
|
|
if (isOn) toggle.classList.add("on");
|
|
if (disabled) toggle.classList.add("disabled");
|
|
|
|
toggle.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
if (disabled) return;
|
|
const nowOn = !toggle.classList.contains("on");
|
|
toggle.classList.toggle("on", nowOn);
|
|
switches[name] = nowOn;
|
|
applyValue(t, mode, nowOn);
|
|
});
|
|
|
|
row.appendChild(toggle);
|
|
|
|
// Hover highlight
|
|
row.addEventListener("mouseenter", () => {
|
|
if (!disabled) row.style.background = "#2a2a2a";
|
|
});
|
|
row.addEventListener("mouseleave", () => {
|
|
row.style.background = "";
|
|
});
|
|
|
|
listEl.appendChild(row);
|
|
});
|
|
|
|
// Overflow note
|
|
if (overflow > 0) {
|
|
const note = document.createElement("div");
|
|
note.style.cssText =
|
|
"padding:3px 6px;color:#666;font-size:11px;text-align:center;border-top:1px solid #333;";
|
|
note.textContent = "他" + overflow + "件";
|
|
listEl.appendChild(note);
|
|
}
|
|
|
|
resizeNode();
|
|
}
|
|
|
|
function resizeNode() {
|
|
const rows = listEl.children.length;
|
|
const h = rows * ROW_HEIGHT + HEADER_PAD;
|
|
rowsHeight = Math.max(h + HEADER_PAD, 60);
|
|
panelNode.size = [panelNode.size[0], rowsHeight];
|
|
panelNode.setDirtyCanvas(true, true);
|
|
}
|
|
|
|
// ── toggle element factory ────────────────────────────
|
|
|
|
function createToggle(disabled) {
|
|
const el = document.createElement("div");
|
|
el.className = "mbp-toggle" + (disabled ? " disabled" : "");
|
|
el.style.cssText =
|
|
"width:32px;height:16px;border-radius:8px;position:relative;cursor:pointer;" +
|
|
"background:#3a3a3a;transition:background 0.15s;flex-shrink:0;margin-left:8px;";
|
|
const knob = document.createElement("div");
|
|
knob.style.cssText =
|
|
"width:12px;height:12px;border-radius:50%;background:#888;position:absolute;" +
|
|
"top:2px;left:2px;transition:left 0.15s,background 0.15s;";
|
|
el.appendChild(knob);
|
|
|
|
// Use a MutationObserver-like approach via class toggle handler
|
|
const observer = new MutationObserver(() => {
|
|
if (el.classList.contains("on")) {
|
|
el.style.background = "#4a7a4a";
|
|
knob.style.left = "18px";
|
|
knob.style.background = "#8f8";
|
|
} else {
|
|
el.style.background = "#3a3a3a";
|
|
knob.style.left = "2px";
|
|
knob.style.background = el.classList.contains("disabled") ? "#555" : "#888";
|
|
}
|
|
});
|
|
observer.observe(el, { attributes: true, attributeFilter: ["class"] });
|
|
|
|
return el;
|
|
}
|
|
|
|
// ── event hooks ───────────────────────────────────────
|
|
|
|
// Graph changes (add/remove/rename nodes) — LiteGraph has no graph.on("change")
|
|
// event; chain the on_change property hook instead (works on old and new litegraph).
|
|
const graph = panelNode.graph;
|
|
const prevOnChange = graph ? graph.on_change : null;
|
|
const onGraphChangeWrapper = (g) => {
|
|
if (prevOnChange) prevOnChange(g);
|
|
scheduleRefresh();
|
|
};
|
|
if (graph) graph.on_change = onGraphChangeWrapper;
|
|
|
|
// Title / property changes
|
|
const origOnPropertyChanged = panelNode.onPropertyChanged;
|
|
panelNode.onPropertyChanged = function (prop, value) {
|
|
if (prop === "title") scheduleRefresh();
|
|
if (origOnPropertyChanged) origOnPropertyChanged.call(this, prop, value);
|
|
};
|
|
|
|
// Mode widget changes — re-evaluate enabled/disabled rows
|
|
const modeW = getModeWidget(panelNode);
|
|
if (modeW) {
|
|
const origCB = modeW.callback;
|
|
modeW.callback = function (v) {
|
|
render();
|
|
if (origCB) origCB.call(this, v);
|
|
};
|
|
}
|
|
|
|
// Initial render
|
|
render();
|
|
|
|
// Cleanup on removal
|
|
const origOnRemoved = panelNode.onRemoved;
|
|
panelNode.onRemoved = function () {
|
|
if (panelNode.graph && panelNode.graph.on_change === onGraphChangeWrapper) {
|
|
panelNode.graph.on_change = prevOnChange;
|
|
}
|
|
if (origOnRemoved) origOnRemoved.call(this);
|
|
};
|
|
}
|
|
|
|
// ── extension registration ───────────────────────────────
|
|
|
|
app.registerExtension({
|
|
name: "MultiBoolPanel",
|
|
async nodeCreated(node) {
|
|
if (node.type === PANEL_TYPE || node.constructor?.type === PANEL_TYPE) {
|
|
buildPanel(node);
|
|
}
|
|
},
|
|
});
|