diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp index a9825ac66..65069fb7f 100644 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -1,1904 +1,1904 @@ /*************************************************************************** * Copyright (C) 2013 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include #include "../core/annotations.h" #include "../core/form.h" #include "../core/page.h" #include "../part.h" #include "../ui/toc.h" #include "../ui/sidebar.h" #include "../ui/pageview.h" #include "../ui/presentationwidget.h" #include "../settings.h" #include "../generators/poppler/config-okular-poppler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class CloseDialogHelper : public QObject { Q_OBJECT public: CloseDialogHelper(Okular::Part *p, QDialogButtonBox::StandardButton b) : m_widget(p->widget()), m_button(b), m_clicked(false) { QTimer::singleShot(0, this, &CloseDialogHelper::closeDialog); } CloseDialogHelper(QWidget *w, QDialogButtonBox::StandardButton b) : m_widget(w), m_button(b), m_clicked(false) { QTimer::singleShot(0, this, &CloseDialogHelper::closeDialog); } ~CloseDialogHelper() { QVERIFY(m_clicked); } private slots: void closeDialog() { QDialog *dialog = m_widget->findChild(); if (!dialog) { QTimer::singleShot(0, this, &CloseDialogHelper::closeDialog); return; } QDialogButtonBox *buttonBox = dialog->findChild(); buttonBox->button(m_button)->click(); m_clicked = true; } private: QWidget *m_widget; QDialogButtonBox::StandardButton m_button; bool m_clicked; }; namespace Okular { class PartTest : public QObject { Q_OBJECT static bool openDocument(Okular::Part *part, const QString &filePath); signals: void urlHandler(const QUrl &url); private slots: void testReload(); void testCanceledReload(); void testTOCReload(); void testForwardPDF(); void testForwardPDF_data(); void testGeneratorPreferences(); void testSelectText(); void testClickInternalLink(); void testOpenUrlArguments(); void test388288(); void testSaveAs(); void testSaveAs_data(); void testSaveAsToNonExistingPath(); void testSaveAsToSymlink(); void testSaveIsSymlink(); void testSidebarItemAfterSaving(); void testSaveAsUndoStackAnnotations(); void testSaveAsUndoStackAnnotations_data(); void testSaveAsUndoStackForms(); void testSaveAsUndoStackForms_data(); void testMouseMoveOverLinkWhileInSelectionMode(); void testClickUrlLinkWhileInSelectionMode(); void testeTextSelectionOverAndAcrossLinks_data(); void testeTextSelectionOverAndAcrossLinks(); void testClickUrlLinkWhileLinkTextIsSelected(); void testRClickWhileLinkTextIsSelected(); void testRClickOverLinkWhileLinkTextIsSelected(); void testRClickOnSelectionModeShoulShowFollowTheLinkMenu(); void testClickAnywhereAfterSelectionShouldUnselect(); void testeRectSelectionStartingOnLinks(); void testCheckBoxReadOnly(); void testCrashTextEditDestroy(); void testAnnotWindow(); void testAdditionalActionTriggers(); void testJumpToPage(); void testTabletProximityBehavior(); private: void simulateMouseSelection(double startX, double startY, double endX, double endY, QWidget *target); }; class PartThatHijacksQueryClose : public Okular::Part { public: PartThatHijacksQueryClose(QWidget* parentWidget, QObject* parent, const QVariantList& args) : Okular::Part(parentWidget, parent, args), behavior(PassThru) {} enum Behavior { PassThru, ReturnTrue, ReturnFalse }; void setQueryCloseBehavior(Behavior new_behavior) { behavior = new_behavior; } bool queryClose() override { if (behavior == PassThru) return Okular::Part::queryClose(); else // ReturnTrue or ReturnFalse return (behavior == ReturnTrue); } private: Behavior behavior; }; bool PartTest::openDocument(Okular::Part *part, const QString &filePath) { part->openDocument( filePath ); return part->m_document->isOpened(); } // Test that Okular doesn't crash after a successful reload void PartTest::testReload() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")) ); part.reload(); qApp->processEvents(); } // Test that Okular doesn't crash after a canceled reload void PartTest::testCanceledReload() { QVariantList dummyArgs; PartThatHijacksQueryClose part(nullptr, nullptr, dummyArgs); QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")) ); // When queryClose() returns false, the reload operation is canceled (as if // the user had chosen Cancel in the "Save changes?" message box) part.setQueryCloseBehavior(PartThatHijacksQueryClose::ReturnFalse); part.reload(); qApp->processEvents(); } void PartTest::testTOCReload() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/tocreload.pdf")) ); QCOMPARE(part.m_toc->expandedNodes().count(), 0); part.m_toc->m_treeView->expandAll(); QCOMPARE(part.m_toc->expandedNodes().count(), 3); part.reload(); qApp->processEvents(); QCOMPARE(part.m_toc->expandedNodes().count(), 3); } void PartTest::testForwardPDF() { QFETCH(QString, dir); QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); // Create temp dir named like this: ${system temp dir}/${random string}/${dir} const QTemporaryDir tempDir; const QDir workDir(QDir(tempDir.path()).filePath(dir)); workDir.mkpath(QStringLiteral(".")); QFile f(QStringLiteral(KDESRCDIR "data/synctextest.tex")); const QString texDestination = workDir.path() + QStringLiteral("/synctextest.tex"); QVERIFY(f.copy(texDestination)); QProcess process; process.setWorkingDirectory(workDir.path()); const QString pdflatexPath(QStandardPaths::findExecutable(QStringLiteral("pdflatex"))); if (pdflatexPath.isEmpty()) { QFAIL("pdflatex executable not found, but needed for the test. Try installing the respective TeXLive packages."); } process.start(pdflatexPath, QStringList() << QStringLiteral("-synctex=1") << QStringLiteral("-interaction=nonstopmode") << texDestination); bool started = process.waitForStarted(); if (!started) { qDebug() << "start error:" << process.error(); qDebug() << "start stdout:" << process.readAllStandardOutput(); qDebug() << "start stderr:" << process.readAllStandardError(); } QVERIFY(started); process.waitForFinished(); if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { qDebug() << "exit error:" << process.error() << "status" << process.exitStatus() << "code" << process.exitCode(); qDebug() << "exit stdout:" << process.readAllStandardOutput(); qDebug() << "exit stderr:" << process.readAllStandardError(); } const QString pdfResult = workDir.path() + QStringLiteral("/synctextest.pdf"); QVERIFY(QFile::exists(pdfResult)); QVERIFY( openDocument(&part, pdfResult) ); part.m_document->setViewportPage(0); QCOMPARE(part.m_document->currentPage(), 0u); part.closeUrl(); QUrl u(QUrl::fromLocalFile(pdfResult)); u.setFragment(QStringLiteral("src:100") + texDestination); part.openUrl(u); QCOMPARE(part.m_document->currentPage(), 1u); } void PartTest::testForwardPDF_data() { QTest::addColumn("dir"); QTest::newRow("non-utf8") << QStringLiteral("synctextest"); //QStringliteral is broken on windows with non ascii chars so using QString::fromUtf8 QTest::newRow("utf8") << QString::fromUtf8("ßðđđŋßðđŋ"); } void PartTest::testGeneratorPreferences() { KConfigDialog * dialog; QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); // Test that we don't crash while opening the dialog dialog = part.slotGeneratorPreferences(); qApp->processEvents(); delete dialog; // closes the dialog and recursively destroys all widgets // Test that we don't crash while opening a new instance of the dialog // This catches attempts to reuse widgets that have been destroyed dialog = part.slotGeneratorPreferences(); qApp->processEvents(); delete dialog; } void PartTest::testSelectText() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf"))); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const int mouseY = height * 0.052; const int mouseStartX = width * 0.12; const int mouseEndX = width * 0.7; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); QApplication::clipboard()->clear(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection")); QCOMPARE(QApplication::clipboard()->text(), QStringLiteral("Hola que tal\n")); } void PartTest::testClickInternalLink() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf"))); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseNormal"); QCOMPARE(part.m_document->currentPage(), 0u); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.17, height * 0.05)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.17, height * 0.05)); QTRY_COMPARE(part.m_document->currentPage(), 1u); } // cursor switches to Hand when hovering over link in TextSelect mode. void PartTest::testMouseMoveOverLinkWhileInSelectionMode() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); // move mouse over link QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.250, height * 0.127)); // check if mouse icon changed to proper icon QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::PointingHandCursor); } // clicking on hyperlink jumps to destination in TextSelect mode. void PartTest::testClickUrlLinkWhileInSelectionMode() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); // overwrite urlHandler for 'mailto' urls QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "urlHandler"); QSignalSpy openUrlSignalSpy(this, &PartTest::urlHandler); // click on url QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.250, height * 0.127)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.250, height * 0.127)); // expect that the urlHandler signal was called QTRY_COMPARE(openUrlSignalSpy.count(), 1); QList arguments = openUrlSignalSpy.takeFirst(); QCOMPARE(arguments.at(0).value(), QUrl("mailto:foo@foo.bar")); } void PartTest::testeTextSelectionOverAndAcrossLinks_data() { QTest::addColumn("mouseStartX"); QTest::addColumn("mouseEndX"); QTest::addColumn("expectedResult"); // can text-select "over and across" hyperlink. QTest::newRow("start selection before link") << 0.1564 << 0.2943 << QStringLiteral(" a link: foo@foo.b"); // can text-select starting at text and ending selection in middle of hyperlink. QTest::newRow("start selection in the middle of the link") << 0.28 << 0.382 << QStringLiteral("o.bar\n"); // text selection works when selecting left to right or right to left QTest::newRow("start selection after link") << 0.40 << 0.05 << QStringLiteral("This is a link: foo@foo.bar\n"); } // can text-select "over and across" hyperlink. void PartTest::testeTextSelectionOverAndAcrossLinks() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.127; QFETCH(double, mouseStartX); QFETCH(double, mouseEndX); mouseStartX = width * mouseStartX; mouseEndX = width * mouseEndX; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); QApplication::clipboard()->clear(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection")); QFETCH(QString, expectedResult); QCOMPARE(QApplication::clipboard()->text(), expectedResult); } // can jump to link while there's an active selection of text. void PartTest::testClickUrlLinkWhileLinkTextIsSelected() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.127; const double mouseStartX = width * 0.13; const double mouseEndX = width * 0.40; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // overwrite urlHandler for 'mailto' urls QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "urlHandler"); QSignalSpy openUrlSignalSpy(this, &PartTest::urlHandler); // click on url const double mouseClickX = width * 0.2997; const double mouseClickY = height * 0.1293; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // expect that the urlHandler signal was called QTRY_COMPARE(openUrlSignalSpy.count(), 1); QList arguments = openUrlSignalSpy.takeFirst(); QCOMPARE(arguments.at(0).value(), QUrl("mailto:foo@foo.bar")); } // r-click on the selected text gives the "Go To:" content menu option void PartTest::testRClickWhileLinkTextIsSelected() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.162; const double mouseStartX = width * 0.42; const double mouseEndX = width * 0.60; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { // check if popup menu is active and visible QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains go-to link action QAction *goToAction = qobject_cast(menu->findChild(QStringLiteral("GoToAction"))); QVERIFY(goToAction); // check if the "follow this link" action is not visible QAction *processLinkAction = qobject_cast(menu->findChild(QStringLiteral("ProcessLinkAction"))); QVERIFY(!processLinkAction); // check if the "copy link address" action is not visible QAction *copyLinkLocation = qobject_cast(menu->findChild(QStringLiteral("CopyLinkLocationAction"))); QVERIFY(!copyLinkLocation); // close menu to continue test menu->close(); menuClosed = true; }); // click on url const double mouseClickX = width * 0.425; const double mouseClickY = height * 0.162; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // will continue after pop-menu get closed QTRY_VERIFY(menuClosed); } // r-click on the link gives the "follow this link" content menu option void PartTest::testRClickOverLinkWhileLinkTextIsSelected() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.162; const double mouseStartX = width * 0.42; const double mouseEndX = width * 0.60; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { // check if popup menu is active and visible QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains "follow this link" action QAction *processLinkAction = qobject_cast(menu->findChild(QStringLiteral("ProcessLinkAction"))); QVERIFY(processLinkAction); // check if the menu contains "copy link address" action QAction *copyLinkLocation = qobject_cast(menu->findChild(QStringLiteral("CopyLinkLocationAction"))); QVERIFY(copyLinkLocation); // close menu to continue test menu->close(); menuClosed = true; }); // click on url const double mouseClickX = width * 0.593; const double mouseClickY = height * 0.162; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // will continue after pop-menu get closed QTRY_VERIFY(menuClosed); } void PartTest::testRClickOnSelectionModeShoulShowFollowTheLinkMenu() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { // check if popup menu is active and visible QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains "Follow this link" action QAction *processLink = qobject_cast(menu->findChild(QStringLiteral("ProcessLinkAction"))); QVERIFY(processLink); // chek if the menu contains "Copy Link Address" action QAction *actCopyLinkLocation = qobject_cast(menu->findChild(QStringLiteral("CopyLinkLocationAction"))); QVERIFY(actCopyLinkLocation); // close menu to continue test menu->close(); menuClosed = true; }); // r-click on url const double mouseClickX = width * 0.604; const double mouseClickY = height * 0.162; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // will continue after pop-menu get closed QTRY_VERIFY(menuClosed); } void PartTest::testClickAnywhereAfterSelectionShouldUnselect() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.162; const double mouseStartX = width * 0.42; const double mouseEndX = width * 0.60; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // click on url const double mouseClickX = width * 0.10; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(mouseClickX, mouseY), 1000); QApplication::clipboard()->clear(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection")); // check if copied text is empty what means no text selected QVERIFY(QApplication::clipboard()->text().isEmpty()); } void PartTest::testeRectSelectionStartingOnLinks() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseSelect")); const double mouseStartY = height * 0.127; const double mouseEndY = height * 0.127; const double mouseStartX = width * 0.28; const double mouseEndX = width * 0.382; // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { QApplication::clipboard()->clear(); // check if popup menu is active and visible QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the copy selected text to clipboard is present QAction *copyAct = qobject_cast(menu->findChild(QStringLiteral("CopyTextToClipboard"))); QVERIFY(copyAct); menu->close(); menuClosed = true; }); simulateMouseSelection(mouseStartX, mouseStartY, mouseEndX, mouseEndY, part.m_pageView->viewport()); // wait menu get closed QTRY_VERIFY(menuClosed); } void PartTest::simulateMouseSelection(double startX, double startY, double endX, double endY, QWidget *target) { const int steps = 5; const double diffX = endX - startX; const double diffY = endY - startY; const double diffXStep = diffX / steps; const double diffYStep = diffY / steps; QTestEventList events; events.addMouseMove(QPoint(startX, startY)); events.addMousePress(Qt::LeftButton, Qt::NoModifier, QPoint(startX, startY)); for (int i = 0; i < steps - 1; ++i) { events.addMouseMove(QPoint(startX + i * diffXStep, startY + i * diffYStep)); events.addDelay(100); } events.addMouseMove(QPoint(endX, endY)); events.addDelay(100); events.addMouseRelease(Qt::LeftButton, Qt::NoModifier, QPoint(endX, endY)); events.simulate(target); } void PartTest::testSaveAsToNonExistingPath() { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( KDESRCDIR "data/file1.pdf" ); QString saveFilePath; { QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); saveFile.open(); saveFilePath = saveFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QVERIFY( !QFileInfo::exists( saveFilePath ) ); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFilePath ), Part::NoSaveAsFlags ) ); QFile::remove( saveFilePath ); } void PartTest::testSaveAsToSymlink() { #ifdef Q_OS_UNIX Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( KDESRCDIR "data/file1.pdf" ); QTemporaryFile newFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); newFile.open(); QString linkFilePath; { QTemporaryFile linkFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); linkFile.open(); linkFilePath = linkFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QFile::link( newFile.fileName(), linkFilePath ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); QVERIFY( part.saveAs( QUrl::fromLocalFile( linkFilePath ), Part::NoSaveAsFlags ) ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); QFile::remove( linkFilePath ); #endif } void PartTest::testSaveIsSymlink() { #ifdef Q_OS_UNIX Okular::Part part(nullptr, nullptr, QVariantList()); QString newFilePath; { QTemporaryFile newFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); newFile.open(); newFilePath = newFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QFile::copy( KDESRCDIR "data/file1.pdf", newFilePath ); QString linkFilePath; { QTemporaryFile linkFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); linkFile.open(); linkFilePath = linkFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QFile::link( newFilePath, linkFilePath ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); part.openDocument( linkFilePath ); QVERIFY( part.saveAs( QUrl::fromLocalFile( linkFilePath ), Part::NoSaveAsFlags ) ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); QFile::remove( newFilePath ); QFile::remove( linkFilePath ); #endif } void PartTest::testSaveAs() { QFETCH(QString, file); QFETCH(QString, extension); QFETCH(bool, nativelySupportsAnnotations); QFETCH(bool, canSwapBackingFile); QScopedPointer closeDialogHelper; QString annotName; QTemporaryFile archiveSave( QStringLiteral( "%1/okrXXXXXX.okular" ).arg( QDir::tempPath() ) ); QTemporaryFile nativeDirectSave( QStringLiteral( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); QTemporaryFile nativeFromArchiveFile( QStringLiteral( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); QVERIFY( archiveSave.open() ); archiveSave.close(); QVERIFY( nativeDirectSave.open() ); nativeDirectSave.close(); QVERIFY( nativeFromArchiveFile.open() ); nativeFromArchiveFile.close(); qDebug() << "Open file, add annotation and save both natively and to .okular"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( file ); part.m_document->documentInfo(); QCOMPARE(part.m_document->canSwapBackingFile(), canSwapBackingFile); Okular::Annotation *annot = new Okular::TextAnnotation(); annot->setBoundingRectangle( Okular::NormalizedRect( 0.1, 0.1, 0.15, 0.15 ) ); annot->setContents( QStringLiteral("annot contents") ); part.m_document->addPageAnnotation( 0, annot ); annotName = annot->uniqueName(); if ( canSwapBackingFile ) { if ( !nativelySupportsAnnotations ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); // For backends that don't support annotations natively we mark the part as still modified // after a save because we keep the annotation around but it will get lost if the user closes the app // so we want to give her a last chance to save on close with the "you have changes dialog" QCOMPARE( part.isModified(), !nativelySupportsAnnotations ); QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), Part::SaveAsOkularArchive ) ); } else { // We need to save to archive first otherwise we lose the annotation closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::Yes )); // this is the "you're going to lose the undo/redo stack" dialog QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), Part::SaveAsOkularArchive ) ); if ( !nativelySupportsAnnotations ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); } QCOMPARE( part.m_document->documentInfo().get( Okular::DocumentInfo::FilePath ), part.m_document->currentDocument().toDisplayString() ); part.closeUrl(); } qDebug() << "Open the .okular, check that the annotation is present and save to native"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( archiveSave.fileName() ); part.m_document->documentInfo(); QCOMPARE( part.m_document->page( 0 )->annotations().size(), 1 ); QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); if ( !nativelySupportsAnnotations ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeFromArchiveFile.fileName() ), Part::NoSaveAsFlags ) ); if ( canSwapBackingFile && !nativelySupportsAnnotations ) { // For backends that don't support annotations natively we mark the part as still modified // after a save because we keep the annotation around but it will get lost if the user closes the app // so we want to give her a last chance to save on close with the "you have changes dialog" closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "do you want to save or discard" dialog } QCOMPARE( part.m_document->documentInfo().get( Okular::DocumentInfo::FilePath ), part.m_document->currentDocument().toDisplayString() ); part.closeUrl(); } qDebug() << "Open the native file saved directly, and check that the annot" << "is there iff we expect it"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( nativeDirectSave.fileName() ); QCOMPARE( part.m_document->page( 0 )->annotations().size(), nativelySupportsAnnotations ? 1 : 0 ); if ( nativelySupportsAnnotations ) QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); part.closeUrl(); } qDebug() << "Open the native file saved from the .okular, and check that the annot" << "is there iff we expect it"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( nativeFromArchiveFile.fileName() ); QCOMPARE( part.m_document->page( 0 )->annotations().size(), nativelySupportsAnnotations ? 1 : 0 ); if ( nativelySupportsAnnotations ) QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); part.closeUrl(); } } void PartTest::testSaveAs_data() { QTest::addColumn("file"); QTest::addColumn("extension"); QTest::addColumn("nativelySupportsAnnotations"); QTest::addColumn("canSwapBackingFile"); QTest::newRow("pdf") << KDESRCDIR "data/file1.pdf" << "pdf" << true << true; QTest::newRow("pdf.gz") << KDESRCDIR "data/file1.pdf.gz" << "pdf" << true << true; QTest::newRow("epub") << KDESRCDIR "data/contents.epub" << "epub" << false << false; QTest::newRow("jpg") << KDESRCDIR "data/potato.jpg" << "jpg" << false << true; } void PartTest::testSidebarItemAfterSaving() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QWidget *currentSidebarItem = part.m_sidebar->currentItem(); // thumbnails openDocument(&part, QStringLiteral(KDESRCDIR "data/tocreload.pdf")); // since it has TOC it changes to TOC QVERIFY(currentSidebarItem != part.m_sidebar->currentItem()); // now change back to thumbnails part.m_sidebar->setCurrentItem(currentSidebarItem); part.saveAs(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/tocreload.pdf"))); // Check it is still thumbnails after saving QCOMPARE(currentSidebarItem, part.m_sidebar->currentItem()); } void PartTest::testSaveAsUndoStackAnnotations() { QFETCH(QString, file); QFETCH(QString, extension); QFETCH(bool, nativelySupportsAnnotations); QFETCH(bool, canSwapBackingFile); QFETCH(bool, saveToArchive); const Part::SaveAsFlag saveFlags = saveToArchive ? Part::SaveAsOkularArchive : Part::NoSaveAsFlags; QScopedPointer closeDialogHelper; // closeDialogHelper relies on the availability of the "Continue" button to drop changes // when saving to a file format not supporting those. However, this button is only sensible // and available for "Save As", but not for "Save". By alternately saving to saveFile1 and // saveFile2 we always force "Save As", so closeDialogHelper keeps working. QTemporaryFile saveFile1( QStringLiteral( "%1/okrXXXXXX_1.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); QVERIFY( saveFile1.open() ); saveFile1.close(); QTemporaryFile saveFile2( QStringLiteral( "%1/okrXXXXXX_2.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); QVERIFY( saveFile2.open() ); saveFile2.close(); Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( file ); QCOMPARE(part.m_document->canSwapBackingFile(), canSwapBackingFile); Okular::Annotation *annot = new Okular::TextAnnotation(); annot->setBoundingRectangle( Okular::NormalizedRect( 0.1, 0.1, 0.15, 0.15 ) ); annot->setContents( QStringLiteral("annot contents") ); part.m_document->addPageAnnotation( 0, annot ); QString annotName = annot->uniqueName(); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); if (!canSwapBackingFile) { // The undo/redo stack gets lost if you can not swap the backing file QVERIFY( !part.m_document->canUndo() ); QVERIFY( !part.m_document->canRedo() ); return; } // Check we can still undo the annot add after save QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Check we can redo the annot add after save QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( !part.m_document->canRedo() ); if ( nativelySupportsAnnotations ) { // If the annots are provided by the backend we need to refetch the pointer after save annot = part.m_document->page( 0 )->annotation( annotName ); QVERIFY( annot ); } // Remove the annotation, creates another undo command QVERIFY( part.m_document->canRemovePageAnnotation( annot ) ); part.m_document->removePageAnnotation( 0, annot ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Check we can still undo the annot remove after save QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); QCOMPARE( part.m_document->page( 0 )->annotations().count(), 1 ); // Check we can still undo the annot add after save if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Redo the add annotation QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canUndo() ); QVERIFY( part.m_document->canRedo() ); if ( nativelySupportsAnnotations ) { // If the annots are provided by the backend we need to refetch the pointer after save annot = part.m_document->page( 0 )->annotation( annotName ); QVERIFY( annot ); } // Add translate, adjust and modify commands part.m_document->translatePageAnnotation( 0, annot, Okular::NormalizedPoint( 0.1, 0.1 ) ); part.m_document->adjustPageAnnotation( 0, annot, Okular::NormalizedPoint( 0.1, 0.1 ), Okular::NormalizedPoint( 0.1, 0.1 ) ); part.m_document->prepareToModifyAnnotationProperties( annot ); part.m_document->modifyPageAnnotationProperties( 0, annot ); // Now check we can still undo/redo/save at all the intermediate states and things still work if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.m_document->canRedo() ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( !part.m_document->canRedo() ); closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "do you want to save or discard" dialog part.closeUrl(); } void PartTest::testSaveAsUndoStackAnnotations_data() { QTest::addColumn("file"); QTest::addColumn("extension"); QTest::addColumn("nativelySupportsAnnotations"); QTest::addColumn("canSwapBackingFile"); QTest::addColumn("saveToArchive"); QTest::newRow("pdf") << KDESRCDIR "data/file1.pdf" << "pdf" << true << true << false; QTest::newRow("epub") << KDESRCDIR "data/contents.epub" << "epub" << false << false << false; QTest::newRow("jpg") << KDESRCDIR "data/potato.jpg" << "jpg" << false << true << false; QTest::newRow("pdfarchive") << KDESRCDIR "data/file1.pdf" << "okular" << true << true << true; QTest::newRow("jpgarchive") << KDESRCDIR "data/potato.jpg" << "okular" << false << true << true; } void PartTest::testSaveAsUndoStackForms() { QFETCH(QString, file); QFETCH(QString, extension); QFETCH(bool, saveToArchive); const Part::SaveAsFlag saveFlags = saveToArchive ? Part::SaveAsOkularArchive : Part::NoSaveAsFlags; QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath(), extension ) ); QVERIFY( saveFile.open() ); saveFile.close(); Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( file ); for ( FormField *ff : part.m_document->page( 0 )->formFields() ) { if ( ff->id() == 65537 ) { QCOMPARE( ff->type(), FormField::FormText ); FormFieldText *fft = static_cast( ff ); part.m_document->editFormText( 0, fft, QStringLiteral("BlaBla"), 6, 0, 0 ); } else if ( ff->id() == 65538 ) { QCOMPARE( ff->type(), FormField::FormButton ); FormFieldButton *ffb = static_cast( ff ); QCOMPARE( ffb->buttonType(), FormFieldButton::Radio ); part.m_document->editFormButtons( 0, QList< FormFieldButton* >() << ffb, QList< bool >() << true ); } else if ( ff->id() == 65542 ) { QCOMPARE( ff->type(), FormField::FormChoice ); FormFieldChoice *ffc = static_cast( ff ); QCOMPARE( ffc->choiceType(), FormFieldChoice::ListBox ); part.m_document->editFormList( 0, ffc, QList< int >() << 1 ); } else if ( ff->id() == 65543 ) { QCOMPARE( ff->type(), FormField::FormChoice ); FormFieldChoice *ffc = static_cast( ff ); QCOMPARE( ffc->choiceType(), FormFieldChoice::ComboBox ); part.m_document->editFormCombo( 0, ffc, QStringLiteral("combo2"), 3, 0, 0); } } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); } void PartTest::testSaveAsUndoStackForms_data() { QTest::addColumn("file"); QTest::addColumn("extension"); QTest::addColumn("saveToArchive"); QTest::newRow("pdf") << KDESRCDIR "data/formSamples.pdf" << "pdf" << false; QTest::newRow("pdfarchive") << KDESRCDIR "data/formSamples.pdf" << "okular" << true; } void PartTest::testOpenUrlArguments() { QVariantList dummyArgs; Okular::Part part(NULL, NULL, dummyArgs); KParts::OpenUrlArguments args; args.setMimeType(QStringLiteral("text/rtf")); part.setArguments(args); part.openUrl(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/file1.pdf"))); QCOMPARE( part.arguments().mimeType(), QStringLiteral("text/rtf") ); } void PartTest::test388288() { Okular::Part part(nullptr, nullptr, QVariantList()); part.openUrl(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/file1.pdf"))); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); QMetaObject::invokeMethod(part.m_pageView, "slotToggleAnnotator", Q_ARG( bool, true )); auto annot = new Okular::HighlightAnnotation(); annot->setHighlightType( Okular::HighlightAnnotation::Highlight ); const Okular::NormalizedRect r(0.36, 0.16, 0.51, 0.17); annot->setBoundingRectangle( r ); Okular::HighlightAnnotation::Quad q; q.setCapStart( false ); q.setCapEnd( false ); q.setFeather( 1.0 ); q.setPoint( Okular::NormalizedPoint( r.left, r.bottom ), 0 ); q.setPoint( Okular::NormalizedPoint( r.right, r.bottom ), 1 ); q.setPoint( Okular::NormalizedPoint( r.right, r.top ), 2 ); q.setPoint( Okular::NormalizedPoint( r.left, r.top ), 3 ); annot->highlightQuads().append( q ); part.m_document->addPageAnnotation( 0, annot ); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.5, height * 0.5)); QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::OpenHandCursor); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.4, height * 0.165)); QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::ArrowCursor); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.1, height * 0.165)); part.m_document->undo(); annot = new Okular::HighlightAnnotation(); annot->setHighlightType( Okular::HighlightAnnotation::Highlight ); annot->setBoundingRectangle( r ); annot->highlightQuads().append( q ); part.m_document->addPageAnnotation( 0, annot ); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.5, height * 0.5)); QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::OpenHandCursor); } void PartTest::testCheckBoxReadOnly() { #ifndef HAVE_POPPLER_0_64 return; #endif const QString testFile = QStringLiteral( KDESRCDIR "data/checkbox_ro.pdf" ); Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); // The test document uses the activation action of checkboxes // to update the read only state. For this we need the part so that // undo / redo activates the activation action. QVERIFY( part.m_document->isOpened() ); const Okular::Page* page = part.m_document->page( 0 ); QMap fields; // Field names in test document are: // CBMakeRW, CBMakeRO, TargetDefaultRO, TargetDefaultRW for ( Okular::FormField *ff: page->formFields() ) { fields.insert( ff->name(), static_cast< Okular::FormField* >( ff ) ); } // First grab all fields and check that the setup is as expected. auto cbMakeRW = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRW" )] ); auto cbMakeRO = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRO" )] ); auto targetDefaultRW = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRw" )] ); auto targetDefaultRO = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRo" )] ); QVERIFY( cbMakeRW ); QVERIFY( cbMakeRO ); QVERIFY( targetDefaultRW ); QVERIFY( targetDefaultRO ); QVERIFY( !cbMakeRW->state() ); QVERIFY( !cbMakeRO->state() ); QVERIFY( !targetDefaultRW->isReadOnly() ); QVERIFY( targetDefaultRO->isReadOnly() ); QList< Okular::FormFieldButton* > btns; btns << cbMakeRW << cbMakeRO; // Now check both boxes QList< bool > btnStates; btnStates << true << true; part.m_document->editFormButtons( 0, btns, btnStates ); // Read only should be inverted QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( !targetDefaultRO->isReadOnly() ); // Test that undo / redo works QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !targetDefaultRW->isReadOnly() ); QVERIFY( targetDefaultRO->isReadOnly() ); part.m_document->redo(); QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( !targetDefaultRO->isReadOnly() ); btnStates.clear(); btnStates << false << true; part.m_document->editFormButtons( 0, btns, btnStates ); QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( targetDefaultRO->isReadOnly() ); // Now set both to checked again and confirm that // save / load works. btnStates.clear(); btnStates << true << true; part.m_document->editFormButtons( 0, btns, btnStates ); QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); QVERIFY( saveFile.open() ); saveFile.close(); // Save QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), Part::NoSaveAsFlags ) ); part.closeUrl(); // Load part.openDocument( saveFile.fileName() ); QVERIFY( part.m_document->isOpened() ); page = part.m_document->page( 0 ); fields.clear(); for ( Okular::FormField *ff: page->formFields() ) { fields.insert( ff->name(), static_cast< Okular::FormField* >( ff ) ); } cbMakeRW = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRW" )] ); cbMakeRO = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRO" )] ); targetDefaultRW = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRw" )] ); targetDefaultRO = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRo" )] ); QVERIFY( cbMakeRW->state() ); QVERIFY( cbMakeRO->state() ); QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( !targetDefaultRO->isReadOnly() ); } void PartTest::testCrashTextEditDestroy() { const QString testFile = QStringLiteral( KDESRCDIR "data/formSamples.pdf" ); Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.widget()->findChild()->setText(QStringLiteral("HOLA")); part.actionCollection()->action(QStringLiteral("view_toggle_forms"))->trigger(); } void PartTest::testAnnotWindow() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf"))); part.widget()->show(); part.widget()->resize(800, 600); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseNormal"); QCOMPARE(part.m_document->currentPage(), 0u); // Create two distinct text annotations Okular::Annotation * annot1 = new Okular::TextAnnotation(); annot1->setBoundingRectangle( Okular::NormalizedRect( 0.8, 0.1, 0.85, 0.15 ) ); annot1->setContents( QStringLiteral("Annot contents 111111") ); Okular::Annotation *annot2 = new Okular::TextAnnotation(); annot2->setBoundingRectangle( Okular::NormalizedRect( 0.8, 0.3, 0.85, 0.35 ) ); annot2->setContents( QStringLiteral("Annot contents 222222") ); // Add annot1 and annot2 to document part.m_document->addPageAnnotation( 0, annot1 ); QTest::qWait(100); part.m_document->addPageAnnotation( 0, annot2 ); QTest::qWait(100); QVERIFY( part.m_document->page( 0 )->annotations().size() == 2 ); // Double click the first annotation to open its window (move mouse for visual feedback) const NormalizedPoint annot1pt = annot1->boundingRectangle().center(); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot1pt.x, height * annot1pt.y)); QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot1pt.x, height * annot1pt.y)); QTRY_COMPARE( part.m_pageView->findChildren("AnnotWindow").size(), 1 ); // Verify that the window is visible QFrame* win1 = part.m_pageView->findChild(QStringLiteral("AnnotWindow")); QVERIFY( !win1->visibleRegion().isEmpty() ); // Double click the second annotation to open its window (move mouse for visual feedback) const NormalizedPoint annot2pt = annot2->boundingRectangle().center(); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot2pt.x, height * annot2pt.y)); QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot2pt.x, height * annot2pt.y)); QTRY_COMPARE( part.m_pageView->findChildren("AnnotWindow").size(), 2 ); // Verify that the first window is hidden covered by the second, which is visible QList lstWin = part.m_pageView->findChildren(QStringLiteral("AnnotWindow")); QFrame * win2; if (lstWin[0] == win1) { win2 = lstWin[1]; } else { win2 = lstWin[0]; } QVERIFY( win1->visibleRegion().isEmpty() ); QVERIFY( !win2->visibleRegion().isEmpty() ); // Double click the first annotation to raise its window (move mouse for visual feedback) QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot1pt.x, height * annot1pt.y)); QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot1pt.x, height * annot1pt.y)); // Verify that the second window is hidden covered by the first, which is visible QVERIFY( !win1->visibleRegion().isEmpty() ); QVERIFY( win2->visibleRegion().isEmpty() ); // Move annotation window 1 to partially show annotation window 2 win1->move(QPoint(win2->pos().x(), win2->pos().y() + 50)); // Verify that both windows are partially visible QVERIFY( !win1->visibleRegion().isEmpty() ); QVERIFY( !win2->visibleRegion().isEmpty() ); // Click the second annotation window to raise it (move mouse for visual feedback) auto widget = win2->window()->childAt(win2->mapTo(win2->window(), QPoint(10, 10))); QTest::mouseMove(win2->window(), win2->mapTo(win2->window(), QPoint(10, 10))); QTest::mouseClick(widget, Qt::LeftButton, Qt::NoModifier, widget->mapFrom(win2, QPoint(10, 10))); QVERIFY( win1->visibleRegion().rectCount() == 3); QVERIFY( win2->visibleRegion().rectCount() == 4); } // Helper for testAdditionalActionTriggers static void verifyTargetStates( const QString & triggerName, const QMap &fields, bool focusVisible, bool cursorVisible, bool mouseVisible, int line) { Okular::FormField *focusTarget = fields.value( triggerName + QStringLiteral ("_focus_target") ); Okular::FormField *cursorTarget = fields.value( triggerName + QStringLiteral ("_cursor_target") ); Okular::FormField *mouseTarget = fields.value( triggerName + QStringLiteral ("_mouse_target") ); QVERIFY( focusTarget ); QVERIFY( cursorTarget ); QVERIFY( mouseTarget ); 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() ); 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() ); 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() ); } void PartTest::testAdditionalActionTriggers() { #ifndef HAVE_POPPLER_0_65 return; #endif const QString testFile = QStringLiteral( KDESRCDIR "data/additionalFormActions.pdf" ); Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); part.widget()->resize(800, 600); part.widget()->show(); QVERIFY( QTest::qWaitForWindowExposed( part.widget() ) ); QMap fields; // Field names in test document are: // For trigger fields: tf, cb, rb, dd, pb // For target fields: _focus_target, _cursor_target, // _mouse_target const Okular::Page* page = part.m_document->page( 0 ); for ( Okular::FormField *ff: page->formFields() ) { fields.insert( ff->name(), static_cast< Okular::FormField* >( ff ) ); } // Verify that everything is set up. verifyTargetStates( QStringLiteral( "tf" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "cb" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "rb" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "dd" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "pb" ), fields, true, true, true, __LINE__ ); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage( 0 ); // wait for pixmap QTRY_VERIFY( part.m_document->page( 0 )->hasPixmap( part.m_pageView) ); part.actionCollection()->action( QStringLiteral( "view_toggle_forms" ) )->trigger(); QPoint tfPos( width * 0.045, height * 0.05 ); QPoint cbPos( width * 0.045, height * 0.08 ); QPoint rbPos( width * 0.045, height * 0.12 ); QPoint ddPos( width * 0.045, height * 0.16 ); QPoint pbPos( width * 0.045, height * 0.26 ); // Test text field auto widget = part.m_pageView->viewport()->childAt( tfPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( tfPos )); verifyTargetStates( QStringLiteral( "tf" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "tf" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "tf" ), fields, false, false, true, __LINE__ ); // Checkbox widget = part.m_pageView->viewport()->childAt( cbPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( cbPos ) ); verifyTargetStates( QStringLiteral( "cb" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "cb" ), fields, false, false, false, __LINE__ ); // Confirm that the textfield no longer has any invisible verifyTargetStates( QStringLiteral( "tf" ), fields, true, true, true, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "cb" ), fields, false, false, true, __LINE__ ); // Radio widget = part.m_pageView->viewport()->childAt( rbPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( rbPos ) ); verifyTargetStates( QStringLiteral( "rb" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "rb" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "rb" ), fields, false, false, true, __LINE__ ); // Dropdown widget = part.m_pageView->viewport()->childAt( ddPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( ddPos ) ); verifyTargetStates( QStringLiteral( "dd" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "dd" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "dd" ), fields, false, false, true, __LINE__ ); // Pushbutton widget = part.m_pageView->viewport()->childAt( pbPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( pbPos ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, true, __LINE__ ); // Confirm that a mouse release outside does not trigger the show action. QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, tfPos ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, false, __LINE__ ); } void PartTest::testJumpToPage() { const QString testFile = QStringLiteral( KDESRCDIR "data/simple-multipage.pdf" ); const int targetPage = 25; Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); part.widget()->resize(800, 600); part.widget()->show(); QVERIFY( QTest::qWaitForWindowExposed( part.widget() ) ); part.m_document->pages(); part.m_document->setViewportPage( targetPage ); /* Document::setViewportPage triggers pixmap rendering in another thread. * We want to test how things look AFTER finished signal arrives back, * because PageView::slotRelayoutPages may displace the viewport again. */ QTRY_VERIFY( part.m_document->page( targetPage )->hasPixmap( part.m_pageView ) ); const int contentAreaHeight = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); const int pageWithSpaceTop = contentAreaHeight / part.m_document->pages() * targetPage; /* * This is a test for a "known by trial" displacement. * We'd need access to part.m_pageView->d->items[targetPage]->croppedGeometry().top(), * to determine the expected viewport position, but we don't have access. */ QCOMPARE(part.m_pageView->verticalScrollBar()->value(), pageWithSpaceTop - 4); } void PartTest::testTabletProximityBehavior() { QVariantList dummyArgs; Okular::Part part{ nullptr, nullptr, dummyArgs }; QVERIFY( openDocument( &part, QStringLiteral( KDESRCDIR "data/file1.pdf" ) ) ); part.slotShowPresentation(); PresentationWidget *w = part.m_presentationWidget; QVERIFY( w ); part.widget()->show(); // close the KMessageBox "There are two ways of exiting[...]" CloseDialogHelper closeDialogHelper( w, QDialogButtonBox::Ok ); // confirm the "To leave, press ESC" QTabletEvent enterProximityEvent{ QEvent::TabletEnterProximity, QPoint( 10, 10 ), QPoint( 10, 10 ), QTabletEvent::Stylus, QTabletEvent::Pen, 1., 0, 0, 1., 1., 0, Qt::NoModifier, 0, Qt::NoButton, Qt::NoButton }; QTabletEvent leaveProximityEvent{ QEvent::TabletLeaveProximity, QPoint( 10, 10 ), QPoint( 10, 10 ), QTabletEvent::Stylus, QTabletEvent::Pen, 1., 0, 0, 1., 1., 0, Qt::NoModifier, 0, Qt::NoButton, Qt::NoButton }; // Test with the Okular::Settings::EnumSlidesCursor::Visible setting Okular::Settings::self()->setSlidesCursor( Okular::Settings::EnumSlidesCursor::Visible ); // Send an enterProximity event qApp->notify( qApp, &enterProximityEvent ); // The cursor should be a cross-hair QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::CrossCursor ) ); // Send a leaveProximity event qApp->notify( qApp, &leaveProximityEvent ); // After the leaveProximityEvent, the cursor should be an arrow again, because // we have set the slidesCursor mode to 'Visible' QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::ArrowCursor ) ); // Test with the Okular::Settings::EnumSlidesCursor::Hidden setting Okular::Settings::self()->setSlidesCursor( Okular::Settings::EnumSlidesCursor::Hidden ); qApp->notify( qApp, &enterProximityEvent ); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::CrossCursor ) ); qApp->notify( qApp, &leaveProximityEvent ); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::BlankCursor ) ); // Moving the mouse should not bring the cursor back QTest::mouseMove( w, QPoint( 100, 100 ) ); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::BlankCursor ) ); // First test with the Okular::Settings::EnumSlidesCursor::HiddenDelay setting Okular::Settings::self()->setSlidesCursor( Okular::Settings::EnumSlidesCursor::HiddenDelay ); qApp->notify( qApp, &enterProximityEvent ); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::CrossCursor ) ); qApp->notify( qApp, &leaveProximityEvent ); // After the leaveProximityEvent, the cursor should be blank, because // we have set the slidesCursor mode to 'HiddenDelay' QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::BlankCursor ) ); // Moving the mouse should bring the cursor back - QTest::mouseMove(w, QPoint( 100, 100 )); + QTest::mouseMove(w, QPoint( 150, 150 )); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::ArrowCursor ) ); } } // namespace Okular int main(int argc, char *argv[]) { // Force consistent locale QLocale locale(QStringLiteral("en_US.UTF-8")); if (locale == QLocale::c()) { // This is the way to check if the above worked locale = QLocale(QLocale::English, QLocale::UnitedStates); } QLocale::setDefault(locale); qputenv("LC_ALL", "en_US.UTF-8"); // For UNIX, third-party libraries // Ensure consistent configs/caches QTemporaryDir homeDir; // QTemporaryDir automatically cleans up when it goes out of scope Q_ASSERT(homeDir.isValid()); QByteArray homePath = QFile::encodeName(homeDir.path()); qDebug() << homePath; qputenv("USERPROFILE", homePath); qputenv("HOME", homePath); qputenv("XDG_DATA_HOME", homePath + "/.local"); qputenv("XDG_CONFIG_HOME", homePath + "/.kde-unit-test/xdg/config"); // Disable fancy debug output qunsetenv("QT_MESSAGE_PATTERN"); QApplication app( argc, argv ); app.setApplicationName(QStringLiteral("okularparttest")); app.setOrganizationDomain(QStringLiteral("kde.org")); app.setQuitOnLastWindowClosed(false); qRegisterMetaType(); /*as done by kapplication*/ qRegisterMetaType>(); Okular::PartTest test; return QTest::qExec( &test, argc, argv ); } #include "parttest.moc" diff --git a/ui/presentationwidget.cpp b/ui/presentationwidget.cpp index d0391d0cb..6188214b0 100644 --- a/ui/presentationwidget.cpp +++ b/ui/presentationwidget.cpp @@ -1,2521 +1,2522 @@ /*************************************************************************** * Copyright (C) 2004 by Enrico Ros * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "presentationwidget.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_LINUX #include #include // For ::close() for sleep inhibition #endif // system includes #include #include // local includes #include "annotationtools.h" #include "debug_ui.h" #include "drawingtoolactions.h" #include "guiutils.h" #include "pagepainter.h" #include "presentationsearchbar.h" #include "priorities.h" #include "videowidget.h" #include "core/action.h" #include "core/annotations.h" #include "core/audioplayer.h" #include "core/document.h" #include "core/generator.h" #include "core/movie.h" #include "core/page.h" #include "settings.h" #include "settings_core.h" // comment this to disable the top-right progress indicator #define ENABLE_PROGRESS_OVERLAY // a frame contains a pointer to the page object, its geometry and the // transition effect to the next frame struct PresentationFrame { PresentationFrame() = default; ~PresentationFrame() { qDeleteAll( videoWidgets ); } PresentationFrame(const PresentationFrame &) = delete; PresentationFrame &operator=(const PresentationFrame &) = delete; void recalcGeometry( int width, int height, float screenRatio ) { // calculate frame geometry keeping constant aspect ratio float pageRatio = page->ratio(); int pageWidth = width, pageHeight = height; if ( pageRatio > screenRatio ) pageWidth = (int)( (float)pageHeight / pageRatio ); else pageHeight = (int)( (float)pageWidth * pageRatio ); geometry.setRect( ( width - pageWidth ) / 2, ( height - pageHeight ) / 2, pageWidth, pageHeight ); Q_FOREACH ( VideoWidget *vw, videoWidgets ) { const Okular::NormalizedRect r = vw->normGeometry(); QRect vwgeom = r.geometry( geometry.width(), geometry.height() ); vw->resize( vwgeom.size() ); vw->move( geometry.topLeft() + vwgeom.topLeft() ); } } const Okular::Page * page; QRect geometry; QHash< Okular::Movie *, VideoWidget * > videoWidgets; QLinkedList< SmoothPath > drawings; }; // a custom QToolBar that basically does not propagate the event if the widget // background is not automatically filled class PresentationToolBar : public QToolBar { Q_OBJECT public: PresentationToolBar( QWidget * parent = Q_NULLPTR ) : QToolBar( parent ) {} protected: void mousePressEvent( QMouseEvent * e ) override { QToolBar::mousePressEvent( e ); e->accept(); } void mouseReleaseEvent( QMouseEvent * e ) override { QToolBar::mouseReleaseEvent( e ); e->accept(); } }; PresentationWidget::PresentationWidget( QWidget * parent, Okular::Document * doc, DrawingToolActions * drawingToolActions, KActionCollection * collection ) : QWidget( nullptr /* must be null, to have an independent widget */, Qt::FramelessWindowHint ), m_pressedLink( nullptr ), m_handCursor( false ), m_drawingEngine( nullptr ), m_screenInhibitCookie(0), m_sleepInhibitFd(-1), m_parentWidget( parent ), m_document( doc ), m_frameIndex( -1 ), m_topBar( nullptr ), m_pagesEdit( nullptr ), m_searchBar( nullptr ), m_ac( collection ), m_screenSelect( nullptr ), m_isSetup( false ), m_blockNotifications( false ), m_inBlackScreenMode( false ), m_showSummaryView( Okular::Settings::slidesShowSummary() ), m_advanceSlides( Okular::SettingsCore::slidesAdvance() ), m_goToPreviousPageOnRelease( false ), m_goToNextPageOnRelease( false ) { Q_UNUSED( parent ) setAttribute( Qt::WA_DeleteOnClose ); setAttribute( Qt::WA_OpaquePaintEvent ); setObjectName( QStringLiteral( "presentationWidget" ) ); QString caption = doc->metaData( QStringLiteral("DocumentTitle") ).toString(); if ( caption.trimmed().isEmpty() ) caption = doc->currentDocument().fileName(); caption = i18nc( "[document title/filename] – Presentation", "%1 – Presentation", caption ); setWindowTitle( caption ); m_width = -1; m_screen = -2; // create top toolbar m_topBar = new PresentationToolBar( this ); m_topBar->setObjectName( QStringLiteral( "presentationBar" ) ); m_topBar->setMovable( false ); m_topBar->layout()->setContentsMargins(0, 0, 0, 0); m_topBar->addAction( QIcon::fromTheme( layoutDirection() == Qt::RightToLeft ? QStringLiteral("go-next") : QStringLiteral("go-previous") ), i18n( "Previous Page" ), this, SLOT(slotPrevPage()) ); m_pagesEdit = new KLineEdit( m_topBar ); QSizePolicy sp = m_pagesEdit->sizePolicy(); sp.setHorizontalPolicy( QSizePolicy::Minimum ); m_pagesEdit->setSizePolicy( sp ); QFontMetrics fm( m_pagesEdit->font() ); QStyleOptionFrame option; option.initFrom( m_pagesEdit ); m_pagesEdit->setMaximumWidth( fm.width( QString::number( m_document->pages() ) ) + 2 * style()->pixelMetric( QStyle::PM_DefaultFrameWidth, &option, m_pagesEdit ) + 4 ); // the 4 comes from 2*horizontalMargin, horizontalMargin being a define in qlineedit.cpp QIntValidator *validator = new QIntValidator( 1, m_document->pages(), m_pagesEdit ); m_pagesEdit->setValidator( validator ); m_topBar->addWidget( m_pagesEdit ); QLabel *pagesLabel = new QLabel( m_topBar ); pagesLabel->setText( QLatin1String( " / " ) + QString::number( m_document->pages() ) + QLatin1String( " " ) ); m_topBar->addWidget( pagesLabel ); connect(m_pagesEdit, &QLineEdit::returnPressed, this, &PresentationWidget::slotPageChanged); m_topBar->addAction( QIcon::fromTheme( layoutDirection() == Qt::RightToLeft ? QStringLiteral("go-previous") : QStringLiteral("go-next") ), i18n( "Next Page" ), this, SLOT(slotNextPage()) ); m_topBar->addSeparator(); QAction *playPauseAct = collection->action( QStringLiteral("presentation_play_pause") ); playPauseAct->setEnabled( true ); connect(playPauseAct, &QAction::triggered, this, &PresentationWidget::slotTogglePlayPause); m_topBar->addAction( playPauseAct ); setPlayPauseIcon(); addAction( playPauseAct ); m_topBar->addSeparator(); foreach(QAction *action, drawingToolActions->actions()) { action->setEnabled( true ); m_topBar->addAction( action ); addAction( action ); } connect( drawingToolActions, &DrawingToolActions::changeEngine, this, &PresentationWidget::slotChangeDrawingToolEngine ); connect( drawingToolActions, &DrawingToolActions::actionsRecreated, this, &PresentationWidget::slotAddDrawingToolActions ); QAction *eraseDrawingAct = collection->action( QStringLiteral("presentation_erase_drawings") ); eraseDrawingAct->setEnabled( true ); connect(eraseDrawingAct, &QAction::triggered, this, &PresentationWidget::clearDrawings); m_topBar->addAction( eraseDrawingAct ); addAction( eraseDrawingAct ); QDesktopWidget *desktop = QApplication::desktop(); if ( desktop->numScreens() > 1 ) { m_topBar->addSeparator(); m_screenSelect = new KSelectAction( QIcon::fromTheme( QStringLiteral("video-display") ), i18n( "Switch Screen" ), m_topBar ); m_screenSelect->setToolBarMode( KSelectAction::MenuMode ); m_screenSelect->setToolButtonPopupMode( QToolButton::InstantPopup ); m_topBar->addAction( m_screenSelect ); const int screenCount = desktop->numScreens(); for ( int i = 0; i < screenCount; ++i ) { QAction *act = m_screenSelect->addAction( i18nc( "%1 is the screen number (0, 1, ...)", "Screen %1", i ) ); act->setData( qVariantFromValue( i ) ); } } QWidget *spacer = new QWidget( m_topBar ); spacer->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::MinimumExpanding ); m_topBar->addWidget( spacer ); m_topBar->addAction( QIcon::fromTheme( QStringLiteral("application-exit") ), i18n( "Exit Presentation Mode" ), this, SLOT(close()) ); m_topBar->setAutoFillBackground( true ); showTopBar( false ); // change topbar background color QPalette p = m_topBar->palette(); p.setColor( QPalette::Active, QPalette::Button, Qt::gray ); p.setColor( QPalette::Active, QPalette::Window, Qt::darkGray ); m_topBar->setPalette( p ); // Grab swipe gestures to change pages grabGesture(Qt::SwipeGesture); // misc stuff setMouseTracking( true ); setContextMenuPolicy( Qt::PreventContextMenu ); m_transitionTimer = new QTimer( this ); m_transitionTimer->setSingleShot( true ); connect(m_transitionTimer, &QTimer::timeout, this, &PresentationWidget::slotTransitionStep); m_overlayHideTimer = new QTimer( this ); m_overlayHideTimer->setSingleShot( true ); connect(m_overlayHideTimer, &QTimer::timeout, this, &PresentationWidget::slotHideOverlay); m_nextPageTimer = new QTimer( this ); m_nextPageTimer->setSingleShot( true ); connect(m_nextPageTimer, &QTimer::timeout, this, &PresentationWidget::slotNextPage); connect(m_document, &Okular::Document::processMovieAction, this, &PresentationWidget::slotProcessMovieAction); connect(m_document, &Okular::Document::processRenditionAction, this, &PresentationWidget::slotProcessRenditionAction); // handle cursor appearance as specified in configuration if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay ) { KCursor::setAutoHideCursor( this, true ); KCursor::setHideCursorDelay( 3000 ); } else if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden ) { setCursor( QCursor( Qt::BlankCursor ) ); } setupActions(); // inhibit power management inhibitPowerManagement(); show(); QTimer::singleShot( 0, this, &PresentationWidget::slotDelayedEvents ); // setFocus() so KCursor::setAutoHideCursor() goes into effect if it's enabled setFocus( Qt::OtherFocusReason ); // Catch TabletEnterProximity and TabletLeaveProximity events from the QApplication qApp->installEventFilter( this ); } PresentationWidget::~PresentationWidget() { // allow power management saver again allowPowerManagement(); // stop the audio playbacks Okular::AudioPlayer::instance()->stopPlaybacks(); // remove our highlights if ( m_searchBar ) { m_document->resetSearch( PRESENTATION_SEARCH_ID ); } // remove this widget from document observer m_document->removeObserver( this ); foreach( QAction *action, m_topBar->actions() ) { action->setChecked( false ); action->setEnabled( false ); } delete m_drawingEngine; // delete frames qDeleteAll( m_frames ); qApp->removeEventFilter( this ); } void PresentationWidget::notifySetup( const QVector< Okular::Page * > & pageSet, int setupFlags ) { // same document, nothing to change - here we assume the document sets up // us with the whole document set as first notifySetup() if ( !( setupFlags & Okular::DocumentObserver::DocumentChanged ) ) return; // delete previous frames (if any (shouldn't be)) qDeleteAll( m_frames ); if ( !m_frames.isEmpty() ) qCWarning(OkularUiDebug) << "Frames setup changed while a Presentation is in progress."; m_frames.clear(); // create the new frames float screenRatio = (float)m_height / (float)m_width; for ( const Okular::Page * page : pageSet ) { PresentationFrame * frame = new PresentationFrame(); frame->page = page; const QLinkedList< Okular::Annotation * > annotations = page->annotations(); for ( Okular::Annotation * a : annotations ) { if ( a->subType() == Okular::Annotation::AMovie ) { Okular::MovieAnnotation * movieAnn = static_cast< Okular::MovieAnnotation * >( a ); VideoWidget * vw = new VideoWidget( movieAnn, movieAnn->movie(), m_document, this ); frame->videoWidgets.insert( movieAnn->movie(), vw ); vw->pageInitialized(); } else if ( a->subType() == Okular::Annotation::ARichMedia ) { Okular::RichMediaAnnotation * richMediaAnn = static_cast< Okular::RichMediaAnnotation * >( a ); if ( richMediaAnn->movie() ) { VideoWidget * vw = new VideoWidget( richMediaAnn, richMediaAnn->movie(), m_document, this ); frame->videoWidgets.insert( richMediaAnn->movie(), vw ); vw->pageInitialized(); } } else if ( a->subType() == Okular::Annotation::AScreen ) { const Okular::ScreenAnnotation * screenAnn = static_cast< Okular::ScreenAnnotation * >( a ); Okular::Movie *movie = GuiUtils::renditionMovieFromScreenAnnotation( screenAnn ); if ( movie ) { VideoWidget * vw = new VideoWidget( screenAnn, movie, m_document, this ); frame->videoWidgets.insert( movie, vw ); vw->pageInitialized(); } } } frame->recalcGeometry( m_width, m_height, screenRatio ); // add the frame to the vector m_frames.push_back( frame ); } // get metadata from the document m_metaStrings.clear(); const Okular::DocumentInfo info = m_document->documentInfo( QSet() << Okular::DocumentInfo::Title << Okular::DocumentInfo::Author ); if ( !info.get( Okular::DocumentInfo::Title ).isNull() ) m_metaStrings += i18n( "Title: %1", info.get( Okular::DocumentInfo::Title ) ); if ( !info.get( Okular::DocumentInfo::Author ).isNull() ) m_metaStrings += i18n( "Author: %1", info.get( Okular::DocumentInfo::Author ) ); m_metaStrings += i18n( "Pages: %1", m_document->pages() ); m_metaStrings += i18n( "Click to begin" ); m_isSetup = true; } void PresentationWidget::notifyViewportChanged( bool /*smoothMove*/ ) { // display the current page changePage( m_document->viewport().pageNumber ); // auto advance to the next page if set startAutoChangeTimer(); } void PresentationWidget::notifyPageChanged( int pageNumber, int changedFlags ) { // if we are blocking the notifications, do nothing if ( m_blockNotifications ) return; // check if it's the last requested pixmap. if so update the widget. if ( (changedFlags & ( DocumentObserver::Pixmap | DocumentObserver::Annotations | DocumentObserver::Highlights ) ) && pageNumber == m_frameIndex ) generatePage( changedFlags & ( DocumentObserver::Annotations | DocumentObserver::Highlights ) ); } void PresentationWidget::notifyCurrentPageChanged( int previousPage, int currentPage ) { if ( previousPage != -1 ) { // stop video playback Q_FOREACH ( VideoWidget *vw, m_frames[ previousPage ]->videoWidgets ) { vw->stop(); vw->pageLeft(); } // stop audio playback, if any Okular::AudioPlayer::instance()->stopPlaybacks(); // perform the page closing action, if any if ( m_document->page( previousPage )->pageAction( Okular::Page::Closing ) ) m_document->processAction( m_document->page( previousPage )->pageAction( Okular::Page::Closing ) ); // perform the additional actions of the page's annotations, if any Q_FOREACH ( const Okular::Annotation *annotation, m_document->page( previousPage )->annotations() ) { Okular::Action *action = nullptr; if ( annotation->subType() == Okular::Annotation::AScreen ) action = static_cast( annotation )->additionalAction( Okular::Annotation::PageClosing ); else if ( annotation->subType() == Okular::Annotation::AWidget ) action = static_cast( annotation )->additionalAction( Okular::Annotation::PageClosing ); if ( action ) m_document->processAction( action ); } } if ( currentPage != -1 ) { m_frameIndex = currentPage; // check if pixmap exists or else request it PresentationFrame * frame = m_frames[ m_frameIndex ]; int pixW = frame->geometry.width(); int pixH = frame->geometry.height(); bool signalsBlocked = m_pagesEdit->signalsBlocked(); m_pagesEdit->blockSignals( true ); m_pagesEdit->setText( QString::number( m_frameIndex + 1 ) ); m_pagesEdit->blockSignals( signalsBlocked ); // if pixmap not inside the Okular::Page we request it and wait for // notifyPixmapChanged call or else we can proceed to pixmap generation if ( !frame->page->hasPixmap( this, ceil(pixW * qApp->devicePixelRatio()), ceil(pixH * qApp->devicePixelRatio()) ) ) { requestPixmaps(); } else { // make the background pixmap generatePage(); } // perform the page opening action, if any if ( m_document->page( m_frameIndex )->pageAction( Okular::Page::Opening ) ) m_document->processAction( m_document->page( m_frameIndex )->pageAction( Okular::Page::Opening ) ); // perform the additional actions of the page's annotations, if any Q_FOREACH ( const Okular::Annotation *annotation, m_document->page( m_frameIndex )->annotations() ) { Okular::Action *action = nullptr; if ( annotation->subType() == Okular::Annotation::AScreen ) action = static_cast( annotation )->additionalAction( Okular::Annotation::PageOpening ); else if ( annotation->subType() == Okular::Annotation::AWidget ) action = static_cast( annotation )->additionalAction( Okular::Annotation::PageOpening ); if ( action ) m_document->processAction( action ); } // start autoplay video playback Q_FOREACH ( VideoWidget *vw, m_frames[ m_frameIndex ]->videoWidgets ) vw->pageEntered(); } } bool PresentationWidget::canUnloadPixmap( int pageNumber ) const { if ( Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Low || Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Normal ) { // can unload all pixmaps except for the currently visible one return pageNumber != m_frameIndex; } else { // can unload all pixmaps except for the currently visible one, previous and next return qAbs(pageNumber - m_frameIndex) <= 1; } } void PresentationWidget::setupActions() { addAction( m_ac->action( QStringLiteral("first_page") ) ); addAction( m_ac->action( QStringLiteral("last_page") ) ); addAction( m_ac->action( QString::fromLocal8Bit(KStandardAction::name( KStandardAction::Prior ) ) ) ); addAction( m_ac->action( QString::fromLocal8Bit(KStandardAction::name( KStandardAction::Next ) ) ) ); addAction( m_ac->action( QString::fromLocal8Bit(KStandardAction::name( KStandardAction::DocumentBack ) ) ) ); addAction( m_ac->action( QString::fromLocal8Bit(KStandardAction::name( KStandardAction::DocumentForward ) ) ) ); QAction *action = m_ac->action( QStringLiteral("switch_blackscreen_mode") ); connect(action, &QAction::toggled, this, &PresentationWidget::toggleBlackScreenMode); action->setEnabled( true ); addAction( action ); } void PresentationWidget::setPlayPauseIcon() { QAction *playPauseAction = m_ac->action( QStringLiteral("presentation_play_pause") ); if ( m_advanceSlides ) { playPauseAction->setIcon( QIcon::fromTheme( QStringLiteral("media-playback-pause") ) ); playPauseAction->setToolTip( i18nc( "For Presentation", "Pause" ) ); } else { playPauseAction->setIcon( QIcon::fromTheme( QStringLiteral("media-playback-start") ) ); playPauseAction->setToolTip( i18nc( "For Presentation", "Play" ) ); } } bool PresentationWidget::eventFilter (QObject *o, QEvent *e ) { if ( o == qApp ) { if ( e->type() == QTabletEvent::TabletEnterProximity ) { setCursor( QCursor( Qt::CrossCursor ) ); } - if ( e->type() == QTabletEvent::TabletLeaveProximity ) + else if ( e->type() == QTabletEvent::TabletLeaveProximity ) { - if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Visible ) - { - setCursor( QCursor( Qt::ArrowCursor ) ); - } - else - { - setCursor( QCursor( Qt::BlankCursor ) ); + setCursor( QCursor( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden ? Qt::BlankCursor : Qt::ArrowCursor ) ); + if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay) { + // Trick KCursor to hide the cursor if needed by sending an "unknown" key press event + // Send also the key release to make the world happy even it's probably not needed + QKeyEvent kp( QEvent::KeyPress, 0, Qt::NoModifier ); + qApp->sendEvent( this, &kp ); + QKeyEvent kr( QEvent::KeyRelease, 0, Qt::NoModifier ); + qApp->sendEvent( this, &kr ); } } } return false; } // bool PresentationWidget::event( QEvent * e ) { if ( e->type() == QEvent::Gesture ) return gestureEvent(static_cast(e)); if ( e->type() == QEvent::ToolTip ) { QHelpEvent * he = (QHelpEvent*)e; QRect r; const Okular::Action * link = getLink( he->x(), he->y(), &r ); if ( link ) { QString tip = link->actionTip(); if ( !tip.isEmpty() ) QToolTip::showText( he->globalPos(), tip, this, r ); } e->accept(); return true; } else // do not stop the event return QWidget::event( e ); } bool PresentationWidget::gestureEvent( QGestureEvent * event ) { // Swiping left or right on a touch screen will go to the previous or next slide, respectively. // The precise gesture is the standard Qt swipe: with three(!) fingers. if (QGesture *swipe = event->gesture(Qt::SwipeGesture)) { QSwipeGesture * swipeEvent = static_cast(swipe); if (swipeEvent->state() == Qt::GestureFinished) { if (swipeEvent->horizontalDirection() == QSwipeGesture::Left) { slotPrevPage(); event->accept(); return true; } if (swipeEvent->horizontalDirection() == QSwipeGesture::Right) { slotNextPage(); event->accept(); return true; } } } return false; } void PresentationWidget::keyPressEvent( QKeyEvent * e ) { if ( !m_isSetup ) return; switch ( e->key() ) { case Qt::Key_Left: case Qt::Key_Backspace: case Qt::Key_PageUp: case Qt::Key_Up: slotPrevPage(); break; case Qt::Key_Right: case Qt::Key_Space: case Qt::Key_PageDown: case Qt::Key_Down: slotNextPage(); break; case Qt::Key_Home: slotFirstPage(); break; case Qt::Key_End: slotLastPage(); break; case Qt::Key_Escape: if ( !m_topBar->isHidden() ) showTopBar( false ); else close(); break; } } void PresentationWidget::wheelEvent( QWheelEvent * e ) { if ( !m_isSetup ) return; // performance note: don't remove the clipping int div = e->delta() / 120; if ( div > 0 ) { if ( div > 3 ) div = 3; while ( div-- ) slotPrevPage(); } else if ( div < 0 ) { if ( div < -3 ) div = -3; while ( div++ ) slotNextPage(); } } void PresentationWidget::mousePressEvent( QMouseEvent * e ) { if ( !m_isSetup ) return; if ( m_drawingEngine ) { QRect r = routeMouseDrawingEvent( e ); if ( r.isValid() ) { m_drawingRect |= r.translated( m_frames[ m_frameIndex ]->geometry.topLeft() ); update( m_drawingRect ); } return; } // pressing left button if ( e->button() == Qt::LeftButton ) { // if pressing on a link, skip other checks if ( ( m_pressedLink = getLink( e->x(), e->y() ) ) ) return; const Okular::Annotation *annotation = getAnnotation( e->x(), e->y() ); if ( annotation ) { if ( annotation->subType() == Okular::Annotation::AMovie ) { const Okular::MovieAnnotation *movieAnnotation = static_cast( annotation ); VideoWidget *vw = m_frames[ m_frameIndex ]->videoWidgets.value( movieAnnotation->movie() ); vw->show(); vw->play(); return; } else if ( annotation->subType() == Okular::Annotation::ARichMedia ) { const Okular::RichMediaAnnotation *richMediaAnnotation = static_cast( annotation ); VideoWidget *vw = m_frames[ m_frameIndex ]->videoWidgets.value( richMediaAnnotation->movie() ); vw->show(); vw->play(); return; } else if ( annotation->subType() == Okular::Annotation::AScreen ) { m_document->processAction( static_cast( annotation )->action() ); return; } } // handle clicking on top-right overlay if ( !( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden ) && m_overlayGeometry.contains( e->pos() ) ) { overlayClick( e->pos() ); return; } // Actual mouse press events always lead to the next page page if ( e->source() == Qt::MouseEventNotSynthesized ) { m_goToNextPageOnRelease = true; } // Touch events may lead to the previous or next page else if ( Okular::Settings::slidesTapNavigation() != Okular::Settings::EnumSlidesTapNavigation::Disabled ) { switch ( Okular::Settings::slidesTapNavigation() ) { case Okular::Settings::EnumSlidesTapNavigation::ForwardBackward: { if ( e->x() < ( geometry().width()/2 ) ) { m_goToPreviousPageOnRelease = true; } else { m_goToNextPageOnRelease = true; } break; } case Okular::Settings::EnumSlidesTapNavigation::Forward: { m_goToNextPageOnRelease = true; break; } case Okular::Settings::EnumSlidesTapNavigation::Disabled: { // Do Nothing } } } } // pressing forward button else if ( e->button() == Qt::ForwardButton ) { m_goToNextPageOnRelease = true; } // pressing right or backward button else if ( e->button() == Qt::RightButton || e->button() == Qt::BackButton ) m_goToPreviousPageOnRelease = true; } void PresentationWidget::mouseReleaseEvent( QMouseEvent * e ) { if ( m_drawingEngine ) { routeMouseDrawingEvent( e ); return; } // if releasing on the same link we pressed over, execute it if ( m_pressedLink && e->button() == Qt::LeftButton ) { const Okular::Action * link = getLink( e->x(), e->y() ); if ( link == m_pressedLink ) m_document->processAction( link ); m_pressedLink = nullptr; } if ( m_goToPreviousPageOnRelease ) { slotPrevPage(); m_goToPreviousPageOnRelease = false; } if ( m_goToNextPageOnRelease ) { slotNextPage(); m_goToNextPageOnRelease = false; } } void PresentationWidget::mouseMoveEvent( QMouseEvent * e ) { // safety check if ( !m_isSetup ) return; // update cursor and tooltip if hovering a link if ( !m_drawingEngine && Okular::Settings::slidesCursor() != Okular::Settings::EnumSlidesCursor::Hidden ) testCursorOnLink( e->x(), e->y() ); if ( !m_topBar->isHidden() ) { // hide a shown bar when exiting the area if ( e->y() > ( m_topBar->height() + 1 ) ) { showTopBar( false ); setFocus( Qt::OtherFocusReason ); } } else { if ( m_drawingEngine && e->buttons() != Qt::NoButton ) { QRect r = routeMouseDrawingEvent( e ); if ( r.isValid() ) { m_drawingRect |= r.translated( m_frames[ m_frameIndex ]->geometry.topLeft() ); update( m_drawingRect ); } } else { // show the bar if reaching top 2 pixels if ( e->y() <= 1 ) showTopBar( true ); // handle "dragging the wheel" if clicking on its geometry else if ( ( QApplication::mouseButtons() & Qt::LeftButton ) && m_overlayGeometry.contains( e->pos() ) ) overlayClick( e->pos() ); } } } void PresentationWidget::paintEvent( QPaintEvent * pe ) { qreal dpr = devicePixelRatioF(); if ( m_inBlackScreenMode ) { QPainter painter( this ); painter.fillRect( pe->rect(), Qt::black ); return; } if ( !m_isSetup ) { m_width = width(); m_height = height(); connect(m_document, &Okular::Document::linkFind, this, &PresentationWidget::slotFind); // register this observer in document. events will come immediately m_document->addObserver( this ); // show summary if requested if ( Okular::Settings::slidesShowSummary() ) generatePage(); } // check painting rect consistency QRect r = pe->rect().intersected( QRect( QPoint( 0, 0 ), geometry().size() ) ); if ( r.isNull() ) return; if ( m_lastRenderedPixmap.isNull() ) { QPainter painter( this ); painter.fillRect( pe->rect(), Okular::Settings::slidesBackgroundColor() ); return; } // blit the pixmap to the screen QPainter painter( this ); for ( const QRect &r : pe->region() ) { if ( !r.isValid() ) continue; #ifdef ENABLE_PROGRESS_OVERLAY const QRect dR(QRectF(r.x() * dpr, r.y() * dpr, r.width() * dpr, r.height() * dpr).toAlignedRect()); if ( Okular::Settings::slidesShowProgress() && r.intersects( m_overlayGeometry ) ) { // backbuffer the overlay operation QPixmap backPixmap( dR.size() ); backPixmap.setDevicePixelRatio( dpr ); QPainter pixPainter( &backPixmap ); // first draw the background on the backbuffer pixPainter.drawPixmap( QPoint(0,0), m_lastRenderedPixmap, dR ); // then blend the overlay (a piece of) over the background QRect ovr = m_overlayGeometry.intersected( r ); pixPainter.drawPixmap( (ovr.left() - r.left()), (ovr.top() - r.top()), m_lastRenderedOverlay, (ovr.left() - m_overlayGeometry.left()) * dpr, (ovr.top() - m_overlayGeometry.top()) * dpr, ovr.width() * dpr, ovr.height() * dpr ); // finally blit the pixmap to the screen pixPainter.end(); const QRect backPixmapRect = backPixmap.rect(); const QRect dBackPixmapRect(QRectF(backPixmapRect.x() * dpr, backPixmapRect.y() * dpr, backPixmapRect.width() * dpr, backPixmapRect.height() * dpr).toAlignedRect()); painter.drawPixmap( r.topLeft(), backPixmap, dBackPixmapRect ); } else #endif // copy the rendered pixmap to the screen painter.drawPixmap( r.topLeft(), m_lastRenderedPixmap, dR ); } // paint drawings if ( m_frameIndex != -1 ) { painter.save(); const QRect & geom = m_frames[ m_frameIndex ]->geometry; const QSize pmSize( geom.width() * dpr, geom.height() * dpr ); QPixmap pm( pmSize ); pm.fill( Qt::transparent ); QPainter pmPainter( &pm ); pm.setDevicePixelRatio( dpr ); pmPainter.setRenderHints( QPainter::Antialiasing ); // Paint old paths foreach ( const SmoothPath &drawing, m_frames[ m_frameIndex ]->drawings ) drawing.paint( &pmPainter, pmSize.width(), pmSize.height() ); // Paint the path that is currently being drawn by the user if ( m_drawingEngine && m_drawingRect.intersects( pe->rect() ) ) m_drawingEngine->paint( &pmPainter, pmSize.width(), pmSize.height(), m_drawingRect.intersected( pe->rect() ) ); painter.setRenderHints( QPainter::Antialiasing ); painter.drawPixmap( geom.topLeft() , pm ); painter.restore(); } painter.end(); } void PresentationWidget::resizeEvent( QResizeEvent *re ) { // qCDebug(OkularUiDebug) << re->oldSize() << "=>" << re->size(); if ( re->oldSize() == QSize( -1, -1 ) ) return; m_screen = QApplication::desktop()->screenNumber( this ); applyNewScreenSize( re->oldSize() ); } void PresentationWidget::leaveEvent( QEvent * e ) { Q_UNUSED( e ) if ( !m_topBar->isHidden() ) { showTopBar( false ); } } // const void * PresentationWidget::getObjectRect( Okular::ObjectRect::ObjectType type, int x, int y, QRect * geometry ) const { // no links on invalid pages if ( geometry && !geometry->isNull() ) geometry->setRect( 0, 0, 0, 0 ); if ( m_frameIndex < 0 || m_frameIndex >= (int)m_frames.size() ) return nullptr; // get frame, page and geometry const PresentationFrame * frame = m_frames[ m_frameIndex ]; const Okular::Page * page = frame->page; const QRect & frameGeometry = frame->geometry; // compute normalized x and y double nx = (double)(x - frameGeometry.left()) / (double)frameGeometry.width(); double ny = (double)(y - frameGeometry.top()) / (double)frameGeometry.height(); // no links outside the pages if ( nx < 0 || nx > 1 || ny < 0 || ny > 1 ) return nullptr; // check if 1) there is an object and 2) it's a link const QRect d = QApplication::desktop()->screenGeometry( m_screen ); const Okular::ObjectRect * object = page->objectRect( type, nx, ny, d.width(), d.height() ); if ( !object ) return nullptr; // compute link geometry if destination rect present if ( geometry ) { *geometry = object->boundingRect( frameGeometry.width(), frameGeometry.height() ); geometry->translate( frameGeometry.left(), frameGeometry.top() ); } // return the link pointer return object->object(); } const Okular::Action * PresentationWidget::getLink( int x, int y, QRect * geometry ) const { return reinterpret_cast( getObjectRect( Okular::ObjectRect::Action, x, y, geometry ) ); } const Okular::Annotation * PresentationWidget::getAnnotation( int x, int y, QRect * geometry ) const { return reinterpret_cast( getObjectRect( Okular::ObjectRect::OAnnotation, x, y, geometry ) ); } void PresentationWidget::testCursorOnLink( int x, int y ) { const Okular::Action * link = getLink( x, y, nullptr ); const Okular::Annotation *annotation = getAnnotation( x, y, nullptr ); const bool needsHandCursor = ( ( link != nullptr ) || ( ( annotation != nullptr ) && ( annotation->subType() == Okular::Annotation::AMovie ) ) || ( ( annotation != nullptr ) && ( annotation->subType() == Okular::Annotation::ARichMedia ) ) || ( ( annotation != nullptr ) && ( annotation->subType() == Okular::Annotation::AScreen ) && ( GuiUtils::renditionMovieFromScreenAnnotation( static_cast< const Okular::ScreenAnnotation * >( annotation ) ) != nullptr ) ) ); // only react on changes (in/out from a link) if ( ( needsHandCursor && !m_handCursor ) || ( !needsHandCursor && m_handCursor ) ) { // change cursor shape m_handCursor = needsHandCursor; setCursor( QCursor( m_handCursor ? Qt::PointingHandCursor : Qt::ArrowCursor ) ); } } void PresentationWidget::overlayClick( const QPoint & position ) { // clicking the progress indicator int xPos = position.x() - m_overlayGeometry.x() - m_overlayGeometry.width() / 2, yPos = m_overlayGeometry.height() / 2 - position.y(); if ( !xPos && !yPos ) return; // compute angle relative to indicator (note coord transformation) float angle = 0.5 + 0.5 * atan2( (double)-xPos, (double)-yPos ) / M_PI; int pageIndex = (int)( angle * ( m_frames.count() - 1 ) + 0.5 ); // go to selected page changePage( pageIndex ); } void PresentationWidget::changePage( int newPage ) { if ( m_showSummaryView ) { m_showSummaryView = false; m_frameIndex = -1; return; } if ( m_frameIndex == newPage ) return; // switch to newPage m_document->setViewportPage( newPage, this ); if ( (Okular::Settings::slidesShowSummary() && !m_showSummaryView) || m_frameIndex == -1 ) notifyCurrentPageChanged( -1, newPage ); } void PresentationWidget::generatePage( bool disableTransition ) { if ( m_lastRenderedPixmap.isNull() ) { qreal dpr = qApp->devicePixelRatio(); m_lastRenderedPixmap = QPixmap( m_width * dpr, m_height * dpr ); m_lastRenderedPixmap.setDevicePixelRatio(dpr); m_previousPagePixmap = QPixmap(); } else { m_previousPagePixmap = m_lastRenderedPixmap; } // opens the painter over the pixmap QPainter pixmapPainter; pixmapPainter.begin( &m_lastRenderedPixmap ); // generate welcome page if ( m_frameIndex == -1 ) generateIntroPage( pixmapPainter ); // generate a normal pixmap with extended margin filling if ( m_frameIndex >= 0 && m_frameIndex < (int)m_document->pages() ) generateContentsPage( m_frameIndex, pixmapPainter ); pixmapPainter.end(); // generate the top-right corner overlay #ifdef ENABLE_PROGRESS_OVERLAY if ( Okular::Settings::slidesShowProgress() && m_frameIndex != -1 ) generateOverlay(); #endif // start transition on pages that have one if ( !disableTransition && Okular::Settings::slidesTransitionsEnabled() ) { const Okular::PageTransition * transition = m_frameIndex != -1 ? m_frames[ m_frameIndex ]->page->transition() : nullptr; if ( transition ) initTransition( transition ); else { Okular::PageTransition trans = defaultTransition(); initTransition( &trans ); } } else { Okular::PageTransition trans = defaultTransition( Okular::Settings::EnumSlidesTransition::Replace ); initTransition( &trans ); } // update cursor + tooltip if ( !m_drawingEngine && Okular::Settings::slidesCursor() != Okular::Settings::EnumSlidesCursor::Hidden ) { QPoint p = mapFromGlobal( QCursor::pos() ); testCursorOnLink( p.x(), p.y() ); } } void PresentationWidget::generateIntroPage( QPainter & p ) { qreal dpr = qApp->devicePixelRatio(); // use a vertical gray gradient background int blend1 = m_height / 10, blend2 = 9 * m_height / 10; int baseTint = QColor(Qt::gray).red(); for ( int i = 0; i < m_height; i++ ) { int k = baseTint; if ( i < blend1 ) k -= (int)( baseTint * (i-blend1)*(i-blend1) / (float)(blend1 * blend1) ); if ( i > blend2 ) k += (int)( (255-baseTint) * (i-blend2)*(i-blend2) / (float)(blend1 * blend1) ); p.fillRect( 0, i, m_width, 1, QColor( k, k, k ) ); } // draw okular logo in the four corners QPixmap logo = DesktopIcon( QStringLiteral("okular"), 64 * dpr ); logo.setDevicePixelRatio( dpr ); if ( !logo.isNull() ) { p.drawPixmap( 5, 5, logo ); p.drawPixmap( m_width - 5 - logo.width(), 5, logo ); p.drawPixmap( m_width - 5 - logo.width(), m_height - 5 - logo.height(), logo ); p.drawPixmap( 5, m_height - 5 - logo.height(), logo ); } // draw metadata text (the last line is 'click to begin') int strNum = m_metaStrings.count(), strHeight = m_height / ( strNum + 4 ), fontHeight = 2 * strHeight / 3; QFont font( p.font() ); font.setPixelSize( fontHeight ); QFontMetrics metrics( font ); for ( int i = 0; i < strNum; i++ ) { // set a font to fit text width float wScale = (float)metrics.boundingRect( m_metaStrings[i] ).width() / (float)m_width; QFont f( font ); if ( wScale > 1.0 ) f.setPixelSize( (int)( (float)fontHeight / (float)wScale ) ); p.setFont( f ); // text shadow p.setPen( Qt::darkGray ); p.drawText( 2, m_height / 4 + strHeight * i + 2, m_width, strHeight, Qt::AlignHCenter | Qt::AlignVCenter, m_metaStrings[i] ); // text body p.setPen( 128 + (127 * i) / strNum ); p.drawText( 0, m_height / 4 + strHeight * i, m_width, strHeight, Qt::AlignHCenter | Qt::AlignVCenter, m_metaStrings[i] ); } } void PresentationWidget::generateContentsPage( int pageNum, QPainter & p ) { PresentationFrame * frame = m_frames[ pageNum ]; // translate painter and contents rect QRect geom( frame->geometry ); p.translate( geom.left(), geom.top() ); geom.translate( -geom.left(), -geom.top() ); // draw the page using the shared PagePainter class int flags = PagePainter::Accessibility | PagePainter::Highlights | PagePainter::Annotations; PagePainter::paintPageOnPainter( &p, frame->page, this, flags, geom.width(), geom.height(), geom ); // restore painter p.translate( -frame->geometry.left(), -frame->geometry.top() ); // fill unpainted areas with background color const QRegion unpainted( QRect( 0, 0, m_width, m_height ) ); const QRegion rgn = unpainted.subtracted( frame->geometry ); for ( const QRect & r : rgn ) { p.fillRect( r, Okular::Settings::slidesBackgroundColor() ); } } // from Arthur - Qt4 - (is defined elsewhere as 'qt_div_255' to not break final compilation) inline int qt_div255(int x) { return (x + (x>>8) + 0x80) >> 8; } void PresentationWidget::generateOverlay() { #ifdef ENABLE_PROGRESS_OVERLAY qreal dpr = qApp->devicePixelRatio(); // calculate overlay geometry and resize pixmap if needed int side = m_width / 16; m_overlayGeometry.setRect( m_width - side - 4, 4, side, side ); // note: to get a sort of antialiasing, we render the pixmap double sized // and the resulting image is smoothly scaled down. So here we open a // painter on the double sized pixmap. side *= 2; QPixmap doublePixmap( side * dpr, side * dpr ); doublePixmap.setDevicePixelRatio( dpr ); doublePixmap.fill( Qt::black ); QPainter pixmapPainter( &doublePixmap ); pixmapPainter.setRenderHints( QPainter::Antialiasing ); // draw PIE SLICES in blue levels (the levels will then be the alpha component) int pages = m_document->pages(); if ( pages > 28 ) { // draw continuous slices int degrees = (int)( 360 * (float)(m_frameIndex + 1) / (float)pages ); pixmapPainter.setPen( 0x05 ); pixmapPainter.setBrush( QColor( 0x40 ) ); pixmapPainter.drawPie( 2, 2, side - 4, side - 4, 90*16, (360-degrees)*16 ); pixmapPainter.setPen( 0x40 ); pixmapPainter.setBrush( QColor( 0xF0 ) ); pixmapPainter.drawPie( 2, 2, side - 4, side - 4, 90*16, -degrees*16 ); } else { // draw discrete slices float oldCoord = -90; for ( int i = 0; i < pages; i++ ) { float newCoord = -90 + 360 * (float)(i + 1) / (float)pages; pixmapPainter.setPen( i <= m_frameIndex ? 0x40 : 0x05 ); pixmapPainter.setBrush( QColor( i <= m_frameIndex ? 0xF0 : 0x40 ) ); pixmapPainter.drawPie( 2, 2, side - 4, side - 4, (int)( -16*(oldCoord + 1) ), (int)( -16*(newCoord - (oldCoord + 2)) ) ); oldCoord = newCoord; } } int circleOut = side / 4; pixmapPainter.setPen( Qt::black ); pixmapPainter.setBrush( Qt::black ); pixmapPainter.drawEllipse( circleOut, circleOut, side - 2*circleOut, side - 2*circleOut ); // draw TEXT using maximum opacity QFont f( pixmapPainter.font() ); f.setPixelSize( side / 4 ); pixmapPainter.setFont( f ); pixmapPainter.setPen( 0xFF ); // use a little offset to prettify output pixmapPainter.drawText( 2, 2, side, side, Qt::AlignCenter, QString::number( m_frameIndex + 1 ) ); // end drawing pixmap and halve image pixmapPainter.end(); QImage image( doublePixmap.toImage().scaled( (side / 2) * dpr, (side / 2) * dpr, Qt::IgnoreAspectRatio, Qt::SmoothTransformation ) ); image.setDevicePixelRatio( dpr ); image = image.convertToFormat( QImage::Format_ARGB32 ); image.setDevicePixelRatio( dpr ); // draw circular shadow using the same technique doublePixmap.fill( Qt::black ); pixmapPainter.begin( &doublePixmap ); pixmapPainter.setPen( 0x40 ); pixmapPainter.setBrush( QColor( 0x80 ) ); pixmapPainter.drawEllipse( 0, 0, side, side ); pixmapPainter.end(); QImage shadow( doublePixmap.toImage().scaled( (side / 2) * dpr, (side / 2) * dpr, Qt::IgnoreAspectRatio, Qt::SmoothTransformation ) ); shadow.setDevicePixelRatio( dpr ); // generate a 2 colors pixmap using mixing shadow (made with highlight color) // and image (made with highlightedText color) QPalette pal = palette(); QColor color = pal.color( QPalette::Active, QPalette::HighlightedText ); int red = color.red(), green = color.green(), blue = color.blue(); color = pal.color( QPalette::Active, QPalette::Highlight ); int sRed = color.red(), sGreen = color.green(), sBlue = color.blue(); // pointers unsigned int * data = (unsigned int *)image.bits(), * shadowData = (unsigned int *)shadow.bits(), pixels = image.width() * image.height(); // cache data (reduce computation time to 26%!) int c1 = -1, c2 = -1, cR = 0, cG = 0, cB = 0, cA = 0; // foreach pixel for( unsigned int i = 0; i < pixels; ++i ) { // alpha for shadow and image int shadowAlpha = shadowData[i] & 0xFF, srcAlpha = data[i] & 0xFF; // cache values if ( srcAlpha != c1 || shadowAlpha != c2 ) { c1 = srcAlpha; c2 = shadowAlpha; // fuse color components and alpha value of image over shadow data[i] = qRgba( cR = qt_div255( srcAlpha * red + (255 - srcAlpha) * sRed ), cG = qt_div255( srcAlpha * green + (255 - srcAlpha) * sGreen ), cB = qt_div255( srcAlpha * blue + (255 - srcAlpha) * sBlue ), cA = qt_div255( srcAlpha * srcAlpha + (255 - srcAlpha) * shadowAlpha ) ); } else data[i] = qRgba( cR, cG, cB, cA ); } m_lastRenderedOverlay = QPixmap::fromImage( image ); m_lastRenderedOverlay.setDevicePixelRatio( dpr ); // start the autohide timer //repaint( m_overlayGeometry ); // toggle with next line update( m_overlayGeometry ); m_overlayHideTimer->start( 2500 ); #endif } QRect PresentationWidget::routeMouseDrawingEvent( QMouseEvent * e ) { if ( m_frameIndex == -1 ) // Can't draw on the summary page return QRect(); const QRect & geom = m_frames[ m_frameIndex ]->geometry; const Okular::Page * page = m_frames[ m_frameIndex ]->page; AnnotatorEngine::EventType eventType; AnnotatorEngine::Button button; // figure out the event type and button AnnotatorEngine::decodeEvent( e, &eventType, &button ); static bool hasclicked = false; if ( eventType == AnnotatorEngine::Press ) hasclicked = true; QPointF mousePos = e->localPos(); double nX = ( mousePos.x() - (double)geom.left() ) / (double)geom.width(); double nY = ( mousePos.y() - (double)geom.top() ) / (double)geom.height(); QRect ret; bool isInside = nX >= 0 && nX < 1 && nY >= 0 && nY < 1; if ( hasclicked && !isInside ) { // Fake a move to the last border pos nX = qBound(0., nX, 1.); nY = qBound(0., nY, 1.); m_drawingEngine->event( AnnotatorEngine::Move, button, nX, nY, geom.width(), geom.height(), page ); // Fake a release in the following lines eventType = AnnotatorEngine::Release; isInside = true; } else if ( !hasclicked && isInside ) { // we're coming from the outside, pretend we started clicking at the closest border if ( nX < ( 1 - nX ) && nX < nY && nX < ( 1 - nY ) ) nX = 0; else if ( nY < ( 1 - nY ) && nY < nX && nY < ( 1 - nX ) ) nY = 0; else if ( ( 1 - nX ) < nX && ( 1 - nX ) < nY && ( 1 - nX ) < ( 1 - nY ) ) nX = 1; else nY = 1; hasclicked = true; eventType = AnnotatorEngine::Press; } if ( hasclicked && isInside ) { ret = m_drawingEngine->event( eventType, button, nX, nY, geom.width(), geom.height(), page ); } if ( eventType == AnnotatorEngine::Release ) { hasclicked = false; } if ( m_drawingEngine->creationCompleted() ) { // add drawing to current page m_frames[ m_frameIndex ]->drawings << m_drawingEngine->endSmoothPath(); // remove the actual drawer and create a new one just after // that - that gives continuous drawing delete m_drawingEngine; m_drawingRect = QRect(); m_drawingEngine = new SmoothPathEngine( m_currentDrawingToolElement ); // schedule repaint update(); } return ret; } void PresentationWidget::startAutoChangeTimer() { double pageDuration = m_frameIndex >= 0 && m_frameIndex < (int)m_frames.count() ? m_frames[ m_frameIndex ]->page->duration() : -1; if ( m_advanceSlides || pageDuration >= 0.0 ) { double secs; if ( pageDuration < 0.0 ) secs = Okular::SettingsCore::slidesAdvanceTime(); else if ( m_advanceSlides ) secs = qMin( pageDuration, Okular::SettingsCore::slidesAdvanceTime() ); else secs = pageDuration; m_nextPageTimer->start( (int)( secs * 1000 ) ); } } void PresentationWidget::recalcGeometry() { QDesktopWidget *desktop = QApplication::desktop(); const int preferenceScreen = Okular::Settings::slidesScreen(); int screen = 0; if ( preferenceScreen == -2 ) { screen = desktop->screenNumber( m_parentWidget ); } else if ( preferenceScreen == -1 ) { screen = desktop->primaryScreen(); } else if ( preferenceScreen >= 0 && preferenceScreen < desktop->numScreens() ) { screen = preferenceScreen; } else { screen = desktop->screenNumber( m_parentWidget ); Okular::Settings::setSlidesScreen( -2 ); } const QRect screenGeom = desktop->screenGeometry( screen ); // qCDebug(OkularUiDebug) << screen << "=>" << screenGeom; m_screen = screen; setGeometry( screenGeom ); } void PresentationWidget::repositionContent() { const QRect ourGeom = geometry(); // tool bar height in pixels, make it large enough to hold the text fields with the page numbers const int toolBarHeight = m_pagesEdit->height() * 1.5; m_topBar->setGeometry( 0, 0, ourGeom.width(), toolBarHeight ); m_topBar->setIconSize( QSize( toolBarHeight * 0.75, toolBarHeight * 0.75 ) ); } void PresentationWidget::requestPixmaps() { PresentationFrame * frame = m_frames[ m_frameIndex ]; int pixW = frame->geometry.width(); int pixH = frame->geometry.height(); // operation will take long: set busy cursor QApplication::setOverrideCursor( QCursor( Qt::BusyCursor ) ); // request the pixmap QLinkedList< Okular::PixmapRequest * > requests; requests.push_back( new Okular::PixmapRequest( this, m_frameIndex, pixW, pixH, PRESENTATION_PRIO, Okular::PixmapRequest::NoFeature ) ); // restore cursor QApplication::restoreOverrideCursor(); // ask for next and previous page if not in low memory usage setting if ( Okular::SettingsCore::memoryLevel() != Okular::SettingsCore::EnumMemoryLevel::Low ) { int pagesToPreload = 1; // If greedy, preload everything if (Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Greedy) pagesToPreload = (int)m_document->pages(); Okular::PixmapRequest::PixmapRequestFeatures requestFeatures = Okular::PixmapRequest::Preload; requestFeatures |= Okular::PixmapRequest::Asynchronous; for( int j = 1; j <= pagesToPreload; j++ ) { int tailRequest = m_frameIndex + j; if ( tailRequest < (int)m_document->pages() ) { PresentationFrame *nextFrame = m_frames[ tailRequest ]; pixW = nextFrame->geometry.width(); pixH = nextFrame->geometry.height(); if ( !nextFrame->page->hasPixmap( this, pixW, pixH ) ) requests.push_back( new Okular::PixmapRequest( this, tailRequest, pixW, pixH, PRESENTATION_PRELOAD_PRIO, requestFeatures ) ); } int headRequest = m_frameIndex - j; if ( headRequest >= 0 ) { PresentationFrame *prevFrame = m_frames[ headRequest ]; pixW = prevFrame->geometry.width(); pixH = prevFrame->geometry.height(); if ( !prevFrame->page->hasPixmap( this, pixW, pixH ) ) requests.push_back( new Okular::PixmapRequest( this, headRequest, pixW, pixH, PRESENTATION_PRELOAD_PRIO, requestFeatures ) ); } // stop if we've already reached both ends of the document if ( headRequest < 0 && tailRequest >= (int)m_document->pages() ) break; } } m_document->requestPixmaps( requests ); } void PresentationWidget::slotNextPage() { int nextIndex = m_frameIndex + 1; // loop when configured if ( nextIndex == m_frames.count() && Okular::Settings::slidesLoop() ) nextIndex = 0; if ( nextIndex < m_frames.count() ) { // go to next page changePage( nextIndex ); // auto advance to the next page if set startAutoChangeTimer(); } else { #ifdef ENABLE_PROGRESS_OVERLAY if ( Okular::Settings::slidesShowProgress() ) generateOverlay(); #endif if ( m_transitionTimer->isActive() ) { m_transitionTimer->stop(); m_lastRenderedPixmap = m_currentPagePixmap; update(); } } // we need the setFocus() call here to let KCursor::autoHide() work correctly setFocus(); } void PresentationWidget::slotPrevPage() { if ( m_frameIndex > 0 ) { // go to previous page changePage( m_frameIndex - 1 ); // auto advance to the next page if set startAutoChangeTimer(); } else { #ifdef ENABLE_PROGRESS_OVERLAY if ( Okular::Settings::slidesShowProgress() ) generateOverlay(); #endif if ( m_transitionTimer->isActive() ) { m_transitionTimer->stop(); m_lastRenderedPixmap = m_currentPagePixmap; update(); } } } void PresentationWidget::slotFirstPage() { changePage( 0 ); } void PresentationWidget::slotLastPage() { changePage( (int)m_frames.count() - 1 ); } void PresentationWidget::slotHideOverlay() { QRect geom( m_overlayGeometry ); m_overlayGeometry.setCoords( 0, 0, -1, -1 ); update( geom ); } void PresentationWidget::slotTransitionStep() { switch( m_currentTransition.type() ) { case Okular::PageTransition::Fade: { QPainter pixmapPainter; m_currentPixmapOpacity += 1.0 / m_transitionSteps; m_lastRenderedPixmap = QPixmap( m_lastRenderedPixmap.size() ); m_lastRenderedPixmap.setDevicePixelRatio( qApp->devicePixelRatio() ); m_lastRenderedPixmap.fill( Qt::transparent ); pixmapPainter.begin( &m_lastRenderedPixmap ); pixmapPainter.setCompositionMode( QPainter::CompositionMode_Source ); pixmapPainter.setOpacity( 1 - m_currentPixmapOpacity ); pixmapPainter.drawPixmap( 0, 0, m_previousPagePixmap ); pixmapPainter.setOpacity( m_currentPixmapOpacity ); pixmapPainter.drawPixmap( 0, 0, m_currentPagePixmap ); update(); if( m_currentPixmapOpacity >= 1 ) return; } break; default: { if ( m_transitionRects.empty() ) { // it's better to fix the transition to cover the whole screen than // enabling the following line that wastes cpu for nothing //update(); return; } for ( int i = 0; i < m_transitionMul && !m_transitionRects.empty(); i++ ) { update( m_transitionRects.first() ); m_transitionRects.pop_front(); } } break; } m_transitionTimer->start( m_transitionDelay ); } void PresentationWidget::slotDelayedEvents() { recalcGeometry(); repositionContent(); if ( m_screenSelect ) { m_screenSelect->setCurrentItem( m_screen ); connect( m_screenSelect->selectableActionGroup(), &QActionGroup::triggered, this, &PresentationWidget::chooseScreen ); } // show widget and take control show(); setWindowState( windowState() | Qt::WindowFullScreen ); connect( QApplication::desktop(), &QDesktopWidget::resized, this, &PresentationWidget::screenResized ); // inform user on how to exit from presentation mode KMessageBox::information( this, i18n("There are two ways of exiting presentation mode, you can press either ESC key or click with the quit button that appears when placing the mouse in the top-right corner. Of course you can cycle windows (Alt+TAB by default)"), QString(), QStringLiteral("presentationInfo") ); } void PresentationWidget::slotPageChanged() { bool ok = true; int p = m_pagesEdit->text().toInt( &ok ); if ( !ok ) return; changePage( p - 1 ); } void PresentationWidget::slotChangeDrawingToolEngine( const QDomElement &element ) { if ( element.isNull() ) { delete m_drawingEngine; m_drawingEngine = nullptr; m_drawingRect = QRect(); setCursor( Qt::ArrowCursor ); } else { m_drawingEngine = new SmoothPathEngine( element ); setCursor( QCursor( QPixmap( QStringLiteral("pencil") ), Qt::ArrowCursor ) ); m_currentDrawingToolElement = element; } } void PresentationWidget::slotAddDrawingToolActions() { DrawingToolActions *drawingToolActions = qobject_cast(sender()); foreach(QAction *action, drawingToolActions->actions()) { action->setEnabled( true ); m_topBar->addAction( action ); addAction( action ); } } void PresentationWidget::clearDrawings() { if ( m_frameIndex != -1 ) m_frames[ m_frameIndex ]->drawings.clear(); update(); } void PresentationWidget::screenResized( int screen ) { // we can ignore if a screen was resized in the case the screen is not // where we are on if ( screen != m_screen ) return; setScreen( screen ); } void PresentationWidget::chooseScreen( QAction *act ) { if ( !act || act->data().type() != QVariant::Int ) return; const int newScreen = act->data().toInt(); setScreen( newScreen ); } void PresentationWidget::toggleBlackScreenMode( bool ) { m_inBlackScreenMode = !m_inBlackScreenMode; update(); } void PresentationWidget::setScreen( int newScreen ) { const QRect screenGeom = QApplication::desktop()->screenGeometry( newScreen ); const QSize oldSize = size(); // qCDebug(OkularUiDebug) << newScreen << "=>" << screenGeom; m_screen = newScreen; setGeometry( screenGeom ); applyNewScreenSize( oldSize ); } void PresentationWidget::applyNewScreenSize( const QSize & oldSize ) { repositionContent(); // if by chance the new screen has the same resolution of the previous, // do not invalidate pixmaps and such.. if ( size() == oldSize ) return; m_width = width(); m_height = height(); // update the frames const float screenRatio = (float)m_height / (float)m_width; for ( PresentationFrame * frame : qAsConst( m_frames ) ) { frame->recalcGeometry( m_width, m_height, screenRatio ); } if ( m_frameIndex != -1 ) { // ugliness alarm! const_cast< Okular::Page * >( m_frames[ m_frameIndex ]->page )->deletePixmap( this ); // force the regeneration of the pixmap m_lastRenderedPixmap = QPixmap(); m_blockNotifications = true; requestPixmaps(); m_blockNotifications = false; } if ( m_transitionTimer->isActive() ) { m_transitionTimer->stop(); } generatePage( true /* no transitions */ ); } void PresentationWidget::inhibitPowerManagement() { #ifdef Q_OS_LINUX QString reason = i18nc( "Reason for inhibiting the screensaver activation, when the presentation mode is active", "Giving a presentation" ); if (!m_screenInhibitCookie) { QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("Inhibit")); message << QCoreApplication::applicationName(); message << reason; QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(message); reply.waitForFinished(); if (reply.isValid()) { m_screenInhibitCookie = reply.value(); qCDebug(OkularUiDebug) << "Screen inhibition cookie" << m_screenInhibitCookie; } else { qCWarning(OkularUiDebug) << "Unable to inhibit screensaver" << reply.error(); } } if (m_sleepInhibitFd != -1) { QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.login1"), QStringLiteral("/org/freedesktop/login1"), QStringLiteral("org.freedesktop.login1.Manager"), QStringLiteral("Inhibit") ); message << QStringLiteral("sleep"); message << QCoreApplication::applicationName(); message << reason; message << QStringLiteral("block"); QDBusPendingReply reply = QDBusConnection::systemBus().asyncCall(message); reply.waitForFinished(); if (reply.isValid()) { m_sleepInhibitFd = dup(reply.value().fileDescriptor()); } else { qCWarning(OkularUiDebug) << "Unable to inhibit sleep" << reply.error(); } } #endif } void PresentationWidget::allowPowerManagement() { #ifdef Q_OS_LINUX if (m_sleepInhibitFd != -1) { ::close(m_sleepInhibitFd); m_sleepInhibitFd = -1; } if (m_screenInhibitCookie) { QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("UnInhibit")); message << m_screenInhibitCookie; QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(message); reply.waitForFinished(); m_screenInhibitCookie = 0; } #endif } void PresentationWidget::showTopBar( bool show ) { if ( show ) { m_topBar->show(); // Don't autohide the mouse cursor if it's over the toolbar if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay ) { KCursor::setAutoHideCursor( this, false ); } // Always show a cursor when topBar is visible if ( !m_drawingEngine ) { setCursor( QCursor( Qt::ArrowCursor ) ); } } else { m_topBar->hide(); // Reenable autohide if need be when leaving the toolbar if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay ) { KCursor::setAutoHideCursor( this, true ); } // Or hide the cursor again if hidden cursor is enabled else if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden ) { // Don't hide the cursor if drawing mode is on if ( !m_drawingEngine ) { setCursor( QCursor( Qt::BlankCursor ) ); } } } // Make sure mouse tracking isn't off after the KCursor::setAutoHideCursor() calls setMouseTracking( true ); } void PresentationWidget::slotFind() { if ( !m_searchBar ) { m_searchBar = new PresentationSearchBar( m_document, this, this ); m_searchBar->forceSnap(); } m_searchBar->focusOnSearchEdit(); m_searchBar->show(); } const Okular::PageTransition PresentationWidget::defaultTransition() const { return defaultTransition( Okular::Settings::slidesTransition() ); } const Okular::PageTransition PresentationWidget::defaultTransition( int type ) const { switch ( type ) { case Okular::Settings::EnumSlidesTransition::BlindsHorizontal: { Okular::PageTransition transition( Okular::PageTransition::Blinds ); transition.setAlignment( Okular::PageTransition::Horizontal ); return transition; break; } case Okular::Settings::EnumSlidesTransition::BlindsVertical: { Okular::PageTransition transition( Okular::PageTransition::Blinds ); transition.setAlignment( Okular::PageTransition::Vertical ); return transition; break; } case Okular::Settings::EnumSlidesTransition::BoxIn: { Okular::PageTransition transition( Okular::PageTransition::Box ); transition.setDirection( Okular::PageTransition::Inward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::BoxOut: { Okular::PageTransition transition( Okular::PageTransition::Box ); transition.setDirection( Okular::PageTransition::Outward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::Dissolve: { return Okular::PageTransition( Okular::PageTransition::Dissolve ); break; } case Okular::Settings::EnumSlidesTransition::GlitterDown: { Okular::PageTransition transition( Okular::PageTransition::Glitter ); transition.setAngle( 270 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::GlitterRight: { Okular::PageTransition transition( Okular::PageTransition::Glitter ); transition.setAngle( 0 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::GlitterRightDown: { Okular::PageTransition transition( Okular::PageTransition::Glitter ); transition.setAngle( 315 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::Random: { return defaultTransition( KRandom::random() % 18 ); break; } case Okular::Settings::EnumSlidesTransition::SplitHorizontalIn: { Okular::PageTransition transition( Okular::PageTransition::Split ); transition.setAlignment( Okular::PageTransition::Horizontal ); transition.setDirection( Okular::PageTransition::Inward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::SplitHorizontalOut: { Okular::PageTransition transition( Okular::PageTransition::Split ); transition.setAlignment( Okular::PageTransition::Horizontal ); transition.setDirection( Okular::PageTransition::Outward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::SplitVerticalIn: { Okular::PageTransition transition( Okular::PageTransition::Split ); transition.setAlignment( Okular::PageTransition::Vertical ); transition.setDirection( Okular::PageTransition::Inward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::SplitVerticalOut: { Okular::PageTransition transition( Okular::PageTransition::Split ); transition.setAlignment( Okular::PageTransition::Vertical ); transition.setDirection( Okular::PageTransition::Outward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::WipeDown: { Okular::PageTransition transition( Okular::PageTransition::Wipe ); transition.setAngle( 270 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::WipeRight: { Okular::PageTransition transition( Okular::PageTransition::Wipe ); transition.setAngle( 0 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::WipeLeft: { Okular::PageTransition transition( Okular::PageTransition::Wipe ); transition.setAngle( 180 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::WipeUp: { Okular::PageTransition transition( Okular::PageTransition::Wipe ); transition.setAngle( 90 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::Fade: { return Okular::PageTransition( Okular::PageTransition::Fade ); break; } case Okular::Settings::EnumSlidesTransition::Replace: default: return Okular::PageTransition( Okular::PageTransition::Replace ); break; } // should not happen, just make gcc happy return Okular::PageTransition(); } /** ONLY the TRANSITIONS GENERATION function from here on **/ void PresentationWidget::initTransition( const Okular::PageTransition *transition ) { // if it's just a 'replace' transition, repaint the screen if ( transition->type() == Okular::PageTransition::Replace ) { update(); return; } const bool isInward = transition->direction() == Okular::PageTransition::Inward; const bool isHorizontal = transition->alignment() == Okular::PageTransition::Horizontal; const float totalTime = transition->duration(); m_transitionRects.clear(); m_currentTransition = *transition; m_currentPagePixmap = m_lastRenderedPixmap; switch( transition->type() ) { // split: horizontal / vertical and inward / outward case Okular::PageTransition::Split: { const int steps = isHorizontal ? 100 : 75; if ( isHorizontal ) { if ( isInward ) { int xPosition = 0; for ( int i = 0; i < steps; i++ ) { int xNext = ((i + 1) * m_width) / (2 * steps); m_transitionRects.push_back( QRect( xPosition, 0, xNext - xPosition, m_height ) ); m_transitionRects.push_back( QRect( m_width - xNext, 0, xNext - xPosition, m_height ) ); xPosition = xNext; } } else { int xPosition = m_width / 2; for ( int i = 0; i < steps; i++ ) { int xNext = ((steps - (i + 1)) * m_width) / (2 * steps); m_transitionRects.push_back( QRect( xNext, 0, xPosition - xNext, m_height ) ); m_transitionRects.push_back( QRect( m_width - xPosition, 0, xPosition - xNext, m_height ) ); xPosition = xNext; } } } else { if ( isInward ) { int yPosition = 0; for ( int i = 0; i < steps; i++ ) { int yNext = ((i + 1) * m_height) / (2 * steps); m_transitionRects.push_back( QRect( 0, yPosition, m_width, yNext - yPosition ) ); m_transitionRects.push_back( QRect( 0, m_height - yNext, m_width, yNext - yPosition ) ); yPosition = yNext; } } else { int yPosition = m_height / 2; for ( int i = 0; i < steps; i++ ) { int yNext = ((steps - (i + 1)) * m_height) / (2 * steps); m_transitionRects.push_back( QRect( 0, yNext, m_width, yPosition - yNext ) ); m_transitionRects.push_back( QRect( 0, m_height - yPosition, m_width, yPosition - yNext ) ); yPosition = yNext; } } } m_transitionMul = 2; m_transitionDelay = (int)( (totalTime * 1000) / steps ); } break; // blinds: horizontal(l-to-r) / vertical(t-to-b) case Okular::PageTransition::Blinds: { const int blinds = isHorizontal ? 8 : 6; const int steps = m_width / (4 * blinds); if ( isHorizontal ) { int xPosition[ 8 ]; for ( int b = 0; b < blinds; b++ ) xPosition[ b ] = (b * m_width) / blinds; for ( int i = 0; i < steps; i++ ) { int stepOffset = (int)( ((float)i * (float)m_width) / ((float)blinds * (float)steps) ); for ( int b = 0; b < blinds; b++ ) { m_transitionRects.push_back( QRect( xPosition[ b ], 0, stepOffset, m_height ) ); xPosition[ b ] = stepOffset + (b * m_width) / blinds; } } } else { int yPosition[ 6 ]; for ( int b = 0; b < blinds; b++ ) yPosition[ b ] = (b * m_height) / blinds; for ( int i = 0; i < steps; i++ ) { int stepOffset = (int)( ((float)i * (float)m_height) / ((float)blinds * (float)steps) ); for ( int b = 0; b < blinds; b++ ) { m_transitionRects.push_back( QRect( 0, yPosition[ b ], m_width, stepOffset ) ); yPosition[ b ] = stepOffset + (b * m_height) / blinds; } } } m_transitionMul = blinds; m_transitionDelay = (int)( (totalTime * 1000) / steps ); } break; // box: inward / outward case Okular::PageTransition::Box: { const int steps = m_width / 10; if ( isInward ) { int L = 0, T = 0, R = m_width, B = m_height; for ( int i = 0; i < steps; i++ ) { // compute shrunk box coords int newL = ((i + 1) * m_width) / (2 * steps); int newT = ((i + 1) * m_height) / (2 * steps); int newR = m_width - newL; int newB = m_height - newT; // add left, right, topcenter, bottomcenter rects m_transitionRects.push_back( QRect( L, T, newL - L, B - T ) ); m_transitionRects.push_back( QRect( newR, T, R - newR, B - T ) ); m_transitionRects.push_back( QRect( newL, T, newR - newL, newT - T ) ); m_transitionRects.push_back( QRect( newL, newB, newR - newL, B - newB ) ); L = newL; T = newT; R = newR, B = newB; } } else { int L = m_width / 2, T = m_height / 2, R = L, B = T; for ( int i = 0; i < steps; i++ ) { // compute shrunk box coords int newL = ((steps - (i + 1)) * m_width) / (2 * steps); int newT = ((steps - (i + 1)) * m_height) / (2 * steps); int newR = m_width - newL; int newB = m_height - newT; // add left, right, topcenter, bottomcenter rects m_transitionRects.push_back( QRect( newL, newT, L - newL, newB - newT ) ); m_transitionRects.push_back( QRect( R, newT, newR - R, newB - newT ) ); m_transitionRects.push_back( QRect( L, newT, R - L, T - newT ) ); m_transitionRects.push_back( QRect( L, B, R - L, newB - B ) ); L = newL; T = newT; R = newR, B = newB; } } m_transitionMul = 4; m_transitionDelay = (int)( (totalTime * 1000) / steps ); } break; // wipe: implemented for 4 canonical angles case Okular::PageTransition::Wipe: { const int angle = transition->angle(); const int steps = (angle == 0) || (angle == 180) ? m_width / 8 : m_height / 8; if ( angle == 0 ) { int xPosition = 0; for ( int i = 0; i < steps; i++ ) { int xNext = ((i + 1) * m_width) / steps; m_transitionRects.push_back( QRect( xPosition, 0, xNext - xPosition, m_height ) ); xPosition = xNext; } } else if ( angle == 90 ) { int yPosition = m_height; for ( int i = 0; i < steps; i++ ) { int yNext = ((steps - (i + 1)) * m_height) / steps; m_transitionRects.push_back( QRect( 0, yNext, m_width, yPosition - yNext ) ); yPosition = yNext; } } else if ( angle == 180 ) { int xPosition = m_width; for ( int i = 0; i < steps; i++ ) { int xNext = ((steps - (i + 1)) * m_width) / steps; m_transitionRects.push_back( QRect( xNext, 0, xPosition - xNext, m_height ) ); xPosition = xNext; } } else if ( angle == 270 ) { int yPosition = 0; for ( int i = 0; i < steps; i++ ) { int yNext = ((i + 1) * m_height) / steps; m_transitionRects.push_back( QRect( 0, yPosition, m_width, yNext - yPosition ) ); yPosition = yNext; } } else { update(); return; } m_transitionMul = 1; m_transitionDelay = (int)( (totalTime * 1000) / steps ); } break; // dissolve: replace 'random' rects case Okular::PageTransition::Dissolve: { const int gridXsteps = 50; const int gridYsteps = 38; const int steps = gridXsteps * gridYsteps; int oldX = 0; int oldY = 0; // create a grid of gridXstep by gridYstep QRects for ( int y = 0; y < gridYsteps; y++ ) { int newY = (int)( m_height * ((float)(y+1) / (float)gridYsteps) ); for ( int x = 0; x < gridXsteps; x++ ) { int newX = (int)( m_width * ((float)(x+1) / (float)gridXsteps) ); m_transitionRects.push_back( QRect( oldX, oldY, newX - oldX, newY - oldY ) ); oldX = newX; } oldX = 0; oldY = newY; } // randomize the grid for ( int i = 0; i < steps; i++ ) { #ifndef Q_OS_WIN int n1 = (int)(steps * drand48()); int n2 = (int)(steps * drand48()); #else int n1 = (int)(steps * (std::rand() / RAND_MAX)); int n2 = (int)(steps * (std::rand() / RAND_MAX)); #endif // swap items if index differs if ( n1 != n2 ) { QRect r = m_transitionRects[ n2 ]; m_transitionRects[ n2 ] = m_transitionRects[ n1 ]; m_transitionRects[ n1 ] = r; } } // set global transition parameters m_transitionMul = 40; m_transitionDelay = (int)( (m_transitionMul * 1000 * totalTime) / steps ); } break; // glitter: similar to dissolve but has a direction case Okular::PageTransition::Glitter: { const int gridXsteps = 50; const int gridYsteps = 38; const int steps = gridXsteps * gridYsteps; const int angle = transition->angle(); // generate boxes using a given direction if ( angle == 90 ) { int yPosition = m_height; for ( int i = 0; i < gridYsteps; i++ ) { int yNext = ((gridYsteps - (i + 1)) * m_height) / gridYsteps; int xPosition = 0; for ( int j = 0; j < gridXsteps; j++ ) { int xNext = ((j + 1) * m_width) / gridXsteps; m_transitionRects.push_back( QRect( xPosition, yNext, xNext - xPosition, yPosition - yNext ) ); xPosition = xNext; } yPosition = yNext; } } else if ( angle == 180 ) { int xPosition = m_width; for ( int i = 0; i < gridXsteps; i++ ) { int xNext = ((gridXsteps - (i + 1)) * m_width) / gridXsteps; int yPosition = 0; for ( int j = 0; j < gridYsteps; j++ ) { int yNext = ((j + 1) * m_height) / gridYsteps; m_transitionRects.push_back( QRect( xNext, yPosition, xPosition - xNext, yNext - yPosition ) ); yPosition = yNext; } xPosition = xNext; } } else if ( angle == 270 ) { int yPosition = 0; for ( int i = 0; i < gridYsteps; i++ ) { int yNext = ((i + 1) * m_height) / gridYsteps; int xPosition = 0; for ( int j = 0; j < gridXsteps; j++ ) { int xNext = ((j + 1) * m_width) / gridXsteps; m_transitionRects.push_back( QRect( xPosition, yPosition, xNext - xPosition, yNext - yPosition ) ); xPosition = xNext; } yPosition = yNext; } } else // if angle is 0 or 315 { int xPosition = 0; for ( int i = 0; i < gridXsteps; i++ ) { int xNext = ((i + 1) * m_width) / gridXsteps; int yPosition = 0; for ( int j = 0; j < gridYsteps; j++ ) { int yNext = ((j + 1) * m_height) / gridYsteps; m_transitionRects.push_back( QRect( xPosition, yPosition, xNext - xPosition, yNext - yPosition ) ); yPosition = yNext; } xPosition = xNext; } } // add a 'glitter' (1 over 10 pieces is randomized) int randomSteps = steps / 20; for ( int i = 0; i < randomSteps; i++ ) { #ifndef Q_OS_WIN int n1 = (int)(steps * drand48()); int n2 = (int)(steps * drand48()); #else int n1 = (int)(steps * (std::rand() / RAND_MAX)); int n2 = (int)(steps * (std::rand() / RAND_MAX)); #endif // swap items if index differs if ( n1 != n2 ) { QRect r = m_transitionRects[ n2 ]; m_transitionRects[ n2 ] = m_transitionRects[ n1 ]; m_transitionRects[ n1 ] = r; } } // set global transition parameters m_transitionMul = (angle == 90) || (angle == 270) ? gridYsteps : gridXsteps; m_transitionMul /= 2; m_transitionDelay = (int)( (m_transitionMul * 1000 * totalTime) / steps ); } break; case Okular::PageTransition::Fade: { enum {FADE_TRANSITION_FPS = 20}; const int steps = totalTime * FADE_TRANSITION_FPS; m_transitionSteps = steps; QPainter pixmapPainter; m_currentPixmapOpacity = (double) 1 / steps; m_transitionDelay = (int)( totalTime * 1000 ) / steps; m_lastRenderedPixmap = QPixmap( m_lastRenderedPixmap.size() ); m_lastRenderedPixmap.fill( Qt::transparent ); pixmapPainter.begin( &m_lastRenderedPixmap ); pixmapPainter.setCompositionMode( QPainter::CompositionMode_Source ); pixmapPainter.setOpacity( 1 - m_currentPixmapOpacity ); pixmapPainter.drawPixmap( 0, 0, m_previousPagePixmap ); pixmapPainter.setOpacity( m_currentPixmapOpacity ); pixmapPainter.drawPixmap( 0, 0, m_currentPagePixmap ); pixmapPainter.end(); update(); } break; // implement missing transitions (a binary raster engine needed here) case Okular::PageTransition::Fly: case Okular::PageTransition::Push: case Okular::PageTransition::Cover: case Okular::PageTransition::Uncover: default: update(); return; } // send the first start to the timer m_transitionTimer->start( 0 ); } void PresentationWidget::slotProcessMovieAction( const Okular::MovieAction *action ) { const Okular::MovieAnnotation *movieAnnotation = action->annotation(); if ( !movieAnnotation ) return; Okular::Movie *movie = movieAnnotation->movie(); if ( !movie ) return; VideoWidget *vw = m_frames[ m_frameIndex ]->videoWidgets.value( movieAnnotation->movie() ); if ( !vw ) return; vw->show(); switch ( action->operation() ) { case Okular::MovieAction::Play: vw->stop(); vw->play(); break; case Okular::MovieAction::Stop: vw->stop(); break; case Okular::MovieAction::Pause: vw->pause(); break; case Okular::MovieAction::Resume: vw->play(); break; }; } void PresentationWidget::slotProcessRenditionAction( const Okular::RenditionAction *action ) { Okular::Movie *movie = action->movie(); if ( !movie ) return; VideoWidget *vw = m_frames[ m_frameIndex ]->videoWidgets.value( movie ); if ( !vw ) return; if ( action->operation() == Okular::RenditionAction::None ) return; vw->show(); switch ( action->operation() ) { case Okular::RenditionAction::Play: vw->stop(); vw->play(); break; case Okular::RenditionAction::Stop: vw->stop(); break; case Okular::RenditionAction::Pause: vw->pause(); break; case Okular::RenditionAction::Resume: vw->play(); break; default: return; }; } void PresentationWidget::slotTogglePlayPause() { m_advanceSlides = !m_advanceSlides; setPlayPauseIcon(); if ( m_advanceSlides ) { startAutoChangeTimer(); } else { m_nextPageTimer->stop(); } } #include "presentationwidget.moc"