From 9f1e35aa2bf79b02cd8441f06cfbeaa4c4fed741 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Fri, 14 Aug 2026 22:50:07 +0200 Subject: [PATCH] Preserve ComfyUI selection across workflow overview toggle --- js/app/canvas_view.js | 101 ++++++++++++++++++++++++++++++++++ src/app/canvas_view.ts | 121 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) diff --git a/js/app/canvas_view.js b/js/app/canvas_view.js index 8fb0322..4719f50 100644 --- a/js/app/canvas_view.js +++ b/js/app/canvas_view.js @@ -1727,8 +1727,75 @@ async function createCanvasWidget(node, widget, _app) { let originalParent = null; let isEditorOpen = false; let viewportAdjustment = { x: 0, y: 0 }; + let workflowOverviewSelectionSnapshot = null; + let workflowOverviewSelectionCleared = false; const workflowOverviewPanelSelector = '[data-testid="properties-panel"]'; + const workflowOverviewNodesTabSelector = workflowOverviewPanelSelector + ' [data-testid="panel-tab-nodes"]'; const nativeWorkflowOverviewToggleSelector = 'button[aria-label="Toggle properties panel"]:not(.lf-workflow-overview-toggle)'; + const getComfyCanvas = () => _app?.canvas ?? app?.canvas; + const getComfySelectedItems = (comfyCanvas) => { + const selectedItems = comfyCanvas?.selectedItems; + if (selectedItems && typeof selectedItems[Symbol.iterator] === "function") { + return Array.from(selectedItems); + } + const selectedNodes = comfyCanvas?.selected_nodes; + if (selectedNodes && typeof selectedNodes === "object") { + return Object.values(selectedNodes); + } + return []; + }; + const notifyComfySelectionChanged = (comfyCanvas) => { + comfyCanvas?.onSelectionChange?.(comfyCanvas.selected_nodes ?? {}); + comfyCanvas?.setDirty?.(true, true); + }; + const clearWorkflowOverviewSelection = () => { + if (workflowOverviewSelectionCleared) { + return; + } + const comfyCanvas = getComfyCanvas(); + if (!comfyCanvas) { + return; + } + workflowOverviewSelectionSnapshot = getComfySelectedItems(comfyCanvas); + if (typeof comfyCanvas.deselectAll === "function") { + comfyCanvas.deselectAll(); + } + else if (comfyCanvas.selectedItems?.clear) { + for (const item of workflowOverviewSelectionSnapshot) { + item.selected = false; + item.onDeselected?.(); + } + comfyCanvas.selectedItems.clear(); + comfyCanvas.selected_nodes = {}; + notifyComfySelectionChanged(comfyCanvas); + } + workflowOverviewSelectionCleared = true; + }; + const restoreWorkflowOverviewSelection = () => { + if (!workflowOverviewSelectionCleared) { + return; + } + const comfyCanvas = getComfyCanvas(); + const selection = workflowOverviewSelectionSnapshot ?? []; + if (comfyCanvas) { + if (typeof comfyCanvas.selectNodes === "function") { + comfyCanvas.selectNodes(selection); + } + else if (typeof comfyCanvas.selectItems === "function") { + comfyCanvas.selectItems(selection); + } + else if (comfyCanvas.selectedItems?.clear) { + comfyCanvas.selectedItems.clear(); + for (const item of selection) { + item.selected = true; + comfyCanvas.selectedItems.add(item); + } + notifyComfySelectionChanged(comfyCanvas); + } + } + workflowOverviewSelectionSnapshot = null; + workflowOverviewSelectionCleared = false; + }; const isVisibleElement = (element) => { if (!(element instanceof HTMLElement)) { return false; @@ -1907,6 +1974,27 @@ async function createCanvasWidget(node, widget, _app) { checkState(); }); }; + const waitForWorkflowOverviewNodesTab = () => { + return new Promise((resolve) => { + const deadline = performance.now() + 1000; + const selectNodesTab = () => { + if (!isEditorOpen || performance.now() >= deadline) { + resolve(); + return; + } + const nodesTab = document.querySelector(workflowOverviewNodesTabSelector); + if (isVisibleElement(nodesTab)) { + if (nodesTab.getAttribute("aria-selected") !== "true") { + nodesTab.click(); + } + resolve(); + return; + } + window.requestAnimationFrame(selectNodesTab); + }; + selectNodesTab(); + }); + }; const toggleWorkflowOverview = async () => { const requestId = ++workflowOverviewToggleRequest; const nativeToggle = findNativeWorkflowOverviewToggle(); @@ -1915,11 +2003,17 @@ async function createCanvasWidget(node, widget, _app) { return; } const expectedState = !isWorkflowOverviewOpen(); + if (expectedState) { + clearWorkflowOverviewSelection(); + } nativeToggle.click(); await waitForWorkflowOverviewState(expectedState); if (!isEditorOpen || requestId !== workflowOverviewToggleRequest) { return; } + if (expectedState) { + await waitForWorkflowOverviewNodesTab(); + } attachWorkflowOverviewResizeObserver(); applyWorkflowOverviewLayout(); }; @@ -1960,6 +2054,7 @@ async function createCanvasWidget(node, widget, _app) { backdrop.parentNode.removeChild(backdrop); } isEditorOpen = false; + restoreWorkflowOverviewSelection(); modalContent = null; workflowOverviewToggleButton = null; openEditorBtn.textContent = "⛶"; @@ -1993,6 +2088,7 @@ async function createCanvasWidget(node, widget, _app) { log.error("Could not find original parent of the canvas container!"); return; } + clearWorkflowOverviewSelection(); backdrop = $el("div.lf-painter-modal-backdrop"); modalContent = $el("div.lf-painter-modal-content"); workflowOverviewToggleButton = document.createElement("button"); @@ -2016,6 +2112,9 @@ async function createCanvasWidget(node, widget, _app) { isEditorOpen = true; applyWorkflowOverviewLayout(); startWorkflowOverviewLayoutTracking(); + if (isWorkflowOverviewOpen()) { + void waitForWorkflowOverviewNodesTab(); + } openEditorBtn.textContent = "X"; tooltipManager.setTooltip(openEditorBtn, "Close Editor (ESC)"); // Add ESC key listener when editor opens @@ -2073,6 +2172,8 @@ async function createCanvasWidget(node, widget, _app) { backdrop.parentNode.removeChild(backdrop); } isEditorOpen = false; + workflowOverviewSelectionSnapshot = null; + workflowOverviewSelectionCleared = false; modalContent = null; workflowOverviewToggleButton = null; } diff --git a/src/app/canvas_view.ts b/src/app/canvas_view.ts index 173ea31..392f2eb 100644 --- a/src/app/canvas_view.ts +++ b/src/app/canvas_view.ts @@ -2003,11 +2003,90 @@ $el("label.lf-clipboard-switch.lf-mask-switch", { let originalParent: HTMLElement | null = null; let isEditorOpen = false; let viewportAdjustment = { x: 0, y: 0 }; + let workflowOverviewSelectionSnapshot: any[] | null = null; + let workflowOverviewSelectionCleared = false; const workflowOverviewPanelSelector = '[data-testid="properties-panel"]'; + const workflowOverviewNodesTabSelector = + workflowOverviewPanelSelector + ' [data-testid="panel-tab-nodes"]'; const nativeWorkflowOverviewToggleSelector = 'button[aria-label="Toggle properties panel"]:not(.lf-workflow-overview-toggle)'; + const getComfyCanvas = (): any => (_app as any)?.canvas ?? (app as any)?.canvas; + + const getComfySelectedItems = (comfyCanvas: any): any[] => { + const selectedItems = comfyCanvas?.selectedItems; + if (selectedItems && typeof selectedItems[Symbol.iterator] === "function") { + return Array.from(selectedItems); + } + + const selectedNodes = comfyCanvas?.selected_nodes; + if (selectedNodes && typeof selectedNodes === "object") { + return Object.values(selectedNodes); + } + + return []; + }; + + const notifyComfySelectionChanged = (comfyCanvas: any) => { + comfyCanvas?.onSelectionChange?.(comfyCanvas.selected_nodes ?? {}); + comfyCanvas?.setDirty?.(true, true); + }; + + const clearWorkflowOverviewSelection = () => { + if (workflowOverviewSelectionCleared) { + return; + } + + const comfyCanvas = getComfyCanvas(); + if (!comfyCanvas) { + return; + } + + workflowOverviewSelectionSnapshot = getComfySelectedItems(comfyCanvas); + + if (typeof comfyCanvas.deselectAll === "function") { + comfyCanvas.deselectAll(); + } else if (comfyCanvas.selectedItems?.clear) { + for (const item of workflowOverviewSelectionSnapshot) { + item.selected = false; + item.onDeselected?.(); + } + comfyCanvas.selectedItems.clear(); + comfyCanvas.selected_nodes = {}; + notifyComfySelectionChanged(comfyCanvas); + } + + workflowOverviewSelectionCleared = true; + }; + + const restoreWorkflowOverviewSelection = () => { + if (!workflowOverviewSelectionCleared) { + return; + } + + const comfyCanvas = getComfyCanvas(); + const selection = workflowOverviewSelectionSnapshot ?? []; + + if (comfyCanvas) { + if (typeof comfyCanvas.selectNodes === "function") { + comfyCanvas.selectNodes(selection); + } else if (typeof comfyCanvas.selectItems === "function") { + comfyCanvas.selectItems(selection); + } else if (comfyCanvas.selectedItems?.clear) { + comfyCanvas.selectedItems.clear(); + for (const item of selection) { + item.selected = true; + comfyCanvas.selectedItems.add(item); + } + notifyComfySelectionChanged(comfyCanvas); + } + } + + workflowOverviewSelectionSnapshot = null; + workflowOverviewSelectionCleared = false; + }; + const isVisibleElement = (element: Element | null): element is HTMLElement => { if (!(element instanceof HTMLElement)) { return false; @@ -2233,6 +2312,33 @@ $el("label.lf-clipboard-switch.lf-mask-switch", { }); }; + const waitForWorkflowOverviewNodesTab = (): Promise => { + return new Promise((resolve) => { + const deadline = performance.now() + 1000; + const selectNodesTab = () => { + if (!isEditorOpen || performance.now() >= deadline) { + resolve(); + return; + } + + const nodesTab = document.querySelector( + workflowOverviewNodesTabSelector + ); + if (isVisibleElement(nodesTab)) { + if (nodesTab.getAttribute("aria-selected") !== "true") { + nodesTab.click(); + } + resolve(); + return; + } + + window.requestAnimationFrame(selectNodesTab); + }; + + selectNodesTab(); + }); + }; + const toggleWorkflowOverview = async () => { const requestId = ++workflowOverviewToggleRequest; const nativeToggle = findNativeWorkflowOverviewToggle(); @@ -2242,6 +2348,9 @@ $el("label.lf-clipboard-switch.lf-mask-switch", { } const expectedState = !isWorkflowOverviewOpen(); + if (expectedState) { + clearWorkflowOverviewSelection(); + } nativeToggle.click(); await waitForWorkflowOverviewState(expectedState); @@ -2249,6 +2358,10 @@ $el("label.lf-clipboard-switch.lf-mask-switch", { return; } + if (expectedState) { + await waitForWorkflowOverviewNodesTab(); + } + attachWorkflowOverviewResizeObserver(); applyWorkflowOverviewLayout(); }; @@ -2300,6 +2413,7 @@ $el("label.lf-clipboard-switch.lf-mask-switch", { } isEditorOpen = false; + restoreWorkflowOverviewSelection(); modalContent = null; workflowOverviewToggleButton = null; openEditorBtn.textContent = "⛶"; @@ -2340,6 +2454,8 @@ $el("label.lf-clipboard-switch.lf-mask-switch", { return; } + clearWorkflowOverviewSelection(); + backdrop = $el("div.lf-painter-modal-backdrop") as HTMLDivElement; modalContent = $el("div.lf-painter-modal-content") as HTMLDivElement; @@ -2367,6 +2483,9 @@ $el("label.lf-clipboard-switch.lf-mask-switch", { isEditorOpen = true; applyWorkflowOverviewLayout(); startWorkflowOverviewLayoutTracking(); + if (isWorkflowOverviewOpen()) { + void waitForWorkflowOverviewNodesTab(); + } openEditorBtn.textContent = "X"; tooltipManager.setTooltip(openEditorBtn, "Close Editor (ESC)"); @@ -2436,6 +2555,8 @@ $el("label.lf-clipboard-switch.lf-mask-switch", { backdrop.parentNode.removeChild(backdrop); } isEditorOpen = false; + workflowOverviewSelectionSnapshot = null; + workflowOverviewSelectionCleared = false; modalContent = null; workflowOverviewToggleButton = null; }