From f0856213ce9b3aa5353147c028e51e97e0bac66c Mon Sep 17 00:00:00 2001 From: solidlime Date: Sun, 2 Aug 2026 14:19:41 +0900 Subject: [PATCH] Fix live list updates: install graph hooks in onAdded + title-hash rename polling (new FE compat) --- web/js/multi_bool_panel.js | 102 ++++++++++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 17 deletions(-) diff --git a/web/js/multi_bool_panel.js b/web/js/multi_bool_panel.js index e2b872e..00e5c74 100644 --- a/web/js/multi_bool_panel.js +++ b/web/js/multi_bool_panel.js @@ -74,6 +74,44 @@ function applyValue(targetNode, mode, value) { app.graph.setDirtyCanvas(true, true); } +// ── rename polling (title hash) ───────────────────────── +// The new frontend renames nodes by assigning node.title directly (no +// graph.change() / onPropertyChanged fired), so renames are detected by +// polling a title hash (cg-use-everywhere approach). One shared 500ms +// interval: started when the first panel installs its hooks, stopped when +// the last panel is removed. +const panels = new Set(); +let titlesPollTimer = null; +let lastTitlesKey = ""; + +function titlesKey() { + return (app.graph?._nodes || []) + .map((n) => (n.title || "") + "\u0000" + (n.type || "")) + .join("|"); +} + +function ensurePolling() { + if (titlesPollTimer == null && panels.size > 0) { + lastTitlesKey = titlesKey(); + titlesPollTimer = setInterval(() => { + const k = titlesKey(); + if (k !== lastTitlesKey) { + lastTitlesKey = k; + panels.forEach((p) => { + if (typeof p.__scheduleRefresh === "function") p.__scheduleRefresh(); + }); + } + }, 500); + } +} + +function stopPollingIfEmpty() { + if (panels.size === 0 && titlesPollTimer != null) { + clearInterval(titlesPollTimer); + titlesPollTimer = null; + } +} + // ── panel widget builder ───────────────────────────────── function buildPanel(panelNode) { @@ -261,21 +299,50 @@ function buildPanel(panelNode) { // ── 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; + // Expose scheduleRefresh so the module-level rename poller can trigger + // refreshes on all live panels. + panelNode.__scheduleRefresh = scheduleRefresh; - // Title / property changes - const origOnPropertyChanged = panelNode.onPropertyChanged; - panelNode.onPropertyChanged = function (prop, value) { - if (prop === "title") scheduleRefresh(); - if (origOnPropertyChanged) origOnPropertyChanged.call(this, prop, value); + // nodeCreated runs inside the ComfyNode constructor, where node.graph is + // always null — so graph hooks must be installed in onAdded (fires with the + // graph argument; node.graph is set by then). Add/remove both fire + // graph.change(), so chaining graph.on_change plus onNodeAdded/onNodeRemoved + // (belt & suspenders) covers structural changes. + let prevChange = null; + let prevNodeAdded = null; + let prevNodeRemoved = null; + const onChangeWrapper = (g) => { prevChange?.(g); scheduleRefresh(); }; + const onNodeAddedWrapper = (n) => { prevNodeAdded?.(n); scheduleRefresh(); }; + const onNodeRemovedWrapper = (n) => { prevNodeRemoved?.(n); scheduleRefresh(); }; + + function installGraphHooks(g) { + if (!g || panelNode.__hooksInstalled) return; + panelNode.__hooksInstalled = true; + prevChange = g.on_change; + prevNodeAdded = g.onNodeAdded; + prevNodeRemoved = g.onNodeRemoved; + g.on_change = onChangeWrapper; + g.onNodeAdded = onNodeAddedWrapper; + g.onNodeRemoved = onNodeRemovedWrapper; + } + + function uninstallGraphHooks() { + const g = panelNode.graph; + if (!panelNode.__hooksInstalled || !g) return; + // Restore only when our wrapper is still the current handler. + if (g.on_change === onChangeWrapper) g.on_change = prevChange; + if (g.onNodeAdded === onNodeAddedWrapper) g.onNodeAdded = prevNodeAdded; + if (g.onNodeRemoved === onNodeRemovedWrapper) g.onNodeRemoved = prevNodeRemoved; + panelNode.__hooksInstalled = false; + prevChange = prevNodeAdded = prevNodeRemoved = null; + } + + const origOnAdded = panelNode.onAdded; + panelNode.onAdded = function (graph) { + installGraphHooks(graph || panelNode.graph); + panels.add(panelNode); + ensurePolling(); + if (origOnAdded) origOnAdded.call(this, graph); }; // Mode widget changes — re-evaluate enabled/disabled rows @@ -294,9 +361,10 @@ function buildPanel(panelNode) { // Cleanup on removal const origOnRemoved = panelNode.onRemoved; panelNode.onRemoved = function () { - if (panelNode.graph && panelNode.graph.on_change === onGraphChangeWrapper) { - panelNode.graph.on_change = prevOnChange; - } + uninstallGraphHooks(); + panels.delete(panelNode); + panelNode.__scheduleRefresh = null; + stopPollingIfEmpty(); if (origOnRemoved) origOnRemoved.call(this); }; }