Files
m5stack-automation-tool/widgets/macro_library_picker.py
T
2026-07-17 15:29:53 -04:00

249 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()