diff --git a/src/info-panel/InfoPanel.ts b/src/info-panel/InfoPanel.ts index d3407b9..d6fd81c 100644 --- a/src/info-panel/InfoPanel.ts +++ b/src/info-panel/InfoPanel.ts @@ -63,6 +63,10 @@ export class InfoPanel { this.magnifyGlass.popOutManager.onStateChange = (isOpen: boolean) => { this.uiManager.updateControlStates(); }; + // Listen for node selection from popout + this.magnifyGlass.popOutManager.onNodeSelect = (nodeId: number) => { + this.onNodeSelected(nodeId); + }; } // Set up node selection callback diff --git a/src/magnify-glass/PopOutManager.ts b/src/magnify-glass/PopOutManager.ts index 54e06bf..3865513 100644 --- a/src/magnify-glass/PopOutManager.ts +++ b/src/magnify-glass/PopOutManager.ts @@ -12,11 +12,19 @@ import { Logger } from '../shared/logger'; * Message types for BroadcastChannel communication */ interface PopOutMessage { - type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong'; - data?: string | Partial | PopOutInfo; + type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong' | 'node-select' | 'nodes-list' | 'request-nodes'; + data?: string | Partial | PopOutInfo | NodeListData | number; timestamp?: number; } +/** + * Node list data sent to popout for dropdown + */ +interface NodeListData { + nodes: { id: number; title: string; type: string; order?: number }[]; + sortBy: 'title' | 'execOrder'; +} + interface PopOutConfig { glassSize: number; borderColor: string; @@ -68,6 +76,7 @@ export class PopOutManager { private isOpen: boolean = false; private popOutWindow: Window | null = null; public onStateChange: ((isOpen: boolean) => void) | null = null; + public onNodeSelect: ((nodeId: number) => void) | null = null; private lastPongTime: number = 0; private pingInterval: number | null = null; private viewerUrl: string; @@ -92,7 +101,7 @@ export class PopOutManager { */ private getViewerUrl(): string { // Cache-busting version - increment to force refresh - const version = 'v20'; + const version = 'v21'; // Find the extension's base URL from the loaded scripts const scripts = document.querySelectorAll('script[src*="magnify"]'); @@ -168,6 +177,17 @@ export class PopOutManager { Logger.debug('[PopOut] Viewer tab closed'); if (this.onStateChange) this.onStateChange(false); break; + case 'request-nodes': + // Popout is requesting the node list for dropdown + Logger.debug('[PopOut] Received request-nodes:', message.data); + this.handleNodeListRequest(message.data as 'title' | 'execOrder'); + break; + case 'node-select': + // Popout selected a node, notify main window + if (this.onNodeSelect && typeof message.data === 'number') { + this.onNodeSelect(message.data); + } + break; } } @@ -316,6 +336,51 @@ export class PopOutManager { }); } + /** + * Handle node list request from popout viewer. + * Fetches nodes from canvas and sends them to the popout. + */ + private handleNodeListRequest(sortBy: 'title' | 'execOrder'): void { + if (!this.channel) return; + + try { + // Access nodes from ComfyUI's app object + const app = (window as any).app; + const nodes = app?.graph?._nodes || []; + + let nodeList: { id: number; title: string; type: string; order?: number }[]; + + if (sortBy === 'execOrder') { + nodeList = nodes + .map((n: any) => ({ + id: n.id, + title: n.title || 'Untitled', + type: n.type || 'Unknown', + order: n.order ?? -1 + })) + .filter((n: any) => n.order >= 0) + .sort((a: any, b: any) => a.order - b.order); + } else { + nodeList = nodes + .map((n: any) => ({ + id: n.id, + title: n.title || 'Untitled', + type: n.type || 'Unknown' + })) + .sort((a: any, b: any) => a.title.localeCompare(b.title)); + } + + this.sendMessage({ + type: 'nodes-list', + data: { nodes: nodeList, sortBy } + }); + + Logger.debug(`[PopOut] Sent ${nodeList.length} nodes to viewer (sorted by ${sortBy})`); + } catch (e) { + Logger.error('[PopOut] Failed to get node list:', e); + } + } + /** * Sanitize info object for BroadcastChannel transfer. * Removes functions, circular references, and non-serializable data. diff --git a/src/types/comfyui.d.ts b/src/types/comfyui.d.ts index 128d212..f0095b7 100644 --- a/src/types/comfyui.d.ts +++ b/src/types/comfyui.d.ts @@ -214,6 +214,7 @@ export interface MagnifyGlassInstance { sendInfo(info: unknown): void; toggle(): void; onStateChange?: ((isOpen: boolean) => void) | null; + onNodeSelect?: ((nodeId: number) => void) | null; updateTheme?(theme: string): void; }; lastKnownMousePosition: { x: number; y: number }; diff --git a/web/info-panel/InfoPanel.js b/web/info-panel/InfoPanel.js index 00e544e..ff94941 100644 --- a/web/info-panel/InfoPanel.js +++ b/web/info-panel/InfoPanel.js @@ -44,6 +44,9 @@ class InfoPanel { this.magnifyGlass.popOutManager.onStateChange = (isOpen) => { this.uiManager.updateControlStates(); }; + this.magnifyGlass.popOutManager.onNodeSelect = (nodeId) => { + this.onNodeSelected(nodeId); + }; } this.uiManager.onNodeSelected = (nodeId) => { this.onNodeSelected(nodeId); diff --git a/web/info-panel/InfoPanel.js.map b/web/info-panel/InfoPanel.js.map index cc9486e..6334957 100644 --- a/web/info-panel/InfoPanel.js.map +++ b/web/info-panel/InfoPanel.js.map @@ -1 +1 @@ -{"version":3,"file":"InfoPanel.js","sources":["../../src/info-panel/InfoPanel.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel Orchestrator (TypeScript)\n * \n * Complete Info Panel Manager extracted from magnify_info_panel.js\n * Main class that orchestrates all info panel components.\n */\n\nimport type { MagnifyGlassInstance } from '../types/comfyui';\nimport { registerPanelSettings } from '../shared/settings';\nimport { Logger } from '../shared/logger';\nimport { StateManager } from './StateManager';\nimport { UIManager } from './UIManager';\nimport { PositionManager } from './PositionManager';\nimport { EventManager } from './EventManager';\nimport { InformationGatherer } from './InformationGatherer';\n\n/**\n * Professional Info Panel Manager.\n * Orchestrates all the other components for the info panel.\n */\nexport class InfoPanel {\n magnifyGlass: MagnifyGlassInstance;\n stateManager: StateManager;\n uiManager: UIManager;\n positionManager: PositionManager;\n eventManager: EventManager;\n informationGatherer: InformationGatherer;\n\n constructor(magnifyGlass: MagnifyGlassInstance) {\n this.magnifyGlass = magnifyGlass;\n\n // Initialize Managers\n this.stateManager = new StateManager();\n this.uiManager = new UIManager(this.stateManager);\n this.informationGatherer = new InformationGatherer();\n\n // Position Manager needs the UI elements\n this.positionManager = new PositionManager(this.stateManager, this.uiManager.elements.panel!);\n\n // Event Manager needs state and UI manager\n this.eventManager = new EventManager(this.stateManager, this.uiManager.elements.panel!, this.positionManager, {\n toggleVisibility: () => {\n if (this.stateManager.state.isPanelVisible) {\n this.uiManager.hide();\n } else {\n this.uiManager.show();\n }\n },\n togglePin: () => {\n this.stateManager.togglePinning();\n this.uiManager.updatePinnedState();\n },\n updatePosition: () => this.positionManager.positionPanel()\n });\n\n this.hookIntoMagnifyGlass();\n\n // Register settings with ComfyUI\n registerPanelSettings(this.stateManager, this.uiManager, this.positionManager);\n\n // Listen for popout state changes to update UI buttons\n if (this.magnifyGlass.popOutManager) {\n this.magnifyGlass.popOutManager.onStateChange = (isOpen: boolean) => {\n this.uiManager.updateControlStates();\n };\n }\n\n // Set up node selection callback\n this.uiManager.onNodeSelected = (nodeId: number) => {\n this.onNodeSelected(nodeId);\n };\n\n Logger.info('Info Panel initialized successfully');\n }\n\n hookIntoMagnifyGlass(): void {\n // Hook into magnify glass update cycle\n const originalUpdateMagnifiedView = this.magnifyGlass.updateMagnifiedView.bind(this.magnifyGlass);\n\n this.magnifyGlass.updateMagnifiedView = (() => {\n originalUpdateMagnifiedView();\n\n // Schedule info update if panel is enabled and glass is active\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] &&\n this.magnifyGlass.state.active) {\n this.scheduleInfoUpdate();\n }\n }).bind(this);\n\n // Hook into show/hide\n const originalShow = this.magnifyGlass.ui.show.bind(this.magnifyGlass.ui);\n const originalHide = this.magnifyGlass.ui.hide.bind(this.magnifyGlass.ui);\n\n this.magnifyGlass.ui.show = (() => {\n originalShow();\n\n // CRITICAL: Only restore panel if the magnify glass is actually active\n // This prevents the panel from showing when toggle() is switching it OFF\n if (!this.magnifyGlass.state.active) {\n return;\n }\n\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"]) {\n // Restore state: If it was visible before hide, make it visible now.\n // Otherwise only show controls.\n if (this.stateManager.state.wasPanelVisibleBeforeHide) {\n this.uiManager.show();\n\n // Position properly\n setTimeout(() => {\n this.positionManager.positionPanel();\n this.positionManager.positionFloatingControls(this.uiManager.elements.controls);\n }, 10);\n } else {\n // Do NOT force panel visible. Only show controls.\n const showControls = this.stateManager.state.settings[\"🔍MagnifyGlass.ShowHoveringControls\"] !== false;\n if (showControls && this.uiManager.elements.controls) {\n this.uiManager.elements.controls.style.display = 'flex';\n }\n\n // Ensure states are updated\n this.uiManager.updateControlStates();\n\n // Position controls after showing (relative to glass since panel is hidden)\n setTimeout(() => {\n this.positionManager.positionFloatingControls(this.uiManager.elements.controls);\n }, 10);\n }\n }\n }).bind(this);\n\n this.magnifyGlass.ui.hide = (() => {\n // Save state before hiding\n this.stateManager.state.wasPanelVisibleBeforeHide = this.stateManager.state.isPanelVisible;\n\n originalHide();\n this.uiManager.hide();\n\n // FORCE HIDE: Directly set panel display to none as a safety measure\n if (this.uiManager.elements.panel) {\n this.uiManager.elements.panel.style.display = 'none';\n }\n\n // Explicitly hide floating controls when glass is disabled\n if (this.uiManager.elements.controls) {\n this.uiManager.elements.controls.style.display = 'none';\n }\n }).bind(this);\n }\n\n scheduleInfoUpdate(): void {\n if (this.stateManager.state.updateScheduled) return;\n\n this.stateManager.state.updateScheduled = true;\n requestAnimationFrame(() => {\n this.updateInfo();\n this.stateManager.state.updateScheduled = false;\n });\n }\n\n // State for persistence\n private lastValidNodeInfo: any = null;\n\n updateInfo(): void {\n const settings = this.stateManager.state.settings;\n const isActive = this.magnifyGlass.state.active;\n\n // Safety: If disabled or inactive, ensure hidden and return\n if (!settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] || !isActive) {\n // Hide the panel completely when the glass is inactive\n this.uiManager.hide();\n if (this.uiManager.elements.controls && this.uiManager.elements.controls.style.display !== 'none') {\n this.uiManager.elements.controls.style.display = 'none';\n }\n return;\n }\n\n // Logic for \"Hold Info\" (Freeze)\n if (this.stateManager.state.isInfoHeld) {\n // If held, we skip gathering new info and skip updating the display content.\n // But we MUST still update positioning to keep the UI responsive to movement.\n this.positionManager.positionPanel();\n this.positionManager.positionFloatingControls(this.uiManager.elements.controls);\n return;\n }\n\n let info = this.informationGatherer.gatherInformation();\n\n // NODE SELECTION OVERRIDE - check if user has selected a specific node\n const selectedNodeId = this.stateManager.state.selectedNodeId;\n if (selectedNodeId !== null) {\n const selectedNode = this.uiManager.nodeSelector.getNodeById(selectedNodeId);\n if (selectedNode) {\n // Override with selected node info\n const detailedInfo = this.informationGatherer.getDetailedNodeInfo(\n selectedNode,\n { x: 0, y: 0 } // No local position when manually selected\n );\n info = {\n ...info,\n hoveredNode: detailedInfo,\n hoveredWidget: null\n };\n }\n }\n // END NODE SELECTION OVERRIDE\n\n // PERSISTENCE LOGIC START\n if (info.hoveredNode) {\n // We have a valid node, update our cache\n this.lastValidNodeInfo = info;\n } else if (settings[\"🔍MagnifyGlass.InfoPanelPersist\"] && this.lastValidNodeInfo) {\n // No node hovered, but persistence is ON and we have a cache.\n // Merge the CACHED node info into the CURRENT info (so we keep current cursor pos/zoom but show old node)\n info = {\n ...info, // Current timestamp, cursor, zoom\n hoveredNode: this.lastValidNodeInfo.hoveredNode,\n hoveredWidget: this.lastValidNodeInfo.hoveredWidget\n };\n }\n // PERSISTENCE LOGIC END\n\n this.stateManager.setCurrentInfo(info);\n\n // Update the UI\n this.uiManager.displayInfo(info);\n\n // Send info to pop-out viewer if active\n if (this.magnifyGlass.popOutManager && this.magnifyGlass.popOutManager.isPopOutOpen()) {\n this.magnifyGlass.popOutManager.sendInfo(info as any);\n }\n\n this.positionManager.positionPanel();\n this.positionManager.positionFloatingControls(this.uiManager.elements.controls);\n\n // Handle node hover state logic if needed for internal state\n if (info.hoveredNode) {\n this.stateManager.state.isHoveringNode = true;\n this.stateManager.expandNodeSections();\n if (this.stateManager.state.lastNodeId !== info.hoveredNode.id) {\n this.stateManager.state.lastNodeId = info.hoveredNode.id;\n }\n } else {\n this.stateManager.state.isHoveringNode = false;\n // Only collapse if we are NOT persisting\n if (!settings[\"🔍MagnifyGlass.InfoPanelPersist\"]) {\n if (this.stateManager.state.lastNodeId !== null) {\n this.stateManager.state.lastNodeId = null;\n if (!this.stateManager.state.isPanelHovered) {\n this.stateManager.scheduleAutoCollapse();\n }\n }\n }\n }\n }\n\n updateSettings(): void {\n const changes = this.stateManager.updateSettings();\n this.uiManager.applyStyles();\n\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] && this.magnifyGlass.state.active) {\n this.uiManager.show();\n // Ensure position is updated after potential dimension changes\n this.positionManager.positionPanel();\n } else {\n this.uiManager.hide();\n }\n }\n\n /**\n * Handle node selection from dropdown.\n * @param nodeId - ID of the selected node\n */\n onNodeSelected(nodeId: number): void {\n Logger.debug(`Node selected from dropdown: ${nodeId}`);\n\n // Force an immediate info update\n this.updateInfo();\n }\n\n /**\n * Clear the selected node, returning to hover-based detection.\n */\n clearSelectedNode(): void {\n this.stateManager.clearSelectedNode();\n this.updateInfo();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"InfoPanel.js","sources":["../../src/info-panel/InfoPanel.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel Orchestrator (TypeScript)\n * \n * Complete Info Panel Manager extracted from magnify_info_panel.js\n * Main class that orchestrates all info panel components.\n */\n\nimport type { MagnifyGlassInstance } from '../types/comfyui';\nimport { registerPanelSettings } from '../shared/settings';\nimport { Logger } from '../shared/logger';\nimport { StateManager } from './StateManager';\nimport { UIManager } from './UIManager';\nimport { PositionManager } from './PositionManager';\nimport { EventManager } from './EventManager';\nimport { InformationGatherer } from './InformationGatherer';\n\n/**\n * Professional Info Panel Manager.\n * Orchestrates all the other components for the info panel.\n */\nexport class InfoPanel {\n magnifyGlass: MagnifyGlassInstance;\n stateManager: StateManager;\n uiManager: UIManager;\n positionManager: PositionManager;\n eventManager: EventManager;\n informationGatherer: InformationGatherer;\n\n constructor(magnifyGlass: MagnifyGlassInstance) {\n this.magnifyGlass = magnifyGlass;\n\n // Initialize Managers\n this.stateManager = new StateManager();\n this.uiManager = new UIManager(this.stateManager);\n this.informationGatherer = new InformationGatherer();\n\n // Position Manager needs the UI elements\n this.positionManager = new PositionManager(this.stateManager, this.uiManager.elements.panel!);\n\n // Event Manager needs state and UI manager\n this.eventManager = new EventManager(this.stateManager, this.uiManager.elements.panel!, this.positionManager, {\n toggleVisibility: () => {\n if (this.stateManager.state.isPanelVisible) {\n this.uiManager.hide();\n } else {\n this.uiManager.show();\n }\n },\n togglePin: () => {\n this.stateManager.togglePinning();\n this.uiManager.updatePinnedState();\n },\n updatePosition: () => this.positionManager.positionPanel()\n });\n\n this.hookIntoMagnifyGlass();\n\n // Register settings with ComfyUI\n registerPanelSettings(this.stateManager, this.uiManager, this.positionManager);\n\n // Listen for popout state changes to update UI buttons\n if (this.magnifyGlass.popOutManager) {\n this.magnifyGlass.popOutManager.onStateChange = (isOpen: boolean) => {\n this.uiManager.updateControlStates();\n };\n // Listen for node selection from popout\n this.magnifyGlass.popOutManager.onNodeSelect = (nodeId: number) => {\n this.onNodeSelected(nodeId);\n };\n }\n\n // Set up node selection callback\n this.uiManager.onNodeSelected = (nodeId: number) => {\n this.onNodeSelected(nodeId);\n };\n\n Logger.info('Info Panel initialized successfully');\n }\n\n hookIntoMagnifyGlass(): void {\n // Hook into magnify glass update cycle\n const originalUpdateMagnifiedView = this.magnifyGlass.updateMagnifiedView.bind(this.magnifyGlass);\n\n this.magnifyGlass.updateMagnifiedView = (() => {\n originalUpdateMagnifiedView();\n\n // Schedule info update if panel is enabled and glass is active\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] &&\n this.magnifyGlass.state.active) {\n this.scheduleInfoUpdate();\n }\n }).bind(this);\n\n // Hook into show/hide\n const originalShow = this.magnifyGlass.ui.show.bind(this.magnifyGlass.ui);\n const originalHide = this.magnifyGlass.ui.hide.bind(this.magnifyGlass.ui);\n\n this.magnifyGlass.ui.show = (() => {\n originalShow();\n\n // CRITICAL: Only restore panel if the magnify glass is actually active\n // This prevents the panel from showing when toggle() is switching it OFF\n if (!this.magnifyGlass.state.active) {\n return;\n }\n\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"]) {\n // Restore state: If it was visible before hide, make it visible now.\n // Otherwise only show controls.\n if (this.stateManager.state.wasPanelVisibleBeforeHide) {\n this.uiManager.show();\n\n // Position properly\n setTimeout(() => {\n this.positionManager.positionPanel();\n this.positionManager.positionFloatingControls(this.uiManager.elements.controls);\n }, 10);\n } else {\n // Do NOT force panel visible. Only show controls.\n const showControls = this.stateManager.state.settings[\"🔍MagnifyGlass.ShowHoveringControls\"] !== false;\n if (showControls && this.uiManager.elements.controls) {\n this.uiManager.elements.controls.style.display = 'flex';\n }\n\n // Ensure states are updated\n this.uiManager.updateControlStates();\n\n // Position controls after showing (relative to glass since panel is hidden)\n setTimeout(() => {\n this.positionManager.positionFloatingControls(this.uiManager.elements.controls);\n }, 10);\n }\n }\n }).bind(this);\n\n this.magnifyGlass.ui.hide = (() => {\n // Save state before hiding\n this.stateManager.state.wasPanelVisibleBeforeHide = this.stateManager.state.isPanelVisible;\n\n originalHide();\n this.uiManager.hide();\n\n // FORCE HIDE: Directly set panel display to none as a safety measure\n if (this.uiManager.elements.panel) {\n this.uiManager.elements.panel.style.display = 'none';\n }\n\n // Explicitly hide floating controls when glass is disabled\n if (this.uiManager.elements.controls) {\n this.uiManager.elements.controls.style.display = 'none';\n }\n }).bind(this);\n }\n\n scheduleInfoUpdate(): void {\n if (this.stateManager.state.updateScheduled) return;\n\n this.stateManager.state.updateScheduled = true;\n requestAnimationFrame(() => {\n this.updateInfo();\n this.stateManager.state.updateScheduled = false;\n });\n }\n\n // State for persistence\n private lastValidNodeInfo: any = null;\n\n updateInfo(): void {\n const settings = this.stateManager.state.settings;\n const isActive = this.magnifyGlass.state.active;\n\n // Safety: If disabled or inactive, ensure hidden and return\n if (!settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] || !isActive) {\n // Hide the panel completely when the glass is inactive\n this.uiManager.hide();\n if (this.uiManager.elements.controls && this.uiManager.elements.controls.style.display !== 'none') {\n this.uiManager.elements.controls.style.display = 'none';\n }\n return;\n }\n\n // Logic for \"Hold Info\" (Freeze)\n if (this.stateManager.state.isInfoHeld) {\n // If held, we skip gathering new info and skip updating the display content.\n // But we MUST still update positioning to keep the UI responsive to movement.\n this.positionManager.positionPanel();\n this.positionManager.positionFloatingControls(this.uiManager.elements.controls);\n return;\n }\n\n let info = this.informationGatherer.gatherInformation();\n\n // NODE SELECTION OVERRIDE - check if user has selected a specific node\n const selectedNodeId = this.stateManager.state.selectedNodeId;\n if (selectedNodeId !== null) {\n const selectedNode = this.uiManager.nodeSelector.getNodeById(selectedNodeId);\n if (selectedNode) {\n // Override with selected node info\n const detailedInfo = this.informationGatherer.getDetailedNodeInfo(\n selectedNode,\n { x: 0, y: 0 } // No local position when manually selected\n );\n info = {\n ...info,\n hoveredNode: detailedInfo,\n hoveredWidget: null\n };\n }\n }\n // END NODE SELECTION OVERRIDE\n\n // PERSISTENCE LOGIC START\n if (info.hoveredNode) {\n // We have a valid node, update our cache\n this.lastValidNodeInfo = info;\n } else if (settings[\"🔍MagnifyGlass.InfoPanelPersist\"] && this.lastValidNodeInfo) {\n // No node hovered, but persistence is ON and we have a cache.\n // Merge the CACHED node info into the CURRENT info (so we keep current cursor pos/zoom but show old node)\n info = {\n ...info, // Current timestamp, cursor, zoom\n hoveredNode: this.lastValidNodeInfo.hoveredNode,\n hoveredWidget: this.lastValidNodeInfo.hoveredWidget\n };\n }\n // PERSISTENCE LOGIC END\n\n this.stateManager.setCurrentInfo(info);\n\n // Update the UI\n this.uiManager.displayInfo(info);\n\n // Send info to pop-out viewer if active\n if (this.magnifyGlass.popOutManager && this.magnifyGlass.popOutManager.isPopOutOpen()) {\n this.magnifyGlass.popOutManager.sendInfo(info as any);\n }\n\n this.positionManager.positionPanel();\n this.positionManager.positionFloatingControls(this.uiManager.elements.controls);\n\n // Handle node hover state logic if needed for internal state\n if (info.hoveredNode) {\n this.stateManager.state.isHoveringNode = true;\n this.stateManager.expandNodeSections();\n if (this.stateManager.state.lastNodeId !== info.hoveredNode.id) {\n this.stateManager.state.lastNodeId = info.hoveredNode.id;\n }\n } else {\n this.stateManager.state.isHoveringNode = false;\n // Only collapse if we are NOT persisting\n if (!settings[\"🔍MagnifyGlass.InfoPanelPersist\"]) {\n if (this.stateManager.state.lastNodeId !== null) {\n this.stateManager.state.lastNodeId = null;\n if (!this.stateManager.state.isPanelHovered) {\n this.stateManager.scheduleAutoCollapse();\n }\n }\n }\n }\n }\n\n updateSettings(): void {\n const changes = this.stateManager.updateSettings();\n this.uiManager.applyStyles();\n\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] && this.magnifyGlass.state.active) {\n this.uiManager.show();\n // Ensure position is updated after potential dimension changes\n this.positionManager.positionPanel();\n } else {\n this.uiManager.hide();\n }\n }\n\n /**\n * Handle node selection from dropdown.\n * @param nodeId - ID of the selected node\n */\n onNodeSelected(nodeId: number): void {\n Logger.debug(`Node selected from dropdown: ${nodeId}`);\n\n // Force an immediate info update\n this.updateInfo();\n }\n\n /**\n * Clear the selected node, returning to hover-based detection.\n */\n clearSelectedNode(): void {\n this.stateManager.clearSelectedNode();\n this.updateInfo();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/PopOutManager.js b/web/magnify-glass/PopOutManager.js index abf8d6b..b002b90 100644 --- a/web/magnify-glass/PopOutManager.js +++ b/web/magnify-glass/PopOutManager.js @@ -9,6 +9,7 @@ class PopOutManager { __publicField(this, "isOpen", false); __publicField(this, "popOutWindow", null); __publicField(this, "onStateChange", null); + __publicField(this, "onNodeSelect", null); __publicField(this, "lastPongTime", 0); __publicField(this, "pingInterval", null); __publicField(this, "viewerUrl"); @@ -27,7 +28,7 @@ class PopOutManager { * Get the URL for the pop-out viewer page. */ getViewerUrl() { - const version = "v20"; + const version = "v21"; const scripts = document.querySelectorAll('script[src*="magnify"]'); Logger.debug(`[PopOut] Found ${scripts.length} magnify scripts`); if (scripts.length > 0) { @@ -87,6 +88,15 @@ class PopOutManager { Logger.debug("[PopOut] Viewer tab closed"); if (this.onStateChange) this.onStateChange(false); break; + case "request-nodes": + Logger.debug("[PopOut] Received request-nodes:", message.data); + this.handleNodeListRequest(message.data); + break; + case "node-select": + if (this.onNodeSelect && typeof message.data === "number") { + this.onNodeSelect(message.data); + } + break; } } /** @@ -201,6 +211,40 @@ class PopOutManager { data: sanitizedInfo || void 0 }); } + /** + * Handle node list request from popout viewer. + * Fetches nodes from canvas and sends them to the popout. + */ + handleNodeListRequest(sortBy) { + var _a; + if (!this.channel) return; + try { + const app = window.app; + const nodes = ((_a = app == null ? void 0 : app.graph) == null ? void 0 : _a._nodes) || []; + let nodeList; + if (sortBy === "execOrder") { + nodeList = nodes.map((n) => ({ + id: n.id, + title: n.title || "Untitled", + type: n.type || "Unknown", + order: n.order ?? -1 + })).filter((n) => n.order >= 0).sort((a, b) => a.order - b.order); + } else { + nodeList = nodes.map((n) => ({ + id: n.id, + title: n.title || "Untitled", + type: n.type || "Unknown" + })).sort((a, b) => a.title.localeCompare(b.title)); + } + this.sendMessage({ + type: "nodes-list", + data: { nodes: nodeList, sortBy } + }); + Logger.debug(`[PopOut] Sent ${nodeList.length} nodes to viewer (sorted by ${sortBy})`); + } catch (e) { + Logger.error("[PopOut] Failed to get node list:", e); + } + } /** * Sanitize info object for BroadcastChannel transfer. * Removes functions, circular references, and non-serializable data. diff --git a/web/magnify-glass/PopOutManager.js.map b/web/magnify-glass/PopOutManager.js.map index 6ede68d..0942e5f 100644 --- a/web/magnify-glass/PopOutManager.js.map +++ b/web/magnify-glass/PopOutManager.js.map @@ -1 +1 @@ -{"version":3,"file":"PopOutManager.js","sources":["../../src/magnify-glass/PopOutManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - PopOutManager (TypeScript)\n * \n * Manages pop-out tab functionality for multi-monitor viewing.\n * Uses BroadcastChannel API for efficient cross-tab communication.\n */\n\nimport { BROADCAST_CHANNEL_NAME } from '../shared/constants';\nimport { Logger } from '../shared/logger';\n\n/**\n * Message types for BroadcastChannel communication\n */\ninterface PopOutMessage {\n type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong';\n data?: string | Partial | PopOutInfo;\n timestamp?: number;\n}\n\ninterface PopOutConfig {\n glassSize: number;\n borderColor: string;\n borderWidth: number;\n glassShape: string;\n theme?: string;\n}\n\n/**\n * Inspector info data structure for pop-out viewer\n */\ninterface PopOutInfo {\n hoveredNode?: {\n title: string;\n type: string;\n id?: number;\n mode?: string | number;\n pos?: { x: number; y: number } | string;\n size?: { w: number; h: number } | string;\n executionOrder?: number;\n category?: string;\n pythonModule?: string;\n widgets?: any[];\n inputs?: any[];\n outputs?: any[];\n properties?: Record;\n };\n cursor?: {\n canvas?: { x: number; y: number };\n };\n canvas?: {\n scale: number;\n };\n magnifier?: {\n zoomFactor: number;\n };\n media?: {\n tagName: string;\n naturalSize?: string;\n };\n}\n\n/**\n * PopOutManager class.\n * Handles opening, closing, and syncing with an external browser tab.\n */\nexport class PopOutManager {\n private channel: BroadcastChannel | null = null;\n private isOpen: boolean = false;\n private popOutWindow: Window | null = null;\n public onStateChange: ((isOpen: boolean) => void) | null = null;\n private lastPongTime: number = 0;\n private pingInterval: number | null = null;\n private viewerUrl: string;\n private currentTheme: string = 'dark'; // Default theme\n\n /** Throttle frame sending to ~30fps */\n private lastFrameTime: number = 0;\n private readonly FRAME_INTERVAL = 33; // ~30fps\n\n /** Connection timeout (ms) */\n private readonly CONNECTION_TIMEOUT = 5000;\n\n constructor() {\n // Get the viewer URL relative to the extension\n // The viewer HTML will be in the web directory\n this.viewerUrl = this.getViewerUrl();\n this.initChannel();\n }\n\n /**\n * Get the URL for the pop-out viewer page.\n */\n private getViewerUrl(): string {\n // Cache-busting version - increment to force refresh\n const version = 'v20';\n\n // Find the extension's base URL from the loaded scripts\n const scripts = document.querySelectorAll('script[src*=\"magnify\"]');\n Logger.debug(`[PopOut] Found ${scripts.length} magnify scripts`);\n\n if (scripts.length > 0) {\n const src = (scripts[0] as HTMLScriptElement).src;\n Logger.debug(`[PopOut] First script src: ${src}`);\n\n // src might be like: /extensions/comfyui-magnifyglass/magnify-glass/MagnifyGlass.js\n // We need to get to the web root: /extensions/comfyui-magnifyglass/\n const urlParts = src.split('/');\n // Find the extension folder (contains 'magnify' in name)\n let extensionIndex = -1;\n for (let i = 0; i < urlParts.length; i++) {\n if (urlParts[i].toLowerCase().includes('magnify')) {\n extensionIndex = i;\n break;\n }\n }\n if (extensionIndex >= 0) {\n const baseUrl = urlParts.slice(0, extensionIndex + 1).join('/');\n const viewerUrl = `${baseUrl}/popout-viewer.html?${version}`;\n Logger.debug(`[PopOut] Using viewer URL: ${viewerUrl}`);\n return viewerUrl;\n }\n }\n // Fallback: assume standard ComfyUI extension path\n const fallbackUrl = `/extensions/comfyui-magnifyglass/popout-viewer.html?${version}`;\n Logger.debug(`[PopOut] Using fallback URL: ${fallbackUrl}`);\n return fallbackUrl;\n }\n\n /**\n * Initialize the BroadcastChannel.\n */\n private initChannel(): void {\n try {\n this.channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);\n this.channel.onmessage = (event: MessageEvent) => {\n this.handleMessage(event.data);\n };\n Logger.debug('[PopOut] BroadcastChannel initialized');\n // Start pinging immediately to detect if viewer is already open (e.g. after reload)\n this.startPing();\n } catch (e) {\n Logger.error('[PopOut] Failed to create BroadcastChannel:', e);\n }\n }\n\n /**\n * Handle incoming messages from the viewer tab.\n */\n private handleMessage(message: PopOutMessage): void {\n switch (message.type) {\n case 'pong':\n this.lastPongTime = Date.now();\n if (!this.isOpen) {\n this.isOpen = true;\n Logger.debug('[PopOut] Viewer tab connected');\n if (this.onStateChange) this.onStateChange(true);\n // Send current theme immediately upon connection\n this.sendConfig({ theme: this.currentTheme });\n }\n break;\n case 'close':\n this.isOpen = false;\n this.popOutWindow = null;\n // Don't stop pinging, we might want to reconnect if user re-opens externally\n // or if we just want to keep checking. \n // However, original logic stopped ping. Let's keep pinging to auto-detect return.\n // this.stopPing(); \n Logger.debug('[PopOut] Viewer tab closed');\n if (this.onStateChange) this.onStateChange(false);\n break;\n }\n }\n\n /**\n * Open the pop-out viewer in a new tab.\n */\n open(): void {\n // If window exists and is open, focus it\n if (this.popOutWindow && !this.popOutWindow.closed) {\n this.popOutWindow.focus();\n return;\n }\n\n // Open new tab (or refocus existing if name matches)\n // Use a constant name to ensure singleton behavior across reloads\n this.popOutWindow = window.open(this.viewerUrl, 'MagnifyGlassPopout');\n\n if (!this.popOutWindow) {\n Logger.error('[PopOut] Failed to open new tab - popup may be blocked');\n return;\n }\n\n // Start pinging to detect when viewer is ready\n this.startPing();\n Logger.debug('[PopOut] Opening viewer tab...');\n }\n\n /**\n * Close the pop-out viewer tab.\n */\n close(): void {\n if (!this.isOpen && !this.popOutWindow) return;\n\n // update state immediately for instant UI feedback\n this.isOpen = false;\n if (this.onStateChange) this.onStateChange(false);\n this.stopPing();\n\n this.sendMessage({ type: 'close' });\n\n if (this.popOutWindow) {\n this.popOutWindow.close();\n this.popOutWindow = null;\n }\n\n Logger.debug('[PopOut] Sent close message to viewer');\n }\n\n /**\n * Toggle the pop-out state.\n */\n toggle(): void {\n if (this.isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /**\n * Check if the pop-out tab is currently open.\n */\n isPopOutOpen(): boolean {\n return this.isOpen;\n }\n\n /**\n * Send a frame to the pop-out viewer.\n * Throttled to ~30fps for performance.\n * @param canvas - The source canvas to send\n */\n sendFrame(canvas: HTMLCanvasElement): void {\n if (!this.isOpen || !this.channel) return;\n\n // Throttle frame rate\n const now = Date.now();\n if (now - this.lastFrameTime < this.FRAME_INTERVAL) {\n return;\n }\n this.lastFrameTime = now;\n\n try {\n // Convert canvas to data URL (efficient for small canvases)\n const dataUrl = canvas.toDataURL('image/png');\n this.sendMessage({\n type: 'frame',\n data: dataUrl,\n timestamp: now\n });\n } catch (e) {\n Logger.error('[PopOut] Failed to send frame:', e);\n }\n }\n\n /**\n * Send configuration to the pop-out viewer.\n */\n /**\n * Send configuration to the pop-out viewer.\n */\n sendConfig(config: Partial): void {\n if (!this.channel) return;\n\n // Merge with current theme if not provided\n const finalConfig = {\n theme: this.currentTheme,\n ...config\n };\n\n // Update local theme cache if provided in config\n if (config.theme) {\n this.currentTheme = config.theme;\n }\n\n this.sendMessage({\n type: 'config',\n data: finalConfig\n });\n }\n\n /**\n * Update the viewer theme.\n * @param theme - Theme name (e.g. 'dark', 'light')\n */\n updateTheme(theme: string): void {\n if (this.currentTheme === theme) return;\n\n this.currentTheme = theme;\n this.sendConfig({ theme });\n Logger.debug(`[PopOut] Theme updated to: ${theme}`);\n }\n\n /**\n * Send inspector info to the pop-out viewer.\n * @param info - Inspector panel information\n */\n sendInfo(info: PopOutInfo | null): void {\n if (!this.isOpen || !this.channel) return;\n\n // Sanitize the info object to remove non-serializable data\n const sanitizedInfo = this.sanitizeInfo(info);\n\n this.sendMessage({\n type: 'info',\n data: sanitizedInfo || undefined\n });\n }\n\n /**\n * Sanitize info object for BroadcastChannel transfer.\n * Removes functions, circular references, and non-serializable data.\n * Handles both GatheredInfo and PopOutInfo formats.\n */\n private sanitizeInfo(info: any): PopOutInfo | null {\n if (!info) return null;\n\n try {\n // Extract only the serializable parts we need\n const sanitized: PopOutInfo = {};\n\n // Handle hoveredNode\n if (info.hoveredNode) {\n sanitized.hoveredNode = {\n title: String(info.hoveredNode.title || ''),\n type: String(info.hoveredNode.type || ''),\n id: info.hoveredNode.id,\n mode: info.hoveredNode.mode,\n executionOrder: info.hoveredNode.executionOrder,\n category: info.hoveredNode.category ? String(info.hoveredNode.category) : undefined,\n pythonModule: info.hoveredNode.pythonModule ? String(info.hoveredNode.pythonModule) : undefined,\n // Map formatting or raw values\n pos: info.hoveredNode.position && typeof info.hoveredNode.position === 'object'\n ? { x: info.hoveredNode.position.x, y: info.hoveredNode.position.y }\n : String(info.hoveredNode.position || ''),\n size: info.hoveredNode.size && typeof info.hoveredNode.size === 'object'\n ? { w: info.hoveredNode.size.width, h: info.hoveredNode.size.height }\n : String(info.hoveredNode.size || ''),\n widgets: this.extractSafeData(info.hoveredNode.widgets),\n inputs: this.extractSafeData(info.hoveredNode.inputs),\n outputs: this.extractSafeData(info.hoveredNode.outputs),\n properties: this.sanitizeOptions(info.hoveredNode.properties) || {}\n };\n }\n\n // Handle cursor - map from GatheredInfo format (canvasX/canvasY)\n if (info.cursor) {\n sanitized.cursor = {\n canvas: {\n x: Number(info.cursor.canvasX || info.cursor.canvas?.x || 0),\n y: Number(info.cursor.canvasY || info.cursor.canvas?.y || 0)\n }\n };\n }\n\n // Handle canvas/zoom - map from GatheredInfo (zoom) to canvas.scale\n if (info.zoom !== undefined || info.canvas) {\n sanitized.canvas = {\n scale: Number(info.zoom || info.canvas?.scale || 1)\n };\n }\n\n // Handle magnifier\n const magnifyGlass = (window as any).comfyUIMagnifyGlass;\n if (magnifyGlass?.config) {\n sanitized.magnifier = {\n zoomFactor: Number(magnifyGlass.config.zoomFactor || 1)\n };\n }\n\n // Handle media - map from GatheredInfo mediaElement\n if (info.mediaElement || info.media) {\n const media = info.mediaElement || info.media;\n sanitized.media = {\n tagName: String(media.tagName || media.type || ''),\n naturalSize: media.naturalWidth && media.naturalHeight\n ? `${media.naturalWidth}×${media.naturalHeight}`\n : media.naturalSize\n };\n }\n\n return sanitized;\n } catch (e) {\n Logger.error('[PopOut] Failed to sanitize info:', e);\n return null;\n }\n }\n\n /**\n * Extract specific safe fields from node lists (widgets, inputs, outputs).\n * Removes functions like onFloatValueChange which cause cloning errors.\n */\n private extractSafeData(list: any[] | undefined): any[] {\n if (!list || !Array.isArray(list)) return [];\n return list.map(item => {\n if (!item || typeof item !== 'object') return {};\n\n // Only extract known safe identifiers and values\n const safeItem: any = {\n name: item.name,\n type: item.type,\n label: item.label\n };\n\n // Safely copy value if it's not a function\n if (item.value !== undefined && typeof item.value !== 'function') {\n safeItem.value = item.value;\n }\n\n // Sanitized options for widgets\n if (item.options) {\n safeItem.options = this.sanitizeOptions(item.options);\n }\n\n return safeItem;\n });\n }\n\n /**\n * Shallow copy object removing functions.\n */\n private sanitizeOptions(options: any): any {\n if (!options || typeof options !== 'object') return {};\n const clean: any = {};\n for (const key in options) {\n const val = options[key];\n if (typeof val !== 'function' && !key.startsWith('_')) {\n // Determine if value is simple enough to copy\n if (typeof val === 'object' && val !== null) {\n try {\n // Simple check if it's a plain object/array without cycles\n JSON.stringify(val);\n clean[key] = val;\n } catch (e) {\n // Skip complex objects\n }\n } else {\n clean[key] = val;\n }\n }\n }\n return clean;\n }\n\n /**\n * Send a message through the BroadcastChannel.\n */\n private sendMessage(message: PopOutMessage): void {\n if (!this.channel) return;\n\n try {\n this.channel.postMessage(message);\n } catch (e) {\n Logger.error('[PopOut] Failed to send message:', e);\n }\n }\n\n /**\n * Start pinging to check connection.\n */\n private startPing(): void {\n // Define ping function\n const ping = () => {\n if (this.channel) {\n this.channel.postMessage({ type: 'ping', timestamp: Date.now() });\n }\n\n // Check for connection timeout\n if (this.isOpen && Date.now() - this.lastPongTime > this.CONNECTION_TIMEOUT) {\n Logger.debug('[PopOut] Connection timeout, marking as closed');\n this.isOpen = false;\n this.stopPing(); // Stop pinging if timeout occurs (assume truly gone)\n // Actually, if we want auto-reconnect, we should NOT stop pinging on timeout?\n // But timeout implies \"It was open, but stopped responding\".\n // If user closed it, we got 'close' message.\n // If it crashed, we get timeout.\n // If we stop pinging, we won't detect if it comes back?\n // But usually we want to stop to save resources.\n // Let's stick to original behavior for timeout: stop pinging.\n // But for 'close' message: keep pinging (handled in handleMessage).\n if (this.onStateChange) this.onStateChange(false);\n }\n };\n\n // Always send an immediate ping when startPing is called, \n // to ensure responsiveness (e.g. when clicking Open button).\n ping();\n\n // If interval is already running, we don't need to start another one\n if (this.pingInterval) return;\n\n // Then ping every second\n this.pingInterval = window.setInterval(ping, 1000);\n }\n\n /**\n * Stop pinging.\n */\n private stopPing(): void {\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n }\n\n /**\n * Cleanup resources.\n */\n cleanup(): void {\n this.close();\n this.stopPing();\n if (this.channel) {\n this.channel.close();\n this.channel = null;\n }\n Logger.debug('[PopOut] Cleaned up');\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"PopOutManager.js","sources":["../../src/magnify-glass/PopOutManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - PopOutManager (TypeScript)\n * \n * Manages pop-out tab functionality for multi-monitor viewing.\n * Uses BroadcastChannel API for efficient cross-tab communication.\n */\n\nimport { BROADCAST_CHANNEL_NAME } from '../shared/constants';\nimport { Logger } from '../shared/logger';\n\n/**\n * Message types for BroadcastChannel communication\n */\ninterface PopOutMessage {\n type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong' | 'node-select' | 'nodes-list' | 'request-nodes';\n data?: string | Partial | PopOutInfo | NodeListData | number;\n timestamp?: number;\n}\n\n/**\n * Node list data sent to popout for dropdown\n */\ninterface NodeListData {\n nodes: { id: number; title: string; type: string; order?: number }[];\n sortBy: 'title' | 'execOrder';\n}\n\ninterface PopOutConfig {\n glassSize: number;\n borderColor: string;\n borderWidth: number;\n glassShape: string;\n theme?: string;\n}\n\n/**\n * Inspector info data structure for pop-out viewer\n */\ninterface PopOutInfo {\n hoveredNode?: {\n title: string;\n type: string;\n id?: number;\n mode?: string | number;\n pos?: { x: number; y: number } | string;\n size?: { w: number; h: number } | string;\n executionOrder?: number;\n category?: string;\n pythonModule?: string;\n widgets?: any[];\n inputs?: any[];\n outputs?: any[];\n properties?: Record;\n };\n cursor?: {\n canvas?: { x: number; y: number };\n };\n canvas?: {\n scale: number;\n };\n magnifier?: {\n zoomFactor: number;\n };\n media?: {\n tagName: string;\n naturalSize?: string;\n };\n}\n\n/**\n * PopOutManager class.\n * Handles opening, closing, and syncing with an external browser tab.\n */\nexport class PopOutManager {\n private channel: BroadcastChannel | null = null;\n private isOpen: boolean = false;\n private popOutWindow: Window | null = null;\n public onStateChange: ((isOpen: boolean) => void) | null = null;\n public onNodeSelect: ((nodeId: number) => void) | null = null;\n private lastPongTime: number = 0;\n private pingInterval: number | null = null;\n private viewerUrl: string;\n private currentTheme: string = 'dark'; // Default theme\n\n /** Throttle frame sending to ~30fps */\n private lastFrameTime: number = 0;\n private readonly FRAME_INTERVAL = 33; // ~30fps\n\n /** Connection timeout (ms) */\n private readonly CONNECTION_TIMEOUT = 5000;\n\n constructor() {\n // Get the viewer URL relative to the extension\n // The viewer HTML will be in the web directory\n this.viewerUrl = this.getViewerUrl();\n this.initChannel();\n }\n\n /**\n * Get the URL for the pop-out viewer page.\n */\n private getViewerUrl(): string {\n // Cache-busting version - increment to force refresh\n const version = 'v21';\n\n // Find the extension's base URL from the loaded scripts\n const scripts = document.querySelectorAll('script[src*=\"magnify\"]');\n Logger.debug(`[PopOut] Found ${scripts.length} magnify scripts`);\n\n if (scripts.length > 0) {\n const src = (scripts[0] as HTMLScriptElement).src;\n Logger.debug(`[PopOut] First script src: ${src}`);\n\n // src might be like: /extensions/comfyui-magnifyglass/magnify-glass/MagnifyGlass.js\n // We need to get to the web root: /extensions/comfyui-magnifyglass/\n const urlParts = src.split('/');\n // Find the extension folder (contains 'magnify' in name)\n let extensionIndex = -1;\n for (let i = 0; i < urlParts.length; i++) {\n if (urlParts[i].toLowerCase().includes('magnify')) {\n extensionIndex = i;\n break;\n }\n }\n if (extensionIndex >= 0) {\n const baseUrl = urlParts.slice(0, extensionIndex + 1).join('/');\n const viewerUrl = `${baseUrl}/popout-viewer.html?${version}`;\n Logger.debug(`[PopOut] Using viewer URL: ${viewerUrl}`);\n return viewerUrl;\n }\n }\n // Fallback: assume standard ComfyUI extension path\n const fallbackUrl = `/extensions/comfyui-magnifyglass/popout-viewer.html?${version}`;\n Logger.debug(`[PopOut] Using fallback URL: ${fallbackUrl}`);\n return fallbackUrl;\n }\n\n /**\n * Initialize the BroadcastChannel.\n */\n private initChannel(): void {\n try {\n this.channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);\n this.channel.onmessage = (event: MessageEvent) => {\n this.handleMessage(event.data);\n };\n Logger.debug('[PopOut] BroadcastChannel initialized');\n // Start pinging immediately to detect if viewer is already open (e.g. after reload)\n this.startPing();\n } catch (e) {\n Logger.error('[PopOut] Failed to create BroadcastChannel:', e);\n }\n }\n\n /**\n * Handle incoming messages from the viewer tab.\n */\n private handleMessage(message: PopOutMessage): void {\n switch (message.type) {\n case 'pong':\n this.lastPongTime = Date.now();\n if (!this.isOpen) {\n this.isOpen = true;\n Logger.debug('[PopOut] Viewer tab connected');\n if (this.onStateChange) this.onStateChange(true);\n // Send current theme immediately upon connection\n this.sendConfig({ theme: this.currentTheme });\n }\n break;\n case 'close':\n this.isOpen = false;\n this.popOutWindow = null;\n // Don't stop pinging, we might want to reconnect if user re-opens externally\n // or if we just want to keep checking. \n // However, original logic stopped ping. Let's keep pinging to auto-detect return.\n // this.stopPing(); \n Logger.debug('[PopOut] Viewer tab closed');\n if (this.onStateChange) this.onStateChange(false);\n break;\n case 'request-nodes':\n // Popout is requesting the node list for dropdown\n Logger.debug('[PopOut] Received request-nodes:', message.data);\n this.handleNodeListRequest(message.data as 'title' | 'execOrder');\n break;\n case 'node-select':\n // Popout selected a node, notify main window\n if (this.onNodeSelect && typeof message.data === 'number') {\n this.onNodeSelect(message.data);\n }\n break;\n }\n }\n\n /**\n * Open the pop-out viewer in a new tab.\n */\n open(): void {\n // If window exists and is open, focus it\n if (this.popOutWindow && !this.popOutWindow.closed) {\n this.popOutWindow.focus();\n return;\n }\n\n // Open new tab (or refocus existing if name matches)\n // Use a constant name to ensure singleton behavior across reloads\n this.popOutWindow = window.open(this.viewerUrl, 'MagnifyGlassPopout');\n\n if (!this.popOutWindow) {\n Logger.error('[PopOut] Failed to open new tab - popup may be blocked');\n return;\n }\n\n // Start pinging to detect when viewer is ready\n this.startPing();\n Logger.debug('[PopOut] Opening viewer tab...');\n }\n\n /**\n * Close the pop-out viewer tab.\n */\n close(): void {\n if (!this.isOpen && !this.popOutWindow) return;\n\n // update state immediately for instant UI feedback\n this.isOpen = false;\n if (this.onStateChange) this.onStateChange(false);\n this.stopPing();\n\n this.sendMessage({ type: 'close' });\n\n if (this.popOutWindow) {\n this.popOutWindow.close();\n this.popOutWindow = null;\n }\n\n Logger.debug('[PopOut] Sent close message to viewer');\n }\n\n /**\n * Toggle the pop-out state.\n */\n toggle(): void {\n if (this.isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /**\n * Check if the pop-out tab is currently open.\n */\n isPopOutOpen(): boolean {\n return this.isOpen;\n }\n\n /**\n * Send a frame to the pop-out viewer.\n * Throttled to ~30fps for performance.\n * @param canvas - The source canvas to send\n */\n sendFrame(canvas: HTMLCanvasElement): void {\n if (!this.isOpen || !this.channel) return;\n\n // Throttle frame rate\n const now = Date.now();\n if (now - this.lastFrameTime < this.FRAME_INTERVAL) {\n return;\n }\n this.lastFrameTime = now;\n\n try {\n // Convert canvas to data URL (efficient for small canvases)\n const dataUrl = canvas.toDataURL('image/png');\n this.sendMessage({\n type: 'frame',\n data: dataUrl,\n timestamp: now\n });\n } catch (e) {\n Logger.error('[PopOut] Failed to send frame:', e);\n }\n }\n\n /**\n * Send configuration to the pop-out viewer.\n */\n /**\n * Send configuration to the pop-out viewer.\n */\n sendConfig(config: Partial): void {\n if (!this.channel) return;\n\n // Merge with current theme if not provided\n const finalConfig = {\n theme: this.currentTheme,\n ...config\n };\n\n // Update local theme cache if provided in config\n if (config.theme) {\n this.currentTheme = config.theme;\n }\n\n this.sendMessage({\n type: 'config',\n data: finalConfig\n });\n }\n\n /**\n * Update the viewer theme.\n * @param theme - Theme name (e.g. 'dark', 'light')\n */\n updateTheme(theme: string): void {\n if (this.currentTheme === theme) return;\n\n this.currentTheme = theme;\n this.sendConfig({ theme });\n Logger.debug(`[PopOut] Theme updated to: ${theme}`);\n }\n\n /**\n * Send inspector info to the pop-out viewer.\n * @param info - Inspector panel information\n */\n sendInfo(info: PopOutInfo | null): void {\n if (!this.isOpen || !this.channel) return;\n\n // Sanitize the info object to remove non-serializable data\n const sanitizedInfo = this.sanitizeInfo(info);\n\n this.sendMessage({\n type: 'info',\n data: sanitizedInfo || undefined\n });\n }\n\n /**\n * Handle node list request from popout viewer.\n * Fetches nodes from canvas and sends them to the popout.\n */\n private handleNodeListRequest(sortBy: 'title' | 'execOrder'): void {\n if (!this.channel) return;\n\n try {\n // Access nodes from ComfyUI's app object\n const app = (window as any).app;\n const nodes = app?.graph?._nodes || [];\n\n let nodeList: { id: number; title: string; type: string; order?: number }[];\n\n if (sortBy === 'execOrder') {\n nodeList = nodes\n .map((n: any) => ({\n id: n.id,\n title: n.title || 'Untitled',\n type: n.type || 'Unknown',\n order: n.order ?? -1\n }))\n .filter((n: any) => n.order >= 0)\n .sort((a: any, b: any) => a.order - b.order);\n } else {\n nodeList = nodes\n .map((n: any) => ({\n id: n.id,\n title: n.title || 'Untitled',\n type: n.type || 'Unknown'\n }))\n .sort((a: any, b: any) => a.title.localeCompare(b.title));\n }\n\n this.sendMessage({\n type: 'nodes-list',\n data: { nodes: nodeList, sortBy }\n });\n\n Logger.debug(`[PopOut] Sent ${nodeList.length} nodes to viewer (sorted by ${sortBy})`);\n } catch (e) {\n Logger.error('[PopOut] Failed to get node list:', e);\n }\n }\n\n /**\n * Sanitize info object for BroadcastChannel transfer.\n * Removes functions, circular references, and non-serializable data.\n * Handles both GatheredInfo and PopOutInfo formats.\n */\n private sanitizeInfo(info: any): PopOutInfo | null {\n if (!info) return null;\n\n try {\n // Extract only the serializable parts we need\n const sanitized: PopOutInfo = {};\n\n // Handle hoveredNode\n if (info.hoveredNode) {\n sanitized.hoveredNode = {\n title: String(info.hoveredNode.title || ''),\n type: String(info.hoveredNode.type || ''),\n id: info.hoveredNode.id,\n mode: info.hoveredNode.mode,\n executionOrder: info.hoveredNode.executionOrder,\n category: info.hoveredNode.category ? String(info.hoveredNode.category) : undefined,\n pythonModule: info.hoveredNode.pythonModule ? String(info.hoveredNode.pythonModule) : undefined,\n // Map formatting or raw values\n pos: info.hoveredNode.position && typeof info.hoveredNode.position === 'object'\n ? { x: info.hoveredNode.position.x, y: info.hoveredNode.position.y }\n : String(info.hoveredNode.position || ''),\n size: info.hoveredNode.size && typeof info.hoveredNode.size === 'object'\n ? { w: info.hoveredNode.size.width, h: info.hoveredNode.size.height }\n : String(info.hoveredNode.size || ''),\n widgets: this.extractSafeData(info.hoveredNode.widgets),\n inputs: this.extractSafeData(info.hoveredNode.inputs),\n outputs: this.extractSafeData(info.hoveredNode.outputs),\n properties: this.sanitizeOptions(info.hoveredNode.properties) || {}\n };\n }\n\n // Handle cursor - map from GatheredInfo format (canvasX/canvasY)\n if (info.cursor) {\n sanitized.cursor = {\n canvas: {\n x: Number(info.cursor.canvasX || info.cursor.canvas?.x || 0),\n y: Number(info.cursor.canvasY || info.cursor.canvas?.y || 0)\n }\n };\n }\n\n // Handle canvas/zoom - map from GatheredInfo (zoom) to canvas.scale\n if (info.zoom !== undefined || info.canvas) {\n sanitized.canvas = {\n scale: Number(info.zoom || info.canvas?.scale || 1)\n };\n }\n\n // Handle magnifier\n const magnifyGlass = (window as any).comfyUIMagnifyGlass;\n if (magnifyGlass?.config) {\n sanitized.magnifier = {\n zoomFactor: Number(magnifyGlass.config.zoomFactor || 1)\n };\n }\n\n // Handle media - map from GatheredInfo mediaElement\n if (info.mediaElement || info.media) {\n const media = info.mediaElement || info.media;\n sanitized.media = {\n tagName: String(media.tagName || media.type || ''),\n naturalSize: media.naturalWidth && media.naturalHeight\n ? `${media.naturalWidth}×${media.naturalHeight}`\n : media.naturalSize\n };\n }\n\n return sanitized;\n } catch (e) {\n Logger.error('[PopOut] Failed to sanitize info:', e);\n return null;\n }\n }\n\n /**\n * Extract specific safe fields from node lists (widgets, inputs, outputs).\n * Removes functions like onFloatValueChange which cause cloning errors.\n */\n private extractSafeData(list: any[] | undefined): any[] {\n if (!list || !Array.isArray(list)) return [];\n return list.map(item => {\n if (!item || typeof item !== 'object') return {};\n\n // Only extract known safe identifiers and values\n const safeItem: any = {\n name: item.name,\n type: item.type,\n label: item.label\n };\n\n // Safely copy value if it's not a function\n if (item.value !== undefined && typeof item.value !== 'function') {\n safeItem.value = item.value;\n }\n\n // Sanitized options for widgets\n if (item.options) {\n safeItem.options = this.sanitizeOptions(item.options);\n }\n\n return safeItem;\n });\n }\n\n /**\n * Shallow copy object removing functions.\n */\n private sanitizeOptions(options: any): any {\n if (!options || typeof options !== 'object') return {};\n const clean: any = {};\n for (const key in options) {\n const val = options[key];\n if (typeof val !== 'function' && !key.startsWith('_')) {\n // Determine if value is simple enough to copy\n if (typeof val === 'object' && val !== null) {\n try {\n // Simple check if it's a plain object/array without cycles\n JSON.stringify(val);\n clean[key] = val;\n } catch (e) {\n // Skip complex objects\n }\n } else {\n clean[key] = val;\n }\n }\n }\n return clean;\n }\n\n /**\n * Send a message through the BroadcastChannel.\n */\n private sendMessage(message: PopOutMessage): void {\n if (!this.channel) return;\n\n try {\n this.channel.postMessage(message);\n } catch (e) {\n Logger.error('[PopOut] Failed to send message:', e);\n }\n }\n\n /**\n * Start pinging to check connection.\n */\n private startPing(): void {\n // Define ping function\n const ping = () => {\n if (this.channel) {\n this.channel.postMessage({ type: 'ping', timestamp: Date.now() });\n }\n\n // Check for connection timeout\n if (this.isOpen && Date.now() - this.lastPongTime > this.CONNECTION_TIMEOUT) {\n Logger.debug('[PopOut] Connection timeout, marking as closed');\n this.isOpen = false;\n this.stopPing(); // Stop pinging if timeout occurs (assume truly gone)\n // Actually, if we want auto-reconnect, we should NOT stop pinging on timeout?\n // But timeout implies \"It was open, but stopped responding\".\n // If user closed it, we got 'close' message.\n // If it crashed, we get timeout.\n // If we stop pinging, we won't detect if it comes back?\n // But usually we want to stop to save resources.\n // Let's stick to original behavior for timeout: stop pinging.\n // But for 'close' message: keep pinging (handled in handleMessage).\n if (this.onStateChange) this.onStateChange(false);\n }\n };\n\n // Always send an immediate ping when startPing is called, \n // to ensure responsiveness (e.g. when clicking Open button).\n ping();\n\n // If interval is already running, we don't need to start another one\n if (this.pingInterval) return;\n\n // Then ping every second\n this.pingInterval = window.setInterval(ping, 1000);\n }\n\n /**\n * Stop pinging.\n */\n private stopPing(): void {\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n }\n\n /**\n * Cleanup resources.\n */\n cleanup(): void {\n this.close();\n this.stopPing();\n if (this.channel) {\n this.channel.close();\n this.channel = null;\n }\n Logger.debug('[PopOut] Cleaned up');\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/popout-viewer.html b/web/popout-viewer.html index fc76106..4515db1 100644 --- a/web/popout-viewer.html +++ b/web/popout-viewer.html @@ -730,6 +730,72 @@ box-shadow: none !important; object-fit: cover; } + + /* Clickable row styles */ + .info-row.clickable-row { + cursor: pointer; + transition: background 0.2s ease; + } + + .info-row.clickable-row:hover { + background: var(--bg-elevated); + } + + .dropdown-indicator { + margin-left: 4px; + opacity: 0.6; + font-size: 10px; + } + + /* Node selector dropdown */ + .node-selector-dropdown { + position: absolute; + z-index: 100000; + max-height: 300px; + overflow-y: auto; + background: var(--bg-elevated); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + min-width: 200px; + max-width: 300px; + } + + .node-selector-dropdown .dropdown-item { + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px solid var(--border-subtle); + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + } + + .node-selector-dropdown .dropdown-item:hover { + background: var(--bg-tertiary); + } + + .node-selector-dropdown .dropdown-item:last-child { + border-bottom: none; + } + + .node-selector-dropdown .node-order { + color: var(--accent-cyan); + font-weight: 600; + min-width: 24px; + } + + .node-selector-dropdown .node-title { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .node-selector-dropdown .node-type { + color: var(--text-tertiary); + font-size: 11px; + } @@ -904,6 +970,9 @@ case 'close': markDisconnected(); break; + case 'nodes-list': + handleNodesList(message.data); + break; } }; @@ -1009,14 +1078,14 @@ ${escapeHtml(info.hoveredNode.type || 'Unknown')}
-
+
Title - ${escapeHtml(info.hoveredNode.title || 'Untitled')} + ${escapeHtml(info.hoveredNode.title || 'Untitled')}▼
${info.hoveredNode.executionOrder !== undefined ? ` -
+
Exec Order - ${info.hoveredNode.executionOrder} + ${info.hoveredNode.executionOrder}▼
` : ''} ${info.hoveredNode.category ? `
@@ -1160,6 +1229,123 @@ if (existing) existing.remove(); } + // Node selection dropdown functionality + let currentDropdownAnchorRect = null; + let pendingDropdownType = null; + + function requestNodeDropdown(type, anchorElement) { + console.log('[Popout] requestNodeDropdown called:', type, anchorElement); + // Store anchor position immediately (before potential re-render) + if (anchorElement) { + currentDropdownAnchorRect = anchorElement.getBoundingClientRect(); + console.log('[Popout] Stored anchorRect:', currentDropdownAnchorRect); + } + pendingDropdownType = type; + + // Request node list from main window + if (channel) { + console.log('[Popout] Sending request-nodes message'); + channel.postMessage({ type: 'request-nodes', data: type }); + } else { + console.log('[Popout] ERROR: channel is null!'); + } + } + + function handleNodesList(data) { + console.log('[Popout] handleNodesList called:', data); + console.log('[Popout] currentDropdownAnchorRect:', currentDropdownAnchorRect); + if (!data || !data.nodes || !currentDropdownAnchorRect) { + console.log('[Popout] handleNodesList returning early'); + return; + } + showDropdown(data.nodes, data.sortBy); + } + + function showDropdown(nodes, sortBy) { + // Save anchorRect before hideDropdown clears it + const savedAnchorRect = currentDropdownAnchorRect; + hideDropdown(); + + if (nodes.length === 0) return; + if (!savedAnchorRect) return; + + const dropdown = document.createElement('div'); + dropdown.className = 'node-selector-dropdown'; + dropdown.id = 'node-dropdown'; + + nodes.forEach(node => { + const item = document.createElement('div'); + item.className = 'dropdown-item'; + + if (sortBy === 'execOrder' && node.order !== undefined) { + item.innerHTML = ` + #${node.order} + ${escapeHtml(node.title)} + ${escapeHtml(node.type)} + `; + } else { + item.innerHTML = ` + ${escapeHtml(node.title)} + ${escapeHtml(node.type)} + `; + } + + item.addEventListener('click', (e) => { + e.stopPropagation(); + selectNode(node.id); + hideDropdown(); + }); + + dropdown.appendChild(item); + }); + + // Position dropdown + document.body.appendChild(dropdown); + console.log('[Popout] showDropdown: Created dropdown with', nodes.length, 'nodes'); + const anchorRect = savedAnchorRect; + dropdown.style.top = `${anchorRect.bottom + 4}px`; + dropdown.style.left = `${anchorRect.left}px`; + console.log('[Popout] showDropdown: Positioned at', dropdown.style.top, dropdown.style.left); + + // Adjust if off-screen + const dropdownRect = dropdown.getBoundingClientRect(); + if (dropdownRect.right > window.innerWidth - 10) { + dropdown.style.left = `${window.innerWidth - dropdownRect.width - 10}px`; + } + if (dropdownRect.bottom > window.innerHeight - 10) { + dropdown.style.top = `${anchorRect.top - dropdownRect.height - 4}px`; + } + + // Close on click outside + setTimeout(() => { + document.addEventListener('click', closeDropdownOnClickOutside); + }, 10); + } + + function closeDropdownOnClickOutside(e) { + const dropdown = document.getElementById('node-dropdown'); + if (dropdown && !dropdown.contains(e.target)) { + hideDropdown(); + } + } + + function hideDropdown() { + const dropdown = document.getElementById('node-dropdown'); + if (dropdown) { + dropdown.remove(); + } + document.removeEventListener('click', closeDropdownOnClickOutside); + currentDropdownAnchorRect = null; + pendingDropdownType = null; + } + + function selectNode(nodeId) { + // Send node selection to main window + if (channel) { + channel.postMessage({ type: 'node-select', data: nodeId }); + } + } + function markConnected() { if (!isConnected) { isConnected = true;