# -*- coding: utf-8 -*- import os import json import logging import tkinter as tk from tkinter import messagebox, ttk import tkinter.font as tkFont from tkinter.scrolledtext import ScrolledText from datetime import datetime import subprocess from typing import Any, Dict, List, Optional # Import the registry to find GUI elements during playback from pymsc.gui.components.command_widgets import WIDGET_MAP def center_window(parent: tk.Widget, window: tk.Toplevel, width: int, height: int): """ Centers a Toplevel window relative to its parent. """ p_x = parent.winfo_rootx() p_y = parent.winfo_rooty() p_w = parent.winfo_width() p_h = parent.winfo_height() x = p_x + (p_w - width) // 2 y = p_y + (p_h - height) // 2 window.geometry(f"{width}x{height}+{x}+{y}") class JsonEditor(tk.Toplevel): """ A simple JSON text editor for manual script modification. """ def __init__(self, parent: tk.Widget, script_path: str): super().__init__(parent) self.title("Script Editor") self.script_path = script_path center_window(parent, self, 800, 600) self._create_ui() self.load_script() def _create_ui(self): self.editor = ScrolledText(self, wrap=tk.WORD, font=("Courier New", 10)) self.editor.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) btn_frame = tk.Frame(self) btn_frame.pack(fill=tk.X, pady=5) save_btn = tk.Button(btn_frame, text="Save", command=self.save_script) save_btn.pack(side=tk.LEFT, padx=5) close_btn = tk.Button(btn_frame, text="Close", command=self.destroy) close_btn.pack(side=tk.RIGHT, padx=5) def load_script(self): try: with open(self.script_path, "r") as file: content = json.load(file) pretty = json.dumps(content, indent=4) self.editor.insert(tk.END, pretty) except Exception as e: messagebox.showerror("Error", f"Load failed: {e}") def save_script(self): try: raw_content = self.editor.get("1.0", tk.END).strip() json.loads(raw_content) # Validation with open(self.script_path, "w") as file: file.write(raw_content) messagebox.showinfo("Success", "Script saved.") except json.JSONDecodeError: messagebox.showerror("Error", "Invalid JSON format.") except Exception as e: messagebox.showerror("Error", f"Save failed: {e}") class ScriptManager: """ Singleton class to handle recording and playback of GUI commands. """ _instance = None def __new__(cls, *args, **kwargs): if not cls._instance: cls._instance = super(ScriptManager, cls).__new__(cls) return cls._instance def __init__(self, scripts_dir: str = "scripts"): if hasattr(self, "_initialized"): return self._initialized = True self.scripts_dir = scripts_dir self.current_script_path = None self.is_recording = False self.is_executing = False self.is_paused = False self.commands: List[Dict] = [] self.metadata: Dict = {} self.action_count = 0 self.gui_window: Optional[tk.Toplevel] = None self.logger = logging.getLogger("PyMsc") if not os.path.exists(self.scripts_dir): os.makedirs(self.scripts_dir) def open_manager_gui(self, root: tk.Tk): """ Creates the main Script Manager interface. """ if self.gui_window and self.gui_window.winfo_exists(): self.gui_window.deiconify() return self.gui_window = tk.Toplevel(root) self.gui_window.title("Script Manager") center_window(root, self.gui_window, 600, 700) self._build_ui() self.refresh_list() def _build_ui(self): # Recording Control Frame rec_frame = tk.LabelFrame(self.gui_window, text="Recording Controls") rec_frame.pack(fill=tk.X, padx=10, pady=5) self.start_btn = tk.Button(rec_frame, text="NEW REC", command=self.setup_new_script) self.start_btn.pack(side=tk.LEFT, padx=5, pady=5) self.stop_btn = tk.Button(rec_frame, text="STOP", state=tk.DISABLED, command=self.stop_recording) self.stop_btn.pack(side=tk.LEFT, padx=5, pady=5) # Script List Frame list_frame = tk.LabelFrame(self.gui_window, text="Available Scripts") list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) self.script_listbox = tk.Listbox(list_frame) self.script_listbox.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) btn_frame = tk.Frame(list_frame) btn_frame.pack(fill=tk.X, pady=5) self.exec_btn = tk.Button(btn_frame, text="EXECUTE", command=self.run_selected) self.exec_btn.pack(side=tk.LEFT, padx=5) self.edit_btn = tk.Button(btn_frame, text="EDIT", command=self.edit_selected) self.edit_btn.pack(side=tk.LEFT, padx=5) self.del_btn = tk.Button(btn_frame, text="DELETE", command=self.delete_selected) self.del_btn.pack(side=tk.LEFT, padx=5) # Log and Status log_frame = tk.LabelFrame(self.gui_window, text="Execution Log") log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) self.log_text = ScrolledText(log_frame, state=tk.DISABLED, height=8, font=("Helvetica", 8)) self.log_text.pack(fill=tk.BOTH, expand=True) def log_event(self, message: str, level: str = "info"): """ Adds a timestamped message to the internal log view. """ if not self.gui_window: return timestamp = datetime.now().strftime("%H:%M:%S") self.log_text.config(state=tk.NORMAL) self.log_text.insert(tk.END, f"[{timestamp}] {message}\n") self.log_text.see(tk.END) self.log_text.config(state=tk.DISABLED) def setup_new_script(self): """ Dialog to initialize script metadata. """ diag = tk.Toplevel(self.gui_window) diag.title("New Script Settings") center_window(self.gui_window, diag, 300, 200) tk.Label(diag, text="Script Name:").pack(pady=2) name_var = tk.StringVar() tk.Entry(diag, textvariable=name_var).pack(pady=2) def confirm(): name = name_var.get().strip() if not name: return self.start_recording(name) diag.destroy() tk.Button(diag, text="Start Recording", command=confirm).pack(pady=10) def start_recording(self, name: str): self.metadata = { "name": name, "date": datetime.now().isoformat(), "commands": [] } self.current_script_path = os.path.join(self.scripts_dir, f"{name}.json") self.is_recording = True self.action_count = 0 self.start_btn.config(state=tk.DISABLED) self.stop_btn.config(state=tk.NORMAL) self.log_event(f"Started recording: {name}") def write_command(self, control_id: str, action: str, value: Any): """ Hook called by widgets to record an action. """ if not self.is_recording: return self.action_count += 1 cmd = { "index": self.action_count, "control_id": control_id, "action": action, "value": value, "delay_ms": 1000 # Default delay for playback } self.metadata["commands"].append(cmd) self.log_event(f"Recorded: {control_id} -> {action}({value})") def stop_recording(self): if not self.is_recording: return with open(self.current_script_path, "w") as f: json.dump(self.metadata, f, indent=4) self.is_recording = False self.start_btn.config(state=tk.NORMAL) self.stop_btn.config(state=tk.DISABLED) self.log_event("Recording saved.") self.refresh_list() def refresh_list(self): self.script_listbox.delete(0, tk.END) for f in os.listdir(self.scripts_dir): if f.endswith(".json"): self.script_listbox.insert(tk.END, f) def run_selected(self): selection = self.script_listbox.curselection() if not selection: return filename = self.script_listbox.get(selection[0]) path = os.path.join(self.scripts_dir, filename) try: with open(path, "r") as f: data = json.load(f) self.is_executing = True self.log_event(f"Executing script: {filename}") self._playback_loop(data["commands"], 0) except Exception as e: messagebox.showerror("Error", f"Playback failed: {e}") def _playback_loop(self, commands: List[Dict], index: int): """ Recursive execution of commands using Tkinter's event loop. """ if not self.is_executing or index >= len(commands): self.is_executing = False self.log_event("Execution finished.") return cmd = commands[index] self._execute_single_command(cmd) # Schedule next command delay = cmd.get("delay_ms", 1000) self.gui_window.after(delay, lambda: self._playback_loop(commands, index + 1)) def _execute_single_command(self, cmd: Dict): """ Finds the widget and triggers the corresponding action. """ ctrl_id = cmd["control_id"] action = cmd["action"] value = cmd["value"] widget_data = WIDGET_MAP.get(ctrl_id) if not widget_data: self.log_event(f"Widget not found: {ctrl_id}", "error") return # Case 1: Complex Control from WIDGET_MAP (dict entry) if isinstance(widget_data, dict): widget = widget_data["widget"] var = widget_data["var"] if action == "toggle" and isinstance(widget, tk.Checkbutton): new_bool = (value == "on") var.set(new_bool) widget.invoke() elif action == "set_value": var.set(value) if isinstance(widget, ttk.Combobox): widget.event_generate("<>") # Case 2: CommandFrame class instance else: if action == "toggle" and hasattr(widget_data, "command_var"): widget_data.command_var.set(value == "on") widget_data.on_toggle() elif action == "set_value" and hasattr(widget_data, "cmd_var"): widget_data.cmd_var.set(value) if hasattr(widget_data, "on_select"): widget_data.on_select() elif hasattr(widget_data, "on_change"): widget_data.on_change() self.log_event(f"Executed: {ctrl_id} -> {value}") def edit_selected(self): selection = self.script_listbox.curselection() if not selection: return filename = self.script_listbox.get(selection[0]) path = os.path.join(self.scripts_dir, filename) JsonEditor(self.gui_window, path) def delete_selected(self): selection = self.script_listbox.curselection() if not selection: return filename = self.script_listbox.get(selection[0]) if messagebox.askyesno("Delete", f"Delete script {filename}?"): os.remove(os.path.join(self.scripts_dir, filename)) self.refresh_list() # Global accessor def get_script_manager() -> ScriptManager: return ScriptManager()