/* * AgentMail 鸿蒙客户端 — 2in1(PC / 平板键盘态)键盘快捷键 * * ★★ 2026-09-24 新增。用户:「还有快捷键,比如主页回车打开写信。 * 邮件详情页回车打开回复。esc返回上一级」。 * * ## 为什么单独一个模块 * * 这三条键各自要落在**不同的组件**上(主页在 `MainPage` 根、详情在 * `MailDetailView`),而它们的**判定规则是同一套**(哪些键、什么条件、 * 要不要吞掉事件)。规则写两遍必然分叉 —— 本仓反复出现的形状。 * 所以规则住在这里,两处只做"接线"。 * * ## 为什么不用 `keyboardShortcut`(Ctrl+N 那条用了) * * `keyboardShortcut` 只接受**组合键**: * · 字符键必须配修饰键(Ctrl / Shift / Alt); * · 不配修饰键时只接受 `FunctionKey`。 * 而框架的 `FunctionKey`(`component/enums.d.ts:3444`)只有 * ESC / F1–F12 / TAB / DPAD_UP / DPAD_DOWN / DPAD_LEFT / DPAD_RIGHT * —— **没有 Enter**。所以"单按回车"用它表达不出来。 * * 官方给单键的路是 `onKeyEvent`(`common.d.ts:19510`),而它 * 「Triggered when a key operation is performed on the bound component * **after it obtains focus**」—— 需要焦点。 * * ## 焦点问题怎么解(这是本条能力的关键) * * 邮件列表里焦点会随点击乱跑,所以"绑在某个列表项上等它获焦"不可靠。 * 但官方同时给了**冒泡**语义(`common.d.ts:19560-19562`): * 「If the callback returns **true**, the key event is marked as consumed * and will not **bubble up** to parent components.」 * 反过来说:**回调返回 false(或不处理)时,事件会冒泡到父组件**。 * * ⇒ 于是有了一条可靠的路: * · 绑在**页面根容器**上(它不是 `Text`/`Image` 那种默认不可聚焦的节点, * 而是布局容器 —— 见 `isConsumableKey` 的注释); * · 子组件里真正的输入框(收件人输入、回复框)先拿到键, * 它们自己消费掉(`ComposePage.onToKey` 就是这么做的); * · 没被子组件消费的键**冒泡到页面根**,由这里处理。 * * 这样"详情页优先、主页兜底"是**框架保证**的,不是我自己排的优先级。 * * ## 与 WebUI 的关系(这条不是对齐项) * * WebUI **没有**这三条快捷键(`grep` 过全部 `onKeyDown`:只有 * `AccountSwitcher.tsx:40` 的 Esc 关下拉、`AddressInput.tsx` 的候选导航)。 * 所以它们是**鸿蒙侧新增的 2in1 能力**,不是"WebUI 有而鸿蒙没有"。 * 这一点要写明,免得后面审计时被当成缺失项去"补"。 */ import { KeyCode } from '@kit.InputKit'; /** * 一个键事件该不该由本模块处理。 * * ★ 只认 `KeyType.Down`:`onKeyEvent` 对**按下与抬起**各触发一次, * 两条都处理会让一次按键走两步(`ComposePage.onToKey` 头一行就是这道门, * 本仓 2in1 判据也钉过)。 */ export function isKeyDown(type: KeyType): boolean { return type === KeyType.Down; } /** 回车:打开发信 / 打开回复 —— 两条路都要它 */ export function isEnterKey(keyCode: number): boolean { return keyCode === KeyCode.KEYCODE_ENTER || keyCode === KeyCode.KEYCODE_NUMPAD_ENTER || keyCode === KeyCode.KEYCODE_DPAD_CENTER; } /** Esc:返回上一级 */ export function isEscapeKey(keyCode: number): boolean { return keyCode === KeyCode.KEYCODE_ESCAPE; } /** * 这次按键是不是"文本框该自己管的"。 * * ★ 为什么需要这道门(真实后果,不是理论): * 回复框、收件人输入框里按回车是**换行 / 提交**,不该被页面级 * "回车 = 打开回复"抢走。抢走的后果是:用户想换行,结果又开了一层回复框, * 而且 Esc 想取消输入却返回到列表 —— 输入到一半的内容全没了。 * * 判定用键本身而不是"焦点在哪":焦点位置要读 `FocusController`, * 而它与"这个键是否已被消费"是两件事 —— 官方给的冒泡机制已经表达了 * "子组件消费掉了就别往上冒",这里只再兜一层**文本输入语义**的键。 */ export function isTextEditingKey(keyCode: number): boolean { /* * 只列真正属于"文字输入"的键。 * ★ **不含 Enter** —— 故意的: * 本页没有"回车换行"的多行输入(回复框是单行 `TextInput`, * 见 `MailDetailPage` 的回复条),回车在这里的语义就是"提交/打开"。 * 若哪天回复框改成多行 `TextArea`,**必须**把 Enter 加进来, * 否则用户换行会被抢去开新回复框。 * (这一条写在代码里而不是文档里 —— 改的人一定会看到这里。) */ return keyCode === KeyCode.KEYCODE_SPACE || keyCode === KeyCode.KEYCODE_TAB; }