diff --git a/autotests/data/file1-docdata.xml b/autotests/data/file1-docdata.xml new file mode 100644 index 000000000..0904d748b --- /dev/null +++ b/autotests/data/file1-docdata.xml @@ -0,0 +1,427 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/autotests/data/potato.jpg b/autotests/data/potato.jpg new file mode 100644 index 000000000..b7dd4d188 Binary files /dev/null and b/autotests/data/potato.jpg differ diff --git a/autotests/documenttest.cpp b/autotests/documenttest.cpp index e96322834..2430be3b4 100644 --- a/autotests/documenttest.cpp +++ b/autotests/documenttest.cpp @@ -1,63 +1,133 @@ /*************************************************************************** * Copyright (C) 2013 by Fabio D'Urso * + * 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 +#include "../core/annotations.h" #include "../core/document.h" +#include "../core/document_p.h" #include "../core/generator.h" #include "../core/observer.h" +#include "../core/page.h" #include "../core/rotationjob_p.h" #include "../settings_core.h" class DocumentTest : public QObject { Q_OBJECT private slots: void testCloseDuringRotationJob(); + void testDocdataMigration(); }; // Test that we don't crash if the document is closed while a RotationJob // is enqueued/running void DocumentTest::testCloseDuringRotationJob() { Okular::SettingsCore::instance( QStringLiteral("documenttest") ); Okular::Document *m_document = new Okular::Document( nullptr ); const QString testFile = QStringLiteral(KDESRCDIR "data/file1.pdf"); QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( testFile ); Okular::DocumentObserver *dummyDocumentObserver = new Okular::DocumentObserver(); m_document->addObserver( dummyDocumentObserver ); m_document->openDocument( testFile, QUrl(), mime ); m_document->setRotation( 1 ); // Tell ThreadWeaver not to start any new job ThreadWeaver::Queue::instance()->suspend(); // Request a pixmap. A RotationJob will be enqueued but not started Okular::PixmapRequest *pixmapReq = new Okular::PixmapRequest( dummyDocumentObserver, 0, 100, 100, 1, Okular::PixmapRequest::NoFeature ); m_document->requestPixmaps( QLinkedList() << pixmapReq ); // Delete the document delete m_document; // Resume job processing and wait for the RotationJob to finish ThreadWeaver::Queue::instance()->resume(); ThreadWeaver::Queue::instance()->finish(); qApp->processEvents(); + + delete dummyDocumentObserver; +} + +// Test that, if there's a XML file in docdata referring to a document, we +// detect that it must be migrated, that it doesn't get wiped out if you close +// the document without migrating and that it does get wiped out after migrating +void DocumentTest::testDocdataMigration() +{ + Okular::SettingsCore::instance( "documenttest" ); + + const QUrl testFileUrl = QUrl::fromLocalFile(KDESRCDIR "data/file1.pdf"); + const QString testFilePath = testFileUrl.toLocalFile(); + const qint64 testFileSize = QFileInfo(testFilePath).size(); + + // Copy XML file to the docdata/ directory + const QString docDataPath = Okular::DocumentPrivate::docDataFileName(testFileUrl, testFileSize); + QFile::remove(docDataPath); + QVERIFY( QFile::copy(KDESRCDIR "data/file1-docdata.xml", docDataPath) ); + + // Open our document + Okular::Document *m_document = new Okular::Document( 0 ); + QMimeDatabase db; + const QMimeType mime = db.mimeTypeForFile( testFilePath ); + QCOMPARE( m_document->openDocument( testFilePath, testFileUrl, mime ), Okular::Document::OpenSuccess ); + + // Check that the annotation from file1-docdata.xml was loaded + QCOMPARE( m_document->page( 0 )->annotations().size(), 1 ); + QCOMPARE( m_document->page( 0 )->annotations().first()->uniqueName(), QString("testannot") ); + + // Check that we detect that it must be migrated + QCOMPARE( m_document->isDocdataMigrationNeeded(), true ); + m_document->closeDocument(); + + // Reopen the document and check that the annotation is still present + // (because we have not migrated) + QCOMPARE( m_document->openDocument( testFilePath, testFileUrl, mime ), Okular::Document::OpenSuccess ); + QCOMPARE( m_document->page( 0 )->annotations().size(), 1 ); + QCOMPARE( m_document->page( 0 )->annotations().first()->uniqueName(), QString("testannot") ); + QCOMPARE( m_document->isDocdataMigrationNeeded(), true ); + + // Do the migration + QTemporaryFile migratedSaveFile( QString( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); + QVERIFY( migratedSaveFile.open() ); + migratedSaveFile.close(); + QVERIFY( m_document->saveChanges( migratedSaveFile.fileName() ) ); + m_document->docdataMigrationDone(); + QCOMPARE( m_document->isDocdataMigrationNeeded(), false ); + m_document->closeDocument(); + + // Now the docdata file should have no annotations, let's check + QCOMPARE( m_document->openDocument( testFilePath, testFileUrl, mime ), Okular::Document::OpenSuccess ); + QCOMPARE( m_document->page( 0 )->annotations().size(), 0 ); + QCOMPARE( m_document->isDocdataMigrationNeeded(), false ); + m_document->closeDocument(); + + // And the new file should have 1 annotation, let's check + QCOMPARE( m_document->openDocument( migratedSaveFile.fileName(), migratedSaveFile.fileName(), mime ), Okular::Document::OpenSuccess ); + QCOMPARE( m_document->page( 0 )->annotations().size(), 1 ); + QCOMPARE( m_document->isDocdataMigrationNeeded(), false ); + m_document->closeDocument(); + + delete m_document; } QTEST_MAIN( DocumentTest ) #include "documenttest.moc" diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp index 7d7d17594..0b25f4423 100644 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -1,767 +1,1220 @@ /*************************************************************************** * 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 #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 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(); 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()); process.start(QStringLiteral("pdflatex"), 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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; QTimer::singleShot(2000, [view]() { // 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(); }); // 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 } // 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(); 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; QTimer::singleShot(2000, [view]() { // 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(); }); // 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 } 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(); 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; QTimer::singleShot(2000, [view]() { // 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(); }); // 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); QTest::qWait(3000); // will continue after pop-menu get closed } 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(); 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(); 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; QTimer::singleShot(2000, [view]() { 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(); }); simulateMouseSelection(mouseStartX, mouseStartY, mouseEndX, mouseEndY, part.m_pageView->viewport()); // wait menu get closed } void PartTest::simulateMouseSelection(double startX, double startY, double endX, double endY, QWidget *target) { QTestEventList events; events.addMouseMove(QPoint(startX, startY)); events.addMousePress(Qt::LeftButton, Qt::NoModifier, QPoint(startX, startY)); events.addMouseMove(QPoint(endX, endY)); // without this wait the test fails. 100ms were enough on my local system, but when running under valgrind // or on the CI server we need to wait longer. events.addDelay(1000); 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("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; + + QTemporaryFile saveFile( QString( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); + QVERIFY( saveFile.open() ); + saveFile.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( saveFile.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( saveFile.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( saveFile.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( saveFile.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( saveFile.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( saveFile.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( saveFile.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( saveFile.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( saveFile.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( saveFile.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( saveFile.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( saveFile.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( saveFile.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; +} + } 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/document.cpp b/core/document.cpp index 3ac8de145..63e5ab8be 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -1,5202 +1,5390 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2008 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 "document.h" #include "document_p.h" #include "documentcommands_p.h" #include #ifdef Q_OS_WIN #define _WIN32_WINNT 0x0500 #include #elif defined(Q_OS_FREEBSD) #include #include #include #endif // qt/kde/system includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // local includes #include "action.h" #include "annotations.h" #include "annotations_p.h" #include "audioplayer.h" #include "audioplayer_p.h" #include "bookmarkmanager.h" #include "chooseenginedialog_p.h" #include "debug_p.h" #include "generator_p.h" #include "interfaces/configinterface.h" #include "interfaces/guiinterface.h" #include "interfaces/printinterface.h" #include "interfaces/saveinterface.h" #include "observer.h" #include "misc.h" #include "page.h" #include "page_p.h" #include "pagecontroller_p.h" #include "scripter.h" #include "settings_core.h" #include "sourcereference.h" #include "sourcereference_p.h" #include "texteditors_p.h" #include "tile.h" #include "tilesmanager_p.h" #include "utils_p.h" #include "view.h" #include "view_p.h" #include "form.h" #include "utils.h" #include #include using namespace Okular; struct AllocatedPixmap { // owner of the page DocumentObserver *observer; int page; qulonglong memory; // public constructor: initialize data AllocatedPixmap( DocumentObserver *o, int p, qulonglong m ) : observer( o ), page( p ), memory( m ) {} }; struct ArchiveData { ArchiveData() { } + QString originalFileName; QTemporaryFile document; QTemporaryFile metadataFile; }; struct RunningSearch { // store search properties int continueOnPage; RegularAreaRect continueOnMatch; QSet< int > highlightedPages; // fields related to previous searches (used for 'continueSearch') QString cachedString; Document::SearchType cachedType; Qt::CaseSensitivity cachedCaseSensitivity; bool cachedViewportMove : 1; bool isCurrentlySearching : 1; QColor cachedColor; int pagesDone; }; #define foreachObserver( cmd ) {\ QSet< DocumentObserver * >::const_iterator it=d->m_observers.constBegin(), end=d->m_observers.constEnd();\ for ( ; it != end ; ++ it ) { (*it)-> cmd ; } } #define foreachObserverD( cmd ) {\ QSet< DocumentObserver * >::const_iterator it = m_observers.constBegin(), end = m_observers.constEnd();\ for ( ; it != end ; ++ it ) { (*it)-> cmd ; } } #define OKULAR_HISTORY_MAXSTEPS 100 #define OKULAR_HISTORY_SAVEDSTEPS 10 /***** Document ******/ QString DocumentPrivate::pagesSizeString() const { if (m_generator) { if (m_generator->pagesSizeMetric() != Generator::None) { QSizeF size = m_parent->allPagesSize(); if (size.isValid()) return localizedSize(size); else return QString(); } else return QString(); } else return QString(); } QString DocumentPrivate::namePaperSize(double inchesWidth, double inchesHeight) const { const QPrinter::Orientation orientation = inchesWidth > inchesHeight ? QPrinter::Landscape : QPrinter::Portrait; const QSize pointsSize(inchesWidth *72.0, inchesHeight*72.0); const QPageSize::PageSizeId paperSize = QPageSize::id(pointsSize, QPageSize::FuzzyOrientationMatch); const QString paperName = QPageSize::name(paperSize); if (orientation == QPrinter::Portrait) { return i18nc("paper type and orientation (eg: Portrait A4)", "Portrait %0").arg(paperName); } else { return i18nc("paper type and orientation (eg: Portrait A4)", "Landscape %0").arg(paperName); } } QString DocumentPrivate::localizedSize(const QSizeF &size) const { double inchesWidth = 0, inchesHeight = 0; switch (m_generator->pagesSizeMetric()) { case Generator::Points: inchesWidth = size.width() / 72.0; inchesHeight = size.height() / 72.0; break; case Generator::Pixels: { const QSizeF dpi = m_generator->dpi(); inchesWidth = size.width() / dpi.width(); inchesHeight = size.height() / dpi.height(); } break; case Generator::None: break; } if (QLocale::system().measurementSystem() == QLocale::ImperialSystem) { return i18nc("%1 is width, %2 is height, %3 is paper size name", "%1 x %2 in (%3)", inchesWidth, inchesHeight, namePaperSize(inchesWidth, inchesHeight)); } else { return i18nc("%1 is width, %2 is height, %3 is paper size name", "%1 x %2 mm (%3)", QString::number(inchesWidth * 25.4, 'd', 0), QString::number(inchesHeight * 25.4, 'd', 0), namePaperSize(inchesWidth, inchesHeight)); } } qulonglong DocumentPrivate::calculateMemoryToFree() { // [MEM] choose memory parameters based on configuration profile qulonglong clipValue = 0; qulonglong memoryToFree = 0; switch ( SettingsCore::memoryLevel() ) { case SettingsCore::EnumMemoryLevel::Low: memoryToFree = m_allocatedPixmapsTotalMemory; break; case SettingsCore::EnumMemoryLevel::Normal: { qulonglong thirdTotalMemory = getTotalMemory() / 3; qulonglong freeMemory = getFreeMemory(); if (m_allocatedPixmapsTotalMemory > thirdTotalMemory) memoryToFree = m_allocatedPixmapsTotalMemory - thirdTotalMemory; if (m_allocatedPixmapsTotalMemory > freeMemory) clipValue = (m_allocatedPixmapsTotalMemory - freeMemory) / 2; } break; case SettingsCore::EnumMemoryLevel::Aggressive: { qulonglong freeMemory = getFreeMemory(); if (m_allocatedPixmapsTotalMemory > freeMemory) clipValue = (m_allocatedPixmapsTotalMemory - freeMemory) / 2; } break; case SettingsCore::EnumMemoryLevel::Greedy: { qulonglong freeSwap; qulonglong freeMemory = getFreeMemory( &freeSwap ); const qulonglong memoryLimit = qMin( qMax( freeMemory, getTotalMemory()/2 ), freeMemory+freeSwap ); if (m_allocatedPixmapsTotalMemory > memoryLimit) clipValue = (m_allocatedPixmapsTotalMemory - memoryLimit) / 2; } break; } if ( clipValue > memoryToFree ) memoryToFree = clipValue; return memoryToFree; } void DocumentPrivate::cleanupPixmapMemory() { cleanupPixmapMemory( calculateMemoryToFree() ); } void DocumentPrivate::cleanupPixmapMemory( qulonglong memoryToFree ) { if ( memoryToFree < 1 ) return; const int currentViewportPage = (*m_viewportIterator).pageNumber; // Create a QMap of visible rects, indexed by page number QMap< int, VisiblePageRect * > visibleRects; QVector< Okular::VisiblePageRect * >::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) visibleRects.insert( (*vIt)->pageNumber, (*vIt) ); // Free memory starting from pages that are farthest from the current one int pagesFreed = 0; while ( memoryToFree > 0 ) { AllocatedPixmap * p = searchLowestPriorityPixmap( true, true ); if ( !p ) // No pixmap to remove break; qCDebug(OkularCoreDebug).nospace() << "Evicting cache pixmap observer=" << p->observer << " page=" << p->page; // m_allocatedPixmapsTotalMemory can't underflow because we always add or remove // the memory used by the AllocatedPixmap so at most it can reach zero m_allocatedPixmapsTotalMemory -= p->memory; // Make sure memoryToFree does not underflow if ( p->memory > memoryToFree ) memoryToFree = 0; else memoryToFree -= p->memory; pagesFreed++; // delete pixmap m_pagesVector.at( p->page )->deletePixmap( p->observer ); // delete allocation descriptor delete p; } // If we're still on low memory, try to free individual tiles // Store pages that weren't completely removed QLinkedList< AllocatedPixmap * > pixmapsToKeep; while (memoryToFree > 0) { int clean_hits = 0; foreach (DocumentObserver *observer, m_observers) { AllocatedPixmap * p = searchLowestPriorityPixmap( false, true, observer ); if ( !p ) // No pixmap to remove continue; clean_hits++; TilesManager *tilesManager = m_pagesVector.at( p->page )->d->tilesManager( observer ); if ( tilesManager && tilesManager->totalMemory() > 0 ) { qulonglong memoryDiff = p->memory; NormalizedRect visibleRect; if ( visibleRects.contains( p->page ) ) visibleRect = visibleRects[ p->page ]->rect; // Free non visible tiles tilesManager->cleanupPixmapMemory( memoryToFree, visibleRect, currentViewportPage ); p->memory = tilesManager->totalMemory(); memoryDiff -= p->memory; memoryToFree = (memoryDiff < memoryToFree) ? (memoryToFree - memoryDiff) : 0; m_allocatedPixmapsTotalMemory -= memoryDiff; if ( p->memory > 0 ) pixmapsToKeep.append( p ); else delete p; } else pixmapsToKeep.append( p ); } if (clean_hits == 0) break; } m_allocatedPixmaps += pixmapsToKeep; //p--rintf("freeMemory A:[%d -%d = %d] \n", m_allocatedPixmaps.count() + pagesFreed, pagesFreed, m_allocatedPixmaps.count() ); } /* Returns the next pixmap to evict from cache, or NULL if no suitable pixmap * if found. If unloadableOnly is set, only unloadable pixmaps are returned. If * thenRemoveIt is set, the pixmap is removed from m_allocatedPixmaps before * returning it */ AllocatedPixmap * DocumentPrivate::searchLowestPriorityPixmap( bool unloadableOnly, bool thenRemoveIt, DocumentObserver *observer ) { QLinkedList< AllocatedPixmap * >::iterator pIt = m_allocatedPixmaps.begin(); QLinkedList< AllocatedPixmap * >::iterator pEnd = m_allocatedPixmaps.end(); QLinkedList< AllocatedPixmap * >::iterator farthestPixmap = pEnd; const int currentViewportPage = (*m_viewportIterator).pageNumber; /* Find the pixmap that is farthest from the current viewport */ int maxDistance = -1; while ( pIt != pEnd ) { const AllocatedPixmap * p = *pIt; // Filter by observer if ( observer == nullptr || p->observer == observer ) { const int distance = qAbs( p->page - currentViewportPage ); if ( maxDistance < distance && ( !unloadableOnly || p->observer->canUnloadPixmap( p->page ) ) ) { maxDistance = distance; farthestPixmap = pIt; } } ++pIt; } /* No pixmap to remove */ if ( farthestPixmap == pEnd ) return nullptr; AllocatedPixmap * selectedPixmap = *farthestPixmap; if ( thenRemoveIt ) m_allocatedPixmaps.erase( farthestPixmap ); return selectedPixmap; } qulonglong DocumentPrivate::getTotalMemory() { static qulonglong cachedValue = 0; if ( cachedValue ) return cachedValue; #if defined(Q_OS_LINUX) // if /proc/meminfo doesn't exist, return 128MB QFile memFile( QStringLiteral("/proc/meminfo") ); if ( !memFile.open( QIODevice::ReadOnly ) ) return (cachedValue = 134217728); QTextStream readStream( &memFile ); while ( true ) { QString entry = readStream.readLine(); if ( entry.isNull() ) break; if ( entry.startsWith( QLatin1String("MemTotal:") ) ) return (cachedValue = (Q_UINT64_C(1024) * entry.section( QLatin1Char ( ' ' ), -2, -2 ).toULongLong())); } #elif defined(Q_OS_FREEBSD) qulonglong physmem; int mib[] = {CTL_HW, HW_PHYSMEM}; size_t len = sizeof( physmem ); if ( sysctl( mib, 2, &physmem, &len, NULL, 0 ) == 0 ) return (cachedValue = physmem); #elif defined(Q_OS_WIN) MEMORYSTATUSEX stat; stat.dwLength = sizeof(stat); GlobalMemoryStatusEx (&stat); return ( cachedValue = stat.ullTotalPhys ); #endif return (cachedValue = 134217728); } qulonglong DocumentPrivate::getFreeMemory( qulonglong *freeSwap ) { static QTime lastUpdate = QTime::currentTime().addSecs(-3); static qulonglong cachedValue = 0; static qulonglong cachedFreeSwap = 0; if ( qAbs( lastUpdate.secsTo( QTime::currentTime() ) ) <= 2 ) { if (freeSwap) *freeSwap = cachedFreeSwap; return cachedValue; } /* Initialize the returned free swap value to 0. It is overwritten if the * actual value is available */ if (freeSwap) *freeSwap = 0; #if defined(Q_OS_LINUX) // if /proc/meminfo doesn't exist, return MEMORY FULL QFile memFile( QStringLiteral("/proc/meminfo") ); if ( !memFile.open( QIODevice::ReadOnly ) ) return 0; // read /proc/meminfo and sum up the contents of 'MemFree', 'Buffers' // and 'Cached' fields. consider swapped memory as used memory. qulonglong memoryFree = 0; QString entry; QTextStream readStream( &memFile ); static const int nElems = 5; QString names[nElems] = { QStringLiteral("MemFree:"), QStringLiteral("Buffers:"), QStringLiteral("Cached:"), QStringLiteral("SwapFree:"), QStringLiteral("SwapTotal:") }; qulonglong values[nElems] = { 0, 0, 0, 0, 0 }; bool foundValues[nElems] = { false, false, false, false, false }; while ( true ) { entry = readStream.readLine(); if ( entry.isNull() ) break; for ( int i = 0; i < nElems; ++i ) { if ( entry.startsWith( names[i] ) ) { values[i] = entry.section( QLatin1Char ( ' ' ), -2, -2 ).toULongLong( &foundValues[i] ); } } } memFile.close(); bool found = true; for ( int i = 0; found && i < nElems; ++i ) found = found && foundValues[i]; if ( found ) { /* MemFree + Buffers + Cached - SwapUsed = * = MemFree + Buffers + Cached - (SwapTotal - SwapFree) = * = MemFree + Buffers + Cached + SwapFree - SwapTotal */ memoryFree = values[0] + values[1] + values[2] + values[3]; if ( values[4] > memoryFree ) memoryFree = 0; else memoryFree -= values[4]; } else { return 0; } lastUpdate = QTime::currentTime(); if (freeSwap) *freeSwap = ( cachedFreeSwap = (Q_UINT64_C(1024) * values[3]) ); return ( cachedValue = (Q_UINT64_C(1024) * memoryFree) ); #elif defined(Q_OS_FREEBSD) qulonglong cache, inact, free, psize; size_t cachelen, inactlen, freelen, psizelen; cachelen = sizeof( cache ); inactlen = sizeof( inact ); freelen = sizeof( free ); psizelen = sizeof( psize ); // sum up inactive, cached and free memory if ( sysctlbyname( "vm.stats.vm.v_cache_count", &cache, &cachelen, NULL, 0 ) == 0 && sysctlbyname( "vm.stats.vm.v_inactive_count", &inact, &inactlen, NULL, 0 ) == 0 && sysctlbyname( "vm.stats.vm.v_free_count", &free, &freelen, NULL, 0 ) == 0 && sysctlbyname( "vm.stats.vm.v_page_size", &psize, &psizelen, NULL, 0 ) == 0 ) { lastUpdate = QTime::currentTime(); return (cachedValue = (cache + inact + free) * psize); } else { return 0; } #elif defined(Q_OS_WIN) MEMORYSTATUSEX stat; stat.dwLength = sizeof(stat); GlobalMemoryStatusEx (&stat); lastUpdate = QTime::currentTime(); if (freeSwap) *freeSwap = ( cachedFreeSwap = stat.ullAvailPageFile ); return ( cachedValue = stat.ullAvailPhys ); #else // tell the memory is full.. will act as in LOW profile return 0; #endif } -void DocumentPrivate::loadDocumentInfo() +bool DocumentPrivate::loadDocumentInfo( LoadDocumentInfoFlags loadWhat ) // note: load data and stores it internally (document or pages). observers // are still uninitialized at this point so don't access them { //qCDebug(OkularCoreDebug).nospace() << "Using '" << d->m_xmlFileName << "' as document info file."; if ( m_xmlFileName.isEmpty() ) - return; + return false; QFile infoFile( m_xmlFileName ); - loadDocumentInfo( infoFile ); + return loadDocumentInfo( infoFile, loadWhat ); } -void DocumentPrivate::loadDocumentInfo( QFile &infoFile ) +bool DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat ) { if ( !infoFile.exists() || !infoFile.open( QIODevice::ReadOnly ) ) - return; + return false; // Load DOM from XML file QDomDocument doc( QStringLiteral("documentInfo") ); if ( !doc.setContent( &infoFile ) ) { qCDebug(OkularCoreDebug) << "Can't load XML pair! Check for broken xml."; infoFile.close(); - return; + return false; } infoFile.close(); QDomElement root = doc.documentElement(); + if ( root.tagName() != QLatin1String("documentInfo") ) - return; + return false; + + QUrl documentUrl( root.attribute( "url" ) ); + bool loadedAnything = false; // set if something gets actually loaded // Parse the DOM tree QDomNode topLevelNode = root.firstChild(); while ( topLevelNode.isElement() ) { QString catName = topLevelNode.toElement().tagName(); // Restore page attributes (bookmark, annotations, ...) from the DOM - if ( catName == QLatin1String("pageList") ) + if ( catName == QLatin1String("pageList") && ( loadWhat & LoadPageInfo ) ) { QDomNode pageNode = topLevelNode.firstChild(); while ( pageNode.isElement() ) { QDomElement pageElement = pageNode.toElement(); if ( pageElement.hasAttribute( QStringLiteral("number") ) ) { // get page number (node's attribute) bool ok; int pageNumber = pageElement.attribute( QStringLiteral("number") ).toInt( &ok ); // pass the domElement to the right page, to read config data from if ( ok && pageNumber >= 0 && pageNumber < (int)m_pagesVector.count() ) - m_pagesVector[ pageNumber ]->d->restoreLocalContents( pageElement ); + { + if ( m_pagesVector[ pageNumber ]->d->restoreLocalContents( pageElement ) ) + loadedAnything = true; + } } pageNode = pageNode.nextSibling(); } } // Restore 'general info' from the DOM - else if ( catName == QLatin1String("generalInfo") ) + else if ( catName == QLatin1String("generalInfo") && ( loadWhat & LoadGeneralInfo ) ) { QDomNode infoNode = topLevelNode.firstChild(); while ( infoNode.isElement() ) { QDomElement infoElement = infoNode.toElement(); // restore viewports history if ( infoElement.tagName() == QLatin1String("history") ) { // clear history m_viewportHistory.clear(); // append old viewports QDomNode historyNode = infoNode.firstChild(); while ( historyNode.isElement() ) { QDomElement historyElement = historyNode.toElement(); if ( historyElement.hasAttribute( QStringLiteral("viewport") ) ) { QString vpString = historyElement.attribute( QStringLiteral("viewport") ); m_viewportIterator = m_viewportHistory.insert( m_viewportHistory.end(), DocumentViewport( vpString ) ); + loadedAnything = true; } historyNode = historyNode.nextSibling(); } // consistancy check if ( m_viewportHistory.isEmpty() ) m_viewportIterator = m_viewportHistory.insert( m_viewportHistory.end(), DocumentViewport() ); } else if ( infoElement.tagName() == QLatin1String("rotation") ) { QString str = infoElement.text(); bool ok = true; int newrotation = !str.isEmpty() ? ( str.toInt( &ok ) % 4 ) : 0; if ( ok && newrotation != 0 ) { setRotationInternal( newrotation, false ); + loadedAnything = true; } } else if ( infoElement.tagName() == QLatin1String("views") ) { QDomNode viewNode = infoNode.firstChild(); while ( viewNode.isElement() ) { QDomElement viewElement = viewNode.toElement(); if ( viewElement.tagName() == QLatin1String("view") ) { const QString viewName = viewElement.attribute( QStringLiteral("name") ); Q_FOREACH ( View * view, m_views ) { if ( view->name() == viewName ) { loadViewsInfo( view, viewElement ); + loadedAnything = true; break; } } } viewNode = viewNode.nextSibling(); } } infoNode = infoNode.nextSibling(); } } topLevelNode = topLevelNode.nextSibling(); } // + + return loadedAnything; } void DocumentPrivate::loadViewsInfo( View *view, const QDomElement &e ) { QDomNode viewNode = e.firstChild(); while ( viewNode.isElement() ) { QDomElement viewElement = viewNode.toElement(); if ( viewElement.tagName() == QLatin1String("zoom") ) { const QString valueString = viewElement.attribute( QStringLiteral("value") ); bool newzoom_ok = true; const double newzoom = !valueString.isEmpty() ? valueString.toDouble( &newzoom_ok ) : 1.0; if ( newzoom_ok && newzoom != 0 && view->supportsCapability( View::Zoom ) && ( view->capabilityFlags( View::Zoom ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { view->setCapability( View::Zoom, newzoom ); } const QString modeString = viewElement.attribute( QStringLiteral("mode") ); bool newmode_ok = true; const int newmode = !modeString.isEmpty() ? modeString.toInt( &newmode_ok ) : 2; if ( newmode_ok && view->supportsCapability( View::ZoomModality ) && ( view->capabilityFlags( View::ZoomModality ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { view->setCapability( View::ZoomModality, newmode ); } } viewNode = viewNode.nextSibling(); } } void DocumentPrivate::saveViewsInfo( View *view, QDomElement &e ) const { if ( view->supportsCapability( View::Zoom ) && ( view->capabilityFlags( View::Zoom ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) && view->supportsCapability( View::ZoomModality ) && ( view->capabilityFlags( View::ZoomModality ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { QDomElement zoomEl = e.ownerDocument().createElement( QStringLiteral("zoom") ); e.appendChild( zoomEl ); bool ok = true; const double zoom = view->capability( View::Zoom ).toDouble( &ok ); if ( ok && zoom != 0 ) { zoomEl.setAttribute( QStringLiteral("value"), QString::number(zoom) ); } const int mode = view->capability( View::ZoomModality ).toInt( &ok ); if ( ok ) { zoomEl.setAttribute( QStringLiteral("mode"), mode ); } } } QUrl DocumentPrivate::giveAbsoluteUrl( const QString & fileName ) const { if ( !QDir::isRelativePath( fileName ) ) return QUrl::fromLocalFile(fileName); if ( !m_url.isValid() ) return QUrl(); return QUrl(KIO::upUrl(m_url).toString() + fileName); } bool DocumentPrivate::openRelativeFile( const QString & fileName ) { QUrl url = giveAbsoluteUrl( fileName ); if ( url.isEmpty() ) return false; qCDebug(OkularCoreDebug).nospace() << "openRelativeFile: '" << url << "'"; emit m_parent->openUrl( url ); return true; } Generator * DocumentPrivate::loadGeneratorLibrary( const KPluginMetaData &service ) { KPluginLoader loader( service.fileName() ); qCDebug(OkularCoreDebug) << service.fileName(); KPluginFactory *factory = loader.factory(); if ( !factory ) { qCWarning(OkularCoreDebug).nospace() << "Invalid plugin factory for " << service.fileName() << ":" << loader.errorString(); return nullptr; } Generator * plugin = factory->create(); GeneratorInfo info( plugin, service ); m_loadedGenerators.insert( service.pluginId(), info ); return plugin; } void DocumentPrivate::loadAllGeneratorLibraries() { if ( m_generatorsLoaded ) return; loadServiceList( availableGenerators() ); m_generatorsLoaded = true; } void DocumentPrivate::loadServiceList( const QVector& offers ) { int count = offers.count(); if ( count <= 0 ) return; for ( int i = 0; i < count; ++i ) { QString id = offers.at(i).pluginId(); // don't load already loaded generators QHash< QString, GeneratorInfo >::const_iterator genIt = m_loadedGenerators.constFind( id ); if ( !m_loadedGenerators.isEmpty() && genIt != m_loadedGenerators.constEnd() ) continue; Generator * g = loadGeneratorLibrary( offers.at(i) ); (void)g; } } void DocumentPrivate::unloadGenerator( const GeneratorInfo& info ) { delete info.generator; } void DocumentPrivate::cacheExportFormats() { if ( m_exportCached ) return; const ExportFormat::List formats = m_generator->exportFormats(); for ( int i = 0; i < formats.count(); ++i ) { if ( formats.at( i ).mimeType().name() == QLatin1String( "text/plain" ) ) m_exportToText = formats.at( i ); else m_exportFormats.append( formats.at( i ) ); } m_exportCached = true; } ConfigInterface* DocumentPrivate::generatorConfig( GeneratorInfo& info ) { if ( info.configChecked ) return info.config; info.config = qobject_cast< Okular::ConfigInterface * >( info.generator ); info.configChecked = true; return info.config; } SaveInterface* DocumentPrivate::generatorSave( GeneratorInfo& info ) { if ( info.saveChecked ) return info.save; info.save = qobject_cast< Okular::SaveInterface * >( info.generator ); info.saveChecked = true; return info.save; } Document::OpenResult DocumentPrivate::openDocumentInternal( const KPluginMetaData& offer, bool isstdin, const QString& docFile, const QByteArray& filedata, const QString& password ) { QString propName = offer.pluginId(); QHash< QString, GeneratorInfo >::const_iterator genIt = m_loadedGenerators.constFind( propName ); m_walletGenerator = nullptr; if ( genIt != m_loadedGenerators.constEnd() ) { m_generator = genIt.value().generator; } else { m_generator = loadGeneratorLibrary( offer ); if ( !m_generator ) return Document::OpenError; genIt = m_loadedGenerators.constFind( propName ); Q_ASSERT( genIt != m_loadedGenerators.constEnd() ); } Q_ASSERT_X( m_generator, "Document::load()", "null generator?!" ); m_generator->d_func()->m_document = this; // connect error reporting signals QObject::connect( m_generator, &Generator::error, m_parent, &Document::error ); QObject::connect( m_generator, &Generator::warning, m_parent, &Document::warning ); QObject::connect( m_generator, &Generator::notice, m_parent, &Document::notice ); QApplication::setOverrideCursor( Qt::WaitCursor ); const QSizeF dpi = Utils::realDpi(m_widget); qCDebug(OkularCoreDebug) << "Output DPI:" << dpi; m_generator->setDPI(dpi); Document::OpenResult openResult = Document::OpenError; if ( !isstdin ) { openResult = m_generator->loadDocumentWithPassword( docFile, m_pagesVector, password ); } else if ( !filedata.isEmpty() ) { if ( m_generator->hasFeature( Generator::ReadRawData ) ) { openResult = m_generator->loadDocumentFromDataWithPassword( filedata, m_pagesVector, password ); } else { m_tempFile = new QTemporaryFile(); if ( !m_tempFile->open() ) { delete m_tempFile; m_tempFile = nullptr; } else { m_tempFile->write( filedata ); QString tmpFileName = m_tempFile->fileName(); m_tempFile->close(); openResult = m_generator->loadDocumentWithPassword( tmpFileName, m_pagesVector, password ); } } } QApplication::restoreOverrideCursor(); if ( openResult != Document::OpenSuccess || m_pagesVector.size() <= 0 ) { m_generator->d_func()->m_document = nullptr; QObject::disconnect( m_generator, nullptr, m_parent, nullptr ); // TODO this is a bit of a hack, since basically means that // you can only call walletDataForFile after calling openDocument // but since in reality it's what happens I've decided not to refactor/break API // One solution is just kill walletDataForFile and make OpenResult be an object // where the wallet data is also returned when OpenNeedsPassword m_walletGenerator = m_generator; m_generator = nullptr; qDeleteAll( m_pagesVector ); m_pagesVector.clear(); delete m_tempFile; m_tempFile = nullptr; // TODO: emit a message telling the document is empty if ( openResult == Document::OpenSuccess ) openResult = Document::OpenError; } return openResult; } bool DocumentPrivate::savePageDocumentInfo( QTemporaryFile *infoFile, int what ) const { if ( infoFile->open() ) { // 1. Create DOM QDomDocument doc( QStringLiteral("documentInfo") ); QDomProcessingInstruction xmlPi = doc.createProcessingInstruction( QStringLiteral( "xml" ), QStringLiteral( "version=\"1.0\" encoding=\"utf-8\"" ) ); doc.appendChild( xmlPi ); QDomElement root = doc.createElement( QStringLiteral("documentInfo") ); doc.appendChild( root ); // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM QDomElement pageList = doc.createElement( QStringLiteral("pageList") ); root.appendChild( pageList ); // .... save pages that hold data QVector< Page * >::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->saveLocalContents( pageList, doc, PageItems( what ) ); // 3. Save DOM to XML file QString xml = doc.toString(); QTextStream os( infoFile ); os.setCodec( "UTF-8" ); os << xml; return true; } return false; } DocumentViewport DocumentPrivate::nextDocumentViewport() const { DocumentViewport ret = m_nextDocumentViewport; if ( !m_nextDocumentDestination.isEmpty() && m_generator ) { DocumentViewport vp( m_parent->metaData( QStringLiteral("NamedViewport"), m_nextDocumentDestination ).toString() ); if ( vp.isValid() ) { ret = vp; } } return ret; } -void DocumentPrivate::warnLimitedAnnotSupport() -{ - if ( !m_showWarningLimitedAnnotSupport ) - return; - m_showWarningLimitedAnnotSupport = false; // Show the warning once - - if ( m_annotationsNeedSaveAs ) - { - // Shown if the user is editing annotations in a file whose metadata is - // not stored locally (.okular archives belong to this category) - KMessageBox::information( m_widget, i18n("Your annotation changes will not be saved automatically. Use File -> Save As...\nor your changes will be lost once the document is closed"), QString(), QStringLiteral("annotNeedSaveAs") ); - } - else if ( !canAddAnnotationsNatively() ) - { - // If the generator doesn't support native annotations - KMessageBox::information( m_widget, i18n("Your annotations are saved internally by Okular.\nYou can export the annotated document using File -> Export As -> Document Archive"), QString(), QStringLiteral("annotExportAsArchive") ); - } -} - void DocumentPrivate::performAddPageAnnotation( int page, Annotation * annotation ) { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr; // find out the page to attach annotation Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; // the annotation belongs already to a page if ( annotation->d_ptr->m_page ) return; // add annotation to the page kp->addAnnotation( annotation ); // tell the annotation proxy if ( proxy && proxy->supports(AnnotationProxy::Addition) ) proxy->notifyAddition( annotation, page ); // notify observers about the change notifyAnnotationChanges( page ); if ( annotation->flags() & Annotation::ExternallyDrawn ) { // Redraw everything, including ExternallyDrawn annotations refreshPixmaps( page ); } - - warnLimitedAnnotSupport(); } void DocumentPrivate::performRemovePageAnnotation( int page, Annotation * annotation ) { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr; bool isExternallyDrawn; // find out the page Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; if ( annotation->flags() & Annotation::ExternallyDrawn ) isExternallyDrawn = true; else isExternallyDrawn = false; // try to remove the annotation if ( m_parent->canRemovePageAnnotation( annotation ) ) { // tell the annotation proxy if ( proxy && proxy->supports(AnnotationProxy::Removal) ) proxy->notifyRemoval( annotation, page ); kp->removeAnnotation( annotation ); // Also destroys the object // in case of success, notify observers about the change notifyAnnotationChanges( page ); if ( isExternallyDrawn ) { // Redraw everything, including ExternallyDrawn annotations refreshPixmaps( page ); } } - - warnLimitedAnnotSupport(); } void DocumentPrivate::performModifyPageAnnotation( int page, Annotation * annotation, bool appearanceChanged ) { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr; // find out the page Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; // tell the annotation proxy if ( proxy && proxy->supports(AnnotationProxy::Modification) ) { proxy->notifyModification( annotation, page, appearanceChanged ); } // notify observers about the change notifyAnnotationChanges( page ); if ( appearanceChanged && (annotation->flags() & Annotation::ExternallyDrawn) ) { /* When an annotation is being moved, the generator will not render it. * Therefore there's no need to refresh pixmaps after the first time */ if ( annotation->flags() & (Annotation::BeingMoved | Annotation::BeingResized) ) { if ( m_annotationBeingModified ) return; else // First time: take note m_annotationBeingModified = true; } else { m_annotationBeingModified = false; } // Redraw everything, including ExternallyDrawn annotations qCDebug(OkularCoreDebug) << "Refreshing Pixmaps"; refreshPixmaps( page ); } - - // If the user is moving or resizing the annotation, don't steal the focus - if ( (annotation->flags() & (Annotation::BeingMoved | Annotation::BeingResized) ) == 0 ) - warnLimitedAnnotSupport(); } void DocumentPrivate::performSetAnnotationContents( const QString & newContents, Annotation *annot, int pageNumber ) { bool appearanceChanged = false; // Check if appearanceChanged should be true switch ( annot->subType() ) { // If it's an in-place TextAnnotation, set the inplace text case Okular::Annotation::AText: { Okular::TextAnnotation * txtann = static_cast< Okular::TextAnnotation * >( annot ); if ( txtann->textType() == Okular::TextAnnotation::InPlace ) { appearanceChanged = true; } break; } // If it's a LineAnnotation, check if caption text is visible case Okular::Annotation::ALine: { Okular::LineAnnotation * lineann = static_cast< Okular::LineAnnotation * >( annot ); if ( lineann->showCaption() ) appearanceChanged = true; break; } default: break; } // Set contents annot->setContents( newContents ); // Tell the document the annotation has been modified performModifyPageAnnotation( pageNumber, annot, appearanceChanged ); } void DocumentPrivate::recalculateForms() { const QVariant fco = m_parent->metaData(QLatin1String("FormCalculateOrder")); const QVector formCalculateOrder = fco.value>(); foreach(int formId, formCalculateOrder) { for ( uint pageIdx = 0; pageIdx < m_parent->pages(); pageIdx++ ) { const Page *p = m_parent->page( pageIdx ); if (p) { foreach( FormField *form, p->formFields() ) { if ( form->id() == formId ) { Action *action = form->additionalAction( FormField::CalculateField ); if (action) { m_parent->processAction( action ); } else { qWarning() << "Form that is part of calculate order doesn't have a calculate action"; } } } } } } } void DocumentPrivate::saveDocumentInfo() const { if ( m_xmlFileName.isEmpty() ) return; QFile infoFile( m_xmlFileName ); qCDebug(OkularCoreDebug) << "About to save document info to" << m_xmlFileName; if (!infoFile.open( QIODevice::WriteOnly | QIODevice::Truncate)) { qCWarning(OkularCoreDebug) << "Failed to open docdata file" << m_xmlFileName; return; - } // 1. Create DOM QDomDocument doc( QStringLiteral("documentInfo") ); QDomProcessingInstruction xmlPi = doc.createProcessingInstruction( QStringLiteral( "xml" ), QStringLiteral( "version=\"1.0\" encoding=\"utf-8\"" ) ); doc.appendChild( xmlPi ); QDomElement root = doc.createElement( QStringLiteral("documentInfo") ); root.setAttribute( QStringLiteral("url"), m_url.toDisplayString(QUrl::PreferLocalFile) ); doc.appendChild( root ); // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM - QDomElement pageList = doc.createElement( QStringLiteral("pageList") ); - root.appendChild( pageList ); - PageItems saveWhat = AllPageItems; - if ( m_annotationsNeedSaveAs ) - { - /* In this case, if the user makes a modification, he's requested to - * save to a new document. Therefore, if there are existing local - * annotations, we save them back unmodified in the original - * document's metadata, so that it appears that it was not changed */ - saveWhat |= OriginalAnnotationPageItems; - } - // .... save pages that hold data - QVector< Page * >::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd(); - for ( ; pIt != pEnd; ++pIt ) - (*pIt)->d->saveLocalContents( pageList, doc, saveWhat ); + // -> do this if there are not-yet-migrated annots or forms in docdata/ + if ( m_docdataMigrationNeeded ) + { + QDomElement pageList = doc.createElement( "pageList" ); + root.appendChild( pageList ); + // OriginalAnnotationPageItems and OriginalFormFieldPageItems tell to + // store the same unmodified annotation list and form contents that we + // read when we opened the file and ignore any change made by the user. + // Since we don't store annotations and forms in docdata/ any more, this is + // necessary to preserve annotations/forms that previous Okular version + // had stored there. + const PageItems saveWhat = AllPageItems | OriginalAnnotationPageItems | OriginalFormFieldPageItems; + // .... save pages that hold data + QVector< Page * >::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd(); + for ( ; pIt != pEnd; ++pIt ) + (*pIt)->d->saveLocalContents( pageList, doc, saveWhat ); + } // 2.2. Save document info (current viewport, history, ... ) to DOM QDomElement generalInfo = doc.createElement( QStringLiteral("generalInfo") ); root.appendChild( generalInfo ); // create rotation node if ( m_rotation != Rotation0 ) { QDomElement rotationNode = doc.createElement( QStringLiteral("rotation") ); generalInfo.appendChild( rotationNode ); rotationNode.appendChild( doc.createTextNode( QString::number( (int)m_rotation ) ) ); } // ... save history up to OKULAR_HISTORY_SAVEDSTEPS viewports QLinkedList< DocumentViewport >::const_iterator backIterator = m_viewportIterator; if ( backIterator != m_viewportHistory.constEnd() ) { // go back up to OKULAR_HISTORY_SAVEDSTEPS steps from the current viewportIterator int backSteps = OKULAR_HISTORY_SAVEDSTEPS; while ( backSteps-- && backIterator != m_viewportHistory.constBegin() ) --backIterator; // create history root node QDomElement historyNode = doc.createElement( QStringLiteral("history") ); generalInfo.appendChild( historyNode ); // add old[backIterator] and present[viewportIterator] items QLinkedList< DocumentViewport >::const_iterator endIt = m_viewportIterator; ++endIt; while ( backIterator != endIt ) { QString name = (backIterator == m_viewportIterator) ? QStringLiteral ("current") : QStringLiteral ("oldPage"); QDomElement historyEntry = doc.createElement( name ); historyEntry.setAttribute( QStringLiteral("viewport"), (*backIterator).toString() ); historyNode.appendChild( historyEntry ); ++backIterator; } } // create views root node QDomElement viewsNode = doc.createElement( QStringLiteral("views") ); generalInfo.appendChild( viewsNode ); Q_FOREACH ( View * view, m_views ) { QDomElement viewEntry = doc.createElement( QStringLiteral("view") ); viewEntry.setAttribute( QStringLiteral("name"), view->name() ); viewsNode.appendChild( viewEntry ); saveViewsInfo( view, viewEntry ); } // 3. Save DOM to XML file QString xml = doc.toString(); QTextStream os( &infoFile ); os.setCodec( "UTF-8" ); os << xml; infoFile.close(); } void DocumentPrivate::slotTimedMemoryCheck() { // [MEM] clean memory (for 'free mem dependant' profiles only) if ( SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Low && m_allocatedPixmapsTotalMemory > 1024*1024 ) cleanupPixmapMemory(); } void DocumentPrivate::sendGeneratorPixmapRequest() { /* If the pixmap cache will have to be cleaned in order to make room for the * next request, get the distance from the current viewport of the page * whose pixmap will be removed. We will ignore preload requests for pages * that are at the same distance or farther */ const qulonglong memoryToFree = calculateMemoryToFree(); const int currentViewportPage = (*m_viewportIterator).pageNumber; int maxDistance = INT_MAX; // Default: No maximum if ( memoryToFree ) { AllocatedPixmap *pixmapToReplace = searchLowestPriorityPixmap( true ); if ( pixmapToReplace ) maxDistance = qAbs( pixmapToReplace->page - currentViewportPage ); } // find a request PixmapRequest * request = nullptr; m_pixmapRequestsMutex.lock(); while ( !m_pixmapRequestsStack.isEmpty() && !request ) { PixmapRequest * r = m_pixmapRequestsStack.last(); if (!r) { m_pixmapRequestsStack.pop_back(); continue; } QRect requestRect = r->isTile() ? r->normalizedRect().geometry( r->width(), r->height() ) : QRect( 0, 0, r->width(), r->height() ); TilesManager *tilesManager = r->d->tilesManager(); // If it's a preload but the generator is not threaded no point in trying to preload if ( r->preload() && !m_generator->hasFeature( Generator::Threaded ) ) { m_pixmapRequestsStack.pop_back(); delete r; } // request only if page isn't already present and request has valid id // request only if page isn't already present and request has valid id else if ( ( !r->d->mForce && r->page()->hasPixmap( r->observer(), r->width(), r->height(), r->normalizedRect() ) ) || !m_observers.contains(r->observer()) ) { m_pixmapRequestsStack.pop_back(); delete r; } else if ( !r->d->mForce && r->preload() && qAbs( r->pageNumber() - currentViewportPage ) >= maxDistance ) { m_pixmapRequestsStack.pop_back(); //qCDebug(OkularCoreDebug) << "Ignoring request that doesn't fit in cache"; delete r; } // Ignore requests for pixmaps that are already being generated else if ( tilesManager && tilesManager->isRequesting( r->normalizedRect(), r->width(), r->height() ) ) { m_pixmapRequestsStack.pop_back(); delete r; } // If the requested area is above 8000000 pixels, switch on the tile manager else if ( !tilesManager && m_generator->hasFeature( Generator::TiledRendering ) && (long)r->width() * (long)r->height() > 8000000L ) { // if the image is too big. start using tiles qCDebug(OkularCoreDebug).nospace() << "Start using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);"; // fill the tiles manager with the last rendered pixmap const QPixmap *pixmap = r->page()->_o_nearestPixmap( r->observer(), r->width(), r->height() ); if ( pixmap ) { tilesManager = new TilesManager( r->pageNumber(), pixmap->width(), pixmap->height(), r->page()->rotation() ); tilesManager->setPixmap( pixmap, NormalizedRect( 0, 0, 1, 1 ) ); tilesManager->setSize( r->width(), r->height() ); } else { // create new tiles manager tilesManager = new TilesManager( r->pageNumber(), r->width(), r->height(), r->page()->rotation() ); } tilesManager->setRequest( r->normalizedRect(), r->width(), r->height() ); r->page()->deletePixmap( r->observer() ); r->page()->d->setTilesManager( r->observer(), tilesManager ); r->setTile( true ); // Change normalizedRect to the smallest rect that contains all // visible tiles. if ( !r->normalizedRect().isNull() ) { NormalizedRect tilesRect; const QList tiles = tilesManager->tilesAt( r->normalizedRect(), TilesManager::TerminalTile ); QList::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd(); while ( tIt != tEnd ) { Tile tile = *tIt; if ( tilesRect.isNull() ) tilesRect = tile.rect(); else tilesRect |= tile.rect(); ++tIt; } r->setNormalizedRect( tilesRect ); request = r; } else { // Discard request if normalizedRect is null. This happens in // preload requests issued by PageView if the requested page is // not visible and the user has just switched from a non-tiled // zoom level to a tiled one m_pixmapRequestsStack.pop_back(); delete r; } } // If the requested area is below 6000000 pixels, switch off the tile manager else if ( tilesManager && (long)r->width() * (long)r->height() < 6000000L ) { qCDebug(OkularCoreDebug).nospace() << "Stop using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);"; // page is too small. stop using tiles. r->page()->deletePixmap( r->observer() ); r->setTile( false ); request = r; } else if ( (long)requestRect.width() * (long)requestRect.height() > 200000000L && (SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Greedy ) ) { m_pixmapRequestsStack.pop_back(); if ( !m_warnedOutOfMemory ) { qCWarning(OkularCoreDebug).nospace() << "Running out of memory on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);"; qCWarning(OkularCoreDebug) << "this message will be reported only once."; m_warnedOutOfMemory = true; } delete r; } else { request = r; } } // if no request found (or already generated), return if ( !request ) { m_pixmapRequestsMutex.unlock(); return; } // [MEM] preventive memory freeing qulonglong pixmapBytes = 0; TilesManager * tm = request->d->tilesManager(); if ( tm ) pixmapBytes = tm->totalMemory(); else pixmapBytes = 4 * request->width() * request->height(); if ( pixmapBytes > (1024 * 1024) ) cleanupPixmapMemory( memoryToFree /* previously calculated value */ ); // submit the request to the generator if ( m_generator->canGeneratePixmap() ) { QRect requestRect = !request->isTile() ? QRect(0, 0, request->width(), request->height() ) : request->normalizedRect().geometry( request->width(), request->height() ); qCDebug(OkularCoreDebug).nospace() << "sending request observer=" << request->observer() << " " <pageNumber() << " async == " << request->asynchronous() << " isTile == " << request->isTile(); m_pixmapRequestsStack.removeAll ( request ); if ( tm ) tm->setRequest( request->normalizedRect(), request->width(), request->height() ); if ( (int)m_rotation % 2 ) request->d->swap(); if ( m_rotation != Rotation0 && !request->normalizedRect().isNull() ) request->setNormalizedRect( TilesManager::fromRotatedRect( request->normalizedRect(), m_rotation ) ); request->setPartialUpdatesWanted( request->asynchronous() && !request->page()->hasPixmap( request->observer() ) ); // we always have to unlock _before_ the generatePixmap() because // a sync generation would end with requestDone() -> deadlock, and // we can not really know if the generator can do async requests m_executingPixmapRequests.push_back( request ); m_pixmapRequestsMutex.unlock(); m_generator->generatePixmap( request ); } else { m_pixmapRequestsMutex.unlock(); // pino (7/4/2006): set the polling interval from 10 to 30 QTimer::singleShot( 30, m_parent, SLOT(sendGeneratorPixmapRequest()) ); } } void DocumentPrivate::rotationFinished( int page, Okular::Page *okularPage ) { Okular::Page *wantedPage = m_pagesVector.value( page, 0 ); if ( !wantedPage || wantedPage != okularPage ) return; foreach(DocumentObserver *o, m_observers) o->notifyPageChanged( page, DocumentObserver::Pixmap | DocumentObserver::Annotations ); } void DocumentPrivate::slotFontReadingProgress( int page ) { emit m_parent->fontReadingProgress( page ); if ( page >= (int)m_parent->pages() - 1 ) { emit m_parent->fontReadingEnded(); m_fontThread = nullptr; m_fontsCached = true; } } void DocumentPrivate::fontReadingGotFont( const Okular::FontInfo& font ) { // Try to avoid duplicate fonts if (m_fontsCache.indexOf(font) == -1) { m_fontsCache.append( font ); emit m_parent->gotFont( font ); } } void DocumentPrivate::slotGeneratorConfigChanged( const QString& ) { if ( !m_generator ) return; // reparse generator config and if something changed clear Pages bool configchanged = false; QHash< QString, GeneratorInfo >::iterator it = m_loadedGenerators.begin(), itEnd = m_loadedGenerators.end(); for ( ; it != itEnd; ++it ) { Okular::ConfigInterface * iface = generatorConfig( it.value() ); if ( iface ) { bool it_changed = iface->reparseConfig(); if ( it_changed && ( m_generator == it.value().generator ) ) configchanged = true; } } if ( configchanged ) { // invalidate pixmaps QVector::const_iterator it = m_pagesVector.constBegin(), end = m_pagesVector.constEnd(); for ( ; it != end; ++it ) { (*it)->deletePixmaps(); } // [MEM] remove allocation descriptors qDeleteAll( m_allocatedPixmaps ); m_allocatedPixmaps.clear(); m_allocatedPixmapsTotalMemory = 0; // send reload signals to observers foreachObserverD( notifyContentsCleared( DocumentObserver::Pixmap ) ); } // free memory if in 'low' profile if ( SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !m_allocatedPixmaps.isEmpty() && !m_pagesVector.isEmpty() ) cleanupPixmapMemory(); } void DocumentPrivate::refreshPixmaps( int pageNumber ) { Page* page = m_pagesVector.value( pageNumber, 0 ); if ( !page ) return; QLinkedList< Okular::PixmapRequest * > requestedPixmaps; QMap< DocumentObserver*, PagePrivate::PixmapObject >::ConstIterator it = page->d->m_pixmaps.constBegin(), itEnd = page->d->m_pixmaps.constEnd(); for ( ; it != itEnd; ++it ) { QSize size = (*it).m_pixmap->size(); PixmapRequest * p = new PixmapRequest( it.key(), pageNumber, size.width() / qApp->devicePixelRatio(), size.height() / qApp->devicePixelRatio(), 1, PixmapRequest::Asynchronous ); p->d->mForce = true; requestedPixmaps.push_back( p ); } foreach (DocumentObserver *observer, m_observers) { TilesManager *tilesManager = page->d->tilesManager( observer ); if ( tilesManager ) { tilesManager->markDirty(); PixmapRequest * p = new PixmapRequest( observer, pageNumber, tilesManager->width() / qApp->devicePixelRatio(), tilesManager->height() / qApp->devicePixelRatio(), 1, PixmapRequest::Asynchronous ); NormalizedRect tilesRect; // Get the visible page rect NormalizedRect visibleRect; QVector< Okular::VisiblePageRect * >::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) { if ( (*vIt)->pageNumber == pageNumber ) { visibleRect = (*vIt)->rect; break; } } if ( !visibleRect.isNull() ) { p->setNormalizedRect( visibleRect ); p->setTile( true ); p->d->mForce = true; requestedPixmaps.push_back( p ); } else { delete p; } } } if ( !requestedPixmaps.isEmpty() ) m_parent->requestPixmaps( requestedPixmaps, Okular::Document::NoOption ); } void DocumentPrivate::_o_configChanged() { // free text pages if needed calculateMaxTextPages(); while (m_allocatedTextPagesFifo.count() > m_maxAllocatedTextPages) { int pageToKick = m_allocatedTextPagesFifo.takeFirst(); m_pagesVector.at(pageToKick)->setTextPage( nullptr ); // deletes the textpage } } void DocumentPrivate::doContinueDirectionMatchSearch(void *doContinueDirectionMatchSearchStruct) { DoContinueDirectionMatchSearchStruct *searchStruct = static_cast(doContinueDirectionMatchSearchStruct); RunningSearch *search = m_searches.value(searchStruct->searchID); if ((m_searchCancelled && !searchStruct->match) || !search) { // if the user cancelled but he just got a match, give him the match! QApplication::restoreOverrideCursor(); if (search) search->isCurrentlySearching = false; emit m_parent->searchFinished( searchStruct->searchID, Document::SearchCancelled ); delete searchStruct->pagesToNotify; delete searchStruct; return; } const bool forward = search->cachedType == Document::NextMatch; bool doContinue = false; // if no match found, loop through the whole doc, starting from currentPage if ( !searchStruct->match ) { const int pageCount = m_pagesVector.count(); if (search->pagesDone < pageCount) { doContinue = true; if ( searchStruct->currentPage >= pageCount ) { searchStruct->currentPage = 0; emit m_parent->notice(i18n("Continuing search from beginning"), 3000); } else if ( searchStruct->currentPage < 0 ) { searchStruct->currentPage = pageCount - 1; emit m_parent->notice(i18n("Continuing search from bottom"), 3000); } } } if (doContinue) { // get page Page * page = m_pagesVector[ searchStruct->currentPage ]; // request search page if needed if ( !page->hasTextPage() ) m_parent->requestTextPage( page->number() ); // if found a match on the current page, end the loop searchStruct->match = page->findText( searchStruct->searchID, search->cachedString, forward ? FromTop : FromBottom, search->cachedCaseSensitivity ); if ( !searchStruct->match ) { if (forward) searchStruct->currentPage++; else searchStruct->currentPage--; search->pagesDone++; } else { search->pagesDone = 1; } // Both of the previous if branches need to call doContinueDirectionMatchSearch QMetaObject::invokeMethod(m_parent, "doContinueDirectionMatchSearch", Qt::QueuedConnection, Q_ARG(void *, searchStruct)); } else { doProcessSearchMatch( searchStruct->match, search, searchStruct->pagesToNotify, searchStruct->currentPage, searchStruct->searchID, search->cachedViewportMove, search->cachedColor ); delete searchStruct; } } void DocumentPrivate::doProcessSearchMatch( RegularAreaRect *match, RunningSearch *search, QSet< int > *pagesToNotify, int currentPage, int searchID, bool moveViewport, const QColor & color ) { // reset cursor to previous shape QApplication::restoreOverrideCursor(); bool foundAMatch = false; search->isCurrentlySearching = false; // if a match has been found.. if ( match ) { // update the RunningSearch structure adding this match.. foundAMatch = true; search->continueOnPage = currentPage; search->continueOnMatch = *match; search->highlightedPages.insert( currentPage ); // ..add highlight to the page.. m_pagesVector[ currentPage ]->d->setHighlight( searchID, match, color ); // ..queue page for notifying changes.. pagesToNotify->insert( currentPage ); // Create a normalized rectangle around the search match that includes a 5% buffer on all sides. const Okular::NormalizedRect matchRectWithBuffer = Okular::NormalizedRect( match->first().left - 0.05, match->first().top - 0.05, match->first().right + 0.05, match->first().bottom + 0.05 ); const bool matchRectFullyVisible = isNormalizedRectangleFullyVisible( matchRectWithBuffer, currentPage ); // ..move the viewport to show the first of the searched word sequence centered if ( moveViewport && !matchRectFullyVisible ) { DocumentViewport searchViewport( currentPage ); searchViewport.rePos.enabled = true; searchViewport.rePos.normalizedX = (match->first().left + match->first().right) / 2.0; searchViewport.rePos.normalizedY = (match->first().top + match->first().bottom) / 2.0; m_parent->setViewport( searchViewport, nullptr, true ); } delete match; } // notify observers about highlights changes foreach(int pageNumber, *pagesToNotify) foreach(DocumentObserver *observer, m_observers) observer->notifyPageChanged( pageNumber, DocumentObserver::Highlights ); if (foundAMatch) emit m_parent->searchFinished( searchID, Document::MatchFound ); else emit m_parent->searchFinished( searchID, Document::NoMatchFound ); delete pagesToNotify; } void DocumentPrivate::doContinueAllDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID) { QMap< Page *, QVector > *pageMatches = static_cast< QMap< Page *, QVector > * >(pageMatchesMap); QSet< int > *pagesToNotify = static_cast< QSet< int > * >( pagesToNotifySet ); RunningSearch *search = m_searches.value(searchID); if (m_searchCancelled || !search) { typedef QVector MatchesVector; QApplication::restoreOverrideCursor(); if (search) search->isCurrentlySearching = false; emit m_parent->searchFinished( searchID, Document::SearchCancelled ); foreach(const MatchesVector &mv, *pageMatches) qDeleteAll(mv); delete pageMatches; delete pagesToNotify; return; } if (currentPage < m_pagesVector.count()) { // get page (from the first to the last) Page *page = m_pagesVector.at(currentPage); int pageNumber = page->number(); // redundant? is it == currentPage ? // request search page if needed if ( !page->hasTextPage() ) m_parent->requestTextPage( pageNumber ); // loop on a page adding highlights for all found items RegularAreaRect * lastMatch = nullptr; while ( 1 ) { if ( lastMatch ) lastMatch = page->findText( searchID, search->cachedString, NextResult, search->cachedCaseSensitivity, lastMatch ); else lastMatch = page->findText( searchID, search->cachedString, FromTop, search->cachedCaseSensitivity ); if ( !lastMatch ) break; // add highligh rect to the matches map (*pageMatches)[page].append(lastMatch); } delete lastMatch; QMetaObject::invokeMethod(m_parent, "doContinueAllDocumentSearch", Qt::QueuedConnection, Q_ARG(void *, pagesToNotifySet), Q_ARG(void *, pageMatches), Q_ARG(int, currentPage + 1), Q_ARG(int, searchID)); } else { // reset cursor to previous shape QApplication::restoreOverrideCursor(); search->isCurrentlySearching = false; bool foundAMatch = pageMatches->count() != 0; QMap< Page *, QVector >::const_iterator it, itEnd; it = pageMatches->constBegin(); itEnd = pageMatches->constEnd(); for ( ; it != itEnd; ++it) { foreach(RegularAreaRect *match, it.value()) { it.key()->d->setHighlight( searchID, match, search->cachedColor ); delete match; } search->highlightedPages.insert( it.key()->number() ); pagesToNotify->insert( it.key()->number() ); } foreach(DocumentObserver *observer, m_observers) observer->notifySetup( m_pagesVector, 0 ); // notify observers about highlights changes foreach(int pageNumber, *pagesToNotify) foreach(DocumentObserver *observer, m_observers) observer->notifyPageChanged( pageNumber, DocumentObserver::Highlights ); if (foundAMatch) emit m_parent->searchFinished(searchID, Document::MatchFound ); else emit m_parent->searchFinished( searchID, Document::NoMatchFound ); delete pageMatches; delete pagesToNotify; } } void DocumentPrivate::doContinueGooglesDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID, const QStringList & words) { typedef QPair MatchColor; QMap< Page *, QVector > *pageMatches = static_cast< QMap< Page *, QVector > * >(pageMatchesMap); QSet< int > *pagesToNotify = static_cast< QSet< int > * >( pagesToNotifySet ); RunningSearch *search = m_searches.value(searchID); if (m_searchCancelled || !search) { typedef QVector MatchesVector; QApplication::restoreOverrideCursor(); if (search) search->isCurrentlySearching = false; emit m_parent->searchFinished( searchID, Document::SearchCancelled ); foreach(const MatchesVector &mv, *pageMatches) { foreach(const MatchColor &mc, mv) delete mc.first; } delete pageMatches; delete pagesToNotify; return; } const int wordCount = words.count(); const int hueStep = (wordCount > 1) ? (60 / (wordCount - 1)) : 60; int baseHue, baseSat, baseVal; search->cachedColor.getHsv( &baseHue, &baseSat, &baseVal ); if (currentPage < m_pagesVector.count()) { // get page (from the first to the last) Page *page = m_pagesVector.at(currentPage); int pageNumber = page->number(); // redundant? is it == currentPage ? // request search page if needed if ( !page->hasTextPage() ) m_parent->requestTextPage( pageNumber ); // loop on a page adding highlights for all found items bool allMatched = wordCount > 0, anyMatched = false; for ( int w = 0; w < wordCount; w++ ) { const QString &word = words[ w ]; int newHue = baseHue - w * hueStep; if ( newHue < 0 ) newHue += 360; QColor wordColor = QColor::fromHsv( newHue, baseSat, baseVal ); RegularAreaRect * lastMatch = nullptr; // add all highlights for current word bool wordMatched = false; while ( 1 ) { if ( lastMatch ) lastMatch = page->findText( searchID, word, NextResult, search->cachedCaseSensitivity, lastMatch ); else lastMatch = page->findText( searchID, word, FromTop, search->cachedCaseSensitivity); if ( !lastMatch ) break; // add highligh rect to the matches map (*pageMatches)[page].append(MatchColor(lastMatch, wordColor)); wordMatched = true; } allMatched = allMatched && wordMatched; anyMatched = anyMatched || wordMatched; } // if not all words are present in page, remove partial highlights const bool matchAll = search->cachedType == Document::GoogleAll; if ( !allMatched && matchAll ) { QVector &matches = (*pageMatches)[page]; foreach(const MatchColor &mc, matches) delete mc.first; pageMatches->remove(page); } QMetaObject::invokeMethod(m_parent, "doContinueGooglesDocumentSearch", Qt::QueuedConnection, Q_ARG(void *, pagesToNotifySet), Q_ARG(void *, pageMatches), Q_ARG(int, currentPage + 1), Q_ARG(int, searchID), Q_ARG(QStringList, words)); } else { // reset cursor to previous shape QApplication::restoreOverrideCursor(); search->isCurrentlySearching = false; bool foundAMatch = pageMatches->count() != 0; QMap< Page *, QVector >::const_iterator it, itEnd; it = pageMatches->constBegin(); itEnd = pageMatches->constEnd(); for ( ; it != itEnd; ++it) { foreach(const MatchColor &mc, it.value()) { it.key()->d->setHighlight( searchID, mc.first, mc.second ); delete mc.first; } search->highlightedPages.insert( it.key()->number() ); pagesToNotify->insert( it.key()->number() ); } // send page lists to update observers (since some filter on bookmarks) foreach(DocumentObserver *observer, m_observers) observer->notifySetup( m_pagesVector, 0 ); // notify observers about highlights changes foreach(int pageNumber, *pagesToNotify) foreach(DocumentObserver *observer, m_observers) observer->notifyPageChanged( pageNumber, DocumentObserver::Highlights ); if (foundAMatch) emit m_parent->searchFinished( searchID, Document::MatchFound ); else emit m_parent->searchFinished( searchID, Document::NoMatchFound ); delete pageMatches; delete pagesToNotify; } } QVariant DocumentPrivate::documentMetaData( const Generator::DocumentMetaDataKey key, const QVariant &option ) const { switch ( key ) { case Generator::PaperColorMetaData: { bool giveDefault = option.toBool(); QColor color; if ( ( SettingsCore::renderMode() == SettingsCore::EnumRenderMode::Paper ) && SettingsCore::changeColors() ) { color = SettingsCore::paperColor(); } else if ( giveDefault ) { color = Qt::white; } return color; } break; case Generator::TextAntialiasMetaData: switch ( SettingsCore::textAntialias() ) { case SettingsCore::EnumTextAntialias::Enabled: return true; break; #if 0 case Settings::EnumTextAntialias::UseKDESettings: // TODO: read the KDE configuration return true; break; #endif case SettingsCore::EnumTextAntialias::Disabled: return false; break; } break; case Generator::GraphicsAntialiasMetaData: switch ( SettingsCore::graphicsAntialias() ) { case SettingsCore::EnumGraphicsAntialias::Enabled: return true; break; case SettingsCore::EnumGraphicsAntialias::Disabled: return false; break; } break; case Generator::TextHintingMetaData: switch ( SettingsCore::textHinting() ) { case SettingsCore::EnumTextHinting::Enabled: return true; break; case SettingsCore::EnumTextHinting::Disabled: return false; break; } break; } return QVariant(); } bool DocumentPrivate::isNormalizedRectangleFullyVisible( const Okular::NormalizedRect & rectOfInterest, int rectPage ) { bool rectFullyVisible = false; const QVector & visibleRects = m_parent->visiblePageRects(); QVector::const_iterator vEnd = visibleRects.end(); QVector::const_iterator vIt = visibleRects.begin(); for ( ; ( vIt != vEnd ) && !rectFullyVisible; ++vIt ) { if ( (*vIt)->pageNumber == rectPage && (*vIt)->rect.contains( rectOfInterest.left, rectOfInterest.top ) && (*vIt)->rect.contains( rectOfInterest.right, rectOfInterest.bottom ) ) { rectFullyVisible = true; } } return rectFullyVisible; } struct pdfsyncpoint { QString file; qlonglong x; qlonglong y; int row; int column; int page; }; void DocumentPrivate::loadSyncFile( const QString & filePath ) { QFile f( filePath + QLatin1String( "sync" ) ); if ( !f.open( QIODevice::ReadOnly ) ) return; QTextStream ts( &f ); // first row: core name of the pdf output const QString coreName = ts.readLine(); // second row: version string, in the form 'Version %u' QString versionstr = ts.readLine(); QRegExp versionre( QStringLiteral("Version (\\d+)") ); versionre.setCaseSensitivity( Qt::CaseInsensitive ); if ( !versionre.exactMatch( versionstr ) ) return; QHash points; QStack fileStack; int currentpage = -1; const QLatin1String texStr( ".tex" ); const QChar spaceChar = QChar::fromLatin1( ' ' ); fileStack.push( coreName + texStr ); const QSizeF dpi = m_generator->dpi(); QString line; while ( !ts.atEnd() ) { line = ts.readLine(); const QStringList tokens = line.split( spaceChar, QString::SkipEmptyParts ); const int tokenSize = tokens.count(); if ( tokenSize < 1 ) continue; if ( tokens.first() == QLatin1String( "l" ) && tokenSize >= 3 ) { int id = tokens.at( 1 ).toInt(); QHash::const_iterator it = points.constFind( id ); if ( it == points.constEnd() ) { pdfsyncpoint pt; pt.x = 0; pt.y = 0; pt.row = tokens.at( 2 ).toInt(); pt.column = 0; // TODO pt.page = -1; pt.file = fileStack.top(); points[ id ] = pt; } } else if ( tokens.first() == QLatin1String( "s" ) && tokenSize >= 2 ) { currentpage = tokens.at( 1 ).toInt() - 1; } else if ( tokens.first() == QLatin1String( "p*" ) && tokenSize >= 4 ) { // TODO qCDebug(OkularCoreDebug) << "PdfSync: 'p*' line ignored"; } else if ( tokens.first() == QLatin1String( "p" ) && tokenSize >= 4 ) { int id = tokens.at( 1 ).toInt(); QHash::iterator it = points.find( id ); if ( it != points.end() ) { it->x = tokens.at( 2 ).toInt(); it->y = tokens.at( 3 ).toInt(); it->page = currentpage; } } else if ( line.startsWith( QLatin1Char( '(' ) ) && tokenSize == 1 ) { QString newfile = line; // chop the leading '(' newfile.remove( 0, 1 ); if ( !newfile.endsWith( texStr ) ) { newfile += texStr; } fileStack.push( newfile ); } else if ( line == QLatin1String( ")" ) ) { if ( !fileStack.isEmpty() ) { fileStack.pop(); } else qCDebug(OkularCoreDebug) << "PdfSync: going one level down too much"; } else qCDebug(OkularCoreDebug).nospace() << "PdfSync: unknown line format: '" << line << "'"; } QVector< QLinkedList< Okular::SourceRefObjectRect * > > refRects( m_pagesVector.size() ); foreach ( const pdfsyncpoint& pt, points ) { // drop pdfsync points not completely valid if ( pt.page < 0 || pt.page >= m_pagesVector.size() ) continue; // magic numbers for TeX's RSU's (Ridiculously Small Units) conversion to pixels Okular::NormalizedPoint p( ( pt.x * dpi.width() ) / ( 72.27 * 65536.0 * m_pagesVector[pt.page]->width() ), ( pt.y * dpi.height() ) / ( 72.27 * 65536.0 * m_pagesVector[pt.page]->height() ) ); QString file = pt.file; Okular::SourceReference * sourceRef = new Okular::SourceReference( file, pt.row, pt.column ); refRects[ pt.page ].append( new Okular::SourceRefObjectRect( p, sourceRef ) ); } for ( int i = 0; i < refRects.size(); ++i ) if ( !refRects.at(i).isEmpty() ) m_pagesVector[i]->setSourceReferences( refRects.at(i) ); } Document::Document( QWidget *widget ) : QObject( nullptr ), d( new DocumentPrivate( this ) ) { d->m_widget = widget; d->m_bookmarkManager = new BookmarkManager( d ); d->m_viewportIterator = d->m_viewportHistory.insert( d->m_viewportHistory.end(), DocumentViewport() ); d->m_undoStack = new QUndoStack(this); connect( SettingsCore::self(), SIGNAL(configChanged()), this, SLOT(_o_configChanged()) ); connect(d->m_undoStack, &QUndoStack::canUndoChanged, this, &Document::canUndoChanged); connect(d->m_undoStack, &QUndoStack::canRedoChanged, this, &Document::canRedoChanged); + connect(d->m_undoStack, &QUndoStack::cleanChanged, this, &Document::undoHistoryCleanChanged); qRegisterMetaType(); } Document::~Document() { // delete generator, pages, and related stuff closeDocument(); QSet< View * >::const_iterator viewIt = d->m_views.constBegin(), viewEnd = d->m_views.constEnd(); for ( ; viewIt != viewEnd; ++viewIt ) { View *v = *viewIt; v->d_func()->document = nullptr; } // delete the bookmark manager delete d->m_bookmarkManager; // delete the loaded generators QHash< QString, GeneratorInfo >::const_iterator it = d->m_loadedGenerators.constBegin(), itEnd = d->m_loadedGenerators.constEnd(); for ( ; it != itEnd; ++it ) d->unloadGenerator( it.value() ); d->m_loadedGenerators.clear(); // delete the private structure delete d; } QString DocumentPrivate::docDataFileName(const QUrl &url, qint64 document_size) { QString fn = url.fileName(); fn = QString::number( document_size ) + QLatin1Char('.') + fn + QStringLiteral(".xml"); QString docdataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/okular/docdata"); // make sure that the okular/docdata/ directory exists (probably this used to be handled by KStandardDirs) if (!QFileInfo::exists(docdataDir)) { qCDebug(OkularCoreDebug) << "creating docdata folder" << docdataDir; QDir().mkpath(docdataDir); } QString newokularfile = docdataDir + QLatin1Char('/') + fn; // we don't want to accidentally migrate old files when running unit tests if (!QFile::exists( newokularfile ) && !QStandardPaths::isTestModeEnabled()) { // see if an KDE4 file still exists static Kdelibs4Migration k4migration; QString oldfile = k4migration.locateLocal("data", QStringLiteral("okular/docdata/") + fn); if (oldfile.isEmpty()) { oldfile = k4migration.locateLocal("data", QStringLiteral("kpdf/") + fn); } if ( !oldfile.isEmpty() && QFile::exists( oldfile ) ) { // ### copy or move? if ( !QFile::copy( oldfile, newokularfile ) ) return QString(); } } return newokularfile; } QVector DocumentPrivate::availableGenerators() { static QVector result; if (result.isEmpty()) { result = KPluginLoader::findPlugins( QLatin1String ( "okular/generators" ) ); } return result; } KPluginMetaData DocumentPrivate::generatorForMimeType(const QMimeType& type, QWidget* widget, const QVector &triedOffers) { // First try to find an exact match, and then look for more general ones (e. g. the plain text one) // Ideally we would rank these by "closeness", but that might be overdoing it const QVector available = availableGenerators(); QVector offers; QVector exactMatches; QMimeDatabase mimeDatabase; for (const KPluginMetaData& md : available) { if (triedOffers.contains(md)) continue; foreach (const QString& supported, md.mimeTypes()) { QMimeType mimeType = mimeDatabase.mimeTypeForName(supported); if (mimeType == type && !exactMatches.contains(md)) { exactMatches << md; } if (type.inherits(supported) && !offers.contains(md)) { offers << md; } } } if (!exactMatches.isEmpty()) { offers = exactMatches; } if (offers.isEmpty()) { return KPluginMetaData(); } int hRank=0; // best ranked offer search int offercount = offers.size(); if (offercount > 1) { // sort the offers: the offers with an higher priority come before auto cmp = [](const KPluginMetaData& s1, const KPluginMetaData& s2) { const QString property = QStringLiteral("X-KDE-Priority"); return s1.rawData()[property].toInt() > s2.rawData()[property].toInt(); }; std::stable_sort(offers.begin(), offers.end(), cmp); if (SettingsCore::chooseGenerators()) { QStringList list; for (int i = 0; i < offercount; ++i) { list << offers.at(i).pluginId(); } ChooseEngineDialog choose(list, type, widget); if (choose.exec() == QDialog::Rejected) return KPluginMetaData(); hRank = choose.selectedGenerator(); } } Q_ASSERT(hRank < offers.size()); return offers.at(hRank); } Document::OpenResult Document::openDocument(const QString & docFile, const QUrl &url, const QMimeType &_mime, const QString & password ) { QMimeDatabase db; QMimeType mime = _mime; QByteArray filedata; - qint64 document_size = -1; bool isstdin = url.fileName() == QLatin1String( "-" ); bool triedMimeFromFileContent = false; if ( !isstdin ) { if ( !mime.isValid() ) return OpenError; - // docFile is always local so we can use QFileInfo on it - QFileInfo fileReadTest( docFile ); - if ( fileReadTest.isFile() && !fileReadTest.isReadable() ) - { - d->m_docFileName.clear(); - return OpenError; - } - // determine the related "xml document-info" filename d->m_url = url; d->m_docFileName = docFile; - if ( url.isLocalFile() && !d->m_archiveData ) - { - document_size = fileReadTest.size(); - d->m_xmlFileName = DocumentPrivate::docDataFileName(url, document_size); - } + + if ( !d->updateMetadataXmlNameAndDocSize() ) + return OpenError; } else { QFile qstdin; qstdin.open( stdin, QIODevice::ReadOnly ); filedata = qstdin.readAll(); mime = db.mimeTypeForData( filedata ); if ( !mime.isValid() || mime.isDefault() ) return OpenError; - document_size = filedata.size(); + d->m_docSize = filedata.size(); triedMimeFromFileContent = true; } // 0. load Generator // request only valid non-disabled plugins suitable for the mimetype KPluginMetaData offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget); if ( !offer.isValid() && !triedMimeFromFileContent ) { QMimeType newmime = db.mimeTypeForFile(docFile, QMimeDatabase::MatchExtension); triedMimeFromFileContent = true; if ( newmime != mime ) { mime = newmime; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget); } if ( !offer.isValid() ) { // There's still no offers, do a final mime search based on the filename // We need this because sometimes (e.g. when downloading from a webserver) the mimetype we // use is the one fed by the server, that may be wrong newmime = db.mimeTypeForUrl( url ); if ( !newmime.isDefault() && newmime != mime ) { mime = newmime; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget); } } } if (!offer.isValid()) { emit error( i18n( "Can not find a plugin which is able to handle the document being passed." ), -1 ); qCWarning(OkularCoreDebug).nospace() << "No plugin for mimetype '" << mime.name() << "'."; return OpenError; } // 1. load Document OpenResult openResult = d->openDocumentInternal( offer, isstdin, docFile, filedata, password ); if ( openResult == OpenError ) { QVector triedOffers; triedOffers << offer; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); while ( offer.isValid() ) { openResult = d->openDocumentInternal( offer, isstdin, docFile, filedata, password ); if ( openResult == OpenError ) { triedOffers << offer; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); } else break; } if (openResult == OpenError && !triedMimeFromFileContent ) { QMimeType newmime = db.mimeTypeForFile(docFile, QMimeDatabase::MatchExtension); triedMimeFromFileContent = true; if ( newmime != mime ) { mime = newmime; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); while ( offer.isValid() ) { openResult = d->openDocumentInternal( offer, isstdin, docFile, filedata, password ); if ( openResult == OpenError ) { triedOffers << offer; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); } else break; } } } if ( openResult == OpenSuccess ) { // Clear errors, since we're trying various generators, maybe one of them errored out // but we finally succeeded // TODO one can still see the error message animating out but since this is a very rare // condition we can leave this for future work emit error( QString(), -1 ); } } if ( openResult != OpenSuccess ) { return openResult; } // no need to check for the existence of a synctex file, no parser will be // created if none exists d->m_synctex_scanner = synctex_scanner_new_with_output_file( QFile::encodeName( docFile ).constData(), nullptr, 1); if ( !d->m_synctex_scanner && QFile::exists(docFile + QLatin1String( "sync" ) ) ) { d->loadSyncFile(docFile); } d->m_generatorName = offer.pluginId(); d->m_pageController = new PageController(); connect( d->m_pageController, SIGNAL(rotationFinished(int,Okular::Page*)), this, SLOT(rotationFinished(int,Okular::Page*)) ); - bool containsExternalAnnotations = false; foreach ( Page * p, d->m_pagesVector ) - { p->d->m_doc = d; - if ( !p->annotations().empty() ) - containsExternalAnnotations = true; - } - // Be quiet while restoring local annotations - d->m_showWarningLimitedAnnotSupport = false; - d->m_annotationsNeedSaveAs = false; + d->m_metadataLoadingCompleted = false; + d->m_docdataMigrationNeeded = false; // 2. load Additional Data (bookmarks, local annotations and metadata) about the document if ( d->m_archiveData ) { - d->loadDocumentInfo( d->m_archiveData->metadataFile ); - d->m_annotationsNeedSaveAs = true; + d->loadDocumentInfo( d->m_archiveData->metadataFile, LoadPageInfo ); + d->loadDocumentInfo( LoadGeneralInfo ); } else { - d->loadDocumentInfo(); - d->m_annotationsNeedSaveAs = ( d->canAddAnnotationsNatively() && containsExternalAnnotations ); + if ( d->loadDocumentInfo( LoadPageInfo ) ) + d->m_docdataMigrationNeeded = true; + d->loadDocumentInfo( LoadGeneralInfo ); } - d->m_showWarningLimitedAnnotSupport = true; + d->m_metadataLoadingCompleted = true; d->m_bookmarkManager->setUrl( d->m_url ); // 3. setup observers inernal lists and data - foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged ) ); + foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ) ); // 4. set initial page (restoring the page saved in xml if loaded) DocumentViewport loadedViewport = (*d->m_viewportIterator); if ( loadedViewport.isValid() ) { (*d->m_viewportIterator) = DocumentViewport(); if ( loadedViewport.pageNumber >= (int)d->m_pagesVector.size() ) loadedViewport.pageNumber = d->m_pagesVector.size() - 1; } else loadedViewport.pageNumber = 0; setViewport( loadedViewport ); // start bookmark saver timer if ( !d->m_saveBookmarksTimer ) { d->m_saveBookmarksTimer = new QTimer( this ); connect( d->m_saveBookmarksTimer, SIGNAL(timeout()), this, SLOT(saveDocumentInfo()) ); } d->m_saveBookmarksTimer->start( 5 * 60 * 1000 ); // start memory check timer if ( !d->m_memCheckTimer ) { d->m_memCheckTimer = new QTimer( this ); connect( d->m_memCheckTimer, SIGNAL(timeout()), this, SLOT(slotTimedMemoryCheck()) ); } d->m_memCheckTimer->start( 2000 ); const DocumentViewport nextViewport = d->nextDocumentViewport(); if ( nextViewport.isValid() ) { setViewport( nextViewport ); d->m_nextDocumentViewport = DocumentViewport(); d->m_nextDocumentDestination = QString(); } AudioPlayer::instance()->d->m_currentDocument = isstdin ? QUrl() : d->m_url; - d->m_docSize = document_size; const QStringList docScripts = d->m_generator->metaData( QStringLiteral("DocumentScripts"), QStringLiteral ( "JavaScript" ) ).toStringList(); if ( !docScripts.isEmpty() ) { d->m_scripter = new Scripter( d ); Q_FOREACH ( const QString &docscript, docScripts ) { d->m_scripter->execute( JavaScript, docscript ); } } return OpenSuccess; } +bool DocumentPrivate::updateMetadataXmlNameAndDocSize() +{ + // m_docFileName is always local so we can use QFileInfo on it + QFileInfo fileReadTest( m_docFileName ); + if ( !fileReadTest.isFile() && !fileReadTest.isReadable() ) + return false; + + m_docSize = fileReadTest.size(); + + // determine the related "xml document-info" filename + if ( m_url.isLocalFile() ) + { + const QString filePath = docDataFileName( m_url, m_docSize ); + qCDebug(OkularCoreDebug) << "Metadata file is now:" << filePath; + m_xmlFileName = filePath; + } + else + { + qCDebug(OkularCoreDebug) << "Metadata file: disabled"; + m_xmlFileName = QString(); + } + + return true; +} + KXMLGUIClient* Document::guiClient() { if ( d->m_generator ) { Okular::GuiInterface * iface = qobject_cast< Okular::GuiInterface * >( d->m_generator ); if ( iface ) return iface->guiClient(); } return nullptr; } void Document::closeDocument() { // check if there's anything to close... if ( !d->m_generator ) return; delete d->m_pageController; d->m_pageController = nullptr; delete d->m_scripter; d->m_scripter = nullptr; // remove requests left in queue d->m_pixmapRequestsMutex.lock(); QLinkedList< PixmapRequest * >::const_iterator sIt = d->m_pixmapRequestsStack.constBegin(); QLinkedList< PixmapRequest * >::const_iterator sEnd = d->m_pixmapRequestsStack.constEnd(); for ( ; sIt != sEnd; ++sIt ) delete *sIt; d->m_pixmapRequestsStack.clear(); d->m_pixmapRequestsMutex.unlock(); QEventLoop loop; bool startEventLoop = false; do { d->m_pixmapRequestsMutex.lock(); startEventLoop = !d->m_executingPixmapRequests.isEmpty(); d->m_pixmapRequestsMutex.unlock(); if ( startEventLoop ) { d->m_closingLoop = &loop; loop.exec(); d->m_closingLoop = nullptr; } } while ( startEventLoop ); if ( d->m_fontThread ) { disconnect( d->m_fontThread, nullptr, this, nullptr ); d->m_fontThread->stopExtraction(); d->m_fontThread->wait(); d->m_fontThread = nullptr; } // stop any audio playback AudioPlayer::instance()->stopPlaybacks(); // close the current document and save document info if a document is still opened if ( d->m_generator && d->m_pagesVector.size() > 0 ) { d->saveDocumentInfo(); d->m_generator->closeDocument(); } if ( d->m_synctex_scanner ) { synctex_scanner_free( d->m_synctex_scanner ); d->m_synctex_scanner = nullptr; } // stop timers if ( d->m_memCheckTimer ) d->m_memCheckTimer->stop(); if ( d->m_saveBookmarksTimer ) d->m_saveBookmarksTimer->stop(); if ( d->m_generator ) { // disconnect the generator from this document ... d->m_generator->d_func()->m_document = nullptr; // .. and this document from the generator signals disconnect( d->m_generator, nullptr, this, nullptr ); QHash< QString, GeneratorInfo >::const_iterator genIt = d->m_loadedGenerators.constFind( d->m_generatorName ); Q_ASSERT( genIt != d->m_loadedGenerators.constEnd() ); } d->m_generator = nullptr; d->m_generatorName = QString(); d->m_url = QUrl(); d->m_walletGenerator = nullptr; d->m_docFileName = QString(); d->m_xmlFileName = QString(); delete d->m_tempFile; d->m_tempFile = nullptr; delete d->m_archiveData; d->m_archiveData = nullptr; d->m_docSize = -1; d->m_exportCached = false; d->m_exportFormats.clear(); d->m_exportToText = ExportFormat(); d->m_fontsCached = false; d->m_fontsCache.clear(); d->m_rotation = Rotation0; // send an empty list to observers (to free their data) - foreachObserver( notifySetup( QVector< Page * >(), DocumentObserver::DocumentChanged ) ); + foreachObserver( notifySetup( QVector< Page * >(), DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ) ); // delete pages and clear 'd->m_pagesVector' container QVector< Page * >::const_iterator pIt = d->m_pagesVector.constBegin(); QVector< Page * >::const_iterator pEnd = d->m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) delete *pIt; d->m_pagesVector.clear(); // clear 'memory allocation' descriptors qDeleteAll( d->m_allocatedPixmaps ); d->m_allocatedPixmaps.clear(); // clear 'running searches' descriptors QMap< int, RunningSearch * >::const_iterator rIt = d->m_searches.constBegin(); QMap< int, RunningSearch * >::const_iterator rEnd = d->m_searches.constEnd(); for ( ; rIt != rEnd; ++rIt ) delete *rIt; d->m_searches.clear(); // clear the visible areas and notify the observers QVector< VisiblePageRect * >::const_iterator vIt = d->m_pageRects.constBegin(); QVector< VisiblePageRect * >::const_iterator vEnd = d->m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) delete *vIt; d->m_pageRects.clear(); foreachObserver( notifyVisibleRectsChanged() ); // reset internal variables d->m_viewportHistory.clear(); d->m_viewportHistory.append( DocumentViewport() ); d->m_viewportIterator = d->m_viewportHistory.begin(); d->m_allocatedPixmapsTotalMemory = 0; d->m_allocatedTextPagesFifo.clear(); d->m_pageSize = PageSize(); d->m_pageSizes.clear(); d->m_documentInfo = DocumentInfo(); d->m_documentInfoAskedKeys.clear(); AudioPlayer::instance()->d->m_currentDocument = QUrl(); d->m_undoStack->clear(); + d->m_docdataMigrationNeeded = false; } void Document::addObserver( DocumentObserver * pObserver ) { Q_ASSERT( !d->m_observers.contains( pObserver ) ); d->m_observers << pObserver; // if the observer is added while a document is already opened, tell it if ( !d->m_pagesVector.isEmpty() ) { - pObserver->notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged ); + pObserver->notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ); pObserver->notifyViewportChanged( false /*disables smoothMove*/ ); } } void Document::removeObserver( DocumentObserver * pObserver ) { // remove observer from the map. it won't receive notifications anymore if ( d->m_observers.contains( pObserver ) ) { // free observer's pixmap data QVector::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd(); for ( ; it != end; ++it ) (*it)->deletePixmap( pObserver ); // [MEM] free observer's allocation descriptors QLinkedList< AllocatedPixmap * >::iterator aIt = d->m_allocatedPixmaps.begin(); QLinkedList< AllocatedPixmap * >::iterator aEnd = d->m_allocatedPixmaps.end(); while ( aIt != aEnd ) { AllocatedPixmap * p = *aIt; if ( p->observer == pObserver ) { aIt = d->m_allocatedPixmaps.erase( aIt ); delete p; } else ++aIt; } // delete observer entry from the map d->m_observers.remove( pObserver ); } } void Document::reparseConfig() { // reparse generator config and if something changed clear Pages bool configchanged = false; if ( d->m_generator ) { Okular::ConfigInterface * iface = qobject_cast< Okular::ConfigInterface * >( d->m_generator ); if ( iface ) configchanged = iface->reparseConfig(); } if ( configchanged ) { // invalidate pixmaps QVector::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd(); for ( ; it != end; ++it ) { (*it)->deletePixmaps(); } // [MEM] remove allocation descriptors qDeleteAll( d->m_allocatedPixmaps ); d->m_allocatedPixmaps.clear(); d->m_allocatedPixmapsTotalMemory = 0; // send reload signals to observers foreachObserver( notifyContentsCleared( DocumentObserver::Pixmap ) ); } // free memory if in 'low' profile if ( SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !d->m_allocatedPixmaps.isEmpty() && !d->m_pagesVector.isEmpty() ) d->cleanupPixmapMemory(); } bool Document::isOpened() const { return d->m_generator; } bool Document::canConfigurePrinter( ) const { if ( d->m_generator ) { Okular::PrintInterface * iface = qobject_cast< Okular::PrintInterface * >( d->m_generator ); return iface ? true : false; } else return 0; } DocumentInfo Document::documentInfo() const { QSet keys; for (Okular::DocumentInfo::Key ks = Okular::DocumentInfo::Title; ks < Okular::DocumentInfo::Invalid; ks = Okular::DocumentInfo::Key( ks+1 ) ) { keys << ks; } return documentInfo( keys ); } DocumentInfo Document::documentInfo( const QSet &keys ) const { DocumentInfo result = d->m_documentInfo; const QSet missingKeys = keys - d->m_documentInfoAskedKeys; if ( d->m_generator && !missingKeys.isEmpty() ) { DocumentInfo info = d->m_generator->generateDocumentInfo( missingKeys ); if ( missingKeys.contains( DocumentInfo::FilePath ) ) { info.set( DocumentInfo::FilePath, currentDocument().toDisplayString() ); } if ( d->m_docSize != -1 && missingKeys.contains( DocumentInfo::DocumentSize ) ) { const QString sizeString = KFormat().formatByteSize( d->m_docSize ); info.set( DocumentInfo::DocumentSize, sizeString ); } if ( missingKeys.contains( DocumentInfo::PagesSize ) ) { const QString pagesSize = d->pagesSizeString(); if ( !pagesSize.isEmpty() ) { info.set( DocumentInfo::PagesSize, pagesSize ); } } if ( missingKeys.contains( DocumentInfo::Pages ) && info.get( DocumentInfo::Pages ).isEmpty() ) { info.set( DocumentInfo::Pages, QString::number( this->pages() ) ); } d->m_documentInfo.d->values.unite(info.d->values); d->m_documentInfo.d->titles.unite(info.d->titles); result.d->values.unite(info.d->values); result.d->titles.unite(info.d->titles); } d->m_documentInfoAskedKeys += keys; return result; } const DocumentSynopsis * Document::documentSynopsis() const { return d->m_generator ? d->m_generator->generateDocumentSynopsis() : nullptr; } void Document::startFontReading() { if ( !d->m_generator || !d->m_generator->hasFeature( Generator::FontInfo ) || d->m_fontThread ) return; if ( d->m_fontsCached ) { // in case we have cached fonts, simulate a reading // this way the API is the same, and users no need to care about the // internal caching for ( int i = 0; i < d->m_fontsCache.count(); ++i ) { emit gotFont( d->m_fontsCache.at( i ) ); emit fontReadingProgress( i / pages() ); } emit fontReadingEnded(); return; } d->m_fontThread = new FontExtractionThread( d->m_generator, pages() ); connect( d->m_fontThread, SIGNAL(gotFont(Okular::FontInfo)), this, SLOT(fontReadingGotFont(Okular::FontInfo)) ); connect( d->m_fontThread.data(), SIGNAL(progress(int)), this, SLOT(slotFontReadingProgress(int)) ); d->m_fontThread->startExtraction( /*d->m_generator->hasFeature( Generator::Threaded )*/true ); } void Document::stopFontReading() { if ( !d->m_fontThread ) return; disconnect( d->m_fontThread, nullptr, this, nullptr ); d->m_fontThread->stopExtraction(); d->m_fontThread = nullptr; d->m_fontsCache.clear(); } bool Document::canProvideFontInformation() const { return d->m_generator ? d->m_generator->hasFeature( Generator::FontInfo ) : false; } const QList *Document::embeddedFiles() const { return d->m_generator ? d->m_generator->embeddedFiles() : nullptr; } const Page * Document::page( int n ) const { return ( n < d->m_pagesVector.count() ) ? d->m_pagesVector.at(n) : 0; } const DocumentViewport & Document::viewport() const { return (*d->m_viewportIterator); } const QVector< VisiblePageRect * > & Document::visiblePageRects() const { return d->m_pageRects; } void Document::setVisiblePageRects( const QVector< VisiblePageRect * > & visiblePageRects, DocumentObserver *excludeObserver ) { QVector< VisiblePageRect * >::const_iterator vIt = d->m_pageRects.constBegin(); QVector< VisiblePageRect * >::const_iterator vEnd = d->m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) delete *vIt; d->m_pageRects = visiblePageRects; // notify change to all other (different from id) observers foreach(DocumentObserver *o, d->m_observers) if ( o != excludeObserver ) o->notifyVisibleRectsChanged(); } uint Document::currentPage() const { return (*d->m_viewportIterator).pageNumber; } uint Document::pages() const { return d->m_pagesVector.size(); } QUrl Document::currentDocument() const { return d->m_url; } bool Document::isAllowed( Permission action ) const { - if ( action == Okular::AllowNotes && !d->m_annotationEditingEnabled ) + if ( action == Okular::AllowNotes && ( d->m_docdataMigrationNeeded || !d->m_annotationEditingEnabled ) ) + return false; + if ( action == Okular::AllowFillForms && d->m_docdataMigrationNeeded ) return false; #if !OKULAR_FORCE_DRM if ( KAuthorized::authorize( QStringLiteral("skip_drm") ) && !SettingsCore::obeyDRM() ) return true; #endif return d->m_generator ? d->m_generator->isAllowed( action ) : false; } bool Document::supportsSearching() const { return d->m_generator ? d->m_generator->hasFeature( Generator::TextExtraction ) : false; } bool Document::supportsPageSizes() const { return d->m_generator ? d->m_generator->hasFeature( Generator::PageSizes ) : false; } bool Document::supportsTiles() const { return d->m_generator ? d->m_generator->hasFeature( Generator::TiledRendering ) : false; } PageSize::List Document::pageSizes() const { if ( d->m_generator ) { if ( d->m_pageSizes.isEmpty() ) d->m_pageSizes = d->m_generator->pageSizes(); return d->m_pageSizes; } return PageSize::List(); } bool Document::canExportToText() const { if ( !d->m_generator ) return false; d->cacheExportFormats(); return !d->m_exportToText.isNull(); } bool Document::exportToText( const QString& fileName ) const { if ( !d->m_generator ) return false; d->cacheExportFormats(); if ( d->m_exportToText.isNull() ) return false; return d->m_generator->exportTo( fileName, d->m_exportToText ); } ExportFormat::List Document::exportFormats() const { if ( !d->m_generator ) return ExportFormat::List(); d->cacheExportFormats(); return d->m_exportFormats; } bool Document::exportTo( const QString& fileName, const ExportFormat& format ) const { return d->m_generator ? d->m_generator->exportTo( fileName, format ) : false; } bool Document::historyAtBegin() const { return d->m_viewportIterator == d->m_viewportHistory.begin(); } bool Document::historyAtEnd() const { return d->m_viewportIterator == --(d->m_viewportHistory.end()); } QVariant Document::metaData( const QString & key, const QVariant & option ) const { // if option starts with "src:" assume that we are handling a // source reference if ( key == QLatin1String("NamedViewport") && option.toString().startsWith( QLatin1String("src:"), Qt::CaseInsensitive ) && d->m_synctex_scanner) { const QString reference = option.toString(); // The reference is of form "src:1111Filename", where "1111" // points to line number 1111 in the file "Filename". // Extract the file name and the numeral part from the reference string. // This will fail if Filename starts with a digit. QString name, lineString; // Remove "src:". Presence of substring has been checked before this // function is called. name = reference.mid( 4 ); // split int nameLength = name.length(); int i = 0; for( i = 0; i < nameLength; ++i ) { if ( !name[i].isDigit() ) break; } lineString = name.left( i ); name = name.mid( i ); // Remove spaces. name = name.trimmed(); lineString = lineString.trimmed(); // Convert line to integer. bool ok; int line = lineString.toInt( &ok ); if (!ok) line = -1; // Use column == -1 for now. if( synctex_display_query( d->m_synctex_scanner, QFile::encodeName(name).constData(), line, -1, 0 ) > 0 ) { synctex_node_p node; // For now use the first hit. Could possibly be made smarter // in case there are multiple hits. while( ( node = synctex_scanner_next_result( d->m_synctex_scanner ) ) ) { Okular::DocumentViewport viewport; // TeX pages start at 1. viewport.pageNumber = synctex_node_page( node ) - 1; if ( viewport.pageNumber >= 0 ) { const QSizeF dpi = d->m_generator->dpi(); // TeX small points ... double px = (synctex_node_visible_h( node ) * dpi.width()) / 72.27; double py = (synctex_node_visible_v( node ) * dpi.height()) / 72.27; viewport.rePos.normalizedX = px / page(viewport.pageNumber)->width(); viewport.rePos.normalizedY = ( py + 0.5 ) / page(viewport.pageNumber)->height(); viewport.rePos.enabled = true; viewport.rePos.pos = Okular::DocumentViewport::Center; return viewport.toString(); } } } } return d->m_generator ? d->m_generator->metaData( key, option ) : QVariant(); } Rotation Document::rotation() const { return d->m_rotation; } QSizeF Document::allPagesSize() const { bool allPagesSameSize = true; QSizeF size; for (int i = 0; allPagesSameSize && i < d->m_pagesVector.count(); ++i) { const Page *p = d->m_pagesVector.at(i); if (i == 0) size = QSizeF(p->width(), p->height()); else { allPagesSameSize = (size == QSizeF(p->width(), p->height())); } } if (allPagesSameSize) return size; else return QSizeF(); } QString Document::pageSizeString(int page) const { if (d->m_generator) { if (d->m_generator->pagesSizeMetric() != Generator::None) { const Page *p = d->m_pagesVector.at( page ); return d->localizedSize(QSizeF(p->width(), p->height())); } } return QString(); } void Document::requestPixmaps( const QLinkedList< PixmapRequest * > & requests ) { requestPixmaps( requests, RemoveAllPrevious ); } void Document::requestPixmaps( const QLinkedList< PixmapRequest * > & requests, PixmapRequestFlags reqOptions ) { if ( requests.isEmpty() ) return; if ( !d->m_pageController ) { // delete requests.. QLinkedList< PixmapRequest * >::const_iterator rIt = requests.constBegin(), rEnd = requests.constEnd(); for ( ; rIt != rEnd; ++rIt ) delete *rIt; // ..and return return; } // 1. [CLEAN STACK] remove previous requests of requesterID // FIXME This assumes all requests come from the same observer, that is true atm but not enforced anywhere DocumentObserver *requesterObserver = requests.first()->observer(); QSet< int > requestedPages; { QLinkedList< PixmapRequest * >::const_iterator rIt = requests.constBegin(), rEnd = requests.constEnd(); for ( ; rIt != rEnd; ++rIt ) requestedPages.insert( (*rIt)->pageNumber() ); } const bool removeAllPrevious = reqOptions & RemoveAllPrevious; d->m_pixmapRequestsMutex.lock(); QLinkedList< PixmapRequest * >::iterator sIt = d->m_pixmapRequestsStack.begin(), sEnd = d->m_pixmapRequestsStack.end(); while ( sIt != sEnd ) { if ( (*sIt)->observer() == requesterObserver && ( removeAllPrevious || requestedPages.contains( (*sIt)->pageNumber() ) ) ) { // delete request and remove it from stack delete *sIt; sIt = d->m_pixmapRequestsStack.erase( sIt ); } else ++sIt; } // 2. [ADD TO STACK] add requests to stack QLinkedList< PixmapRequest * >::const_iterator rIt = requests.constBegin(), rEnd = requests.constEnd(); for ( ; rIt != rEnd; ++rIt ) { // set the 'page field' (see PixmapRequest) and check if it is valid PixmapRequest * request = *rIt; qCDebug(OkularCoreDebug).nospace() << "request observer=" << request->observer() << " " <width() << "x" << request->height() << "@" << request->pageNumber(); if ( d->m_pagesVector.value( request->pageNumber() ) == 0 ) { // skip requests referencing an invalid page (must not happen) delete request; continue; } request->d->mPage = d->m_pagesVector.value( request->pageNumber() ); if ( request->isTile() ) { // Change the current request rect so that only invalid tiles are // requested. Also make sure the rect is tile-aligned. NormalizedRect tilesRect; const QList tiles = request->d->tilesManager()->tilesAt( request->normalizedRect(), TilesManager::TerminalTile ); QList::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd(); while ( tIt != tEnd ) { const Tile &tile = *tIt; if ( !tile.isValid() ) { if ( tilesRect.isNull() ) tilesRect = tile.rect(); else tilesRect |= tile.rect(); } tIt++; } request->setNormalizedRect( tilesRect ); } if ( !request->asynchronous() ) request->d->mPriority = 0; // add request to the 'stack' at the right place if ( !request->priority() ) // add priority zero requests to the top of the stack d->m_pixmapRequestsStack.append( request ); else { // insert in stack sorted by priority sIt = d->m_pixmapRequestsStack.begin(); sEnd = d->m_pixmapRequestsStack.end(); while ( sIt != sEnd && (*sIt)->priority() > request->priority() ) ++sIt; d->m_pixmapRequestsStack.insert( sIt, request ); } } d->m_pixmapRequestsMutex.unlock(); // 3. [START FIRST GENERATION] if generator is ready, start a new generation, // or else (if gen is running) it will be started when the new contents will //come from generator (in requestDone()) // all handling of requests put into sendGeneratorPixmapRequest // if ( generator->canRequestPixmap() ) d->sendGeneratorPixmapRequest(); } void Document::requestTextPage( uint page ) { Page * kp = d->m_pagesVector[ page ]; if ( !d->m_generator || !kp ) return; // Memory management for TextPages d->m_generator->generateTextPage( kp ); } void DocumentPrivate::notifyAnnotationChanges( int page ) { - int flags = DocumentObserver::Annotations; - - if ( m_annotationsNeedSaveAs ) - flags |= DocumentObserver::NeedSaveAs; + foreachObserverD( notifyPageChanged( page, DocumentObserver::Annotations ) ); +} - foreachObserverD( notifyPageChanged( page, flags ) ); +void DocumentPrivate::notifyFormChanges( int /*page*/ ) +{ } void Document::addPageAnnotation( int page, Annotation * annotation ) { // Transform annotation's base boundary rectangle into unrotated coordinates Page *p = d->m_pagesVector[page]; QTransform t = p->d->rotationMatrix(); annotation->d_ptr->baseTransform(t.inverted()); QUndoCommand *uc = new AddAnnotationCommand(this->d, annotation, page); d->m_undoStack->push(uc); } bool Document::canModifyPageAnnotation( const Annotation * annotation ) const { if ( !annotation || ( annotation->flags() & Annotation::DenyWrite ) ) return false; if ( !isAllowed(Okular::AllowNotes) ) return false; if ( ( annotation->flags() & Annotation::External ) && !d->canModifyExternalAnnotations() ) return false; switch ( annotation->subType() ) { case Annotation::AText: case Annotation::ALine: case Annotation::AGeom: case Annotation::AHighlight: case Annotation::AStamp: case Annotation::AInk: return true; default: return false; } } void Document::prepareToModifyAnnotationProperties( Annotation * annotation ) { Q_ASSERT(d->m_prevPropsOfAnnotBeingModified.isNull()); if (!d->m_prevPropsOfAnnotBeingModified.isNull()) { qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties has already been called since last call to Document::modifyPageAnnotationProperties"; return; } d->m_prevPropsOfAnnotBeingModified = annotation->getAnnotationPropertiesDomNode(); } void Document::modifyPageAnnotationProperties( int page, Annotation * annotation ) { Q_ASSERT(!d->m_prevPropsOfAnnotBeingModified.isNull()); if (d->m_prevPropsOfAnnotBeingModified.isNull()) { qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties must be called before Annotation is modified"; return; } QDomNode prevProps = d->m_prevPropsOfAnnotBeingModified; QUndoCommand *uc = new Okular::ModifyAnnotationPropertiesCommand( d, annotation, page, prevProps, annotation->getAnnotationPropertiesDomNode() ); d->m_undoStack->push( uc ); d->m_prevPropsOfAnnotBeingModified.clear(); } void Document::translatePageAnnotation(int page, Annotation* annotation, const NormalizedPoint & delta ) { int complete = (annotation->flags() & Okular::Annotation::BeingMoved) == 0; QUndoCommand *uc = new Okular::TranslateAnnotationCommand( d, annotation, page, delta, complete ); d->m_undoStack->push(uc); } void Document::adjustPageAnnotation( int page, Annotation *annotation, const Okular::NormalizedPoint & delta1, const Okular::NormalizedPoint & delta2 ) { const bool complete = (annotation->flags() & Okular::Annotation::BeingResized) == 0; QUndoCommand *uc = new Okular::AdjustAnnotationCommand( d, annotation, page, delta1, delta2, complete ); d->m_undoStack->push(uc); } void Document::editPageAnnotationContents( int page, Annotation* annotation, const QString & newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos ) { QString prevContents = annotation->contents(); QUndoCommand *uc = new EditAnnotationContentsCommand( d, annotation, page, newContents, newCursorPos, prevContents, prevCursorPos, prevAnchorPos ); d->m_undoStack->push( uc ); } bool Document::canRemovePageAnnotation( const Annotation * annotation ) const { if ( !annotation || ( annotation->flags() & Annotation::DenyDelete ) ) return false; if ( ( annotation->flags() & Annotation::External ) && !d->canRemoveExternalAnnotations() ) return false; switch ( annotation->subType() ) { case Annotation::AText: case Annotation::ALine: case Annotation::AGeom: case Annotation::AHighlight: case Annotation::AStamp: case Annotation::AInk: case Annotation::ACaret: return true; default: return false; } } void Document::removePageAnnotation( int page, Annotation * annotation ) { QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page); d->m_undoStack->push(uc); } void Document::removePageAnnotations( int page, const QList &annotations ) { d->m_undoStack->beginMacro(i18nc("remove a collection of annotations from the page", "remove annotations")); foreach(Annotation* annotation, annotations) { QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page); d->m_undoStack->push(uc); } d->m_undoStack->endMacro(); } bool DocumentPrivate::canAddAnnotationsNatively() const { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); if ( iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Addition) ) return true; return false; } bool DocumentPrivate::canModifyExternalAnnotations() const { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); if ( iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Modification) ) return true; return false; } bool DocumentPrivate::canRemoveExternalAnnotations() const { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); if ( iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Removal) ) return true; return false; } void Document::setPageTextSelection( int page, RegularAreaRect * rect, const QColor & color ) { Page * kp = d->m_pagesVector[ page ]; if ( !d->m_generator || !kp ) return; // add or remove the selection basing whether rect is null or not if ( rect ) kp->d->setTextSelections( rect, color ); else kp->d->deleteTextSelections(); // notify observers about the change foreachObserver( notifyPageChanged( page, DocumentObserver::TextSelection ) ); } bool Document::canUndo() const { return d->m_undoStack->canUndo(); } bool Document::canRedo() const { return d->m_undoStack->canRedo(); } /* REFERENCE IMPLEMENTATION: better calling setViewport from other code void Document::setNextPage() { // advance page and set viewport on observers if ( (*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1 ) setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber + 1 ) ); } void Document::setPrevPage() { // go to previous page and set viewport on observers if ( (*d->m_viewportIterator).pageNumber > 0 ) setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber - 1 ) ); } */ void Document::setViewportPage( int page, DocumentObserver *excludeObserver, bool smoothMove ) { // clamp page in range [0 ... numPages-1] if ( page < 0 ) page = 0; else if ( page > (int)d->m_pagesVector.count() ) page = d->m_pagesVector.count() - 1; // make a viewport from the page and broadcast it setViewport( DocumentViewport( page ), excludeObserver, smoothMove ); } void Document::setViewport( const DocumentViewport & viewport, DocumentObserver *excludeObserver, bool smoothMove ) { if ( !viewport.isValid() ) { qCDebug(OkularCoreDebug) << "invalid viewport:" << viewport.toString(); return; } if ( viewport.pageNumber >= int(d->m_pagesVector.count()) ) { //qCDebug(OkularCoreDebug) << "viewport out of document:" << viewport.toString(); return; } // if already broadcasted, don't redo it DocumentViewport & oldViewport = *d->m_viewportIterator; // disabled by enrico on 2005-03-18 (less debug output) //if ( viewport == oldViewport ) // qCDebug(OkularCoreDebug) << "setViewport with the same viewport."; const int oldPageNumber = oldViewport.pageNumber; // set internal viewport taking care of history if ( oldViewport.pageNumber == viewport.pageNumber || !oldViewport.isValid() ) { // if page is unchanged save the viewport at current position in queue oldViewport = viewport; } else { // remove elements after viewportIterator in queue d->m_viewportHistory.erase( ++d->m_viewportIterator, d->m_viewportHistory.end() ); // keep the list to a reasonable size by removing head when needed if ( d->m_viewportHistory.count() >= OKULAR_HISTORY_MAXSTEPS ) d->m_viewportHistory.pop_front(); // add the item at the end of the queue d->m_viewportIterator = d->m_viewportHistory.insert( d->m_viewportHistory.end(), viewport ); } const int currentViewportPage = (*d->m_viewportIterator).pageNumber; const bool currentPageChanged = (oldPageNumber != currentViewportPage); // notify change to all other (different from id) observers foreach(DocumentObserver *o, d->m_observers) { if ( o != excludeObserver ) o->notifyViewportChanged( smoothMove ); if ( currentPageChanged ) o->notifyCurrentPageChanged( oldPageNumber, currentViewportPage ); } } void Document::setZoom(int factor, DocumentObserver *excludeObserver) { // notify change to all other (different from id) observers foreach(DocumentObserver *o, d->m_observers) if (o != excludeObserver) o->notifyZoom( factor ); } void Document::setPrevViewport() // restore viewport from the history { if ( d->m_viewportIterator != d->m_viewportHistory.begin() ) { const int oldViewportPage = (*d->m_viewportIterator).pageNumber; // restore previous viewport and notify it to observers --d->m_viewportIterator; foreachObserver( notifyViewportChanged( true ) ); const int currentViewportPage = (*d->m_viewportIterator).pageNumber; if (oldViewportPage != currentViewportPage) foreachObserver( notifyCurrentPageChanged( oldViewportPage, currentViewportPage ) ); } } void Document::setNextViewport() // restore next viewport from the history { QLinkedList< DocumentViewport >::const_iterator nextIterator = d->m_viewportIterator; ++nextIterator; if ( nextIterator != d->m_viewportHistory.end() ) { const int oldViewportPage = (*d->m_viewportIterator).pageNumber; // restore next viewport and notify it to observers ++d->m_viewportIterator; foreachObserver( notifyViewportChanged( true ) ); const int currentViewportPage = (*d->m_viewportIterator).pageNumber; if (oldViewportPage != currentViewportPage) foreachObserver( notifyCurrentPageChanged( oldViewportPage, currentViewportPage ) ); } } void Document::setNextDocumentViewport( const DocumentViewport & viewport ) { d->m_nextDocumentViewport = viewport; } void Document::setNextDocumentDestination( const QString &namedDestination ) { d->m_nextDocumentDestination = namedDestination; } void Document::searchText( int searchID, const QString & text, bool fromStart, Qt::CaseSensitivity caseSensitivity, SearchType type, bool moveViewport, const QColor & color ) { d->m_searchCancelled = false; // safety checks: don't perform searches on empty or unsearchable docs if ( !d->m_generator || !d->m_generator->hasFeature( Generator::TextExtraction ) || d->m_pagesVector.isEmpty() ) { emit searchFinished( searchID, NoMatchFound ); return; } // if searchID search not recorded, create new descriptor and init params QMap< int, RunningSearch * >::iterator searchIt = d->m_searches.find( searchID ); if ( searchIt == d->m_searches.end() ) { RunningSearch * search = new RunningSearch(); search->continueOnPage = -1; searchIt = d->m_searches.insert( searchID, search ); } RunningSearch * s = *searchIt; // update search structure bool newText = text != s->cachedString; s->cachedString = text; s->cachedType = type; s->cachedCaseSensitivity = caseSensitivity; s->cachedViewportMove = moveViewport; s->cachedColor = color; s->isCurrentlySearching = true; // global data for search QSet< int > *pagesToNotify = new QSet< int >; // remove highlights from pages and queue them for notifying changes *pagesToNotify += s->highlightedPages; foreach(int pageNumber, s->highlightedPages) d->m_pagesVector.at(pageNumber)->d->deleteHighlights( searchID ); s->highlightedPages.clear(); // set hourglass cursor QApplication::setOverrideCursor( Qt::WaitCursor ); // 1. ALLDOC - proces all document marking pages if ( type == AllDocument ) { QMap< Page *, QVector > *pageMatches = new QMap< Page *, QVector >; // search and highlight 'text' (as a solid phrase) on all pages QMetaObject::invokeMethod(this, "doContinueAllDocumentSearch", Qt::QueuedConnection, Q_ARG(void *, pagesToNotify), Q_ARG(void *, pageMatches), Q_ARG(int, 0), Q_ARG(int, searchID)); } // 2. NEXTMATCH - find next matching item (or start from top) // 3. PREVMATCH - find previous matching item (or start from bottom) else if ( type == NextMatch || type == PreviousMatch ) { // find out from where to start/resume search from const bool forward = type == NextMatch; const int viewportPage = (*d->m_viewportIterator).pageNumber; const int fromStartSearchPage = forward ? 0 : d->m_pagesVector.count() - 1; int currentPage = fromStart ? fromStartSearchPage : ((s->continueOnPage != -1) ? s->continueOnPage : viewportPage); Page * lastPage = fromStart ? 0 : d->m_pagesVector[ currentPage ]; int pagesDone = 0; // continue checking last TextPage first (if it is the current page) RegularAreaRect * match = nullptr; if ( lastPage && lastPage->number() == s->continueOnPage ) { if ( newText ) match = lastPage->findText( searchID, text, forward ? FromTop : FromBottom, caseSensitivity ); else match = lastPage->findText( searchID, text, forward ? NextResult : PreviousResult, caseSensitivity, &s->continueOnMatch ); if ( !match ) { if (forward) currentPage++; else currentPage--; pagesDone++; } } s->pagesDone = pagesDone; DoContinueDirectionMatchSearchStruct *searchStruct = new DoContinueDirectionMatchSearchStruct(); searchStruct->pagesToNotify = pagesToNotify; searchStruct->match = match; searchStruct->currentPage = currentPage; searchStruct->searchID = searchID; QMetaObject::invokeMethod(this, "doContinueDirectionMatchSearch", Qt::QueuedConnection, Q_ARG(void *, searchStruct)); } // 4. GOOGLE* - process all document marking pages else if ( type == GoogleAll || type == GoogleAny ) { QMap< Page *, QVector< QPair > > *pageMatches = new QMap< Page *, QVector > >; const QStringList words = text.split( QLatin1Char ( ' ' ), QString::SkipEmptyParts ); // search and highlight every word in 'text' on all pages QMetaObject::invokeMethod(this, "doContinueGooglesDocumentSearch", Qt::QueuedConnection, Q_ARG(void *, pagesToNotify), Q_ARG(void *, pageMatches), Q_ARG(int, 0), Q_ARG(int, searchID), Q_ARG(QStringList, words)); } } void Document::continueSearch( int searchID ) { // check if searchID is present in runningSearches QMap< int, RunningSearch * >::const_iterator it = d->m_searches.constFind( searchID ); if ( it == d->m_searches.constEnd() ) { emit searchFinished( searchID, NoMatchFound ); return; } // start search with cached parameters from last search by searchID RunningSearch * p = *it; if ( !p->isCurrentlySearching ) searchText( searchID, p->cachedString, false, p->cachedCaseSensitivity, p->cachedType, p->cachedViewportMove, p->cachedColor ); } void Document::continueSearch( int searchID, SearchType type ) { // check if searchID is present in runningSearches QMap< int, RunningSearch * >::const_iterator it = d->m_searches.constFind( searchID ); if ( it == d->m_searches.constEnd() ) { emit searchFinished( searchID, NoMatchFound ); return; } // start search with cached parameters from last search by searchID RunningSearch * p = *it; if ( !p->isCurrentlySearching ) searchText( searchID, p->cachedString, false, p->cachedCaseSensitivity, type, p->cachedViewportMove, p->cachedColor ); } void Document::resetSearch( int searchID ) { // if we are closing down, don't bother doing anything if ( !d->m_generator ) return; // check if searchID is present in runningSearches QMap< int, RunningSearch * >::iterator searchIt = d->m_searches.find( searchID ); if ( searchIt == d->m_searches.end() ) return; // get previous parameters for search RunningSearch * s = *searchIt; // unhighlight pages and inform observers about that foreach(int pageNumber, s->highlightedPages) { d->m_pagesVector.at(pageNumber)->d->deleteHighlights( searchID ); foreachObserver( notifyPageChanged( pageNumber, DocumentObserver::Highlights ) ); } // send the setup signal too (to update views that filter on matches) foreachObserver( notifySetup( d->m_pagesVector, 0 ) ); // remove serch from the runningSearches list and delete it d->m_searches.erase( searchIt ); delete s; } void Document::cancelSearch() { d->m_searchCancelled = true; } void Document::undo() { d->m_undoStack->undo(); } void Document::redo() { d->m_undoStack->redo(); } void Document::editFormText( int pageNumber, Okular::FormFieldText* form, const QString & newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos ) { QUndoCommand *uc = new EditFormTextCommand( this->d, form, pageNumber, newContents, newCursorPos, form->text(), prevCursorPos, prevAnchorPos ); d->m_undoStack->push( uc ); d->recalculateForms(); } void Document::editFormList( int pageNumber, FormFieldChoice* form, const QList< int > & newChoices ) { const QList< int > prevChoices = form->currentChoices(); QUndoCommand *uc = new EditFormListCommand( this->d, form, pageNumber, newChoices, prevChoices ); d->m_undoStack->push( uc ); d->recalculateForms(); } void Document::editFormCombo( int pageNumber, FormFieldChoice* form, const QString & newText, int newCursorPos, int prevCursorPos, int prevAnchorPos ) { QString prevText; if ( form->currentChoices().isEmpty() ) { prevText = form->editChoice(); } else { prevText = form->choices()[form->currentChoices().constFirst()]; } QUndoCommand *uc = new EditFormComboCommand( this->d, form, pageNumber, newText, newCursorPos, prevText, prevCursorPos, prevAnchorPos ); d->m_undoStack->push( uc ); d->recalculateForms(); } void Document::editFormButtons( int pageNumber, const QList< FormFieldButton* >& formButtons, const QList< bool >& newButtonStates ) { QUndoCommand *uc = new EditFormButtonsCommand( this->d, pageNumber, formButtons, newButtonStates ); d->m_undoStack->push( uc ); } void Document::reloadDocument() const { const int numOfPages = pages(); for( int i = currentPage(); i >= 0; i -- ) d->refreshPixmaps( i ); for( int i = currentPage() + 1; i < numOfPages; i ++ ) d->refreshPixmaps( i ); } BookmarkManager * Document::bookmarkManager() const { return d->m_bookmarkManager; } QList Document::bookmarkedPageList() const { QList list; uint docPages = pages(); //pages are 0-indexed internally, but 1-indexed externally for ( uint i = 0; i < docPages; i++ ) { if ( bookmarkManager()->isBookmarked( i ) ) { list << i + 1; } } return list; } QString Document::bookmarkedPageRange() const { // Code formerly in Part::slotPrint() // range detecting QString range; uint docPages = pages(); int startId = -1; int endId = -1; for ( uint i = 0; i < docPages; ++i ) { if ( bookmarkManager()->isBookmarked( i ) ) { if ( startId < 0 ) startId = i; if ( endId < 0 ) endId = startId; else ++endId; } else if ( startId >= 0 && endId >= 0 ) { if ( !range.isEmpty() ) range += QLatin1Char ( ',' ); if ( endId - startId > 0 ) range += QStringLiteral( "%1-%2" ).arg( startId + 1 ).arg( endId + 1 ); else range += QString::number( startId + 1 ); startId = -1; endId = -1; } } if ( startId >= 0 && endId >= 0 ) { if ( !range.isEmpty() ) range += QLatin1Char ( ',' ); if ( endId - startId > 0 ) range += QStringLiteral( "%1-%2" ).arg( startId + 1 ).arg( endId + 1 ); else range += QString::number( startId + 1 ); } return range; } void Document::processAction( const Action * action ) { if ( !action ) return; switch( action->actionType() ) { case Action::Goto: { const GotoAction * go = static_cast< const GotoAction * >( action ); d->m_nextDocumentViewport = go->destViewport(); d->m_nextDocumentDestination = go->destinationName(); // Explanation of why d->m_nextDocumentViewport is needed: // all openRelativeFile does is launch a signal telling we // want to open another URL, the problem is that when the file is // non local, the loading is done assynchronously so you can't // do a setViewport after the if as it was because you are doing the setViewport // on the old file and when the new arrives there is no setViewport for it and // it does not show anything // first open filename if link is pointing outside this document if ( go->isExternal() && !d->openRelativeFile( go->fileName() ) ) { qCWarning(OkularCoreDebug).nospace() << "Action: Error opening '" << go->fileName() << "'."; return; } else { const DocumentViewport nextViewport = d->nextDocumentViewport(); // skip local links that point to nowhere (broken ones) if ( !nextViewport.isValid() ) return; setViewport( nextViewport, nullptr, true ); d->m_nextDocumentViewport = DocumentViewport(); d->m_nextDocumentDestination = QString(); } } break; case Action::Execute: { const ExecuteAction * exe = static_cast< const ExecuteAction * >( action ); const QString fileName = exe->fileName(); if ( fileName.endsWith( QLatin1String(".pdf"), Qt::CaseInsensitive ) ) { d->openRelativeFile( fileName ); return; } // Albert: the only pdf i have that has that kind of link don't define // an application and use the fileName as the file to open QUrl url = d->giveAbsoluteUrl( fileName ); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl( url ); // Check executables if ( KRun::isExecutableFile( url, mime.name() ) ) { // Don't have any pdf that uses this code path, just a guess on how it should work if ( !exe->parameters().isEmpty() ) { url = d->giveAbsoluteUrl( exe->parameters() ); mime = db.mimeTypeForUrl( url ); if ( KRun::isExecutableFile( url, mime.name() ) ) { // this case is a link pointing to an executable with a parameter // that also is an executable, possibly a hand-crafted pdf KMessageBox::information( d->m_widget, i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that.") ); return; } } else { // this case is a link pointing to an executable with no parameters // core developers find unacceptable executing it even after asking the user KMessageBox::information( d->m_widget, i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that.") ); return; } } KService::Ptr ptr = KMimeTypeTrader::self()->preferredService( mime.name(), QStringLiteral("Application") ); if ( ptr ) { QList lst; lst.append( url ); KRun::runService( *ptr, lst, nullptr ); } else KMessageBox::information( d->m_widget, i18n( "No application found for opening file of mimetype %1.", mime.name() ) ); } break; case Action::DocAction: { const DocumentAction * docaction = static_cast< const DocumentAction * >( action ); switch( docaction->documentActionType() ) { case DocumentAction::PageFirst: setViewportPage( 0 ); break; case DocumentAction::PagePrev: if ( (*d->m_viewportIterator).pageNumber > 0 ) setViewportPage( (*d->m_viewportIterator).pageNumber - 1 ); break; case DocumentAction::PageNext: if ( (*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1 ) setViewportPage( (*d->m_viewportIterator).pageNumber + 1 ); break; case DocumentAction::PageLast: setViewportPage( d->m_pagesVector.count() - 1 ); break; case DocumentAction::HistoryBack: setPrevViewport(); break; case DocumentAction::HistoryForward: setNextViewport(); break; case DocumentAction::Quit: emit quit(); break; case DocumentAction::Presentation: emit linkPresentation(); break; case DocumentAction::EndPresentation: emit linkEndPresentation(); break; case DocumentAction::Find: emit linkFind(); break; case DocumentAction::GoToPage: emit linkGoToPage(); break; case DocumentAction::Close: emit close(); break; } } break; case Action::Browse: { const BrowseAction * browse = static_cast< const BrowseAction * >( action ); QString lilySource; int lilyRow = 0, lilyCol = 0; // if the url is a mailto one, invoke mailer if ( browse->url().scheme() == QLatin1String("mailto") ) { QDesktopServices::openUrl( browse->url() ); } else if ( extractLilyPondSourceReference( browse->url(), &lilySource, &lilyRow, &lilyCol ) ) { const SourceReference ref( lilySource, lilyRow, lilyCol ); processSourceReference( &ref ); } else { const QUrl url = browse->url(); // fix for #100366, documents with relative links that are the form of http:foo.pdf if ((url.scheme() == "http") && url.host().isEmpty() && url.fileName().endsWith("pdf")) { d->openRelativeFile(url.fileName()); return; } // handle documents with relative path if ( d->m_url.isValid() ) { const QUrl realUrl = KIO::upUrl(d->m_url).resolved(url); // KRun autodeletes new KRun( realUrl, d->m_widget ); } } } break; case Action::Sound: { const SoundAction * linksound = static_cast< const SoundAction * >( action ); AudioPlayer::instance()->playSound( linksound->sound(), linksound ); } break; case Action::Script: { const ScriptAction * linkscript = static_cast< const ScriptAction * >( action ); if ( !d->m_scripter ) d->m_scripter = new Scripter( d ); d->m_scripter->execute( linkscript->scriptType(), linkscript->script() ); } break; case Action::Movie: emit processMovieAction( static_cast< const MovieAction * >( action ) ); break; case Action::Rendition: { const RenditionAction * linkrendition = static_cast< const RenditionAction * >( action ); if ( !linkrendition->script().isEmpty() ) { if ( !d->m_scripter ) d->m_scripter = new Scripter( d ); d->m_scripter->execute( linkrendition->scriptType(), linkrendition->script() ); } emit processRenditionAction( static_cast< const RenditionAction * >( action ) ); } break; case Action::BackendOpaque: { d->m_generator->opaqueAction( static_cast< const BackendOpaqueAction * >( action ) ); } break; } } void Document::processSourceReference( const SourceReference * ref ) { if ( !ref ) return; const QUrl url = d->giveAbsoluteUrl( ref->fileName() ); if ( !url.isLocalFile() ) { qCDebug(OkularCoreDebug) << url.url() << "is not a local file."; return; } const QString absFileName = url.toLocalFile(); if ( !QFile::exists( absFileName ) ) { qCDebug(OkularCoreDebug) << "No such file:" << absFileName; return; } bool handled = false; emit sourceReferenceActivated(absFileName, ref->row(), ref->column(), &handled); if(handled) { return; } static QHash< int, QString > editors; // init the editors table if empty (on first run, usually) if ( editors.isEmpty() ) { editors = buildEditorsMap(); } QHash< int, QString >::const_iterator it = editors.constFind( SettingsCore::externalEditor() ); QString p; if ( it != editors.constEnd() ) p = *it; else p = SettingsCore::externalEditorCommand(); // custom editor not yet configured if ( p.isEmpty() ) return; // manually append the %f placeholder if not specified if ( p.indexOf( QLatin1String( "%f" ) ) == -1 ) p.append( QLatin1String( " %f" ) ); // replacing the placeholders QHash< QChar, QString > map; map.insert( QLatin1Char ( 'f' ), absFileName ); map.insert( QLatin1Char ( 'c' ), QString::number( ref->column() ) ); map.insert( QLatin1Char ( 'l' ), QString::number( ref->row() ) ); const QString cmd = KMacroExpander::expandMacrosShellQuote( p, map ); if ( cmd.isEmpty() ) return; const QStringList args = KShell::splitArgs( cmd ); if ( args.isEmpty() ) return; KProcess::startDetached( args ); } const SourceReference * Document::dynamicSourceReference( int pageNr, double absX, double absY ) { if ( !d->m_synctex_scanner ) return nullptr; const QSizeF dpi = d->m_generator->dpi(); if (synctex_edit_query(d->m_synctex_scanner, pageNr + 1, absX * 72. / dpi.width(), absY * 72. / dpi.height()) > 0) { synctex_node_p node; // TODO what should we do if there is really more than one node? while (( node = synctex_scanner_next_result( d->m_synctex_scanner ) )) { int line = synctex_node_line(node); int col = synctex_node_column(node); // column extraction does not seem to be implemented in synctex so far. set the SourceReference default value. if ( col == -1 ) { col = 0; } const char *name = synctex_scanner_get_name( d->m_synctex_scanner, synctex_node_tag( node ) ); return new Okular::SourceReference( QFile::decodeName( name ), line, col ); } } return nullptr; } Document::PrintingType Document::printingSupport() const { if ( d->m_generator ) { if ( d->m_generator->hasFeature( Generator::PrintNative ) ) { return NativePrinting; } #ifndef Q_OS_WIN if ( d->m_generator->hasFeature( Generator::PrintPostscript ) ) { return PostscriptPrinting; } #endif } return NoPrinting; } bool Document::supportsPrintToFile() const { return d->m_generator ? d->m_generator->hasFeature( Generator::PrintToFile ) : false; } bool Document::print( QPrinter &printer ) { return d->m_generator ? d->m_generator->print( printer ) : false; } QString Document::printError() const { Okular::Generator::PrintError err = Generator::UnknownPrintError; if ( d->m_generator ) { QMetaObject::invokeMethod( d->m_generator, "printError", Qt::DirectConnection, Q_RETURN_ARG(Okular::Generator::PrintError, err) ); } Q_ASSERT( err != Generator::NoPrintError ); switch ( err ) { case Generator::TemporaryFileOpenPrintError: return i18n( "Could not open a temporary file" ); case Generator::FileConversionPrintError: return i18n( "Print conversion failed" ); case Generator::PrintingProcessCrashPrintError: return i18n( "Printing process crashed" ); case Generator::PrintingProcessStartPrintError: return i18n( "Printing process could not start" ); case Generator::PrintToFilePrintError: return i18n( "Printing to file failed" ); case Generator::InvalidPrinterStatePrintError: return i18n( "Printer was in invalid state" ); case Generator::UnableToFindFilePrintError: return i18n( "Unable to find file to print" ); case Generator::NoFileToPrintError: return i18n( "There was no file to print" ); case Generator::NoBinaryToPrintError: return i18n( "Could not find a suitable binary for printing. Make sure CUPS lpr binary is available" ); case Generator::InvalidPageSizePrintError: return i18n( "The page print size is invalid" ); case Generator::NoPrintError: return QString(); case Generator::UnknownPrintError: return QString(); } return QString(); } QWidget* Document::printConfigurationWidget() const { if ( d->m_generator ) { PrintInterface * iface = qobject_cast< Okular::PrintInterface * >( d->m_generator ); return iface ? iface->printConfigurationWidget() : nullptr; } else return nullptr; } void Document::fillConfigDialog( KConfigDialog * dialog ) { if ( !dialog ) return; // ensure that we have all the generators with settings loaded QVector offers = DocumentPrivate::configurableGenerators(); d->loadServiceList( offers ); bool pagesAdded = false; QHash< QString, GeneratorInfo >::iterator it = d->m_loadedGenerators.begin(); QHash< QString, GeneratorInfo >::iterator itEnd = d->m_loadedGenerators.end(); for ( ; it != itEnd; ++it ) { Okular::ConfigInterface * iface = d->generatorConfig( it.value() ); if ( iface ) { iface->addPages( dialog ); pagesAdded = true; } } if ( pagesAdded ) { connect( dialog, SIGNAL(settingsChanged(QString)), this, SLOT(slotGeneratorConfigChanged(QString)) ); } } QVector DocumentPrivate::configurableGenerators() { const QVector available = availableGenerators(); QVector result; for (const KPluginMetaData& md : available) { if (md.rawData()[QStringLiteral("X-KDE-okularHasInternalSettings")].toBool()) { result << md; } } return result; } KPluginMetaData Document::generatorInfo() const { if (!d->m_generator) return KPluginMetaData(); auto genIt = d->m_loadedGenerators.constFind(d->m_generatorName); Q_ASSERT(genIt != d->m_loadedGenerators.constEnd()); return genIt.value().metadata; } int Document::configurableGenerators() const { return DocumentPrivate::configurableGenerators().size(); } QStringList Document::supportedMimeTypes() const { // TODO: make it a static member of DocumentPrivate? QStringList result = d->m_supportedMimeTypes; if (result.isEmpty()) { const QVector available = DocumentPrivate::availableGenerators(); for (const KPluginMetaData& md : available) { result << md.mimeTypes(); } // Remove duplicate mimetypes represented by different names QMimeDatabase mimeDatabase; QSet uniqueMimetypes; for (const QString &mimeName : result) { uniqueMimetypes.insert(mimeDatabase.mimeTypeForName(mimeName)); } result.clear(); for (const QMimeType &mimeType : uniqueMimetypes) { result.append(mimeType.name()); } + // Add the Okular archive mimetype + result << QStringLiteral("application/vnd.kde.okular-archive"); + // Sorting by mimetype name doesn't make a ton of sense, // but ensures that the list is ordered the same way every time qSort(result); d->m_supportedMimeTypes = result; } return result; } +bool Document::canSwapBackingFile() const +{ + if ( !d->m_generator ) + return false; + Q_ASSERT( !d->m_generatorName.isEmpty() ); + + QHash< QString, GeneratorInfo >::iterator genIt = d->m_loadedGenerators.find( d->m_generatorName ); + Q_ASSERT( genIt != d->m_loadedGenerators.end() ); + + return genIt->generator->hasFeature( Generator::SwapBackingFile ); +} + +bool Document::swapBackingFile( const QString &newFileName, const QUrl &url ) +{ + if ( !d->m_generator ) + return false; + Q_ASSERT( !d->m_generatorName.isEmpty() ); + + QHash< QString, GeneratorInfo >::iterator genIt = d->m_loadedGenerators.find( d->m_generatorName ); + Q_ASSERT( genIt != d->m_loadedGenerators.end() ); + + if ( !genIt->generator->hasFeature( Generator::SwapBackingFile ) ) + return false; + + // Save metadata about the file we're about to close + d->saveDocumentInfo(); + + qCDebug(OkularCoreDebug) << "Swapping backing file to" << newFileName; + QVector< Page * > newPagesVector; + Generator::SwapBackingFileResult result = genIt->generator->swapBackingFile( newFileName, newPagesVector ); + if (result != Generator::SwapBackingFileError) + { + QLinkedList< ObjectRect* > rectsToDelete; + QLinkedList< Annotation* > annotationsToDelete; + QSet< PagePrivate* > pagePrivatesToDelete; + + if (result == Generator::SwapBackingFileReloadInternalData) + { + // Here we need to replace everything that the old generator + // had created with what the new one has without making it look like + // we have actually closed and opened the file again + + // Simple sanity check + if (newPagesVector.count() != d->m_pagesVector.count()) + return false; + + // Update the undo stack contents + for (int i = 0; i < d->m_undoStack->count(); ++i) + { + // Trust me on the const_cast ^_^ + QUndoCommand *uc = const_cast( d->m_undoStack->command( i ) ); + if (OkularUndoCommand *ouc = dynamic_cast( uc )) + { + const bool success = ouc->refreshInternalPageReferences( newPagesVector ); + if ( !success ) + { + qWarning() << "Document::swapBackingFile: refreshInternalPageReferences failed" << ouc; + return false; + } + } + else + { + qWarning() << "Document::swapBackingFile: Unhandled undo command" << uc; + return false; + } + } + + for (int i = 0; i < d->m_pagesVector.count(); ++i) + { + // switch the PagePrivate* from newPage to oldPage + // this way everyone still holding Page* doesn't get + // disturbed by it + Page *oldPage = d->m_pagesVector[i]; + Page *newPage = newPagesVector[i]; + newPage->d->adoptGeneratedContents(oldPage->d); + + pagePrivatesToDelete << oldPage->d; + oldPage->d = newPage->d; + oldPage->d->m_page = oldPage; + oldPage->d->m_doc = d; + newPage->d = nullptr; + + annotationsToDelete << oldPage->m_annotations; + rectsToDelete << oldPage->m_rects; + oldPage->m_annotations = newPage->m_annotations; + oldPage->m_rects = newPage->m_rects; + } + qDeleteAll( newPagesVector ); + } + + d->m_url = url; + d->m_docFileName = newFileName; + d->updateMetadataXmlNameAndDocSize(); + d->m_bookmarkManager->setUrl( d->m_url ); + + if ( d->m_synctex_scanner ) + { + synctex_scanner_free( d->m_synctex_scanner ); + d->m_synctex_scanner = synctex_scanner_new_with_output_file( QFile::encodeName( newFileName ).constData(), nullptr, 1); + if ( !d->m_synctex_scanner && QFile::exists(newFileName + QLatin1String( "sync" ) ) ) + { + d->loadSyncFile(newFileName); + } + } + + foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::UrlChanged ) ); + + qDeleteAll( annotationsToDelete ); + qDeleteAll( rectsToDelete ); + qDeleteAll( pagePrivatesToDelete ); + + return true; + } + else + { + return false; + } +} + +bool Document::swapBackingFileArchive( const QString &newFileName, const QUrl &url ) +{ + qCDebug(OkularCoreDebug) << "Swapping backing archive to" << newFileName; + + ArchiveData *newArchive = DocumentPrivate::unpackDocumentArchive( newFileName ); + if ( !newArchive ) + return false; + + const QString tempFileName = newArchive->document.fileName(); + + const bool success = swapBackingFile( tempFileName, url ); + + if ( success ) + { + delete d->m_archiveData; + d->m_archiveData = newArchive; + } + + return success; +} + +void Document::setHistoryClean( bool clean ) +{ + if ( clean ) + d->m_undoStack->setClean(); + else + d->m_undoStack->resetClean(); +} + bool Document::canSaveChanges() const { if ( !d->m_generator ) return false; Q_ASSERT( !d->m_generatorName.isEmpty() ); QHash< QString, GeneratorInfo >::iterator genIt = d->m_loadedGenerators.find( d->m_generatorName ); Q_ASSERT( genIt != d->m_loadedGenerators.end() ); SaveInterface* saveIface = d->generatorSave( genIt.value() ); if ( !saveIface ) return false; return saveIface->supportsOption( SaveInterface::SaveChanges ); } bool Document::canSaveChanges( SaveCapability cap ) const { switch ( cap ) { case SaveFormsCapability: /* Assume that if the generator supports saving, forms can be saved. * We have no means to actually query the generator at the moment * TODO: Add some method to query the generator in SaveInterface */ return canSaveChanges(); case SaveAnnotationsCapability: return d->canAddAnnotationsNatively(); } return false; } bool Document::saveChanges( const QString &fileName ) { QString errorText; return saveChanges( fileName, &errorText ); } bool Document::saveChanges( const QString &fileName, QString *errorText ) { if ( !d->m_generator || fileName.isEmpty() ) return false; Q_ASSERT( !d->m_generatorName.isEmpty() ); QHash< QString, GeneratorInfo >::iterator genIt = d->m_loadedGenerators.find( d->m_generatorName ); Q_ASSERT( genIt != d->m_loadedGenerators.end() ); SaveInterface* saveIface = d->generatorSave( genIt.value() ); if ( !saveIface || !saveIface->supportsOption( SaveInterface::SaveChanges ) ) return false; return saveIface->save( fileName, SaveInterface::SaveChanges, errorText ); } void Document::registerView( View *view ) { if ( !view ) return; Document *viewDoc = view->viewDocument(); if ( viewDoc ) { // check if already registered for this document if ( viewDoc == this ) return; viewDoc->unregisterView( view ); } d->m_views.insert( view ); view->d_func()->document = d; } void Document::unregisterView( View *view ) { if ( !view ) return; Document *viewDoc = view->viewDocument(); if ( !viewDoc || viewDoc != this ) return; view->d_func()->document = nullptr; d->m_views.remove( view ); } QByteArray Document::fontData(const FontInfo &font) const { QByteArray result; if (d->m_generator) { QMetaObject::invokeMethod(d->m_generator, "requestFontData", Qt::DirectConnection, Q_ARG(Okular::FontInfo, font), Q_ARG(QByteArray *, &result)); } return result; } -Document::OpenResult Document::openDocumentArchive( const QString & docFile, const QUrl & url, const QString & password ) +ArchiveData *DocumentPrivate::unpackDocumentArchive( const QString &archivePath ) { QMimeDatabase db; - const QMimeType mime = db.mimeTypeForFile( docFile, QMimeDatabase::MatchExtension ); + const QMimeType mime = db.mimeTypeForFile( archivePath, QMimeDatabase::MatchExtension ); if ( !mime.inherits( QStringLiteral("application/vnd.kde.okular-archive") ) ) - return OpenError; + return nullptr; - KZip okularArchive( docFile ); + KZip okularArchive( archivePath ); if ( !okularArchive.open( QIODevice::ReadOnly ) ) - return OpenError; + return nullptr; const KArchiveDirectory * mainDir = okularArchive.directory(); const KArchiveEntry * mainEntry = mainDir->entry( QStringLiteral("content.xml") ); if ( !mainEntry || !mainEntry->isFile() ) - return OpenError; + return nullptr; std::unique_ptr< QIODevice > mainEntryDevice( static_cast< const KZipFileEntry * >( mainEntry )->createDevice() ); QDomDocument doc; if ( !doc.setContent( mainEntryDevice.get() ) ) - return OpenError; + return nullptr; mainEntryDevice.reset(); QDomElement root = doc.documentElement(); - if ( root.tagName() != QLatin1String("OkularArchive") ) - return OpenError; + if ( root.tagName() != QLatin1String("OkularArchive") ) + return nullptr; QString documentFileName; QString metadataFileName; QDomElement el = root.firstChild().toElement(); for ( ; !el.isNull(); el = el.nextSibling().toElement() ) { if ( el.tagName() == QLatin1String("Files") ) { QDomElement fileEl = el.firstChild().toElement(); for ( ; !fileEl.isNull(); fileEl = fileEl.nextSibling().toElement() ) { if ( fileEl.tagName() == QLatin1String("DocumentFileName") ) documentFileName = fileEl.text(); else if ( fileEl.tagName() == QLatin1String("MetadataFileName") ) metadataFileName = fileEl.text(); } } } if ( documentFileName.isEmpty() ) - return OpenError; + return nullptr; const KArchiveEntry * docEntry = mainDir->entry( documentFileName ); if ( !docEntry || !docEntry->isFile() ) - return OpenError; + return nullptr; std::unique_ptr< ArchiveData > archiveData( new ArchiveData() ); const int dotPos = documentFileName.indexOf( QLatin1Char('.') ); if ( dotPos != -1 ) archiveData->document.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX") + documentFileName.mid(dotPos)); if ( !archiveData->document.open() ) - return OpenError; + return nullptr; + + archiveData->originalFileName = documentFileName; - QString tempFileName = archiveData->document.fileName(); { std::unique_ptr< QIODevice > docEntryDevice( static_cast< const KZipFileEntry * >( docEntry )->createDevice() ); copyQIODevice( docEntryDevice.get(), &archiveData->document ); archiveData->document.close(); } const KArchiveEntry * metadataEntry = mainDir->entry( metadataFileName ); if ( metadataEntry && metadataEntry->isFile() ) { std::unique_ptr< QIODevice > metadataEntryDevice( static_cast< const KZipFileEntry * >( metadataEntry )->createDevice() ); archiveData->metadataFile.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX.xml")); if ( archiveData->metadataFile.open() ) { copyQIODevice( metadataEntryDevice.get(), &archiveData->metadataFile ); archiveData->metadataFile.close(); } } + return archiveData.release(); +} + +Document::OpenResult Document::openDocumentArchive( const QString & docFile, const QUrl & url, const QString & password ) +{ + d->m_archiveData = DocumentPrivate::unpackDocumentArchive( docFile ); + if ( !d->m_archiveData ) + return OpenError; + + const QString tempFileName = d->m_archiveData->document.fileName(); + QMimeDatabase db; const QMimeType docMime = db.mimeTypeForFile( tempFileName, QMimeDatabase::MatchContent ); - d->m_archiveData = archiveData.get(); - d->m_archivedFileName = documentFileName; const OpenResult ret = openDocument( tempFileName, url, docMime, password ); - if ( ret == OpenSuccess ) - { - archiveData.release(); - } - else + if ( ret != OpenSuccess ) { + delete d->m_archiveData; d->m_archiveData = nullptr; } return ret; } bool Document::saveDocumentArchive( const QString &fileName ) { if ( !d->m_generator ) return false; /* If we opened an archive, use the name of original file (eg foo.pdf) * instead of the archive's one (eg foo.okular) */ - QString docFileName = d->m_archiveData ? d->m_archivedFileName : d->m_url.fileName(); + QString docFileName = d->m_archiveData ? d->m_archiveData->originalFileName : d->m_url.fileName(); if ( docFileName == QLatin1String( "-" ) ) return false; QString docPath = d->m_docFileName; const QFileInfo fi( docPath ); if ( fi.isSymLink() ) docPath = fi.symLinkTarget(); KZip okularArchive( fileName ); if ( !okularArchive.open( QIODevice::WriteOnly ) ) return false; const KUser user; #ifndef Q_OS_WIN const KUserGroup userGroup( user.groupId() ); #else const KUserGroup userGroup( QString( "" ) ); #endif QDomDocument contentDoc( QStringLiteral("OkularArchive") ); QDomProcessingInstruction xmlPi = contentDoc.createProcessingInstruction( QStringLiteral( "xml" ), QStringLiteral( "version=\"1.0\" encoding=\"utf-8\"" ) ); contentDoc.appendChild( xmlPi ); QDomElement root = contentDoc.createElement( QStringLiteral("OkularArchive") ); contentDoc.appendChild( root ); QDomElement filesNode = contentDoc.createElement( QStringLiteral("Files") ); root.appendChild( filesNode ); QDomElement fileNameNode = contentDoc.createElement( QStringLiteral("DocumentFileName") ); filesNode.appendChild( fileNameNode ); fileNameNode.appendChild( contentDoc.createTextNode( docFileName ) ); QDomElement metadataFileNameNode = contentDoc.createElement( QStringLiteral("MetadataFileName") ); filesNode.appendChild( metadataFileNameNode ); metadataFileNameNode.appendChild( contentDoc.createTextNode( QStringLiteral("metadata.xml") ) ); // If the generator can save annotations natively, do it QTemporaryFile modifiedFile; bool annotationsSavedNatively = false; - if ( d->canAddAnnotationsNatively() ) + bool formsSavedNatively = false; + if ( d->canAddAnnotationsNatively() || canSaveChanges( SaveFormsCapability ) ) { if ( !modifiedFile.open() ) return false; modifiedFile.close(); // We're only interested in the file name QString errorText; if ( saveChanges( modifiedFile.fileName(), &errorText ) ) { docPath = modifiedFile.fileName(); // Save this instead of the original file - annotationsSavedNatively = true; + annotationsSavedNatively = d->canAddAnnotationsNatively(); + formsSavedNatively = canSaveChanges( SaveFormsCapability ); } else { qCWarning(OkularCoreDebug) << "saveChanges failed: " << errorText; qCDebug(OkularCoreDebug) << "Falling back to saving a copy of the original file"; } } + PageItems saveWhat = None; + if ( !annotationsSavedNatively ) + saveWhat |= AnnotationPageItems; + if ( !formsSavedNatively ) + saveWhat |= FormFieldPageItems; + QTemporaryFile metadataFile; - PageItems saveWhat = annotationsSavedNatively ? None : AnnotationPageItems; if ( !d->savePageDocumentInfo( &metadataFile, saveWhat ) ) return false; const QByteArray contentDocXml = contentDoc.toByteArray(); const mode_t perm = 0100644; okularArchive.writeFile( QStringLiteral("content.xml"), contentDocXml, perm, user.loginName(), userGroup.name() ); okularArchive.addLocalFile( docPath, docFileName ); okularArchive.addLocalFile( metadataFile.fileName(), QStringLiteral("metadata.xml") ); if ( !okularArchive.close() ) return false; return true; } +bool Document::extractArchivedFile( const QString &destFileName ) +{ + if ( !d->m_archiveData ) + return false; + + // Remove existing file, if present (QFile::copy doesn't overwrite by itself) + QFile::remove( destFileName ); + + return d->m_archiveData->document.copy( destFileName ); +} + QPrinter::Orientation Document::orientation() const { double width, height; int landscape, portrait; const Okular::Page *currentPage; // if some pages are landscape and others are not, the most common wins, as // QPrinter does not accept a per-page setting landscape = 0; portrait = 0; for (uint i = 0; i < pages(); i++) { currentPage = page(i); width = currentPage->width(); height = currentPage->height(); if (currentPage->orientation() == Okular::Rotation90 || currentPage->orientation() == Okular::Rotation270) qSwap(width, height); if (width > height) landscape++; else portrait++; } return (landscape > portrait) ? QPrinter::Landscape : QPrinter::Portrait; } void Document::setAnnotationEditingEnabled( bool enable ) { d->m_annotationEditingEnabled = enable; foreachObserver( notifySetup( d->m_pagesVector, 0 ) ); } void Document::walletDataForFile( const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey ) const { if (d->m_generator) { d->m_generator->walletDataForFile( fileName, walletName, walletFolder, walletKey ); } else if (d->m_walletGenerator) { d->m_walletGenerator->walletDataForFile( fileName, walletName, walletFolder, walletKey ); } } +bool Document::isDocdataMigrationNeeded() const +{ + return d->m_docdataMigrationNeeded; +} + +void Document::docdataMigrationDone() +{ + if (d->m_docdataMigrationNeeded) + { + d->m_docdataMigrationNeeded = false; + foreachObserver( notifySetup( d->m_pagesVector, 0 ) ); + } +} + QAbstractItemModel * Document::layersModel() const { return d->m_generator ? d->m_generator->layersModel() : nullptr; } void DocumentPrivate::requestDone( PixmapRequest * req ) { if ( !req ) return; if ( !m_generator || m_closingLoop ) { m_pixmapRequestsMutex.lock(); m_executingPixmapRequests.removeAll( req ); m_pixmapRequestsMutex.unlock(); delete req; if ( m_closingLoop ) m_closingLoop->exit(); return; } #ifndef NDEBUG if ( !m_generator->canGeneratePixmap() ) qCDebug(OkularCoreDebug) << "requestDone with generator not in READY state."; #endif // [MEM] 1.1 find and remove a previous entry for the same page and id QLinkedList< AllocatedPixmap * >::iterator aIt = m_allocatedPixmaps.begin(); QLinkedList< AllocatedPixmap * >::iterator aEnd = m_allocatedPixmaps.end(); for ( ; aIt != aEnd; ++aIt ) if ( (*aIt)->page == req->pageNumber() && (*aIt)->observer == req->observer() ) { AllocatedPixmap * p = *aIt; m_allocatedPixmaps.erase( aIt ); m_allocatedPixmapsTotalMemory -= p->memory; delete p; break; } DocumentObserver *observer = req->observer(); if ( m_observers.contains(observer) ) { // [MEM] 1.2 append memory allocation descriptor to the FIFO qulonglong memoryBytes = 0; const TilesManager *tm = req->d->tilesManager(); if ( tm ) memoryBytes = tm->totalMemory(); else memoryBytes = 4 * req->width() * req->height(); AllocatedPixmap * memoryPage = new AllocatedPixmap( req->observer(), req->pageNumber(), memoryBytes ); m_allocatedPixmaps.append( memoryPage ); m_allocatedPixmapsTotalMemory += memoryBytes; // 2. notify an observer that its pixmap changed observer->notifyPageChanged( req->pageNumber(), DocumentObserver::Pixmap ); } #ifndef NDEBUG else qCWarning(OkularCoreDebug) << "Receiving a done request for the defunct observer" << observer; #endif // 3. delete request m_pixmapRequestsMutex.lock(); m_executingPixmapRequests.removeAll( req ); m_pixmapRequestsMutex.unlock(); delete req; // 4. start a new generation if some is pending m_pixmapRequestsMutex.lock(); bool hasPixmaps = !m_pixmapRequestsStack.isEmpty(); m_pixmapRequestsMutex.unlock(); if ( hasPixmaps ) sendGeneratorPixmapRequest(); } void DocumentPrivate::setPageBoundingBox( int page, const NormalizedRect& boundingBox ) { Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; if ( kp->boundingBox() == boundingBox ) return; kp->setBoundingBox( boundingBox ); // notify observers about the change foreachObserverD( notifyPageChanged( page, DocumentObserver::BoundingBox ) ); // TODO: For generators that generate the bbox by pixmap scanning, if the first generated pixmap is very small, the bounding box will forever be inaccurate. // TODO: Crop computation should also consider annotations, actions, etc. to make sure they're not cropped away. // TODO: Help compute bounding box for generators that create a QPixmap without a QImage, like text and plucker. // TODO: Don't compute the bounding box if no one needs it (e.g., Trim Borders is off). } void DocumentPrivate::calculateMaxTextPages() { int multipliers = qMax(1, qRound(getTotalMemory() / 536870912.0)); // 512 MB switch (SettingsCore::memoryLevel()) { case SettingsCore::EnumMemoryLevel::Low: m_maxAllocatedTextPages = multipliers * 2; break; case SettingsCore::EnumMemoryLevel::Normal: m_maxAllocatedTextPages = multipliers * 50; break; case SettingsCore::EnumMemoryLevel::Aggressive: m_maxAllocatedTextPages = multipliers * 250; break; case SettingsCore::EnumMemoryLevel::Greedy: m_maxAllocatedTextPages = multipliers * 1250; break; } } void DocumentPrivate::textGenerationDone( Page *page ) { if ( !m_pageController ) return; // 1. If we reached the cache limit, delete the first text page from the fifo if (m_allocatedTextPagesFifo.size() == m_maxAllocatedTextPages) { int pageToKick = m_allocatedTextPagesFifo.takeFirst(); if (pageToKick != page->number()) // this should never happen but better be safe than sorry { m_pagesVector.at(pageToKick)->setTextPage( nullptr ); // deletes the textpage } } // 2. Add the page to the fifo of generated text pages m_allocatedTextPagesFifo.append( page->number() ); } void Document::setRotation( int r ) { d->setRotationInternal( r, true ); } void DocumentPrivate::setRotationInternal( int r, bool notify ) { Rotation rotation = (Rotation)r; if ( !m_generator || ( m_rotation == rotation ) ) return; // tell the pages to rotate QVector< Okular::Page * >::const_iterator pIt = m_pagesVector.constBegin(); QVector< Okular::Page * >::const_iterator pEnd = m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->rotateAt( rotation ); if ( notify ) { // notify the generator that the current rotation has changed m_generator->rotationChanged( rotation, m_rotation ); } // set the new rotation m_rotation = rotation; if ( notify ) { foreachObserverD( notifySetup( m_pagesVector, DocumentObserver::NewLayoutForPages ) ); foreachObserverD( notifyContentsCleared( DocumentObserver::Pixmap | DocumentObserver::Highlights | DocumentObserver::Annotations ) ); } qCDebug(OkularCoreDebug) << "Rotated:" << r; } void Document::setPageSize( const PageSize &size ) { if ( !d->m_generator || !d->m_generator->hasFeature( Generator::PageSizes ) ) return; if ( d->m_pageSizes.isEmpty() ) d->m_pageSizes = d->m_generator->pageSizes(); int sizeid = d->m_pageSizes.indexOf( size ); if ( sizeid == -1 ) return; // tell the pages to change size QVector< Okular::Page * >::const_iterator pIt = d->m_pagesVector.constBegin(); QVector< Okular::Page * >::const_iterator pEnd = d->m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->changeSize( size ); // clear 'memory allocation' descriptors qDeleteAll( d->m_allocatedPixmaps ); d->m_allocatedPixmaps.clear(); d->m_allocatedPixmapsTotalMemory = 0; // notify the generator that the current page size has changed d->m_generator->pageSizeChanged( size, d->m_pageSize ); // set the new page size d->m_pageSize = size; foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::NewLayoutForPages ) ); foreachObserver( notifyContentsCleared( DocumentObserver::Pixmap | DocumentObserver::Highlights ) ); qCDebug(OkularCoreDebug) << "New PageSize id:" << sizeid; } /** DocumentViewport **/ DocumentViewport::DocumentViewport( int n ) : pageNumber( n ) { // default settings rePos.enabled = false; rePos.normalizedX = 0.5; rePos.normalizedY = 0.0; rePos.pos = Center; autoFit.enabled = false; autoFit.width = false; autoFit.height = false; } DocumentViewport::DocumentViewport( const QString & xmlDesc ) : pageNumber( -1 ) { // default settings (maybe overridden below) rePos.enabled = false; rePos.normalizedX = 0.5; rePos.normalizedY = 0.0; rePos.pos = Center; autoFit.enabled = false; autoFit.width = false; autoFit.height = false; // check for string presence if ( xmlDesc.isEmpty() ) return; // decode the string bool ok; int field = 0; QString token = xmlDesc.section( QLatin1Char(';'), field, field ); while ( !token.isEmpty() ) { // decode the current token if ( field == 0 ) { pageNumber = token.toInt( &ok ); if ( !ok ) return; } else if ( token.startsWith( QLatin1String("C1") ) ) { rePos.enabled = true; rePos.normalizedX = token.section( QLatin1Char(':'), 1, 1 ).toDouble(); rePos.normalizedY = token.section( QLatin1Char(':'), 2, 2 ).toDouble(); rePos.pos = Center; } else if ( token.startsWith( QLatin1String("C2") ) ) { rePos.enabled = true; rePos.normalizedX = token.section( QLatin1Char(':'), 1, 1 ).toDouble(); rePos.normalizedY = token.section( QLatin1Char(':'), 2, 2 ).toDouble(); if (token.section( QLatin1Char(':'), 3, 3 ).toInt() == 1) rePos.pos = Center; else rePos.pos = TopLeft; } else if ( token.startsWith( QLatin1String("AF1") ) ) { autoFit.enabled = true; autoFit.width = token.section( QLatin1Char(':'), 1, 1 ) == QLatin1String("T"); autoFit.height = token.section( QLatin1Char(':'), 2, 2 ) == QLatin1String("T"); } // proceed tokenizing string field++; token = xmlDesc.section( QLatin1Char(';'), field, field ); } } QString DocumentViewport::toString() const { // start string with page number QString s = QString::number( pageNumber ); // if has center coordinates, save them on string if ( rePos.enabled ) s += QStringLiteral( ";C2:" ) + QString::number( rePos.normalizedX ) + QLatin1Char(':') + QString::number( rePos.normalizedY ) + QLatin1Char(':') + QString::number( rePos.pos ); // if has autofit enabled, save its state on string if ( autoFit.enabled ) s += QStringLiteral( ";AF1:" ) + (autoFit.width ? QLatin1Char('T') : QLatin1Char('F')) + QLatin1Char(':') + (autoFit.height ? QLatin1Char('T') : QLatin1Char('F')); return s; } bool DocumentViewport::isValid() const { return pageNumber >= 0; } bool DocumentViewport::operator==( const DocumentViewport & vp ) const { bool equal = ( pageNumber == vp.pageNumber ) && ( rePos.enabled == vp.rePos.enabled ) && ( autoFit.enabled == vp.autoFit.enabled ); if ( !equal ) return false; if ( rePos.enabled && (( rePos.normalizedX != vp.rePos.normalizedX) || ( rePos.normalizedY != vp.rePos.normalizedY ) || rePos.pos != vp.rePos.pos) ) return false; if ( autoFit.enabled && (( autoFit.width != vp.autoFit.width ) || ( autoFit.height != vp.autoFit.height )) ) return false; return true; } bool DocumentViewport::operator<( const DocumentViewport & vp ) const { // TODO: Check autoFit and Position if ( pageNumber != vp.pageNumber ) return pageNumber < vp.pageNumber; if ( !rePos.enabled && vp.rePos.enabled ) return true; if ( !vp.rePos.enabled ) return false; if ( rePos.normalizedY != vp.rePos.normalizedY ) return rePos.normalizedY < vp.rePos.normalizedY; return rePos.normalizedX < vp.rePos.normalizedX; } /** DocumentInfo **/ DocumentInfo::DocumentInfo() : d(new DocumentInfoPrivate()) { } DocumentInfo::DocumentInfo(const DocumentInfo &info) : d(new DocumentInfoPrivate()) { *this = info; } DocumentInfo& DocumentInfo::operator=(const DocumentInfo &info) { d->values = info.d->values; d->titles = info.d->titles; return *this; } DocumentInfo::~DocumentInfo() { delete d; } void DocumentInfo::set( const QString &key, const QString &value, const QString &title ) { d->values[ key ] = value; d->titles[ key ] = title; } void DocumentInfo::set( Key key, const QString &value ) { d->values[ getKeyString( key ) ] = value; } QStringList DocumentInfo::keys() const { return d->values.keys(); } QString DocumentInfo::get( Key key ) const { return get( getKeyString( key ) ); } QString DocumentInfo::get( const QString &key ) const { return d->values[ key ]; } QString DocumentInfo::getKeyString( Key key ) //const { switch ( key ) { case Title: return QStringLiteral("title"); break; case Subject: return QStringLiteral("subject"); break; case Description: return QStringLiteral("description"); break; case Author: return QStringLiteral("author"); break; case Creator: return QStringLiteral("creator"); break; case Producer: return QStringLiteral("producer"); break; case Copyright: return QStringLiteral("copyright"); break; case Pages: return QStringLiteral("pages"); break; case CreationDate: return QStringLiteral("creationDate"); break; case ModificationDate: return QStringLiteral("modificationDate"); break; case MimeType: return QStringLiteral("mimeType"); break; case Category: return QStringLiteral("category"); break; case Keywords: return QStringLiteral("keywords"); break; case FilePath: return QStringLiteral("filePath"); break; case DocumentSize: return QStringLiteral("documentSize"); break; case PagesSize: return QStringLiteral("pageSize"); break; default: qCWarning(OkularCoreDebug) << "Unknown" << key; return QString(); break; } } DocumentInfo::Key DocumentInfo::getKeyFromString( const QString &key ) //const { if (key == QLatin1String("title")) return Title; else if (key == QLatin1String("subject")) return Subject; else if (key == QLatin1String("description")) return Description; else if (key == QLatin1String("author")) return Author; else if (key == QLatin1String("creator")) return Creator; else if (key == QLatin1String("producer")) return Producer; else if (key == QLatin1String("copyright")) return Copyright; else if (key == QLatin1String("pages")) return Pages; else if (key == QLatin1String("creationDate")) return CreationDate; else if (key == QLatin1String("modificationDate")) return ModificationDate; else if (key == QLatin1String("mimeType")) return MimeType; else if (key == QLatin1String("category")) return Category; else if (key == QLatin1String("keywords")) return Keywords; else if (key == QLatin1String("filePath")) return FilePath; else if (key == QLatin1String("documentSize")) return DocumentSize; else if (key == QLatin1String("pageSize")) return PagesSize; else return Invalid; } QString DocumentInfo::getKeyTitle( Key key ) //const { switch ( key ) { case Title: return i18n( "Title" ); break; case Subject: return i18n( "Subject" ); break; case Description: return i18n( "Description" ); break; case Author: return i18n( "Author" ); break; case Creator: return i18n( "Creator" ); break; case Producer: return i18n( "Producer" ); break; case Copyright: return i18n( "Copyright" ); break; case Pages: return i18n( "Pages" ); break; case CreationDate: return i18n( "Created" ); break; case ModificationDate: return i18n( "Modified" ); break; case MimeType: return i18n( "Mime Type" ); break; case Category: return i18n( "Category" ); break; case Keywords: return i18n( "Keywords" ); break; case FilePath: return i18n( "File Path" ); break; case DocumentSize: return i18n( "File Size" ); break; case PagesSize: return i18n("Page Size"); break; default: return QString(); break; } } QString DocumentInfo::getKeyTitle( const QString &key ) const { QString title = getKeyTitle ( getKeyFromString( key ) ); if ( title.isEmpty() ) title = d->titles[ key ]; return title; } /** DocumentSynopsis **/ DocumentSynopsis::DocumentSynopsis() : QDomDocument( QStringLiteral("DocumentSynopsis") ) { // void implementation, only subclassed for naming } DocumentSynopsis::DocumentSynopsis( const QDomDocument &document ) : QDomDocument( document ) { } /** EmbeddedFile **/ EmbeddedFile::EmbeddedFile() { } EmbeddedFile::~EmbeddedFile() { } VisiblePageRect::VisiblePageRect( int page, const NormalizedRect &rectangle ) : pageNumber( page ), rect( rectangle ) { } #undef foreachObserver #undef foreachObserverD #include "moc_document.cpp" /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/document.h b/core/document.h index a52dedd20..f996f41f6 100644 --- a/core/document.h +++ b/core/document.h @@ -1,1343 +1,1422 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2008 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. * ***************************************************************************/ #ifndef _OKULAR_DOCUMENT_H_ #define _OKULAR_DOCUMENT_H_ #include "okularcore_export.h" #include "area.h" #include "global.h" #include "pagesize.h" #include #include #include #include #include #include #include class QPrintDialog; class KBookmark; class KConfigDialog; class KPluginMetaData; class KXMLGUIClient; class DocumentItem; class QAbstractItemModel; namespace Okular { class Annotation; class BookmarkManager; class DocumentInfoPrivate; class DocumentObserver; class DocumentPrivate; class DocumentSynopsis; class DocumentViewport; class EmbeddedFile; class ExportFormat; class FontInfo; class FormFieldText; class FormFieldButton; class FormFieldChoice; class Generator; class Action; class MovieAction; class Page; class PixmapRequest; class RenditionAction; class SourceReference; class View; class VisiblePageRect; /** IDs for seaches. Globally defined here. **/ #define PART_SEARCH_ID 1 #define PAGEVIEW_SEARCH_ID 2 #define SW_SEARCH_ID 3 #define PRESENTATION_SEARCH_ID 4 /** * The DocumentInfo structure can be filled in by generators to display * metadata about the currently opened file. */ class OKULARCORE_EXPORT DocumentInfo { friend class Document; public: /** * The list of predefined keys. */ enum Key { Title, ///< The title of the document Subject, ///< The subject of the document Description, ///< The description of the document Author, ///< The author of the document Creator, ///< The creator of the document (this can be different from the author) Producer, ///< The producer of the document (e.g. some software) Copyright, ///< The copyright of the document Pages, ///< The number of pages of the document CreationDate, ///< The date of creation of the document ModificationDate, ///< The date of last modification of the document MimeType, ///< The mime type of the document Category, ///< The category of the document Keywords, ///< The keywords which describe the content of the document FilePath, ///< The path of the file @since 0.10 (KDE 4.4) DocumentSize, ///< The size of the document @since 0.10 (KDE 4.4) PagesSize, ///< The size of the pages (if all pages have the same size) @since 0.10 (KDE 4.4) CustomKeys, ///< All the custom keys the generator supports @since 0.21 Invalid ///< An invalid key @since 0.21. It will always be the last element in the enum }; /** * Creates a new document info. */ DocumentInfo(); DocumentInfo(const DocumentInfo &info); DocumentInfo& operator=( const DocumentInfo& ); ~DocumentInfo(); /** * Returns all the keys present in this DocumentInfo * * @since 0.21 */ QStringList keys() const; /** * Returns the value for a given key or an null string when the * key doesn't exist. */ QString get( Key key ) const; /** * Returns the value for a given key or an null string when the * key doesn't exist. */ QString get( const QString &key ) const; /** * Sets a value for a custom key. The title should be an i18n'ed * string, since it's used in the document information dialog. */ void set( const QString &key, const QString &value, const QString &title = QString() ); /** * Sets a value for a special key. The title should be an i18n'ed * string, since it's used in the document information dialog. */ void set( Key key, const QString &value ); /** * Returns the user visible string for the given key * Takes into account keys added by the set() that takes a QString * * @since 0.21 */ QString getKeyTitle( const QString &key ) const; /** * Returns the internal string for the given key * @since 0.10 (KDE 4.4) */ static QString getKeyString( Key key ); /** * Returns the user visible string for the given key * @since 0.10 (KDE 4.4) */ static QString getKeyTitle( Key key ); /** * Returns the Key from a string key * @since 0.21 */ static Key getKeyFromString( const QString &key ); private: DocumentInfoPrivate *d; }; /** * @short The Document. Heart of everything. Actions take place here. * * The Document is the main object in Okular. All views query the Document to * get data/properties or even for accessing pages (in a 'const' way). * * It is designed to keep it detached from the document type (pdf, ps, you * name it..) so whenever you want to get some data, it asks its internal * generators to do the job and return results in a format-indepedent way. * * Apart from the generator (the currently running one) the document stores * all the Pages ('Page' class) of the current document in a vector and * notifies all the registered DocumentObservers when some content changes. * * For a better understanding of hierarchies @see README.internals.png * @see DocumentObserver, Page */ class OKULARCORE_EXPORT Document : public QObject { Q_OBJECT public: /** * Creates a new document with the given @p widget as widget to relay GUI things (messageboxes, ...). */ explicit Document( QWidget *widget ); /** * Destroys the document. */ ~Document(); /** * Describes the result of an open document operation. * @since 0.20 (KDE 4.14) */ enum OpenResult { OpenSuccess, //< The document was opened successfully OpenError, //< The document failed to open OpenNeedsPassword //< The document needs a password to be opened or the one provided is not the correct }; /** * Opens the document. * @since 0.20 (KDE 4.14) */ OpenResult openDocument( const QString & docFile, const QUrl & url, const QMimeType &mime, const QString &password = QString() ); /** * Closes the document. */ void closeDocument(); /** * Registers a new @p observer for the document. */ void addObserver( DocumentObserver *observer ); /** * Unregisters the given @p observer for the document. */ void removeObserver( DocumentObserver *observer ); /** * Reparses and applies the configuration. */ void reparseConfig(); /** * Returns whether the document is currently opened. */ bool isOpened() const; /** * Returns the meta data of the document. */ DocumentInfo documentInfo() const; /** * Returns the asked set of meta data of the document. The result may contain more * metadata than the one asked for. */ DocumentInfo documentInfo( const QSet &keys ) const; /** * Returns the table of content of the document or 0 if no * table of content is available. */ const DocumentSynopsis * documentSynopsis() const; /** * Starts the reading of the information about the fonts in the * document, if available. * * The results as well the end of the reading is notified using the * signals gotFont(), fontReadingProgress() and fontReadingEnded() */ void startFontReading(); /** * Force the termination of the reading of the information about the * fonts in the document, if running. */ void stopFontReading(); /** * Whether the current document can provide information about the * fonts used in it. */ bool canProvideFontInformation() const; /** * Returns the list of embedded files or 0 if no embedded files * are available. */ const QList *embeddedFiles() const; /** * Returns the page object for the given page @p number or 0 * if the number is out of range. */ const Page * page( int number ) const; /** * Returns the current viewport of the document. */ const DocumentViewport & viewport() const; /** * Sets the list of visible page rectangles. * @see VisiblePageRect */ void setVisiblePageRects( const QVector< VisiblePageRect * > & visiblePageRects, DocumentObserver *excludeObserver = nullptr ); /** * Returns the list of visible page rectangles. */ const QVector< VisiblePageRect * > & visiblePageRects() const; /** * Returns the number of the current page. */ uint currentPage() const; /** * Returns the number of pages of the document. */ uint pages() const; /** * Returns the url of the currently opened document. */ QUrl currentDocument() const; /** * Returns whether the given @p action is allowed in the document. * @see @ref Permission */ bool isAllowed( Permission action ) const; /** * Returns whether the document supports searching. */ bool supportsSearching() const; /** * Returns whether the document supports the listing of page sizes. */ bool supportsPageSizes() const; /** * Returns whether the current document supports tiles * * @since 0.16 (KDE 4.10) */ bool supportsTiles() const; /** * Returns the list of supported page sizes or an empty list if this * feature is not available. * @see supportsPageSizes() */ PageSize::List pageSizes() const; /** * Returns whether the document supports the export to ASCII text. */ bool canExportToText() const; /** * Exports the document as ASCII text and saves it under @p fileName. */ bool exportToText( const QString& fileName ) const; /** * Returns the list of supported export formats. * @see ExportFormat */ QList exportFormats() const; /** * Exports the document in the given @p format and saves it under @p fileName. */ bool exportTo( const QString& fileName, const ExportFormat& format ) const; /** * Returns whether the document history is at the begin. */ bool historyAtBegin() const; /** * Returns whether the document history is at the end. */ bool historyAtEnd() const; /** * Returns the meta data for the given @p key and @p option or an empty variant * if the key doesn't exists. */ QVariant metaData( const QString & key, const QVariant & option = QVariant() ) const; /** * Returns the current rotation of the document. */ Rotation rotation() const; /** * If all pages have the same size this method returns it, if the page sizes * differ an empty size object is returned. */ QSizeF allPagesSize() const; /** * Returns the size string for the given @p page or an empty string * if the page is out of range. */ QString pageSizeString( int page ) const; /** * Returns the gui client of the generator, if it provides one. */ KXMLGUIClient* guiClient(); /** * Sets the current document viewport to the given @p page. * * @param excludeObserver The observer ids which shouldn't be effected by this change. * @param smoothMove Whether the move shall be animated smoothly. */ void setViewportPage( int page, DocumentObserver *excludeObserver = nullptr, bool smoothMove = false ); /** * Sets the current document viewport to the given @p viewport. * * @param excludeObserver The observer which shouldn't be effected by this change. * @param smoothMove Whether the move shall be animated smoothly. */ void setViewport( const DocumentViewport &viewport, DocumentObserver *excludeObserver = nullptr, bool smoothMove = false ); /** * Sets the current document viewport to the next viewport in the * viewport history. */ void setPrevViewport(); /** * Sets the current document viewport to the previous viewport in the * viewport history. */ void setNextViewport(); /** * Sets the next @p viewport in the viewport history. */ void setNextDocumentViewport( const DocumentViewport &viewport ); /** * Sets the next @p namedDestination in the viewport history. * * @since 0.9 (KDE 4.3) */ void setNextDocumentDestination( const QString &namedDestination ); /** * Sets the zoom for the current document. */ void setZoom( int factor, DocumentObserver *excludeObserver = nullptr ); /** * Describes the possible options for the pixmap requests. */ enum PixmapRequestFlag { NoOption = 0, ///< No options RemoveAllPrevious = 1 ///< Remove all the previous requests, even for non requested page pixmaps }; Q_DECLARE_FLAGS( PixmapRequestFlags, PixmapRequestFlag ) /** * Sends @p requests for pixmap generation. * * The same as requestPixmaps( requests, RemoveAllPrevious ); */ void requestPixmaps( const QLinkedList &requests ); /** * Sends @p requests for pixmap generation. * * @param reqOptions the options for the request * * @since 0.7 (KDE 4.1) */ void requestPixmaps( const QLinkedList &requests, PixmapRequestFlags reqOptions ); /** * Sends a request for text page generation for the given page @p number. */ void requestTextPage( uint number ); /** * Adds a new @p annotation to the given @p page. */ void addPageAnnotation( int page, Annotation *annotation ); /** * Tests if the @p annotation can be modified * * @since 0.15 (KDE 4.9) */ bool canModifyPageAnnotation( const Annotation * annotation ) const; /** * Prepares to modify the properties of the given @p annotation. * Must be called before the annotation's properties are modified * * @since 0.17 (KDE 4.11) */ void prepareToModifyAnnotationProperties( Annotation * annotation ); /** * Modifies the given @p annotation on the given @p page. * Must be preceded by a call to prepareToModifyAnnotationProperties before * the annotation's properties are modified * * @since 0.17 (KDE 4.11) */ void modifyPageAnnotationProperties( int page, Annotation * annotation ); /** * Translates the position of the given @p annotation on the given @p page by a distance @p delta in normalized coordinates. * * Consecutive translations applied to the same @p annotation are merged together on the undo stack if the * BeingMoved flag is set on the @P annotation. * * @since 0.17 (KDE 4.11) */ void translatePageAnnotation( int page, Annotation *annotation, const Okular::NormalizedPoint & delta ); /** * Adjusts the position of the top-left and bottom-right corners of given @p annotation on the given @p page. * * Can be used to implement resize functionality. * @p delta1 in normalized coordinates is added to top-left. * @p delta2 in normalized coordinates is added to bottom-right. * * Consecutive adjustments applied to the same @p annotation are merged together on the undo stack if the * BeingResized flag is set on the @P annotation. * * @since 1.1.0 */ void adjustPageAnnotation( int page, Annotation * annotation, const Okular::NormalizedPoint & delta1, const Okular::NormalizedPoint & delta2 ); /** * Edits the plain text contents of the given @p annotation on the given @p page. * * The contents are set to @p newContents with cursor position @p newCursorPos. * The previous cursor position @p prevCursorPos and previous anchor position @p prevAnchorPos * must also be supplied so that they can be restored if the edit action is undone. * * The Annotation's internal contents should not be modified prior to calling this method. * * @since 0.17 (KDE 4.11) */ void editPageAnnotationContents( int page, Annotation* annotation, const QString & newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos ); /** * Tests if the @p annotation can be removed * * @since 0.15 (KDE 4.9) */ bool canRemovePageAnnotation( const Annotation * annotation ) const; /** * Removes the given @p annotation from the given @p page. */ void removePageAnnotation( int page, Annotation *annotation ); /** * Removes the given @p annotations from the given @p page. */ void removePageAnnotations( int page, const QList &annotations ); /** * Sets the text selection for the given @p page. * * @param rect The rectangle of the selection. * @param color The color of the selection. */ void setPageTextSelection( int page, RegularAreaRect * rect, const QColor & color ); /** * Returns true if there is an undo command available; otherwise returns false. * @since 0.17 (KDE 4.11) */ bool canUndo() const; /** * Returns true if there is a redo command available; otherwise returns false. * @since 0.17 (KDE 4.11) */ bool canRedo() const; /** * Describes the possible search types. */ enum SearchType { NextMatch, ///< Search next match PreviousMatch, ///< Search previous match AllDocument, ///< Search complete document GoogleAll, ///< Search complete document (all words in google style) GoogleAny ///< Search complete document (any words in google style) }; /** * Describes how search ended */ // TODO remove EndOfDocumentReached when we break API enum SearchStatus { MatchFound, ///< Any match was found NoMatchFound, ///< No match was found SearchCancelled, ///< The search was cancelled EndOfDocumentReached ///< This is not ever emitted since 1.3. The end of document was reached without any match @since 0.20 (KDE 4.14) }; /** * Searches the given @p text in the document. * * @param searchID The unique id for this search request. * @param fromStart Whether the search should be started at begin of the document. * @param caseSensitivity Whether the search is case sensitive. * @param type The type of the search. @ref SearchType * @param moveViewport Whether the viewport shall be moved to the position of the matches. * @param color The highlighting color of the matches. */ void searchText( int searchID, const QString & text, bool fromStart, Qt::CaseSensitivity caseSensitivity, SearchType type, bool moveViewport, const QColor & color ); /** * Continues the search for the given @p searchID. */ void continueSearch( int searchID ); /** * Continues the search for the given @p searchID, optionally specifying * a new type for the search. * * @since 0.7 (KDE 4.1) */ void continueSearch( int searchID, SearchType type ); /** * Resets the search for the given @p searchID. */ void resetSearch( int searchID ); /** * Returns the bookmark manager of the document. */ BookmarkManager * bookmarkManager() const; /** * Processes the given @p action. */ void processAction( const Action *action ); /** * Returns a list of the bookmarked.pages */ QList bookmarkedPageList() const; /** * Returns the range of the bookmarked.pages */ QString bookmarkedPageRange() const; /** * Processes/Executes the given source @p reference. */ void processSourceReference( const SourceReference *reference ); /** * Returns whether the document can configure the printer itself. */ bool canConfigurePrinter() const; /** * What type of printing a document supports */ enum PrintingType { NoPrinting, ///< Printing Not Supported NativePrinting, ///< Native Cross-Platform Printing PostscriptPrinting ///< Postscript file printing }; /** * Returns what sort of printing the document supports: * Native, Postscript, None */ PrintingType printingSupport() const; /** * Returns whether the document supports printing to both PDF and PS files. */ bool supportsPrintToFile() const; /** * Prints the document to the given @p printer. */ bool print( QPrinter &printer ); /** * Returns the last print error in case print() failed * @since 0.11 (KDE 4.5) */ QString printError() const; /** * Returns a custom printer configuration page or 0 if no * custom printer configuration page is available. */ QWidget* printConfigurationWidget() const; /** * Fill the KConfigDialog @p dialog with the setting pages of the * generators. */ void fillConfigDialog( KConfigDialog * dialog ); /** * Returns the number of generators that have a configuration widget. */ int configurableGenerators() const; /** * Returns the list with the supported MIME types. */ QStringList supportedMimeTypes() const; /** * Returns the metadata associated with the generator. May be invalid. */ KPluginMetaData generatorInfo() const; + /** + * Returns whether the generator supports hot-swapping the current file + * with another identical file + * + * @since 1.3 + */ + bool canSwapBackingFile() const; + + /** + * Reload the document from a new location, without any visible effect + * to the user. + * + * The new file must be identical to the current one or, if the document + * has been modified (eg the user edited forms and annotations), the new + * document must have these changes too. For example, you can call + * saveChanges first to write changes to a file and then swapBackingFile + * to switch to the new location. + * + * @since 1.3 + */ + bool swapBackingFile( const QString &newFileName, const QUrl &url ); + + /** + * Same as swapBackingFile, but newFileName must be a .okular file. + * + * The new file must be identical to the current one or, if the document + * has been modified (eg the user edited forms and annotations), the new + * document must have these changes too. For example, you can call + * saveDocumentArchive first to write changes to a file and then + * swapBackingFileArchive to switch to the new location. + * + * @since 1.3 + */ + bool swapBackingFileArchive( const QString &newFileName, const QUrl &url ); + + /** + * Sets the history to be clean + * + * @since 1.3 + */ + void setHistoryClean( bool clean ); + /** * Saving capabilities. Their availability varies according to the * underlying generator and/or the document type. * * @see canSaveChanges (SaveCapability) * @since 0.15 (KDE 4.9) */ enum SaveCapability { SaveFormsCapability = 1, ///< Can save form changes SaveAnnotationsCapability = 2 ///< Can save annotation changes }; /** * Returns whether it's possible to save a given category of changes to * another document. * * @since 0.15 (KDE 4.9) */ bool canSaveChanges( SaveCapability cap ) const; /** * Returns whether the changes to the document (modified annotations, * values in form fields, etc) can be saved to another document. * * Equivalent to the logical OR of canSaveChanges(SaveCapability) for * each capability. * * @since 0.7 (KDE 4.1) */ bool canSaveChanges() const; /** * Save the document and the optional changes to it to the specified * @p fileName. * * @since 0.7 (KDE 4.1) */ bool saveChanges( const QString &fileName ); /** * Save the document and the optional changes to it to the specified * @p fileName and returns a @p errorText if fails. * * @since 0.10 (KDE 4.4) */ bool saveChanges( const QString &fileName, QString *errorText ); /** * Register the specified @p view for the current document. * * It is unregistered from the previous document, if any. * * @since 0.7 (KDE 4.1) */ void registerView( View *view ); /** * Unregister the specified @p view from the current document. * * @since 0.7 (KDE 4.1) */ void unregisterView( View *view ); /** * Gets the font data for the given font * * @since 0.8 (KDE 4.2) */ QByteArray fontData(const FontInfo &font) const; /** * Opens a document archive. * * @since 0.20 (KDE 4.14) */ OpenResult openDocumentArchive( const QString & docFile, const QUrl & url, const QString &password = QString() ); /** * Saves a document archive. * * @since 0.8 (KDE 4.2) */ bool saveDocumentArchive( const QString &fileName ); + /** + * Extract the document file from the current archive. + * + * @warning This function only works if the current file is a document archive + * + * @since 1.3 + */ + bool extractArchivedFile( const QString &destFileName ); + /** * Asks the generator to dynamically generate a SourceReference for a given * page number and absolute X and Y position on this page. * * @attention Ownership of the returned SourceReference is transferred to the caller. * @note This method does not call processSourceReference( const SourceReference * ) * * @since 0.10 (KDE 4.4) */ const SourceReference * dynamicSourceReference( int pageNr, double absX, double absY ); /** * Returns the orientation of the document (for printing purposes). This * is used in the KPart to initialize the print dialog and in the * generators to check whether the document needs to be rotated or not. * * @since 0.14 (KDE 4.8) */ QPrinter::Orientation orientation() const; /** * Control annotation editing (creation, modification and removal), * which is enabled by default. * * @since 0.15 (KDE 4.9) */ void setAnnotationEditingEnabled( bool enable ); /** * Returns which wallet data to use to read/write the password for the given fileName * * @since 0.20 (KDE 4.14) */ void walletDataForFile( const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey ) const; + /** + * Since version 0.21, okular does not allow editing annotations and + * form data if they are stored in the docdata directory (like older + * okular versions did by default). + * If this flag is set, then annotations and forms cannot be edited. + * + * @since 1.3 + */ + bool isDocdataMigrationNeeded() const; + + /** + * Delete annotations and form data from the docdata folder. Call it if + * isDocdataMigrationNeeded() was true and you've just saved them to an + * external file. + * + * @since 1.3 + */ + void docdataMigrationDone(); + /** * Returns the model for rendering layers (NULL if the document has no layers) * * @since 0.24 */ QAbstractItemModel * layersModel() const; public Q_SLOTS: /** * This slot is called whenever the user changes the @p rotation of * the document. */ void setRotation( int rotation ); /** * This slot is called whenever the user changes the page @p size * of the document. */ void setPageSize( const PageSize &size ); /** * Cancels the current search */ void cancelSearch(); /** * Undo last edit command * @since 0.17 (KDE 4.11) */ void undo(); /** * Redo last undone edit command * @since 0.17 (KDE 4.11) */ void redo(); /** * Edit the text contents of the specified @p form on page @p page to be @p newContents. * The new text cursor position (@p newCursorPos), previous text cursor position (@p prevCursorPos), * and previous cursor anchor position will be restored by the undo / redo commands. * @since 0.17 (KDE 4.11) */ void editFormText( int pageNumber, Okular::FormFieldText* form, const QString & newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos ); /** * Edit the selected list entries in @p form on page @p page to be @p newChoices. * @since 0.17 (KDE 4.11) */ void editFormList( int pageNumber, Okular::FormFieldChoice* form, const QList & newChoices ); /** * Set the active choice in the combo box @p form on page @p page to @p newText * The new cursor position (@p newCursorPos), previous cursor position * (@p prevCursorPos), and previous anchor position (@p prevAnchorPos) * will be restored by the undo / redo commands. * * @since 0.17 (KDE 4.11) */ void editFormCombo( int pageNumber, Okular::FormFieldChoice *form, const QString & newText, int newCursorPos, int prevCursorPos, int prevAnchorPos ); /** * Set the states of the group of form buttons @p formButtons on page @p page to @p newButtonStates. * The lists @p formButtons and @p newButtonStates should be the same length and true values * in @p newButtonStates indicate that the corresponding entry in @p formButtons should be enabled. */ void editFormButtons( int pageNumber, const QList< Okular::FormFieldButton* > & formButtons, const QList< bool > & newButtonStates ); /** * Reloads the pixmaps for whole document * * @since 0.24 */ void reloadDocument() const; Q_SIGNALS: /** * This signal is emitted whenever an action requests a * document close operation. */ void close(); /** * This signal is emitted whenever an action requests an * application quit operation. */ void quit(); /** * This signal is emitted whenever an action requests a * find operation. */ void linkFind(); /** * This signal is emitted whenever an action requests a * goto operation. */ void linkGoToPage(); /** * This signal is emitted whenever an action requests a * start presentation operation. */ void linkPresentation(); /** * This signal is emitted whenever an action requests an * end presentation operation. */ void linkEndPresentation(); /** * This signal is emitted whenever an action requests an * open url operation for the given document @p url. */ void openUrl( const QUrl &url ); /** * This signal is emitted whenever an error occurred. * * @param text The description of the error. * @param duration The time in milliseconds the message should be shown to the user. */ void error( const QString &text, int duration ); /** * This signal is emitted to signal a warning. * * @param text The description of the warning. * @param duration The time in milliseconds the message should be shown to the user. */ void warning( const QString &text, int duration ); /** * This signal is emitted to signal a notice. * * @param text The description of the notice. * @param duration The time in milliseconds the message should be shown to the user. */ void notice( const QString &text, int duration ); /** * Emitted when a new font is found during the reading of the fonts of * the document. */ void gotFont( const Okular::FontInfo& font ); /** * Reports the progress when reading the fonts in the document. * * \param page is the page that was just finished to scan for fonts */ void fontReadingProgress( int page ); /** * Reports that the reading of the fonts in the document is finished. */ void fontReadingEnded(); /** * Reports that the current search finished */ void searchFinished( int searchID, Okular::Document::SearchStatus endStatus ); /** * This signal is emitted whenever a source reference with the given parameters has been * activated. * * \param handled should be set to 'true' if a slot handles this source reference; the * default action to launch the configured editor will then not be performed * by the document * * @since 0.14 (KDE 4.8) */ void sourceReferenceActivated(const QString& absFileName, int line, int col, bool *handled); /** * This signal is emitted whenever an movie action is triggered and the UI should process it. */ void processMovieAction( const Okular::MovieAction *action ); /** * This signal is emmitted whenever the availability of the undo function changes * @since 0.17 (KDE 4.11) */ void canUndoChanged( bool undoAvailable ); /** * This signal is emmitted whenever the availability of the redo function changes * @since 0.17 (KDE 4.11) */ void canRedoChanged( bool redoAvailable ); + /** + * This signal is emmitted whenever the undo history is clean (i.e. the same status the last time it was saved) + * @since 1.3 + */ + void undoHistoryCleanChanged( bool clean ); + /** * This signal is emitted whenever an rendition action is triggered and the UI should process it. * * @since 0.16 (KDE 4.10) */ void processRenditionAction( const Okular::RenditionAction *action ); /** * This signal is emmitted whenever the contents of the given @p annotation are changed by an undo * or redo action. * * The new contents (@p contents), cursor position (@p cursorPos), and anchor position (@p anchorPos) are * included * @since 0.17 (KDE 4.11) */ void annotationContentsChangedByUndoRedo( Okular::Annotation* annotation, const QString & contents, int cursorPos, int anchorPos ); /** * This signal is emmitted whenever the text contents of the given text @p form on the given @p page * are changed by an undo or redo action. * * The new text contents (@p contents), cursor position (@p cursorPos), and anchor position (@p anchorPos) are * included * @since 0.17 (KDE 4.11) */ void formTextChangedByUndoRedo( int page, Okular::FormFieldText* form, const QString & contents, int cursorPos, int anchorPos ); /** * This signal is emmitted whenever the selected @p choices for the given list @p form on the * given @p page are changed by an undo or redo action. * @since 0.17 (KDE 4.11) */ void formListChangedByUndoRedo( int page, Okular::FormFieldChoice* form, const QList< int > & choices ); /** * This signal is emmitted whenever the active @p text for the given combo @p form on the * given @p page is changed by an undo or redo action. * @since 0.17 (KDE 4.11) */ void formComboChangedByUndoRedo( int page, Okular::FormFieldChoice* form, const QString & text, int cursorPos, int anchorPos ); /** * This signal is emmitted whenever the state of the specified group of form buttons (@p formButtons) on the * given @p page is changed by an undo or redo action. * @since 0.17 (KDE 4.11) */ void formButtonsChangedByUndoRedo( int page, const QList< Okular::FormFieldButton* > & formButtons ); private: /// @cond PRIVATE friend class DocumentPrivate; friend class ::DocumentItem; friend class EditAnnotationContentsCommand; friend class EditFormTextCommand; friend class EditFormListCommand; friend class EditFormComboCommand; friend class EditFormButtonsCommand; /// @endcond DocumentPrivate *const d; Q_DISABLE_COPY( Document ) Q_PRIVATE_SLOT( d, void saveDocumentInfo() const ) Q_PRIVATE_SLOT( d, void slotTimedMemoryCheck() ) Q_PRIVATE_SLOT( d, void sendGeneratorPixmapRequest() ) Q_PRIVATE_SLOT( d, void rotationFinished( int page, Okular::Page *okularPage ) ) Q_PRIVATE_SLOT( d, void slotFontReadingProgress( int page ) ) Q_PRIVATE_SLOT( d, void fontReadingGotFont( const Okular::FontInfo& font ) ) Q_PRIVATE_SLOT( d, void slotGeneratorConfigChanged( const QString& ) ) Q_PRIVATE_SLOT( d, void refreshPixmaps( int ) ) Q_PRIVATE_SLOT( d, void _o_configChanged() ) // search thread simulators Q_PRIVATE_SLOT( d, void doContinueDirectionMatchSearch(void *doContinueDirectionMatchSearchStruct) ) Q_PRIVATE_SLOT( d, void doContinueAllDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID) ) Q_PRIVATE_SLOT( d, void doContinueGooglesDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID, const QStringList & words) ) }; /** * @short A view on the document. * * The Viewport structure is the 'current view' over the document. Contained * data is broadcasted between observers to synchronize their viewports to get * the 'I scroll one view and others scroll too' views. */ class OKULARCORE_EXPORT DocumentViewport { public: /** * Creates a new viewport for the given page @p number. */ DocumentViewport( int number = -1 ); /** * Creates a new viewport from the given xml @p description. */ DocumentViewport( const QString &description ); /** * Returns the viewport as xml description. */ QString toString() const; /** * Returns whether the viewport is valid. */ bool isValid() const; /** * @internal */ bool operator==( const DocumentViewport &other ) const; bool operator<( const DocumentViewport &other ) const; /** * The number of the page nearest the center of the viewport. */ int pageNumber; /** * Describes the relative position of the viewport. */ enum Position { Center = 1, ///< Relative to the center of the page. TopLeft = 2 ///< Relative to the top left corner of the page. }; /** * If 'rePos.enabled == true' then this structure contains the * viewport center or top left depending on the value of pos. */ struct { bool enabled; double normalizedX; double normalizedY; Position pos; } rePos; /** * If 'autoFit.enabled == true' then the page must be autofitted in the viewport. */ struct { bool enabled; bool width; bool height; } autoFit; }; /** * @short A DOM tree that describes the Table of Contents. * * The Synopsis (TOC or Table Of Contents for friends) is represented via * a dom tree where each node has an internal name (displayed in the TOC) * and one or more attributes. * * In the tree the tag name is the 'screen' name of the entry. A tag can have * attributes. Here follows the list of tag attributes with meaning: * - Destination: A string description of the referred viewport * - DestinationName: A 'named reference' to the viewport that must be converted * using metaData( "NamedViewport", viewport_name ) * - ExternalFileName: A document to be opened, whose destination is specified * with Destination or DestinationName * - Open: a boolean saying whether its TOC branch is open or not (default: false) * - URL: a URL to be open as destination; if set, no other Destination* or * ExternalFileName entry is used */ class OKULARCORE_EXPORT DocumentSynopsis : public QDomDocument { public: /** * Creates a new document synopsis object. */ DocumentSynopsis(); /** * Creates a new document synopsis object with the given * @p document as parent node. */ DocumentSynopsis( const QDomDocument &document ); }; /** * @short An embedded file into the document. * * This class represents a sort of interface of an embedded file in a document. * * Generators \b must re-implement its members to give the all the information * about an embedded file, like its name, its description, the date of creation * and modification, and the real data of the file. */ class OKULARCORE_EXPORT EmbeddedFile { public: /** * Creates a new embedded file. */ EmbeddedFile(); /** * Destroys the embedded file. */ virtual ~EmbeddedFile(); /** * Returns the name of the file */ virtual QString name() const = 0; /** * Returns the description of the file, or an empty string if not * available */ virtual QString description() const = 0; /** * Returns the real data representing the file contents */ virtual QByteArray data() const = 0; /** * Returns the size (in bytes) of the file, if available, or -1 otherwise. * * @note this method should be a fast way to know the size of the file * with no need to extract all the data from it */ virtual int size() const = 0; /** * Returns the modification date of the file, or an invalid date * if not available */ virtual QDateTime modificationDate() const = 0; /** * Returns the creation date of the file, or an invalid date * if not available */ virtual QDateTime creationDate() const = 0; }; /** * @short An area of a specified page */ class OKULARCORE_EXPORT VisiblePageRect { public: /** * Creates a new visible page rectangle. * * @param pageNumber The page number where the rectangle is located. * @param rectangle The rectangle in normalized coordinates. */ explicit VisiblePageRect( int pageNumber = -1, const NormalizedRect &rectangle = NormalizedRect() ); /** * The page number where the rectangle is located. */ int pageNumber; /** * The rectangle in normalized coordinates. */ NormalizedRect rect; }; } Q_DECLARE_METATYPE( Okular::DocumentInfo::Key ) Q_DECLARE_OPERATORS_FOR_FLAGS( Okular::Document::PixmapRequestFlags ) #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/document_p.h b/core/document_p.h index b6cf425bc..644a3c77b 100644 --- a/core/document_p.h +++ b/core/document_p.h @@ -1,302 +1,320 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2007 by Albert Astals Cid * * * * 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_DOCUMENT_P_H_ #define _OKULAR_DOCUMENT_P_H_ #include "document.h" #include "synctex/synctex_parser.h" // qt/kde/system includes #include #include #include #include #include #include #include // local includes #include "fontinfo.h" #include "generator.h" class QUndoStack; class QEventLoop; class QFile; class QTimer; class QTemporaryFile; class KPluginMetaData; struct AllocatedPixmap; struct ArchiveData; struct RunningSearch; namespace Okular { class ConfigInterface; class PageController; class SaveInterface; class Scripter; class View; } struct GeneratorInfo { explicit GeneratorInfo( Okular::Generator *g, const KPluginMetaData &data) : generator( g ), metadata( data ), config( nullptr ), save( nullptr ), configChecked( false ), saveChecked( false ) {} Okular::Generator * generator; KPluginMetaData metadata; Okular::ConfigInterface * config; Okular::SaveInterface * save; bool configChecked : 1; bool saveChecked : 1; }; namespace Okular { class FontExtractionThread; struct DoContinueDirectionMatchSearchStruct { QSet< int > *pagesToNotify; RegularAreaRect *match; int currentPage; int searchID; }; +enum LoadDocumentInfoFlag +{ + LoadNone = 0, + LoadPageInfo = 1, // Load annotations and forms + LoadGeneralInfo = 2, // History, rotation, ... + LoadAllInfo = 0xff +}; +Q_DECLARE_FLAGS(LoadDocumentInfoFlags, LoadDocumentInfoFlag) + class DocumentPrivate { public: DocumentPrivate( Document *parent ) : m_parent( parent ), m_tempFile( nullptr ), m_docSize( -1 ), m_allocatedPixmapsTotalMemory( 0 ), m_maxAllocatedTextPages( 0 ), m_warnedOutOfMemory( false ), m_rotation( Rotation0 ), m_exportCached( false ), m_bookmarkManager( nullptr ), m_memCheckTimer( nullptr ), m_saveBookmarksTimer( nullptr ), m_generator( nullptr ), m_walletGenerator( nullptr ), m_generatorsLoaded( false ), m_pageController( nullptr ), m_closingLoop( nullptr ), m_scripter( nullptr ), m_archiveData( nullptr ), m_fontsCached( false ), m_annotationEditingEnabled ( true ), m_annotationBeingModified( false ), + m_docdataMigrationNeeded( false ), m_synctex_scanner( nullptr ) { calculateMaxTextPages(); } // private methods + bool updateMetadataXmlNameAndDocSize(); QString pagesSizeString() const; QString namePaperSize(double inchesWidth, double inchesHeight) const; QString localizedSize(const QSizeF &size) const; qulonglong calculateMemoryToFree(); void cleanupPixmapMemory(); void cleanupPixmapMemory( qulonglong memoryToFree ); AllocatedPixmap * searchLowestPriorityPixmap( bool unloadableOnly = false, bool thenRemoveIt = false, DocumentObserver *observer = nullptr /* any */ ); void calculateMaxTextPages(); qulonglong getTotalMemory(); qulonglong getFreeMemory( qulonglong *freeSwap = nullptr ); - void loadDocumentInfo(); - void loadDocumentInfo( QFile &infoFile ); + bool loadDocumentInfo( LoadDocumentInfoFlags loadWhat ); + bool loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat ); void loadViewsInfo( View *view, const QDomElement &e ); void saveViewsInfo( View *view, QDomElement &e ) const; QUrl giveAbsoluteUrl( const QString & fileName ) const; bool openRelativeFile( const QString & fileName ); Generator * loadGeneratorLibrary( const KPluginMetaData& service ); void loadAllGeneratorLibraries(); void loadServiceList( const QVector& offers ); void unloadGenerator( const GeneratorInfo& info ); void cacheExportFormats(); void setRotationInternal( int r, bool notify ); ConfigInterface* generatorConfig( GeneratorInfo& info ); SaveInterface* generatorSave( GeneratorInfo& info ); Document::OpenResult openDocumentInternal( const KPluginMetaData& offer, bool isstdin, const QString& docFile, const QByteArray& filedata, const QString& password ); + static ArchiveData *unpackDocumentArchive( const QString &archivePath ); bool savePageDocumentInfo( QTemporaryFile *infoFile, int what ) const; DocumentViewport nextDocumentViewport() const; void notifyAnnotationChanges( int page ); + void notifyFormChanges( int page ); bool canAddAnnotationsNatively() const; bool canModifyExternalAnnotations() const; bool canRemoveExternalAnnotations() const; - void warnLimitedAnnotSupport(); OKULARCORE_EXPORT static QString docDataFileName(const QUrl &url, qint64 document_size); // Methods that implement functionality needed by undo commands void performAddPageAnnotation( int page, Annotation *annotation ); void performRemovePageAnnotation( int page, Annotation * annotation ); void performModifyPageAnnotation( int page, Annotation * annotation, bool appearanceChanged ); void performSetAnnotationContents( const QString & newContents, Annotation *annot, int pageNumber ); void recalculateForms(); // private slots void saveDocumentInfo() const; void slotTimedMemoryCheck(); void sendGeneratorPixmapRequest(); void rotationFinished( int page, Okular::Page *okularPage ); void slotFontReadingProgress( int page ); void fontReadingGotFont( const Okular::FontInfo& font ); void slotGeneratorConfigChanged( const QString& ); void refreshPixmaps( int ); void _o_configChanged(); void doContinueDirectionMatchSearch(void *doContinueDirectionMatchSearchStruct); void doContinueAllDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID); void doContinueGooglesDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID, const QStringList & words); void doProcessSearchMatch( RegularAreaRect *match, RunningSearch *search, QSet< int > *pagesToNotify, int currentPage, int searchID, bool moveViewport, const QColor & color ); // generators stuff /** * This method is used by the generators to signal the finish of * the pixmap generation @p request. */ void requestDone( PixmapRequest * request ); void textGenerationDone( Page *page ); /** * Sets the bounding box of the given @p page (in terms of upright orientation, i.e., Rotation0). */ void setPageBoundingBox( int page, const NormalizedRect& boundingBox ); /** * Request a particular metadata of the Document itself (ie, not something * depending on the document type/backend). */ QVariant documentMetaData( const Generator::DocumentMetaDataKey key, const QVariant &option ) const; /** * Return whether the normalized rectangle @p rectOfInterest on page number @p rectPage * is fully visible. */ bool isNormalizedRectangleFullyVisible( const Okular::NormalizedRect & rectOfInterest, int rectPage ); // For sync files void loadSyncFile( const QString & filePath ); // member variables Document *m_parent; QPointer m_widget; // find descriptors, mapped by ID (we handle multiple searches) QMap< int, RunningSearch * > m_searches; bool m_searchCancelled; // needed because for remote documents docFileName is a local file and // we want the remote url when the document refers to relativeNames QUrl m_url; // cached stuff QString m_docFileName; QString m_xmlFileName; QTemporaryFile *m_tempFile; qint64 m_docSize; // viewport stuff QLinkedList< DocumentViewport > m_viewportHistory; QLinkedList< DocumentViewport >::iterator m_viewportIterator; DocumentViewport m_nextDocumentViewport; // see Link::Goto for an explanation QString m_nextDocumentDestination; // observers / requests / allocator stuff QSet< DocumentObserver * > m_observers; QLinkedList< PixmapRequest * > m_pixmapRequestsStack; QLinkedList< PixmapRequest * > m_executingPixmapRequests; QMutex m_pixmapRequestsMutex; QLinkedList< AllocatedPixmap * > m_allocatedPixmaps; qulonglong m_allocatedPixmapsTotalMemory; QList< int > m_allocatedTextPagesFifo; int m_maxAllocatedTextPages; bool m_warnedOutOfMemory; // the rotation applied to the document Rotation m_rotation; // the current size of the pages (if available), and the cache of the // available page sizes PageSize m_pageSize; PageSize::List m_pageSizes; // cache of the export formats bool m_exportCached; ExportFormat::List m_exportFormats; ExportFormat m_exportToText; // our bookmark manager BookmarkManager *m_bookmarkManager; // timers (memory checking / info saver) QTimer *m_memCheckTimer; QTimer *m_saveBookmarksTimer; QHash m_loadedGenerators; Generator * m_generator; QString m_generatorName; Generator * m_walletGenerator; bool m_generatorsLoaded; QVector< Page * > m_pagesVector; QVector< VisiblePageRect * > m_pageRects; // cache of the mimetype we support QStringList m_supportedMimeTypes; PageController *m_pageController; QEventLoop *m_closingLoop; Scripter *m_scripter; ArchiveData *m_archiveData; QString m_archivedFileName; QPointer< FontExtractionThread > m_fontThread; bool m_fontsCached; QSet m_documentInfoAskedKeys; DocumentInfo m_documentInfo; FontInfo::List m_fontsCache; QSet< View * > m_views; bool m_annotationEditingEnabled; - bool m_annotationsNeedSaveAs; bool m_annotationBeingModified; // is an annotation currently being moved or resized? - bool m_showWarningLimitedAnnotSupport; + bool m_metadataLoadingCompleted; QUndoStack *m_undoStack; QDomNode m_prevPropsOfAnnotBeingModified; + // Since 0.21, we no longer support saving annotations and form data in + // the docdata/ directory and we ask the user to migrate them to an + // external file as soon as possible, otherwise the document will be + // shown in read-only mode. This flag is set if the docdata/ XML file + // for the current document contains any annotation or form. + bool m_docdataMigrationNeeded; + synctex_scanner_p m_synctex_scanner; // generator selection static QVector availableGenerators(); static QVector configurableGenerators(); static KPluginMetaData generatorForMimeType(const QMimeType& type, QWidget* widget, const QVector &triedOffers = QVector()); }; class DocumentInfoPrivate { public: QMap values; // key -> value QMap titles; // key -> title For the custom keys }; } #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/documentcommands.cpp b/core/documentcommands.cpp index 3239ab058..3b9348bd4 100644 --- a/core/documentcommands.cpp +++ b/core/documentcommands.cpp @@ -1,632 +1,753 @@ /*************************************************************************** * Copyright (C) 2013 Jon Mease * + * 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 "documentcommands_p.h" #include "annotations.h" #include "debug_p.h" #include "document_p.h" #include "form.h" #include "utils_p.h" #include "page.h" +#include "page_p.h" #include namespace Okular { void moveViewportIfBoundingRectNotFullyVisible( Okular::NormalizedRect boundingRect, DocumentPrivate *docPriv, int pageNumber ) { const Rotation pageRotation = docPriv->m_parent->page( pageNumber )->rotation(); const QTransform rotationMatrix = Okular::buildRotationMatrix( pageRotation ); boundingRect.transform( rotationMatrix ); if ( !docPriv->isNormalizedRectangleFullyVisible( boundingRect, pageNumber ) ) { DocumentViewport searchViewport( pageNumber ); searchViewport.rePos.enabled = true; searchViewport.rePos.normalizedX = ( boundingRect.left + boundingRect.right ) / 2.0; searchViewport.rePos.normalizedY = ( boundingRect.top + boundingRect.bottom ) / 2.0; docPriv->m_parent->setViewport( searchViewport, nullptr, true ); } } Okular::NormalizedRect buildBoundingRectangleForButtons( const QList & formButtons ) { // Initialize coordinates of the bounding rect double left = 1.0; double top = 1.0; double right = 0.0; double bottom = 0.0; foreach( FormFieldButton* formButton, formButtons ) { left = qMin( left, formButton->rect().left ); top = qMin( top, formButton->rect().top ); right = qMax( right, formButton->rect().right ); bottom = qMax( bottom, formButton->rect().bottom ); } Okular::NormalizedRect boundingRect( left, top, right, bottom ); return boundingRect; } AddAnnotationCommand::AddAnnotationCommand( Okular::DocumentPrivate * docPriv, Okular::Annotation* annotation, int pageNumber ) : m_docPriv( docPriv ), m_annotation( annotation ), m_pageNumber( pageNumber ), m_done( false ) { setText( i18nc ("Add an annotation to the page", "add annotation" ) ); } AddAnnotationCommand::~AddAnnotationCommand() { if ( !m_done ) { delete m_annotation; } } void AddAnnotationCommand::undo() { moveViewportIfBoundingRectNotFullyVisible( m_annotation->boundingRectangle(), m_docPriv, m_pageNumber ); m_docPriv->performRemovePageAnnotation( m_pageNumber, m_annotation ); m_done = false; } void AddAnnotationCommand::redo() { moveViewportIfBoundingRectNotFullyVisible( m_annotation->boundingRectangle(), m_docPriv, m_pageNumber ); m_docPriv->performAddPageAnnotation( m_pageNumber, m_annotation ); m_done = true; } +bool AddAnnotationCommand::refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) +{ + if ( m_done ) + { + // We don't always update m_annotation because even if the annotation has been added to the document + // it can have been removed later so the annotation pointer is stored inside a following RemoveAnnotationCommand + // and thus doesn't need updating because it didn't change + // because of the document reload + auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() ); + if (a) m_annotation = a; + } + + return true; +} + RemoveAnnotationCommand::RemoveAnnotationCommand(Okular::DocumentPrivate * doc, Okular::Annotation* annotation, int pageNumber) : m_docPriv( doc ), m_annotation( annotation ), m_pageNumber( pageNumber ), m_done( false ) { setText( i18nc( "Remove an annotation from the page", "remove annotation" ) ); } RemoveAnnotationCommand::~RemoveAnnotationCommand() { if ( m_done ) { delete m_annotation; } } void RemoveAnnotationCommand::undo() { moveViewportIfBoundingRectNotFullyVisible( m_annotation->boundingRectangle(), m_docPriv, m_pageNumber ); m_docPriv->performAddPageAnnotation( m_pageNumber, m_annotation ); m_done = false; } -void RemoveAnnotationCommand::redo(){ +void RemoveAnnotationCommand::redo() +{ moveViewportIfBoundingRectNotFullyVisible( m_annotation->boundingRectangle(), m_docPriv, m_pageNumber ); m_docPriv->performRemovePageAnnotation( m_pageNumber, m_annotation ); m_done = true; } +bool RemoveAnnotationCommand::refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) +{ + if ( !m_done ) + { + // We don't always update m_annotation because it can happen that the annotation remove has been undo + // and that annotation addition has also been undone so the the annotation pointer is stored inside + // a previous AddAnnotationCommand and thus doesn't need updating because it didn't change + // because of the document reload + auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() ); + if (a) m_annotation = a; + } + + return true; +} ModifyAnnotationPropertiesCommand::ModifyAnnotationPropertiesCommand( DocumentPrivate* docPriv, Annotation* annotation, int pageNumber, QDomNode oldProperties, QDomNode newProperties ) : m_docPriv( docPriv ), m_annotation( annotation ), m_pageNumber( pageNumber ), m_prevProperties( oldProperties ), m_newProperties( newProperties ) { setText(i18nc("Modify an annotation's internal properties (Color, line-width, etc.)", "modify annotation properties")); } void ModifyAnnotationPropertiesCommand::undo() { moveViewportIfBoundingRectNotFullyVisible( m_annotation->boundingRectangle(), m_docPriv, m_pageNumber ); m_annotation->setAnnotationProperties( m_prevProperties ); m_docPriv->performModifyPageAnnotation( m_pageNumber, m_annotation, true ); } void ModifyAnnotationPropertiesCommand::redo() { moveViewportIfBoundingRectNotFullyVisible( m_annotation->boundingRectangle(), m_docPriv, m_pageNumber ); m_annotation->setAnnotationProperties( m_newProperties ); m_docPriv->performModifyPageAnnotation( m_pageNumber, m_annotation, true ); } +bool ModifyAnnotationPropertiesCommand::refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) +{ + // Same reason for not unconditionally updating m_annotation, the annotation pointer can be stored in an add/Remove command + auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() ); + if (a) m_annotation = a; + + return true; +} + + TranslateAnnotationCommand::TranslateAnnotationCommand( DocumentPrivate* docPriv, Annotation* annotation, int pageNumber, const Okular::NormalizedPoint & delta, bool completeDrag ) : m_docPriv( docPriv ), m_annotation( annotation ), m_pageNumber( pageNumber ), m_delta( delta ), m_completeDrag( completeDrag ) { setText( i18nc( "Translate an annotation's position on the page", "translate annotation" ) ); } void TranslateAnnotationCommand::undo() { moveViewportIfBoundingRectNotFullyVisible(translateBoundingRectangle( minusDelta() ), m_docPriv, m_pageNumber ); m_annotation->translate( minusDelta() ); m_docPriv->performModifyPageAnnotation( m_pageNumber, m_annotation, true ); } void TranslateAnnotationCommand::redo() { moveViewportIfBoundingRectNotFullyVisible(translateBoundingRectangle( m_delta ), m_docPriv, m_pageNumber ); m_annotation->translate( m_delta ); m_docPriv->performModifyPageAnnotation( m_pageNumber, m_annotation, true ); } int TranslateAnnotationCommand::id() const { return 1; } bool TranslateAnnotationCommand::mergeWith( const QUndoCommand* uc ) { TranslateAnnotationCommand *tuc = (TranslateAnnotationCommand*)uc; if ( tuc->m_annotation != m_annotation ) return false; if ( m_completeDrag ) { return false; } m_delta = Okular::NormalizedPoint( tuc->m_delta.x + m_delta.x, tuc->m_delta.y + m_delta.y ); m_completeDrag = tuc->m_completeDrag; return true; } Okular::NormalizedPoint TranslateAnnotationCommand::minusDelta() { return Okular::NormalizedPoint( -m_delta.x, -m_delta.y ); } Okular::NormalizedRect TranslateAnnotationCommand::translateBoundingRectangle( const Okular::NormalizedPoint & delta ) { Okular::NormalizedRect annotBoundingRect = m_annotation->boundingRectangle(); double left = qMin( annotBoundingRect.left, annotBoundingRect.left + delta.x ); double right = qMax( annotBoundingRect.right, annotBoundingRect.right + delta.x ); double top = qMin( annotBoundingRect.top, annotBoundingRect.top + delta.y ); double bottom = qMax( annotBoundingRect.bottom, annotBoundingRect.bottom + delta.y ); Okular::NormalizedRect boundingRect( left, top, right, bottom ); return boundingRect; } +bool TranslateAnnotationCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +{ + // Same reason for not unconditionally updating m_annotation, the annotation pointer can be stored in an add/Remove command + auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() ); + if (a) m_annotation = a; + + return true; +} + + AdjustAnnotationCommand::AdjustAnnotationCommand(Okular::DocumentPrivate * docPriv, Okular::Annotation * annotation, int pageNumber, const Okular::NormalizedPoint & delta1, const Okular::NormalizedPoint & delta2, bool completeDrag ) : m_docPriv( docPriv ), m_annotation( annotation ), m_pageNumber( pageNumber ), m_delta1( delta1 ), m_delta2( delta2 ), m_completeDrag( completeDrag ) { setText( i18nc( "Change an annotation's size", "adjust annotation" ) ); } void AdjustAnnotationCommand::undo() { const NormalizedPoint minusDelta1 = Okular::NormalizedPoint( -m_delta1.x, -m_delta1.y ); const NormalizedPoint minusDelta2 = Okular::NormalizedPoint( -m_delta2.x, -m_delta2.y ); moveViewportIfBoundingRectNotFullyVisible( adjustBoundingRectangle( minusDelta1, minusDelta2 ), m_docPriv, m_pageNumber ); m_annotation->adjust( minusDelta1, minusDelta2 ); m_docPriv->performModifyPageAnnotation( m_pageNumber, m_annotation, true ); } void AdjustAnnotationCommand::redo() { moveViewportIfBoundingRectNotFullyVisible( adjustBoundingRectangle( m_delta1, m_delta2 ), m_docPriv, m_pageNumber ); m_annotation->adjust( m_delta1, m_delta2 ); m_docPriv->performModifyPageAnnotation( m_pageNumber, m_annotation, true ); } int AdjustAnnotationCommand::id() const { return 5; } bool AdjustAnnotationCommand::mergeWith( const QUndoCommand * uc ) { AdjustAnnotationCommand *tuc = (AdjustAnnotationCommand *)uc; if ( tuc->m_annotation != m_annotation ) return false; if ( m_completeDrag ) { return false; } m_delta1 = Okular::NormalizedPoint( tuc->m_delta1.x + m_delta1.x, tuc->m_delta1.y + m_delta1.y ); m_delta2 = Okular::NormalizedPoint( tuc->m_delta2.x + m_delta2.x, tuc->m_delta2.y + m_delta2.y ); m_completeDrag = tuc->m_completeDrag; return true; } Okular::NormalizedRect AdjustAnnotationCommand::adjustBoundingRectangle( const Okular::NormalizedPoint & delta1, const Okular::NormalizedPoint & delta2 ) { const Okular::NormalizedRect annotBoundingRect = m_annotation->boundingRectangle(); const double left = qMin( annotBoundingRect.left, annotBoundingRect.left + delta1.x ); const double right = qMax( annotBoundingRect.right, annotBoundingRect.right + delta2.x ); const double top = qMin( annotBoundingRect.top, annotBoundingRect.top + delta1.y ); const double bottom = qMax( annotBoundingRect.bottom, annotBoundingRect.bottom + delta2.y ); return Okular::NormalizedRect( left, top, right, bottom ); } +bool AdjustAnnotationCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +{ + // Same reason for not unconditionally updating m_annotation, the annotation pointer can be stored in an add/Remove command + auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() ); + if (a) m_annotation = a; + + return true; +} + + EditTextCommand::EditTextCommand( const QString & newContents, int newCursorPos, const QString & prevContents, int prevCursorPos, int prevAnchorPos ) : m_newContents( newContents ), m_newCursorPos( newCursorPos ), m_prevContents( prevContents ), m_prevCursorPos( prevCursorPos ), m_prevAnchorPos( prevAnchorPos ) { setText( i18nc( "Generic text edit command", "edit text" ) ); //// Determine edit type // If There was a selection then edit was not a simple single character backspace, delete, or insert if (m_prevCursorPos != m_prevAnchorPos) { qCDebug(OkularCoreDebug) << "OtherEdit, selection"; m_editType = OtherEdit; } else if ( newContentsRightOfCursor() == oldContentsRightOfCursor() && newContentsLeftOfCursor() == oldContentsLeftOfCursor().left(oldContentsLeftOfCursor().length() - 1) && oldContentsLeftOfCursor().right(1) != QLatin1String("\n") ) { qCDebug(OkularCoreDebug) << "CharBackspace"; m_editType = CharBackspace; } else if ( newContentsLeftOfCursor() == oldContentsLeftOfCursor() && newContentsRightOfCursor() == oldContentsRightOfCursor().right(oldContentsRightOfCursor().length() - 1) && oldContentsRightOfCursor().left(1) != QLatin1String("\n") ) { qCDebug(OkularCoreDebug) << "CharDelete"; m_editType = CharDelete; } else if ( newContentsRightOfCursor() == oldContentsRightOfCursor() && newContentsLeftOfCursor().left( newContentsLeftOfCursor().length() - 1) == oldContentsLeftOfCursor() && newContentsLeftOfCursor().right(1) != QLatin1String("\n") ) { qCDebug(OkularCoreDebug) << "CharInsert"; m_editType = CharInsert; } else { qCDebug(OkularCoreDebug) << "OtherEdit"; m_editType = OtherEdit; } } bool EditTextCommand::mergeWith(const QUndoCommand* uc) { EditTextCommand *euc = (EditTextCommand*)uc; // Only attempt merge of euc into this if our new state matches euc's old state and // the editTypes match and are not type OtherEdit if ( m_newContents == euc->m_prevContents && m_newCursorPos == euc->m_prevCursorPos && m_editType == euc->m_editType && m_editType != OtherEdit ) { m_newContents = euc->m_newContents; m_newCursorPos = euc->m_newCursorPos; return true; } return false; } QString EditTextCommand::oldContentsLeftOfCursor() { return m_prevContents.left(m_prevCursorPos); } QString EditTextCommand::oldContentsRightOfCursor() { return m_prevContents.right(m_prevContents.length() - m_prevCursorPos); } QString EditTextCommand::newContentsLeftOfCursor() { return m_newContents.left(m_newCursorPos); } QString EditTextCommand::newContentsRightOfCursor() { return m_newContents.right(m_newContents.length() - m_newCursorPos); } + EditAnnotationContentsCommand::EditAnnotationContentsCommand( DocumentPrivate* docPriv, Annotation* annotation, int pageNumber, const QString & newContents, int newCursorPos, const QString & prevContents, int prevCursorPos, int prevAnchorPos ) : EditTextCommand( newContents, newCursorPos, prevContents, prevCursorPos, prevAnchorPos ), m_docPriv( docPriv ), m_annotation( annotation ), m_pageNumber( pageNumber ) { setText( i18nc( "Edit an annotation's text contents", "edit annotation contents" ) ); } void EditAnnotationContentsCommand::undo() { moveViewportIfBoundingRectNotFullyVisible( m_annotation->boundingRectangle(), m_docPriv, m_pageNumber ); m_docPriv->performSetAnnotationContents( m_prevContents, m_annotation, m_pageNumber ); emit m_docPriv->m_parent->annotationContentsChangedByUndoRedo( m_annotation, m_prevContents, m_prevCursorPos, m_prevAnchorPos ); } void EditAnnotationContentsCommand::redo() { moveViewportIfBoundingRectNotFullyVisible( m_annotation->boundingRectangle(), m_docPriv, m_pageNumber ); m_docPriv->performSetAnnotationContents( m_newContents, m_annotation, m_pageNumber ); emit m_docPriv->m_parent->annotationContentsChangedByUndoRedo( m_annotation, m_newContents, m_newCursorPos, m_newCursorPos ); } int EditAnnotationContentsCommand::id() const { return 2; } bool EditAnnotationContentsCommand::mergeWith(const QUndoCommand* uc) { EditAnnotationContentsCommand *euc = (EditAnnotationContentsCommand*)uc; // Only attempt merge of euc into this if they modify the same annotation if ( m_annotation == euc->m_annotation ) { return EditTextCommand::mergeWith( uc ); } else { return false; } } +bool EditAnnotationContentsCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +{ + auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() ); + if (a) m_annotation = a; + + return true; +} + + EditFormTextCommand::EditFormTextCommand( Okular::DocumentPrivate* docPriv, Okular::FormFieldText* form, int pageNumber, const QString & newContents, int newCursorPos, const QString & prevContents, int prevCursorPos, int prevAnchorPos ) : EditTextCommand( newContents, newCursorPos, prevContents, prevCursorPos, prevAnchorPos ), m_docPriv ( docPriv ), m_form( form ), m_pageNumber( pageNumber ) { setText( i18nc( "Edit an form's text contents", "edit form contents" ) ); } void EditFormTextCommand::undo() { moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber ); m_form->setText( m_prevContents ); emit m_docPriv->m_parent->formTextChangedByUndoRedo( m_pageNumber, m_form, m_prevContents, m_prevCursorPos, m_prevAnchorPos ); + m_docPriv->notifyFormChanges( m_pageNumber ); } void EditFormTextCommand::redo() { moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber ); m_form->setText( m_newContents ); emit m_docPriv->m_parent->formTextChangedByUndoRedo( m_pageNumber, m_form, m_newContents, m_newCursorPos, m_newCursorPos ); + m_docPriv->notifyFormChanges( m_pageNumber ); } int EditFormTextCommand::id() const { return 3; } bool EditFormTextCommand::mergeWith(const QUndoCommand* uc) { EditFormTextCommand *euc = (EditFormTextCommand*)uc; // Only attempt merge of euc into this if they modify the same form if ( m_form == euc->m_form ) { return EditTextCommand::mergeWith( uc ); } else { return false; } } +bool EditFormTextCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +{ + m_form = dynamic_cast(Okular::PagePrivate::findEquivalentForm( newPagesVector[m_pageNumber], m_form )); + + return m_form; +} + + EditFormListCommand::EditFormListCommand( Okular::DocumentPrivate* docPriv, FormFieldChoice* form, int pageNumber, const QList< int > & newChoices, const QList< int > & prevChoices ) : m_docPriv( docPriv ), m_form( form ), m_pageNumber( pageNumber ), m_newChoices( newChoices ), m_prevChoices( prevChoices ) { setText( i18nc( "Edit a list form's choices", "edit list form choices" ) ); } void EditFormListCommand::undo() { moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber ); m_form->setCurrentChoices( m_prevChoices ); emit m_docPriv->m_parent->formListChangedByUndoRedo( m_pageNumber, m_form, m_prevChoices ); + m_docPriv->notifyFormChanges( m_pageNumber ); } void EditFormListCommand::redo() { moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber ); m_form->setCurrentChoices( m_newChoices ); emit m_docPriv->m_parent->formListChangedByUndoRedo( m_pageNumber, m_form, m_newChoices ); + m_docPriv->notifyFormChanges( m_pageNumber ); } +bool EditFormListCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +{ + m_form = dynamic_cast(Okular::PagePrivate::findEquivalentForm( newPagesVector[m_pageNumber], m_form )); + + return m_form; +} + + EditFormComboCommand::EditFormComboCommand( Okular::DocumentPrivate* docPriv, FormFieldChoice* form, int pageNumber, const QString & newContents, int newCursorPos, const QString & prevContents, int prevCursorPos, int prevAnchorPos ) : EditTextCommand( newContents, newCursorPos, prevContents, prevCursorPos, prevAnchorPos ), m_docPriv( docPriv ), m_form( form ), m_pageNumber( pageNumber ), m_newIndex( -1 ), m_prevIndex( -1 ) { setText( i18nc( "Edit a combo form's selection", "edit combo form selection" ) ); // Determine new and previous choice indices (if any) for ( int i = 0; i < m_form->choices().size(); i++ ) { if ( m_form->choices()[i] == m_prevContents ) { m_prevIndex = i; } if ( m_form->choices()[i] == m_newContents ) { m_newIndex = i; } } } void EditFormComboCommand::undo() { if ( m_prevIndex != -1 ) { m_form->setCurrentChoices( QList() << m_prevIndex ); } else { m_form->setEditChoice( m_prevContents ); } moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber ); emit m_docPriv->m_parent->formComboChangedByUndoRedo( m_pageNumber, m_form, m_prevContents, m_prevCursorPos, m_prevAnchorPos ); + m_docPriv->notifyFormChanges( m_pageNumber ); } void EditFormComboCommand::redo() { if ( m_newIndex != -1 ) { m_form->setCurrentChoices( QList() << m_newIndex ); } else { m_form->setEditChoice( m_newContents ); } moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber ); emit m_docPriv->m_parent->formComboChangedByUndoRedo( m_pageNumber, m_form, m_newContents, m_newCursorPos, m_newCursorPos ); + m_docPriv->notifyFormChanges( m_pageNumber ); } int EditFormComboCommand::id() const { return 4; } bool EditFormComboCommand::mergeWith( const QUndoCommand *uc ) { EditFormComboCommand *euc = (EditFormComboCommand*)uc; // Only attempt merge of euc into this if they modify the same form if ( m_form == euc->m_form ) { bool shouldMerge = EditTextCommand::mergeWith( uc ); if( shouldMerge ) { m_newIndex = euc->m_newIndex; } return shouldMerge; } else { return false; } } +bool EditFormComboCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +{ + m_form = dynamic_cast(Okular::PagePrivate::findEquivalentForm( newPagesVector[m_pageNumber], m_form )); + + return m_form; +} + + EditFormButtonsCommand::EditFormButtonsCommand( Okular::DocumentPrivate* docPriv, int pageNumber, const QList< FormFieldButton* > & formButtons, const QList< bool > & newButtonStates ) : m_docPriv( docPriv ), m_pageNumber( pageNumber ), m_formButtons( formButtons ), m_newButtonStates( newButtonStates ), m_prevButtonStates( QList< bool >() ) { setText( i18nc( "Edit the state of a group of form buttons", "edit form button states" ) ); foreach( FormFieldButton* formButton, m_formButtons ) { m_prevButtonStates.append( formButton->state() ); } } void EditFormButtonsCommand::undo() { clearFormButtonStates(); for( int i = 0; i < m_formButtons.size(); i++ ) { bool checked = m_prevButtonStates.at( i ); if ( checked ) m_formButtons.at( i )->setState( checked ); } Okular::NormalizedRect boundingRect = buildBoundingRectangleForButtons( m_formButtons ); moveViewportIfBoundingRectNotFullyVisible( boundingRect, m_docPriv, m_pageNumber ); emit m_docPriv->m_parent->formButtonsChangedByUndoRedo( m_pageNumber, m_formButtons ); + m_docPriv->notifyFormChanges( m_pageNumber ); } void EditFormButtonsCommand::redo() { clearFormButtonStates(); for( int i = 0; i < m_formButtons.size(); i++ ) { bool checked = m_newButtonStates.at( i ); if ( checked ) m_formButtons.at( i )->setState( checked ); } Okular::NormalizedRect boundingRect = buildBoundingRectangleForButtons( m_formButtons ); moveViewportIfBoundingRectNotFullyVisible( boundingRect, m_docPriv, m_pageNumber ); emit m_docPriv->m_parent->formButtonsChangedByUndoRedo( m_pageNumber, m_formButtons ); + m_docPriv->notifyFormChanges( m_pageNumber ); +} + +bool EditFormButtonsCommand::refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) +{ + const QList< FormFieldButton* > oldFormButtons = m_formButtons; + m_formButtons.clear(); + foreach( FormFieldButton* oldFormButton, oldFormButtons ) + { + FormFieldButton *button = dynamic_cast(Okular::PagePrivate::findEquivalentForm( newPagesVector[m_pageNumber], oldFormButton )); + if ( !button ) + return false; + m_formButtons << button; + } + + return true; } void EditFormButtonsCommand::clearFormButtonStates() { foreach( FormFieldButton* formButton, m_formButtons ) { formButton->setState( false ); } } } diff --git a/core/documentcommands_p.h b/core/documentcommands_p.h index 757397337..2f4cc2d7a 100644 --- a/core/documentcommands_p.h +++ b/core/documentcommands_p.h @@ -1,285 +1,316 @@ /*************************************************************************** * Copyright (C) 2013 Jon Mease * + * 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_DOCUMENT_COMMANDS_P_H_ #define _OKULAR_DOCUMENT_COMMANDS_P_H_ #include #include #include "area.h" namespace Okular { class Document; class Annotation; class DocumentPrivate; class FormFieldText; class FormFieldButton; class FormFieldChoice; +class Page; -class AddAnnotationCommand : public QUndoCommand +class OkularUndoCommand : public QUndoCommand +{ + public: + virtual bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) = 0; +}; + +class AddAnnotationCommand : public OkularUndoCommand { public: AddAnnotationCommand(Okular::DocumentPrivate * docPriv, Okular::Annotation* annotation, int pageNumber); virtual ~AddAnnotationCommand(); void undo() override; void redo() override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate * m_docPriv; Okular::Annotation* m_annotation; int m_pageNumber; bool m_done; }; -class RemoveAnnotationCommand : public QUndoCommand +class RemoveAnnotationCommand : public OkularUndoCommand { public: RemoveAnnotationCommand(Okular::DocumentPrivate * doc, Okular::Annotation* annotation, int pageNumber); virtual ~RemoveAnnotationCommand(); void undo() override; void redo() override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate * m_docPriv; Okular::Annotation* m_annotation; int m_pageNumber; bool m_done; }; -class ModifyAnnotationPropertiesCommand : public QUndoCommand +class ModifyAnnotationPropertiesCommand : public OkularUndoCommand { public: ModifyAnnotationPropertiesCommand( Okular::DocumentPrivate* docPriv, Okular::Annotation* annotation, int pageNumber, QDomNode oldProperties, QDomNode newProperties ); void undo() override; void redo() override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate * m_docPriv; Okular::Annotation* m_annotation; int m_pageNumber; QDomNode m_prevProperties; QDomNode m_newProperties; }; -class TranslateAnnotationCommand : public QUndoCommand +class TranslateAnnotationCommand : public OkularUndoCommand { public: TranslateAnnotationCommand(Okular::DocumentPrivate* docPriv, Okular::Annotation* annotation, int pageNumber, const Okular::NormalizedPoint & delta, bool completeDrag ); void undo() override; void redo() override; int id() const override; bool mergeWith(const QUndoCommand *uc) override; Okular::NormalizedPoint minusDelta(); Okular::NormalizedRect translateBoundingRectangle( const Okular::NormalizedPoint & delta ); + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate * m_docPriv; Okular::Annotation* m_annotation; int m_pageNumber; Okular::NormalizedPoint m_delta; bool m_completeDrag; }; -class AdjustAnnotationCommand : public QUndoCommand +class AdjustAnnotationCommand : public OkularUndoCommand { public: AdjustAnnotationCommand(Okular::DocumentPrivate * docPriv, Okular::Annotation * annotation, int pageNumber, const Okular::NormalizedPoint & delta1, const Okular::NormalizedPoint & delta2, bool completeDrag ); void undo() override; void redo() override; int id() const override; bool mergeWith(const QUndoCommand * uc) override; Okular::NormalizedRect adjustBoundingRectangle( const Okular::NormalizedPoint & delta1, const Okular::NormalizedPoint & delta2 ); + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate * m_docPriv; Okular::Annotation* m_annotation; int m_pageNumber; Okular::NormalizedPoint m_delta1; Okular::NormalizedPoint m_delta2; bool m_completeDrag; }; -class EditTextCommand : public QUndoCommand +class EditTextCommand : public OkularUndoCommand { public: EditTextCommand( const QString & newContents, int newCursorPos, const QString & prevContents, int prevCursorPos, int prevAnchorPos ); void undo() override = 0; void redo() override = 0; int id() const override = 0; bool mergeWith(const QUndoCommand *uc) override; private: enum EditType { CharBackspace, ///< Edit made up of one or more single character backspace operations CharDelete, ///< Edit made up of one or more single character delete operations CharInsert, ///< Edit made up of one or more single character insertion operations OtherEdit ///< All other edit operations (these will not be merged together) }; QString oldContentsLeftOfCursor(); QString newContentsLeftOfCursor(); QString oldContentsRightOfCursor(); QString newContentsRightOfCursor(); protected: QString m_newContents; int m_newCursorPos; QString m_prevContents; int m_prevCursorPos; int m_prevAnchorPos; EditType m_editType; }; class EditAnnotationContentsCommand : public EditTextCommand { public: EditAnnotationContentsCommand(Okular::DocumentPrivate* docPriv, Okular::Annotation* annotation, int pageNumber, const QString & newContents, int newCursorPos, const QString & prevContents, int prevCursorPos, int prevAnchorPos ); void undo() override; void redo() override; int id() const override; bool mergeWith(const QUndoCommand *uc) override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate * m_docPriv; Okular::Annotation* m_annotation; int m_pageNumber; }; class EditFormTextCommand : public EditTextCommand { public: EditFormTextCommand( Okular::DocumentPrivate* docPriv, Okular::FormFieldText* form, int pageNumber, const QString & newContents, int newCursorPos, const QString & prevContents, int prevCursorPos, int prevAnchorPos ); void undo() override; void redo() override; int id() const override; bool mergeWith( const QUndoCommand *uc ) override; + + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate* m_docPriv; Okular::FormFieldText* m_form; int m_pageNumber; }; -class EditFormListCommand : public QUndoCommand +class EditFormListCommand : public OkularUndoCommand { public: EditFormListCommand( Okular::DocumentPrivate* docPriv, FormFieldChoice* form, int pageNumber, const QList< int > & newChoices, const QList< int > & prevChoices ); void undo() override; void redo() override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate* m_docPriv; FormFieldChoice* m_form; int m_pageNumber; QList< int > m_newChoices; QList< int > m_prevChoices; }; class EditFormComboCommand : public EditTextCommand { public: EditFormComboCommand( Okular::DocumentPrivate* docPriv, FormFieldChoice* form, int pageNumber, const QString & newText, int newCursorPos, const QString & prevText, int prevCursorPos, int prevAnchorPos ); void undo() override; void redo() override; int id() const override; bool mergeWith( const QUndoCommand *uc ) override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate* m_docPriv; FormFieldChoice* m_form; int m_pageNumber; int m_newIndex; int m_prevIndex; }; -class EditFormButtonsCommand : public QUndoCommand +class EditFormButtonsCommand : public OkularUndoCommand { public: EditFormButtonsCommand( Okular::DocumentPrivate* docPriv, int pageNumber, const QList< FormFieldButton* > & formButtons, const QList< bool > & newButtonStates ); void undo() override; void redo() override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: void clearFormButtonStates(); private: Okular::DocumentPrivate* m_docPriv; int m_pageNumber; QList< FormFieldButton* > m_formButtons; QList< bool > m_newButtonStates; QList< bool > m_prevButtonStates; }; } #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/generator.cpp b/core/generator.cpp index 8e888557d..ba6013bec 100644 --- a/core/generator.cpp +++ b/core/generator.cpp @@ -1,724 +1,729 @@ /*************************************************************************** * Copyright (C) 2005 by Piotr Szymanski * * Copyright (C) 2008 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 "generator.h" #include "generator_p.h" #include "observer.h" #include #include #include #include #include #include #include #include #include "document.h" #include "document_p.h" #include "page.h" #include "page_p.h" #include "textpage.h" #include "utils.h" using namespace Okular; GeneratorPrivate::GeneratorPrivate() : m_document( nullptr ), mPixmapGenerationThread( nullptr ), mTextPageGenerationThread( nullptr ), m_mutex( nullptr ), m_threadsMutex( nullptr ), mPixmapReady( true ), mTextPageReady( true ), m_closing( false ), m_closingLoop( nullptr ), m_dpi(72.0, 72.0) { qRegisterMetaType(); } GeneratorPrivate::~GeneratorPrivate() { if ( mPixmapGenerationThread ) mPixmapGenerationThread->wait(); delete mPixmapGenerationThread; if ( mTextPageGenerationThread ) mTextPageGenerationThread->wait(); delete mTextPageGenerationThread; delete m_mutex; delete m_threadsMutex; } PixmapGenerationThread* GeneratorPrivate::pixmapGenerationThread() { if ( mPixmapGenerationThread ) return mPixmapGenerationThread; Q_Q( Generator ); mPixmapGenerationThread = new PixmapGenerationThread( q ); QObject::connect( mPixmapGenerationThread, SIGNAL(finished()), q, SLOT(pixmapGenerationFinished()), Qt::QueuedConnection ); return mPixmapGenerationThread; } TextPageGenerationThread* GeneratorPrivate::textPageGenerationThread() { if ( mTextPageGenerationThread ) return mTextPageGenerationThread; Q_Q( Generator ); mTextPageGenerationThread = new TextPageGenerationThread( q ); QObject::connect( mTextPageGenerationThread, SIGNAL(finished()), q, SLOT(textpageGenerationFinished()), Qt::QueuedConnection ); return mTextPageGenerationThread; } void GeneratorPrivate::pixmapGenerationFinished() { Q_Q( Generator ); PixmapRequest *request = mPixmapGenerationThread->request(); mPixmapGenerationThread->endGeneration(); QMutexLocker locker( threadsLock() ); mPixmapReady = true; if ( m_closing ) { delete request; if ( mTextPageReady ) { locker.unlock(); m_closingLoop->quit(); } return; } const QImage& img = mPixmapGenerationThread->image(); request->page()->setPixmap( request->observer(), new QPixmap( QPixmap::fromImage( img ) ), request->normalizedRect() ); const int pageNumber = request->page()->number(); if ( mPixmapGenerationThread->calcBoundingBox() ) q->updatePageBoundingBox( pageNumber, mPixmapGenerationThread->boundingBox() ); q->signalPixmapRequestDone( request ); } void GeneratorPrivate::textpageGenerationFinished() { Q_Q( Generator ); Page *page = mTextPageGenerationThread->page(); mTextPageGenerationThread->endGeneration(); QMutexLocker locker( threadsLock() ); mTextPageReady = true; if ( m_closing ) { delete mTextPageGenerationThread->textPage(); if ( mPixmapReady ) { locker.unlock(); m_closingLoop->quit(); } return; } if ( mTextPageGenerationThread->textPage() ) { TextPage *tp = mTextPageGenerationThread->textPage(); page->setTextPage( tp ); q->signalTextGenerationDone( page, tp ); } } QMutex* GeneratorPrivate::threadsLock() { if ( !m_threadsMutex ) m_threadsMutex = new QMutex(); return m_threadsMutex; } QVariant GeneratorPrivate::metaData( const QString &, const QVariant & ) const { return QVariant(); } QImage GeneratorPrivate::image( PixmapRequest * ) { return QImage(); } Generator::Generator(QObject* parent, const QVariantList&) : QObject(parent) , d_ptr( new GeneratorPrivate() ) { d_ptr->q_ptr = this; } Generator::Generator(GeneratorPrivate &dd, QObject *parent, const QVariantList &args) : QObject(parent), d_ptr(&dd) { d_ptr->q_ptr = this; Q_UNUSED(args) } Generator::~Generator() { delete d_ptr; } bool Generator::loadDocument( const QString & fileName, QVector< Page * > & pagesVector ) { Q_UNUSED(fileName); Q_UNUSED(pagesVector); return false; } bool Generator::loadDocumentFromData( const QByteArray &, QVector< Page * > & ) { return false; } Document::OpenResult Generator::loadDocumentWithPassword( const QString & fileName, QVector< Page * > & pagesVector, const QString & ) { return loadDocument( fileName, pagesVector ) ? Document::OpenSuccess : Document::OpenError; } Document::OpenResult Generator::loadDocumentFromDataWithPassword( const QByteArray & fileData, QVector< Page * > & pagesVector, const QString & ) { return loadDocumentFromData( fileData, pagesVector ) ? Document::OpenSuccess : Document::OpenError; } +Generator::SwapBackingFileResult Generator::swapBackingFile( QString const &/*newFileName */, QVector & /*newPagesVector*/ ) +{ + return SwapBackingFileError; +} + bool Generator::closeDocument() { Q_D( Generator ); d->m_closing = true; d->threadsLock()->lock(); if ( !( d->mPixmapReady && d->mTextPageReady ) ) { QEventLoop loop; d->m_closingLoop = &loop; d->threadsLock()->unlock(); loop.exec(); d->m_closingLoop = nullptr; } else { d->threadsLock()->unlock(); } bool ret = doCloseDocument(); d->m_closing = false; return ret; } bool Generator::canGeneratePixmap() const { Q_D( const Generator ); return d->mPixmapReady; } void Generator::generatePixmap( PixmapRequest *request ) { Q_D( Generator ); d->mPixmapReady = false; const bool calcBoundingBox = !request->isTile() && !request->page()->isBoundingBoxKnown(); if ( request->asynchronous() && hasFeature( Threaded ) ) { d->pixmapGenerationThread()->startGeneration( request, calcBoundingBox ); /** * We create the text page for every page that is visible to the * user, so he can use the text extraction tools without a delay. */ if ( hasFeature( TextExtraction ) && !request->page()->hasTextPage() && canGenerateTextPage() && !d->m_closing ) { d->mTextPageReady = false; // Queue the text generation request so that pixmap generation gets a chance to start before the text generation QMetaObject::invokeMethod(d->textPageGenerationThread(), "startGeneration", Qt::QueuedConnection, Q_ARG(Okular::Page*, request->page())); } return; } const QImage& img = image( request ); request->page()->setPixmap( request->observer(), new QPixmap( QPixmap::fromImage( img ) ), request->normalizedRect() ); const int pageNumber = request->page()->number(); d->mPixmapReady = true; signalPixmapRequestDone( request ); if ( calcBoundingBox ) updatePageBoundingBox( pageNumber, Utils::imageBoundingBox( &img ) ); } bool Generator::canGenerateTextPage() const { Q_D( const Generator ); return d->mTextPageReady; } void Generator::generateTextPage( Page *page ) { TextPage *tp = textPage( page ); page->setTextPage( tp ); signalTextGenerationDone( page, tp ); } QImage Generator::image( PixmapRequest *request ) { Q_D( Generator ); return d->image( request ); } TextPage* Generator::textPage( Page* ) { return nullptr; } DocumentInfo Generator::generateDocumentInfo(const QSet &keys) const { Q_UNUSED(keys); return DocumentInfo(); } const DocumentSynopsis * Generator::generateDocumentSynopsis() { return nullptr; } FontInfo::List Generator::fontsForPage( int ) { return FontInfo::List(); } const QList * Generator::embeddedFiles() const { return nullptr; } Generator::PageSizeMetric Generator::pagesSizeMetric() const { return None; } bool Generator::isAllowed( Permission ) const { return true; } void Generator::rotationChanged( Rotation, Rotation ) { } PageSize::List Generator::pageSizes() const { return PageSize::List(); } void Generator::pageSizeChanged( const PageSize &, const PageSize & ) { } bool Generator::print( QPrinter& ) { return false; } Generator::PrintError Generator::printError() const { return UnknownPrintError; } void Generator::opaqueAction( const BackendOpaqueAction * /*action*/ ) { } QVariant Generator::metaData( const QString &key, const QVariant &option ) const { Q_D( const Generator ); return d->metaData( key, option ); } ExportFormat::List Generator::exportFormats() const { return ExportFormat::List(); } bool Generator::exportTo( const QString&, const ExportFormat& ) { return false; } void Generator::walletDataForFile( const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey ) const { *walletKey = fileName.section( QLatin1Char('/'), -1, -1); *walletName = KWallet::Wallet::NetworkWallet(); *walletFolder = QStringLiteral("KPdf"); } bool Generator::hasFeature( GeneratorFeature feature ) const { Q_D( const Generator ); return d->m_features.contains( feature ); } void Generator::signalPixmapRequestDone( PixmapRequest * request ) { Q_D( Generator ); if ( d->m_document ) d->m_document->requestDone( request ); else { delete request; } } void Generator::signalTextGenerationDone( Page *page, TextPage *textPage ) { Q_D( Generator ); if ( d->m_document ) d->m_document->textGenerationDone( page ); else delete textPage; } void Generator::signalPartialPixmapRequest( PixmapRequest *request, const QImage &image ) { request->page()->setPixmap( request->observer(), new QPixmap( QPixmap::fromImage( image ) ), request->normalizedRect() ); const int pageNumber = request->page()->number(); request->observer()->notifyPageChanged( pageNumber, Okular::DocumentObserver::Pixmap ); } const Document * Generator::document() const { Q_D( const Generator ); if ( d->m_document ) { return d->m_document->m_parent; } return nullptr; } void Generator::setFeature( GeneratorFeature feature, bool on ) { Q_D( Generator ); if ( on ) d->m_features.insert( feature ); else d->m_features.remove( feature ); } QVariant Generator::documentMetaData( const QString &key, const QVariant &option ) const { Q_D( const Generator ); if ( !d->m_document ) return QVariant(); if (key == QLatin1String("PaperColor")) return documentMetaData(PaperColorMetaData, option); if (key == QLatin1String("GraphicsAntialias")) return documentMetaData(GraphicsAntialiasMetaData, option); if (key == QLatin1String("TextAntialias")) return documentMetaData(TextAntialiasMetaData, option); if (key == QLatin1String("TextHinting")) return documentMetaData(TextHintingMetaData, option); return QVariant(); } QVariant Generator::documentMetaData( const DocumentMetaDataKey key, const QVariant &option ) const { Q_D( const Generator ); if ( !d->m_document ) return QVariant(); return d->m_document->documentMetaData( key, option ); } QMutex* Generator::userMutex() const { Q_D( const Generator ); if ( !d->m_mutex ) { d->m_mutex = new QMutex(); } return d->m_mutex; } void Generator::updatePageBoundingBox( int page, const NormalizedRect & boundingBox ) { Q_D( Generator ); if ( d->m_document ) // still connected to document? d->m_document->setPageBoundingBox( page, boundingBox ); } void Generator::requestFontData(const Okular::FontInfo & /*font*/, QByteArray * /*data*/) { } void Generator::setDPI(const QSizeF & dpi) { Q_D( Generator ); d->m_dpi = dpi; } QSizeF Generator::dpi() const { Q_D( const Generator ); return d->m_dpi; } QAbstractItemModel * Generator::layersModel() const { return nullptr; } PixmapRequest::PixmapRequest( DocumentObserver *observer, int pageNumber, int width, int height, int priority, PixmapRequestFeatures features ) : d( new PixmapRequestPrivate ) { d->mObserver = observer; d->mPageNumber = pageNumber; d->mWidth = ceil(width * qApp->devicePixelRatio()); d->mHeight = ceil(height * qApp->devicePixelRatio()); d->mPriority = priority; d->mFeatures = features; d->mForce = false; d->mTile = false; d->mNormalizedRect = NormalizedRect(); d->mPartialUpdatesWanted = false; } PixmapRequest::~PixmapRequest() { delete d; } DocumentObserver *PixmapRequest::observer() const { return d->mObserver; } int PixmapRequest::pageNumber() const { return d->mPageNumber; } int PixmapRequest::width() const { return d->mWidth; } int PixmapRequest::height() const { return d->mHeight; } int PixmapRequest::priority() const { return d->mPriority; } bool PixmapRequest::asynchronous() const { return d->mFeatures & Asynchronous; } bool PixmapRequest::preload() const { return d->mFeatures & Preload; } Page* PixmapRequest::page() const { return d->mPage; } void PixmapRequest::setTile( bool tile ) { d->mTile = tile; } bool PixmapRequest::isTile() const { return d->mTile; } void PixmapRequest::setNormalizedRect( const NormalizedRect &rect ) { if ( d->mNormalizedRect == rect ) return; d->mNormalizedRect = rect; } const NormalizedRect& PixmapRequest::normalizedRect() const { return d->mNormalizedRect; } void PixmapRequest::setPartialUpdatesWanted(bool partialUpdatesWanted) { d->mPartialUpdatesWanted = partialUpdatesWanted; } bool PixmapRequest::partialUpdatesWanted() const { return d->mPartialUpdatesWanted; } Okular::TilesManager* PixmapRequestPrivate::tilesManager() const { return mPage->d->tilesManager(mObserver); } void PixmapRequestPrivate::swap() { qSwap( mWidth, mHeight ); } class Okular::ExportFormatPrivate : public QSharedData { public: ExportFormatPrivate( const QString &description, const QMimeType &mimeType, const QIcon &icon = QIcon() ) : QSharedData(), mDescription( description ), mMimeType( mimeType ), mIcon( icon ) { } ~ExportFormatPrivate() { } QString mDescription; QMimeType mMimeType; QIcon mIcon; }; ExportFormat::ExportFormat() : d( new ExportFormatPrivate( QString(), QMimeType() ) ) { } ExportFormat::ExportFormat( const QString &description, const QMimeType &mimeType ) : d( new ExportFormatPrivate( description, mimeType ) ) { } ExportFormat::ExportFormat( const QIcon &icon, const QString &description, const QMimeType &mimeType ) : d( new ExportFormatPrivate( description, mimeType, icon ) ) { } ExportFormat::~ExportFormat() { } ExportFormat::ExportFormat( const ExportFormat &other ) : d( other.d ) { } ExportFormat& ExportFormat::operator=( const ExportFormat &other ) { if ( this == &other ) return *this; d = other.d; return *this; } QString ExportFormat::description() const { return d->mDescription; } QMimeType ExportFormat::mimeType() const { return d->mMimeType; } QIcon ExportFormat::icon() const { return d->mIcon; } bool ExportFormat::isNull() const { return !d->mMimeType.isValid() || d->mDescription.isNull(); } ExportFormat ExportFormat::standardFormat( StandardExportFormat type ) { QMimeDatabase db; switch ( type ) { case PlainText: return ExportFormat( QIcon::fromTheme( QStringLiteral("text-x-generic") ), i18n( "Plain &Text..." ), db.mimeTypeForName( QStringLiteral("text/plain") ) ); break; case PDF: return ExportFormat( QIcon::fromTheme( QStringLiteral("application-pdf") ), i18n( "PDF" ), db.mimeTypeForName( QStringLiteral("application/pdf") ) ); break; case OpenDocumentText: return ExportFormat( QIcon::fromTheme( QStringLiteral("application-vnd.oasis.opendocument.text") ), i18nc( "This is the document format", "OpenDocument Text" ), db.mimeTypeForName( QStringLiteral("application/vnd.oasis.opendocument.text") ) ); break; case HTML: return ExportFormat( QIcon::fromTheme( QStringLiteral("text-html") ), i18nc( "This is the document format", "HTML" ), db.mimeTypeForName( QStringLiteral("text/html") ) ); break; } return ExportFormat(); } bool ExportFormat::operator==( const ExportFormat &other ) const { return d == other.d; } bool ExportFormat::operator!=( const ExportFormat &other ) const { return d != other.d; } QDebug operator<<( QDebug str, const Okular::PixmapRequest &req ) { QString s = QStringLiteral( "PixmapRequest(#%2, %1, %3x%4, page %6, prio %5)" ) .arg( QString( req.asynchronous() ? QStringLiteral ( "async" ) : QStringLiteral ( "sync" ) ) ) .arg( (qulonglong)req.observer() ) .arg( req.width() ) .arg( req.height() ) .arg( req.priority() ) .arg( req.pageNumber() ); str << qPrintable( s ); return str; } #include "moc_generator.cpp" /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/generator.h b/core/generator.h index 5209093a1..24438548e 100644 --- a/core/generator.h +++ b/core/generator.h @@ -1,751 +1,773 @@ /*************************************************************************** * Copyright (C) 2004-5 by Enrico Ros * * Copyright (C) 2005 by Piotr Szymanski * * Copyright (C) 2008 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. * ***************************************************************************/ #ifndef _OKULAR_GENERATOR_H_ #define _OKULAR_GENERATOR_H_ #include "okularcore_export.h" #include "document.h" #include "fontinfo.h" #include "global.h" #include "pagesize.h" #include #include #include #include #include #include #include #include #include #define OKULAR_EXPORT_PLUGIN(classname, json ) \ static_assert(json[0] != '\0', "arg2 must be a string literal"); \ K_PLUGIN_FACTORY_WITH_JSON(classname ## Factory, json, registerPlugin();) class QByteArray; class QMutex; class QPrinter; class QPrintDialog; class QIcon; namespace Okular { class BackendOpaqueAction; class DocumentFonts; class DocumentInfo; class DocumentObserver; class DocumentSynopsis; class EmbeddedFile; class ExportFormatPrivate; class FontInfo; class GeneratorPrivate; class Page; class PixmapRequest; class PixmapRequestPrivate; class TextPage; class NormalizedRect; class SourceReference; /* Note: on contents generation and asynchronous queries. * Many observers may want to request data syncronously or asynchronously. * - Sync requests. These should be done in-place. * - Async request must be done in real background. That usually means a * thread, such as QThread derived classes. * Once contents are available, they must be immediately stored in the * Page they refer to, and a signal is emitted as soon as storing * (even for sync or async queries) has been done. */ /** * @short Defines an entry for the export menu * * This class encapsulates information about an export format. * Every Generator can support 0 or more export formats which can be * queried with @ref Generator::exportFormats(). */ class OKULARCORE_EXPORT ExportFormat { public: typedef QList List; /** * Creates an empty export format. * * @see isNull() */ ExportFormat(); /** * Creates a new export format. * * @param description The i18n'ed description of the format. * @param mimeType The supported mime type of the format. */ ExportFormat( const QString &description, const QMimeType &mimeType ); /** * Creates a new export format. * * @param icon The icon used in the GUI for this format. * @param description The i18n'ed description of the format. * @param mimeType The supported mime type of the format. */ ExportFormat( const QIcon &icon, const QString &description, const QMimeType &mimeType ); /** * Destroys the export format. */ ~ExportFormat(); /** * @internal */ ExportFormat( const ExportFormat &other ); /** * @internal */ ExportFormat& operator=( const ExportFormat &other ); /** * Returns the description of the format. */ QString description() const; /** * Returns the mime type of the format. */ QMimeType mimeType() const; /** * Returns the icon for GUI representations of the format. */ QIcon icon() const; /** * Returns whether the export format is null/valid. * * An ExportFormat is null if the mimetype is not valid or the * description is empty, or both. */ bool isNull() const; /** * Type of standard export format. */ enum StandardExportFormat { PlainText, ///< Plain text PDF, ///< PDF, aka Portable Document Format OpenDocumentText, ///< OpenDocument Text format @since 0.8 (KDE 4.2) HTML ///< OpenDocument Text format @since 0.8 (KDE 4.2) }; /** * Builds a standard format for the specified @p type . */ static ExportFormat standardFormat( StandardExportFormat type ); bool operator==( const ExportFormat &other ) const; bool operator!=( const ExportFormat &other ) const; private: /// @cond PRIVATE friend class ExportFormatPrivate; /// @endcond QSharedDataPointer d; }; /** * @short [Abstract Class] The information generator. * * Most of class members are virtuals and some of them pure virtual. The pure * virtuals provide the minimal functionalities for a Generator, that is being * able to generate QPixmap for the Page 's of the Document. * * Implementing the other functions will make the Generator able to provide * more contents and/or functionalities (like text extraction). * * Generation/query is requested by the Document class only, and that * class stores the resulting data into Page s. The data will then be * displayed by the GUI components (PageView, ThumbnailList, etc..). * * @see PrintInterface, ConfigInterface, GuiInterface */ class OKULARCORE_EXPORT Generator : public QObject { /// @cond PRIVATE friend class PixmapGenerationThread; friend class TextPageGenerationThread; /// @endcond Q_OBJECT public: /** * Describe the possible optional features that a Generator can * provide. */ enum GeneratorFeature { Threaded, TextExtraction, ///< Whether the Generator can extract text from the document in the form of TextPage's ReadRawData, ///< Whether the Generator can read a document directly from its raw data. FontInfo, ///< Whether the Generator can provide information about the fonts used in the document PageSizes, ///< Whether the Generator can change the size of the document pages. PrintNative, ///< Whether the Generator supports native cross-platform printing (QPainter-based). PrintPostscript, ///< Whether the Generator supports postscript-based file printing. PrintToFile, ///< Whether the Generator supports export to PDF & PS through the Print Dialog - TiledRendering ///< Whether the Generator can render tiles @since 0.16 (KDE 4.10) + TiledRendering, ///< Whether the Generator can render tiles @since 0.16 (KDE 4.10) + SwapBackingFile ///< Whether the Generator can hot-swap the file it's reading from @since 1.3 }; /** * Creates a new generator. */ Generator(QObject* parent = nullptr, const QVariantList& args = QVariantList()); /** * Destroys the generator. */ virtual ~Generator(); /** * Loads the document with the given @p fileName and fills the * @p pagesVector with the parsed pages. * * @note If you implement the WithPassword variants you don't need to implement this one * * @returns true on success, false otherwise. */ virtual bool loadDocument( const QString & fileName, QVector< Page * > & pagesVector ); /** * Loads the document from the raw data @p fileData and fills the * @p pagesVector with the parsed pages. * * @note If you implement the WithPassword variants you don't need to implement this one * * @note the Generator has to have the feature @ref ReadRawData enabled * * @returns true on success, false otherwise. */ virtual bool loadDocumentFromData( const QByteArray & fileData, QVector< Page * > & pagesVector ); /** * Loads the document with the given @p fileName and @p password and fills the * @p pagesVector with the parsed pages. * * @note Do not implement this if your format doesn't support passwords, it'll cleanly call loadDocument() * * @since 0.20 (KDE 4.14) * * @returns a LoadResult defining the result of the operation */ virtual Document::OpenResult loadDocumentWithPassword( const QString & fileName, QVector< Page * > & pagesVector, const QString &password ); /** * Loads the document from the raw data @p fileData and @p password and fills the * @p pagesVector with the parsed pages. * * @note Do not implement this if your format doesn't support passwords, it'll cleanly call loadDocumentFromData() * * @note the Generator has to have the feature @ref ReadRawData enabled * * @since 0.20 (KDE 4.14) * * @returns a LoadResult defining the result of the operation */ virtual Document::OpenResult loadDocumentFromDataWithPassword( const QByteArray & fileData, QVector< Page * > & pagesVector, const QString &password ); + /** + * Describes the result of an swap file operation. + * + * @since 1.3 + */ + enum SwapBackingFileResult + { + SwapBackingFileError, //< The document could not be swapped + SwapBackingFileNoOp, //< The document was swapped and nothing needs to be done + SwapBackingFileReloadInternalData //< The document was swapped and internal data (forms, annotations, etc) needs to be reloaded + }; + + /** + * Changes the path of the file we are reading from. The new path must + * point to a copy of the same document. + * + * @note the Generator has to have the feature @ref SwapBackingFile enabled + * + * @since 1.3 + */ + virtual SwapBackingFileResult swapBackingFile( const QString & newFileName, QVector & newPagesVector ); /** * This method is called when the document is closed and not used * any longer. * * @returns true on success, false otherwise. */ bool closeDocument(); /** * This method returns whether the generator is ready to * handle a new pixmap request. */ virtual bool canGeneratePixmap() const; /** * This method can be called to trigger the generation of * a new pixmap as described by @p request. */ virtual void generatePixmap( PixmapRequest * request ); /** * This method returns whether the generator is ready to * handle a new text page request. */ virtual bool canGenerateTextPage() const; /** * This method can be called to trigger the generation of * a text page for the given @p page. * * The generation is done synchronous or asynchronous, depending * on the @p type parameter and the capabilities of the * generator (e.g. multithreading). * * @see TextPage */ virtual void generateTextPage( Page * page ); /** * Returns the general information object of the document. * * Changed signature in okular version 0.21 */ virtual DocumentInfo generateDocumentInfo( const QSet &keys ) const; /** * Returns the 'table of content' object of the document or 0 if * no table of content is available. */ virtual const DocumentSynopsis * generateDocumentSynopsis(); /** * Returns the 'list of embedded fonts' object of the specified \page * of the document. * * \param page a page of the document, starting from 0 - -1 indicates all * the other fonts */ virtual FontInfo::List fontsForPage( int page ); /** * Returns the 'list of embedded files' object of the document or 0 if * no list of embedded files is available. */ virtual const QList * embeddedFiles() const; /** * This enum identifies the metric of the page size. */ enum PageSizeMetric { None, ///< The page size is not defined in a physical metric. Points, ///< The page size is given in 1/72 inches. Pixels ///< The page size is given in screen pixels @since 0.19 (KDE 4.13) }; /** * This method returns the metric of the page size. Default is @ref None. */ virtual PageSizeMetric pagesSizeMetric() const; /** * This method returns whether given @p action (@ref Permission) is * allowed in this document. */ virtual bool isAllowed( Permission action ) const; /** * This method is called when the orientation has been changed by the user. */ virtual void rotationChanged( Rotation orientation, Rotation oldOrientation ); /** * Returns the list of supported page sizes. */ virtual PageSize::List pageSizes() const; /** * This method is called when the page size has been changed by the user. */ virtual void pageSizeChanged( const PageSize &pageSize, const PageSize &oldPageSize ); /** * This method is called to print the document to the given @p printer. */ virtual bool print( QPrinter &printer ); /** * Possible print errors * @since 0.11 (KDE 4.5) */ enum PrintError { NoPrintError, ///< There was no print error UnknownPrintError, TemporaryFileOpenPrintError, FileConversionPrintError, PrintingProcessCrashPrintError, PrintingProcessStartPrintError, PrintToFilePrintError, InvalidPrinterStatePrintError, UnableToFindFilePrintError, NoFileToPrintError, NoBinaryToPrintError, InvalidPageSizePrintError ///< @since 0.18.2 (KDE 4.12.2) }; /** * This method returns the meta data of the given @p key with the given @p option * of the document. */ virtual QVariant metaData( const QString &key, const QVariant &option ) const; /** * Returns the list of additional supported export formats. */ virtual ExportFormat::List exportFormats() const; /** * This method is called to export the document in the given @p format and save it * under the given @p fileName. The format must be one of the supported export formats. */ virtual bool exportTo( const QString &fileName, const ExportFormat &format ); /** * This method is called to know which wallet data should be used for the given file name. * Unless you have very special requirements to where wallet data should be stored you * don't need to reimplement this method. */ virtual void walletDataForFile( const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey ) const; /** * Query for the specified @p feature. */ bool hasFeature( GeneratorFeature feature ) const; /** * Update DPI of the generator * * @since 0.19 (KDE 4.13) */ void setDPI(const QSizeF &dpi); /** * Returns the 'layers model' object of the document or NULL if * layers model is not available. * * @since 0.24 */ virtual QAbstractItemModel * layersModel() const; /** * Calls the backend to execute an BackendOpaqueAction */ virtual void opaqueAction( const BackendOpaqueAction *action ); Q_SIGNALS: /** * This signal should be emitted whenever an error occurred in the generator. * * @param message The message which should be shown to the user. * @param duration The time that the message should be shown to the user. */ void error( const QString &message, int duration ); /** * This signal should be emitted whenever the user should be warned. * * @param message The message which should be shown to the user. * @param duration The time that the message should be shown to the user. */ void warning( const QString &message, int duration ); /** * This signal should be emitted whenever the user should be noticed. * * @param message The message which should be shown to the user. * @param duration The time that the message should be shown to the user. */ void notice( const QString &message, int duration ); protected: /** * This method must be called when the pixmap request triggered by generatePixmap() * has been finished. */ void signalPixmapRequestDone( PixmapRequest * request ); /** * This method must be called when a text generation has been finished. */ void signalTextGenerationDone( Page *page, TextPage *textPage ); /** * This method is called when the document is closed and not used * any longer. * * @returns true on success, false otherwise. */ virtual bool doCloseDocument() = 0; /** * Returns the image of the page as specified in * the passed pixmap @p request. * * @warning this method may be executed in its own separated thread if the * @ref Threaded is enabled! */ virtual QImage image( PixmapRequest *page ); /** * Returns the text page for the given @p page. * * @warning this method may be executed in its own separated thread if the * @ref Threaded is enabled! */ virtual TextPage* textPage( Page *page ); /** * Returns a pointer to the document. */ const Document * document() const; /** * Toggle the @p feature . */ void setFeature( GeneratorFeature feature, bool on = true ); /** * Internal document setting */ enum DocumentMetaDataKey { PaperColorMetaData, ///< Returns (QColor) the paper color if set in Settings or the default color (white) if option is true (otherwise returns a non initialized QColor) TextAntialiasMetaData, ///< Returns (bool) text antialias from Settings (option is not used) GraphicsAntialiasMetaData, ///< Returns (bool)graphic antialias from Settings (option is not used) TextHintingMetaData ///< Returns (bool)text hinting from Settings (option is not used) }; /** * Request a meta data of the Document, if available, like an internal * setting. * * @since 1.1 */ QVariant documentMetaData( const DocumentMetaDataKey key, const QVariant &option = QVariant() ) const; /** * Request a meta data of the Document, if available, like an internal * setting. */ OKULARCORE_DEPRECATED QVariant documentMetaData( const QString &key, const QVariant &option = QVariant() ) const; /** * Return the pointer to a mutex the generator can use freely. */ QMutex* userMutex() const; /** * Set the bounding box of a page after the page has already been handed * to the Document. Call this instead of Page::setBoundingBox() to ensure * that all observers are notified. * * @since 0.7 (KDE 4.1) */ void updatePageBoundingBox( int page, const NormalizedRect & boundingBox ); /** * Returns DPI, previously set via setDPI() * @since 0.19 (KDE 4.13) */ QSizeF dpi() const; protected Q_SLOTS: /** * Gets the font data for the given font * * @since 0.8 (KDE 4.1) */ void requestFontData(const Okular::FontInfo &font, QByteArray *data); /** * Returns the last print error in case print() failed * @since 0.11 (KDE 4.5) */ Okular::Generator::PrintError printError() const; /** * This method can be called to trigger a partial pixmap update for the given request * Make sure you call it in a way it's executed in the main thread. * @since 1.3 */ void signalPartialPixmapRequest( Okular::PixmapRequest *request, const QImage &image ); protected: /// @cond PRIVATE Generator(GeneratorPrivate &dd, QObject *parent, const QVariantList &args); Q_DECLARE_PRIVATE( Generator ) GeneratorPrivate *d_ptr; friend class Document; friend class DocumentPrivate; /// @endcond PRIVATE private: Q_DISABLE_COPY( Generator ) Q_PRIVATE_SLOT( d_func(), void pixmapGenerationFinished() ) Q_PRIVATE_SLOT( d_func(), void textpageGenerationFinished() ) }; /** * @short Describes a pixmap type request. */ class OKULARCORE_EXPORT PixmapRequest { friend class Document; friend class DocumentPrivate; public: enum PixmapRequestFeature { NoFeature = 0, Asynchronous = 1, Preload = 2 }; Q_DECLARE_FLAGS( PixmapRequestFeatures, PixmapRequestFeature ) /** * Creates a new pixmap request. * * @param observer The observer. * @param pageNumber The page number. * @param width The width of the page. * @param height The height of the page. * @param priority The priority of the request. * @param features The features of generation. */ PixmapRequest( DocumentObserver *observer, int pageNumber, int width, int height, int priority, PixmapRequestFeatures features ); /** * Destroys the pixmap request. */ ~PixmapRequest(); /** * Returns the observer of the request. */ DocumentObserver *observer() const; /** * Returns the page number of the request. */ int pageNumber() const; /** * Returns the page width of the requested pixmap. */ int width() const; /** * Returns the page height of the requested pixmap. */ int height() const; /** * Returns the priority (less it better, 0 is maximum) of the * request. */ int priority() const; /** * Returns whether the generation should be done synchronous or * asynchronous. * * If asynchronous, the pixmap is created in a thread and the observer * is notified when the job is done. */ bool asynchronous() const; /** * Returns whether the generation request is for a page that is not important * i.e. it's just for speeding up future rendering */ bool preload() const; /** * Returns a pointer to the page where the pixmap shall be generated for. */ Page *page() const; /** * Sets whether the generator should render only the given normalized * rect or the entire page * * @since 0.16 (KDE 4.10) */ void setTile( bool tile ); /** * Returns whether the generator should render just the region given by * normalizedRect() or the entire page. * * @since 0.16 (KDE 4.10) */ bool isTile() const; /** * Sets the region of the page to request. * * @since 0.16 (KDE 4.10) */ void setNormalizedRect( const NormalizedRect &rect ); /** * Returns the normalized region of the page to request. * * @since 0.16 (KDE 4.10) */ const NormalizedRect& normalizedRect() const; /** * Sets whether the request should report back updates if possible * * @since 1.3 */ void setPartialUpdatesWanted(bool partialUpdatesWanted); /** * Should the request report back updates if possible? * * @since 1.3 */ bool partialUpdatesWanted() const; private: Q_DISABLE_COPY( PixmapRequest ) friend class PixmapRequestPrivate; PixmapRequestPrivate* const d; }; } Q_DECLARE_METATYPE(Okular::Generator::PrintError) Q_DECLARE_METATYPE(Okular::PixmapRequest*) #define OkularGeneratorInterface_iid "org.kde.okular.Generator" Q_DECLARE_INTERFACE(Okular::Generator, OkularGeneratorInterface_iid) #ifndef QT_NO_DEBUG_STREAM OKULARCORE_EXPORT QDebug operator<<( QDebug str, const Okular::PixmapRequest &req ); #endif #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/observer.h b/core/observer.h index 6f2bf6502..9e01c8707 100644 --- a/core/observer.h +++ b/core/observer.h @@ -1,121 +1,125 @@ /*************************************************************************** * Copyright (C) 2005 by Enrico Ros * * Copyright (C) 2005 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. * ***************************************************************************/ #ifndef _OKULAR_DOCUMENTOBSERVER_H_ #define _OKULAR_DOCUMENTOBSERVER_H_ #include #include "okularcore_export.h" namespace Okular { class Page; /** * @short Base class for objects being notified when something changes. * * Inherit this class and call Document->addObserver( yourClass ) to get * notified of asynchronous events (new pixmap generated, or changed, etc..). */ class OKULARCORE_EXPORT DocumentObserver { public: DocumentObserver(); /** * Destroys the document observer. */ virtual ~DocumentObserver(); /** * Flags that can be sent from the document to all observers to * inform them about the type of object that has been changed. */ enum ChangedFlags { Pixmap = 1, ///< Pixmaps has been changed Bookmark = 2, ///< Bookmarks has been changed Highlights = 4, ///< Highlighting information has been changed TextSelection = 8, ///< Text selection has been changed Annotations = 16, ///< Annotations have been changed BoundingBox = 32, ///< Bounding boxes have been changed - NeedSaveAs = 64 ///< Set along with Annotations when Save As is needed or annotation changes will be lost @since 0.15 (KDE 4.9) + NeedSaveAs = 64 ///< Set when "Save" is needed or annotation/form changes will be lost @since 0.15 (KDE 4.9) @deprecated }; /** * ... */ enum SetupFlags { DocumentChanged = 1, ///< The document is a new document. - NewLayoutForPages = 2 ///< All the pages have + NewLayoutForPages = 2, ///< All the pages have + UrlChanged = 4 ///< The URL has changed @since 1.3 }; /** * This method is called whenever the document is initialized or reconstructed. * * @param pages The vector of pages of the document. * @param setupFlags the flags with the information about the setup */ virtual void notifySetup( const QVector< Okular::Page * > &pages, int setupFlags ); /** * This method is called whenever the viewport has been changed. * * @param smoothMove If true, the move shall be animated. */ virtual void notifyViewportChanged( bool smoothMove ); /** * This method is called whenever the content on @p page described by the * passed @p flags has been changed. */ virtual void notifyPageChanged( int page, int flags ); /** * This method is called whenever the content described by the passed @p flags * has been cleared. */ virtual void notifyContentsCleared( int flags ); /** * This method is called whenever the visible rects have been changed. */ virtual void notifyVisibleRectsChanged(); /** * This method is called whenever the zoom of the document has been changed. */ virtual void notifyZoom( int factor ); /** * Returns whether the observer agrees that all pixmaps for the given * @p page can be unloaded to improve memory usage. * * Returns true per default. */ virtual bool canUnloadPixmap( int page ) const; /** * This method is called after the current page of the document has been entered. * * @param previous The number of the previous page (is @c -1 for the initial page change). * @param current The number of the current page. * * @since 0.16 (KDE 4.10) */ virtual void notifyCurrentPageChanged( int previous, int current ); private: class Private; const Private* d; }; } #endif diff --git a/core/page.cpp b/core/page.cpp index 72890e522..227ebf801 100644 --- a/core/page.cpp +++ b/core/page.cpp @@ -1,1034 +1,1123 @@ /*************************************************************************** * Copyright (C) 2004 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 "page.h" #include "page_p.h" // qt/kde includes #include #include #include #include #include #include #include #include #include // local includes #include "action.h" #include "annotations.h" #include "annotations_p.h" #include "area.h" #include "debug_p.h" #include "document.h" #include "document_p.h" #include "form.h" #include "form_p.h" #include "observer.h" #include "pagecontroller_p.h" #include "pagesize.h" #include "pagetransition.h" #include "rotationjob_p.h" #include "textpage.h" #include "textpage_p.h" #include "tile.h" #include "tilesmanager_p.h" #include "utils_p.h" #include #ifdef PAGE_PROFILE #include #endif using namespace Okular; static const double distanceConsideredEqual = 25; // 5px static void deleteObjectRects( QLinkedList< ObjectRect * >& rects, const QSet& which ) { QLinkedList< ObjectRect * >::iterator it = rects.begin(), end = rects.end(); for ( ; it != end; ) if ( which.contains( (*it)->objectType() ) ) { delete *it; it = rects.erase( it ); } else ++it; } PagePrivate::PagePrivate( Page *page, uint n, double w, double h, Rotation o ) : m_page( page ), m_number( n ), m_orientation( o ), m_width( w ), m_height( h ), m_doc( nullptr ), m_boundingBox( 0, 0, 1, 1 ), m_rotation( Rotation0 ), m_text( nullptr ), m_transition( nullptr ), m_textSelections( nullptr ), m_openingAction( nullptr ), m_closingAction( nullptr ), m_duration( -1 ), m_isBoundingBoxKnown( false ) { // avoid Division-By-Zero problems in the program if ( m_width <= 0 ) m_width = 1; if ( m_height <= 0 ) m_height = 1; } PagePrivate::~PagePrivate() { qDeleteAll( formfields ); delete m_openingAction; delete m_closingAction; delete m_text; delete m_transition; } PagePrivate *PagePrivate::get( Page * page ) { return page->d; } void PagePrivate::imageRotationDone( RotationJob * job ) { TilesManager *tm = tilesManager( job->observer() ); if ( tm ) { QPixmap *pixmap = new QPixmap( QPixmap::fromImage( job->image() ) ); tm->setPixmap( pixmap, job->rect() ); delete pixmap; return; } QMap< DocumentObserver*, PixmapObject >::iterator it = m_pixmaps.find( job->observer() ); if ( it != m_pixmaps.end() ) { PixmapObject &object = it.value(); (*object.m_pixmap) = QPixmap::fromImage( job->image() ); object.m_rotation = job->rotation(); } else { PixmapObject object; object.m_pixmap = new QPixmap( QPixmap::fromImage( job->image() ) ); object.m_rotation = job->rotation(); m_pixmaps.insert( job->observer(), object ); } } QTransform PagePrivate::rotationMatrix() const { return Okular::buildRotationMatrix( m_rotation ); } /** class Page **/ Page::Page( uint page, double w, double h, Rotation o ) : d( new PagePrivate( this, page, w, h, o ) ) { } Page::~Page() { - deletePixmaps(); - deleteRects(); - d->deleteHighlights(); - deleteAnnotations(); - d->deleteTextSelections(); - deleteSourceReferences(); - - delete d; + if (d) + { + deletePixmaps(); + deleteRects(); + d->deleteHighlights(); + deleteAnnotations(); + d->deleteTextSelections(); + deleteSourceReferences(); + + delete d; + } } int Page::number() const { return d->m_number; } Rotation Page::orientation() const { return d->m_orientation; } Rotation Page::rotation() const { return d->m_rotation; } Rotation Page::totalOrientation() const { return (Rotation)( ( (int)d->m_orientation + (int)d->m_rotation ) % 4 ); } double Page::width() const { return d->m_width; } double Page::height() const { return d->m_height; } double Page::ratio() const { return d->m_height / d->m_width; } NormalizedRect Page::boundingBox() const { return d->m_boundingBox; } bool Page::isBoundingBoxKnown() const { return d->m_isBoundingBoxKnown; } void Page::setBoundingBox( const NormalizedRect& bbox ) { if ( d->m_isBoundingBoxKnown && d->m_boundingBox == bbox ) return; // Allow tiny rounding errors (happens during rotation) static const double epsilon = 0.00001; Q_ASSERT( bbox.left >= -epsilon && bbox.top >= -epsilon && bbox.right <= 1 + epsilon && bbox.bottom <= 1 + epsilon ); d->m_boundingBox = bbox & NormalizedRect( 0., 0., 1., 1. ); d->m_isBoundingBoxKnown = true; } bool Page::hasPixmap( DocumentObserver *observer, int width, int height, const NormalizedRect &rect ) const { TilesManager *tm = d->tilesManager( observer ); if ( tm ) { if ( width != tm->width() || height != tm->height() ) { // FIXME hasPixmap should not be calling setSize on the TilesManager this is not very "const" // as this function claims to be if ( width != -1 && height != -1 ) { tm->setSize( width, height ); } return false; } return tm->hasPixmap( rect ); } QMap< DocumentObserver*, PagePrivate::PixmapObject >::const_iterator it = d->m_pixmaps.constFind( observer ); if ( it == d->m_pixmaps.constEnd() ) return false; if ( width == -1 || height == -1 ) return true; const QPixmap *pixmap = it.value().m_pixmap; return (pixmap->width() == width && pixmap->height() == height); } bool Page::hasTextPage() const { return d->m_text != nullptr; } RegularAreaRect * Page::wordAt( const NormalizedPoint &p, QString *word ) const { if ( d->m_text ) return d->m_text->wordAt( p, word ); return nullptr; } RegularAreaRect * Page::textArea ( TextSelection * selection ) const { if ( d->m_text ) return d->m_text->textArea( selection ); return nullptr; } bool Page::hasObjectRect( double x, double y, double xScale, double yScale ) const { if ( m_rects.isEmpty() ) return false; QLinkedList< ObjectRect * >::const_iterator it = m_rects.begin(), end = m_rects.end(); for ( ; it != end; ++it ) if ( (*it)->distanceSqr( x, y, xScale, yScale ) < distanceConsideredEqual ) return true; return false; } bool Page::hasHighlights( int s_id ) const { // simple case: have no highlights if ( m_highlights.isEmpty() ) return false; // simple case: we have highlights and no id to match if ( s_id == -1 ) return true; // iterate on the highlights list to find an entry by id QLinkedList< HighlightAreaRect * >::const_iterator it = m_highlights.begin(), end = m_highlights.end(); for ( ; it != end; ++it ) if ( (*it)->s_id == s_id ) return true; return false; } bool Page::hasTransition() const { return d->m_transition != nullptr; } bool Page::hasAnnotations() const { return !m_annotations.isEmpty(); } RegularAreaRect * Page::findText( int id, const QString & text, SearchDirection direction, Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *lastRect ) const { RegularAreaRect* rect = nullptr; if ( text.isEmpty() || !d->m_text ) return rect; rect = d->m_text->findText( id, text, direction, caseSensitivity, lastRect ); return rect; } QString Page::text( const RegularAreaRect * area ) const { return text( area, TextPage::AnyPixelTextAreaInclusionBehaviour ); } QString Page::text( const RegularAreaRect * area, TextPage::TextAreaInclusionBehaviour b ) const { QString ret; if ( !d->m_text ) return ret; if ( area ) { RegularAreaRect rotatedArea = *area; rotatedArea.transform( d->rotationMatrix().inverted() ); ret = d->m_text->text( &rotatedArea, b ); } else ret = d->m_text->text( nullptr, b ); return ret; } TextEntity::List Page::words( const RegularAreaRect * area, TextPage::TextAreaInclusionBehaviour b ) const { TextEntity::List ret; if ( !d->m_text ) return ret; if ( area ) { RegularAreaRect rotatedArea = *area; rotatedArea.transform( d->rotationMatrix().inverted() ); ret = d->m_text->words( &rotatedArea, b ); } else ret = d->m_text->words( nullptr, b ); for (int i = 0; i < ret.length(); ++i) { const TextEntity * orig = ret[i]; ret[i] = new TextEntity( orig->text(), new Okular::NormalizedRect(orig->transformedArea ( d->rotationMatrix() )) ); delete orig; } return ret; } void PagePrivate::rotateAt( Rotation orientation ) { if ( orientation == m_rotation ) return; deleteHighlights(); deleteTextSelections(); if ( ( (int)m_orientation + (int)m_rotation ) % 2 != ( (int)m_orientation + (int)orientation ) % 2 ) qSwap( m_width, m_height ); Rotation oldRotation = m_rotation; m_rotation = orientation; /** * Rotate the images of the page. */ QMapIterator< DocumentObserver*, PagePrivate::PixmapObject > it( m_pixmaps ); while ( it.hasNext() ) { it.next(); const PagePrivate::PixmapObject &object = it.value(); RotationJob *job = new RotationJob( object.m_pixmap->toImage(), object.m_rotation, m_rotation, it.key() ); job->setPage( this ); m_doc->m_pageController->addRotationJob(job); } /** * Rotate tiles manager */ QMapIterator i(m_tilesManagers); while (i.hasNext()) { i.next(); TilesManager *tm = i.value(); if ( tm ) tm->setRotation( m_rotation ); } /** * Rotate the object rects on the page. */ const QTransform matrix = rotationMatrix(); QLinkedList< ObjectRect * >::const_iterator objectIt = m_page->m_rects.begin(), end = m_page->m_rects.end(); for ( ; objectIt != end; ++objectIt ) (*objectIt)->transform( matrix ); QLinkedList< HighlightAreaRect* >::const_iterator hlIt = m_page->m_highlights.begin(), hlItEnd = m_page->m_highlights.end(); for ( ; hlIt != hlItEnd; ++hlIt ) { (*hlIt)->transform( RotationJob::rotationMatrix( oldRotation, m_rotation ) ); } } void PagePrivate::changeSize( const PageSize &size ) { if ( size.isNull() || ( size.width() == m_width && size.height() == m_height ) ) return; m_page->deletePixmaps(); // deleteHighlights(); // deleteTextSelections(); m_width = size.width(); m_height = size.height(); if ( m_rotation % 2 ) qSwap( m_width, m_height ); } const ObjectRect * Page::objectRect( ObjectRect::ObjectType type, double x, double y, double xScale, double yScale ) const { // Walk list in reverse order so that annotations in the foreground are preferred QLinkedListIterator< ObjectRect * > it( m_rects ); it.toBack(); while ( it.hasPrevious() ) { const ObjectRect *objrect = it.previous(); if ( ( objrect->objectType() == type ) && objrect->distanceSqr( x, y, xScale, yScale ) < distanceConsideredEqual ) return objrect; } return nullptr; } QLinkedList< const ObjectRect * > Page::objectRects( ObjectRect::ObjectType type, double x, double y, double xScale, double yScale ) const { QLinkedList< const ObjectRect * > result; QLinkedListIterator< ObjectRect * > it( m_rects ); it.toBack(); while ( it.hasPrevious() ) { const ObjectRect *objrect = it.previous(); if ( ( objrect->objectType() == type ) && objrect->distanceSqr( x, y, xScale, yScale ) < distanceConsideredEqual ) result.append( objrect ); } return result; } const ObjectRect* Page::nearestObjectRect( ObjectRect::ObjectType type, double x, double y, double xScale, double yScale, double * distance ) const { ObjectRect * res = nullptr; double minDistance = std::numeric_limits::max(); QLinkedList< ObjectRect * >::const_iterator it = m_rects.constBegin(), end = m_rects.constEnd(); for ( ; it != end; ++it ) { if ( (*it)->objectType() == type ) { double d = (*it)->distanceSqr( x, y, xScale, yScale ); if ( d < minDistance ) { res = (*it); minDistance = d; } } } if ( distance ) *distance = minDistance; return res; } const PageTransition * Page::transition() const { return d->m_transition; } QLinkedList< Annotation* > Page::annotations() const { return m_annotations; } +Annotation * Page::annotation( const QString & uniqueName ) const +{ + foreach(Annotation *a, m_annotations) + { + if ( a->uniqueName() == uniqueName ) + return a; + } + return nullptr; +} + const Action * Page::pageAction( PageAction action ) const { switch ( action ) { case Page::Opening: return d->m_openingAction; break; case Page::Closing: return d->m_closingAction; break; } return nullptr; } QLinkedList< FormField * > Page::formFields() const { return d->formfields; } void Page::setPixmap( DocumentObserver *observer, QPixmap *pixmap, const NormalizedRect &rect ) { if ( d->m_rotation == Rotation0 ) { TilesManager *tm = d->tilesManager( observer ); if ( tm ) { tm->setPixmap( pixmap, rect ); delete pixmap; return; } QMap< DocumentObserver*, PagePrivate::PixmapObject >::iterator it = d->m_pixmaps.find( observer ); if ( it != d->m_pixmaps.end() ) { delete it.value().m_pixmap; } else { it = d->m_pixmaps.insert( observer, PagePrivate::PixmapObject() ); } it.value().m_pixmap = pixmap; it.value().m_rotation = d->m_rotation; } else { // it can happen that we get a setPixmap while closing and thus the page controller is gone if ( d->m_doc->m_pageController ) { RotationJob *job = new RotationJob( pixmap->toImage(), Rotation0, d->m_rotation, observer ); job->setPage( d ); job->setRect( TilesManager::toRotatedRect( rect, d->m_rotation ) ); d->m_doc->m_pageController->addRotationJob(job); } delete pixmap; } } void Page::setTextPage( TextPage * textPage ) { delete d->m_text; d->m_text = textPage; if ( d->m_text ) { d->m_text->d->m_page = d; /** * Correct text order for before text selection */ d->m_text->d->correctTextOrder(); } } void Page::setObjectRects( const QLinkedList< ObjectRect * > & rects ) { QSet which; which << ObjectRect::Action << ObjectRect::Image; deleteObjectRects( m_rects, which ); /** * Rotate the object rects of the page. */ const QTransform matrix = d->rotationMatrix(); QLinkedList< ObjectRect * >::const_iterator objectIt = rects.begin(), end = rects.end(); for ( ; objectIt != end; ++objectIt ) (*objectIt)->transform( matrix ); m_rects << rects; } void PagePrivate::setHighlight( int s_id, RegularAreaRect *rect, const QColor & color ) { HighlightAreaRect * hr = new HighlightAreaRect(rect); hr->s_id = s_id; hr->color = color; m_page->m_highlights.append( hr ); } void PagePrivate::setTextSelections( RegularAreaRect *r, const QColor & color ) { deleteTextSelections(); if ( r ) { HighlightAreaRect * hr = new HighlightAreaRect( r ); hr->s_id = -1; hr->color = color; m_textSelections = hr; delete r; } } void Page::setSourceReferences( const QLinkedList< SourceRefObjectRect * > & refRects ) { deleteSourceReferences(); foreach( SourceRefObjectRect * rect, refRects ) m_rects << rect; } void Page::setDuration( double seconds ) { d->m_duration = seconds; } double Page::duration() const { return d->m_duration; } void Page::setLabel( const QString& label ) { d->m_label = label; } QString Page::label() const { return d->m_label; } const RegularAreaRect * Page::textSelection() const { return d->m_textSelections; } QColor Page::textSelectionColor() const { return d->m_textSelections ? d->m_textSelections->color : QColor(); } void Page::addAnnotation( Annotation * annotation ) { // Generate uniqueName: okular-{UUID} if(annotation->uniqueName().isEmpty()) { QString uniqueName = QStringLiteral("okular-") + QUuid::createUuid().toString(); annotation->setUniqueName( uniqueName ); } annotation->d_ptr->m_page = d; m_annotations.append( annotation ); AnnotationObjectRect *rect = new AnnotationObjectRect( annotation ); // Rotate the annotation on the page. const QTransform matrix = d->rotationMatrix(); annotation->d_ptr->annotationTransform( matrix ); m_rects.append( rect ); } bool Page::removeAnnotation( Annotation * annotation ) { if ( !d->m_doc->m_parent->canRemovePageAnnotation(annotation) ) return false; QLinkedList< Annotation * >::iterator aIt = m_annotations.begin(), aEnd = m_annotations.end(); for ( ; aIt != aEnd; ++aIt ) { if((*aIt) && (*aIt)->uniqueName()==annotation->uniqueName()) { int rectfound = false; QLinkedList< ObjectRect * >::iterator it = m_rects.begin(), end = m_rects.end(); for ( ; it != end && !rectfound; ++it ) if ( ( (*it)->objectType() == ObjectRect::OAnnotation ) && ( (*it)->object() == (*aIt) ) ) { delete *it; it = m_rects.erase( it ); rectfound = true; } qCDebug(OkularCoreDebug) << "removed annotation:" << annotation->uniqueName(); annotation->d_ptr->m_page = nullptr; m_annotations.erase( aIt ); break; } } return true; } void Page::setTransition( PageTransition * transition ) { delete d->m_transition; d->m_transition = transition; } void Page::setPageAction( PageAction action, Action * link ) { switch ( action ) { case Page::Opening: delete d->m_openingAction; d->m_openingAction = link; break; case Page::Closing: delete d->m_closingAction; d->m_closingAction = link; break; } } void Page::setFormFields( const QLinkedList< FormField * >& fields ) { qDeleteAll( d->formfields ); d->formfields = fields; QLinkedList< FormField * >::const_iterator it = d->formfields.begin(), itEnd = d->formfields.end(); for ( ; it != itEnd; ++it ) { (*it)->d_ptr->setDefault(); } } void Page::deletePixmap( DocumentObserver *observer ) { TilesManager *tm = d->tilesManager( observer ); if ( tm ) { delete tm; d->m_tilesManagers.remove(observer); } else { PagePrivate::PixmapObject object = d->m_pixmaps.take( observer ); delete object.m_pixmap; } } void Page::deletePixmaps() { QMapIterator< DocumentObserver*, PagePrivate::PixmapObject > it( d->m_pixmaps ); while ( it.hasNext() ) { it.next(); delete it.value().m_pixmap; } d->m_pixmaps.clear(); qDeleteAll(d->m_tilesManagers); d->m_tilesManagers.clear(); } void Page::deleteRects() { // delete ObjectRects of type Link and Image QSet which; which << ObjectRect::Action << ObjectRect::Image; deleteObjectRects( m_rects, which ); } void PagePrivate::deleteHighlights( int s_id ) { // delete highlights by ID QLinkedList< HighlightAreaRect* >::iterator it = m_page->m_highlights.begin(), end = m_page->m_highlights.end(); while ( it != end ) { HighlightAreaRect* highlight = *it; if ( s_id == -1 || highlight->s_id == s_id ) { it = m_page->m_highlights.erase( it ); delete highlight; } else ++it; } } void PagePrivate::deleteTextSelections() { delete m_textSelections; m_textSelections = nullptr; } void Page::deleteSourceReferences() { deleteObjectRects( m_rects, QSet() << ObjectRect::SourceRef ); } void Page::deleteAnnotations() { // delete ObjectRects of type Annotation deleteObjectRects( m_rects, QSet() << ObjectRect::OAnnotation ); // delete all stored annotations QLinkedList< Annotation * >::const_iterator aIt = m_annotations.begin(), aEnd = m_annotations.end(); for ( ; aIt != aEnd; ++aIt ) delete *aIt; m_annotations.clear(); } -void PagePrivate::restoreLocalContents( const QDomNode & pageNode ) +bool PagePrivate::restoreLocalContents( const QDomNode & pageNode ) { + bool loadedAnything = false; // set if something actually gets loaded + // iterate over all chilren (annotationList, ...) QDomNode childNode = pageNode.firstChild(); while ( childNode.isElement() ) { QDomElement childElement = childNode.toElement(); childNode = childNode.nextSibling(); // parse annotationList child element if ( childElement.tagName() == QLatin1String("annotationList") ) { #ifdef PAGE_PROFILE QTime time; time.start(); #endif // Clone annotationList as root node in restoredLocalAnnotationList const QDomNode clonedNode = restoredLocalAnnotationList.importNode( childElement, true ); restoredLocalAnnotationList.appendChild( clonedNode ); // iterate over all annotations QDomNode annotationNode = childElement.firstChild(); while( annotationNode.isElement() ) { // get annotation element and advance to next annot QDomElement annotElement = annotationNode.toElement(); annotationNode = annotationNode.nextSibling(); // get annotation from the dom element Annotation * annotation = AnnotationUtils::createAnnotation( annotElement ); // append annotation to the list or show warning if ( annotation ) { m_doc->performAddPageAnnotation(m_number, annotation); qCDebug(OkularCoreDebug) << "restored annot:" << annotation->uniqueName(); + loadedAnything = true; } else qCWarning(OkularCoreDebug).nospace() << "page (" << m_number << "): can't restore an annotation from XML."; } #ifdef PAGE_PROFILE qCDebug(OkularCoreDebug).nospace() << "annots: XML Load time: " << time.elapsed() << "ms"; #endif } // parse formList child element else if ( childElement.tagName() == QLatin1String("forms") ) { + // Clone forms as root node in restoredFormFieldList + const QDomNode clonedNode = restoredFormFieldList.importNode( childElement, true ); + restoredFormFieldList.appendChild( clonedNode ); + if ( formfields.isEmpty() ) continue; QHash hashedforms; QLinkedList< FormField * >::const_iterator fIt = formfields.begin(), fItEnd = formfields.end(); for ( ; fIt != fItEnd; ++fIt ) { hashedforms[(*fIt)->id()] = (*fIt); } // iterate over all forms QDomNode formsNode = childElement.firstChild(); while( formsNode.isElement() ) { // get annotation element and advance to next annot QDomElement formElement = formsNode.toElement(); formsNode = formsNode.nextSibling(); if ( formElement.tagName() != QLatin1String("form") ) continue; bool ok = true; int index = formElement.attribute( QStringLiteral("id") ).toInt( &ok ); if ( !ok ) continue; QHash::const_iterator wantedIt = hashedforms.constFind( index ); if ( wantedIt == hashedforms.constEnd() ) continue; QString value = formElement.attribute( QStringLiteral("value") ); (*wantedIt)->d_ptr->setValue( value ); + loadedAnything = true; } } } + + return loadedAnything; } void PagePrivate::saveLocalContents( QDomNode & parentNode, QDomDocument & document, PageItems what ) const { // create the page node and set the 'number' attribute QDomElement pageElement = document.createElement( QStringLiteral("page") ); pageElement.setAttribute( QStringLiteral("number"), m_number ); #if 0 // add bookmark info if is bookmarked if ( d->m_bookmarked ) { // create the pageElement's 'bookmark' child QDomElement bookmarkElement = document.createElement( "bookmark" ); pageElement.appendChild( bookmarkElement ); // add attributes to the element //bookmarkElement.setAttribute( "name", bookmark name ); } #endif // add annotations info if has got any if ( ( what & AnnotationPageItems ) && ( what & OriginalAnnotationPageItems ) ) { const QDomElement savedDocRoot = restoredLocalAnnotationList.documentElement(); if ( !savedDocRoot.isNull() ) { // Import and append node in target document const QDomNode importedNode = document.importNode( savedDocRoot, true ); pageElement.appendChild( importedNode ); } } else if ( ( what & AnnotationPageItems ) && !m_page->m_annotations.isEmpty() ) { // create the annotationList QDomElement annotListElement = document.createElement( QStringLiteral("annotationList") ); // add every annotation to the annotationList QLinkedList< Annotation * >::const_iterator aIt = m_page->m_annotations.constBegin(), aEnd = m_page->m_annotations.constEnd(); for ( ; aIt != aEnd; ++aIt ) { // get annotation const Annotation * a = *aIt; // only save okular annotations (not the embedded in file ones) if ( !(a->flags() & Annotation::External) ) { // append an filled-up element called 'annotation' to the list QDomElement annElement = document.createElement( QStringLiteral("annotation") ); AnnotationUtils::storeAnnotation( a, annElement, document ); annotListElement.appendChild( annElement ); qCDebug(OkularCoreDebug) << "save annotation:" << a->uniqueName(); } } // append the annotationList element if annotations have been set if ( annotListElement.hasChildNodes() ) pageElement.appendChild( annotListElement ); } // add forms info if has got any - if ( ( what & FormFieldPageItems ) && !formfields.isEmpty() ) + if ( ( what & FormFieldPageItems ) && ( what & OriginalFormFieldPageItems ) ) + { + const QDomElement savedDocRoot = restoredFormFieldList.documentElement(); + if ( !savedDocRoot.isNull() ) + { + // Import and append node in target document + const QDomNode importedNode = document.importNode( savedDocRoot, true ); + pageElement.appendChild( importedNode ); + } + } + else if ( ( what & FormFieldPageItems ) && !formfields.isEmpty() ) { // create the formList QDomElement formListElement = document.createElement( QStringLiteral("forms") ); // add every form data to the formList QLinkedList< FormField * >::const_iterator fIt = formfields.constBegin(), fItEnd = formfields.constEnd(); for ( ; fIt != fItEnd; ++fIt ) { // get the form field const FormField * f = *fIt; QString newvalue = f->d_ptr->value(); if ( f->d_ptr->m_default == newvalue ) continue; // append an filled-up element called 'annotation' to the list QDomElement formElement = document.createElement( QStringLiteral("form") ); formElement.setAttribute( QStringLiteral("id"), f->id() ); formElement.setAttribute( QStringLiteral("value"), newvalue ); formListElement.appendChild( formElement ); } // append the annotationList element if annotations have been set if ( formListElement.hasChildNodes() ) pageElement.appendChild( formListElement ); } // append the page element only if has children if ( pageElement.hasChildNodes() ) parentNode.appendChild( pageElement ); } const QPixmap * Page::_o_nearestPixmap( DocumentObserver *observer, int w, int h ) const { Q_UNUSED( h ) const QPixmap * pixmap = nullptr; // if a pixmap is present for given id, use it QMap< DocumentObserver*, PagePrivate::PixmapObject >::const_iterator itPixmap = d->m_pixmaps.constFind( observer ); if ( itPixmap != d->m_pixmaps.constEnd() ) pixmap = itPixmap.value().m_pixmap; // else find the closest match using pixmaps of other IDs (great optim!) else if ( !d->m_pixmaps.isEmpty() ) { int minDistance = -1; QMap< DocumentObserver*, PagePrivate::PixmapObject >::const_iterator it = d->m_pixmaps.constBegin(), end = d->m_pixmaps.constEnd(); for ( ; it != end; ++it ) { int pixWidth = (*it).m_pixmap->width(), distance = pixWidth > w ? pixWidth - w : w - pixWidth; if ( minDistance == -1 || distance < minDistance ) { pixmap = (*it).m_pixmap; minDistance = distance; } } } return pixmap; } bool Page::hasTilesManager( const DocumentObserver *observer ) const { return d->tilesManager( observer ) != nullptr; } QList Page::tilesAt( const DocumentObserver *observer, const NormalizedRect &rect ) const { TilesManager *tm = d->m_tilesManagers.value( observer ); if ( tm ) return tm->tilesAt( rect, TilesManager::PixmapTile ); else return QList(); } TilesManager *PagePrivate::tilesManager( const DocumentObserver *observer ) const { return m_tilesManagers.value( observer ); } void PagePrivate::setTilesManager( const DocumentObserver *observer, TilesManager *tm ) { TilesManager *old = m_tilesManagers.value( observer ); delete old; m_tilesManagers.insert(observer, tm); } + +void PagePrivate::adoptGeneratedContents( PagePrivate *oldPage ) +{ + rotateAt( oldPage->m_rotation ); + + m_pixmaps = oldPage->m_pixmaps; + oldPage->m_pixmaps.clear(); + + m_tilesManagers = oldPage->m_tilesManagers; + oldPage->m_tilesManagers.clear(); + + m_boundingBox = oldPage->m_boundingBox; + m_isBoundingBoxKnown = oldPage->m_isBoundingBoxKnown; + m_text = oldPage->m_text; + oldPage->m_text = nullptr; + + m_textSelections = oldPage->m_textSelections; + oldPage->m_textSelections = nullptr; + + restoredLocalAnnotationList = oldPage->restoredLocalAnnotationList; + restoredFormFieldList = oldPage->restoredFormFieldList; +} + +FormField *PagePrivate::findEquivalentForm( const Page *p, FormField *oldField ) +{ + // given how id is not very good of id (at least for pdf) we do a few passes + // same rect, type and id + foreach(FormField *f, p->d->formfields) + { + if (f->rect() == oldField->rect() && f->type() == oldField->type() && f->id() == oldField->id()) + return f; + } + // same rect and type + foreach(FormField *f, p->d->formfields) + { + if (f->rect() == oldField->rect() && f->type() == oldField->type()) + return f; + } + // fuzzy rect, same type and id + foreach(FormField *f, p->d->formfields) + { + if (f->type() == oldField->type() && f->id() == oldField->id() && qFuzzyCompare(f->rect().left, oldField->rect().left) && qFuzzyCompare(f->rect().top, oldField->rect().top) && qFuzzyCompare(f->rect().right, oldField->rect().right) && qFuzzyCompare(f->rect().bottom, oldField->rect().bottom)) + { + return f; + } + } + // fuzzy rect and same type + foreach(FormField *f, p->d->formfields) + { + if (f->type() == oldField->type() && qFuzzyCompare(f->rect().left, oldField->rect().left) && qFuzzyCompare(f->rect().top, oldField->rect().top) && qFuzzyCompare(f->rect().right, oldField->rect().right) && qFuzzyCompare(f->rect().bottom, oldField->rect().bottom)) + { + return f; + } + } + return nullptr; +} diff --git a/core/page.h b/core/page.h index 70f89ea03..7324b2be4 100644 --- a/core/page.h +++ b/core/page.h @@ -1,414 +1,423 @@ /*************************************************************************** * Copyright (C) 2004 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. * ***************************************************************************/ #ifndef _OKULAR_PAGE_H_ #define _OKULAR_PAGE_H_ #include #include "okularcore_export.h" #include "area.h" #include "global.h" #include "textpage.h" class QPixmap; class PagePainter; namespace Okular { class Annotation; class Document; class DocumentObserver; class DocumentPrivate; class FormField; class PagePrivate; class PageTransition; class SourceReference; class TextSelection; class Tile; /** * @short Collector for all the data belonging to a page. * * The Page class contains pixmaps (referenced using observers id as key), * a search page (a class used internally for retrieving text), rect classes * (that describe links or other active areas in the current page) and more. * * All coordinates are normalized to the page, so {x,y} are valid in [0,1] * range as long as NormalizedRect components. * * Note: The class takes ownership of all objects. */ class OKULARCORE_EXPORT Page { public: /** * An action to be executed when particular events happen. */ enum PageAction { Opening, ///< An action to be executed when the page is "opened". Closing ///< An action to be executed when the page is "closed". }; /** * Creates a new page. * * @param number The number of the page in the document. * @param width The width of the page. * @param height The height of the page. * @param orientation The orientation of the page */ Page( uint number, double width, double height, Rotation orientation ); /** * Destroys the page. */ ~Page(); /** * Returns the number of the page in the document. */ int number() const; /** * Returns the orientation of the page as defined by the document. */ Rotation orientation() const; /** * Returns the rotation of the page as defined by the user. */ Rotation rotation() const; /** * Returns the total orientation which is the original orientation plus * the user defined rotation. */ Rotation totalOrientation() const; /** * Returns the width of the page. */ double width() const; /** * Returns the height of the page. */ double height() const; /** * Returns the ration (height / width) of the page. */ double ratio() const; /** * Returns the bounding box of the page content in normalized [0,1] coordinates, * in terms of the upright orientation (Rotation0). * If it has not been computed yet, returns the full page (i.e., (0, 0, 1, 1)). * Note that the bounding box may be null if the page is blank. * * @since 0.7 (KDE 4.1) */ NormalizedRect boundingBox() const; /** * Returns whether the bounding box of the page has been computed. * Note that even if the bounding box is computed, it may be null if the page is blank. * * @since 0.7 (KDE 4.1) */ bool isBoundingBoxKnown() const; /** * Sets the bounding box of the page content in normalized [0,1] coordinates, * in terms of the upright orientation (Rotation0). * (This does not inform the document's observers, call Document::SetPageBoundingBox * instead if you want that.) * * @since 0.7 (KDE 4.1) */ void setBoundingBox( const NormalizedRect& bbox ); /** * Returns whether the page of size @p width x @p height has a @p pixmap * in the region given by @p rect for the given @p observer */ bool hasPixmap( DocumentObserver *observer, int width = -1, int height = -1, const NormalizedRect &rect = NormalizedRect() ) const; /** * Returns whether the page provides a text page (@ref TextPage). */ bool hasTextPage() const; /** * Returns whether the page has an object rect which includes the point (@p x, @p y) * at scale (@p xScale, @p yScale). */ bool hasObjectRect( double x, double y, double xScale, double yScale ) const; /** * Returns whether the page provides highlighting for the observer with the * given @p id. */ bool hasHighlights( int id = -1 ) const; /** * Returns whether the page provides a transition effect. */ bool hasTransition() const; /** * Returns whether the page provides annotations. */ bool hasAnnotations() const; /** * Returns the bounding rect of the text which matches the following criteria * or 0 if the search is not successful. * * @param id An unique id for this search. * @param text The search text. * @param direction The direction of the search (@ref SearchDirection) * @param caseSensitivity If Qt::CaseSensitive, the search is case sensitive; otherwise * the search is case insensitive. * @param lastRect If 0 (default) the search starts at the beginning of the page, otherwise * right/below the coordinates of the given rect. */ RegularAreaRect* findText( int id, const QString & text, SearchDirection direction, Qt::CaseSensitivity caseSensitivity, const RegularAreaRect * lastRect=nullptr) const; /** * Returns the page text (or part of it). * @see TextPage::text() */ QString text( const RegularAreaRect * rect = nullptr ) const; /** * Returns the page text (or part of it). * @see TextPage::text() * @since 0.10 (KDE 4.4) */ QString text( const RegularAreaRect * rect, TextPage::TextAreaInclusionBehaviour b ) const; /** * Returns the page text (or part of it) including the bounding * rectangles. Note that ownership of the contents of the returned * list belongs to the caller. * @see TextPage::words() * @since 0.14 (KDE 4.8) */ TextEntity::List words( const RegularAreaRect * rect, TextPage::TextAreaInclusionBehaviour b ) const; /** * Returns the area and text of the word at the given point * Note that ownership of the returned area belongs to the caller. * @see TextPage::wordAt() * @since 0.15 (KDE 4.9) */ RegularAreaRect * wordAt( const NormalizedPoint &p, QString *word = nullptr ) const; /** * Returns the rectangular area of the given @p selection. */ RegularAreaRect * textArea( TextSelection *selection ) const; /** * Returns the object rect of the given @p type which is at point (@p x, @p y) at scale (@p xScale, @p yScale). */ const ObjectRect * objectRect( ObjectRect::ObjectType type, double x, double y, double xScale, double yScale ) const; /** * Returns all object rects of the given @p type which are at point (@p x, @p y) at scale (@p xScale, @p yScale). * @since 0.16 (KDE 4.10) */ QLinkedList< const ObjectRect * > objectRects( ObjectRect::ObjectType type, double x, double y, double xScale, double yScale ) const; /** * Returns the object rect of the given @p type which is nearest to the point (@p x, @p y) at scale (@p xScale, @p yScale). * * @since 0.8.2 (KDE 4.2.2) */ const ObjectRect * nearestObjectRect( ObjectRect::ObjectType type, double x, double y, double xScale, double yScale, double * distance ) const; /** * Returns the transition effect of the page or 0 if no transition * effect is set (see hasTransition()). */ const PageTransition * transition() const; /** * Returns the list of annotations of the page. */ QLinkedList< Annotation* > annotations() const; + /** + * Returns the annotation with the given unique name. + * @since 1.3 + */ + Annotation * annotation( const QString & uniqueName ) const; + /** * Returns the @ref Action object which is associated with the given page @p action * or 0 if no page action is set. */ const Action * pageAction( PageAction action ) const; /** * Returns the list of FormField of the page. */ QLinkedList< FormField * > formFields() const; /** * Sets the region described by @p rect with @p pixmap for the * given @p observer. * If @p rect is not set (default) the @p pixmap is set to the entire * page. */ void setPixmap( DocumentObserver *observer, QPixmap *pixmap, const NormalizedRect &rect = NormalizedRect() ); /** * Sets the @p text page. */ void setTextPage( TextPage * text ); /** * Sets the list of object @p rects of the page. */ void setObjectRects( const QLinkedList< ObjectRect * > & rects ); /** * Sets the list of source reference objects @p rects. */ void setSourceReferences( const QLinkedList< SourceRefObjectRect * > & rects ); /** * Sets the duration of the page to @p seconds when displayed in presentation mode. * * Setting a negative number disables the duration. */ void setDuration( double seconds ); /** * Returns the duration in seconds of the page when displayed in presentation mode. * * A negative number means that no time is set. */ double duration() const; /** * Sets the labels for the page to @p label . */ void setLabel( const QString& label ); /** * Returns the label of the page, or a null string if not set. */ QString label() const; /** * Returns the current text selection. */ const RegularAreaRect * textSelection() const; /** * Returns the color of the current text selection, or an invalid color * if no text selection has been set. */ QColor textSelectionColor() const; /** * Adds a new @p annotation to the page. */ void addAnnotation( Annotation * annotation ); /** * Removes the @p annotation from the page. */ bool removeAnnotation( Annotation * annotation ); /** * Sets the page @p transition effect. */ void setTransition( PageTransition * transition ); /** * Sets the @p link object for the given page @p action. */ void setPageAction( PageAction action, Action * link ); /** * Sets @p fields as list of FormField of the page. */ void setFormFields( const QLinkedList< FormField * >& fields ); /** * Deletes the pixmap for the given @p observer */ void deletePixmap( DocumentObserver *observer ); /** * Deletes all pixmaps of the page. */ void deletePixmaps(); /** * Deletes all object rects of the page. */ void deleteRects(); /** * Deletes all source reference objects of the page. */ void deleteSourceReferences(); /** * Deletes all annotations of the page. */ void deleteAnnotations(); /** * Returns whether pixmaps for the tiled observer are handled by a * tile manager. * * @since 0.19 (KDE 4.13) */ bool hasTilesManager( const DocumentObserver *observer ) const; /** * Returns a list of all tiles intersecting with @p rect. * * The list contains only tiles with a pixmap * * @since 0.19 (KDE 4.13) */ QList tilesAt( const DocumentObserver *observer, const NormalizedRect &rect ) const; private: - PagePrivate* const d; + PagePrivate* d; /// @cond PRIVATE friend class PagePrivate; friend class Document; friend class DocumentPrivate; friend class PixmapRequestPrivate; /** * To improve performance PagePainter accesses the following * member variables directly. */ friend class ::PagePainter; /// @endcond const QPixmap * _o_nearestPixmap( DocumentObserver *, int, int ) const; QLinkedList< ObjectRect* > m_rects; QLinkedList< HighlightAreaRect* > m_highlights; QLinkedList< Annotation* > m_annotations; Q_DISABLE_COPY( Page ) }; } #endif diff --git a/core/page_p.h b/core/page_p.h index 46daf9641..4b7831c4c 100644 --- a/core/page_p.h +++ b/core/page_p.h @@ -1,153 +1,172 @@ /*************************************************************************** * Copyright (C) 2004 by Enrico Ros * * 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_PAGE_PRIVATE_H_ #define _OKULAR_PAGE_PRIVATE_H_ // qt/kde includes #include #include #include #include #include // local includes #include "global.h" #include "area.h" class QColor; namespace Okular { class Action; class Annotation; class DocumentObserver; class DocumentPrivate; class FormField; class HighlightAreaRect; class Page; class PageSize; class PageTransition; class RotationJob; class TextPage; class TilesManager; enum PageItem { None = 0, AnnotationPageItems = 0x01, FormFieldPageItems = 0x02, AllPageItems = 0xff, /* If set along with AnnotationPageItems, tells saveLocalContents to save * the original annotations (if any) instead of the modified ones */ - OriginalAnnotationPageItems = 0x100 + OriginalAnnotationPageItems = 0x100, + + /* If set along with FormFieldPageItems, tells saveLocalContents to save + * the original form contents (if any) instead of the modified one */ + OriginalFormFieldPageItems = 0x200 }; Q_DECLARE_FLAGS(PageItems, PageItem) class PagePrivate { public: PagePrivate( Page *page, uint n, double w, double h, Rotation o ); ~PagePrivate(); static PagePrivate *get( Page *page ); void imageRotationDone( RotationJob * job ); QTransform rotationMatrix() const; /** * Loads the local contents (e.g. annotations) of the page. */ - void restoreLocalContents( const QDomNode & pageNode ); + bool restoreLocalContents( const QDomNode & pageNode ); /** * Saves the local contents (e.g. annotations) of the page. */ void saveLocalContents( QDomNode & parentNode, QDomDocument & document, PageItems what = AllPageItems ) const; /** * Rotates the image and object rects of the page to the given @p orientation. */ void rotateAt( Rotation orientation ); /** * Changes the size of the page to the given @p size. * * The @p size is meant to be referred to the page not rotated. */ void changeSize( const PageSize &size ); /** * Sets the @p color and @p areas of text selections. */ void setTextSelections( RegularAreaRect *areas, const QColor & color ); /** * Sets the @p color and @p area of the highlight for the observer with * the given @p id. */ void setHighlight( int id, RegularAreaRect *area, const QColor & color ); /** * Deletes all highlight objects for the observer with the given @p id. */ void deleteHighlights( int id = -1 ); /** * Deletes all text selection objects of the page. */ void deleteTextSelections(); /** * Get the tiles manager for the tiled @observer */ TilesManager *tilesManager( const DocumentObserver *observer ) const; /** * Set the tiles manager for the tiled @observer */ void setTilesManager( const DocumentObserver *observer, TilesManager *tm ); + /** + * Moves contents that are generated from oldPage to this. And clears them from page + * so it can be deleted fine. + */ + void adoptGeneratedContents( PagePrivate *oldPage ); + + /* + * Tries to find an equivalent form field to oldField by looking into the rect, type and name + */ + OKULARCORE_EXPORT static FormField *findEquivalentForm( const Page *p, FormField *oldField ); + class PixmapObject { public: QPixmap *m_pixmap; Rotation m_rotation; }; QMap< DocumentObserver*, PixmapObject > m_pixmaps; QMap< const DocumentObserver*, TilesManager *> m_tilesManagers; Page *m_page; int m_number; Rotation m_orientation; double m_width, m_height; DocumentPrivate *m_doc; NormalizedRect m_boundingBox; Rotation m_rotation; TextPage * m_text; PageTransition * m_transition; HighlightAreaRect *m_textSelections; QLinkedList< FormField * > formfields; Action * m_openingAction; Action * m_closingAction; double m_duration; QString m_label; bool m_isBoundingBoxKnown : 1; QDomDocument restoredLocalAnnotationList; // ... + QDomDocument restoredFormFieldList; // ... }; } Q_DECLARE_OPERATORS_FOR_FLAGS(Okular::PageItems) #endif diff --git a/doc/index.docbook b/doc/index.docbook index 3053c5821..354733cf2 100644 --- a/doc/index.docbook +++ b/doc/index.docbook @@ -1,2387 +1,2373 @@ ATEX"> KPDF"> ]> The &okular; Handbook Albert Astals Cid
&Albert.Astals.Cid.mail;
Pino Toscano
pino@kde.org
&FDLNotice; 2017-11-06 1.3 (Applications 17.12) &okular; is a &kde; universal document viewer based on &kpdf; code. KDE okular pdf ps postscript tiff djvu dvi chm xps comicbook fictionbook markdown mobipocket plucker annotation
Introduction &okular; is a &kde; universal document viewer based on the code of the &kpdf; application. Although being based on &kpdf; code, &okular; has some unique features such as overview mode, improved presentation support and annotation support. &okular; supports a lot of different formats like &PDF;, &PostScript;, Tiff, CHM, DjVU, Images (png, jpg, &etc;) XPS, Open Document (ODT), Fiction Books, Comic Book, Plucker, EPub, Fax and Markdown. For all supported formats and their features see &okular; Document Format Handlers. &okular;s Main Window &okular;s Main Window Basic Usage Opening Files To view any supported file in &okular;, select FileOpen... , choose a supported file in the dialog and click Open. Your file should now be displayed in the main window. The new document will be opened in a new tab should the Open new files in tabs option on the General configuration page is checked. If you have already opened files in &okular; before, you can quickly access them by selecting them in the FileOpen Recent menu. &okular; is the default &kde; application for &PDF; and &PostScript; files, launched when you click with the &LMB; on such a file type in the filemanager. If you want to open any file whose format is supported by &okular; use Open with...&okular; from context menu in the filemanager. After having a file opened you probably want to read it and therefore navigate through it. Go to the next section to learn more about this. Navigating This section describes how you can navigate through a document in &okular;. There are multiple ways of scrolling the viewing area. One is to use the Up Arrow and Down Arrow keys. You may also use the scrollbar, your mousewheel or the Page Up and Page Down keys. You can also use vim-like navigation keys, namely H to move to the top of the previous page, L to move to the top of the next page, J to move one line down, and K to move one line up. Another way is to hold the &LMB; down at any place on the document while dragging the mouse in the opposite direction of where you want to move. This procedure only works if the Browse Tool is enabled, which you can select by choosing Tools Browse Tool. When viewing a document in the Presentation mode use Up Arrow and Down Arrow keys to switch between pages or slides. The number and the position of the current slide will be shown in the overlay at the right upper corner of screen. If you want to read a document with several pages use the automatic scrolling feature of &okular;. Start automatic scrolling with &Shift;Down Arrow or &Shift;Up Arrow. Then use these keys to increase and decrease the scrolling speed. You can start or stop automatic scrolling temporarily by pressing the &Shift; key; pressing any other key deactivates this feature. Another way to navigate through a document with several pages is to use the mouse pointer. Drag the page up or down, continue to drag even while reaching the bottom or top of the screen and behold. Once you cross the border of a page, the mouse cursor appears on top or bottom of the screen again and you can just continue to drag. The navigation panel on the left side of the screen enables two more ways of navigating through a document: If you click on a page thumbnail the viewing area will be brought to that page. If the document has a table of contents, clicking on a table of contents item will bring the document to the page linked to that item. If the document has layers, you can control the layers shown by checking or unchecking the items in the Layers list. If the document has bookmarks, enable the Bookmarks view and click them to go to the associated page. If bookmarks are not only shown for the current document, you can quickly switch to bookmarks in all recently opened files. If the document has annotations, enable the Reviews view and click the annotations or select them with the Up Arrow and Down Arrow keys and press Return to go to the associated page. Some documents have links. In this case you can click on them and the view will change to the page it links to. If the link is to a web page or some other document the default &kde; handler for that format will be invoked. For example, clicking on a link pointing to http://www.kde.org will open the web page in the default &kde;'s web browser. The document internal links work only when Browse Tool is used. Additionally, you may use the following functionality to quickly move to specific places in the document: You can go to the beginning of the document using &Ctrl;Home or using Go Beginning of the document . You can go to the end of the document using &Ctrl;End or using Go End of the document . You can go forward in the document using Space or Page Down. To go to the next page of the document use the Next Page Toolbar button or Go Next Page in the menubar. You can go back in the document using &Backspace; or Page Up. To go to the previous page of the document use Previous Page Toolbar button or Go Previous Page in the menubar. You can go back to the positions in the document where you came from in a chronological order. Consider ⪚ reading the phrase As shown in [15], …, and you want to know quickly lookup reference [15]. So you click on it, and &okular; will jump to the list of references. Using &Alt;&Shift;Left or Go Back in the menubar will bring you back to exactly the position where you came from. You can go forward in the document after the jumping back as described above using &Alt;&Shift;Right or Go Forward in the menubar. You can go to the next match when searching using F3 or &Enter; (when the focus is on Find text field) keys or Edit Find Next menu item or move back to the previous match using &Shift;F3 or &Shift;&Enter; (when the focus is on Find text field) keys or Edit Find Previous menu item. Presentation Mode The Presentation mode represents another way to view documents in &okular;. It can be enabled in ViewPresentation. It shows the document on a page per page basis. The pages are shown with zoom to page, that means all the page is visible. &PDF; documents can even specify that they are always opened in presentation mode. When in presentation mode, you have an helper bar located on the top of the screen. Just move the mouse cursor to the top of the screen to make it appear. &okular; in Presentation Mode &okular; in Presentation Mode To navigate between pages you may use the &LMB; (next page) and the &RMB; (previous page), the mouse wheel, the arrow icons or the line edit in the top bar, or the keys specified in the Navigating section. Use Play/Pause button in the top bar to start playing presentation or pause it, correspondingly. You can exit presentation mode at any time by pressing the &Esc; key or clicking the Quit icon in the top bar. You can also draw on the current page with a pencil. Click on the Toggle Drawing Mode icon in the top bar to enable or disable the possibility to draw in the presentation mode. The drawings are cleared automatically when leaving the presentation mode. You can also click on the Erase Drawings icon to remove the drawings in the current page. The presentation mode has support for more than one screen in a multi-monitor configuration. With more than one screen a new button will appear in the top bar, with the icon of a screen: this is a drop down box that allows you to move the presentation to any of the other available screens. Presentation mode has some configuration options, you can find their description in the chapter Configuring &okular;. Inverse Search between &latex; Editors and &okular; Inverse search is a very useful feature when you are writing a &latex; document yourself. If everything is set up properly, you can click into &okular;'s window with the left mouse button while pressing &Shift;. After that editor loads the &latex; source file and jumps to the proper paragraph. Inverse search cannot work unless: The source file has been compiled successfully. &okular; knows which editor you would like to use. The Browse Tool has to be enabled, which you can select by choosing ToolsBrowse Tool . With this feature of &okular;, a left mouse click while pressing &Shift; in the &DVI; or &PDF; document will result in editor opening the corresponding &latex; document and attempt to go to the corresponding line. Remember to tell &okular; to use proper editor, in &okular;'s menu item SettingsConfigure Okular... (on the page Editor). For more details on editor configuration please refer to the corresponding section of this manual. Configuring &okular; Configuring editor in &okular; Configuring editor in &okular; &okular; Advanced Features Embedded Files If the current document has some files embedded in it, when you open it a yellow bar will appear above the page view to notify you about the embedded files. The embedded files bar The embedded files bar In this case, you can either click on the link in the text of the bar or choose FileEmbedded Files to open the embedded files dialog. The dialog allows you to view the embedded files and to extract them. Forms If the current document has forms, when you open it a bar will appear above the page view where you can enable the forms. The forms bar The forms bar In this case, you can either click on Show Forms in the bar or choose ViewShow Forms to enter data into the form fields. Annotations &okular; allows you to review and annotate your documents. - Annotations created in &okular; are automatically saved in the internal local data folder - for each user. - &okular; does not implicitly change any document it opens. &okular;'s Annotations &okular;'s Annotations &okular; has two different kind of annotations: Text annotations like Yellow Highlighter and Black Underlining for files with text like ⪚ &PDF;. Graphic annotations like Pop-up Note, Inline Note, Freehand Line, Highlighter, Straight Line, Polygon, Stamp, Underline, and Ellipse for all formats supported by &okular;. Using the context menu either in the Reviews view of the navigation panel or in the main window you can open a Pop up Note for any kind of annotation and add or edit comments. Annotations are not only limited to &PDF; files, they can be used for any format &okular; supports. - Since &kde; 4.2, &okular; has the "document archiving" feature. This is an &okular;-specific format for carrying the document plus various metadata related to it (currently only annotations). You can save a "document archive" from the open document by choosing FileExport AsDocument Archive. To open an &okular; document archive, just open it with &okular; as it would be ⪚ a &PDF; document. + &okular; has the "document archiving" feature. This is an &okular;-specific format for carrying the document plus various metadata related to it (currently only annotations). You can save a "document archive" from the open document by choosing FileSave As and selecting Okular document archive in the Filter selector. To open an &okular; document archive, just open it with &okular; as it would be ⪚ a &PDF; document. - Since &okular; 0.15 you can also save annotations directly into &PDF; files. This feature is only available if &okular; has been built with version 0.20 or later of Poppler rendering library. You can use File Save As... to save the copy of &PDF; file with annotations. + You can also save annotations directly into &PDF; files. You can use File Save to save it over the current file or File Save As... to save it to a new file. - - - It is not possible to save annotations into &PDF; file if original file was encrypted and &okular; uses Poppler libraries of version which is lower than 0.22. - - - - - If you open a &PDF; with existing annotations, your annotation changes are not automatically saved in the internal local data folder, and you need to save the modified document (using FileSave As...) before closing it. Should you forget to do this &okular; will show confirmation window that allows you to save the document. - - - Due to DRM limitations (typically with &PDF; documents), adding, editing some properties or removing annotations could not be possible. Any action on annotations (creation and removal of annotations, editing arbitrary annotation properties, relocating annotations with &Ctrl;+drag, and editing the text contents of an annotation) can be undone or redone using the corresponding item from the Edit menu. It is also possible to undo the action by pressing &Ctrl;Z and redo the undone action by pressing &Ctrl;&Shift;Z. - Since &okular; 0.17 you can configure the default properties and appearance of each annotating tool. Please refer to the corresponding section in this documentation. + You can configure the default properties and appearance of each annotating tool. Please refer to the corresponding section in this documentation. Adding annotations To add some annotations to the document, you have to activate the annotating toolbar. This is done by either selecting ToolsReview or pressing F6. Once the annotating toolbar is shown, just press one of its buttons or use keyboard shortcuts (keys from 1 to 9) to start constructing that annotation. The annotating toolbar helps you to make annotations with drawings, shapes, and text messages. You can use the annotating toolbar to mark up a document (⪚ add lines, ellipses, polygons, stamps, highlights, underlines &etc;). The table below describes exactly what the default set of annotating toolbar buttons does. Button Tool Name Description Pop-up Note To draw multiline note. The note will can be viewed by double clicking on an icon in the document. Click on the tool button, then click on the place in the document where the pop-up note should be added. Enter the text of pop-up note then click on the Close this note button in the top right corner of the pop-up window. Inline Note To draw inline note. The note will be shown inline as is. Click on the tool button, then click with the &LMB; and hold to place the top-left corner of the note, then drag to place the bottom-right one. Enter the text of the note then click on the OK to save the note, Cancel to cancel note entering or Clear to clear the note. Freehand Line To draw free-form lines. Click on the tool button, then click with the &LMB; and hold to place the start of the line, then drag to draw the line. Highlighter To highlight text in the document with some given background color. Click on the tool button, then click with the &LMB; and hold to place the beginning of the highlighted text snippet, then drag to highlight it. Straight Line To mark with a line. Click on the tool button, then click with the &LMB; to place the starting point of the line, then drag to place of the ending point of the line should be and click once more. Polygon To draw a closed plane figure from three or more segments. The corresponding note can be viewed by double clicking inside the polygon. Click on the tool button, then click with the &LMB; to place the first vertex of the polygon, then drag to place of the second vertex. Proceed until you draw the whole polygon up to the first vertex. Click twice if you want to add some note to the polygon. Enter the text of the note then click on the OK to save the note, Cancel to cancel note entering or Clear to clear the note. Stamp To mark the text or image with some predefined shape. Click on the tool button then click with the &LMB; to place the stamp. A single click just places a square stamp (useful for icons). To add a rectangular stamp you can click with the &LMB; and hold to place the top-left point, then drag to place the bottom-right one. Underline To underline some text. Click on the tool button, then click with the &LMB; and hold to place the beginning of the underlined text snippet, then drag to underline it. Ellipse To draw an ellipse around some chosen area. Click on the tool button, then click with the &LMB; and hold to place the top-left corner of the circumscribed rectangular for the ellipse, then drag to place the bottom-right one. The contents of the annotating toolbar can be configured using the Annotations page of &okular; configuration dialog. This page can be opened with &RMB; clicking on the annotating toolbar then choosing Configure Annotations... from the context menu. With a single &LMB; click on an annotation tool button you can use a tool once. If you ⪚ want to highlight all important parts of a text, activate that tool permanently by double clicking on the tool button. Press the Esc key or click the tool button again to leave the permanent mode. The annotating toolbar can be docked in any side of the viewing area: just drag it to move it to another place. Activating the annotating toolbar will make you switch to the Browse Tool Mode. You can stop the construction any time by pressing again on the button of the annotation you are constructing, or by pressing the &Esc; key. The newly constructed annotation will have as author the author you set in the Annotations page in &okular;s configuration dialog. The Annotations page can also be used to configure the content of the annotating toolbar. Removing annotations To remove an annotation, just click on it with the &RMB;, and select Delete or select an annotation using the &LMB; and press the ∇ key. When removing the annotation, its window will be closed if open. This option could not be enabled because the document does not allow removing annotations. Editing annotations To edit an annotation, click on it with the &RMB; and select Properties. A dialog will appear with the general annotation settings (like color and opacity, author, &etc;) and the settings specific to that annotation type. Annotation Property Dialog Annotation Property Dialog To move an annotation, hold down the &Ctrl; key, move the mouse pointer on it and then start dragging with the &LMB;. If you click with the &LMB; on an annotation, it gets selected. Resize handles appear on the selection rectangle. When the cursor is moved over one of the 8 resize handles on the corners or edges, the cursor shape changes to indicate the resize mode. Click anywhere else on the annotation to move it. Pressing &Esc; or clicking an area outside the annotation cancels a selection. Resizing is only applicable for annotation types Inline Note, Stamp and Ellipse. Depending on the document permissions (typically with &PDF; documents), some options can be disabled. Bookmark Management &okular; has a very flexible bookmark system. &okular; saves the position on the page in bookmark and allows you to define more than one bookmark per page. To manage bookmarks in &okular; you can use Bookmarks view from Navigation Panel, Bookmarks menu or context menu of document view (click with &RMB; to open it). Bookmarks view To open Bookmarks view click on Bookmarks item on the Navigation Panel. If the Navigation Panel is not shown, use F7 SettingsShow Navigation Panel main menu item to make it visible. Bookmark view context menu Bookmark view context menu The filter bar at the top of Bookmarks view can be used to filter the content of bookmark list pane according to the text in the box. The list pane permits to view the bookmark list in a tree-like fashion: each document in the list can be expanded or collapsed by clicking on the < or v icon next to it. Click on icon below the list to show only the bookmarks from the current document. Right-click menu of document item can be used to open document, rename its item or remove it from the list. Remember that the removal of a document item leads to the removal of all bookmarks in the corresponding document. Right-click menus of individual bookmark items allow you to go to the bookmark, rename or remove it. The Right-click menu of items in the table of contents can be used to create bookmarks or go to the next or previous bookmark. Command Line Options Though &okular; may most often be started from the &kde; program menu, or a desktop icon, it can also be opened at the command line prompt of a terminal window. There are a few useful options that are available when doing this. Specify a File By specifying the path and name of a particular file the user can have &okular; open that file immediately upon startup. This option might look something like the following: % okular For &PDF; documents, the name can be given as document_name#named_destination where named_destination is a particular named destination embedded in the document. Other Command Line Options The following command line help options are available okular Open a page with a given number in the document. Also available through okular okular Start the document in presentation mode. okular Start with print dialog. okular Unique instance control. okular Allows to prevent &okular; window raising after the start. Fit window to page The Fit window to page feature resizes the window so that it is exactly the same size as the page at the current zoom factor. If the page doesn't completely fit on the screen, the window is enlarged so that the largest possible part of the page is shown. This feature can be accessed by using the keyboard shortcut &Ctrl;J The Menubar The File Menu &Ctrl;O File Open... - Open a supported file or &okular; archive. If there is already an opened file it will be closed. For more information, see the section about Opening Files. + Open a supported file or &okular; document archive. If there is already an opened file it will be closed. For more information, see the section about Opening Files. File Open Recent Open a file which was used previously from a submenu. If a file is currently being displayed it will be closed. For more information, see the section about Opening Files. File Import PostScript as &PDF;... Open a &PostScript; file and convert it to &PDF;. &Ctrl;S File - Save As... + Save - Saves the document under a new name including all the changes (annotations, form contents, &etc;), provided the document backend supports saving changes. With the &PDF; backend it is possible to save the document with the changed values of the form fields. It can be possible (provided that the data were not secured using DRM) to save annotations with &PDF; files. - - - Note that, due to the way this is implemented, even if there are no changes to the file, the new file need not to be an exact bit-for-bit copy of the original file (⪚ can have a different SHA-1 hash, &etc;). - - + Saves the document including all the changes (annotations, form contents, &etc;), provided the document backend supports saving those changes. If the backend does not support saving the changes, the user will be given the option to either discard or to save them together with the document in an &okular; document archive. - + &Ctrl;&Shift;S File - Save Copy As... + Save As... - Saves a copy of the original document under a new name (completely bypassing the document backend). The saved document will be a bit-for-bit copy of the original. + Saves the document under a new name including all the changes (annotations, form contents, &etc;), provided the document backend supports saving changes. If the backend does not support saving the changes, the user will be given the option to either discard or to save them together with the document in an &okular; document archive. + + + Note that, due to the way this is implemented, even if there are no changes to the file, the new file need not to be an exact bit-for-bit copy of the original file (⪚ can have a different SHA-1 hash, &etc;). + + F5 File Reload Reload the currently open file. &Ctrl;P File Print... Print the currently displayed document. File Print Preview... Show a preview of how the currently displayed document would be printed with the default options. File Properties Display some basic information about the document, such as title, author, creation date, and details about the fonts used. The available information depends on the type of document. File Embedded Files... Show the files embedded in the document, if the document has any. For more information, see the section about the Embedded Files. File Export As This item contains the export formats the current document can be exported to. The first entry for all kind of documents is always Plain Text... The second entry is Document Archive, which allows you to save the document with your annotations into an &okular;-specific archive format. Thus it is easily possible to share the original document and your annotations with other &okular; users or work with them collaboratively. File Share This item contains the services which can be used to share the current document with other people or send it to your devices. The entries in this sub-menu are controlled by the system. They depend on the current document format and the installed KPurpose plugins. &Ctrl;Q File Quit Close &okular;. The Edit Menu &Ctrl;Z Edit Undo Undo the last annotation editing command (creation and removal of annotations, editing arbitrary annotation properties, relocating annotations with &Ctrl;+drag, and editing the text contents of an annotation). &Ctrl;&Shift;Z Edit Redo Redo the last undo step when editing annotations. &Ctrl;C Edit Copy Copy the currently selected text in Text Selection mode to the clipboard. &Ctrl;A Edit Select All Selects all the text (if the document provides it). This works only in Text Selection mode. &Ctrl;F Edit Find... Show the find bar on the bottom of the viewing area that allows you to search for a string in the document. F3 Edit Find Next Try to find the previous searched string again in the document. &Shift;F3 Edit Find Previous Goes to the previous occurrence of the search string in the document. The View Menu &Ctrl;&Shift;P View Presentation Activates the Presentation Mode. For more information, see the section about Presentation Mode. &Ctrl;+ View Zoom In Increase the magnification of the document view. &Ctrl;- View Zoom Out Decrease the magnification of the document view. View Fit Width Change the magnification of the document view to a value that makes the pages' width equal to the document view's width. View Fit Page Change the magnification of the document view to a value that makes at least one whole page visible. View Auto Fit Change the magnification of the document view to a value that, depending on the size relation between the page and the view area, automatically either makes the pages' width equal to the document view's width (like fit-width), the pages' height equal to the document view's height (like fit-height), or the whole page visible (like fit-page). View Continuous Enable the continuous page mode. In continuous mode, all pages of the document are shown, and you can scroll through them without having to use the Go Previous Page and GoNext Page options. View View Mode This submenu makes you choose the view mode for the pages. The possible options are: Single Page (only one page per row), Facing Pages (two pages per row, in a book style), Facing Pages (Center First Page) and Overview (the number of columns is the one specified in the &okular; settings). View Orientation This submenu allows you to changes the orientation of the pages of the document. The rotation is applied to the orientation of every page. You can select Original Orientation to restore the orientation of the document, discarding all the rotations applied manually. View Page Size Changes the size of the pages of the document. This submenu is active only if the current type of document supports different page sizes. View Trim View This submenu allows you to remove the white border of pages when viewing pages (Trim Margins item) or trim viewport to selection (Trim To Selection item). The trim to selection mode allows you to draw a selection over the rendered page in order to define a visible bounding box to be applied to all pages in the document. Selecting a small trim bounding box enforces minimum dimensions size (20% as a percentage of total page size). The trim margins mode is persistent across &okular; restarts. The trim to selection mode is forgotten across &okular; restarts. When you switch &okular; from trim margins mode to trim to selection mode, the view jumps out of trim margins mode for the bounding box selection interaction. The trim mode can be deactivated by selecting the same menu item that activates it once again. View Show/Hide Forms Show or hides the display of the form fields of the document. This menu item is active only if the current document has form fields. The Go Menu Go Previous Page View the previous page of the document. Go Next Page View the next page of the document. &Ctrl;Home Go Beginning of the document Go to the beginning of the document. &Ctrl;End Go End of the document Go to the end of the document. &Alt;&Shift;Left Go Back Go back to the previous view of the document. &Alt;&Shift;Right Go Forward Move forward to the next view of the document. This only works if you have already moved back before. &Ctrl;G Go Go to Page... Open a dialog which allows you to go to any page of the document. The Bookmarks Menu &Ctrl;B Bookmarks Add/Remove Bookmark Add or remove a bookmark for the current position. Bookmarks Rename Bookmark Rename a bookmark for the current position. Bookmarks Previous Bookmark Go to the previous bookmark, or do nothing if there are no bookmarks prior to the current one. Bookmarks Next Bookmark Go to the next bookmark, or do nothing if there are no bookmarks after the current one. Bookmarks No Bookmarks This is an always disabled action that appears in this menu only if the current document has no bookmarks. Otherwise a list of all bookmarks is displayed here. Clicking on these bookmarks allows you to go directly to the associated position. The Tools Menu &Ctrl;1 Tools Browse Tool The mouse will have its normal behavior, &LMB; for dragging the document and following links and &RMB; for adding bookmarks and fit to width. &Ctrl;2 Tools Zoom Tool The mouse will work as a zoom tool. Clicking &LMB; and dragging will zoom the view to the selected area, clicking &RMB; will bring the document back to the previous zoom. &Ctrl;3 Tools Selection Tool The mouse will work as a select tool. In that mode clicking &LMB; and dragging will give the option of copying the text/image of current selected area to the clipboard, speak a text or to save an image to a file. &Ctrl;4 Tools Text Selection Tool The mouse will work as a text selection tool. In that mode clicking &LMB; and dragging will give the option of selecting the text of the document. Then, just click with the &RMB; to copy to the clipboard or speak the current selection. &Ctrl;5 Tools Table Selection Tool Draw a rectangle around the text for the table, then use the click with the &LMB; to divide the text block into rows and columns. A &LMB; click on a existing line removes it and merges the adjacent rows or columns. &Ctrl;6 Tools Magnifier Activates the magnifier mode for the mouse pointer. Press and hold the &LMB; to activate magnifier widget, move the pointer for panning through the document. The magnifier scales each pixel in the document into 10 pixels in the magnifier widget. F6 Tools Review Open the review toolbar. The review toolbar allows you to add annotations on the document you are reading. For more information, please see the section about Annotations. Tools Speak Whole Document Tools Speak Current Page Tools Stop Speaking These items allow you to speak the whole document or just the current page and stop speaking using the &kde; Text-to-Speech system &jovie;. The Speak ... actions are enabled only if &jovie; is available in the system. The Settings and Help Menu Apart from the common &kde; Settings and Help menus described in the Menu chapter of the &kde; Fundamentals documentation &okular; has these application specific menu entries: F7 Settings Show Navigation Panel Toggle the navigation panel on and off. Settings Show Page Bar Toggle the page bar at the bottom of document area on and off to save vertical place in &okular; window. &Ctrl;&Shift;F Settings Full Screen Mode Enables the full screen mode. Note that full screen mode is different from presentation mode insofar as the only peculiarity of full screen mode is that it hides the window decorations, the menubar and the toolbar. Settings Configure Backends... Opens the Backend Configuration window. Configuring &okular; Backends You can configure &okular; backends by choosing Settings Configure Backends... . Currently, configuration options are provided for EPub, &PostScript;, FictionBook, Txt, OpenDocument Text, and &PDF; backends only. The backends configuration dialog The backends configuration dialog Using backend configuration pages for EPub, FictionBook, Txt and OpenDocument Text you can define the font to render documents in the corresponding formats. The Choose... button in these pages opens standard &kde; font configuration window. Please refer to the &kde; Fundamentals documentation for the details. The description of &PostScript; and &PDF; backends configuration pages can be found below. &PostScript; backend configuration You can configure &okular; &PostScript; rendering backend based on Ghostscript by choosing Ghostscript from the list on the left part of the configuration dialog. The only configurable option is as follows. Use platform fonts This option determines whether Ghostscript should be allowed to use platform fonts, if unchecked only usage of fonts embedded in the document will be allowed. This option is checked by default. &PDF; backend configuration You can configure &okular; &PDF; rendering backend based on Poppler by choosing &PDF; from the list on the left part of the configuration dialog. The only configurable option is as follows. Enhance thin lines Drawing lines in &okular; is implemented in two steps: generation of the clipping path and filling this clipping path. When the line in the original document is less than one pixel this two step implementation could cause problems. For those lines, the clipping path is filled with the filling color that depends on the thickness of the line part inside the clipping area. If the part of the line inside the clipping area gets very small the contrast between the shape and the background color can become too low for the line to be recognizable. The grids of such lines then looks very unpretty. Thin line (red), its clipping path (dashed line) and pixel boundaries (black solid lines) Thin line shown at a low contrast To enhance the look of the thin lines &okular; implements two options. The first option is Solid. With this option &okular; adjusts clipping path and line position so that clipping path and line are on the same pixel boundary, &ie; &okular; enlarges the thin lines to one pixel on the output device. This mode is similar to the Enhance thin lines in Adobe Reader. If this option is chosen, the thin lines are always enlarged. Thin line with Solid enhancement Thin line with Solid enhancement The second option is Shape. With this option the clipping path and line are adjusted to pixel boundary as well, but the line intensity is corrected according to its width. Thin line with Shape enhancement Thin line with Shape enhancement The thin lines are not enhanced by default (option No). Configuring &okular; General configuration You can configure &okular; by choosing Settings Configure &okular;.... The configuration dialog is split into six sections. This chapter describes the available options in detail. General Accessibility Performance Presentation Annotations Editor The configuration dialog The configuration dialog Depending on the currently installed backends, the Settings Configure Backends... menu item could be enabled. This particular configuration dialog holds the configurations of the backends that can actually be configured. General Show scrollbars Whether to show scrollbars for the document view. Link the thumbnails with the page Whether the thumbnails view should always display the current page or not. Show hints and info messages Whether to show some informative messages on startup, file load, &etc; Display document title in titlebar if available Whether to show the current document title in the titlebar of &okular; window. If no metadata for title found in the document or this item is unchecked &okular; shows filename of the document. When not displaying document title You can choose any of two options, Display file name only or Display full file path. Use custom background color Enables choosing the document background color (the color around the displayed page). By default, the &Qt; toolkit color is used when this option is unchecked. Open new files in tabs Whether to open new documents in tabs. The tabs are disabled by default. The default shortcuts to switch between tabs are &Ctrl;. (Next tab) and &Ctrl;, (Previous tab). Obey DRM limitations Whether &okular; should obey DRM (Digital Rights Management) restrictions. DRM limitations are used to make it impossible to perform certain actions with &PDF; documents, such as copying content to the clipboard. Note that in some configurations of &okular;, this option is not available. Reload document on file change Whether opened files should be automatically checked for changes and updated, if necessary. Show backend selection dialog Whether &okular; should ask the user which backend to use in case of more than one backend able to open the current file. If unchecked, &okular; will use the backend with the highest priority. Right to left reading direction Whether to use right to left reading direction by default for the opened files. Can be useful for some writing systems. Overview columns This option represents the number of columns to use in the overview mode. Page Up/Down overlap Here you can define the percentage of the current viewing area that should be visible after pressing Page Up/Page Down keys. Default Zoom This options specifies the default zoom mode for file which were never opened before. For those files which were opened before the previous zoom mode is applied. Accessibility Draw border around links Whether to draw a border around links. Change colors Enables the color changing options. Invert Colors Inverts colors on the view, &ie; black objects will be shown white. Change Paper Color Changes the paper's color, &ie; the document's background. Change Dark & Light Colors Changes the dark and light color to your preference, that means black will not be rendered as black but as the selected dark color and white will not be rendered as white but as the selected light color. Convert to Black & White Converts the document to black and white. You can set the threshold and the contrast. Setting the threshold to a higher value by moving it to the right will result in lighter grays used. Performance Enable transparency effects Draw selections and other special graphics using transparency effects. Disable the option to draw them using outline or opaque fill styles and increase speed on selections. Memory Usage &okular; can achieve best performance by tuning the memory usage, based on your system and your tastes. The more memory you let it to use, the faster the program will behave. The Default profile is good for every system, but you can prevent &okular; from using more memory than necessary by selecting the Low profile, or let it get the most out of your system using Aggressive. Use Greedy profile to preload all pages without risk of system memory overfull (only 50% of total memory or free memory will be used). Rendering Using this group of options you can improve text and image rendering in &okular;. The result depends on the device to display the document. Enable Text Antialias and Enable Graphics Antialias items can be used to switch on and off spatial anti-aliasing of text and images in document, correspondingly. Enable Text Hinting item is meant to be a switcher for font hinting. Antialiasing and hinting change how the documents are displayed, you may want to tweak them to your preference. Presentation Advance every Enables automatic advancing of pages given a time period. Loop after last page When navigating on presentation mode and going past the last page the first page will appear. Background color The color that will fill the part of the screen not covered by the page when on presentation mode. Mouse cursor Whether the mouse should be always hidden, always shown or hidden after a small time of inactivity. Show progress indicator Whether to show a progress circle that shows the current page and the total number of pages on the upper right corner of the presentation screen every time you change the page. Show summary page Whether to show a summary page at the beginning of the presentation with the title, author and number of pages of the document. Enable transitions Use this check box to enable or disable transition effects between pages. Default transition The transition effect between page and page if the document does not specify one. Set this to Random Transition to make &okular; randomly choose one of the available effects. Placement In this section you can select the Screen used to display the presentation. Current Screen is same screen of the &okular; window that starts the presentation mode. Default Screen is the screen marked as default in the xinerama configuration. Screen 0, Screen 1 &etc; are the available screens. Drawing Tool Configuration Manage the colors of the pencil used when drawing on the pages during the presentation mode. Annotations The Annotations page of the configuration dialog The Annotations page of the configuration dialog Author The author of the contents added on a document. Default is the name from the Password & User Account page of the &systemsettings; module Account Details. Annotation tools This pane is used to configure your annotating toolbar. There are five buttons (Add, Edit, Remove, Move Up, Move Down) and a list box (which lists the contents of the current annotating toolbar) which are used to configure the toolbar. If you need to add some tool button on the toolbar click on the Add button. You can choose the Name, the Type and the Appearance of the created tool. Please remember that annotation tools in &okular; are highly configurable. For example, you can have two buttons of the same tool but with different color. Do not hesitate to experiment in choosing the button set that is exactly tailored to your workflow. Click on some item in the list box then click on the corresponding button at the right part of the page to edit, remove, move up or move down the item. The keyboard shortcut of the tool (keys from 1 to 9) depends on its position in the list of annotating toolbar. Using Custom Stamps Create the icon you want to use for your own stamp and save it in any graphics format supported by &okular; Click the Add button, select type Stamp and enter a name for your stamp. Enter the full path to your custom icon into the dropdown box in the Stamp Symbol group Editor Editor Choose the editor you want to launch when &okular; wants to open a source file. This is the case when the document has references to the various points (usually row and column number) of sources it was generated from. The &DVI; format supports natively the addition of the information about the sources the LaTeX document was generated from. A similar system exists for &PDF; documents, called pdfsync, which stores these extra information in an external file named after the &PDF; file itself (for example mydocument.pdfsync for mydocument.pdf). &okular; ships with preconfigured settings for the following editors: &kate;, Kile, SciTE, &Emacs; client, and LyX client. To use inverse search in Kile, you have to compile your &latex; file with the Modern configuration. Command This is the command and its parameters to invoke the selected editor with the source file of the actual document. This field will be filled automatically if you use one of the preconfigured editors. Otherwise, please choose Custom Text Editor in Editor drop down box and refer to the documentation on your favorite editor to find the proper command. You can use the following placeholders: %f - the file name %l - the line of the file to be reached %c - the column of the file to be reached If %f is not specified, then the document name is appended to the specified command. Credits and License Program Copyright: &Albert.Astals.Cid; &Albert.Astals.Cid.mail; Current maintainer Pino Toscano pino@kde.org Enrico Ros eros.kde@email.it &kpdf; developer Documentation Copyright: &Albert.Astals.Cid; &Albert.Astals.Cid.mail; Author Titus Laska titus.laska@gmx.de Some updates and additions Pino Toscano pino@kde.org &underFDL; &underGPL; &documentation.index;
diff --git a/generators/kimgio/generator_kimgio.cpp b/generators/kimgio/generator_kimgio.cpp index 2b7db6cdd..f78e9b037 100644 --- a/generators/kimgio/generator_kimgio.cpp +++ b/generators/kimgio/generator_kimgio.cpp @@ -1,156 +1,167 @@ /*************************************************************************** * Copyright (C) 2005 by Albert Astals Cid * * Copyright (C) 2006-2007 by Pino Toscano * * Copyright (C) 2006-2007 by Tobias Koenig * + * 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 "generator_kimgio.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include OKULAR_EXPORT_PLUGIN(KIMGIOGenerator, "libokularGenerator_kimgio.json") KIMGIOGenerator::KIMGIOGenerator( QObject *parent, const QVariantList &args ) : Generator( parent, args ) { setFeature( ReadRawData ); setFeature( Threaded ); setFeature( TiledRendering ); setFeature( PrintNative ); setFeature( PrintToFile ); + setFeature( SwapBackingFile ); } KIMGIOGenerator::~KIMGIOGenerator() { } bool KIMGIOGenerator::loadDocument( const QString & fileName, QVector & pagesVector ) { QFile f( fileName ); if ( !f.open(QFile::ReadOnly) ) { emit error( i18n( "Unable to load document: %1", f.errorString() ), -1 ); return false; } return loadDocumentInternal( f.readAll(), fileName, pagesVector ); } bool KIMGIOGenerator::loadDocumentFromData( const QByteArray & fileData, QVector & pagesVector ) { return loadDocumentInternal( fileData, QString(), pagesVector ); } bool KIMGIOGenerator::loadDocumentInternal(const QByteArray & fileData, const QString & fileName, QVector & pagesVector ) { QBuffer buffer; buffer.setData( fileData ); buffer.open( QIODevice::ReadOnly ); QImageReader reader( &buffer, QImageReader::imageFormat( &buffer ) ); reader.setAutoDetectImageFormat( true ); if ( !reader.read( &m_img ) ) { if (!m_img.isNull()) { emit warning( i18n( "This document appears malformed. Here is a best approximation of the document's intended appearance." ), -1 ); } else { emit error( i18n( "Unable to load document: %1", reader.errorString() ), -1 ); return false; } } QMimeDatabase db; auto mime = db.mimeTypeForFileNameAndData( fileName, fileData ); docInfo.set( Okular::DocumentInfo::MimeType, mime.name() ); // Apply transformations dictated by Exif metadata KExiv2Iface::KExiv2 exifMetadata; if ( exifMetadata.loadFromData( fileData ) ) { exifMetadata.rotateExifQImage(m_img, exifMetadata.getImageOrientation()); } pagesVector.resize( 1 ); Okular::Page * page = new Okular::Page( 0, m_img.width(), m_img.height(), Okular::Rotation0 ); pagesVector[0] = page; return true; } +KIMGIOGenerator::SwapBackingFileResult KIMGIOGenerator::swapBackingFile( QString const &/*newFileName*/, QVector & /*newPagesVector*/ ) +{ + // NOP: We don't actually need to do anything because all data has already + // been loaded in RAM + return SwapBackingFileNoOp; +} + bool KIMGIOGenerator::doCloseDocument() { m_img = QImage(); return true; } QImage KIMGIOGenerator::image( Okular::PixmapRequest * request ) { // perform a smooth scaled generation if ( request->isTile() ) { const QRect srcRect = request->normalizedRect().geometry( m_img.width(), m_img.height() ); const QRect destRect = request->normalizedRect().geometry( request->width(), request->height() ); QImage destImg( destRect.size(), QImage::Format_RGB32 ); destImg.fill( Qt::white ); QPainter p( &destImg ); p.setRenderHint( QPainter::SmoothPixmapTransform ); p.drawImage( destImg.rect(), m_img, srcRect ); return destImg; } else { int width = request->width(); int height = request->height(); if ( request->page()->rotation() % 2 == 1 ) qSwap( width, height ); return m_img.scaled( width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation ); } } bool KIMGIOGenerator::print( QPrinter& printer ) { QPainter p( &printer ); QImage image( m_img ); if ( ( image.width() > printer.width() ) || ( image.height() > printer.height() ) ) image = image.scaled( printer.width(), printer.height(), Qt::KeepAspectRatio, Qt::SmoothTransformation ); p.drawImage( 0, 0, image ); return true; } Okular::DocumentInfo KIMGIOGenerator::generateDocumentInfo( const QSet &keys ) const { Q_UNUSED(keys); return docInfo; } #include "generator_kimgio.moc" diff --git a/generators/kimgio/generator_kimgio.h b/generators/kimgio/generator_kimgio.h index 2c01456a2..bd09bbed0 100644 --- a/generators/kimgio/generator_kimgio.h +++ b/generators/kimgio/generator_kimgio.h @@ -1,49 +1,53 @@ /*************************************************************************** * Copyright (C) 2005 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. * ***************************************************************************/ #ifndef _OKULAR_GENERATOR_KIMGIO_H_ #define _OKULAR_GENERATOR_KIMGIO_H_ #include #include #include class KIMGIOGenerator : public Okular::Generator { Q_OBJECT Q_INTERFACES( Okular::Generator ) public: KIMGIOGenerator( QObject *parent, const QVariantList &args ); virtual ~KIMGIOGenerator(); // [INHERITED] load a document and fill up the pagesVector bool loadDocument( const QString & fileName, QVector & pagesVector ) override; bool loadDocumentFromData( const QByteArray & fileData, QVector & pagesVector ) override; + SwapBackingFileResult swapBackingFile( QString const &newFileName, QVector & newPagesVector ) override; // [INHERITED] print document using already configured kprinter bool print( QPrinter& printer ) override; // [INHERITED] document information Okular::DocumentInfo generateDocumentInfo( const QSet &keys ) const override; protected: bool doCloseDocument() override; QImage image( Okular::PixmapRequest * request ) override; private: bool loadDocumentInternal(const QByteArray & fileData, const QString & fileName, QVector & pagesVector ); private: QImage m_img; Okular::DocumentInfo docInfo; }; #endif diff --git a/generators/poppler/annots.cpp b/generators/poppler/annots.cpp index c7fd79834..a24539e75 100644 --- a/generators/poppler/annots.cpp +++ b/generators/poppler/annots.cpp @@ -1,423 +1,427 @@ /*************************************************************************** * Copyright (C) 2008 by Pino Toscano * * Copyright (C) 2012 by Guillermo A. Amaral B. * + * 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 // qt/kde includes #include #include #include #include #include "annots.h" #include "debug_pdf.h" #include "generator_pdf.h" #include "popplerembeddedfile.h" #include "config-okular-poppler.h" Q_DECLARE_METATYPE( Poppler::Annotation* ) extern Okular::Sound* createSoundFromPopplerSound( const Poppler::SoundObject *popplerSound ); extern Okular::Movie* createMovieFromPopplerMovie( const Poppler::MovieObject *popplerMovie ); extern Okular::Movie* createMovieFromPopplerScreen( const Poppler::LinkRendition *popplerScreen ); #ifdef HAVE_POPPLER_0_36 extern QPair createMovieFromPopplerRichMedia( const Poppler::RichMediaAnnotation *popplerRichMedia ); #endif static void disposeAnnotation( const Okular::Annotation *ann ) { Poppler::Annotation *popplerAnn = qvariant_cast< Poppler::Annotation * >( ann->nativeId() ); delete popplerAnn; } static QPointF normPointToPointF( const Okular::NormalizedPoint& pt ) { return QPointF(pt.x, pt.y); } static QRectF normRectToRectF( const Okular::NormalizedRect& rect ) { return QRectF( QPointF(rect.left, rect.top), QPointF(rect.right, rect.bottom) ); } // Poppler and Okular share the same flag values, but we don't want to export internal flags static int maskExportedFlags(int flags) { return flags & ( Okular::Annotation::Hidden | Okular::Annotation::FixedSize | Okular::Annotation::FixedRotation | Okular::Annotation::DenyPrint | Okular::Annotation::DenyWrite | Okular::Annotation::DenyDelete | Okular::Annotation::ToggleHidingOnMouse ); } //BEGIN PopplerAnnotationProxy implementation -PopplerAnnotationProxy::PopplerAnnotationProxy( Poppler::Document *doc, QMutex *userMutex ) - : ppl_doc ( doc ), mutex ( userMutex ) +PopplerAnnotationProxy::PopplerAnnotationProxy( Poppler::Document *doc, QMutex *userMutex, QHash *annotsOnOpenHash ) + : ppl_doc ( doc ), mutex ( userMutex ), annotationsOnOpenHash( annotsOnOpenHash ) { } PopplerAnnotationProxy::~PopplerAnnotationProxy() { } bool PopplerAnnotationProxy::supports( Capability cap ) const { switch ( cap ) { case Addition: case Modification: case Removal: return true; default: return false; } } void PopplerAnnotationProxy::notifyAddition( Okular::Annotation *okl_ann, int page ) { // Export annotation to DOM QDomDocument doc; QDomElement dom_ann = doc.createElement( QStringLiteral("root") ); Okular::AnnotationUtils::storeAnnotation( okl_ann, dom_ann, doc ); QMutexLocker ml(mutex); // Create poppler annotation Poppler::Annotation *ppl_ann = Poppler::AnnotationUtils::createAnnotation( dom_ann ); // Poppler doesn't render StampAnnotations yet if ( ppl_ann->subType() != Poppler::Annotation::AStamp ) okl_ann->setFlags( okl_ann->flags() | Okular::Annotation::ExternallyDrawn ); // Poppler stores highlight points in swapped order if ( ppl_ann->subType() == Poppler::Annotation::AHighlight ) { Poppler::HighlightAnnotation * hlann = static_cast( ppl_ann ); QList quads = hlann->highlightQuads(); QMutableListIterator it( quads ); while ( it.hasNext() ) { Poppler::HighlightAnnotation::Quad &q = it.next(); QPointF t; t = q.points[3]; q.points[3] = q.points[0]; q.points[0] = t; t = q.points[2]; q.points[2] = q.points[1]; q.points[1] = t; } hlann->setHighlightQuads( quads ); } // Bind poppler object to page Poppler::Page *ppl_page = ppl_doc->page( page ); ppl_page->addAnnotation( ppl_ann ); delete ppl_page; // Set pointer to poppler annotation as native Id okl_ann->setNativeId( qVariantFromValue( ppl_ann ) ); okl_ann->setDisposeDataFunction( disposeAnnotation ); qCDebug(OkularPdfDebug) << okl_ann->uniqueName(); } void PopplerAnnotationProxy::notifyModification( const Okular::Annotation *okl_ann, int page, bool appearanceChanged ) { Q_UNUSED( page ); Q_UNUSED( appearanceChanged ); Poppler::Annotation *ppl_ann = qvariant_cast( okl_ann->nativeId() ); if ( !ppl_ann ) // Ignore non-native annotations return; QMutexLocker ml(mutex); if ( okl_ann->flags() & (Okular::Annotation::BeingMoved | Okular::Annotation::BeingResized) ) { // Okular ui already renders the annotation on its own ppl_ann->setFlags( Poppler::Annotation::Hidden ); return; } // Set basic properties // Note: flags and boundary must be set first in order to correctly handle // FixedRotation annotations. ppl_ann->setFlags(maskExportedFlags( okl_ann->flags() )); ppl_ann->setBoundary(normRectToRectF( okl_ann->boundingRectangle() )); ppl_ann->setAuthor( okl_ann->author() ); ppl_ann->setContents( okl_ann->contents() ); // Set style Poppler::Annotation::Style s; s.setColor( okl_ann->style().color() ); s.setWidth( okl_ann->style().width() ); s.setOpacity( okl_ann->style().opacity() ); ppl_ann->setStyle( s ); // Set type-specific properties (if any) switch ( ppl_ann->subType() ) { case Poppler::Annotation::AText: { const Okular::TextAnnotation * okl_txtann = static_cast(okl_ann); Poppler::TextAnnotation * ppl_txtann = static_cast(ppl_ann); ppl_txtann->setTextIcon( okl_txtann->textIcon() ); ppl_txtann->setTextFont( okl_txtann->textFont() ); ppl_txtann->setInplaceAlign( okl_txtann->inplaceAlignment() ); ppl_txtann->setCalloutPoints( QVector() ); ppl_txtann->setInplaceIntent( (Poppler::TextAnnotation::InplaceIntent)okl_txtann->inplaceIntent() ); break; } case Poppler::Annotation::ALine: { const Okular::LineAnnotation * okl_lineann = static_cast(okl_ann); Poppler::LineAnnotation * ppl_lineann = static_cast(ppl_ann); QLinkedList points; foreach ( const Okular::NormalizedPoint &p, okl_lineann->linePoints() ) points.append(normPointToPointF( p )); ppl_lineann->setLinePoints( points ); ppl_lineann->setLineStartStyle( (Poppler::LineAnnotation::TermStyle)okl_lineann->lineStartStyle() ); ppl_lineann->setLineEndStyle( (Poppler::LineAnnotation::TermStyle)okl_lineann->lineEndStyle() ); ppl_lineann->setLineClosed( okl_lineann->lineClosed() ); ppl_lineann->setLineInnerColor( okl_lineann->lineInnerColor() ); ppl_lineann->setLineLeadingForwardPoint( okl_lineann->lineLeadingForwardPoint() ); ppl_lineann->setLineLeadingBackPoint( okl_lineann->lineLeadingBackwardPoint() ); ppl_lineann->setLineShowCaption( okl_lineann->showCaption() ); ppl_lineann->setLineIntent( (Poppler::LineAnnotation::LineIntent)okl_lineann->lineIntent() ); break; } case Poppler::Annotation::AGeom: { const Okular::GeomAnnotation * okl_geomann = static_cast(okl_ann); Poppler::GeomAnnotation * ppl_geomann = static_cast(ppl_ann); ppl_geomann->setGeomType( (Poppler::GeomAnnotation::GeomType)okl_geomann->geometricalType() ); ppl_geomann->setGeomInnerColor( okl_geomann->geometricalInnerColor() ); break; } case Poppler::Annotation::AHighlight: { const Okular::HighlightAnnotation * okl_hlann = static_cast(okl_ann); Poppler::HighlightAnnotation * ppl_hlann = static_cast(ppl_ann); ppl_hlann->setHighlightType( (Poppler::HighlightAnnotation::HighlightType)okl_hlann->highlightType() ); break; } case Poppler::Annotation::AStamp: { const Okular::StampAnnotation * okl_stampann = static_cast(okl_ann); Poppler::StampAnnotation * ppl_stampann = static_cast(ppl_ann); ppl_stampann->setStampIconName( okl_stampann->stampIconName() ); break; } case Poppler::Annotation::AInk: { const Okular::InkAnnotation * okl_inkann = static_cast(okl_ann); Poppler::InkAnnotation * ppl_inkann = static_cast(ppl_ann); QList< QLinkedList > paths; foreach ( const QLinkedList &path, okl_inkann->inkPaths() ) { QLinkedList points; foreach ( const Okular::NormalizedPoint &p, path ) points.append(normPointToPointF( p )); paths.append( points ); } ppl_inkann->setInkPaths( paths ); break; } default: qCDebug(OkularPdfDebug) << "Type-specific property modification is not implemented for this annotation type"; break; } qCDebug(OkularPdfDebug) << okl_ann->uniqueName(); } void PopplerAnnotationProxy::notifyRemoval( Okular::Annotation *okl_ann, int page ) { Poppler::Annotation *ppl_ann = qvariant_cast( okl_ann->nativeId() ); if ( !ppl_ann ) // Ignore non-native annotations return; QMutexLocker ml(mutex); Poppler::Page *ppl_page = ppl_doc->page( page ); + annotationsOnOpenHash->remove( okl_ann ); ppl_page->removeAnnotation( ppl_ann ); // Also destroys ppl_ann delete ppl_page; okl_ann->setNativeId( qVariantFromValue(0) ); // So that we don't double-free in disposeAnnotation qCDebug(OkularPdfDebug) << okl_ann->uniqueName(); } //END PopplerAnnotationProxy implementation Okular::Annotation* createAnnotationFromPopplerAnnotation( Poppler::Annotation *ann, bool *doDelete ) { Okular::Annotation *annotation = 0; *doDelete = true; bool tieToOkularAnn = false; bool externallyDrawn = false; switch ( ann->subType() ) { case Poppler::Annotation::AFileAttachment: { Poppler::FileAttachmentAnnotation * attachann = static_cast< Poppler::FileAttachmentAnnotation * >( ann ); Okular::FileAttachmentAnnotation * f = new Okular::FileAttachmentAnnotation(); annotation = f; tieToOkularAnn = true; *doDelete = false; f->setFileIconName( attachann->fileIconName() ); f->setEmbeddedFile( new PDFEmbeddedFile( attachann->embeddedFile() ) ); break; } case Poppler::Annotation::ASound: { Poppler::SoundAnnotation * soundann = static_cast< Poppler::SoundAnnotation * >( ann ); Okular::SoundAnnotation * s = new Okular::SoundAnnotation(); annotation = s; s->setSoundIconName( soundann->soundIconName() ); s->setSound( createSoundFromPopplerSound( soundann->sound() ) ); break; } case Poppler::Annotation::AMovie: { Poppler::MovieAnnotation * movieann = static_cast< Poppler::MovieAnnotation * >( ann ); Okular::MovieAnnotation * m = new Okular::MovieAnnotation(); annotation = m; tieToOkularAnn = true; *doDelete = false; m->setMovie( createMovieFromPopplerMovie( movieann->movie() ) ); break; } case Poppler::Annotation::AWidget: { annotation = new Okular::WidgetAnnotation(); break; } case Poppler::Annotation::AScreen: { Okular::ScreenAnnotation * m = new Okular::ScreenAnnotation(); annotation = m; tieToOkularAnn = true; *doDelete = false; break; } #ifdef HAVE_POPPLER_0_36 case Poppler::Annotation::ARichMedia: { Poppler::RichMediaAnnotation * richmediaann = static_cast< Poppler::RichMediaAnnotation * >( ann ); const QPair result = createMovieFromPopplerRichMedia( richmediaann ); if ( result.first ) { Okular::RichMediaAnnotation * r = new Okular::RichMediaAnnotation(); tieToOkularAnn = true; *doDelete = false; annotation = r; r->setMovie( result.first ); r->setEmbeddedFile( result.second ); } break; } #endif case Poppler::Annotation::AText: case Poppler::Annotation::ALine: case Poppler::Annotation::AGeom: case Poppler::Annotation::AHighlight: case Poppler::Annotation::AInk: case Poppler::Annotation::ACaret: externallyDrawn = true; /* fallthrough */ case Poppler::Annotation::AStamp: tieToOkularAnn = true; *doDelete = false; /* fallthrough */ default: { // this is uber ugly but i don't know a better way to do it without introducing a poppler::annotation dependency on core QDomDocument doc; QDomElement root = doc.createElement( QStringLiteral("root") ); doc.appendChild( root ); Poppler::AnnotationUtils::storeAnnotation( ann, root, doc ); annotation = Okular::AnnotationUtils::createAnnotation( root ); break; } } if ( annotation ) { // the Contents field might have lines separated by \r QString contents = ann->contents(); contents.replace( QLatin1Char( '\r' ), QLatin1Char( '\n' ) ); annotation->setAuthor( ann->author() ); annotation->setContents( contents ); annotation->setUniqueName( ann->uniqueName() ); annotation->setModificationDate( ann->modificationDate() ); annotation->setCreationDate( ann->creationDate() ); annotation->setFlags( ann->flags() | Okular::Annotation::External ); annotation->setBoundingRectangle( Okular::NormalizedRect::fromQRectF( ann->boundary() ) ); if (externallyDrawn) annotation->setFlags( annotation->flags() | Okular::Annotation::ExternallyDrawn ); // Poppler stores highlight points in swapped order if ( annotation->subType() == Okular::Annotation::AHighlight ) { Okular::HighlightAnnotation * hlann = static_cast( annotation ); QList &quads = hlann->highlightQuads(); for (QList::iterator it = quads.begin(); it != quads.end(); ++it) { Okular::NormalizedPoint t; t = it->point( 3 ); it->setPoint( it->point(0), 3 ); it->setPoint( t, 0 ); t = it->point( 2 ); it->setPoint( it->point(1), 2 ); it->setPoint( t, 1 ); } } if ( annotation->subType() == Okular::Annotation::AText ) { Okular::TextAnnotation * txtann = static_cast( annotation ); if ( txtann->textType() == Okular::TextAnnotation::Linked ) { Poppler::TextAnnotation * ppl_txtann = static_cast( ann ); // Poppler and Okular assume a different default icon name in XML // We re-read it via getter, which always tells the right one txtann->setTextIcon( ppl_txtann->textIcon() ); } } // TODO clone style // TODO clone window // TODO clone revisions if ( tieToOkularAnn ) { annotation->setNativeId( qVariantFromValue( ann ) ); annotation->setDisposeDataFunction( disposeAnnotation ); } } return annotation; } diff --git a/generators/poppler/annots.h b/generators/poppler/annots.h index 9dc6aa6d7..cf7496a98 100644 --- a/generators/poppler/annots.h +++ b/generators/poppler/annots.h @@ -1,38 +1,42 @@ /*************************************************************************** * Copyright (C) 2012 by Fabio D'Urso * + * 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_GENERATOR_PDF_ANNOTS_H_ #define _OKULAR_GENERATOR_PDF_ANNOTS_H_ #include #include #include #include "core/annotations.h" #include "config-okular-poppler.h" extern Okular::Annotation* createAnnotationFromPopplerAnnotation( Poppler::Annotation *ann, bool * doDelete ); class PopplerAnnotationProxy : public Okular::AnnotationProxy { public: - PopplerAnnotationProxy( Poppler::Document *doc, QMutex *userMutex ); + PopplerAnnotationProxy( Poppler::Document *doc, QMutex *userMutex, QHash *annotsOnOpenHash ); ~PopplerAnnotationProxy(); bool supports( Capability capability ) const override; void notifyAddition( Okular::Annotation *annotation, int page ) override; void notifyModification( const Okular::Annotation *annotation, int page, bool appearanceChanged ) override; void notifyRemoval( Okular::Annotation *annotation, int page ) override; private: Poppler::Document *ppl_doc; QMutex *mutex; + QHash *annotationsOnOpenHash; }; #endif diff --git a/generators/poppler/generator_pdf.cpp b/generators/poppler/generator_pdf.cpp index 465c3e2fb..61a393f22 100644 --- a/generators/poppler/generator_pdf.cpp +++ b/generators/poppler/generator_pdf.cpp @@ -1,1805 +1,1828 @@ /*************************************************************************** * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2004 by Enrico Ros * * Copyright (C) 2012 by Guillermo A. Amaral B. * * 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 "generator_pdf.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ui_pdfsettingswidget.h" #include "pdfsettings.h" #include #include #include "debug_pdf.h" #include "annots.h" #include "formfields.h" #include "popplerembeddedfile.h" Q_DECLARE_METATYPE(Poppler::Annotation*) Q_DECLARE_METATYPE(Poppler::FontInfo) Q_DECLARE_METATYPE(const Poppler::LinkMovie*) Q_DECLARE_METATYPE(const Poppler::LinkRendition*) #ifdef HAVE_POPPLER_0_50 Q_DECLARE_METATYPE(const Poppler::LinkOCGState*) #endif static const int defaultPageWidth = 595; static const int defaultPageHeight = 842; class PDFOptionsPage : public QWidget { Q_OBJECT public: PDFOptionsPage() { setWindowTitle( i18n( "PDF Options" ) ); QVBoxLayout *layout = new QVBoxLayout(this); m_printAnnots = new QCheckBox(i18n("Print annotations"), this); m_printAnnots->setToolTip(i18n("Include annotations in the printed document")); m_printAnnots->setWhatsThis(i18n("Includes annotations in the printed document. You can disable this if you want to print the original unannotated document.")); layout->addWidget(m_printAnnots); m_forceRaster = new QCheckBox(i18n("Force rasterization"), this); m_forceRaster->setToolTip(i18n("Rasterize into an image before printing")); m_forceRaster->setWhatsThis(i18n("Forces the rasterization of each page into an image before printing it. This usually gives somewhat worse results, but is useful when printing documents that appear to print incorrectly.")); layout->addWidget(m_forceRaster); layout->addStretch(1); #if defined(Q_OS_WIN) && !defined HAVE_POPPLER_0_60 m_printAnnots->setVisible( false ); #endif setPrintAnnots( true ); // Default value } bool printAnnots() { return m_printAnnots->isChecked(); } void setPrintAnnots( bool printAnnots ) { m_printAnnots->setChecked( printAnnots ); } bool printForceRaster() { return m_forceRaster->isChecked(); } void setPrintForceRaster( bool forceRaster ) { m_forceRaster->setChecked( forceRaster ); } private: QCheckBox *m_printAnnots; QCheckBox *m_forceRaster; }; static void fillViewportFromLinkDestination( Okular::DocumentViewport &viewport, const Poppler::LinkDestination &destination ) { viewport.pageNumber = destination.pageNumber() - 1; if (!viewport.isValid()) return; // get destination position // TODO add other attributes to the viewport (taken from link) // switch ( destination->getKind() ) // { // case destXYZ: if (destination.isChangeLeft() || destination.isChangeTop()) { // TODO remember to change this if we implement DPI and/or rotation double left, top; left = destination.left(); top = destination.top(); viewport.rePos.normalizedX = left; viewport.rePos.normalizedY = top; viewport.rePos.enabled = true; viewport.rePos.pos = Okular::DocumentViewport::TopLeft; } /* TODO if ( dest->getChangeZoom() ) make zoom change*/ /* break; default: // implement the others cases break;*/ // } } Okular::Sound* createSoundFromPopplerSound( const Poppler::SoundObject *popplerSound ) { Okular::Sound *sound = popplerSound->soundType() == Poppler::SoundObject::Embedded ? new Okular::Sound( popplerSound->data() ) : new Okular::Sound( popplerSound->url() ); sound->setSamplingRate( popplerSound->samplingRate() ); sound->setChannels( popplerSound->channels() ); sound->setBitsPerSample( popplerSound->bitsPerSample() ); switch ( popplerSound->soundEncoding() ) { case Poppler::SoundObject::Raw: sound->setSoundEncoding( Okular::Sound::Raw ); break; case Poppler::SoundObject::Signed: sound->setSoundEncoding( Okular::Sound::Signed ); break; case Poppler::SoundObject::muLaw: sound->setSoundEncoding( Okular::Sound::muLaw ); break; case Poppler::SoundObject::ALaw: sound->setSoundEncoding( Okular::Sound::ALaw ); break; } return sound; } Okular::Movie* createMovieFromPopplerMovie( const Poppler::MovieObject *popplerMovie ) { Okular::Movie *movie = new Okular::Movie( popplerMovie->url() ); movie->setSize( popplerMovie->size() ); movie->setRotation( (Okular::Rotation)( popplerMovie->rotation() / 90 ) ); movie->setShowControls( popplerMovie->showControls() ); movie->setPlayMode( (Okular::Movie::PlayMode)popplerMovie->playMode() ); movie->setAutoPlay( false ); // will be triggered by external MovieAnnotation movie->setShowPosterImage( popplerMovie->showPosterImage() ); movie->setPosterImage( popplerMovie->posterImage() ); return movie; } Okular::Movie* createMovieFromPopplerScreen( const Poppler::LinkRendition *popplerScreen ) { Poppler::MediaRendition *rendition = popplerScreen->rendition(); Okular::Movie *movie = 0; if ( rendition->isEmbedded() ) movie = new Okular::Movie( rendition->fileName(), rendition->data() ); else movie = new Okular::Movie( rendition->fileName() ); movie->setSize( rendition->size() ); movie->setShowControls( rendition->showControls() ); if ( rendition->repeatCount() == 0 ) { movie->setPlayMode( Okular::Movie::PlayRepeat ); } else { movie->setPlayMode( Okular::Movie::PlayLimited ); movie->setPlayRepetitions( rendition->repeatCount() ); } movie->setAutoPlay( rendition->autoPlay() ); return movie; } #ifdef HAVE_POPPLER_0_36 QPair createMovieFromPopplerRichMedia( const Poppler::RichMediaAnnotation *popplerRichMedia ) { const QPair emptyResult(0, 0); /** * To convert a Flash/Video based RichMedia annotation to a movie, we search for the first * Flash/Video richmedia instance and parse the flashVars parameter for the 'source' identifier. * That identifier is then used to find the associated embedded file through the assets * mapping. */ const Poppler::RichMediaAnnotation::Content *content = popplerRichMedia->content(); if ( !content ) return emptyResult; const QList configurations = content->configurations(); if ( configurations.isEmpty() ) return emptyResult; const Poppler::RichMediaAnnotation::Configuration *configuration = configurations[0]; const QList instances = configuration->instances(); if ( instances.isEmpty() ) return emptyResult; const Poppler::RichMediaAnnotation::Instance *instance = instances[0]; if ( ( instance->type() != Poppler::RichMediaAnnotation::Instance::TypeFlash ) && ( instance->type() != Poppler::RichMediaAnnotation::Instance::TypeVideo ) ) return emptyResult; const Poppler::RichMediaAnnotation::Params *params = instance->params(); if ( !params ) return emptyResult; QString sourceId; bool playbackLoops = false; const QStringList flashVars = params->flashVars().split( QLatin1Char( '&' ) ); foreach ( const QString & flashVar, flashVars ) { const int pos = flashVar.indexOf( QLatin1Char( '=' ) ); if ( pos == -1 ) continue; const QString key = flashVar.left( pos ); const QString value = flashVar.mid( pos + 1 ); if ( key == QLatin1String( "source" ) ) sourceId = value; else if ( key == QLatin1String( "loop" ) ) playbackLoops = ( value == QLatin1String( "true" ) ? true : false ); } if ( sourceId.isEmpty() ) return emptyResult; const QList assets = content->assets(); if ( assets.isEmpty() ) return emptyResult; Poppler::RichMediaAnnotation::Asset *matchingAsset = 0; foreach ( Poppler::RichMediaAnnotation::Asset *asset, assets ) { if ( asset->name() == sourceId ) { matchingAsset = asset; break; } } if ( !matchingAsset ) return emptyResult; Poppler::EmbeddedFile *embeddedFile = matchingAsset->embeddedFile(); if ( !embeddedFile ) return emptyResult; Okular::EmbeddedFile *pdfEmbeddedFile = new PDFEmbeddedFile( embeddedFile ); Okular::Movie *movie = new Okular::Movie( embeddedFile->name(), embeddedFile->data() ); movie->setPlayMode( playbackLoops ? Okular::Movie::PlayRepeat : Okular::Movie::PlayLimited ); if ( popplerRichMedia && popplerRichMedia->settings() && popplerRichMedia->settings()->activation() ) { if ( popplerRichMedia->settings()->activation()->condition() == Poppler::RichMediaAnnotation::Activation::PageOpened || popplerRichMedia->settings()->activation()->condition() == Poppler::RichMediaAnnotation::Activation::PageVisible ) { movie->setAutoPlay( true ); } else { movie->setAutoPlay( false ); } } else { movie->setAutoPlay( false ); } return qMakePair(movie, pdfEmbeddedFile); } #endif /** * Note: the function will take ownership of the popplerLink object. */ Okular::Action* createLinkFromPopplerLink(const Poppler::Link *popplerLink) { if (!popplerLink) return nullptr; Okular::Action *link = 0; const Poppler::LinkGoto *popplerLinkGoto; const Poppler::LinkExecute *popplerLinkExecute; const Poppler::LinkBrowse *popplerLinkBrowse; const Poppler::LinkAction *popplerLinkAction; const Poppler::LinkSound *popplerLinkSound; const Poppler::LinkJavaScript *popplerLinkJS; const Poppler::LinkMovie *popplerLinkMovie; const Poppler::LinkRendition *popplerLinkRendition; Okular::DocumentViewport viewport; bool deletePopplerLink = true; switch(popplerLink->linkType()) { case Poppler::Link::None: break; case Poppler::Link::Goto: { popplerLinkGoto = static_cast(popplerLink); const Poppler::LinkDestination dest = popplerLinkGoto->destination(); const QString destName = dest.destinationName(); if (destName.isEmpty()) { fillViewportFromLinkDestination( viewport, dest ); link = new Okular::GotoAction(popplerLinkGoto->fileName(), viewport); } else { link = new Okular::GotoAction(popplerLinkGoto->fileName(), destName); } } break; case Poppler::Link::Execute: popplerLinkExecute = static_cast(popplerLink); link = new Okular::ExecuteAction( popplerLinkExecute->fileName(), popplerLinkExecute->parameters() ); break; case Poppler::Link::Browse: popplerLinkBrowse = static_cast(popplerLink); link = new Okular::BrowseAction( QUrl(popplerLinkBrowse->url()) ); break; case Poppler::Link::Action: popplerLinkAction = static_cast(popplerLink); link = new Okular::DocumentAction( (Okular::DocumentAction::DocumentActionType)popplerLinkAction->actionType() ); break; case Poppler::Link::Sound: { popplerLinkSound = static_cast(popplerLink); Poppler::SoundObject *popplerSound = popplerLinkSound->sound(); Okular::Sound *sound = createSoundFromPopplerSound( popplerSound ); link = new Okular::SoundAction( popplerLinkSound->volume(), popplerLinkSound->synchronous(), popplerLinkSound->repeat(), popplerLinkSound->mix(), sound ); } break; case Poppler::Link::JavaScript: { popplerLinkJS = static_cast(popplerLink); link = new Okular::ScriptAction( Okular::JavaScript, popplerLinkJS->script() ); } break; case Poppler::Link::Rendition: { deletePopplerLink = false; // we'll delete it inside resolveMediaLinkReferences() after we have resolved all references popplerLinkRendition = static_cast( popplerLink ); Okular::RenditionAction::OperationType operation = Okular::RenditionAction::None; switch ( popplerLinkRendition->action() ) { case Poppler::LinkRendition::NoRendition: operation = Okular::RenditionAction::None; break; case Poppler::LinkRendition::PlayRendition: operation = Okular::RenditionAction::Play; break; case Poppler::LinkRendition::StopRendition: operation = Okular::RenditionAction::Stop; break; case Poppler::LinkRendition::PauseRendition: operation = Okular::RenditionAction::Pause; break; case Poppler::LinkRendition::ResumeRendition: operation = Okular::RenditionAction::Resume; break; }; Okular::Movie *movie = 0; if ( popplerLinkRendition->rendition() ) movie = createMovieFromPopplerScreen( popplerLinkRendition ); Okular::RenditionAction *renditionAction = new Okular::RenditionAction( operation, movie, Okular::JavaScript, popplerLinkRendition->script() ); renditionAction->setNativeId( QVariant::fromValue( popplerLinkRendition ) ); link = renditionAction; } break; case Poppler::Link::Movie: { deletePopplerLink = false; // we'll delete it inside resolveMediaLinkReferences() after we have resolved all references popplerLinkMovie = static_cast( popplerLink ); Okular::MovieAction::OperationType operation = Okular::MovieAction::Play; switch ( popplerLinkMovie->operation() ) { case Poppler::LinkMovie::Play: operation = Okular::MovieAction::Play; break; case Poppler::LinkMovie::Stop: operation = Okular::MovieAction::Stop; break; case Poppler::LinkMovie::Pause: operation = Okular::MovieAction::Pause; break; case Poppler::LinkMovie::Resume: operation = Okular::MovieAction::Resume; break; }; Okular::MovieAction *movieAction = new Okular::MovieAction( operation ); movieAction->setNativeId( QVariant::fromValue( popplerLinkMovie ) ); link = movieAction; } break; } if ( deletePopplerLink ) delete popplerLink; return link; } /** * Note: the function will take ownership of the popplerLink objects. */ static QLinkedList generateLinks( const QList &popplerLinks ) { QLinkedList links; foreach(const Poppler::Link *popplerLink, popplerLinks) { QRectF linkArea = popplerLink->linkArea(); double nl = linkArea.left(), nt = linkArea.top(), nr = linkArea.right(), nb = linkArea.bottom(); // create the rect using normalized coords and attach the Okular::Link to it Okular::ObjectRect * rect = new Okular::ObjectRect( nl, nt, nr, nb, false, Okular::ObjectRect::Action, createLinkFromPopplerLink(popplerLink) ); // add the ObjectRect to the container links.push_front( rect ); } return links; } /** NOTES on threading: * internal: thread race prevention is done via the 'docLock' mutex. the * mutex is needed only because we have the asynchronous thread; else * the operations are all within the 'gui' thread, scheduled by the * Qt scheduler and no mutex is needed. * external: dangerous operations are all locked via mutex internally, and the * only needed external thing is the 'canGeneratePixmap' method * that tells if the generator is free (since we don't want an * internal queue to store PixmapRequests). A generatedPixmap call * without the 'ready' flag set, results in undefined behavior. * So, as example, printing while generating a pixmap asynchronously is safe, * it might only block the gui thread by 1) waiting for the mutex to unlock * in async thread and 2) doing the 'heavy' print operation. */ OKULAR_EXPORT_PLUGIN(PDFGenerator, "libokularGenerator_poppler.json") static void PDFGeneratorPopplerDebugFunction(const QString &message, const QVariant &closure) { Q_UNUSED(closure); qCDebug(OkularPdfDebug) << "[Poppler]" << message; } PDFGenerator::PDFGenerator( QObject *parent, const QVariantList &args ) : Generator( parent, args ), pdfdoc( 0 ), docSynopsisDirty( true ), docEmbeddedFilesDirty( true ), nextFontPage( 0 ), annotProxy( 0 ) { setFeature( Threaded ); setFeature( TextExtraction ); setFeature( FontInfo ); #ifdef Q_OS_WIN32 setFeature( PrintNative ); #else setFeature( PrintPostscript ); #endif if ( Okular::FilePrinter::ps2pdfAvailable() ) setFeature( PrintToFile ); setFeature( ReadRawData ); setFeature( TiledRendering ); + setFeature( SwapBackingFile ); // You only need to do it once not for each of the documents but it is cheap enough // so doing it all the time won't hurt either Poppler::setDebugErrorFunction(PDFGeneratorPopplerDebugFunction, QVariant()); } PDFGenerator::~PDFGenerator() { delete pdfOptionsPage; } //BEGIN Generator inherited functions Okular::Document::OpenResult PDFGenerator::loadDocumentWithPassword( const QString & filePath, QVector & pagesVector, const QString &password ) { #ifndef NDEBUG if ( pdfdoc ) { qCDebug(OkularPdfDebug) << "PDFGenerator: multiple calls to loadDocument. Check it."; return Okular::Document::OpenError; } #endif // create PDFDoc for the given file pdfdoc = Poppler::Document::load( filePath, 0, 0 ); return init(pagesVector, password); } Okular::Document::OpenResult PDFGenerator::loadDocumentFromDataWithPassword( const QByteArray & fileData, QVector & pagesVector, const QString &password ) { #ifndef NDEBUG if ( pdfdoc ) { qCDebug(OkularPdfDebug) << "PDFGenerator: multiple calls to loadDocument. Check it."; return Okular::Document::OpenError; } #endif // create PDFDoc for the given file pdfdoc = Poppler::Document::loadFromData( fileData, 0, 0 ); return init(pagesVector, password); } Okular::Document::OpenResult PDFGenerator::init(QVector & pagesVector, const QString &password) { if ( !pdfdoc ) return Okular::Document::OpenError; if ( pdfdoc->isLocked() ) { pdfdoc->unlock( password.toLatin1(), password.toLatin1() ); if ( pdfdoc->isLocked() ) { delete pdfdoc; pdfdoc = 0; return Okular::Document::OpenNeedsPassword; } } // build Pages (currentPage was set -1 by deletePages) int pageCount = pdfdoc->numPages(); if (pageCount < 0) { delete pdfdoc; pdfdoc = 0; return Okular::Document::OpenError; } pagesVector.resize(pageCount); rectsGenerated.fill(false, pageCount); - annotationsHash.clear(); + annotationsOnOpenHash.clear(); loadPages(pagesVector, 0, false); // update the configuration reparseConfig(); // create annotation proxy - annotProxy = new PopplerAnnotationProxy( pdfdoc, userMutex() ); + annotProxy = new PopplerAnnotationProxy( pdfdoc, userMutex(), &annotationsOnOpenHash ); // the file has been loaded correctly return Okular::Document::OpenSuccess; } +PDFGenerator::SwapBackingFileResult PDFGenerator::swapBackingFile( QString const &newFileName, QVector & newPagesVector ) +{ + doCloseDocument(); + auto openResult = loadDocumentWithPassword(newFileName, newPagesVector, QString()); + if (openResult != Okular::Document::OpenSuccess) + return SwapBackingFileError; + + return SwapBackingFileReloadInternalData; +} + bool PDFGenerator::doCloseDocument() { // remove internal objects userMutex()->lock(); delete annotProxy; annotProxy = 0; delete pdfdoc; pdfdoc = 0; userMutex()->unlock(); docSynopsisDirty = true; docSyn.clear(); docEmbeddedFilesDirty = true; qDeleteAll(docEmbeddedFiles); docEmbeddedFiles.clear(); nextFontPage = 0; rectsGenerated.clear(); return true; } void PDFGenerator::loadPages(QVector &pagesVector, int rotation, bool clear) { // TODO XPDF 3.01 check const int count = pagesVector.count(); double w = 0, h = 0; for ( int i = 0; i < count ; i++ ) { // get xpdf page Poppler::Page * p = pdfdoc->page( i ); Okular::Page * page; if (p) { const QSizeF pSize = p->pageSizeF(); w = pSize.width() / 72.0 * dpi().width(); h = pSize.height() / 72.0 * dpi().height(); Okular::Rotation orientation = Okular::Rotation0; switch (p->orientation()) { case Poppler::Page::Landscape: orientation = Okular::Rotation90; break; case Poppler::Page::UpsideDown: orientation = Okular::Rotation180; break; case Poppler::Page::Seascape: orientation = Okular::Rotation270; break; case Poppler::Page::Portrait: orientation = Okular::Rotation0; break; } if (rotation % 2 == 1) qSwap(w,h); // init a Okular::page, add transition and annotation information page = new Okular::Page( i, w, h, orientation ); addTransition( p, page ); if ( true ) //TODO real check addAnnotations( p, page ); Poppler::Link * tmplink = p->action( Poppler::Page::Opening ); if ( tmplink ) { page->setPageAction( Okular::Page::Opening, createLinkFromPopplerLink( tmplink ) ); } tmplink = p->action( Poppler::Page::Closing ); if ( tmplink ) { page->setPageAction( Okular::Page::Closing, createLinkFromPopplerLink( tmplink ) ); } page->setDuration( p->duration() ); page->setLabel( p->label() ); addFormFields( p, page ); // kWarning(PDFDebug).nospace() << page->width() << "x" << page->height(); #ifdef PDFGENERATOR_DEBUG qCDebug(OkularPdfDebug) << "load page" << i << "with rotation" << rotation << "and orientation" << orientation; #endif delete p; if (clear && pagesVector[i]) delete pagesVector[i]; } else { page = new Okular::Page( i, defaultPageWidth, defaultPageHeight, Okular::Rotation0 ); } // set the Okular::page at the right position in document's pages vector pagesVector[i] = page; } } Okular::DocumentInfo PDFGenerator::generateDocumentInfo( const QSet &keys ) const { Okular::DocumentInfo docInfo; docInfo.set( Okular::DocumentInfo::MimeType, QStringLiteral("application/pdf") ); userMutex()->lock(); if ( pdfdoc ) { // compile internal structure reading properties from PDFDoc if ( keys.contains( Okular::DocumentInfo::Title ) ) docInfo.set( Okular::DocumentInfo::Title, pdfdoc->info(QStringLiteral("Title")) ); if ( keys.contains( Okular::DocumentInfo::Subject ) ) docInfo.set( Okular::DocumentInfo::Subject, pdfdoc->info(QStringLiteral("Subject")) ); if ( keys.contains( Okular::DocumentInfo::Author ) ) docInfo.set( Okular::DocumentInfo::Author, pdfdoc->info(QStringLiteral("Author")) ); if ( keys.contains( Okular::DocumentInfo::Keywords ) ) docInfo.set( Okular::DocumentInfo::Keywords, pdfdoc->info(QStringLiteral("Keywords")) ); if ( keys.contains( Okular::DocumentInfo::Creator ) ) docInfo.set( Okular::DocumentInfo::Creator, pdfdoc->info(QStringLiteral("Creator")) ); if ( keys.contains( Okular::DocumentInfo::Producer ) ) docInfo.set( Okular::DocumentInfo::Producer, pdfdoc->info(QStringLiteral("Producer")) ); if ( keys.contains( Okular::DocumentInfo::CreationDate ) ) docInfo.set( Okular::DocumentInfo::CreationDate, QLocale().toString( pdfdoc->date(QStringLiteral("CreationDate")), QLocale::LongFormat ) ); if ( keys.contains( Okular::DocumentInfo::ModificationDate ) ) docInfo.set( Okular::DocumentInfo::ModificationDate, QLocale().toString( pdfdoc->date(QStringLiteral("ModDate")), QLocale::LongFormat ) ); if ( keys.contains( Okular::DocumentInfo::CustomKeys ) ) { int major, minor; pdfdoc->getPdfVersion(&major, &minor); docInfo.set( QStringLiteral("format"), i18nc( "PDF v. ", "PDF v. %1.%2", major, minor ), i18n( "Format" ) ); docInfo.set( QStringLiteral("encryption"), pdfdoc->isEncrypted() ? i18n( "Encrypted" ) : i18n( "Unencrypted" ), i18n("Security") ); docInfo.set( QStringLiteral("optimization"), pdfdoc->isLinearized() ? i18n( "Yes" ) : i18n( "No" ), i18n("Optimized") ); } docInfo.set( Okular::DocumentInfo::Pages, QString::number( pdfdoc->numPages() ) ); } userMutex()->unlock(); return docInfo; } const Okular::DocumentSynopsis * PDFGenerator::generateDocumentSynopsis() { if ( !docSynopsisDirty ) return &docSyn; if ( !pdfdoc ) return NULL; userMutex()->lock(); QDomDocument *toc = pdfdoc->toc(); userMutex()->unlock(); if ( !toc ) return NULL; addSynopsisChildren(toc, &docSyn); delete toc; docSynopsisDirty = false; return &docSyn; } static Okular::FontInfo::FontType convertPopplerFontInfoTypeToOkularFontInfoType( Poppler::FontInfo::Type type ) { switch ( type ) { case Poppler::FontInfo::Type1: return Okular::FontInfo::Type1; break; case Poppler::FontInfo::Type1C: return Okular::FontInfo::Type1C; break; case Poppler::FontInfo::Type3: return Okular::FontInfo::Type3; break; case Poppler::FontInfo::TrueType: return Okular::FontInfo::TrueType; break; case Poppler::FontInfo::CIDType0: return Okular::FontInfo::CIDType0; break; case Poppler::FontInfo::CIDType0C: return Okular::FontInfo::CIDType0C; break; case Poppler::FontInfo::CIDTrueType: return Okular::FontInfo::CIDTrueType; break; case Poppler::FontInfo::Type1COT: return Okular::FontInfo::Type1COT; break; case Poppler::FontInfo::TrueTypeOT: return Okular::FontInfo::TrueTypeOT; break; case Poppler::FontInfo::CIDType0COT: return Okular::FontInfo::CIDType0COT; break; case Poppler::FontInfo::CIDTrueTypeOT: return Okular::FontInfo::CIDTrueTypeOT; break; case Poppler::FontInfo::unknown: default: ; } return Okular::FontInfo::Unknown; } static Okular::FontInfo::EmbedType embedTypeForPopplerFontInfo( const Poppler::FontInfo &fi ) { Okular::FontInfo::EmbedType ret = Okular::FontInfo::NotEmbedded; if ( fi.isEmbedded() ) { if ( fi.isSubset() ) { ret = Okular::FontInfo::EmbeddedSubset; } else { ret = Okular::FontInfo::FullyEmbedded; } } return ret; } Okular::FontInfo::List PDFGenerator::fontsForPage( int page ) { Okular::FontInfo::List list; if ( page != nextFontPage ) return list; QList fonts; userMutex()->lock(); Poppler::FontIterator* it = pdfdoc->newFontIterator(page); if (it->hasNext()) { fonts = it->next(); } userMutex()->unlock(); foreach (const Poppler::FontInfo &font, fonts) { Okular::FontInfo of; of.setName( font.name() ); of.setType( convertPopplerFontInfoTypeToOkularFontInfoType( font.type() ) ); of.setEmbedType( embedTypeForPopplerFontInfo( font) ); of.setFile( font.file() ); of.setCanBeExtracted( of.embedType() != Okular::FontInfo::NotEmbedded ); QVariant nativeId; nativeId.setValue( font ); of.setNativeId( nativeId ); list.append( of ); } ++nextFontPage; return list; } const QList *PDFGenerator::embeddedFiles() const { if (docEmbeddedFilesDirty) { userMutex()->lock(); const QList &popplerFiles = pdfdoc->embeddedFiles(); foreach(Poppler::EmbeddedFile* pef, popplerFiles) { docEmbeddedFiles.append(new PDFEmbeddedFile(pef)); } userMutex()->unlock(); docEmbeddedFilesDirty = false; } return &docEmbeddedFiles; } QAbstractItemModel* PDFGenerator::layersModel() const { return pdfdoc->hasOptionalContent() ? pdfdoc->optionalContentModel() : NULL; } void PDFGenerator::opaqueAction( const Okular::BackendOpaqueAction *action ) { #ifdef HAVE_POPPLER_0_50 const Poppler::LinkOCGState *popplerLink = action->nativeId().value(); pdfdoc->optionalContentModel()->applyLink( const_cast< Poppler::LinkOCGState* >( popplerLink ) ); #else (void)action; #endif } bool PDFGenerator::isAllowed( Okular::Permission permission ) const { bool b = true; switch ( permission ) { case Okular::AllowModify: b = pdfdoc->okToChange(); break; case Okular::AllowCopy: b = pdfdoc->okToCopy(); break; case Okular::AllowPrint: b = pdfdoc->okToPrint(); break; case Okular::AllowNotes: b = pdfdoc->okToAddNotes(); break; case Okular::AllowFillForms: b = pdfdoc->okToFillForm(); break; default: ; } return b; } #ifdef HAVE_POPPLER_0_62 struct PartialUpdatePayload { PartialUpdatePayload(PDFGenerator *g, Okular::PixmapRequest *r) : generator(g), request(r) { // Don't report partial updates for the first 500 ms timer.setInterval(500); timer.setSingleShot(true); timer.start(); } PDFGenerator *generator; Okular::PixmapRequest *request; QTimer timer; }; Q_DECLARE_METATYPE(PartialUpdatePayload*) static bool shouldDoPartialUpdateCallback(const QVariant &vPayload) { auto payload = vPayload.value(); // Since the timer lives in a thread without an event loop we need to stop it ourselves // when the remaining time has reached 0 if (payload->timer.isActive() && payload->timer.remainingTime() == 0) { payload->timer.stop(); } return !payload->timer.isActive(); } static void partialUpdateCallback(const QImage &image, const QVariant &vPayload) { auto payload = vPayload.value(); QMetaObject::invokeMethod(payload->generator, "signalPartialPixmapRequest", Qt::QueuedConnection, Q_ARG(Okular::PixmapRequest*, payload->request), Q_ARG(QImage, image)); } #endif QImage PDFGenerator::image( Okular::PixmapRequest * request ) { // debug requests to this (xpdf) generator //qCDebug(OkularPdfDebug) << "id: " << request->id << " is requesting " << (request->async ? "ASYNC" : "sync") << " pixmap for page " << request->page->number() << " [" << request->width << " x " << request->height << "]."; // compute dpi used to get an image with desired width and height Okular::Page * page = request->page(); double pageWidth = page->width(), pageHeight = page->height(); if ( page->rotation() % 2 ) qSwap( pageWidth, pageHeight ); qreal fakeDpiX = request->width() / pageWidth * dpi().width(); qreal fakeDpiY = request->height() / pageHeight * dpi().height(); // generate links rects only the first time bool genObjectRects = !rectsGenerated.at( page->number() ); // 0. LOCK [waits for the thread end] userMutex()->lock(); // 1. Set OutputDev parameters and Generate contents // note: thread safety is set on 'false' for the GUI (this) thread Poppler::Page *p = pdfdoc->page(page->number()); // 2. Take data from outputdev and attach it to the Page QImage img; if (p) { if ( request->isTile() ) { QRect rect = request->normalizedRect().geometry( request->width(), request->height() ); #ifdef HAVE_POPPLER_0_62 if ( request->partialUpdatesWanted() ) { PartialUpdatePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0, partialUpdateCallback, shouldDoPartialUpdateCallback, QVariant::fromValue( &payload ) ); } else #endif { img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0 ); } } else { #ifdef HAVE_POPPLER_0_62 if ( request->partialUpdatesWanted() ) { PartialUpdatePayload payload(this, request); img = p->renderToImage( fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0, partialUpdateCallback, shouldDoPartialUpdateCallback, QVariant::fromValue( &payload ) ); } else #endif { img = p->renderToImage(fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0 ); } } } else { img = QImage( request->width(), request->height(), QImage::Format_Mono ); img.fill( Qt::white ); } if ( p && genObjectRects ) { // TODO previously we extracted Image type rects too, but that needed porting to poppler // and as we are not doing anything with Image type rects i did not port it, have a look at // dead gp_outputdev.cpp on image extraction page->setObjectRects( generateLinks(p->links()) ); rectsGenerated[ request->page()->number() ] = true; resolveMediaLinkReferences( page ); } // 3. UNLOCK [re-enables shared access] userMutex()->unlock(); delete p; return img; } template void resolveMediaLinks( Okular::Action *action, enum Okular::Annotation::SubType subType, QHash &annotationsHash ) { OkularLinkType *okularAction = static_cast( action ); const PopplerLinkType *popplerLink = action->nativeId().value(); QHashIterator it( annotationsHash ); while ( it.hasNext() ) { it.next(); if ( it.key()->subType() == subType ) { const PopplerAnnotationType *popplerAnnotation = static_cast( it.value() ); if ( popplerLink->isReferencedAnnotation( popplerAnnotation ) ) { okularAction->setAnnotation( static_cast( it.key() ) ); okularAction->setNativeId( QVariant() ); delete popplerLink; // delete the associated Poppler::LinkMovie object, it's not needed anymore break; } } } } void PDFGenerator::resolveMediaLinkReference( Okular::Action *action ) { if ( !action ) return; if ( (action->actionType() != Okular::Action::Movie) && (action->actionType() != Okular::Action::Rendition) ) return; - resolveMediaLinks( action, Okular::Annotation::AMovie, annotationsHash ); - resolveMediaLinks( action, Okular::Annotation::AScreen, annotationsHash ); + resolveMediaLinks( action, Okular::Annotation::AMovie, annotationsOnOpenHash ); + resolveMediaLinks( action, Okular::Annotation::AScreen, annotationsOnOpenHash ); } void PDFGenerator::resolveMediaLinkReferences( Okular::Page *page ) { resolveMediaLinkReference( const_cast( page->pageAction( Okular::Page::Opening ) ) ); resolveMediaLinkReference( const_cast( page->pageAction( Okular::Page::Closing ) ) ); foreach ( Okular::Annotation *annotation, page->annotations() ) { if ( annotation->subType() == Okular::Annotation::AScreen ) { Okular::ScreenAnnotation *screenAnnotation = static_cast( annotation ); resolveMediaLinkReference( screenAnnotation->additionalAction( Okular::Annotation::PageOpening ) ); resolveMediaLinkReference( screenAnnotation->additionalAction( Okular::Annotation::PageClosing ) ); } if ( annotation->subType() == Okular::Annotation::AWidget ) { Okular::WidgetAnnotation *widgetAnnotation = static_cast( annotation ); resolveMediaLinkReference( widgetAnnotation->additionalAction( Okular::Annotation::PageOpening ) ); resolveMediaLinkReference( widgetAnnotation->additionalAction( Okular::Annotation::PageClosing ) ); } } foreach ( Okular::FormField *field, page->formFields() ) resolveMediaLinkReference( field->activationAction() ); } Okular::TextPage* PDFGenerator::textPage( Okular::Page *page ) { #ifdef PDFGENERATOR_DEBUG qCDebug(OkularPdfDebug) << "page" << page->number(); #endif // build a TextList... QList textList; double pageWidth, pageHeight; Poppler::Page *pp = pdfdoc->page( page->number() ); if (pp) { userMutex()->lock(); textList = pp->textList(); userMutex()->unlock(); QSizeF s = pp->pageSizeF(); pageWidth = s.width(); pageHeight = s.height(); delete pp; } else { pageWidth = defaultPageWidth; pageHeight = defaultPageHeight; } Okular::TextPage *tp = abstractTextPage(textList, pageHeight, pageWidth, (Poppler::Page::Rotation)page->orientation()); qDeleteAll(textList); return tp; } void PDFGenerator::requestFontData(const Okular::FontInfo &font, QByteArray *data) { Poppler::FontInfo fi = font.nativeId().value(); *data = pdfdoc->fontData(fi); } #define DUMMY_QPRINTER_COPY bool PDFGenerator::print( QPrinter& printer ) { bool printAnnots = true; bool forceRasterize = false; if ( pdfOptionsPage ) { printAnnots = pdfOptionsPage->printAnnots(); forceRasterize = pdfOptionsPage->printForceRaster(); } #ifdef Q_OS_WIN // Windows can only print by rasterization, because that is // currently the only way Okular implements printing without using UNIX-specific // tools like 'lpr'. forceRasterize = true; #ifndef HAVE_POPPLER_0_60 // The Document::HideAnnotations flags was introduced in poppler 0.60 printAnnots = true; #endif #endif #ifdef HAVE_POPPLER_0_60 if ( forceRasterize ) { pdfdoc->setRenderHint(Poppler::Document::HideAnnotations, !printAnnots); #else if ( forceRasterize && printAnnots) { #endif QPainter painter; painter.begin(&printer); QList pageList = Okular::FilePrinter::pageList( printer, pdfdoc->numPages(), document()->currentPage() + 1, document()->bookmarkedPageList() ); for ( int i = 0; i < pageList.count(); ++i ) { if ( i != 0 ) printer.newPage(); const int page = pageList.at( i ) - 1; userMutex()->lock(); Poppler::Page *pp = pdfdoc->page( page ); if (pp) { #ifdef Q_OS_WIN QImage img = pp->renderToImage( printer.physicalDpiX(), printer.physicalDpiY() ); #else // UNIX: Same resolution as the postscript rasterizer; see discussion at https://git.reviewboard.kde.org/r/130218/ QImage img = pp->renderToImage( 300, 300 ); #endif painter.drawImage( painter.window(), img, QRectF(0, 0, img.width(), img.height()) ); delete pp; } userMutex()->unlock(); } painter.end(); return true; } #ifdef DUMMY_QPRINTER_COPY // Get the real page size to pass to the ps generator QPrinter dummy( QPrinter::PrinterResolution ); dummy.setFullPage( true ); dummy.setOrientation( printer.orientation() ); dummy.setPageSize( printer.pageSize() ); dummy.setPaperSize( printer.paperSize( QPrinter::Millimeter ), QPrinter::Millimeter ); int width = dummy.width(); int height = dummy.height(); #else int width = printer.width(); int height = printer.height(); #endif if (width <= 0 || height <= 0) { lastPrintError = InvalidPageSizePrintError; return false; } // Create the tempfile to send to FilePrinter, which will manage the deletion QTemporaryFile tf(QDir::tempPath() + QLatin1String("/okular_XXXXXX.ps")); if ( !tf.open() ) { lastPrintError = TemporaryFileOpenPrintError; return false; } QString tempfilename = tf.fileName(); // Generate the list of pages to be printed as selected in the print dialog QList pageList = Okular::FilePrinter::pageList( printer, pdfdoc->numPages(), document()->currentPage() + 1, document()->bookmarkedPageList() ); // TODO rotation tf.setAutoRemove(false); QString pstitle = metaData(QStringLiteral("Title"), QVariant()).toString(); if ( pstitle.trimmed().isEmpty() ) { pstitle = document()->currentDocument().fileName(); } Poppler::PSConverter *psConverter = pdfdoc->psConverter(); psConverter->setOutputDevice(&tf); psConverter->setPageList(pageList); psConverter->setPaperWidth(width); psConverter->setPaperHeight(height); psConverter->setRightMargin(0); psConverter->setBottomMargin(0); psConverter->setLeftMargin(0); psConverter->setTopMargin(0); psConverter->setStrictMargins(false); psConverter->setForceRasterize(forceRasterize); psConverter->setTitle(pstitle); if (!printAnnots) psConverter->setPSOptions(psConverter->psOptions() | Poppler::PSConverter::HideAnnotations ); userMutex()->lock(); if (psConverter->convert()) { userMutex()->unlock(); delete psConverter; tf.close(); int ret = Okular::FilePrinter::printFile( printer, tempfilename, document()->orientation(), Okular::FilePrinter::SystemDeletesFiles, Okular::FilePrinter::ApplicationSelectsPages, document()->bookmarkedPageRange() ); lastPrintError = Okular::FilePrinter::printError( ret ); return (lastPrintError == NoPrintError); } else { lastPrintError = FileConversionPrintError; delete psConverter; userMutex()->unlock(); } tf.close(); return false; } QVariant PDFGenerator::metaData( const QString & key, const QVariant & option ) const { if ( key == QLatin1String("StartFullScreen") ) { QMutexLocker ml(userMutex()); // asking for the 'start in fullscreen mode' (pdf property) if ( pdfdoc->pageMode() == Poppler::Document::FullScreen ) return true; } else if ( key == QLatin1String("NamedViewport") && !option.toString().isEmpty() ) { Okular::DocumentViewport viewport; QString optionString = option.toString(); // asking for the page related to a 'named link destination'. the // option is the link name. @see addSynopsisChildren. userMutex()->lock(); Poppler::LinkDestination *ld = pdfdoc->linkDestination( optionString ); userMutex()->unlock(); if ( ld ) { fillViewportFromLinkDestination( viewport, *ld ); } delete ld; if ( viewport.pageNumber >= 0 ) return viewport.toString(); } else if ( key == QLatin1String("DocumentTitle") ) { userMutex()->lock(); QString title = pdfdoc->info( QStringLiteral("Title") ); userMutex()->unlock(); return title; } else if ( key == QLatin1String("OpenTOC") ) { QMutexLocker ml(userMutex()); if ( pdfdoc->pageMode() == Poppler::Document::UseOutlines ) return true; } else if ( key == QLatin1String("DocumentScripts") && option.toString() == QLatin1String("JavaScript") ) { QMutexLocker ml(userMutex()); return pdfdoc->scripts(); } else if ( key == QLatin1String("HasUnsupportedXfaForm") ) { QMutexLocker ml(userMutex()); return pdfdoc->formType() == Poppler::Document::XfaForm; } else if ( key == QLatin1String("FormCalculateOrder") ) { #ifdef HAVE_POPPLER_0_53 QMutexLocker ml(userMutex()); return QVariant::fromValue>(pdfdoc->formCalculateOrder()); #endif } return QVariant(); } bool PDFGenerator::reparseConfig() { if ( !pdfdoc ) return false; bool somethingchanged = false; // load paper color QColor color = documentMetaData( PaperColorMetaData, true ).value< QColor >(); // if paper color is changed we have to rebuild every visible pixmap in addition // to the outputDevice. it's the 'heaviest' case, other effect are just recoloring // over the page rendered on 'standard' white background. if ( color != pdfdoc->paperColor() ) { userMutex()->lock(); pdfdoc->setPaperColor(color); userMutex()->unlock(); somethingchanged = true; } bool aaChanged = setDocumentRenderHints(); somethingchanged = somethingchanged || aaChanged; return somethingchanged; } void PDFGenerator::addPages( KConfigDialog *dlg ) { #ifdef HAVE_POPPLER_0_24 Ui_PDFSettingsWidget pdfsw; QWidget* w = new QWidget(dlg); pdfsw.setupUi(w); dlg->addPage(w, PDFSettings::self(), i18n("PDF"), QStringLiteral("application-pdf"), i18n("PDF Backend Configuration") ); #endif } bool PDFGenerator::setDocumentRenderHints() { bool changed = false; const Poppler::Document::RenderHints oldhints = pdfdoc->renderHints(); #define SET_HINT(hintname, hintdefvalue, hintflag) \ { \ bool newhint = documentMetaData(hintname, hintdefvalue).toBool(); \ if (newhint != oldhints.testFlag(hintflag)) \ { \ pdfdoc->setRenderHint(hintflag, newhint); \ changed = true; \ } \ } SET_HINT(GraphicsAntialiasMetaData, true, Poppler::Document::Antialiasing) SET_HINT(TextAntialiasMetaData, true, Poppler::Document::TextAntialiasing) SET_HINT(TextHintingMetaData, false, Poppler::Document::TextHinting) #undef SET_HINT #ifdef HAVE_POPPLER_0_24 // load thin line mode const int thinLineMode = PDFSettings::enhanceThinLines(); const bool enableThinLineSolid = thinLineMode == PDFSettings::EnumEnhanceThinLines::Solid; const bool enableShapeLineSolid = thinLineMode == PDFSettings::EnumEnhanceThinLines::Shape; const bool thinLineSolidWasEnabled = (oldhints & Poppler::Document::ThinLineSolid) == Poppler::Document::ThinLineSolid; const bool thinLineShapeWasEnabled = (oldhints & Poppler::Document::ThinLineShape) == Poppler::Document::ThinLineShape; if (enableThinLineSolid != thinLineSolidWasEnabled) { pdfdoc->setRenderHint(Poppler::Document::ThinLineSolid, enableThinLineSolid); changed = true; } if (enableShapeLineSolid != thinLineShapeWasEnabled) { pdfdoc->setRenderHint(Poppler::Document::ThinLineShape, enableShapeLineSolid); changed = true; } #endif return changed; } Okular::ExportFormat::List PDFGenerator::exportFormats() const { static Okular::ExportFormat::List formats; if ( formats.isEmpty() ) { formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::PlainText ) ); } return formats; } bool PDFGenerator::exportTo( const QString &fileName, const Okular::ExportFormat &format ) { if ( format.mimeType().inherits( QStringLiteral( "text/plain" ) ) ) { QFile f( fileName ); if ( !f.open( QIODevice::WriteOnly ) ) return false; QTextStream ts( &f ); int num = document()->pages(); for ( int i = 0; i < num; ++i ) { QString text; userMutex()->lock(); Poppler::Page *pp = pdfdoc->page(i); if (pp) { text = pp->text(QRect()).normalized(QString::NormalizationForm_KC); } userMutex()->unlock(); ts << text; delete pp; } f.close(); return true; } return false; } //END Generator inherited functions inline void append (Okular::TextPage* ktp, const QString &s, double l, double b, double r, double t) { // kWarning(PDFDebug).nospace() << "text: " << s << " at (" << l << "," << t << ")x(" << r <<","<append(s, new Okular::NormalizedRect(l, t, r, b)); } Okular::TextPage * PDFGenerator::abstractTextPage(const QList &text, double height, double width,int rot) { Q_UNUSED(rot); Okular::TextPage* ktp=new Okular::TextPage; Poppler::TextBox *next; #ifdef PDFGENERATOR_DEBUG qCDebug(OkularPdfDebug) << "getting text page in generator pdf - rotation:" << rot; #endif QString s; bool addChar; foreach (Poppler::TextBox *word, text) { const int qstringCharCount = word->text().length(); next=word->nextWord(); int textBoxChar = 0; for (int j = 0; j < qstringCharCount; j++) { const QChar c = word->text().at(j); if (c.isHighSurrogate()) { s = c; addChar = false; } else if (c.isLowSurrogate()) { s += c; addChar = true; } else { s = c; addChar = true; } if (addChar) { QRectF charBBox = word->charBoundingBox(textBoxChar); append(ktp, (j==qstringCharCount-1 && !next) ? (s + QLatin1Char('\n')) : s, charBBox.left()/width, charBBox.bottom()/height, charBBox.right()/width, charBBox.top()/height); textBoxChar++; } } if ( word->hasSpaceAfter() && next ) { // TODO Check with a document with vertical text // probably won't work and we will need to do comparisons // between wordBBox and nextWordBBox to see if they are // vertically or horizontally aligned QRectF wordBBox = word->boundingBox(); QRectF nextWordBBox = next->boundingBox(); append(ktp, QStringLiteral(" "), wordBBox.right()/width, wordBBox.bottom()/height, nextWordBBox.left()/width, wordBBox.top()/height); } } return ktp; } void PDFGenerator::addSynopsisChildren( QDomNode * parent, QDomNode * parentDestination ) { // keep track of the current listViewItem QDomNode n = parent->firstChild(); while( !n.isNull() ) { // convert the node to an element (sure it is) QDomElement e = n.toElement(); // The name is the same QDomElement item = docSyn.createElement( e.tagName() ); parentDestination->appendChild(item); if (!e.attribute(QStringLiteral("ExternalFileName")).isNull()) item.setAttribute(QStringLiteral("ExternalFileName"), e.attribute(QStringLiteral("ExternalFileName"))); if (!e.attribute(QStringLiteral("DestinationName")).isNull()) item.setAttribute(QStringLiteral("ViewportName"), e.attribute(QStringLiteral("DestinationName"))); if (!e.attribute(QStringLiteral("Destination")).isNull()) { Okular::DocumentViewport vp; fillViewportFromLinkDestination( vp, Poppler::LinkDestination(e.attribute(QStringLiteral("Destination"))) ); item.setAttribute( QStringLiteral("Viewport"), vp.toString() ); } if (!e.attribute(QStringLiteral("Open")).isNull()) item.setAttribute(QStringLiteral("Open"), e.attribute(QStringLiteral("Open"))); if (!e.attribute(QStringLiteral("DestinationURI")).isNull()) item.setAttribute(QStringLiteral("URL"), e.attribute(QStringLiteral("DestinationURI"))); // descend recursively and advance to the next node if ( e.hasChildNodes() ) addSynopsisChildren( &n, & item ); n = n.nextSibling(); } } void PDFGenerator::addAnnotations( Poppler::Page * popplerPage, Okular::Page * page ) { #ifdef HAVE_POPPLER_0_28 QSet subtypes; subtypes << Poppler::Annotation::AFileAttachment << Poppler::Annotation::ASound << Poppler::Annotation::AMovie << Poppler::Annotation::AWidget << Poppler::Annotation::AScreen << Poppler::Annotation::AText << Poppler::Annotation::ALine << Poppler::Annotation::AGeom << Poppler::Annotation::AHighlight << Poppler::Annotation::AInk << Poppler::Annotation::AStamp << Poppler::Annotation::ACaret; QList popplerAnnotations = popplerPage->annotations( subtypes ); #else QList popplerAnnotations = popplerPage->annotations(); #endif foreach(Poppler::Annotation *a, popplerAnnotations) { bool doDelete = true; Okular::Annotation * newann = createAnnotationFromPopplerAnnotation( a, &doDelete ); if (newann) { page->addAnnotation(newann); if ( a->subType() == Poppler::Annotation::AScreen ) { Poppler::ScreenAnnotation *annotScreen = static_cast( a ); Okular::ScreenAnnotation *screenAnnotation = static_cast( newann ); // The activation action const Poppler::Link *actionLink = annotScreen->action(); if ( actionLink ) screenAnnotation->setAction( createLinkFromPopplerLink( actionLink ) ); // The additional actions const Poppler::Link *pageOpeningLink = annotScreen->additionalAction( Poppler::Annotation::PageOpeningAction ); if ( pageOpeningLink ) screenAnnotation->setAdditionalAction( Okular::Annotation::PageOpening, createLinkFromPopplerLink( pageOpeningLink ) ); const Poppler::Link *pageClosingLink = annotScreen->additionalAction( Poppler::Annotation::PageClosingAction ); if ( pageClosingLink ) screenAnnotation->setAdditionalAction( Okular::Annotation::PageClosing, createLinkFromPopplerLink( pageClosingLink ) ); } if ( a->subType() == Poppler::Annotation::AWidget ) { Poppler::WidgetAnnotation *annotWidget = static_cast( a ); Okular::WidgetAnnotation *widgetAnnotation = static_cast( newann ); // The additional actions const Poppler::Link *pageOpeningLink = annotWidget->additionalAction( Poppler::Annotation::PageOpeningAction ); if ( pageOpeningLink ) widgetAnnotation->setAdditionalAction( Okular::Annotation::PageOpening, createLinkFromPopplerLink( pageOpeningLink ) ); const Poppler::Link *pageClosingLink = annotWidget->additionalAction( Poppler::Annotation::PageClosingAction ); if ( pageClosingLink ) widgetAnnotation->setAdditionalAction( Okular::Annotation::PageClosing, createLinkFromPopplerLink( pageClosingLink ) ); } if ( !doDelete ) - annotationsHash.insert( newann, a ); + annotationsOnOpenHash.insert( newann, a ); } if ( doDelete ) delete a; } } void PDFGenerator::addTransition( Poppler::Page * pdfPage, Okular::Page * page ) // called on opening when MUTEX is not used { Poppler::PageTransition *pdfTransition = pdfPage->transition(); if ( !pdfTransition || pdfTransition->type() == Poppler::PageTransition::Replace ) return; Okular::PageTransition *transition = new Okular::PageTransition(); switch ( pdfTransition->type() ) { case Poppler::PageTransition::Replace: // won't get here, added to avoid warning break; case Poppler::PageTransition::Split: transition->setType( Okular::PageTransition::Split ); break; case Poppler::PageTransition::Blinds: transition->setType( Okular::PageTransition::Blinds ); break; case Poppler::PageTransition::Box: transition->setType( Okular::PageTransition::Box ); break; case Poppler::PageTransition::Wipe: transition->setType( Okular::PageTransition::Wipe ); break; case Poppler::PageTransition::Dissolve: transition->setType( Okular::PageTransition::Dissolve ); break; case Poppler::PageTransition::Glitter: transition->setType( Okular::PageTransition::Glitter ); break; case Poppler::PageTransition::Fly: transition->setType( Okular::PageTransition::Fly ); break; case Poppler::PageTransition::Push: transition->setType( Okular::PageTransition::Push ); break; case Poppler::PageTransition::Cover: transition->setType( Okular::PageTransition::Cover ); break; case Poppler::PageTransition::Uncover: transition->setType( Okular::PageTransition::Uncover ); break; case Poppler::PageTransition::Fade: transition->setType( Okular::PageTransition::Fade ); break; } #ifdef HAVE_POPPLER_0_37 transition->setDuration( pdfTransition->durationReal() ); #else transition->setDuration( pdfTransition->duration() ); #endif switch ( pdfTransition->alignment() ) { case Poppler::PageTransition::Horizontal: transition->setAlignment( Okular::PageTransition::Horizontal ); break; case Poppler::PageTransition::Vertical: transition->setAlignment( Okular::PageTransition::Vertical ); break; } switch ( pdfTransition->direction() ) { case Poppler::PageTransition::Inward: transition->setDirection( Okular::PageTransition::Inward ); break; case Poppler::PageTransition::Outward: transition->setDirection( Okular::PageTransition::Outward ); break; } transition->setAngle( pdfTransition->angle() ); transition->setScale( pdfTransition->scale() ); transition->setIsRectangular( pdfTransition->isRectangular() ); page->setTransition( transition ); } void PDFGenerator::addFormFields( Poppler::Page * popplerPage, Okular::Page * page ) { QList popplerFormFields = popplerPage->formFields(); QLinkedList okularFormFields; foreach( Poppler::FormField *f, popplerFormFields ) { Okular::FormField * of = 0; switch ( f->type() ) { case Poppler::FormField::FormButton: of = new PopplerFormFieldButton( static_cast( f ) ); break; case Poppler::FormField::FormText: of = new PopplerFormFieldText( static_cast( f ) ); break; case Poppler::FormField::FormChoice: of = new PopplerFormFieldChoice( static_cast( f ) ); break; default: ; } if ( of ) // form field created, good - it will take care of the Poppler::FormField okularFormFields.append( of ); else // no form field available - delete the Poppler::FormField delete f; } if ( !okularFormFields.isEmpty() ) page->setFormFields( okularFormFields ); } PDFGenerator::PrintError PDFGenerator::printError() const { return lastPrintError; } QWidget* PDFGenerator::printConfigurationWidget() const { if ( !pdfOptionsPage ) { const_cast(this)->pdfOptionsPage = new PDFOptionsPage(); } return pdfOptionsPage; } bool PDFGenerator::supportsOption( SaveOption option ) const { switch ( option ) { case SaveChanges: { return true; } default: ; } return false; } bool PDFGenerator::save( const QString &fileName, SaveOptions options, QString *errorText ) { Q_UNUSED(errorText); Poppler::PDFConverter *pdfConv = pdfdoc->pdfConverter(); pdfConv->setOutputFileName( fileName ); if ( options & SaveChanges ) pdfConv->setPDFOptions( pdfConv->pdfOptions() | Poppler::PDFConverter::WithChanges ); QMutexLocker locker( userMutex() ); + + QHashIterator it( annotationsOnOpenHash ); + while ( it.hasNext() ) + { + it.next(); + + if ( it.value()->uniqueName().isEmpty() ) + { + it.value()->setUniqueName( it.key()->uniqueName() ); + } + } + bool success = pdfConv->convert(); if (!success) { switch (pdfConv->lastError()) { case Poppler::BaseConverter::NotSupportedInputFileError: // This can only happen with Poppler before 0.22 which did not have qt5 version break; case Poppler::BaseConverter::NoError: case Poppler::BaseConverter::FileLockedError: // we can't get here break; case Poppler::BaseConverter::OpenOutputError: // the default text message is good for this case break; } } delete pdfConv; return success; } Okular::AnnotationProxy* PDFGenerator::annotationProxy() const { return annotProxy; } #include "generator_pdf.moc" Q_LOGGING_CATEGORY(OkularPdfDebug, "org.kde.okular.generators.pdf", QtWarningMsg) /* kate: replace-tabs on; indent-width 4; */ diff --git a/generators/poppler/generator_pdf.h b/generators/poppler/generator_pdf.h index a078f50bd..d6ea065ba 100644 --- a/generators/poppler/generator_pdf.h +++ b/generators/poppler/generator_pdf.h @@ -1,150 +1,156 @@ /*************************************************************************** * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2004 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. * ***************************************************************************/ #ifndef _OKULAR_GENERATOR_PDF_H_ #define _OKULAR_GENERATOR_PDF_H_ //#include "synctex/synctex_parser.h" #include #include #include #include #include #include #include #include namespace Okular { class ObjectRect; class SourceReference; } class PDFOptionsPage; class PopplerAnnotationProxy; /** * @short A generator that builds contents from a PDF document. * * All Generator features are supported and implented by this one. * Internally this holds a reference to xpdf's core objects and provides * contents generation using the PDFDoc object and a couple of OutputDevices * called Okular::OutputDev and Okular::TextDev (both defined in gp_outputdev.h). * * For generating page contents we tell PDFDoc to render a page and grab * contents from out OutputDevs when rendering finishes. * */ class PDFGenerator : public Okular::Generator, public Okular::ConfigInterface, public Okular::PrintInterface, public Okular::SaveInterface { Q_OBJECT Q_INTERFACES( Okular::Generator ) Q_INTERFACES( Okular::ConfigInterface ) Q_INTERFACES( Okular::PrintInterface ) Q_INTERFACES( Okular::SaveInterface ) public: PDFGenerator( QObject *parent, const QVariantList &args ); virtual ~PDFGenerator(); // [INHERITED] load a document and fill up the pagesVector Okular::Document::OpenResult loadDocumentWithPassword( const QString & fileName, QVector & pagesVector, const QString & password ) override; Okular::Document::OpenResult loadDocumentFromDataWithPassword( const QByteArray & fileData, QVector & pagesVector, const QString & password ) override; void loadPages(QVector &pagesVector, int rotation=-1, bool clear=false); // [INHERITED] document information Okular::DocumentInfo generateDocumentInfo( const QSet &keys ) const override; const Okular::DocumentSynopsis * generateDocumentSynopsis() override; Okular::FontInfo::List fontsForPage( int page ) override; const QList * embeddedFiles() const override; PageSizeMetric pagesSizeMetric() const override{ return Pixels; } QAbstractItemModel * layersModel() const override; void opaqueAction( const Okular::BackendOpaqueAction *action ) override; // [INHERITED] document information bool isAllowed( Okular::Permission permission ) const override; // [INHERITED] perform actions on document / pages QImage image( Okular::PixmapRequest *page ) override; // [INHERITED] print page using an already configured kprinter bool print( QPrinter& printer ) override; // [INHERITED] reply to some metadata requests QVariant metaData( const QString & key, const QVariant & option ) const override; // [INHERITED] reparse configuration bool reparseConfig() override; void addPages( KConfigDialog * ) override; // [INHERITED] text exporting Okular::ExportFormat::List exportFormats() const override; bool exportTo( const QString &fileName, const Okular::ExportFormat &format ) override; // [INHERITED] print interface QWidget* printConfigurationWidget() const override; // [INHERITED] save interface bool supportsOption( SaveOption ) const override; bool save( const QString &fileName, SaveOptions options, QString *errorText ) override; Okular::AnnotationProxy* annotationProxy() const override; protected: + SwapBackingFileResult swapBackingFile( QString const &newFileName, QVector & newPagesVector ) override; bool doCloseDocument() override; Okular::TextPage* textPage( Okular::Page *page ) override; protected Q_SLOTS: void requestFontData(const Okular::FontInfo &font, QByteArray *data); Okular::Generator::PrintError printError() const; private: Okular::Document::OpenResult init(QVector & pagesVector, const QString &password); // create the document synopsis hieracy void addSynopsisChildren( QDomNode * parentSource, QDomNode * parentDestination ); // fetch annotations from the pdf file and add they to the page void addAnnotations( Poppler::Page * popplerPage, Okular::Page * page ); // fetch the transition information and add it to the page void addTransition( Poppler::Page * popplerPage, Okular::Page * page ); // fetch the form fields and add them to the page void addFormFields( Poppler::Page * popplerPage, Okular::Page * page ); Okular::TextPage * abstractTextPage(const QList &text, double height, double width, int rot); void resolveMediaLinkReferences( Okular::Page *page ); void resolveMediaLinkReference( Okular::Action *action ); bool setDocumentRenderHints(); // poppler dependant stuff Poppler::Document *pdfdoc; // misc variables for document info and synopsis caching bool docSynopsisDirty; Okular::DocumentSynopsis docSyn; mutable bool docEmbeddedFilesDirty; mutable QList docEmbeddedFiles; int nextFontPage; PopplerAnnotationProxy *annotProxy; - QHash annotationsHash; + // the hash below only contains annotations that were present on the file at open time + // this is enough for what we use it for + QHash annotationsOnOpenHash; QBitArray rectsGenerated; QPointer pdfOptionsPage; PrintError lastPrintError; }; #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/part.cpp b/part.cpp index 909f91739..97baedc8a 100644 --- a/part.cpp +++ b/part.cpp @@ -1,3186 +1,3484 @@ /*************************************************************************** * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2002 by Chris Cheney * * Copyright (C) 2002 by Malcolm Hunter * * Copyright (C) 2003-2004 by Christophe Devriese * * * * Copyright (C) 2003 by Daniel Molkentin * * Copyright (C) 2003 by Andy Goossens * * Copyright (C) 2003 by Dirk Mueller * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2004 by Dominique Devriese * * Copyright (C) 2004 by Christoph Cullmann * * Copyright (C) 2004 by Henrique Pinto * * Copyright (C) 2004 by Waldo Bastian * * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2004 by Antti Markus * + * 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 "part.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if PURPOSE_FOUND #include #include #endif #if 0 #include #endif // local includes #include "aboutdata.h" #include "extensions.h" #include "ui/debug_ui.h" #include "ui/drawingtoolactions.h" #include "ui/pageview.h" #include "ui/toc.h" #include "ui/searchwidget.h" #include "ui/thumbnaillist.h" #include "ui/side_reviews.h" #include "ui/minibar.h" #include "ui/embeddedfilesdialog.h" #include "ui/propertiesdialog.h" #include "ui/presentationwidget.h" #include "ui/pagesizelabel.h" #include "ui/bookmarklist.h" #include "ui/findbar.h" #include "ui/sidebar.h" #include "ui/fileprinterpreview.h" #include "ui/guiutils.h" #include "ui/layers.h" #include "ui/okmenutitle.h" #include "conf/preferencesdialog.h" #include "settings.h" #include "core/action.h" #include "core/annotations.h" #include "core/bookmarkmanager.h" #include "core/document.h" #include "core/generator.h" #include "core/page.h" #include "core/fileprinter.h" #include #include class FileKeeper { public: FileKeeper() : m_handle( nullptr ) { } ~FileKeeper() { } void open( const QString & path ) { if ( !m_handle ) m_handle = std::fopen( QFile::encodeName( path ).constData(), "r" ); } void close() { if ( m_handle ) { int ret = std::fclose( m_handle ); Q_UNUSED( ret ) m_handle = nullptr; } } QTemporaryFile* copyToTemporary() const { if ( !m_handle ) return nullptr; QTemporaryFile * retFile = new QTemporaryFile; retFile->open(); std::rewind( m_handle ); int c = -1; do { c = std::fgetc( m_handle ); if ( c == EOF ) break; if ( !retFile->putChar( (char)c ) ) break; } while ( !feof( m_handle ) ); retFile->flush(); return retFile; } private: std::FILE * m_handle; }; K_PLUGIN_FACTORY(OkularPartFactory, registerPlugin();) static QAction* actionForExportFormat( const Okular::ExportFormat& format, QObject *parent = Q_NULLPTR ) { QAction *act = new QAction( format.description(), parent ); if ( !format.icon().isNull() ) { act->setIcon( format.icon() ); } return act; } static KFilterDev::CompressionType compressionTypeFor( const QString& mime_to_check ) { // The compressedMimeMap is here in case you have a very old shared mime database // that doesn't have inheritance info for things like gzeps, etc // Otherwise the "is()" calls below are just good enough static QHash< QString, KFilterDev::CompressionType > compressedMimeMap; static bool supportBzip = false; static bool supportXz = false; const QString app_gzip( QStringLiteral( "application/x-gzip" ) ); const QString app_bzip( QStringLiteral( "application/x-bzip" ) ); const QString app_xz( QStringLiteral( "application/x-xz" ) ); if ( compressedMimeMap.isEmpty() ) { std::unique_ptr< KFilterBase > f; compressedMimeMap[ QLatin1String( "image/x-gzeps" ) ] = KFilterDev::GZip; // check we can read bzip2-compressed files f.reset( KCompressionDevice::filterForCompressionType( KCompressionDevice::BZip2 ) ); if ( f.get() ) { supportBzip = true; compressedMimeMap[ QLatin1String( "application/x-bzpdf" ) ] = KFilterDev::BZip2; compressedMimeMap[ QLatin1String( "application/x-bzpostscript" ) ] = KFilterDev::BZip2; compressedMimeMap[ QLatin1String( "application/x-bzdvi" ) ] = KFilterDev::BZip2; compressedMimeMap[ QLatin1String( "image/x-bzeps" ) ] = KFilterDev::BZip2; } // check if we can read XZ-compressed files f.reset( KCompressionDevice::filterForCompressionType( KCompressionDevice::Xz ) ); if ( f.get() ) { supportXz = true; } } QHash< QString, KFilterDev::CompressionType >::const_iterator it = compressedMimeMap.constFind( mime_to_check ); if ( it != compressedMimeMap.constEnd() ) return it.value(); QMimeDatabase db; QMimeType mime = db.mimeTypeForName( mime_to_check ); if ( mime.isValid() ) { if ( mime.inherits( app_gzip ) ) return KFilterDev::GZip; else if ( supportBzip && mime.inherits( app_bzip ) ) return KFilterDev::BZip2; else if ( supportXz && mime.inherits( app_xz ) ) return KFilterDev::Xz; } return KFilterDev::None; } static Okular::EmbedMode detectEmbedMode( QWidget *parentWidget, QObject *parent, const QVariantList &args ) { Q_UNUSED( parentWidget ); if ( parent && ( parent->objectName().startsWith( QLatin1String( "okular::Shell" ) ) || parent->objectName().startsWith( QLatin1String( "okular/okular__Shell" ) ) ) ) return Okular::NativeShellMode; if ( parent && ( QByteArray( "KHTMLPart" ) == parent->metaObject()->className() ) ) return Okular::KHTMLPartMode; Q_FOREACH ( const QVariant &arg, args ) { if ( arg.type() == QVariant::String ) { if ( arg.toString() == QLatin1String( "Print/Preview" ) ) { return Okular::PrintPreviewMode; } else if ( arg.toString() == QLatin1String( "ViewerWidget" ) ) { return Okular::ViewerWidgetMode; } } } return Okular::UnknownEmbedMode; } static QString detectConfigFileName( const QVariantList &args ) { Q_FOREACH ( const QVariant &arg, args ) { if ( arg.type() == QVariant::String ) { QString argString = arg.toString(); int separatorIndex = argString.indexOf( QStringLiteral("=") ); if ( separatorIndex >= 0 && argString.left( separatorIndex ) == QLatin1String( "ConfigFileName" ) ) { return argString.mid( separatorIndex + 1 ); } } } return QString(); } #undef OKULAR_KEEP_FILE_OPEN #ifdef OKULAR_KEEP_FILE_OPEN static bool keepFileOpen() { static bool keep_file_open = !qgetenv("OKULAR_NO_KEEP_FILE_OPEN").toInt(); return keep_file_open; } #endif int Okular::Part::numberOfParts = 0; namespace Okular { Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList &args) : KParts::ReadWritePart(parent), -m_tempfile( nullptr ), m_isReloading( false ), m_fileWasRemoved( false ), m_showMenuBarAction( nullptr ), m_showFullScreenAction( nullptr ), m_actionsSearched( false ), +m_tempfile( nullptr ), m_documentOpenWithPassword( false ), m_swapInsteadOfOpening( false ), m_isReloading( false ), m_fileWasRemoved( false ), m_showMenuBarAction( nullptr ), m_showFullScreenAction( nullptr ), m_actionsSearched( false ), m_cliPresentation(false), m_cliPrint(false), m_embedMode(detectEmbedMode(parentWidget, parent, args)), m_generatorGuiClient(nullptr), m_keeper( nullptr ) { // make sure that the component name is okular otherwise the XMLGUI .rc files are not found // when this part is used in an application other than okular (e.g. unit tests) setComponentName(QStringLiteral("okular"), QString()); const QLatin1String configFileName("okularpartrc"); // first, we check if a config file name has been specified QString configFilePath = detectConfigFileName( args ); if ( configFilePath.isEmpty() ) { configFilePath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1Char('/') + configFileName; } // Migrate old config if ( !QFile::exists( configFilePath ) ) { qCDebug(OkularUiDebug) << "Did not find a config file, attempting to look for old config"; // Migrate old config + UI Kdelibs4ConfigMigrator configMigrator( componentName() ); // UI file is handled automatically, we only need to specify config name because we're a part configMigrator.setConfigFiles( QStringList( configFileName ) ); // If there's no old okular config to migrate, look for kpdf if ( !configMigrator.migrate() ) { qCDebug(OkularUiDebug) << "Did not find an old okular config file, attempting to look for kpdf config"; // First try the automatic detection, using $KDEHOME etc. Kdelibs4Migration migration; QString kpdfConfig = migration.locateLocal( "config", QStringLiteral("kpdfpartrc") ); // Fallback just in case it tried e. g. ~/.kde4 if ( kpdfConfig.isEmpty() ) { kpdfConfig = QDir::homePath() + QStringLiteral("/.kde/share/config/kpdfpartrc"); } if ( QFile::exists( kpdfConfig ) ) { qCDebug(OkularUiDebug) << "Found old kpdf config" << kpdfConfig << "copying to" << configFilePath; QFile::copy( kpdfConfig, configFilePath ); } else { qCDebug(OkularUiDebug) << "Did not find an old kpdf config file"; } } else { qCDebug(OkularUiDebug) << "Migrated old okular config"; } } Okular::Settings::instance( configFilePath ); numberOfParts++; if (numberOfParts == 1) { m_registerDbusName = QStringLiteral("/okular"); } else { m_registerDbusName = QStringLiteral("/okular%1").arg(numberOfParts); } QDBusConnection::sessionBus().registerObject(m_registerDbusName, this, QDBusConnection::ExportScriptableSlots); // connect the started signal to tell the job the mimetypes we like, // and get some more information from it connect(this, &KParts::ReadOnlyPart::started, this, &Part::slotJobStarted); // connect the completed signal so we can put the window caption when loading remote files connect(this, SIGNAL(completed()), this, SLOT(setWindowTitleFromDocument())); connect(this, &KParts::ReadOnlyPart::canceled, this, &Part::loadCancelled); // create browser extension (for printing when embedded into browser) m_bExtension = new BrowserExtension(this); // create live connect extension (for integrating with browser scripting) new OkularLiveConnectExtension( this ); GuiUtils::addIconLoader( iconLoader() ); m_sidebar = new Sidebar( parentWidget ); setWidget( m_sidebar ); connect( m_sidebar, &Sidebar::urlsDropped, this, &Part::handleDroppedUrls ); // build the document m_document = new Okular::Document(widget()); connect( m_document, &Document::linkFind, this, &Part::slotFind ); connect( m_document, &Document::linkGoToPage, this, &Part::slotGoToPage ); connect( m_document, &Document::linkPresentation, this, &Part::slotShowPresentation ); connect( m_document, &Document::linkEndPresentation, this, &Part::slotHidePresentation ); connect( m_document, &Document::openUrl, this, &Part::openUrlFromDocument ); connect( m_document->bookmarkManager(), &BookmarkManager::openUrl, this, &Part::openUrlFromBookmarks ); connect( m_document, &Document::close, this, &Part::close ); + connect( m_document, &Document::undoHistoryCleanChanged, this, + [this](bool clean) + { + setModified( !clean ); + setWindowTitleFromDocument(); + } + ); if ( parent && parent->metaObject()->indexOfSlot( QMetaObject::normalizedSignature( "slotQuit()" ).constData() ) != -1 ) connect( m_document, SIGNAL(quit()), parent, SLOT(slotQuit()) ); else connect( m_document, &Document::quit, this, &Part::cannotQuit ); // widgets: ^searchbar (toolbar containing label and SearchWidget) // m_searchToolBar = new KToolBar( parentWidget, "searchBar" ); // m_searchToolBar->boxLayout()->setSpacing( KDialog::spacingHint() ); // QLabel * sLabel = new QLabel( i18n( "&Search:" ), m_searchToolBar, "kde toolbar widget" ); // m_searchWidget = new SearchWidget( m_searchToolBar, m_document ); // sLabel->setBuddy( m_searchWidget ); // m_searchToolBar->setStretchableWidget( m_searchWidget ); // [left toolbox: Table of Contents] | [] m_toc = new TOC( nullptr, m_document ); connect( m_toc.data(), &TOC::hasTOC, this, &Part::enableTOC ); connect( m_toc.data(), &TOC::rightClick, this, &Part::slotShowTOCMenu ); m_sidebar->addItem( m_toc, QIcon::fromTheme(QApplication::isLeftToRight() ? QStringLiteral("format-justify-left") : QStringLiteral("format-justify-right")), i18n("Contents") ); enableTOC( false ); // [left toolbox: Layers] | [] m_layers = new Layers( nullptr, m_document ); connect( m_layers.data(), &Layers::hasLayers, this, &Part::enableLayers ); m_sidebar->addItem( m_layers, QIcon::fromTheme( QStringLiteral("draw-freehand") ), i18n( "Layers" ) ); enableLayers( false ); // [left toolbox: Thumbnails and Bookmarks] | [] QWidget * thumbsBox = new ThumbnailsBox( nullptr ); thumbsBox->layout()->setSpacing( 6 ); m_searchWidget = new SearchWidget( thumbsBox, m_document ); thumbsBox->layout()->addWidget(m_searchWidget); m_thumbnailList = new ThumbnailList( thumbsBox, m_document ); thumbsBox->layout()->addWidget(m_thumbnailList); // ThumbnailController * m_tc = new ThumbnailController( thumbsBox, m_thumbnailList ); connect( m_thumbnailList.data(), &ThumbnailList::rightClick, this, &Part::slotShowMenu ); m_sidebar->addItem( thumbsBox, QIcon::fromTheme( QStringLiteral("view-preview") ), i18n("Thumbnails") ); m_sidebar->setCurrentItem( thumbsBox ); // [left toolbox: Reviews] | [] m_reviewsWidget = new Reviews( nullptr, m_document ); m_sidebar->addItem( m_reviewsWidget, QIcon::fromTheme(QStringLiteral("draw-freehand")), i18n("Reviews") ); m_sidebar->setItemEnabled( m_reviewsWidget, false ); // [left toolbox: Bookmarks] | [] m_bookmarkList = new BookmarkList( m_document, nullptr ); m_sidebar->addItem( m_bookmarkList, QIcon::fromTheme(QStringLiteral("bookmarks")), i18n("Bookmarks") ); m_sidebar->setItemEnabled( m_bookmarkList, false ); // widgets: [../miniBarContainer] | [] #ifdef OKULAR_ENABLE_MINIBAR QWidget * miniBarContainer = new QWidget( 0 ); m_sidebar->setBottomWidget( miniBarContainer ); QVBoxLayout * miniBarLayout = new QVBoxLayout( miniBarContainer ); miniBarLayout->setMargin( 0 ); // widgets: [../[spacer/..]] | [] miniBarLayout->addItem( new QSpacerItem( 6, 6, QSizePolicy::Fixed, QSizePolicy::Fixed ) ); // widgets: [../[../MiniBar]] | [] QFrame * bevelContainer = new QFrame( miniBarContainer ); bevelContainer->setFrameStyle( QFrame::StyledPanel | QFrame::Sunken ); QVBoxLayout * bevelContainerLayout = new QVBoxLayout( bevelContainer ); bevelContainerLayout->setMargin( 4 ); m_progressWidget = new ProgressWidget( bevelContainer, m_document ); bevelContainerLayout->addWidget( m_progressWidget ); miniBarLayout->addWidget( bevelContainer ); miniBarLayout->addItem( new QSpacerItem( 6, 6, QSizePolicy::Fixed, QSizePolicy::Fixed ) ); #endif // widgets: [] | [right 'pageView'] QWidget * rightContainer = new QWidget( nullptr ); m_sidebar->setMainWidget( rightContainer ); QVBoxLayout * rightLayout = new QVBoxLayout( rightContainer ); rightLayout->setMargin( 0 ); rightLayout->setSpacing( 0 ); // KToolBar * rtb = new KToolBar( rightContainer, "mainToolBarSS" ); // rightLayout->addWidget( rtb ); + m_migrationMessage = new KMessageWidget( rightContainer ); + m_migrationMessage->setVisible( false ); + m_migrationMessage->setWordWrap( true ); + m_migrationMessage->setMessageType( KMessageWidget::Warning ); + m_migrationMessage->setText( i18n( "This document contains annotations or form data that were saved internally by a previous Okular version. Internal storage is no longer supported.
Please save to a file in order to move them if you want to continue to edit the document." ) ); + rightLayout->addWidget( m_migrationMessage ); m_topMessage = new KMessageWidget( rightContainer ); m_topMessage->setVisible( false ); m_topMessage->setWordWrap( true ); m_topMessage->setMessageType( KMessageWidget::Information ); m_topMessage->setText( i18n( "This document has embedded files. Click here to see them or go to File -> Embedded Files." ) ); m_topMessage->setIcon( QIcon::fromTheme( QStringLiteral("mail-attachment") ) ); connect( m_topMessage, &KMessageWidget::linkActivated, this, &Part::slotShowEmbeddedFiles ); rightLayout->addWidget( m_topMessage ); m_formsMessage = new KMessageWidget( rightContainer ); m_formsMessage->setVisible( false ); m_formsMessage->setWordWrap( true ); m_formsMessage->setMessageType( KMessageWidget::Information ); rightLayout->addWidget( m_formsMessage ); m_infoMessage = new KMessageWidget( rightContainer ); m_infoMessage->setVisible( false ); m_infoMessage->setWordWrap( true ); m_infoMessage->setMessageType( KMessageWidget::Information ); rightLayout->addWidget( m_infoMessage ); m_infoTimer = new QTimer(); m_infoTimer->setSingleShot( true ); connect( m_infoTimer, &QTimer::timeout, m_infoMessage, &KMessageWidget::animatedHide ); m_pageView = new PageView( rightContainer, m_document ); QMetaObject::invokeMethod( m_pageView, "setFocus", Qt::QueuedConnection ); //usability setting // m_splitter->setFocusProxy(m_pageView); connect( m_pageView.data(), &PageView::rightClick, this, &Part::slotShowMenu ); connect( m_document, &Document::error, this, &Part::errorMessage ); connect( m_document, &Document::warning, this, &Part::warningMessage ); connect( m_document, &Document::notice, this, &Part::noticeMessage ); connect( m_document, &Document::sourceReferenceActivated, this, &Part::slotHandleActivatedSourceReference ); connect( m_pageView.data(), &PageView::fitWindowToPage, this, &Part::fitWindowToPage ); rightLayout->addWidget( m_pageView ); m_layers->setPageView( m_pageView ); m_findBar = new FindBar( m_document, rightContainer ); rightLayout->addWidget( m_findBar ); m_bottomBar = new QWidget( rightContainer ); QHBoxLayout * bottomBarLayout = new QHBoxLayout( m_bottomBar ); m_pageSizeLabel = new PageSizeLabel( m_bottomBar, m_document ); bottomBarLayout->setMargin( 0 ); bottomBarLayout->setSpacing( 0 ); bottomBarLayout->addItem( new QSpacerItem( 5, 5, QSizePolicy::Expanding, QSizePolicy::Minimum ) ); m_miniBarLogic = new MiniBarLogic( this, m_document ); m_miniBar = new MiniBar( m_bottomBar, m_miniBarLogic ); bottomBarLayout->addWidget( m_miniBar ); bottomBarLayout->addWidget( m_pageSizeLabel ); rightLayout->addWidget( m_bottomBar ); m_pageNumberTool = new MiniBar( nullptr, m_miniBarLogic ); connect( m_findBar, SIGNAL(forwardKeyPressEvent(QKeyEvent*)), m_pageView, SLOT(externalKeyPressEvent(QKeyEvent*))); connect( m_findBar, SIGNAL(onCloseButtonPressed()), m_pageView, SLOT(setFocus())); connect( m_miniBar, SIGNAL(forwardKeyPressEvent(QKeyEvent*)), m_pageView, SLOT(externalKeyPressEvent(QKeyEvent*))); connect( m_pageView.data(), &PageView::escPressed, m_findBar, &FindBar::resetSearch ); connect( m_pageNumberTool, SIGNAL(forwardKeyPressEvent(QKeyEvent*)), m_pageView, SLOT(externalKeyPressEvent(QKeyEvent*))); connect( m_reviewsWidget.data(), &Reviews::openAnnotationWindow, m_pageView.data(), &PageView::openAnnotationWindow ); // add document observers m_document->addObserver( this ); m_document->addObserver( m_thumbnailList ); m_document->addObserver( m_pageView ); m_document->registerView( m_pageView ); m_document->addObserver( m_toc ); m_document->addObserver( m_miniBarLogic ); #ifdef OKULAR_ENABLE_MINIBAR m_document->addObserver( m_progressWidget ); #endif m_document->addObserver( m_reviewsWidget ); m_document->addObserver( m_pageSizeLabel ); m_document->addObserver( m_bookmarkList ); connect( m_document->bookmarkManager(), &BookmarkManager::saved, this, &Part::slotRebuildBookmarkMenu ); setupViewerActions(); if ( m_embedMode != ViewerWidgetMode ) { setupActions(); } else { setViewerShortcuts(); } // document watcher and reloader m_watcher = new KDirWatch( this ); connect( m_watcher, &KDirWatch::dirty, this, &Part::slotFileDirty ); connect( m_watcher, &KDirWatch::created, this, &Part::slotFileDirty ); + connect( m_watcher, &KDirWatch::deleted, this, &Part::slotFileDirty ); m_dirtyHandler = new QTimer( this ); m_dirtyHandler->setSingleShot( true ); - connect( m_dirtyHandler, &QTimer::timeout,this, &Part::slotDoFileDirty ); + connect( m_dirtyHandler, &QTimer::timeout, this, [this] { slotAttemptReload(); } ); slotNewConfig(); // keep us informed when the user changes settings connect( Okular::Settings::self(), &KCoreConfigSkeleton::configChanged, this, &Part::slotNewConfig ); #ifdef HAVE_SPEECH // [SPEECH] check for TTS presence and usability Okular::Settings::setUseTTS( true ); Okular::Settings::self()->save(); #endif rebuildBookmarkMenu( false ); if ( m_embedMode == ViewerWidgetMode ) { // set the XML-UI resource file for the viewer mode setXMLFile(QStringLiteral("part-viewermode.rc")); } else { // set our main XML-UI resource file setXMLFile(QStringLiteral("part.rc")); } m_pageView->setupBaseActions( actionCollection() ); m_sidebar->setSidebarVisibility( false ); if ( m_embedMode != PrintPreviewMode ) { // now set up actions that are required for all remaining modes m_pageView->setupViewerActions( actionCollection() ); // and if we are not in viewer mode, we want the full GUI if ( m_embedMode != ViewerWidgetMode ) { unsetDummyMode(); } } // ensure history actions are in the correct state updateViewActions(); // also update the state of the actions in the page view m_pageView->updateActionState( false, false, false ); if ( m_embedMode == NativeShellMode ) m_sidebar->setAutoFillBackground( false ); #ifdef OKULAR_KEEP_FILE_OPEN m_keeper = new FileKeeper(); #endif } void Part::setupViewerActions() { // ACTIONS KActionCollection * ac = actionCollection(); // Page Traversal actions m_gotoPage = KStandardAction::gotoPage( this, SLOT(slotGoToPage()), ac ); ac->setDefaultShortcuts(m_gotoPage, KStandardShortcut::gotoLine()); // dirty way to activate gotopage when pressing miniBar's button connect( m_miniBar.data(), &MiniBar::gotoPage, m_gotoPage, &QAction::trigger ); connect( m_pageNumberTool.data(), &MiniBar::gotoPage, m_gotoPage, &QAction::trigger ); m_prevPage = KStandardAction::prior(this, SLOT(slotPreviousPage()), ac); m_prevPage->setIconText( i18nc( "Previous page", "Previous" ) ); m_prevPage->setToolTip( i18n( "Go back to the Previous Page" ) ); m_prevPage->setWhatsThis( i18n( "Moves to the previous page of the document" ) ); ac->setDefaultShortcut(m_prevPage, QKeySequence()); // dirty way to activate prev page when pressing miniBar's button connect( m_miniBar.data(), &MiniBar::prevPage, m_prevPage, &QAction::trigger ); connect( m_pageNumberTool.data(), &MiniBar::prevPage, m_prevPage, &QAction::trigger ); #ifdef OKULAR_ENABLE_MINIBAR connect( m_progressWidget, SIGNAL(prevPage()), m_prevPage, SLOT(trigger()) ); #endif m_nextPage = KStandardAction::next(this, SLOT(slotNextPage()), ac ); m_nextPage->setIconText( i18nc( "Next page", "Next" ) ); m_nextPage->setToolTip( i18n( "Advance to the Next Page" ) ); m_nextPage->setWhatsThis( i18n( "Moves to the next page of the document" ) ); ac->setDefaultShortcut(m_nextPage, QKeySequence()); // dirty way to activate next page when pressing miniBar's button connect( m_miniBar.data(), &MiniBar::nextPage, m_nextPage, &QAction::trigger ); connect( m_pageNumberTool.data(), &MiniBar::nextPage, m_nextPage, &QAction::trigger ); #ifdef OKULAR_ENABLE_MINIBAR connect( m_progressWidget, SIGNAL(nextPage()), m_nextPage, SLOT(trigger()) ); #endif m_beginningOfDocument = KStandardAction::firstPage( this, SLOT(slotGotoFirst()), ac ); ac->addAction(QStringLiteral("first_page"), m_beginningOfDocument); m_beginningOfDocument->setText(i18n( "Beginning of the document")); m_beginningOfDocument->setWhatsThis( i18n( "Moves to the beginning of the document" ) ); m_endOfDocument = KStandardAction::lastPage( this, SLOT(slotGotoLast()), ac ); ac->addAction(QStringLiteral("last_page"),m_endOfDocument); m_endOfDocument->setText(i18n( "End of the document")); m_endOfDocument->setWhatsThis( i18n( "Moves to the end of the document" ) ); // we do not want back and next in history in the dummy mode m_historyBack = nullptr; m_historyNext = nullptr; m_addBookmark = KStandardAction::addBookmark( this, SLOT(slotAddBookmark()), ac ); m_addBookmarkText = m_addBookmark->text(); m_addBookmarkIcon = m_addBookmark->icon(); m_renameBookmark = ac->addAction(QStringLiteral("rename_bookmark")); m_renameBookmark->setText(i18n( "Rename Bookmark" )); m_renameBookmark->setIcon(QIcon::fromTheme( QStringLiteral("edit-rename") )); m_renameBookmark->setWhatsThis( i18n( "Rename the current bookmark" ) ); connect( m_renameBookmark, &QAction::triggered, this, &Part::slotRenameCurrentViewportBookmark ); m_prevBookmark = ac->addAction(QStringLiteral("previous_bookmark")); m_prevBookmark->setText(i18n( "Previous Bookmark" )); m_prevBookmark->setIcon(QIcon::fromTheme( QStringLiteral("go-up-search") )); m_prevBookmark->setWhatsThis( i18n( "Go to the previous bookmark" ) ); connect( m_prevBookmark, &QAction::triggered, this, &Part::slotPreviousBookmark ); m_nextBookmark = ac->addAction(QStringLiteral("next_bookmark")); m_nextBookmark->setText(i18n( "Next Bookmark" )); m_nextBookmark->setIcon(QIcon::fromTheme( QStringLiteral("go-down-search") )); m_nextBookmark->setWhatsThis( i18n( "Go to the next bookmark" ) ); connect( m_nextBookmark, &QAction::triggered, this, &Part::slotNextBookmark ); m_copy = nullptr; m_selectAll = nullptr; // Find and other actions m_find = KStandardAction::find( this, SLOT(slotShowFindBar()), ac ); QList s = m_find->shortcuts(); s.append( QKeySequence( Qt::Key_Slash ) ); ac->setDefaultShortcuts(m_find, s); m_find->setEnabled( false ); m_findNext = KStandardAction::findNext( this, SLOT(slotFindNext()), ac); m_findNext->setEnabled( false ); m_findPrev = KStandardAction::findPrev( this, SLOT(slotFindPrev()), ac ); m_findPrev->setEnabled( false ); - m_saveCopyAs = nullptr; + m_save = nullptr; m_saveAs = nullptr; QAction * prefs = KStandardAction::preferences( this, SLOT(slotPreferences()), ac); if ( m_embedMode == NativeShellMode ) { prefs->setText( i18n( "Configure Okular..." ) ); } else { // TODO: improve this message prefs->setText( i18n( "Configure Viewer..." ) ); } QAction * genPrefs = new QAction( ac ); ac->addAction(QStringLiteral("options_configure_generators"), genPrefs); if ( m_embedMode == ViewerWidgetMode ) { genPrefs->setText( i18n( "Configure Viewer Backends..." ) ); } else { genPrefs->setText( i18n( "Configure Backends..." ) ); } genPrefs->setIcon( QIcon::fromTheme( QStringLiteral("configure") ) ); genPrefs->setEnabled( m_document->configurableGenerators() > 0 ); connect( genPrefs, &QAction::triggered, this, &Part::slotGeneratorPreferences ); m_printPreview = KStandardAction::printPreview( this, SLOT(slotPrintPreview()), ac ); m_printPreview->setEnabled( false ); m_showLeftPanel = nullptr; m_showBottomBar = nullptr; m_showProperties = ac->addAction(QStringLiteral("properties")); m_showProperties->setText(i18n("&Properties")); m_showProperties->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); connect(m_showProperties, &QAction::triggered, this, &Part::slotShowProperties); m_showProperties->setEnabled( false ); m_showEmbeddedFiles = nullptr; m_showPresentation = nullptr; m_exportAs = nullptr; m_exportAsMenu = nullptr; m_exportAsText = nullptr; m_exportAsDocArchive = nullptr; #if PURPOSE_FOUND m_share = nullptr; m_shareMenu = nullptr; #endif m_presentationDrawingActions = nullptr; m_aboutBackend = ac->addAction(QStringLiteral("help_about_backend")); m_aboutBackend->setText(i18n("About Backend")); m_aboutBackend->setEnabled( false ); connect(m_aboutBackend, &QAction::triggered, this, &Part::slotAboutBackend); QAction *reload = ac->add( QStringLiteral("file_reload") ); reload->setText( i18n( "Reloa&d" ) ); reload->setIcon( QIcon::fromTheme( QStringLiteral("view-refresh") ) ); reload->setWhatsThis( i18n( "Reload the current document from disk." ) ); connect( reload, &QAction::triggered, this, &Part::slotReload ); ac->setDefaultShortcuts(reload, KStandardShortcut::reload()); m_reload = reload; m_closeFindBar = ac->addAction( QStringLiteral("close_find_bar"), this, SLOT(slotHideFindBar()) ); m_closeFindBar->setText( i18n("Close &Find Bar") ); ac->setDefaultShortcut(m_closeFindBar, QKeySequence(Qt::Key_Escape)); m_closeFindBar->setEnabled( false ); QWidgetAction *pageno = new QWidgetAction( ac ); pageno->setText( i18n( "Page Number" ) ); pageno->setDefaultWidget( m_pageNumberTool ); ac->addAction( QStringLiteral("page_number"), pageno ); } void Part::setViewerShortcuts() { KActionCollection * ac = actionCollection(); ac->setDefaultShortcut(m_gotoPage, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_G)); ac->setDefaultShortcut(m_find, QKeySequence()); ac->setDefaultShortcut(m_findNext, QKeySequence()); ac->setDefaultShortcut(m_findPrev, QKeySequence()); ac->setDefaultShortcut(m_addBookmark, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_B)); ac->setDefaultShortcut(m_beginningOfDocument, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Home)); ac->setDefaultShortcut(m_endOfDocument, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_End)); QAction *action = static_cast( ac->action( QStringLiteral("file_reload") ) ); if (action) { ac->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::Key_F5)); } } void Part::setupActions() { KActionCollection * ac = actionCollection(); QMimeDatabase db; m_copy = KStandardAction::create( KStandardAction::Copy, m_pageView, SLOT(copyTextSelection()), ac ); m_selectAll = KStandardAction::selectAll( m_pageView, SLOT(selectAll()), ac ); - m_saveCopyAs = KStandardAction::saveAs( this, SLOT(slotSaveCopyAs()), ac ); - m_saveCopyAs->setText( i18n( "Save &Copy As..." ) ); - ac->addAction( QStringLiteral("file_save_copy"), m_saveCopyAs ); - ac->setDefaultShortcuts(m_saveCopyAs, KStandardShortcut::shortcut(KStandardShortcut::SaveAs)); - m_saveCopyAs->setEnabled( false ); + m_save = KStandardAction::save( this, [this] { saveFile(); }, ac ); + m_save->setEnabled( false ); m_saveAs = KStandardAction::saveAs( this, SLOT(slotSaveFileAs()), ac ); - ac->setDefaultShortcuts(m_saveAs, KStandardShortcut::shortcut(KStandardShortcut::Save)); m_saveAs->setEnabled( false ); + m_migrationMessage->addAction( m_saveAs ); m_showLeftPanel = ac->add(QStringLiteral("show_leftpanel")); m_showLeftPanel->setText(i18n( "Show &Navigation Panel")); m_showLeftPanel->setIcon(QIcon::fromTheme( QStringLiteral("view-sidetree") )); connect( m_showLeftPanel, &QAction::toggled, this, &Part::slotShowLeftPanel ); ac->setDefaultShortcut(m_showLeftPanel, QKeySequence(Qt::Key_F7)); m_showLeftPanel->setChecked( Okular::Settings::showLeftPanel() ); slotShowLeftPanel(); m_showBottomBar = ac->add(QStringLiteral("show_bottombar")); m_showBottomBar->setText(i18n( "Show &Page Bar")); connect( m_showBottomBar, &QAction::toggled, this, &Part::slotShowBottomBar ); m_showBottomBar->setChecked( Okular::Settings::showBottomBar() ); slotShowBottomBar(); m_showEmbeddedFiles = ac->addAction(QStringLiteral("embedded_files")); m_showEmbeddedFiles->setText(i18n("&Embedded Files")); m_showEmbeddedFiles->setIcon( QIcon::fromTheme( QStringLiteral("mail-attachment") ) ); connect(m_showEmbeddedFiles, &QAction::triggered, this, &Part::slotShowEmbeddedFiles); m_showEmbeddedFiles->setEnabled( false ); m_exportAs = ac->addAction(QStringLiteral("file_export_as")); m_exportAs->setText(i18n("E&xport As")); m_exportAs->setIcon( QIcon::fromTheme( QStringLiteral("document-export") ) ); m_exportAsMenu = new QMenu(); connect(m_exportAsMenu, &QMenu::triggered, this, &Part::slotExportAs); m_exportAs->setMenu( m_exportAsMenu ); m_exportAsText = actionForExportFormat( Okular::ExportFormat::standardFormat( Okular::ExportFormat::PlainText ), m_exportAsMenu ); m_exportAsMenu->addAction( m_exportAsText ); m_exportAs->setEnabled( false ); m_exportAsText->setEnabled( false ); - m_exportAsDocArchive = actionForExportFormat( Okular::ExportFormat( - i18nc( "A document format, Okular-specific", "Document Archive" ), - db.mimeTypeForName( QStringLiteral("application/vnd.kde.okular-archive") ) ), m_exportAsMenu ); - m_exportAsMenu->addAction( m_exportAsDocArchive ); - m_exportAsDocArchive->setEnabled( false ); #if PURPOSE_FOUND m_share = ac->addAction( QStringLiteral("file_share") ); m_share->setText( i18n("S&hare") ); m_share->setIcon( QIcon::fromTheme( QStringLiteral("document-share") ) ); m_share->setEnabled( false ); m_shareMenu = new Purpose::Menu(); connect(m_shareMenu, &Purpose::Menu::finished, this, &Part::slotShareActionFinished); m_share->setMenu( m_shareMenu ); #endif m_showPresentation = ac->addAction(QStringLiteral("presentation")); m_showPresentation->setText(i18n("P&resentation")); m_showPresentation->setIcon( QIcon::fromTheme( QStringLiteral("view-presentation") ) ); connect(m_showPresentation, &QAction::triggered, this, &Part::slotShowPresentation); ac->setDefaultShortcut(m_showPresentation, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_P)); m_showPresentation->setEnabled( false ); QAction * importPS = ac->addAction(QStringLiteral("import_ps")); importPS->setText(i18n("&Import PostScript as PDF...")); importPS->setIcon(QIcon::fromTheme(QStringLiteral("document-import"))); connect(importPS, &QAction::triggered, this, &Part::slotImportPSFile); #if 0 QAction * ghns = ac->addAction("get_new_stuff"); ghns->setText(i18n("&Get Books From Internet...")); ghns->setIcon(QIcon::fromTheme("get-hot-new-stuff")); connect(ghns, SIGNAL(triggered()), this, SLOT(slotGetNewStuff())); #endif KToggleAction *blackscreenAction = new KToggleAction( i18n( "Switch Blackscreen Mode" ), ac ); ac->addAction( QStringLiteral("switch_blackscreen_mode"), blackscreenAction ); ac->setDefaultShortcut(blackscreenAction, QKeySequence(Qt::Key_B)); blackscreenAction->setIcon( QIcon::fromTheme( QStringLiteral("view-presentation") ) ); blackscreenAction->setEnabled( false ); m_presentationDrawingActions = new DrawingToolActions( ac ); QAction *eraseDrawingAction = new QAction( i18n( "Erase Drawings" ), ac ); ac->addAction( QStringLiteral("presentation_erase_drawings"), eraseDrawingAction ); eraseDrawingAction->setIcon( QIcon::fromTheme( QStringLiteral("draw-eraser") ) ); eraseDrawingAction->setEnabled( false ); QAction *configureAnnotations = new QAction( i18n( "Configure Annotations..." ), ac ); ac->addAction( QStringLiteral("options_configure_annotations"), configureAnnotations ); configureAnnotations->setIcon( QIcon::fromTheme( QStringLiteral("configure") ) ); connect(configureAnnotations, &QAction::triggered, this, &Part::slotAnnotationPreferences); QAction *playPauseAction = new QAction( i18n( "Play/Pause Presentation" ), ac ); ac->addAction( QStringLiteral("presentation_play_pause"), playPauseAction ); playPauseAction->setEnabled( false ); } Part::~Part() { QDBusConnection::sessionBus().unregisterObject(m_registerDbusName); GuiUtils::removeIconLoader( iconLoader() ); m_document->removeObserver( this ); if ( m_document->isOpened() ) Part::closeUrl( false ); delete m_toc; delete m_layers; delete m_pageView; delete m_thumbnailList; delete m_miniBar; delete m_pageNumberTool; delete m_miniBarLogic; delete m_bottomBar; #ifdef OKULAR_ENABLE_MINIBAR delete m_progressWidget; #endif delete m_pageSizeLabel; delete m_reviewsWidget; delete m_bookmarkList; delete m_infoTimer; delete m_document; delete m_tempfile; qDeleteAll( m_bookmarkActions ); delete m_exportAsMenu; #if PURPOSE_FOUND delete m_shareMenu; #endif #ifdef OKULAR_KEEP_FILE_OPEN delete m_keeper; #endif } bool Part::openDocument(const QUrl& url, uint page) { Okular::DocumentViewport vp( page - 1 ); vp.rePos.enabled = true; vp.rePos.normalizedX = 0; vp.rePos.normalizedY = 0; vp.rePos.pos = Okular::DocumentViewport::TopLeft; if ( vp.isValid() ) m_document->setNextDocumentViewport( vp ); return openUrl( url ); } void Part::startPresentation() { m_cliPresentation = true; } QStringList Part::supportedMimeTypes() const { return m_document->supportedMimeTypes(); } QUrl Part::realUrl() const { if ( !m_realUrl.isEmpty() ) return m_realUrl; return url(); } // ViewerInterface void Part::showSourceLocation(const QString& fileName, int line, int column, bool showGraphically) { Q_UNUSED(column); const QString u = QStringLiteral( "src:%1 %2" ).arg( line + 1 ).arg( fileName ); GotoAction action( QString(), u ); m_document->processAction( &action ); if( showGraphically ) { m_pageView->setLastSourceLocationViewport( m_document->viewport() ); } } void Part::clearLastShownSourceLocation() { m_pageView->clearLastSourceLocationViewport(); } bool Part::isWatchFileModeEnabled() const { return !m_watcher->isStopped(); } void Part::setWatchFileModeEnabled(bool enabled) { if ( enabled && m_watcher->isStopped() ) { m_watcher->startScan(); } else if( !enabled && !m_watcher->isStopped() ) { m_dirtyHandler->stop(); m_watcher->stopScan(); } } bool Part::areSourceLocationsShownGraphically() const { return m_pageView->areSourceLocationsShownGraphically(); } void Part::setShowSourceLocationsGraphically(bool show) { m_pageView->setShowSourceLocationsGraphically(show); } bool Part::openNewFilesInTabs() const { return Okular::Settings::self()->shellOpenFileInTabs(); } void Part::slotHandleActivatedSourceReference(const QString& absFileName, int line, int col, bool *handled) { emit openSourceReference( absFileName, line, col ); if ( m_embedMode == Okular::ViewerWidgetMode ) { *handled = true; } } void Part::openUrlFromDocument(const QUrl &url) { if ( m_embedMode == PrintPreviewMode ) return; if (url.isLocalFile()) { if (!QFile::exists(url.toLocalFile())) { KMessageBox::error( widget(), i18n("Could not open '%1'. File does not exist", url.toDisplayString() ) ); return; } } else { KIO::StatJob *statJob = KIO::stat(url, KIO::StatJob::SourceSide, 0); KJobWidgets::setWindow(statJob, widget()); if (!statJob->exec() || statJob->error()) { KMessageBox::error( widget(), i18n("Could not open '%1' (%2) ", url.toDisplayString(), statJob->errorString() ) ); return; } } m_bExtension->openUrlNotify(); m_bExtension->setLocationBarUrl(url.toDisplayString()); openUrl(url); } void Part::openUrlFromBookmarks(const QUrl &_url) { QUrl url = _url; Okular::DocumentViewport vp( _url.fragment(QUrl::FullyDecoded) ); if ( vp.isValid() ) m_document->setNextDocumentViewport( vp ); url.setFragment( QString() ); if ( m_document->currentDocument() == url ) { if ( vp.isValid() ) m_document->setViewport( vp ); } else openUrl( url ); } void Part::handleDroppedUrls( const QList& urls ) { if ( urls.isEmpty() ) return; if ( m_embedMode != NativeShellMode || !openNewFilesInTabs() ) { openUrlFromDocument( urls.first() ); return; } emit urlsDropped( urls ); } void Part::slotJobStarted(KIO::Job *job) { if (job) { QStringList supportedMimeTypes = m_document->supportedMimeTypes(); job->addMetaData(QStringLiteral("accept"), supportedMimeTypes.join(QStringLiteral(", ")) + QStringLiteral(", */*;q=0.5")); connect(job, &KJob::result, this, &Part::slotJobFinished); } } void Part::slotJobFinished(KJob *job) { if ( job->error() == KIO::ERR_USER_CANCELED ) { m_pageView->displayMessage( i18n( "The loading of %1 has been canceled.", realUrl().toDisplayString(QUrl::PreferLocalFile) ) ); } } void Part::loadCancelled(const QString &reason) { emit setWindowCaption( QString() ); resetStartArguments(); - // when m_viewportDirty.pageNumber != -1 we come from slotDoFileDirty + // when m_viewportDirty.pageNumber != -1 we come from slotAttemptReload // so we don't want to show an ugly messagebox just because the document is // taking more than usual to be recreated if (m_viewportDirty.pageNumber == -1) { if (!reason.isEmpty()) { KMessageBox::error( widget(), i18n("Could not open %1. Reason: %2", url().toDisplayString(), reason ) ); } } } void Part::setWindowTitleFromDocument() { // If 'DocumentTitle' should be used, check if the document has one. If // either case is false, use the file name. QString title = Okular::Settings::displayDocumentNameOrPath() == Okular::Settings::EnumDisplayDocumentNameOrPath::Path ? realUrl().toDisplayString(QUrl::PreferLocalFile) : realUrl().fileName(); if ( Okular::Settings::displayDocumentTitle() ) { const QString docTitle = m_document->metaData( QStringLiteral("DocumentTitle") ).toString(); if ( !docTitle.isEmpty() && !docTitle.trimmed().isEmpty() ) { title = docTitle; } } emit setWindowCaption( title ); } KConfigDialog * Part::slotGeneratorPreferences( ) { // Create dialog KConfigDialog * dialog = new KConfigDialog( m_pageView, QStringLiteral("generator_prefs"), Okular::Settings::self() ); dialog->setAttribute( Qt::WA_DeleteOnClose ); if( m_embedMode == ViewerWidgetMode ) { dialog->setWindowTitle( i18n( "Configure Viewer Backends" ) ); } else { dialog->setWindowTitle( i18n( "Configure Backends" ) ); } m_document->fillConfigDialog( dialog ); // Show it dialog->setWindowModality( Qt::ApplicationModal ); dialog->show(); return dialog; } void Part::notifySetup( const QVector< Okular::Page * > & /*pages*/, int setupFlags ) { + // Hide the migration message if the user has just migrated. Otherwise, + // if m_migrationMessage is already hidden, this does nothing. + if ( !m_document->isDocdataMigrationNeeded() ) + m_migrationMessage->animatedHide(); + if ( !( setupFlags & Okular::DocumentObserver::DocumentChanged ) ) return; rebuildBookmarkMenu(); updateAboutBackendAction(); m_findBar->resetSearch(); m_searchWidget->setEnabled( m_document->supportsSearching() ); } void Part::notifyViewportChanged( bool /*smoothMove*/ ) { updateViewActions(); } void Part::notifyPageChanged( int page, int flags ) { - if ( flags & Okular::DocumentObserver::NeedSaveAs ) - setModified(); - if ( !(flags & Okular::DocumentObserver::Bookmark ) ) return; rebuildBookmarkMenu(); if ( page == m_document->viewport().pageNumber ) updateBookmarksActions(); } void Part::goToPage(uint i) { if ( i <= m_document->pages() ) m_document->setViewportPage( i - 1 ); } void Part::openDocument( const QString &doc ) { openUrl( QUrl::fromUserInput( doc ) ); } uint Part::pages() { return m_document->pages(); } uint Part::currentPage() { return m_document->pages() ? m_document->currentPage() + 1 : 0; } QString Part::currentDocument() { return m_document->currentDocument().toDisplayString(QUrl::PreferLocalFile); } QString Part::documentMetaData( const QString &metaData ) const { const Okular::DocumentInfo info = m_document->documentInfo(); return info.get( metaData ); } bool Part::slotImportPSFile() { QString app = QStandardPaths::findExecutable(QStringLiteral("ps2pdf") ); if ( app.isEmpty() ) { // TODO point the user to their distro packages? KMessageBox::error( widget(), i18n( "The program \"ps2pdf\" was not found, so Okular can not import PS files using it." ), i18n("ps2pdf not found") ); return false; } QMimeDatabase mimeDatabase; QString filter = i18n("PostScript files (%1)", mimeDatabase.mimeTypeForName(QStringLiteral("application/postscript")).globPatterns().join(QLatin1Char(' '))); QUrl url = QFileDialog::getOpenFileUrl( widget(), QString(), QUrl(), filter ); if ( url.isLocalFile() ) { QTemporaryFile tf(QDir::tempPath() + QLatin1String("/okular_XXXXXX.pdf")); tf.setAutoRemove( false ); if ( !tf.open() ) return false; m_temporaryLocalFile = tf.fileName(); tf.close(); setLocalFilePath( url.toLocalFile() ); QStringList args; QProcess *p = new QProcess(); args << url.toLocalFile() << m_temporaryLocalFile; m_pageView->displayMessage(i18n("Importing PS file as PDF (this may take a while)...")); connect(p, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(psTransformEnded(int,QProcess::ExitStatus))); p->start(app, args); return true; } m_temporaryLocalFile.clear(); return false; } -static void addFileToWatcher( KDirWatch *watcher, const QString &filePath) +void Part::setFileToWatch( const QString &filePath ) { - if ( !watcher->contains( filePath ) ) watcher->addFile(filePath); + if ( !m_watchedFilePath.isEmpty() ) + unsetFileToWatch(); + const QFileInfo fi(filePath); - if ( !watcher->contains( fi.absolutePath() ) ) watcher->addDir(fi.absolutePath()); - if ( fi.isSymLink() ) watcher->addFile( fi.readLink() ); + + m_watchedFilePath = filePath; + m_watcher->addFile( m_watchedFilePath ); + + if ( fi.isSymLink() ) + { + m_watchedFileSymlinkTarget = fi.readLink(); + m_watcher->addFile( m_watchedFileSymlinkTarget ); + } + else + { + m_watchedFileSymlinkTarget.clear(); + } +} + +void Part::unsetFileToWatch() +{ + if ( m_watchedFilePath.isEmpty() ) + return; + + m_watcher->removeFile( m_watchedFilePath ); + + if ( !m_watchedFileSymlinkTarget.isEmpty() ) + m_watcher->removeFile( m_watchedFileSymlinkTarget ); + + m_watchedFilePath.clear(); + m_watchedFileSymlinkTarget.clear(); } Document::OpenResult Part::doOpenFile( const QMimeType &mimeA, const QString &fileNameToOpenA, bool *isCompressedFile ) { QMimeDatabase db; Document::OpenResult openResult = Document::OpenError; bool uncompressOk = true; QMimeType mime = mimeA; QString fileNameToOpen = fileNameToOpenA; KFilterDev::CompressionType compressionType = compressionTypeFor( mime.name() ); if ( compressionType != KFilterDev::None ) { *isCompressedFile = true; uncompressOk = handleCompressed( fileNameToOpen, localFilePath(), compressionType ); mime = db.mimeTypeForFile( fileNameToOpen ); } else { *isCompressedFile = false; } + if ( m_swapInsteadOfOpening ) + { + m_swapInsteadOfOpening = false; + + if ( !uncompressOk ) + return Document::OpenError; + + if ( mime.inherits( QStringLiteral("application/vnd.kde.okular-archive") ) ) + { + isDocumentArchive = true; + if (!m_document->swapBackingFileArchive( fileNameToOpen, url() )) + return Document::OpenError; + } + else + { + isDocumentArchive = false; + if (!m_document->swapBackingFile( fileNameToOpen, url() )) + return Document::OpenError; + } + + return Document::OpenSuccess; + } + isDocumentArchive = false; if ( uncompressOk ) { if ( mime.inherits( QStringLiteral("application/vnd.kde.okular-archive") ) ) { openResult = m_document->openDocumentArchive( fileNameToOpen, url() ); isDocumentArchive = true; } else { openResult = m_document->openDocument( fileNameToOpen, url(), mime ); } + m_documentOpenWithPassword = false; // if the file didn't open correctly it might be encrypted, so ask for a pass QString walletName, walletFolder, walletKey; m_document->walletDataForFile(fileNameToOpen, &walletName, &walletFolder, &walletKey); bool firstInput = true; bool triedWallet = false; KWallet::Wallet * wallet = nullptr; bool keep = true; while ( openResult == Document::OpenNeedsPassword ) { QString password; // 1.A. try to retrieve the first password from the kde wallet system if ( !triedWallet && !walletKey.isNull() ) { const WId parentwid = widget()->effectiveWinId(); wallet = KWallet::Wallet::openWallet( walletName, parentwid ); if ( wallet ) { // use the KPdf folder (and create if missing) if ( !wallet->hasFolder( walletFolder ) ) wallet->createFolder( walletFolder ); wallet->setFolder( walletFolder ); // look for the pass in that folder QString retrievedPass; if ( !wallet->readPassword( walletKey, retrievedPass ) ) password = retrievedPass; } triedWallet = true; } // 1.B. if not retrieved, ask the password using the kde password dialog if ( password.isNull() ) { QString prompt; if ( firstInput ) prompt = i18n( "Please enter the password to read the document:" ); else prompt = i18n( "Incorrect password. Try again:" ); firstInput = false; // if the user presses cancel, abort opening KPasswordDialog dlg( widget(), wallet ? KPasswordDialog::ShowKeepPassword : KPasswordDialog::KPasswordDialogFlags() ); dlg.setWindowTitle( i18n( "Document Password" ) ); dlg.setPrompt( prompt ); if( !dlg.exec() ) break; password = dlg.password(); if ( wallet ) keep = dlg.keepPassword(); } // 2. reopen the document using the password if ( mime.inherits( QStringLiteral("application/vnd.kde.okular-archive") ) ) { openResult = m_document->openDocumentArchive( fileNameToOpen, url(), password ); isDocumentArchive = true; } else { openResult = m_document->openDocument( fileNameToOpen, url(), mime, password ); } - // 3. if the password is correct and the user chose to remember it, store it to the wallet - if ( openResult == Document::OpenSuccess && wallet && /*safety check*/ wallet->isOpen() && keep ) + if ( openResult == Document::OpenSuccess ) { - wallet->writePassword( walletKey, password ); + m_documentOpenWithPassword = true; + + // 3. if the password is correct and the user chose to remember it, store it to the wallet + if (wallet && /*safety check*/ wallet->isOpen() && keep ) + { + wallet->writePassword( walletKey, password ); + } } } } return openResult; } bool Part::openFile() { QList mimes; QString fileNameToOpen = localFilePath(); const bool isstdin = url().isLocalFile() && url().fileName() == QLatin1String( "-" ); const QFileInfo fileInfo( fileNameToOpen ); if ( (!isstdin) && (!fileInfo.exists()) ) return false; QMimeDatabase db; QMimeType pathMime = db.mimeTypeForFile( fileNameToOpen ); if ( !arguments().mimeType().isEmpty() ) { QMimeType argMime = db.mimeTypeForName( arguments().mimeType() ); // Select the "childmost" mimetype, if none of them // inherits the other trust more what pathMime says // but still do a second try if that one fails if ( argMime.inherits( pathMime.name() ) ) { mimes << argMime; } else if ( pathMime.inherits( argMime.name() ) ) { mimes << pathMime; } else { mimes << pathMime << argMime; } if (mimes[0].name() == QLatin1String("text/plain")) { QMimeType contentMime = db.mimeTypeForFile(fileNameToOpen, QMimeDatabase::MatchContent); mimes.prepend( contentMime ); } } else { mimes << pathMime; } QMimeType mime; Document::OpenResult openResult = Document::OpenError; bool isCompressedFile = false; while ( !mimes.isEmpty() && openResult == Document::OpenError ) { mime = mimes.takeFirst(); openResult = doOpenFile( mime, fileNameToOpen, &isCompressedFile ); } bool canSearch = m_document->supportsSearching(); emit mimeTypeChanged( mime ); // update one-time actions const bool ok = openResult == Document::OpenSuccess; emit enableCloseAction( ok ); m_find->setEnabled( ok && canSearch ); m_findNext->setEnabled( ok && canSearch ); m_findPrev->setEnabled( ok && canSearch ); - if( m_saveAs ) m_saveAs->setEnabled( ok && (m_document->canSaveChanges() || isDocumentArchive) ); - if( m_saveCopyAs ) m_saveCopyAs->setEnabled( ok ); + if( m_save ) m_save->setEnabled( ok && !( isstdin || mime.inherits( "inode/directory" ) ) ); + if( m_saveAs ) m_saveAs->setEnabled( ok && !( isstdin || mime.inherits( "inode/directory" ) ) ); emit enablePrintAction( ok && m_document->printingSupport() != Okular::Document::NoPrinting ); m_printPreview->setEnabled( ok && m_document->printingSupport() != Okular::Document::NoPrinting ); m_showProperties->setEnabled( ok ); bool hasEmbeddedFiles = ok && m_document->embeddedFiles() && m_document->embeddedFiles()->count() > 0; if ( m_showEmbeddedFiles ) m_showEmbeddedFiles->setEnabled( hasEmbeddedFiles ); m_topMessage->setVisible( hasEmbeddedFiles && Okular::Settings::showOSD() ); + m_migrationMessage->setVisible( m_document->isDocdataMigrationNeeded() ); // Warn the user that XFA forms are not supported yet (NOTE: poppler generator only) if ( ok && m_document->metaData( QStringLiteral("HasUnsupportedXfaForm") ).toBool() == true ) { m_formsMessage->setText( i18n( "This document has XFA forms, which are currently unsupported." ) ); m_formsMessage->setIcon( QIcon::fromTheme( QStringLiteral("dialog-warning") ) ); m_formsMessage->setMessageType( KMessageWidget::Warning ); m_formsMessage->setVisible( true ); } // m_pageView->toggleFormsAction() may be null on dummy mode else if ( ok && m_pageView->toggleFormsAction() && m_pageView->toggleFormsAction()->isEnabled() ) { m_formsMessage->setText( i18n( "This document has forms. Click on the button to interact with them, or use View -> Show Forms." ) ); m_formsMessage->setMessageType( KMessageWidget::Information ); m_formsMessage->setVisible( true ); } else { m_formsMessage->setVisible( false ); } if ( m_showPresentation ) m_showPresentation->setEnabled( ok ); if ( ok ) { if ( m_exportAs ) { m_exportFormats = m_document->exportFormats(); QList::ConstIterator it = m_exportFormats.constBegin(); QList::ConstIterator itEnd = m_exportFormats.constEnd(); QMenu *menu = m_exportAs->menu(); for ( ; it != itEnd; ++it ) { menu->addAction( actionForExportFormat( *it ) ); } } #if PURPOSE_FOUND if ( m_share ) { m_shareMenu->model()->setInputData(QJsonObject{ { QStringLiteral("mimeType"), mime.name() }, { QStringLiteral("urls"), QJsonArray{ url().toString() } } }); m_shareMenu->model()->setPluginType( QStringLiteral("Export") ); m_shareMenu->reload(); } #endif if ( isCompressedFile ) { m_realUrl = url(); } #ifdef OKULAR_KEEP_FILE_OPEN if ( keepFileOpen() ) m_keeper->open( fileNameToOpen ); #endif } if ( m_exportAsText ) m_exportAsText->setEnabled( ok && m_document->canExportToText() ); - if ( m_exportAsDocArchive ) m_exportAsDocArchive->setEnabled( ok ); if ( m_exportAs ) m_exportAs->setEnabled( ok ); #if PURPOSE_FOUND if ( m_share ) m_share->setEnabled( ok ); #endif // update viewing actions updateViewActions(); m_fileWasRemoved = false; if ( !ok ) { // if can't open document, update windows so they display blank contents m_pageView->viewport()->update(); m_thumbnailList->update(); setUrl( QUrl() ); return false; } // set the file to the fileWatcher if ( url().isLocalFile() ) - { - addFileToWatcher( m_watcher, localFilePath() ); - } + setFileToWatch( localFilePath() ); // if the 'OpenTOC' flag is set, open the TOC if ( m_document->metaData( QStringLiteral("OpenTOC") ).toBool() && m_sidebar->isItemEnabled( m_toc ) && !m_sidebar->isCollapsed() && m_sidebar->currentItem() != m_toc ) { m_sidebar->setCurrentItem( m_toc, Sidebar::DoNotUncollapseIfCollapsed ); } // if the 'StartFullScreen' flag is set, or the command line flag was // specified, start presentation if ( m_document->metaData( QStringLiteral("StartFullScreen") ).toBool() || m_cliPresentation ) { bool goAheadWithPresentationMode = true; if ( !m_cliPresentation ) { const QString text = i18n( "The document requested to be launched in presentation mode.\n" "Do you want to allow it?" ); const QString caption = i18n( "Presentation Mode" ); const KGuiItem yesItem = KGuiItem( i18n( "Allow" ), QStringLiteral("dialog-ok"), i18n( "Allow the presentation mode" ) ); const KGuiItem noItem = KGuiItem( i18n( "Do Not Allow" ), QStringLiteral("process-stop"), i18n( "Do not allow the presentation mode" ) ); const int result = KMessageBox::questionYesNo( widget(), text, caption, yesItem, noItem ); if ( result == KMessageBox::No ) goAheadWithPresentationMode = false; } m_cliPresentation = false; if ( goAheadWithPresentationMode ) QMetaObject::invokeMethod( this, "slotShowPresentation", Qt::QueuedConnection ); } m_generatorGuiClient = factory() ? m_document->guiClient() : nullptr; if ( m_generatorGuiClient ) factory()->addClient( m_generatorGuiClient ); if ( m_cliPrint ) { m_cliPrint = false; slotPrint(); } return true; } -bool Part::openUrl(const QUrl &_url) +bool Part::openUrl( const QUrl &url ) +{ + return openUrl( url, false /* swapInsteadOfOpening */ ); +} + +bool Part::openUrl( const QUrl &_url, bool swapInsteadOfOpening ) { + /* Store swapInsteadOfOpening, so that closeUrl and openFile will be able + * to read it */ + m_swapInsteadOfOpening = swapInsteadOfOpening; + // Close current document if any if ( !closeUrl() ) return false; QUrl url( _url ); if ( url.hasFragment() ) { const QString dest = url.fragment(QUrl::FullyDecoded); bool ok = true; const int page = dest.toInt( &ok ); if ( ok ) { Okular::DocumentViewport vp( page - 1 ); vp.rePos.enabled = true; vp.rePos.normalizedX = 0; vp.rePos.normalizedY = 0; vp.rePos.pos = Okular::DocumentViewport::TopLeft; m_document->setNextDocumentViewport( vp ); } else { m_document->setNextDocumentDestination( dest ); } url.setFragment( QString() ); } // this calls in sequence the 'closeUrl' and 'openFile' methods bool openOk = KParts::ReadWritePart::openUrl( url ); if ( openOk ) { m_viewportDirty.pageNumber = -1; setWindowTitleFromDocument(); } else { resetStartArguments(); KMessageBox::error( widget(), i18n("Could not open %1", url.toDisplayString() ) ); } return openOk; } bool Part::queryClose() { if ( !isReadWrite() || !isModified() ) return true; const int res = KMessageBox::warningYesNoCancel( widget(), - i18n( "Do you want to save your annotation changes or discard them?" ), + i18n( "Do you want to save your changes to \"%1\" or discard them?", url().fileName() ), i18n( "Close Document" ), - KStandardGuiItem::saveAs(), + KStandardGuiItem::save(), KStandardGuiItem::discard() ); switch ( res ) { - case KMessageBox::Yes: // Save as - slotSaveFileAs(); + case KMessageBox::Yes: // Save + saveFile(); return !isModified(); // Only allow closing if file was really saved case KMessageBox::No: // Discard return true; default: // Cancel return false; } } bool Part::closeUrl(bool promptToSave) { if ( promptToSave && !queryClose() ) return false; - setModified( false ); + if ( m_swapInsteadOfOpening ) + { + // If we're swapping the backing file, we don't want to close the + // current one when openUrl() calls us internally + return true; // pretend it worked + } + + m_document->setHistoryClean( true ); if (!m_temporaryLocalFile.isNull() && m_temporaryLocalFile != localFilePath()) { QFile::remove( m_temporaryLocalFile ); m_temporaryLocalFile.clear(); } slotHidePresentation(); emit enableCloseAction( false ); m_find->setEnabled( false ); m_findNext->setEnabled( false ); m_findPrev->setEnabled( false ); + if( m_save ) m_save->setEnabled( false ); if( m_saveAs ) m_saveAs->setEnabled( false ); - if( m_saveCopyAs ) m_saveCopyAs->setEnabled( false ); m_printPreview->setEnabled( false ); m_showProperties->setEnabled( false ); if ( m_showEmbeddedFiles ) m_showEmbeddedFiles->setEnabled( false ); if ( m_exportAs ) m_exportAs->setEnabled( false ); if ( m_exportAsText ) m_exportAsText->setEnabled( false ); - if ( m_exportAsDocArchive ) m_exportAsDocArchive->setEnabled( false ); m_exportFormats.clear(); if ( m_exportAs ) { QMenu *menu = m_exportAs->menu(); QList acts = menu->actions(); int num = acts.count(); - for ( int i = 2; i < num; ++i ) + for ( int i = 1; i < num; ++i ) { menu->removeAction( acts.at(i) ); delete acts.at(i); } } #if PURPOSE_FOUND if ( m_share ) { m_share->setEnabled(false); m_shareMenu->clear(); } #endif if ( m_showPresentation ) m_showPresentation->setEnabled( false ); emit setWindowCaption(QLatin1String("")); emit enablePrintAction(false); m_realUrl = QUrl(); if ( url().isLocalFile() ) - { - m_watcher->removeFile( localFilePath() ); - QFileInfo fi(localFilePath()); - m_watcher->removeDir( fi.absolutePath() ); - if ( fi.isSymLink() ) m_watcher->removeFile( fi.readLink() ); - } + unsetFileToWatch(); m_fileWasRemoved = false; if ( m_generatorGuiClient ) factory()->removeClient( m_generatorGuiClient ); m_generatorGuiClient = nullptr; m_document->closeDocument(); updateViewActions(); delete m_tempfile; m_tempfile = nullptr; if ( widget() ) { m_searchWidget->clearText(); + m_migrationMessage->setVisible( false ); m_topMessage->setVisible( false ); m_formsMessage->setVisible( false ); } #ifdef OKULAR_KEEP_FILE_OPEN m_keeper->close(); #endif bool r = KParts::ReadWritePart::closeUrl(); setUrl(QUrl()); return r; } bool Part::closeUrl() { return closeUrl( true ); } void Part::guiActivateEvent(KParts::GUIActivateEvent *event) { updateViewActions(); KParts::ReadWritePart::guiActivateEvent(event); setWindowTitleFromDocument(); } void Part::close() { if ( m_embedMode == NativeShellMode ) { closeUrl(); } else KMessageBox::information( widget(), i18n( "This link points to a close document action that does not work when using the embedded viewer." ), QString(), QStringLiteral("warnNoCloseIfNotInOkular") ); } void Part::cannotQuit() { KMessageBox::information( widget(), i18n( "This link points to a quit application action that does not work when using the embedded viewer." ), QString(), QStringLiteral("warnNoQuitIfNotInOkular") ); } void Part::slotShowLeftPanel() { bool showLeft = m_showLeftPanel->isChecked(); Okular::Settings::setShowLeftPanel( showLeft ); Okular::Settings::self()->save(); // show/hide left panel m_sidebar->setSidebarVisibility( showLeft ); } void Part::slotShowBottomBar() { const bool showBottom = m_showBottomBar->isChecked(); Okular::Settings::setShowBottomBar( showBottom ); Okular::Settings::self()->save(); // show/hide bottom bar m_bottomBar->setVisible( showBottom ); } void Part::slotFileDirty( const QString& path ) { // The beauty of this is that each start cancels the previous one. // This means that timeout() is only fired when there have // no changes to the file for the last 750 milisecs. // This ensures that we don't update on every other byte that gets // written to the file. if ( path == localFilePath() ) { // Only start watching the file in case if it wasn't removed if (QFile::exists(localFilePath())) m_dirtyHandler->start( 750 ); else m_fileWasRemoved = true; } else { const QFileInfo fi(localFilePath()); if ( fi.absolutePath() == path ) { // Our parent has been dirtified if (!QFile::exists(localFilePath())) { m_fileWasRemoved = true; } else if (m_fileWasRemoved && QFile::exists(localFilePath())) { // we need to watch the new file - m_watcher->removeFile(localFilePath()); - m_watcher->addFile(localFilePath()); + unsetFileToWatch(); + setFileToWatch( localFilePath() ); m_dirtyHandler->start( 750 ); } } else if ( fi.isSymLink() && fi.readLink() == path ) { if ( QFile::exists( fi.readLink() )) m_dirtyHandler->start( 750 ); else m_fileWasRemoved = true; } } } - -void Part::slotDoFileDirty() +// Attempt to reload the document, one or more times, optionally from a different URL +bool Part::slotAttemptReload( bool oneShot, const QUrl &newUrl ) { // Skip reload when another reload is already in progress if ( m_isReloading ) { - return; + return false; } QScopedValueRollback rollback(m_isReloading, true); bool tocReloadPrepared = false; // do the following the first time the file is reloaded if ( m_viewportDirty.pageNumber == -1 ) { // store the url of the current document - m_oldUrl = url(); + m_oldUrl = newUrl.isEmpty() ? url() : newUrl; // store the current viewport m_viewportDirty = m_document->viewport(); // store the current toolbox pane m_dirtyToolboxItem = m_sidebar->currentItem(); m_wasSidebarVisible = m_sidebar->isSidebarVisible(); m_wasSidebarCollapsed = m_sidebar->isCollapsed(); // store if presentation view was open m_wasPresentationOpen = ((PresentationWidget*)m_presentationWidget != nullptr); // preserves the toc state after reload m_toc->prepareForReload(); tocReloadPrepared = true; // store the page rotation m_dirtyPageRotation = m_document->rotation(); // inform the user about the operation in progress // TODO: Remove this line and integrate reload info in queryClose m_pageView->displayMessage( i18n("Reloading the document...") ); } // close and (try to) reopen the document if ( !closeUrl() ) { m_viewportDirty.pageNumber = -1; if ( tocReloadPrepared ) { m_toc->rollbackReload(); } - return; + return false; } if ( tocReloadPrepared ) m_toc->finishReload(); // inform the user about the operation in progress m_pageView->displayMessage( i18n("Reloading the document...") ); + bool reloadSucceeded = false; + if ( KParts::ReadWritePart::openUrl( m_oldUrl ) ) { // on successful opening, restore the previous viewport if ( m_viewportDirty.pageNumber >= (int) m_document->pages() ) m_viewportDirty.pageNumber = (int) m_document->pages() - 1; m_document->setViewport( m_viewportDirty ); m_oldUrl = QUrl(); m_viewportDirty.pageNumber = -1; m_document->setRotation( m_dirtyPageRotation ); if ( m_sidebar->currentItem() != m_dirtyToolboxItem && m_sidebar->isItemEnabled( m_dirtyToolboxItem ) && !m_sidebar->isCollapsed() ) { m_sidebar->setCurrentItem( m_dirtyToolboxItem ); } if ( m_sidebar->isSidebarVisible() != m_wasSidebarVisible ) { m_sidebar->setSidebarVisibility( m_wasSidebarVisible ); } if ( m_sidebar->isCollapsed() != m_wasSidebarCollapsed ) { m_sidebar->setCollapsed( m_wasSidebarCollapsed ); } if (m_wasPresentationOpen) slotShowPresentation(); emit enablePrintAction(true && m_document->printingSupport() != Okular::Document::NoPrinting); + + reloadSucceeded = true; } - else + else if ( !oneShot ) { - // start watching the file again (since we dropped it on close) - addFileToWatcher( m_watcher, localFilePath() ); + // start watching the file again (since we dropped it on close) + setFileToWatch( localFilePath() ); m_dirtyHandler->start( 750 ); } + + return reloadSucceeded; } void Part::updateViewActions() { bool opened = m_document->pages() > 0; if ( opened ) { m_gotoPage->setEnabled( m_document->pages() > 1 ); // Check if you are at the beginning or not if (m_document->currentPage() != 0) { m_beginningOfDocument->setEnabled( true ); m_prevPage->setEnabled( true ); } else { if (m_pageView->verticalScrollBar()->value() != 0) { // The page isn't at the very beginning m_beginningOfDocument->setEnabled( true ); } else { // The page is at the very beginning of the document m_beginningOfDocument->setEnabled( false ); } // The document is at the first page, you can go to a page before m_prevPage->setEnabled( false ); } if (m_document->pages() == m_document->currentPage() + 1 ) { // If you are at the end, disable go to next page m_nextPage->setEnabled( false ); if (m_pageView->verticalScrollBar()->value() == m_pageView->verticalScrollBar()->maximum()) { // If you are the end of the page of the last document, you can't go to the last page m_endOfDocument->setEnabled( false ); } else { // Otherwise you can move to the endif m_endOfDocument->setEnabled( true ); } } else { // If you are not at the end, enable go to next page m_nextPage->setEnabled( true ); m_endOfDocument->setEnabled( true ); } if (m_historyBack) m_historyBack->setEnabled( !m_document->historyAtBegin() ); if (m_historyNext) m_historyNext->setEnabled( !m_document->historyAtEnd() ); m_reload->setEnabled( true ); if (m_copy) m_copy->setEnabled( true ); if (m_selectAll) m_selectAll->setEnabled( true ); } else { m_gotoPage->setEnabled( false ); m_beginningOfDocument->setEnabled( false ); m_endOfDocument->setEnabled( false ); m_prevPage->setEnabled( false ); m_nextPage->setEnabled( false ); if (m_historyBack) m_historyBack->setEnabled( false ); if (m_historyNext) m_historyNext->setEnabled( false ); m_reload->setEnabled( false ); if (m_copy) m_copy->setEnabled( false ); if (m_selectAll) m_selectAll->setEnabled( false ); } if ( factory() ) { QWidget *menu = factory()->container(QStringLiteral("menu_okular_part_viewer"), this); if (menu) menu->setEnabled( opened ); menu = factory()->container(QStringLiteral("view_orientation"), this); if (menu) menu->setEnabled( opened ); } emit viewerMenuStateChange( opened ); updateBookmarksActions(); } void Part::updateBookmarksActions() { bool opened = m_document->pages() > 0; if ( opened ) { m_addBookmark->setEnabled( true ); if ( m_document->bookmarkManager()->isBookmarked( m_document->viewport() ) ) { m_addBookmark->setText( i18n( "Remove Bookmark" ) ); m_addBookmark->setIcon( QIcon::fromTheme( QStringLiteral("edit-delete-bookmark") ) ); m_renameBookmark->setEnabled( true ); } else { m_addBookmark->setText( m_addBookmarkText ); m_addBookmark->setIcon( m_addBookmarkIcon ); m_renameBookmark->setEnabled( false ); } } else { m_addBookmark->setEnabled( false ); m_addBookmark->setText( m_addBookmarkText ); m_addBookmark->setIcon( m_addBookmarkIcon ); m_renameBookmark->setEnabled( false ); } } void Part::enableTOC(bool enable) { m_sidebar->setItemEnabled(m_toc, enable); // If present, show the TOC when a document is opened if ( enable && m_sidebar->currentItem() != m_toc ) { m_sidebar->setCurrentItem( m_toc, Sidebar::DoNotUncollapseIfCollapsed ); } } void Part::slotRebuildBookmarkMenu() { rebuildBookmarkMenu(); } void Part::enableLayers(bool enable) { m_sidebar->setItemVisible( m_layers, enable ); } void Part::slotShowFindBar() { m_findBar->show(); m_findBar->focusAndSetCursor(); m_closeFindBar->setEnabled( true ); } void Part::slotHideFindBar() { if ( m_findBar->maybeHide() ) { m_pageView->setFocus(); m_closeFindBar->setEnabled( false ); } } //BEGIN go to page dialog class GotoPageDialog : public QDialog { Q_OBJECT public: GotoPageDialog(QWidget *p, int current, int max) : QDialog(p) { setWindowTitle(i18n("Go to Page")); buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QVBoxLayout *topLayout = new QVBoxLayout(this); topLayout->setMargin(6); QHBoxLayout *midLayout = new QHBoxLayout(); spinbox = new QSpinBox(this); spinbox->setRange(1, max); spinbox->setValue(current); spinbox->setFocus(); slider = new QSlider(Qt::Horizontal, this); slider->setRange(1, max); slider->setValue(current); slider->setSingleStep(1); slider->setTickPosition(QSlider::TicksBelow); slider->setTickInterval(max/10); connect(slider, &QSlider::valueChanged, spinbox, &QSpinBox::setValue); connect(spinbox, static_cast(&QSpinBox::valueChanged), slider, &QSlider::setValue); QLabel *label = new QLabel(i18n("&Page:"), this); label->setBuddy(spinbox); topLayout->addWidget(label); topLayout->addLayout(midLayout); midLayout->addWidget(slider); midLayout->addWidget(spinbox); // A little bit extra space topLayout->addStretch(10); topLayout->addWidget(buttonBox); spinbox->setFocus(); } int getPage() const { return spinbox->value(); } protected: QSpinBox *spinbox; QSlider *slider; QDialogButtonBox *buttonBox; }; //END go to page dialog void Part::slotGoToPage() { GotoPageDialog pageDialog( m_pageView, m_document->currentPage() + 1, m_document->pages() ); if ( pageDialog.exec() == QDialog::Accepted ) m_document->setViewportPage( pageDialog.getPage() - 1 ); } void Part::slotPreviousPage() { if ( m_document->isOpened() && !(m_document->currentPage() < 1) ) m_document->setViewportPage( m_document->currentPage() - 1 ); } void Part::slotNextPage() { if ( m_document->isOpened() && m_document->currentPage() < (m_document->pages() - 1) ) m_document->setViewportPage( m_document->currentPage() + 1 ); } void Part::slotGotoFirst() { if ( m_document->isOpened() ) { m_document->setViewportPage( 0 ); m_beginningOfDocument->setEnabled( false ); } } void Part::slotGotoLast() { if ( m_document->isOpened() ) { DocumentViewport endPage(m_document->pages() -1 ); endPage.rePos.enabled = true; endPage.rePos.normalizedX = 0; endPage.rePos.normalizedY = 1; endPage.rePos.pos = Okular::DocumentViewport::TopLeft; m_document->setViewport(endPage); m_endOfDocument->setEnabled(false); } } void Part::slotHistoryBack() { m_document->setPrevViewport(); } void Part::slotHistoryNext() { m_document->setNextViewport(); } void Part::slotAddBookmark() { DocumentViewport vp = m_document->viewport(); if ( m_document->bookmarkManager()->isBookmarked( vp ) ) { m_document->bookmarkManager()->removeBookmark( vp ); } else { m_document->bookmarkManager()->addBookmark( vp ); } } void Part::slotRenameBookmark( const DocumentViewport &viewport ) { Q_ASSERT(m_document->bookmarkManager()->isBookmarked( viewport )); if ( m_document->bookmarkManager()->isBookmarked( viewport ) ) { KBookmark bookmark = m_document->bookmarkManager()->bookmark( viewport ); const QString newName = QInputDialog::getText(widget(), i18n( "Rename Bookmark" ), i18n( "Enter the new name of the bookmark:" ), QLineEdit::Normal, bookmark.fullText()); if (!newName.isEmpty()) { m_document->bookmarkManager()->renameBookmark(&bookmark, newName); } } } void Part::slotRenameBookmarkFromMenu() { QAction *action = dynamic_cast(sender()); Q_ASSERT( action ); if ( action ) { DocumentViewport vp( action->data().toString() ); slotRenameBookmark( vp ); } } void Part::slotRemoveBookmarkFromMenu() { QAction *action = dynamic_cast(sender()); Q_ASSERT( action ); if ( action ) { DocumentViewport vp ( action->data().toString() ); slotRemoveBookmark( vp ); } } void Part::slotRemoveBookmark(const DocumentViewport &viewport) { Q_ASSERT(m_document->bookmarkManager()->isBookmarked( viewport )); if ( m_document->bookmarkManager()->isBookmarked( viewport ) ) { m_document->bookmarkManager()->removeBookmark( viewport ); } } void Part::slotRenameCurrentViewportBookmark() { slotRenameBookmark( m_document->viewport() ); } bool Part::aboutToShowContextMenu(QMenu * /*menu*/, QAction *action, QMenu *contextMenu) { KBookmarkAction *ba = dynamic_cast(action); if (ba != nullptr) { QAction *separatorAction = contextMenu->addSeparator(); separatorAction->setObjectName(QStringLiteral("OkularPrivateRenameBookmarkActions")); QAction *renameAction = contextMenu->addAction( QIcon::fromTheme( QStringLiteral("edit-rename") ), i18n( "Rename this Bookmark" ), this, SLOT(slotRenameBookmarkFromMenu()) ); renameAction->setData(ba->property("htmlRef").toString()); renameAction->setObjectName(QStringLiteral("OkularPrivateRenameBookmarkActions")); QAction *deleteAction = contextMenu->addAction( QIcon::fromTheme( QStringLiteral("list-remove") ), i18n("Remove this Bookmark"), this, SLOT(slotRemoveBookmarkFromMenu())); deleteAction->setData(ba->property("htmlRef").toString()); deleteAction->setObjectName(QStringLiteral("OkularPrivateRenameBookmarkActions")); } return ba; } void Part::slotPreviousBookmark() { const KBookmark bookmark = m_document->bookmarkManager()->previousBookmark( m_document->viewport() ); if ( !bookmark.isNull() ) { DocumentViewport vp( bookmark.url().fragment(QUrl::FullyDecoded) ); m_document->setViewport( vp ); } } void Part::slotNextBookmark() { const KBookmark bookmark = m_document->bookmarkManager()->nextBookmark( m_document->viewport() ); if ( !bookmark.isNull() ) { DocumentViewport vp( bookmark.url().fragment(QUrl::FullyDecoded) ); m_document->setViewport( vp ); } } void Part::slotFind() { // when in presentation mode, there's already a search bar, taking care of // the 'find' requests if ( (PresentationWidget*)m_presentationWidget != nullptr ) { m_presentationWidget->slotFind(); } else { slotShowFindBar(); } } void Part::slotFindNext() { if (m_findBar->isHidden()) slotShowFindBar(); else m_findBar->findNext(); } void Part::slotFindPrev() { if (m_findBar->isHidden()) slotShowFindBar(); else m_findBar->findPrev(); } bool Part::saveFile() { - qCDebug(OkularUiDebug) << "Okular part doesn't support saving the file in the location from which it was opened"; - return false; + if ( !isModified() ) + return true; + else + return saveAs( url() ); } -void Part::slotSaveFileAs() +bool Part::slotSaveFileAs( bool showOkularArchiveAsDefaultFormat ) { if ( m_embedMode == PrintPreviewMode ) - return; + return false; - /* Show a warning before saving if the generator can't save annotations, - * unless we are going to save a .okular archive. */ - if ( !isDocumentArchive && !m_document->canSaveChanges( Document::SaveAnnotationsCapability ) ) - { - /* Search local annotations */ - bool containsLocalAnnotations = false; - const int pagecount = m_document->pages(); + // Determine the document's mimetype + QMimeDatabase db; + QMimeType originalMimeType; + const QString typeName = m_document->documentInfo().get( DocumentInfo::MimeType ); + if ( !typeName.isEmpty() ) + originalMimeType = db.mimeTypeForName( typeName ); - for ( int pageno = 0; pageno < pagecount; ++pageno ) - { - const Okular::Page *page = m_document->page( pageno ); - foreach ( const Okular::Annotation *ann, page->annotations() ) - { - if ( !(ann->flags() & Okular::Annotation::External) ) - { - containsLocalAnnotations = true; - break; - } - } - if ( containsLocalAnnotations ) - break; - } + // What data would we lose if we saved natively? + bool wontSaveForms, wontSaveAnnotations; + checkNativeSaveDataLoss(&wontSaveForms, &wontSaveAnnotations); - /* Don't show it if there are no local annotations */ - if ( containsLocalAnnotations ) - { - int res = KMessageBox::warningContinueCancel( widget(), i18n("Your annotations will not be exported.\nYou can export the annotated document using File -> Export As -> Document Archive") ); - if ( res != KMessageBox::Continue ) - return; // Canceled - } - } + const QMimeType okularArchiveMimeType = db.mimeTypeForName( QStringLiteral("application/vnd.kde.okular-archive") ); + + // Prepare "Save As" dialog + const QString originalMimeTypeFilter = i18nc("File type name and pattern", "%1 (%2)", originalMimeType.comment(), originalMimeType.globPatterns().join(QLatin1Char(' '))); + const QString okularArchiveMimeTypeFilter = i18nc("File type name and pattern", "%1 (%2)", okularArchiveMimeType.comment(), okularArchiveMimeType.globPatterns().join(QLatin1Char(' '))); + + // What format choice should we show as default? + QString selectedFilter = (isDocumentArchive || showOkularArchiveAsDefaultFormat || + wontSaveForms || wontSaveAnnotations) ? + okularArchiveMimeTypeFilter : originalMimeTypeFilter; + + QString filter = originalMimeTypeFilter + QStringLiteral(";;") + okularArchiveMimeTypeFilter; + + const QUrl saveUrl = QFileDialog::getSaveFileUrl(widget(), i18n("Save As"), url(), filter, &selectedFilter); - QUrl saveUrl = QFileDialog::getSaveFileUrl( widget(), QString(), url() ); if ( !saveUrl.isValid() || saveUrl.isEmpty() ) - return; + return false; + + // Has the user chosen to save in .okular archive format? + const bool saveAsOkularArchive = ( selectedFilter == okularArchiveMimeTypeFilter ); - saveAs( saveUrl ); + return saveAs( saveUrl, saveAsOkularArchive ? SaveAsOkularArchive : NoSaveAsFlags ); +} + +bool Part::saveAs(const QUrl & saveUrl) +{ + // Save in the same format (.okular vs native) as the current file + return saveAs( saveUrl, isDocumentArchive ? SaveAsOkularArchive : NoSaveAsFlags ); } -bool Part::saveAs( const QUrl & saveUrl ) +bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) { + bool hasUserAcceptedReload = false; + if ( m_documentOpenWithPassword ) + { + const int res = KMessageBox::warningYesNo( widget(), + i18n( "The current document is protected with a password.
In order to save, the file needs to be reloaded. You will be asked for the password again and your undo/redo history will be lost.
Do you want to continue?" ), + i18n( "Save - Warning" ) ); + + switch ( res ) + { + case KMessageBox::Yes: + hasUserAcceptedReload = true; + // do nothing + break; + case KMessageBox::No: // User said no to continue, so return true even if save didn't happen otherwise we will get an error + return true; + } + } + + bool setModifiedAfterSave = false; + QTemporaryFile tf; QString fileName; if ( !tf.open() ) { KMessageBox::information( widget(), i18n("Could not open the temporary file for saving." ) ); return false; } fileName = tf.fileName(); tf.close(); - QString errorText; - bool saved; - - if ( isDocumentArchive ) - saved = m_document->saveDocumentArchive( fileName ); - else - saved = m_document->saveChanges( fileName, &errorText ); + QScopedPointer tempFile; + KIO::Job *copyJob = nullptr; // this will be filled with the job that writes to saveUrl - if ( !saved ) + // Does the user want a .okular archive? + if ( flags & SaveAsOkularArchive ) { - if (errorText.isEmpty()) + if ( !hasUserAcceptedReload && !m_document->canSwapBackingFile() ) + { + const int res = KMessageBox::warningYesNo( widget(), + i18n( "After saving, the current document format requires the file to be reloaded. Your undo/redo history will be lost.
Do you want to continue?" ), + i18n( "Save - Warning" ) ); + + switch ( res ) + { + case KMessageBox::Yes: + // do nothing + break; + case KMessageBox::No: // User said no to continue, so return true even if save didn't happen otherwise we will get an error + return true; + } + } + + if ( !m_document->saveDocumentArchive( fileName ) ) { KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", fileName ) ); + return false; + } + + copyJob = KIO::file_copy( QUrl::fromLocalFile( fileName ), saveUrl, -1, KIO::Overwrite ); + } + else + { + bool wontSaveForms, wontSaveAnnotations; + checkNativeSaveDataLoss(&wontSaveForms, &wontSaveAnnotations); + + // If something can't be saved in this format, ask for confirmation + QStringList listOfwontSaves; + if ( wontSaveForms ) listOfwontSaves << i18n( "Filled form contents" ); + if ( wontSaveAnnotations ) listOfwontSaves << i18n( "User annotations" ); + if ( !listOfwontSaves.isEmpty() ) + { + if ( saveUrl == url() ) + { + // Save + const QString warningMessage = i18n( "You are about to save changes, but the current file format does not support saving the following elements. Please use the Okular document archive format to preserve them." ); + const int result = KMessageBox::warningYesNoList( widget(), + warningMessage, + listOfwontSaves, i18n( "Warning" ), + KGuiItem( i18n( "Save as Okular document archive..." ), "document-save-as" ), // <- KMessageBox::Yes + KStandardGuiItem::cancel() ); + + switch (result) + { + case KMessageBox::Yes: // -> Save as Okular document archive + return slotSaveFileAs( true /* showOkularArchiveAsDefaultFormat */ ); + default: + return false; + } + } + else + { + // Save as + const QString warningMessage = m_document->canSwapBackingFile() ? + i18n( "You are about to save changes, but the current file format does not support saving the following elements. Please use the Okular document archive format to preserve them. Click Continue to save the document and discard these elements." ) : + i18n( "You are about to save changes, but the current file format does not support saving the following elements. Please use the Okular document archive format to preserve them. Click Continue to save, but you will lose these elements as well as the undo/redo history." ); + const QString continueMessage = m_document->canSwapBackingFile() ? + i18n( "Continue" ) : + i18n( "Continue losing changes" ); + const int result = KMessageBox::warningYesNoCancelList( widget(), + warningMessage, + listOfwontSaves, i18n( "Warning" ), + KGuiItem( i18n( "Save as Okular document archive..." ), "document-save-as" ), // <- KMessageBox::Yes + KGuiItem( continueMessage, "arrow-right" ) ); // <- KMessageBox::NO + + switch (result) + { + case KMessageBox::Yes: // -> Save as Okular document archive + return slotSaveFileAs( true /* showOkularArchiveAsDefaultFormat */ ); + case KMessageBox::No: // -> Continue + setModifiedAfterSave = m_document->canSwapBackingFile(); + break; + case KMessageBox::Cancel: + return false; + } + } + } + + if ( m_document->canSaveChanges() ) + { + // If the generator supports saving changes, save them + + QString errorText; + if ( !m_document->saveChanges( fileName, &errorText ) ) + { + if (errorText.isEmpty()) + KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", fileName ) ); + else + KMessageBox::information( widget(), i18n("File could not be saved in '%1'. %2", fileName, errorText ) ); + + return false; + } + + copyJob = KIO::file_copy( QUrl::fromLocalFile( fileName ), saveUrl, -1, KIO::Overwrite ); } else { - KMessageBox::information( widget(), i18n("File could not be saved in '%1'. %2", fileName, errorText ) ); + // If the generators doesn't support saving changes, we will + // just copy the original file. + + if ( isDocumentArchive ) + { + // Special case: if the user is extracting the contents of a + // .okular archive back to the native format, we can't just copy + // the open file (which is a .okular). So let's ask to core to + // extract and give us the real file + + if ( !m_document->extractArchivedFile( fileName ) ) + { + KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", fileName ) ); + return false; + } + + copyJob = KIO::file_copy( QUrl::fromLocalFile( fileName ), saveUrl, -1, KIO::Overwrite ); + } + else + { + // Otherwise just copy the open file. + // make use of the already downloaded (in case of remote URLs) file, + // no point in downloading that again + QUrl srcUrl = QUrl::fromLocalFile( localFilePath() ); + // duh, our local file disappeared... + if ( !QFile::exists( localFilePath() ) ) + { + if ( url().isLocalFile() ) + { +#ifdef OKULAR_KEEP_FILE_OPEN + // local file: try to get it back from the open handle on it + tempFile.reset( m_keeper->copyToTemporary() ); + if ( tempFile ) + srcUrl = KUrl::fromPath( tempFile->fileName() ); +#else + const QString msg = i18n( "Okular cannot copy %1 to the specified location.\n\nThe document does not exist anymore.", localFilePath() ); + KMessageBox::sorry( widget(), msg ); + return false; +#endif + } + else + { + // we still have the original remote URL of the document, + // so copy the document from there + srcUrl = url(); + } + } + + if ( srcUrl != saveUrl ) + { + copyJob = KIO::file_copy( srcUrl, saveUrl, -1, KIO::Overwrite ); + } + else + { + // Don't do a real copy in this case, just update the timestamps + copyJob = KIO::setModificationTime( saveUrl, QDateTime::currentDateTime() ); + } + } } - return false; } - KIO::Job *copyJob = KIO::file_copy( QUrl::fromLocalFile(fileName), saveUrl, -1, KIO::Overwrite ); + // Stop watching for changes while we write the new file (useful when + // overwriting) + if ( url().isLocalFile() ) + unsetFileToWatch(); + KJobWidgets::setWindow(copyJob, widget()); if ( !copyJob->exec() ) { - KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", saveUrl.toDisplayString() ) ); + KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Error: '%2'. Try to save it to another location.", saveUrl.toDisplayString(), copyJob->errorString() ) ); + + // Restore watcher + if ( url().isLocalFile() ) + setFileToWatch( localFilePath() ); + return false; } - setModified( false ); - return true; -} - + m_document->setHistoryClean( true ); -void Part::slotSaveCopyAs() -{ - if ( m_embedMode == PrintPreviewMode ) - return; + if ( m_document->isDocdataMigrationNeeded() ) + m_document->docdataMigrationDone(); - QUrl saveUrl = QFileDialog::getSaveFileUrl( widget(), QString(), url()); + bool reloadedCorrectly = true; - if ( saveUrl.isValid() && !saveUrl.isEmpty() ) + // Make the generator use the new new file instead of the old one + if ( m_document->canSwapBackingFile() && !m_documentOpenWithPassword ) { - // make use of the already downloaded (in case of remote URLs) file, - // no point in downloading that again - QUrl srcUrl = QUrl::fromLocalFile( localFilePath() ); - QTemporaryFile * tempFile = nullptr; - // duh, our local file disappeared... - if ( !QFile::exists( localFilePath() ) ) + // this calls openFile internally, which in turn actually calls + // m_document->swapBackingFile() instead of the regular loadDocument + if ( openUrl( saveUrl, true /* swapInsteadOfOpening */ ) ) { - if ( url().isLocalFile() ) + if ( setModifiedAfterSave ) { -#ifdef OKULAR_KEEP_FILE_OPEN - // local file: try to get it back from the open handle on it - if ( ( tempFile = m_keeper->copyToTemporary() ) ) - srcUrl = QUrl::fromLocalFile( tempFile->fileName() ); -#else - const QString msg = i18n( "Okular cannot copy %1 to the specified location.\n\nThe document does not exist anymore.", localFilePath() ); - KMessageBox::sorry( widget(), msg ); - return; -#endif + m_document->setHistoryClean( false ); } - else + } + else + { + reloadedCorrectly = false; + } + } + else + { + // If the generator doesn't support swapping file, then just reload + // the document from the new location + if ( !slotAttemptReload( true, saveUrl ) ) + reloadedCorrectly = false; + } + + // In case of file swapping errors, close the document to avoid inconsistencies + if ( !reloadedCorrectly ) + { + qWarning() << "The document hasn't been reloaded/swapped correctly"; + closeUrl(); + } + + // Restore watcher + if ( url().isLocalFile() ) + setFileToWatch( localFilePath() ); + + return true; +} + +// If the user wants to save in the original file's format, some features might +// not be available. Find out what cannot be saved in this format +void Part::checkNativeSaveDataLoss(bool *out_wontSaveForms, bool *out_wontSaveAnnotations) const +{ + bool wontSaveForms = false; + bool wontSaveAnnotations = false; + + if ( !m_document->canSaveChanges( Document::SaveFormsCapability ) ) + { + /* Set wontSaveForms only if there are forms */ + const int pagecount = m_document->pages(); + + for ( int pageno = 0; pageno < pagecount; ++pageno ) + { + const Okular::Page *page = m_document->page( pageno ); + if ( !page->formFields().empty() ) { - // we still have the original remote URL of the document, - // so copy the document from there - srcUrl = url(); + wontSaveForms = true; + break; } } + } - KIO::Job *copyJob = KIO::file_copy( srcUrl, saveUrl, -1, KIO::Overwrite ); - KJobWidgets::setWindow(copyJob, widget()); - if ( !copyJob->exec() ) - KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", saveUrl.toDisplayString() ) ); + if ( !m_document->canSaveChanges( Document::SaveAnnotationsCapability ) ) + { + /* Set wontSaveAnnotations only if there are local annotations */ + const int pagecount = m_document->pages(); - delete tempFile; + for ( int pageno = 0; pageno < pagecount; ++pageno ) + { + const Okular::Page *page = m_document->page( pageno ); + foreach ( const Okular::Annotation *ann, page->annotations() ) + { + if ( !(ann->flags() & Okular::Annotation::External) ) + { + wontSaveAnnotations = true; + break; + } + } + if ( wontSaveAnnotations ) + break; + } } -} + *out_wontSaveForms = wontSaveForms; + *out_wontSaveAnnotations = wontSaveAnnotations; +} void Part::slotGetNewStuff() { #if 0 KNS::Engine engine(widget()); engine.init( "okular.knsrc" ); // show the modal dialog over pageview and execute it KNS::Entry::List entries = engine.downloadDialogModal( m_pageView ); Q_UNUSED( entries ) #endif } void Part::slotPreferences() { // Create dialog PreferencesDialog * dialog = new PreferencesDialog( m_pageView, Okular::Settings::self(), m_embedMode ); dialog->setAttribute( Qt::WA_DeleteOnClose ); // Show it dialog->show(); } void Part::slotAnnotationPreferences() { // Create dialog PreferencesDialog * dialog = new PreferencesDialog( m_pageView, Okular::Settings::self(), m_embedMode ); dialog->setAttribute( Qt::WA_DeleteOnClose ); // Show it dialog->switchToAnnotationsPage(); dialog->show(); } void Part::slotNewConfig() { // Apply settings here. A good policy is to check whether the setting has // changed before applying changes. // Watch File setWatchFileModeEnabled(Okular::Settings::watchFile()); // Main View (pageView) m_pageView->reparseConfig(); // update document settings m_document->reparseConfig(); // update TOC settings if ( m_sidebar->isItemEnabled(m_toc) ) m_toc->reparseConfig(); // update ThumbnailList contents if ( Okular::Settings::showLeftPanel() && !m_thumbnailList->isHidden() ) m_thumbnailList->updateWidgets(); // update Reviews settings if ( m_sidebar->isItemEnabled(m_reviewsWidget) ) m_reviewsWidget->reparseConfig(); setWindowTitleFromDocument (); if ( m_presentationDrawingActions ) { m_presentationDrawingActions->reparseConfig(); if (factory()) { factory()->refreshActionProperties(); } } } void Part::slotPrintPreview() { if (m_document->pages() == 0) return; QPrinter printer; QString tempFilePattern; if ( m_document->printingSupport() == Okular::Document::PostscriptPrinting ) { tempFilePattern = (QDir::tempPath() + QLatin1String("/okular_XXXXXX.ps")); } else if ( m_document->printingSupport() == Okular::Document::NativePrinting ) { tempFilePattern = (QDir::tempPath() + QLatin1String("/okular_XXXXXX.pdf")); } else { return; } // Generate a temp filename for Print to File, then release the file so generator can write to it QTemporaryFile tf(tempFilePattern); tf.setAutoRemove( true ); tf.open(); printer.setOutputFileName( tf.fileName() ); tf.close(); setupPrint( printer ); doPrint( printer ); if ( QFile::exists( printer.outputFileName() ) ) { Okular::FilePrinterPreview previewdlg( printer.outputFileName(), widget() ); previewdlg.exec(); } } void Part::slotShowTOCMenu(const Okular::DocumentViewport &vp, const QPoint &point, const QString &title) { showMenu(m_document->page(vp.pageNumber), point, title, vp); } void Part::slotShowMenu(const Okular::Page *page, const QPoint &point) { showMenu(page, point); } void Part::showMenu(const Okular::Page *page, const QPoint &point, const QString &bookmarkTitle, const Okular::DocumentViewport &vp) { if ( m_embedMode == PrintPreviewMode ) return; bool reallyShow = false; const bool currentPage = page && page->number() == m_document->viewport().pageNumber; if (!m_actionsSearched) { // the quest for options_show_menubar KActionCollection *ac; QAction *act; if (factory()) { const QList clients(factory()->clients()); for(int i = 0 ; (!m_showMenuBarAction || !m_showFullScreenAction) && i < clients.size(); ++i) { ac = clients.at(i)->actionCollection(); // show_menubar act = ac->action(QStringLiteral("options_show_menubar")); if (act && qobject_cast(act)) m_showMenuBarAction = qobject_cast(act); // fullscreen act = ac->action(QStringLiteral("fullscreen")); if (act && qobject_cast(act)) m_showFullScreenAction = qobject_cast(act); } } m_actionsSearched = true; } QMenu *popup = new QMenu( widget() ); QAction *addBookmark = nullptr; QAction *removeBookmark = nullptr; QAction *fitPageWidth = nullptr; if (page) { popup->addAction( new OKMenuTitle( popup, i18n( "Page %1", page->number() + 1 ) ) ); if ( ( !currentPage && m_document->bookmarkManager()->isBookmarked( page->number() ) ) || ( currentPage && m_document->bookmarkManager()->isBookmarked( m_document->viewport() ) ) ) removeBookmark = popup->addAction( QIcon::fromTheme(QStringLiteral("edit-delete-bookmark")), i18n("Remove Bookmark") ); else addBookmark = popup->addAction( QIcon::fromTheme(QStringLiteral("bookmark-new")), i18n("Add Bookmark") ); if ( m_pageView->canFitPageWidth() ) fitPageWidth = popup->addAction( QIcon::fromTheme(QStringLiteral("zoom-fit-best")), i18n("Fit Width") ); popup->addAction( m_prevBookmark ); popup->addAction( m_nextBookmark ); reallyShow = true; } if ((m_showMenuBarAction && !m_showMenuBarAction->isChecked()) || (m_showFullScreenAction && m_showFullScreenAction->isChecked())) { popup->addAction( new OKMenuTitle( popup, i18n( "Tools" ) ) ); if (m_showMenuBarAction && !m_showMenuBarAction->isChecked()) popup->addAction(m_showMenuBarAction); if (m_showFullScreenAction && m_showFullScreenAction->isChecked()) popup->addAction(m_showFullScreenAction); reallyShow = true; } if (reallyShow) { QAction *res = popup->exec(point); if (res) { if (res == addBookmark) { if ( currentPage && bookmarkTitle.isEmpty() ) m_document->bookmarkManager()->addBookmark( m_document->viewport() ); else if ( !bookmarkTitle.isEmpty() ) m_document->bookmarkManager()->addBookmark( m_document->currentDocument(), vp, bookmarkTitle ); else m_document->bookmarkManager()->addBookmark( page->number() ); } else if (res == removeBookmark) { if (currentPage) m_document->bookmarkManager()->removeBookmark( m_document->viewport() ); else m_document->bookmarkManager()->removeBookmark( page->number() ); } else if (res == fitPageWidth) { m_pageView->fitPageWidth( page->number() ); } } } delete popup; } void Part::slotShowProperties() { PropertiesDialog *d = new PropertiesDialog(widget(), m_document); connect(d, &QDialog::finished, d, &QObject::deleteLater); d->open(); } void Part::slotShowEmbeddedFiles() { EmbeddedFilesDialog *d = new EmbeddedFilesDialog(widget(), m_document); connect(d, &QDialog::finished, d, &QObject::deleteLater); d->open(); } void Part::slotShowPresentation() { if ( !m_presentationWidget ) { m_presentationWidget = new PresentationWidget( widget(), m_document, m_presentationDrawingActions, actionCollection() ); } } void Part::slotHidePresentation() { if ( m_presentationWidget ) delete (PresentationWidget*) m_presentationWidget; } void Part::slotTogglePresentation() { if ( m_document->isOpened() ) { if ( !m_presentationWidget ) m_presentationWidget = new PresentationWidget( widget(), m_document, m_presentationDrawingActions, actionCollection() ); else delete (PresentationWidget*) m_presentationWidget; } } void Part::reload() { if ( m_document->isOpened() ) { slotReload(); } } void Part::enableStartWithPrint() { m_cliPrint = true; } void Part::slotAboutBackend() { const KPluginMetaData data = m_document->generatorInfo(); if (!data.isValid()) return; KAboutData aboutData = KAboutData::fromPluginMetaData(data); QIcon icon = QIcon::fromTheme(data.iconName()); // fall back to mime type icon if (icon.isNull()) { const Okular::DocumentInfo documentInfo = m_document->documentInfo(QSet() << DocumentInfo::MimeType); const QString mimeTypeName = documentInfo.get(DocumentInfo::MimeType); if (!mimeTypeName.isEmpty()) { QMimeDatabase db; QMimeType type = db.mimeTypeForName(mimeTypeName); if (type.isValid()) { icon = QIcon::fromTheme(type.iconName()); } } } if (!icon.isNull()) { // 48x48 is what KAboutApplicationDialog wants, which doesn't match any default so we hardcode it aboutData.setProgramLogo(icon.pixmap(48, 48)); } KAboutApplicationDialog dlg(aboutData, widget()); dlg.exec(); } void Part::slotExportAs(QAction * act) { QList acts = m_exportAs->menu() ? m_exportAs->menu()->actions() : QList(); int id = acts.indexOf( act ); if ( ( id < 0 ) || ( id >= acts.count() ) ) return; QMimeDatabase mimeDatabase; QMimeType mimeType; switch ( id ) { case 0: mimeType = mimeDatabase.mimeTypeForName(QStringLiteral("text/plain")); break; - case 1: - mimeType = mimeDatabase.mimeTypeForName(QStringLiteral("application/vnd.kde.okular-archive")); - break; default: mimeType = m_exportFormats.at( id - 2 ).mimeType(); break; } QString filter = i18nc("File type name and pattern", "%1 (%2)", mimeType.comment(), mimeType.globPatterns().join(QLatin1Char(' '))); QString fileName = QFileDialog::getSaveFileName( widget(), QString(), QString(), filter); if ( !fileName.isEmpty() ) { bool saved = false; switch ( id ) { case 0: saved = m_document->exportToText( fileName ); break; - case 1: - saved = m_document->saveDocumentArchive( fileName ); - break; default: - saved = m_document->exportTo( fileName, m_exportFormats.at( id - 2 ) ); + saved = m_document->exportTo( fileName, m_exportFormats.at( id - 1 ) ); break; } if ( !saved ) KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", fileName ) ); } } void Part::slotReload() { // stop the dirty handler timer, otherwise we may conflict with the // auto-refresh system m_dirtyHandler->stop(); - slotDoFileDirty(); + slotAttemptReload(); } void Part::slotPrint() { if (m_document->pages() == 0) return; #ifdef Q_OS_WIN QPrinter printer(QPrinter::HighResolution); #else QPrinter printer; #endif QPrintDialog *printDialog = nullptr; QWidget *printConfigWidget = nullptr; // Must do certain QPrinter setup before creating QPrintDialog setupPrint( printer ); // Create the Print Dialog with extra config widgets if required if ( m_document->canConfigurePrinter() ) { printConfigWidget = m_document->printConfigurationWidget(); } printDialog = new QPrintDialog(&printer, widget()); printDialog->setWindowTitle(i18nc("@title:window", "Print")); QList options; if (printConfigWidget) { options << printConfigWidget; } printDialog->setOptionTabs(options); if ( printDialog ) { // Set the available Print Range printDialog->setMinMax( 1, m_document->pages() ); printDialog->setFromTo( 1, m_document->pages() ); // If the user has bookmarked pages for printing, then enable Selection if ( !m_document->bookmarkedPageRange().isEmpty() ) { printDialog->addEnabledOption( QAbstractPrintDialog::PrintSelection ); } // If the Document type doesn't support print to both PS & PDF then disable the Print Dialog option if ( printDialog->isOptionEnabled( QAbstractPrintDialog::PrintToFile ) && !m_document->supportsPrintToFile() ) { printDialog->setEnabledOptions( printDialog->enabledOptions() ^ QAbstractPrintDialog::PrintToFile ); } // Enable the Current Page option in the dialog. if ( m_document->pages() > 1 && currentPage() > 0 ) { printDialog->setOption( QAbstractPrintDialog::PrintCurrentPage ); } if ( printDialog->exec() ) doPrint( printer ); delete printDialog; } } void Part::setupPrint( QPrinter &printer ) { printer.setOrientation(m_document->orientation()); // title QString title = m_document->metaData( QStringLiteral("DocumentTitle") ).toString(); if ( title.isEmpty() ) { title = m_document->currentDocument().fileName(); } if ( !title.isEmpty() ) { printer.setDocName( title ); } } void Part::doPrint(QPrinter &printer) { if (!m_document->isAllowed(Okular::AllowPrint)) { KMessageBox::error(widget(), i18n("Printing this document is not allowed.")); return; } if (!m_document->print(printer)) { const QString error = m_document->printError(); if (error.isEmpty()) { KMessageBox::error(widget(), i18n("Could not print the document. Unknown error. Please report to bugs.kde.org")); } else { KMessageBox::error(widget(), i18n("Could not print the document. Detailed error is \"%1\". Please report to bugs.kde.org", error)); } } } void Part::psTransformEnded(int exit, QProcess::ExitStatus status) { Q_UNUSED( exit ) if ( status != QProcess::NormalExit ) return; QProcess *senderobj = sender() ? qobject_cast< QProcess * >( sender() ) : 0; if ( senderobj ) { senderobj->close(); senderobj->deleteLater(); } setLocalFilePath( m_temporaryLocalFile ); openUrl( QUrl::fromLocalFile(m_temporaryLocalFile) ); m_temporaryLocalFile.clear(); } void Part::displayInfoMessage( const QString &message, KMessageWidget::MessageType messageType, int duration ) { if ( !Okular::Settings::showOSD() ) { if (messageType == KMessageWidget::Error) { KMessageBox::error( widget(), message ); } return; } // hide messageWindow if string is empty if ( message.isEmpty() ) m_infoMessage->animatedHide(); // display message (duration is length dependant) if ( duration < 0 ) { duration = 500 + 100 * message.length(); } m_infoTimer->start( duration ); m_infoMessage->setText( message ); m_infoMessage->setMessageType( messageType ); m_infoMessage->setVisible( true ); } void Part::errorMessage( const QString &message, int duration ) { displayInfoMessage( message, KMessageWidget::Error, duration ); } void Part::warningMessage( const QString &message, int duration ) { displayInfoMessage( message, KMessageWidget::Warning, duration ); } void Part::noticeMessage( const QString &message, int duration ) { // less important message -> simpleer display widget in the PageView m_pageView->displayMessage( message, QString(), PageViewMessage::Info, duration ); } void Part::moveSplitter(int sideWidgetSize) { m_sidebar->moveSplitter( sideWidgetSize ); } void Part::unsetDummyMode() { if ( m_embedMode == PrintPreviewMode ) return; m_sidebar->setItemEnabled( m_reviewsWidget, true ); m_sidebar->setItemEnabled( m_bookmarkList, true ); m_sidebar->setSidebarVisibility( Okular::Settings::showLeftPanel() ); // add back and next in history m_historyBack = KStandardAction::documentBack( this, SLOT(slotHistoryBack()), actionCollection() ); m_historyBack->setWhatsThis( i18n( "Go to the place you were before" ) ); connect(m_pageView.data(), &PageView::mouseBackButtonClick, m_historyBack, &QAction::trigger); m_historyNext = KStandardAction::documentForward( this, SLOT(slotHistoryNext()), actionCollection()); m_historyNext->setWhatsThis( i18n( "Go to the place you were after" ) ); connect(m_pageView.data(), &PageView::mouseForwardButtonClick, m_historyNext, &QAction::trigger); m_pageView->setupActions( actionCollection() ); // attach the actions of the children widgets too m_formsMessage->addAction( m_pageView->toggleFormsAction() ); m_formsMessage->setVisible( m_pageView->toggleFormsAction() != nullptr ); // ensure history actions are in the correct state updateViewActions(); } bool Part::handleCompressed( QString &destpath, const QString &path, KFilterDev::CompressionType compressionType) { m_tempfile = nullptr; // we are working with a compressed file, decompressing // temporary file for decompressing QTemporaryFile *newtempfile = new QTemporaryFile(); newtempfile->setAutoRemove(true); if ( !newtempfile->open() ) { KMessageBox::error( widget(), i18n("File Error! Could not create temporary file " "%1.", newtempfile->errorString())); delete newtempfile; return false; } // decompression filer KCompressionDevice dev( path, compressionType ); if ( !dev.open(QIODevice::ReadOnly) ) { KMessageBox::detailedError( widget(), i18n("File Error! Could not open the file " "%1 for uncompression. " "The file will not be loaded.", path), i18n("This error typically occurs if you do " "not have enough permissions to read the file. " "You can check ownership and permissions if you " "right-click on the file in the Dolphin " "file manager and then choose the 'Properties' tab.")); delete newtempfile; return false; } char buf[65536]; int read = 0, wrtn = 0; while ((read = dev.read(buf, sizeof(buf))) > 0) { wrtn = newtempfile->write(buf, read); if ( read != wrtn ) break; } if ((read != 0) || (newtempfile->size() == 0)) { KMessageBox::detailedError(widget(), i18n("File Error! Could not uncompress " "the file %1. " "The file will not be loaded.", path ), i18n("This error typically occurs if the file is corrupt. " "If you want to be sure, try to decompress the file manually " "using command-line tools.")); delete newtempfile; return false; } m_tempfile = newtempfile; destpath = m_tempfile->fileName(); return true; } void Part::rebuildBookmarkMenu( bool unplugActions ) { if ( unplugActions ) { unplugActionList( QStringLiteral("bookmarks_currentdocument") ); qDeleteAll( m_bookmarkActions ); m_bookmarkActions.clear(); } QUrl u = m_document->currentDocument(); if ( u.isValid() ) { m_bookmarkActions = m_document->bookmarkManager()->actionsForUrl( u ); } bool havebookmarks = true; if ( m_bookmarkActions.isEmpty() ) { havebookmarks = false; QAction * a = new QAction( nullptr ); a->setText( i18n( "No Bookmarks" ) ); a->setEnabled( false ); m_bookmarkActions.append( a ); } plugActionList( QStringLiteral("bookmarks_currentdocument"), m_bookmarkActions ); if (factory()) { const QList clients(factory()->clients()); bool containerFound = false; for (int i = 0; !containerFound && i < clients.size(); ++i) { QMenu *container = dynamic_cast(factory()->container(QStringLiteral("bookmarks"), clients[i])); if (container && container->actions().contains(m_bookmarkActions.first())) { container->installEventFilter(this); containerFound = true; } } } m_prevBookmark->setEnabled( havebookmarks ); m_nextBookmark->setEnabled( havebookmarks ); } bool Part::eventFilter(QObject * watched, QEvent * event) { switch (event->type()) { case QEvent::ContextMenu: { QContextMenuEvent *e = static_cast(event); QMenu *menu = static_cast(watched); QScopedPointer ctxMenu(new QMenu); QPoint pos; bool ret = false; if (e->reason() == QContextMenuEvent::Mouse) { pos = e->pos(); ret = aboutToShowContextMenu(menu, menu->actionAt(e->pos()), ctxMenu.data()); } else if (menu->activeAction()) { pos = menu->actionGeometry(menu->activeAction()).center(); ret = aboutToShowContextMenu(menu, menu->activeAction(), ctxMenu.data()); } ctxMenu->exec(menu->mapToGlobal(pos)); if (ret) { event->accept(); } return ret; } default: break; } return false; } void Part::updateAboutBackendAction() { const KPluginMetaData data = m_document->generatorInfo(); m_aboutBackend->setEnabled(data.isValid()); } void Part::resetStartArguments() { m_cliPrint = false; } #if PURPOSE_FOUND void Part::slotShareActionFinished(const QJsonObject &output, int error, const QString &message) { if (error) { KMessageBox::error(widget(), i18n("There was a problem sharing the document: %1", message), i18n("Share")); } else { const QString url = output["url"].toString(); if (url.isEmpty()) { m_pageView->displayMessage(i18n("Document shared successfully")); } else { KMessageBox::information(widget(), i18n("You can find the shared document at: %1", url), i18n("Share"), QString(), KMessageBox::Notify | KMessageBox::AllowLink); } } } #endif void Part::setReadWrite(bool readwrite) { m_document->setAnnotationEditingEnabled( readwrite ); ReadWritePart::setReadWrite( readwrite ); } } // namespace Okular #include "part.moc" /* kate: replace-tabs on; indent-width 4; */ diff --git a/part.h b/part.h index a0553731b..8d85ccb06 100644 --- a/part.h +++ b/part.h @@ -1,393 +1,414 @@ /*************************************************************************** * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2003-2004 by Christophe Devriese * * * * Copyright (C) 2003 by Andy Goossens * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2004 by Dominique Devriese * * Copyright (C) 2004-2007 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. * ***************************************************************************/ #ifndef _PART_H_ #define _PART_H_ #include #include #include #include #include #include #include #include #include #include #include #include "core/observer.h" #include "core/document.h" #include "kdocumentviewer.h" #include "interfaces/viewerinterface.h" #include "okularpart_export.h" #include class QAction; class QWidget; class QPrinter; class QMenu; class KConfigDialog; class KConfigGroup; class KDirWatch; class KToggleAction; class KToggleFullScreenAction; class KSelectAction; class KAboutData; class QTemporaryFile; class QAction; class QJsonObject; namespace KParts { class GUIActivateEvent; } class FindBar; class ThumbnailList; class PageSizeLabel; class PageView; class PresentationWidget; class ProgressWidget; class SearchWidget; class Sidebar; class TOC; class MiniBar; class MiniBarLogic; class FileKeeper; class Reviews; class BookmarkList; class DrawingToolActions; class Layers; #if PURPOSE_FOUND namespace Purpose { class Menu; } #endif namespace Okular { class BrowserExtension; class ExportFormat; /** * Describes the possible embedding modes of the part * * @since 0.14 (KDE 4.8) */ enum EmbedMode { UnknownEmbedMode, NativeShellMode, // embedded in the native Okular' shell PrintPreviewMode, // embedded to show the print preview of a document KHTMLPartMode, // embedded in KHTML ViewerWidgetMode // the part acts as a widget that can display all kinds of documents }; /** * This is a "Part". It that does all the real work in a KPart * application. * * @short Main Part * @author Wilco Greven * @version 0.2 */ class OKULARPART_EXPORT Part : public KParts::ReadWritePart, public Okular::DocumentObserver, public KDocumentViewer, public Okular::ViewerInterface { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.okular") Q_INTERFACES(KDocumentViewer) Q_INTERFACES(Okular::ViewerInterface) friend class PartTest; public: // Default constructor /** * If one element of 'args' contains one of the strings "Print/Preview" or "ViewerWidget", * the part will be set up in the corresponding mode. Additionally, it is possible to specify * which config file should be used by adding a string containing "ConfigFileName=" * to 'args'. **/ Part(QWidget* parentWidget, QObject* parent, const QVariantList& args); // Destructor ~Part(); // inherited from DocumentObserver void notifySetup( const QVector< Okular::Page * > &pages, int setupFlags ) override; void notifyViewportChanged( bool smoothMove ) override; void notifyPageChanged( int page, int flags ) override; bool openDocument(const QUrl &url, uint page) override; void startPresentation() override; QStringList supportedMimeTypes() const override; QUrl realUrl() const; void showSourceLocation(const QString& fileName, int line, int column, bool showGraphically = true) override; void clearLastShownSourceLocation() override; bool isWatchFileModeEnabled() const override; void setWatchFileModeEnabled(bool enable) override; bool areSourceLocationsShownGraphically() const override; void setShowSourceLocationsGraphically(bool show) override; bool openNewFilesInTabs() const override; public Q_SLOTS: // dbus Q_SCRIPTABLE Q_NOREPLY void goToPage(uint page) override; Q_SCRIPTABLE Q_NOREPLY void openDocument( const QString &doc ); Q_SCRIPTABLE uint pages(); Q_SCRIPTABLE uint currentPage(); Q_SCRIPTABLE QString currentDocument(); Q_SCRIPTABLE QString documentMetaData( const QString &metaData ) const; Q_SCRIPTABLE void slotPreferences(); Q_SCRIPTABLE void slotFind(); Q_SCRIPTABLE void slotPrintPreview(); Q_SCRIPTABLE void slotPreviousPage(); Q_SCRIPTABLE void slotNextPage(); Q_SCRIPTABLE void slotGotoFirst(); Q_SCRIPTABLE void slotGotoLast(); Q_SCRIPTABLE void slotTogglePresentation(); Q_SCRIPTABLE Q_NOREPLY void reload(); Q_SCRIPTABLE Q_NOREPLY void enableStartWithPrint(); Q_SIGNALS: void enablePrintAction(bool enable); void openSourceReference(const QString& absFileName, int line, int column); void viewerMenuStateChange(bool enabled); void enableCloseAction(bool enable); void mimeTypeChanged(QMimeType mimeType); void urlsDropped( const QList& urls ); void fitWindowToPage( const QSize& pageViewPortSize, const QSize& pageSize ); protected: // reimplemented from KParts::ReadWritePart bool openFile() override; bool openUrl(const QUrl &url) override; void guiActivateEvent(KParts::GUIActivateEvent *event) override; void displayInfoMessage( const QString &message, KMessageWidget::MessageType messageType = KMessageWidget::Information, int duration = -1 ); public: - bool saveFile() override; bool queryClose() override; bool closeUrl() override; bool closeUrl(bool promptToSave) override; void setReadWrite(bool readwrite) override; bool saveAs(const QUrl & saveUrl) override; protected Q_SLOTS: // connected to actions void openUrlFromDocument(const QUrl &url); void openUrlFromBookmarks(const QUrl &url); void handleDroppedUrls( const QList& urls ); void slotGoToPage(); void slotHistoryBack(); void slotHistoryNext(); void slotAddBookmark(); void slotRenameBookmarkFromMenu(); void slotRemoveBookmarkFromMenu(); void slotRenameCurrentViewportBookmark(); void slotPreviousBookmark(); void slotNextBookmark(); void slotFindNext(); void slotFindPrev(); - void slotSaveFileAs(); - void slotSaveCopyAs(); + bool slotSaveFileAs(bool showOkularArchiveAsDefaultFormat = false); void slotGetNewStuff(); void slotNewConfig(); void slotShowMenu(const Okular::Page *page, const QPoint &point); void slotShowTOCMenu(const Okular::DocumentViewport &vp, const QPoint &point, const QString &title); void slotShowProperties(); void slotShowEmbeddedFiles(); void slotShowLeftPanel(); void slotShowBottomBar(); void slotShowPresentation(); void slotHidePresentation(); void slotExportAs(QAction *); bool slotImportPSFile(); void slotAboutBackend(); void slotReload(); void close(); void cannotQuit(); void slotShowFindBar(); void slotHideFindBar(); void slotJobStarted(KIO::Job *job); void slotJobFinished(KJob *job); void loadCancelled(const QString &reason); void setWindowTitleFromDocument(); // can be connected to widget elements void updateViewActions(); void updateBookmarksActions(); void enableTOC(bool enable); void slotRebuildBookmarkMenu(); void enableLayers( bool enable ); public Q_SLOTS: + bool saveFile() override; // connected to Shell action (and browserExtension), not local one void slotPrint(); void slotFileDirty( const QString& ); - void slotDoFileDirty(); + bool slotAttemptReload( bool oneShot = false, const QUrl &newUrl = QUrl() ); void psTransformEnded(int, QProcess::ExitStatus); KConfigDialog * slotGeneratorPreferences(); void errorMessage( const QString &message, int duration = 0 ); void warningMessage( const QString &message, int duration = -1 ); void noticeMessage( const QString &message, int duration = -1 ); void moveSplitter( const int sideWidgetSize ); private: bool aboutToShowContextMenu(QMenu *menu, QAction *action, QMenu *contextMenu); void showMenu(const Okular::Page *page, const QPoint &point, const QString &bookmarkTitle = QString(), const Okular::DocumentViewport &vp = DocumentViewport()); bool eventFilter(QObject * watched, QEvent * event) override; Document::OpenResult doOpenFile(const QMimeType &mime, const QString &fileNameToOpen, bool *isCompressedFile); + bool openUrl( const QUrl &url, bool swapInsteadOfOpening ); void setupViewerActions(); void setViewerShortcuts(); void setupActions(); void setupPrint( QPrinter &printer ); void doPrint( QPrinter &printer ); bool handleCompressed(QString &destpath, const QString &path, KCompressionDevice::CompressionType compressionType ); void rebuildBookmarkMenu( bool unplugActions = true ); void updateAboutBackendAction(); void unsetDummyMode(); void slotRenameBookmark( const DocumentViewport &viewport ); void slotRemoveBookmark( const DocumentViewport &viewport ); void resetStartArguments(); + void checkNativeSaveDataLoss(bool *out_wontSaveForms, bool *out_wontSaveAnnotations) const; + + enum SaveAsFlag + { + NoSaveAsFlags = 0, ///< No options + SaveAsOkularArchive = 1 ///< Save as Okular Archive (.okular) instead of document's native format + }; + Q_DECLARE_FLAGS( SaveAsFlags, SaveAsFlag ) + + bool saveAs( const QUrl & saveUrl, SaveAsFlags flags ); + + void setFileToWatch( const QString &filePath ); + void unsetFileToWatch(); #if PURPOSE_FOUND void slotShareActionFinished(const QJsonObject &output, int error, const QString &message); #endif static int numberOfParts; QTemporaryFile *m_tempfile; // the document Okular::Document * m_document; QString m_temporaryLocalFile; bool isDocumentArchive; + bool m_documentOpenWithPassword; + bool m_swapInsteadOfOpening; // if set, the next open operation will replace the backing file (used when reloading just saved files) // main widgets Sidebar *m_sidebar; SearchWidget *m_searchWidget; FindBar * m_findBar; + KMessageWidget * m_migrationMessage; KMessageWidget * m_topMessage; KMessageWidget * m_formsMessage; KMessageWidget * m_infoMessage; QPointer m_thumbnailList; QPointer m_pageView; QPointer m_toc; QPointer m_miniBarLogic; QPointer m_miniBar; QPointer m_pageNumberTool; QPointer m_bottomBar; QPointer m_presentationWidget; QPointer m_progressWidget; QPointer m_pageSizeLabel; QPointer m_reviewsWidget; QPointer m_bookmarkList; QPointer m_layers; // document watcher (and reloader) variables KDirWatch *m_watcher; + QString m_watchedFilePath, m_watchedFileSymlinkTarget; QTimer *m_dirtyHandler; QUrl m_oldUrl; Okular::DocumentViewport m_viewportDirty; bool m_isReloading; bool m_wasPresentationOpen; QWidget *m_dirtyToolboxItem; bool m_wasSidebarVisible; bool m_wasSidebarCollapsed; bool m_fileWasRemoved; Rotation m_dirtyPageRotation; // Remember the search history QStringList m_searchHistory; // actions QAction *m_gotoPage; QAction *m_prevPage; QAction *m_nextPage; QAction *m_beginningOfDocument; QAction *m_endOfDocument; QAction *m_historyBack; QAction *m_historyNext; QAction *m_addBookmark; QAction *m_renameBookmark; QAction *m_prevBookmark; QAction *m_nextBookmark; QAction *m_copy; QAction *m_selectAll; QAction *m_find; QAction *m_findNext; QAction *m_findPrev; + QAction *m_save; QAction *m_saveAs; QAction *m_saveCopyAs; QAction *m_printPreview; QAction *m_showProperties; QAction *m_showEmbeddedFiles; QAction *m_exportAs; QAction *m_exportAsText; QAction *m_exportAsDocArchive; #if PURPOSE_FOUND QAction *m_share; #endif QAction *m_showPresentation; KToggleAction* m_showMenuBarAction; KToggleAction* m_showLeftPanel; KToggleAction* m_showBottomBar; KToggleFullScreenAction* m_showFullScreenAction; QAction *m_aboutBackend; QAction *m_reload; QMenu *m_exportAsMenu; #if PURPOSE_FOUND Purpose::Menu *m_shareMenu; #endif QAction *m_closeFindBar; DrawingToolActions *m_presentationDrawingActions; bool m_actionsSearched; BrowserExtension *m_bExtension; QList m_exportFormats; QList m_bookmarkActions; bool m_cliPresentation; bool m_cliPrint; QString m_addBookmarkText; QIcon m_addBookmarkIcon; EmbedMode m_embedMode; QUrl m_realUrl; KXMLGUIClient *m_generatorGuiClient; FileKeeper *m_keeper; // Timer for m_infoMessage QTimer *m_infoTimer; QString m_registerDbusName; private Q_SLOTS: void slotAnnotationPreferences(); void slotHandleActivatedSourceReference(const QString& absFileName, int line, int col, bool *handled); }; } #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/part.rc b/part.rc index 4212a1dab..4eba35d00 100644 --- a/part.rc +++ b/part.rc @@ -1,107 +1,107 @@ - + &File + - &Edit &View &Orientation &Go &Bookmarks &Tools &Settings &Help Main Toolbar diff --git a/shell/shell.cpp b/shell/shell.cpp index 717edc70a..16290dbcc 100644 --- a/shell/shell.cpp +++ b/shell/shell.cpp @@ -1,766 +1,791 @@ /*************************************************************************** * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2002 by Chris Cheney * * Copyright (C) 2003 by Benjamin Meyer * * Copyright (C) 2003-2004 by Christophe Devriese * * * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003-2004 by Albert Astals Cid * * Copyright (C) 2003 by Luboš Luňák * * Copyright (C) 2003 by Malcolm Hunter * * Copyright (C) 2004 by Dominique Devriese * * Copyright (C) 2004 by Dirk Mueller * + * 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 "shell.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef Q_OS_WIN #include #endif // local includes #include "kdocumentviewer.h" #include "../interfaces/viewerinterface.h" #include "shellutils.h" static const char *shouldShowMenuBarComingFromFullScreen = "shouldShowMenuBarComingFromFullScreen"; static const char *shouldShowToolBarComingFromFullScreen = "shouldShowToolBarComingFromFullScreen"; static const char* const SESSION_URL_KEY = "Urls"; static const char* const SESSION_TAB_KEY = "ActiveTab"; Shell::Shell( const QString &serializedOptions ) : KParts::MainWindow(), m_menuBarWasShown(true), m_toolBarWasShown(true) #ifndef Q_OS_WIN , m_activityResource(nullptr) #endif , m_isValid(true) { setObjectName( QStringLiteral( "okular::Shell#" ) ); setContextMenuPolicy( Qt::NoContextMenu ); // otherwise .rc file won't be found by unit test setComponentName(QStringLiteral("okular"), QString()); // set the shell's ui resource file setXMLFile(QStringLiteral("shell.rc")); m_fileformatsscanned = false; m_showMenuBarAction = nullptr; // this routine will find and load our Part. it finds the Part by // name which is a bad idea usually.. but it's alright in this // case since our Part is made for this Shell KPluginLoader loader(QStringLiteral("okularpart")); m_partFactory = loader.factory(); if (!m_partFactory) { // if we couldn't find our Part, we exit since the Shell by // itself can't do anything useful m_isValid = false; KMessageBox::error(this, i18n("Unable to find the Okular component: %1", loader.errorString())); return; } // now that the Part plugin is loaded, create the part KParts::ReadWritePart* const firstPart = m_partFactory->create< KParts::ReadWritePart >( this ); if (firstPart) { // Setup tab bar m_tabWidget = new QTabWidget( this ); m_tabWidget->setTabsClosable( true ); m_tabWidget->setElideMode( Qt::ElideRight ); m_tabWidget->tabBar()->hide(); m_tabWidget->setDocumentMode( true ); m_tabWidget->setMovable( true ); m_tabWidget->setAcceptDrops(true); m_tabWidget->installEventFilter(this); connect( m_tabWidget, &QTabWidget::currentChanged, this, &Shell::setActiveTab ); connect( m_tabWidget, &QTabWidget::tabCloseRequested, this, &Shell::closeTab ); connect( m_tabWidget->tabBar(), &QTabBar::tabMoved, this, &Shell::moveTabData ); setCentralWidget( m_tabWidget ); // then, setup our actions setupActions(); connect( QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &QObject::deleteLater ); // and integrate the part's GUI with the shell's setupGUI(Keys | ToolBar | Save); createGUI(firstPart); connectPart( firstPart ); m_tabs.append( firstPart ); m_tabWidget->addTab( firstPart->widget(), QString() ); readSettings(); m_unique = ShellUtils::unique(serializedOptions); if (m_unique) { m_unique = QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.okular")); if (!m_unique) KMessageBox::information(this, i18n("There is already a unique Okular instance running. This instance won't be the unique one.")); } else { QString serviceName = QStringLiteral("org.kde.okular-") + QString::number(qApp->applicationPid()); QDBusConnection::sessionBus().registerService(serviceName); } if (ShellUtils::noRaise(serializedOptions)) { setAttribute(Qt::WA_ShowWithoutActivating); } QDBusConnection::sessionBus().registerObject(QStringLiteral("/okularshell"), this, QDBusConnection::ExportScriptableSlots); } else { m_isValid = false; KMessageBox::error(this, i18n("Unable to find the Okular component.")); } } bool Shell::eventFilter(QObject *obj, QEvent *event) { Q_UNUSED(obj); QDragMoveEvent* dmEvent = dynamic_cast(event); if (dmEvent) { bool accept = dmEvent->mimeData()->hasUrls(); event->setAccepted(accept); return accept; } QDropEvent* dEvent = dynamic_cast(event); if (dEvent) { const QList list = KUrlMimeData::urlsFromMimeData(dEvent->mimeData()); handleDroppedUrls(list); dEvent->setAccepted(true); return true; } return false; } bool Shell::isValid() const { return m_isValid; } void Shell::showOpenRecentMenu() { m_recent->menu()->popup(QCursor::pos()); } Shell::~Shell() { if( !m_tabs.empty() ) { writeSettings(); for( QList::iterator it = m_tabs.begin(); it != m_tabs.end(); ++it ) { it->part->closeUrl( false ); } m_tabs.clear(); } if (m_unique) QDBusConnection::sessionBus().unregisterService(QStringLiteral("org.kde.okular")); delete m_tabWidget; } // Open a new document if we have space for it // This can hang if called on a unique instance and openUrl pops a messageBox bool Shell::openDocument( const QUrl& url, const QString &serializedOptions ) { if( m_tabs.size() <= 0 ) return false; KParts::ReadWritePart* const part = m_tabs[0].part; // Return false if we can't open new tabs and the only part is occupied if ( !dynamic_cast(part)->openNewFilesInTabs() && !part->url().isEmpty() && !ShellUtils::unique(serializedOptions)) { return false; } openUrl( url, serializedOptions ); return true; } bool Shell::openDocument( const QString& urlString, const QString &serializedOptions ) { return openDocument(QUrl(urlString), serializedOptions); } bool Shell::canOpenDocs( int numDocs, int desktop ) { if( m_tabs.size() <= 0 || numDocs <= 0 || m_unique ) return false; KParts::ReadWritePart* const part = m_tabs[0].part; const bool allowTabs = dynamic_cast(part)->openNewFilesInTabs(); if( !allowTabs && (numDocs > 1 || !part->url().isEmpty()) ) return false; const KWindowInfo winfo( window()->effectiveWinId(), KWindowSystem::WMDesktop ); if( winfo.desktop() != desktop ) return false; return true; } void Shell::openUrl( const QUrl & url, const QString &serializedOptions ) { const int activeTab = m_tabWidget->currentIndex(); if ( activeTab < m_tabs.size() ) { KParts::ReadWritePart* const activePart = m_tabs[activeTab].part; if( !activePart->url().isEmpty() ) { if( m_unique ) { applyOptionsToPart( activePart, serializedOptions ); activePart->openUrl( url ); } else { if( dynamic_cast(activePart)->openNewFilesInTabs() ) { openNewTab( url, serializedOptions ); } else { Shell* newShell = new Shell( serializedOptions ); - newShell->openUrl( url, serializedOptions ); newShell->show(); + newShell->openUrl( url, serializedOptions ); } } } else { m_tabWidget->setTabText( activeTab, url.fileName() ); applyOptionsToPart( activePart, serializedOptions ); bool openOk = activePart->openUrl( url ); const bool isstdin = url.fileName() == QLatin1String( "-" ); if ( !isstdin ) { if ( openOk ) { #ifndef Q_OS_WIN if ( !m_activityResource ) m_activityResource = new KActivities::ResourceInstance( window()->winId(), this ); m_activityResource->setUri( url ); #endif m_recent->addUrl( url ); } else m_recent->removeUrl( url ); } } } } void Shell::closeUrl() { closeTab( m_tabWidget->currentIndex() ); } void Shell::readSettings() { m_recent->loadEntries( KSharedConfig::openConfig()->group( "Recent Files" ) ); m_recent->setEnabled( true ); // force enabling const KConfigGroup group = KSharedConfig::openConfig()->group( "Desktop Entry" ); bool fullScreen = group.readEntry( "FullScreen", false ); setFullScreen( fullScreen ); if (fullScreen) { m_menuBarWasShown = group.readEntry( shouldShowMenuBarComingFromFullScreen, true ); m_toolBarWasShown = group.readEntry( shouldShowToolBarComingFromFullScreen, true ); } } void Shell::writeSettings() { m_recent->saveEntries( KSharedConfig::openConfig()->group( "Recent Files" ) ); KConfigGroup group = KSharedConfig::openConfig()->group( "Desktop Entry" ); group.writeEntry( "FullScreen", m_fullScreenAction->isChecked() ); if (m_fullScreenAction->isChecked()) { group.writeEntry( shouldShowMenuBarComingFromFullScreen, m_menuBarWasShown ); group.writeEntry( shouldShowToolBarComingFromFullScreen, m_toolBarWasShown ); } KSharedConfig::openConfig()->sync(); } void Shell::setupActions() { KStandardAction::open(this, SLOT(fileOpen()), actionCollection()); m_recent = KStandardAction::openRecent( this, SLOT(openUrl(QUrl)), actionCollection() ); m_recent->setToolBarMode( KRecentFilesAction::MenuMode ); connect( m_recent, &QAction::triggered, this, &Shell::showOpenRecentMenu ); m_recent->setToolTip( i18n("Click to open a file\nClick and hold to open a recent file") ); m_recent->setWhatsThis( i18n( "Click to open a file or Click and hold to select a recent file" ) ); m_printAction = KStandardAction::print( this, SLOT(print()), actionCollection() ); m_printAction->setEnabled( false ); m_closeAction = KStandardAction::close( this, SLOT(closeUrl()), actionCollection() ); m_closeAction->setEnabled( false ); KStandardAction::quit(this, SLOT(close()), actionCollection()); setStandardToolBarMenuEnabled(true); m_showMenuBarAction = KStandardAction::showMenubar( this, SLOT(slotShowMenubar()), actionCollection()); m_fullScreenAction = KStandardAction::fullScreen( this, SLOT(slotUpdateFullScreen()), this,actionCollection() ); m_nextTabAction = actionCollection()->addAction(QStringLiteral("tab-next")); m_nextTabAction->setText( i18n("Next Tab") ); actionCollection()->setDefaultShortcuts(m_nextTabAction, KStandardShortcut::tabNext()); m_nextTabAction->setEnabled( false ); connect( m_nextTabAction, &QAction::triggered, this, &Shell::activateNextTab ); m_prevTabAction = actionCollection()->addAction(QStringLiteral("tab-previous")); m_prevTabAction->setText( i18n("Previous Tab") ); actionCollection()->setDefaultShortcuts(m_prevTabAction, KStandardShortcut::tabPrev()); m_prevTabAction->setEnabled( false ); connect( m_prevTabAction, &QAction::triggered, this, &Shell::activatePrevTab ); } void Shell::saveProperties(KConfigGroup &group) { if ( !m_isValid ) // part couldn't be loaded, nothing to save return; // Gather lists of settings to preserve QStringList urls; for( int i = 0; i < m_tabs.size(); ++i ) { urls.append( m_tabs[i].part->url().url() ); } group.writePathEntry( SESSION_URL_KEY, urls ); group.writeEntry( SESSION_TAB_KEY, m_tabWidget->currentIndex() ); } void Shell::readProperties(const KConfigGroup &group) { // Reopen documents based on saved settings QStringList urls = group.readPathEntry( SESSION_URL_KEY, QStringList() ); while( !urls.isEmpty() ) { openUrl( QUrl(urls.takeFirst()) ); } int desiredTab = group.readEntry( SESSION_TAB_KEY, 0 ); if( desiredTab < m_tabs.size() ) { setActiveTab( desiredTab ); } } QStringList Shell::fileFormats() const { QStringList supportedPatterns; QString constraint( QStringLiteral("(Library == 'okularpart')") ); QLatin1String basePartService( "KParts/ReadOnlyPart" ); KService::List offers = KServiceTypeTrader::self()->query( basePartService, constraint ); KService::List::ConstIterator it = offers.constBegin(), itEnd = offers.constEnd(); for ( ; it != itEnd; ++it ) { KService::Ptr service = *it; QStringList mimeTypes = service->mimeTypes(); supportedPatterns += mimeTypes; } return supportedPatterns; } void Shell::fileOpen() { // this slot is called whenever the File->Open menu is selected, // the Open shortcut is pressed (usually CTRL+O) or the Open toolbar // button is clicked const int activeTab = m_tabWidget->currentIndex(); if ( !m_fileformatsscanned ) { const KDocumentViewer* const doc = qobject_cast(m_tabs[activeTab].part); if ( doc ) m_fileformats = doc->supportedMimeTypes(); if ( m_fileformats.isEmpty() ) m_fileformats = fileFormats(); m_fileformatsscanned = true; } QUrl startDir; const KParts::ReadWritePart* const curPart = m_tabs[activeTab].part; if ( curPart->url().isLocalFile() ) startDir = KIO::upUrl(curPart->url()); QPointer dlg( new QFileDialog( this )); dlg->setDirectoryUrl( startDir ); dlg->setAcceptMode( QFileDialog::AcceptOpen ); dlg->setOption( QFileDialog::HideNameFilterDetails, true ); QMimeDatabase mimeDatabase; QSet globPatterns; QMap namedGlobs; foreach ( const QString &mimeName, m_fileformats ) { QMimeType mimeType = mimeDatabase.mimeTypeForName( mimeName ); const QStringList globs( mimeType.globPatterns() ); if ( globs.isEmpty() ) { continue; } globPatterns.unite( globs.toSet() ) ; namedGlobs[ mimeType.comment() ].append( globs ); } QStringList namePatterns; foreach( const QString &name, namedGlobs.keys()) { namePatterns.append( name + QStringLiteral(" (") + namedGlobs[name].join( QLatin1Char(' ') ) + QStringLiteral(")") ); } namePatterns.prepend( i18n("All files (*)") ); namePatterns.prepend( i18n("All supported files (%1)", globPatterns.toList().join( QLatin1Char(' ') ) ) ); dlg->setNameFilters( namePatterns ); dlg->setWindowTitle( i18n("Open Document") ); if ( dlg->exec() && dlg ) { foreach(const QUrl& url, dlg->selectedUrls()) { openUrl( url ); } } if ( dlg ) { delete dlg.data(); } } void Shell::tryRaise() { KWindowSystem::forceActiveWindow( window()->effectiveWinId() ); } // only called when starting the program void Shell::setFullScreen( bool useFullScreen ) { if( useFullScreen ) setWindowState( windowState() | Qt::WindowFullScreen ); // set else setWindowState( windowState() & ~Qt::WindowFullScreen ); // reset } +void Shell::setCaption( const QString &caption ) +{ + bool modified = false; + + const int activeTab = m_tabWidget->currentIndex(); + if ( activeTab >= 0 && activeTab < m_tabs.size() ) + { + KParts::ReadWritePart* const activePart = m_tabs[activeTab].part; + QString tabCaption = activePart->url().fileName(); + if ( activePart->isModified() ) { + modified = true; + if ( !tabCaption.isEmpty() ) { + tabCaption.append( QStringLiteral( " *" ) ); + } + } + + m_tabWidget->setTabText( activeTab, tabCaption ); + } + + setCaption( caption, modified ); +} + void Shell::showEvent(QShowEvent *e) { if (!menuBar()->isNativeMenuBar() && m_showMenuBarAction) m_showMenuBarAction->setChecked( menuBar()->isVisible() ); KParts::MainWindow::showEvent(e); } void Shell::slotUpdateFullScreen() { if(m_fullScreenAction->isChecked()) { m_menuBarWasShown = !menuBar()->isHidden(); menuBar()->hide(); m_toolBarWasShown = !toolBar()->isHidden(); toolBar()->hide(); KToggleFullScreenAction::setFullScreen(this, true); } else { if (m_menuBarWasShown) { menuBar()->show(); } if (m_toolBarWasShown) { toolBar()->show(); } KToggleFullScreenAction::setFullScreen(this, false); } } void Shell::slotShowMenubar() { if ( menuBar()->isHidden() ) menuBar()->show(); else menuBar()->hide(); } QSize Shell::sizeHint() const { return QApplication::desktop()->availableGeometry( this ).size() * 0.75; } bool Shell::queryClose() { if (m_tabs.count() > 1) { const QString dontAskAgainName = "ShowTabWarning"; KMessageBox::ButtonCode dummy; if (shouldBeShownYesNo(dontAskAgainName, dummy)) { QDialog *dialog = new QDialog(this); dialog->setWindowTitle(i18n("Confirm Close")); QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog); buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Yes), KGuiItem(i18n("Close Tabs"), "tab-close")); KGuiItem::assign(buttonBox->button(QDialogButtonBox::No), KStandardGuiItem::cancel()); bool checkboxResult = true; const int result = KMessageBox::createKMessageBox(dialog, buttonBox, QMessageBox::Question, i18n("You are about to close %1 tabs. Are you sure you want to continue?", m_tabs.count()), QStringList(), i18n("Warn me when I attempt to close multiple tabs"), &checkboxResult, KMessageBox::Notify); if (!checkboxResult) { saveDontShowAgainYesNo(dontAskAgainName, dummy); } if (result != QDialogButtonBox::Yes) { return false; } } } for( int i = 0; i < m_tabs.size(); ++i ) { KParts::ReadWritePart* const part = m_tabs[i].part; // To resolve confusion about multiple modified docs, switch to relevant tab if( part->isModified() ) setActiveTab( i ); if( !part->queryClose() ) return false; } return true; } void Shell::setActiveTab( int tab ) { m_tabWidget->setCurrentIndex( tab ); createGUI( m_tabs[tab].part ); m_printAction->setEnabled( m_tabs[tab].printEnabled ); m_closeAction->setEnabled( m_tabs[tab].closeEnabled ); } void Shell::closeTab( int tab ) { KParts::ReadWritePart* const part = m_tabs[tab].part; if( part->closeUrl() && m_tabs.count() > 1 ) { if( part->factory() ) part->factory()->removeClient( part ); part->disconnect(); part->deleteLater(); m_tabs.removeAt( tab ); m_tabWidget->removeTab( tab ); if( m_tabWidget->count() == 1 ) { m_tabWidget->tabBar()->hide(); m_nextTabAction->setEnabled( false ); m_prevTabAction->setEnabled( false ); } } } void Shell::openNewTab( const QUrl& url, const QString &serializedOptions ) { // Tabs are hidden when there's only one, so show it if( m_tabs.size() == 1 ) { m_tabWidget->tabBar()->show(); m_nextTabAction->setEnabled( true ); m_prevTabAction->setEnabled( true ); } const int newIndex = m_tabs.size(); // Make new part m_tabs.append( m_partFactory->create(this) ); connectPart( m_tabs[newIndex].part ); // Update GUI KParts::ReadWritePart* const part = m_tabs[newIndex].part; m_tabWidget->addTab( part->widget(), url.fileName() ); applyOptionsToPart(part, serializedOptions); int previousActiveTab = m_tabWidget->currentIndex(); setActiveTab( m_tabs.size() - 1 ); if( part->openUrl(url) ) m_recent->addUrl( url ); else setActiveTab( previousActiveTab ); } void Shell::applyOptionsToPart( QObject* part, const QString &serializedOptions ) { KDocumentViewer* const doc = qobject_cast(part); if ( ShellUtils::startInPresentation(serializedOptions) ) doc->startPresentation(); if ( ShellUtils::showPrintDialog(serializedOptions) ) QMetaObject::invokeMethod( part, "enableStartWithPrint" ); } void Shell::connectPart( QObject* part ) { connect( this, SIGNAL(moveSplitter(int)), part, SLOT(moveSplitter(int)) ); connect( part, SIGNAL(enablePrintAction(bool)), this, SLOT(setPrintEnabled(bool))); connect( part, SIGNAL(enableCloseAction(bool)), this, SLOT(setCloseEnabled(bool))); connect( part, SIGNAL(mimeTypeChanged(QMimeType)), this, SLOT(setTabIcon(QMimeType))); connect( part, SIGNAL(urlsDropped(QList)), this, SLOT(handleDroppedUrls(QList)) ); connect( part, SIGNAL(fitWindowToPage(QSize,QSize)), this, SLOT(slotFitWindowToPage(QSize,QSize)) ); } void Shell::print() { QMetaObject::invokeMethod( m_tabs[m_tabWidget->currentIndex()].part, "slotPrint" ); } void Shell::setPrintEnabled( bool enabled ) { int i = findTabIndex( sender() ); if( i != -1 ) { m_tabs[i].printEnabled = enabled; if( i == m_tabWidget->currentIndex() ) m_printAction->setEnabled( enabled ); } } void Shell::setCloseEnabled( bool enabled ) { int i = findTabIndex( sender() ); if( i != -1 ) { m_tabs[i].closeEnabled = enabled; if( i == m_tabWidget->currentIndex() ) m_closeAction->setEnabled( enabled ); } } void Shell::activateNextTab() { if( m_tabs.size() < 2 ) return; const int activeTab = m_tabWidget->currentIndex(); const int nextTab = (activeTab == m_tabs.size()-1) ? 0 : activeTab+1; setActiveTab( nextTab ); } void Shell::activatePrevTab() { if( m_tabs.size() < 2 ) return; const int activeTab = m_tabWidget->currentIndex(); const int prevTab = (activeTab == 0) ? m_tabs.size()-1 : activeTab-1; setActiveTab( prevTab ); } void Shell::setTabIcon( const QMimeType& mimeType ) { int i = findTabIndex( sender() ); if( i != -1 ) { m_tabWidget->setTabIcon( i, QIcon::fromTheme(mimeType.iconName()) ); } } int Shell::findTabIndex( QObject* sender ) { for( int i = 0; i < m_tabs.size(); ++i ) { if( m_tabs[i].part == sender ) { return i; } } return -1; } void Shell::handleDroppedUrls( const QList& urls ) { foreach( const QUrl& url, urls ) { openUrl( url ); } } void Shell::moveTabData( int from, int to ) { m_tabs.move( from, to ); } void Shell::slotFitWindowToPage(const QSize& pageViewSize, const QSize& pageSize ) { const int xOffset = pageViewSize.width() - pageSize.width(); const int yOffset = pageViewSize.height() - pageSize.height(); showNormal(); resize( width() - xOffset, height() - yOffset); moveSplitter(pageSize.width()); } /* kate: replace-tabs on; indent-width 4; */ diff --git a/shell/shell.h b/shell/shell.h index 3727018ac..4c5604be7 100644 --- a/shell/shell.h +++ b/shell/shell.h @@ -1,181 +1,185 @@ /*************************************************************************** * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2003 by Benjamin Meyer * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003 by Luboš Luňák * * Copyright (C) 2004 by Christophe Devriese * * * * Copyright (C) 2004 by Albert Astals Cid * * * * 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_SHELL_H_ #define _OKULAR_SHELL_H_ #include #include #include #include #include #include class KRecentFilesAction; class KToggleAction; class QTabWidget; class KPluginFactory; class KDocumentViewer; class Part; #ifndef Q_OS_WIN namespace KActivities { class ResourceInstance; } #endif /** * This is the application "Shell". It has a menubar and a toolbar * but relies on the "Part" to do all the real work. * * @short Application Shell * @author Wilco Greven * @version 0.1 */ class Shell : public KParts::MainWindow { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.okular") friend class MainShellTest; public: /** * Constructor */ explicit Shell( const QString &serializedOptions = QString() ); /** * Default Destructor */ virtual ~Shell(); QSize sizeHint() const override; /** * Returns false if Okular component wasn't found **/ bool isValid() const; bool openDocument(const QUrl &url, const QString &serializedOptions); public Q_SLOTS: Q_SCRIPTABLE Q_NOREPLY void tryRaise(); Q_SCRIPTABLE bool openDocument(const QString &urlString, const QString &serializedOptions = QString() ); Q_SCRIPTABLE bool canOpenDocs( int numDocs, int desktop ); protected: /** * This method is called when it is time for the app to save its * properties for session management purposes. */ void saveProperties(KConfigGroup&) override; /** * This method is called when this app is restored. The KConfig * object points to the session management config file that was saved * with @ref saveProperties */ void readProperties(const KConfigGroup&) override; /** * Expose internal functions for session restore testing */ void savePropertiesInternal(KConfig* config, int num) {KMainWindow::savePropertiesInternal(config,num);} void readPropertiesInternal(KConfig* config, int num) {KMainWindow::readPropertiesInternal(config,num);} void readSettings(); void writeSettings(); void setFullScreen( bool ); + + using KParts::MainWindow::setCaption; + void setCaption( const QString &caption ) override; + bool queryClose() override; void showEvent(QShowEvent *event) override; private Q_SLOTS: void fileOpen(); void slotUpdateFullScreen(); void slotShowMenubar(); void openUrl( const QUrl & url, const QString &serializedOptions = QString() ); void showOpenRecentMenu(); void closeUrl(); void print(); void setPrintEnabled( bool enabled ); void setCloseEnabled( bool enabled ); void setTabIcon(const QMimeType& mimeType ); void handleDroppedUrls( const QList& urls ); // Tab event handlers void setActiveTab( int tab ); void closeTab( int tab ); void activateNextTab(); void activatePrevTab(); void moveTabData( int from, int to ); void slotFitWindowToPage( const QSize& pageViewSize, const QSize& pageSize ); Q_SIGNALS: void moveSplitter(int sideWidgetSize); private: void setupAccel(); void setupActions(); QStringList fileFormats() const; void openNewTab( const QUrl& url, const QString &serializedOptions ); void applyOptionsToPart( QObject* part, const QString &serializedOptions ); void connectPart( QObject* part ); int findTabIndex( QObject* sender ); private: bool eventFilter(QObject *obj, QEvent *event) override; KPluginFactory* m_partFactory; KRecentFilesAction* m_recent; QStringList m_fileformats; bool m_fileformatsscanned; QAction* m_printAction; QAction* m_closeAction; KToggleAction* m_fullScreenAction; KToggleAction* m_showMenuBarAction; bool m_menuBarWasShown, m_toolBarWasShown; bool m_unique; QTabWidget* m_tabWidget; KToggleAction* m_openInTab; struct TabState { TabState( KParts::ReadWritePart* p ) : part(p), printEnabled(false), closeEnabled(false) {} KParts::ReadWritePart* part; bool printEnabled; bool closeEnabled; }; QList m_tabs; QAction* m_nextTabAction; QAction* m_prevTabAction; #ifndef Q_OS_WIN KActivities::ResourceInstance* m_activityResource; #endif bool m_isValid; }; #endif // vim:ts=2:sw=2:tw=78:et diff --git a/ui/annotationmodel.cpp b/ui/annotationmodel.cpp index df6b89c9a..8f4861c8f 100644 --- a/ui/annotationmodel.cpp +++ b/ui/annotationmodel.cpp @@ -1,396 +1,421 @@ /*************************************************************************** * Copyright (C) 2006 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 "annotationmodel.h" #include #include #include #include #include #include "core/annotations.h" #include "core/document.h" #include "core/observer.h" #include "core/page.h" #include "ui/guiutils.h" struct AnnItem { AnnItem(); AnnItem( AnnItem *parent, Okular::Annotation *ann ); AnnItem( AnnItem *parent, int page ); ~AnnItem(); AnnItem *parent; QList< AnnItem* > children; Okular::Annotation *annotation; int page; }; static QLinkedList< Okular::Annotation* > filterOutWidgetAnnotations( const QLinkedList< Okular::Annotation* > &annotations ) { QLinkedList< Okular::Annotation* > result; foreach ( Okular::Annotation *annotation, annotations ) { if ( annotation->subType() == Okular::Annotation::AWidget ) continue; result.append( annotation ); } return result; } class AnnotationModelPrivate : public Okular::DocumentObserver { public: AnnotationModelPrivate( AnnotationModel *qq ); ~AnnotationModelPrivate() override; void notifySetup( const QVector< Okular::Page * > &pages, int setupFlags ) override; void notifyPageChanged( int page, int flags ) override; QModelIndex indexForItem( AnnItem *item ) const; void rebuildTree( const QVector< Okular::Page * > &pages ); AnnItem* findItem( int page, int *index ) const; AnnotationModel *q; AnnItem *root; QPointer< Okular::Document > document; }; AnnItem::AnnItem() : parent( nullptr ), annotation( nullptr ), page( -1 ) { } AnnItem::AnnItem( AnnItem *_parent, Okular::Annotation *ann ) : parent( _parent ), annotation( ann ), page( _parent->page ) { Q_ASSERT( !parent->annotation ); parent->children.append( this ); } AnnItem::AnnItem( AnnItem *_parent, int _page ) : parent( _parent ), annotation( nullptr ), page( _page ) { Q_ASSERT( !parent->parent ); parent->children.append( this ); } AnnItem::~AnnItem() { qDeleteAll( children ); } AnnotationModelPrivate::AnnotationModelPrivate( AnnotationModel *qq ) : q( qq ), root( new AnnItem ) { } AnnotationModelPrivate::~AnnotationModelPrivate() { delete root; } +static void updateAnnotationPointer( AnnItem *item, const QVector< Okular::Page * > &pages ) +{ + if ( item->annotation ) { + item->annotation = pages[ item->page ]->annotation( item->annotation->uniqueName() ); + if ( !item->annotation ) + qWarning() << "Lost annotation on document save, something went wrong"; + } + + foreach ( AnnItem *child, item->children ) + updateAnnotationPointer( child, pages ); +} + void AnnotationModelPrivate::notifySetup( const QVector< Okular::Page * > &pages, int setupFlags ) { if ( !( setupFlags & Okular::DocumentObserver::DocumentChanged ) ) + { + if ( setupFlags & Okular::DocumentObserver::UrlChanged ) + { + // Here with UrlChanged and no document changed it means we + // need to update all the Annotation* otherwise + // they still point to the old document ones, luckily the old ones are still + // around so we can look for the new ones using unique ids, etc + updateAnnotationPointer( root, pages ); + } return; + } q->beginResetModel(); qDeleteAll( root->children ); root->children.clear(); rebuildTree( pages ); q->endResetModel(); } void AnnotationModelPrivate::notifyPageChanged( int page, int flags ) { // we are strictly interested in annotations if ( !(flags & Okular::DocumentObserver::Annotations ) ) return; const QLinkedList< Okular::Annotation* > annots = filterOutWidgetAnnotations( document->page( page )->annotations() ); int annItemIndex = -1; AnnItem *annItem = findItem( page, &annItemIndex ); // case 1: the page has no more annotations // => remove the branch, if any if ( annots.isEmpty() ) { if ( annItem ) { q->beginRemoveRows( indexForItem( root ), annItemIndex, annItemIndex ); delete root->children.at( annItemIndex ); root->children.removeAt( annItemIndex ); q->endRemoveRows(); } return; } // case 2: no existing branch // => add a new branch, and add the annotations for the page if ( !annItem ) { int i = 0; while ( i < root->children.count() && root->children.at( i )->page < page ) ++i; AnnItem *annItem = new AnnItem(); annItem->page = page; annItem->parent = root; q->beginInsertRows( indexForItem( root ), i, i ); annItem->parent->children.insert( i, annItem ); q->endInsertRows(); QLinkedList< Okular::Annotation* >::ConstIterator it = annots.begin(), itEnd = annots.end(); int newid = 0; for ( ; it != itEnd; ++it, ++newid ) { q->beginInsertRows( indexForItem( annItem ), newid, newid ); new AnnItem( annItem, *it ); q->endInsertRows(); } return; } // case 3: existing branch, less annotations than items // => lookup and remove the annotations if ( annItem->children.count() > annots.count() ) { for ( int i = annItem->children.count(); i > 0; --i ) { Okular::Annotation *ref = annItem->children.at( i - 1 )->annotation; bool found = false; QLinkedList< Okular::Annotation* >::ConstIterator it = annots.begin(), itEnd = annots.end(); for ( ; !found && it != itEnd; ++it ) { if ( ( *it ) == ref ) found = true; } if ( !found ) { q->beginRemoveRows( indexForItem( annItem ), i - 1, i - 1 ); delete annItem->children.at( i - 1 ); annItem->children.removeAt( i - 1 ); q->endRemoveRows(); } } return; } // case 4: existing branch, less items than annotations // => lookup and add annotations if not in the branch if ( annots.count() > annItem->children.count() ) { QLinkedList< Okular::Annotation* >::ConstIterator it = annots.begin(), itEnd = annots.end(); for ( ; it != itEnd; ++it ) { Okular::Annotation *ref = *it; bool found = false; int count = annItem->children.count(); for ( int i = 0; !found && i < count; ++i ) { if ( ref == annItem->children.at( i )->annotation ) found = true; } if ( !found ) { q->beginInsertRows( indexForItem( annItem ), count, count ); new AnnItem( annItem, ref ); q->endInsertRows(); } } return; } // case 5: the data of some annotation changed // TODO: what do we do in this case? // FIXME: for now, update ALL the annotations for that page for ( int i = 0; i < annItem->children.count(); ++i ) { QModelIndex index = indexForItem( annItem->children.at( i ) ); emit q->dataChanged( index, index ); } } QModelIndex AnnotationModelPrivate::indexForItem( AnnItem *item ) const { if ( item->parent ) { int id = item->parent->children.indexOf( item ); if ( id >= 0 && id < item->parent->children.count() ) return q->createIndex( id, 0, item ); } return QModelIndex(); } void AnnotationModelPrivate::rebuildTree( const QVector< Okular::Page * > &pages ) { if ( pages.isEmpty() ) return; emit q->layoutAboutToBeChanged(); for ( int i = 0; i < pages.count(); ++i ) { const QLinkedList< Okular::Annotation* > annots = filterOutWidgetAnnotations( pages.at( i )->annotations() ); if ( annots.isEmpty() ) continue; AnnItem *annItem = new AnnItem( root, i ); QLinkedList< Okular::Annotation* >::ConstIterator it = annots.begin(), itEnd = annots.end(); for ( ; it != itEnd; ++it ) { new AnnItem( annItem, *it ); } } emit q->layoutChanged(); } AnnItem* AnnotationModelPrivate::findItem( int page, int *index ) const { for ( int i = 0; i < root->children.count(); ++i ) { AnnItem *tmp = root->children.at( i ); if ( tmp->page == page ) { if ( index ) *index = i; return tmp; } } if ( index ) *index = -1; return nullptr; } AnnotationModel::AnnotationModel( Okular::Document *document, QObject *parent ) : QAbstractItemModel( parent ), d( new AnnotationModelPrivate( this ) ) { d->document = document; d->document->addObserver( d ); } AnnotationModel::~AnnotationModel() { if ( d->document ) d->document->removeObserver( d ); delete d; } int AnnotationModel::columnCount( const QModelIndex &parent ) const { Q_UNUSED( parent ) return 1; } QVariant AnnotationModel::data( const QModelIndex &index, int role ) const { if ( !index.isValid() ) return QVariant(); AnnItem *item = static_cast< AnnItem* >( index.internalPointer() ); if ( !item->annotation ) { if ( role == Qt::DisplayRole ) return i18n( "Page %1", item->page + 1 ); else if ( role == Qt::DecorationRole ) return QIcon::fromTheme( QStringLiteral("text-plain") ); else if ( role == PageRole ) return item->page; return QVariant(); } switch ( role ) { case Qt::DisplayRole: return GuiUtils::captionForAnnotation( item->annotation ); break; case Qt::DecorationRole: return QIcon::fromTheme( QStringLiteral("okular") ); break; case Qt::ToolTipRole: return GuiUtils::prettyToolTip( item->annotation ); break; case AuthorRole: return item->annotation->author(); break; case PageRole: return item->page; break; } return QVariant(); } bool AnnotationModel::hasChildren( const QModelIndex &parent ) const { if ( !parent.isValid() ) return true; AnnItem *item = static_cast< AnnItem* >( parent.internalPointer() ); return !item->children.isEmpty(); } QVariant AnnotationModel::headerData( int section, Qt::Orientation orientation, int role ) const { if ( orientation != Qt::Horizontal ) return QVariant(); if ( section == 0 && role == Qt::DisplayRole ) return QString::fromLocal8Bit("Annotations"); return QVariant(); } QModelIndex AnnotationModel::index( int row, int column, const QModelIndex &parent ) const { if ( row < 0 || column != 0 ) return QModelIndex(); AnnItem *item = parent.isValid() ? static_cast< AnnItem* >( parent.internalPointer() ) : d->root; if ( row < item->children.count() ) return createIndex( row, column, item->children.at( row ) ); return QModelIndex(); } QModelIndex AnnotationModel::parent( const QModelIndex &index ) const { if ( !index.isValid() ) return QModelIndex(); AnnItem *item = static_cast< AnnItem* >( index.internalPointer() ); return d->indexForItem( item->parent ); } int AnnotationModel::rowCount( const QModelIndex &parent ) const { AnnItem *item = parent.isValid() ? static_cast< AnnItem* >( parent.internalPointer() ) : d->root; return item->children.count(); } bool AnnotationModel::isAnnotation( const QModelIndex &index ) const { return annotationForIndex( index ); } Okular::Annotation* AnnotationModel::annotationForIndex( const QModelIndex &index ) const { if ( !index.isValid() ) return nullptr; AnnItem *item = static_cast< AnnItem* >( index.internalPointer() ); return item->annotation; } #include "moc_annotationmodel.cpp" diff --git a/ui/annotwindow.cpp b/ui/annotwindow.cpp index 4cca403b8..ab4e0cc5b 100644 --- a/ui/annotwindow.cpp +++ b/ui/annotwindow.cpp @@ -1,409 +1,427 @@ /*************************************************************************** * Copyright (C) 2006 by Chu Xiaodong * * Copyright (C) 2006 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 "annotwindow.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // local includes #include "core/annotations.h" #include "core/document.h" #include "latexrenderer.h" #include #include class CloseButton : public QPushButton { Q_OBJECT public: CloseButton( QWidget * parent = Q_NULLPTR ) : QPushButton( parent ) { setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); QSize size = QSize( 14, 14 ).expandedTo( QApplication::globalStrut() ); setFixedSize( size ); setIcon( style()->standardIcon( QStyle::SP_DockWidgetCloseButton ) ); setIconSize( size ); setToolTip( i18n( "Close this note" ) ); } }; class MovableTitle : public QWidget { Q_OBJECT public: MovableTitle( QWidget * parent ) : QWidget( parent ) { QVBoxLayout * mainlay = new QVBoxLayout( this ); mainlay->setMargin( 0 ); mainlay->setSpacing( 0 ); // close button row QHBoxLayout * buttonlay = new QHBoxLayout(); mainlay->addLayout( buttonlay ); titleLabel = new QLabel( this ); QFont f = titleLabel->font(); f.setBold( true ); titleLabel->setFont( f ); titleLabel->setCursor( Qt::SizeAllCursor ); buttonlay->addWidget( titleLabel ); dateLabel = new QLabel( this ); dateLabel->setAlignment( Qt::AlignTop | Qt::AlignRight ); f = dateLabel->font(); f.setPointSize( QFontInfo( f ).pointSize() - 2 ); dateLabel->setFont( f ); dateLabel->setCursor( Qt::SizeAllCursor ); buttonlay->addWidget( dateLabel ); CloseButton * close = new CloseButton( this ); connect( close, &QAbstractButton::clicked, parent, &QWidget::close ); buttonlay->addWidget( close ); // option button row QHBoxLayout * optionlay = new QHBoxLayout(); mainlay->addLayout( optionlay ); authorLabel = new QLabel( this ); authorLabel->setCursor( Qt::SizeAllCursor ); authorLabel->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Minimum ); optionlay->addWidget( authorLabel ); optionButton = new QToolButton( this ); QString opttext = i18n( "Options" ); optionButton->setText( opttext ); optionButton->setAutoRaise( true ); QSize s = QFontMetrics( optionButton->font() ).boundingRect( opttext ).size() + QSize( 8, 8 ); optionButton->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); optionButton->setFixedSize( s ); optionlay->addWidget( optionButton ); // ### disabled for now optionButton->hide(); latexButton = new QToolButton( this ); QHBoxLayout * latexlay = new QHBoxLayout(); QString latextext = i18n ( "This annotation may contain LaTeX code.\nClick here to render." ); latexButton->setText( latextext ); latexButton->setAutoRaise( true ); s = QFontMetrics( latexButton->font() ).boundingRect(0, 0, this->width(), this->height(), 0, latextext ).size() + QSize( 8, 8 ); latexButton->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); latexButton->setFixedSize( s ); latexButton->setCheckable( true ); latexButton->setVisible( false ); latexlay->addSpacing( 1 ); latexlay->addWidget( latexButton ); latexlay->addSpacing( 1 ); mainlay->addLayout( latexlay ); connect(latexButton, SIGNAL(clicked(bool)), parent, SLOT(renderLatex(bool))); connect(parent, SIGNAL(containsLatex(bool)), latexButton, SLOT(setVisible(bool))); titleLabel->installEventFilter( this ); dateLabel->installEventFilter( this ); authorLabel->installEventFilter( this ); } bool eventFilter( QObject * obj, QEvent * e ) override { if ( obj != titleLabel && obj != authorLabel && obj != dateLabel ) return false; QMouseEvent * me = nullptr; switch ( e->type() ) { case QEvent::MouseButtonPress: me = (QMouseEvent*)e; mousePressPos = me->pos(); break; case QEvent::MouseButtonRelease: mousePressPos = QPoint(); break; case QEvent::MouseMove: me = (QMouseEvent*)e; parentWidget()->move( me->pos() - mousePressPos + parentWidget()->pos() ); break; default: return false; } return true; } void setTitle( const QString& title ) { titleLabel->setText( QStringLiteral( " " ) + title ); } void setDate( const QDateTime& dt ) { dateLabel->setText( QLocale().toString( dt, QLocale::ShortFormat ) + QLatin1Char(' ') ); } void setAuthor( const QString& author ) { authorLabel->setText( QStringLiteral( " " ) + author ); } void connectOptionButton( QObject * recv, const char* method ) { connect( optionButton, SIGNAL(clicked()), recv, method ); } void uncheckLatexButton() { latexButton->setChecked( false ); } private: QLabel * titleLabel; QLabel * dateLabel; QLabel * authorLabel; QPoint mousePressPos; QToolButton * optionButton; QToolButton * latexButton; }; // Qt::SubWindow is needed to make QSizeGrip work AnnotWindow::AnnotWindow( QWidget * parent, Okular::Annotation * annot, Okular::Document *document, int page ) : QFrame( parent, Qt::SubWindow ), m_annot( annot ), m_document( document ), m_page( page ) { setAutoFillBackground( true ); setFrameStyle( Panel | Raised ); setAttribute( Qt::WA_DeleteOnClose ); const bool canEditAnnotation = m_document->canModifyPageAnnotation( annot ); textEdit = new KTextEdit( this ); textEdit->setAcceptRichText( false ); textEdit->setPlainText( m_annot->contents() ); textEdit->installEventFilter( this ); textEdit->setUndoRedoEnabled( false ); m_prevCursorPos = textEdit->textCursor().position(); m_prevAnchorPos = textEdit->textCursor().anchor(); connect(textEdit, &KTextEdit::textChanged, this, &AnnotWindow::slotsaveWindowText); connect(textEdit, &KTextEdit::cursorPositionChanged, this, &AnnotWindow::slotsaveWindowText); connect(textEdit, &KTextEdit::aboutToShowContextMenu, this, &AnnotWindow::slotUpdateUndoAndRedoInContextMenu); connect(m_document, &Okular::Document::annotationContentsChangedByUndoRedo, this, &AnnotWindow::slotHandleContentsChangedByUndoRedo); if (!canEditAnnotation) textEdit->setReadOnly(true); QVBoxLayout * mainlay = new QVBoxLayout( this ); mainlay->setMargin( 2 ); mainlay->setSpacing( 0 ); m_title = new MovableTitle( this ); mainlay->addWidget( m_title ); mainlay->addWidget( textEdit ); QHBoxLayout * lowerlay = new QHBoxLayout(); mainlay->addLayout( lowerlay ); lowerlay->addItem( new QSpacerItem( 5, 5, QSizePolicy::Expanding, QSizePolicy::Fixed ) ); QSizeGrip * sb = new QSizeGrip( this ); lowerlay->addWidget( sb ); m_latexRenderer = new GuiUtils::LatexRenderer(); emit containsLatex( GuiUtils::LatexRenderer::mightContainLatex( m_annot->contents() ) ); m_title->setTitle( m_annot->window().summary() ); m_title->connectOptionButton( this, SLOT(slotOptionBtn()) ); setGeometry(10,10,300,300 ); reloadInfo(); } AnnotWindow::~AnnotWindow() { delete m_latexRenderer; } +Okular::Annotation * AnnotWindow::annotation() const +{ + return m_annot; +} + +void AnnotWindow::updateAnnotation( Okular::Annotation * a ) +{ + m_annot = a; +} + void AnnotWindow::reloadInfo() { const QColor newcolor = m_annot->style().color().isValid() ? m_annot->style().color() : Qt::yellow; if ( newcolor != m_color ) { m_color = newcolor; setPalette( QPalette( m_color ) ); QPalette pl = textEdit->palette(); pl.setColor( QPalette::Base, m_color ); textEdit->setPalette( pl ); } m_title->setAuthor( m_annot->author() ); m_title->setDate( m_annot->modificationDate() ); } +int AnnotWindow::pageNumber() const +{ + return m_page; +} + void AnnotWindow::showEvent( QShowEvent * event ) { QFrame::showEvent( event ); // focus the content area by default textEdit->setFocus(); } bool AnnotWindow::eventFilter(QObject *, QEvent *e) { if ( e->type () == QEvent::ShortcutOverride ) { QKeyEvent * keyEvent = static_cast< QKeyEvent * >( e ); if ( keyEvent->key() == Qt::Key_Escape ) { close(); return true; } } else if (e->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(e); if (keyEvent == QKeySequence::Undo) { m_document->undo(); return true; } else if (keyEvent == QKeySequence::Redo) { m_document->redo(); return true; } } return false; } void AnnotWindow::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_document, SLOT(undo()), menu); QAction *kredo = KStandardAction::create( KStandardAction::Redo, m_document, SLOT(redo()), menu); connect(m_document, &Okular::Document::canUndoChanged, kundo, &QAction::setEnabled); connect(m_document, &Okular::Document::canRedoChanged, kredo, &QAction::setEnabled); kundo->setEnabled(m_document->canUndo()); kredo->setEnabled(m_document->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 AnnotWindow::slotOptionBtn() { //TODO: call context menu in pageview //emit sig... } void AnnotWindow::slotsaveWindowText() { const QString contents = textEdit->toPlainText(); const int cursorPos = textEdit->textCursor().position(); if (contents != m_annot->contents()) { m_document->editPageAnnotationContents( m_page, m_annot, contents, cursorPos, m_prevCursorPos, m_prevAnchorPos); emit containsLatex( GuiUtils::LatexRenderer::mightContainLatex( textEdit->toPlainText() ) ); } m_prevCursorPos = cursorPos; m_prevAnchorPos = textEdit->textCursor().anchor(); } void AnnotWindow::renderLatex( bool render ) { if (render) { textEdit->setReadOnly( true ); disconnect(textEdit, &KTextEdit::textChanged, this, &AnnotWindow::slotsaveWindowText); disconnect(textEdit, &KTextEdit::cursorPositionChanged, this, &AnnotWindow::slotsaveWindowText); textEdit->setAcceptRichText( true ); QString contents = m_annot->contents(); contents = Qt::convertFromPlainText( contents ); QColor fontColor = textEdit->textColor(); int fontSize = textEdit->fontPointSize(); QString latexOutput; GuiUtils::LatexRenderer::Error errorCode = m_latexRenderer->renderLatexInHtml( contents, fontColor, fontSize, Okular::Utils::realDpi(nullptr).width(), latexOutput ); switch ( errorCode ) { case GuiUtils::LatexRenderer::LatexNotFound: KMessageBox::sorry( this, i18n( "Cannot find latex executable." ), i18n( "LaTeX rendering failed" ) ); m_title->uncheckLatexButton(); renderLatex( false ); break; case GuiUtils::LatexRenderer::DvipngNotFound: KMessageBox::sorry( this, i18n( "Cannot find dvipng executable." ), i18n( "LaTeX rendering failed" ) ); m_title->uncheckLatexButton(); renderLatex( false ); break; case GuiUtils::LatexRenderer::LatexFailed: KMessageBox::detailedSorry( this, i18n( "A problem occurred during the execution of the 'latex' command." ), latexOutput, i18n( "LaTeX rendering failed" ) ); m_title->uncheckLatexButton(); renderLatex( false ); break; case GuiUtils::LatexRenderer::DvipngFailed: KMessageBox::sorry( this, i18n( "A problem occurred during the execution of the 'dvipng' command." ), i18n( "LaTeX rendering failed" ) ); m_title->uncheckLatexButton(); renderLatex( false ); break; case GuiUtils::LatexRenderer::NoError: default: textEdit->setHtml( contents ); break; } } else { textEdit->setAcceptRichText( false ); textEdit->setPlainText( m_annot->contents() ); connect(textEdit, &KTextEdit::textChanged, this, &AnnotWindow::slotsaveWindowText); connect(textEdit, &KTextEdit::cursorPositionChanged, this, &AnnotWindow::slotsaveWindowText); textEdit->setReadOnly( false ); } } void AnnotWindow::slotHandleContentsChangedByUndoRedo(Okular::Annotation* annot, QString contents, int cursorPos, int anchorPos) { if ( annot != m_annot ) { return; } textEdit->setPlainText(contents); QTextCursor c = textEdit->textCursor(); c.setPosition(anchorPos); c.setPosition(cursorPos,QTextCursor::KeepAnchor); m_prevCursorPos = cursorPos; m_prevAnchorPos = anchorPos; textEdit->setTextCursor(c); textEdit->setFocus(); emit containsLatex( GuiUtils::LatexRenderer::mightContainLatex( m_annot->contents() ) ); } #include "annotwindow.moc" diff --git a/ui/annotwindow.h b/ui/annotwindow.h index d34c3ea20..1af2c32b3 100644 --- a/ui/annotwindow.h +++ b/ui/annotwindow.h @@ -1,66 +1,74 @@ /*************************************************************************** * Copyright (C) 2006 by Chu Xiaodong * * Copyright (C) 2006 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 _ANNOTWINDOW_H_ #define _ANNOTWINDOW_H_ #include #include namespace Okular { class Annotation; class Document; } namespace GuiUtils { class LatexRenderer; } class KTextEdit; class MovableTitle; class QMenu; class AnnotWindow : public QFrame { Q_OBJECT public: AnnotWindow( QWidget * parent, Okular::Annotation * annot, Okular::Document * document, int page ); ~AnnotWindow(); void reloadInfo(); + Okular::Annotation * annotation() const; + int pageNumber() const; + + void updateAnnotation( Okular::Annotation * a ); + private: MovableTitle * m_title; KTextEdit *textEdit; QColor m_color; GuiUtils::LatexRenderer *m_latexRenderer; Okular::Annotation* m_annot; Okular::Document* m_document; int m_page; int m_prevCursorPos; int m_prevAnchorPos; protected: void showEvent( QShowEvent * event ) override; bool eventFilter( QObject * obj, QEvent * event ) override; private Q_SLOTS: void slotUpdateUndoAndRedoInContextMenu(QMenu *menu); void slotOptionBtn(); void slotsaveWindowText(); void renderLatex( bool render ); void slotHandleContentsChangedByUndoRedo( Okular::Annotation* annot, QString contents, int cursorPos, int anchorPos); Q_SIGNALS: void containsLatex( bool ); }; #endif diff --git a/ui/bookmarklist.cpp b/ui/bookmarklist.cpp index 7f41fbc62..ff26132fc 100644 --- a/ui/bookmarklist.cpp +++ b/ui/bookmarklist.cpp @@ -1,471 +1,471 @@ /*************************************************************************** * Copyright (C) 2006 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 "bookmarklist.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include "pageitemdelegate.h" #include "core/action.h" #include "core/bookmarkmanager.h" #include "core/document.h" static const int BookmarkItemType = QTreeWidgetItem::UserType + 1; static const int FileItemType = QTreeWidgetItem::UserType + 2; static const int UrlRole = Qt::UserRole + 1; class BookmarkItem : public QTreeWidgetItem { public: BookmarkItem( const KBookmark& bm ) : QTreeWidgetItem( BookmarkItemType ), m_bookmark( bm ) { setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable ); m_url = m_bookmark.url(); m_viewport = Okular::DocumentViewport( m_url.fragment(QUrl::FullyDecoded) ); m_url.setFragment( QString() ); setText( 0, m_bookmark.fullText() ); if ( m_viewport.isValid() ) setData( 0, PageItemDelegate::PageRole, QString::number( m_viewport.pageNumber + 1 ) ); } QVariant data( int column, int role ) const override { switch ( role ) { case Qt::ToolTipRole: return m_bookmark.fullText(); } return QTreeWidgetItem::data( column, role ); } bool operator<( const QTreeWidgetItem& other ) const override { if ( other.type() == BookmarkItemType ) { const BookmarkItem *cmp = static_cast< const BookmarkItem* >( &other ); return m_viewport < cmp->m_viewport; } return QTreeWidgetItem::operator<( other ); } KBookmark& bookmark() { return m_bookmark; } const Okular::DocumentViewport& viewport() const { return m_viewport; } QUrl url() const { return m_url; } private: KBookmark m_bookmark; QUrl m_url; Okular::DocumentViewport m_viewport; }; class FileItem : public QTreeWidgetItem { public: FileItem( const QUrl & url, QTreeWidget *tree, Okular::Document *document ) : QTreeWidgetItem( tree, FileItemType ) { setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable ); const QString fileString = document->bookmarkManager()->titleForUrl( url ); setText( 0, fileString ); setData( 0, UrlRole, qVariantFromValue( url ) ); } QVariant data( int column, int role ) const override { switch ( role ) { case Qt::ToolTipRole: return i18ncp( "%1 is the file name", "%1\n\nOne bookmark", "%1\n\n%2 bookmarks", text( 0 ), childCount() ); } return QTreeWidgetItem::data( column, role ); } }; BookmarkList::BookmarkList( Okular::Document *document, QWidget *parent ) : QWidget( parent ), m_document( document ), m_currentDocumentItem( nullptr ) { QVBoxLayout *mainlay = new QVBoxLayout( this ); mainlay->setMargin( 0 ); mainlay->setSpacing( 6 ); m_searchLine = new KTreeWidgetSearchLine( this ); mainlay->addWidget( m_searchLine ); m_tree = new QTreeWidget( this ); mainlay->addWidget( m_tree ); QStringList cols; cols.append( QStringLiteral("Bookmarks") ); m_tree->setContextMenuPolicy( Qt::CustomContextMenu ); m_tree->setHeaderLabels( cols ); m_tree->setSortingEnabled( false ); m_tree->setRootIsDecorated( true ); m_tree->setAlternatingRowColors( true ); m_tree->setItemDelegate( new PageItemDelegate( m_tree ) ); m_tree->header()->hide(); m_tree->setSelectionBehavior( QAbstractItemView::SelectRows ); m_tree->setEditTriggers( QAbstractItemView::EditKeyPressed ); connect(m_tree, &QTreeWidget::itemActivated, this, &BookmarkList::slotExecuted); connect(m_tree, &QTreeWidget::customContextMenuRequested, this, &BookmarkList::slotContextMenu); m_searchLine->addTreeWidget( m_tree ); QToolBar * bookmarkController = new QToolBar( this ); mainlay->addWidget( bookmarkController ); bookmarkController->setObjectName( QStringLiteral( "BookmarkControlBar" ) ); // change toolbar appearance bookmarkController->setIconSize( QSize( 16, 16 ) ); bookmarkController->setMovable( false ); QSizePolicy sp = bookmarkController->sizePolicy(); sp.setVerticalPolicy( QSizePolicy::Minimum ); bookmarkController->setSizePolicy( sp ); // insert a togglebutton [show only bookmarks in the current document] m_showBoomarkOnlyAction = bookmarkController->addAction( QIcon::fromTheme( QStringLiteral("bookmarks") ), i18n( "Current document only" ) ); m_showBoomarkOnlyAction->setCheckable( true ); connect(m_showBoomarkOnlyAction, &QAction::toggled, this, &BookmarkList::slotFilterBookmarks); connect( m_document->bookmarkManager(), &Okular::BookmarkManager::bookmarksChanged, this, &BookmarkList::slotBookmarksChanged ); rebuildTree( m_showBoomarkOnlyAction->isChecked() ); } BookmarkList::~BookmarkList() { m_document->removeObserver( this ); } void BookmarkList::notifySetup( const QVector< Okular::Page * > & pages, int setupFlags ) { Q_UNUSED( pages ); - if ( !( setupFlags & Okular::DocumentObserver::DocumentChanged ) ) + if ( !( setupFlags & Okular::DocumentObserver::UrlChanged ) ) return; // clear contents m_searchLine->clear(); if ( m_showBoomarkOnlyAction->isChecked() ) { rebuildTree( m_showBoomarkOnlyAction->isChecked() ); } else { disconnect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged); if ( m_currentDocumentItem && m_currentDocumentItem != m_tree->invisibleRootItem() ) { m_currentDocumentItem->setIcon( 0, QIcon() ); } m_currentDocumentItem = itemForUrl( m_document->currentDocument() ); if ( m_currentDocumentItem && m_currentDocumentItem != m_tree->invisibleRootItem() ) { m_currentDocumentItem->setIcon( 0, QIcon::fromTheme( QStringLiteral("bookmarks") ) ); m_currentDocumentItem->setExpanded( true ); } connect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged); } } void BookmarkList::slotFilterBookmarks( bool on ) { rebuildTree( on ); } void BookmarkList::slotExecuted( QTreeWidgetItem * item ) { BookmarkItem* bmItem = dynamic_cast( item ); if ( !bmItem || !bmItem->viewport().isValid() ) return; goTo( bmItem ); } void BookmarkList::slotChanged( QTreeWidgetItem * item ) { BookmarkItem* bmItem = dynamic_cast( item ); if ( bmItem && bmItem->viewport().isValid() ) { bmItem->bookmark().setFullText( bmItem->text( 0 ) ); m_document->bookmarkManager()->save(); } FileItem* fItem = dynamic_cast( item ); if ( fItem ) { const QUrl url = fItem->data( 0, UrlRole ).value< QUrl >(); m_document->bookmarkManager()->renameBookmark( url, fItem->text( 0 ) ); m_document->bookmarkManager()->save(); } } void BookmarkList::slotContextMenu( const QPoint& p ) { QTreeWidgetItem * item = m_tree->itemAt( p ); BookmarkItem* bmItem = item ? dynamic_cast( item ) : nullptr; if ( bmItem ) contextMenuForBookmarkItem( p, bmItem ); else if ( FileItem* fItem = dynamic_cast< FileItem * >( item ) ) contextMenuForFileItem( p, fItem ); } void BookmarkList::contextMenuForBookmarkItem( const QPoint& p, BookmarkItem* bmItem ) { Q_UNUSED( p ); if ( !bmItem || !bmItem->viewport().isValid() ) return; QMenu menu( this ); QAction * gotobm = menu.addAction( i18n( "Go to This Bookmark" ) ); QAction * editbm = menu.addAction( QIcon::fromTheme( QStringLiteral("edit-rename") ), i18n( "Rename Bookmark" ) ); QAction * removebm = menu.addAction( QIcon::fromTheme( QStringLiteral("list-remove") ), i18n( "Remove Bookmark" ) ); QAction * res = menu.exec( QCursor::pos() ); if ( !res ) return; if ( res == gotobm ) goTo( bmItem ); else if ( res == editbm ) m_tree->editItem( bmItem, 0 ); else if ( res == removebm ) m_document->bookmarkManager()->removeBookmark( bmItem->url(), bmItem->bookmark() ); } void BookmarkList::contextMenuForFileItem( const QPoint& p, FileItem* fItem ) { Q_UNUSED( p ); if ( !fItem ) return; const QUrl itemurl = fItem->data( 0, UrlRole ).value< QUrl >(); const bool thisdoc = itemurl == m_document->currentDocument(); QMenu menu( this ); QAction * open = nullptr; if ( !thisdoc ) open = menu.addAction( i18nc( "Opens the selected document", "Open Document" ) ); QAction * editbm = menu.addAction( QIcon::fromTheme( QStringLiteral("edit-rename") ), i18n( "Rename Bookmark" ) ); QAction * removebm = menu.addAction( QIcon::fromTheme( QStringLiteral("list-remove") ), i18n( "Remove Bookmarks" ) ); QAction * res = menu.exec( QCursor::pos() ); if ( !res ) return; if ( res == open ) { Okular::GotoAction action( itemurl.toDisplayString(QUrl::PreferLocalFile), Okular::DocumentViewport() ); m_document->processAction( &action ); } else if ( res == editbm ) m_tree->editItem( fItem, 0 ); else if ( res == removebm ) { KBookmark::List list; for ( int i = 0; i < fItem->childCount(); ++i ) { list.append( static_cast( fItem->child( i ) )->bookmark() ); } m_document->bookmarkManager()->removeBookmarks( itemurl, list ); } } void BookmarkList::slotBookmarksChanged(const QUrl &url ) { // special case here, as m_currentDocumentItem could represent // the invisible root item if ( url == m_document->currentDocument() ) { selectiveUrlUpdate( m_document->currentDocument(), m_currentDocumentItem ); return; } // we are showing the bookmarks for the current document only if ( m_showBoomarkOnlyAction->isChecked() ) return; QTreeWidgetItem *item = itemForUrl( url ); selectiveUrlUpdate( url, item ); } QList createItems( const QUrl& baseurl, const KBookmark::List& bmlist ) { Q_UNUSED(baseurl) QList ret; foreach ( const KBookmark& bm, bmlist ) { // qCDebug(OkularUiDebug).nospace() << "checking '" << tmp << "'"; // qCDebug(OkularUiDebug).nospace() << " vs '" << baseurl << "'"; // TODO check that bm and baseurl are the same (#ref excluded) QTreeWidgetItem * item = new BookmarkItem( bm ); ret.append( item ); } return ret; } void BookmarkList::rebuildTree( bool filter ) { // disconnect and reconnect later, otherwise we'll get many itemChanged() // signals for all the current items disconnect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged); m_currentDocumentItem = nullptr; m_tree->clear(); QList urls = m_document->bookmarkManager()->files(); if ( filter ) { if ( m_document->isOpened() ) { foreach ( const QUrl& url, urls ) { if ( url == m_document->currentDocument() ) { m_tree->addTopLevelItems( createItems( url, m_document->bookmarkManager()->bookmarks( url ) ) ); m_currentDocumentItem = m_tree->invisibleRootItem(); break; } } } } else { QTreeWidgetItem * currenturlitem = nullptr; foreach ( const QUrl& url, urls ) { QList subitems = createItems( url, m_document->bookmarkManager()->bookmarks( url ) ); if ( !subitems.isEmpty() ) { FileItem * item = new FileItem( url, m_tree, m_document ); item->addChildren( subitems ); if ( !currenturlitem && url == m_document->currentDocument() ) { currenturlitem = item; } } } if ( currenturlitem ) { currenturlitem->setExpanded( true ); currenturlitem->setIcon( 0, QIcon::fromTheme( QStringLiteral("bookmarks") ) ); m_tree->scrollToItem( currenturlitem, QAbstractItemView::PositionAtTop ); m_currentDocumentItem = currenturlitem; } } m_tree->sortItems( 0, Qt::AscendingOrder ); connect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged); } void BookmarkList::goTo( BookmarkItem * item ) { if ( item->url() == m_document->currentDocument() ) { m_document->setViewport( item->viewport() ); } else { Okular::GotoAction action( item->url().toDisplayString(QUrl::PreferLocalFile), item->viewport() ); m_document->processAction( &action ); } } void BookmarkList::selectiveUrlUpdate( const QUrl& url, QTreeWidgetItem*& item ) { disconnect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged); const KBookmark::List urlbookmarks = m_document->bookmarkManager()->bookmarks( url ); if ( urlbookmarks.isEmpty() ) { if ( item != m_tree->invisibleRootItem() ) { m_tree->invisibleRootItem()->removeChild( item ); item = nullptr; } else if ( item ) { for ( int i = item->childCount(); i >= 0; --i ) { item->removeChild( item->child( i ) ); } } } else { bool fileitem_created = false; if ( item ) { for ( int i = item->childCount() - 1; i >= 0; --i ) { item->removeChild( item->child( i ) ); } } else { item = new FileItem( url, m_tree, m_document ); fileitem_created = true; } if ( m_document->isOpened() && url == m_document->currentDocument() ) { item->setIcon( 0, QIcon::fromTheme( QStringLiteral("bookmarks") ) ); item->setExpanded( true ); } item->addChildren( createItems( url, urlbookmarks ) ); if ( fileitem_created ) { // we need to sort also the parent of the new file item, // so it can be properly shown in the correct place m_tree->invisibleRootItem()->sortChildren( 0, Qt::AscendingOrder ); } item->sortChildren( 0, Qt::AscendingOrder ); } connect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged); } QTreeWidgetItem* BookmarkList::itemForUrl( const QUrl& url ) const { const int count = m_tree->topLevelItemCount(); for ( int i = 0; i < count; ++i ) { QTreeWidgetItem *item = m_tree->topLevelItem( i ); const QUrl itemurl = item->data( 0, UrlRole ).value< QUrl >(); if ( itemurl.isValid() && itemurl == url ) { return item; } } return nullptr; } #include "moc_bookmarklist.cpp" diff --git a/ui/formwidgets.cpp b/ui/formwidgets.cpp index a1215ce7d..7f3d188c8 100644 --- a/ui/formwidgets.cpp +++ b/ui/formwidgets.cpp @@ -1,974 +1,982 @@ /*************************************************************************** * 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 ); } FormWidgetsController::~FormWidgetsController() { } void FormWidgetsController::signalAction( Okular::Action *a ) { emit action( a ); } -QButtonGroup* FormWidgetsController::registerRadioButton( QAbstractButton *button, Okular::FormFieldButton *formButton ) +void FormWidgetsController::registerRadioButton( FormWidgetIface *fwButton, Okular::FormFieldButton *formButton ) { + if ( !fwButton ) + return; + + QAbstractButton *button = dynamic_cast(fwButton); if ( !button ) - return nullptr; + { + 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_formButtons.insert( id, formButton ); 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 (*it).group; + 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 ); - return newdata.group; } 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(); - m_formButtons.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 ) ) { // Checkboxes need to be uncheckable so if clicking a checked one // disable the exclusive status temporarily and uncheck it - if (m_formButtons[check->formField()->id()]->state()) { + 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() ); - int id = button->group()->id( button ); - formButtons.append( m_formButtons[id] ); - prevChecked.append( m_formButtons[id]->state() ); + Okular::FormFieldButton *formButton = static_cast( dynamic_cast(button)->formField() ); + formButtons.append( formButton ); + prevChecked.append( formButton->state() ); } if (checked != prevChecked) emit formButtonsChangedByWidget( pageNumber, formButtons, checked ); } void FormWidgetsController::slotFormButtonsChangedByUndoRedo( int pageNumber, const QList< Okular::FormFieldButton* > & formButtons) { foreach ( Okular::FormFieldButton* formButton, formButtons ) { int id = formButton->id(); QAbstractButton* button = m_buttons[id]; // 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: ; } return widget; } -FormWidgetIface::FormWidgetIface( QWidget * w, Okular::FormField * ff ) - : m_controller( nullptr ), m_widget( w ), m_ff( ff ), m_pageItem( nullptr ) +FormWidgetIface::FormWidgetIface( QWidget * w, Okular::FormField * ff, bool canBeEnabled ) + : m_controller( nullptr ), m_ff( ff ), m_widget( w ), m_pageItem( nullptr ), + m_canBeEnabled( canBeEnabled ) { + 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 ) { - if ( m_widget->isEnabled() ) - { - m_widget->setEnabled( fill ); - } + m_widget->setEnabled( fill && m_canBeEnabled ); } 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; } -QAbstractButton* FormWidgetIface::button() -{ - return nullptr; -} - PushButtonEdit::PushButtonEdit( Okular::FormFieldButton * button, QWidget * parent ) - : QPushButton( parent ), FormWidgetIface( this, button ), m_form( button ) + : QPushButton( parent ), FormWidgetIface( this, button, !button->isReadOnly() ) { - setText( m_form->caption() ); - setVisible( m_form->isVisible() ); + setText( button->caption() ); + setVisible( button->isVisible() ); setCursor( Qt::ArrowCursor ); connect( this, &QAbstractButton::clicked, this, &PushButtonEdit::slotClicked ); } void PushButtonEdit::slotClicked() { - if ( m_form->activationAction() ) - m_controller->signalAction( m_form->activationAction() ); + if ( m_ff->activationAction() ) + m_controller->signalAction( m_ff->activationAction() ); } CheckBoxEdit::CheckBoxEdit( Okular::FormFieldButton * button, QWidget * parent ) - : QCheckBox( parent ), FormWidgetIface( this, button ), m_form( button ) + : QCheckBox( parent ), FormWidgetIface( this, button, !button->isReadOnly() ) { - setText( m_form->caption() ); + setText( button->caption() ); - setVisible( m_form->isVisible() ); + setVisible( button->isVisible() ); setCursor( Qt::ArrowCursor ); } void CheckBoxEdit::setFormWidgetsController( FormWidgetsController *controller ) { + Okular::FormFieldButton *form = static_cast(m_ff); FormWidgetIface::setFormWidgetsController( controller ); - m_controller->registerRadioButton( button(), m_form ); - setChecked( m_form->state() ); + m_controller->registerRadioButton( this, form ); + setChecked( form->state() ); connect( this, &QCheckBox::stateChanged, this, &CheckBoxEdit::slotStateChanged ); } -QAbstractButton* CheckBoxEdit::button() -{ - return this; -} - void CheckBoxEdit::slotStateChanged( int state ) { - if ( state == Qt::Checked && m_form->activationAction() ) - m_controller->signalAction( m_form->activationAction() ); + Okular::FormFieldButton *form = static_cast(m_ff); + if ( state == Qt::Checked && form->activationAction() ) + m_controller->signalAction( form->activationAction() ); } RadioButtonEdit::RadioButtonEdit( Okular::FormFieldButton * button, QWidget * parent ) - : QRadioButton( parent ), FormWidgetIface( this, button ), m_form( button ) + : QRadioButton( parent ), FormWidgetIface( this, button, !button->isReadOnly() ) { - setText( m_form->caption() ); + setText( button->caption() ); - setVisible( m_form->isVisible() ); + setVisible( button->isVisible() ); setCursor( Qt::ArrowCursor ); } void RadioButtonEdit::setFormWidgetsController( FormWidgetsController *controller ) { + Okular::FormFieldButton *form = static_cast(m_ff); FormWidgetIface::setFormWidgetsController( controller ); - m_controller->registerRadioButton( button(), m_form ); - setChecked( m_form->state() ); + m_controller->registerRadioButton( this, form ); + setChecked( form->state() ); } -QAbstractButton* RadioButtonEdit::button() -{ - return this; -} FormLineEdit::FormLineEdit( Okular::FormFieldText * text, QWidget * parent ) - : QLineEdit( parent ), FormWidgetIface( this, text ), m_form( text ) + : QLineEdit( parent ), FormWidgetIface( this, text, true ) { - int maxlen = m_form->maximumLength(); + int maxlen = text->maximumLength(); if ( maxlen >= 0 ) setMaxLength( maxlen ); - setAlignment( m_form->textAlignment() ); - setText( m_form->text() ); - if ( m_form->isPassword() ) + 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( m_form->isVisible() ); + setVisible( text->isVisible() ); } void FormLineEdit::setFormWidgetsController(FormWidgetsController* controller) { FormWidgetIface::setFormWidgetsController(controller); connect( m_controller, &FormWidgetsController::formTextChangedByUndoRedo, this, &FormLineEdit::slotHandleTextChangedByUndoRedo ); } 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 != m_form->text() ) + if ( contents != form->text() ) { m_controller->formTextChangedByWidget( pageItem()->pageNumber(), - m_form, + 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_form || contents == text() ) + 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(); } TextAreaEdit::TextAreaEdit( Okular::FormFieldText * text, QWidget * parent ) -: KTextEdit( parent ), FormWidgetIface( this, text ), m_form( text ) +: KTextEdit( parent ), FormWidgetIface( this, text, true ) { - setAcceptRichText( m_form->isRichText() ); - setCheckSpellingEnabled( m_form->canBeSpellChecked() ); - setAlignment( m_form->textAlignment() ); - setPlainText( m_form->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( m_form->isVisible() ); + 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 ); } void TextAreaEdit::slotHandleTextChangedByUndoRedo( int pageNumber, Okular::FormFieldText* textForm, const QString & contents, int cursorPos, int anchorPos ) { Q_UNUSED(pageNumber); - if ( textForm != m_form ) + 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 != m_form->text()) + if (contents != form->text()) { m_controller->formTextChangedByWidget( pageItem()->pageNumber(), - m_form, + form, contents, cursorPos, m_prevCursorPos, m_prevAnchorPos ); } m_prevCursorPos = cursorPos; m_prevAnchorPos = textCursor().anchor(); } FileEdit::FileEdit( Okular::FormFieldText * text, QWidget * parent ) - : KUrlRequester( parent ), FormWidgetIface( this, text ), m_form( text ) + : KUrlRequester( parent ), FormWidgetIface( this, text, !text->isReadOnly() ) { setMode( KFile::File | KFile::ExistingOnly | KFile::LocalOnly ); setFilter( i18n( "*|All Files" ) ); - setUrl( QUrl::fromUserInput( m_form->text() ) ); - lineEdit()->setAlignment( m_form->textAlignment() ); + 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( m_form->isVisible() ); + 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 != m_form->text()) + if (contents != form->text()) { m_controller->formTextChangedByWidget( pageItem()->pageNumber(), - m_form, + 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_form || contents == text() ) + 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 ), m_form( choice ) + : QListWidget( parent ), FormWidgetIface( this, choice, !choice->isReadOnly() ) { - addItems( m_form->choices() ); - setSelectionMode( m_form->multiSelect() ? QAbstractItemView::ExtendedSelection : QAbstractItemView::SingleSelection ); + addItems( choice->choices() ); + setSelectionMode( choice->multiSelect() ? QAbstractItemView::ExtendedSelection : QAbstractItemView::SingleSelection ); setVerticalScrollMode( QAbstractItemView::ScrollPerPixel ); - QList< int > selectedItems = m_form->currentChoices(); - if ( m_form->multiSelect() ) + 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( m_form->isVisible() ); + 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 ) ); - if ( rows != m_form->currentChoices() ) { + Okular::FormFieldChoice *form = static_cast(m_ff); + if ( rows != form->currentChoices() ) { m_controller->formListChangedByWidget( pageItem()->pageNumber(), - m_form, + form, rows ); } } void ListEdit::slotHandleFormListChangedByUndoRedo( int pageNumber, Okular::FormFieldChoice* listForm, const QList< int > & choices ) { Q_UNUSED(pageNumber); - if ( m_form != listForm ) { + 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 ), m_form( choice ) + : QComboBox( parent ), FormWidgetIface( this, choice, !choice->isReadOnly() ) { - addItems( m_form->choices() ); + addItems( choice->choices() ); setEditable( true ); setInsertPolicy( NoInsert ); - lineEdit()->setReadOnly( !m_form->isEditable() ); - QList< int > selectedItems = m_form->currentChoices(); + 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 ( m_form->isEditable() && !m_form->editChoice().isEmpty() ) - lineEdit()->setText( m_form->editChoice() ); + 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( m_form->isVisible() ); + 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 ( m_form->currentChoices().isEmpty() ) + if ( form->currentChoices().isEmpty() ) { - prevText = m_form->editChoice(); + prevText = form->editChoice(); } else { - prevText = m_form->choices()[m_form->currentChoices()[0]]; + prevText = form->choices()[form->currentChoices()[0]]; } int cursorPos = lineEdit()->cursorPosition(); if ( text != prevText ) { m_controller->formComboChangedByWidget( pageItem()->pageNumber(), - m_form, + 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_form != form ) { + 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 b1bc11a4f..d8ba9619d 100644 --- a/ui/formwidgets.h +++ b/ui/formwidgets.h @@ -1,339 +1,324 @@ /*************************************************************************** * 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 ); - QButtonGroup* registerRadioButton( QAbstractButton *button, Okular::FormFieldButton *formButton ); + 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 ); 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, Okular::FormFieldButton* > m_formButtons; 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 ); + FormWidgetIface( QWidget * w, Okular::FormField * ff, bool canBeEnabled ); 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 ); - Okular::FormField* formField() const; PageViewItem* pageItem() const; + void setFormField( Okular::FormField *field ); + Okular::FormField* formField() const; virtual void setFormWidgetsController( FormWidgetsController *controller ); - virtual QAbstractButton* button(); protected: FormWidgetsController * m_controller; + Okular::FormField * m_ff; private: QWidget * m_widget; - Okular::FormField * m_ff; 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(); - - private: - Okular::FormFieldButton * m_form; }; 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; - QAbstractButton* button() override; private Q_SLOTS: void slotStateChanged( int state ); - - private: - Okular::FormFieldButton * m_form; }; 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; - QAbstractButton* button() override; - - private: - Okular::FormFieldButton * m_form; }; 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(); private: - Okular::FormFieldText * m_form; 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(); private: - Okular::FormFieldText * m_form; 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: - Okular::FormFieldText * m_form; 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 ); - - private: - Okular::FormFieldChoice * m_form; }; 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: - Okular::FormFieldChoice * m_form; int m_prevCursorPos; int m_prevAnchorPos; }; #endif diff --git a/ui/pageview.cpp b/ui/pageview.cpp index 0f3934b5c..16f57c23d 100644 --- a/ui/pageview.cpp +++ b/ui/pageview.cpp @@ -1,5453 +1,5532 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2006 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 * * * * With portions of code from kpdf/kpdf_pagewidget.cc by: * * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2003 by Christophe Devriese * * * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003 by Dirk Mueller * * Copyright (C) 2004 by James Ots * * Copyright (C) 2011 by Jiri Baum - NICTA * * * * 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 "pageview.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // system includes #include #include // local includes #include "debug_ui.h" #include "formwidgets.h" #include "pageviewutils.h" #include "pagepainter.h" #include "core/annotations.h" #include "annotwindow.h" #include "guiutils.h" #include "annotationpopup.h" #include "pageviewannotator.h" #include "pageviewmouseannotation.h" #include "priorities.h" #include "toolaction.h" #include "okmenutitle.h" #ifdef HAVE_SPEECH #include "tts.h" #endif #include "videowidget.h" #include "core/action.h" #include "core/area.h" #include "core/document_p.h" #include "core/form.h" #include "core/page.h" +#include "core/page_p.h" #include "core/misc.h" #include "core/generator.h" #include "core/movie.h" #include "core/audioplayer.h" #include "core/sourcereference.h" #include "core/tile.h" #include "settings.h" #include "settings_core.h" #include "url_utils.h" #include "magnifierview.h" static const int pageflags = PagePainter::Accessibility | PagePainter::EnhanceLinks | PagePainter::EnhanceImages | PagePainter::Highlights | PagePainter::TextSelection | PagePainter::Annotations; static const float kZoomValues[] = { 0.12, 0.25, 0.33, 0.50, 0.66, 0.75, 1.00, 1.25, 1.50, 2.00, 4.00, 8.00, 16.00 }; static inline double normClamp( double value, double def ) { return ( value < 0.0 || value > 1.0 ) ? def : value; } struct TableSelectionPart { PageViewItem * item; Okular::NormalizedRect rectInItem; Okular::NormalizedRect rectInSelection; TableSelectionPart(PageViewItem * item_p, const Okular::NormalizedRect &rectInItem_p, const Okular::NormalizedRect &rectInSelection_p); }; TableSelectionPart::TableSelectionPart( PageViewItem * item_p, const Okular::NormalizedRect &rectInItem_p, const Okular::NormalizedRect &rectInSelection_p) : item ( item_p ), rectInItem (rectInItem_p), rectInSelection (rectInSelection_p) { } // structure used internally by PageView for data storage class PageViewPrivate { public: PageViewPrivate( PageView *qq ); FormWidgetsController* formWidgetsController(); #ifdef HAVE_SPEECH OkularTTS* tts(); #endif QString selectedText() const; // the document, pageviewItems and the 'visible cache' PageView *q; Okular::Document * document; QVector< PageViewItem * > items; QLinkedList< PageViewItem * > visibleItems; MagnifierView *magnifierView; // view layout (columns and continuous in Settings), zoom and mouse PageView::ZoomMode zoomMode; float zoomFactor; QPoint mouseGrabPos; QPoint mousePressPos; QPoint mouseSelectPos; int mouseMidLastY; bool mouseSelecting; QRect mouseSelectionRect; QColor mouseSelectionColor; bool mouseTextSelecting; QSet< int > pagesWithTextSelection; bool mouseOnRect; int mouseMode; MouseAnnotation * mouseAnnotation; // table selection QList tableSelectionCols; QList tableSelectionRows; QList tableSelectionParts; bool tableDividersGuessed; // viewport move bool viewportMoveActive; QTime viewportMoveTime; QPoint viewportMoveDest; int lastSourceLocationViewportPageNumber; double lastSourceLocationViewportNormalizedX; double lastSourceLocationViewportNormalizedY; QTimer * viewportMoveTimer; int controlWheelAccumulatedDelta; // auto scroll int scrollIncrement; QTimer * autoScrollTimer; // annotations PageViewAnnotator * annotator; //text annotation dialogs list - QHash< Okular::Annotation *, AnnotWindow * > m_annowindows; + QSet< AnnotWindow * > m_annowindows; // other stuff QTimer * delayResizeEventTimer; bool dirtyLayout; bool blockViewport; // prevents changes to viewport bool blockPixmapsRequest; // prevent pixmap requests PageViewMessage * messageWindow; // in pageviewutils.h bool m_formsVisible; FormWidgetsController *formsWidgetController; #ifdef HAVE_SPEECH OkularTTS * m_tts; #endif QTimer * refreshTimer; QSet refreshPages; // bbox state for Trim to Selection mode Okular::NormalizedRect trimBoundingBox; // infinite resizing loop prevention bool verticalScrollBarVisible; bool horizontalScrollBarVisible; // drag scroll QPoint dragScrollVector; QTimer dragScrollTimer; // left click depress QTimer leftClickTimer; // actions QAction * aRotateClockwise; QAction * aRotateCounterClockwise; QAction * aRotateOriginal; KSelectAction * aPageSizes; KActionMenu * aTrimMode; KToggleAction * aTrimMargins; QAction * aMouseNormal; QAction * aMouseSelect; QAction * aMouseTextSelect; QAction * aMouseTableSelect; QAction * aMouseMagnifier; KToggleAction * aTrimToSelection; KToggleAction * aToggleAnnotator; KSelectAction * aZoom; QAction * aZoomIn; QAction * aZoomOut; KToggleAction * aZoomFitWidth; KToggleAction * aZoomFitPage; KToggleAction * aZoomAutoFit; KActionMenu * aViewMode; KToggleAction * aViewContinuous; QAction * aPrevAction; QAction * aToggleForms; QAction * aSpeakDoc; QAction * aSpeakPage; QAction * aSpeakStop; KActionCollection * actionCollection; QActionGroup * mouseModeActionGroup; QAction * aFitWindowToPage; int setting_viewCols; bool rtl_Mode; // Keep track of whether tablet pen is currently pressed down bool penDown; // Keep track of mouse over link object const Okular::ObjectRect * mouseOverLinkObject; }; PageViewPrivate::PageViewPrivate( PageView *qq ) : q( qq ) #ifdef HAVE_SPEECH , m_tts( nullptr ) #endif { } FormWidgetsController* PageViewPrivate::formWidgetsController() { if ( !formsWidgetController ) { formsWidgetController = new FormWidgetsController( document ); QObject::connect( formsWidgetController, SIGNAL( changed( int ) ), q, SLOT( slotFormChanged( int ) ) ); QObject::connect( formsWidgetController, SIGNAL( action( Okular::Action* ) ), q, SLOT( slotAction( Okular::Action* ) ) ); } return formsWidgetController; } #ifdef HAVE_SPEECH OkularTTS* PageViewPrivate::tts() { if ( !m_tts ) { m_tts = new OkularTTS( q ); if ( aSpeakStop ) { QObject::connect( m_tts, &OkularTTS::isSpeaking, aSpeakStop, &QAction::setEnabled ); } } return m_tts; } #endif /* PageView. What's in this file? -> quick overview. * Code weight (in rows) and meaning: * 160 - constructor and creating actions plus their connected slots (empty stuff) * 70 - DocumentObserver inherited methodes (important) * 550 - events: mouse, keyboard, drag * 170 - slotRelayoutPages: set contents of the scrollview on continuous/single modes * 100 - zoom: zooming pages in different ways, keeping update the toolbar actions, etc.. * other misc functions: only slotRequestVisiblePixmaps and pickItemOnPoint noticeable, * and many insignificant stuff like this comment :-) */ PageView::PageView( QWidget *parent, Okular::Document *document ) : QAbstractScrollArea( parent ) , Okular::View( QLatin1String( "PageView" ) ) { // create and initialize private storage structure d = new PageViewPrivate( this ); d->document = document; d->aRotateClockwise = nullptr; d->aRotateCounterClockwise = nullptr; d->aRotateOriginal = nullptr; d->aViewMode = nullptr; d->zoomMode = PageView::ZoomFitWidth; d->zoomFactor = 1.0; d->mouseSelecting = false; d->mouseTextSelecting = false; d->mouseOnRect = false; d->mouseMode = Okular::Settings::mouseMode(); d->mouseAnnotation = new MouseAnnotation( this, document ); d->tableDividersGuessed = false; d->viewportMoveActive = false; d->lastSourceLocationViewportPageNumber = -1; d->lastSourceLocationViewportNormalizedX = 0.0; d->lastSourceLocationViewportNormalizedY = 0.0; d->viewportMoveTimer = nullptr; d->controlWheelAccumulatedDelta = 0; d->scrollIncrement = 0; d->autoScrollTimer = nullptr; d->annotator = nullptr; d->dirtyLayout = false; d->blockViewport = false; d->blockPixmapsRequest = false; d->messageWindow = new PageViewMessage(this); d->m_formsVisible = false; d->formsWidgetController = nullptr; #ifdef HAVE_SPEECH d->m_tts = nullptr; #endif d->refreshTimer = nullptr; d->aRotateClockwise = nullptr; d->aRotateCounterClockwise = nullptr; d->aRotateOriginal = nullptr; d->aPageSizes = nullptr; d->aTrimMode = nullptr; d->aTrimMargins = nullptr; d->aTrimToSelection = nullptr; d->aMouseNormal = nullptr; d->aMouseSelect = nullptr; d->aMouseTextSelect = nullptr; d->aToggleAnnotator = nullptr; d->aZoomFitWidth = nullptr; d->aZoomFitPage = nullptr; d->aZoomAutoFit = nullptr; d->aViewMode = nullptr; d->aViewContinuous = nullptr; d->aPrevAction = nullptr; d->aToggleForms = nullptr; d->aSpeakDoc = nullptr; d->aSpeakPage = nullptr; d->aSpeakStop = nullptr; d->actionCollection = nullptr; d->aPageSizes=nullptr; d->setting_viewCols = Okular::Settings::viewColumns(); d->rtl_Mode = Okular::Settings::rtlReadingDirection(); d->mouseModeActionGroup = nullptr; d->penDown = false; d->aMouseMagnifier = nullptr; d->aFitWindowToPage = nullptr; d->trimBoundingBox = Okular::NormalizedRect(); // Null box switch( Okular::Settings::zoomMode() ) { case 0: { d->zoomFactor = 1; d->zoomMode = PageView::ZoomFixed; break; } case 1: { d->zoomMode = PageView::ZoomFitWidth; break; } case 2: { d->zoomMode = PageView::ZoomFitPage; break; } case 3: { d->zoomMode = PageView::ZoomFitAuto; break; } } d->delayResizeEventTimer = new QTimer( this ); d->delayResizeEventTimer->setSingleShot( true ); connect( d->delayResizeEventTimer, &QTimer::timeout, this, &PageView::delayedResizeEvent ); setFrameStyle(QFrame::NoFrame); setAttribute( Qt::WA_StaticContents ); setObjectName( QStringLiteral( "okular::pageView" ) ); // viewport setup: setup focus, and track mouse viewport()->setFocusProxy( this ); viewport()->setFocusPolicy( Qt::StrongFocus ); viewport()->setAttribute( Qt::WA_OpaquePaintEvent ); viewport()->setAttribute( Qt::WA_NoSystemBackground ); viewport()->setMouseTracking( true ); viewport()->setAutoFillBackground( false ); // the apparently "magic" value of 20 is the same used internally in QScrollArea verticalScrollBar()->setCursor( Qt::ArrowCursor ); verticalScrollBar()->setSingleStep( 20 ); horizontalScrollBar()->setCursor( Qt::ArrowCursor ); horizontalScrollBar()->setSingleStep( 20 ); // conntect the padding of the viewport to pixmaps requests connect(horizontalScrollBar(), &QAbstractSlider::valueChanged, this, &PageView::slotRequestVisiblePixmaps); connect(verticalScrollBar(), &QAbstractSlider::valueChanged, this, &PageView::slotRequestVisiblePixmaps); connect( &d->dragScrollTimer, &QTimer::timeout, this, &PageView::slotDragScroll ); d->leftClickTimer.setSingleShot( true ); connect( &d->leftClickTimer, &QTimer::timeout, this, &PageView::slotShowSizeAllCursor ); // set a corner button to resize the view to the page size // QPushButton * resizeButton = new QPushButton( viewport() ); // resizeButton->setPixmap( SmallIcon("crop") ); // setCornerWidget( resizeButton ); // resizeButton->setEnabled( false ); // connect(...); setAttribute( Qt::WA_InputMethodEnabled, true ); // Grab pinch gestures to zoom and rotate the view grabGesture(Qt::PinchGesture); d->magnifierView = new MagnifierView(document, this); d->magnifierView->hide(); d->magnifierView->setGeometry(0, 0, 351, 201); // TODO: more dynamic? connect(document, &Okular::Document::processMovieAction, this, &PageView::slotProcessMovieAction); connect(document, &Okular::Document::processRenditionAction, this, &PageView::slotProcessRenditionAction); // schedule the welcome message QMetaObject::invokeMethod(this, "slotShowWelcome", Qt::QueuedConnection); } PageView::~PageView() { #ifdef HAVE_SPEECH if ( d->m_tts ) d->m_tts->stopAllSpeechs(); #endif delete d->mouseAnnotation; // delete the local storage structure // We need to assign it to a different list otherwise slotAnnotationWindowDestroyed // will bite us and clear d->m_annowindows - QHash< Okular::Annotation *, AnnotWindow * > annowindows = d->m_annowindows; + QSet< AnnotWindow * > annowindows = d->m_annowindows; d->m_annowindows.clear(); qDeleteAll( annowindows ); // delete all widgets QVector< PageViewItem * >::const_iterator dIt = d->items.constBegin(), dEnd = d->items.constEnd(); for ( ; dIt != dEnd; ++dIt ) delete *dIt; delete d->formsWidgetController; d->document->removeObserver( this ); delete d; } void PageView::setupBaseActions( KActionCollection * ac ) { d->actionCollection = ac; // Zoom actions ( higher scales takes lots of memory! ) d->aZoom = new KSelectAction(QIcon::fromTheme( QStringLiteral("page-zoom") ), i18n("Zoom"), this); ac->addAction(QStringLiteral("zoom_to"), d->aZoom ); d->aZoom->setEditable( true ); d->aZoom->setMaxComboViewCount( 14 ); connect( d->aZoom, SIGNAL(triggered(QAction*)), this, SLOT(slotZoom()) ); updateZoomText(); d->aZoomIn = KStandardAction::zoomIn( this, SLOT(slotZoomIn()), ac ); d->aZoomOut = KStandardAction::zoomOut( this, SLOT(slotZoomOut()), ac ); } void PageView::setupViewerActions( KActionCollection * ac ) { d->actionCollection = ac; ac->setDefaultShortcut(d->aZoomIn, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Plus)); ac->setDefaultShortcut(d->aZoomOut, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Minus)); // orientation menu actions d->aRotateClockwise = new QAction( QIcon::fromTheme( QStringLiteral("object-rotate-right") ), i18n( "Rotate &Right" ), this ); d->aRotateClockwise->setIconText( i18nc( "Rotate right", "Right" ) ); ac->addAction( QStringLiteral("view_orientation_rotate_cw"), d->aRotateClockwise ); d->aRotateClockwise->setEnabled( false ); connect( d->aRotateClockwise, &QAction::triggered, this, &PageView::slotRotateClockwise ); d->aRotateCounterClockwise = new QAction( QIcon::fromTheme( QStringLiteral("object-rotate-left") ), i18n( "Rotate &Left" ), this ); d->aRotateCounterClockwise->setIconText( i18nc( "Rotate left", "Left" ) ); ac->addAction( QStringLiteral("view_orientation_rotate_ccw"), d->aRotateCounterClockwise ); d->aRotateCounterClockwise->setEnabled( false ); connect( d->aRotateCounterClockwise, &QAction::triggered, this, &PageView::slotRotateCounterClockwise ); d->aRotateOriginal = new QAction( i18n( "Original Orientation" ), this ); ac->addAction( QStringLiteral("view_orientation_original"), d->aRotateOriginal ); d->aRotateOriginal->setEnabled( false ); connect( d->aRotateOriginal, &QAction::triggered, this, &PageView::slotRotateOriginal ); d->aPageSizes = new KSelectAction(i18n("&Page Size"), this); ac->addAction(QStringLiteral("view_pagesizes"), d->aPageSizes); d->aPageSizes->setEnabled( false ); connect( d->aPageSizes , SIGNAL(triggered(int)), this, SLOT(slotPageSizes(int)) ); // Trim View actions d->aTrimMode = new KActionMenu(i18n( "&Trim View" ), this ); d->aTrimMode->setDelayed( false ); ac->addAction(QStringLiteral("view_trim_mode"), d->aTrimMode ); d->aTrimMargins = new KToggleAction( i18n( "&Trim Margins" ), d->aTrimMode->menu() ); d->aTrimMode->addAction( d->aTrimMargins ); ac->addAction( QStringLiteral("view_trim_margins"), d->aTrimMargins ); d->aTrimMargins->setData( qVariantFromValue( (int)Okular::Settings::EnumTrimMode::Margins ) ); connect( d->aTrimMargins, &QAction::toggled, this, &PageView::slotTrimMarginsToggled ); d->aTrimMargins->setChecked( Okular::Settings::trimMargins() ); d->aTrimToSelection = new KToggleAction( i18n( "Trim To &Selection" ), d->aTrimMode->menu() ); d->aTrimMode->addAction( d->aTrimToSelection); ac->addAction( QStringLiteral("view_trim_selection"), d->aTrimToSelection); d->aTrimToSelection->setData( qVariantFromValue( (int)Okular::Settings::EnumTrimMode::Selection ) ); connect( d->aTrimToSelection, &QAction::toggled, this, &PageView::slotTrimToSelectionToggled ); d->aZoomFitWidth = new KToggleAction(QIcon::fromTheme( QStringLiteral("zoom-fit-width") ), i18n("Fit &Width"), this); ac->addAction(QStringLiteral("view_fit_to_width"), d->aZoomFitWidth ); connect( d->aZoomFitWidth, &QAction::toggled, this, &PageView::slotFitToWidthToggled ); d->aZoomFitPage = new KToggleAction(QIcon::fromTheme( QStringLiteral("zoom-fit-best") ), i18n("Fit &Page"), this); ac->addAction(QStringLiteral("view_fit_to_page"), d->aZoomFitPage ); connect( d->aZoomFitPage, &QAction::toggled, this, &PageView::slotFitToPageToggled ); d->aZoomAutoFit = new KToggleAction(QIcon::fromTheme( QStringLiteral("zoom-fit-best") ), i18n("&Auto Fit"), this); ac->addAction(QStringLiteral("view_auto_fit"), d->aZoomAutoFit ); connect( d->aZoomAutoFit, &QAction::toggled, this, &PageView::slotAutoFitToggled ); d->aFitWindowToPage = new QAction(QIcon::fromTheme( QStringLiteral("zoom-fit-width") ), i18n("Fit Wi&ndow to Page"), this); d->aFitWindowToPage->setEnabled( Okular::Settings::viewMode() == (int)Okular::Settings::EnumViewMode::Single ); ac->setDefaultShortcut(d->aFitWindowToPage, QKeySequence(Qt::CTRL + Qt::Key_J) ); ac->addAction( QStringLiteral("fit_window_to_page"), d->aFitWindowToPage ); connect( d->aFitWindowToPage, &QAction::triggered, this, &PageView::slotFitWindowToPage ); // View-Layout actions d->aViewMode = new KActionMenu( QIcon::fromTheme( QStringLiteral("view-split-left-right") ), i18n( "&View Mode" ), this ); d->aViewMode->setDelayed( false ); #define ADD_VIEWMODE_ACTION( text, name, id ) \ do { \ QAction *vm = new QAction( text, this ); \ vm->setCheckable( true ); \ vm->setData( qVariantFromValue( id ) ); \ d->aViewMode->addAction( vm ); \ ac->addAction( QStringLiteral(name), vm ); \ vmGroup->addAction( vm ); \ } while( 0 ) ac->addAction(QStringLiteral("view_render_mode"), d->aViewMode ); QActionGroup *vmGroup = new QActionGroup( this ); //d->aViewMode->menu() ); ADD_VIEWMODE_ACTION( i18n( "Single Page" ), "view_render_mode_single", (int)Okular::Settings::EnumViewMode::Single ); ADD_VIEWMODE_ACTION( i18n( "Facing Pages" ), "view_render_mode_facing", (int)Okular::Settings::EnumViewMode::Facing ); ADD_VIEWMODE_ACTION( i18n( "Facing Pages (Center First Page)" ), "view_render_mode_facing_center_first", (int)Okular::Settings::EnumViewMode::FacingFirstCentered ); ADD_VIEWMODE_ACTION( i18n( "Overview" ), "view_render_mode_overview", (int)Okular::Settings::EnumViewMode::Summary ); const QList viewModeActions = d->aViewMode->menu()->actions(); foreach(QAction *viewModeAction, viewModeActions) { if (viewModeAction->data().toInt() == Okular::Settings::viewMode()) { viewModeAction->setChecked( true ); } } connect( vmGroup, &QActionGroup::triggered, this, &PageView::slotViewMode ); #undef ADD_VIEWMODE_ACTION d->aViewContinuous = new KToggleAction(QIcon::fromTheme( QStringLiteral("view-list-text") ), i18n("&Continuous"), this); ac->addAction(QStringLiteral("view_continuous"), d->aViewContinuous ); connect( d->aViewContinuous, &QAction::toggled, this, &PageView::slotContinuousToggled ); d->aViewContinuous->setChecked( Okular::Settings::viewContinuous() ); // Mouse mode actions for viewer mode d->mouseModeActionGroup = new QActionGroup( this ); d->mouseModeActionGroup->setExclusive( true ); d->aMouseNormal = new QAction( QIcon::fromTheme( QStringLiteral("input-mouse") ), i18n( "&Browse Tool" ), this ); ac->addAction(QStringLiteral("mouse_drag"), d->aMouseNormal ); connect( d->aMouseNormal, &QAction::triggered, this, &PageView::slotSetMouseNormal ); d->aMouseNormal->setIconText( i18nc( "Browse Tool", "Browse" ) ); d->aMouseNormal->setCheckable( true ); ac->setDefaultShortcut(d->aMouseNormal, QKeySequence(Qt::CTRL + Qt::Key_1)); d->aMouseNormal->setActionGroup( d->mouseModeActionGroup ); d->aMouseNormal->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Browse ); QAction * mz = new QAction(QIcon::fromTheme( QStringLiteral("page-zoom") ), i18n("&Zoom Tool"), this); ac->addAction(QStringLiteral("mouse_zoom"), mz ); connect( mz, &QAction::triggered, this, &PageView::slotSetMouseZoom ); mz->setIconText( i18nc( "Zoom Tool", "Zoom" ) ); mz->setCheckable( true ); ac->setDefaultShortcut(mz, QKeySequence(Qt::CTRL + Qt::Key_2)); mz->setActionGroup( d->mouseModeActionGroup ); mz->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Zoom ); QAction * aToggleChangeColors = new QAction(i18n("&Toggle Change Colors"), this); ac->addAction(QStringLiteral("toggle_change_colors"), aToggleChangeColors ); connect( aToggleChangeColors, &QAction::triggered, this, &PageView::slotToggleChangeColors ); } // WARNING: 'setupViewerActions' must have been called before this method void PageView::setupActions( KActionCollection * ac ) { d->actionCollection = ac; ac->setDefaultShortcuts(d->aZoomIn, KStandardShortcut::zoomIn()); ac->setDefaultShortcuts(d->aZoomOut, KStandardShortcut::zoomOut()); // Mouse-Mode actions d->aMouseSelect = new QAction(QIcon::fromTheme( QStringLiteral("select-rectangular") ), i18n("&Selection Tool"), this); ac->addAction(QStringLiteral("mouse_select"), d->aMouseSelect ); connect( d->aMouseSelect, &QAction::triggered, this, &PageView::slotSetMouseSelect ); d->aMouseSelect->setIconText( i18nc( "Select Tool", "Selection" ) ); d->aMouseSelect->setCheckable( true ); ac->setDefaultShortcut(d->aMouseSelect, Qt::CTRL + Qt::Key_3); d->aMouseSelect->setActionGroup( d->mouseModeActionGroup ); d->aMouseSelect->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::RectSelect ); d->aMouseTextSelect = new QAction(QIcon::fromTheme( QStringLiteral("draw-text") ), i18n("&Text Selection Tool"), this); ac->addAction(QStringLiteral("mouse_textselect"), d->aMouseTextSelect ); connect( d->aMouseTextSelect, &QAction::triggered, this, &PageView::slotSetMouseTextSelect ); d->aMouseTextSelect->setIconText( i18nc( "Text Selection Tool", "Text Selection" ) ); d->aMouseTextSelect->setCheckable( true ); ac->setDefaultShortcut(d->aMouseTextSelect, Qt::CTRL + Qt::Key_4); d->aMouseTextSelect->setActionGroup( d->mouseModeActionGroup ); d->aMouseTextSelect->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::TextSelect ); d->aMouseTableSelect = new QAction(QIcon::fromTheme( QStringLiteral("table") ), i18n("T&able Selection Tool"), this); ac->addAction(QStringLiteral("mouse_tableselect"), d->aMouseTableSelect ); connect( d->aMouseTableSelect, &QAction::triggered, this, &PageView::slotSetMouseTableSelect ); d->aMouseTableSelect->setIconText( i18nc( "Table Selection Tool", "Table Selection" ) ); d->aMouseTableSelect->setCheckable( true ); ac->setDefaultShortcut(d->aMouseTableSelect, Qt::CTRL + Qt::Key_5); d->aMouseTableSelect->setActionGroup( d->mouseModeActionGroup ); d->aMouseTableSelect->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::TableSelect ); d->aMouseMagnifier = new QAction(QIcon::fromTheme( QStringLiteral("document-preview") ), i18n("&Magnifier"), this); ac->addAction(QStringLiteral("mouse_magnifier"), d->aMouseMagnifier ); connect( d->aMouseMagnifier, &QAction::triggered, this, &PageView::slotSetMouseMagnifier ); d->aMouseMagnifier->setIconText( i18nc( "Magnifier Tool", "Magnifier" ) ); d->aMouseMagnifier->setCheckable( true ); ac->setDefaultShortcut(d->aMouseMagnifier, Qt::CTRL + Qt::Key_6); d->aMouseMagnifier->setActionGroup( d->mouseModeActionGroup ); d->aMouseMagnifier->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Magnifier ); d->aToggleAnnotator = new KToggleAction(QIcon::fromTheme( QStringLiteral("draw-freehand") ), i18n("&Review"), this); ac->addAction(QStringLiteral("mouse_toggle_annotate"), d->aToggleAnnotator ); d->aToggleAnnotator->setCheckable( true ); connect( d->aToggleAnnotator, &QAction::toggled, this, &PageView::slotToggleAnnotator ); ac->setDefaultShortcut(d->aToggleAnnotator, Qt::Key_F6); ToolAction *ta = new ToolAction( this ); ac->addAction( QStringLiteral("mouse_selecttools"), ta ); ta->addAction( d->aMouseSelect ); ta->addAction( d->aMouseTextSelect ); ta->addAction( d->aMouseTableSelect ); // speak actions #ifdef HAVE_SPEECH d->aSpeakDoc = new QAction( QIcon::fromTheme( QStringLiteral("text-speak") ), i18n( "Speak Whole Document" ), this ); ac->addAction( QStringLiteral("speak_document"), d->aSpeakDoc ); d->aSpeakDoc->setEnabled( false ); connect( d->aSpeakDoc, &QAction::triggered, this, &PageView::slotSpeakDocument ); d->aSpeakPage = new QAction( QIcon::fromTheme( QStringLiteral("text-speak") ), i18n( "Speak Current Page" ), this ); ac->addAction( QStringLiteral("speak_current_page"), d->aSpeakPage ); d->aSpeakPage->setEnabled( false ); connect( d->aSpeakPage, &QAction::triggered, this, &PageView::slotSpeakCurrentPage ); d->aSpeakStop = new QAction( QIcon::fromTheme( QStringLiteral("media-playback-stop") ), i18n( "Stop Speaking" ), this ); ac->addAction( QStringLiteral("speak_stop_all"), d->aSpeakStop ); d->aSpeakStop->setEnabled( false ); connect( d->aSpeakStop, &QAction::triggered, this, &PageView::slotStopSpeaks ); #else d->aSpeakDoc = 0; d->aSpeakPage = 0; d->aSpeakStop = 0; #endif // Other actions QAction * su = new QAction(i18n("Scroll Up"), this); ac->addAction(QStringLiteral("view_scroll_up"), su ); connect( su, &QAction::triggered, this, &PageView::slotAutoScrollUp ); ac->setDefaultShortcut(su, QKeySequence(Qt::SHIFT + Qt::Key_Up)); addAction(su); QAction * sd = new QAction(i18n("Scroll Down"), this); ac->addAction(QStringLiteral("view_scroll_down"), sd ); connect( sd, &QAction::triggered, this, &PageView::slotAutoScrollDown ); ac->setDefaultShortcut(sd, QKeySequence(Qt::SHIFT + Qt::Key_Down)); addAction(sd); QAction * spu = new QAction(i18n("Scroll Page Up"), this); ac->addAction( QStringLiteral("view_scroll_page_up"), spu ); connect( spu, &QAction::triggered, this, &PageView::slotScrollUp ); ac->setDefaultShortcut(spu, QKeySequence(Qt::SHIFT + Qt::Key_Space)); addAction( spu ); QAction * spd = new QAction(i18n("Scroll Page Down"), this); ac->addAction( QStringLiteral("view_scroll_page_down"), spd ); connect( spd, &QAction::triggered, this, &PageView::slotScrollDown ); ac->setDefaultShortcut(spd, QKeySequence(Qt::Key_Space)); addAction( spd ); d->aToggleForms = new QAction( this ); ac->addAction( QStringLiteral("view_toggle_forms"), d->aToggleForms ); connect( d->aToggleForms, &QAction::triggered, this, &PageView::slotToggleForms ); d->aToggleForms->setEnabled( false ); toggleFormWidgets( false ); // Setup undo and redo actions QAction *kundo = KStandardAction::create( KStandardAction::Undo, d->document, SLOT(undo()), ac ); QAction *kredo = KStandardAction::create( KStandardAction::Redo, d->document, SLOT(redo()), ac ); connect(d->document, &Okular::Document::canUndoChanged, kundo, &QAction::setEnabled); connect(d->document, &Okular::Document::canRedoChanged, kredo, &QAction::setEnabled); kundo->setEnabled(false); kredo->setEnabled(false); } bool PageView::canFitPageWidth() const { return Okular::Settings::viewMode() != Okular::Settings::EnumViewMode::Single || d->zoomMode != ZoomFitWidth; } void PageView::fitPageWidth( int page ) { // zoom: Fit Width, columns: 1. setActions + relayout + setPage + update d->zoomMode = ZoomFitWidth; Okular::Settings::setViewMode( 0 ); d->aZoomFitWidth->setChecked( true ); d->aZoomFitPage->setChecked( false ); d->aZoomAutoFit->setChecked( false ); d->aViewMode->menu()->actions().at( 0 )->setChecked( true ); viewport()->setUpdatesEnabled( false ); slotRelayoutPages(); viewport()->setUpdatesEnabled( true ); d->document->setViewportPage( page ); updateZoomText(); setFocus(); } void PageView::openAnnotationWindow( Okular::Annotation * annotation, int pageNumber ) { if ( !annotation ) return; // find the annot window AnnotWindow* existWindow = nullptr; - QHash< Okular::Annotation *, AnnotWindow * >::ConstIterator it = d->m_annowindows.constFind( annotation ); - if ( it != d->m_annowindows.constEnd() ) + foreach(AnnotWindow *aw, d->m_annowindows) { - existWindow = *it; + if ( aw->annotation() == annotation ) + { + existWindow = aw; + break; + } } if ( existWindow == nullptr ) { existWindow = new AnnotWindow( this, annotation, d->document, pageNumber ); connect(existWindow, &QObject::destroyed, this, &PageView::slotAnnotationWindowDestroyed); - d->m_annowindows.insert( annotation, existWindow ); + d->m_annowindows << existWindow; } existWindow->show(); } void PageView::slotAnnotationWindowDestroyed( QObject * window ) { - QHash< Okular::Annotation*, AnnotWindow * >::Iterator it = d->m_annowindows.begin(); - QHash< Okular::Annotation*, AnnotWindow * >::Iterator itEnd = d->m_annowindows.end(); - while ( it != itEnd ) - { - if ( it.value() == window ) - { - it = d->m_annowindows.erase( it ); - } - else - { - ++it; - } - } + d->m_annowindows.remove( static_cast( window ) ); } void PageView::displayMessage( const QString & message, const QString & details, PageViewMessage::Icon icon, int duration ) { if ( !Okular::Settings::showOSD() ) { if (icon == PageViewMessage::Error) { if ( !details.isEmpty() ) KMessageBox::detailedError( this, message, details ); else KMessageBox::error( this, message ); } return; } // hide messageWindow if string is empty if ( message.isEmpty() ) return d->messageWindow->hide(); // display message (duration is length dependant) if (duration==-1) { duration = 500 + 100 * message.length(); if ( !details.isEmpty() ) duration += 500 + 100 * details.length(); } d->messageWindow->display( message, details, icon, duration ); } void PageView::reparseConfig() { // set the scroll bars policies Qt::ScrollBarPolicy scrollBarMode = Okular::Settings::showScrollBars() ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff; if ( horizontalScrollBarPolicy() != scrollBarMode ) { setHorizontalScrollBarPolicy( scrollBarMode ); setVerticalScrollBarPolicy( scrollBarMode ); } if ( Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Summary && ( (int)Okular::Settings::viewColumns() != d->setting_viewCols ) ) { d->setting_viewCols = Okular::Settings::viewColumns(); slotRelayoutPages(); } if (Okular::Settings::rtlReadingDirection() != d->rtl_Mode ) { d->rtl_Mode = Okular::Settings::rtlReadingDirection(); slotRelayoutPages(); } updatePageStep(); if ( d->annotator ) { d->annotator->setEnabled( false ); d->annotator->reparseConfig(); if ( d->aToggleAnnotator->isChecked() ) slotToggleAnnotator( true ); } // Something like invert colors may have changed // As we don't have a way to find out the old value // We just update the viewport, this shouldn't be that bad // since it's just a repaint of pixmaps we already have viewport()->update(); } KActionCollection *PageView::actionCollection() const { return d->actionCollection; } QAction *PageView::toggleFormsAction() const { return d->aToggleForms; } int PageView::contentAreaWidth() const { return horizontalScrollBar()->maximum() + viewport()->width(); } int PageView::contentAreaHeight() const { return verticalScrollBar()->maximum() + viewport()->height(); } QPoint PageView::contentAreaPosition() const { return QPoint( horizontalScrollBar()->value(), verticalScrollBar()->value() ); } QPoint PageView::contentAreaPoint( const QPoint & pos ) const { return pos + contentAreaPosition(); } QPointF PageView::contentAreaPoint( const QPointF & pos ) const { return pos + contentAreaPosition(); } QString PageViewPrivate::selectedText() const { if ( pagesWithTextSelection.isEmpty() ) return QString(); QString text; QList< int > selpages = pagesWithTextSelection.toList(); qSort( selpages ); const Okular::Page * pg = nullptr; if ( selpages.count() == 1 ) { pg = document->page( selpages.first() ); text.append( pg->text( pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ) ); } else { pg = document->page( selpages.first() ); text.append( pg->text( pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ) ); int end = selpages.count() - 1; for( int i = 1; i < end; ++i ) { pg = document->page( selpages.at( i ) ); text.append( pg->text( nullptr, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ) ); } pg = document->page( selpages.last() ); text.append( pg->text( pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ) ); } return text; } void PageView::copyTextSelection() const { const QString text = d->selectedText(); if ( !text.isEmpty() ) { QClipboard *cb = QApplication::clipboard(); cb->setText( text, QClipboard::Clipboard ); } } void PageView::selectAll() { QVector< PageViewItem * >::const_iterator it = d->items.constBegin(), itEnd = d->items.constEnd(); for ( ; it < itEnd; ++it ) { Okular::RegularAreaRect * area = textSelectionForItem( *it ); d->pagesWithTextSelection.insert( (*it)->pageNumber() ); d->document->setPageTextSelection( (*it)->pageNumber(), area, palette().color( QPalette::Active, QPalette::Highlight ) ); } } +void PageView::createAnnotationsVideoWidgets(PageViewItem *item, const QLinkedList< Okular::Annotation * > &annotations) +{ + qDeleteAll( item->videoWidgets() ); + item->videoWidgets().clear(); + + QLinkedList< Okular::Annotation * >::const_iterator aIt = annotations.constBegin(), aEnd = annotations.constEnd(); + for ( ; aIt != aEnd; ++aIt ) + { + Okular::Annotation * a = *aIt; + if ( a->subType() == Okular::Annotation::AMovie ) + { + Okular::MovieAnnotation * movieAnn = static_cast< Okular::MovieAnnotation * >( a ); + VideoWidget * vw = new VideoWidget( movieAnn, movieAnn->movie(), d->document, viewport() ); + item->videoWidgets().insert( movieAnn->movie(), vw ); + vw->pageInitialized(); + } + else if ( a->subType() == Okular::Annotation::ARichMedia ) + { + Okular::RichMediaAnnotation * richMediaAnn = static_cast< Okular::RichMediaAnnotation * >( a ); + VideoWidget * vw = new VideoWidget( richMediaAnn, richMediaAnn->movie(), d->document, viewport() ); + item->videoWidgets().insert( richMediaAnn->movie(), vw ); + vw->pageInitialized(); + } + else if ( a->subType() == Okular::Annotation::AScreen ) + { + const Okular::ScreenAnnotation * screenAnn = static_cast< Okular::ScreenAnnotation * >( a ); + Okular::Movie *movie = GuiUtils::renditionMovieFromScreenAnnotation( screenAnn ); + if ( movie ) + { + VideoWidget * vw = new VideoWidget( screenAnn, movie, d->document, viewport() ); + item->videoWidgets().insert( movie, vw ); + vw->pageInitialized(); + } + } + } +} + //BEGIN DocumentObserver inherited methods void PageView::notifySetup( const QVector< Okular::Page * > & pageSet, int setupFlags ) { bool documentChanged = setupFlags & Okular::DocumentObserver::DocumentChanged; + const bool allownotes = d->document->isAllowed( Okular::AllowNotes ); + const bool allowfillforms = d->document->isAllowed( Okular::AllowFillForms ); + + // allownotes may have changed + if ( d->aToggleAnnotator ) + d->aToggleAnnotator->setEnabled( allownotes ); + // reuse current pages if nothing new if ( ( pageSet.count() == d->items.count() ) && !documentChanged && !( setupFlags & Okular::DocumentObserver::NewLayoutForPages ) ) { int count = pageSet.count(); for ( int i = 0; (i < count) && !documentChanged; i++ ) + { if ( (int)pageSet[i]->number() != d->items[i]->pageNumber() ) + { documentChanged = true; + } + else + { + // even if the document has not changed, allowfillforms may have + // changed, so update all fields' "canBeFilled" flag + foreach ( FormWidgetIface * w, d->items[i]->formWidgets() ) + w->setCanBeFilled( allowfillforms ); + } + } + if ( !documentChanged ) + { + if ( setupFlags & Okular::DocumentObserver::UrlChanged ) + { + // Here with UrlChanged and no document changed it means we + // need to update all the Annotation* and Form* otherwise + // they still point to the old document ones, luckily the old ones are still + // around so we can look for the new ones using unique ids, etc + d->mouseAnnotation->updateAnnotationPointers(); + + foreach(AnnotWindow *aw, d->m_annowindows) + { + Okular::Annotation *newA = d->document->page( aw->pageNumber() )->annotation( aw->annotation()->uniqueName() ); + aw->updateAnnotation( newA ); + } + + const QRect viewportRect( horizontalScrollBar()->value(), verticalScrollBar()->value(), + viewport()->width(), viewport()->height() ); + for ( int i = 0; i < count; i++ ) + { + PageViewItem *item = d->items[i]; + const QSet fws = item->formWidgets(); + foreach ( FormWidgetIface * w, fws ) + { + Okular::FormField *f = Okular::PagePrivate::findEquivalentForm( d->document->page( i ), w->formField() ); + if (f) + { + w->setFormField( f ); + } + else + { + qWarning() << "Lost form field on document save, something is wrong"; + item->formWidgets().remove(w); + delete w; + } + } + + // For the video widgets we don't really care about reusing them since they don't contain much info so just + // create them again + createAnnotationsVideoWidgets( item, pageSet[i]->annotations() ); + Q_FOREACH ( VideoWidget *vw, item->videoWidgets() ) + { + const Okular::NormalizedRect r = vw->normGeometry(); + vw->setGeometry( + qRound( item->uncroppedGeometry().left() + item->uncroppedWidth() * r.left ) + 1 - viewportRect.left(), + qRound( item->uncroppedGeometry().top() + item->uncroppedHeight() * r.top ) + 1 - viewportRect.top(), + qRound( fabs( r.right - r.left ) * item->uncroppedGeometry().width() ), + qRound( fabs( r.bottom - r.top ) * item->uncroppedGeometry().height() ) ); + + // Workaround, otherwise the size somehow gets lost + vw->show(); + vw->hide(); + } + + } + } + return; + } } // mouseAnnotation must not access our PageViewItem widgets any longer d->mouseAnnotation->reset(); // delete all widgets (one for each page in pageSet) QVector< PageViewItem * >::const_iterator dIt = d->items.constBegin(), dEnd = d->items.constEnd(); for ( ; dIt != dEnd; ++dIt ) delete *dIt; d->items.clear(); d->visibleItems.clear(); d->pagesWithTextSelection.clear(); toggleFormWidgets( false ); if ( d->formsWidgetController ) d->formsWidgetController->dropRadioButtons(); bool haspages = !pageSet.isEmpty(); bool hasformwidgets = false; // create children widgets QVector< Okular::Page * >::const_iterator setIt = pageSet.constBegin(), setEnd = pageSet.constEnd(); for ( ; setIt != setEnd; ++setIt ) { PageViewItem * item = new PageViewItem( *setIt ); d->items.push_back( item ); #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug).nospace() << "cropped geom for " << d->items.last()->pageNumber() << " is " << d->items.last()->croppedGeometry(); #endif const QLinkedList< Okular::FormField * > pageFields = (*setIt)->formFields(); QLinkedList< Okular::FormField * >::const_iterator ffIt = pageFields.constBegin(), ffEnd = pageFields.constEnd(); for ( ; ffIt != ffEnd; ++ffIt ) { Okular::FormField * ff = *ffIt; FormWidgetIface * w = FormWidgetFactory::createWidget( ff, viewport() ); if ( w ) { w->setPageItem( item ); w->setFormWidgetsController( d->formWidgetsController() ); w->setVisibility( false ); - w->setCanBeFilled( d->document->isAllowed( Okular::AllowFillForms ) ); - item->formWidgets().insert( ff->id(), w ); + w->setCanBeFilled( allowfillforms ); + item->formWidgets().insert( w ); hasformwidgets = true; } } - const QLinkedList< Okular::Annotation * > annotations = (*setIt)->annotations(); - QLinkedList< Okular::Annotation * >::const_iterator aIt = annotations.constBegin(), aEnd = annotations.constEnd(); - for ( ; aIt != aEnd; ++aIt ) - { - Okular::Annotation * a = *aIt; - if ( a->subType() == Okular::Annotation::AMovie ) - { - Okular::MovieAnnotation * movieAnn = static_cast< Okular::MovieAnnotation * >( a ); - VideoWidget * vw = new VideoWidget( movieAnn, movieAnn->movie(), d->document, viewport() ); - item->videoWidgets().insert( movieAnn->movie(), vw ); - vw->pageInitialized(); - } - else if ( a->subType() == Okular::Annotation::ARichMedia ) - { - Okular::RichMediaAnnotation * richMediaAnn = static_cast< Okular::RichMediaAnnotation * >( a ); - VideoWidget * vw = new VideoWidget( richMediaAnn, richMediaAnn->movie(), d->document, viewport() ); - item->videoWidgets().insert( richMediaAnn->movie(), vw ); - vw->pageInitialized(); - } - else if ( a->subType() == Okular::Annotation::AScreen ) - { - const Okular::ScreenAnnotation * screenAnn = static_cast< Okular::ScreenAnnotation * >( a ); - Okular::Movie *movie = GuiUtils::renditionMovieFromScreenAnnotation( screenAnn ); - if ( movie ) - { - VideoWidget * vw = new VideoWidget( screenAnn, movie, d->document, viewport() ); - item->videoWidgets().insert( movie, vw ); - vw->pageInitialized(); - } - } - } + + createAnnotationsVideoWidgets( item, (*setIt)->annotations() ); } // invalidate layout so relayout/repaint will happen on next viewport change if ( haspages ) { // We do a delayed call to slotRelayoutPages but also set the dirtyLayout // because we might end up in notifyViewportChanged while slotRelayoutPages // has not been done and we don't want that to happen d->dirtyLayout = true; QMetaObject::invokeMethod(this, "slotRelayoutPages", Qt::QueuedConnection); } else { // update the mouse cursor when closing because we may have close through a link and // want the cursor to come back to the normal cursor updateCursor(); // then, make the message window and scrollbars disappear, and trigger a repaint d->messageWindow->hide(); resizeContentArea( QSize( 0,0 ) ); viewport()->update(); // when there is no change to the scrollbars, no repaint would // be done and the old document would still be shown } // OSD to display pages if ( documentChanged && pageSet.count() > 0 && Okular::Settings::showOSD() ) d->messageWindow->display( i18np(" Loaded a one-page document.", " Loaded a %1-page document.", pageSet.count() ), QString(), PageViewMessage::Info, 4000 ); updateActionState( haspages, documentChanged, hasformwidgets ); // We need to assign it to a different list otherwise slotAnnotationWindowDestroyed // will bite us and clear d->m_annowindows - QHash< Okular::Annotation *, AnnotWindow * > annowindows = d->m_annowindows; + QSet< AnnotWindow * > annowindows = d->m_annowindows; d->m_annowindows.clear(); qDeleteAll( annowindows ); selectionClear(); } void PageView::updateActionState( bool haspages, bool documentChanged, bool hasformwidgets ) { if ( d->aPageSizes ) { // may be null if dummy mode is on bool pageSizes = d->document->supportsPageSizes(); d->aPageSizes->setEnabled( pageSizes ); // set the new page sizes: // - if the generator supports them // - if the document changed if ( pageSizes && documentChanged ) { QStringList items; foreach ( const Okular::PageSize &p, d->document->pageSizes() ) items.append( p.name() ); d->aPageSizes->setItems( items ); } } if ( d->aTrimMargins ) d->aTrimMargins->setEnabled( haspages ); if ( d->aTrimToSelection ) d->aTrimToSelection->setEnabled( haspages ); if ( d->aViewMode ) d->aViewMode->setEnabled( haspages ); if ( d->aViewContinuous ) d->aViewContinuous->setEnabled( haspages ); if ( d->aZoomFitWidth ) d->aZoomFitWidth->setEnabled( haspages ); if ( d->aZoomFitPage ) d->aZoomFitPage->setEnabled( haspages ); if ( d->aZoomAutoFit ) d->aZoomAutoFit->setEnabled( haspages ); if ( d->aZoom ) { d->aZoom->selectableActionGroup()->setEnabled( haspages ); d->aZoom->setEnabled( haspages ); } if ( d->aZoomIn ) d->aZoomIn->setEnabled( haspages ); if ( d->aZoomOut ) d->aZoomOut->setEnabled( haspages ); if ( d->mouseModeActionGroup ) d->mouseModeActionGroup->setEnabled( haspages ); if ( d->aRotateClockwise ) d->aRotateClockwise->setEnabled( haspages ); if ( d->aRotateCounterClockwise ) d->aRotateCounterClockwise->setEnabled( haspages ); if ( d->aRotateOriginal ) d->aRotateOriginal->setEnabled( haspages ); if ( d->aToggleForms ) { // may be null if dummy mode is on d->aToggleForms->setEnabled( haspages && hasformwidgets ); } bool allowAnnotations = d->document->isAllowed( Okular::AllowNotes ); if ( d->annotator ) { bool allowTools = haspages && allowAnnotations; d->annotator->setToolsEnabled( allowTools ); d->annotator->setTextToolsEnabled( allowTools && d->document->supportsSearching() ); } if ( d->aToggleAnnotator ) { if ( !allowAnnotations && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); } d->aToggleAnnotator->setEnabled( allowAnnotations ); } #ifdef HAVE_SPEECH if ( d->aSpeakDoc ) { const bool enablettsactions = haspages ? Okular::Settings::useTTS() : false; d->aSpeakDoc->setEnabled( enablettsactions ); d->aSpeakPage->setEnabled( enablettsactions ); } #endif if (d->aMouseMagnifier) d->aMouseMagnifier->setEnabled(d->document->supportsTiles()); if ( d->aFitWindowToPage ) d->aFitWindowToPage->setEnabled( haspages && !Okular::Settings::viewContinuous() ); } bool PageView::areSourceLocationsShownGraphically() const { return Okular::Settings::showSourceLocationsGraphically(); } void PageView::setShowSourceLocationsGraphically(bool show) { if( show == Okular::Settings::showSourceLocationsGraphically() ) { return; } Okular::Settings::setShowSourceLocationsGraphically( show ); viewport()->update(); } void PageView::setLastSourceLocationViewport( const Okular::DocumentViewport& vp ) { if( vp.rePos.enabled ) { d->lastSourceLocationViewportNormalizedX = normClamp( vp.rePos.normalizedX, 0.5 ); d->lastSourceLocationViewportNormalizedY = normClamp( vp.rePos.normalizedY, 0.0 ); } else { d->lastSourceLocationViewportNormalizedX = 0.5; d->lastSourceLocationViewportNormalizedY = 0.0; } d->lastSourceLocationViewportPageNumber = vp.pageNumber; viewport()->update(); } void PageView::clearLastSourceLocationViewport() { d->lastSourceLocationViewportPageNumber = -1; d->lastSourceLocationViewportNormalizedX = 0.0; d->lastSourceLocationViewportNormalizedY = 0.0; viewport()->update(); } void PageView::notifyViewportChanged( bool smoothMove ) { QMetaObject::invokeMethod(this, "slotRealNotifyViewportChanged", Qt::QueuedConnection, Q_ARG( bool, smoothMove )); } void PageView::slotRealNotifyViewportChanged( bool smoothMove ) { // if we are the one changing viewport, skip this nofity if ( d->blockViewport ) return; // block setViewport outgoing calls d->blockViewport = true; // find PageViewItem matching the viewport description const Okular::DocumentViewport & vp = d->document->viewport(); PageViewItem * item = nullptr; QVector< PageViewItem * >::const_iterator iIt = d->items.constBegin(), iEnd = d->items.constEnd(); for ( ; iIt != iEnd; ++iIt ) if ( (*iIt)->pageNumber() == vp.pageNumber ) { item = *iIt; break; } if ( !item ) { qCWarning(OkularUiDebug) << "viewport for page" << vp.pageNumber << "has no matching item!"; d->blockViewport = false; return; } #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << "document viewport changed"; #endif // relayout in "Single Pages" mode or if a relayout is pending d->blockPixmapsRequest = true; if ( !Okular::Settings::viewContinuous() || d->dirtyLayout ) slotRelayoutPages(); // restore viewport center or use default {x-center,v-top} alignment const QRect & r = item->croppedGeometry(); int newCenterX = r.left(), newCenterY = r.top(); if ( vp.rePos.enabled ) { if ( vp.rePos.pos == Okular::DocumentViewport::Center ) { newCenterX += (int)( normClamp( vp.rePos.normalizedX, 0.5 ) * (double)r.width() ); newCenterY += (int)( normClamp( vp.rePos.normalizedY, 0.0 ) * (double)r.height() ); } else { // TopLeft newCenterX += (int)( normClamp( vp.rePos.normalizedX, 0.0 ) * (double)r.width() + viewport()->width() / 2 ); newCenterY += (int)( normClamp( vp.rePos.normalizedY, 0.0 ) * (double)r.height() + viewport()->height() / 2 ); } } else { newCenterX += r.width() / 2; newCenterY += viewport()->height() / 2 - 10; } // if smooth movement requested, setup parameters and start it if ( smoothMove ) { d->viewportMoveActive = true; d->viewportMoveTime.start(); d->viewportMoveDest.setX( newCenterX ); d->viewportMoveDest.setY( newCenterY ); if ( !d->viewportMoveTimer ) { d->viewportMoveTimer = new QTimer( this ); connect( d->viewportMoveTimer, &QTimer::timeout, this, &PageView::slotMoveViewport ); } d->viewportMoveTimer->start( 25 ); verticalScrollBar()->setEnabled( false ); horizontalScrollBar()->setEnabled( false ); } else center( newCenterX, newCenterY ); d->blockPixmapsRequest = false; // request visible pixmaps in the current viewport and recompute it slotRequestVisiblePixmaps(); // enable setViewport calls d->blockViewport = false; if( viewport() ) { viewport()->update(); } // since the page has moved below cursor, update it updateCursor(); } void PageView::notifyPageChanged( int pageNumber, int changedFlags ) { // only handle pixmap / highlight changes notifies if ( changedFlags & DocumentObserver::Bookmark ) return; if ( changedFlags & DocumentObserver::Annotations ) { const QLinkedList< Okular::Annotation * > annots = d->document->page( pageNumber )->annotations(); const QLinkedList< Okular::Annotation * >::ConstIterator annItEnd = annots.end(); - QHash< Okular::Annotation*, AnnotWindow * >::Iterator it = d->m_annowindows.begin(); + QSet< AnnotWindow * >::Iterator it = d->m_annowindows.begin(); for ( ; it != d->m_annowindows.end(); ) { - QLinkedList< Okular::Annotation * >::ConstIterator annIt = qFind( annots, it.key() ); + QLinkedList< Okular::Annotation * >::ConstIterator annIt = qFind( annots, (*it)->annotation() ); if ( annIt != annItEnd ) { (*it)->reloadInfo(); ++it; } else { AnnotWindow *w = *it; it = d->m_annowindows.erase( it ); // Need to delete after removing from the list // otherwise deleting will call slotAnnotationWindowDestroyed which will mess // the list and the iterators delete w; } } QLinkedList< Okular::Annotation * >::ConstIterator annIt = qFind( annots, d->mouseAnnotation->annotation() ); if ( annIt == annItEnd ) { d->mouseAnnotation->cancel(); } } if ( changedFlags & DocumentObserver::BoundingBox ) { #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << "BoundingBox change on page" << pageNumber; #endif slotRelayoutPages(); slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already! // Repaint the whole widget since layout may have changed viewport()->update(); return; } // iterate over visible items: if page(pageNumber) is one of them, repaint it QLinkedList< PageViewItem * >::const_iterator iIt = d->visibleItems.constBegin(), iEnd = d->visibleItems.constEnd(); for ( ; iIt != iEnd; ++iIt ) if ( (*iIt)->pageNumber() == pageNumber && (*iIt)->isVisible() ) { // update item's rectangle plus the little outline QRect expandedRect = (*iIt)->croppedGeometry(); // a PageViewItem is placed in the global page layout, // while we need to map its position in the viewport coordinates // (to get the correct area to repaint) expandedRect.translate( -contentAreaPosition() ); expandedRect.adjust( -1, -1, 3, 3 ); viewport()->update( expandedRect ); // if we were "zoom-dragging" do not overwrite the "zoom-drag" cursor if ( cursor().shape() != Qt::SizeVerCursor ) { // since the page has been regenerated below cursor, update it updateCursor(); } break; } } void PageView::notifyContentsCleared( int changedFlags ) { // if pixmaps were cleared, re-ask them if ( changedFlags & DocumentObserver::Pixmap ) QMetaObject::invokeMethod(this, "slotRequestVisiblePixmaps", Qt::QueuedConnection); } void PageView::notifyZoom( int factor ) { if ( factor > 0 ) updateZoom( ZoomIn ); else updateZoom( ZoomOut ); } bool PageView::canUnloadPixmap( int pageNumber ) const { if ( Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Low || Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Normal ) { // if the item is visible, forbid unloading QLinkedList< PageViewItem * >::const_iterator vIt = d->visibleItems.constBegin(), vEnd = d->visibleItems.constEnd(); for ( ; vIt != vEnd; ++vIt ) if ( (*vIt)->pageNumber() == pageNumber ) return false; } else { // forbid unloading of the visible items, and of the previous and next QLinkedList< PageViewItem * >::const_iterator vIt = d->visibleItems.constBegin(), vEnd = d->visibleItems.constEnd(); for ( ; vIt != vEnd; ++vIt ) if ( abs( (*vIt)->pageNumber() - pageNumber ) <= 1 ) return false; } // if hidden premit unloading return true; } void PageView::notifyCurrentPageChanged( int previous, int current ) { if ( previous != -1 ) { PageViewItem * item = d->items.at( previous ); if ( item ) { Q_FOREACH ( VideoWidget *videoWidget, item->videoWidgets() ) videoWidget->pageLeft(); } } if ( current != -1 ) { PageViewItem * item = d->items.at( current ); if ( item ) { Q_FOREACH ( VideoWidget *videoWidget, item->videoWidgets() ) videoWidget->pageEntered(); } // update zoom text and factor if in a ZoomFit/* zoom mode if ( d->zoomMode != ZoomFixed ) updateZoomText(); } } //END DocumentObserver inherited methods //BEGIN View inherited methods bool PageView::supportsCapability( ViewCapability capability ) const { switch ( capability ) { case Zoom: case ZoomModality: return true; } return false; } Okular::View::CapabilityFlags PageView::capabilityFlags( ViewCapability capability ) const { switch ( capability ) { case Zoom: case ZoomModality: return CapabilityRead | CapabilityWrite | CapabilitySerializable; } return nullptr; } QVariant PageView::capability( ViewCapability capability ) const { switch ( capability ) { case Zoom: return d->zoomFactor; case ZoomModality: return d->zoomMode; } return QVariant(); } void PageView::setCapability( ViewCapability capability, const QVariant &option ) { switch ( capability ) { case Zoom: { bool ok = true; double factor = option.toDouble( &ok ); if ( ok && factor > 0.0 ) { d->zoomFactor = static_cast< float >( factor ); updateZoom( ZoomRefreshCurrent ); } break; } case ZoomModality: { bool ok = true; int mode = option.toInt( &ok ); if ( ok ) { if ( mode >= 0 && mode < 3 ) updateZoom( (ZoomMode)mode ); } break; } } } //END View inherited methods //BEGIN widget events bool PageView::event( QEvent * event ) { if ( event->type() == QEvent::Gesture ) { return gestureEvent(static_cast( event )); } // do not stop the event return QAbstractScrollArea::event( event ); } bool PageView::gestureEvent( QGestureEvent * event ) { QPinchGesture *pinch = static_cast(event->gesture(Qt::PinchGesture)); if (pinch) { // Viewport zoom level at the moment where the pinch gesture starts. // The viewport zoom level _during_ the gesture will be this value // times the relative zoom reported by QGestureEvent. static qreal vanillaZoom = d->zoomFactor; if (pinch->state() == Qt::GestureStarted) { vanillaZoom = d->zoomFactor; } const QPinchGesture::ChangeFlags changeFlags = pinch->changeFlags(); // Zoom if (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged) { d->zoomFactor = vanillaZoom * pinch->totalScaleFactor(); d->blockPixmapsRequest = true; updateZoom( ZoomRefreshCurrent ); d->blockPixmapsRequest = false; viewport()->repaint(); } // Count the number of 90-degree rotations we did since the start of the pinch gesture. // Otherwise a pinch turned to 90 degrees and held there will rotate the page again and again. static int rotations = 0; if (changeFlags & QPinchGesture::RotationAngleChanged) { // Rotation angle relative to the accumulated page rotations triggered by the current pinch // We actually turn at 80 degrees rather than at 90 degrees. That's less strain on the hands. const qreal relativeAngle = pinch->rotationAngle() - rotations*90; if (relativeAngle > 80) { slotRotateClockwise(); rotations++; } if (relativeAngle < -80) { slotRotateCounterClockwise(); rotations--; } } if (pinch->state() == Qt::GestureFinished) { rotations = 0; } return true; } return false; } void PageView::paintEvent(QPaintEvent *pe) { const QPoint areaPos = contentAreaPosition(); // create the rect into contents from the clipped screen rect QRect viewportRect = viewport()->rect(); viewportRect.translate( areaPos ); QRect contentsRect = pe->rect().translated( areaPos ).intersected( viewportRect ); if ( !contentsRect.isValid() ) return; #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << "paintevent" << contentsRect; #endif // create the screen painter. a pixel painted at contentsX,contentsY // appears to the top-left corner of the scrollview. QPainter screenPainter( viewport() ); // translate to simulate the scrolled content widget screenPainter.translate( -areaPos ); // selectionRect is the normalized mouse selection rect QRect selectionRect = d->mouseSelectionRect; if ( !selectionRect.isNull() ) selectionRect = selectionRect.normalized(); // selectionRectInternal without the border QRect selectionRectInternal = selectionRect; selectionRectInternal.adjust( 1, 1, -1, -1 ); // color for blending QColor selBlendColor = (selectionRect.width() > 8 || selectionRect.height() > 8) ? d->mouseSelectionColor : Qt::red; // subdivide region into rects const QVector &allRects = pe->region().rects(); uint numRects = allRects.count(); // preprocess rects area to see if it worths or not using subdivision uint summedArea = 0; for ( uint i = 0; i < numRects; i++ ) { const QRect & r = allRects[i]; summedArea += r.width() * r.height(); } // very elementary check: SUMj(Region[j].area) is less than boundingRect.area bool useSubdivision = summedArea < (0.6 * contentsRect.width() * contentsRect.height()); if ( !useSubdivision ) numRects = 1; // iterate over the rects (only one loop if not using subdivision) for ( uint i = 0; i < numRects; i++ ) { if ( useSubdivision ) { // set 'contentsRect' to a part of the sub-divided region contentsRect = allRects[i].translated( areaPos ).intersected( viewportRect ); if ( !contentsRect.isValid() ) continue; } #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << contentsRect; #endif // note: this check will take care of all things requiring alpha blending (not only selection) bool wantCompositing = !selectionRect.isNull() && contentsRect.intersects( selectionRect ); // also alpha-blend when there is a table selection... wantCompositing |= !d->tableSelectionParts.isEmpty(); if ( wantCompositing && Okular::Settings::enableCompositing() ) { // create pixmap and open a painter over it (contents{left,top} becomes pixmap {0,0}) QPixmap doubleBuffer( contentsRect.size() * devicePixelRatioF() ); doubleBuffer.setDevicePixelRatio(devicePixelRatioF()); QPainter pixmapPainter( &doubleBuffer ); pixmapPainter.translate( -contentsRect.left(), -contentsRect.top() ); // 1) Layer 0: paint items and clear bg on unpainted rects drawDocumentOnPainter( contentsRect, &pixmapPainter ); // 2a) Layer 1a: paint (blend) transparent selection (rectangle) if ( !selectionRect.isNull() && selectionRect.intersects( contentsRect ) && !selectionRectInternal.contains( contentsRect ) ) { QRect blendRect = selectionRectInternal.intersected( contentsRect ); // skip rectangles covered by the selection's border if ( blendRect.isValid() ) { // grab current pixmap into a new one to colorize contents QPixmap blendedPixmap( blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF() ); blendedPixmap.setDevicePixelRatio(devicePixelRatioF()); QPainter p( &blendedPixmap ); p.drawPixmap( 0, 0, doubleBuffer, blendRect.left() - contentsRect.left(), blendRect.top() - contentsRect.top(), blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF() ); QColor blCol = selBlendColor.dark( 140 ); blCol.setAlphaF( 0.2 ); p.fillRect( blendedPixmap.rect(), blCol ); p.end(); // copy the blended pixmap back to its place pixmapPainter.drawPixmap( blendRect.left(), blendRect.top(), blendedPixmap ); } // draw border (red if the selection is too small) pixmapPainter.setPen( selBlendColor ); pixmapPainter.drawRect( selectionRect.adjusted( 0, 0, -1, -1 ) ); } // 2b) Layer 1b: paint (blend) transparent selection (table) foreach (const TableSelectionPart &tsp, d->tableSelectionParts) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); QRect selectionPartRectInternal = selectionPartRect; selectionPartRectInternal.adjust( 1, 1, -1, -1 ); if ( !selectionPartRect.isNull() && selectionPartRect.intersects( contentsRect ) && !selectionPartRectInternal.contains( contentsRect ) ) { QRect blendRect = selectionPartRectInternal.intersected( contentsRect ); // skip rectangles covered by the selection's border if ( blendRect.isValid() ) { // grab current pixmap into a new one to colorize contents QPixmap blendedPixmap( blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF() ); blendedPixmap.setDevicePixelRatio(devicePixelRatioF()); QPainter p( &blendedPixmap ); p.drawPixmap( 0, 0, doubleBuffer, blendRect.left() - contentsRect.left(), blendRect.top() - contentsRect.top(), blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF() ); QColor blCol = d->mouseSelectionColor.dark( 140 ); blCol.setAlphaF( 0.2 ); p.fillRect( blendedPixmap.rect(), blCol ); p.end(); // copy the blended pixmap back to its place pixmapPainter.drawPixmap( blendRect.left(), blendRect.top(), blendedPixmap ); } // draw border (red if the selection is too small) pixmapPainter.setPen( d->mouseSelectionColor ); pixmapPainter.drawRect( selectionPartRect.adjusted( 0, 0, -1, -1 ) ); } } drawTableDividers( &pixmapPainter ); // 3a) Layer 1: give annotator painting control if ( d->annotator && d->annotator->routePaints( contentsRect ) ) d->annotator->routePaint( &pixmapPainter, contentsRect ); // 3b) Layer 1: give mouseAnnotation painting control d->mouseAnnotation->routePaint( &pixmapPainter, contentsRect ); // 4) Layer 2: overlays if ( Okular::Settings::debugDrawBoundaries() ) { pixmapPainter.setPen( Qt::blue ); pixmapPainter.drawRect( contentsRect ); } // finish painting and draw contents pixmapPainter.end(); screenPainter.drawPixmap( contentsRect.left(), contentsRect.top(), doubleBuffer ); } else { // 1) Layer 0: paint items and clear bg on unpainted rects drawDocumentOnPainter( contentsRect, &screenPainter ); // 2a) Layer 1a: paint opaque selection (rectangle) if ( !selectionRect.isNull() && selectionRect.intersects( contentsRect ) && !selectionRectInternal.contains( contentsRect ) ) { screenPainter.setPen( palette().color( QPalette::Active, QPalette::Highlight ).dark(110) ); screenPainter.drawRect( selectionRect ); } // 2b) Layer 1b: paint opaque selection (table) foreach (const TableSelectionPart &tsp, d->tableSelectionParts) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); QRect selectionPartRectInternal = selectionPartRect; selectionPartRectInternal.adjust( 1, 1, -1, -1 ); if ( !selectionPartRect.isNull() && selectionPartRect.intersects( contentsRect ) && !selectionPartRectInternal.contains( contentsRect ) ) { screenPainter.setPen( palette().color( QPalette::Active, QPalette::Highlight ).dark(110) ); screenPainter.drawRect( selectionPartRect ); } } drawTableDividers( &screenPainter ); // 3a) Layer 1: give annotator painting control if ( d->annotator && d->annotator->routePaints( contentsRect ) ) d->annotator->routePaint( &screenPainter, contentsRect ); // 3b) Layer 1: give mouseAnnotation painting control d->mouseAnnotation->routePaint( &screenPainter, contentsRect ); // 4) Layer 2: overlays if ( Okular::Settings::debugDrawBoundaries() ) { screenPainter.setPen( Qt::red ); screenPainter.drawRect( contentsRect ); } } } } void PageView::drawTableDividers(QPainter * screenPainter) { if (!d->tableSelectionParts.isEmpty()) { screenPainter->setPen( d->mouseSelectionColor.dark() ); if (d->tableDividersGuessed) { QPen p = screenPainter->pen(); p.setStyle( Qt::DashLine ); screenPainter->setPen( p ); } foreach (const TableSelectionPart &tsp, d->tableSelectionParts) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); QRect selectionPartRectInternal = selectionPartRect; selectionPartRectInternal.adjust( 1, 1, -1, -1 ); foreach(double col, d->tableSelectionCols) { if (col >= tsp.rectInSelection.left && col <= tsp.rectInSelection.right) { col = (col - tsp.rectInSelection.left) / (tsp.rectInSelection.right - tsp.rectInSelection.left); const int x = selectionPartRect.left() + col * selectionPartRect.width() + 0.5; screenPainter->drawLine( x, selectionPartRectInternal.top(), x, selectionPartRectInternal.top() + selectionPartRectInternal.height() ); } } foreach(double row, d->tableSelectionRows) { if (row >= tsp.rectInSelection.top && row <= tsp.rectInSelection.bottom) { row = (row - tsp.rectInSelection.top) / (tsp.rectInSelection.bottom - tsp.rectInSelection.top); const int y = selectionPartRect.top() + row * selectionPartRect.height() + 0.5; screenPainter->drawLine( selectionPartRectInternal.left(), y, selectionPartRectInternal.left() + selectionPartRectInternal.width(), y ); } } } } } void PageView::resizeEvent( QResizeEvent *e ) { if ( d->items.isEmpty() ) { resizeContentArea( e->size() ); return; } if ( ( d->zoomMode == ZoomFitWidth || d->zoomMode == ZoomFitAuto ) && !verticalScrollBar()->isVisible() && qAbs(e->oldSize().height() - e->size().height()) < verticalScrollBar()->width() && d->verticalScrollBarVisible ) { // this saves us from infinite resizing loop because of scrollbars appearing and disappearing // see bug 160628 for more info // TODO looks are still a bit ugly because things are left uncentered // but better a bit ugly than unusable d->verticalScrollBarVisible = false; resizeContentArea( e->size() ); return; } else if ( d->zoomMode == ZoomFitAuto && !horizontalScrollBar()->isVisible() && qAbs(e->oldSize().width() - e->size().width()) < horizontalScrollBar()->height() && d->horizontalScrollBarVisible ) { // this saves us from infinite resizing loop because of scrollbars appearing and disappearing // TODO looks are still a bit ugly because things are left uncentered // but better a bit ugly than unusable d->horizontalScrollBarVisible = false; resizeContentArea( e->size() ); return; } // start a timer that will refresh the pixmap after 0.2s d->delayResizeEventTimer->start( 200 ); d->verticalScrollBarVisible = verticalScrollBar()->isVisible(); d->horizontalScrollBarVisible = horizontalScrollBar()->isVisible(); } void PageView::keyPressEvent( QKeyEvent * e ) { e->accept(); // if performing a selection or dyn zooming, disable keys handling if ( ( d->mouseSelecting && e->key() != Qt::Key_Escape ) || ( QApplication::mouseButtons () & Qt::MidButton ) ) return; // if viewport is moving, disable keys handling if ( d->viewportMoveActive ) return; // move/scroll page by using keys switch ( e->key() ) { case Qt::Key_J: case Qt::Key_K: case Qt::Key_Down: case Qt::Key_PageDown: case Qt::Key_Up: case Qt::Key_PageUp: case Qt::Key_Backspace: if ( e->key() == Qt::Key_Down || e->key() == Qt::Key_PageDown || e->key() == Qt::Key_J ) { bool singleStep = e->key() == Qt::Key_Down || e->key() == Qt::Key_J; slotScrollDown( singleStep ); } else { bool singleStep = e->key() == Qt::Key_Up || e->key() == Qt::Key_K; slotScrollUp( singleStep ); } break; case Qt::Key_Left: case Qt::Key_H: if ( horizontalScrollBar()->maximum() == 0 ) { //if we cannot scroll we go to the previous page vertically int next_page = d->document->currentPage() - viewColumns(); d->document->setViewportPage(next_page); } else horizontalScrollBar()->triggerAction( QScrollBar::SliderSingleStepSub ); break; case Qt::Key_Right: case Qt::Key_L: if ( horizontalScrollBar()->maximum() == 0 ) { //if we cannot scroll we advance the page vertically int next_page = d->document->currentPage() + viewColumns(); d->document->setViewportPage(next_page); } else horizontalScrollBar()->triggerAction( QScrollBar::SliderSingleStepAdd ); break; case Qt::Key_Escape: emit escPressed(); selectionClear( d->tableDividersGuessed ? ClearOnlyDividers : ClearAllSelection ); d->mousePressPos = QPoint(); if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } d->mouseAnnotation->routeKeyPressEvent( e ); break; case Qt::Key_Delete: d->mouseAnnotation->routeKeyPressEvent( e ); break; case Qt::Key_Shift: case Qt::Key_Control: if ( d->autoScrollTimer ) { if ( d->autoScrollTimer->isActive() ) d->autoScrollTimer->stop(); else slotAutoScroll(); return; } // fallthrough default: e->ignore(); return; } // if a known key has been pressed, stop scrolling the page if ( d->autoScrollTimer ) { d->scrollIncrement = 0; d->autoScrollTimer->stop(); } } void PageView::keyReleaseEvent( QKeyEvent * e ) { e->accept(); if ( d->annotator && d->annotator->active() ) { if ( d->annotator->routeKeyEvent( e ) ) return; } if ( e->key() == Qt::Key_Escape && d->autoScrollTimer ) { d->scrollIncrement = 0; d->autoScrollTimer->stop(); } } void PageView::inputMethodEvent( QInputMethodEvent * e ) { Q_UNUSED(e) } void PageView::tabletEvent( QTabletEvent * e ) { // Ignore tablet events that we don't care about if ( !( e->type() == QEvent::TabletPress || e->type() == QEvent::TabletRelease || e->type() == QEvent::TabletMove ) ) { e->ignore(); return; } // Determine pen state bool penReleased = false; if ( e->type() == QEvent::TabletPress ) { d->penDown = true; } if ( e->type() == QEvent::TabletRelease ) { d->penDown = false; penReleased = true; } // If we're editing an annotation and the tablet pen is either down or just released // then dispatch event to annotator if ( d->annotator && d->annotator->active() && ( d->penDown || penReleased ) ) { const QPoint eventPos = contentAreaPoint( e->pos() ); PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); const QPoint localOriginInGlobal = mapToGlobal( QPoint(0,0) ); // routeTabletEvent will accept or ignore event as appropriate d->annotator->routeTabletEvent( e, pageItem, localOriginInGlobal ); } else { e->ignore(); } } void PageView::mouseMoveEvent( QMouseEvent * e ) { d->controlWheelAccumulatedDelta = 0; // don't perform any mouse action when no document is shown if ( d->items.isEmpty() ) return; // don't perform any mouse action when viewport is autoscrolling if ( d->viewportMoveActive ) return; // if holding mouse mid button, perform zoom if ( e->buttons() & Qt::MidButton ) { int mouseY = e->globalPos().y(); int deltaY = d->mouseMidLastY - mouseY; // wrap mouse from top to bottom const QRect mouseContainer = QApplication::desktop()->screenGeometry( this ); const int absDeltaY = abs(deltaY); if ( absDeltaY > mouseContainer.height() / 2 ) { deltaY = mouseContainer.height() - absDeltaY; } const float upperZoomLimit = d->document->supportsTiles() ? 15.99 : 3.99; if ( mouseY <= mouseContainer.top() + 4 && d->zoomFactor < upperZoomLimit ) { mouseY = mouseContainer.bottom() - 5; QCursor::setPos( e->globalPos().x(), mouseY ); } // wrap mouse from bottom to top else if ( mouseY >= mouseContainer.bottom() - 4 && d->zoomFactor > 0.101 ) { mouseY = mouseContainer.top() + 5; QCursor::setPos( e->globalPos().x(), mouseY ); } // remember last position d->mouseMidLastY = mouseY; // update zoom level, perform zoom and redraw if ( deltaY ) { d->zoomFactor *= ( 1.0 + ( (double)deltaY / 500.0 ) ); d->blockPixmapsRequest = true; updateZoom( ZoomRefreshCurrent ); d->blockPixmapsRequest = false; viewport()->repaint(); } return; } const QPoint eventPos = contentAreaPoint( e->pos() ); // if we're editing an annotation, dispatch event to it if ( d->annotator && d->annotator->active() ) { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); updateCursor( eventPos ); d->annotator->routeMouseEvent( e, pageItem ); return; } bool leftButton = (e->buttons() == Qt::LeftButton); bool rightButton = (e->buttons() == Qt::RightButton); switch ( d->mouseMode ) { case Okular::Settings::EnumMouseMode::Browse: { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); if ( leftButton ) { d->leftClickTimer.stop(); if ( pageItem && d->mouseAnnotation->isActive() ) { // if left button pressed and annotation is focused, forward move event d->mouseAnnotation->routeMouseMoveEvent( pageItem, eventPos, leftButton ); } // drag page else if ( !d->mouseGrabPos.isNull() ) { setCursor( Qt::ClosedHandCursor ); QPoint mousePos = e->globalPos(); QPoint delta = d->mouseGrabPos - mousePos; // wrap mouse from top to bottom const QRect mouseContainer = QApplication::desktop()->screenGeometry( this ); // If the delta is huge it probably means we just wrapped in that direction const QPoint absDelta(abs(delta.x()), abs(delta.y())); if ( absDelta.y() > mouseContainer.height() / 2 ) { delta.setY(mouseContainer.height() - absDelta.y()); } if ( absDelta.x() > mouseContainer.width() / 2 ) { delta.setX(mouseContainer.width() - absDelta.x()); } if ( mousePos.y() <= mouseContainer.top() + 4 && verticalScrollBar()->value() < verticalScrollBar()->maximum() - 10 ) { mousePos.setY( mouseContainer.bottom() - 5 ); QCursor::setPos( mousePos ); } // wrap mouse from bottom to top else if ( mousePos.y() >= mouseContainer.bottom() - 4 && verticalScrollBar()->value() > 10 ) { mousePos.setY( mouseContainer.top() + 5 ); QCursor::setPos( mousePos ); } // remember last position d->mouseGrabPos = mousePos; // scroll page by position increment scrollTo( horizontalScrollBar()->value() + delta.x(), verticalScrollBar()->value() + delta.y() ); } } else if ( rightButton && !d->mousePressPos.isNull() && d->aMouseSelect ) { // if mouse moves 5 px away from the press point, switch to 'selection' int deltaX = d->mousePressPos.x() - e->globalPos().x(), deltaY = d->mousePressPos.y() - e->globalPos().y(); if ( deltaX > 5 || deltaX < -5 || deltaY > 5 || deltaY < -5 ) { d->aPrevAction = d->aMouseNormal; d->aMouseSelect->trigger(); QPoint newPos = eventPos + QPoint( deltaX, deltaY ); selectionStart( newPos, palette().color( QPalette::Active, QPalette::Highlight ).light( 120 ), false ); updateSelection( eventPos ); break; } } else { /* Forward move events which are still not yet consumed by "mouse grab" or aMouseSelect */ d->mouseAnnotation->routeMouseMoveEvent( pageItem, eventPos, leftButton ); updateCursor(); } } break; case Okular::Settings::EnumMouseMode::Zoom: case Okular::Settings::EnumMouseMode::RectSelect: case Okular::Settings::EnumMouseMode::TableSelect: case Okular::Settings::EnumMouseMode::TrimSelect: // set second corner of selection if ( d->mouseSelecting ) { updateSelection( eventPos ); d->mouseOverLinkObject = nullptr; } updateCursor(); break; case Okular::Settings::EnumMouseMode::Magnifier: if ( e->buttons() ) // if any button is pressed at all { moveMagnifier( e->pos() ); updateMagnifier( eventPos ); } break; case Okular::Settings::EnumMouseMode::TextSelect: // if mouse moves 5 px away from the press point and the document soupports text extraction, do 'textselection' if ( !d->mouseTextSelecting && !d->mousePressPos.isNull() && d->document->supportsSearching() && ( ( eventPos - d->mouseSelectPos ).manhattanLength() > 5 ) ) { d->mouseTextSelecting = true; } updateSelection( eventPos ); updateCursor(); break; } } void PageView::mousePressEvent( QMouseEvent * e ) { d->controlWheelAccumulatedDelta = 0; // don't perform any mouse action when no document is shown if ( d->items.isEmpty() ) return; // if performing a selection or dyn zooming, disable mouse press if ( d->mouseSelecting || ( e->button() != Qt::MidButton && ( e->buttons() & Qt::MidButton) ) || d->viewportMoveActive ) return; // if the page is scrolling, stop it if ( d->autoScrollTimer ) { d->scrollIncrement = 0; d->autoScrollTimer->stop(); } // if pressing mid mouse button while not doing other things, begin 'continuous zoom' mode if ( e->button() == Qt::MidButton ) { d->mouseMidLastY = e->globalPos().y(); setCursor( Qt::SizeVerCursor ); return; } const QPoint eventPos = contentAreaPoint( e->pos() ); // if we're editing an annotation, dispatch event to it if ( d->annotator && d->annotator->active() ) { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); d->annotator->routeMouseEvent( e, pageItem ); return; } // trigger history navigation for additional mouse buttons if ( e->button() == Qt::XButton1 ) { emit mouseBackButtonClick(); return; } if ( e->button() == Qt::XButton2 ) { emit mouseForwardButtonClick(); return; } // update press / 'start drag' mouse position d->mousePressPos = e->globalPos(); // handle mode dependant mouse press actions bool leftButton = e->button() == Qt::LeftButton, rightButton = e->button() == Qt::RightButton; // Not sure we should erase the selection when clicking with left. if ( d->mouseMode != Okular::Settings::EnumMouseMode::TextSelect ) textSelectionClear(); switch ( d->mouseMode ) { case Okular::Settings::EnumMouseMode::Browse: // drag start / click / link following { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); if ( leftButton ) { if ( pageItem ) { d->mouseAnnotation->routeMousePressEvent( pageItem, eventPos ); } d->mouseGrabPos = d->mouseOnRect ? QPoint() : d->mousePressPos; if ( !d->mouseOnRect ) d->leftClickTimer.start( QApplication::doubleClickInterval() + 10 ); } else if ( rightButton ) { if ( pageItem ) { // find out normalized mouse coords inside current item const QRect & itemRect = pageItem->uncroppedGeometry(); double nX = pageItem->absToPageX(eventPos.x()); double nY = pageItem->absToPageY(eventPos.y()); const QLinkedList< const Okular::ObjectRect *> orects = pageItem->page()->objectRects( Okular::ObjectRect::OAnnotation, nX, nY, itemRect.width(), itemRect.height() ); if ( !orects.isEmpty() ) { AnnotationPopup popup( d->document, AnnotationPopup::MultiAnnotationMode, this ); foreach ( const Okular::ObjectRect * orect, orects ) { Okular::Annotation * ann = ( (Okular::AnnotationObjectRect *)orect )->annotation(); if ( ann && (ann->subType() != Okular::Annotation::AWidget) ) popup.addAnnotation( ann, pageItem->pageNumber() ); } connect( &popup, &AnnotationPopup::openAnnotationWindow, this, &PageView::openAnnotationWindow ); popup.exec( e->globalPos() ); // Since ↑ spins its own event loop we won't get the mouse release event // so reset mousePressPos here d->mousePressPos = QPoint(); } } } } break; case Okular::Settings::EnumMouseMode::Zoom: // set first corner of the zoom rect if ( leftButton ) selectionStart( eventPos, palette().color( QPalette::Active, QPalette::Highlight ), false ); else if ( rightButton ) updateZoom( ZoomOut ); break; case Okular::Settings::EnumMouseMode::Magnifier: moveMagnifier( e->pos() ); d->magnifierView->show(); updateMagnifier( eventPos ); break; case Okular::Settings::EnumMouseMode::RectSelect: // set first corner of the selection rect case Okular::Settings::EnumMouseMode::TrimSelect: if ( leftButton ) { selectionStart( eventPos, palette().color( QPalette::Active, QPalette::Highlight ).light( 120 ), false ); } break; case Okular::Settings::EnumMouseMode::TableSelect: if ( leftButton ) { if (d->tableSelectionParts.isEmpty()) { selectionStart( eventPos, palette().color( QPalette::Active, QPalette::Highlight ).light( 120 ), false ); } else { QRect updatedRect; foreach (const TableSelectionPart &tsp, d->tableSelectionParts) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); // This will update the whole table rather than just the added/removed divider // (which can span more than one part). updatedRect = updatedRect.united(selectionPartRect); if (!selectionPartRect.contains(eventPos)) continue; // At this point it's clear we're either adding or removing a divider manually, so obviously the user is happy with the guess (if any). d->tableDividersGuessed = false; // There's probably a neat trick to finding which edge it's closest to, // but this way has the advantage of simplicity. const int fromLeft = abs(selectionPartRect.left() - eventPos.x()); const int fromRight = abs(selectionPartRect.left() + selectionPartRect.width() - eventPos.x()); const int fromTop = abs(selectionPartRect.top() - eventPos.y()); const int fromBottom = abs(selectionPartRect.top() + selectionPartRect.height() - eventPos.y()); const int colScore = fromToptableSelectionCols.length(); i++) { const double col = (d->tableSelectionCols[i] - tsp.rectInSelection.left) / (tsp.rectInSelection.right - tsp.rectInSelection.left); const int colX = selectionPartRect.left() + col * selectionPartRect.width() + 0.5; if (abs(colX - eventPos.x())<=3) { d->tableSelectionCols.removeAt(i); deleted=true; break; } } if (!deleted) { double col = eventPos.x() - selectionPartRect.left(); col /= selectionPartRect.width(); // at this point, it's normalised within the part col *= (tsp.rectInSelection.right - tsp.rectInSelection.left); col += tsp.rectInSelection.left; // at this point, it's normalised within the whole table d->tableSelectionCols.append(col); qSort(d->tableSelectionCols); } } else { bool deleted=false; for(int i=0; itableSelectionRows.length(); i++) { const double row = (d->tableSelectionRows[i] - tsp.rectInSelection.top) / (tsp.rectInSelection.bottom - tsp.rectInSelection.top); const int rowY = selectionPartRect.top() + row * selectionPartRect.height() + 0.5; if (abs(rowY - eventPos.y())<=3) { d->tableSelectionRows.removeAt(i); deleted=true; break; } } if (!deleted) { double row = eventPos.y() - selectionPartRect.top(); row /= selectionPartRect.height(); // at this point, it's normalised within the part row *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); row += tsp.rectInSelection.top; // at this point, it's normalised within the whole table d->tableSelectionRows.append(row); qSort(d->tableSelectionRows); } } } updatedRect.translate( -contentAreaPosition() ); viewport()->update( updatedRect ); } } break; case Okular::Settings::EnumMouseMode::TextSelect: d->mouseSelectPos = eventPos; if ( !rightButton ) { textSelectionClear(); } break; } } void PageView::mouseReleaseEvent( QMouseEvent * e ) { d->controlWheelAccumulatedDelta = 0; // stop the drag scrolling d->dragScrollTimer.stop(); d->leftClickTimer.stop(); const bool leftButton = e->button() == Qt::LeftButton; const bool rightButton = e->button() == Qt::RightButton; if ( d->mouseAnnotation->isActive() && leftButton ) { // Just finished to move the annotation d->mouseAnnotation->routeMouseReleaseEvent(); } // don't perform any mouse action when no document is shown.. if ( d->items.isEmpty() ) { // ..except for right Clicks (emitted even it viewport is empty) if ( e->button() == Qt::RightButton ) emit rightClick( nullptr, e->globalPos() ); return; } // don't perform any mouse action when viewport is autoscrolling if ( d->viewportMoveActive ) return; const QPoint eventPos = contentAreaPoint( e->pos() ); // handle mode indepent mid buttom zoom if ( e->button() == Qt::MidButton ) { // request pixmaps since it was disabled during drag slotRequestVisiblePixmaps(); // the cursor may now be over a link.. update it updateCursor( eventPos ); return; } // if we're editing an annotation, dispatch event to it if ( d->annotator && d->annotator->active() ) { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); d->annotator->routeMouseEvent( e, pageItem ); return; } switch ( d->mouseMode ) { case Okular::Settings::EnumMouseMode::Browse:{ // return the cursor to its normal state after dragging if ( cursor().shape() == Qt::ClosedHandCursor ) updateCursor( eventPos ); PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); const QPoint pressPos = contentAreaPoint( mapFromGlobal( d->mousePressPos ) ); const PageViewItem * pageItemPressPos = pickItemOnPoint( pressPos.x(), pressPos.y() ); // if the mouse has not moved since the press, that's a -click- if ( leftButton && pageItem && pageItem == pageItemPressPos && ( (d->mousePressPos - e->globalPos()).manhattanLength() < QApplication::startDragDistance() ) ) { if ( !mouseReleaseOverLink( d->mouseOverLinkObject ) && ( e->modifiers() == Qt::ShiftModifier ) ) { const double nX = pageItem->absToPageX(eventPos.x()); const double nY = pageItem->absToPageY(eventPos.y()); const Okular::ObjectRect * rect; // TODO: find a better way to activate the source reference "links" // for the moment they are activated with Shift + left click // Search the nearest source reference. rect = pageItem->page()->objectRect( Okular::ObjectRect::SourceRef, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); if ( !rect ) { static const double s_minDistance = 0.025; // FIXME?: empirical value? double distance = 0.0; rect = pageItem->page()->nearestObjectRect( Okular::ObjectRect::SourceRef, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight(), &distance ); // distance is distanceSqr, adapt it to a normalized value distance = distance / (pow( pageItem->uncroppedWidth(), 2 ) + pow( pageItem->uncroppedHeight(), 2 )); if ( rect && ( distance > s_minDistance ) ) rect = nullptr; } if ( rect ) { const Okular::SourceReference * ref = static_cast< const Okular::SourceReference * >( rect->object() ); d->document->processSourceReference( ref ); } else { const Okular::SourceReference * ref = d->document->dynamicSourceReference( pageItem-> pageNumber(), nX * pageItem->page()->width(), nY * pageItem->page()->height() ); if ( ref ) { d->document->processSourceReference( ref ); delete ref; } } } #if 0 else { // a link can move us to another page or even to another document, there's no point in trying to // process the click on the image once we have processes the click on the link rect = pageItem->page()->objectRect( Okular::ObjectRect::Image, nX, nY, pageItem->width(), pageItem->height() ); if ( rect ) { // handle click over a image } /* Enrico and me have decided this is not worth the trouble it generates else { // if not on a rect, the click selects the page // if ( pageItem->pageNumber() != (int)d->document->currentPage() ) d->document->setViewportPage( pageItem->pageNumber(), this ); }*/ } #endif } else if ( rightButton && !d->mouseAnnotation->isModified() ) { if ( pageItem && pageItem == pageItemPressPos && ( (d->mousePressPos - e->globalPos()).manhattanLength() < QApplication::startDragDistance() ) ) { QMenu * menu = createProcessLinkMenu(pageItem, eventPos ); if ( menu ) { menu->exec( e->globalPos() ); menu->deleteLater(); } else { const double nX = pageItem->absToPageX(eventPos.x()); const double nY = pageItem->absToPageY(eventPos.y()); // a link can move us to another page or even to another document, there's no point in trying to // process the click on the image once we have processes the click on the link const Okular::ObjectRect * rect = pageItem->page()->objectRect( Okular::ObjectRect::Image, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); if ( rect ) { // handle right click over a image } else { // right click (if not within 5 px of the press point, the mode // had been already changed to 'Selection' instead of 'Normal') emit rightClick( pageItem->page(), e->globalPos() ); } } } else { // right click (if not within 5 px of the press point, the mode // had been already changed to 'Selection' instead of 'Normal') emit rightClick( pageItem ? pageItem->page() : nullptr, e->globalPos() ); } } }break; case Okular::Settings::EnumMouseMode::Zoom: // if a selection rect has been defined, zoom into it if ( leftButton && d->mouseSelecting ) { QRect selRect = d->mouseSelectionRect.normalized(); if ( selRect.width() <= 8 && selRect.height() <= 8 ) { selectionClear(); break; } // find out new zoom ratio and normalized view center (relative to the contentsRect) double zoom = qMin( (double)viewport()->width() / (double)selRect.width(), (double)viewport()->height() / (double)selRect.height() ); double nX = (double)(selRect.left() + selRect.right()) / (2.0 * (double)contentAreaWidth()); double nY = (double)(selRect.top() + selRect.bottom()) / (2.0 * (double)contentAreaHeight()); const float upperZoomLimit = d->document->supportsTiles() ? 16.0 : 4.0; if ( d->zoomFactor <= upperZoomLimit || zoom <= 1.0 ) { d->zoomFactor *= zoom; viewport()->setUpdatesEnabled( false ); updateZoom( ZoomRefreshCurrent ); viewport()->setUpdatesEnabled( true ); } // recenter view and update the viewport center( (int)(nX * contentAreaWidth()), (int)(nY * contentAreaHeight()) ); viewport()->update(); // hide message box and delete overlay window selectionClear(); } break; case Okular::Settings::EnumMouseMode::Magnifier: d->magnifierView->hide(); break; case Okular::Settings::EnumMouseMode::TrimSelect: { // if it is a left release checks if is over a previous link press if ( leftButton && mouseReleaseOverLink ( d->mouseOverLinkObject ) ) { selectionClear(); break; } // if mouse is released and selection is null this is a rightClick if ( rightButton && !d->mouseSelecting ) { break; } PageViewItem * pageItem = pickItemOnPoint(eventPos.x(), eventPos.y()); // ensure end point rests within a page, or ignore if (!pageItem) { break; } QRect selectionRect = d->mouseSelectionRect.normalized(); double nLeft = pageItem->absToPageX(selectionRect.left()); double nRight = pageItem->absToPageX(selectionRect.right()); double nTop = pageItem->absToPageY(selectionRect.top()); double nBottom = pageItem->absToPageY(selectionRect.bottom()); if ( nLeft < 0 ) nLeft = 0; if ( nTop < 0 ) nTop = 0; if ( nRight > 1 ) nRight = 1; if ( nBottom > 1 ) nBottom = 1; d->trimBoundingBox = Okular::NormalizedRect(nLeft, nTop, nRight, nBottom); // Trim Selection successfully done, hide prompt d->messageWindow->hide(); // clear widget selection and invalidate rect selectionClear(); // When Trim selection bbox interaction is over, we should switch to another mousemode. if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } else { d->aMouseNormal->trigger(); } // with d->trimBoundingBox defined, redraw for trim to take visual effect if ( d->document->pages() > 0 ) { slotRelayoutPages(); slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already! } break; } case Okular::Settings::EnumMouseMode::RectSelect: { // if it is a left release checks if is over a previous link press if ( leftButton && mouseReleaseOverLink ( d->mouseOverLinkObject ) ) { selectionClear(); break; } // if mouse is released and selection is null this is a rightClick if ( rightButton && !d->mouseSelecting ) { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); emit rightClick( pageItem ? pageItem->page() : nullptr, e->globalPos() ); break; } // if a selection is defined, display a popup if ( (!leftButton && !d->aPrevAction) || (!rightButton && d->aPrevAction) || !d->mouseSelecting ) break; QRect selectionRect = d->mouseSelectionRect.normalized(); if ( selectionRect.width() <= 8 && selectionRect.height() <= 8 ) { selectionClear(); if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } break; } // if we support text generation QString selectedText; if (d->document->supportsSearching()) { // grab text in selection by extracting it from all intersected pages const Okular::Page * okularPage=nullptr; QVector< PageViewItem * >::const_iterator iIt = d->items.constBegin(), iEnd = d->items.constEnd(); for ( ; iIt != iEnd; ++iIt ) { PageViewItem * item = *iIt; if ( !item->isVisible() ) continue; const QRect & itemRect = item->croppedGeometry(); if ( selectionRect.intersects( itemRect ) ) { // request the textpage if there isn't one okularPage= item->page(); qCDebug(OkularUiDebug) << "checking if page" << item->pageNumber() << "has text:" << okularPage->hasTextPage(); if ( !okularPage->hasTextPage() ) d->document->requestTextPage( okularPage->number() ); // grab text in the rect that intersects itemRect QRect relativeRect = selectionRect.intersected( itemRect ); relativeRect.translate( -item->uncroppedGeometry().topLeft() ); Okular::RegularAreaRect rects; rects.append( Okular::NormalizedRect( relativeRect, item->uncroppedWidth(), item->uncroppedHeight() ) ); selectedText += okularPage->text( &rects ); } } } // popup that ask to copy:text and copy/save:image QMenu menu( this ); menu.setObjectName("PopupMenu"); QAction *textToClipboard = nullptr; #ifdef HAVE_SPEECH QAction *speakText = nullptr; #endif QAction *imageToClipboard = nullptr; QAction *imageToFile = nullptr; if ( d->document->supportsSearching() && !selectedText.isEmpty() ) { menu.addAction( new OKMenuTitle( &menu, i18np( "Text (1 character)", "Text (%1 characters)", selectedText.length() ) ) ); textToClipboard = menu.addAction( QIcon::fromTheme(QStringLiteral("edit-copy")), i18n( "Copy to Clipboard" ) ); textToClipboard->setObjectName("CopyTextToClipboard"); bool copyAllowed = d->document->isAllowed( Okular::AllowCopy ); if ( !copyAllowed ) { textToClipboard->setEnabled( false ); textToClipboard->setText( i18n("Copy forbidden by DRM") ); } #ifdef HAVE_SPEECH if ( Okular::Settings::useTTS() ) speakText = menu.addAction( QIcon::fromTheme(QStringLiteral("text-speak")), i18n( "Speak Text" ) ); #endif if ( copyAllowed ) { addWebShortcutsMenu( &menu, selectedText ); } } menu.addAction( new OKMenuTitle( &menu, i18n( "Image (%1 by %2 pixels)", selectionRect.width(), selectionRect.height() ) ) ); imageToClipboard = menu.addAction( QIcon::fromTheme(QStringLiteral("image-x-generic")), i18n( "Copy to Clipboard" ) ); imageToFile = menu.addAction( QIcon::fromTheme(QStringLiteral("document-save")), i18n( "Save to File..." ) ); QAction *choice = menu.exec( e->globalPos() ); // check if the user really selected an action if ( choice ) { // IMAGE operation chosen if ( choice == imageToClipboard || choice == imageToFile ) { // renders page into a pixmap QPixmap copyPix( selectionRect.width(), selectionRect.height() ); QPainter copyPainter( ©Pix ); copyPainter.translate( -selectionRect.left(), -selectionRect.top() ); drawDocumentOnPainter( selectionRect, ©Painter ); copyPainter.end(); if ( choice == imageToClipboard ) { // [2] copy pixmap to clipboard QClipboard *cb = QApplication::clipboard(); cb->setPixmap( copyPix, QClipboard::Clipboard ); if ( cb->supportsSelection() ) cb->setPixmap( copyPix, QClipboard::Selection ); d->messageWindow->display( i18n( "Image [%1x%2] copied to clipboard.", copyPix.width(), copyPix.height() ) ); } else if ( choice == imageToFile ) { // [3] save pixmap to file QString fileName = QFileDialog::getSaveFileName(this, i18n("Save file"), QString(), i18n("Images (*.png *.jpeg)")); if ( fileName.isEmpty() ) d->messageWindow->display( i18n( "File not saved." ), QString(), PageViewMessage::Warning ); else { QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl( QUrl::fromLocalFile(fileName) ); QString type; if ( !mime.isDefault() ) type = QStringLiteral("PNG"); else type = mime.name().section( QLatin1Char('/'), -1 ).toUpper(); copyPix.save( fileName, qPrintable( type ) ); d->messageWindow->display( i18n( "Image [%1x%2] saved to %3 file.", copyPix.width(), copyPix.height(), type ) ); } } } // TEXT operation chosen else { if ( choice == textToClipboard ) { // [1] copy text to clipboard QClipboard *cb = QApplication::clipboard(); cb->setText( selectedText, QClipboard::Clipboard ); if ( cb->supportsSelection() ) cb->setText( selectedText, QClipboard::Selection ); } #ifdef HAVE_SPEECH else if ( choice == speakText ) { // [2] speech selection using TTS d->tts()->say( selectedText ); } #endif } } // clear widget selection and invalidate rect selectionClear(); // restore previous action if came from it using right button if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } }break; case Okular::Settings::EnumMouseMode::TableSelect: { // if it is a left release checks if is over a previous link press if ( leftButton && mouseReleaseOverLink ( d->mouseOverLinkObject ) ) { selectionClear(); break; } // if mouse is released and selection is null this is a rightClick if ( rightButton && !d->mouseSelecting ) { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); emit rightClick( pageItem ? pageItem->page() : nullptr, e->globalPos() ); break; } QRect selectionRect = d->mouseSelectionRect.normalized(); if ( selectionRect.width() <= 8 && selectionRect.height() <= 8 && d->tableSelectionParts.isEmpty() ) { selectionClear(); if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } break; } if (d->mouseSelecting) { // break up the selection into page-relative pieces d->tableSelectionParts.clear(); const Okular::Page * okularPage=nullptr; QVector< PageViewItem * >::const_iterator iIt = d->items.constBegin(), iEnd = d->items.constEnd(); for ( ; iIt != iEnd; ++iIt ) { PageViewItem * item = *iIt; if ( !item->isVisible() ) continue; const QRect & itemRect = item->croppedGeometry(); if ( selectionRect.intersects( itemRect ) ) { // request the textpage if there isn't one okularPage= item->page(); qCDebug(OkularUiDebug) << "checking if page" << item->pageNumber() << "has text:" << okularPage->hasTextPage(); if ( !okularPage->hasTextPage() ) d->document->requestTextPage( okularPage->number() ); // grab text in the rect that intersects itemRect QRect rectInItem = selectionRect.intersected( itemRect ); rectInItem.translate( -item->uncroppedGeometry().topLeft() ); QRect rectInSelection = selectionRect.intersected( itemRect ); rectInSelection.translate( -selectionRect.topLeft() ); d->tableSelectionParts.append( TableSelectionPart( item, Okular::NormalizedRect( rectInItem, item->uncroppedWidth(), item->uncroppedHeight() ), Okular::NormalizedRect( rectInSelection, selectionRect.width(), selectionRect.height() ) ) ); } } QRect updatedRect = d->mouseSelectionRect.normalized().adjusted( 0, 0, 1, 1 ); updatedRect.translate( -contentAreaPosition() ); d->mouseSelecting = false; d->mouseSelectionRect.setCoords( 0, 0, 0, 0 ); d->tableSelectionCols.clear(); d->tableSelectionRows.clear(); guessTableDividers(); viewport()->update( updatedRect ); } if ( !d->document->isAllowed( Okular::AllowCopy ) ) { d->messageWindow->display( i18n("Copy forbidden by DRM"), QString(), PageViewMessage::Info, -1 ); break; } QString selText; QString selHtml; QList xs = d->tableSelectionCols; QList ys = d->tableSelectionRows; xs.prepend(0.0); xs.append(1.0); ys.prepend(0.0); ys.append(1.0); selHtml = QString::fromLatin1("" "" ""); for (int r=0; r+1"); for (int c=0; c+1tableSelectionParts) { // first, crop the cell to this part if (!tsp.rectInSelection.intersects(cell)) continue; Okular::NormalizedRect cellPart = tsp.rectInSelection & cell; // intersection // second, convert it from table coordinates to part coordinates cellPart.left -= tsp.rectInSelection.left; cellPart.left /= (tsp.rectInSelection.right - tsp.rectInSelection.left); cellPart.right -= tsp.rectInSelection.left; cellPart.right /= (tsp.rectInSelection.right - tsp.rectInSelection.left); cellPart.top -= tsp.rectInSelection.top; cellPart.top /= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); cellPart.bottom -= tsp.rectInSelection.top; cellPart.bottom /= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); // third, convert from part coordinates to item coordinates cellPart.left *= (tsp.rectInItem.right - tsp.rectInItem.left); cellPart.left += tsp.rectInItem.left; cellPart.right *= (tsp.rectInItem.right - tsp.rectInItem.left); cellPart.right += tsp.rectInItem.left; cellPart.top *= (tsp.rectInItem.bottom - tsp.rectInItem.top); cellPart.top += tsp.rectInItem.top; cellPart.bottom *= (tsp.rectInItem.bottom - tsp.rectInItem.top); cellPart.bottom += tsp.rectInItem.top; // now get the text Okular::RegularAreaRect rects; rects.append( cellPart ); txt += tsp.item->page()->text( &rects, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ); } QString html = txt; selText += txt.replace(QLatin1Char('\n'), QLatin1Char(' ')); html.replace(QLatin1Char('&'), QLatin1String("&")).replace(QLatin1Char('<'), QLatin1String("<")).replace(QLatin1Char('>'), QLatin1String(">")); // Remove newlines, do not turn them into
, because // Excel interprets
within cell as new cell... html.replace(QLatin1Char('\n'), QLatin1String(" ")); selHtml += QStringLiteral("
"); } selText += QLatin1Char('\n'); selHtml += QLatin1String("\n"); } selHtml += QLatin1String("
") + html + QStringLiteral("
\n"); QClipboard *cb = QApplication::clipboard(); QMimeData *md = new QMimeData(); md->setText(selText); md->setHtml(selHtml); cb->setMimeData( md, QClipboard::Clipboard ); if ( cb->supportsSelection() ) cb->setMimeData( md, QClipboard::Selection ); }break; case Okular::Settings::EnumMouseMode::TextSelect: // if it is a left release checks if is over a previous link press if ( leftButton && mouseReleaseOverLink ( d->mouseOverLinkObject ) ) { selectionClear(); break; } if ( d->mouseTextSelecting ) { d->mouseTextSelecting = false; // textSelectionClear(); if ( d->document->isAllowed( Okular::AllowCopy ) ) { const QString text = d->selectedText(); if ( !text.isEmpty() ) { QClipboard *cb = QApplication::clipboard(); if ( cb->supportsSelection() ) cb->setText( text, QClipboard::Selection ); } } } else if ( !d->mousePressPos.isNull() && rightButton ) { PageViewItem* item = pickItemOnPoint(eventPos.x(),eventPos.y()); const Okular::Page *page; //if there is text selected in the page if (item) { QAction * httpLink = nullptr; QAction * textToClipboard = nullptr; QString url; QMenu * menu = createProcessLinkMenu( item, eventPos ); const bool mouseClickOverLink = (menu != nullptr); #ifdef HAVE_SPEECH QAction *speakText = nullptr; #endif if ( (page = item->page())->textSelection() ) { if ( !menu ) { menu = new QMenu(this); } textToClipboard = menu->addAction( QIcon::fromTheme( QStringLiteral("edit-copy") ), i18n( "Copy Text" ) ); #ifdef HAVE_SPEECH if ( Okular::Settings::useTTS() ) speakText = menu->addAction( QIcon::fromTheme( QStringLiteral("text-speak") ), i18n( "Speak Text" ) ); #endif if ( !d->document->isAllowed( Okular::AllowCopy ) ) { textToClipboard->setEnabled( false ); textToClipboard->setText( i18n("Copy forbidden by DRM") ); } else { addWebShortcutsMenu( menu, d->selectedText() ); } // if the right-click was over a link add "Follow This link" instead of "Go to" if (!mouseClickOverLink) { url = UrlUtils::getUrl( d->selectedText() ); if ( !url.isEmpty() ) { const QString squeezedText = KStringHandler::rsqueeze( url, 30 ); httpLink = menu->addAction( i18n( "Go to '%1'", squeezedText ) ); httpLink->setObjectName("GoToAction"); } } } if ( menu ) { menu->setObjectName("PopupMenu"); QAction *choice = menu->exec( e->globalPos() ); // check if the user really selected an action if ( choice ) { if ( choice == textToClipboard ) copyTextSelection(); #ifdef HAVE_SPEECH else if ( choice == speakText ) { const QString text = d->selectedText(); d->tts()->say( text ); } #endif else if ( choice == httpLink ) { new KRun( QUrl( url ), this ); } } menu->deleteLater(); } } } break; } // reset mouse press / 'drag start' position d->mousePressPos = QPoint(); } void PageView::guessTableDividers() { QList< QPair > colTicks, rowTicks, colSelectionTicks, rowSelectionTicks; foreach ( const TableSelectionPart& tsp, d->tableSelectionParts ) { // add ticks for the edges of this area... colSelectionTicks.append( qMakePair( tsp.rectInSelection.left, +1 ) ); colSelectionTicks.append( qMakePair( tsp.rectInSelection.right, -1 ) ); rowSelectionTicks.append( qMakePair( tsp.rectInSelection.top, +1 ) ); rowSelectionTicks.append( qMakePair( tsp.rectInSelection.bottom, -1 ) ); // get the words in this part Okular::RegularAreaRect rects; rects.append( tsp.rectInItem ); const Okular::TextEntity::List words = tsp.item->page()->words( &rects, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ); foreach (Okular::TextEntity *te, words) { if (te->text().isEmpty()) { delete te; continue; } Okular::NormalizedRect wordArea = *te->area(); // convert it from item coordinates to part coordinates wordArea.left -= tsp.rectInItem.left; wordArea.left /= (tsp.rectInItem.right - tsp.rectInItem.left); wordArea.right -= tsp.rectInItem.left; wordArea.right /= (tsp.rectInItem.right - tsp.rectInItem.left); wordArea.top -= tsp.rectInItem.top; wordArea.top /= (tsp.rectInItem.bottom - tsp.rectInItem.top); wordArea.bottom -= tsp.rectInItem.top; wordArea.bottom /= (tsp.rectInItem.bottom - tsp.rectInItem.top); // convert from part coordinates to table coordinates wordArea.left *= (tsp.rectInSelection.right - tsp.rectInSelection.left); wordArea.left += tsp.rectInSelection.left; wordArea.right *= (tsp.rectInSelection.right - tsp.rectInSelection.left); wordArea.right += tsp.rectInSelection.left; wordArea.top *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); wordArea.top += tsp.rectInSelection.top; wordArea.bottom *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); wordArea.bottom += tsp.rectInSelection.top; // add to the ticks arrays... colTicks.append( qMakePair( wordArea.left, +1) ); colTicks.append( qMakePair( wordArea.right, -1) ); rowTicks.append( qMakePair( wordArea.top, +1) ); rowTicks.append( qMakePair( wordArea.bottom, -1) ); delete te; } } int tally = 0; qSort( colSelectionTicks ); qSort( rowSelectionTicks ); for (int i = 0; i < colSelectionTicks.length(); ++i) { tally += colSelectionTicks[i].second; if ( tally == 0 && i + 1 < colSelectionTicks.length() && colSelectionTicks[i+1].first != colSelectionTicks[i].first) { colTicks.append( qMakePair( colSelectionTicks[i].first, +1 ) ); colTicks.append( qMakePair( colSelectionTicks[i+1].first, -1 ) ); } } Q_ASSERT( tally == 0 ); for (int i = 0; i < rowSelectionTicks.length(); ++i) { tally += rowSelectionTicks[i].second; if ( tally == 0 && i + 1 < rowSelectionTicks.length() && rowSelectionTicks[i+1].first != rowSelectionTicks[i].first) { rowTicks.append( qMakePair( rowSelectionTicks[i].first, +1 ) ); rowTicks.append( qMakePair( rowSelectionTicks[i+1].first, -1 ) ); } } Q_ASSERT( tally == 0 ); qSort( colTicks ); qSort( rowTicks ); for (int i = 0; i < colTicks.length(); ++i) { tally += colTicks[i].second; if ( tally == 0 && i + 1 < colTicks.length() && colTicks[i+1].first != colTicks[i].first) { d->tableSelectionCols.append( (colTicks[i].first+colTicks[i+1].first) / 2 ); d->tableDividersGuessed = true; } } Q_ASSERT( tally == 0 ); for (int i = 0; i < rowTicks.length(); ++i) { tally += rowTicks[i].second; if ( tally == 0 && i + 1 < rowTicks.length() && rowTicks[i+1].first != rowTicks[i].first) { d->tableSelectionRows.append( (rowTicks[i].first+rowTicks[i+1].first) / 2 ); d->tableDividersGuessed = true; } } Q_ASSERT( tally == 0 ); } void PageView::mouseDoubleClickEvent( QMouseEvent * e ) { d->controlWheelAccumulatedDelta = 0; if ( e->button() == Qt::LeftButton ) { const QPoint eventPos = contentAreaPoint( e->pos() ); PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); if ( pageItem ) { // find out normalized mouse coords inside current item double nX = pageItem->absToPageX(eventPos.x()); double nY = pageItem->absToPageY(eventPos.y()); if ( d->mouseMode == Okular::Settings::EnumMouseMode::TextSelect ) { textSelectionClear(); Okular::RegularAreaRect *wordRect = pageItem->page()->wordAt( Okular::NormalizedPoint( nX, nY ) ); if ( wordRect ) { // TODO words with hyphens across pages d->document->setPageTextSelection( pageItem->pageNumber(), wordRect, palette().color( QPalette::Active, QPalette::Highlight ) ); d->pagesWithTextSelection << pageItem->pageNumber(); if ( d->document->isAllowed( Okular::AllowCopy ) ) { const QString text = d->selectedText(); if ( !text.isEmpty() ) { QClipboard *cb = QApplication::clipboard(); if ( cb->supportsSelection() ) cb->setText( text, QClipboard::Selection ); } } return; } } const QRect & itemRect = pageItem->uncroppedGeometry(); Okular::Annotation * ann = nullptr; const Okular::ObjectRect * orect = pageItem->page()->objectRect( Okular::ObjectRect::OAnnotation, nX, nY, itemRect.width(), itemRect.height() ); if ( orect ) ann = ( (Okular::AnnotationObjectRect *)orect )->annotation(); if ( ann && ann->subType() != Okular::Annotation::AWidget ) { openAnnotationWindow( ann, pageItem->pageNumber() ); } } } } void PageView::wheelEvent( QWheelEvent *e ) { // don't perform any mouse action when viewport is autoscrolling if ( d->viewportMoveActive ) return; if ( !d->document->isOpened() ) { QAbstractScrollArea::wheelEvent( e ); return; } int delta = e->delta(), vScroll = verticalScrollBar()->value(); e->accept(); if ( (e->modifiers() & Qt::ControlModifier) == Qt::ControlModifier ) { d->controlWheelAccumulatedDelta += delta; if ( d->controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep ) { slotZoomOut(); d->controlWheelAccumulatedDelta = 0; } else if ( d->controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep ) { slotZoomIn(); d->controlWheelAccumulatedDelta = 0; } } else { d->controlWheelAccumulatedDelta = 0; if ( delta <= -QWheelEvent::DefaultDeltasPerStep && !Okular::Settings::viewContinuous() && vScroll == verticalScrollBar()->maximum() ) { // go to next page if ( (int)d->document->currentPage() < d->items.count() - 1 ) { // more optimized than document->setNextPage and then move view to top Okular::DocumentViewport newViewport = d->document->viewport(); newViewport.pageNumber += viewColumns(); if ( newViewport.pageNumber >= (int)d->items.count() ) newViewport.pageNumber = d->items.count() - 1; newViewport.rePos.enabled = true; newViewport.rePos.normalizedY = 0.0; d->document->setViewport( newViewport ); } } else if ( delta >= QWheelEvent::DefaultDeltasPerStep && !Okular::Settings::viewContinuous() && vScroll == verticalScrollBar()->minimum() ) { // go to prev page if ( d->document->currentPage() > 0 ) { // more optimized than document->setPrevPage and then move view to bottom Okular::DocumentViewport newViewport = d->document->viewport(); newViewport.pageNumber -= viewColumns(); if ( newViewport.pageNumber < 0 ) newViewport.pageNumber = 0; newViewport.rePos.enabled = true; newViewport.rePos.normalizedY = 1.0; d->document->setViewport( newViewport ); } } else QAbstractScrollArea::wheelEvent( e ); } updateCursor(); } bool PageView::viewportEvent( QEvent * e ) { if ( e->type() == QEvent::ToolTip && d->mouseMode == Okular::Settings::EnumMouseMode::Browse ) { QHelpEvent * he = static_cast< QHelpEvent* >( e ); if ( d->mouseAnnotation->isMouseOver() ) { d->mouseAnnotation->routeTooltipEvent( he ); } else { const QPoint eventPos = contentAreaPoint( he->pos() ); PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); const Okular::ObjectRect * rect = nullptr; const Okular::Action * link = nullptr; if ( pageItem ) { double nX = pageItem->absToPageX( eventPos.x() ); double nY = pageItem->absToPageY( eventPos.y() ); rect = pageItem->page()->objectRect( Okular::ObjectRect::Action, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); if ( rect ) link = static_cast< const Okular::Action * >( rect->object() ); } if ( link ) { QRect r = rect->boundingRect( pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); r.translate( pageItem->uncroppedGeometry().topLeft() ); r.translate( -contentAreaPosition() ); QString tip = link->actionTip(); if ( !tip.isEmpty() ) QToolTip::showText( he->globalPos(), tip, viewport(), r ); } } e->accept(); return true; } else // do not stop the event return QAbstractScrollArea::viewportEvent( e ); } void PageView::scrollContentsBy( int dx, int dy ) { const QRect r = viewport()->rect(); viewport()->scroll( dx, dy, r ); // HACK manually repaint the damaged regions, as it seems some updates are missed // thus leaving artifacts around QRegion rgn( r ); rgn -= rgn & r.translated( dx, dy ); foreach ( const QRect &rect, rgn.rects() ) viewport()->repaint( rect ); } //END widget events QList< Okular::RegularAreaRect * > PageView::textSelections( const QPoint& start, const QPoint& end, int& firstpage ) { firstpage = -1; QList< Okular::RegularAreaRect * > ret; QSet< int > affectedItemsSet; QRect selectionRect = QRect( start, end ).normalized(); foreach( PageViewItem * item, d->items ) { if ( item->isVisible() && selectionRect.intersects( item->croppedGeometry() ) ) affectedItemsSet.insert( item->pageNumber() ); } #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << ">>>> item selected by mouse:" << affectedItemsSet.count(); #endif if ( !affectedItemsSet.isEmpty() ) { // is the mouse drag line the ne-sw diagonal of the selection rect? bool direction_ne_sw = start == selectionRect.topRight() || start == selectionRect.bottomLeft(); int tmpmin = d->document->pages(); int tmpmax = 0; foreach( int p, affectedItemsSet ) { if ( p < tmpmin ) tmpmin = p; if ( p > tmpmax ) tmpmax = p; } PageViewItem * a = pickItemOnPoint( (int)( direction_ne_sw ? selectionRect.right() : selectionRect.left() ), (int)selectionRect.top() ); int min = a && ( a->pageNumber() != tmpmax ) ? a->pageNumber() : tmpmin; PageViewItem * b = pickItemOnPoint( (int)( direction_ne_sw ? selectionRect.left() : selectionRect.right() ), (int)selectionRect.bottom() ); int max = b && ( b->pageNumber() != tmpmin ) ? b->pageNumber() : tmpmax; QList< int > affectedItemsIds; for ( int i = min; i <= max; ++i ) affectedItemsIds.append( i ); #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << ">>>> pages:" << affectedItemsIds; #endif firstpage = affectedItemsIds.first(); if ( affectedItemsIds.count() == 1 ) { PageViewItem * item = d->items[ affectedItemsIds.first() ]; selectionRect.translate( -item->uncroppedGeometry().topLeft() ); ret.append( textSelectionForItem( item, direction_ne_sw ? selectionRect.topRight() : selectionRect.topLeft(), direction_ne_sw ? selectionRect.bottomLeft() : selectionRect.bottomRight() ) ); } else if ( affectedItemsIds.count() > 1 ) { // first item PageViewItem * first = d->items[ affectedItemsIds.first() ]; QRect geom = first->croppedGeometry().intersected( selectionRect ).translated( -first->uncroppedGeometry().topLeft() ); ret.append( textSelectionForItem( first, selectionRect.bottom() > geom.height() ? ( direction_ne_sw ? geom.topRight() : geom.topLeft() ) : ( direction_ne_sw ? geom.bottomRight() : geom.bottomLeft() ), QPoint() ) ); // last item PageViewItem * last = d->items[ affectedItemsIds.last() ]; geom = last->croppedGeometry().intersected( selectionRect ).translated( -last->uncroppedGeometry().topLeft() ); // the last item needs to appended at last... Okular::RegularAreaRect * lastArea = textSelectionForItem( last, QPoint(), selectionRect.bottom() > geom.height() ? ( direction_ne_sw ? geom.bottomLeft() : geom.bottomRight() ) : ( direction_ne_sw ? geom.topLeft() : geom.topRight() ) ); affectedItemsIds.removeFirst(); affectedItemsIds.removeLast(); // item between the two above foreach( int page, affectedItemsIds ) { ret.append( textSelectionForItem( d->items[ page ] ) ); } ret.append( lastArea ); } } return ret; } void PageView::drawDocumentOnPainter( const QRect & contentsRect, QPainter * p ) { QColor backColor; if ( Okular::Settings::useCustomBackgroundColor() ) backColor = Okular::Settings::backgroundColor(); else backColor = viewport()->palette().color( QPalette::Dark ); // when checking if an Item is contained in contentsRect, instead of // growing PageViewItems rects (for keeping outline into account), we // grow the contentsRect QRect checkRect = contentsRect; checkRect.adjust( -3, -3, 1, 1 ); // create a region from which we'll subtract painted rects QRegion remainingArea( contentsRect ); // iterate over all items painting the ones intersecting contentsRect QVector< PageViewItem * >::const_iterator iIt = d->items.constBegin(), iEnd = d->items.constEnd(); for ( ; iIt != iEnd; ++iIt ) { // check if a piece of the page intersects the contents rect if ( !(*iIt)->isVisible() || !(*iIt)->croppedGeometry().intersects( checkRect ) ) continue; // get item and item's outline geometries PageViewItem * item = *iIt; QRect itemGeometry = item->croppedGeometry(), outlineGeometry = itemGeometry; outlineGeometry.adjust( -1, -1, 3, 3 ); // move the painter to the top-left corner of the real page p->save(); p->translate( itemGeometry.left(), itemGeometry.top() ); // draw the page outline (black border and 2px bottom-right shadow) if ( !itemGeometry.contains( contentsRect ) ) { int itemWidth = itemGeometry.width(), itemHeight = itemGeometry.height(); // draw simple outline p->setPen( Qt::black ); p->drawRect( -1, -1, itemWidth + 1, itemHeight + 1 ); // draw bottom/right gradient static const int levels = 2; int r = backColor.red() / (levels + 2) + 6, g = backColor.green() / (levels + 2) + 6, b = backColor.blue() / (levels + 2) + 6; for ( int i = 0; i < levels; i++ ) { p->setPen( QColor( r * (i+2), g * (i+2), b * (i+2) ) ); p->drawLine( i, i + itemHeight + 1, i + itemWidth + 1, i + itemHeight + 1 ); p->drawLine( i + itemWidth + 1, i, i + itemWidth + 1, i + itemHeight ); p->setPen( backColor ); p->drawLine( -1, i + itemHeight + 1, i - 1, i + itemHeight + 1 ); p->drawLine( i + itemWidth + 1, -1, i + itemWidth + 1, i - 1 ); } } // draw the page using the PagePainter with all flags active if ( contentsRect.intersects( itemGeometry ) ) { Okular::NormalizedPoint *viewPortPoint = nullptr; Okular::NormalizedPoint point( d->lastSourceLocationViewportNormalizedX, d->lastSourceLocationViewportNormalizedY ); if( Okular::Settings::showSourceLocationsGraphically() && item->pageNumber() == d->lastSourceLocationViewportPageNumber ) { viewPortPoint = &point; } QRect pixmapRect = contentsRect.intersected( itemGeometry ); pixmapRect.translate( -item->croppedGeometry().topLeft() ); PagePainter::paintCroppedPageOnPainter( p, item->page(), this, pageflags, item->uncroppedWidth(), item->uncroppedHeight(), pixmapRect, item->crop(), viewPortPoint ); } // remove painted area from 'remainingArea' and restore painter remainingArea -= outlineGeometry.intersected( contentsRect ); p->restore(); } // fill with background color the unpainted area const QVector &backRects = remainingArea.rects(); int backRectsNumber = backRects.count(); for ( int jr = 0; jr < backRectsNumber; jr++ ) p->fillRect( backRects[ jr ], backColor ); } void PageView::updateItemSize( PageViewItem * item, int colWidth, int rowHeight ) { const Okular::Page * okularPage = item->page(); double width = okularPage->width(), height = okularPage->height(), zoom = d->zoomFactor; Okular::NormalizedRect crop( 0., 0., 1., 1. ); // Handle cropping, due to either "Trim Margin" or "Trim to Selection" cases if (( Okular::Settings::trimMargins() && okularPage->isBoundingBoxKnown() && !okularPage->boundingBox().isNull() ) || ( d->aTrimToSelection && d->aTrimToSelection->isChecked() && !d->trimBoundingBox.isNull())) { crop = Okular::Settings::trimMargins() ? okularPage->boundingBox() : d->trimBoundingBox; // Rotate the bounding box for ( int i = okularPage->rotation(); i > 0; --i ) { Okular::NormalizedRect rot = crop; crop.left = 1 - rot.bottom; crop.top = rot.left; crop.right = 1 - rot.top; crop.bottom = rot.right; } // Expand the crop slightly beyond the bounding box (for Trim Margins only) if (Okular::Settings::trimMargins()) { static const double cropExpandRatio = 0.04; const double cropExpand = cropExpandRatio * ( (crop.right-crop.left) + (crop.bottom-crop.top) ) / 2; crop = Okular::NormalizedRect( crop.left - cropExpand, crop.top - cropExpand, crop.right + cropExpand, crop.bottom + cropExpand ) & Okular::NormalizedRect( 0, 0, 1, 1 ); } // We currently generate a larger image and then crop it, so if the // crop rect is very small the generated image is huge. Hence, we shouldn't // let the crop rect become too small. static double minCropRatio; if (Okular::Settings::trimMargins()) { // Make sure we crop by at most 50% in either dimension: minCropRatio = 0.5; } else { // Looser Constraint for "Trim Selection" minCropRatio = 0.20; } if ( ( crop.right - crop.left ) < minCropRatio ) { const double newLeft = ( crop.left + crop.right ) / 2 - minCropRatio/2; crop.left = qMax( 0.0, qMin( 1.0 - minCropRatio, newLeft ) ); crop.right = crop.left + minCropRatio; } if ( ( crop.bottom - crop.top ) < minCropRatio ) { const double newTop = ( crop.top + crop.bottom ) / 2 - minCropRatio/2; crop.top = qMax( 0.0, qMin( 1.0 - minCropRatio, newTop ) ); crop.bottom = crop.top + minCropRatio; } width *= ( crop.right - crop.left ); height *= ( crop.bottom - crop.top ); #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << "Cropped page" << okularPage->number() << "to" << crop << "width" << width << "height" << height << "by bbox" << okularPage->boundingBox(); #endif } if ( d->zoomMode == ZoomFixed ) { width *= zoom; height *= zoom; item->setWHZC( (int)width, (int)height, d->zoomFactor, crop ); } else if ( d->zoomMode == ZoomFitWidth ) { height = ( height / width ) * colWidth; zoom = (double)colWidth / width; item->setWHZC( colWidth, (int)height, zoom, crop ); if ((uint)item->pageNumber() == d->document->currentPage()) d->zoomFactor = zoom; } else if ( d->zoomMode == ZoomFitPage ) { const double scaleW = (double)colWidth / (double)width; const double scaleH = (double)rowHeight / (double)height; zoom = qMin( scaleW, scaleH ); item->setWHZC( (int)(zoom * width), (int)(zoom * height), zoom, crop ); if ((uint)item->pageNumber() == d->document->currentPage()) d->zoomFactor = zoom; } else if ( d->zoomMode == ZoomFitAuto ) { const double aspectRatioRelation = 1.25; // relation between aspect ratios for "auto fit" const double uiAspect = (double)rowHeight / (double)colWidth; const double pageAspect = (double)height / (double)width; const double rel = uiAspect / pageAspect; const bool isContinuous = Okular::Settings::viewContinuous(); if ( !isContinuous && rel > aspectRatioRelation ) { // UI space is relatively much higher than the page zoom = (double)rowHeight / (double)height; } else if ( rel < 1.0 / aspectRatioRelation ) { // UI space is relatively much wider than the page in relation zoom = (double)colWidth / (double)width; } else { // aspect ratios of page and UI space are very similar const double scaleW = (double)colWidth / (double)width; const double scaleH = (double)rowHeight / (double)height; zoom = qMin( scaleW, scaleH ); } item->setWHZC( (int)(zoom * width), (int)(zoom * height), zoom, crop ); if ((uint)item->pageNumber() == d->document->currentPage()) d->zoomFactor = zoom; } #ifndef NDEBUG else qCDebug(OkularUiDebug) << "calling updateItemSize with unrecognized d->zoomMode!"; #endif } PageViewItem * PageView::pickItemOnPoint( int x, int y ) { PageViewItem * item = nullptr; QLinkedList< PageViewItem * >::const_iterator iIt = d->visibleItems.constBegin(), iEnd = d->visibleItems.constEnd(); for ( ; iIt != iEnd; ++iIt ) { PageViewItem * i = *iIt; const QRect & r = i->croppedGeometry(); if ( x < r.right() && x > r.left() && y < r.bottom() ) { if ( y > r.top() ) item = i; break; } } return item; } void PageView::textSelectionClear() { // something to clear if ( !d->pagesWithTextSelection.isEmpty() ) { QSet< int >::ConstIterator it = d->pagesWithTextSelection.constBegin(), itEnd = d->pagesWithTextSelection.constEnd(); for ( ; it != itEnd; ++it ) d->document->setPageTextSelection( *it, nullptr, QColor() ); d->pagesWithTextSelection.clear(); } } void PageView::selectionStart( const QPoint & pos, const QColor & color, bool /*aboveAll*/ ) { selectionClear(); d->mouseSelecting = true; d->mouseSelectionRect.setRect( pos.x(), pos.y(), 1, 1 ); d->mouseSelectionColor = color; // ensures page doesn't scroll if ( d->autoScrollTimer ) { d->scrollIncrement = 0; d->autoScrollTimer->stop(); } } void PageView::scrollPosIntoView( const QPoint & pos ) { if (pos.x() < horizontalScrollBar()->value()) d->dragScrollVector.setX(pos.x() - horizontalScrollBar()->value()); else if (horizontalScrollBar()->value() + viewport()->width() < pos.x()) d->dragScrollVector.setX(pos.x() - horizontalScrollBar()->value() - viewport()->width()); else d->dragScrollVector.setX(0); if (pos.y() < verticalScrollBar()->value()) d->dragScrollVector.setY(pos.y() - verticalScrollBar()->value()); else if (verticalScrollBar()->value() + viewport()->height() < pos.y()) d->dragScrollVector.setY(pos.y() - verticalScrollBar()->value() - viewport()->height()); else d->dragScrollVector.setY(0); if (d->dragScrollVector != QPoint(0, 0)) { if (!d->dragScrollTimer.isActive()) d->dragScrollTimer.start(100); } else d->dragScrollTimer.stop(); } void PageView::updateSelection( const QPoint & pos ) { if ( d->mouseSelecting ) { scrollPosIntoView( pos ); // update the selection rect QRect updateRect = d->mouseSelectionRect; d->mouseSelectionRect.setBottomLeft( pos ); updateRect |= d->mouseSelectionRect; updateRect.translate( -contentAreaPosition() ); viewport()->update( updateRect.adjusted( -1, -2, 2, 1 ) ); } else if ( d->mouseTextSelecting) { scrollPosIntoView( pos ); int first = -1; const QList< Okular::RegularAreaRect * > selections = textSelections( pos, d->mouseSelectPos, first ); QSet< int > pagesWithSelectionSet; for ( int i = 0; i < selections.count(); ++i ) pagesWithSelectionSet.insert( i + first ); const QSet< int > noMoreSelectedPages = d->pagesWithTextSelection - pagesWithSelectionSet; // clear the selection from pages not selected anymore foreach( int p, noMoreSelectedPages ) { d->document->setPageTextSelection( p, nullptr, QColor() ); } // set the new selection for the selected pages foreach( int p, pagesWithSelectionSet ) { d->document->setPageTextSelection( p, selections[ p - first ], palette().color( QPalette::Active, QPalette::Highlight ) ); } d->pagesWithTextSelection = pagesWithSelectionSet; } } static Okular::NormalizedPoint rotateInNormRect( const QPoint &rotated, const QRect &rect, Okular::Rotation rotation ) { Okular::NormalizedPoint ret; switch ( rotation ) { case Okular::Rotation0: ret = Okular::NormalizedPoint( rotated.x(), rotated.y(), rect.width(), rect.height() ); break; case Okular::Rotation90: ret = Okular::NormalizedPoint( rotated.y(), rect.width() - rotated.x(), rect.height(), rect.width() ); break; case Okular::Rotation180: ret = Okular::NormalizedPoint( rect.width() - rotated.x(), rect.height() - rotated.y(), rect.width(), rect.height() ); break; case Okular::Rotation270: ret = Okular::NormalizedPoint( rect.height() - rotated.y(), rotated.x(), rect.height(), rect.width() ); break; } return ret; } Okular::RegularAreaRect * PageView::textSelectionForItem( PageViewItem * item, const QPoint & startPoint, const QPoint & endPoint ) { const QRect & geometry = item->uncroppedGeometry(); Okular::NormalizedPoint startCursor( 0.0, 0.0 ); if ( !startPoint.isNull() ) { startCursor = rotateInNormRect( startPoint, geometry, item->page()->rotation() ); } Okular::NormalizedPoint endCursor( 1.0, 1.0 ); if ( !endPoint.isNull() ) { endCursor = rotateInNormRect( endPoint, geometry, item->page()->rotation() ); } Okular::TextSelection mouseTextSelectionInfo( startCursor, endCursor ); const Okular::Page * okularPage = item->page(); if ( !okularPage->hasTextPage() ) d->document->requestTextPage( okularPage->number() ); Okular::RegularAreaRect * selectionArea = okularPage->textArea( &mouseTextSelectionInfo ); #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug).nospace() << "text areas (" << okularPage->number() << "): " << ( selectionArea ? QString::number( selectionArea->count() ) : "(none)" ); #endif return selectionArea; } void PageView::selectionClear(const ClearMode mode) { QRect updatedRect = d->mouseSelectionRect.normalized().adjusted( -2, -2, 2, 2 ); d->mouseSelecting = false; d->mouseSelectionRect.setCoords( 0, 0, 0, 0 ); d->tableSelectionCols.clear(); d->tableSelectionRows.clear(); d->tableDividersGuessed = false; foreach (const TableSelectionPart &tsp, d->tableSelectionParts) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); // should check whether this is on-screen here? updatedRect = updatedRect.united(selectionPartRect); } if ( mode != ClearOnlyDividers ) { d->tableSelectionParts.clear(); } d->tableSelectionParts.clear(); updatedRect.translate( -contentAreaPosition() ); viewport()->update( updatedRect ); } // const to be used for both zoomFactorFitMode function and slotRelayoutPages. static const int kcolWidthMargin = 6; static const int krowHeightMargin = 12; double PageView::zoomFactorFitMode( ZoomMode mode ) { const int pageCount = d->items.count(); if ( pageCount == 0 ) return 0; const bool facingCentered = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered || (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount == 1); const bool overrideCentering = facingCentered && pageCount < 3; const int nCols = overrideCentering ? 1 : viewColumns(); const double colWidth = viewport()->width() / nCols - kcolWidthMargin; const double rowHeight = viewport()->height() - krowHeightMargin; const PageViewItem * currentItem = d->items[ qMax( 0, (int)d->document->currentPage()) ]; // prevent segmentation fault when openning a new document; if ( !currentItem ) return 0; const Okular::Page * okularPage = currentItem->page(); const double width = okularPage->width(), height = okularPage->height(); if ( mode == ZoomFitWidth ) return (double) colWidth / width; if ( mode == ZoomFitPage ) { const double scaleW = (double) colWidth / (double)width; const double scaleH = (double) rowHeight / (double)height; return qMin(scaleW, scaleH); } return 0; } void PageView::updateZoom( ZoomMode newZoomMode ) { if ( newZoomMode == ZoomFixed ) { if ( d->aZoom->currentItem() == 0 ) newZoomMode = ZoomFitWidth; else if ( d->aZoom->currentItem() == 1 ) newZoomMode = ZoomFitPage; else if ( d->aZoom->currentItem() == 2 ) newZoomMode = ZoomFitAuto; } float newFactor = d->zoomFactor; QAction * checkedZoomAction = nullptr; switch ( newZoomMode ) { case ZoomFixed:{ //ZoomFixed case QString z = d->aZoom->currentText(); // kdelibs4 sometimes adds accelerators to actions' text directly :( z.remove (QLatin1Char('&')); z.remove (QLatin1Char('%')); newFactor = QLocale().toDouble( z ) / 100.0; }break; case ZoomIn: case ZoomOut:{ const float zoomFactorFitWidth = zoomFactorFitMode(ZoomFitWidth); const float zoomFactorFitPage = zoomFactorFitMode(ZoomFitPage); QVector zoomValue(15); qCopy(kZoomValues, kZoomValues + 13, zoomValue.begin()); zoomValue[13] = zoomFactorFitWidth; zoomValue[14] = zoomFactorFitPage; qSort(zoomValue.begin(), zoomValue.end()); QVector::iterator i; if ( newZoomMode == ZoomOut ) { if (newFactor <= zoomValue.first()) return; i = qLowerBound(zoomValue.begin(), zoomValue.end(), newFactor) - 1; } else { if (newFactor >= zoomValue.last()) return; i = qUpperBound(zoomValue.begin(), zoomValue.end(), newFactor); } const float tmpFactor = *i; if ( tmpFactor == zoomFactorFitWidth ) { newZoomMode = ZoomFitWidth; checkedZoomAction = d->aZoomFitWidth; } else if ( tmpFactor == zoomFactorFitPage ) { newZoomMode = ZoomFitPage; checkedZoomAction = d->aZoomFitPage; } else { newFactor = tmpFactor; newZoomMode = ZoomFixed; } } break; case ZoomFitWidth: checkedZoomAction = d->aZoomFitWidth; break; case ZoomFitPage: checkedZoomAction = d->aZoomFitPage; break; case ZoomFitAuto: checkedZoomAction = d->aZoomAutoFit; break; case ZoomRefreshCurrent: newZoomMode = ZoomFixed; d->zoomFactor = -1; break; } const float upperZoomLimit = d->document->supportsTiles() ? 16.0 : 4.0; if ( newFactor > upperZoomLimit ) newFactor = upperZoomLimit; if ( newFactor < 0.1 ) newFactor = 0.1; if ( newZoomMode != d->zoomMode || (newZoomMode == ZoomFixed && newFactor != d->zoomFactor ) ) { // rebuild layout and update the whole viewport d->zoomMode = newZoomMode; d->zoomFactor = newFactor; // be sure to block updates to document's viewport bool prevState = d->blockViewport; d->blockViewport = true; slotRelayoutPages(); d->blockViewport = prevState; // request pixmaps slotRequestVisiblePixmaps(); // update zoom text updateZoomText(); // update actions checked state if ( d->aZoomFitWidth ) { d->aZoomFitWidth->setChecked( checkedZoomAction == d->aZoomFitWidth ); d->aZoomFitPage->setChecked( checkedZoomAction == d->aZoomFitPage ); d->aZoomAutoFit->setChecked( checkedZoomAction == d->aZoomAutoFit ); } } else if ( newZoomMode == ZoomFixed && newFactor == d->zoomFactor ) updateZoomText(); d->aZoomIn->setEnabled( d->zoomFactor < upperZoomLimit-0.001 ); d->aZoomOut->setEnabled( d->zoomFactor > 0.101 ); } void PageView::updateZoomText() { // use current page zoom as zoomFactor if in ZoomFit/* mode if ( d->zoomMode != ZoomFixed && d->items.count() > 0 ) d->zoomFactor = d->items[ qMax( 0, (int)d->document->currentPage() ) ]->zoomFactor(); float newFactor = d->zoomFactor; d->aZoom->removeAllActions(); // add items that describe fit actions QStringList translated; translated << i18n("Fit Width") << i18n("Fit Page") << i18n("Auto Fit"); // add percent items int idx = 0, selIdx = 3; bool inserted = false; //use: "d->zoomMode != ZoomFixed" to hide Fit/* zoom ratio int zoomValueCount = 11; if ( d->document->supportsTiles() ) zoomValueCount = 13; while ( idx < zoomValueCount || !inserted ) { float value = idx < zoomValueCount ? kZoomValues[ idx ] : newFactor; if ( !inserted && newFactor < (value - 0.0001) ) value = newFactor; else idx ++; if ( value > (newFactor - 0.0001) && value < (newFactor + 0.0001) ) inserted = true; if ( !inserted ) selIdx++; // we do not need to display 2-digit precision QString localValue( QLocale().toString( value * 100.0, 'f', 1 ) ); localValue.remove( QLocale().decimalPoint() + QLatin1Char('0') ); // remove a trailing zero in numbers like 66.70 if ( localValue.right( 1 ) == QLatin1String( "0" ) && localValue.indexOf( QLocale().decimalPoint() ) > -1 ) localValue.chop( 1 ); translated << QStringLiteral( "%1%" ).arg( localValue ); } d->aZoom->setItems( translated ); // select current item in list if ( d->zoomMode == ZoomFitWidth ) selIdx = 0; else if ( d->zoomMode == ZoomFitPage ) selIdx = 1; else if ( d->zoomMode == ZoomFitAuto ) selIdx = 2; // we have to temporarily enable the actions as otherwise we can't set a new current item d->aZoom->setEnabled( true ); d->aZoom->selectableActionGroup()->setEnabled( true ); d->aZoom->setCurrentItem( selIdx ); d->aZoom->setEnabled( d->items.size() > 0 ); d->aZoom->selectableActionGroup()->setEnabled( d->items.size() > 0 ); } void PageView::updateCursor() { const QPoint p = contentAreaPosition() + viewport()->mapFromGlobal( QCursor::pos() ); updateCursor( p ); } void PageView::updateCursor( const QPoint &p ) { // reset mouse over link it will be re-set if that still valid d->mouseOverLinkObject = nullptr; // detect the underlaying page (if present) PageViewItem * pageItem = pickItemOnPoint( p.x(), p.y() ); if ( d->annotator && d->annotator->active() ) { if ( pageItem || d->annotator->annotating() ) setCursor( d->annotator->cursor() ); else setCursor( Qt::ForbiddenCursor ); } else if ( pageItem ) { double nX = pageItem->absToPageX(p.x()); double nY = pageItem->absToPageY(p.y()); Qt::CursorShape cursorShapeFallback; // if over a ObjectRect (of type Link) change cursor to hand switch ( d->mouseMode ) { case Okular::Settings::EnumMouseMode::TextSelect: if (d->mouseTextSelecting) { setCursor( Qt::IBeamCursor ); return; } cursorShapeFallback = Qt::IBeamCursor; break; case Okular::Settings::EnumMouseMode::Magnifier: setCursor( Qt::CrossCursor ); return; case Okular::Settings::EnumMouseMode::RectSelect: case Okular::Settings::EnumMouseMode::TrimSelect: if (d->mouseSelecting) { setCursor( Qt::CrossCursor ); return; } cursorShapeFallback = Qt::CrossCursor; break; case Okular::Settings::EnumMouseMode::Browse: d->mouseOnRect = false; if ( d->mouseAnnotation->isMouseOver() ) { d->mouseOnRect = true; setCursor( d->mouseAnnotation->cursor() ); return; } else { cursorShapeFallback = Qt::OpenHandCursor; } break; default: setCursor( Qt::ArrowCursor ); return; } const Okular::ObjectRect * linkobj = pageItem->page()->objectRect( Okular::ObjectRect::Action, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); if ( linkobj ) { d->mouseOverLinkObject = linkobj; d->mouseOnRect = true; setCursor( Qt::PointingHandCursor ); } else { setCursor(cursorShapeFallback); } } else { // if there's no page over the cursor and we were showing the pointingHandCursor // go back to the normal one d->mouseOnRect = false; setCursor( Qt::ArrowCursor ); } } void PageView::reloadForms() { QLinkedList< PageViewItem * >::const_iterator iIt = d->visibleItems.constBegin(), iEnd = d->visibleItems.constEnd(); if( d->m_formsVisible ) { for ( ; iIt != iEnd; ++iIt ) { (*iIt)->reloadFormWidgetsState(); } } } void PageView::moveMagnifier( const QPoint& p ) // non scaled point { const int w = d->magnifierView->width() * 0.5; const int h = d->magnifierView->height() * 0.5; int x = p.x() - w; int y = p.y() - h; const int max_x = viewport()->width(); const int max_y = viewport()->height(); QPoint scroll(0,0); if (x < 0) { if (horizontalScrollBar()->value() > 0) scroll.setX(x - w); x = 0; } if (y < 0) { if (verticalScrollBar()->value() > 0) scroll.setY(y - h); y = 0; } if (p.x() + w > max_x) { if (horizontalScrollBar()->value() < horizontalScrollBar()->maximum()) scroll.setX(p.x() + 2 * w - max_x); x = max_x - d->magnifierView->width() - 1; } if (p.y() + h > max_y) { if (verticalScrollBar()->value() < verticalScrollBar()->maximum()) scroll.setY(p.y() + 2 * h - max_y); y = max_y - d->magnifierView->height() - 1; } if (!scroll.isNull()) scrollPosIntoView(contentAreaPoint(p + scroll)); d->magnifierView->move(x, y); } void PageView::updateMagnifier( const QPoint& p ) // scaled point { /* translate mouse coordinates to page coordinates and inform the magnifier of the situation */ PageViewItem *item = pickItemOnPoint(p.x(), p.y()); if (item) { Okular::NormalizedPoint np(item->absToPageX(p.x()), item->absToPageY(p.y())); d->magnifierView->updateView( np, item->page() ); } } int PageView::viewColumns() const { int vm = Okular::Settings::viewMode(); if (vm == Okular::Settings::EnumViewMode::Single) return 1; else if (vm == Okular::Settings::EnumViewMode::Facing || vm == Okular::Settings::EnumViewMode::FacingFirstCentered) return 2; else if (vm == Okular::Settings::EnumViewMode::Summary && d->document->pages() < Okular::Settings::viewColumns() ) return d->document->pages(); else return Okular::Settings::viewColumns(); } void PageView::center(int cx, int cy) { scrollTo( cx - viewport()->width() / 2, cy - viewport()->height() / 2 ); } void PageView::scrollTo( int x, int y ) { bool prevState = d->blockPixmapsRequest; int newValue = -1; if ( x != horizontalScrollBar()->value() || y != verticalScrollBar()->value() ) newValue = 1; // Pretend this call is the result of a scrollbar event d->blockPixmapsRequest = true; horizontalScrollBar()->setValue( x ); verticalScrollBar()->setValue( y ); d->blockPixmapsRequest = prevState; slotRequestVisiblePixmaps( newValue ); } void PageView::toggleFormWidgets( bool on ) { bool somehadfocus = false; QVector< PageViewItem * >::const_iterator dIt = d->items.constBegin(), dEnd = d->items.constEnd(); for ( ; dIt != dEnd; ++dIt ) { bool hadfocus = (*dIt)->setFormWidgetsVisible( on ); somehadfocus = somehadfocus || hadfocus; } if ( somehadfocus ) setFocus(); d->m_formsVisible = on; if ( d->aToggleForms ) // it may not exist if we are on dummy mode { if ( d->m_formsVisible ) { d->aToggleForms->setText( i18n( "Hide Forms" ) ); } else { d->aToggleForms->setText( i18n( "Show Forms" ) ); } } } void PageView::resizeContentArea( const QSize & newSize ) { const QSize vs = viewport()->size(); int hRange = newSize.width() - vs.width(); int vRange = newSize.height() - vs.height(); if ( horizontalScrollBar()->isVisible() && hRange == verticalScrollBar()->width() && verticalScrollBar()->isVisible() && vRange == horizontalScrollBar()->height() && Okular::Settings::showScrollBars() ) { hRange = 0; vRange = 0; } horizontalScrollBar()->setRange( 0, hRange ); verticalScrollBar()->setRange( 0, vRange ); updatePageStep(); } void PageView::updatePageStep() { const QSize vs = viewport()->size(); horizontalScrollBar()->setPageStep( vs.width() ); verticalScrollBar()->setPageStep( vs.height() * (100 - Okular::Settings::scrollOverlap()) / 100 ); } void PageView::addWebShortcutsMenu( QMenu * menu, const QString & text ) { if ( text.isEmpty() ) { return; } QString searchText = text; searchText = searchText.replace( QLatin1Char('\n'), QLatin1Char(' ') ).replace(QLatin1Char( '\r'), QLatin1Char(' ') ).simplified(); if ( searchText.isEmpty() ) { return; } KUriFilterData filterData( searchText ); filterData.setSearchFilteringOptions( KUriFilterData::RetrievePreferredSearchProvidersOnly ); if ( KUriFilter::self()->filterSearchUri( filterData, KUriFilter::NormalTextFilter ) ) { const QStringList searchProviders = filterData.preferredSearchProviders(); if ( !searchProviders.isEmpty() ) { QMenu *webShortcutsMenu = new QMenu( menu ); webShortcutsMenu->setIcon( QIcon::fromTheme( QStringLiteral("preferences-web-browser-shortcuts") ) ); const QString squeezedText = KStringHandler::rsqueeze( searchText, 21 ); webShortcutsMenu->setTitle( i18n( "Search for '%1' with", squeezedText ) ); QAction *action = nullptr; foreach( const QString &searchProvider, searchProviders ) { action = new QAction( searchProvider, webShortcutsMenu ); action->setIcon( QIcon::fromTheme( filterData.iconNameForPreferredSearchProvider( searchProvider ) ) ); action->setData( filterData.queryForPreferredSearchProvider( searchProvider ) ); connect( action, &QAction::triggered, this, &PageView::slotHandleWebShortcutAction ); webShortcutsMenu->addAction( action ); } webShortcutsMenu->addSeparator(); action = new QAction( i18n( "Configure Web Shortcuts..." ), webShortcutsMenu ); action->setIcon( QIcon::fromTheme( QStringLiteral("configure") ) ); connect( action, &QAction::triggered, this, &PageView::slotConfigureWebShortcuts ); webShortcutsMenu->addAction( action ); menu->addMenu(webShortcutsMenu); } } } QMenu* PageView::createProcessLinkMenu(PageViewItem *item, const QPoint &eventPos) { // check if the right-click was over a link const double nX = item->absToPageX(eventPos.x()); const double nY = item->absToPageY(eventPos.y()); const Okular::ObjectRect * rect = item->page()->objectRect( Okular::ObjectRect::Action, nX, nY, item->uncroppedWidth(), item->uncroppedHeight() ); if ( rect ) { QMenu *menu = new QMenu(this); const Okular::Action * link = static_cast< const Okular::Action * >( rect->object() ); // creating the menu and its actions QAction * processLink = menu->addAction( i18n( "Follow This Link" ) ); processLink->setObjectName("ProcessLinkAction"); if ( link->actionType() == Okular::Action::Sound ) { processLink->setText( i18n( "Play this Sound" ) ); if ( Okular::AudioPlayer::instance()->state() == Okular::AudioPlayer::PlayingState ) { QAction * actStopSound = menu->addAction( i18n( "Stop Sound" ) ); connect( actStopSound, &QAction::triggered, []() { Okular::AudioPlayer::instance()->stopPlaybacks(); }); } } if ( dynamic_cast< const Okular::BrowseAction * >( link ) ) { QAction * actCopyLinkLocation = menu->addAction( QIcon::fromTheme( QStringLiteral("edit-copy") ), i18n( "Copy Link Address" ) ); actCopyLinkLocation->setObjectName("CopyLinkLocationAction"); connect( actCopyLinkLocation, &QAction::triggered, [ link ]() { const Okular::BrowseAction * browseLink = static_cast< const Okular::BrowseAction * >( link ); QClipboard *cb = QApplication::clipboard(); cb->setText( browseLink->url().toDisplayString(), QClipboard::Clipboard ); if ( cb->supportsSelection() ) cb->setText( browseLink->url().toDisplayString(), QClipboard::Selection ); } ); } connect( processLink, &QAction::triggered, [this, link]() { d->document->processAction( link ); }); return menu; } return nullptr; } //BEGIN private SLOTS void PageView::slotRelayoutPages() // called by: notifySetup, viewportResizeEvent, slotViewMode, slotContinuousToggled, updateZoom { // set an empty container if we have no pages const int pageCount = d->items.count(); if ( pageCount < 1 ) { return; } // if viewport was auto-moving, stop it if ( d->viewportMoveActive ) { center( d->viewportMoveDest.x(), d->viewportMoveDest.y() ); d->viewportMoveActive = false; d->viewportMoveTimer->stop(); verticalScrollBar()->setEnabled( true ); horizontalScrollBar()->setEnabled( true ); } // common iterator used in this method and viewport parameters QVector< PageViewItem * >::const_iterator iIt, iEnd = d->items.constEnd(); int viewportWidth = viewport()->width(), viewportHeight = viewport()->height(), fullWidth = 0, fullHeight = 0; QRect viewportRect( horizontalScrollBar()->value(), verticalScrollBar()->value(), viewportWidth, viewportHeight ); // handle the 'center first page in row' stuff const bool facing = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount > 1; const bool facingCentered = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered || (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount == 1); const bool overrideCentering = facingCentered && pageCount < 3; const bool centerFirstPage = facingCentered && !overrideCentering; const bool facingPages = facing || centerFirstPage; const bool centerLastPage = centerFirstPage && pageCount % 2 == 0; const bool continuousView = Okular::Settings::viewContinuous(); const int nCols = overrideCentering ? 1 : viewColumns(); const bool singlePageViewMode = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Single; if ( d->aFitWindowToPage ) d->aFitWindowToPage->setEnabled( !continuousView && singlePageViewMode ); // set all items geometry and resize contents. handle 'continuous' and 'single' modes separately PageViewItem * currentItem = d->items[ qMax( 0, (int)d->document->currentPage() ) ]; // Here we find out column's width and row's height to compute a table // so we can place widgets 'centered in virtual cells'. const int nRows = (int)ceil( (float)(centerFirstPage ? (pageCount + nCols - 1) : pageCount) / (float)nCols ); int * colWidth = new int[ nCols ], * rowHeight = new int[ nRows ], cIdx = 0, rIdx = 0; for ( int i = 0; i < nCols; i++ ) colWidth[ i ] = viewportWidth / nCols; for ( int i = 0; i < nRows; i++ ) rowHeight[ i ] = 0; // handle the 'centering on first row' stuff if ( centerFirstPage ) cIdx += nCols - 1; // 1) find the maximum columns width and rows height for a grid in // which each page must well-fit inside a cell for ( iIt = d->items.constBegin(); iIt != iEnd; ++iIt ) { PageViewItem * item = *iIt; // update internal page size (leaving a little margin in case of Fit* modes) updateItemSize( item, colWidth[ cIdx ] - kcolWidthMargin, viewportHeight - krowHeightMargin ); // find row's maximum height and column's max width if ( item->croppedWidth() + kcolWidthMargin > colWidth[ cIdx ] ) colWidth[ cIdx ] = item->croppedWidth() + kcolWidthMargin; if ( item->croppedHeight() + krowHeightMargin > rowHeight[ rIdx ] ) rowHeight[ rIdx ] = item->croppedHeight() + krowHeightMargin; // handle the 'centering on first row' stuff // update col/row indices if ( ++cIdx == nCols ) { cIdx = 0; rIdx++; } } const int pageRowIdx = ( ( centerFirstPage ? nCols - 1 : 0 ) + currentItem->pageNumber() ) / nCols; // 2) compute full size for ( int i = 0; i < nCols; i++ ) fullWidth += colWidth[ i ]; if ( continuousView ) { for ( int i = 0; i < nRows; i++ ) fullHeight += rowHeight[ i ]; } else fullHeight = rowHeight[ pageRowIdx ]; // 3) arrange widgets inside cells (and refine fullHeight if needed) int insertX = 0, insertY = fullHeight < viewportHeight ? ( viewportHeight - fullHeight ) / 2 : 0; const int origInsertY = insertY; cIdx = 0; rIdx = 0; if ( centerFirstPage ) { cIdx += nCols - 1; for ( int i = 0; i < cIdx; ++i ) insertX += colWidth[ i ]; } for ( iIt = d->items.constBegin(); iIt != iEnd; ++iIt ) { PageViewItem * item = *iIt; int cWidth = colWidth[ cIdx ], rHeight = rowHeight[ rIdx ]; if ( continuousView || rIdx == pageRowIdx ) { const bool reallyDoCenterFirst = item->pageNumber() == 0 && centerFirstPage; const bool reallyDoCenterLast = item->pageNumber() == pageCount - 1 && centerLastPage; int actualX = 0; if ( reallyDoCenterFirst || reallyDoCenterLast ) { // page is centered across entire viewport actualX = (fullWidth - item->croppedWidth()) / 2; } else if ( facingPages ) { if (Okular::Settings::rtlReadingDirection()){ // RTL reading mode actualX = ( (centerFirstPage && item->pageNumber() % 2 == 0) || (!centerFirstPage && item->pageNumber() % 2 == 1) ) ? (fullWidth / 2) - item->croppedWidth() - 1 : (fullWidth / 2) + 1; } else { // page edges 'touch' the center of the viewport actualX = ( (centerFirstPage && item->pageNumber() % 2 == 1) || (!centerFirstPage && item->pageNumber() % 2 == 0) ) ? (fullWidth / 2) - item->croppedWidth() - 1 : (fullWidth / 2) + 1; } } else { // page is centered within its virtual column //actualX = insertX + (cWidth - item->croppedWidth()) / 2; if (Okular::Settings::rtlReadingDirection()){ actualX = fullWidth - insertX - cWidth +( (cWidth - item->croppedWidth()) / 2); } else { actualX = insertX + (cWidth - item->croppedWidth()) / 2; } } item->moveTo( actualX, (continuousView ? insertY : origInsertY) + (rHeight - item->croppedHeight()) / 2 ); item->setVisible( true ); } else { item->moveTo( 0, 0 ); item->setVisible( false ); } item->setFormWidgetsVisible( d->m_formsVisible ); // advance col/row index insertX += cWidth; if ( ++cIdx == nCols ) { cIdx = 0; rIdx++; insertX = 0; insertY += rHeight; } #ifdef PAGEVIEW_DEBUG kWarning() << "updating size for pageno" << item->pageNumber() << "cropped" << item->croppedGeometry() << "uncropped" << item->uncroppedGeometry(); #endif } delete [] colWidth; delete [] rowHeight; // 3) reset dirty state d->dirtyLayout = false; // 4) update scrollview's contents size and recenter view bool wasUpdatesEnabled = viewport()->updatesEnabled(); if ( fullWidth != contentAreaWidth() || fullHeight != contentAreaHeight() ) { const Okular::DocumentViewport vp = d->document->viewport(); // disable updates and resize the viewportContents if ( wasUpdatesEnabled ) viewport()->setUpdatesEnabled( false ); resizeContentArea( QSize( fullWidth, fullHeight ) ); // restore previous viewport if defined and updates enabled if ( wasUpdatesEnabled ) { if ( vp.pageNumber >= 0 ) { int prevX = horizontalScrollBar()->value(), prevY = verticalScrollBar()->value(); const QRect & geometry = d->items[ vp.pageNumber ]->croppedGeometry(); double nX = vp.rePos.enabled ? normClamp( vp.rePos.normalizedX, 0.5 ) : 0.5, nY = vp.rePos.enabled ? normClamp( vp.rePos.normalizedY, 0.0 ) : 0.0; center( geometry.left() + qRound( nX * (double)geometry.width() ), geometry.top() + qRound( nY * (double)geometry.height() ) ); // center() usually moves the viewport, that requests pixmaps too. // if that doesn't happen we have to request them by hand if ( prevX == horizontalScrollBar()->value() && prevY == verticalScrollBar()->value() ) slotRequestVisiblePixmaps(); } // or else go to center page else center( fullWidth / 2, 0 ); viewport()->setUpdatesEnabled( true ); } } // 5) update the whole viewport if updated enabled if ( wasUpdatesEnabled ) viewport()->update(); } void PageView::delayedResizeEvent() { // If we already got here we don't need to execute the timer slot again d->delayResizeEventTimer->stop(); slotRelayoutPages(); slotRequestVisiblePixmaps(); } static void slotRequestPreloadPixmap( Okular::DocumentObserver * observer, const PageViewItem * i, const QRect &expandedViewportRect, QLinkedList< Okular::PixmapRequest * > *requestedPixmaps ) { Okular::NormalizedRect preRenderRegion; const QRect intersectionRect = expandedViewportRect.intersected( i->croppedGeometry() ); if ( !intersectionRect.isEmpty() ) preRenderRegion = Okular::NormalizedRect( intersectionRect.translated( -i->uncroppedGeometry().topLeft() ), i->uncroppedWidth(), i->uncroppedHeight() ); // request the pixmap if not already present if ( !i->page()->hasPixmap( observer, i->uncroppedWidth(), i->uncroppedHeight(), preRenderRegion ) && i->uncroppedWidth() > 0 ) { Okular::PixmapRequest::PixmapRequestFeatures requestFeatures = Okular::PixmapRequest::Preload; requestFeatures |= Okular::PixmapRequest::Asynchronous; const bool pageHasTilesManager = i->page()->hasTilesManager( observer ); if ( pageHasTilesManager && !preRenderRegion.isNull() ) { Okular::PixmapRequest * p = new Okular::PixmapRequest( observer, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), PAGEVIEW_PRELOAD_PRIO, requestFeatures ); requestedPixmaps->push_back( p ); p->setNormalizedRect( preRenderRegion ); p->setTile( true ); } else if ( !pageHasTilesManager ) { Okular::PixmapRequest * p = new Okular::PixmapRequest( observer, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), PAGEVIEW_PRELOAD_PRIO, requestFeatures ); requestedPixmaps->push_back( p ); p->setNormalizedRect( preRenderRegion ); } } } void PageView::slotRequestVisiblePixmaps( int newValue ) { // if requests are blocked (because raised by an unwanted event), exit if ( d->blockPixmapsRequest || d->viewportMoveActive ) return; // precalc view limits for intersecting with page coords inside the loop const bool isEvent = newValue != -1 && !d->blockViewport; const QRect viewportRect( horizontalScrollBar()->value(), verticalScrollBar()->value(), viewport()->width(), viewport()->height() ); const QRect viewportRectAtZeroZero( 0, 0, viewport()->width(), viewport()->height() ); // some variables used to determine the viewport int nearPageNumber = -1; const double viewportCenterX = (viewportRect.left() + viewportRect.right()) / 2.0; const double viewportCenterY = (viewportRect.top() + viewportRect.bottom()) / 2.0; double focusedX = 0.5, focusedY = 0.0, minDistance = -1.0; // Margin (in pixels) around the viewport to preload const int pixelsToExpand = 512; // iterate over all items d->visibleItems.clear(); QLinkedList< Okular::PixmapRequest * > requestedPixmaps; QVector< Okular::VisiblePageRect * > visibleRects; QVector< PageViewItem * >::const_iterator iIt = d->items.constBegin(), iEnd = d->items.constEnd(); for ( ; iIt != iEnd; ++iIt ) { PageViewItem * i = *iIt; foreach( FormWidgetIface *fwi, i->formWidgets() ) { Okular::NormalizedRect r = fwi->rect(); fwi->moveTo( qRound( i->uncroppedGeometry().left() + i->uncroppedWidth() * r.left ) + 1 - viewportRect.left(), qRound( i->uncroppedGeometry().top() + i->uncroppedHeight() * r.top ) + 1 - viewportRect.top() ); } Q_FOREACH ( VideoWidget *vw, i->videoWidgets() ) { const Okular::NormalizedRect r = vw->normGeometry(); vw->move( qRound( i->uncroppedGeometry().left() + i->uncroppedWidth() * r.left ) + 1 - viewportRect.left(), qRound( i->uncroppedGeometry().top() + i->uncroppedHeight() * r.top ) + 1 - viewportRect.top() ); if ( vw->isPlaying() && viewportRectAtZeroZero.intersected( vw->geometry() ).isEmpty() ) { vw->stop(); vw->pageLeft(); } } if ( !i->isVisible() ) continue; #ifdef PAGEVIEW_DEBUG kWarning() << "checking page" << i->pageNumber(); kWarning().nospace() << "viewportRect is " << viewportRect << ", page item is " << i->croppedGeometry() << " intersect : " << viewportRect.intersects( i->croppedGeometry() ); #endif // if the item doesn't intersect the viewport, skip it QRect intersectionRect = viewportRect.intersected( i->croppedGeometry() ); if ( intersectionRect.isEmpty() ) { continue; } // add the item to the 'visible list' d->visibleItems.push_back( i ); Okular::VisiblePageRect * vItem = new Okular::VisiblePageRect( i->pageNumber(), Okular::NormalizedRect( intersectionRect.translated( -i->uncroppedGeometry().topLeft() ), i->uncroppedWidth(), i->uncroppedHeight() ) ); visibleRects.push_back( vItem ); #ifdef PAGEVIEW_DEBUG kWarning() << "checking for pixmap for page" << i->pageNumber() << "=" << i->page()->hasPixmap( this, i->uncroppedWidth(), i->uncroppedHeight() ); kWarning() << "checking for text for page" << i->pageNumber() << "=" << i->page()->hasTextPage(); #endif Okular::NormalizedRect expandedVisibleRect = vItem->rect; if ( i->page()->hasTilesManager( this ) && Okular::Settings::memoryLevel() != Okular::Settings::EnumMemoryLevel::Low ) { double rectMargin = pixelsToExpand/(double)i->uncroppedHeight(); expandedVisibleRect.left = qMax( 0.0, vItem->rect.left - rectMargin ); expandedVisibleRect.top = qMax( 0.0, vItem->rect.top - rectMargin ); expandedVisibleRect.right = qMin( 1.0, vItem->rect.right + rectMargin ); expandedVisibleRect.bottom = qMin( 1.0, vItem->rect.bottom + rectMargin ); } // if the item has not the right pixmap, add a request for it if ( !i->page()->hasPixmap( this, i->uncroppedWidth(), i->uncroppedHeight(), expandedVisibleRect ) ) { #ifdef PAGEVIEW_DEBUG kWarning() << "rerequesting visible pixmaps for page" << i->pageNumber() << "!"; #endif Okular::PixmapRequest * p = new Okular::PixmapRequest( this, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), PAGEVIEW_PRIO, Okular::PixmapRequest::Asynchronous ); requestedPixmaps.push_back( p ); if ( i->page()->hasTilesManager( this ) ) { p->setNormalizedRect( expandedVisibleRect ); p->setTile( true ); } else p->setNormalizedRect( vItem->rect ); } // look for the item closest to viewport center and the relative // position between the item and the viewport center if ( isEvent ) { const QRect & geometry = i->croppedGeometry(); // compute distance between item center and viewport center (slightly moved left) double distance = hypot( (geometry.left() + geometry.right()) / 2 - (viewportCenterX - 4), (geometry.top() + geometry.bottom()) / 2 - viewportCenterY ); if ( distance >= minDistance && nearPageNumber != -1 ) continue; nearPageNumber = i->pageNumber(); minDistance = distance; if ( geometry.height() > 0 && geometry.width() > 0 ) { focusedX = ( viewportCenterX - (double)geometry.left() ) / (double)geometry.width(); focusedY = ( viewportCenterY - (double)geometry.top() ) / (double)geometry.height(); } } } // if preloading is enabled, add the pages before and after in preloading if ( !d->visibleItems.isEmpty() && Okular::SettingsCore::memoryLevel() != Okular::SettingsCore::EnumMemoryLevel::Low ) { // as the requests are done in the order as they appear in the list, // request first the next page and then the previous int pagesToPreload = viewColumns(); // if the greedy option is set, preload all pages if (Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Greedy) pagesToPreload = d->items.count(); const QRect expandedViewportRect = viewportRect.adjusted( 0, -pixelsToExpand, 0, pixelsToExpand ); for( int j = 1; j <= pagesToPreload; j++ ) { // add the page after the 'visible series' in preload const int tailRequest = d->visibleItems.last()->pageNumber() + j; if ( tailRequest < (int)d->items.count() ) { slotRequestPreloadPixmap( this, d->items[ tailRequest ], expandedViewportRect, &requestedPixmaps ); } // add the page before the 'visible series' in preload const int headRequest = d->visibleItems.first()->pageNumber() - j; if ( headRequest >= 0 ) { slotRequestPreloadPixmap( this, d->items[ headRequest ], expandedViewportRect, &requestedPixmaps ); } // stop if we've already reached both ends of the document if ( headRequest < 0 && tailRequest >= (int)d->items.count() ) break; } } // send requests to the document if ( !requestedPixmaps.isEmpty() ) { d->document->requestPixmaps( requestedPixmaps ); } // if this functions was invoked by viewport events, send update to document if ( isEvent && nearPageNumber != -1 ) { // determine the document viewport Okular::DocumentViewport newViewport( nearPageNumber ); newViewport.rePos.enabled = true; newViewport.rePos.normalizedX = focusedX; newViewport.rePos.normalizedY = focusedY; // set the viewport to other observers d->document->setViewport( newViewport , this ); } d->document->setVisiblePageRects( visibleRects, this ); } void PageView::slotMoveViewport() { // converge to viewportMoveDest in 1 second int diffTime = d->viewportMoveTime.elapsed(); if ( diffTime >= 667 || !d->viewportMoveActive ) { center( d->viewportMoveDest.x(), d->viewportMoveDest.y() ); d->viewportMoveTimer->stop(); d->viewportMoveActive = false; slotRequestVisiblePixmaps(); verticalScrollBar()->setEnabled( true ); horizontalScrollBar()->setEnabled( true ); return; } // move the viewport smoothly (kmplot: p(x)=1+0.47*(x-1)^3-0.25*(x-1)^4) float convergeSpeed = (float)diffTime / 667.0, x = ((float)viewport()->width() / 2.0) + horizontalScrollBar()->value(), y = ((float)viewport()->height() / 2.0) + verticalScrollBar()->value(), diffX = (float)d->viewportMoveDest.x() - x, diffY = (float)d->viewportMoveDest.y() - y; convergeSpeed *= convergeSpeed * (1.4 - convergeSpeed); center( (int)(x + diffX * convergeSpeed), (int)(y + diffY * convergeSpeed ) ); } void PageView::slotAutoScroll() { // the first time create the timer if ( !d->autoScrollTimer ) { d->autoScrollTimer = new QTimer( this ); d->autoScrollTimer->setSingleShot( true ); connect( d->autoScrollTimer, &QTimer::timeout, this, &PageView::slotAutoScroll ); } // if scrollIncrement is zero, stop the timer if ( !d->scrollIncrement ) { d->autoScrollTimer->stop(); return; } // compute delay between timer ticks and scroll amount per tick int index = abs( d->scrollIncrement ) - 1; // 0..9 const int scrollDelay[10] = { 200, 100, 50, 30, 20, 30, 25, 20, 30, 20 }; const int scrollOffset[10] = { 1, 1, 1, 1, 1, 2, 2, 2, 4, 4 }; d->autoScrollTimer->start( scrollDelay[ index ] ); int delta = d->scrollIncrement > 0 ? scrollOffset[ index ] : -scrollOffset[ index ]; verticalScrollBar()->setValue(verticalScrollBar()->value() + delta); } void PageView::slotDragScroll() { scrollTo( horizontalScrollBar()->value() + d->dragScrollVector.x(), verticalScrollBar()->value() + d->dragScrollVector.y() ); QPoint p = contentAreaPosition() + viewport()->mapFromGlobal( QCursor::pos() ); updateSelection( p ); } void PageView::slotShowWelcome() { // show initial welcome text d->messageWindow->display( i18n( "Welcome" ), QString(), PageViewMessage::Info, 2000 ); } void PageView::slotShowSizeAllCursor() { setCursor( Qt::SizeAllCursor ); } void PageView::slotHandleWebShortcutAction() { QAction *action = qobject_cast( sender() ); if (action) { KUriFilterData filterData( action->data().toString() ); if ( KUriFilter::self()->filterSearchUri( filterData, KUriFilter::WebShortcutFilter ) ) { QDesktopServices::openUrl( filterData.uri() ); } } } void PageView::slotConfigureWebShortcuts() { KToolInvocation::kdeinitExec( QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("webshortcuts") ); } void PageView::slotZoom() { if ( !d->aZoom->selectableActionGroup()->isEnabled() ) return; setFocus(); updateZoom( ZoomFixed ); } void PageView::slotZoomIn() { updateZoom( ZoomIn ); } void PageView::slotZoomOut() { updateZoom( ZoomOut ); } void PageView::slotFitToWidthToggled( bool on ) { if ( on ) updateZoom( ZoomFitWidth ); } void PageView::slotFitToPageToggled( bool on ) { if ( on ) updateZoom( ZoomFitPage ); } void PageView::slotAutoFitToggled( bool on ) { if ( on ) updateZoom( ZoomFitAuto ); } void PageView::slotViewMode( QAction *action ) { const int nr = action->data().toInt(); if ( (int)Okular::Settings::viewMode() != nr ) { Okular::Settings::setViewMode( nr ); Okular::Settings::self()->save(); if ( d->document->pages() > 0 ) slotRelayoutPages(); } } void PageView::slotContinuousToggled( bool on ) { if ( Okular::Settings::viewContinuous() != on ) { Okular::Settings::setViewContinuous( on ); Okular::Settings::self()->save(); if ( d->document->pages() > 0 ) slotRelayoutPages(); } } void PageView::slotSetMouseNormal() { d->mouseMode = Okular::Settings::EnumMouseMode::Browse; Okular::Settings::setMouseMode( d->mouseMode ); // hide the messageWindow d->messageWindow->hide(); // reshow the annotator toolbar if hiding was forced (and if it is not already visible) if ( d->annotator && d->annotator->hidingWasForced() && d->aToggleAnnotator && !d->aToggleAnnotator->isChecked() ) d->aToggleAnnotator->trigger(); // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseZoom() { d->mouseMode = Okular::Settings::EnumMouseMode::Zoom; Okular::Settings::setMouseMode( d->mouseMode ); // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Select zooming area. Right-click to zoom out." ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseMagnifier() { d->mouseMode = Okular::Settings::EnumMouseMode::Magnifier; Okular::Settings::setMouseMode( d->mouseMode ); d->messageWindow->display( i18n( "Click to see the magnified view." ), QString() ); // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseSelect() { d->mouseMode = Okular::Settings::EnumMouseMode::RectSelect; Okular::Settings::setMouseMode( d->mouseMode ); // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Draw a rectangle around the text/graphics to copy." ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseTextSelect() { d->mouseMode = Okular::Settings::EnumMouseMode::TextSelect; Okular::Settings::setMouseMode( d->mouseMode ); // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Select text" ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseTableSelect() { d->mouseMode = Okular::Settings::EnumMouseMode::TableSelect; Okular::Settings::setMouseMode( d->mouseMode ); // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Draw a rectangle around the table, then click near edges to divide up; press Esc to clear." ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotToggleAnnotator( bool on ) { // the 'inHere' trick is needed as the slotSetMouseZoom() calls this static bool inHere = false; if ( inHere ) return; inHere = true; // the annotator can be used in normal mouse mode only, so if asked for it, // switch to normal mode if ( on && d->mouseMode != Okular::Settings::EnumMouseMode::Browse ) d->aMouseNormal->trigger(); // ask for Author's name if not already set if ( Okular::Settings::identityAuthor().isEmpty() ) { // get default username from the kdelibs/kdecore/KUser KUser currentUser; QString userName = currentUser.property( KUser::FullName ).toString(); // ask the user for confirmation/change if ( userName.isEmpty() ) { bool ok = false; userName = QInputDialog::getText(nullptr, i18n( "Annotations author" ), i18n( "Please insert your name or initials:" ), QLineEdit::Normal, QString(), &ok ); if ( !ok ) { d->aToggleAnnotator->trigger(); inHere = false; return; } } // save the name Okular::Settings::setIdentityAuthor( userName ); Okular::Settings::self()->save(); } // create the annotator object if not present if ( !d->annotator ) { d->annotator = new PageViewAnnotator( this, d->document ); bool allowTools = d->document->pages() > 0 && d->document->isAllowed( Okular::AllowNotes ); d->annotator->setToolsEnabled( allowTools ); d->annotator->setTextToolsEnabled( allowTools && d->document->supportsSearching() ); } // initialize/reset annotator (and show/hide toolbar) d->annotator->setEnabled( on ); d->annotator->setHidingForced( false ); inHere = false; } void PageView::slotAutoScrollUp() { if ( d->scrollIncrement < -9 ) return; d->scrollIncrement--; slotAutoScroll(); setFocus(); } void PageView::slotAutoScrollDown() { if ( d->scrollIncrement > 9 ) return; d->scrollIncrement++; slotAutoScroll(); setFocus(); } void PageView::slotScrollUp( bool singleStep ) { // if in single page mode and at the top of the screen, go to \ page if ( Okular::Settings::viewContinuous() || verticalScrollBar()->value() > verticalScrollBar()->minimum() ) { if ( singleStep ) verticalScrollBar()->triggerAction( QScrollBar::SliderSingleStepSub ); else verticalScrollBar()->triggerAction( QScrollBar::SliderPageStepSub ); } else if ( d->document->currentPage() > 0 ) { // more optimized than document->setPrevPage and then move view to bottom Okular::DocumentViewport newViewport = d->document->viewport(); newViewport.pageNumber -= viewColumns(); if ( newViewport.pageNumber < 0 ) newViewport.pageNumber = 0; newViewport.rePos.enabled = true; newViewport.rePos.normalizedY = 1.0; d->document->setViewport( newViewport ); } } void PageView::slotScrollDown( bool singleStep ) { // if in single page mode and at the bottom of the screen, go to next page if ( Okular::Settings::viewContinuous() || verticalScrollBar()->value() < verticalScrollBar()->maximum() ) { if ( singleStep ) verticalScrollBar()->triggerAction( QScrollBar::SliderSingleStepAdd ); else verticalScrollBar()->triggerAction( QScrollBar::SliderPageStepAdd ); } else if ( (int)d->document->currentPage() < d->items.count() - 1 ) { // more optimized than document->setNextPage and then move view to top Okular::DocumentViewport newViewport = d->document->viewport(); newViewport.pageNumber += viewColumns(); if ( newViewport.pageNumber >= (int)d->items.count() ) newViewport.pageNumber = d->items.count() - 1; newViewport.rePos.enabled = true; newViewport.rePos.normalizedY = 0.0; d->document->setViewport( newViewport ); } } void PageView::slotRotateClockwise() { int id = ( (int)d->document->rotation() + 1 ) % 4; d->document->setRotation( id ); } void PageView::slotRotateCounterClockwise() { int id = ( (int)d->document->rotation() + 3 ) % 4; d->document->setRotation( id ); } void PageView::slotRotateOriginal() { d->document->setRotation( 0 ); } void PageView::slotPageSizes( int newsize ) { if ( newsize < 0 || newsize >= d->document->pageSizes().count() ) return; d->document->setPageSize( d->document->pageSizes().at( newsize ) ); } // Enforce mutual-exclusion between trim modes // Each mode is uniquely identified by a single value // From Okular::Settings::EnumTrimMode void PageView::updateTrimMode( int except_id ) { const QList trimModeActions = d->aTrimMode->menu()->actions(); foreach(QAction *trimModeAction, trimModeActions) { if (trimModeAction->data().toInt() != except_id) trimModeAction->setChecked( false ); } } bool PageView::mouseReleaseOverLink( const Okular::ObjectRect * rect ) const { if ( rect ) { // handle click over a link const Okular::Action * action = static_cast< const Okular::Action * >( rect->object() ); d->document->processAction( action ); return true; } return false; } void PageView::slotTrimMarginsToggled( bool on ) { if (on) { // Turn off any other Trim modes updateTrimMode(d->aTrimMargins->data().toInt()); } if ( Okular::Settings::trimMargins() != on ) { Okular::Settings::setTrimMargins( on ); Okular::Settings::self()->save(); if ( d->document->pages() > 0 ) { slotRelayoutPages(); slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already! } } } void PageView::slotTrimToSelectionToggled( bool on ) { if ( on ) { // Turn off any other Trim modes updateTrimMode(d->aTrimToSelection->data().toInt()); d->mouseMode = Okular::Settings::EnumMouseMode::TrimSelect; // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Draw a rectangle around the page area you wish to keep visible" ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); } else { // toggled off while making selection if ( Okular::Settings::EnumMouseMode::TrimSelect == d->mouseMode ) { // clear widget selection and invalidate rect selectionClear(); // When Trim selection bbox interaction is over, we should switch to another mousemode. if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } else { d->aMouseNormal->trigger(); } } d->trimBoundingBox = Okular::NormalizedRect(); // invalidate box if ( d->document->pages() > 0 ) { slotRelayoutPages(); slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already! } } } void PageView::slotToggleForms() { toggleFormWidgets( !d->m_formsVisible ); } void PageView::slotFormChanged( int pageNumber ) { if ( !d->refreshTimer ) { d->refreshTimer = new QTimer( this ); d->refreshTimer->setSingleShot( true ); connect( d->refreshTimer, &QTimer::timeout, this, &PageView::slotRefreshPage ); } d->refreshPages << pageNumber; int delay = 0; if ( d->m_formsVisible ) { delay = 1000; } d->refreshTimer->start( delay ); } void PageView::slotRefreshPage() { foreach(int req, d->refreshPages) { QMetaObject::invokeMethod( d->document, "refreshPixmaps", Qt::QueuedConnection, Q_ARG( int, req ) ); } d->refreshPages.clear(); } #ifdef HAVE_SPEECH void PageView::slotSpeakDocument() { QString text; QVector< PageViewItem * >::const_iterator it = d->items.constBegin(), itEnd = d->items.constEnd(); for ( ; it < itEnd; ++it ) { Okular::RegularAreaRect * area = textSelectionForItem( *it ); text.append( (*it)->page()->text( area ) ); text.append( '\n' ); delete area; } d->tts()->say( text ); } void PageView::slotSpeakCurrentPage() { const int currentPage = d->document->viewport().pageNumber; PageViewItem *item = d->items.at( currentPage ); Okular::RegularAreaRect * area = textSelectionForItem( item ); const QString text = item->page()->text( area ); delete area; d->tts()->say( text ); } void PageView::slotStopSpeaks() { if ( !d->m_tts ) return; d->m_tts->stopAllSpeechs(); } #endif void PageView::slotAction( Okular::Action *action ) { d->document->processAction( action ); } void PageView::externalKeyPressEvent( QKeyEvent *e ) { keyPressEvent( e ); } void PageView::slotProcessMovieAction( const Okular::MovieAction *action ) { const Okular::MovieAnnotation *movieAnnotation = action->annotation(); if ( !movieAnnotation ) return; Okular::Movie *movie = movieAnnotation->movie(); if ( !movie ) return; const int currentPage = d->document->viewport().pageNumber; PageViewItem *item = d->items.at( currentPage ); if ( !item ) return; VideoWidget *vw = item->videoWidgets().value( movie ); if ( !vw ) return; vw->show(); switch ( action->operation() ) { case Okular::MovieAction::Play: vw->stop(); vw->play(); break; case Okular::MovieAction::Stop: vw->stop(); break; case Okular::MovieAction::Pause: vw->pause(); break; case Okular::MovieAction::Resume: vw->play(); break; }; } void PageView::slotProcessRenditionAction( const Okular::RenditionAction *action ) { Okular::Movie *movie = action->movie(); if ( !movie ) return; const int currentPage = d->document->viewport().pageNumber; PageViewItem *item = d->items.at( currentPage ); if ( !item ) return; VideoWidget *vw = item->videoWidgets().value( movie ); if ( !vw ) return; if ( action->operation() == Okular::RenditionAction::None ) return; vw->show(); switch ( action->operation() ) { case Okular::RenditionAction::Play: vw->stop(); vw->play(); break; case Okular::RenditionAction::Stop: vw->stop(); break; case Okular::RenditionAction::Pause: vw->pause(); break; case Okular::RenditionAction::Resume: vw->play(); break; default: return; }; } void PageView::slotToggleChangeColors() { Okular::SettingsCore::setChangeColors( !Okular::SettingsCore::changeColors() ); Okular::Settings::self()->save(); viewport()->update(); } void PageView::slotFitWindowToPage() { PageViewItem currentPageItem = nullptr; QSize viewportSize = viewport()->size(); foreach ( const PageViewItem * pageItem, d->items ) { if ( pageItem->isVisible() ) { currentPageItem = *pageItem; break; } } const QSize pageSize = QSize( currentPageItem.uncroppedWidth() + kcolWidthMargin, currentPageItem.uncroppedHeight() + krowHeightMargin ); if ( verticalScrollBar()->isVisible() ) viewportSize.setWidth( viewportSize.width() + verticalScrollBar()->width() ); if ( horizontalScrollBar()->isVisible() ) viewportSize.setHeight( viewportSize.height() + horizontalScrollBar()->height() ); emit fitWindowToPage( viewportSize, pageSize ); } //END private SLOTS #include "moc_pageview.cpp" /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/pageview.h b/ui/pageview.h index 2b0de2b22..d0d015ba7 100644 --- a/ui/pageview.h +++ b/ui/pageview.h @@ -1,273 +1,278 @@ /*************************************************************************** * Copyright (C) 2004 by Enrico Ros * * Copyright (C) 2004 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 * * * * With portions of code from kpdf/kpdf_pagewidget.h by: * * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2003 by Christophe Devriese * * * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003 by Kurt Pfeifle * * * * 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. * ***************************************************************************/ // This file follows coding style described in kdebase/kicker/HACKING #ifndef _OKULAR_PAGEVIEW_H_ #define _OKULAR_PAGEVIEW_H_ #include #include #include #include "ui/pageviewutils.h" #include "core/area.h" #include "core/observer.h" #include "core/view.h" class KActionCollection; namespace Okular { class Action; class Document; class DocumentViewport; class Annotation; class MovieAction; class RenditionAction; class PixmapRequest; } class FormWidgetIface; class PageViewPrivate; class MagnifierView; class QGestureEvent; /** * @short The main view. Handles zoom and continuous mode.. oh, and page * @short display of course :-) * ... */ class PageView : public QAbstractScrollArea, public Okular::DocumentObserver, public Okular::View { Q_OBJECT public: PageView( QWidget *parent, Okular::Document *document ); ~PageView(); // Zoom mode ( last 3 are internally used only! ) enum ZoomMode { ZoomFixed = 0, ZoomFitWidth = 1, ZoomFitPage = 2, ZoomFitAuto = 3, ZoomIn, ZoomOut, ZoomRefreshCurrent }; enum ClearMode { ClearAllSelection, ClearOnlyDividers }; // create actions that interact with this widget void setupBaseActions( KActionCollection * collection ); void setupViewerActions( KActionCollection * collection ); void setupActions( KActionCollection * collection ); void updateActionState( bool docHasPages, bool docChanged, bool docHasFormWidgets ); // misc methods (from RMB menu/children) bool canFitPageWidth() const; void fitPageWidth( int page ); // keep in sync with pageviewutils void displayMessage( const QString & message, const QString & details = QString(), PageViewMessage::Icon icon=PageViewMessage::Info, int duration=-1 ); // inherited from DocumentObserver void notifySetup( const QVector< Okular::Page * > & pages, int setupFlags ) override; void notifyViewportChanged( bool smoothMove ) override; void notifyPageChanged( int pageNumber, int changedFlags ) override; void notifyContentsCleared( int changedFlags ) override; void notifyZoom(int factor) override; bool canUnloadPixmap( int pageNum ) const override; void notifyCurrentPageChanged( int previous, int current ) override; // inherited from View bool supportsCapability( ViewCapability capability ) const override; CapabilityFlags capabilityFlags( ViewCapability capability ) const override; QVariant capability( ViewCapability capability ) const override; void setCapability( ViewCapability capability, const QVariant &option ) override; QList< Okular::RegularAreaRect * > textSelections( const QPoint& start, const QPoint& end, int& firstpage ); Okular::RegularAreaRect * textSelectionForItem( PageViewItem * item, const QPoint & startPoint = QPoint(), const QPoint & endPoint = QPoint() ); void reparseConfig(); KActionCollection *actionCollection() const; QAction *toggleFormsAction() const; int contentAreaWidth() const; int contentAreaHeight() const; QPoint contentAreaPosition() const; QPoint contentAreaPoint( const QPoint & pos ) const; QPointF contentAreaPoint( const QPointF & pos ) const; bool areSourceLocationsShownGraphically() const; void setShowSourceLocationsGraphically(bool show); void setLastSourceLocationViewport( const Okular::DocumentViewport& vp ); void clearLastSourceLocationViewport(); void updateCursor(); public Q_SLOTS: void copyTextSelection() const; void selectAll(); void openAnnotationWindow( Okular::Annotation *annotation, int pageNumber ); void reloadForms(); Q_SIGNALS: void rightClick( const Okular::Page *, const QPoint & ); void mouseBackButtonClick(); void mouseForwardButtonClick(); void escPressed(); void fitWindowToPage( const QSize& pageViewPortSize, const QSize& pageSize ); protected: bool event( QEvent * event ) override; void resizeEvent( QResizeEvent* ) override; bool gestureEvent( QGestureEvent * e ); // mouse / keyboard events void keyPressEvent( QKeyEvent* ) override; void keyReleaseEvent( QKeyEvent* ) override; void inputMethodEvent( QInputMethodEvent * ) override; void wheelEvent( QWheelEvent* ) override; void paintEvent( QPaintEvent *e ) override; void tabletEvent (QTabletEvent *e ) override; void mouseMoveEvent( QMouseEvent *e ) override; void mousePressEvent( QMouseEvent *e ) override; void mouseReleaseEvent( QMouseEvent *e ) override; void mouseDoubleClickEvent( QMouseEvent *e ) override; bool viewportEvent( QEvent *e ) override; void scrollContentsBy( int dx, int dy ) override; private: // draw background and items on the opened qpainter void drawDocumentOnPainter( const QRect & pageViewRect, QPainter * p ); // update item width and height using current zoom parameters void updateItemSize( PageViewItem * item, int columnWidth, int rowHeight ); // return the widget placed on a certain point or 0 if clicking on empty space PageViewItem * pickItemOnPoint( int x, int y ); // start / modify / clear selection rectangle void selectionStart( const QPoint & pos, const QColor & color, bool aboveAll = false ); void selectionClear( const ClearMode mode = ClearAllSelection ); void drawTableDividers(QPainter * screenPainter); void guessTableDividers(); // update either text or rectangle selection void updateSelection( const QPoint & pos ); // compute the zoom factor value for FitWidth and FitPage mode double zoomFactorFitMode( ZoomMode mode ); // update internal zoom values and end in a slotRelayoutPages(); void updateZoom( ZoomMode newZm ); // update the text on the label using global zoom value or current page's one void updateZoomText(); void textSelectionClear(); // updates cursor void updateCursor( const QPoint &p ); void moveMagnifier( const QPoint &p ); void updateMagnifier( const QPoint &p ); int viewColumns() const; void center(int cx, int cy); void scrollTo( int x, int y ); void toggleFormWidgets( bool on ); void resizeContentArea( const QSize & newSize ); void updatePageStep(); void addWebShortcutsMenu( QMenu * menu, const QString & text ); QMenu* createProcessLinkMenu( PageViewItem *item, const QPoint & eventPos ); // used when selecting stuff, makes the view scroll as necessary to keep the mouse inside the view void scrollPosIntoView( const QPoint & pos ); // called from slots to turn off trim modes mutually exclusive to id void updateTrimMode( int except_id ); // handle link clicked bool mouseReleaseOverLink( const Okular::ObjectRect * rect ) const; + void createAnnotationsVideoWidgets(PageViewItem *item, const QLinkedList< Okular::Annotation * > &annotations); + // don't want to expose classes in here class PageViewPrivate * d; private Q_SLOTS: // used to decouple the notifyViewportChanged calle void slotRealNotifyViewportChanged(bool smoothMove); // activated either directly or via queued connection on notifySetup void slotRelayoutPages(); // activated by the resize event delay timer void delayedResizeEvent(); // activated either directly or via the contentsMoving(int,int) signal void slotRequestVisiblePixmaps( int newValue = -1 ); // activated by the viewport move timer void slotMoveViewport(); // activated by the autoscroll timer (Shift+Up/Down keys) void slotAutoScroll(); // activated by the dragScroll timer void slotDragScroll(); // show the welcome message void slotShowWelcome(); // activated by left click timer void slotShowSizeAllCursor(); void slotHandleWebShortcutAction(); void slotConfigureWebShortcuts(); // connected to local actions (toolbar, menu, ..) void slotZoom(); void slotZoomIn(); void slotZoomOut(); void slotFitToWidthToggled( bool ); void slotFitToPageToggled( bool ); void slotAutoFitToggled( bool ); void slotViewMode( QAction *action ); void slotContinuousToggled( bool ); void slotSetMouseNormal(); void slotSetMouseZoom(); void slotSetMouseMagnifier(); void slotSetMouseSelect(); void slotSetMouseTextSelect(); void slotSetMouseTableSelect(); void slotToggleAnnotator( bool ); void slotAutoScrollUp(); void slotAutoScrollDown(); void slotScrollUp( bool singleStep = false ); void slotScrollDown( bool singleStep = false ); void slotRotateClockwise(); void slotRotateCounterClockwise(); void slotRotateOriginal(); void slotPageSizes( int ); void slotTrimMarginsToggled( bool ); void slotTrimToSelectionToggled( bool ); void slotToggleForms(); void slotFormChanged( int pageNumber ); void slotRefreshPage(); #ifdef HAVE_SPEECH void slotSpeakDocument(); void slotSpeakCurrentPage(); void slotStopSpeaks(); #endif void slotAction( Okular::Action *action ); void externalKeyPressEvent( QKeyEvent *e ); void slotAnnotationWindowDestroyed( QObject *window ); void slotProcessMovieAction( const Okular::MovieAction *action ); void slotProcessRenditionAction( const Okular::RenditionAction *action ); void slotToggleChangeColors(); void slotFitWindowToPage(); }; #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/pageviewmouseannotation.cpp b/ui/pageviewmouseannotation.cpp index 01ae335c0..de9d46f5f 100644 --- a/ui/pageviewmouseannotation.cpp +++ b/ui/pageviewmouseannotation.cpp @@ -1,716 +1,732 @@ /*************************************************************************** * Copyright (C) 2017 by Tobias Deiminger * * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2006 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 * * * * With portions of code from kpdf/kpdf_pagewidget.cc by: * * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2003 by Christophe Devriese * * * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003 by Dirk Mueller * * Copyright (C) 2004 by James Ots * * Copyright (C) 2011 by Jiri Baum - NICTA * * * * 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 "pageviewmouseannotation.h" #include #include #include #include "core/annotations.h" #include "core/document.h" #include "core/page.h" #include "ui/guiutils.h" #include "ui/pageview.h" #include "ui/videowidget.h" static const int handleSize = 10; static const int handleSizeHalf = handleSize / 2; bool AnnotationDescription::isValid() const { return ( annotation != nullptr ); } void AnnotationDescription::invalidate() { annotation = nullptr; pageViewItem = nullptr; pageNumber = -1; } AnnotationDescription::AnnotationDescription( PageViewItem * newPageViewItem, const QPoint& eventPos ) { const Okular::AnnotationObjectRect * annObjRect = nullptr; if ( newPageViewItem ) { const QRect & uncroppedPage = newPageViewItem->uncroppedGeometry(); /* find out normalized mouse coords inside current item (nX and nY will be in the range of 0..1). */ const double nX = newPageViewItem->absToPageX( eventPos.x() ); const double nY = newPageViewItem->absToPageY( eventPos.y() ); annObjRect = (Okular::AnnotationObjectRect *) newPageViewItem->page()->objectRect( Okular::ObjectRect::OAnnotation, nX, nY, uncroppedPage.width(), uncroppedPage.height() ); } if ( annObjRect ) { annotation = annObjRect->annotation(); pageViewItem = newPageViewItem; pageNumber = pageViewItem->pageNumber(); } else { invalidate(); } } MouseAnnotation::MouseAnnotation( PageView * parent, Okular::Document * document) : QObject( parent ), m_document( document ), m_pageView( parent ), m_state( StateInactive ), m_handle( RH_None ) { m_resizeHandleList << RH_Left << RH_Right << RH_Top << RH_Bottom << RH_TopLeft << RH_TopRight << RH_BottomLeft << RH_BottomRight; } MouseAnnotation::~MouseAnnotation() { } void MouseAnnotation::routeMousePressEvent( PageViewItem * pageViewItem, const QPoint & eventPos ) { /* Is there a selected annotation? */ if ( m_focusedAnnotation.isValid() ) { m_mousePosition = eventPos - pageViewItem->uncroppedGeometry().topLeft(); m_handle = getHandleAt( m_mousePosition, m_focusedAnnotation ); if ( m_handle != RH_None ) { /* Returning here means, the selection-rectangle gets control, unconditionally. * Even if it overlaps with another annotation. */ return; } } AnnotationDescription ad( pageViewItem, eventPos ); /* qDebug() << "routeMousePressEvent: eventPos = " << eventPos; */ if ( ad.isValid() ) { if ( ad.annotation->subType() == Okular::Annotation::AMovie || ad.annotation->subType() == Okular::Annotation::AScreen || ad.annotation->subType() == Okular::Annotation::AFileAttachment ) { /* qDebug() << "routeMousePressEvent: trigger action for AMovie/AScreen/AFileAttachment"; */ processAction( ad ); } else { /* qDebug() << "routeMousePressEvent: select for modification"; */ m_mousePosition = eventPos - pageViewItem->uncroppedGeometry().topLeft(); m_handle = getHandleAt( m_mousePosition, ad ); if ( m_handle != RH_None ) { setState( StateFocused, ad ); } } } else { /* qDebug() << "routeMousePressEvent: no annotation under mouse, enter StateInactive"; */ setState( StateInactive, ad ); } } void MouseAnnotation::routeMouseReleaseEvent() { if ( isModified() ) { /* qDebug() << "routeMouseReleaseEvent: finish command"; */ finishCommand(); setState( StateFocused, m_focusedAnnotation ); } /* else { qDebug() << "routeMouseReleaseEvent: ignore"; } */ } void MouseAnnotation::routeMouseMoveEvent( PageViewItem * pageViewItem, const QPoint & eventPos, bool leftButtonPressed ) { if ( !pageViewItem ) { /* qDebug() << "routeMouseMoveEvent: no pageViewItem provided, ignore"; */ return; } if ( leftButtonPressed ) { if ( isFocused() ) { /* On first move event after annotation is selected, enter modification state */ if ( m_handle == RH_Content ) { /* qDebug() << "routeMouseMoveEvent: handle " << m_handle << ", enter StateMoving"; */ setState( StateMoving, m_focusedAnnotation ); } else if ( m_handle != RH_None ) { /* qDebug() << "routeMouseMoveEvent: handle " << m_handle << ", enter StateResizing"; */ setState( StateResizing, m_focusedAnnotation ); } } if ( isModified() ) { /* qDebug() << "routeMouseMoveEvent: perform command, delta " << eventPos - m_mousePosition; */ updateViewport( m_focusedAnnotation ); performCommand( eventPos ); m_mousePosition = eventPos - pageViewItem->uncroppedGeometry().topLeft(); updateViewport( m_focusedAnnotation ); } } else { if ( isFocused() ) { /* qDebug() << "routeMouseMoveEvent: update cursor for focused annotation, new eventPos " << eventPos; */ m_mousePosition = eventPos - pageViewItem->uncroppedGeometry().topLeft(); m_handle = getHandleAt( m_mousePosition, m_focusedAnnotation ); m_pageView->updateCursor(); } /* We get here quite frequently. */ const AnnotationDescription ad( pageViewItem, eventPos ); m_mousePosition = eventPos - pageViewItem->uncroppedGeometry().topLeft(); if ( ad.isValid() ) { if ( !( m_mouseOverAnnotation == ad ) ) { /* qDebug() << "routeMouseMoveEvent: Annotation under mouse (subtype " << ad.annotation->subType() << ", flags " << ad.annotation->flags() << ")"; */ m_mouseOverAnnotation = ad; m_pageView->updateCursor(); } } else { if ( !( m_mouseOverAnnotation == ad ) ) { /* qDebug() << "routeMouseMoveEvent: Annotation disappeared under mouse."; */ m_mouseOverAnnotation.invalidate(); m_pageView->updateCursor(); } } } } void MouseAnnotation::routeKeyPressEvent( const QKeyEvent * e ) { switch ( e->key() ) { case Qt::Key_Escape: cancel(); break; case Qt::Key_Delete: if ( m_focusedAnnotation.isValid() ) { AnnotationDescription adToBeDeleted = m_focusedAnnotation; cancel(); m_document->removePageAnnotation( adToBeDeleted.pageNumber, adToBeDeleted.annotation ); } break; } } void MouseAnnotation::routeTooltipEvent( const QHelpEvent * helpEvent ) { /* qDebug() << "MouseAnnotation::routeTooltipEvent, event " << helpEvent; */ if ( m_mouseOverAnnotation.isValid() && m_mouseOverAnnotation.annotation->subType() != Okular::Annotation::AWidget ) { /* get boundingRect in uncropped page coordinates */ QRect boundingRect = Okular::AnnotationUtils::annotationGeometry( m_mouseOverAnnotation.annotation, m_mouseOverAnnotation.pageViewItem->uncroppedWidth(), m_mouseOverAnnotation.pageViewItem->uncroppedHeight() ); /* uncropped page to content area */ boundingRect.translate( m_mouseOverAnnotation.pageViewItem->uncroppedGeometry().topLeft() ); /* content area to viewport */ boundingRect.translate( -m_pageView->contentAreaPosition() ); const QString tip = GuiUtils::prettyToolTip( m_mouseOverAnnotation.annotation ); QToolTip::showText( helpEvent->globalPos(), tip, m_pageView->viewport(), boundingRect ); } } void MouseAnnotation::routePaint( QPainter * painter, const QRect & paintRect ) { /* QPainter draws relative to the origin of uncropped viewport. */ static const QColor borderColor = QColor::fromHsvF( 0, 0, 1.0 ); static const QColor fillColor = QColor::fromHsvF( 0, 0, 0.75, 0.66 ); if ( !isFocused() ) return; /* * Get annotation bounding rectangle in uncropped page coordinates. * Distinction between AnnotationUtils::annotationGeometry() and AnnotationObjectRect::boundingRect() is, * that boundingRect would enlarge the QRect to a minimum size of 14 x 14. * This is useful for getting focus an a very small annotation, * but for drawing and modification we want the real size. */ const QRect boundingRect = Okular::AnnotationUtils::annotationGeometry( m_focusedAnnotation.annotation, m_focusedAnnotation.pageViewItem->uncroppedWidth(), m_focusedAnnotation.pageViewItem->uncroppedHeight() ); if ( !paintRect.intersects( boundingRect .translated( m_focusedAnnotation.pageViewItem->uncroppedGeometry().topLeft() ) .adjusted( -handleSizeHalf, -handleSizeHalf, handleSizeHalf, handleSizeHalf ) ) ) { /* Our selection rectangle is not in a region that needs to be (re-)drawn. */ return; } painter->save(); painter->translate( m_focusedAnnotation.pageViewItem->uncroppedGeometry().topLeft() ); painter->setPen( QPen( fillColor, 2, Qt::SolidLine, Qt::SquareCap, Qt::BevelJoin ) ); painter->drawRect( boundingRect ); if ( m_focusedAnnotation.annotation->canBeResized() ) { painter->setPen(borderColor); painter->setBrush(fillColor); Q_FOREACH( const ResizeHandle & handle, m_resizeHandleList ) { QRect rect = getHandleRect( handle, m_focusedAnnotation ); painter->drawRect( rect ); } } painter->restore(); } Okular::Annotation * MouseAnnotation::annotation() const { if ( m_focusedAnnotation.isValid() ) { return m_focusedAnnotation.annotation; } return nullptr; } bool MouseAnnotation::isActive() const { return ( m_state != StateInactive ); } bool MouseAnnotation::isMouseOver() const { return ( m_mouseOverAnnotation.isValid() || m_handle != RH_None ); } bool MouseAnnotation::isFocused() const { return ( m_state == StateFocused ); } bool MouseAnnotation::isMoved() const { return ( m_state == StateMoving ); } bool MouseAnnotation::isResized() const { return ( m_state == StateResizing ); } bool MouseAnnotation::isModified() const { return ( m_state == StateMoving || m_state == StateResizing ); } Qt::CursorShape MouseAnnotation::cursor() const { if ( m_handle != RH_None ) { if ( isMoved() ) { return Qt::SizeAllCursor; } else if ( isFocused() || isResized() ) { switch ( m_handle ) { case RH_Top: return Qt::SizeVerCursor; case RH_TopRight: return Qt::SizeBDiagCursor; case RH_Right: return Qt::SizeHorCursor; case RH_BottomRight: return Qt::SizeFDiagCursor; case RH_Bottom: return Qt::SizeVerCursor; case RH_BottomLeft: return Qt::SizeBDiagCursor; case RH_Left: return Qt::SizeHorCursor; case RH_TopLeft: return Qt::SizeFDiagCursor; case RH_Content: return Qt::SizeAllCursor; default: return Qt::OpenHandCursor; } } } else if ( m_mouseOverAnnotation.isValid() ) { /* Mouse is over annotation, but the annotation is not yet selected. */ if ( m_mouseOverAnnotation.annotation->subType() == Okular::Annotation::AMovie ) { return Qt::PointingHandCursor; } else if ( m_mouseOverAnnotation.annotation->subType() == Okular::Annotation::ARichMedia ) { return Qt::PointingHandCursor; } else if ( m_mouseOverAnnotation.annotation->subType() == Okular::Annotation::AScreen ) { if ( GuiUtils::renditionMovieFromScreenAnnotation( static_cast< const Okular::ScreenAnnotation * >( m_mouseOverAnnotation.annotation ) ) != nullptr ) { return Qt::PointingHandCursor; } } else if ( m_mouseOverAnnotation.annotation->subType() == Okular::Annotation::AFileAttachment ) { return Qt::PointingHandCursor; } else { return Qt::ArrowCursor; } } /* There's no none cursor, so we still have to return something. */ return Qt::ArrowCursor; } +void MouseAnnotation::updateAnnotationPointers() +{ + if (m_focusedAnnotation.annotation) + { + m_focusedAnnotation.annotation = m_document->page( m_focusedAnnotation.pageNumber )->annotation( m_focusedAnnotation.annotation->uniqueName() ); + } + + if (m_mouseOverAnnotation.annotation) + { + m_mouseOverAnnotation.annotation = m_document->page( m_mouseOverAnnotation.pageNumber )->annotation( m_mouseOverAnnotation.annotation->uniqueName() ); + } +} + void MouseAnnotation::cancel() { if ( isActive() ) { finishCommand(); setState( StateInactive, m_focusedAnnotation ); } } void MouseAnnotation::reset() { cancel(); m_focusedAnnotation.invalidate(); m_mouseOverAnnotation.invalidate(); } /* Handle state changes for the focused annotation. */ void MouseAnnotation::setState( MouseAnnotationState state, const AnnotationDescription & ad ) { /* qDebug() << "setState: requested " << state; */ if ( m_focusedAnnotation.isValid() ) { /* If there was a annotation before, request also repaint for the previous area. */ updateViewport( m_focusedAnnotation ); } if ( !ad.isValid() ) { /* qDebug() << "No annotation provided, forcing state inactive." << state; */ state = StateInactive; } else if ( ( state == StateMoving && !ad.annotation->canBeMoved() ) || ( state == StateResizing && !ad.annotation->canBeResized() ) ) { /* qDebug() << "Annotation does not support requested state, forcing state selected." << state; */ state = StateInactive; } switch( state ) { case StateMoving: m_focusedAnnotation = ad; m_focusedAnnotation.annotation->setFlags( m_focusedAnnotation.annotation->flags() | Okular::Annotation::BeingMoved ); updateViewport( m_focusedAnnotation ); break; case StateResizing: m_focusedAnnotation = ad; m_focusedAnnotation.annotation->setFlags( m_focusedAnnotation.annotation->flags() | Okular::Annotation::BeingResized ); updateViewport( m_focusedAnnotation ); break; case StateFocused: m_focusedAnnotation = ad; m_focusedAnnotation.annotation->setFlags( m_focusedAnnotation.annotation->flags() & ~(Okular::Annotation::BeingMoved | Okular::Annotation::BeingResized) ); updateViewport( m_focusedAnnotation ); break; case StateInactive: default: if ( m_focusedAnnotation.isValid() ) { m_focusedAnnotation.annotation->setFlags( m_focusedAnnotation.annotation->flags() & ~(Okular::Annotation::BeingMoved | Okular::Annotation::BeingResized) ); } m_focusedAnnotation.invalidate(); m_handle = RH_None; } /* qDebug() << "setState: enter " << state; */ m_state = state; m_pageView->updateCursor(); } /* Get the rectangular boundary of the given annotation, enlarged for space needed by resize handles. * Returns a QRect in page view item coordinates. */ QRect MouseAnnotation::getFullBoundingRect( const AnnotationDescription & ad ) const { QRect boundingRect; if ( ad.isValid() ) { boundingRect = Okular::AnnotationUtils::annotationGeometry( ad.annotation, ad.pageViewItem->uncroppedWidth(), ad.pageViewItem->uncroppedHeight() ); boundingRect = boundingRect.adjusted( -handleSizeHalf, -handleSizeHalf, handleSizeHalf, handleSizeHalf ); } return boundingRect; } /* Apply the command determined by m_state to the currently focused annotation. */ void MouseAnnotation::performCommand( const QPoint & newPos ) { const QRect & pageViewItemRect = m_focusedAnnotation.pageViewItem->uncroppedGeometry(); QPointF mouseDelta( newPos - pageViewItemRect.topLeft() - m_mousePosition ); QPointF normalizedRotatedMouseDelta( rotateInRect( QPointF( mouseDelta.x() / pageViewItemRect.width(), mouseDelta.y() / pageViewItemRect.height() ), m_focusedAnnotation.pageViewItem->page()->rotation() ) ); if ( isMoved() ) { m_document->translatePageAnnotation( m_focusedAnnotation.pageNumber, m_focusedAnnotation.annotation, Okular::NormalizedPoint( normalizedRotatedMouseDelta.x(), normalizedRotatedMouseDelta.y() ) ); } else if ( isResized() ) { QPointF delta1, delta2; handleToAdjust( normalizedRotatedMouseDelta, delta1, delta2, m_handle, m_focusedAnnotation.pageViewItem->page()->rotation() ); m_document->adjustPageAnnotation( m_focusedAnnotation.pageNumber, m_focusedAnnotation.annotation, Okular::NormalizedPoint( delta1.x(), delta1.y() ), Okular::NormalizedPoint( delta2.x(), delta2.y() ) ); } } /* Finalize a command in progress for the currently focused annotation. */ void MouseAnnotation::finishCommand() { /* * Note: * Translate-/resizePageAnnotation causes PopplerAnnotationProxy::notifyModification, * where modify flag needs to be already cleared. So it is important to call * setFlags before translatePageAnnotation-/adjustPageAnnotation. */ if ( isMoved() ) { m_focusedAnnotation.annotation->setFlags( m_focusedAnnotation.annotation->flags() & ~Okular::Annotation::BeingMoved ); m_document->translatePageAnnotation( m_focusedAnnotation.pageNumber, m_focusedAnnotation.annotation, Okular::NormalizedPoint( 0.0, 0.0 ) ); } else if ( isResized() ) { m_focusedAnnotation.annotation->setFlags( m_focusedAnnotation.annotation->flags() & ~Okular::Annotation::BeingResized ); m_document->adjustPageAnnotation( m_focusedAnnotation.pageNumber, m_focusedAnnotation.annotation, Okular::NormalizedPoint( 0.0, 0.0 ), Okular::NormalizedPoint( 0.0, 0.0 ) ); } } /* Tell viewport widget that the rectangular of the given annotation needs to be repainted. */ void MouseAnnotation::updateViewport( const AnnotationDescription & ad ) const { const QRect & changedPageViewItemRect = getFullBoundingRect( ad ); m_pageView->viewport()->update( changedPageViewItemRect .translated( ad.pageViewItem->uncroppedGeometry().topLeft() ) .translated( -m_pageView->contentAreaPosition() ) ); } /* eventPos: Mouse position in uncropped page coordinates. ad: The annotation to get the handle for. */ MouseAnnotation::ResizeHandle MouseAnnotation::getHandleAt( const QPoint & eventPos, const AnnotationDescription & ad ) const { ResizeHandle selected = RH_None; if ( ad.annotation->canBeResized() ) { Q_FOREACH( const ResizeHandle & handle, m_resizeHandleList ) { const QRect rect = getHandleRect( handle, ad ); if ( rect.contains( eventPos ) ) { selected |= handle; } } /* * Handles may overlap when selection is very small. * Then it can happen that cursor is over more than one handles, * and therefore maybe more than two flags are set. * Favor one handle in that case. */ if ( ( selected & RH_BottomRight ) == RH_BottomRight ) return RH_BottomRight; if ( ( selected & RH_TopRight ) == RH_TopRight ) return RH_TopRight; if ( ( selected & RH_TopLeft ) == RH_TopLeft ) return RH_TopLeft; if ( ( selected & RH_BottomLeft ) == RH_BottomLeft ) return RH_BottomLeft; } if ( selected == RH_None && ad.annotation->canBeMoved() ) { const QRect boundingRect = Okular::AnnotationUtils::annotationGeometry( ad.annotation, ad.pageViewItem->uncroppedWidth(), ad.pageViewItem->uncroppedHeight() ); if ( boundingRect.contains( eventPos ) ) { return RH_Content; } } return selected; } /* Get the rectangle for a specified resizie handle. */ QRect MouseAnnotation::getHandleRect( ResizeHandle handle, const AnnotationDescription & ad ) const { const QRect boundingRect = Okular::AnnotationUtils::annotationGeometry( ad.annotation, ad.pageViewItem->uncroppedWidth(), ad.pageViewItem->uncroppedHeight() ); int left, top; if ( handle & RH_Top ) { top = boundingRect.top() - handleSizeHalf; } else if ( handle & RH_Bottom ) { top = boundingRect.bottom() - handleSizeHalf; } else { top = boundingRect.top() + boundingRect.height() / 2 - handleSizeHalf; } if ( handle & RH_Left ) { left = boundingRect.left() - handleSizeHalf; } else if ( handle & RH_Right ) { left = boundingRect.right() - handleSizeHalf; } else { left = boundingRect.left() + boundingRect.width() / 2 - handleSizeHalf; } return QRect( left, top, handleSize, handleSize ); } /* Convert a resize handle delta into two adjust delta coordinates. */ void MouseAnnotation::handleToAdjust( const QPointF & dIn, QPointF & dOut1, QPointF & dOut2, MouseAnnotation::ResizeHandle handle, Okular::Rotation rotation ) { const MouseAnnotation::ResizeHandle rotatedHandle = MouseAnnotation::rotateHandle( handle, rotation); dOut1.rx() = ( rotatedHandle & MouseAnnotation::RH_Left ) ? dIn.x() : 0; dOut1.ry() = ( rotatedHandle & MouseAnnotation::RH_Top ) ? dIn.y() : 0; dOut2.rx() = ( rotatedHandle & MouseAnnotation::RH_Right ) ? dIn.x() : 0; dOut2.ry() = ( rotatedHandle & MouseAnnotation::RH_Bottom ) ? dIn.y() : 0; } QPointF MouseAnnotation::rotateInRect( const QPointF & rotated, Okular::Rotation rotation ) { QPointF ret; switch ( rotation ) { case Okular::Rotation90: ret = QPointF( rotated.y(), -rotated.x() ); break; case Okular::Rotation180: ret = QPointF( -rotated.x(), -rotated.y() ); break; case Okular::Rotation270: ret = QPointF( -rotated.y(), rotated.x() ); break; case Okular::Rotation0: /* no modifications */ default: /* other cases */ ret = rotated; } return ret; } MouseAnnotation::ResizeHandle MouseAnnotation::rotateHandle( MouseAnnotation::ResizeHandle handle, Okular::Rotation rotation ) { unsigned int rotatedHandle = 0; switch( rotation ) { case Okular::Rotation90: /* bit rotation: #1 => #4, #2 => #1, #3 => #2, #4 => #3 */ rotatedHandle = (handle << 3 | handle >> (4-3)) & RH_AllHandles; break; case Okular::Rotation180: /* bit rotation: #1 => #3, #2 => #4, #3 => #1, #4 => #2 */ rotatedHandle = (handle << 2 | handle >> (4-2)) & RH_AllHandles; break; case Okular::Rotation270: /* bit rotation: #1 => #2, #2 => #3, #3 => #4, #4 => #1 */ rotatedHandle = (handle << 1 | handle >> (4-1)) & RH_AllHandles; break; case Okular::Rotation0: /* no modifications */ default: /* other cases */ rotatedHandle = handle; break; } return (MouseAnnotation::ResizeHandle) rotatedHandle; } /* Start according action for AMovie/ARichMedia/AScreen/AFileAttachment. * It was formerly (before mouse annotation refactoring) called on mouse release event. * Now it's called on mouse press. Should we keep the former behavior? */ void MouseAnnotation::processAction( const AnnotationDescription& ad ) { if ( ad.isValid() ) { Okular::Annotation *ann = ad.annotation; PageViewItem * pageItem = ad.pageViewItem; if ( ann->subType() == Okular::Annotation::AMovie ) { VideoWidget *vw = pageItem->videoWidgets().value( static_cast( ann )->movie() ); vw->show(); vw->play(); } else if ( ann->subType() == Okular::Annotation::ARichMedia ) { VideoWidget *vw = pageItem->videoWidgets().value( static_cast( ann )->movie() ); vw->show(); vw->play(); } else if ( ann->subType() == Okular::Annotation::AScreen ) { m_document->processAction( static_cast( ann )->action() ); } else if ( ann->subType() == Okular::Annotation::AFileAttachment ) { const Okular::FileAttachmentAnnotation * fileAttachAnnot = static_cast< Okular::FileAttachmentAnnotation * >( ann ); GuiUtils::saveEmbeddedFile( fileAttachAnnot->embeddedFile(), m_pageView ); } } } diff --git a/ui/pageviewmouseannotation.h b/ui/pageviewmouseannotation.h index 0c9545137..87dbb8db2 100644 --- a/ui/pageviewmouseannotation.h +++ b/ui/pageviewmouseannotation.h @@ -1,167 +1,173 @@ /*************************************************************************** * Copyright (C) 2017 by Tobias Deiminger * * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2006 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 * * * * With portions of code from kpdf/kpdf_pagewidget.cc by: * * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2003 by Christophe Devriese * * * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003 by Dirk Mueller * * Copyright (C) 2004 by James Ots * * Copyright (C) 2011 by Jiri Baum - NICTA * * * * 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_PAGEVIEWMOUSEANNOTATION_H_ #define _OKULAR_PAGEVIEWMOUSEANNOTATION_H_ #include #include "pageviewutils.h" #include "core/annotations.h" class QHelpEvent; class QPainter; class QPoint; class PageView; class PageViewItem; class AnnotationDescription; namespace Okular { class Document; } /* This class shall help to keep data for one annotation consistent. */ class AnnotationDescription { public: AnnotationDescription() : annotation( nullptr ), pageViewItem( nullptr ), pageNumber( 0 ) {} AnnotationDescription( PageViewItem * newPageViewItem, const QPoint& eventPos ); bool isValid() const; void invalidate(); bool operator==( const AnnotationDescription & rhs ) const { return ( annotation == rhs.annotation ); } Okular::Annotation * annotation; PageViewItem * pageViewItem; int pageNumber; }; /** * @short Handle UI for annotation interactions, like moving, resizing and triggering actions. * * An object of this class tracks which annotation is currently under the mouse cursor. * Some annotation types can be focused in order to move or resize them. * State is determined from mouse and keyboard events, which are forwarded from the parent PageView object. * Move and resize actions are dispatched to the Document object. */ class MouseAnnotation : public QObject { Q_OBJECT public: MouseAnnotation( PageView * parent, Okular::Document * document ); ~MouseAnnotation(); /* Process a mouse press event. eventPos: Mouse position in content area coordinates. */ void routeMousePressEvent( PageViewItem * pageViewItem, const QPoint & eventPos ); /* Process a mouse release event. */ void routeMouseReleaseEvent(); /* Process a mouse move event. eventPos: Mouse position in content area coordinates. */ void routeMouseMoveEvent( PageViewItem * pageViewItem, const QPoint & eventPos, bool leftButtonPressed ); /* Process a key event. */ void routeKeyPressEvent( const QKeyEvent * e ); /* Process a tooltip event. eventPos: Mouse position in content area coordinates. */ void routeTooltipEvent( const QHelpEvent * helpEvent ); /* Process a paint event. */ void routePaint( QPainter * painter, const QRect & paintRect ); /* Cancel the current selection or action, if any. */ void cancel(); /* Reset to initial state. Cancel current action and relinquish references to PageViewItem widgets. */ void reset(); Okular::Annotation * annotation() const; /* Return true, if MouseAnnotation demands control for a mouse click on the current cursor position. */ bool isMouseOver() const; bool isActive() const; bool isFocused() const; bool isMoved() const; bool isResized() const; bool isModified() const; Qt::CursorShape cursor() const; + // needs to be called after document save + void updateAnnotationPointers(); + enum MouseAnnotationState { StateInactive, StateFocused, StateMoving, StateResizing }; enum ResizeHandleFlag { RH_None = 0, RH_Top = 1, RH_Right = 2, RH_Bottom = 4, RH_Left = 8, RH_TopLeft = RH_Top | RH_Left, RH_BottomLeft = RH_Bottom | RH_Left, RH_TopRight = RH_Top | RH_Right, RH_BottomRight = RH_Bottom | RH_Right, RH_Content = 16, RH_AllHandles = RH_Top | RH_Right | RH_Bottom | RH_Left }; Q_DECLARE_FLAGS( ResizeHandle, ResizeHandleFlag ) private: void setState( MouseAnnotationState state, const AnnotationDescription & ad ); QRect getFullBoundingRect( const AnnotationDescription & ad ) const; void performCommand( const QPoint & newPos ); void finishCommand(); void updateViewport( const AnnotationDescription & ad ) const; ResizeHandle getHandleAt( const QPoint & eventPos, const AnnotationDescription & ad ) const; QRect getHandleRect( ResizeHandle handle, const AnnotationDescription & ad ) const; static void handleToAdjust( const QPointF & dIn, QPointF & dOut1, QPointF & dOut2, MouseAnnotation::ResizeHandle handle, Okular::Rotation rotation ); static QPointF rotateInRect( const QPointF & rotated, Okular::Rotation rotation ); static ResizeHandle rotateHandle( ResizeHandle handle, Okular::Rotation rotation ); void processAction( const AnnotationDescription& ad ); /* We often have to delegate to the document model and our parent widget. */ Okular::Document * m_document; PageView * m_pageView; /* Remember which annotation is currently focused/modified. */ MouseAnnotationState m_state; MouseAnnotation::ResizeHandle m_handle; AnnotationDescription m_focusedAnnotation; /* Mouse tracking, always kept up to date with the latest mouse position and annotation under mouse cursor. */ AnnotationDescription m_mouseOverAnnotation; QPoint m_mousePosition; // in page view item coordinates QList m_resizeHandleList; }; #endif diff --git a/ui/pageviewutils.cpp b/ui/pageviewutils.cpp index a46384afb..d8a8698bc 100644 --- a/ui/pageviewutils.cpp +++ b/ui/pageviewutils.cpp @@ -1,946 +1,947 @@ /*************************************************************************** * 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() { - QHash::iterator it = m_formWidgets.begin(), itEnd = m_formWidgets.end(); - for ( ; it != itEnd; ++it ) - delete *it; + 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; } -QHash& PageViewItem::formWidgets() +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() ) ); - QHash::iterator it = m_formWidgets.begin(), itEnd = m_formWidgets.end(); + 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; - QHash::iterator it = m_formWidgets.begin(), itEnd = m_formWidgets.end(); + QSet::iterator it = m_formWidgets.begin(), itEnd = m_formWidgets.end(); for ( ; it != itEnd; ++it ) { bool hadfocus = (*it)->setVisibility( visible && (*it)->formField()->isVisible() ); somehadfocus = somehadfocus || hadfocus; } return somehadfocus; } void PageViewItem::reloadFormWidgetsState() { foreach(FormWidgetIface *fwi, m_formWidgets) { fwi->setVisibility( fwi->formField()->isVisible() ); } } /*********************/ /** 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" diff --git a/ui/pageviewutils.h b/ui/pageviewutils.h index ca8bd78a4..6c3740927 100644 --- a/ui/pageviewutils.h +++ b/ui/pageviewutils.h @@ -1,220 +1,223 @@ /*************************************************************************** * 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. * ***************************************************************************/ #ifndef _PAGEVIEW_UTILS_H_ #define _PAGEVIEW_UTILS_H_ #include #include #include #include #include #include "core/area.h" class QAction; class QLabel; class QTimer; class FormWidgetIface; class PageView; class VideoWidget; namespace Okular { class Movie; class Page; } /** * @short PageViewItem represents graphically a page into the PageView. * * It has methods for settings Item's geometry and other visual properties such * as the individual zoom factor. */ class PageViewItem { public: PageViewItem( const Okular::Page * page ); ~PageViewItem(); const Okular::Page * page() const; int pageNumber() const; double zoomFactor() const; bool isVisible() const; - QHash& formWidgets(); + QSet& formWidgets(); QHash< Okular::Movie *, VideoWidget * >& videoWidgets(); /* The page is cropped as follows: */ const Okular::NormalizedRect & crop() const; /* Real geometry into which the cropped page is rendered: */ const QRect& croppedGeometry() const; int croppedWidth() const; int croppedHeight() const; /* "Uncropped" geometry: * If the whole page was rendered into the uncropped geometry then the * cropped page would be rendered into the real geometry. * (Hence, uncropped always contains cropped, and they are equal only if * the page is uncropped.) This is just for convenience in calculations. */ const QRect& uncroppedGeometry() const; int uncroppedWidth() const; int uncroppedHeight() const; /* Convert absolute geometry coordinates to normalized [0,1] page coordinates: */ double absToPageX(double absX) const; double absToPageY(double absY) const; void setWHZC( int w, int h, double zoom, const Okular::NormalizedRect & c ); void moveTo( int x, int y ); void setVisible( bool visible ); void invalidate(); bool setFormWidgetsVisible( bool visible ); void reloadFormWidgetsState(); private: const Okular::Page * m_page; double m_zoomFactor; bool m_visible; bool m_formsVisible; QRect m_croppedGeometry; QRect m_uncroppedGeometry; Okular::NormalizedRect m_crop; - QHash m_formWidgets; + QSet m_formWidgets; QHash< Okular::Movie *, VideoWidget * > m_videoWidgets; }; /** * @short A widget that displays messages in the top-left corner. * * This is a widget with thin border and rounded corners that displays a given * text along as an icon. It's meant to be used for displaying messages to the * user by placing this above other widgets. */ class PageViewMessage : public QWidget { Q_OBJECT public: PageViewMessage( QWidget * parent ); enum Icon { None, Info, Warning, Error, Find, Annotation }; void display( const QString & message, const QString & details = QString(), Icon icon = Info, int durationMs = 4000 ); protected: bool eventFilter(QObject * obj, QEvent * event ) override; void paintEvent( QPaintEvent * e ) override; void mousePressEvent( QMouseEvent * e ) override; private: QRect computeTextRect( const QString & message, int extra_width ) const; void computeSizeAndResize(); QString m_message; QString m_details; QPixmap m_symbol; QTimer * m_timer; int m_lineSpacing; }; struct AnnotationToolItem { AnnotationToolItem() : id( -1 ), isText( false ) { } int id; QString text; QPixmap pixmap; QString shortcut; bool isText; }; class ToolBarButton : public QToolButton { Q_OBJECT public: static const int iconSize = 32; static const int buttonSize = 40; ToolBarButton( QWidget * parent, const AnnotationToolItem &item ); int buttonID() const { return m_id; } bool isText() const { return m_isText; } Q_SIGNALS: void buttonDoubleClicked( int buttonID ); protected: void mouseDoubleClickEvent( QMouseEvent * event ) override; private: int m_id; bool m_isText; }; /** * @short A widget containing exclusive buttons, that slides in from a side. * * This is a shaped widget that slides in from a side of the 'anchor widget' * it's attached to. It can be dragged and docked on {left,top,right,bottom} * sides and contains toggable exclusive buttons. * When a 'tool' of this 'toolBar' is selected, a signal is emitted. */ class PageViewToolBar : public QWidget { Q_OBJECT public: PageViewToolBar( PageView * parent, QWidget * anchorWidget ); ~PageViewToolBar(); // animated widget controls enum Side { Left = 0, Top = 1, Right = 2, Bottom = 3 }; void setItems( const QLinkedList &items ); void setSide( Side side ); void showAndAnimate(); void hideAndDestroy(); void selectButton( int id ); void setToolsEnabled( bool on ); void setTextToolsEnabled( bool on ); // query properties Q_SIGNALS: // the tool 'toolID' has been selected void toolSelected( int toolID ); // orientation has been changed void orientationChanged( int side ); // a tool button of this toolbar has been double clicked void buttonDoubleClicked( int buttonID ); protected: // handle widget events { anchor_resize, paint, animation, drag } bool eventFilter( QObject * o, QEvent * e ) override; void paintEvent( QPaintEvent * ) override; void mousePressEvent( QMouseEvent * e ) override; void mouseMoveEvent( QMouseEvent * e ) override; void mouseReleaseEvent( QMouseEvent * e ) override; private: // private variables friend class ToolBarPrivate; class ToolBarPrivate * d; private Q_SLOTS: void slotAnimate(); void slotButtonClicked(); }; #endif