feat(settings): add global monitor selection via settings dialog
- Move monitor selection from node dropdown to global settings file - Add monitor selection UI to settings dialog with live window repositioning - Keep Tkinter window persistent to reduce threading errors - Update documentation to reflect new configuration approach
This commit is contained in:
+12
-3
@@ -13,11 +13,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Removed watchpoint_utils.py and watchpoint_debug_config.json.
|
||||
- Removed debug-specific logging and dump features.
|
||||
|
||||
### Added
|
||||
- Global monitor configuration via `watchpoint_settings.json` using `monitor_index`.
|
||||
- Monitor selection UI inside the Tkinter settings dialog (⚙), with live window repositioning.
|
||||
|
||||
### Changed
|
||||
- Simplified WatchPoint node structure.
|
||||
- Integrated text input (formerly Signal Scout) directly into WatchPoint node.
|
||||
- External monitor selection moved from the node dropdown to the global settings file.
|
||||
- `monitor_preview` now controls whether the image updates, instead of creating/destroying the Tkinter window.
|
||||
- Optimized WatchPointLogger for standard logging only.
|
||||
- Updated documentation to reflect cleanup.
|
||||
- Updated documentation to reflect cleanup and new monitor configuration.
|
||||
|
||||
### Fixed
|
||||
- Reduced `main thread is not in main loop` and `Tcl_AsyncDelete` errors by keeping a single persistent Tkinter window instance.
|
||||
- Avoided dead preview threads by no longer destroying the window when `monitor_preview` is disabled.
|
||||
|
||||
## [1.0.0] - 2026-01-24
|
||||
|
||||
@@ -27,5 +37,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Signal Scout node for text debugging.
|
||||
- Window minimization protection to prevent threading crashes.
|
||||
- MIT License and standardized documentation.
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
@@ -91,8 +91,8 @@ The main preview node.
|
||||
2. Connect an `IMAGE` input.
|
||||
3. Enable/disable previews:
|
||||
* `floating_preview`: Enable ComfyUI's built-in preview.
|
||||
* `monitor_preview`: Enable the external monitor window.
|
||||
4. Select the target `monitor` from the dropdown.
|
||||
* `monitor_preview`: Enable or pause the external monitor window (window stays alive, image updates only when True).
|
||||
4. Configure the target monitor via the **⚙ Settings** dialog or by editing `watchpoint_settings.json` (`monitor_index`).
|
||||
5. The node acts as a pass-through for the `IMAGE` output.
|
||||
|
||||
### Optional string input
|
||||
@@ -142,14 +142,19 @@ Access settings via the **⚙ Settings** button in the toolbar.
|
||||
|
||||
### Save Options
|
||||
|
||||
* Default format: PNG, JPEG, or WebP
|
||||
* Default format: PNG or JPEG
|
||||
* JPEG quality slider (10-100)
|
||||
|
||||
### Monitor Options
|
||||
|
||||
* Select the target monitor from the **Monitor** dropdown in the Settings dialog.
|
||||
* Persist the monitor selection globally via `monitor_index` in `watchpoint_settings.json`.
|
||||
|
||||
### UI Options
|
||||
|
||||
* Show/hide toolbar by default
|
||||
|
||||
All settings are saved to `watchpoint_settings.json`
|
||||
All settings are saved to `watchpoint_settings.json`.
|
||||
|
||||
---
|
||||
|
||||
@@ -168,7 +173,8 @@ Edit `watchpoint_settings.json` to customize defaults for the external Tkinter w
|
||||
"window_size_mode": "fixed",
|
||||
"show_toolbar": true,
|
||||
"save_format": "png",
|
||||
"jpeg_quality": 90
|
||||
"jpeg_quality": 90,
|
||||
"monitor_index": 0
|
||||
}
|
||||
|
||||
```
|
||||
@@ -178,8 +184,9 @@ Edit `watchpoint_settings.json` to customize defaults for the external Tkinter w
|
||||
* `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")
|
||||
* `save_format`: Default save format ("png" or "jpeg")
|
||||
* `jpeg_quality`: JPEG compression quality (10-100)
|
||||
* `monitor_index`: Index of the monitor used for the external preview (0 = first monitor).
|
||||
|
||||
### Floating Preview Configuration
|
||||
|
||||
@@ -385,9 +392,10 @@ pip install pywin32
|
||||
|
||||
### Window doesn't appear
|
||||
|
||||
* Check if `monitor_preview` is enabled (True)
|
||||
* Try different monitor selection
|
||||
* Check console for error messages
|
||||
* Check if `monitor_preview` is enabled (True).
|
||||
* Open the **⚙ Settings** dialog and verify that a valid monitor is selected.
|
||||
* Alternatively, check that `monitor_index` in `watchpoint_settings.json` points to an existing monitor.
|
||||
* Check console for error messages.
|
||||
|
||||
### Window appears in wrong location
|
||||
|
||||
@@ -396,9 +404,10 @@ pip install pywin32
|
||||
|
||||
### Image not updating
|
||||
|
||||
* Ensure workflow is executing
|
||||
* Check that image is connected to Watch Point input
|
||||
* Try disabling/re-enabling monitor preview
|
||||
* Ensure workflow is executing.
|
||||
* Check that image is connected to Watch Point input.
|
||||
* Verify that `monitor_preview` is enabled (True); when False, the window stays open but the image will not refresh.
|
||||
* Try disabling/re-enabling monitor preview.
|
||||
|
||||
### Floating preview not working
|
||||
|
||||
|
||||
+71
-43
@@ -142,6 +142,7 @@ class SettingsManager:
|
||||
"window_x": None, "window_y": None,
|
||||
"window_size_mode": "fixed",
|
||||
"show_toolbar": True, "save_format": "png", "jpeg_quality": 90,
|
||||
"monitor_index": 0,
|
||||
}
|
||||
self.settings = self.load()
|
||||
|
||||
@@ -197,9 +198,11 @@ class WindowManager:
|
||||
|
||||
wp_logger.info("WindowManager initialized", "WindowManager")
|
||||
|
||||
def show_image(self, display_idx, pil_img, text=None):
|
||||
def show_image(self, pil_img, text=None):
|
||||
"""Creates or updates THE SINGLE global window to display an image and optional text."""
|
||||
|
||||
target_monitor_idx = self.settings_manager.get("monitor_index", 0)
|
||||
|
||||
# Check if any window exists, reuse it
|
||||
if len(self.windows) > 0:
|
||||
# Get the index of the existing window (there should only be 1)
|
||||
@@ -224,22 +227,28 @@ class WindowManager:
|
||||
if text and win_data.get("instance"):
|
||||
win_data["instance"].update_signal_text(text)
|
||||
|
||||
# Update title if monitor changed
|
||||
if existing_idx != display_idx:
|
||||
# Monitor changed, update title
|
||||
wp_logger.info(f"Monitor changed from {existing_idx} to {display_idx} (only updates title)", "ShowImage")
|
||||
instance = win_data.get("instance")
|
||||
if instance and hasattr(instance, 'root'):
|
||||
# Update monitor if changed
|
||||
if existing_idx != target_monitor_idx:
|
||||
# Monitor changed, move window
|
||||
wp_logger.info(f"Monitor changed from {existing_idx} to {target_monitor_idx} - Moving Window", "ShowImage")
|
||||
|
||||
# Move data to new key
|
||||
self.windows[target_monitor_idx] = self.windows.pop(existing_idx)
|
||||
win_data = self.windows[target_monitor_idx]
|
||||
|
||||
if win_data.get("instance"):
|
||||
win_data["instance"].display_idx = target_monitor_idx
|
||||
# Move the window
|
||||
try:
|
||||
# Update simplified title
|
||||
instance.root.title("Watch Point")
|
||||
self._apply_geometry(win_data["instance"].root, target_monitor_idx)
|
||||
except Exception as e:
|
||||
wp_logger.warning(f"Could not update title: {e}", "ShowImage")
|
||||
wp_logger.error(f"Error moving window: {e}", "ShowImage")
|
||||
|
||||
return
|
||||
|
||||
# If we reach here, NO window exists, create a new one
|
||||
wp_logger.info(f"Creating new global window on Monitor {display_idx}", "ShowImage")
|
||||
wp_logger.info(f"Creating new global window on Monitor {target_monitor_idx}", "ShowImage")
|
||||
display_idx = target_monitor_idx
|
||||
|
||||
lock = Lock()
|
||||
win_data = {
|
||||
@@ -586,24 +595,17 @@ class WatchPoint:
|
||||
"""The main ComfyUI node class."""
|
||||
def __init__(self):
|
||||
self.window_manager = window_manager
|
||||
self.last_display_idx = -1
|
||||
|
||||
# Register for cleanup
|
||||
shutdown_registry.register(self)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
monitors = ["Monitor 0 (Default)"]
|
||||
if SCREENINFO_AVAILABLE:
|
||||
try:
|
||||
monitors = [f"Monitor {i} ({m.width}x{m.height})" for i, m in enumerate(get_monitors())]
|
||||
except Exception: pass
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"floating_preview": ("BOOLEAN", {"default": True}),
|
||||
"monitor_preview": ("BOOLEAN", {"default": True}),
|
||||
"monitor": (monitors, {"default": monitors[0]}),
|
||||
},
|
||||
"optional": {"opt_signal_text": ("STRING", {"forceInput": True, "multiline": True})}
|
||||
}
|
||||
@@ -613,22 +615,15 @@ class WatchPoint:
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "WatchPoint"
|
||||
|
||||
def watch(self, images, floating_preview=True, monitor_preview=True, monitor="Monitor 0", opt_signal_text=None):
|
||||
try:
|
||||
display_idx = int(monitor.split(" ")[1])
|
||||
except (ValueError, IndexError):
|
||||
display_idx = 0
|
||||
|
||||
self.last_display_idx = display_idx
|
||||
|
||||
def watch(self, images, floating_preview=True, monitor_preview=True, opt_signal_text=None):
|
||||
if monitor_preview:
|
||||
wp_logger.debug(f"Showing preview on monitor {display_idx}", "WatchPoint")
|
||||
image = 255.0 * images[0].cpu().numpy()
|
||||
pil_img = Image.fromarray(np.clip(image, 0, 255).astype(np.uint8))
|
||||
self.window_manager.show_image(display_idx, pil_img, opt_signal_text)
|
||||
else:
|
||||
wp_logger.debug(f"Hiding preview on monitor {display_idx}", "WatchPoint")
|
||||
self.window_manager.hide_window(display_idx)
|
||||
self.window_manager.show_image(pil_img, opt_signal_text)
|
||||
|
||||
# If monitor_preview is False, we do NOTHING.
|
||||
# The window remains open (static) if it was already open.
|
||||
# We do NOT call hide_window().
|
||||
|
||||
ui_images = self._prepare_preview(images) if floating_preview else []
|
||||
|
||||
@@ -908,7 +903,7 @@ class WatchPointWindow:
|
||||
except Exception as e:
|
||||
messagebox.showerror("Save Error", f"Could not save image:\n{e}")
|
||||
|
||||
def _open_settings(self): WatchPointSettingsDialog(self.root, self.settings)
|
||||
def _open_settings(self): WatchPointSettingsDialog(self.root, self.settings, self)
|
||||
def _on_mouse_wheel(self, e): self._zoom_in() if e.delta > 0 else self._zoom_out()
|
||||
def _on_mouse_down(self, e): self.is_dragging, self.drag_start_x, self.drag_start_y = True, e.x, e.y
|
||||
def _on_mouse_up(self, e): self.is_dragging = False
|
||||
@@ -926,8 +921,9 @@ class WatchPointWindow:
|
||||
|
||||
class WatchPointSettingsDialog:
|
||||
"""A dialog for changing application settings."""
|
||||
def __init__(self, parent, settings_manager):
|
||||
def __init__(self, parent, settings_manager, window_instance=None):
|
||||
self.settings = settings_manager
|
||||
self.window_instance = window_instance
|
||||
self.dialog = tk.Toplevel(parent)
|
||||
self.dialog.title("Settings")
|
||||
self.dialog.transient(parent)
|
||||
@@ -940,6 +936,26 @@ class WatchPointSettingsDialog:
|
||||
|
||||
frame = tk.Frame(self.dialog, padx=15, pady=15)
|
||||
frame.pack(fill="both", expand=True)
|
||||
|
||||
# Monitor Selection
|
||||
if SCREENINFO_AVAILABLE:
|
||||
try:
|
||||
monitors = [f"Monitor {i} ({m.width}x{m.height})" for i, m in enumerate(get_monitors())]
|
||||
if monitors:
|
||||
m_frame = tk.LabelFrame(frame, text="Monitor", padx=10, pady=10)
|
||||
m_frame.pack(anchor="w", fill="x", pady=(0, 10))
|
||||
|
||||
current_idx = self.settings.get("monitor_index", 0)
|
||||
self.monitor_var = tk.StringVar()
|
||||
try:
|
||||
self.monitor_var.set(monitors[current_idx])
|
||||
except IndexError:
|
||||
self.monitor_var.set(monitors[0])
|
||||
|
||||
tk.OptionMenu(m_frame, self.monitor_var, *monitors, command=self._on_monitor_change).pack(anchor="w")
|
||||
except Exception as e:
|
||||
print(f"Watch Point: Error listing monitors: {e}")
|
||||
|
||||
tk.Checkbutton(frame, text="Show Toolbar on Startup", variable=self.show_toolbar_var).pack(anchor="w")
|
||||
|
||||
fmt_frame = tk.LabelFrame(frame, text="Default Save Format", padx=10, pady=10)
|
||||
@@ -949,21 +965,33 @@ class WatchPointSettingsDialog:
|
||||
|
||||
self.q_frame = tk.LabelFrame(frame, text="JPEG Quality", padx=10, pady=10)
|
||||
self.q_frame.pack(anchor="w", fill="x", pady=(0, 10))
|
||||
self.q_scale = tk.Scale(self.q_frame, from_=1, to=100, orient="horizontal", variable=self.jpeg_quality_var)
|
||||
self.q_scale.pack(anchor="w", fill="x")
|
||||
|
||||
tk.Scale(self.q_frame, from_=10, to=100, orient=tk.HORIZONTAL, variable=self.jpeg_quality_var).pack(fill="x")
|
||||
self._toggle_quality()
|
||||
|
||||
btn_frame = tk.Frame(frame)
|
||||
btn_frame.pack(fill="x", side="bottom")
|
||||
tk.Button(btn_frame, text="Save", command=self._save_close).pack(side="right", padx=(5,0))
|
||||
tk.Button(btn_frame, text="Cancel", command=self.dialog.destroy).pack(side="right")
|
||||
self.dialog.wait_window()
|
||||
btn_frame = tk.Frame(self.dialog, pady=10)
|
||||
btn_frame.pack(fill="x")
|
||||
tk.Button(btn_frame, text="Save & Close", command=self._save_and_close, bg="#4a4a4a", fg="white").pack(side="right")
|
||||
|
||||
def _on_monitor_change(self, selected_str):
|
||||
try:
|
||||
idx = int(selected_str.split(" ")[1])
|
||||
self.settings.set("monitor_index", idx)
|
||||
self.settings.save()
|
||||
|
||||
# Live update if possible
|
||||
if self.window_instance and self.window_instance.current_pil_image:
|
||||
self.window_instance.manager.show_image(self.window_instance.current_pil_image)
|
||||
except Exception as e:
|
||||
print(f"Watch Point: Error changing monitor: {e}")
|
||||
|
||||
def _toggle_quality(self):
|
||||
state = "normal" if self.save_format_var.get() == "jpeg" else "disabled"
|
||||
for child in self.q_frame.winfo_children(): child.configure(state=state)
|
||||
if self.save_format_var.get() == "jpeg":
|
||||
self.q_frame.pack(anchor="w", fill="x", pady=(0, 10), after=self.dialog.winfo_children()[0].winfo_children()[-2])
|
||||
else:
|
||||
self.q_frame.pack_forget()
|
||||
|
||||
def _save_close(self):
|
||||
def _save_and_close(self):
|
||||
self.settings.set("show_toolbar", self.show_toolbar_var.get())
|
||||
self.settings.set("save_format", self.save_format_var.get())
|
||||
self.settings.set("jpeg_quality", self.jpeg_quality_var.get())
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
"window_size_mode": "Half Vertical",
|
||||
"show_toolbar": true,
|
||||
"save_format": "png",
|
||||
"jpeg_quality": 90
|
||||
"jpeg_quality": 90,
|
||||
"monitor_index": 1
|
||||
}
|
||||
Reference in New Issue
Block a user