diff --git a/.gitignore b/.gitignore index 0a6f88e..0a84992 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,36 @@ # Python __pycache__/ -*.pyc -*.pyo -*.pyd +*.py[cod] +*$py.class +*.so +.Python + +# Virtual environments +venv/ +env/ +ENV/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +debug_logs/ + +# Temporal +*.tmp +*.temp + +# Settings personales (opcional) +watchpoint_debug_config.json # State files (no subir el JSON con datos) -data/list_state.json +beta/ -# IDE -.vscode/ -.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 435d12f..b8e750b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 👁️ ComfyUI Watch Point - +![alt text](media/preview.png) **Dual preview system for ComfyUI workflows** Watch Point provides two simultaneous preview methods: @@ -16,20 +16,40 @@ Watch Point provides two simultaneous preview methods: - 📋 Copy to clipboard - ⚙️ Customizable settings - 📡 **Signal Scout Panel**: Display text from your workflow in a side panel. +- 🔒 **Thread Safety**: Windows minimize instead of closing to prevent ComfyUI crashes ### Floating Preview (JavaScript) - 📌 Uses ComfyUI's native preview system - 🎨 Works with existing preview extensions - 🔄 Auto-updates during workflow execution +- 💾 Configurable via browser localStorage (no file editing) +- ⌨️ Customizable keyboard shortcuts +- 🎯 Snap-to-grid positioning +- 👻 Adjustable opacity levels + +## Thread Safety & Window Protection + +WatchPoint includes built-in protection against threading issues that can crash ComfyUI: + +### 🛡️ Window Minimization Protection +- **Close Button Behavior**: Clicking the Windows close button minimizes the window instead of closing it +- **Thread Safety**: Prevents `Tcl_AsyncDelete` errors and threading conflicts +- **ComfyUI Stability**: Ensures ComfyUI continues running even if windows are "minimized" +- **Window Recovery**: Minimized windows can be restored from the taskbar + +### 🔧 Technical Details +- Uses thread-safe Tkinter operations +- Implements proper cleanup procedures +- Prevents memory leaks and threading deadlocks +- Global window management for consistent behavior ## Installation 1. Clone into your ComfyUI custom_nodes directory: ```bash cd ComfyUI/custom_nodes -git clone https://github.com/yourusername/ComfyUI-WatchPoint.git +git clone https://github.com/gmorks/ComfyUI-WatchPoint.git ``` - 2. Install dependencies (if needed): ```bash pip install pillow screeninfo @@ -96,6 +116,12 @@ A utility node to restore minimized WatchPoint windows. - **1**: Zoom 1:1 (100% actual size) - **ESC**: Close window +### Window Close Behavior +**Important**: The Windows close button (❌) minimizes the window instead of closing it. This prevents threading errors and keeps ComfyUI stable. To "close" the window: +- Click the close button → Window minimizes to taskbar +- Use **ESC** key to properly close the window +- Right-click taskbar icon → "Close window" for forced closure + ### Toolbar Buttons - **↻ Reset**: Reset zoom and pan - **⊕ Zoom In**: Increase zoom level @@ -151,9 +177,10 @@ except ImportError as e: ### Disabling Utility Nodes Comment out or remove the import block in `__init__.py` to disable utility nodes. -## Configuration File +## Configuration Files -Edit `watchpoint_settings.json` to customize defaults: +### Monitor Preview Settings +Edit `watchpoint_settings.json` to customize defaults for the external Tkinter window: ```json { @@ -168,13 +195,149 @@ Edit `watchpoint_settings.json` to customize defaults: } ``` -### Settings Explained +#### Settings Explained - `window_x`, `window_y`: Window position (null = auto) - `window_size_mode`: "fixed", "Half Vertical", "Half Horizontal", or "Quarter" - `show_toolbar`: Show toolbar on startup - `save_format`: Default save format ("png", "jpeg", "webp") - `jpeg_quality`: JPEG compression quality (10-100) +### Floating Preview Configuration +The floating preview window is configured via browser localStorage. No file editing required! + +#### Quick Start + +**View Current Configuration** + +Open browser console (F12) and run: +```javascript +WatchPointFloating.getConfig() +``` + +**Change Configuration** +```javascript +WatchPointFloating.setConfig({ + window: { + defaultWidth: 500, + defaultHeight: 500, + defaultPosition: "top-right", + defaultOpacity: 80 + }, + shortcuts: { + toggle: "Ctrl+Alt+KeyP" // Change toggle shortcut + } +}) +``` + +**Reset to Defaults** +```javascript +WatchPointFloating.resetConfig() +``` + +**Show Help** +```javascript +WatchPointFloating.showHelp() +``` + +**Note**: Reload the page after changing config to apply changes. + +#### Available Configuration Options + +##### Window Settings +```javascript +{ + window: { + defaultWidth: 320, // Width in pixels + defaultHeight: 320, // Height in pixels + defaultPosition: "middle-right", // Initial position + defaultOpacity: 100 // 0-100 (percentage) + } +} +``` + +**Available Positions**: +- `"top-left"`, `"top-center"`, `"top-right"` +- `"middle-left"`, `"middle-center"`, `"middle-right"` +- `"bottom-left"`, `"bottom-center"`, `"bottom-right"` + +##### Opacity Settings +```javascript +{ + opacity: { + levels: [20, 40, 60, 80, 100], // Available opacity levels (%) + showIndicator: true, // Show opacity % when changing + indicatorDuration: 1000 // How long indicator shows (ms) + } +} +``` + +##### UI Settings +```javascript +{ + ui: { + showHeader: true, // Show title bar + showTitle: true, // Show "👁️ Watch Point" title + showCloseButton: true, // Show X button + dragFromAnywhere: false // Drag from image area (only if header=false) + } +} +``` + +##### Keyboard Shortcuts +```javascript +{ + shortcuts: { + // Main controls + toggle: "Ctrl+Alt+KeyW", + increaseOpacity: "Ctrl+Alt+Equal", // or Ctrl+Alt++ + decreaseOpacity: "Ctrl+Alt+Minus", // or Ctrl+Alt+- + + // Arrow navigation (relative) + snapUp: "Ctrl+Alt+ArrowUp", + snapDown: "Ctrl+Alt+ArrowDown", + snapLeft: "Ctrl+Alt+ArrowLeft", + snapRight: "Ctrl+Alt+ArrowRight", + + // Numpad navigation (absolute positions) + snapToPosition7: "Ctrl+Alt+Numpad7", // Top-left + snapToPosition8: "Ctrl+Alt+Numpad8", // Top-center + snapToPosition9: "Ctrl+Alt+Numpad9", // Top-right + snapToPosition4: "Ctrl+Alt+Numpad4", // Middle-left + snapToPosition5: "Ctrl+Alt+Numpad5", // Middle-center + snapToPosition6: "Ctrl+Alt+Numpad6", // Middle-right + snapToPosition1: "Ctrl+Alt+Numpad1", // Bottom-left + snapToPosition2: "Ctrl+Alt+Numpad2", // Bottom-center + snapToPosition3: "Ctrl+Alt+Numpad3" // Bottom-right + } +} +``` + +#### Default Keyboard Shortcuts + +| Action | Shortcut | Description | +|--------|----------|-------------| +| **Toggle window** | `Ctrl+Alt+W` | Show/hide floating preview | +| **Increase opacity** | `Ctrl+Alt++` | Make more opaque | +| **Decrease opacity** | `Ctrl+Alt+-` | Make more transparent | +| **Move up** | `Ctrl+Alt+↑` | Snap to position above | +| **Move down** | `Ctrl+Alt+↓` | Snap to position below | +| **Move left** | `Ctrl+Alt+←` | Snap to position left | +| **Move right** | `Ctrl+Alt+→` | Snap to position right | +| **Snap to corner** | `Ctrl+Alt+Numpad[1-9]` | Jump to specific position | + +#### Storage Location + +Configuration is stored in browser localStorage under the key: +``` +watchpoint-floating-config +``` + +This means: +- ✅ Configuration persists between sessions +- ✅ Each browser/profile has independent settings +- ✅ No server-side storage needed +- ⚠️ Clearing browser data will reset to defaults + ## Use Cases ### Dual Monitor Setup @@ -232,8 +395,22 @@ pip install pywin32 ## Credits -Created for ComfyUI by [Your Name] +Vibe Coded for ComfyUI by gmorks using AI. ## License -MIT License - Feel free to use and modify! \ No newline at end of file +MIT License - Feel free to use and modify! + +## 🚧 Development Status + +**Note**: This extension is not yet compatible with ComfyUI Nodes 2.0. Support for the new node system is on our to-do list and will be implemented in a future update. + +Stay tuned for updates! + +## Recent Changes + +### v1.0.0 - Initial Release +- Dual preview system (Monitor + Floating) +- Signal Scout text display +- Basic window controls and settings +- Multi-monitor support \ No newline at end of file diff --git a/WATCHPOINT_CONSERVATION.md b/WATCHPOINT_CONSERVATION.md deleted file mode 100644 index 7cee7b0..0000000 --- a/WATCHPOINT_CONSERVATION.md +++ /dev/null @@ -1,167 +0,0 @@ -# 🛡️ WatchPoint Conservation Guide - -## 📋 Document Purpose -This document serves as a "contra PRD" - a conservation guide for maintaining WatchPoint functionality while allowing future modifications. It outlines what MUST be preserved, what can be modified, and critical implementation details. - -## 🔒 CRITICAL: Must Preserve - -### Core Stability Features -1. **Window Minimization Protection** - - `safe_close()` function in `watch_point.py` - - Prevents accidental window closure from crashing ComfyUI - - Uses `iconify()` as primary method, `withdraw()` as fallback - - Updates window state tracking (`minimized`, `running` flags) - -2. **Thread Safety** - - All Tkinter operations must run on main thread - - Use `self.window_manager.master.after()` for thread-safe GUI updates - - Prevents `Tcl_AsyncDelete` crashes - -3. **Debug Mode Persistence** - - `watchpoint_debug_config.json` file-based configuration - - `load_debug_config()` and `save_debug_config()` functions - - Debug state survives ComfyUI restarts - -4. **Recursion Prevention** - - `_saving_dump` flag in `save_debug_dump()` - - Prevents infinite loops during debug dump saving - -### File Structure -``` -ComfyUI-WatchPoint/ -├── watch_point.py # Core functionality (preserve stability features) -├── __init__.py # Optional utility imports (modify carefully) -├── watchpoint_debug_config.json # Debug persistence (auto-generated) -├── watchpoint_settings.json # User settings (auto-generated) -├── nodes/ -│ ├── __init__.py # Node package init -│ ├── list_cycler.py # Template for utility nodes -│ └── watchpoint_utils.py # Optional utility nodes (safe to modify) -└── WATCHPOINT_CONSERVATION.md # This file -``` - -## 📝 Implementation Rules - -### When Adding New Features -1. **Utility Nodes**: Add to `nodes/watchpoint_utils.py`, not `watch_point.py` -2. **Core Modifications**: Only modify `watch_point.py` for critical fixes -3. **Optional Imports**: Use try/except blocks in `__init__.py` -4. **State Management**: Use file-based persistence for critical state - -### Window Management -```python -# Correct pattern for window operations -def safe_window_operation(self, display_idx, operation): - try: - self.window_manager.master.after(0, lambda: self._perform_operation(display_idx, operation)) - except Exception as e: - wp_logger.warning(f"Window operation failed: {e}", "WindowManager") -``` - -### Debug Mode Handling -```python -# Always check debug state before operations -if self.debug_mode or load_debug_config().get("debug_enabled", False): - # Perform debug operations - pass -``` - -## 🔄 Utility Node Management - -### Enabling/Disabling Utilities -Located in `__init__.py`: -```python -try: - from .nodes.watchpoint_utils import NODE_CLASS_MAPPINGS as UTILS_CLASS, NODE_DISPLAY_NAME_MAPPINGS as UTILS_DISPLAY - NODE_CLASS_MAPPINGS.update(UTILS_CLASS) - NODE_DISPLAY_NAME_MAPPINGS.update(UTILS_DISPLAY) - print("WatchPoint Utils loaded: Debug Toggle and Restore Window available") -except ImportError as e: - print(f"WatchPoint Utils not available: {e}") - pass -``` - -### Safe Modifications -- ✅ Add new utility nodes to `watchpoint_utils.py` -- ✅ Modify utility node functionality -- ✅ Add new optional features -- ❌ Remove core stability features from `watch_point.py` -- ❌ Modify thread safety mechanisms -- ❌ Remove file-based state management - -## 🚨 Common Pitfalls to Avoid - -1. **Don't Create Direct Tkinter Windows** - ```python - # ❌ WRONG - Causes thread issues - window = tk.Toplevel() - - # ✅ CORRECT - Use existing window manager - self.window_manager.create_window(...) - ``` - -2. **Don't Skip Error Handling** - ```python - # ❌ WRONG - No error handling - root.protocol("WM_DELETE_WINDOW", root.iconify) - - # ✅ CORRECT - With error handling - def safe_close(): - try: - root.iconify() - except: - root.withdraw() - root.protocol("WM_DELETE_WINDOW", safe_close) - ``` - -3. **Don't Forget State Persistence** - ```python - # ❌ WRONG - State lost on restart - self.debug_mode = True - - # ✅ CORRECT - Persistent state - save_debug_config({"debug_enabled": True}) - ``` - -## 🔧 Testing Checklist - -Before committing changes: -- [ ] Test window minimization (click X button) -- [ ] Test debug mode persistence (restart ComfyUI) -- [ ] Test with utility nodes enabled/disabled -- [ ] Test multi-monitor setup -- [ ] Check for `Tcl_AsyncDelete` errors -- [ ] Verify no infinite recursion in debug dumps - -## 📚 Key Functions Reference - -### Window Management -- `safe_close()` - Window close protection -- `window_manager.create_window()` - Safe window creation -- `window_manager.restore_window()` - Window restoration - -### Debug System -- `load_debug_config()` - Load persistent debug state -- `save_debug_config()` - Save debug state -- `save_debug_dump()` - Safe debug dump (with recursion protection) - -### State Management -- File: `watchpoint_debug_config.json` -- Keys: `debug_enabled`, `debug_data` -- Auto-created on first use - -## 🎯 Philosophy - -**Conservative Approach**: This project prioritizes stability over features. Every new feature must: -1. Not compromise core stability -2. Be optional when possible -3. Include proper error handling -4. Maintain backward compatibility - -**Modular Design**: Separate core functionality from utilities to allow safe experimentation without risking core stability. - ---- - -*Last Updated: 2026-01-23* -*Version: 1.0* -*Purpose: Preserve WatchPoint stability while enabling safe evolution* \ No newline at end of file diff --git a/__init__.py b/__init__.py index 612fda2..7d0c42d 100644 --- a/__init__.py +++ b/__init__.py @@ -1,9 +1,8 @@ from .watch_point import NODE_CLASS_MAPPINGS as WP_CLASS, NODE_DISPLAY_NAME_MAPPINGS as WP_DISPLAY, cleanup_all_watchpoints -from .nodes.list_cycler import NODE_CLASS_MAPPINGS as LC_CLASS, NODE_DISPLAY_NAME_MAPPINGS as LC_DISPLAY # Combinamos lo básico -NODE_CLASS_MAPPINGS = {**WP_CLASS, **LC_CLASS} -NODE_DISPLAY_NAME_MAPPINGS = {**WP_DISPLAY, **LC_DISPLAY} +NODE_CLASS_MAPPINGS = {**WP_CLASS} +NODE_DISPLAY_NAME_MAPPINGS = {**WP_DISPLAY} # Importar nodos utils de forma opcional (pueden ser desactivados si no se necesitan) try: diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc index 06d5458..4a727ed 100644 Binary files a/__pycache__/__init__.cpython-312.pyc and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/__pycache__/watch_point.cpython-312.pyc b/__pycache__/watch_point.cpython-312.pyc index f96d5ca..68eed24 100644 Binary files a/__pycache__/watch_point.cpython-312.pyc and b/__pycache__/watch_point.cpython-312.pyc differ diff --git a/example/example_workflow.json b/example/example_workflow.json new file mode 100644 index 0000000..c0eb683 --- /dev/null +++ b/example/example_workflow.json @@ -0,0 +1,199 @@ +{ + "id": "d744fb0e-f3c7-4705-9960-73b30103a194", + "revision": 0, + "last_node_id": 6, + "last_link_id": 7, + "nodes": [ + { + "id": 1, + "type": "LoadImage", + "pos": [ + 17300, + -9670 + ], + "size": [ + 222.83333587646484, + 314 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 4 + ] + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.10.0", + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "example.png", + "image" + ] + }, + { + "id": 4, + "type": "PrimitiveString", + "pos": [ + 17100, + -9270 + ], + "size": [ + 500, + 58 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 5 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.10.0", + "Node name for S&R": "PrimitiveString" + }, + "widgets_values": [ + "\"opt_signal_text\" can be used to send any string, to the tkinter window" + ] + }, + { + "id": 5, + "type": "WatchPoint", + "pos": [ + 17600, + -9670 + ], + "size": [ + 210, + 338 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 4 + }, + { + "name": "opt_signal_text", + "shape": 7, + "type": "STRING", + "link": 5 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 7 + ] + } + ], + "properties": { + "aux_id": "gmorks/ComfyUI-WatchPoint", + "ver": "9c383314405e0767edb67ac208b87b4bac7a2448", + "Node name for S&R": "WatchPoint" + }, + "widgets_values": [ + true, + true, + "Monitor 0 (1920x1080)" + ] + }, + { + "id": 6, + "type": "PreviewImage", + "pos": [ + 17900, + -9670 + ], + "size": [ + 140, + 246 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 7 + } + ], + "outputs": [], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.10.0", + "Node name for S&R": "PreviewImage" + }, + "widgets_values": [] + } + ], + "links": [ + [ + 4, + 1, + 0, + 5, + 0, + "IMAGE" + ], + [ + 5, + 4, + 0, + 5, + 1, + "STRING" + ], + [ + 7, + 5, + 0, + 6, + 0, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.725, + "offset": [ + -16829.321120689645, + 9914.394612068972 + ] + }, + "frontendVersion": "1.37.11", + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file diff --git a/js/watchpoint-floating.js b/js/watchpoint-floating.js index 9c5c3e6..e728d4f 100644 --- a/js/watchpoint-floating.js +++ b/js/watchpoint-floating.js @@ -1,6 +1,7 @@ // watchpoint_floating.js // Watch Point Floating Preview Integration // Only captures images from Watch Point nodes +// Configuration via localStorage import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; @@ -64,21 +65,58 @@ const DEFAULT_CONFIG = { } }; -// Configuration loader -async function loadConfig() { +// Configuration loader with localStorage support +function loadConfig() { try { - const response = await fetch('/extensions/ComfyUI-WatchPoint/watchpoint_config.json'); - if (response.ok) { - const userConfig = await response.json(); - console.log("✅ Watch Point Floating: Loaded user config"); - return { ...DEFAULT_CONFIG, ...userConfig }; + const saved = localStorage.getItem('watchpoint-floating-config'); + if (saved) { + const userConfig = JSON.parse(saved); + console.log("✅ Watch Point Floating: Loaded config from localStorage"); + // Deep merge user config with defaults + return deepMerge(DEFAULT_CONFIG, userConfig); } } catch (e) { - console.log("ℹ️ Watch Point Floating: No custom config found, using defaults"); + console.warn("⚠️ Watch Point Floating: Error loading localStorage config:", e); } + + console.log("ℹ️ Watch Point Floating: Using default config"); return DEFAULT_CONFIG; } +// Save configuration to localStorage +function saveConfig(config) { + try { + localStorage.setItem('watchpoint-floating-config', JSON.stringify(config)); + console.log("✅ Watch Point Floating: Config saved to localStorage"); + return true; + } catch (e) { + console.error("❌ Watch Point Floating: Could not save config:", e); + return false; + } +} + +// Deep merge helper +function deepMerge(target, source) { + const output = Object.assign({}, target); + if (isObject(target) && isObject(source)) { + Object.keys(source).forEach(key => { + if (isObject(source[key])) { + if (!(key in target)) + Object.assign(output, { [key]: source[key] }); + else + output[key] = deepMerge(target[key], source[key]); + } else { + Object.assign(output, { [key]: source[key] }); + } + }); + } + return output; +} + +function isObject(item) { + return item && typeof item === 'object' && !Array.isArray(item); +} + // Main extension app.registerExtension({ name: "WatchPoint.FloatingPreview", @@ -87,7 +125,7 @@ app.registerExtension({ console.log("👁️ Watch Point Floating: Initializing..."); // Load configuration - const config = await loadConfig(); + const config = loadConfig(); // Create the floating preview window const pip = new WatchPointFloatingWindow(config); @@ -213,6 +251,65 @@ app.registerExtension({ console.log(" - Toggle:", config.shortcuts.toggle); console.log(" - Opacity: Ctrl+Alt+[+/-]"); console.log(" - Position: Ctrl+Alt+[Arrows] or Ctrl+Alt+[Numpad1-9]"); + + // Expose global API for configuration + window.WatchPointFloating = { + getConfig: () => loadConfig(), + setConfig: (newConfig) => { + const merged = deepMerge(DEFAULT_CONFIG, newConfig); + if (saveConfig(merged)) { + console.log("✅ Configuration saved. Reload page to apply changes."); + return merged; + } + return null; + }, + resetConfig: () => { + try { + localStorage.removeItem('watchpoint-floating-config'); + console.log("✅ Watch Point Floating: Config reset to defaults. Reload page to apply."); + return true; + } catch (e) { + console.error("❌ Could not reset config:", e); + return false; + } + }, + showHelp: () => { + console.log(` +🎨 Watch Point Floating Configuration Help +========================================== + +View current config: + WatchPointFloating.getConfig() + +Change settings: + WatchPointFloating.setConfig({ + window: { + defaultWidth: 500, + defaultHeight: 500, + defaultPosition: "top-right", + defaultOpacity: 80 + }, + shortcuts: { + toggle: "Ctrl+Alt+KeyP" + } + }) + +Reset to defaults: + WatchPointFloating.resetConfig() + +Available positions: + top-left, top-center, top-right + middle-left, middle-center, middle-right + bottom-left, bottom-center, bottom-right + +Note: Reload page after changing config to apply changes. + `); + }, + instance: pip + }; + + console.log("🔧 Watch Point Floating: Global API exposed as window.WatchPointFloating"); + console.log(" Run WatchPointFloating.showHelp() for configuration help"); } }); diff --git a/js/wp_list_buttons.js b/js/wp_list_buttons.js deleted file mode 100644 index 8269f25..0000000 --- a/js/wp_list_buttons.js +++ /dev/null @@ -1,36 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; - -app.registerExtension({ - name: "WatchPoint.ListCyclerButtons", - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === "WPSmartListCycler") { - - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function() { - onNodeCreated?.apply(this, arguments); - - // Crear el botón de Reset - const btn = this.addWidget("button", "RESET STATE", "reset", () => { - const node_id = this.id; - - // Llamar a nuestra API personalizada en el servidor - api.fetchApi("/wp/list_cycler/reset", { - method: "POST", - body: JSON.stringify({ node_id }), - }).then(response => { - if (response.ok) { - if (DEBUG) { - //alert(`Nodo ${node_id}: Estado del JSON limpiado. En la próxima ejecución se regenerará la lista.`); - this.color = "#225522"; // Feedback visual momentáneo - setTimeout(() => { this.color = ""; }, 1000); - } - } - }); - }); - - btn.serialize = false; // No necesitamos guardar el estado del botón - }; - } - } -}); \ No newline at end of file diff --git a/media/preview.png b/media/preview.png new file mode 100644 index 0000000..ecfd5b8 Binary files /dev/null and b/media/preview.png differ diff --git a/nodes/list_cycler.py b/nodes/list_cycler.py deleted file mode 100644 index 18cc64e..0000000 --- a/nodes/list_cycler.py +++ /dev/null @@ -1,115 +0,0 @@ -import json -import os -import random -from server import PromptServer -from aiohttp import web - -DEBUG = True - -class WPSmartListCycler: - def __init__(self): - self.data_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "list_state.json") - os.makedirs(os.path.dirname(self.data_path), exist_ok=True) - - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "list_string": ("STRING", {"default": "0,1,2,3,4,5"}), - "mode": (["increment-wrap", "random-shuffled", "fixed"],), - "fixed_index": ("INT", {"default": 0, "min": 0, "max": 1000}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - }, - "hidden": {"unique_id": "UNIQUE_ID"}, - } - - RETURN_TYPES = ("INT", "STRING") - RETURN_NAMES = ("index", "index_as_string") - FUNCTION = "get_next_value" - CATEGORY = "WatchPoint/Logic" - - @classmethod - def IS_CHANGED(s, **kwargs): - """ - Forzamos a ComfyUI a que ignore la caché de este nodo devolviendo - un valor distinto en cada consulta. - """ - return random.random() - - def load_state(self): - try: - if os.path.exists(self.data_path): - with open(self.data_path, 'r', encoding='utf-8') as f: - return json.load(f) - except Exception: pass - return {} - - def save_state(self, state): - try: - with open(self.data_path, 'w', encoding='utf-8') as f: - json.dump(state, f, indent=4) - except Exception as e: - if DEBUG: - print(f"WP_Error: Fallo al guardar JSON: {e}") - - def get_next_value(self, list_string, mode, fixed_index, seed, unique_id): - # 1. Preparar items - items = [int(x.strip()) for x in list_string.split(",") if x.strip()] or [0] - state = self.load_state() - - if unique_id not in state: - state[unique_id] = {"last_idx": -1, "shuffled_list": []} - - node_state = state[unique_id] - out_value = items[0] - - # 2. Lógica de selección - if mode == "fixed": - idx = fixed_index % len(items) - out_value = items[idx] - node_state["last_idx"] = idx - - elif mode == "increment-wrap": - new_idx = (node_state.get("last_idx", -1) + 1) % len(items) - out_value = items[new_idx] - node_state["last_idx"] = new_idx - - elif mode == "random-shuffled": - shuffled = node_state.get("shuffled_list", []) - - # Regenerar bolsa si está vacía o el tamaño de la lista cambió - if DEBUG: - print(f"WP_Log: Nodo {unique_id} estado actual: {node_state}") - if not shuffled or max(shuffled, default=-1) >= len(items): - shuffled = list(range(len(items))) - random.shuffle(shuffled) - if DEBUG: - print(f"WP_Log: Nodo {unique_id} barajando nueva lista: {shuffled}" ) - - idx_to_use = shuffled.pop(0) - out_value = items[idx_to_use] - node_state["shuffled_list"] = shuffled - node_state["last_idx"] = idx_to_use - - # 3. Guardado inmediato - state[unique_id] = node_state - self.save_state(state) - if DEBUG: - print(f"WP_Log: Nodo {unique_id} ejecutado. Resultado: {out_value}") - return (int(out_value), str(out_value)) - -# --- API ENDPOINT (Mismo de antes) --- -@PromptServer.instance.routes.post("/wp/list_cycler/reset") -async def reset_node_state(request): - json_data = await request.json() - node_id = str(json_data.get("node_id")) - temp_instance = WPSmartListCycler() - state = temp_instance.load_state() - if node_id in state: - del state[node_id] - temp_instance.save_state(state) - return web.json_response({"status": "success"}) - return web.json_response({"status": "not_found"}) - -NODE_CLASS_MAPPINGS = {"WPSmartListCycler": WPSmartListCycler} -NODE_DISPLAY_NAME_MAPPINGS = {"WPSmartListCycler": "🔄 Smart List Cycler (WP)"} diff --git a/watch_point.py b/watch_point.py index 021cae2..e7055ed 100644 --- a/watch_point.py +++ b/watch_point.py @@ -1,6 +1,6 @@ import os import json -import sys # NUEVO: Importar sys para debug dumps +import sys import tkinter as tk from tkinter import filedialog, messagebox from PIL import Image, ImageTk @@ -55,11 +55,11 @@ class WatchPointLogger: self.log_level = "INFO" # DEBUG, INFO, WARNING, ERROR self.logs = [] self.max_logs = 100 - self.debug_mode = False # NUEVO: Modo debug - self.debug_log_dir = "debug_logs" # NUEVO: Directorio para logs de debug - self.debug_session_id = None # NUEVO: ID única para sesión de debug + self.debug_mode = False + self.debug_log_dir = "debug_logs" + self.debug_session_id = None self.config_file = os.path.join(os.path.dirname(__file__), "watchpoint_debug_config.json") - self.load_debug_config() # Cargar configuración persistente + self.load_debug_config() def log(self, level, message, component="WatchPoint"): """Log un mensaje con nivel y componente""" @@ -124,13 +124,13 @@ class WatchPointLogger: self.debug_mode = enabled if enabled: self.debug_session_id = time.strftime("%Y%m%d_%H%M%S") - # Crear directorio de debug si no existe + os.makedirs(self.debug_log_dir, exist_ok=True) self.info(f"Modo debug activado - Sesión: {self.debug_session_id}", "Debug") else: self.info("Modo debug desactivado", "Debug") - # Guardar configuración persistente + self.save_debug_config() def load_debug_config(self): @@ -183,16 +183,16 @@ class WatchPointLogger: } } - # Agregar stats del WatchPoint si está disponible + if watch_point_instance and hasattr(watch_point_instance, 'get_health_stats'): debug_data["health_stats"] = watch_point_instance.get_health_stats() - # Agregar info de threads SI ESTÁ DISPONIBLE Y NO ESTAMOS EN RECURSIÓN + if watch_point_instance and hasattr(watch_point_instance, 'debug_threads'): # Llamar directamente a la función sin pasar por el logger para evitar recursión debug_data["thread_info"] = watch_point_instance.debug_threads() - # Agregar logs recientes + debug_data["recent_logs"] = self.get_logs()[-50:] # Últimos 50 logs # Guardar a archivo @@ -207,7 +207,7 @@ class WatchPointLogger: self.error(f"Error guardando debug dump: {e}", "Debug") return None finally: - # Limpiar bandera de recursión + # Clear recursion flag self._saving_dump = False # Crear logger global @@ -283,7 +283,7 @@ class WindowManager: os.path.join(os.path.dirname(__file__), "watchpoint_settings.json") ) self.shutdown_event = threading.Event() - self._start_time = time.time() # Tiempo de inicio para estadísticas + self._start_time = time.time() # Start time for statistics self.initialized = True # Iniciar watchdog @@ -293,23 +293,53 @@ class WindowManager: wp_logger.info("WindowManager inicializado", "WindowManager") def show_image(self, display_idx, pil_img, text=None): - """Creates or updates a window to display an image and optional text.""" - if display_idx in self.windows: - win_data = self.windows[display_idx] + """Creates or updates THE SINGLE global window to display an image and optional text.""" + + # Check if any window exists, reuse it + if len(self.windows) > 0: + # Obtener el índice de la ventana existente (solo debería haber 1) + existing_idx = list(self.windows.keys())[0] + win_data = self.windows[existing_idx] + + # Verificar que la ventana esté corriendo if not win_data.get("running", False): - self._cleanup_window(display_idx) + # Window exists but is dead, clean up + wp_logger.warning(f"Ventana {existing_idx} existe pero no está running, limpiando", "ShowImage") + self._cleanup_window(existing_idx) + # Continuar a creación de nueva ventana (abajo) else: + # Window is alive, REUSE + wp_logger.debug(f"Reutilizando ventana existente {existing_idx} para nueva imagen", "ShowImage") + + # Actualizar imagen with win_data["lock"]: win_data["image"] = pil_img + + # Actualizar texto si existe if text and win_data.get("instance"): win_data["instance"].update_signal_text(text) + + # Update title if monitor changed + if existing_idx != display_idx: + wp_logger.info(f"Monitor cambiado de {existing_idx} a {display_idx} (solo actualiza título)", "ShowImage") + instance = win_data.get("instance") + if instance and hasattr(instance, 'root'): + try: + # Update simplified title + instance.root.title("Watch Point") + except Exception as e: + wp_logger.warning(f"No se pudo actualizar título: {e}", "ShowImage") + return - + + # If we reach here, NO window exists, create a new one + wp_logger.info(f"Creando nueva ventana global en Monitor {display_idx}", "ShowImage") + lock = Lock() win_data = { "image": pil_img, "lock": lock, "running": True, "instance": None, "pending_text": text, - "minimized": False # NUEVO: Estado de minimización + "minimized": False } self.windows[display_idx] = win_data @@ -324,7 +354,7 @@ class WindowManager: self.windows[display_idx]["closing"] = True self.windows[display_idx]["close_started"] = time.time() - # MEJORADO: Esperar hasta 3 segundos con timeout + try: thread = self.windows[display_idx].get("thread") if thread and thread.is_alive(): @@ -349,9 +379,9 @@ class WindowManager: root = None win_instance = None try: - # === CÓDIGO EXISTENTE SE MANTIENE IGUAL === + # Create main window root = tk.Tk() - root.title(f"Watch Point - Monitor {display_idx}") + root.title("Watch Point") self._apply_icon(root) self._apply_geometry(root, display_idx) @@ -685,11 +715,6 @@ class WatchPoint: except (ValueError, IndexError): display_idx = 0 - # Close the previous window if the monitor has changed - if self.last_display_idx != -1 and self.last_display_idx != display_idx: - wp_logger.info(f"Cambiando de monitor {self.last_display_idx} a {display_idx}", "WatchPoint") - self.window_manager.hide_window(self.last_display_idx) - self.last_display_idx = display_idx if monitor_preview: