diff --git a/devel/0169.md b/devel/0169.md new file mode 100644 index 0000000000..66bc68ff88 --- /dev/null +++ b/devel/0169.md @@ -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_` 标志位,避免在同一状态下重复触发,覆盖用户手动缩放。 diff --git a/src/Plugins/Qt/qt_pdf_reader_widget.cpp b/src/Plugins/Qt/qt_pdf_reader_widget.cpp index b85671036e..bfd02f8c7a 100644 --- a/src/Plugins/Qt/qt_pdf_reader_widget.cpp +++ b/src/Plugins/Qt/qt_pdf_reader_widget.cpp @@ -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); @@ -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); @@ -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 (); @@ -976,6 +1040,7 @@ PDFReaderWidget::rebuildPages () { bool PDFReaderWidget::loadFromFile (const QString& filePath, int dpi) { clear (); + autoFitApplied_= false; targetDpi_= dpi; hasError_ = false; @@ -1089,6 +1154,7 @@ PDFReaderWidget::loadFromFile (const QString& filePath, int dpi) { } pageLayout_->addStretch (1); + maybeAutoFitWidth (); rebuildPages (); contentWidget_->adjustSize (); updateZoomDisplay (); @@ -1105,6 +1171,7 @@ PDFReaderWidget::clear () { pageAspectRatio_ = 0.0; pageBaseWidthPts_= 0.0; pageAspectRatios_.clear (); + autoFitApplied_= false; clearPageLinks (); pageCache_.clear (); diff --git a/src/Plugins/Qt/qt_pdf_reader_widget.hpp b/src/Plugins/Qt/qt_pdf_reader_widget.hpp index 6dd7979e8e..ea93472663 100644 --- a/src/Plugins/Qt/qt_pdf_reader_widget.hpp +++ b/src/Plugins/Qt/qt_pdf_reader_widget.hpp @@ -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 (); @@ -180,6 +182,7 @@ private slots: bool inPinchGesture_; bool blockRender_; + bool autoFitApplied_; double pinchStartZoom_; int renderCallCount_; diff --git a/tests/Plugins/Qt/qt_pdf_reader_widget_test.cpp b/tests/Plugins/Qt/qt_pdf_reader_widget_test.cpp index cb671f4e73..b1916838c8 100644 --- a/tests/Plugins/Qt/qt_pdf_reader_widget_test.cpp +++ b/tests/Plugins/Qt/qt_pdf_reader_widget_test.cpp @@ -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)