SXXXXXXX_PyMsc/pymsc/gui/script_manager.py

326 lines
12 KiB
Python

# -*- 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("<<ComboboxSelected>>")
# 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()