Initial public release
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user