跨端: 鸿蒙 2in1 键盘派发重构 + 右键菜单(补上一轮的实测修正)
上一轮提交(e54dc39)的快捷键实测后发现**详情页的回车开错了东西**,
这一轮是修正 + 补齐。
① 详情页回车开出的是"写信"而不是"回复"(实测截图硬证)
根因:详情页自己的 `onKeyEvent` **从不触发** —— 官方要求组件**获得焦点**
才响应(common.d.ts:19510),而页面根 Stack 默认不可聚焦,
加 `.focusable(true)` 也没人 requestFocus。于是键直接冒到主页根,
被那条"Enter=写信"抢先。
⇒ 改成**根上按状态派发**(与 WebUI 同构:它也是一处全局监听 + 按状态分派):
· 发布 `KEY_OPEN_MAIL_ID`(CommPage.openMail 写、NavDestinations 返回时清)
⇒ 判断"此刻是不是在看某封邮件"
· 发布 `KEY_COMM_STACK_DEPTH`(navPathStack.size())
⇒ 判断 Esc 还有没有层可退(写信也占一层)
· 根上据此决定:Enter = 回复 or 写信;Esc = 弹一层 or 交还系统
新增 `ReplyIntent` / `PopIntent`,与既有 `ComposeIntent` **同一"两半"形状**
(有人听就当场给、没人听就存着)——根组件够不着那两处的实例。
② 右键菜单(用户选「右键菜单」)
· 邮件行挂 `bindContextMenu(…, ResponseType.RightClick)`;官方枚举只有
RightClick / LongPress 两项 —— 长按是触屏语义,且左键单击已被
"打开邮件"占用,只剩右键可用。
· 菜单项**只放列表层能当场完成**的:标记已读 / 归档会话 / 复制主题。
★ **不放**回复/转发:那两个要详情页的表单,在列表行上做只能"先跳详情",
那不是菜单项该有的语义(点了当场就该有结果)。
· 归档走系统确认框(破坏性操作,与联系人页同一分寸)。
实测(2in1 模拟器 3120×2080,xdotool 注入真实键鼠)
· 列表 Enter → 写信页 ✅ 截图
· 写信页 Esc → 回列表 ✅ 截图
· 详情页 Enter → 回复框("回复给 pi@…")✅ 截图
· 邮件行右键 → 菜单(归档会话/复制主题)✅ 截图
· 复制主题 → 无报错、菜单关闭
★ 一个重要的自我更正
我先前说"2in1 模拟器上键盘注入不生效、属环境限制"——**那是错的**。
xdotool 的键事件一直都能到 App(探针日志明确显示
`Node Stack/68 handle KeyEvent` + handler 被调用)。误判的原因是我当时
在**登录页**测 Ctrl+N(那页本来就没实现它,当然没反应)。
教训:探针打进去之前,不要把"没反应"归因于环境。
判据(harmony-2in1 新增 7 条 → 12→19)
· 三页用同一套键判定(不许各写一遍 KeyCode 比较)
· 登录页回车提交(WebUI 靠 <form> 天然有,鸿蒙原来一行监听都没有)
· 详情页 Enter/Esc + 弹层开着时 Esc 先关弹层
· 右手菜单:挂了 bindContextMenu、类型是 RightClick、
菜单项只用当场能完成的动作(不放回复/转发)、归档要先确认
★ 两条判据第一版是**我自己判红了自己**,都是判据比事实严格:
① 详情页确实有 KEYCODE_ENTER —— 那是**输入框内的候选导航**(onFwdKey),
与页面级快捷键是两件事 ⇒ 改成只查页面级 onKeyEvent 那一段;
② isEscapeKey 先在 import 行出现,从那里切片取到的是注释 ⇒
改成从页面级 onKeyEvent 内部起切。
This commit is contained in:
@ -495,3 +495,72 @@ test('2in1|2in1/PC 上窗口装饰必须隐掉(否则标题栏下多一条"
|
||||
assert.match(ABILITY, /win\.setWindowLayoutFullScreen\(true\)/,
|
||||
'两条是**互补**的:这条管屏幕四边、装饰那条管窗口标题栏,删任一条都会露馅');
|
||||
});
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────
|
||||
* ⑥ 右键菜单(2026-09-24 用户选「右键菜单」)
|
||||
*
|
||||
* ★ 这不是对齐项:WebUI **没有**右键菜单(`grep onContextMenu` 全仓为空)——
|
||||
* 所以它是 2in1/PC 的新增桌面能力。写明,免得日后被当成缺失项去"补"。
|
||||
* ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
test('2in1|邮件行要有右键菜单,且用系统的 RightClick(不是自己接鼠标事件)', () => {
|
||||
/*
|
||||
* 钉两条:
|
||||
* ① 菜单挂上了(`bindContextMenu`)—— 删掉它菜单就没了;
|
||||
* ② 类型是 `ResponseType.RightClick` —— 官方枚举只有
|
||||
* `RightClick` / `LongPress` 两项,用错就变成"长按出菜单",
|
||||
* 而长按在触屏上是另一套语义(且左键单击已被"打开邮件"占用)。
|
||||
*
|
||||
* 改坏会红:删 bindContextMenu、或把 RightClick 换成 LongPress。
|
||||
*/
|
||||
assert.match(MAIN, /\.bindContextMenu\(/,
|
||||
'邮件行要挂右键菜单(bindContextMenu)');
|
||||
assert.match(MAIN, /ResponseType\.RightClick/,
|
||||
'必须是鼠标右键(RightClick)—— LongPress 是触屏长按,语义不同');
|
||||
});
|
||||
|
||||
test('2in1|右键菜单项只用**列表层能当场完成**的动作(不放回复/转发)', () => {
|
||||
/*
|
||||
* ★ 这条钉的是一条**判断**,不是实现细节:
|
||||
* 菜单项应当"点了当场就有结果"。
|
||||
*
|
||||
* 回复/转发需要详情页的表单(收件人、正文、附件)——
|
||||
* 在列表行上做只能"先跳详情再操作",那还不如直接单击那一行。
|
||||
* 凑一个"点了等于跳转"的菜单项比没有更坏(用户以为在这里能完成)。
|
||||
*
|
||||
* 所以菜单里只有三个**当场能完成**的:
|
||||
* · 标记已读(`MailApi.markRead`)
|
||||
* · 归档会话(`MailApi.archiveContact`)
|
||||
* · 复制主题(`pasteboard`,纯本地)
|
||||
*
|
||||
* 改坏会红:往菜单里加"回复"/"转发"项。
|
||||
*/
|
||||
const at = MAIN.indexOf('MailContextMenu(mail: MailLike)');
|
||||
assert.ok(at > 0, '菜单 Builder 要存在');
|
||||
const body = MAIN.slice(at, MAIN.indexOf('@Builder', at + 10));
|
||||
|
||||
assert.match(body, /标记已读/, '要有「标记已读」(markRead 在列表层可独立完成)');
|
||||
assert.match(body, /归档会话/, '要有「归档会话」(archiveContact 同上)');
|
||||
assert.match(body, /复制主题/, '要有「复制主题」(纯本地)');
|
||||
assert.ok(!/回复|转发/.test(body),
|
||||
'菜单里**不放**回复/转发 —— 那两个要详情页的表单,在这里做只能"先跳详情",' +
|
||||
'那不是菜单项该有的语义(点了当场就该有结果)');
|
||||
});
|
||||
|
||||
test('2in1|右键菜单的破坏性动作(归档)必须先确认', () => {
|
||||
/*
|
||||
* 归档是**破坏性**的(Agent 侧会话归档 + 邮箱界面同时移除),
|
||||
* 而右键菜单是"点一下就发生了"—— 误触代价太大。
|
||||
* 联系人页那边的归档一直有确认(`ArchiveConfirm` 弹层 / `confirmArchive`),
|
||||
* 列表行这条也必须有,否则两处对同一件事的分寸不一致。
|
||||
*
|
||||
* 改坏会红:把 showAlertDialog 那层确认删掉、直接发请求。
|
||||
*/
|
||||
const at = MAIN.indexOf('private archiveRow(mail: MailLike)');
|
||||
assert.ok(at > 0, 'archiveRow 要存在');
|
||||
const body = MAIN.slice(at, at + 1200);
|
||||
assert.match(body, /showAlertDialog|AlertDialog/,
|
||||
'归档要先弹确认(与联系人页同一分寸)');
|
||||
/* 真正的请求在确认回调之后(另一个方法里)——确保不是"点了就发" */
|
||||
assert.match(body, /doArchiveRow/, '确认的回调里才发请求');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user