diff --git a/autotests/data/checkbox_ro.pdf b/autotests/data/checkbox_ro.pdf new file mode 100644 index 000000000..e5d62b02d Binary files /dev/null and b/autotests/data/checkbox_ro.pdf differ diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp index 7fd37c3c9..f2932fb1e 100644 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -1,1323 +1,1441 @@ /*************************************************************************** * 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/pageview.h" +#include "../generators/poppler/config-okular-poppler.h" + #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_part(p), m_button(b), m_clicked(false) { QTimer::singleShot(0, this, &CloseDialogHelper::closeDialog); } ~CloseDialogHelper() { QVERIFY(m_clicked); } private slots: void closeDialog() { QDialog *dialog = m_part->widget()->findChild(); if (!dialog) { QTimer::singleShot(0, this, &CloseDialogHelper::closeDialog); return; } QDialogButtonBox *buttonBox = dialog->findChild(); buttonBox->button(m_button)->click(); m_clicked = true; } private: Okular::Part *m_part; 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 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(); 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("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") << QString::fromUtf8("synctextest"); 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())); 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)); 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())); 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)); 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())); 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)); // 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())); 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)); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); // overwrite urlHandler for 'mailto' urls QDesktopServices::setUrlHandler("mailto", this, "urlHandler"); QSignalSpy openUrlSignalSpy(this, SIGNAL(urlHandler(QUrl))); // 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())); 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)); // 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())); 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)); // 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("mailto", this, "urlHandler"); QSignalSpy openUrlSignalSpy(this, SIGNAL(urlHandler(QUrl))); // 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())); 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)); // 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("PopupMenu")); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains go-to link action QAction *goToAction = qobject_cast(menu->findChild("GoToAction")); QVERIFY(goToAction); // check if the "follow this link" action is not visible QAction *processLinkAction = qobject_cast(menu->findChild("ProcessLinkAction")); QVERIFY(!processLinkAction); // check if the "copy link address" action is not visible QAction *copyLinkLocation = qobject_cast(menu->findChild("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())); 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)); // 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("PopupMenu")); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains "follow this link" action QAction *processLinkAction = qobject_cast(menu->findChild("ProcessLinkAction")); QVERIFY(processLinkAction); // check if the menu contains "copy link address" action QAction *copyLinkLocation = qobject_cast(menu->findChild("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())); 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)); // 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("PopupMenu")); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains "Follow this link" action QAction *processLink = qobject_cast(menu->findChild("ProcessLinkAction")); QVERIFY(processLink); // chek if the menu contains "Copy Link Address" action QAction *actCopyLinkLocation = qobject_cast(menu->findChild("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())); 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)); // 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())); 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)); // 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("PopupMenu")); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the copy selected text to clipboard is present QAction *copyAct = qobject_cast(menu->findChild("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::testSaveAs() { QFETCH(QString, file); QFETCH(QString, extension); QFETCH(bool, nativelySupportsAnnotations); QFETCH(bool, canSwapBackingFile); QScopedPointer closeDialogHelper; QString annotName; QTemporaryFile archiveSave( QString( "%1/okrXXXXXX.okular" ).arg( QDir::tempPath() ) ); QTemporaryFile nativeDirectSave( QString( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); QTemporaryFile nativeFromArchiveFile( QString( "%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 ); 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( "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 ) ); } 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() ); 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 } 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::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( QString( "%1/okrXXXXXX_1.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); QVERIFY( saveFile1.open() ); saveFile1.close(); QTemporaryFile saveFile2( QString( "%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( "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 provived 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 provived 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( QString( "%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, "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, "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( QString( "%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() ); +} + } 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(QLatin1String("okularparttest")); app.setOrganizationDomain(QLatin1String("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/core/form.cpp b/core/form.cpp index 530499fa3..35b05c2a6 100644 --- a/core/form.cpp +++ b/core/form.cpp @@ -1,273 +1,277 @@ /*************************************************************************** * Copyright (C) 2007 by Pino Toscano * * * * 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 "form.h" #include "form_p.h" // qt includes #include #include "action.h" using namespace Okular; FormFieldPrivate::FormFieldPrivate( FormField::FieldType type ) : m_type( type ), m_activateAction( nullptr ) { } FormFieldPrivate::~FormFieldPrivate() { delete m_activateAction; } void FormFieldPrivate::setDefault() { m_default = value(); } FormField::FormField( FormFieldPrivate &dd ) : d_ptr( &dd ) { d_ptr->q_ptr = this; } FormField::~FormField() { delete d_ptr; } FormField::FieldType FormField::type() const { Q_D( const FormField ); return d->m_type; } bool FormField::isReadOnly() const { return false; } +void FormField::setReadOnly( bool ) +{ +} + bool FormField::isVisible() const { return true; } Action* FormField::activationAction() const { Q_D( const FormField ); return d->m_activateAction; } void FormField::setActivationAction( Action *action ) { Q_D( FormField ); delete d->m_activateAction; d->m_activateAction = action; } Action* FormField::additionalAction( AdditionalActionType type ) const { Q_D( const FormField ); return d->m_additionalActions.value(type); } void FormField::setAdditionalAction( AdditionalActionType type, Action *action ) { Q_D( FormField ); delete d->m_additionalActions.value(type); d->m_additionalActions[type] = action; } class Okular::FormFieldButtonPrivate : public Okular::FormFieldPrivate { public: FormFieldButtonPrivate() : FormFieldPrivate( FormField::FormButton ) { } Q_DECLARE_PUBLIC( FormFieldButton ) void setValue( const QString& v ) override { Q_Q( FormFieldButton ); q->setState( QVariant( v ).toBool() ); } QString value() const override { Q_Q( const FormFieldButton ); return qVariantFromValue( q->state() ).toString(); } }; FormFieldButton::FormFieldButton() : FormField( *new FormFieldButtonPrivate() ) { } FormFieldButton::~FormFieldButton() { } void FormFieldButton::setState( bool ) { } class Okular::FormFieldTextPrivate : public Okular::FormFieldPrivate { public: FormFieldTextPrivate() : FormFieldPrivate( FormField::FormText ) { } Q_DECLARE_PUBLIC( FormFieldText ) void setValue( const QString& v ) override { Q_Q( FormFieldText ); q->setText( v ); } QString value() const override { Q_Q( const FormFieldText ); return q->text(); } }; FormFieldText::FormFieldText() : FormField( *new FormFieldTextPrivate() ) { } FormFieldText::~FormFieldText() { } void FormFieldText::setText( const QString& ) { } bool FormFieldText::isPassword() const { return false; } bool FormFieldText::isRichText() const { return false; } int FormFieldText::maximumLength() const { return -1; } Qt::Alignment FormFieldText::textAlignment() const { return Qt::AlignVCenter | Qt::AlignLeft; } bool FormFieldText::canBeSpellChecked() const { return false; } class Okular::FormFieldChoicePrivate : public Okular::FormFieldPrivate { public: FormFieldChoicePrivate() : FormFieldPrivate( FormField::FormChoice ) { } Q_DECLARE_PUBLIC( FormFieldChoice ) void setValue( const QString& v ) override { Q_Q( FormFieldChoice ); QStringList choices = v.split( QLatin1Char (';'), QString::SkipEmptyParts ); QList newchoices; foreach ( const QString& str, choices ) { bool ok = true; int val = str.toInt( &ok ); if ( ok ) newchoices.append( val ); } if ( !newchoices.isEmpty() ) q->setCurrentChoices( newchoices ); } QString value() const override { Q_Q( const FormFieldChoice ); QList choices = q->currentChoices(); qSort( choices ); QStringList list; foreach ( int c, choices ) { list.append( QString::number( c ) ); } return list.join( QStringLiteral( ";" ) ); } }; FormFieldChoice::FormFieldChoice() : FormField( *new FormFieldChoicePrivate() ) { } FormFieldChoice::~FormFieldChoice() { } bool FormFieldChoice::isEditable() const { return false; } bool FormFieldChoice::multiSelect() const { return false; } void FormFieldChoice::setCurrentChoices( const QList< int >& ) { } QString FormFieldChoice::editChoice() const { return QString(); } void FormFieldChoice::setEditChoice( const QString& ) { } Qt::Alignment FormFieldChoice::textAlignment() const { return Qt::AlignVCenter | Qt::AlignLeft; } bool FormFieldChoice::canBeSpellChecked() const { return false; } diff --git a/core/form.h b/core/form.h index 359019e7d..20b68c517 100644 --- a/core/form.h +++ b/core/form.h @@ -1,368 +1,375 @@ /*************************************************************************** * Copyright (C) 2007 by Pino Toscano * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_FORM_H_ #define _OKULAR_FORM_H_ #include "okularcore_export.h" #include "area.h" #include namespace Okular { class Action; class Page; class PagePrivate; class FormFieldPrivate; class FormFieldButtonPrivate; class FormFieldTextPrivate; class FormFieldChoicePrivate; /** * @short The base interface of a form field. * * This is the very basic interface to represent a field in a form. * * This is not meant to be used as a direct base for the form fields in a * document, but its abstract subclasses are. */ class OKULARCORE_EXPORT FormField { /// @cond PRIVATE friend class Page; friend class PagePrivate; /// @endcond public: /** * The types of form field. */ enum FieldType { FormButton, ///< A "button". See @ref FormFieldButton::ButtonType. FormText, ///< A field of variable text. See @ref FormFieldText::TextType. FormChoice, ///< A choice field. See @ref FormFieldChoice::ChoiceType. FormSignature ///< A signature. }; virtual ~FormField(); /** * The type of the field. */ FieldType type() const; /** * The bouding rect of the field, in normalized coordinates. */ virtual NormalizedRect rect() const = 0; /** * The ID of the field. */ virtual int id() const = 0; /** * The internal name of the field, to be used when referring to the * field in eg scripts. */ virtual QString name() const = 0; /** * The visible name of the field, to be used in the user interface * (eg in error messages, etc). */ virtual QString uiName() const = 0; /** * Whether the field is read-only. */ virtual bool isReadOnly() const; + /** + * Whether the field is read-only. + * + * @since 1.4 + */ + virtual void setReadOnly( bool value ); + /** * Whether this form field is visible. */ virtual bool isVisible() const; Action* activationAction() const; /** * Describes the type of form additional action. * * @since 1.1 */ enum AdditionalActionType { FieldModified, ///< An action to be performed when the user modifies the field FormatField, ///< An action to be performed before the field is formatted to display its value ValidateField, ///< An action to be performed when the field value changes CalculateField, ///< An action to be performed when the field needs to be recalculated }; /** * Returns the additional action of the given @p type or @c nullptr if no action has been defined. * * @since 1.1 */ Action* additionalAction( AdditionalActionType type ) const; protected: /// @cond PRIVATE FormField( FormFieldPrivate &dd ); Q_DECLARE_PRIVATE( FormField ) FormFieldPrivate *d_ptr; /// @endcond void setActivationAction( Action *action ); void setAdditionalAction( AdditionalActionType type, Action *action ); private: Q_DISABLE_COPY( FormField ) }; /** * @short Interface of a button form field. * * This is the base interface to reimplement to represent a button field, like * a push button, a check box or a radio button. * * @since 0.7 (KDE 4.1) */ class OKULARCORE_EXPORT FormFieldButton : public FormField { public: /** * The types of button field. */ enum ButtonType { Push, ///< A simple push button. CheckBox, ///< A check box. Radio ///< A radio button. }; virtual ~FormFieldButton(); /** The particular type of the button field. */ virtual ButtonType buttonType() const = 0; /** * The caption to be used for the button. */ virtual QString caption() const = 0; /** * The state of the button. */ virtual bool state() const = 0; /** * Sets the state of the button to the new \p state . */ virtual void setState( bool state ); /** * The list with the IDs of siblings (ie, buttons belonging to the same * group as the current one. * * Valid only for \ref Radio buttons, an empty list otherwise. */ virtual QList< int > siblings() const = 0; protected: FormFieldButton(); private: Q_DECLARE_PRIVATE( FormFieldButton ) Q_DISABLE_COPY( FormFieldButton ) }; /** * @short Interface of a text form field. * * This is the base interface to reimplement to represent a text field, ie a * field where the user insert text. */ class OKULARCORE_EXPORT FormFieldText : public FormField { public: /** * The types of text field. */ enum TextType { Normal, ///< A simple singleline text field. Multiline, ///< A multiline text field. FileSelect ///< An input field to select the path of a file on disk. }; virtual ~FormFieldText(); /** * The particular type of the text field. */ virtual TextType textType() const = 0; /** * The text of text field. */ virtual QString text() const = 0; /** * Sets the new @p text in the text field. * * The default implementation does nothing. * * Reimplemented only if the setting of new text is supported. */ virtual void setText( const QString& text ); /** * Whether this text field is a password input, eg its text @b must be * replaced with asterisks. * * Always false for @ref FileSelect text fields. */ virtual bool isPassword() const; /** * Whether this text field should allow rich text. */ virtual bool isRichText() const; /** * The maximum length allowed for the text of text field, or -1 if * there is no limitation for the text. */ virtual int maximumLength() const; /** * The alignment of the text within the field. */ virtual Qt::Alignment textAlignment() const; /** * Whether the text inserted manually in the field (where possible) * can be spell-checked. * * @note meaningful only if the field is editable. */ virtual bool canBeSpellChecked() const; protected: FormFieldText(); private: Q_DECLARE_PRIVATE( FormFieldText ) Q_DISABLE_COPY( FormFieldText ) }; /** * @short Interface of a choice form field. * * This is the base interface to reimplement to represent a choice field, ie a * field where the user can select one (of more) element(s) among a set of * choices. */ class OKULARCORE_EXPORT FormFieldChoice : public FormField { public: /** * The types of choice field. */ enum ChoiceType { ComboBox, ///< A combo box choice field. ListBox ///< A list box choice field. }; virtual ~FormFieldChoice(); /** * The particular type of the choice field. */ virtual ChoiceType choiceType() const = 0; /** * The possible choices of the choice field. */ virtual QStringList choices() const = 0; /** * Whether this ComboBox is editable, ie the user can type in a custom * value. * * Always false for the other types of choices. */ virtual bool isEditable() const; /** * Whether more than one choice of this ListBox can be selected at the * same time. * * Always false for the other types of choices. */ virtual bool multiSelect() const; /** * The currently selected choices. * * Always one element in the list in case of single choice elements. */ virtual QList< int > currentChoices() const = 0; /** * Sets the selected choices to @p choices . */ virtual void setCurrentChoices( const QList< int >& choices ); /** The text entered into an editable combo box choice field @since 0.16 (KDE 4.10) */ virtual QString editChoice() const; /** Sets the text entered into an editable combo box choice field @since 0.16 (KDE 4.10) */ virtual void setEditChoice( const QString& text ); /** * The alignment of the text within the field. */ virtual Qt::Alignment textAlignment() const; /** * Whether the text inserted manually in the field (where possible) * can be spell-checked. * * @note meaningful only if the field is editable. */ virtual bool canBeSpellChecked() const; protected: FormFieldChoice(); private: Q_DECLARE_PRIVATE( FormFieldChoice ) Q_DISABLE_COPY( FormFieldChoice ) }; } #endif diff --git a/core/script/kjs_field.cpp b/core/script/kjs_field.cpp index b3798ebb7..2cc9bd026 100644 --- a/core/script/kjs_field.cpp +++ b/core/script/kjs_field.cpp @@ -1,229 +1,243 @@ /*************************************************************************** * Copyright (C) 2008 by Pino Toscano * * Copyright (C) 2008 by Harri Porten * * * * 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 "kjs_field_p.h" #include #include #include #include #include #include "../debug_p.h" #include "../document_p.h" #include "../form.h" #include "../page.h" #include "../page_p.h" using namespace Okular; static KJSPrototype *g_fieldProto; typedef QHash< FormField *, Page * > FormCache; Q_GLOBAL_STATIC( FormCache, g_fieldCache ) + +// Helper for modified fields +static void updateField( FormField *field ) +{ + Page *page = g_fieldCache->value( field ); + if (page) + { + Document *doc = PagePrivate::get( page )->m_doc->m_parent; + QMetaObject::invokeMethod( doc, "refreshPixmaps", Qt::QueuedConnection, Q_ARG( int, page->number() ) ); + emit doc->refreshFormWidget( field ); + } + else + { + qWarning() << "Could not get page of field" << field; + } +} + // Field.doc static KJSObject fieldGetDoc( KJSContext *context, void * ) { return context->interpreter().globalObject(); } // Field.name static KJSObject fieldGetName( KJSContext *, void *object ) { const FormField *field = reinterpret_cast< FormField * >( object ); return KJSString( field->name() ); } // Field.readonly (getter) static KJSObject fieldGetReadOnly( KJSContext *, void *object ) { const FormField *field = reinterpret_cast< FormField * >( object ); return KJSBoolean( field->isReadOnly() ); } // Field.readonly (setter) static void fieldSetReadOnly( KJSContext *context, void *object, KJSObject value ) { -#if 0 FormField *field = reinterpret_cast< FormField * >( object ); bool b = value.toBoolean( context ); field->setReadOnly( b ); -#else - Q_UNUSED( context ); - Q_UNUSED( object ); - Q_UNUSED( value ); - qCDebug(OkularCoreDebug) << "Not implemented: setting readonly property"; -#endif + + updateField( field ); } static QString fieldGetTypeHelper( const FormField *field ) { switch ( field->type() ) { case FormField::FormButton: { const FormFieldButton *button = static_cast< const FormFieldButton * >( field ); switch ( button->buttonType() ) { case FormFieldButton::Push: return QStringLiteral("button"); case FormFieldButton::CheckBox: return QStringLiteral("checkbox"); case FormFieldButton::Radio: return QStringLiteral("radiobutton"); } break; } case FormField::FormText: return QStringLiteral("text"); case FormField::FormChoice: { const FormFieldChoice *choice = static_cast< const FormFieldChoice * >( field ); switch ( choice->choiceType() ) { case FormFieldChoice::ComboBox: return QStringLiteral("combobox"); case FormFieldChoice::ListBox: return QStringLiteral("listbox"); } break; } case FormField::FormSignature: return QStringLiteral("signature"); } return QString(); } // Field.type static KJSObject fieldGetType( KJSContext *, void *object ) { const FormField *field = reinterpret_cast< FormField * >( object ); return KJSString( fieldGetTypeHelper( field ) ); } // Field.value (getter) static KJSObject fieldGetValue( KJSContext */*context*/, void *object ) { FormField *field = reinterpret_cast< FormField * >( object ); switch ( field->type() ) { case FormField::FormButton: { const FormFieldButton *button = static_cast< const FormFieldButton * >( field ); - Q_UNUSED( button ); // ### - break; + if ( button->state() ) + { + return KJSString( QStringLiteral( "Yes" ) ); + } + return KJSString( QStringLiteral( "Off" ) ); } case FormField::FormText: { const FormFieldText *text = static_cast< const FormFieldText * >( field ); return KJSString( text->text() ); } case FormField::FormChoice: { const FormFieldChoice *choice = static_cast< const FormFieldChoice * >( field ); Q_UNUSED( choice ); // ### break; } case FormField::FormSignature: { break; } } return KJSUndefined(); } // Field.value (setter) static void fieldSetValue( KJSContext *context, void *object, KJSObject value ) { FormField *field = reinterpret_cast< FormField * >( object ); switch ( field->type() ) { case FormField::FormButton: { FormFieldButton *button = static_cast< FormFieldButton * >( field ); - Q_UNUSED( button ); // ### + const QString text = value.toString( context ); + if ( text == QStringLiteral( "Yes" ) ) + { + button->setState( true ); + updateField( field ); + } + else if ( text == QStringLiteral( "Off" ) ) + { + button->setState( false ); + updateField( field ); + } break; } case FormField::FormText: { FormFieldText *textField = static_cast< FormFieldText * >( field ); const QString text = value.toString( context ); if ( text != textField->text() ) { textField->setText( text ); - - Page *page = g_fieldCache->value( field ); - if (page) - { - Document *doc = PagePrivate::get( page )->m_doc->m_parent; - QMetaObject::invokeMethod( doc, "refreshPixmaps", Qt::QueuedConnection, Q_ARG( int, page->number() ) ); - emit doc->refreshFormWidget( field ); - } - else - { - qWarning() << "Could not get page of field" << field; - } + updateField( field ); } break; } case FormField::FormChoice: { FormFieldChoice *choice = static_cast< FormFieldChoice * >( field ); Q_UNUSED( choice ); // ### break; } case FormField::FormSignature: { break; } } } void JSField::initType( KJSContext *ctx ) { static bool initialized = false; if ( initialized ) return; initialized = true; if ( !g_fieldProto ) g_fieldProto = new KJSPrototype(); g_fieldProto->defineProperty( ctx, QStringLiteral("doc"), fieldGetDoc ); g_fieldProto->defineProperty( ctx, QStringLiteral("name"), fieldGetName ); g_fieldProto->defineProperty( ctx, QStringLiteral("readonly"), fieldGetReadOnly, fieldSetReadOnly ); g_fieldProto->defineProperty( ctx, QStringLiteral("type"), fieldGetType ); g_fieldProto->defineProperty( ctx, QStringLiteral("value"), fieldGetValue, fieldSetValue ); } KJSObject JSField::wrapField( KJSContext *ctx, FormField *field, Page *page ) { // ### cache unique wrapper KJSObject f = g_fieldProto->constructObject( ctx, field ); f.setProperty( ctx, QStringLiteral("page"), page->number() ); g_fieldCache->insert( field, page ); return f; } void JSField::clearCachedFields() { if ( g_fieldCache.exists() ) { g_fieldCache->clear(); } } diff --git a/generators/poppler/CMakeLists.txt b/generators/poppler/CMakeLists.txt index a3dd38dc9..73881f0e1 100644 --- a/generators/poppler/CMakeLists.txt +++ b/generators/poppler/CMakeLists.txt @@ -1,105 +1,115 @@ remove_definitions(-DTRANSLATION_DOMAIN="okular") add_definitions(-DTRANSLATION_DOMAIN="okular_poppler") add_subdirectory( conf ) if (Poppler_VERSION VERSION_GREATER "0.23.99") set (HAVE_POPPLER_0_24 1) endif() if (Poppler_VERSION VERSION_GREATER "0.27.99") set (HAVE_POPPLER_0_28 1) endif() if (Poppler_VERSION VERSION_GREATER "0.35.99") set (HAVE_POPPLER_0_36 1) endif() if (Poppler_VERSION VERSION_GREATER "0.36.99") set (HAVE_POPPLER_0_37 1) endif() set(CMAKE_REQUIRED_LIBRARIES Poppler::Qt5 Qt5::Core Qt5::Gui) check_cxx_source_compiles(" #include int main() { Poppler::LinkOCGState *l = 0; return 0; } " HAVE_POPPLER_0_50) check_cxx_source_compiles(" #include #include int main() { Poppler::FormFieldButton *ff = 0; Poppler::Link *l = ff->additionalAction(Poppler::FormField::CalculateField); return 0; } " HAVE_POPPLER_0_53) check_cxx_source_compiles(" #include int main() { Poppler::Document::RenderHint hint = Poppler::Document::HideAnnotations; return 0; } " HAVE_POPPLER_0_60) check_cxx_source_compiles(" #include #include int main() { Poppler::Page *p; p->renderToImage(0, 0, 0, 0, 0, 0, Poppler::Page::Rotate0, nullptr, nullptr, QVariant()); return 0; } " HAVE_POPPLER_0_62) check_cxx_source_compiles(" #include #include int main() { Poppler::Page *p; p->renderToImage(0, 0, 0, 0, 0, 0, Poppler::Page::Rotate0, nullptr, nullptr, nullptr, QVariant()); return 0; } " HAVE_POPPLER_0_63) +check_cxx_source_compiles(" +#include +#include +int main() +{ + Poppler::FormField *f; + f->setReadOnly(true); +} +" HAVE_POPPLER_0_64) + configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/config-okular-poppler.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-okular-poppler.h ) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/../.. ) ########### next target ############### set(okularGenerator_poppler_PART_SRCS generator_pdf.cpp formfields.cpp annots.cpp ) ki18n_wrap_ui(okularGenerator_poppler_PART_SRCS conf/pdfsettingswidget.ui ) kconfig_add_kcfg_files(okularGenerator_poppler_PART_SRCS conf/pdfsettings.kcfgc ) okular_add_generator(okularGenerator_poppler ${okularGenerator_poppler_PART_SRCS}) target_link_libraries(okularGenerator_poppler okularcore KF5::I18n KF5::Completion Poppler::Qt5 Qt5::Xml) ########### install files ############### install( FILES okularPoppler.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR} ) install( PROGRAMS okularApplication_pdf.desktop org.kde.mobile.okular_pdf.desktop DESTINATION ${KDE_INSTALL_APPDIR} ) install( FILES org.kde.okular-poppler.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) diff --git a/generators/poppler/config-okular-poppler.h.cmake b/generators/poppler/config-okular-poppler.h.cmake index 16831f749..99f23fc86 100644 --- a/generators/poppler/config-okular-poppler.h.cmake +++ b/generators/poppler/config-okular-poppler.h.cmake @@ -1,26 +1,29 @@ /* Defined if we have the 0.24 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_24 1 /* Defined if we have the 0.28 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_28 1 /* Defined if we have the 0.36 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_36 1 /* Defined if we have the 0.37 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_37 1 /* Defined if we have the 0.50 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_50 1 /* Defined if we have the 0.53 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_53 1 /* Defined if we have the 0.60 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_60 1 /* Defined if we have the 0.62 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_62 1 /* Defined if we have the 0.63 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_63 1 + +/* Defined if we have the 0.64 version of the Poppler library */ +#cmakedefine HAVE_POPPLER_0_64 1 diff --git a/generators/poppler/formfields.cpp b/generators/poppler/formfields.cpp index 5539525e7..78fc69d12 100644 --- a/generators/poppler/formfields.cpp +++ b/generators/poppler/formfields.cpp @@ -1,303 +1,330 @@ /*************************************************************************** * Copyright (C) 2007 by Pino Toscano * * * * 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 "formfields.h" #include "core/action.h" #include #include extern Okular::Action* createLinkFromPopplerLink(const Poppler::Link *popplerLink); #ifdef HAVE_POPPLER_0_53 #define SET_ACTIONS \ setActivationAction( createLinkFromPopplerLink( field->activationAction() ) ); \ setAdditionalAction( Okular::FormField::FieldModified, createLinkFromPopplerLink( field->additionalAction( Poppler::FormField::FieldModified ) ) ); \ setAdditionalAction( Okular::FormField::FormatField, createLinkFromPopplerLink( field->additionalAction( Poppler::FormField::FormatField ) ) ); \ setAdditionalAction( Okular::FormField::ValidateField, createLinkFromPopplerLink( field->additionalAction( Poppler::FormField::ValidateField ) ) ); \ setAdditionalAction( Okular::FormField::CalculateField, createLinkFromPopplerLink( field->additionalAction( Poppler::FormField::CalculateField ) ) ); #else #define SET_ACTIONS \ setActivationAction( createLinkFromPopplerLink( field->activationAction() ) ); #endif PopplerFormFieldButton::PopplerFormFieldButton( Poppler::FormFieldButton * field ) : Okular::FormFieldButton(), m_field( field ) { m_rect = Okular::NormalizedRect::fromQRectF( m_field->rect() ); m_id = m_field->id(); SET_ACTIONS } PopplerFormFieldButton::~PopplerFormFieldButton() { delete m_field; } Okular::NormalizedRect PopplerFormFieldButton::rect() const { return m_rect; } int PopplerFormFieldButton::id() const { return m_id; } QString PopplerFormFieldButton::name() const { return m_field->name(); } QString PopplerFormFieldButton::uiName() const { return m_field->uiName(); } bool PopplerFormFieldButton::isReadOnly() const { return m_field->isReadOnly(); } +void PopplerFormFieldButton::setReadOnly( bool value ) +{ +#ifdef HAVE_POPPLER_0_64 + m_field->setReadOnly( value ); +#else + Q_UNUSED( value ); +#endif +} + bool PopplerFormFieldButton::isVisible() const { return m_field->isVisible(); } Okular::FormFieldButton::ButtonType PopplerFormFieldButton::buttonType() const { switch ( m_field->buttonType() ) { case Poppler::FormFieldButton::Push: return Okular::FormFieldButton::Push; case Poppler::FormFieldButton::CheckBox: return Okular::FormFieldButton::CheckBox; case Poppler::FormFieldButton::Radio: return Okular::FormFieldButton::Radio; } return Okular::FormFieldButton::Push; } QString PopplerFormFieldButton::caption() const { return m_field->caption(); } bool PopplerFormFieldButton::state() const { return m_field->state(); } void PopplerFormFieldButton::setState( bool state ) { m_field->setState( state ); } QList< int > PopplerFormFieldButton::siblings() const { return m_field->siblings(); } PopplerFormFieldText::PopplerFormFieldText( Poppler::FormFieldText * field ) : Okular::FormFieldText(), m_field( field ) { m_rect = Okular::NormalizedRect::fromQRectF( m_field->rect() ); m_id = m_field->id(); SET_ACTIONS } PopplerFormFieldText::~PopplerFormFieldText() { delete m_field; } Okular::NormalizedRect PopplerFormFieldText::rect() const { return m_rect; } int PopplerFormFieldText::id() const { return m_id; } QString PopplerFormFieldText::name() const { return m_field->name(); } QString PopplerFormFieldText::uiName() const { return m_field->uiName(); } bool PopplerFormFieldText::isReadOnly() const { return m_field->isReadOnly(); } +void PopplerFormFieldText::setReadOnly( bool value ) +{ +#ifdef HAVE_POPPLER_0_64 + m_field->setReadOnly( value ); +#else + Q_UNUSED( value ); +#endif +} + bool PopplerFormFieldText::isVisible() const { return m_field->isVisible(); } Okular::FormFieldText::TextType PopplerFormFieldText::textType() const { switch ( m_field->textType() ) { case Poppler::FormFieldText::Normal: return Okular::FormFieldText::Normal; case Poppler::FormFieldText::Multiline: return Okular::FormFieldText::Multiline; case Poppler::FormFieldText::FileSelect: return Okular::FormFieldText::FileSelect; } return Okular::FormFieldText::Normal; } QString PopplerFormFieldText::text() const { return m_field->text(); } void PopplerFormFieldText::setText( const QString& text ) { m_field->setText( text ); } bool PopplerFormFieldText::isPassword() const { return m_field->isPassword(); } bool PopplerFormFieldText::isRichText() const { return m_field->isRichText(); } int PopplerFormFieldText::maximumLength() const { return m_field->maximumLength(); } Qt::Alignment PopplerFormFieldText::textAlignment() const { return Qt::AlignTop | m_field->textAlignment(); } bool PopplerFormFieldText::canBeSpellChecked() const { return m_field->canBeSpellChecked(); } PopplerFormFieldChoice::PopplerFormFieldChoice( Poppler::FormFieldChoice * field ) : Okular::FormFieldChoice(), m_field( field ) { m_rect = Okular::NormalizedRect::fromQRectF( m_field->rect() ); m_id = m_field->id(); SET_ACTIONS } PopplerFormFieldChoice::~PopplerFormFieldChoice() { delete m_field; } Okular::NormalizedRect PopplerFormFieldChoice::rect() const { return m_rect; } int PopplerFormFieldChoice::id() const { return m_id; } QString PopplerFormFieldChoice::name() const { return m_field->name(); } QString PopplerFormFieldChoice::uiName() const { return m_field->uiName(); } bool PopplerFormFieldChoice::isReadOnly() const { return m_field->isReadOnly(); } +void PopplerFormFieldChoice::setReadOnly( bool value ) +{ +#ifdef HAVE_POPPLER_0_64 + m_field->setReadOnly( value ); +#else + Q_UNUSED( value ); +#endif +} + bool PopplerFormFieldChoice::isVisible() const { return m_field->isVisible(); } Okular::FormFieldChoice::ChoiceType PopplerFormFieldChoice::choiceType() const { switch ( m_field->choiceType() ) { case Poppler::FormFieldChoice::ComboBox: return Okular::FormFieldChoice::ComboBox; case Poppler::FormFieldChoice::ListBox: return Okular::FormFieldChoice::ListBox; } return Okular::FormFieldChoice::ListBox; } QStringList PopplerFormFieldChoice::choices() const { return m_field->choices(); } bool PopplerFormFieldChoice::isEditable() const { return m_field->isEditable(); } bool PopplerFormFieldChoice::multiSelect() const { return m_field->multiSelect(); } QList PopplerFormFieldChoice::currentChoices() const { return m_field->currentChoices(); } void PopplerFormFieldChoice::setCurrentChoices( const QList& choices ) { m_field->setCurrentChoices( choices ); } QString PopplerFormFieldChoice::editChoice() const { return m_field->editChoice(); } void PopplerFormFieldChoice::setEditChoice( const QString& text ) { m_field->setEditChoice( text ); } Qt::Alignment PopplerFormFieldChoice::textAlignment() const { return Qt::AlignTop | m_field->textAlignment(); } bool PopplerFormFieldChoice::canBeSpellChecked() const { return m_field->canBeSpellChecked(); } diff --git a/generators/poppler/formfields.h b/generators/poppler/formfields.h index c5dfcf5fc..eff478e26 100644 --- a/generators/poppler/formfields.h +++ b/generators/poppler/formfields.h @@ -1,108 +1,111 @@ /*************************************************************************** * Copyright (C) 2007 by Pino Toscano * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_GENERATOR_PDF_FORMFIELDS_H_ #define _OKULAR_GENERATOR_PDF_FORMFIELDS_H_ #include #include "core/form.h" class PopplerFormFieldButton : public Okular::FormFieldButton { public: PopplerFormFieldButton( Poppler::FormFieldButton * field ); virtual ~PopplerFormFieldButton(); // inherited from Okular::FormField Okular::NormalizedRect rect() const override; int id() const override; QString name() const override; QString uiName() const override; bool isReadOnly() const override; + void setReadOnly( bool value ) override; bool isVisible() const override; // inherited from Okular::FormFieldButton ButtonType buttonType() const override; QString caption() const override; bool state() const override; void setState( bool state ) override; QList< int > siblings() const override; private: Poppler::FormFieldButton * m_field; Okular::NormalizedRect m_rect; int m_id; }; class PopplerFormFieldText : public Okular::FormFieldText { public: PopplerFormFieldText( Poppler::FormFieldText * field ); virtual ~PopplerFormFieldText(); // inherited from Okular::FormField Okular::NormalizedRect rect() const override; int id() const override; QString name() const override; QString uiName() const override; bool isReadOnly() const override; + void setReadOnly( bool value ) override; bool isVisible() const override; // inherited from Okular::FormFieldText Okular::FormFieldText::TextType textType() const override; QString text() const override; void setText( const QString& text ) override; bool isPassword() const override; bool isRichText() const override; int maximumLength() const override; Qt::Alignment textAlignment() const override; bool canBeSpellChecked() const override; private: Poppler::FormFieldText * m_field; Okular::NormalizedRect m_rect; int m_id; }; class PopplerFormFieldChoice : public Okular::FormFieldChoice { public: PopplerFormFieldChoice( Poppler::FormFieldChoice * field ); virtual ~PopplerFormFieldChoice(); // inherited from Okular::FormField Okular::NormalizedRect rect() const override; int id() const override; QString name() const override; QString uiName() const override; bool isReadOnly() const override; + void setReadOnly( bool value ) override; bool isVisible() const override; // inherited from Okular::FormFieldChoice ChoiceType choiceType() const override; QStringList choices() const override; bool isEditable() const override; bool multiSelect() const override; QList currentChoices() const override; void setCurrentChoices( const QList& choices ) override; QString editChoice() const override; void setEditChoice( const QString& text ) override; Qt::Alignment textAlignment() const override; bool canBeSpellChecked() const override; private: Poppler::FormFieldChoice * m_field; Okular::NormalizedRect m_rect; int m_id; }; #endif diff --git a/ui/formwidgets.cpp b/ui/formwidgets.cpp index d39bd2699..68f459571 100644 --- a/ui/formwidgets.cpp +++ b/ui/formwidgets.cpp @@ -1,1011 +1,1054 @@ /*************************************************************************** * Copyright (C) 2007 by Pino Toscano * * 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 "formwidgets.h" #include "pageviewutils.h" #include #include #include #include #include #include #include #include #include // local includes #include "core/form.h" #include "core/document.h" #include "debug_ui.h" FormWidgetsController::FormWidgetsController( Okular::Document *doc ) : QObject( doc ), m_doc( doc ) { // emit changed signal when a form has changed connect( this, &FormWidgetsController::formTextChangedByUndoRedo, this, &FormWidgetsController::changed ); connect( this, &FormWidgetsController::formListChangedByUndoRedo, this, &FormWidgetsController::changed ); connect( this, &FormWidgetsController::formComboChangedByUndoRedo, this, &FormWidgetsController::changed ); // connect form modification signals to and from document connect( this, &FormWidgetsController::formTextChangedByWidget, doc, &Okular::Document::editFormText ); connect( doc, &Okular::Document::formTextChangedByUndoRedo, this, &FormWidgetsController::formTextChangedByUndoRedo ); connect( this, &FormWidgetsController::formListChangedByWidget, doc, &Okular::Document::editFormList ); connect( doc, &Okular::Document::formListChangedByUndoRedo, this, &FormWidgetsController::formListChangedByUndoRedo ); connect( this, &FormWidgetsController::formComboChangedByWidget, doc, &Okular::Document::editFormCombo ); connect( doc, &Okular::Document::formComboChangedByUndoRedo, this, &FormWidgetsController::formComboChangedByUndoRedo ); connect( this, &FormWidgetsController::formButtonsChangedByWidget, doc, &Okular::Document::editFormButtons ); connect( doc, &Okular::Document::formButtonsChangedByUndoRedo, this, &FormWidgetsController::slotFormButtonsChangedByUndoRedo ); // Connect undo/redo signals connect( this, &FormWidgetsController::requestUndo, doc, &Okular::Document::undo ); connect( this, &FormWidgetsController::requestRedo, doc, &Okular::Document::redo ); connect( doc, &Okular::Document::canUndoChanged, this, &FormWidgetsController::canUndoChanged ); connect( doc, &Okular::Document::canRedoChanged, this, &FormWidgetsController::canRedoChanged ); // Connect the generic formWidget refresh signal connect( doc, &Okular::Document::refreshFormWidget, this, &FormWidgetsController::refreshFormWidget ); } FormWidgetsController::~FormWidgetsController() { } void FormWidgetsController::signalAction( Okular::Action *a ) { emit action( a ); } void FormWidgetsController::registerRadioButton( FormWidgetIface *fwButton, Okular::FormFieldButton *formButton ) { if ( !fwButton ) return; QAbstractButton *button = dynamic_cast(fwButton); if ( !button ) { qWarning() << "fwButton is not a QAbstractButton" << fwButton; return; } QList< RadioData >::iterator it = m_radios.begin(), itEnd = m_radios.end(); const int id = formButton->id(); m_buttons.insert( id, button ); for ( ; it != itEnd; ++it ) { const QList< int >::const_iterator idsIt = qFind( (*it).ids, id ); if ( idsIt != (*it).ids.constEnd() ) { qCDebug(OkularUiDebug) << "Adding id" << id << "To group including" << (*it).ids; (*it).group->addButton( button ); (*it).group->setId( button, id ); return; } } const QList< int > siblings = formButton->siblings(); RadioData newdata; newdata.ids = siblings; newdata.ids.append( id ); newdata.group = new QButtonGroup(); newdata.group->addButton( button ); newdata.group->setId( button, id ); // Groups of 1 (like checkboxes) can't be exclusive if (siblings.isEmpty()) newdata.group->setExclusive( false ); connect( newdata.group, SIGNAL( buttonClicked(QAbstractButton* ) ), this, SLOT( slotButtonClicked( QAbstractButton* ) ) ); m_radios.append( newdata ); } void FormWidgetsController::dropRadioButtons() { QList< RadioData >::iterator it = m_radios.begin(), itEnd = m_radios.end(); for ( ; it != itEnd; ++it ) { delete (*it).group; } m_radios.clear(); m_buttons.clear(); } bool FormWidgetsController::canUndo() { return m_doc->canUndo(); } bool FormWidgetsController::canRedo() { return m_doc->canRedo(); } void FormWidgetsController::slotButtonClicked( QAbstractButton *button ) { int pageNumber = -1; - if ( CheckBoxEdit *check = qobject_cast< CheckBoxEdit * >( button ) ) + CheckBoxEdit *check = qobject_cast< CheckBoxEdit * >( button ); + if ( check ) { // Checkboxes need to be uncheckable so if clicking a checked one // disable the exclusive status temporarily and uncheck it Okular::FormFieldButton *formButton = static_cast( check->formField() ); if ( formButton->state() ) { const bool wasExclusive = button->group()->exclusive(); button->group()->setExclusive(false); check->setChecked(false); button->group()->setExclusive(wasExclusive); } pageNumber = check->pageItem()->pageNumber(); } else if ( RadioButtonEdit *radio = qobject_cast< RadioButtonEdit * >( button ) ) { pageNumber = radio->pageItem()->pageNumber(); } const QList< QAbstractButton* > buttons = button->group()->buttons(); QList< bool > checked; QList< bool > prevChecked; QList< Okular::FormFieldButton*> formButtons; foreach ( QAbstractButton* button, buttons ) { checked.append( button->isChecked() ); Okular::FormFieldButton *formButton = static_cast( dynamic_cast(button)->formField() ); formButtons.append( formButton ); prevChecked.append( formButton->state() ); } if (checked != prevChecked) emit formButtonsChangedByWidget( pageNumber, formButtons, checked ); + if ( check ) + { + // The formButtonsChangedByWidget signal changes the value of the underlying + // Okular::FormField of the checkbox. We need to execute the activiation + // action after this. + check->doActivateAction(); + } } void FormWidgetsController::slotFormButtonsChangedByUndoRedo( int pageNumber, const QList< Okular::FormFieldButton* > & formButtons) { foreach ( Okular::FormFieldButton* formButton, formButtons ) { int id = formButton->id(); QAbstractButton* button = m_buttons[id]; + CheckBoxEdit *check = qobject_cast< CheckBoxEdit * >( button ); + if ( check ) + { + emit refreshFormWidget( check->formField() ); + } // temporarily disable exclusiveness of the button group // since it breaks doing/redoing steps into which all the checkboxes // are unchecked const bool wasExclusive = button->group()->exclusive(); button->group()->setExclusive(false); bool checked = formButton->state(); button->setChecked( checked ); button->group()->setExclusive(wasExclusive); button->setFocus(); } emit changed( pageNumber ); } FormWidgetIface * FormWidgetFactory::createWidget( Okular::FormField * ff, QWidget * parent ) { FormWidgetIface * widget = nullptr; - if (ff->isReadOnly()) - return nullptr; - switch ( ff->type() ) { case Okular::FormField::FormButton: { Okular::FormFieldButton * ffb = static_cast< Okular::FormFieldButton * >( ff ); switch ( ffb->buttonType() ) { case Okular::FormFieldButton::Push: widget = new PushButtonEdit( ffb, parent ); break; case Okular::FormFieldButton::CheckBox: widget = new CheckBoxEdit( ffb, parent ); break; case Okular::FormFieldButton::Radio: widget = new RadioButtonEdit( ffb, parent ); break; default: ; } break; } case Okular::FormField::FormText: { Okular::FormFieldText * fft = static_cast< Okular::FormFieldText * >( ff ); switch ( fft->textType() ) { case Okular::FormFieldText::Multiline: widget = new TextAreaEdit( fft, parent ); break; case Okular::FormFieldText::Normal: widget = new FormLineEdit( fft, parent ); break; case Okular::FormFieldText::FileSelect: widget = new FileEdit( fft, parent ); break; } break; } case Okular::FormField::FormChoice: { Okular::FormFieldChoice * ffc = static_cast< Okular::FormFieldChoice * >( ff ); switch ( ffc->choiceType() ) { case Okular::FormFieldChoice::ListBox: widget = new ListEdit( ffc, parent ); break; case Okular::FormFieldChoice::ComboBox: widget = new ComboEdit( ffc, parent ); break; } break; } default: ; } + + if ( ff->isReadOnly() ) + widget->setVisibility( false ); + return widget; } -FormWidgetIface::FormWidgetIface( QWidget * w, Okular::FormField * ff, bool canBeEnabled ) - : m_controller( nullptr ), m_ff( ff ), m_widget( w ), m_pageItem( nullptr ), - m_canBeEnabled( canBeEnabled ) +FormWidgetIface::FormWidgetIface( QWidget * w, Okular::FormField * ff ) + : m_controller( nullptr ), m_ff( ff ), m_widget( w ), m_pageItem( nullptr ) { - m_widget->setEnabled( m_canBeEnabled ); } FormWidgetIface::~FormWidgetIface() { } Okular::NormalizedRect FormWidgetIface::rect() const { return m_ff->rect(); } void FormWidgetIface::setWidthHeight( int w, int h ) { m_widget->resize( w, h ); } void FormWidgetIface::moveTo( int x, int y ) { m_widget->move( x, y ); } bool FormWidgetIface::setVisibility( bool visible ) { bool hadfocus = m_widget->hasFocus(); if ( hadfocus ) m_widget->clearFocus(); m_widget->setVisible( visible ); return hadfocus; } void FormWidgetIface::setCanBeFilled( bool fill ) { - m_widget->setEnabled( fill && m_canBeEnabled ); + m_widget->setEnabled( fill ); } void FormWidgetIface::setPageItem( PageViewItem *pageItem ) { m_pageItem = pageItem; } void FormWidgetIface::setFormField( Okular::FormField *field ) { m_ff = field; } Okular::FormField* FormWidgetIface::formField() const { return m_ff; } PageViewItem* FormWidgetIface::pageItem() const { return m_pageItem; } void FormWidgetIface::setFormWidgetsController( FormWidgetsController *controller ) { m_controller = controller; + QObject *obj = dynamic_cast< QObject * > ( this ); + QObject::connect( m_controller, &FormWidgetsController::refreshFormWidget, obj, + [this] ( Okular::FormField *form ) { + slotRefresh ( form ); + }); +} + +void FormWidgetIface::slotRefresh( Okular::FormField * form ) +{ + if ( m_ff != form ) + { + return; + } + setVisibility( form->isVisible() && !form->isReadOnly() ); + + m_widget->setEnabled( !form->isReadOnly() ); } PushButtonEdit::PushButtonEdit( Okular::FormFieldButton * button, QWidget * parent ) - : QPushButton( parent ), FormWidgetIface( this, button, !button->isReadOnly() ) + : QPushButton( parent ), FormWidgetIface( this, button ) { setText( button->caption() ); setVisible( button->isVisible() ); setCursor( Qt::ArrowCursor ); connect( this, &QAbstractButton::clicked, this, &PushButtonEdit::slotClicked ); } void PushButtonEdit::slotClicked() { if ( m_ff->activationAction() ) m_controller->signalAction( m_ff->activationAction() ); } CheckBoxEdit::CheckBoxEdit( Okular::FormFieldButton * button, QWidget * parent ) - : QCheckBox( parent ), FormWidgetIface( this, button, !button->isReadOnly() ) + : QCheckBox( parent ), FormWidgetIface( this, button ) { setText( button->caption() ); setVisible( button->isVisible() ); setCursor( Qt::ArrowCursor ); } void CheckBoxEdit::setFormWidgetsController( FormWidgetsController *controller ) { Okular::FormFieldButton *form = static_cast(m_ff); FormWidgetIface::setFormWidgetsController( controller ); m_controller->registerRadioButton( this, form ); setChecked( form->state() ); - connect( this, &QCheckBox::stateChanged, this, &CheckBoxEdit::slotStateChanged ); } -void CheckBoxEdit::slotStateChanged( int state ) +void CheckBoxEdit::doActivateAction() { Okular::FormFieldButton *form = static_cast(m_ff); - if ( state == Qt::Checked && form->activationAction() ) + if ( form->activationAction() ) m_controller->signalAction( form->activationAction() ); } +void CheckBoxEdit::slotRefresh( Okular::FormField * form ) +{ + if ( form != m_ff ) + { + return; + } + FormWidgetIface::slotRefresh( form ); + + Okular::FormFieldButton *button = static_cast(m_ff); + bool oldState = isChecked(); + bool newState = button->state(); + if ( oldState != newState ) + { + setChecked( button->state() ); + doActivateAction(); + } +} + RadioButtonEdit::RadioButtonEdit( Okular::FormFieldButton * button, QWidget * parent ) - : QRadioButton( parent ), FormWidgetIface( this, button, !button->isReadOnly() ) + : QRadioButton( parent ), FormWidgetIface( this, button ) { setText( button->caption() ); setVisible( button->isVisible() ); setCursor( Qt::ArrowCursor ); } void RadioButtonEdit::setFormWidgetsController( FormWidgetsController *controller ) { Okular::FormFieldButton *form = static_cast(m_ff); FormWidgetIface::setFormWidgetsController( controller ); m_controller->registerRadioButton( this, form ); setChecked( form->state() ); } FormLineEdit::FormLineEdit( Okular::FormFieldText * text, QWidget * parent ) - : QLineEdit( parent ), FormWidgetIface( this, text, true ) + : QLineEdit( parent ), FormWidgetIface( this, text ) { int maxlen = text->maximumLength(); if ( maxlen >= 0 ) setMaxLength( maxlen ); setAlignment( text->textAlignment() ); setText( text->text() ); if ( text->isPassword() ) setEchoMode( QLineEdit::Password ); m_prevCursorPos = cursorPosition(); m_prevAnchorPos = cursorPosition(); connect( this, &QLineEdit::textEdited, this, &FormLineEdit::slotChanged ); connect( this, &QLineEdit::cursorPositionChanged, this, &FormLineEdit::slotChanged ); setVisible( text->isVisible() ); } void FormLineEdit::setFormWidgetsController(FormWidgetsController* controller) { FormWidgetIface::setFormWidgetsController(controller); connect( m_controller, &FormWidgetsController::formTextChangedByUndoRedo, this, &FormLineEdit::slotHandleTextChangedByUndoRedo ); - connect( m_controller, &FormWidgetsController::refreshFormWidget, - this, &FormLineEdit::slotRefresh ); } bool FormLineEdit::event( QEvent* e ) { if ( e->type() == QEvent::KeyPress ) { QKeyEvent *keyEvent = static_cast< QKeyEvent* >( e ); if ( keyEvent == QKeySequence::Undo ) { emit m_controller->requestUndo(); return true; } else if ( keyEvent == QKeySequence::Redo ) { emit m_controller->requestRedo(); return true; } } return QLineEdit::event( e ); } void FormLineEdit::contextMenuEvent( QContextMenuEvent* event ) { QMenu *menu = createStandardContextMenu(); QList actionList = menu->actions(); enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, DeleteAct, SelectAllAct }; QAction *kundo = KStandardAction::create( KStandardAction::Undo, m_controller, SIGNAL( requestUndo() ), menu ); QAction *kredo = KStandardAction::create( KStandardAction::Redo, m_controller, SIGNAL( requestRedo() ), menu ); connect( m_controller, &FormWidgetsController::canUndoChanged, kundo, &QAction::setEnabled ); connect( m_controller, &FormWidgetsController::canRedoChanged, kredo, &QAction::setEnabled ); kundo->setEnabled( m_controller->canUndo() ); kredo->setEnabled( m_controller->canRedo() ); QAction *oldUndo, *oldRedo; oldUndo = actionList[UndoAct]; oldRedo = actionList[RedoAct]; menu->insertAction( oldUndo, kundo ); menu->insertAction( oldRedo, kredo ); menu->removeAction( oldUndo ); menu->removeAction( oldRedo ); menu->exec( event->globalPos() ); delete menu; } void FormLineEdit::slotChanged() { Okular::FormFieldText *form = static_cast(m_ff); QString contents = text(); int cursorPos = cursorPosition(); if ( contents != form->text() ) { m_controller->formTextChangedByWidget( pageItem()->pageNumber(), form, contents, cursorPos, m_prevCursorPos, m_prevAnchorPos ); } m_prevCursorPos = cursorPos; m_prevAnchorPos = cursorPos; if ( hasSelectedText() ) { if ( cursorPos == selectionStart() ) { m_prevAnchorPos = selectionStart() + selectedText().size(); } else { m_prevAnchorPos = selectionStart(); } } } void FormLineEdit::slotHandleTextChangedByUndoRedo( int pageNumber, Okular::FormFieldText* textForm, const QString & contents, int cursorPos, int anchorPos ) { Q_UNUSED(pageNumber); if ( textForm != m_ff || contents == text() ) { return; } disconnect( this, &QLineEdit::cursorPositionChanged, this, &FormLineEdit::slotChanged ); setText(contents); setCursorPosition(anchorPos); cursorForward( true, cursorPos - anchorPos ); connect( this, &QLineEdit::cursorPositionChanged, this, &FormLineEdit::slotChanged ); m_prevCursorPos = cursorPos; m_prevAnchorPos = anchorPos; setFocus(); } void FormLineEdit::slotRefresh( Okular::FormField *form ) { if (form != m_ff) { return; } - Okular::FormFieldText *text = static_cast ( form ); + FormWidgetIface::slotRefresh( form ); + Okular::FormFieldText *text = static_cast ( form ); setText( text->text() ); } TextAreaEdit::TextAreaEdit( Okular::FormFieldText * text, QWidget * parent ) -: KTextEdit( parent ), FormWidgetIface( this, text, true ) +: KTextEdit( parent ), FormWidgetIface( this, text ) { setAcceptRichText( text->isRichText() ); setCheckSpellingEnabled( text->canBeSpellChecked() ); setAlignment( text->textAlignment() ); setPlainText( text->text() ); setUndoRedoEnabled( false ); connect( this, &QTextEdit::textChanged, this, &TextAreaEdit::slotChanged ); connect( this, &QTextEdit::cursorPositionChanged, this, &TextAreaEdit::slotChanged ); connect( this, &KTextEdit::aboutToShowContextMenu, this, &TextAreaEdit::slotUpdateUndoAndRedoInContextMenu ); m_prevCursorPos = textCursor().position(); m_prevAnchorPos = textCursor().anchor(); setVisible( text->isVisible() ); } bool TextAreaEdit::event( QEvent* e ) { if ( e->type() == QEvent::KeyPress ) { QKeyEvent *keyEvent = static_cast< QKeyEvent* >(e); if ( keyEvent == QKeySequence::Undo ) { emit m_controller->requestUndo(); return true; } else if ( keyEvent == QKeySequence::Redo ) { emit m_controller->requestRedo(); return true; } } return KTextEdit::event( e ); } void TextAreaEdit::slotUpdateUndoAndRedoInContextMenu( QMenu* menu ) { if ( !menu ) return; QList actionList = menu->actions(); enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, ClearAct, SelectAllAct, NCountActs }; QAction *kundo = KStandardAction::create( KStandardAction::Undo, m_controller, SIGNAL( requestUndo() ), menu ); QAction *kredo = KStandardAction::create( KStandardAction::Redo, m_controller, SIGNAL( requestRedo() ), menu ); connect(m_controller, &FormWidgetsController::canUndoChanged, kundo, &QAction::setEnabled ); connect(m_controller, &FormWidgetsController::canRedoChanged, kredo, &QAction::setEnabled ); kundo->setEnabled( m_controller->canUndo() ); kredo->setEnabled( m_controller->canRedo() ); QAction *oldUndo, *oldRedo; oldUndo = actionList[UndoAct]; oldRedo = actionList[RedoAct]; menu->insertAction( oldUndo, kundo ); menu->insertAction( oldRedo, kredo ); menu->removeAction( oldUndo ); menu->removeAction( oldRedo ); } void TextAreaEdit::setFormWidgetsController( FormWidgetsController* controller ) { FormWidgetIface::setFormWidgetsController( controller ); connect( m_controller, &FormWidgetsController::formTextChangedByUndoRedo, this, &TextAreaEdit::slotHandleTextChangedByUndoRedo ); - connect( m_controller, &FormWidgetsController::refreshFormWidget, - this, &TextAreaEdit::slotRefresh ); } void TextAreaEdit::slotHandleTextChangedByUndoRedo( int pageNumber, Okular::FormFieldText* textForm, const QString & contents, int cursorPos, int anchorPos ) { Q_UNUSED(pageNumber); if ( textForm != m_ff ) { return; } setPlainText( contents ); QTextCursor c = textCursor(); c.setPosition( anchorPos ); c.setPosition( cursorPos,QTextCursor::KeepAnchor ); m_prevCursorPos = cursorPos; m_prevAnchorPos = anchorPos; setTextCursor( c ); setFocus(); } void TextAreaEdit::slotChanged() { Okular::FormFieldText *form = static_cast(m_ff); QString contents = toPlainText(); int cursorPos = textCursor().position(); if (contents != form->text()) { m_controller->formTextChangedByWidget( pageItem()->pageNumber(), form, contents, cursorPos, m_prevCursorPos, m_prevAnchorPos ); } m_prevCursorPos = cursorPos; m_prevAnchorPos = textCursor().anchor(); } void TextAreaEdit::slotRefresh( Okular::FormField *form ) { if (form != m_ff) { return; } - Okular::FormFieldText *text = static_cast ( form ); + FormWidgetIface::slotRefresh( form ); + Okular::FormFieldText *text = static_cast ( form ); setPlainText( text->text() ); } FileEdit::FileEdit( Okular::FormFieldText * text, QWidget * parent ) - : KUrlRequester( parent ), FormWidgetIface( this, text, !text->isReadOnly() ) + : KUrlRequester( parent ), FormWidgetIface( this, text ) { setMode( KFile::File | KFile::ExistingOnly | KFile::LocalOnly ); setFilter( i18n( "*|All Files" ) ); setUrl( QUrl::fromUserInput( text->text() ) ); lineEdit()->setAlignment( text->textAlignment() ); m_prevCursorPos = lineEdit()->cursorPosition(); m_prevAnchorPos = lineEdit()->cursorPosition(); connect( this, &KUrlRequester::textChanged, this, &FileEdit::slotChanged ); connect( lineEdit(), &QLineEdit::cursorPositionChanged, this, &FileEdit::slotChanged ); setVisible( text->isVisible() ); } void FileEdit::setFormWidgetsController( FormWidgetsController* controller ) { FormWidgetIface::setFormWidgetsController( controller ); connect( m_controller, &FormWidgetsController::formTextChangedByUndoRedo, this, &FileEdit::slotHandleFileChangedByUndoRedo ); } bool FileEdit::eventFilter( QObject* obj, QEvent* event ) { if ( obj == lineEdit() ) { if ( event->type() == QEvent::KeyPress ) { QKeyEvent *keyEvent = static_cast< QKeyEvent* >( event ); if ( keyEvent == QKeySequence::Undo ) { emit m_controller->requestUndo(); return true; } else if ( keyEvent == QKeySequence::Redo ) { emit m_controller->requestRedo(); return true; } } else if( event->type() == QEvent::ContextMenu ) { QContextMenuEvent *contextMenuEvent = static_cast< QContextMenuEvent* >( event ); QMenu *menu = ( (QLineEdit*) lineEdit() )->createStandardContextMenu(); QList< QAction* > actionList = menu->actions(); enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, DeleteAct, SelectAllAct }; QAction *kundo = KStandardAction::create( KStandardAction::Undo, m_controller, SIGNAL( requestUndo() ), menu ); QAction *kredo = KStandardAction::create( KStandardAction::Redo, m_controller, SIGNAL( requestRedo() ), menu ); connect(m_controller, &FormWidgetsController::canUndoChanged, kundo, &QAction::setEnabled ); connect(m_controller, &FormWidgetsController::canRedoChanged, kredo, &QAction::setEnabled ); kundo->setEnabled( m_controller->canUndo() ); kredo->setEnabled( m_controller->canRedo() ); QAction *oldUndo, *oldRedo; oldUndo = actionList[UndoAct]; oldRedo = actionList[RedoAct]; menu->insertAction( oldUndo, kundo ); menu->insertAction( oldRedo, kredo ); menu->removeAction( oldUndo ); menu->removeAction( oldRedo ); menu->exec( contextMenuEvent->globalPos() ); delete menu; return true; } } return KUrlRequester::eventFilter( obj, event ); } void FileEdit::slotChanged() { // Make sure line edit's text matches url expansion if ( text() != url().toLocalFile() ) this->setText( url().toLocalFile() ); Okular::FormFieldText *form = static_cast(m_ff); QString contents = text(); int cursorPos = lineEdit()->cursorPosition(); if (contents != form->text()) { m_controller->formTextChangedByWidget( pageItem()->pageNumber(), form, contents, cursorPos, m_prevCursorPos, m_prevAnchorPos ); } m_prevCursorPos = cursorPos; m_prevAnchorPos = cursorPos; if ( lineEdit()->hasSelectedText() ) { if ( cursorPos == lineEdit()->selectionStart() ) { m_prevAnchorPos = lineEdit()->selectionStart() + lineEdit()->selectedText().size(); } else { m_prevAnchorPos = lineEdit()->selectionStart(); } } } void FileEdit::slotHandleFileChangedByUndoRedo( int pageNumber, Okular::FormFieldText* form, const QString & contents, int cursorPos, int anchorPos ) { Q_UNUSED(pageNumber); if ( form != m_ff || contents == text() ) { return; } disconnect( this, SIGNAL( cursorPositionChanged( int, int ) ), this, SLOT( slotChanged() ) ); setText( contents ); lineEdit()->setCursorPosition( anchorPos ); lineEdit()->cursorForward( true, cursorPos - anchorPos ); connect( this, SIGNAL(cursorPositionChanged( int, int ) ), this, SLOT( slotChanged() ) ); m_prevCursorPos = cursorPos; m_prevAnchorPos = anchorPos; setFocus(); } ListEdit::ListEdit( Okular::FormFieldChoice * choice, QWidget * parent ) - : QListWidget( parent ), FormWidgetIface( this, choice, !choice->isReadOnly() ) + : QListWidget( parent ), FormWidgetIface( this, choice ) { addItems( choice->choices() ); setSelectionMode( choice->multiSelect() ? QAbstractItemView::ExtendedSelection : QAbstractItemView::SingleSelection ); setVerticalScrollMode( QAbstractItemView::ScrollPerPixel ); QList< int > selectedItems = choice->currentChoices(); if ( choice->multiSelect() ) { foreach ( int index, selectedItems ) if ( index >= 0 && index < count() ) item( index )->setSelected( true ); } else { if ( selectedItems.count() == 1 && selectedItems.at(0) >= 0 && selectedItems.at(0) < count() ) { setCurrentRow( selectedItems.at(0) ); scrollToItem( item( selectedItems.at(0) ) ); } } connect( this, &QListWidget::itemSelectionChanged, this, &ListEdit::slotSelectionChanged ); setVisible( choice->isVisible() ); setCursor( Qt::ArrowCursor ); } void ListEdit::setFormWidgetsController( FormWidgetsController* controller ) { FormWidgetIface::setFormWidgetsController( controller ); connect( m_controller, &FormWidgetsController::formListChangedByUndoRedo, this, &ListEdit::slotHandleFormListChangedByUndoRedo ); } void ListEdit::slotSelectionChanged() { QList< QListWidgetItem * > selection = selectedItems(); QList< int > rows; foreach( const QListWidgetItem * item, selection ) rows.append( row( item ) ); Okular::FormFieldChoice *form = static_cast(m_ff); if ( rows != form->currentChoices() ) { m_controller->formListChangedByWidget( pageItem()->pageNumber(), form, rows ); } } void ListEdit::slotHandleFormListChangedByUndoRedo( int pageNumber, Okular::FormFieldChoice* listForm, const QList< int > & choices ) { Q_UNUSED(pageNumber); if ( m_ff != listForm ) { return; } disconnect( this, &QListWidget::itemSelectionChanged, this, &ListEdit::slotSelectionChanged ); for(int i=0; i < count(); i++) { item( i )->setSelected( choices.contains(i) ); } connect( this, &QListWidget::itemSelectionChanged, this, &ListEdit::slotSelectionChanged ); setFocus(); } ComboEdit::ComboEdit( Okular::FormFieldChoice * choice, QWidget * parent ) - : QComboBox( parent ), FormWidgetIface( this, choice, !choice->isReadOnly() ) + : QComboBox( parent ), FormWidgetIface( this, choice ) { addItems( choice->choices() ); setEditable( true ); setInsertPolicy( NoInsert ); lineEdit()->setReadOnly( !choice->isEditable() ); QList< int > selectedItems = choice->currentChoices(); if ( selectedItems.count() == 1 && selectedItems.at(0) >= 0 && selectedItems.at(0) < count() ) setCurrentIndex( selectedItems.at(0) ); if ( choice->isEditable() && !choice->editChoice().isEmpty() ) lineEdit()->setText( choice->editChoice() ); connect( this, SIGNAL(currentIndexChanged(int)), this, SLOT(slotValueChanged()) ); connect( this, &QComboBox::editTextChanged, this, &ComboEdit::slotValueChanged ); connect( lineEdit(), &QLineEdit::cursorPositionChanged, this, &ComboEdit::slotValueChanged ); setVisible( choice->isVisible() ); setCursor( Qt::ArrowCursor ); m_prevCursorPos = lineEdit()->cursorPosition(); m_prevAnchorPos = lineEdit()->cursorPosition(); } void ComboEdit::setFormWidgetsController(FormWidgetsController* controller) { FormWidgetIface::setFormWidgetsController(controller); connect( m_controller, &FormWidgetsController::formComboChangedByUndoRedo, this, &ComboEdit::slotHandleFormComboChangedByUndoRedo); } void ComboEdit::slotValueChanged() { const QString text = lineEdit()->text(); Okular::FormFieldChoice *form = static_cast(m_ff); QString prevText; if ( form->currentChoices().isEmpty() ) { prevText = form->editChoice(); } else { prevText = form->choices()[form->currentChoices()[0]]; } int cursorPos = lineEdit()->cursorPosition(); if ( text != prevText ) { m_controller->formComboChangedByWidget( pageItem()->pageNumber(), form, currentText(), cursorPos, m_prevCursorPos, m_prevAnchorPos ); } prevText = text; m_prevCursorPos = cursorPos; m_prevAnchorPos = cursorPos; if ( lineEdit()->hasSelectedText() ) { if ( cursorPos == lineEdit()->selectionStart() ) { m_prevAnchorPos = lineEdit()->selectionStart() + lineEdit()->selectedText().size(); } else { m_prevAnchorPos = lineEdit()->selectionStart(); } } } void ComboEdit::slotHandleFormComboChangedByUndoRedo( int pageNumber, Okular::FormFieldChoice* form, const QString & text, int cursorPos, int anchorPos ) { Q_UNUSED(pageNumber); if ( m_ff != form ) { return; } // Determine if text corrisponds to an index choices int index = -1; for ( int i = 0; i < count(); i++ ) { if ( itemText(i) == text ) { index = i; } } m_prevCursorPos = cursorPos; m_prevAnchorPos = anchorPos; disconnect( lineEdit(), &QLineEdit::cursorPositionChanged, this, &ComboEdit::slotValueChanged ); const bool isCustomValue = index == -1; if ( isCustomValue ) { setEditText( text ); } else { setCurrentIndex( index ); } lineEdit()->setCursorPosition( anchorPos ); lineEdit()->cursorForward( true, cursorPos - anchorPos ); connect( lineEdit(), &QLineEdit::cursorPositionChanged, this, &ComboEdit::slotValueChanged ); setFocus(); } void ComboEdit::contextMenuEvent( QContextMenuEvent* event ) { QMenu *menu = lineEdit()->createStandardContextMenu(); QList actionList = menu->actions(); enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, DeleteAct, SelectAllAct }; QAction *kundo = KStandardAction::create( KStandardAction::Undo, m_controller, SIGNAL( requestUndo() ), menu ); QAction *kredo = KStandardAction::create( KStandardAction::Redo, m_controller, SIGNAL( requestRedo() ), menu ); connect( m_controller, &FormWidgetsController::canUndoChanged, kundo, &QAction::setEnabled ); connect( m_controller, &FormWidgetsController::canRedoChanged, kredo, &QAction::setEnabled ); kundo->setEnabled( m_controller->canUndo() ); kredo->setEnabled( m_controller->canRedo() ); QAction *oldUndo, *oldRedo; oldUndo = actionList[UndoAct]; oldRedo = actionList[RedoAct]; menu->insertAction( oldUndo, kundo ); menu->insertAction( oldRedo, kredo ); menu->removeAction( oldUndo ); menu->removeAction( oldRedo ); menu->exec( event->globalPos() ); delete menu; } bool ComboEdit::event( QEvent* e ) { if ( e->type() == QEvent::KeyPress ) { QKeyEvent *keyEvent = static_cast< QKeyEvent* >(e); if ( keyEvent == QKeySequence::Undo ) { emit m_controller->requestUndo(); return true; } else if ( keyEvent == QKeySequence::Redo ) { emit m_controller->requestRedo(); return true; } } return QComboBox::event( e ); } #include "moc_formwidgets.cpp" diff --git a/ui/formwidgets.h b/ui/formwidgets.h index e0b1f03ae..22ea721af 100644 --- a/ui/formwidgets.h +++ b/ui/formwidgets.h @@ -1,328 +1,335 @@ /*************************************************************************** * Copyright (C) 2007 by Pino Toscano * * 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. * ***************************************************************************/ #ifndef _OKULAR_FORMWIDGETS_H_ #define _OKULAR_FORMWIDGETS_H_ #include "core/area.h" #include #include #include #include #include #include #include #include class ComboEdit; class QMenu; class QButtonGroup; class FormWidgetIface; class PageViewItem; class RadioButtonEdit; class QEvent; namespace Okular { class Action; class FormField; class FormFieldButton; class FormFieldChoice; class FormFieldText; class Document; } struct RadioData { RadioData() {} QList< int > ids; QButtonGroup *group; }; class FormWidgetsController : public QObject { Q_OBJECT public: FormWidgetsController( Okular::Document *doc ); virtual ~FormWidgetsController(); void signalAction( Okular::Action *action ); void registerRadioButton( FormWidgetIface *fwButton, Okular::FormFieldButton *formButton ); void dropRadioButtons(); bool canUndo(); bool canRedo(); Q_SIGNALS: void changed( int pageNumber ); void requestUndo(); void requestRedo(); void canUndoChanged( bool undoAvailable ); void canRedoChanged( bool redoAvailable); void formTextChangedByWidget( int pageNumber, Okular::FormFieldText *form, const QString & newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos ); void formTextChangedByUndoRedo( int pageNumber, Okular::FormFieldText *form, const QString & contents, int cursorPos, int anchorPos ); void formListChangedByWidget( int pageNumber, Okular::FormFieldChoice *form, const QList< int > & newChoices ); void formListChangedByUndoRedo( int pageNumber, Okular::FormFieldChoice *form, const QList< int > & choices ); void formComboChangedByWidget( int pageNumber, Okular::FormFieldChoice *form, const QString & newText, int newCursorPos, int prevCursorPos, int prevAnchorPos ); void formComboChangedByUndoRedo( int pageNumber, Okular::FormFieldChoice *form, const QString & text, int cursorPos, int anchorPos ); void formButtonsChangedByWidget( int pageNumber, const QList< Okular::FormFieldButton* > & formButtons, const QList< bool > & newButtonStates ); void action( Okular::Action *action ); void refreshFormWidget( Okular::FormField * form ); private Q_SLOTS: void slotButtonClicked( QAbstractButton *button ); void slotFormButtonsChangedByUndoRedo( int pageNumber, const QList< Okular::FormFieldButton* > & formButtons ); private: friend class TextAreaEdit; friend class FormLineEdit; friend class FileEdit; friend class ListEdit; friend class ComboEdit; QList< RadioData > m_radios; QHash< int, QAbstractButton* > m_buttons; Okular::Document* m_doc; }; class FormWidgetFactory { public: static FormWidgetIface * createWidget( Okular::FormField * ff, QWidget * parent = nullptr ); }; class FormWidgetIface { public: - FormWidgetIface( QWidget * w, Okular::FormField * ff, bool canBeEnabled ); + FormWidgetIface( QWidget * w, Okular::FormField * ff ); virtual ~FormWidgetIface(); Okular::NormalizedRect rect() const; void setWidthHeight( int w, int h ); void moveTo( int x, int y ); bool setVisibility( bool visible ); void setCanBeFilled( bool fill ); void setPageItem( PageViewItem *pageItem ); PageViewItem* pageItem() const; void setFormField( Okular::FormField *field ); Okular::FormField* formField() const; virtual void setFormWidgetsController( FormWidgetsController *controller ); protected: + virtual void slotRefresh( Okular::FormField *form ); + FormWidgetsController * m_controller; Okular::FormField * m_ff; private: QWidget * m_widget; PageViewItem * m_pageItem; - bool m_canBeEnabled; }; class PushButtonEdit : public QPushButton, public FormWidgetIface { Q_OBJECT public: explicit PushButtonEdit( Okular::FormFieldButton * button, QWidget * parent = nullptr ); private Q_SLOTS: void slotClicked(); }; class CheckBoxEdit : public QCheckBox, public FormWidgetIface { Q_OBJECT public: explicit CheckBoxEdit( Okular::FormFieldButton * button, QWidget * parent = nullptr ); // reimplemented from FormWidgetIface void setFormWidgetsController( FormWidgetsController *controller ) override; - private Q_SLOTS: - void slotStateChanged( int state ); + void doActivateAction(); + + protected: + void slotRefresh( Okular::FormField *form ) override; }; class RadioButtonEdit : public QRadioButton, public FormWidgetIface { Q_OBJECT public: explicit RadioButtonEdit( Okular::FormFieldButton * button, QWidget * parent = nullptr ); // reimplemented from FormWidgetIface void setFormWidgetsController( FormWidgetsController *controller ) override; }; class FormLineEdit : public QLineEdit, public FormWidgetIface { Q_OBJECT public: explicit FormLineEdit( Okular::FormFieldText * text, QWidget * parent = nullptr ); void setFormWidgetsController( FormWidgetsController *controller ) override; bool event ( QEvent * e ) override; void contextMenuEvent( QContextMenuEvent* event ) override; public Q_SLOTS: void slotHandleTextChangedByUndoRedo( int pageNumber, Okular::FormFieldText* textForm, const QString & contents, int cursorPos, int anchorPos ); private Q_SLOTS: void slotChanged(); - void slotRefresh(Okular::FormField* form); + + protected: + void slotRefresh( Okular::FormField* form ) override; private: int m_prevCursorPos; int m_prevAnchorPos; }; class TextAreaEdit : public KTextEdit, public FormWidgetIface { Q_OBJECT public: explicit TextAreaEdit( Okular::FormFieldText * text, QWidget * parent = nullptr ); void setFormWidgetsController( FormWidgetsController *controller ) override; bool event ( QEvent * e ) override; public Q_SLOTS: void slotHandleTextChangedByUndoRedo( int pageNumber, Okular::FormFieldText * textForm, const QString & contents, int cursorPos, int anchorPos ); void slotUpdateUndoAndRedoInContextMenu( QMenu* menu ); private Q_SLOTS: void slotChanged(); - void slotRefresh(Okular::FormField* form); + + protected: + void slotRefresh( Okular::FormField* form ) override; private: int m_prevCursorPos; int m_prevAnchorPos; }; class FileEdit : public KUrlRequester, public FormWidgetIface { Q_OBJECT public: explicit FileEdit( Okular::FormFieldText * text, QWidget * parent = nullptr ); void setFormWidgetsController( FormWidgetsController *controller ) override; protected: bool eventFilter( QObject *obj, QEvent *event ) override; private Q_SLOTS: void slotChanged(); void slotHandleFileChangedByUndoRedo( int pageNumber, Okular::FormFieldText * form, const QString & contents, int cursorPos, int anchorPos ); private: int m_prevCursorPos; int m_prevAnchorPos; }; class ListEdit : public QListWidget, public FormWidgetIface { Q_OBJECT public: explicit ListEdit( Okular::FormFieldChoice * choice, QWidget * parent = nullptr ); void setFormWidgetsController( FormWidgetsController *controller ) override; private Q_SLOTS: void slotSelectionChanged(); void slotHandleFormListChangedByUndoRedo( int pageNumber, Okular::FormFieldChoice * listForm, const QList< int > & choices ); }; class ComboEdit : public QComboBox, public FormWidgetIface { Q_OBJECT public: explicit ComboEdit( Okular::FormFieldChoice * choice, QWidget * parent = nullptr ); void setFormWidgetsController( FormWidgetsController *controller ) override; bool event ( QEvent * e ) override; void contextMenuEvent( QContextMenuEvent* event ) override; private Q_SLOTS: void slotValueChanged(); void slotHandleFormComboChangedByUndoRedo( int pageNumber, Okular::FormFieldChoice * comboForm, const QString & text, int cursorPos, int anchorPos ); private: int m_prevCursorPos; int m_prevAnchorPos; }; #endif diff --git a/ui/pageviewutils.cpp b/ui/pageviewutils.cpp index d8a8698bc..d639b0d1b 100644 --- a/ui/pageviewutils.cpp +++ b/ui/pageviewutils.cpp @@ -1,947 +1,948 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * 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 "pageviewutils.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // system includes #include // local includes #include "formwidgets.h" #include "pageview.h" #include "videowidget.h" #include "core/movie.h" #include "core/page.h" #include "core/form.h" #include "settings.h" /*********************/ /** PageViewItem */ /*********************/ PageViewItem::PageViewItem( const Okular::Page * page ) : m_page( page ), m_zoomFactor( 1.0 ), m_visible( true ), m_formsVisible( false ), m_crop( 0., 0., 1., 1. ) { } PageViewItem::~PageViewItem() { qDeleteAll( m_formWidgets ); qDeleteAll( m_videoWidgets ); } const Okular::Page * PageViewItem::page() const { return m_page; } int PageViewItem::pageNumber() const { return m_page->number(); } const QRect& PageViewItem::croppedGeometry() const { return m_croppedGeometry; } int PageViewItem::croppedWidth() const { return m_croppedGeometry.width(); } int PageViewItem::croppedHeight() const { return m_croppedGeometry.height(); } const QRect& PageViewItem::uncroppedGeometry() const { return m_uncroppedGeometry; } int PageViewItem::uncroppedWidth() const { return m_uncroppedGeometry.width(); } int PageViewItem::uncroppedHeight() const { return m_uncroppedGeometry.height(); } const Okular::NormalizedRect & PageViewItem::crop() const { return m_crop; } double PageViewItem::zoomFactor() const { return m_zoomFactor; } double PageViewItem::absToPageX( double absX ) const { return ( absX - m_uncroppedGeometry.left() ) / m_uncroppedGeometry.width(); } double PageViewItem::absToPageY( double absY ) const { return ( absY - m_uncroppedGeometry.top() ) / m_uncroppedGeometry.height(); } bool PageViewItem::isVisible() const { return m_visible; } QSet& PageViewItem::formWidgets() { return m_formWidgets; } QHash< Okular::Movie *, VideoWidget* >& PageViewItem::videoWidgets() { return m_videoWidgets; } void PageViewItem::setWHZC( int w, int h, double z, const Okular:: NormalizedRect & c ) { m_croppedGeometry.setWidth( w ); m_croppedGeometry.setHeight( h ); m_zoomFactor = z; m_crop = c; m_uncroppedGeometry.setWidth( qRound( w / ( c.right - c.left ) ) ); m_uncroppedGeometry.setHeight( qRound( h / ( c.bottom - c.top ) ) ); foreach(FormWidgetIface *fwi, m_formWidgets) { Okular::NormalizedRect r = fwi->rect(); fwi->setWidthHeight( qRound( fabs( r.right - r.left ) * m_uncroppedGeometry.width() ), qRound( fabs( r.bottom - r.top ) * m_uncroppedGeometry.height() ) ); } Q_FOREACH ( VideoWidget *vw, m_videoWidgets ) { const Okular::NormalizedRect r = vw->normGeometry(); vw->resize( qRound( fabs( r.right - r.left ) * m_uncroppedGeometry.width() ), qRound( fabs( r.bottom - r.top ) * m_uncroppedGeometry.height() ) ); } } void PageViewItem::moveTo( int x, int y ) // Assumes setWHZC() has already been called { m_croppedGeometry.moveLeft( x ); m_croppedGeometry.moveTop( y ); m_uncroppedGeometry.moveLeft( qRound( x - m_crop.left * m_uncroppedGeometry.width() ) ); m_uncroppedGeometry.moveTop( qRound( y - m_crop.top * m_uncroppedGeometry.height() ) ); QSet::iterator it = m_formWidgets.begin(), itEnd = m_formWidgets.end(); for ( ; it != itEnd; ++it ) { Okular::NormalizedRect r = (*it)->rect(); (*it)->moveTo( qRound( x + m_uncroppedGeometry.width() * r.left ) + 1, qRound( y + m_uncroppedGeometry.height() * r.top ) + 1 ); } Q_FOREACH ( VideoWidget *vw, m_videoWidgets ) { const Okular::NormalizedRect r = vw->normGeometry(); vw->move( qRound( x + m_uncroppedGeometry.width() * r.left ) + 1, qRound( y + m_uncroppedGeometry.height() * r.top ) + 1 ); } } void PageViewItem::setVisible( bool visible ) { setFormWidgetsVisible( visible && m_formsVisible ); m_visible = visible; } void PageViewItem::invalidate() { m_croppedGeometry.setRect( 0, 0, 0, 0 ); m_uncroppedGeometry.setRect( 0, 0, 0, 0 ); } bool PageViewItem::setFormWidgetsVisible( bool visible ) { m_formsVisible = visible; if ( !m_visible ) return false; bool somehadfocus = false; QSet::iterator it = m_formWidgets.begin(), itEnd = m_formWidgets.end(); for ( ; it != itEnd; ++it ) { - bool hadfocus = (*it)->setVisibility( visible && (*it)->formField()->isVisible() ); + bool hadfocus = (*it)->setVisibility( visible && (*it)->formField()->isVisible() && + !(*it)->formField()->isReadOnly() ); somehadfocus = somehadfocus || hadfocus; } return somehadfocus; } void PageViewItem::reloadFormWidgetsState() { foreach(FormWidgetIface *fwi, m_formWidgets) { - fwi->setVisibility( fwi->formField()->isVisible() ); + fwi->setVisibility( fwi->formField()->isVisible() && !fwi->formField()->isReadOnly() ); } } /*********************/ /** PageViewMessage */ /*********************/ PageViewMessage::PageViewMessage( QWidget * parent ) : QWidget( parent ), m_timer( nullptr ) , m_lineSpacing( 0 ) { setObjectName( QStringLiteral( "pageViewMessage" ) ); setFocusPolicy( Qt::NoFocus ); QPalette pal = palette(); pal.setColor( QPalette::Active, QPalette::Window, QApplication::palette().color( QPalette::Active, QPalette::Window ) ); setPalette( pal ); // if the layout is LtR, we can safely place it in the right position if ( layoutDirection() == Qt::LeftToRight ) move( 10, 10 ); resize( 0, 0 ); hide(); } void PageViewMessage::display( const QString & message, const QString & details, Icon icon, int durationMs ) // give Caesar what belongs to Caesar: code taken from Amarok's osd.h/.cpp // "redde (reddite, pl.) cesari quae sunt cesaris", just btw. :) // The code has been heavily modified since then. { if ( !Okular::Settings::showOSD() ) { hide(); return; } // set text m_message = message; m_details = details; // reset vars m_lineSpacing = 0; // load icon (if set) m_symbol = QPixmap(); if ( icon != None ) { switch ( icon ) { case Annotation: m_symbol = SmallIcon( QStringLiteral("draw-freehand") ); break; case Find: m_symbol = SmallIcon( QStringLiteral("zoom-original") ); break; case Error: m_symbol = SmallIcon( QStringLiteral("dialog-error") ); break; case Warning: m_symbol = SmallIcon( QStringLiteral("dialog-warning") ); break; default: m_symbol = SmallIcon( QStringLiteral("dialog-information") ); break; } } computeSizeAndResize(); // show widget and schedule a repaint show(); update(); // close the message window after given mS if ( durationMs > 0 ) { if ( !m_timer ) { m_timer = new QTimer( this ); m_timer->setSingleShot( true ); connect(m_timer, &QTimer::timeout, this, &PageViewMessage::hide); } m_timer->start( durationMs ); } else if ( m_timer ) m_timer->stop(); qobject_cast(parentWidget())->viewport()->installEventFilter(this); } QRect PageViewMessage::computeTextRect( const QString & message, int extra_width ) const // Return the QRect which embeds the text { int charSize = fontMetrics().averageCharWidth(); /* width of the viewport, minus 20 (~ size removed by further resizing), minus the extra size (usually the icon width), minus (a bit empirical) twice the mean width of a character to ensure that the bounding box is really smaller than the container. */ const int boundingWidth = qobject_cast(parentWidget())->viewport()->width() - 20 - ( extra_width > 0 ? 2 + extra_width : 0 ) - 2*charSize; QRect textRect = fontMetrics().boundingRect( 0, 0, boundingWidth, 0, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, message ); textRect.translate( -textRect.left(), -textRect.top() ); textRect.adjust( 0, 0, 2, 2 ); return textRect; } void PageViewMessage::computeSizeAndResize() { // determine text rectangle const QRect textRect = computeTextRect( m_message, m_symbol.width() ); int width = textRect.width(), height = textRect.height(); if ( !m_details.isEmpty() ) { // determine details text rectangle const QRect detailsRect = computeTextRect( m_details, m_symbol.width() ); width = qMax( width, detailsRect.width() ); height += detailsRect.height(); // plus add a ~60% line spacing m_lineSpacing = static_cast< int >( fontMetrics().height() * 0.6 ); height += m_lineSpacing; } // update geometry with icon information if ( ! m_symbol.isNull() ) { width += 2 + m_symbol.width(); height = qMax( height, m_symbol.height() ); } // resize widget resize( QRect( 0, 0, width + 10, height + 8 ).size() ); // if the layout is RtL, we can move it to the right place only after we // know how much size it will take if ( layoutDirection() == Qt::RightToLeft ) move( parentWidget()->width() - geometry().width() - 10 - 1, 10 ); } bool PageViewMessage::eventFilter(QObject * obj, QEvent * event ) { /* if the parent object (scroll area) resizes, the message should resize as well */ if (event->type() == QEvent::Resize) { QResizeEvent *resizeEvent = static_cast(event); if ( resizeEvent->oldSize() != resizeEvent->size() ) { computeSizeAndResize(); } } // standard event processing return QObject::eventFilter(obj, event); } void PageViewMessage::paintEvent( QPaintEvent * /* e */ ) { const QRect textRect = computeTextRect( m_message, m_symbol.width() ); QRect detailsRect; if ( !m_details.isEmpty() ) { detailsRect = computeTextRect( m_details, m_symbol.width() ); } int textXOffset = 0, // add 2 to account for the reduced drawRoundRect later textYOffset = ( geometry().height() - textRect.height() - detailsRect.height() - m_lineSpacing + 2 ) / 2, iconXOffset = 0, iconYOffset = !m_symbol.isNull() ? ( geometry().height() - m_symbol.height() ) / 2 : 0, shadowOffset = 1; if ( layoutDirection() == Qt::RightToLeft ) iconXOffset = 2 + textRect.width(); else textXOffset = 2 + m_symbol.width(); // draw background QPainter painter( this ); painter.setRenderHint( QPainter::Antialiasing, true ); painter.setPen( Qt::black ); painter.setBrush( palette().color( QPalette::Window ) ); painter.translate( 0.5, 0.5 ); painter.drawRoundRect( 1, 1, width()-2, height()-2, 1600 / width(), 1600 / height() ); // draw icon if present if ( !m_symbol.isNull() ) painter.drawPixmap( 5 + iconXOffset, iconYOffset, m_symbol, 0, 0, m_symbol.width(), m_symbol.height() ); const int xStartPoint = 5 + textXOffset; const int yStartPoint = textYOffset; const int textDrawingFlags = Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap; // draw shadow and text painter.setPen( palette().color( QPalette::Window ).dark( 115 ) ); painter.drawText( xStartPoint + shadowOffset, yStartPoint + shadowOffset, textRect.width(), textRect.height(), textDrawingFlags, m_message ); if ( !m_details.isEmpty() ) painter.drawText( xStartPoint + shadowOffset, yStartPoint + textRect.height() + m_lineSpacing + shadowOffset, textRect.width(), detailsRect.height(), textDrawingFlags, m_details ); painter.setPen( palette().color( QPalette::WindowText ) ); painter.drawText( xStartPoint, yStartPoint, textRect.width(), textRect.height(), textDrawingFlags, m_message ); if ( !m_details.isEmpty() ) painter.drawText( xStartPoint + shadowOffset, yStartPoint + textRect.height() + m_lineSpacing, textRect.width(), detailsRect.height(), textDrawingFlags, m_details ); } void PageViewMessage::mousePressEvent( QMouseEvent * /*e*/ ) { if ( m_timer ) m_timer->stop(); hide(); } /*********************/ /** PageViewToolBar */ /*********************/ ToolBarButton::ToolBarButton( QWidget * parent, const AnnotationToolItem &item ) : QToolButton( parent ), m_id( item.id ), m_isText( item.isText ) { setCheckable( true ); setAutoRaise( true ); resize( buttonSize, buttonSize ); setIconSize( QSize( iconSize, iconSize ) ); setIcon( QIcon( item.pixmap ) ); // set shortcut if defined if ( !item.shortcut.isEmpty() ) setShortcut( QKeySequence( item.shortcut ) ); else KAcceleratorManager::setNoAccel( this ); // if accel is set display it along name QString accelString = shortcut().toString( QKeySequence::NativeText ); if ( !accelString.isEmpty() ) setToolTip( QStringLiteral("%1 [%2]").arg( item.text, accelString ) ); else setToolTip( item.text ); } void ToolBarButton::mouseDoubleClickEvent( QMouseEvent * /*event*/ ) { emit buttonDoubleClicked( buttonID() ); } /* PageViewToolBar */ static const int toolBarGridSize = 40; class ToolBarPrivate { public: ToolBarPrivate( PageViewToolBar * qq ) : q( qq ) { } // rebuild contents and reposition then widget void buildToolBar(); void reposition(); // compute the visible and hidden positions along current side QPoint getInnerPoint() const; QPoint getOuterPoint() const; void selectButton( ToolBarButton * button ); PageViewToolBar * q; // anchored widget and side QWidget * anchorWidget; PageViewToolBar::Side anchorSide; // slide in/out stuff QTimer * animTimer; QPoint currentPosition; QPoint endPosition; bool hiding; bool visible; // background pixmap and buttons QPixmap backgroundPixmap; QLinkedList< ToolBarButton * > buttons; }; PageViewToolBar::PageViewToolBar( PageView * parent, QWidget * anchorWidget ) : QWidget( parent ), d( new ToolBarPrivate( this ) ) { // initialize values of the private data storage structure d->anchorWidget = anchorWidget; d->anchorSide = Left; d->hiding = false; d->visible = false; // create the animation timer d->animTimer = new QTimer( this ); connect( d->animTimer, &QTimer::timeout, this, &PageViewToolBar::slotAnimate ); // apply a filter to get notified when anchor changes geometry d->anchorWidget->installEventFilter( this ); setContextMenuPolicy( Qt::ActionsContextMenu ); addAction( parent->actionCollection()->action( QStringLiteral("options_configure_annotations") ) ); } PageViewToolBar::~PageViewToolBar() { // delete the private data storage structure delete d; } void PageViewToolBar::setItems( const QLinkedList &items ) { // delete buttons if already present if ( !d->buttons.isEmpty() ) { QLinkedList< ToolBarButton * >::iterator it = d->buttons.begin(), end = d->buttons.end(); for ( ; it != end; ++it ) delete *it; d->buttons.clear(); } // create new buttons for given items QLinkedList::const_iterator it = items.begin(), end = items.end(); for ( ; it != end; ++it ) { ToolBarButton * button = new ToolBarButton( this, *it ); connect(button, &ToolBarButton::clicked, this, &PageViewToolBar::slotButtonClicked); connect(button, &ToolBarButton::buttonDoubleClicked, this, &PageViewToolBar::buttonDoubleClicked); d->buttons.append( button ); } // rebuild toolbar shape and contents d->reposition(); } void PageViewToolBar::setSide( Side side ) { d->anchorSide = side; d->reposition(); } void PageViewToolBar::showAndAnimate() { // set parameters for sliding in d->hiding = false; show(); #ifdef OKULAR_ANIMATE_REVIEW_TOOBAR // start scrolling in d->animTimer->start( 20 ); #else d->currentPosition = d->endPosition; move( d->currentPosition ); d->visible = true; #endif } void PageViewToolBar::hideAndDestroy() { // set parameters for sliding out d->hiding = true; d->endPosition = d->getOuterPoint(); #ifdef OKULAR_ANIMATE_REVIEW_TOOBAR // start scrolling out d->animTimer->start( 20 ); #else d->currentPosition = d->endPosition; move( d->currentPosition ); d->visible = false; deleteLater(); #endif } void PageViewToolBar::selectButton( int id ) { ToolBarButton * button = nullptr; if ( id >= 0 && id < d->buttons.count() ) button = *(d->buttons.begin() + id); else { QLinkedList< ToolBarButton * >::const_iterator it = d->buttons.begin(), end = d->buttons.end(); for ( ; !button && it != end; ++it ) if ( (*it)->isChecked() ) button = *it; if ( button ) button->setChecked( false ); } d->selectButton( button ); } bool PageViewToolBar::eventFilter( QObject * obj, QEvent * e ) { // if anchorWidget changed geometry reposition toolbar if ( obj == d->anchorWidget && e->type() == QEvent::Resize ) { d->animTimer->stop(); if ( d->hiding ) deleteLater(); else d->reposition(); } // don't block event return false; } void PageViewToolBar::paintEvent( QPaintEvent * e ) { // paint the internal pixmap over the widget QPainter p( this ); p.drawImage( e->rect().topLeft(), d->backgroundPixmap.toImage(), e->rect() ); } void PageViewToolBar::mousePressEvent( QMouseEvent * e ) { // set 'dragging' cursor if ( e->button() == Qt::LeftButton ) setCursor( Qt::SizeAllCursor ); } void PageViewToolBar::mouseMoveEvent( QMouseEvent * e ) { if ( ( QApplication::mouseButtons() & Qt::LeftButton ) != Qt::LeftButton ) return; // compute the nearest side to attach the widget to QPoint parentPos = mapToParent( e->pos() ); float nX = (float)parentPos.x() / (float)d->anchorWidget->width(), nY = (float)parentPos.y() / (float)d->anchorWidget->height(); if ( nX > 0.3 && nX < 0.7 && nY > 0.3 && nY < 0.7 ) return; bool LT = nX < (1.0 - nY); bool LB = nX < (nY); Side side = LT ? ( LB ? Left : Top ) : ( LB ? Bottom : Right ); // check if side changed if ( side == d->anchorSide ) return; d->anchorSide = side; d->reposition(); emit orientationChanged( (int)side ); } void PageViewToolBar::mouseReleaseEvent( QMouseEvent * e ) { // set normal cursor if ( e->button() == Qt::LeftButton ) setCursor( Qt::ArrowCursor ); } void ToolBarPrivate::buildToolBar() { int buttonsNumber = buttons.count(), parentWidth = anchorWidget->width(), parentHeight = anchorWidget->height(), myCols = 1, myRows = 1; // 1. find out columns and rows we're going to use bool topLeft = anchorSide == PageViewToolBar::Left || anchorSide == PageViewToolBar::Top; bool vertical = anchorSide == PageViewToolBar::Left || anchorSide == PageViewToolBar::Right; if ( vertical ) { myCols = 1 + (buttonsNumber * toolBarGridSize) / (parentHeight - toolBarGridSize); myRows = (int)ceil( (float)buttonsNumber / (float)myCols ); } else { myRows = 1 + (buttonsNumber * toolBarGridSize) / (parentWidth - toolBarGridSize); myCols = (int)ceil( (float)buttonsNumber / (float)myRows ); } // 2. compute widget size (from rows/cols) int myWidth = myCols * toolBarGridSize, myHeight = myRows * toolBarGridSize, xOffset = (toolBarGridSize - ToolBarButton::buttonSize) / 2, yOffset = (toolBarGridSize - ToolBarButton::buttonSize) / 2; if ( vertical ) { myHeight += 16; myWidth += 4; yOffset += 12; if ( anchorSide == PageViewToolBar::Right ) xOffset += 4; } else { myWidth += 16; myHeight += 4; xOffset += 12; if ( anchorSide == PageViewToolBar::Bottom ) yOffset += 4; } bool prevUpdates = q->updatesEnabled(); q->setUpdatesEnabled( false ); // 3. resize pixmap, mask and widget QBitmap mask( myWidth + 1, myHeight + 1 ); backgroundPixmap = QPixmap( myWidth + 1, myHeight + 1 ); backgroundPixmap.fill(Qt::transparent); q->resize( myWidth + 1, myHeight + 1 ); // 4. create and set transparency mask // 4. draw background QPainter maskPainter( &mask); mask.fill( Qt::white ); maskPainter.setBrush( Qt::black ); if ( vertical ) maskPainter.drawRoundRect( topLeft ? -10 : 0, 0, myWidth + 11, myHeight, 2000 / (myWidth + 10), 2000 / myHeight ); else maskPainter.drawRoundRect( 0, topLeft ? -10 : 0, myWidth, myHeight + 11, 2000 / myWidth, 2000 / (myHeight + 10) ); maskPainter.end(); q->setMask( mask ); // 5. draw background QPainter bufferPainter( &backgroundPixmap ); bufferPainter.translate( 0.5, 0.5 ); QPalette pal = q->palette(); // 5.1. draw horizontal/vertical gradient QLinearGradient grad; switch ( anchorSide ) { case PageViewToolBar::Left: grad = QLinearGradient( 0, 1, myWidth + 1, 1 ); break; case PageViewToolBar::Right: grad = QLinearGradient( myWidth + 1, 1, 0, 1 ); break; case PageViewToolBar::Top: grad = QLinearGradient( 1, 0, 1, myHeight + 1 ); break; case PageViewToolBar::Bottom: grad = QLinearGradient( 1, myHeight + 1, 0, 1 ); break; } grad.setColorAt( 0, pal.color( QPalette::Active, QPalette::Button ) ); grad.setColorAt( 1, pal.color( QPalette::Active, QPalette::Light ) ); bufferPainter.setBrush( QBrush( grad ) ); // 5.2. draw rounded border bufferPainter.setPen( pal.color( QPalette::Active, QPalette::Dark ).lighter( 140 ) ); bufferPainter.setRenderHints( QPainter::Antialiasing ); if ( vertical ) bufferPainter.drawRoundRect( topLeft ? -10 : 0, 0, myWidth + 10, myHeight, 2000 / (myWidth + 10), 2000 / myHeight ); else bufferPainter.drawRoundRect( 0, topLeft ? -10 : 0, myWidth, myHeight + 10, 2000 / myWidth, 2000 / (myHeight + 10) ); // 5.3. draw handle bufferPainter.translate( -0.5, -0.5 ); bufferPainter.setPen( pal.color( QPalette::Active, QPalette::Mid ) ); if ( vertical ) { int dx = anchorSide == PageViewToolBar::Left ? 2 : 4; bufferPainter.drawLine( dx, 6, dx + myWidth - 8, 6 ); bufferPainter.drawLine( dx, 9, dx + myWidth - 8, 9 ); bufferPainter.setPen( pal.color( QPalette::Active, QPalette::Light ) ); bufferPainter.drawLine( dx + 1, 7, dx + myWidth - 7, 7 ); bufferPainter.drawLine( dx + 1, 10, dx + myWidth - 7, 10 ); } else { int dy = anchorSide == PageViewToolBar::Top ? 2 : 4; bufferPainter.drawLine( 6, dy, 6, dy + myHeight - 8 ); bufferPainter.drawLine( 9, dy, 9, dy + myHeight - 8 ); bufferPainter.setPen( pal.color( QPalette::Active, QPalette::Light ) ); bufferPainter.drawLine( 7, dy + 1, 7, dy + myHeight - 7 ); bufferPainter.drawLine( 10, dy + 1, 10, dy + myHeight - 7 ); } bufferPainter.end(); // 6. reposition buttons (in rows/col grid) int gridX = 0, gridY = 0; QLinkedList< ToolBarButton * >::const_iterator it = buttons.begin(), end = buttons.end(); for ( ; it != end; ++it ) { ToolBarButton * button = *it; button->move( gridX * toolBarGridSize + xOffset, gridY * toolBarGridSize + yOffset ); button->show(); if ( ++gridX == myCols ) { gridX = 0; gridY++; } } q->setUpdatesEnabled( prevUpdates ); } void ToolBarPrivate::reposition() { // note: hiding widget here will gives better gfx, but ends drag operation // rebuild widget and move it to its final place buildToolBar(); if ( !visible ) { currentPosition = getOuterPoint(); endPosition = getInnerPoint(); } else { currentPosition = getInnerPoint(); endPosition = getOuterPoint(); } q->move( currentPosition ); // repaint all buttons (to update background) QLinkedList< ToolBarButton * >::const_iterator it = buttons.begin(), end = buttons.end(); for ( ; it != end; ++it ) (*it)->update(); } QPoint ToolBarPrivate::getInnerPoint() const { // returns the final position of the widget QPoint newPos; switch ( anchorSide ) { case PageViewToolBar::Left: newPos = QPoint( 0, ( anchorWidget->height() - q->height() ) / 2 ); break; case PageViewToolBar::Top: newPos = QPoint( ( anchorWidget->width() - q->width() ) / 2, 0 ); break; case PageViewToolBar::Right: newPos = QPoint( anchorWidget->width() - q->width(), ( anchorWidget->height() - q->height() ) / 2 ); break; case PageViewToolBar::Bottom: newPos = QPoint( ( anchorWidget->width() - q->width()) / 2, anchorWidget->height() - q->height() ); break; } return newPos + anchorWidget->pos(); } QPoint ToolBarPrivate::getOuterPoint() const { // returns the point from which the transition starts QPoint newPos; switch ( anchorSide ) { case PageViewToolBar::Left: newPos = QPoint( -q->width(), ( anchorWidget->height() - q->height() ) / 2 ); break; case PageViewToolBar::Top: newPos = QPoint( ( anchorWidget->width() - q->width() ) / 2, -q->height() ); break; case PageViewToolBar::Right: newPos = QPoint( anchorWidget->width(), ( anchorWidget->height() - q->height() ) / 2 ); break; case PageViewToolBar::Bottom: newPos = QPoint( ( anchorWidget->width() - q->width() ) / 2, anchorWidget->height() ); break; } return newPos + anchorWidget->pos(); } void PageViewToolBar::slotAnimate() { // move currentPosition towards endPosition int dX = d->endPosition.x() - d->currentPosition.x(), dY = d->endPosition.y() - d->currentPosition.y(); dX = dX / 6 + qMax( -1, qMin( 1, dX) ); dY = dY / 6 + qMax( -1, qMin( 1, dY) ); d->currentPosition.setX( d->currentPosition.x() + dX ); d->currentPosition.setY( d->currentPosition.y() + dY ); // move the widget move( d->currentPosition ); // handle arrival to the end if ( d->currentPosition == d->endPosition ) { d->animTimer->stop(); if ( d->hiding ) { d->visible = false; deleteLater(); } else { d->visible = true; } } } void PageViewToolBar::slotButtonClicked() { ToolBarButton * button = qobject_cast( sender() ); d->selectButton( button ); } void ToolBarPrivate::selectButton( ToolBarButton * button ) { if ( button ) { // deselect other buttons QLinkedList< ToolBarButton * >::const_iterator it = buttons.begin(), end = buttons.end(); for ( ; it != end; ++it ) if ( *it != button ) (*it)->setChecked( false ); // emit signal (-1 if button has been unselected) emit q->toolSelected( button->isChecked() ? button->buttonID() : -1 ); } } void PageViewToolBar::setToolsEnabled( bool on ) { QLinkedList< ToolBarButton * >::const_iterator it = d->buttons.begin(), end = d->buttons.end(); for ( ; it != end; ++it ) (*it)->setEnabled( on ); } void PageViewToolBar::setTextToolsEnabled( bool on ) { QLinkedList< ToolBarButton * >::const_iterator it = d->buttons.begin(), end = d->buttons.end(); for ( ; it != end; ++it ) if ( (*it)->isText() ) (*it)->setEnabled( on ); } #include "moc_pageviewutils.cpp"