"""Modal browser for the BT-Keyboard macro library (bt_macros). Two modes: * manage (default): used from the BT Keyboard window. Bottom row exposes Quick Run / Load / Delete and a Loop checkbox. * select : used from the node editor's macro node to pick a macro to embed. Bottom row exposes Select / Cancel. Folder management (Create / Rename / Delete) is available in both modes, visually separated from the macro actions. Macros are NOT renameable. Callbacks (all optional): on_quick_run(folder, name, events, loop) — Quick Run clicked (stays open) on_load(folder, name, events, loop) — Load clicked (closes) on_select(folder, name, events) — Select clicked (closes) """ from __future__ import annotations import tkinter as tk from tkinter import simpledialog, messagebox import bt_macros _BG = "#2D2D3D" _PANEL = "#1E1E2E" _SEL = "#3B82F6" class MacroLibraryPicker(tk.Toplevel): def __init__(self, parent, *, select_mode: bool = False, on_quick_run=None, on_load=None, on_select=None): super().__init__(parent) self._select_mode = select_mode self._on_quick_run = on_quick_run self._on_load = on_load self._on_select = on_select self._cur_folder = None self.title("Macro library") self.configure(bg=_BG) self.transient(parent) self.grab_set() self.geometry("620x440") self.loop_var = tk.BooleanVar(value=False) self._build() self._refresh_folders() # ---- layout ---- def _build(self): tk.Label(self, text="Macro library", bg=_BG, fg="white", font=("Segoe UI", 12, "bold")).pack(anchor="w", padx=14, pady=(12, 2)) # Folder management toolbar — its own clearly separated group. fbar = tk.Frame(self, bg=_BG) fbar.pack(fill="x", padx=14, pady=(2, 6)) tk.Label(fbar, text="Folders:", bg=_BG, fg="#9CA3AF", font=("Segoe UI", 9, "bold")).pack(side="left", padx=(0, 8)) tk.Button(fbar, text="+ New", bg="#0F766E", fg="white", activebackground="#0D5C56", font=("Segoe UI", 9), relief="flat", padx=10, command=self._create_folder).pack( side="left") tk.Button(fbar, text="Rename", bg="#4A4A6A", fg="white", font=("Segoe UI", 9), relief="flat", padx=10, command=self._rename_folder).pack(side="left", padx=(6, 0)) tk.Button(fbar, text="Delete", bg="#7F1D1D", fg="white", font=("Segoe UI", 9), relief="flat", padx=10, command=self._delete_folder).pack(side="left", padx=(6, 0)) # Two side-by-side lists: folders | macros. lists = tk.Frame(self, bg=_BG) lists.pack(fill="both", expand=True, padx=14, pady=(0, 8)) lists.columnconfigure(0, weight=1, uniform="lb") lists.columnconfigure(1, weight=2, uniform="lb") lists.rowconfigure(1, weight=1) tk.Label(lists, text="Folder", bg=_BG, fg="#9CA3AF", font=("Segoe UI", 9)).grid(row=0, column=0, sticky="w") tk.Label(lists, text="Macro", bg=_BG, fg="#9CA3AF", font=("Segoe UI", 9)).grid(row=0, column=1, sticky="w") self.folder_lb = tk.Listbox(lists, bg=_PANEL, fg="white", selectbackground=_SEL, font=("Consolas", 10), activestyle="none", relief="flat", exportselection=False) self.folder_lb.grid(row=1, column=0, sticky="nsew", padx=(0, 6)) self.folder_lb.bind("<>", self._on_folder_select) self.macro_lb = tk.Listbox(lists, bg=_PANEL, fg="white", selectbackground=_SEL, font=("Consolas", 10), activestyle="none", relief="flat", exportselection=False) self.macro_lb.grid(row=1, column=1, sticky="nsew") # Bottom action row. actions = tk.Frame(self, bg=_BG) actions.pack(fill="x", padx=14, pady=(0, 12)) tk.Button(actions, text="Close" if not self._select_mode else "Cancel", bg="#4A4A6A", fg="white", font=("Segoe UI", 9), relief="flat", padx=14, command=self.destroy).pack( side="right") if self._select_mode: tk.Button(actions, text="Select", bg="#22C55E", fg="white", font=("Segoe UI", 9, "bold"), relief="flat", padx=14, command=self._do_select).pack(side="right", padx=(0, 6)) else: tk.Checkbutton(actions, text="Loop", variable=self.loop_var, bg=_BG, fg="white", selectcolor=_PANEL, activebackground=_BG, activeforeground="white", font=("Segoe UI", 9)).pack(side="right", padx=(0, 12)) tk.Button(actions, text="Delete", bg="#7F1D1D", fg="white", font=("Segoe UI", 9), relief="flat", padx=12, command=self._do_delete).pack(side="left") tk.Button(actions, text="↻ Quick Run", bg="#7C3AED", fg="white", activebackground="#6D28D9", font=("Segoe UI", 9, "bold"), relief="flat", padx=14, command=self._do_quick_run).pack( side="right", padx=(0, 6)) tk.Button(actions, text="Load", bg="#1D4ED8", fg="white", activebackground="#1E40AF", font=("Segoe UI", 9, "bold"), relief="flat", padx=14, command=self._do_load).pack( side="right", padx=(0, 6)) # ---- folder / macro listing ---- def _refresh_folders(self, select: str | None = None): folders = bt_macros.list_folders() self.folder_lb.delete(0, "end") for f in folders: self.folder_lb.insert("end", f) target = select if (select in folders) else ( self._cur_folder if self._cur_folder in folders else (folders[0] if folders else None)) if target is not None: idx = folders.index(target) self.folder_lb.selection_set(idx) self.folder_lb.see(idx) self._cur_folder = target self._refresh_macros() def _selected_folder(self): sel = self.folder_lb.curselection() if sel: return self.folder_lb.get(sel[0]) return self._cur_folder def _refresh_macros(self): self.macro_lb.delete(0, "end") folder = self._selected_folder() self._cur_folder = folder if not folder: return for name in bt_macros.list_macros(folder): m = bt_macros.load_macro(folder, name) or {} n = len(m.get("events", [])) dur = (m.get("duration_ms", 0) or 0) / 1000.0 self.macro_lb.insert("end", f"{name} ({n} ev, {dur:.1f}s)") def _on_folder_select(self, _e=None): self._refresh_macros() def _selected_macro(self): sel = self.macro_lb.curselection() folder = self._selected_folder() if not sel or not folder: return None, None names = bt_macros.list_macros(folder) idx = sel[0] if idx >= len(names): return folder, None return folder, names[idx] def _selected_events(self): folder, name = self._selected_macro() if not name: messagebox.showinfo("No macro selected", "Select a macro first.", parent=self) return None m = bt_macros.load_macro(folder, name) if not m: return None return folder, name, m["events"] # ---- folder ops ---- def _create_folder(self): name = simpledialog.askstring("New folder", "Folder name:", parent=self) if name and bt_macros.create_folder(name): self._refresh_folders(select=name.strip()) def _rename_folder(self): folder = self._selected_folder() if not folder: return new = simpledialog.askstring("Rename folder", f"New name for '{folder}':", parent=self) if new and bt_macros.rename_folder(folder, new): self._refresh_folders(select=new.strip()) def _delete_folder(self): folder = self._selected_folder() if not folder: return if not messagebox.askyesno( "Delete folder?", f"Delete folder '{folder}' and ALL macros inside it?", parent=self): return bt_macros.delete_folder(folder) self._cur_folder = None self._refresh_folders() # ---- macro actions ---- def _do_quick_run(self): res = self._selected_events() if res and self._on_quick_run: folder, name, events = res self._on_quick_run(folder, name, events, self.loop_var.get()) def _do_load(self): res = self._selected_events() if res and self._on_load: folder, name, events = res self._on_load(folder, name, events, self.loop_var.get()) self.destroy() def _do_select(self): res = self._selected_events() if res and self._on_select: folder, name, events = res self._on_select(folder, name, events) self.destroy() def _do_delete(self): folder, name = self._selected_macro() if not name: return if not messagebox.askyesno("Delete macro?", f"Delete macro '{name}'?", parent=self): return bt_macros.delete_macro(folder, name) self._refresh_macros()