diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index eea05ebf6..2daaa46ca 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,54 +1,54 @@ include: - https://invent.kde.org/sysadmin/ci-tooling/raw/master/invent/ci-before.yml - https://invent.kde.org/sysadmin/ci-tooling/raw/master/invent/ci-applications-linux.yml build_ubuntu_18_04: stage: build image: ubuntu:bionic only: - merge_requests before_script: - sed -i -e 's/# deb-src/deb-src/g' /etc/apt/sources.list - apt-get update - apt-get build-dep --yes --no-install-recommends okular - apt-get install --yes --no-install-recommends ninja-build libkf5crash-dev script: - mkdir -p build && cd build - cmake -G Ninja .. - ninja build_ubuntu_20_04: stage: build image: ubuntu:focal only: - merge_requests before_script: - sed -i -e 's/# deb-src/deb-src/g' /etc/apt/sources.list - apt-get update - apt-get build-dep --yes --no-install-recommends okular - apt-get install --yes --no-install-recommends ninja-build script: - mkdir -p build && cd build - cmake -DOKULAR_UI=desktop -G Ninja .. - ninja - rm -rf * - cmake -DOKULAR_UI=mobile -G Ninja .. - ninja build_clazy_clang_tidy: stage: build image: debian:unstable only: - merge_requests before_script: - echo 'deb-src http://deb.debian.org/debian unstable main' >> /etc/apt/sources.list - apt-get update - apt-get build-dep --yes --no-install-recommends okular - apt-get install --yes --no-install-recommends ninja-build clazy clang clang-tidy python python-yaml libkf5crash-dev libkf5purpose-dev libegl-dev jq script: - srcdir=`pwd` && mkdir -p /tmp/okular_build && cd /tmp/okular_build && CC=clang CXX=clazy CXXFLAGS="-Werror -Wno-deprecated-declarations" cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -G Ninja $srcdir && cat compile_commands.json | jq '[.[] | select(.file | contains("'"$srcdir"'"))]' > compile_commands.aux.json && cat compile_commands.aux.json | jq '[.[] | select(.file | contains("/synctex/")| not)]' > compile_commands.json - - CLAZY_CHECKS="level0,level1" ninja + - CLAZY_CHECKS="level0,level1,old-style-connect" ninja # Fix the poppler header, remove when debian:unstable ships poppler 0.82 or later - sed -i "N;N;N;N; s#class MediaRendition\;\nclass MovieAnnotation\;\nclass ScreenAnnotation;#class MediaRendition\;#g" /usr/include/poppler/qt5/poppler-link.h - "run-clang-tidy -header-filter='.*/okular/.*' -checks='-*,performance-*,bugprone-*,readability-inconsistent-declaration-parameter-name,readability-string-compare,modernize-redundant-void-arg,modernize-use-bool-literals,modernize-make-unique,modernize-make-shared,modernize-use-override,modernize-use-equals-delete,modernize-use-emplace,modernize-loop-convert,modernize-use-nullptr,-bugprone-macro-parentheses,-bugprone-narrowing-conversions,-bugprone-branch-clone,-bugprone-incorrect-roundings' -config=\"{WarningsAsErrors: '*'}\"" diff --git a/autotests/mainshelltest.cpp b/autotests/mainshelltest.cpp index a9941e9e0..d71dfed13 100644 --- a/autotests/mainshelltest.cpp +++ b/autotests/mainshelltest.cpp @@ -1,695 +1,695 @@ /*************************************************************************** * 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 #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 "closedialoghelper.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: +public 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 testMiddleButtonCloseUndo(); void testSessionRestore_data(); void testSessionRestore(); void testOpenInvalidFiles_data(); void testOpenInvalidFiles(); private: }; QList getShells() { QList shells; const QList< KMainWindow * > mainWindows = KMainWindow::memberList(); for ( KMainWindow* kmw : mainWindows ) { Shell* shell = qobject_cast( kmw ); if( shell ) { shells.append( shell ); } } return shells; } Shell *findShell(Shell *ignore = nullptr) { const QWidgetList wList = QApplication::topLevelWidgets(); for (QWidget *widget : wList ) { 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 const QList urls = { QUrl::fromUserInput(QStringLiteral("file://" KDESRCDIR "data/file1.pdf")) , QUrl::fromUserInput(QStringLiteral("file://" KDESRCDIR "data/tocreload.pdf")) , QUrl::fromUserInput(QStringLiteral("file://" KDESRCDIR "data/contents.epub")) }; for (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 << 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())); + QTimer::singleShot(0, helper.data(), &ClosePrintDialogHelper::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(); for (const QString &path : qAsConst(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; for ( Shell *shell : qAsConst(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 ); for ( Shell *shell : qAsConst(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; for ( Shell* shell : qAsConst(shells) ) { QVERIFY( QTest::qWaitForWindowExposed( shell ) ); numDocs += shell->m_tabs.size(); } QCOMPARE( numDocs, paths.size() ); QCOMPARE( shells.size(), useTabsRestore ? numWindows : paths.size() ); } void MainShellTest::testOpenInvalidFiles_data() { QTest::addColumn>("files"); QTest::addColumn("options"); QString options = ShellUtils::serializeOptions( false, false, false, false, false, QString(), QString() ); QUrl validFile1 = ShellUtils::urlFromArg( QStringLiteral( KDESRCDIR "data/file1.pdf" ), ShellUtils::qfileExistFunc(), QString() ); QUrl validFile2 = ShellUtils::urlFromArg( QStringLiteral( KDESRCDIR "data/file2.pdf" ), ShellUtils::qfileExistFunc(), QString() ); QUrl invalidFile = ShellUtils::urlFromArg( QStringLiteral( KDESRCDIR "data/non-existing-doc.pdf" ), ShellUtils::qfileExistFunc(), QString() ); QList firstCase { invalidFile, validFile1, validFile2 }; QList secondCase { validFile1, validFile2, invalidFile }; QTest::newRow( "opening the invalid file first" ) << firstCase << options; QTest::newRow( "opening the valids file first" ) << secondCase << options; } void MainShellTest::testOpenInvalidFiles() { QFETCH( QList, files ); QFETCH( QString, options ); /* * The purpose of this test is to verify that when we open an invalid file, no tab is created in the * shell. * */ Okular::Settings::self()->setShellOpenFileInTabs( true ); Okular::Status status = Okular::main( QStringList(), options ); QCOMPARE( status, Okular::Success ); Shell *shell = findShell(); QVERIFY( shell ); /* * We need to make sure that the KrecentFilesAction is empty before starting, because we will also test that * the file gets removed from the recent documents * */ shell->m_recent->clear(); QScopedPointer closeDialogHelper { new TestingUtils::CloseDialogHelper( QDialogButtonBox::StandardButton::Ok ) }; for (const QUrl& file: files) { shell->openUrl( file ); } QList recentFiles = shell->m_recent->urls(); QVERIFY( shell->m_tabs.size() == 2 ); QVERIFY( shell->m_tabWidget->tabBar()->isVisible() ); QVERIFY( ! shell->m_tabWidget->tabIcon(0).isNull() ); QVERIFY( ! shell->m_tabWidget->tabIcon(1).isNull() ); QVERIFY( recentFiles.size() == 2 ); } void MainShellTest::testMiddleButtonCloseUndo() { const QStringList paths = { QStringLiteral(KDESRCDIR "data/file1.pdf"), QStringLiteral(KDESRCDIR "data/file2.pdf") }; QString serializedOptions; serializedOptions = ShellUtils::serializeOptions(false, false, false, false, false, QString(), QString()); Okular::Settings::self()->setShellOpenFileInTabs(true); Okular::Status status = Okular::main(paths, serializedOptions); QCOMPARE(status, Okular::Success); Shell *s = findShell(); QVERIFY(s); QCOMPARE(s->m_tabWidget->count(), paths.size()); // Close a tab using middle key QWidget *firstTab = s->m_tabWidget->tabBar()->tabButton(0, QTabBar::RightSide); QVERIFY(firstTab); QTest::mouseClick(firstTab, Qt::MiddleButton); QCOMPARE(s->m_tabWidget->count(), paths.size() - 1); // Undo tab close s->undoCloseTab(); QCOMPARE(s->m_tabWidget->count(), paths.size()); } QTEST_MAIN( MainShellTest ) #include "mainshelltest.moc" diff --git a/autotests/searchtest.cpp b/autotests/searchtest.cpp index bfb687f74..fc9e07bb5 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: + public 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, &Okular::Document::searchFinished); - QObject::connect(&d, SIGNAL(searchFinished(int,Okular::Document::SearchStatus)), &receiver, SLOT(searchFinished(int,Okular::Document::SearchStatus))); + QObject::connect(&d, &Okular::Document::searchFinished, &receiver, &SearchFinishedReceiver::searchFinished); 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 https://www.unicode.org/Public/6.2.0/ucd/CaseFolding.txt). QVector text; text << QStringLiteral("İ"); QVector rect; rect << Okular::NormalizedRect(1, 2, 3, 4); CREATE_PAGE; 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/conf/dlggeneral.cpp b/conf/dlggeneral.cpp index 8836566a6..93d84919d 100644 --- a/conf/dlggeneral.cpp +++ b/conf/dlggeneral.cpp @@ -1,57 +1,59 @@ /*************************************************************************** * Copyright (C) 2006 by Pino Toscano * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "dlggeneral.h" #include #include #include "ui_dlggeneralbase.h" #include "settings.h" DlgGeneral::DlgGeneral( QWidget * parent, Okular::EmbedMode embedMode ) : QWidget( parent ) { m_dlg = new Ui_DlgGeneralBase(); m_dlg->setupUi( this ); setCustomBackgroundColorButton( Okular::Settings::useCustomBackgroundColor() ); if( embedMode == Okular::ViewerWidgetMode ) { m_dlg->kcfg_SyncThumbnailsViewport->setVisible( false ); m_dlg->kcfg_DisplayDocumentTitle->setVisible( false ); m_dlg->kcfg_WatchFile->setVisible( false ); m_dlg->kcfg_rtlReadingDirection->setVisible(false); } m_dlg->kcfg_ShellOpenFileInTabs->setVisible( embedMode == Okular::NativeShellMode ); + + connect(m_dlg->kcfg_UseCustomBackgroundColor, &QCheckBox::toggled, this, &DlgGeneral::setCustomBackgroundColorButton); } DlgGeneral::~DlgGeneral() { delete m_dlg; } void DlgGeneral::showEvent( QShowEvent * ) { #if OKULAR_FORCE_DRM m_dlg->kcfg_ObeyDRM->hide(); #else if ( KAuthorized::authorize( QStringLiteral("skip_drm") ) ) m_dlg->kcfg_ObeyDRM->show(); else m_dlg->kcfg_ObeyDRM->hide(); #endif } void DlgGeneral::setCustomBackgroundColorButton( bool value ) { m_dlg->kcfg_BackgroundColor->setEnabled( value ); } diff --git a/conf/dlggeneralbase.ui b/conf/dlggeneralbase.ui index 089ac43cf..bc381f506 100644 --- a/conf/dlggeneralbase.ui +++ b/conf/dlggeneralbase.ui @@ -1,504 +1,483 @@ DlgGeneralBase true 0 0 558 632 0 0 0 0 Appearance 6 9 9 9 9 6 0 0 0 0 Show scroll&bars true Link the &thumbnails with the page Show &hints and info messages Display document title in titlebar if available When not displaying document title: QGroupBox {border:0; } true 8 0 0 Display file name only true Display full file path false 6 0 false Use custom background color 4 0 6 0 0 0 0 0 0 Qt::Vertical QSizePolicy::Minimum 20 1 Program Features 6 9 9 9 9 6 0 0 0 0 Open new files in &tabs &Obey DRM limitations &Reload document on file change Show backend selection dialog 6 0 0 0 0 Right to left reading direction Qt::Vertical 20 1 View Options Overview &columns: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter kcfg_ViewColumns 3 10 Defines how much of the current viewing area will still be visible when pressing the Page Up/Down keys. &Page Up/Down overlap: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter kcfg_ScrollOverlap % 50 5 Defines the default zoom mode for files which were never opened before. For files which were opened before the previous zoom is applied. Defines the default zoom mode for files which were never opened before. For files which were opened before the previous zoom is applied. &Default Zoom: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter kcfg_ZoomMode Defines the default zoom mode for files which were never opened before. For files which were opened before the previous zoom is applied. Defines the default zoom mode for files which were never opened before. For files which were opened before the previous zoom is applied. 100% Fit Width Fit Page Auto Fit Qt::Vertical 20 4 KColorButton QPushButton
kcolorbutton.h
useDefaultBackgroundColor()
kiconloader.h - - - kcfg_UseCustomBackgroundColor - toggled(bool) - DlgGeneralBase - setCustomBackgroundColorButton(bool) - - - 130 - 229 - - - 203 - -9 - - - - - - setCustomBackgroundColorButton(bool) -
diff --git a/conf/dlgpresentation.cpp b/conf/dlgpresentation.cpp index ca1d2c302..ea980bb62 100644 --- a/conf/dlgpresentation.cpp +++ b/conf/dlgpresentation.cpp @@ -1,70 +1,71 @@ /*************************************************************************** * Copyright (C) 2006,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 "dlgpresentation.h" #include "ui_dlgpresentationbase.h" #include "widgetdrawingtools.h" #include #include #include #include #include "settings.h" DlgPresentation::DlgPresentation( QWidget * parent ) : QWidget( parent ) { m_dlg = new Ui_DlgPresentationBase(); m_dlg->setupUi( this ); WidgetDrawingTools * kcfg_DrawingTools = new WidgetDrawingTools( m_dlg->annotationToolsGroupBox ); m_dlg->verticalLayout_4->addWidget( kcfg_DrawingTools ); kcfg_DrawingTools->setObjectName( QStringLiteral("kcfg_DrawingTools") ); KConfigDialogManager::changedMap()->insert( QStringLiteral("WidgetDrawingTools"), SIGNAL(changed()) ); QStringList choices; choices.append( i18nc( "@label:listbox The current screen, for the presentation mode", "Current Screen" ) ); choices.append( i18nc( "@label:listbox The default screen for the presentation mode", "Default Screen" ) ); const int screenCount = QApplication::desktop()->numScreens(); for ( int i = 0; i < screenCount; ++i ) { choices.append( i18nc( "@label:listbox %1 is the screen number (0, 1, ...)", "Screen %1", i ) ); } m_dlg->screenCombo->addItems( choices ); const int screen = Okular::Settings::slidesScreen(); if ( screen >= -2 && screen < screenCount ) { m_dlg->screenCombo->setCurrentIndex( screen + 2 ); } else { m_dlg->screenCombo->setCurrentIndex( 0 ); Okular::Settings::setSlidesScreen( -2 ); } m_dlg->kcfg_SlidesAdvanceTime->setSuffix(ki18ncp("Advance every %1 seconds", " second", " seconds")); connect(m_dlg->screenCombo, static_cast(&QComboBox::activated), this, &DlgPresentation::screenComboChanged); + connect(m_dlg->kcfg_SlidesAdvance, &QAbstractButton::toggled, m_dlg->kcfg_SlidesAdvanceTime, &QWidget::setEnabled); } DlgPresentation::~DlgPresentation() { delete m_dlg; } void DlgPresentation::screenComboChanged( int which ) { Okular::Settings::setSlidesScreen( which - 2 ); } #include "moc_dlgpresentation.cpp" diff --git a/conf/dlgpresentationbase.ui b/conf/dlgpresentationbase.ui index 5b9d13b80..d2cb5975c 100644 --- a/conf/dlgpresentationbase.ui +++ b/conf/dlgpresentationbase.ui @@ -1,382 +1,364 @@ DlgPresentationBase 0 0 400 525 0 0 0 0 Navigation 6 9 9 9 9 6 0 0 0 0 Advance every: false 5 Loop after last page Touch navigation: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Tap left/right side to go back/forward Tap anywhere to go forward Disabled Appearance Background color: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Mouse cursor: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Hidden After Delay Always Visible Always Hidden Show &progress indicator Show s&ummary page Enable transitions true true Default transition: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Blinds Vertical Blinds Horizontal Box In Box Out Dissolve Fade Glitter Down Glitter Right Glitter Right-Down Random Transition Replace Split Horizontal In Split Horizontal Out Split Vertical In Split Vertical Out Wipe Down Wipe Right Wipe Left Wipe Up Placement Screen: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Drawing Tool Configuration KColorButton QPushButton
kcolorbutton.h
KPluralHandlingSpinBox QSpinBox
KPluralHandlingSpinBox
- - - kcfg_SlidesAdvance - toggled(bool) - kcfg_SlidesAdvanceTime - setEnabled(bool) - - - 88 - 43 - - - 280 - 49 - - - -
diff --git a/core/pagecontroller.cpp b/core/pagecontroller.cpp index ae2864fd3..3319cf4e7 100644 --- a/core/pagecontroller.cpp +++ b/core/pagecontroller.cpp @@ -1,46 +1,46 @@ /*************************************************************************** * Copyright (C) 2007 by Pino Toscano * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "pagecontroller_p.h" // local includes #include "page_p.h" #include "rotationjob_p.h" #include using namespace Okular; PageController::PageController() : QObject() { } PageController::~PageController() { } void PageController::addRotationJob(RotationJob *job) { - connect( job, SIGNAL(done(ThreadWeaver::JobPointer)), - this, SLOT(imageRotationDone(ThreadWeaver::JobPointer)) ); + connect( job, &RotationJob::done, + this, &PageController::imageRotationDone ); ThreadWeaver::enqueue(&m_weaver, job); } void PageController::imageRotationDone(const ThreadWeaver::JobPointer &j) { RotationJob *job = static_cast< RotationJob * >( j.data() ); if ( job->page() ) { job->page()->imageRotationDone( job ); emit rotationFinished( job->page()->m_number, job->page()->m_page ); } } diff --git a/generators/chm/generator_chm.cpp b/generators/chm/generator_chm.cpp index 9e432fc0c..c4f073ea3 100644 --- a/generators/chm/generator_chm.cpp +++ b/generators/chm/generator_chm.cpp @@ -1,449 +1,449 @@ /*************************************************************************** * Copyright (C) 2005 by Piotr Szymański * * Copyright (C) 2008 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 "generator_chm.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include OKULAR_EXPORT_PLUGIN(CHMGenerator, "libokularGenerator_chmlib.json") static QString absolutePath( const QString &baseUrl, const QString &path ) { QString absPath; if ( path.startsWith(QLatin1Char( '/' )) ) { // already absolute absPath = path; } else { QUrl url = QUrl::fromLocalFile( baseUrl ).adjusted(QUrl::RemoveFilename); url.setPath( url.path() + path ); absPath = url.toLocalFile(); } return absPath; } CHMGenerator::CHMGenerator( QObject *parent, const QVariantList &args ) : Okular::Generator( parent, args ) { setFeature( TextExtraction ); m_syncGen=nullptr; m_file=nullptr; m_request = nullptr; } CHMGenerator::~CHMGenerator() { delete m_syncGen; } bool CHMGenerator::loadDocument( const QString & fileName, QVector< Okular::Page * > & pagesVector ) { m_file = new EBook_CHM(); m_file = EBook::loadFile( fileName ); if (!m_file) { return false; } m_fileName=fileName; QList< EBookTocEntry > topics; m_file->getTableOfContents(topics); // fill m_docSyn QMap lastIndentElement; QMap tmpPageList; int pageNum = 0; for (const EBookTocEntry &e : qAsConst(topics)) { QDomElement item = m_docSyn.createElement(e.name); if (!e.url.isEmpty()) { QString url = e.url.toString(); item.setAttribute(QStringLiteral("ViewportName"), url); if(!tmpPageList.contains(url)) {//add a page only once tmpPageList.insert(url, pageNum); pageNum++; } } item.setAttribute(QStringLiteral("Icon"), e.iconid); if (e.indent == 0) m_docSyn.appendChild(item); else lastIndentElement[e.indent - 1].appendChild(item); lastIndentElement[e.indent] = item; } // fill m_urlPage and m_pageUrl QList pageList; m_file->enumerateFiles(pageList); const QUrl home = m_file->homeUrl(); if (home.path() != QLatin1String("/")) pageList.prepend(home); m_pageUrl.resize(pageNum); for (const QUrl &qurl : qAsConst(pageList)) { QString url = qurl.toString(); const QString urlLower = url.toLower(); if (!urlLower.endsWith(QLatin1String(".html")) && !urlLower.endsWith(QLatin1String(".htm"))) continue; int pos = url.indexOf (QLatin1Char(('#'))); // insert the url into the maps, but insert always the variant without the #ref part QString tmpUrl = pos == -1 ? url : url.left(pos); // url already there, abort insertion if (m_urlPage.contains(tmpUrl)) continue; int foundPage = tmpPageList.value(tmpUrl, -1); if (foundPage != -1 ) { m_urlPage.insert(tmpUrl, foundPage); m_pageUrl[foundPage] = tmpUrl; } else { //add pages not present in toc m_urlPage.insert(tmpUrl, pageNum); m_pageUrl.append(tmpUrl); pageNum++; } } pagesVector.resize(m_pageUrl.count()); m_textpageAddedList.fill(false, pagesVector.count()); m_rectsGenerated.fill(false, pagesVector.count()); if (!m_syncGen) { m_syncGen = new KHTMLPart(); } disconnect( m_syncGen, nullptr, this, nullptr ); for (int i = 0; i < m_pageUrl.count(); ++i) { preparePageForSyncOperation(m_pageUrl.at(i)); pagesVector[ i ] = new Okular::Page (i, m_syncGen->view()->contentsWidth(), m_syncGen->view()->contentsHeight(), Okular::Rotation0 ); } - connect( m_syncGen, SIGNAL(completed()), this, SLOT(slotCompleted()) ); + connect( m_syncGen, QOverload<>::of(&KHTMLPart::completed), this, &CHMGenerator::slotCompleted ); connect( m_syncGen, &KParts::ReadOnlyPart::canceled, this, &CHMGenerator::slotCompleted ); return true; } bool CHMGenerator::doCloseDocument() { // delete the document information of the old document delete m_file; m_file=nullptr; m_textpageAddedList.clear(); m_rectsGenerated.clear(); m_urlPage.clear(); m_pageUrl.clear(); m_docSyn.clear(); if (m_syncGen) { m_syncGen->closeUrl(); } return true; } void CHMGenerator::preparePageForSyncOperation(const QString & url) { QString pAddress = QStringLiteral("ms-its:") + m_fileName + QStringLiteral("::") + m_file->urlToPath(QUrl(url)); m_chmUrl = url; m_syncGen->openUrl(QUrl(pAddress)); m_syncGen->view()->layout(); QEventLoop loop; - connect( m_syncGen, SIGNAL(completed()), &loop, SLOT(quit()) ); + connect( m_syncGen, QOverload<>::of(&KHTMLPart::completed), &loop, &QEventLoop::quit ); connect( m_syncGen, &KParts::ReadOnlyPart::canceled, &loop, &QEventLoop::quit ); // discard any user input, otherwise it breaks the "synchronicity" of this // function loop.exec( QEventLoop::ExcludeUserInputEvents ); } void CHMGenerator::slotCompleted() { if ( !m_request ) return; QImage image( m_request->width(), m_request->height(), QImage::Format_ARGB32 ); image.fill( Qt::white ); QPainter p( &image ); QRect r( 0, 0, m_request->width(), m_request->height() ); bool moreToPaint; m_syncGen->paint( &p, r, 0, &moreToPaint ); p.end(); if ( !m_textpageAddedList.at( m_request->pageNumber() ) ) { additionalRequestData(); m_textpageAddedList[ m_request->pageNumber() ] = true; } m_syncGen->closeUrl(); m_chmUrl = QString(); userMutex()->unlock(); Okular::PixmapRequest *req = m_request; m_request = nullptr; if ( !req->page()->isBoundingBoxKnown() ) updatePageBoundingBox( req->page()->number(), Okular::Utils::imageBoundingBox( &image ) ); req->page()->setPixmap( req->observer(), new QPixmap( QPixmap::fromImage( image ) ) ); signalPixmapRequestDone( req ); } Okular::DocumentInfo CHMGenerator::generateDocumentInfo( const QSet &keys ) const { Okular::DocumentInfo docInfo; if ( keys.contains( Okular::DocumentInfo::MimeType ) ) docInfo.set( Okular::DocumentInfo::MimeType, QStringLiteral("application/x-chm") ); if ( keys.contains( Okular::DocumentInfo::Title ) ) docInfo.set( Okular::DocumentInfo::Title, m_file->title() ); return docInfo; } const Okular::DocumentSynopsis * CHMGenerator::generateDocumentSynopsis() { return &m_docSyn; } bool CHMGenerator::canGeneratePixmap () const { bool isLocked = true; if ( userMutex()->tryLock() ) { userMutex()->unlock(); isLocked = false; } return !isLocked; } void CHMGenerator::generatePixmap( Okular::PixmapRequest * request ) { int requestWidth = request->width(); int requestHeight = request->height(); userMutex()->lock(); QString url= m_pageUrl[request->pageNumber()]; QString pAddress= QStringLiteral("ms-its:") + m_fileName + QStringLiteral("::") + m_file->urlToPath(QUrl(url)); m_chmUrl = url; m_syncGen->view()->resizeContents(requestWidth,requestHeight); m_request=request; // will emit openURL without problems m_syncGen->openUrl ( QUrl(pAddress) ); } void CHMGenerator::recursiveExploreNodes(DOM::Node node,Okular::TextPage *tp) { if (node.nodeType() == DOM::Node::TEXT_NODE && !node.getRect().isNull()) { QString nodeText=node.nodeValue().string(); QRect r=node.getRect(); int vWidth=m_syncGen->view()->width(); int vHeight=m_syncGen->view()->height(); Okular::NormalizedRect *nodeNormRect; #define NOEXP #ifndef NOEXP int x,y,height; int x_next,y_next,height_next; int nodeTextLength = nodeText.length(); if (nodeTextLength==1) { nodeNormRect=new Okular::NormalizedRect (r,vWidth,vHeight); tp->append(nodeText,nodeNormRect,nodeNormRect->bottom,0,(nodeText=="\n")); } else { for (int i=0;iappend(nodeText,nodeNormRect/*,0*/); #endif } DOM::Node child = node.firstChild(); while ( !child.isNull() ) { recursiveExploreNodes(child,tp); child = child.nextSibling(); } } void CHMGenerator::additionalRequestData() { Okular::Page * page=m_request->page(); const bool genObjectRects = !m_rectsGenerated.at( m_request->page()->number() ); const bool genTextPage = !m_request->page()->hasTextPage() && genObjectRects; if (genObjectRects || genTextPage ) { DOM::HTMLDocument domDoc=m_syncGen->htmlDocument(); // only generate object info when generating a full page not a thumbnail if ( genObjectRects ) { QLinkedList< Okular::ObjectRect * > objRects; int xScale=m_syncGen->view()->width(); int yScale=m_syncGen->view()->height(); // getting links DOM::HTMLCollection coll=domDoc.links(); DOM::Node n; QRect r; if (! coll.isNull() ) { int size=coll.length(); for(int i=0;ipage()->setObjectRects( objRects ); m_rectsGenerated[ m_request->page()->number() ] = true; } if ( genTextPage ) { Okular::TextPage *tp=new Okular::TextPage(); recursiveExploreNodes(domDoc,tp); page->setTextPage (tp); } } } Okular::TextPage* CHMGenerator::textPage( Okular::TextRequest * request ) { userMutex()->lock(); const Okular::Page *page = request->page(); m_syncGen->view()->resize(page->width(), page->height()); preparePageForSyncOperation(m_pageUrl[page->number()]); Okular::TextPage *tp=new Okular::TextPage(); recursiveExploreNodes( m_syncGen->htmlDocument(), tp); userMutex()->unlock(); return tp; } QVariant CHMGenerator::metaData( const QString &key, const QVariant &option ) const { if ( key == QLatin1String("NamedViewport") && !option.toString().isEmpty() ) { const int pos = option.toString().indexOf(QLatin1Char('#')); QString tmpUrl = pos == -1 ? option.toString() : option.toString().left(pos); Okular::DocumentViewport viewport; QMap::const_iterator it = m_urlPage.find(tmpUrl); if (it != m_urlPage.end()) { viewport.pageNumber = it.value(); return viewport.toString(); } } else if ( key == QLatin1String("DocumentTitle") ) { return m_file->title(); } return QVariant(); } /* kate: replace-tabs on; tab-width 4; */ #include "generator_chm.moc" diff --git a/generators/chm/lib/helper_search_index.cpp b/generators/chm/lib/helper_search_index.cpp index 6f2b515ed..92d50f34b 100644 --- a/generators/chm/lib/helper_search_index.cpp +++ b/generators/chm/lib/helper_search_index.cpp @@ -1,492 +1,492 @@ /* * Kchmviewer - a CHM and EPUB file viewer with broad language support * Copyright (C) 2004-2014 George Yunaev, gyunaev@ulduzsoft.com * * 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 3 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, see . */ #include #include #include "ebook.h" #include "ebook_search.h" #include "helper_search_index.h" static const int DICT_VERSION = 4; namespace QtAs { // Those characters are splitters (i.e. split the word), but added themselves into dictionary too. // This makes the dictionary MUCH larger, but ensure that for the piece of "window->print" both // search for "print" and "->print" will find it. static const char SPLIT_CHARACTERS[] = "!()*&^%#@[]{}':;,.?/|/?<>\\-+=~`"; // Those characters are parts of word - for example, '_' is here, and search for _debug will find only _debug. static const char WORD_CHARACTERS[] = "$_"; struct Term { Term() : frequency(-1) {} Term( const QString &t, int f, const QVector &l ) : term( t ), frequency( f ), documents( l ) {} QString term; int frequency; QVectordocuments; bool operator<( const Term &i2 ) const { return frequency < i2.frequency; } }; QDataStream &operator>>( QDataStream &s, Document &l ) { s >> l.docNumber; s >> l.frequency; return s; } QDataStream &operator<<( QDataStream &s, const Document &l ) { s << (short)l.docNumber; s << (short)l.frequency; return s; } Index::Index() : QObject( nullptr ) { lastWindowClosed = false; - connect( qApp, SIGNAL( lastWindowClosed() ), this, SLOT( setLastWinClosed() ) ); + connect( qApp, &QGuiApplication::lastWindowClosed, this, &Index::setLastWinClosed ); } void Index::setLastWinClosed() { lastWindowClosed = true; } bool Index::makeIndex(const QList< QUrl >& docs, EBook *chmFile ) { if ( docs.isEmpty() ) return false; docList = docs; if ( chmFile->hasFeature( EBook::FEATURE_ENCODING ) ) entityDecoder.changeEncoding( QTextCodec::codecForName( chmFile->currentEncoding().toUtf8() ) ); QList< QUrl >::ConstIterator it = docList.constBegin(); int steps = docList.count() / 100; if ( !steps ) steps++; int prog = 0; for ( int i = 0; it != docList.constEnd(); ++it, ++i ) { if ( lastWindowClosed ) return false; QUrl filename = *it; QStringList terms; if ( parseDocumentToStringlist( chmFile, filename, terms ) ) { for ( QStringList::ConstIterator tit = terms.constBegin(); tit != terms.constEnd(); ++tit ) insertInDict( *tit, i ); } if ( i%steps == 0 ) { prog++; prog = qMin( prog, 99 ); emit indexingProgress( prog, tr("Processing document %1") .arg( (*it).path() ) ); } } emit indexingProgress( 100, tr("Processing completed") ); return true; } void Index::insertInDict( const QString &str, int docNum ) { Entry *e = nullptr; if ( dict.count() ) e = dict[ str ]; if ( e ) { if ( e->documents.last().docNumber != docNum ) e->documents.append( Document(docNum, 1 ) ); else e->documents.last().frequency++; } else { dict.insert( str, new Entry( docNum ) ); } } bool Index::parseDocumentToStringlist(EBook *chmFile, const QUrl& filename, QStringList& tokenlist ) { QString parsedbuf, parseentity, text; if ( !chmFile->getFileContentAsString( text, filename ) || text.isEmpty() ) { qWarning( "Search index generator: could not retrieve the document content for %s", qPrintable( filename.toString() ) ); return false; } m_charssplit = SPLIT_CHARACTERS; m_charsword = WORD_CHARACTERS; tokenlist.clear(); // State machine states enum state_t { STATE_OUTSIDE_TAGS, // outside HTML tags; parse text STATE_IN_HTML_TAG, // inside HTML tags; wait for end tag STATE_IN_QUOTES, // inside HTML tags and inside quotes; wait for end quote (in var QuoteChar) STATE_IN_HTML_ENTITY // inside HTML entity; parse the entity }; state_t state = STATE_OUTSIDE_TAGS; QChar QuoteChar; // used in STATE_IN_QUOTES for ( int j = 0; j < text.length(); j++ ) { QChar ch = text[j]; if ( (j % 20000) == 0 ) qApp->processEvents( QEventLoop::ExcludeUserInputEvents ); if ( state == STATE_IN_HTML_TAG ) { // We are inside HTML tag. // Ignore everything until we see '>' (end of HTML tag) or quote char (quote start) if ( ch == '"' || ch == '\'' ) { state = STATE_IN_QUOTES; QuoteChar = ch; } else if ( ch == '>' ) state = STATE_OUTSIDE_TAGS; continue; } else if ( state == STATE_IN_QUOTES ) { // We are inside quoted text inside HTML tag. // Ignore everything until we see the quote character again if ( ch == QuoteChar ) state = STATE_IN_HTML_TAG; continue; } else if ( state == STATE_IN_HTML_ENTITY ) { // We are inside encoded HTML entity (like  ). // Collect to parsedbuf everything until we see ; if ( ch.isLetterOrNumber() ) { // get next character of this entity parseentity.append( ch ); continue; } // The entity ended state = STATE_OUTSIDE_TAGS; // Some shitty HTML does not terminate entities correctly. Screw it. if ( ch != ';' && ch != '<' ) { if ( parseentity.isEmpty() ) { // straight '&' symbol. Add and continue. parsedbuf += "&"; } else qWarning( "Index::parseDocument: incorrectly terminated HTML entity '&%s%c', ignoring", qPrintable( parseentity ), ch.toLatin1() ); j--; // parse this character again, but in different state continue; } // Don't we have a space? if ( parseentity.toLower() != "nbsp" ) { QString entity = entityDecoder.decode( parseentity ); if ( entity.isNull() ) { // decodeEntity() already printed error message //qWarning( "Index::parseDocument: failed to decode entity &%s;", parsedbuf.ascii() ); continue; } parsedbuf += entity; continue; } else ch = ' '; // We got a space, so treat it like it, and not add it to parsebuf } // // Now process STATE_OUTSIDE_TAGS // // Check for start of HTML tag, and switch to STATE_IN_HTML_TAG if it is if ( ch == '<' ) { state = STATE_IN_HTML_TAG; goto tokenize_buf; } // Check for start of HTML entity if ( ch == '&' ) { state = STATE_IN_HTML_ENTITY; parseentity = QString(); continue; } // Replace quote by ' - quotes are used in search window to set the phrase if ( ch == '"' ) ch = '\''; // Ok, we have a valid character outside HTML tags, and probably some in buffer already. // If it is char or letter, add it and continue if ( ch.isLetterOrNumber() || m_charsword.indexOf( ch ) != -1 ) { parsedbuf.append( ch ); continue; } // If it is a split char, add the word to the dictionary, and then add the char itself. if ( m_charssplit.indexOf( ch ) != -1 ) { if ( !parsedbuf.isEmpty() ) tokenlist.push_back( parsedbuf.toLower() ); tokenlist.push_back( ch.toLower() ); parsedbuf = QString(); continue; } tokenize_buf: // Just add the word; it is most likely a space or terminated by tokenizer. if ( !parsedbuf.isEmpty() ) { tokenlist.push_back( parsedbuf.toLower() ); parsedbuf = QString(); } } // Add the last word if still here - for broken htmls. if ( !parsedbuf.isEmpty() ) tokenlist.push_back( parsedbuf.toLower() ); return true; } void Index::writeDict( QDataStream& stream ) { stream << DICT_VERSION; stream << m_charssplit; stream << m_charsword; // Document list stream << docList; // Dictionary for( QHash::ConstIterator it = dict.constBegin(); it != dict.constEnd(); ++it ) { stream << it.key(); stream << (int) it.value()->documents.count(); stream << it.value()->documents; } } bool Index::readDict( QDataStream& stream ) { dict.clear(); docList.clear(); QString key; int version, numOfDocs; stream >> version; if ( version < 2 ) return false; stream >> m_charssplit; stream >> m_charsword; // Read the document list stream >> docList; while ( !stream.atEnd() ) { stream >> key; stream >> numOfDocs; QVector docs( numOfDocs ); stream >> docs; dict.insert( key, new Entry( docs ) ); } return dict.size() > 0; } QList< QUrl > Index::query(const QStringList &terms, const QStringList &termSeq, const QStringList &seqWords, EBook *chmFile ) { QList termList; QStringList::ConstIterator it = terms.begin(); for ( it = terms.begin(); it != terms.end(); ++it ) { Entry *e = nullptr; if ( dict[ *it ] ) { e = dict[ *it ]; termList.append( Term( *it, e->documents.count(), e->documents ) ); } else { return QList< QUrl >(); } } if ( !termList.count() ) return QList< QUrl >(); std::sort(termList.begin(), termList.end()); QVector minDocs = termList.takeFirst().documents; for(const Term &t : qAsConst(termList)) { const QVector docs = t.documents; for(QVector::Iterator minDoc_it = minDocs.begin(); minDoc_it != minDocs.end(); ) { bool found = false; for (QVector::ConstIterator doc_it = docs.constBegin(); doc_it != docs.constEnd(); ++doc_it ) { if ( (*minDoc_it).docNumber == (*doc_it).docNumber ) { (*minDoc_it).frequency += (*doc_it).frequency; found = true; break; } } if ( !found ) minDoc_it = minDocs.erase( minDoc_it ); else ++minDoc_it; } } QList< QUrl > results; std::sort(minDocs.begin(), minDocs.end()); if ( termSeq.isEmpty() ) { for(const Document &doc : qAsConst(minDocs)) results << docList.at((int)doc.docNumber); return results; } QUrl fileName; for(const Document &doc : qAsConst(minDocs)) { fileName = docList[ (int)doc.docNumber ]; if ( searchForPhrases( termSeq, seqWords, fileName, chmFile ) ) results << fileName; } return results; } bool Index::searchForPhrases( const QStringList &phrases, const QStringList &words, const QUrl &filename, EBook * chmFile ) { QStringList parsed_document; if ( !parseDocumentToStringlist( chmFile, filename, parsed_document ) ) return false; miniDict.clear(); // Initialize the dictionary with the words in phrase(s) for ( const QString &word : words ) miniDict.insert( word, new PosEntry( 0 ) ); // Fill the dictionary with the words from the document unsigned int word_offset = 3; for ( QStringList::ConstIterator it = parsed_document.constBegin(); it != parsed_document.constEnd(); it++, word_offset++ ) { PosEntry * entry = miniDict[ *it ]; if ( entry ) entry->positions.append( word_offset ); } // Dump it /* QDictIterator it( miniDict ); for( ; it.current(); ++it ) { QString text( it.currentKey() ); QValueList pos = miniDict[text]->positions; for ( unsigned int i = 1; i < pos.size(); i++ ) text += " " + QString::number( pos[i] ); qDebug( "%s", text.ascii()); } */ QList first_word_positions; for ( QStringList::ConstIterator phrase_it = phrases.constBegin(); phrase_it != phrases.constEnd(); phrase_it++ ) { QStringList phrasewords = phrase_it->split( ' ' ); first_word_positions = miniDict[ phrasewords[0] ]->positions; for ( int j = 1; j < phrasewords.count(); ++j ) { QList next_word_it = miniDict[ phrasewords[j] ]->positions; QList::iterator dict_it = first_word_positions.begin(); while ( dict_it != first_word_positions.end() ) { if ( next_word_it.indexOf( *dict_it + 1 ) != -1 ) { (*dict_it)++; ++dict_it; } else dict_it = first_word_positions.erase( dict_it ); } } } if ( first_word_positions.count() ) return true; return false; } }; diff --git a/part.cpp b/part.cpp index 703621875..ddc09f39c 100644 --- a/part.cpp +++ b/part.cpp @@ -1,3726 +1,3726 @@ /*************************************************************************** * 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[ 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[ 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; for ( 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 ) { for ( 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, QOverload<>::of(&Part::completed), this, &Part::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()) ); + connect( m_document, SIGNAL(quit()), parent, SLOT(slotQuit()) ); // clazy:exclude=old-style-connect 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->setContentsMargins( 0, 0, 0, 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->setContentsMargins( 4, 4, 4, 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->setContentsMargins( 0, 0, 0, 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_pageView, &PageView::triggerSearch, this, [this] (const QString& searchText){ m_findBar->startSearch(searchText); slotShowFindBar(); } ); 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->setContentsMargins( 0, 0, 0, 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_findBar, &FindBar::forwardKeyPressEvent, m_pageView, &PageView::externalKeyPressEvent ); + connect( m_findBar, &FindBar::onCloseButtonPressed, m_pageView, QOverload<>::of(&PageView::setFocus) ); + connect( m_miniBar, &MiniBar::forwardKeyPressEvent, m_pageView, &PageView::externalKeyPressEvent ); connect( m_pageView.data(), &PageView::escPressed, m_findBar, &FindBar::resetSearch ); - connect( m_pageNumberTool, SIGNAL(forwardKeyPressEvent(QKeyEvent*)), m_pageView, SLOT(externalKeyPressEvent(QKeyEvent*))); + connect( m_pageNumberTool, &MiniBar::forwardKeyPressEvent, m_pageView, &PageView::externalKeyPressEvent); 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; m_openContainingFolder = 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 ); m_openContainingFolder = ac->addAction(QStringLiteral("open_containing_folder")); m_openContainingFolder->setText(i18n("Open Con&taining Folder")); m_openContainingFolder->setIcon( QIcon::fromTheme( QStringLiteral("document-open-folder") ) ); connect(m_openContainingFolder, &QAction::triggered, this, &Part::slotOpenContainingFolder); m_openContainingFolder->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; } } emit m_bExtension->openUrlNotify(); emit 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 page) { if ( page <= m_document->pages() ) m_document->setViewportPage( page - 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))); + connect(p, QOverload::of(&QProcess::finished), this, &Part::psTransformEnded); 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( 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 ); if( m_openContainingFolder ) m_openContainingFolder->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(); /* TRANSLATORS: Adding the reason (%2) why the opening failed (if any). */ QString errorMessage = i18n("Could not open %1. %2", url.toDisplayString(), QString("\n%1").arg(m_document->openError()) ); KMessageBox::error( widget(), errorMessage ); } 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->setContentsMargins(6, 6, 6, 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, nullptr, true ); } void Part::slotPreviousPage() { if ( m_document->isOpened() && !(m_document->currentPage() < 1) ) m_document->setViewportPage( m_document->currentPage() - 1, nullptr, true ); } void Part::slotNextPage() { if ( m_document->isOpened() && m_document->currentPage() < (m_document->pages() - 1) ) m_document->setViewportPage( m_document->currentPage() + 1, nullptr, true ); } void Part::slotGotoFirst() { if ( m_document->isOpened() ) { m_document->setViewportPage( 0, nullptr, true); 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, nullptr, true); 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, &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, &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, nullptr, true ); } } 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, nullptr, true ); } } 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..." ), 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..." ), 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 QLinkedList< Okular::Annotation* > annotations = m_document->page( pageno )->annotations(); for ( const Okular::Annotation *ann : 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[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); } void Part::slotOpenContainingFolder() { KIO::highlightInFileManager( { QUrl(localFilePath()) } ); } } // namespace Okular #include "part.moc" /* kate: replace-tabs on; indent-width 4; */ diff --git a/shell/shell.cpp b/shell/shell.cpp index 387a8e69b..c484407c3 100644 --- a/shell/shell.cpp +++ b/shell/shell.cpp @@ -1,845 +1,846 @@ /*************************************************************************** * 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; } // Handle middle button click events on the tab bar if (obj == m_tabWidget && event->type() == QEvent::MouseButtonRelease) { QMouseEvent* mEvent = static_cast(event); if (mEvent->button() == Qt::MiddleButton) { int tabIndex = m_tabWidget->tabBar()->tabAt(mEvent->pos()); if (tabIndex != -1) { closeTab(tabIndex); 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( const TabState &tab : qAsConst( m_tabs ) ) { tab.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 ); closeTab( activeTab ); } } } } } 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 ); m_undoCloseTab = actionCollection()->addAction(QStringLiteral("undo-close-tab")); m_undoCloseTab->setText( i18n("Undo close tab") ); actionCollection()->setDefaultShortcut(m_undoCloseTab, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_T)); m_undoCloseTab->setIcon(QIcon::fromTheme(QStringLiteral("edit-undo"))); m_undoCloseTab->setEnabled( false ); connect( m_undoCloseTab, &QAction::triggered, this, &Shell::undoCloseTab ); } 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( const TabState &tab : qAsConst( m_tabs ) ) { urls.append( tab.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; for ( const QString &mimeName : qAsConst(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; for ( auto it = namedGlobs.cbegin(); it != namedGlobs.cend(); ++it ) { namePatterns.append( it.key() + QLatin1String(" (") + it.value().join( QLatin1Char(' ') ) + QLatin1Char(')') ); } const QStringList allGlobPatterns = globPatterns.values(); namePatterns.prepend( i18n("All files (*)") ); namePatterns.prepend( i18n("All supported files (%1)", allGlobPatterns.join( QLatin1Char(' ') ) ) ); dlg->setNameFilters( namePatterns ); dlg->setWindowTitle( i18n("Open Document") ); if ( dlg->exec() && dlg ) { const QList urlList = dlg->selectedUrls(); for (const QUrl &url : urlList) { 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 = QStringLiteral("ShowTabWarning"); KMessageBox::ButtonCode dummy = KMessageBox::Yes; 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"), 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; QUrl url = part->url(); 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 ); m_undoCloseTab->setEnabled( true ); m_closedTabUrls.append( url ); 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 ); closeTab( m_tabs.size() - 1 ); m_recent->removeUrl( url ); } } 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(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)) ); + // We're abusing the fact we know the part is our part here + connect( this, SIGNAL(moveSplitter(int)), part, SLOT(moveSplitter(int)) ); // clazy:exclude=old-style-connect + connect( part, SIGNAL(enablePrintAction(bool)), this, SLOT(setPrintEnabled(bool))); // clazy:exclude=old-style-connect + connect( part, SIGNAL(enableCloseAction(bool)), this, SLOT(setCloseEnabled(bool))); // clazy:exclude=old-style-connect + connect( part, SIGNAL(mimeTypeChanged(QMimeType)), this, SLOT(setTabIcon(QMimeType))); // clazy:exclude=old-style-connect + connect( part, SIGNAL(urlsDropped(QList)), this, SLOT(handleDroppedUrls(QList)) ); // clazy:exclude=old-style-connect + connect( part, SIGNAL(fitWindowToPage(QSize,QSize)), this, SLOT(slotFitWindowToPage(QSize,QSize)) ); // clazy:exclude=old-style-connect } 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::undoCloseTab() { if ( m_closedTabUrls.isEmpty() ) { return; } const QUrl lastTabUrl = m_closedTabUrls.takeLast(); if ( m_closedTabUrls.isEmpty() ) { m_undoCloseTab->setEnabled(false); } openUrl(lastTabUrl); } 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 ) { for ( 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); emit moveSplitter(pageSize.width()); } /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/annotationwidgets.cpp b/ui/annotationwidgets.cpp index 81d723e39..ddff9ae45 100644 --- a/ui/annotationwidgets.cpp +++ b/ui/annotationwidgets.cpp @@ -1,893 +1,893 @@ /*************************************************************************** * Copyright (C) 2006 by Pino Toscano * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "annotationwidgets.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 "core/document.h" #include "guiutils.h" #include "pagepainter.h" #define FILEATTACH_ICONSIZE 48 PixmapPreviewSelector::PixmapPreviewSelector( QWidget * parent, PreviewPosition position ) : QWidget( parent ), m_previewPosition( position ) { QVBoxLayout * mainlay = new QVBoxLayout( this ); mainlay->setContentsMargins( 0, 0, 0, 0 ); QHBoxLayout * toplay = new QHBoxLayout( this ); toplay->setContentsMargins( 0, 0, 0, 0 ); mainlay->addLayout( toplay ); m_comboItems = new KComboBox( this ); toplay->addWidget( m_comboItems ); m_stampPushButton = new QPushButton(QIcon::fromTheme( "document-open" ), QString(), this ); m_stampPushButton->setVisible( false ); m_stampPushButton->setToolTip( i18nc( "@info:tooltip", "Select a custom stamp symbol from file") ); toplay->addWidget(m_stampPushButton); m_iconLabel = new QLabel( this ); switch ( m_previewPosition ) { case Side: toplay->addWidget( m_iconLabel ); break; case Below: mainlay->addWidget( m_iconLabel ); mainlay->setAlignment( m_iconLabel, Qt::AlignHCenter ); break; } m_iconLabel->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); m_iconLabel->setAlignment( Qt::AlignCenter ); m_iconLabel->setFrameStyle( QFrame::StyledPanel ); setPreviewSize( 32 ); setFocusPolicy( Qt::TabFocus ); setFocusProxy( m_comboItems ); - connect( m_comboItems, SIGNAL(currentIndexChanged(QString)), this, SLOT(iconComboChanged(QString)) ); + connect( m_comboItems, QOverload::of(&QComboBox::currentIndexChanged), this, &PixmapPreviewSelector::iconComboChanged ); connect( m_comboItems, &QComboBox::editTextChanged, this, &PixmapPreviewSelector::iconComboChanged ); connect( m_stampPushButton, &QPushButton::clicked, this, &PixmapPreviewSelector::selectCustomStamp ); } PixmapPreviewSelector::~PixmapPreviewSelector() { } void PixmapPreviewSelector::setIcon( const QString& icon ) { int id = m_comboItems->findData( QVariant( icon ), Qt::UserRole, Qt::MatchFixedString ); if ( id == -1 ) id = m_comboItems->findText( icon, Qt::MatchFixedString ); if ( id > -1 ) { m_comboItems->setCurrentIndex( id ); } else if ( m_comboItems->isEditable() ) { m_comboItems->addItem( icon, QVariant( icon ) ); m_comboItems->setCurrentIndex( m_comboItems->findText( icon, Qt::MatchFixedString ) ); } } QString PixmapPreviewSelector::icon() const { return m_icon; } void PixmapPreviewSelector::addItem( const QString& item, const QString& id ) { m_comboItems->addItem( item, QVariant( id ) ); setIcon( m_icon ); } void PixmapPreviewSelector::setPreviewSize( int size ) { m_previewSize = size; switch( m_previewPosition ) { case Side: m_iconLabel->setFixedSize( m_previewSize + 8, m_previewSize + 8 ); break; case Below: m_iconLabel->setFixedSize( 3 * m_previewSize + 8, m_previewSize + 8 ); break; } iconComboChanged( m_icon ); } int PixmapPreviewSelector::previewSize() const { return m_previewSize; } void PixmapPreviewSelector::setEditable( bool editable ) { m_comboItems->setEditable( editable ); m_stampPushButton->setVisible( editable ); } void PixmapPreviewSelector::iconComboChanged( const QString& icon ) { int id = m_comboItems->findText( icon, Qt::MatchFixedString ); if ( id >= 0 ) { m_icon = m_comboItems->itemData( id ).toString(); } else { m_icon = icon; } QPixmap pixmap = GuiUtils::loadStamp( m_icon, m_previewSize ); const QRect cr = m_iconLabel->contentsRect(); if ( pixmap.width() > cr.width() || pixmap.height() > cr.height() ) pixmap = pixmap.scaled( cr.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation ); m_iconLabel->setPixmap( pixmap ); emit iconChanged( m_icon ); } void PixmapPreviewSelector::selectCustomStamp() { const QString customStampFile = QFileDialog::getOpenFileName(this, i18nc("@title:window file chooser", "Select custom stamp symbol"), QString(), i18n("*.ico *.png *.xpm *.svg *.svgz | Icon Files (*.ico *.png *.xpm *.svg *.svgz)") ); if ( !customStampFile.isEmpty() ) { QPixmap pixmap = GuiUtils::loadStamp( customStampFile, m_previewSize ); if( pixmap.isNull() ) { KMessageBox::sorry( this, xi18nc("@info", "Could not load the file %1", customStampFile ), i18nc("@title:window", "Invalid file") ); } else { m_comboItems->setEditText(customStampFile); } } } AnnotationWidget * AnnotationWidgetFactory::widgetFor( Okular::Annotation * ann ) { switch ( ann->subType() ) { case Okular::Annotation::AStamp: return new StampAnnotationWidget( ann ); break; case Okular::Annotation::AText: return new TextAnnotationWidget( ann ); break; case Okular::Annotation::ALine: return new LineAnnotationWidget( ann ); break; case Okular::Annotation::AHighlight: return new HighlightAnnotationWidget( ann ); break; case Okular::Annotation::AInk: return new InkAnnotationWidget( ann ); break; case Okular::Annotation::AGeom: return new GeomAnnotationWidget( ann ); break; case Okular::Annotation::AFileAttachment: return new FileAttachmentAnnotationWidget( ann ); break; case Okular::Annotation::ACaret: return new CaretAnnotationWidget( ann ); break; // shut up gcc default: ; } // cases not covered yet: return a generic widget return new AnnotationWidget( ann ); } AnnotationWidget::AnnotationWidget( Okular::Annotation * ann ) : m_ann( ann ) { } AnnotationWidget::~AnnotationWidget() { } Okular::Annotation::SubType AnnotationWidget::annotationType() const { return m_ann->subType(); } QWidget * AnnotationWidget::appearanceWidget() { if ( m_appearanceWidget ) return m_appearanceWidget; m_appearanceWidget = createAppearanceWidget(); return m_appearanceWidget; } QWidget * AnnotationWidget::extraWidget() { if ( m_extraWidget ) return m_extraWidget; m_extraWidget = createExtraWidget(); return m_extraWidget; } void AnnotationWidget::applyChanges() { if (m_colorBn) m_ann->style().setColor( m_colorBn->color() ); if (m_opacity) m_ann->style().setOpacity( (double)m_opacity->value() / 100.0 ); } QWidget * AnnotationWidget::createAppearanceWidget() { QWidget * widget = new QWidget(); QFormLayout * formlayout = new QFormLayout( widget ); formlayout->setLabelAlignment( Qt::AlignRight ); formlayout->setFieldGrowthPolicy( QFormLayout::AllNonFixedFieldsGrow ); createStyleWidget( formlayout ); return widget; } void AnnotationWidget::createStyleWidget( QFormLayout * formlayout ) { Q_UNUSED( formlayout ); } void AnnotationWidget::addColorButton( QWidget * widget, QFormLayout * formlayout ) { m_colorBn = new KColorButton( widget ); m_colorBn->setColor( m_ann->style().color() ); formlayout->addRow( i18n( "&Color:" ), m_colorBn ); connect( m_colorBn, &KColorButton::changed, this, &AnnotationWidget::dataChanged ); } void AnnotationWidget::addOpacitySpinBox( QWidget * widget, QFormLayout * formlayout ) { m_opacity = new QSpinBox( widget ); m_opacity->setRange( 0, 100 ); m_opacity->setValue( (int)( m_ann->style().opacity() * 100 ) ); m_opacity->setSuffix( i18nc( "Suffix for the opacity level, eg '80 %'", " %" ) ); formlayout->addRow( i18n( "&Opacity:" ), m_opacity); - connect( m_opacity, SIGNAL(valueChanged(int)), this, SIGNAL(dataChanged()) ); + connect( m_opacity, QOverload::of(&QSpinBox::valueChanged), this, &AnnotationWidget::dataChanged ); } void AnnotationWidget::addVerticalSpacer( QFormLayout * formlayout ) { formlayout->addItem( new QSpacerItem( 0, 5, QSizePolicy::Fixed, QSizePolicy::Fixed ) ); } QWidget * AnnotationWidget::createExtraWidget() { return nullptr; } TextAnnotationWidget::TextAnnotationWidget( Okular::Annotation * ann ) : AnnotationWidget( ann ) { m_textAnn = static_cast< Okular::TextAnnotation * >( ann ); } void TextAnnotationWidget::createStyleWidget( QFormLayout * formlayout ) { QWidget * widget = qobject_cast( formlayout->parent() ); if ( m_textAnn->textType() == Okular::TextAnnotation::Linked ) { createPopupNoteStyleUi( widget, formlayout ); } else if ( m_textAnn->textType() == Okular::TextAnnotation::InPlace ) { if ( isTypewriter() ) createTypewriterStyleUi( widget, formlayout ); else createInlineNoteStyleUi( widget, formlayout ); } } void TextAnnotationWidget::applyChanges() { AnnotationWidget::applyChanges(); if ( m_textAnn->textType() == Okular::TextAnnotation::Linked ) { Q_ASSERT( m_pixmapSelector ); m_textAnn->setTextIcon( m_pixmapSelector->icon() ); } else if ( m_textAnn->textType() == Okular::TextAnnotation::InPlace ) { Q_ASSERT( m_fontReq ); m_textAnn->setTextFont( m_fontReq->font() ); if ( !isTypewriter() ) { Q_ASSERT( m_textAlign && m_spinWidth ); m_textAnn->setInplaceAlignment( m_textAlign->currentIndex() ); m_textAnn->style().setWidth( m_spinWidth->value() ); } else { Q_ASSERT( m_textColorBn ); m_textAnn->setTextColor( m_textColorBn->color() ); } } } void TextAnnotationWidget::createPopupNoteStyleUi( QWidget * widget, QFormLayout * formlayout ) { addColorButton( widget, formlayout ); addOpacitySpinBox( widget, formlayout ); addVerticalSpacer( formlayout ); addPixmapSelector( widget, formlayout ); } void TextAnnotationWidget::createInlineNoteStyleUi( QWidget * widget, QFormLayout * formlayout ) { addColorButton( widget, formlayout ); addOpacitySpinBox( widget, formlayout ); addVerticalSpacer( formlayout ); addFontRequester( widget, formlayout ); addTextAlignComboBox( widget, formlayout ); addVerticalSpacer( formlayout ); addWidthSpinBox( widget, formlayout ); } void TextAnnotationWidget::createTypewriterStyleUi( QWidget * widget, QFormLayout * formlayout ) { addFontRequester( widget, formlayout ); addTextColorButton( widget, formlayout ); } void TextAnnotationWidget::addPixmapSelector( QWidget * widget, QFormLayout * formlayout ) { m_pixmapSelector = new PixmapPreviewSelector( widget ); formlayout->addRow( i18n( "Icon:" ), m_pixmapSelector ); m_pixmapSelector->addItem( i18n( "Comment" ), QStringLiteral("Comment") ); m_pixmapSelector->addItem( i18n( "Help" ), QStringLiteral("Help") ); m_pixmapSelector->addItem( i18n( "Insert" ), QStringLiteral("Insert") ); m_pixmapSelector->addItem( i18n( "Key" ), QStringLiteral("Key") ); m_pixmapSelector->addItem( i18n( "New paragraph" ), QStringLiteral("NewParagraph") ); m_pixmapSelector->addItem( i18n( "Note" ), QStringLiteral("Note") ); m_pixmapSelector->addItem( i18n( "Paragraph" ), QStringLiteral("Paragraph") ); m_pixmapSelector->setIcon( m_textAnn->textIcon() ); connect( m_pixmapSelector, &PixmapPreviewSelector::iconChanged, this, &AnnotationWidget::dataChanged ); } void TextAnnotationWidget::addFontRequester( QWidget * widget, QFormLayout * formlayout ) { m_fontReq = new KFontRequester( widget ); formlayout->addRow( i18n( "Font:" ), m_fontReq ); m_fontReq->setFont( m_textAnn->textFont() ); connect( m_fontReq, &KFontRequester::fontSelected, this, &AnnotationWidget::dataChanged ); } void TextAnnotationWidget::addTextColorButton( QWidget * widget, QFormLayout * formlayout ) { m_textColorBn = new KColorButton( widget ); m_textColorBn->setColor( m_textAnn->textColor() ); formlayout->addRow( i18n( "Text &color:" ), m_textColorBn ); connect( m_textColorBn, &KColorButton::changed, this, &AnnotationWidget::dataChanged ); } void TextAnnotationWidget::addTextAlignComboBox( QWidget * widget, QFormLayout * formlayout ) { m_textAlign = new KComboBox( widget ); formlayout->addRow( i18n( "&Align:" ), m_textAlign ); m_textAlign->addItem( i18n("Left") ); m_textAlign->addItem( i18n("Center") ); m_textAlign->addItem( i18n("Right") ); m_textAlign->setCurrentIndex( m_textAnn->inplaceAlignment() ); - connect( m_textAlign, SIGNAL(currentIndexChanged(int)), this, SIGNAL(dataChanged()) ); + connect( m_textAlign, QOverload::of(&KComboBox::currentIndexChanged), this, &AnnotationWidget::dataChanged ); } void TextAnnotationWidget::addWidthSpinBox( QWidget * widget, QFormLayout * formlayout ) { m_spinWidth = new QDoubleSpinBox( widget ); formlayout->addRow( i18n( "Border &width:" ), m_spinWidth ); m_spinWidth->setRange( 0, 100 ); m_spinWidth->setValue( m_textAnn->style().width() ); m_spinWidth->setSingleStep( 0.1 ); - connect( m_spinWidth, SIGNAL(valueChanged(double)), this, SIGNAL(dataChanged()) ); + connect( m_spinWidth, QOverload::of(&QDoubleSpinBox::valueChanged), this, &AnnotationWidget::dataChanged ); } StampAnnotationWidget::StampAnnotationWidget( Okular::Annotation * ann ) : AnnotationWidget( ann ), m_pixmapSelector( nullptr ) { m_stampAnn = static_cast< Okular::StampAnnotation * >( ann ); } void StampAnnotationWidget::createStyleWidget( QFormLayout * formlayout ) { QWidget * widget = qobject_cast( formlayout->parent() ); KMessageWidget * brokenStampSupportWarning = new KMessageWidget( widget ); brokenStampSupportWarning->setText( xi18nc("@info", "experimental feature." "Stamps inserted in PDF documents are not visible in PDF readers other than Okular.") ); brokenStampSupportWarning->setMessageType( KMessageWidget::Warning ); brokenStampSupportWarning->setWordWrap( true ); brokenStampSupportWarning->setCloseButtonVisible( false ); formlayout->insertRow( 0, brokenStampSupportWarning ); addOpacitySpinBox( widget, formlayout ); addVerticalSpacer( formlayout ); m_pixmapSelector = new PixmapPreviewSelector( widget, PixmapPreviewSelector::Below ); formlayout->addRow( i18n( "Stamp symbol:" ), m_pixmapSelector ); m_pixmapSelector->setEditable( true ); m_pixmapSelector->addItem( i18n( "Okular" ), QStringLiteral("okular") ); m_pixmapSelector->addItem( i18n( "Bookmark" ), QStringLiteral("bookmarks") ); m_pixmapSelector->addItem( i18n( "KDE" ), QStringLiteral("kde") ); m_pixmapSelector->addItem( i18n( "Information" ), QStringLiteral("help-about") ); m_pixmapSelector->addItem( i18n( "Approved" ), QStringLiteral("Approved") ); m_pixmapSelector->addItem( i18n( "As Is" ), QStringLiteral("AsIs") ); m_pixmapSelector->addItem( i18n( "Confidential" ), QStringLiteral("Confidential") ); m_pixmapSelector->addItem( i18n( "Departmental" ), QStringLiteral("Departmental") ); m_pixmapSelector->addItem( i18n( "Draft" ), QStringLiteral("Draft") ); m_pixmapSelector->addItem( i18n( "Experimental" ), QStringLiteral("Experimental") ); m_pixmapSelector->addItem( i18n( "Expired" ), QStringLiteral("Expired") ); m_pixmapSelector->addItem( i18n( "Final" ), QStringLiteral("Final") ); m_pixmapSelector->addItem( i18n( "For Comment" ), QStringLiteral("ForComment") ); m_pixmapSelector->addItem( i18n( "For Public Release" ), QStringLiteral("ForPublicRelease") ); m_pixmapSelector->addItem( i18n( "Not Approved" ), QStringLiteral("NotApproved") ); m_pixmapSelector->addItem( i18n( "Not For Public Release" ), QStringLiteral("NotForPublicRelease") ); m_pixmapSelector->addItem( i18n( "Sold" ), QStringLiteral("Sold") ); m_pixmapSelector->addItem( i18n( "Top Secret" ), QStringLiteral("TopSecret") ); m_pixmapSelector->setIcon( m_stampAnn->stampIconName() ); m_pixmapSelector->setPreviewSize( 64 ); connect( m_pixmapSelector, &PixmapPreviewSelector::iconChanged, this, &AnnotationWidget::dataChanged ); } void StampAnnotationWidget::applyChanges() { AnnotationWidget::applyChanges(); m_stampAnn->setStampIconName( m_pixmapSelector->icon() ); } LineAnnotationWidget::LineAnnotationWidget( Okular::Annotation * ann ) : AnnotationWidget( ann ) { m_lineAnn = static_cast< Okular::LineAnnotation * >( ann ); if ( m_lineAnn->linePoints().count() == 2 ) m_lineType = 0; // line else if ( m_lineAnn->lineClosed() ) m_lineType = 1; // polygon else m_lineType = 2; // polyline } void LineAnnotationWidget::createStyleWidget( QFormLayout * formlayout ) { QWidget * widget = qobject_cast( formlayout->parent() ); addColorButton( widget, formlayout ); addOpacitySpinBox( widget, formlayout ); m_spinSize = new QDoubleSpinBox( widget ); m_spinSize->setRange( 1, 100 ); m_spinSize->setValue( m_lineAnn->style().width() ); connect( m_spinSize, QOverload::of(&QDoubleSpinBox::valueChanged), this, &LineAnnotationWidget::dataChanged ); // Straight line if ( m_lineType == 0 ) { addVerticalSpacer( formlayout ); formlayout->addRow( i18n( "&Width:" ), m_spinSize ); //Line Term Styles addVerticalSpacer( formlayout ); m_startStyleCombo = new QComboBox( widget ); formlayout->addRow( i18n( "Line start:" ), m_startStyleCombo); m_endStyleCombo = new QComboBox( widget ); formlayout->addRow( i18n( "Line end:" ), m_endStyleCombo); //FIXME: Where does the tooltip goes?? const QList> termStyles { { Okular::LineAnnotation::Square, i18n( "Square" ) }, { Okular::LineAnnotation::Circle, i18n( "Circle" ) }, { Okular::LineAnnotation::Diamond, i18n( "Diamond" ) }, { Okular::LineAnnotation::OpenArrow, i18n( "Open Arrow" ) }, { Okular::LineAnnotation::ClosedArrow, i18n( "Closed Arrow" ) }, { Okular::LineAnnotation::None, i18n( "None" ) }, { Okular::LineAnnotation::Butt, i18n( "Butt" ) }, { Okular::LineAnnotation::ROpenArrow, i18n( "Right Open Arrow" ) }, { Okular::LineAnnotation::RClosedArrow, i18n( "Right Closed Arrow" ) }, { Okular::LineAnnotation::Slash, i18n( "Slash" ) } }; for ( const auto &item: termStyles ) { const QIcon icon = endStyleIcon( item.first, QGuiApplication::palette().color( QPalette::WindowText ) ); m_startStyleCombo->addItem( icon, item.second ); m_endStyleCombo->addItem( icon, item.second ); } m_startStyleCombo->setCurrentIndex( m_lineAnn->lineStartStyle() ); m_endStyleCombo->setCurrentIndex( m_lineAnn->lineEndStyle() ); //Leaders lengths addVerticalSpacer( formlayout ); m_spinLL = new QDoubleSpinBox( widget ); formlayout->addRow( i18n( "Leader line length:" ), m_spinLL ); m_spinLLE = new QDoubleSpinBox( widget ); formlayout->addRow( i18n( "Leader line extensions length:" ), m_spinLLE ); m_spinLL->setRange( -500, 500 ); m_spinLL->setValue( m_lineAnn->lineLeadingForwardPoint() ); m_spinLLE->setRange( 0, 500 ); m_spinLLE->setValue( m_lineAnn->lineLeadingBackwardPoint() ); connect( m_startStyleCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &LineAnnotationWidget::dataChanged ); connect( m_endStyleCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &LineAnnotationWidget::dataChanged ); - connect( m_spinLL, SIGNAL(valueChanged(double)), this, SIGNAL(dataChanged()) ); - connect( m_spinLLE, SIGNAL(valueChanged(double)), this, SIGNAL(dataChanged()) ); + connect( m_spinLL, QOverload::of(&QDoubleSpinBox::valueChanged), this, &LineAnnotationWidget::dataChanged ); + connect( m_spinLLE, QOverload::of(&QDoubleSpinBox::valueChanged), this, &LineAnnotationWidget::dataChanged ); } else if ( m_lineType == 1 ) // Polygon { QHBoxLayout * colorlay = new QHBoxLayout(); m_useColor = new QCheckBox( i18n( "Enabled" ), widget ); colorlay->addWidget( m_useColor ); m_innerColor = new KColorButton( widget ); colorlay->addWidget( m_innerColor); formlayout->addRow( i18n( "Shape fill:" ), colorlay ); m_innerColor->setColor( m_lineAnn->lineInnerColor() ); if ( m_lineAnn->lineInnerColor().isValid() ) { m_useColor->setChecked( true ); } else { m_innerColor->setEnabled( false ); } addVerticalSpacer( formlayout ); formlayout->addRow( i18n( "&Width:" ), m_spinSize ); connect( m_innerColor, &KColorButton::changed, this, &AnnotationWidget::dataChanged ); connect( m_useColor, &QAbstractButton::toggled, this, &AnnotationWidget::dataChanged ); connect( m_useColor, &QCheckBox::toggled, m_innerColor, &KColorButton::setEnabled ); } } void LineAnnotationWidget::applyChanges() { AnnotationWidget::applyChanges(); if ( m_lineType == 0 ) { Q_ASSERT(m_spinLL && m_spinLLE && m_startStyleCombo && m_endStyleCombo); m_lineAnn->setLineLeadingForwardPoint( m_spinLL->value() ); m_lineAnn->setLineLeadingBackwardPoint( m_spinLLE->value() ); m_lineAnn->setLineStartStyle( (Okular::LineAnnotation::TermStyle)m_startStyleCombo->currentIndex() ); m_lineAnn->setLineEndStyle( (Okular::LineAnnotation::TermStyle)m_endStyleCombo->currentIndex() ); } else if ( m_lineType == 1 ) { Q_ASSERT( m_useColor && m_innerColor ); if ( !m_useColor->isChecked() ) { m_lineAnn->setLineInnerColor( QColor() ); } else { m_lineAnn->setLineInnerColor( m_innerColor->color() ); } } Q_ASSERT( m_spinSize ); m_lineAnn->style().setWidth( m_spinSize->value() ); } QIcon LineAnnotationWidget::endStyleIcon( Okular::LineAnnotation::TermStyle endStyle, const QColor &lineColor ) { const int iconSize { 48 }; QImage image { iconSize, iconSize, QImage::Format_ARGB32}; image.fill( qRgba(0, 0, 0, 0) ); Okular::LineAnnotation prototype; prototype.setLinePoints( { { 0, 0.5 }, { 0.65, 0.5 } } ); prototype.setLineStartStyle(Okular::LineAnnotation::TermStyle::None); prototype.setLineEndStyle( endStyle ); prototype.style().setWidth( 4 ); prototype.style().setColor( lineColor ); prototype.style().setLineStyle( Okular::Annotation::LineStyle::Solid ); prototype.setBoundingRectangle( { 0, 0, 1, 1 } ); LineAnnotPainter linepainter { &prototype, QSize { iconSize, iconSize }, 1, QTransform() }; linepainter.draw( image ); return QIcon( QPixmap::fromImage( image ) ); } InkAnnotationWidget::InkAnnotationWidget( Okular::Annotation * ann ) : AnnotationWidget( ann ) { m_inkAnn = static_cast< Okular::InkAnnotation * >( ann ); } void InkAnnotationWidget::createStyleWidget( QFormLayout * formlayout ) { QWidget * widget = qobject_cast( formlayout->parent() ); addColorButton( widget, formlayout ); addOpacitySpinBox( widget, formlayout ); addVerticalSpacer( formlayout ); m_spinSize = new QDoubleSpinBox( widget ); formlayout->addRow( i18n( "&Width:" ), m_spinSize ); m_spinSize->setRange( 1, 100 ); m_spinSize->setValue( m_inkAnn->style().width() ); - connect( m_spinSize, SIGNAL(valueChanged(double)), this, SIGNAL(dataChanged()) ); + connect( m_spinSize, QOverload::of(&QDoubleSpinBox::valueChanged), this, &AnnotationWidget::dataChanged ); } void InkAnnotationWidget::applyChanges() { AnnotationWidget::applyChanges(); m_inkAnn->style().setWidth( m_spinSize->value() ); } HighlightAnnotationWidget::HighlightAnnotationWidget( Okular::Annotation * ann ) : AnnotationWidget( ann ) { m_hlAnn = static_cast< Okular::HighlightAnnotation * >( ann ); } void HighlightAnnotationWidget::createStyleWidget( QFormLayout * formlayout ) { QWidget * widget = qobject_cast( formlayout->parent() ); m_typeCombo = new KComboBox( widget ); formlayout->addRow( i18n( "Type:" ), m_typeCombo ); m_typeCombo->addItem( i18n( "Highlight" ) ); m_typeCombo->addItem( i18n( "Squiggle" ) ); m_typeCombo->addItem( i18n( "Underline" ) ); m_typeCombo->addItem( i18n( "Strike out" ) ); m_typeCombo->setCurrentIndex( m_hlAnn->highlightType() ); addVerticalSpacer( formlayout ); addColorButton( widget, formlayout ); addOpacitySpinBox( widget, formlayout ); - connect( m_typeCombo, SIGNAL(currentIndexChanged(int)), this, SIGNAL(dataChanged()) ); + connect( m_typeCombo, QOverload::of(&KComboBox::currentIndexChanged), this, &AnnotationWidget::dataChanged ); } void HighlightAnnotationWidget::applyChanges() { AnnotationWidget::applyChanges(); m_hlAnn->setHighlightType( (Okular::HighlightAnnotation::HighlightType)m_typeCombo->currentIndex() ); } GeomAnnotationWidget::GeomAnnotationWidget( Okular::Annotation * ann ) : AnnotationWidget( ann ) { m_geomAnn = static_cast< Okular::GeomAnnotation * >( ann ); } void GeomAnnotationWidget::createStyleWidget( QFormLayout * formlayout ) { QWidget * widget = qobject_cast( formlayout->parent() ); m_typeCombo = new KComboBox( widget ); formlayout->addRow( i18n( "Type:" ), m_typeCombo ); addVerticalSpacer( formlayout ); addColorButton( widget, formlayout ); addOpacitySpinBox( widget, formlayout ); QHBoxLayout * colorlay = new QHBoxLayout(); m_useColor = new QCheckBox( i18n( "Enabled" ), widget ); colorlay->addWidget(m_useColor); m_innerColor = new KColorButton( widget ); colorlay->addWidget( m_innerColor); formlayout->addRow( i18n( "Shape fill:" ), colorlay ); addVerticalSpacer( formlayout ); m_spinSize = new QDoubleSpinBox( widget ); formlayout->addRow( i18n( "&Width:" ), m_spinSize ); m_typeCombo->addItem( i18n( "Rectangle" ) ); m_typeCombo->addItem( i18n( "Ellipse" ) ); m_typeCombo->setCurrentIndex( m_geomAnn->geometricalType() ); m_innerColor->setColor( m_geomAnn->geometricalInnerColor() ); if ( m_geomAnn->geometricalInnerColor().isValid() ) { m_useColor->setChecked( true ); } else { m_innerColor->setEnabled( false ); } m_spinSize->setRange( 0, 100 ); m_spinSize->setValue( m_geomAnn->style().width() ); - connect( m_typeCombo, SIGNAL(currentIndexChanged(int)), this, SIGNAL(dataChanged()) ); + connect( m_typeCombo, QOverload::of(&KComboBox::currentIndexChanged), this, &AnnotationWidget::dataChanged ); connect( m_innerColor, &KColorButton::changed, this, &AnnotationWidget::dataChanged ); connect( m_useColor, &QAbstractButton::toggled, this, &AnnotationWidget::dataChanged ); connect(m_useColor, &QCheckBox::toggled, m_innerColor, &KColorButton::setEnabled); - connect( m_spinSize, SIGNAL(valueChanged(double)), this, SIGNAL(dataChanged()) ); + connect( m_spinSize, QOverload::of(&QDoubleSpinBox::valueChanged), this, &AnnotationWidget::dataChanged ); } void GeomAnnotationWidget::applyChanges() { AnnotationWidget::applyChanges(); m_geomAnn->setGeometricalType( (Okular::GeomAnnotation::GeomType)m_typeCombo->currentIndex() ); if ( !m_useColor->isChecked() ) { m_geomAnn->setGeometricalInnerColor( QColor() ); } else { m_geomAnn->setGeometricalInnerColor( m_innerColor->color() ); } m_geomAnn->style().setWidth( m_spinSize->value() ); } FileAttachmentAnnotationWidget::FileAttachmentAnnotationWidget( Okular::Annotation * ann ) : AnnotationWidget( ann ), m_pixmapSelector( nullptr ) { m_attachAnn = static_cast< Okular::FileAttachmentAnnotation * >( ann ); } void FileAttachmentAnnotationWidget::createStyleWidget( QFormLayout * formlayout ) { QWidget * widget = qobject_cast( formlayout->parent() ); addOpacitySpinBox( widget, formlayout ); m_pixmapSelector = new PixmapPreviewSelector( widget ); formlayout->addRow( i18n( "File attachment symbol:" ), m_pixmapSelector ); m_pixmapSelector->setEditable( true ); m_pixmapSelector->addItem( i18nc( "Symbol for file attachment annotations", "Graph" ), QStringLiteral("graph") ); m_pixmapSelector->addItem( i18nc( "Symbol for file attachment annotations", "Push Pin" ), QStringLiteral("pushpin") ); m_pixmapSelector->addItem( i18nc( "Symbol for file attachment annotations", "Paperclip" ), QStringLiteral("paperclip") ); m_pixmapSelector->addItem( i18nc( "Symbol for file attachment annotations", "Tag" ), QStringLiteral("tag") ); m_pixmapSelector->setIcon( m_attachAnn->fileIconName() ); connect( m_pixmapSelector, &PixmapPreviewSelector::iconChanged, this, &AnnotationWidget::dataChanged ); } QWidget * FileAttachmentAnnotationWidget::createExtraWidget() { QWidget * widget = new QWidget(); widget->setWindowTitle( i18nc( "'File' as normal file, that can be opened, saved, etc..", "File" ) ); Okular::EmbeddedFile *ef = m_attachAnn->embeddedFile(); const int size = ef->size(); const QString sizeString = size <= 0 ? i18nc( "Not available size", "N/A" ) : KFormat().formatByteSize( size ); const QString descString = ef->description().isEmpty() ? i18n( "No description available." ) : ef->description(); QHBoxLayout * mainLay = new QHBoxLayout( widget ); QFormLayout * lay = new QFormLayout(); mainLay->addLayout( lay ); QLabel * tmplabel = new QLabel( ef->name() , widget ); tmplabel->setTextInteractionFlags( Qt::TextSelectableByMouse ); lay->addRow( i18n( "Name:" ), tmplabel ); tmplabel = new QLabel( sizeString, widget ); tmplabel->setTextInteractionFlags( Qt::TextSelectableByMouse ); lay->addRow( i18n( "&Width:" ), tmplabel ); tmplabel = new QLabel( widget ); tmplabel->setTextFormat( Qt::PlainText ); tmplabel->setWordWrap( true ); tmplabel->setText( descString ); tmplabel->setTextInteractionFlags( Qt::TextSelectableByMouse ); lay->addRow( i18n( "Description:" ), tmplabel ); QMimeDatabase db; QMimeType mime = db.mimeTypeForFile( ef->name(), QMimeDatabase::MatchExtension); if ( mime.isValid() ) { tmplabel = new QLabel( widget ); tmplabel->setPixmap( QIcon::fromTheme( mime.iconName() ).pixmap( FILEATTACH_ICONSIZE, FILEATTACH_ICONSIZE ) ); tmplabel->setFixedSize( FILEATTACH_ICONSIZE, FILEATTACH_ICONSIZE ); QVBoxLayout * tmpLayout = new QVBoxLayout( widget ); tmpLayout->setAlignment( Qt::AlignTop ); mainLay->addLayout( tmpLayout ); tmpLayout->addWidget( tmplabel ); } return widget; } void FileAttachmentAnnotationWidget::applyChanges() { AnnotationWidget::applyChanges(); m_attachAnn->setFileIconName( m_pixmapSelector->icon() ); } static QString caretSymbolToIcon( Okular::CaretAnnotation::CaretSymbol symbol ) { switch ( symbol ) { case Okular::CaretAnnotation::None: return QStringLiteral( "caret-none" ); case Okular::CaretAnnotation::P: return QStringLiteral( "caret-p" ); } return QString(); } static Okular::CaretAnnotation::CaretSymbol caretSymbolFromIcon( const QString &icon ) { if ( icon == QLatin1String( "caret-none" ) ) return Okular::CaretAnnotation::None; else if ( icon == QLatin1String( "caret-p" ) ) return Okular::CaretAnnotation::P; return Okular::CaretAnnotation::None; } CaretAnnotationWidget::CaretAnnotationWidget( Okular::Annotation * ann ) : AnnotationWidget( ann ), m_pixmapSelector( nullptr ) { m_caretAnn = static_cast< Okular::CaretAnnotation * >( ann ); } void CaretAnnotationWidget::createStyleWidget( QFormLayout * formlayout ) { QWidget * widget = qobject_cast( formlayout->parent() ); addColorButton( widget, formlayout ); addOpacitySpinBox( widget, formlayout ); m_pixmapSelector = new PixmapPreviewSelector( widget ); formlayout->addRow( i18n( "Caret symbol:" ), m_pixmapSelector ); m_pixmapSelector->addItem( i18nc( "Symbol for caret annotations", "None" ), QStringLiteral("caret-none") ); m_pixmapSelector->addItem( i18nc( "Symbol for caret annotations", "P" ), QStringLiteral("caret-p") ); m_pixmapSelector->setIcon( caretSymbolToIcon( m_caretAnn->caretSymbol() ) ); connect( m_pixmapSelector, &PixmapPreviewSelector::iconChanged, this, &AnnotationWidget::dataChanged ); } void CaretAnnotationWidget::applyChanges() { AnnotationWidget::applyChanges(); m_caretAnn->setCaretSymbol( caretSymbolFromIcon( m_pixmapSelector->icon() ) ); } #include "moc_annotationwidgets.cpp" diff --git a/ui/annotwindow.cpp b/ui/annotwindow.cpp index 3eacde023..ea5591b2c 100644 --- a/ui/annotwindow.cpp +++ b/ui/annotwindow.cpp @@ -1,444 +1,444 @@ /*************************************************************************** * 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 ) + MovableTitle( AnnotWindow * parent ) : QWidget( parent ) { QVBoxLayout * mainlay = new QVBoxLayout( this ); mainlay->setContentsMargins( 0, 0, 0, 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))); + connect(latexButton, &QToolButton::clicked, parent, &AnnotWindow::renderLatex); + connect(parent, &AnnotWindow::containsLatex, latexButton, &QWidget::setVisible); 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.toTimeSpec(Qt::LocalTime), 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(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->setContentsMargins( 2, 2, 2, 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(); // The emit below is not wrong even if emitting signals from the constructor it's usually wrong // in this case the signal it's connected to inside MovableTitle constructor a few lines above emit containsLatex( GuiUtils::LatexRenderer::mightContainLatex( m_annot->contents() ) ); // clazy:exclude=incorrect-emit 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(0xfd, 0xfd, 0x96); } 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, 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 4e7a83a73..07f33949f 100644 --- a/ui/annotwindow.h +++ b/ui/annotwindow.h @@ -1,74 +1,76 @@ /*************************************************************************** * 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() override; 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; + public Q_SLOTS: + void renderLatex( bool render ); + 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, const QString &contents, int cursorPos, int anchorPos); Q_SIGNALS: void containsLatex( bool ); }; #endif diff --git a/ui/embeddedfilesdialog.cpp b/ui/embeddedfilesdialog.cpp index c926bc9fc..2f071d08c 100644 --- a/ui/embeddedfilesdialog.cpp +++ b/ui/embeddedfilesdialog.cpp @@ -1,206 +1,206 @@ /*************************************************************************** * 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"), 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); // embeddedFiles() returns a const QList for (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, QVariant::fromValue( 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(mUser1Button, &QPushButton::clicked, this, &EmbeddedFilesDialog::saveFileFromButton); + connect(mUser2Button, &QPushButton::clicked, this, &EmbeddedFilesDialog::viewFileFromButton); 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() +void EmbeddedFilesDialog::saveFileFromButton() { const QList selected = m_tw->selectedItems(); for (const QTreeWidgetItem *twi : selected) { Okular::EmbeddedFile* ef = qvariant_cast< Okular::EmbeddedFile* >( twi->data( 0, EmbeddedFileRole ) ); saveFile(ef); } } -void EmbeddedFilesDialog::viewFile() +void EmbeddedFilesDialog::viewFileFromButton() { const QList selected = m_tw->selectedItems(); for (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() ? QLatin1String("") : QString("." + fileInfo.completeSuffix())) ); 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/embeddedfilesdialog.h b/ui/embeddedfilesdialog.h index 89d9430b0..ee059f34e 100644 --- a/ui/embeddedfilesdialog.h +++ b/ui/embeddedfilesdialog.h @@ -1,50 +1,50 @@ /*************************************************************************** * 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. * ***************************************************************************/ #ifndef _EMBEDDEDFILESDIALOG_H_ #define _EMBEDDEDFILESDIALOG_H_ #include class QTreeWidget; class QPushButton; class QTemporaryFile; class QTreeWidgetItem; namespace Okular { class Document; class EmbeddedFile; } class EmbeddedFilesDialog : public QDialog { Q_OBJECT public: EmbeddedFilesDialog(QWidget *parent, const Okular::Document *document); private Q_SLOTS: - void saveFile(); + void saveFileFromButton(); void attachViewContextMenu( const QPoint& pos ); void updateSaveButton(); - void viewFile(); + void viewFileFromButton(); void viewFileItem( QTreeWidgetItem* item, int column ); private: void saveFile( Okular::EmbeddedFile* ); void viewFile( Okular::EmbeddedFile* ); QTreeWidget *m_tw; QPushButton *mUser1Button; QPushButton *mUser2Button; QList< QSharedPointer > m_openedFiles; }; #endif diff --git a/ui/formwidgets.cpp b/ui/formwidgets.cpp index eddc4f67f..46fd4b6dd 100644 --- a/ui/formwidgets.cpp +++ b/ui/formwidgets.cpp @@ -1,1379 +1,1379 @@ /*************************************************************************** * Copyright (C) 2007 by Pino Toscano * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * Copyright (C) 2018 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 "formwidgets.h" #include "pageviewutils.h" #include "revisionviewer.h" #include "signaturepropertiesdialog.h" #include #include #include #include #include #include #include #include #include #include // local includes #include "core/action.h" #include "core/form.h" #include "core/document.h" #include "debug_ui.h" FormWidgetsController::FormWidgetsController( Okular::Document *doc ) : QObject( doc ), m_doc( doc ) { // emit changed signal when a form has changed connect( this, &FormWidgetsController::formTextChangedByUndoRedo, this, &FormWidgetsController::changed ); connect( this, &FormWidgetsController::formListChangedByUndoRedo, this, &FormWidgetsController::changed ); connect( this, &FormWidgetsController::formComboChangedByUndoRedo, this, &FormWidgetsController::changed ); // connect form modification signals to and from document connect( this, &FormWidgetsController::formTextChangedByWidget, doc, &Okular::Document::editFormText ); connect( doc, &Okular::Document::formTextChangedByUndoRedo, this, &FormWidgetsController::formTextChangedByUndoRedo ); connect( this, &FormWidgetsController::formListChangedByWidget, doc, &Okular::Document::editFormList ); connect( doc, &Okular::Document::formListChangedByUndoRedo, this, &FormWidgetsController::formListChangedByUndoRedo ); connect( this, &FormWidgetsController::formComboChangedByWidget, doc, &Okular::Document::editFormCombo ); connect( doc, &Okular::Document::formComboChangedByUndoRedo, this, &FormWidgetsController::formComboChangedByUndoRedo ); connect( this, &FormWidgetsController::formButtonsChangedByWidget, doc, &Okular::Document::editFormButtons ); connect( doc, &Okular::Document::formButtonsChangedByUndoRedo, this, &FormWidgetsController::slotFormButtonsChangedByUndoRedo ); // Connect undo/redo signals connect( this, &FormWidgetsController::requestUndo, doc, &Okular::Document::undo ); connect( this, &FormWidgetsController::requestRedo, doc, &Okular::Document::redo ); connect( doc, &Okular::Document::canUndoChanged, this, &FormWidgetsController::canUndoChanged ); connect( doc, &Okular::Document::canRedoChanged, this, &FormWidgetsController::canRedoChanged ); // Connect the generic formWidget refresh signal connect( doc, &Okular::Document::refreshFormWidget, this, &FormWidgetsController::refreshFormWidget ); } FormWidgetsController::~FormWidgetsController() { } void FormWidgetsController::signalAction( Okular::Action *a ) { emit action( a ); } void FormWidgetsController::processScriptAction( Okular::Action *a, Okular::FormField * field, Okular::Annotation::AdditionalActionType type ) { // If it's not a Action Script or if the field is not a FormText, handle it normally if( a->actionType() != Okular::Action::Script || field->type() != Okular::FormField::FormText ) { emit action( a ); return; } switch( type ) { // These cases are to be handled by the FormField text, so we let it happen. case Okular::Annotation::FocusIn: case Okular::Annotation::FocusOut: return; case Okular::Annotation::PageOpening: case Okular::Annotation::PageClosing: case Okular::Annotation::CursorEntering: case Okular::Annotation::CursorLeaving: case Okular::Annotation::MousePressed: case Okular::Annotation::MouseReleased: emit action( a ); } } void FormWidgetsController::registerRadioButton( FormWidgetIface *fwButton, Okular::FormFieldButton *formButton ) { if ( !fwButton ) return; QAbstractButton *button = dynamic_cast(fwButton); if ( !button ) { qWarning() << "fwButton is not a QAbstractButton" << fwButton; return; } QList< RadioData >::iterator it = m_radios.begin(), itEnd = m_radios.end(); const int id = formButton->id(); m_buttons.insert( id, button ); for ( ; it != itEnd; ++it ) { const RadioData &rd = *it; const QList< int >::const_iterator idsIt = std::find( rd.ids.begin(), rd.ids.end(), id ); if ( idsIt != rd.ids.constEnd() ) { qCDebug(OkularUiDebug) << "Adding id" << id << "To group including" << rd.ids; rd.group->addButton( button ); rd.group->setId( button, id ); return; } } const QList< int > siblings = formButton->siblings(); RadioData newdata; newdata.ids = siblings; newdata.ids.append( id ); newdata.group = new QButtonGroup(); newdata.group->addButton( button ); newdata.group->setId( button, id ); // Groups of 1 (like checkboxes) can't be exclusive if (siblings.isEmpty()) newdata.group->setExclusive( false ); connect( newdata.group, QOverload::of(&QButtonGroup::buttonClicked), this, &FormWidgetsController::slotButtonClicked ); m_radios.append( newdata ); } void FormWidgetsController::dropRadioButtons() { QList< RadioData >::iterator it = m_radios.begin(), itEnd = m_radios.end(); for ( ; it != itEnd; ++it ) { delete (*it).group; } m_radios.clear(); m_buttons.clear(); } bool FormWidgetsController::canUndo() { return m_doc->canUndo(); } bool FormWidgetsController::canRedo() { return m_doc->canRedo(); } bool FormWidgetsController::shouldFormWidgetBeShown( Okular::FormField *form ) { return !form->isReadOnly() || form->type() == Okular::FormField::FormSignature; } void FormWidgetsController::slotButtonClicked( QAbstractButton *button ) { int pageNumber = -1; CheckBoxEdit *check = qobject_cast< CheckBoxEdit * >( button ); if ( check ) { // Checkboxes need to be uncheckable so if clicking a checked one // disable the exclusive status temporarily and uncheck it Okular::FormFieldButton *formButton = static_cast( check->formField() ); if ( formButton->state() ) { const bool wasExclusive = button->group()->exclusive(); button->group()->setExclusive(false); check->setChecked(false); button->group()->setExclusive(wasExclusive); } pageNumber = check->pageItem()->pageNumber(); } else if ( RadioButtonEdit *radio = qobject_cast< RadioButtonEdit * >( button ) ) { pageNumber = radio->pageItem()->pageNumber(); } const QList< QAbstractButton* > buttons = button->group()->buttons(); QList< bool > checked; QList< bool > prevChecked; QList< Okular::FormFieldButton*> formButtons; for ( QAbstractButton* button : buttons ) { checked.append( button->isChecked() ); Okular::FormFieldButton *formButton = static_cast( dynamic_cast(button)->formField() ); formButtons.append( formButton ); prevChecked.append( formButton->state() ); } if (checked != prevChecked) emit formButtonsChangedByWidget( pageNumber, formButtons, checked ); if ( check ) { // The formButtonsChangedByWidget signal changes the value of the underlying // Okular::FormField of the checkbox. We need to execute the activation // action after this. check->doActivateAction(); } } void FormWidgetsController::slotFormButtonsChangedByUndoRedo( int pageNumber, const QList< Okular::FormFieldButton* > & formButtons) { for ( const Okular::FormFieldButton* formButton : formButtons ) { int id = formButton->id(); QAbstractButton* button = m_buttons[id]; CheckBoxEdit *check = qobject_cast< CheckBoxEdit * >( button ); if ( check ) { emit refreshFormWidget( check->formField() ); } // temporarily disable exclusiveness of the button group // since it breaks doing/redoing steps into which all the checkboxes // are unchecked const bool wasExclusive = button->group()->exclusive(); button->group()->setExclusive(false); bool checked = formButton->state(); button->setChecked( checked ); button->group()->setExclusive(wasExclusive); button->setFocus(); } emit changed( pageNumber ); } FormWidgetIface * FormWidgetFactory::createWidget( Okular::FormField * ff, QWidget * parent ) { FormWidgetIface * widget = nullptr; switch ( ff->type() ) { case Okular::FormField::FormButton: { Okular::FormFieldButton * ffb = static_cast< Okular::FormFieldButton * >( ff ); switch ( ffb->buttonType() ) { case Okular::FormFieldButton::Push: widget = new PushButtonEdit( ffb, parent ); break; case Okular::FormFieldButton::CheckBox: widget = new CheckBoxEdit( ffb, parent ); break; case Okular::FormFieldButton::Radio: widget = new RadioButtonEdit( ffb, parent ); break; default: ; } break; } case Okular::FormField::FormText: { Okular::FormFieldText * fft = static_cast< Okular::FormFieldText * >( ff ); switch ( fft->textType() ) { case Okular::FormFieldText::Multiline: widget = new TextAreaEdit( fft, parent ); break; case Okular::FormFieldText::Normal: widget = new FormLineEdit( fft, parent ); break; case Okular::FormFieldText::FileSelect: widget = new FileEdit( fft, parent ); break; } break; } case Okular::FormField::FormChoice: { Okular::FormFieldChoice * ffc = static_cast< Okular::FormFieldChoice * >( ff ); switch ( ffc->choiceType() ) { case Okular::FormFieldChoice::ListBox: widget = new ListEdit( ffc, parent ); break; case Okular::FormFieldChoice::ComboBox: widget = new ComboEdit( ffc, parent ); break; } break; } case Okular::FormField::FormSignature: { Okular::FormFieldSignature * ffs = static_cast< Okular::FormFieldSignature * >( ff ); if ( ffs->isVisible() && ffs->signatureType() != Okular::FormFieldSignature::UnknownType ) widget = new SignatureEdit( ffs, parent ); break; } default: ; } if ( !FormWidgetsController::shouldFormWidgetBeShown( ff ) ) widget->setVisibility( false ); return widget; } FormWidgetIface::FormWidgetIface( QWidget * w, Okular::FormField * ff ) : m_controller( nullptr ), m_ff( ff ), m_widget( w ), m_pageItem( nullptr ) { } FormWidgetIface::~FormWidgetIface() { } Okular::NormalizedRect FormWidgetIface::rect() const { return m_ff->rect(); } void FormWidgetIface::setWidthHeight( int w, int h ) { m_widget->resize( w, h ); } void FormWidgetIface::moveTo( int x, int y ) { m_widget->move( x, y ); } bool FormWidgetIface::setVisibility( bool visible ) { bool hadfocus = m_widget->hasFocus(); if ( hadfocus ) m_widget->clearFocus(); m_widget->setVisible( visible ); return hadfocus; } void FormWidgetIface::setCanBeFilled( bool fill ) { m_widget->setEnabled( fill ); } void FormWidgetIface::setPageItem( PageViewItem *pageItem ) { m_pageItem = pageItem; } void FormWidgetIface::setFormField( Okular::FormField *field ) { m_ff = field; } Okular::FormField* FormWidgetIface::formField() const { return m_ff; } PageViewItem* FormWidgetIface::pageItem() const { return m_pageItem; } void FormWidgetIface::setFormWidgetsController( FormWidgetsController *controller ) { m_controller = controller; QObject *obj = dynamic_cast< QObject * > ( this ); QObject::connect( m_controller, &FormWidgetsController::refreshFormWidget, obj, [this] ( Okular::FormField *form ) { slotRefresh ( form ); }); } void FormWidgetIface::slotRefresh( Okular::FormField * form ) { if ( m_ff != form ) { return; } setVisibility( form->isVisible() && m_controller->shouldFormWidgetBeShown( form ) ); m_widget->setEnabled( !form->isReadOnly() ); } PushButtonEdit::PushButtonEdit( Okular::FormFieldButton * button, QWidget * parent ) : QPushButton( parent ), FormWidgetIface( this, button ) { setText( button->caption() ); if( button->caption().isEmpty() ) { setFlat( true ); } setVisible( button->isVisible() ); setCursor( Qt::ArrowCursor ); } CheckBoxEdit::CheckBoxEdit( Okular::FormFieldButton * button, QWidget * parent ) : QCheckBox( parent ), FormWidgetIface( this, button ) { setText( button->caption() ); setVisible( button->isVisible() ); setCursor( Qt::ArrowCursor ); } void CheckBoxEdit::setFormWidgetsController( FormWidgetsController *controller ) { Okular::FormFieldButton *form = static_cast(m_ff); FormWidgetIface::setFormWidgetsController( controller ); m_controller->registerRadioButton( this, form ); setChecked( form->state() ); } void CheckBoxEdit::doActivateAction() { Okular::FormFieldButton *form = static_cast(m_ff); if ( form->activationAction() ) m_controller->signalAction( form->activationAction() ); } void CheckBoxEdit::slotRefresh( Okular::FormField * form ) { if ( form != m_ff ) { return; } FormWidgetIface::slotRefresh( form ); Okular::FormFieldButton *button = static_cast(m_ff); bool oldState = isChecked(); bool newState = button->state(); if ( oldState != newState ) { setChecked( button->state() ); doActivateAction(); } } RadioButtonEdit::RadioButtonEdit( Okular::FormFieldButton * button, QWidget * parent ) : QRadioButton( parent ), FormWidgetIface( this, button ) { setText( button->caption() ); setVisible( button->isVisible() ); setCursor( Qt::ArrowCursor ); } void RadioButtonEdit::setFormWidgetsController( FormWidgetsController *controller ) { Okular::FormFieldButton *form = static_cast(m_ff); FormWidgetIface::setFormWidgetsController( controller ); m_controller->registerRadioButton( this, form ); setChecked( form->state() ); } FormLineEdit::FormLineEdit( Okular::FormFieldText * text, QWidget * parent ) : QLineEdit( parent ), FormWidgetIface( this, text ) { int maxlen = text->maximumLength(); if ( maxlen >= 0 ) setMaxLength( maxlen ); setAlignment( text->textAlignment() ); setText( text->text() ); if ( text->isPassword() ) setEchoMode( QLineEdit::Password ); m_prevCursorPos = cursorPosition(); m_prevAnchorPos = cursorPosition(); m_editing = false; connect( this, &QLineEdit::textEdited, this, &FormLineEdit::slotChanged ); connect( this, &QLineEdit::cursorPositionChanged, this, &FormLineEdit::slotChanged ); setVisible( text->isVisible() ); } void FormLineEdit::setFormWidgetsController(FormWidgetsController* controller) { FormWidgetIface::setFormWidgetsController(controller); connect( m_controller, &FormWidgetsController::formTextChangedByUndoRedo, this, &FormLineEdit::slotHandleTextChangedByUndoRedo ); } bool FormLineEdit::event( QEvent* e ) { if ( e->type() == QEvent::KeyPress ) { QKeyEvent *keyEvent = static_cast< QKeyEvent* >( e ); if ( keyEvent == QKeySequence::Undo ) { emit m_controller->requestUndo(); return true; } else if ( keyEvent == QKeySequence::Redo ) { emit m_controller->requestRedo(); return true; } } else if ( e->type() == QEvent::FocusIn ) { const auto fft = static_cast< Okular::FormFieldText * > ( m_ff ); setText( fft->text() ); m_editing = true; if( const Okular::Action *action = m_ff->additionalAction( Okular::Annotation::FocusIn ) ) emit m_controller->focusAction( action, fft ); setFocus(); } else if ( e->type() == QEvent::FocusOut ) { // Don't worry about focus events from other sources than the user FocusEvent to edit the field QFocusEvent *focusEvent = static_cast< QFocusEvent* >( e ); if( focusEvent->reason() == Qt::OtherFocusReason ) return true; m_editing = false; if( const Okular::Action *action = m_ff->additionalAction( Okular::Annotation::FocusOut ) ) { bool ok = false; emit m_controller->validateAction( action, static_cast< Okular::FormFieldText * > ( m_ff ), ok ); } if ( const Okular::Action *action = m_ff->additionalAction( Okular::FormField::FormatField ) ) { emit m_controller->formatAction( action, static_cast< Okular::FormFieldText * > ( m_ff ) ); } } return QLineEdit::event( e ); } void FormLineEdit::contextMenuEvent( QContextMenuEvent* event ) { QMenu *menu = createStandardContextMenu(); QList actionList = menu->actions(); enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, DeleteAct, SelectAllAct }; QAction *kundo = KStandardAction::create( KStandardAction::Undo, m_controller, SIGNAL( requestUndo() ), menu ); QAction *kredo = KStandardAction::create( KStandardAction::Redo, m_controller, SIGNAL( requestRedo() ), menu ); connect( m_controller, &FormWidgetsController::canUndoChanged, kundo, &QAction::setEnabled ); connect( m_controller, &FormWidgetsController::canRedoChanged, kredo, &QAction::setEnabled ); kundo->setEnabled( m_controller->canUndo() ); kredo->setEnabled( m_controller->canRedo() ); QAction *oldUndo, *oldRedo; oldUndo = actionList[UndoAct]; oldRedo = actionList[RedoAct]; menu->insertAction( oldUndo, kundo ); menu->insertAction( oldRedo, kredo ); menu->removeAction( oldUndo ); menu->removeAction( oldRedo ); menu->exec( event->globalPos() ); delete menu; } void FormLineEdit::slotChanged() { Okular::FormFieldText *form = static_cast(m_ff); QString contents = text(); int cursorPos = cursorPosition(); if( form->additionalAction( Okular::FormField::FieldModified ) && m_editing && !form->isReadOnly() ) { bool ok = false; QString oldInputText = form->text(); form->setText( text() ); emit m_controller->keystrokeAction( form->additionalAction( Okular::FormField::FieldModified ), form, ok ); form->setText( oldInputText ); if(!ok) { setText( oldInputText ); return; } } if ( contents != form->text() ) { emit m_controller->formTextChangedByWidget( pageItem()->pageNumber(), form, contents, cursorPos, m_prevCursorPos, m_prevAnchorPos ); } m_prevCursorPos = cursorPos; m_prevAnchorPos = cursorPos; if ( hasSelectedText() ) { if ( cursorPos == selectionStart() ) { m_prevAnchorPos = selectionStart() + selectedText().size(); } else { m_prevAnchorPos = selectionStart(); } } } void FormLineEdit::slotHandleTextChangedByUndoRedo( int pageNumber, Okular::FormFieldText* textForm, const QString & contents, int cursorPos, int anchorPos ) { Q_UNUSED(pageNumber); if ( textForm != m_ff || contents == text() ) { return; } disconnect( this, &QLineEdit::cursorPositionChanged, this, &FormLineEdit::slotChanged ); setText(contents); setCursorPosition(anchorPos); cursorForward( true, cursorPos - anchorPos ); connect( this, &QLineEdit::cursorPositionChanged, this, &FormLineEdit::slotChanged ); m_prevCursorPos = cursorPos; m_prevAnchorPos = anchorPos; setFocus(); } void FormLineEdit::slotRefresh( Okular::FormField *form ) { if (form != m_ff) { return; } FormWidgetIface::slotRefresh( form ); Okular::FormFieldText *text = static_cast ( form ); setText( text->text() ); } TextAreaEdit::TextAreaEdit( Okular::FormFieldText * text, QWidget * parent ) : KTextEdit( parent ), FormWidgetIface( this, text ) { setAcceptRichText( text->isRichText() ); setCheckSpellingEnabled( text->canBeSpellChecked() ); setAlignment( text->textAlignment() ); setPlainText( text->text() ); setUndoRedoEnabled( false ); connect( this, &QTextEdit::textChanged, this, &TextAreaEdit::slotChanged ); connect( this, &QTextEdit::cursorPositionChanged, this, &TextAreaEdit::slotChanged ); connect( this, &KTextEdit::aboutToShowContextMenu, this, &TextAreaEdit::slotUpdateUndoAndRedoInContextMenu ); m_prevCursorPos = textCursor().position(); m_prevAnchorPos = textCursor().anchor(); m_editing = false; setVisible( text->isVisible() ); } TextAreaEdit::~TextAreaEdit() { // We need this because otherwise on destruction we destruct the syntax highlighter // that ends up calling text changed but then we go to slotChanged and we're already // half destructed and all is bad disconnect( this, &QTextEdit::textChanged, this, &TextAreaEdit::slotChanged ); } bool TextAreaEdit::event( QEvent* e ) { if ( e->type() == QEvent::KeyPress ) { QKeyEvent *keyEvent = static_cast< QKeyEvent* >(e); if ( keyEvent == QKeySequence::Undo ) { emit m_controller->requestUndo(); return true; } else if ( keyEvent == QKeySequence::Redo ) { emit m_controller->requestRedo(); return true; } } else if ( e->type() == QEvent::FocusIn ) { const auto fft = static_cast< Okular::FormFieldText * > ( m_ff ); setText( fft->text() ); m_editing = true; } else if ( e->type() == QEvent::FocusOut ) { m_editing = false; if ( const Okular::Action *action = m_ff->additionalAction( Okular::FormField::FormatField ) ) { emit m_controller->formatAction( action, static_cast< Okular::FormFieldText * > ( m_ff ) ); } } return KTextEdit::event( e ); } void TextAreaEdit::slotUpdateUndoAndRedoInContextMenu( QMenu* menu ) { if ( !menu ) return; QList actionList = menu->actions(); enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, ClearAct, SelectAllAct, NCountActs }; QAction *kundo = KStandardAction::create( KStandardAction::Undo, m_controller, SIGNAL( requestUndo() ), menu ); QAction *kredo = KStandardAction::create( KStandardAction::Redo, m_controller, SIGNAL( requestRedo() ), menu ); connect(m_controller, &FormWidgetsController::canUndoChanged, kundo, &QAction::setEnabled ); connect(m_controller, &FormWidgetsController::canRedoChanged, kredo, &QAction::setEnabled ); kundo->setEnabled( m_controller->canUndo() ); kredo->setEnabled( m_controller->canRedo() ); QAction *oldUndo, *oldRedo; oldUndo = actionList[UndoAct]; oldRedo = actionList[RedoAct]; menu->insertAction( oldUndo, kundo ); menu->insertAction( oldRedo, kredo ); menu->removeAction( oldUndo ); menu->removeAction( oldRedo ); } void TextAreaEdit::setFormWidgetsController( FormWidgetsController* controller ) { FormWidgetIface::setFormWidgetsController( controller ); connect( m_controller, &FormWidgetsController::formTextChangedByUndoRedo, this, &TextAreaEdit::slotHandleTextChangedByUndoRedo ); } void TextAreaEdit::slotHandleTextChangedByUndoRedo( int pageNumber, Okular::FormFieldText* textForm, const QString & contents, int cursorPos, int anchorPos ) { Q_UNUSED(pageNumber); if ( textForm != m_ff ) { return; } setPlainText( contents ); QTextCursor c = textCursor(); c.setPosition( anchorPos ); c.setPosition( cursorPos,QTextCursor::KeepAnchor ); m_prevCursorPos = cursorPos; m_prevAnchorPos = anchorPos; setTextCursor( c ); setFocus(); } void TextAreaEdit::slotChanged() { Okular::FormFieldText *form = static_cast(m_ff); QString contents = toPlainText(); int cursorPos = textCursor().position(); if( form->additionalAction( Okular::FormField::FieldModified ) && m_editing && !form->isReadOnly() ) { bool ok = false; QString oldInputText = form->text(); form->setText( toPlainText() ); emit m_controller->keystrokeAction( form->additionalAction( Okular::FormField::FieldModified ), form, ok ); form->setText( oldInputText ); if(!ok) { setText( oldInputText ); return; } } if (contents != form->text()) { emit m_controller->formTextChangedByWidget( pageItem()->pageNumber(), form, contents, cursorPos, m_prevCursorPos, m_prevAnchorPos ); } m_prevCursorPos = cursorPos; m_prevAnchorPos = textCursor().anchor(); } void TextAreaEdit::slotRefresh( Okular::FormField *form ) { if (form != m_ff) { return; } FormWidgetIface::slotRefresh( form ); Okular::FormFieldText *text = static_cast ( form ); setPlainText( text->text() ); } FileEdit::FileEdit( Okular::FormFieldText * text, QWidget * parent ) : KUrlRequester( parent ), FormWidgetIface( this, text ) { setMode( KFile::File | KFile::ExistingOnly | KFile::LocalOnly ); setFilter( i18n( "*|All Files" ) ); setUrl( QUrl::fromUserInput( text->text() ) ); lineEdit()->setAlignment( text->textAlignment() ); m_prevCursorPos = lineEdit()->cursorPosition(); m_prevAnchorPos = lineEdit()->cursorPosition(); connect( this, &KUrlRequester::textChanged, this, &FileEdit::slotChanged ); connect( lineEdit(), &QLineEdit::cursorPositionChanged, this, &FileEdit::slotChanged ); setVisible( text->isVisible() ); } void FileEdit::setFormWidgetsController( FormWidgetsController* controller ) { FormWidgetIface::setFormWidgetsController( controller ); connect( m_controller, &FormWidgetsController::formTextChangedByUndoRedo, this, &FileEdit::slotHandleFileChangedByUndoRedo ); } bool FileEdit::eventFilter( QObject* obj, QEvent* event ) { if ( obj == lineEdit() ) { if ( event->type() == QEvent::KeyPress ) { QKeyEvent *keyEvent = static_cast< QKeyEvent* >( event ); if ( keyEvent == QKeySequence::Undo ) { emit m_controller->requestUndo(); return true; } else if ( keyEvent == QKeySequence::Redo ) { emit m_controller->requestRedo(); return true; } } else if( event->type() == QEvent::ContextMenu ) { QContextMenuEvent *contextMenuEvent = static_cast< QContextMenuEvent* >( event ); QMenu *menu = ( (QLineEdit*) lineEdit() )->createStandardContextMenu(); QList< QAction* > actionList = menu->actions(); enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, DeleteAct, SelectAllAct }; QAction *kundo = KStandardAction::create( KStandardAction::Undo, m_controller, SIGNAL( requestUndo() ), menu ); QAction *kredo = KStandardAction::create( KStandardAction::Redo, m_controller, SIGNAL( requestRedo() ), menu ); connect(m_controller, &FormWidgetsController::canUndoChanged, kundo, &QAction::setEnabled ); connect(m_controller, &FormWidgetsController::canRedoChanged, kredo, &QAction::setEnabled ); kundo->setEnabled( m_controller->canUndo() ); kredo->setEnabled( m_controller->canRedo() ); QAction *oldUndo, *oldRedo; oldUndo = actionList[UndoAct]; oldRedo = actionList[RedoAct]; menu->insertAction( oldUndo, kundo ); menu->insertAction( oldRedo, kredo ); menu->removeAction( oldUndo ); menu->removeAction( oldRedo ); menu->exec( contextMenuEvent->globalPos() ); delete menu; return true; } } return KUrlRequester::eventFilter( obj, event ); } void FileEdit::slotChanged() { // Make sure line edit's text matches url expansion if ( text() != url().toLocalFile() ) this->setText( url().toLocalFile() ); Okular::FormFieldText *form = static_cast(m_ff); QString contents = text(); int cursorPos = lineEdit()->cursorPosition(); if (contents != form->text()) { emit m_controller->formTextChangedByWidget( pageItem()->pageNumber(), form, contents, cursorPos, m_prevCursorPos, m_prevAnchorPos ); } m_prevCursorPos = cursorPos; m_prevAnchorPos = cursorPos; if ( lineEdit()->hasSelectedText() ) { if ( cursorPos == lineEdit()->selectionStart() ) { m_prevAnchorPos = lineEdit()->selectionStart() + lineEdit()->selectedText().size(); } else { m_prevAnchorPos = lineEdit()->selectionStart(); } } } void FileEdit::slotHandleFileChangedByUndoRedo( int pageNumber, Okular::FormFieldText* form, const QString & contents, int cursorPos, int anchorPos ) { Q_UNUSED(pageNumber); if ( form != m_ff || contents == text() ) { return; } - disconnect( this, SIGNAL( cursorPositionChanged( int, int ) ), this, SLOT( slotChanged() ) ); + disconnect( lineEdit(), &QLineEdit::cursorPositionChanged, this, &FileEdit::slotChanged ); setText( contents ); lineEdit()->setCursorPosition( anchorPos ); lineEdit()->cursorForward( true, cursorPos - anchorPos ); - connect( this, SIGNAL(cursorPositionChanged(int,int)), this, SLOT( slotChanged() ) ); + connect( lineEdit(), &QLineEdit::cursorPositionChanged, this, &FileEdit::slotChanged ); m_prevCursorPos = cursorPos; m_prevAnchorPos = anchorPos; setFocus(); } ListEdit::ListEdit( Okular::FormFieldChoice * choice, QWidget * parent ) : QListWidget( parent ), FormWidgetIface( this, choice ) { addItems( choice->choices() ); setSelectionMode( choice->multiSelect() ? QAbstractItemView::ExtendedSelection : QAbstractItemView::SingleSelection ); setVerticalScrollMode( QAbstractItemView::ScrollPerPixel ); const QList< int > selectedItems = choice->currentChoices(); if ( choice->multiSelect() ) { for ( const int index : selectedItems ) if ( index >= 0 && index < count() ) item( index )->setSelected( true ); } else { if ( selectedItems.count() == 1 && selectedItems.at(0) >= 0 && selectedItems.at(0) < count() ) { setCurrentRow( selectedItems.at(0) ); scrollToItem( item( selectedItems.at(0) ) ); } } connect( this, &QListWidget::itemSelectionChanged, this, &ListEdit::slotSelectionChanged ); setVisible( choice->isVisible() ); setCursor( Qt::ArrowCursor ); } void ListEdit::setFormWidgetsController( FormWidgetsController* controller ) { FormWidgetIface::setFormWidgetsController( controller ); connect( m_controller, &FormWidgetsController::formListChangedByUndoRedo, this, &ListEdit::slotHandleFormListChangedByUndoRedo ); } void ListEdit::slotSelectionChanged() { const QList< QListWidgetItem * > selection = selectedItems(); QList< int > rows; for ( const QListWidgetItem * item : selection ) { rows.append( row( item ) ); } Okular::FormFieldChoice *form = static_cast(m_ff); if ( rows != form->currentChoices() ) { emit m_controller->formListChangedByWidget( pageItem()->pageNumber(), form, rows ); } } void ListEdit::slotHandleFormListChangedByUndoRedo( int pageNumber, Okular::FormFieldChoice* listForm, const QList< int > & choices ) { Q_UNUSED(pageNumber); if ( m_ff != listForm ) { return; } disconnect( this, &QListWidget::itemSelectionChanged, this, &ListEdit::slotSelectionChanged ); for(int i=0; i < count(); i++) { item( i )->setSelected( choices.contains(i) ); } connect( this, &QListWidget::itemSelectionChanged, this, &ListEdit::slotSelectionChanged ); setFocus(); } ComboEdit::ComboEdit( Okular::FormFieldChoice * choice, QWidget * parent ) : QComboBox( parent ), FormWidgetIface( this, choice ) { addItems( choice->choices() ); setEditable( true ); setInsertPolicy( NoInsert ); lineEdit()->setReadOnly( !choice->isEditable() ); QList< int > selectedItems = choice->currentChoices(); if ( selectedItems.count() == 1 && selectedItems.at(0) >= 0 && selectedItems.at(0) < count() ) setCurrentIndex( selectedItems.at(0) ); if ( choice->isEditable() && !choice->editChoice().isEmpty() ) lineEdit()->setText( choice->editChoice() ); - connect( this, SIGNAL(currentIndexChanged(int)), this, SLOT(slotValueChanged()) ); + connect( this, QOverload::of(&QComboBox::currentIndexChanged), this, &ComboEdit::slotValueChanged ); connect( this, &QComboBox::editTextChanged, this, &ComboEdit::slotValueChanged ); connect( lineEdit(), &QLineEdit::cursorPositionChanged, this, &ComboEdit::slotValueChanged ); setVisible( choice->isVisible() ); setCursor( Qt::ArrowCursor ); m_prevCursorPos = lineEdit()->cursorPosition(); m_prevAnchorPos = lineEdit()->cursorPosition(); } void ComboEdit::setFormWidgetsController(FormWidgetsController* controller) { FormWidgetIface::setFormWidgetsController(controller); connect( m_controller, &FormWidgetsController::formComboChangedByUndoRedo, this, &ComboEdit::slotHandleFormComboChangedByUndoRedo); } void ComboEdit::slotValueChanged() { const QString text = lineEdit()->text(); Okular::FormFieldChoice *form = static_cast(m_ff); QString prevText; if ( form->currentChoices().isEmpty() ) { prevText = form->editChoice(); } else { prevText = form->choices().at(form->currentChoices().constFirst()); } int cursorPos = lineEdit()->cursorPosition(); if ( text != prevText ) { emit m_controller->formComboChangedByWidget( pageItem()->pageNumber(), form, currentText(), cursorPos, m_prevCursorPos, m_prevAnchorPos ); } prevText = text; m_prevCursorPos = cursorPos; m_prevAnchorPos = cursorPos; if ( lineEdit()->hasSelectedText() ) { if ( cursorPos == lineEdit()->selectionStart() ) { m_prevAnchorPos = lineEdit()->selectionStart() + lineEdit()->selectedText().size(); } else { m_prevAnchorPos = lineEdit()->selectionStart(); } } } void ComboEdit::slotHandleFormComboChangedByUndoRedo( int pageNumber, Okular::FormFieldChoice* form, const QString & text, int cursorPos, int anchorPos ) { Q_UNUSED(pageNumber); if ( m_ff != form ) { return; } // Determine if text corrisponds to an index choices int index = -1; for ( int i = 0; i < count(); i++ ) { if ( itemText(i) == text ) { index = i; } } m_prevCursorPos = cursorPos; m_prevAnchorPos = anchorPos; disconnect( lineEdit(), &QLineEdit::cursorPositionChanged, this, &ComboEdit::slotValueChanged ); const bool isCustomValue = index == -1; if ( isCustomValue ) { setEditText( text ); } else { setCurrentIndex( index ); } lineEdit()->setCursorPosition( anchorPos ); lineEdit()->cursorForward( true, cursorPos - anchorPos ); connect( lineEdit(), &QLineEdit::cursorPositionChanged, this, &ComboEdit::slotValueChanged ); setFocus(); } void ComboEdit::contextMenuEvent( QContextMenuEvent* event ) { QMenu *menu = lineEdit()->createStandardContextMenu(); QList actionList = menu->actions(); enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, DeleteAct, SelectAllAct }; QAction *kundo = KStandardAction::create( KStandardAction::Undo, m_controller, SIGNAL( requestUndo() ), menu ); QAction *kredo = KStandardAction::create( KStandardAction::Redo, m_controller, SIGNAL( requestRedo() ), menu ); connect( m_controller, &FormWidgetsController::canUndoChanged, kundo, &QAction::setEnabled ); connect( m_controller, &FormWidgetsController::canRedoChanged, kredo, &QAction::setEnabled ); kundo->setEnabled( m_controller->canUndo() ); kredo->setEnabled( m_controller->canRedo() ); QAction *oldUndo, *oldRedo; oldUndo = actionList[UndoAct]; oldRedo = actionList[RedoAct]; menu->insertAction( oldUndo, kundo ); menu->insertAction( oldRedo, kredo ); menu->removeAction( oldUndo ); menu->removeAction( oldRedo ); menu->exec( event->globalPos() ); delete menu; } bool ComboEdit::event( QEvent* e ) { if ( e->type() == QEvent::KeyPress ) { QKeyEvent *keyEvent = static_cast< QKeyEvent* >(e); if ( keyEvent == QKeySequence::Undo ) { emit m_controller->requestUndo(); return true; } else if ( keyEvent == QKeySequence::Redo ) { emit m_controller->requestRedo(); return true; } } return QComboBox::event( e ); } SignatureEdit::SignatureEdit( Okular::FormFieldSignature * signature, QWidget * parent ) : QAbstractButton( parent ), FormWidgetIface( this, signature ), m_widgetPressed( false ), m_dummyMode( false ), m_wasVisible( false ) { setCursor( Qt::PointingHandCursor ); connect( this, &SignatureEdit::clicked, this, &SignatureEdit::slotViewProperties ); } void SignatureEdit::setDummyMode( bool set ) { m_dummyMode = set; if ( m_dummyMode ) { m_wasVisible = isVisible(); //if widget was hidden then show it. //even if it wasn't hidden calling this will still update the background. setVisibility( true ); } else { //forms were not visible before this call so hide this widget. if ( !m_wasVisible ) setVisibility( false ); //forms were visible even before this call so only update the background color. else update(); } } bool SignatureEdit::event( QEvent * e ) { if ( m_dummyMode && e->type() != QEvent::Paint ) { e->accept(); return true; } switch ( e->type() ) { case QEvent::MouseButtonPress: { QMouseEvent *ev = static_cast< QMouseEvent * >( e ); if ( ev->button() == Qt::LeftButton ) { m_widgetPressed = true; update(); } break; } case QEvent::MouseButtonRelease: { QMouseEvent *ev = static_cast< QMouseEvent * >( e ); if ( ev->button() == Qt::LeftButton) { m_widgetPressed = false; update(); } break; } case QEvent::Leave: { m_widgetPressed = false; update(); } default: break; } return QAbstractButton::event( e ); } void SignatureEdit::contextMenuEvent( QContextMenuEvent * event ) { QMenu *menu = new QMenu( this ); QAction *signatureProperties = new QAction( i18n("Signature Properties"), menu ); connect( signatureProperties, &QAction::triggered, this, &SignatureEdit::slotViewProperties ); menu->addAction( signatureProperties ); menu->exec( event->globalPos() ); delete menu; } void SignatureEdit::paintEvent( QPaintEvent * ) { QPainter painter( this ); //no borders when user hasn't allowed the forms to be shown if ( m_dummyMode && !m_wasVisible ) { painter.setPen( Qt::transparent ); } else { painter.setPen( Qt::black ); } if ( m_widgetPressed || m_dummyMode ) { QColor col = palette().color( QPalette::Active, QPalette::Highlight ); col.setAlpha(50); painter.setBrush( col ); } else { painter.setBrush( Qt::transparent ); } painter.drawRect( 0, 0, width()-2, height()-2 ); } void SignatureEdit::slotViewProperties() { if ( m_dummyMode ) return; Okular::FormFieldSignature *formSignature = static_cast< Okular::FormFieldSignature * >( formField() ); SignaturePropertiesDialog propDlg( m_controller->m_doc, formSignature, this ); propDlg.exec(); } // Code for additional action handling. // Challenge: Change preprocessor magic to C++ magic! // // The mouseRelease event is special because the PDF spec // says that the activation action takes precedence over this. // So the mouse release action is only signaled if no activation // action exists. // // For checkboxes the activation action is not triggered as // they are still triggered from the clicked signal and additionally // when the checked state changes. #define DEFINE_ADDITIONAL_ACTIONS(FormClass, BaseClass) \ void FormClass::mousePressEvent( QMouseEvent *event ) \ { \ Okular::Action *act = m_ff->additionalAction( Okular::Annotation::MousePressed ); \ if ( act ) \ { \ m_controller->signalAction( act ); \ } \ BaseClass::mousePressEvent( event ); \ } \ void FormClass::mouseReleaseEvent( QMouseEvent *event ) \ { \ if ( !QWidget::rect().contains( event->localPos().toPoint() ) ) \ { \ BaseClass::mouseReleaseEvent( event ); \ return; \ } \ Okular::Action *act = m_ff->activationAction(); \ if ( act && !qobject_cast< CheckBoxEdit* > ( this ) ) \ { \ m_controller->signalAction( act ); \ } \ else if ( ( act = m_ff->additionalAction( Okular::Annotation::MouseReleased ) ) ) \ { \ m_controller->signalAction( act ); \ } \ BaseClass::mouseReleaseEvent( event ); \ } \ void FormClass::focusInEvent( QFocusEvent *event ) \ { \ Okular::Action *act = m_ff->additionalAction( Okular::Annotation::FocusIn ); \ if ( act ) \ { \ m_controller->processScriptAction( act, m_ff, Okular::Annotation::FocusIn ); \ } \ BaseClass::focusInEvent( event ); \ } \ void FormClass::focusOutEvent( QFocusEvent *event ) \ { \ Okular::Action *act = m_ff->additionalAction( Okular::Annotation::FocusOut ); \ if ( act ) \ { \ m_controller->processScriptAction( act, m_ff, Okular::Annotation::FocusOut ); \ } \ BaseClass::focusOutEvent( event ); \ } \ void FormClass::leaveEvent( QEvent *event ) \ { \ Okular::Action *act = m_ff->additionalAction( Okular::Annotation::CursorLeaving ); \ if ( act ) \ { \ m_controller->signalAction( act ); \ } \ BaseClass::leaveEvent( event ); \ } \ void FormClass::enterEvent( QEvent *event ) \ { \ Okular::Action *act = m_ff->additionalAction( Okular::Annotation::CursorEntering ); \ if ( act ) \ { \ m_controller->signalAction( act ); \ } \ BaseClass::enterEvent( event ); \ } DEFINE_ADDITIONAL_ACTIONS( PushButtonEdit, QPushButton ) DEFINE_ADDITIONAL_ACTIONS( CheckBoxEdit, QCheckBox ) DEFINE_ADDITIONAL_ACTIONS( RadioButtonEdit, QRadioButton ) DEFINE_ADDITIONAL_ACTIONS( FormLineEdit, QLineEdit ) DEFINE_ADDITIONAL_ACTIONS( TextAreaEdit, KTextEdit ) DEFINE_ADDITIONAL_ACTIONS( FileEdit, KUrlRequester ) DEFINE_ADDITIONAL_ACTIONS( ListEdit, QListWidget ) DEFINE_ADDITIONAL_ACTIONS( ComboEdit, QComboBox ) DEFINE_ADDITIONAL_ACTIONS( SignatureEdit, QAbstractButton ) #undef DEFINE_ADDITIONAL_ACTIONS diff --git a/ui/minibar.cpp b/ui/minibar.cpp index 3d9a986fe..e51c9bc87 100644 --- a/ui/minibar.cpp +++ b/ui/minibar.cpp @@ -1,598 +1,598 @@ /*************************************************************************** * Copyright (C) 2005 by Enrico Ros * * 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 "minibar.h" // qt / kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // local includes #include "core/document.h" #include "core/page.h" // [private widget] a flat qpushbutton that enlights on hover class HoverButton : public QToolButton { Q_OBJECT public: HoverButton( QWidget * parent ); }; MiniBarLogic::MiniBarLogic( QObject * parent, Okular::Document * document ) : QObject(parent) , m_document( document ) { } MiniBarLogic::~MiniBarLogic() { m_document->removeObserver( this ); } void MiniBarLogic::addMiniBar( MiniBar * miniBar ) { m_miniBars.insert( miniBar ); } void MiniBarLogic::removeMiniBar( MiniBar * miniBar ) { m_miniBars.remove( miniBar ); } Okular::Document *MiniBarLogic::document() const { return m_document; } int MiniBarLogic::currentPage() const { return m_document->currentPage(); } void MiniBarLogic::notifySetup( const QVector< Okular::Page * > & pageVector, int setupFlags ) { // only process data when document changes if ( !( setupFlags & Okular::DocumentObserver::DocumentChanged ) ) return; // if document is closed or has no pages, hide widget const int pages = pageVector.count(); if ( pages < 1 ) { for ( MiniBar *miniBar : qAsConst(m_miniBars) ) { miniBar->setEnabled( false ); } return; } bool labelsDiffer = false; for (const Okular::Page * page : pageVector) { if (!page->label().isEmpty()) { if (page->label().toInt() != (page->number() + 1)) { labelsDiffer = true; } } } const QString pagesString = QString::number( pages ); for ( MiniBar *miniBar : qAsConst(m_miniBars) ) { // resize width of widgets miniBar->resizeForPage( pages ); // update child widgets miniBar->m_pageLabelEdit->setPageLabels( pageVector ); miniBar->m_pageNumberEdit->setPagesNumber( pages ); miniBar->m_pagesButton->setText( pagesString ); miniBar->m_prevButton->setEnabled( false ); miniBar->m_nextButton->setEnabled( false ); miniBar->m_pageLabelEdit->setVisible( labelsDiffer ); miniBar->m_pageNumberLabel->setVisible( labelsDiffer ); miniBar->m_pageNumberEdit->setVisible( !labelsDiffer ); miniBar->adjustSize(); miniBar->setEnabled( true ); } } void MiniBarLogic::notifyCurrentPageChanged( int previousPage, int currentPage ) { Q_UNUSED( previousPage ) // get current page number const int pages = m_document->pages(); // if the document is opened and page is changed if ( pages > 0 ) { const QString pageNumber = QString::number( currentPage + 1 ); const QString pageLabel = m_document->page( currentPage )->label(); for ( MiniBar *miniBar : qAsConst(m_miniBars) ) { // update prev/next button state miniBar->m_prevButton->setEnabled( currentPage > 0 ); miniBar->m_nextButton->setEnabled( currentPage < ( pages - 1 ) ); // update text on widgets miniBar->m_pageNumberEdit->setText( pageNumber ); miniBar->m_pageNumberLabel->setText( pageNumber ); miniBar->m_pageLabelEdit->setText( pageLabel ); } } } /** MiniBar **/ MiniBar::MiniBar( QWidget * parent, MiniBarLogic * miniBarLogic ) : QWidget( parent ) , m_miniBarLogic( miniBarLogic ) , m_oldToobarParent( nullptr ) { setObjectName( QStringLiteral( "miniBar" ) ); m_miniBarLogic->addMiniBar( this ); QHBoxLayout * horLayout = new QHBoxLayout( this ); horLayout->setContentsMargins( 0, 0, 0, 0 ); horLayout->setSpacing( 3 ); QSize buttonSize( KIconLoader::SizeSmallMedium, KIconLoader::SizeSmallMedium ); // bottom: left prev_page button m_prevButton = new HoverButton( this ); m_prevButton->setIcon( QIcon::fromTheme( QStringLiteral("arrow-up") ) ); m_prevButton->setIconSize( buttonSize ); horLayout->addWidget( m_prevButton ); // bottom: left lineEdit (current page box) m_pageNumberEdit = new PageNumberEdit( this ); horLayout->addWidget( m_pageNumberEdit ); m_pageNumberEdit->installEventFilter( this ); // bottom: left labelWidget (current page label) m_pageLabelEdit = new PageLabelEdit( this ); horLayout->addWidget(m_pageLabelEdit); m_pageLabelEdit->installEventFilter( this ); // bottom: left labelWidget (current page label) m_pageNumberLabel = new QLabel( this ); m_pageNumberLabel->setAlignment( Qt::AlignCenter ); horLayout->addWidget(m_pageNumberLabel); // bottom: central 'of' label horLayout->addSpacing(5); horLayout->addWidget( new QLabel( i18nc( "Layouted like: '5 [pages] of 10'", "of" ), this ) ); // bottom: right button m_pagesButton = new HoverButton( this ); horLayout->addWidget( m_pagesButton ); // bottom: right next_page button m_nextButton = new HoverButton( this ); m_nextButton->setIcon( QIcon::fromTheme( QStringLiteral ("arrow-down") ) ); m_nextButton->setIconSize( buttonSize ); horLayout->addWidget( m_nextButton ); QSizePolicy sp = sizePolicy(); sp.setHorizontalPolicy( QSizePolicy::Fixed ); sp.setVerticalPolicy( QSizePolicy::Fixed ); setSizePolicy( sp ); // resize width of widgets resizeForPage( 0 ); // connect signals from child widgets to internal handlers / signals bouncers - connect( m_pageNumberEdit, SIGNAL(returnPressed()), this, SLOT(slotChangePage()) ); - connect( m_pageLabelEdit, SIGNAL(pageNumberChosen(int)), this, SLOT(slotChangePage(int)) ); + connect( m_pageNumberEdit, &PageNumberEdit::returnPressed, this, &MiniBar::slotChangePageFromReturn ); + connect( m_pageLabelEdit, &PageLabelEdit::pageNumberChosen, this, &MiniBar::slotChangePage ); connect( m_pagesButton, &QAbstractButton::clicked, this, &MiniBar::gotoPage ); connect( m_prevButton, &QAbstractButton::clicked, this, &MiniBar::prevPage ); connect( m_nextButton, &QAbstractButton::clicked, this, &MiniBar::nextPage ); adjustSize(); // widget starts disabled (will be enabled after opening a document) setEnabled( false ); } MiniBar::~MiniBar() { m_miniBarLogic->removeMiniBar( this ); } void MiniBar::changeEvent( QEvent * event ) { if ( event->type() == QEvent::ParentChange ) { QToolBar *tb = dynamic_cast( parent() ); if ( tb != m_oldToobarParent ) { if ( m_oldToobarParent ) { disconnect( m_oldToobarParent, &QToolBar::iconSizeChanged, this, &MiniBar::slotToolBarIconSizeChanged); } m_oldToobarParent = tb; if ( tb ) { connect( tb, &QToolBar::iconSizeChanged, this, &MiniBar::slotToolBarIconSizeChanged); slotToolBarIconSizeChanged(); } } } } bool MiniBar::eventFilter( QObject *target, QEvent *event ) { if ( target == m_pageNumberEdit || target == m_pageLabelEdit ) { if ( event->type() == QEvent::KeyPress ) { QKeyEvent *keyEvent = static_cast( event ); int key = keyEvent->key(); if ( key == Qt::Key_PageUp || key == Qt::Key_PageDown || key == Qt::Key_Up || key == Qt::Key_Down ) { emit forwardKeyPressEvent( keyEvent ); return true; } } } return false; } -void MiniBar::slotChangePage() +void MiniBar::slotChangePageFromReturn() { // get text from the lineEdit const QString pageNumber = m_pageNumberEdit->text(); // convert it to page number and go to that page bool ok; int number = pageNumber.toInt( &ok ) - 1; if ( ok && number >= 0 && number < (int)m_miniBarLogic->document()->pages() && number != m_miniBarLogic->currentPage() ) { slotChangePage( number ); } } void MiniBar::slotChangePage( int pageNumber ) { m_miniBarLogic->document()->setViewportPage( pageNumber ); m_pageNumberEdit->clearFocus(); m_pageLabelEdit->clearFocus(); } void MiniBar::slotEmitNextPage() { // emit signal emit nextPage(); } void MiniBar::slotEmitPrevPage() { // emit signal emit prevPage(); } void MiniBar::slotToolBarIconSizeChanged() { const QSize buttonSize = m_oldToobarParent->iconSize(); m_prevButton->setIconSize( buttonSize ); m_nextButton->setIconSize( buttonSize ); } void MiniBar::resizeForPage( int pages ) { int numberWidth = 10 + fontMetrics().width( QString::number( pages ) ); m_pageNumberEdit->setMinimumWidth( numberWidth ); m_pageNumberEdit->setMaximumWidth( 2 * numberWidth ); m_pageLabelEdit->setMinimumWidth( numberWidth ); m_pageLabelEdit->setMaximumWidth( 2 * numberWidth ); m_pageNumberLabel->setMinimumWidth( numberWidth ); m_pageNumberLabel->setMaximumWidth( 2 * numberWidth ); m_pagesButton->setMinimumWidth( numberWidth ); m_pagesButton->setMaximumWidth( 2 * numberWidth ); } /** ProgressWidget **/ ProgressWidget::ProgressWidget( QWidget * parent, Okular::Document * document ) : QWidget( parent ), m_document( document ), m_progressPercentage( -1 ) { setObjectName( QStringLiteral( "progress" ) ); setAttribute( Qt::WA_OpaquePaintEvent, true ); setFixedHeight( 4 ); setMouseTracking( true ); } ProgressWidget::~ProgressWidget() { m_document->removeObserver( this ); } void ProgressWidget::notifyCurrentPageChanged( int previousPage, int currentPage ) { Q_UNUSED( previousPage ) // get current page number int pages = m_document->pages(); // if the document is opened and page is changed if ( pages > 0 ) { // update percentage const float percentage = pages < 2 ? 1.0 : (float)currentPage / (float)(pages - 1); setProgress( percentage ); } } void ProgressWidget::setProgress( float percentage ) { m_progressPercentage = percentage; update(); } void ProgressWidget::slotGotoNormalizedPage( float index ) { // figure out page number and go to that page int number = (int)( index * (float)m_document->pages() ); if ( number >= 0 && number < (int)m_document->pages() && number != (int)m_document->currentPage() ) m_document->setViewportPage( number ); } void ProgressWidget::mouseMoveEvent( QMouseEvent * e ) { if ( ( QApplication::mouseButtons() & Qt::LeftButton ) && width() > 0 ) slotGotoNormalizedPage( (float)( QApplication::isRightToLeft() ? width() - e->x() : e->x() ) / (float)width() ); } void ProgressWidget::mousePressEvent( QMouseEvent * e ) { if ( e->button() == Qt::LeftButton && width() > 0 ) slotGotoNormalizedPage( (float)( QApplication::isRightToLeft() ? width() - e->x() : e->x() ) / (float)width() ); } void ProgressWidget::wheelEvent( QWheelEvent * e ) { if ( e->angleDelta().y() > 0 ) emit nextPage(); else emit prevPage(); } void ProgressWidget::paintEvent( QPaintEvent * e ) { QPainter p( this ); if ( m_progressPercentage < 0.0 ) { p.fillRect( rect(), palette().color( QPalette::Active, QPalette::HighlightedText ) ); return; } // find out the 'fill' and the 'clear' rectangles int w = width(), h = height(), l = (int)( (float)w * m_progressPercentage ); QRect cRect = ( QApplication::isRightToLeft() ? QRect( 0, 0, w - l, h ) : QRect( l, 0, w - l, h ) ).intersected( e->rect() ); QRect fRect = ( QApplication::isRightToLeft() ? QRect( w - l, 0, l, h ) : QRect( 0, 0, l, h ) ).intersected( e->rect() ); QPalette pal = palette(); // paint clear rect if ( cRect.isValid() ) p.fillRect( cRect, pal.color( QPalette::Active, QPalette::HighlightedText ) ); // draw a frame-like outline //p.setPen( palette().active().mid() ); //p.drawRect( 0,0, w, h ); // paint fill rect if ( fRect.isValid() ) p.fillRect( fRect, pal.color( QPalette::Active, QPalette::Highlight ) ); if ( l && l != w ) { p.setPen( pal.color( QPalette::Active, QPalette::Highlight ).darker( 120 ) ); int delta = QApplication::isRightToLeft() ? w - l : l; p.drawLine( delta, 0, delta, h ); } } /** PageLabelEdit **/ PageLabelEdit::PageLabelEdit( MiniBar * parent ) : PagesEdit( parent ) { setVisible( false ); - connect( this, SIGNAL(returnPressed()), this, SLOT(pageChosen()) ); + connect( this, &PageLabelEdit::returnPressed, this, &PageLabelEdit::pageChosen ); } void PageLabelEdit::setText( const QString & newText ) { m_lastLabel = newText; PagesEdit::setText( newText ); } void PageLabelEdit::setPageLabels( const QVector &pageVector ) { m_labelPageMap.clear(); completionObject()->clear(); for (const Okular::Page * page : pageVector) { if ( !page->label().isEmpty() ) { m_labelPageMap.insert( page->label(), page->number() ); bool ok; page->label().toInt( &ok ); if ( !ok ) { // Only add to the completion objects labels that are not numbers completionObject()->addItem( page->label() ); } } } } void PageLabelEdit::pageChosen() { const QString newInput = text(); const int pageNumber = m_labelPageMap.value( newInput, -1 ); if (pageNumber != -1) { emit pageNumberChosen( pageNumber ); } else { setText( m_lastLabel ); } } /** PageNumberEdit **/ PageNumberEdit::PageNumberEdit( MiniBar * miniBar ) : PagesEdit( miniBar ) { // use an integer validator m_validator = new QIntValidator( 1, 1, this ); setValidator( m_validator ); } void PageNumberEdit::setPagesNumber( int pages ) { m_validator->setTop( pages ); } /** PagesEdit **/ PagesEdit::PagesEdit( MiniBar * parent ) : KLineEdit( parent ), m_miniBar( parent ), m_eatClick( false ) { // customize text properties setAlignment( Qt::AlignCenter ); // send a focus out event QFocusEvent fe( QEvent::FocusOut ); QApplication::sendEvent( this, &fe ); connect( qApp, &QGuiApplication::paletteChanged, this, &PagesEdit::updatePalette ); } void PagesEdit::setText( const QString & newText ) { // call default handler if hasn't focus if ( !hasFocus() ) { KLineEdit::setText( newText ); } // else preserve existing selection else { // save selection and adapt it to the new text length int selectionLength = selectedText().length(); const bool allSelected = ( selectionLength == text().length() ); if ( allSelected ) { KLineEdit::setText( newText ); selectAll(); } else { int newSelectionStart = newText.length() - text().length() + selectionStart(); if ( newSelectionStart < 0 ) { // the new text is shorter than the old one, and the front part, which is "cut off", is selected // shorten the selection accordingly selectionLength += newSelectionStart; newSelectionStart = 0; } KLineEdit::setText( newText ); setSelection( newSelectionStart, selectionLength ); } } } void PagesEdit::updatePalette() { QPalette pal; if ( hasFocus() ) pal.setColor( QPalette::Active, QPalette::Base, QApplication::palette().color( QPalette::Active, QPalette::Base ) ); else pal.setColor( QPalette::Base, QApplication::palette().color( QPalette::Base ).darker( 102 ) ); setPalette( pal ); } void PagesEdit::focusInEvent( QFocusEvent * e ) { // select all text selectAll(); if ( e->reason() == Qt::MouseFocusReason ) m_eatClick = true; // change background color to the default 'edit' color updatePalette(); // call default handler KLineEdit::focusInEvent( e ); } void PagesEdit::focusOutEvent( QFocusEvent * e ) { // change background color to a dark tone updatePalette(); // call default handler KLineEdit::focusOutEvent( e ); } void PagesEdit::mousePressEvent( QMouseEvent * e ) { // if this click got the focus in, don't process the event if ( !m_eatClick ) KLineEdit::mousePressEvent( e ); m_eatClick = false; } void PagesEdit::wheelEvent( QWheelEvent * e ) { if ( e->angleDelta().y() > 0 ) m_miniBar->slotEmitNextPage(); else m_miniBar->slotEmitPrevPage(); } /** HoverButton **/ HoverButton::HoverButton( QWidget * parent ) : QToolButton( parent ) { setAutoRaise(true); setFocusPolicy(Qt::NoFocus); setToolButtonStyle(Qt::ToolButtonIconOnly); KAcceleratorManager::setNoAccel( this ); } #include "minibar.moc" /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/minibar.h b/ui/minibar.h index 95c386828..63374cab8 100644 --- a/ui/minibar.h +++ b/ui/minibar.h @@ -1,182 +1,182 @@ /*************************************************************************** * Copyright (C) 2005 by Enrico Ros * * 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. * ***************************************************************************/ #ifndef _OKULAR_MINIBAR_H_ #define _OKULAR_MINIBAR_H_ #include #include #include #include "core/observer.h" namespace Okular { class Document; } class MiniBar; class HoverButton; class QIntValidator; class QLabel; class QToolBar; // [private widget] lineEdit for entering/validating page numbers class PagesEdit : public KLineEdit { Q_OBJECT public: explicit PagesEdit( MiniBar * parent ); void setText( const QString & ) override; protected: void focusInEvent( QFocusEvent * e ) override; void focusOutEvent( QFocusEvent * e ) override; void mousePressEvent( QMouseEvent * e ) override; void wheelEvent( QWheelEvent * e ) override; private Q_SLOTS: void updatePalette(); private: MiniBar * m_miniBar; bool m_eatClick; }; class PageNumberEdit : public PagesEdit { Q_OBJECT public: explicit PageNumberEdit( MiniBar * miniBar ); void setPagesNumber( int pages ); private: QIntValidator * m_validator; }; class PageLabelEdit : public PagesEdit { Q_OBJECT public: explicit PageLabelEdit( MiniBar * parent ); void setText( const QString & newText ) override; void setPageLabels( const QVector< Okular::Page * > & pageVector ); Q_SIGNALS: void pageNumberChosen( int page ); private Q_SLOTS: void pageChosen(); private: QString m_lastLabel; QMap m_labelPageMap; }; /** * @short The object that observes the document and feeds the minibars */ class MiniBarLogic : public QObject, public Okular::DocumentObserver { Q_OBJECT public: MiniBarLogic( QObject * parent, Okular::Document * m_document ); ~MiniBarLogic() override; void addMiniBar( MiniBar * miniBar ); void removeMiniBar( MiniBar * miniBar ); Okular::Document *document() const; int currentPage() const; // [INHERITED] from DocumentObserver void notifySetup( const QVector< Okular::Page * > & pageVector, int setupFlags ) override; void notifyCurrentPageChanged( int previous, int current ) override; private: QSet m_miniBars; Okular::Document * m_document; }; /** * @short A widget to display page number and change current page. */ class MiniBar : public QWidget { Q_OBJECT friend class MiniBarLogic; public: MiniBar( QWidget *parent, MiniBarLogic * miniBarLogic ); ~MiniBar() override; void changeEvent( QEvent * event ) override; Q_SIGNALS: void gotoPage(); void prevPage(); void nextPage(); void forwardKeyPressEvent( QKeyEvent *e ); public Q_SLOTS: - void slotChangePage(); + void slotChangePageFromReturn(); void slotChangePage(int page); void slotEmitNextPage(); void slotEmitPrevPage(); void slotToolBarIconSizeChanged(); private: void resizeForPage( int pages ); bool eventFilter( QObject *target, QEvent *event ) override; MiniBarLogic * m_miniBarLogic; PageNumberEdit * m_pageNumberEdit; PageLabelEdit * m_pageLabelEdit; QLabel * m_pageNumberLabel; HoverButton * m_prevButton; HoverButton * m_pagesButton; HoverButton * m_nextButton; QToolBar * m_oldToobarParent; }; /** * @short A small progress bar. */ class ProgressWidget : public QWidget, public Okular::DocumentObserver { Q_OBJECT public: ProgressWidget( QWidget * parent, Okular::Document * document ); ~ProgressWidget() override; // [INHERITED] from DocumentObserver void notifyCurrentPageChanged( int previous, int current ) override; void slotGotoNormalizedPage( float index ); Q_SIGNALS: void prevPage(); void nextPage(); protected: void setProgress( float percentage ); void mouseMoveEvent( QMouseEvent * e ) override; void mousePressEvent( QMouseEvent * e ) override; void wheelEvent( QWheelEvent * e ) override; void paintEvent( QPaintEvent * e ) override; private: Okular::Document * m_document; float m_progressPercentage; }; #endif diff --git a/ui/pageview.cpp b/ui/pageview.cpp index fbaf2bcf1..ff3968891 100644 --- a/ui/pageview.cpp +++ b/ui/pageview.cpp @@ -1,5753 +1,5753 @@ /*************************************************************************** * 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 #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 "toggleactionmenu.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 }; // This is the length of the text that will be shown when the user is searching for a specific piece of text. static const int searchTextPreviewLength = 21; // When following a link, only a preview of this length will be used to set the text of the action. static const int linkTextPreviewLength = 30; 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 mouseGrabOffset; 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; int lastSourceLocationViewportPageNumber; double lastSourceLocationViewportNormalizedX; double lastSourceLocationViewportNormalizedY; 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; QAction * aSpeakPauseResume; KActionCollection * actionCollection; QActionGroup * mouseModeActionGroup; ToggleActionMenu * aMouseModeMenu; 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; QScroller * scroller; }; 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, &FormWidgetsController::changed, q, &PageView::slotFormChanged ); QObject::connect( formsWidgetController, &FormWidgetsController::action, q, &PageView::slotAction ); QObject::connect( formsWidgetController, &FormWidgetsController::formatAction, q, [this] (const Okular::Action *action, Okular::FormFieldText *fft ) { document->processFormatAction( action, fft ); } ); QObject::connect( formsWidgetController, &FormWidgetsController::keystrokeAction, q, [this] (const Okular::Action *action, Okular::FormFieldText *fft, bool &ok ) { document->processKeystrokeAction( action, fft, ok ); } ); QObject::connect( formsWidgetController, &FormWidgetsController::focusAction, q, [this] (const Okular::Action *action, Okular::FormFieldText *fft ) { document->processFocusAction( action, fft ); } ); QObject::connect( formsWidgetController, &FormWidgetsController::validateAction, q, [this] (const Okular::Action *action, Okular::FormFieldText *fft, bool &ok ) { document->processValidateAction( action, fft, ok ); } ); } return formsWidgetController; } #ifdef HAVE_SPEECH OkularTTS* PageViewPrivate::tts() { if ( !m_tts ) { m_tts = new OkularTTS( q ); if ( aSpeakStop ) { QObject::connect( m_tts, &OkularTTS::canPauseOrResume, aSpeakStop, &QAction::setEnabled ); } if ( aSpeakPauseResume ) { QObject::connect( m_tts, &OkularTTS::canPauseOrResume, aSpeakPauseResume, &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( 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->lastSourceLocationViewportPageNumber = -1; d->lastSourceLocationViewportNormalizedX = 0.0; d->lastSourceLocationViewportNormalizedY = 0.0; 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->aSpeakPauseResume = nullptr; d->actionCollection = nullptr; d->aPageSizes=nullptr; d->setting_viewCols = Okular::Settings::viewColumns(); d->rtl_Mode = Okular::Settings::rtlReadingDirection(); d->mouseModeActionGroup = nullptr; d->aMouseModeMenu = 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 ); d->delayResizeEventTimer->setObjectName("delayResizeEventTimer"); 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 ); d->scroller = QScroller::scroller(viewport()); QScrollerProperties prop; prop.setScrollMetric(QScrollerProperties::DecelerationFactor, 0.3); prop.setScrollMetric(QScrollerProperties::MaximumVelocity, 1); prop.setScrollMetric(QScrollerProperties::OvershootScrollDistanceFactor, 0.05); prop.setScrollMetric(QScrollerProperties::OvershootDragDistanceFactor, 0.05); d->scroller->setScrollerProperties(prop); connect(d->scroller, &QScroller::stateChanged, this, &PageView::slotRequestVisiblePixmaps); // 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); auto update_scroller = [=](){ d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); //sync scroller with scrollbar }; connect(verticalScrollBar(), &QAbstractSlider::sliderReleased, this, update_scroller); connect(horizontalScrollBar(), &QAbstractSlider::sliderReleased, this, update_scroller); connect(verticalScrollBar(), &QAbstractSlider::sliderMoved, this, update_scroller); connect(horizontalScrollBar(), &QAbstractSlider::sliderMoved, this, update_scroller); 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()) ); + connect( d->aZoom, QOverload::of(&KSelectAction::triggered), this, &PageView::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)) ); + connect( d->aPageSizes , QOverload::of(&KSelectAction::triggered), + this, &PageView::slotPageSizes ); // 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( QVariant::fromValue( (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( QVariant::fromValue( (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( QVariant::fromValue( 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(); for (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("transform-browse") ), i18n( "&Browse" ), this ); ac->addAction(QStringLiteral("mouse_drag"), d->aMouseNormal ); connect( d->aMouseNormal, &QAction::triggered, this, &PageView::slotSetMouseNormal ); 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"), this); ac->addAction(QStringLiteral("mouse_zoom"), mz ); connect( mz, &QAction::triggered, this, &PageView::slotSetMouseZoom ); 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("Area &Selection"), this); ac->addAction(QStringLiteral("mouse_select"), d->aMouseSelect ); connect( d->aMouseSelect, &QAction::triggered, this, &PageView::slotSetMouseSelect ); 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("edit-select-text") ), i18n("&Text Selection"), this); ac->addAction(QStringLiteral("mouse_textselect"), d->aMouseTextSelect ); connect( d->aMouseTextSelect, &QAction::triggered, this, &PageView::slotSetMouseTextSelect ); 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"), this); ac->addAction(QStringLiteral("mouse_tableselect"), d->aMouseTableSelect ); connect( d->aMouseTableSelect, &QAction::triggered, this, &PageView::slotSetMouseTableSelect ); 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->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 ); // Mouse-Mode action menu d->aMouseModeMenu = new ToggleActionMenu( QIcon(),QString(), this, ToggleActionMenu::MenuButtonPopup, ToggleActionMenu::ImplicitDefaultAction ); d->aMouseModeMenu->addAction( d->aMouseSelect ); d->aMouseModeMenu->addAction( d->aMouseTextSelect ); d->aMouseModeMenu->addAction( d->aMouseTableSelect ); d->aMouseModeMenu->suggestDefaultAction( d->aMouseTextSelect ); d->aMouseModeMenu->setText( i18nc( "@action", "Selection Tools" ) ); ac->addAction( QStringLiteral( "mouse_selecttools" ), d->aMouseModeMenu ); 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); // 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 ); d->aSpeakPauseResume = new QAction( QIcon::fromTheme( QStringLiteral("media-playback-pause") ), i18n( "Pause/Resume Speaking" ), this ); ac->addAction( QStringLiteral("speak_pause_resume"), d->aSpeakPauseResume ); d->aSpeakPauseResume->setEnabled( false ); connect( d->aSpeakPauseResume, &QAction::triggered, this, &PageView::slotPauseResumeSpeech ); #else d->aSpeakDoc = nullptr; d->aSpeakPage = nullptr; d->aSpeakStop = nullptr; d->aSpeakPauseResume = nullptr; #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; for (AnnotWindow *aw : qAsConst(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.values(); 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 const QSet formWidgetsList = d->items[i]->formWidgets(); for ( FormWidgetIface *w : formWidgetsList ) 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(); for (AnnotWindow *aw : qAsConst(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(); for ( 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() ); const QHash videoWidgets = item->videoWidgets(); for ( VideoWidget *vw : 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; const QList sizes = d->document->pageSizes(); for ( const Okular::PageSize &p : sizes ) 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->aMouseModeMenu ) d->aMouseModeMenu->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 center( centerCoord.x(), centerCoord.y(), smoothMove ); 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 = std::find( annots.begin(), annots.end(), (*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 ) { const QHash videoWidgetsList = item->videoWidgets(); for ( VideoWidget *videoWidget : videoWidgetsList ) videoWidget->pageLeft(); } // On close, run the widget scripts, needed for running animated PDF const Okular::Page *page = d->document->page( previous ); const QLinkedList annotations = page->annotations(); for ( Okular::Annotation *annotation : annotations ) { if ( annotation->subType() == Okular::Annotation::AWidget ) { Okular::WidgetAnnotation *widgetAnnotation = static_cast( annotation ); d->document->processAction( widgetAnnotation->additionalAction( Okular::Annotation::PageClosing ) ); } } } if ( current != -1 ) { PageViewItem * item = d->items.at( current ); if ( item ) { const QHash videoWidgetsList = item->videoWidgets(); for ( VideoWidget *videoWidget : videoWidgetsList ) videoWidget->pageEntered(); } // update zoom text and factor if in a ZoomFit/* zoom mode if ( d->zoomMode != ZoomFixed ) updateZoomText(); // Opening any widget scripts, needed for running animated PDF const Okular::Page *page = d->document->page( current ); const QLinkedList annotations = page->annotations(); for ( Okular::Annotation *annotation : annotations ) { if ( annotation->subType() == Okular::Annotation::AWidget ) { Okular::WidgetAnnotation *widgetAnnotation = static_cast( annotation ); d->document->processAction( widgetAnnotation->additionalAction( Okular::Annotation::PageOpening ) ); } } } } //END DocumentObserver inherited methods //BEGIN View inherited methods bool PageView::supportsCapability( ViewCapability capability ) const { switch ( capability ) { case Zoom: case ZoomModality: case Continuous: case ViewModeModality: case TrimMargins: return true; } return false; } Okular::View::CapabilityFlags PageView::capabilityFlags( ViewCapability capability ) const { switch ( capability ) { case Zoom: case ZoomModality: case Continuous: case ViewModeModality: case TrimMargins: return CapabilityRead | CapabilityWrite | CapabilitySerializable; } return NoFlag; } QVariant PageView::capability( ViewCapability capability ) const { switch ( capability ) { case Zoom: return d->zoomFactor; case ZoomModality: return d->zoomMode; case Continuous: return d->aViewContinuous ? d->aViewContinuous->isChecked() : true; case ViewModeModality: { const int nActions = d->aViewMode ? d->aViewMode->menu()->actions().size() : 0; for (int i=0; i < nActions; ++i) { const QAction* action = d->aViewMode->menu()->actions().at(i); if ( action->isChecked() ) return action->data(); } return QVariant(); } case TrimMargins: return d->aTrimMargins ? d->aTrimMargins->isChecked() : false; } 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; } case ViewModeModality: { bool ok = true; int mode = option.toInt( &ok ); if ( ok ) { if ( mode >= 0 && mode < Okular::Settings::EnumViewMode::COUNT) updateViewMode(mode); } break; } case Continuous: { bool mode = option.toBool( ); d->aViewContinuous->setChecked(mode); slotContinuousToggled(mode); break; } case TrimMargins: { bool value = option.toBool( ); d->aTrimMargins->setChecked(value); slotTrimMarginsToggled(value); 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()->update(); } // 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 QRegion rgn = pe->region(); // preprocess rects area to see if it worths or not using subdivision uint summedArea = 0; for ( const QRect & r : rgn ) { summedArea += r.width() * r.height(); } // very elementary check: SUMj(Region[j].area) is less than boundingRect.area const bool useSubdivision = summedArea < (0.6 * contentsRect.width() * contentsRect.height()); if ( !useSubdivision ) { rgn = contentsRect; } // iterate over the rects (only one loop if not using subdivision) for ( const QRect & r : rgn ) { if ( useSubdivision ) { // set 'contentsRect' to a part of the sub-divided region contentsRect = r.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) for (const TableSelectionPart &tsp : qAsConst(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) for (const TableSelectionPart &tsp : qAsConst(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 ); } for (const TableSelectionPart &tsp : qAsConst(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 ); for (double col : qAsConst(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() ); } } for (double row : qAsConst(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; // move/scroll page by using keys switch ( e->key() ) { case Qt::Key_J: case Qt::Key_Down: slotScrollDown( 1 /* go down 1 step */ ); break; case Qt::Key_PageDown: slotScrollDown(); break; case Qt::Key_K: case Qt::Key_Up: slotScrollUp( 1 /* go up 1 step */ ); break; case Qt::Key_PageUp: case Qt::Key_Backspace: slotScrollUp(); 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{ d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(-horizontalScrollBar()->singleStep(), 0), 100); } 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{ d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(horizontalScrollBar()->singleStep(), 0), 100); } 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; // 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()->update(); } 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->scroller->state() == QScroller::Inactive || d->scroller->state() == QScroller::Scrolling) { d->mouseGrabOffset = QPoint(0,0); d->scroller->handleInput(QScroller::InputPress, e->pos(), e->timestamp()-1); } setCursor( Qt::ClosedHandCursor ); QPoint mousePos = e->globalPos(); const QRect mouseContainer = QApplication::desktop()->screenGeometry( this ); // wrap mouse from top to bottom if ( mousePos.y() <= mouseContainer.top() + 4 && verticalScrollBar()->value() < verticalScrollBar()->maximum() - 10 ) { mousePos.setY( mouseContainer.bottom() - 5 ); QCursor::setPos( mousePos ); d->mouseGrabOffset -= QPoint(0, mouseContainer.height()); } // wrap mouse from bottom to top else if ( mousePos.y() >= mouseContainer.bottom() - 4 && verticalScrollBar()->value() > 10 ) { mousePos.setY( mouseContainer.top() + 5 ); d->mouseGrabOffset += QPoint(0, mouseContainer.height()); QCursor::setPos( mousePos ); } d->scroller->handleInput(QScroller::InputMove, e->pos() + d->mouseGrabOffset, e->timestamp()); } } 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) )) 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() ) { d->scroller->stop(); 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 ); } if( !d->mouseOnRect ){ d->mouseGrabOffset = QPoint(0,0); d->scroller->handleInput(QScroller::InputPress, e->pos(), e->timestamp()); 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 ); for ( 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; for (const TableSelectionPart &tsp : qAsConst(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; } 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:{ d->scroller->handleInput(QScroller::InputRelease, e->pos() + d->mouseGrabOffset, e->timestamp()); //disable flick if the cursor has wrapped around if(d->mouseGrabOffset != QPoint(0,0)) d->scroller->stop(); // 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(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(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 ) { addSearchWithinDocumentAction( &menu, selectedText ); 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 { addSearchWithinDocumentAction(menu, d->selectedText()); 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, linkTextPreviewLength ); httpLink = menu->addAction( i18n( "Go to '%1'", squeezedText ) ); httpLink->setObjectName(QStringLiteral("GoToAction")); } } } if ( menu ) { 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; for ( const TableSelectionPart& tsp : qAsConst(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 ); for (const 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 ) { if ( !d->document->isOpened() ) { QAbstractScrollArea::wheelEvent( e ); return; } int delta = e->angleDelta().y(), 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 ); d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); //sync scroller with scrollbar } } 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 ); d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); //sync scroller with scrollbar } } else{ if(delta != 0 && delta % QWheelEvent::DefaultDeltasPerStep == 0 ){ //number of scroll wheel steps Qt gives to us at the same time int count = abs(delta / QWheelEvent::DefaultDeltasPerStep); if(delta<0){ slotScrollDown(count); }else{ slotScrollUp(count); } } else{ d->scroller->scrollTo(d->scroller->finalPosition() - e->angleDelta()/4.0 , 0 ); } } } 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 ); for ( const QRect &rect : rgn ) viewport()->update( 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(); for ( const PageViewItem *item : qAsConst(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; for ( const int p : qAsConst(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 for ( const int page : qAsConst(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 ); // create a region from which we'll subtract painted rects QRegion remainingArea( contentsRect ); // This loop draws the actual pages // 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( contentsRect ) ) continue; // get item and item's outline geometries QRect itemGeometry = item->croppedGeometry(); // move the painter to the top-left corner of the real page p->save(); p->translate( itemGeometry.left(), itemGeometry.top() ); // 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 -= itemGeometry; p->restore(); } // fill the visible area around the page with the background color for (const QRect& backRect : remainingArea ) p->fillRect( backRect, backColor ); // take outline and shadow into account when testing whether a repaint is necessary auto dpr = devicePixelRatioF(); QRect checkRect = contentsRect; checkRect.adjust( -3, -3, 1, 1 ); // Method to linearly interpolate between black (=(0,0,0), omitted) and the background color auto interpolateColor = [&backColor]( double t ) { return QColor( t*backColor.red(), t*backColor.green(), t*backColor.blue() ); }; // width of the shadow in device pixels static const int shadowWidth = 2*dpr; // iterate over all items painting a black outline and a simple bottom/right gradient 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(); // 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 bottom-right shadow) if ( !itemGeometry.contains( contentsRect ) ) { int itemWidth = itemGeometry.width(); int itemHeight = itemGeometry.height(); // draw simple outline QPen pen( Qt::black ); pen.setWidth(0); p->setPen( pen ); QRectF outline( -1.0/dpr, -1.0/dpr, itemWidth + 1.0/dpr, itemHeight + 1.0/dpr ); p->drawRect( outline ); // draw bottom/right gradient for ( int i = 1; i <= shadowWidth; i++ ) { pen.setColor( interpolateColor( double(i)/( shadowWidth+1 ) ) ); p->setPen( pen ); QPointF left( (i-1)/dpr, itemHeight + i/dpr ); QPointF up( itemWidth + i/dpr, (i-1)/dpr ); QPointF corner( itemWidth + i/dpr, itemHeight + i/dpr); p->drawLine( left, corner ); p->drawLine( up, corner ); } } p->restore(); } } 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.0 ); c.ry() += qRound( normClamp( vp.rePos.normalizedY, 0.0 ) * (double)r.height() + viewport()->height() / 2.0 ); } } 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 for ( int p : noMoreSelectedPages ) { d->document->setPageTextSelection( p, nullptr, QColor() ); } // set the new selection for the selected pages for ( int p : qAsConst(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; for (const TableSelectionPart &tsp : qAsConst(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() / static_cast(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); std::copy(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 = std::lower_bound(zoomValue.begin(), zoomValue.end(), newFactor) - 1; } else { if (newFactor >= zoomValue.last()) return; i = std::upper_bound(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::updateViewMode(const int nr) { const QList actions = d->aViewMode->menu()->actions(); for ( QAction* action : actions ) { QVariant mode_id = action->data(); if (mode_id.toInt() == nr) { action->trigger(); } } } 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, bool smoothMove) { scrollTo( cx - viewport()->width() / 2, cy - viewport()->height() / 2, smoothMove ); } void PageView::scrollTo( int x, int y, bool smoothMove ) { 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; if(smoothMove) d->scroller->scrollTo(QPoint(x, y)); else d->scroller->scrollTo(QPoint(x, y), 0); 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, searchTextPreviewLength ); webShortcutsMenu->setTitle( i18n( "Search for '%1' with", squeezedText ) ); QAction *action = nullptr; for ( 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 ) { const Okular::Action * link = static_cast< const Okular::Action * >( rect->object() ); if (!link) return nullptr; QMenu *menu = new QMenu(this); // creating the menu and its actions QAction * processLink = menu->addAction( i18n( "Follow This Link" ) ); 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(QStringLiteral("CopyLinkLocationAction")); connect( actCopyLinkLocation, &QAction::triggered, menu, [ 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, [this, link]() { d->document->processAction( link ); }); return menu; } return nullptr; } void PageView::addSearchWithinDocumentAction(QMenu *menu, const QString &searchText) { const QString squeezedText = KStringHandler::rsqueeze( searchText, searchTextPreviewLength ); QAction *action = new QAction(i18n("Search for '%1' in this document", squeezedText), menu); action->setIcon( QIcon::fromTheme( QStringLiteral("document-preview") ) ); connect(action, &QAction::triggered, this, [this, searchText]{Q_EMIT triggerSearch(searchText);}); menu->addAction( action ); } //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; } 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->scroller->state() == QScroller::Scrolling) 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 ) ) { const QSet formWidgetsList = i->formWidgets(); for ( FormWidgetIface *fwi : formWidgetsList) { 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() ); } const QHash videoWidgets = i->videoWidgets(); for ( VideoWidget *vw : 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) const double distance = hypot( (geometry.left() + geometry.right()) / 2.0 - (viewportCenterX - 4), (geometry.top() + geometry.bottom()) / 2.0 - 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::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 ]; d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, delta), scrollDelay[ index ]); } 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( int nSteps ) { //if we are too far behind the animation, do nothing and let it catch up auto limit_value = nSteps ? 200 : verticalScrollBar()->rect().height(); if(d->scroller->state() == QScroller::Scrolling && abs(d->scroller->finalPosition().y() - verticalScrollBar()->value()) > limit_value){ return; } // if in single page mode and at the top of the screen, go to \ page if ( Okular::Settings::viewContinuous() || verticalScrollBar()->value() > verticalScrollBar()->minimum() ) { if ( nSteps ){ d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0,-100*nSteps), 100); }else{ if(d->scroller->finalPosition().y() > verticalScrollBar()->minimum()) d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, -verticalScrollBar()->rect().height() )); } } 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( int nSteps ) { //if we are too far behind the animation, do nothing and let it catch up auto limit_value = nSteps ? 200 : verticalScrollBar()->rect().height(); if(d->scroller->state() == QScroller::Scrolling && abs(d->scroller->finalPosition().y() - verticalScrollBar()->value()) > limit_value){ return; } // 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 ( nSteps ){ d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0,100*nSteps), 100); }else{ if(d->scroller->finalPosition().y() < verticalScrollBar()->maximum()) d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, verticalScrollBar()->rect().height() )); } } 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(); for (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() { for (int req : qAsConst(d->refreshPages)) { QTimer::singleShot(0, this, [this, req] { d->document->refreshPixmaps(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(); } void PageView::slotPauseResumeSpeech() { if ( !d->m_tts ) return; d->m_tts->pauseResumeSpeech(); } #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(); for ( const PageViewItem *pageItem : qAsConst(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 ); 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 ) { const QSet fwi = (*dIt)->formWidgets(); for ( FormWidgetIface *fw : fwi ) { if ( fw->formField() == form ) { SignatureEdit *widget = static_cast< SignatureEdit * >( fw ); widget->setDummyMode( true ); QTimer::singleShot( 250, this, [=]{ widget->setDummyMode( false ); }); return; } } } } //END private SLOTS /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/pageview.h b/ui/pageview.h index 5de995f22..61b9d20e2 100644 --- a/ui/pageview.h +++ b/ui/pageview.h @@ -1,288 +1,289 @@ /*************************************************************************** * Copyright (C) 2004 by Enrico Ros * * Copyright (C) 2004 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * With portions of code from kpdf/kpdf_pagewidget.h by: * * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2003 by Christophe Devriese * * * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003 by Kurt Pfeifle * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ // This file follows coding style described in kdebase/kicker/HACKING #ifndef _OKULAR_PAGEVIEW_H_ #define _OKULAR_PAGEVIEW_H_ #include #include #include #include "ui/pageviewutils.h" #include "core/area.h" #include "core/observer.h" #include "core/view.h" class KActionCollection; namespace Okular { class Action; class Document; class DocumentViewport; class FormFieldSignature; class Annotation; class MovieAction; class RenditionAction; } class PageViewPrivate; class QGestureEvent; /** * @short The main view. Handles zoom and continuous mode.. oh, and page * @short display of course :-) * ... */ class PageView : public QAbstractScrollArea, public Okular::DocumentObserver, public Okular::View { Q_OBJECT public: PageView( QWidget *parent, Okular::Document *document ); ~PageView() override; // Zoom mode ( last 4 are internally used only! ) enum ZoomMode { ZoomFixed = 0, ZoomFitWidth = 1, ZoomFitPage = 2, ZoomFitAuto = 3, ZoomIn, ZoomOut, ZoomRefreshCurrent, ZoomActual }; enum ClearMode { ClearAllSelection, ClearOnlyDividers }; // create actions that interact with this widget void setupBaseActions( KActionCollection * ac ); void setupViewerActions( KActionCollection * ac ); void setupActions( KActionCollection * ac ); void updateActionState( bool docHasPages, bool documentChanged, bool docHasFormWidgets ); // misc methods (from RMB menu/children) bool canFitPageWidth() const; void fitPageWidth( int page ); // keep in sync with pageviewutils void displayMessage( const QString & message, const QString & details = QString(), PageViewMessage::Icon icon=PageViewMessage::Info, int duration=-1 ); // inherited from DocumentObserver void notifySetup( const QVector< Okular::Page * > & pages, int setupFlags ) override; void notifyViewportChanged( bool smoothMove ) override; void notifyPageChanged( int pageNumber, int changedFlags ) override; void notifyContentsCleared( int changedFlags ) override; void notifyZoom(int factor) override; bool canUnloadPixmap( int pageNum ) const override; void notifyCurrentPageChanged( int previous, int current ) override; // inherited from View bool supportsCapability( ViewCapability capability ) const override; CapabilityFlags capabilityFlags( ViewCapability capability ) const override; QVariant capability( ViewCapability capability ) const override; void setCapability( ViewCapability capability, const QVariant &option ) override; QList< Okular::RegularAreaRect * > textSelections( const QPoint& start, const QPoint& end, int& firstpage ); Okular::RegularAreaRect * textSelectionForItem( const PageViewItem * item, const QPoint & startPoint = QPoint(), const QPoint & endPoint = QPoint() ); void reparseConfig(); KActionCollection *actionCollection() const; QAction *toggleFormsAction() const; int contentAreaWidth() const; int contentAreaHeight() const; QPoint contentAreaPosition() const; QPoint contentAreaPoint( const QPoint & pos ) const; QPointF contentAreaPoint( const QPointF & pos ) const; bool areSourceLocationsShownGraphically() const; void setShowSourceLocationsGraphically(bool show); void setLastSourceLocationViewport( const Okular::DocumentViewport& vp ); void clearLastSourceLocationViewport(); void updateCursor(); void highlightSignatureFormWidget( const Okular::FormFieldSignature *form ); public Q_SLOTS: void copyTextSelection() const; void selectAll(); void openAnnotationWindow( Okular::Annotation *annotation, int pageNumber ); void reloadForms(); void slotToggleChangeColors(); void slotSetChangeColors(bool active); void slotSelectPage(); void slotAction( Okular::Action *action ); void slotFormChanged( int pageNumber ); + void externalKeyPressEvent( QKeyEvent *e ); + Q_SIGNALS: void rightClick( const Okular::Page *, const QPoint & ); void mouseBackButtonClick(); void mouseForwardButtonClick(); void escPressed(); void fitWindowToPage( const QSize& pageViewPortSize, const QSize& pageSize ); void triggerSearch( const QString& text ); protected: bool event( QEvent * event ) override; void resizeEvent( QResizeEvent* ) override; bool gestureEvent( QGestureEvent * e ); // mouse / keyboard events void keyPressEvent( QKeyEvent* ) override; void keyReleaseEvent( QKeyEvent* ) override; void inputMethodEvent( QInputMethodEvent * ) override; void wheelEvent( QWheelEvent* ) override; void paintEvent( QPaintEvent *e ) override; void tabletEvent (QTabletEvent *e ) override; void mouseMoveEvent( QMouseEvent *e ) override; void mousePressEvent( QMouseEvent *e ) override; void mouseReleaseEvent( QMouseEvent *e ) override; void mouseDoubleClickEvent( QMouseEvent *e ) override; bool viewportEvent( QEvent *e ) override; void scrollContentsBy( int dx, int dy ) override; private: // draw background and items on the opened qpainter void drawDocumentOnPainter( const QRect & contentsRect, QPainter * p ); // update item width and height using current zoom parameters void updateItemSize( PageViewItem * item, int colWidth, int rowHeight ); // return the widget placed on a certain point or 0 if clicking on empty space PageViewItem * pickItemOnPoint( int x, int y ); // start / modify / clear selection rectangle void selectionStart( const QPoint & pos, const QColor & color, bool aboveAll = false ); void selectionClear( const ClearMode mode = ClearAllSelection ); void drawTableDividers(QPainter * screenPainter); void guessTableDividers(); // update either text or rectangle selection void updateSelection( const QPoint & pos ); // compute the zoom factor value for FitWidth and FitPage mode double zoomFactorFitMode( ZoomMode mode ); // update internal zoom values and end in a slotRelayoutPages(); void updateZoom( ZoomMode newZoomMode ); // update the text on the label using global zoom value or current page's one void updateZoomText(); // update view mode (single, facing...) void updateViewMode ( const int nr ); void textSelectionClear(); // updates cursor void updateCursor( const QPoint &p ); void moveMagnifier( const QPoint &p ); void updateMagnifier( const QPoint &p ); int viewColumns() const; void center(int cx, int cy, bool smoothMove = false); void scrollTo( int x, int y, bool smoothMove = false); void toggleFormWidgets( bool on ); void resizeContentArea( const QSize & newSize ); void updatePageStep(); void addSearchWithinDocumentAction(QMenu * menu, const QString & searchText ); void addWebShortcutsMenu( QMenu * menu, const QString & text ); QMenu* createProcessLinkMenu( PageViewItem *item, const QPoint & eventPos ); // used when selecting stuff, makes the view scroll as necessary to keep the mouse inside the view void scrollPosIntoView( const QPoint & pos ); QPoint viewportToContentArea( const Okular::DocumentViewport & vp ) const; // called from slots to turn off trim modes mutually exclusive to id void updateTrimMode( int except_id ); // handle link clicked bool mouseReleaseOverLink( const Okular::ObjectRect * rect ) const; void createAnnotationsVideoWidgets(PageViewItem *item, const QLinkedList< Okular::Annotation * > &annotations); // don't want to expose classes in here class PageViewPrivate * d; private Q_SLOTS: // used to decouple the notifyViewportChanged calle void slotRealNotifyViewportChanged(bool smoothMove); // activated either directly or via queued connection on notifySetup void slotRelayoutPages(); // activated by the resize event delay timer void delayedResizeEvent(); // activated either directly or via the contentsMoving(int,int) signal void slotRequestVisiblePixmaps( int newValue = -1 ); // activated by the autoscroll timer (Shift+Up/Down keys) void slotAutoScroll(); // activated by the dragScroll timer void slotDragScroll(); // show the welcome message void slotShowWelcome(); // activated by left click timer void slotShowSizeAllCursor(); void slotHandleWebShortcutAction(); void slotConfigureWebShortcuts(); // connected to local actions (toolbar, menu, ..) void slotZoom(); void slotZoomIn(); void slotZoomOut(); void slotZoomActual(); void slotFitToWidthToggled( bool ); void slotFitToPageToggled( bool ); void slotAutoFitToggled( bool ); void slotViewMode( QAction *action ); void slotContinuousToggled( bool ); void slotSetMouseNormal(); void slotSetMouseZoom(); void slotSetMouseMagnifier(); void slotSetMouseSelect(); void slotSetMouseTextSelect(); void slotSetMouseTableSelect(); void slotToggleAnnotator( bool ); void slotAutoScrollUp(); void slotAutoScrollDown(); void slotScrollUp( int nSteps = 0 ); void slotScrollDown(int nSteps = 0 ); void slotRotateClockwise(); void slotRotateCounterClockwise(); void slotRotateOriginal(); void slotPageSizes( int ); void slotTrimMarginsToggled( bool ); void slotTrimToSelectionToggled( bool ); void slotToggleForms(); void slotRefreshPage(); #ifdef HAVE_SPEECH void slotSpeakDocument(); void slotSpeakCurrentPage(); void slotStopSpeaks(); void slotPauseResumeSpeech(); #endif - void externalKeyPressEvent( QKeyEvent *e ); void slotAnnotationWindowDestroyed( QObject *window ); void slotProcessMovieAction( const Okular::MovieAction *action ); void slotProcessRenditionAction( const Okular::RenditionAction *action ); void slotFitWindowToPage(); }; #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/propertiesdialog.cpp b/ui/propertiesdialog.cpp index 0d81d549c..182851dc8 100644 --- a/ui/propertiesdialog.cpp +++ b/ui/propertiesdialog.cpp @@ -1,429 +1,429 @@ /*************************************************************************** * Copyright (C) 2004 by Albert Astals Cid * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "propertiesdialog.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // local includes #include "core/document.h" #include "core/fontinfo.h" static const int IsExtractableRole = Qt::UserRole; static const int FontInfoRole = Qt::UserRole + 1; PropertiesDialog::PropertiesDialog(QWidget *parent, Okular::Document *doc) : KPageDialog( parent ), m_document( doc ), m_fontPage( nullptr ), m_fontModel( nullptr ), m_fontInfo( nullptr ), m_fontProgressBar( nullptr ), m_fontScanStarted( false ) { setFaceType( Tabbed ); setWindowTitle( i18n( "Unknown File" ) ); setStandardButtons( QDialogButtonBox::Ok ); // PROPERTIES QFrame *page = new QFrame(); KPageWidgetItem *item = addPage( page, i18n( "&Properties" ) ); item->setIcon( QIcon::fromTheme( QStringLiteral("document-properties") ) ); // get document info const Okular::DocumentInfo info = doc->documentInfo(); QFormLayout *layout = new QFormLayout( page ); // mime name based on mimetype id QString mimeName = info.get( Okular::DocumentInfo::MimeType ).section( QLatin1Char('/'), -1 ).toUpper(); setWindowTitle( i18n( "%1 Properties", mimeName ) ); /* obtains the properties list, conveniently ordered */ QStringList orderedProperties; orderedProperties << Okular::DocumentInfo::getKeyString( Okular::DocumentInfo::FilePath ) << Okular::DocumentInfo::getKeyString( Okular::DocumentInfo::PagesSize ) << Okular::DocumentInfo::getKeyString( Okular::DocumentInfo::DocumentSize ); for (Okular::DocumentInfo::Key ks = Okular::DocumentInfo::Title; ks <= Okular::DocumentInfo::Keywords; ks = Okular::DocumentInfo::Key( ks+1 ) ) { orderedProperties << Okular::DocumentInfo::getKeyString( ks ); } const QStringList infoKeys = info.keys(); for (const QString &ks : infoKeys) { if ( !orderedProperties.contains( ks ) ) { orderedProperties << ks; } } for ( const QString &key : qAsConst(orderedProperties) ) { const QString titleString = info.getKeyTitle( key ); const QString valueString = info.get( key ); if ( titleString.isNull() || valueString.isNull() ) continue; // create labels and layout them QWidget *value = nullptr; if ( key == Okular::DocumentInfo::getKeyString( Okular::DocumentInfo::MimeType ) ) { /// for mime type fields, show icon as well value = new QWidget( page ); /// place icon left of mime type's name QHBoxLayout *hboxLayout = new QHBoxLayout( value ); hboxLayout->setContentsMargins( 0, 0, 0, 0 ); /// retrieve icon and place it in a QLabel QMimeDatabase db; QMimeType mimeType = db.mimeTypeForName( valueString ); KSqueezedTextLabel *squeezed; if (mimeType.isValid()) { /// retrieve icon and place it in a QLabel QLabel *pixmapLabel = new QLabel( value ); hboxLayout->addWidget( pixmapLabel, 0 ); pixmapLabel->setPixmap( KIconLoader::global()->loadMimeTypeIcon( mimeType.iconName(), KIconLoader::Small ) ); /// mime type's name and label squeezed = new KSqueezedTextLabel( i18nc( "mimetype information, example: \"PDF Document (application/pdf)\"", "%1 (%2)", mimeType.comment(), valueString ), value ); } else { /// only mime type name squeezed = new KSqueezedTextLabel( valueString, value ); } squeezed->setTextInteractionFlags( Qt::TextSelectableByMouse ); hboxLayout->addWidget( squeezed, 1 ); } else { /// default for any other document information KSqueezedTextLabel *label = new KSqueezedTextLabel( valueString, page ); label->setTextInteractionFlags( Qt::TextSelectableByMouse ); value = label; } layout->addRow( new QLabel( i18n( "%1:", titleString ) ), value); } // FONTS if ( doc->canProvideFontInformation() ) { // create fonts tab and layout it QFrame *page2 = new QFrame(); m_fontPage = addPage(page2, i18n("&Fonts")); m_fontPage->setIcon( QIcon::fromTheme( QStringLiteral("preferences-desktop-font") ) ); QVBoxLayout *page2Layout = new QVBoxLayout(page2); // add a tree view QTreeView *view = new QTreeView(page2); view->setContextMenuPolicy(Qt::CustomContextMenu); connect(view, &QTreeView::customContextMenuRequested, this, &PropertiesDialog::showFontsMenu); page2Layout->addWidget(view); view->setRootIsDecorated(false); view->setAlternatingRowColors(true); view->setSortingEnabled( true ); // creating a proxy model so we can sort the data QSortFilterProxyModel *proxymodel = new QSortFilterProxyModel(view); proxymodel->setDynamicSortFilter( true ); proxymodel->setSortCaseSensitivity( Qt::CaseInsensitive ); m_fontModel = new FontsListModel(view); proxymodel->setSourceModel(m_fontModel); view->setModel(proxymodel); view->sortByColumn( 0, Qt::AscendingOrder ); m_fontInfo = new QLabel( this ); page2Layout->addWidget( m_fontInfo ); m_fontInfo->setText( i18n( "Reading font information..." ) ); m_fontInfo->hide(); m_fontProgressBar = new QProgressBar( this ); page2Layout->addWidget( m_fontProgressBar ); m_fontProgressBar->setRange( 0, 100 ); m_fontProgressBar->setValue( 0 ); m_fontProgressBar->hide(); } // KPageDialog is a bit buggy, it doesn't fix its own sizeHint, so we have to manually resize resize(layout->sizeHint()); - connect( pageWidget(), SIGNAL(currentPageChanged(KPageWidgetItem*,KPageWidgetItem*)), - this, SLOT(pageChanged(KPageWidgetItem*,KPageWidgetItem*)) ); + connect( pageWidget(), QOverload::of(&KPageWidget::currentPageChanged), + this, &PropertiesDialog::pageChanged ); } PropertiesDialog::~PropertiesDialog() { m_document->stopFontReading(); } void PropertiesDialog::pageChanged( KPageWidgetItem *current, KPageWidgetItem * ) { if ( current == m_fontPage && !m_fontScanStarted ) { connect(m_document, &Okular::Document::gotFont, m_fontModel, &FontsListModel::addFont); connect(m_document, &Okular::Document::fontReadingProgress, this, &PropertiesDialog::slotFontReadingProgress); connect(m_document, &Okular::Document::fontReadingEnded, this, &PropertiesDialog::slotFontReadingEnded); QTimer::singleShot( 0, this, &PropertiesDialog::reallyStartFontReading ); m_fontScanStarted = true; } } void PropertiesDialog::slotFontReadingProgress( int page ) { m_fontProgressBar->setValue( m_fontProgressBar->maximum() * ( page + 1 ) / m_document->pages() ); } void PropertiesDialog::slotFontReadingEnded() { m_fontInfo->hide(); m_fontProgressBar->hide(); } void PropertiesDialog::reallyStartFontReading() { m_fontInfo->show(); m_fontProgressBar->show(); m_document->startFontReading(); } void PropertiesDialog::showFontsMenu(const QPoint &pos) { QTreeView *view = static_cast(sender()); QModelIndex index = view->indexAt(pos); if (index.data(IsExtractableRole).toBool()) { QMenu *menu = new QMenu(this); menu->addAction( i18nc("@action:inmenu", "&Extract Font") ); QAction *result = menu->exec(view->viewport()->mapToGlobal(pos)); if (result) { Okular::FontInfo fi = index.data(FontInfoRole).value(); const QString caption = i18n( "Where do you want to save %1?", fi.name() ); const QString path = QFileDialog::getSaveFileName( this, caption, fi.name() ); if ( path.isEmpty() ) return; QFile f( path ); if ( f.open( QIODevice::WriteOnly ) ) { f.write( m_document->fontData(fi) ); f.close(); } else { KMessageBox::error( this, i18n( "Could not open \"%1\" for writing. File was not saved.", path ) ); } } } } FontsListModel::FontsListModel( QObject * parent ) : QAbstractTableModel( parent ) { } FontsListModel::~FontsListModel() { } void FontsListModel::addFont( const Okular::FontInfo &fi ) { beginInsertRows( QModelIndex(), m_fonts.size(), m_fonts.size() ); m_fonts << fi; endInsertRows(); } int FontsListModel::columnCount( const QModelIndex &parent ) const { return parent.isValid() ? 0 : 3; } static QString descriptionForFontType( Okular::FontInfo::FontType type ) { switch ( type ) { case Okular::FontInfo::Type1: return i18n("Type 1"); break; case Okular::FontInfo::Type1C: return i18n("Type 1C"); break; case Okular::FontInfo::Type1COT: return i18nc("OT means OpenType", "Type 1C (OT)"); break; case Okular::FontInfo::Type3: return i18n("Type 3"); break; case Okular::FontInfo::TrueType: return i18n("TrueType"); break; case Okular::FontInfo::TrueTypeOT: return i18nc("OT means OpenType", "TrueType (OT)"); break; case Okular::FontInfo::CIDType0: return i18n("CID Type 0"); break; case Okular::FontInfo::CIDType0C: return i18n("CID Type 0C"); break; case Okular::FontInfo::CIDType0COT: return i18nc("OT means OpenType", "CID Type 0C (OT)"); break; case Okular::FontInfo::CIDTrueType: return i18n("CID TrueType"); break; case Okular::FontInfo::CIDTrueTypeOT: return i18nc("OT means OpenType", "CID TrueType (OT)"); break; case Okular::FontInfo::TeXPK: return i18n("TeX PK"); break; case Okular::FontInfo::TeXVirtual: return i18n("TeX virtual"); break; case Okular::FontInfo::TeXFontMetric: return i18n("TeX Font Metric"); break; case Okular::FontInfo::TeXFreeTypeHandled: return i18n("TeX FreeType-handled"); break; case Okular::FontInfo::Unknown: return i18nc("Unknown font type", "Unknown"); break; } return QString(); } static QString pathOrDescription( const Okular::FontInfo &font ) { switch ( font.embedType() ) { case Okular::FontInfo::NotEmbedded: return font.file(); break; case Okular::FontInfo::EmbeddedSubset: return i18n("Embedded (subset)"); break; case Okular::FontInfo::FullyEmbedded: return i18n("Fully embedded"); break; } return QString(); } static QString descriptionForEmbedType( Okular::FontInfo::EmbedType type ) { switch ( type ) { case Okular::FontInfo::NotEmbedded: return i18n("No"); break; case Okular::FontInfo::EmbeddedSubset: return i18n("Yes (subset)"); break; case Okular::FontInfo::FullyEmbedded: return i18n("Yes"); break; } return QString(); } QVariant FontsListModel::data( const QModelIndex &index, int role ) const { if ( !index.isValid() || index.row() < 0 || index.row() >= m_fonts.count() ) return QVariant(); switch ( role ) { case Qt::DisplayRole: switch ( index.column() ) { case 0: { const Okular::FontInfo &fi = m_fonts.at( index.row() ); const QString fontname = fi.name(); const QString substituteName = fi.substituteName(); if ( fi.embedType() == Okular::FontInfo::NotEmbedded && !substituteName.isEmpty() && !fontname.isEmpty() && substituteName != fontname ) { return i18nc("Replacing missing font with another one", "%1 (substituting with %2)", fontname, substituteName); } return fontname.isEmpty() ? i18nc( "font name not available (empty)", "[n/a]" ) : fontname; break; } case 1: return descriptionForFontType( m_fonts.at( index.row() ).type() ); break; case 2: return pathOrDescription( m_fonts.at( index.row() ) ); break; } break; case Qt::ToolTipRole: { QString fontname = m_fonts.at( index.row() ).name(); if ( fontname.isEmpty() ) fontname = i18n( "Unknown font" ); QString tooltip = QLatin1String( "" ) + fontname + QLatin1String( "" ); if ( m_fonts.at( index.row() ).embedType() == Okular::FontInfo::NotEmbedded ) tooltip += QStringLiteral( " (%2)" ).arg( fontname, fontname ); tooltip += QLatin1String( "
" ) + i18n( "Embedded: %1", descriptionForEmbedType( m_fonts.at( index.row() ).embedType() ) ); tooltip += QLatin1String( "" ); return tooltip; break; } case IsExtractableRole: { return m_fonts.at( index.row() ).canBeExtracted(); } case FontInfoRole: { QVariant v; v.setValue( m_fonts.at( index.row() ) ); return v; } } return QVariant(); } QVariant FontsListModel::headerData( int section, Qt::Orientation orientation, int role ) const { if ( orientation != Qt::Horizontal ) return QVariant(); if ( role == Qt::TextAlignmentRole ) return QVariant( Qt::AlignLeft ); if ( role != Qt::DisplayRole ) return QVariant(); switch ( section ) { case 0: return i18n( "Name" ); break; case 1: return i18n( "Type" ); break; case 2: return i18n( "File" ); break; default: return QVariant(); } } int FontsListModel::rowCount( const QModelIndex &parent ) const { return parent.isValid() ? 0 : m_fonts.size(); } #include "moc_propertiesdialog.cpp" /* kate: replace-tabs on; indent-width 4; */