134 lines
5.7 KiB
Python
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()
|