From 1f898cf9688cb48a7ede4aadabfcb7144f90247c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=86motion?= <163354043+AEmotionStudio@users.noreply.github.com> Date: Thu, 12 Jun 2025 23:13:04 -0700 Subject: [PATCH] Added Dockable Info Panel --- README.md | 43 +- __init__.py | 2 +- images/inspector_preview.gif | 3 + pyproject.toml | 2 +- web/magnify_glass.js | 54 +- web/magnify_info_panel.js | 2370 ++++++++++++++++++++++++++++++++++ 6 files changed, 2437 insertions(+), 37 deletions(-) create mode 100644 images/inspector_preview.gif create mode 100644 web/magnify_info_panel.js diff --git a/README.md b/README.md index b9b69c9..2aa32dc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ComfyUI-MagnifyGlass -![Version](https://img.shields.io/badge/version-1.2.0-blue.svg) +![Version](https://img.shields.io/badge/version-1.3.0-blue.svg) ![ComfyUI](https://img.shields.io/badge/ComfyUI-compatible-green) ![License](https://img.shields.io/badge/license-GPL--3.0-brightgreen.svg) ![Dependencies](https://img.shields.io/badge/dependencies-none-brightgreen.svg) @@ -12,11 +12,17 @@ ComfyUI-MagnifyGlass adds an intuitive magnifying glass to your ComfyUI canvas, > [!TIP] > Perfect for inspecting fine details in your generated images, node connections, or any part of the ComfyUI canvas! -![Magnify Glass in Action WEBP](images/magnify_glass_action.webp) +![Magnify Glass in Action Wepb](images/magnify_glass_action.webp) + +## 🚀 Latest Updates + +**6/12/25 (v1.3.0)** + +- **Inspector Information Panel:** I've introduced a new dockable panel that provides real-time data about whatever you're hovering over on the canvas. It includes detailed node analysis, media information, and a technical "Inspector" tab. Check out the "Features" and "Configuration" sections below for more details! ## ✨ Features -![Magnify Glass Settings PNG](images/magnify_glass_settings.png) +![Magnify Glass Settings Webp](images/magnify_glass_settings.png) - **🔍 Smooth Zooming**: Magnify any part of the ComfyUI canvas with a configurable zoom factor. - **🖱️ Cursor Interaction**: @@ -39,8 +45,13 @@ ComfyUI-MagnifyGlass adds an intuitive magnifying glass to your ComfyUI canvas, - **🖼️ Texture Filtering**: Choose between Linear (smooth) and Nearest (pixelated) texture filtering for the magnified view. - **🔒 Always Active Mode**: Toggle the magnifier to stay on/off without holding keys. - **🔑 Toggle Follow Key**: Use a configurable key (default 'H') to toggle the 'Follow Cursor' behavior on the fly. +- **📊 Information Panel**: An optional, dockable panel that provides real-time data about the object you are hovering over. + - **Node Analysis**: See detailed information about nodes, including title, type, important parameters (like seed, steps, CFG), and content from text boxes. + - **Media Details**: Get information on images and other media. + - **Inspector Tab**: A special tab showing technical details like cursor coordinates, canvas scale, and magnifier zoom level. + - **Highly Customizable**: Pin, lock, and move the panel. Change its appearance, opacity, and more through settings. -![Always Active Mode in Action WEBP](images/always_active_action.webp) +![Always Active Mode in Action Webp](images/always_active_action.webp) *(🔒 Always Active & 🔑 Toggle Follow Key Showcase: you can essentially set the glass preview where ever you want on the canvas and toggle it off and on.)* ## 📥 Installation @@ -78,7 +89,7 @@ Restart ComfyUI after installation. No additional `pip install` steps are typica 4. **Configuration**: - Access all settings by clicking the ⚙️ (Settings) icon in ComfyUI, then find the "🔍 Magnify Glass" section. -![Magnify Glass Usage Example PNG](images/magnify_glass_usage.webp) +![Magnify Glass Usage Example Webp](images/magnify_glass_usage.webp) *(🐞 Debug Mode & Arrow Keys Showcase: ⚠️ some flickering may occur with the VHS Combine Node Preview when moving the glass across the preview service while active.)* ## ⚙️ Configuration Options @@ -104,6 +115,28 @@ All options are available in the ComfyUI settings dialog under the "🔍 Magnify | **Always Active Mode** | If Yes, activating the magnifier keeps it on until activated again. If No, it deactivates on key release. | `Yes (true)` | | **Toggle Follow Key** | The key to toggle 'Follow Cursor' behavior. Works with Alt/Option if 'Require Alt/Option Key' is Yes. | `h` | +### 📊 Information Panel Options + +![Inspector Usage Example Webp](images/inspector_preview.webp) + +These settings control the behavior and appearance of the optional Info Panel. + +| Option | Description | Default Value | +| --- | --- | --- | +| **Info Panel** | Enable or disable the professional information panel. | `Enabled` | +| **Info Panel Position** | Position of the info panel relative to the magnifying glass. | `Left` | +| **Info Panel Width** | Width of the information panel in pixels. | `320` | +| **Info Panel Opacity** | Opacity of the information panel background. | `0.95` | +| **Info Panel Max Height** | Maximum height of the information panel in pixels. | `500` | +| **Info Panel Theme** | Color theme for the information panel. | `Dark` | +| **Info Panel Animations**| Enable or disable animations for the info panel. | `Enabled` | +| **Show Inspector Tab** | Show or hide the Inspector tab with cursor and canvas information. | `Disabled` | +| **Info Panel Toggle Hotkey**| Key to toggle the info panel visibility while the magnifier is active. | `i` | +| **Preview Toggle Hotkey**| Key to toggle the magnifying glass preview visibility. | `g` | +| **Pin Panel Hotkey** | Key to use with Alt to pin the info panel at mouse location (e.g., Alt+P). | `p` | +| **Show Hover Controls**| Show or hide hovering UI controls above the info panel. | `Enabled` | +| **Controls Position** | Position of the floating control buttons relative to the info panel. | `bottom-centered` | + ## ❓ Troubleshooting - **Magnifier not appearing**: diff --git a/__init__.py b/__init__.py index 6bfcc0e..d687ac4 100644 --- a/__init__.py +++ b/__init__.py @@ -6,7 +6,7 @@ NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} # List of JS files to be loaded -__js_files__ = ["magnify_glass.js"] +__js_files__ = ["magnify_glass.js", "magnify_info_panel.js"] # List of exported elements __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY', '__js_files__'] diff --git a/images/inspector_preview.gif b/images/inspector_preview.gif new file mode 100644 index 0000000..cc41ec5 --- /dev/null +++ b/images/inspector_preview.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30bc23a030e6905aa8427cf554b4939a72b6767a30180f05e7993e548b2d218d +size 3139153 diff --git a/pyproject.toml b/pyproject.toml index 46b8856..0ae53c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-magnifyglass" description = "ComfyUI-MagnifyGlass: A powerful & customizable magnifying glass for ComfyUI. Zoom into canvas details with smooth controls, configurable activation, custom styles (shape, size, border) & WebGL performance." -version = "1.2.0" +version = "1.3.0" license = {file = "LICENSE"} [project.urls] diff --git a/web/magnify_glass.js b/web/magnify_glass.js index 0538db1..2d0026d 100644 --- a/web/magnify_glass.js +++ b/web/magnify_glass.js @@ -21,7 +21,7 @@ app.registerExtension({ "🔍MagnifyGlass.FollowCursor": true, "🔍MagnifyGlass.DebugMode": false, "🔍MagnifyGlass.OffsetStep": 5, - "🔍MagnifyGlass.GlassPosition": "Bottom", + "🔍MagnifyGlass.GlassPosition": "Top-Right", "🔍MagnifyGlass.ResetKey": "o", "🔍MagnifyGlass.GlassShape": "Rounded Square", "🔍MagnifyGlass.BorderEnabled": true, @@ -505,6 +505,7 @@ app.registerExtension({ class MagnifierState { constructor() { this.active = false; + this.wasActivatedBefore = false; // Track if the glass has been activated before this.x = 0; // Cursor X relative to litegraphCanvas this.y = 0; // Cursor Y relative to litegraphCanvas this.sourceX = 0; // Calculated source area X @@ -581,7 +582,7 @@ app.registerExtension({ this.debugCanvas = document.createElement("canvas"); this.debugCanvas.id = "comfyui-magnify-debug"; this.debugCanvas.width = 400; - this.debugCanvas.height = 320; + this.debugCanvas.height = 350; // Increased height to 350px this.debugCanvas.style.cssText = ` position: fixed; top: 10px; @@ -1107,8 +1108,21 @@ app.registerExtension({ this.magnifyGlass.state.x = pixelX; this.magnifyGlass.state.y = pixelY; - // Position the glass immediately based on current cursor - this.magnifyGlass.ui.positionGlass(clientX, clientY); + // Position the glass initially at the top right corner of the window instead of at cursor + // when first activated + const glassSize = this.magnifyGlass.config.glassSize; + const padding = 20; // Padding from window edges + + if (!this.magnifyGlass.state.wasActivatedBefore) { + // First activation - position at top right of the window + this.magnifyGlass.ui.glassDiv.style.left = `${window.innerWidth - glassSize - padding}px`; + this.magnifyGlass.ui.glassDiv.style.top = `${padding}px`; + this.magnifyGlass.state.wasActivatedBefore = true; // Mark as activated once + } else { + // For subsequent activations, use normal positioning based on cursor and settings + this.magnifyGlass.ui.positionGlass(clientX, clientY); + } + this.magnifyGlass.updateMagnifiedView(); } } @@ -1550,34 +1564,14 @@ app.registerExtension({ // Initialize the magnifier (reads config, creates UI, attaches listeners) magnifyGlass.init(); - // Optional: Apply default settings if any are missing (similar to link_animations) - // This ensures that if new settings are added later, they get a default value - // without needing a full reset from the user. - // You might want a more sophisticated version check later. - // Object.entries(DEFAULT_SETTINGS).forEach(([key, value]) => { - // const fullKey = `MagnifyGlass.${key.split('.')[1]}`; // Construct full ID - // const setting = app.ui.settings.items.find(s => s.id === fullKey); - // if (setting && app.ui.settings.getSettingValue(fullKey) === undefined) { - // app.ui.settings.setSettingValue(fullKey, value); - // // Manually update the config if a default was just applied - // const configKey = key.split('.')[1].charAt(0).toLowerCase() + key.split('.')[1].slice(1); // e.g., ZoomFactor -> zoomFactor - // if (magnifyGlass.config.hasOwnProperty(configKey)) { - // magnifyGlass.config[configKey] = value; - // console.log(`Applied default for ${fullKey}`); - // } - // // Apply immediate UI changes if needed for the defaulted setting - // if(setting.onChange) { - // setting.onChange(value); - // } else { - // magnifyGlass.applyUiChanges(); - // } - // } - // }); - magnifyGlass.applyUiChanges(); // Ensure UI reflects loaded/default settings initially + // Apply UI changes + magnifyGlass.applyUiChanges(); + // Expose magnifyGlass globally for extensions + window.comfyUIMagnifyGlass = magnifyGlass; - // --- Diagnostic Info (moved inside setup) --- - console.log("---- Magnifier Diagnostic Info ----"); + // Diagnostic Info + console.log("---- Enhanced Magnifier Diagnostic Info ----"); // Browser Info console.log("User Agent:", navigator.userAgent); console.log("Device Pixel Ratio:", window.devicePixelRatio); diff --git a/web/magnify_info_panel.js b/web/magnify_info_panel.js new file mode 100644 index 0000000..85bc2eb --- /dev/null +++ b/web/magnify_info_panel.js @@ -0,0 +1,2370 @@ +import { app } from "../../../scripts/app.js"; + +/** + * ComfyUI Magnifying Glass - Professional Information Panel Extension (Refactored) + * + * This script provides a professional, highly interactive information panel that displays + * detailed analysis of whatever you're hovering over when the magnifying glass is active. + * + * Key Features: + * - Unified state management + * - Smooth mode transitions + * - Improved pin functionality with drag support + * - Lock feature to prevent dragging when pinned + * - Clean control system + * - Better positioning logic + */ + +app.registerExtension({ + name: "comfyui.magnify.glass.info.panel.pro.v2", + async setup() { + // Wait for the main magnifying glass to be ready + let magnifyGlass = null; + let retryCount = 0; + const maxRetries = 50; + + const waitForMagnifyGlass = () => { + if (window.comfyUIMagnifyGlass) { + magnifyGlass = window.comfyUIMagnifyGlass; + console.log("ComfyUI Magnify Info Panel Pro V2: Found magnifying glass instance"); + initializeInfoPanel(); + } else if (retryCount < maxRetries) { + retryCount++; + setTimeout(waitForMagnifyGlass, 100); + } else { + console.warn("ComfyUI Magnify Info Panel Pro V2: Could not find magnifying glass instance after 5 seconds"); + } + }; + + setTimeout(waitForMagnifyGlass, 100); + + function initializeInfoPanel() { + // Default settings configuration + const DEFAULT_SETTINGS = { + "🔍MagnifyGlass.InfoPanelEnabled": true, + "🔍MagnifyGlass.InfoPanelPosition": "Left", + "🔍MagnifyGlass.InfoPanelWidth": 320, + "🔍MagnifyGlass.InfoPanelOpacity": 0.95, + "🔍MagnifyGlass.InfoPanelMaxHeight": 500, + "🔍MagnifyGlass.InfoPanelTheme": "Dark", + "🔍MagnifyGlass.InfoPanelAnimations": true, + "🔍MagnifyGlass.ShowInspectorTab": false, + "🔍MagnifyGlass.ToggleHotkey": "i", + "🔍MagnifyGlass.GlassPreviewToggleHotkey": "g", + "🔍MagnifyGlass.PinPanelHotkey": "p", + "🔍MagnifyGlass.ShowHoveringControls": true, + "🔍MagnifyGlass.ControlsPosition": "bottom-centered", + }; + + const getSettingValue = (key, defaultValue) => { + try { + const value = app.ui.settings.getSettingValue(key); + return value === undefined ? defaultValue : value; + } catch (e) { + console.warn(`ComfyUI Magnify Info Panel Pro V2: Could not get setting ${key}, using default ${defaultValue}. Error: ${e}`); + return defaultValue; + } + }; + + /** + * Centralized State Manager + * Handles all state-related operations and provides clean state transitions + */ + class StateManager { + constructor() { + this.state = { + // Panel visibility and positioning + isPanelVisible: false, + isPanelMinimized: false, + isPanelPinned: false, + isPanelLocked: false, // New lock state + pinnedPosition: { x: 0, y: 0 }, + lastPinnedPosition: null, // Remember last pinned location + + // Glass visibility + isGlassPreviewVisible: true, + + // Section expansion + expandedSections: new Set(['node']), + + // Interaction states + isPanelHovered: false, + isHoveringNode: false, + + // Auto-collapse + autoExpandTimer: null, + lastNodeId: null, + + // Update scheduling + updateScheduled: false, + isInitialLoading: false, + + // Current data + currentInfo: {}, + + // Settings cache + settings: {} + }; + + this.loadSettings(); + } + + loadSettings() { + Object.keys(DEFAULT_SETTINGS).forEach(key => { + this.state.settings[key] = getSettingValue(key, DEFAULT_SETTINGS[key]); + }); + } + + updateSettings() { + const oldSettings = { ...this.state.settings }; + this.loadSettings(); + + // Return what changed for reactive updates + const changes = {}; + Object.keys(this.state.settings).forEach(key => { + if (oldSettings[key] !== this.state.settings[key]) { + changes[key] = { + old: oldSettings[key], + new: this.state.settings[key] + }; + } + }); + + return changes; + } + + togglePanelVisibility() { + this.state.isPanelVisible = !this.state.isPanelVisible; + console.log(`Info panel visibility toggled to ${this.state.isPanelVisible ? 'visible' : 'hidden'}`); + return this.state.isPanelVisible; + } + + toggleGlassPreview() { + this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible; + console.log(`Glass preview visibility toggled to ${this.state.isGlassPreviewVisible ? 'visible' : 'hidden'}`); + return this.state.isGlassPreviewVisible; + } + + togglePinning() { + const wasPinned = this.state.isPanelPinned; + this.state.isPanelPinned = !this.state.isPanelPinned; + + // When unpinning, also unlock + if (!this.state.isPanelPinned) { + this.state.isPanelLocked = false; + } + + if (this.state.isPanelPinned && this.state.lastPinnedPosition) { + // Restore to last pinned position + this.state.pinnedPosition = { ...this.state.lastPinnedPosition }; + console.log(`Panel pinned at remembered position: (${this.state.pinnedPosition.x}, ${this.state.pinnedPosition.y})`); + } else if (!this.state.isPanelPinned && wasPinned) { + // Save current position when unpinning + this.state.lastPinnedPosition = { ...this.state.pinnedPosition }; + console.log(`Panel unpinned, position saved: (${this.state.lastPinnedPosition.x}, ${this.state.lastPinnedPosition.y})`); + } + + console.log(`Panel ${this.state.isPanelPinned ? 'pinned' : 'unpinned'}`); + return this.state.isPanelPinned; + } + + toggleLocking() { + // Only allow locking when pinned + if (!this.state.isPanelPinned) { + console.log("Cannot lock panel when not pinned"); + return false; + } + + this.state.isPanelLocked = !this.state.isPanelLocked; + console.log(`Panel ${this.state.isPanelLocked ? 'locked' : 'unlocked'}`); + return this.state.isPanelLocked; + } + + toggleMinimized() { + this.state.isPanelMinimized = !this.state.isPanelMinimized; + return this.state.isPanelMinimized; + } + + toggleSection(sectionId) { + if (sectionId === 'node') return false; // Node section always expanded + + if (this.state.expandedSections.has(sectionId)) { + this.state.expandedSections.delete(sectionId); + } else { + this.state.expandedSections.add(sectionId); + } + return true; + } + + setPinnedPosition(x, y) { + this.state.pinnedPosition = { x, y }; + // Also save as last pinned position for memory + this.state.lastPinnedPosition = { x, y }; + } + + setCurrentInfo(info) { + this.state.currentInfo = info; + } + + scheduleAutoCollapse() { + this.clearAutoExpandTimer(); + if (this.state.settings["🔍MagnifyGlass.InfoPanelAnimations"]) { + this.state.autoExpandTimer = setTimeout(() => { + if (!this.state.isPanelHovered) { + this.collapseNodeSections(); + } + }, 1500); + } + } + + clearAutoExpandTimer() { + if (this.state.autoExpandTimer) { + clearTimeout(this.state.autoExpandTimer); + this.state.autoExpandTimer = null; + } + } + + expandNodeSections() { + this.state.expandedSections.add('hoveredNode'); + this.state.expandedSections.add('node'); + this.state.expandedSections.add('cursor'); + this.state.expandedSections.add('canvas'); + this.state.expandedSections.add('magnifier'); + this.clearAutoExpandTimer(); + } + + collapseNodeSections() { + this.state.expandedSections.delete('hoveredNode'); + this.state.expandedSections.delete('node'); + this.state.expandedSections.delete('widget'); + } + + cleanup() { + this.clearAutoExpandTimer(); + } + } + + /** + * UI Manager + * Handles all DOM manipulation and UI creation + */ + class UIManager { + constructor(stateManager) { + this.stateManager = stateManager; + this.elements = { + panel: null, + header: null, + content: null, + controls: null + }; + + this.createPanel(); + this.injectStyles(); + } + + createPanel() { + // Main panel container + this.elements.panel = document.createElement("div"); + this.elements.panel.id = "comfyui-magnify-info-panel-pro-v2"; + this.elements.panel.className = `magnify-info-panel theme-${this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelTheme"].toLowerCase()}`; + + // Header + this.elements.header = document.createElement("div"); + this.elements.header.className = "panel-header"; + this.elements.header.innerHTML = ` +
+
🔍
+
Inspector
+
Real-time analysis
+
+
+ +
+ `; + + // Content container + this.elements.content = document.createElement("div"); + this.elements.content.className = "panel-content"; + + this.elements.panel.appendChild(this.elements.header); + this.elements.panel.appendChild(this.elements.content); + + this.applyStyles(); + document.body.appendChild(this.elements.panel); + + // Create floating controls after panel is in DOM + if (this.stateManager.state.settings["🔍MagnifyGlass.ShowHoveringControls"]) { + this.createFloatingControls(); + // Update control states immediately after creation + this.updateControlStates(); + } + } + + createFloatingControls() { + this.elements.controls = document.createElement("div"); + this.elements.controls.className = "floating-controls vertical-layout"; // Default to vertical + this.elements.controls.innerHTML = ` + + + + + `; + + // Insert before the panel in the document body, not as a child + document.body.appendChild(this.elements.controls); + + // Set initial layout based on settings + const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "top-right"; + this.updateControlsLayout(controlsPosition); + } + + updateControlStates() { + if (!this.elements.controls) return; + + const pinBtn = this.elements.controls.querySelector('[data-action="pin"]'); + const lockBtn = this.elements.controls.querySelector('[data-action="lock"]'); + const visibilityBtn = this.elements.controls.querySelector('[data-action="toggle-panel"]'); + const glassBtn = this.elements.controls.querySelector('[data-action="toggle-glass"]'); + + if (pinBtn) { + pinBtn.classList.toggle('active', this.stateManager.state.isPanelPinned); + pinBtn.title = this.stateManager.state.isPanelPinned ? "Lock Panel" : "Unlock Panel"; + } + + if (lockBtn) { + // Only show/enable lock button when pinned + lockBtn.style.display = this.stateManager.state.isPanelPinned ? 'flex' : 'none'; + lockBtn.classList.toggle('active', this.stateManager.state.isPanelLocked); + lockBtn.title = this.stateManager.state.isPanelLocked ? "Upin Panel Position" : "Pin Panel Position"; + lockBtn.disabled = !this.stateManager.state.isPanelPinned; + } + + if (visibilityBtn) { + visibilityBtn.classList.toggle('active', this.stateManager.state.isPanelVisible); + visibilityBtn.title = this.stateManager.state.isPanelVisible ? "Show Panel" : "Hide Panel"; + } + + if (glassBtn) { + glassBtn.classList.toggle('active', this.stateManager.state.isGlassPreviewVisible); + glassBtn.title = this.stateManager.state.isGlassPreviewVisible ? "Hide Glass Preview" : "Show Glass Preview"; + } + } + + updateControlsLayout(position) { + if (!this.elements.controls) return; + + // Remove existing layout classes + this.elements.controls.classList.remove('horizontal-layout', 'vertical-layout'); + + // Add appropriate layout class + if (['top', 'bottom'].includes(position)) { + this.elements.controls.classList.add('horizontal-layout'); + } else { + this.elements.controls.classList.add('vertical-layout'); + } + } + + applyStyles() { + const settings = this.stateManager.state.settings; + + this.elements.panel.style.cssText = ` + position: absolute; + width: ${settings["🔍MagnifyGlass.InfoPanelWidth"]}px; + max-height: ${settings["🔍MagnifyGlass.InfoPanelMaxHeight"]}px; + z-index: 10001; + display: none; + opacity: 0; + transform: translateY(-10px); + transition: ${settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'none'}; + pointer-events: auto; + user-select: none; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + `; + } + + show() { + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { + this.elements.panel.style.display = "block"; + if (this.elements.controls) { + this.elements.controls.style.display = "flex"; + } + // Trigger reflow + this.elements.panel.offsetHeight; + this.elements.panel.classList.add('visible'); + this.stateManager.state.isPanelVisible = true; + } + } + + hide() { + this.elements.panel.classList.remove('visible'); + setTimeout(() => { + if (!this.stateManager.state.isPanelVisible) { + this.elements.panel.style.display = "none"; + if (this.elements.controls) { + this.elements.controls.style.display = "none"; + } + } + }, this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 300 : 0); + this.stateManager.state.isPanelVisible = false; + } + + updateMinimizedState() { + this.elements.panel.classList.toggle('panel-minimized', this.stateManager.state.isPanelMinimized); + const minimizeBtn = this.elements.header.querySelector('.minimize-btn'); + if (minimizeBtn) { + minimizeBtn.textContent = this.stateManager.state.isPanelMinimized ? '+' : '−'; + minimizeBtn.title = this.stateManager.state.isPanelMinimized ? 'Expand Panel' : 'Minimize Panel'; + } + } + + updatePinnedState() { + this.elements.panel.classList.toggle('panel-pinned', this.stateManager.state.isPanelPinned); + this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked); + this.updateControlStates(); + } + + updateTheme(newTheme) { + this.elements.panel.className = this.elements.panel.className.replace(/theme-\w+/, `theme-${newTheme.toLowerCase()}`); + } + + displayInfo(info) { + const sections = this.buildSections(info); + this.renderSections(sections); + this.updateSectionStates(); + this.updateHeaderSubtitle(info); + } + + buildSections(info) { + const sections = []; + const settings = this.stateManager.state.settings; + + // Inspector section + if (settings["🔍MagnifyGlass.ShowInspectorTab"]) { + const inspectorContent = [ + { label: 'Cursor Canvas', value: `(${Math.round(info.cursor.canvas.x)}, ${Math.round(info.cursor.canvas.y)})` }, + { label: 'Canvas Scale', value: `${(info.canvas.scale * 100).toFixed(1)}%` }, + { label: 'Magnifier Zoom', value: `${info.magnifier.zoomFactor}×` } + ]; + + sections.push({ + id: 'inspector', + icon: '🔍', + title: 'Inspector', + content: inspectorContent + }); + } + + // Media section + if (info.media) { + const mediaContent = [ + { label: 'Type', value: info.media.tagName }, + { label: 'Source', value: info.media.src } + ]; + + if (info.media.naturalSize) { + mediaContent.push({ label: 'Natural', value: info.media.naturalSize }); + } + + sections.push({ + id: 'media', + icon: '📷', + title: 'Media', + badge: info.media.tagName, + content: mediaContent + }); + } + + // Node Details section + if (info.hoveredNode) { + const nodeContent = [ + { label: 'Title', value: info.hoveredNode.title } + ]; + + // Add important node information + const checkpointInfo = this.getCheckpointInfo(info.hoveredNode); + if (checkpointInfo) { + nodeContent.push({ label: 'Model', value: checkpointInfo }); + } + + const imageInfo = this.getImageInfo(info.hoveredNode); + if (imageInfo) { + if (typeof imageInfo === 'string') { + nodeContent.push({ label: 'Image', value: imageInfo }); + } else if (typeof imageInfo === 'object') { + nodeContent.push({ label: 'Image Size', value: `${imageInfo.width}×${imageInfo.height}` }); + if (imageInfo.src) { + nodeContent.push({ label: 'Image Source', value: imageInfo.src }); + } + } + } + + const importantParameters = this.getImportantNodeParameters(info.hoveredNode); + nodeContent.push(...importantParameters); + + const textBoxContent = this.getTextBoxContent(info.hoveredNode); + if (textBoxContent) { + nodeContent.push({ label: 'Text', value: textBoxContent }); + } + + sections.push({ + id: 'node', + icon: '🎯', + title: 'Node', + badge: info.hoveredNode.type, + content: nodeContent + }); + } + + return sections; + } + + renderSections(sections) { + if (sections.length === 0) { + this.elements.content.innerHTML = ` +
+
📍
+
Empty canvas area
+
+ `; + return; + } + + this.elements.content.innerHTML = sections.map(section => ` +
+
+ ${section.icon} + ${section.title} + ${section.badge ? `${section.badge}` : ''} + ${section.id !== 'node' ? '▶' : ''} +
+
+
+ ${section.content.map(item => ` +
+ ${item.label} + ${item.value} +
+ `).join('')} +
+
+
+ `).join(''); + } + + updateSectionStates() { + const sections = this.elements.content.querySelectorAll('.info-section'); + sections.forEach(section => { + const sectionId = section.dataset.section; + const header = section.querySelector('.section-header'); + const content = section.querySelector('.section-content'); + + if (this.stateManager.state.expandedSections.has(sectionId)) { + header.classList.add('expanded'); + content.classList.add('expanded'); + } else { + header.classList.remove('expanded'); + content.classList.remove('expanded'); + } + }); + } + + updateHeaderSubtitle(info) { + const subtitleElement = this.elements.header.querySelector('.header-subtitle'); + if (subtitleElement) { + if (info.hoveredNode) { + subtitleElement.textContent = `Analyzing: ${info.hoveredNode.title}`; + subtitleElement.style.color = '#74b9ff'; + } else if (info.media) { + subtitleElement.textContent = `Media: ${info.media.tagName}`; + subtitleElement.style.color = '#a0d468'; + } else if (info.connection) { + subtitleElement.textContent = `Connection: ${info.connection.type}`; + subtitleElement.style.color = '#fdcb6e'; + } else { + subtitleElement.textContent = 'Real-time analysis'; + subtitleElement.style.color = ''; + } + } + } + + // Helper methods for node information extraction + getCheckpointInfo(nodeInfo) { + if (nodeInfo.type && ( + nodeInfo.type.includes("CheckpointLoader") || + nodeInfo.type.includes("LoadCheckpoint") || + nodeInfo.type.includes("ModelLoader") || + nodeInfo.type.includes("UNETLoader") || + nodeInfo.type.includes("VAELoader") || + nodeInfo.type.includes("LoraLoader") + )) { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + for (const widget of nodeInfo.widgets) { + if (widget.name && ( + widget.name.toLowerCase().includes("model") || + widget.name.toLowerCase().includes("checkpoint") || + widget.name.toLowerCase().includes("ckpt") || + widget.name.toLowerCase().includes("lora") || + widget.name.toLowerCase().includes("vae") || + widget.name.toLowerCase().includes("file") + )) { + const value = String(widget.value); + const filename = value.split(/[\/\\]/).pop(); + return filename || value; + } + } + } + } + return null; + } + + getImageInfo(nodeInfo) { + if (nodeInfo.type && ( + nodeInfo.type.includes("SaveImage") || + nodeInfo.type.includes("PreviewImage") || + nodeInfo.type.includes("VisionOutput") || + nodeInfo.type.includes("ImageOutput") || + nodeInfo.type.includes("LoadImage") || + nodeInfo.type.includes("Display") + )) { + if (nodeInfo.widgets) { + for (const widget of nodeInfo.widgets) { + if (widget.name && ( + widget.name.toLowerCase().includes("image") || + widget.name.toLowerCase().includes("filename") || + widget.name.toLowerCase().includes("file") + )) { + return widget.value; + } + } + } + + if (nodeInfo.properties && nodeInfo.properties.img) { + const img = nodeInfo.properties.img; + return { + width: img.width || "unknown", + height: img.height || "unknown", + src: img.src ? img.src.split(/[\/\\]/).pop() : "Preview available" + }; + } + + if (nodeInfo.outputs) { + for (const output of nodeInfo.outputs) { + if (output.links && output.links.length > 0) { + return "Image connected to " + output.links.length + " node(s)"; + } + } + } + + return "Image node"; + } + return null; + } + + getTextBoxContent(nodeInfo) { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + if (nodeInfo.type && nodeInfo.type.includes("CLIPTextEncode")) { + for (const widget of nodeInfo.widgets) { + if (widget.name === "text" && typeof widget.value === 'string') { + return widget.value; + } + } + } + + for (const widget of nodeInfo.widgets) { + if ((widget.name.toLowerCase().includes("prompt") || + widget.name.toLowerCase().includes("conditioning")) && + typeof widget.value === 'string' && widget.value.length > 0) { + return widget.value; + } + } + + for (const widget of nodeInfo.widgets) { + if ((widget.type === 'text' || widget.type === 'textarea' || + widget.type === 'string' || widget.name.toLowerCase().includes('text')) && + typeof widget.value === 'string' && widget.value.length > 0) { + + const maxLength = 150; + if (widget.value.length > maxLength) { + return widget.value.substring(0, maxLength) + '...'; + } + return widget.value; + } + } + } + return null; + } + + getImportantNodeParameters(nodeInfo) { + const parameters = []; + const importantParams = [ + 'seed', 'steps', 'cfg', 'scale', 'sampler', 'scheduler', + 'positive', 'negative', 'width', 'height', 'denoise', 'strength', + 'noise', 'count', 'batch', 'size', 'phase', 'color', 'intensity' + ]; + + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + for (const widget of nodeInfo.widgets) { + const paramName = widget.name.toLowerCase(); + if (importantParams.some(param => paramName.includes(param))) { + parameters.push({ + label: widget.name, + value: this.formatValue(widget.value) + }); + } + } + } + + return parameters; + } + + formatValue(value) { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") { + return value.length > 30 ? value.substring(0, 27) + "..." : value; + } + if (typeof value === "number") { + return Number.isInteger(value) ? value.toString() : value.toFixed(3); + } + if (typeof value === "boolean") return value.toString(); + if (Array.isArray(value)) { + return `Array(${value.length})`; + } + if (typeof value === "object") { + return "Object"; + } + return String(value); + } + + injectStyles() { + if (!document.getElementById('magnify-info-panel-styles-v2')) { + const style = document.createElement('style'); + style.id = 'magnify-info-panel-styles-v2'; + style.textContent = this.getCSS(); + document.head.appendChild(style); + } + } + + getCSS() { + return ` + .magnify-info-panel { + border-radius: 12px; + backdrop-filter: blur(20px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1); + overflow: hidden; + font-size: 16px; + line-height: 1.5; + pointer-events: auto; + } + + .magnify-info-panel * { + pointer-events: auto; + } + + .magnify-info-panel.theme-dark { + background: linear-gradient(135deg, rgba(0, 0, 0, 0.95), rgba(10, 10, 15, 0.95)); + color: #e0e0e0; + border: 1px solid rgba(100, 100, 120, 0.3); + } + + .magnify-info-panel.theme-light { + background: linear-gradient(135deg, rgba(250, 250, 255, 0.95), rgba(240, 240, 250, 0.95)); + color: #2a2a2a; + border: 1px solid rgba(200, 200, 220, 0.4); + } + + .magnify-info-panel.visible { + opacity: 1 !important; + transform: translateY(0) !important; + } + + .magnify-info-panel.panel-pinned { + cursor: grab; + box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); + } + + .magnify-info-panel.panel-locked { + cursor: default !important; + box-shadow: 0 0 0 2px rgba(255, 69, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); + } + + .magnify-info-panel.panel-locked::after { + content: '📌'; + position: absolute; + top: 8px; + right: 8px; + font-size: 14px; + opacity: 0.7; + pointer-events: none; + z-index: 1; + } + + .magnify-info-panel.panel-pinned.panel-dragging { + cursor: grabbing; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5), 0 0 0 2px rgba(255, 215, 0, 0.7); + transform: scale(1.02) !important; + z-index: 10010 !important; + transition: none !important; + } + + .magnify-info-panel.panel-pinned::before { + content: ''; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + background: linear-gradient(45deg, rgba(255, 215, 0, 0.05) 0%, transparent 100%); + pointer-events: none; + border-radius: inherit; + } + + .panel-dragging * { + pointer-events: none !important; + user-select: none !important; + transition: none !important; + } + + .magnify-info-panel.panel-minimized .panel-content { + max-height: 0 !important; + overflow: hidden; + transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + + .floating-controls { + position: fixed; + display: flex; + padding: 4px; + background: rgba(40, 40, 50, 0.95); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 4px; + opacity: 1; + z-index: 10003; + backdrop-filter: blur(8px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); + } + + .floating-controls.vertical-layout { + flex-direction: column; + gap: 4px; + } + + .floating-controls.horizontal-layout { + flex-direction: row; + gap: 4px; + } + + .control-btn { + background: rgba(60, 60, 70, 0.8); + border: 1px solid rgba(255, 255, 255, 0.2); + width: 28px; + height: 28px; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #ddd; + border-radius: 3px; + transition: background-color 0.15s ease; + } + + .control-btn:hover { + background: rgba(80, 80, 90, 0.9); + color: white; + } + + .control-btn:active { + background: rgba(100, 100, 110, 0.9); + } + + .control-btn:disabled { + background: rgba(40, 40, 50, 0.5); + color: #666; + cursor: not-allowed; + opacity: 0.5; + } + + .control-btn.active { + background: rgba(255, 200, 50, 0.3); + border-color: rgba(255, 200, 50, 0.6); + color: #ffcc33; + } + + .control-btn.lock-btn.active { + background: rgba(255, 69, 0, 0.3); + border-color: rgba(255, 69, 0, 0.6); + color: #ff4500; + } + + .panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + background: linear-gradient(135deg, rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.05)); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + user-select: none; + } + + .header-content { + display: flex; + align-items: center; + gap: 10px; + } + + .header-icon { + font-size: 18px; + opacity: 0.9; + } + + .header-title { + font-weight: 600; + font-size: 18px; + } + + .header-subtitle { + font-size: 14px; + opacity: 0.7; + font-style: italic; + } + + .header-controls { + display: flex; + gap: 6px; + } + + .minimize-btn { + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 6px; + color: inherit; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + font-weight: bold; + font-size: 16px; + } + + .minimize-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.05); + } + + .minimize-btn:active { + transform: scale(0.95); + } + + .panel-content { + padding: 0; + max-height: calc(100% - 60px); + overflow-y: auto; + scrollbar-width: thin; + } + + .panel-content::-webkit-scrollbar { + width: 8px; + } + + .panel-content::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + } + + .panel-content::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 3px; + } + + .info-section { + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + } + + .section-header { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 18px; + cursor: pointer; + transition: all 0.2s; + font-weight: 500; + border-radius: 6px; + margin: 2px; + } + + .section-header:hover { + background: rgba(255, 255, 255, 0.08); + transform: translateX(2px); + box-shadow: inset 2px 0 0 rgba(116, 185, 255, 0.5); + } + + .section-header:active { + background: rgba(255, 255, 255, 0.12); + transform: translateX(1px); + } + + .section-header.expanded { + background: rgba(116, 185, 255, 0.1); + border-left: 2px solid #74b9ff; + } + + .info-section[data-section="node"] { + background: linear-gradient(135deg, rgba(116, 185, 255, 0.05), rgba(160, 212, 104, 0.05)); + border: 1px solid rgba(116, 185, 255, 0.2); + border-radius: 8px; + margin: 6px; + } + + .info-section[data-section="node"] .section-header { + background: linear-gradient(135deg, rgba(116, 185, 255, 0.15), rgba(160, 212, 104, 0.15)); + border-radius: 8px 8px 0 0; + cursor: default; + } + + .info-section[data-section="node"] .section-header:hover { + transform: none; + box-shadow: none; + } + + .info-section[data-section="node"] .section-title { + color: #74b9ff; + font-weight: 700; + } + + .info-section[data-section="inspector"] { + background: linear-gradient(135deg, rgba(100, 150, 255, 0.05), rgba(74, 144, 226, 0.05)); + border: 1px solid rgba(100, 150, 255, 0.2); + border-radius: 8px; + margin: 6px; + } + + .info-section[data-section="inspector"] .section-header { + background: linear-gradient(135deg, rgba(100, 150, 255, 0.15), rgba(74, 144, 226, 0.15)); + border-radius: 8px 8px 0 0; + } + + .info-section[data-section="inspector"] .section-title { + color: #6496ff; + font-weight: 700; + } + + .info-section[data-section="media"] { + background: linear-gradient(135deg, rgba(253, 203, 110, 0.05), rgba(255, 215, 0, 0.05)); + border: 1px solid rgba(253, 203, 110, 0.2); + border-radius: 8px; + margin: 6px; + } + + .info-section[data-section="media"] .section-header { + background: linear-gradient(135deg, rgba(253, 203, 110, 0.15), rgba(255, 215, 0, 0.15)); + border-radius: 8px 8px 0 0; + } + + .info-section[data-section="media"] .section-title { + color: #fdcb6e; + font-weight: 700; + } + + .section-icon { + font-size: 18px; + width: 22px; + text-align: center; + } + + .section-title { + flex: 1; + font-size: 16px; + font-weight: 600; + } + + .section-badge { + background: rgba(100, 150, 255, 0.2); + color: #8bb3ff; + padding: 3px 10px; + border-radius: 10px; + font-size: 13px; + font-weight: 500; + } + + .expand-icon { + font-size: 13px; + transition: all 0.2s; + opacity: 0.7; + margin-left: auto; + } + + .section-header:hover .expand-icon { + opacity: 1; + color: #74b9ff; + } + + .section-header.expanded .expand-icon { + transform: rotate(90deg); + color: #74b9ff; + } + + .section-content { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + + .section-content.expanded { + max-height: 1000px; + } + + .section-body { + padding: 0 18px 14px 42px; + } + + .info-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.03); + } + + .info-row:last-child { + border-bottom: none; + } + + .info-label { + font-weight: 500; + opacity: 0.8; + font-size: 15px; + } + + .info-value { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + color: #a0d468; + background: rgba(160, 212, 104, 0.1); + padding: 3px 8px; + border-radius: 4px; + } + + .empty-state { + text-align: center; + padding: 50px 25px; + opacity: 0.6; + } + + .empty-state-icon { + font-size: 36px; + margin-bottom: 15px; + opacity: 0.5; + } + + .empty-state-text { + font-size: 16px; + font-style: italic; + } + + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } + } + + .pulse { + animation: pulse 2s infinite; + } + `; + } + + cleanup() { + if (this.elements.panel) { + this.elements.panel.remove(); + } + + if (this.elements.controls) { + this.elements.controls.remove(); + } + + const styles = document.getElementById('magnify-info-panel-styles-v2'); + if (styles) { + styles.remove(); + } + } + } + + /** + * Position Manager + * Handles all positioning logic including pinning and boundary checking + */ + class PositionManager { + constructor(stateManager, uiManager) { + this.stateManager = stateManager; + this.uiManager = uiManager; + } + + positionPanel() { + if (!this.uiManager.elements.panel) return; + + // If panel is pinned, keep it at its pinned position + if (this.stateManager.state.isPanelPinned) { + this.applyPinnedPosition(); + this.positionFloatingControls(); + return; + } + + // Normal positioning logic + this.calculateNormalPosition(); + this.positionFloatingControls(); + } + + positionFloatingControls() { + if (!this.uiManager.elements.controls) return; + + const panelRect = this.uiManager.elements.panel.getBoundingClientRect(); + const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "top-right"; + + let idealX, idealY; + // Determine dimensions based on layout. Fallback values are estimates. + const isHorizontal = ['top', 'bottom'].includes(controlsPosition); + const controlsWidth = this.uiManager.elements.controls.offsetWidth || (isHorizontal ? 160 : 40); + const controlsHeight = this.uiManager.elements.controls.offsetHeight || (isHorizontal ? 40 : 160); + const margin = 5; + + // Determine the ideal, unconstrained position for the controls + switch (controlsPosition) { + case "top-left": + idealX = panelRect.left - controlsWidth - margin; + idealY = panelRect.top; + break; + case "top-right": + idealX = panelRect.right + margin; + idealY = panelRect.top; + break; + case "bottom-left": + idealX = panelRect.left - controlsWidth - margin; + idealY = panelRect.bottom - controlsHeight; + break; + case "bottom-right": + idealX = panelRect.right + margin; + idealY = panelRect.bottom - controlsHeight; + break; + case "top": + idealX = panelRect.left + (panelRect.width - controlsWidth) / 2; + idealY = panelRect.top - controlsHeight - margin; + break; + case "bottom": + idealX = panelRect.left + (panelRect.width - controlsWidth) / 2; + idealY = panelRect.bottom + margin; + break; + case "left": + idealX = panelRect.left - controlsWidth - margin; + idealY = panelRect.top + (panelRect.height - controlsHeight) / 2; + break; + case "right": + idealX = panelRect.right + margin; + idealY = panelRect.top + (panelRect.height - controlsHeight) / 2; + break; + default: // Fallback to top-right + idealX = panelRect.right + margin; + idealY = panelRect.top; + break; + } + + let finalX = idealX; + let finalY = idealY; + + // For side-positioned controls, check if they are forced to move horizontally. + // If so, override their vertical alignment to be "top" instead of "center". + if (['left', 'right'].includes(controlsPosition)) { + if (idealX < margin || (idealX + controlsWidth) > (window.innerWidth - margin)) { + finalY = panelRect.top; // Switch to top alignment + + // Also, place it slightly inside the panel to ensure it's visible. + if (controlsPosition === 'left') { + finalX = margin; + } else { // 'right' + finalX = window.innerWidth - controlsWidth - margin; + } + } + } + + // Apply boundary constraints to the final calculated position + finalX = Math.max(margin, Math.min(finalX, window.innerWidth - controlsWidth - margin)); + finalY = Math.max(margin, Math.min(finalY, window.innerHeight - controlsHeight - margin)); + + this.uiManager.elements.controls.style.left = `${finalX}px`; + this.uiManager.elements.controls.style.top = `${finalY}px`; + + // Update layout class based on the setting + this.uiManager.updateControlsLayout(controlsPosition); + } + + applyPinnedPosition() { + let { x, y } = this.stateManager.state.pinnedPosition; + const panel = this.uiManager.elements.panel; + const panelWidth = panel.offsetWidth; + const panelHeight = panel.offsetHeight; + const margin = 10; + + // Ensure the pinned panel stays within the viewport boundaries, e.g., after content expands. + // This adjustment is now temporary and won't update the stored pinned position. + const boundedX = Math.max(margin, Math.min(x, window.innerWidth - panelWidth - margin)); + const boundedY = Math.max(margin, Math.min(y, window.innerHeight - panelHeight - margin)); + + panel.style.left = `${boundedX}px`; + panel.style.top = `${boundedY}px`; + } + + calculateNormalPosition() { + const settings = this.stateManager.state.settings; + const panelWidth = settings["🔍MagnifyGlass.InfoPanelWidth"]; + const panelHeight = Math.min(settings["🔍MagnifyGlass.InfoPanelMaxHeight"], this.uiManager.elements.panel.scrollHeight); + + let left, top; + const margin = 15; + + if (!this.stateManager.state.isGlassPreviewVisible) { + // Position at mouse cursor when glass is hidden + left = magnifyGlass.lastKnownMousePosition.x - (panelWidth / 2); + top = magnifyGlass.lastKnownMousePosition.y - 20; + } else { + // Position relative to magnify glass + const glassRect = magnifyGlass.ui.glassDiv?.getBoundingClientRect(); + if (glassRect) { + const position = settings["🔍MagnifyGlass.InfoPanelPosition"]; + + switch (position) { + case "Right": + left = glassRect.right + margin; + top = glassRect.top; + break; + case "Left": + left = glassRect.left - panelWidth - margin; + top = glassRect.top; + break; + case "Top": + left = glassRect.left; + top = glassRect.top - panelHeight - margin; + break; + case "Bottom": + left = glassRect.left; + top = glassRect.bottom + margin; + break; + default: + left = glassRect.right + margin; + top = glassRect.top; + break; + } + } else { + // Fallback to mouse position + left = magnifyGlass.lastKnownMousePosition.x - (panelWidth / 2); + top = magnifyGlass.lastKnownMousePosition.y - 20; + } + } + + // Apply boundary constraints + left = Math.max(10, Math.min(left, window.innerWidth - panelWidth - 10)); + top = Math.max(10, Math.min(top, window.innerHeight - panelHeight - 10)); + + this.uiManager.elements.panel.style.left = `${left}px`; + this.uiManager.elements.panel.style.top = `${top}px`; + } + } + + /** + * Event Manager + * Handles all event binding and delegation + */ + class EventManager { + constructor(stateManager, uiManager, positionManager) { + this.stateManager = stateManager; + this.uiManager = uiManager; + this.positionManager = positionManager; + + this.setupEventListeners(); + } + + setupEventListeners() { + this.setupPanelEvents(); + this.setupHotkeyEvents(); + this.setupDragEvents(); + this.setupHoverEvents(); + } + + setupPanelEvents() { + // Event delegation for panel controls + this.uiManager.elements.panel.addEventListener('click', (e) => { + const action = e.target.dataset.action; + if (action) { + e.stopPropagation(); + e.preventDefault(); + this.handleControlAction(action); + } + + // Section header clicks + const sectionHeader = e.target.closest('.section-header'); + if (sectionHeader && sectionHeader.dataset.section) { + const sectionId = sectionHeader.dataset.section; + if (sectionId !== 'node') { // Node section is always expanded + this.stateManager.toggleSection(sectionId); + this.uiManager.updateSectionStates(); + } + } + }); + + // Event delegation for floating controls (separate since they're not children of panel) + if (this.uiManager.elements.controls) { + this.uiManager.elements.controls.addEventListener('click', (e) => { + const action = e.target.dataset.action; + if (action) { + e.stopPropagation(); + e.preventDefault(); + this.handleControlAction(action); + } + }); + + // Prevent floating controls from propagating events + this.uiManager.elements.controls.addEventListener('mousedown', (e) => { + e.stopPropagation(); + }); + + this.uiManager.elements.controls.addEventListener('mouseup', (e) => { + e.stopPropagation(); + }); + } + + // Prevent panel interactions from propagating + this.uiManager.elements.panel.addEventListener('mousedown', (e) => { + e.stopPropagation(); + }); + + this.uiManager.elements.panel.addEventListener('mouseup', (e) => { + e.stopPropagation(); + }); + } + + setupHotkeyEvents() { + document.addEventListener('keydown', (e) => { + // Only handle hotkeys when magnifying glass is active + if (!magnifyGlass.state.active) return; + + const settings = this.stateManager.state.settings; + + // Toggle info panel + if (e.key.toLowerCase() === settings["🔍MagnifyGlass.ToggleHotkey"].toLowerCase() && !e.repeat) { + e.preventDefault(); + e.stopPropagation(); + this.handleControlAction('toggle-panel'); + } + + // Toggle glass preview + if (e.key.toLowerCase() === settings["🔍MagnifyGlass.GlassPreviewToggleHotkey"].toLowerCase() && !e.repeat) { + e.preventDefault(); + e.stopPropagation(); + this.handleControlAction('toggle-glass'); + } + + // Pin at mouse location with Alt + configured key + if (e.key.toLowerCase() === settings["🔍MagnifyGlass.PinPanelHotkey"].toLowerCase() && e.altKey && !e.repeat) { + e.preventDefault(); + e.stopPropagation(); + this.handleControlAction('pin-at-mouse'); + } + }); + } + + setupDragEvents() { + let dragState = { + isDragging: false, + startX: 0, + startY: 0, + currentX: 0, + currentY: 0, + offsetX: 0, + offsetY: 0 + }; + + // Start drag + const startDrag = (e) => { + // Only when pinned and not locked + if (!this.stateManager.state.isPanelPinned || this.stateManager.state.isPanelLocked) return; + + // Block on specific elements only + if (e.target.closest('.minimize-btn') || + e.target.closest('.control-btn') || + e.target.tagName === 'BUTTON') { + return; + } + + console.log('[DRAG] Starting drag'); + + // Stop all event propagation immediately + e.preventDefault(); + e.stopImmediatePropagation(); + + // Set drag state + dragState.isDragging = true; + dragState.startX = e.clientX; + dragState.startY = e.clientY; + + // Get current panel position + const rect = this.uiManager.elements.panel.getBoundingClientRect(); + dragState.currentX = rect.left; + dragState.currentY = rect.top; + dragState.offsetX = e.clientX - rect.left; + dragState.offsetY = e.clientY - rect.top; + + // Visual feedback + this.uiManager.elements.panel.classList.add('panel-dragging'); + this.uiManager.elements.panel.style.cursor = 'grabbing'; + document.body.style.cursor = 'grabbing'; + document.body.style.userSelect = 'none'; + + console.log('[DRAG] Drag started at:', { x: dragState.currentX, y: dragState.currentY }); + }; + + // Handle drag movement + const doDrag = (e) => { + if (!dragState.isDragging) return; + + e.preventDefault(); + e.stopImmediatePropagation(); + + // Calculate new position based on mouse movement + const newX = e.clientX - dragState.offsetX; + const newY = e.clientY - dragState.offsetY; + + // Apply boundaries + const panelWidth = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelWidth"]; + const panelHeight = this.uiManager.elements.panel.offsetHeight; + const margin = 10; + + const boundedX = Math.max(margin, Math.min(newX, window.innerWidth - panelWidth - margin)); + const boundedY = Math.max(margin, Math.min(newY, window.innerHeight - panelHeight - margin)); + + // Apply position immediately and forcefully + this.uiManager.elements.panel.style.position = 'fixed'; + this.uiManager.elements.panel.style.left = `${boundedX}px`; + this.uiManager.elements.panel.style.top = `${boundedY}px`; + this.uiManager.elements.panel.style.transform = 'none'; + this.uiManager.elements.panel.style.zIndex = '10010'; + + // Update current position + dragState.currentX = boundedX; + dragState.currentY = boundedY; + + // Update floating controls + if (this.uiManager.elements.controls) { + this.positionManager.positionFloatingControls(); + } + + console.log('[DRAG] Moving to:', { x: boundedX, y: boundedY }); + }; + + // End drag + const endDrag = (e) => { + if (!dragState.isDragging) return; + + console.log('[DRAG] Ending drag'); + + e.preventDefault(); + e.stopImmediatePropagation(); + + // Calculate if we actually moved significantly + const deltaX = Math.abs(e.clientX - dragState.startX); + const deltaY = Math.abs(e.clientY - dragState.startY); + const moved = deltaX > 5 || deltaY > 5; + + if (moved) { + // Save the final position + this.stateManager.setPinnedPosition(dragState.currentX, dragState.currentY); + console.log('[DRAG] Position saved:', { x: dragState.currentX, y: dragState.currentY }); + } + + // Reset state + dragState.isDragging = false; + + // Remove visual feedback + this.uiManager.elements.panel.classList.remove('panel-dragging'); + this.uiManager.elements.panel.style.cursor = ''; + this.uiManager.elements.panel.style.zIndex = ''; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + + // Cancel drag + const cancelDrag = () => { + if (!dragState.isDragging) return; + + console.log('[DRAG] Cancelling drag'); + + // Restore original position from pinned state + const { x, y } = this.stateManager.state.pinnedPosition; + this.uiManager.elements.panel.style.left = `${x}px`; + this.uiManager.elements.panel.style.top = `${y}px`; + + // Reset state + dragState.isDragging = false; + + // Remove visual feedback + this.uiManager.elements.panel.classList.remove('panel-dragging'); + this.uiManager.elements.panel.style.cursor = ''; + this.uiManager.elements.panel.style.zIndex = ''; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + + // Update controls + if (this.uiManager.elements.controls) { + this.positionManager.positionFloatingControls(); + } + }; + + // Attach events with capture to prevent interference + this.uiManager.elements.panel.addEventListener('mousedown', startDrag, { capture: true, passive: false }); + document.addEventListener('mousemove', doDrag, { capture: true, passive: false }); + document.addEventListener('mouseup', endDrag, { capture: true, passive: false }); + + // Handle escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + cancelDrag(); + } + }); + + // Handle mouse leaving window + document.addEventListener('mouseleave', endDrag); + + // Visual feedback + this.uiManager.elements.panel.addEventListener('mouseenter', () => { + if (this.stateManager.state.isPanelPinned && !this.stateManager.state.isPanelLocked && !dragState.isDragging) { + this.uiManager.elements.panel.style.cursor = 'grab'; + } + }); + + this.uiManager.elements.panel.addEventListener('mouseleave', () => { + if (!dragState.isDragging) { + this.uiManager.elements.panel.style.cursor = ''; + } + }); + + console.log('[DRAG] Event listeners attached with capture mode'); + + // Store cleanup function + this.dragCleanup = () => { + this.uiManager.elements.panel.removeEventListener('mousedown', startDrag, { capture: true }); + document.removeEventListener('mousemove', doDrag, { capture: true }); + document.removeEventListener('mouseup', endDrag, { capture: true }); + document.removeEventListener('mouseleave', endDrag); + cancelDrag(); + }; + } + + setupHoverEvents() { + this.uiManager.elements.panel.addEventListener('mouseenter', () => { + this.stateManager.state.isPanelHovered = true; + this.stateManager.clearAutoExpandTimer(); + }); + + this.uiManager.elements.panel.addEventListener('mouseleave', () => { + this.stateManager.state.isPanelHovered = false; + if (!this.stateManager.state.isHoveringNode) { + this.stateManager.scheduleAutoCollapse(); + } + }); + } + + captureCurrentPanelPosition() { + // Capture current position when pinning is activated + const panelRect = this.uiManager.elements.panel.getBoundingClientRect(); + this.stateManager.setPinnedPosition(panelRect.left, panelRect.top); + console.log(`Panel pinned at current position: (${panelRect.left}, ${panelRect.top})`); + } + + handleControlAction(action) { + switch (action) { + case 'minimize': + this.stateManager.toggleMinimized(); + this.uiManager.updateMinimizedState(); + break; + + case 'pin': + // If not currently pinned, either restore to last position or capture current + if (!this.stateManager.state.isPanelPinned) { + // If we don't have a remembered position, capture current position + if (!this.stateManager.state.lastPinnedPosition) { + this.captureCurrentPanelPosition(); + } + // Note: togglePinning will restore lastPinnedPosition if it exists + } + + this.stateManager.togglePinning(); + this.uiManager.updatePinnedState(); + this.positionManager.positionPanel(); + break; + + case 'lock': + this.stateManager.toggleLocking(); + this.uiManager.updatePinnedState(); // This updates both pinned and locked states + break; + + case 'pin-at-mouse': + // Pin at current mouse location (Alt + configured key) + const settings = this.stateManager.state.settings; + const panelWidth = settings["🔍MagnifyGlass.InfoPanelWidth"]; + const panelHeight = Math.min(settings["🔍MagnifyGlass.InfoPanelMaxHeight"], 400); + + // Position panel near mouse with some offset to avoid covering cursor area + const mouseX = magnifyGlass.lastKnownMousePosition.x; + const mouseY = magnifyGlass.lastKnownMousePosition.y; + const pinKey = settings["🔍MagnifyGlass.PinPanelHotkey"]; + + let pinX = mouseX + 50; // Offset to the right of mouse + let pinY = mouseY - 100; // Offset above mouse + + // Apply boundary constraints + pinX = Math.max(10, Math.min(pinX, window.innerWidth - panelWidth - 10)); + pinY = Math.max(10, Math.min(pinY, window.innerHeight - panelHeight - 10)); + + // Set the new pin position (this will also save as lastPinnedPosition) + this.stateManager.setPinnedPosition(pinX, pinY); + + // Enable pinning if not already pinned + if (!this.stateManager.state.isPanelPinned) { + this.stateManager.state.isPanelPinned = true; + } + + this.uiManager.updatePinnedState(); + this.positionManager.positionPanel(); + console.log(`Panel pinned at mouse location: (${pinX}, ${pinY}) using Alt+${pinKey.toUpperCase()}`); + break; + + case 'toggle-panel': + if (this.stateManager.togglePanelVisibility()) { + this.uiManager.show(); + } else { + this.uiManager.hide(); + } + this.uiManager.updateControlStates(); + break; + + case 'toggle-glass': + const isVisible = this.stateManager.toggleGlassPreview(); + this.applyGlassVisibility(isVisible); + + // When the glass preview is hidden, automatically pin the panel at its current location. + if (!isVisible && !this.stateManager.state.isPanelPinned) { + this.captureCurrentPanelPosition(); // Capture current position before pinning + this.stateManager.togglePinning(); + this.uiManager.updatePinnedState(); + } + + this.uiManager.updateControlStates(); + this.positionManager.positionPanel(); // Reposition based on new glass state + break; + } + } + + applyGlassVisibility(isVisible) { + if (magnifyGlass && magnifyGlass.ui && magnifyGlass.ui.glassDiv) { + magnifyGlass.ui.glassDiv.style.opacity = isVisible ? "1" : "0"; + } + } + } + + /** + * Information Gatherer + * Responsible for collecting information about the current state + */ + class InformationGatherer { + constructor() {} + + gatherInformation() { + const info = { + timestamp: Date.now(), + cursor: { + canvas: { x: magnifyGlass.state.x, y: magnifyGlass.state.y }, + screen: { + x: magnifyGlass.lastKnownMousePosition.x, + y: magnifyGlass.lastKnownMousePosition.y + } + }, + canvas: { + scale: magnifyGlass.state.canvasScale, + offset: { + x: magnifyGlass.state.canvasOffsetX, + y: magnifyGlass.state.canvasOffsetY + } + }, + magnifier: { + zoomFactor: magnifyGlass.config.zoomFactor, + offsetX: magnifyGlass.config.offsetX, + offsetY: magnifyGlass.config.offsetY, + sourceRegion: { + x: magnifyGlass.state.sourceX, + y: magnifyGlass.state.sourceY, + width: magnifyGlass.state.sourceWidth, + height: magnifyGlass.state.sourceHeight + } + }, + hoveredNode: null, + node: null, + widget: null, + connection: null, + media: null + }; + + if (magnifyGlass.isOverMedia && magnifyGlass.currentMediaElement) { + info.media = this.getMediaInfo(magnifyGlass.currentMediaElement); + } + + const nodeUnderCursor = this.getNodeUnderCursor(); + if (nodeUnderCursor) { + info.hoveredNode = this.getDetailedNodeInfo(nodeUnderCursor.node, nodeUnderCursor.localPos); + info.node = this.getNodeInfo(nodeUnderCursor.node); + + const widget = this.getWidgetUnderCursor(nodeUnderCursor.node, nodeUnderCursor.localPos); + if (widget) { + info.widget = this.getWidgetInfo(widget); + } + } + + return info; + } + + getNodeUnderCursor() { + if (!app.graph || !app.canvas) { + return null; + } + + try { + // Try to get the node from ComfyUI's state + if (app.canvas.node_over) { + const node = app.canvas.node_over; + if (node && node.pos && node.size) { + const canvasRect = app.canvas.canvas.getBoundingClientRect(); + const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; + const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; + + let graphPos; + if (app.canvas.convertOffsetToCanvasPos) { + graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); + } else { + const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; + graphPos = [ + (mouseX / ds.scale) - (ds.offset[0] / ds.scale), + (mouseY / ds.scale) - (ds.offset[1] / ds.scale) + ]; + } + + return { + node: node, + localPos: { + x: graphPos[0] - node.pos[0], + y: graphPos[1] - node.pos[1] + } + }; + } + } + + // Fallback to manual detection + const canvasRect = app.canvas.canvas.getBoundingClientRect(); + const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; + const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; + + let graphPos; + if (app.canvas.convertOffsetToCanvasPos) { + graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); + } else { + const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; + graphPos = [ + (mouseX / ds.scale) - (ds.offset[0] / ds.scale), + (mouseY / ds.scale) - (ds.offset[1] / ds.scale) + ]; + } + + for (let i = app.graph._nodes.length - 1; i >= 0; i--) { + const node = app.graph._nodes[i]; + if (!node.pos || !node.size || node.flags?.collapsed) continue; + + if (graphPos[0] >= node.pos[0] && + graphPos[0] <= node.pos[0] + node.size[0] && + graphPos[1] >= node.pos[1] && + graphPos[1] <= node.pos[1] + node.size[1]) { + + return { + node: node, + localPos: { + x: graphPos[0] - node.pos[0], + y: graphPos[1] - node.pos[1] + } + }; + } + } + } catch (err) { + console.warn("Error in node detection:", err); + } + + return null; + } + + getDetailedNodeInfo(node, localPos) { + return { + id: node.id, + title: node.title || "Untitled Node", + type: node.type, + mode: this.getNodeModeText(node.mode), + position: { + x: Math.round(node.pos[0]), + y: Math.round(node.pos[1]), + formatted: `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` + }, + size: { + width: Math.round(node.size[0]), + height: Math.round(node.size[1]), + formatted: `${Math.round(node.size[0])}×${Math.round(node.size[1])}` + }, + localPosition: { + x: Math.round(localPos.x), + y: Math.round(localPos.y), + formatted: `(${Math.round(localPos.x)}, ${Math.round(localPos.y)})`, + percentage: { + x: ((localPos.x / node.size[0]) * 100).toFixed(1), + y: ((localPos.y / node.size[1]) * 100).toFixed(1) + } + }, + counts: { + widgets: node.widgets ? node.widgets.length : 0, + inputs: node.inputs ? node.inputs.length : 0, + outputs: node.outputs ? node.outputs.length : 0, + properties: node.properties ? Object.keys(node.properties).length : 0 + }, + widgets: node.widgets || [], + inputs: node.inputs || [], + outputs: node.outputs || [], + properties: node.properties || {}, + hoverRegion: this.detectNodeRegion(localPos, node) + }; + } + + getNodeInfo(node) { + return { + id: node.id, + title: node.title || "Untitled", + type: node.type, + mode: node.mode, + size: node.size ? `${Math.round(node.size[0])}×${Math.round(node.size[1])}` : "Unknown", + position: node.pos ? `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` : "Unknown", + widgets: node.widgets || [], + inputs: node.inputs || [], + outputs: node.outputs || [], + properties: node.properties || {} + }; + } + + getWidgetUnderCursor(node, localPos) { + if (!node.widgets || !node.widgets.length) return null; + + const titleHeight = 30; + let currentY = titleHeight; + + for (const widget of node.widgets) { + const widgetHeight = 25; + + if (localPos.y >= currentY && localPos.y <= currentY + widgetHeight) { + return widget; + } + + currentY += widgetHeight + 5; + } + + return null; + } + + getWidgetInfo(widget) { + return { + name: widget.name, + type: widget.type, + value: this.formatValue(widget.value), + options: widget.options || null, + min: widget.min, + max: widget.max, + step: widget.step + }; + } + + getMediaInfo(mediaElement) { + const info = { + tagName: mediaElement.tagName, + src: mediaElement.src ? mediaElement.src.substring(mediaElement.src.lastIndexOf('/') + 1) : "No source" + }; + + if (mediaElement.tagName === 'IMG') { + info.naturalSize = `${mediaElement.naturalWidth}×${mediaElement.naturalHeight}`; + info.displaySize = `${Math.round(mediaElement.width)}×${Math.round(mediaElement.height)}`; + info.complete = mediaElement.complete; + } else if (mediaElement.tagName === 'VIDEO') { + info.videoSize = `${mediaElement.videoWidth}×${mediaElement.videoHeight}`; + info.duration = mediaElement.duration ? `${mediaElement.duration.toFixed(2)}s` : "Unknown"; + info.currentTime = `${mediaElement.currentTime.toFixed(2)}s`; + info.paused = mediaElement.paused; + info.readyState = mediaElement.readyState; + } + + return info; + } + + getNodeModeText(mode) { + const modes = { + 0: "Always", + 1: "On Event", + 2: "Never", + 3: "On Trigger", + 4: "On Request" + }; + return modes[mode] || `Mode ${mode}`; + } + + detectNodeRegion(localPos, node) { + const titleHeight = 30; + const regions = []; + + if (localPos.y <= titleHeight) { + regions.push("Title Bar"); + } + + if (localPos.x <= 10) { + regions.push("Left Edge"); + } else if (localPos.x >= node.size[0] - 10) { + regions.push("Right Edge"); + } + + if (localPos.y <= 10) { + regions.push("Top Edge"); + } else if (localPos.y >= node.size[1] - 10) { + regions.push("Bottom Edge"); + } + + if (regions.length === 0) { + if (localPos.y > titleHeight) { + regions.push("Content Area"); + } + } + + if (localPos.x <= 20 && localPos.y > titleHeight) { + regions.push("Input Area"); + } else if (localPos.x >= node.size[0] - 20 && localPos.y > titleHeight) { + regions.push("Output Area"); + } + + return regions.length > 0 ? regions.join(", ") : "Unknown"; + } + + formatValue(value) { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") { + return value.length > 30 ? value.substring(0, 27) + "..." : value; + } + if (typeof value === "number") { + return Number.isInteger(value) ? value.toString() : value.toFixed(3); + } + if (typeof value === "boolean") return value.toString(); + if (Array.isArray(value)) { + return `Array(${value.length})`; + } + if (typeof value === "object") { + return "Object"; + } + return String(value); + } + } + + /** + * Main Professional Info Panel Manager + * Orchestrates all the other components + */ + class ProfessionalInfoPanelManager { + constructor(magnifyGlass) { + this.magnifyGlass = magnifyGlass; + + // Initialize all managers + this.stateManager = new StateManager(); + this.uiManager = new UIManager(this.stateManager); + this.positionManager = new PositionManager(this.stateManager, this.uiManager); + this.eventManager = new EventManager(this.stateManager, this.uiManager, this.positionManager); + this.informationGatherer = new InformationGatherer(); + + this.hookIntoMagnifyGlass(); + + console.log("ComfyUI Magnify Info Panel Pro V2: Initialized successfully"); + } + + hookIntoMagnifyGlass() { + // Hook into magnify glass update cycle + const originalUpdateMagnifiedView = this.magnifyGlass.updateMagnifiedView.bind(this.magnifyGlass); + + this.magnifyGlass.updateMagnifiedView = (() => { + originalUpdateMagnifiedView(); + + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { + this.scheduleInfoUpdate(); + } + }).bind(this); + + // Hook into show/hide + const originalShow = this.magnifyGlass.ui.show.bind(this.magnifyGlass.ui); + const originalHide = this.magnifyGlass.ui.hide.bind(this.magnifyGlass.ui); + + this.magnifyGlass.ui.show = (() => { + originalShow(); + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { + this.uiManager.show(); + // Position controls after showing + setTimeout(() => this.positionManager.positionPanel(), 10); + } + }).bind(this); + + this.magnifyGlass.ui.hide = (() => { + originalHide(); + this.uiManager.hide(); + }).bind(this); + } + + scheduleInfoUpdate() { + if (this.stateManager.state.updateScheduled) return; + + this.stateManager.state.updateScheduled = true; + requestAnimationFrame(() => { + this.updateInfo(); + this.stateManager.state.updateScheduled = false; + }); + } + + updateInfo() { + if (!this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] || !this.magnifyGlass.state.active) return; + + const info = this.informationGatherer.gatherInformation(); + this.stateManager.setCurrentInfo(info); + this.uiManager.displayInfo(info); + this.positionManager.positionPanel(); + + // Handle node hover state + if (info.hoveredNode) { + this.stateManager.state.isHoveringNode = true; + this.stateManager.expandNodeSections(); + + if (this.stateManager.state.lastNodeId !== info.hoveredNode.id) { + this.stateManager.state.lastNodeId = info.hoveredNode.id; + } + } else { + this.stateManager.state.isHoveringNode = false; + + if (this.stateManager.state.lastNodeId !== null) { + this.stateManager.state.lastNodeId = null; + if (!this.stateManager.state.isPanelHovered) { + this.stateManager.scheduleAutoCollapse(); + } + } + } + } + + updateSettings() { + const changes = this.stateManager.updateSettings(); + + // React to setting changes + if (changes["🔍MagnifyGlass.InfoPanelTheme"]) { + this.uiManager.updateTheme(changes["🔍MagnifyGlass.InfoPanelTheme"].new); + } + + if (changes["🔍MagnifyGlass.ControlsPosition"]) { + // Update controls layout and position when position setting changes + this.positionManager.positionPanel(); + } + + // Apply new styles + this.uiManager.applyStyles(); + + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { + this.uiManager.show(); + } else { + this.uiManager.hide(); + } + } + + cleanup() { + this.stateManager.cleanup(); + this.uiManager.cleanup(); + } + } + + // Create the main manager instance + const infoPanelManager = new ProfessionalInfoPanelManager(magnifyGlass); + + // Store reference globally for cleanup + if (!window.comfyUIMagnifyGlassExtensions) { + window.comfyUIMagnifyGlassExtensions = []; + } + window.comfyUIMagnifyGlassExtensions.push(infoPanelManager); + + // Register settings with ComfyUI + Object.keys(DEFAULT_SETTINGS).forEach(settingKey => { + const settingConfig = getSettingConfig(settingKey, DEFAULT_SETTINGS[settingKey]); + if (settingConfig) { + app.ui.settings.addSetting({ + ...settingConfig, + onChange: (value) => { + infoPanelManager.updateSettings(); + } + }); + } + }); + + function getSettingConfig(key, defaultValue) { + const configs = { + "🔍MagnifyGlass.InfoPanelEnabled": { + id: key, + name: "📋 Magnify Glass: Info Panel", + type: "combo", + options: [ + { value: true, text: "Enabled" }, + { value: false, text: "Disabled" } + ], + defaultValue, + tooltip: "Enable or disable the professional information panel." + }, + "🔍MagnifyGlass.InfoPanelPosition": { + id: key, + name: "📋 Magnify Glass: Info Panel Position", + type: "combo", + options: [ + { value: "Right", text: "Right" }, + { value: "Left", text: "Left" }, + { value: "Top", text: "Top" }, + { value: "Bottom", text: "Bottom" } + ], + defaultValue, + tooltip: "Position of the info panel relative to the magnifying glass." + }, + "🔍MagnifyGlass.InfoPanelWidth": { + id: key, + name: "📋 Magnify Glass: Info Panel Width", + type: "slider", + defaultValue, + min: 250, + max: 450, + step: 10, + tooltip: "Width of the information panel in pixels." + }, + "🔍MagnifyGlass.InfoPanelOpacity": { + id: key, + name: "📋 Magnify Glass: Info Panel Opacity", + type: "slider", + defaultValue, + min: 0.7, + max: 1.0, + step: 0.05, + tooltip: "Opacity of the information panel background." + }, + "🔍MagnifyGlass.InfoPanelMaxHeight": { + id: key, + name: "📋 Magnify Glass: Info Panel Max Height", + type: "slider", + defaultValue, + min: 300, + max: 700, + step: 25, + tooltip: "Maximum height of the information panel in pixels." + }, + "🔍MagnifyGlass.InfoPanelTheme": { + id: key, + name: "🎨 Magnify Glass: Info Panel Theme", + type: "combo", + options: [ + { value: "Dark", text: "Dark" }, + { value: "Light", text: "Light" } + ], + defaultValue, + tooltip: "Color theme for the information panel." + }, + "🔍MagnifyGlass.InfoPanelAnimations": { + id: key, + name: "🎬 Magnify Glass: Info Panel Animations", + type: "combo", + options: [ + { value: true, text: "Enabled" }, + { value: false, text: "Disabled" } + ], + defaultValue, + tooltip: "Enable or disable animations for the info panel." + }, + "🔍MagnifyGlass.ShowInspectorTab": { + id: key, + name: "📋 Magnify Glass: Show Inspector Tab", + type: "combo", + options: [ + { value: true, text: "Enabled" }, + { value: false, text: "Disabled" } + ], + defaultValue, + tooltip: "Show or hide the Inspector tab with cursor and canvas information." + }, + "🔍MagnifyGlass.ToggleHotkey": { + id: key, + name: "🔑 Magnify Glass: Info Panel Toggle Hotkey", + type: "combo", + options: ["i", "p", "o", "l", "k"], + defaultValue, + tooltip: "Key to toggle the info panel visibility while the magnifier is active." + }, + "🔍MagnifyGlass.GlassPreviewToggleHotkey": { + id: key, + name: "🔑 Magnify Glass: Preview Toggle Hotkey", + type: "combo", + options: ["g", "v", "b", "m", "n"], + defaultValue, + tooltip: "Key to toggle the magnifying glass preview visibility." + }, + "🔍MagnifyGlass.PinPanelHotkey": { + id: key, + name: "📌 Magnify Glass: Pin Panel Hotkey", + type: "combo", + options: ["p", "f", "x", "z", "h"], + defaultValue, + tooltip: "Key to use with Alt to pin the info panel at mouse location (e.g., Alt+P)." + }, + "🔍MagnifyGlass.ShowHoveringControls": { + id: key, + name: "🎮 Magnify Glass: Show Hover Controls", + type: "combo", + options: [ + { value: true, text: "Enabled" }, + { value: false, text: "Disabled" } + ], + defaultValue, + tooltip: "Show or hide hovering UI controls above the info panel." + }, + "🔍MagnifyGlass.ControlsPosition": { + id: key, + name: "📍 Magnify Glass: Controls Position", + type: "combo", + options: [ + { value: "top-left", text: "Top-Left" }, + { value: "top-right", text: "Top-Right" }, + { value: "bottom-left", text: "Bottom-Left" }, + { value: "bottom-right", text: "Bottom-Right" }, + { value: "top", text: "Top (Centered)" }, + { value: "bottom", text: "Bottom (Centered)" }, + { value: "left", text: "Left (Centered)" }, + { value: "right", text: "Right (Centered)" } + ], + defaultValue, + tooltip: "Position of the floating control buttons relative to the info panel." + } + }; + + return configs[key] || null; + } + + console.log("ComfyUI Magnify Info Panel Pro V2: All settings registered and initialized"); + } + } +}); \ No newline at end of file