"""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("", lambda _e: self._save()) self.dlg.bind("", 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()