|
| 1 | +# [0120] 在 Mogan STEM 中实现 PDF 阅读功能 |
| 2 | + |
| 3 | +## 1 相关文档 |
| 4 | +- [dddd.md](dddd.md) - 任务文档模板 |
| 5 | +- [216_22.md](216_22.md) - PDF 预览控件悬停按钮修复(`QTPdfPreviewWidget` 文件缓存、HTTP 条件请求) |
| 6 | +- [216_25.md](216_25.md) - 模板页面缩略图缓存与 UI 优化(`QTPdfPreviewWidget` 会话级验证、即时加载缓存) |
| 7 | + |
| 8 | +## 2 任务相关的代码文件 |
| 9 | +- `src/Plugins/Qt/qt_utilities.cpp` |
| 10 | +- `src/Plugins/Qt/qt_utilities.hpp` |
| 11 | +- `src/Plugins/Qt/qt_tm_widget.cpp` |
| 12 | +- `src/Plugins/Qt/qt_tm_widget.hpp` |
| 13 | +- `TeXmacs/progs/texmacs/texmacs/tm-files.scm` |
| 14 | +- `tests/Plugins/Qt/qt_pdf_tab_utils_test.cpp` |
| 15 | +- `tests/Plugins/Qt/qt_pdf_preview_widget_test.cpp` |
| 16 | +- `xmake/tests.lua` |
| 17 | + |
| 18 | +## 3 如何测试 |
| 19 | + |
| 20 | +### 3.1 确定性测试(单元测试) |
| 21 | +```bash |
| 22 | +xmake run qt_pdf_tab_utils_test |
| 23 | +xmake run qt_pdf_preview_widget_test |
| 24 | +``` |
| 25 | + |
| 26 | +### 3.2 非确定性测试(文档验证) |
| 27 | +```bash |
| 28 | +# 启动 Mogan STEM,通过 File -> Open 打开 PDF 文件 |
| 29 | +# 验证: |
| 30 | +# 1. PDF 在标签页内打开,而不是外部软件 |
| 31 | +# 2. 可以正常翻页阅读 |
| 32 | +# 3. 窗口大小调整时 PDF 自适应 |
| 33 | +# 4. PDF 标签页和普通文档标签页可以正常切换 |
| 34 | +``` |
| 35 | + |
| 36 | +## 4 如何提交 |
| 37 | + |
| 38 | +提交前执行以下最少步骤: |
| 39 | + |
| 40 | +```bash |
| 41 | +xmake build qt_pdf_tab_utils_test qt_pdf_preview_widget_test stem |
| 42 | +xmake run qt_pdf_tab_utils_test |
| 43 | +xmake run qt_pdf_preview_widget_test |
| 44 | +``` |
| 45 | + |
| 46 | +## 5 What |
| 47 | + |
| 48 | +实现 Mogan STEM 的基础 PDF 阅读功能,使打开 PDF 文件时直接在应用内以标签页形式展示,而非调用外部软件(浏览器/PDF 阅读器)。复用已有的 `QTPdfPreviewWidget`(基于 MuPDF 矢量渲染)。 |
| 49 | + |
| 50 | +1. **C++ 基础设施**:新增 `is_pdf_tab_file()` 工具函数(通过文件后缀检测 PDF);在 `qt_tm_widget_rep` 中集成 `QTPdfPreviewWidget`,实现编辑器/启动页/PDF 阅读器三种 central widget 模式的切换。 |
| 51 | +2. **Scheme 层**:新增 `load-pdf-buffer` 函数,用原始 PDF 路径直接创建 buffer;在所有 PDF 打开入口(`load-document`、`load-buffer-main`、`load-browse-buffer`)进行拦截。 |
| 52 | +3. **C++ 单元测试**:编写 `qt_pdf_tab_utils_test` 和 `qt_pdf_preview_widget_test`,覆盖 URL 解析和 Widget 加载。 |
| 53 | +4. **空格键滚动**:`PDFReaderWidget` 支持按空格键向下滚动约 90% 视口高度,到达底部后自动跳到下一页顶部,提供连贯的阅读体验。 |
| 54 | + |
| 55 | +## 6 Why |
| 56 | + |
| 57 | +目前 Mogan STEM 打开 PDF 文件时走 `buffer-external?` 判定,最终通过 `QDesktopServices::openUrl` 调用外部软件。用户需要在 Mogan STEM 内部直接阅读 PDF,以获得一致的标签页管理体验。已有的 `QTPdfPreviewWidget` 具备 MuPDF 渲染、翻页控制、自适应大小能力,可直接复用。 |
| 58 | + |
| 59 | +## 7 How |
| 60 | + |
| 61 | +### 7.1 核心思路 |
| 62 | + |
| 63 | +参考启动页机制(`tmfs://startup-tab`): |
| 64 | +- Scheme 层:PDF 文件不再走外部打开流程,而是直接用原始路径创建 buffer(buffer 名即 PDF 文件路径)。 |
| 65 | +- C++ 层:`qt_tm_widget_rep::send(SLOT_FILE)` 检测到文件后缀为 `pdf` 时,设置 `pdfTabMode = true`。`sync_startup_tab_mode()` 根据模式决定显示编辑器、启动页或 `QTPdfPreviewWidget`。 |
| 66 | + |
| 67 | +### 7.2 C++ 层修改 |
| 68 | + |
| 69 | +**`qt_utilities.cpp/hpp`** 新增: |
| 70 | +```cpp |
| 71 | +bool is_pdf_tab_file (string file); |
| 72 | +``` |
| 73 | +
|
| 74 | +**`qt_tm_widget.hpp`** 新增成员: |
| 75 | +```cpp |
| 76 | +QTPdfPreviewWidget* pdfViewerWidget; |
| 77 | +bool pdfTabMode; |
| 78 | +QString currentPdfPath; |
| 79 | +``` |
| 80 | + |
| 81 | +**`qt_tm_widget.cpp`** 关键修改: |
| 82 | +- 构造函数初始化 `pdfViewerWidget(nullptr)`、`pdfTabMode(false)`、`currentPdfPath("")`。 |
| 83 | +- 析构函数 `delete pdfViewerWidget`。 |
| 84 | +- `sync_startup_tab_mode()` 增加 `pdfTabMode` 分支:隐藏 editor 和 startup widget,延迟创建 `QTPdfPreviewWidget`,`loadFromFile(currentPdfPath)`,并设置焦点。 |
| 85 | +- `send(SLOT_FILE)`:`pdfTabMode = is_pdf_tab_file(file)`(通过后缀名检测),`currentPdfPath` 直接使用 buffer 路径。 |
| 86 | +- `update_visibility()`:PDF 模式下隐藏所有工具栏,保留 menu、status、tab、title bar。 |
| 87 | + |
| 88 | +### 7.3 Scheme 层修改 |
| 89 | + |
| 90 | +**`TeXmacs/progs/texmacs/texmacs/tm-files.scm`** 新增 `load-pdf-buffer`: |
| 91 | +```scheme |
| 92 | +(tm-define (load-pdf-buffer u) |
| 93 | + (when (not (url-rooted? u)) |
| 94 | + (set! u (url-relative (current-buffer) u))) |
| 95 | + (if (buffer-exists? u) |
| 96 | + (switch-to-buffer u) |
| 97 | + (begin |
| 98 | + (buffer-set u '(document)) |
| 99 | + (buffer-set-title u (url-tail u)) |
| 100 | + (switch-to-buffer u)))) |
| 101 | +``` |
| 102 | + |
| 103 | +**入口拦截**:确保所有打开 PDF 的路径都进入 `load-pdf-buffer`: |
| 104 | +- `load-buffer-main`:在调用 `load-buffer-check-autosave` 之前,若 `(== (url-suffix name) "pdf")` 则跳转 `load-pdf-buffer`。此修改覆盖 `File -> Open`(`open-buffer` -> `load-buffer` -> `load-buffer-main`)。 |
| 105 | +- `load-browse-buffer`:在 `buffer-external?` 判定之前,增加 PDF 分支。 |
| 106 | +- `load-document` / `load-document*`:增加 PDF 分支。 |
| 107 | + |
| 108 | +### 7.4 测试策略(TDD) |
| 109 | + |
| 110 | +**测试 1:`qt_pdf_tab_utils_test.cpp`** |
| 111 | +- `test_is_pdf_tab_file()`:验证后缀为 `.pdf` 的文件路径返回 true,其他返回 false。 |
| 112 | + |
| 113 | +**测试 2:`qt_pdf_preview_widget_test.cpp`** |
| 114 | +- `test_creation()`:验证 Widget 创建后状态正确。 |
| 115 | +- `test_loadFromFile_validPdf()`:加载 `$TEXMACS_PATH/tests/PDF/pdf_1_4_sample.pdf`,验证 `pageCount() > 0`。 |
| 116 | +- `test_loadFromFile_invalidFile()`:加载不存在的文件,验证 `hasError() == true`。 |
| 117 | +- `test_clearPreview()`:验证清除后状态正确。 |
| 118 | + |
| 119 | +### 7.5 注意事项 |
| 120 | + |
| 121 | +- `QTPdfPreviewWidget` 依赖 `QtNetwork`,需在 `xmake/tests.lua` 的测试框架中添加 `"QtNetwork"`。 |
| 122 | +- `gf fix` 会重新格式化整个 `tm-files.scm`,造成巨大 diff,应避免在该文件上运行。 |
| 123 | +- 每个 PDF 文件直接以其路径作为 buffer 名,支持多标签页同时打开不同 PDF。 |
| 124 | +- 关闭 PDF 标签页走正常的 `kill-buffer` 流程。 |
| 125 | + |
| 126 | +### 7.6 `PDFReaderWidget` 空格键滚动 |
| 127 | + |
| 128 | +**实现方式**: |
| 129 | +- 重写 `keyPressEvent(QKeyEvent* event)`,检测 `Qt::Key_Space`。 |
| 130 | +- 滚动距离为 `viewport()->height() * 0.9`(约 90% 视口高度)。 |
| 131 | +- 使用 `QScrollArea::verticalScrollBar()->setValue()` 实现平滑滚动。 |
| 132 | +- 若当前已滚动到底部,则继续滚动到下一页顶部(或直接到底部保持连续阅读)。 |
0 commit comments