"""Native control-panel UI for a WatcheRobot Application.""" from __future__ import annotations import tkinter as tk from tkinter import scrolledtext, ttk # 机器人动作清单:动作ID -> 按钮显示名(与桌面端 desktop-expression-resources 一致) ACTIONS: list[tuple[str, str]] = [ ("happy", "开心"), ("smile", "微笑"), ("get", "打招呼"), ("sunglasses", "得意"), ("thinking", "思考"), ("listening", "聆听"), ("speaking", "说话"), ("focus", "专注"), ("sad", "难过"), ("shock", "惊讶"), ("fondle_love", "摸摸头"), ("fondle_anger", "生气"), ] # 灯光控制:颜色 -> 显示名(关灯用 lights.off) LIGHTS: list[tuple[str, str]] = [ ("#ff4d4f", "红"), ("#52c41a", "绿"), ("#4096ff", "蓝"), ("#ffd666", "黄"), ] FONT_MAIN = ("PingFang SC", 13) FONT_TITLE = ("PingFang SC", 18, "bold") FONT_SMALL = ("PingFang SC", 11) class ControlPanel: """Button control panel: robot actions, lights, stop, and a live log.""" def __init__( self, *, on_action=None, on_light=None, on_lights_off=None, on_stop=None, ) -> None: self._is_open = True self._on_action = on_action self._on_light = on_light self._on_lights_off = on_lights_off self._on_stop = on_stop self._root = tk.Tk() self._root.title("机器人控制台 - WatcheRobot") self._root.geometry("680x560") self._root.minsize(600, 480) self._root.protocol("WM_DELETE_WINDOW", self.close) container = ttk.Frame(self._root, padding=20) container.pack(fill="both", expand=True) # 标题与连接状态 header = ttk.Frame(container) header.pack(fill="x") ttk.Label(header, text="机器人控制台", font=FONT_TITLE).pack(side="left") self._status_var = tk.StringVar(value="正在连接机器人…") ttk.Label(header, textvariable=self._status_var, font=FONT_SMALL).pack( side="right", pady=(6, 0) ) ttk.Separator(container).pack(fill="x", pady=(12, 10)) # 动作按钮区 ttk.Label(container, text="表情 / 动作", font=FONT_MAIN).pack(anchor="w") grid = ttk.Frame(container) grid.pack(fill="x", pady=(6, 10)) for index, (action_id, label) in enumerate(ACTIONS): row, col = divmod(index, 4) button = ttk.Button( grid, text=f"{label}\n{action_id}", command=lambda aid=action_id, name=label: self._fire_action(aid, name), ) button.grid(row=row, column=col, sticky="nsew", padx=5, pady=5, ipady=6) for col in range(4): grid.columnconfigure(col, weight=1) # 灯光按钮区 ttk.Label(container, text="灯光", font=FONT_MAIN).pack(anchor="w") light_row = ttk.Frame(container) light_row.pack(fill="x", pady=(6, 10)) for color, label in LIGHTS: ttk.Button( light_row, text=label, command=lambda c=color, name=label: self._fire_light(c, name), ).pack(side="left", padx=5, ipady=4, expand=True, fill="x") ttk.Button( light_row, text="关灯", command=self._fire_lights_off ).pack(side="left", padx=5, ipady=4, expand=True, fill="x") # 停止按钮 ttk.Button( container, text="■ 立即停止所有动作", command=self._fire_stop ).pack(fill="x", ipady=6, pady=(4, 10)) # 日志区 ttk.Label(container, text="运行日志", font=FONT_MAIN).pack(anchor="w") self._log = scrolledtext.ScrolledText( container, height=8, font=("Menlo", 11), state="disabled" ) self._log.pack(fill="both", expand=True) # ---- 对外接口 ---- def set_status(self, text: str) -> None: self._status_var.set(text) def log(self, text: str) -> None: self._log.configure(state="normal") self._log.insert("end", text + "\n") self._log.see("end") self._log.configure(state="disabled") @property def is_open(self) -> bool: return self._is_open def pump(self) -> None: """Process one native UI event cycle without blocking asyncio.""" if not self._is_open: return try: self._root.update_idletasks() self._root.update() except tk.TclError: self._is_open = False def close(self) -> None: if not self._is_open: return self._is_open = False try: self._root.destroy() except tk.TclError: pass # ---- 按钮事件转发(回调由 app.py 注入) ---- def _fire_action(self, action_id: str, name: str) -> None: self.log(f"▶ 请求动作:{name}({action_id})") if self._on_action: self._on_action(action_id, name) def _fire_light(self, color: str, name: str) -> None: self.log(f"◆ 请求灯光:{name}({color})") if self._on_light: self._on_light(color, name) def _fire_lights_off(self) -> None: self.log("◇ 请求关灯") if self._on_lights_off: self._on_lights_off() def _fire_stop(self) -> None: self.log("■ 请求停止所有动作") if self._on_stop: self._on_stop()