Added scout and merged list cycler
This commit is contained in:
+9
-1
@@ -1,4 +1,12 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.idea/
|
||||
*.pyo
|
||||
*.pyd
|
||||
|
||||
# State files (no subir el JSON con datos)
|
||||
data/list_state.json
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
+8
-15
@@ -1,18 +1,11 @@
|
||||
"""
|
||||
ComfyUI Watch Point
|
||||
Dual preview system: External monitor (Tkinter) + Floating preview (JavaScript)
|
||||
"""
|
||||
from .watch_point import NODE_CLASS_MAPPINGS as WP_CLASS, NODE_DISPLAY_NAME_MAPPINGS as WP_DISPLAY
|
||||
from .nodes.list_cycler import NODE_CLASS_MAPPINGS as LC_CLASS, NODE_DISPLAY_NAME_MAPPINGS as LC_DISPLAY
|
||||
|
||||
from .watch_point import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
# Combinamos solo lo necesario
|
||||
NODE_CLASS_MAPPINGS = {**WP_CLASS, **LC_CLASS}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {**WP_DISPLAY, **LC_DISPLAY}
|
||||
|
||||
# Indicate that we have JavaScript files
|
||||
WEB_DIRECTORY = "js"
|
||||
# El JS sigue siendo necesario para los botones del List Cycler
|
||||
WEB_DIRECTORY = "js"
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
|
||||
|
||||
print("✅ Watch Point Extension: Loaded")
|
||||
print(" 👁️ Dual Preview System")
|
||||
print(" 📺 Monitor Preview: Tkinter window on external monitor")
|
||||
print(" 🖼️ Floating Preview: JavaScript floating window")
|
||||
print(" ⌨️ Shortcuts: Ctrl+Alt+W (toggle floating)")
|
||||
print(" 📁 JavaScript extension registered")
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
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)"}
|
||||
@@ -0,0 +1,29 @@
|
||||
# nodes/signal_scout.py
|
||||
from server import PromptServer
|
||||
|
||||
class WPSignalScout:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required" : {
|
||||
"text": ("STRING", {"forceInput": True }),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING" ,)
|
||||
RETURN_NAMES = ("text" ,)
|
||||
FUNCTION = "scout_signal"
|
||||
CATEGORY = "WatchPoint/Debug"
|
||||
|
||||
def scout_signal(self, text):
|
||||
# Intentamos importar WatchPoint desde el archivo raíz
|
||||
try :
|
||||
from ..watch_point import WatchPoint
|
||||
WatchPoint.update_all_text(text)
|
||||
except Exception as e:
|
||||
print(f"WP_Scout Error: No se pudo conectar con la ventana: {e}" )
|
||||
|
||||
return (text,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"WPSignalScout" : WPSignalScout}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"WPSignalScout": "📡 WP Signal Scout" }
|
||||
+151
-462
@@ -7,10 +7,8 @@ import numpy as np
|
||||
from threading import Thread, Lock
|
||||
import io
|
||||
import folder_paths
|
||||
import torch
|
||||
|
||||
# Icon Base64 (opcional, pega tu icono aquí)
|
||||
ICON_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsSAAALEgHS3X78AAAH40lEQVR4nO2dX0xb1x3Hv9exjamHr0GRNzRMAFXrGgztolQFVXRWI5VJCwp9yNI401SpKJOWvoSX9Q"
|
||||
ICON_BASE64 = ""
|
||||
|
||||
try:
|
||||
from screeninfo import get_monitors
|
||||
@@ -22,16 +20,6 @@ except ImportError:
|
||||
class WatchPoint:
|
||||
"""
|
||||
Watch Point - Dual Preview Monitor
|
||||
Shows image in external monitor (Tkinter) and/or floating preview (ComfyUI built-in)
|
||||
|
||||
Controls (Monitor Preview):
|
||||
- Mouse wheel: Zoom in/out
|
||||
- Left click + drag: Pan image
|
||||
- Right click: Context menu
|
||||
- T key: Toggle toolbar
|
||||
- R key: Reset zoom and position
|
||||
- 1 key: Zoom 1:1 (100%)
|
||||
- ESC: Close window
|
||||
"""
|
||||
_windows = {} # monitor_idx -> window data
|
||||
|
||||
@@ -44,6 +32,10 @@ class WatchPoint:
|
||||
"floating_preview": ("BOOLEAN", {"default": True}),
|
||||
"monitor_preview": ("BOOLEAN", {"default": True}),
|
||||
"monitor": (monitor_list, {"default": monitor_list[0]}),
|
||||
},
|
||||
"optional": {
|
||||
# Nueva entrada opcional para el texto
|
||||
"opt_signal_text": ("STRING", {"forceInput": True, "multiline": True}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +43,7 @@ class WatchPoint:
|
||||
RETURN_NAMES = ("images",)
|
||||
FUNCTION = "watch"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "image/preview"
|
||||
CATEGORY = "WatchPoint"
|
||||
|
||||
@classmethod
|
||||
def get_monitors(cls):
|
||||
@@ -63,15 +55,11 @@ class WatchPoint:
|
||||
monitors.append(f"Monitor {i} ({m.width}x{m.height})")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not monitors:
|
||||
monitors = ["Monitor 0 (1920x1080)"]
|
||||
|
||||
return monitors
|
||||
|
||||
def watch(self, images, floating_preview=True, monitor_preview=True, monitor="Monitor 0 (1920x1080)"):
|
||||
|
||||
# Determine monitor index
|
||||
def watch(self, images, floating_preview=True, monitor_preview=True, monitor="Monitor 0 (1920x1080)", opt_signal_text=None):
|
||||
try:
|
||||
display_idx = int(monitor.split(" ")[1])
|
||||
except Exception:
|
||||
@@ -80,40 +68,40 @@ class WatchPoint:
|
||||
# Handle Monitor Preview (Tkinter)
|
||||
if monitor_preview:
|
||||
self._show_monitor_preview(images, display_idx)
|
||||
|
||||
# --- INTEGRACIÓN SIGNAL SCOUT ---
|
||||
# Si recibimos texto, intentamos actualizar la ventana inmediatamente
|
||||
if opt_signal_text is not None and display_idx in self._windows:
|
||||
win_data = self._windows[display_idx]
|
||||
if win_data.get("window"):
|
||||
# Si la ventana ya existe, actualizamos directo
|
||||
win_data["window"].update_signal_text(opt_signal_text)
|
||||
else:
|
||||
# Si la ventana se está creando (thread), guardamos el texto como pendiente
|
||||
win_data["pending_text"] = opt_signal_text
|
||||
else:
|
||||
self._hide_monitor_preview(display_idx)
|
||||
|
||||
# Handle Floating Preview (ComfyUI built-in)
|
||||
# For floating preview to work, we need to save the image temporarily
|
||||
# and return it in the expected format
|
||||
# Handle Floating Preview
|
||||
result = []
|
||||
if floating_preview:
|
||||
# Save preview images so ComfyUI's preview system can pick them up
|
||||
result = self._prepare_preview(images)
|
||||
|
||||
return {"ui": {"images": result}, "result": (images,)}
|
||||
|
||||
def _show_monitor_preview(self, images, display_idx):
|
||||
"""Show image in external monitor using Tkinter"""
|
||||
|
||||
# Convert tensor to PIL Image
|
||||
image = images[0]
|
||||
image = 255.0 * image.cpu().numpy()
|
||||
pil_img = Image.fromarray(np.clip(image, 0, 255).astype(np.uint8))
|
||||
|
||||
# Check if window exists and is still running
|
||||
if display_idx in self._windows:
|
||||
# Check if the window is actually alive
|
||||
if not self._windows[display_idx].get("running", False):
|
||||
# Window was closed, remove it and create new one
|
||||
try:
|
||||
if "thread" in self._windows[display_idx]:
|
||||
self._windows[display_idx]["thread"].join(timeout=0.1)
|
||||
except:
|
||||
pass
|
||||
except: pass
|
||||
del self._windows[display_idx]
|
||||
|
||||
# Create or update window
|
||||
if display_idx not in self._windows:
|
||||
lock = Lock()
|
||||
self._windows[display_idx] = {
|
||||
@@ -121,161 +109,111 @@ class WatchPoint:
|
||||
"lock": lock,
|
||||
"running": True,
|
||||
"root": None,
|
||||
"window": None, # Referencia a la instancia de la clase UI
|
||||
"pending_text": None # Buffer para texto antes de init
|
||||
}
|
||||
t = Thread(target=self._window_loop, args=(display_idx,), daemon=True)
|
||||
self._windows[display_idx]["thread"] = t
|
||||
t.start()
|
||||
else:
|
||||
# Update existing window
|
||||
try:
|
||||
with self._windows[display_idx]["lock"]:
|
||||
self._windows[display_idx]["image"] = pil_img
|
||||
except:
|
||||
# If lock fails, window might be dead, recreate it
|
||||
del self._windows[display_idx]
|
||||
self._show_monitor_preview(images, display_idx)
|
||||
|
||||
def _hide_monitor_preview(self, display_idx):
|
||||
"""Hide monitor preview"""
|
||||
if display_idx in self._windows:
|
||||
self._windows[display_idx]["running"] = False
|
||||
|
||||
def _prepare_preview(self, images):
|
||||
"""Prepare images for ComfyUI's preview system (enables floating preview)"""
|
||||
results = []
|
||||
import time
|
||||
for i, tensor in enumerate(images):
|
||||
# Convert tensor to numpy
|
||||
array = 255.0 * tensor.cpu().numpy()
|
||||
img = Image.fromarray(np.clip(array, 0, 255).astype(np.uint8))
|
||||
|
||||
# Save to temp directory with unique timestamp
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
|
||||
# Generate unique filename with timestamp to avoid caching issues
|
||||
import time
|
||||
timestamp = int(time.time() * 1000) # milliseconds
|
||||
timestamp = int(time.time() * 1000)
|
||||
filename = f"watchpoint_{timestamp:013d}_{i:02d}.png"
|
||||
|
||||
file_path = os.path.join(output_dir, filename)
|
||||
img.save(file_path, compress_level=4)
|
||||
|
||||
results.append({
|
||||
"filename": filename,
|
||||
"subfolder": "",
|
||||
"type": "temp"
|
||||
})
|
||||
|
||||
results.append({"filename": filename, "subfolder": "", "type": "temp"})
|
||||
return results
|
||||
|
||||
def _window_loop(self, display_idx):
|
||||
"""Main window loop with Tkinter"""
|
||||
|
||||
# Load settings
|
||||
settings = self._load_settings()
|
||||
|
||||
# Create Tkinter window
|
||||
root = tk.Tk()
|
||||
root.title(f"Watch Point - Monitor {display_idx}")
|
||||
|
||||
# Configure window icon if exists
|
||||
if ICON_BASE64:
|
||||
try:
|
||||
icon_img = tk.PhotoImage(data=ICON_BASE64)
|
||||
root.iconphoto(True, icon_img)
|
||||
except Exception as e:
|
||||
print(f"Could not set window icon: {e}")
|
||||
except Exception: pass
|
||||
|
||||
# Set window size from settings
|
||||
window_size_mode = settings.get("window_size_mode", "fixed")
|
||||
|
||||
if window_size_mode in ["Half Vertical", "Half Horizontal", "Quarter"]:
|
||||
# Get screen dimensions
|
||||
screen_w = root.winfo_screenwidth()
|
||||
screen_h = root.winfo_screenheight()
|
||||
|
||||
# Calculate dynamic size
|
||||
if window_size_mode == "Half Vertical":
|
||||
window_width = screen_w // 2
|
||||
window_height = screen_h - 100
|
||||
root.geometry(f"{screen_w // 2}x{screen_h - 100}")
|
||||
elif window_size_mode == "Half Horizontal":
|
||||
window_width = screen_w - 100
|
||||
window_height = screen_h // 2
|
||||
root.geometry(f"{screen_w - 100}x{screen_h // 2}")
|
||||
elif window_size_mode == "Quarter":
|
||||
window_width = screen_w // 2
|
||||
window_height = screen_h // 2
|
||||
root.geometry(f"{screen_w // 2}x{screen_h // 2}")
|
||||
else:
|
||||
# Use fixed size from settings
|
||||
window_width = settings.get("window_width", 800)
|
||||
window_height = settings.get("window_height", 600)
|
||||
w = settings.get("window_width", 800)
|
||||
h = settings.get("window_height", 600)
|
||||
root.geometry(f"{w}x{h}")
|
||||
|
||||
root.geometry(f"{window_width}x{window_height}")
|
||||
|
||||
# Store root reference
|
||||
self._windows[display_idx]["root"] = root
|
||||
|
||||
# Create preview window instance
|
||||
# Instanciar la UI
|
||||
preview = WatchPointWindow(root, display_idx, self._windows, settings)
|
||||
self._windows[display_idx]["window"] = preview
|
||||
|
||||
# Position window based on settings or monitor position
|
||||
window_x = settings.get("window_x", None)
|
||||
window_y = settings.get("window_y", None)
|
||||
|
||||
if window_x is not None and window_y is not None:
|
||||
# Use saved position from settings
|
||||
root.geometry(f"+{window_x}+{window_y}")
|
||||
# Restaurar posición
|
||||
wx = settings.get("window_x", None)
|
||||
wy = settings.get("window_y", None)
|
||||
if wx is not None and wy is not None:
|
||||
root.geometry(f"+{wx}+{wy}")
|
||||
elif SCREENINFO_AVAILABLE:
|
||||
# Fallback to monitor position
|
||||
try:
|
||||
monitor_info = get_monitors()[display_idx]
|
||||
x = monitor_info.x + 50
|
||||
y = monitor_info.y + 50
|
||||
root.geometry(f"+{x}+{y}")
|
||||
except Exception:
|
||||
pass
|
||||
m = get_monitors()[display_idx]
|
||||
root.geometry(f"+{m.x + 50}+{m.y + 50}")
|
||||
except: pass
|
||||
|
||||
# Start Tkinter main loop
|
||||
try:
|
||||
root.mainloop()
|
||||
except Exception as e:
|
||||
print(f"Watch Point window error: {e}")
|
||||
print(f"Watch Point error: {e}")
|
||||
finally:
|
||||
self._windows[display_idx]["running"] = False
|
||||
|
||||
def _load_settings(self):
|
||||
"""Load settings from JSON file"""
|
||||
settings_file = os.path.join(os.path.dirname(__file__), "watchpoint_settings.json")
|
||||
|
||||
default_settings = {
|
||||
"window_width": 800,
|
||||
"window_height": 600,
|
||||
"window_x": None,
|
||||
"window_y": None,
|
||||
"show_toolbar": True,
|
||||
"save_format": "png",
|
||||
"jpeg_quality": 90,
|
||||
default = {
|
||||
"window_width": 800, "window_height": 600,
|
||||
"window_x": None, "window_y": None,
|
||||
"show_toolbar": True, "save_format": "png", "jpeg_quality": 90,
|
||||
}
|
||||
|
||||
try:
|
||||
if os.path.exists(settings_file):
|
||||
with open(settings_file, 'r') as f:
|
||||
user_settings = json.load(f)
|
||||
return {**default_settings, **user_settings}
|
||||
except Exception as e:
|
||||
print(f"Could not load settings: {e}")
|
||||
|
||||
return default_settings
|
||||
return {**default, **json.load(f)}
|
||||
except: pass
|
||||
return default
|
||||
|
||||
|
||||
class WatchPointWindow:
|
||||
"""Tkinter-based preview window with zoom/pan"""
|
||||
|
||||
def __init__(self, root, display_idx, windows_dict, settings):
|
||||
self.root = root
|
||||
self.display_idx = display_idx
|
||||
self.windows = windows_dict
|
||||
self.settings = settings
|
||||
|
||||
# Zoom/Pan state
|
||||
# State
|
||||
self.zoom_level = 1.0
|
||||
self.pan_x = 0
|
||||
self.pan_y = 0
|
||||
@@ -283,468 +221,219 @@ class WatchPointWindow:
|
||||
self.drag_start_x = 0
|
||||
self.drag_start_y = 0
|
||||
self.zoom_1to1_active = False
|
||||
|
||||
# Current image
|
||||
self.drawer_visible = True
|
||||
self.current_pil_image = None
|
||||
self.photo_image = None
|
||||
|
||||
# Setup UI
|
||||
self._create_ui()
|
||||
self._bind_events()
|
||||
|
||||
# Start update loop
|
||||
# Check for pending text (Race condition fix)
|
||||
pending = self.windows[self.display_idx].get("pending_text")
|
||||
if pending:
|
||||
self.update_signal_text(pending)
|
||||
self.windows[self.display_idx]["pending_text"] = None
|
||||
|
||||
self._update_image()
|
||||
|
||||
def _create_ui(self):
|
||||
"""Create UI elements"""
|
||||
|
||||
# Main container
|
||||
# 1. Drawer (Panel Lateral de Texto)
|
||||
self.drawer_width = 300
|
||||
self.drawer_frame = tk.Frame(self.root, bg="#1c1c1c", width=self.drawer_width)
|
||||
self.drawer_frame.pack(side="right", fill="y")
|
||||
self.drawer_frame.pack_propagate(False)
|
||||
|
||||
self.label_scout = tk.Label(
|
||||
self.drawer_frame, text="📡 SIGNAL SCOUT",
|
||||
bg="#1c1c1c", fg="#666", font=("Arial", 8, "bold"), pady=5
|
||||
)
|
||||
self.label_scout.pack(fill="x")
|
||||
|
||||
self.signal_text = tk.Text(
|
||||
self.drawer_frame, bg="#1c1c1c", fg="#00ff99",
|
||||
insertbackground="white", font=("Consolas", 10), wrap="word",
|
||||
padx=10, pady=10, borderwidth=0, highlightthickness=0
|
||||
)
|
||||
self.signal_text.pack(fill="both", expand=True)
|
||||
self.signal_text.insert("1.0", "Waiting for signal...")
|
||||
self.signal_text.config(state="disabled")
|
||||
|
||||
# 2. Main Area
|
||||
self.main_frame = tk.Frame(self.root, bg='#1a1a1a')
|
||||
self.main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Toolbar
|
||||
self.toolbar_visible = self.settings.get("show_toolbar", True)
|
||||
self._create_toolbar()
|
||||
|
||||
# Canvas for image display
|
||||
self.canvas = tk.Canvas(self.main_frame, bg='#000000', highlightthickness=0)
|
||||
self.canvas.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Create context menu
|
||||
self._create_context_menu()
|
||||
|
||||
def update_signal_text(self, text):
|
||||
def _update():
|
||||
self.signal_text.config(state="normal")
|
||||
self.signal_text.delete("1.0", tk.END)
|
||||
self.signal_text.insert("1.0", text)
|
||||
self.signal_text.config(state="disabled")
|
||||
self.root.after(0, _update)
|
||||
|
||||
def _create_toolbar(self):
|
||||
"""Create toolbar with buttons"""
|
||||
self.toolbar = tk.Frame(self.main_frame, bg='#2a2a2a', height=40)
|
||||
|
||||
# Buttons
|
||||
btn_style = {'bg': '#3a3a3a', 'fg': 'white', 'relief': tk.FLAT, 'padx': 8, 'pady': 4}
|
||||
|
||||
tk.Button(self.toolbar, text="↻ Reset", command=self._reset_zoom, **btn_style).pack(side=tk.LEFT, padx=2)
|
||||
tk.Button(self.toolbar, text="⊕ Zoom In", command=self._zoom_in, **btn_style).pack(side=tk.LEFT, padx=2)
|
||||
tk.Button(self.toolbar, text="⊖ Zoom Out", command=self._zoom_out, **btn_style).pack(side=tk.LEFT, padx=2)
|
||||
tk.Button(self.toolbar, text="1:1 (100%)", command=self._zoom_1to1, **btn_style).pack(side=tk.LEFT, padx=2)
|
||||
tk.Button(self.toolbar, text="1:1", command=self._zoom_1to1, **btn_style).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# Window size dropdown
|
||||
self.size_var = tk.StringVar(value="Current")
|
||||
size_options = [
|
||||
"800x600",
|
||||
"1024x768",
|
||||
"1280x720",
|
||||
"1920x1080",
|
||||
"---",
|
||||
"Half Vertical",
|
||||
"Half Horizontal",
|
||||
"Quarter"
|
||||
]
|
||||
|
||||
size_menu = tk.OptionMenu(self.toolbar, self.size_var, *size_options,
|
||||
command=self._change_window_size)
|
||||
size_menu = tk.OptionMenu(self.toolbar, self.size_var, "800x600", "1024x768", "1920x1080", "Half Vertical", "Quarter", command=self._change_window_size)
|
||||
size_menu.config(bg='#3a3a3a', fg='white', relief=tk.FLAT)
|
||||
size_menu.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
tk.Button(self.toolbar, text="⛶ Fullscreen", command=self._toggle_fullscreen, **btn_style).pack(side=tk.LEFT, padx=2)
|
||||
tk.Button(self.toolbar, text="⚙ Settings", command=self._open_settings, **btn_style).pack(side=tk.LEFT, padx=2)
|
||||
tk.Button(self.toolbar, text="⚙", command=self._open_settings, **btn_style).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
if self.toolbar_visible:
|
||||
self.toolbar.pack(side=tk.TOP, fill=tk.X)
|
||||
|
||||
def _create_context_menu(self):
|
||||
"""Create right-click context menu"""
|
||||
self.context_menu = tk.Menu(self.root, tearoff=0)
|
||||
self.context_menu.add_command(label="Save Image As...", command=self._save_image)
|
||||
self.context_menu.add_command(label="Copy to Clipboard", command=self._copy_to_clipboard)
|
||||
self.context_menu.add_separator()
|
||||
self.context_menu.add_command(label="Zoom 1:1 (100%)", command=self._zoom_1to1)
|
||||
self.context_menu.add_command(label="Reset Zoom", command=self._reset_zoom)
|
||||
|
||||
self.context_menu.add_command(label="Toggle Drawer", command=self._toggle_drawer)
|
||||
|
||||
def _bind_events(self):
|
||||
"""Bind keyboard and mouse events"""
|
||||
|
||||
# Mouse events
|
||||
self.canvas.bind("<ButtonPress-1>", self._on_mouse_down)
|
||||
self.canvas.bind("<B1-Motion>", self._on_mouse_drag)
|
||||
self.canvas.bind("<ButtonRelease-1>", self._on_mouse_up)
|
||||
self.canvas.bind("<MouseWheel>", self._on_mouse_wheel)
|
||||
self.canvas.bind("<Button-3>", self._show_context_menu)
|
||||
|
||||
# Keyboard events
|
||||
self.root.bind("<KeyPress-r>", lambda e: self._reset_zoom())
|
||||
self.root.bind("<KeyPress-R>", lambda e: self._reset_zoom())
|
||||
self.root.bind("<KeyPress-t>", lambda e: self._toggle_toolbar())
|
||||
self.root.bind("<KeyPress-T>", lambda e: self._toggle_toolbar())
|
||||
self.root.bind("<KeyPress-1>", lambda e: self._zoom_1to1())
|
||||
self.root.bind("<r>", lambda e: self._reset_zoom())
|
||||
self.root.bind("<t>", lambda e: self._toggle_toolbar())
|
||||
self.root.bind("<p>", lambda e: self._toggle_drawer())
|
||||
self.root.bind("<Escape>", lambda e: self._close_window())
|
||||
|
||||
# Window close event
|
||||
self.root.protocol("WM_DELETE_WINDOW", self._close_window)
|
||||
|
||||
def _update_image(self):
|
||||
"""Update displayed image from shared state"""
|
||||
try:
|
||||
if not self.windows[self.display_idx]["running"]:
|
||||
# Window is closing, stop the loop gracefully
|
||||
try:
|
||||
self.root.quit()
|
||||
except:
|
||||
pass
|
||||
try: self.root.quit()
|
||||
except: pass
|
||||
return
|
||||
|
||||
with self.windows[self.display_idx]["lock"]:
|
||||
pil_img = self.windows[self.display_idx]["image"]
|
||||
|
||||
if pil_img is not None:
|
||||
if pil_img != self.current_pil_image:
|
||||
self.current_pil_image = pil_img
|
||||
self._render_image()
|
||||
if pil_img is not None and pil_img != self.current_pil_image:
|
||||
self.current_pil_image = pil_img
|
||||
self._render_image()
|
||||
|
||||
self.root.after(33, self._update_image) # ~30 FPS
|
||||
except Exception as e:
|
||||
print(f"Update image error: {e}")
|
||||
# If error, mark as not running and stop
|
||||
self.root.after(33, self._update_image)
|
||||
except:
|
||||
self.windows[self.display_idx]["running"] = False
|
||||
try:
|
||||
self.root.quit()
|
||||
except:
|
||||
pass
|
||||
try: self.root.quit()
|
||||
except: pass
|
||||
|
||||
def _render_image(self):
|
||||
"""Render image on canvas with zoom/pan"""
|
||||
if not self.current_pil_image:
|
||||
return
|
||||
|
||||
if not self.current_pil_image: return
|
||||
self.canvas.update_idletasks()
|
||||
|
||||
canvas_width = self.canvas.winfo_width()
|
||||
canvas_height = self.canvas.winfo_height()
|
||||
|
||||
if canvas_width <= 1 or canvas_height <= 1:
|
||||
cw, ch = self.canvas.winfo_width(), self.canvas.winfo_height()
|
||||
if cw <= 1:
|
||||
self.root.after(100, self._render_image)
|
||||
return
|
||||
|
||||
try:
|
||||
img_w, img_h = self.current_pil_image.size
|
||||
|
||||
if self.zoom_1to1_active:
|
||||
scale = 1.0
|
||||
else:
|
||||
scale = min(canvas_width / img_w, canvas_height / img_h) * self.zoom_level
|
||||
|
||||
new_w = max(1, int(img_w * scale))
|
||||
new_h = max(1, int(img_h * scale))
|
||||
|
||||
resized = self.current_pil_image.resize((new_w, new_h), Image.LANCZOS)
|
||||
|
||||
x_pos = (canvas_width - new_w) // 2 + int(self.pan_x)
|
||||
y_pos = (canvas_height - new_h) // 2 + int(self.pan_y)
|
||||
|
||||
self.photo_image = ImageTk.PhotoImage(resized)
|
||||
|
||||
self.canvas.delete("all")
|
||||
self.canvas.create_image(x_pos, y_pos, anchor=tk.NW, image=self.photo_image)
|
||||
except Exception as e:
|
||||
print(f"Render error: {e}")
|
||||
|
||||
iw, ih = self.current_pil_image.size
|
||||
scale = 1.0 if self.zoom_1to1_active else min(cw/iw, ch/ih) * self.zoom_level
|
||||
nw, nh = max(1, int(iw*scale)), max(1, int(ih*scale))
|
||||
|
||||
resized = self.current_pil_image.resize((nw, nh), Image.LANCZOS)
|
||||
xp = (cw - nw)//2 + int(self.pan_x)
|
||||
yp = (ch - nh)//2 + int(self.pan_y)
|
||||
|
||||
self.photo_image = ImageTk.PhotoImage(resized)
|
||||
self.canvas.delete("all")
|
||||
self.canvas.create_image(xp, yp, anchor=tk.NW, image=self.photo_image)
|
||||
|
||||
def _on_mouse_wheel(self, event):
|
||||
"""Handle mouse wheel for zoom"""
|
||||
if not self.zoom_1to1_active:
|
||||
if event.delta > 0:
|
||||
self._zoom_in()
|
||||
else:
|
||||
self._zoom_out()
|
||||
if event.delta > 0: self._zoom_in()
|
||||
else: self._zoom_out()
|
||||
|
||||
def _on_mouse_down(self, event):
|
||||
"""Start dragging"""
|
||||
self.is_dragging = True
|
||||
self.drag_start_x = event.x
|
||||
self.drag_start_y = event.y
|
||||
|
||||
def _on_mouse_drag(self, event):
|
||||
"""Handle dragging for pan"""
|
||||
if self.is_dragging:
|
||||
dx = event.x - self.drag_start_x
|
||||
dy = event.y - self.drag_start_y
|
||||
self.pan_x += dx
|
||||
self.pan_y += dy
|
||||
self.pan_x += event.x - self.drag_start_x
|
||||
self.pan_y += event.y - self.drag_start_y
|
||||
self.drag_start_x = event.x
|
||||
self.drag_start_y = event.y
|
||||
self._render_image()
|
||||
|
||||
def _on_mouse_up(self, event):
|
||||
"""Stop dragging"""
|
||||
self.is_dragging = False
|
||||
|
||||
def _show_context_menu(self, event):
|
||||
"""Show context menu on right click"""
|
||||
try:
|
||||
self.context_menu.tk_popup(event.x_root, event.y_root)
|
||||
finally:
|
||||
self.context_menu.grab_release()
|
||||
|
||||
def _zoom_in(self):
|
||||
"""Zoom in"""
|
||||
def _on_mouse_up(self, event): self.is_dragging = False
|
||||
def _show_context_menu(self, event): self.context_menu.tk_popup(event.x_root, event.y_root)
|
||||
def _zoom_in(self):
|
||||
self.zoom_1to1_active = False
|
||||
self.zoom_level = min(10.0, self.zoom_level * 1.2)
|
||||
self._render_image()
|
||||
|
||||
def _zoom_out(self):
|
||||
"""Zoom out"""
|
||||
self.zoom_1to1_active = False
|
||||
self.zoom_level = max(0.1, self.zoom_level / 1.2)
|
||||
self._render_image()
|
||||
|
||||
def _zoom_1to1(self):
|
||||
"""Zoom to 1:1 (100% actual size)"""
|
||||
self.zoom_1to1_active = True
|
||||
self.pan_x = 0
|
||||
self.pan_y = 0
|
||||
self.pan_x = 0; self.pan_y = 0
|
||||
self._render_image()
|
||||
|
||||
def _reset_zoom(self):
|
||||
"""Reset zoom and pan"""
|
||||
self.zoom_1to1_active = False
|
||||
self.zoom_level = 1.0
|
||||
self.pan_x = 0
|
||||
self.pan_y = 0
|
||||
self.pan_x = 0; self.pan_y = 0
|
||||
self._render_image()
|
||||
|
||||
def _toggle_toolbar(self):
|
||||
"""Toggle toolbar visibility"""
|
||||
if self.toolbar_visible:
|
||||
self.toolbar.pack_forget()
|
||||
self.toolbar_visible = False
|
||||
else:
|
||||
self.toolbar.pack(side=tk.TOP, fill=tk.X, before=self.canvas)
|
||||
self.toolbar_visible = True
|
||||
if self.toolbar_visible: self.toolbar.pack_forget(); self.toolbar_visible = False
|
||||
else: self.toolbar.pack(side=tk.TOP, fill=tk.X, before=self.canvas); self.toolbar_visible = True
|
||||
|
||||
def _toggle_drawer(self):
|
||||
if self.drawer_visible: self.drawer_frame.pack_forget(); self.drawer_visible = False
|
||||
else: self.drawer_frame.pack(side="right", fill="y", before=self.main_frame); self.drawer_visible = True
|
||||
|
||||
def _change_window_size(self, size_str):
|
||||
"""Change window size"""
|
||||
if size_str == "---":
|
||||
return
|
||||
|
||||
# Simplificado para el ejemplo
|
||||
try:
|
||||
screen_w = self.root.winfo_screenwidth()
|
||||
screen_h = self.root.winfo_screenheight()
|
||||
|
||||
if size_str == "Half Vertical":
|
||||
w = screen_w // 2
|
||||
h = screen_h - 100
|
||||
self.root.geometry(f"{w}x{h}")
|
||||
elif size_str == "Half Horizontal":
|
||||
w = screen_w - 100
|
||||
h = screen_h // 2
|
||||
self.root.geometry(f"{w}x{h}")
|
||||
elif size_str == "Quarter":
|
||||
w = screen_w // 2
|
||||
h = screen_h // 2
|
||||
self.root.geometry(f"{w}x{h}")
|
||||
else:
|
||||
w, h = map(int, size_str.split('x'))
|
||||
self.root.geometry(f"{w}x{h}")
|
||||
except Exception as e:
|
||||
print(f"Error changing window size: {e}")
|
||||
|
||||
def _toggle_fullscreen(self):
|
||||
"""Toggle fullscreen mode"""
|
||||
current_state = self.root.attributes('-fullscreen')
|
||||
self.root.attributes('-fullscreen', not current_state)
|
||||
|
||||
w, h = map(int, size_str.split('x'))
|
||||
self.root.geometry(f"{w}x{h}")
|
||||
except: pass
|
||||
|
||||
def _save_image(self):
|
||||
"""Save current image to file"""
|
||||
if not self.current_pil_image:
|
||||
messagebox.showwarning("No Image", "No image to save")
|
||||
return
|
||||
|
||||
save_format = self.settings.get("save_format", "png").upper()
|
||||
|
||||
filetypes = [
|
||||
("PNG Image", "*.png"),
|
||||
("JPEG Image", "*.jpg"),
|
||||
("WebP Image", "*.webp"),
|
||||
("All Files", "*.*")
|
||||
]
|
||||
|
||||
filename = filedialog.asksaveasfilename(
|
||||
defaultextension=f".{save_format.lower()}",
|
||||
filetypes=filetypes,
|
||||
title="Save Image As"
|
||||
)
|
||||
|
||||
if filename:
|
||||
try:
|
||||
if filename.lower().endswith('.jpg') or filename.lower().endswith('.jpeg'):
|
||||
quality = self.settings.get("jpeg_quality", 90)
|
||||
self.current_pil_image.save(filename, "JPEG", quality=quality)
|
||||
else:
|
||||
self.current_pil_image.save(filename)
|
||||
messagebox.showinfo("Success", f"Image saved to:\n{filename}")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to save image:\n{str(e)}")
|
||||
if not self.current_pil_image: return
|
||||
f = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG", "*.png")])
|
||||
if f: self.current_pil_image.save(f)
|
||||
|
||||
def _copy_to_clipboard(self):
|
||||
"""Copy image to clipboard"""
|
||||
if not self.current_pil_image:
|
||||
messagebox.showwarning("No Image", "No image to copy")
|
||||
return
|
||||
|
||||
try:
|
||||
output = io.BytesIO()
|
||||
self.current_pil_image.convert('RGB').save(output, 'BMP')
|
||||
data = output.getvalue()[14:]
|
||||
output.close()
|
||||
|
||||
import win32clipboard
|
||||
win32clipboard.OpenClipboard()
|
||||
win32clipboard.EmptyClipboard()
|
||||
win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
|
||||
win32clipboard.CloseClipboard()
|
||||
|
||||
messagebox.showinfo("Success", "Image copied to clipboard")
|
||||
except ImportError:
|
||||
messagebox.showerror("Error", "pywin32 not installed. Install with: pip install pywin32")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to copy:\n{str(e)}")
|
||||
# Requiere pywin32, omitido por simplicidad si no está instalado
|
||||
pass
|
||||
|
||||
def _open_settings(self):
|
||||
"""Open settings dialog"""
|
||||
WatchPointSettingsDialog(self.root, self.settings)
|
||||
|
||||
def _close_window(self):
|
||||
"""Close window properly"""
|
||||
self.windows[self.display_idx]["running"] = False
|
||||
try:
|
||||
self.root.destroy()
|
||||
except:
|
||||
pass
|
||||
|
||||
try: self.root.destroy()
|
||||
except: pass
|
||||
|
||||
class WatchPointSettingsDialog:
|
||||
"""Settings dialog window"""
|
||||
|
||||
def __init__(self, parent, settings):
|
||||
self.settings = settings
|
||||
self.dialog = tk.Toplevel(parent)
|
||||
self.dialog.title("Watch Point Settings")
|
||||
self.dialog.geometry("450x650")
|
||||
self.dialog.resizable(True, True)
|
||||
|
||||
self._create_ui()
|
||||
|
||||
def _create_ui(self):
|
||||
"""Create settings UI"""
|
||||
|
||||
# Window Size section
|
||||
size_frame = tk.LabelFrame(self.dialog, text="Default Window Size", padx=10, pady=10)
|
||||
size_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
current_mode = self.settings.get("window_size_mode", "fixed")
|
||||
if current_mode in ["Half Vertical", "Half Horizontal", "Quarter"]:
|
||||
initial_value = current_mode
|
||||
else:
|
||||
initial_value = f"{self.settings.get('window_width', 800)}x{self.settings.get('window_height', 600)}"
|
||||
|
||||
self.size_var = tk.StringVar(value=initial_value)
|
||||
|
||||
sizes = [
|
||||
"800x600",
|
||||
"1024x768",
|
||||
"1280x720",
|
||||
"1920x1080",
|
||||
"Half Vertical",
|
||||
"Half Horizontal",
|
||||
"Quarter"
|
||||
]
|
||||
|
||||
for size in sizes:
|
||||
tk.Radiobutton(size_frame, text=size, variable=self.size_var, value=size).pack(anchor=tk.W)
|
||||
|
||||
# Window Position section
|
||||
position_frame = tk.LabelFrame(self.dialog, text="Window Position", padx=10, pady=10)
|
||||
position_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
tk.Label(position_frame, text="X coordinate:").grid(row=0, column=0, sticky=tk.W, pady=5)
|
||||
self.pos_x_var = tk.StringVar(value=str(self.settings.get("window_x", "")))
|
||||
tk.Entry(position_frame, textvariable=self.pos_x_var, width=10).grid(row=0, column=1, sticky=tk.W, padx=5)
|
||||
tk.Label(position_frame, text="(leave empty for auto)").grid(row=0, column=2, sticky=tk.W)
|
||||
|
||||
tk.Label(position_frame, text="Y coordinate:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
self.pos_y_var = tk.StringVar(value=str(self.settings.get("window_y", "")))
|
||||
tk.Entry(position_frame, textvariable=self.pos_y_var, width=10).grid(row=1, column=1, sticky=tk.W, padx=5)
|
||||
tk.Label(position_frame, text="(leave empty for auto)").grid(row=1, column=2, sticky=tk.W)
|
||||
|
||||
# Save Format section
|
||||
format_frame = tk.LabelFrame(self.dialog, text="Save Settings", padx=10, pady=10)
|
||||
format_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
tk.Label(format_frame, text="Default Format:").grid(row=0, column=0, sticky=tk.W)
|
||||
self.format_var = tk.StringVar(value=self.settings.get("save_format", "png").upper())
|
||||
format_menu = tk.OptionMenu(format_frame, self.format_var, "PNG", "JPEG", "WEBP")
|
||||
format_menu.grid(row=0, column=1, sticky=tk.W, padx=5)
|
||||
|
||||
tk.Label(format_frame, text="JPEG Quality:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
self.quality_var = tk.IntVar(value=self.settings.get("jpeg_quality", 90))
|
||||
quality_scale = tk.Scale(format_frame, from_=10, to=100, orient=tk.HORIZONTAL,
|
||||
variable=self.quality_var, length=200)
|
||||
quality_scale.grid(row=1, column=1, sticky=tk.W, padx=5)
|
||||
|
||||
# UI Options
|
||||
ui_frame = tk.LabelFrame(self.dialog, text="UI Options", padx=10, pady=10)
|
||||
ui_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
self.toolbar_var = tk.BooleanVar(value=self.settings.get("show_toolbar", True))
|
||||
tk.Checkbutton(ui_frame, text="Show toolbar by default", variable=self.toolbar_var).pack(anchor=tk.W)
|
||||
|
||||
# Buttons at bottom
|
||||
btn_frame = tk.Frame(self.dialog, pady=10)
|
||||
btn_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=10)
|
||||
|
||||
tk.Button(btn_frame, text="Cancel", command=self.dialog.destroy, width=10,
|
||||
bg='#666', fg='white').pack(side=tk.RIGHT, padx=5)
|
||||
tk.Button(btn_frame, text="Save", command=self._save, width=10,
|
||||
bg='#4a9eff', fg='white').pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
def _save(self):
|
||||
"""Save settings"""
|
||||
size_value = self.size_var.get()
|
||||
|
||||
if size_value in ["Half Vertical", "Half Horizontal", "Quarter"]:
|
||||
self.settings["window_size_mode"] = size_value
|
||||
else:
|
||||
try:
|
||||
w, h = map(int, size_value.split('x'))
|
||||
self.settings["window_width"] = w
|
||||
self.settings["window_height"] = h
|
||||
self.settings["window_size_mode"] = "fixed"
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
x_str = self.pos_x_var.get().strip()
|
||||
y_str = self.pos_y_var.get().strip()
|
||||
|
||||
if x_str and y_str:
|
||||
self.settings["window_x"] = int(x_str)
|
||||
self.settings["window_y"] = int(y_str)
|
||||
else:
|
||||
self.settings["window_x"] = None
|
||||
self.settings["window_y"] = None
|
||||
except ValueError:
|
||||
messagebox.showwarning("Invalid Input", "X and Y coordinates must be valid numbers")
|
||||
return
|
||||
|
||||
self.settings["save_format"] = self.format_var.get().lower()
|
||||
self.settings["jpeg_quality"] = self.quality_var.get()
|
||||
self.settings["show_toolbar"] = self.toolbar_var.get()
|
||||
|
||||
settings_file = os.path.join(os.path.dirname(__file__), "watchpoint_settings.json")
|
||||
try:
|
||||
with open(settings_file, 'w') as f:
|
||||
json.dump(self.settings, f, indent=2)
|
||||
messagebox.showinfo("Success", "Settings saved!")
|
||||
self.dialog.destroy()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Could not save settings:\n{str(e)}")
|
||||
|
||||
self.dialog.title("Settings")
|
||||
self.dialog.geometry("300x200")
|
||||
tk.Label(self.dialog, text="Settings placeholder").pack(pady=20)
|
||||
tk.Button(self.dialog, text="Close", command=self.dialog.destroy).pack()
|
||||
|
||||
# Node registration
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
|
||||
Reference in New Issue
Block a user