diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 40db6af06..5bd26991b 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_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="qstring-arg,incorrect-emit,qhash-namespace,detaching-temporary" ninja + - CLAZY_CHECKS="qstring-arg,incorrect-emit,qhash-namespace,detaching-temporary,range-loop" 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/calculatetexttest.cpp b/autotests/calculatetexttest.cpp index f105daba4..fb4c54a65 100644 --- a/autotests/calculatetexttest.cpp +++ b/autotests/calculatetexttest.cpp @@ -1,106 +1,107 @@ /*************************************************************************** * Copyright (C) 2018 by Intevation GmbH * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include #include #include #include #include "../settings_core.h" #include "core/document.h" #include #include class CalculateTextTest: public QObject { Q_OBJECT private slots: void initTestCase(); void cleanupTestCase(); void testSimpleCalculate(); private: Okular::Document *m_document; }; void CalculateTextTest::initTestCase() { Okular::SettingsCore::instance( QStringLiteral("calculatetexttest") ); m_document = new Okular::Document( nullptr ); } void CalculateTextTest::cleanupTestCase() { m_document->closeDocument(); delete m_document; } void CalculateTextTest::testSimpleCalculate() { const QString testFile = QStringLiteral( KDESRCDIR "data/simpleCalculate.pdf" ); QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( testFile ); QCOMPARE( m_document->openDocument( testFile, QUrl(), mime), Okular::Document::OpenSuccess ); const Okular::Page* page = m_document->page( 0 ); QMap fields; // Field names in test document are: // field1, field2, field3, Sum, AVG, Prod, Min, Max - for ( Okular::FormField *ff: page->formFields() ) + const QLinkedList pageFormFields = page->formFields(); + for ( Okular::FormField *ff: pageFormFields ) { fields.insert( ff->name(), static_cast( ff ) ); } // Set some values and do calculation Okular::FormFieldText *field1 = fields[QStringLiteral( "field1" )]; QVERIFY( field1 ); m_document->editFormText( 0, field1, QStringLiteral( "10" ), 0, 0, 0 ); Okular::FormFieldText *field2 = fields[QStringLiteral( "field2" )]; QVERIFY( field2 ); m_document->editFormText( 0, field2, QStringLiteral( "20" ), 0, 0, 0 ); Okular::FormFieldText *field3 = fields[QStringLiteral( "field3" )]; QVERIFY( field3 ); m_document->editFormText( 0, field3, QStringLiteral( "30" ), 0, 0, 0 ); // Verify the results QCOMPARE (fields[QStringLiteral ("Sum")]->text(), QStringLiteral( "60" )); QCOMPARE (fields[QStringLiteral ("AVG")]->text(), QStringLiteral( "20" )); QCOMPARE (fields[QStringLiteral ("Prod")]->text(), QStringLiteral( "6000" )); QCOMPARE (fields[QStringLiteral ("Min")]->text(), QStringLiteral( "10" )); QCOMPARE (fields[QStringLiteral ("Max")]->text(), QStringLiteral( "30" )); // Verify that Sum was not recalculated after set without edit QCOMPARE (fields[QStringLiteral ("Sum")]->text(), QStringLiteral( "60" )); // Test that multiplication with zero works m_document->editFormText( 0, field2, QStringLiteral( "0" ), 0, 0, 0 ); QCOMPARE (fields[QStringLiteral ("Prod")]->text(), QStringLiteral( "0" )); // Test that updating the field also worked with sum QCOMPARE (fields[QStringLiteral ("Sum")]->text(), QStringLiteral( "40" )); // Test that undo / redo works QVERIFY( m_document->canUndo() ); m_document->undo(); QCOMPARE( fields[QStringLiteral ("Sum")]->text(), QStringLiteral( "60" ) ); QVERIFY( m_document->canRedo() ); m_document->redo(); QCOMPARE( fields[QStringLiteral ("Sum")]->text(), QStringLiteral( "40" ) ); } QTEST_MAIN( CalculateTextTest ) #include "calculatetexttest.moc" diff --git a/autotests/formattest.cpp b/autotests/formattest.cpp index 84d3c1644..2e76a376c 100644 --- a/autotests/formattest.cpp +++ b/autotests/formattest.cpp @@ -1,189 +1,190 @@ /*************************************************************************** * Copyright (C) 2019 by João Netto * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include #include #include #include #include "../settings_core.h" #include #include #include #include #include "../generators/poppler/config-okular-poppler.h" class FormatTest: public QObject { Q_OBJECT private slots: void initTestCase(); void cleanupTestCase(); void testTimeFormat(); void testTimeFormat_data(); void testSpecialFormat(); void testSpecialFormat_data(); void testFocusAction(); void testFocusAction_data(); void testValidateAction(); void testValidateAction_data(); private: Okular::Document *m_document; QMap m_fields; QString m_formattedText; }; void FormatTest::initTestCase() { Okular::SettingsCore::instance( QStringLiteral( "formattest" ) ); m_document = new Okular::Document( nullptr ); // Force consistent locale QLocale locale( QStringLiteral( "en_US" ) ); QLocale::setDefault( locale ); const QString testFile = QStringLiteral( KDESRCDIR "data/formattest.pdf" ); QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( testFile ); QCOMPARE( m_document->openDocument( testFile, QUrl(), mime), Okular::Document::OpenSuccess ); connect( m_document, &Okular::Document::refreshFormWidget, [=]( Okular::FormField * form ) { Okular::FormFieldText *fft = reinterpret_cast< Okular::FormFieldText * >( form ); if( fft ) m_formattedText = fft->text(); }); const Okular::Page* page = m_document->page( 0 ); - for ( Okular::FormField *ff: page->formFields() ) + const QLinkedList pageFormFields = page->formFields(); + for ( Okular::FormField *ff: pageFormFields ) { m_fields.insert( ff->name(), ff ); } } void FormatTest::testTimeFormat() { QFETCH( QString, fieldName ); QFETCH( QString, text ); QFETCH( QString, result ); Okular::FormFieldText *fft = reinterpret_cast< Okular::FormFieldText * >( m_fields[ fieldName ] ); fft->setText( text ); m_document->processFormatAction( fft->additionalAction( Okular::FormField::FormatField ), fft ); QCOMPARE( m_formattedText, result ); } void FormatTest::testTimeFormat_data() { QTest::addColumn< QString >( "fieldName" ); QTest::addColumn< QString >( "text" ); QTest::addColumn< QString > ( "result" ); QTest::newRow( "field hh:mm" ) << QStringLiteral( "time1" ) << QStringLiteral( "1:20" ) << QStringLiteral( "01:20" ); QTest::newRow( "field hh:mm with pm" ) << QStringLiteral( "time1" ) << QStringLiteral( "1:20 pm" ) << QStringLiteral( "13:20" ); QTest::newRow( "field hh:mm invalid one number" ) << QStringLiteral( "time1" ) << QStringLiteral( "1" ) << QStringLiteral( "" ); QTest::newRow( "field hh:mm invalid time" ) << QStringLiteral( "time1" ) << QStringLiteral( "25:12" ) << QStringLiteral( "" ); QTest::newRow( "field hh:mm invalid only letters" ) << QStringLiteral( "time1" ) << QStringLiteral( "abcd" ) << QStringLiteral( "" ); QTest::newRow( "field hh:mm ap" ) << QStringLiteral( "time2" ) << QStringLiteral( "1:20" ) << QStringLiteral( "1:20 am" ); QTest::newRow( "field hh:mm ap remove zero" ) << QStringLiteral( "time2" ) << QStringLiteral( "01:20 pm" ) << QStringLiteral( "1:20 pm" ); QTest::newRow( "field hh:mm ap change to AM/PM" ) << QStringLiteral( "time2" ) << QStringLiteral( "13:20" ) << QStringLiteral( "1:20 pm" ); QTest::newRow( "field hh:mm:ss without seconds" ) << QStringLiteral( "time3" ) << QStringLiteral( "1:20" ) << QStringLiteral( "01:20:00" ); QTest::newRow( "field hh:mm:ss with pm" ) << QStringLiteral( "time3" ) << QStringLiteral( "1:20:00 pm" ) << QStringLiteral( "13:20:00" ); QTest::newRow( "field hh:mm:ss ap without am" ) << QStringLiteral( "time4" ) << QStringLiteral( "1:20:00" ) << QStringLiteral( "1:20:00 am" ); QTest::newRow( "field hh:mm:ss ap remove 0" ) << QStringLiteral( "time4" ) << QStringLiteral( "01:20:00 pm" ) << QStringLiteral( "1:20:00 pm" ); QTest::newRow( "field hh:mm:ss ap change to AM/PM" ) << QStringLiteral( "time4" ) << QStringLiteral( "13:20:00" ) << QStringLiteral( "1:20:00 pm" ); } void FormatTest::testSpecialFormat() { m_formattedText = QStringLiteral( "" ); QFETCH( QString, fieldName ); QFETCH( QString, text ); QFETCH( bool, edited ); QFETCH( QString, result ); Okular::FormFieldText *fft = reinterpret_cast< Okular::FormFieldText * >( m_fields[ fieldName ] ); fft->setText( text ); bool ok = false; m_document->processFormatAction( fft->additionalAction( Okular::FormField::FormatField ), fft ); m_document->processKeystrokeAction( fft->additionalAction( Okular::FormField::FieldModified ), fft, ok ); QCOMPARE( m_formattedText, result ); QCOMPARE( ok, edited ); } void FormatTest::testSpecialFormat_data() { QTest::addColumn< QString >( "fieldName" ); QTest::addColumn< QString >( "text" ); QTest::addColumn< bool >( "edited" ); QTest::addColumn< QString > ( "result" ); // The tests which have invalid edited, keep the same value as when it was formatted before. QTest::newRow( "field validated but not changed" ) << QStringLiteral( "CEP" ) << QStringLiteral( "12345" ) << true << QStringLiteral( "" ); QTest::newRow( "field invalid but not changed" ) << QStringLiteral( "CEP" ) << QStringLiteral( "123456" ) << false << QStringLiteral( "" ); QTest::newRow( "field formatted and changed" ) << QStringLiteral( "8Digits" ) << QStringLiteral( "123456789" ) << true << QStringLiteral( "12345-6789" ); QTest::newRow( "field invalid 10 digits" ) << QStringLiteral( "8Digits" ) << QStringLiteral( "1234567890" ) << false << QStringLiteral( "12345-6789" ); QTest::newRow( "field formatted telephone" ) << QStringLiteral( "telefone" ) << QStringLiteral( "1234567890" ) << true << QStringLiteral( "(123) 456-7890" ); QTest::newRow( "field invalid telephone" ) << QStringLiteral( "telefone" ) << QStringLiteral( "12345678900" ) << false << QStringLiteral( "(123) 456-7890" ); QTest::newRow( "field formatted SSN" ) << QStringLiteral( "CPF" ) << QStringLiteral( "123456789" ) << true << QStringLiteral( "123-45-6789" ); QTest::newRow( "field invalid SSN" ) << QStringLiteral( "CPF" ) << QStringLiteral( "1234567890" ) << false << QStringLiteral( "123-45-6789" ); } void FormatTest::testFocusAction() { QFETCH( QString, result ); Okular::FormFieldText *fft = reinterpret_cast< Okular::FormFieldText * >( m_fields[ "Validate/Focus" ] ); m_document->processFocusAction( fft->additionalAction( Okular::Annotation::FocusIn ), fft ); QCOMPARE( fft->text(), result ); } void FormatTest::testFocusAction_data() { QTest::addColumn< QString >( "result" ); QTest::newRow( "when focuses" ) << QStringLiteral( "No" ); } void FormatTest::testValidateAction() { QFETCH( QString, text ); QFETCH( QString, result ); Okular::FormFieldText *fft = reinterpret_cast< Okular::FormFieldText * >( m_fields[ "Validate/Focus" ] ); fft->setText( text ); bool ok = false; m_document->processValidateAction( fft->additionalAction( Okular::Annotation::FocusOut ), fft, ok ); QCOMPARE( fft->text(), result ); QVERIFY( ok ); } void FormatTest::testValidateAction_data() { QTest::addColumn< QString >( "text" ); QTest::addColumn< QString >( "result" ); QTest::newRow( "valid text was set" ) << QStringLiteral( "123" ) << QStringLiteral( "valid" ); QTest::newRow( "invalid text was set" ) << QStringLiteral( "abc" ) << QStringLiteral( "invalid" ); } void FormatTest::cleanupTestCase() { m_document->closeDocument(); delete m_document; } QTEST_MAIN( FormatTest ) #include "formattest.moc" diff --git a/autotests/kjsfunctionstest.cpp b/autotests/kjsfunctionstest.cpp index 7f0856320..44f2549b5 100644 --- a/autotests/kjsfunctionstest.cpp +++ b/autotests/kjsfunctionstest.cpp @@ -1,390 +1,391 @@ /*************************************************************************** * Copyright (C) 2019 by João Netto * * * * 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 "../settings_core.h" #include "core/action.h" #include "core/document.h" #include "core/scripter.h" #include #include #include #include "../generators/poppler/config-okular-poppler.h" class MessageBoxHelper : public QObject { Q_OBJECT public: MessageBoxHelper(QMessageBox::StandardButton b, QString message, QMessageBox::Icon icon, QString title, bool hasCheckBox ) : m_button(b), m_clicked(false), m_message(std::move(message)), m_icon(icon), m_title(std::move(title)), m_checkBox(hasCheckBox) { QTimer::singleShot(0, this, &MessageBoxHelper::closeMessageBox); } ~MessageBoxHelper() override { QVERIFY(m_clicked); } private slots: void closeMessageBox() { const QWidgetList allToplevelWidgets = QApplication::topLevelWidgets(); QMessageBox *mb = nullptr; for ( QWidget *w : allToplevelWidgets ) { if ( w->inherits( "QMessageBox" ) ) { mb = qobject_cast< QMessageBox * >( w ); QCOMPARE( mb->text(), m_message ); QCOMPARE( mb->windowTitle(), m_title ); QCOMPARE( mb->icon(), m_icon ); QCheckBox *box = mb->checkBox(); QCOMPARE( box != nullptr, m_checkBox ); mb->button( m_button )->click(); } } if (!mb) { QTimer::singleShot(0, this, &MessageBoxHelper::closeMessageBox); return; } m_clicked = true; } private: QMessageBox::StandardButton m_button; bool m_clicked; QString m_message; QMessageBox::Icon m_icon; QString m_title; bool m_checkBox; }; class KJSFunctionsTest: public QObject { Q_OBJECT private slots: #ifdef HAVE_POPPLER_0_79 void initTestCase(); void testNthFieldName(); void testDisplay(); void testSetClearInterval(); void testSetClearTimeOut(); void testGetOCGs(); void cleanupTestCase(); void testAlert(); void testPrintD(); void testPrintD_data(); private: Okular::Document *m_document; QMap m_fields; #endif }; #ifdef HAVE_POPPLER_0_79 void KJSFunctionsTest::initTestCase() { Okular::SettingsCore::instance( QStringLiteral("kjsfunctionstest") ); m_document = new Okular::Document( nullptr ); const QString testFile = QStringLiteral( KDESRCDIR "data/kjsfunctionstest.pdf" ); QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( testFile ); QCOMPARE( m_document->openDocument( testFile, QUrl(), mime), Okular::Document::OpenSuccess ); const Okular::Page* page = m_document->page( 0 ); - for ( Okular::FormField *ff: page->formFields() ) + const QLinkedList pageFormFields = page->formFields(); + for ( Okular::FormField *ff: pageFormFields ) { m_fields.insert( ff->name(), ff ); } } void KJSFunctionsTest::testNthFieldName() { for(int i = 0;i < 21;++i) { Okular::ScriptAction *action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "var field = Doc.getField( Doc.getNthFieldName(%1) );\ field.display = display.visible;" ).arg( i ) ); m_document->processAction( action ); QVERIFY( m_fields[QString( "0.%1" ).arg(i)]->isVisible() ); m_fields[QString( "0.%1" ).arg(i)]->setVisible( false ); delete action; } } void KJSFunctionsTest::testDisplay() { Okular::ScriptAction *action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "field = Doc.getField(\"0.0\");field.display=display.hidden;\ field = Doc.getField(\"0.10\");field.display=display.visible;" ) ); m_document->processAction( action ); QVERIFY( !m_fields["0.0"]->isVisible() ); QVERIFY( !m_fields["0.0"]->isPrintable() ); QVERIFY( m_fields["0.10"]->isVisible() ); QVERIFY( m_fields["0.10"]->isPrintable() ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "field = Doc.getField(\"0.10\");field.display=display.noView;\ field = Doc.getField(\"0.15\");field.display=display.noPrint;" ) ); m_document->processAction( action ); QVERIFY( !m_fields["0.10"]->isVisible() ); QVERIFY( m_fields["0.10"]->isPrintable() ); QVERIFY( m_fields["0.15"]->isVisible() ); QVERIFY( !m_fields["0.15"]->isPrintable() ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "field = Doc.getField(\"0.15\");field.display=display.hidden;\ field = Doc.getField(\"0.20\");field.display=display.visible;" ) ); m_document->processAction( action ); QVERIFY( !m_fields["0.15"]->isVisible() ); QVERIFY( !m_fields["0.15"]->isPrintable() ); QVERIFY( m_fields["0.20"]->isVisible() ); QVERIFY( m_fields["0.20"]->isPrintable() ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "field = Doc.getField(\"0.20\");field.display=display.hidden;\ field = Doc.getField(\"0.0\");field.display=display.visible;" ) ); m_document->processAction( action ); QVERIFY( !m_fields["0.20"]->isVisible() ); QVERIFY( !m_fields["0.20"]->isPrintable() ); QVERIFY( m_fields["0.0"]->isVisible() ); QVERIFY( m_fields["0.0"]->isPrintable() ); delete action; } void delay() { QTime dieTime= QTime::currentTime().addSecs( 2 ); while (QTime::currentTime() < dieTime) QCoreApplication::processEvents(QEventLoop::AllEvents, 100); } void KJSFunctionsTest::testSetClearInterval() { Okular::ScriptAction *action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "obj = new Object();obj.idx=0;\ obj.inc=function(){field = Doc.getField(Doc.getNthFieldName(obj.idx));\ field.display = display.visible;\ obj.idx = obj.idx + 1;};\ intv = app.setInterval('obj.inc()', 450);obj.idx;" ) ); m_document->processAction( action ); QVERIFY( m_fields["0.0"]->isVisible() ); QVERIFY( !m_fields["0.3"]->isVisible() ); delete action; delay(); action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "app.clearInterval(intv);obj.idx;" ) ); m_document->processAction( action ); QVERIFY( m_fields["0.3"]->isVisible() ); delete action; } void KJSFunctionsTest::testSetClearTimeOut() { Okular::ScriptAction *action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "intv = app.setTimeOut('obj.inc()', 1);obj.idx;" ) ); m_document->processAction( action ); QVERIFY( m_fields["0.3"]->isVisible() ); QVERIFY( !m_fields["0.4"]->isVisible() ); delay(); delete action; QVERIFY( m_fields["0.4"]->isVisible() ); action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "intv = app.setTimeOut('obj.inc()', 2000);obj.idx;" ) ); m_document->processAction( action ); QVERIFY( m_fields["0.4"]->isVisible() ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "app.clearTimeOut(intv);obj.idx;" ) ); m_document->processAction( action ); QVERIFY( m_fields["0.4"]->isVisible() ); delay(); QVERIFY( m_fields["0.4"]->isVisible() ); delete action; } void KJSFunctionsTest::testGetOCGs() { QAbstractItemModel *model = m_document->layersModel(); Okular::ScriptAction *action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "var ocg = this.getOCGs(this.pageNum);\ ocg[0].state = false;" ) ); m_document->processAction( action ); QVERIFY( !model->data( model->index( 0, 0 ), Qt::CheckStateRole ).toBool() ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "ocg[0].state = true;" ) ); m_document->processAction( action ); QVERIFY( model->data( model->index( 0, 0 ), Qt::CheckStateRole ).toBool() ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "ocg[1].state = false;" ) ); m_document->processAction( action ); QVERIFY( !model->data( model->index( 1, 0 ), Qt::CheckStateRole ).toBool() ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "ocg[1].state = true;" ) ); m_document->processAction( action ); QVERIFY( model->data( model->index( 1, 0 ), Qt::CheckStateRole ).toBool() ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "ocg[2].state = false;" ) ); m_document->processAction( action ); QVERIFY( !model->data( model->index( 2, 0 ), Qt::CheckStateRole ).toBool() ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "ocg[2].state = true;" ) ); m_document->processAction( action ); QVERIFY( model->data( model->index( 2, 0 ), Qt::CheckStateRole ).toBool() ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "ocg[3].state = false;" ) ); m_document->processAction( action ); QVERIFY( !model->data( model->index( 3, 0 ), Qt::CheckStateRole ).toBool() ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "ocg[3].state = true;" ) ); m_document->processAction( action ); QVERIFY( model->data( model->index( 3, 0 ), Qt::CheckStateRole ).toBool() ); delete action; } void KJSFunctionsTest::testAlert() { Okular::ScriptAction *action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "ret = app.alert( \"Random Message\" );" ) ); QScopedPointer< MessageBoxHelper > messageBoxHelper; messageBoxHelper.reset( new MessageBoxHelper( QMessageBox::Ok, QStringLiteral( "Random Message" ), QMessageBox::Critical, QStringLiteral( "Okular" ), false ) ); m_document->processAction( action ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "ret = app.alert( \"Empty Message\", 1 );" ) ); messageBoxHelper.reset( new MessageBoxHelper( QMessageBox::Ok, QStringLiteral( "Empty Message" ), QMessageBox::Warning, QStringLiteral( "Okular" ), false ) ); m_document->processAction( action ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "ret = app.alert( \"No Message\", 2, 2 );" ) ); messageBoxHelper.reset( new MessageBoxHelper( QMessageBox::Yes, QStringLiteral( "No Message" ), QMessageBox::Question, QStringLiteral( "Okular" ), false ) ); m_document->processAction( action ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "ret = app.alert( \"No\", 3, 2, \"Test Dialog\" );" ) ); messageBoxHelper.reset( new MessageBoxHelper( QMessageBox::No, QStringLiteral( "No" ), QMessageBox::Information, QStringLiteral( "Test Dialog" ), false ) ); m_document->processAction( action ); delete action; action = new Okular::ScriptAction( Okular::JavaScript, QStringLiteral( "var oCheckBox = new Object();\ ret = app.alert( \"Cancel\", 3, 3, \"Test Dialog\", 0, oCheckBox );" ) ); messageBoxHelper.reset( new MessageBoxHelper( QMessageBox::Cancel, QStringLiteral( "Cancel" ), QMessageBox::Information, QStringLiteral( "Test Dialog" ), true ) ); m_document->processAction( action ); delete action; } /** @brief Checks a single JS action against an expected result * * Runs an action with the given @p script and checks that it * does pop-up a messagebox with the given @p result text. */ class PrintDHelper { public: PrintDHelper( Okular::Document* document, const QString& script, const QString& result ) : action( new Okular::ScriptAction( Okular::JavaScript, script ) ), box( new MessageBoxHelper( QMessageBox::Ok, result, QMessageBox::Critical, QStringLiteral( "Okular" ), false ) ) { document->processAction( action.data() ); } private: QScopedPointer< Okular::ScriptAction > action; QScopedPointer< MessageBoxHelper > box; } ; void KJSFunctionsTest::testPrintD_data() { // Force consistent locale QLocale locale( QStringLiteral( "en_US" ) ); QLocale::setDefault( locale ); QTest::addColumn("script"); QTest::addColumn("result"); QTest::newRow("mmyyy") << QStringLiteral( "var date = new Date( 2010, 0, 5, 11, 10, 32, 1 );\ ret = app.alert( util.printd( \"mm\\\\yyyy\", date ) );" ) << QStringLiteral( "01\\2010" ); QTest::newRow("myy") << QStringLiteral( "ret = app.alert( util.printd( \"m\\\\yy\", date ) );" ) << QStringLiteral( "1\\10" ); QTest::newRow("ddmmHHMM") << QStringLiteral( "ret = app.alert( util.printd( \"dd\\\\mm HH:MM\", date ) );" ) << QStringLiteral( "05\\01 11:10" ); QTest::newRow("ddmmHHMMss") << QStringLiteral( "ret = app.alert( util.printd( \"dd\\\\mm HH:MM:ss\", date ) );" ) << QStringLiteral( "05\\01 11:10:32" ); QTest::newRow("yyyymmHHMMss") << QStringLiteral( "ret = app.alert( util.printd( \"yyyy\\\\mm HH:MM:ss\", date ) );" ) << QStringLiteral( "2010\\01 11:10:32" ); QTest::newRow("0") << QStringLiteral( "ret = app.alert( util.printd( 0, date ) );" ) << QStringLiteral( "D:20100105111032" ); QTest::newRow("1") << QStringLiteral( "ret = app.alert( util.printd( 1, date ) );" ) << QStringLiteral( "2010.01.05 11:10:32" ); QDate date( 2010, 1, 5 ); QTest::newRow("2") << QStringLiteral( "ret = app.alert( util.printd( 2, date ) );" ) << QString( date.toString( locale.dateFormat( QLocale::ShortFormat ) ) + QStringLiteral( " 11:10:32 AM" ) ); } void KJSFunctionsTest::testPrintD() { QFETCH(QString, script); QFETCH(QString, result); QVERIFY( script.contains( "printd" ) ); PrintDHelper test( m_document, script, result ); } void KJSFunctionsTest::cleanupTestCase() { m_document->closeDocument(); delete m_document; } #endif QTEST_MAIN( KJSFunctionsTest ) #include "kjsfunctionstest.moc" diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp index 7c6067e46..0067eb309 100644 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -1,1991 +1,1997 @@ /*************************************************************************** * Copyright (C) 2013 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include #include "../core/annotations.h" #include "../core/form.h" #include "../core/page.h" #include "../part.h" #include "../ui/toc.h" #include "../ui/sidebar.h" #include "../ui/pageview.h" #include "../ui/presentationwidget.h" #include "../settings.h" #include "closedialoghelper.h" #include "../generators/poppler/config-okular-poppler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Okular { class PartTest : public QObject { Q_OBJECT static bool openDocument(Okular::Part *part, const QString &filePath); signals: void urlHandler(const QUrl &url); // NOLINT(readability-inconsistent-declaration-parameter-name) private slots: void testReload(); void testCanceledReload(); void testTOCReload(); void testForwardPDF(); void testForwardPDF_data(); void testGeneratorPreferences(); void testSelectText(); void testClickInternalLink(); void testOpenUrlArguments(); void test388288(); void testSaveAs(); void testSaveAs_data(); void testSaveAsToNonExistingPath(); void testSaveAsToSymlink(); void testSaveIsSymlink(); void testSidebarItemAfterSaving(); void testViewModeSavingPerFile(); void testSaveAsUndoStackAnnotations(); void testSaveAsUndoStackAnnotations_data(); void testSaveAsUndoStackForms(); void testSaveAsUndoStackForms_data(); void testMouseMoveOverLinkWhileInSelectionMode(); void testClickUrlLinkWhileInSelectionMode(); void testeTextSelectionOverAndAcrossLinks_data(); void testeTextSelectionOverAndAcrossLinks(); void testClickUrlLinkWhileLinkTextIsSelected(); void testRClickWhileLinkTextIsSelected(); void testRClickOverLinkWhileLinkTextIsSelected(); void testRClickOnSelectionModeShoulShowFollowTheLinkMenu(); void testClickAnywhereAfterSelectionShouldUnselect(); void testeRectSelectionStartingOnLinks(); void testCheckBoxReadOnly(); void testCrashTextEditDestroy(); void testAnnotWindow(); void testAdditionalActionTriggers(); void testTypewriterAnnotTool(); void testJumpToPage(); void testForwardBackwardNavigation(); void testTabletProximityBehavior(); void testOpenPrintPreview(); private: void simulateMouseSelection(double startX, double startY, double endX, double endY, QWidget *target); }; class PartThatHijacksQueryClose : public Okular::Part { public: PartThatHijacksQueryClose(QWidget* parentWidget, QObject* parent, const QVariantList& args) : Okular::Part(parentWidget, parent, args), behavior(PassThru) {} enum Behavior { PassThru, ReturnTrue, ReturnFalse }; void setQueryCloseBehavior(Behavior new_behavior) { behavior = new_behavior; } bool queryClose() override { if (behavior == PassThru) return Okular::Part::queryClose(); else // ReturnTrue or ReturnFalse return (behavior == ReturnTrue); } private: Behavior behavior; }; bool PartTest::openDocument(Okular::Part *part, const QString &filePath) { part->openDocument( filePath ); return part->m_document->isOpened(); } // Test that Okular doesn't crash after a successful reload void PartTest::testReload() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")) ); part.reload(); qApp->processEvents(); } // Test that Okular doesn't crash after a canceled reload void PartTest::testCanceledReload() { QVariantList dummyArgs; PartThatHijacksQueryClose part(nullptr, nullptr, dummyArgs); QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")) ); // When queryClose() returns false, the reload operation is canceled (as if // the user had chosen Cancel in the "Save changes?" message box) part.setQueryCloseBehavior(PartThatHijacksQueryClose::ReturnFalse); part.reload(); qApp->processEvents(); } void PartTest::testTOCReload() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/tocreload.pdf")) ); QCOMPARE(part.m_toc->expandedNodes().count(), 0); part.m_toc->m_treeView->expandAll(); QCOMPARE(part.m_toc->expandedNodes().count(), 3); part.reload(); qApp->processEvents(); QCOMPARE(part.m_toc->expandedNodes().count(), 3); } void PartTest::testForwardPDF() { QFETCH(QString, dir); QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); // Create temp dir named like this: ${system temp dir}/${random string}/${dir} const QTemporaryDir tempDir; const QDir workDir(QDir(tempDir.path()).filePath(dir)); workDir.mkpath(QStringLiteral(".")); QFile f(QStringLiteral(KDESRCDIR "data/synctextest.tex")); const QString texDestination = workDir.path() + QStringLiteral("/synctextest.tex"); QVERIFY(f.copy(texDestination)); QProcess process; process.setWorkingDirectory(workDir.path()); const QString pdflatexPath(QStandardPaths::findExecutable(QStringLiteral("pdflatex"))); if (pdflatexPath.isEmpty()) { QFAIL("pdflatex executable not found, but needed for the test. Try installing the respective TeXLive packages."); } process.start(pdflatexPath, QStringList() << QStringLiteral("-synctex=1") << QStringLiteral("-interaction=nonstopmode") << texDestination); bool started = process.waitForStarted(); if (!started) { qDebug() << "start error:" << process.error(); qDebug() << "start stdout:" << process.readAllStandardOutput(); qDebug() << "start stderr:" << process.readAllStandardError(); } QVERIFY(started); process.waitForFinished(); if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { qDebug() << "exit error:" << process.error() << "status" << process.exitStatus() << "code" << process.exitCode(); qDebug() << "exit stdout:" << process.readAllStandardOutput(); qDebug() << "exit stderr:" << process.readAllStandardError(); } const QString pdfResult = workDir.path() + QStringLiteral("/synctextest.pdf"); QVERIFY(QFile::exists(pdfResult)); QVERIFY( openDocument(&part, pdfResult) ); part.m_document->setViewportPage(0); QCOMPARE(part.m_document->currentPage(), 0u); part.closeUrl(); QUrl u(QUrl::fromLocalFile(pdfResult)); u.setFragment(QStringLiteral("src:100") + texDestination); part.openUrl(u); QCOMPARE(part.m_document->currentPage(), 1u); } void PartTest::testForwardPDF_data() { QTest::addColumn("dir"); QTest::newRow("non-utf8") << QStringLiteral("synctextest"); //QStringliteral is broken on windows with non ascii chars so using QString::fromUtf8 QTest::newRow("utf8") << QString::fromUtf8("ßðđđŋßðđŋ"); } void PartTest::testGeneratorPreferences() { KConfigDialog * dialog; QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); // Test that we don't crash while opening the dialog dialog = part.slotGeneratorPreferences(); qApp->processEvents(); delete dialog; // closes the dialog and recursively destroys all widgets // Test that we don't crash while opening a new instance of the dialog // This catches attempts to reuse widgets that have been destroyed dialog = part.slotGeneratorPreferences(); qApp->processEvents(); delete dialog; } void PartTest::testSelectText() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf"))); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const int mouseY = height * 0.052; const int mouseStartX = width * 0.12; const int mouseEndX = width * 0.7; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); QApplication::clipboard()->clear(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection")); QCOMPARE(QApplication::clipboard()->text(), QStringLiteral("Hola que tal\n")); } void PartTest::testClickInternalLink() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf"))); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseNormal"); QCOMPARE(part.m_document->currentPage(), 0u); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.17, height * 0.05)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.17, height * 0.05)); QTRY_COMPARE(part.m_document->currentPage(), 1u); } // cursor switches to Hand when hovering over link in TextSelect mode. void PartTest::testMouseMoveOverLinkWhileInSelectionMode() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); // move mouse over link QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.250, height * 0.127)); // check if mouse icon changed to proper icon QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::PointingHandCursor); } // clicking on hyperlink jumps to destination in TextSelect mode. void PartTest::testClickUrlLinkWhileInSelectionMode() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); // overwrite urlHandler for 'mailto' urls QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "urlHandler"); QSignalSpy openUrlSignalSpy(this, &PartTest::urlHandler); // click on url QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.250, height * 0.127)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.250, height * 0.127)); // expect that the urlHandler signal was called QTRY_COMPARE(openUrlSignalSpy.count(), 1); QList arguments = openUrlSignalSpy.takeFirst(); QCOMPARE(arguments.at(0).value(), QUrl("mailto:foo@foo.bar")); } void PartTest::testeTextSelectionOverAndAcrossLinks_data() { QTest::addColumn("mouseStartX"); QTest::addColumn("mouseEndX"); QTest::addColumn("expectedResult"); // can text-select "over and across" hyperlink. QTest::newRow("start selection before link") << 0.1564 << 0.2943 << QStringLiteral(" a link: foo@foo.b"); // can text-select starting at text and ending selection in middle of hyperlink. QTest::newRow("start selection in the middle of the link") << 0.28 << 0.382 << QStringLiteral("o.bar\n"); // text selection works when selecting left to right or right to left QTest::newRow("start selection after link") << 0.40 << 0.05 << QStringLiteral("This is a link: foo@foo.bar\n"); } // can text-select "over and across" hyperlink. void PartTest::testeTextSelectionOverAndAcrossLinks() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.127; QFETCH(double, mouseStartX); QFETCH(double, mouseEndX); mouseStartX = width * mouseStartX; mouseEndX = width * mouseEndX; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); QApplication::clipboard()->clear(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection")); QFETCH(QString, expectedResult); QCOMPARE(QApplication::clipboard()->text(), expectedResult); } // can jump to link while there's an active selection of text. void PartTest::testClickUrlLinkWhileLinkTextIsSelected() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.127; const double mouseStartX = width * 0.13; const double mouseEndX = width * 0.40; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // overwrite urlHandler for 'mailto' urls QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "urlHandler"); QSignalSpy openUrlSignalSpy(this, &PartTest::urlHandler); // click on url const double mouseClickX = width * 0.2997; const double mouseClickY = height * 0.1293; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // expect that the urlHandler signal was called QTRY_COMPARE(openUrlSignalSpy.count(), 1); QList arguments = openUrlSignalSpy.takeFirst(); QCOMPARE(arguments.at(0).value(), QUrl("mailto:foo@foo.bar")); } // r-click on the selected text gives the "Go To:" content menu option void PartTest::testRClickWhileLinkTextIsSelected() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.162; const double mouseStartX = width * 0.42; const double mouseEndX = width * 0.60; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { // check if popup menu is active and visible QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains go-to link action QAction *goToAction = qobject_cast(menu->findChild(QStringLiteral("GoToAction"))); QVERIFY(goToAction); // check if the "follow this link" action is not visible QAction *processLinkAction = qobject_cast(menu->findChild(QStringLiteral("ProcessLinkAction"))); QVERIFY(!processLinkAction); // check if the "copy link address" action is not visible QAction *copyLinkLocation = qobject_cast(menu->findChild(QStringLiteral("CopyLinkLocationAction"))); QVERIFY(!copyLinkLocation); // close menu to continue test menu->close(); menuClosed = true; }); // click on url const double mouseClickX = width * 0.425; const double mouseClickY = height * 0.162; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // will continue after pop-menu get closed QTRY_VERIFY(menuClosed); } // r-click on the link gives the "follow this link" content menu option void PartTest::testRClickOverLinkWhileLinkTextIsSelected() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.162; const double mouseStartX = width * 0.42; const double mouseEndX = width * 0.60; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { // check if popup menu is active and visible QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains "follow this link" action QAction *processLinkAction = qobject_cast(menu->findChild(QStringLiteral("ProcessLinkAction"))); QVERIFY(processLinkAction); // check if the menu contains "copy link address" action QAction *copyLinkLocation = qobject_cast(menu->findChild(QStringLiteral("CopyLinkLocationAction"))); QVERIFY(copyLinkLocation); // close menu to continue test menu->close(); menuClosed = true; }); // click on url const double mouseClickX = width * 0.593; const double mouseClickY = height * 0.162; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // will continue after pop-menu get closed QTRY_VERIFY(menuClosed); } void PartTest::testRClickOnSelectionModeShoulShowFollowTheLinkMenu() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { // check if popup menu is active and visible QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains "Follow this link" action QAction *processLink = qobject_cast(menu->findChild(QStringLiteral("ProcessLinkAction"))); QVERIFY(processLink); // chek if the menu contains "Copy Link Address" action QAction *actCopyLinkLocation = qobject_cast(menu->findChild(QStringLiteral("CopyLinkLocationAction"))); QVERIFY(actCopyLinkLocation); // close menu to continue test menu->close(); menuClosed = true; }); // r-click on url const double mouseClickX = width * 0.604; const double mouseClickY = height * 0.162; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // will continue after pop-menu get closed QTRY_VERIFY(menuClosed); } void PartTest::testClickAnywhereAfterSelectionShouldUnselect() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.162; const double mouseStartX = width * 0.42; const double mouseEndX = width * 0.60; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // click on url const double mouseClickX = width * 0.10; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(mouseClickX, mouseY), 1000); QApplication::clipboard()->clear(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection")); // check if copied text is empty what means no text selected QVERIFY(QApplication::clipboard()->text().isEmpty()); } void PartTest::testeRectSelectionStartingOnLinks() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // hide info messages as they interfere with selection area Okular::Settings::self()->setShowOSD(false);; part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseSelect")); const double mouseStartY = height * 0.127; const double mouseEndY = height * 0.127; const double mouseStartX = width * 0.28; const double mouseEndX = width * 0.382; // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { QApplication::clipboard()->clear(); // check if popup menu is active and visible QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the copy selected text to clipboard is present QAction *copyAct = qobject_cast(menu->findChild(QStringLiteral("CopyTextToClipboard"))); QVERIFY(copyAct); menu->close(); menuClosed = true; }); simulateMouseSelection(mouseStartX, mouseStartY, mouseEndX, mouseEndY, part.m_pageView->viewport()); // wait menu get closed QTRY_VERIFY(menuClosed); } void PartTest::simulateMouseSelection(double startX, double startY, double endX, double endY, QWidget *target) { const int steps = 5; const double diffX = endX - startX; const double diffY = endY - startY; const double diffXStep = diffX / steps; const double diffYStep = diffY / steps; QTestEventList events; events.addMouseMove(QPoint(startX, startY)); events.addMousePress(Qt::LeftButton, Qt::NoModifier, QPoint(startX, startY)); for (int i = 0; i < steps - 1; ++i) { events.addMouseMove(QPoint(startX + i * diffXStep, startY + i * diffYStep)); events.addDelay(100); } events.addMouseMove(QPoint(endX, endY)); events.addDelay(100); events.addMouseRelease(Qt::LeftButton, Qt::NoModifier, QPoint(endX, endY)); events.simulate(target); } void PartTest::testSaveAsToNonExistingPath() { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( KDESRCDIR "data/file1.pdf" ); QString saveFilePath; { QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); saveFile.open(); saveFilePath = saveFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QVERIFY( !QFileInfo::exists( saveFilePath ) ); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFilePath ), Part::NoSaveAsFlags ) ); QFile::remove( saveFilePath ); } void PartTest::testSaveAsToSymlink() { #ifdef Q_OS_UNIX Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( KDESRCDIR "data/file1.pdf" ); QTemporaryFile newFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); newFile.open(); QString linkFilePath; { QTemporaryFile linkFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); linkFile.open(); linkFilePath = linkFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QFile::link( newFile.fileName(), linkFilePath ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); QVERIFY( part.saveAs( QUrl::fromLocalFile( linkFilePath ), Part::NoSaveAsFlags ) ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); QFile::remove( linkFilePath ); #endif } void PartTest::testSaveIsSymlink() { #ifdef Q_OS_UNIX Okular::Part part(nullptr, nullptr, QVariantList()); QString newFilePath; { QTemporaryFile newFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); newFile.open(); newFilePath = newFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QFile::copy( KDESRCDIR "data/file1.pdf", newFilePath ); QString linkFilePath; { QTemporaryFile linkFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); linkFile.open(); linkFilePath = linkFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QFile::link( newFilePath, linkFilePath ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); part.openDocument( linkFilePath ); QVERIFY( part.saveAs( QUrl::fromLocalFile( linkFilePath ), Part::NoSaveAsFlags ) ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); QFile::remove( newFilePath ); QFile::remove( linkFilePath ); #endif } void PartTest::testSaveAs() { QFETCH(QString, file); QFETCH(QString, extension); QFETCH(bool, nativelySupportsAnnotations); QFETCH(bool, canSwapBackingFile); QScopedPointer closeDialogHelper; QString annotName; QTemporaryFile archiveSave( QStringLiteral( "%1/okrXXXXXX.okular" ).arg( QDir::tempPath() ) ); QTemporaryFile nativeDirectSave( QStringLiteral( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath(), extension ) ); QTemporaryFile nativeFromArchiveFile( QStringLiteral( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath(), extension ) ); QVERIFY( archiveSave.open() ); archiveSave.close(); QVERIFY( nativeDirectSave.open() ); nativeDirectSave.close(); QVERIFY( nativeFromArchiveFile.open() ); nativeFromArchiveFile.close(); qDebug() << "Open file, add annotation and save both natively and to .okular"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( file ); part.m_document->documentInfo(); QCOMPARE(part.m_document->canSwapBackingFile(), canSwapBackingFile); Okular::Annotation *annot = new Okular::TextAnnotation(); annot->setBoundingRectangle( Okular::NormalizedRect( 0.1, 0.1, 0.15, 0.15 ) ); annot->setContents( QStringLiteral("annot contents") ); part.m_document->addPageAnnotation( 0, annot ); annotName = annot->uniqueName(); if ( canSwapBackingFile ) { if ( !nativelySupportsAnnotations ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); // For backends that don't support annotations natively we mark the part as still modified // after a save because we keep the annotation around but it will get lost if the user closes the app // so we want to give her a last chance to save on close with the "you have changes dialog" QCOMPARE( part.isModified(), !nativelySupportsAnnotations ); QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), Part::SaveAsOkularArchive ) ); } else { // We need to save to archive first otherwise we lose the annotation closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::Yes )); // this is the "you're going to lose the undo/redo stack" dialog QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), Part::SaveAsOkularArchive ) ); if ( !nativelySupportsAnnotations ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); } QCOMPARE( part.m_document->documentInfo().get( Okular::DocumentInfo::FilePath ), part.m_document->currentDocument().toDisplayString() ); part.closeUrl(); } qDebug() << "Open the .okular, check that the annotation is present and save to native"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( archiveSave.fileName() ); part.m_document->documentInfo(); QCOMPARE( part.m_document->page( 0 )->annotations().size(), 1 ); QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); if ( !nativelySupportsAnnotations ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeFromArchiveFile.fileName() ), Part::NoSaveAsFlags ) ); if ( canSwapBackingFile && !nativelySupportsAnnotations ) { // For backends that don't support annotations natively we mark the part as still modified // after a save because we keep the annotation around but it will get lost if the user closes the app // so we want to give her a last chance to save on close with the "you have changes dialog" closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "do you want to save or discard" dialog } QCOMPARE( part.m_document->documentInfo().get( Okular::DocumentInfo::FilePath ), part.m_document->currentDocument().toDisplayString() ); part.closeUrl(); } qDebug() << "Open the native file saved directly, and check that the annot" << "is there iff we expect it"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( nativeDirectSave.fileName() ); QCOMPARE( part.m_document->page( 0 )->annotations().size(), nativelySupportsAnnotations ? 1 : 0 ); if ( nativelySupportsAnnotations ) QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); part.closeUrl(); } qDebug() << "Open the native file saved from the .okular, and check that the annot" << "is there iff we expect it"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( nativeFromArchiveFile.fileName() ); QCOMPARE( part.m_document->page( 0 )->annotations().size(), nativelySupportsAnnotations ? 1 : 0 ); if ( nativelySupportsAnnotations ) QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); part.closeUrl(); } } void PartTest::testSaveAs_data() { QTest::addColumn("file"); QTest::addColumn("extension"); QTest::addColumn("nativelySupportsAnnotations"); QTest::addColumn("canSwapBackingFile"); QTest::newRow("pdf") << KDESRCDIR "data/file1.pdf" << "pdf" << true << true; QTest::newRow("pdf.gz") << KDESRCDIR "data/file1.pdf.gz" << "pdf" << true << true; QTest::newRow("epub") << KDESRCDIR "data/contents.epub" << "epub" << false << false; QTest::newRow("jpg") << KDESRCDIR "data/potato.jpg" << "jpg" << false << true; } void PartTest::testSidebarItemAfterSaving() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QWidget *currentSidebarItem = part.m_sidebar->currentItem(); // thumbnails openDocument(&part, QStringLiteral(KDESRCDIR "data/tocreload.pdf")); // since it has TOC it changes to TOC QVERIFY(currentSidebarItem != part.m_sidebar->currentItem()); // now change back to thumbnails part.m_sidebar->setCurrentItem(currentSidebarItem); part.saveAs(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/tocreload.pdf"))); // Check it is still thumbnails after saving QCOMPARE(currentSidebarItem, part.m_sidebar->currentItem()); } void PartTest::testViewModeSavingPerFile() { QVariantList dummyArgs; Okular::Part part( nullptr, nullptr, dummyArgs ); // Open some file QVERIFY( openDocument( &part, QStringLiteral( KDESRCDIR "data/file1.pdf" ) ) ); // Switch to 'continuous' view mode part.m_pageView->setCapability( Okular::View::ViewCapability::Continuous, QVariant( true ) ); // Close document part.closeUrl(); // Open another file QVERIFY( openDocument( &part, QStringLiteral( KDESRCDIR "data/file2.pdf" ) ) ); // Switch to 'non-continuous' mode part.m_pageView->setCapability( Okular::View::ViewCapability::Continuous, QVariant( false ) ); // Close that document, too part.closeUrl(); // Open first document again QVERIFY( openDocument( &part, QStringLiteral( KDESRCDIR "data/file1.pdf" ) ) ); // If per-file view mode saving works, the view mode should be 'continuous' again. QVERIFY( part.m_pageView->capability( Okular::View::ViewCapability::Continuous).toBool() ); } void PartTest::testSaveAsUndoStackAnnotations() { QFETCH(QString, file); QFETCH(QString, extension); QFETCH(bool, nativelySupportsAnnotations); QFETCH(bool, canSwapBackingFile); QFETCH(bool, saveToArchive); const Part::SaveAsFlag saveFlags = saveToArchive ? Part::SaveAsOkularArchive : Part::NoSaveAsFlags; QScopedPointer closeDialogHelper; // closeDialogHelper relies on the availability of the "Continue" button to drop changes // when saving to a file format not supporting those. However, this button is only sensible // and available for "Save As", but not for "Save". By alternately saving to saveFile1 and // saveFile2 we always force "Save As", so closeDialogHelper keeps working. QTemporaryFile saveFile1( QStringLiteral( "%1/okrXXXXXX_1.%2" ).arg( QDir::tempPath(), extension ) ); QVERIFY( saveFile1.open() ); saveFile1.close(); QTemporaryFile saveFile2( QStringLiteral( "%1/okrXXXXXX_2.%2" ).arg( QDir::tempPath(), extension ) ); QVERIFY( saveFile2.open() ); saveFile2.close(); Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( file ); QCOMPARE(part.m_document->canSwapBackingFile(), canSwapBackingFile); Okular::Annotation *annot = new Okular::TextAnnotation(); annot->setBoundingRectangle( Okular::NormalizedRect( 0.1, 0.1, 0.15, 0.15 ) ); annot->setContents( QStringLiteral("annot contents") ); part.m_document->addPageAnnotation( 0, annot ); QString annotName = annot->uniqueName(); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); if (!canSwapBackingFile) { // The undo/redo stack gets lost if you can not swap the backing file QVERIFY( !part.m_document->canUndo() ); QVERIFY( !part.m_document->canRedo() ); return; } // Check we can still undo the annot add after save QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Check we can redo the annot add after save QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( !part.m_document->canRedo() ); if ( nativelySupportsAnnotations ) { // If the annots are provided by the backend we need to refetch the pointer after save annot = part.m_document->page( 0 )->annotation( annotName ); QVERIFY( annot ); } // Remove the annotation, creates another undo command QVERIFY( part.m_document->canRemovePageAnnotation( annot ) ); part.m_document->removePageAnnotation( 0, annot ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Check we can still undo the annot remove after save QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); QCOMPARE( part.m_document->page( 0 )->annotations().count(), 1 ); // Check we can still undo the annot add after save if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Redo the add annotation QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canUndo() ); QVERIFY( part.m_document->canRedo() ); if ( nativelySupportsAnnotations ) { // If the annots are provided by the backend we need to refetch the pointer after save annot = part.m_document->page( 0 )->annotation( annotName ); QVERIFY( annot ); } // Add translate, adjust and modify commands part.m_document->translatePageAnnotation( 0, annot, Okular::NormalizedPoint( 0.1, 0.1 ) ); part.m_document->adjustPageAnnotation( 0, annot, Okular::NormalizedPoint( 0.1, 0.1 ), Okular::NormalizedPoint( 0.1, 0.1 ) ); part.m_document->prepareToModifyAnnotationProperties( annot ); part.m_document->modifyPageAnnotationProperties( 0, annot ); // Now check we can still undo/redo/save at all the intermediate states and things still work if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.m_document->canRedo() ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( !part.m_document->canRedo() ); closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "do you want to save or discard" dialog part.closeUrl(); } void PartTest::testSaveAsUndoStackAnnotations_data() { QTest::addColumn("file"); QTest::addColumn("extension"); QTest::addColumn("nativelySupportsAnnotations"); QTest::addColumn("canSwapBackingFile"); QTest::addColumn("saveToArchive"); QTest::newRow("pdf") << KDESRCDIR "data/file1.pdf" << "pdf" << true << true << false; QTest::newRow("epub") << KDESRCDIR "data/contents.epub" << "epub" << false << false << false; QTest::newRow("jpg") << KDESRCDIR "data/potato.jpg" << "jpg" << false << true << false; QTest::newRow("pdfarchive") << KDESRCDIR "data/file1.pdf" << "okular" << true << true << true; QTest::newRow("jpgarchive") << KDESRCDIR "data/potato.jpg" << "okular" << false << true << true; } void PartTest::testSaveAsUndoStackForms() { QFETCH(QString, file); QFETCH(QString, extension); QFETCH(bool, saveToArchive); const Part::SaveAsFlag saveFlags = saveToArchive ? Part::SaveAsOkularArchive : Part::NoSaveAsFlags; QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath(), extension ) ); QVERIFY( saveFile.open() ); saveFile.close(); Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( file ); - for ( FormField *ff : part.m_document->page( 0 )->formFields() ) + const QLinkedList pageFormFields = part.m_document->page( 0 )->formFields(); + for ( FormField *ff : pageFormFields ) { if ( ff->id() == 65537 ) { QCOMPARE( ff->type(), FormField::FormText ); FormFieldText *fft = static_cast( ff ); part.m_document->editFormText( 0, fft, QStringLiteral("BlaBla"), 6, 0, 0 ); } else if ( ff->id() == 65538 ) { QCOMPARE( ff->type(), FormField::FormButton ); FormFieldButton *ffb = static_cast( ff ); QCOMPARE( ffb->buttonType(), FormFieldButton::Radio ); part.m_document->editFormButtons( 0, QList< FormFieldButton* >() << ffb, QList< bool >() << true ); } else if ( ff->id() == 65542 ) { QCOMPARE( ff->type(), FormField::FormChoice ); FormFieldChoice *ffc = static_cast( ff ); QCOMPARE( ffc->choiceType(), FormFieldChoice::ListBox ); part.m_document->editFormList( 0, ffc, QList< int >() << 1 ); } else if ( ff->id() == 65543 ) { QCOMPARE( ff->type(), FormField::FormChoice ); FormFieldChoice *ffc = static_cast( ff ); QCOMPARE( ffc->choiceType(), FormFieldChoice::ComboBox ); part.m_document->editFormCombo( 0, ffc, QStringLiteral("combo2"), 3, 0, 0); } } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); } void PartTest::testSaveAsUndoStackForms_data() { QTest::addColumn("file"); QTest::addColumn("extension"); QTest::addColumn("saveToArchive"); QTest::newRow("pdf") << KDESRCDIR "data/formSamples.pdf" << "pdf" << false; QTest::newRow("pdfarchive") << KDESRCDIR "data/formSamples.pdf" << "okular" << true; } void PartTest::testOpenUrlArguments() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); KParts::OpenUrlArguments args; args.setMimeType(QStringLiteral("text/rtf")); part.setArguments(args); part.openUrl(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/file1.pdf"))); QCOMPARE( part.arguments().mimeType(), QStringLiteral("text/rtf") ); } void PartTest::test388288() { Okular::Part part(nullptr, nullptr, QVariantList()); part.openUrl(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/file1.pdf"))); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); QMetaObject::invokeMethod(part.m_pageView, "slotToggleAnnotator", Q_ARG( bool, true )); auto annot = new Okular::HighlightAnnotation(); annot->setHighlightType( Okular::HighlightAnnotation::Highlight ); const Okular::NormalizedRect r(0.36, 0.16, 0.51, 0.17); annot->setBoundingRectangle( r ); Okular::HighlightAnnotation::Quad q; q.setCapStart( false ); q.setCapEnd( false ); q.setFeather( 1.0 ); q.setPoint( Okular::NormalizedPoint( r.left, r.bottom ), 0 ); q.setPoint( Okular::NormalizedPoint( r.right, r.bottom ), 1 ); q.setPoint( Okular::NormalizedPoint( r.right, r.top ), 2 ); q.setPoint( Okular::NormalizedPoint( r.left, r.top ), 3 ); annot->highlightQuads().append( q ); part.m_document->addPageAnnotation( 0, annot ); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.5, height * 0.5)); QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::OpenHandCursor); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.4, height * 0.165)); QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::ArrowCursor); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.1, height * 0.165)); part.m_document->undo(); annot = new Okular::HighlightAnnotation(); annot->setHighlightType( Okular::HighlightAnnotation::Highlight ); annot->setBoundingRectangle( r ); annot->highlightQuads().append( q ); part.m_document->addPageAnnotation( 0, annot ); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.5, height * 0.5)); QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::OpenHandCursor); } void PartTest::testCheckBoxReadOnly() { #ifndef HAVE_POPPLER_0_64 return; #endif const QString testFile = QStringLiteral( KDESRCDIR "data/checkbox_ro.pdf" ); Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); // The test document uses the activation action of checkboxes // to update the read only state. For this we need the part so that // undo / redo activates the activation action. QVERIFY( part.m_document->isOpened() ); const Okular::Page* page = part.m_document->page( 0 ); QMap fields; // Field names in test document are: // CBMakeRW, CBMakeRO, TargetDefaultRO, TargetDefaultRW - for ( Okular::FormField *ff: page->formFields() ) + const QLinkedList pageFormFields = page->formFields(); + for ( Okular::FormField *ff: pageFormFields ) { fields.insert( ff->name(), static_cast< Okular::FormField* >( ff ) ); } // First grab all fields and check that the setup is as expected. auto cbMakeRW = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRW" )] ); auto cbMakeRO = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRO" )] ); auto targetDefaultRW = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRw" )] ); auto targetDefaultRO = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRo" )] ); QVERIFY( cbMakeRW ); QVERIFY( cbMakeRO ); QVERIFY( targetDefaultRW ); QVERIFY( targetDefaultRO ); QVERIFY( !cbMakeRW->state() ); QVERIFY( !cbMakeRO->state() ); QVERIFY( !targetDefaultRW->isReadOnly() ); QVERIFY( targetDefaultRO->isReadOnly() ); QList< Okular::FormFieldButton* > btns; btns << cbMakeRW << cbMakeRO; // Now check both boxes QList< bool > btnStates; btnStates << true << true; part.m_document->editFormButtons( 0, btns, btnStates ); // Read only should be inverted QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( !targetDefaultRO->isReadOnly() ); // Test that undo / redo works QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !targetDefaultRW->isReadOnly() ); QVERIFY( targetDefaultRO->isReadOnly() ); part.m_document->redo(); QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( !targetDefaultRO->isReadOnly() ); btnStates.clear(); btnStates << false << true; part.m_document->editFormButtons( 0, btns, btnStates ); QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( targetDefaultRO->isReadOnly() ); // Now set both to checked again and confirm that // save / load works. btnStates.clear(); btnStates << true << true; part.m_document->editFormButtons( 0, btns, btnStates ); QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); QVERIFY( saveFile.open() ); saveFile.close(); // Save QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), Part::NoSaveAsFlags ) ); part.closeUrl(); // Load part.openDocument( saveFile.fileName() ); QVERIFY( part.m_document->isOpened() ); page = part.m_document->page( 0 ); fields.clear(); - for ( Okular::FormField *ff: page->formFields() ) { - fields.insert( ff->name(), static_cast< Okular::FormField* >( ff ) ); + const QLinkedList pageFormFields = page->formFields(); + for ( Okular::FormField *ff: pageFormFields ) + { + fields.insert( ff->name(), static_cast< Okular::FormField* >( ff ) ); + } } cbMakeRW = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRW" )] ); cbMakeRO = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRO" )] ); targetDefaultRW = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRw" )] ); targetDefaultRO = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRo" )] ); QVERIFY( cbMakeRW->state() ); QVERIFY( cbMakeRO->state() ); QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( !targetDefaultRO->isReadOnly() ); } void PartTest::testCrashTextEditDestroy() { const QString testFile = QStringLiteral( KDESRCDIR "data/formSamples.pdf" ); Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.widget()->findChild()->setText(QStringLiteral("HOLA")); part.actionCollection()->action(QStringLiteral("view_toggle_forms"))->trigger(); } void PartTest::testAnnotWindow() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf"))); part.widget()->show(); part.widget()->resize(800, 600); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseNormal"); QCOMPARE(part.m_document->currentPage(), 0u); // Create two distinct text annotations Okular::Annotation * annot1 = new Okular::TextAnnotation(); annot1->setBoundingRectangle( Okular::NormalizedRect( 0.8, 0.1, 0.85, 0.15 ) ); annot1->setContents( QStringLiteral("Annot contents 111111") ); Okular::Annotation *annot2 = new Okular::TextAnnotation(); annot2->setBoundingRectangle( Okular::NormalizedRect( 0.8, 0.3, 0.85, 0.35 ) ); annot2->setContents( QStringLiteral("Annot contents 222222") ); // Add annot1 and annot2 to document part.m_document->addPageAnnotation( 0, annot1 ); part.m_document->addPageAnnotation( 0, annot2 ); QVERIFY( part.m_document->page( 0 )->annotations().size() == 2 ); QTimer *delayResizeEventTimer = part.m_pageView->findChildren("delayResizeEventTimer").at(0); QVERIFY(delayResizeEventTimer->isActive()); QTest::qWait(delayResizeEventTimer->interval() * 2); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // Double click the first annotation to open its window (move mouse for visual feedback) const NormalizedPoint annot1pt = annot1->boundingRectangle().center(); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot1pt.x, height * annot1pt.y)); QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot1pt.x, height * annot1pt.y)); QTRY_COMPARE( part.m_pageView->findChildren("AnnotWindow").size(), 1 ); // Verify that the window is visible QFrame* win1 = part.m_pageView->findChild(QStringLiteral("AnnotWindow")); QVERIFY( !win1->visibleRegion().isEmpty() ); // Double click the second annotation to open its window (move mouse for visual feedback) const NormalizedPoint annot2pt = annot2->boundingRectangle().center(); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot2pt.x, height * annot2pt.y)); QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot2pt.x, height * annot2pt.y)); QTRY_COMPARE( part.m_pageView->findChildren("AnnotWindow").size(), 2 ); // Verify that the first window is hidden covered by the second, which is visible QList lstWin = part.m_pageView->findChildren(QStringLiteral("AnnotWindow")); QFrame * win2; if (lstWin[0] == win1) { win2 = lstWin[1]; } else { win2 = lstWin[0]; } QVERIFY( win1->visibleRegion().isEmpty() ); QVERIFY( !win2->visibleRegion().isEmpty() ); // Double click the first annotation to raise its window (move mouse for visual feedback) QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot1pt.x, height * annot1pt.y)); QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot1pt.x, height * annot1pt.y)); // Verify that the second window is hidden covered by the first, which is visible QVERIFY( !win1->visibleRegion().isEmpty() ); QVERIFY( win2->visibleRegion().isEmpty() ); // Move annotation window 1 to partially show annotation window 2 win1->move(QPoint(win2->pos().x(), win2->pos().y() + 50)); // Verify that both windows are partially visible QVERIFY( !win1->visibleRegion().isEmpty() ); QVERIFY( !win2->visibleRegion().isEmpty() ); // Click the second annotation window to raise it (move mouse for visual feedback) auto widget = win2->window()->childAt(win2->mapTo(win2->window(), QPoint(10, 10))); QTest::mouseMove(win2->window(), win2->mapTo(win2->window(), QPoint(10, 10))); QTest::mouseClick(widget, Qt::LeftButton, Qt::NoModifier, widget->mapFrom(win2, QPoint(10, 10))); QVERIFY( win1->visibleRegion().rectCount() == 3); QVERIFY( win2->visibleRegion().rectCount() == 4); } // Helper for testAdditionalActionTriggers static void verifyTargetStates( const QString & triggerName, const QMap &fields, bool focusVisible, bool cursorVisible, bool mouseVisible, int line) { Okular::FormField *focusTarget = fields.value( triggerName + QStringLiteral ("_focus_target") ); Okular::FormField *cursorTarget = fields.value( triggerName + QStringLiteral ("_cursor_target") ); Okular::FormField *mouseTarget = fields.value( triggerName + QStringLiteral ("_mouse_target") ); QVERIFY( focusTarget ); QVERIFY( cursorTarget ); QVERIFY( mouseTarget ); QTRY_VERIFY2( focusTarget->isVisible() == focusVisible, QStringLiteral ("line: %1 focus for %2 not matched. Expected %3 Actual %4"). arg( line ).arg( triggerName ).arg( focusTarget->isVisible() ).arg( focusVisible ).toUtf8().constData() ); QTRY_VERIFY2( cursorTarget->isVisible() == cursorVisible, QStringLiteral ("line: %1 cursor for %2 not matched. Actual %3 Expected %4"). arg( line ).arg( triggerName ).arg( cursorTarget->isVisible() ).arg( cursorVisible ).toUtf8().constData() ); QTRY_VERIFY2( mouseTarget->isVisible() == mouseVisible, QStringLiteral ("line: %1 mouse for %2 not matched. Expected %3 Actual %4"). arg( line ).arg( triggerName ).arg( mouseTarget->isVisible() ).arg( mouseVisible ).toUtf8().constData() ); } void PartTest::testAdditionalActionTriggers() { #ifndef HAVE_POPPLER_0_65 return; #endif const QString testFile = QStringLiteral( KDESRCDIR "data/additionalFormActions.pdf" ); Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); part.widget()->resize(800, 600); part.widget()->show(); QVERIFY( QTest::qWaitForWindowExposed( part.widget() ) ); QTimer *delayResizeEventTimer = part.m_pageView->findChildren("delayResizeEventTimer").at(0); QVERIFY(delayResizeEventTimer->isActive()); QTest::qWait(delayResizeEventTimer->interval() * 2); part.m_document->setViewportPage( 0 ); // wait for pixmap QTRY_VERIFY( part.m_document->page( 0 )->hasPixmap( part.m_pageView) ); QMap fields; // Field names in test document are: // For trigger fields: tf, cb, rb, dd, pb // For target fields: _focus_target, _cursor_target, // _mouse_target const Okular::Page* page = part.m_document->page( 0 ); - for ( Okular::FormField *ff: page->formFields() ) + const QLinkedList pageFormFields = page->formFields(); + for ( Okular::FormField *ff: pageFormFields ) { fields.insert( ff->name(), static_cast< Okular::FormField* >( ff ) ); } // Verify that everything is set up. verifyTargetStates( QStringLiteral( "tf" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "cb" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "rb" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "dd" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "pb" ), fields, true, true, true, __LINE__ ); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.actionCollection()->action( QStringLiteral( "view_toggle_forms" ) )->trigger(); QPoint tfPos( width * 0.045, height * 0.05 ); QPoint cbPos( width * 0.045, height * 0.08 ); QPoint rbPos( width * 0.045, height * 0.12 ); QPoint ddPos( width * 0.045, height * 0.16 ); QPoint pbPos( width * 0.045, height * 0.26 ); // Test text field auto widget = part.m_pageView->viewport()->childAt( tfPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( tfPos )); verifyTargetStates( QStringLiteral( "tf" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "tf" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "tf" ), fields, false, false, true, __LINE__ ); // Checkbox widget = part.m_pageView->viewport()->childAt( cbPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( cbPos ) ); verifyTargetStates( QStringLiteral( "cb" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "cb" ), fields, false, false, false, __LINE__ ); // Confirm that the textfield no longer has any invisible verifyTargetStates( QStringLiteral( "tf" ), fields, true, true, true, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "cb" ), fields, false, false, true, __LINE__ ); // Radio widget = part.m_pageView->viewport()->childAt( rbPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( rbPos ) ); verifyTargetStates( QStringLiteral( "rb" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "rb" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "rb" ), fields, false, false, true, __LINE__ ); // Dropdown widget = part.m_pageView->viewport()->childAt( ddPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( ddPos ) ); verifyTargetStates( QStringLiteral( "dd" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "dd" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "dd" ), fields, false, false, true, __LINE__ ); // Pushbutton widget = part.m_pageView->viewport()->childAt( pbPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( pbPos ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, true, __LINE__ ); // Confirm that a mouse release outside does not trigger the show action. QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, tfPos ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, false, __LINE__ ); } void PartTest::testTypewriterAnnotTool() { Okular::Part part(nullptr, nullptr, QVariantList()); part.openUrl(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/file1.pdf"))); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage(0); QMetaObject::invokeMethod(part.m_pageView, "slotToggleAnnotator", Q_ARG( bool, true )); // Find the button for the TypeWriter annotation QList toolbuttonList = part.m_pageView->findChildren(); auto it = std::find_if( toolbuttonList.begin(), toolbuttonList.end(), [](const QToolButton * x) { return x->toolTip().contains("Typewriter"); } ); QVERIFY(it != toolbuttonList.end()); QToolButton* typewriterButton = *it; typewriterButton->click(); QTest::qWait(1000); // Wait for the "add new note" dialog to appear TestingUtils::CloseDialogHelper closeDialogHelper( QDialogButtonBox::Ok ); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.5, height * 0.2)); Annotation* annot = part.m_document->page(0)->annotations().first(); TextAnnotation* ta = static_cast( annot ); QVERIFY( annot ); QVERIFY( ta ); QCOMPARE( annot->subType(), Okular::Annotation::AText ); QCOMPARE( annot->style().color(), QColor(255,255,255,0) ); QCOMPARE( ta->textType(), Okular::TextAnnotation::InPlace ); QCOMPARE( ta->inplaceIntent(), Okular::TextAnnotation::TypeWriter ); } void PartTest::testJumpToPage() { const QString testFile = QStringLiteral( KDESRCDIR "data/simple-multipage.pdf" ); const int targetPage = 25; Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); part.widget()->resize(800, 600); part.widget()->show(); QVERIFY( QTest::qWaitForWindowExposed( part.widget() ) ); part.m_document->pages(); part.m_document->setViewportPage( targetPage ); /* Document::setViewportPage triggers pixmap rendering in another thread. * We want to test how things look AFTER finished signal arrives back, * because PageView::slotRelayoutPages may displace the viewport again. */ QTRY_VERIFY( part.m_document->page( targetPage )->hasPixmap( part.m_pageView ) ); const int contentAreaHeight = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); const int pageWithSpaceTop = contentAreaHeight / part.m_document->pages() * targetPage; /* * This is a test for a "known by trial" displacement. * We'd need access to part.m_pageView->d->items[targetPage]->croppedGeometry().top(), * to determine the expected viewport position, but we don't have access. */ QCOMPARE(part.m_pageView->verticalScrollBar()->value(), pageWithSpaceTop - 4); } void PartTest::testForwardBackwardNavigation() { const QString testFile = QStringLiteral( KDESRCDIR "data/simple-multipage.pdf" ); Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); part.widget()->resize(800, 600); part.widget()->show(); QVERIFY( QTest::qWaitForWindowExposed( part.widget() ) ); // Go to some page const int targetPageA = 15; part.m_document->setViewportPage( targetPageA ); QVERIFY( part.m_document->viewport() == targetPageA ); // Go to some other page const int targetPageB = 25; part.m_document->setViewportPage( targetPageB ); QVERIFY( part.m_document->viewport() == targetPageB ); // Go back to page A QVERIFY(QMetaObject::invokeMethod(&part, "slotHistoryBack")); QVERIFY( part.m_document->viewport().pageNumber == targetPageA ); // Go back to page B QVERIFY(QMetaObject::invokeMethod(&part, "slotHistoryNext")); QVERIFY( part.m_document->viewport().pageNumber == targetPageB ); } void PartTest::testTabletProximityBehavior() { QVariantList dummyArgs; Okular::Part part{ nullptr, nullptr, dummyArgs }; QVERIFY( openDocument( &part, QStringLiteral( KDESRCDIR "data/file1.pdf" ) ) ); part.slotShowPresentation(); PresentationWidget *w = part.m_presentationWidget; QVERIFY( w ); part.widget()->show(); // close the KMessageBox "There are two ways of exiting[...]" TestingUtils::CloseDialogHelper closeDialogHelper( w, QDialogButtonBox::Ok ); // confirm the "To leave, press ESC" QTabletEvent enterProximityEvent{ QEvent::TabletEnterProximity, QPoint( 10, 10 ), QPoint( 10, 10 ), QTabletEvent::Stylus, QTabletEvent::Pen, 1., 0, 0, 1., 1., 0, Qt::NoModifier, 0, Qt::NoButton, Qt::NoButton }; QTabletEvent leaveProximityEvent{ QEvent::TabletLeaveProximity, QPoint( 10, 10 ), QPoint( 10, 10 ), QTabletEvent::Stylus, QTabletEvent::Pen, 1., 0, 0, 1., 1., 0, Qt::NoModifier, 0, Qt::NoButton, Qt::NoButton }; // Test with the Okular::Settings::EnumSlidesCursor::Visible setting Okular::Settings::self()->setSlidesCursor( Okular::Settings::EnumSlidesCursor::Visible ); // Send an enterProximity event qApp->notify( qApp, &enterProximityEvent ); // The cursor should be a cross-hair QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::CrossCursor ) ); // Send a leaveProximity event qApp->notify( qApp, &leaveProximityEvent ); // After the leaveProximityEvent, the cursor should be an arrow again, because // we have set the slidesCursor mode to 'Visible' QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::ArrowCursor ) ); // Test with the Okular::Settings::EnumSlidesCursor::Hidden setting Okular::Settings::self()->setSlidesCursor( Okular::Settings::EnumSlidesCursor::Hidden ); qApp->notify( qApp, &enterProximityEvent ); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::CrossCursor ) ); qApp->notify( qApp, &leaveProximityEvent ); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::BlankCursor ) ); // Moving the mouse should not bring the cursor back QTest::mouseMove( w, QPoint( 100, 100 ) ); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::BlankCursor ) ); // First test with the Okular::Settings::EnumSlidesCursor::HiddenDelay setting Okular::Settings::self()->setSlidesCursor( Okular::Settings::EnumSlidesCursor::HiddenDelay ); qApp->notify( qApp, &enterProximityEvent ); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::CrossCursor ) ); qApp->notify( qApp, &leaveProximityEvent ); // After the leaveProximityEvent, the cursor should be blank, because // we have set the slidesCursor mode to 'HiddenDelay' QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::BlankCursor ) ); // Moving the mouse should bring the cursor back QTest::mouseMove(w, QPoint( 150, 150 )); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::ArrowCursor ) ); } void PartTest::testOpenPrintPreview() { QVariantList dummyArgs; Okular::Part part{ nullptr, nullptr, dummyArgs }; QVERIFY( openDocument( &part, QStringLiteral( KDESRCDIR "data/file1.pdf" ) ) ); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); TestingUtils::CloseDialogHelper closeDialogHelper( QDialogButtonBox::Close ); part.slotPrintPreview(); } } // namespace Okular int main(int argc, char *argv[]) { // Force consistent locale QLocale locale(QStringLiteral("en_US.UTF-8")); if (locale == QLocale::c()) { // This is the way to check if the above worked locale = QLocale(QLocale::English, QLocale::UnitedStates); } QLocale::setDefault(locale); qputenv("LC_ALL", "en_US.UTF-8"); // For UNIX, third-party libraries // Ensure consistent configs/caches QTemporaryDir homeDir; // QTemporaryDir automatically cleans up when it goes out of scope Q_ASSERT(homeDir.isValid()); QByteArray homePath = QFile::encodeName(homeDir.path()); qDebug() << homePath; qputenv("USERPROFILE", homePath); qputenv("HOME", homePath); qputenv("XDG_DATA_HOME", homePath + "/.local"); qputenv("XDG_CONFIG_HOME", homePath + "/.kde-unit-test/xdg/config"); // Disable fancy debug output qunsetenv("QT_MESSAGE_PATTERN"); QApplication app( argc, argv ); app.setApplicationName(QStringLiteral("okularparttest")); app.setOrganizationDomain(QStringLiteral("kde.org")); app.setQuitOnLastWindowClosed(false); qRegisterMetaType(); /*as done by kapplication*/ qRegisterMetaType>(); Okular::PartTest test; return QTest::qExec( &test, argc, argv ); } #include "parttest.moc" diff --git a/autotests/visibilitytest.cpp b/autotests/visibilitytest.cpp index c5a38c89b..2b2cb605f 100644 --- a/autotests/visibilitytest.cpp +++ b/autotests/visibilitytest.cpp @@ -1,177 +1,179 @@ /*************************************************************************** * Copyright (C) 2018 by Intevation GmbH * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include #include #include #include #include "../settings_core.h" #include "core/document.h" #include #include #include "../generators/poppler/config-okular-poppler.h" class VisibilityTest: public QObject { Q_OBJECT private slots: void initTestCase(); void cleanupTestCase(); void testJavaScriptVisibility(); void testSaveLoad(); void testActionVisibility(); private: void verifyTargetStates( bool visible ); Okular::Document *m_document; QMap m_fields; }; void VisibilityTest::initTestCase() { Okular::SettingsCore::instance( QStringLiteral("visibilitytest") ); m_document = new Okular::Document( nullptr ); const QString testFile = QStringLiteral( KDESRCDIR "data/visibilitytest.pdf" ); QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( testFile ); QCOMPARE( m_document->openDocument( testFile, QUrl(), mime), Okular::Document::OpenSuccess ); // The test document has four buttons: // HideScriptButton -> Hides targets with JavaScript // ShowScriptButton -> Shows targets with JavaScript // HideActionButton -> Hides targets with HideAction // ShowActionButton -> Shows targets with HideAction // // The target fields are: // TargetButton TargetText TargetCheck TargetDropDown TargetRadio // // With two radio buttons named TargetRadio. const Okular::Page* page = m_document->page( 0 ); - for ( Okular::FormField *ff: page->formFields() ) + const QLinkedList pageFormFields = page->formFields(); + for ( Okular::FormField *ff : pageFormFields ) { m_fields.insert( ff->name(), ff ); } } void VisibilityTest::cleanupTestCase() { m_document->closeDocument(); delete m_document; } void VisibilityTest::verifyTargetStates( bool visible ) { QCOMPARE( m_fields[QStringLiteral( "TargetButton" )]->isVisible(), visible ); QCOMPARE( m_fields[QStringLiteral( "TargetText" )]->isVisible(), visible ); QCOMPARE( m_fields[QStringLiteral( "TargetCheck" )]->isVisible(), visible ); QCOMPARE( m_fields[QStringLiteral( "TargetDropDown" )]->isVisible(), visible ); // Radios do not properly inherit a name from the parent group so // this does not work yet (And would probably need some list handling). // QCOMPARE( m_fields[QStringLiteral( "TargetRadio" )].isVisible(), visible ); } void VisibilityTest::testJavaScriptVisibility() { #ifndef HAVE_POPPLER_0_64 return; #endif auto hideBtn = m_fields[QStringLiteral( "HideScriptButton" )]; auto showBtn = m_fields[QStringLiteral( "ShowScriptButton" )]; // We start with all fields visible verifyTargetStates( true ); m_document->processAction( hideBtn->activationAction() ); // Now all should be hidden verifyTargetStates( false ); // And show again m_document->processAction( showBtn->activationAction() ); verifyTargetStates( true ); } void VisibilityTest::testSaveLoad() { #ifndef HAVE_POPPLER_0_64 return; #endif auto hideBtn = m_fields[QStringLiteral( "HideScriptButton" )]; auto showBtn = m_fields[QStringLiteral( "ShowScriptButton" )]; // We start with all fields visible verifyTargetStates( true ); m_document->processAction( hideBtn->activationAction() ); // Now all should be hidden verifyTargetStates( false ); // Save the changed states QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); QVERIFY( saveFile.open() ); saveFile.close(); QVERIFY( m_document->saveChanges( saveFile.fileName() ) ); auto newDoc = new Okular::Document( nullptr ); QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( saveFile.fileName() ); QCOMPARE( newDoc->openDocument( saveFile.fileName(), QUrl(), mime), Okular::Document::OpenSuccess ); const Okular::Page* page = newDoc->page( 0 ); bool anyChecked = false; // Saveguard against accidental test passing here ;-) - for ( Okular::FormField *ff: page->formFields() ) + const QLinkedList pageFormFields = page->formFields(); + for ( Okular::FormField *ff: pageFormFields ) { if ( ff->name().startsWith( QStringLiteral( "Target" ) ) ) { QVERIFY( !ff->isVisible() ); anyChecked = true; } } QVERIFY(anyChecked); newDoc->closeDocument(); delete newDoc; // Restore the state of the member document m_document->processAction( showBtn->activationAction() ); } void VisibilityTest::testActionVisibility() { #ifndef HAVE_POPPLER_0_64 return; #endif auto hideBtn = m_fields[QStringLiteral( "HideActionButton" )]; auto showBtn = m_fields[QStringLiteral( "ShowActionButton" )]; verifyTargetStates( true ); m_document->processAction( hideBtn->activationAction() ); verifyTargetStates( false ); m_document->processAction( showBtn->activationAction() ); verifyTargetStates( true ); } QTEST_MAIN( VisibilityTest ) #include "visibilitytest.moc" diff --git a/core/document.cpp b/core/document.cpp index 053bc7f47..e88360dd8 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -1,5934 +1,5936 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2017, 2018 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "document.h" #include "document_p.h" #include "documentcommands_p.h" #include #include #ifdef Q_OS_WIN #define _WIN32_WINNT 0x0500 #include #elif defined(Q_OS_FREEBSD) #include #include #include #endif // qt/kde/system includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // local includes #include "action.h" #include "annotations.h" #include "annotations_p.h" #include "audioplayer.h" #include "audioplayer_p.h" #include "bookmarkmanager.h" #include "chooseenginedialog_p.h" #include "debug_p.h" #include "generator_p.h" #include "interfaces/configinterface.h" #include "interfaces/guiinterface.h" #include "interfaces/printinterface.h" #include "interfaces/saveinterface.h" #include "observer.h" #include "misc.h" #include "page.h" #include "page_p.h" #include "pagecontroller_p.h" #include "scripter.h" #include "script/event_p.h" #include "settings_core.h" #include "sourcereference.h" #include "sourcereference_p.h" #include "texteditors_p.h" #include "tile.h" #include "tilesmanager_p.h" #include "utils_p.h" #include "view.h" #include "view_p.h" #include "form.h" #include "utils.h" #include #if HAVE_MALLOC_TRIM #include "malloc.h" #endif using namespace Okular; struct AllocatedPixmap { // owner of the page DocumentObserver *observer; int page; qulonglong memory; // public constructor: initialize data AllocatedPixmap( DocumentObserver *o, int p, qulonglong m ) : observer( o ), page( p ), memory( m ) {} }; struct ArchiveData { ArchiveData() { } QString originalFileName; QTemporaryFile document; QTemporaryFile metadataFile; }; struct RunningSearch { // store search properties int continueOnPage; RegularAreaRect continueOnMatch; QSet< int > highlightedPages; // fields related to previous searches (used for 'continueSearch') QString cachedString; Document::SearchType cachedType; Qt::CaseSensitivity cachedCaseSensitivity; bool cachedViewportMove : 1; bool isCurrentlySearching : 1; QColor cachedColor; int pagesDone; }; #define foreachObserver( cmd ) {\ QSet< DocumentObserver * >::const_iterator it=d->m_observers.constBegin(), end=d->m_observers.constEnd();\ for ( ; it != end ; ++ it ) { (*it)-> cmd ; } } #define foreachObserverD( cmd ) {\ QSet< DocumentObserver * >::const_iterator it = m_observers.constBegin(), end = m_observers.constEnd();\ for ( ; it != end ; ++ it ) { (*it)-> cmd ; } } #define OKULAR_HISTORY_MAXSTEPS 100 #define OKULAR_HISTORY_SAVEDSTEPS 10 /***** Document ******/ QString DocumentPrivate::pagesSizeString() const { if (m_generator) { if (m_generator->pagesSizeMetric() != Generator::None) { QSizeF size = m_parent->allPagesSize(); if (size.isValid()) return localizedSize(size); else return QString(); } else return QString(); } else return QString(); } QString DocumentPrivate::namePaperSize(double inchesWidth, double inchesHeight) const { const QPrinter::Orientation orientation = inchesWidth > inchesHeight ? QPrinter::Landscape : QPrinter::Portrait; const QSize pointsSize(inchesWidth *72.0, inchesHeight*72.0); const QPageSize::PageSizeId paperSize = QPageSize::id(pointsSize, QPageSize::FuzzyOrientationMatch); const QString paperName = QPageSize::name(paperSize); if (orientation == QPrinter::Portrait) { return i18nc("paper type and orientation (eg: Portrait A4)", "Portrait %1", paperName); } else { return i18nc("paper type and orientation (eg: Portrait A4)", "Landscape %1", paperName); } } QString DocumentPrivate::localizedSize(const QSizeF &size) const { double inchesWidth = 0, inchesHeight = 0; switch (m_generator->pagesSizeMetric()) { case Generator::Points: inchesWidth = size.width() / 72.0; inchesHeight = size.height() / 72.0; break; case Generator::Pixels: { const QSizeF dpi = m_generator->dpi(); inchesWidth = size.width() / dpi.width(); inchesHeight = size.height() / dpi.height(); } break; case Generator::None: break; } if (QLocale::system().measurementSystem() == QLocale::ImperialSystem) { return i18nc("%1 is width, %2 is height, %3 is paper size name", "%1 x %2 in (%3)", inchesWidth, inchesHeight, namePaperSize(inchesWidth, inchesHeight)); } else { return i18nc("%1 is width, %2 is height, %3 is paper size name", "%1 x %2 mm (%3)", QString::number(inchesWidth * 25.4, 'd', 0), QString::number(inchesHeight * 25.4, 'd', 0), namePaperSize(inchesWidth, inchesHeight)); } } qulonglong DocumentPrivate::calculateMemoryToFree() { // [MEM] choose memory parameters based on configuration profile qulonglong clipValue = 0; qulonglong memoryToFree = 0; switch ( SettingsCore::memoryLevel() ) { case SettingsCore::EnumMemoryLevel::Low: memoryToFree = m_allocatedPixmapsTotalMemory; break; case SettingsCore::EnumMemoryLevel::Normal: { qulonglong thirdTotalMemory = getTotalMemory() / 3; qulonglong freeMemory = getFreeMemory(); if (m_allocatedPixmapsTotalMemory > thirdTotalMemory) memoryToFree = m_allocatedPixmapsTotalMemory - thirdTotalMemory; if (m_allocatedPixmapsTotalMemory > freeMemory) clipValue = (m_allocatedPixmapsTotalMemory - freeMemory) / 2; } break; case SettingsCore::EnumMemoryLevel::Aggressive: { qulonglong freeMemory = getFreeMemory(); if (m_allocatedPixmapsTotalMemory > freeMemory) clipValue = (m_allocatedPixmapsTotalMemory - freeMemory) / 2; } break; case SettingsCore::EnumMemoryLevel::Greedy: { qulonglong freeSwap; qulonglong freeMemory = getFreeMemory( &freeSwap ); const qulonglong memoryLimit = qMin( qMax( freeMemory, getTotalMemory()/2 ), freeMemory+freeSwap ); if (m_allocatedPixmapsTotalMemory > memoryLimit) clipValue = (m_allocatedPixmapsTotalMemory - memoryLimit) / 2; } break; } if ( clipValue > memoryToFree ) memoryToFree = clipValue; return memoryToFree; } void DocumentPrivate::cleanupPixmapMemory() { cleanupPixmapMemory( calculateMemoryToFree() ); } void DocumentPrivate::cleanupPixmapMemory( qulonglong memoryToFree ) { if ( memoryToFree < 1 ) return; const int currentViewportPage = (*m_viewportIterator).pageNumber; // Create a QMap of visible rects, indexed by page number QMap< int, VisiblePageRect * > visibleRects; QVector< Okular::VisiblePageRect * >::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) visibleRects.insert( (*vIt)->pageNumber, (*vIt) ); // Free memory starting from pages that are farthest from the current one int pagesFreed = 0; while ( memoryToFree > 0 ) { AllocatedPixmap * p = searchLowestPriorityPixmap( true, true ); if ( !p ) // No pixmap to remove break; qCDebug(OkularCoreDebug).nospace() << "Evicting cache pixmap observer=" << p->observer << " page=" << p->page; // m_allocatedPixmapsTotalMemory can't underflow because we always add or remove // the memory used by the AllocatedPixmap so at most it can reach zero m_allocatedPixmapsTotalMemory -= p->memory; // Make sure memoryToFree does not underflow if ( p->memory > memoryToFree ) memoryToFree = 0; else memoryToFree -= p->memory; pagesFreed++; // delete pixmap m_pagesVector.at( p->page )->deletePixmap( p->observer ); // delete allocation descriptor delete p; } // If we're still on low memory, try to free individual tiles // Store pages that weren't completely removed QLinkedList< AllocatedPixmap * > pixmapsToKeep; while (memoryToFree > 0) { int clean_hits = 0; for (DocumentObserver *observer : qAsConst(m_observers)) { AllocatedPixmap * p = searchLowestPriorityPixmap( false, true, observer ); if ( !p ) // No pixmap to remove continue; clean_hits++; TilesManager *tilesManager = m_pagesVector.at( p->page )->d->tilesManager( observer ); if ( tilesManager && tilesManager->totalMemory() > 0 ) { qulonglong memoryDiff = p->memory; NormalizedRect visibleRect; if ( visibleRects.contains( p->page ) ) visibleRect = visibleRects[ p->page ]->rect; // Free non visible tiles tilesManager->cleanupPixmapMemory( memoryToFree, visibleRect, currentViewportPage ); p->memory = tilesManager->totalMemory(); memoryDiff -= p->memory; memoryToFree = (memoryDiff < memoryToFree) ? (memoryToFree - memoryDiff) : 0; m_allocatedPixmapsTotalMemory -= memoryDiff; if ( p->memory > 0 ) pixmapsToKeep.append( p ); else delete p; } else pixmapsToKeep.append( p ); } if (clean_hits == 0) break; } m_allocatedPixmaps += pixmapsToKeep; //p--rintf("freeMemory A:[%d -%d = %d] \n", m_allocatedPixmaps.count() + pagesFreed, pagesFreed, m_allocatedPixmaps.count() ); } /* Returns the next pixmap to evict from cache, or NULL if no suitable pixmap * if found. If unloadableOnly is set, only unloadable pixmaps are returned. If * thenRemoveIt is set, the pixmap is removed from m_allocatedPixmaps before * returning it */ AllocatedPixmap * DocumentPrivate::searchLowestPriorityPixmap( bool unloadableOnly, bool thenRemoveIt, DocumentObserver *observer ) { QLinkedList< AllocatedPixmap * >::iterator pIt = m_allocatedPixmaps.begin(); QLinkedList< AllocatedPixmap * >::iterator pEnd = m_allocatedPixmaps.end(); QLinkedList< AllocatedPixmap * >::iterator farthestPixmap = pEnd; const int currentViewportPage = (*m_viewportIterator).pageNumber; /* Find the pixmap that is farthest from the current viewport */ int maxDistance = -1; while ( pIt != pEnd ) { const AllocatedPixmap * p = *pIt; // Filter by observer if ( observer == nullptr || p->observer == observer ) { const int distance = qAbs( p->page - currentViewportPage ); if ( maxDistance < distance && ( !unloadableOnly || p->observer->canUnloadPixmap( p->page ) ) ) { maxDistance = distance; farthestPixmap = pIt; } } ++pIt; } /* No pixmap to remove */ if ( farthestPixmap == pEnd ) return nullptr; AllocatedPixmap * selectedPixmap = *farthestPixmap; if ( thenRemoveIt ) m_allocatedPixmaps.erase( farthestPixmap ); return selectedPixmap; } qulonglong DocumentPrivate::getTotalMemory() { static qulonglong cachedValue = 0; if ( cachedValue ) return cachedValue; #if defined(Q_OS_LINUX) // if /proc/meminfo doesn't exist, return 128MB QFile memFile( QStringLiteral("/proc/meminfo") ); if ( !memFile.open( QIODevice::ReadOnly ) ) return (cachedValue = 134217728); QTextStream readStream( &memFile ); while ( true ) { QString entry = readStream.readLine(); if ( entry.isNull() ) break; if ( entry.startsWith( QLatin1String("MemTotal:") ) ) return (cachedValue = (Q_UINT64_C(1024) * entry.section( QLatin1Char ( ' ' ), -2, -2 ).toULongLong())); } #elif defined(Q_OS_FREEBSD) qulonglong physmem; int mib[] = {CTL_HW, HW_PHYSMEM}; size_t len = sizeof( physmem ); if ( sysctl( mib, 2, &physmem, &len, NULL, 0 ) == 0 ) return (cachedValue = physmem); #elif defined(Q_OS_WIN) MEMORYSTATUSEX stat; stat.dwLength = sizeof(stat); GlobalMemoryStatusEx (&stat); return ( cachedValue = stat.ullTotalPhys ); #endif return (cachedValue = 134217728); } qulonglong DocumentPrivate::getFreeMemory( qulonglong *freeSwap ) { static QTime lastUpdate = QTime::currentTime().addSecs(-3); static qulonglong cachedValue = 0; static qulonglong cachedFreeSwap = 0; if ( qAbs( lastUpdate.secsTo( QTime::currentTime() ) ) <= 2 ) { if (freeSwap) *freeSwap = cachedFreeSwap; return cachedValue; } /* Initialize the returned free swap value to 0. It is overwritten if the * actual value is available */ if (freeSwap) *freeSwap = 0; #if defined(Q_OS_LINUX) // if /proc/meminfo doesn't exist, return MEMORY FULL QFile memFile( QStringLiteral("/proc/meminfo") ); if ( !memFile.open( QIODevice::ReadOnly ) ) return 0; // read /proc/meminfo and sum up the contents of 'MemFree', 'Buffers' // and 'Cached' fields. consider swapped memory as used memory. qulonglong memoryFree = 0; QString entry; QTextStream readStream( &memFile ); static const int nElems = 5; QString names[nElems] = { QStringLiteral("MemFree:"), QStringLiteral("Buffers:"), QStringLiteral("Cached:"), QStringLiteral("SwapFree:"), QStringLiteral("SwapTotal:") }; qulonglong values[nElems] = { 0, 0, 0, 0, 0 }; bool foundValues[nElems] = { false, false, false, false, false }; while ( true ) { entry = readStream.readLine(); if ( entry.isNull() ) break; for ( int i = 0; i < nElems; ++i ) { if ( entry.startsWith( names[i] ) ) { values[i] = entry.section( QLatin1Char ( ' ' ), -2, -2 ).toULongLong( &foundValues[i] ); } } } memFile.close(); bool found = true; for ( int i = 0; found && i < nElems; ++i ) found = found && foundValues[i]; if ( found ) { /* MemFree + Buffers + Cached - SwapUsed = * = MemFree + Buffers + Cached - (SwapTotal - SwapFree) = * = MemFree + Buffers + Cached + SwapFree - SwapTotal */ memoryFree = values[0] + values[1] + values[2] + values[3]; if ( values[4] > memoryFree ) memoryFree = 0; else memoryFree -= values[4]; } else { return 0; } lastUpdate = QTime::currentTime(); if (freeSwap) *freeSwap = ( cachedFreeSwap = (Q_UINT64_C(1024) * values[3]) ); return ( cachedValue = (Q_UINT64_C(1024) * memoryFree) ); #elif defined(Q_OS_FREEBSD) qulonglong cache, inact, free, psize; size_t cachelen, inactlen, freelen, psizelen; cachelen = sizeof( cache ); inactlen = sizeof( inact ); freelen = sizeof( free ); psizelen = sizeof( psize ); // sum up inactive, cached and free memory if ( sysctlbyname( "vm.stats.vm.v_cache_count", &cache, &cachelen, NULL, 0 ) == 0 && sysctlbyname( "vm.stats.vm.v_inactive_count", &inact, &inactlen, NULL, 0 ) == 0 && sysctlbyname( "vm.stats.vm.v_free_count", &free, &freelen, NULL, 0 ) == 0 && sysctlbyname( "vm.stats.vm.v_page_size", &psize, &psizelen, NULL, 0 ) == 0 ) { lastUpdate = QTime::currentTime(); return (cachedValue = (cache + inact + free) * psize); } else { return 0; } #elif defined(Q_OS_WIN) MEMORYSTATUSEX stat; stat.dwLength = sizeof(stat); GlobalMemoryStatusEx (&stat); lastUpdate = QTime::currentTime(); if (freeSwap) *freeSwap = ( cachedFreeSwap = stat.ullAvailPageFile ); return ( cachedValue = stat.ullAvailPhys ); #else // tell the memory is full.. will act as in LOW profile return 0; #endif } bool DocumentPrivate::loadDocumentInfo( LoadDocumentInfoFlags loadWhat ) // note: load data and stores it internally (document or pages). observers // are still uninitialized at this point so don't access them { //qCDebug(OkularCoreDebug).nospace() << "Using '" << d->m_xmlFileName << "' as document info file."; if ( m_xmlFileName.isEmpty() ) return false; QFile infoFile( m_xmlFileName ); return loadDocumentInfo( infoFile, loadWhat ); } bool DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat ) { if ( !infoFile.exists() || !infoFile.open( QIODevice::ReadOnly ) ) return false; // Load DOM from XML file QDomDocument doc( QStringLiteral("documentInfo") ); if ( !doc.setContent( &infoFile ) ) { qCDebug(OkularCoreDebug) << "Can't load XML pair! Check for broken xml."; infoFile.close(); return false; } infoFile.close(); QDomElement root = doc.documentElement(); if ( root.tagName() != QLatin1String("documentInfo") ) return false; bool loadedAnything = false; // set if something gets actually loaded // Parse the DOM tree QDomNode topLevelNode = root.firstChild(); while ( topLevelNode.isElement() ) { QString catName = topLevelNode.toElement().tagName(); // Restore page attributes (bookmark, annotations, ...) from the DOM if ( catName == QLatin1String("pageList") && ( loadWhat & LoadPageInfo ) ) { QDomNode pageNode = topLevelNode.firstChild(); while ( pageNode.isElement() ) { QDomElement pageElement = pageNode.toElement(); if ( pageElement.hasAttribute( QStringLiteral("number") ) ) { // get page number (node's attribute) bool ok; int pageNumber = pageElement.attribute( QStringLiteral("number") ).toInt( &ok ); // pass the domElement to the right page, to read config data from if ( ok && pageNumber >= 0 && pageNumber < (int)m_pagesVector.count() ) { if ( m_pagesVector[ pageNumber ]->d->restoreLocalContents( pageElement ) ) loadedAnything = true; } } pageNode = pageNode.nextSibling(); } } // Restore 'general info' from the DOM else if ( catName == QLatin1String("generalInfo") && ( loadWhat & LoadGeneralInfo ) ) { QDomNode infoNode = topLevelNode.firstChild(); while ( infoNode.isElement() ) { QDomElement infoElement = infoNode.toElement(); // restore viewports history if ( infoElement.tagName() == QLatin1String("history") ) { // clear history m_viewportHistory.clear(); // append old viewports QDomNode historyNode = infoNode.firstChild(); while ( historyNode.isElement() ) { QDomElement historyElement = historyNode.toElement(); if ( historyElement.hasAttribute( QStringLiteral("viewport") ) ) { QString vpString = historyElement.attribute( QStringLiteral("viewport") ); m_viewportIterator = m_viewportHistory.insert( m_viewportHistory.end(), DocumentViewport( vpString ) ); loadedAnything = true; } historyNode = historyNode.nextSibling(); } // consistency check if ( m_viewportHistory.isEmpty() ) m_viewportIterator = m_viewportHistory.insert( m_viewportHistory.end(), DocumentViewport() ); } else if ( infoElement.tagName() == QLatin1String("rotation") ) { QString str = infoElement.text(); bool ok = true; int newrotation = !str.isEmpty() ? ( str.toInt( &ok ) % 4 ) : 0; if ( ok && newrotation != 0 ) { setRotationInternal( newrotation, false ); loadedAnything = true; } } else if ( infoElement.tagName() == QLatin1String("views") ) { QDomNode viewNode = infoNode.firstChild(); while ( viewNode.isElement() ) { QDomElement viewElement = viewNode.toElement(); if ( viewElement.tagName() == QLatin1String("view") ) { const QString viewName = viewElement.attribute( QStringLiteral("name") ); for ( View *view : qAsConst(m_views) ) { if ( view->name() == viewName ) { loadViewsInfo( view, viewElement ); loadedAnything = true; break; } } } viewNode = viewNode.nextSibling(); } } infoNode = infoNode.nextSibling(); } } topLevelNode = topLevelNode.nextSibling(); } // return loadedAnything; } void DocumentPrivate::loadViewsInfo( View *view, const QDomElement &e ) { QDomNode viewNode = e.firstChild(); while ( viewNode.isElement() ) { QDomElement viewElement = viewNode.toElement(); if ( viewElement.tagName() == QLatin1String("zoom") ) { const QString valueString = viewElement.attribute( QStringLiteral("value") ); bool newzoom_ok = true; const double newzoom = !valueString.isEmpty() ? valueString.toDouble( &newzoom_ok ) : 1.0; if ( newzoom_ok && newzoom != 0 && view->supportsCapability( View::Zoom ) && ( view->capabilityFlags( View::Zoom ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { view->setCapability( View::Zoom, newzoom ); } const QString modeString = viewElement.attribute( QStringLiteral("mode") ); bool newmode_ok = true; const int newmode = !modeString.isEmpty() ? modeString.toInt( &newmode_ok ) : 2; if ( newmode_ok && view->supportsCapability( View::ZoomModality ) && ( view->capabilityFlags( View::ZoomModality ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { view->setCapability( View::ZoomModality, newmode ); } } else if ( viewElement.tagName() == "viewMode" ) { const QString modeString = viewElement.attribute( "mode" ); bool newmode_ok = true; const int newmode = !modeString.isEmpty() ? modeString.toInt( &newmode_ok ) : 2; if ( newmode_ok && view->supportsCapability( View::ViewModeModality ) && ( view->capabilityFlags( View::ViewModeModality ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { view->setCapability( View::ViewModeModality, newmode ); } } else if ( viewElement.tagName() == "continuous" ) { const QString modeString = viewElement.attribute( "mode" ); bool newmode_ok = true; const int newmode = !modeString.isEmpty() ? modeString.toInt( &newmode_ok ) : 2; if ( newmode_ok && view->supportsCapability( View::Continuous ) && ( view->capabilityFlags( View::Continuous ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { view->setCapability( View::Continuous, newmode ); } } else if ( viewElement.tagName() == "trimMargins" ) { const QString valueString = viewElement.attribute( "value" ); bool newmode_ok = true; const int newmode = !valueString.isEmpty() ? valueString.toInt( &newmode_ok ) : 2; if ( newmode_ok && view->supportsCapability( View::TrimMargins ) && ( view->capabilityFlags( View::TrimMargins ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { view->setCapability( View::TrimMargins, newmode ); } } viewNode = viewNode.nextSibling(); } } void DocumentPrivate::saveViewsInfo( View *view, QDomElement &e ) const { if ( view->supportsCapability( View::Zoom ) && ( view->capabilityFlags( View::Zoom ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) && view->supportsCapability( View::ZoomModality ) && ( view->capabilityFlags( View::ZoomModality ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { QDomElement zoomEl = e.ownerDocument().createElement( QStringLiteral("zoom") ); e.appendChild( zoomEl ); bool ok = true; const double zoom = view->capability( View::Zoom ).toDouble( &ok ); if ( ok && zoom != 0 ) { zoomEl.setAttribute( QStringLiteral("value"), QString::number(zoom) ); } const int mode = view->capability( View::ZoomModality ).toInt( &ok ); if ( ok ) { zoomEl.setAttribute( QStringLiteral("mode"), mode ); } } if ( view->supportsCapability( View::Continuous ) && ( view->capabilityFlags( View::Continuous ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { QDomElement contEl = e.ownerDocument().createElement( "continuous" ); e.appendChild( contEl ); const bool mode = view->capability( View::Continuous ).toBool(); contEl.setAttribute( "mode", mode ); } if ( view->supportsCapability( View::ViewModeModality ) && ( view->capabilityFlags( View::ViewModeModality ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { QDomElement viewEl = e.ownerDocument().createElement( "viewMode" ); e.appendChild( viewEl ); bool ok = true; const int mode = view->capability( View::ViewModeModality ).toInt( &ok ); if ( ok ) { viewEl.setAttribute( "mode", mode ); } } if ( view->supportsCapability( View::TrimMargins ) && ( view->capabilityFlags( View::TrimMargins ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { QDomElement contEl = e.ownerDocument().createElement( "trimMargins" ); e.appendChild( contEl ); const bool value = view->capability( View::TrimMargins ).toBool(); contEl.setAttribute( "value", value ); } } QUrl DocumentPrivate::giveAbsoluteUrl( const QString & fileName ) const { if ( !QDir::isRelativePath( fileName ) ) return QUrl::fromLocalFile(fileName); if ( !m_url.isValid() ) return QUrl(); return QUrl(KIO::upUrl(m_url).toString() + fileName); } bool DocumentPrivate::openRelativeFile( const QString & fileName ) { QUrl url = giveAbsoluteUrl( fileName ); if ( url.isEmpty() ) return false; qCDebug(OkularCoreDebug).nospace() << "openRelativeFile: '" << url << "'"; emit m_parent->openUrl( url ); return true; } Generator * DocumentPrivate::loadGeneratorLibrary( const KPluginMetaData &service ) { KPluginLoader loader( service.fileName() ); qCDebug(OkularCoreDebug) << service.fileName(); KPluginFactory *factory = loader.factory(); if ( !factory ) { qCWarning(OkularCoreDebug).nospace() << "Invalid plugin factory for " << service.fileName() << ":" << loader.errorString(); return nullptr; } Generator * plugin = factory->create(); GeneratorInfo info( plugin, service ); m_loadedGenerators.insert( service.pluginId(), info ); return plugin; } void DocumentPrivate::loadAllGeneratorLibraries() { if ( m_generatorsLoaded ) return; loadServiceList( availableGenerators() ); m_generatorsLoaded = true; } void DocumentPrivate::loadServiceList( const QVector& offers ) { int count = offers.count(); if ( count <= 0 ) return; for ( int i = 0; i < count; ++i ) { QString id = offers.at(i).pluginId(); // don't load already loaded generators QHash< QString, GeneratorInfo >::const_iterator genIt = m_loadedGenerators.constFind( id ); if ( !m_loadedGenerators.isEmpty() && genIt != m_loadedGenerators.constEnd() ) continue; Generator * g = loadGeneratorLibrary( offers.at(i) ); (void)g; } } void DocumentPrivate::unloadGenerator( const GeneratorInfo& info ) { delete info.generator; } void DocumentPrivate::cacheExportFormats() { if ( m_exportCached ) return; const ExportFormat::List formats = m_generator->exportFormats(); for ( int i = 0; i < formats.count(); ++i ) { if ( formats.at( i ).mimeType().name() == QLatin1String( "text/plain" ) ) m_exportToText = formats.at( i ); else m_exportFormats.append( formats.at( i ) ); } m_exportCached = true; } ConfigInterface* DocumentPrivate::generatorConfig( GeneratorInfo& info ) { if ( info.configChecked ) return info.config; info.config = qobject_cast< Okular::ConfigInterface * >( info.generator ); info.configChecked = true; return info.config; } SaveInterface* DocumentPrivate::generatorSave( GeneratorInfo& info ) { if ( info.saveChecked ) return info.save; info.save = qobject_cast< Okular::SaveInterface * >( info.generator ); info.saveChecked = true; return info.save; } Document::OpenResult DocumentPrivate::openDocumentInternal( const KPluginMetaData& offer, bool isstdin, const QString& docFile, const QByteArray& filedata, const QString& password ) { QString propName = offer.pluginId(); QHash< QString, GeneratorInfo >::const_iterator genIt = m_loadedGenerators.constFind( propName ); m_walletGenerator = nullptr; if ( genIt != m_loadedGenerators.constEnd() ) { m_generator = genIt.value().generator; } else { m_generator = loadGeneratorLibrary( offer ); if ( !m_generator ) return Document::OpenError; genIt = m_loadedGenerators.constFind( propName ); Q_ASSERT( genIt != m_loadedGenerators.constEnd() ); } Q_ASSERT_X( m_generator, "Document::load()", "null generator?!" ); m_generator->d_func()->m_document = this; // connect error reporting signals m_openError.clear(); QMetaObject::Connection errorToOpenErrorConnection = QObject::connect( m_generator, &Generator::error, m_parent, [this](const QString &message) { m_openError = message; } ); QObject::connect( m_generator, &Generator::warning, m_parent, &Document::warning ); QObject::connect( m_generator, &Generator::notice, m_parent, &Document::notice ); QApplication::setOverrideCursor( Qt::WaitCursor ); const QSizeF dpi = Utils::realDpi(m_widget); qCDebug(OkularCoreDebug) << "Output DPI:" << dpi; m_generator->setDPI(dpi); Document::OpenResult openResult = Document::OpenError; if ( !isstdin ) { openResult = m_generator->loadDocumentWithPassword( docFile, m_pagesVector, password ); } else if ( !filedata.isEmpty() ) { if ( m_generator->hasFeature( Generator::ReadRawData ) ) { openResult = m_generator->loadDocumentFromDataWithPassword( filedata, m_pagesVector, password ); } else { m_tempFile = new QTemporaryFile(); if ( !m_tempFile->open() ) { delete m_tempFile; m_tempFile = nullptr; } else { m_tempFile->write( filedata ); QString tmpFileName = m_tempFile->fileName(); m_tempFile->close(); openResult = m_generator->loadDocumentWithPassword( tmpFileName, m_pagesVector, password ); } } } QApplication::restoreOverrideCursor(); if ( openResult != Document::OpenSuccess || m_pagesVector.size() <= 0 ) { m_generator->d_func()->m_document = nullptr; QObject::disconnect( m_generator, nullptr, m_parent, nullptr ); // TODO this is a bit of a hack, since basically means that // you can only call walletDataForFile after calling openDocument // but since in reality it's what happens I've decided not to refactor/break API // One solution is just kill walletDataForFile and make OpenResult be an object // where the wallet data is also returned when OpenNeedsPassword m_walletGenerator = m_generator; m_generator = nullptr; qDeleteAll( m_pagesVector ); m_pagesVector.clear(); delete m_tempFile; m_tempFile = nullptr; // TODO: emit a message telling the document is empty if ( openResult == Document::OpenSuccess ) openResult = Document::OpenError; } else { /* * Now that the documen is opened, the tab (if using tabs) is visible, which mean that * we can now connect the error reporting signal directly to the parent */ QObject::disconnect(errorToOpenErrorConnection); QObject::connect( m_generator, &Generator::error, m_parent, &Document::error ); } return openResult; } bool DocumentPrivate::savePageDocumentInfo( QTemporaryFile *infoFile, int what ) const { if ( infoFile->open() ) { // 1. Create DOM QDomDocument doc( QStringLiteral("documentInfo") ); QDomProcessingInstruction xmlPi = doc.createProcessingInstruction( QStringLiteral( "xml" ), QStringLiteral( "version=\"1.0\" encoding=\"utf-8\"" ) ); doc.appendChild( xmlPi ); QDomElement root = doc.createElement( QStringLiteral("documentInfo") ); doc.appendChild( root ); // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM QDomElement pageList = doc.createElement( QStringLiteral("pageList") ); root.appendChild( pageList ); // .... save pages that hold data QVector< Page * >::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->saveLocalContents( pageList, doc, PageItems( what ) ); // 3. Save DOM to XML file QString xml = doc.toString(); QTextStream os( infoFile ); os.setCodec( "UTF-8" ); os << xml; return true; } return false; } DocumentViewport DocumentPrivate::nextDocumentViewport() const { DocumentViewport ret = m_nextDocumentViewport; if ( !m_nextDocumentDestination.isEmpty() && m_generator ) { DocumentViewport vp( m_parent->metaData( QStringLiteral("NamedViewport"), m_nextDocumentDestination ).toString() ); if ( vp.isValid() ) { ret = vp; } } return ret; } void DocumentPrivate::performAddPageAnnotation( int page, Annotation * annotation ) { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr; // find out the page to attach annotation Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; // the annotation belongs already to a page if ( annotation->d_ptr->m_page ) return; // add annotation to the page kp->addAnnotation( annotation ); // tell the annotation proxy if ( proxy && proxy->supports(AnnotationProxy::Addition) ) proxy->notifyAddition( annotation, page ); // notify observers about the change notifyAnnotationChanges( page ); if ( annotation->flags() & Annotation::ExternallyDrawn ) { // Redraw everything, including ExternallyDrawn annotations refreshPixmaps( page ); } } void DocumentPrivate::performRemovePageAnnotation( int page, Annotation * annotation ) { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr; bool isExternallyDrawn; // find out the page Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; if ( annotation->flags() & Annotation::ExternallyDrawn ) isExternallyDrawn = true; else isExternallyDrawn = false; // try to remove the annotation if ( m_parent->canRemovePageAnnotation( annotation ) ) { // tell the annotation proxy if ( proxy && proxy->supports(AnnotationProxy::Removal) ) proxy->notifyRemoval( annotation, page ); kp->removeAnnotation( annotation ); // Also destroys the object // in case of success, notify observers about the change notifyAnnotationChanges( page ); if ( isExternallyDrawn ) { // Redraw everything, including ExternallyDrawn annotations refreshPixmaps( page ); } } } void DocumentPrivate::performModifyPageAnnotation( int page, Annotation * annotation, bool appearanceChanged ) { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr; // find out the page Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; // tell the annotation proxy if ( proxy && proxy->supports(AnnotationProxy::Modification) ) { proxy->notifyModification( annotation, page, appearanceChanged ); } // notify observers about the change notifyAnnotationChanges( page ); if ( appearanceChanged && (annotation->flags() & Annotation::ExternallyDrawn) ) { /* When an annotation is being moved, the generator will not render it. * Therefore there's no need to refresh pixmaps after the first time */ if ( annotation->flags() & (Annotation::BeingMoved | Annotation::BeingResized) ) { if ( m_annotationBeingModified ) return; else // First time: take note m_annotationBeingModified = true; } else { m_annotationBeingModified = false; } // Redraw everything, including ExternallyDrawn annotations qCDebug(OkularCoreDebug) << "Refreshing Pixmaps"; refreshPixmaps( page ); } } void DocumentPrivate::performSetAnnotationContents( const QString & newContents, Annotation *annot, int pageNumber ) { bool appearanceChanged = false; // Check if appearanceChanged should be true switch ( annot->subType() ) { // If it's an in-place TextAnnotation, set the inplace text case Okular::Annotation::AText: { Okular::TextAnnotation * txtann = static_cast< Okular::TextAnnotation * >( annot ); if ( txtann->textType() == Okular::TextAnnotation::InPlace ) { appearanceChanged = true; } break; } // If it's a LineAnnotation, check if caption text is visible case Okular::Annotation::ALine: { Okular::LineAnnotation * lineann = static_cast< Okular::LineAnnotation * >( annot ); if ( lineann->showCaption() ) appearanceChanged = true; break; } default: break; } // Set contents annot->setContents( newContents ); // Tell the document the annotation has been modified performModifyPageAnnotation( pageNumber, annot, appearanceChanged ); } void DocumentPrivate::recalculateForms() { const QVariant fco = m_parent->metaData(QStringLiteral("FormCalculateOrder")); const QVector formCalculateOrder = fco.value>(); foreach(int formId, formCalculateOrder) { for ( uint pageIdx = 0; pageIdx < m_parent->pages(); pageIdx++ ) { const Page *p = m_parent->page( pageIdx ); if (p) { bool pageNeedsRefresh = false; foreach( FormField *form, p->formFields() ) { if ( form->id() == formId ) { Action *action = form->additionalAction( FormField::CalculateField ); if (action) { FormFieldText *fft = dynamic_cast< FormFieldText * >( form ); std::shared_ptr event; QString oldVal; if ( fft ) { // Prepare text calculate event event = Event::createFormCalculateEvent( fft, m_pagesVector[pageIdx] ); if ( !m_scripter ) m_scripter = new Scripter( this ); m_scripter->setEvent( event.get() ); // The value maybe changed in javascript so save it first. oldVal = fft->text(); } m_parent->processAction( action ); if ( event && fft ) { // Update text field from calculate m_scripter->setEvent( nullptr ); const QString newVal = event->value().toString(); if ( newVal != oldVal ) { fft->setText( newVal ); if ( const Okular::Action *action = fft->additionalAction( Okular::FormField::FormatField ) ) { // The format action handles the refresh. m_parent->processFormatAction( action, fft ); } else { emit m_parent->refreshFormWidget( fft ); pageNeedsRefresh = true; } } } } else { qWarning() << "Form that is part of calculate order doesn't have a calculate action"; } } } if ( pageNeedsRefresh ) { refreshPixmaps( p->number() ); } } } } } void DocumentPrivate::saveDocumentInfo() const { if ( m_xmlFileName.isEmpty() ) return; QFile infoFile( m_xmlFileName ); qCDebug(OkularCoreDebug) << "About to save document info to" << m_xmlFileName; if (!infoFile.open( QIODevice::WriteOnly | QIODevice::Truncate)) { qCWarning(OkularCoreDebug) << "Failed to open docdata file" << m_xmlFileName; return; } // 1. Create DOM QDomDocument doc( QStringLiteral("documentInfo") ); QDomProcessingInstruction xmlPi = doc.createProcessingInstruction( QStringLiteral( "xml" ), QStringLiteral( "version=\"1.0\" encoding=\"utf-8\"" ) ); doc.appendChild( xmlPi ); QDomElement root = doc.createElement( QStringLiteral("documentInfo") ); root.setAttribute( QStringLiteral("url"), m_url.toDisplayString(QUrl::PreferLocalFile) ); doc.appendChild( root ); // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM // -> do this if there are not-yet-migrated annots or forms in docdata/ if ( m_docdataMigrationNeeded ) { QDomElement pageList = doc.createElement( QStringLiteral("pageList") ); root.appendChild( pageList ); // OriginalAnnotationPageItems and OriginalFormFieldPageItems tell to // store the same unmodified annotation list and form contents that we // read when we opened the file and ignore any change made by the user. // Since we don't store annotations and forms in docdata/ any more, this is // necessary to preserve annotations/forms that previous Okular version // had stored there. const PageItems saveWhat = AllPageItems | OriginalAnnotationPageItems | OriginalFormFieldPageItems; // .... save pages that hold data QVector< Page * >::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->saveLocalContents( pageList, doc, saveWhat ); } // 2.2. Save document info (current viewport, history, ... ) to DOM QDomElement generalInfo = doc.createElement( QStringLiteral("generalInfo") ); root.appendChild( generalInfo ); // create rotation node if ( m_rotation != Rotation0 ) { QDomElement rotationNode = doc.createElement( QStringLiteral("rotation") ); generalInfo.appendChild( rotationNode ); rotationNode.appendChild( doc.createTextNode( QString::number( (int)m_rotation ) ) ); } // ... save history up to OKULAR_HISTORY_SAVEDSTEPS viewports QLinkedList< DocumentViewport >::const_iterator backIterator = m_viewportIterator; if ( backIterator != m_viewportHistory.constEnd() ) { // go back up to OKULAR_HISTORY_SAVEDSTEPS steps from the current viewportIterator int backSteps = OKULAR_HISTORY_SAVEDSTEPS; while ( backSteps-- && backIterator != m_viewportHistory.constBegin() ) --backIterator; // create history root node QDomElement historyNode = doc.createElement( QStringLiteral("history") ); generalInfo.appendChild( historyNode ); // add old[backIterator] and present[viewportIterator] items QLinkedList< DocumentViewport >::const_iterator endIt = m_viewportIterator; ++endIt; while ( backIterator != endIt ) { QString name = (backIterator == m_viewportIterator) ? QStringLiteral ("current") : QStringLiteral ("oldPage"); QDomElement historyEntry = doc.createElement( name ); historyEntry.setAttribute( QStringLiteral("viewport"), (*backIterator).toString() ); historyNode.appendChild( historyEntry ); ++backIterator; } } // create views root node QDomElement viewsNode = doc.createElement( QStringLiteral("views") ); generalInfo.appendChild( viewsNode ); for ( View *view : qAsConst(m_views) ) { QDomElement viewEntry = doc.createElement( QStringLiteral("view") ); viewEntry.setAttribute( QStringLiteral("name"), view->name() ); viewsNode.appendChild( viewEntry ); saveViewsInfo( view, viewEntry ); } // 3. Save DOM to XML file QString xml = doc.toString(); QTextStream os( &infoFile ); os.setCodec( "UTF-8" ); os << xml; infoFile.close(); } void DocumentPrivate::slotTimedMemoryCheck() { // [MEM] clean memory (for 'free mem dependent' profiles only) if ( SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Low && m_allocatedPixmapsTotalMemory > 1024*1024 ) cleanupPixmapMemory(); } void DocumentPrivate::sendGeneratorPixmapRequest() { /* If the pixmap cache will have to be cleaned in order to make room for the * next request, get the distance from the current viewport of the page * whose pixmap will be removed. We will ignore preload requests for pages * that are at the same distance or farther */ const qulonglong memoryToFree = calculateMemoryToFree(); const int currentViewportPage = (*m_viewportIterator).pageNumber; int maxDistance = INT_MAX; // Default: No maximum if ( memoryToFree ) { AllocatedPixmap *pixmapToReplace = searchLowestPriorityPixmap( true ); if ( pixmapToReplace ) maxDistance = qAbs( pixmapToReplace->page - currentViewportPage ); } // find a request PixmapRequest * request = nullptr; m_pixmapRequestsMutex.lock(); while ( !m_pixmapRequestsStack.isEmpty() && !request ) { PixmapRequest * r = m_pixmapRequestsStack.last(); if (!r) { m_pixmapRequestsStack.pop_back(); continue; } QRect requestRect = r->isTile() ? r->normalizedRect().geometry( r->width(), r->height() ) : QRect( 0, 0, r->width(), r->height() ); TilesManager *tilesManager = r->d->tilesManager(); const double normalizedArea = r->normalizedRect().width() * r->normalizedRect().height(); // If it's a preload but the generator is not threaded no point in trying to preload if ( r->preload() && !m_generator->hasFeature( Generator::Threaded ) ) { m_pixmapRequestsStack.pop_back(); delete r; } // request only if page isn't already present and request has valid id else if ( ( !r->d->mForce && r->page()->hasPixmap( r->observer(), r->width(), r->height(), r->normalizedRect() ) ) || !m_observers.contains(r->observer()) ) { m_pixmapRequestsStack.pop_back(); delete r; } else if ( !r->d->mForce && r->preload() && qAbs( r->pageNumber() - currentViewportPage ) >= maxDistance ) { m_pixmapRequestsStack.pop_back(); //qCDebug(OkularCoreDebug) << "Ignoring request that doesn't fit in cache"; delete r; } // Ignore requests for pixmaps that are already being generated else if ( tilesManager && tilesManager->isRequesting( r->normalizedRect(), r->width(), r->height() ) ) { m_pixmapRequestsStack.pop_back(); delete r; } // If the requested area is above 8000000 pixels, and we're not rendering most of the page, switch on the tile manager else if ( !tilesManager && m_generator->hasFeature( Generator::TiledRendering ) && (long)r->width() * (long)r->height() > 8000000L && normalizedArea < 0.75 && normalizedArea != 0 ) { // if the image is too big. start using tiles qCDebug(OkularCoreDebug).nospace() << "Start using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);"; // fill the tiles manager with the last rendered pixmap const QPixmap *pixmap = r->page()->_o_nearestPixmap( r->observer(), r->width(), r->height() ); if ( pixmap ) { tilesManager = new TilesManager( r->pageNumber(), pixmap->width(), pixmap->height(), r->page()->rotation() ); tilesManager->setPixmap( pixmap, NormalizedRect( 0, 0, 1, 1 ), true /*isPartialPixmap*/ ); tilesManager->setSize( r->width(), r->height() ); } else { // create new tiles manager tilesManager = new TilesManager( r->pageNumber(), r->width(), r->height(), r->page()->rotation() ); } tilesManager->setRequest( r->normalizedRect(), r->width(), r->height() ); r->page()->deletePixmap( r->observer() ); r->page()->d->setTilesManager( r->observer(), tilesManager ); r->setTile( true ); // Change normalizedRect to the smallest rect that contains all // visible tiles. if ( !r->normalizedRect().isNull() ) { NormalizedRect tilesRect; const QList tiles = tilesManager->tilesAt( r->normalizedRect(), TilesManager::TerminalTile ); QList::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd(); while ( tIt != tEnd ) { Tile tile = *tIt; if ( tilesRect.isNull() ) tilesRect = tile.rect(); else tilesRect |= tile.rect(); ++tIt; } r->setNormalizedRect( tilesRect ); request = r; } else { // Discard request if normalizedRect is null. This happens in // preload requests issued by PageView if the requested page is // not visible and the user has just switched from a non-tiled // zoom level to a tiled one m_pixmapRequestsStack.pop_back(); delete r; } } // If the requested area is below 6000000 pixels, switch off the tile manager else if ( tilesManager && (long)r->width() * (long)r->height() < 6000000L ) { qCDebug(OkularCoreDebug).nospace() << "Stop using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);"; // page is too small. stop using tiles. r->page()->deletePixmap( r->observer() ); r->setTile( false ); request = r; } else if ( (long)requestRect.width() * (long)requestRect.height() > 200000000L && (SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Greedy ) ) { m_pixmapRequestsStack.pop_back(); if ( !m_warnedOutOfMemory ) { qCWarning(OkularCoreDebug).nospace() << "Running out of memory on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);"; qCWarning(OkularCoreDebug) << "this message will be reported only once."; m_warnedOutOfMemory = true; } delete r; } else { request = r; } } // if no request found (or already generated), return if ( !request ) { m_pixmapRequestsMutex.unlock(); return; } // [MEM] preventive memory freeing qulonglong pixmapBytes = 0; TilesManager * tm = request->d->tilesManager(); if ( tm ) pixmapBytes = tm->totalMemory(); else pixmapBytes = 4 * request->width() * request->height(); if ( pixmapBytes > (1024 * 1024) ) cleanupPixmapMemory( memoryToFree /* previously calculated value */ ); // submit the request to the generator if ( m_generator->canGeneratePixmap() ) { QRect requestRect = !request->isTile() ? QRect(0, 0, request->width(), request->height() ) : request->normalizedRect().geometry( request->width(), request->height() ); qCDebug(OkularCoreDebug).nospace() << "sending request observer=" << request->observer() << " " <pageNumber() << " async == " << request->asynchronous() << " isTile == " << request->isTile(); m_pixmapRequestsStack.removeAll ( request ); if ( tm ) tm->setRequest( request->normalizedRect(), request->width(), request->height() ); if ( (int)m_rotation % 2 ) request->d->swap(); if ( m_rotation != Rotation0 && !request->normalizedRect().isNull() ) request->setNormalizedRect( TilesManager::fromRotatedRect( request->normalizedRect(), m_rotation ) ); // If set elsewhere we already know we want it to be partial if ( !request->partialUpdatesWanted() ) { request->setPartialUpdatesWanted( request->asynchronous() && !request->page()->hasPixmap( request->observer() ) ); } // we always have to unlock _before_ the generatePixmap() because // a sync generation would end with requestDone() -> deadlock, and // we can not really know if the generator can do async requests m_executingPixmapRequests.push_back( request ); m_pixmapRequestsMutex.unlock(); m_generator->generatePixmap( request ); } else { m_pixmapRequestsMutex.unlock(); // pino (7/4/2006): set the polling interval from 10 to 30 QTimer::singleShot( 30, m_parent, [this] { sendGeneratorPixmapRequest(); } ); } } void DocumentPrivate::rotationFinished( int page, Okular::Page *okularPage ) { Okular::Page *wantedPage = m_pagesVector.value( page, 0 ); if ( !wantedPage || wantedPage != okularPage ) return; foreach(DocumentObserver *o, m_observers) o->notifyPageChanged( page, DocumentObserver::Pixmap | DocumentObserver::Annotations ); } void DocumentPrivate::slotFontReadingProgress( int page ) { emit m_parent->fontReadingProgress( page ); if ( page >= (int)m_parent->pages() - 1 ) { emit m_parent->fontReadingEnded(); m_fontThread = nullptr; m_fontsCached = true; } } void DocumentPrivate::fontReadingGotFont( const Okular::FontInfo& font ) { // Try to avoid duplicate fonts if (m_fontsCache.indexOf(font) == -1) { m_fontsCache.append( font ); emit m_parent->gotFont( font ); } } void DocumentPrivate::slotGeneratorConfigChanged() { if ( !m_generator ) return; // reparse generator config and if something changed clear Pages bool configchanged = false; QHash< QString, GeneratorInfo >::iterator it = m_loadedGenerators.begin(), itEnd = m_loadedGenerators.end(); for ( ; it != itEnd; ++it ) { Okular::ConfigInterface * iface = generatorConfig( it.value() ); if ( iface ) { bool it_changed = iface->reparseConfig(); if ( it_changed && ( m_generator == it.value().generator ) ) configchanged = true; } } if ( configchanged ) { // invalidate pixmaps QVector::const_iterator it = m_pagesVector.constBegin(), end = m_pagesVector.constEnd(); for ( ; it != end; ++it ) { (*it)->deletePixmaps(); } // [MEM] remove allocation descriptors qDeleteAll( m_allocatedPixmaps ); m_allocatedPixmaps.clear(); m_allocatedPixmapsTotalMemory = 0; // send reload signals to observers foreachObserverD( notifyContentsCleared( DocumentObserver::Pixmap ) ); } // free memory if in 'low' profile if ( SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !m_allocatedPixmaps.isEmpty() && !m_pagesVector.isEmpty() ) cleanupPixmapMemory(); } void DocumentPrivate::refreshPixmaps( int pageNumber ) { Page* page = m_pagesVector.value( pageNumber, 0 ); if ( !page ) return; QMap< DocumentObserver*, PagePrivate::PixmapObject >::ConstIterator it = page->d->m_pixmaps.constBegin(), itEnd = page->d->m_pixmaps.constEnd(); QVector< Okular::PixmapRequest * > pixmapsToRequest; for ( ; it != itEnd; ++it ) { const QSize size = (*it).m_pixmap->size(); PixmapRequest * p = new PixmapRequest( it.key(), pageNumber, size.width() / qApp->devicePixelRatio(), size.height() / qApp->devicePixelRatio(), 1, PixmapRequest::Asynchronous ); p->d->mForce = true; pixmapsToRequest << p; } // Need to do this ↑↓ in two steps since requestPixmaps can end up calling cancelRenderingBecauseOf // which changes m_pixmaps and thus breaks the loop above for ( PixmapRequest *pr : qAsConst( pixmapsToRequest ) ) { QLinkedList< Okular::PixmapRequest * > requestedPixmaps; requestedPixmaps.push_back( pr ); m_parent->requestPixmaps( requestedPixmaps, Okular::Document::NoOption ); } for (DocumentObserver *observer : qAsConst(m_observers)) { QLinkedList< Okular::PixmapRequest * > requestedPixmaps; TilesManager *tilesManager = page->d->tilesManager( observer ); if ( tilesManager ) { tilesManager->markDirty(); PixmapRequest * p = new PixmapRequest( observer, pageNumber, tilesManager->width() / qApp->devicePixelRatio(), tilesManager->height() / qApp->devicePixelRatio(), 1, PixmapRequest::Asynchronous ); // Get the visible page rect NormalizedRect visibleRect; QVector< Okular::VisiblePageRect * >::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) { if ( (*vIt)->pageNumber == pageNumber ) { visibleRect = (*vIt)->rect; break; } } if ( !visibleRect.isNull() ) { p->setNormalizedRect( visibleRect ); p->setTile( true ); p->d->mForce = true; requestedPixmaps.push_back( p ); } else { delete p; } } m_parent->requestPixmaps( requestedPixmaps, Okular::Document::NoOption ); } } void DocumentPrivate::_o_configChanged() { // free text pages if needed calculateMaxTextPages(); while (m_allocatedTextPagesFifo.count() > m_maxAllocatedTextPages) { int pageToKick = m_allocatedTextPagesFifo.takeFirst(); m_pagesVector.at(pageToKick)->setTextPage( nullptr ); // deletes the textpage } } void DocumentPrivate::doContinueDirectionMatchSearch(void *doContinueDirectionMatchSearchStruct) { DoContinueDirectionMatchSearchStruct *searchStruct = static_cast(doContinueDirectionMatchSearchStruct); RunningSearch *search = m_searches.value(searchStruct->searchID); if ((m_searchCancelled && !searchStruct->match) || !search) { // if the user cancelled but he just got a match, give him the match! QApplication::restoreOverrideCursor(); if (search) search->isCurrentlySearching = false; emit m_parent->searchFinished( searchStruct->searchID, Document::SearchCancelled ); delete searchStruct->pagesToNotify; delete searchStruct; return; } const bool forward = search->cachedType == Document::NextMatch; bool doContinue = false; // if no match found, loop through the whole doc, starting from currentPage if ( !searchStruct->match ) { const int pageCount = m_pagesVector.count(); if (search->pagesDone < pageCount) { doContinue = true; if ( searchStruct->currentPage >= pageCount ) { searchStruct->currentPage = 0; emit m_parent->notice(i18n("Continuing search from beginning"), 3000); } else if ( searchStruct->currentPage < 0 ) { searchStruct->currentPage = pageCount - 1; emit m_parent->notice(i18n("Continuing search from bottom"), 3000); } } } if (doContinue) { // get page Page * page = m_pagesVector[ searchStruct->currentPage ]; // request search page if needed if ( !page->hasTextPage() ) m_parent->requestTextPage( page->number() ); // if found a match on the current page, end the loop searchStruct->match = page->findText( searchStruct->searchID, search->cachedString, forward ? FromTop : FromBottom, search->cachedCaseSensitivity ); if ( !searchStruct->match ) { if (forward) searchStruct->currentPage++; else searchStruct->currentPage--; search->pagesDone++; } else { search->pagesDone = 1; } // Both of the previous if branches need to call doContinueDirectionMatchSearch QTimer::singleShot(0, m_parent, [this, searchStruct] { doContinueDirectionMatchSearch(searchStruct); }); } else { doProcessSearchMatch( searchStruct->match, search, searchStruct->pagesToNotify, searchStruct->currentPage, searchStruct->searchID, search->cachedViewportMove, search->cachedColor ); delete searchStruct; } } void DocumentPrivate::doProcessSearchMatch( RegularAreaRect *match, RunningSearch *search, QSet< int > *pagesToNotify, int currentPage, int searchID, bool moveViewport, const QColor & color ) { // reset cursor to previous shape QApplication::restoreOverrideCursor(); bool foundAMatch = false; search->isCurrentlySearching = false; // if a match has been found.. if ( match ) { // update the RunningSearch structure adding this match.. foundAMatch = true; search->continueOnPage = currentPage; search->continueOnMatch = *match; search->highlightedPages.insert( currentPage ); // ..add highlight to the page.. m_pagesVector[ currentPage ]->d->setHighlight( searchID, match, color ); // ..queue page for notifying changes.. pagesToNotify->insert( currentPage ); // Create a normalized rectangle around the search match that includes a 5% buffer on all sides. const Okular::NormalizedRect matchRectWithBuffer = Okular::NormalizedRect( match->first().left - 0.05, match->first().top - 0.05, match->first().right + 0.05, match->first().bottom + 0.05 ); const bool matchRectFullyVisible = isNormalizedRectangleFullyVisible( matchRectWithBuffer, currentPage ); // ..move the viewport to show the first of the searched word sequence centered if ( moveViewport && !matchRectFullyVisible ) { DocumentViewport searchViewport( currentPage ); searchViewport.rePos.enabled = true; searchViewport.rePos.normalizedX = (match->first().left + match->first().right) / 2.0; searchViewport.rePos.normalizedY = (match->first().top + match->first().bottom) / 2.0; m_parent->setViewport( searchViewport, nullptr, true ); } delete match; } // notify observers about highlights changes foreach(int pageNumber, *pagesToNotify) foreach(DocumentObserver *observer, m_observers) observer->notifyPageChanged( pageNumber, DocumentObserver::Highlights ); if (foundAMatch) emit m_parent->searchFinished( searchID, Document::MatchFound ); else emit m_parent->searchFinished( searchID, Document::NoMatchFound ); delete pagesToNotify; } void DocumentPrivate::doContinueAllDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID) { QMap< Page *, QVector > *pageMatches = static_cast< QMap< Page *, QVector > * >(pageMatchesMap); QSet< int > *pagesToNotify = static_cast< QSet< int > * >( pagesToNotifySet ); RunningSearch *search = m_searches.value(searchID); if (m_searchCancelled || !search) { typedef QVector MatchesVector; QApplication::restoreOverrideCursor(); if (search) search->isCurrentlySearching = false; emit m_parent->searchFinished( searchID, Document::SearchCancelled ); foreach(const MatchesVector &mv, *pageMatches) qDeleteAll(mv); delete pageMatches; delete pagesToNotify; return; } if (currentPage < m_pagesVector.count()) { // get page (from the first to the last) Page *page = m_pagesVector.at(currentPage); int pageNumber = page->number(); // redundant? is it == currentPage ? // request search page if needed if ( !page->hasTextPage() ) m_parent->requestTextPage( pageNumber ); // loop on a page adding highlights for all found items RegularAreaRect * lastMatch = nullptr; while ( true ) { if ( lastMatch ) lastMatch = page->findText( searchID, search->cachedString, NextResult, search->cachedCaseSensitivity, lastMatch ); else lastMatch = page->findText( searchID, search->cachedString, FromTop, search->cachedCaseSensitivity ); if ( !lastMatch ) break; // add highlight rect to the matches map (*pageMatches)[page].append(lastMatch); } delete lastMatch; QTimer::singleShot(0, m_parent, [this, pagesToNotifySet, pageMatches, currentPage, searchID] { doContinueAllDocumentSearch(pagesToNotifySet, pageMatches, currentPage + 1, searchID); }); } else { // reset cursor to previous shape QApplication::restoreOverrideCursor(); search->isCurrentlySearching = false; bool foundAMatch = pageMatches->count() != 0; QMap< Page *, QVector >::const_iterator it, itEnd; it = pageMatches->constBegin(); itEnd = pageMatches->constEnd(); for ( ; it != itEnd; ++it) { foreach(RegularAreaRect *match, it.value()) { it.key()->d->setHighlight( searchID, match, search->cachedColor ); delete match; } search->highlightedPages.insert( it.key()->number() ); pagesToNotify->insert( it.key()->number() ); } foreach(DocumentObserver *observer, m_observers) observer->notifySetup( m_pagesVector, 0 ); // notify observers about highlights changes foreach(int pageNumber, *pagesToNotify) foreach(DocumentObserver *observer, m_observers) observer->notifyPageChanged( pageNumber, DocumentObserver::Highlights ); if (foundAMatch) emit m_parent->searchFinished(searchID, Document::MatchFound ); else emit m_parent->searchFinished( searchID, Document::NoMatchFound ); delete pageMatches; delete pagesToNotify; } } void DocumentPrivate::doContinueGooglesDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID, const QStringList & words) { typedef QPair MatchColor; QMap< Page *, QVector > *pageMatches = static_cast< QMap< Page *, QVector > * >(pageMatchesMap); QSet< int > *pagesToNotify = static_cast< QSet< int > * >( pagesToNotifySet ); RunningSearch *search = m_searches.value(searchID); if (m_searchCancelled || !search) { typedef QVector MatchesVector; QApplication::restoreOverrideCursor(); if (search) search->isCurrentlySearching = false; emit m_parent->searchFinished( searchID, Document::SearchCancelled ); foreach(const MatchesVector &mv, *pageMatches) { foreach(const MatchColor &mc, mv) delete mc.first; } delete pageMatches; delete pagesToNotify; return; } const int wordCount = words.count(); const int hueStep = (wordCount > 1) ? (60 / (wordCount - 1)) : 60; int baseHue, baseSat, baseVal; search->cachedColor.getHsv( &baseHue, &baseSat, &baseVal ); if (currentPage < m_pagesVector.count()) { // get page (from the first to the last) Page *page = m_pagesVector.at(currentPage); int pageNumber = page->number(); // redundant? is it == currentPage ? // request search page if needed if ( !page->hasTextPage() ) m_parent->requestTextPage( pageNumber ); // loop on a page adding highlights for all found items bool allMatched = wordCount > 0, anyMatched = false; for ( int w = 0; w < wordCount; w++ ) { const QString &word = words[ w ]; int newHue = baseHue - w * hueStep; if ( newHue < 0 ) newHue += 360; QColor wordColor = QColor::fromHsv( newHue, baseSat, baseVal ); RegularAreaRect * lastMatch = nullptr; // add all highlights for current word bool wordMatched = false; while ( true ) { if ( lastMatch ) lastMatch = page->findText( searchID, word, NextResult, search->cachedCaseSensitivity, lastMatch ); else lastMatch = page->findText( searchID, word, FromTop, search->cachedCaseSensitivity); if ( !lastMatch ) break; // add highligh rect to the matches map (*pageMatches)[page].append(MatchColor(lastMatch, wordColor)); wordMatched = true; } allMatched = allMatched && wordMatched; anyMatched = anyMatched || wordMatched; } // if not all words are present in page, remove partial highlights const bool matchAll = search->cachedType == Document::GoogleAll; if ( !allMatched && matchAll ) { QVector &matches = (*pageMatches)[page]; foreach(const MatchColor &mc, matches) delete mc.first; pageMatches->remove(page); } QTimer::singleShot(0, m_parent, [this, pagesToNotifySet, pageMatches, currentPage, searchID, words] { doContinueGooglesDocumentSearch(pagesToNotifySet, pageMatches, currentPage + 1, searchID, words); }); } else { // reset cursor to previous shape QApplication::restoreOverrideCursor(); search->isCurrentlySearching = false; bool foundAMatch = pageMatches->count() != 0; QMap< Page *, QVector >::const_iterator it, itEnd; it = pageMatches->constBegin(); itEnd = pageMatches->constEnd(); for ( ; it != itEnd; ++it) { foreach(const MatchColor &mc, it.value()) { it.key()->d->setHighlight( searchID, mc.first, mc.second ); delete mc.first; } search->highlightedPages.insert( it.key()->number() ); pagesToNotify->insert( it.key()->number() ); } // send page lists to update observers (since some filter on bookmarks) foreach(DocumentObserver *observer, m_observers) observer->notifySetup( m_pagesVector, 0 ); // notify observers about highlights changes foreach(int pageNumber, *pagesToNotify) foreach(DocumentObserver *observer, m_observers) observer->notifyPageChanged( pageNumber, DocumentObserver::Highlights ); if (foundAMatch) emit m_parent->searchFinished( searchID, Document::MatchFound ); else emit m_parent->searchFinished( searchID, Document::NoMatchFound ); delete pageMatches; delete pagesToNotify; } } QVariant DocumentPrivate::documentMetaData( const Generator::DocumentMetaDataKey key, const QVariant &option ) const { switch ( key ) { case Generator::PaperColorMetaData: { bool giveDefault = option.toBool(); QColor color; if ( ( SettingsCore::renderMode() == SettingsCore::EnumRenderMode::Paper ) && SettingsCore::changeColors() ) { color = SettingsCore::paperColor(); } else if ( giveDefault ) { color = Qt::white; } return color; } break; case Generator::TextAntialiasMetaData: switch ( SettingsCore::textAntialias() ) { case SettingsCore::EnumTextAntialias::Enabled: return true; break; #if 0 case Settings::EnumTextAntialias::UseKDESettings: // TODO: read the KDE configuration return true; break; #endif case SettingsCore::EnumTextAntialias::Disabled: return false; break; } break; case Generator::GraphicsAntialiasMetaData: switch ( SettingsCore::graphicsAntialias() ) { case SettingsCore::EnumGraphicsAntialias::Enabled: return true; break; case SettingsCore::EnumGraphicsAntialias::Disabled: return false; break; } break; case Generator::TextHintingMetaData: switch ( SettingsCore::textHinting() ) { case SettingsCore::EnumTextHinting::Enabled: return true; break; case SettingsCore::EnumTextHinting::Disabled: return false; break; } break; } return QVariant(); } bool DocumentPrivate::isNormalizedRectangleFullyVisible( const Okular::NormalizedRect & rectOfInterest, int rectPage ) { bool rectFullyVisible = false; const QVector & visibleRects = m_parent->visiblePageRects(); QVector::const_iterator vEnd = visibleRects.end(); QVector::const_iterator vIt = visibleRects.begin(); for ( ; ( vIt != vEnd ) && !rectFullyVisible; ++vIt ) { if ( (*vIt)->pageNumber == rectPage && (*vIt)->rect.contains( rectOfInterest.left, rectOfInterest.top ) && (*vIt)->rect.contains( rectOfInterest.right, rectOfInterest.bottom ) ) { rectFullyVisible = true; } } return rectFullyVisible; } struct pdfsyncpoint { QString file; qlonglong x; qlonglong y; int row; int column; int page; }; void DocumentPrivate::loadSyncFile( const QString & filePath ) { QFile f( filePath + QLatin1String( "sync" ) ); if ( !f.open( QIODevice::ReadOnly ) ) return; QTextStream ts( &f ); // first row: core name of the pdf output const QString coreName = ts.readLine(); // second row: version string, in the form 'Version %u' const QString versionstr = ts.readLine(); // anchor the pattern with \A and \z to match the entire subject string // TODO: with Qt 5.12 QRegularExpression::anchoredPattern() can be used instead QRegularExpression versionre( QStringLiteral("\\AVersion \\d+\\z") , QRegularExpression::CaseInsensitiveOption ); QRegularExpressionMatch match = versionre.match( versionstr ); if ( !match.hasMatch() ) { return; } QHash points; QStack fileStack; int currentpage = -1; const QLatin1String texStr( ".tex" ); const QChar spaceChar = QChar::fromLatin1( ' ' ); fileStack.push( coreName + texStr ); const QSizeF dpi = m_generator->dpi(); QString line; while ( !ts.atEnd() ) { line = ts.readLine(); const QStringList tokens = line.split( spaceChar, QString::SkipEmptyParts ); const int tokenSize = tokens.count(); if ( tokenSize < 1 ) continue; if ( tokens.first() == QLatin1String( "l" ) && tokenSize >= 3 ) { int id = tokens.at( 1 ).toInt(); QHash::const_iterator it = points.constFind( id ); if ( it == points.constEnd() ) { pdfsyncpoint pt; pt.x = 0; pt.y = 0; pt.row = tokens.at( 2 ).toInt(); pt.column = 0; // TODO pt.page = -1; pt.file = fileStack.top(); points[ id ] = pt; } } else if ( tokens.first() == QLatin1String( "s" ) && tokenSize >= 2 ) { currentpage = tokens.at( 1 ).toInt() - 1; } else if ( tokens.first() == QLatin1String( "p*" ) && tokenSize >= 4 ) { // TODO qCDebug(OkularCoreDebug) << "PdfSync: 'p*' line ignored"; } else if ( tokens.first() == QLatin1String( "p" ) && tokenSize >= 4 ) { int id = tokens.at( 1 ).toInt(); QHash::iterator it = points.find( id ); if ( it != points.end() ) { it->x = tokens.at( 2 ).toInt(); it->y = tokens.at( 3 ).toInt(); it->page = currentpage; } } else if ( line.startsWith( QLatin1Char( '(' ) ) && tokenSize == 1 ) { QString newfile = line; // chop the leading '(' newfile.remove( 0, 1 ); if ( !newfile.endsWith( texStr ) ) { newfile += texStr; } fileStack.push( newfile ); } else if ( line == QLatin1String( ")" ) ) { if ( !fileStack.isEmpty() ) { fileStack.pop(); } else qCDebug(OkularCoreDebug) << "PdfSync: going one level down too much"; } else qCDebug(OkularCoreDebug).nospace() << "PdfSync: unknown line format: '" << line << "'"; } QVector< QLinkedList< Okular::SourceRefObjectRect * > > refRects( m_pagesVector.size() ); for ( const pdfsyncpoint &pt : qAsConst(points) ) { // drop pdfsync points not completely valid if ( pt.page < 0 || pt.page >= m_pagesVector.size() ) continue; // magic numbers for TeX's RSU's (Ridiculously Small Units) conversion to pixels Okular::NormalizedPoint p( ( pt.x * dpi.width() ) / ( 72.27 * 65536.0 * m_pagesVector[pt.page]->width() ), ( pt.y * dpi.height() ) / ( 72.27 * 65536.0 * m_pagesVector[pt.page]->height() ) ); QString file = pt.file; Okular::SourceReference * sourceRef = new Okular::SourceReference( file, pt.row, pt.column ); refRects[ pt.page ].append( new Okular::SourceRefObjectRect( p, sourceRef ) ); } for ( int i = 0; i < refRects.size(); ++i ) if ( !refRects.at(i).isEmpty() ) m_pagesVector[i]->setSourceReferences( refRects.at(i) ); } void DocumentPrivate::clearAndWaitForRequests() { m_pixmapRequestsMutex.lock(); QLinkedList< PixmapRequest * >::const_iterator sIt = m_pixmapRequestsStack.constBegin(); QLinkedList< PixmapRequest * >::const_iterator sEnd = m_pixmapRequestsStack.constEnd(); for ( ; sIt != sEnd; ++sIt ) delete *sIt; m_pixmapRequestsStack.clear(); m_pixmapRequestsMutex.unlock(); QEventLoop loop; bool startEventLoop = false; do { m_pixmapRequestsMutex.lock(); startEventLoop = !m_executingPixmapRequests.isEmpty(); if ( m_generator->hasFeature( Generator::SupportsCancelling ) ) { for ( PixmapRequest *executingRequest : qAsConst( m_executingPixmapRequests ) ) executingRequest->d->mShouldAbortRender = 1; if ( m_generator->d_ptr->mTextPageGenerationThread ) m_generator->d_ptr->mTextPageGenerationThread->abortExtraction(); } m_pixmapRequestsMutex.unlock(); if ( startEventLoop ) { m_closingLoop = &loop; loop.exec(); m_closingLoop = nullptr; } } while ( startEventLoop ); } int DocumentPrivate::findFieldPageNumber( Okular::FormField *field ) { // Lookup the page of the FormField int foundPage = -1; for ( uint pageIdx = 0, nPages = m_parent->pages(); pageIdx < nPages; pageIdx++ ) { const Page *p = m_parent->page( pageIdx ); if ( p && p->formFields().contains( field ) ) { foundPage = static_cast< int >( pageIdx ); break; } } return foundPage; } void DocumentPrivate::executeScriptEvent( const std::shared_ptr< Event > &event, const Okular::ScriptAction * linkscript ) { if ( !m_scripter ) { m_scripter = new Scripter( this ); } m_scripter->setEvent( event.get() ); m_scripter->execute( linkscript->scriptType(), linkscript->script() ); // Clear out the event after execution m_scripter->setEvent( nullptr ); } Document::Document( QWidget *widget ) : QObject( nullptr ), d( new DocumentPrivate( this ) ) { d->m_widget = widget; d->m_bookmarkManager = new BookmarkManager( d ); d->m_viewportIterator = d->m_viewportHistory.insert( d->m_viewportHistory.end(), DocumentViewport() ); d->m_undoStack = new QUndoStack(this); connect( SettingsCore::self(), &SettingsCore::configChanged, this, [this] { d->_o_configChanged(); } ); connect(d->m_undoStack, &QUndoStack::canUndoChanged, this, &Document::canUndoChanged); connect(d->m_undoStack, &QUndoStack::canRedoChanged, this, &Document::canRedoChanged); connect(d->m_undoStack, &QUndoStack::cleanChanged, this, &Document::undoHistoryCleanChanged); qRegisterMetaType(); } Document::~Document() { // delete generator, pages, and related stuff closeDocument(); QSet< View * >::const_iterator viewIt = d->m_views.constBegin(), viewEnd = d->m_views.constEnd(); for ( ; viewIt != viewEnd; ++viewIt ) { View *v = *viewIt; v->d_func()->document = nullptr; } // delete the bookmark manager delete d->m_bookmarkManager; // delete the loaded generators QHash< QString, GeneratorInfo >::const_iterator it = d->m_loadedGenerators.constBegin(), itEnd = d->m_loadedGenerators.constEnd(); for ( ; it != itEnd; ++it ) d->unloadGenerator( it.value() ); d->m_loadedGenerators.clear(); // delete the private structure delete d; } QString DocumentPrivate::docDataFileName(const QUrl &url, qint64 document_size) { QString fn = url.fileName(); fn = QString::number( document_size ) + QLatin1Char('.') + fn + QStringLiteral(".xml"); QString docdataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/okular/docdata"); // make sure that the okular/docdata/ directory exists (probably this used to be handled by KStandardDirs) if (!QFileInfo::exists(docdataDir)) { qCDebug(OkularCoreDebug) << "creating docdata folder" << docdataDir; QDir().mkpath(docdataDir); } QString newokularfile = docdataDir + QLatin1Char('/') + fn; // we don't want to accidentally migrate old files when running unit tests if (!QFile::exists( newokularfile ) && !QStandardPaths::isTestModeEnabled()) { // see if an KDE4 file still exists static Kdelibs4Migration k4migration; QString oldfile = k4migration.locateLocal("data", QStringLiteral("okular/docdata/") + fn); if (oldfile.isEmpty()) { oldfile = k4migration.locateLocal("data", QStringLiteral("kpdf/") + fn); } if ( !oldfile.isEmpty() && QFile::exists( oldfile ) ) { // ### copy or move? if ( !QFile::copy( oldfile, newokularfile ) ) return QString(); } } return newokularfile; } QVector DocumentPrivate::availableGenerators() { static QVector result; if (result.isEmpty()) { result = KPluginLoader::findPlugins( QStringLiteral ( "okular/generators" ) ); } return result; } KPluginMetaData DocumentPrivate::generatorForMimeType(const QMimeType& type, QWidget* widget, const QVector &triedOffers) { // First try to find an exact match, and then look for more general ones (e. g. the plain text one) // Ideally we would rank these by "closeness", but that might be overdoing it const QVector available = availableGenerators(); QVector offers; QVector exactMatches; QMimeDatabase mimeDatabase; for (const KPluginMetaData& md : available) { if (triedOffers.contains(md)) continue; const QStringList mimetypes = md.mimeTypes(); for (const QString &supported : mimetypes) { QMimeType mimeType = mimeDatabase.mimeTypeForName(supported); if (mimeType == type && !exactMatches.contains(md)) { exactMatches << md; } if (type.inherits(supported) && !offers.contains(md)) { offers << md; } } } if (!exactMatches.isEmpty()) { offers = exactMatches; } if (offers.isEmpty()) { return KPluginMetaData(); } int hRank=0; // best ranked offer search int offercount = offers.size(); if (offercount > 1) { // sort the offers: the offers with an higher priority come before auto cmp = [](const KPluginMetaData& s1, const KPluginMetaData& s2) { const QString property = QStringLiteral("X-KDE-Priority"); return s1.rawData()[property].toInt() > s2.rawData()[property].toInt(); }; std::stable_sort(offers.begin(), offers.end(), cmp); if (SettingsCore::chooseGenerators()) { QStringList list; for (int i = 0; i < offercount; ++i) { list << offers.at(i).pluginId(); } ChooseEngineDialog choose(list, type, widget); if (choose.exec() == QDialog::Rejected) return KPluginMetaData(); hRank = choose.selectedGenerator(); } } Q_ASSERT(hRank < offers.size()); return offers.at(hRank); } Document::OpenResult Document::openDocument(const QString & docFile, const QUrl &url, const QMimeType &_mime, const QString & password ) { QMimeDatabase db; QMimeType mime = _mime; QByteArray filedata; int fd = -1; if (url.scheme() == QLatin1String("fd")) { bool ok; fd = url.path().midRef(1).toInt(&ok); if (!ok) { return OpenError; } } else if (url.fileName() == QLatin1String( "-" )) { fd = 0; } bool triedMimeFromFileContent = false; if ( fd < 0 ) { if ( !mime.isValid() ) return OpenError; d->m_url = url; d->m_docFileName = docFile; if ( !d->updateMetadataXmlNameAndDocSize() ) return OpenError; } else { QFile qstdin; const bool ret = qstdin.open( fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle ); if (!ret) { qWarning() << "failed to read" << url << filedata; return OpenError; } filedata = qstdin.readAll(); mime = db.mimeTypeForData( filedata ); if ( !mime.isValid() || mime.isDefault() ) return OpenError; d->m_docSize = filedata.size(); triedMimeFromFileContent = true; } const bool fromFileDescriptor = fd >= 0; // 0. load Generator // request only valid non-disabled plugins suitable for the mimetype KPluginMetaData offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget); if ( !offer.isValid() && !triedMimeFromFileContent ) { QMimeType newmime = db.mimeTypeForFile(docFile, QMimeDatabase::MatchContent); triedMimeFromFileContent = true; if ( newmime != mime ) { mime = newmime; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget); } if ( !offer.isValid() ) { // There's still no offers, do a final mime search based on the filename // We need this because sometimes (e.g. when downloading from a webserver) the mimetype we // use is the one fed by the server, that may be wrong newmime = db.mimeTypeForUrl( url ); if ( !newmime.isDefault() && newmime != mime ) { mime = newmime; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget); } } } if (!offer.isValid()) { d->m_openError = i18n( "Can not find a plugin which is able to handle the document being passed." ); emit error( d->m_openError, -1 ); qCWarning(OkularCoreDebug).nospace() << "No plugin for mimetype '" << mime.name() << "'."; return OpenError; } // 1. load Document OpenResult openResult = d->openDocumentInternal( offer, fromFileDescriptor, docFile, filedata, password ); if ( openResult == OpenError ) { QVector triedOffers; triedOffers << offer; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); while ( offer.isValid() ) { openResult = d->openDocumentInternal( offer, fromFileDescriptor, docFile, filedata, password ); if ( openResult == OpenError ) { triedOffers << offer; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); } else break; } if (openResult == OpenError && !triedMimeFromFileContent ) { QMimeType newmime = db.mimeTypeForFile(docFile, QMimeDatabase::MatchContent); triedMimeFromFileContent = true; if ( newmime != mime ) { mime = newmime; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); while ( offer.isValid() ) { openResult = d->openDocumentInternal( offer, fromFileDescriptor, docFile, filedata, password ); if ( openResult == OpenError ) { triedOffers << offer; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); } else break; } } } if ( openResult == OpenSuccess ) { // Clear errors, since we're trying various generators, maybe one of them errored out // but we finally succeeded // TODO one can still see the error message animating out but since this is a very rare // condition we can leave this for future work emit error( QString(), -1 ); } } if ( openResult != OpenSuccess ) { return openResult; } // no need to check for the existence of a synctex file, no parser will be // created if none exists d->m_synctex_scanner = synctex_scanner_new_with_output_file( QFile::encodeName( docFile ).constData(), nullptr, 1); if ( !d->m_synctex_scanner && QFile::exists(docFile + QLatin1String( "sync" ) ) ) { d->loadSyncFile(docFile); } d->m_generatorName = offer.pluginId(); d->m_pageController = new PageController(); connect( d->m_pageController, &PageController::rotationFinished, this, [this](int p, Okular::Page *op) { d->rotationFinished(p, op); } ); for ( Page *p : qAsConst(d->m_pagesVector) ) p->d->m_doc = d; d->m_metadataLoadingCompleted = false; d->m_docdataMigrationNeeded = false; // 2. load Additional Data (bookmarks, local annotations and metadata) about the document if ( d->m_archiveData ) { // QTemporaryFile is weird and will return false in exists if fileName wasn't called before d->m_archiveData->metadataFile.fileName(); d->loadDocumentInfo( d->m_archiveData->metadataFile, LoadPageInfo ); d->loadDocumentInfo( LoadGeneralInfo ); } else { if ( d->loadDocumentInfo( LoadPageInfo ) ) d->m_docdataMigrationNeeded = true; d->loadDocumentInfo( LoadGeneralInfo ); } d->m_metadataLoadingCompleted = true; d->m_bookmarkManager->setUrl( d->m_url ); // 3. setup observers internal lists and data foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ) ); // 4. set initial page (restoring the page saved in xml if loaded) DocumentViewport loadedViewport = (*d->m_viewportIterator); if ( loadedViewport.isValid() ) { (*d->m_viewportIterator) = DocumentViewport(); if ( loadedViewport.pageNumber >= (int)d->m_pagesVector.size() ) loadedViewport.pageNumber = d->m_pagesVector.size() - 1; } else loadedViewport.pageNumber = 0; setViewport( loadedViewport ); // start bookmark saver timer if ( !d->m_saveBookmarksTimer ) { d->m_saveBookmarksTimer = new QTimer( this ); connect( d->m_saveBookmarksTimer, &QTimer::timeout, this, [this] { d->saveDocumentInfo(); } ); } d->m_saveBookmarksTimer->start( 5 * 60 * 1000 ); // start memory check timer if ( !d->m_memCheckTimer ) { d->m_memCheckTimer = new QTimer( this ); connect( d->m_memCheckTimer, &QTimer::timeout, this, [this] { d->slotTimedMemoryCheck(); } ); } d->m_memCheckTimer->start( 2000 ); const DocumentViewport nextViewport = d->nextDocumentViewport(); if ( nextViewport.isValid() ) { setViewport( nextViewport ); d->m_nextDocumentViewport = DocumentViewport(); d->m_nextDocumentDestination = QString(); } AudioPlayer::instance()->d->m_currentDocument = fromFileDescriptor ? QUrl() : d->m_url; const QStringList docScripts = d->m_generator->metaData( QStringLiteral("DocumentScripts"), QStringLiteral ( "JavaScript" ) ).toStringList(); if ( !docScripts.isEmpty() ) { d->m_scripter = new Scripter( d ); for ( const QString &docscript : docScripts ) { d->m_scripter->execute( JavaScript, docscript ); } } return OpenSuccess; } bool DocumentPrivate::updateMetadataXmlNameAndDocSize() { // m_docFileName is always local so we can use QFileInfo on it QFileInfo fileReadTest( m_docFileName ); if ( !fileReadTest.isFile() && !fileReadTest.isReadable() ) return false; m_docSize = fileReadTest.size(); // determine the related "xml document-info" filename if ( m_url.isLocalFile() ) { const QString filePath = docDataFileName( m_url, m_docSize ); qCDebug(OkularCoreDebug) << "Metadata file is now:" << filePath; m_xmlFileName = filePath; } else { qCDebug(OkularCoreDebug) << "Metadata file: disabled"; m_xmlFileName = QString(); } return true; } KXMLGUIClient* Document::guiClient() { if ( d->m_generator ) { Okular::GuiInterface * iface = qobject_cast< Okular::GuiInterface * >( d->m_generator ); if ( iface ) return iface->guiClient(); } return nullptr; } void Document::closeDocument() { // check if there's anything to close... if ( !d->m_generator ) return; emit aboutToClose(); delete d->m_pageController; d->m_pageController = nullptr; delete d->m_scripter; d->m_scripter = nullptr; // remove requests left in queue d->clearAndWaitForRequests(); if ( d->m_fontThread ) { disconnect( d->m_fontThread, nullptr, this, nullptr ); d->m_fontThread->stopExtraction(); d->m_fontThread->wait(); d->m_fontThread = nullptr; } // stop any audio playback AudioPlayer::instance()->stopPlaybacks(); // close the current document and save document info if a document is still opened if ( d->m_generator && d->m_pagesVector.size() > 0 ) { d->saveDocumentInfo(); d->m_generator->closeDocument(); } if ( d->m_synctex_scanner ) { synctex_scanner_free( d->m_synctex_scanner ); d->m_synctex_scanner = nullptr; } // stop timers if ( d->m_memCheckTimer ) d->m_memCheckTimer->stop(); if ( d->m_saveBookmarksTimer ) d->m_saveBookmarksTimer->stop(); if ( d->m_generator ) { // disconnect the generator from this document ... d->m_generator->d_func()->m_document = nullptr; // .. and this document from the generator signals disconnect( d->m_generator, nullptr, this, nullptr ); QHash< QString, GeneratorInfo >::const_iterator genIt = d->m_loadedGenerators.constFind( d->m_generatorName ); Q_ASSERT( genIt != d->m_loadedGenerators.constEnd() ); } d->m_generator = nullptr; d->m_generatorName = QString(); d->m_url = QUrl(); d->m_walletGenerator = nullptr; d->m_docFileName = QString(); d->m_xmlFileName = QString(); delete d->m_tempFile; d->m_tempFile = nullptr; delete d->m_archiveData; d->m_archiveData = nullptr; d->m_docSize = -1; d->m_exportCached = false; d->m_exportFormats.clear(); d->m_exportToText = ExportFormat(); d->m_fontsCached = false; d->m_fontsCache.clear(); d->m_rotation = Rotation0; // send an empty list to observers (to free their data) foreachObserver( notifySetup( QVector< Page * >(), DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ) ); // delete pages and clear 'd->m_pagesVector' container QVector< Page * >::const_iterator pIt = d->m_pagesVector.constBegin(); QVector< Page * >::const_iterator pEnd = d->m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) delete *pIt; d->m_pagesVector.clear(); // clear 'memory allocation' descriptors qDeleteAll( d->m_allocatedPixmaps ); d->m_allocatedPixmaps.clear(); // clear 'running searches' descriptors QMap< int, RunningSearch * >::const_iterator rIt = d->m_searches.constBegin(); QMap< int, RunningSearch * >::const_iterator rEnd = d->m_searches.constEnd(); for ( ; rIt != rEnd; ++rIt ) delete *rIt; d->m_searches.clear(); // clear the visible areas and notify the observers QVector< VisiblePageRect * >::const_iterator vIt = d->m_pageRects.constBegin(); QVector< VisiblePageRect * >::const_iterator vEnd = d->m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) delete *vIt; d->m_pageRects.clear(); foreachObserver( notifyVisibleRectsChanged() ); // reset internal variables d->m_viewportHistory.clear(); d->m_viewportHistory.append( DocumentViewport() ); d->m_viewportIterator = d->m_viewportHistory.begin(); d->m_allocatedPixmapsTotalMemory = 0; d->m_allocatedTextPagesFifo.clear(); d->m_pageSize = PageSize(); d->m_pageSizes.clear(); d->m_documentInfo = DocumentInfo(); d->m_documentInfoAskedKeys.clear(); AudioPlayer::instance()->d->m_currentDocument = QUrl(); d->m_undoStack->clear(); d->m_docdataMigrationNeeded = false; #if HAVE_MALLOC_TRIM // trim unused memory, glibc should do this but it seems it does not // this can greatly decrease the [perceived] memory consumption of okular // see: https://sourceware.org/bugzilla/show_bug.cgi?id=14827 malloc_trim(0); #endif } void Document::addObserver( DocumentObserver * pObserver ) { Q_ASSERT( !d->m_observers.contains( pObserver ) ); d->m_observers << pObserver; // if the observer is added while a document is already opened, tell it if ( !d->m_pagesVector.isEmpty() ) { pObserver->notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ); pObserver->notifyViewportChanged( false /*disables smoothMove*/ ); } } void Document::removeObserver( DocumentObserver * pObserver ) { // remove observer from the set. it won't receive notifications anymore if ( d->m_observers.contains( pObserver ) ) { // free observer's pixmap data QVector::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd(); for ( ; it != end; ++it ) (*it)->deletePixmap( pObserver ); // [MEM] free observer's allocation descriptors QLinkedList< AllocatedPixmap * >::iterator aIt = d->m_allocatedPixmaps.begin(); QLinkedList< AllocatedPixmap * >::iterator aEnd = d->m_allocatedPixmaps.end(); while ( aIt != aEnd ) { AllocatedPixmap * p = *aIt; if ( p->observer == pObserver ) { aIt = d->m_allocatedPixmaps.erase( aIt ); delete p; } else ++aIt; } for ( PixmapRequest *executingRequest : qAsConst( d->m_executingPixmapRequests ) ) { if ( executingRequest->observer() == pObserver ) { d->cancelRenderingBecauseOf( executingRequest, nullptr ); } } // remove observer entry from the set d->m_observers.remove( pObserver ); } } void Document::reparseConfig() { // reparse generator config and if something changed clear Pages bool configchanged = false; if ( d->m_generator ) { Okular::ConfigInterface * iface = qobject_cast< Okular::ConfigInterface * >( d->m_generator ); if ( iface ) configchanged = iface->reparseConfig(); } if ( configchanged ) { // invalidate pixmaps QVector::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd(); for ( ; it != end; ++it ) { (*it)->deletePixmaps(); } // [MEM] remove allocation descriptors qDeleteAll( d->m_allocatedPixmaps ); d->m_allocatedPixmaps.clear(); d->m_allocatedPixmapsTotalMemory = 0; // send reload signals to observers foreachObserver( notifyContentsCleared( DocumentObserver::Pixmap ) ); } // free memory if in 'low' profile if ( SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !d->m_allocatedPixmaps.isEmpty() && !d->m_pagesVector.isEmpty() ) d->cleanupPixmapMemory(); } bool Document::isOpened() const { return d->m_generator; } bool Document::canConfigurePrinter( ) const { if ( d->m_generator ) { Okular::PrintInterface * iface = qobject_cast< Okular::PrintInterface * >( d->m_generator ); return iface ? true : false; } else return false; } DocumentInfo Document::documentInfo() const { QSet keys; for (Okular::DocumentInfo::Key ks = Okular::DocumentInfo::Title; ks < Okular::DocumentInfo::Invalid; ks = Okular::DocumentInfo::Key( ks+1 ) ) { keys << ks; } return documentInfo( keys ); } DocumentInfo Document::documentInfo( const QSet &keys ) const { DocumentInfo result = d->m_documentInfo; const QSet missingKeys = keys - d->m_documentInfoAskedKeys; if ( d->m_generator && !missingKeys.isEmpty() ) { DocumentInfo info = d->m_generator->generateDocumentInfo( missingKeys ); if ( missingKeys.contains( DocumentInfo::FilePath ) ) { info.set( DocumentInfo::FilePath, currentDocument().toDisplayString() ); } if ( d->m_docSize != -1 && missingKeys.contains( DocumentInfo::DocumentSize ) ) { const QString sizeString = KFormat().formatByteSize( d->m_docSize ); info.set( DocumentInfo::DocumentSize, sizeString ); } if ( missingKeys.contains( DocumentInfo::PagesSize ) ) { const QString pagesSize = d->pagesSizeString(); if ( !pagesSize.isEmpty() ) { info.set( DocumentInfo::PagesSize, pagesSize ); } } if ( missingKeys.contains( DocumentInfo::Pages ) && info.get( DocumentInfo::Pages ).isEmpty() ) { info.set( DocumentInfo::Pages, QString::number( this->pages() ) ); } d->m_documentInfo.d->values.unite(info.d->values); d->m_documentInfo.d->titles.unite(info.d->titles); result.d->values.unite(info.d->values); result.d->titles.unite(info.d->titles); } d->m_documentInfoAskedKeys += keys; return result; } const DocumentSynopsis * Document::documentSynopsis() const { return d->m_generator ? d->m_generator->generateDocumentSynopsis() : nullptr; } void Document::startFontReading() { if ( !d->m_generator || !d->m_generator->hasFeature( Generator::FontInfo ) || d->m_fontThread ) return; if ( d->m_fontsCached ) { // in case we have cached fonts, simulate a reading // this way the API is the same, and users no need to care about the // internal caching for ( int i = 0; i < d->m_fontsCache.count(); ++i ) { emit gotFont( d->m_fontsCache.at( i ) ); emit fontReadingProgress( i / pages() ); } emit fontReadingEnded(); return; } d->m_fontThread = new FontExtractionThread( d->m_generator, pages() ); connect( d->m_fontThread, &FontExtractionThread::gotFont, this, [this](const Okular::FontInfo &f) { d->fontReadingGotFont(f); } ); connect( d->m_fontThread.data(), &FontExtractionThread::progress, this, [this](int p) { d->slotFontReadingProgress(p); } ); d->m_fontThread->startExtraction( /*d->m_generator->hasFeature( Generator::Threaded )*/true ); } void Document::stopFontReading() { if ( !d->m_fontThread ) return; disconnect( d->m_fontThread, nullptr, this, nullptr ); d->m_fontThread->stopExtraction(); d->m_fontThread = nullptr; d->m_fontsCache.clear(); } bool Document::canProvideFontInformation() const { return d->m_generator ? d->m_generator->hasFeature( Generator::FontInfo ) : false; } const QList *Document::embeddedFiles() const { return d->m_generator ? d->m_generator->embeddedFiles() : nullptr; } const Page * Document::page( int n ) const { return ( n >= 0 && n < d->m_pagesVector.count() ) ? d->m_pagesVector.at(n) : nullptr; } const DocumentViewport & Document::viewport() const { return (*d->m_viewportIterator); } const QVector< VisiblePageRect * > & Document::visiblePageRects() const { return d->m_pageRects; } void Document::setVisiblePageRects( const QVector< VisiblePageRect * > & visiblePageRects, DocumentObserver *excludeObserver ) { QVector< VisiblePageRect * >::const_iterator vIt = d->m_pageRects.constBegin(); QVector< VisiblePageRect * >::const_iterator vEnd = d->m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) delete *vIt; d->m_pageRects = visiblePageRects; // notify change to all other (different from id) observers foreach(DocumentObserver *o, d->m_observers) if ( o != excludeObserver ) o->notifyVisibleRectsChanged(); } uint Document::currentPage() const { return (*d->m_viewportIterator).pageNumber; } uint Document::pages() const { return d->m_pagesVector.size(); } QUrl Document::currentDocument() const { return d->m_url; } bool Document::isAllowed( Permission action ) const { if ( action == Okular::AllowNotes && ( d->m_docdataMigrationNeeded || !d->m_annotationEditingEnabled ) ) return false; if ( action == Okular::AllowFillForms && d->m_docdataMigrationNeeded ) return false; #if !OKULAR_FORCE_DRM if ( KAuthorized::authorize( QStringLiteral("skip_drm") ) && !SettingsCore::obeyDRM() ) return true; #endif return d->m_generator ? d->m_generator->isAllowed( action ) : false; } bool Document::supportsSearching() const { return d->m_generator ? d->m_generator->hasFeature( Generator::TextExtraction ) : false; } bool Document::supportsPageSizes() const { return d->m_generator ? d->m_generator->hasFeature( Generator::PageSizes ) : false; } bool Document::supportsTiles() const { return d->m_generator ? d->m_generator->hasFeature( Generator::TiledRendering ) : false; } PageSize::List Document::pageSizes() const { if ( d->m_generator ) { if ( d->m_pageSizes.isEmpty() ) d->m_pageSizes = d->m_generator->pageSizes(); return d->m_pageSizes; } return PageSize::List(); } bool Document::canExportToText() const { if ( !d->m_generator ) return false; d->cacheExportFormats(); return !d->m_exportToText.isNull(); } bool Document::exportToText( const QString& fileName ) const { if ( !d->m_generator ) return false; d->cacheExportFormats(); if ( d->m_exportToText.isNull() ) return false; return d->m_generator->exportTo( fileName, d->m_exportToText ); } ExportFormat::List Document::exportFormats() const { if ( !d->m_generator ) return ExportFormat::List(); d->cacheExportFormats(); return d->m_exportFormats; } bool Document::exportTo( const QString& fileName, const ExportFormat& format ) const { return d->m_generator ? d->m_generator->exportTo( fileName, format ) : false; } bool Document::historyAtBegin() const { return d->m_viewportIterator == d->m_viewportHistory.begin(); } bool Document::historyAtEnd() const { return d->m_viewportIterator == --(d->m_viewportHistory.end()); } QVariant Document::metaData( const QString & key, const QVariant & option ) const { // if option starts with "src:" assume that we are handling a // source reference if ( key == QLatin1String("NamedViewport") && option.toString().startsWith( QLatin1String("src:"), Qt::CaseInsensitive ) && d->m_synctex_scanner) { const QString reference = option.toString(); // The reference is of form "src:1111Filename", where "1111" // points to line number 1111 in the file "Filename". // Extract the file name and the numeral part from the reference string. // This will fail if Filename starts with a digit. QString name, lineString; // Remove "src:". Presence of substring has been checked before this // function is called. name = reference.mid( 4 ); // split int nameLength = name.length(); int i = 0; for( i = 0; i < nameLength; ++i ) { if ( !name[i].isDigit() ) break; } lineString = name.left( i ); name = name.mid( i ); // Remove spaces. name = name.trimmed(); lineString = lineString.trimmed(); // Convert line to integer. bool ok; int line = lineString.toInt( &ok ); if (!ok) line = -1; // Use column == -1 for now. if( synctex_display_query( d->m_synctex_scanner, QFile::encodeName(name).constData(), line, -1, 0 ) > 0 ) { synctex_node_p node; // For now use the first hit. Could possibly be made smarter // in case there are multiple hits. while( ( node = synctex_scanner_next_result( d->m_synctex_scanner ) ) ) { Okular::DocumentViewport viewport; // TeX pages start at 1. viewport.pageNumber = synctex_node_page( node ) - 1; if ( viewport.pageNumber >= 0 ) { const QSizeF dpi = d->m_generator->dpi(); // TeX small points ... double px = (synctex_node_visible_h( node ) * dpi.width()) / 72.27; double py = (synctex_node_visible_v( node ) * dpi.height()) / 72.27; viewport.rePos.normalizedX = px / page(viewport.pageNumber)->width(); viewport.rePos.normalizedY = ( py + 0.5 ) / page(viewport.pageNumber)->height(); viewport.rePos.enabled = true; viewport.rePos.pos = Okular::DocumentViewport::Center; return viewport.toString(); } } } } return d->m_generator ? d->m_generator->metaData( key, option ) : QVariant(); } Rotation Document::rotation() const { return d->m_rotation; } QSizeF Document::allPagesSize() const { bool allPagesSameSize = true; QSizeF size; for (int i = 0; allPagesSameSize && i < d->m_pagesVector.count(); ++i) { const Page *p = d->m_pagesVector.at(i); if (i == 0) size = QSizeF(p->width(), p->height()); else { allPagesSameSize = (size == QSizeF(p->width(), p->height())); } } if (allPagesSameSize) return size; else return QSizeF(); } QString Document::pageSizeString(int page) const { if (d->m_generator) { if (d->m_generator->pagesSizeMetric() != Generator::None) { const Page *p = d->m_pagesVector.at( page ); return d->localizedSize(QSizeF(p->width(), p->height())); } } return QString(); } static bool shouldCancelRenderingBecauseOf( const PixmapRequest & executingRequest, const PixmapRequest & otherRequest ) { // New request has higher priority -> cancel if ( executingRequest.priority() > otherRequest.priority() ) return true; // New request has lower priority -> don't cancel if ( executingRequest.priority() < otherRequest.priority() ) return false; // New request has same priority and is from a different observer -> don't cancel // AFAIK this never happens since all observers have different priorities if ( executingRequest.observer() != otherRequest.observer() ) return false; // Same priority and observer, different page number -> don't cancel // may still end up cancelled later in the parent caller if none of the requests // is of the executingRequest page and RemoveAllPrevious is specified if ( executingRequest.pageNumber() != otherRequest.pageNumber() ) return false; // Same priority, observer, page, different size -> cancel if ( executingRequest.width() != otherRequest.width() ) return true; // Same priority, observer, page, different size -> cancel if ( executingRequest.height() != otherRequest.height() ) return true; // Same priority, observer, page, different tiling -> cancel if ( executingRequest.isTile() != otherRequest.isTile() ) return true; // Same priority, observer, page, different tiling -> cancel if ( executingRequest.isTile() ) { const NormalizedRect bothRequestsRect = executingRequest.normalizedRect() | otherRequest.normalizedRect(); if ( !( bothRequestsRect == executingRequest.normalizedRect() ) ) return true; } return false; } bool DocumentPrivate::cancelRenderingBecauseOf( PixmapRequest *executingRequest, PixmapRequest *newRequest ) { // No point in aborting the rendering already finished, let it go through if ( !executingRequest->d->mResultImage.isNull() ) return false; if ( newRequest && newRequest->asynchronous() && executingRequest->partialUpdatesWanted() ) { newRequest->setPartialUpdatesWanted( true ); } TilesManager *tm = executingRequest->d->tilesManager(); if ( tm ) { tm->setPixmap( nullptr, executingRequest->normalizedRect(), true /*isPartialPixmap*/ ); tm->setRequest( NormalizedRect(), 0, 0 ); } PagePrivate::PixmapObject object = executingRequest->page()->d->m_pixmaps.take( executingRequest->observer() ); delete object.m_pixmap; if ( executingRequest->d->mShouldAbortRender != 0) return false; executingRequest->d->mShouldAbortRender = 1; if ( m_generator->d_ptr->mTextPageGenerationThread && m_generator->d_ptr->mTextPageGenerationThread->page() == executingRequest->page() ) { m_generator->d_ptr->mTextPageGenerationThread->abortExtraction(); } return true; } void Document::requestPixmaps( const QLinkedList< PixmapRequest * > & requests ) { requestPixmaps( requests, RemoveAllPrevious ); } void Document::requestPixmaps( const QLinkedList< PixmapRequest * > & requests, PixmapRequestFlags reqOptions ) { if ( requests.isEmpty() ) return; if ( !d->m_pageController ) { // delete requests.. QLinkedList< PixmapRequest * >::const_iterator rIt = requests.constBegin(), rEnd = requests.constEnd(); for ( ; rIt != rEnd; ++rIt ) delete *rIt; // ..and return return; } QSet< DocumentObserver * > observersPixmapCleared; // 1. [CLEAN STACK] remove previous requests of requesterID DocumentObserver *requesterObserver = requests.first()->observer(); QSet< int > requestedPages; { QLinkedList< PixmapRequest * >::const_iterator rIt = requests.constBegin(), rEnd = requests.constEnd(); for ( ; rIt != rEnd; ++rIt ) { Q_ASSERT( (*rIt)->observer() == requesterObserver ); requestedPages.insert( (*rIt)->pageNumber() ); } } const bool removeAllPrevious = reqOptions & RemoveAllPrevious; d->m_pixmapRequestsMutex.lock(); QLinkedList< PixmapRequest * >::iterator sIt = d->m_pixmapRequestsStack.begin(), sEnd = d->m_pixmapRequestsStack.end(); while ( sIt != sEnd ) { if ( (*sIt)->observer() == requesterObserver && ( removeAllPrevious || requestedPages.contains( (*sIt)->pageNumber() ) ) ) { // delete request and remove it from stack delete *sIt; sIt = d->m_pixmapRequestsStack.erase( sIt ); } else ++sIt; } // 1.B [PREPROCESS REQUESTS] tweak some values of the requests for ( PixmapRequest *request : requests ) { // set the 'page field' (see PixmapRequest) and check if it is valid qCDebug(OkularCoreDebug).nospace() << "request observer=" << request->observer() << " " <width() << "x" << request->height() << "@" << request->pageNumber(); if ( d->m_pagesVector.value( request->pageNumber() ) == 0 ) { // skip requests referencing an invalid page (must not happen) delete request; continue; } request->d->mPage = d->m_pagesVector.value( request->pageNumber() ); if ( request->isTile() ) { // Change the current request rect so that only invalid tiles are // requested. Also make sure the rect is tile-aligned. NormalizedRect tilesRect; const QList tiles = request->d->tilesManager()->tilesAt( request->normalizedRect(), TilesManager::TerminalTile ); QList::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd(); while ( tIt != tEnd ) { const Tile &tile = *tIt; if ( !tile.isValid() ) { if ( tilesRect.isNull() ) tilesRect = tile.rect(); else tilesRect |= tile.rect(); } tIt++; } request->setNormalizedRect( tilesRect ); } if ( !request->asynchronous() ) request->d->mPriority = 0; } // 1.C [CANCEL REQUESTS] cancel those requests that are running and should be cancelled because of the new requests coming in if ( d->m_generator->hasFeature( Generator::SupportsCancelling ) ) { for ( PixmapRequest *executingRequest : qAsConst( d->m_executingPixmapRequests ) ) { bool newRequestsContainExecutingRequestPage = false; bool requestCancelled = false; for ( PixmapRequest *newRequest : requests ) { if ( newRequest->pageNumber() == executingRequest->pageNumber() && requesterObserver == executingRequest->observer()) { newRequestsContainExecutingRequestPage = true; } if ( shouldCancelRenderingBecauseOf( *executingRequest, *newRequest ) ) { requestCancelled = d->cancelRenderingBecauseOf( executingRequest, newRequest ); } } // If we were told to remove all the previous requests and the executing request page is not part of the new requests, cancel it if ( !requestCancelled && removeAllPrevious && requesterObserver == executingRequest->observer() && !newRequestsContainExecutingRequestPage ) { requestCancelled = d->cancelRenderingBecauseOf( executingRequest, nullptr ); } if ( requestCancelled ) { observersPixmapCleared << executingRequest->observer(); } } } // 2. [ADD TO STACK] add requests to stack for ( PixmapRequest *request : requests ) { // add request to the 'stack' at the right place if ( !request->priority() ) // add priority zero requests to the top of the stack d->m_pixmapRequestsStack.append( request ); else { // insert in stack sorted by priority sIt = d->m_pixmapRequestsStack.begin(); sEnd = d->m_pixmapRequestsStack.end(); while ( sIt != sEnd && (*sIt)->priority() > request->priority() ) ++sIt; d->m_pixmapRequestsStack.insert( sIt, request ); } } d->m_pixmapRequestsMutex.unlock(); // 3. [START FIRST GENERATION] if generator is ready, start a new generation, // or else (if gen is running) it will be started when the new contents will //come from generator (in requestDone()) // all handling of requests put into sendGeneratorPixmapRequest // if ( generator->canRequestPixmap() ) d->sendGeneratorPixmapRequest(); for ( DocumentObserver *o : qAsConst( observersPixmapCleared ) ) o->notifyContentsCleared( Okular::DocumentObserver::Pixmap ); } void Document::requestTextPage( uint pageNumber ) { Page * kp = d->m_pagesVector[ pageNumber ]; if ( !d->m_generator || !kp ) return; // Memory management for TextPages d->m_generator->generateTextPage( kp ); } void DocumentPrivate::notifyAnnotationChanges( int page ) { foreachObserverD( notifyPageChanged( page, DocumentObserver::Annotations ) ); } void DocumentPrivate::notifyFormChanges( int /*page*/ ) { recalculateForms(); } void Document::addPageAnnotation( int page, Annotation * annotation ) { // Transform annotation's base boundary rectangle into unrotated coordinates Page *p = d->m_pagesVector[page]; QTransform t = p->d->rotationMatrix(); annotation->d_ptr->baseTransform(t.inverted()); QUndoCommand *uc = new AddAnnotationCommand(this->d, annotation, page); d->m_undoStack->push(uc); } bool Document::canModifyPageAnnotation( const Annotation * annotation ) const { if ( !annotation || ( annotation->flags() & Annotation::DenyWrite ) ) return false; if ( !isAllowed(Okular::AllowNotes) ) return false; if ( ( annotation->flags() & Annotation::External ) && !d->canModifyExternalAnnotations() ) return false; switch ( annotation->subType() ) { case Annotation::AText: case Annotation::ALine: case Annotation::AGeom: case Annotation::AHighlight: case Annotation::AStamp: case Annotation::AInk: return true; default: return false; } } void Document::prepareToModifyAnnotationProperties( Annotation * annotation ) { Q_ASSERT(d->m_prevPropsOfAnnotBeingModified.isNull()); if (!d->m_prevPropsOfAnnotBeingModified.isNull()) { qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties has already been called since last call to Document::modifyPageAnnotationProperties"; return; } d->m_prevPropsOfAnnotBeingModified = annotation->getAnnotationPropertiesDomNode(); } void Document::modifyPageAnnotationProperties( int page, Annotation * annotation ) { Q_ASSERT(!d->m_prevPropsOfAnnotBeingModified.isNull()); if (d->m_prevPropsOfAnnotBeingModified.isNull()) { qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties must be called before Annotation is modified"; return; } QDomNode prevProps = d->m_prevPropsOfAnnotBeingModified; QUndoCommand *uc = new Okular::ModifyAnnotationPropertiesCommand( d, annotation, page, prevProps, annotation->getAnnotationPropertiesDomNode() ); d->m_undoStack->push( uc ); d->m_prevPropsOfAnnotBeingModified.clear(); } void Document::translatePageAnnotation(int page, Annotation* annotation, const NormalizedPoint & delta ) { int complete = (annotation->flags() & Okular::Annotation::BeingMoved) == 0; QUndoCommand *uc = new Okular::TranslateAnnotationCommand( d, annotation, page, delta, complete ); d->m_undoStack->push(uc); } void Document::adjustPageAnnotation( int page, Annotation *annotation, const Okular::NormalizedPoint & delta1, const Okular::NormalizedPoint & delta2 ) { const bool complete = (annotation->flags() & Okular::Annotation::BeingResized) == 0; QUndoCommand *uc = new Okular::AdjustAnnotationCommand( d, annotation, page, delta1, delta2, complete ); d->m_undoStack->push(uc); } void Document::editPageAnnotationContents( int page, Annotation* annotation, const QString & newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos ) { QString prevContents = annotation->contents(); QUndoCommand *uc = new EditAnnotationContentsCommand( d, annotation, page, newContents, newCursorPos, prevContents, prevCursorPos, prevAnchorPos ); d->m_undoStack->push( uc ); } bool Document::canRemovePageAnnotation( const Annotation * annotation ) const { if ( !annotation || ( annotation->flags() & Annotation::DenyDelete ) ) return false; if ( ( annotation->flags() & Annotation::External ) && !d->canRemoveExternalAnnotations() ) return false; switch ( annotation->subType() ) { case Annotation::AText: case Annotation::ALine: case Annotation::AGeom: case Annotation::AHighlight: case Annotation::AStamp: case Annotation::AInk: case Annotation::ACaret: return true; default: return false; } } void Document::removePageAnnotation( int page, Annotation * annotation ) { QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page); d->m_undoStack->push(uc); } void Document::removePageAnnotations( int page, const QList &annotations ) { d->m_undoStack->beginMacro(i18nc("remove a collection of annotations from the page", "remove annotations")); foreach(Annotation* annotation, annotations) { QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page); d->m_undoStack->push(uc); } d->m_undoStack->endMacro(); } bool DocumentPrivate::canAddAnnotationsNatively() const { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); if ( iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Addition) ) return true; return false; } bool DocumentPrivate::canModifyExternalAnnotations() const { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); if ( iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Modification) ) return true; return false; } bool DocumentPrivate::canRemoveExternalAnnotations() const { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); if ( iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Removal) ) return true; return false; } void Document::setPageTextSelection( int page, RegularAreaRect * rect, const QColor & color ) { Page * kp = d->m_pagesVector[ page ]; if ( !d->m_generator || !kp ) return; // add or remove the selection basing whether rect is null or not if ( rect ) kp->d->setTextSelections( rect, color ); else kp->d->deleteTextSelections(); // notify observers about the change foreachObserver( notifyPageChanged( page, DocumentObserver::TextSelection ) ); } bool Document::canUndo() const { return d->m_undoStack->canUndo(); } bool Document::canRedo() const { return d->m_undoStack->canRedo(); } /* REFERENCE IMPLEMENTATION: better calling setViewport from other code void Document::setNextPage() { // advance page and set viewport on observers if ( (*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1 ) setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber + 1 ) ); } void Document::setPrevPage() { // go to previous page and set viewport on observers if ( (*d->m_viewportIterator).pageNumber > 0 ) setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber - 1 ) ); } */ void Document::setViewportPage( int page, DocumentObserver *excludeObserver, bool smoothMove ) { // clamp page in range [0 ... numPages-1] if ( page < 0 ) page = 0; else if ( page > (int)d->m_pagesVector.count() ) page = d->m_pagesVector.count() - 1; // make a viewport from the page and broadcast it setViewport( DocumentViewport( page ), excludeObserver, smoothMove ); } void Document::setViewport( const DocumentViewport & viewport, DocumentObserver *excludeObserver, bool smoothMove ) { if ( !viewport.isValid() ) { qCDebug(OkularCoreDebug) << "invalid viewport:" << viewport.toString(); return; } if ( viewport.pageNumber >= int(d->m_pagesVector.count()) ) { //qCDebug(OkularCoreDebug) << "viewport out of document:" << viewport.toString(); return; } // if already broadcasted, don't redo it DocumentViewport & oldViewport = *d->m_viewportIterator; // disabled by enrico on 2005-03-18 (less debug output) //if ( viewport == oldViewport ) // qCDebug(OkularCoreDebug) << "setViewport with the same viewport."; const int oldPageNumber = oldViewport.pageNumber; // set internal viewport taking care of history if ( oldViewport.pageNumber == viewport.pageNumber || !oldViewport.isValid() ) { // if page is unchanged save the viewport at current position in queue oldViewport = viewport; } else { // remove elements after viewportIterator in queue d->m_viewportHistory.erase( ++d->m_viewportIterator, d->m_viewportHistory.end() ); // keep the list to a reasonable size by removing head when needed if ( d->m_viewportHistory.count() >= OKULAR_HISTORY_MAXSTEPS ) d->m_viewportHistory.pop_front(); // add the item at the end of the queue d->m_viewportIterator = d->m_viewportHistory.insert( d->m_viewportHistory.end(), viewport ); } const int currentViewportPage = (*d->m_viewportIterator).pageNumber; const bool currentPageChanged = (oldPageNumber != currentViewportPage); // notify change to all other (different from id) observers for (DocumentObserver *o : qAsConst(d->m_observers)) { if ( o != excludeObserver ) o->notifyViewportChanged( smoothMove ); if ( currentPageChanged ) o->notifyCurrentPageChanged( oldPageNumber, currentViewportPage ); } } void Document::setZoom(int factor, DocumentObserver *excludeObserver) { // notify change to all other (different from id) observers for (DocumentObserver *o : qAsConst(d->m_observers)) if (o != excludeObserver) o->notifyZoom( factor ); } void Document::setPrevViewport() // restore viewport from the history { if ( d->m_viewportIterator != d->m_viewportHistory.begin() ) { const int oldViewportPage = (*d->m_viewportIterator).pageNumber; // restore previous viewport and notify it to observers --d->m_viewportIterator; foreachObserver( notifyViewportChanged( true ) ); const int currentViewportPage = (*d->m_viewportIterator).pageNumber; if (oldViewportPage != currentViewportPage) foreachObserver( notifyCurrentPageChanged( oldViewportPage, currentViewportPage ) ); } } void Document::setNextViewport() // restore next viewport from the history { QLinkedList< DocumentViewport >::const_iterator nextIterator = d->m_viewportIterator; ++nextIterator; if ( nextIterator != d->m_viewportHistory.end() ) { const int oldViewportPage = (*d->m_viewportIterator).pageNumber; // restore next viewport and notify it to observers ++d->m_viewportIterator; foreachObserver( notifyViewportChanged( true ) ); const int currentViewportPage = (*d->m_viewportIterator).pageNumber; if (oldViewportPage != currentViewportPage) foreachObserver( notifyCurrentPageChanged( oldViewportPage, currentViewportPage ) ); } } void Document::setNextDocumentViewport( const DocumentViewport & viewport ) { d->m_nextDocumentViewport = viewport; } void Document::setNextDocumentDestination( const QString &namedDestination ) { d->m_nextDocumentDestination = namedDestination; } void Document::searchText( int searchID, const QString & text, bool fromStart, Qt::CaseSensitivity caseSensitivity, SearchType type, bool moveViewport, const QColor & color ) { d->m_searchCancelled = false; // safety checks: don't perform searches on empty or unsearchable docs if ( !d->m_generator || !d->m_generator->hasFeature( Generator::TextExtraction ) || d->m_pagesVector.isEmpty() ) { emit searchFinished( searchID, NoMatchFound ); return; } // if searchID search not recorded, create new descriptor and init params QMap< int, RunningSearch * >::iterator searchIt = d->m_searches.find( searchID ); if ( searchIt == d->m_searches.end() ) { RunningSearch * search = new RunningSearch(); search->continueOnPage = -1; searchIt = d->m_searches.insert( searchID, search ); } RunningSearch * s = *searchIt; // update search structure bool newText = text != s->cachedString; s->cachedString = text; s->cachedType = type; s->cachedCaseSensitivity = caseSensitivity; s->cachedViewportMove = moveViewport; s->cachedColor = color; s->isCurrentlySearching = true; // global data for search QSet< int > *pagesToNotify = new QSet< int >; // remove highlights from pages and queue them for notifying changes *pagesToNotify += s->highlightedPages; for (const int pageNumber : qAsConst(s->highlightedPages)) { d->m_pagesVector.at(pageNumber)->d->deleteHighlights( searchID ); } s->highlightedPages.clear(); // set hourglass cursor QApplication::setOverrideCursor( Qt::WaitCursor ); // 1. ALLDOC - process all document marking pages if ( type == AllDocument ) { QMap< Page *, QVector > *pageMatches = new QMap< Page *, QVector >; // search and highlight 'text' (as a solid phrase) on all pages QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID] { d->doContinueAllDocumentSearch(pagesToNotify, pageMatches, 0, searchID); }); } // 2. NEXTMATCH - find next matching item (or start from top) // 3. PREVMATCH - find previous matching item (or start from bottom) else if ( type == NextMatch || type == PreviousMatch ) { // find out from where to start/resume search from const bool forward = type == NextMatch; const int viewportPage = (*d->m_viewportIterator).pageNumber; const int fromStartSearchPage = forward ? 0 : d->m_pagesVector.count() - 1; int currentPage = fromStart ? fromStartSearchPage : ((s->continueOnPage != -1) ? s->continueOnPage : viewportPage); Page * lastPage = fromStart ? 0 : d->m_pagesVector[ currentPage ]; int pagesDone = 0; // continue checking last TextPage first (if it is the current page) RegularAreaRect * match = nullptr; if ( lastPage && lastPage->number() == s->continueOnPage ) { if ( newText ) match = lastPage->findText( searchID, text, forward ? FromTop : FromBottom, caseSensitivity ); else match = lastPage->findText( searchID, text, forward ? NextResult : PreviousResult, caseSensitivity, &s->continueOnMatch ); if ( !match ) { if (forward) currentPage++; else currentPage--; pagesDone++; } } s->pagesDone = pagesDone; DoContinueDirectionMatchSearchStruct *searchStruct = new DoContinueDirectionMatchSearchStruct(); searchStruct->pagesToNotify = pagesToNotify; searchStruct->match = match; searchStruct->currentPage = currentPage; searchStruct->searchID = searchID; QTimer::singleShot(0, this, [this, searchStruct] { d->doContinueDirectionMatchSearch(searchStruct); }); } // 4. GOOGLE* - process all document marking pages else if ( type == GoogleAll || type == GoogleAny ) { QMap< Page *, QVector< QPair > > *pageMatches = new QMap< Page *, QVector > >; const QStringList words = text.split( QLatin1Char ( ' ' ), QString::SkipEmptyParts ); // search and highlight every word in 'text' on all pages QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID, words] { d->doContinueGooglesDocumentSearch(pagesToNotify, pageMatches, 0, searchID, words); }); } } void Document::continueSearch( int searchID ) { // check if searchID is present in runningSearches QMap< int, RunningSearch * >::const_iterator it = d->m_searches.constFind( searchID ); if ( it == d->m_searches.constEnd() ) { emit searchFinished( searchID, NoMatchFound ); return; } // start search with cached parameters from last search by searchID RunningSearch * p = *it; if ( !p->isCurrentlySearching ) searchText( searchID, p->cachedString, false, p->cachedCaseSensitivity, p->cachedType, p->cachedViewportMove, p->cachedColor ); } void Document::continueSearch( int searchID, SearchType type ) { // check if searchID is present in runningSearches QMap< int, RunningSearch * >::const_iterator it = d->m_searches.constFind( searchID ); if ( it == d->m_searches.constEnd() ) { emit searchFinished( searchID, NoMatchFound ); return; } // start search with cached parameters from last search by searchID RunningSearch * p = *it; if ( !p->isCurrentlySearching ) searchText( searchID, p->cachedString, false, p->cachedCaseSensitivity, type, p->cachedViewportMove, p->cachedColor ); } void Document::resetSearch( int searchID ) { // if we are closing down, don't bother doing anything if ( !d->m_generator ) return; // check if searchID is present in runningSearches QMap< int, RunningSearch * >::iterator searchIt = d->m_searches.find( searchID ); if ( searchIt == d->m_searches.end() ) return; // get previous parameters for search RunningSearch * s = *searchIt; // unhighlight pages and inform observers about that for (const int pageNumber : qAsConst(s->highlightedPages)) { d->m_pagesVector.at(pageNumber)->d->deleteHighlights( searchID ); foreachObserver( notifyPageChanged( pageNumber, DocumentObserver::Highlights ) ); } // send the setup signal too (to update views that filter on matches) foreachObserver( notifySetup( d->m_pagesVector, 0 ) ); // remove search from the runningSearches list and delete it d->m_searches.erase( searchIt ); delete s; } void Document::cancelSearch() { d->m_searchCancelled = true; } void Document::undo() { d->m_undoStack->undo(); } void Document::redo() { d->m_undoStack->redo(); } void Document::editFormText( int pageNumber, Okular::FormFieldText* form, const QString & newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos ) { QUndoCommand *uc = new EditFormTextCommand( this->d, form, pageNumber, newContents, newCursorPos, form->text(), prevCursorPos, prevAnchorPos ); d->m_undoStack->push( uc ); } void Document::editFormList( int pageNumber, FormFieldChoice* form, const QList< int > & newChoices ) { const QList< int > prevChoices = form->currentChoices(); QUndoCommand *uc = new EditFormListCommand( this->d, form, pageNumber, newChoices, prevChoices ); d->m_undoStack->push( uc ); } void Document::editFormCombo( int pageNumber, FormFieldChoice* form, const QString & newText, int newCursorPos, int prevCursorPos, int prevAnchorPos ) { QString prevText; if ( form->currentChoices().isEmpty() ) { prevText = form->editChoice(); } else { prevText = form->choices().at(form->currentChoices().constFirst()); } QUndoCommand *uc = new EditFormComboCommand( this->d, form, pageNumber, newText, newCursorPos, prevText, prevCursorPos, prevAnchorPos ); d->m_undoStack->push( uc ); } void Document::editFormButtons( int pageNumber, const QList< FormFieldButton* >& formButtons, const QList< bool >& newButtonStates ) { QUndoCommand *uc = new EditFormButtonsCommand( this->d, pageNumber, formButtons, newButtonStates ); d->m_undoStack->push( uc ); } void Document::reloadDocument() const { const int numOfPages = pages(); for( int i = currentPage(); i >= 0; i -- ) d->refreshPixmaps( i ); for( int i = currentPage() + 1; i < numOfPages; i ++ ) d->refreshPixmaps( i ); } BookmarkManager * Document::bookmarkManager() const { return d->m_bookmarkManager; } QList Document::bookmarkedPageList() const { QList list; uint docPages = pages(); //pages are 0-indexed internally, but 1-indexed externally for ( uint i = 0; i < docPages; i++ ) { if ( bookmarkManager()->isBookmarked( i ) ) { list << i + 1; } } return list; } QString Document::bookmarkedPageRange() const { // Code formerly in Part::slotPrint() // range detecting QString range; uint docPages = pages(); int startId = -1; int endId = -1; for ( uint i = 0; i < docPages; ++i ) { if ( bookmarkManager()->isBookmarked( i ) ) { if ( startId < 0 ) startId = i; if ( endId < 0 ) endId = startId; else ++endId; } else if ( startId >= 0 && endId >= 0 ) { if ( !range.isEmpty() ) range += QLatin1Char ( ',' ); if ( endId - startId > 0 ) range += QStringLiteral( "%1-%2" ).arg( startId + 1 ).arg( endId + 1 ); else range += QString::number( startId + 1 ); startId = -1; endId = -1; } } if ( startId >= 0 && endId >= 0 ) { if ( !range.isEmpty() ) range += QLatin1Char ( ',' ); if ( endId - startId > 0 ) range += QStringLiteral( "%1-%2" ).arg( startId + 1 ).arg( endId + 1 ); else range += QString::number( startId + 1 ); } return range; } void Document::processAction( const Action * action ) { if ( !action ) return; // Don't execute next actions if the action itself caused the closing of the document bool executeNextActions = true; auto connectionId = connect( this, &Document::aboutToClose, [&executeNextActions] { executeNextActions = false; } ); switch( action->actionType() ) { case Action::Goto: { const GotoAction * go = static_cast< const GotoAction * >( action ); d->m_nextDocumentViewport = go->destViewport(); d->m_nextDocumentDestination = go->destinationName(); // Explanation of why d->m_nextDocumentViewport is needed: // all openRelativeFile does is launch a signal telling we // want to open another URL, the problem is that when the file is // non local, the loading is done asynchronously so you can't // do a setViewport after the if as it was because you are doing the setViewport // on the old file and when the new arrives there is no setViewport for it and // it does not show anything // first open filename if link is pointing outside this document if ( go->isExternal() && !d->openRelativeFile( go->fileName() ) ) { qCWarning(OkularCoreDebug).nospace() << "Action: Error opening '" << go->fileName() << "'."; break; } else { const DocumentViewport nextViewport = d->nextDocumentViewport(); // skip local links that point to nowhere (broken ones) if ( !nextViewport.isValid() ) break; setViewport( nextViewport, nullptr, true ); d->m_nextDocumentViewport = DocumentViewport(); d->m_nextDocumentDestination = QString(); } } break; case Action::Execute: { const ExecuteAction * exe = static_cast< const ExecuteAction * >( action ); const QString fileName = exe->fileName(); if ( fileName.endsWith( QLatin1String(".pdf"), Qt::CaseInsensitive ) ) { d->openRelativeFile( fileName ); break; } // Albert: the only pdf i have that has that kind of link don't define // an application and use the fileName as the file to open QUrl url = d->giveAbsoluteUrl( fileName ); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl( url ); // Check executables if ( KRun::isExecutableFile( url, mime.name() ) ) { // Don't have any pdf that uses this code path, just a guess on how it should work if ( !exe->parameters().isEmpty() ) { url = d->giveAbsoluteUrl( exe->parameters() ); mime = db.mimeTypeForUrl( url ); if ( KRun::isExecutableFile( url, mime.name() ) ) { // this case is a link pointing to an executable with a parameter // that also is an executable, possibly a hand-crafted pdf KMessageBox::information( d->m_widget, i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that.") ); break; } } else { // this case is a link pointing to an executable with no parameters // core developers find unacceptable executing it even after asking the user KMessageBox::information( d->m_widget, i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that.") ); break; } } KService::Ptr ptr = KMimeTypeTrader::self()->preferredService( mime.name(), QStringLiteral("Application") ); if ( ptr ) { QList lst; lst.append( url ); KRun::runService( *ptr, lst, nullptr ); } else KMessageBox::information( d->m_widget, i18n( "No application found for opening file of mimetype %1.", mime.name() ) ); } break; case Action::DocAction: { const DocumentAction * docaction = static_cast< const DocumentAction * >( action ); switch( docaction->documentActionType() ) { case DocumentAction::PageFirst: setViewportPage( 0 ); break; case DocumentAction::PagePrev: if ( (*d->m_viewportIterator).pageNumber > 0 ) setViewportPage( (*d->m_viewportIterator).pageNumber - 1 ); break; case DocumentAction::PageNext: if ( (*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1 ) setViewportPage( (*d->m_viewportIterator).pageNumber + 1 ); break; case DocumentAction::PageLast: setViewportPage( d->m_pagesVector.count() - 1 ); break; case DocumentAction::HistoryBack: setPrevViewport(); break; case DocumentAction::HistoryForward: setNextViewport(); break; case DocumentAction::Quit: emit quit(); break; case DocumentAction::Presentation: emit linkPresentation(); break; case DocumentAction::EndPresentation: emit linkEndPresentation(); break; case DocumentAction::Find: emit linkFind(); break; case DocumentAction::GoToPage: emit linkGoToPage(); break; case DocumentAction::Close: emit close(); break; } } break; case Action::Browse: { const BrowseAction * browse = static_cast< const BrowseAction * >( action ); QString lilySource; int lilyRow = 0, lilyCol = 0; // if the url is a mailto one, invoke mailer if ( browse->url().scheme() == QLatin1String("mailto") ) { QDesktopServices::openUrl( browse->url() ); } else if ( extractLilyPondSourceReference( browse->url(), &lilySource, &lilyRow, &lilyCol ) ) { const SourceReference ref( lilySource, lilyRow, lilyCol ); processSourceReference( &ref ); } else { const QUrl url = browse->url(); // fix for #100366, documents with relative links that are the form of http:foo.pdf if ((url.scheme() == QLatin1String("http")) && url.host().isEmpty() && url.fileName().endsWith(QLatin1String("pdf"))) { d->openRelativeFile(url.fileName()); break; } // handle documents with relative path if ( d->m_url.isValid() ) { const QUrl realUrl = KIO::upUrl(d->m_url).resolved(url); // KRun autodeletes new KRun( realUrl, d->m_widget ); } } } break; case Action::Sound: { const SoundAction * linksound = static_cast< const SoundAction * >( action ); AudioPlayer::instance()->playSound( linksound->sound(), linksound ); } break; case Action::Script: { const ScriptAction * linkscript = static_cast< const ScriptAction * >( action ); if ( !d->m_scripter ) d->m_scripter = new Scripter( d ); d->m_scripter->execute( linkscript->scriptType(), linkscript->script() ); } break; case Action::Movie: emit processMovieAction( static_cast< const MovieAction * >( action ) ); break; case Action::Rendition: { const RenditionAction * linkrendition = static_cast< const RenditionAction * >( action ); if ( !linkrendition->script().isEmpty() ) { if ( !d->m_scripter ) d->m_scripter = new Scripter( d ); d->m_scripter->execute( linkrendition->scriptType(), linkrendition->script() ); } emit processRenditionAction( static_cast< const RenditionAction * >( action ) ); } break; case Action::BackendOpaque: { d->m_generator->opaqueAction( static_cast< const BackendOpaqueAction * >( action ) ); } break; } disconnect( connectionId ); if ( executeNextActions ) { - for ( const Action *a : action->nextActions() ) + const QVector nextActions = action->nextActions(); + for ( const Action *a : nextActions ) { processAction( a ); } } } void Document::processFormatAction( const Action * action, Okular::FormFieldText *fft ) { if ( action->actionType() != Action::Script ) { qCDebug( OkularCoreDebug ) << "Unsupported action type" << action->actionType() << "for formatting."; return; } // Lookup the page of the FormFieldText int foundPage = d->findFieldPageNumber( fft ); if ( foundPage == -1 ) { qCDebug( OkularCoreDebug ) << "Could not find page for formfield!"; return; } const QString unformattedText = fft->text(); std::shared_ptr< Event > event = Event::createFormatEvent( fft, d->m_pagesVector[foundPage] ); const ScriptAction * linkscript = static_cast< const ScriptAction * >( action ); d->executeScriptEvent( event, linkscript ); const QString formattedText = event->value().toString(); if ( formattedText != unformattedText ) { // We set the formattedText, because when we call refreshFormWidget // It will set the QLineEdit to this formattedText fft->setText( formattedText ); fft->setAppearanceText( formattedText ); emit refreshFormWidget( fft ); d->refreshPixmaps( foundPage ); // Then we make the form have the unformatted text, to use // in calculations and other things. fft->setText( unformattedText ); } else if ( fft->additionalAction( FormField::CalculateField ) ) { // When the field was calculated we need to refresh even // if the format script changed nothing. e.g. on error. // This is because the recalculateForms function delegated // the responsiblity for the refresh to us. emit refreshFormWidget( fft ); d->refreshPixmaps( foundPage ); } } void Document::processKeystrokeAction( const Action * action, Okular::FormFieldText *fft, bool &returnCode ) { if ( action->actionType() != Action::Script ) { qCDebug( OkularCoreDebug ) << "Unsupported action type" << action->actionType() << "for keystroke."; return; } // Lookup the page of the FormFieldText int foundPage = d->findFieldPageNumber( fft ); if ( foundPage == -1 ) { qCDebug( OkularCoreDebug ) << "Could not find page for formfield!"; return; } std::shared_ptr< Event > event = Event::createKeystrokeEvent( fft, d->m_pagesVector[foundPage] ); const ScriptAction * linkscript = static_cast< const ScriptAction * >( action ); d->executeScriptEvent( event, linkscript ); returnCode = event->returnCode(); } void Document::processFocusAction( const Action * action, Okular::FormField *field ) { if ( !action || action->actionType() != Action::Script ) return; // Lookup the page of the FormFieldText int foundPage = d->findFieldPageNumber( field ); if ( foundPage == -1 ) { qCDebug( OkularCoreDebug ) << "Could not find page for formfield!"; return; } std::shared_ptr< Event > event = Event::createFormFocusEvent( field, d->m_pagesVector[foundPage] ); const ScriptAction * linkscript = static_cast< const ScriptAction * >( action ); d->executeScriptEvent( event, linkscript ); } void Document::processValidateAction( const Action * action, Okular::FormFieldText *fft, bool &returnCode ) { if ( !action || action->actionType() != Action::Script ) return; // Lookup the page of the FormFieldText int foundPage = d->findFieldPageNumber( fft ); if ( foundPage == -1 ) { qCDebug( OkularCoreDebug ) << "Could not find page for formfield!"; return; } std::shared_ptr< Event > event = Event::createFormValidateEvent( fft, d->m_pagesVector[foundPage] ); const ScriptAction * linkscript = static_cast< const ScriptAction * >( action ); d->executeScriptEvent( event, linkscript ); returnCode = event->returnCode(); } void Document::processSourceReference( const SourceReference * ref ) { if ( !ref ) return; const QUrl url = d->giveAbsoluteUrl( ref->fileName() ); if ( !url.isLocalFile() ) { qCDebug(OkularCoreDebug) << url.url() << "is not a local file."; return; } const QString absFileName = url.toLocalFile(); if ( !QFile::exists( absFileName ) ) { qCDebug(OkularCoreDebug) << "No such file:" << absFileName; return; } bool handled = false; emit sourceReferenceActivated(absFileName, ref->row(), ref->column(), &handled); if(handled) { return; } static QHash< int, QString > editors; // init the editors table if empty (on first run, usually) if ( editors.isEmpty() ) { editors = buildEditorsMap(); } QHash< int, QString >::const_iterator it = editors.constFind( SettingsCore::externalEditor() ); QString p; if ( it != editors.constEnd() ) p = *it; else p = SettingsCore::externalEditorCommand(); // custom editor not yet configured if ( p.isEmpty() ) return; // manually append the %f placeholder if not specified if ( p.indexOf( QLatin1String( "%f" ) ) == -1 ) p.append( QLatin1String( " %f" ) ); // replacing the placeholders QHash< QChar, QString > map; map.insert( QLatin1Char ( 'f' ), absFileName ); map.insert( QLatin1Char ( 'c' ), QString::number( ref->column() ) ); map.insert( QLatin1Char ( 'l' ), QString::number( ref->row() ) ); const QString cmd = KMacroExpander::expandMacrosShellQuote( p, map ); if ( cmd.isEmpty() ) return; const QStringList args = KShell::splitArgs( cmd ); if ( args.isEmpty() ) return; KProcess::startDetached( args ); } const SourceReference * Document::dynamicSourceReference( int pageNr, double absX, double absY ) { if ( !d->m_synctex_scanner ) return nullptr; const QSizeF dpi = d->m_generator->dpi(); if (synctex_edit_query(d->m_synctex_scanner, pageNr + 1, absX * 72. / dpi.width(), absY * 72. / dpi.height()) > 0) { synctex_node_p node; // TODO what should we do if there is really more than one node? while (( node = synctex_scanner_next_result( d->m_synctex_scanner ) )) { int line = synctex_node_line(node); int col = synctex_node_column(node); // column extraction does not seem to be implemented in synctex so far. set the SourceReference default value. if ( col == -1 ) { col = 0; } const char *name = synctex_scanner_get_name( d->m_synctex_scanner, synctex_node_tag( node ) ); return new Okular::SourceReference( QFile::decodeName( name ), line, col ); } } return nullptr; } Document::PrintingType Document::printingSupport() const { if ( d->m_generator ) { if ( d->m_generator->hasFeature( Generator::PrintNative ) ) { return NativePrinting; } #ifndef Q_OS_WIN if ( d->m_generator->hasFeature( Generator::PrintPostscript ) ) { return PostscriptPrinting; } #endif } return NoPrinting; } bool Document::supportsPrintToFile() const { return d->m_generator ? d->m_generator->hasFeature( Generator::PrintToFile ) : false; } bool Document::print( QPrinter &printer ) { return d->m_generator ? d->m_generator->print( printer ) : false; } QString Document::printError() const { Okular::Generator::PrintError err = Generator::UnknownPrintError; if ( d->m_generator ) { QMetaObject::invokeMethod( d->m_generator, "printError", Qt::DirectConnection, Q_RETURN_ARG(Okular::Generator::PrintError, err) ); } Q_ASSERT( err != Generator::NoPrintError ); switch ( err ) { case Generator::TemporaryFileOpenPrintError: return i18n( "Could not open a temporary file" ); case Generator::FileConversionPrintError: return i18n( "Print conversion failed" ); case Generator::PrintingProcessCrashPrintError: return i18n( "Printing process crashed" ); case Generator::PrintingProcessStartPrintError: return i18n( "Printing process could not start" ); case Generator::PrintToFilePrintError: return i18n( "Printing to file failed" ); case Generator::InvalidPrinterStatePrintError: return i18n( "Printer was in invalid state" ); case Generator::UnableToFindFilePrintError: return i18n( "Unable to find file to print" ); case Generator::NoFileToPrintError: return i18n( "There was no file to print" ); case Generator::NoBinaryToPrintError: return i18n( "Could not find a suitable binary for printing. Make sure CUPS lpr binary is available" ); case Generator::InvalidPageSizePrintError: return i18n( "The page print size is invalid" ); case Generator::NoPrintError: return QString(); case Generator::UnknownPrintError: return QString(); } return QString(); } QWidget* Document::printConfigurationWidget() const { if ( d->m_generator ) { PrintInterface * iface = qobject_cast< Okular::PrintInterface * >( d->m_generator ); return iface ? iface->printConfigurationWidget() : nullptr; } else return nullptr; } void Document::fillConfigDialog( KConfigDialog * dialog ) { if ( !dialog ) return; // We know it's a BackendConfigDialog, but check anyway BackendConfigDialog *bcd = dynamic_cast( dialog ); if ( !bcd ) return; // ensure that we have all the generators with settings loaded QVector offers = DocumentPrivate::configurableGenerators(); d->loadServiceList( offers ); // We want the generators to be sorted by name so let's fill in a QMap // this sorts by internal id which is not awesome, but at least the sorting // is stable between runs that before it wasn't QMap sortedGenerators; QHash< QString, GeneratorInfo >::iterator it = d->m_loadedGenerators.begin(); QHash< QString, GeneratorInfo >::iterator itEnd = d->m_loadedGenerators.end(); for ( ; it != itEnd; ++it ) { sortedGenerators.insert(it.key(), it.value()); } bool pagesAdded = false; QMap< QString, GeneratorInfo >::iterator sit = sortedGenerators.begin(); QMap< QString, GeneratorInfo >::iterator sitEnd = sortedGenerators.end(); for ( ; sit != sitEnd; ++sit ) { Okular::ConfigInterface * iface = d->generatorConfig( sit.value() ); if ( iface ) { iface->addPages( dialog ); pagesAdded = true; if ( sit.value().generator == d->m_generator ) { const int rowCount = bcd->thePageWidget()->model()->rowCount(); KPageView *view = bcd->thePageWidget(); view->setCurrentPage( view->model()->index( rowCount - 1, 0 ) ); } } } if ( pagesAdded ) { connect( dialog, &KConfigDialog::settingsChanged, this, [this] { d->slotGeneratorConfigChanged(); } ); } } QVector DocumentPrivate::configurableGenerators() { const QVector available = availableGenerators(); QVector result; for (const KPluginMetaData& md : available) { if (md.rawData()[QStringLiteral("X-KDE-okularHasInternalSettings")].toBool()) { result << md; } } return result; } KPluginMetaData Document::generatorInfo() const { if (!d->m_generator) return KPluginMetaData(); auto genIt = d->m_loadedGenerators.constFind(d->m_generatorName); Q_ASSERT(genIt != d->m_loadedGenerators.constEnd()); return genIt.value().metadata; } int Document::configurableGenerators() const { return DocumentPrivate::configurableGenerators().size(); } QStringList Document::supportedMimeTypes() const { // TODO: make it a static member of DocumentPrivate? QStringList result = d->m_supportedMimeTypes; if (result.isEmpty()) { const QVector available = DocumentPrivate::availableGenerators(); for (const KPluginMetaData& md : available) { result << md.mimeTypes(); } // Remove duplicate mimetypes represented by different names QMimeDatabase mimeDatabase; QSet uniqueMimetypes; - for (const QString &mimeName : result) { + for (const QString &mimeName : qAsConst(result)) { uniqueMimetypes.insert(mimeDatabase.mimeTypeForName(mimeName)); } result.clear(); for (const QMimeType &mimeType : uniqueMimetypes) { result.append(mimeType.name()); } // Add the Okular archive mimetype result << QStringLiteral("application/vnd.kde.okular-archive"); // Sorting by mimetype name doesn't make a ton of sense, // but ensures that the list is ordered the same way every time std::sort(result.begin(), result.end()); d->m_supportedMimeTypes = result; } return result; } bool Document::canSwapBackingFile() const { if ( !d->m_generator ) return false; return d->m_generator->hasFeature( Generator::SwapBackingFile ); } bool Document::swapBackingFile( const QString &newFileName, const QUrl &url ) { if ( !d->m_generator ) return false; if ( !d->m_generator->hasFeature( Generator::SwapBackingFile ) ) return false; // Save metadata about the file we're about to close d->saveDocumentInfo(); d->clearAndWaitForRequests(); qCDebug(OkularCoreDebug) << "Swapping backing file to" << newFileName; QVector< Page * > newPagesVector; Generator::SwapBackingFileResult result = d->m_generator->swapBackingFile( newFileName, newPagesVector ); if (result != Generator::SwapBackingFileError) { QLinkedList< ObjectRect* > rectsToDelete; QLinkedList< Annotation* > annotationsToDelete; QSet< PagePrivate* > pagePrivatesToDelete; if (result == Generator::SwapBackingFileReloadInternalData) { // Here we need to replace everything that the old generator // had created with what the new one has without making it look like // we have actually closed and opened the file again // Simple sanity check if (newPagesVector.count() != d->m_pagesVector.count()) return false; // Update the undo stack contents for (int i = 0; i < d->m_undoStack->count(); ++i) { // Trust me on the const_cast ^_^ QUndoCommand *uc = const_cast( d->m_undoStack->command( i ) ); if (OkularUndoCommand *ouc = dynamic_cast( uc )) { const bool success = ouc->refreshInternalPageReferences( newPagesVector ); if ( !success ) { qWarning() << "Document::swapBackingFile: refreshInternalPageReferences failed" << ouc; return false; } } else { qWarning() << "Document::swapBackingFile: Unhandled undo command" << uc; return false; } } for (int i = 0; i < d->m_pagesVector.count(); ++i) { // switch the PagePrivate* from newPage to oldPage // this way everyone still holding Page* doesn't get // disturbed by it Page *oldPage = d->m_pagesVector[i]; Page *newPage = newPagesVector[i]; newPage->d->adoptGeneratedContents(oldPage->d); pagePrivatesToDelete << oldPage->d; oldPage->d = newPage->d; oldPage->d->m_page = oldPage; oldPage->d->m_doc = d; newPage->d = nullptr; annotationsToDelete << oldPage->m_annotations; rectsToDelete << oldPage->m_rects; oldPage->m_annotations = newPage->m_annotations; oldPage->m_rects = newPage->m_rects; } qDeleteAll( newPagesVector ); } d->m_url = url; d->m_docFileName = newFileName; d->updateMetadataXmlNameAndDocSize(); d->m_bookmarkManager->setUrl( d->m_url ); d->m_documentInfo = DocumentInfo(); d->m_documentInfoAskedKeys.clear(); if ( d->m_synctex_scanner ) { synctex_scanner_free( d->m_synctex_scanner ); d->m_synctex_scanner = synctex_scanner_new_with_output_file( QFile::encodeName( newFileName ).constData(), nullptr, 1); if ( !d->m_synctex_scanner && QFile::exists(newFileName + QLatin1String( "sync" ) ) ) { d->loadSyncFile(newFileName); } } foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::UrlChanged ) ); qDeleteAll( annotationsToDelete ); qDeleteAll( rectsToDelete ); qDeleteAll( pagePrivatesToDelete ); return true; } else { return false; } } bool Document::swapBackingFileArchive( const QString &newFileName, const QUrl &url ) { qCDebug(OkularCoreDebug) << "Swapping backing archive to" << newFileName; ArchiveData *newArchive = DocumentPrivate::unpackDocumentArchive( newFileName ); if ( !newArchive ) return false; const QString tempFileName = newArchive->document.fileName(); const bool success = swapBackingFile( tempFileName, url ); if ( success ) { delete d->m_archiveData; d->m_archiveData = newArchive; } return success; } void Document::setHistoryClean( bool clean ) { if ( clean ) d->m_undoStack->setClean(); else d->m_undoStack->resetClean(); } bool Document::canSaveChanges() const { if ( !d->m_generator ) return false; Q_ASSERT( !d->m_generatorName.isEmpty() ); QHash< QString, GeneratorInfo >::iterator genIt = d->m_loadedGenerators.find( d->m_generatorName ); Q_ASSERT( genIt != d->m_loadedGenerators.end() ); SaveInterface* saveIface = d->generatorSave( genIt.value() ); if ( !saveIface ) return false; return saveIface->supportsOption( SaveInterface::SaveChanges ); } bool Document::canSaveChanges( SaveCapability cap ) const { switch ( cap ) { case SaveFormsCapability: /* Assume that if the generator supports saving, forms can be saved. * We have no means to actually query the generator at the moment * TODO: Add some method to query the generator in SaveInterface */ return canSaveChanges(); case SaveAnnotationsCapability: return d->canAddAnnotationsNatively(); } return false; } bool Document::saveChanges( const QString &fileName ) { QString errorText; return saveChanges( fileName, &errorText ); } bool Document::saveChanges( const QString &fileName, QString *errorText ) { if ( !d->m_generator || fileName.isEmpty() ) return false; Q_ASSERT( !d->m_generatorName.isEmpty() ); QHash< QString, GeneratorInfo >::iterator genIt = d->m_loadedGenerators.find( d->m_generatorName ); Q_ASSERT( genIt != d->m_loadedGenerators.end() ); SaveInterface* saveIface = d->generatorSave( genIt.value() ); if ( !saveIface || !saveIface->supportsOption( SaveInterface::SaveChanges ) ) return false; return saveIface->save( fileName, SaveInterface::SaveChanges, errorText ); } void Document::registerView( View *view ) { if ( !view ) return; Document *viewDoc = view->viewDocument(); if ( viewDoc ) { // check if already registered for this document if ( viewDoc == this ) return; viewDoc->unregisterView( view ); } d->m_views.insert( view ); view->d_func()->document = d; } void Document::unregisterView( View *view ) { if ( !view ) return; Document *viewDoc = view->viewDocument(); if ( !viewDoc || viewDoc != this ) return; view->d_func()->document = nullptr; d->m_views.remove( view ); } QByteArray Document::fontData(const FontInfo &font) const { QByteArray result; if (d->m_generator) { QMetaObject::invokeMethod(d->m_generator, "requestFontData", Qt::DirectConnection, Q_ARG(Okular::FontInfo, font), Q_ARG(QByteArray *, &result)); } return result; } ArchiveData *DocumentPrivate::unpackDocumentArchive( const QString &archivePath ) { QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( archivePath, QMimeDatabase::MatchExtension ); if ( !mime.inherits( QStringLiteral("application/vnd.kde.okular-archive") ) ) return nullptr; KZip okularArchive( archivePath ); if ( !okularArchive.open( QIODevice::ReadOnly ) ) return nullptr; const KArchiveDirectory * mainDir = okularArchive.directory(); // Check the archive doesn't have folders, we don't create them when saving the archive // and folders mean paths and paths mean path traversal issues - for ( const QString &entry : mainDir->entries() ) + const QStringList mainDirEntries = mainDir->entries(); + for ( const QString &entry : mainDirEntries ) { if ( mainDir->entry( entry )->isDirectory() ) { qWarning() << "Warning: Found a directory inside" << archivePath << " - Okular does not create files like that so it is most probably forged."; return nullptr; } } const KArchiveEntry * mainEntry = mainDir->entry( QStringLiteral("content.xml") ); if ( !mainEntry || !mainEntry->isFile() ) return nullptr; std::unique_ptr< QIODevice > mainEntryDevice( static_cast< const KZipFileEntry * >( mainEntry )->createDevice() ); QDomDocument doc; if ( !doc.setContent( mainEntryDevice.get() ) ) return nullptr; mainEntryDevice.reset(); QDomElement root = doc.documentElement(); if ( root.tagName() != QLatin1String("OkularArchive") ) return nullptr; QString documentFileName; QString metadataFileName; QDomElement el = root.firstChild().toElement(); for ( ; !el.isNull(); el = el.nextSibling().toElement() ) { if ( el.tagName() == QLatin1String("Files") ) { QDomElement fileEl = el.firstChild().toElement(); for ( ; !fileEl.isNull(); fileEl = fileEl.nextSibling().toElement() ) { if ( fileEl.tagName() == QLatin1String("DocumentFileName") ) documentFileName = fileEl.text(); else if ( fileEl.tagName() == QLatin1String("MetadataFileName") ) metadataFileName = fileEl.text(); } } } if ( documentFileName.isEmpty() ) return nullptr; const KArchiveEntry * docEntry = mainDir->entry( documentFileName ); if ( !docEntry || !docEntry->isFile() ) return nullptr; std::unique_ptr< ArchiveData > archiveData( new ArchiveData() ); const int dotPos = documentFileName.indexOf( QLatin1Char('.') ); if ( dotPos != -1 ) archiveData->document.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX") + documentFileName.mid(dotPos)); if ( !archiveData->document.open() ) return nullptr; archiveData->originalFileName = documentFileName; { std::unique_ptr< QIODevice > docEntryDevice( static_cast< const KZipFileEntry * >( docEntry )->createDevice() ); copyQIODevice( docEntryDevice.get(), &archiveData->document ); archiveData->document.close(); } const KArchiveEntry * metadataEntry = mainDir->entry( metadataFileName ); if ( metadataEntry && metadataEntry->isFile() ) { std::unique_ptr< QIODevice > metadataEntryDevice( static_cast< const KZipFileEntry * >( metadataEntry )->createDevice() ); archiveData->metadataFile.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX.xml")); if ( archiveData->metadataFile.open() ) { copyQIODevice( metadataEntryDevice.get(), &archiveData->metadataFile ); archiveData->metadataFile.close(); } } return archiveData.release(); } Document::OpenResult Document::openDocumentArchive( const QString & docFile, const QUrl & url, const QString & password ) { d->m_archiveData = DocumentPrivate::unpackDocumentArchive( docFile ); if ( !d->m_archiveData ) return OpenError; const QString tempFileName = d->m_archiveData->document.fileName(); QMimeDatabase db; const QMimeType docMime = db.mimeTypeForFile( tempFileName, QMimeDatabase::MatchExtension ); const OpenResult ret = openDocument( tempFileName, url, docMime, password ); if ( ret != OpenSuccess ) { delete d->m_archiveData; d->m_archiveData = nullptr; } return ret; } bool Document::saveDocumentArchive( const QString &fileName ) { if ( !d->m_generator ) return false; /* If we opened an archive, use the name of original file (eg foo.pdf) * instead of the archive's one (eg foo.okular) */ QString docFileName = d->m_archiveData ? d->m_archiveData->originalFileName : d->m_url.fileName(); if ( docFileName == QLatin1String( "-" ) ) return false; QString docPath = d->m_docFileName; const QFileInfo fi( docPath ); if ( fi.isSymLink() ) docPath = fi.symLinkTarget(); KZip okularArchive( fileName ); if ( !okularArchive.open( QIODevice::WriteOnly ) ) return false; const KUser user; #ifndef Q_OS_WIN const KUserGroup userGroup( user.groupId() ); #else const KUserGroup userGroup( QString( "" ) ); #endif QDomDocument contentDoc( QStringLiteral("OkularArchive") ); QDomProcessingInstruction xmlPi = contentDoc.createProcessingInstruction( QStringLiteral( "xml" ), QStringLiteral( "version=\"1.0\" encoding=\"utf-8\"" ) ); contentDoc.appendChild( xmlPi ); QDomElement root = contentDoc.createElement( QStringLiteral("OkularArchive") ); contentDoc.appendChild( root ); QDomElement filesNode = contentDoc.createElement( QStringLiteral("Files") ); root.appendChild( filesNode ); QDomElement fileNameNode = contentDoc.createElement( QStringLiteral("DocumentFileName") ); filesNode.appendChild( fileNameNode ); fileNameNode.appendChild( contentDoc.createTextNode( docFileName ) ); QDomElement metadataFileNameNode = contentDoc.createElement( QStringLiteral("MetadataFileName") ); filesNode.appendChild( metadataFileNameNode ); metadataFileNameNode.appendChild( contentDoc.createTextNode( QStringLiteral("metadata.xml") ) ); // If the generator can save annotations natively, do it QTemporaryFile modifiedFile; bool annotationsSavedNatively = false; bool formsSavedNatively = false; if ( d->canAddAnnotationsNatively() || canSaveChanges( SaveFormsCapability ) ) { if ( !modifiedFile.open() ) return false; const QString modifiedFileName = modifiedFile.fileName(); modifiedFile.close(); // We're only interested in the file name QString errorText; if ( saveChanges( modifiedFileName, &errorText ) ) { docPath = modifiedFileName; // Save this instead of the original file annotationsSavedNatively = d->canAddAnnotationsNatively(); formsSavedNatively = canSaveChanges( SaveFormsCapability ); } else { qCWarning(OkularCoreDebug) << "saveChanges failed: " << errorText; qCDebug(OkularCoreDebug) << "Falling back to saving a copy of the original file"; } } PageItems saveWhat = None; if ( !annotationsSavedNatively ) saveWhat |= AnnotationPageItems; if ( !formsSavedNatively ) saveWhat |= FormFieldPageItems; QTemporaryFile metadataFile; if ( !d->savePageDocumentInfo( &metadataFile, saveWhat ) ) return false; const QByteArray contentDocXml = contentDoc.toByteArray(); const mode_t perm = 0100644; okularArchive.writeFile( QStringLiteral("content.xml"), contentDocXml, perm, user.loginName(), userGroup.name() ); okularArchive.addLocalFile( docPath, docFileName ); okularArchive.addLocalFile( metadataFile.fileName(), QStringLiteral("metadata.xml") ); if ( !okularArchive.close() ) return false; return true; } bool Document::extractArchivedFile( const QString &destFileName ) { if ( !d->m_archiveData ) return false; // Remove existing file, if present (QFile::copy doesn't overwrite by itself) QFile::remove( destFileName ); return d->m_archiveData->document.copy( destFileName ); } QPrinter::Orientation Document::orientation() const { double width, height; int landscape, portrait; const Okular::Page *currentPage; // if some pages are landscape and others are not, the most common wins, as // QPrinter does not accept a per-page setting landscape = 0; portrait = 0; for (uint i = 0; i < pages(); i++) { currentPage = page(i); width = currentPage->width(); height = currentPage->height(); if (currentPage->orientation() == Okular::Rotation90 || currentPage->orientation() == Okular::Rotation270) qSwap(width, height); if (width > height) landscape++; else portrait++; } return (landscape > portrait) ? QPrinter::Landscape : QPrinter::Portrait; } void Document::setAnnotationEditingEnabled( bool enable ) { d->m_annotationEditingEnabled = enable; foreachObserver( notifySetup( d->m_pagesVector, 0 ) ); } void Document::walletDataForFile( const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey ) const { if (d->m_generator) { d->m_generator->walletDataForFile( fileName, walletName, walletFolder, walletKey ); } else if (d->m_walletGenerator) { d->m_walletGenerator->walletDataForFile( fileName, walletName, walletFolder, walletKey ); } } bool Document::isDocdataMigrationNeeded() const { return d->m_docdataMigrationNeeded; } void Document::docdataMigrationDone() { if (d->m_docdataMigrationNeeded) { d->m_docdataMigrationNeeded = false; foreachObserver( notifySetup( d->m_pagesVector, 0 ) ); } } QAbstractItemModel * Document::layersModel() const { return d->m_generator ? d->m_generator->layersModel() : nullptr; } QString Document::openError() const { return d->m_openError; } QByteArray Document::requestSignedRevisionData( const Okular::SignatureInfo &info ) { QFile f( d->m_docFileName ); if ( !f.open( QIODevice::ReadOnly ) ) { KMessageBox::error( nullptr, i18n("Could not open '%1'. File does not exist", d->m_docFileName ) ); return {}; } const QList byteRange = info.signedRangeBounds(); f.seek( byteRange.first() ); QByteArray data; QDataStream stream( &data, QIODevice::WriteOnly ); stream << f.read( byteRange.last() - byteRange.first() ); f.close(); return data; } void Document::refreshPixmaps( int pageNumber ) { d->refreshPixmaps( pageNumber ); } void DocumentPrivate::executeScript( const QString &function ) { if( !m_scripter ) m_scripter = new Scripter( this ); m_scripter->execute( JavaScript, function ); } void DocumentPrivate::requestDone( PixmapRequest * req ) { if ( !req ) return; if ( !m_generator || m_closingLoop ) { m_pixmapRequestsMutex.lock(); m_executingPixmapRequests.removeAll( req ); m_pixmapRequestsMutex.unlock(); delete req; if ( m_closingLoop ) m_closingLoop->exit(); return; } #ifndef NDEBUG if ( !m_generator->canGeneratePixmap() ) qCDebug(OkularCoreDebug) << "requestDone with generator not in READY state."; #endif if ( !req->shouldAbortRender() ) { // [MEM] 1.1 find and remove a previous entry for the same page and id QLinkedList< AllocatedPixmap * >::iterator aIt = m_allocatedPixmaps.begin(); QLinkedList< AllocatedPixmap * >::iterator aEnd = m_allocatedPixmaps.end(); for ( ; aIt != aEnd; ++aIt ) if ( (*aIt)->page == req->pageNumber() && (*aIt)->observer == req->observer() ) { AllocatedPixmap * p = *aIt; m_allocatedPixmaps.erase( aIt ); m_allocatedPixmapsTotalMemory -= p->memory; delete p; break; } DocumentObserver *observer = req->observer(); if ( m_observers.contains(observer) ) { // [MEM] 1.2 append memory allocation descriptor to the FIFO qulonglong memoryBytes = 0; const TilesManager *tm = req->d->tilesManager(); if ( tm ) memoryBytes = tm->totalMemory(); else memoryBytes = 4 * req->width() * req->height(); AllocatedPixmap * memoryPage = new AllocatedPixmap( req->observer(), req->pageNumber(), memoryBytes ); m_allocatedPixmaps.append( memoryPage ); m_allocatedPixmapsTotalMemory += memoryBytes; // 2. notify an observer that its pixmap changed observer->notifyPageChanged( req->pageNumber(), DocumentObserver::Pixmap ); } #ifndef NDEBUG else qCWarning(OkularCoreDebug) << "Receiving a done request for the defunct observer" << observer; #endif } // 3. delete request m_pixmapRequestsMutex.lock(); m_executingPixmapRequests.removeAll( req ); m_pixmapRequestsMutex.unlock(); delete req; // 4. start a new generation if some is pending m_pixmapRequestsMutex.lock(); bool hasPixmaps = !m_pixmapRequestsStack.isEmpty(); m_pixmapRequestsMutex.unlock(); if ( hasPixmaps ) sendGeneratorPixmapRequest(); } void DocumentPrivate::setPageBoundingBox( int page, const NormalizedRect& boundingBox ) { Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; if ( kp->boundingBox() == boundingBox ) return; kp->setBoundingBox( boundingBox ); // notify observers about the change foreachObserverD( notifyPageChanged( page, DocumentObserver::BoundingBox ) ); // TODO: For generators that generate the bbox by pixmap scanning, if the first generated pixmap is very small, the bounding box will forever be inaccurate. // TODO: Crop computation should also consider annotations, actions, etc. to make sure they're not cropped away. // TODO: Help compute bounding box for generators that create a QPixmap without a QImage, like text and plucker. // TODO: Don't compute the bounding box if no one needs it (e.g., Trim Borders is off). } void DocumentPrivate::calculateMaxTextPages() { int multipliers = qMax(1, qRound(getTotalMemory() / 536870912.0)); // 512 MB switch (SettingsCore::memoryLevel()) { case SettingsCore::EnumMemoryLevel::Low: m_maxAllocatedTextPages = multipliers * 2; break; case SettingsCore::EnumMemoryLevel::Normal: m_maxAllocatedTextPages = multipliers * 50; break; case SettingsCore::EnumMemoryLevel::Aggressive: m_maxAllocatedTextPages = multipliers * 250; break; case SettingsCore::EnumMemoryLevel::Greedy: m_maxAllocatedTextPages = multipliers * 1250; break; } } void DocumentPrivate::textGenerationDone( Page *page ) { if ( !m_pageController ) return; // 1. If we reached the cache limit, delete the first text page from the fifo if (m_allocatedTextPagesFifo.size() == m_maxAllocatedTextPages) { int pageToKick = m_allocatedTextPagesFifo.takeFirst(); if (pageToKick != page->number()) // this should never happen but better be safe than sorry { m_pagesVector.at(pageToKick)->setTextPage( nullptr ); // deletes the textpage } } // 2. Add the page to the fifo of generated text pages m_allocatedTextPagesFifo.append( page->number() ); } void Document::setRotation( int r ) { d->setRotationInternal( r, true ); } void DocumentPrivate::setRotationInternal( int r, bool notify ) { Rotation rotation = (Rotation)r; if ( !m_generator || ( m_rotation == rotation ) ) return; // tell the pages to rotate QVector< Okular::Page * >::const_iterator pIt = m_pagesVector.constBegin(); QVector< Okular::Page * >::const_iterator pEnd = m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->rotateAt( rotation ); if ( notify ) { // notify the generator that the current rotation has changed m_generator->rotationChanged( rotation, m_rotation ); } // set the new rotation m_rotation = rotation; if ( notify ) { foreachObserverD( notifySetup( m_pagesVector, DocumentObserver::NewLayoutForPages ) ); foreachObserverD( notifyContentsCleared( DocumentObserver::Pixmap | DocumentObserver::Highlights | DocumentObserver::Annotations ) ); } qCDebug(OkularCoreDebug) << "Rotated:" << r; } void Document::setPageSize( const PageSize &size ) { if ( !d->m_generator || !d->m_generator->hasFeature( Generator::PageSizes ) ) return; if ( d->m_pageSizes.isEmpty() ) d->m_pageSizes = d->m_generator->pageSizes(); int sizeid = d->m_pageSizes.indexOf( size ); if ( sizeid == -1 ) return; // tell the pages to change size QVector< Okular::Page * >::const_iterator pIt = d->m_pagesVector.constBegin(); QVector< Okular::Page * >::const_iterator pEnd = d->m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->changeSize( size ); // clear 'memory allocation' descriptors qDeleteAll( d->m_allocatedPixmaps ); d->m_allocatedPixmaps.clear(); d->m_allocatedPixmapsTotalMemory = 0; // notify the generator that the current page size has changed d->m_generator->pageSizeChanged( size, d->m_pageSize ); // set the new page size d->m_pageSize = size; foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::NewLayoutForPages ) ); foreachObserver( notifyContentsCleared( DocumentObserver::Pixmap | DocumentObserver::Highlights ) ); qCDebug(OkularCoreDebug) << "New PageSize id:" << sizeid; } /** DocumentViewport **/ DocumentViewport::DocumentViewport( int n ) : pageNumber( n ) { // default settings rePos.enabled = false; rePos.normalizedX = 0.5; rePos.normalizedY = 0.0; rePos.pos = Center; autoFit.enabled = false; autoFit.width = false; autoFit.height = false; } DocumentViewport::DocumentViewport( const QString & xmlDesc ) : pageNumber( -1 ) { // default settings (maybe overridden below) rePos.enabled = false; rePos.normalizedX = 0.5; rePos.normalizedY = 0.0; rePos.pos = Center; autoFit.enabled = false; autoFit.width = false; autoFit.height = false; // check for string presence if ( xmlDesc.isEmpty() ) return; // decode the string bool ok; int field = 0; QString token = xmlDesc.section( QLatin1Char(';'), field, field ); while ( !token.isEmpty() ) { // decode the current token if ( field == 0 ) { pageNumber = token.toInt( &ok ); if ( !ok ) return; } else if ( token.startsWith( QLatin1String("C1") ) ) { rePos.enabled = true; rePos.normalizedX = token.section( QLatin1Char(':'), 1, 1 ).toDouble(); rePos.normalizedY = token.section( QLatin1Char(':'), 2, 2 ).toDouble(); rePos.pos = Center; } else if ( token.startsWith( QLatin1String("C2") ) ) { rePos.enabled = true; rePos.normalizedX = token.section( QLatin1Char(':'), 1, 1 ).toDouble(); rePos.normalizedY = token.section( QLatin1Char(':'), 2, 2 ).toDouble(); if (token.section( QLatin1Char(':'), 3, 3 ).toInt() == 1) rePos.pos = Center; else rePos.pos = TopLeft; } else if ( token.startsWith( QLatin1String("AF1") ) ) { autoFit.enabled = true; autoFit.width = token.section( QLatin1Char(':'), 1, 1 ) == QLatin1String("T"); autoFit.height = token.section( QLatin1Char(':'), 2, 2 ) == QLatin1String("T"); } // proceed tokenizing string field++; token = xmlDesc.section( QLatin1Char(';'), field, field ); } } QString DocumentViewport::toString() const { // start string with page number QString s = QString::number( pageNumber ); // if has center coordinates, save them on string if ( rePos.enabled ) s += QStringLiteral( ";C2:" ) + QString::number( rePos.normalizedX ) + QLatin1Char(':') + QString::number( rePos.normalizedY ) + QLatin1Char(':') + QString::number( rePos.pos ); // if has autofit enabled, save its state on string if ( autoFit.enabled ) s += QStringLiteral( ";AF1:" ) + (autoFit.width ? QLatin1Char('T') : QLatin1Char('F')) + QLatin1Char(':') + (autoFit.height ? QLatin1Char('T') : QLatin1Char('F')); return s; } bool DocumentViewport::isValid() const { return pageNumber >= 0; } bool DocumentViewport::operator==( const DocumentViewport & other ) const { bool equal = ( pageNumber == other.pageNumber ) && ( rePos.enabled == other.rePos.enabled ) && ( autoFit.enabled == other.autoFit.enabled ); if ( !equal ) return false; if ( rePos.enabled && (( rePos.normalizedX != other.rePos.normalizedX) || ( rePos.normalizedY != other.rePos.normalizedY ) || rePos.pos != other.rePos.pos) ) return false; if ( autoFit.enabled && (( autoFit.width != other.autoFit.width ) || ( autoFit.height != other.autoFit.height )) ) return false; return true; } bool DocumentViewport::operator<( const DocumentViewport & other ) const { // TODO: Check autoFit and Position if ( pageNumber != other.pageNumber ) return pageNumber < other.pageNumber; if ( !rePos.enabled && other.rePos.enabled ) return true; if ( !other.rePos.enabled ) return false; if ( rePos.normalizedY != other.rePos.normalizedY ) return rePos.normalizedY < other.rePos.normalizedY; return rePos.normalizedX < other.rePos.normalizedX; } /** DocumentInfo **/ DocumentInfo::DocumentInfo() : d(new DocumentInfoPrivate()) { } DocumentInfo::DocumentInfo(const DocumentInfo &info) : d(new DocumentInfoPrivate()) { *this = info; } DocumentInfo& DocumentInfo::operator=(const DocumentInfo &info) { if (this != &info) { d->values = info.d->values; d->titles = info.d->titles; } return *this; } DocumentInfo::~DocumentInfo() { delete d; } void DocumentInfo::set( const QString &key, const QString &value, const QString &title ) { d->values[ key ] = value; d->titles[ key ] = title; } void DocumentInfo::set( Key key, const QString &value ) { d->values[ getKeyString( key ) ] = value; } QStringList DocumentInfo::keys() const { return d->values.keys(); } QString DocumentInfo::get( Key key ) const { return get( getKeyString( key ) ); } QString DocumentInfo::get( const QString &key ) const { return d->values[ key ]; } QString DocumentInfo::getKeyString( Key key ) //const { switch ( key ) { case Title: return QStringLiteral("title"); break; case Subject: return QStringLiteral("subject"); break; case Description: return QStringLiteral("description"); break; case Author: return QStringLiteral("author"); break; case Creator: return QStringLiteral("creator"); break; case Producer: return QStringLiteral("producer"); break; case Copyright: return QStringLiteral("copyright"); break; case Pages: return QStringLiteral("pages"); break; case CreationDate: return QStringLiteral("creationDate"); break; case ModificationDate: return QStringLiteral("modificationDate"); break; case MimeType: return QStringLiteral("mimeType"); break; case Category: return QStringLiteral("category"); break; case Keywords: return QStringLiteral("keywords"); break; case FilePath: return QStringLiteral("filePath"); break; case DocumentSize: return QStringLiteral("documentSize"); break; case PagesSize: return QStringLiteral("pageSize"); break; default: qCWarning(OkularCoreDebug) << "Unknown" << key; return QString(); break; } } DocumentInfo::Key DocumentInfo::getKeyFromString( const QString &key ) //const { if (key == QLatin1String("title")) return Title; else if (key == QLatin1String("subject")) return Subject; else if (key == QLatin1String("description")) return Description; else if (key == QLatin1String("author")) return Author; else if (key == QLatin1String("creator")) return Creator; else if (key == QLatin1String("producer")) return Producer; else if (key == QLatin1String("copyright")) return Copyright; else if (key == QLatin1String("pages")) return Pages; else if (key == QLatin1String("creationDate")) return CreationDate; else if (key == QLatin1String("modificationDate")) return ModificationDate; else if (key == QLatin1String("mimeType")) return MimeType; else if (key == QLatin1String("category")) return Category; else if (key == QLatin1String("keywords")) return Keywords; else if (key == QLatin1String("filePath")) return FilePath; else if (key == QLatin1String("documentSize")) return DocumentSize; else if (key == QLatin1String("pageSize")) return PagesSize; else return Invalid; } QString DocumentInfo::getKeyTitle( Key key ) //const { switch ( key ) { case Title: return i18n( "Title" ); break; case Subject: return i18n( "Subject" ); break; case Description: return i18n( "Description" ); break; case Author: return i18n( "Author" ); break; case Creator: return i18n( "Creator" ); break; case Producer: return i18n( "Producer" ); break; case Copyright: return i18n( "Copyright" ); break; case Pages: return i18n( "Pages" ); break; case CreationDate: return i18n( "Created" ); break; case ModificationDate: return i18n( "Modified" ); break; case MimeType: return i18n( "Mime Type" ); break; case Category: return i18n( "Category" ); break; case Keywords: return i18n( "Keywords" ); break; case FilePath: return i18n( "File Path" ); break; case DocumentSize: return i18n( "File Size" ); break; case PagesSize: return i18n("Page Size"); break; default: return QString(); break; } } QString DocumentInfo::getKeyTitle( const QString &key ) const { QString title = getKeyTitle ( getKeyFromString( key ) ); if ( title.isEmpty() ) title = d->titles[ key ]; return title; } /** DocumentSynopsis **/ DocumentSynopsis::DocumentSynopsis() : QDomDocument( QStringLiteral("DocumentSynopsis") ) { // void implementation, only subclassed for naming } DocumentSynopsis::DocumentSynopsis( const QDomDocument &document ) : QDomDocument( document ) { } /** EmbeddedFile **/ EmbeddedFile::EmbeddedFile() { } EmbeddedFile::~EmbeddedFile() { } VisiblePageRect::VisiblePageRect( int page, const NormalizedRect &rectangle ) : pageNumber( page ), rect( rectangle ) { } #undef foreachObserver #undef foreachObserverD /* kate: replace-tabs on; indent-width 4; */ diff --git a/generators/poppler/generator_pdf.cpp b/generators/poppler/generator_pdf.cpp index 6e7631263..f8e53ad1c 100644 --- a/generators/poppler/generator_pdf.cpp +++ b/generators/poppler/generator_pdf.cpp @@ -1,2037 +1,2039 @@ /*************************************************************************** * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2004 by Enrico Ros * * Copyright (C) 2012 by Guillermo A. Amaral B. * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * Copyright (C) 2019 by Oliver Sander * * * * 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 "generator_pdf.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ui_pdfsettingswidget.h" #include "pdfsettings.h" #include #include #ifdef HAVE_POPPLER_0_73 #include #endif #include "debug_pdf.h" #include "annots.h" #include "formfields.h" #include "popplerembeddedfile.h" Q_DECLARE_METATYPE(Poppler::Annotation*) Q_DECLARE_METATYPE(Poppler::FontInfo) Q_DECLARE_METATYPE(const Poppler::LinkMovie*) Q_DECLARE_METATYPE(const Poppler::LinkRendition*) Q_DECLARE_METATYPE(const Poppler::LinkOCGState*) static const int defaultPageWidth = 595; static const int defaultPageHeight = 842; class PDFOptionsPage : public Okular::PrintOptionsWidget { Q_OBJECT public: enum ScaleMode { FitToPrintableArea, FitToPage, None }; Q_ENUM(ScaleMode) PDFOptionsPage() { setWindowTitle( i18n( "PDF Options" ) ); QVBoxLayout *layout = new QVBoxLayout(this); m_printAnnots = new QCheckBox(i18n("Print annotations"), this); m_printAnnots->setToolTip(i18n("Include annotations in the printed document")); m_printAnnots->setWhatsThis(i18n("Includes annotations in the printed document. You can disable this if you want to print the original unannotated document.")); layout->addWidget(m_printAnnots); m_forceRaster = new QCheckBox(i18n("Force rasterization"), this); m_forceRaster->setToolTip(i18n("Rasterize into an image before printing")); m_forceRaster->setWhatsThis(i18n("Forces the rasterization of each page into an image before printing it. This usually gives somewhat worse results, but is useful when printing documents that appear to print incorrectly.")); layout->addWidget(m_forceRaster); QWidget* formWidget = new QWidget(this); QFormLayout* printBackendLayout = new QFormLayout(formWidget); m_scaleMode = new QComboBox; m_scaleMode->insertItem(FitToPrintableArea, i18n("Fit to printable area"), FitToPrintableArea); m_scaleMode->insertItem(FitToPage, i18n("Fit to full page"), FitToPage); m_scaleMode->insertItem(None, i18n("None; print original size"), None); m_scaleMode->setToolTip(i18n( "Scaling mode for the printed pages" ) ); printBackendLayout->addRow(i18n("Scale mode:"), m_scaleMode); layout->addWidget(formWidget); layout->addStretch(1); setPrintAnnots( true ); // Default value } bool ignorePrintMargins() const override { return scaleMode() == FitToPage; } bool printAnnots() { return m_printAnnots->isChecked(); } void setPrintAnnots( bool printAnnots ) { m_printAnnots->setChecked( printAnnots ); } bool printForceRaster() { return m_forceRaster->isChecked(); } void setPrintForceRaster( bool forceRaster ) { m_forceRaster->setChecked( forceRaster ); } ScaleMode scaleMode() const { return m_scaleMode->currentData().value(); } private: QCheckBox *m_printAnnots; QCheckBox *m_forceRaster; QComboBox *m_scaleMode; }; static void fillViewportFromLinkDestination( Okular::DocumentViewport &viewport, const Poppler::LinkDestination &destination ) { viewport.pageNumber = destination.pageNumber() - 1; if (!viewport.isValid()) return; // get destination position // TODO add other attributes to the viewport (taken from link) // switch ( destination->getKind() ) // { // case destXYZ: if (destination.isChangeLeft() || destination.isChangeTop()) { // TODO remember to change this if we implement DPI and/or rotation double left, top; left = destination.left(); top = destination.top(); viewport.rePos.normalizedX = left; viewport.rePos.normalizedY = top; viewport.rePos.enabled = true; viewport.rePos.pos = Okular::DocumentViewport::TopLeft; } /* TODO if ( dest->getChangeZoom() ) make zoom change*/ /* break; default: // implement the others cases break;*/ // } } Okular::Sound* createSoundFromPopplerSound( const Poppler::SoundObject *popplerSound ) { Okular::Sound *sound = popplerSound->soundType() == Poppler::SoundObject::Embedded ? new Okular::Sound( popplerSound->data() ) : new Okular::Sound( popplerSound->url() ); sound->setSamplingRate( popplerSound->samplingRate() ); sound->setChannels( popplerSound->channels() ); sound->setBitsPerSample( popplerSound->bitsPerSample() ); switch ( popplerSound->soundEncoding() ) { case Poppler::SoundObject::Raw: sound->setSoundEncoding( Okular::Sound::Raw ); break; case Poppler::SoundObject::Signed: sound->setSoundEncoding( Okular::Sound::Signed ); break; case Poppler::SoundObject::muLaw: sound->setSoundEncoding( Okular::Sound::muLaw ); break; case Poppler::SoundObject::ALaw: sound->setSoundEncoding( Okular::Sound::ALaw ); break; } return sound; } Okular::Movie* createMovieFromPopplerMovie( const Poppler::MovieObject *popplerMovie ) { Okular::Movie *movie = new Okular::Movie( popplerMovie->url() ); movie->setSize( popplerMovie->size() ); movie->setRotation( (Okular::Rotation)( popplerMovie->rotation() / 90 ) ); movie->setShowControls( popplerMovie->showControls() ); movie->setPlayMode( (Okular::Movie::PlayMode)popplerMovie->playMode() ); movie->setAutoPlay( false ); // will be triggered by external MovieAnnotation movie->setShowPosterImage( popplerMovie->showPosterImage() ); movie->setPosterImage( popplerMovie->posterImage() ); return movie; } Okular::Movie* createMovieFromPopplerScreen( const Poppler::LinkRendition *popplerScreen ) { Poppler::MediaRendition *rendition = popplerScreen->rendition(); Okular::Movie *movie = nullptr; if ( rendition->isEmbedded() ) movie = new Okular::Movie( rendition->fileName(), rendition->data() ); else movie = new Okular::Movie( rendition->fileName() ); movie->setSize( rendition->size() ); movie->setShowControls( rendition->showControls() ); if ( rendition->repeatCount() == 0 ) { movie->setPlayMode( Okular::Movie::PlayRepeat ); } else { movie->setPlayMode( Okular::Movie::PlayLimited ); movie->setPlayRepetitions( rendition->repeatCount() ); } movie->setAutoPlay( rendition->autoPlay() ); return movie; } QPair createMovieFromPopplerRichMedia( const Poppler::RichMediaAnnotation *popplerRichMedia ) { const QPair emptyResult(0, 0); /** * To convert a Flash/Video based RichMedia annotation to a movie, we search for the first * Flash/Video richmedia instance and parse the flashVars parameter for the 'source' identifier. * That identifier is then used to find the associated embedded file through the assets * mapping. */ const Poppler::RichMediaAnnotation::Content *content = popplerRichMedia->content(); if ( !content ) return emptyResult; const QList configurations = content->configurations(); if ( configurations.isEmpty() ) return emptyResult; const Poppler::RichMediaAnnotation::Configuration *configuration = configurations[0]; const QList instances = configuration->instances(); if ( instances.isEmpty() ) return emptyResult; const Poppler::RichMediaAnnotation::Instance *instance = instances[0]; if ( ( instance->type() != Poppler::RichMediaAnnotation::Instance::TypeFlash ) && ( instance->type() != Poppler::RichMediaAnnotation::Instance::TypeVideo ) ) return emptyResult; const Poppler::RichMediaAnnotation::Params *params = instance->params(); if ( !params ) return emptyResult; QString sourceId; bool playbackLoops = false; const QStringList flashVars = params->flashVars().split( QLatin1Char( '&' ) ); for ( const QString &flashVar : flashVars ) { const int pos = flashVar.indexOf( QLatin1Char( '=' ) ); if ( pos == -1 ) continue; const QString key = flashVar.left( pos ); const QString value = flashVar.mid( pos + 1 ); if ( key == QLatin1String( "source" ) ) sourceId = value; else if ( key == QLatin1String( "loop" ) ) playbackLoops = ( value == QLatin1String( "true" ) ? true : false ); } if ( sourceId.isEmpty() ) return emptyResult; const QList assets = content->assets(); if ( assets.isEmpty() ) return emptyResult; Poppler::RichMediaAnnotation::Asset *matchingAsset = nullptr; for ( Poppler::RichMediaAnnotation::Asset *asset : assets ) { if ( asset->name() == sourceId ) { matchingAsset = asset; break; } } if ( !matchingAsset ) return emptyResult; Poppler::EmbeddedFile *embeddedFile = matchingAsset->embeddedFile(); if ( !embeddedFile ) return emptyResult; Okular::EmbeddedFile *pdfEmbeddedFile = new PDFEmbeddedFile( embeddedFile ); Okular::Movie *movie = new Okular::Movie( embeddedFile->name(), embeddedFile->data() ); movie->setPlayMode( playbackLoops ? Okular::Movie::PlayRepeat : Okular::Movie::PlayLimited ); if ( popplerRichMedia && popplerRichMedia->settings() && popplerRichMedia->settings()->activation() ) { if ( popplerRichMedia->settings()->activation()->condition() == Poppler::RichMediaAnnotation::Activation::PageOpened || popplerRichMedia->settings()->activation()->condition() == Poppler::RichMediaAnnotation::Activation::PageVisible ) { movie->setAutoPlay( true ); } else { movie->setAutoPlay( false ); } } else { movie->setAutoPlay( false ); } return qMakePair(movie, pdfEmbeddedFile); } /** * Note: the function will take ownership of the popplerLink object. */ Okular::Action* createLinkFromPopplerLink(const Poppler::Link *popplerLink, bool deletePopplerLink = true) { if (!popplerLink) return nullptr; Okular::Action *link = nullptr; const Poppler::LinkGoto *popplerLinkGoto; const Poppler::LinkExecute *popplerLinkExecute; const Poppler::LinkBrowse *popplerLinkBrowse; const Poppler::LinkAction *popplerLinkAction; const Poppler::LinkSound *popplerLinkSound; const Poppler::LinkJavaScript *popplerLinkJS; const Poppler::LinkMovie *popplerLinkMovie; const Poppler::LinkRendition *popplerLinkRendition; Okular::DocumentViewport viewport; switch(popplerLink->linkType()) { case Poppler::Link::None: break; case Poppler::Link::Goto: { popplerLinkGoto = static_cast(popplerLink); const Poppler::LinkDestination dest = popplerLinkGoto->destination(); const QString destName = dest.destinationName(); if (destName.isEmpty()) { fillViewportFromLinkDestination( viewport, dest ); link = new Okular::GotoAction(popplerLinkGoto->fileName(), viewport); } else { link = new Okular::GotoAction(popplerLinkGoto->fileName(), destName); } } break; case Poppler::Link::Execute: popplerLinkExecute = static_cast(popplerLink); link = new Okular::ExecuteAction( popplerLinkExecute->fileName(), popplerLinkExecute->parameters() ); break; case Poppler::Link::Browse: popplerLinkBrowse = static_cast(popplerLink); link = new Okular::BrowseAction( QUrl(popplerLinkBrowse->url()) ); break; case Poppler::Link::Action: popplerLinkAction = static_cast(popplerLink); link = new Okular::DocumentAction( (Okular::DocumentAction::DocumentActionType)popplerLinkAction->actionType() ); break; case Poppler::Link::Sound: { popplerLinkSound = static_cast(popplerLink); Poppler::SoundObject *popplerSound = popplerLinkSound->sound(); Okular::Sound *sound = createSoundFromPopplerSound( popplerSound ); link = new Okular::SoundAction( popplerLinkSound->volume(), popplerLinkSound->synchronous(), popplerLinkSound->repeat(), popplerLinkSound->mix(), sound ); } break; case Poppler::Link::JavaScript: { popplerLinkJS = static_cast(popplerLink); link = new Okular::ScriptAction( Okular::JavaScript, popplerLinkJS->script() ); } break; case Poppler::Link::Rendition: { if (!deletePopplerLink) { // If links should not be deleted it probably means that they // are part of a nextActions chain. There is no support // to resolveMediaLinkReferences on nextActions. It would also // be necessary to ensure that resolveMediaLinkReferences does // not delete the Links which are part of a nextActions list // to avoid a double deletion. qCDebug(OkularPdfDebug) << "parsing rendition link without deletion is not supported. Action chain might be broken."; break; } deletePopplerLink = false; // we'll delete it inside resolveMediaLinkReferences() after we have resolved all references popplerLinkRendition = static_cast( popplerLink ); Okular::RenditionAction::OperationType operation = Okular::RenditionAction::None; switch ( popplerLinkRendition->action() ) { case Poppler::LinkRendition::NoRendition: operation = Okular::RenditionAction::None; break; case Poppler::LinkRendition::PlayRendition: operation = Okular::RenditionAction::Play; break; case Poppler::LinkRendition::StopRendition: operation = Okular::RenditionAction::Stop; break; case Poppler::LinkRendition::PauseRendition: operation = Okular::RenditionAction::Pause; break; case Poppler::LinkRendition::ResumeRendition: operation = Okular::RenditionAction::Resume; break; }; Okular::Movie *movie = nullptr; if ( popplerLinkRendition->rendition() ) movie = createMovieFromPopplerScreen( popplerLinkRendition ); Okular::RenditionAction *renditionAction = new Okular::RenditionAction( operation, movie, Okular::JavaScript, popplerLinkRendition->script() ); renditionAction->setNativeId( QVariant::fromValue( popplerLinkRendition ) ); link = renditionAction; } break; case Poppler::Link::Movie: { if (!deletePopplerLink) { // See comment above in Link::Rendition qCDebug(OkularPdfDebug) << "parsing movie link without deletion is not supported. Action chain might be broken."; break; } deletePopplerLink = false; // we'll delete it inside resolveMediaLinkReferences() after we have resolved all references popplerLinkMovie = static_cast( popplerLink ); Okular::MovieAction::OperationType operation = Okular::MovieAction::Play; switch ( popplerLinkMovie->operation() ) { case Poppler::LinkMovie::Play: operation = Okular::MovieAction::Play; break; case Poppler::LinkMovie::Stop: operation = Okular::MovieAction::Stop; break; case Poppler::LinkMovie::Pause: operation = Okular::MovieAction::Pause; break; case Poppler::LinkMovie::Resume: operation = Okular::MovieAction::Resume; break; }; Okular::MovieAction *movieAction = new Okular::MovieAction( operation ); movieAction->setNativeId( QVariant::fromValue( popplerLinkMovie ) ); link = movieAction; } break; #ifdef HAVE_POPPLER_0_64 case Poppler::Link::Hide: { const Poppler::LinkHide * l = static_cast( popplerLink ); QStringList scripts; - for ( const QString &target: l->targets() ) + const QVector targets = l->targets(); + for ( const QString &target : targets ) { scripts << QStringLiteral( "getField(\"%1\").hidden = %2;" ).arg( target ).arg( l->isShowAction() ? QLatin1String( "false" ) : QLatin1String( "true" ) ); } link = new Okular::ScriptAction( Okular::JavaScript, scripts.join( QLatin1Char( '\n' ) ) ); } break; #endif case Poppler::Link::OCGState: link = new Okular::BackendOpaqueAction(); link->setNativeId( QVariant::fromValue( static_cast( popplerLink ) ) ); deletePopplerLink = false; break; } #ifdef HAVE_POPPLER_0_64 if (link) { QVector< Okular::Action * > nextActions; - for ( const Poppler::Link *nl : popplerLink->nextLinks() ) + const QVector nextLinks = popplerLink->nextLinks(); + for ( const Poppler::Link *nl : nextLinks ) { nextActions << createLinkFromPopplerLink( nl, false ); } link->setNextActions( nextActions ); } #endif if ( deletePopplerLink ) delete popplerLink; return link; } /** * Note: the function will take ownership of the popplerLink objects. */ static QLinkedList generateLinks( const QList &popplerLinks ) { QLinkedList links; for (const Poppler::Link *popplerLink : popplerLinks) { QRectF linkArea = popplerLink->linkArea(); double nl = linkArea.left(), nt = linkArea.top(), nr = linkArea.right(), nb = linkArea.bottom(); // create the rect using normalized coords and attach the Okular::Link to it Okular::ObjectRect * rect = new Okular::ObjectRect( nl, nt, nr, nb, false, Okular::ObjectRect::Action, createLinkFromPopplerLink(popplerLink) ); // add the ObjectRect to the container links.push_front( rect ); } return links; } /** NOTES on threading: * internal: thread race prevention is done via the 'docLock' mutex. the * mutex is needed only because we have the asynchronous thread; else * the operations are all within the 'gui' thread, scheduled by the * Qt scheduler and no mutex is needed. * external: dangerous operations are all locked via mutex internally, and the * only needed external thing is the 'canGeneratePixmap' method * that tells if the generator is free (since we don't want an * internal queue to store PixmapRequests). A generatedPixmap call * without the 'ready' flag set, results in undefined behavior. * So, as example, printing while generating a pixmap asynchronously is safe, * it might only block the gui thread by 1) waiting for the mutex to unlock * in async thread and 2) doing the 'heavy' print operation. */ OKULAR_EXPORT_PLUGIN(PDFGenerator, "libokularGenerator_poppler.json") static void PDFGeneratorPopplerDebugFunction(const QString &message, const QVariant &closure) { Q_UNUSED(closure); qCDebug(OkularPdfDebug) << "[Poppler]" << message; } PDFGenerator::PDFGenerator( QObject *parent, const QVariantList &args ) : Generator( parent, args ), pdfdoc( nullptr ), docSynopsisDirty( true ), docEmbeddedFilesDirty( true ), nextFontPage( 0 ), annotProxy( nullptr ) { setFeature( Threaded ); setFeature( TextExtraction ); setFeature( FontInfo ); #ifdef Q_OS_WIN32 setFeature( PrintNative ); #else setFeature( PrintPostscript ); #endif if ( Okular::FilePrinter::ps2pdfAvailable() ) setFeature( PrintToFile ); setFeature( ReadRawData ); setFeature( TiledRendering ); setFeature( SwapBackingFile ); #ifdef HAVE_POPPLER_0_63 setFeature( SupportsCancelling ); #endif // You only need to do it once not for each of the documents but it is cheap enough // so doing it all the time won't hurt either Poppler::setDebugErrorFunction(PDFGeneratorPopplerDebugFunction, QVariant()); } PDFGenerator::~PDFGenerator() { delete pdfOptionsPage; } //BEGIN Generator inherited functions Okular::Document::OpenResult PDFGenerator::loadDocumentWithPassword( const QString & filePath, QVector & pagesVector, const QString &password ) { #ifndef NDEBUG if ( pdfdoc ) { qCDebug(OkularPdfDebug) << "PDFGenerator: multiple calls to loadDocument. Check it."; return Okular::Document::OpenError; } #endif // create PDFDoc for the given file pdfdoc = Poppler::Document::load( filePath, nullptr, nullptr ); return init(pagesVector, password); } Okular::Document::OpenResult PDFGenerator::loadDocumentFromDataWithPassword( const QByteArray & fileData, QVector & pagesVector, const QString &password ) { #ifndef NDEBUG if ( pdfdoc ) { qCDebug(OkularPdfDebug) << "PDFGenerator: multiple calls to loadDocument. Check it."; return Okular::Document::OpenError; } #endif // create PDFDoc for the given file pdfdoc = Poppler::Document::loadFromData( fileData, nullptr, nullptr ); return init(pagesVector, password); } Okular::Document::OpenResult PDFGenerator::init(QVector & pagesVector, const QString &password) { if ( !pdfdoc ) return Okular::Document::OpenError; if ( pdfdoc->isLocked() ) { pdfdoc->unlock( password.toLatin1(), password.toLatin1() ); if ( pdfdoc->isLocked() ) { delete pdfdoc; pdfdoc = nullptr; return Okular::Document::OpenNeedsPassword; } } // build Pages (currentPage was set -1 by deletePages) int pageCount = pdfdoc->numPages(); if (pageCount < 0) { delete pdfdoc; pdfdoc = nullptr; return Okular::Document::OpenError; } pagesVector.resize(pageCount); rectsGenerated.fill(false, pageCount); annotationsOnOpenHash.clear(); loadPages(pagesVector, 0, false); // update the configuration reparseConfig(); // create annotation proxy annotProxy = new PopplerAnnotationProxy( pdfdoc, userMutex(), &annotationsOnOpenHash ); // the file has been loaded correctly return Okular::Document::OpenSuccess; } PDFGenerator::SwapBackingFileResult PDFGenerator::swapBackingFile( QString const &newFileName, QVector & newPagesVector ) { const QBitArray oldRectsGenerated = rectsGenerated; doCloseDocument(); auto openResult = loadDocumentWithPassword(newFileName, newPagesVector, QString()); if (openResult != Okular::Document::OpenSuccess) return SwapBackingFileError; // Recreate links if needed since they are done on image() and image() is not called when swapping the file // since the page is already rendered if (oldRectsGenerated.count() == rectsGenerated.count()) { for (int i = 0; i < oldRectsGenerated.count(); ++i) { if (oldRectsGenerated[i]) { Okular::Page *page = newPagesVector[i]; Poppler::Page *pp = pdfdoc->page( i ); if (pp) { page->setObjectRects(generateLinks(pp->links())); rectsGenerated[i] = true; resolveMediaLinkReferences(page); delete pp; } } } } return SwapBackingFileReloadInternalData; } bool PDFGenerator::doCloseDocument() { // remove internal objects userMutex()->lock(); delete annotProxy; annotProxy = nullptr; delete pdfdoc; pdfdoc = nullptr; userMutex()->unlock(); docSynopsisDirty = true; docSyn.clear(); docEmbeddedFilesDirty = true; qDeleteAll(docEmbeddedFiles); docEmbeddedFiles.clear(); nextFontPage = 0; rectsGenerated.clear(); return true; } void PDFGenerator::loadPages(QVector &pagesVector, int rotation, bool clear) { // TODO XPDF 3.01 check const int count = pagesVector.count(); double w = 0, h = 0; for ( int i = 0; i < count ; i++ ) { // get xpdf page Poppler::Page * p = pdfdoc->page( i ); Okular::Page * page; if (p) { const QSizeF pSize = p->pageSizeF(); w = pSize.width() / 72.0 * dpi().width(); h = pSize.height() / 72.0 * dpi().height(); Okular::Rotation orientation = Okular::Rotation0; switch (p->orientation()) { case Poppler::Page::Landscape: orientation = Okular::Rotation90; break; case Poppler::Page::UpsideDown: orientation = Okular::Rotation180; break; case Poppler::Page::Seascape: orientation = Okular::Rotation270; break; case Poppler::Page::Portrait: orientation = Okular::Rotation0; break; } if (rotation % 2 == 1) qSwap(w,h); // init a Okular::page, add transition and annotation information page = new Okular::Page( i, w, h, orientation ); addTransition( p, page ); if ( true ) //TODO real check addAnnotations( p, page ); Poppler::Link * tmplink = p->action( Poppler::Page::Opening ); if ( tmplink ) { page->setPageAction( Okular::Page::Opening, createLinkFromPopplerLink( tmplink ) ); } tmplink = p->action( Poppler::Page::Closing ); if ( tmplink ) { page->setPageAction( Okular::Page::Closing, createLinkFromPopplerLink( tmplink ) ); } page->setDuration( p->duration() ); page->setLabel( p->label() ); addFormFields( p, page ); // kWarning(PDFDebug).nospace() << page->width() << "x" << page->height(); #ifdef PDFGENERATOR_DEBUG qCDebug(OkularPdfDebug) << "load page" << i << "with rotation" << rotation << "and orientation" << orientation; #endif delete p; if (clear && pagesVector[i]) delete pagesVector[i]; } else { page = new Okular::Page( i, defaultPageWidth, defaultPageHeight, Okular::Rotation0 ); } // set the Okular::page at the right position in document's pages vector pagesVector[i] = page; } } Okular::DocumentInfo PDFGenerator::generateDocumentInfo( const QSet &keys ) const { Okular::DocumentInfo docInfo; docInfo.set( Okular::DocumentInfo::MimeType, QStringLiteral("application/pdf") ); userMutex()->lock(); if ( pdfdoc ) { // compile internal structure reading properties from PDFDoc if ( keys.contains( Okular::DocumentInfo::Title ) ) docInfo.set( Okular::DocumentInfo::Title, pdfdoc->info(QStringLiteral("Title")) ); if ( keys.contains( Okular::DocumentInfo::Subject ) ) docInfo.set( Okular::DocumentInfo::Subject, pdfdoc->info(QStringLiteral("Subject")) ); if ( keys.contains( Okular::DocumentInfo::Author ) ) docInfo.set( Okular::DocumentInfo::Author, pdfdoc->info(QStringLiteral("Author")) ); if ( keys.contains( Okular::DocumentInfo::Keywords ) ) docInfo.set( Okular::DocumentInfo::Keywords, pdfdoc->info(QStringLiteral("Keywords")) ); if ( keys.contains( Okular::DocumentInfo::Creator ) ) docInfo.set( Okular::DocumentInfo::Creator, pdfdoc->info(QStringLiteral("Creator")) ); if ( keys.contains( Okular::DocumentInfo::Producer ) ) docInfo.set( Okular::DocumentInfo::Producer, pdfdoc->info(QStringLiteral("Producer")) ); if ( keys.contains( Okular::DocumentInfo::CreationDate ) ) docInfo.set( Okular::DocumentInfo::CreationDate, QLocale().toString( pdfdoc->date(QStringLiteral("CreationDate")), QLocale::LongFormat ) ); if ( keys.contains( Okular::DocumentInfo::ModificationDate ) ) docInfo.set( Okular::DocumentInfo::ModificationDate, QLocale().toString( pdfdoc->date(QStringLiteral("ModDate")), QLocale::LongFormat ) ); if ( keys.contains( Okular::DocumentInfo::CustomKeys ) ) { int major, minor; pdfdoc->getPdfVersion(&major, &minor); docInfo.set( QStringLiteral("format"), i18nc( "PDF v. ", "PDF v. %1.%2", major, minor ), i18n( "Format" ) ); docInfo.set( QStringLiteral("encryption"), pdfdoc->isEncrypted() ? i18n( "Encrypted" ) : i18n( "Unencrypted" ), i18n("Security") ); docInfo.set( QStringLiteral("optimization"), pdfdoc->isLinearized() ? i18n( "Yes" ) : i18n( "No" ), i18n("Optimized") ); } docInfo.set( Okular::DocumentInfo::Pages, QString::number( pdfdoc->numPages() ) ); } userMutex()->unlock(); return docInfo; } const Okular::DocumentSynopsis * PDFGenerator::generateDocumentSynopsis() { if ( !docSynopsisDirty ) return &docSyn; if ( !pdfdoc ) return nullptr; userMutex()->lock(); QDomDocument *toc = pdfdoc->toc(); userMutex()->unlock(); if ( !toc ) return nullptr; addSynopsisChildren(toc, &docSyn); delete toc; docSynopsisDirty = false; return &docSyn; } static Okular::FontInfo::FontType convertPopplerFontInfoTypeToOkularFontInfoType( Poppler::FontInfo::Type type ) { switch ( type ) { case Poppler::FontInfo::Type1: return Okular::FontInfo::Type1; break; case Poppler::FontInfo::Type1C: return Okular::FontInfo::Type1C; break; case Poppler::FontInfo::Type3: return Okular::FontInfo::Type3; break; case Poppler::FontInfo::TrueType: return Okular::FontInfo::TrueType; break; case Poppler::FontInfo::CIDType0: return Okular::FontInfo::CIDType0; break; case Poppler::FontInfo::CIDType0C: return Okular::FontInfo::CIDType0C; break; case Poppler::FontInfo::CIDTrueType: return Okular::FontInfo::CIDTrueType; break; case Poppler::FontInfo::Type1COT: return Okular::FontInfo::Type1COT; break; case Poppler::FontInfo::TrueTypeOT: return Okular::FontInfo::TrueTypeOT; break; case Poppler::FontInfo::CIDType0COT: return Okular::FontInfo::CIDType0COT; break; case Poppler::FontInfo::CIDTrueTypeOT: return Okular::FontInfo::CIDTrueTypeOT; break; case Poppler::FontInfo::unknown: default: ; } return Okular::FontInfo::Unknown; } static Okular::FontInfo::EmbedType embedTypeForPopplerFontInfo( const Poppler::FontInfo &fi ) { Okular::FontInfo::EmbedType ret = Okular::FontInfo::NotEmbedded; if ( fi.isEmbedded() ) { if ( fi.isSubset() ) { ret = Okular::FontInfo::EmbeddedSubset; } else { ret = Okular::FontInfo::FullyEmbedded; } } return ret; } Okular::FontInfo::List PDFGenerator::fontsForPage( int page ) { Okular::FontInfo::List list; if ( page != nextFontPage ) return list; QList fonts; userMutex()->lock(); Poppler::FontIterator* it = pdfdoc->newFontIterator(page); if (it->hasNext()) { fonts = it->next(); } userMutex()->unlock(); for (const Poppler::FontInfo &font : qAsConst(fonts)) { Okular::FontInfo of; of.setName( font.name() ); #ifdef HAVE_POPPLER_0_80 of.setSubstituteName( font.substituteName() ); #endif of.setType( convertPopplerFontInfoTypeToOkularFontInfoType( font.type() ) ); of.setEmbedType( embedTypeForPopplerFontInfo( font) ); of.setFile( font.file() ); of.setCanBeExtracted( of.embedType() != Okular::FontInfo::NotEmbedded ); QVariant nativeId; nativeId.setValue( font ); of.setNativeId( nativeId ); list.append( of ); } ++nextFontPage; return list; } const QList *PDFGenerator::embeddedFiles() const { if (docEmbeddedFilesDirty) { userMutex()->lock(); const QList &popplerFiles = pdfdoc->embeddedFiles(); for (Poppler::EmbeddedFile *pef : popplerFiles) { docEmbeddedFiles.append(new PDFEmbeddedFile(pef)); } userMutex()->unlock(); docEmbeddedFilesDirty = false; } return &docEmbeddedFiles; } QAbstractItemModel* PDFGenerator::layersModel() const { return pdfdoc->hasOptionalContent() ? pdfdoc->optionalContentModel() : nullptr; } void PDFGenerator::opaqueAction( const Okular::BackendOpaqueAction *action ) { const Poppler::LinkOCGState *popplerLink = action->nativeId().value(); pdfdoc->optionalContentModel()->applyLink( const_cast< Poppler::LinkOCGState* >( popplerLink ) ); } bool PDFGenerator::isAllowed( Okular::Permission permission ) const { bool b = true; switch ( permission ) { case Okular::AllowModify: b = pdfdoc->okToChange(); break; case Okular::AllowCopy: b = pdfdoc->okToCopy(); break; case Okular::AllowPrint: b = pdfdoc->okToPrint(); break; case Okular::AllowNotes: b = pdfdoc->okToAddNotes(); break; case Okular::AllowFillForms: b = pdfdoc->okToFillForm(); break; default: ; } return b; } struct RenderImagePayload { RenderImagePayload(PDFGenerator *g, Okular::PixmapRequest *r) : generator(g), request(r) { // Don't report partial updates for the first 500 ms timer.setInterval(500); timer.setSingleShot(true); timer.start(); } PDFGenerator *generator; Okular::PixmapRequest *request; QTimer timer; }; Q_DECLARE_METATYPE(RenderImagePayload*) static bool shouldDoPartialUpdateCallback(const QVariant &vPayload) { auto payload = vPayload.value(); // Since the timer lives in a thread without an event loop we need to stop it ourselves // when the remaining time has reached 0 if (payload->timer.isActive() && payload->timer.remainingTime() == 0) { payload->timer.stop(); } return !payload->timer.isActive(); } static void partialUpdateCallback(const QImage &image, const QVariant &vPayload) { auto payload = vPayload.value(); QMetaObject::invokeMethod(payload->generator, "signalPartialPixmapRequest", Qt::QueuedConnection, Q_ARG(Okular::PixmapRequest*, payload->request), Q_ARG(QImage, image)); } #ifdef HAVE_POPPLER_0_63 static bool shouldAbortRenderCallback(const QVariant &vPayload) { auto payload = vPayload.value(); return payload->request->shouldAbortRender(); } #endif QImage PDFGenerator::image( Okular::PixmapRequest * request ) { // debug requests to this (xpdf) generator //qCDebug(OkularPdfDebug) << "id: " << request->id << " is requesting " << (request->async ? "ASYNC" : "sync") << " pixmap for page " << request->page->number() << " [" << request->width << " x " << request->height << "]."; // compute dpi used to get an image with desired width and height Okular::Page * page = request->page(); double pageWidth = page->width(), pageHeight = page->height(); if ( page->rotation() % 2 ) qSwap( pageWidth, pageHeight ); qreal fakeDpiX = request->width() / pageWidth * dpi().width(); qreal fakeDpiY = request->height() / pageHeight * dpi().height(); // generate links rects only the first time bool genObjectRects = !rectsGenerated.at( page->number() ); // 0. LOCK [waits for the thread end] userMutex()->lock(); if ( request->shouldAbortRender() ) { userMutex()->unlock(); return QImage(); } // 1. Set OutputDev parameters and Generate contents // note: thread safety is set on 'false' for the GUI (this) thread Poppler::Page *p = pdfdoc->page(page->number()); // 2. Take data from outputdev and attach it to the Page QImage img; if (p) { #ifdef HAVE_POPPLER_0_63 if ( request->isTile() ) { const QRect rect = request->normalizedRect().geometry( request->width(), request->height() ); if ( request->partialUpdatesWanted() ) { RenderImagePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0, partialUpdateCallback, shouldDoPartialUpdateCallback, shouldAbortRenderCallback, QVariant::fromValue( &payload ) ); } else { RenderImagePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0, nullptr, nullptr, shouldAbortRenderCallback, QVariant::fromValue( &payload ) ); } } else { if ( request->partialUpdatesWanted() ) { RenderImagePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0, partialUpdateCallback, shouldDoPartialUpdateCallback, shouldAbortRenderCallback, QVariant::fromValue( &payload ) ); } else { RenderImagePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0, nullptr, nullptr, shouldAbortRenderCallback, QVariant::fromValue( &payload ) ); } } #else if ( request->isTile() ) { const QRect rect = request->normalizedRect().geometry( request->width(), request->height() ); if ( request->partialUpdatesWanted() ) { RenderImagePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0, partialUpdateCallback, shouldDoPartialUpdateCallback, QVariant::fromValue( &payload ) ); } else { img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0 ); } } else { if ( request->partialUpdatesWanted() ) { RenderImagePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0, partialUpdateCallback, shouldDoPartialUpdateCallback, QVariant::fromValue( &payload ) ); } else { img = p->renderToImage( fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0 ); } } #endif } else { img = QImage( request->width(), request->height(), QImage::Format_Mono ); img.fill( Qt::white ); } if ( p && genObjectRects ) { // TODO previously we extracted Image type rects too, but that needed porting to poppler // and as we are not doing anything with Image type rects i did not port it, have a look at // dead gp_outputdev.cpp on image extraction page->setObjectRects( generateLinks(p->links()) ); rectsGenerated[ request->page()->number() ] = true; resolveMediaLinkReferences( page ); } // 3. UNLOCK [re-enables shared access] userMutex()->unlock(); delete p; return img; } template void resolveMediaLinks( Okular::Action *action, enum Okular::Annotation::SubType subType, QHash &annotationsHash ) { OkularLinkType *okularAction = static_cast( action ); const PopplerLinkType *popplerLink = action->nativeId().value(); QHashIterator it( annotationsHash ); while ( it.hasNext() ) { it.next(); if ( it.key()->subType() == subType ) { const PopplerAnnotationType *popplerAnnotation = static_cast( it.value() ); if ( popplerLink->isReferencedAnnotation( popplerAnnotation ) ) { okularAction->setAnnotation( static_cast( it.key() ) ); okularAction->setNativeId( QVariant() ); delete popplerLink; // delete the associated Poppler::LinkMovie object, it's not needed anymore break; } } } } void PDFGenerator::resolveMediaLinkReference( Okular::Action *action ) { if ( !action ) return; if ( (action->actionType() != Okular::Action::Movie) && (action->actionType() != Okular::Action::Rendition) ) return; resolveMediaLinks( action, Okular::Annotation::AMovie, annotationsOnOpenHash ); resolveMediaLinks( action, Okular::Annotation::AScreen, annotationsOnOpenHash ); } void PDFGenerator::resolveMediaLinkReferences( Okular::Page *page ) { resolveMediaLinkReference( const_cast( page->pageAction( Okular::Page::Opening ) ) ); resolveMediaLinkReference( const_cast( page->pageAction( Okular::Page::Closing ) ) ); const QLinkedList< Okular::Annotation* > annotations = page->annotations(); for ( Okular::Annotation *annotation : annotations ) { if ( annotation->subType() == Okular::Annotation::AScreen ) { Okular::ScreenAnnotation *screenAnnotation = static_cast( annotation ); resolveMediaLinkReference( screenAnnotation->additionalAction( Okular::Annotation::PageOpening ) ); resolveMediaLinkReference( screenAnnotation->additionalAction( Okular::Annotation::PageClosing ) ); } if ( annotation->subType() == Okular::Annotation::AWidget ) { Okular::WidgetAnnotation *widgetAnnotation = static_cast( annotation ); resolveMediaLinkReference( widgetAnnotation->additionalAction( Okular::Annotation::PageOpening ) ); resolveMediaLinkReference( widgetAnnotation->additionalAction( Okular::Annotation::PageClosing ) ); } } const QLinkedList< Okular::FormField * > fields = page->formFields(); for ( Okular::FormField *field : fields ) { resolveMediaLinkReference( field->activationAction() ); } } #ifdef HAVE_POPPLER_0_63 struct TextExtractionPayload { TextExtractionPayload(Okular::TextRequest *r) : request(r) { } Okular::TextRequest *request; }; Q_DECLARE_METATYPE(TextExtractionPayload*) static bool shouldAbortTextExtractionCallback(const QVariant &vPayload) { auto payload = vPayload.value(); return payload->request->shouldAbortExtraction(); } #endif Okular::TextPage* PDFGenerator::textPage( Okular::TextRequest *request ) { const Okular::Page *page = request->page(); #ifdef PDFGENERATOR_DEBUG qCDebug(OkularPdfDebug) << "page" << page->number(); #endif // build a TextList... QList textList; double pageWidth, pageHeight; userMutex()->lock(); Poppler::Page *pp = pdfdoc->page( page->number() ); if (pp) { #ifdef HAVE_POPPLER_0_63 TextExtractionPayload payload(request); textList = pp->textList( Poppler::Page::Rotate0, shouldAbortTextExtractionCallback, QVariant::fromValue( &payload ) ); #else textList = pp->textList(); #endif const QSizeF s = pp->pageSizeF(); pageWidth = s.width(); pageHeight = s.height(); } else { pageWidth = defaultPageWidth; pageHeight = defaultPageHeight; } delete pp; userMutex()->unlock(); if ( textList.isEmpty() && request->shouldAbortExtraction() ) return nullptr; Okular::TextPage *tp = abstractTextPage(textList, pageHeight, pageWidth, (Poppler::Page::Rotation)page->orientation()); qDeleteAll(textList); return tp; } void PDFGenerator::requestFontData(const Okular::FontInfo &font, QByteArray *data) { Poppler::FontInfo fi = font.nativeId().value(); *data = pdfdoc->fontData(fi); } #define DUMMY_QPRINTER_COPY bool PDFGenerator::print( QPrinter& printer ) { bool printAnnots = true; bool forceRasterize = false; PDFOptionsPage::ScaleMode scaleMode = PDFOptionsPage::FitToPrintableArea; if ( pdfOptionsPage ) { printAnnots = pdfOptionsPage->printAnnots(); forceRasterize = pdfOptionsPage->printForceRaster(); scaleMode = pdfOptionsPage->scaleMode(); } #ifdef Q_OS_WIN // Windows can only print by rasterization, because that is // currently the only way Okular implements printing without using UNIX-specific // tools like 'lpr'. forceRasterize = true; #endif if ( forceRasterize ) { pdfdoc->setRenderHint(Poppler::Document::HideAnnotations, !printAnnots); if ( pdfOptionsPage ) { // If requested, scale to full page instead of the printable area printer.setFullPage( pdfOptionsPage->ignorePrintMargins() ); } QPainter painter; painter.begin(&printer); QList pageList = Okular::FilePrinter::pageList( printer, pdfdoc->numPages(), document()->currentPage() + 1, document()->bookmarkedPageList() ); for ( int i = 0; i < pageList.count(); ++i ) { if ( i != 0 ) printer.newPage(); const int page = pageList.at( i ) - 1; userMutex()->lock(); std::unique_ptr pp( pdfdoc->page( page ) ); if (pp) { QSizeF pageSize = pp->pageSizeF(); // Unit is 'points' (i.e., 1/72th of an inch) QRect painterWindow = painter.window(); // Unit is 'QPrinter::DevicePixel' // Default: no scaling at all, but we need to go from DevicePixel units to 'points' // Warning: We compute the horizontal scaling, and later assume that the vertical scaling will be the same. double scaling = printer.paperRect(QPrinter::DevicePixel).width() / printer.paperRect(QPrinter::Point).width(); if ( scaleMode != PDFOptionsPage::None ) { // Get the two scaling factors needed to fit the page onto paper horizontally or vertically auto horizontalScaling = painterWindow.width() / pageSize.width(); auto verticalScaling = painterWindow.height() / pageSize.height(); // We use the smaller of the two for both directions, to keep the aspect ratio scaling = std::min(horizontalScaling, verticalScaling); } #ifdef Q_OS_WIN QImage img = pp->renderToImage( printer.physicalDpiX(), printer.physicalDpiY() ); #else // UNIX: Same resolution as the postscript rasterizer; see discussion at https://git.reviewboard.kde.org/r/130218/ QImage img = pp->renderToImage( 300, 300 ); #endif painter.drawImage( QRectF( QPointF( 0, 0 ), scaling * pp->pageSizeF() ), img ); } userMutex()->unlock(); } painter.end(); return true; } #ifdef DUMMY_QPRINTER_COPY // Get the real page size to pass to the ps generator QPrinter dummy( QPrinter::PrinterResolution ); dummy.setFullPage( true ); dummy.setOrientation( printer.orientation() ); dummy.setPageSize( printer.pageSize() ); dummy.setPaperSize( printer.paperSize( QPrinter::Millimeter ), QPrinter::Millimeter ); int width = dummy.width(); int height = dummy.height(); #else int width = printer.width(); int height = printer.height(); #endif if (width <= 0 || height <= 0) { lastPrintError = InvalidPageSizePrintError; return false; } // Create the tempfile to send to FilePrinter, which will manage the deletion QTemporaryFile tf(QDir::tempPath() + QLatin1String("/okular_XXXXXX.ps")); if ( !tf.open() ) { lastPrintError = TemporaryFileOpenPrintError; return false; } QString tempfilename = tf.fileName(); // Generate the list of pages to be printed as selected in the print dialog QList pageList = Okular::FilePrinter::pageList( printer, pdfdoc->numPages(), document()->currentPage() + 1, document()->bookmarkedPageList() ); // TODO rotation tf.setAutoRemove(false); QString pstitle = metaData(QStringLiteral("Title"), QVariant()).toString(); if ( pstitle.trimmed().isEmpty() ) { pstitle = document()->currentDocument().fileName(); } Poppler::PSConverter *psConverter = pdfdoc->psConverter(); psConverter->setOutputDevice(&tf); psConverter->setPageList(pageList); psConverter->setPaperWidth(width); psConverter->setPaperHeight(height); psConverter->setRightMargin(0); psConverter->setBottomMargin(0); psConverter->setLeftMargin(0); psConverter->setTopMargin(0); psConverter->setStrictMargins(false); psConverter->setForceRasterize(forceRasterize); psConverter->setTitle(pstitle); if (!printAnnots) psConverter->setPSOptions(psConverter->psOptions() | Poppler::PSConverter::HideAnnotations ); userMutex()->lock(); if (psConverter->convert()) { userMutex()->unlock(); delete psConverter; tf.close(); const Okular::FilePrinter::ScaleMode filePrinterScaleMode = (scaleMode == PDFOptionsPage::None) ? Okular::FilePrinter::ScaleMode::NoScaling : Okular::FilePrinter::ScaleMode::FitToPrintArea; int ret = Okular::FilePrinter::printFile( printer, tempfilename, document()->orientation(), Okular::FilePrinter::SystemDeletesFiles, Okular::FilePrinter::ApplicationSelectsPages, document()->bookmarkedPageRange(), filePrinterScaleMode ); lastPrintError = Okular::FilePrinter::printError( ret ); return (lastPrintError == NoPrintError); } else { lastPrintError = FileConversionPrintError; delete psConverter; userMutex()->unlock(); } tf.close(); return false; } QVariant PDFGenerator::metaData( const QString & key, const QVariant & option ) const { if ( key == QLatin1String("StartFullScreen") ) { QMutexLocker ml(userMutex()); // asking for the 'start in fullscreen mode' (pdf property) if ( pdfdoc->pageMode() == Poppler::Document::FullScreen ) return true; } else if ( key == QLatin1String("NamedViewport") && !option.toString().isEmpty() ) { Okular::DocumentViewport viewport; QString optionString = option.toString(); // asking for the page related to a 'named link destination'. the // option is the link name. @see addSynopsisChildren. userMutex()->lock(); Poppler::LinkDestination *ld = pdfdoc->linkDestination( optionString ); userMutex()->unlock(); if ( ld ) { fillViewportFromLinkDestination( viewport, *ld ); } delete ld; if ( viewport.pageNumber >= 0 ) return viewport.toString(); } else if ( key == QLatin1String("DocumentTitle") ) { userMutex()->lock(); QString title = pdfdoc->info( QStringLiteral("Title") ); userMutex()->unlock(); return title; } else if ( key == QLatin1String("OpenTOC") ) { QMutexLocker ml(userMutex()); if ( pdfdoc->pageMode() == Poppler::Document::UseOutlines ) return true; } else if ( key == QLatin1String("DocumentScripts") && option.toString() == QLatin1String("JavaScript") ) { QMutexLocker ml(userMutex()); return pdfdoc->scripts(); } else if ( key == QLatin1String("HasUnsupportedXfaForm") ) { QMutexLocker ml(userMutex()); return pdfdoc->formType() == Poppler::Document::XfaForm; } else if ( key == QLatin1String("FormCalculateOrder") ) { QMutexLocker ml(userMutex()); return QVariant::fromValue>(pdfdoc->formCalculateOrder()); } else if ( key == QLatin1String("GeneratorExtraDescription") ) { #ifdef HAVE_POPPLER_0_73 if (Poppler::Version::string() == POPPLER_VERSION) { return i18n("Using Poppler %1", Poppler::Version::string()); } else { return i18n("Using Poppler %1\n\nBuilt against Poppler %2", Poppler::Version::string(), POPPLER_VERSION); } #endif } else if ( key == QLatin1String("IsDigitallySigned") ) { const Okular::Document *doc = document(); uint numPages = doc->pages(); for ( uint i = 0; i < numPages; i++ ) { const QLinkedList formFields = doc->page( i )->formFields(); for ( const Okular::FormField *f : formFields ) { if ( f->type() == Okular::FormField::FormSignature ) return true; } } return false; } return QVariant(); } bool PDFGenerator::reparseConfig() { if ( !pdfdoc ) return false; bool somethingchanged = false; // load paper color QColor color = documentMetaData( PaperColorMetaData, true ).value< QColor >(); // if paper color is changed we have to rebuild every visible pixmap in addition // to the outputDevice. it's the 'heaviest' case, other effect are just recoloring // over the page rendered on 'standard' white background. if ( color != pdfdoc->paperColor() ) { userMutex()->lock(); pdfdoc->setPaperColor(color); userMutex()->unlock(); somethingchanged = true; } bool aaChanged = setDocumentRenderHints(); somethingchanged = somethingchanged || aaChanged; return somethingchanged; } void PDFGenerator::addPages( KConfigDialog *dlg ) { Ui_PDFSettingsWidget pdfsw; QWidget* w = new QWidget(dlg); pdfsw.setupUi(w); dlg->addPage(w, PDFSettings::self(), i18n("PDF"), QStringLiteral("application-pdf"), i18n("PDF Backend Configuration") ); } bool PDFGenerator::setDocumentRenderHints() { bool changed = false; const Poppler::Document::RenderHints oldhints = pdfdoc->renderHints(); #define SET_HINT(hintname, hintdefvalue, hintflag) \ { \ bool newhint = documentMetaData(hintname, hintdefvalue).toBool(); \ if (newhint != oldhints.testFlag(hintflag)) \ { \ pdfdoc->setRenderHint(hintflag, newhint); \ changed = true; \ } \ } SET_HINT(GraphicsAntialiasMetaData, true, Poppler::Document::Antialiasing) SET_HINT(TextAntialiasMetaData, true, Poppler::Document::TextAntialiasing) SET_HINT(TextHintingMetaData, false, Poppler::Document::TextHinting) #undef SET_HINT // load thin line mode const int thinLineMode = PDFSettings::enhanceThinLines(); const bool enableThinLineSolid = thinLineMode == PDFSettings::EnumEnhanceThinLines::Solid; const bool enableShapeLineSolid = thinLineMode == PDFSettings::EnumEnhanceThinLines::Shape; const bool thinLineSolidWasEnabled = (oldhints & Poppler::Document::ThinLineSolid) == Poppler::Document::ThinLineSolid; const bool thinLineShapeWasEnabled = (oldhints & Poppler::Document::ThinLineShape) == Poppler::Document::ThinLineShape; if (enableThinLineSolid != thinLineSolidWasEnabled) { pdfdoc->setRenderHint(Poppler::Document::ThinLineSolid, enableThinLineSolid); changed = true; } if (enableShapeLineSolid != thinLineShapeWasEnabled) { pdfdoc->setRenderHint(Poppler::Document::ThinLineShape, enableShapeLineSolid); changed = true; } return changed; } Okular::ExportFormat::List PDFGenerator::exportFormats() const { static Okular::ExportFormat::List formats; if ( formats.isEmpty() ) { formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::PlainText ) ); } return formats; } bool PDFGenerator::exportTo( const QString &fileName, const Okular::ExportFormat &format ) { if ( format.mimeType().inherits( QStringLiteral( "text/plain" ) ) ) { QFile f( fileName ); if ( !f.open( QIODevice::WriteOnly ) ) return false; QTextStream ts( &f ); int num = document()->pages(); for ( int i = 0; i < num; ++i ) { QString text; userMutex()->lock(); Poppler::Page *pp = pdfdoc->page(i); if (pp) { text = pp->text(QRect()).normalized(QString::NormalizationForm_KC); } userMutex()->unlock(); ts << text; delete pp; } f.close(); return true; } return false; } //END Generator inherited functions inline void append (Okular::TextPage* ktp, const QString &s, double l, double b, double r, double t) { // kWarning(PDFDebug).nospace() << "text: " << s << " at (" << l << "," << t << ")x(" << r <<","<append(s, new Okular::NormalizedRect(l, t, r, b)); } Okular::TextPage * PDFGenerator::abstractTextPage(const QList &text, double height, double width,int rot) { Q_UNUSED(rot); Okular::TextPage* ktp=new Okular::TextPage; Poppler::TextBox *next; #ifdef PDFGENERATOR_DEBUG qCDebug(OkularPdfDebug) << "getting text page in generator pdf - rotation:" << rot; #endif QString s; bool addChar; for (const Poppler::TextBox *word : text) { const int qstringCharCount = word->text().length(); next=word->nextWord(); int textBoxChar = 0; for (int j = 0; j < qstringCharCount; j++) { const QChar c = word->text().at(j); if (c.isHighSurrogate()) { s = c; addChar = false; } else if (c.isLowSurrogate()) { s += c; addChar = true; } else { s = c; addChar = true; } if (addChar) { QRectF charBBox = word->charBoundingBox(textBoxChar); append(ktp, (j==qstringCharCount-1 && !next) ? (s + QLatin1Char('\n')) : s, charBBox.left()/width, charBBox.bottom()/height, charBBox.right()/width, charBBox.top()/height); textBoxChar++; } } if ( word->hasSpaceAfter() && next ) { // TODO Check with a document with vertical text // probably won't work and we will need to do comparisons // between wordBBox and nextWordBBox to see if they are // vertically or horizontally aligned QRectF wordBBox = word->boundingBox(); QRectF nextWordBBox = next->boundingBox(); append(ktp, QStringLiteral(" "), wordBBox.right()/width, wordBBox.bottom()/height, nextWordBBox.left()/width, wordBBox.top()/height); } } return ktp; } void PDFGenerator::addSynopsisChildren( QDomNode * parent, QDomNode * parentDestination ) { // keep track of the current listViewItem QDomNode n = parent->firstChild(); while( !n.isNull() ) { // convert the node to an element (sure it is) QDomElement e = n.toElement(); // The name is the same QDomElement item = docSyn.createElement( e.tagName() ); parentDestination->appendChild(item); if (!e.attribute(QStringLiteral("ExternalFileName")).isNull()) item.setAttribute(QStringLiteral("ExternalFileName"), e.attribute(QStringLiteral("ExternalFileName"))); if (!e.attribute(QStringLiteral("DestinationName")).isNull()) item.setAttribute(QStringLiteral("ViewportName"), e.attribute(QStringLiteral("DestinationName"))); if (!e.attribute(QStringLiteral("Destination")).isNull()) { Okular::DocumentViewport vp; fillViewportFromLinkDestination( vp, Poppler::LinkDestination(e.attribute(QStringLiteral("Destination"))) ); item.setAttribute( QStringLiteral("Viewport"), vp.toString() ); } if (!e.attribute(QStringLiteral("Open")).isNull()) item.setAttribute(QStringLiteral("Open"), e.attribute(QStringLiteral("Open"))); if (!e.attribute(QStringLiteral("DestinationURI")).isNull()) item.setAttribute(QStringLiteral("URL"), e.attribute(QStringLiteral("DestinationURI"))); // descend recursively and advance to the next node if ( e.hasChildNodes() ) addSynopsisChildren( &n, & item ); n = n.nextSibling(); } } void PDFGenerator::addAnnotations( Poppler::Page * popplerPage, Okular::Page * page ) { QSet subtypes; subtypes << Poppler::Annotation::AFileAttachment << Poppler::Annotation::ASound << Poppler::Annotation::AMovie << Poppler::Annotation::AWidget << Poppler::Annotation::AScreen << Poppler::Annotation::AText << Poppler::Annotation::ALine << Poppler::Annotation::AGeom << Poppler::Annotation::AHighlight << Poppler::Annotation::AInk << Poppler::Annotation::AStamp << Poppler::Annotation::ACaret; const QList popplerAnnotations = popplerPage->annotations( subtypes ); for (Poppler::Annotation *a : popplerAnnotations) { bool doDelete = true; Okular::Annotation * newann = createAnnotationFromPopplerAnnotation( a, &doDelete ); if (newann) { page->addAnnotation(newann); if ( a->subType() == Poppler::Annotation::AScreen ) { Poppler::ScreenAnnotation *annotScreen = static_cast( a ); Okular::ScreenAnnotation *screenAnnotation = static_cast( newann ); // The activation action const Poppler::Link *actionLink = annotScreen->action(); if ( actionLink ) screenAnnotation->setAction( createLinkFromPopplerLink( actionLink ) ); // The additional actions const Poppler::Link *pageOpeningLink = annotScreen->additionalAction( Poppler::Annotation::PageOpeningAction ); if ( pageOpeningLink ) screenAnnotation->setAdditionalAction( Okular::Annotation::PageOpening, createLinkFromPopplerLink( pageOpeningLink ) ); const Poppler::Link *pageClosingLink = annotScreen->additionalAction( Poppler::Annotation::PageClosingAction ); if ( pageClosingLink ) screenAnnotation->setAdditionalAction( Okular::Annotation::PageClosing, createLinkFromPopplerLink( pageClosingLink ) ); } if ( a->subType() == Poppler::Annotation::AWidget ) { Poppler::WidgetAnnotation *annotWidget = static_cast( a ); Okular::WidgetAnnotation *widgetAnnotation = static_cast( newann ); // The additional actions const Poppler::Link *pageOpeningLink = annotWidget->additionalAction( Poppler::Annotation::PageOpeningAction ); if ( pageOpeningLink ) widgetAnnotation->setAdditionalAction( Okular::Annotation::PageOpening, createLinkFromPopplerLink( pageOpeningLink ) ); const Poppler::Link *pageClosingLink = annotWidget->additionalAction( Poppler::Annotation::PageClosingAction ); if ( pageClosingLink ) widgetAnnotation->setAdditionalAction( Okular::Annotation::PageClosing, createLinkFromPopplerLink( pageClosingLink ) ); } if ( !doDelete ) annotationsOnOpenHash.insert( newann, a ); } if ( doDelete ) delete a; } } void PDFGenerator::addTransition( Poppler::Page * pdfPage, Okular::Page * page ) // called on opening when MUTEX is not used { Poppler::PageTransition *pdfTransition = pdfPage->transition(); if ( !pdfTransition || pdfTransition->type() == Poppler::PageTransition::Replace ) return; Okular::PageTransition *transition = new Okular::PageTransition(); switch ( pdfTransition->type() ) { case Poppler::PageTransition::Replace: // won't get here, added to avoid warning break; case Poppler::PageTransition::Split: transition->setType( Okular::PageTransition::Split ); break; case Poppler::PageTransition::Blinds: transition->setType( Okular::PageTransition::Blinds ); break; case Poppler::PageTransition::Box: transition->setType( Okular::PageTransition::Box ); break; case Poppler::PageTransition::Wipe: transition->setType( Okular::PageTransition::Wipe ); break; case Poppler::PageTransition::Dissolve: transition->setType( Okular::PageTransition::Dissolve ); break; case Poppler::PageTransition::Glitter: transition->setType( Okular::PageTransition::Glitter ); break; case Poppler::PageTransition::Fly: transition->setType( Okular::PageTransition::Fly ); break; case Poppler::PageTransition::Push: transition->setType( Okular::PageTransition::Push ); break; case Poppler::PageTransition::Cover: transition->setType( Okular::PageTransition::Cover ); break; case Poppler::PageTransition::Uncover: transition->setType( Okular::PageTransition::Uncover ); break; case Poppler::PageTransition::Fade: transition->setType( Okular::PageTransition::Fade ); break; } transition->setDuration( pdfTransition->durationReal() ); switch ( pdfTransition->alignment() ) { case Poppler::PageTransition::Horizontal: transition->setAlignment( Okular::PageTransition::Horizontal ); break; case Poppler::PageTransition::Vertical: transition->setAlignment( Okular::PageTransition::Vertical ); break; } switch ( pdfTransition->direction() ) { case Poppler::PageTransition::Inward: transition->setDirection( Okular::PageTransition::Inward ); break; case Poppler::PageTransition::Outward: transition->setDirection( Okular::PageTransition::Outward ); break; } transition->setAngle( pdfTransition->angle() ); transition->setScale( pdfTransition->scale() ); transition->setIsRectangular( pdfTransition->isRectangular() ); page->setTransition( transition ); } void PDFGenerator::addFormFields( Poppler::Page * popplerPage, Okular::Page * page ) { const QList popplerFormFields = popplerPage->formFields(); QLinkedList okularFormFields; for ( Poppler::FormField *f : popplerFormFields ) { Okular::FormField * of = nullptr; switch ( f->type() ) { case Poppler::FormField::FormButton: of = new PopplerFormFieldButton( std::unique_ptr( static_cast( f ) ) ); break; case Poppler::FormField::FormText: of = new PopplerFormFieldText( std::unique_ptr( static_cast( f ) ) ); break; case Poppler::FormField::FormChoice: of = new PopplerFormFieldChoice( std::unique_ptr( static_cast( f ) ) ); break; case Poppler::FormField::FormSignature: { of = new PopplerFormFieldSignature( std::unique_ptr( static_cast( f ) ) ); break; } default: ; } if ( of ) // form field created, good - it will take care of the Poppler::FormField okularFormFields.append( of ); else // no form field available - delete the Poppler::FormField delete f; } if ( !okularFormFields.isEmpty() ) page->setFormFields( okularFormFields ); } PDFGenerator::PrintError PDFGenerator::printError() const { return lastPrintError; } Okular::PrintOptionsWidget* PDFGenerator::printConfigurationWidget() const { if ( !pdfOptionsPage ) { const_cast(this)->pdfOptionsPage = new PDFOptionsPage(); } return pdfOptionsPage; } bool PDFGenerator::supportsOption( SaveOption option ) const { switch ( option ) { case SaveChanges: { return true; } default: ; } return false; } bool PDFGenerator::save( const QString &fileName, SaveOptions options, QString *errorText ) { Q_UNUSED(errorText); Poppler::PDFConverter *pdfConv = pdfdoc->pdfConverter(); pdfConv->setOutputFileName( fileName ); if ( options & SaveChanges ) pdfConv->setPDFOptions( pdfConv->pdfOptions() | Poppler::PDFConverter::WithChanges ); QMutexLocker locker( userMutex() ); QHashIterator it( annotationsOnOpenHash ); while ( it.hasNext() ) { it.next(); if ( it.value()->uniqueName().isEmpty() ) { it.value()->setUniqueName( it.key()->uniqueName() ); } } bool success = pdfConv->convert(); if (!success) { switch (pdfConv->lastError()) { case Poppler::BaseConverter::NotSupportedInputFileError: // This can only happen with Poppler before 0.22 which did not have qt5 version break; case Poppler::BaseConverter::NoError: case Poppler::BaseConverter::FileLockedError: // we can't get here break; case Poppler::BaseConverter::OpenOutputError: // the default text message is good for this case break; } } delete pdfConv; return success; } Okular::AnnotationProxy* PDFGenerator::annotationProxy() const { return annotProxy; } #include "generator_pdf.moc" Q_LOGGING_CATEGORY(OkularPdfDebug, "org.kde.okular.generators.pdf", QtWarningMsg) /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/pagepainter.cpp b/ui/pagepainter.cpp index 13db2624b..65ef25ec8 100644 --- a/ui/pagepainter.cpp +++ b/ui/pagepainter.cpp @@ -1,1230 +1,1230 @@ /*************************************************************************** * Copyright (C) 2005 by Enrico Ros * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "pagepainter.h" // qt / kde includes #include #include #include #include #include #include #include #include #include #include // system includes #include // local includes #include "core/area.h" #include "core/page.h" #include "core/page_p.h" #include "core/annotations.h" #include "core/utils.h" #include "guiutils.h" #include "settings.h" #include "core/observer.h" #include "core/tile.h" #include "settings_core.h" #include "ui/debug_ui.h" Q_GLOBAL_STATIC_WITH_ARGS( QPixmap, busyPixmap, ( KIconLoader::global()->loadIcon(QLatin1String("okular"), KIconLoader::NoGroup, IconSize(KIconLoader::Desktop), KIconLoader::DefaultState, QStringList(), 0, true) ) ) #define TEXTANNOTATION_ICONSIZE 24 inline QPen buildPen( const Okular::Annotation *ann, double width, const QColor &color ) { QPen p( QBrush( color ), width, ann->style().lineStyle() == Okular::Annotation::Dashed ? Qt::DashLine : Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin ); return p; } void PagePainter::paintPageOnPainter( QPainter * destPainter, const Okular::Page * page, Okular::DocumentObserver *observer, int flags, int scaledWidth, int scaledHeight, const QRect &limits ) { paintCroppedPageOnPainter( destPainter, page, observer, flags, scaledWidth, scaledHeight, limits, Okular::NormalizedRect( 0, 0, 1, 1 ), nullptr ); } void PagePainter::paintCroppedPageOnPainter( QPainter * destPainter, const Okular::Page * page, Okular::DocumentObserver *observer, int flags, int scaledWidth, int scaledHeight, const QRect &limits, const Okular::NormalizedRect &crop, Okular::NormalizedPoint *viewPortPoint ) { qreal dpr = destPainter->device()->devicePixelRatioF(); /* Calculate the cropped geometry of the page */ QRect scaledCrop = crop.geometry( scaledWidth, scaledHeight ); /* variables prefixed with d are in the device pixels coordinate system, which translates to the rendered output - that means, * multiplied with the device pixel ratio of the target PaintDevice */ const QRect dScaledCrop(QRectF(scaledCrop.x() * dpr, scaledCrop.y() * dpr, scaledCrop.width() * dpr, scaledCrop.height() * dpr).toAlignedRect()); int croppedWidth = scaledCrop.width(); int croppedHeight = scaledCrop.height(); int dScaledWidth = ceil(scaledWidth * dpr); int dScaledHeight = ceil(scaledHeight * dpr); const QRect dLimits(QRectF(limits.x() * dpr, limits.y() * dpr, limits.width() * dpr, limits.height() * dpr).toAlignedRect()); QColor paperColor = Qt::white; QColor backgroundColor = paperColor; if ( Okular::SettingsCore::changeColors() ) { switch ( Okular::SettingsCore::renderMode() ) { case Okular::SettingsCore::EnumRenderMode::Inverted: backgroundColor = Qt::black; break; case Okular::SettingsCore::EnumRenderMode::Paper: paperColor = Okular::SettingsCore::paperColor(); backgroundColor = paperColor; break; case Okular::SettingsCore::EnumRenderMode::Recolor: backgroundColor = Okular::Settings::recolorBackground(); break; default: ; } } destPainter->fillRect( limits, backgroundColor ); const bool hasTilesManager = page->hasTilesManager( observer ); QPixmap pixmap; if ( !hasTilesManager ) { /** 1 - RETRIEVE THE 'PAGE+ID' PIXMAP OR A SIMILAR 'PAGE' ONE **/ const QPixmap *p = page->_o_nearestPixmap( observer, dScaledWidth, dScaledHeight ); if (p != nullptr) { pixmap = *p; pixmap.setDevicePixelRatio( qApp->devicePixelRatio() ); } /** 1B - IF NO PIXMAP, DRAW EMPTY PAGE **/ double pixmapRescaleRatio = !pixmap.isNull() ? dScaledWidth / (double)pixmap.width() : -1; long pixmapPixels = !pixmap.isNull() ? (long)pixmap.width() * (long)pixmap.height() : 0; if ( pixmap.isNull() || pixmapRescaleRatio > 20.0 || pixmapRescaleRatio < 0.25 || (dScaledWidth > pixmap.width() && pixmapPixels > 60000000L) ) { // draw something on the blank page: the okular icon or a cross (as a fallback) if ( !busyPixmap()->isNull() ) { busyPixmap->setDevicePixelRatio(dpr); destPainter->drawPixmap( QPoint( 10, 10 ), *busyPixmap() ); } else { destPainter->setPen( Qt::gray ); destPainter->drawLine( 0, 0, croppedWidth-1, croppedHeight-1 ); destPainter->drawLine( 0, croppedHeight-1, croppedWidth-1, 0 ); } return; } } /** 2 - FIND OUT WHAT TO PAINT (Flags + Configuration + Presence) **/ bool canDrawHighlights = (flags & Highlights) && !page->m_highlights.isEmpty(); bool canDrawTextSelection = (flags & TextSelection) && page->textSelection(); bool canDrawAnnotations = (flags & Annotations) && !page->m_annotations.isEmpty(); bool enhanceLinks = (flags & EnhanceLinks) && Okular::Settings::highlightLinks(); bool enhanceImages = (flags & EnhanceImages) && Okular::Settings::highlightImages(); // vectors containing objects to draw // make this a qcolor, rect map, since we don't need // to know s_id here! we are only drawing this right? QList< QPair > * bufferedHighlights = nullptr; QList< Okular::Annotation * > * bufferedAnnotations = nullptr; QList< Okular::Annotation * > * unbufferedAnnotations = nullptr; Okular::Annotation *boundingRectOnlyAnn = nullptr; // Paint the bounding rect of this annotation // fill up lists with visible annotation/highlight objects/text selections if ( canDrawHighlights || canDrawTextSelection || canDrawAnnotations ) { // precalc normalized 'limits rect' for intersection double nXMin = ( (double)limits.left() / scaledWidth ) + crop.left, nXMax = ( (double)limits.right() / scaledWidth ) + crop.left, nYMin = ( (double)limits.top() / scaledHeight ) + crop.top, nYMax = ( (double)limits.bottom() / scaledHeight ) + crop.top; // append all highlights inside limits to their list if ( canDrawHighlights ) { if ( !bufferedHighlights ) bufferedHighlights = new QList< QPair >(); /* else {*/ Okular::NormalizedRect* limitRect = new Okular::NormalizedRect(nXMin, nYMin, nXMax, nYMax ); QLinkedList< Okular::HighlightAreaRect * >::const_iterator h2It = page->m_highlights.constBegin(), hEnd = page->m_highlights.constEnd(); Okular::HighlightAreaRect::const_iterator hIt; for ( ; h2It != hEnd; ++h2It ) for (hIt=(*h2It)->constBegin(); hIt!=(*h2It)->constEnd(); ++hIt) { if ((*hIt).intersects(limitRect)) bufferedHighlights->append( qMakePair((*h2It)->color,*hIt) ); } delete limitRect; //} } if ( canDrawTextSelection ) { if ( !bufferedHighlights ) bufferedHighlights = new QList< QPair >(); /* else {*/ Okular::NormalizedRect* limitRect = new Okular::NormalizedRect(nXMin, nYMin, nXMax, nYMax ); const Okular::RegularAreaRect *textSelection = page->textSelection(); Okular::HighlightAreaRect::const_iterator hIt = textSelection->constBegin(), hEnd = textSelection->constEnd(); for ( ; hIt != hEnd; ++hIt ) { if ( (*hIt).intersects( limitRect ) ) bufferedHighlights->append( qMakePair( page->textSelectionColor(), *hIt ) ); } delete limitRect; //} } // append annotations inside limits to the un/buffered list if ( canDrawAnnotations ) { QLinkedList< Okular::Annotation * >::const_iterator aIt = page->m_annotations.constBegin(), aEnd = page->m_annotations.constEnd(); for ( ; aIt != aEnd; ++aIt ) { Okular::Annotation * ann = *aIt; int flags = ann->flags(); if ( flags & Okular::Annotation::Hidden ) continue; if ( flags & Okular::Annotation::ExternallyDrawn ) { // ExternallyDrawn annots are never rendered by PagePainter. // Just paint the boundingRect if the annot is moved or resized. if ( flags & (Okular::Annotation::BeingMoved | Okular::Annotation::BeingResized) ) { boundingRectOnlyAnn = ann; } continue; } bool intersects = ann->transformedBoundingRectangle().intersects( nXMin, nYMin, nXMax, nYMax ); if ( ann->subType() == Okular::Annotation::AText ) { Okular::TextAnnotation * ta = static_cast< Okular::TextAnnotation * >( ann ); if ( ta->textType() == Okular::TextAnnotation::Linked ) { Okular::NormalizedRect iconrect( ann->transformedBoundingRectangle().left, ann->transformedBoundingRectangle().top, ann->transformedBoundingRectangle().left + TEXTANNOTATION_ICONSIZE / page->width(), ann->transformedBoundingRectangle().top + TEXTANNOTATION_ICONSIZE / page->height() ); intersects = iconrect.intersects( nXMin, nYMin, nXMax, nYMax ); } } if ( intersects ) { Okular::Annotation::SubType type = ann->subType(); if ( type == Okular::Annotation::ALine || type == Okular::Annotation::AHighlight || type == Okular::Annotation::AInk /*|| (type == Annotation::AGeom && ann->style().opacity() < 0.99)*/ ) { if ( !bufferedAnnotations ) bufferedAnnotations = new QList< Okular::Annotation * >(); bufferedAnnotations->append( ann ); } else { if ( !unbufferedAnnotations ) unbufferedAnnotations = new QList< Okular::Annotation * >(); unbufferedAnnotations->append( ann ); } } } } // end of intersections checking } /** 3 - ENABLE BACKBUFFERING IF DIRECT IMAGE MANIPULATION IS NEEDED **/ bool bufferAccessibility = (flags & Accessibility) && Okular::SettingsCore::changeColors() && (Okular::SettingsCore::renderMode() != Okular::SettingsCore::EnumRenderMode::Paper); bool useBackBuffer = bufferAccessibility || bufferedHighlights || bufferedAnnotations || viewPortPoint; QPixmap * backPixmap = nullptr; QPainter * mixedPainter = nullptr; QRect limitsInPixmap = limits.translated( scaledCrop.topLeft() ); QRect dLimitsInPixmap = dLimits.translated( dScaledCrop.topLeft() ); // limits within full (scaled but uncropped) pixmap /** 4A -- REGULAR FLOW. PAINT PIXMAP NORMAL OR RESCALED USING GIVEN QPAINTER **/ if ( !useBackBuffer ) { if ( hasTilesManager ) { const Okular::NormalizedRect normalizedLimits( limitsInPixmap, scaledWidth, scaledHeight ); const QList tiles = page->tilesAt( observer, normalizedLimits ); QList::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd(); while ( tIt != tEnd ) { const Okular::Tile &tile = *tIt; QRect tileRect = tile.rect().geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ); QRect dTileRect = QRectF(tileRect.x() * dpr, tileRect.y() * dpr, tileRect.width() * dpr, tileRect.height() * dpr).toAlignedRect(); QRect limitsInTile = limits & tileRect; QRectF dLimitsInTile = dLimits & dTileRect; if ( !limitsInTile.isEmpty() ) { QPixmap* tilePixmap = tile.pixmap(); tilePixmap->setDevicePixelRatio( qApp->devicePixelRatio() ); if ( tilePixmap->width() == dTileRect.width() && tilePixmap->height() == dTileRect.height() ) { destPainter->drawPixmap( limitsInTile.topLeft(), *tilePixmap, dLimitsInTile.translated( -dTileRect.topLeft() ) ); } else { destPainter->drawPixmap( tileRect, *tilePixmap ); } } tIt++; } } else { QPixmap scaledCroppedPixmap = pixmap.scaled(dScaledWidth, dScaledHeight).copy(dLimitsInPixmap); scaledCroppedPixmap.setDevicePixelRatio(dpr); destPainter->drawPixmap( limits.topLeft(), scaledCroppedPixmap, QRectF(0, 0, dLimits.width(),dLimits.height())); } // 4A.2. active painter is the one passed to this method mixedPainter = destPainter; } /** 4B -- BUFFERED FLOW. IMAGE PAINTING + OPERATIONS. QPAINTER OVER PIXMAP **/ else { // the image over which we are going to draw QImage backImage = QImage( dLimits.width(), dLimits.height(), QImage::Format_ARGB32_Premultiplied ); backImage.setDevicePixelRatio(dpr); backImage.fill( paperColor ); QPainter p( &backImage ); if ( hasTilesManager ) { const Okular::NormalizedRect normalizedLimits( limitsInPixmap, scaledWidth, scaledHeight ); const QList tiles = page->tilesAt( observer, normalizedLimits ); QList::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd(); while ( tIt != tEnd ) { const Okular::Tile &tile = *tIt; QRect tileRect = tile.rect().geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ); QRect dTileRect(QRectF(tileRect.x() * dpr, tileRect.y() * dpr, tileRect.width() * dpr, tileRect.height() * dpr).toAlignedRect()); QRect limitsInTile = limits & tileRect; QRect dLimitsInTile = dLimits & dTileRect; if ( !limitsInTile.isEmpty() ) { QPixmap* tilePixmap = tile.pixmap(); tilePixmap->setDevicePixelRatio( qApp->devicePixelRatio() ); if ( tilePixmap->width() == dTileRect.width() && tilePixmap->height() == dTileRect.height() ) { p.drawPixmap( limitsInTile.translated( -limits.topLeft() ).topLeft(), *tilePixmap, dLimitsInTile.translated( -dTileRect.topLeft() ) ); } else { double xScale = tilePixmap->width() / (double)dTileRect.width(); double yScale = tilePixmap->height() / (double)dTileRect.height(); QTransform transform( xScale, 0, 0, yScale, 0, 0 ); p.drawPixmap( limitsInTile.translated( -limits.topLeft() ), *tilePixmap, transform.mapRect( dLimitsInTile ).translated( -transform.mapRect( dTileRect ).topLeft() ) ); } } ++tIt; } } else { // 4B.1. draw the page pixmap: normal or scaled QPixmap scaledCroppedPixmap = pixmap.scaled(dScaledWidth, dScaledHeight).copy(dLimitsInPixmap); scaledCroppedPixmap.setDevicePixelRatio(dpr); p.drawPixmap( 0, 0, scaledCroppedPixmap ); } p.end(); // 4B.2. modify pixmap following accessibility settings if ( bufferAccessibility ) { switch ( Okular::SettingsCore::renderMode() ) { case Okular::SettingsCore::EnumRenderMode::Inverted: // Invert image pixels using QImage internal function backImage.invertPixels(QImage::InvertRgb); break; case Okular::SettingsCore::EnumRenderMode::Recolor: recolor(&backImage, Okular::Settings::recolorForeground(), Okular::Settings::recolorBackground()); break; case Okular::SettingsCore::EnumRenderMode::BlackWhite: // Manual Gray and Contrast unsigned int * data = reinterpret_cast(backImage.bits()); int val, pixels = backImage.width() * backImage.height(), con = Okular::Settings::bWContrast(), thr = 255 - Okular::Settings::bWThreshold(); for( int i = 0; i < pixels; ++i ) { val = qGray( data[i] ); if ( val > thr ) val = 128 + (127 * (val - thr)) / (255 - thr); else if ( val < thr ) val = (128 * val) / thr; if ( con > 2 ) { val = con * ( val - thr ) / 2 + thr; if ( val > 255 ) val = 255; else if ( val < 0 ) val = 0; } data[i] = qRgba( val, val, val, 255 ); } break; } } // 4B.3. highlight rects in page if ( bufferedHighlights ) { // draw highlights that are inside the 'limits' paint region - for (const auto& highlight : *bufferedHighlights) + for (const auto& highlight : qAsConst(*bufferedHighlights)) { const Okular::NormalizedRect & r = highlight.second; // find out the rect to highlight on pixmap QRect highlightRect = r.geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ).intersected( limits ); highlightRect.translate( -limits.left(), -limits.top() ); const QColor highlightColor = highlight.first; QPainter painter(&backImage); painter.setCompositionMode(QPainter::CompositionMode_Multiply); painter.fillRect(highlightRect, highlightColor); auto frameColor = highlightColor.darker(150); const QRect frameRect = r.geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ).translated( -limits.left(), -limits.top() ); painter.setPen(frameColor); painter.drawRect(frameRect); } } // 4B.4. paint annotations [COMPOSITED ONES] if ( bufferedAnnotations ) { // Albert: This is quite "heavy" but all the backImage that reach here are QImage::Format_ARGB32_Premultiplied // and have to be so that the QPainter::CompositionMode_Multiply works // we could also put a // backImage = backImage.convertToFormat(QImage::Format_ARGB32_Premultiplied) // that would be almost a noop, but we'll leave the assert for now Q_ASSERT(backImage.format() == QImage::Format_ARGB32_Premultiplied); // precalc constants for normalizing [0,1] page coordinates into normalized [0,1] limit rect coordinates double pageScale = (double)croppedWidth / page->width(); double xOffset = (double)limits.left() / (double)scaledWidth + crop.left, xScale = (double)scaledWidth / (double)limits.width(), yOffset = (double)limits.top() / (double)scaledHeight + crop.top, yScale = (double)scaledHeight / (double)limits.height(); // paint all buffered annotations in the page QList< Okular::Annotation * >::const_iterator aIt = bufferedAnnotations->constBegin(), aEnd = bufferedAnnotations->constEnd(); for ( ; aIt != aEnd; ++aIt ) { Okular::Annotation * a = *aIt; Okular::Annotation::SubType type = a->subType(); QColor acolor = a->style().color(); if ( !acolor.isValid() ) acolor = Qt::yellow; acolor.setAlphaF( a->style().opacity() ); // draw LineAnnotation MISSING: caption, dash pattern, endings for multipoint lines if ( type == Okular::Annotation::ALine ) { LineAnnotPainter linepainter { (Okular::LineAnnotation *) a, { page->width(), page->height() }, pageScale, { xScale, 0., 0., yScale, -xOffset * xScale, -yOffset * yScale } }; linepainter.draw( backImage ); } // draw HighlightAnnotation MISSING: under/strike width, feather, capping else if ( type == Okular::Annotation::AHighlight ) { // get the annotation Okular::HighlightAnnotation * ha = (Okular::HighlightAnnotation *) a; Okular::HighlightAnnotation::HighlightType type = ha->highlightType(); // draw each quad of the annotation int quads = ha->highlightQuads().size(); for ( int q = 0; q < quads; q++ ) { NormalizedPath path; const Okular::HighlightAnnotation::Quad & quad = ha->highlightQuads()[ q ]; // normalize page point to image for ( int i = 0; i < 4; i++ ) { Okular::NormalizedPoint point; point.x = (quad.transformedPoint( i ).x - xOffset) * xScale; point.y = (quad.transformedPoint( i ).y - yOffset) * yScale; path.append( point ); } // draw the normalized path into image switch ( type ) { // highlight the whole rect case Okular::HighlightAnnotation::Highlight: drawShapeOnImage( backImage, path, true, Qt::NoPen, acolor, pageScale, Multiply ); break; // highlight the bottom part of the rect case Okular::HighlightAnnotation::Squiggly: path[ 3 ].x = ( path[ 0 ].x + path[ 3 ].x ) / 2.0; path[ 3 ].y = ( path[ 0 ].y + path[ 3 ].y ) / 2.0; path[ 2 ].x = ( path[ 1 ].x + path[ 2 ].x ) / 2.0; path[ 2 ].y = ( path[ 1 ].y + path[ 2 ].y ) / 2.0; drawShapeOnImage( backImage, path, true, Qt::NoPen, acolor, pageScale, Multiply ); break; // make a line at 3/4 of the height case Okular::HighlightAnnotation::Underline: path[ 0 ].x = ( 3 * path[ 0 ].x + path[ 3 ].x ) / 4.0; path[ 0 ].y = ( 3 * path[ 0 ].y + path[ 3 ].y ) / 4.0; path[ 1 ].x = ( 3 * path[ 1 ].x + path[ 2 ].x ) / 4.0; path[ 1 ].y = ( 3 * path[ 1 ].y + path[ 2 ].y ) / 4.0; path.pop_back(); path.pop_back(); drawShapeOnImage( backImage, path, false, QPen( acolor, 2 ), QBrush(), pageScale ); break; // make a line at 1/2 of the height case Okular::HighlightAnnotation::StrikeOut: path[ 0 ].x = ( path[ 0 ].x + path[ 3 ].x ) / 2.0; path[ 0 ].y = ( path[ 0 ].y + path[ 3 ].y ) / 2.0; path[ 1 ].x = ( path[ 1 ].x + path[ 2 ].x ) / 2.0; path[ 1 ].y = ( path[ 1 ].y + path[ 2 ].y ) / 2.0; path.pop_back(); path.pop_back(); drawShapeOnImage( backImage, path, false, QPen( acolor, 2 ), QBrush(), pageScale ); break; } } } // draw InkAnnotation MISSING:invar width, PENTRACER else if ( type == Okular::Annotation::AInk ) { // get the annotation Okular::InkAnnotation * ia = (Okular::InkAnnotation *) a; // draw each ink path const QList< QLinkedList > transformedInkPaths = ia->transformedInkPaths(); const QPen inkPen = buildPen( a, a->style().width(), acolor ); int paths = transformedInkPaths.size(); for ( int p = 0; p < paths; p++ ) { NormalizedPath path; const QLinkedList & inkPath = transformedInkPaths[ p ]; // normalize page point to image QLinkedList::const_iterator pIt = inkPath.constBegin(), pEnd = inkPath.constEnd(); for ( ; pIt != pEnd; ++pIt ) { const Okular::NormalizedPoint & inkPoint = *pIt; Okular::NormalizedPoint point; point.x = (inkPoint.x - xOffset) * xScale; point.y = (inkPoint.y - yOffset) * yScale; path.append( point ); } // draw the normalized path into image drawShapeOnImage( backImage, path, false, inkPen, QBrush(), pageScale ); } } } // end current annotation drawing } if(viewPortPoint) { QPainter painter(&backImage); painter.translate( -limits.left(), -limits.top() ); painter.setPen( QApplication::palette().color( QPalette::Active, QPalette::Highlight ) ); painter.drawLine( 0, viewPortPoint->y * scaledHeight + 1, scaledWidth - 1, viewPortPoint->y * scaledHeight + 1 ); // ROTATION CURRENTLY NOT IMPLEMENTED /* if( page->rotation() == Okular::Rotation0) { } else if(page->rotation() == Okular::Rotation270) { painter.drawLine( viewPortPoint->y * scaledHeight + 1, 0, viewPortPoint->y * scaledHeight + 1, scaledWidth - 1); } else if(page->rotation() == Okular::Rotation180) { painter.drawLine( 0, (1.0 - viewPortPoint->y) * scaledHeight - 1, scaledWidth - 1, (1.0 - viewPortPoint->y) * scaledHeight - 1 ); } else if(page->rotation() == Okular::Rotation90) // not right, rotation clock-wise { painter.drawLine( scaledWidth - (viewPortPoint->y * scaledHeight + 1), 0, scaledWidth - (viewPortPoint->y * scaledHeight + 1), scaledWidth - 1); } */ } // 4B.5. create the back pixmap converting from the local image backPixmap = new QPixmap( QPixmap::fromImage( backImage ) ); backPixmap->setDevicePixelRatio(dpr); // 4B.6. create a painter over the pixmap and set it as the active one mixedPainter = new QPainter( backPixmap ); mixedPainter->translate( -limits.left(), -limits.top() ); } /** 5 -- MIXED FLOW. Draw ANNOTATIONS [OPAQUE ONES] on ACTIVE PAINTER **/ if ( unbufferedAnnotations ) { // iterate over annotations and paint AText, AGeom, AStamp QList< Okular::Annotation * >::const_iterator aIt = unbufferedAnnotations->constBegin(), aEnd = unbufferedAnnotations->constEnd(); for ( ; aIt != aEnd; ++aIt ) { Okular::Annotation * a = *aIt; // honor opacity settings on supported types unsigned int opacity = (unsigned int)( a->style().color().alpha() * a->style().opacity() ); // skip the annotation drawing if all the annotation is fully // transparent, but not with text annotations if ( opacity <= 0 && a->subType() != Okular::Annotation::AText ) continue; QColor acolor = a->style().color(); if ( !acolor.isValid() ) acolor = Qt::yellow; acolor.setAlpha( opacity ); // get annotation boundary and drawn rect QRect annotBoundary = a->transformedBoundingRectangle().geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ); QRect annotRect = annotBoundary.intersected( limits ); QRect innerRect( annotRect.left() - annotBoundary.left(), annotRect.top() - annotBoundary.top(), annotRect.width(), annotRect.height() ); QRectF dInnerRect(innerRect.x() * dpr, innerRect.y() * dpr, innerRect.width() * dpr, innerRect.height() * dpr); Okular::Annotation::SubType type = a->subType(); // draw TextAnnotation if ( type == Okular::Annotation::AText ) { Okular::TextAnnotation * text = (Okular::TextAnnotation *)a; if ( text->textType() == Okular::TextAnnotation::InPlace ) { QImage image( annotBoundary.size(), QImage::Format_ARGB32 ); image.fill( acolor.rgba() ); QPainter painter( &image ); painter.setFont( text->textFont() ); painter.setPen( text->textColor() ); Qt::AlignmentFlag halign = ( text->inplaceAlignment() == 1 ? Qt::AlignHCenter : ( text->inplaceAlignment() == 2 ? Qt::AlignRight : Qt::AlignLeft ) ); const double invXScale = (double)page->width() / scaledWidth; const double invYScale = (double)page->height() / scaledHeight; const double borderWidth = text->style().width(); painter.scale( 1 / invXScale, 1 / invYScale ); painter.drawText( borderWidth * invXScale, borderWidth * invYScale, (image.width() - 2 * borderWidth) * invXScale, (image.height() - 2 * borderWidth) * invYScale, Qt::AlignTop | halign | Qt::TextWordWrap, text->contents() ); painter.resetTransform(); //Required as asking for a zero width pen results //in a default width pen (1.0) being created if ( borderWidth != 0 ) { QPen pen( Qt::black, borderWidth ); painter.setPen( pen ); painter.drawRect( 0, 0, image.width() - 1, image.height() - 1 ); } painter.end(); mixedPainter->drawImage( annotBoundary.topLeft(), image ); } else if ( text->textType() == Okular::TextAnnotation::Linked ) { // get pixmap, colorize and alpha-blend it QString path; QPixmap pixmap = GuiUtils::iconLoader()->loadIcon( text->textIcon().toLower(), KIconLoader::User, 32, KIconLoader::DefaultState, QStringList(), &path, true ); if ( path.isEmpty() ) pixmap = GuiUtils::iconLoader()->loadIcon( text->textIcon().toLower(), KIconLoader::NoGroup, 32 ); QRect annotBoundary2 = QRect( annotBoundary.topLeft(), QSize( TEXTANNOTATION_ICONSIZE * dpr, TEXTANNOTATION_ICONSIZE * dpr ) ); QRect annotRect2 = annotBoundary2.intersected( limits ); QRect innerRect2( annotRect2.left() - annotBoundary2.left(), annotRect2.top() - annotBoundary2.top(), annotRect2.width(), annotRect2.height() ); QPixmap scaledCroppedPixmap = pixmap.scaled(TEXTANNOTATION_ICONSIZE * dpr, TEXTANNOTATION_ICONSIZE * dpr).copy(dInnerRect.toAlignedRect()); scaledCroppedPixmap.setDevicePixelRatio(dpr); QImage scaledCroppedImage = scaledCroppedPixmap.toImage(); // if the annotation color is valid (ie it was set), then // use it to colorize the icon, otherwise the icon will be // "gray" if ( a->style().color().isValid() ) GuiUtils::colorizeImage( scaledCroppedImage, a->style().color(), opacity ); pixmap = QPixmap::fromImage( scaledCroppedImage ); // draw the mangled image to painter mixedPainter->drawPixmap( annotRect.topLeft(), pixmap); } } // draw StampAnnotation else if ( type == Okular::Annotation::AStamp ) { Okular::StampAnnotation * stamp = (Okular::StampAnnotation *)a; // get pixmap and alpha blend it if needed QPixmap pixmap = GuiUtils::loadStamp( stamp->stampIconName(), annotBoundary.width() ); if ( !pixmap.isNull() ) // should never happen but can happen on huge sizes { const QRect dInnerRect(QRectF(innerRect.x() * dpr, innerRect.y() * dpr, innerRect.width() * dpr, innerRect.height() * dpr).toAlignedRect()); QPixmap scaledCroppedPixmap = pixmap.scaled(annotBoundary.width() * dpr, annotBoundary.height() * dpr).copy(dInnerRect); scaledCroppedPixmap.setDevicePixelRatio(dpr); QImage scaledCroppedImage = scaledCroppedPixmap.toImage(); if ( opacity < 255 ) changeImageAlpha( scaledCroppedImage, opacity ); pixmap = QPixmap::fromImage( scaledCroppedImage ); // draw the scaled and al mixedPainter->drawPixmap( annotRect.topLeft(), pixmap ); } } // draw GeomAnnotation else if ( type == Okular::Annotation::AGeom ) { Okular::GeomAnnotation * geom = (Okular::GeomAnnotation *)a; // check whether there's anything to draw if ( geom->style().width() || geom->geometricalInnerColor().isValid() ) { mixedPainter->save(); const double width = geom->style().width() * Okular::Utils::realDpi(nullptr).width() / ( 72.0 * 2.0 ) * scaledWidth / page->width(); QRectF r( .0, .0, annotBoundary.width(), annotBoundary.height() ); r.adjust( width, width, -width, -width ); r.translate( annotBoundary.topLeft() ); if ( geom->geometricalInnerColor().isValid() ) { r.adjust( width, width, -width, -width ); const QColor color = geom->geometricalInnerColor(); mixedPainter->setPen( Qt::NoPen ); mixedPainter->setBrush( QColor( color.red(), color.green(), color.blue(), opacity ) ); if ( geom->geometricalType() == Okular::GeomAnnotation::InscribedSquare ) mixedPainter->drawRect( r ); else mixedPainter->drawEllipse( r ); r.adjust( -width, -width, width, width ); } if ( geom->style().width() ) // need to check the original size here.. { mixedPainter->setPen( buildPen( a, width * 2, acolor ) ); mixedPainter->setBrush( Qt::NoBrush ); if ( geom->geometricalType() == Okular::GeomAnnotation::InscribedSquare ) mixedPainter->drawRect( r ); else mixedPainter->drawEllipse( r ); } mixedPainter->restore(); } } // draw extents rectangle if ( Okular::Settings::debugDrawAnnotationRect() ) { mixedPainter->setPen( a->style().color() ); mixedPainter->drawRect( annotBoundary ); } } } if ( boundingRectOnlyAnn ) { QRect annotBoundary = boundingRectOnlyAnn->transformedBoundingRectangle().geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ); mixedPainter->setPen( Qt::DashLine ); mixedPainter->drawRect( annotBoundary ); } /** 6 -- MIXED FLOW. Draw LINKS+IMAGES BORDER on ACTIVE PAINTER **/ if ( enhanceLinks || enhanceImages ) { mixedPainter->save(); mixedPainter->scale( scaledWidth, scaledHeight ); mixedPainter->translate( -crop.left, -crop.top ); QColor normalColor = QApplication::palette().color( QPalette::Active, QPalette::Highlight ); // enlarging limits for intersection is like growing the 'rectGeometry' below QRect limitsEnlarged = limits; limitsEnlarged.adjust( -2, -2, 2, 2 ); // draw rects that are inside the 'limits' paint region as opaque rects QLinkedList< Okular::ObjectRect * >::const_iterator lIt = page->m_rects.constBegin(), lEnd = page->m_rects.constEnd(); for ( ; lIt != lEnd; ++lIt ) { Okular::ObjectRect * rect = *lIt; if ( (enhanceLinks && rect->objectType() == Okular::ObjectRect::Action) || (enhanceImages && rect->objectType() == Okular::ObjectRect::Image) ) { if ( limitsEnlarged.intersects( rect->boundingRect( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ) ) ) { mixedPainter->strokePath( rect->region(), QPen( normalColor, 0 ) ); } } } mixedPainter->restore(); } /** 7 -- BUFFERED FLOW. Copy BACKPIXMAP on DESTINATION PAINTER **/ if ( useBackBuffer ) { delete mixedPainter; destPainter->drawPixmap( limits.left(), limits.top(), *backPixmap ); delete backPixmap; } // delete object containers delete bufferedHighlights; delete bufferedAnnotations; delete unbufferedAnnotations; } /** Private Helpers :: Pixmap conversion **/ void PagePainter::cropPixmapOnImage( QImage & dest, const QPixmap * src, const QRect & r ) { qreal dpr = src->devicePixelRatioF(); // handle quickly the case in which the whole pixmap has to be converted if ( r == QRect( 0, 0, src->width() / dpr, src->height() / dpr ) ) { dest = src->toImage(); dest = dest.convertToFormat(QImage::Format_ARGB32_Premultiplied); } // else copy a portion of the src to an internal pixmap (smaller) and convert it else { QImage croppedImage( r.width() * dpr, r.height() * dpr, QImage::Format_ARGB32_Premultiplied ); croppedImage.setDevicePixelRatio(dpr); QPainter p( &croppedImage ); p.drawPixmap( 0, 0, *src, r.left(), r.top(), r.width(), r.height() ); p.end(); dest = croppedImage; } } void PagePainter::recolor(QImage *image, const QColor &foreground, const QColor &background) { if (image->format() != QImage::Format_ARGB32_Premultiplied) { qCWarning(OkularUiDebug) << "Wrong image format! Converting..."; *image = image->convertToFormat(QImage::Format_ARGB32_Premultiplied); } Q_ASSERT(image->format() == QImage::Format_ARGB32_Premultiplied); const float scaleRed = background.redF() - foreground.redF(); const float scaleGreen = background.greenF() - foreground.greenF(); const float scaleBlue = background.blueF() - foreground.blueF(); for (int y=0; yheight(); y++) { QRgb *pixels = reinterpret_cast(image->scanLine(y)); for (int x=0; xwidth(); x++) { const int lightness = qGray(pixels[x]); pixels[x] = qRgba(scaleRed * lightness + foreground.red(), scaleGreen * lightness + foreground.green(), scaleBlue * lightness + foreground.blue(), qAlpha(pixels[x])); } } } /** Private Helpers :: Image Drawing **/ // from Arthur - qt4 static inline int qt_div_255(int x) { return (x + (x>>8) + 0x80) >> 8; } void PagePainter::changeImageAlpha( QImage & image, unsigned int destAlpha ) { // iterate over all pixels changing the alpha component value unsigned int * data = reinterpret_cast(image.bits()); unsigned int pixels = image.width() * image.height(); int source, sourceAlpha; for( unsigned int i = 0; i < pixels; ++i ) { // optimize this loop keeping byte order into account source = data[i]; if ( (sourceAlpha = qAlpha( source )) == 255 ) { // use destAlpha data[i] = qRgba( qRed(source), qGreen(source), qBlue(source), destAlpha ); } else { // use destAlpha * sourceAlpha product sourceAlpha = qt_div_255( destAlpha * sourceAlpha ); data[i] = qRgba( qRed(source), qGreen(source), qBlue(source), sourceAlpha ); } } } void PagePainter::drawShapeOnImage( QImage & image, const NormalizedPath & normPath, bool closeShape, const QPen & pen, const QBrush & brush, double penWidthMultiplier, RasterOperation op //float antiAliasRadius ) { // safety checks int pointsNumber = normPath.size(); if ( pointsNumber < 2 ) return; int imageWidth = image.width(); int imageHeight = image.height(); double fImageWidth = (double)imageWidth; double fImageHeight = (double)imageHeight; // stroke outline double penWidth = (double)pen.width() * penWidthMultiplier; QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing); QPen pen2 = pen; pen2.setWidthF(penWidth); painter.setPen(pen2); painter.setBrush(brush); if (op == Multiply) { painter.setCompositionMode(QPainter::CompositionMode_Multiply); } if ( brush.style() == Qt::NoBrush ) { // create a polygon QPolygonF poly( closeShape ? pointsNumber + 1 : pointsNumber ); for ( int i = 0; i < pointsNumber; ++i ) { poly[ i ] = QPointF( normPath[ i ].x * fImageWidth, normPath[ i ].y * fImageHeight ); } if ( closeShape ) poly[ pointsNumber ] = poly[ 0 ]; painter.drawPolyline( poly ); } else { // create a 'path' QPainterPath path; path.setFillRule( Qt::WindingFill ); path.moveTo( normPath[ 0 ].x * fImageWidth, normPath[ 0 ].y * fImageHeight ); for ( int i = 1; i < pointsNumber; i++ ) { path.lineTo( normPath[ i ].x * fImageWidth, normPath[ i ].y * fImageHeight ); } if ( closeShape ) path.closeSubpath(); painter.drawPath( path ); } } void PagePainter::drawEllipseOnImage( QImage & image, const NormalizedPath & rect, const QPen & pen, const QBrush & brush, double penWidthMultiplier, RasterOperation op ) { const double fImageWidth = (double) image.width(); const double fImageHeight = (double) image.height(); // stroke outline const double penWidth = (double)pen.width() * penWidthMultiplier; QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing); QPen pen2 = pen; pen2.setWidthF(penWidth); painter.setPen(pen2); painter.setBrush(brush); if ( op == Multiply ) { painter.setCompositionMode(QPainter::CompositionMode_Multiply); } const QPointF &topLeft { rect[0].x * fImageWidth, rect[0].y * fImageHeight }; const QSizeF &size { (rect[1].x - rect[0].x) * fImageWidth, (rect[1].y - rect[0].y) * fImageHeight }; const QRectF imgRect { topLeft, size }; if ( brush.style() == Qt::NoBrush ) { painter.drawArc( imgRect, 0, 16*360 ); } else { painter.drawEllipse( imgRect ); } } LineAnnotPainter::LineAnnotPainter( const Okular::LineAnnotation * a, QSizeF pageSize, double pageScale, const QTransform &toNormalizedImage ) : la { a } , pageSize { pageSize } , pageScale { pageScale } , toNormalizedImage { toNormalizedImage } , aspectRatio { pageSize.height() / pageSize.width() } , linePen { buildPen( a, a->style().width(), a->style().color() ) } { if ( ( la->lineClosed() || la->transformedLinePoints().count() == 2 ) && la->lineInnerColor().isValid() ) { fillBrush = QBrush( la->lineInnerColor() ); } } void LineAnnotPainter::draw( QImage &image ) const { const QLinkedList transformedLinePoints = la->transformedLinePoints(); if ( transformedLinePoints.count() == 2 ) { const Okular::NormalizedPoint delta { transformedLinePoints.last().x - transformedLinePoints.first().x, transformedLinePoints.first().y - transformedLinePoints.last().y }; const double angle { atan2( delta.y * aspectRatio, delta.x ) }; const double cosA { cos( -angle ) }; const double sinA { sin( -angle ) }; const QTransform tmpMatrix = QTransform { cosA, sinA / aspectRatio, -sinA, cosA / aspectRatio, transformedLinePoints.first().x, transformedLinePoints.first().y }; const double deaspectedY { delta.y * aspectRatio }; const double mainSegmentLength { sqrt( delta.x * delta.x + deaspectedY * deaspectedY ) }; const double lineendSize { std::min( 6. * la->style().width() / pageSize.width(), mainSegmentLength / 2. ) }; drawShortenedLine( mainSegmentLength, lineendSize, image, tmpMatrix ); drawLineEnds( mainSegmentLength, lineendSize, image, tmpMatrix ); drawLeaderLine( 0., image, tmpMatrix ); drawLeaderLine( mainSegmentLength, image, tmpMatrix ); } else if ( transformedLinePoints.count() > 2 ) { drawMainLine( image ); } } void LineAnnotPainter::drawMainLine( QImage &image ) const { // draw the line as normalized path into image PagePainter::drawShapeOnImage( image, transformPath( la->transformedLinePoints(), toNormalizedImage ), la->lineClosed(), linePen, fillBrush, pageScale, PagePainter::Multiply ); } void LineAnnotPainter::drawShortenedLine( double mainSegmentLength, double size, QImage &image, const QTransform& toNormalizedPage ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const QList path { { shortenForArrow(size, la->lineStartStyle()), 0 }, { mainSegmentLength - shortenForArrow(size, la->lineEndStyle()), 0 } }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), la->lineClosed(), linePen, fillBrush, pageScale, PagePainter::Multiply ); } void LineAnnotPainter::drawLineEnds( double mainSegmentLength, double size, QImage &image, const QTransform& transform ) const { switch ( la->lineStartStyle() ) { case Okular::LineAnnotation::Square: drawLineEndSquare( 0, -size, transform, image ); break; case Okular::LineAnnotation::Circle: drawLineEndCircle( 0, -size, transform, image ); break; case Okular::LineAnnotation::Diamond: drawLineEndDiamond( 0, -size, transform, image ); break; case Okular::LineAnnotation::OpenArrow: drawLineEndArrow( 0, -size, 1., false, transform, image ); break; case Okular::LineAnnotation::ClosedArrow: drawLineEndArrow( 0, -size, 1., true, transform, image ); break; case Okular::LineAnnotation::None: break; case Okular::LineAnnotation::Butt: drawLineEndButt( 0, size, transform, image ); break; case Okular::LineAnnotation::ROpenArrow: drawLineEndArrow( 0, size, 1., false, transform, image ); break; case Okular::LineAnnotation::RClosedArrow: drawLineEndArrow( 0, size, 1., true, transform, image ); break; case Okular::LineAnnotation::Slash: drawLineEndSlash( 0, -size, transform, image ); break; } switch ( la->lineEndStyle() ) { case Okular::LineAnnotation::Square: drawLineEndSquare( mainSegmentLength, size, transform, image ); break; case Okular::LineAnnotation::Circle: drawLineEndCircle( mainSegmentLength, size, transform, image ); break; case Okular::LineAnnotation::Diamond: drawLineEndDiamond( mainSegmentLength, size, transform, image ); break; case Okular::LineAnnotation::OpenArrow: drawLineEndArrow( mainSegmentLength, size, 1., false, transform, image ); break; case Okular::LineAnnotation::ClosedArrow: drawLineEndArrow( mainSegmentLength, size, 1., true, transform, image ); break; case Okular::LineAnnotation::None: break; case Okular::LineAnnotation::Butt: drawLineEndButt( mainSegmentLength, size, transform, image ); break; case Okular::LineAnnotation::ROpenArrow: drawLineEndArrow( mainSegmentLength, size, -1., false, transform, image ); break; case Okular::LineAnnotation::RClosedArrow: drawLineEndArrow( mainSegmentLength, size, -1., true, transform, image ); break; case Okular::LineAnnotation::Slash: drawLineEndSlash( mainSegmentLength, size, transform, image ); break; } } void LineAnnotPainter::drawLineEndArrow( double xEndPos, double size, double flipX, bool close, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const QList path { { xEndPos - size * flipX, size / 2. }, { xEndPos, 0 }, { xEndPos - size * flipX, -size / 2. }, }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), close, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndButt( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const double halfSize { size / 2. }; const QList path { { xEndPos, halfSize }, { xEndPos, -halfSize }, }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndCircle( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { /* transform the circle midpoint to intermediate normalized coordinates * where it's easy to construct the bounding rect of the circle */ Okular::NormalizedPoint center; toNormalizedPage.map( xEndPos - size / 2., 0, ¢er.x, ¢er.y ); const double halfSize { size / 2. }; const QList path { { center.x - halfSize, center.y - halfSize / aspectRatio }, { center.x + halfSize, center.y + halfSize / aspectRatio }, }; /* then transform bounding rect with toNormalizedImage */ PagePainter::drawEllipseOnImage( image, transformPath(path, toNormalizedImage), linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndSquare( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const QList path { { xEndPos, size / 2. }, { xEndPos - size, size / 2. }, { xEndPos - size, -size / 2. }, { xEndPos, -size / 2. } }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndDiamond( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const QList path { { xEndPos, 0 }, { xEndPos - size / 2., size / 2. }, { xEndPos - size, 0 }, { xEndPos - size / 2., -size / 2. } }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndSlash( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const double halfSize { size / 2. }; const double xOffset { cos(M_PI/3.) * halfSize }; const QList path { { xEndPos - xOffset, halfSize }, { xEndPos + xOffset, -halfSize }, }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLeaderLine( double xEndPos, QImage &image, const QTransform& toNormalizedPage ) const { const QTransform combinedTransform = toNormalizedPage * toNormalizedImage; const double ll = aspectRatio * la->lineLeadingForwardPoint() / pageSize.height(); const double lle = aspectRatio * la->lineLeadingBackwardPoint() / pageSize.height(); const int sign { ll > 0 ? -1 : 1 }; QList path; if ( fabs( ll ) > 0 ) { path.append( { xEndPos, ll } ); // do we have the extension on the "back"? if ( fabs( lle ) > 0 ) { path.append( { xEndPos, sign * lle } ); } else { path.append( { xEndPos, 0 } ); } } PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), false, linePen, fillBrush, pageScale, PagePainter::Multiply); } double LineAnnotPainter::shortenForArrow( double size, Okular::LineAnnotation::TermStyle endStyle ) { double shortenBy { 0 }; if ( endStyle == Okular::LineAnnotation::Square || endStyle == Okular::LineAnnotation::Circle || endStyle == Okular::LineAnnotation::Diamond || endStyle == Okular::LineAnnotation::ClosedArrow ) { shortenBy = size; } return shortenBy; } /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/pageview.cpp b/ui/pageview.cpp index 2fc434f2c..bf7a2308a 100644 --- a/ui/pageview.cpp +++ b/ui/pageview.cpp @@ -1,5753 +1,5754 @@ /*************************************************************************** * 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, SIGNAL( changed( int ) ), q, SLOT( slotFormChanged( int ) ) ); QObject::connect( formsWidgetController, SIGNAL( action( Okular::Action* ) ), q, SLOT( slotAction( Okular::Action* ) ) ); 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()) ); updateZoomText(); d->aZoomIn = KStandardAction::zoomIn( this, SLOT(slotZoomIn()), ac ); d->aZoomOut = KStandardAction::zoomOut( this, SLOT(slotZoomOut()), ac ); d->aZoomActual = KStandardAction::actualSize( this, &PageView::slotZoomActual, ac ); d->aZoomActual->setText(i18n("Zoom to 100%")); } void PageView::setupViewerActions( KActionCollection * ac ) { d->actionCollection = ac; ac->setDefaultShortcut(d->aZoomIn, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Plus)); ac->setDefaultShortcut(d->aZoomOut, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Minus)); // orientation menu actions d->aRotateClockwise = new QAction( QIcon::fromTheme( QStringLiteral("object-rotate-right") ), i18n( "Rotate &Right" ), this ); d->aRotateClockwise->setIconText( i18nc( "Rotate right", "Right" ) ); ac->addAction( QStringLiteral("view_orientation_rotate_cw"), d->aRotateClockwise ); d->aRotateClockwise->setEnabled( false ); connect( d->aRotateClockwise, &QAction::triggered, this, &PageView::slotRotateClockwise ); d->aRotateCounterClockwise = new QAction( QIcon::fromTheme( QStringLiteral("object-rotate-left") ), i18n( "Rotate &Left" ), this ); d->aRotateCounterClockwise->setIconText( i18nc( "Rotate left", "Left" ) ); ac->addAction( QStringLiteral("view_orientation_rotate_ccw"), d->aRotateCounterClockwise ); d->aRotateCounterClockwise->setEnabled( false ); connect( d->aRotateCounterClockwise, &QAction::triggered, this, &PageView::slotRotateCounterClockwise ); d->aRotateOriginal = new QAction( i18n( "Original Orientation" ), this ); ac->addAction( QStringLiteral("view_orientation_original"), d->aRotateOriginal ); d->aRotateOriginal->setEnabled( false ); connect( d->aRotateOriginal, &QAction::triggered, this, &PageView::slotRotateOriginal ); d->aPageSizes = new KSelectAction(i18n("&Page Size"), this); ac->addAction(QStringLiteral("view_pagesizes"), d->aPageSizes); d->aPageSizes->setEnabled( false ); connect( d->aPageSizes , SIGNAL(triggered(int)), this, SLOT(slotPageSizes(int)) ); // Trim View actions d->aTrimMode = new KActionMenu(i18n( "&Trim View" ), this ); d->aTrimMode->setDelayed( false ); ac->addAction(QStringLiteral("view_trim_mode"), d->aTrimMode ); d->aTrimMargins = new KToggleAction(QIcon::fromTheme( QStringLiteral("trim-margins") ), i18n( "&Trim Margins" ), d->aTrimMode->menu() ); d->aTrimMode->addAction( d->aTrimMargins ); ac->addAction( QStringLiteral("view_trim_margins"), d->aTrimMargins ); d->aTrimMargins->setData( 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) { - for ( QAction* action : d->aViewMode->menu()->actions() ) { + 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, [ link ]() { const Okular::BrowseAction * browseLink = static_cast< const Okular::BrowseAction * >( link ); QClipboard *cb = QApplication::clipboard(); cb->setText( browseLink->url().toDisplayString(), QClipboard::Clipboard ); if ( cb->supportsSelection() ) cb->setText( browseLink->url().toDisplayString(), QClipboard::Selection ); } ); } connect( processLink, &QAction::triggered, [this, link]() { d->document->processAction( link ); }); return menu; } return nullptr; } 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, 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 ); const QString text = item->page()->text( area ); d->pagesWithTextSelection.insert( currentPage ); d->document->setPageTextSelection( currentPage, area, palette().color( QPalette::Active, QPalette::Highlight ) ); } } void PageView::highlightSignatureFormWidget( const Okular::FormFieldSignature *form ) { QVector< PageViewItem * >::const_iterator dIt = d->items.constBegin(), dEnd = d->items.constEnd(); for ( ; dIt != dEnd; ++dIt ) { 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/propertiesdialog.cpp b/ui/propertiesdialog.cpp index e1a028f0a..0d81d549c 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 ); } - // keys() is a const method - for (const QString &ks : info.keys()) { + 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*)) ); } 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; */ diff --git a/ui/signaturemodel.cpp b/ui/signaturemodel.cpp index 8d843c2cc..01cf21398 100644 --- a/ui/signaturemodel.cpp +++ b/ui/signaturemodel.cpp @@ -1,278 +1,278 @@ /*************************************************************************** * Copyright (C) 2018 by Chinmoy Ranjan Pradhan #include #include #include #include "core/document.h" #include "core/observer.h" #include "core/page.h" #include "core/form.h" #include "core/signatureutils.h" struct SignatureItem { enum DataType { Root, RevisionInfo, ValidityStatus, SigningTime, Reason, FieldInfo }; SignatureItem(); SignatureItem( SignatureItem *parent, const Okular::FormFieldSignature *form, DataType type, int page ); ~SignatureItem(); SignatureItem(const SignatureItem &) = delete; SignatureItem &operator=(const SignatureItem &) = delete; QVector children; SignatureItem *parent; const Okular::FormFieldSignature *form; QString displayString; DataType type; int page; }; SignatureItem::SignatureItem() : parent( nullptr ), form( nullptr ), type( Root ), page( -1 ) { } SignatureItem::SignatureItem( SignatureItem *_parent, const Okular::FormFieldSignature *_form , DataType _type, int _page ) : parent( _parent ), form( _form ), type( _type ), page( _page ) { Q_ASSERT( parent ); parent->children.append( this ); } SignatureItem::~SignatureItem() { qDeleteAll( children ); } class SignatureModelPrivate : public Okular::DocumentObserver { public: SignatureModelPrivate( SignatureModel *qq ); ~SignatureModelPrivate() override; void notifySetup( const QVector &pages, int setupFlags ) override; QModelIndex indexForItem( SignatureItem *item ) const; SignatureModel *q; SignatureItem *root; QPointer document; }; SignatureModelPrivate::SignatureModelPrivate( SignatureModel *qq ) : q( qq ), root( new SignatureItem ) { } SignatureModelPrivate::~SignatureModelPrivate() { delete root; } static void updateFormFieldSignaturePointer( SignatureItem *item, const QVector &pages ) { if ( item->form ) { const QLinkedList< Okular::FormField * > formFields = pages[item->page]->formFields(); - for ( Okular::FormField *f : pages[item->page]->formFields() ) + for ( Okular::FormField *f : formFields ) { if ( item->form->id() == f->id() ) { item->form = static_cast( f ); break; } } if ( !item->form ) qWarning() << "Lost signature form field, something went wrong"; } for ( SignatureItem *child : qAsConst(item->children) ) { updateFormFieldSignaturePointer( child, pages ); } } void SignatureModelPrivate::notifySetup( const QVector &pages, int setupFlags ) { if ( !( setupFlags & Okular::DocumentObserver::DocumentChanged ) ) { if ( setupFlags & Okular::DocumentObserver::UrlChanged ) { updateFormFieldSignaturePointer( root, pages ); } return; } q->beginResetModel(); qDeleteAll( root->children ); root->children.clear(); for ( const Okular::Page *page : pages ) { const int currentPage = page->number(); // get form fields page by page so that page number and index of the form can be determined. const QVector signatureFormFields = SignatureGuiUtils::getSignatureFormFields( document, false, currentPage ); if ( signatureFormFields.isEmpty() ) continue; for ( int i = 0; i < signatureFormFields.count(); i++ ) { const Okular::FormFieldSignature *sf = signatureFormFields[i]; const Okular::SignatureInfo &info = sf->signatureInfo(); // based on whether or not signature form is a nullptr it is decided if clicking on an item should change the viewport. auto *parentItem = new SignatureItem( root, sf, SignatureItem::RevisionInfo, currentPage ); parentItem->displayString = i18n( "Rev. %1: Signed By %2", i+1, info.signerName() ); auto childItem1 = new SignatureItem( parentItem, nullptr, SignatureItem::ValidityStatus, currentPage ); childItem1->displayString = SignatureGuiUtils::getReadableSignatureStatus( info.signatureStatus() ); auto childItem2 = new SignatureItem( parentItem, nullptr, SignatureItem::SigningTime, currentPage ); childItem2->displayString = i18n("Signing Time: %1", info.signingTime().toString( Qt::DefaultLocaleLongDate ) ); auto childItem3 = new SignatureItem( parentItem, nullptr, SignatureItem::Reason, currentPage ); childItem3->displayString = i18n("Reason: %1", !info.reason().isEmpty() ? info.reason() : i18n("Not Available") ); auto childItem4 = new SignatureItem( parentItem, sf, SignatureItem::FieldInfo, currentPage ); childItem4->displayString = i18n("Field: %1 on page %2", sf->name(), currentPage+1 ); } } q->endResetModel(); } QModelIndex SignatureModelPrivate::indexForItem( SignatureItem *item ) const { if ( item->parent ) { const int index = item->parent->children.indexOf( item ); if ( index >= 0 && index < item->parent->children.count() ) return q->createIndex( index, 0, item ); } return QModelIndex(); } SignatureModel::SignatureModel( Okular::Document *doc, QObject *parent ) : QAbstractItemModel( parent ), d_ptr( new SignatureModelPrivate( this ) ) { Q_D( SignatureModel ); d->document = doc; d->document->addObserver( d ); } SignatureModel::~SignatureModel() { Q_D( SignatureModel ); d->document->removeObserver( d ); } int SignatureModel::columnCount( const QModelIndex & ) const { return 1; } QVariant SignatureModel::data( const QModelIndex &index, int role ) const { Q_D( const SignatureModel ); if ( !index.isValid() ) return QVariant(); const SignatureItem *item = static_cast( index.internalPointer() ); if ( item == d->root ) return QVariant(); switch ( role ) { case Qt::DisplayRole: case Qt::ToolTipRole: return item->displayString; case Qt::DecorationRole: if ( item->type == SignatureItem::RevisionInfo ) { const Okular::SignatureInfo::SignatureStatus signatureStatus = item->form->signatureInfo().signatureStatus(); switch ( signatureStatus ) { case Okular::SignatureInfo::SignatureValid: return QIcon::fromTheme( QStringLiteral("dialog-ok") ); case Okular::SignatureInfo::SignatureInvalid: return QIcon::fromTheme( QStringLiteral("dialog-close") ); case Okular::SignatureInfo::SignatureDigestMismatch: return QIcon::fromTheme( QStringLiteral("dialog-warning") ); default: return QIcon::fromTheme( QStringLiteral("dialog-question") ); } } return QIcon(); case FormRole: return QVariant::fromValue(item->form); case PageRole: return item->page; } return QVariant(); } bool SignatureModel::hasChildren( const QModelIndex &parent ) const { if ( !parent.isValid() ) return true; const SignatureItem *item = static_cast( parent.internalPointer() ); return !item->children.isEmpty(); } QModelIndex SignatureModel::index( int row, int column, const QModelIndex &parent ) const { Q_D( const SignatureModel ); if ( row < 0 || column != 0 ) return QModelIndex(); const SignatureItem *item = parent.isValid() ? static_cast( parent.internalPointer() ) : d->root; if ( row < item->children.count() ) return createIndex( row, column, item->children.at( row ) ); return QModelIndex(); } QModelIndex SignatureModel::parent( const QModelIndex &index ) const { Q_D( const SignatureModel ); if ( !index.isValid() ) return QModelIndex(); const SignatureItem *item = static_cast( index.internalPointer() ); return d->indexForItem( item->parent ); } int SignatureModel::rowCount( const QModelIndex &parent ) const { Q_D( const SignatureModel ); const SignatureItem *item = parent.isValid() ? static_cast( parent.internalPointer() ) : d->root; return item->children.count(); } #include "moc_signaturemodel.cpp" diff --git a/ui/toggleactionmenu.cpp b/ui/toggleactionmenu.cpp index 1e7eed39e..674f5d9fb 100644 --- a/ui/toggleactionmenu.cpp +++ b/ui/toggleactionmenu.cpp @@ -1,152 +1,153 @@ /*************************************************************************** * Copyright (C) 2019 by David Hurka * * * * Inspired by and replacing toolaction.h by: * * Copyright (C) 2004-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 "toggleactionmenu.h" #include #include ToggleActionMenu::ToggleActionMenu( QObject * parent ) : ToggleActionMenu( QIcon(), QString(), parent ) { } ToggleActionMenu::ToggleActionMenu( const QString &text, QObject * parent ) : ToggleActionMenu( QIcon(), text, parent ) { } ToggleActionMenu::ToggleActionMenu( const QIcon &icon, const QString &text, QObject * parent, PopupMode popupMode, MenuLogic logic ) : KActionMenu( icon, text, parent ), m_defaultAction( nullptr ), m_suggestedDefaultAction( nullptr ), m_menuLogic( logic ) { connect( this, &QAction::changed, this, &ToggleActionMenu::updateButtons ); if ( popupMode == DelayedPopup ) { setDelayed( true ); } else { setDelayed( false ); } setStickyMenu( false ); if ( logic & ImplicitDefaultAction ) { connect( menu(), &QMenu::triggered, this, &ToggleActionMenu::setDefaultAction ); } } QWidget * ToggleActionMenu::createWidget( QWidget * parent ) { QToolButton * button = qobject_cast< QToolButton * >( KActionMenu::createWidget( parent ) ); if ( !button ) { // This function is used to add a button into the toolbar. // KActionMenu will plug itself as QToolButton. // So, if no QToolButton was returned, this was not called the intended way. return button; } // Remove this menu action from the button, // so it doesn't compose a menu of this menu action and its own menu. button->removeAction( this ); // The button has lost the menu now, let it use the correct menu. button->setMenu( menu() ); m_buttons.append( QPointer< QToolButton >( button ) ); // Apply other properties to the button. updateButtons(); return button; } void ToggleActionMenu::setDefaultAction( QAction *action ) { m_defaultAction = action; updateButtons(); } void ToggleActionMenu::suggestDefaultAction( QAction *action ) { m_suggestedDefaultAction = action; } QAction * ToggleActionMenu::checkedAction( QMenu *menu ) const { // Look at each action a in the menu whether it is checked. // If a is a menu, recursively call checkedAction(). - for ( QAction * a : menu->actions() ) + const QList actions = menu->actions(); + for ( QAction * a : actions ) { if ( a->isChecked() ) { return a; } else if ( a->menu() ) { QAction * b = checkedAction( a->menu() ); if ( b ) { return b; } } } return nullptr; } void ToggleActionMenu::updateButtons() { for ( const QPointer< QToolButton > &button : qAsConst( m_buttons ) ) { if ( button ) { button->setDefaultAction( defaultAction() ); // Override some properties of the default action, // where the property of this menu makes more sense. button->setEnabled( isEnabled() ); if ( delayed() ) { button->setPopupMode( QToolButton::DelayedPopup ); } else if ( stickyMenu() ) { button->setPopupMode( QToolButton::InstantPopup ); } else { button->setPopupMode( QToolButton::MenuButtonPopup ); } } } } QAction * ToggleActionMenu::defaultAction() { if ( ( m_menuLogic & ImplicitDefaultAction ) && !m_defaultAction ) { m_defaultAction = checkedAction( menu() ); } if ( !m_defaultAction ) { m_defaultAction = m_suggestedDefaultAction; } return m_defaultAction; }