diff --git a/web/js/multi_bool_panel.js b/web/js/multi_bool_panel.js index 00e5c74..5704165 100644 --- a/web/js/multi_bool_panel.js +++ b/web/js/multi_bool_panel.js @@ -74,41 +74,63 @@ 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. +// ── canvas.setDirty hook (rename detection) ────────────── +// The new frontend renames nodes by assigning node.title directly and only +// calls canvas.setDirty — neither graph.change() nor onPropertyChanged fire. +// Wrapping canvas.setDirty (installed once) is the only observable signal +// for label changes; panels then re-render only when their own relevant key +// actually changed. Add/remove are already covered by graph hooks. const panels = new Set(); -let titlesPollTimer = null; -let lastTitlesKey = ""; +let panelsCheckTimer = null; +const PANELS_CHECK_MS = 150; -function titlesKey() { - return (app.graph?._nodes || []) - .map((n) => (n.title || "") + "\u0000" + (n.type || "")) - .join("|"); +function relevantKey(panel) { + const mode = getPanelMode(panel); + const names = resolveTargets(panel) + .map((t) => itemName(panel, t).toLowerCase()) + .sort(); + return (panel.title || "") + "\u0000" + mode + "\u0000" + names.join("\u0000"); } -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 checkPanels() { + panels.forEach((p) => { + if (typeof p.__scheduleRefresh !== "function") return; + const k = relevantKey(p); + if (k !== p.__mbpLastKey) { + p.__mbpLastKey = k; + p.__scheduleRefresh(); + } + }); } -function stopPollingIfEmpty() { - if (panels.size === 0 && titlesPollTimer != null) { - clearInterval(titlesPollTimer); - titlesPollTimer = null; +function schedulePanelsCheck() { + if (panelsCheckTimer) clearTimeout(panelsCheckTimer); + panelsCheckTimer = setTimeout(() => { + panelsCheckTimer = null; + checkPanels(); + }, PANELS_CHECK_MS); +} + +function mbpSetDirtyWrapper(fg, bg) { + const r = this.__mbpPrevSetDirty.call(this, fg, bg); + schedulePanelsCheck(); + return r; +} + +function installCanvasHook(canvas) { + if (!canvas || canvas.__mbpHooked) return; + canvas.__mbpPrevSetDirty = canvas.setDirty; + canvas.setDirty = mbpSetDirtyWrapper; + canvas.__mbpHooked = true; +} + +function uninstallCanvasHook() { + if (panels.size > 0) return; + const canvas = app.canvas; + if (canvas && canvas.__mbpHooked && canvas.setDirty === mbpSetDirtyWrapper) { + canvas.setDirty = canvas.__mbpPrevSetDirty; + canvas.__mbpPrevSetDirty = null; + canvas.__mbpHooked = false; } } @@ -262,8 +284,12 @@ function buildPanel(panelNode) { 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); + // Skip resize/redraw when the height is unchanged (avoids flicker from + // redundant setDirtyCanvas on every render pass). + if (panelNode.size[1] !== rowsHeight) { + panelNode.size = [panelNode.size[0], rowsHeight]; + panelNode.setDirtyCanvas(true, true); + } } // ── toggle element factory ──────────────────────────── @@ -299,8 +325,8 @@ function buildPanel(panelNode) { // ── event hooks ─────────────────────────────────────── - // Expose scheduleRefresh so the module-level rename poller can trigger - // refreshes on all live panels. + // Expose scheduleRefresh so checkPanels() (driven by the canvas.setDirty + // hook) can trigger refreshes on this panel. panelNode.__scheduleRefresh = scheduleRefresh; // nodeCreated runs inside the ComfyNode constructor, where node.graph is @@ -341,7 +367,7 @@ function buildPanel(panelNode) { panelNode.onAdded = function (graph) { installGraphHooks(graph || panelNode.graph); panels.add(panelNode); - ensurePolling(); + installCanvasHook(app.canvas); if (origOnAdded) origOnAdded.call(this, graph); }; @@ -357,6 +383,8 @@ function buildPanel(panelNode) { // Initial render render(); + // Baseline for change detection so the first relevant change re-renders. + panelNode.__mbpLastKey = relevantKey(panelNode); // Cleanup on removal const origOnRemoved = panelNode.onRemoved; @@ -364,7 +392,8 @@ function buildPanel(panelNode) { uninstallGraphHooks(); panels.delete(panelNode); panelNode.__scheduleRefresh = null; - stopPollingIfEmpty(); + panelNode.__mbpLastKey = null; + uninstallCanvasHook(); if (origOnRemoved) origOnRemoved.call(this); }; }