249 lines
9.8 KiB
Python
249 lines
9.8 KiB
Python
"""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("<<ListboxSelect>>", 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()
|