Files
2026-07-17 15:29:53 -04:00

134 lines
5.7 KiB
Python

"""Per-event editor dialog for a macro recording.
Opened by double-clicking a row in the live recorder dialog. Lets the user
tweak the timestamp, action (press/release), and HID code of an existing
event, or seed a new event via "Insert before / Insert after".
The list of selectable HID codes is derived from utils.constants.TKKEYSYM_TO_HID,
which is the same map the live recorder uses on capture — so anything you
record live can be re-selected here.
"""
import tkinter as tk
from tkinter import ttk
from utils.constants import TKKEYSYM_TO_HID, hid_code_label
ACTION_DOWN = 0
ACTION_UP = 1
def _key_choices() -> list[tuple[int, str]]:
"""Return a sorted (hid_code, label) list, deduped by code.
`hid_code_label` already produces friendly names for everything in
the macro recorder's vocabulary, so we drive the list off that
rather than the raw Tk keysym map (which has shift-pair duplicates).
"""
codes = sorted({code for code in TKKEYSYM_TO_HID.values()})
return [(code, f"{hid_code_label(code)} (0x{code:02X})") for code in codes]
class MacroEventEditorDialog:
"""Modal editor for a single macro event."""
def __init__(self, parent, event, on_commit, title="Edit event"):
"""event is a 3-element list [t_ms, action, hid_code].
on_commit(new_event) is invoked with a freshly-built [t, a, c]
list when the user clicks Save. Cancel makes no callback.
"""
self._event = event
self._on_commit = on_commit
self._build(parent, title)
def _build(self, parent, title):
self.dlg = tk.Toplevel(parent)
self.dlg.title(title)
self.dlg.configure(bg="#2D2D3D")
self.dlg.transient(parent.winfo_toplevel())
self.dlg.grab_set()
self.dlg.resizable(False, False)
body = tk.Frame(self.dlg, bg="#2D2D3D")
body.pack(padx=14, pady=12, fill="both", expand=True)
tk.Label(body, text="Time (ms)", bg="#2D2D3D", fg="white",
font=("Segoe UI", 9)).grid(row=0, column=0, sticky="w",
pady=(0, 4))
self.t_var = tk.IntVar(value=int(self._event[0]))
tk.Spinbox(body, from_=0, to=24 * 60 * 60 * 1000, increment=1,
textvariable=self.t_var, width=12,
bg="#1E1E2E", fg="white", insertbackground="white",
relief="flat", font=("Segoe UI", 10)).grid(
row=0, column=1, sticky="w", padx=(8, 0), pady=(0, 4))
tk.Label(body, text="Action", bg="#2D2D3D", fg="white",
font=("Segoe UI", 9)).grid(row=1, column=0, sticky="w",
pady=(4, 4))
self.action_var = tk.IntVar(value=int(self._event[1]))
action_frame = tk.Frame(body, bg="#2D2D3D")
action_frame.grid(row=1, column=1, sticky="w", padx=(8, 0))
tk.Radiobutton(action_frame, text="↓ Press", variable=self.action_var,
value=ACTION_DOWN, bg="#2D2D3D", fg="white",
selectcolor="#1E1E2E", activebackground="#2D2D3D",
activeforeground="white", font=("Segoe UI", 9)).pack(
side="left")
tk.Radiobutton(action_frame, text="↑ Release", variable=self.action_var,
value=ACTION_UP, bg="#2D2D3D", fg="white",
selectcolor="#1E1E2E", activebackground="#2D2D3D",
activeforeground="white", font=("Segoe UI", 9)).pack(
side="left", padx=(10, 0))
tk.Label(body, text="Key", bg="#2D2D3D", fg="white",
font=("Segoe UI", 9)).grid(row=2, column=0, sticky="w",
pady=(4, 4))
self._choices = _key_choices()
choice_labels = [label for (_c, label) in self._choices]
self.key_label_var = tk.StringVar()
# Pre-select the current event's code, or fall back to the closest.
current_code = int(self._event[2])
try:
idx = next(i for i, (c, _l) in enumerate(self._choices)
if c == current_code)
except StopIteration:
idx = 0
self.key_label_var.set(choice_labels[idx])
combo = ttk.Combobox(body, textvariable=self.key_label_var,
values=choice_labels, state="readonly",
width=24, font=("Consolas", 9))
combo.grid(row=2, column=1, sticky="w", padx=(8, 0))
footer = tk.Frame(self.dlg, bg="#2D2D3D")
footer.pack(fill="x", side="bottom", padx=14, pady=(0, 12))
tk.Button(footer, text="Cancel", bg="#4A4A6A", fg="white",
font=("Segoe UI", 9), relief="flat", padx=14,
command=self._cancel).pack(side="right")
tk.Button(footer, text="Save", bg="#22C55E", fg="white",
activebackground="#16A34A",
font=("Segoe UI", 9, "bold"), relief="flat", padx=14,
command=self._save).pack(side="right", padx=(0, 6))
self.dlg.protocol("WM_DELETE_WINDOW", self._cancel)
self.dlg.bind("<Return>", lambda _e: self._save())
self.dlg.bind("<Escape>", lambda _e: self._cancel())
def _save(self):
try:
t_ms = max(0, int(self.t_var.get()))
except (tk.TclError, ValueError):
t_ms = 0
action = int(self.action_var.get())
# Recover the code from the chosen label.
try:
idx = next(i for i, (_c, l) in enumerate(self._choices)
if l == self.key_label_var.get())
code = self._choices[idx][0]
except StopIteration:
code = int(self._event[2])
self._on_commit([t_ms, action, code])
self.dlg.destroy()
def _cancel(self):
self.dlg.destroy()