Bobss's picture
Publish hello_watcher 0.2.0
9f5687a verified
Raw
History Blame Contribute Delete
5.51 kB
"""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()