Files
ykxmz/_build/add_watch_quickbar.py
xmz7891 f0c05079f7 2.1.7:手表快捷栏 + 鼠标触摸板 + 弹窗可配 + 后台远程更新
用户这次提了一整套升级,这是最后一批。

== 一、手表端顶部快捷栏 ==

用户原话:"手表端不管干什么,上面都有一栏,栏里的按钮可以在设置页
自定义,最多可以加 4 个,其中有一个按钮不能改,功能就是一键返回桌面"。

("返回桌面"经确认 = **让被控电脑显示桌面**,即 Win+D。)

  * 一条常驻的栏,贴在屏幕顶部,切页面时不动
  * **4 个格子**:3 个可自定义 + **1 个固定的「显示桌面」(改不了)**
  * 7 个可选动作:传文件 / 屏幕镜像 / 鼠标控制 / 音效 / 关机 /
    语音播报 / 投图到电脑
  * 设置页里点一下换;也能把某一格留空
  * 没连上电脑时按钮置灰(不是藏起来 —— "一直在但暂时不能用"
    比"时有时无"更好理解)

实现上的几个要点:

  * 快捷栏 addView 在 5 个面板**之后** —— FrameLayout 里后加的在上层,
    加在前面会被面板(MATCH_PARENT)整个盖住
  * 面板加了 topMargin 给栏让位,集中在 applyPanelTopMargin 里改
    (每个面板各写一遍很容易漏一个,表现是"就某一页顶部被切掉")
  * 第 4 格在代码里**写死** desktop,而且 QUICK_ACTIONS 里**没有**它 ——
    这样它既不会出现在可选列表里,也不可能被改成别的
  * 存储用 SharedPreferences。读取时**过滤不认识的 id + 补齐到 3 个**:
    以后删了某个动作,老配置也不会让栏变空或下标越界

== 二、远程鼠标控制(手表上的触摸板)==

  * 拖动 = 移动光标,单击 = 左键,双击 = 右键
  * 下面还有专门的 左键 / 右键 / 滚轮上 / 滚轮下 按钮
  * 电脑端用 SendInput 注入(见下)

== 三、弹文字窗口的位置和大小可配 ==

用户原话:"弹文字窗口 …… 位置和大小都能自定义"。

  * 位置:右下(默认,保持老习惯)/ 右上 / 左上 / 左下 /
    屏幕正中间 / 自定义坐标
  * 字号:8~32(夹住,免得设成 200 一个通知占满屏幕)
  * 宽度:可指定,0 = 按内容自适应
  * 三处窗口定位统一到一个 _notice_geometry() —— 以前三处各写死一遍
    右下角,改一次要改三个地方,漏一个用户就看得见

== 四、屏幕被观看时的提示条改成默认不显示 ==

用户原话:"以后不需要弹,啥窗户都没有,就可以像 UU 远程一样"。

  * 默认**连窗口都不建**
  * **但保留成开关**,设置里可以打开

  关于这条:那条提示原来是**故意加**的(有人在看屏幕时,电脑前面的人
  应该知道)。用户要求去掉,这是他自己的电脑、自己的教室,
  属于正当的远程管理需求。所以能力保留、默认关闭 ——
  这软件应该始终有一个"让本机用户看得见正在被控制"的选项可查。

== 五、中转后台能看到版本并远程点更新 ==

  * 设备表新增「版本」列
  * 每行一个「更新」按钮:点一下让那台电脑自己静默升级
  * 离线设备的按钮**置灰并说明原因**(命令只会挂在队列里等它上线,
    那时用户早忘了自己点过,突然重启很莫名其妙)
  * 和手表那边走**同一条命令**(cmd=update, action=start),
    不给"后台触发"单独写一套逻辑 —— 少一套就少一处不一致

== 六、手表端可开关「允许新设备连接」 ==

  * 关掉后只有配对过的电脑能连,教室里防别人乱连
  * 电脑端新增 _do_allow_new_pairing 这个 feature
  * 只影响**配对新设备**,不踢现有会话
    (否则老师一点"禁止",自己正连着的那台也被踢掉,成了"自杀开关")

== 修掉的几个自己造的坑 ==

1. **管理页 JS 裸换行** —— 最严重的一个。
   ADMIN_JS 是 Python 三引号字符串,我在里面写 `\n` 拼多行文案,
   结果是**真的换行**插进 JS,字符串字面量跨行 -> 语法非法 ->
   **整个 script 块解析失败 -> 管理页所有按钮全死**(改码/删除/踢下线/全选)。
   改成 String.fromCharCode(10),不依赖任何转义层。

2. **路由落进了 do_GET** —— 我拿 "没有这个管理接口" 当锚点,
   但那个字符串在文件里有**两处**(do_GET 和 _route_post 各一个),
   于是路由加到了 GET 块里,管理页的 POST 打不到它。
   表现是"点了更新没反应"。

3. **isConnected() 根本不存在** —— 写快捷栏时凭直觉调了个方法名,
   编译才发现。顺手包了一层并加上说明。

4. **两个不存在的方法名** —— onCreate 里写了
   buildMousePanelIfNeeded / mousePanelHolder,编译报 cannot find symbol。

5. **未调用方法扫描的假阳性** —— GestureDetector 的
   onSingleTapConfirmed / onDoubleTap / onScroll 是框架回调,
   扫描把"名字只出现 1 次"当成死代码。改成**跳过 @Override 的方法**。
   这种假阳性最坏的结果是有人为了让它变绿把回调删了,手势就失灵了。

6. **测试断言太严导致误报** —— "不能有写死的 240",
   但 helper 里 `auto = max(240, ...)` 是自适应的兜底,故意的。

7. **屏幕共享提示那个测试断言的是旧行为** —— 没有简单删掉,
   改成验两个方向:开关打开时提示条正常(位置/尺寸/出现/消失),
   关闭时默认值必须是 False 且代码路径上不建窗口。

== 验证 ==

    全量回归(19 关)全部 PASS,零失败
    新增关卡:3a10 静默更新 / 3a11 鼠标控制 / 3a12 后台更新+弹窗 /
              3a13 手表快捷栏
    Java 编译通过(android-34, --release 8)
    APK / 安装包 / 服务端包 全部重建,Win7 兼容校验通过
    623 个 Python 文件语法通过
2026-10-09 22:55:28 +08:00

557 lines
20 KiB
Python
Raw Permalink 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.
# -*- coding: utf-8 -*-
r'''手表端:常驻顶部快捷栏。
== 用户的要求(原话)==
"手表端不管干什么,上面都有一栏,栏里的按钮可以在设置页自定义,
最多可以加 4 个,其中有一个按钮不能改,功能就是一键返回桌面"
("返回桌面"经确认 = **让被控电脑显示桌面**,即 Win+D。)
== 做什么 ==
1. **一条常驻的快捷栏**:贴在屏幕顶部,任何页面都在
(控制页 / 镜像页 / 设置页 / 局域网页面板切换时不动)
2. **最多 4 个按钮**:
* **1 个固定**:显示桌面(Win+D)—— 不给改,这是用户明确要求的
* **最多 3 个自定义**:从 7 个动作里挑
3. **7 个可选动作**(都有现成调用,见下面的 QUICK_ACTIONS):
file 传文件到电脑
mirror 屏幕镜像
mouse 鼠标控制(本补丁配套的触摸板)
sfx 音效
power 关机 / 锁屏
tts 语音播报
image 投图到电脑
4. **设置页里能改**:勾选哪 3 个动作。
== 为什么贴在 FrameLayout 最上层 ==
现有架构是:一个 FrameLayout `root`,5 个面板都 MATCH_PARENT 叠在里面,
靠 `showOnly()` 切可见性。
快捷栏也加到 `root`,**在 5 个面板之后 addView** —— 这样它在最上层:
* 切页面时它不受影响(不用每个页面各建一份)
* 面板填满整个屏幕也不会盖住它
代价:面板内容会被栏盖住一截,所以给每个面板加了 topMargin
(见 PATCH_PANEL_MARGIN)。
== 为什么栏在没连上时也显示 ==
用户说"不管干什么上面都有一栏",那就**一直在**。
没连上的时候按钮置灰(不是隐藏)—— "一直在但暂时不能用"
比"时有时无"更好理解,也不会让用户以为功能没了。
== 存储 ==
用 SharedPreferences(和 App 其他设置一致),键 `quick_slots`,
存逗号分隔的动作 id。默认给 file,mirror,sfx。
读取时会**过滤掉不认识的 id**(比如以后删了某个动作),
不够 3 个就按默认顺序补齐 —— 这样老配置永远不会让栏变空。
'''
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
MAIN = ROOT / "android" / "src" / "com" / "lanremote" / "watch" / "MainActivity.java"
# ---------------------------------------------------------------- 字段
FIELDS_ANCHOR = " private Button updateButton;"
FIELDS = ''' private Button updateButton;
// ---------------- 顶部快捷栏 ----------------
//
// 用户要求:"不管干什么,上面都有一栏,栏里的按钮可以在设置页
// 自定义,最多可以加 4 个,其中有一个按钮不能改,
// 功能就是一键返回桌面"。
//
// 所以这里是 1 个固定(显示桌面)+ 最多 3 个自定义。
private LinearLayout quickBar;
private final java.util.List<Button> quickButtons =
new java.util.ArrayList<Button>();
/** 设置页里那 3 个位置各放什么动作。null 表示这个位置空着。 */
private String[] quickSlots = new String[]{"file", "mirror", "sfx"};
/** 快捷栏上能放的动作。id 会存进设置,**改名会让老配置失效**,
* 所以这几个英文 id 不要动,界面上显示的中文随便改。 */
private static final String[][] QUICK_ACTIONS = {
{"file", "传文件"},
{"mirror", "屏幕镜像"},
{"mouse", "鼠标控制"},
{"sfx", "音效"},
{"power", "关机"},
{"tts", "语音播报"},
{"image", "投图到电脑"},
};
private static final String QUICK_PREF = "quick_slots";
private static final int QUICK_CUSTOM_SLOTS = 3; // 固定那个不算在内
// ---------------- 鼠标控制(触摸板)----------------
private FrameLayout mousePanel;
private View mousePad;
private boolean mouseDragging = false;
private float mouseLastX = 0f;
private float mouseLastY = 0f;'''
# ---------------------------------------------------------------- 建栏方法
BUILD_ANCHOR = " private void showOnly(View keep) {"
BUILD_METHOD = ''' // ==================================================================
// 顶部快捷栏
// ==================================================================
/**
* 建快捷栏。挂在 root 最上层,任何页面都在。
*
* <p>布局:[动作1][动作2][动作3][显示桌面]
* 前三个按设置里的选择走,最后一个**永远**是显示桌面(不给改)。
*/
private void buildQuickBar() {
quickBar = new LinearLayout(this);
quickBar.setOrientation(LinearLayout.HORIZONTAL);
quickBar.setBackgroundColor(0xE6185CE8); // 半透明蓝,压在任何页面上都看得清
quickBar.setPadding(dp(3), dp(3), dp(3), dp(3));
quickBar.setGravity(android.view.Gravity.CENTER_VERTICAL);
quickButtons.clear();
// 4 个按钮:3 个自定义 + 1 个固定
for (int i = 0; i < QUICK_CUSTOM_SLOTS + 1; i++) {
final int slot = i;
Button b = new Button(this);
b.setTextSize(11f);
b.setAllCaps(false);
b.setTextColor(0xFFFFFFFF);
b.setBackgroundColor(0x33FFFFFF);
b.setPadding(dp(2), 0, dp(2), 0);
b.setMinWidth(0);
b.setMinimumWidth(0);
b.setMinHeight(0);
b.setMinimumHeight(0);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
0, ViewGroup.LayoutParams.MATCH_PARENT, 1f);
lp.setMargins(dp(2), 0, dp(2), 0);
b.setLayoutParams(lp);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onQuickClick(slot);
}
});
quickButtons.add(b);
quickBar.addView(b);
}
// 高度固定,免得按钮里的字多一个字整条栏就变高、页面跟着抖
FrameLayout.LayoutParams barLp = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(30));
barLp.gravity = android.view.Gravity.TOP;
quickBar.setLayoutParams(barLp);
refreshQuickBar();
}
/**
* 按设置刷新快捷栏上的文字和可用状态。
*
* <p>连不上的时候置灰而不是隐藏 —— 用户说"不管干什么上面都有一栏",
* 时有时无会让人以为功能没了。
*/
private void refreshQuickBar() {
if (quickBar == null || quickButtons.isEmpty()) {
return;
}
boolean connected = isConnected();
for (int i = 0; i < quickButtons.size(); i++) {
Button b = quickButtons.get(i);
String id = (i < QUICK_CUSTOM_SLOTS) ? quickSlots[i] : "desktop";
if (id == null || id.isEmpty()) {
b.setText("-"); // 这个位置空着
b.setEnabled(false);
b.setAlpha(0.45f);
continue;
}
b.setText(quickLabel(id));
b.setEnabled(connected);
b.setAlpha(connected ? 1f : 0.5f);
}
}
private static String quickLabel(String id) {
if ("desktop".equals(id)) {
return "显示桌面"; // 固定那个
}
for (String[] pair : QUICK_ACTIONS) {
if (pair[0].equals(id)) {
return pair[1];
}
}
return "-";
}
/** 点第 slot 个按钮。0..2 是自定义,3 是那个固定的显示桌面。 */
private void onQuickClick(int slot) {
String id = (slot < QUICK_CUSTOM_SLOTS) ? quickSlots[slot] : "desktop";
if (id == null || id.isEmpty()) {
return;
}
runQuickAction(id);
}
/** 执行一个快捷动作。设置页里选的也是这几个 id。 */
private void runQuickAction(String id) {
try {
if ("desktop".equals(id)) {
// 用户确认过:"一键返回桌面" = 让**被控电脑**显示桌面(Win+D)
sendCommand(Json.obj("cmd", "show_desktop"), "显示桌面");
return;
}
if ("file".equals(id)) {
pickFile(REQ_FILE, "*/*");
return;
}
if ("mirror".equals(id)) {
openMirrorPanel();
return;
}
if ("mouse".equals(id)) {
showMousePanel();
return;
}
if ("sfx".equals(id)) {
pickSfx();
return;
}
if ("power".equals(id)) {
showPowerChooser();
return;
}
if ("tts".equals(id)) {
promptSay();
return;
}
if ("image".equals(id)) {
pickFile(REQ_IMAGE, "image/*");
return;
}
toast("不认识的动作:" + id);
} catch (Throwable error) {
toast("执行失败:" + shortReason(error));
}
}
/** 关机这一格点开后让用户选锁屏还是关机(不直接关,太危险)。 */
private void showPowerChooser() {
final java.util.List<String> items = new java.util.ArrayList<String>();
items.add("锁屏");
items.add("关机");
items.add("重启");
showChooser("电源", items, new Chooser() {
@Override
public void onPick(int index) {
if (index == 0) {
confirmPower("lock", "锁屏");
} else if (index == 1) {
confirmPower("shutdown", "关机");
} else {
confirmPower("restart", "重启");
}
}
});
}
// ---------------- 快捷栏的存取 ----------------
private void loadQuickSlots() {
String raw = prefs().getString(QUICK_PREF, "");
String[] want = (raw == null || raw.isEmpty())
? new String[]{"file", "mirror", "sfx"}
: raw.split(",", -1);
// ⚠ 一定要过滤 + 补齐:
// 过滤 —— 万一以后删了某个动作,老配置里的 id 就不认识了,
// 不过滤的话那格会显示空白,用户以为坏了
// 补齐 —— 保证长度正好 3,少了下标会越界
String[] out = new String[QUICK_CUSTOM_SLOTS];
int n = 0;
for (String id : want) {
if (n >= QUICK_CUSTOM_SLOTS) {
break;
}
if (id != null && !id.isEmpty() && !"desktop".equals(id)
&& isKnownQuickAction(id)) {
out[n++] = id;
}
}
String[] fallback = {"file", "mirror", "sfx"};
for (int i = 0; i < QUICK_CUSTOM_SLOTS; i++) {
if (out[i] == null) {
// 补一个没被用过的默认动作
for (String f : fallback) {
boolean used = false;
for (String o : out) {
if (f.equals(o)) {
used = true;
break;
}
}
if (!used) {
out[i] = f;
break;
}
}
}
}
quickSlots = out;
}
private static boolean isKnownQuickAction(String id) {
for (String[] pair : QUICK_ACTIONS) {
if (pair[0].equals(id)) {
return true;
}
}
return false;
}
private void saveQuickSlots() {
StringBuilder sb = new StringBuilder();
for (String id : quickSlots) {
if (sb.length() > 0) {
sb.append(',');
}
sb.append(id == null ? "" : id);
}
prefs().edit().putString(QUICK_PREF, sb.toString()).apply();
refreshQuickBar();
}
private android.content.SharedPreferences prefs() {
return getSharedPreferences("ykxmz", MODE_PRIVATE);
}
'''
# ---------------------------------------------------------------- 鼠标触摸板
MOUSE_METHOD = ''' // ==================================================================
// 鼠标控制(触摸板)
// ==================================================================
private void showMousePanel() {
if (mousePanel == null) {
buildMousePanel();
}
showOnly(mousePanel);
appendLog("鼠标控制:拖动=移动光标,单击=左键,双击=右键");
toast("拖动移动光标 · 单击左键 · 双击右键");
}
/**
* 触摸板。整块区域接收触摸,按比例映射到电脑屏幕的像素坐标。
*
* <p>为什么用"按比例映射"而不是"相对位移":
* 手表表盘和电脑屏幕尺寸差得远,相对位移要让手表自己猜倍率,
* 猜错了光标就飘;绝对映射由电脑端负责落到那个像素,
* 责任清晰,也不会累积漂移。
*
* <p>但表盘太小,绝对映射的精度只有几十像素 —— 所以
* **双击左右半屏**这类大幅移动之后,用拖动做细调更实际。
* 这里两个都支持:拖动 = 相对微调,点击 = 在当前位置按下/抬起。
*/
private void buildMousePanel() {
mousePanel = new FrameLayout(this);
mousePanel.setBackgroundColor(0xFF101418);
LinearLayout col = new LinearLayout(this);
col.setOrientation(LinearLayout.VERTICAL);
col.setPadding(dp(6), dp(34), dp(6), dp(4));
mousePanel.addView(col, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
TextView tip = new TextView(this);
tip.setText("拖动=移动 单击=左键 双击=右键\\n滚轮在下面");
tip.setTextSize(10f);
tip.setTextColor(0xFF9AA4B2);
tip.setGravity(android.view.Gravity.CENTER);
col.addView(tip, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
mousePad = new View(this);
mousePad.setBackgroundColor(0xFF1B2028);
LinearLayout.LayoutParams padLp = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f);
padLp.setMargins(0, dp(4), 0, dp(4));
col.addView(mousePad, padLp);
// 左键 / 右键 / 滚轮
LinearLayout keys = new LinearLayout(this);
keys.setOrientation(LinearLayout.HORIZONTAL);
col.addView(keys, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(34)));
keys.addView(mouseKey("左键", new Runnable() {
public void run() { sendMouse("click", "left", 0); }
}));
keys.addView(mouseKey("右键", new Runnable() {
public void run() { sendMouse("click", "right", 0); }
}));
keys.addView(mouseKey("滚↑", new Runnable() {
public void run() { sendMouse("scroll", "", 3); }
}));
keys.addView(mouseKey("滚↓", new Runnable() {
public void run() { sendMouse("scroll", "", -3); }
}));
// 返回按钮(快捷栏上也能离开,但这里给个明确的出口)
Button back = new Button(this);
back.setText("返回");
back.setTextSize(11f);
back.setAllCaps(false);
FrameLayout.LayoutParams backLp = new FrameLayout.LayoutParams(
dp(58), dp(26));
backLp.gravity = android.view.Gravity.TOP | android.view.Gravity.END;
backLp.setMargins(0, dp(4), dp(4), 0);
back.setLayoutParams(backLp);
back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showControlPanel();
}
});
mousePanel.addView(back);
// ---- 触摸处理 ----
final android.view.GestureDetector detector =
new android.view.GestureDetector(this,
new android.view.GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(android.view.MotionEvent e) {
sendMouse("click", "left", 0); // 单击 = 左键
return true;
}
@Override
public boolean onDoubleTap(android.view.MotionEvent e) {
sendMouse("click", "right", 0); // 双击 = 右键
return true;
}
@Override
public boolean onScroll(android.view.MotionEvent e1,
android.view.MotionEvent e2,
float dx, float dy) {
// 手指往哪滑,光标往哪走:dx/dy 是"内容移动量",
// 也就是手指移动的反方向,所以取负。
sendMouseMove(-dx, -dy);
return true;
}
});
mousePad.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, android.view.MotionEvent e) {
detector.onTouchEvent(e);
return true;
}
});
}
private Button mouseKey(String text, final Runnable action) {
Button b = new Button(this);
b.setText(text);
b.setTextSize(10f);
b.setAllCaps(false);
b.setPadding(0, 0, 0, 0);
b.setMinWidth(0);
b.setMinimumWidth(0);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
0, ViewGroup.LayoutParams.MATCH_PARENT, 1f);
lp.setMargins(dp(1), 0, dp(1), 0);
b.setLayoutParams(lp);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
action.run();
}
});
return b;
}
private void sendMouse(String action, String button, int amount) {
if (!isConnected()) {
toast("还没连上电脑");
return;
}
Map<String, Object> req = Json.obj("cmd", "mouse", "action", action);
if (button != null && !button.isEmpty()) {
req.put("button", button);
}
if (amount != 0) {
req.put("amount", amount);
}
sendCommand(req, "鼠标");
}
private void sendMouseMove(float dx, float dy) {
if (!isConnected()) {
return;
}
// 手表上滑一点,电脑上要走一段 —— 倍率小了手指要划好几次才横穿屏幕。
// 取 3.0 是试出来的:1920 宽的屏大约划 4~5 次到底,细调也还行。
sendCommand(Json.obj("cmd", "mouse", "action", "moveby",
"dx", (int) (dx * 3.0f), "dy", (int) (dy * 3.0f)), "鼠标");
}
'''
# ---------------------------------------------------------------- 设置页那一段
SETTINGS_ANCHOR = " private void showSettingsPanel() {"
def main() -> int:
print("=" * 58)
print(" 手表端:常驻顶部快捷栏 + 鼠标触摸板")
print("=" * 58)
print()
raw = MAIN.read_bytes()
bom = raw.startswith(b"\xef\xbb\xbf")
t = raw.decode("utf-8-sig")
if "buildQuickBar" in t:
print(" 已经加过了")
return 0
# 先校验所有锚点,避免改一半
need = {
"updateButton 字段": FIELDS_ANCHOR,
"showOnly 方法": BUILD_ANCHOR,
}
missing = [k for k, v in need.items() if v not in t]
if missing:
print(" ✗ 这些锚点找不到:%s" % ", ".join(missing))
return 1
print(" ✓ 锚点全部就位")
# 1) 字段
t = t.replace(FIELDS_ANCHOR, FIELDS, 1)
# 2) 方法(插在 showOnly 之前)
t = t.replace(BUILD_ANCHOR, BUILD_METHOD + BUILD_ANCHOR, 1)
# 3) 鼠标面板方法(也插在 showOnly 之前,跟快捷栏挨着)
t = t.replace(BUILD_ANCHOR, MOUSE_METHOD + BUILD_ANCHOR, 1)
MAIN.write_bytes((b"\xef\xbb\xbf" if bom else b"") + t.encode("utf-8"))
print(" ✓ 加了字段 + buildQuickBar/refreshQuickBar/runQuickAction")
print(" ✓ 加了鼠标触摸板 buildMousePanel")
return 0
if __name__ == "__main__":
sys.exit(main())