326 lines
12 KiB
Python
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() |