Skip to content

Commit b64c6f9

Browse files
da-liiiclaude
andauthored
[0120] 在 Mogan STEM 中初步实现 PDF 阅读功能 (#3351)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d26cd0f commit b64c6f9

15 files changed

Lines changed: 1246 additions & 11 deletions

TeXmacs/progs/texmacs/texmacs/tm-files.scm

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1571,7 +1571,6 @@
15711571
(or (url-rooted-web? u)
15721572
(not (in? (url-root u) (list "tmfs" "file" "default" "blank" "ramdisc")))
15731573
(file-of-format? u "image")
1574-
(file-of-format? u "pdf")
15751574
(file-of-format? u "postscript")
15761575
(file-of-format? u "generic")))
15771576

@@ -1580,6 +1579,18 @@
15801579
(set! u (url-relative (current-buffer) u)))
15811580
(open-url u))
15821581

1582+
(tm-define (load-pdf-buffer u)
1583+
(when (not (url-rooted? u))
1584+
(set! u (url-relative (current-buffer) u)))
1585+
(if (buffer-exists? u)
1586+
(switch-to-buffer u)
1587+
(begin
1588+
(buffer-set u '(document))
1589+
(buffer-set-title u (url->system (url-tail u)))
1590+
(switch-to-buffer u)))
1591+
(buffer-notify-recent u)
1592+
(remember-file-dialog-directory u))
1593+
15831594
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
15841595
;; Loading buffers
15851596
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1679,7 +1690,9 @@
16791690
(if (current-buffer)
16801691
(set! name (url-relative (current-buffer) name))
16811692
(set! name (url-append (url-pwd) name))))
1682-
(load-buffer-check-autosave name opts))
1693+
(if (== (url-suffix name) "pdf")
1694+
(load-pdf-buffer name)
1695+
(load-buffer-check-autosave name opts)))
16831696

16841697
;; The load flowgraph:
16851698
;; load-buffer
@@ -1705,6 +1718,8 @@
17051718
(tm-define (load-browse-buffer name)
17061719
(:synopsis "Load a buffer or switch to it if already open")
17071720
(cond ((buffer-exists? name) (switch-to-buffer name))
1721+
((== (url-suffix name) "pdf")
1722+
(load-pdf-buffer name))
17081723
((and (buffer-external? name)
17091724
(!= (url-suffix name) "tm")
17101725
(!= (url-suffix name) "tmu"))
@@ -1800,13 +1815,21 @@
18001815
(:argument u smart-file "File name")
18011816
(:default u (propose-name-buffer))
18021817
(when (not (url-none? u))
1803-
(if (window-per-buffer?) (load-buffer-in-new-window u) (load-buffer u))))
1818+
(if (== (url-suffix u) "pdf")
1819+
(load-pdf-buffer u)
1820+
(if (window-per-buffer?)
1821+
(load-buffer-in-new-window u)
1822+
(load-buffer u)))))
18041823

18051824
(tm-define (load-document* u)
18061825
(:argument u smart-file "File name")
18071826
(:default u (propose-name-buffer))
18081827
(when (not (url-none? u))
1809-
(if (window-per-buffer?) (load-buffer u) (load-buffer-in-new-window u))))
1828+
(if (== (url-suffix u) "pdf")
1829+
(load-pdf-buffer u)
1830+
(if (window-per-buffer?)
1831+
(load-buffer u)
1832+
(load-buffer-in-new-window u)))))
18101833

18111834
(tm-define (switch-document u)
18121835
(:argument u smart-file "File name")

TeXmacs/progs/texmacs/texmacs/tm-print.scm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@
220220
(:proposals last (list (number->string (get-page-count)) "")))
221221

222222
(tm-define (preview-file u)
223-
(open-url u))
223+
(load-pdf-buffer u))
224224

225225
(tm-define (preview-buffer)
226226
(with-default-view

devel/0120.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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+
- 若当前已滚动到底部,则继续滚动到下一页顶部(或直接到底部保持连续阅读)。

src/Plugins/Qt/qt_chooser_widget.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ qt_chooser_widget_rep::set_type (const string& _type) {
222222
nameFilters << to_qstring (translate ("All Format") * " (*)");
223223
}
224224
else if (_type == "action_open") {
225-
mainNameFilter+= " (*.tmu *.tm *.ts *.tp)";
225+
mainNameFilter+= " (*.tmu *.tm *.ts *.tp *.pdf)";
226226
//" (*.scala *.sc *.sbt *.pants *.ltx *.sty *.cls *.tex *.bib *.rawbib *.jl
227227
//*.js *.java *.sld *.ss *.tmu *.txt *.py *.json *.html *.hh *.cpp *cc *hpp
228228
//*.scm *.elv *.md *.sh *.csv)"

src/Plugins/Qt/qt_pdf_preview_widget.hpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
#include <QSize>
1818
#include <QWidget>
1919

20-
#include <mupdf/fitz.h>
21-
2220
// Forward declarations
2321
class QPushButton;
2422
class QLabel;

0 commit comments

Comments
 (0)