|
| 1 | +# ListView / DetailView 焦点状态修复方案(P1/P2/P3)✅ 已完成 |
| 2 | + |
| 3 | +## 实施总结 |
| 4 | + |
| 5 | +✅ **P1 方案已成功实施并验证** |
| 6 | +- **实施时间**:2025-12-29 |
| 7 | +- **修改文件**:`SyncNos/Views/Components/Main/MainListView+FocusManager.swift` |
| 8 | +- **核心改动**:在 `startMouseDownMonitorIfNeeded()` 中添加点击 Detail 区域的焦点强制切换逻辑 |
| 9 | +- **验证结果**:✅ 鼠标点击 Detail 后 List 选中项高亮正确变灰;✅ 点击 List 恢复蓝色;✅ 键盘 ←/→ 导航不受影响 |
| 10 | +- **P2/P3 方案**:无需实施 |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## 背景与目标 |
| 15 | + |
| 16 | +### 现象 |
| 17 | +- **键盘 ←/→ 导航**:List 选中项高亮会在“强调色(蓝色)↔ 灰色”之间正确切换 ✅ |
| 18 | +- **鼠标点击 DetailView**:List 选中项高亮不会变灰,仍保持强调色 ❌ |
| 19 | + |
| 20 | +### 目标行为 |
| 21 | +- 焦点在 **List**:选中项高亮为 **强调色(蓝色)** |
| 22 | +- 焦点在 **Detail**:选中项高亮为 **非激活(灰色)** |
| 23 | + |
| 24 | +## 现状实现盘点(关键文件) |
| 25 | + |
| 26 | +### 焦点/键盘基础设施(已存在) |
| 27 | +- `SyncNos/Views/Components/Main/MainListView+KeyboardMonitor.swift` |
| 28 | + - 处理 ←/→:在 `.list → .detail` 时调用 `focusDetailScrollViewIfPossible(window:)`,内部通过 `window.makeFirstResponder(scrollView.contentView)` 强制切换 firstResponder,因此高亮表现正确。 |
| 29 | +- `SyncNos/Views/Components/Main/MainListView+FocusManager.swift` |
| 30 | + - 通过 `mouseDownMonitor` 监听 `.leftMouseDown`,点击后延迟 `syncNavigationTargetWithFocus()`,但**只做“同步状态”不做“切焦点”**。 |
| 31 | + |
| 32 | +### List 强制拿焦点(导致问题更明显) |
| 33 | +各数据源 `*ListView.swift` 都有类似逻辑: |
| 34 | +- `@FocusState private var isListFocused: Bool` |
| 35 | +- `List(...).focused($isListFocused)` |
| 36 | +- `.onAppear / DataSourceSwitchedToXxx` 时 `isListFocused = true` |
| 37 | + |
| 38 | +典型例子: |
| 39 | +- `SyncNos/Views/AppleBooks/AppleBooksListView.swift` |
| 40 | +- `SyncNos/Views/GoodLinks/GoodLinksListView.swift` |
| 41 | +- `SyncNos/Views/WeRead/WeReadListView.swift` |
| 42 | +- `SyncNos/Views/Dedao/DedaoListView.swift` |
| 43 | +- `SyncNos/Views/Chats/ChatListView.swift` |
| 44 | + |
| 45 | +### Detail 的 ScrollView 可解析(但鼠标点击不会自动成为 firstResponder) |
| 46 | +各数据源 Detail 都用 `EnclosingScrollViewReader` 回传底层 `NSScrollView`: |
| 47 | +- `SyncNos/Views/Components/Keyboard/EnclosingScrollViewReader.swift` |
| 48 | +- e.g. `SyncNos/Views/AppleBooks/AppleBooksDetailView.swift` / `GoodLinksDetailView.swift` / `ChatDetailView.swift` … |
| 49 | + |
| 50 | +## 根因判断(当前最符合现象的解释) |
| 51 | + |
| 52 | +键盘导航之所以“正确”,是因为代码显式调用了: |
| 53 | +- `window.makeFirstResponder(detailScrollView.contentView)` |
| 54 | + |
| 55 | +鼠标点击 Detail 之所以“不变灰”,是因为: |
| 56 | +- Detail 的多数区域(尤其是空白处/仅展示内容的区域)**不会抢 firstResponder** |
| 57 | +- 左侧 List 由于 `.focused($isListFocused)` 长期维持“可聚焦/已聚焦”,于是 **List 仍是 firstResponder** → 选中高亮持续强调色 |
| 58 | + |
| 59 | +## 方案总览(按优先级) |
| 60 | + |
| 61 | +### P1(推荐,最小改动,复用现有基础设施)✅ 已实施并验证成功 |
| 62 | +**思路**:在现有 `mouseDownMonitor` 里补齐“点击 Detail 但 firstResponder 仍在 List”的兜底逻辑:若点击发生在当前 Detail 的 `NSScrollView` 范围内,则主动 `makeFirstResponder(scrollView.contentView)`。 |
| 63 | + |
| 64 | +**改动点** |
| 65 | +- 文件:`SyncNos/Views/Components/Main/MainListView+FocusManager.swift` |
| 66 | + - `startMouseDownMonitorIfNeeded()`: |
| 67 | + - 记录点击坐标 |
| 68 | + - 点击后延迟判断:若点击在 `currentDetailScrollView` 内,但 `syncNavigationTargetWithFocus()` 判断仍是 `.list`,则: |
| 69 | + - `savedMasterFirstResponder = window.firstResponder`(用于 ← 返回) |
| 70 | + - `keyboardNavigationTarget = .detail` |
| 71 | + - `focusDetailScrollViewIfPossible(window:)`(让 List 立刻变“非激活灰”) |
| 72 | + |
| 73 | +**优点** |
| 74 | +- 改动面小,只动一处焦点管理代码 |
| 75 | +- 不需要改每个 DetailView / ListView |
| 76 | +- 与现有键盘 ←/→ 策略一致(统一用 AppKit firstResponder 驱动) |
| 77 | + |
| 78 | +**潜在风险** |
| 79 | +- 需避免覆盖文本输入控件的 firstResponder(例如弹出的输入框)。解决方式:只在 “点击发生在 Detail 区域 + firstResponder 仍旧是 List” 时才强制切换。 |
| 80 | + |
| 81 | +**验证步骤** |
| 82 | +- 手动: |
| 83 | + - 在任意数据源选中一个条目(确保右侧有 Detail) |
| 84 | + - 鼠标点击 Detail 的空白区域/卡片区域:左侧选中高亮应从蓝变灰 |
| 85 | + - 鼠标点击左侧 List:高亮应恢复为蓝 |
| 86 | + - 键盘 →/←:行为应与此前一致 |
| 87 | +- 构建: |
| 88 | + - `xcodebuild -scheme SyncNos -configuration Debug -destination "platform=macOS" build` |
| 89 | + |
| 90 | +--- |
| 91 | + |
| 92 | +### P2(更"SwiftUI 内聚",但改动面更大)❌ 无需实施 |
| 93 | +**思路**:不使用全局 `mouseDownMonitor`,改为在 `detailColumn` 根部叠加一个 `NSViewRepresentable` 的透明点击捕获层(或用 `NSClickGestureRecognizer`),只在 detail 视图树内处理点击 → 切焦点到 `currentDetailScrollView`。 |
| 94 | + |
| 95 | +**改动点(示例方向)** |
| 96 | +- 新增组件:`DetailClickFocusCatcher.swift`(NSViewRepresentable) |
| 97 | +- 在 `MainListView+DetailViews.swift` 的 `detailColumn` 外层包一层 `.background(DetailClickFocusCatcher(...))` |
| 98 | + |
| 99 | +**优点** |
| 100 | +- 不依赖全局事件监听器 |
| 101 | +- 只在 detail 区域内工作,语义更清晰 |
| 102 | + |
| 103 | +**缺点/风险** |
| 104 | +- 需要处理手势穿透、与内部控件点击冲突等边界 |
| 105 | +- 实现复杂度略高 |
| 106 | + |
| 107 | +**验证** |
| 108 | +- 同 P1 |
| 109 | + |
| 110 | +--- |
| 111 | + |
| 112 | +### P3(破坏性更强:移除 List 的 FocusState 强制逻辑)❌ 无需实施 |
| 113 | +**思路**:彻底去掉各 `*ListView` 的 `.focused($isListFocused)` + `isListFocused = true`,只靠系统/firstResponder 自然流转(配合已有键盘监控)。 |
| 114 | + |
| 115 | +**优点** |
| 116 | +- 从源头减少“List 抢焦点”问题 |
| 117 | + |
| 118 | +**缺点/风险** |
| 119 | +- 可能导致数据源切换后 List 不再自动获得焦点,影响 ↑/↓ 导航体验 |
| 120 | +- 改动面大(5 个 ListView),需要更充分回归测试 |
| 121 | + |
| 122 | +**验证** |
| 123 | +- 除 P1 验证项外,还需要验证: |
| 124 | + - 切换数据源后,↑/↓ 能直接在 List 中工作 |
| 125 | + - 菜单命令/快捷键相关交互不受影响 |
| 126 | + |
| 127 | +## 落地顺序建议 |
| 128 | +- **先做 P1**(最小改动、最快验证) |
| 129 | +- 若仍有遗漏场景(例如某些 Detail 控件点击后仍不变灰),再考虑 **P2** 细化点击判定 |
| 130 | +- 若确认 `.focused($isListFocused)` 是长期副作用源头,再评估 **P3**(破坏性较强) |
| 131 | + |
| 132 | + |
0 commit comments