| """Native control-panel UI for a WatcheRobot Application.""" |
|
|
| from __future__ import annotations |
|
|
| import tkinter as tk |
| from tkinter import scrolledtext, ttk |
|
|
| |
| ACTIONS: list[tuple[str, str]] = [ |
| ("happy", "开心"), |
| ("smile", "微笑"), |
| ("get", "打招呼"), |
| ("sunglasses", "得意"), |
| ("thinking", "思考"), |
| ("listening", "聆听"), |
| ("speaking", "说话"), |
| ("focus", "专注"), |
| ("sad", "难过"), |
| ("shock", "惊讶"), |
| ("fondle_love", "摸摸头"), |
| ("fondle_anger", "生气"), |
| ] |
|
|
| |
| 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 |
|
|
| |
|
|
| 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() |
|
|