Files
m5stack-automation-tool/widgets/sequence_editor_dialog.py
T
2026-07-17 15:29:53 -04:00

389 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Pop-out editor for the Get Variables Semi-Auto sequence.
A sequence is an ordered list of steps that the host renders to PowerShell at
upload time. Each step is one of:
wait {"kind": "wait", "ms": int}
send_keys {"kind": "send_keys", "keys": str}
run {"kind": "run", "command": str}
check {"kind": "check", "command": str,
"operator": "equals"|"contains"|"regex"|"numeric_gt"|"numeric_lt"|"exists",
"expected": str, # for `exists`, this is the path
"value": str} # value the variable is set to on match
The 1-based index of each ``check`` step becomes its toggle_count outcome,
so check steps are read top-to-bottom by the device's Scroll Lock decoder.
"""
import tkinter as tk
from tkinter import ttk, messagebox
_BG = "#2D2D3D"
_CARD_BG = "#252535"
_ENTRY_BG = "#1E1E2E"
_FG = "#FFFFFF"
_DIM_FG = "#AAAAAA"
_BTN_BG = "#3D3D5C"
_SAVE_BG = "#0077CC"
_DEL_FG = "#FF7777"
STEP_KINDS = [
("Wait", "wait"),
("Send Keys", "send_keys"),
("Run Command", "run"),
("Check + Signal", "check"),
]
CHECK_OPERATORS = [
("Output equals", "equals"),
("Output contains", "contains"),
("Output matches regex", "regex"),
("Output > number", "numeric_gt"),
("Output < number", "numeric_lt"),
("Path exists", "exists"),
]
def _kind_label(kind: str) -> str:
for label, k in STEP_KINDS:
if k == kind:
return label
return kind
def _op_label(op: str) -> str:
for label, k in CHECK_OPERATORS:
if k == op:
return label
return op
class SequenceEditorDialog(tk.Toplevel):
"""Modal dialog that edits a sequence in-place via on_save callback."""
def __init__(self, parent, sequence: list, on_save):
super().__init__(parent)
self.title("Get Variables — Sequence Editor")
self.geometry("720x600")
self.minsize(560, 420)
self.configure(bg=_BG)
self.transient(parent.winfo_toplevel())
self._on_save = on_save
# Per-step copy so Cancel discards edits (steps are flat dicts, no nested mutables)
self._steps = [dict(s) for s in (sequence or [])]
try:
import ctypes
self.update_idletasks()
hwnd = ctypes.windll.user32.GetParent(self.winfo_id())
ctypes.windll.dwmapi.DwmSetWindowAttribute(
hwnd, 20, ctypes.byref(ctypes.c_int(1)), ctypes.sizeof(ctypes.c_int))
except Exception:
pass
self._build_ui()
self._refresh()
self.protocol("WM_DELETE_WINDOW", self.destroy)
def _build_ui(self):
hdr = tk.Frame(self, bg=_BG)
hdr.pack(fill="x", padx=14, pady=(12, 6))
tk.Label(hdr, text="Sequence", bg=_BG, fg="#0AACFF",
font=("Segoe UI", 12, "bold")).pack(side="left")
tk.Label(hdr,
text="Each Check step's index (1, 2, 3 …) becomes its toggle "
"count. First match wins — no match = Fail output.",
bg=_BG, fg=_DIM_FG, font=("Segoe UI", 8),
justify="left", wraplength=460).pack(side="left", padx=(12, 0))
bar = tk.Frame(self, bg=_BG)
bar.pack(fill="x", padx=14, pady=(0, 6))
tk.Label(bar, text="Add step:", bg=_BG, fg=_FG,
font=("Segoe UI", 9, "bold")).pack(side="left")
for label, kind in STEP_KINDS:
tk.Button(bar, text=f"+ {label}", bg=_BTN_BG, fg=_FG,
relief="flat", font=("Segoe UI", 9), padx=8,
command=lambda k=kind: self._add_step(k)).pack(
side="left", padx=(6, 0))
outer = tk.Frame(self, bg=_BG)
outer.pack(fill="both", expand=True, padx=14, pady=4)
canvas = tk.Canvas(outer, bg=_BG, highlightthickness=0)
scrollbar = tk.Scrollbar(outer, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side="right", fill="y")
canvas.pack(side="left", fill="both", expand=True)
self._canvas = canvas
self._cards_frame = tk.Frame(canvas, bg=_BG)
self._cards_window = canvas.create_window((0, 0), window=self._cards_frame,
anchor="nw")
self._cards_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.bind(
"<Configure>",
lambda e: canvas.itemconfig(self._cards_window, width=e.width))
canvas.bind_all("<MouseWheel>", lambda e: canvas.yview_scroll(
int(-1 * (e.delta / 120)), "units"))
btns = tk.Frame(self, bg=_BG)
btns.pack(fill="x", padx=14, pady=(4, 12))
tk.Button(btns, text="Cancel", bg=_BTN_BG, fg=_FG, relief="flat",
padx=12, font=("Segoe UI", 9),
command=self.destroy).pack(side="right", padx=(6, 0))
tk.Button(btns, text="Save", bg=_SAVE_BG, fg=_FG, relief="flat",
padx=14, font=("Segoe UI", 9, "bold"),
command=self._save).pack(side="right")
def _refresh(self):
for w in self._cards_frame.winfo_children():
w.destroy()
check_idx = 0
for i, step in enumerate(self._steps):
if step.get("kind") == "check":
check_idx += 1
self._draw_card(i, step, check_idx if step.get("kind") == "check" else None)
if not self._steps:
tk.Label(self._cards_frame,
text="No steps yet — click an + Add button above to begin.",
bg=_BG, fg=_DIM_FG, font=("Segoe UI", 9, "italic")).pack(
pady=30)
def _draw_card(self, idx: int, step: dict, check_n):
card = tk.Frame(self._cards_frame, bg=_CARD_BG, bd=0)
card.pack(fill="x", pady=4, padx=2)
head = tk.Frame(card, bg=_CARD_BG)
head.pack(fill="x", padx=8, pady=(6, 2))
kind = step.get("kind", "")
title = f"{idx + 1}. {_kind_label(kind)}"
if check_n is not None:
title += f" [toggle = {check_n}]"
tk.Label(head, text=title, bg=_CARD_BG, fg="#0AACFF",
font=("Segoe UI", 10, "bold")).pack(side="left")
tk.Button(head, text="×", bg=_CARD_BG, fg=_DEL_FG, relief="flat",
font=("Segoe UI", 10, "bold"), width=2,
command=lambda i=idx: self._delete(i)).pack(side="right")
tk.Button(head, text="↓", bg=_CARD_BG, fg=_FG, relief="flat",
font=("Segoe UI", 10), width=2,
command=lambda i=idx: self._move(i, +1)).pack(side="right")
tk.Button(head, text="↑", bg=_CARD_BG, fg=_FG, relief="flat",
font=("Segoe UI", 10), width=2,
command=lambda i=idx: self._move(i, -1)).pack(side="right")
body = tk.Frame(card, bg=_CARD_BG)
body.pack(fill="x", padx=10, pady=(0, 8))
if kind == "wait":
self._draw_wait_fields(body, step)
elif kind == "send_keys":
self._draw_send_keys_fields(body, step)
elif kind == "run":
self._draw_run_fields(body, step)
elif kind == "check":
self._draw_check_fields(body, step)
else:
tk.Label(body, text=f"Unknown step kind: {kind}",
bg=_CARD_BG, fg=_DEL_FG,
font=("Segoe UI", 9, "italic")).pack(anchor="w")
def _draw_wait_fields(self, parent, step):
row = tk.Frame(parent, bg=_CARD_BG)
row.pack(fill="x")
tk.Label(row, text="Wait (ms):", bg=_CARD_BG, fg=_FG,
font=("Segoe UI", 9), width=12, anchor="w").pack(side="left")
v = tk.StringVar(value=str(step.get("ms", 500)))
e = tk.Entry(row, textvariable=v, bg=_ENTRY_BG, fg=_FG,
insertbackground=_FG, font=("Consolas", 10),
relief="flat", width=10)
e.pack(side="left", padx=4, ipady=2)
def save(*_):
try:
step["ms"] = max(0, int(v.get()))
except ValueError:
pass
v.trace_add("write", save)
def _draw_send_keys_fields(self, parent, step):
row = tk.Frame(parent, bg=_CARD_BG)
row.pack(fill="x")
tk.Label(row, text="Keys:", bg=_CARD_BG, fg=_FG,
font=("Segoe UI", 9), width=12, anchor="w").pack(side="left")
v = tk.StringVar(value=str(step.get("keys", "")))
tk.Entry(row, textvariable=v, bg=_ENTRY_BG, fg=_FG,
insertbackground=_FG, font=("Consolas", 10),
relief="flat").pack(side="left", fill="x", expand=True,
padx=4, ipady=2)
tk.Label(parent,
text="WScript.Shell SendKeys syntax — e.g. {ENTER}, ^c, +{TAB 3}",
bg=_CARD_BG, fg=_DIM_FG, font=("Segoe UI", 8)).pack(
anchor="w", pady=(2, 0))
def save(*_):
step["keys"] = v.get()
v.trace_add("write", save)
def _draw_run_fields(self, parent, step):
tk.Label(parent, text="PowerShell:", bg=_CARD_BG, fg=_FG,
font=("Segoe UI", 9)).pack(anchor="w")
text = tk.Text(parent, height=3, bg=_ENTRY_BG, fg=_FG,
insertbackground=_FG, font=("Consolas", 10),
relief="flat", wrap="none")
text.insert("1.0", step.get("command", ""))
text.pack(fill="x", pady=2)
def save(*_):
step["command"] = text.get("1.0", "end-1c")
text.bind("<KeyRelease>", save)
def _draw_check_fields(self, parent, step):
op_row = tk.Frame(parent, bg=_CARD_BG)
op_row.pack(fill="x")
tk.Label(op_row, text="Operator:", bg=_CARD_BG, fg=_FG,
font=("Segoe UI", 9), width=12, anchor="w").pack(side="left")
op_display = [d for d, _ in CHECK_OPERATORS]
op_value = [v for _, v in CHECK_OPERATORS]
cur = step.get("operator", "equals")
idx = op_value.index(cur) if cur in op_value else 0
op_var = tk.StringVar(value=op_display[idx])
op_box = ttk.Combobox(op_row, textvariable=op_var, values=op_display,
state="readonly")
op_box.pack(side="left", fill="x", expand=True, padx=4)
# Operator-specific fields; rebuilt on operator change
spec = tk.Frame(parent, bg=_CARD_BG)
spec.pack(fill="x", pady=(4, 0))
val_row = tk.Frame(parent, bg=_CARD_BG)
val_row.pack(fill="x", pady=(6, 0))
tk.Label(val_row, text="On match, set var to:",
bg=_CARD_BG, fg=_FG,
font=("Segoe UI", 9), width=20, anchor="w").pack(side="left")
val_var = tk.StringVar(value=str(step.get("value", "")))
tk.Entry(val_row, textvariable=val_var, bg=_ENTRY_BG, fg=_FG,
insertbackground=_FG, font=("Consolas", 10),
relief="flat").pack(side="left", fill="x", expand=True,
padx=4, ipady=2)
def save_value(*_):
step["value"] = val_var.get()
val_var.trace_add("write", save_value)
def render_spec():
for w in spec.winfo_children():
w.destroy()
current_op = step.get("operator", "equals")
if current_op == "exists":
tk.Label(spec, text="Path:", bg=_CARD_BG, fg=_FG,
font=("Segoe UI", 9), width=12,
anchor="w").pack(side="left")
pv = tk.StringVar(value=step.get("expected", ""))
tk.Entry(spec, textvariable=pv, bg=_ENTRY_BG, fg=_FG,
insertbackground=_FG, font=("Consolas", 10),
relief="flat").pack(side="left", fill="x",
expand=True, padx=4, ipady=2)
def save_path(*_):
step["expected"] = pv.get()
pv.trace_add("write", save_path)
else:
tk.Label(spec, text="Command:", bg=_CARD_BG, fg=_FG,
font=("Segoe UI", 9)).pack(anchor="w")
cmd_text = tk.Text(spec, height=2, bg=_ENTRY_BG, fg=_FG,
insertbackground=_FG, font=("Consolas", 10),
relief="flat", wrap="none")
cmd_text.insert("1.0", step.get("command", ""))
cmd_text.pack(fill="x", pady=2)
def save_cmd(*_):
step["command"] = cmd_text.get("1.0", "end-1c")
cmd_text.bind("<KeyRelease>", save_cmd)
exp_row = tk.Frame(spec, bg=_CARD_BG)
exp_row.pack(fill="x", pady=(2, 0))
exp_label = "Number:" if current_op in (
"numeric_gt", "numeric_lt") else (
"Pattern:" if current_op == "regex" else "Expected:")
tk.Label(exp_row, text=exp_label, bg=_CARD_BG, fg=_FG,
font=("Segoe UI", 9), width=12,
anchor="w").pack(side="left")
ev = tk.StringVar(value=step.get("expected", ""))
tk.Entry(exp_row, textvariable=ev, bg=_ENTRY_BG, fg=_FG,
insertbackground=_FG, font=("Consolas", 10),
relief="flat").pack(side="left", fill="x",
expand=True, padx=4, ipady=2)
def save_exp(*_):
step["expected"] = ev.get()
ev.trace_add("write", save_exp)
def on_op_change(*_):
disp = op_var.get()
i = op_display.index(disp) if disp in op_display else 0
new_op = op_value[i]
if new_op != step.get("operator"):
step["operator"] = new_op
render_spec()
op_var.trace_add("write", on_op_change)
render_spec()
def _add_step(self, kind: str):
step = {"kind": kind}
if kind == "wait":
step["ms"] = 500
elif kind == "send_keys":
step["keys"] = ""
elif kind == "run":
step["command"] = ""
elif kind == "check":
step.update({
"operator": "equals",
"command": "",
"expected": "",
"value": "",
})
self._steps.append(step)
self._refresh()
def _delete(self, i: int):
if 0 <= i < len(self._steps):
self._steps.pop(i)
self._refresh()
def _move(self, i: int, delta: int):
j = i + delta
if 0 <= j < len(self._steps):
self._steps[i], self._steps[j] = self._steps[j], self._steps[i]
self._refresh()
def _save(self):
# An empty value would silently set the variable to "", which is rarely intended
check_count = 0
for i, step in enumerate(self._steps):
if step.get("kind") == "check":
check_count += 1
if not (step.get("value") or "").strip():
if not messagebox.askyesno(
"Empty value",
f"Check step {check_count} has no value to set the "
f"variable to. Save anyway? (Variable will be set "
f"to an empty string on match.)",
parent=self,
):
return
break
try:
self._on_save(self._steps)
except Exception as e:
messagebox.showerror("Sequence Editor", str(e), parent=self)
return
self.destroy()