File indexing completed on 2024-05-12 04:33:30

0001 /*
0002     SPDX-FileCopyrightText: 2013 Albert Astals Cid <aacid@kde.org>
0003 
0004     Work sponsored by the LiMux project of the city of Munich:
0005     SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 // clazy:excludeall=qstring-allocations
0011 
0012 #include <QSignalSpy>
0013 #include <QTest>
0014 
0015 #include "../core/annotations.h"
0016 #include "../core/document_p.h"
0017 #include "../core/form.h"
0018 #include "../core/page.h"
0019 #include "../part/pageview.h"
0020 #include "../part/part.h"
0021 #include "../part/presentationwidget.h"
0022 #include "../part/sidebar.h"
0023 #include "../part/toc.h"
0024 #include "../part/toggleactionmenu.h"
0025 #include "../settings.h"
0026 #include "closedialoghelper.h"
0027 
0028 #include <KActionCollection>
0029 #include <KConfigDialog>
0030 #include <KParts/OpenUrlArguments>
0031 
0032 #include <QApplication>
0033 #include <QClipboard>
0034 #include <QDesktopServices>
0035 #include <QLineEdit>
0036 #include <QMenu>
0037 #include <QMessageBox>
0038 #include <QPushButton>
0039 #include <QScrollBar>
0040 #include <QTabletEvent>
0041 #include <QTemporaryDir>
0042 #include <QTemporaryFile>
0043 #include <QTextEdit>
0044 #include <QTimer>
0045 #include <QToolBar>
0046 #include <QTreeView>
0047 #include <QUrl>
0048 
0049 namespace Okular
0050 {
0051 class PartTest : public QObject
0052 {
0053     Q_OBJECT
0054 
0055     static bool openDocument(Okular::Part *part, const QString &filePath);
0056 
0057 Q_SIGNALS:
0058     void urlHandler(const QUrl &url); // NOLINT(readability-inconsistent-declaration-parameter-name)
0059 
0060 private Q_SLOTS:
0061     void init();
0062 
0063     void testZoomWithCrop();
0064     void testReload();
0065     void testCanceledReload();
0066     void testTOCReload();
0067     void testForwardPDF();
0068     void testForwardPDF_data();
0069     void testGeneratorPreferences();
0070     void testSelectText();
0071     void testClickInternalLink();
0072     void testScrollBarAndMouseWheel();
0073     void testOpenUrlArguments();
0074     void test388288();
0075     void testSaveAs();
0076     void testSaveAs_data();
0077     void testSaveAsToNonExistingPath();
0078     void testSaveAsToSymlink();
0079     void testSaveIsSymlink();
0080     void testSidebarItemAfterSaving();
0081     void testViewModeSavingPerFile();
0082     void testSaveAsUndoStackAnnotations();
0083     void testSaveAsUndoStackAnnotations_data();
0084     void testSaveAsUndoStackForms();
0085     void testSaveAsUndoStackForms_data();
0086     void testMouseMoveOverLinkWhileInSelectionMode();
0087     void testClickUrlLinkWhileInSelectionMode();
0088     void testeTextSelectionOverAndAcrossLinks_data();
0089     void testeTextSelectionOverAndAcrossLinks();
0090     void testClickUrlLinkWhileLinkTextIsSelected();
0091     void testRClickWhileLinkTextIsSelected();
0092     void testRClickOverLinkWhileLinkTextIsSelected();
0093     void testRClickOnSelectionModeShoulShowFollowTheLinkMenu();
0094     void testClickAnywhereAfterSelectionShouldUnselect();
0095     void testeRectSelectionStartingOnLinks();
0096     void testCheckBoxReadOnly();
0097     void testCrashTextEditDestroy();
0098     void testAnnotWindow();
0099     void testAdditionalActionTriggers();
0100     void testTypewriterAnnotTool();
0101     void testJumpToPage();
0102     void testOpenAtPage();
0103     void testForwardBackwardNavigation();
0104     void testTabletProximityBehavior();
0105     void testOpenPrintPreview();
0106     void testMouseModeMenu();
0107     void testFullScreenRequest();
0108     void testZoomInFacingPages();
0109     void testLinkWithCrop();
0110     void testFieldFormatting();
0111 
0112 private:
0113     void simulateMouseSelection(double startX, double startY, double endX, double endY, QWidget *target);
0114 };
0115 
0116 class PartThatHijacksQueryClose : public Okular::Part
0117 {
0118     Q_OBJECT
0119 public:
0120     PartThatHijacksQueryClose(QObject *parent, const QVariantList &args)
0121         : Okular::Part(parent, args)
0122         , behavior(PassThru)
0123     {
0124     }
0125 
0126     enum Behavior { PassThru, ReturnTrue, ReturnFalse };
0127 
0128     void setQueryCloseBehavior(Behavior new_behavior)
0129     {
0130         behavior = new_behavior;
0131     }
0132 
0133     bool queryClose() override
0134     {
0135         if (behavior == PassThru) {
0136             return Okular::Part::queryClose();
0137         } else { // ReturnTrue or ReturnFalse
0138             return (behavior == ReturnTrue);
0139         }
0140     }
0141 
0142 private:
0143     Behavior behavior;
0144 };
0145 
0146 bool PartTest::openDocument(Okular::Part *part, const QString &filePath)
0147 {
0148     part->openDocument(filePath);
0149     return part->m_document->isOpened();
0150 }
0151 
0152 void PartTest::init()
0153 {
0154     // Default settings for every test
0155     Okular::Settings::self()->setDefaults();
0156 
0157     // Clean docdatas
0158     const QList<QUrl> urls = {QUrl::fromUserInput(QStringLiteral("file://" KDESRCDIR "data/file1.pdf")),
0159                               QUrl::fromUserInput(QStringLiteral("file://" KDESRCDIR "data/file2.pdf")),
0160                               QUrl::fromUserInput(QStringLiteral("file://" KDESRCDIR "data/simple-multipage.pdf")),
0161                               QUrl::fromUserInput(QStringLiteral("file://" KDESRCDIR "data/tocreload.pdf")),
0162                               QUrl::fromUserInput(QStringLiteral("file://" KDESRCDIR "data/pdf_with_links.pdf")),
0163                               QUrl::fromUserInput(QStringLiteral("file://" KDESRCDIR "data/RequestFullScreen.pdf"))};
0164 
0165     for (const QUrl &url : urls) {
0166         QFileInfo fileReadTest(url.toLocalFile());
0167         const QString docDataPath = Okular::DocumentPrivate::docDataFileName(url, fileReadTest.size());
0168         QFile::remove(docDataPath);
0169     }
0170 }
0171 
0172 // Test that Okular doesn't crash after a successful reload
0173 void PartTest::testReload()
0174 {
0175     QVariantList dummyArgs;
0176     Okular::Part part(nullptr, dummyArgs);
0177     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")));
0178     part.reload();
0179     qApp->processEvents();
0180 }
0181 
0182 // Test that Okular doesn't crash after a canceled reload
0183 void PartTest::testCanceledReload()
0184 {
0185     QVariantList dummyArgs;
0186     PartThatHijacksQueryClose part(nullptr, dummyArgs);
0187     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")));
0188 
0189     // When queryClose() returns false, the reload operation is canceled (as if
0190     // the user had chosen Cancel in the "Save changes?" message box)
0191     part.setQueryCloseBehavior(PartThatHijacksQueryClose::ReturnFalse);
0192 
0193     part.reload();
0194 
0195     qApp->processEvents();
0196 }
0197 
0198 void PartTest::testTOCReload()
0199 {
0200     QVariantList dummyArgs;
0201     Okular::Part part(nullptr, dummyArgs);
0202     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/tocreload.pdf")));
0203     QCOMPARE(part.m_toc->expandedNodes().count(), 0);
0204     part.m_toc->m_treeView->expandAll();
0205     QCOMPARE(part.m_toc->expandedNodes().count(), 3);
0206     part.reload();
0207     qApp->processEvents();
0208     QCOMPARE(part.m_toc->expandedNodes().count(), 3);
0209 }
0210 
0211 void PartTest::testForwardPDF()
0212 {
0213     QFETCH(QString, dir);
0214 
0215     QVariantList dummyArgs;
0216     Okular::Part part(nullptr, dummyArgs);
0217 
0218     // Create temp dir named like this: ${system temp dir}/${random string}/${dir}
0219     const QTemporaryDir tempDir;
0220     const QDir workDir(QDir(tempDir.path()).filePath(dir));
0221     workDir.mkpath(QStringLiteral("."));
0222 
0223     const QString pdfResult = workDir.path() + QStringLiteral("/synctextest.pdf");
0224     QVERIFY(QFile::copy(QStringLiteral(KDESRCDIR "data/synctextest.pdf"), pdfResult));
0225     const QString gzDestination = workDir.path() + QStringLiteral("/synctextest.synctex.gz");
0226     QVERIFY(QFile::copy(QStringLiteral(KDESRCDIR "data/synctextest.synctex.gz"), gzDestination));
0227 
0228     QVERIFY(openDocument(&part, pdfResult));
0229     part.m_document->setViewportPage(0);
0230     QCOMPARE(part.m_document->currentPage(), 0u);
0231     part.closeUrl();
0232 
0233     QUrl u(QUrl::fromLocalFile(pdfResult));
0234     // Update this if you regenerate the synctextest.pdf somewhere else
0235     u.setFragment(QStringLiteral("src:100/home/tsdgeos/devel/kde/okular/autotests/data/synctextest.tex"));
0236     part.openUrl(u);
0237     QCOMPARE(part.m_document->currentPage(), 1u);
0238 }
0239 
0240 void PartTest::testForwardPDF_data()
0241 {
0242     QTest::addColumn<QString>("dir");
0243 
0244     QTest::newRow("non-utf8") << QStringLiteral("synctextest");
0245     // QStringliteral is broken on windows with non ascii chars so using QString::fromUtf8
0246     QTest::newRow("utf8") << QString::fromUtf8("ßðđđŋßðđŋ");
0247 }
0248 
0249 void PartTest::testGeneratorPreferences()
0250 {
0251     KConfigDialog *dialog;
0252     QVariantList dummyArgs;
0253     Okular::Part part(nullptr, dummyArgs);
0254 
0255     // Test that we don't crash while opening the dialog
0256     dialog = part.slotGeneratorPreferences();
0257     qApp->processEvents();
0258     delete dialog; // closes the dialog and recursively destroys all widgets
0259 
0260     // Test that we don't crash while opening a new instance of the dialog
0261     // This catches attempts to reuse widgets that have been destroyed
0262     dialog = part.slotGeneratorPreferences();
0263     qApp->processEvents();
0264     delete dialog;
0265 }
0266 
0267 void PartTest::testSelectText()
0268 {
0269     QVariantList dummyArgs;
0270     Okular::Part part(nullptr, dummyArgs);
0271     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf")));
0272     part.widget()->show();
0273     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
0274 
0275     part.m_document->setViewportPage(0);
0276 
0277     // wait for pixmap
0278     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
0279 
0280     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
0281     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
0282 
0283     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));
0284 
0285     const int mouseY = height * 0.052;
0286     const int mouseStartX = width * 0.12;
0287     const int mouseEndX = width * 0.7;
0288 
0289     simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport());
0290 
0291     QApplication::clipboard()->clear();
0292     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection"));
0293 
0294     QCOMPARE(QApplication::clipboard()->text(), QStringLiteral("Hola que tal"));
0295 }
0296 
0297 void PartTest::testClickInternalLink()
0298 {
0299     QVariantList dummyArgs;
0300     Okular::Part part(nullptr, dummyArgs);
0301     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf")));
0302     part.widget()->show();
0303     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
0304 
0305     part.m_document->setViewportPage(0);
0306 
0307     // wait for pixmap
0308     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
0309 
0310     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
0311     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
0312 
0313     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseNormal"));
0314 
0315     QCOMPARE(part.m_document->currentPage(), 0u);
0316     QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.17, height * 0.05));
0317     QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.17, height * 0.05));
0318     QTRY_COMPARE(part.m_document->currentPage(), 1u);
0319 
0320     // make sure cursor goes back to being an open hand again.  Bug 421437
0321     QTRY_COMPARE_WITH_TIMEOUT(part.m_pageView->cursor().shape(), Qt::OpenHandCursor, 1000);
0322 }
0323 
0324 // Test for bug 421159, which is: When scrolling down with the scroll bar
0325 // followed by scrolling down with the mouse wheel, the mouse wheel scrolling
0326 // will make the viewport jump back to the first page.
0327 void PartTest::testScrollBarAndMouseWheel()
0328 {
0329     QVariantList dummyArgs;
0330     Okular::Part part(nullptr, dummyArgs);
0331     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/simple-multipage.pdf")));
0332     part.widget()->show();
0333     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
0334 
0335     part.m_document->setViewportPage(0);
0336 
0337     // wait for pixmap
0338     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
0339 
0340     // Make sure we are on the first page
0341     QCOMPARE(part.m_document->currentPage(), 0u);
0342 
0343     // Two clicks on the vertical scrollbar
0344     auto scrollBar = part.m_pageView->verticalScrollBar();
0345 
0346     QTest::mouseClick(scrollBar, Qt::LeftButton);
0347     QTest::qWait(QApplication::doubleClickInterval() * 2); // Wait a tiny bit
0348     QTest::mouseClick(scrollBar, Qt::LeftButton);
0349 
0350     // We have scrolled enough to be on the second page now
0351     QCOMPARE(part.m_document->currentPage(), 1u);
0352 
0353     // Scroll further down using the mouse wheel
0354     auto wheelDown = new QWheelEvent({}, {}, {}, {0, -150}, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false);
0355     QCoreApplication::postEvent(part.m_pageView->viewport(), wheelDown);
0356 
0357     // Wait a little for the scrolling to actually happen.
0358     // We should still be on the second page after that.
0359     QTest::qWait(1000);
0360 
0361     QCOMPARE(part.m_document->currentPage(), 1u);
0362 }
0363 
0364 // cursor switches to Hand when hovering over link in TextSelect mode.
0365 void PartTest::testMouseMoveOverLinkWhileInSelectionMode()
0366 {
0367     QVariantList dummyArgs;
0368     Okular::Part part(nullptr, dummyArgs);
0369     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
0370     // resize window to avoid problem with selection areas
0371     part.widget()->resize(800, 600);
0372     part.widget()->show();
0373     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
0374 
0375     part.m_document->setViewportPage(0);
0376 
0377     // wait for pixmap
0378     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
0379 
0380     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
0381     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
0382 
0383     // enter text-selection mode
0384     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));
0385 
0386     // move mouse over link
0387     QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.250, height * 0.127));
0388 
0389     // check if mouse icon changed to proper icon
0390     QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::PointingHandCursor);
0391 }
0392 
0393 // clicking on hyperlink jumps to destination in TextSelect mode.
0394 void PartTest::testClickUrlLinkWhileInSelectionMode()
0395 {
0396     QVariantList dummyArgs;
0397     Okular::Part part(nullptr, dummyArgs);
0398     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
0399     // resize window to avoid problem with selection areas
0400     part.widget()->resize(800, 600);
0401     part.widget()->show();
0402     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
0403 
0404     part.m_document->setViewportPage(0);
0405 
0406     // wait for pixmap
0407     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
0408 
0409     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
0410     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
0411 
0412     // enter text-selection mode
0413     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));
0414 
0415     // overwrite urlHandler for 'mailto' urls
0416     QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "urlHandler");
0417     QSignalSpy openUrlSignalSpy(this, &PartTest::urlHandler);
0418 
0419     // click on url
0420     QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.250, height * 0.127));
0421     QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.250, height * 0.127));
0422 
0423     // expect that the urlHandler signal was called
0424     QTRY_COMPARE(openUrlSignalSpy.count(), 1);
0425     QList<QVariant> arguments = openUrlSignalSpy.takeFirst();
0426     QCOMPARE(arguments.at(0).value<QUrl>(), QUrl(QStringLiteral("mailto:foo@foo.bar")));
0427 }
0428 
0429 void PartTest::testeTextSelectionOverAndAcrossLinks_data()
0430 {
0431     QTest::addColumn<double>("mouseStartX");
0432     QTest::addColumn<double>("mouseEndX");
0433     QTest::addColumn<QString>("expectedResult");
0434 
0435     // can text-select "over and across" hyperlink.
0436     QTest::newRow("start selection before link") << 0.1564 << 0.2943 << QStringLiteral(" a link: foo@foo.b");
0437     // can text-select starting at text and ending selection in middle of hyperlink.
0438     QTest::newRow("start selection in the middle of the link") << 0.28 << 0.382 << QStringLiteral("o.bar");
0439     // text selection works when selecting left to right or right to left
0440     QTest::newRow("start selection after link") << 0.40 << 0.05 << QStringLiteral("This is a link: foo@foo.bar");
0441 }
0442 
0443 // can text-select "over and across" hyperlink.
0444 void PartTest::testeTextSelectionOverAndAcrossLinks()
0445 {
0446     QVariantList dummyArgs;
0447     Okular::Part part(nullptr, dummyArgs);
0448     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
0449     // resize window to avoid problem with selection areas
0450     part.widget()->resize(800, 600);
0451     part.widget()->show();
0452     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
0453 
0454     part.m_document->setViewportPage(0);
0455 
0456     // wait for pixmap
0457     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
0458 
0459     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
0460     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
0461 
0462     // enter text-selection mode
0463     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));
0464 
0465     const double mouseY = height * 0.127;
0466     QFETCH(double, mouseStartX);
0467     QFETCH(double, mouseEndX);
0468 
0469     mouseStartX = width * mouseStartX;
0470     mouseEndX = width * mouseEndX;
0471 
0472     simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport());
0473 
0474     QApplication::clipboard()->clear();
0475     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection"));
0476 
0477     QFETCH(QString, expectedResult);
0478     QCOMPARE(QApplication::clipboard()->text(), expectedResult);
0479 }
0480 
0481 // can jump to link while there's an active selection of text.
0482 void PartTest::testClickUrlLinkWhileLinkTextIsSelected()
0483 {
0484     QVariantList dummyArgs;
0485     Okular::Part part(nullptr, dummyArgs);
0486     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
0487     // resize window to avoid problem with selection areas
0488     part.widget()->resize(800, 600);
0489     part.widget()->show();
0490     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
0491 
0492     part.m_document->setViewportPage(0);
0493 
0494     // wait for pixmap
0495     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
0496 
0497     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
0498     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
0499 
0500     // enter text-selection mode
0501     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));
0502 
0503     const double mouseY = height * 0.127;
0504     const double mouseStartX = width * 0.13;
0505     const double mouseEndX = width * 0.40;
0506 
0507     simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport());
0508 
0509     // overwrite urlHandler for 'mailto' urls
0510     QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "urlHandler");
0511     QSignalSpy openUrlSignalSpy(this, &PartTest::urlHandler);
0512 
0513     // click on url
0514     const double mouseClickX = width * 0.2997;
0515     const double mouseClickY = height * 0.1293;
0516 
0517     QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY));
0518     QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000);
0519 
0520     // expect that the urlHandler signal was called
0521     QTRY_COMPARE(openUrlSignalSpy.count(), 1);
0522     QList<QVariant> arguments = openUrlSignalSpy.takeFirst();
0523     QCOMPARE(arguments.at(0).value<QUrl>(), QUrl(QStringLiteral("mailto:foo@foo.bar")));
0524 }
0525 
0526 // r-click on the selected text gives the "Go To:" content menu option
0527 void PartTest::testRClickWhileLinkTextIsSelected()
0528 {
0529     QVariantList dummyArgs;
0530     Okular::Part part(nullptr, dummyArgs);
0531     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
0532     // resize window to avoid problem with selection areas
0533     part.widget()->resize(800, 600);
0534     part.widget()->show();
0535     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
0536 
0537     part.m_document->setViewportPage(0);
0538 
0539     // wait for pixmap
0540     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
0541 
0542     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
0543     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
0544 
0545     // enter text-selection mode
0546     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));
0547 
0548     const double mouseY = height * 0.162;
0549     const double mouseStartX = width * 0.42;
0550     const double mouseEndX = width * 0.60;
0551 
0552     simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport());
0553 
0554     // Need to do this because the pop-menu will have his own mainloop and will block tests until
0555     // the menu disappear
0556     PageView *view = part.m_pageView;
0557     bool menuClosed = false;
0558     QTimer::singleShot(2000, view, [view, &menuClosed]() {
0559         // check if popup menu is active and visible
0560         QMenu *menu = qobject_cast<QMenu *>(view->findChild<QMenu *>(QStringLiteral("PopupMenu")));
0561         QVERIFY(menu);
0562         QVERIFY(menu->isVisible());
0563 
0564         // check if the menu contains go-to link action
0565         QAction *goToAction = qobject_cast<QAction *>(menu->findChild<QAction *>(QStringLiteral("GoToAction")));
0566         QVERIFY(goToAction);
0567 
0568         // check if the "follow this link" action is not visible
0569         QAction *processLinkAction = qobject_cast<QAction *>(menu->findChild<QAction *>(QStringLiteral("ProcessLinkAction")));
0570         QVERIFY(!processLinkAction);
0571 
0572         // check if the "copy link address" action is not visible
0573         QAction *copyLinkLocation = qobject_cast<QAction *>(menu->findChild<QAction *>(QStringLiteral("CopyLinkLocationAction")));
0574         QVERIFY(!copyLinkLocation);
0575 
0576         // close menu to continue test
0577         menu->close();
0578         menuClosed = true;
0579     });
0580 
0581     // click on url
0582     const double mouseClickX = width * 0.425;
0583     const double mouseClickY = height * 0.162;
0584 
0585     QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY));
0586     QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000);
0587 
0588     // will continue after pop-menu get closed
0589     QTRY_VERIFY(menuClosed);
0590 }
0591 
0592 // r-click on the link gives the "follow this link" content menu option
0593 void PartTest::testRClickOverLinkWhileLinkTextIsSelected()
0594 {
0595     QVariantList dummyArgs;
0596     Okular::Part part(nullptr, dummyArgs);
0597     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
0598     // resize window to avoid problem with selection areas
0599     part.widget()->resize(800, 600);
0600     part.widget()->show();
0601     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
0602 
0603     part.m_document->setViewportPage(0);
0604 
0605     // wait for pixmap
0606     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
0607 
0608     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
0609     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
0610 
0611     // enter text-selection mode
0612     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));
0613 
0614     const double mouseY = height * 0.162;
0615     const double mouseStartX = width * 0.42;
0616     const double mouseEndX = width * 0.60;
0617 
0618     simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport());
0619 
0620     // Need to do this because the pop-menu will have his own mainloop and will block tests until
0621     // the menu disappear
0622     PageView *view = part.m_pageView;
0623     bool menuClosed = false;
0624     QTimer::singleShot(2000, view, [view, &menuClosed]() {
0625         // check if popup menu is active and visible
0626         QMenu *menu = qobject_cast<QMenu *>(view->findChild<QMenu *>(QStringLiteral("PopupMenu")));
0627         QVERIFY(menu);
0628         QVERIFY(menu->isVisible());
0629 
0630         // check if the menu contains "follow this link" action
0631         QAction *processLinkAction = qobject_cast<QAction *>(menu->findChild<QAction *>(QStringLiteral("ProcessLinkAction")));
0632         QVERIFY(processLinkAction);
0633 
0634         // check if the menu contains "copy link address" action
0635         QAction *copyLinkLocation = qobject_cast<QAction *>(menu->findChild<QAction *>(QStringLiteral("CopyLinkLocationAction")));
0636         QVERIFY(copyLinkLocation);
0637 
0638         // close menu to continue test
0639         menu->close();
0640         menuClosed = true;
0641     });
0642 
0643     // click on url
0644     const double mouseClickX = width * 0.593;
0645     const double mouseClickY = height * 0.162;
0646 
0647     QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY));
0648     QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000);
0649 
0650     // will continue after pop-menu get closed
0651     QTRY_VERIFY(menuClosed);
0652 }
0653 
0654 void PartTest::testRClickOnSelectionModeShoulShowFollowTheLinkMenu()
0655 {
0656     QVariantList dummyArgs;
0657     Okular::Part part(nullptr, dummyArgs);
0658     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
0659     // resize window to avoid problem with selection areas
0660     part.widget()->resize(800, 600);
0661     part.widget()->show();
0662     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
0663 
0664     part.m_document->setViewportPage(0);
0665 
0666     // wait for pixmap
0667     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
0668 
0669     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
0670     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
0671 
0672     // enter text-selection mode
0673     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));
0674 
0675     // Need to do this because the pop-menu will have his own mainloop and will block tests until
0676     // the menu disappear
0677     PageView *view = part.m_pageView;
0678     bool menuClosed = false;
0679     QTimer::singleShot(2000, view, [view, &menuClosed]() {
0680         // check if popup menu is active and visible
0681         QMenu *menu = qobject_cast<QMenu *>(view->findChild<QMenu *>(QStringLiteral("PopupMenu")));
0682         QVERIFY(menu);
0683         QVERIFY(menu->isVisible());
0684 
0685         // check if the menu contains "Follow this link" action
0686         QAction *processLink = qobject_cast<QAction *>(menu->findChild<QAction *>(QStringLiteral("ProcessLinkAction")));
0687         QVERIFY(processLink);
0688 
0689         // chek if the menu contains  "Copy Link Address" action
0690         QAction *actCopyLinkLocation = qobject_cast<QAction *>(menu->findChild<QAction *>(QStringLiteral("CopyLinkLocationAction")));
0691         QVERIFY(actCopyLinkLocation);
0692 
0693         // close menu to continue test
0694         menu->close();
0695         menuClosed = true;
0696     });
0697 
0698     // r-click on url
0699     const double mouseClickX = width * 0.604;
0700     const double mouseClickY = height * 0.162;
0701 
0702     QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY));
0703     QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000);
0704 
0705     // will continue after pop-menu get closed
0706     QTRY_VERIFY(menuClosed);
0707 }
0708 
0709 void PartTest::testClickAnywhereAfterSelectionShouldUnselect()
0710 {
0711     QVariantList dummyArgs;
0712     Okular::Part part(nullptr, dummyArgs);
0713     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
0714     // resize window to avoid problem with selection areas
0715     part.widget()->resize(800, 600);
0716     part.widget()->show();
0717     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
0718 
0719     part.m_document->setViewportPage(0);
0720 
0721     // wait for pixmap
0722     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
0723 
0724     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
0725     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
0726 
0727     // enter text-selection mode
0728     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect"));
0729 
0730     const double mouseY = height * 0.162;
0731     const double mouseStartX = width * 0.42;
0732     const double mouseEndX = width * 0.60;
0733 
0734     simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport());
0735 
0736     // click on url
0737     const double mouseClickX = width * 0.10;
0738 
0739     QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseY));
0740     QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(mouseClickX, mouseY), 1000);
0741 
0742     QApplication::clipboard()->clear();
0743     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection"));
0744 
0745     // check if copied text is empty what means no text selected
0746     QVERIFY(QApplication::clipboard()->text().isEmpty());
0747 }
0748 
0749 void PartTest::testeRectSelectionStartingOnLinks()
0750 {
0751     QVariantList dummyArgs;
0752     Okular::Part part(nullptr, dummyArgs);
0753     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf")));
0754     // hide info messages as they interfere with selection area
0755     Okular::Settings::self()->setShowEmbeddedContentMessages(false);
0756     Okular::Settings::self()->setShowOSD(false);
0757 
0758     part.widget()->show();
0759     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
0760 
0761     part.m_document->setViewportPage(0);
0762 
0763     // wait for pixmap
0764     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
0765 
0766     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
0767     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
0768 
0769     // enter text-selection mode
0770     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseSelect"));
0771 
0772     const double mouseStartY = height * 0.127;
0773     const double mouseEndY = height * 0.127;
0774     const double mouseStartX = width * 0.28;
0775     const double mouseEndX = width * 0.382;
0776 
0777     // Need to do this because the pop-menu will have his own mainloop and will block tests until
0778     // the menu disappear
0779     PageView *view = part.m_pageView;
0780     bool menuClosed = false;
0781     QTimer::singleShot(2000, view, [view, &menuClosed]() {
0782         QApplication::clipboard()->clear();
0783 
0784         // check if popup menu is active and visible
0785         QMenu *menu = qobject_cast<QMenu *>(view->findChild<QMenu *>(QStringLiteral("PopupMenu")));
0786         QVERIFY(menu);
0787         QVERIFY(menu->isVisible());
0788 
0789         // check if the copy selected text to clipboard is present
0790         QAction *copyAct = qobject_cast<QAction *>(menu->findChild<QAction *>(QStringLiteral("CopyTextToClipboard")));
0791         QVERIFY(copyAct);
0792 
0793         menu->close();
0794         menuClosed = true;
0795     });
0796 
0797     simulateMouseSelection(mouseStartX, mouseStartY, mouseEndX, mouseEndY, part.m_pageView->viewport());
0798 
0799     // wait menu get closed
0800     QTRY_VERIFY(menuClosed);
0801 }
0802 
0803 void PartTest::simulateMouseSelection(double startX, double startY, double endX, double endY, QWidget *target)
0804 {
0805     const int steps = 5;
0806     const double diffX = endX - startX;
0807     const double diffY = endY - startY;
0808     const double diffXStep = diffX / steps;
0809     const double diffYStep = diffY / steps;
0810 
0811     QTestEventList events;
0812     events.addMouseMove(QPoint(startX, startY));
0813     events.addMousePress(Qt::LeftButton, Qt::NoModifier, QPoint(startX, startY));
0814     for (int i = 0; i < steps - 1; ++i) {
0815         events.addMouseMove(QPoint(startX + i * diffXStep, startY + i * diffYStep));
0816         events.addDelay(100);
0817     }
0818     events.addMouseMove(QPoint(endX, endY));
0819     events.addDelay(100);
0820     events.addMouseRelease(Qt::LeftButton, Qt::NoModifier, QPoint(endX, endY));
0821 
0822     events.simulate(target);
0823 }
0824 
0825 void PartTest::testSaveAsToNonExistingPath()
0826 {
0827     Okular::Part part(nullptr, {});
0828     part.openDocument(QStringLiteral(KDESRCDIR "data/file1.pdf"));
0829 
0830     QString saveFilePath;
0831     {
0832         QTemporaryFile saveFile(QStringLiteral("%1/okrXXXXXX.pdf").arg(QDir::tempPath()));
0833         saveFile.open();
0834         saveFilePath = saveFile.fileName();
0835         // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares
0836     }
0837 
0838     QVERIFY(!QFileInfo::exists(saveFilePath));
0839 
0840     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFilePath), Part::NoSaveAsFlags));
0841 
0842     QFile::remove(saveFilePath);
0843 }
0844 
0845 void PartTest::testSaveAsToSymlink()
0846 {
0847 #ifdef Q_OS_UNIX
0848     Okular::Part part(nullptr, {});
0849     part.openDocument(QStringLiteral(KDESRCDIR "data/file1.pdf"));
0850 
0851     QTemporaryFile newFile(QStringLiteral("%1/okrXXXXXX.pdf").arg(QDir::tempPath()));
0852     newFile.open();
0853 
0854     QString linkFilePath;
0855     {
0856         QTemporaryFile linkFile(QStringLiteral("%1/okrXXXXXX.pdf").arg(QDir::tempPath()));
0857         linkFile.open();
0858         linkFilePath = linkFile.fileName();
0859         // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares
0860     }
0861 
0862     QFile::link(newFile.fileName(), linkFilePath);
0863 
0864     QVERIFY(QFileInfo(linkFilePath).isSymLink());
0865 
0866     QVERIFY(part.saveAs(QUrl::fromLocalFile(linkFilePath), Part::NoSaveAsFlags));
0867 
0868     QVERIFY(QFileInfo(linkFilePath).isSymLink());
0869 
0870     QFile::remove(linkFilePath);
0871 #endif
0872 }
0873 
0874 void PartTest::testSaveIsSymlink()
0875 {
0876 #ifdef Q_OS_UNIX
0877     Okular::Part part(nullptr, {});
0878 
0879     QString newFilePath;
0880     {
0881         QTemporaryFile newFile(QStringLiteral("%1/okrXXXXXX.pdf").arg(QDir::tempPath()));
0882         newFile.open();
0883         newFilePath = newFile.fileName();
0884         // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares
0885     }
0886 
0887     QFile::copy(QStringLiteral(KDESRCDIR "data/file1.pdf"), newFilePath);
0888 
0889     QString linkFilePath;
0890     {
0891         QTemporaryFile linkFile(QStringLiteral("%1/okrXXXXXX.pdf").arg(QDir::tempPath()));
0892         linkFile.open();
0893         linkFilePath = linkFile.fileName();
0894         // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares
0895     }
0896 
0897     QFile::link(newFilePath, linkFilePath);
0898 
0899     QVERIFY(QFileInfo(linkFilePath).isSymLink());
0900 
0901     part.openDocument(linkFilePath);
0902     QVERIFY(part.saveAs(QUrl::fromLocalFile(linkFilePath), Part::NoSaveAsFlags));
0903 
0904     QVERIFY(QFileInfo(linkFilePath).isSymLink());
0905 
0906     QFile::remove(newFilePath);
0907     QFile::remove(linkFilePath);
0908 #endif
0909 }
0910 
0911 void PartTest::testSaveAs()
0912 {
0913     QFETCH(QString, file);
0914     QFETCH(QString, extension);
0915     QFETCH(bool, nativelySupportsAnnotations);
0916     QFETCH(bool, canSwapBackingFile);
0917 
0918     QScopedPointer<TestingUtils::CloseDialogHelper> closeDialogHelper;
0919 
0920     QString annotName;
0921     QTemporaryFile archiveSave(QStringLiteral("%1/okrXXXXXX.okular").arg(QDir::tempPath()));
0922     QTemporaryFile nativeDirectSave(QStringLiteral("%1/okrXXXXXX.%2").arg(QDir::tempPath(), extension));
0923     QTemporaryFile nativeFromArchiveFile(QStringLiteral("%1/okrXXXXXX.%2").arg(QDir::tempPath(), extension));
0924     QVERIFY(archiveSave.open());
0925     archiveSave.close();
0926     QVERIFY(nativeDirectSave.open());
0927     nativeDirectSave.close();
0928     QVERIFY(nativeFromArchiveFile.open());
0929     nativeFromArchiveFile.close();
0930 
0931     qDebug() << "Open file, add annotation and save both natively and to .okular";
0932     {
0933         Okular::Part part(nullptr, {});
0934         part.openDocument(file);
0935         part.m_document->documentInfo();
0936 
0937         QCOMPARE(part.m_document->canSwapBackingFile(), canSwapBackingFile);
0938 
0939         Okular::Annotation *annot = new Okular::TextAnnotation();
0940         annot->setBoundingRectangle(Okular::NormalizedRect(0.1, 0.1, 0.15, 0.15));
0941         annot->setContents(QStringLiteral("annot contents"));
0942         part.m_document->addPageAnnotation(0, annot);
0943         annotName = annot->uniqueName();
0944 
0945         if (canSwapBackingFile) {
0946             if (!nativelySupportsAnnotations) {
0947                 closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::No)); // this is the "you're going to lose the annotations" dialog
0948             }
0949             QVERIFY(part.saveAs(QUrl::fromLocalFile(nativeDirectSave.fileName()), Part::NoSaveAsFlags));
0950             // For backends that don't support annotations natively we mark the part as still modified
0951             // after a save because we keep the annotation around but it will get lost if the user closes the app
0952             // so we want to give her a last chance to save on close with the "you have changes dialog"
0953             QCOMPARE(part.isModified(), !nativelySupportsAnnotations);
0954             QVERIFY(part.saveAs(QUrl::fromLocalFile(archiveSave.fileName()), Part::SaveAsOkularArchive));
0955         } else {
0956             // We need to save to archive first otherwise we lose the annotation
0957 
0958             closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::Yes)); // this is the "you're going to lose the undo/redo stack" dialog
0959             QVERIFY(part.saveAs(QUrl::fromLocalFile(archiveSave.fileName()), Part::SaveAsOkularArchive));
0960 
0961             if (!nativelySupportsAnnotations) {
0962                 closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::No)); // this is the "you're going to lose the annotations" dialog
0963             }
0964             QVERIFY(part.saveAs(QUrl::fromLocalFile(nativeDirectSave.fileName()), Part::NoSaveAsFlags));
0965         }
0966 
0967         QCOMPARE(part.m_document->documentInfo().get(Okular::DocumentInfo::FilePath), part.m_document->currentDocument().toDisplayString());
0968         part.closeUrl();
0969     }
0970 
0971     qDebug() << "Open the .okular, check that the annotation is present and save to native";
0972     {
0973         Okular::Part part(nullptr, {});
0974         part.openDocument(archiveSave.fileName());
0975         part.m_document->documentInfo();
0976 
0977         QCOMPARE(part.m_document->page(0)->annotations().size(), 1);
0978         QCOMPARE(part.m_document->page(0)->annotations().first()->uniqueName(), annotName);
0979 
0980         if (!nativelySupportsAnnotations) {
0981             closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::No)); // this is the "you're going to lose the annotations" dialog
0982         }
0983         QVERIFY(part.saveAs(QUrl::fromLocalFile(nativeFromArchiveFile.fileName()), Part::NoSaveAsFlags));
0984 
0985         if (canSwapBackingFile && !nativelySupportsAnnotations) {
0986             // For backends that don't support annotations natively we mark the part as still modified
0987             // after a save because we keep the annotation around but it will get lost if the user closes the app
0988             // so we want to give her a last chance to save on close with the "you have changes dialog"
0989             closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::No)); // this is the "do you want to save or discard" dialog
0990         }
0991 
0992         QCOMPARE(part.m_document->documentInfo().get(Okular::DocumentInfo::FilePath), part.m_document->currentDocument().toDisplayString());
0993         part.closeUrl();
0994     }
0995 
0996     qDebug() << "Open the native file saved directly, and check that the annot"
0997              << "is there iff we expect it";
0998     {
0999         Okular::Part part(nullptr, {});
1000         part.openDocument(nativeDirectSave.fileName());
1001 
1002         QCOMPARE(part.m_document->page(0)->annotations().size(), nativelySupportsAnnotations ? 1 : 0);
1003         if (nativelySupportsAnnotations) {
1004             QCOMPARE(part.m_document->page(0)->annotations().first()->uniqueName(), annotName);
1005         }
1006 
1007         part.closeUrl();
1008     }
1009 
1010     qDebug() << "Open the native file saved from the .okular, and check that the annot"
1011              << "is there iff we expect it";
1012     {
1013         Okular::Part part(nullptr, {});
1014         part.openDocument(nativeFromArchiveFile.fileName());
1015 
1016         QCOMPARE(part.m_document->page(0)->annotations().size(), nativelySupportsAnnotations ? 1 : 0);
1017         if (nativelySupportsAnnotations) {
1018             QCOMPARE(part.m_document->page(0)->annotations().first()->uniqueName(), annotName);
1019         }
1020 
1021         part.closeUrl();
1022     }
1023 }
1024 
1025 void PartTest::testSaveAs_data()
1026 {
1027     QTest::addColumn<QString>("file");
1028     QTest::addColumn<QString>("extension");
1029     QTest::addColumn<bool>("nativelySupportsAnnotations");
1030     QTest::addColumn<bool>("canSwapBackingFile");
1031 
1032     QTest::newRow("pdf") << KDESRCDIR "data/file1.pdf"
1033                          << "pdf" << true << true;
1034     QTest::newRow("pdf.gz") << KDESRCDIR "data/file1.pdf.gz"
1035                             << "pdf" << true << true;
1036     QTest::newRow("epub") << KDESRCDIR "data/contents.epub"
1037                           << "epub" << false << false;
1038     QTest::newRow("jpg") << KDESRCDIR "data/potato.jpg"
1039                          << "jpg" << false << true;
1040 }
1041 
1042 void PartTest::testSidebarItemAfterSaving()
1043 {
1044     Okular::Part part(nullptr, {});
1045     QWidget *currentSidebarItem = part.m_sidebar->currentItem(); // thumbnails
1046     openDocument(&part, QStringLiteral(KDESRCDIR "data/tocreload.pdf"));
1047     // since it has TOC it changes to TOC
1048     QVERIFY(currentSidebarItem != part.m_sidebar->currentItem());
1049     // now change back to thumbnails
1050     part.m_sidebar->setCurrentItem(currentSidebarItem);
1051 
1052     part.saveAs(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/tocreload.pdf")));
1053 
1054     // Check it is still thumbnails after saving
1055     QCOMPARE(currentSidebarItem, part.m_sidebar->currentItem());
1056 }
1057 
1058 void PartTest::testViewModeSavingPerFile()
1059 {
1060     Okular::Part part(nullptr, {});
1061 
1062     // Open some file
1063     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")));
1064 
1065     // Switch to 'continuous' view mode
1066     part.m_pageView->setCapability(Okular::View::ViewCapability::Continuous, QVariant(true));
1067 
1068     // Close document
1069     part.closeUrl();
1070 
1071     // Open another file
1072     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf")));
1073 
1074     // Switch to 'non-continuous' mode
1075     part.m_pageView->setCapability(Okular::View::ViewCapability::Continuous, QVariant(false));
1076 
1077     // Close that document, too
1078     part.closeUrl();
1079 
1080     // Open first document again
1081     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")));
1082 
1083     // If per-file view mode saving works, the view mode should be 'continuous' again.
1084     QVERIFY(part.m_pageView->capability(Okular::View::ViewCapability::Continuous).toBool());
1085 }
1086 
1087 void PartTest::testSaveAsUndoStackAnnotations()
1088 {
1089     QFETCH(QString, file);
1090     QFETCH(QString, extension);
1091     QFETCH(bool, nativelySupportsAnnotations);
1092     QFETCH(bool, canSwapBackingFile);
1093     QFETCH(bool, saveToArchive);
1094 
1095     const Part::SaveAsFlag saveFlags = saveToArchive ? Part::SaveAsOkularArchive : Part::NoSaveAsFlags;
1096 
1097     QScopedPointer<TestingUtils::CloseDialogHelper> closeDialogHelper;
1098 
1099     // closeDialogHelper relies on the availability of the "Continue" button to drop changes
1100     // when saving to a file format not supporting those. However, this button is only sensible
1101     // and available for "Save As", but not for "Save". By alternately saving to saveFile1 and
1102     // saveFile2 we always force "Save As", so closeDialogHelper keeps working.
1103     QTemporaryFile saveFile1(QStringLiteral("%1/okrXXXXXX_1.%2").arg(QDir::tempPath(), extension));
1104     QVERIFY(saveFile1.open());
1105     saveFile1.close();
1106     QTemporaryFile saveFile2(QStringLiteral("%1/okrXXXXXX_2.%2").arg(QDir::tempPath(), extension));
1107     QVERIFY(saveFile2.open());
1108     saveFile2.close();
1109 
1110     Okular::Part part(nullptr, {});
1111     part.openDocument(file);
1112 
1113     QCOMPARE(part.m_document->canSwapBackingFile(), canSwapBackingFile);
1114 
1115     Okular::Annotation *annot = new Okular::TextAnnotation();
1116     annot->setBoundingRectangle(Okular::NormalizedRect(0.1, 0.1, 0.15, 0.15));
1117     annot->setContents(QStringLiteral("annot contents"));
1118     part.m_document->addPageAnnotation(0, annot);
1119     QString annotName = annot->uniqueName();
1120 
1121     if (!nativelySupportsAnnotations && !saveToArchive) {
1122         closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::No)); // this is the "you're going to lose the annotations" dialog
1123     }
1124 
1125     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile1.fileName()), saveFlags));
1126 
1127     if (!canSwapBackingFile) {
1128         // The undo/redo stack gets lost if you can not swap the backing file
1129         QVERIFY(!part.m_document->canUndo());
1130         QVERIFY(!part.m_document->canRedo());
1131         return;
1132     }
1133 
1134     // Check we can still undo the annot add after save
1135     QVERIFY(part.m_document->canUndo());
1136     part.m_document->undo();
1137     QVERIFY(!part.m_document->canUndo());
1138 
1139     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile1.fileName()), saveFlags));
1140     QVERIFY(part.m_document->page(0)->annotations().isEmpty());
1141 
1142     // Check we can redo the annot add after save
1143     QVERIFY(part.m_document->canRedo());
1144     part.m_document->redo();
1145     QVERIFY(!part.m_document->canRedo());
1146 
1147     if (nativelySupportsAnnotations) {
1148         // If the annots are provided by the backend we need to refetch the pointer after save
1149         annot = part.m_document->page(0)->annotation(annotName);
1150         QVERIFY(annot);
1151     }
1152 
1153     // Remove the annotation, creates another undo command
1154     QVERIFY(part.m_document->canRemovePageAnnotation(annot));
1155     part.m_document->removePageAnnotation(0, annot);
1156     QVERIFY(part.m_document->page(0)->annotations().isEmpty());
1157 
1158     // Check we can still undo the annot remove after save
1159     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile1.fileName()), saveFlags));
1160     QVERIFY(part.m_document->canUndo());
1161     part.m_document->undo();
1162     QVERIFY(part.m_document->canUndo());
1163     QCOMPARE(part.m_document->page(0)->annotations().count(), 1);
1164 
1165     // Check we can still undo the annot add after save
1166     if (!nativelySupportsAnnotations && !saveToArchive) {
1167         closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::No)); // this is the "you're going to lose the annotations" dialog
1168     }
1169     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile2.fileName()), saveFlags));
1170     QVERIFY(part.m_document->canUndo());
1171     part.m_document->undo();
1172     QVERIFY(!part.m_document->canUndo());
1173     QVERIFY(part.m_document->page(0)->annotations().isEmpty());
1174 
1175     // Redo the add annotation
1176     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile1.fileName()), saveFlags));
1177     QVERIFY(part.m_document->canRedo());
1178     part.m_document->redo();
1179     QVERIFY(part.m_document->canUndo());
1180     QVERIFY(part.m_document->canRedo());
1181 
1182     if (nativelySupportsAnnotations) {
1183         // If the annots are provided by the backend we need to refetch the pointer after save
1184         annot = part.m_document->page(0)->annotation(annotName);
1185         QVERIFY(annot);
1186     }
1187 
1188     // Add translate, adjust and modify commands
1189     part.m_document->translatePageAnnotation(0, annot, Okular::NormalizedPoint(0.1, 0.1));
1190     part.m_document->adjustPageAnnotation(0, annot, Okular::NormalizedPoint(0.1, 0.1), Okular::NormalizedPoint(0.1, 0.1));
1191     part.m_document->prepareToModifyAnnotationProperties(annot);
1192     part.m_document->modifyPageAnnotationProperties(0, annot);
1193 
1194     // Now check we can still undo/redo/save at all the intermediate states and things still work
1195     if (!nativelySupportsAnnotations && !saveToArchive) {
1196         closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::No)); // this is the "you're going to lose the annotations" dialog
1197     }
1198     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile2.fileName()), saveFlags));
1199     QVERIFY(part.m_document->canUndo());
1200     part.m_document->undo();
1201     QVERIFY(part.m_document->canUndo());
1202 
1203     if (!nativelySupportsAnnotations && !saveToArchive) {
1204         closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::No)); // this is the "you're going to lose the annotations" dialog
1205     }
1206     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile1.fileName()), saveFlags));
1207     QVERIFY(part.m_document->canUndo());
1208     part.m_document->undo();
1209     QVERIFY(part.m_document->canUndo());
1210 
1211     if (!nativelySupportsAnnotations && !saveToArchive) {
1212         closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::No)); // this is the "you're going to lose the annotations" dialog
1213     }
1214     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile2.fileName()), saveFlags));
1215     QVERIFY(part.m_document->canUndo());
1216     part.m_document->undo();
1217     QVERIFY(part.m_document->canUndo());
1218 
1219     if (!nativelySupportsAnnotations && !saveToArchive) {
1220         closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::No)); // this is the "you're going to lose the annotations" dialog
1221     }
1222     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile1.fileName()), saveFlags));
1223     QVERIFY(part.m_document->canUndo());
1224     part.m_document->undo();
1225     QVERIFY(!part.m_document->canUndo());
1226     QVERIFY(part.m_document->canRedo());
1227     QVERIFY(part.m_document->page(0)->annotations().isEmpty());
1228 
1229     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile1.fileName()), saveFlags));
1230     QVERIFY(part.m_document->canRedo());
1231     part.m_document->redo();
1232     QVERIFY(part.m_document->canRedo());
1233 
1234     if (!nativelySupportsAnnotations && !saveToArchive) {
1235         closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::No)); // this is the "you're going to lose the annotations" dialog
1236     }
1237     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile2.fileName()), saveFlags));
1238     QVERIFY(part.m_document->canRedo());
1239     part.m_document->redo();
1240     QVERIFY(part.m_document->canRedo());
1241 
1242     if (!nativelySupportsAnnotations && !saveToArchive) {
1243         closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::No)); // this is the "you're going to lose the annotations" dialog
1244     }
1245     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile1.fileName()), saveFlags));
1246     QVERIFY(part.m_document->canRedo());
1247     part.m_document->redo();
1248     QVERIFY(part.m_document->canRedo());
1249 
1250     if (!nativelySupportsAnnotations && !saveToArchive) {
1251         closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::No)); // this is the "you're going to lose the annotations" dialog
1252     }
1253     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile2.fileName()), saveFlags));
1254     QVERIFY(part.m_document->canRedo());
1255     part.m_document->redo();
1256     QVERIFY(!part.m_document->canRedo());
1257 
1258     closeDialogHelper.reset(new TestingUtils::CloseDialogHelper(&part, QDialogButtonBox::No)); // this is the "do you want to save or discard" dialog
1259     part.closeUrl();
1260 }
1261 
1262 void PartTest::testSaveAsUndoStackAnnotations_data()
1263 {
1264     QTest::addColumn<QString>("file");
1265     QTest::addColumn<QString>("extension");
1266     QTest::addColumn<bool>("nativelySupportsAnnotations");
1267     QTest::addColumn<bool>("canSwapBackingFile");
1268     QTest::addColumn<bool>("saveToArchive");
1269 
1270     QTest::newRow("pdf") << KDESRCDIR "data/file1.pdf"
1271                          << "pdf" << true << true << false;
1272     QTest::newRow("epub") << KDESRCDIR "data/contents.epub"
1273                           << "epub" << false << false << false;
1274     QTest::newRow("jpg") << KDESRCDIR "data/potato.jpg"
1275                          << "jpg" << false << true << false;
1276     QTest::newRow("pdfarchive") << KDESRCDIR "data/file1.pdf"
1277                                 << "okular" << true << true << true;
1278     QTest::newRow("jpgarchive") << KDESRCDIR "data/potato.jpg"
1279                                 << "okular" << false << true << true;
1280 }
1281 
1282 void PartTest::testSaveAsUndoStackForms()
1283 {
1284     QFETCH(QString, file);
1285     QFETCH(QString, extension);
1286     QFETCH(bool, saveToArchive);
1287 
1288     const Part::SaveAsFlag saveFlags = saveToArchive ? Part::SaveAsOkularArchive : Part::NoSaveAsFlags;
1289 
1290     QTemporaryFile saveFile(QStringLiteral("%1/okrXXXXXX.%2").arg(QDir::tempPath(), extension));
1291     QVERIFY(saveFile.open());
1292     saveFile.close();
1293 
1294     Okular::Part part(nullptr, {});
1295     part.openDocument(file);
1296 
1297     const QList<Okular::FormField *> pageFormFields = part.m_document->page(0)->formFields();
1298     for (FormField *ff : pageFormFields) {
1299         if (ff->id() == 65537) {
1300             QCOMPARE(ff->type(), FormField::FormText);
1301             FormFieldText *fft = static_cast<FormFieldText *>(ff);
1302             part.m_document->editFormText(0, fft, QStringLiteral("BlaBla"), 6, 0, 0);
1303         } else if (ff->id() == 65538) {
1304             QCOMPARE(ff->type(), FormField::FormButton);
1305             FormFieldButton *ffb = static_cast<FormFieldButton *>(ff);
1306             QCOMPARE(ffb->buttonType(), FormFieldButton::Radio);
1307             part.m_document->editFormButtons(0, QList<FormFieldButton *>() << ffb, QList<bool>() << true);
1308         } else if (ff->id() == 65542) {
1309             QCOMPARE(ff->type(), FormField::FormChoice);
1310             FormFieldChoice *ffc = static_cast<FormFieldChoice *>(ff);
1311             QCOMPARE(ffc->choiceType(), FormFieldChoice::ListBox);
1312             part.m_document->editFormList(0, ffc, QList<int>() << 1);
1313         } else if (ff->id() == 65543) {
1314             QCOMPARE(ff->type(), FormField::FormChoice);
1315             FormFieldChoice *ffc = static_cast<FormFieldChoice *>(ff);
1316             QCOMPARE(ffc->choiceType(), FormFieldChoice::ComboBox);
1317             part.m_document->editFormCombo(0, ffc, QStringLiteral("combo2"), 3, 0, 0);
1318         }
1319     }
1320 
1321     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile.fileName()), saveFlags));
1322 
1323     QVERIFY(part.m_document->canUndo());
1324     part.m_document->undo();
1325     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile.fileName()), saveFlags));
1326 
1327     QVERIFY(part.m_document->canUndo());
1328     part.m_document->undo();
1329     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile.fileName()), saveFlags));
1330 
1331     QVERIFY(part.m_document->canUndo());
1332     part.m_document->undo();
1333     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile.fileName()), saveFlags));
1334 
1335     QVERIFY(part.m_document->canUndo());
1336     part.m_document->undo();
1337     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile.fileName()), saveFlags));
1338     QVERIFY(!part.m_document->canUndo());
1339 
1340     QVERIFY(part.m_document->canRedo());
1341     part.m_document->redo();
1342     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile.fileName()), saveFlags));
1343 
1344     QVERIFY(part.m_document->canRedo());
1345     part.m_document->redo();
1346     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile.fileName()), saveFlags));
1347 
1348     QVERIFY(part.m_document->canRedo());
1349     part.m_document->redo();
1350     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile.fileName()), saveFlags));
1351 
1352     QVERIFY(part.m_document->canRedo());
1353     part.m_document->redo();
1354     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile.fileName()), saveFlags));
1355 }
1356 
1357 void PartTest::testSaveAsUndoStackForms_data()
1358 {
1359     QTest::addColumn<QString>("file");
1360     QTest::addColumn<QString>("extension");
1361     QTest::addColumn<bool>("saveToArchive");
1362 
1363     QTest::newRow("pdf") << KDESRCDIR "data/formSamples.pdf"
1364                          << "pdf" << false;
1365     QTest::newRow("pdfarchive") << KDESRCDIR "data/formSamples.pdf"
1366                                 << "okular" << true;
1367 }
1368 
1369 void PartTest::testOpenUrlArguments()
1370 {
1371     Okular::Part part(nullptr, {});
1372 
1373     KParts::OpenUrlArguments args;
1374     args.setMimeType(QStringLiteral("text/rtf"));
1375 
1376     part.setArguments(args);
1377 
1378     part.openUrl(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/file1.pdf")));
1379 
1380     QCOMPARE(part.arguments().mimeType(), QStringLiteral("text/rtf"));
1381 }
1382 
1383 void PartTest::test388288()
1384 {
1385     Okular::Part part(nullptr, {});
1386 
1387     part.openUrl(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/file1.pdf")));
1388 
1389     part.widget()->show();
1390     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
1391 
1392     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseNormal"));
1393 
1394     auto annot = new Okular::HighlightAnnotation();
1395     annot->setHighlightType(Okular::HighlightAnnotation::Highlight);
1396     const Okular::NormalizedRect r(0.36, 0.16, 0.51, 0.17);
1397     annot->setBoundingRectangle(r);
1398     Okular::HighlightAnnotation::Quad q;
1399     q.setCapStart(false);
1400     q.setCapEnd(false);
1401     q.setFeather(1.0);
1402     q.setPoint(Okular::NormalizedPoint(r.left, r.bottom), 0);
1403     q.setPoint(Okular::NormalizedPoint(r.right, r.bottom), 1);
1404     q.setPoint(Okular::NormalizedPoint(r.right, r.top), 2);
1405     q.setPoint(Okular::NormalizedPoint(r.left, r.top), 3);
1406     annot->highlightQuads().append(q);
1407 
1408     part.m_document->addPageAnnotation(0, annot);
1409 
1410     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
1411     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
1412 
1413     QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.5, height * 0.5));
1414     QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::OpenHandCursor);
1415 
1416     QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.4, height * 0.165));
1417     QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::ArrowCursor);
1418 
1419     QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.1, height * 0.165));
1420 
1421     part.m_document->undo();
1422 
1423     annot = new Okular::HighlightAnnotation();
1424     annot->setHighlightType(Okular::HighlightAnnotation::Highlight);
1425     annot->setBoundingRectangle(r);
1426     annot->highlightQuads().append(q);
1427 
1428     part.m_document->addPageAnnotation(0, annot);
1429 
1430     QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.5, height * 0.5));
1431     QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::OpenHandCursor);
1432 }
1433 
1434 void PartTest::testCheckBoxReadOnly()
1435 {
1436     const QString testFile = QStringLiteral(KDESRCDIR "data/checkbox_ro.pdf");
1437     Okular::Part part(nullptr, {});
1438     part.openDocument(testFile);
1439 
1440     // The test document uses the activation action of checkboxes
1441     // to update the read only state. For this we need the part so that
1442     // undo / redo activates the activation action.
1443 
1444     QVERIFY(part.m_document->isOpened());
1445 
1446     const Okular::Page *page = part.m_document->page(0);
1447 
1448     QMap<QString, Okular::FormField *> fields;
1449 
1450     // Field names in test document are:
1451     // CBMakeRW, CBMakeRO, TargetDefaultRO, TargetDefaultRW
1452 
1453     const QList<Okular::FormField *> pageFormFields = page->formFields();
1454     for (Okular::FormField *ff : pageFormFields) {
1455         fields.insert(ff->name(), static_cast<Okular::FormField *>(ff));
1456     }
1457 
1458     // First grab all fields and check that the setup is as expected.
1459     auto cbMakeRW = dynamic_cast<Okular::FormFieldButton *>(fields[QStringLiteral("CBMakeRW")]);
1460     auto cbMakeRO = dynamic_cast<Okular::FormFieldButton *>(fields[QStringLiteral("CBMakeRO")]);
1461 
1462     auto targetDefaultRW = dynamic_cast<Okular::FormFieldText *>(fields[QStringLiteral("TargetDefaultRw")]);
1463     auto targetDefaultRO = dynamic_cast<Okular::FormFieldText *>(fields[QStringLiteral("TargetDefaultRo")]);
1464 
1465     QVERIFY(cbMakeRW);
1466     QVERIFY(cbMakeRO);
1467     QVERIFY(targetDefaultRW);
1468     QVERIFY(targetDefaultRO);
1469 
1470     QVERIFY(!cbMakeRW->state());
1471     QVERIFY(!cbMakeRO->state());
1472 
1473     QVERIFY(!targetDefaultRW->isReadOnly());
1474     QVERIFY(targetDefaultRO->isReadOnly());
1475 
1476     QList<Okular::FormFieldButton *> btns;
1477     btns << cbMakeRW << cbMakeRO;
1478 
1479     // Now check both boxes
1480     QList<bool> btnStates;
1481     btnStates << true << true;
1482 
1483     part.m_document->editFormButtons(0, btns, btnStates);
1484 
1485     // Read only should be inverted
1486     QVERIFY(targetDefaultRW->isReadOnly());
1487     QVERIFY(!targetDefaultRO->isReadOnly());
1488 
1489     // Test that undo / redo works
1490     QVERIFY(part.m_document->canUndo());
1491     part.m_document->undo();
1492     QVERIFY(!targetDefaultRW->isReadOnly());
1493     QVERIFY(targetDefaultRO->isReadOnly());
1494 
1495     part.m_document->redo();
1496     QVERIFY(targetDefaultRW->isReadOnly());
1497     QVERIFY(!targetDefaultRO->isReadOnly());
1498 
1499     btnStates.clear();
1500     btnStates << false << true;
1501 
1502     part.m_document->editFormButtons(0, btns, btnStates);
1503     QVERIFY(targetDefaultRW->isReadOnly());
1504     QVERIFY(targetDefaultRO->isReadOnly());
1505 
1506     // Now set both to checked again and confirm that
1507     // save / load works.
1508     btnStates.clear();
1509     btnStates << true << true;
1510     part.m_document->editFormButtons(0, btns, btnStates);
1511 
1512     QTemporaryFile saveFile(QStringLiteral("%1/okrXXXXXX.pdf").arg(QDir::tempPath()));
1513     QVERIFY(saveFile.open());
1514     saveFile.close();
1515 
1516     // Save
1517     QVERIFY(part.saveAs(QUrl::fromLocalFile(saveFile.fileName()), Part::NoSaveAsFlags));
1518     part.closeUrl();
1519 
1520     // Load
1521     part.openDocument(saveFile.fileName());
1522     QVERIFY(part.m_document->isOpened());
1523 
1524     page = part.m_document->page(0);
1525 
1526     fields.clear();
1527 
1528     {
1529         const QList<Okular::FormField *> pageFormFields = page->formFields();
1530         for (Okular::FormField *ff : pageFormFields) {
1531             fields.insert(ff->name(), static_cast<Okular::FormField *>(ff));
1532         }
1533     }
1534 
1535     cbMakeRW = dynamic_cast<Okular::FormFieldButton *>(fields[QStringLiteral("CBMakeRW")]);
1536     cbMakeRO = dynamic_cast<Okular::FormFieldButton *>(fields[QStringLiteral("CBMakeRO")]);
1537 
1538     targetDefaultRW = dynamic_cast<Okular::FormFieldText *>(fields[QStringLiteral("TargetDefaultRw")]);
1539     targetDefaultRO = dynamic_cast<Okular::FormFieldText *>(fields[QStringLiteral("TargetDefaultRo")]);
1540 
1541     QVERIFY(cbMakeRW->state());
1542     QVERIFY(cbMakeRO->state());
1543     QVERIFY(targetDefaultRW->isReadOnly());
1544     QVERIFY(!targetDefaultRO->isReadOnly());
1545 }
1546 
1547 void PartTest::testCrashTextEditDestroy()
1548 {
1549     const QString testFile = QStringLiteral(KDESRCDIR "data/formSamples.pdf");
1550     Okular::Part part(nullptr, {});
1551     part.openDocument(testFile);
1552     part.widget()->show();
1553     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
1554 
1555     part.widget()->findChild<QTextEdit *>()->setText(QStringLiteral("HOLA"));
1556     part.actionCollection()->action(QStringLiteral("view_toggle_forms"))->trigger();
1557 }
1558 
1559 void PartTest::testAnnotWindow()
1560 {
1561     Okular::Part part(nullptr, {});
1562     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")));
1563     part.widget()->show();
1564     part.widget()->resize(800, 600);
1565     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
1566 
1567     part.m_document->setViewportPage(0);
1568 
1569     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseNormal"));
1570 
1571     QCOMPARE(part.m_document->currentPage(), 0u);
1572 
1573     // Create two distinct text annotations
1574     Okular::Annotation *annot1 = new Okular::TextAnnotation();
1575     annot1->setBoundingRectangle(Okular::NormalizedRect(0.8, 0.1, 0.85, 0.15));
1576     annot1->setContents(QStringLiteral("Annot contents 111111"));
1577 
1578     Okular::Annotation *annot2 = new Okular::TextAnnotation();
1579     annot2->setBoundingRectangle(Okular::NormalizedRect(0.8, 0.3, 0.85, 0.35));
1580     annot2->setContents(QStringLiteral("Annot contents 222222"));
1581 
1582     // Add annot1 and annot2 to document
1583     part.m_document->addPageAnnotation(0, annot1);
1584     part.m_document->addPageAnnotation(0, annot2);
1585     QVERIFY(part.m_document->page(0)->annotations().size() == 2);
1586 
1587     QTimer *delayResizeEventTimer = part.m_pageView->findChildren<QTimer *>(QStringLiteral("delayResizeEventTimer")).at(0);
1588     QVERIFY(delayResizeEventTimer->isActive());
1589     QTest::qWait(delayResizeEventTimer->interval() * 2);
1590 
1591     // wait for pixmap
1592     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
1593 
1594     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
1595     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
1596 
1597     // Double click the first annotation to open its window (move mouse for visual feedback)
1598     const NormalizedPoint annot1pt = annot1->boundingRectangle().center();
1599     QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot1pt.x, height * annot1pt.y));
1600     QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot1pt.x, height * annot1pt.y));
1601     QTRY_COMPARE(part.m_pageView->findChildren<QFrame *>(QStringLiteral("AnnotWindow")).size(), 1);
1602     // Verify that the window is visible
1603     QFrame *win1 = part.m_pageView->findChild<QFrame *>(QStringLiteral("AnnotWindow"));
1604     QVERIFY(!win1->visibleRegion().isEmpty());
1605 
1606     // Double click the second annotation to open its window (move mouse for visual feedback)
1607     const NormalizedPoint annot2pt = annot2->boundingRectangle().center();
1608     QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot2pt.x, height * annot2pt.y));
1609     QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot2pt.x, height * annot2pt.y));
1610     QTRY_COMPARE(part.m_pageView->findChildren<QFrame *>(QStringLiteral("AnnotWindow")).size(), 2);
1611     // Verify that the first window is hidden covered by the second, which is visible
1612     QList<QFrame *> lstWin = part.m_pageView->findChildren<QFrame *>(QStringLiteral("AnnotWindow"));
1613     QFrame *win2;
1614     if (lstWin[0] == win1) {
1615         win2 = lstWin[1];
1616     } else {
1617         win2 = lstWin[0];
1618     }
1619     QVERIFY(win1->visibleRegion().isEmpty());
1620     QVERIFY(!win2->visibleRegion().isEmpty());
1621 
1622     // Double click the first annotation to raise its window (move mouse for visual feedback)
1623     QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot1pt.x, height * annot1pt.y));
1624     QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot1pt.x, height * annot1pt.y));
1625     // Verify that the second window is hidden covered by the first, which is visible
1626     QVERIFY(!win1->visibleRegion().isEmpty());
1627     QVERIFY(win2->visibleRegion().isEmpty());
1628 
1629     // Move annotation window 1 to partially show annotation window 2
1630     win1->move(QPoint(win2->pos().x(), win2->pos().y() + 50));
1631     // Verify that both windows are partially visible
1632     QVERIFY(!win1->visibleRegion().isEmpty());
1633     QVERIFY(!win2->visibleRegion().isEmpty());
1634 
1635     // Click the second annotation window to raise it (move mouse for visual feedback)
1636     auto widget = win2->window()->childAt(win2->mapTo(win2->window(), QPoint(10, 10)));
1637     QTest::mouseMove(win2->window(), win2->mapTo(win2->window(), QPoint(10, 10)));
1638     QTest::mouseClick(widget, Qt::LeftButton, Qt::NoModifier, widget->mapFrom(win2, QPoint(10, 10)));
1639     QVERIFY(win1->visibleRegion().rectCount() == 3);
1640     QVERIFY(win2->visibleRegion().rectCount() == 4);
1641 }
1642 
1643 // Helper for testAdditionalActionTriggers
1644 static void verifyTargetStates(const QString &triggerName, const QMap<QString, Okular::FormField *> &fields, bool focusVisible, bool cursorVisible, bool mouseVisible, int line)
1645 {
1646     Okular::FormField *focusTarget = fields.value(triggerName + QStringLiteral("_focus_target"));
1647     Okular::FormField *cursorTarget = fields.value(triggerName + QStringLiteral("_cursor_target"));
1648     Okular::FormField *mouseTarget = fields.value(triggerName + QStringLiteral("_mouse_target"));
1649 
1650     QVERIFY(focusTarget);
1651     QVERIFY(cursorTarget);
1652     QVERIFY(mouseTarget);
1653 
1654     QTRY_VERIFY2(focusTarget->isVisible() == focusVisible, QStringLiteral("line: %1 focus for %2 not matched. Expected %3 Actual %4").arg(line).arg(triggerName).arg(focusTarget->isVisible()).arg(focusVisible).toUtf8().constData());
1655     QTRY_VERIFY2(cursorTarget->isVisible() == cursorVisible, QStringLiteral("line: %1 cursor for %2 not matched. Actual %3 Expected %4").arg(line).arg(triggerName).arg(cursorTarget->isVisible()).arg(cursorVisible).toUtf8().constData());
1656     QTRY_VERIFY2(mouseTarget->isVisible() == mouseVisible, QStringLiteral("line: %1 mouse for %2 not matched. Expected %3 Actual %4").arg(line).arg(triggerName).arg(mouseTarget->isVisible()).arg(mouseVisible).toUtf8().constData());
1657 }
1658 
1659 void PartTest::testAdditionalActionTriggers()
1660 {
1661     const QString testFile = QStringLiteral(KDESRCDIR "data/additionalFormActions.pdf");
1662     Okular::Part part(nullptr, QVariantList());
1663     part.openDocument(testFile);
1664     part.widget()->resize(800, 600);
1665 
1666     part.widget()->show();
1667     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
1668 
1669     QTimer *delayResizeEventTimer = part.m_pageView->findChildren<QTimer *>(QStringLiteral("delayResizeEventTimer")).at(0);
1670     QVERIFY(delayResizeEventTimer->isActive());
1671     QTest::qWait(delayResizeEventTimer->interval() * 2);
1672 
1673     part.m_document->setViewportPage(0);
1674 
1675     // wait for pixmap
1676     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
1677 
1678     QMap<QString, Okular::FormField *> fields;
1679     // Field names in test document are:
1680     // For trigger fields: tf, cb, rb, dd, pb
1681     // For target fields: <trigger_name>_focus_target, <trigger_name>_cursor_target,
1682     // <trigger_name>_mouse_target
1683     const Okular::Page *page = part.m_document->page(0);
1684     const QList<Okular::FormField *> pageFormFields = page->formFields();
1685     for (Okular::FormField *ff : pageFormFields) {
1686         fields.insert(ff->name(), static_cast<Okular::FormField *>(ff));
1687     }
1688 
1689     // Verify that everything is set up.
1690     verifyTargetStates(QStringLiteral("tf"), fields, true, true, true, __LINE__);
1691     verifyTargetStates(QStringLiteral("cb"), fields, true, true, true, __LINE__);
1692     verifyTargetStates(QStringLiteral("rb"), fields, true, true, true, __LINE__);
1693     verifyTargetStates(QStringLiteral("dd"), fields, true, true, true, __LINE__);
1694     verifyTargetStates(QStringLiteral("pb"), fields, true, true, true, __LINE__);
1695 
1696     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
1697     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
1698 
1699     part.actionCollection()->action(QStringLiteral("view_toggle_forms"))->trigger();
1700 
1701     QPoint tfPos(width * 0.045, height * 0.05);
1702     QPoint cbPos(width * 0.045, height * 0.08);
1703     QPoint rbPos(width * 0.045, height * 0.12);
1704     QPoint ddPos(width * 0.045, height * 0.16);
1705     QPoint pbPos(width * 0.045, height * 0.26);
1706 
1707     // Test text field
1708     auto widget = part.m_pageView->viewport()->childAt(tfPos);
1709     QVERIFY(widget);
1710 
1711     QTest::mouseMove(part.m_pageView->viewport(), QPoint(tfPos));
1712     verifyTargetStates(QStringLiteral("tf"), fields, true, false, true, __LINE__);
1713     QTest::mousePress(widget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
1714     verifyTargetStates(QStringLiteral("tf"), fields, false, false, false, __LINE__);
1715     QTest::mouseRelease(widget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
1716     verifyTargetStates(QStringLiteral("tf"), fields, false, false, true, __LINE__);
1717 
1718     // Checkbox
1719     widget = part.m_pageView->viewport()->childAt(cbPos);
1720     QVERIFY(widget);
1721 
1722     QTest::mouseMove(part.m_pageView->viewport(), QPoint(cbPos));
1723     verifyTargetStates(QStringLiteral("cb"), fields, true, false, true, __LINE__);
1724     QTest::mousePress(widget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
1725     verifyTargetStates(QStringLiteral("cb"), fields, false, false, false, __LINE__);
1726     // Confirm that the textfield no longer has any invisible
1727     verifyTargetStates(QStringLiteral("tf"), fields, true, true, true, __LINE__);
1728     QTest::mouseRelease(widget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
1729     verifyTargetStates(QStringLiteral("cb"), fields, false, false, true, __LINE__);
1730 
1731     // Radio
1732     widget = part.m_pageView->viewport()->childAt(rbPos);
1733     QVERIFY(widget);
1734 
1735     QTest::mouseMove(part.m_pageView->viewport(), QPoint(rbPos));
1736     verifyTargetStates(QStringLiteral("rb"), fields, true, false, true, __LINE__);
1737     QTest::mousePress(widget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
1738     verifyTargetStates(QStringLiteral("rb"), fields, false, false, false, __LINE__);
1739     QTest::mouseRelease(widget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
1740     verifyTargetStates(QStringLiteral("rb"), fields, false, false, true, __LINE__);
1741 
1742     // Dropdown
1743     widget = part.m_pageView->viewport()->childAt(ddPos);
1744     QVERIFY(widget);
1745 
1746     QTest::mouseMove(part.m_pageView->viewport(), QPoint(ddPos));
1747     verifyTargetStates(QStringLiteral("dd"), fields, true, false, true, __LINE__);
1748     QTest::mousePress(widget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
1749     verifyTargetStates(QStringLiteral("dd"), fields, false, false, false, __LINE__);
1750     QTest::mouseRelease(widget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
1751     verifyTargetStates(QStringLiteral("dd"), fields, false, false, true, __LINE__);
1752 
1753     // Pushbutton
1754     widget = part.m_pageView->viewport()->childAt(pbPos);
1755     QVERIFY(widget);
1756 
1757     QTest::mouseMove(part.m_pageView->viewport(), QPoint(pbPos));
1758     verifyTargetStates(QStringLiteral("pb"), fields, true, false, true, __LINE__);
1759     QTest::mousePress(widget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
1760     verifyTargetStates(QStringLiteral("pb"), fields, false, false, false, __LINE__);
1761     QTest::mouseRelease(widget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
1762     verifyTargetStates(QStringLiteral("pb"), fields, false, false, true, __LINE__);
1763 
1764     // Confirm that a mouse release outside does not trigger the show action.
1765     QTest::mousePress(widget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
1766     verifyTargetStates(QStringLiteral("pb"), fields, false, false, false, __LINE__);
1767     QTest::mouseRelease(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, tfPos);
1768     verifyTargetStates(QStringLiteral("pb"), fields, false, false, false, __LINE__);
1769 }
1770 
1771 void PartTest::testTypewriterAnnotTool()
1772 {
1773     Okular::Part part(nullptr, QVariantList());
1774 
1775     part.openUrl(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/file1.pdf")));
1776 
1777     part.widget()->show();
1778     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
1779 
1780     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
1781     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
1782 
1783     part.m_document->setViewportPage(0);
1784 
1785     // Find the TypeWriter annotation
1786     QAction *typeWriterAction = part.actionCollection()->action(QStringLiteral("annotation_typewriter"));
1787     QVERIFY(typeWriterAction);
1788 
1789     typeWriterAction->trigger();
1790 
1791     QTest::qWait(1000); // Wait for the "add new note" dialog to appear
1792     TestingUtils::CloseDialogHelper closeDialogHelper(QDialogButtonBox::Ok);
1793 
1794     QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.5, height * 0.2));
1795 
1796     Annotation *annot = part.m_document->page(0)->annotations().first();
1797     TextAnnotation *ta = static_cast<TextAnnotation *>(annot);
1798     QVERIFY(annot);
1799     QVERIFY(ta);
1800     QCOMPARE(annot->subType(), Okular::Annotation::AText);
1801     QCOMPARE(annot->style().color(), QColor(255, 255, 255, 0));
1802     QCOMPARE(ta->textType(), Okular::TextAnnotation::InPlace);
1803     QCOMPARE(ta->inplaceIntent(), Okular::TextAnnotation::TypeWriter);
1804 }
1805 
1806 void PartTest::testJumpToPage()
1807 {
1808     const QString testFile = QStringLiteral(KDESRCDIR "data/simple-multipage.pdf");
1809     const int targetPage = 25;
1810     Okular::Part part(nullptr, QVariantList());
1811     part.openDocument(testFile);
1812     part.widget()->resize(800, 600);
1813     part.widget()->show();
1814     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
1815 
1816     part.m_document->pages();
1817     part.m_document->setViewportPage(targetPage);
1818 
1819     /* Document::setViewportPage triggers pixmap rendering in another thread.
1820      * We want to test how things look AFTER finished signal arrives back,
1821      * because PageView::slotRelayoutPages may displace the viewport again.
1822      */
1823     QTRY_VERIFY(part.m_document->page(targetPage)->hasPixmap(part.m_pageView));
1824 
1825     const int contentAreaHeight = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
1826     const int pageWithSpaceTop = contentAreaHeight / part.m_document->pages() * targetPage;
1827 
1828     /*
1829      * This is a test for a "known by trial" displacement.
1830      * We'd need access to part.m_pageView->d->items[targetPage]->croppedGeometry().top(),
1831      * to determine the expected viewport position, but we don't have access.
1832      */
1833     QCOMPARE(part.m_pageView->verticalScrollBar()->value(), pageWithSpaceTop - 4);
1834 }
1835 
1836 void PartTest::testOpenAtPage()
1837 {
1838     const QString testFile = QStringLiteral(KDESRCDIR "data/simple-multipage.pdf");
1839     QUrl url = QUrl::fromLocalFile(testFile);
1840     Okular::Part part(nullptr, QVariantList());
1841 
1842     const uint targetPageNumA = 25;
1843     const uint expectedPageA = targetPageNumA - 1;
1844     url.setFragment(QString::number(targetPageNumA));
1845     part.openUrl(url);
1846     QCOMPARE(part.m_document->currentPage(), expectedPageA);
1847 
1848     // 'page=<pagenum>' param as specified in RFC 3778
1849     const uint targetPageNumB = 15;
1850     const uint expectedPageB = targetPageNumB - 1;
1851     url.setFragment(QStringLiteral("page=") + QString::number(targetPageNumB));
1852     part.openUrl(url);
1853     QCOMPARE(part.m_document->currentPage(), expectedPageB);
1854 }
1855 
1856 void PartTest::testForwardBackwardNavigation()
1857 {
1858     const QString testFile = QStringLiteral(KDESRCDIR "data/simple-multipage.pdf");
1859     Okular::Part part(nullptr, QVariantList());
1860     part.openDocument(testFile);
1861     part.widget()->resize(800, 600);
1862     part.widget()->show();
1863     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
1864 
1865     // Go to some page
1866     const int targetPageA = 15;
1867     part.m_document->setViewportPage(targetPageA);
1868 
1869     QVERIFY(part.m_document->viewport() == DocumentViewport(targetPageA));
1870 
1871     // Go to some other page
1872     const int targetPageB = 25;
1873     part.m_document->setViewportPage(targetPageB);
1874     QVERIFY(part.m_document->viewport() == DocumentViewport(targetPageB));
1875 
1876     // Go back to page A
1877     QVERIFY(QMetaObject::invokeMethod(&part, "slotHistoryBack"));
1878     QVERIFY(part.m_document->viewport().pageNumber == targetPageA);
1879 
1880     // Go back to page B
1881     QVERIFY(QMetaObject::invokeMethod(&part, "slotHistoryNext"));
1882     QVERIFY(part.m_document->viewport().pageNumber == targetPageB);
1883 }
1884 
1885 void PartTest::testTabletProximityBehavior()
1886 {
1887     QVariantList dummyArgs;
1888     Okular::Part part {nullptr, dummyArgs};
1889     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")));
1890     part.slotShowPresentation();
1891     PresentationWidget *w = part.m_presentationWidget;
1892     QVERIFY(w);
1893     part.widget()->show();
1894 
1895     // close the KMessageBox "There are two ways of exiting[...]"
1896     TestingUtils::CloseDialogHelper closeDialogHelper(w, QDialogButtonBox::Ok); // confirm the "To leave, press ESC"
1897 
1898     auto pointingDevice = new QPointingDevice(QStringLiteral("test"), 42, QInputDevice::DeviceType::Stylus, QPointingDevice::PointerType::Pen, QInputDevice::Capability::All, 3, 3);
1899     QTabletEvent enterProximityEvent {QEvent::TabletEnterProximity, pointingDevice, QPointF(10, 10), QPointF(10, 10), 1., 0, 0, 1., 1., 0, Qt::NoModifier, Qt::NoButton, Qt::NoButton};
1900     QTabletEvent leaveProximityEvent {QEvent::TabletLeaveProximity, pointingDevice, QPointF(10, 10), QPointF(10, 10), 1., 0, 0, 1., 1., 0, Qt::NoModifier, Qt::NoButton, Qt::NoButton};
1901 
1902     // Test with the Okular::Settings::EnumSlidesCursor::Visible setting
1903     Okular::Settings::self()->setSlidesCursor(Okular::Settings::EnumSlidesCursor::Visible);
1904 
1905     // Send an enterProximity event
1906     qApp->notify(qApp, &enterProximityEvent);
1907 
1908     // The cursor should be a cross-hair
1909     QVERIFY(w->cursor().shape() == Qt::CursorShape(Qt::CrossCursor));
1910 
1911     // Send a leaveProximity event
1912     qApp->notify(qApp, &leaveProximityEvent);
1913 
1914     // After the leaveProximityEvent, the cursor should be an arrow again, because
1915     // we have set the slidesCursor mode to 'Visible'
1916     QVERIFY(w->cursor().shape() == Qt::CursorShape(Qt::ArrowCursor));
1917 
1918     // Test with the Okular::Settings::EnumSlidesCursor::Hidden setting
1919     Okular::Settings::self()->setSlidesCursor(Okular::Settings::EnumSlidesCursor::Hidden);
1920 
1921     qApp->notify(qApp, &enterProximityEvent);
1922     QVERIFY(w->cursor().shape() == Qt::CursorShape(Qt::CrossCursor));
1923     qApp->notify(qApp, &leaveProximityEvent);
1924     QVERIFY(w->cursor().shape() == Qt::CursorShape(Qt::BlankCursor));
1925 
1926     // Moving the mouse should not bring the cursor back
1927     QTest::mouseMove(w, QPoint(100, 100));
1928     QVERIFY(w->cursor().shape() == Qt::CursorShape(Qt::BlankCursor));
1929 
1930     // First test with the Okular::Settings::EnumSlidesCursor::HiddenDelay setting
1931     Okular::Settings::self()->setSlidesCursor(Okular::Settings::EnumSlidesCursor::HiddenDelay);
1932 
1933     qApp->notify(qApp, &enterProximityEvent);
1934     QVERIFY(w->cursor().shape() == Qt::CursorShape(Qt::CrossCursor));
1935     qApp->notify(qApp, &leaveProximityEvent);
1936 
1937     // After the leaveProximityEvent, the cursor should be blank, because
1938     // we have set the slidesCursor mode to 'HiddenDelay'
1939     QVERIFY(w->cursor().shape() == Qt::CursorShape(Qt::BlankCursor));
1940 
1941     // Moving the mouse should bring the cursor back
1942     QTest::mouseMove(w, QPoint(150, 150));
1943     QVERIFY(w->cursor().shape() == Qt::CursorShape(Qt::ArrowCursor));
1944 }
1945 
1946 void PartTest::testOpenPrintPreview()
1947 {
1948     QVariantList dummyArgs;
1949     Okular::Part part(nullptr, dummyArgs);
1950     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")));
1951     part.widget()->show();
1952     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
1953     TestingUtils::CloseDialogHelper closeDialogHelper(QDialogButtonBox::Close);
1954     part.slotPrintPreview();
1955 }
1956 
1957 void PartTest::testMouseModeMenu()
1958 {
1959     QVariantList dummyArgs;
1960     Okular::Part part(nullptr, dummyArgs);
1961     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")));
1962 
1963     QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseNormal");
1964 
1965     // Get mouse mode menu action
1966     QAction *mouseModeAction = part.actionCollection()->action(QStringLiteral("mouse_selecttools"));
1967     QVERIFY(mouseModeAction);
1968     QMenu *mouseModeActionMenu = mouseModeAction->menu();
1969 
1970     // Test that actions are usable (not disabled)
1971     QVERIFY(mouseModeActionMenu->actions().at(0)->isEnabled());
1972     QVERIFY(mouseModeActionMenu->actions().at(1)->isEnabled());
1973     QVERIFY(mouseModeActionMenu->actions().at(2)->isEnabled());
1974 
1975     // Test activating area selection mode
1976     mouseModeActionMenu->actions().at(0)->trigger();
1977     QCOMPARE(Okular::Settings::mouseMode(), (int)Okular::Settings::EnumMouseMode::RectSelect);
1978 
1979     // Test activating text selection mode
1980     mouseModeActionMenu->actions().at(1)->trigger();
1981     QCOMPARE(Okular::Settings::mouseMode(), (int)Okular::Settings::EnumMouseMode::TextSelect);
1982 
1983     // Test activating table selection mode
1984     mouseModeActionMenu->actions().at(2)->trigger();
1985     QCOMPARE(Okular::Settings::mouseMode(), (int)Okular::Settings::EnumMouseMode::TableSelect);
1986 }
1987 
1988 void PartTest::testFullScreenRequest()
1989 {
1990     QVariantList dummyArgs;
1991     Okular::Part part(nullptr, dummyArgs);
1992 
1993     // Open file.  For this particular file, a dialog has to appear asking whether
1994     // one wants to comply with the wish to go to presentation mode directly.
1995     // Answer 'no'
1996     auto dialogHelper = std::make_unique<TestingUtils::CloseDialogHelper>(&part, QDialogButtonBox::No);
1997     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/RequestFullScreen.pdf")));
1998 
1999     // Check that we are not in presentation mode
2000     QEXPECT_FAIL("", "The presentation widget should not be shown because we clicked No in the dialog", Continue);
2001     QTRY_VERIFY_WITH_TIMEOUT(part.m_presentationWidget, 1000);
2002 
2003     // Reload the file.  The initial dialog should no appear again.
2004     // (This is https://bugs.kde.org/show_bug.cgi?id=361740)
2005     part.reload();
2006 
2007     // Open the file again.  Now we answer "yes, go to presentation mode"
2008     dialogHelper = std::make_unique<TestingUtils::CloseDialogHelper>(&part, QDialogButtonBox::Yes);
2009     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/RequestFullScreen.pdf")));
2010 
2011     // Test whether we really are in presentation mode
2012     QTRY_VERIFY(part.m_presentationWidget);
2013 }
2014 
2015 void PartTest::testZoomInFacingPages()
2016 {
2017     QVariantList dummyArgs;
2018     Okular::Part part(nullptr, dummyArgs);
2019     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf")));
2020     QAction *facingAction = part.m_pageView->findChild<QAction *>(QStringLiteral("view_render_mode_facing"));
2021     KSelectAction *zoomSelectAction = part.m_pageView->findChild<KSelectAction *>(QStringLiteral("zoom_to"));
2022     part.widget()->resize(600, 400);
2023     part.widget()->show();
2024     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
2025     facingAction->trigger();
2026     while (zoomSelectAction->currentText() != QStringLiteral("12%")) {
2027         QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotZoomOut"));
2028     }
2029     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
2030     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotZoomIn"));
2031     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotZoomIn"));
2032     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotZoomIn"));
2033     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotZoomIn"));
2034     QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotZoomIn"));
2035     QTRY_COMPARE(zoomSelectAction->currentText(), QStringLiteral("66%"));
2036 
2037     // Back to single mode
2038     part.m_pageView->findChild<QAction *>(QStringLiteral("view_render_mode_single"))->trigger();
2039 }
2040 
2041 void PartTest::testZoomWithCrop()
2042 {
2043     // We test that all zoom levels can be achieved with cropped pages, bug 342003
2044 
2045     QVariantList dummyArgs;
2046     Okular::Part part(nullptr, dummyArgs);
2047     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf")));
2048 
2049     KActionMenu *cropMenu = part.m_pageView->findChild<KActionMenu *>(QStringLiteral("view_trim_mode"));
2050     KToggleAction *cropAction = cropMenu->menu()->findChild<KToggleAction *>(QStringLiteral("view_trim_margins"));
2051     KSelectAction *zoomSelectAction = part.m_pageView->findChild<KSelectAction *>(QStringLiteral("zoom_to"));
2052 
2053     part.widget()->resize(600, 400);
2054     part.widget()->show();
2055     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
2056 
2057     // Activate "Trim Margins"
2058     QVERIFY(!Okular::Settings::trimMargins());
2059     cropAction->trigger();
2060     QVERIFY(Okular::Settings::trimMargins());
2061 
2062     // Wait for the bounding boxes
2063     QTRY_VERIFY(part.m_document->page(0)->isBoundingBoxKnown());
2064     QTRY_VERIFY(part.m_document->page(1)->isBoundingBoxKnown());
2065 
2066     // Zoom out
2067     for (int i = 0; i < 20; i++) {
2068         QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotZoomOut"));
2069     }
2070     QCOMPARE(zoomSelectAction->currentText(), QStringLiteral("12%"));
2071 
2072     // Zoom in and out and check that all zoom levels appear
2073     QSet<QString> zooms_ref {QStringLiteral("12%"),
2074                              QStringLiteral("25%"),
2075                              QStringLiteral("33%"),
2076                              QStringLiteral("50%"),
2077                              QStringLiteral("66%"),
2078                              QStringLiteral("75%"),
2079                              QStringLiteral("100%"),
2080                              QStringLiteral("125%"),
2081                              QStringLiteral("150%"),
2082                              QStringLiteral("200%"),
2083                              QStringLiteral("400%"),
2084                              QStringLiteral("800%"),
2085                              QStringLiteral("1,600%"),
2086                              QStringLiteral("2,500%"),
2087                              QStringLiteral("5,000%"),
2088                              QStringLiteral("10,000%")};
2089 
2090     for (int j = 0; j < 2; j++) {
2091         QSet<QString> zooms;
2092         for (int i = 0; i < 18; i++) {
2093             zooms << zoomSelectAction->currentText();
2094             QVERIFY(QMetaObject::invokeMethod(part.m_pageView, j == 0 ? "slotZoomIn" : "slotZoomOut"));
2095         }
2096 
2097         QVERIFY(zooms.contains(zooms_ref));
2098     }
2099 
2100     // Deactivate "Trim Margins"
2101     QVERIFY(Okular::Settings::trimMargins());
2102     cropAction->trigger();
2103     QVERIFY(!Okular::Settings::trimMargins());
2104 }
2105 
2106 void PartTest::testLinkWithCrop()
2107 {
2108     // We test that link targets are correct with cropping, related to bug 198427
2109 
2110     QVariantList dummyArgs;
2111     Okular::Part part(nullptr, dummyArgs);
2112     QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_internal_links.pdf")));
2113 
2114     KActionMenu *cropMenu = part.m_pageView->findChild<KActionMenu *>(QStringLiteral("view_trim_mode"));
2115     KToggleAction *cropAction = cropMenu->menu()->findChild<KToggleAction *>(QStringLiteral("view_trim_selection"));
2116 
2117     part.widget()->resize(600, 400);
2118     part.widget()->show();
2119     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
2120 
2121     // wait for pixmap
2122     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
2123 
2124     const int width = part.m_pageView->viewport()->width();
2125     const int height = part.m_pageView->viewport()->height();
2126 
2127     // Move to a location without a link
2128     QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.1, width * 0.1));
2129 
2130     // The cursor should be normal
2131     QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::CursorShape(Qt::OpenHandCursor));
2132 
2133     // Activate "Trim Margins"
2134     cropAction->trigger();
2135 
2136     // The cursor should be a cross-hair
2137     QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::CursorShape(Qt::CrossCursor));
2138 
2139     const int mouseStartY = height * 0.2;
2140     const int mouseEndY = height * 0.8;
2141     const int mouseStartX = width * 0.2;
2142     const int mouseEndX = width * 0.8;
2143 
2144     // Trim the page
2145     simulateMouseSelection(mouseStartX, mouseStartY, mouseEndX, mouseEndY, part.m_pageView->viewport());
2146 
2147     // We seem to have a trimmed view where we just by sheer luck ends up with mouse over a link at least sometimes
2148     // So move the mouse
2149     QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.1, width * 0.1));
2150 
2151     // The cursor should be normal again
2152     QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::CursorShape(Qt::OpenHandCursor));
2153 
2154     // Click a link
2155     const QPoint click(width * 0.2, height * 0.2);
2156     QTest::mouseMove(part.m_pageView->viewport(), click);
2157     QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, click);
2158 
2159     QTRY_VERIFY2_WITH_TIMEOUT(qAbs(part.m_document->viewport().rePos.normalizedY - 0.167102333237) < 0.01, qPrintable(QStringLiteral("We are at %1").arg(part.m_document->viewport().rePos.normalizedY)), 500);
2160 
2161     // Deactivate "Trim Margins"
2162     cropAction->trigger();
2163 }
2164 
2165 void PartTest::testFieldFormatting()
2166 {
2167     // Test field formatting. This has to be a parttest so that we
2168     // can properly test focus in / out which triggers formatting.
2169     const QString testFile = QStringLiteral(KDESRCDIR "data/fieldFormat.pdf");
2170     Okular::Part part(nullptr, QVariantList());
2171     part.openDocument(testFile);
2172     part.widget()->resize(800, 600);
2173 
2174     part.widget()->show();
2175     QVERIFY(QTest::qWaitForWindowExposed(part.widget()));
2176 
2177     // Field names in test document are:
2178     //
2179     // us_currency_fmt for formatting like "$ 1,234.56"
2180     // de_currency_fmt for formatting like "1.234,56 €"
2181     // de_simple_sum for calculation test and formatting like "1.234,56€"
2182     // date_mm_dd_yyyy for dates like "18/06/2018"
2183     // date_dd_mm_yyyy for dates like "06/18/2018"
2184     // percent_fmt for percent format like "100,00%" if you enter 1
2185     // time_HH_MM_fmt for times like "23:12"
2186     // time_HH_MM_ss_fmt for times like "23:12:34"
2187     // special_phone_number for an example of a special format selectable in Acrobat.
2188     QMap<QString, Okular::FormField *> fields;
2189     const Okular::Page *page = part.m_document->page(0);
2190     const auto formFields = page->formFields();
2191     for (Okular::FormField *ff : formFields) {
2192         fields.insert(ff->name(), static_cast<Okular::FormField *>(ff));
2193     }
2194 
2195     const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width();
2196     const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height();
2197 
2198     part.m_document->setViewportPage(0);
2199 
2200     // wait for pixmap
2201     QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView));
2202 
2203     part.actionCollection()->action(QStringLiteral("view_toggle_forms"))->trigger();
2204 
2205     // Note as of version 1.5:
2206     // The test document is prepared for future extensions to formatting for dates etc.
2207     // Currently we only have the number format to test.
2208     const auto ff_us = dynamic_cast<Okular::FormFieldText *>(fields.value(QStringLiteral("us_currency_fmt")));
2209     const auto ff_de = dynamic_cast<Okular::FormFieldText *>(fields.value(QStringLiteral("de_currency_fmt")));
2210     const auto ff_sum = dynamic_cast<Okular::FormFieldText *>(fields.value(QStringLiteral("de_simple_sum")));
2211 
2212     const QPoint usPos(width * 0.25, height * 0.025);
2213     const QPoint dePos(width * 0.25, height * 0.05);
2214     const QPoint deSumPos(width * 0.25, height * 0.075);
2215 
2216     const auto viewport = part.m_pageView->viewport();
2217 
2218     QVERIFY(viewport);
2219 
2220     auto usCurrencyWidget = dynamic_cast<QLineEdit *>(viewport->childAt(usPos));
2221     auto deCurrencyWidget = dynamic_cast<QLineEdit *>(viewport->childAt(dePos));
2222     auto sumCurrencyWidget = dynamic_cast<QLineEdit *>(viewport->childAt(deSumPos));
2223 
2224     // Check that the widgets were found at the right position
2225     QVERIFY(usCurrencyWidget);
2226     QVERIFY(deCurrencyWidget);
2227     QVERIFY(sumCurrencyWidget);
2228 
2229     QTest::mousePress(usCurrencyWidget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
2230     QTRY_VERIFY(usCurrencyWidget->hasFocus());
2231     // locale is en_US for this test. Enter a value and check it.
2232     usCurrencyWidget->setText(QStringLiteral("1234.56"));
2233     // Check that the internal text matches
2234     QCOMPARE(ff_us->text(), QStringLiteral("1234.56"));
2235 
2236     // Now move the focus to trigger formatting.
2237     QTest::mousePress(deCurrencyWidget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
2238     QTRY_VERIFY(deCurrencyWidget->hasFocus());
2239 
2240     QCOMPARE(usCurrencyWidget->text(), QStringLiteral("$ 1,234.56"));
2241     QCOMPARE(ff_us->text(), QStringLiteral("1234.56"));
2242 
2243     // And again with an invalid number
2244     QTest::mousePress(usCurrencyWidget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
2245     QTRY_VERIFY(usCurrencyWidget->hasFocus());
2246 
2247     usCurrencyWidget->setText(QStringLiteral("131.234,567"));
2248     QTest::mousePress(deCurrencyWidget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
2249     QTRY_VERIFY(deCurrencyWidget->hasFocus());
2250     // Check that the internal text still contains it.
2251     QCOMPARE(ff_us->text(), QStringLiteral("131.234,567"));
2252 
2253     // Just check that the text does not match the internal text.
2254     // We don't check for a concrete value to keep NaN handling flexible
2255     QVERIFY(ff_us->text() != usCurrencyWidget->text());
2256 
2257     // Move the focus back and modify it a bit more
2258     QTest::mousePress(usCurrencyWidget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
2259     QTRY_VERIFY(usCurrencyWidget->hasFocus());
2260 
2261     usCurrencyWidget->setText(QStringLiteral("1,234.567"));
2262     QTest::mousePress(deCurrencyWidget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
2263     QTRY_VERIFY(deCurrencyWidget->hasFocus());
2264 
2265     QCOMPARE(usCurrencyWidget->text(), QStringLiteral("$ 1,234.57"));
2266 
2267     // Sum should already match
2268     QCOMPARE(sumCurrencyWidget->text(), QStringLiteral("1.234,57€"));
2269 
2270     // Set a text in the de field
2271     deCurrencyWidget->setText(QStringLiteral("1,123,234.567"));
2272     QTest::mousePress(usCurrencyWidget, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5));
2273     QTRY_VERIFY(usCurrencyWidget->hasFocus());
2274 
2275     QCOMPARE(deCurrencyWidget->text(), QStringLiteral("1.123.234,57 €"));
2276     QCOMPARE(ff_de->text(), QStringLiteral("1,123,234.567"));
2277     QCOMPARE(sumCurrencyWidget->text(), QStringLiteral("1.124.469,13€"));
2278     QCOMPARE(ff_sum->text(), QStringLiteral("1,124,469.1340000000782310962677002"));
2279 }
2280 
2281 } // namespace Okular
2282 
2283 int main(int argc, char *argv[])
2284 {
2285     // Force consistent locale
2286     QLocale locale(QStringLiteral("en_US.UTF-8"));
2287     if (locale == QLocale::c()) { // This is the way to check if the above worked
2288         locale = QLocale(QLocale::English, QLocale::UnitedStates);
2289     }
2290 
2291     QLocale::setDefault(locale);
2292     qputenv("LC_ALL", "en_US.UTF-8"); // For UNIX, third-party libraries
2293 
2294     // Ensure consistent configs/caches
2295     QTemporaryDir homeDir; // QTemporaryDir automatically cleans up when it goes out of scope
2296     Q_ASSERT(homeDir.isValid());
2297     QByteArray homePath = QFile::encodeName(homeDir.path());
2298     qDebug() << homePath;
2299     qputenv("USERPROFILE", homePath);
2300     qputenv("HOME", homePath);
2301     qputenv("XDG_DATA_HOME", QByteArray(homePath + "/.local"));
2302     qputenv("XDG_CONFIG_HOME", QByteArray(homePath + "/.kde-unit-test/xdg/config"));
2303 
2304     // Disable fancy debug output
2305     qunsetenv("QT_MESSAGE_PATTERN");
2306 
2307     Okular::Settings::instance(QStringLiteral("okularparttest"));
2308 
2309     QApplication app(argc, argv);
2310     app.setApplicationName(QStringLiteral("okularparttest"));
2311     app.setOrganizationDomain(QStringLiteral("kde.org"));
2312     app.setQuitOnLastWindowClosed(false);
2313 
2314     qRegisterMetaType<QUrl>(); /*as done by kapplication*/
2315     qRegisterMetaType<QList<QUrl>>();
2316 
2317     Okular::PartTest test;
2318 
2319     return QTest::qExec(&test, argc, argv);
2320 }
2321 
2322 #include "parttest.moc"