From f069d5b1c4117b17b83c946652796c6da55b0e0a Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Sun, 15 Nov 2026 02:36:58 +0800 Subject: [PATCH 1/4] feat(menu): fixed input-menu order and move symbols hotkey to ctrl+alt+e MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - define InputMenuAction.sections: learning → practice → symbols → guide → update → settings → about, shared by host and preview - add 显示学习区 / 练习… / 符号与表情… entries; learning toggle shows check state - default hotkey.open_symbols alt+s → ctrl+alt+e (⌥S conflicts, ⌥E is a dead key); explicit user config stays untouched - update menu/settings probes, config docs, ADR-360 and report 288 - snapshot cache key now includes libime_ffi.a so default changes re-render Signed-off-by: wangzhizhou <824219521@qq.com> --- HANDOFF.md | 4 + crates/ime-config/src/schema.rs | 4 +- ...288-input-menu-order-and-symbols-hotkey.md | 63 ++++++++++++++ ...63\347\255\226\350\256\260\345\275\225.md" | 24 ++++++ ...15\347\275\256\346\236\266\346\236\204.md" | 5 ++ .../macos/Preview/InputMenuPreview.swift | 49 ++++++++--- platforms/macos/Preview/main.swift | 13 +++ platforms/macos/Sources/InputController.swift | 86 ++++++++++++++----- platforms/macos/Sources/InputMenu.swift | 51 +++++++++++ platforms/macos/dev-preview.sh | 7 ++ platforms/macos/tests/controller-menu.swift | 18 ++-- platforms/macos/tests/input-menu.swift | 11 +++ platforms/macos/tests/preview-menu.swift | 42 ++++++--- .../macos/tests/settings-presentation.swift | 2 +- scripts/ui-snapshots.sh | 4 +- 15 files changed, 329 insertions(+), 54 deletions(-) create mode 100644 docs/reviews/288-input-menu-order-and-symbols-hotkey.md diff --git a/HANDOFF.md b/HANDOFF.md index 3421131f..d0214de5 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -136,6 +136,10 @@ make macos-offline-tests ## 9. 会话日志(最近) +- 2026-11-15 / Claude Code(air):**输入法菜单固定顺序 + 符号快捷键(ADR-360,报告 288)**。菜单动作区固定为 + 「学习区 → 练习 → 符号与表情 → 使用指南 → 检查更新 → 设置 → 关于」,正式/预览共用 `InputMenuAction.sections`; + 新增学习区/练习/符号入口;`hotkey.open_symbols` 默认 `alt+s` → `ctrl+alt+e`(已有显式配置不覆盖)。 + 三处菜单探针与设置页断言同步;快照缓存键加入 `libime_ffi.a`。 - 2026-11-15 / Claude Code(air):**交互式面板验收修复(报告 287)**。`make macos-dev` 在 Claude Code 的 非 TTY stdin 下 `selectors.register` 崩溃;改为非 TTY 时只轮询子进程、关闭窗口即结束。交互会话中真实数据 变化降为 WARN(预览只写私有根);测试期间真实词库 2,046→2,060 行属已装输入法正常学习,未回滚。 diff --git a/crates/ime-config/src/schema.rs b/crates/ime-config/src/schema.rs index 2cb2a689..4d5c3c17 100644 --- a/crates/ime-config/src/schema.rs +++ b/crates/ime-config/src/schema.rs @@ -296,9 +296,9 @@ pub static SCHEMA: &[OptionSpec] = &[ key: "hotkey.open_symbols", value_type: ValueType::String, enum_labels_zh: &[], - default: "\"alt+s\"", + default: "\"ctrl+alt+e\"", title_zh: "符号与 emoji", - doc_zh: "打开符号 / emoji 选择面板(分类与中英文搜索);默认 alt+s,留空 = 关闭。反引号 ` 可在候选窗中快速打开分类", + doc_zh: "打开符号 / emoji 选择面板(分类与中英文搜索);默认 ctrl+alt+e(E = emoji;双修饰键避开应用快捷键,⌥E 是死键不可单用),留空 = 关闭。反引号 ` 可在候选窗中快速打开分类", scope: Scope::Hotkey, }, OptionSpec { diff --git a/docs/reviews/288-input-menu-order-and-symbols-hotkey.md b/docs/reviews/288-input-menu-order-and-symbols-hotkey.md new file mode 100644 index 00000000..698cca7b --- /dev/null +++ b/docs/reviews/288-input-menu-order-and-symbols-hotkey.md @@ -0,0 +1,63 @@ +# 288 — 输入法菜单固定顺序 + 符号面板快捷键调整(ADR-360) + +> 2026-11-15。人工验收反馈:菜单缺「练习」入口、顺序无策略、`⌥S` 易冲突。 + +## 1. 问题 + +1. **入口缺失**:正式宿主的输入法菜单只有「检查更新… / 设置 / 关于 / 使用指南…」, + 没有「练习…」「符号与表情…」;预览菜单连「使用指南…」也没有,人工验收看到的是不完整菜单。 +2. **顺序无策略**:入口按添加历史排列,新增一个就要重新记一次位置。 +3. **快捷键冲突**:`hotkey.open_symbols = "alt+s"` 与用户已有快捷键冲突。 + +## 2. 固定顺序 + +```text +状态区(IMK 契约,永远在最前) + 模式 · 中英 · 全/半角 · 简/繁 · 标点 +──── +学习与工具 + 显示学习区(勾选)· 练习… · 符号与表情… · 使用指南… +──── +应用 + 检查更新… · 设置… · 关于 +──── +异常状态(仅出错时出现,恒在最后) +``` + +规则: + +- 顺序只写在 `InputMenuAction.sections`(`platforms/macos/Sources/InputMenu.swift`),正式宿主与预览共用; +- 新增入口**只能追加到所属组末尾**,并同步更新三处探针; +- 每组之间一条分隔线;错误状态不进固定顺序。 + +## 3. 新增入口 + +| 菜单项 | 快捷键来源 | 行为 | +|---|---|---| +| 显示学习区 | `hotkey.toggle_wubi_learning`(默认 ⌥L) | 与 ⌥L 同源,切换显示偏好并打勾,不提交当前组合 | +| 练习… | `hotkey.open_practice`(默认 ⌥K) | 打开离线练习窗口 | +| 符号与表情… | `hotkey.open_symbols`(默认 **⌃⌥E**) | 先提交当前组合,再把选中符号插入原插入点;预览里插入预览输入区 | +| 使用指南… | 无 | 打开完整使用指南 | + +## 4. 快捷键 + +`hotkey.open_symbols` 默认 `alt+s` → **`ctrl+alt+e`**: + +- E = emoji,保留语义首字母; +- `⌥S` 与应用快捷键冲突(用户反馈);`⌥E` 在 ADR-209 已确认是死键,不能单独使用; +- 双修饰键同时避开「仅 ⌥ + 字母」的应用快捷键面; +- **已有显式配置不静默覆盖**:老用户文件里写了 `alt+s` 就继续用,设置页显示实际值。 + +## 5. 验证 + +- `input-menu.swift`:断言分组与标题顺序、只有「使用指南」无快捷键; +- `controller-menu.swift`:正式宿主冷菜单 **14 项**、两条分隔线、位置与快捷键全部核对; +- `preview-menu.swift`:预览菜单同序,7 个动作各触发一次回调; +- `settings-presentation.swift`:设置页显示 `⌃⌥E`; +- UI 快照:设置快捷键页已重新渲染;快照缓存键加入 `libime_ffi.a`,Schema 默认值变化不再复用旧二进制; +- `make macos-offline-tests` 68 文件 / 337 例 PASS;`make verify` PASS。 + +## 6. 关联 + +ADR-360、ADR-209/313/349/351、`InputMenu.swift`、`InputController.swift`、 +`Preview/InputMenuPreview.swift`、`schema.rs`、`docs/配置架构.md`。 diff --git "a/docs/\345\206\263\347\255\226\350\256\260\345\275\225.md" "b/docs/\345\206\263\347\255\226\350\256\260\345\275\225.md" index 92ca3318..ec464a5d 100644 --- "a/docs/\345\206\263\347\255\226\350\256\260\345\275\225.md" +++ "b/docs/\345\206\263\347\255\226\350\256\260\345\275\225.md" @@ -3963,3 +3963,27 @@ - **状态**:采纳(2026-11-15)。 - **关联**:ADR-358、报告 284/285、`docs/standards/wubi-decomposition.md`、`d2-glyph-aliases.tsv`、 `wubi86-root-inventory.tsv`、`data/sources.toml`。 + +### ADR-360:输入法菜单固定顺序;符号面板快捷键改为 ⌃⌥E + +- **背景**:用户人工验收时反馈 ① 输入法菜单缺「练习」「使用指南」入口(预览菜单尤其明显), + ② 菜单项顺序没有策略、后续迭代不可预期,③ 符号与表情的 `⌥S` 容易与已有快捷键冲突。 +- **决策**: + 1. 菜单分三区并固定顺序:**状态区**(模式 → 中英 → 全/半角 → 简/繁 → 标点,5 行由 IMK 契约固定在最前); + **学习与工具**(显示学习区 → 练习… → 符号与表情… → 使用指南…); + **应用**(检查更新… → 设置… → 关于)。异常状态恒在最后且只在出错时出现。 + 2. 顺序写成 `InputMenuAction.sections`(`InputMenu.swift`),正式宿主与预览共用; + 新增入口只能追加到所属组末尾并更新 `input-menu.swift` / `controller-menu.swift` / `preview-menu.swift` 三处探针。 + 3. 动作区新增「显示学习区」(勾选态,与 ⌥L 同源)、「练习…」(与 ⌥K 同源)、「符号与表情…」 + (与快捷键同源);「使用指南…」保留在工具组。 + 4. `hotkey.open_symbols` 默认从 `alt+s` 改为 **`ctrl+alt+e`**:E = emoji,双修饰键避开应用级 + `⌥+字母` 快捷键;ADR-209 已列 `⌥E` 为死键,不能单用。**已有显式配置不静默覆盖**。 +- **理由**:状态类每次打开菜单都会被看,放最前;学习与工具是日常高频,放中间;应用级操作最少用,放最后。 + 固定顺序让验收和文档可以直接断言,不靠记忆。 +- **结果**:正式菜单 14 项(5 状态 + 2 分隔线 + 4 工具 + 3 应用);预览菜单与正式一致; + 设置页显示 `⌃⌥E`;三处菜单探针与 `settings-presentation` 全部通过。 +- **边界**:不改 IMK 前 5 行的命令语义;不新增配置文件;不迁移用户已显式设置的快捷键; + 预览的「符号与表情」插入到预览输入区,不经过系统 IME。 +- **状态**:采纳(2026-11-15)。 +- **关联**:ADR-209/313/349/351、报告 288、`InputMenu.swift`、`InputController.swift`、 + `Preview/InputMenuPreview.swift`、`crates/ime-config/src/schema.rs`、`docs/配置架构.md`。 diff --git "a/docs/\351\205\215\347\275\256\346\236\266\346\236\204.md" "b/docs/\351\205\215\347\275\256\346\236\266\346\236\204.md" index b92ef5e3..2f09f57b 100644 --- "a/docs/\351\205\215\347\275\256\346\236\266\346\236\204.md" +++ "b/docs/\351\205\215\347\275\256\346\236\266\346\236\204.md" @@ -148,6 +148,9 @@ toggle_variant = "alt+t" # 简/繁(T = 繁体)| 组合键 | none toggle_wubi_learning = "alt+l" # 显示/隐藏学习区(L = Learning)| none open_settings = "alt+," # 打开设置 | 组合键 | none(与菜单项提示同源) open_about = "alt+a" # 关于(A = About)| 组合键 | none(与菜单项提示同源) +open_update = "alt+g" # 检查更新(G = GitHub)| 组合键 | none(与菜单项提示同源) +open_practice = "alt+k" # 练习(K = 练习)| 组合键 | none +open_symbols = "ctrl+alt+e" # 符号与表情(E = emoji;出双修饰键避开应用快捷键)| 组合键 | none [input] punctuation = "chinese" # 标点:chinese = 中文 | english = 英文(⌥P 可切换) @@ -261,6 +264,8 @@ crates/ Schema 增加 input.punctuation = "chinese" | "english"(默认 chinese)和 input.full_width = "half" | "full"(默认半角);默认值仍只由 Schema 定义,配置注释自动生成。中英快捷键(Shift)是会话内临时切换,不写配置;**全/半角快捷键自 ADR-305 起与标点/字形一致,写配置并持久化**(旧版布尔值 `false/true` 读取时迁移为 `half/full`,不改写用户文件)。hotkey.toggle_mode 由宿主消费,正式模式切换经 ime_config_set 保存。 +2026-11-15(ADR-360):`hotkey.open_symbols` 默认从 `alt+s` 改为 `ctrl+alt+e`(E = emoji;⌥S 与应用快捷键冲突,⌥E 又是死键,故用双修饰键)。已有显式配置不静默覆盖;设置页显示 `⌃⌥E`。输入法菜单动作区同时固定为「学习区 → 练习 → 符号与表情 → 使用指南 → 检查更新 → 设置 → 关于」。 + 2026-11-06(ADR-313):新增 `hotkey.open_practice`(默认 `alt+k`),打开离线练习窗口;作用域 `Hotkey`,`none`/空即关闭,与其它快捷键同一套冲突检测;不新增配置文件、不改持久化路径。 2026-10-18(ADR-207):新增 `hotkey.open_settings`(默认 `ctrl+,`)与 `hotkey.open_about`(默认 `ctrl+shift+a`),由 Schema 提供默认值,设置页与其它快捷键统一管理;宿主与预览把同一配置显示为菜单项右侧快捷键,按键路径在控制器中处理,不依赖菜单跟踪。`none`/空 = 关闭。 diff --git a/platforms/macos/Preview/InputMenuPreview.swift b/platforms/macos/Preview/InputMenuPreview.swift index bdd82213..94675d3f 100644 --- a/platforms/macos/Preview/InputMenuPreview.swift +++ b/platforms/macos/Preview/InputMenuPreview.swift @@ -11,6 +11,10 @@ final class PreviewInputMenu: NSObject, NSMenuDelegate { var onCheckUpdates: (() -> Void)? var onSettings: (() -> Void)? var onAbout: (() -> Void)? + var onToggleLearning: (() -> Void)? + var onPractice: (() -> Void)? + var onSymbols: (() -> Void)? + var onGuide: (() -> Void)? lazy var menu: NSMenu = makeMenu() init(session: ImeSession) { @@ -32,25 +36,42 @@ final class PreviewInputMenu: NSObject, NSMenuDelegate { private func makeMenu() -> NSMenu { let result = InputStateMenu.make(state: state, target: self) result.delegate = self - result.addItem(.separator()) - for (title, action) in [("检查更新…", #selector(checkUpdates(_:))), - ("设置", #selector(showSettings(_:))), ("关于", #selector(showAbout(_:)))] { - let item = NSMenuItem(title: title, action: action, keyEquivalent: "") - item.target = self - result.addItem(item) + // ADR-360:与正式宿主共用同一份动作区顺序。 + for section in InputMenuAction.sections { + result.addItem(.separator()) + for action in section { + let item = NSMenuItem(title: action.title, action: selector(for: action), keyEquivalent: "") + item.tag = action.rawValue + item.target = self + result.addItem(item) + } } return result } + private func selector(for action: InputMenuAction) -> Selector { + switch action { + case .toggleLearning: return #selector(toggleLearning(_:)) + case .practice: return #selector(showPractice(_:)) + case .symbols: return #selector(showSymbols(_:)) + case .guide: return #selector(showGuide(_:)) + case .update: return #selector(checkUpdates(_:)) + case .settings: return #selector(showSettings(_:)) + case .about: return #selector(showAbout(_:)) + } + } + func refresh() { - if menu.items.count >= 9 { - for (item, key) in [(menu.items[6], "hotkey.open_update"), - (menu.items[7], "hotkey.open_settings"), - (menu.items[8], "hotkey.open_about")] { - item.keyEquivalent = "" - item.keyEquivalentModifierMask = [] + for item in menu.items where item.tag >= InputMenuAction.toggleLearning.rawValue { + guard let action = InputMenuAction(rawValue: item.tag) else { continue } + item.keyEquivalent = "" + item.keyEquivalentModifierMask = [] + if let key = action.hotkeyKey { _ = item.applyConfiguredHotkey(ImeConfig.hotkey(key)) } + if action == .toggleLearning { + item.state = ImeConfig.bool("candidate.show_wubi_learning") ? .on : .off + } } for (item, key) in zip(menu.items.prefix(5), InputStateMenu.shortcuts) { item.keyEquivalent = "" @@ -80,6 +101,10 @@ final class PreviewInputMenu: NSObject, NSMenuDelegate { } @objc private func checkUpdates(_ sender: Any?) { onCheckUpdates?() } + @objc private func toggleLearning(_ sender: Any?) { onToggleLearning?() } + @objc private func showPractice(_ sender: Any?) { onPractice?() } + @objc private func showSymbols(_ sender: Any?) { onSymbols?() } + @objc private func showGuide(_ sender: Any?) { onGuide?() } private func changed(_ success: Bool = true) { refresh() diff --git a/platforms/macos/Preview/main.swift b/platforms/macos/Preview/main.swift index a3432486..3328ea80 100644 --- a/platforms/macos/Preview/main.swift +++ b/platforms/macos/Preview/main.swift @@ -262,6 +262,19 @@ final class PreviewDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { menu.onCheckUpdates = { UpdateEntry.open() } menu.onSettings = { [weak self] in self?.openSettings() } menu.onAbout = { AboutUI.show() } + menu.onToggleLearning = { [weak self] in + let next = !ImeConfig.bool("candidate.show_wubi_learning") + if ImeConfig.setToml("candidate.show_wubi_learning", next ? "true" : "false") { self?.input?.refresh() } + self?.syncMenuControls() + } + menu.onPractice = { PracticeWindow.shared.show() } + menu.onGuide = { OnboardingWindow.shared.show() } + menu.onSymbols = { [weak self] in + guard let input = self?.input else { return } + SymbolPickerWindow.shared.show { [weak input] text in + input?.insertText(text, replacementRange: NSRange(location: NSNotFound, length: 0)) + } + } previewMenu = menu input.stateChanged = { [weak self] in self?.syncMenuControls() } let menuButton = NSButton(title: "输入法菜单", target: self, action: #selector(openInputMenu(_:))) diff --git a/platforms/macos/Sources/InputController.swift b/platforms/macos/Sources/InputController.swift index 061de424..2216876a 100644 --- a/platforms/macos/Sources/InputController.swift +++ b/platforms/macos/Sources/InputController.swift @@ -231,23 +231,17 @@ final class LampyrisInputController: IMKInputController { let menu = InputStateMenu.make(state: inputMenuState(), target: self) stateMenu = menu refreshStateMenu() - menu.addItem(NSMenuItem.separator()) - let update = NSMenuItem( - title: "检查更新…", action: #selector(checkForUpdates), keyEquivalent: "") - update.target = self - menu.addItem(update) - let settings = NSMenuItem( - title: "设置", action: #selector(showSettings), keyEquivalent: "") - settings.target = self - menu.addItem(settings) - let about = NSMenuItem( - title: "关于", action: #selector(showAbout), keyEquivalent: "") - about.target = self - menu.addItem(about) - let guide = NSMenuItem( - title: "使用指南…", action: #selector(showGuide), keyEquivalent: "") - guide.target = self - menu.addItem(guide) + // ADR-360:动作区顺序由 InputMenuAction.sections 固定;新增入口只追加到所属组末尾。 + for section in InputMenuAction.sections { + menu.addItem(NSMenuItem.separator()) + for action in section { + let item = NSMenuItem(title: action.title, action: selector(for: action), + keyEquivalent: "") + item.tag = action.rawValue + item.target = self + menu.addItem(item) + } + } // 异常状态(仅在出错时出现) var alerts: [String] = [] if let session, session.dictionaryError != IME_OK { @@ -271,10 +265,54 @@ final class LampyrisInputController: IMKInputController { return menu } + private func selector(for action: InputMenuAction) -> Selector { + switch action { + case .toggleLearning: return #selector(toggleLearning) + case .practice: return #selector(showPractice) + case .symbols: return #selector(showSymbols) + case .guide: return #selector(showGuide) + case .update: return #selector(checkForUpdates) + case .settings: return #selector(showSettings) + case .about: return #selector(showAbout) + } + } + @objc private func checkForUpdates() { UpdateEntry.open() } + /// 菜单里的学习区开关与 ⌥L 同源:只改显示偏好,不提交当前组合。 + @objc private func toggleLearning() { + let next = !ImeConfig.bool("candidate.show_wubi_learning") + if ImeConfig.setToml("candidate.show_wubi_learning", next ? "true" : "false") { + showStateFeedback(StateFeedback.learning(next)) + } else { + presentConfigError() + } + refreshStateMenu() + } + + @objc private func showPractice() { + cancelClientComposition() + PracticeWindow.shared.show() + } + + /// 菜单入口与 ⌥S(改为 ⌃⌥E)同源:先提交当前组合,再把选中的符号插入原插入点。 + @objc private func showSymbols() { + if SymbolPickerWindow.shared.isVisible { + SymbolPickerWindow.shared.close() + return + } + guard let client = activeClient else { + NSSound.beep() + return + } + if let session { commitForSymbolPanel(client, session: session) } + SymbolPickerWindow.shared.show { [client] text in + client.insertText(text, replacementRange: NSRange(location: NSNotFound, length: 0)) + } + } + private var stateMenu: NSMenu? private func inputMenuState() -> InputMenuState { @@ -294,14 +332,16 @@ final class LampyrisInputController: IMKInputController { item.keyEquivalentModifierMask = [] } } - if menu.items.count >= 8 { - for (item, key) in [(menu.items[6], "hotkey.open_update"), - (menu.items[7], "hotkey.open_settings"), - (menu.items[8], "hotkey.open_about")] { - item.keyEquivalent = "" - item.keyEquivalentModifierMask = [] + for item in menu.items where item.tag >= InputMenuAction.toggleLearning.rawValue { + guard let action = InputMenuAction(rawValue: item.tag) else { continue } + item.keyEquivalent = "" + item.keyEquivalentModifierMask = [] + if let key = action.hotkeyKey { _ = item.applyConfiguredHotkey(ImeConfig.hotkey(key)) } + if action == .toggleLearning { + item.state = ImeConfig.bool("candidate.show_wubi_learning") ? .on : .off + } } InputStateMenu.refresh(menu, state: inputMenuState()) } diff --git a/platforms/macos/Sources/InputMenu.swift b/platforms/macos/Sources/InputMenu.swift index 1d728d81..34bf392d 100644 --- a/platforms/macos/Sources/InputMenu.swift +++ b/platforms/macos/Sources/InputMenu.swift @@ -67,6 +67,57 @@ enum UpdateEntry { } } +/// 动作区条目与**固定顺序**(ADR-360)。 +/// +/// 状态区 5 行由 IMK 契约固定在最前;动作区分两组: +/// 1. 学习与工具:学习区开关 → 练习 → 符号与表情 → 使用指南; +/// 2. 应用:检查更新 → 设置 → 关于。 +/// +/// 新增入口只能追加到所属组末尾并更新 `input-menu.swift` 探针,不允许打乱既有相对顺序; +/// 异常状态恒在最后且仅在出错时出现。 +enum InputMenuAction: Int, CaseIterable { + case toggleLearning = 10 + case practice = 11 + case symbols = 12 + case guide = 13 + case update = 14 + case settings = 15 + case about = 16 + + /// 组内顺序;空分隔线只加在两组之间。 + static let sections: [[InputMenuAction]] = [ + [.toggleLearning, .practice, .symbols, .guide], + [.update, .settings, .about], + ] + + static var ordered: [InputMenuAction] { sections.flatMap { $0 } } + + var title: String { + switch self { + case .toggleLearning: return "显示学习区" + case .practice: return "练习…" + case .symbols: return "符号与表情…" + case .guide: return "使用指南…" + case .update: return "检查更新…" + case .settings: return "设置…" + case .about: return "关于" + } + } + + /// 与菜单项快捷键提示同源的配置键;`nil` 表示无快捷键。 + var hotkeyKey: String? { + switch self { + case .toggleLearning: return "hotkey.toggle_wubi_learning" + case .practice: return "hotkey.open_practice" + case .symbols: return "hotkey.open_symbols" + case .guide: return nil + case .update: return "hotkey.open_update" + case .settings: return "hotkey.open_settings" + case .about: return "hotkey.open_about" + } + } +} + /// Public AppKit menu contract; callbacks also accept IMK command dictionaries. enum InputStateMenu { enum Command: Int { diff --git a/platforms/macos/dev-preview.sh b/platforms/macos/dev-preview.sh index 075f2fa0..1948a86e 100755 --- a/platforms/macos/dev-preview.sh +++ b/platforms/macos/dev-preview.sh @@ -33,6 +33,9 @@ PLIST cp "${ROOT}/assets/icons/yinghuo-official-app.icns" "${APP}/Contents/Resources/Lampyris.icns" # 学习区结构化提示(ADR-311):预览也需要,便于看真实渲染 [ -f "${DATA}/B0-runtime.tsv" ] && cp "${DATA}/B0-runtime.tsv" "${APP}/Contents/Resources/B0-runtime.tsv" +# 符号面板与正式包同源读取 Resources 下的 TSV,预览也要能打开。 +[ -f "${ROOT}/data/symbols.tsv" ] && cp "${ROOT}/data/symbols.tsv" "${APP}/Contents/Resources/symbols.tsv" +[ -f "${DATA}/emoji.tsv" ] && cp "${DATA}/emoji.tsv" "${APP}/Contents/Resources/emoji.tsv" ln -sfn "${DATA}" "${APP}/Contents/Resources/data" export SWIFT_MODULECACHE_PATH="${BUILD}/module-cache" export CLANG_MODULE_CACHE_PATH="${BUILD}/module-cache" @@ -59,6 +62,10 @@ swiftc -g -Onone -warnings-as-errors -module-name LampyrisPreview \ "${SCRIPT_DIR}/Sources/PassageWindow.swift" \ "${SCRIPT_DIR}/Sources/PracticeAnswerView.swift" \ "${SCRIPT_DIR}/Sources/PracticeExplanationView.swift" \ + "${SCRIPT_DIR}/Sources/Onboarding.swift" \ + "${SCRIPT_DIR}/Sources/OnboardingWindow.swift" \ + "${SCRIPT_DIR}/Sources/SymbolCatalog.swift" \ + "${SCRIPT_DIR}/Sources/SymbolPickerWindow.swift" \ "${ROOT}/modules/keymap/Sources/KeyboardMapView.swift" \ "${ROOT}/modules/keymap/Sources/KeymapPanelView.swift" \ "${ROOT}/modules/keymap/Sources/KeyRootLayout.swift" \ diff --git a/platforms/macos/tests/controller-menu.swift b/platforms/macos/tests/controller-menu.swift index ad47e5bc..1d52810a 100644 --- a/platforms/macos/tests/controller-menu.swift +++ b/platforms/macos/tests/controller-menu.swift @@ -68,14 +68,22 @@ guard let server = IMKServer(name: "cn.jokerhub.lampyris.probe." + UUID().uuidSt } // IMK may request its menu before this controller has a text session. let coldMenu = controller.menu()! -require(coldMenu.items.count == 10, "five rows plus separator, update, settings, about and guide") +require(coldMenu.items.count == 14, "five rows + learning/practice/symbols/guide + update/settings/about") require(coldMenu.items.prefix(5).allSatisfy { $0.submenu == nil }, "no nested menus") require(coldMenu.items[1...4].allSatisfy { !$0.isEnabled }, "session-only toggles stay disabled before activation") -require(coldMenu.items[6].title == "检查更新…" && coldMenu.items[6].keyEquivalent == "g" - && coldMenu.items[6].keyEquivalentModifierMask == [.option], "update entry shows configured shortcut") -require(coldMenu.items[7].keyEquivalent == "," && coldMenu.items[7].keyEquivalentModifierMask == [.option], "settings shortcut in menu") -require(coldMenu.items[8].keyEquivalent == "a" && coldMenu.items[8].keyEquivalentModifierMask == [.option], "about shortcut in menu") +require(coldMenu.items[5].isSeparatorItem && coldMenu.items[10].isSeparatorItem, "two fixed section separators") +require(coldMenu.items[6].title == "显示学习区" && coldMenu.items[6].keyEquivalent == "l" + && coldMenu.items[6].keyEquivalentModifierMask == [.option] && coldMenu.items[6].state == .off, + "learning toggle shows configured shortcut and current check state") +require(coldMenu.items[7].title == "练习…" && coldMenu.items[7].keyEquivalent == "k" + && coldMenu.items[7].keyEquivalentModifierMask == [.option], "practice entry shows configured shortcut") +require(coldMenu.items[8].title == "符号与表情…" && coldMenu.items[8].keyEquivalent == "e" + && coldMenu.items[8].keyEquivalentModifierMask == [.control, .option], "symbols entry shows ctrl+option+e") require(coldMenu.items[9].title == "使用指南…" && coldMenu.items[9].keyEquivalent == "", "guide entry") +require(coldMenu.items[11].title == "检查更新…" && coldMenu.items[11].keyEquivalent == "g" + && coldMenu.items[11].keyEquivalentModifierMask == [.option], "update entry shows configured shortcut") +require(coldMenu.items[12].keyEquivalent == "," && coldMenu.items[12].keyEquivalentModifierMask == [.option], "settings shortcut in menu") +require(coldMenu.items[13].keyEquivalent == "a" && coldMenu.items[13].keyEquivalentModifierMask == [.option], "about shortcut in menu") require(coldMenu.items[0].isEnabled, "mode row enabled before activation") require(coldMenu.items[0].tag == 1, "mode row carries the cycle command") require(LampyrisProbeDispatchMenuCommand(server, controller, coldMenu.items[0], 501), "cold row dispatches through the service boundary") diff --git a/platforms/macos/tests/input-menu.swift b/platforms/macos/tests/input-menu.swift index b9456775..7c29314b 100644 --- a/platforms/macos/tests/input-menu.swift +++ b/platforms/macos/tests/input-menu.swift @@ -36,6 +36,17 @@ final class MenuOwner: NSObject { InputStateMenu.refresh(menu, state: snapshot) } } +// ADR-360:动作区顺序固定,新增入口只能追加到所属组末尾。 +require(InputMenuAction.sections == [[.toggleLearning, .practice, .symbols, .guide], + [.update, .settings, .about]], "fixed action section order") +require(InputMenuAction.ordered.map(\.title) == ["显示学习区", "练习…", "符号与表情…", "使用指南…", + "检查更新…", "设置…", "关于"], "fixed action titles") +require(InputMenuAction.ordered.filter { $0.hotkeyKey == nil } == [.guide], "only guide has no hotkey") +require(InputMenuAction.toggleLearning.hotkeyKey == "hotkey.toggle_wubi_learning", "learning hotkey source") +require(InputMenuAction.symbols.hotkeyKey == "hotkey.open_symbols", "symbols hotkey source") +LocalAcceptanceEvidence.record("menu-view.order", expected: "动作区固定为学习区/练习/符号与表情/使用指南/检查更新/设置/关于", + actual: ["sections": InputMenuAction.sections.map { $0.map(\.title) }]) + let owner = MenuOwner() let menu = owner.menu let parent = menu.items[0] diff --git a/platforms/macos/tests/preview-menu.swift b/platforms/macos/tests/preview-menu.swift index 40afda20..6462aa10 100644 --- a/platforms/macos/tests/preview-menu.swift +++ b/platforms/macos/tests/preview-menu.swift @@ -39,11 +39,18 @@ adapter.onChange = { changes += 1 } adapter.onFailure = { failures.append($0) } adapter.refresh() let menu = adapter.menu -require(menu.delegate === adapter && menu.items.count == 9, "real shared flat menu") -require(menu.items[6].title == "检查更新…" && menu.items[6].keyEquivalent == "g" - && menu.items[6].keyEquivalentModifierMask == [.option], "update entry shown with shortcut") -require(menu.items[7].keyEquivalent == "," && menu.items[7].keyEquivalentModifierMask == [.option], "settings shortcut shown") -require(menu.items[8].keyEquivalent == "a" && menu.items[8].keyEquivalentModifierMask == [.option], "about shortcut shown") +require(menu.delegate === adapter && menu.items.count == 14, "real shared flat menu") +require(menu.items[6].title == "显示学习区" && menu.items[6].keyEquivalent == "l" + && menu.items[6].keyEquivalentModifierMask == [.option] && menu.items[6].state == .off, "learning toggle with shortcut") +require(menu.items[7].title == "练习…" && menu.items[7].keyEquivalent == "k" + && menu.items[7].keyEquivalentModifierMask == [.option], "practice entry shown with shortcut") +require(menu.items[8].title == "符号与表情…" && menu.items[8].keyEquivalent == "e" + && menu.items[8].keyEquivalentModifierMask == [.control, .option], "symbols shortcut is ctrl+option+e") +require(menu.items[9].title == "使用指南…" && menu.items[9].keyEquivalent.isEmpty, "guide entry without shortcut") +require(menu.items[11].title == "检查更新…" && menu.items[11].keyEquivalent == "g" + && menu.items[11].keyEquivalentModifierMask == [.option], "update entry shown with shortcut") +require(menu.items[12].keyEquivalent == "," && menu.items[12].keyEquivalentModifierMask == [.option], "settings shortcut shown") +require(menu.items[13].keyEquivalent == "a" && menu.items[13].keyEquivalentModifierMask == [.option], "about shortcut shown") let parent = menu.items[0] require(parent.submenu == nil && parent.action == InputStateMenu.action && parent.tag == 1, "mode row is a clickable command") require(parent.title == "混输模式", "mode row title shows the current mode") @@ -103,15 +110,30 @@ LocalAcceptanceEvidence.record("menu.recovery", expected: "解除私有锁后可 var settings = 0 var about = 0 var updates = 0 +var learning = 0 +var practice = 0 +var symbols = 0 +var guide = 0 adapter.onCheckUpdates = { updates += 1 } adapter.onSettings = { settings += 1 } adapter.onAbout = { about += 1 } -menu.performActionForItem(at: 6) -menu.performActionForItem(at: 7) -menu.performActionForItem(at: 8) +adapter.onToggleLearning = { learning += 1 } +adapter.onPractice = { practice += 1 } +adapter.onSymbols = { symbols += 1 } +adapter.onGuide = { guide += 1 } +for index in [6, 7, 8, 9, 11, 12, 13] { menu.performActionForItem(at: index) } +require(learning == 1 && practice == 1 && symbols == 1 && guide == 1, + "learning, practice, symbols and guide route through preview callbacks") require(updates == 1 && settings == 1 && about == 1, "update, settings and about route through preview callbacks") -require(menu.items[6].title == "检查更新…" && menu.items[7].title == "设置" && menu.items[8].title == "关于", "user action labels") -LocalAcceptanceEvidence.record("menu.routes", expected: "更新/设置/关于各触发一次回调", actual: ["titles": [menu.items[6].title, menu.items[7].title, menu.items[8].title], "updates_calls": updates, "settings_calls": settings, "about_calls": about]) +require(menu.items[6].title == "显示学习区" && menu.items[7].title == "练习…" + && menu.items[8].title == "符号与表情…" && menu.items[9].title == "使用指南…" + && menu.items[11].title == "检查更新…" && menu.items[12].title == "设置…" + && menu.items[13].title == "关于", "fixed action labels and order") +LocalAcceptanceEvidence.record("menu.routes", expected: "学习/练习/符号/指南/更新/设置/关于各触发一次回调", + actual: ["titles": [6, 7, 8, 9, 11, 12, 13].map { menu.items[$0].title }, + "learning_calls": learning, "practice_calls": practice, + "symbols_calls": symbols, "guide_calls": guide, + "updates_calls": updates, "settings_calls": settings, "about_calls": about]) require((try? Data(contentsOf: dictionary)) == dictionaryBefore, "no learned or committed text") LocalAcceptanceEvidence.record("menu.dictionary", expected: "模式/状态操作不增加私有词库学习", actual: ["unchanged": (try? Data(contentsOf: dictionary)) == dictionaryBefore]) print("PASS preview-menu: real session/shared flat menu/private configuration, click cycle, state toggles, external preferences, keyboard, failure recovery and no learning") diff --git a/platforms/macos/tests/settings-presentation.swift b/platforms/macos/tests/settings-presentation.swift index 8f03ecba..3402f0e6 100644 --- a/platforms/macos/tests/settings-presentation.swift +++ b/platforms/macos/tests/settings-presentation.swift @@ -54,7 +54,7 @@ LocalAcceptanceEvidence.record("settings.count", expected: "接受3/9/7,拒绝 panel.selectSectionForTesting(2) guard panel.visibleOptionKeysForTesting.contains("hotkey.toggle_wubi_learning") else { fail("design learning shortcut placement") } for (key, title) in [("hotkey.toggle_width", "⌥F"), ("hotkey.toggle_wubi_learning", "⌥L"), ("hotkey.toggle_english", "⇧"), - ("hotkey.open_settings", "⌥,"), ("hotkey.open_about", "⌥A"), ("hotkey.open_symbols", "⌥S")] { + ("hotkey.open_settings", "⌥,"), ("hotkey.open_about", "⌥A"), ("hotkey.open_symbols", "⌃⌥E")] { panel.rowForTesting(key)?.reload() // The visible show path reloads rows after prepare. guard let row = panel.rowForTesting(key), row.shortcutTitleForTesting == title else { fail("shortcut title " + key + " actual=" + (panel.rowForTesting(key)?.shortcutTitleForTesting ?? "missing") + " configured=" + (ImeConfig.string(key) ?? "missing")) } } diff --git a/scripts/ui-snapshots.sh b/scripts/ui-snapshots.sh index e47e452e..bceb6409 100755 --- a/scripts/ui-snapshots.sh +++ b/scripts/ui-snapshots.sh @@ -48,7 +48,9 @@ build() { local sources=("$@") local stamp="$CACHE/$name.sha" local hash - hash=$(cat "$TMP/main.swift" "${sources[@]}" | shasum -a 256 | awk '{print $1}') + # 缓存键必须包含 FFI 静态库:Schema 默认值(如快捷键)变化只体现在 Rust 侧, + # 只哈希 Swift 源码会让快照继续复用旧二进制(2026-11-15 实测)。 + hash=$(cat "$TMP/main.swift" "${sources[@]}" target/release/libime_ffi.a | shasum -a 256 | awk '{print $1}') if [[ -x "$CACHE/$name" && -f "$stamp" && "$(cat "$stamp")" == "$hash" ]]; then echo "▶ ${name}:源码未变,复用上次编译" return 0 From ceb29817da8d06bd16054890ab1d0b54fd6286da Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Sun, 15 Nov 2026 02:45:59 +0800 Subject: [PATCH 2/4] fix(menu): use option+c for symbols to keep the single-modifier scheme Signed-off-by: wangzhizhou <824219521@qq.com> --- HANDOFF.md | 2 +- crates/ime-config/src/schema.rs | 4 ++-- .../288-input-menu-order-and-symbols-hotkey.md | 12 ++++++------ ...5\206\263\347\255\226\350\256\260\345\275\225.md" | 9 +++++---- ...1\205\215\347\275\256\346\236\266\346\236\204.md" | 4 ++-- platforms/macos/Sources/InputController.swift | 2 +- platforms/macos/tests/controller-menu.swift | 4 ++-- platforms/macos/tests/preview-menu.swift | 4 ++-- platforms/macos/tests/settings-presentation.swift | 2 +- 9 files changed, 22 insertions(+), 21 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index d0214de5..8d6c08ce 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -138,7 +138,7 @@ make macos-offline-tests - 2026-11-15 / Claude Code(air):**输入法菜单固定顺序 + 符号快捷键(ADR-360,报告 288)**。菜单动作区固定为 「学习区 → 练习 → 符号与表情 → 使用指南 → 检查更新 → 设置 → 关于」,正式/预览共用 `InputMenuAction.sections`; - 新增学习区/练习/符号入口;`hotkey.open_symbols` 默认 `alt+s` → `ctrl+alt+e`(已有显式配置不覆盖)。 + 新增学习区/练习/符号入口;`hotkey.open_symbols` 默认 `alt+s` → `alt+c`(C = Character;保持单修饰键队形,已有显式配置不覆盖)。 三处菜单探针与设置页断言同步;快照缓存键加入 `libime_ffi.a`。 - 2026-11-15 / Claude Code(air):**交互式面板验收修复(报告 287)**。`make macos-dev` 在 Claude Code 的 非 TTY stdin 下 `selectors.register` 崩溃;改为非 TTY 时只轮询子进程、关闭窗口即结束。交互会话中真实数据 diff --git a/crates/ime-config/src/schema.rs b/crates/ime-config/src/schema.rs index 4d5c3c17..11401e01 100644 --- a/crates/ime-config/src/schema.rs +++ b/crates/ime-config/src/schema.rs @@ -296,9 +296,9 @@ pub static SCHEMA: &[OptionSpec] = &[ key: "hotkey.open_symbols", value_type: ValueType::String, enum_labels_zh: &[], - default: "\"ctrl+alt+e\"", + default: "\"alt+c\"", title_zh: "符号与 emoji", - doc_zh: "打开符号 / emoji 选择面板(分类与中英文搜索);默认 ctrl+alt+e(E = emoji;双修饰键避开应用快捷键,⌥E 是死键不可单用),留空 = 关闭。反引号 ` 可在候选窗中快速打开分类", + doc_zh: "打开符号 / emoji 选择面板(分类与中英文搜索);默认 alt+c(C = Character/字符;单修饰键与其它状态键保持同队形,⌥E 是死键不可单用),留空 = 关闭。反引号 ` 可在候选窗中快速打开分类", scope: Scope::Hotkey, }, OptionSpec { diff --git a/docs/reviews/288-input-menu-order-and-symbols-hotkey.md b/docs/reviews/288-input-menu-order-and-symbols-hotkey.md index 698cca7b..9cdee132 100644 --- a/docs/reviews/288-input-menu-order-and-symbols-hotkey.md +++ b/docs/reviews/288-input-menu-order-and-symbols-hotkey.md @@ -1,6 +1,6 @@ # 288 — 输入法菜单固定顺序 + 符号面板快捷键调整(ADR-360) -> 2026-11-15。人工验收反馈:菜单缺「练习」入口、顺序无策略、`⌥S` 易冲突。 +> 2026-11-15。人工验收反馈:菜单缺「练习」入口、顺序无策略、`⌥S` 易冲突,且默认键要保持单修饰键队形。 ## 1. 问题 @@ -36,16 +36,16 @@ |---|---|---| | 显示学习区 | `hotkey.toggle_wubi_learning`(默认 ⌥L) | 与 ⌥L 同源,切换显示偏好并打勾,不提交当前组合 | | 练习… | `hotkey.open_practice`(默认 ⌥K) | 打开离线练习窗口 | -| 符号与表情… | `hotkey.open_symbols`(默认 **⌃⌥E**) | 先提交当前组合,再把选中符号插入原插入点;预览里插入预览输入区 | +| 符号与表情… | `hotkey.open_symbols`(默认 **⌥C**) | 先提交当前组合,再把选中符号插入原插入点;预览里插入预览输入区 | | 使用指南… | 无 | 打开完整使用指南 | ## 4. 快捷键 -`hotkey.open_symbols` 默认 `alt+s` → **`ctrl+alt+e`**: +`hotkey.open_symbols` 默认 `alt+s` → **`alt+c`**: -- E = emoji,保留语义首字母; +- C = Character(字符/符号),与其它状态键保持「单 ⌥ + 字母」的同一队形; - `⌥S` 与应用快捷键冲突(用户反馈);`⌥E` 在 ADR-209 已确认是死键,不能单独使用; -- 双修饰键同时避开「仅 ⌥ + 字母」的应用快捷键面; +- 用户明确要求保持单修饰键队形,故不采用双修饰键; - **已有显式配置不静默覆盖**:老用户文件里写了 `alt+s` 就继续用,设置页显示实际值。 ## 5. 验证 @@ -53,7 +53,7 @@ - `input-menu.swift`:断言分组与标题顺序、只有「使用指南」无快捷键; - `controller-menu.swift`:正式宿主冷菜单 **14 项**、两条分隔线、位置与快捷键全部核对; - `preview-menu.swift`:预览菜单同序,7 个动作各触发一次回调; -- `settings-presentation.swift`:设置页显示 `⌃⌥E`; +- `settings-presentation.swift`:设置页显示 `⌥C`; - UI 快照:设置快捷键页已重新渲染;快照缓存键加入 `libime_ffi.a`,Schema 默认值变化不再复用旧二进制; - `make macos-offline-tests` 68 文件 / 337 例 PASS;`make verify` PASS。 diff --git "a/docs/\345\206\263\347\255\226\350\256\260\345\275\225.md" "b/docs/\345\206\263\347\255\226\350\256\260\345\275\225.md" index ec464a5d..df1b7c48 100644 --- "a/docs/\345\206\263\347\255\226\350\256\260\345\275\225.md" +++ "b/docs/\345\206\263\347\255\226\350\256\260\345\275\225.md" @@ -3964,7 +3964,7 @@ - **关联**:ADR-358、报告 284/285、`docs/standards/wubi-decomposition.md`、`d2-glyph-aliases.tsv`、 `wubi86-root-inventory.tsv`、`data/sources.toml`。 -### ADR-360:输入法菜单固定顺序;符号面板快捷键改为 ⌃⌥E +### ADR-360:输入法菜单固定顺序;符号面板快捷键改为 ⌥C - **背景**:用户人工验收时反馈 ① 输入法菜单缺「练习」「使用指南」入口(预览菜单尤其明显), ② 菜单项顺序没有策略、后续迭代不可预期,③ 符号与表情的 `⌥S` 容易与已有快捷键冲突。 @@ -3976,12 +3976,13 @@ 新增入口只能追加到所属组末尾并更新 `input-menu.swift` / `controller-menu.swift` / `preview-menu.swift` 三处探针。 3. 动作区新增「显示学习区」(勾选态,与 ⌥L 同源)、「练习…」(与 ⌥K 同源)、「符号与表情…」 (与快捷键同源);「使用指南…」保留在工具组。 - 4. `hotkey.open_symbols` 默认从 `alt+s` 改为 **`ctrl+alt+e`**:E = emoji,双修饰键避开应用级 - `⌥+字母` 快捷键;ADR-209 已列 `⌥E` 为死键,不能单用。**已有显式配置不静默覆盖**。 + 4. `hotkey.open_symbols` 默认从 `alt+s` 改为 **`alt+c`**:C = Character(字符/符号), + 与其它状态键保持「单 ⌥ + 字母」同一队形;`⌥S` 与应用快捷键冲突,`⌥E` 按 ADR-209 是死键。 + **已有显式配置不静默覆盖**。 - **理由**:状态类每次打开菜单都会被看,放最前;学习与工具是日常高频,放中间;应用级操作最少用,放最后。 固定顺序让验收和文档可以直接断言,不靠记忆。 - **结果**:正式菜单 14 项(5 状态 + 2 分隔线 + 4 工具 + 3 应用);预览菜单与正式一致; - 设置页显示 `⌃⌥E`;三处菜单探针与 `settings-presentation` 全部通过。 + 设置页显示 `⌥C`;三处菜单探针与 `settings-presentation` 全部通过。 - **边界**:不改 IMK 前 5 行的命令语义;不新增配置文件;不迁移用户已显式设置的快捷键; 预览的「符号与表情」插入到预览输入区,不经过系统 IME。 - **状态**:采纳(2026-11-15)。 diff --git "a/docs/\351\205\215\347\275\256\346\236\266\346\236\204.md" "b/docs/\351\205\215\347\275\256\346\236\266\346\236\204.md" index 2f09f57b..3bf8b753 100644 --- "a/docs/\351\205\215\347\275\256\346\236\266\346\236\204.md" +++ "b/docs/\351\205\215\347\275\256\346\236\266\346\236\204.md" @@ -150,7 +150,7 @@ open_settings = "alt+," # 打开设置 | 组合键 | none(与菜单项提 open_about = "alt+a" # 关于(A = About)| 组合键 | none(与菜单项提示同源) open_update = "alt+g" # 检查更新(G = GitHub)| 组合键 | none(与菜单项提示同源) open_practice = "alt+k" # 练习(K = 练习)| 组合键 | none -open_symbols = "ctrl+alt+e" # 符号与表情(E = emoji;出双修饰键避开应用快捷键)| 组合键 | none +open_symbols = "alt+c" # 符号与表情(C = Character/字符)| 组合键 | none [input] punctuation = "chinese" # 标点:chinese = 中文 | english = 英文(⌥P 可切换) @@ -264,7 +264,7 @@ crates/ Schema 增加 input.punctuation = "chinese" | "english"(默认 chinese)和 input.full_width = "half" | "full"(默认半角);默认值仍只由 Schema 定义,配置注释自动生成。中英快捷键(Shift)是会话内临时切换,不写配置;**全/半角快捷键自 ADR-305 起与标点/字形一致,写配置并持久化**(旧版布尔值 `false/true` 读取时迁移为 `half/full`,不改写用户文件)。hotkey.toggle_mode 由宿主消费,正式模式切换经 ime_config_set 保存。 -2026-11-15(ADR-360):`hotkey.open_symbols` 默认从 `alt+s` 改为 `ctrl+alt+e`(E = emoji;⌥S 与应用快捷键冲突,⌥E 又是死键,故用双修饰键)。已有显式配置不静默覆盖;设置页显示 `⌃⌥E`。输入法菜单动作区同时固定为「学习区 → 练习 → 符号与表情 → 使用指南 → 检查更新 → 设置 → 关于」。 +2026-11-15(ADR-360):`hotkey.open_symbols` 默认从 `alt+s` 改为 `alt+c`(C = Character/字符;⌥S 与应用快捷键冲突,⌥E 是死键)。单修饰键与其它状态键保持同一队形;已有显式配置不静默覆盖;设置页显示 `⌥C`。输入法菜单动作区同时固定为「学习区 → 练习 → 符号与表情 → 使用指南 → 检查更新 → 设置 → 关于」。 2026-11-06(ADR-313):新增 `hotkey.open_practice`(默认 `alt+k`),打开离线练习窗口;作用域 `Hotkey`,`none`/空即关闭,与其它快捷键同一套冲突检测;不新增配置文件、不改持久化路径。 diff --git a/platforms/macos/Sources/InputController.swift b/platforms/macos/Sources/InputController.swift index 2216876a..31a98508 100644 --- a/platforms/macos/Sources/InputController.swift +++ b/platforms/macos/Sources/InputController.swift @@ -297,7 +297,7 @@ final class LampyrisInputController: IMKInputController { PracticeWindow.shared.show() } - /// 菜单入口与 ⌥S(改为 ⌃⌥E)同源:先提交当前组合,再把选中的符号插入原插入点。 + /// 菜单入口与 ⌥C(原 ⌥S)同源:先提交当前组合,再把选中的符号插入原插入点。 @objc private func showSymbols() { if SymbolPickerWindow.shared.isVisible { SymbolPickerWindow.shared.close() diff --git a/platforms/macos/tests/controller-menu.swift b/platforms/macos/tests/controller-menu.swift index 1d52810a..50e9b45a 100644 --- a/platforms/macos/tests/controller-menu.swift +++ b/platforms/macos/tests/controller-menu.swift @@ -77,8 +77,8 @@ require(coldMenu.items[6].title == "显示学习区" && coldMenu.items[6].keyEqu "learning toggle shows configured shortcut and current check state") require(coldMenu.items[7].title == "练习…" && coldMenu.items[7].keyEquivalent == "k" && coldMenu.items[7].keyEquivalentModifierMask == [.option], "practice entry shows configured shortcut") -require(coldMenu.items[8].title == "符号与表情…" && coldMenu.items[8].keyEquivalent == "e" - && coldMenu.items[8].keyEquivalentModifierMask == [.control, .option], "symbols entry shows ctrl+option+e") +require(coldMenu.items[8].title == "符号与表情…" && coldMenu.items[8].keyEquivalent == "c" + && coldMenu.items[8].keyEquivalentModifierMask == [.option], "symbols entry shows option+c") require(coldMenu.items[9].title == "使用指南…" && coldMenu.items[9].keyEquivalent == "", "guide entry") require(coldMenu.items[11].title == "检查更新…" && coldMenu.items[11].keyEquivalent == "g" && coldMenu.items[11].keyEquivalentModifierMask == [.option], "update entry shows configured shortcut") diff --git a/platforms/macos/tests/preview-menu.swift b/platforms/macos/tests/preview-menu.swift index 6462aa10..1e09a34c 100644 --- a/platforms/macos/tests/preview-menu.swift +++ b/platforms/macos/tests/preview-menu.swift @@ -44,8 +44,8 @@ require(menu.items[6].title == "显示学习区" && menu.items[6].keyEquivalent && menu.items[6].keyEquivalentModifierMask == [.option] && menu.items[6].state == .off, "learning toggle with shortcut") require(menu.items[7].title == "练习…" && menu.items[7].keyEquivalent == "k" && menu.items[7].keyEquivalentModifierMask == [.option], "practice entry shown with shortcut") -require(menu.items[8].title == "符号与表情…" && menu.items[8].keyEquivalent == "e" - && menu.items[8].keyEquivalentModifierMask == [.control, .option], "symbols shortcut is ctrl+option+e") +require(menu.items[8].title == "符号与表情…" && menu.items[8].keyEquivalent == "c" + && menu.items[8].keyEquivalentModifierMask == [.option], "symbols shortcut is option+c") require(menu.items[9].title == "使用指南…" && menu.items[9].keyEquivalent.isEmpty, "guide entry without shortcut") require(menu.items[11].title == "检查更新…" && menu.items[11].keyEquivalent == "g" && menu.items[11].keyEquivalentModifierMask == [.option], "update entry shown with shortcut") diff --git a/platforms/macos/tests/settings-presentation.swift b/platforms/macos/tests/settings-presentation.swift index 3402f0e6..3700e24b 100644 --- a/platforms/macos/tests/settings-presentation.swift +++ b/platforms/macos/tests/settings-presentation.swift @@ -54,7 +54,7 @@ LocalAcceptanceEvidence.record("settings.count", expected: "接受3/9/7,拒绝 panel.selectSectionForTesting(2) guard panel.visibleOptionKeysForTesting.contains("hotkey.toggle_wubi_learning") else { fail("design learning shortcut placement") } for (key, title) in [("hotkey.toggle_width", "⌥F"), ("hotkey.toggle_wubi_learning", "⌥L"), ("hotkey.toggle_english", "⇧"), - ("hotkey.open_settings", "⌥,"), ("hotkey.open_about", "⌥A"), ("hotkey.open_symbols", "⌃⌥E")] { + ("hotkey.open_settings", "⌥,"), ("hotkey.open_about", "⌥A"), ("hotkey.open_symbols", "⌥C")] { panel.rowForTesting(key)?.reload() // The visible show path reloads rows after prepare. guard let row = panel.rowForTesting(key), row.shortcutTitleForTesting == title else { fail("shortcut title " + key + " actual=" + (panel.rowForTesting(key)?.shortcutTitleForTesting ?? "missing") + " configured=" + (ImeConfig.string(key) ?? "missing")) } } From 44c30ff326a5c2fe6bdd0a5d3c3e4c0dd60adb53 Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Sun, 15 Nov 2026 02:51:23 +0800 Subject: [PATCH 3/4] fix(panel-acceptance): do not claim real files unchanged when WARN fired Signed-off-by: wangzhizhou <824219521@qq.com> --- platforms/macos/tests/run-panel-interactive.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/platforms/macos/tests/run-panel-interactive.py b/platforms/macos/tests/run-panel-interactive.py index 2603338b..38ace6b5 100644 --- a/platforms/macos/tests/run-panel-interactive.py +++ b/platforms/macos/tests/run-panel-interactive.py @@ -68,7 +68,9 @@ def stamp(path): '不是预览写入):' + '、'.join(path.name for path in changed), flush=True) if completed or process.returncode == 0: shutil.rmtree(work) - print('PASS interactive cleanup: owned process exited; real files unchanged', flush=True) + detail = ('preview used its private root; real files changed externally (see WARN)' + if changed else 'real files unchanged') + print(f'PASS interactive cleanup: owned process exited; {detail}', flush=True) else: raise RuntimeError('interactive session timed out or failed') finally: From a3e0b0eae9b291c9dd758d0e61c54c77c21a87a9 Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Sun, 15 Nov 2026 02:52:27 +0800 Subject: [PATCH 4/4] docs: note real config change observed during interactive panel test Signed-off-by: wangzhizhou <824219521@qq.com> --- docs/reviews/287-interactive-panel-non-tty.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/reviews/287-interactive-panel-non-tty.md b/docs/reviews/287-interactive-panel-non-tty.md index b73e02fb..8e66f730 100644 --- a/docs/reviews/287-interactive-panel-non-tty.md +++ b/docs/reviews/287-interactive-panel-non-tty.md @@ -31,8 +31,10 @@ Claude Code 的 `!` / `Bash` 环境里 stdin 不是可 `select()` 的终端( ## 4. 关于真实数据 -首次崩溃后的手工测试期间,真实 `user-dictionary.tsv` 从 2,046 行变为 2,060 行(新增 14 条五笔词条), -`config.toml` 未变。该变化与已装输入法在其它窗口的正常学习一致,**未回滚**(属于用户真实数据)。 +手工验收期间,真实 `user-dictionary.tsv` 从 2,046 行变为 2,060 行(新增 14 条五笔词条), +`config.toml` 的 `candidate.show_wubi_learning` 变为 `true`(02:20)。自动化隔离探针(同样会 +`ime_config_init(私有根)` 并 `setString("mode", …)`)仍 PASS:真实文件在预览进程外未被写入。 +因此这些变化与已装输入法在其它窗口的正常学习/⌥L 切换一致,**未回滚**(属于用户真实数据)。 ## 5. 关联