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
72 changes: 72 additions & 0 deletions devel/0169.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# [0169] PDF阅读器在占据屏幕一半的时候,自动Fit Width

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

## 任务相关的代码文件
- `src/Plugins/Qt/qt_pdf_reader_widget.hpp`
- `src/Plugins/Qt/qt_pdf_reader_widget.cpp`
- `tests/Plugins/Qt/qt_pdf_reader_widget_test.cpp`

## 如何测试

### 确定性测试(单元测试)
```bash
xmake b qt_pdf_reader_widget_test
xmake r qt_pdf_reader_widget_test
```

新增测试用例验证:
- 当 `PDFReaderWidget` 宽度不超过屏幕一半时,加载 PDF 后自动触发 Fit Width
- 当 `PDFReaderWidget` 宽度超过屏幕一半时,加载 PDF 后保持默认 100% 缩放

> **注意**:由于精确检测半屏贴靠需要窗口几何信息与屏幕边缘对齐,在 headless/虚拟屏幕测试环境中可能无法完全模拟系统分屏(Snap)行为。上述单元测试主要覆盖宽度阈值分支;贴靠边缘的判定建议以手工测试为准。

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

1. **左半屏贴靠测试**
- 打开 Mogan 并加载一个 PDF 文件
- 将软件窗口拖动到屏幕**左边缘**,直到系统自动将其贴靠到左半屏
- 确认 PDF 页面自动执行 Fit Width,宽度适应视口

2. **右半屏贴靠测试**
- 将软件窗口拖动到屏幕**右边缘**,直到系统自动将其贴靠到右半屏
- 确认 PDF 页面自动执行 Fit Width

3. **非贴靠窄窗口测试**
- 手动调整窗口大小,使其宽度小于屏幕一半,但**不贴靠**到屏幕边缘
- 确认**不会**自动触发 Fit Width,保持当前缩放比例

4. **恢复全屏测试**
- 将窗口从半屏状态拖回屏幕中央或最大化
- 确认窗口恢复后,再次贴靠到左/右半屏时仍能自动触发 Fit Width

## 如何提交

提交前执行以下最少步骤:
```bash
gf fmt --changed-since=main
xmake b qt_pdf_reader_widget_test
xmake r qt_pdf_reader_widget_test
```

## What

当 PDF 阅读器被系统贴靠(Snap)到屏幕左半屏或右半屏时,自动执行 Fit Width,使页面宽度适应视口。

## Why

在分屏场景下(如将软件拖动到屏幕左/右边缘自动占据半屏),PDF 阅读器默认以 100% 缩放显示,导致页面内容超出视口,需要用户手动调整缩放。自动 Fit Width 可以提升分屏阅读体验;但如果只是用户手动缩小窗口,则不应强制改变缩放,避免干扰用户意图。

## How

1. 在 `PDFReaderWidget` 中新增 `maybeAutoFitWidth()` 方法,精确检测窗口是否被贴靠到左/右半屏:
- 窗口宽度约等于屏幕可用宽度的一半(允许容差)
- 窗口高度约等于屏幕可用高度(允许容差)
- 窗口左边缘贴近屏幕左边缘(左半屏)或右边缘贴近屏幕右边缘(右半屏)
- 排除最大化、全屏状态
当同时满足以上条件时,自动调用 `fitWidth()`。
2. 在 `loadFromFile()` 成功加载 PDF 后调用 `maybeAutoFitWidth()`,确保初始显示时自动适应。
3. 在 resize 防抖超时回调 `onResizeDebounced()` 中同样调用 `maybeAutoFitWidth()`,确保窗口贴靠变化时也能自动适应。
4. 在 `onResizeDebounced()` 中增加重置逻辑:当窗口明显离开半屏状态时,重置 `autoFitApplied_`,使下次贴靠半屏仍能自动触发。
5. 引入 `autoFitApplied_` 标志位,避免在同一状态下重复触发,覆盖用户手动缩放。
73 changes: 70 additions & 3 deletions src/Plugins/Qt/qt_pdf_reader_widget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ PDFReaderWidget::PDFReaderWidget (QWidget* parent)
zoomFactor_ (1.0), pageAspectRatio_ (0.0), pageBaseWidthPts_ (0.0),
overLink_ (false), zoomDebounceTimer_ (nullptr),
resizeDebounceTimer_ (nullptr), gestureSafetyTimer_ (nullptr),
inPinchGesture_ (false), blockRender_ (false), pinchStartZoom_ (1.0),
renderCallCount_ (0) {
inPinchGesture_ (false), blockRender_ (false), autoFitApplied_ (false),
pinchStartZoom_ (1.0), renderCallCount_ (0) {

mainLayout_= new QVBoxLayout (this);
mainLayout_->setContentsMargins (0, 0, 0, 0);
Expand Down Expand Up @@ -169,7 +169,7 @@ PDFReaderWidget::PDFReaderWidget (QWidget* parent)
resizeDebounceTimer_->setSingleShot (true);
resizeDebounceTimer_->setInterval (RESIZE_DEBOUNCE_MS);
connect (resizeDebounceTimer_, &QTimer::timeout, this,
&PDFReaderWidget::rebuildPages);
&PDFReaderWidget::onResizeDebounced);

gestureSafetyTimer_= new QTimer (this);
gestureSafetyTimer_->setSingleShot (true);
Expand Down Expand Up @@ -346,6 +346,70 @@ PDFReaderWidget::updateZoomDisplay () {
zoomCombo_->blockSignals (blocked);
}

void
PDFReaderWidget::onResizeDebounced () {
// 当窗口离开半屏贴靠状态时,重置自动适配标志,
// 以便下次贴靠到左/右半屏时仍能触发 Fit Width
if (autoFitApplied_) {
QScreen* screen= this->screen ();
if (!screen) screen= QApplication::primaryScreen ();
if (screen) {
QRect screenGeo= screen->availableGeometry ();
int screenW = screenGeo.width ();
QRect winGeo = window ()->frameGeometry ();
int halfWidth= screenW / 2;
int tolerance= qMax (20, screenW / 20);
if (qAbs (winGeo.width () - halfWidth) > tolerance) {
autoFitApplied_= false;
}
}
}

if (!maybeAutoFitWidth ()) {
rebuildPages ();
}
}

bool
PDFReaderWidget::maybeAutoFitWidth () {
if (autoFitApplied_) return false;
if (pdfData_.isEmpty () || pageCount_ <= 0) return false;
if (pageBaseWidthPts_ <= 0) return false;
if (isMaximized () || isFullScreen ()) return false;

QScreen* screen= this->screen ();
if (!screen) screen= QApplication::primaryScreen ();
if (!screen) return false;

QRect screenGeo= screen->availableGeometry ();
int screenW = screenGeo.width ();
int screenH = screenGeo.height ();
QRect winGeo = window ()->frameGeometry ();

// 判断是否贴靠到左半屏或右半屏:
// 1. 宽度约等于屏幕宽度的一半
// 2. 高度约等于屏幕可用高度
// 3. 窗口左边缘贴近屏幕左边缘(左半屏)或
// 窗口右边缘贴近屏幕右边缘(右半屏)
int halfWidth = screenW / 2;
int widthTolerance = qMax (20, screenW / 20);
int heightTolerance= qMax (40, screenH / 20);

if (qAbs (winGeo.width () - halfWidth) > widthTolerance) return false;
if (qAbs (winGeo.height () - screenH) > heightTolerance) return false;

bool snappedLeft = qAbs (winGeo.x () - screenGeo.x ()) <= 10;
bool snappedRight= qAbs ((winGeo.x () + winGeo.width ()) -
(screenGeo.x () + screenGeo.width ())) <= 10;

if (snappedLeft || snappedRight) {
fitWidth ();
autoFitApplied_= true;
return true;
}
return false;
}

void
PDFReaderWidget::applyZoomToLabels () {
int width= pageWidth ();
Expand Down Expand Up @@ -976,6 +1040,7 @@ PDFReaderWidget::rebuildPages () {
bool
PDFReaderWidget::loadFromFile (const QString& filePath, int dpi) {
clear ();
autoFitApplied_= false;

targetDpi_= dpi;
hasError_ = false;
Expand Down Expand Up @@ -1089,6 +1154,7 @@ PDFReaderWidget::loadFromFile (const QString& filePath, int dpi) {
}

pageLayout_->addStretch (1);
maybeAutoFitWidth ();
rebuildPages ();
contentWidget_->adjustSize ();
updateZoomDisplay ();
Expand All @@ -1105,6 +1171,7 @@ PDFReaderWidget::clear () {
pageAspectRatio_ = 0.0;
pageBaseWidthPts_= 0.0;
pageAspectRatios_.clear ();
autoFitApplied_= false;
clearPageLinks ();
pageCache_.clear ();

Expand Down
3 changes: 3 additions & 0 deletions src/Plugins/Qt/qt_pdf_reader_widget.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ private slots:
void finishPinchGesture ();
bool renderPageToLabel (int pageNumber, QLabel* label, int targetWidth);
void rebuildPages ();
void onResizeDebounced ();
bool maybeAutoFitWidth ();
int pageWidth () const;
void setupToolBar ();
void updateZoomDisplay ();
Expand Down Expand Up @@ -180,6 +182,7 @@ private slots:

bool inPinchGesture_;
bool blockRender_;
bool autoFitApplied_;
double pinchStartZoom_;

int renderCallCount_;
Expand Down
38 changes: 38 additions & 0 deletions tests/Plugins/Qt/qt_pdf_reader_widget_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,44 @@ private slots:

delete widget;
}

void test_autoFitWidth_whenNarrow () {
PDFReaderWidget* widget= new PDFReaderWidget ();
QScreen* screen= QApplication::primaryScreen ();
int screenWidth = screen ? screen->availableSize ().width () : 1920;
widget->resize (screenWidth / 4, 300);
widget->show ();

url pdfUrl= url_system ("$TEXMACS_PATH/tests/PDF/pdf_1_4_sample.pdf");
QVERIFY (is_regular (pdfUrl));

bool result= widget->loadFromFile (to_qstring (as_string (pdfUrl)));
QVERIFY (result);
QApplication::processEvents ();

// 当 widget 宽度不超过屏幕一半时,应自动触发 Fit Width
QVERIFY (widget->zoomFactor () != 1.0);
delete widget;
}

void test_noAutoFitWidth_whenWide () {
PDFReaderWidget* widget= new PDFReaderWidget ();
QScreen* screen= QApplication::primaryScreen ();
int screenWidth = screen ? screen->availableSize ().width () : 1920;
widget->resize (screenWidth * 2 / 3, 800);
widget->show ();

url pdfUrl= url_system ("$TEXMACS_PATH/tests/PDF/pdf_1_4_sample.pdf");
QVERIFY (is_regular (pdfUrl));

bool result= widget->loadFromFile (to_qstring (as_string (pdfUrl)));
QVERIFY (result);
QApplication::processEvents ();

// 当 widget 宽度超过屏幕一半时,应保持默认 100% 缩放
QCOMPARE (widget->zoomFactor (), 1.0);
delete widget;
}
};

QTEST_MAIN (TestPdfReaderWidget)
Expand Down
Loading