diff --git a/autotests/documenttest.cpp b/autotests/documenttest.cpp index 7caef8fd7..aa696f36c 100644 --- a/autotests/documenttest.cpp +++ b/autotests/documenttest.cpp @@ -1,133 +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" ); + Okular::SettingsCore::instance( QStringLiteral("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() ) ); + QTemporaryFile migratedSaveFile( QStringLiteral( "%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(), QUrl::fromLocalFile(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/mainshelltest.cpp b/autotests/mainshelltest.cpp index 228157036..58a4749b4 100644 --- a/autotests/mainshelltest.cpp +++ b/autotests/mainshelltest.cpp @@ -1,601 +1,601 @@ /*************************************************************************** * Copyright (C) 2014 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. * ***************************************************************************/ #include #include #include #include #include #include #include #include "../shell/okular_main.h" #include "../shell/shell.h" #include "../shell/shellutils.h" #include "../core/document_p.h" #include "../ui/findbar.h" #include "../ui/presentationwidget.h" #include "../part.h" #include "../settings.h" #include #ifndef Q_OS_WIN #include #else #include #endif namespace Okular { class PartTest { public: Okular::Document *partDocument(Okular::Part *part) const { return part->m_document; } QWidget *presentationWidget(Okular::Part *part) const { return part->m_presentationWidget; } FindBar *findWidget(Okular::Part *part) const { return part->m_findBar; } }; } class ClosePrintDialogHelper : public QObject { Q_OBJECT public: ClosePrintDialogHelper(int expectedTab) : foundDialog(false), m_expectedTab(expectedTab) { } bool foundDialog; private slots: void closePrintDialog(); private: int m_expectedTab; }; class MainShellTest : public QObject, public Okular::PartTest { Q_OBJECT public: static QTabWidget* tabWidget(Shell *s) { return s->m_tabWidget; } private slots: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); void testShell_data(); void testShell(); void testFileRemembersPagePosition_data(); void testFileRemembersPagePosition(); void test2FilesError_data(); void test2FilesError(); void testSessionRestore_data(); void testSessionRestore(); private: }; QList getShells() { QList shells; foreach( KMainWindow* kmw, KMainWindow::memberList() ) { Shell* shell = qobject_cast( kmw ); if( shell ) { shells.append( shell ); } } return shells; } Shell *findShell(Shell *ignore = nullptr) { foreach (QWidget *widget, QApplication::topLevelWidgets()) { Shell *s = qobject_cast(widget); if (s && s != ignore) return s; } return nullptr; } void MainShellTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); // Don't pollute people's okular settings Okular::Settings::instance( QStringLiteral("mainshelltest") ); // Register in bus as okular QDBusConnectionInterface *bus = QDBusConnection::sessionBus().interface(); const QString myPid = QString::number( getpid() ); const QString serviceName = QStringLiteral("org.kde.okular-")+ myPid; QVERIFY( bus->registerService(serviceName) == QDBusConnectionInterface::ServiceRegistered ); // Tell the presentationWidget and queryClose to not be annoying KSharedConfigPtr c = KSharedConfig::openConfig(); KConfigGroup cg = c->group("Notification Messages"); cg.writeEntry("presentationInfo", false); cg.writeEntry("ShowTabWarning", false); } void MainShellTest::cleanupTestCase() { } void MainShellTest::init() { // Default settings for every test Okular::Settings::self()->setDefaults(); // Clean docdatas QList urls; urls << QUrl::fromUserInput(QStringLiteral("file://" KDESRCDIR "data/file1.pdf")); urls << QUrl::fromUserInput(QStringLiteral("file://" KDESRCDIR "data/tocreload.pdf")); urls << QUrl::fromUserInput(QStringLiteral("file://" KDESRCDIR "data/contents.epub")); foreach (const QUrl &url, urls) { QFileInfo fileReadTest( url.toLocalFile() ); const QString docDataPath = Okular::DocumentPrivate::docDataFileName(url, fileReadTest.size()); QFile::remove(docDataPath); } } void MainShellTest::cleanup() { Shell *s; while ((s = findShell())) { delete s; } } void MainShellTest::testShell_data() { QTest::addColumn("paths"); QTest::addColumn("serializedOptions"); QTest::addColumn("useTabs"); QTest::addColumn("externalProcessPath"); QTest::addColumn("expectedPage"); QTest::addColumn("expectPresentation"); QTest::addColumn("expectPrintDialog"); QTest::addColumn("unique"); QTest::addColumn("externalProcessExpectedPage"); QTest::addColumn("externalProcessExpectPresentation"); QTest::addColumn("externalProcessExpectPrintDialog"); QTest::addColumn("externalProcessExpectFind"); const QStringList contentsEpub = QStringList(QStringLiteral(KDESRCDIR "data/contents.epub")); const QStringList file1 = QStringList(QStringLiteral(KDESRCDIR "data/file1.pdf")); QStringList file1AndToc; file1AndToc << QStringLiteral(KDESRCDIR "data/file1.pdf"); file1AndToc << QStringLiteral(KDESRCDIR "data/tocreload.pdf"); const QString tocReload = QStringLiteral(KDESRCDIR "data/tocreload.pdf"); const QString optionsPage2 = ShellUtils::serializeOptions(false, false, false, false, false, QStringLiteral("2"), QString()); const QString optionsPage2Presentation = ShellUtils::serializeOptions(true, false, false, false, false, QStringLiteral("2"), QString()); const QString optionsPrint = ShellUtils::serializeOptions(false, true, false, false, false, QString(), QString()); const QString optionsUnique = ShellUtils::serializeOptions(false, false, false, true, false, QString(), QString()); const QString optionsFind = ShellUtils::serializeOptions(false, false, false, false ,false , QString(), QStringLiteral("si:next-testing parameters!")); QTest::newRow("just show shell") << QStringList() << QString() << false << QString() << 0u << false << false << false << 0u << false << false << QString(); QTest::newRow("open file") << file1 << QString() << false << QString() << 0u << false << false << false << 0u << false << false << QString(); QTest::newRow("two files no tabs") << file1AndToc << QString() << false << QString() << 0u << false << false << false << 0u << false << false << QString(); QTest::newRow("two files with tabs") << file1AndToc << QString() << true << QString() << 0u << false << false << false << 0u << false << false << QString(); QTest::newRow("two files sequence no tabs") << file1 << QString() << false << tocReload << 0u << false << false << false << 0u << false << false << QString(); QTest::newRow("two files sequence with tabs") << file1 << QString() << true << tocReload << 0u << false << false << false << 0u << false << false << QString(); QTest::newRow("open file page number") << contentsEpub << optionsPage2 << false << QString() << 1u << false << false << false << 0u << false << false << QString(); QTest::newRow("open file page number and presentation") << contentsEpub << optionsPage2Presentation << false << QString() << 1u << true << false << false << 0u << false << false << QString(); - QTest::newRow("open file find") << file1 << optionsFind << false << QString() << 0u << false << false << false << 0u << false << false << QString("si:next-testing parameters!"); + QTest::newRow("open file find") << file1 << optionsFind << false << QString() << 0u << false << false << false << 0u << false << false << QStringLiteral("si:next-testing parameters!"); QTest::newRow("open file print") << file1 << optionsPrint << false << QString() << 0u << false << true << false << 0u << false << false << QString(); QTest::newRow("open two files unique") << file1 << optionsUnique << false << tocReload << 0u << false << false << true << 0u << false << false << QString(); QTest::newRow("open two files unique tabs") << file1 << optionsUnique << true << tocReload << 0u << false << false << true << 0u << false << false << QString(); QTest::newRow("page number attach tabs") << file1 << QString() << true << contentsEpub[0] << 0u << false << false << false << 2u << false << false << QString(); QTest::newRow("presentation attach tabs") << file1 << QString() << true << contentsEpub[0] << 0u << false << false << false << 2u << true << false << QString(); QTest::newRow("print attach tabs") << file1 << QString() << true << contentsEpub[0] << 0u << false << true << false << 2u << false << true << QString(); QTest::newRow("page number attach unique") << file1 << optionsUnique << false << contentsEpub[0] << 0u << false << false << true << 3u << false << false << QString(); QTest::newRow("presentation attach unique") << file1 << optionsUnique << false << contentsEpub[0] << 0u << false << false << true << 2u << true << false << QString(); QTest::newRow("print attach unique") << file1 << optionsUnique << false << contentsEpub[0] << 0u << false << false << true << 2u << false << true << QString(); QTest::newRow("page number attach unique tabs") << file1 << optionsUnique << true << contentsEpub[0] << 0u << false << false << true << 3u << false << false << QString(); QTest::newRow("presentation attach unique tabs") << file1 << optionsUnique << true << contentsEpub[0] << 0u << false << false << true << 2u << true << false << QString(); QTest::newRow("print attach unique tabs") << file1 << optionsUnique << true << contentsEpub[0] << 0u << false << false << true << 2u << false << true << QString(); } void MainShellTest::testShell() { QFETCH(QStringList, paths); QFETCH(QString, serializedOptions); QFETCH(bool, useTabs); QFETCH(QString, externalProcessPath); QFETCH(uint, expectedPage); QFETCH(bool, expectPresentation); QFETCH(bool, expectPrintDialog); QFETCH(bool, unique); QFETCH(uint, externalProcessExpectedPage); QFETCH(bool, externalProcessExpectPresentation); QFETCH(bool, externalProcessExpectPrintDialog); QFETCH(QString, externalProcessExpectFind); QScopedPointer helper; Okular::Settings::self()->setShellOpenFileInTabs(useTabs); if (expectPrintDialog || externalProcessExpectPrintDialog) { const int expectedTab = externalProcessExpectPrintDialog && !unique ? 1 : 0; helper.reset(new ClosePrintDialogHelper(expectedTab)); QTimer::singleShot(0, helper.data(), SLOT(closePrintDialog())); } Okular::Status status = Okular::main(paths, serializedOptions); QCOMPARE(status, Okular::Success); Shell *s = findShell(); QVERIFY(s); if (paths.count() == 1) { QCOMPARE(s->m_tabs.count(), 1); Okular::Part *part = s->findChild(); QVERIFY(part); QCOMPARE(part->url().url(), QStringLiteral("file://%1").arg(paths[0])); QCOMPARE(partDocument(part)->currentPage(), expectedPage); // Testing if the bar is shown or hidden as expected QCOMPARE(findWidget(part)->isHidden(), externalProcessExpectFind.isEmpty()); QCOMPARE(findWidget(part)->findChild()->text(), externalProcessExpectFind); // Checking if the encryption/decryption worked QCOMPARE(externalProcessExpectFind, ShellUtils::find(serializedOptions)); } else if (paths.count() == 2) { if (useTabs) { Shell *s = findShell(); QVERIFY(s); Okular::Part *part = dynamic_cast(s->m_tabs[0].part); Okular::Part *part2 = dynamic_cast(s->m_tabs[1].part); QCOMPARE(s->m_tabs.count(), 2); QCOMPARE(part->url().url(), QStringLiteral("file://%1").arg(paths[0])); QCOMPARE(part2->url().url(), QStringLiteral("file://%1").arg(paths[1])); QCOMPARE(partDocument(part)->currentPage(), expectedPage); QCOMPARE(partDocument(part2)->currentPage(), expectedPage); } else { QSet openUrls; Shell *s = findShell(); QVERIFY(s); QCOMPARE(s->m_tabs.count(), 1); Okular::Part *part = s->findChild(); QVERIFY(part); QCOMPARE(partDocument(part)->currentPage(), expectedPage); openUrls << part->url().url(); Shell *s2 = findShell(s); QVERIFY(s2); QCOMPARE(s2->m_tabs.count(), 1); Okular::Part *part2 = s2->findChild(); QVERIFY(part2); QCOMPARE(partDocument(part2)->currentPage(), expectedPage); openUrls << part2->url().url(); foreach(const QString &path, paths) { QVERIFY(openUrls.contains(QStringLiteral("file://%1").arg(path))); } } } if (!externalProcessPath.isEmpty()) { Okular::Part *part = s->findChild(); QProcess p; QStringList args; args << externalProcessPath; if (unique) args << QStringLiteral("-unique"); if (externalProcessExpectedPage != 0) args << QStringLiteral("-page") << QString::number(externalProcessExpectedPage + 1); if (externalProcessExpectPresentation) args << QStringLiteral("-presentation"); if (externalProcessExpectPrintDialog) args << QStringLiteral("-print"); p.start(QStringLiteral(OKULAR_BINARY), args); p.waitForStarted(); QCOMPARE(p.state(), QProcess::Running); if (useTabs || unique) { // It is attaching to us, so will eventually stop QTRY_COMPARE_WITH_TIMEOUT(p.state(), QProcess::NotRunning, 20000); QCOMPARE(p.exitStatus(), QProcess::NormalExit); QCOMPARE(p.exitCode(), 0); if (unique) { // It is unique so part got "overwritten" QCOMPARE(s->m_tabs.count(), 1); QCOMPARE(part->url().url(), QStringLiteral("file://%1").arg(externalProcessPath)); QCOMPARE(partDocument(part)->currentPage(), externalProcessExpectedPage); } else { // It is attaching to us so a second tab is there QCOMPARE(s->m_tabs.count(), 2); Okular::Part *part2 = dynamic_cast(s->m_tabs[1].part); QCOMPARE(part2->url().url(), QStringLiteral("file://%1").arg(externalProcessPath)); QCOMPARE(partDocument(part2)->currentPage(), externalProcessExpectedPage); } } else { QTest::qWait(750); // It opened on a new process, so it is still running, we need to kill it QCOMPARE(p.state(), QProcess::Running); p.terminate(); p.waitForFinished(); QVERIFY(p.state() != QProcess::Running); // It opened on a new process, so no change for us QCOMPARE(s->m_tabs.count(), 1); QCOMPARE(part->url().url(), QStringLiteral("file://%1").arg(paths[0])); QCOMPARE(partDocument(part)->currentPage(), externalProcessExpectedPage); } } if (expectPresentation) { QCOMPARE(paths.count(), 1); Okular::Part *part = s->findChild(); QTRY_VERIFY(presentationWidget(part) != nullptr); } if (externalProcessExpectPresentation) { Okular::Part *part; if (unique) { QCOMPARE(s->m_tabs.count(), 1); part = dynamic_cast(s->m_tabs[0].part); } else { QCOMPARE(s->m_tabs.count(), 2); part = dynamic_cast(s->m_tabs[1].part); } QTRY_VERIFY(presentationWidget(part) != nullptr); } if (helper) { QVERIFY(helper->foundDialog); } } void ClosePrintDialogHelper::closePrintDialog() { Shell *s = findShell(); QPrintDialog *dialog = s->findChild(); if (!dialog) { QTimer::singleShot(0, this, &ClosePrintDialogHelper::closePrintDialog); return; } QVERIFY(dialog); QCOMPARE(MainShellTest::tabWidget(s)->currentIndex(), m_expectedTab); dialog->close(); foundDialog = true; } void MainShellTest::testFileRemembersPagePosition_data() { QTest::addColumn("mode"); QTest::newRow("normal") << 1; QTest::newRow("unique") << 2; QTest::newRow("tabs") << 3; } void MainShellTest::testFileRemembersPagePosition() { QFETCH(int, mode); const QStringList paths = QStringList(QStringLiteral(KDESRCDIR "data/contents.epub")); QString serializedOptions; if (mode == 1 || mode == 3) serializedOptions = ShellUtils::serializeOptions(false, false, false, false, false, QString(), QString()); else serializedOptions = ShellUtils::serializeOptions(false, false, false, true, false, QString(), QString()); Okular::Settings::self()->setShellOpenFileInTabs(mode == 3); Okular::Status status = Okular::main(paths, serializedOptions); QCOMPARE(status, Okular::Success); Shell *s = findShell(); QVERIFY(s); Okular::Part *part = s->findChild(); QVERIFY(part); QCOMPARE(part->url().url(), QStringLiteral("file://%1").arg(paths[0])); QCOMPARE(partDocument(part)->currentPage(), 0u); partDocument(part)->setViewportPage(3); QCOMPARE(partDocument(part)->currentPage(), 3u); s->closeUrl(); QCOMPARE(part->url().url(), QString()); if (mode == 1) { delete s; status = Okular::main(paths, serializedOptions); QCOMPARE(status, Okular::Success); } else { QProcess p; QStringList args; args << paths[0]; if (mode == 2) args << QStringLiteral("-unique"); p.start(QStringLiteral(OKULAR_BINARY), args); p.waitForStarted(); QCOMPARE(p.state(), QProcess::Running); // It is attaching to us, so will eventually stop QTRY_COMPARE_WITH_TIMEOUT((int)p.state(), (int)QProcess::NotRunning, 20000); QCOMPARE((int)p.exitStatus(), (int)QProcess::NormalExit); QCOMPARE(p.exitCode(), 0); } s = findShell(); QVERIFY(s); part = s->findChild(); QVERIFY(part); QCOMPARE(part->url().url(), QStringLiteral("file://%1").arg(paths[0])); QCOMPARE(partDocument(part)->currentPage(), 3u); } void MainShellTest::test2FilesError_data() { QTest::addColumn("serializedOptions"); QTest::newRow("startInPresentation") << ShellUtils::serializeOptions(true, false, false, false, false, QString(), QString()); QTest::newRow("showPrintDialog") << ShellUtils::serializeOptions(false, true, false, false, false, QString(), QString()); QTest::newRow("unique") << ShellUtils::serializeOptions(false, false, false, true, false, QString(), QString()); QTest::newRow("pageNumber") << ShellUtils::serializeOptions(false, false, false, false, false, QStringLiteral("3"), QString()); QTest::newRow("find") << ShellUtils::serializeOptions(false, false, false, false, false, QString(), QStringLiteral("silly")); } void MainShellTest::test2FilesError() { QFETCH(QString, serializedOptions); QStringList paths; paths << QStringLiteral(KDESRCDIR "data/file1.pdf") << QStringLiteral(KDESRCDIR "data/tocreload.pdf"); Okular::Status status = Okular::main(paths, serializedOptions); QCOMPARE(status, Okular::Error); Shell *s = findShell(); QVERIFY(!s); } void MainShellTest::testSessionRestore_data() { QTest::addColumn("paths"); QTest::addColumn("options"); QTest::addColumn("useTabsOpen"); QTest::addColumn("useTabsRestore"); QStringList oneDocPaths(QStringLiteral( KDESRCDIR "data/file1.pdf" ) ); QStringList twoDocPaths( oneDocPaths ); twoDocPaths << QStringLiteral(KDESRCDIR "data/formSamples.pdf"); const QString options = ShellUtils::serializeOptions(false, false, false, false, false, QString(), QString()); QTest::newRow("1 doc, 1 window, tabs") << oneDocPaths << options << true << true; QTest::newRow("2 docs, 1 window, tabs") << twoDocPaths << options << true << true; QTest::newRow("2 docs, 2 windows, tabs") << twoDocPaths << options << false << true; QTest::newRow("2 docs, 2 windows, no tabs") << twoDocPaths << options << false << false; QTest::newRow("2 docs, 1 window, no tabs") << twoDocPaths << options << true << false; } void MainShellTest::testSessionRestore() { QFETCH( QStringList, paths ); QFETCH( QString, options ); QFETCH( bool, useTabsOpen ); QFETCH( bool, useTabsRestore ); Okular::Settings::self()->setShellOpenFileInTabs( useTabsOpen ); Okular::Status status = Okular::main( paths, options ); QCOMPARE( status, Okular::Success ); // Gather some information about the state // Verify that the correct number of windows/tabs were opened QList shells = getShells(); QVERIFY( !shells.isEmpty() ); int numDocs = 0; foreach( Shell* shell, shells ) { QVERIFY( QTest::qWaitForWindowExposed( shell ) ); numDocs += shell->m_tabs.size(); } QCOMPARE( numDocs, paths.size() ); QCOMPARE( shells.size(), useTabsOpen ? 1 : paths.size() ); QTest::qWait( 100 ); // Simulate session shutdown. The actual shutdown path comes through // QSessionManager XSMP handlers, then KApplication::commitData/saveState, // then KMWSessionManager::commitData/saveState. Without simulating an X // session manager, the best we can do here is to make a temporary Config // and call KMainWindows save functions directly. QTemporaryFile configFile; QVERIFY( configFile.open() ); int numWindows = 0; { // Scope for config so that we can reconstruct from file KConfig config( configFile.fileName(), KConfig::SimpleConfig ); foreach( Shell* shell, shells ) { shell->savePropertiesInternal( &config, ++numWindows ); // Windows aren't necessarily closed on shutdown, but we'll use // this as a way to trigger the destructor code, which is normally // connected to the aboutToQuit signal shell->close(); } } // Wait for shells to delete themselves. QTest::qWait doesn't do deferred // deletions so we'll set up a full event loop to do that. QEventLoop eventLoop; QTimer::singleShot( 100, &eventLoop, &QEventLoop::quit ); eventLoop.exec( QEventLoop::AllEvents ); shells = getShells(); QVERIFY( shells.isEmpty() ); Okular::Settings::self()->setShellOpenFileInTabs( useTabsRestore ); // Simulate session restore. We can't call KMainWindow::restore() directly // because it asks for info from the session manager, which doesn't know // about our temporary config. But the logic here mostly mirrors restore(). KConfig config( configFile.fileName(), KConfig::SimpleConfig ); for( int i = 1; i <= numWindows; ++i ) { Shell* shell = new Shell; shell->readPropertiesInternal( &config, i ); shell->show(); } // Verify that the restore state is reasonable shells = getShells(); QVERIFY( !shells.isEmpty() ); numDocs = 0; foreach( Shell* shell, shells ) { QVERIFY( QTest::qWaitForWindowExposed( shell ) ); numDocs += shell->m_tabs.size(); } QCOMPARE( numDocs, paths.size() ); QCOMPARE( shells.size(), useTabsRestore ? numWindows : paths.size() ); } QTEST_MAIN( MainShellTest ) #include "mainshelltest.moc" diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp index 6757892f8..b4bca789b 100644 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -1,1823 +1,1823 @@ /*************************************************************************** * Copyright (C) 2013 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include #include "../core/annotations.h" #include "../core/form.h" #include "../core/page.h" #include "../part.h" #include "../ui/toc.h" #include "../ui/sidebar.h" #include "../ui/pageview.h" #include "../generators/poppler/config-okular-poppler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include class CloseDialogHelper : public QObject { Q_OBJECT public: CloseDialogHelper(Okular::Part *p, QDialogButtonBox::StandardButton b) : m_part(p), m_button(b), m_clicked(false) { QTimer::singleShot(0, this, &CloseDialogHelper::closeDialog); } ~CloseDialogHelper() { QVERIFY(m_clicked); } private slots: void closeDialog() { QDialog *dialog = m_part->widget()->findChild(); if (!dialog) { QTimer::singleShot(0, this, &CloseDialogHelper::closeDialog); return; } QDialogButtonBox *buttonBox = dialog->findChild(); buttonBox->button(m_button)->click(); m_clicked = true; } private: Okular::Part *m_part; QDialogButtonBox::StandardButton m_button; bool m_clicked; }; namespace Okular { class PartTest : public QObject { Q_OBJECT static bool openDocument(Okular::Part *part, const QString &filePath); signals: void urlHandler(const QUrl &url); private slots: void testReload(); void testCanceledReload(); void testTOCReload(); void testForwardPDF(); void testForwardPDF_data(); void testGeneratorPreferences(); void testSelectText(); void testClickInternalLink(); void testOpenUrlArguments(); void test388288(); void testSaveAs(); void testSaveAs_data(); void testSaveAsToNonExistingPath(); void testSaveAsToSymlink(); void testSaveIsSymlink(); void testSidebarItemAfterSaving(); void testSaveAsUndoStackAnnotations(); void testSaveAsUndoStackAnnotations_data(); void testSaveAsUndoStackForms(); void testSaveAsUndoStackForms_data(); void testMouseMoveOverLinkWhileInSelectionMode(); void testClickUrlLinkWhileInSelectionMode(); void testeTextSelectionOverAndAcrossLinks_data(); void testeTextSelectionOverAndAcrossLinks(); void testClickUrlLinkWhileLinkTextIsSelected(); void testRClickWhileLinkTextIsSelected(); void testRClickOverLinkWhileLinkTextIsSelected(); void testRClickOnSelectionModeShoulShowFollowTheLinkMenu(); void testClickAnywhereAfterSelectionShouldUnselect(); void testeRectSelectionStartingOnLinks(); void testCheckBoxReadOnly(); void testCrashTextEditDestroy(); void testAnnotWindow(); void testAdditionalActionTriggers(); void testJumpToPage(); private: void simulateMouseSelection(double startX, double startY, double endX, double endY, QWidget *target); }; class PartThatHijacksQueryClose : public Okular::Part { public: PartThatHijacksQueryClose(QWidget* parentWidget, QObject* parent, const QVariantList& args) : Okular::Part(parentWidget, parent, args), behavior(PassThru) {} enum Behavior { PassThru, ReturnTrue, ReturnFalse }; void setQueryCloseBehavior(Behavior new_behavior) { behavior = new_behavior; } bool queryClose() override { if (behavior == PassThru) return Okular::Part::queryClose(); else // ReturnTrue or ReturnFalse return (behavior == ReturnTrue); } private: Behavior behavior; }; bool PartTest::openDocument(Okular::Part *part, const QString &filePath) { part->openDocument( filePath ); return part->m_document->isOpened(); } // Test that Okular doesn't crash after a successful reload void PartTest::testReload() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")) ); part.reload(); qApp->processEvents(); } // Test that Okular doesn't crash after a canceled reload void PartTest::testCanceledReload() { QVariantList dummyArgs; PartThatHijacksQueryClose part(nullptr, nullptr, dummyArgs); QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")) ); // When queryClose() returns false, the reload operation is canceled (as if // the user had chosen Cancel in the "Save changes?" message box) part.setQueryCloseBehavior(PartThatHijacksQueryClose::ReturnFalse); part.reload(); qApp->processEvents(); } void PartTest::testTOCReload() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/tocreload.pdf")) ); QCOMPARE(part.m_toc->expandedNodes().count(), 0); part.m_toc->m_treeView->expandAll(); QCOMPARE(part.m_toc->expandedNodes().count(), 3); part.reload(); qApp->processEvents(); QCOMPARE(part.m_toc->expandedNodes().count(), 3); } void PartTest::testForwardPDF() { QFETCH(QString, dir); QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); // Create temp dir named like this: ${system temp dir}/${random string}/${dir} const QTemporaryDir tempDir; const QDir workDir(QDir(tempDir.path()).filePath(dir)); workDir.mkpath(QStringLiteral(".")); QFile f(QStringLiteral(KDESRCDIR "data/synctextest.tex")); const QString texDestination = workDir.path() + QStringLiteral("/synctextest.tex"); QVERIFY(f.copy(texDestination)); QProcess process; process.setWorkingDirectory(workDir.path()); - const QString pdflatexPath(QStandardPaths::findExecutable("pdflatex")); + const QString pdflatexPath(QStandardPaths::findExecutable(QStringLiteral("pdflatex"))); if (pdflatexPath.isEmpty()) { QFAIL("pdflatex executable not found, but needed for the test. Try installing the respective TeXLive packages."); } process.start(pdflatexPath, QStringList() << QStringLiteral("-synctex=1") << QStringLiteral("-interaction=nonstopmode") << texDestination); bool started = process.waitForStarted(); if (!started) { qDebug() << "start error:" << process.error(); qDebug() << "start stdout:" << process.readAllStandardOutput(); qDebug() << "start stderr:" << process.readAllStandardError(); } QVERIFY(started); process.waitForFinished(); if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { qDebug() << "exit error:" << process.error() << "status" << process.exitStatus() << "code" << process.exitCode(); qDebug() << "exit stdout:" << process.readAllStandardOutput(); qDebug() << "exit stderr:" << process.readAllStandardError(); } const QString pdfResult = workDir.path() + QStringLiteral("/synctextest.pdf"); QVERIFY(QFile::exists(pdfResult)); QVERIFY( openDocument(&part, pdfResult) ); part.m_document->setViewportPage(0); QCOMPARE(part.m_document->currentPage(), 0u); part.closeUrl(); QUrl u(QUrl::fromLocalFile(pdfResult)); u.setFragment(QStringLiteral("src:100") + texDestination); part.openUrl(u); QCOMPARE(part.m_document->currentPage(), 1u); } void PartTest::testForwardPDF_data() { QTest::addColumn("dir"); - QTest::newRow("non-utf8") << QString::fromUtf8("synctextest"); - QTest::newRow("utf8") << QString::fromUtf8("ßðđđŋßðđŋ"); + QTest::newRow("non-utf8") << QStringLiteral("synctextest"); + QTest::newRow("utf8") << QStringLiteral("ßðđđŋßðđŋ"); } void PartTest::testGeneratorPreferences() { KConfigDialog * dialog; QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); // Test that we don't crash while opening the dialog dialog = part.slotGeneratorPreferences(); qApp->processEvents(); delete dialog; // closes the dialog and recursively destroys all widgets // Test that we don't crash while opening a new instance of the dialog // This catches attempts to reuse widgets that have been destroyed dialog = part.slotGeneratorPreferences(); qApp->processEvents(); delete dialog; } void PartTest::testSelectText() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf"))); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const int mouseY = height * 0.052; const int mouseStartX = width * 0.12; const int mouseEndX = width * 0.7; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); QApplication::clipboard()->clear(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection")); QCOMPARE(QApplication::clipboard()->text(), QStringLiteral("Hola que tal\n")); } void PartTest::testClickInternalLink() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf"))); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseNormal"); QCOMPARE(part.m_document->currentPage(), 0u); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.17, height * 0.05)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.17, height * 0.05)); QTRY_COMPARE(part.m_document->currentPage(), 1u); } // cursor switches to Hand when hovering over link in TextSelect mode. void PartTest::testMouseMoveOverLinkWhileInSelectionMode() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); // move mouse over link QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.250, height * 0.127)); // check if mouse icon changed to proper icon QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::PointingHandCursor); } // clicking on hyperlink jumps to destination in TextSelect mode. void PartTest::testClickUrlLinkWhileInSelectionMode() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); // overwrite urlHandler for 'mailto' urls - QDesktopServices::setUrlHandler("mailto", this, "urlHandler"); - QSignalSpy openUrlSignalSpy(this, SIGNAL(urlHandler(QUrl))); + QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "urlHandler"); + QSignalSpy openUrlSignalSpy(this, &PartTest::urlHandler); // click on url QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.250, height * 0.127)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.250, height * 0.127)); // expect that the urlHandler signal was called QTRY_COMPARE(openUrlSignalSpy.count(), 1); QList arguments = openUrlSignalSpy.takeFirst(); QCOMPARE(arguments.at(0).value(), QUrl("mailto:foo@foo.bar")); } void PartTest::testeTextSelectionOverAndAcrossLinks_data() { QTest::addColumn("mouseStartX"); QTest::addColumn("mouseEndX"); QTest::addColumn("expectedResult"); // can text-select "over and across" hyperlink. QTest::newRow("start selection before link") << 0.1564 << 0.2943 << QStringLiteral(" a link: foo@foo.b"); // can text-select starting at text and ending selection in middle of hyperlink. QTest::newRow("start selection in the middle of the link") << 0.28 << 0.382 << QStringLiteral("o.bar\n"); // text selection works when selecting left to right or right to left QTest::newRow("start selection after link") << 0.40 << 0.05 << QStringLiteral("This is a link: foo@foo.bar\n"); } // can text-select "over and across" hyperlink. void PartTest::testeTextSelectionOverAndAcrossLinks() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.127; QFETCH(double, mouseStartX); QFETCH(double, mouseEndX); mouseStartX = width * mouseStartX; mouseEndX = width * mouseEndX; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); QApplication::clipboard()->clear(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection")); QFETCH(QString, expectedResult); QCOMPARE(QApplication::clipboard()->text(), expectedResult); } // can jump to link while there's an active selection of text. void PartTest::testClickUrlLinkWhileLinkTextIsSelected() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.127; const double mouseStartX = width * 0.13; const double mouseEndX = width * 0.40; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // overwrite urlHandler for 'mailto' urls - QDesktopServices::setUrlHandler("mailto", this, "urlHandler"); - QSignalSpy openUrlSignalSpy(this, SIGNAL(urlHandler(QUrl))); + QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "urlHandler"); + QSignalSpy openUrlSignalSpy(this, &PartTest::urlHandler); // click on url const double mouseClickX = width * 0.2997; const double mouseClickY = height * 0.1293; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // expect that the urlHandler signal was called QTRY_COMPARE(openUrlSignalSpy.count(), 1); QList arguments = openUrlSignalSpy.takeFirst(); QCOMPARE(arguments.at(0).value(), QUrl("mailto:foo@foo.bar")); } // r-click on the selected text gives the "Go To:" content menu option void PartTest::testRClickWhileLinkTextIsSelected() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.162; const double mouseStartX = width * 0.42; const double mouseEndX = width * 0.60; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { // check if popup menu is active and visible - QMenu *menu = qobject_cast(view->findChild("PopupMenu")); + QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains go-to link action - QAction *goToAction = qobject_cast(menu->findChild("GoToAction")); + QAction *goToAction = qobject_cast(menu->findChild(QStringLiteral("GoToAction"))); QVERIFY(goToAction); // check if the "follow this link" action is not visible - QAction *processLinkAction = qobject_cast(menu->findChild("ProcessLinkAction")); + QAction *processLinkAction = qobject_cast(menu->findChild(QStringLiteral("ProcessLinkAction"))); QVERIFY(!processLinkAction); // check if the "copy link address" action is not visible - QAction *copyLinkLocation = qobject_cast(menu->findChild("CopyLinkLocationAction")); + QAction *copyLinkLocation = qobject_cast(menu->findChild(QStringLiteral("CopyLinkLocationAction"))); QVERIFY(!copyLinkLocation); // close menu to continue test menu->close(); menuClosed = true; }); // click on url const double mouseClickX = width * 0.425; const double mouseClickY = height * 0.162; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // will continue after pop-menu get closed QTRY_VERIFY(menuClosed); } // r-click on the link gives the "follow this link" content menu option void PartTest::testRClickOverLinkWhileLinkTextIsSelected() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.162; const double mouseStartX = width * 0.42; const double mouseEndX = width * 0.60; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { // check if popup menu is active and visible - QMenu *menu = qobject_cast(view->findChild("PopupMenu")); + QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains "follow this link" action - QAction *processLinkAction = qobject_cast(menu->findChild("ProcessLinkAction")); + QAction *processLinkAction = qobject_cast(menu->findChild(QStringLiteral("ProcessLinkAction"))); QVERIFY(processLinkAction); // check if the menu contains "copy link address" action - QAction *copyLinkLocation = qobject_cast(menu->findChild("CopyLinkLocationAction")); + QAction *copyLinkLocation = qobject_cast(menu->findChild(QStringLiteral("CopyLinkLocationAction"))); QVERIFY(copyLinkLocation); // close menu to continue test menu->close(); menuClosed = true; }); // click on url const double mouseClickX = width * 0.593; const double mouseClickY = height * 0.162; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // will continue after pop-menu get closed QTRY_VERIFY(menuClosed); } void PartTest::testRClickOnSelectionModeShoulShowFollowTheLinkMenu() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { // check if popup menu is active and visible - QMenu *menu = qobject_cast(view->findChild("PopupMenu")); + QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains "Follow this link" action - QAction *processLink = qobject_cast(menu->findChild("ProcessLinkAction")); + QAction *processLink = qobject_cast(menu->findChild(QStringLiteral("ProcessLinkAction"))); QVERIFY(processLink); // chek if the menu contains "Copy Link Address" action - QAction *actCopyLinkLocation = qobject_cast(menu->findChild("CopyLinkLocationAction")); + QAction *actCopyLinkLocation = qobject_cast(menu->findChild(QStringLiteral("CopyLinkLocationAction"))); QVERIFY(actCopyLinkLocation); // close menu to continue test menu->close(); menuClosed = true; }); // r-click on url const double mouseClickX = width * 0.604; const double mouseClickY = height * 0.162; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // will continue after pop-menu get closed QTRY_VERIFY(menuClosed); } void PartTest::testClickAnywhereAfterSelectionShouldUnselect() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.162; const double mouseStartX = width * 0.42; const double mouseEndX = width * 0.60; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // click on url const double mouseClickX = width * 0.10; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(mouseClickX, mouseY), 1000); QApplication::clipboard()->clear(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection")); // check if copied text is empty what means no text selected QVERIFY(QApplication::clipboard()->text().isEmpty()); } void PartTest::testeRectSelectionStartingOnLinks() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseSelect")); const double mouseStartY = height * 0.127; const double mouseEndY = height * 0.127; const double mouseStartX = width * 0.28; const double mouseEndX = width * 0.382; // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { QApplication::clipboard()->clear(); // check if popup menu is active and visible - QMenu *menu = qobject_cast(view->findChild("PopupMenu")); + QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the copy selected text to clipboard is present - QAction *copyAct = qobject_cast(menu->findChild("CopyTextToClipboard")); + QAction *copyAct = qobject_cast(menu->findChild(QStringLiteral("CopyTextToClipboard"))); QVERIFY(copyAct); menu->close(); menuClosed = true; }); simulateMouseSelection(mouseStartX, mouseStartY, mouseEndX, mouseEndY, part.m_pageView->viewport()); // wait menu get closed QTRY_VERIFY(menuClosed); } void PartTest::simulateMouseSelection(double startX, double startY, double endX, double endY, QWidget *target) { const int steps = 5; const double diffX = endX - startX; const double diffY = endY - startY; const double diffXStep = diffX / steps; const double diffYStep = diffY / steps; QTestEventList events; events.addMouseMove(QPoint(startX, startY)); events.addMousePress(Qt::LeftButton, Qt::NoModifier, QPoint(startX, startY)); for (int i = 0; i < steps - 1; ++i) { events.addMouseMove(QPoint(startX + i * diffXStep, startY + i * diffYStep)); events.addDelay(100); } events.addMouseMove(QPoint(endX, endY)); events.addDelay(100); events.addMouseRelease(Qt::LeftButton, Qt::NoModifier, QPoint(endX, endY)); events.simulate(target); } void PartTest::testSaveAsToNonExistingPath() { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( KDESRCDIR "data/file1.pdf" ); QString saveFilePath; { - QTemporaryFile saveFile( QString( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); + QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); saveFile.open(); saveFilePath = saveFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QVERIFY( !QFileInfo::exists( saveFilePath ) ); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFilePath ), Part::NoSaveAsFlags ) ); QFile::remove( saveFilePath ); } void PartTest::testSaveAsToSymlink() { #ifdef Q_OS_UNIX Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( KDESRCDIR "data/file1.pdf" ); - QTemporaryFile newFile( QString( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); + QTemporaryFile newFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); newFile.open(); QString linkFilePath; { - QTemporaryFile linkFile( QString( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); + QTemporaryFile linkFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); linkFile.open(); linkFilePath = linkFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QFile::link( newFile.fileName(), linkFilePath ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); QVERIFY( part.saveAs( QUrl::fromLocalFile( linkFilePath ), Part::NoSaveAsFlags ) ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); QFile::remove( linkFilePath ); #endif } void PartTest::testSaveIsSymlink() { #ifdef Q_OS_UNIX Okular::Part part(nullptr, nullptr, QVariantList()); QString newFilePath; { - QTemporaryFile newFile( QString( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); + QTemporaryFile newFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); newFile.open(); newFilePath = newFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QFile::copy( KDESRCDIR "data/file1.pdf", newFilePath ); QString linkFilePath; { - QTemporaryFile linkFile( QString( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); + QTemporaryFile linkFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); linkFile.open(); linkFilePath = linkFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QFile::link( newFilePath, linkFilePath ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); part.openDocument( linkFilePath ); QVERIFY( part.saveAs( QUrl::fromLocalFile( linkFilePath ), Part::NoSaveAsFlags ) ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); QFile::remove( newFilePath ); QFile::remove( linkFilePath ); #endif } void PartTest::testSaveAs() { QFETCH(QString, file); QFETCH(QString, extension); QFETCH(bool, nativelySupportsAnnotations); QFETCH(bool, canSwapBackingFile); QScopedPointer closeDialogHelper; QString annotName; - QTemporaryFile archiveSave( 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 ) ); + QTemporaryFile archiveSave( QStringLiteral( "%1/okrXXXXXX.okular" ).arg( QDir::tempPath() ) ); + QTemporaryFile nativeDirectSave( QStringLiteral( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); + QTemporaryFile nativeFromArchiveFile( QStringLiteral( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); QVERIFY( archiveSave.open() ); archiveSave.close(); QVERIFY( nativeDirectSave.open() ); nativeDirectSave.close(); QVERIFY( nativeFromArchiveFile.open() ); nativeFromArchiveFile.close(); qDebug() << "Open file, add annotation and save both natively and to .okular"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( file ); part.m_document->documentInfo(); QCOMPARE(part.m_document->canSwapBackingFile(), canSwapBackingFile); Okular::Annotation *annot = new Okular::TextAnnotation(); annot->setBoundingRectangle( Okular::NormalizedRect( 0.1, 0.1, 0.15, 0.15 ) ); - annot->setContents( "annot contents" ); + annot->setContents( QStringLiteral("annot contents") ); part.m_document->addPageAnnotation( 0, annot ); annotName = annot->uniqueName(); if ( canSwapBackingFile ) { if ( !nativelySupportsAnnotations ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); // For backends that don't support annotations natively we mark the part as still modified // after a save because we keep the annotation around but it will get lost if the user closes the app // so we want to give her a last chance to save on close with the "you have changes dialog" QCOMPARE( part.isModified(), !nativelySupportsAnnotations ); QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), Part::SaveAsOkularArchive ) ); } else { // We need to save to archive first otherwise we lose the annotation closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::Yes )); // this is the "you're going to lose the undo/redo stack" dialog QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), Part::SaveAsOkularArchive ) ); if ( !nativelySupportsAnnotations ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); } QCOMPARE( part.m_document->documentInfo().get( Okular::DocumentInfo::FilePath ), part.m_document->currentDocument().toDisplayString() ); part.closeUrl(); } qDebug() << "Open the .okular, check that the annotation is present and save to native"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( archiveSave.fileName() ); part.m_document->documentInfo(); QCOMPARE( part.m_document->page( 0 )->annotations().size(), 1 ); QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); if ( !nativelySupportsAnnotations ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeFromArchiveFile.fileName() ), Part::NoSaveAsFlags ) ); if ( canSwapBackingFile && !nativelySupportsAnnotations ) { // For backends that don't support annotations natively we mark the part as still modified // after a save because we keep the annotation around but it will get lost if the user closes the app // so we want to give her a last chance to save on close with the "you have changes dialog" closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "do you want to save or discard" dialog } QCOMPARE( part.m_document->documentInfo().get( Okular::DocumentInfo::FilePath ), part.m_document->currentDocument().toDisplayString() ); part.closeUrl(); } qDebug() << "Open the native file saved directly, and check that the annot" << "is there iff we expect it"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( nativeDirectSave.fileName() ); QCOMPARE( part.m_document->page( 0 )->annotations().size(), nativelySupportsAnnotations ? 1 : 0 ); if ( nativelySupportsAnnotations ) QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); part.closeUrl(); } qDebug() << "Open the native file saved from the .okular, and check that the annot" << "is there iff we expect it"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( nativeFromArchiveFile.fileName() ); QCOMPARE( part.m_document->page( 0 )->annotations().size(), nativelySupportsAnnotations ? 1 : 0 ); if ( nativelySupportsAnnotations ) QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); part.closeUrl(); } } void PartTest::testSaveAs_data() { QTest::addColumn("file"); QTest::addColumn("extension"); QTest::addColumn("nativelySupportsAnnotations"); QTest::addColumn("canSwapBackingFile"); QTest::newRow("pdf") << KDESRCDIR "data/file1.pdf" << "pdf" << true << true; QTest::newRow("pdf.gz") << KDESRCDIR "data/file1.pdf.gz" << "pdf" << true << true; QTest::newRow("epub") << KDESRCDIR "data/contents.epub" << "epub" << false << false; QTest::newRow("jpg") << KDESRCDIR "data/potato.jpg" << "jpg" << false << true; } void PartTest::testSidebarItemAfterSaving() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QWidget *currentSidebarItem = part.m_sidebar->currentItem(); // thumbnails openDocument(&part, QStringLiteral(KDESRCDIR "data/tocreload.pdf")); // since it has TOC it changes to TOC QVERIFY(currentSidebarItem != part.m_sidebar->currentItem()); // now change back to thumbnails part.m_sidebar->setCurrentItem(currentSidebarItem); part.saveAs(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/tocreload.pdf"))); // Check it is still thumbnails after saving QCOMPARE(currentSidebarItem, part.m_sidebar->currentItem()); } void PartTest::testSaveAsUndoStackAnnotations() { QFETCH(QString, file); QFETCH(QString, extension); QFETCH(bool, nativelySupportsAnnotations); QFETCH(bool, canSwapBackingFile); QFETCH(bool, saveToArchive); const Part::SaveAsFlag saveFlags = saveToArchive ? Part::SaveAsOkularArchive : Part::NoSaveAsFlags; QScopedPointer closeDialogHelper; // closeDialogHelper relies on the availability of the "Continue" button to drop changes // when saving to a file format not supporting those. However, this button is only sensible // and available for "Save As", but not for "Save". By alternately saving to saveFile1 and // saveFile2 we always force "Save As", so closeDialogHelper keeps working. - QTemporaryFile saveFile1( QString( "%1/okrXXXXXX_1.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); + QTemporaryFile saveFile1( QStringLiteral( "%1/okrXXXXXX_1.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); QVERIFY( saveFile1.open() ); saveFile1.close(); - QTemporaryFile saveFile2( QString( "%1/okrXXXXXX_2.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); + QTemporaryFile saveFile2( QStringLiteral( "%1/okrXXXXXX_2.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); QVERIFY( saveFile2.open() ); saveFile2.close(); Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( file ); QCOMPARE(part.m_document->canSwapBackingFile(), canSwapBackingFile); Okular::Annotation *annot = new Okular::TextAnnotation(); annot->setBoundingRectangle( Okular::NormalizedRect( 0.1, 0.1, 0.15, 0.15 ) ); - annot->setContents( "annot contents" ); + annot->setContents( QStringLiteral("annot contents") ); part.m_document->addPageAnnotation( 0, annot ); QString annotName = annot->uniqueName(); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); if (!canSwapBackingFile) { // The undo/redo stack gets lost if you can not swap the backing file QVERIFY( !part.m_document->canUndo() ); QVERIFY( !part.m_document->canRedo() ); return; } // Check we can still undo the annot add after save QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Check we can redo the annot add after save QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( !part.m_document->canRedo() ); if ( nativelySupportsAnnotations ) { // If the annots are provided by the backend we need to refetch the pointer after save annot = part.m_document->page( 0 )->annotation( annotName ); QVERIFY( annot ); } // Remove the annotation, creates another undo command QVERIFY( part.m_document->canRemovePageAnnotation( annot ) ); part.m_document->removePageAnnotation( 0, annot ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Check we can still undo the annot remove after save QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); QCOMPARE( part.m_document->page( 0 )->annotations().count(), 1 ); // Check we can still undo the annot add after save if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Redo the add annotation QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canUndo() ); QVERIFY( part.m_document->canRedo() ); if ( nativelySupportsAnnotations ) { // If the annots are provided by the backend we need to refetch the pointer after save annot = part.m_document->page( 0 )->annotation( annotName ); QVERIFY( annot ); } // Add translate, adjust and modify commands part.m_document->translatePageAnnotation( 0, annot, Okular::NormalizedPoint( 0.1, 0.1 ) ); part.m_document->adjustPageAnnotation( 0, annot, Okular::NormalizedPoint( 0.1, 0.1 ), Okular::NormalizedPoint( 0.1, 0.1 ) ); part.m_document->prepareToModifyAnnotationProperties( annot ); part.m_document->modifyPageAnnotationProperties( 0, annot ); // Now check we can still undo/redo/save at all the intermediate states and things still work if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.m_document->canRedo() ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( !part.m_document->canRedo() ); closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "do you want to save or discard" dialog part.closeUrl(); } void PartTest::testSaveAsUndoStackAnnotations_data() { QTest::addColumn("file"); QTest::addColumn("extension"); QTest::addColumn("nativelySupportsAnnotations"); QTest::addColumn("canSwapBackingFile"); QTest::addColumn("saveToArchive"); QTest::newRow("pdf") << KDESRCDIR "data/file1.pdf" << "pdf" << true << true << false; QTest::newRow("epub") << KDESRCDIR "data/contents.epub" << "epub" << false << false << false; QTest::newRow("jpg") << KDESRCDIR "data/potato.jpg" << "jpg" << false << true << false; QTest::newRow("pdfarchive") << KDESRCDIR "data/file1.pdf" << "okular" << true << true << true; QTest::newRow("jpgarchive") << KDESRCDIR "data/potato.jpg" << "okular" << false << true << true; } void PartTest::testSaveAsUndoStackForms() { QFETCH(QString, file); QFETCH(QString, extension); QFETCH(bool, saveToArchive); const Part::SaveAsFlag saveFlags = saveToArchive ? Part::SaveAsOkularArchive : Part::NoSaveAsFlags; - QTemporaryFile saveFile( QString( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath(), extension ) ); + QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath(), extension ) ); QVERIFY( saveFile.open() ); saveFile.close(); Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( file ); for ( FormField *ff : part.m_document->page( 0 )->formFields() ) { if ( ff->id() == 65537 ) { QCOMPARE( ff->type(), FormField::FormText ); FormFieldText *fft = static_cast( ff ); - part.m_document->editFormText( 0, fft, "BlaBla", 6, 0, 0 ); + part.m_document->editFormText( 0, fft, QStringLiteral("BlaBla"), 6, 0, 0 ); } else if ( ff->id() == 65538 ) { QCOMPARE( ff->type(), FormField::FormButton ); FormFieldButton *ffb = static_cast( ff ); QCOMPARE( ffb->buttonType(), FormFieldButton::Radio ); part.m_document->editFormButtons( 0, QList< FormFieldButton* >() << ffb, QList< bool >() << true ); } else if ( ff->id() == 65542 ) { QCOMPARE( ff->type(), FormField::FormChoice ); FormFieldChoice *ffc = static_cast( ff ); QCOMPARE( ffc->choiceType(), FormFieldChoice::ListBox ); part.m_document->editFormList( 0, ffc, QList< int >() << 1 ); } else if ( ff->id() == 65543 ) { QCOMPARE( ff->type(), FormField::FormChoice ); FormFieldChoice *ffc = static_cast( ff ); QCOMPARE( ffc->choiceType(), FormFieldChoice::ComboBox ); - part.m_document->editFormCombo( 0, ffc, "combo2", 3, 0, 0); + part.m_document->editFormCombo( 0, ffc, QStringLiteral("combo2"), 3, 0, 0); } } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); } void PartTest::testSaveAsUndoStackForms_data() { QTest::addColumn("file"); QTest::addColumn("extension"); QTest::addColumn("saveToArchive"); QTest::newRow("pdf") << KDESRCDIR "data/formSamples.pdf" << "pdf" << false; QTest::newRow("pdfarchive") << KDESRCDIR "data/formSamples.pdf" << "okular" << true; } void PartTest::testOpenUrlArguments() { QVariantList dummyArgs; Okular::Part part(NULL, NULL, dummyArgs); KParts::OpenUrlArguments args; args.setMimeType(QStringLiteral("text/rtf")); part.setArguments(args); part.openUrl(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/file1.pdf"))); QCOMPARE( part.arguments().mimeType(), QStringLiteral("text/rtf") ); } void PartTest::test388288() { Okular::Part part(nullptr, nullptr, QVariantList()); part.openUrl(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/file1.pdf"))); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); QMetaObject::invokeMethod(part.m_pageView, "slotToggleAnnotator", Q_ARG( bool, true )); auto annot = new Okular::HighlightAnnotation(); annot->setHighlightType( Okular::HighlightAnnotation::Highlight ); const Okular::NormalizedRect r(0.36, 0.16, 0.51, 0.17); annot->setBoundingRectangle( r ); Okular::HighlightAnnotation::Quad q; q.setCapStart( false ); q.setCapEnd( false ); q.setFeather( 1.0 ); q.setPoint( Okular::NormalizedPoint( r.left, r.bottom ), 0 ); q.setPoint( Okular::NormalizedPoint( r.right, r.bottom ), 1 ); q.setPoint( Okular::NormalizedPoint( r.right, r.top ), 2 ); q.setPoint( Okular::NormalizedPoint( r.left, r.top ), 3 ); annot->highlightQuads().append( q ); part.m_document->addPageAnnotation( 0, annot ); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.5, height * 0.5)); QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::OpenHandCursor); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.4, height * 0.165)); QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::ArrowCursor); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.1, height * 0.165)); part.m_document->undo(); annot = new Okular::HighlightAnnotation(); annot->setHighlightType( Okular::HighlightAnnotation::Highlight ); annot->setBoundingRectangle( r ); annot->highlightQuads().append( q ); part.m_document->addPageAnnotation( 0, annot ); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.5, height * 0.5)); QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::OpenHandCursor); } void PartTest::testCheckBoxReadOnly() { #ifndef HAVE_POPPLER_0_64 return; #endif const QString testFile = QStringLiteral( KDESRCDIR "data/checkbox_ro.pdf" ); Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); // The test document uses the activation action of checkboxes // to update the read only state. For this we need the part so that // undo / redo activates the activation action. QVERIFY( part.m_document->isOpened() ); const Okular::Page* page = part.m_document->page( 0 ); QMap fields; // Field names in test document are: // CBMakeRW, CBMakeRO, TargetDefaultRO, TargetDefaultRW for ( Okular::FormField *ff: page->formFields() ) { fields.insert( ff->name(), static_cast< Okular::FormField* >( ff ) ); } // First grab all fields and check that the setup is as expected. auto cbMakeRW = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRW" )] ); auto cbMakeRO = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRO" )] ); auto targetDefaultRW = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRw" )] ); auto targetDefaultRO = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRo" )] ); QVERIFY( cbMakeRW ); QVERIFY( cbMakeRO ); QVERIFY( targetDefaultRW ); QVERIFY( targetDefaultRO ); QVERIFY( !cbMakeRW->state() ); QVERIFY( !cbMakeRO->state() ); QVERIFY( !targetDefaultRW->isReadOnly() ); QVERIFY( targetDefaultRO->isReadOnly() ); QList< Okular::FormFieldButton* > btns; btns << cbMakeRW << cbMakeRO; // Now check both boxes QList< bool > btnStates; btnStates << true << true; part.m_document->editFormButtons( 0, btns, btnStates ); // Read only should be inverted QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( !targetDefaultRO->isReadOnly() ); // Test that undo / redo works QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !targetDefaultRW->isReadOnly() ); QVERIFY( targetDefaultRO->isReadOnly() ); part.m_document->redo(); QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( !targetDefaultRO->isReadOnly() ); btnStates.clear(); btnStates << false << true; part.m_document->editFormButtons( 0, btns, btnStates ); QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( targetDefaultRO->isReadOnly() ); // Now set both to checked again and confirm that // save / load works. btnStates.clear(); btnStates << true << true; part.m_document->editFormButtons( 0, btns, btnStates ); - QTemporaryFile saveFile( QString( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); + QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); QVERIFY( saveFile.open() ); saveFile.close(); // Save QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), Part::NoSaveAsFlags ) ); part.closeUrl(); // Load part.openDocument( saveFile.fileName() ); QVERIFY( part.m_document->isOpened() ); page = part.m_document->page( 0 ); fields.clear(); for ( Okular::FormField *ff: page->formFields() ) { fields.insert( ff->name(), static_cast< Okular::FormField* >( ff ) ); } cbMakeRW = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRW" )] ); cbMakeRO = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRO" )] ); targetDefaultRW = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRw" )] ); targetDefaultRO = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRo" )] ); QVERIFY( cbMakeRW->state() ); QVERIFY( cbMakeRO->state() ); QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( !targetDefaultRO->isReadOnly() ); } void PartTest::testCrashTextEditDestroy() { const QString testFile = QStringLiteral( KDESRCDIR "data/formSamples.pdf" ); Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); - part.widget()->findChild()->setText("HOLA"); + part.widget()->findChild()->setText(QStringLiteral("HOLA")); part.actionCollection()->action(QStringLiteral("view_toggle_forms"))->trigger(); } void PartTest::testAnnotWindow() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf"))); part.widget()->show(); part.widget()->resize(800, 600); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); 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); // Create two distinct text annotations Okular::Annotation * annot1 = new Okular::TextAnnotation(); annot1->setBoundingRectangle( Okular::NormalizedRect( 0.8, 0.1, 0.85, 0.15 ) ); annot1->setContents( QStringLiteral("Annot contents 111111") ); Okular::Annotation *annot2 = new Okular::TextAnnotation(); annot2->setBoundingRectangle( Okular::NormalizedRect( 0.8, 0.3, 0.85, 0.35 ) ); annot2->setContents( QStringLiteral("Annot contents 222222") ); // Add annot1 and annot2 to document part.m_document->addPageAnnotation( 0, annot1 ); QTest::qWait(100); part.m_document->addPageAnnotation( 0, annot2 ); QTest::qWait(100); QVERIFY( part.m_document->page( 0 )->annotations().size() == 2 ); // Double click the first annotation to open its window (move mouse for visual feedback) const NormalizedPoint annot1pt = annot1->boundingRectangle().center(); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot1pt.x, height * annot1pt.y)); QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot1pt.x, height * annot1pt.y)); QTRY_COMPARE( part.m_pageView->findChildren("AnnotWindow").size(), 1 ); // Verify that the window is visible - QFrame* win1 = part.m_pageView->findChild("AnnotWindow"); + QFrame* win1 = part.m_pageView->findChild(QStringLiteral("AnnotWindow")); QVERIFY( !win1->visibleRegion().isEmpty() ); // Double click the second annotation to open its window (move mouse for visual feedback) const NormalizedPoint annot2pt = annot2->boundingRectangle().center(); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot2pt.x, height * annot2pt.y)); QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot2pt.x, height * annot2pt.y)); QTRY_COMPARE( part.m_pageView->findChildren("AnnotWindow").size(), 2 ); // Verify that the first window is hidden covered by the second, which is visible - QList lstWin = part.m_pageView->findChildren("AnnotWindow"); + QList lstWin = part.m_pageView->findChildren(QStringLiteral("AnnotWindow")); QFrame * win2; if (lstWin[0] == win1) { win2 = lstWin[1]; } else { win2 = lstWin[0]; } QVERIFY( win1->visibleRegion().isEmpty() ); QVERIFY( !win2->visibleRegion().isEmpty() ); // Double click the first annotation to raise its window (move mouse for visual feedback) QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot1pt.x, height * annot1pt.y)); QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot1pt.x, height * annot1pt.y)); // Verify that the second window is hidden covered by the first, which is visible QVERIFY( !win1->visibleRegion().isEmpty() ); QVERIFY( win2->visibleRegion().isEmpty() ); // Move annotation window 1 to partially show annotation window 2 win1->move(QPoint(win2->pos().x(), win2->pos().y() + 50)); // Verify that both windows are partially visible QVERIFY( !win1->visibleRegion().isEmpty() ); QVERIFY( !win2->visibleRegion().isEmpty() ); // Click the second annotation window to raise it (move mouse for visual feedback) auto widget = win2->window()->childAt(win2->mapTo(win2->window(), QPoint(10, 10))); QTest::mouseMove(win2->window(), win2->mapTo(win2->window(), QPoint(10, 10))); QTest::mouseClick(widget, Qt::LeftButton, Qt::NoModifier, widget->mapFrom(win2, QPoint(10, 10))); QVERIFY( win1->visibleRegion().rects().count() == 3); QVERIFY( win2->visibleRegion().rects().count() == 4); } // Helper for testAdditionalActionTriggers static void verifyTargetStates( const QString & triggerName, const QMap &fields, bool focusVisible, bool cursorVisible, bool mouseVisible, int line) { Okular::FormField *focusTarget = fields.value( triggerName + QStringLiteral ("_focus_target") ); Okular::FormField *cursorTarget = fields.value( triggerName + QStringLiteral ("_cursor_target") ); Okular::FormField *mouseTarget = fields.value( triggerName + QStringLiteral ("_mouse_target") ); QVERIFY( focusTarget ); QVERIFY( cursorTarget ); QVERIFY( mouseTarget ); QTRY_VERIFY2( focusTarget->isVisible() == focusVisible, QStringLiteral ("line: %1 focus for %2 not matched. Expected %3 Actual %4"). arg( line ).arg( triggerName ).arg( focusTarget->isVisible() ).arg( focusVisible ).toUtf8().constData() ); QTRY_VERIFY2( cursorTarget->isVisible() == cursorVisible, QStringLiteral ("line: %1 cursor for %2 not matched. Actual %3 Expected %4"). arg( line ).arg( triggerName ).arg( cursorTarget->isVisible() ).arg( cursorVisible ).toUtf8().constData() ); QTRY_VERIFY2( mouseTarget->isVisible() == mouseVisible, QStringLiteral ("line: %1 mouse for %2 not matched. Expected %3 Actual %4"). arg( line ).arg( triggerName ).arg( mouseTarget->isVisible() ).arg( mouseVisible ).toUtf8().constData() ); } void PartTest::testAdditionalActionTriggers() { #ifndef HAVE_POPPLER_0_65 return; #endif const QString testFile = QStringLiteral( KDESRCDIR "data/additionalFormActions.pdf" ); Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); part.widget()->resize(800, 600); part.widget()->show(); QVERIFY( QTest::qWaitForWindowExposed( part.widget() ) ); QMap fields; // Field names in test document are: // For trigger fields: tf, cb, rb, dd, pb // For target fields: _focus_target, _cursor_target, // _mouse_target const Okular::Page* page = part.m_document->page( 0 ); for ( Okular::FormField *ff: page->formFields() ) { fields.insert( ff->name(), static_cast< Okular::FormField* >( ff ) ); } // Verify that everything is set up. verifyTargetStates( QStringLiteral( "tf" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "cb" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "rb" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "dd" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "pb" ), fields, true, true, true, __LINE__ ); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage( 0 ); // wait for pixmap QTRY_VERIFY( part.m_document->page( 0 )->hasPixmap( part.m_pageView) ); part.actionCollection()->action( QStringLiteral( "view_toggle_forms" ) )->trigger(); QPoint tfPos( width * 0.045, height * 0.05 ); QPoint cbPos( width * 0.045, height * 0.08 ); QPoint rbPos( width * 0.045, height * 0.12 ); QPoint ddPos( width * 0.045, height * 0.16 ); QPoint pbPos( width * 0.045, height * 0.26 ); // Test text field auto widget = part.m_pageView->viewport()->childAt( tfPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( tfPos )); verifyTargetStates( QStringLiteral( "tf" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "tf" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "tf" ), fields, false, false, true, __LINE__ ); // Checkbox widget = part.m_pageView->viewport()->childAt( cbPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( cbPos ) ); verifyTargetStates( QStringLiteral( "cb" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "cb" ), fields, false, false, false, __LINE__ ); // Confirm that the textfield no longer has any invisible verifyTargetStates( QStringLiteral( "tf" ), fields, true, true, true, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "cb" ), fields, false, false, true, __LINE__ ); // Radio widget = part.m_pageView->viewport()->childAt( rbPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( rbPos ) ); verifyTargetStates( QStringLiteral( "rb" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "rb" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "rb" ), fields, false, false, true, __LINE__ ); // Dropdown widget = part.m_pageView->viewport()->childAt( ddPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( ddPos ) ); verifyTargetStates( QStringLiteral( "dd" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "dd" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "dd" ), fields, false, false, true, __LINE__ ); // Pushbutton widget = part.m_pageView->viewport()->childAt( pbPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( pbPos ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, true, __LINE__ ); // Confirm that a mouse release outside does not trigger the show action. QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, tfPos ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, false, __LINE__ ); } void PartTest::testJumpToPage() { const QString testFile = QStringLiteral( KDESRCDIR "data/simple-multipage.pdf" ); const int targetPage = 25; Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); part.widget()->resize(800, 600); part.widget()->show(); QVERIFY( QTest::qWaitForWindowExposed( part.widget() ) ); part.m_document->pages(); part.m_document->setViewportPage( targetPage ); /* Document::setViewportPage triggers pixmap rendering in another thread. * We want to test how things look AFTER finished signal arrives back, * because PageView::slotRelayoutPages may displace the viewport again. */ QTRY_VERIFY( part.m_document->page( targetPage )->hasPixmap( part.m_pageView ) ); const int contentAreaHeight = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); const int pageWithSpaceTop = contentAreaHeight / part.m_document->pages() * targetPage; /* * This is a test for a "known by trial" displacement. * We'd need access to part.m_pageView->d->items[targetPage]->croppedGeometry().top(), * to determine the expected viewport position, but we don't have access. */ QCOMPARE(part.m_pageView->verticalScrollBar()->value(), pageWithSpaceTop - 4); } } // namespace Okular int main(int argc, char *argv[]) { // Force consistent locale QLocale locale(QStringLiteral("en_US.UTF-8")); if (locale == QLocale::c()) { // This is the way to check if the above worked locale = QLocale(QLocale::English, QLocale::UnitedStates); } QLocale::setDefault(locale); qputenv("LC_ALL", "en_US.UTF-8"); // For UNIX, third-party libraries // Ensure consistent configs/caches QTemporaryDir homeDir; // QTemporaryDir automatically cleans up when it goes out of scope Q_ASSERT(homeDir.isValid()); QByteArray homePath = QFile::encodeName(homeDir.path()); qDebug() << homePath; qputenv("USERPROFILE", homePath); qputenv("HOME", homePath); qputenv("XDG_DATA_HOME", homePath + "/.local"); qputenv("XDG_CONFIG_HOME", homePath + "/.kde-unit-test/xdg/config"); // Disable fancy debug output qunsetenv("QT_MESSAGE_PATTERN"); QApplication app( argc, argv ); - app.setApplicationName(QLatin1String("okularparttest")); - app.setOrganizationDomain(QLatin1String("kde.org")); + app.setApplicationName(QStringLiteral("okularparttest")); + app.setOrganizationDomain(QStringLiteral("kde.org")); app.setQuitOnLastWindowClosed(false); qRegisterMetaType(); /*as done by kapplication*/ qRegisterMetaType>(); Okular::PartTest test; return QTest::qExec( &test, argc, argv ); } #include "parttest.moc" diff --git a/autotests/searchtest.cpp b/autotests/searchtest.cpp index 6c99895d9..b158949ef 100644 --- a/autotests/searchtest.cpp +++ b/autotests/searchtest.cpp @@ -1,427 +1,427 @@ /*************************************************************************** * Copyright (C) 2013 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. * ***************************************************************************/ #include #include "../core/document.h" #include "../core/page.h" #include "../core/textpage.h" #include "../settings_core.h" Q_DECLARE_METATYPE(Okular::Document::SearchStatus) class SearchFinishedReceiver : public QObject { Q_OBJECT private slots: void searchFinished(int id, Okular::Document::SearchStatus status) { m_id = id; m_status = status; } public: int m_id; Okular::Document::SearchStatus m_status; }; class SearchTest : public QObject { Q_OBJECT private slots: void initTestCase(); void testNextAndPrevious(); void test311232(); void test323262(); void test323263(); void testDottedI(); void testHyphenAtEndOfLineWithoutYOverlap(); void testHyphenWithYOverlap(); void testHyphenAtEndOfPage(); void testOneColumn(); void testTwoColumns(); }; void SearchTest::initTestCase() { qRegisterMetaType(); Okular::SettingsCore::instance( QStringLiteral("searchtest") ); } static void createTextPage(const QVector& text, const QVector& rect, Okular::TextPage*& tp, Okular::Page*& page) { tp = new Okular::TextPage(); for (int i = 0; i < text.size(); i++) { tp->append(text[i], new Okular::NormalizedRect(rect[i])); } //The Page::setTextPage method invokes the layout analysis algorithms tested by some tests here //and also sets the tp->d->m_page field (the latter was used in older versions of Okular by //TextPage::stringLengthAdaptedWithHyphen). //Note that calling "delete page;" will delete the TextPage as well. page = new Okular::Page(1, 100, 100, Okular::Rotation0); page -> setTextPage(tp); } #define CREATE_PAGE \ QCOMPARE(text.size(), rect.size()); \ Okular::Page* page; \ Okular::TextPage* tp; \ createTextPage(text, rect, tp, page); #define TEST_NEXT_PREV(searchType, expectedStatus) \ { \ Okular::RegularAreaRect* result = tp->findText(0, searchString, searchType, Qt::CaseSensitive, NULL); \ QCOMPARE(!!result, expectedStatus); \ delete result; \ } //The test testNextAndPrevious checks that //a) if one starts a new search, then the first or last match is found, depending on the search direction // (2 cases: FromTop/FromBottom) //b) if the last search has found a match, // then clicking the "Next" button moves to the next occurrence an "Previous" to the previous one // (if there is any). Altogether there are four combinations of the last search and new search // direction: Next-Next, Previous-Previous, Next-Previous, Previous-Next; the first two combination // have two subcases (the new search may give a match or not, so altogether 6 cases to test). //This gives 8 cases altogether. By taking into account the cases where the last search has given no match, //we would have 4 more cases (Next (no match)-Next, Next (no match)-Previous, Previous (no match)-Previous, //Previous (no match)-Next), but those are more the business of Okular::Document::searchText rather than //Okular::TextPage (at least in the multi-page case). // We have four test situations: four documents and four corresponding search strings. // The first situation (document="ababa", search string="b") is a generic one where the //two matches are not side-by-side and neither the first character nor the last character of //the document match. The only special thing is that the search string has only length 1. // The second situation (document="abab", search string="ab") is notable for that the two occurrences //of the search string are side-by-side with no characters in between, so some off-by-one errors //would be detected by this test. As the first match starts at the beginning at the document the //last match ends at the end of the document, it also detects off-by-one errors for finding the first/last match. // The third situation (document="abababa", search string="aba") is notable for it shows whether //the next match is allowed to contain letters from the previous one: currently it is not //(as in the majority of browsers, viewers and editors), and therefore "abababa" is considered to //contain not three but two occurrences of "aba" (if one starts search from the beginning of the document). // The fourth situation (document="a ba b", search string="a b") demonstrates the case when one TinyTextEntity //contains multiple characters that are contained in different matches (namely, the middle "ba" is one TinyTextEntity); //in particular, since these matches are side-by-side, this test would detect some off-by-one //offset errors. void SearchTest::testNextAndPrevious() { #define TEST_NEXT_PREV_SITUATION_COUNT 4 QVector texts[TEST_NEXT_PREV_SITUATION_COUNT] = { QVector() << QStringLiteral("a") << QStringLiteral("b") << QStringLiteral("a" ) << QStringLiteral("b") << QStringLiteral("a"), QVector() << QStringLiteral("a") << QStringLiteral("b") << QStringLiteral("a" ) << QStringLiteral("b"), QVector() << QStringLiteral("a") << QStringLiteral("b") << QStringLiteral("a" ) << QStringLiteral("b") << QStringLiteral("a") << QStringLiteral("b") << QStringLiteral("a"), QVector() << QStringLiteral("a") << QStringLiteral(" ") << QStringLiteral("ba") << QStringLiteral(" ") << QStringLiteral("b") }; QString searchStrings[TEST_NEXT_PREV_SITUATION_COUNT] = { QStringLiteral("b"), QStringLiteral("ab"), QStringLiteral("aba"), QStringLiteral("a b") }; for (int i = 0; i < TEST_NEXT_PREV_SITUATION_COUNT; i++) { const QVector& text = texts[i]; const QString& searchString = searchStrings[i]; QVector rect; \ for (int i = 0; i < text.size(); i++) { rect << Okular::NormalizedRect(0.1*i, 0.0, 0.1*(i+1), 0.1); \ } CREATE_PAGE; //Test 3 of the 8 cases listed above: //FromTop, Next-Next (match) and Next-Next (no match) TEST_NEXT_PREV(Okular::FromTop, true); TEST_NEXT_PREV(Okular::NextResult, true); TEST_NEXT_PREV(Okular::NextResult, false); //Test 5 cases: FromBottom, Previous-Previous (match), Previous-Next, //Next-Previous, Previous-Previous (no match) TEST_NEXT_PREV(Okular::FromBottom, true); TEST_NEXT_PREV(Okular::PreviousResult, true); TEST_NEXT_PREV(Okular::NextResult, true); TEST_NEXT_PREV(Okular::PreviousResult, true); TEST_NEXT_PREV(Okular::PreviousResult, false); delete page; } } void SearchTest::test311232() { Okular::Document d(nullptr); SearchFinishedReceiver receiver; - QSignalSpy spy(&d, SIGNAL(searchFinished(int,Okular::Document::SearchStatus))); + QSignalSpy spy(&d, &Okular::Document::searchFinished); QObject::connect(&d, SIGNAL(searchFinished(int,Okular::Document::SearchStatus)), &receiver, SLOT(searchFinished(int,Okular::Document::SearchStatus))); const QString testFile = QStringLiteral(KDESRCDIR "data/file1.pdf"); QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( testFile ); d.openDocument(testFile, QUrl(), mime); const int searchId = 0; d.searchText(searchId, QStringLiteral(" i "), true, Qt::CaseSensitive, Okular::Document::NextMatch, false, QColor()); QTRY_COMPARE(spy.count(), 1); QCOMPARE(receiver.m_id, searchId); QCOMPARE(receiver.m_status, Okular::Document::MatchFound); d.continueSearch( searchId, Okular::Document::PreviousMatch ); QTRY_COMPARE(spy.count(), 2); QCOMPARE(receiver.m_id, searchId); QCOMPARE(receiver.m_status, Okular::Document::NoMatchFound); } void SearchTest::test323262() { QVector text; text << QStringLiteral("a\n"); QVector rect; rect << Okular::NormalizedRect(1, 2, 3, 4); CREATE_PAGE; Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("a"), Okular::FromBottom, Qt::CaseSensitive, nullptr); QVERIFY(result); delete result; delete page; } void SearchTest::test323263() { QVector text; text << QStringLiteral("a") << QStringLiteral("a") << QStringLiteral("b"); QVector rect; rect << Okular::NormalizedRect(0, 0, 1, 1) << Okular::NormalizedRect(1, 0, 2, 1) << Okular::NormalizedRect(2, 0, 3, 1); CREATE_PAGE; Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("ab"), Okular::FromTop, Qt::CaseSensitive, nullptr); QVERIFY(result); Okular::RegularAreaRect expected; expected.append(rect[1]); expected.append(rect[2]); expected.simplify(); QCOMPARE(*result, expected); delete result; delete page; } void SearchTest::testDottedI() { //Earlier versions of okular had the bug that the letter "İ" (capital dotter i) did not match itself //in case-insensitive mode (this was caused by an unnecessary call of toLower() and the fact that //QString::fromUtf8("İ").compare(QString::fromUtf8("İ").toLower(), Qt::CaseInsensitive) == FALSE, //at least in Qt 4.8). //In the future it would be nice to add support for matching "İ"<->"i" and "I"<->"ı" in case-insensitive //mode as well (QString::compare does not match them, at least in non-Turkish locales, since it follows //the Unicode case-folding rules http://www.unicode.org/Public/6.2.0/ucd/CaseFolding.txt). QVector text; - text << QString::fromUtf8("İ"); + text << QStringLiteral("İ"); QVector rect; rect << Okular::NormalizedRect(1, 2, 3, 4); CREATE_PAGE; - Okular::RegularAreaRect* result = tp->findText(0, QString::fromUtf8("İ"), Okular::FromTop, Qt::CaseInsensitive, nullptr); + Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("İ"), Okular::FromTop, Qt::CaseInsensitive, nullptr); QVERIFY(result); delete result; delete page; } void SearchTest::testHyphenAtEndOfLineWithoutYOverlap() { QVector text; text << QStringLiteral("super-") << QStringLiteral("cali-\n") << QStringLiteral("fragilistic") << QStringLiteral("-") << QStringLiteral("expiali") << QStringLiteral("-\n") << QStringLiteral("docious"); QVector rect; rect << Okular::NormalizedRect(0.4, 0.0, 0.9, 0.1) << Okular::NormalizedRect(0.0, 0.1, 0.6, 0.2) << Okular::NormalizedRect(0.0, 0.2, 0.8, 0.3) << Okular::NormalizedRect(0.8, 0.2, 0.9, 0.3) << Okular::NormalizedRect(0.0, 0.3, 0.8, 0.4) << Okular::NormalizedRect(0.8, 0.3, 0.9, 0.4) << Okular::NormalizedRect(0.0, 0.4, 0.7, 0.5); CREATE_PAGE; Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("supercalifragilisticexpialidocious"), Okular::FromTop, Qt::CaseSensitive, nullptr); QVERIFY(result); Okular::RegularAreaRect expected; for (int i = 0; i < text.size(); i++) { expected.append(rect[i]); } expected.simplify(); QCOMPARE(*result, expected); delete result; delete page; } #define CREATE_PAGE_AND_TEST_SEARCH(searchString, matchExpected) \ { \ CREATE_PAGE; \ \ Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral(searchString), \ Okular::FromTop, Qt::CaseSensitive, NULL); \ \ QCOMPARE(!!result, matchExpected); \ \ delete result; \ delete page; \ } void SearchTest::testHyphenWithYOverlap() { QVector text; text << QStringLiteral("a-") << QStringLiteral("b"); QVector rect(2); //different lines (50% y-coordinate overlap), first rectangle has larger height rect[0] = Okular::NormalizedRect(0.0, 0.0, 0.9, 0.35); rect[1] = Okular::NormalizedRect(0.0, 0.3, 0.2, 0.4); CREATE_PAGE_AND_TEST_SEARCH("ab", true); //different lines (50% y-coordinate overlap), second rectangle has larger height rect[0] = Okular::NormalizedRect(0.0, 0.0, 0.9, 0.1); rect[1] = Okular::NormalizedRect(0.0, 0.05, 0.2, 0.4); CREATE_PAGE_AND_TEST_SEARCH("ab", true); //same line (90% y-coordinate overlap), first rectangle has larger height rect[0] = Okular::NormalizedRect(0.0, 0.0, 0.4, 0.2); rect[1] = Okular::NormalizedRect(0.4, 0.11, 0.6, 0.21); CREATE_PAGE_AND_TEST_SEARCH("ab", false); CREATE_PAGE_AND_TEST_SEARCH("a-b", true); //same line (90% y-coordinate overlap), second rectangle has larger height rect[0] = Okular::NormalizedRect(0.0, 0.0, 0.4, 0.1); rect[1] = Okular::NormalizedRect(0.4, 0.01, 0.6, 0.2); CREATE_PAGE_AND_TEST_SEARCH("ab", false); CREATE_PAGE_AND_TEST_SEARCH("a-b", true); } void SearchTest::testHyphenAtEndOfPage() { //Tests for segmentation fault that would occur if //we tried look ahead (for determining whether the //next character is at the same line) at the end of the page. QVector text; text << QStringLiteral("a-"); QVector rect; rect << Okular::NormalizedRect(0, 0, 1, 1); CREATE_PAGE; { Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("a"), Okular::FromTop, Qt::CaseSensitive, nullptr); QVERIFY(result); delete result; } { Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("a"), Okular::FromBottom, Qt::CaseSensitive, nullptr); QVERIFY(result); delete result; } delete page; } void SearchTest::testOneColumn() { //Tests that the layout analysis algorithm does not create too many columns. //Bug 326207 was caused by the fact that if all the horizontal breaks in a line //had the same length and were smaller than vertical breaks between lines then //the horizontal breaks were treated as column separators. //(Note that "same length" means "same length after rounding rectangles to integer pixels". //The resolution used by the XY Cut algorithm with a square page is 1000 x 1000, //and the horizontal spaces in the example are 0.1, so they are indeed both exactly 100 pixels.) QVector text; text << QStringLiteral("Only") << QStringLiteral("one") << QStringLiteral("column") << QStringLiteral("here"); //characters and line breaks have length 0.05, word breaks 0.1 QVector rect; rect << Okular::NormalizedRect(0.0, 0.0, 0.2, 0.1) << Okular::NormalizedRect(0.3, 0.0, 0.5, 0.1) << Okular::NormalizedRect(0.6, 0.0, 0.9, 0.1) << Okular::NormalizedRect(0.0, 0.15, 0.2, 0.25); CREATE_PAGE; Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("Only one column"), Okular::FromTop, Qt::CaseSensitive, nullptr); QVERIFY(result); delete result; delete page; } void SearchTest::testTwoColumns() { //Tests that the layout analysis algorithm can detect two columns. QVector text; text << QStringLiteral("This") << QStringLiteral("text") << QStringLiteral("in") << QStringLiteral("two") << QStringLiteral("is") << QStringLiteral("set") << QStringLiteral("columns."); //characters, word breaks and line breaks have length 0.05 QVector rect; rect << Okular::NormalizedRect(0.0, 0.0, 0.20, 0.1) << Okular::NormalizedRect(0.25, 0.0, 0.45, 0.1) << Okular::NormalizedRect(0.6, 0.0, 0.7, 0.1) << Okular::NormalizedRect(0.75, 0.0, 0.9, 0.1) << Okular::NormalizedRect(0.0, 0.15, 0.1, 0.25) << Okular::NormalizedRect(0.15, 0.15, 0.3, 0.25) << Okular::NormalizedRect(0.6, 0.15, 1.0, 0.25); CREATE_PAGE; Okular::RegularAreaRect* result = tp->findText(0, QStringLiteral("This text in"), Okular::FromTop, Qt::CaseSensitive, nullptr); QVERIFY(!result); delete result; delete page; } QTEST_MAIN( SearchTest ) #include "searchtest.moc" diff --git a/autotests/visibilitytest.cpp b/autotests/visibilitytest.cpp index 2e678d264..e00638317 100644 --- a/autotests/visibilitytest.cpp +++ b/autotests/visibilitytest.cpp @@ -1,177 +1,177 @@ /*************************************************************************** * Copyright (C) 2018 by Intevation GmbH * * * * 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 #include #include "../settings_core.h" #include "core/document.h" #include #include #include "../generators/poppler/config-okular-poppler.h" class VisibilityTest: public QObject { Q_OBJECT private slots: void initTestCase(); void cleanupTestCase(); void testJavaScriptVisibility(); void testSaveLoad(); void testActionVisibility(); private: void verifyTargetStates( bool visible ); Okular::Document *m_document; QMap m_fields; }; void VisibilityTest::initTestCase() { Okular::SettingsCore::instance( QStringLiteral("visibilitytest") ); m_document = new Okular::Document( nullptr ); const QString testFile = QStringLiteral( KDESRCDIR "data/visibilitytest.pdf" ); QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( testFile ); QCOMPARE( m_document->openDocument( testFile, QUrl(), mime), Okular::Document::OpenSuccess ); // The test document has four buttons: // HideScriptButton -> Hides targets with JavaScript // ShowScriptButton -> Shows targets with JavaScript // HideActionButton -> Hides targets with HideAction // ShowActionButton -> Shows targets with HideAction // // The target fields are: // TargetButton TargetText TargetCheck TargetDropDown TargetRadio // // With two radio buttons named TargetRadio. const Okular::Page* page = m_document->page( 0 ); for ( Okular::FormField *ff: page->formFields() ) { m_fields.insert( ff->name(), ff ); } } void VisibilityTest::cleanupTestCase() { m_document->closeDocument(); delete m_document; } void VisibilityTest::verifyTargetStates( bool visible ) { QCOMPARE( m_fields[QStringLiteral( "TargetButton" )]->isVisible(), visible ); QCOMPARE( m_fields[QStringLiteral( "TargetText" )]->isVisible(), visible ); QCOMPARE( m_fields[QStringLiteral( "TargetCheck" )]->isVisible(), visible ); QCOMPARE( m_fields[QStringLiteral( "TargetDropDown" )]->isVisible(), visible ); // Radios do not properly inherit a name from the parent group so // this does not work yet (And would probably need some list handling). // QCOMPARE( m_fields[QStringLiteral( "TargetRadio" )].isVisible(), visible ); } void VisibilityTest::testJavaScriptVisibility() { #ifndef HAVE_POPPLER_0_64 return; #endif auto hideBtn = m_fields[QStringLiteral( "HideScriptButton" )]; auto showBtn = m_fields[QStringLiteral( "ShowScriptButton" )]; // We start with all fields visible verifyTargetStates( true ); m_document->processAction( hideBtn->activationAction() ); // Now all should be hidden verifyTargetStates( false ); // And show again m_document->processAction( showBtn->activationAction() ); verifyTargetStates( true ); } void VisibilityTest::testSaveLoad() { #ifndef HAVE_POPPLER_0_64 return; #endif auto hideBtn = m_fields[QStringLiteral( "HideScriptButton" )]; auto showBtn = m_fields[QStringLiteral( "ShowScriptButton" )]; // We start with all fields visible verifyTargetStates( true ); m_document->processAction( hideBtn->activationAction() ); // Now all should be hidden verifyTargetStates( false ); // Save the changed states - QTemporaryFile saveFile( QString( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); + QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); QVERIFY( saveFile.open() ); saveFile.close(); QVERIFY( m_document->saveChanges( saveFile.fileName() ) ); auto newDoc = new Okular::Document( nullptr ); QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( saveFile.fileName() ); QCOMPARE( newDoc->openDocument( saveFile.fileName(), QUrl(), mime), Okular::Document::OpenSuccess ); const Okular::Page* page = newDoc->page( 0 ); bool anyChecked = false; // Saveguard against accidental test passing here ;-) for ( Okular::FormField *ff: page->formFields() ) { if ( ff->name().startsWith( QStringLiteral( "Target" ) ) ) { QCOMPARE( ff->isVisible(), false ); anyChecked = true; } } QVERIFY(anyChecked); newDoc->closeDocument(); delete newDoc; // Restore the state of the member document m_document->processAction( showBtn->activationAction() ); } void VisibilityTest::testActionVisibility() { #ifndef HAVE_POPPLER_0_64 return; #endif auto hideBtn = m_fields[QStringLiteral( "HideActionButton" )]; auto showBtn = m_fields[QStringLiteral( "ShowActionButton" )]; verifyTargetStates( true ); m_document->processAction( hideBtn->activationAction() ); verifyTargetStates( false ); m_document->processAction( showBtn->activationAction() ); verifyTargetStates( true ); } QTEST_MAIN( VisibilityTest ) #include "visibilitytest.moc" diff --git a/core/document.cpp b/core/document.cpp index ddc00e5ee..f6a2bdce4 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -1,5671 +1,5671 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2017, 2018 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 #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 "script/event_p.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 #if HAVE_MALLOC_TRIM #include "malloc.h" #endif 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 %1", paperName); } else { return i18nc("paper type and orientation (eg: Portrait A4)", "Landscape %1", 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 } 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 false; QFile infoFile( m_xmlFileName ); return loadDocumentInfo( infoFile, loadWhat ); } bool DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat ) { if ( !infoFile.exists() || !infoFile.open( QIODevice::ReadOnly ) ) 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 false; } infoFile.close(); QDomElement root = doc.documentElement(); if ( root.tagName() != QLatin1String("documentInfo") ) return false; 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") && ( 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() ) { if ( m_pagesVector[ pageNumber ]->d->restoreLocalContents( pageElement ) ) loadedAnything = true; } } pageNode = pageNode.nextSibling(); } } // Restore 'general info' from the DOM 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(); } // consistency 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::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 ); } } 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 ); } } } 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 ); } } 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 QVariant fco = m_parent->metaData(QStringLiteral("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) { bool pageNeedsRefresh = false; foreach( FormField *form, p->formFields() ) { if ( form->id() == formId ) { Action *action = form->additionalAction( FormField::CalculateField ); if (action) { FormFieldText *fft = dynamic_cast< FormFieldText * >( form ); std::shared_ptr event; QString oldVal; if ( fft ) { // Prepare text calculate event event = Event::createFormCalculateEvent( fft, m_pagesVector[pageIdx] ); if ( !m_scripter ) m_scripter = new Scripter( this ); m_scripter->setEvent( event.get() ); // The value maybe changed in javascript so save it first. oldVal = fft->text(); } m_parent->processAction( action ); if ( event && fft ) { // Update text field from calculate m_scripter->setEvent( nullptr ); const QString newVal = event->value().toString(); if ( newVal != oldVal ) { fft->setText( newVal ); emit m_parent->refreshFormWidget( fft ); pageNeedsRefresh = true; } } } else { qWarning() << "Form that is part of calculate order doesn't have a calculate action"; } } } if ( pageNeedsRefresh ) { refreshPixmaps( p->number() ); } } } } } 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 // -> do this if there are not-yet-migrated annots or forms in docdata/ if ( m_docdataMigrationNeeded ) { - QDomElement pageList = doc.createElement( "pageList" ); + QDomElement pageList = doc.createElement( QStringLiteral("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 dependent' 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 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 ), true /*isPartialPixmap*/ ); 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 ) ); // If set elsewhere we already know we want it to be partial if ( !request->partialUpdatesWanted() ) { 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; QMap< DocumentObserver*, PagePrivate::PixmapObject >::ConstIterator it = page->d->m_pixmaps.constBegin(), itEnd = page->d->m_pixmaps.constEnd(); QVector< Okular::PixmapRequest * > pixmapsToRequest; for ( ; it != itEnd; ++it ) { const 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; pixmapsToRequest << p; } // Need to do this ↑↓ in two steps since requestPixmaps can end up calling cancelRenderingBecauseOf // which changes m_pixmaps and thus breaks the loop above for ( PixmapRequest *pr : qAsConst( pixmapsToRequest ) ) { QLinkedList< Okular::PixmapRequest * > requestedPixmaps; requestedPixmaps.push_back( pr ); m_parent->requestPixmaps( requestedPixmaps, Okular::Document::NoOption ); } foreach (DocumentObserver *observer, m_observers) { QLinkedList< Okular::PixmapRequest * > requestedPixmaps; 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 ); // 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; } } 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 highlight 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) ); } void DocumentPrivate::clearAndWaitForRequests() { m_pixmapRequestsMutex.lock(); QLinkedList< PixmapRequest * >::const_iterator sIt = m_pixmapRequestsStack.constBegin(); QLinkedList< PixmapRequest * >::const_iterator sEnd = m_pixmapRequestsStack.constEnd(); for ( ; sIt != sEnd; ++sIt ) delete *sIt; m_pixmapRequestsStack.clear(); m_pixmapRequestsMutex.unlock(); QEventLoop loop; bool startEventLoop = false; do { m_pixmapRequestsMutex.lock(); startEventLoop = !m_executingPixmapRequests.isEmpty(); if ( m_generator->hasFeature( Generator::SupportsCancelling ) ) { for ( PixmapRequest *executingRequest : qAsConst( m_executingPixmapRequests ) ) executingRequest->d->mShouldAbortRender = 1; if ( m_generator->d_ptr->mTextPageGenerationThread ) m_generator->d_ptr->mTextPageGenerationThread->abortExtraction(); } m_pixmapRequestsMutex.unlock(); if ( startEventLoop ) { m_closingLoop = &loop; loop.exec(); m_closingLoop = nullptr; } } while ( startEventLoop ); } 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" ) ); + result = KPluginLoader::findPlugins( QStringLiteral ( "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; int fd = -1; if (url.scheme() == QLatin1String("fd")) { bool ok; - fd = url.path().mid(1).toInt(&ok); + fd = url.path().midRef(1).toInt(&ok); if (!ok) { return OpenError; } } else if (url.fileName() == QLatin1String( "-" )) { fd = 0; } bool triedMimeFromFileContent = false; if ( fd < 0 ) { if ( !mime.isValid() ) return OpenError; d->m_url = url; d->m_docFileName = docFile; if ( !d->updateMetadataXmlNameAndDocSize() ) return OpenError; } else { QFile qstdin; const bool ret = qstdin.open( fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle ); if (!ret) { qWarning() << "failed to read" << url << filedata; return OpenError; } filedata = qstdin.readAll(); mime = db.mimeTypeForData( filedata ); if ( !mime.isValid() || mime.isDefault() ) return OpenError; d->m_docSize = filedata.size(); triedMimeFromFileContent = true; } const bool fromFileDescriptor = fd >= 0; // 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::MatchContent); 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, fromFileDescriptor, 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, fromFileDescriptor, 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::MatchContent); triedMimeFromFileContent = true; if ( newmime != mime ) { mime = newmime; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); while ( offer.isValid() ) { openResult = d->openDocumentInternal( offer, fromFileDescriptor, 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*)) ); foreach ( Page * p, d->m_pagesVector ) p->d->m_doc = d; d->m_metadataLoadingCompleted = false; d->m_docdataMigrationNeeded = false; // 2. load Additional Data (bookmarks, local annotations and metadata) about the document if ( d->m_archiveData ) { // QTemporaryFile is weird and will return false in exists if fileName wasn't called before d->m_archiveData->metadataFile.fileName(); d->loadDocumentInfo( d->m_archiveData->metadataFile, LoadPageInfo ); d->loadDocumentInfo( LoadGeneralInfo ); } else { if ( d->loadDocumentInfo( LoadPageInfo ) ) d->m_docdataMigrationNeeded = true; d->loadDocumentInfo( LoadGeneralInfo ); } d->m_metadataLoadingCompleted = true; d->m_bookmarkManager->setUrl( d->m_url ); // 3. setup observers internal lists and data 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 = fromFileDescriptor ? QUrl() : d->m_url; 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; emit aboutToClose(); delete d->m_pageController; d->m_pageController = nullptr; delete d->m_scripter; d->m_scripter = nullptr; // remove requests left in queue d->clearAndWaitForRequests(); 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 | 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; #if HAVE_MALLOC_TRIM // trim unused memory, glibc should do this but it seems it does not // this can greatly decrease the [perceived] memory consumption of okular // see: https://sourceware.org/bugzilla/show_bug.cgi?id=14827 malloc_trim(0); #endif } 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 | DocumentObserver::UrlChanged ); pObserver->notifyViewportChanged( false /*disables smoothMove*/ ); } } void Document::removeObserver( DocumentObserver * pObserver ) { // remove observer from the set. 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; } for ( PixmapRequest *executingRequest : qAsConst( d->m_executingPixmapRequests ) ) { if ( executingRequest->observer() == pObserver ) { d->cancelRenderingBecauseOf( executingRequest, nullptr ); } } // remove observer entry from the set 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 >= 0 && n < d->m_pagesVector.count() ) ? d->m_pagesVector.at(n) : nullptr; } 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_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(); } static bool shouldCancelRenderingBecauseOf( const PixmapRequest & executingRequest, const PixmapRequest & otherRequest ) { // New request has higher priority -> cancel if ( executingRequest.priority() > otherRequest.priority() ) return true; // New request has lower priority -> don't cancel if ( executingRequest.priority() < otherRequest.priority() ) return false; // New request has same priority and is from a different observer -> don't cancel // AFAIK this never happens since all observers have different priorities if ( executingRequest.observer() != otherRequest.observer() ) return false; // Same priority and observer, different page number -> don't cancel // may still end up cancelled later in the parent caller if none of the requests // is of the executingRequest page and RemoveAllPrevious is specified if ( executingRequest.pageNumber() != otherRequest.pageNumber() ) return false; // Same priority, observer, page, different size -> cancel if ( executingRequest.width() != otherRequest.width() ) return true; // Same priority, observer, page, different size -> cancel if ( executingRequest.height() != otherRequest.height() ) return true; // Same priority, observer, page, different tiling -> cancel if ( executingRequest.isTile() != otherRequest.isTile() ) return true; // Same priority, observer, page, different tiling -> cancel if ( executingRequest.isTile() ) { const NormalizedRect bothRequestsRect = executingRequest.normalizedRect() | otherRequest.normalizedRect(); if ( !( bothRequestsRect == executingRequest.normalizedRect() ) ) return true; } return false; } bool DocumentPrivate::cancelRenderingBecauseOf( PixmapRequest *executingRequest, PixmapRequest *newRequest ) { // No point in aborting the rendering already finished, let it go through if ( !executingRequest->d->mResultImage.isNull() ) return false; if ( newRequest && newRequest->asynchronous() && executingRequest->partialUpdatesWanted() ) { newRequest->setPartialUpdatesWanted( true ); } TilesManager *tm = executingRequest->d->tilesManager(); if ( tm ) { tm->setPixmap( nullptr, executingRequest->normalizedRect(), true /*isPartialPixmap*/ ); tm->setRequest( NormalizedRect(), 0, 0 ); } PagePrivate::PixmapObject object = executingRequest->page()->d->m_pixmaps.take( executingRequest->observer() ); delete object.m_pixmap; if ( executingRequest->d->mShouldAbortRender != 0) return false; executingRequest->d->mShouldAbortRender = 1; if ( m_generator->d_ptr->mTextPageGenerationThread && m_generator->d_ptr->mTextPageGenerationThread->page() == executingRequest->page() ) { m_generator->d_ptr->mTextPageGenerationThread->abortExtraction(); } return true; } 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; } QSet< DocumentObserver * > observersPixmapCleared; // 1. [CLEAN STACK] remove previous requests of requesterID DocumentObserver *requesterObserver = requests.first()->observer(); QSet< int > requestedPages; { QLinkedList< PixmapRequest * >::const_iterator rIt = requests.constBegin(), rEnd = requests.constEnd(); for ( ; rIt != rEnd; ++rIt ) { Q_ASSERT( (*rIt)->observer() == requesterObserver ); 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; } // 1.B [PREPROCESS REQUESTS] tweak some values of the requests for ( PixmapRequest *request : requests ) { // set the 'page field' (see PixmapRequest) and check if it is valid 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; } // 1.C [CANCEL REQUESTS] cancel those requests that are running and should be cancelled because of the new requests coming in if ( d->m_generator->hasFeature( Generator::SupportsCancelling ) ) { for ( PixmapRequest *executingRequest : qAsConst( d->m_executingPixmapRequests ) ) { bool newRequestsContainExecutingRequestPage = false; bool requestCancelled = false; for ( PixmapRequest *newRequest : requests ) { if ( newRequest->pageNumber() == executingRequest->pageNumber() && requesterObserver == executingRequest->observer()) { newRequestsContainExecutingRequestPage = true; } if ( shouldCancelRenderingBecauseOf( *executingRequest, *newRequest ) ) { requestCancelled = d->cancelRenderingBecauseOf( executingRequest, newRequest ); } } // If we were told to remove all the previous requests and the executing request page is not part of the new requests, cancel it if ( !requestCancelled && removeAllPrevious && requesterObserver == executingRequest->observer() && !newRequestsContainExecutingRequestPage ) { requestCancelled = d->cancelRenderingBecauseOf( executingRequest, nullptr ); } if ( requestCancelled ) { observersPixmapCleared << executingRequest->observer(); } } } // 2. [ADD TO STACK] add requests to stack for ( PixmapRequest *request : requests ) { // 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(); for ( DocumentObserver *o : qAsConst( observersPixmapCleared ) ) o->notifyContentsCleared( Okular::DocumentObserver::Pixmap ); } 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 ) { foreachObserverD( notifyPageChanged( page, DocumentObserver::Annotations ) ); } void DocumentPrivate::notifyFormChanges( int /*page*/ ) { recalculateForms(); } 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 - process 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 search 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 ); } 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 ); } 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 ); } 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; // Don't execute next actions if the action itself caused the closing of the document bool executeNextActions = true; auto connectionId = connect( this, &Document::aboutToClose, [&executeNextActions] { executeNextActions = false; } ); 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 asynchronously 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() << "'."; break; } else { const DocumentViewport nextViewport = d->nextDocumentViewport(); // skip local links that point to nowhere (broken ones) if ( !nextViewport.isValid() ) break; 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 ); break; } // 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.") ); break; } } 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.") ); break; } } 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")) + if ((url.scheme() == QLatin1String("http")) && url.host().isEmpty() && url.fileName().endsWith(QLatin1String("pdf"))) { d->openRelativeFile(url.fileName()); break; } // 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; } disconnect( connectionId ); if ( executeNextActions ) { for ( const Action *a : action->nextActions() ) { processAction( a ); } } } 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; // We know it's a BackendConfigDialog, but check anyway BackendConfigDialog *bcd = dynamic_cast( dialog ); if ( !bcd ) return; // ensure that we have all the generators with settings loaded QVector offers = DocumentPrivate::configurableGenerators(); d->loadServiceList( offers ); // We want the generators to be sorted by name so let's fill in a QMap // this sorts by internal id which is not awesome, but at least the sorting // is stable between runs that before it wasn't QMap sortedGenerators; QHash< QString, GeneratorInfo >::iterator it = d->m_loadedGenerators.begin(); QHash< QString, GeneratorInfo >::iterator itEnd = d->m_loadedGenerators.end(); for ( ; it != itEnd; ++it ) { sortedGenerators.insert(it.key(), it.value()); } bool pagesAdded = false; QMap< QString, GeneratorInfo >::iterator sit = sortedGenerators.begin(); QMap< QString, GeneratorInfo >::iterator sitEnd = sortedGenerators.end(); for ( ; sit != sitEnd; ++sit ) { Okular::ConfigInterface * iface = d->generatorConfig( sit.value() ); if ( iface ) { iface->addPages( dialog ); pagesAdded = true; if ( sit.value().generator == d->m_generator ) { const int rowCount = bcd->thePageWidget()->model()->rowCount(); KPageView *view = bcd->thePageWidget(); view->setCurrentPage( view->model()->index( rowCount - 1, 0 ) ); } } } 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 std::sort(result.begin(), result.end()); d->m_supportedMimeTypes = result; } return result; } bool Document::canSwapBackingFile() const { if ( !d->m_generator ) return false; return d->m_generator->hasFeature( Generator::SwapBackingFile ); } bool Document::swapBackingFile( const QString &newFileName, const QUrl &url ) { if ( !d->m_generator ) return false; if ( !d->m_generator->hasFeature( Generator::SwapBackingFile ) ) return false; // Save metadata about the file we're about to close d->saveDocumentInfo(); d->clearAndWaitForRequests(); qCDebug(OkularCoreDebug) << "Swapping backing file to" << newFileName; QVector< Page * > newPagesVector; Generator::SwapBackingFileResult result = d->m_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 ); d->m_documentInfo = DocumentInfo(); d->m_documentInfoAskedKeys.clear(); 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; } ArchiveData *DocumentPrivate::unpackDocumentArchive( const QString &archivePath ) { QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( archivePath, QMimeDatabase::MatchExtension ); if ( !mime.inherits( QStringLiteral("application/vnd.kde.okular-archive") ) ) return nullptr; KZip okularArchive( archivePath ); if ( !okularArchive.open( QIODevice::ReadOnly ) ) return nullptr; const KArchiveDirectory * mainDir = okularArchive.directory(); // Check the archive doesn't have folders, we don't create them when saving the archive // and folders mean paths and paths mean path traversal issues for ( const QString &entry : mainDir->entries() ) { if ( mainDir->entry( entry )->isDirectory() ) { qWarning() << "Warning: Found a directory inside" << archivePath << " - Okular does not create files like that so it is most probably forged."; return nullptr; } } const KArchiveEntry * mainEntry = mainDir->entry( QStringLiteral("content.xml") ); if ( !mainEntry || !mainEntry->isFile() ) return nullptr; std::unique_ptr< QIODevice > mainEntryDevice( static_cast< const KZipFileEntry * >( mainEntry )->createDevice() ); QDomDocument doc; if ( !doc.setContent( mainEntryDevice.get() ) ) return nullptr; mainEntryDevice.reset(); QDomElement root = doc.documentElement(); 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 nullptr; const KArchiveEntry * docEntry = mainDir->entry( documentFileName ); if ( !docEntry || !docEntry->isFile() ) 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 nullptr; archiveData->originalFileName = documentFileName; { 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::MatchExtension ); const OpenResult ret = openDocument( tempFileName, url, docMime, password ); 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_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; bool formsSavedNatively = false; if ( d->canAddAnnotationsNatively() || canSaveChanges( SaveFormsCapability ) ) { if ( !modifiedFile.open() ) return false; const QString modifiedFileName = modifiedFile.fileName(); modifiedFile.close(); // We're only interested in the file name QString errorText; if ( saveChanges( modifiedFileName, &errorText ) ) { docPath = modifiedFileName; // Save this instead of the original file 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; 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; } QByteArray Document::requestSignedRevisionData( const Okular::SignatureInfo &info ) { QFile f( d->m_docFileName ); if ( !f.open( QIODevice::ReadOnly ) ) { KMessageBox::error( nullptr, i18n("Could not open '%1'. File does not exist", d->m_docFileName ) ); return {}; } const QList byteRange = info.signedRangeBounds(); f.seek( byteRange.first() ); QByteArray data; QDataStream stream( &data, QIODevice::WriteOnly ); stream << f.read( byteRange.last() - byteRange.first() ); f.close(); return data; } 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 if ( !req->shouldAbortRender() ) { // [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/documentcommands.cpp b/core/documentcommands.cpp index 72aec67f5..e3ec520aa 100644 --- a/core/documentcommands.cpp +++ b/core/documentcommands.cpp @@ -1,753 +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() { 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 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 ) + const QDomNode &oldProperties, + const 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 b17abdc93..cb62417fb 100644 --- a/core/documentcommands_p.h +++ b/core/documentcommands_p.h @@ -1,316 +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 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 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 OkularUndoCommand { public: ModifyAnnotationPropertiesCommand( Okular::DocumentPrivate* docPriv, Okular::Annotation* annotation, int pageNumber, - QDomNode oldProperties, - QDomNode newProperties ); + const QDomNode &oldProperties, + const 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 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 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 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 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 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/fileprinter.cpp b/core/fileprinter.cpp index 803d75dff..78b3c4ca5 100644 --- a/core/fileprinter.cpp +++ b/core/fileprinter.cpp @@ -1,685 +1,685 @@ /*************************************************************************** * Copyright (C) 2007,2010 by John Layt * * * * FilePrinterPreview based on KPrintPreview (originally LGPL) * * Copyright (c) 2007 Alex Merry * * * * 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 "fileprinter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "debug_p.h" using namespace Okular; -int FilePrinter::printFile( QPrinter &printer, const QString file, +int FilePrinter::printFile(QPrinter &printer, const QString file, QPrinter::Orientation documentOrientation, FileDeletePolicy fileDeletePolicy, PageSelectPolicy pageSelectPolicy, const QString &pageRange ) { return printFile( printer, file, documentOrientation, fileDeletePolicy, pageSelectPolicy, pageRange, ScaleMode::FitToPrintArea ); } -int FilePrinter::printFile( QPrinter &printer, const QString file, +int FilePrinter::printFile(QPrinter &printer, const QString file, QPrinter::Orientation documentOrientation, FileDeletePolicy fileDeletePolicy, PageSelectPolicy pageSelectPolicy, const QString &pageRange, ScaleMode scaleMode ) { FilePrinter fp; return fp.doPrintFiles( printer, QStringList( file ), fileDeletePolicy, pageSelectPolicy, pageRange, documentOrientation, scaleMode ); } -int FilePrinter::doPrintFiles( QPrinter &printer, QStringList fileList, FileDeletePolicy fileDeletePolicy, +int FilePrinter::doPrintFiles(QPrinter &printer, const QStringList fileList, FileDeletePolicy fileDeletePolicy, PageSelectPolicy pageSelectPolicy, const QString &pageRange, QPrinter::Orientation documentOrientation ) { return doPrintFiles( printer, fileList, fileDeletePolicy, pageSelectPolicy, pageRange, documentOrientation, ScaleMode::FitToPrintArea ); } int FilePrinter::doPrintFiles( QPrinter &printer, QStringList fileList, FileDeletePolicy fileDeletePolicy, PageSelectPolicy pageSelectPolicy, const QString &pageRange, QPrinter::Orientation documentOrientation, ScaleMode scaleMode ) { if ( fileList.size() < 1 ) { return -8; } for (QStringList::ConstIterator it = fileList.constBegin(); it != fileList.constEnd(); ++it) { if (!QFile::exists(*it)) { return -7; } } if ( printer.printerState() == QPrinter::Aborted || printer.printerState() == QPrinter::Error ) { return -6; } QString exe; QStringList argList; int ret; // Print to File if a filename set, assumes there must be only 1 file if ( !printer.outputFileName().isEmpty() ) { if ( QFile::exists( printer.outputFileName() ) ) { QFile::remove( printer.outputFileName() ); } QFileInfo inputFileInfo = QFileInfo( fileList[0] ); QFileInfo outputFileInfo = QFileInfo( printer.outputFileName() ); bool doDeleteFile = (fileDeletePolicy == FilePrinter::SystemDeletesFiles); if ( inputFileInfo.suffix() == outputFileInfo.suffix() ) { if ( doDeleteFile ) { bool res = QFile::rename( fileList[0], printer.outputFileName() ); if ( res ) { doDeleteFile = false; ret = 0; } else { ret = -5; } } else { bool res = QFile::copy( fileList[0], printer.outputFileName() ); if ( res ) { ret = 0; } else { ret = -5; } } } else if ( inputFileInfo.suffix() == QLatin1String("ps") && printer.outputFormat() == QPrinter::PdfFormat && ps2pdfAvailable() ) { exe = QStringLiteral("ps2pdf"); argList << fileList[0] << printer.outputFileName(); qCDebug(OkularCoreDebug) << "Executing" << exe << "with arguments" << argList; ret = KProcess::execute( exe, argList ); - } else if ( inputFileInfo.suffix() == "pdf" && printer.outputFormat() == QPrinter::NativeFormat && pdf2psAvailable() ) { - exe = "pdf2ps"; + } else if ( inputFileInfo.suffix() == QLatin1String("pdf") && printer.outputFormat() == QPrinter::NativeFormat && pdf2psAvailable() ) { + exe = QStringLiteral("pdf2ps"); argList << fileList[0] << printer.outputFileName(); qCDebug(OkularCoreDebug) << "Executing" << exe << "with arguments" << argList; ret = KProcess::execute( exe, argList ); } else { ret = -5; } if ( doDeleteFile ) { QFile::remove( fileList[0] ); } } else { // Print to a printer via lpr command //Decide what executable to use to print with, need the CUPS version of lpr if available //Some distros name the CUPS version of lpr as lpr-cups or lpr.cups so try those first //before default to lpr, or failing that to lp if ( !QStandardPaths::findExecutable(QStringLiteral("lpr-cups")).isEmpty() ) { exe = QStringLiteral("lpr-cups"); } else if ( !QStandardPaths::findExecutable(QStringLiteral("lpr.cups")).isEmpty() ) { exe = QStringLiteral("lpr.cups"); } else if ( !QStandardPaths::findExecutable(QStringLiteral("lpr")).isEmpty() ) { exe = QStringLiteral("lpr"); } else if ( !QStandardPaths::findExecutable(QStringLiteral("lp")).isEmpty() ) { exe = QStringLiteral("lp"); } else { return -9; } bool useCupsOptions = cupsAvailable(); argList = printArguments( printer, fileDeletePolicy, pageSelectPolicy, useCupsOptions, pageRange, exe, documentOrientation, scaleMode ) << fileList; qCDebug(OkularCoreDebug) << "Executing" << exe << "with arguments" << argList; ret = KProcess::execute( exe, argList ); } return ret; } QList FilePrinter::pageList( QPrinter &printer, int lastPage, const QList &selectedPageList ) { return pageList( printer, lastPage, 0, selectedPageList ); } QList FilePrinter::pageList( QPrinter &printer, int lastPage, int currentPage, const QList &selectedPageList ) { if ( printer.printRange() == QPrinter::Selection) { return selectedPageList; } int startPage, endPage; QList list; if ( printer.printRange() == QPrinter::PageRange ) { startPage = printer.fromPage(); endPage = printer.toPage(); } else if ( printer.printRange() == QPrinter::CurrentPage) { startPage = currentPage; endPage = currentPage; } else { //AllPages startPage = 1; endPage = lastPage; } for (int i = startPage; i <= endPage; i++ ) { list << i; } return list; } QString FilePrinter::pageRange( QPrinter &printer, int lastPage, const QList &selectedPageList ) { if ( printer.printRange() == QPrinter::Selection) { return pageListToPageRange( selectedPageList ); } if ( printer.printRange() == QPrinter::PageRange ) { return QStringLiteral("%1-%2").arg(printer.fromPage()).arg(printer.toPage()); } return QStringLiteral("1-%2").arg( lastPage ); } QString FilePrinter::pageListToPageRange( const QList &pageList ) { QString pageRange; int count = pageList.count(); int i = 0; int seqStart = i; int seqEnd; while ( i != count ) { if ( i + 1 == count || pageList[i] + 1 != pageList[i+1] ) { seqEnd = i; if ( !pageRange.isEmpty() ) { pageRange.append(QLatin1Char(',')); } if ( seqStart == seqEnd ) { pageRange.append(pageList[i]); } else { pageRange.append(QStringLiteral("%1-%2").arg(seqStart).arg(seqEnd)); } seqStart = i + 1; } i++; } return pageRange; } bool FilePrinter::ps2pdfAvailable() { return ( !QStandardPaths::findExecutable(QStringLiteral("ps2pdf")).isEmpty() ); } bool FilePrinter::pdf2psAvailable() { return ( !QStandardPaths::findExecutable(QStringLiteral("pdf2ps")).isEmpty() ); } bool FilePrinter::cupsAvailable() { #if defined(Q_OS_UNIX) && !defined(Q_OS_OSX) // Ideally we would have access to the private Qt method // QCUPSSupport::cupsAvailable() to do this as it is very complex routine. // However, if CUPS is available then QPrinter::numCopies() will always return 1 // whereas if CUPS is not available it will return the real number of copies. // This behaviour is guaranteed never to change, so we can use it as a reliable substitute. QPrinter testPrinter; testPrinter.setNumCopies( 2 ); return ( testPrinter.numCopies() == 1 ); #else return false; #endif } bool FilePrinter::detectCupsService() { QTcpSocket qsock; qsock.connectToHost(QStringLiteral("localhost"), 631); bool rtn = qsock.waitForConnected() && qsock.isValid(); qsock.abort(); return rtn; } bool FilePrinter::detectCupsConfig() { if ( QFile::exists(QStringLiteral("/etc/cups/cupsd.conf")) ) return true; if ( QFile::exists(QStringLiteral("/usr/etc/cups/cupsd.conf")) ) return true; if ( QFile::exists(QStringLiteral("/usr/local/etc/cups/cupsd.conf")) ) return true; if ( QFile::exists(QStringLiteral("/opt/etc/cups/cupsd.conf")) ) return true; if ( QFile::exists(QStringLiteral("/opt/local/etc/cups/cupsd.conf")) ) return true; return false; } QSize FilePrinter::psPaperSize( QPrinter &printer ) { QSize size = printer.pageLayout().pageSize().sizePoints(); if ( printer.pageSize() == QPrinter::Custom ) { return QSize( (int) printer.widthMM() * ( 25.4 / 72 ), (int) printer.heightMM() * ( 25.4 / 72 ) ); } if ( printer.orientation() == QPrinter::Landscape ) { size.transpose(); } return size; } Generator::PrintError FilePrinter::printError( int c ) { Generator::PrintError pe; if ( c >= 0 ) { pe = Generator::NoPrintError; } else { switch ( c ) { case -1: pe = Generator::PrintingProcessCrashPrintError; break; case -2: pe = Generator::PrintingProcessStartPrintError; break; case -5: pe = Generator::PrintToFilePrintError; break; case -6: pe = Generator::InvalidPrinterStatePrintError; break; case -7: pe = Generator::UnableToFindFilePrintError; break; case -8: pe = Generator::NoFileToPrintError; break; case -9: pe = Generator::NoBinaryToPrintError; break; default: pe = Generator::UnknownPrintError; } } return pe; } QStringList FilePrinter::printArguments( QPrinter &printer, FileDeletePolicy fileDeletePolicy, PageSelectPolicy pageSelectPolicy, bool useCupsOptions, const QString &pageRange, const QString &version, QPrinter::Orientation documentOrientation ) { return printArguments( printer, fileDeletePolicy, pageSelectPolicy, useCupsOptions, pageRange, version, documentOrientation, ScaleMode::FitToPrintArea); } QStringList FilePrinter::printArguments( QPrinter &printer, FileDeletePolicy fileDeletePolicy, PageSelectPolicy pageSelectPolicy, bool useCupsOptions, const QString &pageRange, const QString &version, QPrinter::Orientation documentOrientation, ScaleMode scaleMode ) { QStringList argList; if ( ! destination( printer, version ).isEmpty() ) { argList << destination( printer, version ); } if ( ! copies( printer, version ).isEmpty() ) { argList << copies( printer, version ); } if ( ! jobname( printer, version ).isEmpty() ) { argList << jobname( printer, version ); } if ( ! pages( printer, pageSelectPolicy, pageRange, useCupsOptions, version ).isEmpty() ) { argList << pages( printer, pageSelectPolicy, pageRange, useCupsOptions, version ); } if ( useCupsOptions && ! cupsOptions( printer, documentOrientation, scaleMode ).isEmpty() ) { argList << cupsOptions( printer, documentOrientation, scaleMode); } if ( ! deleteFile( printer, fileDeletePolicy, version ).isEmpty() ) { argList << deleteFile( printer, fileDeletePolicy, version ); } if ( version == QLatin1String("lp") ) { argList << QStringLiteral("--"); } return argList; } QStringList FilePrinter::destination( QPrinter &printer, const QString &version ) { if ( version == QLatin1String("lp") ) { return QStringList(QStringLiteral("-d")) << printer.printerName(); } if ( version.startsWith( QLatin1String("lpr") ) ) { return QStringList(QStringLiteral("-P")) << printer.printerName(); } return QStringList(); } QStringList FilePrinter::copies( QPrinter &printer, const QString &version ) { int cp = printer.actualNumCopies(); if ( version == QLatin1String("lp") ) { return QStringList(QStringLiteral("-n")) << QStringLiteral("%1").arg( cp ); } if ( version.startsWith( QLatin1String("lpr") ) ) { return QStringList() << QStringLiteral("-#%1").arg( cp ); } return QStringList(); } QStringList FilePrinter::jobname( QPrinter &printer, const QString &version ) { if ( ! printer.docName().isEmpty() ) { if ( version == QLatin1String("lp") ) { return QStringList(QStringLiteral("-t")) << printer.docName(); } if ( version.startsWith( QLatin1String("lpr") ) ) { const QString shortenedDocName = QString::fromUtf8(printer.docName().toUtf8().left(255)); return QStringList(QStringLiteral("-J")) << shortenedDocName; } } return QStringList(); } QStringList FilePrinter::deleteFile( QPrinter &, FileDeletePolicy fileDeletePolicy, const QString &version ) { if ( fileDeletePolicy == FilePrinter::SystemDeletesFiles && version.startsWith( QLatin1String("lpr") ) ) { return QStringList(QStringLiteral("-r")); } return QStringList(); } QStringList FilePrinter::pages( QPrinter &printer, PageSelectPolicy pageSelectPolicy, const QString &pageRange, bool useCupsOptions, const QString &version ) { if ( pageSelectPolicy == FilePrinter::SystemSelectsPages ) { if ( printer.printRange() == QPrinter::Selection && ! pageRange.isEmpty() ) { if ( version == QLatin1String("lp") ) { return QStringList(QStringLiteral("-P")) << pageRange ; } if ( version.startsWith( QLatin1String("lpr") ) && useCupsOptions ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("page-ranges=%1").arg( pageRange ); } } if ( printer.printRange() == QPrinter::PageRange ) { if ( version == QLatin1String("lp") ) { return QStringList(QStringLiteral("-P")) << QStringLiteral("%1-%2").arg( printer.fromPage() ) .arg( printer.toPage() ); } if ( version.startsWith( QLatin1String("lpr") ) && useCupsOptions ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("page-ranges=%1-%2").arg( printer.fromPage() ) .arg( printer.toPage() ); } } } return QStringList(); // AllPages } QStringList FilePrinter::cupsOptions( QPrinter &printer, QPrinter::Orientation documentOrientation ) { return cupsOptions( printer, documentOrientation, ScaleMode::FitToPrintArea ); } QStringList FilePrinter::cupsOptions( QPrinter &printer, QPrinter::Orientation documentOrientation, ScaleMode scaleMode ) { QStringList optionList; if ( ! optionMedia( printer ).isEmpty() ) { optionList << optionMedia( printer ); } if ( ! optionOrientation( printer, documentOrientation ).isEmpty() ) { optionList << optionOrientation( printer, documentOrientation ); } if ( ! optionDoubleSidedPrinting( printer ).isEmpty() ) { optionList << optionDoubleSidedPrinting( printer ); } if ( ! optionPageOrder( printer ).isEmpty() ) { optionList << optionPageOrder( printer ); } if ( ! optionCollateCopies( printer ).isEmpty() ) { optionList << optionCollateCopies( printer ); } if ( ! optionPageMargins( printer, scaleMode ).isEmpty() ) { optionList << optionPageMargins( printer, scaleMode ); } optionList << optionCupsProperties( printer ); return optionList; } QStringList FilePrinter::optionMedia( QPrinter &printer ) { if ( ! mediaPageSize( printer ).isEmpty() && ! mediaPaperSource( printer ).isEmpty() ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("media=%1,%2").arg( mediaPageSize( printer ), mediaPaperSource( printer ) ); } if ( ! mediaPageSize( printer ).isEmpty() ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("media=%1").arg( mediaPageSize( printer ) ); } if ( ! mediaPaperSource( printer ).isEmpty() ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("media=%1").arg( mediaPaperSource( printer ) ); } return QStringList(); } QString FilePrinter::mediaPageSize( QPrinter &printer ) { switch ( printer.pageSize() ) { case QPrinter::A0: return QStringLiteral("A0"); case QPrinter::A1: return QStringLiteral("A1"); case QPrinter::A2: return QStringLiteral("A2"); case QPrinter::A3: return QStringLiteral("A3"); case QPrinter::A4: return QStringLiteral("A4"); case QPrinter::A5: return QStringLiteral("A5"); case QPrinter::A6: return QStringLiteral("A6"); case QPrinter::A7: return QStringLiteral("A7"); case QPrinter::A8: return QStringLiteral("A8"); case QPrinter::A9: return QStringLiteral("A9"); case QPrinter::B0: return QStringLiteral("B0"); case QPrinter::B1: return QStringLiteral("B1"); case QPrinter::B10: return QStringLiteral("B10"); case QPrinter::B2: return QStringLiteral("B2"); case QPrinter::B3: return QStringLiteral("B3"); case QPrinter::B4: return QStringLiteral("B4"); case QPrinter::B5: return QStringLiteral("B5"); case QPrinter::B6: return QStringLiteral("B6"); case QPrinter::B7: return QStringLiteral("B7"); case QPrinter::B8: return QStringLiteral("B8"); case QPrinter::B9: return QStringLiteral("B9"); case QPrinter::C5E: return QStringLiteral("C5"); //Correct Translation? case QPrinter::Comm10E: return QStringLiteral("Comm10"); //Correct Translation? case QPrinter::DLE: return QStringLiteral("DL"); //Correct Translation? case QPrinter::Executive: return QStringLiteral("Executive"); case QPrinter::Folio: return QStringLiteral("Folio"); case QPrinter::Ledger: return QStringLiteral("Ledger"); case QPrinter::Legal: return QStringLiteral("Legal"); case QPrinter::Letter: return QStringLiteral("Letter"); case QPrinter::Tabloid: return QStringLiteral("Tabloid"); case QPrinter::Custom: return QStringLiteral("Custom.%1x%2mm") .arg( printer.widthMM() ) .arg( printer.heightMM() ); default: return QString(); } } // What about Upper and MultiPurpose? And others in PPD??? QString FilePrinter::mediaPaperSource( QPrinter &printer ) { switch ( printer.paperSource() ) { case QPrinter::Auto: return QString(); case QPrinter::Cassette: return QStringLiteral("Cassette"); case QPrinter::Envelope: return QStringLiteral("Envelope"); case QPrinter::EnvelopeManual: return QStringLiteral("EnvelopeManual"); case QPrinter::FormSource: return QStringLiteral("FormSource"); case QPrinter::LargeCapacity: return QStringLiteral("LargeCapacity"); case QPrinter::LargeFormat: return QStringLiteral("LargeFormat"); case QPrinter::Lower: return QStringLiteral("Lower"); case QPrinter::MaxPageSource: return QStringLiteral("MaxPageSource"); case QPrinter::Middle: return QStringLiteral("Middle"); case QPrinter::Manual: return QStringLiteral("Manual"); case QPrinter::OnlyOne: return QStringLiteral("OnlyOne"); case QPrinter::Tractor: return QStringLiteral("Tractor"); case QPrinter::SmallFormat: return QStringLiteral("SmallFormat"); default: return QString(); } } QStringList FilePrinter::optionOrientation( QPrinter &printer, QPrinter::Orientation documentOrientation ) { // portrait and landscape options rotate the document according to the document orientation // If we want to print a landscape document as one would expect it, we have to pass the // portrait option so that the document is not rotated additionally if ( printer.orientation() == documentOrientation ) { // the user wants the document printed as is return QStringList(QStringLiteral("-o")) << QStringLiteral("portrait"); } else { // the user expects the document being rotated by 90 degrees return QStringList(QStringLiteral("-o")) << QStringLiteral("landscape"); } } QStringList FilePrinter::optionDoubleSidedPrinting( QPrinter &printer ) { switch ( printer.duplex() ) { case QPrinter::DuplexNone: return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=one-sided"); case QPrinter::DuplexAuto: if ( printer.orientation() == QPrinter::Landscape ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=two-sided-short-edge"); } else { return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=two-sided-long-edge"); } case QPrinter::DuplexLongSide: return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=two-sided-long-edge"); case QPrinter::DuplexShortSide: return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=two-sided-short-edge"); default: return QStringList(); //Use printer default } } QStringList FilePrinter::optionPageOrder( QPrinter &printer ) { if ( printer.pageOrder() == QPrinter::LastPageFirst ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("outputorder=reverse"); } return QStringList(QStringLiteral("-o")) << QStringLiteral("outputorder=normal"); } QStringList FilePrinter::optionCollateCopies( QPrinter &printer ) { if ( printer.collateCopies() ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("Collate=True"); } return QStringList(QStringLiteral("-o")) << QStringLiteral("Collate=False"); } QStringList FilePrinter::optionPageMargins( QPrinter &printer ) { return optionPageMargins( printer, ScaleMode::FitToPrintArea ); } QStringList FilePrinter::optionPageMargins( QPrinter &printer, ScaleMode scaleMode ) { if (printer.printEngine()->property(QPrintEngine::PPK_PageMargins).isNull()) { return QStringList(); } else { qreal l(0), t(0), r(0), b(0); if (!printer.fullPage()) { printer.getPageMargins( &l, &t, &r, &b, QPrinter::Point ); } QStringList marginOptions; marginOptions << (QStringLiteral("-o")) << QStringLiteral("page-left=%1").arg(l) << QStringLiteral("-o") << QStringLiteral("page-top=%1").arg(t) << QStringLiteral("-o") << QStringLiteral("page-right=%1").arg(r) << QStringLiteral("-o") << QStringLiteral("page-bottom=%1").arg(b); if (scaleMode == ScaleMode::FitToPrintArea) { marginOptions << QStringLiteral("-o") << QStringLiteral("fit-to-page"); } return marginOptions; } } QStringList FilePrinter::optionCupsProperties( QPrinter &printer ) { QStringList dialogOptions = printer.printEngine()->property(QPrintEngine::PrintEnginePropertyKey(0xfe00)).toStringList(); QStringList cupsOptions; for ( int i = 0; i < dialogOptions.count(); i = i + 2 ) { if ( dialogOptions[i+1].isEmpty() ) { cupsOptions << QStringLiteral("-o") << dialogOptions[i]; } else { cupsOptions << QStringLiteral("-o") << dialogOptions[i] + QLatin1Char('=') + dialogOptions[i+1]; } } return cupsOptions; } /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/script/kjs_app.cpp b/core/script/kjs_app.cpp index ece8a67c8..7d594bfbc 100644 --- a/core/script/kjs_app.cpp +++ b/core/script/kjs_app.cpp @@ -1,231 +1,231 @@ /*************************************************************************** * Copyright (C) 2008 by Pino Toscano * * Copyright (C) 2008 by Harri Porten * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "kjs_app_p.h" #include #include #include #include #include #include #include "../document_p.h" #include "kjs_fullscreen_p.h" using namespace Okular; static KJSPrototype *g_appProto; // the acrobat version we fake static const double fake_acroversion = 8.00; static const struct FakePluginInfo { const char *name; bool certified; bool loaded; const char *path; } s_fake_plugins[] = { { "Annots", true, true, "" }, { "EFS", true, true, "" }, { "EScript", true, true, "" }, { "Forms", true, true, "" }, { "ReadOutLoud", true, true, "" }, { "WebLink", true, true, "" } }; static const int s_num_fake_plugins = sizeof( s_fake_plugins ) / sizeof( s_fake_plugins[0] ); static KJSObject appGetFormsVersion( KJSContext *, void * ) { // faking a bit... return KJSNumber( fake_acroversion ); } static KJSObject appGetLanguage( KJSContext *, void * ) { QLocale locale; QString lang = QLocale::languageToString(locale.language()); QString country = QLocale::countryToString(locale.country()); QString acroLang = QStringLiteral( "ENU" ); if ( lang == QLatin1String( "da" ) ) acroLang = QStringLiteral( "DAN" ); // Danish else if ( lang == QLatin1String( "de" ) ) acroLang = QStringLiteral( "DEU" ); // German else if ( lang == QLatin1String( "en" ) ) acroLang = QStringLiteral( "ENU" ); // English else if ( lang == QLatin1String( "es" ) ) acroLang = QStringLiteral( "ESP" ); // Spanish else if ( lang == QLatin1String( "fr" ) ) acroLang = QStringLiteral( "FRA" ); // French else if ( lang == QLatin1String( "it" ) ) acroLang = QStringLiteral( "ITA" ); // Italian else if ( lang == QLatin1String( "ko" ) ) acroLang = QStringLiteral( "KOR" ); // Korean else if ( lang == QLatin1String( "ja" ) ) acroLang = QStringLiteral( "JPN" ); // Japanese else if ( lang == QLatin1String( "nl" ) ) acroLang = QStringLiteral( "NLD" ); // Dutch else if ( lang == QLatin1String( "pt" ) && country == QLatin1String( "BR" ) ) acroLang = QStringLiteral( "PTB" ); // Brazilian Portuguese else if ( lang == QLatin1String( "fi" ) ) acroLang = QStringLiteral( "SUO" ); // Finnish else if ( lang == QLatin1String( "sv" ) ) acroLang = QStringLiteral( "SVE" ); // Swedish else if ( lang == QLatin1String( "zh" ) && country == QLatin1String( "CN" ) ) acroLang = QStringLiteral( "CHS" ); // Chinese Simplified else if ( lang == QLatin1String( "zh" ) && country == QLatin1String( "TW" ) ) acroLang = QStringLiteral( "CHT" ); // Chinese Traditional return KJSString( acroLang ); } static KJSObject appGetNumPlugins( KJSContext *, void * ) { return KJSNumber( s_num_fake_plugins ); } static KJSObject appGetPlatform( KJSContext *, void * ) { #if defined(Q_OS_WIN) return KJSString( QString::fromLatin1( "WIN" ) ); #elif defined(Q_OS_MAC) return KJSString( QString::fromLatin1( "MAC" ) ); #else - return KJSString( QLatin1String( "UNIX" ) ); + return KJSString( QStringLiteral( "UNIX" ) ); #endif } static KJSObject appGetPlugIns( KJSContext *context, void * ) { KJSArray plugins( context, s_num_fake_plugins ); for ( int i = 0; i < s_num_fake_plugins; ++i ) { const FakePluginInfo &info = s_fake_plugins[i]; KJSObject plugin; plugin.setProperty( context, QStringLiteral("certified"), info.certified ); plugin.setProperty( context, QStringLiteral("loaded"), info.loaded ); plugin.setProperty( context, QStringLiteral("name"), info.name ); plugin.setProperty( context, QStringLiteral("path"), info.path ); plugin.setProperty( context, QStringLiteral("version"), fake_acroversion ); plugins.setProperty( context, QString::number( i ), plugin ); } return plugins; } static KJSObject appGetPrintColorProfiles( KJSContext *context, void * ) { return KJSArray( context, 0 ); } static KJSObject appGetPrinterNames( KJSContext *context, void * ) { return KJSArray( context, 0 ); } static KJSObject appGetViewerType( KJSContext *, void * ) { // faking a bit... - return KJSString( QLatin1String( "Reader" ) ); + return KJSString( QStringLiteral( "Reader" ) ); } static KJSObject appGetViewerVariation( KJSContext *, void * ) { // faking a bit... - return KJSString( QLatin1String( "Reader" ) ); + return KJSString( QStringLiteral( "Reader" ) ); } static KJSObject appGetViewerVersion( KJSContext *, void * ) { // faking a bit... return KJSNumber( fake_acroversion ); } static KJSObject appBeep( KJSContext *context, void *, const KJSArguments &arguments ) { if ( arguments.count() < 1 ) { return context->throwException( QStringLiteral("Missing beep type") ); } QApplication::beep(); return KJSUndefined(); } static KJSObject appGetNthPlugInName( KJSContext *context, void *, const KJSArguments &arguments ) { if ( arguments.count() < 1 ) { return context->throwException( QStringLiteral("Missing plugin index") ); } const int nIndex = arguments.at( 0 ).toInt32( context ); if ( nIndex < 0 || nIndex >= s_num_fake_plugins ) return context->throwException( QStringLiteral("PlugIn index out of bounds") ); const FakePluginInfo &info = s_fake_plugins[nIndex]; return KJSString( info.name ); } static KJSObject appGoBack( KJSContext *, void *object, const KJSArguments & ) { const DocumentPrivate *doc = reinterpret_cast< DocumentPrivate * >( object ); if ( doc->m_parent->historyAtBegin() ) return KJSUndefined(); doc->m_parent->setPrevViewport(); return KJSUndefined(); } static KJSObject appGoForward( KJSContext *, void *object, const KJSArguments & ) { const DocumentPrivate *doc = reinterpret_cast< DocumentPrivate * >( object ); if ( doc->m_parent->historyAtEnd() ) return KJSUndefined(); doc->m_parent->setNextViewport(); return KJSUndefined(); } void JSApp::initType( KJSContext *ctx ) { static bool initialized = false; if ( initialized ) return; initialized = true; g_appProto = new KJSPrototype(); g_appProto->defineProperty( ctx, QStringLiteral("formsVersion"), appGetFormsVersion ); g_appProto->defineProperty( ctx, QStringLiteral("language"), appGetLanguage ); g_appProto->defineProperty( ctx, QStringLiteral("numPlugIns"), appGetNumPlugins ); g_appProto->defineProperty( ctx, QStringLiteral("platform"), appGetPlatform ); g_appProto->defineProperty( ctx, QStringLiteral("plugIns"), appGetPlugIns ); g_appProto->defineProperty( ctx, QStringLiteral("printColorProfiles"), appGetPrintColorProfiles ); g_appProto->defineProperty( ctx, QStringLiteral("printerNames"), appGetPrinterNames ); g_appProto->defineProperty( ctx, QStringLiteral("viewerType"), appGetViewerType ); g_appProto->defineProperty( ctx, QStringLiteral("viewerVariation"), appGetViewerVariation ); g_appProto->defineProperty( ctx, QStringLiteral("viewerVersion"), appGetViewerVersion ); g_appProto->defineFunction( ctx, QStringLiteral("beep"), appBeep ); g_appProto->defineFunction( ctx, QStringLiteral("getNthPlugInName"), appGetNthPlugInName ); g_appProto->defineFunction( ctx, QStringLiteral("goBack"), appGoBack ); g_appProto->defineFunction( ctx, QStringLiteral("goForward"), appGoForward ); } KJSObject JSApp::object( KJSContext *ctx, DocumentPrivate *doc ) { return g_appProto->constructObject( ctx, doc ); } diff --git a/core/scripter.cpp b/core/scripter.cpp index 46462793b..c5f86b82a 100644 --- a/core/scripter.cpp +++ b/core/scripter.cpp @@ -1,96 +1,96 @@ /*************************************************************************** * Copyright (C) 2008 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 "scripter.h" #include #include #include "debug_p.h" #include "script/executor_kjs_p.h" using namespace Okular; class Okular::ScripterPrivate { public: ScripterPrivate( DocumentPrivate *doc ) : m_doc( doc ) #ifdef WITH_KJS , m_kjs( nullptr ) #endif , m_event( nullptr ) { } DocumentPrivate *m_doc; #ifdef WITH_KJS QScopedPointer m_kjs; #endif Event *m_event; }; Scripter::Scripter( DocumentPrivate *doc ) : d( new ScripterPrivate( doc ) ) { } Scripter::~Scripter() { delete d; } QString Scripter::execute( ScriptType type, const QString &script ) { qCDebug(OkularCoreDebug) << "executing the script:"; #ifdef WITH_KJS #if 0 if ( script.length() < 1000 ) qDebug() << script; else qDebug() << script.left( 1000 ) << "[...]"; #endif static QString builtInScript; if ( builtInScript.isNull() ) { - QFile builtInResource ( ":/script/builtin.js" ); + QFile builtInResource ( QStringLiteral(":/script/builtin.js") ); if (!builtInResource.open( QIODevice::ReadOnly )) { qCDebug(OkularCoreDebug) << "failed to load builtin script"; } else { builtInScript = QString::fromUtf8( builtInResource.readAll() ); builtInResource.close(); } } switch ( type ) { case JavaScript: if ( !d->m_kjs ) { d->m_kjs.reset(new ExecutorKJS( d->m_doc )); } d->m_kjs->execute( builtInScript + script, d->m_event ); break; } #endif return QString(); } void Scripter::setEvent( Event *event ) { d->m_event = event; } Event *Scripter::event() const { return d->m_event; } diff --git a/generators/epub/converter.cpp b/generators/epub/converter.cpp index e520500d7..f02dcec24 100644 --- a/generators/epub/converter.cpp +++ b/generators/epub/converter.cpp @@ -1,458 +1,458 @@ /*************************************************************************** * Copyright (C) 2008 by Ely Levy * * * * 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 "converter.h" #include #include #include #include #include #include // Because of the HACK #include #include #include #include #include #include #include using namespace Epub; Converter::Converter() : mTextDocument(NULL) { } Converter::~Converter() { } // join the char * array into one QString QString _strPack(char **str, int size) { QString res; res = QString::fromUtf8(str[0]); for (int i=1;igetEpub(), type, &size); if (data) { emit addMetaData(key, _strPack((char **)data, size)); for (int i=0;iend(); bit = bit.next()) { for (QTextBlock::iterator fit = bit.begin(); !(fit.atEnd()); ++fit) { QTextFragment frag = fit.fragment(); if (frag.isValid() && frag.charFormat().isAnchor()) { QString hrefString = frag.charFormat().anchorHref(); // remove ./ or ../ // making it easier to compare, with links while(!hrefString.isNull() && ( hrefString.at(0) == QLatin1Char('.') || hrefString.at(0) == QLatin1Char('/')) ){ hrefString.remove(0,1); } QUrl href(hrefString); if (href.isValid() && !href.isEmpty()) { if (href.isRelative()) { // Inside document link if(!hrefString.indexOf(QLatin1Char('#'))) hrefString = name + hrefString; else if(QFileInfo(hrefString).path() == QLatin1String(".") && curDir != QLatin1String(".")) hrefString = curDir + QLatin1Char('/') + hrefString; // QTextCharFormat sometimes splits a link in two // if there's no white space between words & the first one is an anchor // consider whole word to be an anchor ++fit; int fragLen = frag.length(); if(!fit.atEnd() && ((fit.fragment().position() - frag.position()) == 1)) fragLen += fit.fragment().length(); --fit; _insert_local_links(hrefString, QPair(frag.position(), frag.position()+fragLen)); } else { // Outside document link Okular::BrowseAction *action = new Okular::BrowseAction(QUrl(href.toString())); emit addAction(action, frag.position(), frag.position() + frag.length()); } } const QStringList &names = frag.charFormat().anchorNames(); if (!names.empty()) { for (QStringList::const_iterator lit = names.constBegin(); lit != names.constEnd(); ++lit) { mSectionMap.insert(name + QLatin1Char('#') + *lit, bit); } } } // end anchor case } } } void Converter::_insert_local_links(const QString &key, const QPair &value) { if(mLocalLinks.contains(key)){ mLocalLinks[key].append(value); } else { QVector< QPair > vec; vec.append(value); mLocalLinks.insert(key,vec); } } static QPoint calculateXYPosition( QTextDocument *document, int startPosition ) { const QTextBlock startBlock = document->findBlock( startPosition ); const QRectF startBoundingRect = document->documentLayout()->blockBoundingRect( startBlock ); QTextLayout *startLayout = startBlock.layout(); if (!startLayout) { qWarning() << "Start layout not found" << startLayout; return QPoint(); } int startPos = startPosition - startBlock.position(); const QTextLine startLine = startLayout->lineForTextPosition( startPos ); double x = startBoundingRect.x() ; double y = startBoundingRect.y() + startLine.y(); y = (int)y % 800; return QPoint(x,y); } QTextDocument* Converter::convert( const QString &fileName ) { EpubDocument *newDocument = new EpubDocument(fileName); if (!newDocument->isValid()) { emit error(i18n("Error while opening the EPub document."), -1); delete newDocument; return NULL; } mTextDocument = newDocument; QTextCursor *_cursor = new QTextCursor( mTextDocument ); mLocalLinks.clear(); mSectionMap.clear(); // Emit the document meta data _emitData(Okular::DocumentInfo::Title, EPUB_TITLE); _emitData(Okular::DocumentInfo::Author, EPUB_CREATOR); _emitData(Okular::DocumentInfo::Subject, EPUB_SUBJECT); _emitData(Okular::DocumentInfo::Creator, EPUB_PUBLISHER); _emitData(Okular::DocumentInfo::Description, EPUB_DESCRIPTION); _emitData(Okular::DocumentInfo::CreationDate, EPUB_DATE); _emitData(Okular::DocumentInfo::Category, EPUB_TYPE); _emitData(Okular::DocumentInfo::Copyright, EPUB_RIGHTS); emit addMetaData( Okular::DocumentInfo::MimeType, QStringLiteral("application/epub+zip")); struct eiterator *it; // iterate over the book it = epub_get_iterator(mTextDocument->getEpub(), EITERATOR_SPINE, 0); // if the background color of the document is non-white it will be handled by QTextDocument::setHtml() bool firstPage = true; QVector movieAnnots; QVector soundActions; // HACK BEGIN Get the links without CSS to be blue // Remove if Qt ever gets fixed and the code in textdocumentgenerator.cpp works const QPalette orig = qApp->palette(); QPalette p = orig; p.setColor(QPalette::Link, Qt::blue); // HACK END const QSize videoSize(320, 240); do{ if(!epub_it_get_curr(it)) { continue; } movieAnnots.clear(); soundActions.clear(); const QString link = QString::fromUtf8(epub_it_get_curr_url(it)); mTextDocument->setCurrentSubDocument(link); QString htmlContent = QString::fromUtf8(epub_it_get_curr(it)); // as QTextCharFormat::anchorNames() ignores sections, replace it with

htmlContent.replace(QRegExp(QStringLiteral("< *section")),QStringLiteral("maxContentHeight(); const int maxWidth = mTextDocument->maxContentWidth(); QDomDocument dom; if(dom.setContent(htmlContent)) { QDomNodeList svgs = dom.elementsByTagName(QStringLiteral("svg")); if(!svgs.isEmpty()) { QList< QDomNode > imgNodes; for (int i = 0; i < svgs.length(); ++i) { QDomNodeList images = svgs.at(i).toElement().elementsByTagName(QStringLiteral("image")); for (int j = 0; j < images.length(); ++j) { QString lnk = images.at(i).toElement().attribute(QStringLiteral("xlink:href")); int ht = images.at(i).toElement().attribute(QStringLiteral("height")).toInt(); int wd = images.at(i).toElement().attribute(QStringLiteral("width")).toInt(); QImage img = mTextDocument->loadResource(QTextDocument::ImageResource,QUrl(lnk)).value(); if(ht == 0) ht = img.height(); if(wd == 0) wd = img.width(); if(ht > maxHeight) ht = maxHeight; if(wd > maxWidth) wd = maxWidth; mTextDocument->addResource(QTextDocument::ImageResource,QUrl(lnk),img); QDomDocument newDoc; newDoc.setContent(QStringLiteral("").arg(lnk).arg(ht).arg(wd)); imgNodes.append(newDoc.documentElement()); } foreach (const QDomNode& nd, imgNodes) { svgs.at(i).parentNode().replaceChild(nd,svgs.at(i)); } } } // handle embedded videos QDomNodeList videoTags = dom.elementsByTagName(QStringLiteral("video")); while(!videoTags.isEmpty()) { QDomNodeList sourceTags = videoTags.at(0).toElement().elementsByTagName(QStringLiteral("source")); if(!sourceTags.isEmpty()) { QString lnk = sourceTags.at(0).toElement().attribute(QStringLiteral("src")); Okular::Movie *movie = new Okular::Movie(mTextDocument->loadResource(EpubDocument::MovieResource,QUrl(lnk)).toString()); movie->setSize(videoSize); movie->setShowControls(true); Okular::MovieAnnotation *annot = new Okular::MovieAnnotation; annot->setMovie(movie); movieAnnots.push_back(annot); QDomDocument tempDoc; tempDoc.setContent(QStringLiteral("

<video></video>
")); videoTags.at(0).parentNode().replaceChild(tempDoc.documentElement(),videoTags.at(0)); } } //handle embedded audio QDomNodeList audioTags = dom.elementsByTagName(QStringLiteral("audio")); while(!audioTags.isEmpty()) { QDomElement element = audioTags.at(0).toElement(); bool repeat = element.hasAttribute(QStringLiteral("loop")); QString lnk = element.attribute(QStringLiteral("src")); Okular::Sound *sound = new Okular::Sound(mTextDocument->loadResource( EpubDocument::AudioResource, QUrl(lnk)).toByteArray()); Okular::SoundAction *soundAction = new Okular::SoundAction(1.0,true,repeat,false,sound); soundActions.push_back(soundAction); QDomDocument tempDoc; tempDoc.setContent(QStringLiteral("
<audio></audio>
")); audioTags.at(0).parentNode().replaceChild(tempDoc.documentElement(),audioTags.at(0)); } htmlContent = dom.toString(); } // HACK BEGIN qApp->setPalette(p); // HACK END QTextBlock before; if(firstPage) { mTextDocument->setHtml(htmlContent); firstPage = false; before = mTextDocument->begin(); } else { before = _cursor->block(); _cursor->insertHtml(htmlContent); } // HACK BEGIN qApp->setPalette(orig); // HACK END QTextCursor csr(before); // a temporary cursor pointing at the begin of the last inserted block int index = 0; while( !movieAnnots.isEmpty() && !(csr = mTextDocument->find(QStringLiteral(""),csr)).isNull() ) { const int posStart = csr.position(); const QPoint startPoint = calculateXYPosition(mTextDocument, posStart); QImage img(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("okular/pics/okular-epub-movie.png"))); img = img.scaled(videoSize); csr.insertImage(img); const int posEnd = csr.position(); const QRect videoRect(startPoint,videoSize); movieAnnots[index]->setBoundingRectangle(Okular::NormalizedRect(videoRect,mTextDocument->pageSize().width(), mTextDocument->pageSize().height())); emit addAnnotation(movieAnnots[index++],posStart,posEnd); csr.movePosition(QTextCursor::NextWord); } csr = QTextCursor(before); index = 0; const QString keyToSearch(QStringLiteral("")); while( !soundActions.isEmpty() && !(csr = mTextDocument->find(keyToSearch, csr)).isNull() ) { const int posStart = csr.position() - keyToSearch.size(); const QImage img(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("okular/pics/okular-epub-sound-icon.png"))); csr.insertImage(img); const int posEnd = csr.position(); qDebug() << posStart << posEnd;; emit addAction(soundActions[index++],posStart,posEnd); csr.movePosition(QTextCursor::NextWord); } mSectionMap.insert(link, before); _handle_anchors(before, link); const int page = mTextDocument->pageCount(); // it will clear the previous format // useful when the last line had a bullet _cursor->insertBlock(QTextBlockFormat()); while(mTextDocument->pageCount() == page) _cursor->insertText(QStringLiteral("\n")); } while (epub_it_get_next(it)); epub_free_iterator(it); // handle toc struct titerator *tit; // FIXME: support other method beside NAVMAP and GUIDE tit = epub_get_titerator(mTextDocument->getEpub(), TITERATOR_NAVMAP, 0); if (!tit) tit = epub_get_titerator(mTextDocument->getEpub(), TITERATOR_GUIDE, 0); if (tit) { do { if (epub_tit_curr_valid(tit)) { char *clink = epub_tit_get_curr_link(tit); QString link = QString::fromUtf8(clink); char *label = epub_tit_get_curr_label(tit); QTextBlock block = mTextDocument->begin(); // must point somewhere if (mSectionMap.contains(link)) { block = mSectionMap.value(link); } else { // load missing resource char *data = 0; //epub_get_data can't handle whitespace url encodings - QByteArray ba = link.replace("%20", " ").toLatin1(); + QByteArray ba = link.replace(QLatin1String("%20"), QLatin1String(" ")).toLatin1(); const char *clinkClean = ba.data(); int size = epub_get_data(mTextDocument->getEpub(), clinkClean, &data); if (data) { _cursor->insertBlock(); // try to load as image and if not load as html block = _cursor->block(); QImage image; mSectionMap.insert(link, block); if (image.loadFromData((unsigned char *)data, size)) { mTextDocument->addResource(QTextDocument::ImageResource, QUrl(link), image); _cursor->insertImage(link); } else { _cursor->insertHtml(QString::fromUtf8(data)); // Add anchors to hashes _handle_anchors(block, link); } // Start new file in a new page int page = mTextDocument->pageCount(); while(mTextDocument->pageCount() == page) _cursor->insertText(QStringLiteral("\n")); } free(data); } if (block.isValid()) { // be sure we actually got a block emit addTitle(epub_tit_get_curr_depth(tit), QString::fromUtf8(label), block); } else { qDebug() << "Error: no block found for"<< link; } if (clink) free(clink); if (label) free(label); } } while (epub_tit_next(tit)); epub_free_titerator(tit); } else { qDebug() << "no toc found"; } // adding link actions QHashIterator > > hit(mLocalLinks); while (hit.hasNext()) { hit.next(); const QTextBlock block = mSectionMap.value(hit.key()); for (int i = 0; i < hit.value().size(); ++i) { if (block.isValid()) { // be sure we actually got a block Okular::DocumentViewport viewport = calculateViewport(mTextDocument, block); Okular::GotoAction *action = new Okular::GotoAction(QString(), viewport); emit addAction(action, hit.value()[i].first, hit.value()[i].second); } else { qDebug() << "Error: no block found for "<< hit.key(); } } } delete _cursor; return mTextDocument; } diff --git a/generators/markdown/generator_md.cpp b/generators/markdown/generator_md.cpp index 7aa297189..d78bc1de2 100644 --- a/generators/markdown/generator_md.cpp +++ b/generators/markdown/generator_md.cpp @@ -1,68 +1,68 @@ /*************************************************************************** * Copyright (C) 2017 by Julian Wolff * * * * 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_md.h" #include "converter.h" #include "debug_md.h" #include #include #include #include OKULAR_EXPORT_PLUGIN(MarkdownGenerator, "libokularGenerator_md.json") bool MarkdownGenerator::s_isFancyPantsEnabled = true; bool MarkdownGenerator::s_wasFancyPantsEnabled = true; MarkdownGenerator::MarkdownGenerator( QObject *parent, const QVariantList &args ) : Okular::TextDocumentGenerator( new Markdown::Converter, QStringLiteral("okular_markdown_generator_settings"), parent, args ) { Okular::TextDocumentSettings *mdSettings = generalSettings(); - mdSettings->addItemBool( "SmartyPants", s_isFancyPantsEnabled, true ); + mdSettings->addItemBool( QStringLiteral("SmartyPants"), s_isFancyPantsEnabled, true ); mdSettings->load(); s_wasFancyPantsEnabled = s_isFancyPantsEnabled; } bool MarkdownGenerator::reparseConfig() { const bool textDocumentGeneratorChangedConfig = Okular::TextDocumentGenerator::reparseConfig(); if (s_wasFancyPantsEnabled != s_isFancyPantsEnabled) { s_wasFancyPantsEnabled = s_isFancyPantsEnabled; Markdown::Converter *c = static_cast( converter() ); c->convertAgain(); setTextDocument( c->document() ); return true; } return textDocumentGeneratorChangedConfig; } void MarkdownGenerator::addPages( KConfigDialog* dlg ) { Okular::TextDocumentSettingsWidget *widget = new Okular::TextDocumentSettingsWidget(); QCheckBox *enableSmartyPants = new QCheckBox( dlg ); - enableSmartyPants->setObjectName( QString::fromUtf8( "kcfg_SmartyPants" ) ); + enableSmartyPants->setObjectName( QStringLiteral( "kcfg_SmartyPants" ) ); widget->addRow( i18n("Enable SmartyPants formatting"), enableSmartyPants ); dlg->addPage( widget, generalSettings(), i18n("Markdown"), QStringLiteral("text-markdown"), i18n("Markdown Backend Configuration") ); } Q_LOGGING_CATEGORY(OkularMdDebug, "org.kde.okular.generators.md", QtWarningMsg) #include "generator_md.moc" diff --git a/generators/mobipocket/mobidocument.cpp b/generators/mobipocket/mobidocument.cpp index d89d5580a..e230fe094 100644 --- a/generators/mobipocket/mobidocument.cpp +++ b/generators/mobipocket/mobidocument.cpp @@ -1,116 +1,116 @@ /*************************************************************************** * Copyright (C) 2008 by Jakub Stachowski * * * * 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 "mobidocument.h" #include #include #include #include #include #include #include // Because of the HACK #include // Because of the HACK using namespace Mobi; MobiDocument::MobiDocument(const QString &fileName) : QTextDocument() { file = new Mobipocket::QFileStream(fileName); doc = new Mobipocket::Document(file); if (doc->isValid()) { QString text=doc->text(); QString header=text.left(1024); if (header.contains(QStringLiteral("")) || header.contains(QStringLiteral(""))) { // HACK BEGIN Get the links without CSS to be blue // Remove if Qt ever gets fixed and the code in textdocumentgenerator.cpp works const QPalette orig = qApp->palette(); QPalette p = orig; p.setColor(QPalette::Link, Qt::blue); qApp->setPalette(p); // HACK END setHtml(fixMobiMarkup(text)); // HACK BEGIN qApp->setPalette(orig); // HACK END } else { setPlainText(text); } } } MobiDocument::~MobiDocument() { delete doc; delete file; } QVariant MobiDocument::loadResource(int type, const QUrl &name) { if (type!=QTextDocument::ImageResource || name.scheme()!=QString(QStringLiteral("pdbrec"))) return QVariant(); bool ok; - quint16 recnum=name.path().mid(1).toUShort(&ok); + quint16 recnum=name.path().midRef(1).toUShort(&ok); if (!ok || recnum>=doc->imageCount()) return QVariant(); QVariant resource; resource.setValue(doc->getImage(recnum-1)); addResource(type, name, resource); return resource; } // starting from 'pos', find position in the string that is not inside a tag int outsideTag(const QString& data, int pos) { for (int i=pos-1;i>=0;i--) { if (data[i]==QLatin1Char('>')) return pos; if (data[i]==QLatin1Char('<')) return i; } return pos; } QString MobiDocument::fixMobiMarkup(const QString& data) { QString ret=data; QMap anchorPositions; static QRegExp anchors(QStringLiteral(" it(anchorPositions); while (it.hasNext()) { it.next(); // link pointing outside the document, ignore if ( (it.key()+offset) >= ret.size()) continue; int fixedpos=outsideTag(ret, it.key()+offset); ret.insert(fixedpos,QStringLiteral(" ")); // inserting anchor shifts all offsets after the anchor offset+=21+it.value().size(); } // replace links referencing filepos with normal internal links ret.replace(anchors, QStringLiteral(" where recindex is number of // record containing image static QRegExp imgs(QStringLiteral(""), Qt::CaseInsensitive); imgs.setMinimal(true); ret.replace(imgs, QStringLiteral("")); ret.replace(QStringLiteral(""), QStringLiteral("

")); return ret; } diff --git a/mobile/app/main.cpp b/mobile/app/main.cpp index b7046d7eb..b983f5aaf 100644 --- a/mobile/app/main.cpp +++ b/mobile/app/main.cpp @@ -1,75 +1,75 @@ /************************************************************************************* * Copyright (C) 2010 by Aleix Pol * * * * 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 program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * *************************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __ANDROID__ #include "android.h" Q_DECL_EXPORT #endif int main(int argc, char *argv[]) { QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication app(argc, argv); app.setApplicationName(QStringLiteral("okularkirigami")); QCommandLineParser parser; parser.addVersionOption(); parser.addHelpOption(); //parser.setApplicationDescription(i18n("Okular mobile")); parser.process(app); QQmlApplicationEngine engine; #ifdef __ANDROID__ AndroidInstance::handleViewIntent(); qmlRegisterSingletonType("org.kde.okular.app", 2, 0, "AndroidInstance", [](QQmlEngine*, QJSEngine*) -> QObject* { return new AndroidInstance; }); const QString uri = URIHandler::handler.m_lastUrl; #else qmlRegisterSingletonType("org.kde.okular.app", 2, 0, "AndroidInstance", [](QQmlEngine*, QJSEngine*) -> QObject* { return new QObject; }); const QString uri = parser.positionalArguments().count() == 1 ? QUrl::fromUserInput(parser.positionalArguments().constFirst(), {}, QUrl::AssumeLocalFile).toString() : QString(); #endif engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); engine.rootContext()->setContextProperty(QStringLiteral("uri"), uri); QVariantMap paths; paths[QStringLiteral("desktop")] = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); paths[QStringLiteral("documents")] = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); paths[QStringLiteral("music")] = QStandardPaths::writableLocation(QStandardPaths::MusicLocation); paths[QStringLiteral("movies")] = QStandardPaths::writableLocation(QStandardPaths::MoviesLocation); paths[QStringLiteral("pictures")] = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); paths[QStringLiteral("home")] = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); engine.rootContext()->setContextProperty(QStringLiteral("userPaths"), paths); - engine.setBaseUrl(QUrl("qrc:/package/contents/ui/")); - engine.load(QUrl("qrc:/package/contents/ui/main.qml")); + engine.setBaseUrl(QUrl(QStringLiteral("qrc:/package/contents/ui/"))); + engine.load(QUrl(QStringLiteral("qrc:/package/contents/ui/main.qml"))); return app.exec(); } diff --git a/mobile/components/documentitem.cpp b/mobile/components/documentitem.cpp index 27368c020..403eb11a3 100644 --- a/mobile/components/documentitem.cpp +++ b/mobile/components/documentitem.cpp @@ -1,269 +1,269 @@ /* * Copyright 2012 by Marco Martin * * 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, * or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "documentitem.h" #include // krazy:exclude=includes #include #include #include #include #include "ui/tocmodel.h" DocumentItem::DocumentItem(QObject *parent) : QObject(parent), m_thumbnailObserver(nullptr), m_pageviewObserver(nullptr), m_searchInProgress(false) { qmlRegisterUncreatableType("org.kde.okular", 1, 0, "TOCModel", QStringLiteral("Do not create objects of this type.")); Okular::Settings::instance(QStringLiteral("okularproviderrc")); m_document = new Okular::Document(nullptr); m_tocModel = new TOCModel(m_document, this); connect(m_document, &Okular::Document::searchFinished, this, &DocumentItem::searchFinished); connect(m_document->bookmarkManager(), &Okular::BookmarkManager::bookmarksChanged, this, &DocumentItem::bookmarkedPagesChanged); connect(m_document->bookmarkManager(), &Okular::BookmarkManager::bookmarksChanged, this, &DocumentItem::bookmarksChanged); } DocumentItem::~DocumentItem() { delete m_document; } void DocumentItem::setUrl(const QUrl & url) { m_document->closeDocument(); //TODO: password QMimeDatabase db; - const QString path = url.isLocalFile() ? url.toLocalFile() : QLatin1String("-"); + const QString path = url.isLocalFile() ? url.toLocalFile() : QStringLiteral("-"); m_document->openDocument(path, url, db.mimeTypeForUrl(url)); m_tocModel->clear(); m_tocModel->fill(m_document->documentSynopsis()); m_tocModel->setCurrentViewport(m_document->viewport()); m_matchingPages.clear(); for (uint i = 0; i < m_document->pages(); ++i) { m_matchingPages << (int)i; } emit matchingPagesChanged(); emit urlChanged(); emit pageCountChanged(); emit openedChanged(); emit supportsSearchingChanged(); emit windowTitleForDocumentChanged(); emit bookmarkedPagesChanged(); } QString DocumentItem::windowTitleForDocument() const { // 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 ? m_document->currentDocument().toDisplayString(QUrl::PreferLocalFile) : m_document->currentDocument().fileName(); if (Okular::Settings::displayDocumentTitle()) { const QString docTitle = m_document->metaData( QStringLiteral("DocumentTitle") ).toString(); if (!docTitle.isEmpty() && !docTitle.trimmed().isEmpty()) { title = docTitle; } } return title; } QUrl DocumentItem::url() const { return m_document->currentDocument(); } void DocumentItem::setCurrentPage(int page) { m_document->setViewportPage(page); m_tocModel->setCurrentViewport(m_document->viewport()); emit currentPageChanged(); } int DocumentItem::currentPage() const { return m_document->currentPage(); } bool DocumentItem::isOpened() const { return m_document->isOpened(); } int DocumentItem::pageCount() const { return m_document->pages(); } QVariantList DocumentItem::matchingPages() const { return m_matchingPages; } TOCModel *DocumentItem::tableOfContents() const { return m_tocModel; } QVariantList DocumentItem::bookmarkedPages() const { QList list; QSet pages; foreach (const KBookmark &bookmark, m_document->bookmarkManager()->bookmarks()) { Okular::DocumentViewport viewport(bookmark.url().fragment()); pages << viewport.pageNumber; } list = pages.toList(); std::sort(list.begin(), list.end()); QVariantList variantList; foreach (const int page, list) { variantList << page; } return variantList; } QStringList DocumentItem::bookmarks() const { QStringList list; foreach(const KBookmark &bookmark, m_document->bookmarkManager()->bookmarks()) { list << bookmark.url().toString(); } return list; } bool DocumentItem::supportsSearching() const { return m_document->supportsSearching(); } bool DocumentItem::isSearchInProgress() const { return m_searchInProgress; } void DocumentItem::searchText(const QString &text) { if (text.isEmpty()) { resetSearch(); return; } m_document->cancelSearch(); m_document->resetSearch(PAGEVIEW_SEARCH_ID); m_document->searchText(PAGEVIEW_SEARCH_ID, text, 1, Qt::CaseInsensitive, Okular::Document::AllDocument, true, QColor(100,100,200,40)); if (!m_searchInProgress) { m_searchInProgress = true; emit searchInProgressChanged(); } } void DocumentItem::resetSearch() { m_document->resetSearch(PAGEVIEW_SEARCH_ID); m_matchingPages.clear(); for (uint i = 0; i < m_document->pages(); ++i) { m_matchingPages << (int)i; } if (m_searchInProgress) { m_searchInProgress = false; emit searchInProgressChanged(); } emit matchingPagesChanged(); } Okular::Document *DocumentItem::document() { return m_document; } Observer *DocumentItem::thumbnailObserver() { if (!m_thumbnailObserver) m_thumbnailObserver = new Observer(this); return m_thumbnailObserver; } Observer *DocumentItem::pageviewObserver() { if (!m_pageviewObserver) { m_pageviewObserver = new Observer(this); } return m_pageviewObserver; } void DocumentItem::searchFinished(int id, Okular::Document::SearchStatus endStatus) { Q_UNUSED(endStatus) if (id != PAGEVIEW_SEARCH_ID) { return; } m_matchingPages.clear(); for (uint i = 0; i < m_document->pages(); ++i) { if (m_document->page(i)->hasHighlights(id)) { m_matchingPages << (int)i; } } if (m_searchInProgress) { m_searchInProgress = false; emit searchInProgressChanged(); } emit matchingPagesChanged(); } //Observer Observer::Observer(DocumentItem *parent) : QObject(parent), m_document(parent) { parent->document()->addObserver(this); } Observer::~Observer() { } void Observer::notifyPageChanged(int page, int flags) { emit pageChanged(page, flags); } diff --git a/mobile/components/pageitem.cpp b/mobile/components/pageitem.cpp index f4ae8234a..75930bfe9 100644 --- a/mobile/components/pageitem.cpp +++ b/mobile/components/pageitem.cpp @@ -1,444 +1,444 @@ /* * Copyright 2012 by Marco Martin * * 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, * or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "pageitem.h" #include "documentitem.h" #include #include #include #include #include #include #include #include #include #include "ui/pagepainter.h" #include "ui/priorities.h" #include "settings.h" #define REDRAW_TIMEOUT 250 PageItem::PageItem(QQuickItem *parent) : QQuickItem(parent), - Okular::View( QLatin1String( "PageView" ) ), + Okular::View( QStringLiteral( "PageView" ) ), m_page(nullptr), m_smooth(false), m_bookmarked(false), m_isThumbnail(false) { setFlag(QQuickItem::ItemHasContents, true); m_viewPort.rePos.enabled = true; m_redrawTimer = new QTimer(this); m_redrawTimer->setInterval(REDRAW_TIMEOUT); m_redrawTimer->setSingleShot(true); connect(m_redrawTimer, &QTimer::timeout, this, &PageItem::requestPixmap); connect(this, &QQuickItem::windowChanged, m_redrawTimer, [this]() {m_redrawTimer->start(); }); } PageItem::~PageItem() { } void PageItem::setFlickable(QQuickItem *flickable) { if (m_flickable.data() == flickable) { return; } //check the object can act as a flickable if (!flickable->property("contentX").isValid() || !flickable->property("contentY").isValid()) { return; } if (m_flickable) { disconnect(m_flickable.data(), nullptr, this, nullptr); } //check the object can act as a flickable if (!flickable->property("contentX").isValid() || !flickable->property("contentY").isValid()) { m_flickable.clear(); return; } m_flickable = flickable; if (flickable) { connect(flickable, SIGNAL(contentXChanged()), this, SLOT(contentXChanged())); connect(flickable, SIGNAL(contentYChanged()), this, SLOT(contentYChanged())); } emit flickableChanged(); } QQuickItem *PageItem::flickable() const { return m_flickable.data(); } DocumentItem *PageItem::document() const { return m_documentItem.data(); } void PageItem::setDocument(DocumentItem *doc) { if (doc == m_documentItem.data() || !doc) { return; } m_page = nullptr; disconnect(doc, nullptr, this, nullptr); m_documentItem = doc; Observer *observer = m_isThumbnail ? m_documentItem.data()->thumbnailObserver() : m_documentItem.data()->pageviewObserver(); connect(observer, &Observer::pageChanged, this, &PageItem::pageHasChanged); connect(doc->document()->bookmarkManager(), &Okular::BookmarkManager::bookmarksChanged, this, &PageItem::checkBookmarksChanged); setPageNumber(0); emit documentChanged(); m_redrawTimer->start(); connect(doc, &DocumentItem::urlChanged, this, &PageItem::refreshPage); } int PageItem::pageNumber() const { return m_viewPort.pageNumber; } void PageItem::setPageNumber(int number) { if ((m_page && m_viewPort.pageNumber == number) || !m_documentItem || !m_documentItem.data()->isOpened() || number < 0) { return; } m_viewPort.pageNumber = number; refreshPage(); emit pageNumberChanged(); checkBookmarksChanged(); } void PageItem::refreshPage() { if (uint(m_viewPort.pageNumber) < m_documentItem.data()->document()->pages()) { m_page = m_documentItem.data()->document()->page(m_viewPort.pageNumber); } else { m_page = nullptr; } emit implicitWidthChanged(); emit implicitHeightChanged(); m_redrawTimer->start(); } int PageItem::implicitWidth() const { if (m_page) { return m_page->width(); } return 0; } int PageItem::implicitHeight() const { if (m_page) { return m_page->height(); } return 0; } void PageItem::setSmooth(const bool smooth) { if (smooth == m_smooth) { return; } m_smooth = smooth; update(); } bool PageItem::smooth() const { return m_smooth; } bool PageItem::isBookmarked() { return m_bookmarked; } void PageItem::setBookmarked(bool bookmarked) { if (!m_documentItem) { return; } if (bookmarked == m_bookmarked) { return; } if (bookmarked) { m_documentItem.data()->document()->bookmarkManager()->addBookmark(m_viewPort); } else { m_documentItem.data()->document()->bookmarkManager()->removeBookmark(m_viewPort.pageNumber); } m_bookmarked = bookmarked; emit bookmarkedChanged(); } QStringList PageItem::bookmarks() const { QStringList list; foreach(const KBookmark &bookmark, m_documentItem.data()->document()->bookmarkManager()->bookmarks(m_viewPort.pageNumber)) { list << bookmark.url().toString(); } return list; } void PageItem::goToBookmark(const QString &bookmark) { Okular::DocumentViewport viewPort(QUrl::fromUserInput(bookmark).fragment(QUrl::FullyDecoded)); setPageNumber(viewPort.pageNumber); //Are we in a flickable? if (m_flickable) { //normalizedX is a proportion, so contentX will be the difference between document and viewport times normalizedX m_flickable.data()->setProperty("contentX", qMax((qreal)0, width() - m_flickable.data()->width()) * viewPort.rePos.normalizedX); m_flickable.data()->setProperty("contentY", qMax((qreal)0, height() - m_flickable.data()->height()) * viewPort.rePos.normalizedY); } } QPointF PageItem::bookmarkPosition(const QString &bookmark) const { Okular::DocumentViewport viewPort(QUrl::fromUserInput(bookmark).fragment(QUrl::FullyDecoded)); if (viewPort.pageNumber != m_viewPort.pageNumber) { return QPointF(-1, -1); } return QPointF(qMax((qreal)0, width() - m_flickable.data()->width()) * viewPort.rePos.normalizedX, qMax((qreal)0, height() - m_flickable.data()->height()) * viewPort.rePos.normalizedY); } void PageItem::setBookmarkAtPos(qreal x, qreal y) { Okular::DocumentViewport viewPort(m_viewPort); viewPort.rePos.normalizedX = x; viewPort.rePos.normalizedY = y; m_documentItem.data()->document()->bookmarkManager()->addBookmark(viewPort); if (!m_bookmarked) { m_bookmarked = true; emit bookmarkedChanged(); } emit bookmarksChanged(); } void PageItem::removeBookmarkAtPos(qreal x, qreal y) { Okular::DocumentViewport viewPort(m_viewPort); viewPort.rePos.enabled = true; viewPort.rePos.normalizedX = x; viewPort.rePos.normalizedY = y; m_documentItem.data()->document()->bookmarkManager()->addBookmark(viewPort); if (m_bookmarked && m_documentItem.data()->document()->bookmarkManager()->bookmarks(m_viewPort.pageNumber).count() == 0) { m_bookmarked = false; emit bookmarkedChanged(); } emit bookmarksChanged(); } void PageItem::removeBookmark(const QString &bookmark) { m_documentItem.data()->document()->bookmarkManager()->removeBookmark(bookmark); emit bookmarksChanged(); } //Reimplemented void PageItem::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) { if (newGeometry.size().isEmpty()) { return; } bool changed = false; if (newGeometry.size() != oldGeometry.size()) { changed = true; m_redrawTimer->start(); } QQuickItem::geometryChanged(newGeometry, oldGeometry); if (changed) { //Why aren't they automatically emitted? emit widthChanged(); emit heightChanged(); } } QSGNode * PageItem::updatePaintNode(QSGNode* node, QQuickItem::UpdatePaintNodeData* /*data*/) { if (!window() || m_buffer.isNull()) { delete node; return nullptr; } QSGSimpleTextureNode *n = static_cast(node); if (!n) { n = new QSGSimpleTextureNode(); n->setOwnsTexture(true); } n->setTexture(window()->createTextureFromImage(m_buffer)); n->setRect(boundingRect()); return n; } void PageItem::requestPixmap() { if (!m_documentItem || !m_page || !window() || width() <= 0 || height() < 0) { if (!m_buffer.isNull()) { m_buffer = QImage(); update(); } return; } Observer *observer = m_isThumbnail ? m_documentItem.data()->thumbnailObserver() : m_documentItem.data()->pageviewObserver(); const int priority = m_isThumbnail ? THUMBNAILS_PRIO : PAGEVIEW_PRIO; const qreal dpr = window()->devicePixelRatio(); // Here we want to request the pixmap for the page, but it may happen that the page // already has the pixmap, thus requestPixmaps would not trigger pageHasChanged // and we would not call paint. Always call paint, if we don't have a pixmap // it's a noop. Requesting a page that already has a pixmap is also // almost a noop. // Ideally we would do one or the other but for now this is good enough paint(); { auto request = new Okular::PixmapRequest(observer, m_viewPort.pageNumber, width() * dpr, height() * dpr, priority, Okular::PixmapRequest::Asynchronous); request->setNormalizedRect(Okular::NormalizedRect(0,0,1,1)); const Okular::Document::PixmapRequestFlag prf = Okular::Document::NoOption; m_documentItem.data()->document()->requestPixmaps({request}, prf); } } void PageItem::paint() { Observer *observer = m_isThumbnail ? m_documentItem.data()->thumbnailObserver() : m_documentItem.data()->pageviewObserver(); const int flags = PagePainter::Accessibility | PagePainter::Highlights | PagePainter::Annotations; const qreal dpr = window()->devicePixelRatio(); const QRect limits(QPoint(0, 0), QSize(width()*dpr, height()*dpr)); QPixmap pix(limits.size()); pix.setDevicePixelRatio(dpr); QPainter p(&pix); p.setRenderHint(QPainter::Antialiasing, m_smooth); PagePainter::paintPageOnPainter(&p, m_page, observer, flags, width(), height(), limits); p.end(); m_buffer = pix.toImage(); update(); } //Protected slots void PageItem::pageHasChanged(int page, int flags) { if (m_viewPort.pageNumber == page) { if (flags == Okular::DocumentObserver::BoundingBox) { // skip bounding box updates //kDebug() << "32" << m_page->boundingBox(); } else if (flags == Okular::DocumentObserver::Pixmap) { // if pixmaps have updated, just repaint .. don't bother updating pixmaps AGAIN paint(); } else { m_redrawTimer->start(); } } } void PageItem::checkBookmarksChanged() { if (!m_documentItem) { return; } bool newBookmarked = m_documentItem.data()->document()->bookmarkManager()->isBookmarked(m_viewPort.pageNumber); if (m_bookmarked != newBookmarked) { m_bookmarked = newBookmarked; emit bookmarkedChanged(); } //TODO: check the page emit bookmarksChanged(); } void PageItem::contentXChanged() { if (!m_flickable || !m_flickable.data()->property("contentX").isValid()) { return; } m_viewPort.rePos.normalizedX = m_flickable.data()->property("contentX").toReal() / (width() - m_flickable.data()->width()); } void PageItem::contentYChanged() { if (!m_flickable || !m_flickable.data()->property("contentY").isValid()) { return; } m_viewPort.rePos.normalizedY = m_flickable.data()->property("contentY").toReal() / (height() - m_flickable.data()->height()); } void PageItem::setIsThumbnail(bool thumbnail) { if (thumbnail == m_isThumbnail) { return; } m_isThumbnail = thumbnail; if (thumbnail) { m_smooth = false; } /* m_redrawTimer->setInterval(thumbnail ? 0 : REDRAW_TIMEOUT); m_redrawTimer->setSingleShot(true); */ } diff --git a/part.cpp b/part.cpp index c831f8793..1cbd9acea 100644 --- a/part.cpp +++ b/part.cpp @@ -1,3705 +1,3705 @@ /*************************************************************************** * 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 #ifdef WITH_KWALLET #include #endif #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 "ui/signaturepanel.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/document_p.h" #include "core/generator.h" #include "core/page.h" #include "core/fileprinter.h" #include "core/printoptionswidget.h" #include #ifdef OKULAR_KEEP_FILE_OPEN 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; }; #endif 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; + compressedMimeMap[ QStringLiteral( "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; + compressedMimeMap[ QStringLiteral( "application/x-bzpdf" ) ] = KFilterDev::BZip2; + compressedMimeMap[ QStringLiteral( "application/x-bzpostscript" ) ] = KFilterDev::BZip2; + compressedMimeMap[ QStringLiteral( "application/x-bzdvi" ) ] = KFilterDev::BZip2; + compressedMimeMap[ QStringLiteral( "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_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_cliPrintAndExit(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("format-list-unordered") ), 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 ); // [left toolbox: Signature Panel] | [] m_signaturePanel = new SignaturePanel( m_document, nullptr ); connect( m_signaturePanel.data(), &SignaturePanel::documentHasSignatures, this, &Part::showSidebarSignaturesItem ); m_sidebar->addItem( m_signaturePanel, QIcon::fromTheme(QStringLiteral("application-pkcs7-signature")), i18n("Signatures") ); showSidebarSignaturesItem( 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_signatureMessage = new KMessageWidget( rightContainer ); m_signatureMessage->setVisible( false ); m_signatureMessage->setWordWrap( true ); m_signatureMessage->setMessageType( KMessageWidget::Information ); rightLayout->addWidget( m_signatureMessage ); 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_signaturePanel->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 ); m_document->addObserver( m_signaturePanel ); 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, [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; m_selectCurrentPage = 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_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_showSignaturePanel = 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(); m_copy = KStandardAction::create( KStandardAction::Copy, m_pageView, SLOT(copyTextSelection()), ac ); m_selectAll = KStandardAction::selectAll( m_pageView, SLOT(selectAll()), ac ); // Setup select all action for the current page m_selectCurrentPage = ac->addAction(QStringLiteral("edit_select_all_current_page")); m_selectCurrentPage->setText(i18n("Select All Text on Current Page")); connect( m_selectCurrentPage, &QAction::triggered, m_pageView, &PageView::slotSelectPage ); m_selectCurrentPage->setEnabled( false ); m_save = KStandardAction::save( this, [this] { saveFile(); }, ac ); m_save->setEnabled( false ); m_saveAs = KStandardAction::saveAs( this, SLOT(slotSaveFileAs()), ac ); 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_showSignaturePanel = ac->add(QStringLiteral("show_signatures")); m_showSignaturePanel->setText(i18n("Show &Signatures Panel")); connect( m_showSignaturePanel, &QAction::triggered, this, [this] { if ( m_sidebar->currentItem() != m_signaturePanel) { m_sidebar->setCurrentItem( m_signaturePanel ); } }); 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 ); #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 Drawing" ), ac ); ac->addAction( QStringLiteral("presentation_erase_drawings"), eraseDrawingAction ); eraseDrawingAction->setIcon( QIcon::fromTheme( QStringLiteral("draw-eraser-delete-objects") ) ); 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_signaturePanel; 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->signalsBlocked(); } void Part::setWatchFileModeEnabled(bool enabled) { // Don't call 'KDirWatch::stopScan()' in here (as of KDE Frameworks 5.51.0, see bug 400541)! // 'KDirWatch::stopScan' has a bug that may affect other code paths that make use of KDirWatch // (other loaded KParts, for example). if( isWatchFileModeEnabled() == enabled ) { return; } m_watcher->blockSignals(!enabled); if( !enabled ) { m_dirtyHandler->stop(); } } 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 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 Okular::BackendConfigDialog( 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::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; } void Part::setFileToWatch( const QString &filePath ) { if ( !m_watchedFilePath.isEmpty() ) unsetFileToWatch(); const QFileInfo fi(filePath); m_watchedFilePath = filePath; m_watcher->addFile( m_watchedFilePath ); if ( fi.isSymLink() ) { m_watchedFileSymlinkTarget = fi.symLinkTarget(); 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; } m_fileLastModified = QFileInfo( localFilePath() ).lastModified(); 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; #ifdef WITH_KWALLET // 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 ); } if ( openResult == Document::OpenSuccess ) { 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 ); } } } #endif } if ( openResult == Document::OpenSuccess ) { m_fileLastModified = QFileInfo( localFilePath() ).lastModified(); } 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_save ) m_save->setEnabled( ok && !( isstdin || mime.inherits( "inode/directory" ) ) ); - if( m_saveAs ) m_saveAs->setEnabled( ok && !( isstdin || mime.inherits( "inode/directory" ) ) ); + if( m_save ) m_save->setEnabled( ok && !( isstdin || mime.inherits( QStringLiteral("inode/directory") ) ) ); + if( m_saveAs ) m_saveAs->setEnabled( ok && !( isstdin || mime.inherits( QStringLiteral("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 ( ok && m_document->metaData( QStringLiteral("IsDigitallySigned") ).toBool() ) { if ( m_embedMode == PrintPreviewMode ) { m_signatureMessage->setText( i18n( "All editing and interactive features for this document are disabled. Please save a copy and reopen to edit this document." ) ); } else { m_signatureMessage->setText( i18n( "This document is digitally signed." ) ); } m_signatureMessage->setVisible( true ); } 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 // Tries to find the text passed from terminal after the file is open if(!m_textToFindOnOpen.isEmpty()) { m_findBar->startSearch(m_textToFindOnOpen); m_textToFindOnOpen = QString(); } } if ( m_exportAsText ) m_exportAsText->setEnabled( ok && m_document->canExportToText() ); 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() ) 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 and we're not in viewer widget mode, or the command line flag was // specified, start presentation const bool presentationBecauseOfDocumentMetadata = ( m_embedMode != ViewerWidgetMode ) && m_document->metaData( QStringLiteral("StartFullScreen") ).toBool(); if ( presentationBecauseOfDocumentMetadata || m_cliPresentation ) { bool goAheadWithPresentationMode = true; if ( !m_cliPresentation ) { const QString text = i18n( "This document wants to be shown full screen.\n" "Leave normal mode and enter presentation mode?" ); const QString caption = i18n( "Request to Change Viewing Mode" ); const KGuiItem yesItem = KGuiItem( i18n( "Enter Presentation Mode" ), QStringLiteral("dialog-ok") ); const KGuiItem noItem = KGuiItem( i18n( "Deny Request" ), QStringLiteral("dialog-cancel") ); 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(); } else if ( m_cliPrintAndExit ) { slotPrint(); } return true; } 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; // The subsequent call to closeUrl clears the arguments. // We want to save them and restore them later. const KParts::OpenUrlArguments args = arguments(); // Close current document if any if ( !closeUrl() ) return false; setArguments(args); 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; // TODO When we get different saving backends we need to query the backend // as to if it can save changes even if the open file has been modified, // since we only have poppler as saving backend for now we're skipping that check if ( m_fileLastModified != QFileInfo( localFilePath() ).lastModified() ) { int res; if ( m_isReloading ) { res = KMessageBox::warningYesNo( widget(), i18n( "There are unsaved changes, and the file '%1' has been modified by another program. Your changes will be lost, because the file can no longer be saved.
Do you want to continue reloading the file?", url().fileName() ), i18n( "File Changed" ), KGuiItem( i18n( "Continue Reloading" ) ), // <- KMessageBox::Yes KGuiItem( i18n( "Abort Reloading" ) )); } else { res = KMessageBox::warningYesNo( widget(), i18n( "There are unsaved changes, and the file '%1' has been modified by another program. Your changes will be lost, because the file can no longer be saved.
Do you want to continue closing the file?", url().fileName() ), i18n( "File Changed" ), KGuiItem( i18n( "Continue Closing" ) ), // <- KMessageBox::Yes KGuiItem( i18n( "Abort Closing" ) )); } return res == KMessageBox::Yes; } const int res = KMessageBox::warningYesNoCancel( widget(), i18n( "Do you want to save your changes to \"%1\" or discard them?", url().fileName() ), i18n( "Close Document" ), KStandardGuiItem::save(), KStandardGuiItem::discard() ); switch ( res ) { 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; 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 ); 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 ); m_exportFormats.clear(); if ( m_exportAs ) { QMenu *menu = m_exportAs->menu(); QList acts = menu->actions(); int num = acts.count(); 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() ) unsetFileToWatch(); m_fileWasRemoved = false; if ( m_generatorGuiClient ) factory()->removeClient( m_generatorGuiClient ); m_generatorGuiClient = nullptr; m_document->closeDocument(); m_fileLastModified = QDateTime(); updateViewActions(); delete m_tempfile; m_tempfile = nullptr; if ( widget() ) { m_searchWidget->clearText(); m_migrationMessage->setVisible( false ); m_topMessage->setVisible( false ); m_formsMessage->setVisible( false ); m_signatureMessage->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 millisecs. // 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 unsetFileToWatch(); setFileToWatch( localFilePath() ); m_dirtyHandler->start( 750 ); } } else if ( fi.isSymLink() && fi.symLinkTarget() == path ) { if ( QFile::exists( fi.symLinkTarget() )) m_dirtyHandler->start( 750 ); else m_fileWasRemoved = true; } } } // 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 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 = 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 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 if ( !oneShot ) { // 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 ); if (m_selectCurrentPage) m_selectCurrentPage->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 (m_selectCurrentPage) m_selectCurrentPage->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::showSidebarSignaturesItem( bool show ) { m_sidebar->setItemVisible( m_signaturePanel, show ); } 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()) ); + QAction *renameAction = contextMenu->addAction( QIcon::fromTheme( QStringLiteral("edit-rename") ), i18n( "Rename this Bookmark" ), this, &Part::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())); + QAction *deleteAction = contextMenu->addAction( QIcon::fromTheme( QStringLiteral("list-remove") ), i18n("Remove this Bookmark"), this, &Part::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() { if ( !isModified() ) return true; else return saveAs( url() ); } bool Part::slotSaveFileAs( bool showOkularArchiveAsDefaultFormat ) { if ( m_embedMode == PrintPreviewMode ) return false; // 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 ); // What data would we lose if we saved natively? bool wontSaveForms, wontSaveAnnotations; checkNativeSaveDataLoss(&wontSaveForms, &wontSaveAnnotations); 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); if ( !saveUrl.isValid() || saveUrl.isEmpty() ) return false; // Has the user chosen to save in .okular archive format? const bool saveAsOkularArchive = ( selectedFilter == okularArchiveMimeTypeFilter ); 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 ); } static QUrl resolveSymlinksIfFileExists( const QUrl &saveUrl ) { if ( saveUrl.isLocalFile() ) { const QFileInfo fi( saveUrl.toLocalFile() ); return fi.exists() ? QUrl::fromLocalFile( fi.canonicalFilePath() ) : saveUrl; } else { return saveUrl; } } bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) { // TODO When we get different saving backends we need to query the backend // as to if it can save changes even if the open file has been modified, // since we only have poppler as saving backend for now we're skipping that check if ( m_fileLastModified != QFileInfo( localFilePath() ).lastModified() ) { KMessageBox::sorry( widget(), i18n( "The file '%1' has been modified by another program, which means it can no longer be saved.", url().fileName() ), i18n( "File Changed" ) ); return false; } 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(); // Figure out the real save url, for symlinks we don't want to copy over the symlink but over the target file const QUrl realSaveUrl = resolveSymlinksIfFileExists( saveUrl ); QScopedPointer tempFile; KIO::Job *copyJob = nullptr; // this will be filled with the job that writes to saveUrl // Does the user want a .okular archive? if ( flags & SaveAsOkularArchive ) { 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 ), realSaveUrl, -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 + KGuiItem( i18n( "Save as Okular document archive..." ), QStringLiteral("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 + KGuiItem( i18n( "Save as Okular document archive..." ), QStringLiteral("document-save-as") ), // <- KMessageBox::Yes + KGuiItem( continueMessage, QStringLiteral("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 ), realSaveUrl, -1, KIO::Overwrite ); } else { // 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 ), realSaveUrl, -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, realSaveUrl, -1, KIO::Overwrite ); } else { // Don't do a real copy in this case, just update the timestamps copyJob = KIO::setModificationTime( realSaveUrl, QDateTime::currentDateTime() ); } } } } // 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'. Error: '%2'. Try to save it to another location.", saveUrl.toDisplayString(), copyJob->errorString() ) ); // Restore watcher if ( url().isLocalFile() ) setFileToWatch( localFilePath() ); return false; } m_document->setHistoryClean( true ); if ( m_document->isDocdataMigrationNeeded() ) m_document->docdataMigrationDone(); bool reloadedCorrectly = true; // Make the generator use the new file instead of the old one if ( m_document->canSwapBackingFile() && !m_documentOpenWithPassword ) { QWidget *currentSidebarItem = m_sidebar->currentItem(); // this calls openFile internally, which in turn actually calls // m_document->swapBackingFile() instead of the regular loadDocument if ( openUrl( saveUrl, true /* swapInsteadOfOpening */ ) ) { if ( setModifiedAfterSave ) { m_document->setHistoryClean( false ); } } else { reloadedCorrectly = false; } if ( m_sidebar->currentItem() != currentSidebarItem ) m_sidebar->setCurrentItem( currentSidebarItem ); } 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() ); //Set correct permission taking into account the umask value #ifndef Q_OS_WIN const QString saveFilePath = saveUrl.toLocalFile(); if ( QFile::exists( saveFilePath ) ) { const mode_t mask = umask( 0 ); umask( mask ); const mode_t fileMode = 0666 & ~mask; chmod( QFile::encodeName( saveFilePath ).constData(), fileMode ); } #endif 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() ) { wontSaveForms = true; break; } } } if ( !m_document->canSaveChanges( Document::SaveAnnotationsCapability ) ) { /* Set wontSaveAnnotations only if there are local annotations */ const int pagecount = m_document->pages(); 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::slotToggleChangeColors() { m_pageView->slotToggleChangeColors(); } void Part::slotSetChangeColors(bool active) { m_pageView->slotSetChangeColors(active); } 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, true); } 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, bool showTOCActions) { 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() ); if (showTOCActions) { popup->addAction( i18n("Expand whole section"), m_toc.data(), &TOC::expandRecursively ); popup->addAction( i18n("Collapse whole section"), m_toc.data(), &TOC::collapseRecursively ); popup->addAction( i18n("Expand all"), m_toc.data(), &TOC::expandAll ); popup->addAction( i18n("Collapse all"), m_toc.data(), &TOC::collapseAll ); reallyShow = true; } 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::enableExitAfterPrint() { m_cliPrintAndExit = 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()); } } } const QString extraDescription = m_document->metaData( QStringLiteral("GeneratorExtraDescription") ).toString(); if (!extraDescription.isEmpty()) { aboutData.setShortDescription(aboutData.shortDescription() + QStringLiteral("\n\n") + extraDescription); } 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; default: mimeType = m_exportFormats.at( id - 1 ).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; default: 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(); 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(); } else { printConfigWidget = new DefaultPrintOptionsWidget(); } 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 ); } bool success = true; if ( printDialog->exec() ) { // set option for margins if widget is of corresponding type that holds this information PrintOptionsWidget *optionWidget = dynamic_cast(printConfigWidget); if (optionWidget != nullptr) printer.setFullPage( optionWidget->ignorePrintMargins() ); else { // printConfigurationWidget() method should always return an object of type Okular::PrintOptionsWidget, // (signature does not (yet) require it for ABI stability reasons), so emit a warning if the object is of another type qWarning() << "printConfigurationWidget() method did not return an Okular::PrintOptionsWidget. This is strongly discouraged!"; } success = doPrint( printer ); } delete printDialog; if ( m_cliPrintAndExit ) exit ( success ? EXIT_SUCCESS : EXIT_FAILURE ); } } 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 ); } } bool Part::doPrint(QPrinter &printer) { if (!m_document->isAllowed(Okular::AllowPrint)) { KMessageBox::error(widget(), i18n("Printing this document is not allowed.")); return false; } 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)); } return false; } return true; } 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 dependent) 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 -> simpler 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->setItemEnabled( m_signaturePanel, 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_signatureMessage->addAction( m_showSignaturePanel ); // 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, then choose the 'Properties' option, " "and select 'Permissions' tab in the opened window.")); 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; m_cliPrintAndExit = 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(); + const QString url = output[QStringLiteral("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 ); } void Part::enableStartWithFind(const QString &text) { m_textToFindOnOpen = QString(text); } } // namespace Okular #include "part.moc" /* kate: replace-tabs on; indent-width 4; */ diff --git a/shell/shell.cpp b/shell/shell.cpp index 19fdc311f..bc028e8d2 100644 --- a/shell/shell.cpp +++ b/shell/shell.cpp @@ -1,797 +1,797 @@ /*************************************************************************** * 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 #ifdef WITH_KACTIVITIES #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->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( "-" ) || url.scheme() == QLatin1String( "fd" ); if ( !isstdin ) { if ( openOk ) { #ifdef WITH_KACTIVITIES 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() ); int desiredTab = group.readEntry( SESSION_TAB_KEY, 0 ); while( !urls.isEmpty() ) { openUrl( QUrl(urls.takeFirst()) ); } 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 ); dlg->setFileMode( QFileDialog::ExistingFiles ); // Allow selection of more than one file 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"; + const QString dontAskAgainName = QStringLiteral("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::Yes), KGuiItem(i18n("Close Tabs"), QStringLiteral("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); const QString find = ShellUtils::find(serializedOptions); if ( ShellUtils::startInPresentation(serializedOptions) ) doc->startPresentation(); if ( ShellUtils::showPrintDialog(serializedOptions) ) QMetaObject::invokeMethod( part, "enableStartWithPrint" ); if ( ShellUtils::showPrintDialogAndExit(serializedOptions) ) QMetaObject::invokeMethod( part, "enableExitAfterPrint" ); if(!find.isEmpty()) QMetaObject::invokeMethod( part, "enableStartWithFind", Q_ARG( const QString &, find )); } 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/ui/annotwindow.cpp b/ui/annotwindow.cpp index d09a4f026..e4d94b39e 100644 --- a/ui/annotwindow.cpp +++ b/ui/annotwindow.cpp @@ -1,442 +1,442 @@ /*************************************************************************** * 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" ) ); setCursor( Qt::ArrowCursor ); } }; 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(); parentWidget()->raise(); 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 ); - setObjectName("AnnotWindow"); + setObjectName(QStringLiteral("AnnotWindow")); 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() { QColor newcolor; if ( m_annot->subType() == Okular::Annotation::AText ) { Okular::TextAnnotation * textAnn = static_cast< Okular::TextAnnotation * >( m_annot ); if ( textAnn->textType() == Okular::TextAnnotation::InPlace && textAnn->inplaceIntent() == Okular::TextAnnotation::TypeWriter ) newcolor = QColor("#fdfd96"); } if ( !newcolor.isValid() ) newcolor = m_annot->style().color().isValid() ? QColor(m_annot->style().color().red(), m_annot->style().color().green(), m_annot->style().color().blue(), 255) : 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; } } else if (e->type() == QEvent::FocusIn) { raise(); } 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) +void AnnotWindow::slotHandleContentsChangedByUndoRedo(Okular::Annotation* annot, const 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 1af2c32b3..bcd85688b 100644 --- a/ui/annotwindow.h +++ b/ui/annotwindow.h @@ -1,74 +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); + void slotHandleContentsChangedByUndoRedo( Okular::Annotation* annot, const QString &contents, int cursorPos, int anchorPos); Q_SIGNALS: void containsLatex( bool ); }; #endif diff --git a/ui/certificateviewer.cpp b/ui/certificateviewer.cpp index 979ee8792..01ba6e4e3 100644 --- a/ui/certificateviewer.cpp +++ b/ui/certificateviewer.cpp @@ -1,343 +1,343 @@ /*************************************************************************** * Copyright (C) 2018 by Chinmoy Ranjan Pradhan * * * * 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 "certificateviewer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "signatureguiutils.h" // DN (DistinguishedName) attributes can be // C Country // CN Common name // DC Domain component // E E-mail address // EMAIL E-mail address (preferred) // EMAILADDRESS E-mail address // L Locality // O Organization name // OU Organizational unit name // PC Postal code // S State or province // SN Family name // SP State or province // ST State or province (preferred) // STREET Street // T Title // CN=James Hacker, // L=Basingstoke, // O=Widget Inc, // C=GB // CN=L. Eagle, O="Sue, Grabbit and Runn", C=GB // CN=L. Eagle, O=Sue\, Grabbit and Runn, C=GB // This is a poor man's attempt at parsing DN, if it fails it is not a problem since it's only for display in a list static QString splitDNAttributes(const QStringList &text) { const QStringList attributes = { "C", "CN", "DC", "E", "EMAIL", "EMAILADDRESS", "L", "O", "OU", "PC", "S", "SN", "SP", "ST", "STREET", "T" }; for ( const QString &t : text ) { for ( const QString &attribute : attributes ) { - const QRegularExpression re( QString( "(.*),\\s*(%1=.*)" ).arg( attribute ), QRegularExpression::DotMatchesEverythingOption ); + const QRegularExpression re( QStringLiteral( "(.*),\\s*(%1=.*)" ).arg( attribute ), QRegularExpression::DotMatchesEverythingOption ); const QRegularExpressionMatch match = re.match( t ); if ( match.hasMatch() ) { QStringList results = text; const int index = results.indexOf( t ); results.removeAt( index ); results.insert( index, match.captured( 2 ) ); results.insert( index, match.captured( 1 ) ); return splitDNAttributes( results ); } } } // Clean escaped commas QStringList result = text; for ( QString &t : result ) { - t.replace( "\\,", "," ); + t.replace( QLatin1String("\\,"), QLatin1String(",") ); } // Clean up quoted attributes for ( QString &t : result ) { for ( const QString &attribute : attributes ) { - const QRegularExpression re( QString( "%1=\"(.*)\"" ).arg( attribute ) ); + const QRegularExpression re( QStringLiteral( "%1=\"(.*)\"" ).arg( attribute ) ); const QRegularExpressionMatch match = re.match( t ); if ( match.hasMatch() ) { t = attribute + "=" + match.captured( 1 ); } } } return result.join(QStringLiteral("\n")); } static QString splitDNAttributes(const QString &text) { return splitDNAttributes( QStringList{ text } ); } CertificateModel::CertificateModel( const Okular::CertificateInfo &certInfo, QObject * parent ) : QAbstractTableModel( parent ), m_certificateInfo( certInfo ) { m_certificateProperties = { Version, SerialNumber, Issuer, IssuedOn, ExpiresOn, Subject, PublicKey, KeyUsage }; } int CertificateModel::columnCount( const QModelIndex & ) const { return 2; } int CertificateModel::rowCount( const QModelIndex & ) const { return m_certificateProperties.size(); } static QString propertyVisibleName( CertificateModel::Property p ) { switch ( p ) { case CertificateModel::Version: return i18n("Version"); case CertificateModel::SerialNumber: return i18n("Serial Number"); case CertificateModel::Issuer: return i18n("Issuer"); case CertificateModel::IssuedOn: return i18n("Issued On"); case CertificateModel::ExpiresOn: return i18n("Expires On"); case CertificateModel::Subject: return i18nc("The person/company that made the signature", "Subject"); case CertificateModel::PublicKey: return i18n("Public Key"); case CertificateModel::KeyUsage: return i18n("Key Usage"); } return QString(); } static QString propertyVisibleValue( CertificateModel::Property p, const Okular::CertificateInfo &certInfo ) { switch ( p ) { case CertificateModel::Version: return i18n("V%1", QString::number( certInfo.version() )); case CertificateModel::SerialNumber: return certInfo.serialNumber().toHex(' '); case CertificateModel::Issuer: return certInfo.issuerInfo(Okular::CertificateInfo::DistinguishedName); case CertificateModel::IssuedOn: return certInfo.validityStart().toString( Qt::DefaultLocaleLongDate ); case CertificateModel::ExpiresOn: return certInfo.validityEnd().toString( Qt::DefaultLocaleLongDate ); case CertificateModel::Subject: return certInfo.subjectInfo(Okular::CertificateInfo::DistinguishedName); case CertificateModel::PublicKey: return i18n("%1 (%2 bits)", SignatureGuiUtils::getReadablePublicKeyType( certInfo.publicKeyType() ), certInfo.publicKeyStrength()); case CertificateModel::KeyUsage: return SignatureGuiUtils::getReadableKeyUsageCommaSeparated( certInfo.keyUsageExtensions() ); } return QString(); } QVariant CertificateModel::data( const QModelIndex &index, int role ) const { const int row = index.row(); if ( !index.isValid() || row < 0 || row >= m_certificateProperties.count() ) return QVariant(); switch ( role ) { case Qt::DisplayRole: case Qt::ToolTipRole: switch ( index.column() ) { case 0: return propertyVisibleName( m_certificateProperties[row] ); case 1: return propertyVisibleValue( m_certificateProperties[row], m_certificateInfo ); default: return QString(); } case PropertyKeyRole: return m_certificateProperties[row]; case PropertyVisibleValueRole: return propertyVisibleValue( m_certificateProperties[row], m_certificateInfo ); } return QVariant(); } QVariant CertificateModel::headerData( int section, Qt::Orientation orientation, int role ) const { if ( role == Qt::TextAlignmentRole ) return QVariant( Qt::AlignLeft ); if ( orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant(); switch ( section ) { case 0: return i18n("Property"); case 1: return i18n("Value"); default: return QVariant(); } } CertificateViewer::CertificateViewer( const Okular::CertificateInfo &certInfo, QWidget *parent ) : KPageDialog( parent ), m_certificateInfo( certInfo ) { setModal( true ); setMinimumSize( QSize( 500, 500 )); setFaceType( Tabbed ); setWindowTitle( i18n("Certificate Viewer") ); setStandardButtons( QDialogButtonBox::Close ); auto exportBtn = new QPushButton( i18n("Export...") ); connect( exportBtn, &QPushButton::clicked, this, &CertificateViewer::exportCertificate ); addActionButton( exportBtn ); // General tab auto generalPage = new QFrame( this ); addPage( generalPage, i18n("General") ); auto issuerBox = new QGroupBox( i18n("Issued By"), generalPage ); auto issuerFormLayout = new QFormLayout( issuerBox ); issuerFormLayout->setLabelAlignment( Qt::AlignLeft ); issuerFormLayout->addRow( i18n("Common Name(CN)"), new QLabel( m_certificateInfo.issuerInfo( Okular::CertificateInfo::CommonName ) ) ); issuerFormLayout->addRow( i18n("EMail"), new QLabel( m_certificateInfo.issuerInfo( Okular::CertificateInfo::EmailAddress ) ) ); issuerFormLayout->addRow( i18n("Organization(O)"), new QLabel( m_certificateInfo.issuerInfo( Okular::CertificateInfo::Organization ) ) ); auto subjectBox = new QGroupBox( i18n("Issued To"), generalPage ); auto subjectFormLayout = new QFormLayout( subjectBox ); subjectFormLayout->setLabelAlignment( Qt::AlignLeft ); subjectFormLayout->addRow( i18n("Common Name(CN)"), new QLabel( m_certificateInfo.subjectInfo( Okular::CertificateInfo::CommonName ) ) ); subjectFormLayout->addRow( i18n("EMail"), new QLabel( m_certificateInfo.subjectInfo( Okular::CertificateInfo::EmailAddress ) ) ); subjectFormLayout->addRow( i18n("Organization(O)"), new QLabel( m_certificateInfo.subjectInfo( Okular::CertificateInfo::Organization ) ) ); auto validityBox = new QGroupBox( i18n("Validity"), generalPage ); auto validityFormLayout = new QFormLayout( validityBox ); validityFormLayout->setLabelAlignment( Qt::AlignLeft ); validityFormLayout->addRow( i18n("Issued On"), new QLabel( m_certificateInfo.validityStart().toString( Qt::DefaultLocaleLongDate ) ) ); validityFormLayout->addRow( i18n("Expires On"), new QLabel( m_certificateInfo.validityEnd().toString( Qt::DefaultLocaleLongDate ) ) ); auto fingerprintBox = new QGroupBox( i18n("Fingerprints"), generalPage ); auto fingerprintFormLayout = new QFormLayout( fingerprintBox ); fingerprintFormLayout->setLabelAlignment( Qt::AlignLeft ); QByteArray certData = m_certificateInfo.certificateData(); auto sha1Label = new QLabel( QString( QCryptographicHash::hash( certData, QCryptographicHash::Sha1 ).toHex(' ') ) ); sha1Label->setWordWrap( true ); auto sha256Label = new QLabel( QString( QCryptographicHash::hash( certData, QCryptographicHash::Sha256 ).toHex(' ') ) ); sha256Label->setWordWrap( true ); fingerprintFormLayout->addRow( i18n("SHA-1 Fingerprint"), sha1Label ); fingerprintFormLayout->addRow( i18n("SHA-256 Fingerprint"), sha256Label ); auto generalPageLayout = new QVBoxLayout( generalPage ); generalPageLayout->addWidget( issuerBox ); generalPageLayout->addWidget( subjectBox ); generalPageLayout->addWidget( validityBox ); generalPageLayout->addWidget( fingerprintBox ); generalPageLayout->addStretch(); //force column 1 to have same width auto resizer = new KColumnResizer( this ); resizer->addWidgetsFromLayout( issuerBox->layout(), 0 ); resizer->addWidgetsFromLayout( subjectBox->layout(), 0 ); resizer->addWidgetsFromLayout( validityBox->layout(), 0 ); resizer->addWidgetsFromLayout( fingerprintBox->layout(), 0 ); // Details tab auto detailsFrame = new QFrame( this ); addPage( detailsFrame, i18n("Details") ); auto certDataLabel = new QLabel( i18n("Certificate Data:") ); auto certTree = new QTreeView( this ); certTree->setIndentation( 0 ); m_certificateModel = new CertificateModel( m_certificateInfo, this ); certTree->setModel( m_certificateModel ); connect( certTree->selectionModel(), &QItemSelectionModel::currentChanged, this, &CertificateViewer::updateText ); m_propertyText = new QTextEdit( this ); m_propertyText->setReadOnly( true ); auto detailsPageLayout = new QVBoxLayout( detailsFrame ); detailsPageLayout->addWidget( certDataLabel ); detailsPageLayout->addWidget( certTree ); detailsPageLayout->addWidget( m_propertyText ); } void CertificateViewer::updateText( const QModelIndex &index ) { QString text; const CertificateModel::Property key = m_certificateModel->data( index, CertificateModel::PropertyKeyRole ).value(); switch ( key ) { case CertificateModel::SerialNumber: case CertificateModel::Version: case CertificateModel::IssuedOn: case CertificateModel::ExpiresOn: text = m_certificateModel->data( index, CertificateModel::PropertyVisibleValueRole ).toString(); break; case CertificateModel::Issuer: case CertificateModel::Subject: text = splitDNAttributes( m_certificateModel->data( index, CertificateModel::PropertyVisibleValueRole ).toString() ); break; case CertificateModel::PublicKey: text = m_certificateInfo.publicKey().toHex(' '); break; case CertificateModel::KeyUsage: text = SignatureGuiUtils::getReadableKeyUsageNewLineSeparated( m_certificateInfo.keyUsageExtensions() ); break; } m_propertyText->setText( text ); } void CertificateViewer::exportCertificate() { const QString caption = i18n("Where do you want to save this certificate?"); const QString path = QFileDialog::getSaveFileName( this, caption, QStringLiteral("Certificate.cer"), i18n("Certificate File (*.cer)") ); if ( !path.isEmpty() ) { QFile targetFile( path ); targetFile.open( QIODevice::WriteOnly ); if ( targetFile.write( m_certificateInfo.certificateData() ) == -1 ) { KMessageBox::error( this, i18n("Unable to export certificate!") ); } targetFile.close(); } } diff --git a/ui/embeddedfilesdialog.cpp b/ui/embeddedfilesdialog.cpp index ea162b957..75ccb23e8 100644 --- a/ui/embeddedfilesdialog.cpp +++ b/ui/embeddedfilesdialog.cpp @@ -1,205 +1,205 @@ /*************************************************************************** * Copyright (C) 2006 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. * ***************************************************************************/ #include "embeddedfilesdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core/document.h" #include "guiutils.h" Q_DECLARE_METATYPE( Okular::EmbeddedFile* ) static const int EmbeddedFileRole = Qt::UserRole + 100; static QString dateToString( const QDateTime & date ) { return date.isValid() ? QLocale().toString( date, QLocale::LongFormat ) : i18nc( "Unknown date", "Unknown" ); } EmbeddedFilesDialog::EmbeddedFilesDialog(QWidget *parent, const Okular::Document *document) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Embedded Files")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); mUser1Button = new QPushButton; buttonBox->addButton(mUser1Button, QDialogButtonBox::ActionRole); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); buttonBox->button(QDialogButtonBox::Close)->setDefault(true); KGuiItem::assign(mUser1Button, KStandardGuiItem::save()); mUser1Button->setEnabled(false); mUser2Button = new QPushButton; buttonBox->addButton(mUser2Button, QDialogButtonBox::ActionRole); - KGuiItem::assign(mUser2Button, KGuiItem(i18nc("@action:button", "View"), "document-open")); + KGuiItem::assign(mUser2Button, KGuiItem(i18nc("@action:button", "View"), QStringLiteral("document-open"))); mUser2Button->setEnabled(false); m_tw = new QTreeWidget(this); mainLayout->addWidget(m_tw); mainLayout->addWidget(buttonBox); QStringList header; header.append(i18nc("@title:column", "Name")); header.append(i18nc("@title:column", "Description")); header.append(i18nc("@title:column", "Size")); header.append(i18nc("@title:column", "Created")); header.append(i18nc("@title:column", "Modified")); m_tw->setHeaderLabels(header); m_tw->setRootIsDecorated(false); m_tw->setSelectionMode(QAbstractItemView::ExtendedSelection); m_tw->setContextMenuPolicy(Qt::CustomContextMenu); foreach(Okular::EmbeddedFile* ef, *document->embeddedFiles()) { QTreeWidgetItem *twi = new QTreeWidgetItem(); twi->setText(0, ef->name()); QMimeDatabase db; QMimeType mime = db.mimeTypeForFile( ef->name(), QMimeDatabase::MatchExtension); if (mime.isValid()) { twi->setIcon(0, QIcon::fromTheme(mime.iconName())); } twi->setText(1, ef->description()); twi->setText(2, ef->size() <= 0 ? i18nc("Not available size", "N/A") : KFormat().formatByteSize(ef->size())); twi->setText(3, dateToString( ef->creationDate() ) ); twi->setText(4, dateToString( ef->modificationDate() ) ); twi->setData( 0, EmbeddedFileRole, qVariantFromValue( ef ) ); m_tw->addTopLevelItem(twi); } // Having filled the columns, it is nice to resize them to be able to read the contents for (int lv = 0; lv < m_tw->columnCount(); ++lv) { m_tw->resizeColumnToContents(lv); } // This is a bit dubious, but I'm not seeing a nice way to say "expand to fit contents" m_tw->setMinimumWidth(640); m_tw->updateGeometry(); connect(mUser1Button, SIGNAL(clicked()), this, SLOT(saveFile())); connect(mUser2Button, SIGNAL(clicked()), this, SLOT(viewFile())); connect(m_tw, &QWidget::customContextMenuRequested, this, &EmbeddedFilesDialog::attachViewContextMenu); connect(m_tw, &QTreeWidget::itemSelectionChanged, this, &EmbeddedFilesDialog::updateSaveButton); connect(m_tw, &QTreeWidget::itemDoubleClicked, this, &EmbeddedFilesDialog::viewFileItem); } void EmbeddedFilesDialog::updateSaveButton() { bool enable = (m_tw->selectedItems().count() > 0); mUser1Button->setEnabled(enable); mUser2Button->setEnabled(enable); } void EmbeddedFilesDialog::saveFile() { QList selected = m_tw->selectedItems(); foreach(QTreeWidgetItem *twi, selected) { Okular::EmbeddedFile* ef = qvariant_cast< Okular::EmbeddedFile* >( twi->data( 0, EmbeddedFileRole ) ); saveFile(ef); } } void EmbeddedFilesDialog::viewFile() { QList selected = m_tw->selectedItems(); foreach(QTreeWidgetItem *twi, selected) { Okular::EmbeddedFile* ef = qvariant_cast< Okular::EmbeddedFile* >( twi->data( 0, EmbeddedFileRole ) ); viewFile( ef ); } } void EmbeddedFilesDialog::viewFileItem( QTreeWidgetItem* item, int /*column*/ ) { Okular::EmbeddedFile* ef = qvariant_cast< Okular::EmbeddedFile* >( item->data( 0, EmbeddedFileRole ) ); viewFile( ef ); } void EmbeddedFilesDialog::attachViewContextMenu( const QPoint& /*pos*/ ) { QList selected = m_tw->selectedItems(); if ( selected.isEmpty() ) return; if ( selected.size() > 1 ) return; QMenu menu( this ); QAction* saveAsAct = menu.addAction( QIcon::fromTheme( QStringLiteral("document-save-as") ), i18nc( "@action:inmenu", "&Save As..." ) ); QAction* viewAct = menu.addAction( QIcon::fromTheme( QStringLiteral("document-open" ) ), i18nc( "@action:inmenu", "&View..." ) ); QAction* act = menu.exec( QCursor::pos() ); if ( !act ) return; Okular::EmbeddedFile* ef = qvariant_cast< Okular::EmbeddedFile* >( selected.at( 0 )->data( 0, EmbeddedFileRole ) ); if ( act == saveAsAct ) { saveFile( ef ); } else if ( act == viewAct ) { viewFile( ef ); } } void EmbeddedFilesDialog::viewFile( Okular::EmbeddedFile* ef ) { // get name and extension QFileInfo fileInfo(ef->name()); // save in temporary directory with a unique name resembling the attachment name, // using QTemporaryFile's XXXXXX placeholder QTemporaryFile *tmpFile = new QTemporaryFile( QDir::tempPath() + QDir::separator() + fileInfo.baseName() + ".XXXXXX" - + (fileInfo.completeSuffix().isEmpty() ? QString("") : "." + fileInfo.completeSuffix()) // krazy:exclude=doublequote_chars + + (fileInfo.completeSuffix().isEmpty() ? QStringLiteral("") : "." + fileInfo.completeSuffix()) // krazy:exclude=doublequote_chars ); GuiUtils::writeEmbeddedFile( ef, this, *tmpFile ); // set readonly to prevent the viewer application from modifying it tmpFile->setPermissions( QFile::ReadOwner ); // keep temporary file alive while the dialog is open m_openedFiles.push_back( QSharedPointer< QTemporaryFile >( tmpFile ) ); // view the temporary file with the default application new KRun( QUrl( "file://" + tmpFile->fileName() ), this ); } void EmbeddedFilesDialog::saveFile( Okular::EmbeddedFile* ef ) { GuiUtils::saveEmbeddedFile( ef, this ); } #include "moc_embeddedfilesdialog.cpp" diff --git a/ui/fileprinterpreview.cpp b/ui/fileprinterpreview.cpp index ab186e4d0..e609d9912 100644 --- a/ui/fileprinterpreview.cpp +++ b/ui/fileprinterpreview.cpp @@ -1,185 +1,185 @@ /*************************************************************************** * Copyright (C) 2007 by John Layt * * * * FilePrinterPreview based on KPrintPreview (originally LGPL) * * Copyright (c) 2007 Alex Merry * * * * 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 "fileprinterpreview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "debug_ui.h" using namespace Okular; // This code copied from KPrintPreview by Alex Merry, adapted to do PS files instead of PDF class Okular::FilePrinterPreviewPrivate { public: FilePrinterPreviewPrivate( FilePrinterPreview *host, const QString & _filename ) : q(host) , mainWidget(new QWidget(host)) , previewPart(nullptr) , failMessage(nullptr) , config(KSharedConfig::openConfig(QStringLiteral("okularrc"))) { mainlayout = new QVBoxLayout(q); buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, q); mainlayout->addWidget(buttonBox); filename = _filename; } void getPart(); bool doPreview(); void fail(); FilePrinterPreview *q; QWidget *mainWidget; QDialogButtonBox *buttonBox; QVBoxLayout *mainlayout; QString filename; KParts::ReadOnlyPart *previewPart; QWidget *failMessage; KSharedConfig::Ptr config; }; void FilePrinterPreviewPrivate::getPart() { if (previewPart) { qCDebug(OkularUiDebug) << "already got a part"; return; } qCDebug(OkularUiDebug) << "querying trader for application/ps service"; KPluginFactory *factory(nullptr); KService::List offers; if (filename.endsWith(QStringLiteral(".ps"))) { /* Explicitly look for the Okular/Ghostview part: no other PostScript parts are available now; other parts which handles text are not suitable here (PostScript source code) */ offers = KMimeTypeTrader::self()->query(QStringLiteral("application/postscript"), QStringLiteral("KParts/ReadOnlyPart"), QStringLiteral("[DesktopEntryName] == 'okularghostview'")); } else { - offers = KMimeTypeTrader::self()->query("application/pdf", "KParts/ReadOnlyPart"); + offers = KMimeTypeTrader::self()->query(QStringLiteral("application/pdf"), QStringLiteral("KParts/ReadOnlyPart")); } KService::List::ConstIterator it = offers.constBegin(); while (!factory && it != offers.constEnd()) { KPluginLoader loader(**it); factory = loader.factory(); if (!factory) { qCDebug(OkularUiDebug) << "Loading failed:" << loader.errorString(); } ++it; } if (factory) { qCDebug(OkularUiDebug) << "Trying to create a part"; previewPart = factory->create(q, (QVariantList() << QStringLiteral("Print/Preview"))); if (!previewPart) { qCDebug(OkularUiDebug) << "Part creation failed"; } } } bool FilePrinterPreviewPrivate::doPreview() { if (!QFile::exists(filename)) { qCWarning(OkularUiDebug) << "Nothing was produced to be previewed"; return false; } getPart(); if (!previewPart) { //TODO: error dialog qCWarning(OkularUiDebug) << "Could not find a PS viewer for the preview dialog"; fail(); return false; } else { mainlayout->insertWidget(0, previewPart->widget()); return previewPart->openUrl(QUrl::fromLocalFile(filename)); } } void FilePrinterPreviewPrivate::fail() { if (!failMessage) { failMessage = new QLabel(i18n("Could not load print preview part"), q); } mainlayout->insertWidget(0, failMessage); } FilePrinterPreview::FilePrinterPreview( const QString &filename, QWidget *parent ) : QDialog( parent ) , d( new FilePrinterPreviewPrivate( this, filename ) ) { qCDebug(OkularUiDebug) << "kdeprint: creating preview dialog"; // Set up the dialog setWindowTitle(i18n("Print Preview")); connect(d->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); KWindowConfig::restoreWindowSize(windowHandle(), d->config->group("Print Preview")); } FilePrinterPreview::~FilePrinterPreview() { KConfigGroup group(d->config->group("Print Preview")); KWindowConfig::saveWindowSize(windowHandle(), group); delete d; } QSize FilePrinterPreview::sizeHint() const { // return a more or less useful window size, if not saved already return QSize(600, 500); } void FilePrinterPreview::showEvent(QShowEvent *event) { if (!event->spontaneous()) { // being shown for the first time if (!d->doPreview()) { event->accept(); return; } } QDialog::showEvent(event); } #include "moc_fileprinterpreview.cpp" diff --git a/ui/pageview.cpp b/ui/pageview.cpp index 254a3bfa1..04ab4c72c 100644 --- a/ui/pageview.cpp +++ b/ui/pageview.cpp @@ -1,5583 +1,5583 @@ /*************************************************************************** * 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; QPoint previousMouseMovePos; 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 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; QAction * aZoomActual; 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" ) ) + , Okular::View( QStringLiteral( "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 ); // connect 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 QSet< AnnotWindow * > annowindows = d->m_annowindows; d->m_annowindows.clear(); qDeleteAll( annowindows ); // delete all widgets qDeleteAll( d->items ); 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 ); d->aZoomActual = KStandardAction::actualSize( this, &PageView::slotZoomActual, ac ); d->aZoomActual->setText(i18n("Zoom to 100%")); } 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(QIcon::fromTheme( QStringLiteral("trim-margins") ), 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(QIcon::fromTheme( QStringLiteral("trim-to-selection") ), 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; foreach(AnnotWindow *aw, d->m_annowindows) { 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 << existWindow; } else { existWindow->raise(); existWindow->findChild()->setFocus(); } existWindow->show(); } void PageView::slotAnnotationWindowDestroyed( QObject * window ) { 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 dependent) 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(); std::sort(selpages.begin(), selpages.end()); 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() { for ( const PageViewItem * item : qAsConst( d->items ) ) { Okular::RegularAreaRect * area = textSelectionForItem( item ); d->pagesWithTextSelection.insert( item->pageNumber() ); d->document->setPageTextSelection( item->pageNumber(), area, palette().color( QPalette::Active, QPalette::Highlight ) ); } } void PageView::createAnnotationsVideoWidgets(PageViewItem *item, const QLinkedList< Okular::Annotation * > &annotations) { qDeleteAll( item->videoWidgets() ); item->videoWidgets().clear(); for ( Okular::Annotation * a : annotations ) { if ( a->subType() == Okular::Annotation::AMovie ) { Okular::MovieAnnotation * movieAnn = static_cast< Okular::MovieAnnotation * >( a ); VideoWidget * vw = new VideoWidget( movieAnn, movieAnn->movie(), 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) qDeleteAll( d->items ); 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 for ( const Okular::Page * page : pageSet ) { PageViewItem * item = new PageViewItem( page ); 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 = page->formFields(); for ( Okular::FormField * ff : pageFields ) { FormWidgetIface * w = FormWidgetFactory::createWidget( ff, viewport() ); if ( w ) { w->setPageItem( item ); w->setFormWidgetsController( d->formWidgetsController() ); w->setVisibility( false ); w->setCanBeFilled( allowfillforms ); item->formWidgets().insert( w ); hasformwidgets = true; } } createAnnotationsVideoWidgets( item, page->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 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->aZoomActual ) d->aZoomActual->setEnabled( haspages && d->zoomFactor != 1.0 ); 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 notify if ( d->blockViewport ) return; // block setViewport outgoing calls d->blockViewport = true; // find PageViewItem matching the viewport description const Okular::DocumentViewport & vp = d->document->viewport(); const PageViewItem * item = nullptr; for ( const PageViewItem * tmpItem : qAsConst( d->items ) ) if ( tmpItem->pageNumber() == vp.pageNumber ) { item = tmpItem; 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 QPoint centerCoord = viewportToContentArea( vp ); // if smooth movement requested, setup parameters and start it if ( smoothMove ) { d->viewportMoveActive = true; d->viewportMoveTime.start(); d->viewportMoveDest.setX( centerCoord.x() ); d->viewportMoveDest.setY( centerCoord.y() ); 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( centerCoord.x(), centerCoord.y() ); 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(); QSet< AnnotWindow * >::Iterator it = d->m_annowindows.begin(); for ( ; it != d->m_annowindows.end(); ) { 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; } } d->mouseAnnotation->notifyAnnotationChanged( pageNumber ); } 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 for ( const PageViewItem * visibleItem : qAsConst( d->visibleItems ) ) if ( visibleItem->pageNumber() == pageNumber && visibleItem->isVisible() ) { // update item's rectangle plus the little outline QRect expandedRect = visibleItem->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 for ( const PageViewItem * visibleItem : qAsConst( d->visibleItems ) ) if ( visibleItem->pageNumber() == pageNumber ) return false; } else { // forbid unloading of the visible items, and of the previous and next for ( const PageViewItem * visibleItem : qAsConst( d->visibleItems ) ) if ( abs( visibleItem->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()) * devicePixelRatioF(), (blendRect.top() - contentsRect.top()) * devicePixelRatioF(), blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF() ); QColor blCol = selBlendColor.darker( 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()) * devicePixelRatioF(), (blendRect.top() - contentsRect.top()) * devicePixelRatioF(), blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF() ); QColor blCol = d->mouseSelectionColor.darker( 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 ).darker(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 ).darker(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.darker() ); 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 ) { // For some reason in Qt 5.11.2 (no idea when this started) all wheel // events are followed by mouse move events (without changing position), // so we only actually reset the controlWheelAccumulatedDelta if there is a mouse movement if ( e->globalPos() != d->previousMouseMovePos ) { d->controlWheelAccumulatedDelta = 0; } d->previousMouseMovePos = e->globalPos(); // 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 ).lighter( 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 supports 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 dependent 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 ).lighter( 120 ), false ); } break; case Okular::Settings::EnumMouseMode::TableSelect: if ( leftButton ) { if (d->tableSelectionParts.isEmpty()) { selectionStart( eventPos, palette().color( QPalette::Active, QPalette::Highlight ).lighter( 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); std::sort(d->tableSelectionCols.begin(), d->tableSelectionCols.end()); } } 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); std::sort(d->tableSelectionRows.begin(), d->tableSelectionRows.end()); } } } 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 independent mid bottom 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; for ( const PageViewItem * item : qAsConst( d->items ) ) { 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"); + menu.setObjectName(QStringLiteral("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"); + textToClipboard->setObjectName(QStringLiteral("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; for ( PageViewItem * item : qAsConst( d->items ) ) { 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"); + httpLink->setObjectName(QStringLiteral("GoToAction")); } } } if ( menu ) { - menu->setObjectName("PopupMenu"); + menu->setObjectName(QStringLiteral("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; std::sort(colSelectionTicks.begin(), colSelectionTicks.end()); std::sort(rowSelectionTicks.begin(), rowSelectionTicks.end()); 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 ); std::sort(colTicks.begin(), colTicks.end()); std::sort(rowTicks.begin(), rowTicks.end()); 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 // Show tool tips only for those modes that change the cursor // to a hand when hovering over the link. && ( d->mouseMode == Okular::Settings::EnumMouseMode::Browse || d->mouseMode == Okular::Settings::EnumMouseMode::RectSelect || d->mouseMode == Okular::Settings::EnumMouseMode::TextSelect || d->mouseMode == Okular::Settings::EnumMouseMode::TrimSelect ) ) { 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 for ( const PageViewItem * item : qAsConst( d->items ) ) { // check if a piece of the page intersects the contents rect if ( !item->isVisible() || !item->croppedGeometry().intersects( checkRect ) ) continue; // get item and item's outline geometries 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; for ( PageViewItem * i : qAsConst( d->visibleItems ) ) { 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() ) { for ( const int page : qAsConst( d->pagesWithTextSelection ) ) d->document->setPageTextSelection( page, 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 ) { // this number slows the speed of the page by its value, chosen not to be too fast or too slow, the actual speed is determined from the mouse position, not critical const int damping=6; if (pos.x() < horizontalScrollBar()->value()) d->dragScrollVector.setX((pos.x() - horizontalScrollBar()->value())/damping); else if (horizontalScrollBar()->value() + viewport()->width() < pos.x()) d->dragScrollVector.setX((pos.x() - horizontalScrollBar()->value() - viewport()->width())/damping); else d->dragScrollVector.setX(0); if (pos.y() < verticalScrollBar()->value()) d->dragScrollVector.setY((pos.y() - verticalScrollBar()->value())/damping); else if (verticalScrollBar()->value() + viewport()->height() < pos.y()) d->dragScrollVector.setY((pos.y() - verticalScrollBar()->value() - viewport()->height())/damping); else d->dragScrollVector.setY(0); if (d->dragScrollVector != QPoint(0, 0)) { if (!d->dragScrollTimer.isActive()) d->dragScrollTimer.start(1000/60); //60 fps } else d->dragScrollTimer.stop(); } QPoint PageView::viewportToContentArea( const Okular::DocumentViewport & vp ) const { Q_ASSERT( vp.pageNumber >= 0 ); const QRect & r = d->items[ vp.pageNumber ]->croppedGeometry(); QPoint c { r.left(), r.top() }; if ( vp.rePos.enabled ) { if ( vp.rePos.pos == Okular::DocumentViewport::Center ) { c.rx() += qRound( normClamp( vp.rePos.normalizedX, 0.5 ) * (double)r.width() ); c.ry() += qRound( normClamp( vp.rePos.normalizedY, 0.0 ) * (double)r.height() ); } else { // TopLeft c.rx() += qRound( normClamp( vp.rePos.normalizedX, 0.0 ) * (double)r.width() + viewport()->width() / 2 ); c.ry() += qRound( normClamp( vp.rePos.normalizedY, 0.0 ) * (double)r.height() + viewport()->height() / 2 ); } } else { // exact repositioning disabled, align page top margin with viewport top border by default c.rx() += r.width() / 2; c.ry() += viewport()->height() / 2 - 10; } return c; } 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( const 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 opening 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; std::sort(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 ZoomActual: newZoomMode = ZoomFixed; newFactor = 1.0; 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 ); d->aZoomActual->setEnabled( d->zoomFactor != 1.0 ); } 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() { if( d->m_formsVisible ) { for ( PageViewItem * item : qAsConst( d->visibleItems ) ) { item->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; for ( PageViewItem * item : qAsConst( d->items ) ) { const bool hadfocus = item->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"); + processLink->setObjectName(QStringLiteral("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"); + actCopyLinkLocation->setObjectName(QStringLiteral("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 ); } int viewportWidth = viewport()->width(), viewportHeight = viewport()->height(), fullWidth = 0, fullHeight = 0; // 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 ( PageViewItem * item : qAsConst( d->items ) ) { // 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 ( PageViewItem * item : qAsConst( d->items ) ) { 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 QPoint centerPos = viewportToContentArea( vp ); center( centerPos.x(), centerPos.y() ); // 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; for ( PageViewItem * i : qAsConst( d->items ) ) { 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::slotZoomActual() { updateZoom( ZoomActual ); } 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; for ( const PageViewItem * item : qAsConst( d->items ) ) { Okular::RegularAreaRect * area = textSelectionForItem( item ); text.append( item->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::slotSetChangeColors(bool active) { Okular::SettingsCore::setChangeColors(active); Okular::Settings::self()->save(); viewport()->update(); } void PageView::slotToggleChangeColors() { slotSetChangeColors( !Okular::SettingsCore::changeColors() ); } void PageView::slotFitWindowToPage() { const PageViewItem *currentPageItem = nullptr; QSize viewportSize = viewport()->size(); foreach ( const PageViewItem * pageItem, d->items ) { if ( pageItem->isVisible() ) { currentPageItem = pageItem; break; } } if ( !currentPageItem ) return; 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 ); } void PageView::slotSelectPage() { textSelectionClear(); const int currentPage = d->document->viewport().pageNumber; PageViewItem *item = d->items.at( currentPage ); if ( item ) { Okular::RegularAreaRect * area = textSelectionForItem( item ); const QString text = item->page()->text( area ); d->pagesWithTextSelection.insert( currentPage ); d->document->setPageTextSelection( currentPage, area, palette().color( QPalette::Active, QPalette::Highlight ) ); } } void PageView::highlightSignatureFormWidget( const Okular::FormFieldSignature *form ) { QVector< PageViewItem * >::const_iterator dIt = d->items.constBegin(), dEnd = d->items.constEnd(); for ( ; dIt != dEnd; ++dIt ) { foreach ( auto fw, (*dIt)->formWidgets() ) { if ( fw->formField() == form ) { SignatureEdit *widget = static_cast< SignatureEdit * >( fw ); widget->setDummyMode( true ); QTimer::singleShot( 250, this, [=]{ widget->setDummyMode( false ); }); return; } } } } //END private SLOTS #include "moc_pageview.cpp" /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/pageviewannotator.cpp b/ui/pageviewannotator.cpp index c51f734b8..5e353d7e3 100644 --- a/ui/pageviewannotator.cpp +++ b/ui/pageviewannotator.cpp @@ -1,1283 +1,1283 @@ /*************************************************************************** * Copyright (C) 2005 by Enrico Ros * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "pageviewannotator.h" // qt / kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // system includes #include #include #include // local includes #include "core/area.h" #include "core/document.h" #include "core/page.h" #include "core/annotations.h" #include "settings.h" #include "annotationtools.h" #include "guiutils.h" #include "pageview.h" #include "debug_ui.h" /** @short PickPointEngine */ class PickPointEngine : public AnnotatorEngine { public: PickPointEngine( const QDomElement & engineElement ) : AnnotatorEngine( engineElement ), clicked( false ), xscale( 1.0 ), yscale( 1.0 ) { // parse engine specific attributes hoverIconName = engineElement.attribute( QStringLiteral("hoverIcon") ); iconName = m_annotElement.attribute( QStringLiteral("icon") ); if ( m_annotElement.attribute( QStringLiteral("type") ) == QLatin1String("Stamp") && !iconName.simplified().isEmpty() ) hoverIconName = iconName; center = QVariant( engineElement.attribute( QStringLiteral("center") ) ).toBool(); bool ok = true; size = engineElement.attribute( QStringLiteral("size"), QStringLiteral("32") ).toInt( &ok ); if ( !ok ) size = 32; m_block = QVariant( engineElement.attribute( QStringLiteral("block") ) ).toBool(); // create engine objects if ( !hoverIconName.simplified().isEmpty() ) pixmap = GuiUtils::loadStamp( hoverIconName, QSize( size, size ) ); } QRect event( EventType type, Button button, double nX, double nY, double xScale, double yScale, const Okular::Page * page ) override { xscale=xScale; yscale=yScale; pagewidth = page->width(); pageheight = page->height(); // only proceed if pressing left button if ( button != Left ) return QRect(); // start operation on click if ( type == Press && clicked == false ) { clicked = true; startpoint.x=nX; startpoint.y=nY; } // repaint if moving while pressing else if ( type == Move && clicked == true ) { } // operation finished on release else if ( type == Release && clicked == true ) { m_creationCompleted = true; } else return QRect(); // update variables and extents (zoom invariant rect) point.x = nX; point.y = nY; if ( center ) { rect.left = nX - ( size / ( xScale * 2.0 ) ); rect.top = nY - ( size / ( yScale * 2.0 ) ); } else { rect.left = nX; rect.top = nY; } rect.right = rect.left + size; rect.bottom = rect.top + size; QRect boundrect = rect.geometry( (int)xScale, (int)yScale ).adjusted( 0, 0, 1, 1 ); if ( m_block ) { const Okular::NormalizedRect tmprect( qMin( startpoint.x, point.x ), qMin( startpoint.y, point.y ), qMax( startpoint.x, point.x ), qMax( startpoint.y, point.y ) ); boundrect |= tmprect.geometry( (int)xScale, (int)yScale ).adjusted( 0, 0, 1, 1 ); } return boundrect; } void paint( QPainter * painter, double xScale, double yScale, const QRect & /*clipRect*/ ) override { if ( clicked ) { if ( m_block ) { const QPen origpen = painter->pen(); QPen pen = painter->pen(); pen.setStyle( Qt::DashLine ); painter->setPen( pen ); const Okular::NormalizedRect tmprect( qMin( startpoint.x, point.x ), qMin( startpoint.y, point.y ), qMax( startpoint.x, point.x ), qMax( startpoint.y, point.y ) ); const QRect realrect = tmprect.geometry( (int)xScale, (int)yScale ); painter->drawRect( realrect ); painter->setPen( origpen ); } if ( !pixmap.isNull() ) painter->drawPixmap( QPointF( rect.left * xScale, rect.top * yScale ), pixmap ); } } - void addInPlaceTextAnnotation( Okular::Annotation * &ann, const QString summary, const QString content, Okular::TextAnnotation::InplaceIntent inplaceIntent ) + void addInPlaceTextAnnotation( Okular::Annotation * &ann, const QString &summary, const QString &content, Okular::TextAnnotation::InplaceIntent inplaceIntent ) { Okular::TextAnnotation * ta = new Okular::TextAnnotation(); ann = ta; ta->setFlags( ta->flags() | Okular::Annotation::FixedRotation ); ta->setContents( content ); ta->setTextType( Okular::TextAnnotation::InPlace ); ta->setInplaceIntent( inplaceIntent ); //set alignment if ( m_annotElement.hasAttribute( QStringLiteral("align") ) ) ta->setInplaceAlignment( m_annotElement.attribute( QStringLiteral("align") ).toInt() ); //set font if ( m_annotElement.hasAttribute( QStringLiteral("font") ) ) { QFont f; f.fromString( m_annotElement.attribute( QStringLiteral("font") ) ); ta->setTextFont( f ); } // set font color if ( m_annotElement.hasAttribute( QStringLiteral("textColor") ) ) { if ( inplaceIntent == Okular::TextAnnotation::TypeWriter ) ta->setTextColor( m_annotElement.attribute( QStringLiteral("textColor") ) ); else ta->setTextColor( Qt::black ); } //set width if ( m_annotElement.hasAttribute( QStringLiteral ( "width" ) ) ) { ta->style().setWidth( m_annotElement.attribute( QStringLiteral ( "width" ) ).toDouble() ); } //set boundary rect.left = qMin(startpoint.x,point.x); rect.top = qMin(startpoint.y,point.y); rect.right = qMax(startpoint.x,point.x); rect.bottom = qMax(startpoint.y,point.y); qCDebug(OkularUiDebug).nospace() << "xyScale=" << xscale << "," << yscale; static const int padding = 2; const QFontMetricsF mf(ta->textFont()); const QRectF rcf = mf.boundingRect( Okular::NormalizedRect( rect.left, rect.top, 1.0, 1.0 ).geometry( (int)pagewidth, (int)pageheight ).adjusted( padding, padding, -padding, -padding ), Qt::AlignTop | Qt::AlignLeft | Qt::TextWordWrap, ta->contents() ); rect.right = qMax(rect.right, rect.left+(rcf.width()+padding*2)/pagewidth); rect.bottom = qMax(rect.bottom, rect.top+(rcf.height()+padding*2)/pageheight); ta->window().setSummary( summary ); } QList< Okular::Annotation* > end() override { // find out annotation's description node if ( m_annotElement.isNull() ) { m_creationCompleted = false; clicked = false; return QList< Okular::Annotation* >(); } // find out annotation's type Okular::Annotation * ann = nullptr; const QString typeString = m_annotElement.attribute( QStringLiteral("type") ); // create InPlace TextAnnotation from path if ( typeString == QLatin1String("FreeText") ) { bool resok; const QString content = QInputDialog::getMultiLineText(nullptr, i18n( "New Text Note" ), i18n( "Text of the new note:" ), QString(), &resok); if( resok ) addInPlaceTextAnnotation( ann, i18n("Inline Note"), content, Okular::TextAnnotation::Unknown ); } else if ( typeString == QLatin1String("Typewriter") ) { bool resok; const QString content = QInputDialog::getMultiLineText(nullptr, i18n( "New Text Note" ), i18n( "Text of the new note:" ), QString(), &resok); if( resok ) addInPlaceTextAnnotation( ann, i18n("Typewriter"), content, Okular::TextAnnotation::TypeWriter ); } else if ( typeString == QLatin1String("Text") ) { Okular::TextAnnotation * ta = new Okular::TextAnnotation(); ann = ta; ta->setTextType( Okular::TextAnnotation::Linked ); ta->setTextIcon( iconName ); //ta->window.flags &= ~(Okular::Annotation::Hidden); const double iconhei=0.03; rect.left = point.x; rect.top = point.y; rect.right=rect.left+iconhei; rect.bottom=rect.top+iconhei*xscale/yscale; ta->window().setSummary( i18n( "Pop-up Note" ) ); } // create StampAnnotation from path else if ( typeString == QLatin1String("Stamp") ) { Okular::StampAnnotation * sa = new Okular::StampAnnotation(); ann = sa; sa->setStampIconName( iconName ); // set boundary rect.left = qMin( startpoint.x, point.x ); rect.top = qMin( startpoint.y, point.y ); rect.right = qMax( startpoint.x, point.x ); rect.bottom = qMax( startpoint.y, point.y ); const QRectF rcf = rect.geometry( (int)xscale, (int)yscale ); const int ml = ( rcf.bottomRight() - rcf.topLeft() ).toPoint().manhattanLength(); if ( ml <= QApplication::startDragDistance() ) { const double stampxscale = size / xscale; const double stampyscale = size / yscale; if ( center ) { rect.left = point.x - stampxscale / 2; rect.top = point.y - stampyscale / 2; } else { rect.left = point.x; rect.top = point.y; } rect.right = rect.left + stampxscale; rect.bottom = rect.top + stampyscale; } } // create GeomAnnotation else if ( typeString == QLatin1String("GeomSquare") || typeString == QLatin1String("GeomCircle") ) { Okular::GeomAnnotation * ga = new Okular::GeomAnnotation(); ann = ga; // set the type if ( typeString == QLatin1String("GeomSquare") ) ga->setGeometricalType( Okular::GeomAnnotation::InscribedSquare ); else ga->setGeometricalType( Okular::GeomAnnotation::InscribedCircle ); if ( m_annotElement.hasAttribute( QStringLiteral("width") ) ) ann->style().setWidth( m_annotElement.attribute( QStringLiteral("width") ).toDouble() ); if ( m_annotElement.hasAttribute( QStringLiteral("innerColor") ) ) ga->setGeometricalInnerColor( QColor( m_annotElement.attribute( QStringLiteral("innerColor") ) ) ); //set boundary rect.left = qMin( startpoint.x, point.x ); rect.top = qMin( startpoint.y, point.y ); rect.right = qMax( startpoint.x, point.x ); rect.bottom = qMax( startpoint.y, point.y ); } m_creationCompleted = false; clicked = false; // safety check if ( !ann ) return QList< Okular::Annotation* >(); // set common attributes ann->style().setColor( m_annotElement.hasAttribute( QStringLiteral("color") ) ? m_annotElement.attribute( QStringLiteral("color") ) : m_engineColor ); if ( m_annotElement.hasAttribute( QStringLiteral("opacity") ) ) ann->style().setOpacity( m_annotElement.attribute( QStringLiteral("opacity"), QStringLiteral("1.0") ).toDouble() ); // set the bounding rectangle, and make sure that the newly created // annotation lies within the page by translating it if necessary if ( rect.right > 1 ) { rect.left -= rect.right - 1; rect.right = 1; } if ( rect.bottom > 1 ) { rect.top -= rect.bottom - 1; rect.bottom = 1; } ann->setBoundingRectangle( rect ); // return annotation return QList< Okular::Annotation* >() << ann; } private: bool clicked; Okular::NormalizedRect rect; Okular::NormalizedPoint startpoint; Okular::NormalizedPoint point; QPixmap pixmap; QString hoverIconName, iconName; int size; double xscale,yscale; double pagewidth, pageheight; bool center; bool m_block; }; /** @short PolyLineEngine */ class PolyLineEngine : public AnnotatorEngine { public: PolyLineEngine( const QDomElement & engineElement ) : AnnotatorEngine( engineElement ), last( false ) { // parse engine specific attributes m_block = engineElement.attribute( QStringLiteral("block") ) == QLatin1String("true"); bool ok = true; // numofpoints represents the max number of points for the current // polygon/polyline, with a pair of exceptions: // -1 means: the polyline must close on the first point (polygon) // 0 means: construct as many points as you want, right-click // to construct the last point numofpoints = engineElement.attribute( QStringLiteral("points") ).toInt( &ok ); if ( !ok ) numofpoints = -1; } QRect event( EventType type, Button button, double nX, double nY, double xScale, double yScale, const Okular::Page * /*page*/ ) override { // only proceed if pressing left button // if ( button != Left ) // return rect; // start operation if ( type == Press ) { newPoint.x = nX; newPoint.y = nY; if ( button == Right ) last = true; } // move the second point else if ( type == Move ) { movingpoint.x = nX; movingpoint.y = nY; const QRect oldmovingrect = movingrect; movingrect = rect | QRect( (int)( movingpoint.x * xScale ), (int)( movingpoint.y * yScale ), 1, 1 ); return oldmovingrect | movingrect; } else if ( type == Release ) { const Okular::NormalizedPoint tmppoint(nX, nY); if ( fabs( tmppoint.x - newPoint.x ) + fabs( tmppoint.y - newPoint.y ) > 1e-2 ) return rect; if ( numofpoints == -1 && points.count() > 1 && ( fabs( points[0].x - newPoint.x ) + fabs( points[0].y - newPoint.y ) < 1e-2 ) ) { last = true; } else { points.append( newPoint ); rect |= QRect( (int)( newPoint.x * xScale ), (int)( newPoint.y * yScale ), 1, 1 ); } // end creation if we have constructed the last point of enough points if ( last || points.count() == numofpoints ) { m_creationCompleted = true; last = false; normRect = Okular::NormalizedRect( rect, xScale, yScale ); } } return rect; } void paint( QPainter * painter, double xScale, double yScale, const QRect & /*clipRect*/ ) override { if ( points.count() < 1 ) return; if ( m_block && points.count() == 2 ) { const Okular::NormalizedPoint first = points[0]; const Okular::NormalizedPoint second = points[1]; // draw a semitransparent block around the 2 points painter->setPen( m_engineColor ); painter->setBrush( QBrush( m_engineColor.lighter(), Qt::Dense4Pattern ) ); painter->drawRect( (int)(first.x * (double)xScale), (int)(first.y * (double)yScale), (int)((second.x - first.x) * (double)xScale), (int)((second.y - first.y) * (double)yScale) ); } else { // draw a polyline that connects the constructed points painter->setPen( QPen( m_engineColor, 2 ) ); for ( int i = 1; i < points.count(); ++i ) painter->drawLine( (int)(points[i - 1].x * (double)xScale), (int)(points[i - 1].y * (double)yScale), (int)(points[i].x * (double)xScale), (int)(points[i].y * (double)yScale) ); painter->drawLine( (int)(points.last().x * (double)xScale), (int)(points.last().y * (double)yScale), (int)(movingpoint.x * (double)xScale), (int)(movingpoint.y * (double)yScale) ); } } QList< Okular::Annotation* > end() override { m_creationCompleted = false; // find out annotation's description node if ( m_annotElement.isNull() ) return QList< Okular::Annotation* >(); // find out annotation's type Okular::Annotation * ann = nullptr; const QString typeString = m_annotElement.attribute( QStringLiteral("type") ); // create LineAnnotation from path if ( typeString == QLatin1String("Line") || typeString == QLatin1String("Polyline") || typeString == QLatin1String("Polygon") ) { if ( points.count() < 2 ) return QList< Okular::Annotation* >(); //add note Okular::LineAnnotation * la = new Okular::LineAnnotation(); ann = la; QLinkedList list; for ( int i = 0; i < points.count(); ++i ) list.append( points[ i ] ); la->setLinePoints( list ); if ( numofpoints == -1 ) { la->setLineClosed( true ); if ( m_annotElement.hasAttribute( QStringLiteral("innerColor") ) ) la->setLineInnerColor( QColor( m_annotElement.attribute( QStringLiteral("innerColor") ) ) ); } else if ( numofpoints == 2 ) { if ( m_annotElement.hasAttribute( QStringLiteral("leadFwd") ) ) la->setLineLeadingForwardPoint( m_annotElement.attribute( QStringLiteral("leadFwd") ).toDouble() ); if ( m_annotElement.hasAttribute( QStringLiteral("leadBack") ) ) la->setLineLeadingBackwardPoint( m_annotElement.attribute( QStringLiteral("leadBack") ).toDouble() ); } if ( m_annotElement.hasAttribute( QStringLiteral("startStyle") ) ) la->setLineStartStyle( (Okular::LineAnnotation::TermStyle)m_annotElement.attribute( QStringLiteral("startStyle") ).toInt() ); if ( m_annotElement.hasAttribute( QStringLiteral("endStyle") ) ) la->setLineEndStyle( (Okular::LineAnnotation::TermStyle)m_annotElement.attribute( QStringLiteral("endStyle") ).toInt() ); la->setBoundingRectangle( normRect ); } // safety check if ( !ann ) return QList< Okular::Annotation* >(); if ( m_annotElement.hasAttribute( QStringLiteral("width") ) ) ann->style().setWidth( m_annotElement.attribute( QStringLiteral("width") ).toDouble() ); // set common attributes ann->style().setColor( m_annotElement.hasAttribute( QStringLiteral("color") ) ? m_annotElement.attribute( QStringLiteral("color") ) : m_engineColor ); if ( m_annotElement.hasAttribute( QStringLiteral("opacity") ) ) ann->style().setOpacity( m_annotElement.attribute( QStringLiteral("opacity"), QStringLiteral("1.0") ).toDouble() ); // return annotation return QList< Okular::Annotation* >() << ann; } private: QList points; Okular::NormalizedPoint newPoint; Okular::NormalizedPoint movingpoint; QRect rect; QRect movingrect; Okular::NormalizedRect normRect; bool m_block; bool last; int numofpoints; }; /** @short TextSelectorEngine */ class TextSelectorEngine : public AnnotatorEngine { public: TextSelectorEngine( const QDomElement & engineElement, PageView * pageView ) : AnnotatorEngine( engineElement ), m_pageView( pageView ) { // parse engine specific attributes } QRect event( EventType type, Button button, double nX, double nY, double xScale, double yScale, const Okular::Page * /*page*/ ) override { // only proceed if pressing left button if ( button != Left ) return QRect(); if ( type == Press ) { lastPoint.x = nX; lastPoint.y = nY; const QRect oldrect = rect; rect = QRect(); return oldrect; } else if ( type == Move ) { if ( item() ) { const QPoint start( (int)( lastPoint.x * item()->uncroppedWidth() ), (int)( lastPoint.y * item()->uncroppedHeight() ) ); const QPoint end( (int)( nX * item()->uncroppedWidth() ), (int)( nY * item()->uncroppedHeight() ) ); selection.reset(); std::unique_ptr newselection( m_pageView->textSelectionForItem( item(), start, end ) ); if ( newselection && !newselection->isEmpty() ) { const QList geom = newselection->geometry( (int)xScale, (int)yScale ); QRect newrect; Q_FOREACH ( const QRect& r, geom ) { if ( newrect.isNull() ) newrect = r; else newrect |= r; } rect |= newrect; selection = std::move(newselection); } } } else if ( type == Release && selection ) { m_creationCompleted = true; } return rect; } void paint( QPainter * painter, double xScale, double yScale, const QRect & /*clipRect*/ ) override { if ( selection ) { painter->setPen( Qt::NoPen ); QColor col = m_engineColor; col.setAlphaF( 0.5 ); painter->setBrush( col ); foreach( const Okular::NormalizedRect & r, *selection ) { painter->drawRect( r.geometry( (int)xScale, (int)yScale ) ); } } } QList< Okular::Annotation* > end() override { m_creationCompleted = false; // safety checks if ( m_annotElement.isNull() || !selection ) return QList< Okular::Annotation* >(); // find out annotation's type Okular::Annotation * ann = nullptr; const QString typeString = m_annotElement.attribute( QStringLiteral("type") ); Okular::HighlightAnnotation::HighlightType type = Okular::HighlightAnnotation::Highlight; bool typevalid = false; // create HighlightAnnotation's from the selected area if ( typeString == QLatin1String("Highlight") ) { type = Okular::HighlightAnnotation::Highlight; typevalid = true; } else if ( typeString == QLatin1String("Squiggly") ) { type = Okular::HighlightAnnotation::Squiggly; typevalid = true; } else if ( typeString == QLatin1String("Underline") ) { type = Okular::HighlightAnnotation::Underline; typevalid = true; } else if ( typeString == QLatin1String("StrikeOut") ) { type = Okular::HighlightAnnotation::StrikeOut; typevalid = true; } if ( typevalid ) { Okular::HighlightAnnotation * ha = new Okular::HighlightAnnotation(); ha->setHighlightType( type ); ha->setBoundingRectangle( Okular::NormalizedRect( rect, item()->uncroppedWidth(), item()->uncroppedHeight() ) ); foreach ( const Okular::NormalizedRect & r, *selection ) { Okular::HighlightAnnotation::Quad q; q.setCapStart( false ); q.setCapEnd( false ); q.setFeather( 1.0 ); q.setPoint( Okular::NormalizedPoint( r.left, r.bottom ), 0 ); q.setPoint( Okular::NormalizedPoint( r.right, r.bottom ), 1 ); q.setPoint( Okular::NormalizedPoint( r.right, r.top ), 2 ); q.setPoint( Okular::NormalizedPoint( r.left, r.top ), 3 ); ha->highlightQuads().append( q ); } ann = ha; } selection.reset(); // safety check if ( !ann ) return QList< Okular::Annotation* >(); // set common attributes ann->style().setColor( m_annotElement.hasAttribute( QStringLiteral("color") ) ? m_annotElement.attribute( QStringLiteral("color") ) : m_engineColor ); if ( m_annotElement.hasAttribute( QStringLiteral("opacity") ) ) ann->style().setOpacity( m_annotElement.attribute( QStringLiteral("opacity"), QStringLiteral("1.0") ).toDouble() ); // return annotations return QList< Okular::Annotation* >() << ann; } QCursor cursor() const override { return Qt::IBeamCursor; } private: // data PageView * m_pageView; // TODO: support more pages std::unique_ptr selection; Okular::NormalizedPoint lastPoint; QRect rect; }; /** PageViewAnnotator **/ PageViewAnnotator::PageViewAnnotator( PageView * parent, Okular::Document * storage ) : QObject( parent ), m_document( storage ), m_pageView( parent ), m_toolBar( nullptr ), m_engine( nullptr ), m_textToolsEnabled( false ), m_toolsEnabled( false ), m_continuousMode( false ), m_hidingWasForced( false ), m_lastToolID( -1 ), m_lockedItem( nullptr ) { reparseConfig(); } void PageViewAnnotator::reparseConfig() { m_items.clear(); // Read tool list from configuration. It's a list of XML elements const QStringList userTools = Okular::Settings::annotationTools(); // Populate m_toolsDefinition QDomDocument doc; m_toolsDefinition = doc.createElement( QStringLiteral("annotatingTools") ); foreach ( const QString &toolXml, userTools ) { QDomDocument entryParser; if ( entryParser.setContent( toolXml ) ) m_toolsDefinition.appendChild( doc.importNode( entryParser.documentElement(), true ) ); else qCWarning(OkularUiDebug) << "Skipping malformed tool XML in AnnotationTools setting"; } // Create the AnnotationToolItems from the XML dom tree QDomNode toolDescription = m_toolsDefinition.firstChild(); while ( toolDescription.isElement() ) { QDomElement toolElement = toolDescription.toElement(); if ( toolElement.tagName() == QLatin1String("tool") ) { AnnotationToolItem item; item.id = toolElement.attribute(QStringLiteral("id")).toInt(); if ( toolElement.hasAttribute( QStringLiteral("name") ) ) item.text = toolElement.attribute( QStringLiteral("name") ); else item.text = defaultToolName( toolElement ); item.pixmap = makeToolPixmap( toolElement ); QDomNode shortcutNode = toolElement.elementsByTagName( QStringLiteral("shortcut") ).item( 0 ); if ( shortcutNode.isElement() ) item.shortcut = shortcutNode.toElement().text(); QDomNodeList engineNodeList = toolElement.elementsByTagName( QStringLiteral("engine") ); if ( engineNodeList.size() > 0 ) { QDomElement engineEl = engineNodeList.item( 0 ).toElement(); if ( !engineEl.isNull() && engineEl.hasAttribute( QStringLiteral("type") ) ) item.isText = engineEl.attribute( QStringLiteral("type") ) == QLatin1String( "TextSelector" ); } m_items.push_back( item ); } toolDescription = toolDescription.nextSibling(); } } PageViewAnnotator::~PageViewAnnotator() { delete m_engine; } void PageViewAnnotator::setEnabled( bool on ) { if ( !on ) { // remove toolBar if ( m_toolBar ) m_toolBar->hideAndDestroy(); m_toolBar = nullptr; // deactivate the active tool, if any slotToolSelected( -1 ); return; } // if no tools are defined, don't show the toolbar if ( !m_toolsDefinition.hasChildNodes() ) return; // create toolBar if ( !m_toolBar ) { m_toolBar = new PageViewToolBar( m_pageView, m_pageView->viewport() ); m_toolBar->setSide( (PageViewToolBar::Side)Okular::Settings::editToolBarPlacement() ); m_toolBar->setItems( m_items ); m_toolBar->setToolsEnabled( m_toolsEnabled ); m_toolBar->setTextToolsEnabled( m_textToolsEnabled ); connect(m_toolBar, &PageViewToolBar::toolSelected, this, &PageViewAnnotator::slotToolSelected); connect(m_toolBar, &PageViewToolBar::orientationChanged, this, &PageViewAnnotator::slotSaveToolbarOrientation); connect(m_toolBar, &PageViewToolBar::buttonDoubleClicked, this, &PageViewAnnotator::slotToolDoubleClicked); m_toolBar->setCursor(Qt::ArrowCursor); } // show the toolBar m_toolBar->showAndAnimate(); } void PageViewAnnotator::setTextToolsEnabled( bool enabled ) { m_textToolsEnabled = enabled; if ( m_toolBar ) m_toolBar->setTextToolsEnabled( m_textToolsEnabled ); } void PageViewAnnotator::setToolsEnabled( bool enabled ) { m_toolsEnabled = enabled; if ( m_toolBar ) m_toolBar->setToolsEnabled( m_toolsEnabled ); } void PageViewAnnotator::setHidingForced( bool forced ) { m_hidingWasForced = forced; } bool PageViewAnnotator::hidingWasForced() const { return m_hidingWasForced; } bool PageViewAnnotator::active() const { return m_engine && m_toolBar; } bool PageViewAnnotator::annotating() const { return active() && m_lockedItem; } QCursor PageViewAnnotator::cursor() const { return m_engine->cursor(); } QRect PageViewAnnotator::performRouteMouseOrTabletEvent(const AnnotatorEngine::EventType & eventType, const AnnotatorEngine::Button & button, const QPointF & pos, PageViewItem * item ) { // if the right mouse button was pressed, we simply do nothing. In this way, we are still editing the annotation // and so this function will receive and process the right mouse button release event too. If we detach now the annotation tool, // the release event will be processed by the PageView class which would create the annotation property widget, and we do not want this. if ( button == AnnotatorEngine::Right && eventType == AnnotatorEngine::Press ) return QRect(); else if ( button == AnnotatorEngine::Right && eventType == AnnotatorEngine::Release ) { detachAnnotation(); return QRect(); } // 1. lock engine to current item if ( !m_lockedItem && eventType == AnnotatorEngine::Press ) { m_lockedItem = item; m_engine->setItem( m_lockedItem ); } if ( !m_lockedItem ) { return QRect(); } // find out normalized mouse coords inside current item const QRect & itemRect = m_lockedItem->uncroppedGeometry(); const QPointF eventPos = m_pageView->contentAreaPoint( pos ); const double nX = qBound( 0.0, m_lockedItem->absToPageX( eventPos.x() ), 1.0 ); const double nY = qBound( 0.0, m_lockedItem->absToPageY( eventPos.y() ), 1.0 ); QRect modifiedRect; // 2. use engine to perform operations const QRect paintRect = m_engine->event( eventType, button, nX, nY, itemRect.width(), itemRect.height(), m_lockedItem->page() ); // 3. update absolute extents rect and send paint event(s) if ( paintRect.isValid() ) { // 3.1. unite old and new painting regions QRegion compoundRegion( m_lastDrawnRect ); m_lastDrawnRect = paintRect; m_lastDrawnRect.translate( itemRect.left(), itemRect.top() ); // 3.2. decompose paint region in rects and send paint events const QVector rects = compoundRegion.united( m_lastDrawnRect ).rects(); const QPoint areaPos = m_pageView->contentAreaPosition(); for ( int i = 0; i < rects.count(); i++ ) m_pageView->viewport()->update( rects[i].translated( -areaPos ) ); modifiedRect = compoundRegion.boundingRect() | m_lastDrawnRect; } // 4. if engine has finished, apply Annotation to the page if ( m_engine->creationCompleted() ) { // apply engine data to the Annotation's and reset engine QList< Okular::Annotation* > annotations = m_engine->end(); // attach the newly filled annotations to the page foreach ( Okular::Annotation * annotation, annotations ) { if ( !annotation ) continue; annotation->setCreationDate( QDateTime::currentDateTime() ); annotation->setModificationDate( QDateTime::currentDateTime() ); annotation->setAuthor( Okular::Settings::identityAuthor() ); m_document->addPageAnnotation( m_lockedItem->pageNumber(), annotation ); if ( annotation->openDialogAfterCreation() ) m_pageView->openAnnotationWindow( annotation, m_lockedItem->pageNumber() ); } if ( m_continuousMode ) slotToolSelected( m_lastToolID ); else detachAnnotation(); } return modifiedRect; } QRect PageViewAnnotator::routeMouseEvent( QMouseEvent * e, PageViewItem * item ) { AnnotatorEngine::EventType eventType; AnnotatorEngine::Button button; // figure out the event type and button AnnotatorEngine::decodeEvent( e, &eventType, &button ); return performRouteMouseOrTabletEvent( eventType, button, e->localPos(), item ); } QRect PageViewAnnotator::routeTabletEvent( QTabletEvent * e, PageViewItem * item, const QPoint & localOriginInGlobal ) { // Unlike routeMouseEvent, routeTabletEvent must explicitly ignore events it doesn't care about so that // the corresponding mouse event will later be delivered. if ( !item ) { e->ignore(); return QRect(); } // We set all tablet events that take place over the annotations toolbar to ignore so that corresponding mouse // events will be delivered to the toolbar. However, we still allow the annotations code to handle // TabletMove and TabletRelease events in case the user is drawing an annotation onto the toolbar. const QPoint toolBarPos = m_toolBar->mapFromGlobal( e->globalPos() ); const QRect toolBarRect = m_toolBar->rect(); if ( toolBarRect.contains( toolBarPos ) ) { e->ignore(); if (e->type() == QEvent::TabletPress) return QRect(); } AnnotatorEngine::EventType eventType; AnnotatorEngine::Button button; // figure out the event type and button AnnotatorEngine::decodeEvent( e, &eventType, &button ); const QPointF globalPosF = e->globalPosF(); const QPointF localPosF = globalPosF - localOriginInGlobal; return performRouteMouseOrTabletEvent( eventType, button, localPosF, item ); } bool PageViewAnnotator::routeKeyEvent( QKeyEvent * event ) { if ( event->key() == Qt::Key_Escape ) { detachAnnotation(); return true; } return false; } bool PageViewAnnotator::routePaints( const QRect & wantedRect ) const { return m_engine && m_toolBar && wantedRect.intersects( m_lastDrawnRect ) && m_lockedItem; } void PageViewAnnotator::routePaint( QPainter * painter, const QRect & paintRect ) { // if there's no locked item, then there's no decided place to draw on if ( !m_lockedItem ) return; #ifndef NDEBUG // [DEBUG] draw the paint region if enabled if ( Okular::Settings::debugDrawAnnotationRect() ) painter->drawRect( paintRect ); #endif // move painter to current itemGeometry rect const QRect & itemRect = m_lockedItem->uncroppedGeometry(); painter->save(); painter->translate( itemRect.topLeft() ); // TODO: Clip annotation painting to cropped page. // transform cliprect from absolute to item relative coords QRect annotRect = paintRect.intersected( m_lastDrawnRect ); annotRect.translate( -itemRect.topLeft() ); // use current engine for painting (in virtual page coordinates) m_engine->paint( painter, m_lockedItem->uncroppedWidth(), m_lockedItem->uncroppedHeight(), annotRect ); painter->restore(); } void PageViewAnnotator::slotToolSelected( int toolID ) { // terminate any previous operation if ( m_engine ) { delete m_engine; m_engine = nullptr; } m_lockedItem = nullptr; if ( m_lastDrawnRect.isValid() ) { m_pageView->viewport()->update( m_lastDrawnRect.translated( -m_pageView->contentAreaPosition() ) ); m_lastDrawnRect = QRect(); } if ( toolID != m_lastToolID ) m_continuousMode = false; // store current tool for later usage m_lastToolID = toolID; // handle tool deselection if ( toolID == -1 ) { m_pageView->displayMessage( QString() ); m_pageView->updateCursor(); return; } // for the selected tool create the Engine QDomNode toolNode = m_toolsDefinition.firstChild(); while ( toolNode.isElement() ) { QDomElement toolElement = toolNode.toElement(); toolNode = toolNode.nextSibling(); // only find out the element describing selected tool if ( toolElement.tagName() != QLatin1String("tool") || toolElement.attribute(QStringLiteral("id")).toInt() != toolID ) continue; // parse tool properties QDomNode toolSubNode = toolElement.firstChild(); while ( toolSubNode.isElement() ) { QDomElement toolSubElement = toolSubNode.toElement(); toolSubNode = toolSubNode.nextSibling(); // create the AnnotatorEngine if ( toolSubElement.tagName() == QLatin1String("engine") ) { QString type = toolSubElement.attribute( QStringLiteral("type") ); if ( type == QLatin1String("SmoothLine") ) m_engine = new SmoothPathEngine( toolSubElement ); else if ( type == QLatin1String("PickPoint") ) m_engine = new PickPointEngine( toolSubElement ); else if ( type == QLatin1String("PolyLine") ) m_engine = new PolyLineEngine( toolSubElement ); else if ( type == QLatin1String("TextSelector") ) m_engine = new TextSelectorEngine( toolSubElement, m_pageView ); else qCWarning(OkularUiDebug).nospace() << "tools.xml: engine type:'" << type << "' is not defined!"; } // display the tooltip const QString annotType = toolElement.attribute( QStringLiteral("type") ); QString tip; if ( annotType == QLatin1String("ellipse") ) tip = i18nc( "Annotation tool", "Draw an ellipse (drag to select a zone)" ); else if ( annotType == QLatin1String("highlight") ) tip = i18nc( "Annotation tool", "Highlight text" ); else if ( annotType == QLatin1String("ink") ) tip = i18nc( "Annotation tool", "Draw a freehand line" ); else if ( annotType == QLatin1String("note-inline") ) tip = i18nc( "Annotation tool", "Inline Text Annotation (drag to select a zone)" ); else if ( annotType == QLatin1String("note-linked") ) tip = i18nc( "Annotation tool", "Put a pop-up note" ); else if ( annotType == QLatin1String("polygon") ) tip = i18nc( "Annotation tool", "Draw a polygon (click on the first point to close it)" ); else if ( annotType == QLatin1String("rectangle") ) tip = i18nc( "Annotation tool", "Draw a rectangle" ); else if ( annotType == QLatin1String("squiggly") ) tip = i18nc( "Annotation tool", "Squiggle text" ); else if ( annotType == QLatin1String("stamp") ) tip = i18nc( "Annotation tool", "Put a stamp symbol" ); else if ( annotType == QLatin1String("straight-line") ) tip = i18nc( "Annotation tool", "Draw a straight line" ); else if ( annotType == QLatin1String("strikeout") ) tip = i18nc( "Annotation tool", "Strike out text" ); else if ( annotType == QLatin1String("underline") ) tip = i18nc( "Annotation tool", "Underline text" ); else if ( annotType == QLatin1String("typewriter") ) tip = i18nc( "Annotation tool", "Typewriter Annotation (drag to select a zone)" ); if ( !tip.isEmpty() && !m_continuousMode ) m_pageView->displayMessage( tip, QString(), PageViewMessage::Annotation ); } // consistency warning if ( !m_engine ) { qCWarning(OkularUiDebug) << "tools.xml: couldn't find good engine description. check xml."; } m_pageView->updateCursor(); // stop after parsing selected tool's node break; } } void PageViewAnnotator::slotSaveToolbarOrientation( int side ) { Okular::Settings::setEditToolBarPlacement( (int)side ); Okular::Settings::self()->save(); } void PageViewAnnotator::slotToolDoubleClicked( int /*toolID*/ ) { m_continuousMode = true; } void PageViewAnnotator::detachAnnotation() { m_toolBar->selectButton( -1 ); } QString PageViewAnnotator::defaultToolName( const QDomElement &toolElement ) { const QString annotType = toolElement.attribute( QStringLiteral("type") ); if ( annotType == QLatin1String("ellipse") ) return i18n( "Ellipse" ); else if ( annotType == QLatin1String("highlight") ) return i18n( "Highlighter" ); else if ( annotType == QLatin1String("ink") ) return i18n( "Freehand Line" ); else if ( annotType == QLatin1String("note-inline") ) return i18n( "Inline Note" ); else if ( annotType == QLatin1String("note-linked") ) return i18n( "Pop-up Note" ); else if ( annotType == QLatin1String("polygon") ) return i18n( "Polygon" ); else if ( annotType == QLatin1String("rectangle") ) return i18n( "Rectangle" ); else if ( annotType == QLatin1String("squiggly") ) return i18n( "Squiggle" ); else if ( annotType == QLatin1String("stamp") ) return i18n( "Stamp" ); else if ( annotType == QLatin1String("straight-line") ) return i18n( "Straight Line" ); else if ( annotType == QLatin1String("strikeout") ) return i18n( "Strike out" ); else if ( annotType == QLatin1String("underline") ) return i18n( "Underline" ); else if ( annotType == QLatin1String("typewriter") ) return i18n( "Typewriter" ); else return QString(); } QPixmap PageViewAnnotator::makeToolPixmap( const QDomElement &toolElement ) { QPixmap pixmap( 32 * qApp->devicePixelRatio(), 32 * qApp->devicePixelRatio() ); pixmap.setDevicePixelRatio( qApp->devicePixelRatio() ); const QString annotType = toolElement.attribute( QStringLiteral("type") ); // Load HiDPI variant on HiDPI screen QString imageVariant; if ( qApp->devicePixelRatio() > 1.05 ) { - imageVariant = "@2x"; + imageVariant = QStringLiteral("@2x"); } // Load base pixmap. We'll draw on top of it pixmap.load( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString("okular/pics/tool-base-okular" + imageVariant + ".png") ) ); /* Parse color, innerColor and icon (if present) */ QColor engineColor, innerColor, textColor, annotColor; QString icon; QDomNodeList engineNodeList = toolElement.elementsByTagName( QStringLiteral("engine") ); if ( engineNodeList.size() > 0 ) { QDomElement engineEl = engineNodeList.item( 0 ).toElement(); if ( !engineEl.isNull() && engineEl.hasAttribute( QStringLiteral("color") ) ) engineColor = QColor( engineEl.attribute( QStringLiteral("color") ) ); } QDomNodeList annotationNodeList = toolElement.elementsByTagName( QStringLiteral("annotation") ); if ( annotationNodeList.size() > 0 ) { QDomElement annotationEl = annotationNodeList.item( 0 ).toElement(); if ( !annotationEl.isNull() ) { if ( annotationEl.hasAttribute( QStringLiteral("color") ) ) annotColor = annotationEl.attribute( QStringLiteral("color") ); if ( annotationEl.hasAttribute( QStringLiteral("innerColor") ) ) innerColor = QColor( annotationEl.attribute( QStringLiteral("innerColor") ) ); if ( annotationEl.hasAttribute( QStringLiteral("textColor") ) ) textColor = QColor( annotationEl.attribute( QStringLiteral("textColor") ) ); if ( annotationEl.hasAttribute( QStringLiteral("icon") ) ) icon = annotationEl.attribute( QStringLiteral("icon") ); } } QPainter p( &pixmap ); if ( annotType == QLatin1String("ellipse") ) { p.setRenderHint( QPainter::Antialiasing ); if ( innerColor.isValid() ) p.setBrush( innerColor ); p.setPen( QPen( engineColor, 2 ) ); p.drawEllipse( 2, 7, 21, 14 ); } else if ( annotType == QLatin1String("highlight") ) { QImage overlay( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString("okular/pics/tool-highlighter-okular-colorizable" + imageVariant + ".png") ) ); QImage colorizedOverlay = overlay; GuiUtils::colorizeImage( colorizedOverlay, engineColor ); p.drawImage( QPoint(0,0), colorizedOverlay ); // Trail p.drawImage( QPoint(0,-32), overlay ); // Text + Shadow (uncolorized) p.drawImage( QPoint(0,-64), colorizedOverlay ); // Pen } else if ( annotType == QLatin1String("ink") ) { QImage overlay( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString("okular/pics/tool-ink-okular-colorizable" + imageVariant + ".png") ) ); QImage colorizedOverlay = overlay; GuiUtils::colorizeImage( colorizedOverlay, engineColor ); p.drawImage( QPoint(0,0), colorizedOverlay ); // Trail p.drawImage( QPoint(0,-32), overlay ); // Shadow (uncolorized) p.drawImage( QPoint(0,-64), colorizedOverlay ); // Pen } else if ( annotType == QLatin1String("note-inline") ) { QImage overlay( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString("okular/pics/tool-note-inline-okular-colorizable" + imageVariant + ".png") ) ); GuiUtils::colorizeImage( overlay, engineColor ); p.drawImage( QPoint(0,0), overlay ); } else if ( annotType == QLatin1String("note-linked") ) { QImage overlay( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString("okular/pics/tool-note-okular-colorizable" + imageVariant + ".png") ) ); GuiUtils::colorizeImage( overlay, engineColor ); p.drawImage( QPoint(0,0), overlay ); } else if ( annotType == QLatin1String("polygon") ) { QPainterPath path; path.moveTo( 0, 7 ); path.lineTo( 19, 7 ); path.lineTo( 19, 14 ); path.lineTo( 23, 14 ); path.lineTo( 23, 20 ); path.lineTo( 0, 20 ); if ( innerColor.isValid() ) p.setBrush( innerColor ); p.setPen( QPen( engineColor, 1 ) ); p.drawPath( path ); } else if ( annotType == QLatin1String("rectangle") ) { p.setRenderHint( QPainter::Antialiasing ); if ( innerColor.isValid() ) p.setBrush( innerColor ); p.setPen( QPen( engineColor, 2 ) ); p.drawRect( 2, 7, 21, 14 ); } else if ( annotType == QLatin1String("squiggly") ) { QPen pen( engineColor, 1 ); pen.setDashPattern( QVector() << 1 << 1 ); p.setPen( pen ); p.drawLine( 1, 13, 16, 13 ); p.drawLine( 2, 14, 15, 14 ); p.drawLine( 0, 20, 19, 20 ); p.drawLine( 1, 21, 18, 21 ); } else if ( annotType == QLatin1String("stamp") ) { QPixmap stamp = GuiUtils::loadStamp( icon, QSize( 16, 16 ) ); p.setRenderHint( QPainter::Antialiasing ); p.drawPixmap( 16, 14, stamp ); } else if ( annotType == QLatin1String("straight-line") ) { QPainterPath path; path.moveTo( 1, 8 ); path.lineTo( 20, 8 ); path.lineTo( 1, 27 ); path.lineTo( 20, 27 ); p.setRenderHint( QPainter::Antialiasing ); p.setPen( QPen( engineColor, 1 ) ); p.drawPath( path ); // TODO To be discussed: This is not a straight line! } else if ( annotType == QLatin1String("strikeout") ) { p.setPen( QPen( engineColor, 1 ) ); p.drawLine( 1, 10, 16, 10 ); p.drawLine( 0, 17, 19, 17 ); } else if ( annotType == QLatin1String("underline") ) { p.setPen( QPen( engineColor, 1 ) ); p.drawLine( 1, 13, 16, 13 ); p.drawLine( 0, 20, 19, 20 ); } else if ( annotType == QLatin1String("typewriter") ) { QImage overlay( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString("okular/pics/tool-typewriter-okular-colorizable" + imageVariant + ".png") ) ); GuiUtils::colorizeImage( overlay, textColor ); p.drawImage( QPoint(-2,2), overlay ); } else { /* Unrecognized annotation type -- It shouldn't happen */ p.setPen( QPen( engineColor ) ); p.drawText( QPoint(20, 31), QStringLiteral("?") ); } return pixmap; } #include "moc_pageviewannotator.cpp" /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/presentationwidget.cpp b/ui/presentationwidget.cpp index 89838bea8..af2fe04ac 100644 --- a/ui/presentationwidget.cpp +++ b/ui/presentationwidget.cpp @@ -1,2491 +1,2491 @@ /*************************************************************************** * Copyright (C) 2004 by Enrico Ros * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "presentationwidget.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_LINUX #include #include // For ::close() for sleep inhibition #endif // system includes #include #include // local includes #include "annotationtools.h" #include "debug_ui.h" #include "drawingtoolactions.h" #include "guiutils.h" #include "pagepainter.h" #include "presentationsearchbar.h" #include "priorities.h" #include "videowidget.h" #include "core/action.h" #include "core/annotations.h" #include "core/audioplayer.h" #include "core/document.h" #include "core/generator.h" #include "core/movie.h" #include "core/page.h" #include "settings.h" #include "settings_core.h" // comment this to disable the top-right progress indicator #define ENABLE_PROGRESS_OVERLAY // a frame contains a pointer to the page object, its geometry and the // transition effect to the next frame struct PresentationFrame { PresentationFrame() = default; ~PresentationFrame() { qDeleteAll( videoWidgets ); } PresentationFrame(const PresentationFrame &) = delete; PresentationFrame &operator=(const PresentationFrame &) = delete; void recalcGeometry( int width, int height, float screenRatio ) { // calculate frame geometry keeping constant aspect ratio float pageRatio = page->ratio(); int pageWidth = width, pageHeight = height; if ( pageRatio > screenRatio ) pageWidth = (int)( (float)pageHeight / pageRatio ); else pageHeight = (int)( (float)pageWidth * pageRatio ); geometry.setRect( ( width - pageWidth ) / 2, ( height - pageHeight ) / 2, pageWidth, pageHeight ); Q_FOREACH ( VideoWidget *vw, videoWidgets ) { const Okular::NormalizedRect r = vw->normGeometry(); QRect vwgeom = r.geometry( geometry.width(), geometry.height() ); vw->resize( vwgeom.size() ); vw->move( geometry.topLeft() + vwgeom.topLeft() ); } } const Okular::Page * page; QRect geometry; QHash< Okular::Movie *, VideoWidget * > videoWidgets; QLinkedList< SmoothPath > drawings; }; // a custom QToolBar that basically does not propagate the event if the widget // background is not automatically filled class PresentationToolBar : public QToolBar { Q_OBJECT public: PresentationToolBar( QWidget * parent = Q_NULLPTR ) : QToolBar( parent ) {} protected: void mousePressEvent( QMouseEvent * e ) override { QToolBar::mousePressEvent( e ); e->accept(); } void mouseReleaseEvent( QMouseEvent * e ) override { QToolBar::mouseReleaseEvent( e ); e->accept(); } }; PresentationWidget::PresentationWidget( QWidget * parent, Okular::Document * doc, DrawingToolActions * drawingToolActions, KActionCollection * collection ) : QWidget( nullptr /* must be null, to have an independent widget */, Qt::FramelessWindowHint ), m_pressedLink( nullptr ), m_handCursor( false ), m_drawingEngine( nullptr ), m_screenInhibitCookie(0), m_sleepInhibitFd(-1), m_parentWidget( parent ), m_document( doc ), m_frameIndex( -1 ), m_topBar( nullptr ), m_pagesEdit( nullptr ), m_searchBar( nullptr ), m_ac( collection ), m_screenSelect( nullptr ), m_isSetup( false ), m_blockNotifications( false ), m_inBlackScreenMode( false ), m_showSummaryView( Okular::Settings::slidesShowSummary() ), m_advanceSlides( Okular::SettingsCore::slidesAdvance() ), m_goToPreviousPageOnRelease( false ), m_goToNextPageOnRelease( false ) { Q_UNUSED( parent ) setAttribute( Qt::WA_DeleteOnClose ); setAttribute( Qt::WA_OpaquePaintEvent ); setObjectName( QStringLiteral( "presentationWidget" ) ); QString caption = doc->metaData( QStringLiteral("DocumentTitle") ).toString(); if ( caption.trimmed().isEmpty() ) caption = doc->currentDocument().fileName(); caption = i18nc( "[document title/filename] – Presentation", "%1 – Presentation", caption ); setWindowTitle( caption ); m_width = -1; m_screen = -2; // create top toolbar m_topBar = new PresentationToolBar( this ); m_topBar->setObjectName( QStringLiteral( "presentationBar" ) ); m_topBar->setMovable( false ); m_topBar->layout()->setContentsMargins(0, 0, 0, 0); m_topBar->addAction( QIcon::fromTheme( layoutDirection() == Qt::RightToLeft ? QStringLiteral("go-next") : QStringLiteral("go-previous") ), i18n( "Previous Page" ), this, SLOT(slotPrevPage()) ); m_pagesEdit = new KLineEdit( m_topBar ); QSizePolicy sp = m_pagesEdit->sizePolicy(); sp.setHorizontalPolicy( QSizePolicy::Minimum ); m_pagesEdit->setSizePolicy( sp ); QFontMetrics fm( m_pagesEdit->font() ); QStyleOptionFrame option; option.initFrom( m_pagesEdit ); m_pagesEdit->setMaximumWidth( fm.width( QString::number( m_document->pages() ) ) + 2 * style()->pixelMetric( QStyle::PM_DefaultFrameWidth, &option, m_pagesEdit ) + 4 ); // the 4 comes from 2*horizontalMargin, horizontalMargin being a define in qlineedit.cpp QIntValidator *validator = new QIntValidator( 1, m_document->pages(), m_pagesEdit ); m_pagesEdit->setValidator( validator ); m_topBar->addWidget( m_pagesEdit ); QLabel *pagesLabel = new QLabel( m_topBar ); pagesLabel->setText( QLatin1String( " / " ) + QString::number( m_document->pages() ) + QLatin1String( " " ) ); m_topBar->addWidget( pagesLabel ); connect(m_pagesEdit, &QLineEdit::returnPressed, this, &PresentationWidget::slotPageChanged); m_topBar->addAction( QIcon::fromTheme( layoutDirection() == Qt::RightToLeft ? QStringLiteral("go-previous") : QStringLiteral("go-next") ), i18n( "Next Page" ), this, SLOT(slotNextPage()) ); m_topBar->addSeparator(); QAction *playPauseAct = collection->action( QStringLiteral("presentation_play_pause") ); playPauseAct->setEnabled( true ); connect(playPauseAct, &QAction::triggered, this, &PresentationWidget::slotTogglePlayPause); m_topBar->addAction( playPauseAct ); setPlayPauseIcon(); addAction( playPauseAct ); m_topBar->addSeparator(); foreach(QAction *action, drawingToolActions->actions()) { action->setEnabled( true ); m_topBar->addAction( action ); addAction( action ); } connect( drawingToolActions, &DrawingToolActions::changeEngine, this, &PresentationWidget::slotChangeDrawingToolEngine ); connect( drawingToolActions, &DrawingToolActions::actionsRecreated, this, &PresentationWidget::slotAddDrawingToolActions ); QAction *eraseDrawingAct = collection->action( QStringLiteral("presentation_erase_drawings") ); eraseDrawingAct->setEnabled( true ); connect(eraseDrawingAct, &QAction::triggered, this, &PresentationWidget::clearDrawings); m_topBar->addAction( eraseDrawingAct ); addAction( eraseDrawingAct ); QDesktopWidget *desktop = QApplication::desktop(); if ( desktop->numScreens() > 1 ) { m_topBar->addSeparator(); m_screenSelect = new KSelectAction( QIcon::fromTheme( QStringLiteral("video-display") ), i18n( "Switch Screen" ), m_topBar ); m_screenSelect->setToolBarMode( KSelectAction::MenuMode ); m_screenSelect->setToolButtonPopupMode( QToolButton::InstantPopup ); m_topBar->addAction( m_screenSelect ); const int screenCount = desktop->numScreens(); for ( int i = 0; i < screenCount; ++i ) { QAction *act = m_screenSelect->addAction( i18nc( "%1 is the screen number (0, 1, ...)", "Screen %1", i ) ); act->setData( qVariantFromValue( i ) ); } } QWidget *spacer = new QWidget( m_topBar ); spacer->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::MinimumExpanding ); m_topBar->addWidget( spacer ); m_topBar->addAction( QIcon::fromTheme( QStringLiteral("application-exit") ), i18n( "Exit Presentation Mode" ), this, SLOT(close()) ); m_topBar->setAutoFillBackground( true ); showTopBar( false ); // change topbar background color QPalette p = m_topBar->palette(); p.setColor( QPalette::Active, QPalette::Button, Qt::gray ); p.setColor( QPalette::Active, QPalette::Window, Qt::darkGray ); m_topBar->setPalette( p ); // Grab swipe gestures to change pages grabGesture(Qt::SwipeGesture); // misc stuff setMouseTracking( true ); setContextMenuPolicy( Qt::PreventContextMenu ); m_transitionTimer = new QTimer( this ); m_transitionTimer->setSingleShot( true ); connect(m_transitionTimer, &QTimer::timeout, this, &PresentationWidget::slotTransitionStep); m_overlayHideTimer = new QTimer( this ); m_overlayHideTimer->setSingleShot( true ); connect(m_overlayHideTimer, &QTimer::timeout, this, &PresentationWidget::slotHideOverlay); m_nextPageTimer = new QTimer( this ); m_nextPageTimer->setSingleShot( true ); connect(m_nextPageTimer, &QTimer::timeout, this, &PresentationWidget::slotNextPage); connect(m_document, &Okular::Document::processMovieAction, this, &PresentationWidget::slotProcessMovieAction); connect(m_document, &Okular::Document::processRenditionAction, this, &PresentationWidget::slotProcessRenditionAction); // handle cursor appearance as specified in configuration if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay ) { KCursor::setAutoHideCursor( this, true ); KCursor::setHideCursorDelay( 3000 ); } else if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden ) { setCursor( QCursor( Qt::BlankCursor ) ); } setupActions(); // inhibit power management inhibitPowerManagement(); show(); QTimer::singleShot( 0, this, &PresentationWidget::slotDelayedEvents ); // setFocus() so KCursor::setAutoHideCursor() goes into effect if it's enabled setFocus( Qt::OtherFocusReason ); } PresentationWidget::~PresentationWidget() { // allow power management saver again allowPowerManagement(); // stop the audio playbacks Okular::AudioPlayer::instance()->stopPlaybacks(); // remove our highlights if ( m_searchBar ) { m_document->resetSearch( PRESENTATION_SEARCH_ID ); } // remove this widget from document observer m_document->removeObserver( this ); foreach( QAction *action, m_topBar->actions() ) { action->setChecked( false ); action->setEnabled( false ); } delete m_drawingEngine; // delete frames qDeleteAll( m_frames ); } void PresentationWidget::notifySetup( const QVector< Okular::Page * > & pageSet, int setupFlags ) { // same document, nothing to change - here we assume the document sets up // us with the whole document set as first notifySetup() if ( !( setupFlags & Okular::DocumentObserver::DocumentChanged ) ) return; // delete previous frames (if any (shouldn't be)) qDeleteAll( m_frames ); if ( !m_frames.isEmpty() ) qCWarning(OkularUiDebug) << "Frames setup changed while a Presentation is in progress."; m_frames.clear(); // create the new frames float screenRatio = (float)m_height / (float)m_width; for ( const Okular::Page * page : pageSet ) { PresentationFrame * frame = new PresentationFrame(); frame->page = page; const QLinkedList< Okular::Annotation * > annotations = page->annotations(); for ( Okular::Annotation * a : annotations ) { if ( a->subType() == Okular::Annotation::AMovie ) { Okular::MovieAnnotation * movieAnn = static_cast< Okular::MovieAnnotation * >( a ); VideoWidget * vw = new VideoWidget( movieAnn, movieAnn->movie(), m_document, this ); frame->videoWidgets.insert( movieAnn->movie(), vw ); vw->pageInitialized(); } else if ( a->subType() == Okular::Annotation::ARichMedia ) { Okular::RichMediaAnnotation * richMediaAnn = static_cast< Okular::RichMediaAnnotation * >( a ); if ( richMediaAnn->movie() ) { VideoWidget * vw = new VideoWidget( richMediaAnn, richMediaAnn->movie(), m_document, this ); frame->videoWidgets.insert( richMediaAnn->movie(), vw ); vw->pageInitialized(); } } else if ( a->subType() == Okular::Annotation::AScreen ) { const Okular::ScreenAnnotation * screenAnn = static_cast< Okular::ScreenAnnotation * >( a ); Okular::Movie *movie = GuiUtils::renditionMovieFromScreenAnnotation( screenAnn ); if ( movie ) { VideoWidget * vw = new VideoWidget( screenAnn, movie, m_document, this ); frame->videoWidgets.insert( movie, vw ); vw->pageInitialized(); } } } frame->recalcGeometry( m_width, m_height, screenRatio ); // add the frame to the vector m_frames.push_back( frame ); } // get metadata from the document m_metaStrings.clear(); const Okular::DocumentInfo info = m_document->documentInfo( QSet() << Okular::DocumentInfo::Title << Okular::DocumentInfo::Author ); if ( !info.get( Okular::DocumentInfo::Title ).isNull() ) m_metaStrings += i18n( "Title: %1", info.get( Okular::DocumentInfo::Title ) ); if ( !info.get( Okular::DocumentInfo::Author ).isNull() ) m_metaStrings += i18n( "Author: %1", info.get( Okular::DocumentInfo::Author ) ); m_metaStrings += i18n( "Pages: %1", m_document->pages() ); m_metaStrings += i18n( "Click to begin" ); m_isSetup = true; } void PresentationWidget::notifyViewportChanged( bool /*smoothMove*/ ) { // display the current page changePage( m_document->viewport().pageNumber ); // auto advance to the next page if set startAutoChangeTimer(); } void PresentationWidget::notifyPageChanged( int pageNumber, int changedFlags ) { // if we are blocking the notifications, do nothing if ( m_blockNotifications ) return; // check if it's the last requested pixmap. if so update the widget. if ( (changedFlags & ( DocumentObserver::Pixmap | DocumentObserver::Annotations | DocumentObserver::Highlights ) ) && pageNumber == m_frameIndex ) generatePage( changedFlags & ( DocumentObserver::Annotations | DocumentObserver::Highlights ) ); } void PresentationWidget::notifyCurrentPageChanged( int previousPage, int currentPage ) { if ( previousPage != -1 ) { // stop video playback Q_FOREACH ( VideoWidget *vw, m_frames[ previousPage ]->videoWidgets ) { vw->stop(); vw->pageLeft(); } // stop audio playback, if any Okular::AudioPlayer::instance()->stopPlaybacks(); // perform the page closing action, if any if ( m_document->page( previousPage )->pageAction( Okular::Page::Closing ) ) m_document->processAction( m_document->page( previousPage )->pageAction( Okular::Page::Closing ) ); // perform the additional actions of the page's annotations, if any Q_FOREACH ( const Okular::Annotation *annotation, m_document->page( previousPage )->annotations() ) { Okular::Action *action = nullptr; if ( annotation->subType() == Okular::Annotation::AScreen ) action = static_cast( annotation )->additionalAction( Okular::Annotation::PageClosing ); else if ( annotation->subType() == Okular::Annotation::AWidget ) action = static_cast( annotation )->additionalAction( Okular::Annotation::PageClosing ); if ( action ) m_document->processAction( action ); } } if ( currentPage != -1 ) { m_frameIndex = currentPage; // check if pixmap exists or else request it PresentationFrame * frame = m_frames[ m_frameIndex ]; int pixW = frame->geometry.width(); int pixH = frame->geometry.height(); bool signalsBlocked = m_pagesEdit->signalsBlocked(); m_pagesEdit->blockSignals( true ); m_pagesEdit->setText( QString::number( m_frameIndex + 1 ) ); m_pagesEdit->blockSignals( signalsBlocked ); // if pixmap not inside the Okular::Page we request it and wait for // notifyPixmapChanged call or else we can proceed to pixmap generation if ( !frame->page->hasPixmap( this, ceil(pixW * qApp->devicePixelRatio()), ceil(pixH * qApp->devicePixelRatio()) ) ) { requestPixmaps(); } else { // make the background pixmap generatePage(); } // perform the page opening action, if any if ( m_document->page( m_frameIndex )->pageAction( Okular::Page::Opening ) ) m_document->processAction( m_document->page( m_frameIndex )->pageAction( Okular::Page::Opening ) ); // perform the additional actions of the page's annotations, if any Q_FOREACH ( const Okular::Annotation *annotation, m_document->page( m_frameIndex )->annotations() ) { Okular::Action *action = nullptr; if ( annotation->subType() == Okular::Annotation::AScreen ) action = static_cast( annotation )->additionalAction( Okular::Annotation::PageOpening ); else if ( annotation->subType() == Okular::Annotation::AWidget ) action = static_cast( annotation )->additionalAction( Okular::Annotation::PageOpening ); if ( action ) m_document->processAction( action ); } // start autoplay video playback Q_FOREACH ( VideoWidget *vw, m_frames[ m_frameIndex ]->videoWidgets ) vw->pageEntered(); } } bool PresentationWidget::canUnloadPixmap( int pageNumber ) const { if ( Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Low || Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Normal ) { // can unload all pixmaps except for the currently visible one return pageNumber != m_frameIndex; } else { // can unload all pixmaps except for the currently visible one, previous and next return qAbs(pageNumber - m_frameIndex) <= 1; } } void PresentationWidget::setupActions() { addAction( m_ac->action( QStringLiteral("first_page") ) ); addAction( m_ac->action( QStringLiteral("last_page") ) ); addAction( m_ac->action( QString::fromLocal8Bit(KStandardAction::name( KStandardAction::Prior ) ) ) ); addAction( m_ac->action( QString::fromLocal8Bit(KStandardAction::name( KStandardAction::Next ) ) ) ); addAction( m_ac->action( QString::fromLocal8Bit(KStandardAction::name( KStandardAction::DocumentBack ) ) ) ); addAction( m_ac->action( QString::fromLocal8Bit(KStandardAction::name( KStandardAction::DocumentForward ) ) ) ); QAction *action = m_ac->action( QStringLiteral("switch_blackscreen_mode") ); connect(action, &QAction::toggled, this, &PresentationWidget::toggleBlackScreenMode); action->setEnabled( true ); addAction( action ); } void PresentationWidget::setPlayPauseIcon() { QAction *playPauseAction = m_ac->action( QStringLiteral("presentation_play_pause") ); if ( m_advanceSlides ) { playPauseAction->setIcon( QIcon::fromTheme( QStringLiteral("media-playback-pause") ) ); playPauseAction->setToolTip( i18nc( "For Presentation", "Pause" ) ); } else { playPauseAction->setIcon( QIcon::fromTheme( QStringLiteral("media-playback-start") ) ); playPauseAction->setToolTip( i18nc( "For Presentation", "Play" ) ); } } // bool PresentationWidget::event( QEvent * e ) { if ( e->type() == QEvent::Gesture ) return gestureEvent(static_cast(e)); if ( e->type() == QEvent::ToolTip ) { QHelpEvent * he = (QHelpEvent*)e; QRect r; const Okular::Action * link = getLink( he->x(), he->y(), &r ); if ( link ) { QString tip = link->actionTip(); if ( !tip.isEmpty() ) QToolTip::showText( he->globalPos(), tip, this, r ); } e->accept(); return true; } else // do not stop the event return QWidget::event( e ); } bool PresentationWidget::gestureEvent( QGestureEvent * event ) { // Swiping left or right on a touch screen will go to the previous or next slide, respectively. // The precise gesture is the standard Qt swipe: with three(!) fingers. if (QGesture *swipe = event->gesture(Qt::SwipeGesture)) { QSwipeGesture * swipeEvent = static_cast(swipe); if (swipeEvent->state() == Qt::GestureFinished) { if (swipeEvent->horizontalDirection() == QSwipeGesture::Left) { slotPrevPage(); event->accept(); return true; } if (swipeEvent->horizontalDirection() == QSwipeGesture::Right) { slotNextPage(); event->accept(); return true; } } } return false; } void PresentationWidget::keyPressEvent( QKeyEvent * e ) { if ( !m_isSetup ) return; switch ( e->key() ) { case Qt::Key_Left: case Qt::Key_Backspace: case Qt::Key_PageUp: case Qt::Key_Up: slotPrevPage(); break; case Qt::Key_Right: case Qt::Key_Space: case Qt::Key_PageDown: case Qt::Key_Down: slotNextPage(); break; case Qt::Key_Home: slotFirstPage(); break; case Qt::Key_End: slotLastPage(); break; case Qt::Key_Escape: if ( !m_topBar->isHidden() ) showTopBar( false ); else close(); break; } } void PresentationWidget::wheelEvent( QWheelEvent * e ) { if ( !m_isSetup ) return; // performance note: don't remove the clipping int div = e->delta() / 120; if ( div > 0 ) { if ( div > 3 ) div = 3; while ( div-- ) slotPrevPage(); } else if ( div < 0 ) { if ( div < -3 ) div = -3; while ( div++ ) slotNextPage(); } } void PresentationWidget::mousePressEvent( QMouseEvent * e ) { if ( !m_isSetup ) return; if ( m_drawingEngine ) { QRect r = routeMouseDrawingEvent( e ); if ( r.isValid() ) { m_drawingRect |= r.translated( m_frames[ m_frameIndex ]->geometry.topLeft() ); update( m_drawingRect ); } return; } // pressing left button if ( e->button() == Qt::LeftButton ) { // if pressing on a link, skip other checks if ( ( m_pressedLink = getLink( e->x(), e->y() ) ) ) return; const Okular::Annotation *annotation = getAnnotation( e->x(), e->y() ); if ( annotation ) { if ( annotation->subType() == Okular::Annotation::AMovie ) { const Okular::MovieAnnotation *movieAnnotation = static_cast( annotation ); VideoWidget *vw = m_frames[ m_frameIndex ]->videoWidgets.value( movieAnnotation->movie() ); vw->show(); vw->play(); return; } else if ( annotation->subType() == Okular::Annotation::ARichMedia ) { const Okular::RichMediaAnnotation *richMediaAnnotation = static_cast( annotation ); VideoWidget *vw = m_frames[ m_frameIndex ]->videoWidgets.value( richMediaAnnotation->movie() ); vw->show(); vw->play(); return; } else if ( annotation->subType() == Okular::Annotation::AScreen ) { m_document->processAction( static_cast( annotation )->action() ); return; } } // handle clicking on top-right overlay if ( !( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden ) && m_overlayGeometry.contains( e->pos() ) ) { overlayClick( e->pos() ); return; } // Actual mouse press events always lead to the next page page if ( e->source() == Qt::MouseEventNotSynthesized ) { m_goToNextPageOnRelease = true; } // Touch events may lead to the previous or next page else if ( Okular::Settings::slidesTapNavigation() != Okular::Settings::EnumSlidesTapNavigation::Disabled ) { switch ( Okular::Settings::slidesTapNavigation() ) { case Okular::Settings::EnumSlidesTapNavigation::ForwardBackward: { if ( e->x() < ( geometry().width()/2 ) ) { m_goToPreviousPageOnRelease = true; } else { m_goToNextPageOnRelease = true; } break; } case Okular::Settings::EnumSlidesTapNavigation::Forward: { m_goToNextPageOnRelease = true; break; } case Okular::Settings::EnumSlidesTapNavigation::Disabled: { // Do Nothing } } } } // pressing forward button else if ( e->button() == Qt::ForwardButton ) { m_goToNextPageOnRelease = true; } // pressing right or backward button else if ( e->button() == Qt::RightButton || e->button() == Qt::BackButton ) m_goToPreviousPageOnRelease = true; } void PresentationWidget::mouseReleaseEvent( QMouseEvent * e ) { if ( m_drawingEngine ) { routeMouseDrawingEvent( e ); return; } // if releasing on the same link we pressed over, execute it if ( m_pressedLink && e->button() == Qt::LeftButton ) { const Okular::Action * link = getLink( e->x(), e->y() ); if ( link == m_pressedLink ) m_document->processAction( link ); m_pressedLink = nullptr; } if ( m_goToPreviousPageOnRelease ) { slotPrevPage(); m_goToPreviousPageOnRelease = false; } if ( m_goToNextPageOnRelease ) { slotNextPage(); m_goToNextPageOnRelease = false; } } void PresentationWidget::mouseMoveEvent( QMouseEvent * e ) { // safety check if ( !m_isSetup ) return; // update cursor and tooltip if hovering a link if ( !m_drawingEngine && Okular::Settings::slidesCursor() != Okular::Settings::EnumSlidesCursor::Hidden ) testCursorOnLink( e->x(), e->y() ); if ( !m_topBar->isHidden() ) { // hide a shown bar when exiting the area if ( e->y() > ( m_topBar->height() + 1 ) ) { showTopBar( false ); setFocus( Qt::OtherFocusReason ); } } else { if ( m_drawingEngine && e->buttons() != Qt::NoButton ) { QRect r = routeMouseDrawingEvent( e ); if ( r.isValid() ) { m_drawingRect |= r.translated( m_frames[ m_frameIndex ]->geometry.topLeft() ); update( m_drawingRect ); } } else { // show the bar if reaching top 2 pixels if ( e->y() <= 1 ) showTopBar( true ); // handle "dragging the wheel" if clicking on its geometry else if ( ( QApplication::mouseButtons() & Qt::LeftButton ) && m_overlayGeometry.contains( e->pos() ) ) overlayClick( e->pos() ); } } } void PresentationWidget::paintEvent( QPaintEvent * pe ) { qreal dpr = devicePixelRatioF(); if ( m_inBlackScreenMode ) { QPainter painter( this ); painter.fillRect( pe->rect(), Qt::black ); return; } if ( !m_isSetup ) { m_width = width(); m_height = height(); connect(m_document, &Okular::Document::linkFind, this, &PresentationWidget::slotFind); // register this observer in document. events will come immediately m_document->addObserver( this ); // show summary if requested if ( Okular::Settings::slidesShowSummary() ) generatePage(); } // check painting rect consistency QRect r = pe->rect().intersected( QRect( QPoint( 0, 0 ), geometry().size() ) ); if ( r.isNull() ) return; if ( m_lastRenderedPixmap.isNull() ) { QPainter painter( this ); painter.fillRect( pe->rect(), Okular::Settings::slidesBackgroundColor() ); return; } // blit the pixmap to the screen QVector allRects = pe->region().rects(); uint numRects = allRects.count(); QPainter painter( this ); for ( uint i = 0; i < numRects; i++ ) { const QRect & r = allRects[i]; if ( !r.isValid() ) continue; #ifdef ENABLE_PROGRESS_OVERLAY const QRect dR(QRectF(r.x() * dpr, r.y() * dpr, r.width() * dpr, r.height() * dpr).toAlignedRect()); if ( Okular::Settings::slidesShowProgress() && r.intersects( m_overlayGeometry ) ) { // backbuffer the overlay operation QPixmap backPixmap( dR.size() ); backPixmap.setDevicePixelRatio( dpr ); QPainter pixPainter( &backPixmap ); // first draw the background on the backbuffer pixPainter.drawPixmap( QPoint(0,0), m_lastRenderedPixmap, dR ); // then blend the overlay (a piece of) over the background QRect ovr = m_overlayGeometry.intersected( r ); pixPainter.drawPixmap( (ovr.left() - r.left()), (ovr.top() - r.top()), m_lastRenderedOverlay, (ovr.left() - m_overlayGeometry.left()) * dpr, (ovr.top() - m_overlayGeometry.top()) * dpr, ovr.width() * dpr, ovr.height() * dpr ); // finally blit the pixmap to the screen pixPainter.end(); const QRect backPixmapRect = backPixmap.rect(); const QRect dBackPixmapRect(QRectF(backPixmapRect.x() * dpr, backPixmapRect.y() * dpr, backPixmapRect.width() * dpr, backPixmapRect.height() * dpr).toAlignedRect()); painter.drawPixmap( r.topLeft(), backPixmap, dBackPixmapRect ); } else #endif // copy the rendered pixmap to the screen painter.drawPixmap( r.topLeft(), m_lastRenderedPixmap, dR ); } // paint drawings if ( m_frameIndex != -1 ) { painter.save(); const QRect & geom = m_frames[ m_frameIndex ]->geometry; QPixmap pm( geom.size() ); pm.fill( Qt::transparent ); QPainter pmPainter( &pm ); pmPainter.setRenderHints( QPainter::Antialiasing ); foreach ( const SmoothPath &drawing, m_frames[ m_frameIndex ]->drawings ) drawing.paint( &pmPainter, geom.width(), geom.height() ); if ( m_drawingEngine && m_drawingRect.intersects( pe->rect() ) ) m_drawingEngine->paint( &pmPainter, geom.width(), geom.height(), m_drawingRect.intersected( pe->rect() ) ); painter.setRenderHints( QPainter::Antialiasing ); painter.drawPixmap( geom.topLeft() , pm ); painter.restore(); } painter.end(); } void PresentationWidget::resizeEvent( QResizeEvent *re ) { // qCDebug(OkularUiDebug) << re->oldSize() << "=>" << re->size(); if ( re->oldSize() == QSize( -1, -1 ) ) return; m_screen = QApplication::desktop()->screenNumber( this ); applyNewScreenSize( re->oldSize() ); } void PresentationWidget::leaveEvent( QEvent * e ) { Q_UNUSED( e ) if ( !m_topBar->isHidden() ) { showTopBar( false ); } } // const void * PresentationWidget::getObjectRect( Okular::ObjectRect::ObjectType type, int x, int y, QRect * geometry ) const { // no links on invalid pages if ( geometry && !geometry->isNull() ) geometry->setRect( 0, 0, 0, 0 ); if ( m_frameIndex < 0 || m_frameIndex >= (int)m_frames.size() ) return nullptr; // get frame, page and geometry const PresentationFrame * frame = m_frames[ m_frameIndex ]; const Okular::Page * page = frame->page; const QRect & frameGeometry = frame->geometry; // compute normalized x and y double nx = (double)(x - frameGeometry.left()) / (double)frameGeometry.width(); double ny = (double)(y - frameGeometry.top()) / (double)frameGeometry.height(); // no links outside the pages if ( nx < 0 || nx > 1 || ny < 0 || ny > 1 ) return nullptr; // check if 1) there is an object and 2) it's a link const QRect d = QApplication::desktop()->screenGeometry( m_screen ); const Okular::ObjectRect * object = page->objectRect( type, nx, ny, d.width(), d.height() ); if ( !object ) return nullptr; // compute link geometry if destination rect present if ( geometry ) { *geometry = object->boundingRect( frameGeometry.width(), frameGeometry.height() ); geometry->translate( frameGeometry.left(), frameGeometry.top() ); } // return the link pointer return object->object(); } const Okular::Action * PresentationWidget::getLink( int x, int y, QRect * geometry ) const { return reinterpret_cast( getObjectRect( Okular::ObjectRect::Action, x, y, geometry ) ); } const Okular::Annotation * PresentationWidget::getAnnotation( int x, int y, QRect * geometry ) const { return reinterpret_cast( getObjectRect( Okular::ObjectRect::OAnnotation, x, y, geometry ) ); } void PresentationWidget::testCursorOnLink( int x, int y ) { const Okular::Action * link = getLink( x, y, nullptr ); const Okular::Annotation *annotation = getAnnotation( x, y, nullptr ); const bool needsHandCursor = ( ( link != nullptr ) || ( ( annotation != nullptr ) && ( annotation->subType() == Okular::Annotation::AMovie ) ) || ( ( annotation != nullptr ) && ( annotation->subType() == Okular::Annotation::ARichMedia ) ) || ( ( annotation != nullptr ) && ( annotation->subType() == Okular::Annotation::AScreen ) && ( GuiUtils::renditionMovieFromScreenAnnotation( static_cast< const Okular::ScreenAnnotation * >( annotation ) ) != nullptr ) ) ); // only react on changes (in/out from a link) if ( ( needsHandCursor && !m_handCursor ) || ( !needsHandCursor && m_handCursor ) ) { // change cursor shape m_handCursor = needsHandCursor; setCursor( QCursor( m_handCursor ? Qt::PointingHandCursor : Qt::ArrowCursor ) ); } } void PresentationWidget::overlayClick( const QPoint & position ) { // clicking the progress indicator int xPos = position.x() - m_overlayGeometry.x() - m_overlayGeometry.width() / 2, yPos = m_overlayGeometry.height() / 2 - position.y(); if ( !xPos && !yPos ) return; // compute angle relative to indicator (note coord transformation) float angle = 0.5 + 0.5 * atan2( (double)-xPos, (double)-yPos ) / M_PI; int pageIndex = (int)( angle * ( m_frames.count() - 1 ) + 0.5 ); // go to selected page changePage( pageIndex ); } void PresentationWidget::changePage( int newPage ) { if ( m_showSummaryView ) { m_showSummaryView = false; m_frameIndex = -1; return; } if ( m_frameIndex == newPage ) return; // switch to newPage m_document->setViewportPage( newPage, this ); if ( (Okular::Settings::slidesShowSummary() && !m_showSummaryView) || m_frameIndex == -1 ) notifyCurrentPageChanged( -1, newPage ); } void PresentationWidget::generatePage( bool disableTransition ) { if ( m_lastRenderedPixmap.isNull() ) { qreal dpr = qApp->devicePixelRatio(); m_lastRenderedPixmap = QPixmap( m_width * dpr, m_height * dpr ); m_lastRenderedPixmap.setDevicePixelRatio(dpr); m_previousPagePixmap = QPixmap(); } else { m_previousPagePixmap = m_lastRenderedPixmap; } // opens the painter over the pixmap QPainter pixmapPainter; pixmapPainter.begin( &m_lastRenderedPixmap ); // generate welcome page if ( m_frameIndex == -1 ) generateIntroPage( pixmapPainter ); // generate a normal pixmap with extended margin filling if ( m_frameIndex >= 0 && m_frameIndex < (int)m_document->pages() ) generateContentsPage( m_frameIndex, pixmapPainter ); pixmapPainter.end(); // generate the top-right corner overlay #ifdef ENABLE_PROGRESS_OVERLAY if ( Okular::Settings::slidesShowProgress() && m_frameIndex != -1 ) generateOverlay(); #endif // start transition on pages that have one if ( !disableTransition && Okular::Settings::slidesTransitionsEnabled() ) { const Okular::PageTransition * transition = m_frameIndex != -1 ? m_frames[ m_frameIndex ]->page->transition() : nullptr; if ( transition ) initTransition( transition ); else { Okular::PageTransition trans = defaultTransition(); initTransition( &trans ); } } else { Okular::PageTransition trans = defaultTransition( Okular::Settings::EnumSlidesTransition::Replace ); initTransition( &trans ); } // update cursor + tooltip if ( !m_drawingEngine && Okular::Settings::slidesCursor() != Okular::Settings::EnumSlidesCursor::Hidden ) { QPoint p = mapFromGlobal( QCursor::pos() ); testCursorOnLink( p.x(), p.y() ); } } void PresentationWidget::generateIntroPage( QPainter & p ) { qreal dpr = qApp->devicePixelRatio(); // use a vertical gray gradient background int blend1 = m_height / 10, blend2 = 9 * m_height / 10; int baseTint = QColor(Qt::gray).red(); for ( int i = 0; i < m_height; i++ ) { int k = baseTint; if ( i < blend1 ) k -= (int)( baseTint * (i-blend1)*(i-blend1) / (float)(blend1 * blend1) ); if ( i > blend2 ) k += (int)( (255-baseTint) * (i-blend2)*(i-blend2) / (float)(blend1 * blend1) ); p.fillRect( 0, i, m_width, 1, QColor( k, k, k ) ); } // draw okular logo in the four corners QPixmap logo = DesktopIcon( QStringLiteral("okular"), 64 * dpr ); logo.setDevicePixelRatio( dpr ); if ( !logo.isNull() ) { p.drawPixmap( 5, 5, logo ); p.drawPixmap( m_width - 5 - logo.width(), 5, logo ); p.drawPixmap( m_width - 5 - logo.width(), m_height - 5 - logo.height(), logo ); p.drawPixmap( 5, m_height - 5 - logo.height(), logo ); } // draw metadata text (the last line is 'click to begin') int strNum = m_metaStrings.count(), strHeight = m_height / ( strNum + 4 ), fontHeight = 2 * strHeight / 3; QFont font( p.font() ); font.setPixelSize( fontHeight ); QFontMetrics metrics( font ); for ( int i = 0; i < strNum; i++ ) { // set a font to fit text width float wScale = (float)metrics.boundingRect( m_metaStrings[i] ).width() / (float)m_width; QFont f( font ); if ( wScale > 1.0 ) f.setPixelSize( (int)( (float)fontHeight / (float)wScale ) ); p.setFont( f ); // text shadow p.setPen( Qt::darkGray ); p.drawText( 2, m_height / 4 + strHeight * i + 2, m_width, strHeight, Qt::AlignHCenter | Qt::AlignVCenter, m_metaStrings[i] ); // text body p.setPen( 128 + (127 * i) / strNum ); p.drawText( 0, m_height / 4 + strHeight * i, m_width, strHeight, Qt::AlignHCenter | Qt::AlignVCenter, m_metaStrings[i] ); } } void PresentationWidget::generateContentsPage( int pageNum, QPainter & p ) { PresentationFrame * frame = m_frames[ pageNum ]; // translate painter and contents rect QRect geom( frame->geometry ); p.translate( geom.left(), geom.top() ); geom.translate( -geom.left(), -geom.top() ); // draw the page using the shared PagePainter class int flags = PagePainter::Accessibility | PagePainter::Highlights | PagePainter::Annotations; PagePainter::paintPageOnPainter( &p, frame->page, this, flags, geom.width(), geom.height(), geom ); // restore painter p.translate( -frame->geometry.left(), -frame->geometry.top() ); // fill unpainted areas with background color QRegion unpainted( QRect( 0, 0, m_width, m_height ) ); QVector rects = unpainted.subtracted( frame->geometry ).rects(); for ( int i = 0; i < rects.count(); i++ ) { const QRect & r = rects[i]; p.fillRect( r, Okular::Settings::slidesBackgroundColor() ); } } // from Arthur - Qt4 - (is defined elsewhere as 'qt_div_255' to not break final compilation) inline int qt_div255(int x) { return (x + (x>>8) + 0x80) >> 8; } void PresentationWidget::generateOverlay() { #ifdef ENABLE_PROGRESS_OVERLAY qreal dpr = qApp->devicePixelRatio(); // calculate overlay geometry and resize pixmap if needed int side = m_width / 16; m_overlayGeometry.setRect( m_width - side - 4, 4, side, side ); // note: to get a sort of antialiasing, we render the pixmap double sized // and the resulting image is smoothly scaled down. So here we open a // painter on the double sized pixmap. side *= 2; QPixmap doublePixmap( side * dpr, side * dpr ); doublePixmap.setDevicePixelRatio( dpr ); doublePixmap.fill( Qt::black ); QPainter pixmapPainter( &doublePixmap ); pixmapPainter.setRenderHints( QPainter::Antialiasing ); // draw PIE SLICES in blue levels (the levels will then be the alpha component) int pages = m_document->pages(); if ( pages > 28 ) { // draw continuous slices int degrees = (int)( 360 * (float)(m_frameIndex + 1) / (float)pages ); pixmapPainter.setPen( 0x05 ); pixmapPainter.setBrush( QColor( 0x40 ) ); pixmapPainter.drawPie( 2, 2, side - 4, side - 4, 90*16, (360-degrees)*16 ); pixmapPainter.setPen( 0x40 ); pixmapPainter.setBrush( QColor( 0xF0 ) ); pixmapPainter.drawPie( 2, 2, side - 4, side - 4, 90*16, -degrees*16 ); } else { // draw discrete slices float oldCoord = -90; for ( int i = 0; i < pages; i++ ) { float newCoord = -90 + 360 * (float)(i + 1) / (float)pages; pixmapPainter.setPen( i <= m_frameIndex ? 0x40 : 0x05 ); pixmapPainter.setBrush( QColor( i <= m_frameIndex ? 0xF0 : 0x40 ) ); pixmapPainter.drawPie( 2, 2, side - 4, side - 4, (int)( -16*(oldCoord + 1) ), (int)( -16*(newCoord - (oldCoord + 2)) ) ); oldCoord = newCoord; } } int circleOut = side / 4; pixmapPainter.setPen( Qt::black ); pixmapPainter.setBrush( Qt::black ); pixmapPainter.drawEllipse( circleOut, circleOut, side - 2*circleOut, side - 2*circleOut ); // draw TEXT using maximum opacity QFont f( pixmapPainter.font() ); f.setPixelSize( side / 4 ); pixmapPainter.setFont( f ); pixmapPainter.setPen( 0xFF ); // use a little offset to prettify output pixmapPainter.drawText( 2, 2, side, side, Qt::AlignCenter, QString::number( m_frameIndex + 1 ) ); // end drawing pixmap and halve image pixmapPainter.end(); QImage image( doublePixmap.toImage().scaled( (side / 2) * dpr, (side / 2) * dpr, Qt::IgnoreAspectRatio, Qt::SmoothTransformation ) ); image.setDevicePixelRatio( dpr ); image = image.convertToFormat( QImage::Format_ARGB32 ); image.setDevicePixelRatio( dpr ); // draw circular shadow using the same technique doublePixmap.fill( Qt::black ); pixmapPainter.begin( &doublePixmap ); pixmapPainter.setPen( 0x40 ); pixmapPainter.setBrush( QColor( 0x80 ) ); pixmapPainter.drawEllipse( 0, 0, side, side ); pixmapPainter.end(); QImage shadow( doublePixmap.toImage().scaled( (side / 2) * dpr, (side / 2) * dpr, Qt::IgnoreAspectRatio, Qt::SmoothTransformation ) ); shadow.setDevicePixelRatio( dpr ); // generate a 2 colors pixmap using mixing shadow (made with highlight color) // and image (made with highlightedText color) QPalette pal = palette(); QColor color = pal.color( QPalette::Active, QPalette::HighlightedText ); int red = color.red(), green = color.green(), blue = color.blue(); color = pal.color( QPalette::Active, QPalette::Highlight ); int sRed = color.red(), sGreen = color.green(), sBlue = color.blue(); // pointers unsigned int * data = (unsigned int *)image.bits(), * shadowData = (unsigned int *)shadow.bits(), pixels = image.width() * image.height(); // cache data (reduce computation time to 26%!) int c1 = -1, c2 = -1, cR = 0, cG = 0, cB = 0, cA = 0; // foreach pixel for( unsigned int i = 0; i < pixels; ++i ) { // alpha for shadow and image int shadowAlpha = shadowData[i] & 0xFF, srcAlpha = data[i] & 0xFF; // cache values if ( srcAlpha != c1 || shadowAlpha != c2 ) { c1 = srcAlpha; c2 = shadowAlpha; // fuse color components and alpha value of image over shadow data[i] = qRgba( cR = qt_div255( srcAlpha * red + (255 - srcAlpha) * sRed ), cG = qt_div255( srcAlpha * green + (255 - srcAlpha) * sGreen ), cB = qt_div255( srcAlpha * blue + (255 - srcAlpha) * sBlue ), cA = qt_div255( srcAlpha * srcAlpha + (255 - srcAlpha) * shadowAlpha ) ); } else data[i] = qRgba( cR, cG, cB, cA ); } m_lastRenderedOverlay = QPixmap::fromImage( image ); m_lastRenderedOverlay.setDevicePixelRatio( dpr ); // start the autohide timer //repaint( m_overlayGeometry ); // toggle with next line update( m_overlayGeometry ); m_overlayHideTimer->start( 2500 ); #endif } QRect PresentationWidget::routeMouseDrawingEvent( QMouseEvent * e ) { if ( m_frameIndex == -1 ) // Can't draw on the summary page return QRect(); const QRect & geom = m_frames[ m_frameIndex ]->geometry; const Okular::Page * page = m_frames[ m_frameIndex ]->page; AnnotatorEngine::EventType eventType; AnnotatorEngine::Button button; // figure out the event type and button AnnotatorEngine::decodeEvent( e, &eventType, &button ); static bool hasclicked = false; if ( eventType == AnnotatorEngine::Press ) hasclicked = true; double nX = ( (double)e->x() - (double)geom.left() ) / (double)geom.width(); double nY = ( (double)e->y() - (double)geom.top() ) / (double)geom.height(); QRect ret; bool isInside = nX >= 0 && nX < 1 && nY >= 0 && nY < 1; if ( hasclicked && !isInside ) { // Fake a move to the last border pos nX = qBound(0., nX, 1.); nY = qBound(0., nY, 1.); m_drawingEngine->event( AnnotatorEngine::Move, button, nX, nY, geom.width(), geom.height(), page ); // Fake a release in the following lines eventType = AnnotatorEngine::Release; isInside = true; } else if ( !hasclicked && isInside ) { // we're coming from the outside, pretend we started clicking at the closest border if ( nX < ( 1 - nX ) && nX < nY && nX < ( 1 - nY ) ) nX = 0; else if ( nY < ( 1 - nY ) && nY < nX && nY < ( 1 - nX ) ) nY = 0; else if ( ( 1 - nX ) < nX && ( 1 - nX ) < nY && ( 1 - nX ) < ( 1 - nY ) ) nX = 1; else nY = 1; hasclicked = true; eventType = AnnotatorEngine::Press; } if ( hasclicked && isInside ) { ret = m_drawingEngine->event( eventType, button, nX, nY, geom.width(), geom.height(), page ); } if ( eventType == AnnotatorEngine::Release ) { hasclicked = false; } if ( m_drawingEngine->creationCompleted() ) { // add drawing to current page m_frames[ m_frameIndex ]->drawings << m_drawingEngine->endSmoothPath(); // manually disable and re-enable the pencil mode, so we can do // cleaning of the actual drawer and create a new one just after // that - that gives continuous drawing slotChangeDrawingToolEngine( QDomElement() ); slotChangeDrawingToolEngine( m_currentDrawingToolElement ); // schedule repaint update(); } return ret; } void PresentationWidget::startAutoChangeTimer() { double pageDuration = m_frameIndex >= 0 && m_frameIndex < (int)m_frames.count() ? m_frames[ m_frameIndex ]->page->duration() : -1; if ( m_advanceSlides || pageDuration >= 0.0 ) { double secs; if ( pageDuration < 0.0 ) secs = Okular::SettingsCore::slidesAdvanceTime(); else if ( m_advanceSlides ) secs = qMin( pageDuration, Okular::SettingsCore::slidesAdvanceTime() ); else secs = pageDuration; m_nextPageTimer->start( (int)( secs * 1000 ) ); } } void PresentationWidget::recalcGeometry() { QDesktopWidget *desktop = QApplication::desktop(); const int preferenceScreen = Okular::Settings::slidesScreen(); int screen = 0; if ( preferenceScreen == -2 ) { screen = desktop->screenNumber( m_parentWidget ); } else if ( preferenceScreen == -1 ) { screen = desktop->primaryScreen(); } else if ( preferenceScreen >= 0 && preferenceScreen < desktop->numScreens() ) { screen = preferenceScreen; } else { screen = desktop->screenNumber( m_parentWidget ); Okular::Settings::setSlidesScreen( -2 ); } const QRect screenGeom = desktop->screenGeometry( screen ); // qCDebug(OkularUiDebug) << screen << "=>" << screenGeom; m_screen = screen; setGeometry( screenGeom ); } void PresentationWidget::repositionContent() { const QRect ourGeom = geometry(); // tool bar height in pixels, make it large enough to hold the text fields with the page numbers const int toolBarHeight = m_pagesEdit->height() * 1.5; m_topBar->setGeometry( 0, 0, ourGeom.width(), toolBarHeight ); m_topBar->setIconSize( QSize( toolBarHeight * 0.75, toolBarHeight * 0.75 ) ); } void PresentationWidget::requestPixmaps() { PresentationFrame * frame = m_frames[ m_frameIndex ]; int pixW = frame->geometry.width(); int pixH = frame->geometry.height(); // operation will take long: set busy cursor QApplication::setOverrideCursor( QCursor( Qt::BusyCursor ) ); // request the pixmap QLinkedList< Okular::PixmapRequest * > requests; requests.push_back( new Okular::PixmapRequest( this, m_frameIndex, pixW, pixH, PRESENTATION_PRIO, Okular::PixmapRequest::NoFeature ) ); // restore cursor QApplication::restoreOverrideCursor(); // ask for next and previous page if not in low memory usage setting if ( Okular::SettingsCore::memoryLevel() != Okular::SettingsCore::EnumMemoryLevel::Low ) { int pagesToPreload = 1; // If greedy, preload everything if (Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Greedy) pagesToPreload = (int)m_document->pages(); Okular::PixmapRequest::PixmapRequestFeatures requestFeatures = Okular::PixmapRequest::Preload; requestFeatures |= Okular::PixmapRequest::Asynchronous; for( int j = 1; j <= pagesToPreload; j++ ) { int tailRequest = m_frameIndex + j; if ( tailRequest < (int)m_document->pages() ) { PresentationFrame *nextFrame = m_frames[ tailRequest ]; pixW = nextFrame->geometry.width(); pixH = nextFrame->geometry.height(); if ( !nextFrame->page->hasPixmap( this, pixW, pixH ) ) requests.push_back( new Okular::PixmapRequest( this, tailRequest, pixW, pixH, PRESENTATION_PRELOAD_PRIO, requestFeatures ) ); } int headRequest = m_frameIndex - j; if ( headRequest >= 0 ) { PresentationFrame *prevFrame = m_frames[ headRequest ]; pixW = prevFrame->geometry.width(); pixH = prevFrame->geometry.height(); if ( !prevFrame->page->hasPixmap( this, pixW, pixH ) ) requests.push_back( new Okular::PixmapRequest( this, headRequest, pixW, pixH, PRESENTATION_PRELOAD_PRIO, requestFeatures ) ); } // stop if we've already reached both ends of the document if ( headRequest < 0 && tailRequest >= (int)m_document->pages() ) break; } } m_document->requestPixmaps( requests ); } void PresentationWidget::slotNextPage() { int nextIndex = m_frameIndex + 1; // loop when configured if ( nextIndex == m_frames.count() && Okular::Settings::slidesLoop() ) nextIndex = 0; if ( nextIndex < m_frames.count() ) { // go to next page changePage( nextIndex ); // auto advance to the next page if set startAutoChangeTimer(); } else { #ifdef ENABLE_PROGRESS_OVERLAY if ( Okular::Settings::slidesShowProgress() ) generateOverlay(); #endif if ( m_transitionTimer->isActive() ) { m_transitionTimer->stop(); m_lastRenderedPixmap = m_currentPagePixmap; update(); } } // we need the setFocus() call here to let KCursor::autoHide() work correctly setFocus(); } void PresentationWidget::slotPrevPage() { if ( m_frameIndex > 0 ) { // go to previous page changePage( m_frameIndex - 1 ); // auto advance to the next page if set startAutoChangeTimer(); } else { #ifdef ENABLE_PROGRESS_OVERLAY if ( Okular::Settings::slidesShowProgress() ) generateOverlay(); #endif if ( m_transitionTimer->isActive() ) { m_transitionTimer->stop(); m_lastRenderedPixmap = m_currentPagePixmap; update(); } } } void PresentationWidget::slotFirstPage() { changePage( 0 ); } void PresentationWidget::slotLastPage() { changePage( (int)m_frames.count() - 1 ); } void PresentationWidget::slotHideOverlay() { QRect geom( m_overlayGeometry ); m_overlayGeometry.setCoords( 0, 0, -1, -1 ); update( geom ); } void PresentationWidget::slotTransitionStep() { switch( m_currentTransition.type() ) { case Okular::PageTransition::Fade: { QPainter pixmapPainter; m_currentPixmapOpacity += 1.0 / m_transitionSteps; m_lastRenderedPixmap = QPixmap( m_lastRenderedPixmap.size() ); m_lastRenderedPixmap.setDevicePixelRatio( qApp->devicePixelRatio() ); m_lastRenderedPixmap.fill( Qt::transparent ); pixmapPainter.begin( &m_lastRenderedPixmap ); pixmapPainter.setCompositionMode( QPainter::CompositionMode_Source ); pixmapPainter.setOpacity( 1 - m_currentPixmapOpacity ); pixmapPainter.drawPixmap( 0, 0, m_previousPagePixmap ); pixmapPainter.setOpacity( m_currentPixmapOpacity ); pixmapPainter.drawPixmap( 0, 0, m_currentPagePixmap ); update(); if( m_currentPixmapOpacity >= 1 ) return; } break; default: { if ( m_transitionRects.empty() ) { // it's better to fix the transition to cover the whole screen than // enabling the following line that wastes cpu for nothing //update(); return; } for ( int i = 0; i < m_transitionMul && !m_transitionRects.empty(); i++ ) { update( m_transitionRects.first() ); m_transitionRects.pop_front(); } } break; } m_transitionTimer->start( m_transitionDelay ); } void PresentationWidget::slotDelayedEvents() { recalcGeometry(); repositionContent(); if ( m_screenSelect ) { m_screenSelect->setCurrentItem( m_screen ); connect( m_screenSelect->selectableActionGroup(), &QActionGroup::triggered, this, &PresentationWidget::chooseScreen ); } // show widget and take control show(); setWindowState( windowState() | Qt::WindowFullScreen ); connect( QApplication::desktop(), &QDesktopWidget::resized, this, &PresentationWidget::screenResized ); // inform user on how to exit from presentation mode KMessageBox::information( this, i18n("There are two ways of exiting presentation mode, you can press either ESC key or click with the quit button that appears when placing the mouse in the top-right corner. Of course you can cycle windows (Alt+TAB by default)"), QString(), QStringLiteral("presentationInfo") ); } void PresentationWidget::slotPageChanged() { bool ok = true; int p = m_pagesEdit->text().toInt( &ok ); if ( !ok ) return; changePage( p - 1 ); } void PresentationWidget::slotChangeDrawingToolEngine( const QDomElement &element ) { if ( element.isNull() ) { delete m_drawingEngine; m_drawingEngine = nullptr; m_drawingRect = QRect(); setCursor( Qt::ArrowCursor ); } else { m_drawingEngine = new SmoothPathEngine( element ); setCursor( QCursor( QPixmap( QStringLiteral("pencil") ), Qt::ArrowCursor ) ); m_currentDrawingToolElement = element; } } void PresentationWidget::slotAddDrawingToolActions() { DrawingToolActions *drawingToolActions = qobject_cast(sender()); foreach(QAction *action, drawingToolActions->actions()) { action->setEnabled( true ); m_topBar->addAction( action ); addAction( action ); } } void PresentationWidget::clearDrawings() { if ( m_frameIndex != -1 ) m_frames[ m_frameIndex ]->drawings.clear(); update(); } void PresentationWidget::screenResized( int screen ) { // we can ignore if a screen was resized in the case the screen is not // where we are on if ( screen != m_screen ) return; setScreen( screen ); } void PresentationWidget::chooseScreen( QAction *act ) { if ( !act || act->data().type() != QVariant::Int ) return; const int newScreen = act->data().toInt(); setScreen( newScreen ); } void PresentationWidget::toggleBlackScreenMode( bool ) { m_inBlackScreenMode = !m_inBlackScreenMode; update(); } void PresentationWidget::setScreen( int newScreen ) { const QRect screenGeom = QApplication::desktop()->screenGeometry( newScreen ); const QSize oldSize = size(); // qCDebug(OkularUiDebug) << newScreen << "=>" << screenGeom; m_screen = newScreen; setGeometry( screenGeom ); applyNewScreenSize( oldSize ); } void PresentationWidget::applyNewScreenSize( const QSize & oldSize ) { repositionContent(); // if by chance the new screen has the same resolution of the previous, // do not invalidate pixmaps and such.. if ( size() == oldSize ) return; m_width = width(); m_height = height(); // update the frames const float screenRatio = (float)m_height / (float)m_width; for ( PresentationFrame * frame : qAsConst( m_frames ) ) { frame->recalcGeometry( m_width, m_height, screenRatio ); } if ( m_frameIndex != -1 ) { // ugliness alarm! const_cast< Okular::Page * >( m_frames[ m_frameIndex ]->page )->deletePixmap( this ); // force the regeneration of the pixmap m_lastRenderedPixmap = QPixmap(); m_blockNotifications = true; requestPixmaps(); m_blockNotifications = false; } if ( m_transitionTimer->isActive() ) { m_transitionTimer->stop(); } generatePage( true /* no transitions */ ); } void PresentationWidget::inhibitPowerManagement() { #ifdef Q_OS_LINUX QString reason = i18nc( "Reason for inhibiting the screensaver activation, when the presentation mode is active", "Giving a presentation" ); if (!m_screenInhibitCookie) { - QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.ScreenSaver", "/ScreenSaver", - "org.freedesktop.ScreenSaver", "Inhibit"); + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), + QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("Inhibit")); message << QCoreApplication::applicationName(); message << reason; QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(message); reply.waitForFinished(); if (reply.isValid()) { m_screenInhibitCookie = reply.value(); qCDebug(OkularUiDebug) << "Screen inhibition cookie" << m_screenInhibitCookie; } else { qCWarning(OkularUiDebug) << "Unable to inhibit screensaver" << reply.error(); } } if (m_sleepInhibitFd != -1) { QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.login1"), QStringLiteral("/org/freedesktop/login1"), QStringLiteral("org.freedesktop.login1.Manager"), QStringLiteral("Inhibit") ); message << QStringLiteral("sleep"); message << QCoreApplication::applicationName(); message << reason; message << QStringLiteral("block"); QDBusPendingReply reply = QDBusConnection::systemBus().asyncCall(message); reply.waitForFinished(); if (reply.isValid()) { m_sleepInhibitFd = dup(reply.value().fileDescriptor()); } else { qCWarning(OkularUiDebug) << "Unable to inhibit sleep" << reply.error(); } } #endif } void PresentationWidget::allowPowerManagement() { #ifdef Q_OS_LINUX if (m_sleepInhibitFd != -1) { ::close(m_sleepInhibitFd); m_sleepInhibitFd = -1; } if (m_screenInhibitCookie) { - QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.ScreenSaver", "/ScreenSaver", - "org.freedesktop.ScreenSaver", "UnInhibit"); + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), + QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("UnInhibit")); message << m_screenInhibitCookie; QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(message); reply.waitForFinished(); m_screenInhibitCookie = 0; } #endif } void PresentationWidget::showTopBar( bool show ) { if ( show ) { m_topBar->show(); // Don't autohide the mouse cursor if it's over the toolbar if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay ) { KCursor::setAutoHideCursor( this, false ); } // Always show a cursor when topBar is visible if ( !m_drawingEngine ) { setCursor( QCursor( Qt::ArrowCursor ) ); } } else { m_topBar->hide(); // Reenable autohide if need be when leaving the toolbar if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay ) { KCursor::setAutoHideCursor( this, true ); } // Or hide the cursor again if hidden cursor is enabled else if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden ) { // Don't hide the cursor if drawing mode is on if ( !m_drawingEngine ) { setCursor( QCursor( Qt::BlankCursor ) ); } } } // Make sure mouse tracking isn't off after the KCursor::setAutoHideCursor() calls setMouseTracking( true ); } void PresentationWidget::slotFind() { if ( !m_searchBar ) { m_searchBar = new PresentationSearchBar( m_document, this, this ); m_searchBar->forceSnap(); } m_searchBar->focusOnSearchEdit(); m_searchBar->show(); } const Okular::PageTransition PresentationWidget::defaultTransition() const { return defaultTransition( Okular::Settings::slidesTransition() ); } const Okular::PageTransition PresentationWidget::defaultTransition( int type ) const { switch ( type ) { case Okular::Settings::EnumSlidesTransition::BlindsHorizontal: { Okular::PageTransition transition( Okular::PageTransition::Blinds ); transition.setAlignment( Okular::PageTransition::Horizontal ); return transition; break; } case Okular::Settings::EnumSlidesTransition::BlindsVertical: { Okular::PageTransition transition( Okular::PageTransition::Blinds ); transition.setAlignment( Okular::PageTransition::Vertical ); return transition; break; } case Okular::Settings::EnumSlidesTransition::BoxIn: { Okular::PageTransition transition( Okular::PageTransition::Box ); transition.setDirection( Okular::PageTransition::Inward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::BoxOut: { Okular::PageTransition transition( Okular::PageTransition::Box ); transition.setDirection( Okular::PageTransition::Outward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::Dissolve: { return Okular::PageTransition( Okular::PageTransition::Dissolve ); break; } case Okular::Settings::EnumSlidesTransition::GlitterDown: { Okular::PageTransition transition( Okular::PageTransition::Glitter ); transition.setAngle( 270 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::GlitterRight: { Okular::PageTransition transition( Okular::PageTransition::Glitter ); transition.setAngle( 0 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::GlitterRightDown: { Okular::PageTransition transition( Okular::PageTransition::Glitter ); transition.setAngle( 315 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::Random: { return defaultTransition( KRandom::random() % 18 ); break; } case Okular::Settings::EnumSlidesTransition::SplitHorizontalIn: { Okular::PageTransition transition( Okular::PageTransition::Split ); transition.setAlignment( Okular::PageTransition::Horizontal ); transition.setDirection( Okular::PageTransition::Inward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::SplitHorizontalOut: { Okular::PageTransition transition( Okular::PageTransition::Split ); transition.setAlignment( Okular::PageTransition::Horizontal ); transition.setDirection( Okular::PageTransition::Outward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::SplitVerticalIn: { Okular::PageTransition transition( Okular::PageTransition::Split ); transition.setAlignment( Okular::PageTransition::Vertical ); transition.setDirection( Okular::PageTransition::Inward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::SplitVerticalOut: { Okular::PageTransition transition( Okular::PageTransition::Split ); transition.setAlignment( Okular::PageTransition::Vertical ); transition.setDirection( Okular::PageTransition::Outward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::WipeDown: { Okular::PageTransition transition( Okular::PageTransition::Wipe ); transition.setAngle( 270 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::WipeRight: { Okular::PageTransition transition( Okular::PageTransition::Wipe ); transition.setAngle( 0 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::WipeLeft: { Okular::PageTransition transition( Okular::PageTransition::Wipe ); transition.setAngle( 180 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::WipeUp: { Okular::PageTransition transition( Okular::PageTransition::Wipe ); transition.setAngle( 90 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::Fade: { return Okular::PageTransition( Okular::PageTransition::Fade ); break; } case Okular::Settings::EnumSlidesTransition::Replace: default: return Okular::PageTransition( Okular::PageTransition::Replace ); break; } // should not happen, just make gcc happy return Okular::PageTransition(); } /** ONLY the TRANSITIONS GENERATION function from here on **/ void PresentationWidget::initTransition( const Okular::PageTransition *transition ) { // if it's just a 'replace' transition, repaint the screen if ( transition->type() == Okular::PageTransition::Replace ) { update(); return; } const bool isInward = transition->direction() == Okular::PageTransition::Inward; const bool isHorizontal = transition->alignment() == Okular::PageTransition::Horizontal; const float totalTime = transition->duration(); m_transitionRects.clear(); m_currentTransition = *transition; m_currentPagePixmap = m_lastRenderedPixmap; switch( transition->type() ) { // split: horizontal / vertical and inward / outward case Okular::PageTransition::Split: { const int steps = isHorizontal ? 100 : 75; if ( isHorizontal ) { if ( isInward ) { int xPosition = 0; for ( int i = 0; i < steps; i++ ) { int xNext = ((i + 1) * m_width) / (2 * steps); m_transitionRects.push_back( QRect( xPosition, 0, xNext - xPosition, m_height ) ); m_transitionRects.push_back( QRect( m_width - xNext, 0, xNext - xPosition, m_height ) ); xPosition = xNext; } } else { int xPosition = m_width / 2; for ( int i = 0; i < steps; i++ ) { int xNext = ((steps - (i + 1)) * m_width) / (2 * steps); m_transitionRects.push_back( QRect( xNext, 0, xPosition - xNext, m_height ) ); m_transitionRects.push_back( QRect( m_width - xPosition, 0, xPosition - xNext, m_height ) ); xPosition = xNext; } } } else { if ( isInward ) { int yPosition = 0; for ( int i = 0; i < steps; i++ ) { int yNext = ((i + 1) * m_height) / (2 * steps); m_transitionRects.push_back( QRect( 0, yPosition, m_width, yNext - yPosition ) ); m_transitionRects.push_back( QRect( 0, m_height - yNext, m_width, yNext - yPosition ) ); yPosition = yNext; } } else { int yPosition = m_height / 2; for ( int i = 0; i < steps; i++ ) { int yNext = ((steps - (i + 1)) * m_height) / (2 * steps); m_transitionRects.push_back( QRect( 0, yNext, m_width, yPosition - yNext ) ); m_transitionRects.push_back( QRect( 0, m_height - yPosition, m_width, yPosition - yNext ) ); yPosition = yNext; } } } m_transitionMul = 2; m_transitionDelay = (int)( (totalTime * 1000) / steps ); } break; // blinds: horizontal(l-to-r) / vertical(t-to-b) case Okular::PageTransition::Blinds: { const int blinds = isHorizontal ? 8 : 6; const int steps = m_width / (4 * blinds); if ( isHorizontal ) { int xPosition[ 8 ]; for ( int b = 0; b < blinds; b++ ) xPosition[ b ] = (b * m_width) / blinds; for ( int i = 0; i < steps; i++ ) { int stepOffset = (int)( ((float)i * (float)m_width) / ((float)blinds * (float)steps) ); for ( int b = 0; b < blinds; b++ ) { m_transitionRects.push_back( QRect( xPosition[ b ], 0, stepOffset, m_height ) ); xPosition[ b ] = stepOffset + (b * m_width) / blinds; } } } else { int yPosition[ 6 ]; for ( int b = 0; b < blinds; b++ ) yPosition[ b ] = (b * m_height) / blinds; for ( int i = 0; i < steps; i++ ) { int stepOffset = (int)( ((float)i * (float)m_height) / ((float)blinds * (float)steps) ); for ( int b = 0; b < blinds; b++ ) { m_transitionRects.push_back( QRect( 0, yPosition[ b ], m_width, stepOffset ) ); yPosition[ b ] = stepOffset + (b * m_height) / blinds; } } } m_transitionMul = blinds; m_transitionDelay = (int)( (totalTime * 1000) / steps ); } break; // box: inward / outward case Okular::PageTransition::Box: { const int steps = m_width / 10; if ( isInward ) { int L = 0, T = 0, R = m_width, B = m_height; for ( int i = 0; i < steps; i++ ) { // compute shrunk box coords int newL = ((i + 1) * m_width) / (2 * steps); int newT = ((i + 1) * m_height) / (2 * steps); int newR = m_width - newL; int newB = m_height - newT; // add left, right, topcenter, bottomcenter rects m_transitionRects.push_back( QRect( L, T, newL - L, B - T ) ); m_transitionRects.push_back( QRect( newR, T, R - newR, B - T ) ); m_transitionRects.push_back( QRect( newL, T, newR - newL, newT - T ) ); m_transitionRects.push_back( QRect( newL, newB, newR - newL, B - newB ) ); L = newL; T = newT; R = newR, B = newB; } } else { int L = m_width / 2, T = m_height / 2, R = L, B = T; for ( int i = 0; i < steps; i++ ) { // compute shrunk box coords int newL = ((steps - (i + 1)) * m_width) / (2 * steps); int newT = ((steps - (i + 1)) * m_height) / (2 * steps); int newR = m_width - newL; int newB = m_height - newT; // add left, right, topcenter, bottomcenter rects m_transitionRects.push_back( QRect( newL, newT, L - newL, newB - newT ) ); m_transitionRects.push_back( QRect( R, newT, newR - R, newB - newT ) ); m_transitionRects.push_back( QRect( L, newT, R - L, T - newT ) ); m_transitionRects.push_back( QRect( L, B, R - L, newB - B ) ); L = newL; T = newT; R = newR, B = newB; } } m_transitionMul = 4; m_transitionDelay = (int)( (totalTime * 1000) / steps ); } break; // wipe: implemented for 4 canonical angles case Okular::PageTransition::Wipe: { const int angle = transition->angle(); const int steps = (angle == 0) || (angle == 180) ? m_width / 8 : m_height / 8; if ( angle == 0 ) { int xPosition = 0; for ( int i = 0; i < steps; i++ ) { int xNext = ((i + 1) * m_width) / steps; m_transitionRects.push_back( QRect( xPosition, 0, xNext - xPosition, m_height ) ); xPosition = xNext; } } else if ( angle == 90 ) { int yPosition = m_height; for ( int i = 0; i < steps; i++ ) { int yNext = ((steps - (i + 1)) * m_height) / steps; m_transitionRects.push_back( QRect( 0, yNext, m_width, yPosition - yNext ) ); yPosition = yNext; } } else if ( angle == 180 ) { int xPosition = m_width; for ( int i = 0; i < steps; i++ ) { int xNext = ((steps - (i + 1)) * m_width) / steps; m_transitionRects.push_back( QRect( xNext, 0, xPosition - xNext, m_height ) ); xPosition = xNext; } } else if ( angle == 270 ) { int yPosition = 0; for ( int i = 0; i < steps; i++ ) { int yNext = ((i + 1) * m_height) / steps; m_transitionRects.push_back( QRect( 0, yPosition, m_width, yNext - yPosition ) ); yPosition = yNext; } } else { update(); return; } m_transitionMul = 1; m_transitionDelay = (int)( (totalTime * 1000) / steps ); } break; // dissolve: replace 'random' rects case Okular::PageTransition::Dissolve: { const int gridXsteps = 50; const int gridYsteps = 38; const int steps = gridXsteps * gridYsteps; int oldX = 0; int oldY = 0; // create a grid of gridXstep by gridYstep QRects for ( int y = 0; y < gridYsteps; y++ ) { int newY = (int)( m_height * ((float)(y+1) / (float)gridYsteps) ); for ( int x = 0; x < gridXsteps; x++ ) { int newX = (int)( m_width * ((float)(x+1) / (float)gridXsteps) ); m_transitionRects.push_back( QRect( oldX, oldY, newX - oldX, newY - oldY ) ); oldX = newX; } oldX = 0; oldY = newY; } // randomize the grid for ( int i = 0; i < steps; i++ ) { #ifndef Q_OS_WIN int n1 = (int)(steps * drand48()); int n2 = (int)(steps * drand48()); #else int n1 = (int)(steps * (std::rand() / RAND_MAX)); int n2 = (int)(steps * (std::rand() / RAND_MAX)); #endif // swap items if index differs if ( n1 != n2 ) { QRect r = m_transitionRects[ n2 ]; m_transitionRects[ n2 ] = m_transitionRects[ n1 ]; m_transitionRects[ n1 ] = r; } } // set global transition parameters m_transitionMul = 40; m_transitionDelay = (int)( (m_transitionMul * 1000 * totalTime) / steps ); } break; // glitter: similar to dissolve but has a direction case Okular::PageTransition::Glitter: { const int gridXsteps = 50; const int gridYsteps = 38; const int steps = gridXsteps * gridYsteps; const int angle = transition->angle(); // generate boxes using a given direction if ( angle == 90 ) { int yPosition = m_height; for ( int i = 0; i < gridYsteps; i++ ) { int yNext = ((gridYsteps - (i + 1)) * m_height) / gridYsteps; int xPosition = 0; for ( int j = 0; j < gridXsteps; j++ ) { int xNext = ((j + 1) * m_width) / gridXsteps; m_transitionRects.push_back( QRect( xPosition, yNext, xNext - xPosition, yPosition - yNext ) ); xPosition = xNext; } yPosition = yNext; } } else if ( angle == 180 ) { int xPosition = m_width; for ( int i = 0; i < gridXsteps; i++ ) { int xNext = ((gridXsteps - (i + 1)) * m_width) / gridXsteps; int yPosition = 0; for ( int j = 0; j < gridYsteps; j++ ) { int yNext = ((j + 1) * m_height) / gridYsteps; m_transitionRects.push_back( QRect( xNext, yPosition, xPosition - xNext, yNext - yPosition ) ); yPosition = yNext; } xPosition = xNext; } } else if ( angle == 270 ) { int yPosition = 0; for ( int i = 0; i < gridYsteps; i++ ) { int yNext = ((i + 1) * m_height) / gridYsteps; int xPosition = 0; for ( int j = 0; j < gridXsteps; j++ ) { int xNext = ((j + 1) * m_width) / gridXsteps; m_transitionRects.push_back( QRect( xPosition, yPosition, xNext - xPosition, yNext - yPosition ) ); xPosition = xNext; } yPosition = yNext; } } else // if angle is 0 or 315 { int xPosition = 0; for ( int i = 0; i < gridXsteps; i++ ) { int xNext = ((i + 1) * m_width) / gridXsteps; int yPosition = 0; for ( int j = 0; j < gridYsteps; j++ ) { int yNext = ((j + 1) * m_height) / gridYsteps; m_transitionRects.push_back( QRect( xPosition, yPosition, xNext - xPosition, yNext - yPosition ) ); yPosition = yNext; } xPosition = xNext; } } // add a 'glitter' (1 over 10 pieces is randomized) int randomSteps = steps / 20; for ( int i = 0; i < randomSteps; i++ ) { #ifndef Q_OS_WIN int n1 = (int)(steps * drand48()); int n2 = (int)(steps * drand48()); #else int n1 = (int)(steps * (std::rand() / RAND_MAX)); int n2 = (int)(steps * (std::rand() / RAND_MAX)); #endif // swap items if index differs if ( n1 != n2 ) { QRect r = m_transitionRects[ n2 ]; m_transitionRects[ n2 ] = m_transitionRects[ n1 ]; m_transitionRects[ n1 ] = r; } } // set global transition parameters m_transitionMul = (angle == 90) || (angle == 270) ? gridYsteps : gridXsteps; m_transitionMul /= 2; m_transitionDelay = (int)( (m_transitionMul * 1000 * totalTime) / steps ); } break; case Okular::PageTransition::Fade: { enum {FADE_TRANSITION_FPS = 20}; const int steps = totalTime * FADE_TRANSITION_FPS; m_transitionSteps = steps; QPainter pixmapPainter; m_currentPixmapOpacity = (double) 1 / steps; m_transitionDelay = (int)( totalTime * 1000 ) / steps; m_lastRenderedPixmap = QPixmap( m_lastRenderedPixmap.size() ); m_lastRenderedPixmap.fill( Qt::transparent ); pixmapPainter.begin( &m_lastRenderedPixmap ); pixmapPainter.setCompositionMode( QPainter::CompositionMode_Source ); pixmapPainter.setOpacity( 1 - m_currentPixmapOpacity ); pixmapPainter.drawPixmap( 0, 0, m_previousPagePixmap ); pixmapPainter.setOpacity( m_currentPixmapOpacity ); pixmapPainter.drawPixmap( 0, 0, m_currentPagePixmap ); pixmapPainter.end(); update(); } break; // implement missing transitions (a binary raster engine needed here) case Okular::PageTransition::Fly: case Okular::PageTransition::Push: case Okular::PageTransition::Cover: case Okular::PageTransition::Uncover: default: update(); return; } // send the first start to the timer m_transitionTimer->start( 0 ); } void PresentationWidget::slotProcessMovieAction( const Okular::MovieAction *action ) { const Okular::MovieAnnotation *movieAnnotation = action->annotation(); if ( !movieAnnotation ) return; Okular::Movie *movie = movieAnnotation->movie(); if ( !movie ) return; VideoWidget *vw = m_frames[ m_frameIndex ]->videoWidgets.value( movieAnnotation->movie() ); if ( !vw ) return; vw->show(); switch ( action->operation() ) { case Okular::MovieAction::Play: vw->stop(); vw->play(); break; case Okular::MovieAction::Stop: vw->stop(); break; case Okular::MovieAction::Pause: vw->pause(); break; case Okular::MovieAction::Resume: vw->play(); break; }; } void PresentationWidget::slotProcessRenditionAction( const Okular::RenditionAction *action ) { Okular::Movie *movie = action->movie(); if ( !movie ) return; VideoWidget *vw = m_frames[ m_frameIndex ]->videoWidgets.value( movie ); if ( !vw ) return; if ( action->operation() == Okular::RenditionAction::None ) return; vw->show(); switch ( action->operation() ) { case Okular::RenditionAction::Play: vw->stop(); vw->play(); break; case Okular::RenditionAction::Stop: vw->stop(); break; case Okular::RenditionAction::Pause: vw->pause(); break; case Okular::RenditionAction::Resume: vw->play(); break; default: return; }; } void PresentationWidget::slotTogglePlayPause() { m_advanceSlides = !m_advanceSlides; setPlayPauseIcon(); if ( m_advanceSlides ) { startAutoChangeTimer(); } else { m_nextPageTimer->stop(); } } #include "presentationwidget.moc"