Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions devel/0408.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# [0408] PDF 阅读器支持 macOS 触控板捏合缩放

## 1 相关文档

- [dddd.md](dddd.md) - 任务文档模板

## 2 任务相关的代码文件

- `src/Plugins/Qt/qt_pdf_reader_widget.cpp`
- `src/Plugins/Qt/qt_pdf_reader_widget.hpp`

## 3 如何测试

### 3.1 确定性测试(单元测试)

```bash
xmake run qt_pdf_reader_widget_test
```

新增 TDD 测试:
- `test_pinchZoomIn`:验证捏合放大后 zoom factor 正确增加
- `test_pinchZoomBlocksRender`:验证捏合过程中 `renderCallCount` 不增加(`blockRender_` 生效),手势结束后才触发重渲染

### 3.2 非确定性测试(文档验证)

1. 打开 Mogan STEM,打开一个 PDF 文件。
2. 在 PDF 阅读区域,使用 macOS 触控板双指捏合手势进行缩放。
3. 确认捏合过程中页面平滑跟随缩放比例变化,UI 不卡顿。
4. 确认松手后页面重新渲染为清晰版本(防抖触发)。
5. 确认缩放到极端比例(最小/最大)时,缩放被正确限制在 `12% ~ 800%` 范围内。
6. 对比修复前:捏合时会反复触发重渲染,导致明显卡顿。

## 4 如何提交

提交前执行以下最少步骤:

```bash
xmake run qt_pdf_reader_widget_test
xmake b stem
```

## 5 What

为 Mogan STEM 的 PDF 阅读器添加 macOS 触控板捏合(Pinch)缩放支持,并针对流畅性做深度优化:

1. **macOS 原生手势优先**:在 macOS 上直接处理 `QNativeGestureEvent::ZoomNativeGesture`,绕过 Qt `QPinchGesture` 框架的中间层开销
2. **双平台兼容**:保留 `QPinchGesture` 作为非 macOS 平台(触摸屏等)的 fallback
3. **高频事件节流(Throttle)**:使用 `QElapsedTimer` 限制 gesture 事件处理频率最多 ~60fps(16ms 间隔),避免 macOS trackpad 高频事件(120Hz+)堆积导致主线程阻塞
4. **UI 刷新防抖(Debounce)**:使用 `QTimer`(50ms)限制 combo box 百分比更新最多 20fps,避免 `setCurrentText` 高频重绘造成卡顿
5. **零渲染策略**:捏合过程中仅更新 `zoomFactor_`,完全不触碰页面布局、pixmap 或 MuPDF 渲染
6. **手势结束统一刷新**:`EndNativeGesture` / `GestureFinished` / `GestureCanceled` 时统一调用 `setZoomFactor()` 触发防抖重渲染
7. 缩放比例仍受 `MIN_ZOOM = 0.12` 和 `MAX_ZOOM = 8.0` 约束

## 6 Why

此前 PDF 阅读器在 macOS 上仅支持:
- 工具栏按钮点击缩放
- `Ctrl + 滚轮` 缩放(对应鼠标)
- 快捷键 `Ctrl + +/-` 缩放

缺少对触控板原生捏合手势的支持,这与 macOS 用户的直觉操作习惯不符。直接实现时,若在每次 `ScaleFactorChanged` 都调用 `setZoomFactor()`(内部会启动 200ms 防抖定时器并排队 `rebuildPages()`),会导致捏合过程中页面反复重渲染,造成明显卡顿。

## 7 How

实现思路(参考 Okular `blockPixmapsRequest` + `zoomWithFixedCenter` 模式):

### 7.1 Okular 流畅性秘诀

Okular 的 `zoomWithFixedCenter` 核心逻辑:
1. `d->blockPixmapsRequest = true` —— 阻止耗时 pixmap 请求
2. 更新 zoom factor + 触发 `updateZoom()` —— 实时更新 layout(页面尺寸变化)
3. `d->blockPixmapsRequest = false` —— 恢复
4. `paintEvent` 自动将旧 pixmap 绘制到新目标矩形 —— 零开销视觉缩放

### 7.2 我的适配方案(QLabel + setScaledContents)

由于我用 QLabel 而非自定义 paintEvent,采用等效策略:
1. **Gesture 开始**:对所有 label `setScaledContents(true)`,启用 QLabel 内部自动 pixmap 缩放
2. **Gesture 更新**:只更新 `zoomFactor_` + `setFixedSize()`(layout 变化),QLabel 自动缩放现有 pixmap 显示
3. **Gesture 结束**:`setScaledContents(false)`,触发防抖重渲染,用 MuPDF 生成清晰 pixmap

### 7.3 blockRender_ 机制

添加 `bool blockRender_` 标志,仿照 Okular 的 `blockPixmapsRequest`:
- `rebuildPages()` 中检查 `blockRender_`,为 true 时只做第一轮 layout(`setFixedSize`)后直接返回,跳过第二轮的 `renderPageToLabel()`
- `renderPageToLabel()` 开头增加 `renderCallCount_++` + `cout` 日志(`#ifdef LIII_DEBUG`),供测试和调试验证

### 7.4 移除 throttle/debounce

之前尝试的 `QElapsedTimer` 16ms throttle 和 `QTimer` 50ms debounce 已被移除。Okular 没有这些机制,其流畅性完全来自 `blockPixmapsRequest`。多余的时间控制反而会造成:
- 事件丢弃导致缩放不跟手
- timer 堆积导致 gesture 结束后延迟

### 7.5 双平台事件处理

- **macOS**:优先处理 `QNativeGestureEvent`(`BeginNativeGesture` / `ZoomNativeGesture` / `EndNativeGesture`)
- **非 macOS**:处理 `QGestureEvent`(`QPinchGesture`)
- 两者共用同一套 `blockRender_` + `setScaledContents` 机制
- Gesture 开始时 `scroller_->stop()`,避免 QScroller 与 pinch 冲突

### 7.6 手势安全超时

添加 `gestureSafetyTimer_`(500ms 单发定时器),解决 **EndNativeGesture 丢失导致 blockRender_ 永久为 true** 的临界问题:
- 每次 gesture update(`ZoomNativeGesture` / `ScaleFactorChanged`)重启定时器
- 正常 `EndNativeGesture` / `GestureFinished` 到达时停止定时器
- 若 500ms 内未收到结束事件(如应用失焦、触控板异常),定时器触发 `finishPinchGesture()` 自动恢复状态

### 7.7 UI 刷新优化

捏合过程中不再调用 `updateZoomDisplay()`(避免 `QComboBox` 120Hz 重绘),仅在手势结束时统一更新百分比显示。

### 7.8 TDD 测试

- `test_pinchZoomIn`:验证 zoom factor 正确计算
- `test_pinchZoomBlocksRender`:验证 5 次 gesture update 过程中 `renderCallCount` 不变,手势结束后才增加
Loading
Loading