diff --git a/README.md b/README.md index 644e05b..c8d8145 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ComfyUI-MagnifyGlass -![Version](https://img.shields.io/badge/version-1.4.0-blue.svg) +![Version](https://img.shields.io/badge/version-1.4.1-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) @@ -14,6 +14,28 @@ ComfyUI-MagnifyGlass adds an intuitive magnifying glass to your ComfyUI canvas, ![Magnify Glass in Action Wepb](images/magnify_glass_action.webp) +## 🔥 Recent Improvements + +**Latest Session Updates:** + +- **🎯 Smart Input Detection**: Hotkeys now intelligently detect when you're typing in input fields (similar to ComfyUI's native 'F' key behavior) and won't activate, eliminating the need for Alt key requirements +- **🔓 Enhanced Panel Controls**: + - Pin button now properly toggles between unlocked (🔓) and locked (🔒) states + - 'U' key unlocks panel to mouse location without requiring Alt + - 'G' key intelligently respects manual pin/lock decisions and only auto-unlocks when appropriate +- **🎨 Visual Improvements**: + - Updated default border colors to gray (#6b7280) for better UI integration + - Added customizable info panel text and accent colors with smart defaults for light/dark modes + - Automatic theme synchronization with main ComfyUI frontend for dark and light modes +- **⚙️ Enhanced Settings**: + - Percentage-based controls for opacity (10-100%) and zoom factor (100-1000%) for finer control + - Fixed color picker persistence issues +- **🔄 Improved Reset Functionality**: Reset key ('O') now properly resets both magnify glass and inspector panel to default top-right positions +- **📱 UI Polish**: + - Inspector panel always appears above other UI elements with proper z-index layering + - Fixed blank settings dropdowns and improved tooltip descriptions + - Updated button tooltips to reflect current hotkey behavior + ## 🚀 Latest Updates **6/12/25 (v1.3.0)** diff --git a/pyproject.toml b/pyproject.toml index e440258..9fc17a5 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.4.0" +version = "1.4.1" license = {file = "LICENSE"} [project.urls] diff --git a/web/magnify_glass.js b/web/magnify_glass.js index 2d0026d..165582e 100644 --- a/web/magnify_glass.js +++ b/web/magnify_glass.js @@ -4,7 +4,7 @@ import { app } from "../../../scripts/app.js"; * ComfyUI Magnifying Glass * * This script adds a magnifying glass feature to ComfyUI. - * Hold Alt+X to activate the magnifying glass and see a zoomed view of the canvas. + * Press X to activate the magnifying glass and see a zoomed view of the canvas. */ app.registerExtension({ @@ -12,13 +12,13 @@ app.registerExtension({ async setup() { // Default settings configuration const DEFAULT_SETTINGS = { - "🔍MagnifyGlass.ZoomFactor": 3, + "🔍MagnifyGlass.ZoomFactor": 300, "🔍MagnifyGlass.GlassSize": 300, - "🔍MagnifyGlass.BorderColor": "#ffffff", - "🔍MagnifyGlass.BorderWidth": 2, + "🔍MagnifyGlass.BorderColor": "#6b7280", + "🔍MagnifyGlass.BorderWidth": 1, "🔍MagnifyGlass.ActivationKey": "x", "🔍MagnifyGlass.AltRequired": false, - "🔍MagnifyGlass.FollowCursor": true, + "🔍MagnifyGlass.FollowCursor": false, "🔍MagnifyGlass.DebugMode": false, "🔍MagnifyGlass.OffsetStep": 5, "🔍MagnifyGlass.GlassPosition": "Top-Right", @@ -26,7 +26,7 @@ app.registerExtension({ "🔍MagnifyGlass.GlassShape": "Rounded Square", "🔍MagnifyGlass.BorderEnabled": true, "🔍MagnifyGlass.TextureFiltering": "Linear", - "🔍MagnifyGlass.AlwaysActiveMode": false, + "🔍MagnifyGlass.AlwaysActiveMode": true, "🔍MagnifyGlass.ToggleFollowCursorKey": "h", }; @@ -94,7 +94,7 @@ app.registerExtension({ // Attach event handlers this.eventHandler.attachListeners(); - this.debugger.log(`Initialized (WebGL). Hold ${this.config.altRequired ? 'Alt+' : ''}${this.config.activationKey.toUpperCase()} to activate. Arrow keys to adjust offset, R to reset.`); + this.debugger.log(`Initialized (WebGL) with Smart Input Detection. Press ${this.config.altRequired ? 'Alt+' : ''}${this.config.activationKey.toUpperCase()} to activate. Hotkeys disabled while typing in inputs.`); } findLiteGraphCanvas() { @@ -345,7 +345,7 @@ app.registerExtension({ // Method to update config from settings (can be called from callbacks) updateConfigFromSettings() { - this.config.zoomFactor = getSettingValue("🔍MagnifyGlass.ZoomFactor", DEFAULT_SETTINGS["🔍MagnifyGlass.ZoomFactor"]); + this.config.zoomFactor = getSettingValue("🔍MagnifyGlass.ZoomFactor", DEFAULT_SETTINGS["🔍MagnifyGlass.ZoomFactor"]) / 100; this.config.glassSize = getSettingValue("🔍MagnifyGlass.GlassSize", DEFAULT_SETTINGS["🔍MagnifyGlass.GlassSize"]); this.config.borderColor = getSettingValue("🔍MagnifyGlass.BorderColor", DEFAULT_SETTINGS["🔍MagnifyGlass.BorderColor"]); this.config.borderWidth = getSettingValue("🔍MagnifyGlass.BorderWidth", DEFAULT_SETTINGS["🔍MagnifyGlass.BorderWidth"]); @@ -415,7 +415,40 @@ app.registerExtension({ if (this.state.active) { this.updateMagnifiedView(); // Update if active } - this.debugger.log("Offsets reset to 0, 0"); + + // Reset magnify glass position to top-right corner + if (this.ui.glassDiv) { + const glassSize = this.config.glassSize; + const padding = 20; // Same padding as initial positioning + this.ui.glassDiv.style.left = `${window.innerWidth - glassSize - padding}px`; + this.ui.glassDiv.style.top = `${padding}px`; + // Reset the activation flag so next time it uses default positioning + this.state.wasActivatedBefore = false; + } + + // Reset inspector panel position to default + if (window.comfyUIMagnifyGlassExtensions && window.comfyUIMagnifyGlassExtensions.length > 0) { + window.comfyUIMagnifyGlassExtensions.forEach(extension => { + if (extension && extension.stateManager) { + // Reset panel to default position + extension.stateManager.state.isPanelPinned = false; + extension.stateManager.state.isPanelLocked = false; + extension.stateManager.state.isAutoPinned = false; + extension.stateManager.state.pinnedPosition = { x: 0, y: 0 }; + extension.stateManager.state.lastPinnedPosition = null; + + // Update panel position and controls + if (extension.positionManager) { + extension.positionManager.positionPanel(); + } + if (extension.uiManager && extension.uiManager.updateControlStates) { + extension.uiManager.updateControlStates(); + } + } + }); + } + + this.debugger.log("Reset: Magnify glass and inspector panel positions restored to top-right defaults"); } } @@ -423,7 +456,7 @@ app.registerExtension({ class ConfigManager { constructor() { // Initialize properties that will be set by reading settings - this.zoomFactor = DEFAULT_SETTINGS["🔍MagnifyGlass.ZoomFactor"]; + this.zoomFactor = DEFAULT_SETTINGS["🔍MagnifyGlass.ZoomFactor"] / 100; this.glassSize = DEFAULT_SETTINGS["🔍MagnifyGlass.GlassSize"]; this.borderColor = DEFAULT_SETTINGS["🔍MagnifyGlass.BorderColor"]; this.borderWidth = DEFAULT_SETTINGS["🔍MagnifyGlass.BorderWidth"]; @@ -447,7 +480,7 @@ app.registerExtension({ // Call this after settings are registered and potentially read by MagnifyGlass loadSettings() { - this.zoomFactor = getSettingValue("🔍MagnifyGlass.ZoomFactor", this.zoomFactor); + this.zoomFactor = getSettingValue("🔍MagnifyGlass.ZoomFactor", this.zoomFactor * 100) / 100; this.glassSize = getSettingValue("🔍MagnifyGlass.GlassSize", this.glassSize); this.borderColor = getSettingValue("🔍MagnifyGlass.BorderColor", this.borderColor); this.borderWidth = getSettingValue("🔍MagnifyGlass.BorderWidth", this.borderWidth); @@ -543,7 +576,7 @@ app.registerExtension({ border: ${this.config.borderEnabled ? `${this.config.borderWidth}px solid ${this.config.borderColor}` : 'none'}; overflow: hidden; pointer-events: none; - z-index: 9999; + z-index: 98999; display: none; box-shadow: 0 5px 15px rgba(0,0,0,0.3); background-color: rgba(255,255,255,0.1); @@ -589,7 +622,7 @@ app.registerExtension({ right: 10px; background: rgba(0,0,0,0.7); border: 1px solid #fff; - z-index: 10000; + z-index: 99000; pointer-events: none; color: white; font-family: monospace; @@ -931,10 +964,43 @@ app.registerExtension({ document.addEventListener("mousemove", this.handleMouseMove.bind(this)); } + // Helper function to check if user is typing in an input field + isUserTyping() { + const activeElement = document.activeElement; + if (!activeElement) return false; + + // Check if the active element is an input, textarea, or contenteditable + const tagName = activeElement.tagName.toLowerCase(); + if (tagName === 'input' || tagName === 'textarea') { + return true; + } + + // Check for contenteditable elements + if (activeElement.contentEditable === 'true') { + return true; + } + + // Check if it's inside a form or has input-like classes + if (activeElement.closest('form') || + activeElement.classList.contains('cm-editor') || // CodeMirror editor + activeElement.classList.contains('monaco-editor') || // Monaco editor + activeElement.closest('.cm-editor') || + activeElement.closest('.monaco-editor')) { + return true; + } + + return false; + } + handleKeyDown(e) { const config = this.magnifyGlass.config; const state = this.magnifyGlass.state; + // Don't handle hotkeys if user is typing in an input field (Smart Input Detection) + if (this.isUserTyping()) { + return; + } + // Magnifier activation if (e.key.toLowerCase() === config.activationKey && (!config.altRequired || e.altKey)) { @@ -982,10 +1048,43 @@ app.registerExtension({ e.preventDefault(); } else if (e.key.toLowerCase() === config.resetKey.toLowerCase() && (!config.altRequired || e.altKey)) { // Use configured reset key & check Alt - // Reset offsets to zero + // Reset magnify glass offsets to zero config.offsetX = 0; config.offsetY = 0; offsetChanged = true; + + // Reset magnify glass position to top-right corner + if (this.magnifyGlass.ui.glassDiv) { + const glassSize = config.glassSize; + const padding = 20; // Same padding as initial positioning + this.magnifyGlass.ui.glassDiv.style.left = `${window.innerWidth - glassSize - padding}px`; + this.magnifyGlass.ui.glassDiv.style.top = `${padding}px`; + // Reset the activation flag so next time it uses default positioning + this.magnifyGlass.state.wasActivatedBefore = false; + } + + // Reset inspector panel position to default + if (window.comfyUIMagnifyGlassExtensions && window.comfyUIMagnifyGlassExtensions.length > 0) { + window.comfyUIMagnifyGlassExtensions.forEach(extension => { + if (extension && extension.stateManager) { + // Reset panel to default position + extension.stateManager.state.isPanelPinned = false; + extension.stateManager.state.isPanelLocked = false; + extension.stateManager.state.pinnedPosition = { x: 0, y: 0 }; + extension.stateManager.state.lastPinnedPosition = null; + + // Update panel position and controls + if (extension.positionManager) { + extension.positionManager.positionPanel(); + } + if (extension.uiManager && extension.uiManager.updateControlStates) { + extension.uiManager.updateControlStates(); + } + } + }); + } + + this.magnifyGlass.debugger.log("Reset: Magnify glass and inspector panel positions restored to top-right defaults"); e.preventDefault(); } @@ -1276,16 +1375,17 @@ app.registerExtension({ // Add settings to the ComfyUI settings dialog app.ui.settings.addSetting({ id: "🔍MagnifyGlass.ZoomFactor", - name: "🔍 Magnify Glass: Zoom Factor", + name: "🔍 Magnify Glass: Zoom Factor (%)", type: "slider", defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.ZoomFactor"], - min: 1.0, - max: 10.0, - step: 0.1, - tooltip: "Magnification level (e.g., 2.5 means 2.5x zoom).", + min: 100, + max: 1000, + step: 25, + tooltip: "Magnification level as a percentage (e.g., 300 = 3x zoom, 150 = 1.5x zoom).", onChange: (value) => { if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.zoomFactor = parseFloat(value); + // Convert percentage to decimal (e.g., 300 -> 3.0) + magnifyGlass.config.zoomFactor = value / 100; if (magnifyGlass.state.active) { magnifyGlass.updateMagnifiedView(); } @@ -1320,11 +1420,11 @@ app.registerExtension({ defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.BorderWidth"], min: 0, max: 10, - step: 1, - tooltip: "Width of the border around the magnifying glass.", + step: 0.1, + tooltip: "Width of the border around the magnifying glass (supports decimal values like 0.5, 1.5, etc.).", onChange: (value) => { if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.borderWidth = parseInt(value, 10); + magnifyGlass.config.borderWidth = parseFloat(value); magnifyGlass.applyUiChanges(); // Update border style } } @@ -1338,7 +1438,19 @@ app.registerExtension({ tooltip: "Color of the border around the magnifying glass.", onChange: (value) => { if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.borderColor = value; + // Ensure color value includes # symbol for valid CSS + const normalizedColor = value && !value.startsWith('#') ? `#${value}` : value; + magnifyGlass.config.borderColor = normalizedColor; + + // Only save back if we actually normalized the color (to prevent recursion) + if (value !== normalizedColor) { + try { + app.ui.settings.setSettingValue("🔍MagnifyGlass.BorderColor", normalizedColor); + } catch (e) { + console.warn('Failed to save normalized border color:', e); + } + } + magnifyGlass.applyUiChanges(); // Update border style } } @@ -1365,7 +1477,7 @@ app.registerExtension({ type: "combo", options: [ { value: true, text: "Yes" }, { value: false, text: "No" } ], defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.AltRequired"], - tooltip: "If Yes, Alt (Windows/Linux) or Option (Mac) must be held for activation and for reset.", + tooltip: "If Yes, Alt (Windows/Linux) or Option (Mac) must be held for activation and for reset. Not needed when 'Smart Input Detection' prevents hotkeys during typing.", onChange: (value) => { if (magnifyGlass && magnifyGlass.config) { magnifyGlass.config.altRequired = value; @@ -1410,7 +1522,7 @@ app.registerExtension({ type: "combo", options: ["r", "o", "p", "k", "l"], // Example keys, can be expanded defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.ResetKey"], - tooltip: "The key (case-insensitive) to press to reset the view offset while the magnifier is active. Works with Alt/Option if 'Require Alt for Reset' is Yes.", + tooltip: "The key (case-insensitive) to press to reset the magnify glass offset and inspector panel position to their default locations while the magnifier is active. Works with Alt/Option if 'Require Alt for Reset' is Yes.", onChange: (value) => { if (magnifyGlass && magnifyGlass.config) { magnifyGlass.config.resetKey = value.toLowerCase(); diff --git a/web/magnify_info_panel.js b/web/magnify_info_panel.js index 85bc2eb..c388522 100644 --- a/web/magnify_info_panel.js +++ b/web/magnify_info_panel.js @@ -42,18 +42,19 @@ app.registerExtension({ // Default settings configuration const DEFAULT_SETTINGS = { "🔍MagnifyGlass.InfoPanelEnabled": true, - "🔍MagnifyGlass.InfoPanelPosition": "Left", + "🔍MagnifyGlass.InfoPanelPosition": "Bottom", "🔍MagnifyGlass.InfoPanelWidth": 320, - "🔍MagnifyGlass.InfoPanelOpacity": 0.95, + "🔍MagnifyGlass.InfoPanelOpacity": 100, "🔍MagnifyGlass.InfoPanelMaxHeight": 500, - "🔍MagnifyGlass.InfoPanelTheme": "Dark", - "🔍MagnifyGlass.InfoPanelAnimations": true, + "🔍MagnifyGlass.InfoPanelAnimations": false, "🔍MagnifyGlass.ShowInspectorTab": false, "🔍MagnifyGlass.ToggleHotkey": "i", "🔍MagnifyGlass.GlassPreviewToggleHotkey": "g", - "🔍MagnifyGlass.PinPanelHotkey": "p", + "🔍MagnifyGlass.PinPanelHotkey": "u", "🔍MagnifyGlass.ShowHoveringControls": true, - "🔍MagnifyGlass.ControlsPosition": "bottom-centered", + "🔍MagnifyGlass.ControlsPosition": "bottom", + "🔍MagnifyGlass.InfoPanelTextColor": "#6b7280", + "🔍MagnifyGlass.InfoPanelAccentColor": "#3b82f6", }; const getSettingValue = (key, defaultValue) => { @@ -78,6 +79,7 @@ app.registerExtension({ isPanelMinimized: false, isPanelPinned: false, isPanelLocked: false, // New lock state + isAutoPinned: false, // Track if panel was auto-pinned by glass hide pinnedPosition: { x: 0, y: 0 }, lastPinnedPosition: null, // Remember last pinned location @@ -103,16 +105,265 @@ app.registerExtension({ currentInfo: {}, // Settings cache - settings: {} + settings: {}, + + // Auto-detected theme + currentTheme: 'dark' // Will be auto-detected }; + this.initThemeDetection(); this.loadSettings(); } + initThemeDetection() { + // Immediate forced detection + console.log('ComfyUI Magnify Glass: Starting theme detection...'); + + // Detect ComfyUI's current theme + this.detectCurrentTheme(); + + // Force an immediate check in case we missed the initial state + setTimeout(() => { + console.log('ComfyUI Magnify Glass: Delayed theme check...'); + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 1000); + + // Set up mutation observer to watch for theme changes + this.setupThemeObserver(); + + // Listen for system theme changes as fallback + if (window.matchMedia) { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); + prefersDark.addListener(() => this.detectCurrentTheme()); + } + + // Add click listener to theme toggle buttons for immediate detection + this.setupThemeButtonListeners(); + } + + detectCurrentTheme() { + let detectedTheme = 'dark'; // Default fallback + + const body = document.body; + const html = document.documentElement; + const vueApp = document.querySelector('#vue-app'); + const sidebar = document.querySelector('.comfy-menu, .sidebar, .menu, [class*="sidebar"], [class*="menu"]'); + + // Get all possible theme-related elements + const allElements = [body, html, vueApp, sidebar].filter(Boolean); + + // Debug: Log current classes and attributes with more detail + const debugInfo = { + bodyClasses: Array.from(body.classList), + htmlClasses: Array.from(html.classList), + bodyDataTheme: body.getAttribute('data-theme'), + htmlDataTheme: html.getAttribute('data-theme'), + vueAppClasses: vueApp ? Array.from(vueApp.classList) : null, + sidebarClasses: sidebar ? Array.from(sidebar.classList) : null, + bodyBgColor: window.getComputedStyle(body).backgroundColor, + htmlBgColor: window.getComputedStyle(html).backgroundColor, + allComputedVars: {} + }; + + // Check CSS variables + const rootStyles = window.getComputedStyle(html); + const bodyStyles = window.getComputedStyle(body); + + // Get all CSS custom properties + for (let i = 0; i < rootStyles.length; i++) { + const prop = rootStyles[i]; + if (prop.startsWith('--')) { + debugInfo.allComputedVars[prop] = rootStyles.getPropertyValue(prop).trim(); + } + } + + console.log('ComfyUI Magnify Glass: Comprehensive theme detection debug:', debugInfo); + + // Method 1: Look for specific light theme indicators in CSS variables + const primarySurface = rootStyles.getPropertyValue('--p-primary-color') || + rootStyles.getPropertyValue('--primary-color') || + bodyStyles.getPropertyValue('--p-primary-color') || + bodyStyles.getPropertyValue('--primary-color'); + + const surfaceGround = rootStyles.getPropertyValue('--p-surface-ground') || + rootStyles.getPropertyValue('--surface-ground') || + bodyStyles.getPropertyValue('--p-surface-ground') || + bodyStyles.getPropertyValue('--surface-ground'); + + const textColor = rootStyles.getPropertyValue('--p-text-color') || + rootStyles.getPropertyValue('--text-color') || + bodyStyles.getPropertyValue('--p-text-color') || + bodyStyles.getPropertyValue('--text-color'); + + console.log('ComfyUI Magnify Glass: CSS Variables:', { primarySurface, surfaceGround, textColor }); + + // Method 2: Analyze background colors of multiple elements + const backgrounds = allElements.map(el => { + const styles = window.getComputedStyle(el); + return { + element: el.tagName || el.className, + backgroundColor: styles.backgroundColor, + color: styles.color + }; + }); + + console.log('ComfyUI Magnify Glass: Background analysis:', backgrounds); + + // Method 3: Check for light backgrounds + for (const bg of backgrounds) { + if (bg.backgroundColor && bg.backgroundColor !== 'rgba(0, 0, 0, 0)' && bg.backgroundColor !== 'transparent') { + const rgbMatch = bg.backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (rgbMatch) { + const [, r, g, b] = rgbMatch.map(Number); + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + console.log(`ComfyUI Magnify Glass: Element ${bg.element} brightness: ${brightness}`); + if (brightness > 180) { // Higher threshold for light detection + detectedTheme = 'light'; + console.log(`ComfyUI Magnify Glass: Light theme detected via background analysis of ${bg.element}`); + break; + } + } + } + } + + // Method 4: Check for theme toggle buttons state (they might indicate current theme) + const themeButtons = document.querySelectorAll('[class*="theme"], button[title*="theme"], button[title*="Theme"], .p-button[title*="light"], .p-button[title*="Light"]'); + themeButtons.forEach(btn => { + console.log('ComfyUI Magnify Glass: Found theme button:', { + classes: Array.from(btn.classList), + text: btn.textContent, + title: btn.title, + active: btn.classList.contains('active') || btn.classList.contains('selected') + }); + }); + + // Method 5: Force light theme detection if we see light backgrounds + if (detectedTheme === 'dark') { + // Look for any element with very light background + const lightElements = document.querySelectorAll('*'); + for (let i = 0; i < Math.min(lightElements.length, 50); i++) { // Check first 50 elements + const el = lightElements[i]; + const styles = window.getComputedStyle(el); + const bgColor = styles.backgroundColor; + + if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') { + const rgbMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (rgbMatch) { + const [, r, g, b] = rgbMatch.map(Number); + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + if (brightness > 200) { // Very light elements + detectedTheme = 'light'; + console.log(`ComfyUI Magnify Glass: Light theme forced via element ${el.tagName}.${el.className} with brightness ${brightness}`); + break; + } + } + } + } + } + + // Update theme if it changed + if (this.state.currentTheme !== detectedTheme) { + console.log(`ComfyUI Magnify Glass: Theme changed from ${this.state.currentTheme} to ${detectedTheme}`); + this.state.currentTheme = detectedTheme; + return true; // Theme changed + } + + console.log(`ComfyUI Magnify Glass: Current theme remains: ${detectedTheme}`); + return false; // No change + } + + setupThemeObserver() { + // Watch for changes to body/html classes and attributes + const observer = new MutationObserver((mutations) => { + let shouldCheck = false; + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && + (mutation.attributeName === 'class' || + mutation.attributeName === 'data-theme' || + mutation.attributeName === 'style')) { + shouldCheck = true; + console.log('ComfyUI Magnify Glass: DOM change detected:', { + target: mutation.target.tagName, + attribute: mutation.attributeName, + oldValue: mutation.oldValue, + newValue: mutation.target.getAttribute(mutation.attributeName) + }); + } + }); + + if (shouldCheck && this.detectCurrentTheme()) { + // Theme changed, update UI immediately + this.notifyThemeChange(); + } + }); + + // Observe changes to body, html, and vue app + observer.observe(document.body, { attributes: true, attributeOldValue: true }); + observer.observe(document.documentElement, { attributes: true, attributeOldValue: true }); + + const vueApp = document.querySelector('#vue-app'); + if (vueApp) { + observer.observe(vueApp, { attributes: true, attributeOldValue: true }); + } + + // Also watch for changes to any element with theme-related classes + const themeElements = document.querySelectorAll('[class*="theme"], [class*="dark"], [class*="light"], [data-theme]'); + themeElements.forEach(el => { + observer.observe(el, { attributes: true, attributeOldValue: true }); + }); + + // Periodic fallback check every 2 seconds + setInterval(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 2000); + } + + setupThemeButtonListeners() { + // Listen for clicks on theme toggle buttons + document.addEventListener('click', (e) => { + // Check if clicked element might be a theme toggle + const target = e.target; + const isThemeButton = target.textContent?.toLowerCase().includes('theme') || + target.textContent?.toLowerCase().includes('light') || + target.textContent?.toLowerCase().includes('dark') || + target.title?.toLowerCase().includes('theme') || + target.className?.toLowerCase().includes('theme'); + + if (isThemeButton) { + console.log('ComfyUI Magnify Glass: Theme button clicked, triggering detection'); + setTimeout(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 100); + } + }); + } + + notifyThemeChange() { + // Update settings to reflect new theme + this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; + + // Notify UI components of theme change immediately + if (window.infoPanelManager && window.infoPanelManager.uiManager) { + window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme); + } + } + loadSettings() { Object.keys(DEFAULT_SETTINGS).forEach(key => { - this.state.settings[key] = getSettingValue(key, DEFAULT_SETTINGS[key]); + // Skip the theme setting since we auto-detect it + if (key !== "🔍MagnifyGlass.InfoPanelTheme") { + this.state.settings[key] = getSettingValue(key, DEFAULT_SETTINGS[key]); + } }); + + // Set theme to auto-detected value + this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; } updateSettings() { @@ -149,9 +400,10 @@ app.registerExtension({ const wasPinned = this.state.isPanelPinned; this.state.isPanelPinned = !this.state.isPanelPinned; - // When unpinning, also unlock + // When unpinning, also unlock and clear auto-pin flag if (!this.state.isPanelPinned) { this.state.isPanelLocked = false; + this.state.isAutoPinned = false; // Clear auto-pin flag when manually unpinning } if (this.state.isPanelPinned && this.state.lastPinnedPosition) { @@ -266,7 +518,7 @@ app.registerExtension({ // 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()}`; + this.elements.panel.className = `magnify-info-panel theme-${this.stateManager.state.currentTheme}`; // Header this.elements.header = document.createElement("div"); @@ -304,10 +556,10 @@ app.registerExtension({ 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 @@ -329,6 +581,7 @@ app.registerExtension({ if (pinBtn) { pinBtn.classList.toggle('active', this.stateManager.state.isPanelPinned); pinBtn.title = this.stateManager.state.isPanelPinned ? "Lock Panel" : "Unlock Panel"; + pinBtn.textContent = this.stateManager.state.isPanelPinned ? "🔒" : "🔓"; } if (lockBtn) { @@ -367,11 +620,33 @@ app.registerExtension({ applyStyles() { const settings = this.stateManager.state.settings; + // Apply custom text color via CSS variable if provided + const textColor = settings["🔍MagnifyGlass.InfoPanelTextColor"]; + if (textColor) { + // Ensure color has # symbol + const normalizedTextColor = textColor.startsWith('#') ? textColor : `#${textColor}`; + this.elements.panel.style.setProperty('--info-panel-text-color', normalizedTextColor); + } + + // Apply custom accent color via CSS variable if provided + const accentColor = settings["🔍MagnifyGlass.InfoPanelAccentColor"]; + if (accentColor) { + // Ensure color has # symbol + const normalizedAccentColor = accentColor.startsWith('#') ? accentColor : `#${accentColor}`; + this.elements.panel.style.setProperty('--info-panel-accent-color', normalizedAccentColor); + } + + // Apply opacity setting if panel is visible (convert percentage to decimal) + if (this.stateManager.state.isPanelVisible) { + const opacityPercent = settings["🔍MagnifyGlass.InfoPanelOpacity"]; + this.elements.panel.style.opacity = opacityPercent / 100; + } + this.elements.panel.style.cssText = ` position: absolute; width: ${settings["🔍MagnifyGlass.InfoPanelWidth"]}px; max-height: ${settings["🔍MagnifyGlass.InfoPanelMaxHeight"]}px; - z-index: 10001; + z-index: 99999; display: none; opacity: 0; transform: translateY(-10px); @@ -379,6 +654,8 @@ app.registerExtension({ pointer-events: auto; user-select: none; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + ${textColor ? `--info-panel-text-color: ${textColor.startsWith('#') ? textColor : `#${textColor}`};` : ''} + ${accentColor ? `--info-panel-accent-color: ${accentColor.startsWith('#') ? accentColor : `#${accentColor}`};` : ''} `; } @@ -388,6 +665,9 @@ app.registerExtension({ if (this.elements.controls) { this.elements.controls.style.display = "flex"; } + // Apply user's opacity setting when showing (convert percentage to decimal) + const opacityPercent = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelOpacity"]; + this.elements.panel.style.opacity = opacityPercent / 100; // Trigger reflow this.elements.panel.offsetHeight; this.elements.panel.classList.add('visible'); @@ -424,7 +704,10 @@ app.registerExtension({ } updateTheme(newTheme) { - this.elements.panel.className = this.elements.panel.className.replace(/theme-\w+/, `theme-${newTheme.toLowerCase()}`); + if (this.elements.panel) { + this.elements.panel.className = this.elements.panel.className.replace(/theme-\w+/, `theme-${newTheme.toLowerCase()}`); + console.log(`ComfyUI Magnify Glass: Panel theme updated to ${newTheme}`); + } } displayInfo(info) { @@ -571,15 +854,16 @@ app.registerExtension({ updateHeaderSubtitle(info) { const subtitleElement = this.elements.header.querySelector('.header-subtitle'); if (subtitleElement) { + const accentColor = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"]; if (info.hoveredNode) { subtitleElement.textContent = `Analyzing: ${info.hoveredNode.title}`; - subtitleElement.style.color = '#74b9ff'; + subtitleElement.style.color = accentColor; } else if (info.media) { subtitleElement.textContent = `Media: ${info.media.tagName}`; - subtitleElement.style.color = '#a0d468'; + subtitleElement.style.color = accentColor; } else if (info.connection) { subtitleElement.textContent = `Connection: ${info.connection.type}`; - subtitleElement.style.color = '#fdcb6e'; + subtitleElement.style.color = accentColor; } else { subtitleElement.textContent = 'Real-time analysis'; subtitleElement.style.color = ''; @@ -763,18 +1047,17 @@ app.registerExtension({ .magnify-info-panel.theme-dark { background: linear-gradient(135deg, rgba(0, 0, 0, 0.95), rgba(10, 10, 15, 0.95)); - color: #e0e0e0; + color: var(--info-panel-text-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; + color: var(--info-panel-text-color, #2a2a2a); border: 1px solid rgba(200, 200, 220, 0.4); } .magnify-info-panel.visible { - opacity: 1 !important; transform: translateY(0) !important; } @@ -803,7 +1086,7 @@ app.registerExtension({ 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; + z-index: 100001 !important; transition: none !important; } @@ -839,7 +1122,7 @@ app.registerExtension({ border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px; opacity: 1; - z-index: 10003; + z-index: 100000; backdrop-filter: blur(8px); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); } @@ -1008,7 +1291,7 @@ app.registerExtension({ .section-header.expanded { background: rgba(116, 185, 255, 0.1); - border-left: 2px solid #74b9ff; + border-left: 2px solid var(--info-panel-accent-color, #74b9ff); } .info-section[data-section="node"] { @@ -1030,7 +1313,7 @@ app.registerExtension({ } .info-section[data-section="node"] .section-title { - color: #74b9ff; + color: var(--info-panel-accent-color, #74b9ff); font-weight: 700; } @@ -1064,7 +1347,7 @@ app.registerExtension({ } .info-section[data-section="media"] .section-title { - color: #fdcb6e; + color: var(--info-panel-accent-color, #fdcb6e); font-weight: 700; } @@ -1098,12 +1381,12 @@ app.registerExtension({ .section-header:hover .expand-icon { opacity: 1; - color: #74b9ff; + color: var(--info-panel-accent-color, #74b9ff); } .section-header.expanded .expand-icon { transform: rotate(90deg); - color: #74b9ff; + color: var(--info-panel-accent-color, #74b9ff); } .section-content { @@ -1141,7 +1424,7 @@ app.registerExtension({ .info-value { font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 14px; - color: #a0d468; + color: var(--info-panel-accent-color, #a0d468); background: rgba(160, 212, 104, 0.1); padding: 3px 8px; border-radius: 4px; @@ -1442,29 +1725,64 @@ app.registerExtension({ }); } + // Helper function to check if user is typing in an input field + isUserTyping() { + const activeElement = document.activeElement; + if (!activeElement) return false; + + // Check if the active element is an input, textarea, or contenteditable + const tagName = activeElement.tagName.toLowerCase(); + if (tagName === 'input' || tagName === 'textarea') { + return true; + } + + // Check for contenteditable elements + if (activeElement.contentEditable === 'true') { + return true; + } + + // Check if it's inside a form or has input-like classes + if (activeElement.closest('form') || + activeElement.classList.contains('cm-editor') || // CodeMirror editor + activeElement.classList.contains('monaco-editor') || // Monaco editor + activeElement.closest('.cm-editor') || + activeElement.closest('.monaco-editor')) { + return true; + } + + return false; + } + setupHotkeyEvents() { document.addEventListener('keydown', (e) => { + // Don't handle hotkeys if user is typing in an input field (Smart Input Detection) + if (this.isUserTyping()) { + return; + } + // 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) { + if (e.key.toLowerCase() === settings["🔍MagnifyGlass.ToggleHotkey"].toLowerCase() && + (!magnifyGlass.config.altRequired || e.altKey) && !e.repeat) { e.preventDefault(); e.stopPropagation(); this.handleControlAction('toggle-panel'); } // Toggle glass preview - if (e.key.toLowerCase() === settings["🔍MagnifyGlass.GlassPreviewToggleHotkey"].toLowerCase() && !e.repeat) { + if (e.key.toLowerCase() === settings["🔍MagnifyGlass.GlassPreviewToggleHotkey"].toLowerCase() && + (!magnifyGlass.config.altRequired || e.altKey) && !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) { + // Unlock panel from magnify glass and move to current mouse location (no Alt required due to Smart Input Detection) + if (e.key.toLowerCase() === settings["🔍MagnifyGlass.PinPanelHotkey"].toLowerCase() && !e.repeat) { e.preventDefault(); e.stopPropagation(); this.handleControlAction('pin-at-mouse'); @@ -1754,6 +2072,14 @@ app.registerExtension({ if (!isVisible && !this.stateManager.state.isPanelPinned) { this.captureCurrentPanelPosition(); // Capture current position before pinning this.stateManager.togglePinning(); + this.stateManager.state.isAutoPinned = true; // Mark as auto-pinned + this.uiManager.updatePinnedState(); + } + + // When the glass preview is shown again, only auto-unlock if NOT manually locked with 📌 button + if (isVisible && this.stateManager.state.isPanelPinned && !this.stateManager.state.isPanelLocked) { + this.stateManager.togglePinning(); // Unpin the panel + this.stateManager.state.isAutoPinned = false; // Clear auto-pin flag this.uiManager.updatePinnedState(); } @@ -2170,10 +2496,8 @@ app.registerExtension({ updateSettings() { const changes = this.stateManager.updateSettings(); - // React to setting changes - if (changes["🔍MagnifyGlass.InfoPanelTheme"]) { - this.uiManager.updateTheme(changes["🔍MagnifyGlass.InfoPanelTheme"].new); - } + // React to setting changes (theme is now auto-detected) + // Theme changes are handled automatically by the observer if (changes["🔍MagnifyGlass.ControlsPosition"]) { // Update controls layout and position when position setting changes @@ -2199,12 +2523,15 @@ app.registerExtension({ // Create the main manager instance const infoPanelManager = new ProfessionalInfoPanelManager(magnifyGlass); - // Store reference globally for cleanup + // Store reference globally for cleanup and theme updates if (!window.comfyUIMagnifyGlassExtensions) { window.comfyUIMagnifyGlassExtensions = []; } window.comfyUIMagnifyGlassExtensions.push(infoPanelManager); + // Store main manager reference for theme updates + window.infoPanelManager = infoPanelManager; + // Register settings with ComfyUI Object.keys(DEFAULT_SETTINGS).forEach(settingKey => { const settingConfig = getSettingConfig(settingKey, DEFAULT_SETTINGS[settingKey]); @@ -2256,13 +2583,20 @@ app.registerExtension({ }, "🔍MagnifyGlass.InfoPanelOpacity": { id: key, - name: "📋 Magnify Glass: Info Panel Opacity", + 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." + min: 10, + max: 100, + step: 5, + tooltip: "Opacity of the information panel background as a percentage (10 = very transparent, 100 = fully opaque).", + onChange: (value) => { + // Convert percentage to decimal and apply opacity immediately + const opacity = value / 100; + if (window.infoPanelManager && window.infoPanelManager.uiManager && window.infoPanelManager.uiManager.elements.panel) { + window.infoPanelManager.uiManager.elements.panel.style.opacity = opacity; + } + } }, "🔍MagnifyGlass.InfoPanelMaxHeight": { id: key, @@ -2274,17 +2608,7 @@ app.registerExtension({ 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", @@ -2325,11 +2649,11 @@ app.registerExtension({ }, "🔍MagnifyGlass.PinPanelHotkey": { id: key, - name: "📌 Magnify Glass: Pin Panel Hotkey", + name: "🔓 Magnify Glass: Unlock Panel to Mouse Location", type: "combo", - options: ["p", "f", "x", "z", "h"], + options: ["u", "p", "f", "x", "z"], defaultValue, - tooltip: "Key to use with Alt to pin the info panel at mouse location (e.g., Alt+P)." + tooltip: "Key to unlock the info panel from the magnify glass and move it to your current mouse position (e.g., U). Panel remains movable - use the pin button to lock it in place. Alt not required due to Smart Input Detection." }, "🔍MagnifyGlass.ShowHoveringControls": { id: key, @@ -2358,6 +2682,56 @@ app.registerExtension({ ], defaultValue, tooltip: "Position of the floating control buttons relative to the info panel." + }, + "🔍MagnifyGlass.InfoPanelTextColor": { + id: key, + name: "🎨 Magnify Glass: Info Panel Text Color", + type: "color", + defaultValue, + tooltip: "Color of the text in the info panel. This affects all text content including titles, values, and labels.", + onChange: (value) => { + // Ensure color value includes # symbol for valid CSS and persistence + const normalizedColor = value && !value.startsWith('#') ? `#${value}` : value; + + // Only save back if we actually normalized the color (to prevent recursion) + if (value !== normalizedColor) { + try { + app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelTextColor", normalizedColor); + } catch (e) { + console.warn('Failed to save normalized text color:', e); + } + } + + // Apply the color immediately + if (window.infoPanelManager && window.infoPanelManager.uiManager) { + window.infoPanelManager.uiManager.applyStyles(); + } + } + }, + "🔍MagnifyGlass.InfoPanelAccentColor": { + id: key, + name: "🎨 Magnify Glass: Info Panel Accent Color", + type: "color", + defaultValue, + tooltip: "Color used for highlighted elements like values, section titles, and active states in the info panel.", + onChange: (value) => { + // Ensure color value includes # symbol for valid CSS and persistence + const normalizedColor = value && !value.startsWith('#') ? `#${value}` : value; + + // Only save back if we actually normalized the color (to prevent recursion) + if (value !== normalizedColor) { + try { + app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelAccentColor", normalizedColor); + } catch (e) { + console.warn('Failed to save normalized accent color:', e); + } + } + + // Apply the color immediately + if (window.infoPanelManager && window.infoPanelManager.uiManager) { + window.infoPanelManager.uiManager.applyStyles(); + } + } } };