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